summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2020-09-02 11:25:03 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2020-09-02 11:25:03 +0300
commit1e548eac8c63922315f89c96436b622615c488c7 (patch)
treed2c36fdaaa0bcc42cbc34bb804de5a7201dba2b1 /src
parent8ed2faadc4ca2497fc3b2e9d2eae6d40921de918 (diff)
parente2b5ea14d25dbbb62a1e827803e67c30df79c6a1 (diff)
Merge branch 'master' of skyjake.fi:skyjake/lagrange
Diffstat (limited to 'src')
-rw-r--r--src/app.c296
-rw-r--r--src/app.h35
-rw-r--r--src/bookmarks.c28
-rw-r--r--src/bookmarks.h24
-rw-r--r--src/gmcerts.c359
-rw-r--r--src/gmcerts.h82
-rw-r--r--src/gmdocument.c160
-rw-r--r--src/gmdocument.h24
-rw-r--r--src/gmrequest.c152
-rw-r--r--src/gmrequest.h22
-rw-r--r--src/gmutil.c103
-rw-r--r--src/gmutil.h28
-rw-r--r--src/history.c22
-rw-r--r--src/history.h22
-rw-r--r--src/macos.h31
-rw-r--r--src/macos.m (renamed from src/ui/macos.m)72
-rw-r--r--src/main.c22
-rw-r--r--src/stb_image.h214
-rw-r--r--src/stb_truetype.h4
-rw-r--r--src/ui/color.c53
-rw-r--r--src/ui/color.h63
-rw-r--r--src/ui/command.c22
-rw-r--r--src/ui/command.h22
-rw-r--r--src/ui/documentwidget.c470
-rw-r--r--src/ui/documentwidget.h23
-rw-r--r--src/ui/inputwidget.c452
-rw-r--r--src/ui/inputwidget.h30
-rw-r--r--src/ui/labelwidget.c39
-rw-r--r--src/ui/labelwidget.h25
-rw-r--r--src/ui/macos.h9
-rw-r--r--src/ui/metrics.c22
-rw-r--r--src/ui/metrics.h22
-rw-r--r--src/ui/paint.c22
-rw-r--r--src/ui/paint.h22
-rw-r--r--src/ui/scrollwidget.c23
-rw-r--r--src/ui/scrollwidget.h22
-rw-r--r--src/ui/sidebarwidget.c556
-rw-r--r--src/ui/sidebarwidget.h22
-rw-r--r--src/ui/text.c150
-rw-r--r--src/ui/text.h28
-rw-r--r--src/ui/util.c194
-rw-r--r--src/ui/util.h32
-rw-r--r--src/ui/widget.c81
-rw-r--r--src/ui/widget.h28
-rw-r--r--src/ui/window.c133
-rw-r--r--src/ui/window.h25
-rw-r--r--src/visited.c57
-rw-r--r--src/visited.h27
-rw-r--r--src/win32.c22
-rw-r--r--src/win32.h22
50 files changed, 3806 insertions, 612 deletions
diff --git a/src/app.c b/src/app.c
index 670adee2..dc61c103 100644
--- a/src/app.c
+++ b/src/app.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "app.h" 23#include "app.h"
2#include "bookmarks.h" 24#include "bookmarks.h"
3#include "embedded.h" 25#include "embedded.h"
@@ -25,6 +47,7 @@
25#include <the_Foundation/time.h> 47#include <the_Foundation/time.h>
26#include <SDL_events.h> 48#include <SDL_events.h>
27#include <SDL_render.h> 49#include <SDL_render.h>
50#include <SDL_timer.h>
28#include <SDL_video.h> 51#include <SDL_video.h>
29 52
30#include <stdio.h> 53#include <stdio.h>
@@ -32,26 +55,26 @@
32#include <errno.h> 55#include <errno.h>
33 56
34#if defined (iPlatformApple) && !defined (iPlatformIOS) 57#if defined (iPlatformApple) && !defined (iPlatformIOS)
35# include "ui/macos.h" 58# include "macos.h"
36#endif 59#endif
37 60
38iDeclareType(App) 61iDeclareType(App)
39 62
40#if defined (iPlatformApple) 63#if defined (iPlatformApple)
41#define EMB_BIN "../../Resources/resources.bin" 64#define EMB_BIN "../../Resources/resources.binary"
42static const char *dataDir_App_ = "~/Library/Application Support/fi.skyjake.Lagrange"; 65static const char *dataDir_App_ = "~/Library/Application Support/fi.skyjake.Lagrange";
43#endif 66#endif
44#if defined (iPlatformMsys) 67#if defined (iPlatformMsys)
45#define EMB_BIN "../resources.bin" 68#define EMB_BIN "../resources.binary"
46static const char *dataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange"; 69static const char *dataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange";
47#endif 70#endif
48#if defined (iPlatformLinux) 71#if defined (iPlatformLinux)
49#define EMB_BIN "../../share/lagrange/resources.bin" 72#define EMB_BIN "../../share/lagrange/resources.binary"
50#define EMB_BIN2 "../resources.bin" /* try from build dir as well */
51static const char *dataDir_App_ = "~/.config/lagrange"; 73static const char *dataDir_App_ = "~/.config/lagrange";
52#endif 74#endif
75#define EMB_BIN2 "../resources.binary" /* fallback from build/executable dir */
53static const char *prefsFileName_App_ = "prefs.cfg"; 76static const char *prefsFileName_App_ = "prefs.cfg";
54static const char *stateFileName_App_ = "state.bin"; 77static const char *stateFileName_App_ = "state.binary";
55 78
56struct Impl_App { 79struct Impl_App {
57 iCommandLine args; 80 iCommandLine args;
@@ -60,6 +83,8 @@ struct Impl_App {
60 iBookmarks * bookmarks; 83 iBookmarks * bookmarks;
61 iWindow * window; 84 iWindow * window;
62 iSortedArray tickers; 85 iSortedArray tickers;
86 uint32_t lastTickerTime;
87 uint32_t elapsedSinceLastTicker;
63 iBool running; 88 iBool running;
64 iBool pendingRefresh; 89 iBool pendingRefresh;
65 int tabEnum; 90 int tabEnum;
@@ -70,6 +95,9 @@ struct Impl_App {
70 float uiScale; 95 float uiScale;
71 int zoomPercent; 96 int zoomPercent;
72 enum iColorTheme theme; 97 enum iColorTheme theme;
98 iBool useSystemTheme;
99 iString gopherProxy;
100 iString httpProxy;
73}; 101};
74 102
75static iApp app_; 103static iApp app_;
@@ -99,21 +127,32 @@ const iString *dateStr_(const iDate *date) {
99static iString *serializePrefs_App_(const iApp *d) { 127static iString *serializePrefs_App_(const iApp *d) {
100 iString *str = new_String(); 128 iString *str = new_String();
101 const iSidebarWidget *sidebar = findWidget_App("sidebar"); 129 const iSidebarWidget *sidebar = findWidget_App("sidebar");
130 appendFormat_String(str, "window.retain arg:%d\n", d->retainWindowSize);
102 if (d->retainWindowSize) { 131 if (d->retainWindowSize) {
103 int w, h, x, y; 132 int w, h, x, y;
104 SDL_GetWindowSize(d->window->win, &w, &h); 133 SDL_GetWindowSize(d->window->win, &w, &h);
105 SDL_GetWindowPosition(d->window->win, &x, &y); 134 SDL_GetWindowPosition(d->window->win, &x, &y);
135#if defined (iPlatformLinux)
136 /* Workaround for window position being unaffected by decorations on creation. */ {
137 int bl, bt;
138 SDL_GetWindowBordersSize(d->window->win, &bt, &bl, NULL, NULL);
139 x -= bl;
140 y -= bt;
141 }
142#endif
106 appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y); 143 appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y);
107 appendFormat_String(str, "sidebar.width arg:%d\n", width_SidebarWidget(sidebar)); 144 appendFormat_String(str, "sidebar.width arg:%d\n", width_SidebarWidget(sidebar));
108 } 145 }
109 appendFormat_String(str, "retainwindow arg:%d\n", d->retainWindowSize);
110 if (isVisible_Widget(constAs_Widget(sidebar))) { 146 if (isVisible_Widget(constAs_Widget(sidebar))) {
111 appendCStr_String(str, "sidebar.toggle\n"); 147 appendCStr_String(str, "sidebar.toggle\n");
112 } 148 }
113 appendFormat_String(str, "sidebar.mode arg:%d\n", mode_SidebarWidget(sidebar)); 149 appendFormat_String(str, "sidebar.mode arg:%d\n", mode_SidebarWidget(sidebar));
114 appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window)); 150 appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window));
115 appendFormat_String(str, "zoom.set arg:%d\n", d->zoomPercent); 151 appendFormat_String(str, "zoom.set arg:%d\n", d->zoomPercent);
116 appendFormat_String(str, "theme.set arg:%d\n", d->theme); 152 appendFormat_String(str, "theme.set arg:%d auto:1\n", d->theme);
153 appendFormat_String(str, "ostheme arg:%d\n", d->useSystemTheme);
154 appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->gopherProxy));
155 appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->httpProxy));
117 return str; 156 return str;
118} 157}
119 158
@@ -130,7 +169,7 @@ static void loadPrefs_App_(iApp *d) {
130 iString *str = readString_File(f); 169 iString *str = readString_File(f);
131 const iRangecc src = range_String(str); 170 const iRangecc src = range_String(str);
132 iRangecc line = iNullRange; 171 iRangecc line = iNullRange;
133 while (nextSplit_Rangecc(&src, "\n", &line)) { 172 while (nextSplit_Rangecc(src, "\n", &line)) {
134 iString cmdStr; 173 iString cmdStr;
135 initRange_String(&cmdStr, line); 174 initRange_String(&cmdStr, line);
136 const char *cmd = cstr_String(&cmdStr); 175 const char *cmd = cstr_String(&cmdStr);
@@ -231,9 +270,12 @@ static void saveState_App_(const iApp *d) {
231static void init_App_(iApp *d, int argc, char **argv) { 270static void init_App_(iApp *d, int argc, char **argv) {
232 init_CommandLine(&d->args, argc, argv); 271 init_CommandLine(&d->args, argc, argv);
233 init_SortedArray(&d->tickers, sizeof(iTicker), cmp_Ticker_); 272 init_SortedArray(&d->tickers, sizeof(iTicker), cmp_Ticker_);
273 d->lastTickerTime = SDL_GetTicks();
274 d->elapsedSinceLastTicker = 0;
234 d->commandEcho = checkArgument_CommandLine(&d->args, "echo") != NULL; 275 d->commandEcho = checkArgument_CommandLine(&d->args, "echo") != NULL;
235 d->initialWindowRect = init_Rect(-1, -1, 800, 500); 276 d->initialWindowRect = init_Rect(-1, -1, 800, 500);
236 d->theme = dark_ColorTheme; 277 d->theme = dark_ColorTheme;
278 d->useSystemTheme = iTrue;
237 d->running = iFalse; 279 d->running = iFalse;
238 d->window = NULL; 280 d->window = NULL;
239 d->retainWindowSize = iTrue; 281 d->retainWindowSize = iTrue;
@@ -243,15 +285,17 @@ static void init_App_(iApp *d, int argc, char **argv) {
243 d->visited = new_Visited(); 285 d->visited = new_Visited();
244 d->bookmarks = new_Bookmarks(); 286 d->bookmarks = new_Bookmarks();
245 d->tabEnum = 0; /* generates unique IDs for tab pages */ 287 d->tabEnum = 0; /* generates unique IDs for tab pages */
288 init_String(&d->gopherProxy);
289 init_String(&d->httpProxy);
246 setThemePalette_Color(d->theme); 290 setThemePalette_Color(d->theme);
247 loadPrefs_App_(d); 291 loadPrefs_App_(d);
248 load_Visited(d->visited, dataDir_App_); 292 load_Visited(d->visited, dataDir_App_);
249 load_Bookmarks(d->bookmarks, dataDir_App_); 293 load_Bookmarks(d->bookmarks, dataDir_App_);
250#if defined (iHaveLoadEmbed) 294#if defined (iHaveLoadEmbed)
251 /* Load the resources from a file. */ { 295 /* Load the resources from a file. */ {
252 if (!load_Embed(concatPath_CStr(cstr_String(execPath_App()), "../resources.bin"))) { 296 if (!load_Embed(concatPath_CStr(cstr_String(execPath_App()), EMB_BIN))) {
253 if (!load_Embed(concatPath_CStr(cstr_String(execPath_App()), EMB_BIN))) { 297 if (!load_Embed(concatPath_CStr(cstr_String(execPath_App()), EMB_BIN2))) {
254 fprintf(stderr, "failed to load resources.bin: %s\n", strerror(errno)); 298 fprintf(stderr, "failed to load resources: %s\n", strerror(errno));
255 exit(-1); 299 exit(-1);
256 } 300 }
257 } 301 }
@@ -269,6 +313,8 @@ static void init_App_(iApp *d, int argc, char **argv) {
269static void deinit_App(iApp *d) { 313static void deinit_App(iApp *d) {
270 saveState_App_(d); 314 saveState_App_(d);
271 savePrefs_App_(d); 315 savePrefs_App_(d);
316 deinit_String(&d->httpProxy);
317 deinit_String(&d->gopherProxy);
272 save_Bookmarks(d->bookmarks, dataDir_App_); 318 save_Bookmarks(d->bookmarks, dataDir_App_);
273 delete_Bookmarks(d->bookmarks); 319 delete_Bookmarks(d->bookmarks);
274 save_Visited(d->visited, dataDir_App_); 320 save_Visited(d->visited, dataDir_App_);
@@ -288,12 +334,17 @@ const iString *dataDir_App(void) {
288 return collect_String(cleanedCStr_Path(dataDir_App_)); 334 return collect_String(cleanedCStr_Path(dataDir_App_));
289} 335}
290 336
337iLocalDef iBool isWaitingAllowed_App_(const iApp *d) {
338 return !d->pendingRefresh && isEmpty_SortedArray(&d->tickers);
339}
340
291void processEvents_App(enum iAppEventMode eventMode) { 341void processEvents_App(enum iAppEventMode eventMode) {
292 iApp *d = &app_; 342 iApp *d = &app_;
293 SDL_Event ev; 343 SDL_Event ev;
294 while ( 344 while ((isWaitingAllowed_App_(d) && eventMode == waitForNewEvents_AppEventMode &&
295 (!d->pendingRefresh && eventMode == waitForNewEvents_AppEventMode && SDL_WaitEvent(&ev)) || 345 SDL_WaitEvent(&ev)) ||
296 ((d->pendingRefresh || eventMode == postedEventsOnly_AppEventMode) && SDL_PollEvent(&ev))) { 346 ((!isWaitingAllowed_App_(d) || eventMode == postedEventsOnly_AppEventMode) &&
347 SDL_PollEvent(&ev))) {
297 switch (ev.type) { 348 switch (ev.type) {
298 case SDL_QUIT: 349 case SDL_QUIT:
299 d->running = iFalse; 350 d->running = iFalse;
@@ -325,12 +376,17 @@ backToMainLoop:;
325} 376}
326 377
327static void runTickers_App_(iApp *d) { 378static void runTickers_App_(iApp *d) {
379 const uint32_t now = SDL_GetTicks();
380 d->elapsedSinceLastTicker = (d->lastTickerTime ? now - d->lastTickerTime : 0);
381 d->lastTickerTime = now;
382 if (isEmpty_SortedArray(&d->tickers)) {
383 d->lastTickerTime = 0;
384 return;
385 }
328 /* Tickers may add themselves again, so we'll run off a copy. */ 386 /* Tickers may add themselves again, so we'll run off a copy. */
329 iSortedArray *pending = copy_SortedArray(&d->tickers); 387 iSortedArray *pending = copy_SortedArray(&d->tickers);
330 clear_SortedArray(&d->tickers); 388 clear_SortedArray(&d->tickers);
331 if (!isEmpty_SortedArray(pending)) { 389 postRefresh_App();
332 postRefresh_App();
333 }
334 iConstForEach(Array, i, &pending->values) { 390 iConstForEach(Array, i, &pending->values) {
335 const iTicker *ticker = i.value; 391 const iTicker *ticker = i.value;
336 if (ticker->callback) { 392 if (ticker->callback) {
@@ -338,6 +394,9 @@ static void runTickers_App_(iApp *d) {
338 } 394 }
339 } 395 }
340 delete_SortedArray(pending); 396 delete_SortedArray(pending);
397 if (isEmpty_SortedArray(&d->tickers)) {
398 d->lastTickerTime = 0;
399 }
341} 400}
342 401
343static int run_App_(iApp *d) { 402static int run_App_(iApp *d) {
@@ -345,8 +404,8 @@ static int run_App_(iApp *d) {
345 d->running = iTrue; 404 d->running = iTrue;
346 SDL_EventState(SDL_DROPFILE, SDL_ENABLE); /* open files via drag'n'drop */ 405 SDL_EventState(SDL_DROPFILE, SDL_ENABLE); /* open files via drag'n'drop */
347 while (d->running) { 406 while (d->running) {
348 runTickers_App_(d);
349 processEvents_App(waitForNewEvents_AppEventMode); 407 processEvents_App(waitForNewEvents_AppEventMode);
408 runTickers_App_(d);
350 refresh_App(); 409 refresh_App();
351 recycle_Garbage(); 410 recycle_Garbage();
352 } 411 }
@@ -364,6 +423,10 @@ iBool isRefreshPending_App(void) {
364 return app_.pendingRefresh; 423 return app_.pendingRefresh;
365} 424}
366 425
426uint32_t elapsedSinceLastTicker_App(void) {
427 return app_.elapsedSinceLastTicker;
428}
429
367int zoom_App(void) { 430int zoom_App(void) {
368 return app_.zoomPercent; 431 return app_.zoomPercent;
369} 432}
@@ -372,6 +435,17 @@ enum iColorTheme colorTheme_App(void) {
372 return app_.theme; 435 return app_.theme;
373} 436}
374 437
438const iString *schemeProxy_App(iRangecc scheme) {
439 iApp *d = &app_;
440 if (equalCase_Rangecc(scheme, "gopher")) {
441 return &d->gopherProxy;
442 }
443 if (equalCase_Rangecc(scheme, "http") || equalCase_Rangecc(scheme, "https")) {
444 return &d->httpProxy;
445 }
446 return NULL;
447}
448
375int run_App(int argc, char **argv) { 449int run_App(int argc, char **argv) {
376 init_App_(&app_, argc, argv); 450 init_App_(&app_, argc, argv);
377 const int rc = run_App_(&app_); 451 const int rc = run_App_(&app_);
@@ -394,7 +468,12 @@ void postRefresh_App(void) {
394} 468}
395 469
396void postCommand_App(const char *command) { 470void postCommand_App(const char *command) {
471 iAssert(command);
397 SDL_Event ev; 472 SDL_Event ev;
473 if (*command == '!') {
474 /* Global command; this is global context so just ignore. */
475 command++;
476 }
398 ev.user.type = SDL_USEREVENT; 477 ev.user.type = SDL_USEREVENT;
399 ev.user.code = command_UserEventCode; 478 ev.user.code = command_UserEventCode;
400 ev.user.windowID = get_Window() ? SDL_GetWindowID(get_Window()->win) : 0; 479 ev.user.windowID = get_Window() ? SDL_GetWindowID(get_Window()->win) : 0;
@@ -424,6 +503,7 @@ iAny *findWidget_App(const char *id) {
424void addTicker_App(void (*ticker)(iAny *), iAny *context) { 503void addTicker_App(void (*ticker)(iAny *), iAny *context) {
425 iApp *d = &app_; 504 iApp *d = &app_;
426 insert_SortedArray(&d->tickers, &(iTicker){ context, ticker }); 505 insert_SortedArray(&d->tickers, &(iTicker){ context, ticker });
506 postRefresh_App();
427} 507}
428 508
429iGmCerts *certs_App(void) { 509iGmCerts *certs_App(void) {
@@ -450,13 +530,25 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
450 if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) { 530 if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) {
451 setUiScale_Window(get_Window(), 531 setUiScale_Window(get_Window(),
452 toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale")))); 532 toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale"))));
453 postCommandf_App("retainwindow arg:%d", 533 postCommandf_App("window.retain arg:%d",
454 isSelected_Widget(findChild_Widget(d, "prefs.retainwindow"))); 534 isSelected_Widget(findChild_Widget(d, "prefs.retainwindow")));
535 postCommandf_App("ostheme arg:%d",
536 isSelected_Widget(findChild_Widget(d, "prefs.ostheme")));
537 postCommandf_App("proxy.http address:%s",
538 cstr_String(text_InputWidget(findChild_Widget(d, "prefs.proxy.http"))));
539 postCommandf_App("proxy.gopher address:%s",
540 cstr_String(text_InputWidget(findChild_Widget(d, "prefs.proxy.gopher"))));
455 destroy_Widget(d); 541 destroy_Widget(d);
456 return iTrue; 542 return iTrue;
457 } 543 }
458 else if (equal_Command(cmd, "theme.changed")) { 544 else if (equal_Command(cmd, "prefs.ostheme.changed")) {
545 postCommandf_App("ostheme arg:%d", arg_Command(cmd));
546 }
547 else if (equal_Command(cmd, "theme.changed")) {
459 updatePrefsThemeButtons_(d); 548 updatePrefsThemeButtons_(d);
549 if (!argLabel_Command(cmd, "auto")) {
550 setToggle_Widget(findChild_Widget(d, "prefs.ostheme"), iFalse);
551 }
460 } 552 }
461 return iFalse; 553 return iFalse;
462} 554}
@@ -502,18 +594,81 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf) {
502 return doc; 594 return doc;
503} 595}
504 596
597static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
598 iApp *d = &app_;
599 if (equal_Command(cmd, "ident.accept") || equal_Command(cmd, "cancel")) {
600 if (equal_Command(cmd, "ident.accept")) {
601 const iString *commonName = text_InputWidget (findChild_Widget(dlg, "ident.common"));
602 const iString *email = text_InputWidget (findChild_Widget(dlg, "ident.email"));
603 const iString *userId = text_InputWidget (findChild_Widget(dlg, "ident.userid"));
604 const iString *domain = text_InputWidget (findChild_Widget(dlg, "ident.domain"));
605 const iString *organization = text_InputWidget (findChild_Widget(dlg, "ident.org"));
606 const iString *country = text_InputWidget (findChild_Widget(dlg, "ident.country"));
607 const iBool isTemp = isSelected_Widget(findChild_Widget(dlg, "ident.temp"));
608 if (isEmpty_String(commonName)) {
609 makeMessage_Widget(orange_ColorEscape "MISSING INFO",
610 "A \"Common name\" must be specified.");
611 return iTrue;
612 }
613 iDate until;
614 /* Validate the date. */ {
615 iZap(until);
616 unsigned int val[6];
617 iDate today;
618 initCurrent_Date(&today);
619 const int n =
620 sscanf(cstr_String(text_InputWidget(findChild_Widget(dlg, "ident.until"))),
621 "%04u-%u-%u %u:%u:%u",
622 &val[0], &val[1], &val[2], &val[3], &val[4], &val[5]);
623 if (n <= 0 || val[0] < (unsigned) today.year) {
624 makeMessage_Widget(orange_ColorEscape "INVALID DATE",
625 "Please check the \"Valid until\" date. Examples:\n"
626 "\u2022 2030\n"
627 "\u2022 2025-06-30\n"
628 "\u2022 2021-12-31 23:59:59");
629 return iTrue;
630 }
631 until.year = val[0];
632 until.month = n >= 2 ? val[1] : 1;
633 until.day = n >= 3 ? val[2] : 1;
634 until.hour = n >= 4 ? val[3] : 0;
635 until.minute = n >= 5 ? val[4] : 0;
636 until.second = n == 6 ? val[5] : 0;
637 /* In the past? */ {
638 iTime now, t;
639 initCurrent_Time(&now);
640 init_Time(&t, &until);
641 if (cmp_Time(&t, &now) <= 0) {
642 makeMessage_Widget(orange_ColorEscape "INVALID DATE",
643 "Expiration date must be in the future.");
644 return iTrue;
645 }
646 }
647 }
648 /* The input seems fine. */
649 newIdentity_GmCerts(d->certs, isTemp ? temporary_GmIdentityFlag : 0,
650 until, commonName, email, userId, domain, organization, country);
651 postCommandf_App("sidebar.mode arg:%d show:1", identities_SidebarMode);
652 postCommand_App("idents.changed");
653 }
654 destroy_Widget(dlg);
655 return iTrue;
656 }
657 return iFalse;
658}
659
505iBool handleCommand_App(const char *cmd) { 660iBool handleCommand_App(const char *cmd) {
506 iApp *d = &app_; 661 iApp *d = &app_;
507 if (equal_Command(cmd, "retainwindow")) { 662 if (equal_Command(cmd, "window.retain")) {
508 d->retainWindowSize = arg_Command(cmd); 663 d->retainWindowSize = arg_Command(cmd);
509 return iTrue; 664 return iTrue;
510 } 665 }
511 else if (equal_Command(cmd, "open")) { 666 else if (equal_Command(cmd, "open")) {
512 const iString *url = collect_String(newCStr_String(suffixPtr_Command(cmd, "url"))); 667 const iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url"));
513 iUrl parts; 668 iUrl parts;
514 init_Url(&parts, url); 669 init_Url(&parts, url);
515 if (equalCase_Rangecc(&parts.protocol, "http") || 670 if (isEmpty_String(&d->httpProxy) &&
516 equalCase_Rangecc(&parts.protocol, "https")) { 671 (equalCase_Rangecc(parts.scheme, "http") || equalCase_Rangecc(parts.scheme, "https"))) {
517 openInDefaultBrowser_App(url); 672 openInDefaultBrowser_App(url);
518 return iTrue; 673 return iTrue;
519 } 674 }
@@ -523,8 +678,9 @@ iBool handleCommand_App(const char *cmd) {
523 } 678 }
524 iHistory *history = history_DocumentWidget(doc); 679 iHistory *history = history_DocumentWidget(doc);
525 const iBool isHistory = argLabel_Command(cmd, "history") != 0; 680 const iBool isHistory = argLabel_Command(cmd, "history") != 0;
681 int redirectCount = argLabel_Command(cmd, "redirect");
526 if (!isHistory) { 682 if (!isHistory) {
527 if (argLabel_Command(cmd, "redirect")) { 683 if (redirectCount) {
528 replace_History(history, url); 684 replace_History(history, url);
529 } 685 }
530 else { 686 else {
@@ -533,6 +689,7 @@ iBool handleCommand_App(const char *cmd) {
533 } 689 }
534 visitUrl_Visited(d->visited, url); 690 visitUrl_Visited(d->visited, url);
535 setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll")); 691 setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll"));
692 setRedirectCount_DocumentWidget(doc, redirectCount);
536 setUrlFromCache_DocumentWidget(doc, url, isHistory); 693 setUrlFromCache_DocumentWidget(doc, url, isHistory);
537 } 694 }
538 else if (equal_Command(cmd, "document.request.cancelled")) { 695 else if (equal_Command(cmd, "document.request.cancelled")) {
@@ -601,9 +758,14 @@ iBool handleCommand_App(const char *cmd) {
601 else if (equal_Command(cmd, "preferences")) { 758 else if (equal_Command(cmd, "preferences")) {
602 iWidget *dlg = makePreferences_Widget(); 759 iWidget *dlg = makePreferences_Widget();
603 updatePrefsThemeButtons_(dlg); 760 updatePrefsThemeButtons_(dlg);
761 setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->useSystemTheme);
604 setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->retainWindowSize); 762 setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->retainWindowSize);
605 setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), 763 setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"),
606 collectNewFormat_String("%g", uiScale_Window(d->window))); 764 collectNewFormat_String("%g", uiScale_Window(d->window)));
765 setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.http"),
766 schemeProxy_App(range_CStr("http")));
767 setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.gopher"),
768 schemeProxy_App(range_CStr("gopher")));
607 setCommandHandler_Widget(dlg, handlePrefsCommands_); 769 setCommandHandler_Widget(dlg, handlePrefsCommands_);
608 } 770 }
609 else if (equal_Command(cmd, "navigate.home")) { 771 else if (equal_Command(cmd, "navigate.home")) {
@@ -633,17 +795,46 @@ iBool handleCommand_App(const char *cmd) {
633 } 795 }
634 else if (equal_Command(cmd, "bookmark.add")) { 796 else if (equal_Command(cmd, "bookmark.add")) {
635 iDocumentWidget *doc = document_App(); 797 iDocumentWidget *doc = document_App();
636 add_Bookmarks(d->bookmarks, url_DocumentWidget(doc), 798 makeBookmarkCreation_Widget(url_DocumentWidget(doc),
637 bookmarkTitle_DocumentWidget(doc), 799 bookmarkTitle_DocumentWidget(doc),
638 collectNew_String(), 800 siteIcon_GmDocument(document_DocumentWidget(doc)));
639 siteIcon_GmDocument(document_DocumentWidget(doc))); 801 return iTrue;
640 postCommand_App("bookmarks.changed"); 802 }
803 else if (equal_Command(cmd, "ident.new")) {
804 iWidget *dlg = makeIdentityCreation_Widget();
805 setCommandHandler_Widget(dlg, handleIdentityCreationCommands_);
641 return iTrue; 806 return iTrue;
642 } 807 }
643 else if (equal_Command(cmd, "theme.set")) { 808 else if (equal_Command(cmd, "theme.set")) {
809 const int isAuto = argLabel_Command(cmd, "auto");
644 d->theme = arg_Command(cmd); 810 d->theme = arg_Command(cmd);
811 if (!isAuto) {
812 postCommand_App("ostheme arg:0");
813 }
645 setThemePalette_Color(d->theme); 814 setThemePalette_Color(d->theme);
646 postCommand_App("theme.changed"); 815 postCommandf_App("theme.changed auto:%d", isAuto);
816 return iTrue;
817 }
818 else if (equal_Command(cmd, "ostheme")) {
819 d->useSystemTheme = arg_Command(cmd);
820 return iTrue;
821 }
822 else if (equal_Command(cmd, "os.theme.changed")) {
823 if (d->useSystemTheme) {
824 const int dark = argLabel_Command(cmd, "dark");
825 const int contrast = argLabel_Command(cmd, "contrast");
826 postCommandf_App("theme.set arg:%d auto:1",
827 dark ? (contrast ? pureBlack_ColorTheme : dark_ColorTheme)
828 : (contrast ? pureWhite_ColorTheme : light_ColorTheme));
829 }
830 return iFalse;
831 }
832 else if (equal_Command(cmd, "proxy.gopher")) {
833 setCStr_String(&d->gopherProxy, suffixPtr_Command(cmd, "address"));
834 return iTrue;
835 }
836 else if (equal_Command(cmd, "proxy.http")) {
837 setCStr_String(&d->httpProxy, suffixPtr_Command(cmd, "address"));
647 return iTrue; 838 return iTrue;
648 } 839 }
649 else { 840 else {
@@ -666,3 +857,44 @@ void openInDefaultBrowser_App(const iString *url) {
666 start_Process(proc); 857 start_Process(proc);
667 iRelease(proc); 858 iRelease(proc);
668} 859}
860
861void revealPath_App(const iString *path) {
862#if defined (iPlatformApple)
863 const char *scriptPath = concatPath_CStr(dataDir_App_, "revealfile.scpt");
864 iFile *f = newCStr_File(scriptPath);
865 if (open_File(f, writeOnly_FileMode | text_FileMode)) {
866 /* AppleScript to select a specific file. */
867 write_File(f, collect_Block(newCStr_Block("on run argv\n"
868 " tell application \"Finder\"\n"
869 " activate\n"
870 " reveal POSIX file (item 1 of argv) as text\n"
871 " end tell\n"
872 "end run\n")));
873 close_File(f);
874 iProcess *proc = new_Process();
875 setArguments_Process(
876 proc,
877 iClob(newStringsCStr_StringList(
878 "/usr/bin/osascript", scriptPath, cstr_String(path), NULL)));
879 start_Process(proc);
880 iRelease(proc);
881 }
882 iRelease(f);
883#elif defined (iPlatformLinux)
884 iFileInfo *inf = iClob(new_FileInfo(path));
885 iRangecc target;
886 if (isDirectory_FileInfo(inf)) {
887 target = range_String(path);
888 }
889 else {
890 target = dirName_Path(path);
891 }
892 iProcess *proc = new_Process();
893 setArguments_Process(
894 proc, iClob(newStringsCStr_StringList("/usr/bin/xdg-open", cstr_Rangecc(target), NULL)));
895 start_Process(proc);
896 iRelease(proc);
897#else
898 iAssert(0 /* File revealing not implemented on this platform */);
899#endif
900}
diff --git a/src/app.h b/src/app.h
index 11c8c4a3..3d4808bc 100644
--- a/src/app.h
+++ b/src/app.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3/* Application core: event loop, base event processing, audio synth. */ 25/* Application core: event loop, base event processing, audio synth. */
@@ -26,14 +48,16 @@ enum iUserEventCode {
26const iString *execPath_App (void); 48const iString *execPath_App (void);
27const iString *dataDir_App (void); 49const iString *dataDir_App (void);
28 50
29int run_App (int argc, char **argv); 51int run_App (int argc, char **argv);
30void processEvents_App (enum iAppEventMode mode); 52void processEvents_App (enum iAppEventMode mode);
31iBool handleCommand_App (const char *cmd); 53iBool handleCommand_App (const char *cmd);
32void refresh_App (void); 54void refresh_App (void);
33iBool isRefreshPending_App(void); 55iBool isRefreshPending_App (void);
56uint32_t elapsedSinceLastTicker_App (void); /* milliseconds */
34 57
35int zoom_App (void); 58int zoom_App (void);
36enum iColorTheme colorTheme_App (void); 59enum iColorTheme colorTheme_App (void);
60const iString * schemeProxy_App (iRangecc scheme);
37 61
38iGmCerts * certs_App (void); 62iGmCerts * certs_App (void);
39iVisited * visited_App (void); 63iVisited * visited_App (void);
@@ -53,3 +77,4 @@ iLocalDef void postCommandString_App(const iString *command) {
53} 77}
54 78
55void openInDefaultBrowser_App (const iString *url); 79void openInDefaultBrowser_App (const iString *url);
80void revealPath_App (const iString *path);
diff --git a/src/bookmarks.c b/src/bookmarks.c
index 795e0d44..eff0146d 100644
--- a/src/bookmarks.c
+++ b/src/bookmarks.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "bookmarks.h" 23#include "bookmarks.h"
2 24
3#include <the_Foundation/file.h> 25#include <the_Foundation/file.h>
@@ -63,7 +85,7 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) {
63 if (open_File(f, readOnly_FileMode | text_FileMode)) { 85 if (open_File(f, readOnly_FileMode | text_FileMode)) {
64 const iRangecc src = range_Block(collect_Block(readAll_File(f))); 86 const iRangecc src = range_Block(collect_Block(readAll_File(f)));
65 iRangecc line = iNullRange; 87 iRangecc line = iNullRange;
66 while (nextSplit_Rangecc(&src, "\n", &line)) { 88 while (nextSplit_Rangecc(src, "\n", &line)) {
67 /* Skip empty lines. */ { 89 /* Skip empty lines. */ {
68 iRangecc ln = line; 90 iRangecc ln = line;
69 trim_Rangecc(&ln); 91 trim_Rangecc(&ln);
@@ -78,9 +100,9 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) {
78 initSeconds_Time(&bm->when, strtod(line.start, &endPos)); 100 initSeconds_Time(&bm->when, strtod(line.start, &endPos));
79 line.start = skipSpace_CStr(endPos); 101 line.start = skipSpace_CStr(endPos);
80 setRange_String(&bm->url, line); 102 setRange_String(&bm->url, line);
81 nextSplit_Rangecc(&src, "\n", &line); 103 nextSplit_Rangecc(src, "\n", &line);
82 setRange_String(&bm->title, line); 104 setRange_String(&bm->title, line);
83 nextSplit_Rangecc(&src, "\n", &line); 105 nextSplit_Rangecc(src, "\n", &line);
84 setRange_String(&bm->tags, line); 106 setRange_String(&bm->tags, line);
85 insert_Bookmarks_(d, bm); 107 insert_Bookmarks_(d, bm);
86 } 108 }
diff --git a/src/bookmarks.h b/src/bookmarks.h
index e05aefd3..a3269a59 100644
--- a/src/bookmarks.h
+++ b/src/bookmarks.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/hash.h> 25#include <the_Foundation/hash.h>
@@ -42,7 +64,7 @@ typedef int (*iBookmarksCompareFunc)(const iBookmark **, const iBookmark **);
42 * returned list is sorted by descending creation time. 64 * returned list is sorted by descending creation time.
43 * 65 *
44 * @return Collected array of bookmarks. Caller does not get ownership of the 66 * @return Collected array of bookmarks. Caller does not get ownership of the
45 * list or the bookmarks. 67 * listed bookmarks.
46 */ 68 */
47const iPtrArray *list_Bookmarks(const iBookmarks *, iBookmarksFilterFunc filter, 69const iPtrArray *list_Bookmarks(const iBookmarks *, iBookmarksFilterFunc filter,
48 iBookmarksCompareFunc cmp); 70 iBookmarksCompareFunc cmp);
diff --git a/src/gmcerts.c b/src/gmcerts.c
index 11e9ce2c..b24a6d9c 100644
--- a/src/gmcerts.c
+++ b/src/gmcerts.c
@@ -1,14 +1,41 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "gmcerts.h" 23#include "gmcerts.h"
2 24
3#include <the_Foundation/file.h> 25#include <the_Foundation/file.h>
26#include <the_Foundation/fileinfo.h>
4#include <the_Foundation/mutex.h> 27#include <the_Foundation/mutex.h>
5#include <the_Foundation/path.h> 28#include <the_Foundation/path.h>
6#include <the_Foundation/regexp.h> 29#include <the_Foundation/regexp.h>
30#include <the_Foundation/stringarray.h>
7#include <the_Foundation/stringhash.h> 31#include <the_Foundation/stringhash.h>
32#include <the_Foundation/stringlist.h>
8#include <the_Foundation/time.h> 33#include <the_Foundation/time.h>
9#include <ctype.h> 34#include <ctype.h>
10 35
11static const char *filename_GmCerts_ = "trusted.txt"; 36static const char *filename_GmCerts_ = "trusted.txt";
37static const char *identsDir_GmCerts_ = "idents";
38static const char *identsFilename_GmCerts_ = "idents.binary";
12 39
13iDeclareClass(TrustEntry) 40iDeclareClass(TrustEntry)
14 41
@@ -27,19 +54,158 @@ void deinit_TrustEntry(iTrustEntry *d) {
27 deinit_Block(&d->fingerprint); 54 deinit_Block(&d->fingerprint);
28} 55}
29 56
30iDefineObjectConstructionArgs(TrustEntry, (const iBlock *fingerprint, const iDate *until), fingerprint, until) 57iDefineObjectConstructionArgs(TrustEntry,
58 (const iBlock *fingerprint, const iDate *until),
59 fingerprint, until)
31iDefineClass(TrustEntry) 60iDefineClass(TrustEntry)
32 61
62/*----------------------------------------------------------------------------------------------*/
63
64static int cmpUrl_GmIdentity_(const iString *a, const iString *b) {
65 return cmpStringCase_String(a, b);
66}
67
68void init_GmIdentity(iGmIdentity *d) {
69 d->icon = 0x1f511; /* key */
70 d->flags = 0;
71 d->cert = new_TlsCertificate();
72 init_Block(&d->fingerprint, 0);
73 d->useUrls = newCmp_StringSet(cmpUrl_GmIdentity_);
74 init_String(&d->notes);
75}
76
77void deinit_GmIdentity(iGmIdentity *d) {
78 iRelease(d->useUrls);
79 deinit_String(&d->notes);
80 delete_TlsCertificate(d->cert);
81 deinit_Block(&d->fingerprint);
82}
83
84void serialize_GmIdentity(const iGmIdentity *d, iStream *outs) {
85 serialize_Block(&d->fingerprint, outs);
86 writeU32_Stream(outs, d->icon);
87 serialize_String(&d->notes, outs);
88 write32_Stream(outs, d->flags);
89 writeU32_Stream(outs, size_StringSet(d->useUrls));
90 iConstForEach(StringSet, i, d->useUrls) {
91 serialize_String(i.value, outs);
92 }
93}
94
95void deserialize_GmIdentity(iGmIdentity *d, iStream *ins) {
96 deserialize_Block(&d->fingerprint, ins);
97 d->icon = readU32_Stream(ins);
98 deserialize_String(&d->notes, ins);
99 d->flags = read32_Stream(ins);
100 size_t n = readU32_Stream(ins);
101 while (n-- && !atEnd_Stream(ins)) {
102 iString url;
103 init_String(&url);
104 deserialize_String(&url, ins);
105 insert_StringSet(d->useUrls, &url);
106 deinit_String(&url);
107 }
108}
109
110static iBool isValid_GmIdentity_(const iGmIdentity *d) {
111 return !isEmpty_TlsCertificate(d->cert);
112}
113
114static void setCertificate_GmIdentity_(iGmIdentity *d, iTlsCertificate *cert) {
115 delete_TlsCertificate(d->cert);
116 d->cert = cert;
117 set_Block(&d->fingerprint, collect_Block(fingerprint_TlsCertificate(cert)));
118}
119
120static const iString *readFile_(const iString *path) {
121 iString *str = NULL;
122 iFile *f = new_File(path);
123 if (open_File(f, readOnly_FileMode | text_FileMode)) {
124 str = readString_File(f);
125 }
126 iRelease(f);
127 return str ? collect_String(str) : collectNew_String();
128}
129
130static iBool writeTextFile_(const iString *path, const iString *content) {
131 iFile *f = iClob(new_File(path));
132 if (open_File(f, writeOnly_FileMode | text_FileMode)) {
133 write_File(f, &content->chars);
134 close_File(f);
135 return iTrue;
136 }
137 return iFalse;
138}
139
140iBool isUsed_GmIdentity(const iGmIdentity *d) {
141 return d && !isEmpty_StringSet(d->useUrls);
142}
143
144iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) {
145 size_t pos = iInvalidPos;
146 locate_StringSet(d->useUrls, url, &pos);
147 if (pos < size_StringSet(d->useUrls)) {
148 return startsWithCase_String(url, cstr_String(constAt_StringSet(d->useUrls, pos)));
149 }
150 return iFalse;
151}
152
153void setUse_GmIdentity(iGmIdentity *d, const iString *url, iBool use) {
154 if (use && isUsedOn_GmIdentity(d, url)) {
155 return; /* Redudant. */
156 }
157 if (use) {
158#if !defined (NDEBUG)
159 const iBool wasInserted =
160#endif
161 insert_StringSet(d->useUrls, url);
162 iAssert(wasInserted);
163 }
164 else {
165 remove_StringSet(d->useUrls, url);
166 }
167}
168
169void clearUse_GmIdentity(iGmIdentity *d) {
170 clear_StringSet(d->useUrls);
171}
172
173const iString *name_GmIdentity(const iGmIdentity *d) {
174 return collect_String(subject_TlsCertificate(d->cert));
175}
176
177iDefineTypeConstruction(GmIdentity)
178
33/*-----------------------------------------------------------------------------------------------*/ 179/*-----------------------------------------------------------------------------------------------*/
34 180
35struct Impl_GmCerts { 181struct Impl_GmCerts {
36 iMutex mtx; 182 iMutex mtx;
37 iString saveDir; 183 iString saveDir;
38 iStringHash *trusted; 184 iStringHash *trusted;
185 iPtrArray idents;
39}; 186};
40 187
188static const char *magicIdMeta_GmCerts_ = "lgL2";
189static const char *magicIdentity_GmCerts_ = "iden";
190
41iDefineTypeConstructionArgs(GmCerts, (const char *saveDir), saveDir) 191iDefineTypeConstructionArgs(GmCerts, (const char *saveDir), saveDir)
42 192
193static void saveIdentities_GmCerts_(const iGmCerts *d) {
194 iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_)));
195 if (open_File(f, writeOnly_FileMode)) {
196 writeData_File(f, magicIdMeta_GmCerts_, 4);
197 writeU32_File(f, 0); /* version */
198 iConstForEach(PtrArray, i, &d->idents) {
199 const iGmIdentity *ident = i.ptr;
200 if (~ident->flags & temporary_GmIdentityFlag) {
201 writeData_File(f, magicIdentity_GmCerts_, 4);
202 serialize_GmIdentity(ident, stream_File(f));
203 }
204 }
205 }
206 iRelease(f);
207}
208
43static void save_GmCerts_(const iGmCerts *d) { 209static void save_GmCerts_(const iGmCerts *d) {
44 iBeginCollect(); 210 iBeginCollect();
45 iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_))); 211 iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_)));
@@ -61,14 +227,75 @@ static void save_GmCerts_(const iGmCerts *d) {
61 iEndCollect(); 227 iEndCollect();
62} 228}
63 229
230static void loadIdentities_GmCerts_(iGmCerts *d) {
231 iFile *f =
232 iClob(new_File(collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_))));
233 if (open_File(f, readOnly_FileMode)) {
234 char magic[4];
235 readData_File(f, sizeof(magic), magic);
236 if (memcmp(magic, magicIdMeta_GmCerts_, sizeof(magic))) {
237 printf("%s: format not recognized\n", cstr_String(path_File(f)));
238 return;
239 }
240 setVersion_Stream(stream_File(f), readU32_File(f));
241 while (!atEnd_File(f)) {
242 readData_File(f, sizeof(magic), magic);
243 if (!memcmp(magic, magicIdentity_GmCerts_, sizeof(magic))) {
244 iGmIdentity *id = new_GmIdentity();
245 deserialize_GmIdentity(id, stream_File(f));
246 pushBack_PtrArray(&d->idents, id);
247 }
248 else {
249 printf("%s: invalid file contents\n", cstr_String(path_File(f)));
250 break;
251 }
252 }
253 }
254}
255
256static iGmIdentity *findIdentity_GmCerts_(iGmCerts *d, const iBlock *fingerprint) {
257 iForEach(PtrArray, i, &d->idents) {
258 iGmIdentity *ident = i.ptr;
259 if (cmp_Block(fingerprint, &ident->fingerprint) == 0) { /* TODO: could use a hash */
260 return ident;
261 }
262 }
263 return NULL;
264}
265
266static void loadIdentityFromCertificate_GmCerts_(iGmCerts *d, const iString *crtPath) {
267 iAssert(fileExists_FileInfo(crtPath));
268 iString *keyPath = collect_String(copy_String(crtPath));
269 truncate_Block(&keyPath->chars, size_String(keyPath) - 3);
270 appendCStr_String(keyPath, "key");
271 if (!fileExists_FileInfo(keyPath)) {
272 return;
273 }
274 iTlsCertificate *cert = newPemKey_TlsCertificate(readFile_(crtPath), readFile_(keyPath));
275 iBlock *finger = fingerprint_TlsCertificate(cert);
276 iGmIdentity *ident = findIdentity_GmCerts_(d, finger);
277 if (!ident) {
278 /* User-provided certificate. */
279 ident = new_GmIdentity();
280 ident->flags |= imported_GmIdentityFlag;
281 iDate today;
282 initCurrent_Date(&today);
283 set_String(&ident->notes, collect_String(format_Date(&today, "Imported on %b %d, %Y")));
284 pushBack_PtrArray(&d->idents, ident);
285 }
286 setCertificate_GmIdentity_(ident, cert);
287 delete_Block(finger);
288}
289
64static void load_GmCerts_(iGmCerts *d) { 290static void load_GmCerts_(iGmCerts *d) {
65 iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_))); 291 iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_)));
66 if (open_File(f, readOnly_FileMode | text_FileMode)) { 292 if (open_File(f, readOnly_FileMode | text_FileMode)) {
67 iRegExp * pattern = new_RegExp("([^\\s]+) ([0-9]+) ([a-z0-9]+)", 0); 293 iRegExp * pattern = new_RegExp("([^\\s]+) ([0-9]+) ([a-z0-9]+)", 0);
68 const iRangecc src = range_Block(collect_Block(readAll_File(f))); 294 const iRangecc src = range_Block(collect_Block(readAll_File(f)));
69 iRangecc line = iNullRange; 295 iRangecc line = iNullRange;
70 while (nextSplit_Rangecc(&src, "\n", &line)) { 296 while (nextSplit_Rangecc(src, "\n", &line)) {
71 iRegExpMatch m; 297 iRegExpMatch m;
298 init_RegExpMatch(&m);
72 if (matchRange_RegExp(pattern, line, &m)) { 299 if (matchRange_RegExp(pattern, line, &m)) {
73 const iRangecc domain = capturedRange_RegExpMatch(&m, 1); 300 const iRangecc domain = capturedRange_RegExpMatch(&m, 1);
74 const iRangecc until = capturedRange_RegExpMatch(&m, 2); 301 const iRangecc until = capturedRange_RegExpMatch(&m, 2);
@@ -86,17 +313,44 @@ static void load_GmCerts_(iGmCerts *d) {
86 iRelease(pattern); 313 iRelease(pattern);
87 } 314 }
88 iRelease(f); 315 iRelease(f);
316 /* Load all identity certificates. */ {
317 loadIdentities_GmCerts_(d);
318 const iString *idDir = collect_String(concatCStr_Path(&d->saveDir, identsDir_GmCerts_));
319 if (!fileExists_FileInfo(idDir)) {
320 mkdir_Path(idDir);
321 }
322 iForEach(DirFileInfo, i, iClob(directoryContents_FileInfo(iClob(new_FileInfo(idDir))))) {
323 const iFileInfo *entry = i.value;
324 if (endsWithCase_String(path_FileInfo(entry), ".crt")) {
325 loadIdentityFromCertificate_GmCerts_(d, path_FileInfo(entry));
326 }
327 }
328 /* Remove certificates whose crt/key files were missing. */
329 iForEach(PtrArray, j, &d->idents) {
330 iGmIdentity *ident = j.ptr;
331 if (!isValid_GmIdentity_(ident)) {
332 delete_GmIdentity(ident);
333 remove_PtrArrayIterator(&j);
334 }
335 }
336 }
89} 337}
90 338
91void init_GmCerts(iGmCerts *d, const char *saveDir) { 339void init_GmCerts(iGmCerts *d, const char *saveDir) {
92 init_Mutex(&d->mtx); 340 init_Mutex(&d->mtx);
93 initCStr_String(&d->saveDir, saveDir); 341 initCStr_String(&d->saveDir, saveDir);
94 d->trusted = new_StringHash(); 342 d->trusted = new_StringHash();
343 init_PtrArray(&d->idents);
95 load_GmCerts_(d); 344 load_GmCerts_(d);
96} 345}
97 346
98void deinit_GmCerts(iGmCerts *d) { 347void deinit_GmCerts(iGmCerts *d) {
99 iGuardMutex(&d->mtx, { 348 iGuardMutex(&d->mtx, {
349 saveIdentities_GmCerts_(d);
350 iForEach(PtrArray, i, &d->idents) {
351 delete_GmIdentity(i.ptr);
352 }
353 deinit_PtrArray(&d->idents);
100 iRelease(d->trusted); 354 iRelease(d->trusted);
101 deinit_String(&d->saveDir); 355 deinit_String(&d->saveDir);
102 }); 356 });
@@ -142,3 +396,102 @@ iBool checkTrust_GmCerts(iGmCerts *d, iRangecc domain, const iTlsCertificate *ce
142 unlock_Mutex(&d->mtx); 396 unlock_Mutex(&d->mtx);
143 return iTrue; 397 return iTrue;
144} 398}
399
400iGmIdentity *identity_GmCerts(iGmCerts *d, unsigned int id) {
401 return at_PtrArray(&d->idents, id);
402}
403
404const iGmIdentity *constIdentity_GmCerts(const iGmCerts *d, unsigned int id) {
405 return constAt_PtrArray(&d->idents, id);
406}
407
408const iGmIdentity *identityForUrl_GmCerts(const iGmCerts *d, const iString *url) {
409 iConstForEach(PtrArray, i, &d->idents) {
410 const iGmIdentity *ident = i.ptr;
411 iConstForEach(StringSet, j, ident->useUrls) {
412 const iString *used = j.value;
413 if (startsWithCase_String(url, cstr_String(used))) {
414 return ident;
415 }
416 }
417 }
418 return NULL;
419}
420
421iGmIdentity *newIdentity_GmCerts(iGmCerts *d, int flags, iDate validUntil, const iString *commonName,
422 const iString *email, const iString *userId, const iString *domain,
423 const iString *org, const iString *country) {
424 const iTlsCertificateName names[] = {
425 { issuerCommonName_TlsCertificateNameType, collectNewCStr_String("Lagrange v" LAGRANGE_APP_VERSION) },
426 { issuerDomain_TlsCertificateNameType, collectNewCStr_String("lagrange.skyjake.fi") },
427 { subjectCommonName_TlsCertificateNameType, commonName },
428 { subjectEmailAddress_TlsCertificateNameType, !isEmpty_String(email) ? email : NULL },
429 { subjectUserId_TlsCertificateNameType, !isEmpty_String(userId) ? userId : NULL },
430 { subjectDomain_TlsCertificateNameType, !isEmpty_String(domain) ? domain : NULL },
431 { subjectOrganization_TlsCertificateNameType, !isEmpty_String(org) ? org : NULL },
432 { subjectCountry_TlsCertificateNameType, !isEmpty_String(country) ? country : NULL },
433 { 0, NULL }
434 };
435 iGmIdentity *id = new_GmIdentity();
436 setCertificate_GmIdentity_(id, newSelfSignedRSA_TlsCertificate(2048, validUntil, names));
437 /* Save the certificate and private key as PEM files. */
438 if (~flags & temporary_GmIdentityFlag) {
439 const char *finger = cstrCollect_String(hexEncode_Block(&id->fingerprint));
440 if (!writeTextFile_(
441 collect_String(concatCStr_Path(&d->saveDir, format_CStr("idents/%s.crt", finger))),
442 collect_String(pem_TlsCertificate(id->cert)))) {
443 delete_GmIdentity(id);
444 return NULL;
445 }
446 if (!writeTextFile_(
447 collect_String(concatCStr_Path(&d->saveDir, format_CStr("idents/%s.key", finger))),
448 collect_String(privateKeyPem_TlsCertificate(id->cert)))) {
449 delete_GmIdentity(id);
450 return NULL;
451 }
452 }
453 pushBack_PtrArray(&d->idents, id);
454 return id;
455}
456
457static const char *certPath_GmCerts_(const iGmCerts *d, const iGmIdentity *identity) {
458 if (!(identity->flags & (temporary_GmIdentityFlag | imported_GmIdentityFlag))) {
459 const char *finger = cstrCollect_String(hexEncode_Block(&identity->fingerprint));
460 return concatPath_CStr(cstr_String(&d->saveDir), format_CStr("idents/%s", finger));
461 }
462 return NULL;
463}
464
465void deleteIdentity_GmCerts(iGmCerts *d, iGmIdentity *identity) {
466 /* Only delete the files if we created them. */
467 const char *filename = certPath_GmCerts_(d, identity);
468 if (filename) {
469 remove(format_CStr("%s.crt", filename));
470 remove(format_CStr("%s.key", filename));
471 }
472 removeOne_PtrArray(&d->idents, identity);
473 collect_GmIdentity(identity);
474}
475
476const iString *certificatePath_GmCerts(const iGmCerts *d, const iGmIdentity *identity) {
477 const char *filename = certPath_GmCerts_(d, identity);
478 if (filename) {
479 return collectNewFormat_String("%s.crt", filename);
480 }
481 return NULL;
482}
483
484const iPtrArray *identities_GmCerts(const iGmCerts *d) {
485 return &d->idents;
486}
487
488void signIn_GmCerts(iGmCerts *d, iGmIdentity *identity, const iString *url) {
489 signOut_GmCerts(d, url);
490 setUse_GmIdentity(identity, url, iTrue);
491}
492
493void signOut_GmCerts(iGmCerts *d, const iString *url) {
494 iForEach(PtrArray, i, &d->idents) {
495 setUse_GmIdentity(i.ptr, url, iFalse);
496 }
497}
diff --git a/src/gmcerts.h b/src/gmcerts.h
index a3df8f33..92a12a6a 100644
--- a/src/gmcerts.h
+++ b/src/gmcerts.h
@@ -1,8 +1,88 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
25#include <the_Foundation/ptrarray.h>
26#include <the_Foundation/stringset.h>
3#include <the_Foundation/tlsrequest.h> 27#include <the_Foundation/tlsrequest.h>
4 28
29iDeclareType(GmIdentity)
30iDeclareTypeConstruction(GmIdentity)
31iDeclareTypeSerialization(GmIdentity)
32
33enum iGmIdentityFlags {
34 temporary_GmIdentityFlag = 0x1, /* not saved persistently */
35 imported_GmIdentityFlag = 0x2, /* user-provided files */
36};
37
38struct Impl_GmIdentity {
39 iBlock fingerprint;
40 iTlsCertificate *cert;
41 iStringSet *useUrls;
42 iChar icon;
43 iString notes; /* private, local usage notes */
44 int flags;
45};
46
47iBool isUsed_GmIdentity (const iGmIdentity *);
48iBool isUsedOn_GmIdentity (const iGmIdentity *, const iString *url);
49
50void setUse_GmIdentity (iGmIdentity *, const iString *url, iBool use);
51void clearUse_GmIdentity (iGmIdentity *);
52
53const iString *name_GmIdentity(const iGmIdentity *);
54
55/*----------------------------------------------------------------------------------------------*/
56
5iDeclareType(GmCerts) 57iDeclareType(GmCerts)
6iDeclareTypeConstructionArgs(GmCerts, const char *saveDir) 58iDeclareTypeConstructionArgs(GmCerts, const char *saveDir)
7 59
8iBool checkTrust_GmCerts (iGmCerts *, iRangecc domain, const iTlsCertificate *cert); 60iBool checkTrust_GmCerts (iGmCerts *, iRangecc domain, const iTlsCertificate *cert);
61
62/**
63 * Create a new self-signed TLS client certificate for identifying the user.
64 * @a commonName and the other name parameters are inserted in the subject field
65 * of the certificate.
66 *
67 * @param flags Identity flags. A temporary identity is not saved persistently and
68 * will be erased when the application is shut down.
69 * @param validUntil Expiration date. Must be in the future.
70 *
71 * @returns Created identity. GmCerts retains ownership of returned object.
72 */
73iGmIdentity * newIdentity_GmCerts (iGmCerts *, int flags, iDate validUntil,
74 const iString *commonName, const iString *email,
75 const iString *userId, const iString *domain,
76 const iString *org, const iString *country);
77
78void deleteIdentity_GmCerts (iGmCerts *, iGmIdentity *identity);
79
80const iString * certificatePath_GmCerts (const iGmCerts *, const iGmIdentity *identity);
81
82iGmIdentity * identity_GmCerts (iGmCerts *, unsigned int id);
83const iGmIdentity * constIdentity_GmCerts (const iGmCerts *, unsigned int id);
84const iGmIdentity * identityForUrl_GmCerts (const iGmCerts *, const iString *url);
85const iPtrArray * identities_GmCerts (const iGmCerts *);
86
87void signIn_GmCerts (iGmCerts *, iGmIdentity *identity, const iString *url);
88void signOut_GmCerts (iGmCerts *, const iString *url);
diff --git a/src/gmdocument.c b/src/gmdocument.c
index a6fc39c5..fc49dac4 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "gmdocument.h" 23#include "gmdocument.h"
2#include "gmutil.h" 24#include "gmutil.h"
3#include "ui/color.h" 25#include "ui/color.h"
@@ -106,11 +128,11 @@ enum iGmLineType {
106 max_GmLineType, 128 max_GmLineType,
107}; 129};
108 130
109static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc *line) { 131static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) {
110 if (d->format == plainText_GmDocumentFormat) { 132 if (d->format == plainText_GmDocumentFormat) {
111 return text_GmLineType; 133 return text_GmLineType;
112 } 134 }
113 if (isEmpty_Range(line)) { 135 if (isEmpty_Range(&line)) {
114 return text_GmLineType; 136 return text_GmLineType;
115 } 137 }
116 if (startsWith_Rangecc(line, "=>")) { 138 if (startsWith_Rangecc(line, "=>")) {
@@ -128,10 +150,10 @@ static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangec
128 if (startsWith_Rangecc(line, "```")) { 150 if (startsWith_Rangecc(line, "```")) {
129 return preformatted_GmLineType; 151 return preformatted_GmLineType;
130 } 152 }
131 if (*line->start == '>') { 153 if (*line.start == '>') {
132 return quote_GmLineType; 154 return quote_GmLineType;
133 } 155 }
134 if (size_Range(line) >= 2 && line->start[0] == '*' && isspace(line->start[1])) { 156 if (size_Range(&line) >= 2 && line.start[0] == '*' && isspace(line.start[1])) {
135 return bullet_GmLineType; 157 return bullet_GmLineType;
136 } 158 }
137 return text_GmLineType; 159 return text_GmLineType;
@@ -157,11 +179,11 @@ static int lastVisibleRunBottom_GmDocument_(const iGmDocument *d) {
157iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *start, int font) { 179iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *start, int font) {
158 const iRangecc content = { start, constEnd_String(&d->source) }; 180 const iRangecc content = { start, constEnd_String(&d->source) };
159 iRangecc line = iNullRange; 181 iRangecc line = iNullRange;
160 nextSplit_Rangecc(&content, "\n", &line); 182 nextSplit_Rangecc(content, "\n", &line);
161 iAssert(startsWith_Rangecc(&line, "```")); 183 iAssert(startsWith_Rangecc(line, "```"));
162 iRangecc preBlock = { line.end + 1, line.end + 1 }; 184 iRangecc preBlock = { line.end + 1, line.end + 1 };
163 while (nextSplit_Rangecc(&content, "\n", &line)) { 185 while (nextSplit_Rangecc(content, "\n", &line)) {
164 if (startsWith_Rangecc(&line, "```")) { 186 if (startsWith_Rangecc(line, "```")) {
165 break; 187 break;
166 } 188 }
167 preBlock.end = line.end; 189 preBlock.end = line.end;
@@ -170,31 +192,35 @@ iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *sta
170} 192}
171 193
172static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) { 194static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) {
173 iRegExp *pattern = new_RegExp("=>\\s*([^\\s]+)(\\s.*)?", caseInsensitive_RegExpOption); 195 static iRegExp *pattern_;
196 if (!pattern_) {
197 pattern_ = new_RegExp("=>\\s*([^\\s]+)(\\s.*)?", caseInsensitive_RegExpOption);
198 }
174 iRegExpMatch m; 199 iRegExpMatch m;
175 if (matchRange_RegExp(pattern, line, &m)) { 200 init_RegExpMatch(&m);
201 if (matchRange_RegExp(pattern_, line, &m)) {
176 iGmLink *link = new_GmLink(); 202 iGmLink *link = new_GmLink();
177 setRange_String(&link->url, capturedRange_RegExpMatch(&m, 1)); 203 setRange_String(&link->url, capturedRange_RegExpMatch(&m, 1));
178 set_String(&link->url, absoluteUrl_String(&d->url, &link->url)); 204 set_String(&link->url, absoluteUrl_String(&d->url, &link->url));
179 /* Check the URL. */ { 205 /* Check the URL. */ {
180 iUrl parts; 206 iUrl parts;
181 init_Url(&parts, &link->url); 207 init_Url(&parts, &link->url);
182 if (!equalCase_Rangecc(&parts.host, cstr_String(&d->localHost))) { 208 if (!equalCase_Rangecc(parts.host, cstr_String(&d->localHost))) {
183 link->flags |= remote_GmLinkFlag; 209 link->flags |= remote_GmLinkFlag;
184 } 210 }
185 if (startsWithCase_Rangecc(&parts.protocol, "gemini")) { 211 if (startsWithCase_Rangecc(parts.scheme, "gemini")) {
186 link->flags |= gemini_GmLinkFlag; 212 link->flags |= gemini_GmLinkFlag;
187 } 213 }
188 else if (startsWithCase_Rangecc(&parts.protocol, "http")) { 214 else if (startsWithCase_Rangecc(parts.scheme, "http")) {
189 link->flags |= http_GmLinkFlag; 215 link->flags |= http_GmLinkFlag;
190 } 216 }
191 else if (equalCase_Rangecc(&parts.protocol, "gopher")) { 217 else if (equalCase_Rangecc(parts.scheme, "gopher")) {
192 link->flags |= gopher_GmLinkFlag; 218 link->flags |= gopher_GmLinkFlag;
193 } 219 }
194 else if (equalCase_Rangecc(&parts.protocol, "file")) { 220 else if (equalCase_Rangecc(parts.scheme, "file")) {
195 link->flags |= file_GmLinkFlag; 221 link->flags |= file_GmLinkFlag;
196 } 222 }
197 else if (equalCase_Rangecc(&parts.protocol, "data")) { 223 else if (equalCase_Rangecc(parts.scheme, "data")) {
198 link->flags |= data_GmLinkFlag; 224 link->flags |= data_GmLinkFlag;
199 } 225 }
200 /* Check the file name extension, if present. */ 226 /* Check the file name extension, if present. */
@@ -232,7 +258,6 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
232 line = capturedRange_RegExpMatch(&m, 1); /* Show the URL. */ 258 line = capturedRange_RegExpMatch(&m, 1); /* Show the URL. */
233 } 259 }
234 } 260 }
235 iRelease(pattern);
236 return line; 261 return line;
237} 262}
238 263
@@ -254,16 +279,21 @@ static size_t findLinkImage_GmDocument_(const iGmDocument *d, iGmLinkId linkId)
254 return iInvalidPos; 279 return iInvalidPos;
255} 280}
256 281
282static iBool isGopher_GmDocument_(const iGmDocument *d) {
283 return equalCase_Rangecc(urlScheme_String(&d->url), "gopher");
284}
285
257static void doLayout_GmDocument_(iGmDocument *d) { 286static void doLayout_GmDocument_(iGmDocument *d) {
287 const iBool isGemini = !isGopher_GmDocument_(d);
258 /* TODO: Collect these parameters into a GmTheme. */ 288 /* TODO: Collect these parameters into a GmTheme. */
259 static const int fonts[max_GmLineType] = { 289 const int fonts[max_GmLineType] = {
260 paragraph_FontId, 290 isGemini ? paragraph_FontId : preformatted_FontId,
261 paragraph_FontId, /* bullet */ 291 isGemini ? paragraph_FontId : preformatted_FontId, /* bullet */
262 preformatted_FontId, 292 preformatted_FontId,
263 quote_FontId, 293 quote_FontId,
264 heading1_FontId, 294 isGemini ? heading1_FontId : preformatted_FontId,
265 heading2_FontId, 295 isGemini ? heading2_FontId : preformatted_FontId,
266 heading3_FontId, 296 isGemini ? heading3_FontId : preformatted_FontId,
267 regular_FontId, 297 regular_FontId,
268 }; 298 };
269 static const int colors[max_GmLineType] = { 299 static const int colors[max_GmLineType] = {
@@ -277,7 +307,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
277 tmLinkText_ColorId, 307 tmLinkText_ColorId,
278 }; 308 };
279 static const int indents[max_GmLineType] = { 309 static const int indents[max_GmLineType] = {
280 5, 10, 5, 10, 0, 0, 0, 5 310 6, 12, 6, 12, 0, 0, 0, 6
281 }; 311 };
282 static const float topMargin[max_GmLineType] = { 312 static const float topMargin[max_GmLineType] = {
283 0.0f, 0.5f, 1.0f, 0.5f, 2.0f, 2.0f, 1.5f, 1.0f 313 0.0f, 0.5f, 1.0f, 0.5f, 2.0f, 2.0f, 1.5f, 1.0f
@@ -298,20 +328,21 @@ static void doLayout_GmDocument_(iGmDocument *d) {
298 return; 328 return;
299 } 329 }
300 const iRangecc content = range_String(&d->source); 330 const iRangecc content = range_String(&d->source);
331 iRangecc contentLine = iNullRange;
301 iInt2 pos = zero_I2(); 332 iInt2 pos = zero_I2();
302 iRangecc line = iNullRange; 333 iBool isFirstText = isGemini;
303 iRangecc preAltText = iNullRange;
304 enum iGmLineType prevType; // = text_GmLineType;
305 iBool isPreformat = iFalse; 334 iBool isPreformat = iFalse;
306 iBool isFirstText = iTrue; 335 iRangecc preAltText = iNullRange;
336 int preFont = preformatted_FontId;
307 iBool enableIndents = iFalse; 337 iBool enableIndents = iFalse;
308 iBool addSiteBanner = iTrue; 338 iBool addSiteBanner = iTrue;
309 int preFont = preformatted_FontId; 339 enum iGmLineType prevType;
310 if (d->format == plainText_GmDocumentFormat) { 340 if (d->format == plainText_GmDocumentFormat) {
311 isPreformat = iTrue; 341 isPreformat = iTrue;
312 isFirstText = iFalse; 342 isFirstText = iFalse;
313 } 343 }
314 while (nextSplit_Rangecc(&content, "\n", &line)) { 344 while (nextSplit_Rangecc(content, "\n", &contentLine)) {
345 iRangecc line = contentLine; /* `line` will be trimmed later; would confuse nextSplit */
315 iGmRun run; 346 iGmRun run;
316 run.flags = 0; 347 run.flags = 0;
317 run.color = white_ColorId; 348 run.color = white_ColorId;
@@ -320,7 +351,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
320 enum iGmLineType type; 351 enum iGmLineType type;
321 int indent = 0; 352 int indent = 0;
322 if (!isPreformat) { 353 if (!isPreformat) {
323 type = lineType_GmDocument_(d, &line); 354 type = lineType_GmDocument_(d, line);
324 if (line.start == content.start) { 355 if (line.start == content.start) {
325 prevType = type; 356 prevType = type;
326 } 357 }
@@ -358,7 +389,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
358 /* Preformatted line. */ 389 /* Preformatted line. */
359 type = preformatted_GmLineType; 390 type = preformatted_GmLineType;
360 if (d->format == gemini_GmDocumentFormat && 391 if (d->format == gemini_GmDocumentFormat &&
361 startsWithSc_Rangecc(&line, "```", &iCaseSensitive)) { 392 startsWithSc_Rangecc(line, "```", &iCaseSensitive)) {
362 isPreformat = iFalse; 393 isPreformat = iFalse;
363 preAltText = iNullRange; 394 preAltText = iNullRange;
364 addSiteBanner = iFalse; /* overrides the banner */ 395 addSiteBanner = iFalse; /* overrides the banner */
@@ -664,6 +695,20 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
664 } 695 }
665 /* Set up colors. */ 696 /* Set up colors. */
666 if (d->themeSeed) { 697 if (d->themeSeed) {
698 enum iHue {
699 red_Hue,
700 reddishOrange_Hue,
701 yellowishOrange_Hue,
702 yellow_Hue,
703 greenishYellow_Hue,
704 green_Hue,
705 bluishGreen_Hue,
706 cyan_Hue,
707 skyBlue_Hue,
708 blue_Hue,
709 violet_Hue,
710 pink_Hue
711 };
667 static const float hues[] = { 5, 25, 40, 56, 80, 120, 160, 180, 208, 231, 270, 324 }; 712 static const float hues[] = { 5, 25, 40, 56, 80, 120, 160, 180, 208, 231, 270, 324 };
668 static const struct { 713 static const struct {
669 int index[2]; 714 int index[2];
@@ -681,9 +726,9 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
681 { 8, 9 }, /* violet */ 726 { 8, 9 }, /* violet */
682 { 7, 8 }, /* pink */ 727 { 7, 8 }, /* pink */
683 }; 728 };
684 const float saturationLevel = 1.0f; /* TODO: user setting */
685 const iBool isBannerLighter = (d->themeSeed & 0x4000) != 0; 729 const iBool isBannerLighter = (d->themeSeed & 0x4000) != 0;
686 const size_t primIndex = d->themeSeed ? (d->themeSeed & 0xff) % iElemCount(hues) : 2; 730 const size_t primIndex = d->themeSeed ? (d->themeSeed & 0xff) % iElemCount(hues) : 2;
731 const float saturationLevel = 1.0f; /* TODO: user setting */
687 const iBool isDarkBgSat = 732 const iBool isDarkBgSat =
688 (d->themeSeed & 0x200000) != 0 && (primIndex < 1 || primIndex > 4); 733 (d->themeSeed & 0x200000) != 0 && (primIndex < 1 || primIndex > 4);
689 iHSLColor base = { hues[primIndex], 734 iHSLColor base = { hues[primIndex],
@@ -692,9 +737,10 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
692 1.0f }; 737 1.0f };
693 // printf("background: %d %f %f\n", (int) base.hue, base.sat, base.lum); 738 // printf("background: %d %f %f\n", (int) base.hue, base.sat, base.lum);
694 // printf("isDarkBgSat: %d\n", isDarkBgSat); 739 // printf("isDarkBgSat: %d\n", isDarkBgSat);
695 setHsl_Color(tmBackground_ColorId, base); 740 iHSLColor bgBase = base;
741 setHsl_Color(tmBackground_ColorId, bgBase);
696 742
697 setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(base, 0.1f, 0.04f * (isBannerLighter ? 1 : -1))); 743 setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(bgBase, 0.1f, 0.04f * (isBannerLighter ? 1 : -1)));
698 setHsl_Color(tmBannerTitle_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.1f, 0), 0.55f)); 744 setHsl_Color(tmBannerTitle_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.1f, 0), 0.55f));
699 setHsl_Color(tmBannerIcon_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.35f, 0), 0.65f)); 745 setHsl_Color(tmBannerIcon_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.35f, 0), 0.65f));
700 746
@@ -714,7 +760,7 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
714 setHsl_Color(tmHeading3_ColorId, setLum_HSLColor(altBase, titleLum + 0.60f)); 760 setHsl_Color(tmHeading3_ColorId, setLum_HSLColor(altBase, titleLum + 0.60f));
715 761
716 setHsl_Color(tmParagraph_ColorId, addSatLum_HSLColor(base, 0.1f, 0.6f)); 762 setHsl_Color(tmParagraph_ColorId, addSatLum_HSLColor(base, 0.1f, 0.6f));
717 setHsl_Color(tmFirstParagraph_ColorId, addSatLum_HSLColor(base, 0.2f, 0.8f)); 763 setHsl_Color(tmFirstParagraph_ColorId, addSatLum_HSLColor(base, 0.2f, 0.72f));
718 setHsl_Color(tmPreformatted_ColorId, (iHSLColor){ altHue2, 1.0f, 0.75f, 1.0f }); 764 setHsl_Color(tmPreformatted_ColorId, (iHSLColor){ altHue2, 1.0f, 0.75f, 1.0f });
719 set_Color(tmQuote_ColorId, get_Color(tmPreformatted_ColorId)); 765 set_Color(tmQuote_ColorId, get_Color(tmPreformatted_ColorId));
720 set_Color(tmInlineContentMetadata_ColorId, get_Color(tmHeading3_ColorId)); 766 set_Color(tmInlineContentMetadata_ColorId, get_Color(tmHeading3_ColorId));
@@ -736,31 +782,12 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
736 else if (i == tmHeading3_ColorId) { 782 else if (i == tmHeading3_ColorId) {
737 color.lum *= 0.75f; 783 color.lum *= 0.75f;
738 } 784 }
739#if 0
740 else if (isLink_ColorId(i)) {
741 /* Darken links generally to improve visibility against a
742 light background. */
743 color.lum *= 0.5f;
744 color.sat = 1.0f;
745 }
746#endif
747 else if (i == tmBannerIcon_ColorId || i == tmBannerTitle_ColorId) { 785 else if (i == tmBannerIcon_ColorId || i == tmBannerTitle_ColorId) {
748 if (isBannerLighter) { 786 color.sat = 1.0f;
749 color.lum *= 0.75f; 787 color.lum = 0.35f;
750 }
751 else {
752 color.lum = 0.98f;
753 }
754 } 788 }
755 else if (i == tmBannerBackground_ColorId) { 789 else if (i == tmBannerBackground_ColorId) {
756 if (isBannerLighter) { 790 color = hsl_Color(get_Color(tmBackground_ColorId));
757 //color.lum = iMin(0.9, color.lum);
758 color = hsl_Color(get_Color(tmBackground_ColorId));
759 }
760 else {
761 color.sat *= 0.8f;
762 color.lum = 0.6f;
763 }
764 } 791 }
765 else if (isText_ColorId(i)) { 792 else if (isText_ColorId(i)) {
766 color.sat = 0.9f; 793 color.sat = 0.9f;
@@ -772,7 +799,12 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
772 if (isDarkBgSat) { 799 if (isDarkBgSat) {
773 /* Saturate background, desaturate text. */ 800 /* Saturate background, desaturate text. */
774 if (isBackground_ColorId(i)) { 801 if (isBackground_ColorId(i)) {
775 color.sat = (color.sat + 1) / 2; 802 if (primIndex != green_Hue) {
803 color.sat = (color.sat + 1) / 2;
804 }
805 else {
806 color.sat *= 0.5f;
807 }
776 color.lum *= 0.75f; 808 color.lum *= 0.75f;
777 } 809 }
778 else if (isText_ColorId(i)) { 810 else if (isText_ColorId(i)) {
@@ -822,11 +854,11 @@ static void normalize_GmDocument(iGmDocument *d) {
822 iRangecc src = range_String(&d->source); 854 iRangecc src = range_String(&d->source);
823 iRangecc line = iNullRange; 855 iRangecc line = iNullRange;
824 iBool isPreformat = iFalse; 856 iBool isPreformat = iFalse;
825 if (d->format == plainText_GmDocumentFormat) { 857 if (d->format == plainText_GmDocumentFormat || isGopher_GmDocument_(d)) {
826 isPreformat = iTrue; /* Cannot be turned off. */ 858 isPreformat = iTrue; /* Cannot be turned off. */
827 } 859 }
828 const int preTabWidth = 8; /* TODO: user-configurable parameter */ 860 const int preTabWidth = 4; /* TODO: user-configurable parameter */
829 while (nextSplit_Rangecc(&src, "\n", &line)) { 861 while (nextSplit_Rangecc(src, "\n", &line)) {
830 if (isPreformat) { 862 if (isPreformat) {
831 /* Replace any tab characters with spaces for visualization. */ 863 /* Replace any tab characters with spaces for visualization. */
832 for (const char *ch = line.start; ch != line.end; ch++) { 864 for (const char *ch = line.start; ch != line.end; ch++) {
@@ -842,12 +874,12 @@ static void normalize_GmDocument(iGmDocument *d) {
842 } 874 }
843 } 875 }
844 appendCStr_String(normalized, "\n"); 876 appendCStr_String(normalized, "\n");
845 if (lineType_GmDocument_(d, &line) == preformatted_GmLineType) { 877 if (lineType_GmDocument_(d, line) == preformatted_GmLineType) {
846 isPreformat = iFalse; 878 isPreformat = iFalse;
847 } 879 }
848 continue; 880 continue;
849 } 881 }
850 if (lineType_GmDocument_(d, &line) == preformatted_GmLineType) { 882 if (lineType_GmDocument_(d, line) == preformatted_GmLineType) {
851 isPreformat = iTrue; 883 isPreformat = iTrue;
852 appendRange_String(normalized, line); 884 appendRange_String(normalized, line);
853 appendCStr_String(normalized, "\n"); 885 appendCStr_String(normalized, "\n");
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 9f4bc1ca..b6c1c6ab 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include "gmutil.h" 25#include "gmutil.h"
@@ -66,7 +88,7 @@ iDeclareObjectConstruction(GmDocument)
66 88
67enum iGmDocumentFormat { 89enum iGmDocumentFormat {
68 undefined_GmDocumentFormat = -1, 90 undefined_GmDocumentFormat = -1,
69 gemini_GmDocumentFormat = 0, 91 gemini_GmDocumentFormat = 0,
70 plainText_GmDocumentFormat, 92 plainText_GmDocumentFormat,
71}; 93};
72 94
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 0d69861d..137e8303 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -1,8 +1,31 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "gmrequest.h" 23#include "gmrequest.h"
2#include "gmutil.h" 24#include "gmutil.h"
3#include "gmcerts.h" 25#include "gmcerts.h"
4#include "app.h" /* dataDir_App() */ 26#include "app.h" /* dataDir_App() */
5#include "embedded.h" 27#include "embedded.h"
28#include "ui/text.h"
6 29
7#include <the_Foundation/file.h> 30#include <the_Foundation/file.h>
8#include <the_Foundation/mutex.h> 31#include <the_Foundation/mutex.h>
@@ -171,11 +194,22 @@ static void checkServerCertificate_GmRequest_(iGmRequest *d) {
171 const iTlsCertificate *cert = serverCertificate_TlsRequest(d->req); 194 const iTlsCertificate *cert = serverCertificate_TlsRequest(d->req);
172 d->resp.certFlags = 0; 195 d->resp.certFlags = 0;
173 if (cert) { 196 if (cert) {
174 const iRangecc domain = urlHost_String(&d->url); 197 const iRangecc domain = range_String(hostName_Address(address_TlsRequest(d->req)));
175 d->resp.certFlags |= available_GmCertFlag; 198 d->resp.certFlags |= available_GmCertFlag;
176 if (!isExpired_TlsCertificate(cert)) { 199 if (!isExpired_TlsCertificate(cert)) {
177 d->resp.certFlags |= timeVerified_GmCertFlag; 200 d->resp.certFlags |= timeVerified_GmCertFlag;
178 } 201 }
202 /* TODO: Check for IP too (see below), because it may be specified in the SAN. */
203#if 0
204 iString *ip = toStringFlags_Address(address_TlsRequest(d->req), noPort_SocketStringFlag, 0);
205 if (verifyIp_TlsCertificate(cert, ip)) {
206 printf("[GmRequest] IP address %s matches!\n", cstr_String(ip));
207 }
208 else {
209 printf("[GmRequest] IP address %s not matched\n", cstr_String(ip));
210 }
211 delete_String(ip);
212#endif
179 if (verifyDomain_TlsCertificate(cert, domain)) { 213 if (verifyDomain_TlsCertificate(cert, domain)) {
180 d->resp.certFlags |= domainVerified_GmCertFlag; 214 d->resp.certFlags |= domainVerified_GmCertFlag;
181 } 215 }
@@ -265,31 +299,92 @@ static void requestFinished_GmRequest_(iAnyObject *obj) {
265 299
266static const iBlock *aboutPageSource_(iRangecc path) { 300static const iBlock *aboutPageSource_(iRangecc path) {
267 const iBlock *src = NULL; 301 const iBlock *src = NULL;
268 if (equalCase_Rangecc(&path, "lagrange")) { 302 if (equalCase_Rangecc(path, "lagrange")) {
269 return &blobAbout_Embedded; 303 return &blobLagrange_Embedded;
270 } 304 }
271 if (equalCase_Rangecc(&path, "help")) { 305 if (equalCase_Rangecc(path, "help")) {
272 return &blobHelp_Embedded; 306 return &blobHelp_Embedded;
273 } 307 }
274 if (equalCase_Rangecc(&path, "version")) { 308 if (equalCase_Rangecc(path, "version")) {
275 return &blobVersion_Embedded; 309 return &blobVersion_Embedded;
276 } 310 }
277 return src; 311 return src;
278} 312}
279 313
280static const iBlock *replaceVariables_(const iBlock *block) { 314static const iBlock *replaceVariables_(const iBlock *block) {
281 iRegExp *var = new_RegExp("\\$\\{([A-Z_]+)\\}", 0); 315 iRegExp *var = new_RegExp("\\$\\{([^}]+)\\}", 0);
282 iRegExpMatch m; 316 iRegExpMatch m;
317 init_RegExpMatch(&m);
283 if (matchRange_RegExp(var, range_Block(block), &m)) { 318 if (matchRange_RegExp(var, range_Block(block), &m)) {
284 iBlock *replaced = collect_Block(copy_Block(block)); 319 iBlock *replaced = collect_Block(copy_Block(block));
285 do { 320 do {
286 const iRangei span = m.range; 321 const iRangei span = m.range;
287 remove_Block(replaced, span.start, size_Range(&span));
288 const iRangecc name = capturedRange_RegExpMatch(&m, 1); 322 const iRangecc name = capturedRange_RegExpMatch(&m, 1);
289 if (equal_Rangecc(&name, "APP_VERSION")) { 323 iRangecc repl = iNullRange;
290 insertData_Block(replaced, span.start, 324 if (equal_Rangecc(name, "APP_VERSION")) {
291 LAGRANGE_APP_VERSION, strlen(LAGRANGE_APP_VERSION)); 325 repl = range_CStr(LAGRANGE_APP_VERSION);
326 }
327 else if (startsWith_Rangecc(name, "BT:")) { /* block text */
328 repl = range_String(collect_String(renderBlockChars_Text(
329 &fontFiraSansRegular_Embedded,
330 11, /* should be larger if shaded */
331 quadrants_TextBlockMode,
332 &(iString){ iBlockLiteral(
333 name.start + 3, size_Range(&name) - 3, size_Range(&name) - 3) })));
334 }
335 else if (startsWith_Rangecc(name, "ST:")) { /* shaded text */
336 repl = range_String(collect_String(renderBlockChars_Text(
337 &fontSymbola_Embedded,
338 20,
339 shading_TextBlockMode,
340 &(iString){ iBlockLiteral(
341 name.start + 3, size_Range(&name) - 3, size_Range(&name) - 3) })));
342 }
343 else if (equal_Rangecc(name, "ALT")) {
344#if defined (iPlatformApple)
345 repl = range_CStr("\u2325");
346#else
347 repl = range_CStr("Alt");
348#endif
349 }
350 else if (equal_Rangecc(name, "ALT+")) {
351#if defined (iPlatformApple)
352 repl = range_CStr("\u2325");
353#else
354 repl = range_CStr("Alt+");
355#endif
356 }
357 else if (equal_Rangecc(name, "CTRL")) {
358#if defined (iPlatformApple)
359 repl = range_CStr("\u2318");
360#else
361 repl = range_CStr("Ctrl");
362#endif
292 } 363 }
364 else if (equal_Rangecc(name, "CTRL+")) {
365#if defined (iPlatformApple)
366 repl = range_CStr("\u2318");
367#else
368 repl = range_CStr("Ctrl+");
369#endif
370 }
371 else if (equal_Rangecc(name, "SHIFT")) {
372#if defined (iPlatformApple)
373 repl = range_CStr("\u21e7");
374#else
375 repl = range_CStr("Shift");
376#endif
377 }
378 else if (equal_Rangecc(name, "SHIFT+")) {
379#if defined (iPlatformApple)
380 repl = range_CStr("\u21e7");
381#else
382 repl = range_CStr("Shift+");
383#endif
384 }
385 remove_Block(replaced, span.start, size_Range(&span));
386 insertData_Block(replaced, span.start, repl.start, size_Range(&repl));
387 iZap(m);
293 } while (matchRange_RegExp(var, range_Block(replaced), &m)); 388 } while (matchRange_RegExp(var, range_Block(replaced), &m));
294 block = replaced; 389 block = replaced;
295 } 390 }
@@ -305,9 +400,12 @@ void submit_GmRequest(iGmRequest *d) {
305 clear_GmResponse(&d->resp); 400 clear_GmResponse(&d->resp);
306 iUrl url; 401 iUrl url;
307 init_Url(&url, &d->url); 402 init_Url(&url, &d->url);
308 /* Check for special protocols. */ 403 /* Check for special schemes. */
309 /* TODO: If this were a library, these could be handled via callbacks. */ 404 /* TODO: If this were a library, these could be handled via callbacks. */
310 if (equalCase_Rangecc(&url.protocol, "about")) { 405 /* TODO: Handle app's configured proxies and these via the same mechanism. */
406 const iString *host = collect_String(newRange_String(url.host));
407 uint16_t port = toInt_String(collect_String(newRange_String(url.port)));
408 if (equalCase_Rangecc(url.scheme, "about")) {
311 const iBlock *src = aboutPageSource_(url.path); 409 const iBlock *src = aboutPageSource_(url.path);
312 if (src) { 410 if (src) {
313 d->resp.statusCode = success_GmStatusCode; 411 d->resp.statusCode = success_GmStatusCode;
@@ -323,7 +421,7 @@ void submit_GmRequest(iGmRequest *d) {
323 iNotifyAudience(d, finished, GmRequestFinished); 421 iNotifyAudience(d, finished, GmRequestFinished);
324 return; 422 return;
325 } 423 }
326 else if (equalCase_Rangecc(&url.protocol, "file")) { 424 else if (equalCase_Rangecc(url.scheme, "file")) {
327 iString *path = collect_String(urlDecode_String(collect_String(newRange_String(url.path)))); 425 iString *path = collect_String(urlDecode_String(collect_String(newRange_String(url.path))));
328 iFile * f = new_File(path); 426 iFile * f = new_File(path);
329 if (open_File(f, readOnly_FileMode)) { 427 if (open_File(f, readOnly_FileMode)) {
@@ -361,9 +459,9 @@ void submit_GmRequest(iGmRequest *d) {
361 iNotifyAudience(d, finished, GmRequestFinished); 459 iNotifyAudience(d, finished, GmRequestFinished);
362 return; 460 return;
363 } 461 }
364 else if (equalCase_Rangecc(&url.protocol, "data")) { 462 else if (equalCase_Rangecc(url.scheme, "data")) {
365 d->resp.statusCode = success_GmStatusCode; 463 d->resp.statusCode = success_GmStatusCode;
366 iString *src = collectNewCStr_String(url.protocol.start + 5); 464 iString *src = collectNewCStr_String(url.scheme.start + 5);
367 iRangecc header = { constBegin_String(src), constBegin_String(src) }; 465 iRangecc header = { constBegin_String(src), constBegin_String(src) };
368 while (header.end < constEnd_String(src) && *header.end != ',') { 466 while (header.end < constEnd_String(src) && *header.end != ',') {
369 header.end++; 467 header.end++;
@@ -372,8 +470,8 @@ void submit_GmRequest(iGmRequest *d) {
372 setRange_String(&d->resp.meta, header); 470 setRange_String(&d->resp.meta, header);
373 /* Check what's in the header. */ { 471 /* Check what's in the header. */ {
374 iRangecc entry = iNullRange; 472 iRangecc entry = iNullRange;
375 while (nextSplit_Rangecc(&header, ";", &entry)) { 473 while (nextSplit_Rangecc(header, ";", &entry)) {
376 if (equal_Rangecc(&entry, "base64")) { 474 if (equal_Rangecc(entry, "base64")) {
377 isBase64 = iTrue; 475 isBase64 = iTrue;
378 } 476 }
379 } 477 }
@@ -392,15 +490,31 @@ void submit_GmRequest(iGmRequest *d) {
392 iNotifyAudience(d, finished, GmRequestFinished); 490 iNotifyAudience(d, finished, GmRequestFinished);
393 return; 491 return;
394 } 492 }
493 else if (schemeProxy_App(url.scheme)) {
494 /* User has configured a proxy server for this scheme. */
495 const iString *proxy = schemeProxy_App(url.scheme);
496 if (contains_String(proxy, ':')) {
497 const size_t cpos = indexOf_String(proxy, ':');
498 port = atoi(cstr_String(proxy) + cpos + 1);
499 host = collect_String(newCStrN_String(cstr_String(proxy), cpos));
500 }
501 else {
502 host = proxy;
503 port = 0;
504 }
505 }
395 d->state = receivingHeader_GmRequestState; 506 d->state = receivingHeader_GmRequestState;
396 d->req = new_TlsRequest(); 507 d->req = new_TlsRequest();
508 const iGmIdentity *identity = identityForUrl_GmCerts(d->certs, &d->url);
509 if (identity) {
510 setCertificate_TlsRequest(d->req, identity->cert);
511 }
397 iConnect(TlsRequest, d->req, readyRead, d, readIncoming_GmRequest_); 512 iConnect(TlsRequest, d->req, readyRead, d, readIncoming_GmRequest_);
398 iConnect(TlsRequest, d->req, finished, d, requestFinished_GmRequest_); 513 iConnect(TlsRequest, d->req, finished, d, requestFinished_GmRequest_);
399 uint16_t port = toInt_String(collect_String(newRange_String(url.port)));
400 if (port == 0) { 514 if (port == 0) {
401 port = 1965; /* default Gemini port */ 515 port = 1965; /* default Gemini port */
402 } 516 }
403 setUrl_TlsRequest(d->req, collect_String(newRange_String(url.host)), port); 517 setUrl_TlsRequest(d->req, host, port);
404 setContent_TlsRequest(d->req, 518 setContent_TlsRequest(d->req,
405 utf8_String(collectNewFormat_String("%s\r\n", cstr_String(&d->url)))); 519 utf8_String(collectNewFormat_String("%s\r\n", cstr_String(&d->url))));
406 submit_TlsRequest(d->req); 520 submit_TlsRequest(d->req);
diff --git a/src/gmrequest.h b/src/gmrequest.h
index cb7c151a..2f1a4261 100644
--- a/src/gmrequest.h
+++ b/src/gmrequest.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/audience.h> 25#include <the_Foundation/audience.h>
diff --git a/src/gmutil.c b/src/gmutil.c
index 2138caa3..131734b2 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "gmutil.h" 23#include "gmutil.h"
2 24
3#include <the_Foundation/regexp.h> 25#include <the_Foundation/regexp.h>
@@ -5,12 +27,17 @@
5#include <the_Foundation/path.h> 27#include <the_Foundation/path.h>
6 28
7void init_Url(iUrl *d, const iString *text) { 29void init_Url(iUrl *d, const iString *text) {
8 iRegExp *absPat = 30 static iRegExp *absoluteUrlPattern_;
9 new_RegExp("([a-z]+:)?(//[^/:?]*)(:[0-9]+)?([^?]*)(\\?.*)?", caseInsensitive_RegExpOption); 31 static iRegExp *relativeUrlPattern_;
32 if (!absoluteUrlPattern_) {
33 absoluteUrlPattern_ = new_RegExp("([a-z]+:)?(//[^/:?]*)(:[0-9]+)?([^?]*)(\\?.*)?",
34 caseInsensitive_RegExpOption);
35 }
10 iRegExpMatch m; 36 iRegExpMatch m;
11 if (matchString_RegExp(absPat, text, &m)) { 37 init_RegExpMatch(&m);
12 d->protocol = capturedRange_RegExpMatch(&m, 1); 38 if (matchString_RegExp(absoluteUrlPattern_, text, &m)) {
13 d->host = capturedRange_RegExpMatch(&m, 2); 39 d->scheme = capturedRange_RegExpMatch(&m, 1);
40 d->host = capturedRange_RegExpMatch(&m, 2);
14 if (!isEmpty_Range(&d->host)) { 41 if (!isEmpty_Range(&d->host)) {
15 d->host.start += 2; /* skip the double slash */ 42 d->host.start += 2; /* skip the double slash */
16 } 43 }
@@ -18,28 +45,28 @@ void init_Url(iUrl *d, const iString *text) {
18 if (!isEmpty_Range(&d->port)) { 45 if (!isEmpty_Range(&d->port)) {
19 d->port.start++; /* omit the colon */ 46 d->port.start++; /* omit the colon */
20 } 47 }
21 d->path = capturedRange_RegExpMatch(&m, 4); 48 d->path = capturedRange_RegExpMatch(&m, 4);
22 d->query = capturedRange_RegExpMatch(&m, 5); 49 d->query = capturedRange_RegExpMatch(&m, 5);
23 } 50 }
24 else { 51 else {
25 /* Must be a relative path. */ 52 /* Must be a relative path. */
26 iZap(*d); 53 iZap(*d);
27 iRegExp *relPat = new_RegExp("([a-z]+:)?([^?]*)(\\?.*)?", 0); 54 if (!relativeUrlPattern_) {
28 if (matchString_RegExp(relPat, text, &m)) { 55 relativeUrlPattern_ = new_RegExp("([a-z]+:)?([^?]*)(\\?.*)?", 0);
29 d->protocol = capturedRange_RegExpMatch(&m, 1); 56 }
30 d->path = capturedRange_RegExpMatch(&m, 2); 57 if (matchString_RegExp(relativeUrlPattern_, text, &m)) {
31 d->query = capturedRange_RegExpMatch(&m, 3); 58 d->scheme = capturedRange_RegExpMatch(&m, 1);
59 d->path = capturedRange_RegExpMatch(&m, 2);
60 d->query = capturedRange_RegExpMatch(&m, 3);
32 } 61 }
33 iRelease(relPat);
34 } 62 }
35 iRelease(absPat); 63 if (!isEmpty_Range(&d->scheme)) {
36 if (!isEmpty_Range(&d->protocol)) { 64 d->scheme.end--; /* omit the colon */
37 d->protocol.end--; /* omit the colon */
38 } 65 }
39} 66}
40 67
41static iRangecc dirPath_(iRangecc path) { 68static iRangecc dirPath_(iRangecc path) {
42 const size_t pos = lastIndexOfCStr_Rangecc(&path, "/"); 69 const size_t pos = lastIndexOfCStr_Rangecc(path, "/");
43 if (pos == iInvalidPos) return path; 70 if (pos == iInvalidPos) return path;
44 return (iRangecc){ path.start, path.start + pos }; 71 return (iRangecc){ path.start, path.start + pos };
45} 72}
@@ -62,13 +89,13 @@ void cleanUrlPath_String(iString *d) {
62 iUrl parts; 89 iUrl parts;
63 init_Url(&parts, d); 90 init_Url(&parts, d);
64 iRangecc seg = iNullRange; 91 iRangecc seg = iNullRange;
65 while (nextSplit_Rangecc(&parts.path, "/", &seg)) { 92 while (nextSplit_Rangecc(parts.path, "/", &seg)) {
66 if (equal_Rangecc(&seg, "..")) { 93 if (equal_Rangecc(seg, "..")) {
67 /* Back up one segment. */ 94 /* Back up one segment. */
68 iRangecc last = prevPathSeg_(constEnd_String(&clean), constBegin_String(&clean)); 95 iRangecc last = prevPathSeg_(constEnd_String(&clean), constBegin_String(&clean));
69 truncate_Block(&clean.chars, last.start - constBegin_String(&clean)); 96 truncate_Block(&clean.chars, last.start - constBegin_String(&clean));
70 } 97 }
71 else if (equal_Rangecc(&seg, ".")) { 98 else if (equal_Rangecc(seg, ".")) {
72 /* Skip it. */ 99 /* Skip it. */
73 } 100 }
74 else { 101 else {
@@ -76,11 +103,11 @@ void cleanUrlPath_String(iString *d) {
76 appendRange_String(&clean, seg); 103 appendRange_String(&clean, seg);
77 } 104 }
78 } 105 }
79 if (endsWith_Rangecc(&parts.path, "/")) { 106 if (endsWith_Rangecc(parts.path, "/")) {
80 appendCStr_String(&clean, "/"); 107 appendCStr_String(&clean, "/");
81 } 108 }
82 /* Replace with the new path. */ 109 /* Replace with the new path. */
83 if (cmpCStrNSc_Rangecc(&parts.path, cstr_String(&clean), size_String(&clean), &iCaseSensitive)) { 110 if (cmpCStrNSc_Rangecc(parts.path, cstr_String(&clean), size_String(&clean), &iCaseSensitive)) {
84 const size_t pos = parts.path.start - constBegin_String(d); 111 const size_t pos = parts.path.start - constBegin_String(d);
85 remove_Block(&d->chars, pos, size_Range(&parts.path)); 112 remove_Block(&d->chars, pos, size_Range(&parts.path));
86 insertData_Block(&d->chars, pos, cstr_String(&clean), size_String(&clean)); 113 insertData_Block(&d->chars, pos, cstr_String(&clean), size_String(&clean));
@@ -88,10 +115,10 @@ void cleanUrlPath_String(iString *d) {
88 deinit_String(&clean); 115 deinit_String(&clean);
89} 116}
90 117
91iRangecc urlProtocol_String(const iString *d) { 118iRangecc urlScheme_String(const iString *d) {
92 iUrl url; 119 iUrl url;
93 init_Url(&url, d); 120 init_Url(&url, d);
94 return url.protocol; 121 return url.scheme;
95} 122}
96 123
97iRangecc urlHost_String(const iString *d) { 124iRangecc urlHost_String(const iString *d) {
@@ -105,20 +132,20 @@ const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat
105 iUrl rel; 132 iUrl rel;
106 init_Url(&orig, d); 133 init_Url(&orig, d);
107 init_Url(&rel, urlMaybeRelative); 134 init_Url(&rel, urlMaybeRelative);
108 if (equalCase_Rangecc(&rel.protocol, "data") || equalCase_Rangecc(&rel.protocol, "about")) { 135 if (equalCase_Rangecc(rel.scheme, "data") || equalCase_Rangecc(rel.scheme, "about")) {
109 /* Special case, the contents should be left unparsed. */ 136 /* Special case, the contents should be left unparsed. */
110 return urlMaybeRelative; 137 return urlMaybeRelative;
111 } 138 }
112 const iBool isRelative = !isDef_(rel.host); 139 const iBool isRelative = !isDef_(rel.host);
113 iRangecc protocol = range_CStr("gemini"); 140 iRangecc scheme = range_CStr("gemini");
114 if (isDef_(rel.protocol)) { 141 if (isDef_(rel.scheme)) {
115 protocol = rel.protocol; 142 scheme = rel.scheme;
116 } 143 }
117 else if (isRelative && isDef_(orig.protocol)) { 144 else if (isRelative && isDef_(orig.scheme)) {
118 protocol = orig.protocol; 145 scheme = orig.scheme;
119 } 146 }
120 iString *absolute = collectNew_String(); 147 iString *absolute = collectNew_String();
121 appendRange_String(absolute, protocol); 148 appendRange_String(absolute, scheme);
122 appendCStr_String(absolute, "://"); { 149 appendCStr_String(absolute, "://"); {
123 const iUrl *selHost = isDef_(rel.host) ? &rel : &orig; 150 const iUrl *selHost = isDef_(rel.host) ? &rel : &orig;
124 appendRange_String(absolute, selHost->host); 151 appendRange_String(absolute, selHost->host);
@@ -127,11 +154,11 @@ const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat
127 appendRange_String(absolute, selHost->port); 154 appendRange_String(absolute, selHost->port);
128 } 155 }
129 } 156 }
130 if (isDef_(rel.protocol) || isDef_(rel.host) || startsWith_Rangecc(&rel.path, "/")) { 157 if (isDef_(rel.scheme) || isDef_(rel.host) || startsWith_Rangecc(rel.path, "/")) {
131 appendRange_String(absolute, rel.path); /* absolute path */ 158 appendRange_String(absolute, isDef_(rel.path) ? rel.path : range_CStr("/")); /* absolute path */
132 } 159 }
133 else { 160 else {
134 if (!endsWith_Rangecc(&orig.path, "/")) { 161 if (!endsWith_Rangecc(orig.path, "/")) {
135 /* Referencing a file. */ 162 /* Referencing a file. */
136 appendRange_String(absolute, dirPath_(orig.path)); 163 appendRange_String(absolute, dirPath_(orig.path));
137 } 164 }
@@ -200,6 +227,16 @@ static const struct {
200 "Invalid Redirect", 227 "Invalid Redirect",
201 "The server responded with a redirect but did not provide a valid destination URL. " 228 "The server responded with a redirect but did not provide a valid destination URL. "
202 "Perhaps the server is malfunctioning." } }, 229 "Perhaps the server is malfunctioning." } },
230 { nonGeminiRedirect_GmStatusCode,
231 { 0x27a0, /* dashed arrow */
232 "Redirect to Non-Gemini URL",
233 "The server attempted to redirect us to a non-Gemini URL. Here is the link so you "
234 "can open it manually if appropriate."} },
235 { tooManyRedirects_GmStatusCode,
236 { 0x27a0, /* dashed arrow */
237 "Too Many Redirects",
238 "You may be stuck in a redirection loop. The next redirected URL is below if you "
239 "want to continue manually."} },
203 { temporaryFailure_GmStatusCode, 240 { temporaryFailure_GmStatusCode,
204 { 0x1f50c, /* electric plug */ 241 { 0x1f50c, /* electric plug */
205 "Temporary Failure", 242 "Temporary Failure",
diff --git a/src/gmutil.h b/src/gmutil.h
index 9e8c5934..2c017af4 100644
--- a/src/gmutil.h
+++ b/src/gmutil.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/range.h> 25#include <the_Foundation/range.h>
@@ -11,6 +33,8 @@ enum iGmStatusCode {
11 /* clientside status codes */ 33 /* clientside status codes */
12 clientSide_GmStatusCode = -100, 34 clientSide_GmStatusCode = -100,
13 invalidRedirect_GmStatusCode, 35 invalidRedirect_GmStatusCode,
36 nonGeminiRedirect_GmStatusCode,
37 tooManyRedirects_GmStatusCode,
14 invalidHeader_GmStatusCode, 38 invalidHeader_GmStatusCode,
15 unsupportedMimeType_GmStatusCode, 39 unsupportedMimeType_GmStatusCode,
16 failedToOpenFile_GmStatusCode, 40 failedToOpenFile_GmStatusCode,
@@ -61,7 +85,7 @@ iBool isDefined_GmError (enum iGmStatusCode code);
61const iGmError * get_GmError (enum iGmStatusCode code); 85const iGmError * get_GmError (enum iGmStatusCode code);
62 86
63struct Impl_Url { 87struct Impl_Url {
64 iRangecc protocol; 88 iRangecc scheme;
65 iRangecc host; 89 iRangecc host;
66 iRangecc port; 90 iRangecc port;
67 iRangecc path; 91 iRangecc path;
@@ -70,7 +94,7 @@ struct Impl_Url {
70 94
71void init_Url (iUrl *, const iString *text); 95void init_Url (iUrl *, const iString *text);
72 96
73iRangecc urlProtocol_String (const iString *); 97iRangecc urlScheme_String (const iString *);
74iRangecc urlHost_String (const iString *); 98iRangecc urlHost_String (const iString *);
75const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative); 99const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative);
76iString * makeFileUrl_String (const iString *localFilePath); 100iString * makeFileUrl_String (const iString *localFilePath);
diff --git a/src/history.c b/src/history.c
index 24912d81..6e985903 100644
--- a/src/history.c
+++ b/src/history.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "history.h" 23#include "history.h"
2#include "app.h" 24#include "app.h"
3 25
diff --git a/src/history.h b/src/history.h
index cf3a46be..22fe8bf8 100644
--- a/src/history.h
+++ b/src/history.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include "gmrequest.h" 25#include "gmrequest.h"
diff --git a/src/macos.h b/src/macos.h
new file mode 100644
index 00000000..07990090
--- /dev/null
+++ b/src/macos.h
@@ -0,0 +1,31 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#pragma once
24
25#include "ui/util.h"
26
27/* Platform-specific functionality for macOS */
28
29void setupApplication_MacOS (void);
30void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count);
31void handleCommand_MacOS (const char *cmd);
diff --git a/src/ui/macos.m b/src/macos.m
index 9ff2f96e..edbb6df0 100644
--- a/src/ui/macos.m
+++ b/src/macos.m
@@ -1,8 +1,30 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "macos.h" 23#include "macos.h"
2#include "app.h" 24#include "app.h"
3#include "command.h" 25#include "ui/command.h"
4#include "widget.h" 26#include "ui/widget.h"
5#include "color.h" 27#include "ui/color.h"
6 28
7#import <AppKit/AppKit.h> 29#import <AppKit/AppKit.h>
8 30
@@ -91,6 +113,7 @@ enum iTouchBarVariant {
91 113
92@interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> { 114@interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> {
93 enum iTouchBarVariant touchBarVariant; 115 enum iTouchBarVariant touchBarVariant;
116 NSString *currentAppearanceName;
94 NSObject<NSApplicationDelegate> *sdlDelegate; 117 NSObject<NSApplicationDelegate> *sdlDelegate;
95 NSMutableDictionary<NSString *, NSString*> *menuCommands; 118 NSMutableDictionary<NSString *, NSString*> *menuCommands;
96} 119}
@@ -105,6 +128,7 @@ enum iTouchBarVariant {
105 128
106- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl { 129- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl {
107 [super init]; 130 [super init];
131 currentAppearanceName = nil;
108 menuCommands = [[NSMutableDictionary<NSString *, NSString *> alloc] init]; 132 menuCommands = [[NSMutableDictionary<NSString *, NSString *> alloc] init];
109 touchBarVariant = default_TouchBarVariant; 133 touchBarVariant = default_TouchBarVariant;
110 sdlDelegate = sdl; 134 sdlDelegate = sdl;
@@ -113,6 +137,7 @@ enum iTouchBarVariant {
113 137
114- (void)dealloc { 138- (void)dealloc {
115 [menuCommands release]; 139 [menuCommands release];
140 [currentAppearanceName release];
116 [super dealloc]; 141 [super dealloc];
117} 142}
118 143
@@ -121,6 +146,24 @@ enum iTouchBarVariant {
121 self.touchBar = nil; 146 self.touchBar = nil;
122} 147}
123 148
149static void appearanceChanged_MacOS_(NSString *name) {
150 const iBool isDark = [name containsString:@"Dark"];
151 const iBool isHighContrast = [name containsString:@"HighContrast"];
152 postCommandf_App("os.theme.changed dark:%d contrast:%d", isDark ? 1 : 0, isHighContrast ? 1 : 0);
153// printf("Effective appearance changed: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]);
154// fflush(stdout);
155}
156
157- (void)setAppearance:(NSString *)name {
158 if (!currentAppearanceName || ![name isEqualToString:currentAppearanceName]) {
159 if (currentAppearanceName) {
160 [currentAppearanceName release];
161 }
162 currentAppearanceName = [name retain];
163 appearanceChanged_MacOS_(currentAppearanceName);
164 }
165}
166
124- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem { 167- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem {
125 [menuCommands setObject:command forKey:[menuItem title]]; 168 [menuCommands setObject:command forKey:[menuItem title]];
126} 169}
@@ -133,6 +176,16 @@ enum iTouchBarVariant {
133 [sdlDelegate applicationDidFinishLaunching:notification]; 176 [sdlDelegate applicationDidFinishLaunching:notification];
134} 177}
135 178
179- (void)observeValueForKeyPath:(NSString *)keyPath
180 ofObject:(id)object
181 change:(NSDictionary *)change
182 context:(void *)context {
183 iUnused(object, change);
184 if ([keyPath isEqualToString:@"effectiveAppearance"] && context == self) {
185 [self setAppearance:[[NSApp effectiveAppearance] name]];
186 }
187}
188
136#if 0 189#if 0
137- (NSTouchBar *)makeTouchBar { 190- (NSTouchBar *)makeTouchBar {
138 NSTouchBar *bar = [[NSTouchBar alloc] init]; 191 NSTouchBar *bar = [[NSTouchBar alloc] init];
@@ -225,7 +278,7 @@ enum iTouchBarVariant {
225 return [[CommandButton alloc] initWithIdentifier:identifier 278 return [[CommandButton alloc] initWithIdentifier:identifier
226 title:@"Go to…" 279 title:@"Go to…"
227 command:@"pattern.goto arg:-1"]; 280 command:@"pattern.goto arg:-1"];
228 } 281 }
229 else if ([identifier isEqualToString:event_TouchId_]) { 282 else if ([identifier isEqualToString:event_TouchId_]) {
230 NSTouchBar *events = [[NSTouchBar alloc] init]; 283 NSTouchBar *events = [[NSTouchBar alloc] init];
231 events.delegate = self; 284 events.delegate = self;
@@ -344,8 +397,10 @@ void enableMomentumScroll_MacOS(void) {
344 397
345void setupApplication_MacOS(void) { 398void setupApplication_MacOS(void) {
346 NSApplication *app = [NSApplication sharedApplication]; 399 NSApplication *app = [NSApplication sharedApplication];
400 //appearanceChanged_MacOS_([[app effectiveAppearance] name]);
347 /* Our delegate will override SDL's delegate. */ 401 /* Our delegate will override SDL's delegate. */
348 MyDelegate *myDel = [[MyDelegate alloc] initWithSDLDelegate:app.delegate]; 402 MyDelegate *myDel = [[MyDelegate alloc] initWithSDLDelegate:app.delegate];
403 [myDel setAppearance:[[app effectiveAppearance] name]];
349 app.delegate = myDel; 404 app.delegate = myDel;
350 NSMenu *appMenu = [[[NSApp mainMenu] itemAtIndex:0] submenu]; 405 NSMenu *appMenu = [[[NSApp mainMenu] itemAtIndex:0] submenu];
351 NSMenuItem *prefsItem = [appMenu itemWithTitle:@"Preferences…"]; 406 NSMenuItem *prefsItem = [appMenu itemWithTitle:@"Preferences…"];
@@ -361,6 +416,10 @@ void setupApplication_MacOS(void) {
361void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) { 416void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) {
362 NSApplication *app = [NSApplication sharedApplication]; 417 NSApplication *app = [NSApplication sharedApplication];
363 MyDelegate *myDel = (MyDelegate *) app.delegate; 418 MyDelegate *myDel = (MyDelegate *) app.delegate;
419 [app addObserver:myDel
420 forKeyPath:@"effectiveAppearance"
421 options:0
422 context:myDel];
364 NSMenu *appMenu = [app mainMenu]; 423 NSMenu *appMenu = [app mainMenu];
365 NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel] 424 NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel]
366 action:nil 425 action:nil
@@ -417,6 +476,11 @@ void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *
417} 476}
418 477
419void handleCommand_MacOS(const char *cmd) { 478void handleCommand_MacOS(const char *cmd) {
479 if (equal_Command(cmd, "prefs.ostheme.changed")) {
480 if (arg_Command(cmd)) {
481 appearanceChanged_MacOS_([[NSApp effectiveAppearance] name]);
482 }
483 }
420#if 0 484#if 0
421 if (equal_Command(cmd, "tabs.changed")) { 485 if (equal_Command(cmd, "tabs.changed")) {
422 MyDelegate *myDel = (MyDelegate *) [[NSApplication sharedApplication] delegate]; 486 MyDelegate *myDel = (MyDelegate *) [[NSApplication sharedApplication] delegate];
diff --git a/src/main.c b/src/main.c
index 38d01b94..c13b75d1 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include <the_Foundation/commandline.h> 23#include <the_Foundation/commandline.h>
2#include <stdio.h> 24#include <stdio.h>
3#if defined (iPlatformMsys) 25#if defined (iPlatformMsys)
diff --git a/src/stb_image.h b/src/stb_image.h
index 2857f05d..accef483 100644
--- a/src/stb_image.h
+++ b/src/stb_image.h
@@ -1,4 +1,4 @@
1/* stb_image - v2.25 - public domain image loader - http://nothings.org/stb 1/* stb_image - v2.26 - public domain image loader - http://nothings.org/stb
2 no warranty implied; use at your own risk 2 no warranty implied; use at your own risk
3 3
4 Do this: 4 Do this:
@@ -48,6 +48,7 @@ LICENSE
48 48
49RECENT REVISION HISTORY: 49RECENT REVISION HISTORY:
50 50
51 2.26 (2020-07-13) many minor fixes
51 2.25 (2020-02-02) fix warnings 52 2.25 (2020-02-02) fix warnings
52 2.24 (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically 53 2.24 (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically
53 2.23 (2019-08-11) fix clang static analysis warning 54 2.23 (2019-08-11) fix clang static analysis warning
@@ -93,22 +94,30 @@ RECENT REVISION HISTORY:
93 Carmelo J Fdez-Aguera 94 Carmelo J Fdez-Aguera
94 95
95 Bug & warning fixes 96 Bug & warning fixes
96 Marc LeBlanc David Woo Guillaume George Martins Mozeiko 97 Marc LeBlanc David Woo Guillaume George Martins Mozeiko
97 Christpher Lloyd Jerry Jansson Joseph Thomson Phil Jordan 98 Christpher Lloyd Jerry Jansson Joseph Thomson Blazej Dariusz Roszkowski
98 Dave Moore Roy Eltham Hayaki Saito Nathan Reed 99 Phil Jordan Dave Moore Roy Eltham
99 Won Chun Luke Graham Johan Duparc Nick Verigakis 100 Hayaki Saito Nathan Reed Won Chun
100 the Horde3D community Thomas Ruf Ronny Chevalier github:rlyeh 101 Luke Graham Johan Duparc Nick Verigakis the Horde3D community
101 Janez Zemva John Bartholomew Michal Cichon github:romigrou 102 Thomas Ruf Ronny Chevalier github:rlyeh
102 Jonathan Blow Ken Hamada Tero Hanninen github:svdijk 103 Janez Zemva John Bartholomew Michal Cichon github:romigrou
103 Laurent Gomila Cort Stratton Sergio Gonzalez github:snagar 104 Jonathan Blow Ken Hamada Tero Hanninen github:svdijk
104 Aruelien Pocheville Thibault Reuille Cass Everitt github:Zelex 105 Laurent Gomila Cort Stratton github:snagar
105 Ryamond Barbiero Paul Du Bois Engin Manap github:grim210 106 Aruelien Pocheville Sergio Gonzalez Thibault Reuille github:Zelex
106 Aldo Culquicondor Philipp Wiesemann Dale Weiler github:sammyhw 107 Cass Everitt Ryamond Barbiero github:grim210
107 Oriol Ferrer Mesia Josh Tobin Matthew Gregan github:phprus 108 Paul Du Bois Engin Manap Aldo Culquicondor github:sammyhw
108 Julian Raschke Gregory Mullen Baldur Karlsson github:poppolopoppo 109 Philipp Wiesemann Dale Weiler Oriol Ferrer Mesia github:phprus
109 Christian Floisand Kevin Schmidt JR Smith github:darealshinji 110 Josh Tobin Matthew Gregan github:poppolopoppo
110 Brad Weinberger Matvey Cherevko github:Michaelangel007 111 Julian Raschke Gregory Mullen Christian Floisand github:darealshinji
111 Blazej Dariusz Roszkowski Alexander Veselov 112 Baldur Karlsson Kevin Schmidt JR Smith github:Michaelangel007
113 Brad Weinberger Matvey Cherevko [reserved]
114 Luca Sas Alexander Veselov Zack Middleton [reserved]
115 Ryan C. Gordon [reserved] [reserved]
116 DO NOT ADD YOUR NAME HERE
117
118 To add your name to the credits, pick a random blank space in the middle and fill it.
119 80% of merge conflicts on stb PRs are due to people adding their name at the end
120 of the credits.
112*/ 121*/
113 122
114#ifndef STBI_INCLUDE_STB_IMAGE_H 123#ifndef STBI_INCLUDE_STB_IMAGE_H
@@ -318,7 +327,14 @@ RECENT REVISION HISTORY:
318// - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still 327// - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still
319// want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB 328// want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB
320// 329//
321 330// - If you define STBI_MAX_DIMENSIONS, stb_image will reject images greater
331// than that size (in either width or height) without further processing.
332// This is to let programs in the wild set an upper bound to prevent
333// denial-of-service attacks on untrusted data, as one could generate a
334// valid image of gigantic dimensions and force stb_image to allocate a
335// huge block of memory and spend disproportionate time decoding it. By
336// default this is set to (1 << 24), which is 16777216, but that's still
337// very big.
322 338
323#ifndef STBI_NO_STDIO 339#ifndef STBI_NO_STDIO
324#include <stdio.h> 340#include <stdio.h>
@@ -574,13 +590,19 @@ STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const ch
574#ifndef STBI_NO_THREAD_LOCALS 590#ifndef STBI_NO_THREAD_LOCALS
575 #if defined(__cplusplus) && __cplusplus >= 201103L 591 #if defined(__cplusplus) && __cplusplus >= 201103L
576 #define STBI_THREAD_LOCAL thread_local 592 #define STBI_THREAD_LOCAL thread_local
577 #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L 593 #elif defined(__GNUC__) && __GNUC__ < 5
578 #define STBI_THREAD_LOCAL _Thread_local
579 #elif defined(__GNUC__)
580 #define STBI_THREAD_LOCAL __thread 594 #define STBI_THREAD_LOCAL __thread
581 #elif defined(_MSC_VER) 595 #elif defined(_MSC_VER)
582 #define STBI_THREAD_LOCAL __declspec(thread) 596 #define STBI_THREAD_LOCAL __declspec(thread)
583#endif 597 #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__)
598 #define STBI_THREAD_LOCAL _Thread_local
599 #endif
600
601 #ifndef STBI_THREAD_LOCAL
602 #if defined(__GNUC__)
603 #define STBI_THREAD_LOCAL __thread
604 #endif
605 #endif
584#endif 606#endif
585 607
586#ifdef _MSC_VER 608#ifdef _MSC_VER
@@ -734,6 +756,10 @@ static int stbi__sse2_available(void)
734#define STBI_SIMD_ALIGN(type, name) type name 756#define STBI_SIMD_ALIGN(type, name) type name
735#endif 757#endif
736 758
759#ifndef STBI_MAX_DIMENSIONS
760#define STBI_MAX_DIMENSIONS (1 << 24)
761#endif
762
737/////////////////////////////////////////////// 763///////////////////////////////////////////////
738// 764//
739// stbi__context struct and start_xxx functions 765// stbi__context struct and start_xxx functions
@@ -751,6 +777,7 @@ typedef struct
751 int read_from_callbacks; 777 int read_from_callbacks;
752 int buflen; 778 int buflen;
753 stbi_uc buffer_start[128]; 779 stbi_uc buffer_start[128];
780 int callback_already_read;
754 781
755 stbi_uc *img_buffer, *img_buffer_end; 782 stbi_uc *img_buffer, *img_buffer_end;
756 stbi_uc *img_buffer_original, *img_buffer_original_end; 783 stbi_uc *img_buffer_original, *img_buffer_original_end;
@@ -764,6 +791,7 @@ static void stbi__start_mem(stbi__context *s, stbi_uc const *buffer, int len)
764{ 791{
765 s->io.read = NULL; 792 s->io.read = NULL;
766 s->read_from_callbacks = 0; 793 s->read_from_callbacks = 0;
794 s->callback_already_read = 0;
767 s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer; 795 s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer;
768 s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len; 796 s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len;
769} 797}
@@ -775,7 +803,8 @@ static void stbi__start_callbacks(stbi__context *s, stbi_io_callbacks *c, void *
775 s->io_user_data = user; 803 s->io_user_data = user;
776 s->buflen = sizeof(s->buffer_start); 804 s->buflen = sizeof(s->buffer_start);
777 s->read_from_callbacks = 1; 805 s->read_from_callbacks = 1;
778 s->img_buffer_original = s->buffer_start; 806 s->callback_already_read = 0;
807 s->img_buffer = s->img_buffer_original = s->buffer_start;
779 stbi__refill_buffer(s); 808 stbi__refill_buffer(s);
780 s->img_buffer_original_end = s->img_buffer_end; 809 s->img_buffer_original_end = s->img_buffer_end;
781} 810}
@@ -789,12 +818,17 @@ static int stbi__stdio_read(void *user, char *data, int size)
789 818
790static void stbi__stdio_skip(void *user, int n) 819static void stbi__stdio_skip(void *user, int n)
791{ 820{
821 int ch;
792 fseek((FILE*) user, n, SEEK_CUR); 822 fseek((FILE*) user, n, SEEK_CUR);
823 ch = fgetc((FILE*) user); /* have to read a byte to reset feof()'s flag */
824 if (ch != EOF) {
825 ungetc(ch, (FILE *) user); /* push byte back onto stream if valid. */
826 }
793} 827}
794 828
795static int stbi__stdio_eof(void *user) 829static int stbi__stdio_eof(void *user)
796{ 830{
797 return feof((FILE*) user); 831 return feof((FILE*) user) || ferror((FILE *) user);
798} 832}
799 833
800static stbi_io_callbacks stbi__stdio_callbacks = 834static stbi_io_callbacks stbi__stdio_callbacks =
@@ -1171,8 +1205,10 @@ static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x,
1171 if (result == NULL) 1205 if (result == NULL)
1172 return NULL; 1206 return NULL;
1173 1207
1208 // it is the responsibility of the loaders to make sure we get either 8 or 16 bit.
1209 STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16);
1210
1174 if (ri.bits_per_channel != 8) { 1211 if (ri.bits_per_channel != 8) {
1175 STBI_ASSERT(ri.bits_per_channel == 16);
1176 result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp); 1212 result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp);
1177 ri.bits_per_channel = 8; 1213 ri.bits_per_channel = 8;
1178 } 1214 }
@@ -1195,8 +1231,10 @@ static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x,
1195 if (result == NULL) 1231 if (result == NULL)
1196 return NULL; 1232 return NULL;
1197 1233
1234 // it is the responsibility of the loaders to make sure we get either 8 or 16 bit.
1235 STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16);
1236
1198 if (ri.bits_per_channel != 16) { 1237 if (ri.bits_per_channel != 16) {
1199 STBI_ASSERT(ri.bits_per_channel == 8);
1200 result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp); 1238 result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp);
1201 ri.bits_per_channel = 16; 1239 ri.bits_per_channel = 16;
1202 } 1240 }
@@ -1499,6 +1537,7 @@ enum
1499static void stbi__refill_buffer(stbi__context *s) 1537static void stbi__refill_buffer(stbi__context *s)
1500{ 1538{
1501 int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen); 1539 int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen);
1540 s->callback_already_read += (int) (s->img_buffer - s->img_buffer_original);
1502 if (n == 0) { 1541 if (n == 0) {
1503 // at end of file, treat same as if from memory, but need to handle case 1542 // at end of file, treat same as if from memory, but need to handle case
1504 // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file 1543 // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file
@@ -1544,6 +1583,7 @@ stbi_inline static int stbi__at_eof(stbi__context *s)
1544#else 1583#else
1545static void stbi__skip(stbi__context *s, int n) 1584static void stbi__skip(stbi__context *s, int n)
1546{ 1585{
1586 if (n == 0) return; // already there!
1547 if (n < 0) { 1587 if (n < 0) {
1548 s->img_buffer = s->img_buffer_end; 1588 s->img_buffer = s->img_buffer_end;
1549 return; 1589 return;
@@ -1686,7 +1726,7 @@ static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int r
1686 STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; 1726 STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break;
1687 STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break; 1727 STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break;
1688 STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; 1728 STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break;
1689 default: STBI_ASSERT(0); 1729 default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return stbi__errpuc("unsupported", "Unsupported format conversion");
1690 } 1730 }
1691 #undef STBI__CASE 1731 #undef STBI__CASE
1692 } 1732 }
@@ -1743,7 +1783,7 @@ static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int r
1743 STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; 1783 STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break;
1744 STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break; 1784 STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break;
1745 STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; 1785 STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break;
1746 default: STBI_ASSERT(0); 1786 default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return (stbi__uint16*) stbi__errpuc("unsupported", "Unsupported format conversion");
1747 } 1787 }
1748 #undef STBI__CASE 1788 #undef STBI__CASE
1749 } 1789 }
@@ -2052,7 +2092,7 @@ stbi_inline static int stbi__extend_receive(stbi__jpeg *j, int n)
2052 2092
2053 sgn = (stbi__int32)j->code_buffer >> 31; // sign bit is always in MSB 2093 sgn = (stbi__int32)j->code_buffer >> 31; // sign bit is always in MSB
2054 k = stbi_lrot(j->code_buffer, n); 2094 k = stbi_lrot(j->code_buffer, n);
2055 STBI_ASSERT(n >= 0 && n < (int) (sizeof(stbi__bmask)/sizeof(*stbi__bmask))); 2095 if (n < 0 || n >= (int) (sizeof(stbi__bmask)/sizeof(*stbi__bmask))) return 0;
2056 j->code_buffer = k & ~stbi__bmask[n]; 2096 j->code_buffer = k & ~stbi__bmask[n];
2057 k &= stbi__bmask[n]; 2097 k &= stbi__bmask[n];
2058 j->code_bits -= n; 2098 j->code_bits -= n;
@@ -2163,6 +2203,7 @@ static int stbi__jpeg_decode_block_prog_dc(stbi__jpeg *j, short data[64], stbi__
2163 // first scan for DC coefficient, must be first 2203 // first scan for DC coefficient, must be first
2164 memset(data,0,64*sizeof(data[0])); // 0 all the ac values now 2204 memset(data,0,64*sizeof(data[0])); // 0 all the ac values now
2165 t = stbi__jpeg_huff_decode(j, hdc); 2205 t = stbi__jpeg_huff_decode(j, hdc);
2206 if (t == -1) return stbi__err("can't merge dc and ac", "Corrupt JPEG");
2166 diff = t ? stbi__extend_receive(j, t) : 0; 2207 diff = t ? stbi__extend_receive(j, t) : 0;
2167 2208
2168 dc = j->img_comp[b].dc_pred + diff; 2209 dc = j->img_comp[b].dc_pred + diff;
@@ -3153,6 +3194,8 @@ static int stbi__process_frame_header(stbi__jpeg *z, int scan)
3153 p = stbi__get8(s); if (p != 8) return stbi__err("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline 3194 p = stbi__get8(s); if (p != 8) return stbi__err("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline
3154 s->img_y = stbi__get16be(s); if (s->img_y == 0) return stbi__err("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG 3195 s->img_y = stbi__get16be(s); if (s->img_y == 0) return stbi__err("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG
3155 s->img_x = stbi__get16be(s); if (s->img_x == 0) return stbi__err("0 width","Corrupt JPEG"); // JPEG requires 3196 s->img_x = stbi__get16be(s); if (s->img_x == 0) return stbi__err("0 width","Corrupt JPEG"); // JPEG requires
3197 if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
3198 if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
3156 c = stbi__get8(s); 3199 c = stbi__get8(s);
3157 if (c != 3 && c != 1 && c != 4) return stbi__err("bad component count","Corrupt JPEG"); 3200 if (c != 3 && c != 1 && c != 4) return stbi__err("bad component count","Corrupt JPEG");
3158 s->img_n = c; 3201 s->img_n = c;
@@ -4033,16 +4076,23 @@ typedef struct
4033 stbi__zhuffman z_length, z_distance; 4076 stbi__zhuffman z_length, z_distance;
4034} stbi__zbuf; 4077} stbi__zbuf;
4035 4078
4079stbi_inline static int stbi__zeof(stbi__zbuf *z)
4080{
4081 return (z->zbuffer >= z->zbuffer_end);
4082}
4083
4036stbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z) 4084stbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z)
4037{ 4085{
4038 if (z->zbuffer >= z->zbuffer_end) return 0; 4086 return stbi__zeof(z) ? 0 : *z->zbuffer++;
4039 return *z->zbuffer++;
4040} 4087}
4041 4088
4042static void stbi__fill_bits(stbi__zbuf *z) 4089static void stbi__fill_bits(stbi__zbuf *z)
4043{ 4090{
4044 do { 4091 do {
4045 STBI_ASSERT(z->code_buffer < (1U << z->num_bits)); 4092 if (z->code_buffer >= (1U << z->num_bits)) {
4093 z->zbuffer = z->zbuffer_end; /* treat this as EOF so we fail. */
4094 return;
4095 }
4046 z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits; 4096 z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits;
4047 z->num_bits += 8; 4097 z->num_bits += 8;
4048 } while (z->num_bits <= 24); 4098 } while (z->num_bits <= 24);
@@ -4067,10 +4117,11 @@ static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z)
4067 for (s=STBI__ZFAST_BITS+1; ; ++s) 4117 for (s=STBI__ZFAST_BITS+1; ; ++s)
4068 if (k < z->maxcode[s]) 4118 if (k < z->maxcode[s])
4069 break; 4119 break;
4070 if (s == 16) return -1; // invalid code! 4120 if (s >= 16) return -1; // invalid code!
4071 // code size is s, so: 4121 // code size is s, so:
4072 b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s]; 4122 b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s];
4073 STBI_ASSERT(z->size[b] == s); 4123 if (b >= sizeof (z->size)) return -1; // some data was corrupt somewhere!
4124 if (z->size[b] != s) return -1; // was originally an assert, but report failure instead.
4074 a->code_buffer >>= s; 4125 a->code_buffer >>= s;
4075 a->num_bits -= s; 4126 a->num_bits -= s;
4076 return z->value[b]; 4127 return z->value[b];
@@ -4079,7 +4130,12 @@ static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z)
4079stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) 4130stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z)
4080{ 4131{
4081 int b,s; 4132 int b,s;
4082 if (a->num_bits < 16) stbi__fill_bits(a); 4133 if (a->num_bits < 16) {
4134 if (stbi__zeof(a)) {
4135 return -1; /* report error for unexpected end of data. */
4136 }
4137 stbi__fill_bits(a);
4138 }
4083 b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; 4139 b = z->fast[a->code_buffer & STBI__ZFAST_MASK];
4084 if (b) { 4140 if (b) {
4085 s = b >> 9; 4141 s = b >> 9;
@@ -4093,13 +4149,16 @@ stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z)
4093static int stbi__zexpand(stbi__zbuf *z, char *zout, int n) // need to make room for n bytes 4149static int stbi__zexpand(stbi__zbuf *z, char *zout, int n) // need to make room for n bytes
4094{ 4150{
4095 char *q; 4151 char *q;
4096 int cur, limit, old_limit; 4152 unsigned int cur, limit, old_limit;
4097 z->zout = zout; 4153 z->zout = zout;
4098 if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG"); 4154 if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG");
4099 cur = (int) (z->zout - z->zout_start); 4155 cur = (unsigned int) (z->zout - z->zout_start);
4100 limit = old_limit = (int) (z->zout_end - z->zout_start); 4156 limit = old_limit = (unsigned) (z->zout_end - z->zout_start);
4101 while (cur + n > limit) 4157 if (UINT_MAX - cur < (unsigned) n) return stbi__err("outofmem", "Out of memory");
4158 while (cur + n > limit) {
4159 if(limit > UINT_MAX / 2) return stbi__err("outofmem", "Out of memory");
4102 limit *= 2; 4160 limit *= 2;
4161 }
4103 q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit); 4162 q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit);
4104 STBI_NOTUSED(old_limit); 4163 STBI_NOTUSED(old_limit);
4105 if (q == NULL) return stbi__err("outofmem", "Out of memory"); 4164 if (q == NULL) return stbi__err("outofmem", "Out of memory");
@@ -4197,11 +4256,12 @@ static int stbi__compute_huffman_codes(stbi__zbuf *a)
4197 c = stbi__zreceive(a,2)+3; 4256 c = stbi__zreceive(a,2)+3;
4198 if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG"); 4257 if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG");
4199 fill = lencodes[n-1]; 4258 fill = lencodes[n-1];
4200 } else if (c == 17) 4259 } else if (c == 17) {
4201 c = stbi__zreceive(a,3)+3; 4260 c = stbi__zreceive(a,3)+3;
4202 else { 4261 } else if (c == 18) {
4203 STBI_ASSERT(c == 18);
4204 c = stbi__zreceive(a,7)+11; 4262 c = stbi__zreceive(a,7)+11;
4263 } else {
4264 return stbi__err("bad codelengths", "Corrupt PNG");
4205 } 4265 }
4206 if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG"); 4266 if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG");
4207 memset(lencodes+n, fill, c); 4267 memset(lencodes+n, fill, c);
@@ -4227,7 +4287,7 @@ static int stbi__parse_uncompressed_block(stbi__zbuf *a)
4227 a->code_buffer >>= 8; 4287 a->code_buffer >>= 8;
4228 a->num_bits -= 8; 4288 a->num_bits -= 8;
4229 } 4289 }
4230 STBI_ASSERT(a->num_bits == 0); 4290 if (a->num_bits < 0) return stbi__err("zlib corrupt","Corrupt PNG");
4231 // now fill header the normal way 4291 // now fill header the normal way
4232 while (k < 4) 4292 while (k < 4)
4233 header[k++] = stbi__zget8(a); 4293 header[k++] = stbi__zget8(a);
@@ -4249,6 +4309,7 @@ static int stbi__parse_zlib_header(stbi__zbuf *a)
4249 int cm = cmf & 15; 4309 int cm = cmf & 15;
4250 /* int cinfo = cmf >> 4; */ 4310 /* int cinfo = cmf >> 4; */
4251 int flg = stbi__zget8(a); 4311 int flg = stbi__zget8(a);
4312 if (stbi__zeof(a)) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec
4252 if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec 4313 if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec
4253 if (flg & 32) return stbi__err("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png 4314 if (flg & 32) return stbi__err("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png
4254 if (cm != 8) return stbi__err("bad compression","Corrupt PNG"); // DEFLATE required for png 4315 if (cm != 8) return stbi__err("bad compression","Corrupt PNG"); // DEFLATE required for png
@@ -4510,7 +4571,7 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r
4510 return stbi__err("invalid filter","Corrupt PNG"); 4571 return stbi__err("invalid filter","Corrupt PNG");
4511 4572
4512 if (depth < 8) { 4573 if (depth < 8) {
4513 STBI_ASSERT(img_width_bytes <= x); 4574 if (img_width_bytes > x) return stbi__err("invalid width","Corrupt PNG");
4514 cur += x*out_n - img_width_bytes; // store output to the rightmost img_len bytes, so we can decode in place 4575 cur += x*out_n - img_width_bytes; // store output to the rightmost img_len bytes, so we can decode in place
4515 filter_bytes = 1; 4576 filter_bytes = 1;
4516 width = img_width_bytes; 4577 width = img_width_bytes;
@@ -4905,8 +4966,10 @@ static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp)
4905 if (!first) return stbi__err("multiple IHDR","Corrupt PNG"); 4966 if (!first) return stbi__err("multiple IHDR","Corrupt PNG");
4906 first = 0; 4967 first = 0;
4907 if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG"); 4968 if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG");
4908 s->img_x = stbi__get32be(s); if (s->img_x > (1 << 24)) return stbi__err("too large","Very large image (corrupt?)"); 4969 s->img_x = stbi__get32be(s);
4909 s->img_y = stbi__get32be(s); if (s->img_y > (1 << 24)) return stbi__err("too large","Very large image (corrupt?)"); 4970 s->img_y = stbi__get32be(s);
4971 if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
4972 if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
4910 z->depth = stbi__get8(s); if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16) return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only"); 4973 z->depth = stbi__get8(s); if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16) return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only");
4911 color = stbi__get8(s); if (color > 6) return stbi__err("bad ctype","Corrupt PNG"); 4974 color = stbi__get8(s); if (color > 6) return stbi__err("bad ctype","Corrupt PNG");
4912 if (color == 3 && z->depth == 16) return stbi__err("bad ctype","Corrupt PNG"); 4975 if (color == 3 && z->depth == 16) return stbi__err("bad ctype","Corrupt PNG");
@@ -5055,10 +5118,12 @@ static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, st
5055 void *result=NULL; 5118 void *result=NULL;
5056 if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); 5119 if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error");
5057 if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) { 5120 if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) {
5058 if (p->depth < 8) 5121 if (p->depth <= 8)
5059 ri->bits_per_channel = 8; 5122 ri->bits_per_channel = 8;
5123 else if (p->depth == 16)
5124 ri->bits_per_channel = 16;
5060 else 5125 else
5061 ri->bits_per_channel = p->depth; 5126 return stbi__errpuc("bad bits_per_channel", "PNG not supported: unsupported color depth");
5062 result = p->out; 5127 result = p->out;
5063 p->out = NULL; 5128 p->out = NULL;
5064 if (req_comp && req_comp != p->s->img_out_n) { 5129 if (req_comp && req_comp != p->s->img_out_n) {
@@ -5219,6 +5284,8 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info)
5219 info->mr = info->mg = info->mb = info->ma = 0; 5284 info->mr = info->mg = info->mb = info->ma = 0;
5220 info->extra_read = 14; 5285 info->extra_read = 14;
5221 5286
5287 if (info->offset < 0) return stbi__errpuc("bad BMP", "bad BMP");
5288
5222 if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) return stbi__errpuc("unknown BMP", "BMP type not supported: unknown"); 5289 if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) return stbi__errpuc("unknown BMP", "BMP type not supported: unknown");
5223 if (hsz == 12) { 5290 if (hsz == 12) {
5224 s->img_x = stbi__get16le(s); 5291 s->img_x = stbi__get16le(s);
@@ -5310,6 +5377,9 @@ static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req
5310 flip_vertically = ((int) s->img_y) > 0; 5377 flip_vertically = ((int) s->img_y) > 0;
5311 s->img_y = abs((int) s->img_y); 5378 s->img_y = abs((int) s->img_y);
5312 5379
5380 if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
5381 if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
5382
5313 mr = info.mr; 5383 mr = info.mr;
5314 mg = info.mg; 5384 mg = info.mg;
5315 mb = info.mb; 5385 mb = info.mb;
@@ -5324,7 +5394,10 @@ static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req
5324 psize = (info.offset - info.extra_read - info.hsz) >> 2; 5394 psize = (info.offset - info.extra_read - info.hsz) >> 2;
5325 } 5395 }
5326 if (psize == 0) { 5396 if (psize == 0) {
5327 STBI_ASSERT(info.offset == (s->img_buffer - s->buffer_start)); 5397 STBI_ASSERT(info.offset == s->callback_already_read + (int) (s->img_buffer - s->img_buffer_original));
5398 if (info.offset != s->callback_already_read + (s->img_buffer - s->buffer_start)) {
5399 return stbi__errpuc("bad offset", "Corrupt BMP");
5400 }
5328 } 5401 }
5329 5402
5330 if (info.bpp == 24 && ma == 0xff000000) 5403 if (info.bpp == 24 && ma == 0xff000000)
@@ -5419,6 +5492,7 @@ static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req
5419 gshift = stbi__high_bit(mg)-7; gcount = stbi__bitcount(mg); 5492 gshift = stbi__high_bit(mg)-7; gcount = stbi__bitcount(mg);
5420 bshift = stbi__high_bit(mb)-7; bcount = stbi__bitcount(mb); 5493 bshift = stbi__high_bit(mb)-7; bcount = stbi__bitcount(mb);
5421 ashift = stbi__high_bit(ma)-7; acount = stbi__bitcount(ma); 5494 ashift = stbi__high_bit(ma)-7; acount = stbi__bitcount(ma);
5495 if (rcount > 8 || gcount > 8 || bcount > 8 || acount > 8) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); }
5422 } 5496 }
5423 for (j=0; j < (int) s->img_y; ++j) { 5497 for (j=0; j < (int) s->img_y; ++j) {
5424 if (easy) { 5498 if (easy) {
@@ -5643,6 +5717,9 @@ static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req
5643 STBI_NOTUSED(tga_x_origin); // @TODO 5717 STBI_NOTUSED(tga_x_origin); // @TODO
5644 STBI_NOTUSED(tga_y_origin); // @TODO 5718 STBI_NOTUSED(tga_y_origin); // @TODO
5645 5719
5720 if (tga_height > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
5721 if (tga_width > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
5722
5646 // do a tiny bit of precessing 5723 // do a tiny bit of precessing
5647 if ( tga_image_type >= 8 ) 5724 if ( tga_image_type >= 8 )
5648 { 5725 {
@@ -5682,6 +5759,11 @@ static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req
5682 // do I need to load a palette? 5759 // do I need to load a palette?
5683 if ( tga_indexed) 5760 if ( tga_indexed)
5684 { 5761 {
5762 if (tga_palette_len == 0) { /* you have to have at least one entry! */
5763 STBI_FREE(tga_data);
5764 return stbi__errpuc("bad palette", "Corrupt TGA");
5765 }
5766
5685 // any data to skip? (offset usually = 0) 5767 // any data to skip? (offset usually = 0)
5686 stbi__skip(s, tga_palette_start ); 5768 stbi__skip(s, tga_palette_start );
5687 // load the palette 5769 // load the palette
@@ -5890,6 +5972,9 @@ static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req
5890 h = stbi__get32be(s); 5972 h = stbi__get32be(s);
5891 w = stbi__get32be(s); 5973 w = stbi__get32be(s);
5892 5974
5975 if (h > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
5976 if (w > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
5977
5893 // Make sure the depth is 8 bits. 5978 // Make sure the depth is 8 bits.
5894 bitdepth = stbi__get16be(s); 5979 bitdepth = stbi__get16be(s);
5895 if (bitdepth != 8 && bitdepth != 16) 5980 if (bitdepth != 8 && bitdepth != 16)
@@ -6244,6 +6329,10 @@ static void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_c
6244 6329
6245 x = stbi__get16be(s); 6330 x = stbi__get16be(s);
6246 y = stbi__get16be(s); 6331 y = stbi__get16be(s);
6332
6333 if (y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
6334 if (x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
6335
6247 if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pic header)"); 6336 if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pic header)");
6248 if (!stbi__mad3sizes_valid(x, y, 4, 0)) return stbi__errpuc("too large", "PIC image too large to decode"); 6337 if (!stbi__mad3sizes_valid(x, y, 4, 0)) return stbi__errpuc("too large", "PIC image too large to decode");
6249 6338
@@ -6352,6 +6441,9 @@ static int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_in
6352 g->ratio = stbi__get8(s); 6441 g->ratio = stbi__get8(s);
6353 g->transparent = -1; 6442 g->transparent = -1;
6354 6443
6444 if (g->w > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
6445 if (g->h > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
6446
6355 if (comp != 0) *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the comments 6447 if (comp != 0) *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the comments
6356 6448
6357 if (is_info) return 1; 6449 if (is_info) return 1;
@@ -6529,7 +6621,7 @@ static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, i
6529 memset(g->history, 0x00, pcount); // pixels that were affected previous frame 6621 memset(g->history, 0x00, pcount); // pixels that were affected previous frame
6530 first_frame = 1; 6622 first_frame = 1;
6531 } else { 6623 } else {
6532 // second frame - how do we dispoase of the previous one? 6624 // second frame - how do we dispose of the previous one?
6533 dispose = (g->eflags & 0x1C) >> 2; 6625 dispose = (g->eflags & 0x1C) >> 2;
6534 pcount = g->w * g->h; 6626 pcount = g->w * g->h;
6535 6627
@@ -6683,6 +6775,8 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y,
6683 stbi_uc *two_back = 0; 6775 stbi_uc *two_back = 0;
6684 stbi__gif g; 6776 stbi__gif g;
6685 int stride; 6777 int stride;
6778 int out_size = 0;
6779 int delays_size = 0;
6686 memset(&g, 0, sizeof(g)); 6780 memset(&g, 0, sizeof(g));
6687 if (delays) { 6781 if (delays) {
6688 *delays = 0; 6782 *delays = 0;
@@ -6699,22 +6793,28 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y,
6699 stride = g.w * g.h * 4; 6793 stride = g.w * g.h * 4;
6700 6794
6701 if (out) { 6795 if (out) {
6702 void *tmp = (stbi_uc*) STBI_REALLOC( out, layers * stride ); 6796 void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride );
6703 if (NULL == tmp) { 6797 if (NULL == tmp) {
6704 STBI_FREE(g.out); 6798 STBI_FREE(g.out);
6705 STBI_FREE(g.history); 6799 STBI_FREE(g.history);
6706 STBI_FREE(g.background); 6800 STBI_FREE(g.background);
6707 return stbi__errpuc("outofmem", "Out of memory"); 6801 return stbi__errpuc("outofmem", "Out of memory");
6708 } 6802 }
6709 else 6803 else {
6710 out = (stbi_uc*) tmp; 6804 out = (stbi_uc*) tmp;
6805 out_size = layers * stride;
6806 }
6807
6711 if (delays) { 6808 if (delays) {
6712 *delays = (int*) STBI_REALLOC( *delays, sizeof(int) * layers ); 6809 *delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers );
6810 delays_size = layers * sizeof(int);
6713 } 6811 }
6714 } else { 6812 } else {
6715 out = (stbi_uc*)stbi__malloc( layers * stride ); 6813 out = (stbi_uc*)stbi__malloc( layers * stride );
6814 out_size = layers * stride;
6716 if (delays) { 6815 if (delays) {
6717 *delays = (int*) stbi__malloc( layers * sizeof(int) ); 6816 *delays = (int*) stbi__malloc( layers * sizeof(int) );
6817 delays_size = layers * sizeof(int);
6718 } 6818 }
6719 } 6819 }
6720 memcpy( out + ((layers - 1) * stride), u, stride ); 6820 memcpy( out + ((layers - 1) * stride), u, stride );
@@ -6893,6 +6993,9 @@ static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int re
6893 token += 3; 6993 token += 3;
6894 width = (int) strtol(token, NULL, 10); 6994 width = (int) strtol(token, NULL, 10);
6895 6995
6996 if (height > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)");
6997 if (width > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)");
6998
6896 *x = width; 6999 *x = width;
6897 *y = height; 7000 *y = height;
6898 7001
@@ -7207,6 +7310,9 @@ static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req
7207 if (!stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n)) 7310 if (!stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n))
7208 return 0; 7311 return 0;
7209 7312
7313 if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
7314 if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
7315
7210 *x = s->img_x; 7316 *x = s->img_x;
7211 *y = s->img_y; 7317 *y = s->img_y;
7212 if (comp) *comp = s->img_n; 7318 if (comp) *comp = s->img_n;
diff --git a/src/stb_truetype.h b/src/stb_truetype.h
index 935a6de2..62595a15 100644
--- a/src/stb_truetype.h
+++ b/src/stb_truetype.h
@@ -53,8 +53,8 @@
53// Johan Duparc Thomas Fields 53// Johan Duparc Thomas Fields
54// Hou Qiming Derek Vinyard 54// Hou Qiming Derek Vinyard
55// Rob Loach Cort Stratton 55// Rob Loach Cort Stratton
56// Kenney Phillis Jr. Brian Costabile 56// Kenney Phillis Jr. Brian Costabile
57// Ken Voskuil (kaesve) 57// Ken Voskuil (kaesve)
58// 58//
59// VERSION HISTORY 59// VERSION HISTORY
60// 60//
diff --git a/src/ui/color.c b/src/ui/color.c
index 7d6ab2c2..723e9805 100644
--- a/src/ui/color.c
+++ b/src/ui/color.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "color.h" 23#include "color.h"
2 24
3#include <the_Foundation/string.h> 25#include <the_Foundation/string.h>
@@ -62,6 +84,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
62 copy_(uiTextStrong_ColorId, white_ColorId); 84 copy_(uiTextStrong_ColorId, white_ColorId);
63 copy_(uiTextSelected_ColorId, white_ColorId); 85 copy_(uiTextSelected_ColorId, white_ColorId);
64 copy_(uiTextFramelessHover_ColorId, white_ColorId); 86 copy_(uiTextFramelessHover_ColorId, white_ColorId);
87 copy_(uiTextDisabled_ColorId, gray25_ColorId);
65 copy_(uiTextShortcut_ColorId, cyan_ColorId); 88 copy_(uiTextShortcut_ColorId, cyan_ColorId);
66 copy_(uiTextAction_ColorId, cyan_ColorId); 89 copy_(uiTextAction_ColorId, cyan_ColorId);
67 copy_(uiTextCaution_ColorId, orange_ColorId); 90 copy_(uiTextCaution_ColorId, orange_ColorId);
@@ -86,7 +109,8 @@ void setThemePalette_Color(enum iColorTheme theme) {
86 copy_(uiInputCursor_ColorId, orange_ColorId); 109 copy_(uiInputCursor_ColorId, orange_ColorId);
87 copy_(uiInputCursorText_ColorId, black_ColorId); 110 copy_(uiInputCursorText_ColorId, black_ColorId);
88 copy_(uiHeading_ColorId, cyan_ColorId); 111 copy_(uiHeading_ColorId, cyan_ColorId);
89 copy_(uiIcon_ColorId, teal_ColorId); 112 copy_(uiAnnotation_ColorId, teal_ColorId);
113 copy_(uiIcon_ColorId, cyan_ColorId);
90 copy_(uiIconHover_ColorId, cyan_ColorId); 114 copy_(uiIconHover_ColorId, cyan_ColorId);
91 copy_(uiSeparator_ColorId, gray25_ColorId); 115 copy_(uiSeparator_ColorId, gray25_ColorId);
92 copy_(uiMarked_ColorId, brown_ColorId); 116 copy_(uiMarked_ColorId, brown_ColorId);
@@ -103,6 +127,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
103 copy_(uiTextPressed_ColorId, black_ColorId); 127 copy_(uiTextPressed_ColorId, black_ColorId);
104 copy_(uiTextStrong_ColorId, white_ColorId); 128 copy_(uiTextStrong_ColorId, white_ColorId);
105 copy_(uiTextSelected_ColorId, white_ColorId); 129 copy_(uiTextSelected_ColorId, white_ColorId);
130 copy_(uiTextDisabled_ColorId, gray50_ColorId);
106 copy_(uiTextFramelessHover_ColorId, white_ColorId); 131 copy_(uiTextFramelessHover_ColorId, white_ColorId);
107 copy_(uiTextShortcut_ColorId, cyan_ColorId); 132 copy_(uiTextShortcut_ColorId, cyan_ColorId);
108 copy_(uiTextAction_ColorId, cyan_ColorId); 133 copy_(uiTextAction_ColorId, cyan_ColorId);
@@ -128,6 +153,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
128 copy_(uiInputCursor_ColorId, orange_ColorId); 153 copy_(uiInputCursor_ColorId, orange_ColorId);
129 copy_(uiInputCursorText_ColorId, black_ColorId); 154 copy_(uiInputCursorText_ColorId, black_ColorId);
130 copy_(uiHeading_ColorId, cyan_ColorId); 155 copy_(uiHeading_ColorId, cyan_ColorId);
156 copy_(uiAnnotation_ColorId, teal_ColorId);
131 copy_(uiIcon_ColorId, cyan_ColorId); 157 copy_(uiIcon_ColorId, cyan_ColorId);
132 copy_(uiIconHover_ColorId, cyan_ColorId); 158 copy_(uiIconHover_ColorId, cyan_ColorId);
133 copy_(uiSeparator_ColorId, black_ColorId); 159 copy_(uiSeparator_ColorId, black_ColorId);
@@ -141,9 +167,10 @@ void setThemePalette_Color(enum iColorTheme theme) {
141 copy_(uiBackgroundPressed_ColorId, cyan_ColorId); 167 copy_(uiBackgroundPressed_ColorId, cyan_ColorId);
142 copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId); 168 copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId);
143 copy_(uiText_ColorId, black_ColorId); 169 copy_(uiText_ColorId, black_ColorId);
144 copy_(uiTextStrong_ColorId, brown_ColorId); 170 copy_(uiTextStrong_ColorId, teal_ColorId);
145 copy_(uiTextPressed_ColorId, black_ColorId); 171 copy_(uiTextPressed_ColorId, black_ColorId);
146 copy_(uiTextSelected_ColorId, black_ColorId); 172 copy_(uiTextSelected_ColorId, black_ColorId);
173 copy_(uiTextDisabled_ColorId, gray50_ColorId);
147 copy_(uiTextFramelessHover_ColorId, black_ColorId); 174 copy_(uiTextFramelessHover_ColorId, black_ColorId);
148 copy_(uiTextShortcut_ColorId, brown_ColorId); 175 copy_(uiTextShortcut_ColorId, brown_ColorId);
149 copy_(uiTextAction_ColorId, brown_ColorId); 176 copy_(uiTextAction_ColorId, brown_ColorId);
@@ -163,15 +190,16 @@ void setThemePalette_Color(enum iColorTheme theme) {
163 copy_(uiInputBackgroundFocused_ColorId, white_ColorId); 190 copy_(uiInputBackgroundFocused_ColorId, white_ColorId);
164 copy_(uiInputText_ColorId, gray25_ColorId); 191 copy_(uiInputText_ColorId, gray25_ColorId);
165 copy_(uiInputTextFocused_ColorId, black_ColorId); 192 copy_(uiInputTextFocused_ColorId, black_ColorId);
166 copy_(uiInputFrame_ColorId, gray25_ColorId); 193 copy_(uiInputFrame_ColorId, gray50_ColorId);
167 copy_(uiInputFrameHover_ColorId, brown_ColorId); 194 copy_(uiInputFrameHover_ColorId, brown_ColorId);
168 copy_(uiInputFrameFocused_ColorId, teal_ColorId); 195 copy_(uiInputFrameFocused_ColorId, teal_ColorId);
169 copy_(uiInputCursor_ColorId, teal_ColorId); 196 copy_(uiInputCursor_ColorId, teal_ColorId);
170 copy_(uiInputCursorText_ColorId, white_ColorId); 197 copy_(uiInputCursorText_ColorId, white_ColorId);
171 copy_(uiHeading_ColorId, brown_ColorId); 198 copy_(uiHeading_ColorId, brown_ColorId);
199 copy_(uiAnnotation_ColorId, gray50_ColorId);
172 copy_(uiIcon_ColorId, brown_ColorId); 200 copy_(uiIcon_ColorId, brown_ColorId);
173 copy_(uiIconHover_ColorId, brown_ColorId); 201 copy_(uiIconHover_ColorId, brown_ColorId);
174 copy_(uiSeparator_ColorId, gray25_ColorId); 202 copy_(uiSeparator_ColorId, gray50_ColorId);
175 copy_(uiMarked_ColorId, cyan_ColorId); 203 copy_(uiMarked_ColorId, cyan_ColorId);
176 copy_(uiMatching_ColorId, orange_ColorId); 204 copy_(uiMatching_ColorId, orange_ColorId);
177 break; 205 break;
@@ -183,6 +211,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
183 copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId); 211 copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId);
184 copy_(uiText_ColorId, gray25_ColorId); 212 copy_(uiText_ColorId, gray25_ColorId);
185 copy_(uiTextPressed_ColorId, black_ColorId); 213 copy_(uiTextPressed_ColorId, black_ColorId);
214 copy_(uiTextDisabled_ColorId, gray75_ColorId);
186 copy_(uiTextStrong_ColorId, black_ColorId); 215 copy_(uiTextStrong_ColorId, black_ColorId);
187 copy_(uiTextSelected_ColorId, black_ColorId); 216 copy_(uiTextSelected_ColorId, black_ColorId);
188 copy_(uiTextFramelessHover_ColorId, black_ColorId); 217 copy_(uiTextFramelessHover_ColorId, black_ColorId);
@@ -210,13 +239,16 @@ void setThemePalette_Color(enum iColorTheme theme) {
210 copy_(uiInputCursor_ColorId, teal_ColorId); 239 copy_(uiInputCursor_ColorId, teal_ColorId);
211 copy_(uiInputCursorText_ColorId, white_ColorId); 240 copy_(uiInputCursorText_ColorId, white_ColorId);
212 copy_(uiHeading_ColorId, brown_ColorId); 241 copy_(uiHeading_ColorId, brown_ColorId);
242 copy_(uiAnnotation_ColorId, gray50_ColorId);
213 copy_(uiIcon_ColorId, brown_ColorId); 243 copy_(uiIcon_ColorId, brown_ColorId);
214 copy_(uiIconHover_ColorId, brown_ColorId); 244 copy_(uiIconHover_ColorId, brown_ColorId);
215 copy_(uiSeparator_ColorId, gray50_ColorId); 245 copy_(uiSeparator_ColorId, gray75_ColorId);
216 copy_(uiMarked_ColorId, cyan_ColorId); 246 copy_(uiMarked_ColorId, cyan_ColorId);
217 copy_(uiMatching_ColorId, orange_ColorId); 247 copy_(uiMatching_ColorId, orange_ColorId);
218 break; 248 break;
219 } 249 }
250 palette_[uiMarked_ColorId].a = 128;
251 palette_[uiMatching_ColorId].a = 128;
220} 252}
221 253
222iColor get_Color(int color) { 254iColor get_Color(int color) {
@@ -233,6 +265,14 @@ void set_Color(int color, iColor rgba) {
233 } 265 }
234} 266}
235 267
268iColor mix_Color(iColor c1, iColor c2, float t) {
269 t = iClamp(t, 0.0f, 1.0f);
270 return (iColor){ c1.r * (1 - t) + c2.r * t,
271 c1.g * (1 - t) + c2.g * t,
272 c1.b * (1 - t) + c2.b * t,
273 c1.a * (1 - t) + c2.a * t };
274}
275
236iLocalDef iBool equal_Color_(const iColor *x, const iColor *y) { 276iLocalDef iBool equal_Color_(const iColor *x, const iColor *y) {
237 return memcmp(x, y, sizeof(iColor)) == 0; 277 return memcmp(x, y, sizeof(iColor)) == 0;
238} 278}
@@ -361,7 +401,8 @@ const char *escape_Color(int color) {
361 if (color >= 0 && color < (int) iElemCount(esc)) { 401 if (color >= 0 && color < (int) iElemCount(esc)) {
362 return esc[color]; 402 return esc[color];
363 } 403 }
364 return format_CStr("\r%c", color + '0'); 404 iAssert(asciiBase_ColorEscape + color <= 127);
405 return format_CStr("\r%c", asciiBase_ColorEscape + color);
365} 406}
366 407
367iHSLColor setSat_HSLColor(iHSLColor d, float sat) { 408iHSLColor setSat_HSLColor(iHSLColor d, float sat) {
diff --git a/src/ui/color.h b/src/ui/color.h
index 596fec7a..76e5b2a7 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/range.h> 25#include <the_Foundation/range.h>
@@ -46,6 +68,7 @@ enum iColorId {
46 uiText_ColorId, 68 uiText_ColorId,
47 uiTextPressed_ColorId, 69 uiTextPressed_ColorId,
48 uiTextSelected_ColorId, 70 uiTextSelected_ColorId,
71 uiTextDisabled_ColorId,
49 uiTextFramelessHover_ColorId, 72 uiTextFramelessHover_ColorId,
50 uiTextFramelessSelected_ColorId, 73 uiTextFramelessSelected_ColorId,
51 uiTextStrong_ColorId, 74 uiTextStrong_ColorId,
@@ -73,6 +96,7 @@ enum iColorId {
73 uiInputCursor_ColorId, 96 uiInputCursor_ColorId,
74 uiInputCursorText_ColorId, 97 uiInputCursorText_ColorId,
75 uiHeading_ColorId, 98 uiHeading_ColorId,
99 uiAnnotation_ColorId,
76 uiIcon_ColorId, 100 uiIcon_ColorId,
77 uiIconHover_ColorId, 101 uiIconHover_ColorId,
78 uiSeparator_ColorId, 102 uiSeparator_ColorId,
@@ -139,24 +163,26 @@ iLocalDef iBool isRegularText_ColorId(enum iColorId d) {
139#define mask_ColorId 0x7f 163#define mask_ColorId 0x7f
140#define permanent_ColorId 0x80 /* cannot be changed via escapes */ 164#define permanent_ColorId 0x80 /* cannot be changed via escapes */
141 165
142#define black_ColorEscape "\r0" 166#define asciiBase_ColorEscape 33
143#define gray25_ColorEscape "\r1" 167
144#define gray50_ColorEscape "\r2" 168#define black_ColorEscape "\r!"
145#define gray75_ColorEscape "\r3" 169#define gray25_ColorEscape "\r\""
146#define white_ColorEscape "\r4" 170#define gray50_ColorEscape "\r#"
147#define brown_ColorEscape "\r5" 171#define gray75_ColorEscape "\r$"
148#define orange_ColorEscape "\r6" 172#define white_ColorEscape "\r%"
149#define teal_ColorEscape "\r7" 173#define brown_ColorEscape "\r&"
150#define cyan_ColorEscape "\r8" 174#define orange_ColorEscape "\r'"
151#define yellow_ColorEscape "\r9" 175#define teal_ColorEscape "\r("
152#define red_ColorEscape "\r:" 176#define cyan_ColorEscape "\r)"
153#define magenta_ColorEscape "\r;" 177#define yellow_ColorEscape "\r*"
154#define blue_ColorEscape "\r<" 178#define red_ColorEscape "\r+"
155#define green_ColorEscape "\r=" 179#define magenta_ColorEscape "\r,"
156#define uiText_ColorEscape "\rC" 180#define blue_ColorEscape "\r-"
157#define uiTextAction_ColorEscape "\rJ" 181#define green_ColorEscape "\r."
158#define uiTextCaution_ColorEscape "\rK" 182#define uiText_ColorEscape "\r4"
159#define uiHeading_ColorEscape "\r`" 183#define uiTextAction_ColorEscape "\r<"
184#define uiTextCaution_ColorEscape "\r="
185#define uiHeading_ColorEscape "\rR"
160 186
161iDeclareType(Color) 187iDeclareType(Color)
162iDeclareType(HSLColor) 188iDeclareType(HSLColor)
@@ -180,6 +206,7 @@ iColor get_Color (int color);
180int darker_Color (int color); 206int darker_Color (int color);
181int lighter_Color (int color); 207int lighter_Color (int color);
182void set_Color (int color, iColor rgba); 208void set_Color (int color, iColor rgba);
209iColor mix_Color (iColor c1, iColor c2, float t);
183 210
184iLocalDef void setHsl_Color(int color, iHSLColor hsl) { 211iLocalDef void setHsl_Color(int color, iHSLColor hsl) {
185 set_Color(color, rgb_HSLColor(hsl)); 212 set_Color(color, rgb_HSLColor(hsl));
diff --git a/src/ui/command.c b/src/ui/command.c
index 19228e46..cf8c7032 100644
--- a/src/ui/command.c
+++ b/src/ui/command.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "command.h" 23#include "command.h"
2#include "app.h" 24#include "app.h"
3 25
diff --git a/src/ui/command.h b/src/ui/command.h
index 2124d527..4798baa6 100644
--- a/src/ui/command.h
+++ b/src/ui/command.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/vec2.h> 25#include <the_Foundation/vec2.h>
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 29803252..6ca6e4c2 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "documentwidget.h" 23#include "documentwidget.h"
2#include "scrollwidget.h" 24#include "scrollwidget.h"
3#include "inputwidget.h" 25#include "inputwidget.h"
@@ -105,6 +127,43 @@ iDefineTypeConstruction(Model)
105 127
106/*----------------------------------------------------------------------------------------------*/ 128/*----------------------------------------------------------------------------------------------*/
107 129
130iDeclareType(VisBuffer)
131iDeclareTypeConstruction(VisBuffer)
132
133struct Impl_VisBuffer {
134 SDL_Texture * texture[2];
135 int index;
136 iInt2 size;
137 iRangei validRange;
138};
139
140void init_VisBuffer(iVisBuffer *d) {
141 iZap(*d);
142}
143
144void deinit_VisBuffer(iVisBuffer *d) {
145 iForIndices(i, d->texture) {
146 if (d->texture[i]) {
147 SDL_DestroyTexture(d->texture[i]);
148 }
149 }
150}
151
152void dealloc_VisBuffer(iVisBuffer *d) {
153 d->size = zero_I2();
154 iZap(d->validRange);
155 iForIndices(i, d->texture) {
156 SDL_DestroyTexture(d->texture[i]);
157 d->texture[i] = NULL;
158 }
159}
160
161iDefineTypeConstruction(VisBuffer)
162
163/*----------------------------------------------------------------------------------------------*/
164
165static const int smoothSpeed_DocumentWidget_ = 120; /* unit: gap_Text per second */
166
108enum iRequestState { 167enum iRequestState {
109 blank_RequestState, 168 blank_RequestState,
110 fetching_RequestState, 169 fetching_RequestState,
@@ -124,26 +183,26 @@ struct Impl_DocumentWidget {
124 int certFlags; 183 int certFlags;
125 iDate certExpiry; 184 iDate certExpiry;
126 iString * certSubject; 185 iString * certSubject;
186 int redirectCount;
127 iBool selecting; 187 iBool selecting;
128 iRangecc selectMark; 188 iRangecc selectMark;
129 iRangecc foundMark; 189 iRangecc foundMark;
130 int pageMargin; 190 int pageMargin;
131 iPtrArray visibleLinks; 191 iPtrArray visibleLinks;
132 const iGmRun * hoverLink; 192 const iGmRun * hoverLink;
193 const iGmRun * contextLink;
133 iBool noHoverWhileScrolling; 194 iBool noHoverWhileScrolling;
134 iBool showLinkNumbers; 195 iBool showLinkNumbers;
135 iClick click; 196 iClick click;
136 float initNormScrollY; 197 float initNormScrollY;
137 int scrollY; 198 int scrollY;
138 iScrollWidget *scroll; 199 iScrollWidget *scroll;
200 int smoothScroll;
201 int smoothSpeed;
202 int smoothLastOffset;
203 iBool smoothContinue;
139 iWidget * menu; 204 iWidget * menu;
140 SDL_Cursor * arrowCursor; /* TODO: cursors belong in Window */ 205 iVisBuffer * visBuffer;
141 SDL_Cursor * beamCursor;
142 SDL_Cursor * handCursor;
143 SDL_Texture * visBuffer[2];
144 int visBufferIndex;
145 iInt2 visBufferSize;
146 iRangei visBufferValidRange;
147}; 206};
148 207
149iDefineObjectConstruction(DocumentWidget) 208iDefineObjectConstruction(DocumentWidget)
@@ -163,22 +222,22 @@ void init_DocumentWidget(iDocumentWidget *d) {
163 d->isRequestUpdated = iFalse; 222 d->isRequestUpdated = iFalse;
164 d->media = new_ObjectList(); 223 d->media = new_ObjectList();
165 d->doc = new_GmDocument(); 224 d->doc = new_GmDocument();
225 d->redirectCount = 0;
166 d->initNormScrollY = 0; 226 d->initNormScrollY = 0;
167 d->scrollY = 0; 227 d->scrollY = 0;
228 d->smoothScroll = 0;
229 d->smoothSpeed = 0;
230 d->smoothLastOffset = 0;
231 d->smoothContinue = iFalse;
168 d->selecting = iFalse; 232 d->selecting = iFalse;
169 d->selectMark = iNullRange; 233 d->selectMark = iNullRange;
170 d->foundMark = iNullRange; 234 d->foundMark = iNullRange;
171 d->pageMargin = 5; 235 d->pageMargin = 5;
172 d->hoverLink = NULL; 236 d->hoverLink = NULL;
237 d->contextLink = NULL;
173 d->noHoverWhileScrolling = iFalse; 238 d->noHoverWhileScrolling = iFalse;
174 d->showLinkNumbers = iFalse; 239 d->showLinkNumbers = iFalse;
175 iZap(d->visBuffer); 240 d->visBuffer = new_VisBuffer();
176 d->visBufferIndex = 0;
177 d->visBufferSize = zero_I2();
178 iZap(d->visBufferValidRange);
179 d->arrowCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
180 d->beamCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM);
181 d->handCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND);
182 init_PtrArray(&d->visibleLinks); 241 init_PtrArray(&d->visibleLinks);
183 init_Click(&d->click, d, SDL_BUTTON_LEFT); 242 init_Click(&d->click, d, SDL_BUTTON_LEFT);
184 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 243 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
@@ -197,18 +256,23 @@ void init_DocumentWidget(iDocumentWidget *d) {
197} 256}
198 257
199void deinit_DocumentWidget(iDocumentWidget *d) { 258void deinit_DocumentWidget(iDocumentWidget *d) {
259 delete_VisBuffer(d->visBuffer);
200 iRelease(d->media); 260 iRelease(d->media);
201 iRelease(d->request); 261 iRelease(d->request);
202 iRelease(d->doc); 262 iRelease(d->doc);
203 deinit_PtrArray(&d->visibleLinks); 263 deinit_PtrArray(&d->visibleLinks);
204 delete_String(d->certSubject); 264 delete_String(d->certSubject);
205 delete_String(d->titleUser); 265 delete_String(d->titleUser);
206 SDL_FreeCursor(d->arrowCursor);
207 SDL_FreeCursor(d->beamCursor);
208 SDL_FreeCursor(d->handCursor);
209 deinit_Model(&d->mod); 266 deinit_Model(&d->mod);
210} 267}
211 268
269static void resetSmoothScroll_DocumentWidget_(iDocumentWidget *d) {
270 d->smoothSpeed = 0;
271 d->smoothScroll = 0;
272 d->smoothLastOffset = 0;
273 d->smoothContinue = iFalse;
274}
275
212static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { 276static int documentWidth_DocumentWidget_(const iDocumentWidget *d) {
213 const iWidget *w = constAs_Widget(d); 277 const iWidget *w = constAs_Widget(d);
214 const iRect bounds = bounds_Widget(w); 278 const iRect bounds = bounds_Widget(w);
@@ -242,9 +306,11 @@ iLocalDef int documentToWindowY_DocumentWidget_(const iDocumentWidget *d, int do
242 return docY - d->scrollY + documentBounds_DocumentWidget_(d).pos.y; 306 return docY - d->scrollY + documentBounds_DocumentWidget_(d).pos.y;
243} 307}
244 308
309#if 0
245iLocalDef int windowToDocumentY_DocumentWidget_(const iDocumentWidget *d, int localY) { 310iLocalDef int windowToDocumentY_DocumentWidget_(const iDocumentWidget *d, int localY) {
246 return localY + d->scrollY - documentBounds_DocumentWidget_(d).pos.y; 311 return localY + d->scrollY - documentBounds_DocumentWidget_(d).pos.y;
247} 312}
313#endif
248 314
249static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { 315static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) {
250 return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY); 316 return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY);
@@ -291,15 +357,16 @@ static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) {
291 357
292static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { 358static int scrollMax_DocumentWidget_(const iDocumentWidget *d) {
293 return size_GmDocument(d->doc).y - height_Rect(bounds_Widget(constAs_Widget(d))) + 359 return size_GmDocument(d->doc).y - height_Rect(bounds_Widget(constAs_Widget(d))) +
294 2 * d->pageMargin * gap_UI; 360 (hasSiteBanner_GmDocument(d->doc) ? 1 : 2) * d->pageMargin * gap_UI;
295} 361}
296 362
297static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { 363static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
298 const iRect docBounds = documentBounds_DocumentWidget_(d); 364 const iWidget *w = constAs_Widget(d);
299 const iGmRun *oldHoverLink = d->hoverLink; 365 const iRect docBounds = documentBounds_DocumentWidget_(d);
300 d->hoverLink = NULL; 366 const iGmRun * oldHoverLink = d->hoverLink;
301 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY); 367 d->hoverLink = NULL;
302 if (!d->noHoverWhileScrolling && 368 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY);
369 if (isHover_Widget(w) && !d->noHoverWhileScrolling &&
303 (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { 370 (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) {
304 iConstForEach(PtrArray, i, &d->visibleLinks) { 371 iConstForEach(PtrArray, i, &d->visibleLinks) {
305 const iGmRun *run = i.ptr; 372 const iGmRun *run = i.ptr;
@@ -312,12 +379,9 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
312 if (d->hoverLink != oldHoverLink) { 379 if (d->hoverLink != oldHoverLink) {
313 refresh_Widget(as_Widget(d)); 380 refresh_Widget(as_Widget(d));
314 } 381 }
315 if (!contains_Widget(constAs_Widget(d), mouse) || 382 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) {
316 contains_Widget(constAs_Widget(d->scroll), mouse)) { 383 setCursor_Window(get_Window(),
317 SDL_SetCursor(d->arrowCursor); 384 d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM);
318 }
319 else {
320 SDL_SetCursor(d->hoverLink ? d->handCursor : d->beamCursor);
321 } 385 }
322} 386}
323 387
@@ -424,38 +488,52 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
424 } 488 }
425} 489}
426 490
491static void invalidate_DocumentWidget_(iDocumentWidget *d) {
492 iZap(d->visBuffer->validRange);
493}
494
427static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) { 495static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) {
428 setUrl_GmDocument(d->doc, d->mod.url); 496 setUrl_GmDocument(d->doc, d->mod.url);
429 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); 497 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d));
430 d->foundMark = iNullRange; 498 d->foundMark = iNullRange;
431 d->selectMark = iNullRange; 499 d->selectMark = iNullRange;
432 d->hoverLink = NULL; 500 d->hoverLink = NULL;
501 d->contextLink = NULL;
433 updateWindowTitle_DocumentWidget_(d); 502 updateWindowTitle_DocumentWidget_(d);
434 updateVisible_DocumentWidget_(d); 503 updateVisible_DocumentWidget_(d);
504 invalidate_DocumentWidget_(d);
435 refresh_Widget(as_Widget(d)); 505 refresh_Widget(as_Widget(d));
436} 506}
437 507
438static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code) { 508static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code,
509 const iString *meta) {
439 iString *src = collectNewCStr_String("# "); 510 iString *src = collectNewCStr_String("# ");
440 const iGmError *msg = get_GmError(code); 511 const iGmError *msg = get_GmError(code);
441 appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */ 512 appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */
442 appendFormat_String(src, " %s\n%s", msg->title, msg->info); 513 appendFormat_String(src, " %s\n%s", msg->title, msg->info);
443 switch (code) { 514 if (meta) {
444 case failedToOpenFile_GmStatusCode: 515 switch (code) {
445 case certificateNotValid_GmStatusCode: 516 case nonGeminiRedirect_GmStatusCode:
446 appendFormat_String(src, "\n\n%s", cstr_String(meta_GmRequest(d->request))); 517 case tooManyRedirects_GmStatusCode:
447 break; 518 appendFormat_String(src, "\n=> %s\n", cstr_String(meta));
448 case unsupportedMimeType_GmStatusCode: 519 break;
449 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta_GmRequest(d->request))); 520 case failedToOpenFile_GmStatusCode:
450 break; 521 case certificateNotValid_GmStatusCode:
451 case slowDown_GmStatusCode: 522 appendFormat_String(src, "\n\n%s", cstr_String(meta));
452 appendFormat_String(src, "\n\nWait %s seconds before your next request.", 523 break;
453 cstr_String(meta_GmRequest(d->request))); 524 case unsupportedMimeType_GmStatusCode:
454 break; 525 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta));
455 default: 526 break;
456 break; 527 case slowDown_GmStatusCode:
528 appendFormat_String(src, "\n\nWait %s seconds before your next request.",
529 cstr_String(meta));
530 break;
531 default:
532 break;
533 }
457 } 534 }
458 setSource_DocumentWidget_(d, src); 535 setSource_DocumentWidget_(d, src);
536 resetSmoothScroll_DocumentWidget_(d);
459 d->scrollY = 0; 537 d->scrollY = 0;
460 d->state = ready_RequestState; 538 d->state = ready_RequestState;
461} 539}
@@ -470,10 +548,6 @@ static void updateTheme_DocumentWidget_(iDocumentWidget *d) {
470 } 548 }
471} 549}
472 550
473static void invalidate_DocumentWidget_(iDocumentWidget *d) {
474 iZap(d->visBufferValidRange);
475}
476
477static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { 551static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) {
478 if (d->state == ready_RequestState) { 552 if (d->state == ready_RequestState) {
479 return; 553 return;
@@ -493,16 +567,16 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
493 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ 567 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */
494 iRangecc mime = range_String(mimeStr); 568 iRangecc mime = range_String(mimeStr);
495 iRangecc seg = iNullRange; 569 iRangecc seg = iNullRange;
496 while (nextSplit_Rangecc(&mime, ";", &seg)) { 570 while (nextSplit_Rangecc(mime, ";", &seg)) {
497 iRangecc param = seg; 571 iRangecc param = seg;
498 trim_Rangecc(&param); 572 trim_Rangecc(&param);
499 if (equal_Rangecc(&param, "text/plain")) { 573 if (equal_Rangecc(param, "text/plain")) {
500 docFormat = plainText_GmDocumentFormat; 574 docFormat = plainText_GmDocumentFormat;
501 } 575 }
502 else if (equal_Rangecc(&param, "text/gemini")) { 576 else if (equal_Rangecc(param, "text/gemini")) {
503 docFormat = gemini_GmDocumentFormat; 577 docFormat = gemini_GmDocumentFormat;
504 } 578 }
505 else if (startsWith_Rangecc(&param, "image/")) { 579 else if (startsWith_Rangecc(param, "image/")) {
506 docFormat = gemini_GmDocumentFormat; 580 docFormat = gemini_GmDocumentFormat;
507 if (!d->request || isFinished_GmRequest(d->request)) { 581 if (!d->request || isFinished_GmRequest(d->request)) {
508 /* Make a simple document with an image. */ 582 /* Make a simple document with an image. */
@@ -521,7 +595,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
521 clear_String(&str); 595 clear_String(&str);
522 } 596 }
523 } 597 }
524 else if (startsWith_Rangecc(&param, "charset=")) { 598 else if (startsWith_Rangecc(param, "charset=")) {
525 charset = (iRangecc){ param.start + 8, param.end }; 599 charset = (iRangecc){ param.start + 8, param.end };
526 /* Remove whitespace and quotes. */ 600 /* Remove whitespace and quotes. */
527 trim_Rangecc(&charset); 601 trim_Rangecc(&charset);
@@ -532,12 +606,12 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
532 } 606 }
533 } 607 }
534 if (docFormat == undefined_GmDocumentFormat) { 608 if (docFormat == undefined_GmDocumentFormat) {
535 showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode); 609 showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode, &response->meta);
536 deinit_String(&str); 610 deinit_String(&str);
537 return; 611 return;
538 } 612 }
539 /* Convert the source to UTF-8 if needed. */ 613 /* Convert the source to UTF-8 if needed. */
540 if (!equalCase_Rangecc(&charset, "utf-8")) { 614 if (!equalCase_Rangecc(charset, "utf-8")) {
541 set_String(&str, 615 set_String(&str,
542 collect_String(decode_Block(&str.chars, cstr_Rangecc(charset)))); 616 collect_String(decode_Block(&str.chars, cstr_Rangecc(charset))));
543 } 617 }
@@ -609,6 +683,7 @@ static void parseUser_DocumentWidget_(iDocumentWidget *d) {
609 iRegExp *userPats[2] = { new_RegExp("~([^/?]+)", 0), 683 iRegExp *userPats[2] = { new_RegExp("~([^/?]+)", 0),
610 new_RegExp("/users/([^/?]+)", caseInsensitive_RegExpOption) }; 684 new_RegExp("/users/([^/?]+)", caseInsensitive_RegExpOption) };
611 iRegExpMatch m; 685 iRegExpMatch m;
686 init_RegExpMatch(&m);
612 iForIndices(i, userPats) { 687 iForIndices(i, userPats) {
613 if (matchString_RegExp(userPats[i], d->mod.url, &m)) { 688 if (matchString_RegExp(userPats[i], d->mod.url, &m)) {
614 setRange_String(d->titleUser, capturedRange_RegExpMatch(&m, 1)); 689 setRange_String(d->titleUser, capturedRange_RegExpMatch(&m, 1));
@@ -621,6 +696,8 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
621 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); 696 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url);
622 if (recent && recent->cachedResponse) { 697 if (recent && recent->cachedResponse) {
623 const iGmResponse *resp = recent->cachedResponse; 698 const iGmResponse *resp = recent->cachedResponse;
699 clear_ObjectList(d->media);
700 reset_GmDocument(d->doc);
624 d->state = fetching_RequestState; 701 d->state = fetching_RequestState;
625 d->initNormScrollY = recent->normScrollY; 702 d->initNormScrollY = recent->normScrollY;
626 /* Use the cached response data. */ 703 /* Use the cached response data. */
@@ -676,6 +753,10 @@ void setInitialScroll_DocumentWidget(iDocumentWidget *d, float normScrollY) {
676 d->initNormScrollY = normScrollY; 753 d->initNormScrollY = normScrollY;
677} 754}
678 755
756void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) {
757 d->redirectCount = count;
758}
759
679iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { 760iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) {
680 return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState; 761 return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState;
681} 762}
@@ -696,6 +777,37 @@ static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) {
696 refresh_Widget(as_Widget(d)); 777 refresh_Widget(as_Widget(d));
697} 778}
698 779
780static void doScroll_DocumentWidget_(iAny *ptr) {
781 iDocumentWidget *d = ptr;
782 if (!d->smoothScroll) return; /* was cancelled */
783 const double elapsed = (double) elapsedSinceLastTicker_App() / 1000.0;
784 int delta = d->smoothSpeed * elapsed * iSign(d->smoothScroll);
785 if (iAbs(d->smoothScroll) <= iAbs(delta)) {
786 if (d->smoothContinue) {
787 d->smoothScroll += d->smoothLastOffset;
788 }
789 else {
790 delta = d->smoothScroll;
791 }
792 }
793 scroll_DocumentWidget_(d, delta);
794 d->smoothScroll -= delta;
795 if (d->smoothScroll != 0) {
796 addTicker_App(doScroll_DocumentWidget_, d);
797 }
798}
799
800static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int speed) {
801 if (speed == 0) {
802 scroll_DocumentWidget_(d, offset);
803 return;
804 }
805 d->smoothSpeed = speed;
806 d->smoothScroll += offset;
807 d->smoothLastOffset = offset;
808 addTicker_App(doScroll_DocumentWidget_, d);
809}
810
699static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { 811static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) {
700 d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 : 812 d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 :
701 lineHeight_Text(paragraph_FontId)); 813 lineHeight_Text(paragraph_FontId));
@@ -733,32 +845,43 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
733 } 845 }
734 case categorySuccess_GmStatusCode: 846 case categorySuccess_GmStatusCode:
735 d->scrollY = 0; 847 d->scrollY = 0;
848 resetSmoothScroll_DocumentWidget_(d);
736 reset_GmDocument(d->doc); /* new content incoming */ 849 reset_GmDocument(d->doc); /* new content incoming */
737 updateDocument_DocumentWidget_(d, response_GmRequest(d->request)); 850 updateDocument_DocumentWidget_(d, response_GmRequest(d->request));
738 break; 851 break;
739 case categoryRedirect_GmStatusCode: 852 case categoryRedirect_GmStatusCode:
740 if (isEmpty_String(meta_GmRequest(d->request))) { 853 if (isEmpty_String(meta_GmRequest(d->request))) {
741 showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode); 854 showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode, NULL);
742 } 855 }
743 else { 856 else {
744 /* TODO: only accept redirects that use gemini protocol */ 857 /* Only accept redirects that use gemini scheme. */
745 postCommandf_App( 858 const iString *dstUrl = absoluteUrl_String(d->mod.url, meta_GmRequest(d->request));
746 "open redirect:1 url:%s", 859 if (d->redirectCount >= 5) {
747 cstr_String(absoluteUrl_String(d->mod.url, meta_GmRequest(d->request)))); 860 showErrorPage_DocumentWidget_(d, tooManyRedirects_GmStatusCode, dstUrl);
861 }
862 else if (equalCase_Rangecc(urlScheme_String(dstUrl), "gemini")) {
863 postCommandf_App(
864 "open redirect:%d url:%s", d->redirectCount + 1, cstr_String(dstUrl));
865 }
866 else {
867 showErrorPage_DocumentWidget_(d, nonGeminiRedirect_GmStatusCode, dstUrl);
868 }
748 iReleasePtr(&d->request); 869 iReleasePtr(&d->request);
749 } 870 }
750 break; 871 break;
751 default: 872 default:
752 if (isDefined_GmError(statusCode)) { 873 if (isDefined_GmError(statusCode)) {
753 showErrorPage_DocumentWidget_(d, statusCode); 874 showErrorPage_DocumentWidget_(d, statusCode, meta_GmRequest(d->request));
754 } 875 }
755 else if (category_GmStatusCode(statusCode) == 876 else if (category_GmStatusCode(statusCode) ==
756 categoryTemporaryFailure_GmStatusCode) { 877 categoryTemporaryFailure_GmStatusCode) {
757 showErrorPage_DocumentWidget_(d, temporaryFailure_GmStatusCode); 878 showErrorPage_DocumentWidget_(
879 d, temporaryFailure_GmStatusCode, meta_GmRequest(d->request));
758 } 880 }
759 else if (category_GmStatusCode(statusCode) == 881 else if (category_GmStatusCode(statusCode) ==
760 categoryPermanentFailure_GmStatusCode) { 882 categoryPermanentFailure_GmStatusCode) {
761 showErrorPage_DocumentWidget_(d, permanentFailure_GmStatusCode); 883 showErrorPage_DocumentWidget_(
884 d, permanentFailure_GmStatusCode, meta_GmRequest(d->request));
762 } 885 }
763 break; 886 break;
764 } 887 }
@@ -872,32 +995,33 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
872 return iFalse; 995 return iFalse;
873} 996}
874 997
875static void deallocVisBuffer_DocumentWidget_(iDocumentWidget *d) { 998static void deallocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
876 d->visBufferSize = zero_I2(); 999 d->visBuffer->size = zero_I2();
877 iZap(d->visBufferValidRange); 1000 iZap(d->visBuffer->validRange);
878 iForIndices(i, d->visBuffer) { 1001 iForIndices(i, d->visBuffer->texture) {
879 SDL_DestroyTexture(d->visBuffer[i]); 1002 SDL_DestroyTexture(d->visBuffer->texture[i]);
880 d->visBuffer[i] = NULL; 1003 d->visBuffer->texture[i] = NULL;
881 } 1004 }
882} 1005}
883 1006
884static void allocVisBuffer_DocumentWidget_(iDocumentWidget *d) { 1007static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
885 iWidget *w = as_Widget(d); 1008 const iWidget *w = constAs_Widget(d);
886 const iBool isVisible = isVisible_Widget(w); 1009 const iBool isVisible = isVisible_Widget(w);
887 const iInt2 size = bounds_Widget(w).size; 1010 const iInt2 size = bounds_Widget(w).size;
888 if (!isEqual_I2(size, d->visBufferSize) || !isVisible) { 1011 if (!isEqual_I2(size, d->visBuffer->size) || !isVisible) {
889 deallocVisBuffer_DocumentWidget_(d); 1012 dealloc_VisBuffer(d->visBuffer);
890 } 1013 }
891 if (isVisible && !d->visBuffer[0]) { 1014 if (isVisible && !d->visBuffer->texture[0]) {
892 iZap(d->visBufferValidRange); 1015 iZap(d->visBuffer->validRange);
893 d->visBufferSize = size; 1016 d->visBuffer->size = size;
894 iForIndices(i, d->visBuffer) { 1017 iForIndices(i, d->visBuffer->texture) {
895 d->visBuffer[i] = SDL_CreateTexture(renderer_Window(get_Window()), 1018 d->visBuffer->texture[i] =
896 SDL_PIXELFORMAT_RGBA8888, 1019 SDL_CreateTexture(renderer_Window(get_Window()),
897 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, 1020 SDL_PIXELFORMAT_RGBA8888,
898 size.x, 1021 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
899 size.y); 1022 size.x,
900 SDL_SetTextureBlendMode(d->visBuffer[i], SDL_BLENDMODE_NONE); 1023 size.y);
1024 SDL_SetTextureBlendMode(d->visBuffer->texture[i], SDL_BLENDMODE_NONE);
901 } 1025 }
902 } 1026 }
903} 1027}
@@ -973,7 +1097,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
973 : "Not trusted")); 1097 : "Not trusted"));
974 return iTrue; 1098 return iTrue;
975 } 1099 }
976 else if (equal_Command(cmd, "copy") && document_App() == d) { 1100 else if (equal_Command(cmd, "copy") && document_App() == d && !focus_Widget()) {
977 iString *copied; 1101 iString *copied;
978 if (d->selectMark.start) { 1102 if (d->selectMark.start) {
979 iRangecc mark = d->selectMark; 1103 iRangecc mark = d->selectMark;
@@ -990,10 +1114,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
990 delete_String(copied); 1114 delete_String(copied);
991 return iTrue; 1115 return iTrue;
992 } 1116 }
993 else if (equalWidget_Command(cmd, w, "document.copylink")) { 1117 else if (equal_Command(cmd, "document.copylink") && document_App() == d) {
994 if (d->hoverLink) { 1118 if (d->contextLink) {
995 SDL_SetClipboardText(cstr_String( 1119 SDL_SetClipboardText(cstr_String(
996 absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, d->hoverLink->linkId)))); 1120 absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, d->contextLink->linkId))));
997 } 1121 }
998 else { 1122 else {
999 SDL_SetClipboardText(cstr_String(d->mod.url)); 1123 SDL_SetClipboardText(cstr_String(d->mod.url));
@@ -1001,18 +1125,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1001 return iTrue; 1125 return iTrue;
1002 } 1126 }
1003 else if (equal_Command(cmd, "document.input.submit")) { 1127 else if (equal_Command(cmd, "document.input.submit")) {
1004 if (arg_Command(cmd)) { 1128 iString *value = collect_String(suffix_Command(cmd, "value"));
1005 iString *value = collect_String(suffix_Command(cmd, "value")); 1129 urlEncode_String(value);
1006 urlEncode_String(value); 1130 iString *url = collect_String(copy_String(d->mod.url));
1007 iString *url = collect_String(copy_String(d->mod.url)); 1131 const size_t qPos = indexOfCStr_String(url, "?");
1008 const size_t qPos = indexOfCStr_String(url, "?"); 1132 if (qPos != iInvalidPos) {
1009 if (qPos != iInvalidPos) { 1133 remove_Block(&url->chars, qPos, iInvalidSize);
1010 remove_Block(&url->chars, qPos, iInvalidSize);
1011 }
1012 appendCStr_String(url, "?");
1013 append_String(url, value);
1014 postCommandf_App("open url:%s", cstr_String(url));
1015 } 1134 }
1135 appendCStr_String(url, "?");
1136 append_String(url, value);
1137 postCommandf_App("open url:%s", cstr_String(url));
1016 return iTrue; 1138 return iTrue;
1017 } 1139 }
1018 else if (equal_Command(cmd, "valueinput.cancelled") && 1140 else if (equal_Command(cmd, "valueinput.cancelled") &&
@@ -1028,11 +1150,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1028 else if (equalWidget_Command(cmd, w, "document.request.finished") && 1150 else if (equalWidget_Command(cmd, w, "document.request.finished") &&
1029 pointerLabel_Command(cmd, "request") == d->request) { 1151 pointerLabel_Command(cmd, "request") == d->request) {
1030 checkResponse_DocumentWidget_(d); 1152 checkResponse_DocumentWidget_(d);
1153 resetSmoothScroll_DocumentWidget_(d);
1031 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; 1154 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y;
1032 d->state = ready_RequestState; 1155 d->state = ready_RequestState;
1033 /* The response may be cached. */ { 1156 /* The response may be cached. */ {
1034 const iRangecc proto = urlProtocol_String(d->mod.url); 1157 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about")) {
1035 if (!equal_Rangecc(&proto, "about")) {
1036 setCachedResponse_History(d->mod.history, response_GmRequest(d->request)); 1158 setCachedResponse_History(d->mod.history, response_GmRequest(d->request));
1037 } 1159 }
1038 } 1160 }
@@ -1076,12 +1198,24 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1076 } 1198 }
1077 else if (equalWidget_Command(cmd, w, "scroll.moved")) { 1199 else if (equalWidget_Command(cmd, w, "scroll.moved")) {
1078 d->scrollY = arg_Command(cmd); 1200 d->scrollY = arg_Command(cmd);
1201 resetSmoothScroll_DocumentWidget_(d);
1079 updateVisible_DocumentWidget_(d); 1202 updateVisible_DocumentWidget_(d);
1080 return iTrue; 1203 return iTrue;
1081 } 1204 }
1082 else if (equalWidget_Command(cmd, w, "scroll.page")) { 1205 else if (equalWidget_Command(cmd, w, "scroll.page")) {
1083 scroll_DocumentWidget_(d, 1206 if (argLabel_Command(cmd, "repeat")) {
1084 arg_Command(cmd) * height_Rect(documentBounds_DocumentWidget_(d))); 1207 if (!d->smoothContinue) {
1208 d->smoothContinue = iTrue;
1209 }
1210 else {
1211 return iTrue;
1212 }
1213 }
1214 smoothScroll_DocumentWidget_(d,
1215 arg_Command(cmd) *
1216 (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) -
1217 0 * lineHeight_Text(paragraph_FontId)),
1218 25 * smoothSpeed_DocumentWidget_);
1085 return iTrue; 1219 return iTrue;
1086 } 1220 }
1087 else if (equal_Command(cmd, "document.goto") && document_App() == d) { 1221 else if (equal_Command(cmd, "document.goto") && document_App() == d) {
@@ -1164,6 +1298,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1164 refresh_Widget(w); 1298 refresh_Widget(w);
1165 } 1299 }
1166 break; 1300 break;
1301 case SDLK_PAGEUP:
1302 case SDLK_PAGEDOWN:
1303 case SDLK_SPACE:
1304 case SDLK_UP:
1305 case SDLK_DOWN:
1306 d->smoothContinue = iFalse;
1307 break;
1167 } 1308 }
1168 } 1309 }
1169 if (ev->type == SDL_KEYDOWN) { 1310 if (ev->type == SDL_KEYDOWN) {
@@ -1194,12 +1335,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1194 break; 1335 break;
1195 case SDLK_HOME: 1336 case SDLK_HOME:
1196 d->scrollY = 0; 1337 d->scrollY = 0;
1338 resetSmoothScroll_DocumentWidget_(d);
1197 scroll_DocumentWidget_(d, 0); 1339 scroll_DocumentWidget_(d, 0);
1198 updateVisible_DocumentWidget_(d); 1340 updateVisible_DocumentWidget_(d);
1199 refresh_Widget(w); 1341 refresh_Widget(w);
1200 return iTrue; 1342 return iTrue;
1201 case SDLK_END: 1343 case SDLK_END:
1202 d->scrollY = scrollMax_DocumentWidget_(d); 1344 d->scrollY = scrollMax_DocumentWidget_(d);
1345 resetSmoothScroll_DocumentWidget_(d);
1203 scroll_DocumentWidget_(d, 0); 1346 scroll_DocumentWidget_(d, 0);
1204 updateVisible_DocumentWidget_(d); 1347 updateVisible_DocumentWidget_(d);
1205 refresh_Widget(w); 1348 refresh_Widget(w);
@@ -1207,24 +1350,37 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1207 case SDLK_UP: 1350 case SDLK_UP:
1208 case SDLK_DOWN: 1351 case SDLK_DOWN:
1209 if (mods == 0) { 1352 if (mods == 0) {
1210 scroll_DocumentWidget_(d, 2 * lineHeight_Text(default_FontId) * 1353 if (ev->key.repeat) {
1211 (key == SDLK_UP ? -1 : 1)); 1354 if (!d->smoothContinue) {
1355 d->smoothContinue = iTrue;
1356 }
1357 else return iTrue;
1358 }
1359 smoothScroll_DocumentWidget_(d,
1360 3 * lineHeight_Text(paragraph_FontId) *
1361 (key == SDLK_UP ? -1 : 1),
1362 gap_Text * smoothSpeed_DocumentWidget_);
1212 return iTrue; 1363 return iTrue;
1213 } 1364 }
1214 break; 1365 break;
1215 case SDLK_PAGEUP: 1366 case SDLK_PAGEUP:
1216 case SDLK_PAGEDOWN: 1367 case SDLK_PAGEDOWN:
1217 case ' ': 1368 case SDLK_SPACE:
1218 postCommand_Widget(w, "scroll.page arg:%d", key == SDLK_PAGEUP ? -1 : +1); 1369 postCommand_Widget(
1370 w,
1371 "scroll.page arg:%d repeat:%d",
1372 (key == SDLK_SPACE && mods & KMOD_SHIFT) || key == SDLK_PAGEUP ? -1 : +1,
1373 ev->key.repeat != 0);
1219 return iTrue; 1374 return iTrue;
1220#if 0 1375#if 1
1221 case SDLK_9: { 1376 case SDLK_KP_1: {
1222 iBlock *seed = new_Block(64); 1377 iBlock *seed = new_Block(64);
1223 for (size_t i = 0; i < 64; ++i) { 1378 for (size_t i = 0; i < 64; ++i) {
1224 setByte_Block(seed, i, iRandom(0, 255)); 1379 setByte_Block(seed, i, iRandom(0, 255));
1225 } 1380 }
1226 setThemeSeed_GmDocument(d->doc, seed); 1381 setThemeSeed_GmDocument(d->doc, seed);
1227 delete_Block(seed); 1382 delete_Block(seed);
1383 invalidate_DocumentWidget_(d);
1228 refresh_Widget(w); 1384 refresh_Widget(w);
1229 break; 1385 break;
1230 } 1386 }
@@ -1258,7 +1414,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1258 else if (ev->type == SDL_MOUSEMOTION) { 1414 else if (ev->type == SDL_MOUSEMOTION) {
1259 d->noHoverWhileScrolling = iFalse; 1415 d->noHoverWhileScrolling = iFalse;
1260 if (isVisible_Widget(d->menu)) { 1416 if (isVisible_Widget(d->menu)) {
1261 SDL_SetCursor(d->arrowCursor); 1417 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
1262 } 1418 }
1263 else { 1419 else {
1264 updateHover_DocumentWidget_(d, init_I2(ev->motion.x, ev->motion.y)); 1420 updateHover_DocumentWidget_(d, init_I2(ev->motion.x, ev->motion.y));
@@ -1274,6 +1430,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1274 return iTrue; 1430 return iTrue;
1275 } 1431 }
1276 } 1432 }
1433 if (!isVisible_Widget(d->menu)) {
1434 d->contextLink = d->hoverLink;
1435 }
1277 processContextMenuEvent_Widget(d->menu, ev, d->hoverLink = NULL); 1436 processContextMenuEvent_Widget(d->menu, ev, d->hoverLink = NULL);
1278 switch (processEvent_Click(&d->click, ev)) { 1437 switch (processEvent_Click(&d->click, ev)) {
1279 case started_ClickResult: 1438 case started_ClickResult:
@@ -1282,6 +1441,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1282 case drag_ClickResult: { 1441 case drag_ClickResult: {
1283 /* Begin selecting a range of text. */ 1442 /* Begin selecting a range of text. */
1284 if (!d->selecting) { 1443 if (!d->selecting) {
1444 setFocus_Widget(NULL); /* TODO: Focus this document? */
1285 d->selecting = iTrue; 1445 d->selecting = iTrue;
1286 d->selectMark.start = d->selectMark.end = 1446 d->selectMark.start = d->selectMark.end =
1287 sourceLoc_DocumentWidget_(d, d->click.startPos); 1447 sourceLoc_DocumentWidget_(d, d->click.startPos);
@@ -1418,8 +1578,12 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1418 } 1578 }
1419 /* Text markers. */ 1579 /* Text markers. */
1420 if (d->pass == dynamic_DrawRunPass) { 1580 if (d->pass == dynamic_DrawRunPass) {
1581 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()),
1582 isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD
1583 : SDL_BLENDMODE_BLEND);
1421 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark); 1584 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark);
1422 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark); 1585 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark);
1586 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
1423 } 1587 }
1424 enum iColorId fg = run->color; 1588 enum iColorId fg = run->color;
1425 const iGmDocument *doc = d->widget->doc; 1589 const iGmDocument *doc = d->widget->doc;
@@ -1505,6 +1669,12 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1505 appendFormat_String( 1669 appendFormat_String(
1506 &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : ""); 1670 &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : "");
1507 } 1671 }
1672 const iInt2 size = measureRange_Text(metaFont, range_String(&text));
1673 fillRect_Paint(
1674 &d->paint,
1675 (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)),
1676 addX_I2(size, 2 * gap_UI) },
1677 tmBackground_ColorId);
1508 drawAlign_Text(metaFont, 1678 drawAlign_Text(metaFont,
1509 add_I2(topRight_Rect(run->bounds), origin), 1679 add_I2(topRight_Rect(run->bounds), origin),
1510 fg, 1680 fg,
@@ -1527,9 +1697,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1527 const int flags = linkFlags_GmDocument(doc, linkId); 1697 const int flags = linkFlags_GmDocument(doc, linkId);
1528 iUrl parts; 1698 iUrl parts;
1529 init_Url(&parts, url); 1699 init_Url(&parts, url);
1530 const iString *host = collect_String(newRange_String(parts.host));
1531 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); 1700 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart);
1532 const iBool showHost = (!isEmpty_String(host) && flags & userFriendly_GmLinkFlag); 1701 const iBool showHost = (!isEmpty_Range(&parts.host) && flags & userFriendly_GmLinkFlag);
1533 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; 1702 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0;
1534 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; 1703 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0;
1535 iString str; 1704 iString str;
@@ -1541,11 +1710,16 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1541 &str, 1710 &str,
1542 " \u2014%s%s%s\r%c%s", 1711 " \u2014%s%s%s\r%c%s",
1543 showHost ? " " : "", 1712 showHost ? " " : "",
1544 showHost ? cstr_String(host) : "", 1713 showHost ? (!equalCase_Rangecc(parts.scheme, "gemini")
1714 ? format_CStr("%s://%s",
1715 cstr_Rangecc(parts.scheme),
1716 cstr_Rangecc(parts.host))
1717 : cstr_Rangecc(parts.host))
1718 : "",
1545 showHost && (showImage || showAudio) ? " \u2014" : "", 1719 showHost && (showImage || showAudio) ? " \u2014" : "",
1546 showImage || showAudio 1720 showImage || showAudio
1547 ? '0' + fg 1721 ? asciiBase_ColorEscape + fg
1548 : ('0' + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), 1722 : (asciiBase_ColorEscape + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)),
1549 showImage ? " View Image \U0001f5bc" 1723 showImage ? " View Image \U0001f5bc"
1550 : showAudio ? " Play Audio \U0001f3b5" : ""); 1724 : showAudio ? " Play Audio \U0001f3b5" : "");
1551 } 1725 }
@@ -1599,8 +1773,9 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
1599 const iRect bounds = bounds_Widget(w); 1773 const iRect bounds = bounds_Widget(w);
1600 const iInt2 origin = topLeft_Rect(bounds); 1774 const iInt2 origin = topLeft_Rect(bounds);
1601 const iRangei visRange = visibleRange_DocumentWidget_(d); 1775 const iRangei visRange = visibleRange_DocumentWidget_(d);
1776 iVisBuffer * visBuf = d->visBuffer; /* this may be updated/modified here */
1602 draw_Widget(w); 1777 draw_Widget(w);
1603 allocVisBuffer_DocumentWidget_(iConstCast(iDocumentWidget *, d)); 1778 allocVisBuffer_DocumentWidget_(d);
1604 iDrawContext ctxDynamic = { 1779 iDrawContext ctxDynamic = {
1605 .pass = dynamic_DrawRunPass, 1780 .pass = dynamic_DrawRunPass,
1606 .widget = d, 1781 .widget = d,
@@ -1618,36 +1793,38 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
1618 /* Static content. */ { 1793 /* Static content. */ {
1619 iPaint *p = &ctxStatic.paint; 1794 iPaint *p = &ctxStatic.paint;
1620 init_Paint(p); 1795 init_Paint(p);
1621 const int vbSrc = d->visBufferIndex; 1796 const int vbSrc = visBuf->index;
1622 const int vbDst = d->visBufferIndex ^ 1; 1797 const int vbDst = visBuf->index ^ 1;
1623 iRangei drawRange = visRange; 1798 iRangei drawRange = visRange;
1624 iAssert(d->visBuffer[vbDst]); 1799 iAssert(visBuf->texture[vbDst]);
1625 beginTarget_Paint(p, d->visBuffer[vbDst]); 1800 beginTarget_Paint(p, visBuf->texture[vbDst]);
1626 const iRect visBufferRect = { zero_I2(), d->visBufferSize }; 1801 const iRect visBufferRect = { zero_I2(), visBuf->size };
1627 iRect drawRect = visBufferRect; 1802 iRect drawRect = visBufferRect;
1628 if (!isEmpty_Rangei_(intersect_Rangei_(visRange, d->visBufferValidRange))) { 1803 if (!isEmpty_Rangei_(intersect_Rangei_(visRange, visBuf->validRange))) {
1629 if (visRange.start < d->visBufferValidRange.start) { 1804 if (visRange.start < visBuf->validRange.start) {
1630 drawRange = (iRangei){ visRange.start, d->visBufferValidRange.start }; 1805 drawRange = (iRangei){ visRange.start, visBuf->validRange.start };
1631 } 1806 }
1632 else { 1807 else {
1633 drawRange = (iRangei){ d->visBufferValidRange.end, visRange.end }; 1808 drawRange = (iRangei){ visBuf->validRange.end, visRange.end };
1634 } 1809 }
1635 if (isEmpty_Range(&drawRange)) { 1810 if (isEmpty_Range(&drawRange)) {
1636 SDL_RenderCopy(render, d->visBuffer[vbSrc], NULL, NULL); 1811 SDL_RenderCopy(render, visBuf->texture[vbSrc], NULL, NULL);
1637 } 1812 }
1638 else { 1813 else {
1639 SDL_RenderCopy( 1814 SDL_RenderCopy(
1640 render, 1815 render,
1641 d->visBuffer[vbSrc], 1816 visBuf->texture[vbSrc],
1642 NULL, 1817 NULL,
1643 &(SDL_Rect){ 0, 1818 &(SDL_Rect){ 0,
1644 documentToWindowY_DocumentWidget_(d, d->visBufferValidRange.start) - origin.y, 1819 documentToWindowY_DocumentWidget_(d, visBuf->validRange.start) -
1645 d->visBufferSize.x, 1820 origin.y,
1646 d->visBufferSize.y }); 1821 visBuf->size.x,
1647 drawRect = init_Rect(0, 1822 visBuf->size.y });
1648 documentToWindowY_DocumentWidget_(d, drawRange.start) - origin.y, 1823 drawRect =
1649 d->visBufferSize.x, 1824 init_Rect(0,
1650 size_Range(&drawRange)); 1825 documentToWindowY_DocumentWidget_(d, drawRange.start) - origin.y,
1826 visBuf->size.x,
1827 size_Range(&drawRange));
1651 } 1828 }
1652 } 1829 }
1653 if (!isEmpty_Range(&drawRange)) { 1830 if (!isEmpty_Range(&drawRange)) {
@@ -1657,17 +1834,20 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
1657 unsetClip_Paint(p); 1834 unsetClip_Paint(p);
1658 } 1835 }
1659 endTarget_Paint(p); 1836 endTarget_Paint(p);
1660 SDL_RenderCopy(render, d->visBuffer[vbDst], NULL, 1837 SDL_RenderCopy(render, visBuf->texture[vbDst], NULL,
1661 &(SDL_Rect){ origin.x, origin.y, bounds.size.x, bounds.size.y } ); 1838 &(SDL_Rect){ origin.x, origin.y, bounds.size.x, bounds.size.y } );
1662 iConstCast(iDocumentWidget *, d)->visBufferValidRange = visRange; 1839 visBuf->validRange = visRange;
1663 iConstCast(iDocumentWidget *, d)->visBufferIndex = vbDst; 1840 visBuf->index = vbDst;
1664 } 1841 }
1665 /* Dynamic content. */ { 1842 /* Dynamic content. */ {
1843 extern int enableKerning_Text;
1844 enableKerning_Text = iFalse; /* need to be fast, these is redone on every redraw */
1666 iPaint *p = &ctxDynamic.paint; 1845 iPaint *p = &ctxDynamic.paint;
1667 init_Paint(p); 1846 init_Paint(p);
1668 setClip_Paint(p, bounds); 1847 setClip_Paint(p, bounds);
1669 render_GmDocument(d->doc, visRange, drawRun_DrawContext_, &ctxDynamic); 1848 render_GmDocument(d->doc, visRange, drawRun_DrawContext_, &ctxDynamic);
1670 unsetClip_Paint(p); 1849 unsetClip_Paint(p);
1850 enableKerning_Text = iTrue;
1671 } 1851 }
1672 1852
1673// drawRect_Paint(&ctx.paint, 1853// drawRect_Paint(&ctx.paint,
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h
index 074516c3..5a3125af 100644
--- a/src/ui/documentwidget.h
+++ b/src/ui/documentwidget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include "widget.h" 25#include "widget.h"
@@ -23,5 +45,6 @@ const iString * bookmarkTitle_DocumentWidget (const iDocumentWidget *);
23void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); 45void setUrl_DocumentWidget (iDocumentWidget *, const iString *url);
24void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache); 46void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache);
25void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ 47void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */
48void setRedirectCount_DocumentWidget (iDocumentWidget *, int count);
26 49
27void updateSize_DocumentWidget (iDocumentWidget *); 50void updateSize_DocumentWidget (iDocumentWidget *);
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index d367952d..d583b109 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "inputwidget.h" 23#include "inputwidget.h"
2#include "paint.h" 24#include "paint.h"
3#include "util.h" 25#include "util.h"
@@ -7,17 +29,40 @@
7#include <SDL_clipboard.h> 29#include <SDL_clipboard.h>
8#include <SDL_timer.h> 30#include <SDL_timer.h>
9 31
10static const int REFRESH_INTERVAL = 256; 32static const int refreshInterval_InputWidget_ = 256;
33static const size_t maxUndo_InputWidget_ = 64;
34
35iDeclareType(InputUndo)
36
37struct Impl_InputUndo {
38 iArray text;
39 size_t cursor;
40};
41
42static void init_InputUndo_(iInputUndo *d, const iArray *text, size_t cursor) {
43 initCopy_Array(&d->text, text);
44 d->cursor = cursor;
45}
46
47static void deinit_InputUndo_(iInputUndo *d) {
48 deinit_Array(&d->text);
49}
11 50
12struct Impl_InputWidget { 51struct Impl_InputWidget {
13 iWidget widget; 52 iWidget widget;
14 enum iInputMode mode; 53 enum iInputMode mode;
15 iBool isSensitive; 54 iBool isSensitive;
16 iBool enterPressed; 55 iBool enterPressed;
56 iBool selectAllOnFocus;
17 size_t maxLen; 57 size_t maxLen;
18 iArray text; /* iChar[] */ 58 iArray text; /* iChar[] */
19 iArray oldText; /* iChar[] */ 59 iArray oldText; /* iChar[] */
60 iString hint;
20 size_t cursor; 61 size_t cursor;
62 size_t lastCursor;
63 iBool isMarking;
64 iRanges mark;
65 iArray undoStack;
21 int font; 66 int font;
22 iClick click; 67 iClick click;
23 uint32_t timer; 68 uint32_t timer;
@@ -25,16 +70,29 @@ struct Impl_InputWidget {
25 70
26iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) 71iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen)
27 72
73static void clearUndo_InputWidget_(iInputWidget *d) {
74 iForEach(Array, i, &d->undoStack) {
75 deinit_InputUndo_(i.value);
76 }
77 clear_Array(&d->undoStack);
78}
79
28void init_InputWidget(iInputWidget *d, size_t maxLen) { 80void init_InputWidget(iInputWidget *d, size_t maxLen) {
29 iWidget *w = &d->widget; 81 iWidget *w = &d->widget;
30 init_Widget(w); 82 init_Widget(w);
31 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue); 83 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue);
32 init_Array(&d->text, sizeof(iChar)); 84 init_Array(&d->text, sizeof(iChar));
33 init_Array(&d->oldText, sizeof(iChar)); 85 init_Array(&d->oldText, sizeof(iChar));
86 init_String(&d->hint);
87 init_Array(&d->undoStack, sizeof(iInputUndo));
34 d->font = uiInput_FontId; 88 d->font = uiInput_FontId;
35 d->cursor = 0; 89 d->cursor = 0;
36 d->isSensitive = iFalse; 90 d->lastCursor = 0;
37 d->enterPressed = iFalse; 91 d->isMarking = iFalse;
92 iZap(d->mark);
93 d->isSensitive = iFalse;
94 d->enterPressed = iFalse;
95 d->selectAllOnFocus = iFalse;
38 setMaxLen_InputWidget(d, maxLen); 96 setMaxLen_InputWidget(d, maxLen);
39 /* Caller must arrange the width, but the height is fixed. */ 97 /* Caller must arrange the width, but the height is fixed. */
40 w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI; 98 w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI;
@@ -44,13 +102,39 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
44} 102}
45 103
46void deinit_InputWidget(iInputWidget *d) { 104void deinit_InputWidget(iInputWidget *d) {
105 clearUndo_InputWidget_(d);
106 deinit_Array(&d->undoStack);
47 if (d->timer) { 107 if (d->timer) {
48 SDL_RemoveTimer(d->timer); 108 SDL_RemoveTimer(d->timer);
49 } 109 }
110 deinit_String(&d->hint);
50 deinit_Array(&d->oldText); 111 deinit_Array(&d->oldText);
51 deinit_Array(&d->text); 112 deinit_Array(&d->text);
52} 113}
53 114
115static void pushUndo_InputWidget_(iInputWidget *d) {
116 iInputUndo undo;
117 init_InputUndo_(&undo, &d->text, d->cursor);
118 pushBack_Array(&d->undoStack, &undo);
119 if (size_Array(&d->undoStack) > maxUndo_InputWidget_) {
120 deinit_InputUndo_(front_Array(&d->undoStack));
121 popFront_Array(&d->undoStack);
122 }
123}
124
125static iBool popUndo_InputWidget_(iInputWidget *d) {
126 if (!isEmpty_Array(&d->undoStack)) {
127 iInputUndo *undo = back_Array(&d->undoStack);
128 setCopy_Array(&d->text, &undo->text);
129 d->cursor = undo->cursor;
130 deinit_InputUndo_(undo);
131 popBack_Array(&d->undoStack);
132 iZap(d->mark);
133 return iTrue;
134 }
135 return iFalse;
136}
137
54void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { 138void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) {
55 d->mode = mode; 139 d->mode = mode;
56} 140}
@@ -78,7 +162,12 @@ void setMaxLen_InputWidget(iInputWidget *d, size_t maxLen) {
78 } 162 }
79} 163}
80 164
165void setHint_InputWidget(iInputWidget *d, const char *hintText) {
166 setCStr_String(&d->hint, hintText);
167}
168
81void setText_InputWidget(iInputWidget *d, const iString *text) { 169void setText_InputWidget(iInputWidget *d, const iString *text) {
170 clearUndo_InputWidget_(d);
82 clear_Array(&d->text); 171 clear_Array(&d->text);
83 iConstForEach(String, i, text) { 172 iConstForEach(String, i, text) {
84 pushBack_Array(&d->text, &i.value); 173 pushBack_Array(&d->text, &i.value);
@@ -92,10 +181,6 @@ void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) {
92 delete_String(str); 181 delete_String(str);
93} 182}
94 183
95void setCursor_InputWidget(iInputWidget *d, size_t pos) {
96 d->cursor = iMin(pos, size_Array(&d->text));
97}
98
99static uint32_t refreshTimer_(uint32_t interval, void *d) { 184static uint32_t refreshTimer_(uint32_t interval, void *d) {
100 refresh_Widget(d); 185 refresh_Widget(d);
101 return interval; 186 return interval;
@@ -118,8 +203,14 @@ void begin_InputWidget(iInputWidget *d) {
118 SDL_StartTextInput(); 203 SDL_StartTextInput();
119 setFlags_Widget(w, selected_WidgetFlag, iTrue); 204 setFlags_Widget(w, selected_WidgetFlag, iTrue);
120 refresh_Widget(w); 205 refresh_Widget(w);
121 d->timer = SDL_AddTimer(REFRESH_INTERVAL, refreshTimer_, d); 206 d->timer = SDL_AddTimer(refreshInterval_InputWidget_, refreshTimer_, d);
122 d->enterPressed = iFalse; 207 d->enterPressed = iFalse;
208 if (d->selectAllOnFocus) {
209 d->mark = (iRanges){ 0, size_Array(&d->text) };
210 }
211 else {
212 iZap(d->mark);
213 }
123} 214}
124 215
125void end_InputWidget(iInputWidget *d, iBool accept) { 216void end_InputWidget(iInputWidget *d, iBool accept) {
@@ -159,6 +250,168 @@ static void insertChar_InputWidget_(iInputWidget *d, iChar chr) {
159 refresh_Widget(as_Widget(d)); 250 refresh_Widget(as_Widget(d));
160} 251}
161 252
253iLocalDef size_t cursorMax_InputWidget_(const iInputWidget *d) {
254 return iMin(size_Array(&d->text), d->maxLen - 1);
255}
256
257iLocalDef iBool isMarking_(void) {
258 return (SDL_GetModState() & KMOD_SHIFT) != 0;
259}
260
261void setCursor_InputWidget(iInputWidget *d, size_t pos) {
262 if (isEmpty_Array(&d->text)) {
263 d->cursor = 0;
264 }
265 else {
266 d->cursor = iClamp(pos, 0, cursorMax_InputWidget_(d));
267 }
268 /* Update selection. */
269 if (isMarking_()) {
270 if (isEmpty_Range(&d->mark)) {
271 d->mark.start = d->lastCursor;
272 d->mark.end = d->cursor;
273 }
274 else {
275 d->mark.end = d->cursor;
276 }
277 }
278 else {
279 iZap(d->mark);
280 }
281}
282
283void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) {
284 d->selectAllOnFocus = selectAllOnFocus;
285}
286
287static iRanges mark_InputWidget_(const iInputWidget *d) {
288 return (iRanges){ iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) };
289}
290
291static iBool deleteMarked_InputWidget_(iInputWidget *d) {
292 const iRanges m = mark_InputWidget_(d);
293 if (!isEmpty_Range(&m)) {
294 removeRange_Array(&d->text, m);
295 setCursor_InputWidget(d, m.start);
296 iZap(d->mark);
297 return iTrue;
298 }
299 return iFalse;
300}
301
302static iBool isWordChar_InputWidget_(const iInputWidget *d, size_t pos) {
303 const iChar ch = pos < size_Array(&d->text) ? constValue_Array(&d->text, pos, iChar) : ' ';
304 return isAlphaNumeric_Char(ch);
305}
306
307iLocalDef iBool movePos_InputWidget_(const iInputWidget *d, size_t *pos, int dir) {
308 if (dir < 0) {
309 if (*pos > 0) (*pos)--; else return iFalse;
310 }
311 else {
312 if (*pos < cursorMax_InputWidget_(d)) (*pos)++; else return iFalse;
313 }
314 return iTrue;
315}
316
317static size_t skipWord_InputWidget_(const iInputWidget *d, size_t pos, int dir) {
318 const iBool startedAtNonWord = !isWordChar_InputWidget_(d, pos);
319 if (!movePos_InputWidget_(d, &pos, dir)) {
320 return pos;
321 }
322 /* Skip any non-word characters at start position. */
323 while (!isWordChar_InputWidget_(d, pos)) {
324 if (!movePos_InputWidget_(d, &pos, dir)) {
325 return pos;
326 }
327 }
328 if (startedAtNonWord && dir > 0) {
329 return pos; /* Found the start of a word. */
330 }
331 /* Skip the word. */
332 while (isWordChar_InputWidget_(d, pos)) {
333 if (!movePos_InputWidget_(d, &pos, dir)) {
334 return pos;
335 }
336 }
337 if (dir > 0) {
338 /* Skip to the beginning of the word. */
339 while (!isWordChar_InputWidget_(d, pos)) {
340 if (!movePos_InputWidget_(d, &pos, dir)) {
341 return pos;
342 }
343 }
344 }
345 else {
346 movePos_InputWidget_(d, &pos, +1);
347 }
348 return pos;
349}
350
351static const iChar sensitiveChar_ = 0x25cf; /* black circle */
352
353static iString *visText_InputWidget_(const iInputWidget *d) {
354 iString *text;
355 if (!d->isSensitive) {
356 text = newUnicodeN_String(constData_Array(&d->text), size_Array(&d->text));
357 }
358 else {
359 text = new_String();
360 for (size_t i = 0; i < size_Array(&d->text); ++i) {
361 appendChar_String(text, sensitiveChar_);
362 }
363 }
364 return text;
365}
366
367iLocalDef iInt2 padding_(void) {
368 return init_I2(gap_UI / 2, gap_UI / 2);
369}
370
371static iInt2 textOrigin_InputWidget_(const iInputWidget *d, const char *visText) {
372 const iWidget *w = constAs_Widget(d);
373 iRect bounds = adjusted_Rect(bounds_Widget(w), padding_(), neg_I2(padding_()));
374 const iInt2 emSize = advance_Text(d->font, "M");
375 const int textWidth = advance_Text(d->font, visText).x;
376 const int cursorX = advanceN_Text(d->font, visText, d->cursor).x;
377 int xOff = 0;
378 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0));
379 if (d->maxLen == 0) {
380 if (textWidth > width_Rect(bounds) - emSize.x) {
381 xOff = width_Rect(bounds) - emSize.x - textWidth;
382 }
383 if (cursorX + xOff < width_Rect(bounds) / 2) {
384 xOff = width_Rect(bounds) / 2 - cursorX;
385 }
386 xOff = iMin(xOff, 0);
387 }
388 const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2;
389 return add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff));
390}
391
392static size_t coordIndex_InputWidget_(const iInputWidget *d, iInt2 coord) {
393 iString *visText = visText_InputWidget_(d);
394 iInt2 pos = sub_I2(coord, textOrigin_InputWidget_(d, cstr_String(visText)));
395 size_t index = 0;
396 if (pos.x > 0) {
397 const char *endPos;
398 tryAdvanceNoWrap_Text(d->font, range_String(visText), pos.x, &endPos);
399 if (endPos == constEnd_String(visText)) {
400 index = cursorMax_InputWidget_(d);
401 }
402 else {
403 /* Need to know the actual character index. */
404 /* TODO: tryAdvance could tell us this directly with an extra return value */
405 iConstForEach(String, i, visText) {
406 if (i.pos >= endPos) break;
407 index++;
408 }
409 }
410 }
411 delete_String(visText);
412 return index;
413}
414
162static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { 415static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
163 iWidget *w = as_Widget(d); 416 iWidget *w = as_Widget(d);
164 if (isCommand_Widget(w, ev, "focus.gained")) { 417 if (isCommand_Widget(w, ev, "focus.gained")) {
@@ -173,25 +426,51 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
173 case none_ClickResult: 426 case none_ClickResult:
174 break; 427 break;
175 case started_ClickResult: 428 case started_ClickResult:
176 case drag_ClickResult: 429 setFocus_Widget(w);
430 setCursor_InputWidget(d, coordIndex_InputWidget_(d, pos_Click(&d->click)));
431 iZap(d->mark);
432 d->isMarking = iFalse;
433 return iTrue;
177 case double_ClickResult: 434 case double_ClickResult:
178 case aborted_ClickResult: 435 case aborted_ClickResult:
179 return iTrue; 436 return iTrue;
437 case drag_ClickResult:
438 d->cursor = coordIndex_InputWidget_(d, pos_Click(&d->click));
439 if (!d->isMarking) {
440 d->isMarking = iTrue;
441 d->mark.start = d->cursor;
442 }
443 d->mark.end = d->cursor;
444 refresh_Widget(w);
445 return iTrue;
180 case finished_ClickResult: 446 case finished_ClickResult:
181 setFocus_Widget(as_Widget(d));
182 return iTrue; 447 return iTrue;
183 } 448 }
184 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { 449 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) {
185 return iTrue; 450 return iTrue;
186 } 451 }
187 const size_t curMax = iMin(size_Array(&d->text), d->maxLen - 1); 452 const size_t curMax = cursorMax_InputWidget_(d);
188 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { 453 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) {
189 const int key = ev->key.keysym.sym; 454 const int key = ev->key.keysym.sym;
190 const int mods = keyMods_Sym(ev->key.keysym.mod); 455 const int mods = keyMods_Sym(ev->key.keysym.mod);
191 if (mods == KMOD_PRIMARY) { 456 if (mods == KMOD_PRIMARY) {
192 switch (key) { 457 switch (key) {
458 case 'c':
459 case 'x':
460 if (!isEmpty_Range(&d->mark)) {
461 const iRanges m = mark_InputWidget_(d);
462 SDL_SetClipboardText(cstrCollect_String(
463 newUnicodeN_String(constAt_Array(&d->text, m.start), size_Range(&m))));
464 if (key == 'x') {
465 pushUndo_InputWidget_(d);
466 deleteMarked_InputWidget_(d);
467 }
468 }
469 return iTrue;
193 case 'v': 470 case 'v':
194 if (SDL_HasClipboardText()) { 471 if (SDL_HasClipboardText()) {
472 pushUndo_InputWidget_(d);
473 deleteMarked_InputWidget_(d);
195 char *text = SDL_GetClipboardText(); 474 char *text = SDL_GetClipboardText();
196 iString *paste = collect_String(newCStr_String(text)); 475 iString *paste = collect_String(newCStr_String(text));
197 SDL_free(text); 476 SDL_free(text);
@@ -200,8 +479,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
200 } 479 }
201 } 480 }
202 return iTrue; 481 return iTrue;
482 case 'z':
483 if (popUndo_InputWidget_(d)) {
484 refresh_Widget(w);
485 }
486 return iTrue;
203 } 487 }
204 } 488 }
489 d->lastCursor = d->cursor;
205 switch (key) { 490 switch (key) {
206 case SDLK_RETURN: 491 case SDLK_RETURN:
207 case SDLK_KP_ENTER: 492 case SDLK_KP_ENTER:
@@ -213,11 +498,18 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
213 setFocus_Widget(NULL); 498 setFocus_Widget(NULL);
214 return iTrue; 499 return iTrue;
215 case SDLK_BACKSPACE: 500 case SDLK_BACKSPACE:
216 if (mods & KMOD_ALT) { 501 if (!isEmpty_Range(&d->mark)) {
217 clear_Array(&d->text); 502 pushUndo_InputWidget_(d);
218 d->cursor = 0; 503 deleteMarked_InputWidget_(d);
504 }
505 else if (mods & KMOD_ALT) {
506 pushUndo_InputWidget_(d);
507 d->mark.start = d->cursor;
508 d->mark.end = skipWord_InputWidget_(d, d->cursor, -1);
509 deleteMarked_InputWidget_(d);
219 } 510 }
220 else if (d->cursor > 0) { 511 else if (d->cursor > 0) {
512 pushUndo_InputWidget_(d);
221 remove_Array(&d->text, --d->cursor); 513 remove_Array(&d->text, --d->cursor);
222 } 514 }
223 refresh_Widget(w); 515 refresh_Widget(w);
@@ -225,49 +517,79 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
225 case SDLK_d: 517 case SDLK_d:
226 if (mods != KMOD_CTRL) break; 518 if (mods != KMOD_CTRL) break;
227 case SDLK_DELETE: 519 case SDLK_DELETE:
228 if (d->cursor < size_Array(&d->text)) { 520 if (!isEmpty_Range(&d->mark)) {
521 pushUndo_InputWidget_(d);
522 deleteMarked_InputWidget_(d);
523 }
524 else if (mods & KMOD_ALT) {
525 pushUndo_InputWidget_(d);
526 d->mark.start = d->cursor;
527 d->mark.end = skipWord_InputWidget_(d, d->cursor, +1);
528 deleteMarked_InputWidget_(d);
529 }
530 else if (d->cursor < size_Array(&d->text)) {
531 pushUndo_InputWidget_(d);
229 remove_Array(&d->text, d->cursor); 532 remove_Array(&d->text, d->cursor);
230 refresh_Widget(w);
231 } 533 }
534 refresh_Widget(w);
232 return iTrue; 535 return iTrue;
233 case SDLK_k: 536 case SDLK_k:
234 if (mods == KMOD_CTRL) { 537 if (mods == KMOD_CTRL) {
235 removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor); 538 if (!isEmpty_Range(&d->mark)) {
539 pushUndo_InputWidget_(d);
540 deleteMarked_InputWidget_(d);
541 }
542 else {
543 pushUndo_InputWidget_(d);
544 removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor);
545 }
236 refresh_Widget(w); 546 refresh_Widget(w);
237 return iTrue; 547 return iTrue;
238 } 548 }
239 break; 549 break;
240 case SDLK_HOME: 550 case SDLK_HOME:
241 case SDLK_END: 551 case SDLK_END:
242 d->cursor = (key == SDLK_HOME ? 0 : curMax); 552 setCursor_InputWidget(d, key == SDLK_HOME ? 0 : curMax);
243 refresh_Widget(w); 553 refresh_Widget(w);
244 return iTrue; 554 return iTrue;
245 case SDLK_a: 555 case SDLK_a:
556#if defined (iPlatformApple)
557 if (mods == KMOD_PRIMARY) {
558 d->mark.start = 0;
559 d->mark.end = curMax;
560 d->cursor = curMax;
561 refresh_Widget(w);
562 return iTrue;
563 }
564#endif
565 /* fall through for Emacs-style Home/End */
246 case SDLK_e: 566 case SDLK_e:
247 if (mods == KMOD_CTRL) { 567 if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) {
248 d->cursor = (key == 'a' ? 0 : curMax); 568 setCursor_InputWidget(d, key == 'a' ? 0 : curMax);
249 refresh_Widget(w); 569 refresh_Widget(w);
250 return iTrue; 570 return iTrue;
251 } 571 }
252 break; 572 break;
253 case SDLK_LEFT: 573 case SDLK_LEFT:
574 case SDLK_RIGHT: {
575 const int dir = (key == SDLK_LEFT ? -1 : +1);
254 if (mods & KMOD_PRIMARY) { 576 if (mods & KMOD_PRIMARY) {
255 d->cursor = 0; 577 setCursor_InputWidget(d, dir < 0 ? 0 : curMax);
256 } 578 }
257 else if (d->cursor > 0) { 579 else if (mods & KMOD_ALT) {
258 d->cursor--; 580 setCursor_InputWidget(d, skipWord_InputWidget_(d, d->cursor, dir));
259 } 581 }
260 refresh_Widget(w); 582 else if (!isMarking_() && !isEmpty_Range(&d->mark)) {
261 return iTrue; 583 const iRanges m = mark_InputWidget_(d);
262 case SDLK_RIGHT: 584 setCursor_InputWidget(d, dir < 0 ? m.start : m.end);
263 if (mods & KMOD_PRIMARY) { 585 iZap(d->mark);
264 d->cursor = curMax;
265 } 586 }
266 else if (d->cursor < curMax) { 587 else if ((dir < 0 && d->cursor > 0) || (dir > 0 && d->cursor < curMax)) {
267 d->cursor++; 588 setCursor_InputWidget(d, d->cursor + dir);
268 } 589 }
269 refresh_Widget(w); 590 refresh_Widget(w);
270 return iTrue; 591 return iTrue;
592 }
271 case SDLK_TAB: 593 case SDLK_TAB:
272 /* Allow focus switching. */ 594 /* Allow focus switching. */
273 return processEvent_Widget(as_Widget(d), ev); 595 return processEvent_Widget(as_Widget(d), ev);
@@ -278,6 +600,8 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
278 return iTrue; 600 return iTrue;
279 } 601 }
280 else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) { 602 else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) {
603 pushUndo_InputWidget_(d);
604 deleteMarked_InputWidget_(d);
281 const iString *uni = collectNewCStr_String(ev->text.text); 605 const iString *uni = collectNewCStr_String(ev->text.text);
282 iConstForEach(String, i, uni) { 606 iConstForEach(String, i, uni) {
283 insertChar_InputWidget_(d, i.value); 607 insertChar_InputWidget_(d, i.value);
@@ -287,27 +611,29 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
287 return processEvent_Widget(w, ev); 611 return processEvent_Widget(w, ev);
288} 612}
289 613
290static const iChar sensitiveChar_ = 0x25cf; /* black circle */ 614static iBool isWhite_(const iString *str) {
615 iConstForEach(String, i, str) {
616 if (!isSpace_Char(i.value)) {
617 return iFalse;
618 }
619 }
620 return iTrue;
621}
291 622
292static void draw_InputWidget_(const iInputWidget *d) { 623static void draw_InputWidget_(const iInputWidget *d) {
293 const iWidget *w = constAs_Widget(d); 624 const iWidget *w = constAs_Widget(d);
294 const uint32_t time = frameTime_Window(get_Window()); 625 const uint32_t time = frameTime_Window(get_Window());
295 const iInt2 padding = init_I2(gap_UI / 2, gap_UI / 2); 626 iRect bounds = adjusted_Rect(bounds_Widget(w), padding_(), neg_I2(padding_()));
296 iRect bounds = adjusted_Rect(bounds_Widget(w), padding, neg_I2(padding)); 627 iBool isHint = iFalse;
297 const iBool isFocused = isFocused_Widget(w); 628 const iBool isFocused = isFocused_Widget(w);
298 const iBool isHover = isHover_Widget(w) && 629 const iBool isHover = isHover_Widget(w) &&
299 contains_Widget(w, mouseCoord_Window(get_Window())); 630 contains_Widget(w, mouseCoord_Window(get_Window()));
300 iPaint p; 631 iPaint p;
301 init_Paint(&p); 632 init_Paint(&p);
302 iString text; 633 iString *text = visText_InputWidget_(d);
303 if (!d->isSensitive) { 634 if (isWhite_(text) && !isEmpty_String(&d->hint)) {
304 initUnicodeN_String(&text, constData_Array(&d->text), size_Array(&d->text)); 635 set_String(text, &d->hint);
305 } 636 isHint = iTrue;
306 else {
307 init_String(&text);
308 for (size_t i = 0; i < size_Array(&d->text); ++i) {
309 appendChar_String(&text, sensitiveChar_);
310 }
311 } 637 }
312 fillRect_Paint( 638 fillRect_Paint(
313 &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId); 639 &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId);
@@ -317,34 +643,27 @@ static void draw_InputWidget_(const iInputWidget *d) {
317 isFocused ? uiInputFrameFocused_ColorId 643 isFocused ? uiInputFrameFocused_ColorId
318 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); 644 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId);
319 setClip_Paint(&p, bounds); 645 setClip_Paint(&p, bounds);
320 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); 646 const iInt2 textOrigin = textOrigin_InputWidget_(d, cstr_String(text));
321 const iInt2 emSize = advance_Text(d->font, "M"); 647 if (isFocused && !isEmpty_Range(&d->mark)) {
322 const int textWidth = advance_Text(d->font, cstr_String(&text)).x; 648 /* Draw the selected range. */
323 const int cursorX = advanceN_Text(d->font, cstr_String(&text), d->cursor).x; 649 const int m1 = advanceN_Text(d->font, cstr_String(text), d->mark.start).x;
324 int xOff = 0; 650 const int m2 = advanceN_Text(d->font, cstr_String(text), d->mark.end).x;
325 if (d->maxLen == 0) { 651 fillRect_Paint(&p,
326 if (textWidth > width_Rect(bounds) - emSize.x) { 652 (iRect){ addX_I2(textOrigin, iMin(m1, m2)),
327 xOff = width_Rect(bounds) - emSize.x - textWidth; 653 init_I2(iAbs(m2 - m1), lineHeight_Text(d->font)) },
328 } 654 uiMarked_ColorId);
329 if (cursorX + xOff < width_Rect(bounds) / 2) {
330 xOff = width_Rect(bounds) / 2 - cursorX;
331 }
332 xOff = iMin(xOff, 0);
333 } 655 }
334 const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2;
335 draw_Text(d->font, 656 draw_Text(d->font,
336 add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff)), 657 textOrigin,
337 isFocused ? uiInputTextFocused_ColorId : uiInputText_ColorId, 658 isHint ? uiAnnotation_ColorId
659 : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId
660 : uiInputText_ColorId,
338 "%s", 661 "%s",
339 cstr_String(&text)); 662 cstr_String(text));
340 unsetClip_Paint(&p); 663 unsetClip_Paint(&p);
341 /* Cursor blinking. */ 664 /* Cursor blinking. */
342 if (isFocused && (time & 256)) { 665 if (isFocused && (time & 256)) {
343 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(&text), d->cursor); 666 iString cur;
344 const iInt2 curPos = init_I2(xOff + left_Rect(bounds) + prefixSize.x,
345 yOff + top_Rect(bounds));
346 const iRect curRect = { curPos, addX_I2(emSize, 1) };
347 iString cur;
348 if (d->cursor < size_Array(&d->text)) { 667 if (d->cursor < size_Array(&d->text)) {
349 if (!d->isSensitive) { 668 if (!d->isSensitive) {
350 initUnicodeN_String(&cur, constAt_Array(&d->text, d->cursor), 1); 669 initUnicodeN_String(&cur, constAt_Array(&d->text, d->cursor), 1);
@@ -356,11 +675,14 @@ static void draw_InputWidget_(const iInputWidget *d) {
356 else { 675 else {
357 initCStr_String(&cur, " "); 676 initCStr_String(&cur, " ");
358 } 677 }
678 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(text), d->cursor);
679 const iInt2 curPos = addX_I2(textOrigin, prefixSize.x);
680 const iRect curRect = { curPos, addX_I2(advance_Text(d->font, cstr_String(&cur)), 1) };
359 fillRect_Paint(&p, curRect, uiInputCursor_ColorId); 681 fillRect_Paint(&p, curRect, uiInputCursor_ColorId);
360 draw_Text(d->font, curPos, uiInputCursorText_ColorId, cstr_String(&cur)); 682 draw_Text(d->font, curPos, uiInputCursorText_ColorId, cstr_String(&cur));
361 deinit_String(&cur); 683 deinit_String(&cur);
362 } 684 }
363 deinit_String(&text); 685 delete_String(text);
364} 686}
365 687
366iBeginDefineSubclass(InputWidget, Widget) 688iBeginDefineSubclass(InputWidget, Widget)
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h
index 903e428a..fed9c3d9 100644
--- a/src/ui/inputwidget.h
+++ b/src/ui/inputwidget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include "widget.h" 25#include "widget.h"
@@ -10,13 +32,21 @@ enum iInputMode {
10 overwrite_InputMode, 32 overwrite_InputMode,
11}; 33};
12 34
35void setHint_InputWidget (iInputWidget *, const char *hintText);
13void setSensitive_InputWidget(iInputWidget *, iBool isSensitive); 36void setSensitive_InputWidget(iInputWidget *, iBool isSensitive);
14void setMode_InputWidget (iInputWidget *, enum iInputMode mode); 37void setMode_InputWidget (iInputWidget *, enum iInputMode mode);
15void setMaxLen_InputWidget (iInputWidget *, size_t maxLen); 38void setMaxLen_InputWidget (iInputWidget *, size_t maxLen);
16void setText_InputWidget (iInputWidget *, const iString *text); 39void setText_InputWidget (iInputWidget *, const iString *text);
17void setTextCStr_InputWidget (iInputWidget *, const char *cstr); 40void setTextCStr_InputWidget (iInputWidget *, const char *cstr);
18void setCursor_InputWidget (iInputWidget *, size_t pos); 41void setCursor_InputWidget (iInputWidget *, size_t pos);
42void setSelectAllOnFocus_InputWidget(iInputWidget *, iBool selectAllOnFocus);
19void begin_InputWidget (iInputWidget *); 43void begin_InputWidget (iInputWidget *);
20void end_InputWidget (iInputWidget *, iBool accept); 44void end_InputWidget (iInputWidget *, iBool accept);
21 45
22const iString * text_InputWidget (const iInputWidget *); 46const iString * text_InputWidget (const iInputWidget *);
47
48iLocalDef iInputWidget *newHint_InputWidget(size_t maxLen, const char *hint) {
49 iInputWidget *d = new_InputWidget(maxLen);
50 setHint_InputWidget(d, hint);
51 return d;
52}
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index a3daff44..8b2506e7 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "labelwidget.h" 23#include "labelwidget.h"
2#include "text.h" 24#include "text.h"
3#include "color.h" 25#include "color.h"
@@ -133,6 +155,9 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
133 *fg = uiText_ColorId; 155 *fg = uiText_ColorId;
134 *frame1 = isButton ? uiEmboss1_ColorId : uiFrame_ColorId; 156 *frame1 = isButton ? uiEmboss1_ColorId : uiFrame_ColorId;
135 *frame2 = isButton ? uiEmboss2_ColorId : *frame1; 157 *frame2 = isButton ? uiEmboss2_ColorId : *frame1;
158 if (flags_Widget(w) & disabled_WidgetFlag && isButton) {
159 *fg = uiTextDisabled_ColorId;
160 }
136 if (isSel) { 161 if (isSel) {
137 *bg = uiBackgroundSelected_ColorId; 162 *bg = uiBackgroundSelected_ColorId;
138 *fg = uiTextSelected_ColorId; 163 *fg = uiTextSelected_ColorId;
@@ -150,11 +175,11 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
150 /* Frames matching color escaped text. */ 175 /* Frames matching color escaped text. */
151 if (startsWith_String(&d->label, "\r")) { 176 if (startsWith_String(&d->label, "\r")) {
152 if (isDark_ColorTheme(colorTheme_App())) { 177 if (isDark_ColorTheme(colorTheme_App())) {
153 *frame1 = cstr_String(&d->label)[1] - '0'; 178 *frame1 = cstr_String(&d->label)[1] - asciiBase_ColorEscape;
154 *frame2 = darker_Color(*frame1); 179 *frame2 = darker_Color(*frame1);
155 } 180 }
156 else { 181 else {
157 *bg = *frame1 = *frame2 = cstr_String(&d->label)[1] - '0'; 182 *bg = *frame1 = *frame2 = cstr_String(&d->label)[1] - asciiBase_ColorEscape;
158 *fg = uiBackground_ColorId | permanent_ColorId; 183 *fg = uiBackground_ColorId | permanent_ColorId;
159 } 184 }
160 } 185 }
@@ -311,10 +336,20 @@ void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) {
311 updateSize_LabelWidget(d); 336 updateSize_LabelWidget(d);
312} 337}
313 338
339const iString *label_LabelWidget(const iLabelWidget *d) {
340 return &d->label;
341}
342
314const iString *command_LabelWidget(const iLabelWidget *d) { 343const iString *command_LabelWidget(const iLabelWidget *d) {
315 return &d->command; 344 return &d->command;
316} 345}
317 346
347iLabelWidget *newColor_LabelWidget(const char *text, int color) {
348 iLabelWidget *d = new_LabelWidget(format_CStr("%s%s", escape_Color(color), text), 0, 0, NULL);
349 setFlags_Widget(as_Widget(d), frameless_WidgetFlag, iTrue);
350 return d;
351}
352
318iBeginDefineSubclass(LabelWidget, Widget) 353iBeginDefineSubclass(LabelWidget, Widget)
319 .processEvent = (iAny *) processEvent_LabelWidget_, 354 .processEvent = (iAny *) processEvent_LabelWidget_,
320 .draw = (iAny *) draw_LabelWidget_, 355 .draw = (iAny *) draw_LabelWidget_,
diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h
index aaa897c9..c55ecd08 100644
--- a/src/ui/labelwidget.h
+++ b/src/ui/labelwidget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3/* Text label/button. */ 25/* Text label/button. */
@@ -16,8 +38,11 @@ void updateSize_LabelWidget (iLabelWidget *);
16void updateText_LabelWidget (iLabelWidget *, const iString *text); /* not resized */ 38void updateText_LabelWidget (iLabelWidget *, const iString *text); /* not resized */
17void updateTextCStr_LabelWidget (iLabelWidget *, const char *text); /* not resized */ 39void updateTextCStr_LabelWidget (iLabelWidget *, const char *text); /* not resized */
18 40
41const iString *label_LabelWidget (const iLabelWidget *);
19const iString *command_LabelWidget (const iLabelWidget *); 42const iString *command_LabelWidget (const iLabelWidget *);
20 43
44iLabelWidget *newColor_LabelWidget(const char *text, int color);
45
21iLocalDef iLabelWidget *newEmpty_LabelWidget(void) { 46iLocalDef iLabelWidget *newEmpty_LabelWidget(void) {
22 return new_LabelWidget("", 0, 0, NULL); 47 return new_LabelWidget("", 0, 0, NULL);
23} 48}
diff --git a/src/ui/macos.h b/src/ui/macos.h
deleted file mode 100644
index a491e721..00000000
--- a/src/ui/macos.h
+++ /dev/null
@@ -1,9 +0,0 @@
1#pragma once
2
3#include "util.h"
4
5/* Platform-specific functionality for macOS */
6
7void setupApplication_MacOS (void);
8void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count);
9void handleCommand_MacOS (const char *cmd);
diff --git a/src/ui/metrics.c b/src/ui/metrics.c
index 3c4bfd75..537aaf98 100644
--- a/src/ui/metrics.c
+++ b/src/ui/metrics.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "metrics.h" 23#include "metrics.h"
2 24
3#include <the_Foundation/math.h> 25#include <the_Foundation/math.h>
diff --git a/src/ui/metrics.h b/src/ui/metrics.h
index 39fc6415..207dd59d 100644
--- a/src/ui/metrics.h
+++ b/src/ui/metrics.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/vec2.h> 25#include <the_Foundation/vec2.h>
diff --git a/src/ui/paint.c b/src/ui/paint.c
index 264ca0d8..0166e398 100644
--- a/src/ui/paint.c
+++ b/src/ui/paint.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "paint.h" 23#include "paint.h"
2 24
3#include <SDL_version.h> 25#include <SDL_version.h>
diff --git a/src/ui/paint.h b/src/ui/paint.h
index 5b29b176..7c9e5e51 100644
--- a/src/ui/paint.h
+++ b/src/ui/paint.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/rect.h> 25#include <the_Foundation/rect.h>
diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c
index 3d8f5eaa..f5340897 100644
--- a/src/ui/scrollwidget.c
+++ b/src/ui/scrollwidget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "scrollwidget.h" 23#include "scrollwidget.h"
2#include "paint.h" 24#include "paint.h"
3#include "util.h" 25#include "util.h"
@@ -39,6 +61,7 @@ static iRect thumbRect_ScrollWidget_(const iScrollWidget *d) {
39 const int total = size_Range(&d->range); 61 const int total = size_Range(&d->range);
40 if (total > 0) { 62 if (total > 0) {
41 const int tsize = thumbSize_ScrollWidget_(d); 63 const int tsize = thumbSize_ScrollWidget_(d);
64// iAssert(tsize <= height_Rect(bounds));
42 const int tpos = 65 const int tpos =
43 iClamp((float) d->thumb / (float) total, 0, 1) * (height_Rect(bounds) - tsize); 66 iClamp((float) d->thumb / (float) total, 0, 1) * (height_Rect(bounds) - tsize);
44 rect.pos.y = bounds.pos.y + tpos; 67 rect.pos.y = bounds.pos.y + tpos;
diff --git a/src/ui/scrollwidget.h b/src/ui/scrollwidget.h
index 7b44dced..e6cda03d 100644
--- a/src/ui/scrollwidget.h
+++ b/src/ui/scrollwidget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include "widget.h" 25#include "widget.h"
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 71b641d4..40c3f55e 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -1,16 +1,41 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "sidebarwidget.h" 23#include "sidebarwidget.h"
2#include "labelwidget.h" 24
3#include "scrollwidget.h" 25#include "app.h"
26#include "bookmarks.h"
27#include "command.h"
4#include "documentwidget.h" 28#include "documentwidget.h"
29#include "gmcerts.h"
30#include "gmdocument.h"
5#include "inputwidget.h" 31#include "inputwidget.h"
6#include "bookmarks.h" 32#include "labelwidget.h"
7#include "paint.h" 33#include "paint.h"
34#include "scrollwidget.h"
8#include "util.h" 35#include "util.h"
9#include "command.h" 36#include "visited.h"
10#include "../gmdocument.h"
11#include "app.h"
12 37
13#include <the_Foundation/array.h> 38#include <the_Foundation/stringarray.h>
14#include <SDL_clipboard.h> 39#include <SDL_clipboard.h>
15#include <SDL_mouse.h> 40#include <SDL_mouse.h>
16 41
@@ -23,6 +48,8 @@ struct Impl_SidebarItem {
23 iString label; 48 iString label;
24 iString meta; 49 iString meta;
25 iString url; 50 iString url;
51 iBool isSeparator;
52 iBool isSelected;
26}; 53};
27 54
28void init_SidebarItem(iSidebarItem *d) { 55void init_SidebarItem(iSidebarItem *d) {
@@ -32,6 +59,8 @@ void init_SidebarItem(iSidebarItem *d) {
32 init_String(&d->label); 59 init_String(&d->label);
33 init_String(&d->meta); 60 init_String(&d->meta);
34 init_String(&d->url); 61 init_String(&d->url);
62 d->isSeparator = iFalse;
63 d->isSelected = iFalse;
35} 64}
36 65
37void deinit_SidebarItem(iSidebarItem *d) { 66void deinit_SidebarItem(iSidebarItem *d) {
@@ -49,6 +78,7 @@ struct Impl_SidebarWidget {
49 enum iSidebarMode mode; 78 enum iSidebarMode mode;
50 iScrollWidget *scroll; 79 iScrollWidget *scroll;
51 int scrollY; 80 int scrollY;
81 int modeScroll[max_SidebarMode];
52 int width; 82 int width;
53 iLabelWidget *modeButtons[max_SidebarMode]; 83 iLabelWidget *modeButtons[max_SidebarMode];
54 int itemHeight; 84 int itemHeight;
@@ -141,12 +171,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
141 item.icon = bm->icon; 171 item.icon = bm->icon;
142 set_String(&item.url, &bm->url); 172 set_String(&item.url, &bm->url);
143 set_String(&item.label, &bm->title); 173 set_String(&item.label, &bm->title);
144// iDate date;
145// init_Date(&date, &bm->when);
146// iString *ds = format_Date(&date, "%Y %b %d");
147// set_String(&item.meta, ds);
148 set_String(&item.meta, &bm->tags); 174 set_String(&item.meta, &bm->tags);
149// delete_String(ds);
150 pushBack_Array(&d->items, &item); 175 pushBack_Array(&d->items, &item);
151 } 176 }
152 d->menu = makeMenu_Widget( 177 d->menu = makeMenu_Widget(
@@ -158,8 +183,97 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
158 4); 183 4);
159 break; 184 break;
160 } 185 }
161 case history_SidebarMode: 186 case history_SidebarMode: {
187 iDate on;
188 initCurrent_Date(&on);
189 const int thisYear = on.year;
190 iConstForEach(PtrArray, i, list_Visited(visited_App(), 200)) {
191 const iVisitedUrl *visit = i.ptr;
192 iSidebarItem item;
193 init_SidebarItem(&item);
194 set_String(&item.url, &visit->url);
195 iDate date;
196 init_Date(&date, &visit->when);
197 if (date.day != on.day || date.month != on.month || date.year != on.year) {
198 on = date;
199 /* Date separator. */
200 iSidebarItem sep;
201 init_SidebarItem(&sep);
202 sep.isSeparator = iTrue;
203 set_String(&sep.meta,
204 collect_String(format_Date(
205 &date, date.year != thisYear ? "%b %d %Y" : "%b %d")));
206 pushBack_Array(&d->items, &sep);
207 /* Date separators are two items tall. */
208 init_SidebarItem(&sep);
209 sep.isSeparator = iTrue;
210 pushBack_Array(&d->items, &sep);
211 }
212 pushBack_Array(&d->items, &item);
213 }
214 d->menu = makeMenu_Widget(
215 as_Widget(d),
216 (iMenuItem[]){
217 { "Copy URL", 0, 0, "history.copy" },
218 { "Add Bookmark...", 0, 0, "history.addbookmark" },
219 { "---", 0, 0, NULL },
220 { "Remove URL", 0, 0, "history.delete" },
221 { "---", 0, 0, NULL },
222 { uiTextCaution_ColorEscape "Clear History...", 0, 0, "history.clear confirm:1" },
223 }, 6);
224 break;
225 }
226 case identities_SidebarMode: {
227 const iString *tabUrl = url_DocumentWidget(document_App());
228 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) {
229 const iGmIdentity *ident = i.ptr;
230 iSidebarItem item;
231 init_SidebarItem(&item);
232 item.id = index_PtrArrayConstIterator(&i);
233 item.icon = ident->icon;
234 set_String(&item.label, collect_String(subject_TlsCertificate(ident->cert)));
235 iDate until;
236 validUntil_TlsCertificate(ident->cert, &until);
237 const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl);
238 format_String(
239 &item.meta,
240 "%s",
241 isActive ? "Using"
242 : isUsed_GmIdentity(ident)
243 ? format_CStr("Used on %zu URLs", size_StringSet(ident->useUrls))
244 : "Not used");
245 const char *expiry =
246 ident->flags & temporary_GmIdentityFlag
247 ? "Temporary"
248 : cstrCollect_String(format_Date(&until, "Expires %b %d, %Y"));
249 if (isEmpty_String(&ident->notes)) {
250 appendFormat_String(&item.meta, "\n%s", expiry);
251 }
252 else {
253 appendFormat_String(&item.meta,
254 " \u2014 %s\n%s%s",
255 expiry,
256 escape_Color(uiHeading_ColorId),
257 cstr_String(&ident->notes));
258 }
259 item.isSelected = isActive;
260 pushBack_Array(&d->items, &item);
261 }
262 const iMenuItem menuItems[] = {
263 { "Use on This Page", 0, 0, "ident.use arg:1" },
264 { "Stop Using This Page", 0, 0, "ident.use arg:0" },
265 { "Stop Using Everywhere", 0, 0, "ident.use arg:0 clear:1" },
266 { "Show Usage", 0, 0, "ident.showuse" },
267 { "---", 0, 0, NULL },
268 { "Edit Notes...", 0, 0, "ident.edit" },
269 { "Pick Icon...", 0, 0, "ident.pickicon" },
270 { "---", 0, 0, NULL },
271 { "Reveal Files", 0, 0, "ident.reveal" },
272 { uiTextCaution_ColorEscape "Delete Identity...", 0, 0, "ident.delete confirm:1" },
273 };
274 d->menu = makeMenu_Widget(as_Widget(d), menuItems, iElemCount(menuItems));
162 break; 275 break;
276 }
163 default: 277 default:
164 break; 278 break;
165 } 279 }
@@ -167,14 +281,35 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
167 invalidate_SidebarWidget_(d); 281 invalidate_SidebarWidget_(d);
168} 282}
169 283
284static void scroll_SidebarWidget_(iSidebarWidget *d, int offset) {
285 const int oldScroll = d->scrollY;
286 d->scrollY += offset;
287 if (d->scrollY < 0) {
288 d->scrollY = 0;
289 }
290 const int scrollMax = scrollMax_SidebarWidget_(d);
291 d->scrollY = iMin(d->scrollY, scrollMax);
292 if (oldScroll != d->scrollY) {
293 d->hoverItem = iInvalidPos;
294 updateVisible_SidebarWidget_(d);
295 invalidate_SidebarWidget_(d);
296 }
297}
298
170void setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { 299void setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) {
171 if (d->mode == mode) return; 300 if (d->mode == mode) return;
301 if (d->mode >= 0 && d->mode < max_SidebarMode) {
302 d->modeScroll[d->mode] = d->scrollY; /* saved for later */
303 }
172 d->mode = mode; 304 d->mode = mode;
173 for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) { 305 for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) {
174 setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode); 306 setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode);
175 } 307 }
176 const float heights[max_SidebarMode] = { 1.333f, 3, 3, 1.2f }; 308 const float heights[max_SidebarMode] = { 1.333f, 1.333f, 3.5f, 1.2f };
177 d->itemHeight = heights[mode] * lineHeight_Text(uiContent_FontId); 309 d->itemHeight = heights[mode] * lineHeight_Text(uiContent_FontId);
310 invalidate_SidebarWidget_(d);
311 /* Restore previous scroll position. */
312 d->scrollY = d->modeScroll[mode];
178} 313}
179 314
180enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) { 315enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) {
@@ -209,8 +344,9 @@ void init_SidebarWidget(iSidebarWidget *d) {
209 resizeWidthOfChildren_WidgetFlag | collapse_WidgetFlag, 344 resizeWidthOfChildren_WidgetFlag | collapse_WidgetFlag,
210 iTrue); 345 iTrue);
211 d->scrollY = 0; 346 d->scrollY = 0;
212 d->mode = -1; 347 iZap(d->modeScroll);
213 d->width = 75 * gap_UI; 348 d->mode = -1;
349 d->width = 60 * gap_UI;
214 init_Array(&d->items, sizeof(iSidebarItem)); 350 init_Array(&d->items, sizeof(iSidebarItem));
215 d->hoverItem = iInvalidPos; 351 d->hoverItem = iInvalidPos;
216 init_Click(&d->click, d, SDL_BUTTON_LEFT); 352 init_Click(&d->click, d, SDL_BUTTON_LEFT);
@@ -220,7 +356,7 @@ void init_SidebarWidget(iSidebarWidget *d) {
220 d->modeButtons[i] = addChildFlags_Widget( 356 d->modeButtons[i] = addChildFlags_Widget(
221 w, 357 w,
222 iClob( 358 iClob(
223 new_LabelWidget(normalModeLabels_[i], 0, 0, format_CStr("sidebar.mode arg:%d", i))), 359 new_LabelWidget(tightModeLabels_[i], 0, 0, format_CStr("sidebar.mode arg:%d", i))),
224 frameless_WidgetFlag | expand_WidgetFlag); 360 frameless_WidgetFlag | expand_WidgetFlag);
225 d->maxButtonLabelWidth = 361 d->maxButtonLabelWidth =
226 iMaxi(d->maxButtonLabelWidth, 362 iMaxi(d->maxButtonLabelWidth,
@@ -270,7 +406,53 @@ static size_t itemIndex_SidebarWidget_(const iSidebarWidget *d, iInt2 pos) {
270 return index; 406 return index;
271} 407}
272 408
409static const iSidebarItem *constHoverItem_SidebarWidget_(const iSidebarWidget *d) {
410 if (d->hoverItem < size_Array(&d->items)) {
411 return constAt_Array(&d->items, d->hoverItem);
412 }
413 return NULL;
414}
415
416static iSidebarItem *hoverItem_SidebarWidget_(iSidebarWidget *d) {
417 if (d->hoverItem < size_Array(&d->items)) {
418 return at_Array(&d->items, d->hoverItem);
419 }
420 return NULL;
421}
422
423static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) {
424 if (d->mode == identities_SidebarMode) {
425 const iSidebarItem *hoverItem = constHoverItem_SidebarWidget_(d);
426 if (hoverItem) {
427 return identity_GmCerts(certs_App(), hoverItem->id);
428 }
429 }
430 return NULL;
431}
432
433static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) {
434 return iConstCast(iGmIdentity *, constHoverIdentity_SidebarWidget_(d));
435}
436
437static void setHoverItem_SidebarWidget_(iSidebarWidget *d, size_t index) {
438 if (index < size_Array(&d->items)) {
439 if (constValue_Array(&d->items, index, iSidebarItem).isSeparator) {
440 index = iInvalidPos;
441 }
442 }
443 if (d->hoverItem != index) {
444 d->hoverItem = index;
445 invalidate_SidebarWidget_(d);
446 }
447}
448
449static void updateMouseHover_SidebarWidget_(iSidebarWidget *d) {
450 const iInt2 mouse = mouseCoord_Window(get_Window());
451 setHoverItem_SidebarWidget_(d, itemIndex_SidebarWidget_(d, mouse));
452}
453
273static void itemClicked_SidebarWidget_(iSidebarWidget *d, size_t index) { 454static void itemClicked_SidebarWidget_(iSidebarWidget *d, size_t index) {
455 setFocus_Widget(NULL);
274 const iSidebarItem *item = constAt_Array(&d->items, index); 456 const iSidebarItem *item = constAt_Array(&d->items, index);
275 switch (d->mode) { 457 switch (d->mode) {
276 case documentOutline_SidebarMode: { 458 case documentOutline_SidebarMode: {
@@ -279,25 +461,30 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, size_t index) {
279 postCommandf_App("document.goto loc:%p", head->text.start); 461 postCommandf_App("document.goto loc:%p", head->text.start);
280 break; 462 break;
281 } 463 }
282 case bookmarks_SidebarMode: { 464 case bookmarks_SidebarMode:
283 postCommandf_App("open url:%s", cstr_String(&item->url)); 465 case history_SidebarMode: {
466 if (!isEmpty_String(&item->url)) {
467 postCommandf_App("open url:%s", cstr_String(&item->url));
468 }
284 break; 469 break;
285 } 470 }
286 } 471 case identities_SidebarMode: {
287} 472 iGmIdentity *ident = hoverIdentity_SidebarWidget_(d);
288 473 if (ident) {
289static void scroll_SidebarWidget_(iSidebarWidget *d, int offset) { 474 const iString *tabUrl = url_DocumentWidget(document_App());
290 const int oldScroll = d->scrollY; 475 if (isUsedOn_GmIdentity(ident, tabUrl)) {
291 d->scrollY += offset; 476 signOut_GmCerts(certs_App(), tabUrl);
292 if (d->scrollY < 0) { 477 }
293 d->scrollY = 0; 478 else {
294 } 479 signIn_GmCerts(certs_App(), ident, tabUrl);
295 const int scrollMax = scrollMax_SidebarWidget_(d); 480 }
296 d->scrollY = iMin(d->scrollY, scrollMax); 481 updateItems_SidebarWidget_(d);
297 if (oldScroll != d->scrollY) { 482 updateMouseHover_SidebarWidget_(d);
298 d->hoverItem = iInvalidPos; 483 }
299 updateVisible_SidebarWidget_(d); 484 break;
300 invalidate_SidebarWidget_(d); 485 }
486 default:
487 break;
301 } 488 }
302} 489}
303 490
@@ -316,13 +503,6 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) {
316 } 503 }
317} 504}
318 505
319static iSidebarItem *hoverItem_SidebarWidget_(iSidebarWidget *d) {
320 if (d->hoverItem < size_Array(&d->items)) {
321 return at_Array(&d->items, d->hoverItem);
322 }
323 return NULL;
324}
325
326void setWidth_SidebarWidget(iSidebarWidget *d, int width) { 506void setWidth_SidebarWidget(iSidebarWidget *d, int width) {
327 iWidget *w = as_Widget(d); 507 iWidget *w = as_Widget(d);
328 width = iMax(30 * gap_UI, width); 508 width = iMax(30 * gap_UI, width);
@@ -339,15 +519,18 @@ void setWidth_SidebarWidget(iSidebarWidget *d, int width) {
339} 519}
340 520
341iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *cmd) { 521iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *cmd) {
342 iSidebarWidget *d = findWidget_App("sidebar");
343 if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) { 522 if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) {
523 iSidebarWidget *d = findWidget_App("sidebar");
344 if (equal_Command(cmd, "bmed.accept")) { 524 if (equal_Command(cmd, "bmed.accept")) {
525 const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title"));
526 const iString *url = text_InputWidget(findChild_Widget(editor, "bmed.url"));
527 const iString *tags = text_InputWidget(findChild_Widget(editor, "bmed.tags"));
345 const iSidebarItem *item = hoverItem_SidebarWidget_(d); 528 const iSidebarItem *item = hoverItem_SidebarWidget_(d);
346 iAssert(item); /* hover item cannot have been changed */ 529 iAssert(item); /* hover item cannot have been changed */
347 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); 530 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id);
348 set_String(&bm->title, text_InputWidget(findChild_Widget(editor, "bmed.title"))); 531 set_String(&bm->title, title);
349 set_String(&bm->url, text_InputWidget(findChild_Widget(editor, "bmed.url"))); 532 set_String(&bm->url, url);
350 set_String(&bm->tags, text_InputWidget(findChild_Widget(editor, "bmed.tags"))); 533 set_String(&bm->tags, tags);
351 postCommand_App("bookmarks.changed"); 534 postCommand_App("bookmarks.changed");
352 } 535 }
353 setFlags_Widget(as_Widget(d), disabled_WidgetFlag, iFalse); 536 setFlags_Widget(as_Widget(d), disabled_WidgetFlag, iFalse);
@@ -399,11 +582,15 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
399 setMode_SidebarWidget(d, arg_Command(cmd)); 582 setMode_SidebarWidget(d, arg_Command(cmd));
400 updateItems_SidebarWidget_(d); 583 updateItems_SidebarWidget_(d);
401 if (argLabel_Command(cmd, "show") && !isVisible_Widget(w)) { 584 if (argLabel_Command(cmd, "show") && !isVisible_Widget(w)) {
402 postCommand_App("sidebar.toggle"); 585 postCommand_App("sidebar.toggle arg:1");
403 } 586 }
587 scroll_SidebarWidget_(d, 0);
404 return iTrue; 588 return iTrue;
405 } 589 }
406 else if (equal_Command(cmd, "sidebar.toggle")) { 590 else if (equal_Command(cmd, "sidebar.toggle")) {
591 if (arg_Command(cmd) && isVisible_Widget(w)) {
592 return iTrue;
593 }
407 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); 594 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w));
408 if (isVisible_Widget(w)) { 595 if (isVisible_Widget(w)) {
409 w->rect.size.x = d->width; 596 w->rect.size.x = d->width;
@@ -421,7 +608,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
421 return iTrue; 608 return iTrue;
422 } 609 }
423 else if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) { 610 else if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) {
424 d->scrollY = 0;
425 updateItems_SidebarWidget_(d); 611 updateItems_SidebarWidget_(d);
426 } 612 }
427 else if (equal_Command(cmd, "theme.changed")) { 613 else if (equal_Command(cmd, "theme.changed")) {
@@ -455,25 +641,156 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
455 } 641 }
456 return iTrue; 642 return iTrue;
457 } 643 }
458 else if (equal_Command(cmd, "bookmarks.changed")) { 644 else if (equal_Command(cmd, "bookmarks.changed") && d->mode == bookmarks_SidebarMode) {
645 updateItems_SidebarWidget_(d);
646 }
647 else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) {
648 updateItems_SidebarWidget_(d);
649 }
650 else if (isCommand_Widget(w, ev, "ident.use")) {
651 iGmIdentity * ident = hoverIdentity_SidebarWidget_(d);
652 const iString *tabUrl = url_DocumentWidget(document_App());
653 if (ident) {
654 if (argLabel_Command(cmd, "clear")) {
655 clearUse_GmIdentity(ident);
656 }
657 else if (arg_Command(cmd)) {
658 signIn_GmCerts(certs_App(), ident, tabUrl);
659 }
660 else {
661 signOut_GmCerts(certs_App(), tabUrl);
662 }
663 updateItems_SidebarWidget_(d);
664 }
665 return iTrue;
666 }
667 else if (isCommand_Widget(w, ev, "ident.showuse")) {
668 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
669 if (ident) {
670 makeMessage_Widget(uiHeading_ColorEscape "IDENTITY USAGE",
671 cstrCollect_String(joinCStr_StringSet(ident->useUrls, "\n")));
672 }
673 return iTrue;
674 }
675 else if (isCommand_Widget(w, ev, "ident.edit")) {
676 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
677 if (ident) {
678 makeValueInput_Widget(get_Window()->root,
679 &ident->notes,
680 uiHeading_ColorEscape "IDENTITY NOTES",
681 format_CStr("Notes about %s:", cstr_String(name_GmIdentity(ident))),
682 uiTextAction_ColorEscape "OK",
683 format_CStr("ident.setnotes ident:%p", ident));
684 }
685 return iTrue;
686 }
687 else if (equal_Command(cmd, "ident.setnotes")) {
688 iGmIdentity *ident = pointerLabel_Command(cmd, "ident");
689 if (ident) {
690 setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value"));
691 updateItems_SidebarWidget_(d);
692 }
693 return iTrue;
694 }
695 else if (isCommand_Widget(w, ev, "ident.pickicon")) {
696 return iTrue;
697 }
698 else if (isCommand_Widget(w, ev, "ident.reveal")) {
699 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
700 if (ident) {
701 const iString *crtPath = certificatePath_GmCerts(certs_App(), ident);
702 if (crtPath) {
703 revealPath_App(crtPath);
704 }
705 }
706 return iTrue;
707 }
708 else if (equal_Command(cmd, "ident.delete")) {
709 iSidebarItem *item = hoverItem_SidebarWidget_(d);
710 if (argLabel_Command(cmd, "confirm")) {
711 makeQuestion_Widget(uiTextCaution_ColorEscape "DELETE IDENTITY",
712 format_CStr("Do you really want to delete the identity\n"
713 uiTextAction_ColorEscape "%s\n"
714 uiText_ColorEscape
715 "including its certificate and private key files?",
716 cstr_String(&item->label)),
717 (const char *[]){ "Cancel",
718 uiTextCaution_ColorEscape
719 "Delete Identity and Files" },
720 (const char *[]){ "cancel", "ident.delete confirm:0" },
721 2);
722 return iTrue;
723 }
724 deleteIdentity_GmCerts(certs_App(), hoverIdentity_SidebarWidget_(d));
459 updateItems_SidebarWidget_(d); 725 updateItems_SidebarWidget_(d);
726 return iTrue;
727 }
728 else if (equal_Command(cmd, "history.delete")) {
729 const iSidebarItem *item = hoverItem_SidebarWidget_(d);
730 if (item && !isEmpty_String(&item->url)) {
731 removeUrl_Visited(visited_App(), &item->url);
732 updateItems_SidebarWidget_(d);
733 scroll_SidebarWidget_(d, 0);
734 }
735 return iTrue;
736 }
737 else if (equal_Command(cmd, "history.copy")) {
738 const iSidebarItem *item = hoverItem_SidebarWidget_(d);
739 if (item && !isEmpty_String(&item->url)) {
740 SDL_SetClipboardText(cstr_String(&item->url));
741 }
742 return iTrue;
743 }
744 else if (equal_Command(cmd, "history.addbookmark")) {
745 const iSidebarItem *item = hoverItem_SidebarWidget_(d);
746 if (!isEmpty_String(&item->url)) {
747 makeBookmarkCreation_Widget(
748 &item->url,
749 collect_String(newRange_String(urlHost_String(&item->url))),
750 0x1f310 /* globe */);
751 }
752 }
753 else if (equal_Command(cmd, "history.clear")) {
754 if (argLabel_Command(cmd, "confirm")) {
755 makeQuestion_Widget(
756 uiTextCaution_ColorEscape "CLEAR HISTORY",
757 "Do you really want to erase the history of all visited pages?",
758 (const char *[]){ "Cancel", uiTextCaution_ColorEscape "Clear History" },
759 (const char *[]){ "cancel", "history.clear confirm:0" },
760 2);
761 }
762 else {
763 clear_Visited(visited_App());
764 updateItems_SidebarWidget_(d);
765 scroll_SidebarWidget_(d, 0);
766 }
767 return iTrue;
460 } 768 }
461 } 769 }
462 if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) { 770 if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) {
463 const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y); 771 const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y);
464 size_t hover = iInvalidPos; 772 size_t hover = iInvalidPos;
465 if (contains_Widget(d->resizer, mouse)) { 773 if (contains_Widget(d->resizer, mouse)) {
466 SDL_SetCursor(d->resizeCursor); 774 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_SIZEWE);
467 } 775 }
468 else { 776 else if (contains_Widget(constAs_Widget(d->scroll), mouse)) {
469 SDL_SetCursor(NULL); 777 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
470 if (contains_Widget(w, mouse)) {
471 hover = itemIndex_SidebarWidget_(d, mouse);
472 }
473 } 778 }
474 if (hover != d->hoverItem) { 779 else if (contains_Widget(w, mouse)) {
475 d->hoverItem = hover; 780 hover = itemIndex_SidebarWidget_(d, mouse);
476 invalidate_SidebarWidget_(d); 781 }
782 setHoverItem_SidebarWidget_(d, hover);
783 /* Update cursor. */
784 if (contains_Widget(w, mouse) && !contains_Widget(d->resizer, mouse) &&
785 !contains_Widget(constAs_Widget(d->scroll), mouse)) {
786 const iSidebarItem *item = constHoverItem_SidebarWidget_(d);
787 if (item && d->mode != identities_SidebarMode) {
788 setCursor_Window(get_Window(), item->isSeparator ? SDL_SYSTEM_CURSOR_ARROW
789 : SDL_SYSTEM_CURSOR_HAND);
790 }
791 else {
792 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
793 }
477 } 794 }
478 } 795 }
479 if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { 796 if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) {
@@ -486,10 +803,42 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
486 return iTrue; 803 return iTrue;
487 } 804 }
488 if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) { 805 if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) {
489 if (d->hoverItem != iInvalidPos || isVisible_Widget(d->menu)) { 806 if (ev->button.button == SDL_BUTTON_RIGHT) {
490 processContextMenuEvent_Widget(d->menu, ev, {}); 807 if (!isVisible_Widget(d->menu)) {
808 setHoverItem_SidebarWidget_(
809 d, itemIndex_SidebarWidget_(d, init_I2(ev->button.x, ev->button.y)));
810 }
811 if (d->hoverItem != iInvalidPos || isVisible_Widget(d->menu)) {
812 /* Update menu items. */
813 if (d->mode == identities_SidebarMode) {
814 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
815 const iString * docUrl = url_DocumentWidget(document_App());
816 iForEach(ObjectList, i, children_Widget(d->menu)) {
817 if (isInstance_Object(i.object, &Class_LabelWidget)) {
818 iLabelWidget *menuItem = i.object;
819 const char * cmdItem = cstr_String(command_LabelWidget(menuItem));
820 if (equal_Command(cmdItem, "ident.use")) {
821 const iBool cmdUse = arg_Command(cmdItem) != 0;
822 const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0;
823 setFlags_Widget(
824 as_Widget(menuItem),
825 disabled_WidgetFlag,
826 (cmdClear && !isUsed_GmIdentity(ident)) ||
827 (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) ||
828 (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl)));
829 }
830 else if (equal_Command(cmdItem, "ident.showuse")) {
831 setFlags_Widget(as_Widget(menuItem),
832 disabled_WidgetFlag,
833 !isUsed_GmIdentity(ident));
834 }
835 }
836 }
837 }
838 }
491 } 839 }
492 } 840 }
841 processContextMenuEvent_Widget(d->menu, ev, {});
493 switch (processEvent_Click(&d->click, ev)) { 842 switch (processEvent_Click(&d->click, ev)) {
494 case started_ClickResult: 843 case started_ClickResult:
495 invalidate_SidebarWidget_(d); 844 invalidate_SidebarWidget_(d);
@@ -524,8 +873,8 @@ static void allocVisBuffer_SidebarWidget_(iSidebarWidget *d) {
524} 873}
525 874
526static void draw_SidebarWidget_(const iSidebarWidget *d) { 875static void draw_SidebarWidget_(const iSidebarWidget *d) {
527 const iWidget *w = constAs_Widget(d); 876 const iWidget *w = constAs_Widget(d);
528 const iRect bounds = contentBounds_SidebarWidget_(d); 877 const iRect bounds = contentBounds_SidebarWidget_(d);
529 const iBool isPressing = d->click.isActive && contains_Rect(bounds, pos_Click(&d->click)); 878 const iBool isPressing = d->click.isActive && contains_Rect(bounds, pos_Click(&d->click));
530 iPaint p; 879 iPaint p;
531 init_Paint(&p); 880 init_Paint(&p);
@@ -545,9 +894,12 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
545 for (size_t i = visRange.start; i < visRange.end; i++) { 894 for (size_t i = visRange.start; i < visRange.end; i++) {
546 const iSidebarItem *item = constAt_Array(&d->items, i); 895 const iSidebarItem *item = constAt_Array(&d->items, i);
547 const iRect itemRect = { pos, init_I2(width_Rect(bufBounds), d->itemHeight) }; 896 const iRect itemRect = { pos, init_I2(width_Rect(bufBounds), d->itemHeight) };
548 const iBool isHover = (d->hoverItem == i); 897 const iBool isHover = isHover_Widget(w) && (d->hoverItem == i);
898 const int iconColor =
899 isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId)
900 : uiIcon_ColorId;
549 setClip_Paint(&p, intersect_Rect(itemRect, bufBounds)); 901 setClip_Paint(&p, intersect_Rect(itemRect, bufBounds));
550 if (isHover) { 902 if (isHover && !item->isSeparator) {
551 fillRect_Paint(&p, 903 fillRect_Paint(&p,
552 itemRect, 904 itemRect,
553 isPressing ? uiBackgroundPressed_ColorId 905 isPressing ? uiBackgroundPressed_ColorId
@@ -576,8 +928,7 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
576 font, 928 font,
577 iconArea, 929 iconArea,
578 iTrue, 930 iTrue,
579 isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId) 931 iconColor,
580 : uiIcon_ColorId,
581 "%s", 932 "%s",
582 cstr_String(&str)); 933 cstr_String(&str));
583 deinit_String(&str); 934 deinit_String(&str);
@@ -585,6 +936,81 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
585 (d->itemHeight - lineHeight_Text(font)) / 2); 936 (d->itemHeight - lineHeight_Text(font)) / 2);
586 drawRange_Text(font, textPos, fg, range_String(&item->label)); 937 drawRange_Text(font, textPos, fg, range_String(&item->label));
587 } 938 }
939 else if (d->mode == history_SidebarMode) {
940 iBeginCollect();
941 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId
942 : uiTextFramelessHover_ColorId)
943 : uiText_ColorId;
944 if (item->isSeparator) {
945 if (!isEmpty_String(&item->meta)) {
946 unsetClip_Paint(&p);
947 iInt2 drawPos = addY_I2(topLeft_Rect(itemRect), d->itemHeight * 0.666f);
948 drawHLine_Paint(
949 &p, drawPos, width_Rect(itemRect), uiIcon_ColorId);
950 drawRange_Text(
951 default_FontId,
952 add_I2(drawPos,
953 init_I2(3 * gap_UI,
954 (d->itemHeight - lineHeight_Text(default_FontId)) / 2)),
955 uiIcon_ColorId,
956 range_String(&item->meta));
957 }
958 }
959 else {
960 iUrl parts;
961 init_Url(&parts, &item->url);
962 const iBool isGemini = equalCase_Rangecc(parts.scheme, "gemini");
963 draw_Text(
964 font,
965 add_I2(topLeft_Rect(itemRect),
966 init_I2(3 * gap_UI, (d->itemHeight - lineHeight_Text(font)) / 2)),
967 fg,
968 "%s%s%s%s%s%s",
969 isGemini ? "" : cstr_Rangecc(parts.scheme),
970 isGemini ? "" : "://",
971 escape_Color(isHover ? (isPressing ? uiTextPressed_ColorId
972 : uiTextFramelessHover_ColorId)
973 : uiTextStrong_ColorId),
974 cstr_Rangecc(parts.host),
975 escape_Color(fg),
976 cstr_Rangecc(parts.path));
977 }
978 iEndCollect();
979 }
980 else if (d->mode == identities_SidebarMode) {
981 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId
982 : uiTextFramelessHover_ColorId)
983 : uiTextStrong_ColorId;
984 if (item->isSelected) {
985 drawRectThickness_Paint(&p,
986 adjusted_Rect(itemRect, zero_I2(), init_I2(-2, -1)),
987 gap_UI / 4,
988 isHover && isPressing ? uiTextPressed_ColorId
989 : uiIcon_ColorId);
990 }
991 iString icon;
992 initUnicodeN_String(&icon, &item->icon, 1);
993 iInt2 cPos = topLeft_Rect(itemRect);
994 addv_I2(&cPos,
995 init_I2(3 * gap_UI,
996 (d->itemHeight - lineHeight_Text(default_FontId) * 2 -
997 lineHeight_Text(font)) /
998 2));
999 const int metaFg =
1000 isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId
1001 : uiTextFramelessHover_ColorId)
1002 : uiText_ColorId;
1003 drawRange_Text(
1004 font, cPos, item->isSelected ? iconColor : metaFg, range_String(&icon));
1005 deinit_String(&icon);
1006 drawRange_Text(font, add_I2(cPos, init_I2(6 * gap_UI, 0)),
1007 fg, range_String(&item->label));
1008 drawRange_Text(
1009 default_FontId,
1010 add_I2(cPos, init_I2(6 * gap_UI, lineHeight_Text(font))),
1011 metaFg,
1012 range_String(&item->meta));
1013 }
588 unsetClip_Paint(&p); 1014 unsetClip_Paint(&p);
589 pos.y += d->itemHeight; 1015 pos.y += d->itemHeight;
590 } 1016 }
diff --git a/src/ui/sidebarwidget.h b/src/ui/sidebarwidget.h
index 3ff20b7f..7ccc64dd 100644
--- a/src/ui/sidebarwidget.h
+++ b/src/ui/sidebarwidget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include "widget.h" 25#include "widget.h"
diff --git a/src/ui/text.c b/src/ui/text.c
index 1e702eee..836d540f 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "text.h" 23#include "text.h"
2#include "color.h" 24#include "color.h"
3#include "metrics.h" 25#include "metrics.h"
@@ -11,6 +33,7 @@
11#include <the_Foundation/file.h> 33#include <the_Foundation/file.h>
12#include <the_Foundation/hash.h> 34#include <the_Foundation/hash.h>
13#include <the_Foundation/math.h> 35#include <the_Foundation/math.h>
36#include <the_Foundation/stringlist.h>
14#include <the_Foundation/regexp.h> 37#include <the_Foundation/regexp.h>
15#include <the_Foundation/path.h> 38#include <the_Foundation/path.h>
16#include <the_Foundation/vec2.h> 39#include <the_Foundation/vec2.h>
@@ -25,6 +48,7 @@ iDeclareTypeConstructionArgs(Glyph, iChar ch)
25 48
26int gap_Text; /* cf. gap_UI in metrics.h */ 49int gap_Text; /* cf. gap_UI in metrics.h */
27int enableHalfPixelGlyphs_Text = iTrue; /* debug setting */ 50int enableHalfPixelGlyphs_Text = iTrue; /* debug setting */
51int enableKerning_Text = iTrue; /* looking up kern pairs is slow */
28 52
29struct Impl_Glyph { 53struct Impl_Glyph {
30 iHashNode node; 54 iHashNode node;
@@ -62,6 +86,7 @@ struct Impl_Font {
62 int baseline; 86 int baseline;
63 iHash glyphs; 87 iHash glyphs;
64 iBool isMonospaced; 88 iBool isMonospaced;
89 iBool manualKernOnly;
65 enum iFontId symbolsFont; /* font to use for symbols */ 90 enum iFontId symbolsFont; /* font to use for symbols */
66}; 91};
67 92
@@ -120,7 +145,7 @@ static void initFonts_Text_(iText *d) {
120 int symbolsFont; 145 int symbolsFont;
121 } fontData[max_FontId] = { 146 } fontData[max_FontId] = {
122 { &fontSourceSansProRegular_Embedded, fontSize_UI, defaultSymbols_FontId }, 147 { &fontSourceSansProRegular_Embedded, fontSize_UI, defaultSymbols_FontId },
123 { &fontSourceSansProRegular_Embedded, fontSize_UI * 1.150f, defaultMediumSymbols_FontId }, 148 { &fontSourceSansProRegular_Embedded, fontSize_UI * 1.125f, defaultMediumSymbols_FontId },
124 { &fontFiraMonoRegular_Embedded, fontSize_UI * 0.866f, defaultSymbols_FontId }, 149 { &fontFiraMonoRegular_Embedded, fontSize_UI * 0.866f, defaultSymbols_FontId },
125 { &fontFiraSansRegular_Embedded, textSize, symbols_FontId }, 150 { &fontFiraSansRegular_Embedded, textSize, symbols_FontId },
126 { &fontFiraMonoRegular_Embedded, textSize * 0.866f, smallSymbols_FontId }, 151 { &fontFiraMonoRegular_Embedded, textSize * 0.866f, smallSymbols_FontId },
@@ -133,14 +158,14 @@ static void initFonts_Text_(iText *d) {
133 { &fontFiraSansBold_Embedded, textSize * 2.000f, hugeSymbols_FontId }, 158 { &fontFiraSansBold_Embedded, textSize * 2.000f, hugeSymbols_FontId },
134 { &fontFiraSansLight_Embedded, textSize * 1.666f, largeSymbols_FontId }, 159 { &fontFiraSansLight_Embedded, textSize * 1.666f, largeSymbols_FontId },
135 { &fontSymbola_Embedded, fontSize_UI, defaultSymbols_FontId }, 160 { &fontSymbola_Embedded, fontSize_UI, defaultSymbols_FontId },
136 { &fontSymbola_Embedded, fontSize_UI * 1.150f, defaultMediumSymbols_FontId }, 161 { &fontSymbola_Embedded, fontSize_UI * 1.125f, defaultMediumSymbols_FontId },
137 { &fontSymbola_Embedded, textSize, symbols_FontId }, 162 { &fontSymbola_Embedded, textSize, symbols_FontId },
138 { &fontSymbola_Embedded, textSize * 1.333f, mediumSymbols_FontId }, 163 { &fontSymbola_Embedded, textSize * 1.333f, mediumSymbols_FontId },
139 { &fontSymbola_Embedded, textSize * 1.666f, largeSymbols_FontId }, 164 { &fontSymbola_Embedded, textSize * 1.666f, largeSymbols_FontId },
140 { &fontSymbola_Embedded, textSize * 2.000f, hugeSymbols_FontId }, 165 { &fontSymbola_Embedded, textSize * 2.000f, hugeSymbols_FontId },
141 { &fontSymbola_Embedded, textSize * 0.866f, smallSymbols_FontId }, 166 { &fontSymbola_Embedded, textSize * 0.866f, smallSymbols_FontId },
142 { &fontNotoEmojiRegular_Embedded, fontSize_UI, defaultSymbols_FontId }, 167 { &fontNotoEmojiRegular_Embedded, fontSize_UI, defaultSymbols_FontId },
143 { &fontNotoEmojiRegular_Embedded, fontSize_UI * 1.150f, defaultMediumSymbols_FontId }, 168 { &fontNotoEmojiRegular_Embedded, fontSize_UI * 1.125f, defaultMediumSymbols_FontId },
144 { &fontNotoEmojiRegular_Embedded, textSize, symbols_FontId }, 169 { &fontNotoEmojiRegular_Embedded, textSize, symbols_FontId },
145 { &fontNotoEmojiRegular_Embedded, textSize * 1.333f, mediumSymbols_FontId }, 170 { &fontNotoEmojiRegular_Embedded, textSize * 1.333f, mediumSymbols_FontId },
146 { &fontNotoEmojiRegular_Embedded, textSize * 1.666f, largeSymbols_FontId }, 171 { &fontNotoEmojiRegular_Embedded, textSize * 1.666f, largeSymbols_FontId },
@@ -153,6 +178,9 @@ static void initFonts_Text_(iText *d) {
153 if (fontData[i].ttf == &fontFiraMonoRegular_Embedded) { 178 if (fontData[i].ttf == &fontFiraMonoRegular_Embedded) {
154 font->isMonospaced = iTrue; 179 font->isMonospaced = iTrue;
155 } 180 }
181 if (i == default_FontId || i == defaultMedium_FontId) {
182 font->manualKernOnly = iTrue;
183 }
156 } 184 }
157 gap_Text = iRound(gap_UI * d->contentFontSize); 185 gap_Text = iRound(gap_UI * d->contentFontSize);
158} 186}
@@ -287,8 +315,8 @@ static void cache_Font_(iFont *d, iGlyph *glyph, int hoff) {
287 /* Rasterize the glyph using stbtt. */ { 315 /* Rasterize the glyph using stbtt. */ {
288 surface = rasterizeGlyph_Font_(d, ch, hoff * 0.5f); 316 surface = rasterizeGlyph_Font_(d, ch, hoff * 0.5f);
289 if (hoff == 0) { 317 if (hoff == 0) {
290 int adv, lsb; 318 int adv;
291 stbtt_GetCodepointHMetrics(&d->font, ch, &adv, &lsb); 319 stbtt_GetCodepointHMetrics(&d->font, ch, &adv, NULL);
292 glyph->advance = d->scale * adv; 320 glyph->advance = d->scale * adv;
293 } 321 }
294 stbtt_GetCodepointBitmapBoxSubpixel(&d->font, 322 stbtt_GetCodepointBitmapBoxSubpixel(&d->font,
@@ -406,6 +434,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
406 /* ANSI escape. */ 434 /* ANSI escape. */
407 chPos++; 435 chPos++;
408 iRegExpMatch m; 436 iRegExpMatch m;
437 init_RegExpMatch(&m);
409 if (match_RegExp(text_.ansiEscape, chPos, text.end - chPos, &m)) { 438 if (match_RegExp(text_.ansiEscape, chPos, text.end - chPos, &m)) {
410 if (mode == draw_RunMode) { 439 if (mode == draw_RunMode) {
411 /* Change the color. */ 440 /* Change the color. */
@@ -431,7 +460,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
431 if (ch == '\r') { 460 if (ch == '\r') {
432 const iChar esc = nextChar_(&chPos, text.end); 461 const iChar esc = nextChar_(&chPos, text.end);
433 if (mode == draw_RunMode) { 462 if (mode == draw_RunMode) {
434 const iColor clr = get_Color(esc - '0'); 463 const iColor clr = get_Color(esc - asciiBase_ColorEscape);
435 SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); 464 SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b);
436 } 465 }
437 prevCh = 0; 466 prevCh = 0;
@@ -488,9 +517,11 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
488 /* Manual kerning for double-slash. */ 517 /* Manual kerning for double-slash. */
489 xpos -= glyph->rect[hoff].size.x * 0.5f; 518 xpos -= glyph->rect[hoff].size.x * 0.5f;
490 } 519 }
491 else if (next) { 520#if defined (LAGRANGE_ENABLE_KERNING)
521 else if (enableKerning_Text && !d->manualKernOnly && next) {
492 xpos += d->scale * stbtt_GetCodepointKernAdvance(&d->font, ch, next); 522 xpos += d->scale * stbtt_GetCodepointKernAdvance(&d->font, ch, next);
493 } 523 }
524#endif
494 } 525 }
495 prevCh = ch; 526 prevCh = ch;
496 if (--maxLen == 0) { 527 if (--maxLen == 0) {
@@ -658,6 +689,111 @@ SDL_Texture *glyphCache_Text(void) {
658 return text_.cache; 689 return text_.cache;
659} 690}
660 691
692static void freeBitmap_(void *ptr) {
693 stbtt_FreeBitmap(ptr, NULL);
694}
695
696iString *renderBlockChars_Text(const iBlock *fontData, int height, enum iTextBlockMode mode,
697 const iString *text) {
698 iBeginCollect();
699 stbtt_fontinfo font;
700 iZap(font);
701 stbtt_InitFont(&font, constData_Block(fontData), 0);
702 int ascent;
703 stbtt_GetFontVMetrics(&font, &ascent, NULL, NULL);
704 iDeclareType(CharBuf);
705 struct Impl_CharBuf {
706 uint8_t *pixels;
707 iInt2 size;
708 int dy;
709 int advance;
710 };
711 iArray * chars = collectNew_Array(sizeof(iCharBuf));
712 int pxRatio = (mode == quadrants_TextBlockMode ? 2 : 1);
713 int pxHeight = height * pxRatio;
714 const float scale = stbtt_ScaleForPixelHeight(&font, pxHeight);
715 const float xScale = scale * 2; /* character aspect ratio */
716 const int baseline = ascent * scale;
717 int width = 0;
718 size_t strRemain = length_String(text);
719 iConstForEach(String, i, text) {
720 if (!strRemain) break;
721 if (i.value == variationSelectorEmoji_Char) {
722 strRemain--;
723 continue;
724 }
725 iCharBuf buf;
726 buf.pixels = stbtt_GetCodepointBitmap(
727 &font, xScale, scale, i.value, &buf.size.x, &buf.size.y, 0, &buf.dy);
728 stbtt_GetCodepointHMetrics(&font, i.value, &buf.advance, NULL);
729 buf.advance *= xScale;
730 if (!isSpace_Char(i.value)) {
731 if (mode == quadrants_TextBlockMode) {
732 buf.advance = (buf.size.x - 1) / 2 * 2 + 2;
733 }
734 else {
735 buf.advance = buf.size.x + 1;
736 }
737 }
738 pushBack_Array(chars, &buf);
739 collect_Garbage(buf.pixels, freeBitmap_);
740 width += buf.advance;
741 strRemain--;
742 }
743 const size_t len = (mode == quadrants_TextBlockMode ? height * ((width + 1) / 2 + 1)
744 : (height * (width + 1)));
745 iChar *outBuf = iCollectMem(malloc(sizeof(iChar) * len));
746 for (size_t i = 0; i < len; ++i) {
747 outBuf[i] = 0x20;
748 }
749 iChar *outPos = outBuf;
750 for (int y = 0; y < pxHeight; y += pxRatio) {
751 const iCharBuf *ch = constData_Array(chars);
752 int lx = 0;
753 for (int x = 0; x < width; x += pxRatio, lx += pxRatio) {
754 if (lx >= ch->advance) {
755 ch++;
756 lx = 0;
757 }
758 const int ly = y - baseline - ch->dy;
759 if (mode == quadrants_TextBlockMode) {
760 #define checkPixel_(offx, offy) \
761 (lx + offx < ch->size.x && ly + offy < ch->size.y && ly + offy >= 0 ? \
762 ch->pixels[(lx + offx) + (ly + offy) * ch->size.x] > 155 \
763 : iFalse)
764 const int mask = (checkPixel_(0, 0) ? 1 : 0) |
765 (checkPixel_(1, 0) ? 2 : 0) |
766 (checkPixel_(0, 1) ? 4 : 0) |
767 (checkPixel_(1, 1) ? 8 : 0);
768 #undef checkPixel_
769 static const iChar blocks[16] = { 0x0020, 0x2598, 0x259D, 0x2580, 0x2596, 0x258C,
770 0x259E, 0x259B, 0x2597, 0x259A, 0x2590, 0x259C,
771 0x2584, 0x2599, 0x259F, 0x2588 };
772 *outPos++ = blocks[mask];
773 }
774 else {
775 static const iChar shades[5] = { 0x0020, 0x2591, 0x2592, 0x2593, 0x2588 };
776 *outPos++ = shades[lx < ch->size.x && ly < ch->size.y && ly >= 0 ?
777 ch->pixels[lx + ly * ch->size.x] * 5 / 256 : 0];
778 }
779 }
780 *outPos++ = '\n';
781 }
782 /* We could compose the lines separately, but we'd still need to convert them to Strings
783 individually to trim them. */
784 iStringList *lines = split_String(collect_String(newUnicodeN_String(outBuf, len)), "\n");
785 while (!isEmpty_StringList(lines) &&
786 isEmpty_String(collect_String(trimmed_String(at_StringList(lines, 0))))) {
787 popFront_StringList(lines);
788 }
789 while (!isEmpty_StringList(lines) && isEmpty_String(collect_String(trimmed_String(
790 at_StringList(lines, size_StringList(lines) - 1))))) {
791 popBack_StringList(lines);
792 }
793 iEndCollect();
794 return joinCStr_StringList(iClob(lines), "\n");
795}
796
661/*-----------------------------------------------------------------------------------------------*/ 797/*-----------------------------------------------------------------------------------------------*/
662 798
663iDefineTypeConstructionArgs(TextBuf, (int font, const char *text), font, text) 799iDefineTypeConstructionArgs(TextBuf, (int font, const char *text), font, text)
diff --git a/src/ui/text.h b/src/ui/text.h
index 2d49a1a6..2b4ec5c3 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/rect.h> 25#include <the_Foundation/rect.h>
@@ -36,6 +58,7 @@ enum iFontId {
36 hugeEmoji_FontId, 58 hugeEmoji_FontId,
37 smallEmoji_FontId, 59 smallEmoji_FontId,
38 max_FontId, 60 max_FontId,
61
39 /* Meta: */ 62 /* Meta: */
40 fromSymbolsToEmojiOffset_FontId = 7, 63 fromSymbolsToEmojiOffset_FontId = 7,
41 /* UI fonts: */ 64 /* UI fonts: */
@@ -90,6 +113,11 @@ void drawRange_Text (int fontId, iInt2 pos, int color, iRangecc text);
90 113
91SDL_Texture * glyphCache_Text (void); 114SDL_Texture * glyphCache_Text (void);
92 115
116enum iTextBlockMode { quadrants_TextBlockMode, shading_TextBlockMode };
117
118iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iTextBlockMode,
119 const iString *text);
120
93/*-----------------------------------------------------------------------------------------------*/ 121/*-----------------------------------------------------------------------------------------------*/
94 122
95iDeclareType(TextBuf) 123iDeclareType(TextBuf)
diff --git a/src/ui/util.c b/src/ui/util.c
index 9f4768d9..dfe364a5 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1,8 +1,32 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "util.h" 23#include "util.h"
2 24
3#include "app.h" 25#include "app.h"
26#include "bookmarks.h"
4#include "color.h" 27#include "color.h"
5#include "command.h" 28#include "command.h"
29#include "gmutil.h"
6#include "labelwidget.h" 30#include "labelwidget.h"
7#include "inputwidget.h" 31#include "inputwidget.h"
8#include "widget.h" 32#include "widget.h"
@@ -157,7 +181,13 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) {
157 /* Don't reopen self; instead, root will close the menu. */ 181 /* Don't reopen self; instead, root will close the menu. */
158 return iFalse; 182 return iFalse;
159 } 183 }
160 if (!equal_Command(cmd, "window.resized")) { 184 if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) {
185 /* Dismiss open menus when clicking outside them. */
186 closeMenu_Widget(menu);
187 return iTrue;
188 }
189 if (!equal_Command(cmd, "window.resized") &&
190 !(equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)) /* ignore button release */) {
161 closeMenu_Widget(menu); 191 closeMenu_Widget(menu);
162 } 192 }
163 } 193 }
@@ -234,7 +264,7 @@ void closeMenu_Widget(iWidget *d) {
234} 264}
235 265
236int checkContextMenu_Widget(iWidget *menu, const SDL_Event *ev) { 266int checkContextMenu_Widget(iWidget *menu, const SDL_Event *ev) {
237 if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) { 267 if (menu && ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) {
238 if (isVisible_Widget(menu)) { 268 if (isVisible_Widget(menu)) {
239 closeMenu_Widget(menu); 269 closeMenu_Widget(menu);
240 return 0x1; 270 return 0x1;
@@ -476,6 +506,7 @@ iBool filePathHandler_(iWidget *dlg, const char *cmd) {
476 506
477iWidget *makeSheet_Widget(const char *id) { 507iWidget *makeSheet_Widget(const char *id) {
478 iWidget *sheet = new_Widget(); 508 iWidget *sheet = new_Widget();
509 setPadding1_Widget(sheet, 3 * gap_UI);
479 setId_Widget(sheet, id); 510 setId_Widget(sheet, id);
480 setFrameColor_Widget(sheet, uiSeparator_ColorId); 511 setFrameColor_Widget(sheet, uiSeparator_ColorId);
481 setBackgroundColor_Widget(sheet, uiBackground_ColorId); 512 setBackgroundColor_Widget(sheet, uiBackground_ColorId);
@@ -483,9 +514,6 @@ iWidget *makeSheet_Widget(const char *id) {
483 mouseModal_WidgetFlag | keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag | 514 mouseModal_WidgetFlag | keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag |
484 arrangeSize_WidgetFlag, 515 arrangeSize_WidgetFlag,
485 iTrue); 516 iTrue);
486 // const iInt2 rootSize = rootSize_Window(get_Window());
487 // setSize_Widget(sheet, init_I2(rootSize.x / 2, 0));
488 // setFlags_Widget(sheet, fixedHeight_WidgetFlag, iFalse);
489 return sheet; 517 return sheet;
490} 518}
491 519
@@ -529,11 +557,13 @@ void makeFilePath_Widget(iWidget * parent,
529 557
530static void acceptValueInput_(iWidget *dlg) { 558static void acceptValueInput_(iWidget *dlg) {
531 const iInputWidget *input = findChild_Widget(dlg, "input"); 559 const iInputWidget *input = findChild_Widget(dlg, "input");
532 const iString *val = text_InputWidget(input); 560 if (!isEmpty_String(id_Widget(dlg))) {
533 postCommandf_App("%s arg:%d value:%s", 561 const iString *val = text_InputWidget(input);
534 cstr_String(id_Widget(dlg)), 562 postCommandf_App("%s arg:%d value:%s",
535 toInt_String(val), 563 cstr_String(id_Widget(dlg)),
536 cstr_String(val)); 564 toInt_String(val),
565 cstr_String(val));
566 }
537} 567}
538 568
539static void updateValueInputWidth_(iWidget *dlg) { 569static void updateValueInputWidth_(iWidget *dlg) {
@@ -560,6 +590,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
560 } 590 }
561 else { 591 else {
562 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 592 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
593 setId_Widget(dlg, ""); /* no further commands to emit */
563 } 594 }
564 destroy_Widget(dlg); 595 destroy_Widget(dlg);
565 return iTrue; 596 return iTrue;
@@ -568,6 +599,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
568 } 599 }
569 else if (equal_Command(cmd, "cancel")) { 600 else if (equal_Command(cmd, "cancel")) {
570 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 601 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
602 setId_Widget(dlg, ""); /* no further commands to emit */
571 destroy_Widget(dlg); 603 destroy_Widget(dlg);
572 return iTrue; 604 return iTrue;
573 } 605 }
@@ -633,11 +665,12 @@ static iBool messageHandler_(iWidget *msg, const char *cmd) {
633 return iFalse; 665 return iFalse;
634} 666}
635 667
636void makeMessage_Widget(const char *title, const char *msg) { 668iWidget *makeMessage_Widget(const char *title, const char *msg) {
637 iWidget *dlg = makeQuestion_Widget( 669 iWidget *dlg = makeQuestion_Widget(
638 title, msg, (const char *[]){ "Continue" }, (const char *[]){ "message.ok" }, 1); 670 title, msg, (const char *[]){ "Continue" }, (const char *[]){ "message.ok" }, 1);
639 addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok"); 671 addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok");
640 addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok"); 672 addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok");
673 return dlg;
641} 674}
642 675
643iWidget *makeQuestion_Widget(const char *title, 676iWidget *makeQuestion_Widget(const char *title,
@@ -666,9 +699,11 @@ iWidget *makeQuestion_Widget(const char *title,
666} 699}
667 700
668void setToggle_Widget(iWidget *d, iBool active) { 701void setToggle_Widget(iWidget *d, iBool active) {
669 setFlags_Widget(d, selected_WidgetFlag, active); 702 if (d) {
670 updateText_LabelWidget((iLabelWidget *) d, 703 setFlags_Widget(d, selected_WidgetFlag, active);
671 collectNewFormat_String("%s", isSelected_Widget(d) ? "YES" : "NO")); 704 updateText_LabelWidget((iLabelWidget *) d,
705 collectNewFormat_String("%s", isSelected_Widget(d) ? "YES" : "NO"));
706 }
672} 707}
673 708
674static iBool toggleHandler_(iWidget *d, const char *cmd) { 709static iBool toggleHandler_(iWidget *d, const char *cmd) {
@@ -684,8 +719,9 @@ static iBool toggleHandler_(iWidget *d, const char *cmd) {
684} 719}
685 720
686iWidget *makeToggle_Widget(const char *id) { 721iWidget *makeToggle_Widget(const char *id) {
687 iWidget *toggle = as_Widget(new_LabelWidget("YES", 0, 0, "toggle")); 722 iWidget *toggle = as_Widget(new_LabelWidget("YES", 0, 0, "toggle")); /* "YES" for sizing */
688 setId_Widget(toggle, id); 723 setId_Widget(toggle, id);
724 updateTextCStr_LabelWidget((iLabelWidget *) toggle, "NO"); /* actual initial value */
689 setCommandHandler_Widget(toggle, toggleHandler_); 725 setCommandHandler_Widget(toggle, toggleHandler_);
690 return toggle; 726 return toggle;
691} 727}
@@ -702,9 +738,11 @@ iWidget *makePreferences_Widget(void) {
702 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 738 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
703 iWidget *values = addChildFlags_Widget( 739 iWidget *values = addChildFlags_Widget(
704 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 740 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
705// setBackgroundColor_Widget(headings, none_ColorId); 741#if defined (iPlatformApple) || defined (iPlatformMSys)
706// setBackgroundColor_Widget(values, none_ColorId); 742 addChild_Widget(headings, iClob(makeHeading_Widget("Use system theme:")));
707 addChild_Widget(headings, iClob(makeHeading_Widget("Theme:"))); 743 addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme")));
744#endif
745 addChild_Widget(headings, iClob(makeHeading_Widget("Theme:")));
708 iWidget *themes = new_Widget(); 746 iWidget *themes = new_Widget();
709 /* Themes. */ { 747 /* Themes. */ {
710 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0"); 748 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0");
@@ -717,8 +755,18 @@ iWidget *makePreferences_Widget(void) {
717 addChild_Widget(values, iClob(makeToggle_Widget("prefs.retainwindow"))); 755 addChild_Widget(values, iClob(makeToggle_Widget("prefs.retainwindow")));
718 addChild_Widget(headings, iClob(makeHeading_Widget("UI scale factor:"))); 756 addChild_Widget(headings, iClob(makeHeading_Widget("UI scale factor:")));
719 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(8))), "prefs.uiscale"); 757 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(8))), "prefs.uiscale");
758 addChild_Widget(headings, iClob(makeHeading_Widget(uiHeading_ColorEscape "Proxies")));
759 addChild_Widget(values, iClob(makeHeading_Widget("")));
760 addChild_Widget(headings, iClob(makeHeading_Widget("HTTP proxy:")));
761 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.http");
762 addChild_Widget(headings, iClob(makeHeading_Widget("Gopher proxy:")));
763 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.gopher");
720 arrange_Widget(dlg); 764 arrange_Widget(dlg);
721// as_Widget(songDir)->rect.size.x = dlg->rect.size.x - headings->rect.size.x; 765 /* Text input widths. */ {
766 const int inputWidth = width_Rect(page->rect) - width_Rect(headings->rect);
767 as_Widget(findChild_Widget(values, "prefs.proxy.http"))->rect.size.x = inputWidth;
768 as_Widget(findChild_Widget(values, "prefs.proxy.gopher"))->rect.size.x = inputWidth;
769 }
722 iWidget *div = new_Widget(); { 770 iWidget *div = new_Widget(); {
723 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); 771 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
724 addChild_Widget(div, iClob(new_LabelWidget("Dismiss", SDLK_ESCAPE, 0, "prefs.dismiss"))); 772 addChild_Widget(div, iClob(new_LabelWidget("Dismiss", SDLK_ESCAPE, 0, "prefs.dismiss")));
@@ -731,9 +779,11 @@ iWidget *makePreferences_Widget(void) {
731 779
732iWidget *makeBookmarkEditor_Widget(void) { 780iWidget *makeBookmarkEditor_Widget(void) {
733 iWidget *dlg = makeSheet_Widget("bmed"); 781 iWidget *dlg = makeSheet_Widget("bmed");
734 addChildFlags_Widget(dlg, 782 setId_Widget(addChildFlags_Widget(
735 iClob(new_LabelWidget(uiHeading_ColorEscape "EDIT BOOKMARK", 0, 0, NULL)), 783 dlg,
736 frameless_WidgetFlag); 784 iClob(new_LabelWidget(uiHeading_ColorEscape "EDIT BOOKMARK", 0, 0, NULL)),
785 frameless_WidgetFlag),
786 "bmed.heading");
737 iWidget *page = new_Widget(); 787 iWidget *page = new_Widget();
738 addChild_Widget(dlg, iClob(page)); 788 addChild_Widget(dlg, iClob(page));
739 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); 789 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
@@ -741,9 +791,8 @@ iWidget *makeBookmarkEditor_Widget(void) {
741 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 791 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
742 iWidget *values = addChildFlags_Widget( 792 iWidget *values = addChildFlags_Widget(
743 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 793 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
744 iInputWidget *inputs[4]; 794 iInputWidget *inputs[3];
745 iWidget *hd; 795 addChild_Widget(headings, iClob(makeHeading_Widget("Title:")));
746 addChild_Widget(headings, iClob(hd = makeHeading_Widget("Title:")));
747 setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title"); 796 setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title");
748 addChild_Widget(headings, iClob(makeHeading_Widget("URL:"))); 797 addChild_Widget(headings, iClob(makeHeading_Widget("URL:")));
749 setId_Widget(addChild_Widget(values, iClob(inputs[1] = new_InputWidget(0))), "bmed.url"); 798 setId_Widget(addChild_Widget(values, iClob(inputs[1] = new_InputWidget(0))), "bmed.url");
@@ -759,7 +808,100 @@ iWidget *makeBookmarkEditor_Widget(void) {
759 addChild_Widget( 808 addChild_Widget(
760 div, 809 div,
761 iClob(new_LabelWidget( 810 iClob(new_LabelWidget(
762 uiTextCaution_ColorEscape "Save", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept"))); 811 uiTextCaution_ColorEscape "Save Bookmark", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept")));
812 }
813 addChild_Widget(dlg, iClob(div));
814 addChild_Widget(get_Window()->root, iClob(dlg));
815 centerSheet_Widget(dlg);
816 return dlg;
817}
818
819static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, const char *cmd) {
820 if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) {
821 if (equal_Command(cmd, "bmed.accept")) {
822 const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title"));
823 const iString *url = text_InputWidget(findChild_Widget(editor, "bmed.url"));
824 const iString *tags = text_InputWidget(findChild_Widget(editor, "bmed.tags"));
825 add_Bookmarks(bookmarks_App(),
826 url,
827 title,
828 tags,
829 first_String(label_LabelWidget(findChild_Widget(editor, "bmed.icon"))));
830 postCommand_App("bookmarks.changed");
831 }
832 destroy_Widget(editor);
833 return iTrue;
834 }
835 return iFalse;
836}
837
838iWidget *makeBookmarkCreation_Widget(const iString *url, const iString *title, iChar icon) {
839 iWidget *dlg = makeBookmarkEditor_Widget();
840 setId_Widget(dlg, "bmed.create");
841 setTextCStr_LabelWidget(findChild_Widget(dlg, "bmed.heading"),
842 uiHeading_ColorEscape "ADD BOOKMARK");
843 iUrl parts;
844 init_Url(&parts, url);
845 setTextCStr_InputWidget(findChild_Widget(dlg, "bmed.title"),
846 title ? cstr_String(title) : cstr_Rangecc(parts.host));
847 setText_InputWidget(findChild_Widget(dlg, "bmed.url"), url);
848 setId_Widget(
849 addChildFlags_Widget(
850 dlg,
851 iClob(new_LabelWidget(cstrCollect_String(newUnicodeN_String(&icon, 1)), 0, 0, NULL)),
852 collapse_WidgetFlag | hidden_WidgetFlag | disabled_WidgetFlag),
853 "bmed.icon");
854 setCommandHandler_Widget(dlg, handleBookmarkCreationCommands_SidebarWidget_);
855 return dlg;
856}
857
858iWidget *makeIdentityCreation_Widget(void) {
859 iWidget *dlg = makeSheet_Widget("ident");
860 setId_Widget(addChildFlags_Widget(
861 dlg,
862 iClob(new_LabelWidget(uiHeading_ColorEscape "NEW IDENTITY", 0, 0, NULL)),
863 frameless_WidgetFlag),
864 "ident.heading");
865 iWidget *page = new_Widget();
866 addChildFlags_Widget(
867 dlg,
868 iClob(
869 new_LabelWidget("Creating a 2048-bit self-signed RSA certificate.", 0, 0, NULL)),
870 frameless_WidgetFlag);
871 addChild_Widget(dlg, iClob(page));
872 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
873 iWidget *headings = addChildFlags_Widget(
874 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
875 iWidget *values = addChildFlags_Widget(
876 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
877 iInputWidget *inputs[6];
878 addChild_Widget(headings, iClob(makeHeading_Widget("Common name:")));
879 setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "ident.common");
880 addChild_Widget(headings, iClob(makeHeading_Widget("Email:")));
881 setId_Widget(addChild_Widget(values, iClob(inputs[1] = newHint_InputWidget(0, "optional"))), "ident.email");
882 addChild_Widget(headings, iClob(makeHeading_Widget("User ID:")));
883 setId_Widget(addChild_Widget(values, iClob(inputs[2] = newHint_InputWidget(0, "optional"))), "ident.userid");
884 addChild_Widget(headings, iClob(makeHeading_Widget("Domain:")));
885 setId_Widget(addChild_Widget(values, iClob(inputs[3] = newHint_InputWidget(0, "optional"))), "ident.domain");
886 addChild_Widget(headings, iClob(makeHeading_Widget("Organization:")));
887 setId_Widget(addChild_Widget(values, iClob(inputs[4] = newHint_InputWidget(0, "optional"))), "ident.org");
888 addChild_Widget(headings, iClob(makeHeading_Widget("Country:")));
889 setId_Widget(addChild_Widget(values, iClob(inputs[5] = newHint_InputWidget(0, "optional"))), "ident.country");
890 addChild_Widget(headings, iClob(makeHeading_Widget("Valid until:")));
891 setId_Widget(addChild_Widget(values, iClob(newHint_InputWidget(19, "YYYY-MM-DD HH:MM:SS"))), "ident.until");
892 addChild_Widget(headings, iClob(makeHeading_Widget("Temporary:")));
893 addChild_Widget(values, iClob(makeToggle_Widget("ident.temp")));
894 arrange_Widget(dlg);
895 for (size_t i = 0; i < iElemCount(inputs); ++i) {
896 as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x;
897 }
898 iWidget *div = new_Widget(); {
899 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
900 addChild_Widget(div, iClob(new_LabelWidget("Cancel", SDLK_ESCAPE, 0, "cancel")));
901 addChild_Widget(
902 div,
903 iClob(new_LabelWidget(
904 uiTextAction_ColorEscape "Create Identity", SDLK_RETURN, KMOD_PRIMARY, "ident.accept")));
763 } 905 }
764 addChild_Widget(dlg, iClob(div)); 906 addChild_Widget(dlg, iClob(div));
765 addChild_Widget(get_Window()->root, iClob(dlg)); 907 addChild_Widget(get_Window()->root, iClob(dlg));
diff --git a/src/ui/util.h b/src/ui/util.h
index 6595b94c..9ef166de 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -1,5 +1,28 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
25#include <the_Foundation/string.h>
3#include <the_Foundation/rect.h> 26#include <the_Foundation/rect.h>
4#include <the_Foundation/vec2.h> 27#include <the_Foundation/vec2.h>
5#include <SDL_events.h> 28#include <SDL_events.h>
@@ -117,8 +140,11 @@ void makeFilePath_Widget (iWidget *parent, const iString *initialPath
117iWidget * makeValueInput_Widget (iWidget *parent, const iString *initialValue, const char *title, 140iWidget * makeValueInput_Widget (iWidget *parent, const iString *initialValue, const char *title,
118 const char *prompt, const char *acceptLabel, const char *command); 141 const char *prompt, const char *acceptLabel, const char *command);
119void updateValueInput_Widget (iWidget *, const char *title, const char *prompt); 142void updateValueInput_Widget (iWidget *, const char *title, const char *prompt);
120void makeMessage_Widget (const char *title, const char *msg); 143iWidget * makeMessage_Widget (const char *title, const char *msg);
121iWidget * makeQuestion_Widget (const char *title, const char *msg, 144iWidget * makeQuestion_Widget (const char *title, const char *msg,
122 const char *labels[], const char *commands[], size_t count); 145 const char *labels[], const char *commands[], size_t count);
123iWidget * makePreferences_Widget (void); 146
124iWidget * makeBookmarkEditor_Widget(void); 147iWidget * makePreferences_Widget (void);
148iWidget * makeBookmarkEditor_Widget (void);
149iWidget * makeBookmarkCreation_Widget (const iString *url, const iString *title, iChar icon);
150iWidget * makeIdentityCreation_Widget (void);
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 1c19b70f..b5ea3b0f 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "widget.h" 23#include "widget.h"
2 24
3#include "app.h" 25#include "app.h"
@@ -6,6 +28,7 @@
6#include "util.h" 28#include "util.h"
7#include "window.h" 29#include "window.h"
8 30
31#include <the_Foundation/ptrarray.h>
9#include <the_Foundation/ptrset.h> 32#include <the_Foundation/ptrset.h>
10#include <SDL_mouse.h> 33#include <SDL_mouse.h>
11#include <stdarg.h> 34#include <stdarg.h>
@@ -16,15 +39,15 @@ struct Impl_RootData {
16 iWidget *hover; 39 iWidget *hover;
17 iWidget *mouseGrab; 40 iWidget *mouseGrab;
18 iWidget *focus; 41 iWidget *focus;
19 iPtrSet *onTop; 42 iPtrArray *onTop; /* order is important; last one is topmost */
20 iPtrSet *pendingDestruction; 43 iPtrSet *pendingDestruction;
21}; 44};
22 45
23static iRootData rootData_; 46static iRootData rootData_;
24 47
25iPtrSet *onTop_RootData_(void) { 48iPtrArray *onTop_RootData_(void) {
26 if (!rootData_.onTop) { 49 if (!rootData_.onTop) {
27 rootData_.onTop = new_PtrSet(); 50 rootData_.onTop = new_PtrArray();
28 } 51 }
29 return rootData_.onTop; 52 return rootData_.onTop;
30} 53}
@@ -32,7 +55,7 @@ iPtrSet *onTop_RootData_(void) {
32void destroyPending_Widget(void) { 55void destroyPending_Widget(void) {
33 iForEach(PtrSet, i, rootData_.pendingDestruction) { 56 iForEach(PtrSet, i, rootData_.pendingDestruction) {
34 iWidget *widget = *i.value; 57 iWidget *widget = *i.value;
35 remove_PtrSet(onTop_RootData_(), widget); 58 removeOne_PtrArray(onTop_RootData_(), widget);
36 if (widget->parent) { 59 if (widget->parent) {
37 iRelease(removeChild_Widget(widget->parent, widget)); 60 iRelease(removeChild_Widget(widget->parent, widget));
38 } 61 }
@@ -54,6 +77,7 @@ void init_Widget(iWidget *d) {
54 d->children = NULL; 77 d->children = NULL;
55 d->parent = NULL; 78 d->parent = NULL;
56 d->commandHandler = NULL; 79 d->commandHandler = NULL;
80 iZap(d->padding);
57} 81}
58 82
59void deinit_Widget(iWidget *d) { 83void deinit_Widget(iWidget *d) {
@@ -103,10 +127,10 @@ void setFlags_Widget(iWidget *d, int flags, iBool set) {
103 iChangeFlags(d->flags, flags, set); 127 iChangeFlags(d->flags, flags, set);
104 if (flags & keepOnTop_WidgetFlag) { 128 if (flags & keepOnTop_WidgetFlag) {
105 if (set) { 129 if (set) {
106 insert_PtrSet(onTop_RootData_(), d); 130 pushBack_PtrArray(onTop_RootData_(), d);
107 } 131 }
108 else { 132 else {
109 remove_PtrSet(onTop_RootData_(), d); 133 removeOne_PtrArray(onTop_RootData_(), d);
110 } 134 }
111 } 135 }
112} 136}
@@ -120,6 +144,13 @@ void setSize_Widget(iWidget *d, iInt2 size) {
120 setFlags_Widget(d, fixedSize_WidgetFlag, iTrue); 144 setFlags_Widget(d, fixedSize_WidgetFlag, iTrue);
121} 145}
122 146
147void setPadding_Widget(iWidget *d, int left, int top, int right, int bottom) {
148 d->padding[0] = left;
149 d->padding[1] = top;
150 d->padding[2] = right;
151 d->padding[3] = bottom;
152}
153
123void setBackgroundColor_Widget(iWidget *d, int bgColor) { 154void setBackgroundColor_Widget(iWidget *d, int bgColor) {
124 d->bgColor = bgColor; 155 d->bgColor = bgColor;
125} 156}
@@ -169,19 +200,26 @@ iLocalDef iBool isCollapsed_Widget_(const iWidget *d) {
169 (hidden_WidgetFlag | collapse_WidgetFlag); 200 (hidden_WidgetFlag | collapse_WidgetFlag);
170} 201}
171 202
203iLocalDef iRect innerRect_Widget_(const iWidget *d) {
204 return init_Rect(d->padding[0],
205 d->padding[1],
206 width_Rect(d->rect) - d->padding[0] - d->padding[2],
207 height_Rect(d->rect) - d->padding[1] - d->padding[3]);
208}
209
172void arrange_Widget(iWidget *d) { 210void arrange_Widget(iWidget *d) {
173 if (isCollapsed_Widget_(d)) { 211 if (isCollapsed_Widget_(d)) {
174 setFlags_Widget(d, wasCollapsed_WidgetFlag, iTrue); 212 setFlags_Widget(d, wasCollapsed_WidgetFlag, iTrue);
175 return; 213 return;
176 } 214 }
177 if (d->flags & moveToParentRightEdge_WidgetFlag) { 215 if (d->flags & moveToParentRightEdge_WidgetFlag) {
178 d->rect.pos.x = width_Rect(d->parent->rect) - width_Rect(d->rect); 216 d->rect.pos.x = width_Rect(innerRect_Widget_(d->parent)) - width_Rect(d->rect);
179 } 217 }
180 if (d->flags & resizeToParentWidth_WidgetFlag) { 218 if (d->flags & resizeToParentWidth_WidgetFlag) {
181 setWidth_Widget_(d, d->parent->rect.size.x); 219 setWidth_Widget_(d, width_Rect(innerRect_Widget_(d->parent)));
182 } 220 }
183 if (d->flags & resizeToParentHeight_WidgetFlag) { 221 if (d->flags & resizeToParentHeight_WidgetFlag) {
184 setHeight_Widget_(d, d->parent->rect.size.y); 222 setHeight_Widget_(d, height_Rect(innerRect_Widget_(d->parent)));
185 } 223 }
186 /* The rest of the arrangement depends on child widgets. */ 224 /* The rest of the arrangement depends on child widgets. */
187 if (!d->children) { 225 if (!d->children) {
@@ -214,7 +252,7 @@ void arrange_Widget(iWidget *d) {
214 const int expCount = numExpandingChildren_Widget_(d); 252 const int expCount = numExpandingChildren_Widget_(d);
215 /* Only resize the expanding children, not touching the others. */ 253 /* Only resize the expanding children, not touching the others. */
216 if (expCount > 0) { 254 if (expCount > 0) {
217 iInt2 avail = d->rect.size; 255 iInt2 avail = innerRect_Widget_(d).size;
218 iConstForEach(ObjectList, i, d->children) { 256 iConstForEach(ObjectList, i, d->children) {
219 const iWidget *child = constAs_Widget(i.object); 257 const iWidget *child = constAs_Widget(i.object);
220 if (~child->flags & expand_WidgetFlag) { 258 if (~child->flags & expand_WidgetFlag) {
@@ -228,27 +266,27 @@ void arrange_Widget(iWidget *d) {
228 if (child->flags & expand_WidgetFlag) { 266 if (child->flags & expand_WidgetFlag) {
229 if (d->flags & arrangeHorizontal_WidgetFlag) { 267 if (d->flags & arrangeHorizontal_WidgetFlag) {
230 if (dirs.x) setWidth_Widget_(child, avail.x); 268 if (dirs.x) setWidth_Widget_(child, avail.x);
231 if (dirs.y) setHeight_Widget_(child, d->rect.size.y); 269 if (dirs.y) setHeight_Widget_(child, height_Rect(innerRect_Widget_(d)));
232 } 270 }
233 else if (d->flags & arrangeVertical_WidgetFlag) { 271 else if (d->flags & arrangeVertical_WidgetFlag) {
234 if (dirs.x) setWidth_Widget_(child, d->rect.size.x); 272 if (dirs.x) setWidth_Widget_(child, width_Rect(innerRect_Widget_(d)));
235 if (dirs.y) setHeight_Widget_(child, avail.y); 273 if (dirs.y) setHeight_Widget_(child, avail.y);
236 } 274 }
237 } 275 }
238 else { 276 else {
239 /* Fill the off axis, though. */ 277 /* Fill the off axis, though. */
240 if (d->flags & arrangeHorizontal_WidgetFlag) { 278 if (d->flags & arrangeHorizontal_WidgetFlag) {
241 if (dirs.y) setHeight_Widget_(child, d->rect.size.y); 279 if (dirs.y) setHeight_Widget_(child, height_Rect(innerRect_Widget_(d)));
242 } 280 }
243 else if (d->flags & arrangeVertical_WidgetFlag) { 281 else if (d->flags & arrangeVertical_WidgetFlag) {
244 if (dirs.x) setWidth_Widget_(child, d->rect.size.x); 282 if (dirs.x) setWidth_Widget_(child, width_Rect(innerRect_Widget_(d)));
245 } 283 }
246 } 284 }
247 } 285 }
248 } 286 }
249 else { 287 else {
250 /* Evenly size all children. */ 288 /* Evenly size all children. */
251 iInt2 childSize = d->rect.size; 289 iInt2 childSize = innerRect_Widget_(d).size;
252 if (d->flags & arrangeHorizontal_WidgetFlag) { 290 if (d->flags & arrangeHorizontal_WidgetFlag) {
253 childSize.x /= childCount; 291 childSize.x /= childCount;
254 } 292 }
@@ -270,7 +308,7 @@ void arrange_Widget(iWidget *d) {
270 setWidth_Widget_(as_Widget(i.object), widest); 308 setWidth_Widget_(as_Widget(i.object), widest);
271 } 309 }
272 } 310 }
273 iInt2 pos = zero_I2(); 311 iInt2 pos = initv_I2(d->padding);
274 iForEach(ObjectList, i, d->children) { 312 iForEach(ObjectList, i, d->children) {
275 iWidget *child = as_Widget(i.object); 313 iWidget *child = as_Widget(i.object);
276 arrange_Widget(child); 314 arrange_Widget(child);
@@ -286,6 +324,9 @@ void arrange_Widget(iWidget *d) {
286 pos.y += child->rect.size.y; 324 pos.y += child->rect.size.y;
287 } 325 }
288 } 326 }
327 else if (d->flags & resizeChildren_WidgetFlag) {
328 child->rect.pos = pos;
329 }
289 } 330 }
290 /* Update the size of the widget according to the arrangement. */ 331 /* Update the size of the widget according to the arrangement. */
291 if (d->flags & arrangeSize_WidgetFlag) { 332 if (d->flags & arrangeSize_WidgetFlag) {
@@ -299,6 +340,7 @@ void arrange_Widget(iWidget *d) {
299 bounds = union_Rect(bounds, child->rect); 340 bounds = union_Rect(bounds, child->rect);
300 } 341 }
301 } 342 }
343 adjustEdges_Rect(&bounds, -d->padding[1], d->padding[2], d->padding[3], -d->padding[0]);
302 if (d->flags & arrangeWidth_WidgetFlag) { 344 if (d->flags & arrangeWidth_WidgetFlag) {
303 setWidth_Widget_(d, bounds.size.x); 345 setWidth_Widget_(d, bounds.size.x);
304 /* Parent size changed, must update the children.*/ 346 /* Parent size changed, must update the children.*/
@@ -381,7 +423,7 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
381 } 423 }
382 } 424 }
383 /* Root offers events first to widgets on top. */ 425 /* Root offers events first to widgets on top. */
384 iForEach(PtrSet, i, rootData_.onTop) { 426 iReverseForEach(PtrArray, i, rootData_.onTop) {
385 iWidget *widget = *i.value; 427 iWidget *widget = *i.value;
386 if (isVisible_Widget(widget) && dispatchEvent_Widget(widget, ev)) { 428 if (isVisible_Widget(widget) && dispatchEvent_Widget(widget, ev)) {
387 return iTrue; 429 return iTrue;
@@ -454,6 +496,7 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
454 } 496 }
455 } 497 }
456 if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) { 498 if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) {
499 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
457 return iTrue; 500 return iTrue;
458 } 501 }
459 return iFalse; 502 return iFalse;
@@ -480,7 +523,7 @@ void draw_Widget(const iWidget *d) {
480 } 523 }
481 /* Root draws the on-top widgets on top of everything else. */ 524 /* Root draws the on-top widgets on top of everything else. */
482 if (!d->parent) { 525 if (!d->parent) {
483 iConstForEach(PtrSet, i, onTop_RootData_()) { 526 iConstForEach(PtrArray, i, onTop_RootData_()) {
484 draw_Widget(*i.value); 527 draw_Widget(*i.value);
485 } 528 }
486 } 529 }
@@ -594,7 +637,7 @@ iBool isHover_Widget(const iWidget *d) {
594} 637}
595 638
596iBool isSelected_Widget(const iWidget *d) { 639iBool isSelected_Widget(const iWidget *d) {
597 return (d->flags & selected_WidgetFlag) != 0; 640 return d && (d->flags & selected_WidgetFlag) != 0;
598} 641}
599 642
600iBool equalWidget_Command(const char *cmd, const iWidget *widget, const char *checkCommand) { 643iBool equalWidget_Command(const char *cmd, const iWidget *widget, const char *checkCommand) {
diff --git a/src/ui/widget.h b/src/ui/widget.h
index e88a10dc..3f03fc07 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3/* Base class for UI widgets. */ 25/* Base class for UI widgets. */
@@ -73,6 +95,7 @@ struct Impl_Widget {
73 iString id; 95 iString id;
74 int flags; 96 int flags;
75 iRect rect; 97 iRect rect;
98 int padding[4]; /* left, top, right, bottom */
76 int bgColor; 99 int bgColor;
77 int frameColor; 100 int frameColor;
78 iObjectList *children; 101 iObjectList *children;
@@ -107,7 +130,8 @@ void destroyPending_Widget(void);
107 130
108const iString *id_Widget (const iWidget *); 131const iString *id_Widget (const iWidget *);
109int flags_Widget (const iWidget *); 132int flags_Widget (const iWidget *);
110iRect bounds_Widget (const iWidget *); 133iRect bounds_Widget (const iWidget *); /* outer bounds */
134iRect innerBounds_Widget (const iWidget *);
111iInt2 localCoord_Widget (const iWidget *, iInt2 coord); 135iInt2 localCoord_Widget (const iWidget *, iInt2 coord);
112iBool contains_Widget (const iWidget *, iInt2 coord); 136iBool contains_Widget (const iWidget *, iInt2 coord);
113iAny * findChild_Widget (const iWidget *, const char *id); 137iAny * findChild_Widget (const iWidget *, const char *id);
@@ -131,6 +155,8 @@ void setId_Widget (iWidget *, const char *id);
131void setFlags_Widget (iWidget *, int flags, iBool set); 155void setFlags_Widget (iWidget *, int flags, iBool set);
132void setPos_Widget (iWidget *, iInt2 pos); 156void setPos_Widget (iWidget *, iInt2 pos);
133void setSize_Widget (iWidget *, iInt2 size); 157void setSize_Widget (iWidget *, iInt2 size);
158void setPadding_Widget (iWidget *, int left, int top, int right, int bottom);
159iLocalDef void setPadding1_Widget (iWidget *d, int padding) { setPadding_Widget(d, padding, padding, padding, padding); }
134void setBackgroundColor_Widget (iWidget *, int bgColor); 160void setBackgroundColor_Widget (iWidget *, int bgColor);
135void setFrameColor_Widget (iWidget *, int frameColor); 161void setFrameColor_Widget (iWidget *, int frameColor);
136void setCommandHandler_Widget (iWidget *, iBool (*handler)(iWidget *, const char *)); 162void setCommandHandler_Widget (iWidget *, iBool (*handler)(iWidget *, const char *));
diff --git a/src/ui/window.c b/src/ui/window.c
index 3d9d98d1..650bc9ee 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -1,17 +1,39 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "window.h" 23#include "window.h"
2 24
25#include "labelwidget.h"
26#include "inputwidget.h"
27#include "documentwidget.h"
28#include "sidebarwidget.h"
3#include "embedded.h" 29#include "embedded.h"
4#include "app.h"
5#include "command.h" 30#include "command.h"
6#include "paint.h" 31#include "paint.h"
7#include "text.h"
8#include "util.h" 32#include "util.h"
33#include "../app.h"
9#include "../visited.h" 34#include "../visited.h"
10#include "labelwidget.h" 35#include "../gmcerts.h"
11#include "inputwidget.h" 36#include "../gmutil.h"
12#include "documentwidget.h"
13#include "sidebarwidget.h"
14#include "gmutil.h"
15#if defined (iPlatformMsys) 37#if defined (iPlatformMsys)
16# include "../win32.h" 38# include "../win32.h"
17#endif 39#endif
@@ -80,6 +102,8 @@ static const iMenuItem navMenuItems[] = {
80 { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, 102 { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" },
81 { "---", 0, 0, NULL }, 103 { "---", 0, 0, NULL },
82 { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, 104 { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
105 { "Help", 0, 0, "!open url:about:help" },
106 { "Release Notes", 0, 0, "!open url:about:version" },
83 { "---", 0, 0, NULL }, 107 { "---", 0, 0, NULL },
84 { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" } 108 { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" }
85}; 109};
@@ -94,8 +118,13 @@ static const iMenuItem fileMenuItems[] = {
94 118
95static const iMenuItem editMenuItems[] = { 119static const iMenuItem editMenuItems[] = {
96 { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" }, 120 { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" },
121 { "Copy Link to Page", SDLK_c, KMOD_PRIMARY | KMOD_SHIFT, "document.copylink" },
97 { "---", 0, 0, NULL }, 122 { "---", 0, 0, NULL },
98 { "Bookmark This Page", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, 123 { "Bookmark This Page...", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
124};
125
126static const iMenuItem identityMenuItems[] = {
127 { "New Identity...", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" },
99}; 128};
100 129
101static const iMenuItem viewMenuItems[] = { 130static const iMenuItem viewMenuItems[] = {
@@ -115,14 +144,43 @@ static const iMenuItem viewMenuItems[] = {
115}; 144};
116 145
117static const iMenuItem helpMenuItems[] = { 146static const iMenuItem helpMenuItems[] = {
118 { "Help", 0, 0, "open url:about:help" }, 147 { "Help", 0, 0, "!open url:about:help" },
119 { "Release Notes", 0, 0, "open url:about:version" }, 148 { "Release Notes", 0, 0, "!open url:about:version" },
120}; 149};
121#endif 150#endif
122 151
152static const iMenuItem identityButtonMenuItems[] = {
153 { "No Active Identity", 0, 0, "ident.showactive" },
154 { "---", 0, 0, NULL },
155#if !defined (iHaveNativeMenus)
156 { "New Identity...", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" },
157 { "---", 0, 0, NULL },
158 { "Show Identities", '3', KMOD_PRIMARY, "sidebar.mode arg:2 show:1" },
159#else
160 { "New Identity...", 0, 0, "ident.new" },
161 { "---", 0, 0, NULL },
162 { "Show Identities", 0, 0, "sidebar.mode arg:2 show:1" },
163#endif
164};
165
123static const char *reloadCStr_ = "\U0001f503"; 166static const char *reloadCStr_ = "\U0001f503";
124static const char *stopCStr_ = uiTextCaution_ColorEscape "\U0001f310"; 167static const char *stopCStr_ = uiTextCaution_ColorEscape "\U0001f310";
125 168
169static void updateNavBarIdentity_(iWidget *navBar) {
170 const iGmIdentity *ident =
171 identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App()));
172 iWidget *button = findChild_Widget(navBar, "navbar.ident");
173 setFlags_Widget(button, selected_WidgetFlag, ident != NULL);
174 /* Update menu. */
175 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);
176 setTextCStr_LabelWidget(
177 idItem,
178 ident ? format_CStr(uiTextAction_ColorEscape "%s",
179 cstrCollect_String(subject_TlsCertificate(ident->cert)))
180 : "No Active Identity");
181 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident);
182}
183
126static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { 184static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
127 if (equal_Command(cmd, "window.resized")) { 185 if (equal_Command(cmd, "window.resized")) {
128 const iBool isNarrow = width_Rect(bounds_Widget(navBar)) / gap_UI < 140; 186 const iBool isNarrow = width_Rect(bounds_Widget(navBar)) / gap_UI < 140;
@@ -160,6 +218,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
160 const iString *urlStr = collect_String(suffix_Command(cmd, "url")); 218 const iString *urlStr = collect_String(suffix_Command(cmd, "url"));
161 setText_InputWidget(url, urlStr); 219 setText_InputWidget(url, urlStr);
162 updateTextCStr_LabelWidget(reloadButton, reloadCStr_); 220 updateTextCStr_LabelWidget(reloadButton, reloadCStr_);
221 updateNavBarIdentity_(navBar);
163 return iFalse; 222 return iFalse;
164 } 223 }
165 else if (equal_Command(cmd, "document.request.cancelled")) { 224 else if (equal_Command(cmd, "document.request.cancelled")) {
@@ -184,12 +243,13 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
184 setText_InputWidget(findChild_Widget(navBar, "url"), url_DocumentWidget(doc)); 243 setText_InputWidget(findChild_Widget(navBar, "url"), url_DocumentWidget(doc));
185 updateTextCStr_LabelWidget(findChild_Widget(navBar, "reload"), 244 updateTextCStr_LabelWidget(findChild_Widget(navBar, "reload"),
186 isRequestOngoing_DocumentWidget(doc) ? stopCStr_ : reloadCStr_); 245 isRequestOngoing_DocumentWidget(doc) ? stopCStr_ : reloadCStr_);
246 updateNavBarIdentity_(navBar);
187 } 247 }
188 } 248 }
189 else if (equal_Command(cmd, "mouse.clicked")) { 249 else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) {
190 iWidget *widget = pointer_Command(cmd); 250 iWidget *widget = pointer_Command(cmd);
191 iWidget *menu = findWidget_App("doctabs.menu"); 251 iWidget *menu = findWidget_App("doctabs.menu");
192 if (isTabButton_Widget(widget)) { 252 if (isTabButton_Widget(widget) && !isVisible_Widget(menu)) {
193 iWidget *tabs = findWidget_App("doctabs"); 253 iWidget *tabs = findWidget_App("doctabs");
194 showTabPage_Widget(tabs, 254 showTabPage_Widget(tabs,
195 tabPage_Widget(tabs, childIndex_Widget(widget->parent, widget))); 255 tabPage_Widget(tabs, childIndex_Widget(widget->parent, widget)));
@@ -261,6 +321,7 @@ static void setupUserInterface_Window(iWindow *d) {
261 /* Navigation bar. */ { 321 /* Navigation bar. */ {
262 iWidget *navBar = new_Widget(); 322 iWidget *navBar = new_Widget();
263 setId_Widget(navBar, "navbar"); 323 setId_Widget(navBar, "navbar");
324 setPadding_Widget(navBar, gap_UI / 2, 0, gap_UI / 2, 0);
264 setFlags_Widget(navBar, 325 setFlags_Widget(navBar,
265 arrangeHeight_WidgetFlag | resizeChildren_WidgetFlag | 326 arrangeHeight_WidgetFlag | resizeChildren_WidgetFlag |
266 arrangeHorizontal_WidgetFlag, 327 arrangeHorizontal_WidgetFlag,
@@ -270,7 +331,11 @@ static void setupUserInterface_Window(iWindow *d) {
270 setCommandHandler_Widget(navBar, handleNavBarCommands_); 331 setCommandHandler_Widget(navBar, handleNavBarCommands_);
271 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f850", 0, 0, "navigate.back"))); 332 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f850", 0, 0, "navigate.back")));
272 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f852", 0, 0, "navigate.forward"))); 333 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f852", 0, 0, "navigate.forward")));
273 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f3e0", 0, 0, "navigate.home"))); 334 iLabelWidget *idMenu =
335 makeMenuButton_LabelWidget("\U0001f464", identityButtonMenuItems, iElemCount(identityButtonMenuItems));
336 setAlignVisually_LabelWidget(idMenu, iTrue);
337 addChild_Widget(navBar, iClob(idMenu));
338 setId_Widget(as_Widget(idMenu), "navbar.ident");
274 iLabelWidget *lock = 339 iLabelWidget *lock =
275 addChildFlags_Widget(navBar, 340 addChildFlags_Widget(navBar,
276 iClob(newIcon_LabelWidget("\U0001f513", 0, 0, "server.showcert")), 341 iClob(newIcon_LabelWidget("\U0001f513", 0, 0, "server.showcert")),
@@ -279,14 +344,16 @@ static void setupUserInterface_Window(iWindow *d) {
279 setFont_LabelWidget(lock, defaultSymbols_FontId); 344 setFont_LabelWidget(lock, defaultSymbols_FontId);
280 updateTextCStr_LabelWidget(lock, "\U0001f512"); 345 updateTextCStr_LabelWidget(lock, "\U0001f512");
281 iInputWidget *url = new_InputWidget(0); 346 iInputWidget *url = new_InputWidget(0);
347 setSelectAllOnFocus_InputWidget(url, iTrue);
282 setId_Widget(as_Widget(url), "url"); 348 setId_Widget(as_Widget(url), "url");
283 setTextCStr_InputWidget(url, "gemini://"); 349 setTextCStr_InputWidget(url, "gemini://");
284 addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag); 350 addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag);
285 setId_Widget( 351 setId_Widget(addChild_Widget(
286 addChild_Widget(navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))), 352 navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))),
287 "reload"); 353 "reload");
288 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f464", 0, 0, "cert.client"))); 354 addChild_Widget(navBar,
289 355 iClob(newIcon_LabelWidget(
356 "\U0001f3e0", SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home")));
290#if !defined (iHaveNativeMenus) 357#if !defined (iHaveNativeMenus)
291 iLabelWidget *navMenu = 358 iLabelWidget *navMenu =
292 makeMenuButton_LabelWidget("\U0001d362", navMenuItems, iElemCount(navMenuItems)); 359 makeMenuButton_LabelWidget("\U0001d362", navMenuItems, iElemCount(navMenuItems));
@@ -296,7 +363,8 @@ static void setupUserInterface_Window(iWindow *d) {
296 insertMenuItems_MacOS("File", 1, fileMenuItems, iElemCount(fileMenuItems)); 363 insertMenuItems_MacOS("File", 1, fileMenuItems, iElemCount(fileMenuItems));
297 insertMenuItems_MacOS("Edit", 2, editMenuItems, iElemCount(editMenuItems)); 364 insertMenuItems_MacOS("Edit", 2, editMenuItems, iElemCount(editMenuItems));
298 insertMenuItems_MacOS("View", 3, viewMenuItems, iElemCount(viewMenuItems)); 365 insertMenuItems_MacOS("View", 3, viewMenuItems, iElemCount(viewMenuItems));
299 insertMenuItems_MacOS("Help", 5, helpMenuItems, iElemCount(helpMenuItems)); 366 insertMenuItems_MacOS("Identity", 4, identityMenuItems, iElemCount(identityMenuItems));
367 insertMenuItems_MacOS("Help", 6, helpMenuItems, iElemCount(helpMenuItems));
300#endif 368#endif
301 } 369 }
302 /* Tab bar. */ { 370 /* Tab bar. */ {
@@ -380,6 +448,8 @@ static void drawBlank_Window_(iWindow *d) {
380 448
381void init_Window(iWindow *d, iRect rect) { 449void init_Window(iWindow *d, iRect rect) {
382 theWindow_ = d; 450 theWindow_ = d;
451 iZap(d->cursors);
452 d->pendingCursor = NULL;
383 d->isDrawFrozen = iTrue; 453 d->isDrawFrozen = iTrue;
384 uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; 454 uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
385#if defined (iPlatformApple) 455#if defined (iPlatformApple)
@@ -396,7 +466,7 @@ void init_Window(iWindow *d, iRect rect) {
396 if (left_Rect(rect) >= 0) { 466 if (left_Rect(rect) >= 0) {
397 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); 467 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect));
398 } 468 }
399 SDL_SetWindowMinimumSize(d->win, 400, 200); 469 SDL_SetWindowMinimumSize(d->win, 400, 250);
400 SDL_SetWindowTitle(d->win, "Lagrange"); 470 SDL_SetWindowTitle(d->win, "Lagrange");
401 /* Some info. */ { 471 /* Some info. */ {
402 SDL_RendererInfo info; 472 SDL_RendererInfo info;
@@ -442,6 +512,11 @@ void deinit_Window(iWindow *d) {
442 if (theWindow_ == d) { 512 if (theWindow_ == d) {
443 theWindow_ = NULL; 513 theWindow_ = NULL;
444 } 514 }
515 iForIndices(i, d->cursors) {
516 if (d->cursors[i]) {
517 SDL_FreeCursor(d->cursors[i]);
518 }
519 }
445 iReleasePtr(&d->root); 520 iReleasePtr(&d->root);
446 deinit_Text(); 521 deinit_Text();
447 SDL_DestroyRenderer(d->render); 522 SDL_DestroyRenderer(d->render);
@@ -470,6 +545,13 @@ static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
470 return iFalse; 545 return iFalse;
471} 546}
472 547
548static void applyCursor_Window_(iWindow *d) {
549 if (d->pendingCursor) {
550 SDL_SetCursor(d->pendingCursor);
551 d->pendingCursor = NULL;
552 }
553}
554
473iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { 555iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
474 switch (ev->type) { 556 switch (ev->type) {
475 case SDL_WINDOWEVENT: { 557 case SDL_WINDOWEVENT: {
@@ -484,6 +566,7 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
484 } 566 }
485 /* Map mouse pointer coordinate to our coordinate system. */ 567 /* Map mouse pointer coordinate to our coordinate system. */
486 if (event.type == SDL_MOUSEMOTION) { 568 if (event.type == SDL_MOUSEMOTION) {
569 setCursor_Window(d, SDL_SYSTEM_CURSOR_ARROW); /* default cursor */
487 const iInt2 pos = coord_Window(d, event.motion.x, event.motion.y); 570 const iInt2 pos = coord_Window(d, event.motion.x, event.motion.y);
488 event.motion.x = pos.x; 571 event.motion.x = pos.x;
489 event.motion.y = pos.y; 572 event.motion.y = pos.y;
@@ -506,6 +589,9 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
506 if (oldHover != hover_Widget()) { 589 if (oldHover != hover_Widget()) {
507 postRefresh_App(); 590 postRefresh_App();
508 } 591 }
592 if (event.type == SDL_MOUSEMOTION) {
593 applyCursor_Window_(d);
594 }
509 return wasUsed; 595 return wasUsed;
510 } 596 }
511 } 597 }
@@ -564,6 +650,13 @@ void setFreezeDraw_Window(iWindow *d, iBool freezeDraw) {
564 d->isDrawFrozen = freezeDraw; 650 d->isDrawFrozen = freezeDraw;
565} 651}
566 652
653void setCursor_Window(iWindow *d, int cursor) {
654 if (!d->cursors[cursor]) {
655 d->cursors[cursor] = SDL_CreateSystemCursor(cursor);
656 }
657 d->pendingCursor = d->cursors[cursor];
658}
659
567iInt2 rootSize_Window(const iWindow *d) { 660iInt2 rootSize_Window(const iWindow *d) {
568 return d->root->rect.size; 661 return d->root->rect.size;
569} 662}
diff --git a/src/ui/window.h b/src/ui/window.h
index d6eed841..4aec2fa7 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include "widget.h" 25#include "widget.h"
@@ -19,6 +41,8 @@ struct Impl_Window {
19 float uiScale; 41 float uiScale;
20 uint32_t frameTime; 42 uint32_t frameTime;
21 double presentTime; 43 double presentTime;
44 SDL_Cursor * cursors[SDL_NUM_SYSTEM_CURSORS];
45 SDL_Cursor * pendingCursor;
22}; 46};
23 47
24iBool processEvent_Window (iWindow *, const SDL_Event *); 48iBool processEvent_Window (iWindow *, const SDL_Event *);
@@ -27,6 +51,7 @@ void resize_Window (iWindow *, int w, int h);
27void setTitle_Window (iWindow *, const iString *title); 51void setTitle_Window (iWindow *, const iString *title);
28void setUiScale_Window (iWindow *, float uiScale); 52void setUiScale_Window (iWindow *, float uiScale);
29void setFreezeDraw_Window (iWindow *, iBool freezeDraw); 53void setFreezeDraw_Window (iWindow *, iBool freezeDraw);
54void setCursor_Window (iWindow *, int cursor);
30 55
31iInt2 rootSize_Window (const iWindow *); 56iInt2 rootSize_Window (const iWindow *);
32float uiScale_Window (const iWindow *); 57float uiScale_Window (const iWindow *);
diff --git a/src/visited.c b/src/visited.c
index af976be5..912a6318 100644
--- a/src/visited.c
+++ b/src/visited.c
@@ -1,8 +1,31 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "visited.h" 23#include "visited.h"
2#include "app.h" 24#include "app.h"
3 25
4#include <the_Foundation/file.h> 26#include <the_Foundation/file.h>
5#include <the_Foundation/path.h> 27#include <the_Foundation/path.h>
28#include <the_Foundation/ptrarray.h>
6#include <the_Foundation/sortedarray.h> 29#include <the_Foundation/sortedarray.h>
7 30
8static const size_t maxAgeVisited_Visited_ = 3600 * 24 * 30; /* one month */ 31static const size_t maxAgeVisited_Visited_ = 3600 * 24 * 30; /* one month */
@@ -73,7 +96,7 @@ void load_Visited(iVisited *d, const char *dirPath) {
73 iRangecc line = iNullRange; 96 iRangecc line = iNullRange;
74 iTime now; 97 iTime now;
75 initCurrent_Time(&now); 98 initCurrent_Time(&now);
76 while (nextSplit_Rangecc(&src, "\n", &line)) { 99 while (nextSplit_Rangecc(src, "\n", &line)) {
77 int y, m, D, H, M, S; 100 int y, m, D, H, M, S;
78 sscanf(line.start, "%04d-%02d-%02dT%02d:%02d:%02d ", &y, &m, &D, &H, &M, &S); 101 sscanf(line.start, "%04d-%02d-%02dT%02d:%02d:%02d ", &y, &m, &D, &H, &M, &S);
79 if (!y) break; 102 if (!y) break;
@@ -99,6 +122,16 @@ void clear_Visited(iVisited *d) {
99 clear_SortedArray(&d->visited); 122 clear_SortedArray(&d->visited);
100} 123}
101 124
125static size_t find_Visited_(const iVisited *d, const iString *url) {
126 iVisitedUrl visit;
127 init_VisitedUrl(&visit);
128 set_String(&visit.url, url);
129 size_t pos = iInvalidPos;
130 locate_SortedArray(&d->visited, &visit, &pos);
131 deinit_VisitedUrl(&visit);
132 return pos;
133}
134
102void visitUrl_Visited(iVisited *d, const iString *url) { 135void visitUrl_Visited(iVisited *d, const iString *url) {
103 iVisitedUrl visit; 136 iVisitedUrl visit;
104 init_VisitedUrl(&visit); 137 init_VisitedUrl(&visit);
@@ -115,6 +148,14 @@ void visitUrl_Visited(iVisited *d, const iString *url) {
115 insert_SortedArray(&d->visited, &visit); 148 insert_SortedArray(&d->visited, &visit);
116} 149}
117 150
151void removeUrl_Visited(iVisited *d, const iString *url) {
152 size_t pos = find_Visited_(d, url);
153 if (pos != iInvalidPos) {
154 deinit_VisitedUrl(at_SortedArray(&d->visited, pos));
155 remove_Array(&d->visited.values, pos);
156 }
157}
158
118iTime urlVisitTime_Visited(const iVisited *d, const iString *url) { 159iTime urlVisitTime_Visited(const iVisited *d, const iString *url) {
119 iVisitedUrl item; 160 iVisitedUrl item;
120 size_t pos; 161 size_t pos;
@@ -126,3 +167,17 @@ iTime urlVisitTime_Visited(const iVisited *d, const iString *url) {
126 deinit_String(&item.url); 167 deinit_String(&item.url);
127 return item.when; 168 return item.when;
128} 169}
170
171static int cmpWhenDescending_VisitedUrlPtr_(const void *a, const void *b) {
172 const iVisitedUrl *s = *(const void **) a, *t = *(const void **) b;
173 return -cmp_Time(&s->when, &t->when);
174}
175
176const iArray *list_Visited(const iVisited *d, size_t count) {
177 iPtrArray *urls = collectNew_PtrArray();
178 iConstForEach(Array, i, &d->visited.values) {
179 pushBack_PtrArray(urls, i.value);
180 }
181 sort_Array(urls, cmpWhenDescending_VisitedUrlPtr_);
182 return urls;
183}
diff --git a/src/visited.h b/src/visited.h
index 2f8382c7..5a34d008 100644
--- a/src/visited.h
+++ b/src/visited.h
@@ -1,8 +1,30 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2 24
3#include "gmrequest.h" 25#include "gmrequest.h"
4 26
5#include <the_Foundation/array.h> 27#include <the_Foundation/ptrarray.h>
6#include <the_Foundation/string.h> 28#include <the_Foundation/string.h>
7#include <the_Foundation/time.h> 29#include <the_Foundation/time.h>
8 30
@@ -23,3 +45,6 @@ void save_Visited (const iVisited *, const char *dirPath);
23 45
24iTime urlVisitTime_Visited (const iVisited *, const iString *url); 46iTime urlVisitTime_Visited (const iVisited *, const iString *url);
25void visitUrl_Visited (iVisited *, const iString *url); /* adds URL to the visited URLs set */ 47void visitUrl_Visited (iVisited *, const iString *url); /* adds URL to the visited URLs set */
48void removeUrl_Visited (iVisited *, const iString *url);
49
50const iPtrArray * list_Visited (const iVisited *, size_t count); /* returns collected */
diff --git a/src/win32.c b/src/win32.c
index 1cdcf34c..7685fcac 100644
--- a/src/win32.c
+++ b/src/win32.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "win32.h" 23#include "win32.h"
2#include <SDL_syswm.h> 24#include <SDL_syswm.h>
3 25
diff --git a/src/win32.h b/src/win32.h
index b223ea4d..3dac677c 100644
--- a/src/win32.h
+++ b/src/win32.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#pragma once 23#pragma once
2#include <SDL_video.h> 24#include <SDL_video.h>
3 25