summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.c412
-rw-r--r--src/app.h14
-rw-r--r--src/bookmarks.c228
-rw-r--r--src/bookmarks.h30
-rw-r--r--src/defs.h23
-rw-r--r--src/feeds.c109
-rw-r--r--src/feeds.h1
-rw-r--r--src/gmdocument.c28
-rw-r--r--src/gmdocument.h5
-rw-r--r--src/gmrequest.c12
-rw-r--r--src/gmrequest.h2
-rw-r--r--src/gmutil.c54
-rw-r--r--src/gmutil.h1
-rw-r--r--src/ios.h1
-rw-r--r--src/ios.m42
-rw-r--r--src/macos.h7
-rw-r--r--src/macos.m224
-rw-r--r--src/media.c62
-rw-r--r--src/prefs.c1
-rw-r--r--src/prefs.h1
-rw-r--r--src/ui/certimportwidget.c180
-rw-r--r--src/ui/color.c43
-rw-r--r--src/ui/color.h3
-rw-r--r--src/ui/documentwidget.c90
-rw-r--r--src/ui/inputwidget.c665
-rw-r--r--src/ui/inputwidget.h1
-rw-r--r--src/ui/keys.c1
-rw-r--r--src/ui/labelwidget.c204
-rw-r--r--src/ui/labelwidget.h4
-rw-r--r--src/ui/listwidget.c155
-rw-r--r--src/ui/listwidget.h4
-rw-r--r--src/ui/lookupwidget.c24
-rw-r--r--src/ui/metrics.c2
-rw-r--r--src/ui/mobile.c547
-rw-r--r--src/ui/mobile.h37
-rw-r--r--src/ui/paint.c66
-rw-r--r--src/ui/paint.h4
-rw-r--r--src/ui/root.c221
-rw-r--r--src/ui/root.h2
-rw-r--r--src/ui/scrollwidget.c20
-rw-r--r--src/ui/sidebarwidget.c379
-rw-r--r--src/ui/sidebarwidget.h8
-rw-r--r--src/ui/text.c146
-rw-r--r--src/ui/text.h17
-rw-r--r--src/ui/text_simple.c18
-rw-r--r--src/ui/touch.c25
-rw-r--r--src/ui/translation.c16
-rw-r--r--src/ui/uploadwidget.c388
-rw-r--r--src/ui/util.c1382
-rw-r--r--src/ui/util.h35
-rw-r--r--src/ui/widget.c454
-rw-r--r--src/ui/widget.h27
-rw-r--r--src/ui/window.c732
-rw-r--r--src/ui/window.h155
-rw-r--r--src/visited.c2
55 files changed, 5508 insertions, 1806 deletions
diff --git a/src/app.c b/src/app.c
index f8dc5697..2bad3cb6 100644
--- a/src/app.c
+++ b/src/app.c
@@ -117,7 +117,8 @@ struct Impl_App {
117 iGmCerts * certs; 117 iGmCerts * certs;
118 iVisited * visited; 118 iVisited * visited;
119 iBookmarks * bookmarks; 119 iBookmarks * bookmarks;
120 iWindow * window; 120 iMainWindow *window;
121 iPtrArray popupWindows;
121 iSortedArray tickers; /* per-frame callbacks, used for animations */ 122 iSortedArray tickers; /* per-frame callbacks, used for animations */
122 uint32_t lastTickerTime; 123 uint32_t lastTickerTime;
123 uint32_t elapsedSinceLastTicker; 124 uint32_t elapsedSinceLastTicker;
@@ -188,7 +189,7 @@ static iString *serializePrefs_App_(const iApp *d) {
188 /* On macOS, maximization should be applied at creation time or the window will take 189 /* On macOS, maximization should be applied at creation time or the window will take
189 a moment to animate to its maximized size. */ 190 a moment to animate to its maximized size. */
190#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 191#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
191 if (snap_Window(d->window)) { 192 if (snap_MainWindow(d->window)) {
192 if (~SDL_GetWindowFlags(d->window->win) & SDL_WINDOW_MINIMIZED) { 193 if (~SDL_GetWindowFlags(d->window->win) & SDL_WINDOW_MINIMIZED) {
193 /* Save the actual visible window position, too, because snapped windows may 194 /* Save the actual visible window position, too, because snapped windows may
194 still be resized/moved without affecting normalRect. */ 195 still be resized/moved without affecting normalRect. */
@@ -196,17 +197,17 @@ static iString *serializePrefs_App_(const iApp *d) {
196 SDL_GetWindowSize(d->window->win, &w, &h); 197 SDL_GetWindowSize(d->window->win, &w, &h);
197 appendFormat_String( 198 appendFormat_String(
198 str, "~window.setrect snap:%d width:%d height:%d coord:%d %d\n", 199 str, "~window.setrect snap:%d width:%d height:%d coord:%d %d\n",
199 snap_Window(d->window), w, h, x, y); 200 snap_MainWindow(d->window), w, h, x, y);
200 } 201 }
201 } 202 }
202#elif !defined (iPlatformApple) 203#elif !defined (iPlatformApple)
203 if (snap_Window(d->window) == maximized_WindowSnap) { 204 if (snap_MainWindow(d->window) == maximized_WindowSnap) {
204 appendFormat_String(str, "~window.maximize\n"); 205 appendFormat_String(str, "~window.maximize\n");
205 } 206 }
206#endif 207#endif
207 } 208 }
208 appendFormat_String(str, "uilang id:%s\n", cstr_String(&d->prefs.uiLanguage)); 209 appendFormat_String(str, "uilang id:%s\n", cstr_String(&d->prefs.uiLanguage));
209 appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window)); 210 appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(as_Window(d->window)));
210 appendFormat_String(str, "prefs.dialogtab arg:%d\n", d->prefs.dialogTab); 211 appendFormat_String(str, "prefs.dialogtab arg:%d\n", d->prefs.dialogTab);
211 appendFormat_String(str, "font.set arg:%d\n", d->prefs.font); 212 appendFormat_String(str, "font.set arg:%d\n", d->prefs.font);
212 appendFormat_String(str, "font.user path:%s\n", cstr_String(&d->prefs.symbolFontPath)); 213 appendFormat_String(str, "font.user path:%s\n", cstr_String(&d->prefs.symbolFontPath));
@@ -242,12 +243,15 @@ static iString *serializePrefs_App_(const iApp *d) {
242 appendFormat_String(str, "doctheme.dark.set arg:%d\n", d->prefs.docThemeDark); 243 appendFormat_String(str, "doctheme.dark.set arg:%d\n", d->prefs.docThemeDark);
243 appendFormat_String(str, "doctheme.light.set arg:%d\n", d->prefs.docThemeLight); 244 appendFormat_String(str, "doctheme.light.set arg:%d\n", d->prefs.docThemeLight);
244 appendFormat_String(str, "saturation.set arg:%d\n", (int) ((d->prefs.saturation * 100) + 0.5f)); 245 appendFormat_String(str, "saturation.set arg:%d\n", (int) ((d->prefs.saturation * 100) + 0.5f));
246 appendFormat_String(str, "imagestyle.set arg:%d\n", d->prefs.imageStyle);
245 appendFormat_String(str, "ca.file noset:1 path:%s\n", cstr_String(&d->prefs.caFile)); 247 appendFormat_String(str, "ca.file noset:1 path:%s\n", cstr_String(&d->prefs.caFile));
246 appendFormat_String(str, "ca.path path:%s\n", cstr_String(&d->prefs.caPath)); 248 appendFormat_String(str, "ca.path path:%s\n", cstr_String(&d->prefs.caPath));
247 appendFormat_String(str, "proxy.gemini address:%s\n", cstr_String(&d->prefs.geminiProxy)); 249 appendFormat_String(str, "proxy.gemini address:%s\n", cstr_String(&d->prefs.geminiProxy));
248 appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->prefs.gopherProxy)); 250 appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->prefs.gopherProxy));
249 appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->prefs.httpProxy)); 251 appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->prefs.httpProxy));
252#if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
250 appendFormat_String(str, "downloads path:%s\n", cstr_String(&d->prefs.downloadDir)); 253 appendFormat_String(str, "downloads path:%s\n", cstr_String(&d->prefs.downloadDir));
254#endif
251 appendFormat_String(str, "searchurl address:%s\n", cstr_String(&d->prefs.searchUrl)); 255 appendFormat_String(str, "searchurl address:%s\n", cstr_String(&d->prefs.searchUrl));
252 appendFormat_String(str, "translation.languages from:%d to:%d\n", d->prefs.langFrom, d->prefs.langTo); 256 appendFormat_String(str, "translation.languages from:%d to:%d\n", d->prefs.langFrom, d->prefs.langTo);
253 return str; 257 return str;
@@ -411,8 +415,8 @@ static iBool loadState_App_(iApp *d) {
411 const int splitMode = read32_File(f); 415 const int splitMode = read32_File(f);
412 const int keyRoot = read32_File(f); 416 const int keyRoot = read32_File(f);
413 d->window->pendingSplitMode = splitMode; 417 d->window->pendingSplitMode = splitMode;
414 setSplitMode_Window(d->window, splitMode | noEvents_WindowSplit); 418 setSplitMode_MainWindow(d->window, splitMode | noEvents_WindowSplit);
415 d->window->keyRoot = d->window->roots[keyRoot]; 419 d->window->base.keyRoot = d->window->base.roots[keyRoot];
416 } 420 }
417 else if (!memcmp(magic, magicSidebar_App_, 4)) { 421 else if (!memcmp(magic, magicSidebar_App_, 4)) {
418 const uint16_t bits = readU16_File(f); 422 const uint16_t bits = readU16_File(f);
@@ -421,12 +425,22 @@ static iBool loadState_App_(iApp *d) {
421 readf_Stream(stream_File(f)), 425 readf_Stream(stream_File(f)),
422 readf_Stream(stream_File(f)) 426 readf_Stream(stream_File(f))
423 }; 427 };
428 iIntSet *closedFolders[2] = {
429 collectNew_IntSet(),
430 collectNew_IntSet()
431 };
432 if (version >= bookmarkFolderState_FileVersion) {
433 deserialize_IntSet(closedFolders[0], stream_File(f));
434 deserialize_IntSet(closedFolders[1], stream_File(f));
435 }
424 const uint8_t rootIndex = bits & 0xff; 436 const uint8_t rootIndex = bits & 0xff;
425 const uint8_t flags = bits >> 8; 437 const uint8_t flags = bits >> 8;
426 iRoot *root = d->window->roots[rootIndex]; 438 iRoot *root = d->window->base.roots[rootIndex];
427 if (root) { 439 if (root) {
428 iSidebarWidget *sidebar = findChild_Widget(root->widget, "sidebar"); 440 iSidebarWidget *sidebar = findChild_Widget(root->widget, "sidebar");
429 iSidebarWidget *sidebar2 = findChild_Widget(root->widget, "sidebar2"); 441 iSidebarWidget *sidebar2 = findChild_Widget(root->widget, "sidebar2");
442 setClosedFolders_SidebarWidget(sidebar, closedFolders[0]);
443 setClosedFolders_SidebarWidget(sidebar2, closedFolders[1]);
430 postCommandf_Root(root, "sidebar.mode arg:%u", modes & 0xf); 444 postCommandf_Root(root, "sidebar.mode arg:%u", modes & 0xf);
431 postCommandf_Root(root, "sidebar2.mode arg:%u", modes >> 4); 445 postCommandf_Root(root, "sidebar2.mode arg:%u", modes >> 4);
432 if (deviceType_App() != phone_AppDeviceType) { 446 if (deviceType_App() != phone_AppDeviceType) {
@@ -440,10 +454,10 @@ static iBool loadState_App_(iApp *d) {
440 else if (!memcmp(magic, magicTabDocument_App_, 4)) { 454 else if (!memcmp(magic, magicTabDocument_App_, 4)) {
441 const int8_t flags = read8_File(f); 455 const int8_t flags = read8_File(f);
442 int rootIndex = flags & rootIndex1_DocumentStateFlag ? 1 : 0; 456 int rootIndex = flags & rootIndex1_DocumentStateFlag ? 1 : 0;
443 if (rootIndex > numRoots_Window(d->window) - 1) { 457 if (rootIndex > numRoots_Window(as_Window(d->window)) - 1) {
444 rootIndex = 0; 458 rootIndex = 0;
445 } 459 }
446 setCurrent_Root(d->window->roots[rootIndex]); 460 setCurrent_Root(d->window->base.roots[rootIndex]);
447 if (isFirstTab[rootIndex]) { 461 if (isFirstTab[rootIndex]) {
448 isFirstTab[rootIndex] = iFalse; 462 isFirstTab[rootIndex] = iFalse;
449 /* There is one pre-created tab in each root. */ 463 /* There is one pre-created tab in each root. */
@@ -466,7 +480,7 @@ static iBool loadState_App_(iApp *d) {
466 } 480 }
467 if (d->window->splitMode) { 481 if (d->window->splitMode) {
468 /* Update root placement. */ 482 /* Update root placement. */
469 resize_Window(d->window, -1, -1); 483 resize_MainWindow(d->window, -1, -1);
470 } 484 }
471 iForIndices(i, current) { 485 iForIndices(i, current) {
472 postCommandf_Root(NULL, "tabs.switch page:%p", current[i]); 486 postCommandf_Root(NULL, "tabs.switch page:%p", current[i]);
@@ -480,7 +494,7 @@ static iBool loadState_App_(iApp *d) {
480static void saveState_App_(const iApp *d) { 494static void saveState_App_(const iApp *d) {
481 iUnused(d); 495 iUnused(d);
482 trimCache_App(); 496 trimCache_App();
483 iWindow *win = d->window; 497 iMainWindow *win = d->window;
484 /* UI state is saved in binary because it is quite complex (e.g., 498 /* UI state is saved in binary because it is quite complex (e.g.,
485 navigation history, cached content) and depends closely on the widget 499 navigation history, cached content) and depends closely on the widget
486 tree. The data is largely not reorderable and should not be modified 500 tree. The data is largely not reorderable and should not be modified
@@ -492,11 +506,11 @@ static void saveState_App_(const iApp *d) {
492 /* Begin with window state. */ { 506 /* Begin with window state. */ {
493 writeData_File(f, magicWindow_App_, 4); 507 writeData_File(f, magicWindow_App_, 4);
494 writeU32_File(f, win->splitMode); 508 writeU32_File(f, win->splitMode);
495 writeU32_File(f, win->keyRoot == win->roots[0] ? 0 : 1); 509 writeU32_File(f, win->base.keyRoot == win->base.roots[0] ? 0 : 1);
496 } 510 }
497 /* State of UI elements. */ { 511 /* State of UI elements. */ {
498 iForIndices(i, win->roots) { 512 iForIndices(i, win->base.roots) {
499 const iRoot *root = win->roots[i]; 513 const iRoot *root = win->base.roots[i];
500 if (root) { 514 if (root) {
501 writeData_File(f, magicSidebar_App_, 4); 515 writeData_File(f, magicSidebar_App_, 4);
502 const iSidebarWidget *sidebar = findChild_Widget(root->widget, "sidebar"); 516 const iSidebarWidget *sidebar = findChild_Widget(root->widget, "sidebar");
@@ -509,6 +523,8 @@ static void saveState_App_(const iApp *d) {
509 (mode_SidebarWidget(sidebar2) << 4)); 523 (mode_SidebarWidget(sidebar2) << 4));
510 writef_Stream(stream_File(f), width_SidebarWidget(sidebar)); 524 writef_Stream(stream_File(f), width_SidebarWidget(sidebar));
511 writef_Stream(stream_File(f), width_SidebarWidget(sidebar2)); 525 writef_Stream(stream_File(f), width_SidebarWidget(sidebar2));
526 serialize_IntSet(closedFolders_SidebarWidget(sidebar), stream_File(f));
527 serialize_IntSet(closedFolders_SidebarWidget(sidebar2), stream_File(f));
512 } 528 }
513 } 529 }
514 } 530 }
@@ -517,7 +533,7 @@ static void saveState_App_(const iApp *d) {
517 const iWidget *widget = constAs_Widget(i.object); 533 const iWidget *widget = constAs_Widget(i.object);
518 writeData_File(f, magicTabDocument_App_, 4); 534 writeData_File(f, magicTabDocument_App_, 4);
519 int8_t flags = (document_Root(widget->root) == i.object ? current_DocumentStateFlag : 0); 535 int8_t flags = (document_Root(widget->root) == i.object ? current_DocumentStateFlag : 0);
520 if (widget->root == win->roots[1]) { 536 if (widget->root == win->base.roots[1]) {
521 flags |= rootIndex1_DocumentStateFlag; 537 flags |= rootIndex1_DocumentStateFlag;
522 } 538 }
523 write8_File(f, flags); 539 write8_File(f, flags);
@@ -667,6 +683,8 @@ static void init_App_(iApp *d, int argc, char **argv) {
667 defineValues_CommandLine(&d->args, "help", 0); 683 defineValues_CommandLine(&d->args, "help", 0);
668 defineValues_CommandLine(&d->args, listTabUrls_CommandLineOption, 0); 684 defineValues_CommandLine(&d->args, listTabUrls_CommandLineOption, 0);
669 defineValues_CommandLine(&d->args, openUrlOrSearch_CommandLineOption, 1); 685 defineValues_CommandLine(&d->args, openUrlOrSearch_CommandLineOption, 1);
686 defineValues_CommandLine(&d->args, windowWidth_CommandLineOption, 1);
687 defineValues_CommandLine(&d->args, windowHeight_CommandLineOption, 1);
670 defineValuesN_CommandLine(&d->args, "new-tab", 0, 1); 688 defineValuesN_CommandLine(&d->args, "new-tab", 0, 1);
671 defineValues_CommandLine(&d->args, "tab-url", 0); 689 defineValues_CommandLine(&d->args, "tab-url", 0);
672 defineValues_CommandLine(&d->args, "sw", 0); 690 defineValues_CommandLine(&d->args, "sw", 0);
@@ -756,7 +774,7 @@ static void init_App_(iApp *d, int argc, char **argv) {
756 mulfv_I2(&d->initialWindowRect.size, desktopDPI_Win32()); 774 mulfv_I2(&d->initialWindowRect.size, desktopDPI_Win32());
757#endif 775#endif
758#if defined (iPlatformLinux) 776#if defined (iPlatformLinux)
759 /* Scale by the primary (?) monitor DPI. */ 777 /* Scale by the primary (?) monitor DPI. */
760 if (isRunningUnderWindowSystem_App()) { 778 if (isRunningUnderWindowSystem_App()) {
761 float vdpi; 779 float vdpi;
762 SDL_GetDisplayDPI(0, NULL, NULL, &vdpi); 780 SDL_GetDisplayDPI(0, NULL, NULL, &vdpi);
@@ -786,7 +804,18 @@ static void init_App_(iApp *d, int argc, char **argv) {
786 setThemePalette_Color(d->prefs.theme); /* default UI colors */ 804 setThemePalette_Color(d->prefs.theme); /* default UI colors */
787 loadPrefs_App_(d); 805 loadPrefs_App_(d);
788 load_Keys(dataDir_App_()); 806 load_Keys(dataDir_App_());
789 d->window = new_Window(d->initialWindowRect); 807 /* See if the user wants to override the window size. */ {
808 iCommandLineArg *arg = iClob(checkArgument_CommandLine(&d->args, windowWidth_CommandLineOption));
809 if (arg) {
810 d->initialWindowRect.size.x = toInt_String(value_CommandLineArg(arg, 0));
811 }
812 arg = iClob(checkArgument_CommandLine(&d->args, windowHeight_CommandLineOption));
813 if (arg) {
814 d->initialWindowRect.size.y = toInt_String(value_CommandLineArg(arg, 0));
815 }
816 }
817 init_PtrArray(&d->popupWindows);
818 d->window = new_MainWindow(d->initialWindowRect);
790 load_Visited(d->visited, dataDir_App_()); 819 load_Visited(d->visited, dataDir_App_());
791 load_Bookmarks(d->bookmarks, dataDir_App_()); 820 load_Bookmarks(d->bookmarks, dataDir_App_());
792 load_MimeHooks(d->mimehooks, dataDir_App_()); 821 load_MimeHooks(d->mimehooks, dataDir_App_());
@@ -833,11 +862,16 @@ static void init_App_(iApp *d, int argc, char **argv) {
833 fetchRemote_Bookmarks(d->bookmarks); 862 fetchRemote_Bookmarks(d->bookmarks);
834 if (deviceType_App() != desktop_AppDeviceType) { 863 if (deviceType_App() != desktop_AppDeviceType) {
835 /* HACK: Force a resize so widgets update their state. */ 864 /* HACK: Force a resize so widgets update their state. */
836 resize_Window(d->window, -1, -1); 865 resize_MainWindow(d->window, -1, -1);
837 } 866 }
838} 867}
839 868
840static void deinit_App(iApp *d) { 869static void deinit_App(iApp *d) {
870 iReverseForEach(PtrArray, i, &d->popupWindows) {
871 delete_Window(i.ptr);
872 }
873 iAssert(isEmpty_PtrArray(&d->popupWindows));
874 deinit_PtrArray(&d->popupWindows);
841#if defined (LAGRANGE_ENABLE_IDLE_SLEEP) 875#if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
842 SDL_RemoveTimer(d->sleepTimer); 876 SDL_RemoveTimer(d->sleepTimer);
843#endif 877#endif
@@ -856,7 +890,7 @@ static void deinit_App(iApp *d) {
856 delete_GmCerts(d->certs); 890 delete_GmCerts(d->certs);
857 save_MimeHooks(d->mimehooks); 891 save_MimeHooks(d->mimehooks);
858 delete_MimeHooks(d->mimehooks); 892 delete_MimeHooks(d->mimehooks);
859 delete_Window(d->window); 893 delete_MainWindow(d->window);
860 d->window = NULL; 894 d->window = NULL;
861 deinit_CommandLine(&d->args); 895 deinit_CommandLine(&d->args);
862 iRelease(d->launchCommands); 896 iRelease(d->launchCommands);
@@ -1039,7 +1073,7 @@ void trimMemory_App(void) {
1039 init_ObjectListIterator(&i, docs); 1073 init_ObjectListIterator(&i, docs);
1040 } 1074 }
1041 } 1075 }
1042 iRelease(docs); 1076 iRelease(docs);
1043} 1077}
1044 1078
1045iLocalDef iBool isWaitingAllowed_App_(iApp *d) { 1079iLocalDef iBool isWaitingAllowed_App_(iApp *d) {
@@ -1054,11 +1088,6 @@ iLocalDef iBool isWaitingAllowed_App_(iApp *d) {
1054 return iFalse; 1088 return iFalse;
1055 } 1089 }
1056#endif 1090#endif
1057#if defined (iPlatformMobile)
1058 if (!isFinished_Anim(&d->window->rootOffset)) {
1059 return iFalse;
1060 }
1061#endif
1062 return !value_Atomic(&d->pendingRefresh) && isEmpty_SortedArray(&d->tickers); 1091 return !value_Atomic(&d->pendingRefresh) && isEmpty_SortedArray(&d->tickers);
1063} 1092}
1064 1093
@@ -1076,6 +1105,15 @@ static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *ev
1076 return SDL_PollEvent(event); 1105 return SDL_PollEvent(event);
1077} 1106}
1078 1107
1108static const iPtrArray *listWindows_App_(const iApp *d) {
1109 iPtrArray *list = collectNew_PtrArray();
1110 iReverseConstForEach(PtrArray, i, &d->popupWindows) {
1111 pushBack_PtrArray(list, i.ptr);
1112 }
1113 pushBack_PtrArray(list, d->window);
1114 return list;
1115}
1116
1079void processEvents_App(enum iAppEventMode eventMode) { 1117void processEvents_App(enum iAppEventMode eventMode) {
1080 iApp *d = &app_; 1118 iApp *d = &app_;
1081 iRoot *oldCurrentRoot = current_Root(); /* restored afterwards */ 1119 iRoot *oldCurrentRoot = current_Root(); /* restored afterwards */
@@ -1100,7 +1138,7 @@ void processEvents_App(enum iAppEventMode eventMode) {
1100 clearCache_App_(); 1138 clearCache_App_();
1101 break; 1139 break;
1102 case SDL_APP_WILLENTERFOREGROUND: 1140 case SDL_APP_WILLENTERFOREGROUND:
1103 invalidate_Window(d->window); 1141 invalidate_Window(as_Window(d->window));
1104 break; 1142 break;
1105 case SDL_APP_DIDENTERFOREGROUND: 1143 case SDL_APP_DIDENTERFOREGROUND:
1106 gotEvents = iTrue; 1144 gotEvents = iTrue;
@@ -1115,17 +1153,17 @@ void processEvents_App(enum iAppEventMode eventMode) {
1115#if defined (iPlatformAppleMobile) 1153#if defined (iPlatformAppleMobile)
1116 updateNowPlayingInfo_iOS(); 1154 updateNowPlayingInfo_iOS();
1117#endif 1155#endif
1118 setFreezeDraw_Window(d->window, iTrue); 1156 setFreezeDraw_MainWindow(d->window, iTrue);
1119 savePrefs_App_(d); 1157 savePrefs_App_(d);
1120 saveState_App_(d); 1158 saveState_App_(d);
1121 break; 1159 break;
1122 case SDL_APP_TERMINATING: 1160 case SDL_APP_TERMINATING:
1123 setFreezeDraw_Window(d->window, iTrue); 1161 setFreezeDraw_MainWindow(d->window, iTrue);
1124 savePrefs_App_(d); 1162 savePrefs_App_(d);
1125 saveState_App_(d); 1163 saveState_App_(d);
1126 break; 1164 break;
1127 case SDL_DROPFILE: { 1165 case SDL_DROPFILE: {
1128 iBool wasUsed = processEvent_Window(d->window, &ev); 1166 iBool wasUsed = processEvent_Window(as_Window(d->window), &ev);
1129 if (!wasUsed) { 1167 if (!wasUsed) {
1130 iBool newTab = iFalse; 1168 iBool newTab = iFalse;
1131 if (elapsedSeconds_Time(&d->lastDropTime) < 0.1) { 1169 if (elapsedSeconds_Time(&d->lastDropTime) < 0.1) {
@@ -1165,23 +1203,6 @@ void processEvents_App(enum iAppEventMode eventMode) {
1165 } 1203 }
1166 d->isIdling = iFalse; 1204 d->isIdling = iFalse;
1167#endif 1205#endif
1168 if (ev.type == SDL_USEREVENT && ev.user.code == arrange_UserEventCode) {
1169 printf("[App] rearrange\n");
1170 resize_Window(d->window, -1, -1);
1171 iForIndices(i, d->window->roots) {
1172 if (d->window->roots[i]) {
1173 d->window->roots[i]->pendingArrange = iFalse;
1174 }
1175 }
1176// if (ev.user.data2 == d->window->roots[0]) {
1177// arrange_Widget(d->window->roots[0]->widget);
1178// }
1179// else if (d->window->roots[1]) {
1180// arrange_Widget(d->window->roots[1]->widget);
1181// }
1182// postRefresh_App();
1183 continue;
1184 }
1185 gotEvents = iTrue; 1206 gotEvents = iTrue;
1186 /* Keyboard modifier mapping. */ 1207 /* Keyboard modifier mapping. */
1187 if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) { 1208 if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) {
@@ -1199,8 +1220,8 @@ void processEvents_App(enum iAppEventMode eventMode) {
1199 if (ev.wheel.which == 0) { 1220 if (ev.wheel.which == 0) {
1200 /* Trackpad with precise scrolling w/inertia (points). */ 1221 /* Trackpad with precise scrolling w/inertia (points). */
1201 setPerPixel_MouseWheelEvent(&ev.wheel, iTrue); 1222 setPerPixel_MouseWheelEvent(&ev.wheel, iTrue);
1202 ev.wheel.x *= -d->window->pixelRatio; 1223 ev.wheel.x *= -d->window->base.pixelRatio;
1203 ev.wheel.y *= d->window->pixelRatio; 1224 ev.wheel.y *= d->window->base.pixelRatio;
1204 /* Only scroll on one axis at a time. */ 1225 /* Only scroll on one axis at a time. */
1205 if (iAbs(ev.wheel.x) > iAbs(ev.wheel.y)) { 1226 if (iAbs(ev.wheel.x) > iAbs(ev.wheel.y)) {
1206 ev.wheel.y = 0; 1227 ev.wheel.y = 0;
@@ -1240,7 +1261,7 @@ void processEvents_App(enum iAppEventMode eventMode) {
1240 } 1261 }
1241 else if (ev.type == SDL_MOUSEMOTION) { 1262 else if (ev.type == SDL_MOUSEMOTION) {
1242 if (~ev.motion.state & SDL_BUTTON(SDL_BUTTON_LEFT)) { 1263 if (~ev.motion.state & SDL_BUTTON(SDL_BUTTON_LEFT)) {
1243 continue; /* only when pressing a button */ 1264 continue; /* only when pressing a button */
1244 } 1265 }
1245 const float xf = (d->window->pixelRatio * ev.motion.x) / (float) d->window->size.x; 1266 const float xf = (d->window->pixelRatio * ev.motion.x) / (float) d->window->size.x;
1246 const float yf = (d->window->pixelRatio * ev.motion.y) / (float) d->window->size.y; 1267 const float yf = (d->window->pixelRatio * ev.motion.y) / (float) d->window->size.y;
@@ -1254,13 +1275,26 @@ void processEvents_App(enum iAppEventMode eventMode) {
1254 ev.tfinger.fingerId = 0x1234; 1275 ev.tfinger.fingerId = 0x1234;
1255 ev.tfinger.pressure = 1.0f; 1276 ev.tfinger.pressure = 1.0f;
1256 ev.tfinger.timestamp = SDL_GetTicks(); 1277 ev.tfinger.timestamp = SDL_GetTicks();
1257 ev.tfinger.touchId = SDL_TOUCH_MOUSEID; 1278 ev.tfinger.touchId = SDL_TOUCH_MOUSEID;
1258 } 1279 }
1259 } 1280 }
1260#endif 1281#endif
1261 iBool wasUsed = processEvent_Window(d->window, &ev); 1282 /* Per-window processing. */
1283 iBool wasUsed = iFalse;
1284 const iPtrArray *windows = listWindows_App_(d);
1285 iConstForEach(PtrArray, iter, windows) {
1286 iWindow *window = iter.ptr;
1287 setCurrent_Window(window);
1288 window->lastHover = window->hover;
1289 wasUsed = processEvent_Window(window, &ev);
1290 if (ev.type == SDL_MOUSEMOTION || ev.type == SDL_MOUSEBUTTONDOWN) {
1291 break;
1292 }
1293 if (wasUsed) break;
1294 }
1295 setCurrent_Window(d->window);
1262 if (!wasUsed) { 1296 if (!wasUsed) {
1263 /* There may be a key bindings for this. */ 1297 /* There may be a key binding for this. */
1264 wasUsed = processEvent_Keys(&ev); 1298 wasUsed = processEvent_Keys(&ev);
1265 } 1299 }
1266 if (!wasUsed) { 1300 if (!wasUsed) {
@@ -1278,26 +1312,39 @@ void processEvents_App(enum iAppEventMode eventMode) {
1278 handleCommand_MacOS(command_UserEvent(&ev)); 1312 handleCommand_MacOS(command_UserEvent(&ev));
1279#endif 1313#endif
1280 if (isMetricsChange_UserEvent(&ev)) { 1314 if (isMetricsChange_UserEvent(&ev)) {
1281 iForIndices(i, d->window->roots) { 1315 iConstForEach(PtrArray, iter, windows) {
1282 iRoot *root = d->window->roots[i]; 1316 iWindow *window = iter.ptr;
1283 if (root) { 1317 iForIndices(i, window->roots) {
1284 arrange_Widget(root->widget); 1318 iRoot *root = window->roots[i];
1319 if (root) {
1320 arrange_Widget(root->widget);
1321 }
1285 } 1322 }
1286 } 1323 }
1287 } 1324 }
1288 if (!wasUsed) { 1325 if (!wasUsed) {
1289 /* No widget handled the command, so we'll do it. */ 1326 /* No widget handled the command, so we'll do it. */
1327 setCurrent_Window(d->window);
1290 handleCommand_App(ev.user.data1); 1328 handleCommand_App(ev.user.data1);
1291 } 1329 }
1292 /* Allocated by postCommand_Apps(). */ 1330 /* Allocated by postCommand_Apps(). */
1293 free(ev.user.data1); 1331 free(ev.user.data1);
1294 } 1332 }
1333 /* Refresh after hover changes. */ {
1334 iConstForEach(PtrArray, iter, windows) {
1335 iWindow *window = iter.ptr;
1336 if (window->lastHover != window->hover) {
1337 refresh_Widget(window->lastHover);
1338 refresh_Widget(window->hover);
1339 }
1340 }
1341 }
1295 break; 1342 break;
1296 } 1343 }
1297 } 1344 }
1298 } 1345 }
1299#if defined (LAGRANGE_ENABLE_IDLE_SLEEP) 1346#if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1300 if (d->isIdling && !gotEvents && isFinished_Anim(&d->window->rootOffset)) { 1347 if (d->isIdling && !gotEvents) {
1301 /* This is where we spend most of our time when idle. 60 Hz still quite a lot but we 1348 /* This is where we spend most of our time when idle. 60 Hz still quite a lot but we
1302 can't wait too long after the user tries to interact again with the app. In any 1349 can't wait too long after the user tries to interact again with the app. In any
1303 case, on macOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping. */ 1350 case, on macOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping. */
@@ -1339,22 +1386,23 @@ static int resizeWatcher_(void *user, SDL_Event *event) {
1339 if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { 1386 if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
1340 const SDL_WindowEvent *winev = &event->window; 1387 const SDL_WindowEvent *winev = &event->window;
1341#if defined (iPlatformMsys) 1388#if defined (iPlatformMsys)
1342 resetFonts_Text(); { 1389 resetFonts_Text(text_Window(d->window)); {
1343 SDL_Event u = { .type = SDL_USEREVENT }; 1390 SDL_Event u = { .type = SDL_USEREVENT };
1344 u.user.code = command_UserEventCode; 1391 u.user.code = command_UserEventCode;
1345 u.user.data1 = strdup("theme.changed auto:1"); 1392 u.user.data1 = strdup("theme.changed auto:1");
1346 dispatchEvent_Window(d->window, &u); 1393 dispatchEvent_Window(as_Window(d->window), &u);
1347 } 1394 }
1348#endif 1395#endif
1349 drawWhileResizing_Window(d->window, winev->data1, winev->data2); 1396 drawWhileResizing_MainWindow(d->window, winev->data1, winev->data2);
1350 } 1397 }
1351 return 0; 1398 return 0;
1352} 1399}
1353 1400
1354static int run_App_(iApp *d) { 1401static int run_App_(iApp *d) {
1355 iForIndices(i, d->window->roots) { 1402 /* Initial arrangement. */
1356 if (d->window->roots[i]) { 1403 iForIndices(i, d->window->base.roots) {
1357 arrange_Widget(d->window->roots[i]->widget); 1404 if (d->window->base.roots[i]) {
1405 arrange_Widget(d->window->base.roots[i]->widget);
1358 } 1406 }
1359 } 1407 }
1360 d->isRunning = iTrue; 1408 d->isRunning = iTrue;
@@ -1368,7 +1416,7 @@ static int run_App_(iApp *d) {
1368 runTickers_App_(d); 1416 runTickers_App_(d);
1369 refresh_App(); 1417 refresh_App();
1370 /* Change the widget tree while we are not iterating through it. */ 1418 /* Change the widget tree while we are not iterating through it. */
1371 checkPendingSplit_Window(d->window); 1419 checkPendingSplit_MainWindow(d->window);
1372 recycle_Garbage(); 1420 recycle_Garbage();
1373 } 1421 }
1374 SDL_DelEventWatch(resizeWatcher_, d); 1422 SDL_DelEventWatch(resizeWatcher_, d);
@@ -1377,28 +1425,46 @@ static int run_App_(iApp *d) {
1377 1425
1378void refresh_App(void) { 1426void refresh_App(void) {
1379 iApp *d = &app_; 1427 iApp *d = &app_;
1380 iForIndices(i, d->window->roots) {
1381 iRoot *root = d->window->roots[i];
1382 if (root) {
1383 destroyPending_Root(root);
1384 }
1385 }
1386#if defined (LAGRANGE_ENABLE_IDLE_SLEEP) 1428#if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1387 if (d->warmupFrames == 0 && d->isIdling) { 1429 if (d->warmupFrames == 0 && d->isIdling) {
1388 return; 1430 return;
1389 } 1431 }
1390#endif 1432#endif
1433 const iPtrArray *windows = listWindows_App_(d);
1434 /* Destroy pending widgets. */ {
1435 iConstForEach(PtrArray, j, windows) {
1436 iWindow *win = j.ptr;
1437 setCurrent_Window(win);
1438 iForIndices(i, win->roots) {
1439 iRoot *root = win->roots[i];
1440 if (root) {
1441 destroyPending_Root(root);
1442 }
1443 }
1444 }
1445 }
1446 /* TODO: Pending refresh is window-specific. */
1391 if (!exchange_Atomic(&d->pendingRefresh, iFalse)) { 1447 if (!exchange_Atomic(&d->pendingRefresh, iFalse)) {
1392 /* Refreshing wasn't pending. */ 1448 return;
1393 if (isFinished_Anim(&d->window->rootOffset)) { 1449 }
1394 return; 1450 /* Draw each window. */ {
1451 iConstForEach(PtrArray, j, windows) {
1452 iWindow *win = j.ptr;
1453 setCurrent_Window(win);
1454 switch (win->type) {
1455 case main_WindowType:
1456 // iTime draw;
1457 // initCurrent_Time(&draw);
1458 draw_MainWindow(as_MainWindow(win));
1459 // printf("draw: %lld \u03bcs\n", (long long) (elapsedSeconds_Time(&draw) * 1000000));
1460 // fflush(stdout);
1461 break;
1462 default:
1463 draw_Window(win);
1464 break;
1465 }
1395 } 1466 }
1396 } 1467 }
1397// iTime draw;
1398// initCurrent_Time(&draw);
1399 draw_Window(d->window);
1400// printf("draw: %lld \u03bcs\n", (long long) (elapsedSeconds_Time(&draw) * 1000000));
1401// fflush(stdout);
1402 if (d->warmupFrames > 0) { 1468 if (d->warmupFrames > 0) {
1403 d->warmupFrames--; 1469 d->warmupFrames--;
1404 } 1470 }
@@ -1471,12 +1537,6 @@ void postRefresh_App(void) {
1471 } 1537 }
1472} 1538}
1473 1539
1474void postImmediateRefresh_App(void) {
1475 SDL_Event ev = { .type = SDL_USEREVENT };
1476 ev.user.code = immediateRefresh_UserEventCode;
1477 SDL_PushEvent(&ev);
1478}
1479
1480void postCommand_Root(iRoot *d, const char *command) { 1540void postCommand_Root(iRoot *d, const char *command) {
1481 iAssert(command); 1541 iAssert(command);
1482 if (strlen(command) == 0) { 1542 if (strlen(command) == 0) {
@@ -1532,7 +1592,7 @@ void postCommandf_App(const char *command, ...) {
1532} 1592}
1533 1593
1534void rootOrder_App(iRoot *roots[2]) { 1594void rootOrder_App(iRoot *roots[2]) {
1535 const iWindow *win = app_.window; 1595 const iWindow *win = get_Window();
1536 roots[0] = win->keyRoot; 1596 roots[0] = win->keyRoot;
1537 roots[1] = (roots[0] == win->roots[0] ? win->roots[1] : win->roots[0]); 1597 roots[1] = (roots[0] == win->roots[0] ? win->roots[1] : win->roots[0]);
1538} 1598}
@@ -1569,6 +1629,16 @@ void removeTicker_App(iTickerFunc ticker, iAny *context) {
1569 remove_SortedArray(&d->tickers, &(iTicker){ context, NULL, ticker }); 1629 remove_SortedArray(&d->tickers, &(iTicker){ context, NULL, ticker });
1570} 1630}
1571 1631
1632void addPopup_App(iWindow *popup) {
1633 iApp *d = &app_;
1634 pushBack_PtrArray(&d->popupWindows, popup);
1635}
1636
1637void removePopup_App(iWindow *popup) {
1638 iApp *d = &app_;
1639 removeOne_PtrArray(&d->popupWindows, popup);
1640}
1641
1572iMimeHooks *mimeHooks_App(void) { 1642iMimeHooks *mimeHooks_App(void) {
1573 return app_.mimehooks; 1643 return app_.mimehooks;
1574} 1644}
@@ -1583,7 +1653,11 @@ iBool isLandscape_App(void) {
1583} 1653}
1584 1654
1585enum iAppDeviceType deviceType_App(void) { 1655enum iAppDeviceType deviceType_App(void) {
1586#if defined (iPlatformAppleMobile) 1656#if defined (iPlatformMobilePhone)
1657 return phone_AppDeviceType;
1658#elif defined (iPlatformMobileTablet)
1659 return tablet_AppDeviceType;
1660#elif defined (iPlatformAppleMobile)
1587 return isPhone_iOS() ? phone_AppDeviceType : tablet_AppDeviceType; 1661 return isPhone_iOS() ? phone_AppDeviceType : tablet_AppDeviceType;
1588#else 1662#else
1589 return desktop_AppDeviceType; 1663 return desktop_AppDeviceType;
@@ -1636,28 +1710,20 @@ static void updateScrollSpeedButtons_(iWidget *d, enum iScrollType type, const i
1636 } 1710 }
1637} 1711}
1638 1712
1639static void updateDropdownSelection_(iLabelWidget *dropButton, const char *selectedCommand) {
1640 iWidget *menu = findChild_Widget(as_Widget(dropButton), "menu");
1641 iForEach(ObjectList, i, children_Widget(menu)) {
1642 if (isInstance_Object(i.object, &Class_LabelWidget)) {
1643 iLabelWidget *item = i.object;
1644 const iBool isSelected = endsWith_String(command_LabelWidget(item), selectedCommand);
1645 setFlags_Widget(as_Widget(item), selected_WidgetFlag, isSelected);
1646 if (isSelected) {
1647 updateText_LabelWidget(dropButton, sourceText_LabelWidget(item));
1648 }
1649 }
1650 }
1651}
1652
1653static void updateColorThemeButton_(iLabelWidget *button, int theme) { 1713static void updateColorThemeButton_(iLabelWidget *button, int theme) {
1714 /* TODO: These three functions are all the same? Cleanup? */
1654 if (!button) return; 1715 if (!button) return;
1655 updateDropdownSelection_(button, format_CStr(".set arg:%d", theme)); 1716 updateDropdownSelection_LabelWidget(button, format_CStr(".set arg:%d", theme));
1656} 1717}
1657 1718
1658static void updateFontButton_(iLabelWidget *button, int font) { 1719static void updateFontButton_(iLabelWidget *button, int font) {
1659 if (!button) return; 1720 if (!button) return;
1660 updateDropdownSelection_(button, format_CStr(".set arg:%d", font)); 1721 updateDropdownSelection_LabelWidget(button, format_CStr(".set arg:%d", font));
1722}
1723
1724static void updateImageStyleButton_(iLabelWidget *button, int style) {
1725 if (!button) return;
1726 updateDropdownSelection_LabelWidget(button, format_CStr(".set arg:%d", style));
1661} 1727}
1662 1728
1663static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { 1729static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
@@ -1710,8 +1776,8 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1710 return iTrue; 1776 return iTrue;
1711 } 1777 }
1712 else if (equal_Command(cmd, "uilang")) { 1778 else if (equal_Command(cmd, "uilang")) {
1713 updateDropdownSelection_(findChild_Widget(d, "prefs.uilang"), 1779 updateDropdownSelection_LabelWidget(findChild_Widget(d, "prefs.uilang"),
1714 cstr_String(string_Command(cmd, "id"))); 1780 cstr_String(string_Command(cmd, "id")));
1715 return iFalse; 1781 return iFalse;
1716 } 1782 }
1717 else if (equal_Command(cmd, "quoteicon.set")) { 1783 else if (equal_Command(cmd, "quoteicon.set")) {
@@ -1721,8 +1787,8 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1721 return iFalse; 1787 return iFalse;
1722 } 1788 }
1723 else if (equal_Command(cmd, "returnkey.set")) { 1789 else if (equal_Command(cmd, "returnkey.set")) {
1724 updateDropdownSelection_(findChild_Widget(d, "prefs.returnkey"), 1790 updateDropdownSelection_LabelWidget(findChild_Widget(d, "prefs.returnkey"),
1725 format_CStr("returnkey.set arg:%d", arg_Command(cmd))); 1791 format_CStr("returnkey.set arg:%d", arg_Command(cmd)));
1726 return iFalse; 1792 return iFalse;
1727 } 1793 }
1728 else if (equal_Command(cmd, "pinsplit.set")) { 1794 else if (equal_Command(cmd, "pinsplit.set")) {
@@ -1741,6 +1807,10 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1741 updateColorThemeButton_(findChild_Widget(d, "prefs.doctheme.light"), arg_Command(cmd)); 1807 updateColorThemeButton_(findChild_Widget(d, "prefs.doctheme.light"), arg_Command(cmd));
1742 return iFalse; 1808 return iFalse;
1743 } 1809 }
1810 else if (equal_Command(cmd, "imagestyle.set")) {
1811 updateImageStyleButton_(findChild_Widget(d, "prefs.imagestyle"), arg_Command(cmd));
1812 return iFalse;
1813 }
1744 else if (equal_Command(cmd, "font.set")) { 1814 else if (equal_Command(cmd, "font.set")) {
1745 updateFontButton_(findChild_Widget(d, "prefs.font"), arg_Command(cmd)); 1815 updateFontButton_(findChild_Widget(d, "prefs.font"), arg_Command(cmd));
1746 return iFalse; 1816 return iFalse;
@@ -1749,7 +1819,7 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1749 updateFontButton_(findChild_Widget(d, "prefs.headingfont"), arg_Command(cmd)); 1819 updateFontButton_(findChild_Widget(d, "prefs.headingfont"), arg_Command(cmd));
1750 return iFalse; 1820 return iFalse;
1751 } 1821 }
1752 else if (startsWith_CStr(cmd, "input.ended id:prefs.linespacing")) { 1822 else if (startsWith_CStr(cmd, "input.ended id:prefs.linespacing")) {
1753 /* Apply line spacing changes immediately. */ 1823 /* Apply line spacing changes immediately. */
1754 const iInputWidget *lineSpacing = findWidget_App("prefs.linespacing"); 1824 const iInputWidget *lineSpacing = findWidget_App("prefs.linespacing");
1755 postCommandf_App("linespacing.set arg:%f", toFloat_String(text_InputWidget(lineSpacing))); 1825 postCommandf_App("linespacing.set arg:%f", toFloat_String(text_InputWidget(lineSpacing)));
@@ -1822,7 +1892,10 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe
1822static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) { 1892static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
1823 iApp *d = &app_; 1893 iApp *d = &app_;
1824 if (equal_Command(cmd, "ident.showmore")) { 1894 if (equal_Command(cmd, "ident.showmore")) {
1825 iForEach(ObjectList, i, children_Widget(findChild_Widget(dlg, "headings"))) { 1895 iForEach(ObjectList,
1896 i,
1897 children_Widget(findChild_Widget(
1898 dlg, isUsingPanelLayout_Mobile() ? "panel.top" : "headings"))) {
1826 if (flags_Widget(i.object) & collapse_WidgetFlag) { 1899 if (flags_Widget(i.object) & collapse_WidgetFlag) {
1827 setFlags_Widget(i.object, hidden_WidgetFlag, iFalse); 1900 setFlags_Widget(i.object, hidden_WidgetFlag, iFalse);
1828 } 1901 }
@@ -1832,10 +1905,9 @@ static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
1832 setFlags_Widget(j.object, hidden_WidgetFlag, iFalse); 1905 setFlags_Widget(j.object, hidden_WidgetFlag, iFalse);
1833 } 1906 }
1834 } 1907 }
1835 setFlags_Widget(child_Widget(findChild_Widget(dlg, "dialogbuttons"), 0), disabled_WidgetFlag, 1908 setFlags_Widget(pointer_Command(cmd), disabled_WidgetFlag, iTrue);
1836 iTrue);
1837 arrange_Widget(dlg); 1909 arrange_Widget(dlg);
1838 refresh_Widget(dlg); 1910 refresh_Widget(dlg);
1839 return iTrue; 1911 return iTrue;
1840 } 1912 }
1841 if (equal_Command(cmd, "ident.scope")) { 1913 if (equal_Command(cmd, "ident.scope")) {
@@ -1843,6 +1915,7 @@ static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
1843 setText_LabelWidget(scope, 1915 setText_LabelWidget(scope,
1844 text_LabelWidget(child_Widget( 1916 text_LabelWidget(child_Widget(
1845 findChild_Widget(as_Widget(scope), "menu"), arg_Command(cmd)))); 1917 findChild_Widget(as_Widget(scope), "menu"), arg_Command(cmd))));
1918 arrange_Widget(findWidget_App("ident"));
1846 return iTrue; 1919 return iTrue;
1847 } 1920 }
1848 if (equal_Command(cmd, "ident.temp.changed")) { 1921 if (equal_Command(cmd, "ident.temp.changed")) {
@@ -1963,9 +2036,16 @@ const iString *searchQueryUrl_App(const iString *queryStringUnescaped) {
1963 return collectNewFormat_String("%s?%s", cstr_String(&d->prefs.searchUrl), cstr_String(escaped)); 2036 return collectNewFormat_String("%s?%s", cstr_String(&d->prefs.searchUrl), cstr_String(escaped));
1964} 2037}
1965 2038
2039static void resetFonts_App_(iApp *d) {
2040 iConstForEach(PtrArray, win, listWindows_App_(d)) {
2041 resetFonts_Text(text_Window(win.ptr));
2042 }
2043}
2044
1966iBool handleCommand_App(const char *cmd) { 2045iBool handleCommand_App(const char *cmd) {
1967 iApp *d = &app_; 2046 iApp *d = &app_;
1968 const iBool isFrozen = !d->window || d->window->isDrawFrozen; 2047 const iBool isFrozen = !d->window || d->window->isDrawFrozen;
2048 /* TODO: Maybe break this up a little bit? There's a very long list of ifs here. */
1969 if (equal_Command(cmd, "config.error")) { 2049 if (equal_Command(cmd, "config.error")) {
1970 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "CONFIG ERROR", 2050 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "CONFIG ERROR",
1971 format_CStr("Error in config file: %s\n" 2051 format_CStr("Error in config file: %s\n"
@@ -1997,13 +2077,13 @@ iBool handleCommand_App(const char *cmd) {
1997 } 2077 }
1998 else if (equal_Command(cmd, "ui.split")) { 2078 else if (equal_Command(cmd, "ui.split")) {
1999 if (argLabel_Command(cmd, "swap")) { 2079 if (argLabel_Command(cmd, "swap")) {
2000 swapRoots_Window(d->window); 2080 swapRoots_MainWindow(d->window);
2001 return iTrue; 2081 return iTrue;
2002 } 2082 }
2003 d->window->pendingSplitMode = 2083 d->window->pendingSplitMode =
2004 (argLabel_Command(cmd, "axis") ? vertical_WindowSplit : 0) | (arg_Command(cmd) << 1); 2084 (argLabel_Command(cmd, "axis") ? vertical_WindowSplit : 0) | (arg_Command(cmd) << 1);
2005 const char *url = suffixPtr_Command(cmd, "url"); 2085 const char *url = suffixPtr_Command(cmd, "url");
2006 setCStr_String(get_Window()->pendingSplitUrl, url ? url : ""); 2086 setCStr_String(d->window->pendingSplitUrl, url ? url : "");
2007 postRefresh_App(); 2087 postRefresh_App();
2008 return iTrue; 2088 return iTrue;
2009 } 2089 }
@@ -2017,33 +2097,33 @@ iBool handleCommand_App(const char *cmd) {
2017 } 2097 }
2018 else if (equal_Command(cmd, "window.maximize")) { 2098 else if (equal_Command(cmd, "window.maximize")) {
2019 if (!argLabel_Command(cmd, "toggle")) { 2099 if (!argLabel_Command(cmd, "toggle")) {
2020 setSnap_Window(d->window, maximized_WindowSnap); 2100 setSnap_MainWindow(d->window, maximized_WindowSnap);
2021 } 2101 }
2022 else { 2102 else {
2023 setSnap_Window(d->window, snap_Window(d->window) == maximized_WindowSnap ? 0 : 2103 setSnap_MainWindow(d->window, snap_MainWindow(d->window) == maximized_WindowSnap ? 0 :
2024 maximized_WindowSnap); 2104 maximized_WindowSnap);
2025 } 2105 }
2026 return iTrue; 2106 return iTrue;
2027 } 2107 }
2028 else if (equal_Command(cmd, "window.fullscreen")) { 2108 else if (equal_Command(cmd, "window.fullscreen")) {
2029 const iBool wasFull = snap_Window(d->window) == fullscreen_WindowSnap; 2109 const iBool wasFull = snap_MainWindow(d->window) == fullscreen_WindowSnap;
2030 setSnap_Window(d->window, wasFull ? 0 : fullscreen_WindowSnap); 2110 setSnap_MainWindow(d->window, wasFull ? 0 : fullscreen_WindowSnap);
2031 postCommandf_App("window.fullscreen.changed arg:%d", !wasFull); 2111 postCommandf_App("window.fullscreen.changed arg:%d", !wasFull);
2032 return iTrue; 2112 return iTrue;
2033 } 2113 }
2034 else if (equal_Command(cmd, "font.reset")) { 2114 else if (equal_Command(cmd, "font.reset")) {
2035 resetFonts_Text(); 2115 resetFonts_App_(d);
2036 return iTrue; 2116 return iTrue;
2037 } 2117 }
2038 else if (equal_Command(cmd, "font.user")) { 2118 else if (equal_Command(cmd, "font.user")) {
2039 const char *path = suffixPtr_Command(cmd, "path"); 2119 const char *path = suffixPtr_Command(cmd, "path");
2040 if (cmp_String(&d->prefs.symbolFontPath, path)) { 2120 if (cmp_String(&d->prefs.symbolFontPath, path)) {
2041 if (!isFrozen) { 2121 if (!isFrozen) {
2042 setFreezeDraw_Window(get_Window(), iTrue); 2122 setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
2043 } 2123 }
2044 setCStr_String(&d->prefs.symbolFontPath, path); 2124 setCStr_String(&d->prefs.symbolFontPath, path);
2045 loadUserFonts_Text(); 2125 loadUserFonts_Text();
2046 resetFonts_Text(); 2126 resetFonts_App_(d);
2047 if (!isFrozen) { 2127 if (!isFrozen) {
2048 postCommand_App("font.changed"); 2128 postCommand_App("font.changed");
2049 postCommand_App("window.unfreeze"); 2129 postCommand_App("window.unfreeze");
@@ -2053,10 +2133,10 @@ iBool handleCommand_App(const char *cmd) {
2053 } 2133 }
2054 else if (equal_Command(cmd, "font.set")) { 2134 else if (equal_Command(cmd, "font.set")) {
2055 if (!isFrozen) { 2135 if (!isFrozen) {
2056 setFreezeDraw_Window(get_Window(), iTrue); 2136 setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
2057 } 2137 }
2058 d->prefs.font = arg_Command(cmd); 2138 d->prefs.font = arg_Command(cmd);
2059 setContentFont_Text(d->prefs.font); 2139 setContentFont_Text(text_Window(d->window), d->prefs.font);
2060 if (!isFrozen) { 2140 if (!isFrozen) {
2061 postCommand_App("font.changed"); 2141 postCommand_App("font.changed");
2062 postCommand_App("window.unfreeze"); 2142 postCommand_App("window.unfreeze");
@@ -2065,10 +2145,10 @@ iBool handleCommand_App(const char *cmd) {
2065 } 2145 }
2066 else if (equal_Command(cmd, "headingfont.set")) { 2146 else if (equal_Command(cmd, "headingfont.set")) {
2067 if (!isFrozen) { 2147 if (!isFrozen) {
2068 setFreezeDraw_Window(get_Window(), iTrue); 2148 setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
2069 } 2149 }
2070 d->prefs.headingFont = arg_Command(cmd); 2150 d->prefs.headingFont = arg_Command(cmd);
2071 setHeadingFont_Text(d->prefs.headingFont); 2151 setHeadingFont_Text(text_Window(d->window), d->prefs.headingFont);
2072 if (!isFrozen) { 2152 if (!isFrozen) {
2073 postCommand_App("font.changed"); 2153 postCommand_App("font.changed");
2074 postCommand_App("window.unfreeze"); 2154 postCommand_App("window.unfreeze");
@@ -2077,10 +2157,10 @@ iBool handleCommand_App(const char *cmd) {
2077 } 2157 }
2078 else if (equal_Command(cmd, "zoom.set")) { 2158 else if (equal_Command(cmd, "zoom.set")) {
2079 if (!isFrozen) { 2159 if (!isFrozen) {
2080 setFreezeDraw_Window(get_Window(), iTrue); /* no intermediate draws before docs updated */ 2160 setFreezeDraw_MainWindow(get_MainWindow(), iTrue); /* no intermediate draws before docs updated */
2081 } 2161 }
2082 d->prefs.zoomPercent = arg_Command(cmd); 2162 d->prefs.zoomPercent = arg_Command(cmd);
2083 setContentFontSize_Text((float) d->prefs.zoomPercent / 100.0f); 2163 setContentFontSize_Text(text_Window(d->window), (float) d->prefs.zoomPercent / 100.0f);
2084 if (!isFrozen) { 2164 if (!isFrozen) {
2085 postCommand_App("font.changed"); 2165 postCommand_App("font.changed");
2086 postCommand_App("window.unfreeze"); 2166 postCommand_App("window.unfreeze");
@@ -2089,14 +2169,14 @@ iBool handleCommand_App(const char *cmd) {
2089 } 2169 }
2090 else if (equal_Command(cmd, "zoom.delta")) { 2170 else if (equal_Command(cmd, "zoom.delta")) {
2091 if (!isFrozen) { 2171 if (!isFrozen) {
2092 setFreezeDraw_Window(get_Window(), iTrue); /* no intermediate draws before docs updated */ 2172 setFreezeDraw_MainWindow(get_MainWindow(), iTrue); /* no intermediate draws before docs updated */
2093 } 2173 }
2094 int delta = arg_Command(cmd); 2174 int delta = arg_Command(cmd);
2095 if (d->prefs.zoomPercent < 100 || (delta < 0 && d->prefs.zoomPercent == 100)) { 2175 if (d->prefs.zoomPercent < 100 || (delta < 0 && d->prefs.zoomPercent == 100)) {
2096 delta /= 2; 2176 delta /= 2;
2097 } 2177 }
2098 d->prefs.zoomPercent = iClamp(d->prefs.zoomPercent + delta, 50, 200); 2178 d->prefs.zoomPercent = iClamp(d->prefs.zoomPercent + delta, 50, 200);
2099 setContentFontSize_Text((float) d->prefs.zoomPercent / 100.0f); 2179 setContentFontSize_Text(text_Window(d->window), (float) d->prefs.zoomPercent / 100.0f);
2100 if (!isFrozen) { 2180 if (!isFrozen) {
2101 postCommand_App("font.changed"); 2181 postCommand_App("font.changed");
2102 postCommand_App("window.unfreeze"); 2182 postCommand_App("window.unfreeze");
@@ -2173,6 +2253,10 @@ iBool handleCommand_App(const char *cmd) {
2173 } 2253 }
2174 return iTrue; 2254 return iTrue;
2175 } 2255 }
2256 else if (equal_Command(cmd, "imagestyle.set")) {
2257 d->prefs.imageStyle = arg_Command(cmd);
2258 return iTrue;
2259 }
2176 else if (equal_Command(cmd, "linewidth.set")) { 2260 else if (equal_Command(cmd, "linewidth.set")) {
2177 d->prefs.lineWidth = iMax(20, arg_Command(cmd)); 2261 d->prefs.lineWidth = iMax(20, arg_Command(cmd));
2178 postCommand_App("document.layout.changed"); 2262 postCommand_App("document.layout.changed");
@@ -2192,7 +2276,7 @@ iBool handleCommand_App(const char *cmd) {
2192 equal_Command(cmd, "prefs.mono.gopher.changed")) { 2276 equal_Command(cmd, "prefs.mono.gopher.changed")) {
2193 const iBool isSet = (arg_Command(cmd) != 0); 2277 const iBool isSet = (arg_Command(cmd) != 0);
2194 if (!isFrozen) { 2278 if (!isFrozen) {
2195 setFreezeDraw_Window(d->window, iTrue); 2279 setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
2196 } 2280 }
2197 if (startsWith_CStr(cmd, "prefs.mono.gemini")) { 2281 if (startsWith_CStr(cmd, "prefs.mono.gemini")) {
2198 d->prefs.monospaceGemini = isSet; 2282 d->prefs.monospaceGemini = isSet;
@@ -2313,10 +2397,12 @@ iBool handleCommand_App(const char *cmd) {
2313 setCStr_String(&d->prefs.httpProxy, suffixPtr_Command(cmd, "address")); 2397 setCStr_String(&d->prefs.httpProxy, suffixPtr_Command(cmd, "address"));
2314 return iTrue; 2398 return iTrue;
2315 } 2399 }
2400#if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
2316 else if (equal_Command(cmd, "downloads")) { 2401 else if (equal_Command(cmd, "downloads")) {
2317 setCStr_String(&d->prefs.downloadDir, suffixPtr_Command(cmd, "path")); 2402 setCStr_String(&d->prefs.downloadDir, suffixPtr_Command(cmd, "path"));
2318 return iTrue; 2403 return iTrue;
2319 } 2404 }
2405#endif
2320 else if (equal_Command(cmd, "downloads.open")) { 2406 else if (equal_Command(cmd, "downloads.open")) {
2321 postCommandf_App("open url:%s", cstrCollect_String(makeFileUrl_String(downloadDir_App()))); 2407 postCommandf_App("open url:%s", cstrCollect_String(makeFileUrl_String(downloadDir_App())));
2322 return iTrue; 2408 return iTrue;
@@ -2360,7 +2446,8 @@ iBool handleCommand_App(const char *cmd) {
2360 setUrl_UploadWidget(upload, url); 2446 setUrl_UploadWidget(upload, url);
2361 setResponseViewer_UploadWidget(upload, document_App()); 2447 setResponseViewer_UploadWidget(upload, document_App());
2362 addChild_Widget(get_Root()->widget, iClob(upload)); 2448 addChild_Widget(get_Root()->widget, iClob(upload));
2363 finalizeSheet_Mobile(as_Widget(upload)); 2449// finalizeSheet_Mobile(as_Widget(upload));
2450 setupSheetTransition_Mobile(as_Widget(upload), iTrue);
2364 postRefresh_App(); 2451 postRefresh_App();
2365 return iTrue; 2452 return iTrue;
2366 } 2453 }
@@ -2384,8 +2471,8 @@ iBool handleCommand_App(const char *cmd) {
2384 iRoot *root = get_Root(); 2471 iRoot *root = get_Root();
2385 iRoot *oldRoot = root; 2472 iRoot *oldRoot = root;
2386 if (newTab & otherRoot_OpenTabFlag) { 2473 if (newTab & otherRoot_OpenTabFlag) {
2387 root = otherRoot_Window(d->window, root); 2474 root = otherRoot_Window(as_Window(d->window), root);
2388 setKeyRoot_Window(d->window, root); 2475 setKeyRoot_Window(as_Window(d->window), root);
2389 setCurrent_Root(root); /* need to change for widget creation */ 2476 setCurrent_Root(root); /* need to change for widget creation */
2390 } 2477 }
2391 iDocumentWidget *doc = document_Command(cmd); 2478 iDocumentWidget *doc = document_Command(cmd);
@@ -2484,7 +2571,7 @@ iBool handleCommand_App(const char *cmd) {
2484 } 2571 }
2485 else if (equal_Command(cmd, "tabs.close")) { 2572 else if (equal_Command(cmd, "tabs.close")) {
2486 iWidget *tabs = findWidget_App("doctabs"); 2573 iWidget *tabs = findWidget_App("doctabs");
2487#if defined (iPlatformAppleMobile) 2574#if defined (iPlatformMobile)
2488 /* Can't close the last on mobile. */ 2575 /* Can't close the last on mobile. */
2489 if (tabCount_Widget(tabs) == 1 && numRoots_Window(get_Window()) == 1) { 2576 if (tabCount_Widget(tabs) == 1 && numRoots_Window(get_Window()) == 1) {
2490 postCommand_App("navigate.home"); 2577 postCommand_App("navigate.home");
@@ -2539,7 +2626,8 @@ iBool handleCommand_App(const char *cmd) {
2539 return iTrue; 2626 return iTrue;
2540 } 2627 }
2541 else if (equal_Command(cmd, "keyroot.next")) { 2628 else if (equal_Command(cmd, "keyroot.next")) {
2542 if (setKeyRoot_Window(d->window, otherRoot_Window(d->window, d->window->keyRoot))) { 2629 if (setKeyRoot_Window(as_Window(d->window),
2630 otherRoot_Window(as_Window(d->window), d->window->base.keyRoot))) {
2543 setFocus_Widget(NULL); 2631 setFocus_Widget(NULL);
2544 } 2632 }
2545 return iTrue; 2633 return iTrue;
@@ -2565,12 +2653,13 @@ iBool handleCommand_App(const char *cmd) {
2565 updatePrefsPinSplitButtons_(dlg, d->prefs.pinSplit); 2653 updatePrefsPinSplitButtons_(dlg, d->prefs.pinSplit);
2566 updateScrollSpeedButtons_(dlg, mouse_ScrollType, d->prefs.smoothScrollSpeed[mouse_ScrollType]); 2654 updateScrollSpeedButtons_(dlg, mouse_ScrollType, d->prefs.smoothScrollSpeed[mouse_ScrollType]);
2567 updateScrollSpeedButtons_(dlg, keyboard_ScrollType, d->prefs.smoothScrollSpeed[keyboard_ScrollType]); 2655 updateScrollSpeedButtons_(dlg, keyboard_ScrollType, d->prefs.smoothScrollSpeed[keyboard_ScrollType]);
2568 updateDropdownSelection_(findChild_Widget(dlg, "prefs.uilang"), cstr_String(&d->prefs.uiLanguage)); 2656 updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "prefs.uilang"), cstr_String(&d->prefs.uiLanguage));
2569 updateDropdownSelection_(findChild_Widget(dlg, "prefs.returnkey"), 2657 updateDropdownSelection_LabelWidget(
2570 format_CStr("returnkey.set arg:%d", d->prefs.returnKey)); 2658 findChild_Widget(dlg, "prefs.returnkey"),
2659 format_CStr("returnkey.set arg:%d", d->prefs.returnKey));
2571 setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize); 2660 setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize);
2572 setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), 2661 setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"),
2573 collectNewFormat_String("%g", uiScale_Window(d->window))); 2662 collectNewFormat_String("%g", uiScale_Window(as_Window(d->window))));
2574 setFlags_Widget(findChild_Widget(dlg, format_CStr("prefs.font.%d", d->prefs.font)), 2663 setFlags_Widget(findChild_Widget(dlg, format_CStr("prefs.font.%d", d->prefs.font)),
2575 selected_WidgetFlag, 2664 selected_WidgetFlag,
2576 iTrue); 2665 iTrue);
@@ -2607,6 +2696,7 @@ iBool handleCommand_App(const char *cmd) {
2607 setToggle_Widget(findChild_Widget(dlg, "prefs.collapsepreonload"), d->prefs.collapsePreOnLoad); 2696 setToggle_Widget(findChild_Widget(dlg, "prefs.collapsepreonload"), d->prefs.collapsePreOnLoad);
2608 updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.dark"), d->prefs.docThemeDark); 2697 updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.dark"), d->prefs.docThemeDark);
2609 updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.light"), d->prefs.docThemeLight); 2698 updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.light"), d->prefs.docThemeLight);
2699 updateImageStyleButton_(findChild_Widget(dlg, "prefs.imagestyle"), d->prefs.imageStyle);
2610 updateFontButton_(findChild_Widget(dlg, "prefs.font"), d->prefs.font); 2700 updateFontButton_(findChild_Widget(dlg, "prefs.font"), d->prefs.font);
2611 updateFontButton_(findChild_Widget(dlg, "prefs.headingfont"), d->prefs.headingFont); 2701 updateFontButton_(findChild_Widget(dlg, "prefs.headingfont"), d->prefs.headingFont);
2612 setFlags_Widget( 2702 setFlags_Widget(
@@ -2687,6 +2777,31 @@ iBool handleCommand_App(const char *cmd) {
2687 makeFeedSettings_Widget(findUrl_Bookmarks(d->bookmarks, url)); 2777 makeFeedSettings_Widget(findUrl_Bookmarks(d->bookmarks, url));
2688 return iTrue; 2778 return iTrue;
2689 } 2779 }
2780 else if (equal_Command(cmd, "bookmarks.addfolder")) {
2781 const int parentId = argLabel_Command(cmd, "parent");
2782 if (suffixPtr_Command(cmd, "value")) {
2783 uint32_t id = add_Bookmarks(d->bookmarks, NULL,
2784 collect_String(suffix_Command(cmd, "value")), NULL, 0);
2785 if (parentId) {
2786 get_Bookmarks(d->bookmarks, id)->parentId = parentId;
2787 }
2788 postCommand_App("bookmarks.changed");
2789 }
2790 else {
2791 iWidget *dlg = makeValueInput_Widget(
2792 get_Root()->widget, collectNewCStr_String(cstr_Lang("dlg.addfolder.defaulttitle")),
2793 uiHeading_ColorEscape "${heading.addfolder}", "${dlg.addfolder.prompt}",
2794 uiTextAction_ColorEscape "${dlg.addfolder}",
2795 format_CStr("bookmarks.addfolder parent:%d", parentId));
2796 setSelectAllOnFocus_InputWidget(findChild_Widget(dlg, "input"), iTrue);
2797 }
2798 return iTrue;
2799 }
2800 else if (equal_Command(cmd, "bookmarks.sort")) {
2801 sort_Bookmarks(d->bookmarks, arg_Command(cmd), cmpTitleAscending_Bookmark);
2802 postCommand_App("bookmarks.changed");
2803 return iTrue;
2804 }
2690 else if (equal_Command(cmd, "bookmarks.reload.remote")) { 2805 else if (equal_Command(cmd, "bookmarks.reload.remote")) {
2691 fetchRemote_Bookmarks(bookmarks_App()); 2806 fetchRemote_Bookmarks(bookmarks_App());
2692 return iTrue; 2807 return iTrue;
@@ -2715,7 +2830,7 @@ iBool handleCommand_App(const char *cmd) {
2715 else if (equal_Command(cmd, "feeds.update.finished")) { 2830 else if (equal_Command(cmd, "feeds.update.finished")) {
2716 showCollapsed_Widget(findWidget_Root("feeds.progress"), iFalse); 2831 showCollapsed_Widget(findWidget_Root("feeds.progress"), iFalse);
2717 refreshFinished_Feeds(); 2832 refreshFinished_Feeds();
2718 postRefresh_App(); 2833 refresh_Widget(findWidget_App("url"));
2719 return iFalse; 2834 return iFalse;
2720 } 2835 }
2721 else if (equal_Command(cmd, "visited.changed")) { 2836 else if (equal_Command(cmd, "visited.changed")) {
@@ -2725,6 +2840,9 @@ iBool handleCommand_App(const char *cmd) {
2725 else if (equal_Command(cmd, "document.changed")) { 2840 else if (equal_Command(cmd, "document.changed")) {
2726 /* Set of open tabs has changed. */ 2841 /* Set of open tabs has changed. */
2727 postCommand_App("document.openurls.changed"); 2842 postCommand_App("document.openurls.changed");
2843 if (deviceType_App() == phone_AppDeviceType) {
2844 showToolbar_Root(d->window->base.roots[0], iTrue);
2845 }
2728 return iFalse; 2846 return iFalse;
2729 } 2847 }
2730 else if (equal_Command(cmd, "ident.new")) { 2848 else if (equal_Command(cmd, "ident.new")) {
@@ -2737,7 +2855,9 @@ iBool handleCommand_App(const char *cmd) {
2737 iCertImportWidget *imp = new_CertImportWidget(); 2855 iCertImportWidget *imp = new_CertImportWidget();
2738 setPageContent_CertImportWidget(imp, sourceContent_DocumentWidget(document_App())); 2856 setPageContent_CertImportWidget(imp, sourceContent_DocumentWidget(document_App()));
2739 addChild_Widget(get_Root()->widget, iClob(imp)); 2857 addChild_Widget(get_Root()->widget, iClob(imp));
2740 finalizeSheet_Mobile(as_Widget(imp)); 2858// finalizeSheet_Mobile(as_Widget(imp));
2859 arrange_Widget(as_Widget(imp));
2860 setupSheetTransition_Mobile(as_Widget(imp), iTrue);
2741 postRefresh_App(); 2861 postRefresh_App();
2742 return iTrue; 2862 return iTrue;
2743 } 2863 }
@@ -2793,12 +2913,12 @@ iBool handleCommand_App(const char *cmd) {
2793 write_Ipc(argLabel_Command(cmd, "pid"), 2913 write_Ipc(argLabel_Command(cmd, "pid"),
2794 collectNewFormat_String("%s\n", cstr_String(url_DocumentWidget(document_App()))), 2914 collectNewFormat_String("%s\n", cstr_String(url_DocumentWidget(document_App()))),
2795 response_IpcWrite); 2915 response_IpcWrite);
2796 return iTrue; 2916 return iTrue;
2797 } 2917 }
2798 else if (equal_Command(cmd, "ipc.signal")) { 2918 else if (equal_Command(cmd, "ipc.signal")) {
2799 if (argLabel_Command(cmd, "raise")) { 2919 if (argLabel_Command(cmd, "raise")) {
2800 if (d->window && d->window->win) { 2920 if (d->window && d->window->base.win) {
2801 SDL_RaiseWindow(d->window->win); 2921 SDL_RaiseWindow(d->window->base.win);
2802 } 2922 }
2803 } 2923 }
2804 signal_Ipc(arg_Command(cmd)); 2924 signal_Ipc(arg_Command(cmd));
@@ -2906,3 +3026,7 @@ iStringSet *listOpenURLs_App(void) {
2906 iRelease(docs); 3026 iRelease(docs);
2907 return set; 3027 return set;
2908} 3028}
3029
3030iMainWindow *mainWindow_App(void) {
3031 return app_.window;
3032}
diff --git a/src/app.h b/src/app.h
index 55bec5a6..0dff939f 100644
--- a/src/app.h
+++ b/src/app.h
@@ -22,8 +22,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#pragma once 23#pragma once
24 24
25/* Application core: event loop, base event processing, audio synth. */
26
27#include <the_Foundation/objectlist.h> 25#include <the_Foundation/objectlist.h>
28#include <the_Foundation/string.h> 26#include <the_Foundation/string.h>
29#include <the_Foundation/stringset.h> 27#include <the_Foundation/stringset.h>
@@ -35,6 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
35iDeclareType(Bookmarks) 33iDeclareType(Bookmarks)
36iDeclareType(DocumentWidget) 34iDeclareType(DocumentWidget)
37iDeclareType(GmCerts) 35iDeclareType(GmCerts)
36iDeclareType(MainWindow)
38iDeclareType(MimeHooks) 37iDeclareType(MimeHooks)
39iDeclareType(Periodic) 38iDeclareType(Periodic)
40iDeclareType(Root) 39iDeclareType(Root)
@@ -44,6 +43,8 @@ iDeclareType(Window)
44/* Command line options strings. */ 43/* Command line options strings. */
45#define listTabUrls_CommandLineOption "list-tab-urls;L" 44#define listTabUrls_CommandLineOption "list-tab-urls;L"
46#define openUrlOrSearch_CommandLineOption "url-or-search;u" 45#define openUrlOrSearch_CommandLineOption "url-or-search;u"
46#define windowWidth_CommandLineOption "width;w"
47#define windowHeight_CommandLineOption "height;h"
47 48
48enum iAppDeviceType { 49enum iAppDeviceType {
49 desktop_AppDeviceType, 50 desktop_AppDeviceType,
@@ -59,14 +60,12 @@ enum iAppEventMode {
59enum iUserEventCode { 60enum iUserEventCode {
60 command_UserEventCode = 1, 61 command_UserEventCode = 1,
61 refresh_UserEventCode, 62 refresh_UserEventCode,
62 arrange_UserEventCode,
63 asleep_UserEventCode, 63 asleep_UserEventCode,
64 /* The start of a potential touch tap event is notified via a custom event because 64 /* The start of a potential touch tap event is notified via a custom event because
65 sending SDL_MOUSEBUTTONDOWN would be premature: we don't know how long the tap will 65 sending SDL_MOUSEBUTTONDOWN would be premature: we don't know how long the tap will
66 take, it could turn into a tap-and-hold for example. */ 66 take, it could turn into a tap-and-hold for example. */
67 widgetTapBegins_UserEventCode, 67 widgetTapBegins_UserEventCode,
68 widgetTouchEnds_UserEventCode, /* finger lifted, but momentum may continue */ 68 widgetTouchEnds_UserEventCode, /* finger lifted, but momentum may continue */
69 immediateRefresh_UserEventCode, /* refresh even though more events are pending */
70}; 69};
71 70
72const iString *execPath_App (void); 71const iString *execPath_App (void);
@@ -117,8 +116,9 @@ iAny * findWidget_App (const char *id);
117void addTicker_App (iTickerFunc ticker, iAny *context); 116void addTicker_App (iTickerFunc ticker, iAny *context);
118void addTickerRoot_App (iTickerFunc ticker, iRoot *root, iAny *context); 117void addTickerRoot_App (iTickerFunc ticker, iRoot *root, iAny *context);
119void removeTicker_App (iTickerFunc ticker, iAny *context); 118void removeTicker_App (iTickerFunc ticker, iAny *context);
119void addPopup_App (iWindow *popup);
120void removePopup_App (iWindow *popup);
120void postRefresh_App (void); 121void postRefresh_App (void);
121void postImmediateRefresh_App(void);
122void postCommand_Root (iRoot *, const char *command); 122void postCommand_Root (iRoot *, const char *command);
123void postCommandf_Root (iRoot *, const char *command, ...); 123void postCommandf_Root (iRoot *, const char *command, ...);
124void postCommandf_App (const char *command, ...); 124void postCommandf_App (const char *command, ...);
@@ -129,10 +129,12 @@ iLocalDef void postCommandString_Root(iRoot *d, const iString *command) {
129 } 129 }
130} 130}
131iLocalDef void postCommand_App(const char *command) { 131iLocalDef void postCommand_App(const char *command) {
132 postCommandf_App(command); 132 postCommand_Root(NULL, command);
133} 133}
134 134
135iDocumentWidget * document_Command (const char *cmd); 135iDocumentWidget * document_Command (const char *cmd);
136 136
137void openInDefaultBrowser_App (const iString *url); 137void openInDefaultBrowser_App (const iString *url);
138void revealPath_App (const iString *path); 138void revealPath_App (const iString *path);
139
140iMainWindow *mainWindow_App(void);
diff --git a/src/bookmarks.c b/src/bookmarks.c
index c27efbfe..fe2ca47a 100644
--- a/src/bookmarks.c
+++ b/src/bookmarks.c
@@ -31,13 +31,15 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
31#include <the_Foundation/path.h> 31#include <the_Foundation/path.h>
32#include <the_Foundation/regexp.h> 32#include <the_Foundation/regexp.h>
33#include <the_Foundation/stringset.h> 33#include <the_Foundation/stringset.h>
34#include <the_Foundation/toml.h>
34 35
35void init_Bookmark(iBookmark *d) { 36void init_Bookmark(iBookmark *d) {
36 init_String(&d->url); 37 init_String(&d->url);
37 init_String(&d->title); 38 init_String(&d->title);
38 init_String(&d->tags); 39 init_String(&d->tags);
39 iZap(d->when); 40 iZap(d->when);
40 d->sourceId = 0; 41 d->parentId = 0;
42 d->order = 0;
41} 43}
42 44
43void deinit_Bookmark(iBookmark *d) { 45void deinit_Bookmark(iBookmark *d) {
@@ -77,13 +79,18 @@ static int cmpTimeDescending_Bookmark_(const iBookmark **a, const iBookmark **b)
77 return iCmp(seconds_Time(&(*b)->when), seconds_Time(&(*a)->when)); 79 return iCmp(seconds_Time(&(*b)->when), seconds_Time(&(*a)->when));
78} 80}
79 81
80static int cmpTitleAscending_Bookmark_(const iBookmark **a, const iBookmark **b) { 82int cmpTitleAscending_Bookmark(const iBookmark **a, const iBookmark **b) {
81 return cmpStringCase_String(&(*a)->title, &(*b)->title); 83 return cmpStringCase_String(&(*a)->title, &(*b)->title);
82} 84}
83 85
86iBool filterInsideFolder_Bookmark(void *context, const iBookmark *bm) {
87 return hasParent_Bookmark(bm, id_Bookmark(context));
88}
89
84/*----------------------------------------------------------------------------------------------*/ 90/*----------------------------------------------------------------------------------------------*/
85 91
86static const char *fileName_Bookmarks_ = "bookmarks.txt"; 92static const char *oldFileName_Bookmarks_ = "bookmarks.txt";
93static const char *fileName_Bookmarks_ = "bookmarks.ini"; /* since v1.7 (TOML subset) */
87 94
88struct Impl_Bookmarks { 95struct Impl_Bookmarks {
89 iMutex * mtx; 96 iMutex * mtx;
@@ -123,16 +130,19 @@ void clear_Bookmarks(iBookmarks *d) {
123 unlock_Mutex(d->mtx); 130 unlock_Mutex(d->mtx);
124} 131}
125 132
133static void insertId_Bookmarks_(iBookmarks *d, iBookmark *bookmark, int id) {
134 bookmark->node.key = id;
135 insert_Hash(&d->bookmarks, &bookmark->node);
136}
137
126static void insert_Bookmarks_(iBookmarks *d, iBookmark *bookmark) { 138static void insert_Bookmarks_(iBookmarks *d, iBookmark *bookmark) {
127 lock_Mutex(d->mtx); 139 lock_Mutex(d->mtx);
128 bookmark->node.key = ++d->idEnum; 140 insertId_Bookmarks_(d, bookmark, ++d->idEnum);
129 insert_Hash(&d->bookmarks, &bookmark->node);
130 unlock_Mutex(d->mtx); 141 unlock_Mutex(d->mtx);
131} 142}
132 143
133void load_Bookmarks(iBookmarks *d, const char *dirPath) { 144static void loadOldFormat_Bookmarks(iBookmarks *d, const char *dirPath) {
134 clear_Bookmarks(d); 145 iFile *f = newCStr_File(concatPath_CStr(dirPath, oldFileName_Bookmarks_));
135 iFile *f = newCStr_File(concatPath_CStr(dirPath, fileName_Bookmarks_));
136 if (open_File(f, readOnly_FileMode | text_FileMode)) { 146 if (open_File(f, readOnly_FileMode | text_FileMode)) {
137 const iRangecc src = range_Block(collect_Block(readAll_File(f))); 147 const iRangecc src = range_Block(collect_Block(readAll_File(f)));
138 iRangecc line = iNullRange; 148 iRangecc line = iNullRange;
@@ -170,6 +180,111 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) {
170 iRelease(f); 180 iRelease(f);
171} 181}
172 182
183/*----------------------------------------------------------------------------------------------*/
184
185iDeclareType(BookmarkLoader)
186
187struct Impl_BookmarkLoader {
188 iTomlParser *toml;
189 iBookmarks * bookmarks;
190 iBookmark * bm;
191};
192
193static void handleTable_BookmarkLoader_(void *context, const iString *table, iBool isStart) {
194 iBookmarkLoader *d = context;
195 if (isStart) {
196 iAssert(!d->bm);
197 d->bm = new_Bookmark();
198 const int id = toInt_String(table);
199 d->bookmarks->idEnum = iMax(d->bookmarks->idEnum, id);
200 insertId_Bookmarks_(d->bookmarks, d->bm, id);
201 }
202 else {
203 d->bm = NULL;
204 }
205}
206
207static void handleKeyValue_BookmarkLoader_(void *context, const iString *table, const iString *key,
208 const iTomlValue *tv) {
209 iBookmarkLoader *d = context;
210 iBookmark *bm = d->bm;
211 if (bm) {
212 iUnused(table); /* it's the current one */
213 if (!cmp_String(key, "url") && tv->type == string_TomlType) {
214 set_String(&bm->url, tv->value.string);
215 }
216 else if (!cmp_String(key, "title") && tv->type == string_TomlType) {
217 set_String(&bm->title, tv->value.string);
218 }
219 else if (!cmp_String(key, "tags") && tv->type == string_TomlType) {
220 set_String(&bm->tags, tv->value.string);
221 }
222 else if (!cmp_String(key, "icon") && tv->type == int64_TomlType) {
223 bm->icon = (iChar) tv->value.int64;
224 }
225 else if (!cmp_String(key, "created") && tv->type == int64_TomlType) {
226 initSeconds_Time(&bm->when, tv->value.int64);
227 }
228 else if (!cmp_String(key, "parent") && tv->type == int64_TomlType) {
229 bm->parentId = tv->value.int64;
230 }
231 else if (!cmp_String(key, "order") && tv->type == int64_TomlType) {
232 bm->order = tv->value.int64;
233 }
234 }
235}
236
237static void init_BookmarkLoader(iBookmarkLoader *d, iBookmarks *bookmarks) {
238 d->toml = new_TomlParser();
239 setHandlers_TomlParser(d->toml, handleTable_BookmarkLoader_, handleKeyValue_BookmarkLoader_, d);
240 d->bookmarks = bookmarks;
241 d->bm = NULL;
242}
243
244static void deinit_BookmarkLoader(iBookmarkLoader *d) {
245 delete_TomlParser(d->toml);
246}
247
248static void load_BookmarkLoader(iBookmarkLoader *d, iFile *file) {
249 if (!parse_TomlParser(d->toml, collect_String(readString_File(file)))) {
250 fprintf(stderr, "[Bookmarks] syntax error(s) in %s\n", cstr_String(path_File(file)));
251 }
252}
253
254iDefineTypeConstructionArgs(BookmarkLoader, (iBookmarks *b), b)
255
256/*----------------------------------------------------------------------------------------------*/
257
258static iBool isMatchingParent_Bookmark_(void *context, const iBookmark *bm) {
259 return bm->parentId == *(const uint32_t *) context;
260}
261
262void sort_Bookmarks(iBookmarks *d, uint32_t parentId, iBookmarksCompareFunc cmp) {
263 lock_Mutex(d->mtx);
264 iConstForEach(PtrArray, i, list_Bookmarks(d, cmp, isMatchingParent_Bookmark_, &parentId)) {
265 iBookmark *bm = i.ptr;
266 bm->order = index_PtrArrayConstIterator(&i) + 1;
267 }
268 unlock_Mutex(d->mtx);
269}
270
271void load_Bookmarks(iBookmarks *d, const char *dirPath) {
272 clear_Bookmarks(d);
273 /* Load new .ini bookmarks, if present. */
274 iFile *f = iClob(newCStr_File(concatPath_CStr(dirPath, fileName_Bookmarks_)));
275 if (!open_File(f, readOnly_FileMode | text_FileMode)) {
276 /* As a fallback, try loading the v1.6 bookmarks file. */
277 loadOldFormat_Bookmarks(d, dirPath);
278 /* Old format has an implicit alphabetic sort order. */
279 sort_Bookmarks(d, 0, cmpTitleAscending_Bookmark);
280 return;
281 }
282 iBookmarkLoader loader;
283 init_BookmarkLoader(&loader, d);
284 load_BookmarkLoader(&loader, f);
285 deinit_BookmarkLoader(&loader);
286}
287
173void save_Bookmarks(const iBookmarks *d, const char *dirPath) { 288void save_Bookmarks(const iBookmarks *d, const char *dirPath) {
174 lock_Mutex(d->mtx); 289 lock_Mutex(d->mtx);
175 iRegExp *remotePattern = iClob(new_RegExp("\\bremote\\b", caseSensitive_RegExpOption)); 290 iRegExp *remotePattern = iClob(new_RegExp("\\bremote\\b", caseSensitive_RegExpOption));
@@ -185,12 +300,26 @@ void save_Bookmarks(const iBookmarks *d, const char *dirPath) {
185 continue; 300 continue;
186 } 301 }
187 format_String(str, 302 format_String(str,
188 "%08x %.0lf %s\n%s\n%s\n", 303 "[%d]\n"
304 "url = \"%s\"\n"
305 "title = \"%s\"\n"
306 "tags = \"%s\"\n"
307 "icon = 0x%x\n"
308 "created = %.0f # %s\n",
309 id_Bookmark(bm),
310 cstrCollect_String(quote_String(&bm->url, iFalse)),
311 cstrCollect_String(quote_String(&bm->title, iFalse)),
312 cstrCollect_String(quote_String(&bm->tags, iFalse)),
189 bm->icon, 313 bm->icon,
190 seconds_Time(&bm->when), 314 seconds_Time(&bm->when),
191 cstr_String(&bm->url), 315 cstrCollect_String(format_Time(&bm->when, "%Y-%m-%d")));
192 cstr_String(&bm->title), 316 if (bm->parentId) {
193 cstr_String(&bm->tags)); 317 appendFormat_String(str, "parent = %d\n", bm->parentId);
318 }
319 if (bm->order) {
320 appendFormat_String(str, "order = %d\n", bm->order);
321 }
322 appendCStr_String(str, "\n");
194 writeData_File(f, cstr_String(str), size_String(str)); 323 writeData_File(f, cstr_String(str), size_String(str));
195 } 324 }
196 } 325 }
@@ -202,7 +331,9 @@ uint32_t add_Bookmarks(iBookmarks *d, const iString *url, const iString *title,
202 iChar icon) { 331 iChar icon) {
203 lock_Mutex(d->mtx); 332 lock_Mutex(d->mtx);
204 iBookmark *bm = new_Bookmark(); 333 iBookmark *bm = new_Bookmark();
205 set_String(&bm->url, canonicalUrl_String(url)); 334 if (url) {
335 set_String(&bm->url, canonicalUrl_String(url));
336 }
206 set_String(&bm->title, title); 337 set_String(&bm->title, title);
207 if (tags) { 338 if (tags) {
208 set_String(&bm->tags, tags); 339 set_String(&bm->tags, tags);
@@ -218,16 +349,9 @@ iBool remove_Bookmarks(iBookmarks *d, uint32_t id) {
218 lock_Mutex(d->mtx); 349 lock_Mutex(d->mtx);
219 iBookmark *bm = (iBookmark *) remove_Hash(&d->bookmarks, id); 350 iBookmark *bm = (iBookmark *) remove_Hash(&d->bookmarks, id);
220 if (bm) { 351 if (bm) {
221 /* If this is a remote source, make sure all the remote bookmarks are 352 /* Remove all the contained bookmarks as well. */
222 removed as well. */ 353 iConstForEach(PtrArray, i, list_Bookmarks(d, NULL, filterInsideFolder_Bookmark, bm)) {
223 if (hasTag_Bookmark(bm, remoteSource_BookmarkTag)) { 354 delete_Bookmark((iBookmark *) remove_Hash(&d->bookmarks, id_Bookmark(i.ptr)));
224 iForEach(Hash, i, &d->bookmarks) {
225 iBookmark *j = (iBookmark *) i.value;
226 if (j->sourceId == id_Bookmark(bm)) {
227 remove_HashIterator(&i);
228 delete_Bookmark(j);
229 }
230 }
231 } 355 }
232 delete_Bookmark(bm); 356 delete_Bookmark(bm);
233 } 357 }
@@ -287,6 +411,20 @@ iBookmark *get_Bookmarks(iBookmarks *d, uint32_t id) {
287 return (iBookmark *) value_Hash(&d->bookmarks, id); 411 return (iBookmark *) value_Hash(&d->bookmarks, id);
288} 412}
289 413
414void reorder_Bookmarks(iBookmarks *d, uint32_t id, int newOrder) {
415 lock_Mutex(d->mtx);
416 iForEach(Hash, i, &d->bookmarks) {
417 iBookmark *bm = (iBookmark *) i.value;
418 if (id_Bookmark(bm) == id) {
419 bm->order = newOrder;
420 }
421 else if (bm->order >= newOrder) {
422 bm->order++;
423 }
424 }
425 unlock_Mutex(d->mtx);
426}
427
290iBool filterTagsRegExp_Bookmarks(void *regExp, const iBookmark *bm) { 428iBool filterTagsRegExp_Bookmarks(void *regExp, const iBookmark *bm) {
291 iRegExpMatch m; 429 iRegExpMatch m;
292 init_RegExpMatch(&m); 430 init_RegExpMatch(&m);
@@ -321,6 +459,16 @@ const iPtrArray *list_Bookmarks(const iBookmarks *d, iBookmarksCompareFunc cmp,
321 return list; 459 return list;
322} 460}
323 461
462size_t count_Bookmarks(const iBookmarks *d) {
463 size_t n = 0;
464 iConstForEach(Hash, i, &d->bookmarks) {
465 if (!isFolder_Bookmark((const iBookmark *) i.value)) {
466 n++;
467 }
468 }
469 return n;
470}
471
324const iString *bookmarkListPage_Bookmarks(const iBookmarks *d, enum iBookmarkListType listType) { 472const iString *bookmarkListPage_Bookmarks(const iBookmarks *d, enum iBookmarkListType listType) {
325 iString *str = collectNew_String(); 473 iString *str = collectNew_String();
326 lock_Mutex(d->mtx); 474 lock_Mutex(d->mtx);
@@ -333,21 +481,37 @@ const iString *bookmarkListPage_Bookmarks(const iBookmarks *d, enum iBookmarkLis
333 appendFormat_String(str, 481 appendFormat_String(str,
334 "%s\n\n" 482 "%s\n\n"
335 "${bookmark.export.saving}\n\n", 483 "${bookmark.export.saving}\n\n",
336 formatCStrs_Lang("bookmark.export.count.n", size_Hash(&d->bookmarks))); 484 formatCStrs_Lang("bookmark.export.count.n", count_Bookmarks(d)));
337 } 485 }
338 else if (listType == listByTag_BookmarkListType) { 486 else if (listType == listByTag_BookmarkListType) {
339 appendFormat_String(str, "${bookmark.export.taginfo}\n\n"); 487 appendFormat_String(str, "${bookmark.export.taginfo}\n\n");
340 } 488 }
341 iStringSet *tags = new_StringSet(); 489 iStringSet *tags = new_StringSet();
342 const iPtrArray *bmList = list_Bookmarks(d, 490 const iPtrArray *bmList =
343 listType == listByCreationTime_BookmarkListType 491 list_Bookmarks(d,
344 ? cmpTimeDescending_Bookmark_ 492 listType == listByCreationTime_BookmarkListType ? cmpTimeDescending_Bookmark_
345 : cmpTitleAscending_Bookmark_, 493 : listType == listByTag_BookmarkListType ? cmpTitleAscending_Bookmark
346 NULL, 494 : cmpTree_Bookmark,
347 NULL); 495 NULL, NULL);
496 if (listType == listByFolder_BookmarkListType) {
497 iConstForEach(PtrArray, i, bmList) {
498 const iBookmark *bm = i.ptr;
499 if (!isFolder_Bookmark(bm) && !bm->parentId) {
500 appendFormat_String(str, "=> %s %s\n", cstr_String(&bm->url), cstr_String(&bm->title));
501 }
502 }
503 }
348 iConstForEach(PtrArray, i, bmList) { 504 iConstForEach(PtrArray, i, bmList) {
349 const iBookmark *bm = i.ptr; 505 const iBookmark *bm = i.ptr;
350 if (listType == listByFolder_BookmarkListType) { 506 if (isFolder_Bookmark(bm)) {
507 if (listType == listByFolder_BookmarkListType) {
508 const int depth = depth_Bookmark(bm);
509 appendFormat_String(str, "\n%s %s\n",
510 depth == 0 ? "##" : "###", cstr_String(&bm->title));
511 }
512 continue;
513 }
514 if (listType == listByFolder_BookmarkListType && bm->parentId) {
351 appendFormat_String(str, "=> %s %s\n", cstr_String(&bm->url), cstr_String(&bm->title)); 515 appendFormat_String(str, "=> %s %s\n", cstr_String(&bm->url), cstr_String(&bm->title));
352 } 516 }
353 else if (listType == listByCreationTime_BookmarkListType) { 517 else if (listType == listByCreationTime_BookmarkListType) {
@@ -452,7 +616,7 @@ void requestFinished_Bookmarks(iBookmarks *d, iGmRequest *req) {
452 } 616 }
453 const uint32_t bmId = add_Bookmarks(d, absUrl, titleStr, remoteTag, 0x2913); 617 const uint32_t bmId = add_Bookmarks(d, absUrl, titleStr, remoteTag, 0x2913);
454 iBookmark *bm = get_Bookmarks(d, bmId); 618 iBookmark *bm = get_Bookmarks(d, bmId);
455 bm->sourceId = *(uint32_t *) userData_Object(req); 619 bm->parentId = *(uint32_t *) userData_Object(req);
456 delete_String(titleStr); 620 delete_String(titleStr);
457 } 621 }
458 delete_String(urlStr); 622 delete_String(urlStr);
diff --git a/src/bookmarks.h b/src/bookmarks.h
index 353b4197..13501ded 100644
--- a/src/bookmarks.h
+++ b/src/bookmarks.h
@@ -32,6 +32,8 @@ iDeclareType(GmRequest)
32iDeclareType(Bookmark) 32iDeclareType(Bookmark)
33iDeclareTypeConstruction(Bookmark) 33iDeclareTypeConstruction(Bookmark)
34 34
35/* TODO: Make the special internal tags a bitfield, separate from user's tags. */
36
35#define headings_BookmarkTag "headings" 37#define headings_BookmarkTag "headings"
36#define homepage_BookmarkTag "homepage" 38#define homepage_BookmarkTag "homepage"
37#define linkSplit_BookmarkTag "linksplit" 39#define linkSplit_BookmarkTag "linksplit"
@@ -47,11 +49,15 @@ struct Impl_Bookmark {
47 iString tags; 49 iString tags;
48 iChar icon; 50 iChar icon;
49 iTime when; 51 iTime when;
50 uint32_t sourceId; /* remote */ 52 uint32_t parentId; /* remote source or folder */
53 int order; /* sort order */
51}; 54};
52 55
53iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; } 56iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; }
57iLocalDef iBool isFolder_Bookmark (const iBookmark *d) { return isEmpty_String(&d->url); }
54 58
59iBool hasParent_Bookmark (const iBookmark *, uint32_t parentId);
60int depth_Bookmark (const iBookmark *);
55iBool hasTag_Bookmark (const iBookmark *, const char *tag); 61iBool hasTag_Bookmark (const iBookmark *, const char *tag);
56void addTag_Bookmark (iBookmark *, const char *tag); 62void addTag_Bookmark (iBookmark *, const char *tag);
57void removeTag_Bookmark (iBookmark *, const char *tag); 63void removeTag_Bookmark (iBookmark *, const char *tag);
@@ -70,28 +76,36 @@ iLocalDef void addOrRemoveTag_Bookmark(iBookmark *d, const char *tag, iBool add)
70 } 76 }
71} 77}
72 78
79int cmpTitleAscending_Bookmark (const iBookmark **, const iBookmark **);
80int cmpTree_Bookmark (const iBookmark **, const iBookmark **);
81
82iBool filterInsideFolder_Bookmark (void *parentFolder, const iBookmark *);
83
73/*----------------------------------------------------------------------------------------------*/ 84/*----------------------------------------------------------------------------------------------*/
74 85
75iDeclareType(Bookmarks) 86iDeclareType(Bookmarks)
76iDeclareTypeConstruction(Bookmarks) 87iDeclareTypeConstruction(Bookmarks)
77 88
89typedef iBool (*iBookmarksFilterFunc) (void *context, const iBookmark *);
90typedef int (*iBookmarksCompareFunc) (const iBookmark **, const iBookmark **);
91
78void clear_Bookmarks (iBookmarks *); 92void clear_Bookmarks (iBookmarks *);
79void load_Bookmarks (iBookmarks *, const char *dirPath); 93void load_Bookmarks (iBookmarks *, const char *dirPath);
94void save_Bookmarks (const iBookmarks *, const char *dirPath);
95
80uint32_t add_Bookmarks (iBookmarks *, const iString *url, const iString *title, 96uint32_t add_Bookmarks (iBookmarks *, const iString *url, const iString *title,
81 const iString *tags, iChar icon); 97 const iString *tags, iChar icon);
82iBool remove_Bookmarks (iBookmarks *, uint32_t id); 98iBool remove_Bookmarks (iBookmarks *, uint32_t id);
83iBookmark * get_Bookmarks (iBookmarks *, uint32_t id); 99iBookmark * get_Bookmarks (iBookmarks *, uint32_t id);
100void reorder_Bookmarks (iBookmarks *, uint32_t id, int newOrder);
101iBool updateBookmarkIcon_Bookmarks(iBookmarks *, const iString *url, iChar icon);
102void sort_Bookmarks (iBookmarks *, uint32_t parentId, iBookmarksCompareFunc cmp);
84void fetchRemote_Bookmarks (iBookmarks *); 103void fetchRemote_Bookmarks (iBookmarks *);
85void requestFinished_Bookmarks (iBookmarks *, iGmRequest *req); 104void requestFinished_Bookmarks (iBookmarks *, iGmRequest *req);
86iBool updateBookmarkIcon_Bookmarks(iBookmarks *, const iString *url, iChar icon);
87iChar siteIcon_Bookmarks (const iBookmarks *, const iString *url);
88 105
89void save_Bookmarks (const iBookmarks *, const char *dirPath); 106iChar siteIcon_Bookmarks (const iBookmarks *, const iString *url);
90uint32_t findUrl_Bookmarks (const iBookmarks *, const iString *url); /* O(n) */ 107uint32_t findUrl_Bookmarks (const iBookmarks *, const iString *url); /* O(n) */
91 108
92typedef iBool (*iBookmarksFilterFunc) (void *context, const iBookmark *);
93typedef int (*iBookmarksCompareFunc)(const iBookmark **, const iBookmark **);
94
95iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *); 109iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *);
96 110
97/** 111/**
diff --git a/src/defs.h b/src/defs.h
index cf04514e..e1c0a125 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -36,9 +36,18 @@ enum iFileVersion {
36 multipleRoots_FileVersion = 2, 36 multipleRoots_FileVersion = 2,
37 serializedSidebarState_FileVersion = 3, 37 serializedSidebarState_FileVersion = 3,
38 addedRecentUrlFlags_FileVersion = 4, 38 addedRecentUrlFlags_FileVersion = 4,
39 bookmarkFolderState_FileVersion = 5,
39 /* meta */ 40 /* meta */
40 idents_FileVersion = 1, /* version used by GmCerts/idents.lgr */ 41 idents_FileVersion = 1, /* version used by GmCerts/idents.lgr */
41 latest_FileVersion = 4, 42 latest_FileVersion = 5,
43};
44
45enum iImageStyle {
46 original_ImageStyle = 0,
47 grayscale_ImageStyle = 1,
48 bgFg_ImageStyle = 2,
49 textColorized_ImageStyle = 3,
50 preformatColorized_ImageStyle = 4,
42}; 51};
43 52
44enum iScrollType { 53enum iScrollType {
@@ -99,11 +108,13 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
99#define rightArrow_Icon "\u279e" 108#define rightArrow_Icon "\u279e"
100#define barLeftArrow_Icon "\u21a4" 109#define barLeftArrow_Icon "\u21a4"
101#define barRightArrow_Icon "\u21a6" 110#define barRightArrow_Icon "\u21a6"
111#define upDownArrow_Icon "\u21c5"
102#define clock_Icon "\U0001f553" 112#define clock_Icon "\U0001f553"
103#define pin_Icon "\U0001f588" 113#define pin_Icon "\U0001f588"
104#define star_Icon "\u2605" 114#define star_Icon "\u2605"
105#define whiteStar_Icon "\u2606" 115#define whiteStar_Icon "\u2606"
106#define person_Icon "\U0001f464" 116#define person_Icon "\U0001f464"
117#define key_Icon "\U0001f511"
107#define download_Icon "\u2ba7" 118#define download_Icon "\u2ba7"
108#define upload_Icon "\u2ba5" 119#define upload_Icon "\u2ba5"
109#define export_Icon "\U0001f4e4" 120#define export_Icon "\U0001f4e4"
@@ -140,9 +151,14 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
140#define clipboard_Icon "\U0001f4cb" 151#define clipboard_Icon "\U0001f4cb"
141#define unhappy_Icon "\U0001f641" 152#define unhappy_Icon "\U0001f641"
142#define globe_Icon "\U0001f310" 153#define globe_Icon "\U0001f310"
154#define envelope_Icon "\U0001f4e7"
143#define magnifyingGlass_Icon "\U0001f50d" 155#define magnifyingGlass_Icon "\U0001f50d"
144#define midEllipsis_Icon "\u00b7\u00b7\u00b7" 156#define midEllipsis_Icon "\u00b7\u00b7\u00b7"
145#define return_Icon "\u23ce" 157#define return_Icon "\u23ce"
158#define undo_Icon "\u23ea"
159#define select_Icon "\u2b1a"
160#define downAngle_Icon "\ufe40"
161#define photo_Icon "\U0001f5bb"
146 162
147#if defined (iPlatformApple) 163#if defined (iPlatformApple)
148# define shift_Icon "\u21e7" 164# define shift_Icon "\u21e7"
@@ -153,7 +169,10 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
153#endif 169#endif
154 170
155#if defined (iPlatformAppleDesktop) 171#if defined (iPlatformAppleDesktop)
156# define iHaveNativeMenus 172# define iHaveNativeMenus /* main menu */
173# if defined (LAGRANGE_ENABLE_MAC_MENUS)
174# define iHaveNativeContextMenus
175# endif
157#endif 176#endif
158 177
159/* UI labels that depend on the platform */ 178/* UI labels that depend on the platform */
diff --git a/src/feeds.c b/src/feeds.c
index 91ef8c2c..9770ca0a 100644
--- a/src/feeds.c
+++ b/src/feeds.c
@@ -50,6 +50,7 @@ void init_FeedEntry(iFeedEntry *d) {
50 init_String(&d->url); 50 init_String(&d->url);
51 init_String(&d->title); 51 init_String(&d->title);
52 d->bookmarkId = 0; 52 d->bookmarkId = 0;
53 d->isHeading = iFalse;
53} 54}
54 55
55void deinit_FeedEntry(iFeedEntry *d) { 56void deinit_FeedEntry(iFeedEntry *d) {
@@ -234,6 +235,7 @@ static void parseResult_FeedJob_(iFeedJob *d) {
234 } 235 }
235 trimStart_Rangecc(&line); 236 trimStart_Rangecc(&line);
236 iFeedEntry *entry = new_FeedEntry(); 237 iFeedEntry *entry = new_FeedEntry();
238 entry->isHeading = iTrue;
237 entry->posted = now; 239 entry->posted = now;
238 if (!d->isFirstUpdate) { 240 if (!d->isFirstUpdate) {
239 entry->discovered = now; 241 entry->discovered = now;
@@ -275,7 +277,8 @@ static void save_Feeds_(iFeeds *d) {
275 initCurrent_Time(&now); 277 initCurrent_Time(&now);
276 iConstForEach(Array, i, &d->entries.values) { 278 iConstForEach(Array, i, &d->entries.values) {
277 const iFeedEntry *entry = *(const iFeedEntry **) i.value; 279 const iFeedEntry *entry = *(const iFeedEntry **) i.value;
278 if (isValid_Time(&entry->discovered) && 280 /* Heading entries are kept as long as they are present in the source. */
281 if (!entry->isHeading && isValid_Time(&entry->discovered) &&
279 secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { 282 secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) {
280 continue; /* Forget entries discovered long ago. */ 283 continue; /* Forget entries discovered long ago. */
281 } 284 }
@@ -298,25 +301,78 @@ static iBool isHeadingEntry_FeedEntry_(const iFeedEntry *d) {
298 return contains_String(&d->url, '#'); 301 return contains_String(&d->url, '#');
299} 302}
300 303
301static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) { 304static iStringSet *listHeadingEntriesFrom_Feeds_(const iFeeds *d, uint32_t sourceId) {
305 iStringSet *set = new_StringSet();
306 iConstForEach(Array, i, &d->entries.values) {
307 const iFeedEntry *entry = *(const iFeedEntry **) i.value;
308 if (entry->bookmarkId == sourceId) {
309 insert_StringSet(set, &entry->url);
310 }
311 }
312 return set;
313}
314
315static iBool updateEntries_Feeds_(iFeeds *d, iBool isHeadings, uint32_t sourceId,
316 iPtrArray *incoming) {
317 /* Entries are removed from `incoming` if they are added to the Feeds entries array.
318 Anything remaining in `incoming` will be deleted afterwards. */
302 iBool gotNew = iFalse; 319 iBool gotNew = iFalse;
303 lock_Mutex(d->mtx); 320 lock_Mutex(d->mtx);
304 iTime now; 321 iTime now;
305 initCurrent_Time(&now); 322 initCurrent_Time(&now);
306 iForEach(PtrArray, i, incoming) { 323 if (isHeadings) {
307 iFeedEntry *entry = i.ptr; 324// printf("Updating sourceID %d...\n", sourceId);
308 /* Disregard old entries. */ 325 iStringSet *known = listHeadingEntriesFrom_Feeds_(d, sourceId);
309 if (secondsSince_Time(&now, &entry->posted) >= maxAge_Visited) { 326// puts(" Known URLs:");
310 /* We don't remember this far back, so the unread status of the entry would 327// iConstForEach(StringSet, ss, known) {
311 be incorrect. */ 328// printf(" {%s}\n", cstr_String(ss.value));
312 continue; 329// }
330 iStringSet *presentInSource = new_StringSet();
331 /* Look for unknown entries. */
332 iForEach(PtrArray, i, incoming) {
333 iFeedEntry *entry = i.ptr;
334 insert_StringSet(presentInSource, &entry->url);
335 if (!contains_StringSet(known, &entry->url)) {
336// printf(" {%s} is new\n", cstr_String(&entry->url));
337 insert_SortedArray(&d->entries, &entry);
338 gotNew = iTrue;
339 remove_PtrArrayIterator(&i);
340 }
341 }
342// puts(" URLs present in source:");
343// iConstForEach(StringSet, ps, presentInSource) {
344// printf(" {%s}\n", cstr_String(ps.value));
345// }
346// puts(" URLs to purge:");
347 /* All known entries that are no longer present in source must be deleted. */
348 iForEach(Array, e, &d->entries.values) {
349 iFeedEntry *entry = *(iFeedEntry **) e.value;
350 if (entry->bookmarkId == sourceId &&
351 !contains_StringSet(presentInSource, &entry->url)) {
352// printf(" {%s}\n", cstr_String(&entry->url));
353 delete_FeedEntry(entry);
354 remove_ArrayIterator(&e);
355 }
313 } 356 }
314 size_t pos; 357// puts("Done.");
315 if (locate_SortedArray(&d->entries, &entry, &pos)) { 358 iRelease(presentInSource);
316 iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos); 359 iRelease(known);
317 iAssert(isHeadingEntry_FeedEntry_(existing) == isHeadingEntry_FeedEntry_(entry)); 360 }
318 /* Already known, but update it, maybe the time and label have changed. */ 361 else {
319 if (!isHeadingEntry_FeedEntry_(existing)) { 362 iForEach(PtrArray, i, incoming) {
363 iFeedEntry *entry = i.ptr;
364 /* Disregard old incoming entries. */
365 if (secondsSince_Time(&now, &entry->posted) >= maxAge_Visited) {
366 /* We don't remember this far back, so the unread status of the entry would
367 be incorrect. */
368 continue;
369 }
370 size_t pos;
371 if (locate_SortedArray(&d->entries, &entry, &pos)) {
372 iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos);
373 iAssert(!isHeadingEntry_FeedEntry_(existing));
374 iAssert(!isHeadingEntry_FeedEntry_(entry));
375 /* Already known, but update it, maybe the time and label have changed. */
320 iBool changed = iFalse; 376 iBool changed = iFalse;
321 iDate newDate; 377 iDate newDate;
322 iDate oldDate; 378 iDate oldDate;
@@ -336,12 +392,12 @@ static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) {
336 gotNew = iTrue; 392 gotNew = iTrue;
337 } 393 }
338 } 394 }
395 else {
396 insert_SortedArray(&d->entries, &entry);
397 gotNew = iTrue;
398 }
399 remove_PtrArrayIterator(&i);
339 } 400 }
340 else {
341 insert_SortedArray(&d->entries, &entry);
342 gotNew = iTrue;
343 }
344 remove_PtrArrayIterator(&i);
345 } 401 }
346 unlock_Mutex(d->mtx); 402 unlock_Mutex(d->mtx);
347 return gotNew; 403 return gotNew;
@@ -369,7 +425,8 @@ static iThreadResult fetch_Feeds_(iThread *thread) {
369 if (isFinished_GmRequest(work[i]->request)) { 425 if (isFinished_GmRequest(work[i]->request)) {
370 /* TODO: Handle redirects. Need to resubmit the job with new URL. */ 426 /* TODO: Handle redirects. Need to resubmit the job with new URL. */
371 parseResult_FeedJob_(work[i]); 427 parseResult_FeedJob_(work[i]);
372 gotNew |= updateEntries_Feeds_(d, &work[i]->results); 428 gotNew |= updateEntries_Feeds_(
429 d, work[i]->checkHeadings, work[i]->bookmarkId, &work[i]->results);
373 delete_FeedJob(work[i]); 430 delete_FeedJob(work[i]);
374 work[i] = NULL; 431 work[i] = NULL;
375 } 432 }
@@ -407,7 +464,7 @@ static iBool startWorker_Feeds_(iFeeds *d) {
407 if (!contains_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm))) { 464 if (!contains_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm))) {
408 job->isFirstUpdate = iTrue; 465 job->isFirstUpdate = iTrue;
409// printf("first check of %x: %s\n", id_Bookmark(bm), cstr_String(&bm->title)); 466// printf("first check of %x: %s\n", id_Bookmark(bm), cstr_String(&bm->title));
410 fflush(stdout); 467// fflush(stdout);
411 insert_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm)); 468 insert_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm));
412 } 469 }
413 pushBack_PtrArray(&d->jobs, job); 470 pushBack_PtrArray(&d->jobs, job);
@@ -539,6 +596,11 @@ static void load_Feeds_(iFeeds *d) {
539 stripDefaultUrlPort_String(&entry->url); 596 stripDefaultUrlPort_String(&entry->url);
540 set_String(&entry->url, canonicalUrl_String(&entry->url)); 597 set_String(&entry->url, canonicalUrl_String(&entry->url));
541 set_String(&entry->title, title); 598 set_String(&entry->title, title);
599 entry->isHeading = isHeadingEntry_FeedEntry_(entry);
600 if (entry->isHeading) {
601 printf("[Feeds] src:%d url:{%s}\n", entry->bookmarkId,
602 cstr_String(&entry->url));
603 }
542 insert_SortedArray(&d->entries, &entry); 604 insert_SortedArray(&d->entries, &entry);
543 } 605 }
544 delete_String(title); 606 delete_String(title);
@@ -642,7 +704,8 @@ size_t numUnread_Feeds(void) {
642 size_t max = 100; /* match the number of items shown in the sidebar */ 704 size_t max = 100; /* match the number of items shown in the sidebar */
643 iConstForEach(PtrArray, i, listEntries_Feeds()) { 705 iConstForEach(PtrArray, i, listEntries_Feeds()) {
644 if (!max--) break; 706 if (!max--) break;
645 if (isUnread_FeedEntry(i.ptr)) { 707 const iFeedEntry *entry = i.ptr;
708 if (isValid_Time(&entry->discovered) && isUnread_FeedEntry(i.ptr)) {
646 count++; 709 count++;
647 } 710 }
648 } 711 }
diff --git a/src/feeds.h b/src/feeds.h
index 5ff2adfb..8863d24f 100644
--- a/src/feeds.h
+++ b/src/feeds.h
@@ -34,6 +34,7 @@ struct Impl_FeedEntry {
34 iTime discovered; 34 iTime discovered;
35 iString url; 35 iString url;
36 iString title; 36 iString title;
37 iBool isHeading; /* URL fragment points to a heading */
37 uint32_t bookmarkId; /* note: runtime only, not a persistent ID */ 38 uint32_t bookmarkId; /* note: runtime only, not a persistent ID */
38}; 39};
39 40
diff --git a/src/gmdocument.c b/src/gmdocument.c
index b9832f38..22f409a6 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -82,6 +82,7 @@ struct Impl_GmDocument {
82 iString url; /* for resolving relative links */ 82 iString url; /* for resolving relative links */
83 iString localHost; 83 iString localHost;
84 iInt2 size; 84 iInt2 size;
85 int outsideMargin;
85 iArray layout; /* contents of source, laid out in document space */ 86 iArray layout; /* contents of source, laid out in document space */
86 iPtrArray links; 87 iPtrArray links;
87 enum iGmDocumentBanner bannerType; 88 enum iGmDocumentBanner bannerType;
@@ -239,7 +240,10 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
239 iString *path = newRange_String(parts.path); 240 iString *path = newRange_String(parts.path);
240 if (endsWithCase_String(path, ".gif") || endsWithCase_String(path, ".jpg") || 241 if (endsWithCase_String(path, ".gif") || endsWithCase_String(path, ".jpg") ||
241 endsWithCase_String(path, ".jpeg") || endsWithCase_String(path, ".png") || 242 endsWithCase_String(path, ".jpeg") || endsWithCase_String(path, ".png") ||
242 endsWithCase_String(path, ".tga") || endsWithCase_String(path, ".psd") || 243 endsWithCase_String(path, ".tga") || endsWithCase_String(path, ".psd") ||
244#if defined (LAGRANGE_ENABLE_WEBP)
245 endsWithCase_String(path, ".webp") ||
246#endif
243 endsWithCase_String(path, ".hdr") || endsWithCase_String(path, ".pic")) { 247 endsWithCase_String(path, ".hdr") || endsWithCase_String(path, ".pic")) {
244 link->flags |= imageFileExtension_GmLinkFlag; 248 link->flags |= imageFileExtension_GmLinkFlag;
245 } 249 }
@@ -460,6 +464,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
460 const iBool isNarrow = d->size.x < 90 * gap_Text; 464 const iBool isNarrow = d->size.x < 90 * gap_Text;
461 const iBool isVeryNarrow = d->size.x <= 70 * gap_Text; 465 const iBool isVeryNarrow = d->size.x <= 70 * gap_Text;
462 const iBool isExtremelyNarrow = d->size.x <= 60 * gap_Text; 466 const iBool isExtremelyNarrow = d->size.x <= 60 * gap_Text;
467 const iBool isFullWidthImages = (d->outsideMargin < 5 * gap_UI);
463 const iBool isDarkBg = isDark_GmDocumentTheme( 468 const iBool isDarkBg = isDark_GmDocumentTheme(
464 isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight); 469 isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
465 /* TODO: Collect these parameters into a GmTheme. */ 470 /* TODO: Collect these parameters into a GmTheme. */
@@ -495,7 +500,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
495 0.0f, 0.25f, 1.0f, 0.5f, 1.5f, 0.5f, 0.5f, 0.25f 500 0.0f, 0.25f, 1.0f, 0.5f, 1.5f, 0.5f, 0.5f, 0.25f
496 }; 501 };
497 static const char *arrow = rightArrowhead_Icon; 502 static const char *arrow = rightArrowhead_Icon;
498 static const char *envelope = "\U0001f4e7"; 503 static const char *envelope = envelope_Icon;
499 static const char *bullet = "\u2022"; 504 static const char *bullet = "\u2022";
500 static const char *folder = "\U0001f4c1"; 505 static const char *folder = "\U0001f4c1";
501 static const char *globe = globe_Icon; 506 static const char *globe = globe_Icon;
@@ -503,6 +508,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
503 static const char *magnifyingGlass = "\U0001f50d"; 508 static const char *magnifyingGlass = "\U0001f50d";
504 static const char *pointingFinger = "\U0001f449"; 509 static const char *pointingFinger = "\U0001f449";
505 static const char *uploadArrow = upload_Icon; 510 static const char *uploadArrow = upload_Icon;
511 static const char *image = photo_Icon;
506 clear_Array(&d->layout); 512 clear_Array(&d->layout);
507 clearLinks_GmDocument_(d); 513 clearLinks_GmDocument_(d);
508 clear_Array(&d->headings); 514 clear_Array(&d->headings);
@@ -761,6 +767,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
761 : scheme == finger_GmLinkScheme ? pointingFinger 767 : scheme == finger_GmLinkScheme ? pointingFinger
762 : scheme == mailto_GmLinkScheme ? envelope 768 : scheme == mailto_GmLinkScheme ? envelope
763 : link->flags & remote_GmLinkFlag ? globe 769 : link->flags & remote_GmLinkFlag ? globe
770 : link->flags & imageFileExtension_GmLinkFlag ? image
764 : arrow); 771 : arrow);
765 /* Custom link icon is shown on local Gemini links only. */ 772 /* Custom link icon is shown on local Gemini links only. */
766 if (!isEmpty_Range(&link->labelIcon)) { 773 if (!isEmpty_Range(&link->labelIcon)) {
@@ -813,7 +820,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
813 rts.layoutWidth = d->size.x; 820 rts.layoutWidth = d->size.x;
814 rts.indent = indent * gap_Text; 821 rts.indent = indent * gap_Text;
815 /* The right margin is used for balancing lines horizontally. */ 822 /* The right margin is used for balancing lines horizontally. */
816 if (isVeryNarrow) { 823 if (isVeryNarrow || isFullWidthImages) {
817 rts.rightMargin = 0; 824 rts.rightMargin = 0;
818 } 825 }
819 else { 826 else {
@@ -898,6 +905,13 @@ static void doLayout_GmDocument_(iGmDocument *d) {
898 run.bounds.size.x = d->size.x; 905 run.bounds.size.x = d->size.x;
899 const float aspect = (float) imgSize.y / (float) imgSize.x; 906 const float aspect = (float) imgSize.y / (float) imgSize.x;
900 run.bounds.size.y = d->size.x * aspect; 907 run.bounds.size.y = d->size.x * aspect;
908 /* Extend the image to full width, including outside margin, if the viewport
909 is narrow enough. */
910 if (isFullWidthImages) {
911 run.bounds.size.x += d->outsideMargin * 2;
912 run.bounds.size.y += d->outsideMargin * 2 * aspect;
913 run.bounds.pos.x -= d->outsideMargin;
914 }
901 run.visBounds = run.bounds; 915 run.visBounds = run.bounds;
902 const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio); 916 const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);
903 if (width_Rect(run.visBounds) > maxSize.x) { 917 if (width_Rect(run.visBounds) > maxSize.x) {
@@ -990,6 +1004,7 @@ void init_GmDocument(iGmDocument *d) {
990 init_String(&d->url); 1004 init_String(&d->url);
991 init_String(&d->localHost); 1005 init_String(&d->localHost);
992 d->bannerType = siteDomain_GmDocumentBanner; 1006 d->bannerType = siteDomain_GmDocumentBanner;
1007 d->outsideMargin = 0;
993 d->size = zero_I2(); 1008 d->size = zero_I2();
994 init_Array(&d->layout, sizeof(iGmRun)); 1009 init_Array(&d->layout, sizeof(iGmRun));
995 init_PtrArray(&d->links); 1010 init_PtrArray(&d->links);
@@ -1543,8 +1558,9 @@ void setBanner_GmDocument(iGmDocument *d, enum iGmDocumentBanner type) {
1543 d->bannerType = type; 1558 d->bannerType = type;
1544} 1559}
1545 1560
1546void setWidth_GmDocument(iGmDocument *d, int width) { 1561void setWidth_GmDocument(iGmDocument *d, int width, int outsideMargin) {
1547 d->size.x = width; 1562 d->size.x = width;
1563 d->outsideMargin = outsideMargin; /* distance to edge of the viewport */
1548 doLayout_GmDocument_(d); /* TODO: just flag need-layout and do it later */ 1564 doLayout_GmDocument_(d); /* TODO: just flag need-layout and do it later */
1549} 1565}
1550 1566
@@ -1698,7 +1714,7 @@ void setUrl_GmDocument(iGmDocument *d, const iString *url) {
1698 updateIconBasedOnUrl_GmDocument_(d); 1714 updateIconBasedOnUrl_GmDocument_(d);
1699} 1715}
1700 1716
1701void setSource_GmDocument(iGmDocument *d, const iString *source, int width, 1717void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int outsideMargin,
1702 enum iGmDocumentUpdate updateType) { 1718 enum iGmDocumentUpdate updateType) {
1703// printf("[GmDocument] source update (%zu bytes), width:%d, final:%d\n", 1719// printf("[GmDocument] source update (%zu bytes), width:%d, final:%d\n",
1704// size_String(source), width, updateType == final_GmDocumentUpdate); 1720// size_String(source), width, updateType == final_GmDocumentUpdate);
@@ -1713,7 +1729,7 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width,
1713 if (isNormalized_GmDocument_(d)) { 1729 if (isNormalized_GmDocument_(d)) {
1714 normalize_GmDocument(d); 1730 normalize_GmDocument(d);
1715 } 1731 }
1716 setWidth_GmDocument(d, width); /* re-do layout */ 1732 setWidth_GmDocument(d, width, outsideMargin); /* re-do layout */
1717} 1733}
1718 1734
1719void foldPre_GmDocument(iGmDocument *d, uint16_t preId) { 1735void foldPre_GmDocument(iGmDocument *d, uint16_t preId) {
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 9a7a70df..332c3e00 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -59,6 +59,7 @@ enum iGmDocumentTheme {
59 white_GmDocumentTheme, 59 white_GmDocumentTheme,
60 sepia_GmDocumentTheme, 60 sepia_GmDocumentTheme,
61 highContrast_GmDocumentTheme, 61 highContrast_GmDocumentTheme,
62 max_GmDocumentTheme
62}; 63};
63 64
64iBool isDark_GmDocumentTheme(enum iGmDocumentTheme); 65iBool isDark_GmDocumentTheme(enum iGmDocumentTheme);
@@ -174,11 +175,11 @@ enum iGmDocumentUpdate {
174void setThemeSeed_GmDocument (iGmDocument *, const iBlock *seed); 175void setThemeSeed_GmDocument (iGmDocument *, const iBlock *seed);
175void setFormat_GmDocument (iGmDocument *, enum iSourceFormat format); 176void setFormat_GmDocument (iGmDocument *, enum iSourceFormat format);
176void setBanner_GmDocument (iGmDocument *, enum iGmDocumentBanner type); 177void setBanner_GmDocument (iGmDocument *, enum iGmDocumentBanner type);
177void setWidth_GmDocument (iGmDocument *, int width); 178void setWidth_GmDocument (iGmDocument *, int width, int outsideMargin);
178void redoLayout_GmDocument (iGmDocument *); 179void redoLayout_GmDocument (iGmDocument *);
179iBool updateOpenURLs_GmDocument(iGmDocument *); 180iBool updateOpenURLs_GmDocument(iGmDocument *);
180void setUrl_GmDocument (iGmDocument *, const iString *url); 181void setUrl_GmDocument (iGmDocument *, const iString *url);
181void setSource_GmDocument (iGmDocument *, const iString *source, int width, 182void setSource_GmDocument (iGmDocument *, const iString *source, int width, int outsideMargin,
182 enum iGmDocumentUpdate updateType); 183 enum iGmDocumentUpdate updateType);
183void foldPre_GmDocument (iGmDocument *, uint16_t preId); 184void foldPre_GmDocument (iGmDocument *, uint16_t preId);
184 185
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 00a02983..1a9e83a9 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -158,6 +158,7 @@ struct Impl_GmRequest {
158 uint32_t id; 158 uint32_t id;
159 iMutex * mtx; 159 iMutex * mtx;
160 iGmCerts * certs; /* not owned */ 160 iGmCerts * certs; /* not owned */
161 const iGmIdentity * identity;
161 enum iGmRequestState state; 162 enum iGmRequestState state;
162 iString url; 163 iString url;
163 iTitanData * titan; 164 iTitanData * titan;
@@ -527,6 +528,7 @@ static void beginGopherConnection_GmRequest_(iGmRequest *d, const iString *host,
527void init_GmRequest(iGmRequest *d, iGmCerts *certs) { 528void init_GmRequest(iGmRequest *d, iGmCerts *certs) {
528 d->mtx = new_Mutex(); 529 d->mtx = new_Mutex();
529 d->id = add_Atomic(&idGen_, 1) + 1; 530 d->id = add_Atomic(&idGen_, 1) + 1;
531 d->identity = NULL;
530 d->resp = new_GmResponse(); 532 d->resp = new_GmResponse();
531 d->isFilterEnabled = iTrue; 533 d->isFilterEnabled = iTrue;
532 d->isRespLocked = iFalse; 534 d->isRespLocked = iFalse;
@@ -582,6 +584,11 @@ void setUrl_GmRequest(iGmRequest *d, const iString *url) {
582 the web. */ 584 the web. */
583 urlEncodePath_String(&d->url); 585 urlEncodePath_String(&d->url);
584 urlEncodeSpaces_String(&d->url); 586 urlEncodeSpaces_String(&d->url);
587 d->identity = identityForUrl_GmCerts(d->certs, &d->url);
588}
589
590void setIdentity_GmRequest(iGmRequest *d, const iGmIdentity *id) {
591 d->identity = id;
585} 592}
586 593
587static iBool isTitan_GmRequest_(const iGmRequest *d) { 594static iBool isTitan_GmRequest_(const iGmRequest *d) {
@@ -902,9 +909,8 @@ void submit_GmRequest(iGmRequest *d) {
902 } 909 }
903 d->state = receivingHeader_GmRequestState; 910 d->state = receivingHeader_GmRequestState;
904 d->req = new_TlsRequest(); 911 d->req = new_TlsRequest();
905 const iGmIdentity *identity = identityForUrl_GmCerts(d->certs, &d->url); 912 if (d->identity) {
906 if (identity) { 913 setCertificate_TlsRequest(d->req, d->identity->cert);
907 setCertificate_TlsRequest(d->req, identity->cert);
908 } 914 }
909 iConnect(TlsRequest, d->req, readyRead, d, readIncoming_GmRequest_); 915 iConnect(TlsRequest, d->req, readyRead, d, readIncoming_GmRequest_);
910 iConnect(TlsRequest, d->req, sent, d, bytesSent_GmRequest_); 916 iConnect(TlsRequest, d->req, sent, d, bytesSent_GmRequest_);
diff --git a/src/gmrequest.h b/src/gmrequest.h
index 97b23f3c..a377ac91 100644
--- a/src/gmrequest.h
+++ b/src/gmrequest.h
@@ -28,6 +28,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
28#include "gmutil.h" 28#include "gmutil.h"
29 29
30iDeclareType(GmCerts) 30iDeclareType(GmCerts)
31iDeclareType(GmIdentity)
31iDeclareType(GmResponse) 32iDeclareType(GmResponse)
32 33
33enum iGmCertFlag { 34enum iGmCertFlag {
@@ -69,6 +70,7 @@ typedef void (*iGmRequestProgressFunc)(iGmRequest *, size_t current, size_t tota
69 70
70void enableFilters_GmRequest (iGmRequest *, iBool enable); 71void enableFilters_GmRequest (iGmRequest *, iBool enable);
71void setUrl_GmRequest (iGmRequest *, const iString *url); 72void setUrl_GmRequest (iGmRequest *, const iString *url);
73void setIdentity_GmRequest (iGmRequest *, const iGmIdentity *id);
72void setTitanData_GmRequest (iGmRequest *, const iString *mime, 74void setTitanData_GmRequest (iGmRequest *, const iString *mime,
73 const iBlock *payload, const iString *token); 75 const iBlock *payload, const iString *token);
74void setSendProgressFunc_GmRequest(iGmRequest *, iGmRequestProgressFunc func); 76void setSendProgressFunc_GmRequest(iGmRequest *, iGmRequestProgressFunc func);
diff --git a/src/gmutil.c b/src/gmutil.c
index 9552c2a1..d87de8f6 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -511,54 +511,64 @@ const iString *findContainerArchive_Path(const iString *path) {
511 return NULL; 511 return NULL;
512} 512}
513 513
514const char *mediaType_Path(const iString *path) { 514const char *mediaTypeFromFileExtension_String(const iString *d) {
515 if (endsWithCase_String(path, ".gmi") || endsWithCase_String(path, ".gemini")) { 515 if (endsWithCase_String(d, ".gmi") || endsWithCase_String(d, ".gemini")) {
516 return "text/gemini; charset=utf-8"; 516 return "text/gemini; charset=utf-8";
517 } 517 }
518 else if (endsWithCase_String(path, ".pem")) { 518 else if (endsWithCase_String(d, ".pem")) {
519 return "application/x-pem-file"; 519 return "application/x-pem-file";
520 } 520 }
521 else if (endsWithCase_String(path, ".zip")) { 521 else if (endsWithCase_String(d, ".zip")) {
522 return "application/zip"; 522 return "application/zip";
523 } 523 }
524 else if (endsWithCase_String(path, ".gpub")) { 524 else if (endsWithCase_String(d, ".gpub")) {
525 return "application/gpub+zip"; 525 return "application/gpub+zip";
526 } 526 }
527 else if (endsWithCase_String(path, ".xml")) { 527 else if (endsWithCase_String(d, ".xml")) {
528 return "text/xml"; 528 return "text/xml";
529 } 529 }
530 else if (endsWithCase_String(path, ".png")) { 530 else if (endsWithCase_String(d, ".png")) {
531 return "image/png"; 531 return "image/png";
532 } 532 }
533 else if (endsWithCase_String(path, ".jpg") || endsWithCase_String(path, ".jpeg")) { 533 else if (endsWithCase_String(d, ".webp")) {
534 return "image/webp";
535 }
536 else if (endsWithCase_String(d, ".jpg") || endsWithCase_String(d, ".jpeg")) {
534 return "image/jpeg"; 537 return "image/jpeg";
535 } 538 }
536 else if (endsWithCase_String(path, ".gif")) { 539 else if (endsWithCase_String(d, ".gif")) {
537 return "image/gif"; 540 return "image/gif";
538 } 541 }
539 else if (endsWithCase_String(path, ".wav")) { 542 else if (endsWithCase_String(d, ".wav")) {
540 return "audio/wave"; 543 return "audio/wave";
541 } 544 }
542 else if (endsWithCase_String(path, ".ogg")) { 545 else if (endsWithCase_String(d, ".ogg")) {
543 return "audio/ogg"; 546 return "audio/ogg";
544 } 547 }
545 else if (endsWithCase_String(path, ".mp3")) { 548 else if (endsWithCase_String(d, ".mp3")) {
546 return "audio/mpeg"; 549 return "audio/mpeg";
547 } 550 }
548 else if (endsWithCase_String(path, ".mid")) { 551 else if (endsWithCase_String(d, ".mid")) {
549 return "audio/midi"; 552 return "audio/midi";
550 } 553 }
551 else if (endsWithCase_String(path, ".txt") || 554 else if (endsWithCase_String(d, ".txt") ||
552 endsWithCase_String(path, ".md") || 555 endsWithCase_String(d, ".md") ||
553 endsWithCase_String(path, ".c") || 556 endsWithCase_String(d, ".c") ||
554 endsWithCase_String(path, ".h") || 557 endsWithCase_String(d, ".h") ||
555 endsWithCase_String(path, ".cc") || 558 endsWithCase_String(d, ".cc") ||
556 endsWithCase_String(path, ".hh") || 559 endsWithCase_String(d, ".hh") ||
557 endsWithCase_String(path, ".cpp") || 560 endsWithCase_String(d, ".cpp") ||
558 endsWithCase_String(path, ".hpp")) { 561 endsWithCase_String(d, ".hpp")) {
559 return "text/plain"; 562 return "text/plain";
560 } 563 }
561 const char *mtype = "application/octet-stream"; 564 return "application/octet-stream";
565}
566
567const char *mediaType_Path(const iString *path) {
568 const char *mtype = mediaTypeFromFileExtension_String(path);
569 if (iCmpStr(mtype, "application/octet-stream")) {
570 return mtype; /* extension recognized */
571 }
562 /* If the file is reasonably small and looks like UTF-8, we'll display it as text/plain. */ 572 /* If the file is reasonably small and looks like UTF-8, we'll display it as text/plain. */
563 if (fileExists_FileInfo(path) && fileSize_FileInfo(path) <= 5000000) { 573 if (fileExists_FileInfo(path) && fileSize_FileInfo(path) <= 5000000) {
564 iFile *f = new_File(path); 574 iFile *f = new_File(path);
diff --git a/src/gmutil.h b/src/gmutil.h
index f8491781..3c10d45b 100644
--- a/src/gmutil.h
+++ b/src/gmutil.h
@@ -133,6 +133,7 @@ const iString * withSpacesEncoded_String(const iString *);
133const iString * canonicalUrl_String (const iString *); 133const iString * canonicalUrl_String (const iString *);
134 134
135const char * mediaType_Path (const iString *path); 135const char * mediaType_Path (const iString *path);
136const char * mediaTypeFromFileExtension_String (const iString *);
136iRangecc mediaTypeWithoutParameters_Rangecc (iRangecc mime); 137iRangecc mediaTypeWithoutParameters_Rangecc (iRangecc mime);
137 138
138const iString * findContainerArchive_Path (const iString *path); 139const iString * findContainerArchive_Path (const iString *path);
diff --git a/src/ios.h b/src/ios.h
index 70b889bf..85177409 100644
--- a/src/ios.h
+++ b/src/ios.h
@@ -37,6 +37,7 @@ iBool processEvent_iOS (const SDL_Event *);
37void playHapticEffect_iOS (enum iHapticEffect effect); 37void playHapticEffect_iOS (enum iHapticEffect effect);
38void exportDownloadedFile_iOS(const iString *path); 38void exportDownloadedFile_iOS(const iString *path);
39void pickFileForOpening_iOS (void); 39void pickFileForOpening_iOS (void);
40void pickFile_iOS (const char *command); /* ` path:%s` will be appended */
40 41
41iBool isPhone_iOS (void); 42iBool isPhone_iOS (void);
42void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom); 43void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom);
diff --git a/src/ios.m b/src/ios.m
index b82d54a7..b46fb8dc 100644
--- a/src/ios.m
+++ b/src/ios.m
@@ -161,6 +161,7 @@ API_AVAILABLE(ios(13.0))
161 161
162@interface AppState : NSObject<UIDocumentPickerDelegate> { 162@interface AppState : NSObject<UIDocumentPickerDelegate> {
163 iString *fileBeingSaved; 163 iString *fileBeingSaved;
164 iString *pickFileCommand;
164} 165}
165@property (nonatomic, assign) BOOL isHapticsAvailable; 166@property (nonatomic, assign) BOOL isHapticsAvailable;
166@property (nonatomic, strong) NSObject *haptic; 167@property (nonatomic, strong) NSObject *haptic;
@@ -173,9 +174,17 @@ static AppState *appState_;
173-(instancetype)init { 174-(instancetype)init {
174 self = [super init]; 175 self = [super init];
175 fileBeingSaved = NULL; 176 fileBeingSaved = NULL;
177 pickFileCommand = NULL;
176 return self; 178 return self;
177} 179}
178 180
181-(void)setPickFileCommand:(const char *)command {
182 if (!pickFileCommand) {
183 pickFileCommand = new_String();
184 }
185 setCStr_String(pickFileCommand, command);
186}
187
179-(void)setFileBeingSaved:(const iString *)path { 188-(void)setFileBeingSaved:(const iString *)path {
180 fileBeingSaved = copy_String(path); 189 fileBeingSaved = copy_String(path);
181} 190}
@@ -213,7 +222,11 @@ didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
213 NSURL *url = [urls firstObject]; 222 NSURL *url = [urls firstObject];
214 iString *path = localFilePathFromUrl_String(collectNewCStr_String([[url absoluteString] 223 iString *path = localFilePathFromUrl_String(collectNewCStr_String([[url absoluteString]
215 UTF8String])); 224 UTF8String]));
216 postCommandf_App("file.open temp:1 path:%s", cstrCollect_String(path)); 225 postCommandf_App("%s temp:1 path:%s",
226 cstr_String(pickFileCommand),
227 cstrCollect_String(path));
228 delete_String(pickFileCommand);
229 pickFileCommand = NULL;
217 } 230 }
218} 231}
219 232
@@ -221,6 +234,10 @@ didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
221 if (fileBeingSaved) { 234 if (fileBeingSaved) {
222 [self removeSavedFile]; 235 [self removeSavedFile];
223 } 236 }
237 if (pickFileCommand) {
238 delete_String(pickFileCommand);
239 pickFileCommand = NULL;
240 }
224} 241}
225 242
226-(void)keyboardOnScreen:(NSNotification *)notification { 243-(void)keyboardOnScreen:(NSNotification *)notification {
@@ -230,14 +247,14 @@ didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
230 UIView *view = [viewController_(get_Window()) view]; 247 UIView *view = [viewController_(get_Window()) view];
231 CGRect keyboardFrame = [view convertRect:rawFrame fromView:nil]; 248 CGRect keyboardFrame = [view convertRect:rawFrame fromView:nil];
232// NSLog(@"keyboardFrame: %@", NSStringFromCGRect(keyboardFrame)); 249// NSLog(@"keyboardFrame: %@", NSStringFromCGRect(keyboardFrame));
233 iWindow *window = get_Window(); 250 iMainWindow *window = get_MainWindow();
234 const iInt2 rootSize = size_Root(window->roots[0]); 251 const iInt2 rootSize = size_Root(window->base.roots[0]);
235 const int keyTop = keyboardFrame.origin.y * window->pixelRatio; 252 const int keyTop = keyboardFrame.origin.y * window->base.pixelRatio;
236 setKeyboardHeight_Window(window, rootSize.y - keyTop); 253 setKeyboardHeight_MainWindow(window, rootSize.y - keyTop);
237} 254}
238 255
239-(void)keyboardOffScreen:(NSNotification *)notification { 256-(void)keyboardOffScreen:(NSNotification *)notification {
240 setKeyboardHeight_Window(get_Window(), 0); 257 setKeyboardHeight_MainWindow(get_MainWindow(), 0);
241} 258}
242@end 259@end
243 260
@@ -264,7 +281,6 @@ void setupApplication_iOS(void) {
264 selector:@selector(keyboardOffScreen:) 281 selector:@selector(keyboardOffScreen:)
265 name:UIKeyboardWillHideNotification 282 name:UIKeyboardWillHideNotification
266 object:nil]; 283 object:nil];
267 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
268 /* Media player remote controls. */ 284 /* Media player remote controls. */
269 MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; 285 MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
270 [[commandCenter pauseCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { 286 [[commandCenter pauseCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
@@ -455,11 +471,13 @@ void exportDownloadedFile_iOS(const iString *path) {
455} 471}
456 472
457void pickFileForOpening_iOS(void) { 473void pickFileForOpening_iOS(void) {
474 pickFile_iOS("file.open");
475}
476
477void pickFile_iOS(const char *command) {
478 [appState_ setPickFileCommand:command];
458 UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] 479 UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc]
459 initWithDocumentTypes:@[@"fi.skyjake.lagrange.gemini", 480 initWithDocumentTypes:@[@"public.data"]
460 @"public.text",
461 @"public.image",
462 @"public.audio"]
463 inMode:UIDocumentPickerModeImport]; 481 inMode:UIDocumentPickerModeImport];
464 picker.delegate = appState_; 482 picker.delegate = appState_;
465 [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; 483 [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil];
@@ -487,6 +505,8 @@ void init_AVFAudioPlayer(iAVFAudioPlayer *d) {
487 d->player = NULL; 505 d->player = NULL;
488 d->volume = 1.0f; 506 d->volume = 1.0f;
489 d->state = initialized_AVFAudioPlayerState; 507 d->state = initialized_AVFAudioPlayerState;
508 /* Playback is imminent. */
509 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
490} 510}
491 511
492void deinit_AVFAudioPlayer(iAVFAudioPlayer *d) { 512void deinit_AVFAudioPlayer(iAVFAudioPlayer *d) {
diff --git a/src/macos.h b/src/macos.h
index 0d3f097a..22a6dfff 100644
--- a/src/macos.h
+++ b/src/macos.h
@@ -24,6 +24,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
24 24
25#include "ui/util.h" 25#include "ui/util.h"
26 26
27iDeclareType(MenuItem)
28iDeclareType(Window)
29iDeclareType(Widget)
30
27/* Platform-specific functionality for macOS */ 31/* Platform-specific functionality for macOS */
28 32
29iBool shouldDefaultToMetalRenderer_MacOS (void); 33iBool shouldDefaultToMetalRenderer_MacOS (void);
@@ -31,9 +35,12 @@ iBool shouldDefaultToMetalRenderer_MacOS (void);
31void enableMomentumScroll_MacOS (void); 35void enableMomentumScroll_MacOS (void);
32void registerURLHandler_MacOS (void); 36void registerURLHandler_MacOS (void);
33void setupApplication_MacOS (void); 37void setupApplication_MacOS (void);
38void hideTitleBar_MacOS (iWindow *window);
34void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count); 39void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count);
35void removeMenu_MacOS (int atIndex); 40void removeMenu_MacOS (int atIndex);
36void enableMenu_MacOS (const char *menuLabel, iBool enable); 41void enableMenu_MacOS (const char *menuLabel, iBool enable);
37void enableMenuItem_MacOS (const char *menuItemCommand, iBool enable); 42void enableMenuItem_MacOS (const char *menuItemCommand, iBool enable);
38void enableMenuItemsByKey_MacOS (int key, int kmods, iBool enable); 43void enableMenuItemsByKey_MacOS (int key, int kmods, iBool enable);
39void handleCommand_MacOS (const char *cmd); 44void handleCommand_MacOS (const char *cmd);
45
46void showPopupMenu_MacOS (iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n);
diff --git a/src/macos.m b/src/macos.m
index d588fa4a..53a6da00 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
30#include "ui/window.h" 30#include "ui/window.h"
31 31
32#include <SDL_timer.h> 32#include <SDL_timer.h>
33#include <SDL_syswm.h>
33 34
34#import <AppKit/AppKit.h> 35#import <AppKit/AppKit.h>
35 36
@@ -51,6 +52,16 @@ static iInt2 macVer_(void) {
51 return init_I2(10, 10); 52 return init_I2(10, 10);
52} 53}
53 54
55static NSWindow *nsWindow_(SDL_Window *window) {
56 SDL_SysWMinfo wm;
57 SDL_VERSION(&wm.version);
58 if (SDL_GetWindowWMInfo(window, &wm)) {
59 return wm.info.cocoa.window;
60 }
61 iAssert(false);
62 return nil;
63}
64
54static NSString *currentSystemAppearance_(void) { 65static NSString *currentSystemAppearance_(void) {
55 /* This API does not exist on 10.13. */ 66 /* This API does not exist on 10.13. */
56 if ([NSApp respondsToSelector:@selector(effectiveAppearance)]) { 67 if ([NSApp respondsToSelector:@selector(effectiveAppearance)]) {
@@ -66,6 +77,14 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
66 return ver.x > 10 || ver.y > 13;*/ 77 return ver.x > 10 || ver.y > 13;*/
67} 78}
68 79
80static void ignoreImmediateKeyDownEvents_(void) {
81 /* SDL ignores menu key equivalents so the keydown events will be posted regardless.
82 However, we shouldn't double-activate menu items when a shortcut key is used in our
83 widgets. Quite a kludge: take advantage of Window's focus-acquisition threshold to
84 ignore the immediately following key down events. */
85 get_Window()->focusGainedAt = SDL_GetTicks();
86}
87
69/*----------------------------------------------------------------------------------------------*/ 88/*----------------------------------------------------------------------------------------------*/
70 89
71@interface CommandButton : NSCustomTouchBarItem { 90@interface CommandButton : NSCustomTouchBarItem {
@@ -135,11 +154,60 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
135 154
136/*----------------------------------------------------------------------------------------------*/ 155/*----------------------------------------------------------------------------------------------*/
137 156
157@interface MenuCommands : NSObject {
158 NSMutableDictionary<NSString *, NSString *> *commands;
159 iWidget *source;
160}
161@end
162
163@implementation MenuCommands
164
165- (id)init {
166 commands = [[NSMutableDictionary<NSString *, NSString *> alloc] init];
167 source = NULL;
168 return self;
169}
170
171- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem {
172 [commands setObject:command forKey:[menuItem title]];
173}
174
175- (void)setSource:(iWidget *)widget {
176 source = widget;
177}
178
179- (void)clear {
180 [commands removeAllObjects];
181}
182
183- (NSString *)commandForMenuItem:(NSMenuItem *)menuItem {
184 return [commands objectForKey:[menuItem title]];
185}
186
187- (void)postMenuItemCommand:(id)sender {
188 NSString *command = [commands objectForKey:[(NSMenuItem *)sender title]];
189 if (command) {
190 const char *cstr = [command cStringUsingEncoding:NSUTF8StringEncoding];
191 if (source) {
192 postCommand_Widget(source, "%s", cstr);
193 }
194 else {
195 postCommand_Root(NULL, cstr);
196 }
197 ignoreImmediateKeyDownEvents_();
198 }
199}
200
201@end
202
203/*----------------------------------------------------------------------------------------------*/
204
138@interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> { 205@interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> {
139 enum iTouchBarVariant touchBarVariant; 206 enum iTouchBarVariant touchBarVariant;
140 NSString *currentAppearanceName; 207 NSString *currentAppearanceName;
141 NSObject<NSApplicationDelegate> *sdlDelegate; 208 NSObject<NSApplicationDelegate> *sdlDelegate;
142 NSMutableDictionary<NSString *, NSString*> *menuCommands; 209 //NSMutableDictionary<NSString *, NSString*> *menuCommands;
210 MenuCommands *menuCommands;
143} 211}
144- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl; 212- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl;
145- (NSTouchBar *)makeTouchBar; 213- (NSTouchBar *)makeTouchBar;
@@ -154,7 +222,7 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
154- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl { 222- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl {
155 [super init]; 223 [super init];
156 currentAppearanceName = nil; 224 currentAppearanceName = nil;
157 menuCommands = [[NSMutableDictionary<NSString *, NSString *> alloc] init]; 225 menuCommands = [[MenuCommands alloc] init];
158 touchBarVariant = default_TouchBarVariant; 226 touchBarVariant = default_TouchBarVariant;
159 sdlDelegate = sdl; 227 sdlDelegate = sdl;
160 return self; 228 return self;
@@ -171,6 +239,14 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
171 self.touchBar = nil; 239 self.touchBar = nil;
172} 240}
173 241
242- (MenuCommands *)menuCommands {
243 return menuCommands;
244}
245
246- (void)postMenuItemCommand:(id)sender {
247 [menuCommands postMenuItemCommand:sender];
248}
249
174static void appearanceChanged_MacOS_(NSString *name) { 250static void appearanceChanged_MacOS_(NSString *name) {
175 const iBool isDark = [name containsString:@"Dark"]; 251 const iBool isDark = [name containsString:@"Dark"];
176 const iBool isHighContrast = [name containsString:@"HighContrast"]; 252 const iBool isHighContrast = [name containsString:@"HighContrast"];
@@ -187,10 +263,6 @@ static void appearanceChanged_MacOS_(NSString *name) {
187 } 263 }
188} 264}
189 265
190- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem {
191 [menuCommands setObject:command forKey:[menuItem title]];
192}
193
194- (BOOL)application:(NSApplication *)app openFile:(NSString *)filename { 266- (BOOL)application:(NSApplication *)app openFile:(NSString *)filename {
195 return [sdlDelegate application:app openFile:filename]; 267 return [sdlDelegate application:app openFile:filename];
196} 268}
@@ -247,31 +319,11 @@ static void appearanceChanged_MacOS_(NSString *name) {
247 ignoreImmediateKeyDownEvents_(); 319 ignoreImmediateKeyDownEvents_();
248} 320}
249 321
250static void ignoreImmediateKeyDownEvents_(void) {
251 /* SDL ignores menu key equivalents so the keydown events will be posted regardless.
252 However, we shouldn't double-activate menu items when a shortcut key is used in our
253 widgets. Quite a kludge: take advantage of Window's focus-acquisition threshold to
254 ignore the immediately following key down events. */
255 get_Window()->focusGainedAt = SDL_GetTicks();
256}
257
258- (void)closeTab { 322- (void)closeTab {
259 postCommand_App("tabs.close"); 323 postCommand_App("tabs.close");
260 ignoreImmediateKeyDownEvents_(); 324 ignoreImmediateKeyDownEvents_();
261} 325}
262 326
263- (NSString *)commandForItem:(NSMenuItem *)menuItem {
264 return [menuCommands objectForKey:[menuItem title]];
265}
266
267- (void)postMenuItemCommand:(id)sender {
268 NSString *command = [menuCommands objectForKey:[(NSMenuItem *)sender title]];
269 if (command) {
270 postCommand_App([command cStringUsingEncoding:NSUTF8StringEncoding]);
271 ignoreImmediateKeyDownEvents_();
272 }
273}
274
275- (void)sidebarModePressed:(id)sender { 327- (void)sidebarModePressed:(id)sender {
276 NSSegmentedControl *seg = sender; 328 NSSegmentedControl *seg = sender;
277 postCommandf_App("sidebar.mode arg:%d toggle:1", (int) [seg selectedSegment]); 329 postCommandf_App("sidebar.mode arg:%d toggle:1", (int) [seg selectedSegment]);
@@ -370,6 +422,11 @@ void setupApplication_MacOS(void) {
370 windowCloseItem.action = @selector(closeTab); 422 windowCloseItem.action = @selector(closeTab);
371} 423}
372 424
425void hideTitleBar_MacOS(iWindow *window) {
426 NSWindow *w = nsWindow_(window->win);
427 w.styleMask = 0; /* borderless */
428}
429
373void enableMenu_MacOS(const char *menuLabel, iBool enable) { 430void enableMenu_MacOS(const char *menuLabel, iBool enable) {
374 menuLabel = translateCStr_Lang(menuLabel); 431 menuLabel = translateCStr_Lang(menuLabel);
375 NSApplication *app = [NSApplication sharedApplication]; 432 NSApplication *app = [NSApplication sharedApplication];
@@ -377,7 +434,6 @@ void enableMenu_MacOS(const char *menuLabel, iBool enable) {
377 NSString *label = [NSString stringWithUTF8String:menuLabel]; 434 NSString *label = [NSString stringWithUTF8String:menuLabel];
378 NSMenuItem *menuItem = [appMenu itemAtIndex:[appMenu indexOfItemWithTitle:label]]; 435 NSMenuItem *menuItem = [appMenu itemAtIndex:[appMenu indexOfItemWithTitle:label]];
379 [menuItem setEnabled:enable]; 436 [menuItem setEnabled:enable];
380 [label release];
381} 437}
382 438
383void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) { 439void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) {
@@ -388,7 +444,7 @@ void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) {
388 NSMenu *menu = mainMenuItem.submenu; 444 NSMenu *menu = mainMenuItem.submenu;
389 if (menu) { 445 if (menu) {
390 for (NSMenuItem *menuItem in menu.itemArray) { 446 for (NSMenuItem *menuItem in menu.itemArray) {
391 NSString *command = [myDel commandForItem:menuItem]; 447 NSString *command = [[myDel menuCommands] commandForMenuItem:menuItem];
392 if (command) { 448 if (command) {
393 if (!iCmpStr([command cStringUsingEncoding:NSUTF8StringEncoding], 449 if (!iCmpStr([command cStringUsingEncoding:NSUTF8StringEncoding],
394 menuItemCommand)) { 450 menuItemCommand)) {
@@ -468,35 +524,58 @@ void removeMenu_MacOS(int atIndex) {
468 [appMenu removeItemAtIndex:atIndex]; 524 [appMenu removeItemAtIndex:atIndex];
469} 525}
470 526
471void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) { 527enum iColorId removeColorEscapes_String(iString *d) {
472 NSApplication *app = [NSApplication sharedApplication]; 528 enum iColorId color = none_ColorId;
473 MyDelegate *myDel = (MyDelegate *) app.delegate; 529 for (;;) {
474 NSMenu *appMenu = [app mainMenu]; 530 const char *esc = strchr(cstr_String(d), '\v');
475 menuLabel = translateCStr_Lang(menuLabel); 531 if (esc) {
476 NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel] 532 const char *endp;
477 action:nil 533 color = parseEscape_Color(esc, &endp);
478 keyEquivalent:@"" 534 remove_Block(&d->chars, esc - cstr_String(d), endp - esc);
479 atIndex:atIndex];
480 NSMenu *menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menuLabel]];
481 [menu setAutoenablesItems:NO];
482 for (size_t i = 0; i < count; ++i) {
483 const char *label = translateCStr_Lang(items[i].label);
484 if (label[0] == '\v') {
485 /* Skip the formatting escape. */
486 label += 2;
487 } 535 }
536 else break;
537 }
538 return color;
539}
540
541static void makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iMenuItem *items, size_t n) {
542 for (size_t i = 0; i < n && items[i].label; ++i) {
543 const char *label = translateCStr_Lang(items[i].label);
488 if (equal_CStr(label, "---")) { 544 if (equal_CStr(label, "---")) {
489 [menu addItem:[NSMenuItem separatorItem]]; 545 [menu addItem:[NSMenuItem separatorItem]];
490 } 546 }
491 else { 547 else {
492 const iBool hasCommand = (items[i].command && items[i].command[0]); 548 const iBool hasCommand = (items[i].command && items[i].command[0]);
493 NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:label] 549 iBool isChecked = iFalse;
550 iBool isDisabled = iFalse;
551 if (startsWith_CStr(label, "###")) {
552 isChecked = iTrue;
553 label += 3;
554 }
555 else if (startsWith_CStr(label, "///")) {
556 isDisabled = iTrue;
557 label += 3;
558 }
559 iString itemTitle;
560 initCStr_String(&itemTitle, label);
561 removeIconPrefix_String(&itemTitle);
562 if (removeColorEscapes_String(&itemTitle) == uiTextCaution_ColorId) {
563// prependCStr_String(&itemTitle, "\u26a0\ufe0f ");
564 }
565 NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:cstr_String(&itemTitle)]
494 action:(hasCommand ? @selector(postMenuItemCommand:) : nil) 566 action:(hasCommand ? @selector(postMenuItemCommand:) : nil)
495 keyEquivalent:@""]; 567 keyEquivalent:@""];
568 deinit_String(&itemTitle);
569 [item setTarget:commands];
570 if (isChecked) {
571 [item setState:NSControlStateValueOn];
572 }
573 [item setEnabled:!isDisabled];
496 int key = items[i].key; 574 int key = items[i].key;
497 int kmods = items[i].kmods; 575 int kmods = items[i].kmods;
498 if (hasCommand) { 576 if (hasCommand) {
499 [myDel setCommand:[NSString stringWithUTF8String:items[i].command] forMenuItem:item]; 577 [commands setCommand:[NSString stringWithUTF8String:items[i].command]
578 forMenuItem:item];
500 /* Bindings may have a different key. */ 579 /* Bindings may have a different key. */
501 const iBinding *bind = findCommand_Keys(items[i].command); 580 const iBinding *bind = findCommand_Keys(items[i].command);
502 if (bind && bind->id < builtIn_BindingId) { 581 if (bind && bind->id < builtIn_BindingId) {
@@ -507,6 +586,20 @@ void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *
507 setShortcut_NSMenuItem_(item, key, kmods); 586 setShortcut_NSMenuItem_(item, key, kmods);
508 } 587 }
509 } 588 }
589}
590
591void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) {
592 NSApplication *app = [NSApplication sharedApplication];
593 MyDelegate *myDel = (MyDelegate *) app.delegate;
594 NSMenu *appMenu = [app mainMenu];
595 menuLabel = translateCStr_Lang(menuLabel);
596 NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel]
597 action:nil
598 keyEquivalent:@""
599 atIndex:atIndex];
600 NSMenu *menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menuLabel]];
601 [menu setAutoenablesItems:NO];
602 makeMenuItems_(menu, [myDel menuCommands], items, count);
510 [mainItem setSubmenu:menu]; 603 [mainItem setSubmenu:menu];
511 [menu release]; 604 [menu release];
512} 605}
@@ -527,7 +620,7 @@ void handleCommand_MacOS(const char *cmd) {
527 if (menu) { 620 if (menu) {
528 int itemIndex = 0; 621 int itemIndex = 0;
529 for (NSMenuItem *menuItem in menu.itemArray) { 622 for (NSMenuItem *menuItem in menu.itemArray) {
530 NSString *command = [myDel commandForItem:menuItem]; 623 NSString *command = [[myDel menuCommands] commandForMenuItem:menuItem];
531 if (!command && mainIndex == 6 && itemIndex == 0) { 624 if (!command && mainIndex == 6 && itemIndex == 0) {
532 /* Window > Close */ 625 /* Window > Close */
533 command = @"tabs.close"; 626 command = @"tabs.close";
@@ -553,3 +646,40 @@ void handleCommand_MacOS(const char *cmd) {
553void log_MacOS(const char *msg) { 646void log_MacOS(const char *msg) {
554 NSLog(@"%s", msg); 647 NSLog(@"%s", msg);
555} 648}
649
650void showPopupMenu_MacOS(iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n) {
651 NSMenu * menu = [[NSMenu alloc] init];
652 MenuCommands *menuCommands = [[MenuCommands alloc] init];
653 iWindow * window = as_Window(mainWindow_App());
654 NSWindow * nsWindow = nsWindow_(window->win);
655 /* View coordinates are flipped. */
656 iBool isCentered = iFalse;
657 if (isEqual_I2(windowCoord, zero_I2())) {
658 windowCoord = divi_I2(window->size, 2);
659 isCentered = iTrue;
660 }
661 windowCoord.y = window->size.y - windowCoord.y;
662 windowCoord = divf_I2(windowCoord, window->pixelRatio);
663 NSPoint screenPoint = [nsWindow convertPointToScreen:(CGPoint){ windowCoord.x, windowCoord.y }];
664 makeMenuItems_(menu, menuCommands, items, n);
665 [menuCommands setSource:source];
666 if (isCentered) {
667 NSSize menuSize = [menu size];
668 screenPoint.x -= menuSize.width / 2;
669 screenPoint.y += menuSize.height / 2;
670 }
671 [menu setAutoenablesItems:NO];
672 [menu popUpMenuPositioningItem:nil atLocation:screenPoint inView:nil];
673 [menu release];
674 [menuCommands release];
675 /* The right mouse button has now been released so let SDL know about it. The button up event
676 was consumed by the popup menu so it got never passed to SDL. */
677 SEL sel = NSSelectorFromString(@"syncMouseButtonState"); /* custom method */
678 if ([[nsWindow delegate] respondsToSelector:sel]) {
679 NSInvocation *call = [NSInvocation invocationWithMethodSignature:
680 [NSMethodSignature signatureWithObjCTypes:"v@:"]];
681 [call setSelector:sel];
682 [call invokeWithTarget:[nsWindow delegate]];
683 }
684}
685
diff --git a/src/media.c b/src/media.c
index eb4a8311..636cd91f 100644
--- a/src/media.c
+++ b/src/media.c
@@ -30,6 +30,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
30#include "stb_image.h" 30#include "stb_image.h"
31#include "stb_image_resize.h" 31#include "stb_image_resize.h"
32 32
33#if defined (LAGRANGE_ENABLE_WEBP)
34# include <webp/decode.h>
35#endif
36
33#include <the_Foundation/file.h> 37#include <the_Foundation/file.h>
34#include <the_Foundation/ptrarray.h> 38#include <the_Foundation/ptrarray.h>
35#include <SDL_hints.h> 39#include <SDL_hints.h>
@@ -83,16 +87,70 @@ void deinit_GmImage(iGmImage *d) {
83 deinit_GmMediaProps_(&d->props); 87 deinit_GmMediaProps_(&d->props);
84} 88}
85 89
90static void applyImageStyle_(enum iImageStyle style, iInt2 size, uint8_t *imgData) {
91 if (style == original_ImageStyle) {
92 return;
93 }
94 uint8_t *pos = imgData;
95 size_t numPixels = size.x * size.y;
96 float brighten = 0.0f;
97 if (style == bgFg_ImageStyle) {
98 iColor dark = get_Color(tmBackground_ColorId);
99 iColor light = get_Color(tmParagraph_ColorId);
100 if (hsl_Color(dark).lum > hsl_Color(light).lum) {
101 iSwap(iColor, dark, light);
102 }
103 while (numPixels-- > 0) {
104 iHSLColor hsl = hsl_Color((iColor){ pos[0], pos[1], pos[2], 255 });
105 const float s = 1.0f - hsl.lum;
106 const float t = hsl.lum;
107 pos[0] = dark.r * s + light.r * t;
108 pos[1] = dark.g * s + light.g * t;
109 pos[2] = dark.b * s + light.b * t;
110 pos += 4;
111 }
112 return;
113 }
114 iColor colorize = (iColor){ 255, 255, 255, 255 };
115 if (style != grayscale_ImageStyle) {
116 colorize = get_Color(style == textColorized_ImageStyle ? tmParagraph_ColorId
117 : tmPreformatted_ColorId);
118 /* Compensate for change in mid-tones. */
119 const int colMax = iMax(iMax(colorize.r, colorize.g), colorize.b);
120 brighten = iClamp(1.0f - (colorize.r + colorize.g + colorize.b) / (colMax * 3), 0.0f, 0.5f);
121 }
122 iHSLColor hslColorize = hsl_Color(colorize);
123 while (numPixels-- > 0) {
124 iHSLColor hsl = hsl_Color((iColor){ pos[0], pos[1], pos[2], 255 });
125 iHSLColor out = { hslColorize.hue, hslColorize.sat, hsl.lum, 1.0f };
126 out.lum = powf(out.lum, 1.0f + brighten * 2);
127 iColor outRgb = rgb_HSLColor(out);
128 pos[0] = powf(outRgb.r / 255.0f, 1.0f - brighten * 0.75f) * 255;
129 pos[1] = powf(outRgb.g / 255.0f, 1.0f - brighten * 0.75f) * 255;
130 pos[2] = powf(outRgb.b / 255.0f, 1.0f - brighten * 0.75f) * 255;
131 pos += 4;
132 }
133}
134
86void makeTexture_GmImage(iGmImage *d) { 135void makeTexture_GmImage(iGmImage *d) {
87 iBlock *data = &d->partialData; 136 iBlock *data = &d->partialData;
88 d->numBytes = size_Block(data); 137 d->numBytes = size_Block(data);
89 uint8_t *imgData = stbi_load_from_memory( 138 uint8_t *imgData = NULL;
90 constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4); 139 if (cmp_String(&d->props.mime, "image/webp") == 0) {
140#if defined (LAGRANGE_ENABLE_WEBP)
141 imgData = WebPDecodeRGBA(constData_Block(data), size_Block(data), &d->size.x, &d->size.y);
142#endif
143 }
144 else {
145 imgData = stbi_load_from_memory(
146 constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4);
147 }
91 if (!imgData) { 148 if (!imgData) {
92 d->size = zero_I2(); 149 d->size = zero_I2();
93 d->texture = NULL; 150 d->texture = NULL;
94 } 151 }
95 else { 152 else {
153 applyImageStyle_(prefs_App()->imageStyle, d->size, imgData);
96 /* TODO: Save some memory by checking if the alpha channel is actually in use. */ 154 /* TODO: Save some memory by checking if the alpha channel is actually in use. */
97 iWindow *window = get_Window(); 155 iWindow *window = get_Window();
98 iInt2 texSize = d->size; 156 iInt2 texSize = d->size;
diff --git a/src/prefs.c b/src/prefs.c
index ef1ce1b0..088cc7bc 100644
--- a/src/prefs.c
+++ b/src/prefs.c
@@ -62,6 +62,7 @@ void init_Prefs(iPrefs *d) {
62 d->quoteIcon = iTrue; 62 d->quoteIcon = iTrue;
63 d->centerShortDocs = iTrue; 63 d->centerShortDocs = iTrue;
64 d->plainTextWrap = iTrue; 64 d->plainTextWrap = iTrue;
65 d->imageStyle = original_ImageStyle;
65 d->docThemeDark = colorfulDark_GmDocumentTheme; 66 d->docThemeDark = colorfulDark_GmDocumentTheme;
66 d->docThemeLight = white_GmDocumentTheme; 67 d->docThemeLight = white_GmDocumentTheme;
67 d->saturation = 1.0f; 68 d->saturation = 1.0f;
diff --git a/src/prefs.h b/src/prefs.h
index a947a595..87c9a6e6 100644
--- a/src/prefs.h
+++ b/src/prefs.h
@@ -86,6 +86,7 @@ struct Impl_Prefs {
86 iBool quoteIcon; 86 iBool quoteIcon;
87 iBool centerShortDocs; 87 iBool centerShortDocs;
88 iBool plainTextWrap; 88 iBool plainTextWrap;
89 enum iImageStyle imageStyle;
89 /* Colors */ 90 /* Colors */
90 enum iGmDocumentTheme docThemeDark; 91 enum iGmDocumentTheme docThemeDark;
91 enum iGmDocumentTheme docThemeLight; 92 enum iGmDocumentTheme docThemeLight;
diff --git a/src/ui/certimportwidget.c b/src/ui/certimportwidget.c
index a8346e19..f4dfdefa 100644
--- a/src/ui/certimportwidget.c
+++ b/src/ui/certimportwidget.c
@@ -31,6 +31,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
31#include "text.h" 31#include "text.h"
32#include "ui/util.h" 32#include "ui/util.h"
33 33
34#if defined (iPlatformAppleMobile)
35# include "ios.h"
36#endif
37
34#include <the_Foundation/file.h> 38#include <the_Foundation/file.h>
35#include <the_Foundation/tlsrequest.h> 39#include <the_Foundation/tlsrequest.h>
36#include <the_Foundation/path.h> 40#include <the_Foundation/path.h>
@@ -75,7 +79,7 @@ static iBool tryImport_CertImportWidget_(iCertImportWidget *d, const iBlock *dat
75 deinit_String(&pem); 79 deinit_String(&pem);
76 /* Update the labels. */ { 80 /* Update the labels. */ {
77 if (d->cert && !isEmpty_TlsCertificate(d->cert)) { 81 if (d->cert && !isEmpty_TlsCertificate(d->cert)) {
78 setTextCStr_LabelWidget( 82 updateTextCStr_LabelWidget(
79 d->crtLabel, 83 d->crtLabel,
80 format_CStr("%s%s", 84 format_CStr("%s%s",
81 uiTextAction_ColorEscape, 85 uiTextAction_ColorEscape,
@@ -83,19 +87,19 @@ static iBool tryImport_CertImportWidget_(iCertImportWidget *d, const iBlock *dat
83 setFrameColor_Widget(as_Widget(d->crtLabel), uiTextAction_ColorId); 87 setFrameColor_Widget(as_Widget(d->crtLabel), uiTextAction_ColorId);
84 } 88 }
85 else { 89 else {
86 setTextCStr_LabelWidget(d->crtLabel, uiTextCaution_ColorEscape "${dlg.certimport.nocert}"); 90 updateTextCStr_LabelWidget(d->crtLabel, uiTextCaution_ColorEscape "${dlg.certimport.nocert}");
87 setFrameColor_Widget(as_Widget(d->crtLabel), uiTextCaution_ColorId); 91 setFrameColor_Widget(as_Widget(d->crtLabel), uiTextCaution_ColorId);
88 } 92 }
89 if (d->cert && hasPrivateKey_TlsCertificate(d->cert)) { 93 if (d->cert && hasPrivateKey_TlsCertificate(d->cert)) {
90 iString *fng = collect_String( 94 iString *fng = collect_String(
91 hexEncode_Block(collect_Block(privateKeyFingerprint_TlsCertificate(d->cert)))); 95 hexEncode_Block(collect_Block(privateKeyFingerprint_TlsCertificate(d->cert))));
92 insertData_Block(&fng->chars, size_String(fng) / 2, "\n", 1); 96 insertData_Block(&fng->chars, size_String(fng) / 2, "\n", 1);
93 setTextCStr_LabelWidget( 97 updateTextCStr_LabelWidget(
94 d->keyLabel, format_CStr("%s%s", uiTextAction_ColorEscape, cstr_String(fng))); 98 d->keyLabel, format_CStr("%s%s", uiTextAction_ColorEscape, cstr_String(fng)));
95 setFrameColor_Widget(as_Widget(d->keyLabel), uiTextAction_ColorId); 99 setFrameColor_Widget(as_Widget(d->keyLabel), uiTextAction_ColorId);
96 } 100 }
97 else { 101 else {
98 setTextCStr_LabelWidget(d->keyLabel, uiTextCaution_ColorEscape "${dlg.certimport.nokey}"); 102 updateTextCStr_LabelWidget(d->keyLabel, uiTextCaution_ColorEscape "${dlg.certimport.nokey}");
99 setFrameColor_Widget(as_Widget(d->keyLabel), uiTextCaution_ColorId); 103 setFrameColor_Widget(as_Widget(d->keyLabel), uiTextCaution_ColorId);
100 } 104 }
101 } 105 }
@@ -104,61 +108,85 @@ static iBool tryImport_CertImportWidget_(iCertImportWidget *d, const iBlock *dat
104 108
105void init_CertImportWidget(iCertImportWidget *d) { 109void init_CertImportWidget(iCertImportWidget *d) {
106 iWidget *w = as_Widget(d); 110 iWidget *w = as_Widget(d);
111 const iMenuItem actions[] = {
112#if defined (iPlatformAppleMobile)
113 { "${dlg.certimport.pickfile}", 0, 0, "certimport.pickfile" },
114 { "---" },
115#endif
116 { "${cancel}" },
117 { uiTextAction_ColorEscape "${dlg.certimport.import}",
118 SDLK_RETURN, KMOD_PRIMARY,
119 "certimport.accept" }
120 };
107 init_Widget(w); 121 init_Widget(w);
108 setId_Widget(w, "certimport"); 122 setId_Widget(w, "certimport");
109 d->cert = NULL; 123 d->cert = NULL;
110 /* This should behave similar to sheets. */ 124 if (isUsingPanelLayout_Mobile()) {
111 useSheetStyle_Widget(w); 125 initPanels_Mobile(w, NULL, (iMenuItem[]){
112 addChildFlags_Widget( 126 { "title id:heading.certimport" },
113 w, 127 { format_CStr("label id:certimport.info text:%s", infoText_) },
114 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.certimport}", NULL)), 128 //{ "padding" },
115 frameless_WidgetFlag); 129 { "label id:certimport.crt nowrap:1 frame:1" },
116 d->info = addChildFlags_Widget(w, iClob(new_LabelWidget(infoText_, NULL)), frameless_WidgetFlag); 130 { "padding arg:0.25" },
117 addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); 131 { "label id:certimport.key nowrap:1 frame:1" },
118 d->crtLabel = new_LabelWidget("", NULL); { 132 { "heading text:${dlg.certimport.notes}" },
133 { "input id:certimport.notes hint:hint.certimport.description noheading:1" },
134 { NULL }
135 }, actions, iElemCount(actions));
136 d->info = findChild_Widget(w, "certimport.info");
137 d->crtLabel = findChild_Widget(w, "certimport.crt");
138 d->keyLabel = findChild_Widget(w, "certimport.key");
139 d->notes = findChild_Widget(w, "certimport.notes");
119 setFont_LabelWidget(d->crtLabel, uiContent_FontId); 140 setFont_LabelWidget(d->crtLabel, uiContent_FontId);
120 addChildFlags_Widget(w, iClob(d->crtLabel), 0);
121 setFrameColor_Widget(as_Widget(d->crtLabel), uiTextCaution_ColorId);
122 }
123 d->keyLabel = new_LabelWidget("", NULL); {
124 setFont_LabelWidget(d->keyLabel, uiContent_FontId); 141 setFont_LabelWidget(d->keyLabel, uiContent_FontId);
125 addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); 142 setFixedSize_Widget(as_Widget(d->crtLabel), init_I2(-1, gap_UI * 12));
126 addChildFlags_Widget(w, iClob(d->keyLabel), 0); 143 setFixedSize_Widget(as_Widget(d->keyLabel), init_I2(-1, gap_UI * 12));
127 setFrameColor_Widget(as_Widget(d->keyLabel), uiTextCaution_ColorId);
128 } 144 }
129 addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); 145 else {
130 /* TODO: Use makeTwoColumnWidget_() */ 146 /* This should behave similar to sheets. */
131 iWidget *page = new_Widget(); { 147 useSheetStyle_Widget(w);
132 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); 148 addChildFlags_Widget(
133 iWidget *headings = addChildFlags_Widget( 149 w,
134 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 150 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.certimport}", NULL)),
135 iWidget *values = addChildFlags_Widget( 151 frameless_WidgetFlag);
136 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 152 d->info = addChildFlags_Widget(w, iClob(new_LabelWidget(infoText_, NULL)), frameless_WidgetFlag);
137// addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.certimport.notes}"))); 153 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
138// addChild_Widget(values, iClob(d->notes = new_InputWidget(0))); 154 d->crtLabel = new_LabelWidget("", NULL); {
139// setHint_InputWidget(d->notes, "${hint.certimport.description}"); 155 setFont_LabelWidget(d->crtLabel, uiContent_FontId);
140 addTwoColumnDialogInputField_Widget( 156 addChildFlags_Widget(w, iClob(d->crtLabel), 0);
141 headings, 157 }
142 values, 158 d->keyLabel = new_LabelWidget("", NULL); {
143 "${dlg.certimport.notes}", 159 setFont_LabelWidget(d->keyLabel, uiContent_FontId);
144 "", 160 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
145 iClob(d->notes = newHint_InputWidget(0, "${hint.certimport.description}"))); 161 addChildFlags_Widget(w, iClob(d->keyLabel), 0);
146 as_Widget(d->notes)->rect.size.x = gap_UI * 70; 162 }
163 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
164 /* TODO: Use makeTwoColumnWidget_() */
165 iWidget *page = new_Widget(); {
166 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
167 iWidget *headings = addChildFlags_Widget(
168 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
169 iWidget *values = addChildFlags_Widget(
170 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
171 addTwoColumnDialogInputField_Widget(
172 headings,
173 values,
174 "${dlg.certimport.notes}",
175 "",
176 iClob(d->notes = newHint_InputWidget(0, "${hint.certimport.description}")));
177 as_Widget(d->notes)->rect.size.x = gap_UI * 70;
178 }
179 addChild_Widget(w, iClob(page));
180 arrange_Widget(w);
181 setFixedSize_Widget(as_Widget(d->crtLabel), init_I2(width_Widget(w) - 6.5 * gap_UI, gap_UI * 12));
182 setFixedSize_Widget(as_Widget(d->keyLabel), init_I2(width_Widget(w) - 6.5 * gap_UI, gap_UI * 12));
183 /* Buttons. */
184 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
185 iWidget *buttons = makeDialogButtons_Widget(actions, iElemCount(actions));
186 addChild_Widget(w, iClob(buttons));
147 } 187 }
148 addChild_Widget(w, iClob(page)); 188 setFrameColor_Widget(as_Widget(d->crtLabel), uiTextCaution_ColorId);
149 arrange_Widget(w); 189 setFrameColor_Widget(as_Widget(d->keyLabel), uiTextCaution_ColorId);
150 setFixedSize_Widget(as_Widget(d->crtLabel), init_I2(width_Widget(w) - 6.5 * gap_UI, gap_UI * 12));
151 setFixedSize_Widget(as_Widget(d->keyLabel), init_I2(width_Widget(w) - 6.5 * gap_UI, gap_UI * 12));
152 /* Buttons. */
153 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
154 iWidget *buttons = makeDialogButtons_Widget(
155 (iMenuItem[]){ { "${cancel}", 0, 0, NULL },
156 { uiTextAction_ColorEscape "${dlg.certimport.import}",
157 SDLK_RETURN,
158 KMOD_PRIMARY,
159 "certimport.accept" } },
160 2);
161 addChild_Widget(w, iClob(buttons));
162 if (deviceType_App() != desktop_AppDeviceType) { 190 if (deviceType_App() != desktop_AppDeviceType) {
163 /* Try auto-pasting. */ 191 /* Try auto-pasting. */
164 postCommand_App("certimport.paste"); 192 postCommand_App("certimport.paste");
@@ -191,6 +219,25 @@ static iBool tryImportFromClipboard_CertImportWidget_(iCertImportWidget *d) {
191 return tryImport_CertImportWidget_(d, collect_Block(newCStr_Block(SDL_GetClipboardText()))); 219 return tryImport_CertImportWidget_(d, collect_Block(newCStr_Block(SDL_GetClipboardText())));
192} 220}
193 221
222static iBool tryImportFromFile_CertImportWidget_(iCertImportWidget *d, const iString *path) {
223 iBool success = iFalse;
224 iFile *f = new_File(path);
225 if (open_File(f, readOnly_FileMode | text_FileMode)) {
226 if (tryImport_CertImportWidget_(d, collect_Block(readAll_File(f)))) {
227 success = iTrue;
228 if (isComplete_CertImportWidget_(d)) {
229 setFocus_Widget(as_Widget(d->notes));
230 }
231 }
232 else {
233 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.certimport.dropped}",
234 "${dlg.certimport.notfound}");
235 }
236 }
237 iRelease(f);
238 return success;
239}
240
194static iBool processEvent_CertImportWidget_(iCertImportWidget *d, const SDL_Event *ev) { 241static iBool processEvent_CertImportWidget_(iCertImportWidget *d, const SDL_Event *ev) {
195 iWidget *w = as_Widget(d); 242 iWidget *w = as_Widget(d);
196 if (ev->type == SDL_KEYDOWN) { 243 if (ev->type == SDL_KEYDOWN) {
@@ -232,21 +279,22 @@ static iBool processEvent_CertImportWidget_(iCertImportWidget *d, const SDL_Even
232 } 279 }
233 return iTrue; 280 return iTrue;
234 } 281 }
235 if (ev->type == SDL_DROPFILE) { 282#if defined (iPlatformAppleMobile)
236 const iString *name = collectNewCStr_String(ev->drop.file); 283 if (isCommand_UserEvent(ev, "certimport.pickfile")) {
237 iFile *f = new_File(name); 284 const char *cmd = command_UserEvent(ev);
238 if (open_File(f, readOnly_FileMode | text_FileMode)) { 285 if (hasLabel_Command(cmd, "path")) {
239 if (tryImport_CertImportWidget_(d, collect_Block(readAll_File(f)))) { 286 const iString *path = collect_String(suffix_Command(cmd, "path"));
240 if (isComplete_CertImportWidget_(d)) { 287 tryImportFromFile_CertImportWidget_(d, path);
241 setFocus_Widget(as_Widget(d->notes)); 288 remove(cstr_String(path)); /* it is a temporary copy */
242 } 289 }
243 } 290 else {
244 else { 291 pickFile_iOS("certimport.pickfile");
245 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.certimport.dropped}",
246 "${dlg.certimport.notfound}");
247 }
248 } 292 }
249 iRelease(f); 293 return iTrue;
294 }
295#endif
296 if (ev->type == SDL_DROPFILE) {
297 tryImportFromFile_CertImportWidget_(d, collectNewCStr_String(ev->drop.file));
250 return iTrue; 298 return iTrue;
251 } 299 }
252 return processEvent_Widget(w, ev); 300 return processEvent_Widget(w, ev);
diff --git a/src/ui/color.c b/src/ui/color.c
index 656de6f0..072c4b3f 100644
--- a/src/ui/color.c
+++ b/src/ui/color.c
@@ -85,6 +85,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
85 const int accentLo = (prefs->accent == cyan_ColorAccent ? teal_ColorId : brown_ColorId); 85 const int accentLo = (prefs->accent == cyan_ColorAccent ? teal_ColorId : brown_ColorId);
86 const int altAccentHi = (prefs->accent == cyan_ColorAccent ? orange_ColorId : cyan_ColorId); 86 const int altAccentHi = (prefs->accent == cyan_ColorAccent ? orange_ColorId : cyan_ColorId);
87 const int altAccentLo = (prefs->accent == cyan_ColorAccent ? brown_ColorId : teal_ColorId); 87 const int altAccentLo = (prefs->accent == cyan_ColorAccent ? brown_ColorId : teal_ColorId);
88 const iColor accentMid = mix_Color(get_Color(accentHi), get_Color(accentLo), 0.5f);
88 const iColor altAccentMid = mix_Color(get_Color(altAccentHi), get_Color(altAccentLo), 0.5f); 89 const iColor altAccentMid = mix_Color(get_Color(altAccentHi), get_Color(altAccentLo), 0.5f);
89 switch (theme) { 90 switch (theme) {
90 case pureBlack_ColorTheme: { 91 case pureBlack_ColorTheme: {
@@ -124,7 +125,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
124 copy_(uiInputTextFocused_ColorId, white_ColorId); 125 copy_(uiInputTextFocused_ColorId, white_ColorId);
125 copy_(uiInputFrame_ColorId, gray25_ColorId); 126 copy_(uiInputFrame_ColorId, gray25_ColorId);
126 copy_(uiInputFrameHover_ColorId, accentHi); 127 copy_(uiInputFrameHover_ColorId, accentHi);
127 set_Color(uiInputFrameFocused_ColorId, altAccentMid); 128 copy_(uiInputFrameFocused_ColorId, accentLo);
128 copy_(uiInputCursor_ColorId, altAccentHi); 129 copy_(uiInputCursor_ColorId, altAccentHi);
129 copy_(uiInputCursorText_ColorId, black_ColorId); 130 copy_(uiInputCursorText_ColorId, black_ColorId);
130 copy_(uiHeading_ColorId, accentHi); 131 copy_(uiHeading_ColorId, accentHi);
@@ -132,8 +133,8 @@ void setThemePalette_Color(enum iColorTheme theme) {
132 copy_(uiIcon_ColorId, accentHi); 133 copy_(uiIcon_ColorId, accentHi);
133 copy_(uiIconHover_ColorId, accentHi); 134 copy_(uiIconHover_ColorId, accentHi);
134 copy_(uiSeparator_ColorId, gray25_ColorId); 135 copy_(uiSeparator_ColorId, gray25_ColorId);
135 copy_(uiMarked_ColorId, altAccentLo); 136 copy_(uiMarked_ColorId, accentLo);
136 copy_(uiMatching_ColorId, accentLo); 137 copy_(uiMatching_ColorId, altAccentLo);
137 break; 138 break;
138 } 139 }
139 default: 140 default:
@@ -177,7 +178,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
177 get_Color(altAccentHi), 0.15f)); 178 get_Color(altAccentHi), 0.15f));
178 copy_(uiInputFrame_ColorId, uiInputBackground_ColorId); 179 copy_(uiInputFrame_ColorId, uiInputBackground_ColorId);
179 copy_(uiInputFrameHover_ColorId, accentHi); 180 copy_(uiInputFrameHover_ColorId, accentHi);
180 set_Color(uiInputFrameFocused_ColorId, altAccentMid); 181 copy_(uiInputFrameFocused_ColorId, accentLo);
181 copy_(uiInputCursor_ColorId, altAccentHi); 182 copy_(uiInputCursor_ColorId, altAccentHi);
182 copy_(uiInputCursorText_ColorId, black_ColorId); 183 copy_(uiInputCursorText_ColorId, black_ColorId);
183 copy_(uiHeading_ColorId, accentHi); 184 copy_(uiHeading_ColorId, accentHi);
@@ -185,8 +186,8 @@ void setThemePalette_Color(enum iColorTheme theme) {
185 copy_(uiIcon_ColorId, accentHi); 186 copy_(uiIcon_ColorId, accentHi);
186 copy_(uiIconHover_ColorId, accentHi); 187 copy_(uiIconHover_ColorId, accentHi);
187 copy_(uiSeparator_ColorId, black_ColorId); 188 copy_(uiSeparator_ColorId, black_ColorId);
188 copy_(uiMarked_ColorId, altAccentLo); 189 copy_(uiMarked_ColorId, accentLo);
189 copy_(uiMatching_ColorId, accentLo); 190 copy_(uiMatching_ColorId, altAccentLo);
190 break; 191 break;
191 } 192 }
192 case light_ColorTheme: 193 case light_ColorTheme:
@@ -227,7 +228,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
227 set_Color(uiInputFrame_ColorId, 228 set_Color(uiInputFrame_ColorId,
228 mix_Color(get_Color(gray50_ColorId), get_Color(gray75_ColorId), 0.5f)); 229 mix_Color(get_Color(gray50_ColorId), get_Color(gray75_ColorId), 0.5f));
229 copy_(uiInputFrameHover_ColorId, accentLo); 230 copy_(uiInputFrameHover_ColorId, accentLo);
230 copy_(uiInputFrameFocused_ColorId, altAccentLo); 231 copy_(uiInputFrameFocused_ColorId, accentLo);
231 copy_(uiInputCursor_ColorId, altAccentLo); 232 copy_(uiInputCursor_ColorId, altAccentLo);
232 copy_(uiInputCursorText_ColorId, white_ColorId); 233 copy_(uiInputCursorText_ColorId, white_ColorId);
233 copy_(uiHeading_ColorId, accentLo); 234 copy_(uiHeading_ColorId, accentLo);
@@ -236,8 +237,8 @@ void setThemePalette_Color(enum iColorTheme theme) {
236 copy_(uiIconHover_ColorId, accentLo); 237 copy_(uiIconHover_ColorId, accentLo);
237 set_Color(uiSeparator_ColorId, 238 set_Color(uiSeparator_ColorId,
238 mix_Color(get_Color(gray50_ColorId), get_Color(gray75_ColorId), 0.5f)); 239 mix_Color(get_Color(gray50_ColorId), get_Color(gray75_ColorId), 0.5f));
239 copy_(uiMarked_ColorId, altAccentHi); 240 copy_(uiMarked_ColorId, accentHi);
240 copy_(uiMatching_ColorId, accentHi); 241 copy_(uiMatching_ColorId, altAccentHi);
241 break; 242 break;
242 case pureWhite_ColorTheme: 243 case pureWhite_ColorTheme:
243 copy_(uiBackground_ColorId, white_ColorId); 244 copy_(uiBackground_ColorId, white_ColorId);
@@ -278,7 +279,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
278 copy_(uiInputTextFocused_ColorId, black_ColorId); 279 copy_(uiInputTextFocused_ColorId, black_ColorId);
279 copy_(uiInputFrame_ColorId, gray50_ColorId); 280 copy_(uiInputFrame_ColorId, gray50_ColorId);
280 copy_(uiInputFrameHover_ColorId, accentLo); 281 copy_(uiInputFrameHover_ColorId, accentLo);
281 copy_(uiInputFrameFocused_ColorId, altAccentLo); 282 copy_(uiInputFrameFocused_ColorId, accentLo);
282 copy_(uiInputCursor_ColorId, altAccentLo); 283 copy_(uiInputCursor_ColorId, altAccentLo);
283 copy_(uiInputCursorText_ColorId, white_ColorId); 284 copy_(uiInputCursorText_ColorId, white_ColorId);
284 copy_(uiHeading_ColorId, accentLo); 285 copy_(uiHeading_ColorId, accentLo);
@@ -287,8 +288,8 @@ void setThemePalette_Color(enum iColorTheme theme) {
287 copy_(uiIconHover_ColorId, accentLo); 288 copy_(uiIconHover_ColorId, accentLo);
288 set_Color(uiSeparator_ColorId, 289 set_Color(uiSeparator_ColorId,
289 mix_Color(get_Color(gray50_ColorId), get_Color(gray75_ColorId), 0.67f)); 290 mix_Color(get_Color(gray50_ColorId), get_Color(gray75_ColorId), 0.67f));
290 copy_(uiMarked_ColorId, altAccentHi); 291 copy_(uiMarked_ColorId, accentHi);
291 copy_(uiMatching_ColorId, accentHi); 292 copy_(uiMatching_ColorId, altAccentHi);
292 break; 293 break;
293 } 294 }
294 set_Color(uiSubheading_ColorId, 295 set_Color(uiSubheading_ColorId,
@@ -481,6 +482,24 @@ const char *escape_Color(int color) {
481 return format_CStr("\v%c", color + asciiBase_ColorEscape); 482 return format_CStr("\v%c", color + asciiBase_ColorEscape);
482} 483}
483 484
485enum iColorId parseEscape_Color(const char *cstr, const char **endp) {
486 enum iColorId color = none_ColorId;
487 if (*cstr == '\v') {
488 cstr++;
489 color = 0;
490 if (*cstr == '\v') {
491 color += asciiExtended_ColorEscape;
492 cstr++;
493 }
494 color += *cstr - asciiBase_ColorEscape;
495 cstr++;
496 }
497 if (endp) {
498 *endp = cstr;
499 }
500 return color;
501}
502
484iHSLColor setSat_HSLColor(iHSLColor d, float sat) { 503iHSLColor setSat_HSLColor(iHSLColor d, float sat) {
485 d.sat = iClamp(sat, 0, 1); 504 d.sat = iClamp(sat, 0, 1);
486 return d; 505 return d;
diff --git a/src/ui/color.h b/src/ui/color.h
index 37ec49eb..b6571c86 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -183,6 +183,7 @@ iLocalDef iBool isRegularText_ColorId(enum iColorId d) {
183#define mask_ColorId 0x7f 183#define mask_ColorId 0x7f
184#define permanent_ColorId 0x80 /* cannot be changed via escapes */ 184#define permanent_ColorId 0x80 /* cannot be changed via escapes */
185#define fillBackground_ColorId 0x100 /* fill background with same color, but alpha 0 */ 185#define fillBackground_ColorId 0x100 /* fill background with same color, but alpha 0 */
186#define opaque_ColorId 0x200
186 187
187#define asciiBase_ColorEscape 33 188#define asciiBase_ColorEscape 33
188#define asciiExtended_ColorEscape (128 - asciiBase_ColorEscape) 189#define asciiExtended_ColorEscape (128 - asciiBase_ColorEscape)
@@ -249,4 +250,4 @@ void setThemePalette_Color (enum iColorTheme theme);
249 250
250iColor ansiForeground_Color (iRangecc escapeSequence, int fallback); 251iColor ansiForeground_Color (iRangecc escapeSequence, int fallback);
251const char * escape_Color (int color); 252const char * escape_Color (int color);
252 253enum iColorId parseEscape_Color (const char *cstr, const char **endp);
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 638906cf..5c1e473f 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -750,7 +750,7 @@ static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
750 if (document_App() != d) { 750 if (document_App() != d) {
751 return 0; 751 return 0;
752 } 752 }
753 if (get_Window()->isDrawFrozen) { 753 if (as_MainWindow(window_Widget(d))->isDrawFrozen) {
754 return 0; 754 return 0;
755 } 755 }
756 static const uint32_t invalidInterval_ = ~0u; 756 static const uint32_t invalidInterval_ = ~0u;
@@ -934,7 +934,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
934 iString *text = collect_String(joinCStr_StringArray(title, " \u2014 ")); 934 iString *text = collect_String(joinCStr_StringArray(title, " \u2014 "));
935 if (setWindow) { 935 if (setWindow) {
936 /* Longest version for the window title, and omit the icon. */ 936 /* Longest version for the window title, and omit the icon. */
937 setTitle_Window(get_Window(), text); 937 setTitle_MainWindow(get_MainWindow(), text);
938 setWindow = iFalse; 938 setWindow = iFalse;
939 } 939 }
940 const iChar siteIcon = siteIcon_GmDocument(d->doc); 940 const iChar siteIcon = siteIcon_GmDocument(d->doc);
@@ -1010,6 +1010,9 @@ static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {
1010} 1010}
1011 1011
1012iBool isPinned_DocumentWidget_(const iDocumentWidget *d) { 1012iBool isPinned_DocumentWidget_(const iDocumentWidget *d) {
1013 if (deviceType_App() == phone_AppDeviceType) {
1014 return iFalse;
1015 }
1013 if (d->flags & otherRootByDefault_DocumentWidgetFlag) { 1016 if (d->flags & otherRootByDefault_DocumentWidgetFlag) {
1014 return iTrue; 1017 return iTrue;
1015 } 1018 }
@@ -1054,9 +1057,12 @@ static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) {
1054 1057
1055void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { 1058void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
1056 setUrl_GmDocument(d->doc, d->mod.url); 1059 setUrl_GmDocument(d->doc, d->mod.url);
1060 const int docWidth = documentWidth_DocumentWidget_(d);
1061 const int outsideMargin = (width_Widget(d) - docWidth) / 2;
1057 setSource_GmDocument(d->doc, 1062 setSource_GmDocument(d->doc,
1058 source, 1063 source,
1059 documentWidth_DocumentWidget_(d), 1064 docWidth,
1065 outsideMargin,
1060 isFinished_GmRequest(d->request) ? final_GmDocumentUpdate 1066 isFinished_GmRequest(d->request) ? final_GmDocumentUpdate
1061 : partial_GmDocumentUpdate); 1067 : partial_GmDocumentUpdate);
1062 documentWasChanged_DocumentWidget_(d); 1068 documentWasChanged_DocumentWidget_(d);
@@ -1165,13 +1171,25 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1165 iString *key = collectNew_String(); 1171 iString *key = collectNew_String();
1166 toString_Sym(SDLK_s, KMOD_PRIMARY, key); 1172 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
1167 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta)); 1173 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta));
1168 makeFooterButtons_DocumentWidget_( 1174 const char *mtype = mediaTypeFromFileExtension_String(d->mod.url);
1169 d, 1175 iArray items;
1170 (iMenuItem[]){ { translateCStr_Lang(download_Icon " " saveToDownloads_Label), 1176 init_Array(&items, sizeof(iMenuItem));
1171 0, 1177 if (iCmpStr(mtype, "application/octet-stream")) {
1172 0, 1178 pushBack_Array(
1173 "document.save" } }, 1179 &items,
1174 1); 1180 &(iMenuItem){ translateCStr_Lang(format_CStr("View as \"%s\"", mtype)),
1181 SDLK_RETURN,
1182 0,
1183 format_CStr("document.setmediatype mime:%s", mtype) });
1184 }
1185 pushBack_Array(
1186 &items,
1187 &(iMenuItem){ translateCStr_Lang(download_Icon " " saveToDownloads_Label),
1188 0,
1189 0,
1190 "document.save" });
1191 makeFooterButtons_DocumentWidget_(d, data_Array(&items), size_Array(&items));
1192 deinit_Array(&items);
1175 break; 1193 break;
1176 } 1194 }
1177 default: 1195 default:
@@ -2254,7 +2272,7 @@ static iBool updateDocumentWidthRetainingScrollPosition_DocumentWidget_(iDocumen
2254 /* TODO: First *fully* visible run? */ 2272 /* TODO: First *fully* visible run? */
2255 voffset = visibleRange_DocumentWidget_(d).start - top_Rect(run->visBounds); 2273 voffset = visibleRange_DocumentWidget_(d).start - top_Rect(run->visBounds);
2256 } 2274 }
2257 setWidth_GmDocument(d->doc, newWidth); 2275 setWidth_GmDocument(d->doc, newWidth, (width_Widget(d) - newWidth) / 2);
2258 documentRunsInvalidated_DocumentWidget_(d); 2276 documentRunsInvalidated_DocumentWidget_(d);
2259 if (runLoc && !keepCenter) { 2277 if (runLoc && !keepCenter) {
2260 run = findRunAtLoc_GmDocument(d->doc, runLoc); 2278 run = findRunAtLoc_GmDocument(d->doc, runLoc);
@@ -2834,7 +2852,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2834 setUrl_UploadWidget(upload, d->mod.url); 2852 setUrl_UploadWidget(upload, d->mod.url);
2835 setResponseViewer_UploadWidget(upload, d); 2853 setResponseViewer_UploadWidget(upload, d);
2836 addChild_Widget(get_Root()->widget, iClob(upload)); 2854 addChild_Widget(get_Root()->widget, iClob(upload));
2837 finalizeSheet_Mobile(as_Widget(upload)); 2855// finalizeSheet_Mobile(as_Widget(upload));
2856 setupSheetTransition_Mobile(as_Widget(upload), iTrue);
2838 postRefresh_App(); 2857 postRefresh_App();
2839 } 2858 }
2840 return iTrue; 2859 return iTrue;
@@ -3109,7 +3128,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3109 makeQuestion_Widget( 3128 makeQuestion_Widget(
3110 uiHeading_ColorEscape "${heading.import.bookmarks}", 3129 uiHeading_ColorEscape "${heading.import.bookmarks}",
3111 formatCStrs_Lang("dlg.import.found.n", count), 3130 formatCStrs_Lang("dlg.import.found.n", count),
3112 (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, 3131 (iMenuItem[]){ { "${cancel}" },
3113 { format_CStr(cstrCount_Lang("dlg.import.add.n", (int) count), 3132 { format_CStr(cstrCount_Lang("dlg.import.add.n", (int) count),
3114 uiTextAction_ColorEscape, 3133 uiTextAction_ColorEscape,
3115 count), 3134 count),
@@ -3173,6 +3192,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3173 document_App() == d) { 3192 document_App() == d) {
3174 return handleSwipe_DocumentWidget_(d, cmd); 3193 return handleSwipe_DocumentWidget_(d, cmd);
3175 } 3194 }
3195 else if (equal_Command(cmd, "document.setmediatype") && document_App() == d) {
3196 setUrlAndSource_DocumentWidget(d, d->mod.url, string_Command(cmd, "mime"), &d->sourceContent);
3197 return iTrue;
3198 }
3176 return iFalse; 3199 return iFalse;
3177} 3200}
3178 3201
@@ -3277,7 +3300,7 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev
3277 d->playerMenu = makeMenu_Widget( 3300 d->playerMenu = makeMenu_Widget(
3278 as_Widget(d), 3301 as_Widget(d),
3279 (iMenuItem[]){ 3302 (iMenuItem[]){
3280 { cstrCollect_String(metadataLabel_Player(plr)), 0, 0, NULL }, 3303 { cstrCollect_String(metadataLabel_Player(plr)) },
3281 }, 3304 },
3282 1); 3305 1);
3283 openMenu_Widget(d->playerMenu, bottomLeft_Rect(ui.menuRect)); 3306 openMenu_Widget(d->playerMenu, bottomLeft_Rect(ui.menuRect));
@@ -3582,7 +3605,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3582 pushBackN_Array( 3605 pushBackN_Array(
3583 &items, 3606 &items,
3584 (iMenuItem[]){ 3607 (iMenuItem[]){
3585 { "---", 0, 0, NULL }, 3608 { "---" },
3586 { isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}", 3609 { isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}",
3587 0, 3610 0,
3588 0, 3611 0,
@@ -3593,7 +3616,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3593 linkLabel_GmDocument(d->doc, d->contextLink->linkId)); 3616 linkLabel_GmDocument(d->doc, d->contextLink->linkId));
3594 urlEncodeSpaces_String(linkLabel); 3617 urlEncodeSpaces_String(linkLabel);
3595 pushBackN_Array(&items, 3618 pushBackN_Array(&items,
3596 (iMenuItem[]){ { "---", 0, 0, NULL }, 3619 (iMenuItem[]){ { "---" },
3597 { "${link.copy}", 0, 0, "document.copylink" }, 3620 { "${link.copy}", 0, 0, "document.copylink" },
3598 { bookmark_Icon " ${link.bookmark}", 3621 { bookmark_Icon " ${link.bookmark}",
3599 0, 3622 0,
@@ -3605,7 +3628,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3605 3); 3628 3);
3606 if (isNative && d->contextLink->mediaType != download_GmRunMediaType) { 3629 if (isNative && d->contextLink->mediaType != download_GmRunMediaType) {
3607 pushBackN_Array(&items, (iMenuItem[]){ 3630 pushBackN_Array(&items, (iMenuItem[]){
3608 { "---", 0, 0, NULL }, 3631 { "---" },
3609 { download_Icon " ${link.download}", 0, 0, "document.downloadlink" }, 3632 { download_Icon " ${link.download}", 0, 0, "document.downloadlink" },
3610 }, 2); 3633 }, 2);
3611 } 3634 }
@@ -3648,17 +3671,17 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3648 { "${menu.forward}", navigateForward_KeyShortcut, "navigate.forward" }, 3671 { "${menu.forward}", navigateForward_KeyShortcut, "navigate.forward" },
3649 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" }, 3672 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
3650 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" }, 3673 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
3651 { "---", 0, 0, NULL }, 3674 { "---" },
3652 { reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" }, 3675 { reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" },
3653 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" }, 3676 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" },
3654 { "---", 0, 0, NULL }, 3677 { "---" },
3655 { bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, 3678 { bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
3656 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, 3679 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
3657 { "---", 0, 0, NULL }, 3680 { "---" },
3658 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, 3681 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
3659 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, 3682 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
3660 { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, 3683 { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" },
3661 { "---", 0, 0, NULL }, 3684 { "---" },
3662 { "${menu.page.copyurl}", 0, 0, "document.copylink" } }, 3685 { "${menu.page.copyurl}", 0, 0, "document.copylink" } },
3663 15); 3686 15);
3664 if (isEmpty_Range(&d->selectMark)) { 3687 if (isEmpty_Range(&d->selectMark)) {
@@ -3834,7 +3857,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3834 } 3857 }
3835 d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){ 3858 d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){
3836 { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" }, 3859 { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" },
3837 { "---", 0, 0, NULL }, 3860 { "---" },
3838 { close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" }, 3861 { close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" },
3839 }, 3); 3862 }, 3);
3840 setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue); 3863 setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue);
@@ -3927,7 +3950,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3927 uiTextAction_ColorEscape, 3950 uiTextAction_ColorEscape,
3928 cstr_String(url)), 3951 cstr_String(url)),
3929 (iMenuItem[]){ 3952 (iMenuItem[]){
3930 { "${cancel}", 0, 0, NULL }, 3953 { "${cancel}" },
3931 { uiTextCaution_ColorEscape "${dlg.openlink}", 3954 { uiTextCaution_ColorEscape "${dlg.openlink}",
3932 0, 0, format_CStr("!open default:1 url:%s", cstr_String(url)) } }, 3955 0, 0, format_CStr("!open default:1 url:%s", cstr_String(url)) } },
3933 2); 3956 2);
@@ -4574,23 +4597,6 @@ static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) {
4574 } 4597 }
4575} 4598}
4576 4599
4577static void drawPin_(iPaint *p, iRect rangeRect, int dir) {
4578 const int pinColor = tmQuote_ColorId;
4579 const int height = height_Rect(rangeRect);
4580 iRect pin;
4581 if (dir == 0) {
4582 pin = (iRect){ add_I2(topLeft_Rect(rangeRect), init_I2(-gap_UI / 4, -gap_UI)),
4583 init_I2(gap_UI / 2, height + gap_UI) };
4584 }
4585 else {
4586 pin = (iRect){ addX_I2(topRight_Rect(rangeRect), -gap_UI / 4),
4587 init_I2(gap_UI / 2, height + gap_UI) };
4588 }
4589 fillRect_Paint(p, pin, pinColor);
4590 fillRect_Paint(p, initCentered_Rect(dir == 0 ? topMid_Rect(pin) : bottomMid_Rect(pin),
4591 init1_I2(gap_UI * 2)), pinColor);
4592}
4593
4594static void extend_GmRunRange_(iGmRunRange *runs) { 4600static void extend_GmRunRange_(iGmRunRange *runs) {
4595 if (runs->start) { 4601 if (runs->start) {
4596 runs->start--; 4602 runs->start--;
@@ -4834,8 +4840,8 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
4834 SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE); 4840 SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
4835 /* Selection range pins. */ 4841 /* Selection range pins. */
4836 if (isTouchSelecting) { 4842 if (isTouchSelecting) {
4837 drawPin_(&ctx.paint, ctx.firstMarkRect, 0); 4843 drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId);
4838 drawPin_(&ctx.paint, ctx.lastMarkRect, 1); 4844 drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId);
4839 } 4845 }
4840 } 4846 }
4841 drawMedia_DocumentWidget_(d, &ctx.paint); 4847 drawMedia_DocumentWidget_(d, &ctx.paint);
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 690107a2..874cf2b5 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
27#include "keys.h" 27#include "keys.h"
28#include "prefs.h" 28#include "prefs.h"
29#include "lang.h" 29#include "lang.h"
30#include "touch.h"
30#include "app.h" 31#include "app.h"
31 32
32#include <the_Foundation/array.h> 33#include <the_Foundation/array.h>
@@ -178,19 +179,23 @@ static void deinit_InputUndo_(iInputUndo *d) {
178} 179}
179 180
180enum iInputWidgetFlag { 181enum iInputWidgetFlag {
181 isSensitive_InputWidgetFlag = iBit(1), 182 isSensitive_InputWidgetFlag = iBit(1),
182 isUrl_InputWidgetFlag = iBit(2), /* affected by decoding preference */ 183 isUrl_InputWidgetFlag = iBit(2), /* affected by decoding preference */
183 enterPressed_InputWidgetFlag = iBit(3), 184 enterPressed_InputWidgetFlag = iBit(3),
184 selectAllOnFocus_InputWidgetFlag = iBit(4), 185 selectAllOnFocus_InputWidgetFlag = iBit(4),
185 notifyEdits_InputWidgetFlag = iBit(5), 186 notifyEdits_InputWidgetFlag = iBit(5),
186 eatEscape_InputWidgetFlag = iBit(6), 187 eatEscape_InputWidgetFlag = iBit(6),
187 isMarking_InputWidgetFlag = iBit(7), 188 isMarking_InputWidgetFlag = iBit(7),
188 markWords_InputWidgetFlag = iBit(8), 189 markWords_InputWidgetFlag = iBit(8),
189 needUpdateBuffer_InputWidgetFlag = iBit(9), 190 needUpdateBuffer_InputWidgetFlag = iBit(9),
190 enterKeyEnabled_InputWidgetFlag = iBit(10), 191 enterKeyEnabled_InputWidgetFlag = iBit(10),
191 lineBreaksEnabled_InputWidgetFlag= iBit(11), 192 lineBreaksEnabled_InputWidgetFlag = iBit(11),
192 needBackup_InputWidgetFlag = iBit(12), 193 needBackup_InputWidgetFlag = iBit(12),
193 useReturnKeyBehavior_InputWidgetFlag = iBit(13), 194 useReturnKeyBehavior_InputWidgetFlag = iBit(13),
195 //touchBehavior_InputWidgetFlag = iBit(14), /* different behavior depending on interaction method */
196 dragCursor_InputWidgetFlag = iBit(14),
197 dragMarkerStart_InputWidgetFlag = iBit(15),
198 dragMarkerEnd_InputWidgetFlag = iBit(16),
194}; 199};
195 200
196/*----------------------------------------------------------------------------------------------*/ 201/*----------------------------------------------------------------------------------------------*/
@@ -216,6 +221,10 @@ struct Impl_InputWidget {
216 iArray undoStack; 221 iArray undoStack;
217 int font; 222 int font;
218 iClick click; 223 iClick click;
224 uint32_t tapStartTime;
225 uint32_t lastTapTime;
226 iInt2 lastTapPos;
227 int tapCount;
219 int wheelAccum; 228 int wheelAccum;
220 int cursorVis; 229 int cursorVis;
221 uint32_t timer; 230 uint32_t timer;
@@ -459,14 +468,54 @@ static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) {
459 }; 468 };
460} 469}
461 470
462static iInt2 relativeCursorCoord_InputWidget_(const iInputWidget *d) { 471static iRangei visibleLineRange_InputWidget_(const iInputWidget *d) {
463 /* Relative to the start of the line on which the cursor is. */ 472 iRangei vis = { -1, -1 };
464 iWrapText wt = wrap_InputWidget_(d, d->cursor.y); 473 /* Determine which lines are in the potentially visible range. */
465 wt.hitChar = wt.text.start + d->cursor.x; 474 for (int i = 0; i < size_Array(&d->lines); i++) {
475 const iInputLine *line = constAt_Array(&d->lines, i);
476 if (vis.start < 0 && line->wrapLines.end > d->visWrapLines.start) {
477 vis.start = vis.end = i;
478 }
479 if (line->wrapLines.start < d->visWrapLines.end) {
480 vis.end = i + 1;
481 }
482 else break;
483 }
484 iAssert(isEmpty_Range(&vis) || (vis.start >= 0 && vis.end >= vis.start));
485 return vis;
486}
487
488static iInt2 relativeCoordOnLine_InputWidget_(const iInputWidget *d, iInt2 pos) {
489 /* Relative to the start of the line on which the position is. */
490 iWrapText wt = wrap_InputWidget_(d, pos.y);
491 wt.hitChar = wt.text.start + pos.x;
466 measure_WrapText(&wt, d->font); 492 measure_WrapText(&wt, d->font);
467 return wt.hitAdvance_out; 493 return wt.hitAdvance_out;
468} 494}
469 495
496static iInt2 cursorToWindowCoord_InputWidget_(const iInputWidget *d, iInt2 pos, iBool *isInsideBounds) {
497 /* Maps a cursor XY position to a window coordinate. */
498 const iRect bounds = contentBounds_InputWidget_(d);
499 iInt2 wc = addY_I2(topLeft_Rect(bounds), visLineOffsetY_InputWidget_(d));
500 iRangei visLines = visibleLineRange_InputWidget_(d);
501 if (!contains_Range(&visLines, pos.y)) {
502 /* This line is not visible. */
503 *isInsideBounds = iFalse;
504 return zero_I2();
505 }
506 for (int i = visLines.start; i < pos.y; i++) {
507 wc.y += lineHeight_Text(d->font) * numWrapLines_InputLine_(line_InputWidget_(d, i));
508 }
509 const iInputLine *line = line_InputWidget_(d, pos.y);
510 addv_I2(&wc, relativeCoordOnLine_InputWidget_(d, pos));
511 *isInsideBounds = contains_Rect(bounds, wc);
512 return wc;
513}
514
515static iInt2 relativeCursorCoord_InputWidget_(const iInputWidget *d) {
516 return relativeCoordOnLine_InputWidget_(d, d->cursor);
517}
518
470static void updateVisible_InputWidget_(iInputWidget *d) { 519static void updateVisible_InputWidget_(iInputWidget *d) {
471 const int totalWraps = numWrapLines_InputWidget_(d); 520 const int totalWraps = numWrapLines_InputWidget_(d);
472 const int visWraps = iClamp(totalWraps, d->minWrapLines, d->maxWrapLines); 521 const int visWraps = iClamp(totalWraps, d->minWrapLines, d->maxWrapLines);
@@ -492,6 +541,8 @@ static void updateVisible_InputWidget_(iInputWidget *d) {
492 d->visWrapLines.start = 0; 541 d->visWrapLines.start = 0;
493 d->visWrapLines.end = 1; 542 d->visWrapLines.end = 1;
494 } 543 }
544// printf("[InputWidget %p] total:%d viswrp:%d cur:%d vis:%d..%d\n",
545// d, totalWraps, visWraps, d->cursor.y, d->visWrapLines.start, d->visWrapLines.end);
495} 546}
496 547
497static void showCursor_InputWidget_(iInputWidget *d) { 548static void showCursor_InputWidget_(iInputWidget *d) {
@@ -542,8 +593,10 @@ static int contentHeight_InputWidget_(const iInputWidget *d) {
542} 593}
543 594
544static void updateTextInputRect_InputWidget_(const iInputWidget *d) { 595static void updateTextInputRect_InputWidget_(const iInputWidget *d) {
596#if !defined (iPlatformAppleMobile)
545 const iRect bounds = bounds_Widget(constAs_Widget(d)); 597 const iRect bounds = bounds_Widget(constAs_Widget(d));
546 SDL_SetTextInputRect(&(SDL_Rect){ bounds.pos.x, bounds.pos.y, bounds.size.x, bounds.size.y }); 598 SDL_SetTextInputRect(&(SDL_Rect){ bounds.pos.x, bounds.pos.y, bounds.size.x, bounds.size.y });
599#endif
547} 600}
548 601
549static void updateMetrics_InputWidget_(iInputWidget *d) { 602static void updateMetrics_InputWidget_(iInputWidget *d) {
@@ -629,7 +682,7 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
629 init_Widget(w); 682 init_Widget(w);
630 d->validator = NULL; 683 d->validator = NULL;
631 d->validatorContext = NULL; 684 d->validatorContext = NULL;
632 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag | touchDrag_WidgetFlag, iTrue); 685 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue);
633#if defined (iPlatformMobile) 686#if defined (iPlatformMobile)
634 setFlags_Widget(w, extraPadding_WidgetFlag, iTrue); 687 setFlags_Widget(w, extraPadding_WidgetFlag, iTrue);
635#endif 688#endif
@@ -659,6 +712,8 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
659 splitToLines_(&iStringLiteral(""), &d->lines); 712 splitToLines_(&iStringLiteral(""), &d->lines);
660 setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue); /* resizes its own height */ 713 setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue); /* resizes its own height */
661 init_Click(&d->click, d, SDL_BUTTON_LEFT); 714 init_Click(&d->click, d, SDL_BUTTON_LEFT);
715 d->lastTapTime = 0;
716 d->tapCount = 0;
662 d->wheelAccum = 0; 717 d->wheelAccum = 0;
663 d->timer = 0; 718 d->timer = 0;
664 d->cursorVis = 0; 719 d->cursorVis = 0;
@@ -753,6 +808,10 @@ const iString *text_InputWidget(const iInputWidget *d) {
753 return collectNew_String(); 808 return collectNew_String();
754} 809}
755 810
811int font_InputWidget(const iInputWidget *d) {
812 return d->font;
813}
814
756iInputWidgetContentPadding contentPadding_InputWidget(const iInputWidget *d) { 815iInputWidgetContentPadding contentPadding_InputWidget(const iInputWidget *d) {
757 return (iInputWidgetContentPadding){ d->leftPadding, d->rightPadding }; 816 return (iInputWidgetContentPadding){ d->leftPadding, d->rightPadding };
758} 817}
@@ -764,6 +823,7 @@ void setMaxLen_InputWidget(iInputWidget *d, size_t maxLen) {
764} 823}
765 824
766void setLineLimits_InputWidget(iInputWidget *d, int minLines, int maxLines) { 825void setLineLimits_InputWidget(iInputWidget *d, int minLines, int maxLines) {
826 maxLines = iMax(minLines, maxLines);
767 if (d->minWrapLines != minLines || d->maxWrapLines != maxLines) { 827 if (d->minWrapLines != minLines || d->maxWrapLines != maxLines) {
768 d->minWrapLines = minLines; 828 d->minWrapLines = minLines;
769 d->maxWrapLines = maxLines; 829 d->maxWrapLines = maxLines;
@@ -822,23 +882,6 @@ static iBool isHintVisible_InputWidget_(const iInputWidget *d) {
822 return !isEmpty_String(&d->hint) && isEmpty_InputWidget_(d); 882 return !isEmpty_String(&d->hint) && isEmpty_InputWidget_(d);
823} 883}
824 884
825static iRangei visibleLineRange_InputWidget_(const iInputWidget *d) {
826 iRangei vis = { -1, -1 };
827 /* Determine which lines are in the potentially visible range. */
828 for (int i = 0; i < size_Array(&d->lines); i++) {
829 const iInputLine *line = constAt_Array(&d->lines, i);
830 if (vis.start < 0 && line->wrapLines.end > d->visWrapLines.start) {
831 vis.start = vis.end = i;
832 }
833 if (line->wrapLines.start < d->visWrapLines.end) {
834 vis.end = i + 1;
835 }
836 else break;
837 }
838 iAssert(isEmpty_Range(&vis) || (vis.start >= 0 && vis.end >= vis.start));
839 return vis;
840}
841
842static void updateBuffered_InputWidget_(iInputWidget *d) { 885static void updateBuffered_InputWidget_(iInputWidget *d) {
843 invalidateBuffered_InputWidget_(d); 886 invalidateBuffered_InputWidget_(d);
844 if (isHintVisible_InputWidget_(d)) { 887 if (isHintVisible_InputWidget_(d)) {
@@ -990,7 +1033,7 @@ void begin_InputWidget(iInputWidget *d) {
990 d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; 1033 d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end };
991 d->cursor = cursorMax_InputWidget_(d); 1034 d->cursor = cursorMax_InputWidget_(d);
992 } 1035 }
993 else { 1036 else if (~d->inFlags & isMarking_InputWidgetFlag) {
994 iZap(d->mark); 1037 iZap(d->mark);
995 } 1038 }
996 enableEditorKeysInMenus_(iFalse); 1039 enableEditorKeysInMenus_(iFalse);
@@ -1010,9 +1053,10 @@ void end_InputWidget(iInputWidget *d, iBool accept) {
1010 splitToLines_(&d->oldText, &d->lines); 1053 splitToLines_(&d->oldText, &d->lines);
1011 } 1054 }
1012 d->inFlags |= needUpdateBuffer_InputWidgetFlag; 1055 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
1056 d->inFlags &= ~isMarking_InputWidgetFlag;
1013 startOrStopCursorTimer_InputWidget_(d, iFalse); 1057 startOrStopCursorTimer_InputWidget_(d, iFalse);
1014 SDL_StopTextInput(); 1058 SDL_StopTextInput();
1015 setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag, iFalse); 1059 setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag | touchDrag_WidgetFlag, iFalse);
1016 const char *id = cstr_String(id_Widget(as_Widget(d))); 1060 const char *id = cstr_String(id_Widget(as_Widget(d)));
1017 if (!*id) id = "_"; 1061 if (!*id) id = "_";
1018 refresh_Widget(w); 1062 refresh_Widget(w);
@@ -1314,9 +1358,10 @@ static iInt2 coordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) {
1314 if (relCoord.y < 0) { 1358 if (relCoord.y < 0) {
1315 return zero_I2(); 1359 return zero_I2();
1316 } 1360 }
1317 if (relCoord.y >= height_Rect(bounds)) { 1361// if (relCoord.y >= height_Rect(bounds)) {
1318 return cursorMax_InputWidget_(d); 1362// printf("relCoord > bounds.h\n"); fflush(stdout);
1319 } 1363// return cursorMax_InputWidget_(d);
1364// }
1320 iWrapText wrapText = { 1365 iWrapText wrapText = {
1321 .maxWidth = d->maxLen == 0 ? width_Rect(bounds) : unlimitedWidth_InputWidget_, 1366 .maxWidth = d->maxLen == 0 ? width_Rect(bounds) : unlimitedWidth_InputWidget_,
1322 .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), 1367 .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode),
@@ -1442,6 +1487,374 @@ static iBool checkAcceptMods_InputWidget_(const iInputWidget *d, int mods) {
1442 return mods == 0; 1487 return mods == 0;
1443} 1488}
1444 1489
1490enum iEventResult {
1491 ignored_EventResult = 0, /* event was not processed */
1492 false_EventResult = 1, /* event was processed but other widgets can still process it, too*/
1493 true_EventResult = 2, /* event was processed and should not be passed on */
1494};
1495
1496static void markWordAtCursor_InputWidget_(iInputWidget *d) {
1497 d->mark.start = d->mark.end = cursorToIndex_InputWidget_(d, d->cursor);
1498 extendRange_InputWidget_(d, &d->mark.start, -1);
1499 extendRange_InputWidget_(d, &d->mark.end, +1);
1500 d->initialMark = d->mark;
1501}
1502
1503static void showClipMenu_(iInt2 coord) {
1504 iWidget *clipMenu = findWidget_App("clipmenu");
1505 if (isVisible_Widget(clipMenu)) {
1506 closeMenu_Widget(clipMenu);
1507 }
1508 else {
1509 openMenuFlags_Widget(clipMenu, coord, iFalse);
1510 }
1511}
1512
1513static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1514 iWidget *w = as_Widget(d);
1515 if (ev->type == SDL_MOUSEMOTION && (isHover_Widget(d) || flags_Widget(w) & keepOnTop_WidgetFlag)) {
1516 const iInt2 coord = init_I2(ev->motion.x, ev->motion.y);
1517 const iInt2 inner = windowToInner_Widget(w, coord);
1518 setCursor_Window(get_Window(),
1519 inner.x >= 2 * gap_UI + d->leftPadding &&
1520 inner.x < width_Widget(w) - d->rightPadding
1521 ? SDL_SYSTEM_CURSOR_IBEAM
1522 : SDL_SYSTEM_CURSOR_ARROW);
1523 }
1524 if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT &&
1525 contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
1526 showClipMenu_(mouseCoord_Window(get_Window(), ev->button.which));
1527 return iTrue;
1528 }
1529 switch (processEvent_Click(&d->click, ev)) {
1530 case none_ClickResult:
1531 break;
1532 case started_ClickResult: {
1533 setFocus_Widget(w);
1534 const iInt2 oldCursor = d->cursor;
1535 setCursor_InputWidget(d, coordCursor_InputWidget_(d, pos_Click(&d->click)));
1536 if (keyMods_Sym(modState_Keys()) == KMOD_SHIFT) {
1537 d->mark = d->initialMark = (iRanges){
1538 cursorToIndex_InputWidget_(d, oldCursor),
1539 cursorToIndex_InputWidget_(d, d->cursor)
1540 };
1541 d->inFlags |= isMarking_InputWidgetFlag;
1542 }
1543 else {
1544 iZap(d->mark);
1545 iZap(d->initialMark);
1546 d->inFlags &= ~(isMarking_InputWidgetFlag | markWords_InputWidgetFlag);
1547 if (d->click.count == 2) {
1548 d->inFlags |= isMarking_InputWidgetFlag | markWords_InputWidgetFlag;
1549 markWordAtCursor_InputWidget_(d);
1550 refresh_Widget(w);
1551 }
1552 if (d->click.count == 3) {
1553 selectAll_InputWidget(d);
1554 }
1555 }
1556 refresh_Widget(d);
1557 return true_EventResult;
1558 }
1559 case aborted_ClickResult:
1560 d->inFlags &= ~isMarking_InputWidgetFlag;
1561 return true_EventResult;
1562 case drag_ClickResult:
1563 d->cursor = coordCursor_InputWidget_(d, pos_Click(&d->click));
1564 showCursor_InputWidget_(d);
1565 if (~d->inFlags & isMarking_InputWidgetFlag) {
1566 d->inFlags |= isMarking_InputWidgetFlag;
1567 d->mark.start = cursorToIndex_InputWidget_(d, d->cursor);
1568 }
1569 d->mark.end = cursorToIndex_InputWidget_(d, d->cursor);
1570 if (d->inFlags & markWords_InputWidgetFlag) {
1571 const iBool isFwd = d->mark.end >= d->mark.start;
1572 extendRange_InputWidget_(d, &d->mark.end, isFwd ? +1 : -1);
1573 d->mark.start = isFwd ? d->initialMark.start : d->initialMark.end;
1574 }
1575 refresh_Widget(w);
1576 return true_EventResult;
1577 case finished_ClickResult:
1578 d->inFlags &= ~isMarking_InputWidgetFlag;
1579 return true_EventResult;
1580 }
1581 if (ev->type == SDL_MOUSEMOTION && flags_Widget(w) & keepOnTop_WidgetFlag) {
1582 const iInt2 coord = init_I2(ev->motion.x, ev->motion.y);
1583 if (contains_Click(&d->click, coord)) {
1584 return true_EventResult;
1585 }
1586 }
1587 return ignored_EventResult;
1588}
1589
1590static iInt2 touchCoordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) {
1591 /* Clamp to the bounds so the cursor doesn't wrap at the ends. */
1592 iRect bounds = shrunk_Rect(contentBounds_InputWidget_(d), one_I2());
1593 bounds.size.y = iMini(numWrapLines_InputWidget_(d), d->maxWrapLines) * lineHeight_Text(d->font) - 2;
1594 return coordCursor_InputWidget_(d, min_I2(bottomRight_Rect(bounds),
1595 max_I2(coord, topLeft_Rect(bounds))));
1596}
1597
1598static iBool isInsideMark_InputWidget_(const iInputWidget *d, size_t pos) {
1599 const iRanges mark = mark_InputWidget_(d);
1600 return contains_Range(&mark, pos);
1601}
1602
1603static int distanceToPos_InputWidget_(const iInputWidget *d, iInt2 uiCoord, iInt2 textPos) {
1604 iBool isInside;
1605 const iInt2 winCoord = cursorToWindowCoord_InputWidget_(d, textPos, &isInside);
1606 if (!isInside) {
1607 return INT_MAX;
1608 }
1609 return dist_I2(addY_I2(winCoord, lineHeight_Text(d->font) / 2), uiCoord);
1610}
1611
1612static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1613 iWidget *w = as_Widget(d);
1614 /*
1615 + first tap to focus & select all/place cursor
1616 + focused tap to place cursor
1617 - drag cursor to move it
1618 - double-click to select a word
1619 - drag to move selection handles
1620 - long-press for context menu: copy, paste, delete, select all, deselect
1621 - double-click and hold to select words
1622 - triple-click to select all
1623 - drag/wheel elsewhere to scroll (contents or overflow), no change in focus
1624 */
1625// if (ev->type != SDL_MOUSEBUTTONUP && ev->type != SDL_MOUSEBUTTONDOWN &&
1626// ev->type != SDL_MOUSEWHEEL && ev->type != SDL_MOUSEMOTION &&
1627// !(ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) &&
1628// !(ev->type == SDL_USEREVENT && ev->user.code == widgetTouchEnds_UserEventCode)) {
1629// return ignored_EventResult;
1630// }
1631 if (isFocused_Widget(w)) {
1632 if (ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) {
1633 d->lastTapTime = d->tapStartTime;
1634 d->tapStartTime = SDL_GetTicks();
1635 const int tapDist = dist_I2(latestPosition_Touch(), d->lastTapPos);
1636 d->lastTapPos = latestPosition_Touch();
1637// printf("[%p] tap start time: %u (%u) %d\n", w, d->tapStartTime, d->tapStartTime - d->lastTapTime, tapDist);
1638 if (d->tapStartTime - d->lastTapTime < 400 && tapDist < gap_UI * 4) {
1639 d->tapCount++;
1640// printf("[%p] >> tap count: %d\n", w, d->tapCount);
1641 }
1642 else {
1643 d->tapCount = 0;
1644 }
1645 if (!isEmpty_Range(&d->mark)) {
1646 const int dist[2] = {
1647 distanceToPos_InputWidget_(d, latestPosition_Touch(),
1648 indexToCursor_InputWidget_(d, d->mark.start)),
1649 distanceToPos_InputWidget_(d, latestPosition_Touch(),
1650 indexToCursor_InputWidget_(d, d->mark.end))
1651 };
1652 if (dist[0] < dist[1]) {
1653// printf("[%p] begin marker start drag\n", w);
1654 d->inFlags |= dragMarkerStart_InputWidgetFlag;
1655 }
1656 else {
1657// printf("[%p] begin marker end drag\n", w);
1658 d->inFlags |= dragMarkerEnd_InputWidgetFlag;
1659 }
1660 d->inFlags |= isMarking_InputWidgetFlag;
1661 setFlags_Widget(w, touchDrag_WidgetFlag, iTrue);
1662 }
1663 else {
1664 const int dist = distanceToPos_InputWidget_(d, latestPosition_Touch(), d->cursor);
1665// printf("[%p] tap dist: %d\n", w, dist);
1666 if (dist < gap_UI * 10) {
1667// printf("[%p] begin cursor drag\n", w);
1668 setFlags_Widget(w, touchDrag_WidgetFlag, iTrue);
1669 d->inFlags |= dragCursor_InputWidgetFlag;
1670// d->inFlags |= touchBehavior_InputWidgetFlag;
1671// setMouseGrab_Widget(w);
1672// return iTrue;
1673 }
1674 }
1675// if (~d->inFlags & selectAllOnFocus_InputWidgetFlag) {
1676// d->cursor = coordCursor_InputWidget_(d, pos_Click(&d->click));
1677// showCursor_InputWidget_(d);
1678// }
1679 return true_EventResult;
1680 }
1681 }
1682#if 0
1683 else if (isFocused_Widget(w)) {
1684 if (ev->type == SDL_MOUSEMOTION) {
1685 if (~d->inFlags & touchBehavior_InputWidgetFlag) {
1686 const iInt2 curPos = relativeCursorCoord_InputWidget_(d);
1687 const iInt2 relClick = sub_I2(pos_Click(&d->click),
1688 topLeft_Rect(contentBounds_InputWidget_(d)));
1689 if (dist_I2(curPos, relClick) < gap_UI * 8) {
1690// printf("tap on cursor!\n");
1691 setFlags_Widget(w, touchDrag_WidgetFlag, iTrue);
1692 d->inFlags |= touchBehavior_InputWidgetFlag;
1693// printf("[Input] begin cursor drag\n");
1694 setMouseGrab_Widget(w);
1695 return iTrue;
1696 }
1697 }
1698 else if (ev->motion.x > 0 && ev->motion.y > 0) {
1699// printf("[Input] cursor being dragged\n");
1700 iRect bounds = shrunk_Rect(contentBounds_InputWidget_(d), one_I2());
1701 bounds.size.y = iMini(numWrapLines_InputWidget_(d), d->maxWrapLines) * lineHeight_Text(d->font) - 2;
1702 iInt2 mpos = init_I2(ev->motion.x, ev->motion.y);
1703 mpos = min_I2(bottomRight_Rect(bounds), max_I2(mpos, topLeft_Rect(bounds)));
1704 d->cursor = coordCursor_InputWidget_(d, mpos);
1705 showCursor_InputWidget_(d);
1706 refresh_Widget(w);
1707 return iTrue;
1708 }
1709 }
1710 if (d->inFlags & touchBehavior_InputWidgetFlag) {
1711 if (ev->type == SDL_MOUSEBUTTONUP ||
1712 (ev->type == SDL_USEREVENT && ev->user.code == widgetTouchEnds_UserEventCode)) {
1713 d->inFlags &= ~touchBehavior_InputWidgetFlag;
1714 setFlags_Widget(w, touchDrag_WidgetFlag, iFalse);
1715 setMouseGrab_Widget(NULL);
1716// printf("[Input] touch ends\n");
1717 return iFalse;
1718 }
1719 }
1720 }
1721#endif
1722#if 1
1723 if ((ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) &&
1724 ev->button.button == SDL_BUTTON_RIGHT && contains_Widget(w, latestPosition_Touch())) {
1725 if (ev->type == SDL_MOUSEBUTTONDOWN) {
1726 /*if (isFocused_Widget(w)) {
1727 d->inFlags |= isMarking_InputWidgetFlag;
1728 d->cursor = touchCoordCursor_InputWidget_(d, latestPosition_Touch());
1729 markWordAtCursor_InputWidget_(d);
1730 refresh_Widget(d);
1731 return true_EventResult;
1732 }*/
1733 setFocus_Widget(w);
1734 d->inFlags |= isMarking_InputWidgetFlag;
1735 d->cursor = touchCoordCursor_InputWidget_(d, latestPosition_Touch());
1736 markWordAtCursor_InputWidget_(d);
1737 d->cursor = indexToCursor_InputWidget_(d, d->mark.end);
1738 refresh_Widget(d);
1739 }
1740 return true_EventResult;
1741 }
1742 switch (processEvent_Click(&d->click, ev)) {
1743 case none_ClickResult:
1744 break;
1745 case started_ClickResult: {
1746// printf("[%p] started\n", w);
1747 /*
1748 const iInt2 curPos = relativeCursorCoord_InputWidget_(d);
1749 const iInt2 relClick = sub_I2(pos_Click(&d->click),
1750 topLeft_Rect(contentBounds_InputWidget_(d)));
1751 if (dist_I2(curPos, relClick) < gap_UI * 8) {
1752 printf("tap on cursor!\n");
1753 setFlags_Widget(w, touchDrag_WidgetFlag, iTrue);
1754 }
1755 else {
1756 printf("tap elsewhere\n");
1757 }*/
1758 return true_EventResult;
1759 }
1760 case drag_ClickResult:
1761// printf("[%p] drag %d,%d\n", w, pos_Click(&d->click).x, pos_Click(&d->click).y);
1762 if (d->inFlags & dragCursor_InputWidgetFlag) {
1763 iZap(d->mark);
1764 d->cursor = touchCoordCursor_InputWidget_(d, pos_Click(&d->click));
1765 showCursor_InputWidget_(d);
1766 refresh_Widget(w);
1767 }
1768 else if (d->inFlags & dragMarkerStart_InputWidgetFlag) {
1769 d->mark.start = cursorToIndex_InputWidget_(d, touchCoordCursor_InputWidget_(d, pos_Click(&d->click)));
1770 refresh_Widget(w);
1771 }
1772 else if (d->inFlags & dragMarkerEnd_InputWidgetFlag) {
1773 d->mark.end = cursorToIndex_InputWidget_(d, touchCoordCursor_InputWidget_(d, pos_Click(&d->click)));
1774 refresh_Widget(w);
1775 }
1776 return true_EventResult;
1777 // printf("[%p] aborted\n", w);
1778// d->inFlags &= ~touchBehavior_InputWidgetFlag;
1779// setFlags_Widget(w, touchDrag_WidgetFlag, iFalse);
1780// return true_EventResult;
1781 case finished_ClickResult:
1782 case aborted_ClickResult: {
1783// printf("[%p] ended\n", w);
1784 uint32_t tapElapsed = SDL_GetTicks() - d->tapStartTime;
1785// printf("tapElapsed: %u\n", tapElapsed);
1786 if (!isFocused_Widget(w)) {
1787 setFocus_Widget(w);
1788 d->lastTapPos = latestPosition_Touch();
1789 d->tapStartTime = SDL_GetTicks();
1790 d->tapCount = 0;
1791 d->cursor = touchCoordCursor_InputWidget_(d, pos_Click(&d->click));
1792 showCursor_InputWidget_(d);
1793 }
1794 else if (!isEmpty_Range(&d->mark) && !isMoved_Click(&d->click)) {
1795 if (isInsideMark_InputWidget_(d, cursorToIndex_InputWidget_(d, touchCoordCursor_InputWidget_(d, latestPosition_Touch())))) {
1796 showClipMenu_(latestPosition_Touch());
1797 }
1798 else {
1799 iZap(d->mark);
1800 d->cursor = touchCoordCursor_InputWidget_(d, pos_Click(&d->click));
1801 }
1802 }
1803 else if (SDL_GetTicks() - d->lastTapTime > 1000 &&
1804 d->tapCount == 0 && isEmpty_Range(&d->mark) && !isMoved_Click(&d->click) &&
1805 distanceToPos_InputWidget_(d, latestPosition_Touch(), d->cursor) < gap_UI * 5) {
1806 showClipMenu_(latestPosition_Touch());
1807 }
1808 else {
1809 if (~d->inFlags & isMarking_InputWidgetFlag) {
1810 iZap(d->mark);
1811 d->cursor = touchCoordCursor_InputWidget_(d, pos_Click(&d->click));
1812 }
1813 }
1814 if (d->inFlags & (dragCursor_InputWidgetFlag | dragMarkerStart_InputWidgetFlag |
1815 dragMarkerEnd_InputWidgetFlag)) {
1816// printf("[%p] finished cursor/marker drag\n", w);
1817 d->inFlags &= ~(dragCursor_InputWidgetFlag |
1818 dragMarkerStart_InputWidgetFlag |
1819 dragMarkerEnd_InputWidgetFlag);
1820 setFlags_Widget(w, touchDrag_WidgetFlag, iFalse);
1821 }
1822 d->inFlags &= ~isMarking_InputWidgetFlag;
1823 showCursor_InputWidget_(d);
1824 refresh_Widget(w);
1825#if 0
1826 d->inFlags &= ~touchBehavior_InputWidgetFlag;
1827 if (flags_Widget(w) & touchDrag_WidgetFlag) {
1828 setFlags_Widget(w, touchDrag_WidgetFlag, iFalse);
1829 return true_EventResult;
1830 }
1831 if (!isMoved_Click(&d->click)) {
1832 if (!isFocused_Widget(w)) {
1833 setFocus_Widget(w);
1834 if (~d->inFlags & selectAllOnFocus_InputWidgetFlag) {
1835 d->cursor = coordCursor_InputWidget_(d, pos_Click(&d->click));
1836 showCursor_InputWidget_(d);
1837 }
1838 }
1839 else {
1840 iZap(d->mark);
1841 d->cursor = coordCursor_InputWidget_(d, pos_Click(&d->click));
1842 showCursor_InputWidget_(d);
1843 }
1844 }
1845#endif
1846 return true_EventResult;
1847 }
1848 }
1849#endif
1850// if ((ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) &&
1851// contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
1852// /* Eat all mouse clicks on the widget. */
1853// return true_EventResult;
1854// }
1855 return ignored_EventResult;
1856}
1857
1445static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { 1858static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1446 iWidget *w = as_Widget(d); 1859 iWidget *w = as_Widget(d);
1447 /* Resize according to width immediately. */ 1860 /* Resize according to width immediately. */
@@ -1486,23 +1899,35 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1486 paste_InputWidget_(d); 1899 paste_InputWidget_(d);
1487 return iTrue; 1900 return iTrue;
1488 } 1901 }
1902 else if (isCommand_UserEvent(ev, "input.undo") && isEditing_InputWidget_(d)) {
1903 if (popUndo_InputWidget_(d)) {
1904 refresh_Widget(w);
1905 contentsWereChanged_InputWidget_(d);
1906 }
1907 return iTrue;
1908 }
1909 else if (isCommand_UserEvent(ev, "input.selectall") && isEditing_InputWidget_(d)) {
1910 selectAll_InputWidget(d);
1911 return iTrue;
1912 }
1489 else if (isCommand_UserEvent(ev, "theme.changed")) { 1913 else if (isCommand_UserEvent(ev, "theme.changed")) {
1490 if (d->buffered) { 1914 if (d->buffered) {
1491 d->inFlags |= needUpdateBuffer_InputWidgetFlag; 1915 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
1492 } 1916 }
1493 return iFalse; 1917 return iFalse;
1494 } 1918 }
1495 else if (isCommand_UserEvent(ev, "keyboard.changed")) { 1919 /* TODO: Scroll to keep widget visible when keyboard appears. */
1496 if (isFocused_Widget(d) && arg_Command(command_UserEvent(ev))) { 1920// else if (isCommand_UserEvent(ev, "keyboard.changed")) {
1497 iRect rect = bounds_Widget(w); 1921// if (isFocused_Widget(d) && arg_Command(command_UserEvent(ev))) {
1498 rect.pos.y -= value_Anim(&get_Window()->rootOffset); 1922// iRect rect = bounds_Widget(w);
1499 const iInt2 visRoot = visibleSize_Root(w->root); 1923// rect.pos.y -= value_Anim(&get_Window()->rootOffset);
1500 if (bottom_Rect(rect) > visRoot.y) { 1924// const iInt2 visRoot = visibleSize_Root(w->root);
1501 setValue_Anim(&get_Window()->rootOffset, -(bottom_Rect(rect) - visRoot.y), 250); 1925// if (bottom_Rect(rect) > visRoot.y) {
1502 } 1926// setValue_Anim(&get_Window()->rootOffset, -(bottom_Rect(rect) - visRoot.y), 250);
1503 } 1927// }
1504 return iFalse; 1928// }
1505 } 1929// return iFalse;
1930// }
1506 else if (isCommand_UserEvent(ev, "text.insert")) { 1931 else if (isCommand_UserEvent(ev, "text.insert")) {
1507 pushUndo_InputWidget_(d); 1932 pushUndo_InputWidget_(d);
1508 deleteMarked_InputWidget_(d); 1933 deleteMarked_InputWidget_(d);
@@ -1524,16 +1949,10 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1524 copy_InputWidget_(d, iFalse); 1949 copy_InputWidget_(d, iFalse);
1525 return iTrue; 1950 return iTrue;
1526 } 1951 }
1527 if (ev->type == SDL_MOUSEMOTION && (isHover_Widget(d) || flags_Widget(w) & keepOnTop_WidgetFlag)) {
1528 const iInt2 coord = init_I2(ev->motion.x, ev->motion.y);
1529 const iInt2 inner = windowToInner_Widget(w, coord);
1530 setCursor_Window(get_Window(),
1531 inner.x >= 2 * gap_UI + d->leftPadding &&
1532 inner.x < width_Widget(w) - d->rightPadding
1533 ? SDL_SYSTEM_CURSOR_IBEAM
1534 : SDL_SYSTEM_CURSOR_ARROW);
1535 }
1536 if (ev->type == SDL_MOUSEWHEEL && contains_Widget(w, coord_MouseWheelEvent(&ev->wheel))) { 1952 if (ev->type == SDL_MOUSEWHEEL && contains_Widget(w, coord_MouseWheelEvent(&ev->wheel))) {
1953 if (numWrapLines_InputWidget_(d) <= size_Range(&d->visWrapLines)) {
1954 return ignored_EventResult;
1955 }
1537 const int lineHeight = lineHeight_Text(d->font); 1956 const int lineHeight = lineHeight_Text(d->font);
1538 if (isPerPixel_MouseWheelEvent(&ev->wheel)) { 1957 if (isPerPixel_MouseWheelEvent(&ev->wheel)) {
1539 d->wheelAccum -= ev->wheel.y; 1958 d->wheelAccum -= ev->wheel.y;
@@ -1551,87 +1970,24 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1551 lastLine_InputWidget_(d)->wrapLines.end - d->visWrapLines.end); 1970 lastLine_InputWidget_(d)->wrapLines.end - d->visWrapLines.end);
1552 if (!lineDelta) d->wheelAccum = 0; 1971 if (!lineDelta) d->wheelAccum = 0;
1553 } 1972 }
1554 d->wheelAccum -= lineDelta * lineHeight; 1973 if (lineDelta) {
1555 d->visWrapLines.start += lineDelta; 1974 d->wheelAccum -= lineDelta * lineHeight;
1556 d->visWrapLines.end += lineDelta; 1975 d->visWrapLines.start += lineDelta;
1557 d->inFlags |= needUpdateBuffer_InputWidgetFlag; 1976 d->visWrapLines.end += lineDelta;
1558 refresh_Widget(d); 1977 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
1559 return iTrue;
1560 }
1561 switch (processEvent_Click(&d->click, ev)) {
1562 case none_ClickResult:
1563 break;
1564 case started_ClickResult: {
1565 setFocus_Widget(w);
1566 const iInt2 oldCursor = d->cursor;
1567 setCursor_InputWidget(d, coordCursor_InputWidget_(d, pos_Click(&d->click)));
1568 if (keyMods_Sym(modState_Keys()) == KMOD_SHIFT) {
1569 d->mark = d->initialMark = (iRanges){
1570 cursorToIndex_InputWidget_(d, oldCursor),
1571 cursorToIndex_InputWidget_(d, d->cursor)
1572 };
1573 d->inFlags |= isMarking_InputWidgetFlag;
1574 }
1575 else {
1576 iZap(d->mark);
1577 iZap(d->initialMark);
1578 d->inFlags &= ~(isMarking_InputWidgetFlag | markWords_InputWidgetFlag);
1579 if (d->click.count == 2) {
1580 d->inFlags |= isMarking_InputWidgetFlag | markWords_InputWidgetFlag;
1581 d->mark.start = d->mark.end = cursorToIndex_InputWidget_(d, d->cursor);
1582 extendRange_InputWidget_(d, &d->mark.start, -1);
1583 extendRange_InputWidget_(d, &d->mark.end, +1);
1584 d->initialMark = d->mark;
1585 refresh_Widget(w);
1586 }
1587 if (d->click.count == 3) {
1588 selectAll_InputWidget(d);
1589 }
1590 }
1591 refresh_Widget(d); 1978 refresh_Widget(d);
1592 return iTrue; 1979 return true_EventResult;
1593 } 1980 }
1594 case aborted_ClickResult: 1981 return false_EventResult;
1595 d->inFlags &= ~isMarking_InputWidgetFlag;
1596 return iTrue;
1597 case drag_ClickResult:
1598 d->cursor = coordCursor_InputWidget_(d, pos_Click(&d->click));
1599 showCursor_InputWidget_(d);
1600 if (~d->inFlags & isMarking_InputWidgetFlag) {
1601 d->inFlags |= isMarking_InputWidgetFlag;
1602 d->mark.start = cursorToIndex_InputWidget_(d, d->cursor);
1603 }
1604 d->mark.end = cursorToIndex_InputWidget_(d, d->cursor);
1605 if (d->inFlags & markWords_InputWidgetFlag) {
1606 const iBool isFwd = d->mark.end >= d->mark.start;
1607 extendRange_InputWidget_(d, &d->mark.end, isFwd ? +1 : -1);
1608 d->mark.start = isFwd ? d->initialMark.start : d->initialMark.end;
1609 }
1610 refresh_Widget(w);
1611 return iTrue;
1612 case finished_ClickResult:
1613 d->inFlags &= ~isMarking_InputWidgetFlag;
1614 return iTrue;
1615 } 1982 }
1616 if (ev->type == SDL_MOUSEMOTION && flags_Widget(w) & keepOnTop_WidgetFlag) { 1983 /* Click behavior depends on device type. */ {
1617 const iInt2 coord = init_I2(ev->motion.x, ev->motion.y); 1984 const int mbResult = (deviceType_App() == desktop_AppDeviceType
1618 if (contains_Click(&d->click, coord)) { 1985 ? processPointerEvents_InputWidget_(d, ev)
1619 return iTrue; 1986 : processTouchEvents_InputWidget_(d, ev));
1987 if (mbResult) {
1988 return mbResult >> 1;
1620 } 1989 }
1621 } 1990 }
1622 if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT &&
1623 contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
1624 iWidget *clipMenu = findWidget_App("clipmenu");
1625 if (isVisible_Widget(clipMenu)) {
1626 closeMenu_Widget(clipMenu);
1627 }
1628 else {
1629 openMenuFlags_Widget(clipMenu,
1630 mouseCoord_Window(get_Window(), ev->button.which),
1631 iFalse);
1632 }
1633 return iTrue;
1634 }
1635 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { 1991 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) {
1636 return iTrue; 1992 return iTrue;
1637 } 1993 }
@@ -1833,6 +2189,13 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1833 return iTrue; 2189 return iTrue;
1834 } 2190 }
1835 case SDLK_TAB: 2191 case SDLK_TAB:
2192 if (mods == (KMOD_ALT | KMOD_SHIFT)) {
2193 pushUndo_InputWidget_(d);
2194 deleteMarked_InputWidget_(d);
2195 insertChar_InputWidget_(d, '\t');
2196 contentsWereChanged_InputWidget_(d);
2197 return iTrue;
2198 }
1836 /* Allow focus switching. */ 2199 /* Allow focus switching. */
1837 return processEvent_Widget(as_Widget(d), ev); 2200 return processEvent_Widget(as_Widget(d), ev);
1838 case SDLK_UP: 2201 case SDLK_UP:
@@ -1878,6 +2241,8 @@ struct Impl_MarkPainter {
1878 const iInputLine * line; 2241 const iInputLine * line;
1879 iInt2 pos; 2242 iInt2 pos;
1880 iRanges mark; 2243 iRanges mark;
2244 iRect firstMarkRect;
2245 iRect lastMarkRect;
1881}; 2246};
1882 2247
1883static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, int origin, int advance, 2248static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, int origin, int advance,
@@ -1916,7 +2281,11 @@ static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, int or
1916 } 2281 }
1917 rect.size.x = iMax(gap_UI / 3, rect.size.x); 2282 rect.size.x = iMax(gap_UI / 3, rect.size.x);
1918 mp->pos.y += lineHeight_Text(mp->d->font); 2283 mp->pos.y += lineHeight_Text(mp->d->font);
1919 fillRect_Paint(mp->paint, rect, uiMarked_ColorId); 2284 fillRect_Paint(mp->paint, rect, uiMarked_ColorId | opaque_ColorId);
2285 if (deviceType_App() != desktop_AppDeviceType) {
2286 if (isEmpty_Rect(mp->firstMarkRect)) mp->firstMarkRect = rect;
2287 mp->lastMarkRect = rect;
2288 }
1920 return iTrue; 2289 return iTrue;
1921} 2290}
1922 2291
@@ -1924,8 +2293,9 @@ static void draw_InputWidget_(const iInputWidget *d) {
1924 const iWidget *w = constAs_Widget(d); 2293 const iWidget *w = constAs_Widget(d);
1925 iRect bounds = adjusted_Rect(bounds_InputWidget_(d), padding_(), neg_I2(padding_())); 2294 iRect bounds = adjusted_Rect(bounds_InputWidget_(d), padding_(), neg_I2(padding_()));
1926 iBool isHint = isHintVisible_InputWidget_(d); 2295 iBool isHint = isHintVisible_InputWidget_(d);
1927 const iBool isFocused = isFocused_Widget(w); 2296 const iBool isFocused = isFocused_Widget(w);
1928 const iBool isHover = isHover_Widget(w) && 2297 const iBool isHover = deviceType_App() == desktop_AppDeviceType &&
2298 isHover_Widget(w) &&
1929 contains_InputWidget_(d, mouseCoord_Window(get_Window(), 0)); 2299 contains_InputWidget_(d, mouseCoord_Window(get_Window(), 0));
1930 if (d->inFlags & needUpdateBuffer_InputWidgetFlag) { 2300 if (d->inFlags & needUpdateBuffer_InputWidgetFlag) {
1931 updateBuffered_InputWidget_(iConstCast(iInputWidget *, d)); 2301 updateBuffered_InputWidget_(iConstCast(iInputWidget *, d));
@@ -1955,6 +2325,7 @@ static void draw_InputWidget_(const iInputWidget *d) {
1955 }; 2325 };
1956 const iRangei visLines = visibleLineRange_InputWidget_(d); 2326 const iRangei visLines = visibleLineRange_InputWidget_(d);
1957 const int visLineOffsetY = visLineOffsetY_InputWidget_(d); 2327 const int visLineOffsetY = visLineOffsetY_InputWidget_(d);
2328 iRect markerRects[2] = { zero_Rect(), zero_Rect() };
1958 /* If buffered, just draw the buffered copy. */ 2329 /* If buffered, just draw the buffered copy. */
1959 if (d->buffered && !isFocused) { 2330 if (d->buffered && !isFocused) {
1960 /* Most input widgets will use this, since only one is focused at a time. */ 2331 /* Most input widgets will use this, since only one is focused at a time. */
@@ -1970,7 +2341,7 @@ static void draw_InputWidget_(const iInputWidget *d) {
1970 .paint = &p, 2341 .paint = &p,
1971 .d = d, 2342 .d = d,
1972 .contentBounds = contentBounds, 2343 .contentBounds = contentBounds,
1973 .mark = mark_InputWidget_(d) 2344 .mark = mark_InputWidget_(d),
1974 }; 2345 };
1975 wrapText.context = &marker; 2346 wrapText.context = &marker;
1976 wrapText.wrapFunc = isFocused ? draw_MarkPainter_ : NULL; /* mark is drawn under each line of text */ 2347 wrapText.wrapFunc = isFocused ? draw_MarkPainter_ : NULL; /* mark is drawn under each line of text */
@@ -1981,11 +2352,14 @@ static void draw_InputWidget_(const iInputWidget *d) {
1981 marker.pos = drawPos; 2352 marker.pos = drawPos;
1982 addv_I2(&drawPos, draw_WrapText(&wrapText, d->font, drawPos, fg).advance); /* lines end with \n */ 2353 addv_I2(&drawPos, draw_WrapText(&wrapText, d->font, drawPos, fg).advance); /* lines end with \n */
1983 } 2354 }
2355 markerRects[0] = marker.firstMarkRect;
2356 markerRects[1] = marker.lastMarkRect;
1984 wrapText.wrapFunc = NULL; 2357 wrapText.wrapFunc = NULL;
1985 wrapText.context = NULL; 2358 wrapText.context = NULL;
1986 } 2359 }
1987 /* Draw the insertion point. */ 2360 /* Draw the insertion point. */
1988 if (isFocused && d->cursorVis && contains_Range(&visLines, d->cursor.y)) { 2361 if (isFocused && d->cursorVis && contains_Range(&visLines, d->cursor.y) &&
2362 (deviceType_App() == desktop_AppDeviceType || isEmpty_Range(&d->mark))) {
1989 iInt2 curSize; 2363 iInt2 curSize;
1990 iRangecc cursorChar = iNullRange; 2364 iRangecc cursorChar = iNullRange;
1991 int visWrapsAbove = 0; 2365 int visWrapsAbove = 0;
@@ -1998,8 +2372,8 @@ static void draw_InputWidget_(const iInputWidget *d) {
1998 cursorChar.start = charPos_InputWidget_(d, d->cursor); 2372 cursorChar.start = charPos_InputWidget_(d, d->cursor);
1999 iChar ch = 0; 2373 iChar ch = 0;
2000 int n = decodeBytes_MultibyteChar(cursorChar.start, 2374 int n = decodeBytes_MultibyteChar(cursorChar.start,
2001 constEnd_String(&constCursorLine_InputWidget_(d)->text), 2375 constEnd_String(&constCursorLine_InputWidget_(d)->text),
2002 &ch); 2376 &ch);
2003 cursorChar.end = cursorChar.start + iMax(n, 0); 2377 cursorChar.end = cursorChar.start + iMax(n, 0);
2004 if (ch) { 2378 if (ch) {
2005 if (d->inFlags & isSensitive_InputWidgetFlag) { 2379 if (d->inFlags & isSensitive_InputWidgetFlag) {
@@ -2033,6 +2407,11 @@ static void draw_InputWidget_(const iInputWidget *d) {
2033 } 2407 }
2034 } 2408 }
2035 unsetClip_Paint(&p); 2409 unsetClip_Paint(&p);
2410 if (!isEmpty_Rect(markerRects[0])) {
2411 for (int i = 0; i < 2; ++i) {
2412 drawPin_Paint(&p, markerRects[i], i, uiTextCaution_ColorId);
2413 }
2414 }
2036 drawChildren_Widget(w); 2415 drawChildren_Widget(w);
2037} 2416}
2038 2417
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h
index 0d327ca6..f70c81af 100644
--- a/src/ui/inputwidget.h
+++ b/src/ui/inputwidget.h
@@ -68,6 +68,7 @@ int minLines_InputWidget (const iInputWidget *);
68int maxLines_InputWidget (const iInputWidget *); 68int maxLines_InputWidget (const iInputWidget *);
69iInputWidgetContentPadding contentPadding_InputWidget (const iInputWidget *); 69iInputWidgetContentPadding contentPadding_InputWidget (const iInputWidget *);
70const iString * text_InputWidget (const iInputWidget *); 70const iString * text_InputWidget (const iInputWidget *);
71int font_InputWidget (const iInputWidget *);
71 72
72iLocalDef const char *cstrText_InputWidget(const iInputWidget *d) { 73iLocalDef const char *cstrText_InputWidget(const iInputWidget *d) {
73 return cstr_String(text_InputWidget(d)); 74 return cstr_String(text_InputWidget(d));
diff --git a/src/ui/keys.c b/src/ui/keys.c
index 6de30f57..30072572 100644
--- a/src/ui/keys.c
+++ b/src/ui/keys.c
@@ -213,6 +213,7 @@ static const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] =
213 { 46, { "${keys.link.homerow.hover}", 'h', 0, "document.linkkeys arg:1 hover:1" }, 0 }, 213 { 46, { "${keys.link.homerow.hover}", 'h', 0, "document.linkkeys arg:1 hover:1" }, 0 },
214 { 47, { "${keys.link.homerow.next}", '.', 0, "document.linkkeys more:1" }, 0 }, 214 { 47, { "${keys.link.homerow.next}", '.', 0, "document.linkkeys more:1" }, 0 },
215 { 50, { "${keys.bookmark.add}", 'd', KMOD_PRIMARY, "bookmark.add" }, 0 }, 215 { 50, { "${keys.bookmark.add}", 'd', KMOD_PRIMARY, "bookmark.add" }, 0 },
216 { 51, { "${keys.bookmark.addfolder}", 'n', KMOD_SHIFT, "bookmarks.addfolder" }, 0 },
216 { 55, { "${keys.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, 0 }, 217 { 55, { "${keys.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, 0 },
217 { 60, { "${keys.findtext}", 'f', KMOD_PRIMARY, "focus.set id:find.input" }, 0 }, 218 { 60, { "${keys.findtext}", 'f', KMOD_PRIMARY, "focus.set id:find.input" }, 0 },
218 { 70, { "${keys.zoom.in}", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" }, 0 }, 219 { 70, { "${keys.zoom.in}", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" }, 0 },
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index ef306ab9..cfc81863 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -44,11 +44,13 @@ struct Impl_LabelWidget {
44 iString command; 44 iString command;
45 iClick click; 45 iClick click;
46 struct { 46 struct {
47 uint8_t alignVisual : 1; /* align according to visible bounds, not font metrics */ 47 uint8_t alignVisual : 1; /* align according to visible bounds, not font metrics */
48 uint8_t noAutoMinHeight : 1; /* minimum height is not set automatically */ 48 uint8_t noAutoMinHeight : 1; /* minimum height is not set automatically */
49 uint8_t drawAsOutline : 1; /* draw as outline, filled with background color */ 49 uint8_t drawAsOutline : 1; /* draw as outline, filled with background color */
50 uint8_t noTopFrame : 1; 50 uint8_t noTopFrame : 1;
51 uint8_t wrap : 1; 51 uint8_t wrap : 1;
52 uint8_t allCaps : 1;
53 uint8_t removeTrailingColon : 1;
52 } flags; 54 } flags;
53}; 55};
54 56
@@ -68,7 +70,7 @@ static iInt2 padding_LabelWidget_(const iLabelWidget *d, int corner) {
68 : corner == 1 ? init_I2(w->padding[2], w->padding[1]) 70 : corner == 1 ? init_I2(w->padding[2], w->padding[1])
69 : corner == 2 ? init_I2(w->padding[2], w->padding[3]) 71 : corner == 2 ? init_I2(w->padding[2], w->padding[3])
70 : init_I2(w->padding[0], w->padding[3])); 72 : init_I2(w->padding[0], w->padding[3]));
71#if defined (iPlatformAppleMobile) 73#if defined (iPlatformMobile)
72 return add_I2(widgetPad, 74 return add_I2(widgetPad,
73 init_I2(flags & tight_WidgetFlag ? 2 * gap_UI : (4 * gap_UI), 75 init_I2(flags & tight_WidgetFlag ? 2 * gap_UI : (4 * gap_UI),
74 (flags & extraPadding_WidgetFlag ? 1.5f : 1.0f) * 3 * gap_UI / 2)); 76 (flags & extraPadding_WidgetFlag ? 1.5f : 1.0f) * 3 * gap_UI / 2));
@@ -124,6 +126,11 @@ static iBool processEvent_LabelWidget_(iLabelWidget *d, const SDL_Event *ev) {
124 updateKey_LabelWidget_(d); 126 updateKey_LabelWidget_(d);
125 return iFalse; 127 return iFalse;
126 } 128 }
129 else if (isCommand_Widget(w, ev, "focus.gained") ||
130 isCommand_Widget(w, ev, "focus.lost")) {
131 refresh_Widget(d);
132 return iFalse;
133 }
127 if (!isEmpty_String(&d->command)) { 134 if (!isEmpty_String(&d->command)) {
128#if 0 && defined (iPlatformAppleMobile) 135#if 0 && defined (iPlatformAppleMobile)
129 /* Touch allows activating any button on release. */ 136 /* Touch allows activating any button on release. */
@@ -159,8 +166,15 @@ static iBool processEvent_LabelWidget_(iLabelWidget *d, const SDL_Event *ev) {
159 switch (ev->type) { 166 switch (ev->type) {
160 case SDL_KEYDOWN: { 167 case SDL_KEYDOWN: {
161 const int mods = ev->key.keysym.mod; 168 const int mods = ev->key.keysym.mod;
162 if (d->key && ev->key.keysym.sym == d->key && checkModifiers_(mods, d->kmods)) { 169 const int sym = ev->key.keysym.sym;
170 if (d->key && sym == d->key && checkModifiers_(mods, d->kmods)) {
171 trigger_LabelWidget_(d);
172 return iTrue;
173 }
174 if (isFocused_Widget(d) && mods == 0 &&
175 (sym == SDLK_RETURN || sym == SDLK_KP_ENTER)) {
163 trigger_LabelWidget_(d); 176 trigger_LabelWidget_(d);
177 refresh_Widget(d);
164 return iTrue; 178 return iTrue;
165 } 179 }
166 break; 180 break;
@@ -174,14 +188,17 @@ static void keyStr_LabelWidget_(const iLabelWidget *d, iString *str) {
174 toString_Sym(d->key, d->kmods, str); 188 toString_Sym(d->key, d->kmods, str);
175} 189}
176 190
177static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *frame1, int *frame2) { 191static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *frame1, int *frame2,
192 int *icon, int *meta) {
178 const iWidget *w = constAs_Widget(d); 193 const iWidget *w = constAs_Widget(d);
179 const int64_t flags = flags_Widget(w); 194 const int64_t flags = flags_Widget(w);
195 const iBool isFocus = (flags & focusable_WidgetFlag && isFocused_Widget(d));
180 const iBool isPress = (flags & pressed_WidgetFlag) != 0; 196 const iBool isPress = (flags & pressed_WidgetFlag) != 0;
181 const iBool isSel = (flags & selected_WidgetFlag) != 0; 197 const iBool isSel = (flags & selected_WidgetFlag) != 0;
182 const iBool isFrameless = (flags & frameless_WidgetFlag) != 0; 198 const iBool isFrameless = (flags & frameless_WidgetFlag) != 0;
183 const iBool isButton = d->click.button != 0; 199 const iBool isButton = d->click.button != 0;
184 const iBool isKeyRoot = (w->root == get_Window()->keyRoot); 200 const iBool isKeyRoot = (w->root == get_Window()->keyRoot);
201 const iBool isDarkTheme = isDark_ColorTheme(colorTheme_App());
185 /* Default color state. */ 202 /* Default color state. */
186 *bg = isButton && ~flags & noBackground_WidgetFlag ? (d->widget.bgColor != none_ColorId ? 203 *bg = isButton && ~flags & noBackground_WidgetFlag ? (d->widget.bgColor != none_ColorId ?
187 d->widget.bgColor : uiBackground_ColorId) 204 d->widget.bgColor : uiBackground_ColorId)
@@ -189,8 +206,12 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
189 *fg = uiText_ColorId; 206 *fg = uiText_ColorId;
190 *frame1 = isButton ? uiEmboss1_ColorId : d->widget.frameColor; 207 *frame1 = isButton ? uiEmboss1_ColorId : d->widget.frameColor;
191 *frame2 = isButton ? uiEmboss2_ColorId : *frame1; 208 *frame2 = isButton ? uiEmboss2_ColorId : *frame1;
209 *icon = uiIcon_ColorId;
210 *meta = uiTextShortcut_ColorId;
192 if (flags & disabled_WidgetFlag && isButton) { 211 if (flags & disabled_WidgetFlag && isButton) {
193 *fg = uiTextDisabled_ColorId; 212 *icon = uiTextDisabled_ColorId;
213 *fg = uiTextDisabled_ColorId;
214 *meta = uiTextDisabled_ColorId;
194 } 215 }
195 if (isSel) { 216 if (isSel) {
196 *bg = uiBackgroundSelected_ColorId; 217 *bg = uiBackgroundSelected_ColorId;
@@ -210,9 +231,15 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
210 } 231 }
211 } 232 }
212 } 233 }
234 if (isFocus) {
235 *frame1 = *frame2 = (isSel ? uiText_ColorId : uiInputFrameFocused_ColorId);
236 }
213 int colorEscape = none_ColorId; 237 int colorEscape = none_ColorId;
214 if (startsWith_String(&d->label, "\v")) { 238 if (startsWith_String(&d->label, "\v")) {
215 colorEscape = cstr_String(&d->label)[1] - asciiBase_ColorEscape; /* TODO: can be two bytes long */ 239 colorEscape = parseEscape_Color(cstr_String(&d->label), NULL);
240 }
241 if (colorEscape == uiTextCaution_ColorId) {
242 *icon = *meta = colorEscape;
216 } 243 }
217 if (isHover_LabelWidget_(d)) { 244 if (isHover_LabelWidget_(d)) {
218 if (isFrameless) { 245 if (isFrameless) {
@@ -221,43 +248,48 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
221 } 248 }
222 else { 249 else {
223 /* Frames matching color escaped text. */ 250 /* Frames matching color escaped text. */
224 if (colorEscape != none_ColorId) { 251 if (colorEscape == uiTextCaution_ColorId) {
225 if (isDark_ColorTheme(colorTheme_App())) { 252 *frame1 = colorEscape;
226 *frame1 = colorEscape; 253 *frame2 = isDarkTheme ? darker_Color(*frame1) : lighter_Color(*frame1);
227 *frame2 = darker_Color(*frame1);
228 }
229 else {
230 *bg = *frame1 = *frame2 = colorEscape;
231 *fg = white_ColorId | permanent_ColorId;
232 }
233 } 254 }
234 else if (isSel) { 255 else if (isSel) {
235 *frame1 = uiEmbossSelectedHover1_ColorId; 256 *frame1 = uiEmbossSelectedHover1_ColorId;
236 *frame2 = uiEmbossSelectedHover2_ColorId; 257 *frame2 = uiEmbossSelectedHover2_ColorId;
237 } 258 }
238 else { 259 else {
239 if (isButton) *bg = uiBackgroundHover_ColorId;
240 *frame1 = uiEmbossHover1_ColorId; 260 *frame1 = uiEmbossHover1_ColorId;
241 *frame2 = uiEmbossHover2_ColorId; 261 *frame2 = uiEmbossHover2_ColorId;
242 } 262 }
243 } 263 }
264 if (colorEscape == uiTextCaution_ColorId) {
265 *icon = *meta = *fg = colorEscape;
266 *bg = isDarkTheme ? darker_Color(colorEscape) : lighter_Color(colorEscape);
267 }
268 }
269 if (d->forceFg >= 0) {
270 *fg = *icon = *meta = d->forceFg;
244 } 271 }
245 if (isPress) { 272 if (isPress) {
246 *bg = uiBackgroundPressed_ColorId | permanent_ColorId; 273 if (colorEscape == uiTextAction_ColorId || colorEscape == uiTextCaution_ColorId) {
247 if (isButton) { 274 *bg = colorEscape;
248 *frame1 = uiEmbossPressed1_ColorId; 275 *frame1 = *bg;
249 *frame2 = colorEscape != none_ColorId ? colorEscape : uiEmbossPressed2_ColorId; 276 *frame2 = *bg;
250 } 277 *fg = *icon = *meta = (isDarkTheme ? black_ColorId : white_ColorId) | permanent_ColorId;
251 if (colorEscape == none_ColorId || colorEscape == uiTextAction_ColorId) {
252 *fg = uiTextPressed_ColorId | permanent_ColorId;
253 } 278 }
254 else { 279 else {
255 *fg = isDark_ColorTheme(colorTheme_App()) ? white_ColorId : black_ColorId; 280 *bg = uiBackgroundPressed_ColorId | permanent_ColorId;
281 if (isButton) {
282 *frame1 = uiEmbossPressed1_ColorId;
283 *frame2 = colorEscape != none_ColorId ? colorEscape : uiEmbossPressed2_ColorId;
284 }
285 //if (colorEscape == none_ColorId || colorEscape == uiTextAction_ColorId) {
286 *fg = *icon = *meta = uiTextPressed_ColorId | permanent_ColorId;
287 // }
288 // else {
289 // *fg = (isDark_ColorTheme(colorTheme_App()) ? white_ColorId : black_ColorId) | permanent_ColorId;
290 // }
256 } 291 }
257 } 292 }
258 if (d->forceFg >= 0) {
259 *fg = d->forceFg;
260 }
261} 293}
262 294
263iLocalDef int iconPadding_LabelWidget_(const iLabelWidget *d) { 295iLocalDef int iconPadding_LabelWidget_(const iLabelWidget *d) {
@@ -287,13 +319,18 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
287 } 319 }
288 iPaint p; 320 iPaint p;
289 init_Paint(&p); 321 init_Paint(&p);
290 int bg, fg, frame, frame2; 322 int bg, fg, frame, frame2, iconColor, metaColor;
291 getColors_LabelWidget_(d, &bg, &fg, &frame, &frame2); 323 getColors_LabelWidget_(d, &bg, &fg, &frame, &frame2, &iconColor, &metaColor);
292 const iBool isCaution = startsWith_String(&d->label, uiTextCaution_ColorEscape); 324 const enum iColorId colorEscape = parseEscape_Color(cstr_String(&d->label), NULL);
325 const iBool isCaution = (colorEscape == uiTextCaution_ColorId);
293 if (bg >= 0) { 326 if (bg >= 0) {
294 fillRect_Paint(&p, rect, isCaution && isHover ? uiMarked_ColorId : bg); 327 fillRect_Paint(&p, rect, bg);
328 }
329 if (isFocused_Widget(w)) {
330 iRect frameRect = adjusted_Rect(rect, zero_I2(), init1_I2(-1));
331 drawRectThickness_Paint(&p, frameRect, gap_UI / 4, frame);
295 } 332 }
296 if (~flags & frameless_WidgetFlag) { 333 else if (~flags & frameless_WidgetFlag) {
297 iRect frameRect = adjusted_Rect(rect, zero_I2(), init1_I2(-1)); 334 iRect frameRect = adjusted_Rect(rect, zero_I2(), init1_I2(-1));
298 if (isButton) { 335 if (isButton) {
299 iInt2 points[] = { 336 iInt2 points[] = {
@@ -310,12 +347,18 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
310 } 347 }
311#endif 348#endif
312 drawLines_Paint(&p, points + 2, 3, frame2); 349 drawLines_Paint(&p, points + 2, 3, frame2);
313 drawLines_Paint( 350 drawLines_Paint(&p,
314 &p, points, !isHover && d->flags.noTopFrame ? 2 : 3, frame); 351 points,
352 isFocused_Widget(w) ? 3 : (!isHover && d->flags.noTopFrame ? 2 : 3),
353 frame);
315 } 354 }
316 } 355 }
317 setClip_Paint(&p, rect); 356 setClip_Paint(&p, rect);
318 const int iconPad = iconPadding_LabelWidget_(d); 357 const int iconPad = iconPadding_LabelWidget_(d);
358// const int iconColor = isCaution ? uiTextCaution_ColorId
359// : flags & (disabled_WidgetFlag | pressed_WidgetFlag) ? fg
360// : isHover ? uiIconHover_ColorId
361// : uiIcon_ColorId;
319 if (d->icon && d->icon != 0x20) { /* no need to draw an empty icon */ 362 if (d->icon && d->icon != 0x20) { /* no need to draw an empty icon */
320 iString str; 363 iString str;
321 initUnicodeN_String(&str, &d->icon, 1); 364 initUnicodeN_String(&str, &d->icon, 1);
@@ -329,16 +372,13 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
329 -gap_UI / 8)), 372 -gap_UI / 8)),
330 init_I2(iconPad, lineHeight_Text(d->font)) }, 373 init_I2(iconPad, lineHeight_Text(d->font)) },
331 iTrue, 374 iTrue,
332 isCaution ? uiTextCaution_ColorId 375 iconColor,
333 : flags & (disabled_WidgetFlag | pressed_WidgetFlag) ? fg
334 : isHover ? uiIconHover_ColorId
335 : uiIcon_ColorId,
336 "%s", 376 "%s",
337 cstr_String(&str)); 377 cstr_String(&str));
338 deinit_String(&str); 378 deinit_String(&str);
339 } 379 }
340 if (d->flags.wrap) { 380 if (d->flags.wrap) {
341 const iRect cont = contentBounds_LabelWidget_(d); //djusted_Rect(innerBounds_Widget(w), init_I2(iconPad, 0), zero_I2()); 381 const iRect cont = contentBounds_LabelWidget_(d);
342 drawWrapRange_Text( 382 drawWrapRange_Text(
343 d->font, topLeft_Rect(cont), width_Rect(cont), fg, range_String(&d->label)); 383 d->font, topLeft_Rect(cont), width_Rect(cont), fg, range_String(&d->label));
344 } 384 }
@@ -353,9 +393,11 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
353 add_I2(topRight_Rect(bounds), 393 add_I2(topRight_Rect(bounds),
354 addX_I2(negX_I2(padding_LabelWidget_(d, 1)), 394 addX_I2(negX_I2(padding_LabelWidget_(d, 1)),
355 deviceType_App() == tablet_AppDeviceType ? gap_UI : 0)), 395 deviceType_App() == tablet_AppDeviceType ? gap_UI : 0)),
356 flags & pressed_WidgetFlag ? fg 396 metaColor,/*
357 : isCaution ? uiTextCaution_ColorId 397 isHover || flags & pressed_WidgetFlag ? fg
358 : uiTextShortcut_ColorId, 398// : isCaution ? uiTextCaution_ColorId
399 : colorEscape != none_ColorId ? colorEscape
400 : uiTextShortcut_ColorId,*/
359 right_Alignment, 401 right_Alignment,
360 cstr_String(&str)); 402 cstr_String(&str));
361 deinit_String(&str); 403 deinit_String(&str);
@@ -385,7 +427,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
385 drawCentered_Text(d->font, 427 drawCentered_Text(d->font,
386 (iRect){ addX_I2(topRight_Rect(chRect), -iconPad), 428 (iRect){ addX_I2(topRight_Rect(chRect), -iconPad),
387 init_I2(chSize, height_Rect(chRect)) }, 429 init_I2(chSize, height_Rect(chRect)) },
388 iTrue, uiSeparator_ColorId, rightAngle_Icon); 430 iTrue, iconColor, rightAngle_Icon);
389 } 431 }
390 unsetClip_Paint(&p); 432 unsetClip_Paint(&p);
391} 433}
@@ -442,11 +484,18 @@ void updateSize_LabelWidget(iLabelWidget *d) {
442 484
443static void replaceVariables_LabelWidget_(iLabelWidget *d) { 485static void replaceVariables_LabelWidget_(iLabelWidget *d) {
444 translate_Lang(&d->label); 486 translate_Lang(&d->label);
487 if (d->flags.allCaps) {
488 set_String(&d->label, collect_String(upper_String(&d->label)));
489 }
490 if (d->flags.removeTrailingColon && endsWith_String(&d->label, ":")) {
491 removeEnd_String(&d->label, 1);
492 }
445} 493}
446 494
447void init_LabelWidget(iLabelWidget *d, const char *label, const char *cmd) { 495void init_LabelWidget(iLabelWidget *d, const char *label, const char *cmd) {
448 iWidget *w = &d->widget; 496 iWidget *w = &d->widget;
449 init_Widget(w); 497 init_Widget(w);
498 iZap(d->flags);
450 d->font = uiLabel_FontId; 499 d->font = uiLabel_FontId;
451 d->forceFg = none_ColorId; 500 d->forceFg = none_ColorId;
452 d->icon = 0; 501 d->icon = 0;
@@ -463,12 +512,7 @@ void init_LabelWidget(iLabelWidget *d, const char *label, const char *cmd) {
463 d->key = 0; 512 d->key = 0;
464 d->kmods = 0; 513 d->kmods = 0;
465 init_Click(&d->click, d, !isEmpty_String(&d->command) ? SDL_BUTTON_LEFT : 0); 514 init_Click(&d->click, d, !isEmpty_String(&d->command) ? SDL_BUTTON_LEFT : 0);
466 setFlags_Widget(w, hover_WidgetFlag, d->click.button != 0); 515 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, d->click.button != 0);
467 d->flags.alignVisual = iFalse;
468 d->flags.noAutoMinHeight = iFalse;
469 d->flags.drawAsOutline = iFalse;
470 d->flags.noTopFrame = iFalse;
471 d->flags.wrap = iFalse;
472 updateSize_LabelWidget(d); 516 updateSize_LabelWidget(d);
473 updateKey_LabelWidget_(d); /* could be bound to another key */ 517 updateKey_LabelWidget_(d); /* could be bound to another key */
474} 518}
@@ -499,6 +543,14 @@ void setText_LabelWidget(iLabelWidget *d, const iString *text) {
499 } 543 }
500} 544}
501 545
546void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) {
547 updateTextCStr_LabelWidget(d, text);
548 updateSize_LabelWidget(d);
549 if (isWrapped_LabelWidget(d)) {
550 sizeChanged_LabelWidget_(d);
551 }
552}
553
502void setAlignVisually_LabelWidget(iLabelWidget *d, iBool alignVisual) { 554void setAlignVisually_LabelWidget(iLabelWidget *d, iBool alignVisual) {
503 d->flags.alignVisual = alignVisual; 555 d->flags.alignVisual = alignVisual;
504} 556}
@@ -525,6 +577,20 @@ void setOutline_LabelWidget(iLabelWidget *d, iBool drawAsOutline) {
525 } 577 }
526} 578}
527 579
580void setAllCaps_LabelWidget(iLabelWidget *d, iBool allCaps) {
581 if (d) {
582 d->flags.allCaps = allCaps;
583 replaceVariables_LabelWidget_(d);
584 }
585}
586
587void setRemoveTrailingColon_LabelWidget(iLabelWidget *d, iBool removeTrailingColon) {
588 if (d) {
589 d->flags.removeTrailingColon = removeTrailingColon;
590 replaceVariables_LabelWidget_(d);
591 }
592}
593
528void updateText_LabelWidget(iLabelWidget *d, const iString *text) { 594void updateText_LabelWidget(iLabelWidget *d, const iString *text) {
529 set_String(&d->label, text); 595 set_String(&d->label, text);
530 set_String(&d->srcLabel, text); 596 set_String(&d->srcLabel, text);
@@ -533,10 +599,12 @@ void updateText_LabelWidget(iLabelWidget *d, const iString *text) {
533} 599}
534 600
535void updateTextCStr_LabelWidget(iLabelWidget *d, const char *text) { 601void updateTextCStr_LabelWidget(iLabelWidget *d, const char *text) {
536 setCStr_String(&d->label, text); 602 if (d) {
537 set_String(&d->srcLabel, &d->label); 603 setCStr_String(&d->label, text);
538 replaceVariables_LabelWidget_(d); 604 set_String(&d->srcLabel, &d->label);
539 refresh_Widget(&d->widget); 605 replaceVariables_LabelWidget_(d);
606 refresh_Widget(&d->widget);
607 }
540} 608}
541 609
542void updateTextAndResizeWidthCStr_LabelWidget(iLabelWidget *d, const char *text) { 610void updateTextAndResizeWidthCStr_LabelWidget(iLabelWidget *d, const char *text) {
@@ -544,13 +612,6 @@ void updateTextAndResizeWidthCStr_LabelWidget(iLabelWidget *d, const char *text)
544 d->widget.rect.size.x = defaultSize_LabelWidget(d).x; 612 d->widget.rect.size.x = defaultSize_LabelWidget(d).x;
545} 613}
546 614
547void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) {
548 setCStr_String(&d->label, text);
549 set_String(&d->srcLabel, &d->label);
550 replaceVariables_LabelWidget_(d);
551 updateSize_LabelWidget(d);
552}
553
554void setCommand_LabelWidget(iLabelWidget *d, const iString *command) { 615void setCommand_LabelWidget(iLabelWidget *d, const iString *command) {
555 set_String(&d->command, command); 616 set_String(&d->command, command);
556} 617}
@@ -567,19 +628,8 @@ iBool checkIcon_LabelWidget(iLabelWidget *d) {
567 d->icon = 0; 628 d->icon = 0;
568 return iFalse; 629 return iFalse;
569 } 630 }
570 iStringConstIterator iter; 631 d->icon = removeIconPrefix_String(&d->label);
571 init_StringConstIterator(&iter, &d->label); 632 return d->icon != 0;
572 const iChar icon = iter.value;
573 next_StringConstIterator(&iter);
574 if (iter.value == ' ' && icon >= 0x100) {
575 d->icon = icon;
576 remove_Block(&d->label.chars, 0, iter.next - constBegin_String(&d->label));
577 return iTrue;
578 }
579 else {
580 d->icon = 0;
581 }
582 return iFalse;
583} 633}
584 634
585iChar icon_LabelWidget(const iLabelWidget *d) { 635iChar icon_LabelWidget(const iLabelWidget *d) {
diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h
index b8b6fd87..6275d2c8 100644
--- a/src/ui/labelwidget.h
+++ b/src/ui/labelwidget.h
@@ -30,10 +30,12 @@ iDeclareWidgetClass(LabelWidget)
30iDeclareObjectConstructionArgs(LabelWidget, const char *label, const char *command) 30iDeclareObjectConstructionArgs(LabelWidget, const char *label, const char *command)
31 31
32void setAlignVisually_LabelWidget(iLabelWidget *, iBool alignVisual); 32void setAlignVisually_LabelWidget(iLabelWidget *, iBool alignVisual);
33void setNoAutoMinHeight_LabelWidget(iLabelWidget *, iBool noAutoMinHeight); 33void setNoAutoMinHeight_LabelWidget (iLabelWidget *, iBool noAutoMinHeight);
34void setNoTopFrame_LabelWidget (iLabelWidget *, iBool noTopFrame); 34void setNoTopFrame_LabelWidget (iLabelWidget *, iBool noTopFrame);
35void setWrap_LabelWidget (iLabelWidget *, iBool wrap); 35void setWrap_LabelWidget (iLabelWidget *, iBool wrap);
36void setOutline_LabelWidget (iLabelWidget *, iBool drawAsOutline); 36void setOutline_LabelWidget (iLabelWidget *, iBool drawAsOutline);
37void setAllCaps_LabelWidget (iLabelWidget *, iBool allCaps);
38void setRemoveTrailingColon_LabelWidget (iLabelWidget *, iBool removeTrailingColon);
37void setFont_LabelWidget (iLabelWidget *, int fontId); 39void setFont_LabelWidget (iLabelWidget *, int fontId);
38void setTextColor_LabelWidget (iLabelWidget *, int color); 40void setTextColor_LabelWidget (iLabelWidget *, int color);
39void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */ 41void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */
diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c
index d51516d1..ca15cc20 100644
--- a/src/ui/listwidget.c
+++ b/src/ui/listwidget.c
@@ -32,8 +32,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
32#include <the_Foundation/intset.h> 32#include <the_Foundation/intset.h>
33 33
34void init_ListItem(iListItem *d) { 34void init_ListItem(iListItem *d) {
35 d->isSeparator = iFalse; 35 d->isSeparator = iFalse;
36 d->isSelected = iFalse; 36 d->isSelected = iFalse;
37 d->isDraggable = iFalse;
38 d->isDropTarget = iFalse;
37} 39}
38 40
39void deinit_ListItem(iListItem *d) { 41void deinit_ListItem(iListItem *d) {
@@ -54,6 +56,8 @@ struct Impl_ListWidget {
54 int itemHeight; 56 int itemHeight;
55 iPtrArray items; 57 iPtrArray items;
56 size_t hoverItem; 58 size_t hoverItem;
59 size_t dragItem;
60 iInt2 dragOrigin; /* offset from mouse to drag item's top-left corner */
57 iClick click; 61 iClick click;
58 iIntSet invalidItems; 62 iIntSet invalidItems;
59 iVisBuf *visBuf; 63 iVisBuf *visBuf;
@@ -95,6 +99,8 @@ void init_ListWidget(iListWidget *d) {
95 d->noHoverWhileScrolling = iFalse; 99 d->noHoverWhileScrolling = iFalse;
96 init_PtrArray(&d->items); 100 init_PtrArray(&d->items);
97 d->hoverItem = iInvalidPos; 101 d->hoverItem = iInvalidPos;
102 d->dragItem = iInvalidPos;
103 d->dragOrigin = zero_I2();
98 init_Click(&d->click, d, SDL_BUTTON_LEFT); 104 init_Click(&d->click, d, SDL_BUTTON_LEFT);
99 init_IntSet(&d->invalidItems); 105 init_IntSet(&d->invalidItems);
100 d->visBuf = new_VisBuf(); 106 d->visBuf = new_VisBuf();
@@ -248,6 +254,10 @@ const iAnyObject *constItem_ListWidget(const iListWidget *d, size_t index) {
248 return NULL; 254 return NULL;
249} 255}
250 256
257const iAnyObject *constDragItem_ListWidget(const iListWidget *d) {
258 return constItem_ListWidget(d, d->dragItem);
259}
260
251const iAnyObject *constHoverItem_ListWidget(const iListWidget *d) { 261const iAnyObject *constHoverItem_ListWidget(const iListWidget *d) {
252 return constItem_ListWidget(d, d->hoverItem); 262 return constItem_ListWidget(d, d->hoverItem);
253} 263}
@@ -267,7 +277,7 @@ size_t hoverItemIndex_ListWidget(const iListWidget *d) {
267 return d->hoverItem; 277 return d->hoverItem;
268} 278}
269 279
270static void setHoverItem_ListWidget_(iListWidget *d, size_t index) { 280void setHoverItem_ListWidget(iListWidget *d, size_t index) {
271 if (index < size_PtrArray(&d->items)) { 281 if (index < size_PtrArray(&d->items)) {
272 const iListItem *item = at_PtrArray(&d->items, index); 282 const iListItem *item = at_PtrArray(&d->items, index);
273 if (item->isSeparator) { 283 if (item->isSeparator) {
@@ -284,7 +294,7 @@ static void setHoverItem_ListWidget_(iListWidget *d, size_t index) {
284 294
285void updateMouseHover_ListWidget(iListWidget *d) { 295void updateMouseHover_ListWidget(iListWidget *d) {
286 const iInt2 mouse = mouseCoord_Window(get_Window(), 0); 296 const iInt2 mouse = mouseCoord_Window(get_Window(), 0);
287 setHoverItem_ListWidget_(d, itemIndex_ListWidget(d, mouse)); 297 setHoverItem_ListWidget(d, itemIndex_ListWidget(d, mouse));
288} 298}
289 299
290void sort_ListWidget(iListWidget *d, int (*cmp)(const iListItem **item1, const iListItem **item2)) { 300void sort_ListWidget(iListWidget *d, int (*cmp)(const iListItem **item1, const iListItem **item2)) {
@@ -308,7 +318,51 @@ static void updateHover_ListWidget_(iListWidget *d, const iInt2 mouse) {
308 contains_Widget(constAs_Widget(d), mouse)) { 318 contains_Widget(constAs_Widget(d), mouse)) {
309 hover = itemIndex_ListWidget(d, mouse); 319 hover = itemIndex_ListWidget(d, mouse);
310 } 320 }
311 setHoverItem_ListWidget_(d, hover); 321 setHoverItem_ListWidget(d, hover);
322}
323
324static size_t resolveDragDestination_ListWidget_(const iListWidget *d, iInt2 dstPos, iBool *isOnto) {
325 size_t index = itemIndex_ListWidget(d, dstPos);
326 const iListItem *item = constItem_ListWidget(d, index);
327 if (!item) {
328 index = (dstPos.y < mid_Rect(bounds_Widget(constAs_Widget(d))).y ? 0 : (numItems_ListWidget(d) - 1));
329 item = constItem_ListWidget(d, index);
330 }
331 const iRect rect = itemRect_ListWidget(d, index);
332 const iRangei span = ySpan_Rect(rect);
333 if (item->isDropTarget) {
334 const int pad = size_Range(&span) / 3;
335 if (dstPos.y >= span.start + pad && dstPos.y < span.end - pad) {
336 *isOnto = iTrue;
337 return index;
338 }
339 }
340 if (dstPos.y - span.start > span.end - dstPos.y) {
341 index++;
342 }
343 index = iMin(index, numItems_ListWidget(d));
344 *isOnto = iFalse;
345 return index;
346}
347
348static iBool endDrag_ListWidget_(iListWidget *d, iInt2 endPos) {
349 if (d->dragItem == iInvalidPos) {
350 return iFalse;
351 }
352 stop_Anim(&d->scrollY.pos);
353 iBool isOnto;
354 const size_t index = resolveDragDestination_ListWidget_(d, endPos, &isOnto);
355 if (index != d->dragItem) {
356 if (isOnto) {
357 postCommand_Widget(d, "list.dragged arg:%zu onto:%zu", d->dragItem, index);
358 }
359 else {
360 postCommand_Widget(d, "list.dragged arg:%zu before:%zu", d->dragItem, index);
361 }
362 }
363 invalidateItem_ListWidget(d, d->dragItem);
364 d->dragItem = iInvalidPos;
365 return iTrue;
312} 366}
313 367
314static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { 368static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
@@ -333,10 +387,35 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
333 d->noHoverWhileScrolling = iFalse; 387 d->noHoverWhileScrolling = iFalse;
334 } 388 }
335 if (ev->type == SDL_MOUSEMOTION) { 389 if (ev->type == SDL_MOUSEMOTION) {
336 if (ev->motion.which != SDL_TOUCH_MOUSEID) { 390 const iInt2 mousePos = init_I2(ev->motion.x, ev->motion.y);
337 d->noHoverWhileScrolling = iFalse; 391 if (ev->motion.state == 0 /* not dragging */) {
392 if (ev->motion.which != SDL_TOUCH_MOUSEID) {
393 d->noHoverWhileScrolling = iFalse;
394 }
395 updateHover_ListWidget_(d, mousePos);
396 }
397 else if (d->dragItem != iInvalidPos) {
398 /* Start scrolling if near the ends. */
399 const int zone = 2 * d->itemHeight;
400 const iRect bounds = bounds_Widget(w);
401 float scrollSpeed = 0.0f;
402 if (mousePos.y > bottom_Rect(bounds) - zone) {
403 scrollSpeed = (mousePos.y - bottom_Rect(bounds) + zone) / (float) zone;
404 }
405 else if (mousePos.y < top_Rect(bounds) + zone) {
406 scrollSpeed = -(top_Rect(bounds) + zone - mousePos.y) / (float) zone;
407 }
408 scrollSpeed = iClamp(scrollSpeed, -1.0f, 1.0f);
409 if (iAbs(scrollSpeed) < 0.001f) {
410 stop_Anim(&d->scrollY.pos);
411 refresh_Widget(d);
412 }
413 else {
414 setValueSpeed_Anim(&d->scrollY.pos, scrollSpeed < 0 ? 0 : scrollMax_ListWidget_(d),
415 scrollSpeed * scrollSpeed * gap_UI * 400);
416 refreshWhileScrolling_ListWidget_(d);
417 }
338 } 418 }
339 updateHover_ListWidget_(d, init_I2(ev->motion.x, ev->motion.y));
340 } 419 }
341 if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { 420 if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) {
342 int amount = -ev->wheel.y; 421 int amount = -ev->wheel.y;
@@ -359,12 +438,33 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
359 redrawHoverItem_ListWidget_(d); 438 redrawHoverItem_ListWidget_(d);
360 return iTrue; 439 return iTrue;
361 case aborted_ClickResult: 440 case aborted_ClickResult:
441// endDrag_ListWidget_(d, pos_Click(&d->click));
442 if (d->dragItem != iInvalidPos) {
443 stop_Anim(&d->scrollY.pos);
444 invalidateItem_ListWidget(d, d->dragItem);
445 d->dragItem = iInvalidPos;
446 }
362 redrawHoverItem_ListWidget_(d); 447 redrawHoverItem_ListWidget_(d);
363 break; 448 break;
449 case drag_ClickResult:
450 if (d->dragItem == iInvalidPos && length_I2(delta_Click(&d->click)) > gap_UI) {
451 const size_t over = itemIndex_ListWidget(d, d->click.startPos);
452 if (over != iInvalidPos &&
453 ((const iListItem *) item_ListWidget(d, over))->isDraggable) {
454 d->dragItem = over;
455 d->dragOrigin = sub_I2(topLeft_Rect(itemRect_ListWidget(d, over)),
456 d->click.startPos);
457 invalidateItem_ListWidget(d, d->dragItem);
458 }
459 }
460 return d->dragItem != iInvalidPos;
364 case finished_ClickResult: 461 case finished_ClickResult:
462 if (endDrag_ListWidget_(d, pos_Click(&d->click))) {
463 return iTrue;
464 }
365 redrawHoverItem_ListWidget_(d); 465 redrawHoverItem_ListWidget_(d);
366 if (contains_Rect(innerBounds_Widget(w), pos_Click(&d->click)) && 466 if (contains_Rect(itemRect_ListWidget(d, d->hoverItem), pos_Click(&d->click)) &&
367 d->hoverItem != iInvalidSize) { 467 d->hoverItem != iInvalidPos) {
368 postCommand_Widget(w, "list.clicked arg:%zu item:%p", 468 postCommand_Widget(w, "list.clicked arg:%zu item:%p",
369 d->hoverItem, constHoverItem_ListWidget(d)); 469 d->hoverItem, constHoverItem_ListWidget(d));
370 } 470 }
@@ -391,6 +491,7 @@ static void draw_ListWidget_(const iListWidget *d) {
391 const int scrollY = pos_SmoothScroll(&d->scrollY); 491 const int scrollY = pos_SmoothScroll(&d->scrollY);
392 iPaint p; 492 iPaint p;
393 init_Paint(&p); 493 init_Paint(&p);
494 drawLayerEffects_Widget(w);
394 drawBackground_Widget(w); 495 drawBackground_Widget(w);
395 alloc_VisBuf(d->visBuf, bounds.size, d->itemHeight); 496 alloc_VisBuf(d->visBuf, bounds.size, d->itemHeight);
396 /* Update invalid regions/items. */ { 497 /* Update invalid regions/items. */ {
@@ -433,7 +534,9 @@ static void draw_ListWidget_(const iListWidget *d) {
433 init_I2(d->visBuf->texSize.x, d->itemHeight) }; 534 init_I2(d->visBuf->texSize.x, d->itemHeight) };
434 beginTarget_Paint(&p, buf->texture); 535 beginTarget_Paint(&p, buf->texture);
435 fillRect_Paint(&p, itemRect, bg[i]); 536 fillRect_Paint(&p, itemRect, bg[i]);
436 class_ListItem(item)->draw(item, &p, itemRect, d); 537 if (index != d->dragItem) {
538 class_ListItem(item)->draw(item, &p, itemRect, d);
539 }
437 fillRect_Paint(&p, moved_Rect(sbBlankRect, init_I2(0, top_Rect(itemRect))), bg[i]); 540 fillRect_Paint(&p, moved_Rect(sbBlankRect, init_I2(0, top_Rect(itemRect))), bg[i]);
438 } 541 }
439 } 542 }
@@ -447,7 +550,9 @@ static void draw_ListWidget_(const iListWidget *d) {
447 const iRect itemRect = { init_I2(0, j * d->itemHeight - buf->origin), 550 const iRect itemRect = { init_I2(0, j * d->itemHeight - buf->origin),
448 init_I2(d->visBuf->texSize.x, d->itemHeight) }; 551 init_I2(d->visBuf->texSize.x, d->itemHeight) };
449 fillRect_Paint(&p, itemRect, bg[i]); 552 fillRect_Paint(&p, itemRect, bg[i]);
450 class_ListItem(item)->draw(item, &p, itemRect, d); 553 if (j != d->dragItem) {
554 class_ListItem(item)->draw(item, &p, itemRect, d);
555 }
451 fillRect_Paint(&p, moved_Rect(sbBlankRect, init_I2(0, top_Rect(itemRect))), bg[i]); 556 fillRect_Paint(&p, moved_Rect(sbBlankRect, init_I2(0, top_Rect(itemRect))), bg[i]);
452 } 557 }
453 } 558 }
@@ -458,6 +563,32 @@ static void draw_ListWidget_(const iListWidget *d) {
458 } 563 }
459 setClip_Paint(&p, bounds_Widget(w)); 564 setClip_Paint(&p, bounds_Widget(w));
460 draw_VisBuf(d->visBuf, addY_I2(topLeft_Rect(bounds), -scrollY), ySpan_Rect(bounds)); 565 draw_VisBuf(d->visBuf, addY_I2(topLeft_Rect(bounds), -scrollY), ySpan_Rect(bounds));
566 if (d->dragItem != iInvalidPos) {
567 const iInt2 mousePos = mouseCoord_Window(get_Window(), 0);
568 iInt2 pos = add_I2(mousePos, d->dragOrigin);
569 const iListItem *item = constAt_PtrArray(&d->items, d->dragItem);
570 const iRect itemRect = { init_I2(left_Rect(bounds), pos.y), init_I2(d->visBuf->texSize.x, d->itemHeight) };
571 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
572 iBool dstOnto;
573 const size_t dstIndex = resolveDragDestination_ListWidget_(d, mousePos, &dstOnto);
574 if (dstIndex != d->dragItem) {
575 const iRect dstRect = itemRect_ListWidget(d, dstIndex);
576 p.alpha = 0xff;
577 if (dstOnto) {
578 drawRectThickness_Paint(&p, dstRect, gap_UI / 2, uiTextAction_ColorId);
579 }
580 else if (dstIndex != d->dragItem + 1) {
581 fillRect_Paint(&p, (iRect){ addY_I2(dstRect.pos, -gap_UI / 4),
582 init_I2(width_Rect(dstRect), gap_UI / 2) },
583 uiTextAction_ColorId);
584 }
585 }
586 p.alpha = 0x80;
587 setOpacity_Text(0.5f);
588 class_ListItem(item)->draw(item, &p, itemRect, d);
589 setOpacity_Text(1.0f);
590 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
591 }
461 unsetClip_Paint(&p); 592 unsetClip_Paint(&p);
462 drawChildren_Widget(w); 593 drawChildren_Widget(w);
463} 594}
diff --git a/src/ui/listwidget.h b/src/ui/listwidget.h
index 314c183a..dfa24c07 100644
--- a/src/ui/listwidget.h
+++ b/src/ui/listwidget.h
@@ -39,6 +39,8 @@ struct Impl_ListItem {
39 iObject object; 39 iObject object;
40 iBool isSeparator; 40 iBool isSeparator;
41 iBool isSelected; 41 iBool isSelected;
42 iBool isDraggable;
43 iBool isDropTarget; /* may drag-and-drop another item on this */
42}; 44};
43 45
44iDeclareObjectConstruction(ListItem) 46iDeclareObjectConstruction(ListItem)
@@ -64,6 +66,7 @@ void scrollToItem_ListWidget (iListWidget *, size_t index);
64void scrollOffset_ListWidget (iListWidget *, int offset); 66void scrollOffset_ListWidget (iListWidget *, int offset);
65void updateVisible_ListWidget (iListWidget *); 67void updateVisible_ListWidget (iListWidget *);
66void updateMouseHover_ListWidget (iListWidget *); 68void updateMouseHover_ListWidget (iListWidget *);
69void setHoverItem_ListWidget (iListWidget *, size_t index);
67 70
68void sort_ListWidget (iListWidget *, int (*cmp)(const iListItem **item1, const iListItem **item2)); 71void sort_ListWidget (iListWidget *, int (*cmp)(const iListItem **item1, const iListItem **item2));
69 72
@@ -75,6 +78,7 @@ int visCount_ListWidget (const iListWidget *);
75size_t itemIndex_ListWidget (const iListWidget *, iInt2 pos); 78size_t itemIndex_ListWidget (const iListWidget *, iInt2 pos);
76iRect itemRect_ListWidget (const iListWidget *, size_t index); 79iRect itemRect_ListWidget (const iListWidget *, size_t index);
77const iAnyObject * constItem_ListWidget (const iListWidget *, size_t index); 80const iAnyObject * constItem_ListWidget (const iListWidget *, size_t index);
81const iAnyObject * constDragItem_ListWidget (const iListWidget *);
78const iAnyObject * constHoverItem_ListWidget (const iListWidget *); 82const iAnyObject * constHoverItem_ListWidget (const iListWidget *);
79size_t hoverItemIndex_ListWidget (const iListWidget *); 83size_t hoverItemIndex_ListWidget (const iListWidget *);
80 84
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index a0a507ca..ab649eee 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -171,6 +171,9 @@ static float scoreMatch_(const iRegExp *pattern, iRangecc text) {
171} 171}
172 172
173static float bookmarkRelevance_LookupJob_(const iLookupJob *d, const iBookmark *bm) { 173static float bookmarkRelevance_LookupJob_(const iLookupJob *d, const iBookmark *bm) {
174 if (isFolder_Bookmark(bm)) {
175 return 0.0f;
176 }
174 iUrl parts; 177 iUrl parts;
175 init_Url(&parts, &bm->url); 178 init_Url(&parts, &bm->url);
176 const float t = scoreMatch_(d->term, range_String(&bm->title)); 179 const float t = scoreMatch_(d->term, range_String(&bm->title));
@@ -388,7 +391,7 @@ void init_LookupWidget(iLookupWidget *d) {
388 init_Widget(w); 391 init_Widget(w);
389 setId_Widget(w, "lookup"); 392 setId_Widget(w, "lookup");
390 setFlags_Widget(w, focusable_WidgetFlag, iTrue); 393 setFlags_Widget(w, focusable_WidgetFlag, iTrue);
391#if defined (iPlatformAppleMobile) 394#if defined (iPlatformMobile)
392 setFlags_Widget(w, unhittable_WidgetFlag, iTrue); 395 setFlags_Widget(w, unhittable_WidgetFlag, iTrue);
393#endif 396#endif
394 d->list = addChildFlags_Widget(w, iClob(new_ListWidget()), 397 d->list = addChildFlags_Widget(w, iClob(new_ListWidget()),
@@ -747,12 +750,19 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) {
747 return iTrue; 750 return iTrue;
748 } 751 }
749 } 752 }
750 if (isVisible_Widget(w) && 753 /* Focus switching between URL bar and lookup results. */
751 key == SDLK_DOWN && !mods && focus_Widget() == findWidget_App("url") && 754 if (isVisible_Widget(w)) {
752 numItems_ListWidget(d->list)) { 755 if (((key == SDLK_DOWN && !mods) || key == SDLK_TAB) &&
753 setCursor_LookupWidget_(d, 1); /* item 0 is always the first heading */ 756 focus_Widget() == findWidget_App("url") &&
754 setFocus_Widget(w); 757 numItems_ListWidget(d->list)) {
755 return iTrue; 758 setCursor_LookupWidget_(d, 1); /* item 0 is always the first heading */
759 setFocus_Widget(w);
760 return iTrue;
761 }
762 else if (key == SDLK_TAB && isFocused_Widget(w)) {
763 setFocus_Widget(findWidget_App("url"));
764 return iTrue;
765 }
756 } 766 }
757 } 767 }
758 return processEvent_Widget(w, ev); 768 return processEvent_Widget(w, ev);
diff --git a/src/ui/metrics.c b/src/ui/metrics.c
index 32561ed7..53a52afb 100644
--- a/src/ui/metrics.c
+++ b/src/ui/metrics.c
@@ -33,7 +33,7 @@ iInt2 gap2_UI = { defaultGap_Metrics, defaultGap_Metrics };
33int fontSize_UI = defaultFontSize_Metrics; 33int fontSize_UI = defaultFontSize_Metrics;
34 34
35void setScale_Metrics(float scale) { 35void setScale_Metrics(float scale) {
36#if defined (iPlatformAppleMobile) 36#if defined (iPlatformMobile)
37 /* iPad needs a bit larger UI elements as the viewing distance is generally longer.*/ 37 /* iPad needs a bit larger UI elements as the viewing distance is generally longer.*/
38 if (deviceType_App() == tablet_AppDeviceType) { 38 if (deviceType_App() == tablet_AppDeviceType) {
39 scale *= 1.1f; 39 scale *= 1.1f;
diff --git a/src/ui/mobile.c b/src/ui/mobile.c
index 0ff3fe85..3cb6e631 100644
--- a/src/ui/mobile.c
+++ b/src/ui/mobile.c
@@ -36,7 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
36# include "ios.h" 36# include "ios.h"
37#endif 37#endif
38 38
39static iBool useMobileSheetLayout_(void) { 39iBool isUsingPanelLayout_Mobile(void) {
40 return deviceType_App() != desktop_AppDeviceType; 40 return deviceType_App() != desktop_AppDeviceType;
41} 41}
42 42
@@ -57,11 +57,12 @@ static enum iFontId labelBoldFont_(void) {
57 57
58static void updatePanelSheetMetrics_(iWidget *sheet) { 58static void updatePanelSheetMetrics_(iWidget *sheet) {
59 iWidget *navi = findChild_Widget(sheet, "panel.navi"); 59 iWidget *navi = findChild_Widget(sheet, "panel.navi");
60 iWidget *naviPad = child_Widget(navi, 0);
61 int naviHeight = lineHeight_Text(labelFont_()) + 4 * gap_UI; 60 int naviHeight = lineHeight_Text(labelFont_()) + 4 * gap_UI;
61#if defined (iPlatformMobile)
62 float left = 0.0f, right = 0.0f, top = 0.0f, bottom = 0.0f;
62#if defined (iPlatformAppleMobile) 63#if defined (iPlatformAppleMobile)
63 float left, right, top, bottom;
64 safeAreaInsets_iOS(&left, &top, &right, &bottom); 64 safeAreaInsets_iOS(&left, &top, &right, &bottom);
65#endif
65 setPadding_Widget(sheet, left, 0, right, 0); 66 setPadding_Widget(sheet, left, 0, right, 0);
66 navi->rect.pos = init_I2(left, top); 67 navi->rect.pos = init_I2(left, top);
67 iConstForEach(PtrArray, i, findChildren_Widget(sheet, "panel.toppad")) { 68 iConstForEach(PtrArray, i, findChildren_Widget(sheet, "panel.toppad")) {
@@ -87,17 +88,28 @@ static void unselectAllPanelButtons_(iWidget *topPanel) {
87 } 88 }
88} 89}
89 90
91static iWidget *findTitleLabel_(iWidget *panel) {
92 iForEach(ObjectList, i, children_Widget(panel)) {
93 iWidget *child = i.object;
94 if (flags_Widget(child) & collapse_WidgetFlag &&
95 isInstance_Object(child, &Class_LabelWidget)) {
96 return child;
97 }
98 }
99 return NULL;
100}
101
90static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) { 102static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) {
91 if (equal_Command(cmd, "window.resized")) { 103 if (equal_Command(cmd, "window.resized")) {
92 const iBool isPortrait = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); 104 const iBool isPortrait = (deviceType_App() == phone_AppDeviceType && isPortrait_App());
93 const iRect safeRoot = safeRect_Root(mainDetailSplit->root); 105 const iRect safeRoot = safeRect_Root(mainDetailSplit->root);
94 setPos_Widget(mainDetailSplit, topLeft_Rect(safeRoot));
95 setFixedSize_Widget(mainDetailSplit, safeRoot.size);
96 iWidget * sheet = parent_Widget(mainDetailSplit); 106 iWidget * sheet = parent_Widget(mainDetailSplit);
97 iWidget * navi = findChild_Widget(sheet, "panel.navi"); 107 iWidget * navi = findChild_Widget(sheet, "panel.navi");
98 iWidget * detailStack = findChild_Widget(mainDetailSplit, "detailstack"); 108 iWidget * detailStack = findChild_Widget(mainDetailSplit, "detailstack");
99 const size_t numPanels = childCount_Widget(detailStack); 109 const size_t numPanels = childCount_Widget(detailStack);
100 const iBool isSideBySide = isSideBySideLayout_() && numPanels > 0; 110 const iBool isSideBySide = isSideBySideLayout_() && numPanels > 0;
111 setPos_Widget(mainDetailSplit, topLeft_Rect(safeRoot));
112 setFixedSize_Widget(mainDetailSplit, safeRoot.size);
101 setFlags_Widget(mainDetailSplit, arrangeHorizontal_WidgetFlag, isSideBySide); 113 setFlags_Widget(mainDetailSplit, arrangeHorizontal_WidgetFlag, isSideBySide);
102 setFlags_Widget(detailStack, expand_WidgetFlag, isSideBySide); 114 setFlags_Widget(detailStack, expand_WidgetFlag, isSideBySide);
103 setFlags_Widget(detailStack, hidden_WidgetFlag, numPanels == 0); 115 setFlags_Widget(detailStack, hidden_WidgetFlag, numPanels == 0);
@@ -107,7 +119,7 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd)
107 iAssert(topPanel); 119 iAssert(topPanel);
108 topPanel->rect.size.x = (deviceType_App() == phone_AppDeviceType ? 120 topPanel->rect.size.x = (deviceType_App() == phone_AppDeviceType ?
109 safeRoot.size.x * 2 / 5 : (safeRoot.size.x / 3)); 121 safeRoot.size.x * 2 / 5 : (safeRoot.size.x / 3));
110 } 122 }
111 if (deviceType_App() == tablet_AppDeviceType) { 123 if (deviceType_App() == tablet_AppDeviceType) {
112 setPadding_Widget(topPanel, pad, 0, pad, pad); 124 setPadding_Widget(topPanel, pad, 0, pad, pad);
113 if (numPanels == 0) { 125 if (numPanels == 0) {
@@ -118,8 +130,15 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd)
118 setFixedSize_Widget(navi, init_I2(sheetWidth, -1)); 130 setFixedSize_Widget(navi, init_I2(sheetWidth, -1));
119 } 131 }
120 } 132 }
133 iWidget *detailTitle = findChild_Widget(navi, "detailtitle"); {
134 setPos_Widget(detailTitle, init_I2(width_Widget(topPanel), 0));
135 setFixedSize_Widget(detailTitle,
136 init_I2(width_Widget(detailStack), height_Widget(navi)));
137 setFlags_Widget(detailTitle, hidden_WidgetFlag, !isSideBySide);
138 }
121 iForEach(ObjectList, i, children_Widget(detailStack)) { 139 iForEach(ObjectList, i, children_Widget(detailStack)) {
122 iWidget *panel = i.object; 140 iWidget *panel = i.object;
141 setFlags_Widget(findTitleLabel_(panel), hidden_WidgetFlag, isSideBySide);
123 setFlags_Widget(panel, leftEdgeDraggable_WidgetFlag, !isSideBySide); 142 setFlags_Widget(panel, leftEdgeDraggable_WidgetFlag, !isSideBySide);
124 if (isSideBySide) { 143 if (isSideBySide) {
125 setVisualOffset_Widget(panel, 0, 0, 0); 144 setVisualOffset_Widget(panel, 0, 0, 0);
@@ -128,9 +147,27 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd)
128 } 147 }
129 arrange_Widget(mainDetailSplit); 148 arrange_Widget(mainDetailSplit);
130 } 149 }
150 else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) {
151 if (focus_Widget() && class_Widget(focus_Widget()) == &Class_InputWidget) {
152 setFocus_Widget(NULL);
153 return iTrue;
154 }
155 }
131 return iFalse; 156 return iFalse;
132} 157}
133 158
159size_t currentPanelIndex_Mobile(const iWidget *panels) {
160 size_t index = 0;
161 iConstForEach(ObjectList, i, children_Widget(findChild_Widget(panels, "detailstack"))) {
162 const iWidget *child = i.object;
163 if (isVisible_Widget(child)) {
164 return index;
165 }
166 index++;
167 }
168 return iInvalidPos;
169}
170
134static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { 171static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) {
135 const iBool isPortrait = !isSideBySideLayout_(); 172 const iBool isPortrait = !isSideBySideLayout_();
136 if (equal_Command(cmd, "panel.open")) { 173 if (equal_Command(cmd, "panel.open")) {
@@ -147,6 +184,12 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) {
147 setupSheetTransition_Mobile(panel, iTrue); 184 setupSheetTransition_Mobile(panel, iTrue);
148 } 185 }
149 } 186 }
187 iLabelWidget *detailTitle =
188 findChild_Widget(parent_Widget(parent_Widget(topPanel)), "detailtitle");
189// setFlags_Widget(as_Widget(detailTitle), hidden_WidgetFlag, !isSideBySideLayout_());
190 setFont_LabelWidget(detailTitle, uiLabelLargeBold_FontId);
191 setTextColor_LabelWidget(detailTitle, uiHeading_ColorId);
192 setText_LabelWidget(detailTitle, text_LabelWidget((iLabelWidget *) findTitleLabel_(panel)));
150 setFlags_Widget(button, selected_WidgetFlag, iTrue); 193 setFlags_Widget(button, selected_WidgetFlag, iTrue);
151 return iTrue; 194 return iTrue;
152 } 195 }
@@ -171,7 +214,22 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) {
171 } 214 }
172 unselectAllPanelButtons_(topPanel); 215 unselectAllPanelButtons_(topPanel);
173 if (!wasClosed) { 216 if (!wasClosed) {
174 postCommand_App("prefs.dismiss"); 217 /* TODO: Should come up with a more general-purpose approach here. */
218 if (findWidget_App("prefs")) {
219 postCommand_App("prefs.dismiss");
220 }
221 else if (findWidget_App("upload")) {
222 postCommand_App("upload.cancel");
223 }
224 else if (findWidget_App("ident")) {
225 postCommand_Widget(topPanel, "ident.cancel");
226 }
227 else if (findWidget_App("xlt")) {
228 postCommand_Widget(topPanel, "translation.cancel");
229 }
230 else {
231 postCommand_Widget(topPanel, "cancel");
232 }
175 } 233 }
176 return iTrue; 234 return iTrue;
177 } 235 }
@@ -186,6 +244,7 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) {
186 return iFalse; 244 return iFalse;
187} 245}
188 246
247#if 0
189static iBool isTwoColumnPage_(iWidget *d) { 248static iBool isTwoColumnPage_(iWidget *d) {
190 if (cmp_String(id_Widget(d), "dialogbuttons") == 0 || 249 if (cmp_String(id_Widget(d), "dialogbuttons") == 0 ||
191 cmp_String(id_Widget(d), "prefs.tabs") == 0) { 250 cmp_String(id_Widget(d), "prefs.tabs") == 0) {
@@ -273,12 +332,13 @@ static void stripTrailingColon_(iLabelWidget *label) {
273 delete_String(mod); 332 delete_String(mod);
274 } 333 }
275} 334}
335#endif
276 336
277static iLabelWidget *makePanelButton_(const char *text, const char *command) { 337static iLabelWidget *makePanelButton_(const char *text, const char *command) {
278 iLabelWidget *btn = new_LabelWidget(text, command); 338 iLabelWidget *btn = new_LabelWidget(text, command);
279 setFlags_Widget(as_Widget(btn), 339 setFlags_Widget(as_Widget(btn),
280 borderBottom_WidgetFlag | alignLeft_WidgetFlag | 340 borderTop_WidgetFlag | borderBottom_WidgetFlag | alignLeft_WidgetFlag |
281 frameless_WidgetFlag | extraPadding_WidgetFlag, 341 frameless_WidgetFlag | extraPadding_WidgetFlag,
282 iTrue); 342 iTrue);
283 checkIcon_LabelWidget(btn); 343 checkIcon_LabelWidget(btn);
284 setFont_LabelWidget(btn, labelFont_()); 344 setFont_LabelWidget(btn, labelFont_());
@@ -298,11 +358,9 @@ static iWidget *makeValuePadding_(iWidget *value) {
298 setPadding_Widget(pad, 0, 1 * gap_UI, 0, 1 * gap_UI); 358 setPadding_Widget(pad, 0, 1 * gap_UI, 0, 1 * gap_UI);
299 addChild_Widget(pad, iClob(value)); 359 addChild_Widget(pad, iClob(value));
300 setFlags_Widget(pad, 360 setFlags_Widget(pad,
301 borderBottom_WidgetFlag | 361 borderTop_WidgetFlag | borderBottom_WidgetFlag | arrangeVertical_WidgetFlag |
302 arrangeVertical_WidgetFlag | 362 resizeToParentWidth_WidgetFlag | resizeWidthOfChildren_WidgetFlag |
303 resizeToParentWidth_WidgetFlag | 363 arrangeHeight_WidgetFlag,
304 resizeWidthOfChildren_WidgetFlag |
305 arrangeHeight_WidgetFlag,
306 iTrue); 364 iTrue);
307 return pad; 365 return pad;
308} 366}
@@ -311,7 +369,7 @@ static iWidget *makeValuePaddingWithHeading_(iLabelWidget *heading, iWidget *val
311 const iBool isInput = isInstance_Object(value, &Class_InputWidget); 369 const iBool isInput = isInstance_Object(value, &Class_InputWidget);
312 iWidget *div = new_Widget(); 370 iWidget *div = new_Widget();
313 setFlags_Widget(div, 371 setFlags_Widget(div,
314 borderBottom_WidgetFlag | arrangeHeight_WidgetFlag | 372 borderTop_WidgetFlag | borderBottom_WidgetFlag | arrangeHeight_WidgetFlag |
315 resizeWidthOfChildren_WidgetFlag | 373 resizeWidthOfChildren_WidgetFlag |
316 arrangeHorizontal_WidgetFlag, iTrue); 374 arrangeHorizontal_WidgetFlag, iTrue);
317 setBackgroundColor_Widget(div, uiBackgroundSidebar_ColorId); 375 setBackgroundColor_Widget(div, uiBackgroundSidebar_ColorId);
@@ -321,7 +379,7 @@ static iWidget *makeValuePaddingWithHeading_(iLabelWidget *heading, iWidget *val
321 //setFixedSize_Widget(as_Widget(heading), init_I2(-1, height_Widget(value))); 379 //setFixedSize_Widget(as_Widget(heading), init_I2(-1, height_Widget(value)));
322 setFont_LabelWidget(heading, labelFont_()); 380 setFont_LabelWidget(heading, labelFont_());
323 setTextColor_LabelWidget(heading, uiTextStrong_ColorId); 381 setTextColor_LabelWidget(heading, uiTextStrong_ColorId);
324 if (isInput) { 382 if (isInput && ~value->flags & fixedWidth_WidgetFlag) {
325 addChildFlags_Widget(div, iClob(value), expand_WidgetFlag); 383 addChildFlags_Widget(div, iClob(value), expand_WidgetFlag);
326 } 384 }
327 else if (isInstance_Object(value, &Class_LabelWidget) && 385 else if (isInstance_Object(value, &Class_LabelWidget) &&
@@ -337,7 +395,7 @@ static iWidget *makeValuePaddingWithHeading_(iLabelWidget *heading, iWidget *val
337 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); 395 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag);
338 addChild_Widget(div, iClob(value)); 396 addChild_Widget(div, iClob(value));
339 } 397 }
340 printTree_Widget(div); 398// printTree_Widget(div);
341 return div; 399 return div;
342} 400}
343 401
@@ -347,6 +405,7 @@ static iWidget *addChildPanel_(iWidget *parent, iLabelWidget *panelButton,
347 setId_Widget(panel, "panel"); 405 setId_Widget(panel, "panel");
348 setUserData_Object(panelButton, panel); 406 setUserData_Object(panelButton, panel);
349 setBackgroundColor_Widget(panel, uiBackground_ColorId); 407 setBackgroundColor_Widget(panel, uiBackground_ColorId);
408 setDrawBufferEnabled_Widget(panel, iTrue);
350 setId_Widget(addChild_Widget(panel, iClob(makePadding_Widget(0))), "panel.toppad"); 409 setId_Widget(addChild_Widget(panel, iClob(makePadding_Widget(0))), "panel.toppad");
351 if (titleText) { 410 if (titleText) {
352 iLabelWidget *title = 411 iLabelWidget *title =
@@ -366,7 +425,396 @@ static iWidget *addChildPanel_(iWidget *parent, iLabelWidget *panelButton,
366 return panel; 425 return panel;
367} 426}
368 427
369void finalizeSheet_Mobile(iWidget *sheet) { 428//void finalizeSheet_Mobile(iWidget *sheet) {
429// arrange_Widget(sheet);
430// postRefresh_App();
431//}
432
433static size_t countItems_(const iMenuItem *itemsNullTerminated) {
434 size_t num = 0;
435 for (; itemsNullTerminated->label; num++, itemsNullTerminated++) {}
436 return num;
437}
438
439static iBool dropdownHeadingHandler_(iWidget *d, const char *cmd) {
440 if (isVisible_Widget(d) &&
441 equal_Command(cmd, "mouse.clicked") && contains_Widget(d, coord_Command(cmd)) &&
442 arg_Command(cmd)) {
443 postCommand_Widget(userData_Object(d),
444 cstr_String(command_LabelWidget(userData_Object(d))));
445 return iTrue;
446 }
447 return iFalse;
448}
449
450static iBool inputHeadingHandler_(iWidget *d, const char *cmd) {
451 if (isVisible_Widget(d) &&
452 equal_Command(cmd, "mouse.clicked") && contains_Widget(d, coord_Command(cmd)) &&
453 arg_Command(cmd)) {
454 setFocus_Widget(userData_Object(d));
455 return iTrue;
456 }
457 return iFalse;
458}
459
460void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) {
461 iWidget * widget = NULL;
462 iLabelWidget *heading = NULL;
463 iWidget * value = NULL;
464 const char * spec = item->label;
465 const char * id = cstr_Rangecc(range_Command(spec, "id"));
466 const char * label = hasLabel_Command(spec, "text")
467 ? suffixPtr_Command(spec, "text")
468 : format_CStr("${%s}", id);
469 if (hasLabel_Command(spec, "device") && deviceType_App() != argLabel_Command(spec, "device")) {
470 return;
471 }
472 if (equal_Command(spec, "title")) {
473 iLabelWidget *title = addChildFlags_Widget(panel,
474 iClob(new_LabelWidget(label, NULL)),
475 alignLeft_WidgetFlag | frameless_WidgetFlag |
476 collapse_WidgetFlag);
477 setFont_LabelWidget(title, uiLabelLargeBold_FontId);
478 setTextColor_LabelWidget(title, uiHeading_ColorId);
479 setAllCaps_LabelWidget(title, iTrue);
480 setId_Widget(as_Widget(title), id);
481 }
482 else if (equal_Command(spec, "heading")) {
483 addChild_Widget(panel, iClob(makePadding_Widget(lineHeight_Text(labelFont_()))));
484 heading = makeHeading_Widget(label);
485 setAllCaps_LabelWidget(heading, iTrue);
486 setRemoveTrailingColon_LabelWidget(heading, iTrue);
487 addChild_Widget(panel, iClob(heading));
488 setId_Widget(as_Widget(heading), id);
489 }
490 else if (equal_Command(spec, "toggle")) {
491 iLabelWidget *toggle = (iLabelWidget *) makeToggle_Widget(id);
492 setFont_LabelWidget(toggle, labelFont_());
493 widget = makeValuePaddingWithHeading_(heading = makeHeading_Widget(label),
494 as_Widget(toggle));
495 }
496 else if (equal_Command(spec, "dropdown")) {
497 const iMenuItem *dropItems = item->data;
498 iLabelWidget *drop = makeMenuButton_LabelWidget(dropItems[0].label,
499 dropItems, countItems_(dropItems));
500 value = as_Widget(drop);
501 setFont_LabelWidget(drop, labelFont_());
502 setFlags_Widget(as_Widget(drop),
503 alignRight_WidgetFlag | noBackground_WidgetFlag |
504 frameless_WidgetFlag, iTrue);
505 setId_Widget(as_Widget(drop), id);
506 widget = makeValuePaddingWithHeading_(heading = makeHeading_Widget(label), as_Widget(drop));
507 setCommandHandler_Widget(widget, dropdownHeadingHandler_);
508 setUserData_Object(widget, drop);
509 }
510 else if (equal_Command(spec, "radio") || equal_Command(spec, "buttons")) {
511 const iBool isRadio = equal_Command(spec, "radio");
512 addChild_Widget(panel, iClob(makePadding_Widget(lineHeight_Text(labelFont_()))));
513 iLabelWidget *head = makeHeading_Widget(label);
514 setAllCaps_LabelWidget(head, iTrue);
515 setRemoveTrailingColon_LabelWidget(head, iTrue);
516 addChild_Widget(panel, iClob(head));
517 widget = new_Widget();
518 setBackgroundColor_Widget(widget, uiBackgroundSidebar_ColorId);
519 setPadding_Widget(widget, 4 * gap_UI, 2 * gap_UI, 4 * gap_UI, 2 * gap_UI);
520 setFlags_Widget(widget,
521 borderTop_WidgetFlag |
522 borderBottom_WidgetFlag |
523 arrangeHorizontal_WidgetFlag |
524 arrangeHeight_WidgetFlag |
525 resizeToParentWidth_WidgetFlag |
526 resizeWidthOfChildren_WidgetFlag,
527 iTrue);
528 setId_Widget(widget, id);
529 for (const iMenuItem *radioItem = item->data; radioItem->label; radioItem++) {
530 const char * radId = cstr_Rangecc(range_Command(radioItem->label, "id"));
531 int64_t flags = noBackground_WidgetFlag;
532 iLabelWidget *button;
533 if (isRadio) {
534 const char *radLabel =
535 hasLabel_Command(radioItem->label, "label")
536 ? format_CStr("${%s}",
537 cstr_Rangecc(range_Command(radioItem->label, "label")))
538 : suffixPtr_Command(radioItem->label, "text");
539 button = new_LabelWidget(radLabel, radioItem->command);
540 flags |= radio_WidgetFlag;
541 }
542 else {
543 button = (iLabelWidget *) makeToggle_Widget(radId);
544 setTextCStr_LabelWidget(button, format_CStr("${%s}", radId));
545 setFlags_Widget(as_Widget(button), fixedWidth_WidgetFlag, iFalse);
546 updateSize_LabelWidget(button);
547 }
548 setId_Widget(as_Widget(button), radId);
549 setFont_LabelWidget(button, defaultMedium_FontId);
550 addChildFlags_Widget(widget, iClob(button), flags);
551 }
552 }
553 else if (equal_Command(spec, "input")) {
554 iInputWidget *input = new_InputWidget(argU32Label_Command(spec, "maxlen"));
555 if (hasLabel_Command(spec, "hint")) {
556 setHint_InputWidget(input, cstr_Lang(cstr_Rangecc(range_Command(spec, "hint"))));
557 }
558 setId_Widget(as_Widget(input), id);
559 setUrlContent_InputWidget(input, argLabel_Command(spec, "url"));
560 setSelectAllOnFocus_InputWidget(input, argLabel_Command(spec, "selectall"));
561 setFont_InputWidget(input, labelFont_());
562 if (argLabel_Command(spec, "noheading")) {
563 widget = makeValuePadding_(as_Widget(input));
564 setFlags_Widget(widget, expand_WidgetFlag, iTrue);
565 }
566 else {
567 setContentPadding_InputWidget(input, 3 * gap_UI, 0);
568 if (hasLabel_Command(spec, "unit")) {
569 iWidget *unit = addChildFlags_Widget(
570 as_Widget(input),
571 iClob(new_LabelWidget(
572 format_CStr("${%s}", cstr_Rangecc(range_Command(spec, "unit"))), NULL)),
573 frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag |
574 resizeToParentHeight_WidgetFlag);
575 setContentPadding_InputWidget(input, -1, width_Widget(unit) - 4 * gap_UI);
576 }
577 widget = makeValuePaddingWithHeading_(heading = makeHeading_Widget(label),
578 as_Widget(input));
579 setCommandHandler_Widget(widget, inputHeadingHandler_);
580 setUserData_Object(widget, input);
581 }
582 }
583 else if (equal_Command(spec, "button")) {
584 widget = as_Widget(heading = makePanelButton_(label, item->command));
585 setFlags_Widget(widget, selected_WidgetFlag, argLabel_Command(spec, "selected") != 0);
586 }
587 else if (equal_Command(spec, "label")) {
588 iLabelWidget *lab = new_LabelWidget(label, NULL);
589 widget = as_Widget(lab);
590 setId_Widget(widget, id);
591 setWrap_LabelWidget(lab, !argLabel_Command(spec, "nowrap"));
592 setFlags_Widget(widget,
593 fixedHeight_WidgetFlag |
594 (!argLabel_Command(spec, "frame") ? frameless_WidgetFlag : 0),
595 iTrue);
596 }
597 else if (equal_Command(spec, "padding")) {
598 float height = 1.5f;
599 if (hasLabel_Command(spec, "arg")) {
600 height *= argfLabel_Command(spec, "arg");
601 }
602 widget = makePadding_Widget(lineHeight_Text(labelFont_()) * height);
603 }
604 /* Apply common styling to the heading. */
605 if (heading) {
606 setRemoveTrailingColon_LabelWidget(heading, iTrue);
607 const iChar icon = toInt_String(string_Command(item->label, "icon"));
608 if (icon) {
609 setIcon_LabelWidget(heading, icon);
610 }
611 if (value && as_Widget(heading) != value) {
612 as_Widget(heading)->sizeRef = value; /* heading height matches value widget */
613 }
614 }
615 if (widget) {
616 setFlags_Widget(widget,
617 collapse_WidgetFlag | hidden_WidgetFlag,
618 argLabel_Command(spec, "collapse") != 0);
619 addChild_Widget(panel, iClob(widget));
620 }
621}
622
623void makePanelItems_Mobile(iWidget *panel, const iMenuItem *itemsNullTerminated) {
624 for (const iMenuItem *item = itemsNullTerminated; item->label; item++) {
625 makePanelItem_Mobile(panel, item);
626 }
627}
628
629static const iMenuItem *findDialogCancelAction_(const iMenuItem *items, size_t n) {
630 if (n <= 1) {
631 return NULL;
632 }
633 for (size_t i = 0; i < n; i++) {
634 if (!iCmpStr(items[i].label, "${cancel}") || !iCmpStr(items[i].label, "${close}")) {
635 return &items[i];
636 }
637 }
638 return NULL;
639}
640
641iWidget *makePanels_Mobile(const char *id,
642 const iMenuItem *itemsNullTerminated,
643 const iMenuItem *actions, size_t numActions) {
644 return makePanelsParent_Mobile(get_Root()->widget, id, itemsNullTerminated, actions, numActions);
645}
646
647iWidget *makePanelsParent_Mobile(iWidget *parentWidget,
648 const char *id,
649 const iMenuItem *itemsNullTerminated,
650 const iMenuItem *actions, size_t numActions) {
651 iWidget *panels = new_Widget();
652 setId_Widget(panels, id);
653 initPanels_Mobile(panels, parentWidget, itemsNullTerminated, actions, numActions);
654 return panels;
655}
656
657void initPanels_Mobile(iWidget *panels, iWidget *parentWidget,
658 const iMenuItem *itemsNullTerminated,
659 const iMenuItem *actions, size_t numActions) {
660 /* A multipanel widget has a top panel and one or more detail panels. In a horizontal layout,
661 the detail panels slide in from the right and cover the top panel. In a landscape layout,
662 the detail panels are always visible on the side. */
663 setBackgroundColor_Widget(panels, uiBackground_ColorId);
664 setFlags_Widget(panels,
665 resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag |
666 frameless_WidgetFlag | focusRoot_WidgetFlag | commandOnClick_WidgetFlag |
667 /*overflowScrollable_WidgetFlag |*/ leftEdgeDraggable_WidgetFlag,
668 iTrue);
669 setFlags_Widget(panels, overflowScrollable_WidgetFlag, iFalse);
670 /* The top-level split between main and detail panels. */
671 iWidget *mainDetailSplit = makeHDiv_Widget(); {
672 setCommandHandler_Widget(mainDetailSplit, mainDetailSplitHandler_);
673 setFlags_Widget(mainDetailSplit, resizeHeightOfChildren_WidgetFlag, iFalse);
674 setId_Widget(mainDetailSplit, "mdsplit");
675 addChild_Widget(panels, iClob(mainDetailSplit));
676 }
677 /* The panel roots. */
678 iWidget *topPanel = new_Widget(); {
679 setId_Widget(topPanel, "panel.top");
680 setDrawBufferEnabled_Widget(topPanel, iTrue);
681 setCommandHandler_Widget(topPanel, topPanelHandler_);
682 setFlags_Widget(topPanel,
683 arrangeVertical_WidgetFlag | resizeWidthOfChildren_WidgetFlag |
684 arrangeHeight_WidgetFlag | overflowScrollable_WidgetFlag |
685 commandOnClick_WidgetFlag,
686 iTrue);
687 addChild_Widget(mainDetailSplit, iClob(topPanel));
688 setId_Widget(addChild_Widget(topPanel, iClob(makePadding_Widget(0))), "panel.toppad");
689 }
690 iWidget *detailStack = new_Widget(); {
691 setId_Widget(detailStack, "detailstack");
692 setFlags_Widget(detailStack, collapse_WidgetFlag | resizeWidthOfChildren_WidgetFlag, iTrue);
693 addChild_Widget(mainDetailSplit, iClob(detailStack));
694 }
695 /* Slide top panel with detail panels. */ {
696 setFlags_Widget(topPanel, refChildrenOffset_WidgetFlag, iTrue);
697 topPanel->offsetRef = detailStack;
698 }
699 /* Navigation bar at the top. */
700 iLabelWidget *naviBack;
701 iWidget *navi = new_Widget(); {
702 setId_Widget(navi, "panel.navi");
703 setBackgroundColor_Widget(navi, uiBackground_ColorId);
704 setId_Widget(addChildFlags_Widget(navi,
705 iClob(new_LabelWidget("", NULL)),
706 alignLeft_WidgetFlag | fixedPosition_WidgetFlag |
707 fixedSize_WidgetFlag | hidden_WidgetFlag |
708 frameless_WidgetFlag),
709 "detailtitle");
710 naviBack = addChildFlags_Widget(
711 navi,
712 iClob(newKeyMods_LabelWidget(
713 leftAngle_Icon " ${panel.back}", SDLK_ESCAPE, 0, "panel.close")),
714 noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag |
715 extraPadding_WidgetFlag);
716 checkIcon_LabelWidget(naviBack);
717 setId_Widget(as_Widget(naviBack), "panel.back");
718 setFont_LabelWidget(naviBack, labelFont_());
719 addChildFlags_Widget(panels, iClob(navi),
720 drawBackgroundToVerticalSafeArea_WidgetFlag |
721 arrangeHeight_WidgetFlag | resizeWidthOfChildren_WidgetFlag |
722 resizeToParentWidth_WidgetFlag | arrangeVertical_WidgetFlag);
723 }
724 iBool haveDetailPanels = iFalse;
725 /* Create panel contents based on provided items. */
726 for (size_t i = 0; itemsNullTerminated[i].label; i++) {
727 const iMenuItem *item = &itemsNullTerminated[i];
728 if (equal_Command(item->label, "panel")) {
729 haveDetailPanels = iTrue;
730 const char *id = cstr_Rangecc(range_Command(item->label, "id"));
731 const iString *label = hasLabel_Command(item->label, "text")
732 ? collect_String(suffix_Command(item->label, "text"))
733 : collectNewFormat_String("${%s}", id);
734 iLabelWidget * button =
735 addChildFlags_Widget(topPanel,
736 iClob(makePanelButton_(cstr_String(label), "panel.open")),
737 chevron_WidgetFlag | borderTop_WidgetFlag);
738 const iChar icon = toInt_String(string_Command(item->label, "icon"));
739 if (icon) {
740 setIcon_LabelWidget(button, icon);
741 }
742 iWidget *panel = addChildPanel_(detailStack, button, NULL);
743 makePanelItems_Mobile(panel, item->data);
744 }
745 else {
746 makePanelItem_Mobile(topPanel, item);
747 }
748 }
749 /* Actions. */
750 if (numActions) {
751 /* Some actions go in the navigation bar and some go on the top panel. */
752 const iMenuItem *cancelItem = findDialogCancelAction_(actions, numActions);
753 const iMenuItem *defaultItem = &actions[numActions - 1];
754 iAssert(defaultItem);
755 if (defaultItem && !cancelItem) {
756 updateTextCStr_LabelWidget(naviBack, defaultItem->label);
757 setCommand_LabelWidget(naviBack, collectNewCStr_String(defaultItem->command));
758 setFlags_Widget(as_Widget(naviBack), alignLeft_WidgetFlag, iFalse);
759 setFlags_Widget(as_Widget(naviBack), alignRight_WidgetFlag, iTrue);
760 setIcon_LabelWidget(naviBack, 0);
761 setFont_LabelWidget(naviBack, labelBoldFont_());
762 }
763 else if (defaultItem && defaultItem != cancelItem) {
764 if (!haveDetailPanels) {
765 updateTextCStr_LabelWidget(naviBack, cancelItem->label);
766 setCommand_LabelWidget(naviBack, collectNewCStr_String(cancelItem->command
767 ? cancelItem->command
768 : "cancel"));
769 }
770 iLabelWidget *defaultButton = new_LabelWidget(defaultItem->label, defaultItem->command);
771 setFont_LabelWidget(defaultButton, labelBoldFont_());
772 setFlags_Widget(as_Widget(defaultButton),
773 frameless_WidgetFlag | extraPadding_WidgetFlag |
774 noBackground_WidgetFlag,
775 iTrue);
776 addChildFlags_Widget(as_Widget(naviBack), iClob(defaultButton),
777 moveToParentRightEdge_WidgetFlag);
778 updateSize_LabelWidget(defaultButton);
779 }
780 /* All other actions are added as buttons. */
781 iBool needPadding = iTrue;
782 for (size_t i = 0; i < numActions; i++) {
783 const iMenuItem *act = &actions[i];
784 if (act == cancelItem || act == defaultItem) {
785 continue;
786 }
787 const char *label = act->label;
788 if (*label == '*' || *label == '&') {
789 continue; /* Special value selection items for a Question dialog. */
790 }
791 if (!iCmpStr(label, "---")) {
792 continue; /* Separator. */
793 }
794 if (needPadding) {
795 makePanelItem_Mobile(topPanel, &(iMenuItem){ "padding" });
796 needPadding = iFalse;
797 }
798 makePanelItem_Mobile(
799 topPanel,
800 &(iMenuItem){ format_CStr("button text:" uiTextAction_ColorEscape "%s", act->label),
801 0,
802 0,
803 act->command });
804 }
805 }
806 /* Finalize the layout. */
807 if (parentWidget) {
808 addChild_Widget(parentWidget, iClob(panels));
809 }
810 mainDetailSplitHandler_(mainDetailSplit, "window.resized"); /* make it resize the split */
811 updatePanelSheetMetrics_(panels);
812 arrange_Widget(panels);
813 postCommand_App("widget.overflow"); /* with the correct dimensions */
814// printTree_Widget(panels);
815}
816
817#if 0
370 /* The sheet contents are completely rearranged and restyled on a phone. 818 /* The sheet contents are completely rearranged and restyled on a phone.
371 We'll set up a linear fullscreen arrangement of the widgets. Sheets are already 819 We'll set up a linear fullscreen arrangement of the widgets. Sheets are already
372 scrollable so they can be taller than the display. In hindsight, it may have been 820 scrollable so they can be taller than the display. In hindsight, it may have been
@@ -397,7 +845,7 @@ void finalizeSheet_Mobile(iWidget *sheet) {
397 │ │ └┤ ││ │ │└┤ ││ 845 │ │ └┤ ││ │ │└┤ ││
398 │ │ └───────────────────┘│ │ │ └──────┘ 846 │ │ └───────────────────┘│ │ │ └──────┘
399 └─────────┴───────────────────────┘ └─────────┴ ─ ─ ─ ─ ┘ 847 └─────────┴───────────────────────┘ └─────────┴ ─ ─ ─ ─ ┘
400 offscreen 848 underneath
401 */ 849 */
402 /* Modify the top sheet to act as a fullscreen background. */ 850 /* Modify the top sheet to act as a fullscreen background. */
403 setPadding1_Widget(sheet, 0); 851 setPadding1_Widget(sheet, 0);
@@ -772,9 +1220,10 @@ void finalizeSheet_Mobile(iWidget *sheet) {
772 } 1220 }
773 postRefresh_App(); 1221 postRefresh_App();
774} 1222}
1223#endif
775 1224
776void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) { 1225void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) {
777 if (!useMobileSheetLayout_()) { 1226 if (!isUsingPanelLayout_Mobile()) {
778 return; 1227 return;
779 } 1228 }
780 const iBool isSlidePanel = (flags_Widget(sheet) & horizontalOffset_WidgetFlag) != 0; 1229 const iBool isSlidePanel = (flags_Widget(sheet) & horizontalOffset_WidgetFlag) != 0;
@@ -794,8 +1243,10 @@ void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) {
794 } 1243 }
795} 1244}
796 1245
797void setupSheetTransition_Mobile(iWidget *sheet, iBool isIncoming) { 1246void setupSheetTransition_Mobile(iWidget *sheet, int flags) {
798 if (!useMobileSheetLayout_()) { 1247 const iBool isIncoming = (flags & incoming_TransitionFlag) != 0;
1248 const int dir = flags & dirMask_TransitionFlag;
1249 if (!isUsingPanelLayout_Mobile()) {
799 if (prefs_App()->uiAnimations) { 1250 if (prefs_App()->uiAnimations) {
800 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iFalse); 1251 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iFalse);
801 if (isIncoming) { 1252 if (isIncoming) {
@@ -808,17 +1259,51 @@ void setupSheetTransition_Mobile(iWidget *sheet, iBool isIncoming) {
808 } 1259 }
809 return; 1260 return;
810 } 1261 }
811 if(isSideBySideLayout_()) { 1262 if (isSideBySideLayout_()) {
1263 /* TODO: Landscape transitions? */
812 return; 1264 return;
813 } 1265 }
814 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iTrue); 1266 setFlags_Widget(sheet,
1267 horizontalOffset_WidgetFlag,
1268 dir == right_TransitionDir || dir == left_TransitionDir);
815 if (isIncoming) { 1269 if (isIncoming) {
816 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, 0, 0); 1270 switch (dir) {
1271 case right_TransitionDir:
1272 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, 0, 0);
1273 break;
1274 case left_TransitionDir:
1275 setVisualOffset_Widget(sheet, -size_Root(sheet->root).x, 0, 0);
1276 break;
1277 case top_TransitionDir:
1278 setVisualOffset_Widget(
1279 sheet, -bottom_Rect(boundsWithoutVisualOffset_Widget(sheet)), 0, 0);
1280 break;
1281 case bottom_TransitionDir:
1282 setVisualOffset_Widget(sheet, height_Widget(sheet), 0, 0);
1283 break;
1284 }
817 setVisualOffset_Widget(sheet, 0, 200, easeOut_AnimFlag); 1285 setVisualOffset_Widget(sheet, 0, 200, easeOut_AnimFlag);
818 } 1286 }
819 else { 1287 else {
820 const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset)) > 0; 1288 switch (dir) {
821 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, wasDragged ? 100 : 200, 1289 case right_TransitionDir: {
822 wasDragged ? 0 : easeIn_AnimFlag); 1290 const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset)) > 0;
1291 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, wasDragged ? 100 : 200,
1292 wasDragged ? 0 : easeIn_AnimFlag);
1293 break;
1294 }
1295 case left_TransitionDir:
1296 setVisualOffset_Widget(sheet, -size_Root(sheet->root).x, 200, easeIn_AnimFlag);
1297 break;
1298 case top_TransitionDir:
1299 setVisualOffset_Widget(sheet,
1300 -bottom_Rect(boundsWithoutVisualOffset_Widget(sheet)),
1301 200,
1302 easeIn_AnimFlag);
1303 break;
1304 case bottom_TransitionDir:
1305 setVisualOffset_Widget(sheet, height_Widget(sheet), 200, easeIn_AnimFlag);
1306 break;
1307 }
823 } 1308 }
824} 1309}
diff --git a/src/ui/mobile.h b/src/ui/mobile.h
index 44134389..9d7ac8e4 100644
--- a/src/ui/mobile.h
+++ b/src/ui/mobile.h
@@ -22,11 +22,36 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#pragma once 23#pragma once
24 24
25#include <the_Foundation/defs.h> 25#include <the_Foundation/rect.h>
26 26
27iDeclareType(Widget) 27iDeclareType(Widget)
28 28iDeclareType(MenuItem)
29void setupMenuTransition_Mobile (iWidget *menu, iBool isIncoming); 29
30void setupSheetTransition_Mobile (iWidget *sheet, iBool isIncoming); 30iBool isUsingPanelLayout_Mobile (void);
31 31iWidget * makePanels_Mobile (const char *id,
32void finalizeSheet_Mobile (iWidget *sheet); 32 const iMenuItem *itemsNullTerminated,
33 const iMenuItem *actions, size_t numActions);
34iWidget * makePanelsParent_Mobile (iWidget *parent,
35 const char *id,
36 const iMenuItem *itemsNullTerminated,
37 const iMenuItem *actions, size_t numActions);
38void initPanels_Mobile (iWidget *panels, iWidget *parentWidget,
39 const iMenuItem *itemsNullTerminated,
40 const iMenuItem *actions, size_t numActions);
41
42size_t currentPanelIndex_Mobile (const iWidget *panels);
43
44enum iTransitionFlags {
45 incoming_TransitionFlag = iBit(1),
46 dirMask_TransitionFlag = iBit(2) | iBit(3),
47};
48
49enum iTransitionDir {
50 right_TransitionDir = 0,
51 bottom_TransitionDir = 2,
52 left_TransitionDir = 4,
53 top_TransitionDir = 6,
54};
55
56void setupMenuTransition_Mobile (iWidget *menu, iBool isIncoming);
57void setupSheetTransition_Mobile (iWidget *sheet, int flags);
diff --git a/src/ui/paint.c b/src/ui/paint.c
index 79adb7d1..5506f845 100644
--- a/src/ui/paint.c
+++ b/src/ui/paint.c
@@ -24,6 +24,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
24 24
25#include <SDL_version.h> 25#include <SDL_version.h>
26 26
27iInt2 origin_Paint;
28
27iLocalDef SDL_Renderer *renderer_Paint_(const iPaint *d) { 29iLocalDef SDL_Renderer *renderer_Paint_(const iPaint *d) {
28 iAssert(d->dst); 30 iAssert(d->dst);
29 return d->dst->render; 31 return d->dst->render;
@@ -31,7 +33,8 @@ iLocalDef SDL_Renderer *renderer_Paint_(const iPaint *d) {
31 33
32static void setColor_Paint_(const iPaint *d, int color) { 34static void setColor_Paint_(const iPaint *d, int color) {
33 const iColor clr = get_Color(color & mask_ColorId); 35 const iColor clr = get_Color(color & mask_ColorId);
34 SDL_SetRenderDrawColor(renderer_Paint_(d), clr.r, clr.g, clr.b, clr.a * d->alpha / 255); 36 SDL_SetRenderDrawColor(renderer_Paint_(d), clr.r, clr.g, clr.b,
37 (color & opaque_ColorId ? 255 : clr.a) * d->alpha / 255);
35} 38}
36 39
37void init_Paint(iPaint *d) { 40void init_Paint(iPaint *d) {
@@ -62,17 +65,40 @@ void endTarget_Paint(iPaint *d) {
62} 65}
63 66
64void setClip_Paint(iPaint *d, iRect rect) { 67void setClip_Paint(iPaint *d, iRect rect) {
65 rect = intersect_Rect(rect, rect_Root(get_Root())); 68 //rect = intersect_Rect(rect, rect_Root(get_Root()));
69 addv_I2(&rect.pos, origin_Paint);
66 if (isEmpty_Rect(rect)) { 70 if (isEmpty_Rect(rect)) {
67 rect = init_Rect(0, 0, 1, 1); 71 rect = init_Rect(0, 0, 1, 1);
68 } 72 }
73// iRect root = rect_Root(get_Root());
74 iRect targetRect = zero_Rect();
75 SDL_Texture *target = SDL_GetRenderTarget(renderer_Paint_(d));
76 if (target) {
77 SDL_QueryTexture(target, NULL, NULL, &targetRect.size.x, &targetRect.size.y);
78 rect = intersect_Rect(rect, targetRect);
79 }
80 else {
81 rect = intersect_Rect(rect, rect_Root(get_Root()));
82 }
83
84 /*if (rect.pos.x < 0) {
85 adjustEdges_Rect(&rect, 0, 0, 0, -rect.pos.x);
86 }
87 if (rect.pos.y < 0) {
88 adjustEdges_Rect(&rect, -rect.pos.y, 0, 0, 0);
89 }
90 if (right_Rect(rect) > right_Rect(root)) {
91 adjustEdges_Rect(&rect, 0, right_Rect(root) - right_Rect(rect), 0, 0);
92 }
93 if (bottom_Rect(rect) > bottom_Rect(root)) {
94 adjustEdges_Rect(&rect, 0, bottom_Rect(root) - bottom_Rect(rect), 0, 0);
95 }*/
69 SDL_RenderSetClipRect(renderer_Paint_(d), (const SDL_Rect *) &rect); 96 SDL_RenderSetClipRect(renderer_Paint_(d), (const SDL_Rect *) &rect);
70} 97}
71 98
72void unsetClip_Paint(iPaint *d) { 99void unsetClip_Paint(iPaint *d) {
73 if (numRoots_Window(get_Window()) > 1) { 100 if (numRoots_Window(get_Window()) > 1) {
74 const iRect rect = rect_Root(get_Root()); 101 setClip_Paint(d, rect_Root(get_Root()));
75 SDL_RenderSetClipRect(renderer_Paint_(d), (const SDL_Rect *) &rect);
76 return; 102 return;
77 } 103 }
78#if SDL_VERSION_ATLEAST(2, 0, 12) 104#if SDL_VERSION_ATLEAST(2, 0, 12)
@@ -85,6 +111,7 @@ void unsetClip_Paint(iPaint *d) {
85} 111}
86 112
87void drawRect_Paint(const iPaint *d, iRect rect, int color) { 113void drawRect_Paint(const iPaint *d, iRect rect, int color) {
114 addv_I2(&rect.pos, origin_Paint);
88 iInt2 br = bottomRight_Rect(rect); 115 iInt2 br = bottomRight_Rect(rect);
89 /* Keep the right/bottom edge visible in the window. */ 116 /* Keep the right/bottom edge visible in the window. */
90 if (br.x == d->dst->size.x) br.x--; 117 if (br.x == d->dst->size.x) br.x--;
@@ -115,13 +142,19 @@ void drawRectThickness_Paint(const iPaint *d, iRect rect, int thickness, int col
115} 142}
116 143
117void fillRect_Paint(const iPaint *d, iRect rect, int color) { 144void fillRect_Paint(const iPaint *d, iRect rect, int color) {
145 addv_I2(&rect.pos, origin_Paint);
118 setColor_Paint_(d, color); 146 setColor_Paint_(d, color);
147// printf("fillRect_Paint: %d,%d %dx%d (%d)\n", rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, color);
119 SDL_RenderFillRect(renderer_Paint_(d), (SDL_Rect *) &rect); 148 SDL_RenderFillRect(renderer_Paint_(d), (SDL_Rect *) &rect);
120} 149}
121 150
122void drawSoftShadow_Paint(const iPaint *d, iRect inner, int thickness, int color, int alpha) { 151void drawSoftShadow_Paint(const iPaint *d, iRect inner, int thickness, int color, int alpha) {
152 addv_I2(&inner.pos, origin_Paint);
123 SDL_Renderer *render = renderer_Paint_(d); 153 SDL_Renderer *render = renderer_Paint_(d);
124 SDL_Texture *shadow = get_Window()->borderShadow; 154 SDL_Texture *shadow = get_Window()->borderShadow;
155 if (!shadow) {
156 return;
157 }
125 const iInt2 size = size_SDLTexture(shadow); 158 const iInt2 size = size_SDLTexture(shadow);
126 const iRect outer = expanded_Rect(inner, init1_I2(thickness)); 159 const iRect outer = expanded_Rect(inner, init1_I2(thickness));
127 const iColor clr = get_Color(color); 160 const iColor clr = get_Color(color);
@@ -146,9 +179,30 @@ void drawSoftShadow_Paint(const iPaint *d, iRect inner, int thickness, int color
146 &(SDL_Rect){ outer.pos.x, inner.pos.y, thickness, inner.size.y }); 179 &(SDL_Rect){ outer.pos.x, inner.pos.y, thickness, inner.size.y });
147} 180}
148 181
149void drawLines_Paint(const iPaint *d, const iInt2 *points, size_t count, int color) { 182void drawLines_Paint(const iPaint *d, const iInt2 *points, size_t n, int color) {
150 setColor_Paint_(d, color); 183 setColor_Paint_(d, color);
151 SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) points, count); 184 iInt2 *offsetPoints = malloc(sizeof(iInt2) * n);
185 for (size_t i = 0; i < n; i++) {
186 offsetPoints[i] = add_I2(points[i], origin_Paint);
187 }
188 SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) offsetPoints, n);
189 free(offsetPoints);
190}
191
192void drawPin_Paint(iPaint *d, iRect rangeRect, int dir, int pinColor) {
193 const int height = height_Rect(rangeRect);
194 iRect pin;
195 if (dir == 0) {
196 pin = (iRect){ add_I2(topLeft_Rect(rangeRect), init_I2(-gap_UI / 4, -gap_UI)),
197 init_I2(gap_UI / 2, height + gap_UI) };
198 }
199 else {
200 pin = (iRect){ addX_I2(topRight_Rect(rangeRect), -gap_UI / 4),
201 init_I2(gap_UI / 2, height + gap_UI) };
202 }
203 fillRect_Paint(d, pin, pinColor);
204 fillRect_Paint(d, initCentered_Rect(dir == 0 ? topMid_Rect(pin) : bottomMid_Rect(pin),
205 init1_I2(gap_UI * 2)), pinColor);
152} 206}
153 207
154iInt2 size_SDLTexture(SDL_Texture *d) { 208iInt2 size_SDLTexture(SDL_Texture *d) {
diff --git a/src/ui/paint.h b/src/ui/paint.h
index 90cc2aef..e894b62f 100644
--- a/src/ui/paint.h
+++ b/src/ui/paint.h
@@ -36,6 +36,8 @@ struct Impl_Paint {
36 uint8_t alpha; 36 uint8_t alpha;
37}; 37};
38 38
39extern iInt2 origin_Paint; /* add this to all drawn positions so buffered graphics are correctly offset */
40
39void init_Paint (iPaint *); 41void init_Paint (iPaint *);
40 42
41void beginTarget_Paint (iPaint *, SDL_Texture *target); 43void beginTarget_Paint (iPaint *, SDL_Texture *target);
@@ -61,4 +63,6 @@ iLocalDef void drawVLine_Paint(const iPaint *d, iInt2 pos, int len, int color) {
61 drawLine_Paint(d, pos, addY_I2(pos, len), color); 63 drawLine_Paint(d, pos, addY_I2(pos, len), color);
62} 64}
63 65
66void drawPin_Paint (iPaint *, iRect rangeRect, int dir, int pinColor);
67
64iInt2 size_SDLTexture (SDL_Texture *); 68iInt2 size_SDLTexture (SDL_Texture *);
diff --git a/src/ui/root.c b/src/ui/root.c
index a8b9f998..90c0c6e4 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -57,49 +57,49 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
57static const iMenuItem navMenuItems_[] = { 57static const iMenuItem navMenuItems_[] = {
58 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" }, 58 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
59 { "${menu.openlocation}", SDLK_l, KMOD_PRIMARY, "navigate.focus" }, 59 { "${menu.openlocation}", SDLK_l, KMOD_PRIMARY, "navigate.focus" },
60 { "---", 0, 0, NULL }, 60 { "---" },
61 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" }, 61 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" },
62 { "${menu.page.copysource}", SDLK_c, KMOD_PRIMARY, "copy" }, 62 { "${menu.page.copysource}", SDLK_c, KMOD_PRIMARY, "copy" },
63 { "---", 0, 0, NULL }, 63 { "---" },
64 { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, 64 { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
65 { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" }, 65 { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" },
66 { "${menu.view.split}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" }, 66 { "${menu.view.split}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" },
67 { "${menu.zoom.in}", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" }, 67 { "${menu.zoom.in}", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" },
68 { "${menu.zoom.out}", SDLK_MINUS, KMOD_PRIMARY, "zoom.delta arg:-10" }, 68 { "${menu.zoom.out}", SDLK_MINUS, KMOD_PRIMARY, "zoom.delta arg:-10" },
69 { "${menu.zoom.reset}", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, 69 { "${menu.zoom.reset}", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" },
70 { "---", 0, 0, NULL }, 70 { "---" },
71 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, 71 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
72 { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" }, 72 { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" },
73 { "${menu.bookmarks.bytime}", 0, 0, "!open url:about:bookmarks?created" }, 73 { "${menu.bookmarks.bytime}", 0, 0, "!open url:about:bookmarks?created" },
74 { "---", 0, 0, NULL }, 74 { "---" },
75 { "${menu.downloads}", 0, 0, "downloads.open" }, 75 { "${menu.downloads}", 0, 0, "downloads.open" },
76 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, 76 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
77 { "---", 0, 0, NULL }, 77 { "---" },
78 { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, 78 { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
79 { "${menu.help}", SDLK_F1, 0, "!open url:about:help" }, 79 { "${menu.help}", SDLK_F1, 0, "!open url:about:help" },
80 { "${menu.releasenotes}", 0, 0, "!open url:about:version" }, 80 { "${menu.releasenotes}", 0, 0, "!open url:about:version" },
81 { "---", 0, 0, NULL }, 81 { "---" },
82 { "${menu.quit}", 'q', KMOD_PRIMARY, "quit" } 82 { "${menu.quit}", 'q', KMOD_PRIMARY, "quit" }
83}; 83};
84#endif 84#endif
85 85
86#if defined (iPlatformAppleMobile) 86#if defined (iPlatformMobile)
87/* Tablet menu. */ 87/* Tablet menu. */
88static const iMenuItem tabletNavMenuItems_[] = { 88static const iMenuItem tabletNavMenuItems_[] = {
89 { folder_Icon " ${menu.openfile}", SDLK_o, KMOD_PRIMARY, "file.open" }, 89 { folder_Icon " ${menu.openfile}", SDLK_o, KMOD_PRIMARY, "file.open" },
90 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" }, 90 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
91 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, 91 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" },
92 { "---", 0, 0, NULL }, 92 { "---" },
93 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" }, 93 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" },
94 { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, 94 { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
95 { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" }, 95 { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" },
96 { "${menu.view.split}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" }, 96 { "${menu.view.split}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" },
97 { "---", 0, 0, NULL }, 97 { "---" },
98 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, 98 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
99 { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" }, 99 { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" },
100 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, 100 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
101 { "${menu.downloads}", 0, 0, "downloads.open" }, 101 { "${menu.downloads}", 0, 0, "downloads.open" },
102 { "---", 0, 0, NULL }, 102 { "---" },
103 { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, 103 { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
104 { "${menu.help}", SDLK_F1, 0, "!open url:about:help" }, 104 { "${menu.help}", SDLK_F1, 0, "!open url:about:help" },
105 { "${menu.releasenotes}", 0, 0, "!open url:about:version" }, 105 { "${menu.releasenotes}", 0, 0, "!open url:about:version" },
@@ -110,39 +110,39 @@ static const iMenuItem phoneNavMenuItems_[] = {
110 { folder_Icon " ${menu.openfile}", SDLK_o, KMOD_PRIMARY, "file.open" }, 110 { folder_Icon " ${menu.openfile}", SDLK_o, KMOD_PRIMARY, "file.open" },
111 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" }, 111 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
112 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, 112 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" },
113 { "---", 0, 0, NULL }, 113 { "---" },
114 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" }, 114 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" },
115 { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, 115 { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
116 { "---", 0, 0, NULL }, 116 { "---" },
117 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, 117 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
118 { "${menu.downloads}", 0, 0, "downloads.open" }, 118 { "${menu.downloads}", 0, 0, "downloads.open" },
119 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, 119 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
120 { "---", 0, 0, NULL }, 120 { "---" },
121 { gear_Icon " Settings...", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, 121 { gear_Icon " ${menu.settings}", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
122}; 122};
123#endif /* AppleMobile */ 123#endif /* Mobile */
124 124
125#if defined (iPlatformAppleMobile) 125#if defined (iPlatformMobile)
126static const iMenuItem identityButtonMenuItems_[] = { 126static const iMenuItem identityButtonMenuItems_[] = {
127 { "${menu.identity.notactive}", 0, 0, "ident.showactive" }, 127 { "${menu.identity.notactive}", 0, 0, "ident.showactive" },
128 { "---", 0, 0, NULL }, 128 { "---" },
129 { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" }, 129 { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" },
130 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" }, 130 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
131 { "---", 0, 0, NULL }, 131 { "---" },
132 { person_Icon " ${menu.show.identities}", 0, 0, "toolbar.showident" }, 132 { person_Icon " ${menu.show.identities}", 0, 0, "toolbar.showident" },
133}; 133};
134#else /* desktop */ 134#else /* desktop */
135static const iMenuItem identityButtonMenuItems_[] = { 135static const iMenuItem identityButtonMenuItems_[] = {
136 { "${menu.identity.notactive}", 0, 0, "ident.showactive" }, 136 { "${menu.identity.notactive}", 0, 0, "ident.showactive" },
137 { "---", 0, 0, NULL }, 137 { "---" },
138# if !defined (iPlatformAppleDesktop) 138# if !defined (iPlatformAppleDesktop)
139 { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" }, 139 { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" },
140 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" }, 140 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
141 { "---", 0, 0, NULL }, 141 { "---" },
142 { person_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" }, 142 { person_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" },
143# else 143# else
144 { add_Icon " ${menu.identity.new}", 0, 0, "ident.new" }, 144 { add_Icon " ${menu.identity.new}", 0, 0, "ident.new" },
145 { "---", 0, 0, NULL }, 145 { "---" },
146 { person_Icon " ${menu.show.identities}", 0, 0, "sidebar.mode arg:3 show:1" }, 146 { person_Icon " ${menu.show.identities}", 0, 0, "sidebar.mode arg:3 show:1" },
147# endif 147# endif
148}; 148};
@@ -271,13 +271,16 @@ void destroyPending_Root(iRoot *d) {
271 setCurrent_Root(d); 271 setCurrent_Root(d);
272 iForEach(PtrSet, i, d->pendingDestruction) { 272 iForEach(PtrSet, i, d->pendingDestruction) {
273 iWidget *widget = *i.value; 273 iWidget *widget = *i.value;
274 iAssert(widget->root == d);
274 if (!isFinished_Anim(&widget->visualOffset) || 275 if (!isFinished_Anim(&widget->visualOffset) ||
275 isBeingVisuallyOffsetByReference_Widget(widget)) { 276 isBeingVisuallyOffsetByReference_Widget(widget)) {
276 continue; 277 continue;
277 } 278 }
278 if (widget->flags & keepOnTop_WidgetFlag) { 279 if (widget->flags & keepOnTop_WidgetFlag) {
279 removeOne_PtrArray(onTop_Root(widget->root), widget); 280 removeOne_PtrArray(d->onTop, widget);
281 widget->flags &= ~keepOnTop_WidgetFlag;
280 } 282 }
283 iAssert(indexOf_PtrArray(d->onTop, widget) == iInvalidPos);
281 if (widget->parent) { 284 if (widget->parent) {
282 removeChild_Widget(widget->parent, widget); 285 removeChild_Widget(widget->parent, widget);
283 } 286 }
@@ -285,17 +288,14 @@ void destroyPending_Root(iRoot *d) {
285 iRelease(widget); 288 iRelease(widget);
286 remove_PtrSetIterator(&i); 289 remove_PtrSetIterator(&i);
287 } 290 }
288 setCurrent_Root(oldRoot); 291#if 0
289} 292 printf("Root %p onTop (%zu):\n", d, size_PtrArray(d->onTop));
290 293 iConstForEach(PtrArray, t, d->onTop) {
291void postArrange_Root(iRoot *d) { 294 const iWidget *p = *t.value;
292 if (!d->pendingArrange) { 295 printf(" - %p {%s}\n", p, cstr_String(id_Widget(p)));
293 d->pendingArrange = iTrue;
294 SDL_Event ev = { .type = SDL_USEREVENT };
295 ev.user.code = arrange_UserEventCode;
296 ev.user.data2 = d;
297 SDL_PushEvent(&ev);
298 } 296 }
297#endif
298 setCurrent_Root(oldRoot);
299} 299}
300 300
301iPtrArray *onTop_Root(iRoot *d) { 301iPtrArray *onTop_Root(iRoot *d) {
@@ -312,7 +312,7 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
312 iWidget *menu = findChild_Widget(button, "menu"); 312 iWidget *menu = findChild_Widget(button, "menu");
313 iAssert(menu); 313 iAssert(menu);
314 if (!isVisible_Widget(menu)) { 314 if (!isVisible_Widget(menu)) {
315 openMenu_Widget(menu, bottomLeft_Rect(bounds_Widget(button))); 315 openMenu_Widget(menu, topLeft_Rect(bounds_Widget(button)));
316 } 316 }
317 else { 317 else {
318 closeMenu_Widget(menu); 318 closeMenu_Widget(menu);
@@ -373,18 +373,18 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
373 else if (equal_Command(cmd, "window.setrect")) { 373 else if (equal_Command(cmd, "window.setrect")) {
374 const int snap = argLabel_Command(cmd, "snap"); 374 const int snap = argLabel_Command(cmd, "snap");
375 if (snap) { 375 if (snap) {
376 iWindow *window = get_Window(); 376 iMainWindow *window = get_MainWindow();
377 iInt2 coord = coord_Command(cmd); 377 iInt2 coord = coord_Command(cmd);
378 iInt2 size = init_I2(argLabel_Command(cmd, "width"), 378 iInt2 size = init_I2(argLabel_Command(cmd, "width"),
379 argLabel_Command(cmd, "height")); 379 argLabel_Command(cmd, "height"));
380 SDL_SetWindowPosition(window->win, coord.x, coord.y); 380 SDL_SetWindowPosition(window->base.win, coord.x, coord.y);
381 SDL_SetWindowSize(window->win, size.x, size.y); 381 SDL_SetWindowSize(window->base.win, size.x, size.y);
382 window->place.snap = snap; 382 window->place.snap = snap;
383 return iTrue; 383 return iTrue;
384 } 384 }
385 } 385 }
386 else if (equal_Command(cmd, "window.restore")) { 386 else if (equal_Command(cmd, "window.restore")) {
387 setSnap_Window(get_Window(), none_WindowSnap); 387 setSnap_MainWindow(get_MainWindow(), none_WindowSnap);
388 return iTrue; 388 return iTrue;
389 } 389 }
390 else if (equal_Command(cmd, "window.minimize")) { 390 else if (equal_Command(cmd, "window.minimize")) {
@@ -400,8 +400,6 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
400 iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); 400 iSidebarWidget *sidebar = findChild_Widget(root, "sidebar");
401 iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2"); 401 iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2");
402 removeChild_Widget(parent_Widget(sidebar), sidebar); 402 removeChild_Widget(parent_Widget(sidebar), sidebar);
403 setButtonFont_SidebarWidget(sidebar, isLandscape_App() ? uiLabel_FontId : defaultBig_FontId);
404 setButtonFont_SidebarWidget(sidebar2, isLandscape_App() ? uiLabel_FontId : defaultBig_FontId);
405 // setBackgroundColor_Widget(findChild_Widget(as_Widget(sidebar), "buttons"), 403 // setBackgroundColor_Widget(findChild_Widget(as_Widget(sidebar), "buttons"),
406 // isPortrait_App() ? uiBackgroundUnfocusedSelection_ColorId 404 // isPortrait_App() ? uiBackgroundUnfocusedSelection_ColorId
407 // : uiBackgroundSidebar_ColorId); 405 // : uiBackgroundSidebar_ColorId);
@@ -432,19 +430,19 @@ static void updateNavBarIdentity_(iWidget *navBar) {
432 const iGmIdentity *ident = 430 const iGmIdentity *ident =
433 identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App())); 431 identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App()));
434 iWidget *button = findChild_Widget(navBar, "navbar.ident"); 432 iWidget *button = findChild_Widget(navBar, "navbar.ident");
435 iLabelWidget *toolButton = findWidget_App("toolbar.ident"); 433 iWidget *menu = findChild_Widget(button, "menu");
436 setFlags_Widget(button, selected_WidgetFlag, ident != NULL); 434 setFlags_Widget(button, selected_WidgetFlag, ident != NULL);
437 setOutline_LabelWidget(toolButton, ident == NULL);
438 /* Update menu. */ 435 /* Update menu. */
439 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);
440 const iString *subjectName = ident ? name_GmIdentity(ident) : NULL; 436 const iString *subjectName = ident ? name_GmIdentity(ident) : NULL;
441 setTextCStr_LabelWidget( 437 const char * idLabel = subjectName
442 idItem, 438 ? format_CStr(uiTextAction_ColorEscape "%s", cstr_String(subjectName))
443 subjectName ? format_CStr(uiTextAction_ColorEscape "%s", cstr_String(subjectName)) 439 : "${menu.identity.notactive}";
444 : "${menu.identity.notactive}"); 440 setMenuItemLabelByIndex_Widget(menu, 0, idLabel);
445 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident); 441 setMenuItemDisabledByIndex_Widget(menu, 0, !ident);
442 iLabelWidget *toolButton = findWidget_App("toolbar.ident");
446 iLabelWidget *toolName = findWidget_App("toolbar.name"); 443 iLabelWidget *toolName = findWidget_App("toolbar.name");
447 if (toolName) { 444 if (toolName) {
445 setOutline_LabelWidget(toolButton, ident == NULL);
448 updateTextCStr_LabelWidget(toolName, subjectName ? cstr_String(subjectName) : ""); 446 updateTextCStr_LabelWidget(toolName, subjectName ? cstr_String(subjectName) : "");
449 setFont_LabelWidget(toolButton, subjectName ? defaultMedium_FontId : uiLabelLarge_FontId); 447 setFont_LabelWidget(toolButton, subjectName ? defaultMedium_FontId : uiLabelLarge_FontId);
450 arrange_Widget(parent_Widget(toolButton)); 448 arrange_Widget(parent_Widget(toolButton));
@@ -498,9 +496,10 @@ static void checkLoadAnimation_Root_(iRoot *d) {
498 496
499void updatePadding_Root(iRoot *d) { 497void updatePadding_Root(iRoot *d) {
500 if (d == NULL) return; 498 if (d == NULL) return;
501#if defined (iPlatformAppleMobile)
502 iWidget *toolBar = findChild_Widget(d->widget, "toolbar"); 499 iWidget *toolBar = findChild_Widget(d->widget, "toolbar");
503 float left, top, right, bottom; 500 float bottom = 0.0f;
501#if defined (iPlatformAppleMobile)
502 float left, top, right;
504 safeAreaInsets_iOS(&left, &top, &right, &bottom); 503 safeAreaInsets_iOS(&left, &top, &right, &bottom);
505 /* Respect the safe area insets. */ { 504 /* Respect the safe area insets. */ {
506 setPadding_Widget(findChild_Widget(d->widget, "navdiv"), left, top, right, 0); 505 setPadding_Widget(findChild_Widget(d->widget, "navdiv"), left, top, right, 0);
@@ -508,6 +507,7 @@ void updatePadding_Root(iRoot *d) {
508 setPadding_Widget(toolBar, left, 0, right, bottom); 507 setPadding_Widget(toolBar, left, 0, right, bottom);
509 } 508 }
510 } 509 }
510#endif
511 if (toolBar) { 511 if (toolBar) {
512 /* TODO: get this from toolBar height, but it's buggy for some reason */ 512 /* TODO: get this from toolBar height, but it's buggy for some reason */
513 const int sidebarBottomPad = isPortrait_App() ? 11 * gap_UI + bottom : 0; 513 const int sidebarBottomPad = isPortrait_App() ? 11 * gap_UI + bottom : 0;
@@ -517,14 +517,15 @@ void updatePadding_Root(iRoot *d) {
517 are not arranged correctly until it's hidden and reshown. */ 517 are not arranged correctly until it's hidden and reshown. */
518 } 518 }
519 /* Note that `handleNavBarCommands_` also adjusts padding and spacing. */ 519 /* Note that `handleNavBarCommands_` also adjusts padding and spacing. */
520#endif
521} 520}
522 521
523void updateToolbarColors_Root(iRoot *d) { 522void updateToolbarColors_Root(iRoot *d) {
524#if defined (iPlatformMobile) 523#if defined (iPlatformMobile)
525 iWidget *toolBar = findChild_Widget(d->widget, "toolbar"); 524 iWidget *toolBar = findChild_Widget(d->widget, "toolbar");
526 if (toolBar) { 525 if (toolBar) {
527 const iBool isSidebarVisible = isVisible_Widget(findChild_Widget(d->widget, "sidebar")); 526 const iBool isSidebarVisible =
527 isVisible_Widget(findChild_Widget(d->widget, "sidebar")) ||
528 isVisible_Widget(findChild_Widget(d->widget, "sidebar2"));
528 const int bg = isSidebarVisible ? uiBackgroundSidebar_ColorId : 529 const int bg = isSidebarVisible ? uiBackgroundSidebar_ColorId :
529 tmBannerBackground_ColorId; 530 tmBannerBackground_ColorId;
530 setBackgroundColor_Widget(toolBar, bg); 531 setBackgroundColor_Widget(toolBar, bg);
@@ -684,6 +685,35 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
684 } 685 }
685 return iTrue; 686 return iTrue;
686 } 687 }
688 else if (deviceType_App() != desktop_AppDeviceType &&
689 (equal_Command(cmd, "focus.gained") || equal_Command(cmd, "focus.lost"))) {
690 iInputWidget *url = findChild_Widget(navBar, "url");
691 if (pointer_Command(cmd) == url) {
692 const iBool isFocused = equal_Command(cmd, "focus.gained");
693 setFlags_Widget(findChild_Widget(navBar, "navbar.lock"), hidden_WidgetFlag, isFocused);
694 setFlags_Widget(findChild_Widget(navBar, "navbar.clear"), hidden_WidgetFlag, !isFocused);
695 showCollapsed_Widget(findChild_Widget(navBar, "navbar.cancel"), isFocused);
696 showCollapsed_Widget(findChild_Widget(navBar, "pagemenubutton"), !isFocused);
697 showCollapsed_Widget(findChild_Widget(navBar, "reload"), !isFocused);
698 }
699 return iFalse;
700 }
701 else if (equal_Command(cmd, "navbar.clear")) {
702 iInputWidget *url = findChild_Widget(navBar, "url");
703 selectAll_InputWidget(url);
704 /* Emulate a Backspace keypress. */
705 class_InputWidget(url)->processEvent(
706 as_Widget(url),
707 (SDL_Event *) &(SDL_KeyboardEvent){ .type = SDL_KEYDOWN,
708 .timestamp = SDL_GetTicks(),
709 .state = SDL_PRESSED,
710 .keysym = { .sym = SDLK_BACKSPACE } });
711 return iTrue;
712 }
713 else if (equal_Command(cmd, "navbar.cancel")) {
714 setFocus_Widget(NULL);
715 return iTrue;
716 }
687 else if (equal_Command(cmd, "input.edited")) { 717 else if (equal_Command(cmd, "input.edited")) {
688 iAnyObject * url = findChild_Widget(navBar, "url"); 718 iAnyObject * url = findChild_Widget(navBar, "url");
689 const iString *text = text_InputWidget(url); 719 const iString *text = text_InputWidget(url);
@@ -829,13 +859,13 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) {
829 return iFalse; 859 return iFalse;
830} 860}
831 861
832#if defined (iPlatformAppleMobile) 862#if defined (iPlatformMobile)
833static void dismissSidebar_(iWidget *sidebar, const char *toolButtonId) { 863static void dismissSidebar_(iWidget *sidebar, const char *toolButtonId) {
834 if (isVisible_Widget(sidebar)) { 864 if (isVisible_Widget(sidebar)) {
835 postCommandf_App("%s.toggle", cstr_String(id_Widget(sidebar))); 865 postCommandf_App("%s.toggle", cstr_String(id_Widget(sidebar)));
836 if (toolButtonId) { 866// if (toolButtonId) {
837 // setFlags_Widget(findWidget_App(toolButtonId), noBackground_WidgetFlag, iTrue); 867 // setFlags_Widget(findWidget_App(toolButtonId), noBackground_WidgetFlag, iTrue);
838 } 868// }
839 setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag); 869 setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag);
840 } 870 }
841} 871}
@@ -909,7 +939,7 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) {
909 } 939 }
910 return iFalse; 940 return iFalse;
911} 941}
912#endif /* defined (iPlatformAppleMobile) */ 942#endif /* defined (iPlatformMobile) */
913 943
914static iLabelWidget *newLargeIcon_LabelWidget(const char *text, const char *cmd) { 944static iLabelWidget *newLargeIcon_LabelWidget(const char *text, const char *cmd) {
915 iLabelWidget *lab = newIcon_LabelWidget(text, 0, 0, cmd); 945 iLabelWidget *lab = newIcon_LabelWidget(text, 0, 0, cmd);
@@ -940,7 +970,7 @@ void updateMetrics_Root(iRoot *d) {
940 setFixedSize_Widget(appIcon, init_I2(appIconSize_Root(), appMin->rect.size.y)); 970 setFixedSize_Widget(appIcon, init_I2(appIconSize_Root(), appMin->rect.size.y));
941 } 971 }
942 iWidget *navBar = findChild_Widget(d->widget, "navbar"); 972 iWidget *navBar = findChild_Widget(d->widget, "navbar");
943 iWidget *lock = findChild_Widget(navBar, "navbar.lock"); 973// iWidget *lock = findChild_Widget(navBar, "navbar.lock");
944 iWidget *url = findChild_Widget(d->widget, "url"); 974 iWidget *url = findChild_Widget(d->widget, "url");
945 iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed"); 975 iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed");
946 iWidget *embedPad = findChild_Widget(navBar, "url.embedpad"); 976 iWidget *embedPad = findChild_Widget(navBar, "url.embedpad");
@@ -1043,6 +1073,7 @@ void createUserInterface_Root(iRoot *d) {
1043 /* Navigation bar. */ { 1073 /* Navigation bar. */ {
1044 navBar = new_Widget(); 1074 navBar = new_Widget();
1045 setId_Widget(navBar, "navbar"); 1075 setId_Widget(navBar, "navbar");
1076 setDrawBufferEnabled_Widget(navBar, iTrue);
1046 setFlags_Widget(navBar, 1077 setFlags_Widget(navBar,
1047 hittable_WidgetFlag | /* context menu */ 1078 hittable_WidgetFlag | /* context menu */
1048 arrangeHeight_WidgetFlag | 1079 arrangeHeight_WidgetFlag |
@@ -1094,6 +1125,16 @@ void createUserInterface_Root(iRoot *d) {
1094 setFont_LabelWidget(lock, symbols_FontId + uiNormal_FontSize); 1125 setFont_LabelWidget(lock, symbols_FontId + uiNormal_FontSize);
1095 updateTextCStr_LabelWidget(lock, "\U0001f512"); 1126 updateTextCStr_LabelWidget(lock, "\U0001f512");
1096 } 1127 }
1128 /* Button for clearing the URL bar contents. */ {
1129 iLabelWidget *clear = addChildFlags_Widget(
1130 as_Widget(url),
1131 iClob(newIcon_LabelWidget(delete_Icon, 0, 0, "navbar.clear")),
1132 hidden_WidgetFlag | embedFlags | moveToParentLeftEdge_WidgetFlag | tight_WidgetFlag);
1133 setId_Widget(as_Widget(clear), "navbar.clear");
1134 setFont_LabelWidget(clear, symbols2_FontId + uiNormal_FontSize);
1135// setFlags_Widget(as_Widget(clear), noBackground_WidgetFlag, iFalse);
1136// setBackgroundColor_Widget(as_Widget(clear), uiBackground_ColorId);
1137 }
1097 iWidget *rightEmbed = new_Widget(); 1138 iWidget *rightEmbed = new_Widget();
1098 setId_Widget(rightEmbed, "url.rightembed"); 1139 setId_Widget(rightEmbed, "url.rightembed");
1099 addChildFlags_Widget(as_Widget(url), 1140 addChildFlags_Widget(as_Widget(url),
@@ -1150,6 +1191,13 @@ void createUserInterface_Root(iRoot *d) {
1150 setFlags_Widget(urlButtons, embedFlags | arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); 1191 setFlags_Widget(urlButtons, embedFlags | arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
1151 /* Mobile page menu. */ 1192 /* Mobile page menu. */
1152 if (deviceType_App() != desktop_AppDeviceType) { 1193 if (deviceType_App() != desktop_AppDeviceType) {
1194 iLabelWidget *navCancel = new_LabelWidget("${cancel}", "navbar.cancel");
1195 addChildFlags_Widget(urlButtons, iClob(navCancel),
1196 (embedFlags | tight_WidgetFlag | hidden_WidgetFlag |
1197 collapse_WidgetFlag) /*& ~noBackground_WidgetFlag*/);
1198 as_Widget(navCancel)->sizeRef = as_Widget(url);
1199 setFont_LabelWidget(navCancel, uiContentBold_FontId);
1200 setId_Widget(as_Widget(navCancel), "navbar.cancel");
1153 iLabelWidget *pageMenuButton; 1201 iLabelWidget *pageMenuButton;
1154 /* In a mobile layout, the reload button is replaced with the Page/Ellipsis menu. */ 1202 /* In a mobile layout, the reload button is replaced with the Page/Ellipsis menu. */
1155 pageMenuButton = makeMenuButton_LabelWidget(pageMenuCStr_, 1203 pageMenuButton = makeMenuButton_LabelWidget(pageMenuCStr_,
@@ -1157,13 +1205,13 @@ void createUserInterface_Root(iRoot *d) {
1157 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" }, 1205 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
1158 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" }, 1206 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
1159 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" }, 1207 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" },
1160 { "---", 0, 0, NULL }, 1208 { "---" },
1161 { bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, 1209 { bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
1162 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, 1210 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
1163 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, 1211 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
1164 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, 1212 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
1165 { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, 1213 { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" },
1166 { "---", 0, 0, NULL }, 1214 { "---" },
1167 { "${menu.page.copyurl}", 0, 0, "document.copylink" }, 1215 { "${menu.page.copyurl}", 0, 0, "document.copylink" },
1168 { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" }, 1216 { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" },
1169 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } }, 1217 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } },
@@ -1171,13 +1219,14 @@ void createUserInterface_Root(iRoot *d) {
1171 setId_Widget(as_Widget(pageMenuButton), "pagemenubutton"); 1219 setId_Widget(as_Widget(pageMenuButton), "pagemenubutton");
1172 setFont_LabelWidget(pageMenuButton, uiContentBold_FontId); 1220 setFont_LabelWidget(pageMenuButton, uiContentBold_FontId);
1173 setAlignVisually_LabelWidget(pageMenuButton, iTrue); 1221 setAlignVisually_LabelWidget(pageMenuButton, iTrue);
1174 addChildFlags_Widget(urlButtons, iClob(pageMenuButton), embedFlags | tight_WidgetFlag); 1222 addChildFlags_Widget(urlButtons, iClob(pageMenuButton),
1223 embedFlags | tight_WidgetFlag | collapse_WidgetFlag);
1175 updateSize_LabelWidget(pageMenuButton); 1224 updateSize_LabelWidget(pageMenuButton);
1176 } 1225 }
1177 /* Reload button. */ { 1226 /* Reload button. */ {
1178 iLabelWidget *reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"); 1227 iLabelWidget *reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload");
1179 setId_Widget(as_Widget(reload), "reload"); 1228 setId_Widget(as_Widget(reload), "reload");
1180 addChildFlags_Widget(urlButtons, iClob(reload), embedFlags); 1229 addChildFlags_Widget(urlButtons, iClob(reload), embedFlags | collapse_WidgetFlag);
1181 updateSize_LabelWidget(reload); 1230 updateSize_LabelWidget(reload);
1182 } 1231 }
1183 addChildFlags_Widget(as_Widget(url), iClob(urlButtons), moveToParentRightEdge_WidgetFlag); 1232 addChildFlags_Widget(as_Widget(url), iClob(urlButtons), moveToParentRightEdge_WidgetFlag);
@@ -1198,8 +1247,8 @@ void createUserInterface_Root(iRoot *d) {
1198#if defined (iPlatformMobile) 1247#if defined (iPlatformMobile)
1199 const iBool isPhone = (deviceType_App() == phone_AppDeviceType); 1248 const iBool isPhone = (deviceType_App() == phone_AppDeviceType);
1200#endif 1249#endif
1201#if !defined (iHaveNativeMenus) 1250#if !defined (iHaveNativeMenus) || defined (iPlatformMobile)
1202# if defined (iPlatformAppleMobile) 1251# if defined (iPlatformMobile)
1203 iLabelWidget *navMenu = 1252 iLabelWidget *navMenu =
1204 makeMenuButton_LabelWidget(menu_Icon, isPhone ? phoneNavMenuItems_ : tabletNavMenuItems_, 1253 makeMenuButton_LabelWidget(menu_Icon, isPhone ? phoneNavMenuItems_ : tabletNavMenuItems_,
1205 isPhone ? iElemCount(phoneNavMenuItems_) : iElemCount(tabletNavMenuItems_)); 1254 isPhone ? iElemCount(phoneNavMenuItems_) : iElemCount(tabletNavMenuItems_));
@@ -1280,12 +1329,13 @@ void createUserInterface_Root(iRoot *d) {
1280 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(" \u2b9d ", 'g', KMOD_PRIMARY | KMOD_SHIFT, "find.prev"))); 1329 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(" \u2b9d ", 'g', KMOD_PRIMARY | KMOD_SHIFT, "find.prev")));
1281 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(close_Icon, SDLK_ESCAPE, 0, "find.close"))); 1330 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(close_Icon, SDLK_ESCAPE, 0, "find.close")));
1282 } 1331 }
1283#if defined (iPlatformAppleMobile) 1332#if defined (iPlatformMobile)
1284 /* Bottom toolbar. */ 1333 /* Bottom toolbar. */
1285 if (isPhone_iOS()) { 1334 if (deviceType_App() == phone_AppDeviceType) {
1286 iWidget *toolBar = new_Widget(); 1335 iWidget *toolBar = new_Widget();
1287 addChild_Widget(root, iClob(toolBar)); 1336 addChild_Widget(root, iClob(toolBar));
1288 setId_Widget(toolBar, "toolbar"); 1337 setId_Widget(toolBar, "toolbar");
1338 setDrawBufferEnabled_Widget(toolBar, iTrue);
1289 setCommandHandler_Widget(toolBar, handleToolBarCommands_); 1339 setCommandHandler_Widget(toolBar, handleToolBarCommands_);
1290 setFlags_Widget(toolBar, moveToParentBottomEdge_WidgetFlag | 1340 setFlags_Widget(toolBar, moveToParentBottomEdge_WidgetFlag |
1291 parentCannotResizeHeight_WidgetFlag | 1341 parentCannotResizeHeight_WidgetFlag |
@@ -1346,35 +1396,50 @@ void createUserInterface_Root(iRoot *d) {
1346 (iMenuItem[]){ 1396 (iMenuItem[]){
1347 { close_Icon " ${menu.closetab}", 0, 0, "tabs.close" }, 1397 { close_Icon " ${menu.closetab}", 0, 0, "tabs.close" },
1348 { copy_Icon " ${menu.duptab}", 0, 0, "tabs.new duplicate:1" }, 1398 { copy_Icon " ${menu.duptab}", 0, 0, "tabs.new duplicate:1" },
1349 { "---", 0, 0, NULL }, 1399 { "---" },
1350 { "${menu.closetab.other}", 0, 0, "tabs.close toleft:1 toright:1" }, 1400 { "${menu.closetab.other}", 0, 0, "tabs.close toleft:1 toright:1" },
1351 { barLeftArrow_Icon " ${menu.closetab.left}", 0, 0, "tabs.close toleft:1" }, 1401 { barLeftArrow_Icon " ${menu.closetab.left}", 0, 0, "tabs.close toleft:1" },
1352 { barRightArrow_Icon " ${menu.closetab.right}", 0, 0, "tabs.close toright:1" }, 1402 { barRightArrow_Icon " ${menu.closetab.right}", 0, 0, "tabs.close toright:1" },
1353 }, 1403 },
1354 6); 1404 6);
1355 iWidget *barMenu = 1405 iWidget *barMenu =
1356 makeMenu_Widget(root, 1406 makeMenu_Widget(root,
1357 (iMenuItem[]){ 1407 (iMenuItem[]){
1358 { leftHalf_Icon " ${menu.sidebar.left}", 0, 0, "sidebar.toggle" }, 1408 { leftHalf_Icon " ${menu.sidebar.left}", 0, 0, "sidebar.toggle" },
1359 { rightHalf_Icon " ${menu.sidebar.right}", 0, 0, "sidebar2.toggle" }, 1409 { rightHalf_Icon " ${menu.sidebar.right}", 0, 0, "sidebar2.toggle" },
1360 }, 1410 },
1361 deviceType_App() == phone_AppDeviceType ? 1 : 2); 1411 deviceType_App() == phone_AppDeviceType ? 1 : 2);
1362 iWidget *clipMenu = makeMenu_Widget(root, 1412 iWidget *clipMenu = makeMenu_Widget(root,
1363 (iMenuItem[]){ 1413#if defined (iPlatformMobile)
1364 { scissor_Icon " ${menu.cut}", 0, 0, "input.copy cut:1" }, 1414 (iMenuItem[]){
1365 { clipCopy_Icon " ${menu.copy}", 0, 0, "input.copy" }, 1415 { ">>>" scissor_Icon " ${menu.cut}", 0, 0, "input.copy cut:1" },
1366 { "---", 0, 0, NULL }, 1416 { ">>>" clipCopy_Icon " ${menu.copy}", 0, 0, "input.copy" },
1367 { clipboard_Icon " ${menu.paste}", 0, 0, "input.paste" }, 1417 { ">>>" clipboard_Icon " ${menu.paste}", 0, 0, "input.paste" },
1368 }, 1418 { "---" },
1369 4); 1419 { ">>>" delete_Icon " " uiTextCaution_ColorEscape "${menu.delete}", 0, 0, "input.delete" },
1420 { ">>>" select_Icon " ${menu.selectall}", 0, 0, "input.selectall" },
1421 { ">>>" undo_Icon " ${menu.undo}", 0, 0, "input.undo" },
1422 }, 7);
1423#else
1424 (iMenuItem[]){
1425 { scissor_Icon " ${menu.cut}", 0, 0, "input.copy cut:1" },
1426 { clipCopy_Icon " ${menu.copy}", 0, 0, "input.copy" },
1427 { clipboard_Icon " ${menu.paste}", 0, 0, "input.paste" },
1428 { "---" },
1429 { delete_Icon " " uiTextCaution_ColorEscape "${menu.delete}", 0, 0, "input.delete" },
1430 { undo_Icon " ${menu.undo}", 0, 0, "input.undo" },
1431 { "---" },
1432 { select_Icon " ${menu.selectall}", 0, 0, "input.selectall" },
1433 }, 8);
1434#endif
1370 iWidget *splitMenu = makeMenu_Widget(root, (iMenuItem[]){ 1435 iWidget *splitMenu = makeMenu_Widget(root, (iMenuItem[]){
1371 { "${menu.split.merge}", '1', 0, "ui.split arg:0" }, 1436 { "${menu.split.merge}", '1', 0, "ui.split arg:0" },
1372 { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" }, 1437 { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" },
1373 { "---", 0, 0, NULL }, 1438 { "---" },
1374 { "${menu.split.horizontal}", '3', 0, "ui.split arg:3 axis:0" }, 1439 { "${menu.split.horizontal}", '3', 0, "ui.split arg:3 axis:0" },
1375 { "${menu.split.horizontal} 1:2", SDLK_d, 0, "ui.split arg:1 axis:0" }, 1440 { "${menu.split.horizontal} 1:2", SDLK_d, 0, "ui.split arg:1 axis:0" },
1376 { "${menu.split.horizontal} 2:1", SDLK_e, 0, "ui.split arg:2 axis:0" }, 1441 { "${menu.split.horizontal} 2:1", SDLK_e, 0, "ui.split arg:2 axis:0" },
1377 { "---", 0, 0, NULL }, 1442 { "---" },
1378 { "${menu.split.vertical}", '2', 0, "ui.split arg:3 axis:1" }, 1443 { "${menu.split.vertical}", '2', 0, "ui.split arg:3 axis:1" },
1379 { "${menu.split.vertical} 1:2", SDLK_f, 0, "ui.split arg:1 axis:1" }, 1444 { "${menu.split.vertical} 1:2", SDLK_f, 0, "ui.split arg:1 axis:1" },
1380 { "${menu.split.vertical} 2:1", SDLK_r, 0, "ui.split arg:2 axis:1" }, 1445 { "${menu.split.vertical} 2:1", SDLK_r, 0, "ui.split arg:2 axis:1" },
@@ -1440,7 +1505,7 @@ iRect rect_Root(const iRoot *d) {
1440} 1505}
1441 1506
1442iRect safeRect_Root(const iRoot *d) { 1507iRect safeRect_Root(const iRoot *d) {
1443 iRect rect = { zero_I2(), size_Root(d) }; 1508 iRect rect = rect_Root(d);
1444#if defined (iPlatformAppleMobile) 1509#if defined (iPlatformAppleMobile)
1445 float left, top, right, bottom; 1510 float left, top, right, bottom;
1446 safeAreaInsets_iOS(&left, &top, &right, &bottom); 1511 safeAreaInsets_iOS(&left, &top, &right, &bottom);
@@ -1450,5 +1515,5 @@ iRect safeRect_Root(const iRoot *d) {
1450} 1515}
1451 1516
1452iInt2 visibleSize_Root(const iRoot *d) { 1517iInt2 visibleSize_Root(const iRoot *d) {
1453 return addY_I2(size_Root(d), -get_Window()->keyboardHeight); 1518 return addY_I2(size_Root(d), -get_MainWindow()->keyboardHeight);
1454} 1519}
diff --git a/src/ui/root.h b/src/ui/root.h
index 740e97c9..04dd5e16 100644
--- a/src/ui/root.h
+++ b/src/ui/root.h
@@ -9,6 +9,7 @@ iDeclareType(Root)
9 9
10struct Impl_Root { 10struct Impl_Root {
11 iWidget * widget; 11 iWidget * widget;
12 iWindow * window;
12 iPtrArray *onTop; /* order is important; last one is topmost */ 13 iPtrArray *onTop; /* order is important; last one is topmost */
13 iPtrSet * pendingDestruction; 14 iPtrSet * pendingDestruction;
14 iBool pendingArrange; 15 iBool pendingArrange;
@@ -29,7 +30,6 @@ iAnyObject *findWidget_Root (const char *id); /* under curre
29 30
30iPtrArray * onTop_Root (iRoot *); 31iPtrArray * onTop_Root (iRoot *);
31void destroyPending_Root (iRoot *); 32void destroyPending_Root (iRoot *);
32void postArrange_Root (iRoot *);
33 33
34void updateMetrics_Root (iRoot *); 34void updateMetrics_Root (iRoot *);
35void updatePadding_Root (iRoot *); /* TODO: is part of metrics? */ 35void updatePadding_Root (iRoot *); /* TODO: is part of metrics? */
diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c
index 0bab601a..b6f73b6c 100644
--- a/src/ui/scrollwidget.c
+++ b/src/ui/scrollwidget.c
@@ -90,8 +90,22 @@ static int thumbSize_ScrollWidget_(const iScrollWidget *d) {
90 return iMax(gap_UI * 6, d->thumbSize); 90 return iMax(gap_UI * 6, d->thumbSize);
91} 91}
92 92
93static iRect bounds_ScrollWidget_(const iScrollWidget *d) {
94 const iWidget *w = constAs_Widget(d);
95 iRect bounds = bounds_Widget(w);
96 if (deviceType_App() == phone_AppDeviceType && isPortrait_App()) {
97 /* Account for the hidable toolbar. */
98 int toolbarHeight = lineHeight_Text(uiLabelLarge_FontId) + 3 * gap_UI;
99 int excess = bottom_Rect(bounds) - (bottom_Rect(safeRect_Root(w->root)) - toolbarHeight);
100 if (excess > 0) {
101 adjustEdges_Rect(&bounds, 0, 0, -excess, 0);
102 }
103 }
104 return bounds;
105}
106
93static iRect thumbRect_ScrollWidget_(const iScrollWidget *d) { 107static iRect thumbRect_ScrollWidget_(const iScrollWidget *d) {
94 const iRect bounds = bounds_Widget(constAs_Widget(d)); 108 const iRect bounds = bounds_ScrollWidget_(d);
95 iRect rect = init_Rect(bounds.pos.x, bounds.pos.y, bounds.size.x, 0); 109 iRect rect = init_Rect(bounds.pos.x, bounds.pos.y, bounds.size.x, 0);
96 const int total = size_Range(&d->range); 110 const int total = size_Range(&d->range);
97 if (total > 0) { 111 if (total > 0) {
@@ -181,7 +195,7 @@ static iBool processEvent_ScrollWidget_(iScrollWidget *d, const SDL_Event *ev) {
181 refresh_Widget(w); 195 refresh_Widget(w);
182 return iTrue; 196 return iTrue;
183 case drag_ClickResult: { 197 case drag_ClickResult: {
184 const iRect bounds = bounds_Widget(w); 198 const iRect bounds = bounds_ScrollWidget_(d);
185 const int offset = delta_Click(&d->click).y; 199 const int offset = delta_Click(&d->click).y;
186 const int total = size_Range(&d->range); 200 const int total = size_Range(&d->range);
187 int dpos = (float) offset / (float) (height_Rect(bounds) - thumbSize_ScrollWidget_(d)) * total; 201 int dpos = (float) offset / (float) (height_Rect(bounds) - thumbSize_ScrollWidget_(d)) * total;
@@ -218,7 +232,7 @@ static iBool processEvent_ScrollWidget_(iScrollWidget *d, const SDL_Event *ev) {
218 232
219static void draw_ScrollWidget_(const iScrollWidget *d) { 233static void draw_ScrollWidget_(const iScrollWidget *d) {
220 const iWidget *w = constAs_Widget(d); 234 const iWidget *w = constAs_Widget(d);
221 const iRect bounds = bounds_Widget(w); 235 const iRect bounds = bounds_ScrollWidget_(d);
222 const iBool isPressed = (flags_Widget(w) & pressed_WidgetFlag) != 0; 236 const iBool isPressed = (flags_Widget(w) & pressed_WidgetFlag) != 0;
223 if (bounds.size.x > 0) { 237 if (bounds.size.x > 0) {
224 iPaint p; 238 iPaint p;
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index c2ad7bc6..3018f16d 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -100,7 +100,7 @@ struct Impl_SidebarWidget {
100 int modeScroll[max_SidebarMode]; 100 int modeScroll[max_SidebarMode];
101 iLabelWidget * modeButtons[max_SidebarMode]; 101 iLabelWidget * modeButtons[max_SidebarMode];
102 int maxButtonLabelWidth; 102 int maxButtonLabelWidth;
103 int widthAsGaps; 103 float widthAsGaps;
104 int buttonFont; 104 int buttonFont;
105 int itemFonts[2]; 105 int itemFonts[2];
106 size_t numUnreadEntries; 106 size_t numUnreadEntries;
@@ -108,6 +108,7 @@ struct Impl_SidebarWidget {
108 iWidget * menu; 108 iWidget * menu;
109 iSidebarItem * contextItem; /* list item accessed in the context menu */ 109 iSidebarItem * contextItem; /* list item accessed in the context menu */
110 size_t contextIndex; /* index of list item accessed in the context menu */ 110 size_t contextIndex; /* index of list item accessed in the context menu */
111 iIntSet * closedFolders; /* otherwise open */
111}; 112};
112 113
113iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side) 114iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side)
@@ -116,23 +117,66 @@ static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) {
116 return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0; 117 return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0;
117} 118}
118 119
119static int cmpTitle_Bookmark_(const iBookmark **a, const iBookmark **b) { 120iBookmark *parent_Bookmark(const iBookmark *d) {
121 /* TODO: Parent pointers should be prefetched! */
122 if (d->parentId) {
123 return get_Bookmarks(bookmarks_App(), d->parentId);
124 }
125 return NULL;
126}
127
128iBool hasParent_Bookmark(const iBookmark *d, uint32_t parentId) {
129 /* TODO: Parent pointers should be prefetched! */
130 while (d->parentId) {
131 if (d->parentId == parentId) {
132 return iTrue;
133 }
134 d = get_Bookmarks(bookmarks_App(), d->parentId);
135 }
136 return iFalse;
137}
138
139int depth_Bookmark(const iBookmark *d) {
140 /* TODO: Precalculate this! */
141 int depth = 0;
142 for (; d->parentId; depth++) {
143 d = get_Bookmarks(bookmarks_App(), d->parentId);
144 }
145 return depth;
146}
147
148int cmpTree_Bookmark(const iBookmark **a, const iBookmark **b) {
120 const iBookmark *bm1 = *a, *bm2 = *b; 149 const iBookmark *bm1 = *a, *bm2 = *b;
121 if (bm2->sourceId == id_Bookmark(bm1)) { 150 /* Contents of a parent come after it. */
151 if (hasParent_Bookmark(bm2, id_Bookmark(bm1))) {
122 return -1; 152 return -1;
123 } 153 }
124 if (bm1->sourceId == id_Bookmark(bm2)) { 154 if (hasParent_Bookmark(bm1, id_Bookmark(bm2))) {
125 return 1; 155 return 1;
126 } 156 }
127 if (bm1->sourceId == bm2->sourceId) { 157 /* Comparisons are only valid inside the same parent. */
128 return cmpStringCase_String(&bm1->title, &bm2->title); 158 while (bm1->parentId != bm2->parentId) {
129 } 159 int depth1 = depth_Bookmark(bm1);
130 if (bm1->sourceId) { 160 int depth2 = depth_Bookmark(bm2);
131 bm1 = get_Bookmarks(bookmarks_App(), bm1->sourceId); 161 if (depth1 != depth2) {
132 } 162 /* Equalize the depth. */
133 if (bm2->sourceId) { 163 while (depth1 > depth2) {
134 bm2 = get_Bookmarks(bookmarks_App(), bm2->sourceId); 164 bm1 = parent_Bookmark(bm1);
165 depth1--;
166 }
167 while (depth2 > depth1) {
168 bm2 = parent_Bookmark(bm2);
169 depth2--;
170 }
171 continue;
172 }
173 bm1 = parent_Bookmark(bm1);
174 depth1--;
175 bm2 = parent_Bookmark(bm2);
176 depth2--;
135 } 177 }
178 const int cmp = iCmp(bm1->order, bm2->order);
179 if (cmp) return cmp;
136 return cmpStringCase_String(&bm1->title, &bm2->title); 180 return cmpStringCase_String(&bm1->title, &bm2->title);
137} 181}
138 182
@@ -143,7 +187,9 @@ static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const cha
143 //(deviceType_App() != desktop_AppDeviceType ? 187 //(deviceType_App() != desktop_AppDeviceType ?
144 // extraPadding_WidgetFlag : 0) | 188 // extraPadding_WidgetFlag : 0) |
145 flags); 189 flags);
146 setFont_LabelWidget(btn, d->buttonFont); 190 setFont_LabelWidget(btn, deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide
191 ? defaultBig_FontId
192 : d->buttonFont);
147 checkIcon_LabelWidget(btn); 193 checkIcon_LabelWidget(btn);
148 return btn; 194 return btn;
149} 195}
@@ -207,6 +253,16 @@ static void updateContextMenu_SidebarWidget_(iSidebarWidget *d) {
207 d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items)); 253 d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));
208} 254}
209 255
256static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBookmark *bm) {
257 while (bm->parentId) {
258 if (contains_IntSet(d->closedFolders, bm->parentId)) {
259 return iTrue;
260 }
261 bm = get_Bookmarks(bookmarks_App(), bm->parentId);
262 }
263 return iFalse;
264}
265
210static void updateItems_SidebarWidget_(iSidebarWidget *d) { 266static void updateItems_SidebarWidget_(iSidebarWidget *d) {
211 clear_ListWidget(d->list); 267 clear_ListWidget(d->list);
212 releaseChildren_Widget(d->blank); 268 releaseChildren_Widget(d->blank);
@@ -328,12 +384,24 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
328 iRegExp *homeTag = iClob(new_RegExp("\\b" homepage_BookmarkTag "\\b", caseSensitive_RegExpOption)); 384 iRegExp *homeTag = iClob(new_RegExp("\\b" homepage_BookmarkTag "\\b", caseSensitive_RegExpOption));
329 iRegExp *subTag = iClob(new_RegExp("\\b" subscribed_BookmarkTag "\\b", caseSensitive_RegExpOption)); 385 iRegExp *subTag = iClob(new_RegExp("\\b" subscribed_BookmarkTag "\\b", caseSensitive_RegExpOption));
330 iRegExp *remoteSourceTag = iClob(new_RegExp("\\b" remoteSource_BookmarkTag "\\b", caseSensitive_RegExpOption)); 386 iRegExp *remoteSourceTag = iClob(new_RegExp("\\b" remoteSource_BookmarkTag "\\b", caseSensitive_RegExpOption));
387 iRegExp *remoteTag = iClob(new_RegExp("\\b" remote_BookmarkTag "\\b", caseSensitive_RegExpOption));
331 iRegExp *linkSplitTag = iClob(new_RegExp("\\b" linkSplit_BookmarkTag "\\b", caseSensitive_RegExpOption)); 388 iRegExp *linkSplitTag = iClob(new_RegExp("\\b" linkSplit_BookmarkTag "\\b", caseSensitive_RegExpOption));
332 iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTitle_Bookmark_, NULL, NULL)) { 389 iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTree_Bookmark, NULL, NULL)) {
333 const iBookmark *bm = i.ptr; 390 const iBookmark *bm = i.ptr;
391 if (isBookmarkFolded_SidebarWidget_(d, bm)) {
392 continue; /* inside a closed folder */
393 }
334 iSidebarItem *item = new_SidebarItem(); 394 iSidebarItem *item = new_SidebarItem();
395 item->listItem.isDraggable = iTrue;
396 item->isBold = item->listItem.isDropTarget = isFolder_Bookmark(bm);
335 item->id = id_Bookmark(bm); 397 item->id = id_Bookmark(bm);
336 item->icon = bm->icon; 398 item->indent = depth_Bookmark(bm);
399 if (isFolder_Bookmark(bm)) {
400 item->icon = contains_IntSet(d->closedFolders, item->id) ? 0x27e9 : 0xfe40;
401 }
402 else {
403 item->icon = bm->icon;
404 }
337 set_String(&item->url, &bm->url); 405 set_String(&item->url, &bm->url);
338 set_String(&item->label, &bm->title); 406 set_String(&item->label, &bm->title);
339 /* Icons for special tags. */ { 407 /* Icons for special tags. */ {
@@ -347,6 +415,10 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
347 appendChar_String(&item->meta, 0x1f3e0); 415 appendChar_String(&item->meta, 0x1f3e0);
348 } 416 }
349 init_RegExpMatch(&m); 417 init_RegExpMatch(&m);
418 if (matchString_RegExp(remoteTag, &bm->tags, &m)) {
419 item->listItem.isDraggable = iFalse;
420 }
421 init_RegExpMatch(&m);
350 if (matchString_RegExp(remoteSourceTag, &bm->tags, &m)) { 422 if (matchString_RegExp(remoteSourceTag, &bm->tags, &m)) {
351 appendChar_String(&item->meta, 0x2913); 423 appendChar_String(&item->meta, 0x2913);
352 item->isBold = iTrue; 424 item->isBold = iTrue;
@@ -374,8 +446,11 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
374 { "---", 0, 0, NULL }, 446 { "---", 0, 0, NULL },
375 { delete_Icon " " uiTextCaution_ColorEscape "${bookmark.delete}", 0, 0, "bookmark.delete" }, 447 { delete_Icon " " uiTextCaution_ColorEscape "${bookmark.delete}", 0, 0, "bookmark.delete" },
376 { "---", 0, 0, NULL }, 448 { "---", 0, 0, NULL },
377 { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" } }, 449 { add_Icon " ${menu.newfolder}", 0, 0, "bookmark.addfolder" },
378 14); 450 { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" },
451 { "---", 0, 0, NULL },
452 { upDownArrow_Icon " ${menu.sort.alpha}", 0, 0, "bookmark.sortfolder" } },
453 17);
379 break; 454 break;
380 } 455 }
381 case history_SidebarMode: { 456 case history_SidebarMode: {
@@ -550,6 +625,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
550#endif 625#endif
551 arrange_Widget(d->actions); 626 arrange_Widget(d->actions);
552 arrange_Widget(as_Widget(d)); 627 arrange_Widget(as_Widget(d));
628 updateMouseHover_ListWidget(d->list);
553} 629}
554 630
555static void updateItemHeight_SidebarWidget_(iSidebarWidget *d) { 631static void updateItemHeight_SidebarWidget_(iSidebarWidget *d) {
@@ -579,6 +655,11 @@ iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) {
579 return iTrue; 655 return iTrue;
580} 656}
581 657
658void setClosedFolders_SidebarWidget(iSidebarWidget *d, const iIntSet *closedFolders) {
659 delete_IntSet(d->closedFolders);
660 d->closedFolders = copy_IntSet(closedFolders);
661}
662
582enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) { 663enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) {
583 return d ? d->mode : 0; 664 return d ? d->mode : 0;
584} 665}
@@ -587,6 +668,10 @@ float width_SidebarWidget(const iSidebarWidget *d) {
587 return d ? d->widthAsGaps : 0; 668 return d ? d->widthAsGaps : 0;
588} 669}
589 670
671const iIntSet *closedFolders_SidebarWidget(const iSidebarWidget *d) {
672 return d->closedFolders;
673}
674
590static const char *normalModeLabels_[max_SidebarMode] = { 675static const char *normalModeLabels_[max_SidebarMode] = {
591 book_Icon " ${sidebar.bookmarks}", 676 book_Icon " ${sidebar.bookmarks}",
592 star_Icon " ${sidebar.feeds}", 677 star_Icon " ${sidebar.feeds}",
@@ -641,17 +726,17 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
641 d->mode = -1; 726 d->mode = -1;
642 d->feedsMode = all_FeedsMode; 727 d->feedsMode = all_FeedsMode;
643 d->numUnreadEntries = 0; 728 d->numUnreadEntries = 0;
644 d->buttonFont = uiLabel_FontId; 729 d->buttonFont = uiLabel_FontId; /* wiil be changed later */
645 d->itemFonts[0] = uiContent_FontId; 730 d->itemFonts[0] = uiContent_FontId;
646 d->itemFonts[1] = uiContentBold_FontId; 731 d->itemFonts[1] = uiContentBold_FontId;
647#if defined (iPlatformAppleMobile) 732#if defined (iPlatformMobile)
648 if (deviceType_App() == phone_AppDeviceType) { 733 if (deviceType_App() == phone_AppDeviceType) {
649 d->itemFonts[0] = defaultBig_FontId; 734 d->itemFonts[0] = defaultBig_FontId;
650 d->itemFonts[1] = defaultBigBold_FontId; 735 d->itemFonts[1] = defaultBigBold_FontId;
651 } 736 }
652 d->widthAsGaps = 73; 737 d->widthAsGaps = 73.0f;
653#else 738#else
654 d->widthAsGaps = 60; 739 d->widthAsGaps = 60.0f;
655#endif 740#endif
656 setFlags_Widget(w, fixedWidth_WidgetFlag, iTrue); 741 setFlags_Widget(w, fixedWidth_WidgetFlag, iTrue);
657 iWidget *vdiv = makeVDiv_Widget(); 742 iWidget *vdiv = makeVDiv_Widget();
@@ -660,11 +745,13 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
660 d->resizer = NULL; 745 d->resizer = NULL;
661 d->list = NULL; 746 d->list = NULL;
662 d->actions = NULL; 747 d->actions = NULL;
748 d->closedFolders = new_IntSet();
663 /* On a phone, the right sidebar is used exclusively for Identities. */ 749 /* On a phone, the right sidebar is used exclusively for Identities. */
664 const iBool isPhone = deviceType_App() == phone_AppDeviceType; 750 const iBool isPhone = deviceType_App() == phone_AppDeviceType;
665 if (!isPhone || d->side == left_SidebarSide) { 751 if (!isPhone || d->side == left_SidebarSide) {
666 iWidget *buttons = new_Widget(); 752 iWidget *buttons = new_Widget();
667 setId_Widget(buttons, "buttons"); 753 setId_Widget(buttons, "buttons");
754 setDrawBufferEnabled_Widget(buttons, iTrue);
668 for (int i = 0; i < max_SidebarMode; i++) { 755 for (int i = 0; i < max_SidebarMode; i++) {
669 if (deviceType_App() == phone_AppDeviceType && i == identities_SidebarMode) { 756 if (deviceType_App() == phone_AppDeviceType && i == identities_SidebarMode) {
670 continue; 757 continue;
@@ -742,16 +829,21 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
742 829
743void deinit_SidebarWidget(iSidebarWidget *d) { 830void deinit_SidebarWidget(iSidebarWidget *d) {
744 deinit_String(&d->cmdPrefix); 831 deinit_String(&d->cmdPrefix);
832 delete_IntSet(d->closedFolders);
745} 833}
746 834
747void setButtonFont_SidebarWidget(iSidebarWidget *d, int font) { 835iBool setButtonFont_SidebarWidget(iSidebarWidget *d, int font) {
748 d->buttonFont = font; 836 if (d->buttonFont != font) {
749 for (int i = 0; i < max_SidebarMode; i++) { 837 d->buttonFont = font;
750 if (d->modeButtons[i]) { 838 for (int i = 0; i < max_SidebarMode; i++) {
751 setFont_LabelWidget(d->modeButtons[i], font); 839 if (d->modeButtons[i]) {
840 setFont_LabelWidget(d->modeButtons[i], font);
841 }
752 } 842 }
843 updateMetrics_SidebarWidget_(d);
844 return iTrue;
753 } 845 }
754 updateMetrics_SidebarWidget_(d); 846 return iFalse;
755} 847}
756 848
757static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) { 849static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) {
@@ -785,6 +877,17 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si
785 break; 877 break;
786 } 878 }
787 case bookmarks_SidebarMode: 879 case bookmarks_SidebarMode:
880 if (isEmpty_String(&item->url)) /* a folder */ {
881 if (contains_IntSet(d->closedFolders, item->id)) {
882 remove_IntSet(d->closedFolders, item->id);
883 }
884 else {
885 insert_IntSet(d->closedFolders, item->id);
886 }
887 updateItems_SidebarWidget_(d);
888 break;
889 }
890 /* fall through */
788 case history_SidebarMode: { 891 case history_SidebarMode: {
789 if (!isEmpty_String(&item->url)) { 892 if (!isEmpty_String(&item->url)) {
790 postCommandf_Root(get_Root(), "open fromsidebar:1 newtab:%d url:%s", 893 postCommandf_Root(get_Root(), "open fromsidebar:1 newtab:%d url:%s",
@@ -826,8 +929,10 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) {
826 if (d->itemFonts[0] != fonts[0]) { 929 if (d->itemFonts[0] != fonts[0]) {
827 d->itemFonts[0] = fonts[0]; 930 d->itemFonts[0] = fonts[0];
828 d->itemFonts[1] = fonts[1]; 931 d->itemFonts[1] = fonts[1];
829 updateMetrics_SidebarWidget_(d); 932// updateMetrics_SidebarWidget_(d);
933 updateItemHeight_SidebarWidget_(d);
830 } 934 }
935 setButtonFont_SidebarWidget(d, isPortrait_App() ? defaultBig_FontId : uiLabel_FontId);
831 } 936 }
832 const iBool isTight = 937 const iBool isTight =
833 (width_Rect(bounds_Widget(as_Widget(d->modeButtons[0]))) < d->maxButtonLabelWidth); 938 (width_Rect(bounds_Widget(as_Widget(d->modeButtons[0]))) < d->maxButtonLabelWidth);
@@ -857,17 +962,15 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) {
857void setWidth_SidebarWidget(iSidebarWidget *d, float widthAsGaps) { 962void setWidth_SidebarWidget(iSidebarWidget *d, float widthAsGaps) {
858 iWidget *w = as_Widget(d); 963 iWidget *w = as_Widget(d);
859 const iBool isFixedWidth = deviceType_App() == phone_AppDeviceType; 964 const iBool isFixedWidth = deviceType_App() == phone_AppDeviceType;
860 int width = widthAsGaps * gap_UI; 965 int width = widthAsGaps * gap_UI; /* in pixels */
861 if (!isFixedWidth) { 966 if (!isFixedWidth) {
862 /* Even less space if the other sidebar is visible, too. */ 967 /* Even less space if the other sidebar is visible, too. */
863 const int otherWidth = 968 const iWidget *other = findWidget_App(d->side == left_SidebarSide ? "sidebar2" : "sidebar");
864 width_Widget(findWidget_App(d->side == left_SidebarSide ? "sidebar2" : "sidebar")); 969 const int otherWidth = isVisible_Widget(other) ? width_Widget(other) : 0;
865 width = iClamp(width, 30 * gap_UI, size_Root(w->root).x - 50 * gap_UI - otherWidth); 970 width = iClamp(width, 30 * gap_UI, size_Root(w->root).x - 50 * gap_UI - otherWidth);
866 } 971 }
867 d->widthAsGaps = (float) width / (float) gap_UI; 972 d->widthAsGaps = (float) width / (float) gap_UI;
868 if (isVisible_Widget(w)) { 973 w->rect.size.x = width;
869 w->rect.size.x = width;
870 }
871 arrange_Widget(findWidget_Root("stack")); 974 arrange_Widget(findWidget_Root("stack"));
872 checkModeButtonLayout_SidebarWidget_(d); 975 checkModeButtonLayout_SidebarWidget_(d);
873 updateItemHeight_SidebarWidget_(d); 976 updateItemHeight_SidebarWidget_(d);
@@ -934,6 +1037,7 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *
934 if (wasChanged) { 1037 if (wasChanged) {
935 postCommandf_App("%s.mode.changed arg:%d", cstr_String(id_Widget(w)), d->mode); 1038 postCommandf_App("%s.mode.changed arg:%d", cstr_String(id_Widget(w)), d->mode);
936 } 1039 }
1040 refresh_Widget(findChild_Widget(w, "buttons"));
937 return iTrue; 1041 return iTrue;
938 } 1042 }
939 else if (equal_Command(cmd, "toggle")) { 1043 else if (equal_Command(cmd, "toggle")) {
@@ -989,6 +1093,43 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *
989 return iFalse; 1093 return iFalse;
990} 1094}
991 1095
1096static void bookmarkMoved_SidebarWidget_(iSidebarWidget *d, size_t index, size_t beforeIndex) {
1097 const iSidebarItem *movingItem = item_ListWidget(d->list, index);
1098 const iBool isLast = (beforeIndex == numItems_ListWidget(d->list));
1099 const iSidebarItem *dstItem = item_ListWidget(d->list,
1100 isLast ? numItems_ListWidget(d->list) - 1
1101 : beforeIndex);
1102 const iBookmark *dst = get_Bookmarks(bookmarks_App(), dstItem->id);
1103 if (hasParent_Bookmark(dst, movingItem->id)) {
1104 return;
1105 }
1106 reorder_Bookmarks(bookmarks_App(), movingItem->id, dst->order + (isLast ? 1 : 0));
1107 get_Bookmarks(bookmarks_App(), movingItem->id)->parentId = dst->parentId;
1108 updateItems_SidebarWidget_(d);
1109 /* Don't confuse the user: keep the dragged item in hover state. */
1110 setHoverItem_ListWidget(d->list, index < beforeIndex ? beforeIndex - 1 : beforeIndex);
1111 postCommandf_App("bookmarks.changed nosidebar:%p", d); /* skip this sidebar since we updated already */
1112}
1113
1114static void bookmarkMovedOntoFolder_SidebarWidget_(iSidebarWidget *d, size_t index,
1115 size_t folderIndex) {
1116 const iSidebarItem *movingItem = item_ListWidget(d->list, index);
1117 const iSidebarItem *dstItem = item_ListWidget(d->list, folderIndex);
1118 iBookmark *bm = get_Bookmarks(bookmarks_App(), movingItem->id);
1119 bm->parentId = dstItem->id;
1120 postCommand_App("bookmarks.changed");
1121}
1122
1123static size_t numBookmarks_(const iPtrArray *bmList) {
1124 size_t num = 0;
1125 iConstForEach(PtrArray, i, bmList) {
1126 if (!isFolder_Bookmark(i.ptr) && !hasTag_Bookmark(i.ptr, remote_BookmarkTag)) {
1127 num++;
1128 }
1129 }
1130 return num;
1131}
1132
992static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) { 1133static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) {
993 iWidget *w = as_Widget(d); 1134 iWidget *w = as_Widget(d);
994 /* Handle commands. */ 1135 /* Handle commands. */
@@ -1040,7 +1181,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1040 } 1181 }
1041 else if (equal_Command(cmd, "bookmarks.changed") && (d->mode == bookmarks_SidebarMode || 1182 else if (equal_Command(cmd, "bookmarks.changed") && (d->mode == bookmarks_SidebarMode ||
1042 d->mode == feeds_SidebarMode)) { 1183 d->mode == feeds_SidebarMode)) {
1043 updateItems_SidebarWidget_(d); 1184 if (pointerLabel_Command(cmd, "nosidebar") != d) {
1185 updateItems_SidebarWidget_(d);
1186 }
1044 } 1187 }
1045 else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) { 1188 else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) {
1046 updateItems_SidebarWidget_(d); 1189 updateItems_SidebarWidget_(d);
@@ -1096,6 +1239,21 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1096 d, pointerLabel_Command(cmd, "item"), argU32Label_Command(cmd, "arg")); 1239 d, pointerLabel_Command(cmd, "item"), argU32Label_Command(cmd, "arg"));
1097 return iTrue; 1240 return iTrue;
1098 } 1241 }
1242 else if (isCommand_Widget(w, ev, "list.dragged")) {
1243 iAssert(d->mode == bookmarks_SidebarMode);
1244 if (hasLabel_Command(cmd, "before")) {
1245 bookmarkMoved_SidebarWidget_(d,
1246 argU32Label_Command(cmd, "arg"),
1247 argU32Label_Command(cmd, "before"));
1248 }
1249 else {
1250 /* Dragged onto a folder. */
1251 bookmarkMovedOntoFolder_SidebarWidget_(d,
1252 argU32Label_Command(cmd, "arg"),
1253 argU32Label_Command(cmd, "onto"));
1254 }
1255 return iTrue;
1256 }
1099 else if (isCommand_Widget(w, ev, "menu.closed")) { 1257 else if (isCommand_Widget(w, ev, "menu.closed")) {
1100 // invalidateItem_ListWidget(d->list, d->contextIndex); 1258 // invalidateItem_ListWidget(d->list, d->contextIndex);
1101 } 1259 }
@@ -1128,15 +1286,12 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1128 setText_InputWidget(findChild_Widget(dlg, "bmed.icon"), 1286 setText_InputWidget(findChild_Widget(dlg, "bmed.icon"),
1129 collect_String(newUnicodeN_String(&bm->icon, 1))); 1287 collect_String(newUnicodeN_String(&bm->icon, 1)));
1130 } 1288 }
1131 setFlags_Widget(findChild_Widget(dlg, "bmed.tag.home"), 1289 setToggle_Widget(findChild_Widget(dlg, "bmed.tag.home"),
1132 selected_WidgetFlag, 1290 hasTag_Bookmark(bm, homepage_BookmarkTag));
1133 hasTag_Bookmark(bm, homepage_BookmarkTag)); 1291 setToggle_Widget(findChild_Widget(dlg, "bmed.tag.remote"),
1134 setFlags_Widget(findChild_Widget(dlg, "bmed.tag.remote"), 1292 hasTag_Bookmark(bm, remoteSource_BookmarkTag));
1135 selected_WidgetFlag, 1293 setToggle_Widget(findChild_Widget(dlg, "bmed.tag.linksplit"),
1136 hasTag_Bookmark(bm, remoteSource_BookmarkTag)); 1294 hasTag_Bookmark(bm, linkSplit_BookmarkTag));
1137 setFlags_Widget(findChild_Widget(dlg, "bmed.tag.linksplit"),
1138 selected_WidgetFlag,
1139 hasTag_Bookmark(bm, linkSplit_BookmarkTag));
1140 setCommandHandler_Widget(dlg, handleBookmarkEditorCommands_SidebarWidget_); 1295 setCommandHandler_Widget(dlg, handleBookmarkEditorCommands_SidebarWidget_);
1141 setFocus_Widget(findChild_Widget(dlg, "bmed.title")); 1296 setFocus_Widget(findChild_Widget(dlg, "bmed.title"));
1142 } 1297 }
@@ -1177,9 +1332,60 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1177 } 1332 }
1178 else if (isCommand_Widget(w, ev, "bookmark.delete")) { 1333 else if (isCommand_Widget(w, ev, "bookmark.delete")) {
1179 const iSidebarItem *item = d->contextItem; 1334 const iSidebarItem *item = d->contextItem;
1180 if (d->mode == bookmarks_SidebarMode && item && remove_Bookmarks(bookmarks_App(), item->id)) { 1335 if (d->mode == bookmarks_SidebarMode && item) {
1181 removeEntries_Feeds(item->id); 1336 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id);
1182 postCommand_App("bookmarks.changed"); 1337 if (isFolder_Bookmark(bm)) {
1338 const iPtrArray *list = list_Bookmarks(bookmarks_App(), NULL,
1339 filterInsideFolder_Bookmark, bm);
1340 /* Folder deletion requires confirmation because folders can contain
1341 any number of bookmarks and other folders. */
1342 if (argLabel_Command(cmd, "confirmed") || isEmpty_PtrArray(list)) {
1343 iConstForEach(PtrArray, i, list) {
1344 removeEntries_Feeds(id_Bookmark(i.ptr));
1345 }
1346 remove_Bookmarks(bookmarks_App(), item->id);
1347 postCommand_App("bookmarks.changed");
1348 }
1349 else {
1350 const size_t numBookmarks = numBookmarks_(list);
1351 makeQuestion_Widget(uiHeading_ColorEscape "${heading.confirm.bookmarks.delete}",
1352 formatCStrs_Lang("dlg.confirm.bookmarks.delete.n", numBookmarks),
1353 (iMenuItem[]){
1354 { "${cancel}" },
1355 { format_CStr(uiTextCaution_ColorEscape "%s",
1356 formatCStrs_Lang("dlg.bookmarks.delete.n", numBookmarks)),
1357 0, 0, format_CStr("!bookmark.delete confirmed:1 ptr:%p", d) },
1358 }, 2);
1359 }
1360 }
1361 else {
1362 /* TODO: Move it to a Trash folder? */
1363 if (remove_Bookmarks(bookmarks_App(), item->id)) {
1364 removeEntries_Feeds(item->id);
1365 postCommand_App("bookmarks.changed");
1366 }
1367 }
1368 }
1369 return iTrue;
1370 }
1371 else if (isCommand_Widget(w, ev, "bookmark.addfolder")) {
1372 const iSidebarItem *item = d->contextItem;
1373 if (d->mode == bookmarks_SidebarMode) {
1374 postCommandf_App("bookmarks.addfolder parent:%zu",
1375 !item ? 0
1376 : item->listItem.isDropTarget
1377 ? item->id
1378 : get_Bookmarks(bookmarks_App(), item->id)->parentId);
1379 }
1380 return iTrue;
1381 }
1382 else if (isCommand_Widget(w, ev, "bookmark.sortfolder")) {
1383 const iSidebarItem *item = d->contextItem;
1384 if (d->mode == bookmarks_SidebarMode && item) {
1385 postCommandf_App("bookmarks.sort arg:%zu",
1386 item->listItem.isDropTarget
1387 ? item->id
1388 : get_Bookmarks(bookmarks_App(), item->id)->parentId);
1183 } 1389 }
1184 return iTrue; 1390 return iTrue;
1185 } 1391 }
@@ -1449,40 +1655,29 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1449 if (d->mode == bookmarks_SidebarMode && d->contextItem) { 1655 if (d->mode == bookmarks_SidebarMode && d->contextItem) {
1450 const iBookmark *bm = get_Bookmarks(bookmarks_App(), d->contextItem->id); 1656 const iBookmark *bm = get_Bookmarks(bookmarks_App(), d->contextItem->id);
1451 if (bm) { 1657 if (bm) {
1452 iLabelWidget *menuItem = findMenuItem_Widget(d->menu, 1658 setMenuItemLabel_Widget(d->menu,
1453 "bookmark.tag tag:homepage"); 1659 "bookmark.tag tag:homepage",
1454 if (menuItem) { 1660 hasTag_Bookmark(bm, homepage_BookmarkTag)
1455 setTextCStr_LabelWidget(menuItem, 1661 ? home_Icon " ${bookmark.untag.home}"
1456 hasTag_Bookmark(bm, homepage_BookmarkTag) 1662 : home_Icon " ${bookmark.tag.home}");
1457 ? home_Icon " ${bookmark.untag.home}" 1663 setMenuItemLabel_Widget(d->menu,
1458 : home_Icon " ${bookmark.tag.home}"); 1664 "bookmark.tag tag:subscribed",
1459 checkIcon_LabelWidget(menuItem); 1665 hasTag_Bookmark(bm, subscribed_BookmarkTag)
1460 } 1666 ? star_Icon " ${bookmark.untag.sub}"
1461 menuItem = findMenuItem_Widget(d->menu, "bookmark.tag tag:subscribed"); 1667 : star_Icon " ${bookmark.tag.sub}");
1462 if (menuItem) { 1668 setMenuItemLabel_Widget(d->menu,
1463 setTextCStr_LabelWidget(menuItem, 1669 "bookmark.tag tag:remotesource",
1464 hasTag_Bookmark(bm, subscribed_BookmarkTag) 1670 hasTag_Bookmark(bm, remoteSource_BookmarkTag)
1465 ? star_Icon " ${bookmark.untag.sub}" 1671 ? downArrowBar_Icon " ${bookmark.untag.remote}"
1466 : star_Icon " ${bookmark.tag.sub}"); 1672 : downArrowBar_Icon " ${bookmark.tag.remote}");
1467 checkIcon_LabelWidget(menuItem);
1468 }
1469 menuItem = findMenuItem_Widget(d->menu, "bookmark.tag tag:remotesource");
1470 if (menuItem) {
1471 setTextCStr_LabelWidget(menuItem,
1472 hasTag_Bookmark(bm, remoteSource_BookmarkTag)
1473 ? downArrowBar_Icon " ${bookmark.untag.remote}"
1474 : downArrowBar_Icon " ${bookmark.tag.remote}");
1475 checkIcon_LabelWidget(menuItem);
1476 }
1477 } 1673 }
1478 } 1674 }
1479 else if (d->mode == feeds_SidebarMode && d->contextItem) { 1675 else if (d->mode == feeds_SidebarMode && d->contextItem) {
1480 iLabelWidget *menuItem = findMenuItem_Widget(d->menu, "feed.entry.toggleread");
1481 const iBool isRead = d->contextItem->indent == 0; 1676 const iBool isRead = d->contextItem->indent == 0;
1482 setTextCStr_LabelWidget(menuItem, 1677 setMenuItemLabel_Widget(d->menu,
1678 "feed.entry.toggleread",
1483 isRead ? circle_Icon " ${feeds.entry.markunread}" 1679 isRead ? circle_Icon " ${feeds.entry.markunread}"
1484 : circleWhite_Icon " ${feeds.entry.markread}"); 1680 : circleWhite_Icon " ${feeds.entry.markread}");
1485 checkIcon_LabelWidget(menuItem);
1486 } 1681 }
1487 else if (d->mode == identities_SidebarMode) { 1682 else if (d->mode == identities_SidebarMode) {
1488 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d); 1683 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
@@ -1555,7 +1750,7 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
1555 const iRect bounds = bounds_Widget(w); 1750 const iRect bounds = bounds_Widget(w);
1556 iPaint p; 1751 iPaint p;
1557 init_Paint(&p); 1752 init_Paint(&p);
1558 if (deviceType_App() != phone_AppDeviceType) { 1753 if (!isPortraitPhone_App()) { /* this would erase page contents during transition on the phone */
1559 if (flags_Widget(w) & visualOffset_WidgetFlag && 1754 if (flags_Widget(w) & visualOffset_WidgetFlag &&
1560 flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) { 1755 flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) {
1561 fillRect_Paint(&p, boundsWithoutVisualOffset_Widget(w), tmBackground_ColorId); 1756 fillRect_Paint(&p, boundsWithoutVisualOffset_Widget(w), tmBackground_ColorId);
@@ -1576,12 +1771,14 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1576 const iSidebarWidget *sidebar = findParentClass_Widget(constAs_Widget(list), 1771 const iSidebarWidget *sidebar = findParentClass_Widget(constAs_Widget(list),
1577 &Class_SidebarWidget); 1772 &Class_SidebarWidget);
1578 const iBool isMenuVisible = isVisible_Widget(sidebar->menu); 1773 const iBool isMenuVisible = isVisible_Widget(sidebar->menu);
1579 const iBool isPressing = isMouseDown_ListWidget(list); 1774 const iBool isDragging = constDragItem_ListWidget(list) == d;
1775 const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging;
1580 const iBool isHover = 1776 const iBool isHover =
1581 (!isMenuVisible && 1777 (!isMenuVisible &&
1582 isHover_Widget(constAs_Widget(list)) && 1778 isHover_Widget(constAs_Widget(list)) &&
1583 constHoverItem_ListWidget(list) == d) || 1779 constHoverItem_ListWidget(list) == d) ||
1584 (isMenuVisible && sidebar->contextItem == d); 1780 (isMenuVisible && sidebar->contextItem == d) ||
1781 isDragging;
1585 const int scrollBarWidth = scrollBarWidth_ListWidget(list); 1782 const int scrollBarWidth = scrollBarWidth_ListWidget(list);
1586#if defined (iPlatformApple) 1783#if defined (iPlatformApple)
1587 const int blankWidth = 0; 1784 const int blankWidth = 0;
@@ -1605,7 +1802,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1605 fillRect_Paint(p, itemRect, bg); 1802 fillRect_Paint(p, itemRect, bg);
1606 } 1803 }
1607 else if (sidebar->mode == bookmarks_SidebarMode) { 1804 else if (sidebar->mode == bookmarks_SidebarMode) {
1608 if (d->icon == 0x2913) { /* TODO: Remote icon; meaning: is this in a folder? */ 1805 if (d->indent) /* remote icon */ {
1609 bg = uiBackgroundFolder_ColorId; 1806 bg = uiBackgroundFolder_ColorId;
1610 fillRect_Paint(p, itemRect, bg); 1807 fillRect_Paint(p, itemRect, bg);
1611 } 1808 }
@@ -1707,11 +1904,13 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1707 } 1904 }
1708 else if (sidebar->mode == bookmarks_SidebarMode) { 1905 else if (sidebar->mode == bookmarks_SidebarMode) {
1709 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId) 1906 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)
1710 : uiText_ColorId; 1907 : d->listItem.isDropTarget ? uiHeading_ColorId : uiText_ColorId;
1908 /* The icon. */
1711 iString str; 1909 iString str;
1712 init_String(&str); 1910 init_String(&str);
1713 appendChar_String(&str, d->icon ? d->icon : 0x1f588); 1911 appendChar_String(&str, d->icon ? d->icon : 0x1f588);
1714 const iRect iconArea = { addX_I2(pos, gap_UI), 1912 const int leftIndent = d->indent * gap_UI * 4;
1913 const iRect iconArea = { addX_I2(pos, gap_UI + leftIndent),
1715 init_I2(1.75f * lineHeight_Text(font), itemHeight) }; 1914 init_I2(1.75f * lineHeight_Text(font), itemHeight) };
1716 drawCentered_Text(font, 1915 drawCentered_Text(font,
1717 iconArea, 1916 iconArea,
@@ -1732,12 +1931,14 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1732 metaIconWidth 1931 metaIconWidth
1733 - 2 * gap_UI - (blankWidth ? blankWidth - 1.5f * gap_UI : (gap_UI / 2)), 1932 - 2 * gap_UI - (blankWidth ? blankWidth - 1.5f * gap_UI : (gap_UI / 2)),
1734 textPos.y); 1933 textPos.y);
1735 fillRect_Paint(p, 1934 if (!isDragging) {
1736 init_Rect(metaPos.x, 1935 fillRect_Paint(p,
1737 top_Rect(itemRect), 1936 init_Rect(metaPos.x,
1738 right_Rect(itemRect) - metaPos.x, 1937 top_Rect(itemRect),
1739 height_Rect(itemRect)), 1938 right_Rect(itemRect) - metaPos.x,
1740 bg); 1939 height_Rect(itemRect)),
1940 bg);
1941 }
1741 iInt2 mpos = metaPos; 1942 iInt2 mpos = metaPos;
1742 iStringConstIterator iter; 1943 iStringConstIterator iter;
1743 init_StringConstIterator(&iter, &d->meta); 1944 init_StringConstIterator(&iter, &d->meta);
diff --git a/src/ui/sidebarwidget.h b/src/ui/sidebarwidget.h
index 130242ab..638a1f2f 100644
--- a/src/ui/sidebarwidget.h
+++ b/src/ui/sidebarwidget.h
@@ -24,6 +24,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
24 24
25#include "widget.h" 25#include "widget.h"
26 26
27#include <the_Foundation/intset.h>
28
27enum iSidebarMode { 29enum iSidebarMode {
28 bookmarks_SidebarMode, 30 bookmarks_SidebarMode,
29 feeds_SidebarMode, 31 feeds_SidebarMode,
@@ -49,8 +51,10 @@ iDeclareWidgetClass(SidebarWidget)
49iDeclareObjectConstructionArgs(SidebarWidget, enum iSidebarSide side) 51iDeclareObjectConstructionArgs(SidebarWidget, enum iSidebarSide side)
50 52
51iBool setMode_SidebarWidget (iSidebarWidget *, enum iSidebarMode mode); 53iBool setMode_SidebarWidget (iSidebarWidget *, enum iSidebarMode mode);
52void setButtonFont_SidebarWidget (iSidebarWidget *, int font); 54void setWidth_SidebarWidget (iSidebarWidget *, float widthAsGaps);
55iBool setButtonFont_SidebarWidget (iSidebarWidget *, int font);
56void setClosedFolders_SidebarWidget (iSidebarWidget *, const iIntSet *closedFolders);
53 57
54enum iSidebarMode mode_SidebarWidget (const iSidebarWidget *); 58enum iSidebarMode mode_SidebarWidget (const iSidebarWidget *);
55float width_SidebarWidget (const iSidebarWidget *); 59float width_SidebarWidget (const iSidebarWidget *);
56void setWidth_SidebarWidget (iSidebarWidget *, float widthAsGaps); 60const iIntSet * closedFolders_SidebarWidget (const iSidebarWidget *);
diff --git a/src/ui/text.c b/src/ui/text.c
index 006a4d0b..de8f10a4 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
25#include "metrics.h" 25#include "metrics.h"
26#include "embedded.h" 26#include "embedded.h"
27#include "window.h" 27#include "window.h"
28#include "paint.h"
28#include "app.h" 29#include "app.h"
29 30
30#define STB_TRUETYPE_IMPLEMENTATION 31#define STB_TRUETYPE_IMPLEMENTATION
@@ -120,6 +121,8 @@ iDefineTypeConstructionArgs(Glyph, (iChar ch), ch)
120 121
121/*-----------------------------------------------------------------------------------------------*/ 122/*-----------------------------------------------------------------------------------------------*/
122 123
124static iGlyph *glyph_Font_(iFont *d, iChar ch);
125
123struct Impl_Font { 126struct Impl_Font {
124 iBlock * data; 127 iBlock * data;
125 enum iTextFont family; 128 enum iTextFont family;
@@ -130,6 +133,7 @@ struct Impl_Font {
130 int baseline; 133 int baseline;
131 iHash glyphs; /* key is glyph index in the font */ /* TODO: does not need to be a Hash */ 134 iHash glyphs; /* key is glyph index in the font */ /* TODO: does not need to be a Hash */
132 iBool isMonospaced; 135 iBool isMonospaced;
136 float emAdvance;
133 enum iFontSize sizeId; /* used to look up different fonts of matching size */ 137 enum iFontSize sizeId; /* used to look up different fonts of matching size */
134 uint32_t indexTable[128 - 32]; /* quick ASCII lookup */ 138 uint32_t indexTable[128 - 32]; /* quick ASCII lookup */
135#if defined (LAGRANGE_ENABLE_HARFBUZZ) 139#if defined (LAGRANGE_ENABLE_HARFBUZZ)
@@ -177,20 +181,20 @@ static void init_Font(iFont *d, const iBlock *data, int height, float scale,
177 d->height = height; 181 d->height = height;
178 iZap(d->font); 182 iZap(d->font);
179 stbtt_InitFont(&d->font, constData_Block(data), 0); 183 stbtt_InitFont(&d->font, constData_Block(data), 0);
180 int ascent, descent; 184 int ascent, descent, emAdv;
181 stbtt_GetFontVMetrics(&d->font, &ascent, &descent, NULL); 185 stbtt_GetFontVMetrics(&d->font, &ascent, &descent, NULL);
186 stbtt_GetCodepointHMetrics(&d->font, 'M', &emAdv, NULL);
182 d->xScale = d->yScale = stbtt_ScaleForPixelHeight(&d->font, height) * scale; 187 d->xScale = d->yScale = stbtt_ScaleForPixelHeight(&d->font, height) * scale;
183 if (d->isMonospaced) { 188 if (d->isMonospaced) {
184 /* It is important that monospaced fonts align 1:1 with the pixel grid so that 189 /* It is important that monospaced fonts align 1:1 with the pixel grid so that
185 box-drawing characters don't have partially occupied edge pixels, leading to seams 190 box-drawing characters don't have partially occupied edge pixels, leading to seams
186 between adjacent glyphs. */ 191 between adjacent glyphs. */
187 int adv; 192 const float advance = (float) emAdv * d->xScale;
188 stbtt_GetCodepointHMetrics(&d->font, 'M', &adv, NULL);
189 const float advance = (float) adv * d->xScale;
190 if (advance > 4) { /* not too tiny */ 193 if (advance > 4) { /* not too tiny */
191 d->xScale *= floorf(advance) / advance; 194 d->xScale *= floorf(advance) / advance;
192 } 195 }
193 } 196 }
197 d->emAdvance = emAdv * d->xScale;
194 d->baseline = ascent * d->yScale; 198 d->baseline = ascent * d->yScale;
195 d->vertOffset = height * (1.0f - scale) / 2; 199 d->vertOffset = height * (1.0f - scale) / 2;
196 /* Custom tweaks. */ 200 /* Custom tweaks. */
@@ -289,7 +293,9 @@ struct Impl_Text {
289 iRegExp * ansiEscape; 293 iRegExp * ansiEscape;
290}; 294};
291 295
292static iText text_; 296iDefineTypeConstructionArgs(Text, (SDL_Renderer *render), render)
297
298static iText *activeText_;
293static iBlock *userFont_; 299static iBlock *userFont_;
294 300
295static void initFonts_Text_(iText *d) { 301static void initFonts_Text_(iText *d) {
@@ -361,7 +367,7 @@ static void initFonts_Text_(iText *d) {
361 h12Font = &fontIosevkaTermExtended_Embedded; 367 h12Font = &fontIosevkaTermExtended_Embedded;
362 h3Font = &fontIosevkaTermExtended_Embedded; 368 h3Font = &fontIosevkaTermExtended_Embedded;
363 } 369 }
364#if defined (iPlatformAppleMobile) 370#if defined (iPlatformMobile)
365 const float uiSize = fontSize_UI * 1.1f; 371 const float uiSize = fontSize_UI * 1.1f;
366#else 372#else
367 const float uiSize = fontSize_UI; 373 const float uiSize = fontSize_UI;
@@ -500,8 +506,7 @@ void loadUserFonts_Text(void) {
500 } 506 }
501} 507}
502 508
503void init_Text(SDL_Renderer *render) { 509void init_Text(iText *d, SDL_Renderer *render) {
504 iText *d = &text_;
505 loadUserFonts_Text(); 510 loadUserFonts_Text();
506 d->contentFont = nunito_TextFont; 511 d->contentFont = nunito_TextFont;
507 d->headingFont = nunito_TextFont; 512 d->headingFont = nunito_TextFont;
@@ -520,8 +525,7 @@ void init_Text(SDL_Renderer *render) {
520 initFonts_Text_(d); 525 initFonts_Text_(d);
521} 526}
522 527
523void deinit_Text(void) { 528void deinit_Text(iText *d) {
524 iText *d = &text_;
525 SDL_FreePalette(d->grayscale); 529 SDL_FreePalette(d->grayscale);
526 deinitFonts_Text_(d); 530 deinitFonts_Text_(d);
527 deinitCache_Text_(d); 531 deinitCache_Text_(d);
@@ -529,30 +533,34 @@ void deinit_Text(void) {
529 iRelease(d->ansiEscape); 533 iRelease(d->ansiEscape);
530} 534}
531 535
536void setCurrent_Text(iText *d) {
537 activeText_ = d;
538}
539
532void setOpacity_Text(float opacity) { 540void setOpacity_Text(float opacity) {
533 SDL_SetTextureAlphaMod(text_.cache, iClamp(opacity, 0.0f, 1.0f) * 255 + 0.5f); 541 SDL_SetTextureAlphaMod(activeText_->cache, iClamp(opacity, 0.0f, 1.0f) * 255 + 0.5f);
534} 542}
535 543
536void setContentFont_Text(enum iTextFont font) { 544void setContentFont_Text(iText *d, enum iTextFont font) {
537 if (text_.contentFont != font) { 545 if (d->contentFont != font) {
538 text_.contentFont = font; 546 d->contentFont = font;
539 resetFonts_Text(); 547 resetFonts_Text(d);
540 } 548 }
541} 549}
542 550
543void setHeadingFont_Text(enum iTextFont font) { 551void setHeadingFont_Text(iText *d, enum iTextFont font) {
544 if (text_.headingFont != font) { 552 if (d->headingFont != font) {
545 text_.headingFont = font; 553 d->headingFont = font;
546 resetFonts_Text(); 554 resetFonts_Text(d);
547 } 555 }
548} 556}
549 557
550void setContentFontSize_Text(float fontSizeFactor) { 558void setContentFontSize_Text(iText *d, float fontSizeFactor) {
551 fontSizeFactor *= contentScale_Text_; 559 fontSizeFactor *= contentScale_Text_;
552 iAssert(fontSizeFactor > 0); 560 iAssert(fontSizeFactor > 0);
553 if (iAbs(text_.contentFontSize - fontSizeFactor) > 0.001f) { 561 if (iAbs(d->contentFontSize - fontSizeFactor) > 0.001f) {
554 text_.contentFontSize = fontSizeFactor; 562 d->contentFontSize = fontSizeFactor;
555 resetFonts_Text(); 563 resetFonts_Text(d);
556 } 564 }
557} 565}
558 566
@@ -564,8 +572,7 @@ static void resetCache_Text_(iText *d) {
564 initCache_Text_(d); 572 initCache_Text_(d);
565} 573}
566 574
567void resetFonts_Text(void) { 575void resetFonts_Text(iText *d) {
568 iText *d = &text_;
569 deinitFonts_Text_(d); 576 deinitFonts_Text_(d);
570 deinitCache_Text_(d); 577 deinitCache_Text_(d);
571 initCache_Text_(d); 578 initCache_Text_(d);
@@ -573,7 +580,7 @@ void resetFonts_Text(void) {
573} 580}
574 581
575iLocalDef iFont *font_Text_(enum iFontId id) { 582iLocalDef iFont *font_Text_(enum iFontId id) {
576 return &text_.fonts[id & mask_FontId]; 583 return &activeText_->fonts[id & mask_FontId];
577} 584}
578 585
579static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, float xShift) { 586static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, float xShift) {
@@ -583,7 +590,7 @@ static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, fl
583 SDL_Surface *surface8 = 590 SDL_Surface *surface8 =
584 SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8); 591 SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8);
585 SDL_SetSurfaceBlendMode(surface8, SDL_BLENDMODE_NONE); 592 SDL_SetSurfaceBlendMode(surface8, SDL_BLENDMODE_NONE);
586 SDL_SetSurfacePalette(surface8, text_.grayscale); 593 SDL_SetSurfacePalette(surface8, activeText_->grayscale);
587#if LAGRANGE_RASTER_DEPTH != 8 594#if LAGRANGE_RASTER_DEPTH != 8
588 /* Convert to the cache format. */ 595 /* Convert to the cache format. */
589 SDL_Surface *surf = SDL_ConvertSurfaceFormat(surface8, LAGRANGE_RASTER_FORMAT, 0); 596 SDL_Surface *surf = SDL_ConvertSurfaceFormat(surface8, LAGRANGE_RASTER_FORMAT, 0);
@@ -630,7 +637,7 @@ static void allocate_Font_(iFont *d, iGlyph *glyph, int hoff) {
630 &d->font, index_Glyph_(glyph), d->xScale, d->yScale, hoff * 0.5f, 0.0f, &x0, &y0, &x1, &y1); 637 &d->font, index_Glyph_(glyph), d->xScale, d->yScale, hoff * 0.5f, 0.0f, &x0, &y0, &x1, &y1);
631 glRect->size = init_I2(x1 - x0, y1 - y0); 638 glRect->size = init_I2(x1 - x0, y1 - y0);
632 /* Determine placement in the glyph cache texture, advancing in rows. */ 639 /* Determine placement in the glyph cache texture, advancing in rows. */
633 glRect->pos = assignCachePos_Text_(&text_, glRect->size); 640 glRect->pos = assignCachePos_Text_(activeText_, glRect->size);
634 glyph->d[hoff] = init_I2(x0, y0); 641 glyph->d[hoff] = init_I2(x0, y0);
635 glyph->d[hoff].y += d->vertOffset; 642 glyph->d[hoff].y += d->vertOffset;
636 if (hoff == 0) { /* hoff==1 uses same metrics as `glyph` */ 643 if (hoff == 0) { /* hoff==1 uses same metrics as `glyph` */
@@ -736,11 +743,11 @@ static iGlyph *glyphByIndex_Font_(iFont *d, uint32_t glyphIndex) {
736 } 743 }
737 else { 744 else {
738 /* If the cache is running out of space, clear it and we'll recache what's needed currently. */ 745 /* If the cache is running out of space, clear it and we'll recache what's needed currently. */
739 if (text_.cacheBottom > text_.cacheSize.y - maxGlyphHeight_Text_(&text_)) { 746 if (activeText_->cacheBottom > activeText_->cacheSize.y - maxGlyphHeight_Text_(activeText_)) {
740#if !defined (NDEBUG) 747#if !defined (NDEBUG)
741 printf("[Text] glyph cache is full, clearing!\n"); fflush(stdout); 748 printf("[Text] glyph cache is full, clearing!\n"); fflush(stdout);
742#endif 749#endif
743 resetCache_Text_(&text_); 750 resetCache_Text_(activeText_);
744 } 751 }
745 glyph = new_Glyph(glyphIndex); 752 glyph = new_Glyph(glyphIndex);
746 glyph->font = d; 753 glyph->font = d;
@@ -857,7 +864,7 @@ static void finishRun_AttributedText_(iAttributedText *d, iAttributedRun *run, i
857} 864}
858 865
859static enum iFontId fontId_Text_(const iFont *font) { 866static enum iFontId fontId_Text_(const iFont *font) {
860 return (enum iFontId) (font - text_.fonts); 867 return (enum iFontId) (font - activeText_->fonts);
861} 868}
862 869
863static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iChar overrideChar) { 870static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iChar overrideChar) {
@@ -889,6 +896,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
889 resize_Array(&d->visualToLogical, length); 896 resize_Array(&d->visualToLogical, length);
890 d->bidiLevels = length ? malloc(length) : NULL; 897 d->bidiLevels = length ? malloc(length) : NULL;
891 FriBidiParType baseDir = (FriBidiParType) FRIBIDI_TYPE_ON; 898 FriBidiParType baseDir = (FriBidiParType) FRIBIDI_TYPE_ON;
899 /* TODO: If this returns zero (error occurred), act like everything is LTR. */
892 fribidi_log2vis(constData_Array(&d->logical), 900 fribidi_log2vis(constData_Array(&d->logical),
893 length, 901 length,
894 &baseDir, 902 &baseDir,
@@ -947,7 +955,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
947 /* Do a regexp match in the source text. */ 955 /* Do a regexp match in the source text. */
948 iRegExpMatch m; 956 iRegExpMatch m;
949 init_RegExpMatch(&m); 957 init_RegExpMatch(&m);
950 if (match_RegExp(text_.ansiEscape, srcPos, d->source.end - srcPos, &m)) { 958 if (match_RegExp(activeText_->ansiEscape, srcPos, d->source.end - srcPos, &m)) {
951 finishRun_AttributedText_(d, &run, pos - 1); 959 finishRun_AttributedText_(d, &run, pos - 1);
952 run.fgColor = ansiForeground_Color(capturedRange_RegExpMatch(&m, 1), 960 run.fgColor = ansiForeground_Color(capturedRange_RegExpMatch(&m, 1),
953 tmParagraph_ColorId); 961 tmParagraph_ColorId);
@@ -1080,9 +1088,9 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) {
1080 while (index < size_Array(glyphIndices)) { 1088 while (index < size_Array(glyphIndices)) {
1081 for (; index < size_Array(glyphIndices); index++) { 1089 for (; index < size_Array(glyphIndices); index++) {
1082 const uint32_t glyphIndex = constValue_Array(glyphIndices, index, uint32_t); 1090 const uint32_t glyphIndex = constValue_Array(glyphIndices, index, uint32_t);
1083 const int lastCacheBottom = text_.cacheBottom; 1091 const int lastCacheBottom = activeText_->cacheBottom;
1084 iGlyph *glyph = glyphByIndex_Font_(d, glyphIndex); 1092 iGlyph *glyph = glyphByIndex_Font_(d, glyphIndex);
1085 if (text_.cacheBottom < lastCacheBottom) { 1093 if (activeText_->cacheBottom < lastCacheBottom) {
1086 /* The cache was reset due to running out of space. We need to restart from 1094 /* The cache was reset due to running out of space. We need to restart from
1087 the beginning! */ 1095 the beginning! */
1088 bufX = 0; 1096 bufX = 0;
@@ -1101,7 +1109,7 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) {
1101 LAGRANGE_RASTER_DEPTH, 1109 LAGRANGE_RASTER_DEPTH,
1102 LAGRANGE_RASTER_FORMAT); 1110 LAGRANGE_RASTER_FORMAT);
1103 SDL_SetSurfaceBlendMode(buf, SDL_BLENDMODE_NONE); 1111 SDL_SetSurfaceBlendMode(buf, SDL_BLENDMODE_NONE);
1104 SDL_SetSurfacePalette(buf, text_.grayscale); 1112 SDL_SetSurfacePalette(buf, activeText_->grayscale);
1105 } 1113 }
1106 SDL_Surface *surfaces[2] = { 1114 SDL_Surface *surfaces[2] = {
1107 !isRasterized_Glyph_(glyph, 0) ? 1115 !isRasterized_Glyph_(glyph, 0) ?
@@ -1145,19 +1153,19 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) {
1145 } 1153 }
1146 /* Finished or the buffer is full, copy the glyphs to the cache texture. */ 1154 /* Finished or the buffer is full, copy the glyphs to the cache texture. */
1147 if (!isEmpty_Array(rasters)) { 1155 if (!isEmpty_Array(rasters)) {
1148 SDL_Texture *bufTex = SDL_CreateTextureFromSurface(text_.render, buf); 1156 SDL_Texture *bufTex = SDL_CreateTextureFromSurface(activeText_->render, buf);
1149 SDL_SetTextureBlendMode(bufTex, SDL_BLENDMODE_NONE); 1157 SDL_SetTextureBlendMode(bufTex, SDL_BLENDMODE_NONE);
1150 if (!isTargetChanged) { 1158 if (!isTargetChanged) {
1151 isTargetChanged = iTrue; 1159 isTargetChanged = iTrue;
1152 oldTarget = SDL_GetRenderTarget(text_.render); 1160 oldTarget = SDL_GetRenderTarget(activeText_->render);
1153 SDL_SetRenderTarget(text_.render, text_.cache); 1161 SDL_SetRenderTarget(activeText_->render, activeText_->cache);
1154 } 1162 }
1155// printf("copying %zu rasters from %p\n", size_Array(rasters), bufTex); fflush(stdout); 1163// printf("copying %zu rasters from %p\n", size_Array(rasters), bufTex); fflush(stdout);
1156 iConstForEach(Array, i, rasters) { 1164 iConstForEach(Array, i, rasters) {
1157 const iRasterGlyph *rg = i.value; 1165 const iRasterGlyph *rg = i.value;
1158// iAssert(isEqual_I2(rg->rect.size, rg->glyph->rect[rg->hoff].size)); 1166// iAssert(isEqual_I2(rg->rect.size, rg->glyph->rect[rg->hoff].size));
1159 const iRect *glRect = &rg->glyph->rect[rg->hoff]; 1167 const iRect *glRect = &rg->glyph->rect[rg->hoff];
1160 SDL_RenderCopy(text_.render, 1168 SDL_RenderCopy(activeText_->render,
1161 bufTex, 1169 bufTex,
1162 (const SDL_Rect *) &rg->rect, 1170 (const SDL_Rect *) &rg->rect,
1163 (const SDL_Rect *) glRect); 1171 (const SDL_Rect *) glRect);
@@ -1177,7 +1185,7 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) {
1177 SDL_FreeSurface(buf); 1185 SDL_FreeSurface(buf);
1178 } 1186 }
1179 if (isTargetChanged) { 1187 if (isTargetChanged) {
1180 SDL_SetRenderTarget(text_.render, oldTarget); 1188 SDL_SetRenderTarget(activeText_->render, oldTarget);
1181 } 1189 }
1182} 1190}
1183 1191
@@ -1330,6 +1338,11 @@ static void shape_GlyphBuffer_(iGlyphBuffer *d) {
1330 } 1338 }
1331} 1339}
1332 1340
1341static float nextTabStop_Font_(const iFont *d, float x) {
1342 const float stop = 8 * d->emAdvance;
1343 return floorf(x / stop) * stop + stop;
1344}
1345
1333static float advance_GlyphBuffer_(const iGlyphBuffer *d, iRangei wrapPosRange) { 1346static float advance_GlyphBuffer_(const iGlyphBuffer *d, iRangei wrapPosRange) {
1334 float x = 0.0f; 1347 float x = 0.0f;
1335 for (unsigned int i = 0; i < d->glyphCount; i++) { 1348 for (unsigned int i = 0; i < d->glyphCount; i++) {
@@ -1338,6 +1351,9 @@ static float advance_GlyphBuffer_(const iGlyphBuffer *d, iRangei wrapPosRange) {
1338 continue; 1351 continue;
1339 } 1352 }
1340 x += d->font->xScale * d->glyphPos[i].x_advance; 1353 x += d->font->xScale * d->glyphPos[i].x_advance;
1354 if (d->logicalText[logPos] == '\t') {
1355 x = nextTabStop_Font_(d->font, x);
1356 }
1341 if (i + 1 < d->glyphCount) { 1357 if (i + 1 < d->glyphCount) {
1342 x += horizKern_Font_(d->font, 1358 x += horizKern_Font_(d->font,
1343 d->glyphInfo[i].codepoint, 1359 d->glyphInfo[i].codepoint,
@@ -1349,7 +1365,7 @@ static float advance_GlyphBuffer_(const iGlyphBuffer *d, iRangei wrapPosRange) {
1349 1365
1350static void evenMonospaceAdvances_GlyphBuffer_(iGlyphBuffer *d, iFont *baseFont) { 1366static void evenMonospaceAdvances_GlyphBuffer_(iGlyphBuffer *d, iFont *baseFont) {
1351 shape_GlyphBuffer_(d); 1367 shape_GlyphBuffer_(d);
1352 const float monoAdvance = glyph_Font_(baseFont, 'M')->advance; 1368 const float monoAdvance = baseFont->emAdvance;
1353 for (unsigned int i = 0; i < d->glyphCount; ++i) { 1369 for (unsigned int i = 0; i < d->glyphCount; ++i) {
1354 const hb_glyph_info_t *info = d->glyphInfo + i; 1370 const hb_glyph_info_t *info = d->glyphInfo + i;
1355 if (d->glyphPos[i].x_advance > 0 && d->font != baseFont) { 1371 if (d->glyphPos[i].x_advance > 0 && d->font != baseFont) {
@@ -1493,10 +1509,10 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1493 const int glyphFlags = hb_glyph_info_get_glyph_flags(info); 1509 const int glyphFlags = hb_glyph_info_get_glyph_flags(info);
1494 const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; 1510 const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset;
1495 const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; 1511 const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance;
1512 const iChar ch = logicalText[logPos];
1496 iAssert(xAdvance >= 0); 1513 iAssert(xAdvance >= 0);
1497 if (args->wrap->mode == word_WrapTextMode) { 1514 if (args->wrap->mode == word_WrapTextMode) {
1498 /* When word wrapping, only consider certain places breakable. */ 1515 /* When word wrapping, only consider certain places breakable. */
1499 const iChar ch = logicalText[logPos];
1500 if ((ch >= 128 || !ispunct(ch)) && (prevCh == '-' || prevCh == '/')) { 1516 if ((ch >= 128 || !ispunct(ch)) && (prevCh == '-' || prevCh == '/')) {
1501 safeBreakPos = logPos; 1517 safeBreakPos = logPos;
1502 breakAdvance = wrapAdvance; 1518 breakAdvance = wrapAdvance;
@@ -1522,6 +1538,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1522 wrap->hitGlyphNormX_out = (wrap->hitPoint.x - wrapAdvance) / xAdvance; 1538 wrap->hitGlyphNormX_out = (wrap->hitPoint.x - wrapAdvance) / xAdvance;
1523 } 1539 }
1524 } 1540 }
1541 if (ch == '\t') {
1542 wrapAdvance = nextTabStop_Font_(d, wrapAdvance) - xAdvance;
1543 }
1525 /* Out of room? */ 1544 /* Out of room? */
1526 if (wrap->maxWidth > 0 && 1545 if (wrap->maxWidth > 0 &&
1527 wrapAdvance + xOffset + glyph->d[0].x + glyph->rect[0].size.x > 1546 wrapAdvance + xOffset + glyph->d[0].x + glyph->rect[0].size.x >
@@ -1676,6 +1695,23 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1676 const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; 1695 const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance;
1677 const float yAdvance = run->font->yScale * buf->glyphPos[i].y_advance; 1696 const float yAdvance = run->font->yScale * buf->glyphPos[i].y_advance;
1678 const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId); 1697 const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId);
1698 if (logicalText[logPos] == '\t') {
1699#if 0
1700 if (mode & draw_RunMode) {
1701 /* Tab indicator. */
1702 iColor tabColor = get_Color(uiTextAction_ColorId);
1703 SDL_SetRenderDrawColor(activeText_->render, tabColor.r, tabColor.g, tabColor.b, 255);
1704 const int pad = d->height / 6;
1705 SDL_RenderFillRect(activeText_->render, &(SDL_Rect){
1706 orig.x + xCursor,
1707 orig.y + yCursor + d->height / 2 - pad / 2,
1708 pad,
1709 pad
1710 });
1711 }
1712#endif
1713 xCursor = nextTabStop_Font_(d, xCursor) - xAdvance;
1714 }
1679 const float xf = xCursor + xOffset; 1715 const float xf = xCursor + xOffset;
1680 const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0; 1716 const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0;
1681 /* Output position for the glyph. */ 1717 /* Output position for the glyph. */
@@ -1704,20 +1740,22 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1704 } 1740 }
1705 if (~mode & permanentColorFlag_RunMode) { 1741 if (~mode & permanentColorFlag_RunMode) {
1706 const iColor clr = run->fgColor; 1742 const iColor clr = run->fgColor;
1707 SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); 1743 SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b);
1708 if (args->mode & fillBackground_RunMode) { 1744 if (args->mode & fillBackground_RunMode) {
1709 SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0); 1745 SDL_SetRenderDrawColor(activeText_->render, clr.r, clr.g, clr.b, 0);
1710 } 1746 }
1711 } 1747 }
1712 SDL_Rect src; 1748 SDL_Rect src;
1713 memcpy(&src, &glyph->rect[hoff], sizeof(SDL_Rect)); 1749 memcpy(&src, &glyph->rect[hoff], sizeof(SDL_Rect));
1750 dst.x += origin_Paint.x;
1751 dst.y += origin_Paint.y;
1714 if (args->mode & fillBackground_RunMode) { 1752 if (args->mode & fillBackground_RunMode) {
1715 /* Alpha blending looks much better if the RGB components don't change in 1753 /* Alpha blending looks much better if the RGB components don't change in
1716 the partially transparent pixels. */ 1754 the partially transparent pixels. */
1717 /* TODO: Backgrounds of all glyphs should be cleared before drawing anything else. */ 1755 /* TODO: Backgrounds of all glyphs should be cleared before drawing anything else. */
1718 SDL_RenderFillRect(text_.render, &dst); 1756 SDL_RenderFillRect(activeText_->render, &dst);
1719 } 1757 }
1720 SDL_RenderCopy(text_.render, text_.cache, &src, &dst); 1758 SDL_RenderCopy(activeText_->render, activeText_->cache, &src, &dst);
1721#if 0 1759#if 0
1722 /* Show spaces and direction. */ 1760 /* Show spaces and direction. */
1723 if (logicalText[logPos] == 0x20) { 1761 if (logicalText[logPos] == 0x20) {
@@ -1859,7 +1897,7 @@ iTextMetrics measureN_Text(int fontId, const char *text, size_t n) {
1859} 1897}
1860 1898
1861static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text, size_t maxLen) { 1899static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text, size_t maxLen) {
1862 iText * d = &text_; 1900 iText * d = activeText_;
1863 iFont * font = font_Text_(fontId); 1901 iFont * font = font_Text_(fontId);
1864 const iColor clr = get_Color(color & mask_ColorId); 1902 const iColor clr = get_Color(color & mask_ColorId);
1865 SDL_SetTextureColorMod(d->cache, clr.r, clr.g, clr.b); 1903 SDL_SetTextureColorMod(d->cache, clr.r, clr.g, clr.b);
@@ -2053,7 +2091,7 @@ iTextMetrics draw_WrapText(iWrapText *d, int fontId, iInt2 pos, int color) {
2053} 2091}
2054 2092
2055SDL_Texture *glyphCache_Text(void) { 2093SDL_Texture *glyphCache_Text(void) {
2056 return text_.cache; 2094 return activeText_->cache;
2057} 2095}
2058 2096
2059static void freeBitmap_(void *ptr) { 2097static void freeBitmap_(void *ptr) {
@@ -2166,7 +2204,7 @@ iString *renderBlockChars_Text(const iBlock *fontData, int height, enum iTextBlo
2166iDefineTypeConstructionArgs(TextBuf, (iWrapText *wrapText, int font, int color), wrapText, font, color) 2204iDefineTypeConstructionArgs(TextBuf, (iWrapText *wrapText, int font, int color), wrapText, font, color)
2167 2205
2168void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) { 2206void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) {
2169 SDL_Renderer *render = text_.render; 2207 SDL_Renderer *render = activeText_->render;
2170 d->size = measure_WrapText(wrapText, font).bounds.size; 2208 d->size = measure_WrapText(wrapText, font).bounds.size;
2171 SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); 2209 SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0");
2172 if (d->size.x * d->size.y) { 2210 if (d->size.x * d->size.y) {
@@ -2181,14 +2219,17 @@ void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) {
2181 } 2219 }
2182 if (d->texture) { 2220 if (d->texture) {
2183 SDL_Texture *oldTarget = SDL_GetRenderTarget(render); 2221 SDL_Texture *oldTarget = SDL_GetRenderTarget(render);
2222 const iInt2 oldOrigin = origin_Paint;
2223 origin_Paint = zero_I2();
2184 SDL_SetRenderTarget(render, d->texture); 2224 SDL_SetRenderTarget(render, d->texture);
2185 SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE); 2225 SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
2186 SDL_SetRenderDrawColor(render, 255, 255, 255, 0); 2226 SDL_SetRenderDrawColor(render, 255, 255, 255, 0);
2187 SDL_RenderClear(render); 2227 SDL_RenderClear(render);
2188 SDL_SetTextureBlendMode(text_.cache, SDL_BLENDMODE_NONE); /* blended when TextBuf is drawn */ 2228 SDL_SetTextureBlendMode(activeText_->cache, SDL_BLENDMODE_NONE); /* blended when TextBuf is drawn */
2189 draw_WrapText(wrapText, font, zero_I2(), color | fillBackground_ColorId); 2229 draw_WrapText(wrapText, font, zero_I2(), color | fillBackground_ColorId);
2190 SDL_SetTextureBlendMode(text_.cache, SDL_BLENDMODE_BLEND); 2230 SDL_SetTextureBlendMode(activeText_->cache, SDL_BLENDMODE_BLEND);
2191 SDL_SetRenderTarget(render, oldTarget); 2231 SDL_SetRenderTarget(render, oldTarget);
2232 origin_Paint = oldOrigin;
2192 SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND); 2233 SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND);
2193 } 2234 }
2194} 2235}
@@ -2202,9 +2243,10 @@ iTextBuf *newRange_TextBuf(int font, int color, iRangecc text) {
2202} 2243}
2203 2244
2204void draw_TextBuf(const iTextBuf *d, iInt2 pos, int color) { 2245void draw_TextBuf(const iTextBuf *d, iInt2 pos, int color) {
2246 addv_I2(&pos, origin_Paint);
2205 const iColor clr = get_Color(color); 2247 const iColor clr = get_Color(color);
2206 SDL_SetTextureColorMod(d->texture, clr.r, clr.g, clr.b); 2248 SDL_SetTextureColorMod(d->texture, clr.r, clr.g, clr.b);
2207 SDL_RenderCopy(text_.render, 2249 SDL_RenderCopy(activeText_->render,
2208 d->texture, 2250 d->texture,
2209 &(SDL_Rect){ 0, 0, d->size.x, d->size.y }, 2251 &(SDL_Rect){ 0, 0, d->size.x, d->size.y },
2210 &(SDL_Rect){ pos.x, pos.y, d->size.x, d->size.y }); 2252 &(SDL_Rect){ pos.x, pos.y, d->size.x, d->size.y });
diff --git a/src/ui/text.h b/src/ui/text.h
index ac6cc1c1..1da43818 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -139,15 +139,20 @@ enum iTextFont {
139 139
140extern int gap_Text; /* affected by content font size */ 140extern int gap_Text; /* affected by content font size */
141 141
142void init_Text (SDL_Renderer *); 142iDeclareType(Text)
143void deinit_Text (void); 143iDeclareTypeConstructionArgs(Text, SDL_Renderer *)
144
145void init_Text (iText *, SDL_Renderer *);
146void deinit_Text (iText *);
147
148void setCurrent_Text (iText *);
144 149
145void loadUserFonts_Text (void); /* based on Prefs */ 150void loadUserFonts_Text (void); /* based on Prefs */
146 151
147void setContentFont_Text (enum iTextFont font); 152void setContentFont_Text (iText *, enum iTextFont font);
148void setHeadingFont_Text (enum iTextFont font); 153void setHeadingFont_Text (iText *, enum iTextFont font);
149void setContentFontSize_Text (float fontSizeFactor); /* affects all except `default*` fonts */ 154void setContentFontSize_Text (iText *, float fontSizeFactor); /* affects all except `default*` fonts */
150void resetFonts_Text (void); 155void resetFonts_Text (iText *);
151 156
152int lineHeight_Text (int fontId); 157int lineHeight_Text (int fontId);
153iRect visualBounds_Text (int fontId, iRangecc text); 158iRect visualBounds_Text (int fontId, iRangecc text);
diff --git a/src/ui/text_simple.c b/src/ui/text_simple.c
index e88b09a8..8b1de64a 100644
--- a/src/ui/text_simple.c
+++ b/src/ui/text_simple.c
@@ -92,7 +92,7 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
92 } 92 }
93 if (args->mode & fillBackground_RunMode) { 93 if (args->mode & fillBackground_RunMode) {
94 const iColor initial = get_Color(args->color); 94 const iColor initial = get_Color(args->color);
95 SDL_SetRenderDrawColor(text_.render, initial.r, initial.g, initial.b, 0); 95 SDL_SetRenderDrawColor(activeText_->render, initial.r, initial.g, initial.b, 0);
96 } 96 }
97 /* Text rendering is not very straightforward! Let's dive in... */ 97 /* Text rendering is not very straightforward! Let's dive in... */
98 iChar prevCh = 0; 98 iChar prevCh = 0;
@@ -114,14 +114,14 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
114 chPos++; 114 chPos++;
115 iRegExpMatch m; 115 iRegExpMatch m;
116 init_RegExpMatch(&m); 116 init_RegExpMatch(&m);
117 if (match_RegExp(text_.ansiEscape, chPos, args->text.end - chPos, &m)) { 117 if (match_RegExp(activeText_->ansiEscape, chPos, args->text.end - chPos, &m)) {
118 if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { 118 if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) {
119 /* Change the color. */ 119 /* Change the color. */
120 const iColor clr = 120 const iColor clr =
121 ansiForeground_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId); 121 ansiForeground_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId);
122 SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); 122 SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b);
123 if (args->mode & fillBackground_RunMode) { 123 if (args->mode & fillBackground_RunMode) {
124 SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0); 124 SDL_SetRenderDrawColor(activeText_->render, clr.r, clr.g, clr.b, 0);
125 } 125 }
126 } 126 }
127 chPos = end_RegExpMatch(&m); 127 chPos = end_RegExpMatch(&m);
@@ -205,9 +205,9 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
205 } 205 }
206 if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { 206 if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) {
207 const iColor clr = get_Color(colorNum); 207 const iColor clr = get_Color(colorNum);
208 SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); 208 SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b);
209 if (args->mode & fillBackground_RunMode) { 209 if (args->mode & fillBackground_RunMode) {
210 SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0); 210 SDL_SetRenderDrawColor(activeText_->render, clr.r, clr.g, clr.b, 0);
211 } 211 }
212 } 212 }
213 prevCh = 0; 213 prevCh = 0;
@@ -306,12 +306,14 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
306 src.y += over; 306 src.y += over;
307 src.h -= over; 307 src.h -= over;
308 } 308 }
309 dst.x += origin_Paint.x;
310 dst.y += origin_Paint.y;
309 if (args->mode & fillBackground_RunMode) { 311 if (args->mode & fillBackground_RunMode) {
310 /* Alpha blending looks much better if the RGB components don't change in 312 /* Alpha blending looks much better if the RGB components don't change in
311 the partially transparent pixels. */ 313 the partially transparent pixels. */
312 SDL_RenderFillRect(text_.render, &dst); 314 SDL_RenderFillRect(activeText_->render, &dst);
313 } 315 }
314 SDL_RenderCopy(text_.render, text_.cache, &src, &dst); 316 SDL_RenderCopy(activeText_->render, activeText_->cache, &src, &dst);
315 } 317 }
316 xpos += advance; 318 xpos += advance;
317 if (!isSpace_Char(ch)) { 319 if (!isSpace_Char(ch)) {
diff --git a/src/ui/touch.c b/src/ui/touch.c
index dac1152e..613f2c0d 100644
--- a/src/ui/touch.c
+++ b/src/ui/touch.c
@@ -59,7 +59,6 @@ enum iTouchAxis {
59struct Impl_Touch { 59struct Impl_Touch {
60 SDL_FingerID id; 60 SDL_FingerID id;
61 iWidget *affinity; /* widget on which the touch started */ 61 iWidget *affinity; /* widget on which the touch started */
62// iWidget *edgeDragging;
63 iBool hasMoved; 62 iBool hasMoved;
64 iBool isTapBegun; 63 iBool isTapBegun;
65 iBool isLeftDown; 64 iBool isLeftDown;
@@ -254,6 +253,8 @@ static iFloat3 gestureVector_Touch_(const iTouch *d) {
254} 253}
255 254
256static void update_TouchState_(void *ptr) { 255static void update_TouchState_(void *ptr) {
256 iWindow *win = get_Window();
257 const iWidget *oldHover = win->hover;
257 iTouchState *d = ptr; 258 iTouchState *d = ptr;
258 /* Check for long presses to simulate right clicks. */ 259 /* Check for long presses to simulate right clicks. */
259 const uint32_t nowTime = SDL_GetTicks(); 260 const uint32_t nowTime = SDL_GetTicks();
@@ -291,8 +292,10 @@ static void update_TouchState_(void *ptr) {
291 } 292 }
292 if (elapsed > 50 && !touch->isTapBegun) { 293 if (elapsed > 50 && !touch->isTapBegun) {
293 /* Looks like a possible tap. */ 294 /* Looks like a possible tap. */
295 touchState_()->currentTouchPos = initF3_I2(touch->pos[0]);
294 dispatchNotification_Touch_(touch, widgetTapBegins_UserEventCode); 296 dispatchNotification_Touch_(touch, widgetTapBegins_UserEventCode);
295 dispatchMotion_Touch_(touch->pos[0], 0); 297 dispatchMotion_Touch_(touch->pos[0], 0);
298 refresh_Widget(touch->affinity);
296 touch->isTapBegun = iTrue; 299 touch->isTapBegun = iTrue;
297 } 300 }
298 if (!touch->isTapAndHold && nowTime - touch->startTime >= longPressSpanMs_ && 301 if (!touch->isTapAndHold && nowTime - touch->startTime >= longPressSpanMs_ &&
@@ -347,6 +350,7 @@ static void update_TouchState_(void *ptr) {
347 setCurrent_Root(mom->affinity->root); 350 setCurrent_Root(mom->affinity->root);
348 dispatchEvent_Widget(mom->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){ 351 dispatchEvent_Widget(mom->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){
349 .type = SDL_MOUSEWHEEL, 352 .type = SDL_MOUSEWHEEL,
353 .which = SDL_TOUCH_MOUSEID,
350 .timestamp = nowTime, 354 .timestamp = nowTime,
351 .x = pixels.x, 355 .x = pixels.x,
352 .y = pixels.y, 356 .y = pixels.y,
@@ -363,6 +367,10 @@ static void update_TouchState_(void *ptr) {
363 if (!isEmpty_Array(d->touches) || !isEmpty_Array(d->moms)) { 367 if (!isEmpty_Array(d->touches) || !isEmpty_Array(d->moms)) {
364 addTickerRoot_App(update_TouchState_, NULL, ptr); 368 addTickerRoot_App(update_TouchState_, NULL, ptr);
365 } 369 }
370 if (oldHover != win->hover) {
371 refresh_Widget(oldHover);
372 refresh_Widget(win->hover);
373 }
366} 374}
367 375
368#if 0 376#if 0
@@ -464,13 +472,9 @@ iBool processEvent_Touch(const SDL_Event *ev) {
464 } 472 }
465 iTouchState *d = touchState_(); 473 iTouchState *d = touchState_();
466 iWindow *window = get_Window(); 474 iWindow *window = get_Window();
467 if (!isFinished_Anim(&window->rootOffset)) {
468 return iFalse;
469 }
470 const iInt2 rootSize = size_Window(window); 475 const iInt2 rootSize = size_Window(window);
471 const SDL_TouchFingerEvent *fing = &ev->tfinger; 476 const SDL_TouchFingerEvent *fing = &ev->tfinger;
472 const iFloat3 pos = add_F3(init_F3(fing->x * rootSize.x, fing->y * rootSize.y, 0), /* pixels */ 477 const iFloat3 pos = init_F3(fing->x * rootSize.x, fing->y * rootSize.y, 0); /* pixels */
473 init_F3(0, -value_Anim(&window->rootOffset), 0));
474 const uint32_t nowTime = SDL_GetTicks(); 478 const uint32_t nowTime = SDL_GetTicks();
475 if (ev->type == SDL_FINGERDOWN) { 479 if (ev->type == SDL_FINGERDOWN) {
476 /* Register the new touch. */ 480 /* Register the new touch. */
@@ -614,15 +618,16 @@ iBool processEvent_Touch(const SDL_Event *ev) {
614// pixels.y, y_F3(amount), y_F3(touch->accum), 618// pixels.y, y_F3(amount), y_F3(touch->accum),
615// touch->edge); 619// touch->edge);
616 if (pixels.x || pixels.y) { 620 if (pixels.x || pixels.y) {
617 setFocus_Widget(NULL); 621 //setFocus_Widget(NULL);
618 dispatchMotion_Touch_(touch->pos[0], 0); 622 dispatchMotion_Touch_(touch->startPos /*pos[0]*/, 0);
619 setCurrent_Root(touch->affinity->root); 623 setCurrent_Root(touch->affinity->root);
620 dispatchEvent_Widget(touch->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){ 624 dispatchEvent_Widget(touch->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){
621 .type = SDL_MOUSEWHEEL, 625 .type = SDL_MOUSEWHEEL,
626 .which = SDL_TOUCH_MOUSEID,
622 .timestamp = SDL_GetTicks(), 627 .timestamp = SDL_GetTicks(),
623 .x = pixels.x, 628 .x = pixels.x,
624 .y = pixels.y, 629 .y = pixels.y,
625 .direction = perPixel_MouseWheelFlag 630 .direction = perPixel_MouseWheelFlag,
626 }); 631 });
627 /* TODO: Keep increasing movement if the direction is the same. */ 632 /* TODO: Keep increasing movement if the direction is the same. */
628 clearWidgetMomentum_TouchState_(d, touch->affinity); 633 clearWidgetMomentum_TouchState_(d, touch->affinity);
@@ -715,7 +720,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
715 iMomentum mom = { 720 iMomentum mom = {
716 .affinity = touch->affinity, 721 .affinity = touch->affinity,
717 .releaseTime = nowTime, 722 .releaseTime = nowTime,
718 .pos = touch->pos[0], 723 .pos = touch->startPos, // pos[0],
719 .velocity = velocity 724 .velocity = velocity
720 }; 725 };
721 if (isEmpty_Array(d->moms)) { 726 if (isEmpty_Array(d->moms)) {
diff --git a/src/ui/translation.c b/src/ui/translation.c
index 3ffa961b..b86e6e52 100644
--- a/src/ui/translation.c
+++ b/src/ui/translation.c
@@ -136,7 +136,8 @@ static void draw_TranslationProgressWidget_(const iTranslationProgressWidget *d)
136 get_Color(palette[palCur]), get_Color(palette[palNext]), palPos - (int) palPos); 136 get_Color(palette[palCur]), get_Color(palette[palNext]), palPos - (int) palPos);
137 SDL_SetRenderDrawColor(renderer_Window(get_Window()), back.r, back.g, back.b, p.alpha); 137 SDL_SetRenderDrawColor(renderer_Window(get_Window()), back.r, back.g, back.b, p.alpha);
138 SDL_RenderFillRect(renderer_Window(get_Window()), 138 SDL_RenderFillRect(renderer_Window(get_Window()),
139 &(SDL_Rect){ pos.x, pos.y, spr->size.x, spr->size.y }); 139 &(SDL_Rect){ pos.x + origin_Paint.x, pos.y + origin_Paint.y,
140 spr->size.x, spr->size.y });
140 if (fg >= 0) { 141 if (fg >= 0) {
141 setOpacity_Text(opacity * 2); 142 setOpacity_Text(opacity * 2);
142 drawRange_Text(d->font, addX_I2(pos, spr->xoff), fg, range_String(&spr->text)); 143 drawRange_Text(d->font, addX_I2(pos, spr->xoff), fg, range_String(&spr->text));
@@ -424,19 +425,18 @@ static iBool processResult_Translation_(iTranslation *d) {
424} 425}
425 426
426static iLabelWidget *acceptButton_Translation_(const iTranslation *d) { 427static iLabelWidget *acceptButton_Translation_(const iTranslation *d) {
427 iWidget *buttonParent = findChild_Widget(d->dlg, "dialogbuttons"); 428 return dialogAcceptButton_Widget(d->dlg);
428// if (!buttonParent) {
429// buttonParent = findChild_Widget(d->dlg, "panel.back");
430// }
431 return (iLabelWidget *) lastChild_Widget(buttonParent);
432} 429}
433 430
434iBool handleCommand_Translation(iTranslation *d, const char *cmd) { 431iBool handleCommand_Translation(iTranslation *d, const char *cmd) {
435 iWidget *w = as_Widget(d->doc); 432 iWidget *w = as_Widget(d->doc);
436 if (equalWidget_Command(cmd, w, "translation.submit")) { 433 if (equalWidget_Command(cmd, w, "translation.submit")) {
437 if (status_TlsRequest(d->request) == initialized_TlsRequestStatus) { 434 if (status_TlsRequest(d->request) == initialized_TlsRequestStatus) {
438 iWidget *langs = findChild_Widget(d->dlg, "xlt.langs"); 435 iWidget *langs = findChild_Widget(d->dlg, "xlt.langs");
439 setFlags_Widget(langs, hidden_WidgetFlag, iTrue); 436// setFlags_Widget(langs, hidden_WidgetFlag, iTrue);
437 setFlags_Widget(findChild_Widget(d->dlg, "xlt.from"), hidden_WidgetFlag, iTrue);
438 setFlags_Widget(findChild_Widget(d->dlg, "xlt.to"), hidden_WidgetFlag, iTrue);
439 if (!langs) langs = d->dlg;
440 iLabelWidget *acceptButton = acceptButton_Translation_(d); 440 iLabelWidget *acceptButton = acceptButton_Translation_(d);
441 updateTextCStr_LabelWidget(acceptButton, "00:00"); 441 updateTextCStr_LabelWidget(acceptButton, "00:00");
442 setFlags_Widget(as_Widget(acceptButton), disabled_WidgetFlag, iTrue); 442 setFlags_Widget(as_Widget(acceptButton), disabled_WidgetFlag, iTrue);
diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c
index 5e1ee493..ba7545fd 100644
--- a/src/ui/uploadwidget.c
+++ b/src/ui/uploadwidget.c
@@ -29,12 +29,25 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
29#include "command.h" 29#include "command.h"
30#include "gmrequest.h" 30#include "gmrequest.h"
31#include "sitespec.h" 31#include "sitespec.h"
32#include "window.h"
33#include "gmcerts.h"
32#include "app.h" 34#include "app.h"
33 35
36#if defined (iPlatformAppleMobile)
37# include "ios.h"
38#endif
39
34#include <the_Foundation/file.h> 40#include <the_Foundation/file.h>
35#include <the_Foundation/fileinfo.h> 41#include <the_Foundation/fileinfo.h>
42#include <the_Foundation/path.h>
36 43
37iDefineObjectConstruction(UploadWidget) 44iDefineObjectConstruction(UploadWidget)
45
46enum iUploadIdentity {
47 none_UploadIdentity,
48 defaultForUrl_UploadIdentity,
49 dropdown_UploadIdentity,
50};
38 51
39struct Impl_UploadWidget { 52struct Impl_UploadWidget {
40 iWidget widget; 53 iWidget widget;
@@ -51,9 +64,21 @@ struct Impl_UploadWidget {
51 iLabelWidget * counter; 64 iLabelWidget * counter;
52 iString filePath; 65 iString filePath;
53 size_t fileSize; 66 size_t fileSize;
67 enum iUploadIdentity idMode;
68 iBlock idFingerprint;
54 iAtomicInt isRequestUpdated; 69 iAtomicInt isRequestUpdated;
55}; 70};
56 71
72static void releaseFile_UploadWidget_(iUploadWidget *d) {
73#if defined (iPlatformAppleMobile)
74 if (!isEmpty_String(&d->filePath)) {
75 /* Delete the temporary file that was copied for uploading. */
76 remove(cstr_String(&d->filePath));
77 }
78#endif
79 clear_String(&d->filePath);
80}
81
57static void updateProgress_UploadWidget_(iGmRequest *request, size_t current, size_t total) { 82static void updateProgress_UploadWidget_(iGmRequest *request, size_t current, size_t total) {
58 iUploadWidget *d = userData_Object(request); 83 iUploadWidget *d = userData_Object(request);
59 postCommand_Widget(d, 84 postCommand_Widget(d,
@@ -67,116 +92,202 @@ static void updateInputMaxHeight_UploadWidget_(iUploadWidget *d) {
67 iWidget *w = as_Widget(d); 92 iWidget *w = as_Widget(d);
68 /* Calculate how many lines fits vertically in the view. */ 93 /* Calculate how many lines fits vertically in the view. */
69 const iInt2 inputPos = topLeft_Rect(bounds_Widget(as_Widget(d->input))); 94 const iInt2 inputPos = topLeft_Rect(bounds_Widget(as_Widget(d->input)));
70 const int footerHeight = height_Widget(d->token) + 95 const int footerHeight = isUsingPanelLayout_Mobile() ? 0 :
71 height_Widget(findChild_Widget(w, "dialogbuttons")) + 96 (height_Widget(d->token) +
72 6 * gap_UI; 97 height_Widget(findChild_Widget(w, "dialogbuttons")) +
73 const int avail = bottom_Rect(safeRect_Root(w->root)) - footerHeight; 98 12 * gap_UI);
99 const int avail = bottom_Rect(safeRect_Root(w->root)) - footerHeight -
100 get_MainWindow()->keyboardHeight;
74 setLineLimits_InputWidget(d->input, 101 setLineLimits_InputWidget(d->input,
75 minLines_InputWidget(d->input), 102 minLines_InputWidget(d->input),
76 iMaxi(minLines_InputWidget(d->input), 103 iMaxi(minLines_InputWidget(d->input),
77 (avail - inputPos.y) / lineHeight_Text(monospace_FontId))); 104 (avail - inputPos.y) / lineHeight_Text(font_InputWidget(d->input))));
105}
106
107static const iArray *makeIdentityItems_UploadWidget_(const iUploadWidget *d) {
108 iArray *items = collectNew_Array(sizeof(iMenuItem));
109 const iGmIdentity *urlId = identityForUrl_GmCerts(certs_App(), &d->url);
110 pushBack_Array(items,
111 &(iMenuItem){ format_CStr("${dlg.upload.id.default} (%s)",
112 urlId ? cstr_String(name_GmIdentity(urlId))
113 : "${dlg.upload.id.none}"),
114 0, 0, "upload.setid arg:1" });
115 pushBack_Array(items, &(iMenuItem){ "${dlg.upload.id.none}", 0, 0, "upload.setid arg:0" });
116 pushBack_Array(items, &(iMenuItem){ "---" });
117 iConstForEach(PtrArray, i, listIdentities_GmCerts(certs_App(), NULL, NULL)) {
118 const iGmIdentity *id = i.ptr;
119 pushBack_Array(
120 items,
121 &(iMenuItem){ cstr_String(name_GmIdentity(id)), 0, 0,
122 format_CStr("upload.setid fp:%s",
123 cstrCollect_String(hexEncode_Block(&id->fingerprint))) });
124 }
125 pushBack_Array(items, &(iMenuItem){ NULL });
126 return items;
78} 127}
79 128
80void init_UploadWidget(iUploadWidget *d) { 129void init_UploadWidget(iUploadWidget *d) {
81 iWidget *w = as_Widget(d); 130 iWidget *w = as_Widget(d);
82 init_Widget(w); 131 init_Widget(w);
83 setId_Widget(w, "upload"); 132 setId_Widget(w, "upload");
84 useSheetStyle_Widget(w);
85 init_String(&d->originalUrl); 133 init_String(&d->originalUrl);
86 init_String(&d->url); 134 init_String(&d->url);
87 d->viewer = NULL; 135 d->viewer = NULL;
88 d->request = NULL; 136 d->request = NULL;
89 init_String(&d->filePath); 137 init_String(&d->filePath);
90 d->fileSize = 0; 138 d->fileSize = 0;
91 addChildFlags_Widget(w, 139 d->idMode = defaultForUrl_UploadIdentity;
92 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.upload}", NULL)), 140 init_Block(&d->idFingerprint, 0);
93 frameless_WidgetFlag); 141 const iMenuItem actions[] = {
94 d->info = addChildFlags_Widget(w, iClob(new_LabelWidget("", NULL)), 142 { "${upload.port}", 0, 0, "upload.setport" },
95 frameless_WidgetFlag | resizeToParentWidth_WidgetFlag | 143 { "---" },
96 fixedHeight_WidgetFlag); 144 { "${close}", SDLK_ESCAPE, 0, "upload.cancel" },
97 setWrap_LabelWidget(d->info, iTrue); 145 { uiTextAction_ColorEscape "${dlg.upload.send}", SDLK_RETURN, KMOD_PRIMARY, "upload.accept" }
98 /* Tabs for input data. */ 146 };
99 iWidget *tabs = makeTabs_Widget(w); 147 if (isUsingPanelLayout_Mobile()) {
100 /* Make the tabs support vertical expansion based on content. */ { 148 const iMenuItem textItems[] = {
101 setFlags_Widget(tabs, resizeHeightOfChildren_WidgetFlag, iFalse); 149 { "title id:heading.upload.text" },
102 setFlags_Widget(tabs, arrangeHeight_WidgetFlag, iTrue); 150 { "input id:upload.text noheading:1" },
103 iWidget *tabPages = findChild_Widget(tabs, "tabs.pages"); 151 { NULL }
104 setFlags_Widget(tabPages, resizeHeightOfChildren_WidgetFlag, iFalse); 152 };
105 setFlags_Widget(tabPages, arrangeHeight_WidgetFlag, iTrue); 153 const iMenuItem fileItems[] = {
106 } 154 { "title id:heading.upload.file" },
107 iWidget *headings, *values; 155 { "button text:" uiTextAction_ColorEscape "${dlg.upload.pickfile}", 0, 0, "upload.pickfile" },
108 setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); 156 { "heading id:upload.file.name" },
109 setId_Widget(tabs, "upload.tabs"); 157 { "label id:upload.filepathlabel text:\u2014" },
110// const int bigGap = lineHeight_Text(uiLabel_FontId) * 3 / 4; 158 { "heading id:upload.file.size" },
111 /* Text input. */ { 159 { "label id:upload.filesizelabel text:\u2014" },
112 //appendTwoColumnTabPage_Widget(tabs, "${heading.upload.text}", '1', &headings, &values); 160 { "padding" },
113 iWidget *page = new_Widget(); 161 { "input id:upload.mime" },
114 setFlags_Widget(page, arrangeSize_WidgetFlag, iTrue); 162 { "label id:upload.counter text:" },
115 d->input = new_InputWidget(0); 163 { NULL }
116 setId_Widget(as_Widget(d->input), "upload.text"); 164 };
117 setFont_InputWidget(d->input, monospace_FontId); 165 initPanels_Mobile(w, NULL, (iMenuItem[]){
118 setLineLimits_InputWidget(d->input, 7, 20); 166 { "title id:heading.upload" },
119 setUseReturnKeyBehavior_InputWidget(d->input, iFalse); /* traditional text editor */ 167 { "label id:upload.info" },
120 setHint_InputWidget(d->input, "${hint.upload.text}"); 168 { "panel id:dlg.upload.text icon:0x1f5b9", 0, 0, (const void *) textItems },
121 setFixedSize_Widget(as_Widget(d->input), init_I2(120 * gap_UI, -1)); 169 { "panel id:dlg.upload.file icon:0x1f4c1", 0, 0, (const void *) fileItems },
122 addChild_Widget(page, iClob(d->input)); 170 { "padding" },
123 appendFramelessTabPage_Widget(tabs, iClob(page), "${heading.upload.text}", '1', 0); 171 { "dropdown id:upload.id icon:0x1f464", 0, 0, constData_Array(makeIdentityItems_UploadWidget_(d)) },
124 } 172 { "input id:upload.token hint:hint.upload.token icon:0x1f511" },
125 /* File content. */ { 173 { NULL }
126 appendTwoColumnTabPage_Widget(tabs, "${heading.upload.file}", '2', &headings, &values); 174 }, actions, iElemCount(actions));
127// iWidget *pad = addChild_Widget(headings, iClob(makePadding_Widget(0))); 175 d->info = findChild_Widget(w, "upload.info");
128// iWidget *hint = addChild_Widget(values, iClob(new_LabelWidget("${upload.file.drophint}", NULL))); 176 d->input = findChild_Widget(w, "upload.text");
129// pad->sizeRef = hint; 177 d->filePathLabel = findChild_Widget(w, "upload.filepathlabel");
130 addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.name}", NULL)), frameless_WidgetFlag); 178 d->fileSizeLabel = findChild_Widget(w, "upload.filesizelabel");
131 d->filePathLabel = addChildFlags_Widget(values, iClob(new_LabelWidget(uiTextAction_ColorEscape "${upload.file.drophere}", NULL)), frameless_WidgetFlag); 179 d->mime = findChild_Widget(w, "upload.mime");
132 addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.size}", NULL)), frameless_WidgetFlag); 180 d->token = findChild_Widget(w, "upload.token");
133 d->fileSizeLabel = addChildFlags_Widget(values, iClob(new_LabelWidget("\u2014", NULL)), frameless_WidgetFlag); 181 d->counter = findChild_Widget(w, "upload.counter");
134 d->mime = new_InputWidget(0);
135 setFixedSize_Widget(as_Widget(d->mime), init_I2(70 * gap_UI, -1));
136 addTwoColumnDialogInputField_Widget(headings, values, "${upload.mime}", "upload.mime", iClob(d->mime));
137 }
138 /* Token. */ {
139 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
140 iWidget *page = makeTwoColumns_Widget(&headings, &values);
141 d->token = addTwoColumnDialogInputField_Widget(
142 headings, values, "${upload.token}", "upload.token", iClob(new_InputWidget(0)));
143 setHint_InputWidget(d->token, "${hint.upload.token}");
144 setFixedSize_Widget(as_Widget(d->token), init_I2(50 * gap_UI, -1));
145 addChild_Widget(w, iClob(page));
146 } 182 }
147 /* Buttons. */ { 183 else {
148 addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); 184 useSheetStyle_Widget(w);
149 iWidget *buttons = 185 setFlags_Widget(w, overflowScrollable_WidgetFlag, iFalse);
150 makeDialogButtons_Widget((iMenuItem[]){ { "${upload.port}", 0, 0, "upload.setport" }, 186 addChildFlags_Widget(w,
151 { "---", 0, 0, NULL }, 187 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.upload}", NULL)),
152 { "${close}", SDLK_ESCAPE, 0, "upload.cancel" }, 188 frameless_WidgetFlag);
153 { uiTextAction_ColorEscape "${dlg.upload.send}", 189 d->info = addChildFlags_Widget(w, iClob(new_LabelWidget("", NULL)),
154 SDLK_RETURN, 190 frameless_WidgetFlag | resizeToParentWidth_WidgetFlag |
155 KMOD_PRIMARY, 191 fixedHeight_WidgetFlag);
156 "upload.accept" } }, 192 setWrap_LabelWidget(d->info, iTrue);
157 4); 193 /* Tabs for input data. */
158 setId_Widget(insertChildAfterFlags_Widget(buttons, 194 iWidget *tabs = makeTabs_Widget(w);
159 iClob(d->counter = new_LabelWidget("", NULL)), 195 /* Make the tabs support vertical expansion based on content. */ {
160 0, frameless_WidgetFlag), 196 setFlags_Widget(tabs, resizeHeightOfChildren_WidgetFlag, iFalse);
161 "upload.counter"); 197 setFlags_Widget(tabs, arrangeHeight_WidgetFlag, iTrue);
162 addChild_Widget(w, iClob(buttons)); 198 iWidget *tabPages = findChild_Widget(tabs, "tabs.pages");
199 setFlags_Widget(tabPages, resizeHeightOfChildren_WidgetFlag, iFalse);
200 setFlags_Widget(tabPages, arrangeHeight_WidgetFlag, iTrue);
201 }
202 iWidget *headings, *values;
203 setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId);
204 setId_Widget(tabs, "upload.tabs");
205 /* Text input. */ {
206 iWidget *page = new_Widget();
207 setFlags_Widget(page, arrangeSize_WidgetFlag, iTrue);
208 d->input = new_InputWidget(0);
209 setId_Widget(as_Widget(d->input), "upload.text");
210 setFixedSize_Widget(as_Widget(d->input), init_I2(120 * gap_UI, -1));
211 addChild_Widget(page, iClob(d->input));
212 appendFramelessTabPage_Widget(tabs, iClob(page), "${heading.upload.text}", '1', 0);
213 }
214 /* File content. */ {
215 appendTwoColumnTabPage_Widget(tabs, "${heading.upload.file}", '2', &headings, &values);
216 addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.name}", NULL)), frameless_WidgetFlag);
217 d->filePathLabel = addChildFlags_Widget(values, iClob(new_LabelWidget(uiTextAction_ColorEscape "${upload.file.drophere}", NULL)), frameless_WidgetFlag);
218 addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.size}", NULL)), frameless_WidgetFlag);
219 d->fileSizeLabel = addChildFlags_Widget(values, iClob(new_LabelWidget("\u2014", NULL)), frameless_WidgetFlag);
220 d->mime = new_InputWidget(0);
221 setFixedSize_Widget(as_Widget(d->mime), init_I2(70 * gap_UI, -1));
222 addTwoColumnDialogInputField_Widget(headings, values, "${upload.mime}", "upload.mime", iClob(d->mime));
223 }
224 /* Identity and Token. */ {
225 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
226 iWidget *page = makeTwoColumns_Widget(&headings, &values);
227 /* Token. */
228 d->token = addTwoColumnDialogInputField_Widget(
229 headings, values, "${upload.token}", "upload.token", iClob(new_InputWidget(0)));
230 setHint_InputWidget(d->token, "${hint.upload.token}");
231 setFixedSize_Widget(as_Widget(d->token), init_I2(50 * gap_UI, -1));
232 /* Identity. */
233 const iArray * identItems = makeIdentityItems_UploadWidget_(d);
234 const iMenuItem *items = constData_Array(identItems);
235 const size_t numItems = size_Array(identItems);
236 iLabelWidget * ident = makeMenuButton_LabelWidget("${upload.id}", items, numItems);
237 setTextCStr_LabelWidget(ident, items[findWidestLabel_MenuItem(items, numItems)].label);
238 addChild_Widget(headings, iClob(makeHeading_Widget("${upload.id}")));
239 setId_Widget(addChildFlags_Widget(values, iClob(ident), alignLeft_WidgetFlag), "upload.id");
240 addChild_Widget(w, iClob(page));
241 }
242 /* Buttons. */ {
243 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
244 iWidget *buttons = makeDialogButtons_Widget(actions, iElemCount(actions));
245 setId_Widget(insertChildAfterFlags_Widget(buttons,
246 iClob(d->counter = new_LabelWidget("", NULL)),
247 0,
248 frameless_WidgetFlag),
249 "upload.counter");
250 addChild_Widget(w, iClob(buttons));
251 }
252 resizeToLargestPage_Widget(tabs);
253 arrange_Widget(w);
254 setFixedSize_Widget(as_Widget(d->token), init_I2(width_Widget(tabs) - left_Rect(parent_Widget(d->token)->rect), -1));
255 setFlags_Widget(as_Widget(d->token), expand_WidgetFlag, iTrue);
256 setFocus_Widget(as_Widget(d->input));
163 } 257 }
164 resizeToLargestPage_Widget(tabs); 258 setFont_InputWidget(d->input, iosevka_FontId);
165 arrange_Widget(w); 259 setUseReturnKeyBehavior_InputWidget(d->input, iFalse); /* traditional text editor */
166 setFixedSize_Widget(as_Widget(d->token), init_I2(width_Widget(tabs) - left_Rect(parent_Widget(d->token)->rect), -1)); 260 setLineLimits_InputWidget(d->input, 7, 20);
167 setFlags_Widget(as_Widget(d->token), expand_WidgetFlag, iTrue); 261 setHint_InputWidget(d->input, "${hint.upload.text}");
168 setFocus_Widget(as_Widget(d->input));
169 setBackupFileName_InputWidget(d->input, "uploadbackup.txt"); 262 setBackupFileName_InputWidget(d->input, "uploadbackup.txt");
170 updateInputMaxHeight_UploadWidget_(d); 263 updateInputMaxHeight_UploadWidget_(d);
171} 264}
172 265
173void deinit_UploadWidget(iUploadWidget *d) { 266void deinit_UploadWidget(iUploadWidget *d) {
267 releaseFile_UploadWidget_(d);
268 deinit_Block(&d->idFingerprint);
174 deinit_String(&d->filePath); 269 deinit_String(&d->filePath);
175 deinit_String(&d->url); 270 deinit_String(&d->url);
176 deinit_String(&d->originalUrl); 271 deinit_String(&d->originalUrl);
177 iRelease(d->request); 272 iRelease(d->request);
178} 273}
179 274
275static void remakeIdentityItems_UploadWidget_(iUploadWidget *d) {
276 iWidget *dropMenu = findChild_Widget(findChild_Widget(as_Widget(d), "upload.id"), "menu");
277 releaseChildren_Widget(dropMenu);
278 const iArray *items = makeIdentityItems_UploadWidget_(d);
279 makeMenuItems_Widget(dropMenu, constData_Array(items), size_Array(items));
280}
281
282static void updateIdentityDropdown_UploadWidget_(iUploadWidget *d) {
283 updateDropdownSelection_LabelWidget(
284 findChild_Widget(as_Widget(d), "upload.id"),
285 d->idMode == none_UploadIdentity ? " arg:0"
286 : d->idMode == defaultForUrl_UploadIdentity
287 ? " arg:1"
288 : format_CStr(" fp:%s", cstrCollect_String(hexEncode_Block(&d->idFingerprint))));
289}
290
180static uint16_t titanPortForUrl_(const iString *url) { 291static uint16_t titanPortForUrl_(const iString *url) {
181 uint16_t port = 0; 292 uint16_t port = 0;
182 const iString *root = collectNewRange_String(urlRoot_String(url)); 293 const iString *root = collectNewRange_String(urlRoot_String(url));
@@ -201,10 +312,13 @@ static void setUrlPort_UploadWidget_(iUploadWidget *d, const iString *url, uint1
201 appendFormat_String(&d->url, ":%u", overridePort ? overridePort : titanPortForUrl_(url)); 312 appendFormat_String(&d->url, ":%u", overridePort ? overridePort : titanPortForUrl_(url));
202 appendRange_String(&d->url, (iRangecc){ parts.path.start, constEnd_String(url) }); 313 appendRange_String(&d->url, (iRangecc){ parts.path.start, constEnd_String(url) });
203 setText_LabelWidget(d->info, &d->url); 314 setText_LabelWidget(d->info, &d->url);
315 arrange_Widget(as_Widget(d));
204} 316}
205 317
206void setUrl_UploadWidget(iUploadWidget *d, const iString *url) { 318void setUrl_UploadWidget(iUploadWidget *d, const iString *url) {
207 setUrlPort_UploadWidget_(d, url, 0); 319 setUrlPort_UploadWidget_(d, url, 0);
320 remakeIdentityItems_UploadWidget_(d);
321 updateIdentityDropdown_UploadWidget_(d);
208} 322}
209 323
210void setResponseViewer_UploadWidget(iUploadWidget *d, iDocumentWidget *doc) { 324void setResponseViewer_UploadWidget(iUploadWidget *d, iDocumentWidget *doc) {
@@ -227,13 +341,33 @@ static void requestFinished_UploadWidget_(iUploadWidget *d, iGmRequest *req) {
227 postCommand_Widget(d, "upload.request.finished reqid:%u", id_GmRequest(req)); 341 postCommand_Widget(d, "upload.request.finished reqid:%u", id_GmRequest(req));
228} 342}
229 343
344static void updateFileInfo_UploadWidget_(iUploadWidget *d) {
345 iFileInfo *info = iClob(new_FileInfo(&d->filePath));
346 if (isDirectory_FileInfo(info)) {
347 makeMessage_Widget("${heading.upload.error.file}",
348 "${upload.error.directory}",
349 (iMenuItem[]){ "${dlg.message.ok}", 0, 0, "message.ok" }, 1);
350 clear_String(&d->filePath);
351 d->fileSize = 0;
352 return;
353 }
354 d->fileSize = size_FileInfo(info);
355#if defined (iPlatformMobile)
356 setTextCStr_LabelWidget(d->filePathLabel, cstr_Rangecc(baseName_Path(&d->filePath)));
357#else
358 setText_LabelWidget(d->filePathLabel, &d->filePath);
359#endif
360 setTextCStr_LabelWidget(d->fileSizeLabel, formatCStrs_Lang("num.bytes.n", d->fileSize));
361 setTextCStr_InputWidget(d->mime, mediaType_Path(&d->filePath));
362}
363
230static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { 364static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
231 iWidget *w = as_Widget(d); 365 iWidget *w = as_Widget(d);
232 const char *cmd = command_UserEvent(ev); 366 const char *cmd = command_UserEvent(ev);
233 if (isResize_UserEvent(ev)) { 367 if (isResize_UserEvent(ev) || equal_Command(cmd, "keyboard.changed")) {
234 updateInputMaxHeight_UploadWidget_(d); 368 updateInputMaxHeight_UploadWidget_(d);
235 } 369 }
236 if (isCommand_Widget(w, ev, "upload.cancel")) { 370 if (equal_Command(cmd, "upload.cancel")) {
237 setupSheetTransition_Mobile(w, iFalse); 371 setupSheetTransition_Mobile(w, iFalse);
238 destroy_Widget(w); 372 destroy_Widget(w);
239 return iTrue; 373 return iTrue;
@@ -254,9 +388,36 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
254 } 388 }
255 return iTrue; 389 return iTrue;
256 } 390 }
391 if (isCommand_Widget(w, ev, "upload.setid")) {
392 if (hasLabel_Command(cmd, "fp")) {
393 set_Block(&d->idFingerprint, collect_Block(hexDecode_Rangecc(range_Command(cmd, "fp"))));
394 d->idMode = dropdown_UploadIdentity;
395 }
396 else if (arg_Command(cmd)) {
397 clear_Block(&d->idFingerprint);
398 d->idMode = defaultForUrl_UploadIdentity;
399 }
400 else {
401 clear_Block(&d->idFingerprint);
402 d->idMode = none_UploadIdentity;
403 }
404 updateIdentityDropdown_UploadWidget_(d);
405 return iTrue;
406 }
257 if (isCommand_Widget(w, ev, "upload.accept")) { 407 if (isCommand_Widget(w, ev, "upload.accept")) {
258 iWidget * tabs = findChild_Widget(w, "upload.tabs"); 408 iBool isText;
259 const int tabIndex = tabPageIndex_Widget(tabs, currentTabPage_Widget(tabs)); 409 iWidget *tabs = findChild_Widget(w, "upload.tabs");
410 if (tabs) {
411 const size_t tabIndex = tabPageIndex_Widget(tabs, currentTabPage_Widget(tabs));
412 isText = (tabIndex == 0);
413 }
414 else {
415 const size_t panelIndex = currentPanelIndex_Mobile(w);
416 if (panelIndex == iInvalidPos) {
417 return iTrue;
418 }
419 isText = (currentPanelIndex_Mobile(w) == 0);
420 }
260 /* Make a GmRequest and send the data. */ 421 /* Make a GmRequest and send the data. */
261 iAssert(d->request == NULL); 422 iAssert(d->request == NULL);
262 iAssert(!isEmpty_String(&d->url)); 423 iAssert(!isEmpty_String(&d->url));
@@ -264,7 +425,21 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
264 setSendProgressFunc_GmRequest(d->request, updateProgress_UploadWidget_); 425 setSendProgressFunc_GmRequest(d->request, updateProgress_UploadWidget_);
265 setUserData_Object(d->request, d); 426 setUserData_Object(d->request, d);
266 setUrl_GmRequest(d->request, &d->url); 427 setUrl_GmRequest(d->request, &d->url);
267 if (tabIndex == 0) { 428 switch (d->idMode) {
429 case defaultForUrl_UploadIdentity:
430 break; /* GmRequest handles it */
431 case none_UploadIdentity:
432 setIdentity_GmRequest(d->request, NULL);
433 signOut_GmCerts(certs_App(), url_GmRequest(d->request));
434 break;
435 case dropdown_UploadIdentity: {
436 iGmIdentity *ident = findIdentity_GmCerts(certs_App(), &d->idFingerprint);
437 setIdentity_GmRequest(d->request, ident);
438 signIn_GmCerts(certs_App(), ident, url_GmRequest(d->request));
439 break;
440 }
441 }
442 if (isText) {
268 /* Uploading text. */ 443 /* Uploading text. */
269 setTitanData_GmRequest(d->request, 444 setTitanData_GmRequest(d->request,
270 collectNewCStr_String("text/plain"), 445 collectNewCStr_String("text/plain"),
@@ -312,6 +487,7 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
312 d->request = NULL; /* DocumentWidget has it now. */ 487 d->request = NULL; /* DocumentWidget has it now. */
313 } 488 }
314 setupSheetTransition_Mobile(w, iFalse); 489 setupSheetTransition_Mobile(w, iFalse);
490 releaseFile_UploadWidget_(d);
315 destroy_Widget(w); 491 destroy_Widget(w);
316 return iTrue; 492 return iTrue;
317 } 493 }
@@ -321,34 +497,36 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
321 refresh_Widget(w); 497 refresh_Widget(w);
322 return iTrue; 498 return iTrue;
323 } 499 }
500 else if (isCommand_Widget(w, ev, "upload.pickfile")) {
501#if defined (iPlatformAppleMobile)
502 if (hasLabel_Command(cmd, "path")) {
503 releaseFile_UploadWidget_(d);
504 set_String(&d->filePath, collect_String(suffix_Command(cmd, "path")));
505 updateFileInfo_UploadWidget_(d);
506 }
507 else {
508 pickFile_iOS(format_CStr("upload.pickfile ptr:%p", d));
509 }
510#endif
511 return iTrue;
512 }
324 if (ev->type == SDL_DROPFILE) { 513 if (ev->type == SDL_DROPFILE) {
325 /* Switch to File tab. */ 514 /* Switch to File tab. */
326 iWidget *tabs = findChild_Widget(w, "upload.tabs"); 515 iWidget *tabs = findChild_Widget(w, "upload.tabs");
327 showTabPage_Widget(tabs, tabPage_Widget(tabs, 1)); 516 showTabPage_Widget(tabs, tabPage_Widget(tabs, 1));
517 releaseFile_UploadWidget_(d);
328 setCStr_String(&d->filePath, ev->drop.file); 518 setCStr_String(&d->filePath, ev->drop.file);
329 iFileInfo *info = iClob(new_FileInfo(&d->filePath)); 519 updateFileInfo_UploadWidget_(d);
330 if (isDirectory_FileInfo(info)) {
331 makeMessage_Widget("${heading.upload.error.file}",
332 "${upload.error.directory}",
333 (iMenuItem[]){ "${dlg.message.ok}", 0, 0, "message.ok" }, 1);
334 clear_String(&d->filePath);
335 d->fileSize = 0;
336 return iTrue;
337 }
338 d->fileSize = size_FileInfo(info);
339 setText_LabelWidget(d->filePathLabel, &d->filePath);
340 setTextCStr_LabelWidget(d->fileSizeLabel, formatCStrs_Lang("num.bytes.n", d->fileSize));
341 setTextCStr_InputWidget(d->mime, mediaType_Path(&d->filePath));
342 return iTrue; 520 return iTrue;
343 } 521 }
344 return processEvent_Widget(w, ev); 522 return processEvent_Widget(w, ev);
345} 523}
346 524
347static void draw_UploadWidget_(const iUploadWidget *d) { 525//static void draw_UploadWidget_(const iUploadWidget *d) {
348 draw_Widget(constAs_Widget(d)); 526// draw_Widget(constAs_Widget(d));
349} 527//}
350 528
351iBeginDefineSubclass(UploadWidget, Widget) 529iBeginDefineSubclass(UploadWidget, Widget)
352 .processEvent = (iAny *) processEvent_UploadWidget_, 530 .processEvent = (iAny *) processEvent_UploadWidget_,
353 .draw = (iAny *) draw_UploadWidget_, 531 .draw = draw_Widget,
354iEndDefineSubclass(UploadWidget) 532iEndDefineSubclass(UploadWidget)
diff --git a/src/ui/util.c b/src/ui/util.c
index b6ecc7d5..5b9f15a9 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -44,6 +44,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
44# include "../ios.h" 44# include "../ios.h"
45#endif 45#endif
46 46
47#if defined (iPlatformAppleDesktop)
48# include "macos.h"
49#endif
50
47#include <the_Foundation/math.h> 51#include <the_Foundation/math.h>
48#include <the_Foundation/path.h> 52#include <the_Foundation/path.h>
49#include <SDL_timer.h> 53#include <SDL_timer.h>
@@ -333,7 +337,7 @@ void setValue_Anim(iAnim *d, float to, uint32_t span) {
333} 337}
334 338
335void setValueSpeed_Anim(iAnim *d, float to, float unitsPerSecond) { 339void setValueSpeed_Anim(iAnim *d, float to, float unitsPerSecond) {
336 if (iAbs(d->to - to) > 0.0001f) { 340 if (iAbs(d->to - to) > 0.0001f || !isFinished_Anim(d)) {
337 const uint32_t now = SDL_GetTicks(); 341 const uint32_t now = SDL_GetTicks();
338 const float from = valueAt_Anim_(d, now); 342 const float from = valueAt_Anim_(d, now);
339 const float delta = to - from; 343 const float delta = to - from;
@@ -613,6 +617,8 @@ iBool isAction_Widget(const iWidget *d) {
613/*-----------------------------------------------------------------------------------------------*/ 617/*-----------------------------------------------------------------------------------------------*/
614 618
615static iBool isCommandIgnoredByMenus_(const char *cmd) { 619static iBool isCommandIgnoredByMenus_(const char *cmd) {
620 if (equal_Command(cmd, "window.focus.lost") ||
621 equal_Command(cmd, "window.focus.gained")) return iTrue;
616 /* TODO: Perhaps a common way of indicating which commands are notifications and should not 622 /* TODO: Perhaps a common way of indicating which commands are notifications and should not
617 be reacted to by menus? */ 623 be reacted to by menus? */
618 return equal_Command(cmd, "media.updated") || 624 return equal_Command(cmd, "media.updated") ||
@@ -683,49 +689,55 @@ static iWidget *makeMenuSeparator_(void) {
683 return sep; 689 return sep;
684} 690}
685 691
686iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) { 692void makeMenuItems_Widget(iWidget *menu, const iMenuItem *items, size_t n) {
687 iWidget *menu = new_Widget();
688 setBackgroundColor_Widget(menu, uiBackgroundMenu_ColorId);
689 if (deviceType_App() != desktop_AppDeviceType) {
690 setPadding1_Widget(menu, 2 * gap_UI);
691 }
692 else {
693 setPadding1_Widget(menu, gap_UI / 2);
694 }
695 const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); 693 const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App());
696 int64_t itemFlags = (deviceType_App() != desktop_AppDeviceType ? 0 : 0) | 694 int64_t itemFlags = (deviceType_App() != desktop_AppDeviceType ? 0 : 0) |
697 (isPortraitPhone ? extraPadding_WidgetFlag : 0); 695 (isPortraitPhone ? extraPadding_WidgetFlag : 0);
698 setFlags_Widget(menu, 696 iBool haveIcons = iFalse;
699 keepOnTop_WidgetFlag | collapse_WidgetFlag | hidden_WidgetFlag | 697 iWidget *horizGroup = NULL;
700 arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag |
701 resizeChildrenToWidestChild_WidgetFlag | overflowScrollable_WidgetFlag |
702 (isPortraitPhone ? drawBackgroundToVerticalSafeArea_WidgetFlag : 0),
703 iTrue);
704 if (!isPortraitPhone) {
705 setFrameColor_Widget(menu, uiSeparator_ColorId);
706 }
707 iBool haveIcons = iFalse;
708 for (size_t i = 0; i < n; ++i) { 698 for (size_t i = 0; i < n; ++i) {
709 const iMenuItem *item = &items[i]; 699 const iMenuItem *item = &items[i];
710 if (equal_CStr(item->label, "---")) { 700 if (!item->label) {
701 break;
702 }
703 const char *labelText = item->label;
704 if (!startsWith_CStr(labelText, ">>>")) {
705 horizGroup = NULL;
706 }
707 if (equal_CStr(labelText, "---")) {
711 addChild_Widget(menu, iClob(makeMenuSeparator_())); 708 addChild_Widget(menu, iClob(makeMenuSeparator_()));
712 } 709 }
713 else { 710 else {
714 iBool isInfo = iFalse; 711 iBool isInfo = iFalse;
715 const char *labelText = item->label; 712 iBool isDisabled = iFalse;
713 if (startsWith_CStr(labelText, ">>>")) {
714 labelText += 3;
715 if (!horizGroup) {
716 horizGroup = makeHDiv_Widget();
717 setFlags_Widget(horizGroup, resizeHeightOfChildren_WidgetFlag, iFalse);
718 setFlags_Widget(horizGroup, arrangeHeight_WidgetFlag, iTrue);
719 addChild_Widget(menu, iClob(horizGroup));
720 }
721 }
716 if (startsWith_CStr(labelText, "```")) { 722 if (startsWith_CStr(labelText, "```")) {
717 labelText += 3; 723 labelText += 3;
718 isInfo = iTrue; 724 isInfo = iTrue;
719 } 725 }
726 if (startsWith_CStr(labelText, "///")) {
727 labelText += 3;
728 isDisabled = iTrue;
729 }
720 iLabelWidget *label = addChildFlags_Widget( 730 iLabelWidget *label = addChildFlags_Widget(
721 menu, 731 horizGroup ? horizGroup : menu,
722 iClob(newKeyMods_LabelWidget(labelText, item->key, item->kmods, item->command)), 732 iClob(newKeyMods_LabelWidget(labelText, item->key, item->kmods, item->command)),
723 noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag | 733 noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag |
724 drawKey_WidgetFlag | itemFlags); 734 drawKey_WidgetFlag | itemFlags);
725 setWrap_LabelWidget(label, isInfo); 735 setWrap_LabelWidget(label, isInfo);
726 haveIcons |= checkIcon_LabelWidget(label); 736 haveIcons |= checkIcon_LabelWidget(label);
727 updateSize_LabelWidget(label); /* drawKey was set */ 737 updateSize_LabelWidget(label); /* drawKey was set */
738 setFlags_Widget(as_Widget(label), disabled_WidgetFlag, isDisabled);
728 if (isInfo) { 739 if (isInfo) {
740 setFlags_Widget(as_Widget(label), fixedHeight_WidgetFlag, iTrue); /* wrap changes height */
729 setTextColor_LabelWidget(label, uiTextAction_ColorId); 741 setTextColor_LabelWidget(label, uiTextAction_ColorId);
730 } 742 }
731 } 743 }
@@ -742,18 +754,103 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
742 iForEach(ObjectList, i, children_Widget(menu)) { 754 iForEach(ObjectList, i, children_Widget(menu)) {
743 if (isInstance_Object(i.object, &Class_LabelWidget)) { 755 if (isInstance_Object(i.object, &Class_LabelWidget)) {
744 iLabelWidget *label = i.object; 756 iLabelWidget *label = i.object;
745 if (icon_LabelWidget(label) == 0) { 757 if (!isWrapped_LabelWidget(label) && icon_LabelWidget(label) == 0) {
746 setIcon_LabelWidget(label, ' '); 758 setIcon_LabelWidget(label, ' ');
747 } 759 }
748 } 760 }
749 } 761 }
750 } 762 }
763}
764
765static iArray *deepCopyMenuItems_(iWidget *menu, const iMenuItem *items, size_t n) {
766 iArray *array = new_Array(sizeof(iMenuItem));
767 iString cmd;
768 init_String(&cmd);
769 for (size_t i = 0; i < n; i++) {
770 const iMenuItem *item = &items[i];
771 const char *itemCommand = item->command;
772#if 0
773 if (itemCommand) {
774 /* Make it appear the command is coming from the right widget. */
775 setCStr_String(&cmd, itemCommand);
776 if (!hasLabel_Command(itemCommand, "ptr")) {
777 size_t firstSpace = indexOf_String(&cmd, ' ');
778 iBlock ptr;
779 init_Block(&ptr, 0);
780 printf_Block(&ptr, " ptr:%p", menu);
781 if (firstSpace != iInvalidPos) {
782 insertData_Block(&cmd.chars, firstSpace, data_Block(&ptr), size_Block(&ptr));
783 }
784 else {
785 append_Block(&cmd.chars, &ptr);
786 }
787 deinit_Block(&ptr);
788 }
789 itemCommand = cstr_String(&cmd);
790 }
791#endif
792 pushBack_Array(array, &(iMenuItem){
793 item->label ? iDupStr(item->label) : NULL,
794 item->key,
795 item->kmods,
796 itemCommand ? iDupStr(itemCommand) : NULL /* NOTE: Only works with string commands. */
797 });
798 }
799 deinit_String(&cmd);
800 return array;
801}
802
803static void deleteMenuItems_(iArray *items) {
804 iForEach(Array, i, items) {
805 iMenuItem *item = i.value;
806 free((void *) item->label);
807 free((void *) item->command);
808 }
809 delete_Array(items);
810}
811
812iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
813 iWidget *menu = new_Widget();
814#if defined (iHaveNativeContextMenus)
815 setFlags_Widget(menu, hidden_WidgetFlag | nativeMenu_WidgetFlag, iTrue);
816 setUserData_Object(menu, deepCopyMenuItems_(menu, items, n));
817 addChild_Widget(parent, menu);
818 iRelease(menu); /* owned by parent now */
819 /* Keyboard shortcuts still need to triggerable via the menu, although the items don't exist. */ {
820 for (size_t i = 0; i < n; i++) {
821 const iMenuItem *item = &items[i];
822 if (item->key) {
823 addAction_Widget(menu, item->key, item->kmods, item->command);
824 }
825 }
826 }
827#else
828 /* Non-native custom popup menu. This may still be displayed inside a separate window. */
829 setDrawBufferEnabled_Widget(menu, iTrue);
830 setBackgroundColor_Widget(menu, uiBackgroundMenu_ColorId);
831 if (deviceType_App() != desktop_AppDeviceType) {
832 setPadding1_Widget(menu, 2 * gap_UI);
833 }
834 else {
835 setPadding1_Widget(menu, gap_UI / 2);
836 }
837 setFlags_Widget(menu,
838 keepOnTop_WidgetFlag | collapse_WidgetFlag | hidden_WidgetFlag |
839 arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag |
840 resizeChildrenToWidestChild_WidgetFlag | overflowScrollable_WidgetFlag |
841 (isPortraitPhone_App() ? drawBackgroundToVerticalSafeArea_WidgetFlag : 0),
842 iTrue);
843 if (!isPortraitPhone_App()) {
844 setFrameColor_Widget(menu, uiSeparator_ColorId);
845 }
846 makeMenuItems_Widget(menu, items, n);
751 addChild_Widget(parent, menu); 847 addChild_Widget(parent, menu);
752 iRelease(menu); /* owned by parent now */ 848 iRelease(menu); /* owned by parent now */
753 setCommandHandler_Widget(menu, menuHandler_); 849 setCommandHandler_Widget(menu, menuHandler_);
754 iWidget *cancel = addAction_Widget(menu, SDLK_ESCAPE, 0, "cancel"); 850 iWidget *cancel = addAction_Widget(menu, SDLK_ESCAPE, 0, "cancel");
755 setId_Widget(cancel, "menu.cancel"); 851 setId_Widget(cancel, "menu.cancel");
756 setFlags_Widget(cancel, disabled_WidgetFlag, iTrue); 852 setFlags_Widget(cancel, disabled_WidgetFlag, iTrue);
853#endif
757 return menu; 854 return menu;
758} 855}
759 856
@@ -761,7 +858,157 @@ void openMenu_Widget(iWidget *d, iInt2 windowCoord) {
761 openMenuFlags_Widget(d, windowCoord, iTrue); 858 openMenuFlags_Widget(d, windowCoord, iTrue);
762} 859}
763 860
861static void updateMenuItemFonts_Widget_(iWidget *d) {
862 const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App());
863 const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0;
864 iForEach(ObjectList, i, children_Widget(d)) {
865 if (isInstance_Object(i.object, &Class_LabelWidget)) {
866 iLabelWidget *label = i.object;
867 const iBool isCaution = startsWith_String(text_LabelWidget(label), uiTextCaution_ColorEscape);
868 if (isWrapped_LabelWidget(label)) {
869 continue;
870 }
871 if (deviceType_App() == desktop_AppDeviceType) {
872 setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId);
873 }
874 else if (isPortraitPhone) {
875 if (!isSlidePanel) {
876 setFont_LabelWidget(label, isCaution ? defaultBigBold_FontId : defaultBig_FontId);
877 }
878 }
879 else {
880 setFont_LabelWidget(label, isCaution ? uiContentBold_FontId : uiContent_FontId);
881 }
882 }
883 else if (childCount_Widget(i.object)) {
884 updateMenuItemFonts_Widget_(i.object);
885 }
886 }
887}
888
889iMenuItem *findNativeMenuItem_Widget(iWidget *menu, const char *commandSuffix) {
890 iAssert(flags_Widget(menu) & nativeMenu_WidgetFlag);
891 iForEach(Array, i, userData_Object(menu)) {
892 iMenuItem *item = i.value;
893 if (item->command && endsWith_Rangecc(range_CStr(item->command), commandSuffix)) {
894 return item;
895 }
896 }
897 return NULL;
898}
899
900void setPrefix_NativeMenuItem(iMenuItem *item, const char *prefix, iBool set) {
901 if (!item->label) {
902 return;
903 }
904 const iBool hasPrefix = startsWith_CStr(item->label, prefix);
905 if (hasPrefix && !set) {
906 char *label = iDupStr(item->label + 3);
907 free((char *) item->label);
908 item->label = label;
909 }
910 else if (!hasPrefix && set) {
911 char *label = malloc(strlen(item->label) + 4);
912 memcpy(label, prefix, 3);
913 strcpy(label + 3, item->label);
914 free((char *) item->label);
915 item->label = label;
916 }
917}
918
919void setSelected_NativeMenuItem(iMenuItem *item, iBool isSelected) {
920 if (item) {
921 setPrefix_NativeMenuItem(item, "///", iFalse);
922 setPrefix_NativeMenuItem(item, "###", isSelected);
923 }
924}
925
926void setDisabled_NativeMenuItem(iMenuItem *item, iBool isDisabled) {
927 if (item) {
928 setPrefix_NativeMenuItem(item, "###", iFalse);
929 setPrefix_NativeMenuItem(item, "///", isDisabled);
930 }
931}
932
933void setLabel_NativeMenuItem(iMenuItem *item, const char *label) {
934 free((char *) item->label);
935 item->label = iDupStr(label);
936}
937
938void setMenuItemLabel_Widget(iWidget *menu, const char *command, const char *newLabel) {
939 if (flags_Widget(menu) & nativeMenu_WidgetFlag) {
940 iArray *items = userData_Object(menu);
941 iAssert(items);
942 iForEach(Array, i, items) {
943 iMenuItem *item = i.value;
944 if (item->command && !iCmpStr(item->command, command)) {
945 setLabel_NativeMenuItem(item, newLabel);
946 break;
947 }
948 }
949 }
950 else {
951 iLabelWidget *menuItem = findMenuItem_Widget(menu, command);
952 if (menuItem) {
953 setTextCStr_LabelWidget(menuItem, newLabel);
954 checkIcon_LabelWidget(menuItem);
955 }
956 }
957}
958
959void setMenuItemLabelByIndex_Widget(iWidget *menu, size_t index, const char *newLabel) {
960 if (!menu) {
961 return;
962 }
963 if (flags_Widget(menu) & nativeMenu_WidgetFlag) {
964 iArray *items = userData_Object(menu);
965 iAssert(items);
966 iAssert(index < size_Array(items));
967 setLabel_NativeMenuItem(at_Array(items, index), newLabel);
968 }
969 else {
970 iLabelWidget *menuItem = child_Widget(menu, index);
971 iAssert(isInstance_Object(menuItem, &Class_LabelWidget));
972 setTextCStr_LabelWidget(menuItem, newLabel);
973 checkIcon_LabelWidget(menuItem);
974 }
975}
976
977void unselectAllNativeMenuItems_Widget(iWidget *menu) {
978 iArray *items = userData_Object(menu);
979 iAssert(items);
980 iForEach(Array, i, items) {
981 setSelected_NativeMenuItem(i.value, iFalse);
982 }
983}
984
985iLocalDef iBool isUsingMenuPopupWindows_(void) {
986#if defined (LAGRANGE_ENABLE_POPUP_MENUS)
987 return deviceType_App() == desktop_AppDeviceType;
988#else
989 return iFalse;
990#endif
991}
992
993void releaseNativeMenu_Widget(iWidget *d) {
994#if defined (iHaveNativeContextMenus)
995 iArray *items = userData_Object(d);
996 iAssert(flags_Widget(d) & nativeMenu_WidgetFlag);
997 iAssert(items);
998 deleteMenuItems_(items);
999 setUserData_Object(d, NULL);
1000#else
1001 iUnused(d);
1002#endif
1003}
1004
764void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) { 1005void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
1006#if defined (iHaveNativeContextMenus)
1007 const iArray *items = userData_Object(d);
1008 iAssert(flags_Widget(d) & nativeMenu_WidgetFlag);
1009 iAssert(items);
1010 showPopupMenu_MacOS(d, windowCoord, constData_Array(items), size_Array(items));
1011#else
765 const iRect rootRect = rect_Root(d->root); 1012 const iRect rootRect = rect_Root(d->root);
766 const iInt2 rootSize = rootRect.size; 1013 const iInt2 rootSize = rootRect.size;
767 const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); 1014 const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App());
@@ -773,6 +1020,36 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
773 processEvents_App(postedEventsOnly_AppEventMode); 1020 processEvents_App(postedEventsOnly_AppEventMode);
774 setFlags_Widget(d, hidden_WidgetFlag, iFalse); 1021 setFlags_Widget(d, hidden_WidgetFlag, iFalse);
775 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue); 1022 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue);
1023 if (isUsingMenuPopupWindows_()) {
1024 if (postCommands) {
1025 postCommand_Widget(d, "menu.opened");
1026 }
1027 updateMenuItemFonts_Widget_(d);
1028 iRoot *oldRoot = current_Root();
1029 setFlags_Widget(d, keepOnTop_WidgetFlag, iFalse);
1030 setUserData_Object(d, parent_Widget(d));
1031 removeChild_Widget(parent_Widget(d), d); /* we'll borrow the widget for a while */
1032 const float pixelRatio = get_Window()->pixelRatio;
1033 iInt2 menuPos = add_I2(get_MainWindow()->place.normalRect.pos,
1034 divf_I2(sub_I2(windowCoord, divi_I2(gap2_UI, 2)), pixelRatio));
1035 arrange_Widget(d);
1036 /* Check display bounds. */ {
1037 const iInt2 menuSize = divf_I2(d->rect.size, pixelRatio);
1038 SDL_Rect displayRect;
1039 SDL_GetDisplayBounds(SDL_GetWindowDisplayIndex(get_Window()->win), &displayRect);
1040 menuPos.x = iMin(menuPos.x, displayRect.x + displayRect.w - menuSize.x);
1041 menuPos.y = iMin(menuPos.y, displayRect.y + displayRect.h - menuSize.y);
1042 }
1043 // SDL_GetGlobalMouseState(&mousePos.x, &mousePos.y);
1044 iWindow *win = newPopup_Window(menuPos, d);
1045 SDL_SetWindowTitle(win->win, "Menu");
1046 addPopup_App(win); /* window takes the widget */
1047 SDL_ShowWindow(win->win);
1048 draw_Window(win);
1049 setCurrent_Window(mainWindow_App());
1050 setCurrent_Root(oldRoot);
1051 return;
1052 }
776 raise_Widget(d); 1053 raise_Widget(d);
777 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); 1054 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse);
778 if (isPortraitPhone) { 1055 if (isPortraitPhone) {
@@ -783,32 +1060,11 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
783 } 1060 }
784 d->rect.size.x = rootSize.x; 1061 d->rect.size.x = rootSize.x;
785 } 1062 }
786 /* Update item fonts. */ { 1063 updateMenuItemFonts_Widget_(d);
787 iForEach(ObjectList, i, children_Widget(d)) {
788 if (isInstance_Object(i.object, &Class_LabelWidget)) {
789 iLabelWidget *label = i.object;
790 const iBool isCaution = startsWith_String(text_LabelWidget(label), uiTextCaution_ColorEscape);
791 if (isWrapped_LabelWidget(label)) {
792 continue;
793 }
794 if (deviceType_App() == desktop_AppDeviceType) {
795 setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId);
796 }
797 else if (isPortraitPhone) {
798 if (!isSlidePanel) {
799 setFont_LabelWidget(label, isCaution ? defaultBigBold_FontId : defaultBig_FontId);
800 }
801 }
802 else {
803 setFont_LabelWidget(label, isCaution ? uiContentBold_FontId : uiContent_FontId);
804 }
805 }
806 }
807 }
808 arrange_Widget(d); 1064 arrange_Widget(d);
809 if (isPortraitPhone) { 1065 if (isPortraitPhone) {
810 if (isSlidePanel) { 1066 if (isSlidePanel) {
811 d->rect.pos = zero_I2(); //neg_I2(bounds_Widget(parent_Widget(d)).pos); 1067 d->rect.pos = zero_I2();
812 } 1068 }
813 else { 1069 else {
814 d->rect.pos = init_I2(0, rootSize.y); 1070 d->rect.pos = init_I2(0, rootSize.y);
@@ -828,7 +1084,7 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
828 float l, t, r, b; 1084 float l, t, r, b;
829 safeAreaInsets_iOS(&l, &t, &r, &b); 1085 safeAreaInsets_iOS(&l, &t, &r, &b);
830 topExcess += t; 1086 topExcess += t;
831 bottomExcess += iMax(b, get_Window()->keyboardHeight); 1087 bottomExcess += iMax(b, get_MainWindow()->keyboardHeight);
832 leftExcess += l; 1088 leftExcess += l;
833 rightExcess += r; 1089 rightExcess += r;
834 } 1090 }
@@ -850,12 +1106,28 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
850 postCommand_Widget(d, "menu.opened"); 1106 postCommand_Widget(d, "menu.opened");
851 } 1107 }
852 setupMenuTransition_Mobile(d, iTrue); 1108 setupMenuTransition_Mobile(d, iTrue);
1109#endif
853} 1110}
854 1111
855void closeMenu_Widget(iWidget *d) { 1112void closeMenu_Widget(iWidget *d) {
1113 if (flags_Widget(d) & nativeMenu_WidgetFlag) {
1114 return; /* Handled natively. */
1115 }
856 if (d == NULL || flags_Widget(d) & hidden_WidgetFlag) { 1116 if (d == NULL || flags_Widget(d) & hidden_WidgetFlag) {
857 return; /* Already closed. */ 1117 return; /* Already closed. */
858 } 1118 }
1119 if (isUsingMenuPopupWindows_()) {
1120 iWindow *win = window_Widget(d);
1121 iAssert(type_Window(win) == popup_WindowType);
1122 iWidget *originalParent = userData_Object(d);
1123 setUserData_Object(d, NULL);
1124 win->roots[0]->widget = NULL;
1125 setRoot_Widget(d, originalParent->root);
1126 addChild_Widget(originalParent, d);
1127 setFlags_Widget(d, keepOnTop_WidgetFlag, iTrue);
1128 SDL_HideWindow(win->win);
1129 collect_Garbage(win, (iDeleteFunc) delete_Window); /* get rid of it after event processing */
1130 }
859 setFlags_Widget(d, hidden_WidgetFlag, iTrue); 1131 setFlags_Widget(d, hidden_WidgetFlag, iTrue);
860 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iTrue); 1132 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iTrue);
861 postRefresh_App(); 1133 postRefresh_App();
@@ -876,10 +1148,27 @@ iLabelWidget *findMenuItem_Widget(iWidget *menu, const char *command) {
876} 1148}
877 1149
878void setMenuItemDisabled_Widget(iWidget *menu, const char *command, iBool disable) { 1150void setMenuItemDisabled_Widget(iWidget *menu, const char *command, iBool disable) {
879 iLabelWidget *item = findMenuItem_Widget(menu, command); 1151 if (flags_Widget(menu) & nativeMenu_WidgetFlag) {
880 if (item) { 1152 setDisabled_NativeMenuItem(findNativeMenuItem_Widget(menu, command), disable);
881 setFlags_Widget(as_Widget(item), disabled_WidgetFlag, disable); 1153 }
1154 else {
1155 iLabelWidget *item = findMenuItem_Widget(menu, command);
1156 if (item) {
1157 setFlags_Widget(as_Widget(item), disabled_WidgetFlag, disable);
1158 }
1159 }
1160}
1161
1162void setMenuItemDisabledByIndex_Widget(iWidget *menu, size_t index, iBool disable) {
1163 if (!menu) {
1164 return;
882 } 1165 }
1166 if (flags_Widget(menu) & nativeMenu_WidgetFlag) {
1167 setDisabled_NativeMenuItem(at_Array(userData_Object(menu), index), disable);
1168 }
1169 else {
1170 setFlags_Widget(child_Widget(menu, index), disabled_WidgetFlag, disable);
1171 }
883} 1172}
884 1173
885int checkContextMenu_Widget(iWidget *menu, const SDL_Event *ev) { 1174int checkContextMenu_Widget(iWidget *menu, const SDL_Event *ev) {
@@ -904,6 +1193,50 @@ iLabelWidget *makeMenuButton_LabelWidget(const char *label, const iMenuItem *ite
904 return button; 1193 return button;
905} 1194}
906 1195
1196const iString *removeMenuItemLabelPrefixes_String(const iString *d) {
1197 iString *str = copy_String(d);
1198 for (;;) {
1199 if (startsWith_String(str, "###")) {
1200 remove_Block(&str->chars, 0, 3);
1201 continue;
1202 }
1203 if (startsWith_String(str, "///")) {
1204 remove_Block(&str->chars, 0, 3);
1205 continue;
1206 }
1207 if (startsWith_String(str, "```")) {
1208 remove_Block(&str->chars, 0, 3);
1209 continue;
1210 }
1211 break;
1212 }
1213 return collect_String(str);
1214}
1215
1216void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *selectedCommand) {
1217 iWidget *menu = findChild_Widget(as_Widget(dropButton), "menu");
1218 if (flags_Widget(menu) & nativeMenu_WidgetFlag) {
1219 unselectAllNativeMenuItems_Widget(menu);
1220 iMenuItem *item = findNativeMenuItem_Widget(menu, selectedCommand);
1221 if (item) {
1222 setSelected_NativeMenuItem(item, iTrue);
1223 updateText_LabelWidget(dropButton,
1224 removeMenuItemLabelPrefixes_String(collectNewCStr_String(item->label)));
1225 }
1226 return;
1227 }
1228 iForEach(ObjectList, i, children_Widget(menu)) {
1229 if (isInstance_Object(i.object, &Class_LabelWidget)) {
1230 iLabelWidget *item = i.object;
1231 const iBool isSelected = endsWith_String(command_LabelWidget(item), selectedCommand);
1232 setFlags_Widget(as_Widget(item), selected_WidgetFlag, isSelected);
1233 if (isSelected) {
1234 updateText_LabelWidget(dropButton, sourceText_LabelWidget(item));
1235 }
1236 }
1237 }
1238}
1239
907/*-----------------------------------------------------------------------------------------------*/ 1240/*-----------------------------------------------------------------------------------------------*/
908 1241
909static iBool isTabPage_Widget_(const iWidget *tabs, const iWidget *page) { 1242static iBool isTabPage_Widget_(const iWidget *tabs, const iWidget *page) {
@@ -1043,6 +1376,7 @@ iWidget *removeTabPage_Widget(iWidget *tabs, size_t index) {
1043} 1376}
1044 1377
1045void resizeToLargestPage_Widget(iWidget *tabs) { 1378void resizeToLargestPage_Widget(iWidget *tabs) {
1379 if (!tabs) return;
1046// puts("RESIZE TO LARGEST PAGE ..."); 1380// puts("RESIZE TO LARGEST PAGE ...");
1047 iWidget *pages = findChild_Widget(tabs, "tabs.pages"); 1381 iWidget *pages = findChild_Widget(tabs, "tabs.pages");
1048 iForEach(ObjectList, i, children_Widget(pages)) { 1382 iForEach(ObjectList, i, children_Widget(pages)) {
@@ -1178,11 +1512,23 @@ static void updateValueInputWidth_(iWidget *dlg) {
1178 dlg->rect.size.x = 1512 dlg->rect.size.x =
1179 iMin(rootSize.x, iMaxi(iMaxi(100 * gap_UI, title->rect.size.x), prompt->rect.size.x)); 1513 iMin(rootSize.x, iMaxi(iMaxi(100 * gap_UI, title->rect.size.x), prompt->rect.size.x));
1180 } 1514 }
1515 /* Adjust the maximum number of visible lines. */
1516 int footer = 6 * gap_UI + get_MainWindow()->keyboardHeight;
1517 iWidget *buttons = findChild_Widget(dlg, "dialogbuttons");
1518 if (buttons) {
1519 footer += height_Widget(buttons);
1520 }
1521 iInputWidget *input = findChild_Widget(dlg, "input");
1522 setLineLimits_InputWidget(input,
1523 1,
1524 (bottom_Rect(safeRect_Root(dlg->root)) - footer -
1525 top_Rect(boundsWithoutVisualOffset_Widget(as_Widget(input)))) /
1526 lineHeight_Text(font_InputWidget(input)));
1181} 1527}
1182 1528
1183iBool valueInputHandler_(iWidget *dlg, const char *cmd) { 1529iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
1184 iWidget *ptr = as_Widget(pointer_Command(cmd)); 1530 iWidget *ptr = as_Widget(pointer_Command(cmd));
1185 if (equal_Command(cmd, "window.resized")) { 1531 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "keyboard.changed")) {
1186 if (isVisible_Widget(dlg)) { 1532 if (isVisible_Widget(dlg)) {
1187 updateValueInputWidth_(dlg); 1533 updateValueInputWidth_(dlg);
1188 arrange_Widget(dlg); 1534 arrange_Widget(dlg);
@@ -1198,7 +1544,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
1198 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 1544 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
1199 setId_Widget(dlg, ""); /* no further commands to emit */ 1545 setId_Widget(dlg, ""); /* no further commands to emit */
1200 } 1546 }
1201 setupSheetTransition_Mobile(dlg, iFalse); 1547 setupSheetTransition_Mobile(dlg, top_TransitionDir);
1202 destroy_Widget(dlg); 1548 destroy_Widget(dlg);
1203 return iTrue; 1549 return iTrue;
1204 } 1550 }
@@ -1207,13 +1553,13 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
1207 else if (equal_Command(cmd, "valueinput.cancel")) { 1553 else if (equal_Command(cmd, "valueinput.cancel")) {
1208 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 1554 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
1209 setId_Widget(dlg, ""); /* no further commands to emit */ 1555 setId_Widget(dlg, ""); /* no further commands to emit */
1210 setupSheetTransition_Mobile(dlg, iFalse); 1556 setupSheetTransition_Mobile(dlg, top_TransitionDir);
1211 destroy_Widget(dlg); 1557 destroy_Widget(dlg);
1212 return iTrue; 1558 return iTrue;
1213 } 1559 }
1214 else if (equal_Command(cmd, "valueinput.accept")) { 1560 else if (equal_Command(cmd, "valueinput.accept")) {
1215 acceptValueInput_(dlg); 1561 acceptValueInput_(dlg);
1216 setupSheetTransition_Mobile(dlg, iFalse); 1562 setupSheetTransition_Mobile(dlg, top_TransitionDir);
1217 destroy_Widget(dlg); 1563 destroy_Widget(dlg);
1218 return iTrue; 1564 return iTrue;
1219 } 1565 }
@@ -1317,7 +1663,6 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con
1317 setText_InputWidget(input, initialValue); 1663 setText_InputWidget(input, initialValue);
1318 } 1664 }
1319 setId_Widget(as_Widget(input), "input"); 1665 setId_Widget(as_Widget(input), "input");
1320 updateValueInputWidth_(dlg);
1321 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); 1666 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
1322 addChild_Widget(dlg, 1667 addChild_Widget(dlg,
1323 iClob(makeDialogButtons_Widget( 1668 iClob(makeDialogButtons_Widget(
@@ -1327,10 +1672,20 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con
1327 acceptKeyMod_ReturnKeyBehavior(prefs_App()->returnKey), 1672 acceptKeyMod_ReturnKeyBehavior(prefs_App()->returnKey),
1328 "valueinput.accept" } }, 1673 "valueinput.accept" } },
1329 2))); 1674 2)));
1330 finalizeSheet_Mobile(dlg); 1675// finalizeSheet_Mobile(dlg);
1676 arrange_Widget(dlg);
1331 if (parent) { 1677 if (parent) {
1332 setFocus_Widget(as_Widget(input)); 1678 setFocus_Widget(as_Widget(input));
1333 } 1679 }
1680 /* Check that the top is in the safe area. */ {
1681 int top = top_Rect(bounds_Widget(dlg));
1682 int delta = top - top_Rect(safeRect_Root(dlg->root));
1683 if (delta < 0) {
1684 dlg->rect.pos.y -= delta;
1685 }
1686 }
1687 updateValueInputWidth_(dlg);
1688 setupSheetTransition_Mobile(dlg, incoming_TransitionFlag | top_TransitionDir);
1334 return dlg; 1689 return dlg;
1335} 1690}
1336 1691
@@ -1377,6 +1732,28 @@ iWidget *makeMessage_Widget(const char *title, const char *msg, const iMenuItem
1377iWidget *makeQuestion_Widget(const char *title, const char *msg, 1732iWidget *makeQuestion_Widget(const char *title, const char *msg,
1378 const iMenuItem *items, size_t numItems) { 1733 const iMenuItem *items, size_t numItems) {
1379 processEvents_App(postedEventsOnly_AppEventMode); 1734 processEvents_App(postedEventsOnly_AppEventMode);
1735 if (isUsingPanelLayout_Mobile()) {
1736 iArray *panelItems = collectNew_Array(sizeof(iMenuItem));
1737 pushBackN_Array(panelItems, (iMenuItem[]){
1738 { format_CStr("title text:%s", title) },
1739 { format_CStr("label text:%s", msg) },
1740 { NULL }
1741 }, 3);
1742 for (size_t i = 0; i < numItems; i++) {
1743 const iMenuItem *item = &items[i];
1744 const char first = item->label[0];
1745 if (first == '*' || first == '&') {
1746 insert_Array(panelItems, size_Array(panelItems) - 1,
1747 &(iMenuItem){ format_CStr("button selected:%d text:%s",
1748 first == '&' ? 1 : 0, item->label + 1),
1749 0, 0, item->command });
1750 }
1751 }
1752 iWidget *dlg = makePanels_Mobile("", data_Array(panelItems), items, numItems);
1753 setCommandHandler_Widget(dlg, messageHandler_);
1754 setupSheetTransition_Mobile(dlg, iTrue);
1755 return dlg;
1756 }
1380 iWidget *dlg = makeSheet_Widget(""); 1757 iWidget *dlg = makeSheet_Widget("");
1381 setCommandHandler_Widget(dlg, messageHandler_); 1758 setCommandHandler_Widget(dlg, messageHandler_);
1382 addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag); 1759 addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag);
@@ -1404,7 +1781,7 @@ iWidget *makeQuestion_Widget(const char *title, const char *msg,
1404 addChild_Widget(dlg->root->widget, iClob(dlg)); 1781 addChild_Widget(dlg->root->widget, iClob(dlg));
1405 arrange_Widget(dlg); /* BUG: This extra arrange shouldn't be needed but the dialog won't 1782 arrange_Widget(dlg); /* BUG: This extra arrange shouldn't be needed but the dialog won't
1406 be arranged correctly unless it's here. */ 1783 be arranged correctly unless it's here. */
1407 finalizeSheet_Mobile(dlg); 1784 setupSheetTransition_Mobile(dlg, iTrue);
1408 return dlg; 1785 return dlg;
1409} 1786}
1410 1787
@@ -1468,6 +1845,15 @@ iWidget *makeTwoColumns_Widget(iWidget **headings, iWidget **values) {
1468 return page; 1845 return page;
1469} 1846}
1470 1847
1848iLabelWidget *dialogAcceptButton_Widget(const iWidget *d) {
1849 iWidget *buttonParent = findChild_Widget(d, "dialogbuttons");
1850 if (!buttonParent) {
1851 iAssert(isUsingPanelLayout_Mobile());
1852 buttonParent = findChild_Widget(d, "panel.back");
1853 }
1854 return (iLabelWidget *) lastChild_Widget(buttonParent);
1855}
1856
1471iWidget *appendTwoColumnTabPage_Widget(iWidget *tabs, const char *title, int shortcut, iWidget **headings, 1857iWidget *appendTwoColumnTabPage_Widget(iWidget *tabs, const char *title, int shortcut, iWidget **headings,
1472 iWidget **values) { 1858 iWidget **values) {
1473 /* TODO: Use `makeTwoColumnWidget_()`, see above. */ 1859 /* TODO: Use `makeTwoColumnWidget_()`, see above. */
@@ -1506,7 +1892,7 @@ static void addRadioButton_(iWidget *parent, const char *id, const char *label,
1506 id); 1892 id);
1507} 1893}
1508 1894
1509static void addFontButtons_(iWidget *parent, const char *id) { 1895static const iArray *makeFontItems_(const char *id) {
1510 const struct { 1896 const struct {
1511 const char * name; 1897 const char * name;
1512 enum iTextFont cfgId; 1898 enum iTextFont cfgId;
@@ -1518,7 +1904,7 @@ static void addFontButtons_(iWidget *parent, const char *id) {
1518 { "Tinos", tinos_TextFont }, 1904 { "Tinos", tinos_TextFont },
1519 { "---", -1 }, 1905 { "---", -1 },
1520 { "Iosevka", iosevka_TextFont } }; 1906 { "Iosevka", iosevka_TextFont } };
1521 iArray *items = new_Array(sizeof(iMenuItem)); 1907 iArray *items = collectNew_Array(sizeof(iMenuItem));
1522 iForIndices(i, fonts) { 1908 iForIndices(i, fonts) {
1523 pushBack_Array(items, 1909 pushBack_Array(items,
1524 &(iMenuItem){ fonts[i].name, 1910 &(iMenuItem){ fonts[i].name,
@@ -1528,11 +1914,18 @@ static void addFontButtons_(iWidget *parent, const char *id) {
1528 ? format_CStr("!%s.set arg:%d", id, fonts[i].cfgId) 1914 ? format_CStr("!%s.set arg:%d", id, fonts[i].cfgId)
1529 : NULL }); 1915 : NULL });
1530 } 1916 }
1531 iLabelWidget *button = makeMenuButton_LabelWidget("Source Sans 3", data_Array(items), size_Array(items)); 1917 pushBack_Array(items, &(iMenuItem){ NULL }); /* terminator */
1532 setBackgroundColor_Widget(findChild_Widget(as_Widget(button), "menu"), uiBackgroundMenu_ColorId); 1918 return items;
1919}
1920
1921static void addFontButtons_(iWidget *parent, const char *id) {
1922 const iArray *items = makeFontItems_(id);
1923 iLabelWidget *button = makeMenuButton_LabelWidget("Source Sans 3",
1924 constData_Array(items), size_Array(items));
1925 setBackgroundColor_Widget(findChild_Widget(as_Widget(button), "menu"),
1926 uiBackgroundMenu_ColorId);
1533 setId_Widget(as_Widget(button), format_CStr("prefs.%s", id)); 1927 setId_Widget(as_Widget(button), format_CStr("prefs.%s", id));
1534 addChildFlags_Widget(parent, iClob(button), alignLeft_WidgetFlag); 1928 addChildFlags_Widget(parent, iClob(button), alignLeft_WidgetFlag);
1535 delete_Array(items);
1536} 1929}
1537 1930
1538#if 0 1931#if 0
@@ -1607,15 +2000,27 @@ iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *va
1607 return input; 2000 return input;
1608} 2001}
1609 2002
2003static void addDialogPadding_(iWidget *headings, iWidget *values) {
2004 const int bigGap = lineHeight_Text(uiLabel_FontId) * 3 / 4;
2005 addChild_Widget(headings, iClob(makePadding_Widget(bigGap)));
2006 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
2007}
2008
1610static void addPrefsInputWithHeading_(iWidget *headings, iWidget *values, 2009static void addPrefsInputWithHeading_(iWidget *headings, iWidget *values,
1611 const char *id, iInputWidget *input) { 2010 const char *id, iInputWidget *input) {
1612 addDialogInputWithHeading_(headings, values, format_CStr("${%s}", id), id, input); 2011 addDialogInputWithHeading_(headings, values, format_CStr("${%s}", id), id, input);
1613} 2012}
1614 2013
1615static size_t findWidestItemLabel_(const iMenuItem *items, size_t num) { 2014static void addDialogToggle_(iWidget *headings, iWidget *values,
2015 const char *heading, const char *toggleId) {
2016 addChild_Widget(headings, iClob(makeHeading_Widget(heading)));
2017 addChild_Widget(values, iClob(makeToggle_Widget(toggleId)));
2018}
2019
2020size_t findWidestLabel_MenuItem(const iMenuItem *items, size_t num) {
1616 int widest = 0; 2021 int widest = 0;
1617 size_t widestPos = iInvalidPos; 2022 size_t widestPos = iInvalidPos;
1618 for (size_t i = 0; i < num; i++) { 2023 for (size_t i = 0; i < num && items[i].label; i++) {
1619 const int width = 2024 const int width =
1620 measure_Text(uiLabel_FontId, 2025 measure_Text(uiLabel_FontId,
1621 translateCStr_Lang(items[i].label)) 2026 translateCStr_Lang(items[i].label))
@@ -1628,7 +2033,260 @@ static size_t findWidestItemLabel_(const iMenuItem *items, size_t num) {
1628 return widestPos; 2033 return widestPos;
1629} 2034}
1630 2035
2036iChar removeIconPrefix_String(iString *d) {
2037 if (isEmpty_String(d)) {
2038 return 0;
2039 }
2040 iStringConstIterator iter;
2041 init_StringConstIterator(&iter, d);
2042 iChar icon = iter.value;
2043 next_StringConstIterator(&iter);
2044 if (iter.value == ' ' && icon >= 0x100) {
2045 remove_Block(&d->chars, 0, iter.next - constBegin_String(d));
2046 return icon;
2047 }
2048 return 0;
2049}
2050
2051iWidget *makeDialog_Widget(const char *id,
2052 const iMenuItem *itemsNullTerminated,
2053 const iMenuItem *actions, size_t numActions) {
2054 iWidget *dlg = makeSheet_Widget(id);
2055 /* TODO: Construct desktop dialogs using NULL-terminated item arrays, like mobile panels. */
2056 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
2057 addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, numActions)));
2058 addChild_Widget(dlg->root->widget, iClob(dlg));
2059 arrange_Widget(dlg);
2060 setupSheetTransition_Mobile(dlg, iTrue);
2061 return dlg;
2062}
2063
1631iWidget *makePreferences_Widget(void) { 2064iWidget *makePreferences_Widget(void) {
2065 /* Common items. */
2066 const iMenuItem langItems[] = { { "${lang.de} - de", 0, 0, "uilang id:de" },
2067 { "${lang.en} - en", 0, 0, "uilang id:en" },
2068 { "${lang.es} - es", 0, 0, "uilang id:es" },
2069 { "${lang.fi} - fi", 0, 0, "uilang id:fi" },
2070 { "${lang.fr} - fr", 0, 0, "uilang id:fr" },
2071 { "${lang.ia} - ia", 0, 0, "uilang id:ia" },
2072 { "${lang.ie} - ie", 0, 0, "uilang id:ie" },
2073 { "${lang.pl} - pl", 0, 0, "uilang id:pl" },
2074 { "${lang.ru} - ru", 0, 0, "uilang id:ru" },
2075 { "${lang.sr} - sr", 0, 0, "uilang id:sr" },
2076 { "${lang.tok} - tok", 0, 0, "uilang id:tok" },
2077 { "${lang.zh.hans} - zh", 0, 0, "uilang id:zh_Hans" },
2078 { "${lang.zh.hant} - zh", 0, 0, "uilang id:zh_Hant" },
2079 { NULL } };
2080 const iMenuItem returnKeyBehaviors[] = {
2081 { "${prefs.returnkey.linebreak} " uiTextAction_ColorEscape shift_Icon return_Icon
2082 restore_ColorEscape
2083 " ${prefs.returnkey.accept} " uiTextAction_ColorEscape return_Icon,
2084 0,
2085 0,
2086 format_CStr("returnkey.set arg:%d", default_ReturnKeyBehavior) },
2087 { "${prefs.returnkey.linebreak} " uiTextAction_ColorEscape return_Icon restore_ColorEscape
2088 " ${prefs.returnkey.accept} " uiTextAction_ColorEscape shift_Icon return_Icon,
2089 0,
2090 0,
2091 format_CStr("returnkey.set arg:%d", acceptWithShift_ReturnKeyBehavior) },
2092 { "${prefs.returnkey.linebreak} " uiTextAction_ColorEscape return_Icon restore_ColorEscape
2093 " ${prefs.returnkey.accept} " uiTextAction_ColorEscape
2094#if defined (iPlatformApple)
2095 "\u2318" return_Icon,
2096#else
2097 "Ctrl" return_Icon,
2098#endif
2099 0,
2100 0,
2101 format_CStr("returnkey.set arg:%d", acceptWithPrimaryMod_ReturnKeyBehavior) },
2102 { NULL }
2103 };
2104 iMenuItem docThemes[2][max_GmDocumentTheme + 1];
2105 for (int i = 0; i < 2; ++i) {
2106 const iBool isDark = (i == 0);
2107 const char *mode = isDark ? "dark" : "light";
2108 const iMenuItem items[max_GmDocumentTheme + 1] = {
2109 { "${prefs.doctheme.name.colorfuldark}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, colorfulDark_GmDocumentTheme) },
2110 { "${prefs.doctheme.name.colorfullight}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, colorfulLight_GmDocumentTheme) },
2111 { "${prefs.doctheme.name.black}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, black_GmDocumentTheme) },
2112 { "${prefs.doctheme.name.gray}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, gray_GmDocumentTheme) },
2113 { "${prefs.doctheme.name.white}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, white_GmDocumentTheme) },
2114 { "${prefs.doctheme.name.sepia}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, sepia_GmDocumentTheme) },
2115 { "${prefs.doctheme.name.highcontrast}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, highContrast_GmDocumentTheme) },
2116 { NULL }
2117 };
2118 memcpy(docThemes[i], items, sizeof(items));
2119 }
2120 const iMenuItem imgStyles[] = {
2121 { "${prefs.imagestyle.original}", 0, 0, format_CStr("imagestyle.set arg:%d", original_ImageStyle) },
2122 { "${prefs.imagestyle.grayscale}", 0, 0, format_CStr("imagestyle.set arg:%d", grayscale_ImageStyle) },
2123 { "${prefs.imagestyle.bgfg}", 0, 0, format_CStr("imagestyle.set arg:%d", bgFg_ImageStyle) },
2124 { "${prefs.imagestyle.text}", 0, 0, format_CStr("imagestyle.set arg:%d", textColorized_ImageStyle) },
2125 { "${prefs.imagestyle.preformat}", 0, 0, format_CStr("imagestyle.set arg:%d", preformatColorized_ImageStyle) },
2126 { NULL }
2127 };
2128 /* Create the Preferences UI. */
2129 if (isUsingPanelLayout_Mobile()) {
2130 const iMenuItem pinSplitItems[] = {
2131 { "button id:prefs.pinsplit.0 label:prefs.pinsplit.none", 0, 0, "pinsplit.set arg:0" },
2132 { "button id:prefs.pinsplit.1 label:prefs.pinsplit.left", 0, 0, "pinsplit.set arg:1" },
2133 { "button id:prefs.pinsplit.2 label:prefs.pinsplit.right", 0, 0, "pinsplit.set arg:2" },
2134 { NULL }
2135 };
2136 const iMenuItem themeItems[] = {
2137 { "button id:prefs.theme.0 label:prefs.theme.black", 0, 0, "theme.set arg:0" },
2138 { "button id:prefs.theme.1 label:prefs.theme.dark", 0, 0, "theme.set arg:1" },
2139 { "button id:prefs.theme.2 label:prefs.theme.light", 0, 0, "theme.set arg:2" },
2140 { "button id:prefs.theme.3 label:prefs.theme.white", 0, 0, "theme.set arg:3" },
2141 { NULL }
2142 };
2143 const iMenuItem accentItems[] = {
2144 { "button id:prefs.accent.0 label:prefs.accent.teal", 0, 0, "accent.set arg:0" },
2145 { "button id:prefs.accent.1 label:prefs.accent.orange", 0, 0, "accent.set arg:1" },
2146 { NULL }
2147 };
2148 const iMenuItem satItems[] = {
2149 { "button id:prefs.saturation.3 text:100 %", 0, 0, "saturation.set arg:100" },
2150 { "button id:prefs.saturation.2 text:66 %", 0, 0, "saturation.set arg:66" },
2151 { "button id:prefs.saturation.1 text:33 %", 0, 0, "saturation.set arg:33" },
2152 { "button id:prefs.saturation.0 text:0 %", 0, 0, "saturation.set arg:0" },
2153 { NULL }
2154 };
2155 const iMenuItem monoFontItems[] = {
2156 { "button id:prefs.mono.gemini" },
2157 { "button id:prefs.mono.gopher" },
2158 { NULL }
2159 };
2160 const iMenuItem boldLinkItems[] = {
2161 { "button id:prefs.boldlink.dark" },
2162 { "button id:prefs.boldlink.light" },
2163 { NULL }
2164 };
2165 const iMenuItem lineWidthItems[] = {
2166 { "button id:prefs.linewidth.30 text:\u20132", 0, 0, "linewidth.set arg:30" },
2167 { "button id:prefs.linewidth.34 text:\u20131", 0, 0, "linewidth.set arg:34" },
2168 { "button id:prefs.linewidth.38 label:prefs.linewidth.normal", 0, 0, "linewidth.set arg:38" },
2169 { "button id:prefs.linewidth.43 text:+1", 0, 0, "linewidth.set arg:43" },
2170 { "button id:prefs.linewidth.48 text:+2", 0, 0, "linewidth.set arg:48" },
2171 { "button id:prefs.linewidth.1000 label:prefs.linewidth.fill", 0, 0, "linewidth.set arg:1000" },
2172 { NULL }
2173 };
2174 const iMenuItem quoteItems[] = {
2175 { "button id:prefs.quoteicon.1 label:prefs.quoteicon.icon", 0, 0, "quoteicon.set arg:1" },
2176 { "button id:prefs.quoteicon.0 label:prefs.quoteicon.line", 0, 0, "quoteicon.set arg:0" },
2177 { NULL }
2178 };
2179 const iMenuItem generalPanelItems[] = {
2180 { "title id:heading.prefs.general" },
2181 { "heading text:${prefs.searchurl}" },
2182 { "input id:prefs.searchurl url:1 noheading:1" },
2183 { "padding" },
2184 { "toggle id:prefs.archive.openindex" },
2185 { "radio device:1 id:prefs.pinsplit", 0, 0, (const void *) pinSplitItems },
2186 { "padding" },
2187 { "dropdown id:prefs.uilang", 0, 0, (const void *) langItems },
2188 { NULL }
2189 };
2190 const iMenuItem uiPanelItems[] = {
2191 { "title id:heading.prefs.interface" },
2192 { "dropdown device:1 id:prefs.returnkey", 0, 0, (const void *) returnKeyBehaviors },
2193 { "padding device:1" },
2194 { "toggle id:prefs.hoverlink" },
2195 { "toggle device:2 id:prefs.hidetoolbarscroll" },
2196 { "heading id:heading.prefs.sizing" },
2197 { "input id:prefs.uiscale maxlen:8" },
2198 { NULL }
2199 };
2200 const iMenuItem colorPanelItems[] = {
2201 { "title id:heading.prefs.colors" },
2202 { "heading id:heading.prefs.uitheme" },
2203 { "toggle id:prefs.ostheme" },
2204 { "radio id:prefs.theme", 0, 0, (const void *) themeItems },
2205 { "radio id:prefs.accent", 0, 0, (const void *) accentItems },
2206 { "heading id:heading.prefs.pagecontent" },
2207 { "dropdown id:prefs.doctheme.dark", 0, 0, (const void *) docThemes[0] },
2208 { "dropdown id:prefs.doctheme.light", 0, 0, (const void *) docThemes[1] },
2209 { "radio id:prefs.saturation", 0, 0, (const void *) satItems },
2210 { "padding" },
2211 { "dropdown id:prefs.imagestyle", 0, 0, (const void *) imgStyles },
2212 { NULL }
2213 };
2214 const iMenuItem fontPanelItems[] = {
2215 { "title id:heading.prefs.fonts" },
2216 { "dropdown id:prefs.headingfont", 0, 0, (const void *) constData_Array(makeFontItems_("headingfont")) },
2217 { "dropdown id:prefs.font", 0, 0, (const void *) constData_Array(makeFontItems_("font")) },
2218 { "buttons id:prefs.mono", 0, 0, (const void *) monoFontItems },
2219 { "buttons id:prefs.boldlink", 0, 0, (const void *) boldLinkItems },
2220 { NULL }
2221 };
2222 const iMenuItem stylePanelItems[] = {
2223 { "title id:heading.prefs.style" },
2224 { "radio id:prefs.linewidth", 0, 0, (const void *) lineWidthItems },
2225 { "padding" },
2226 { "input id:prefs.linespacing maxlen:5" },
2227 { "radio id:prefs.quoteicon", 0, 0, (const void *) quoteItems },
2228 { "padding" },
2229 { "toggle id:prefs.biglede" },
2230 { "toggle id:prefs.plaintext.wrap" },
2231 { "toggle id:prefs.collapsepreonload" },
2232 { "toggle id:prefs.sideicon" },
2233 { "toggle id:prefs.centershort" },
2234 { NULL }
2235 };
2236 const iMenuItem networkPanelItems[] = {
2237 { "title id:heading.prefs.network" },
2238 { "toggle id:prefs.decodeurls" },
2239 { "padding" },
2240 { "input id:prefs.cachesize maxlen:4 selectall:1 unit:mb" },
2241 { "input id:prefs.memorysize maxlen:4 selectall:1 unit:mb" },
2242 { "heading text:${prefs.proxy.gemini}" },
2243 { "input id:prefs.proxy.gemini noheading:1" },
2244 { "heading text:${prefs.proxy.gopher}" },
2245 { "input id:prefs.proxy.gopher noheading:1" },
2246 { "heading text:${prefs.proxy.http}" },
2247 { "input id:prefs.proxy.http noheading:1" },
2248 { NULL }
2249 };
2250 const iMenuItem identityPanelItems[] = {
2251 { "title id:sidebar.identities" },
2252 { NULL }
2253 };
2254 iString *aboutText = collectNew_String(); {
2255 setCStr_String(aboutText, "Lagrange " LAGRANGE_APP_VERSION);
2256#if defined (iPlatformAppleMobile)
2257 appendFormat_String(aboutText, " (" LAGRANGE_IOS_VERSION ") %s" LAGRANGE_IOS_BUILD_DATE,
2258 escape_Color(uiTextDim_ColorId));
2259#endif
2260 }
2261 const iMenuItem aboutPanelItems[] = {
2262 { format_CStr("heading text:%s", cstr_String(aboutText)) },
2263 { "button text:" clock_Icon " ${menu.releasenotes}", 0, 0, "!open url:about:version" },
2264 { "button text:" globe_Icon " ${menu.website}", 0, 0, "!open url:https://gmi.skyjake.fi/lagrange" },
2265 { "button text:" envelope_Icon " @jk@skyjake.fi", 0, 0, "!open url:https://skyjake.fi/@jk" },
2266 { "padding" },
2267 { "button text:" info_Icon " ${menu.aboutpages}", 0, 0, "!open url:about:about" },
2268 { "button text:" bug_Icon " ${menu.debug}", 0, 0, "!open url:about:debug" },
2269 { NULL }
2270 };
2271 iWidget *dlg = makePanels_Mobile("prefs", (iMenuItem[]){
2272 { "title id:heading.settings" },
2273 { "panel text:" gear_Icon " ${heading.prefs.general}", 0, 0, (const void *) generalPanelItems },
2274 { "panel icon:0x1f5a7 id:heading.prefs.network", 0, 0, (const void *) networkPanelItems },
2275 { "panel text:" person_Icon " ${sidebar.identities}", 0, 0, (const void *) identityPanelItems },
2276 { "padding" },
2277 { "panel icon:0x1f4f1 id:heading.prefs.interface", 0, 0, (const void *) uiPanelItems },
2278 { "panel icon:0x1f3a8 id:heading.prefs.colors", 0, 0, (const void *) colorPanelItems },
2279 { "panel icon:0x1f5da id:heading.prefs.fonts", 0, 0, (const void *) fontPanelItems },
2280 { "panel icon:0x1f660 id:heading.prefs.style", 0, 0, (const void *) stylePanelItems },
2281 { "padding" },
2282 { "button text:" info_Icon " ${menu.help}", 0, 0, "!open url:about:help" },
2283 { "padding" },
2284 { "panel text:" planet_Icon " ${menu.about}", 0, 0, (const void *) aboutPanelItems },
2285 { NULL }
2286 }, NULL, 0);
2287 setupSheetTransition_Mobile(dlg, iTrue);
2288 return dlg;
2289 }
1632 iWidget *dlg = makeSheet_Widget("prefs"); 2290 iWidget *dlg = makeSheet_Widget("prefs");
1633 addChildFlags_Widget(dlg, 2291 addChildFlags_Widget(dlg,
1634 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.prefs}", NULL)), 2292 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.prefs}", NULL)),
@@ -1637,7 +2295,6 @@ iWidget *makePreferences_Widget(void) {
1637 setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); 2295 setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId);
1638 setId_Widget(tabs, "prefs.tabs"); 2296 setId_Widget(tabs, "prefs.tabs");
1639 iWidget *headings, *values; 2297 iWidget *headings, *values;
1640 const int bigGap = lineHeight_Text(uiLabel_FontId) * 3 / 4;
1641 /* General preferences. */ { 2298 /* General preferences. */ {
1642 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.general}", '1', &headings, &values); 2299 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.general}", '1', &headings, &values);
1643#if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT) 2300#if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
@@ -1646,12 +2303,9 @@ iWidget *makePreferences_Widget(void) {
1646 iInputWidget *searchUrl; 2303 iInputWidget *searchUrl;
1647 addPrefsInputWithHeading_(headings, values, "prefs.searchurl", iClob(searchUrl = new_InputWidget(0))); 2304 addPrefsInputWithHeading_(headings, values, "prefs.searchurl", iClob(searchUrl = new_InputWidget(0)));
1648 setUrlContent_InputWidget(searchUrl, iTrue); 2305 setUrlContent_InputWidget(searchUrl, iTrue);
1649 addChild_Widget(headings, iClob(makePadding_Widget(bigGap))); 2306 addDialogPadding_(headings, values);
1650 addChild_Widget(values, iClob(makePadding_Widget(bigGap))); 2307 addDialogToggle_(headings, values, "${prefs.hoverlink}", "prefs.hoverlink");
1651 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.hoverlink}"))); 2308 addDialogToggle_(headings, values, "${prefs.archive.openindex}", "prefs.archive.openindex");
1652 addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoverlink")));
1653 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.archive.openindex}")));
1654 addChild_Widget(values, iClob(makeToggle_Widget("prefs.archive.openindex")));
1655 if (deviceType_App() != phone_AppDeviceType) { 2309 if (deviceType_App() != phone_AppDeviceType) {
1656 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.pinsplit}"))); 2310 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.pinsplit}")));
1657 iWidget *pinSplit = new_Widget(); 2311 iWidget *pinSplit = new_Widget();
@@ -1662,28 +2316,12 @@ iWidget *makePreferences_Widget(void) {
1662 } 2316 }
1663 addChildFlags_Widget(values, iClob(pinSplit), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 2317 addChildFlags_Widget(values, iClob(pinSplit), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1664 } 2318 }
1665 addChild_Widget(headings, iClob(makePadding_Widget(bigGap))); 2319 addDialogPadding_(headings, values);
1666 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
1667 /* UI languages. */ { 2320 /* UI languages. */ {
1668 iArray *uiLangs = collectNew_Array(sizeof(iMenuItem)); 2321 iArray *uiLangs = collectNew_Array(sizeof(iMenuItem));
1669 const iMenuItem langItems[] = { 2322 pushBackN_Array(uiLangs, langItems, iElemCount(langItems) - 1);
1670 { "${lang.de} - de", 0, 0, "uilang id:de" },
1671 { "${lang.en} - en", 0, 0, "uilang id:en" },
1672 { "${lang.es} - es", 0, 0, "uilang id:es" },
1673 { "${lang.fi} - fi", 0, 0, "uilang id:fi" },
1674 { "${lang.fr} - fr", 0, 0, "uilang id:fr" },
1675 { "${lang.ia} - ia", 0, 0, "uilang id:ia" },
1676 { "${lang.ie} - ie", 0, 0, "uilang id:ie" },
1677 { "${lang.pl} - pl", 0, 0, "uilang id:pl" },
1678 { "${lang.ru} - ru", 0, 0, "uilang id:ru" },
1679 { "${lang.sr} - sr", 0, 0, "uilang id:sr" },
1680 { "${lang.tok} - tok", 0, 0, "uilang id:tok" },
1681 { "${lang.zh.hans} - zh", 0, 0, "uilang id:zh_Hans" },
1682 { "${lang.zh.hant} - zh", 0, 0, "uilang id:zh_Hant" },
1683 };
1684 pushBackN_Array(uiLangs, langItems, iElemCount(langItems));
1685 /* TODO: Add an arrange flag for resizing parent to widest child. */ 2323 /* TODO: Add an arrange flag for resizing parent to widest child. */
1686 size_t widestPos = findWidestItemLabel_(data_Array(uiLangs), size_Array(uiLangs)); 2324 size_t widestPos = findWidestLabel_MenuItem(data_Array(uiLangs), size_Array(uiLangs));
1687 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.uilang}"))); 2325 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.uilang}")));
1688 setId_Widget(addChildFlags_Widget(values, 2326 setId_Widget(addChildFlags_Widget(values,
1689 iClob(makeMenuButton_LabelWidget( 2327 iClob(makeMenuButton_LabelWidget(
@@ -1697,54 +2335,24 @@ iWidget *makePreferences_Widget(void) {
1697 /* User Interface. */ { 2335 /* User Interface. */ {
1698 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.interface}", '2', &headings, &values); 2336 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.interface}", '2', &headings, &values);
1699#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 2337#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
1700 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.customframe}"))); 2338 addDialogToggle_(headings, values, "${prefs.customframe}", "prefs.customframe");
1701 addChild_Widget(values, iClob(makeToggle_Widget("prefs.customframe")));
1702#endif 2339#endif
1703 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.returnkey}"))); 2340 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.returnkey}")));
1704 /* Return key behaviors. */ { 2341 /* Return key behaviors. */ {
1705 const iMenuItem returnKeyBehaviors[] = {
1706 { "${prefs.returnkey.linebreak} "
1707 uiTextAction_ColorEscape shift_Icon return_Icon restore_ColorEscape
1708 " ${prefs.returnkey.accept} "
1709 uiTextAction_ColorEscape return_Icon,
1710 0,
1711 0,
1712 format_CStr("returnkey.set arg:%d", default_ReturnKeyBehavior) },
1713 { "${prefs.returnkey.linebreak} "
1714 uiTextAction_ColorEscape return_Icon restore_ColorEscape
1715 " ${prefs.returnkey.accept} "
1716 uiTextAction_ColorEscape shift_Icon return_Icon,
1717 0,
1718 0,
1719 format_CStr("returnkey.set arg:%d", acceptWithShift_ReturnKeyBehavior) },
1720 { "${prefs.returnkey.linebreak} "
1721 uiTextAction_ColorEscape return_Icon restore_ColorEscape
1722 " ${prefs.returnkey.accept} " uiTextAction_ColorEscape
1723#if defined (iPlatformApple)
1724 "\u2318" return_Icon,
1725#else
1726 "Ctrl" return_Icon,
1727#endif
1728 0,
1729 0,
1730 format_CStr("returnkey.set arg:%d", acceptWithPrimaryMod_ReturnKeyBehavior) },
1731 };
1732 iLabelWidget *returnKey = makeMenuButton_LabelWidget( 2342 iLabelWidget *returnKey = makeMenuButton_LabelWidget(
1733 returnKeyBehaviors[findWidestItemLabel_(returnKeyBehaviors, 2343 returnKeyBehaviors[findWidestLabel_MenuItem(returnKeyBehaviors,
1734 iElemCount(returnKeyBehaviors))] 2344 iElemCount(returnKeyBehaviors) - 1)]
1735 .label, 2345 .label,
1736 returnKeyBehaviors, 2346 returnKeyBehaviors,
1737 iElemCount(returnKeyBehaviors)); 2347 iElemCount(returnKeyBehaviors) - 1);
1738 setBackgroundColor_Widget(findChild_Widget(as_Widget(returnKey), "menu"), 2348 setBackgroundColor_Widget(findChild_Widget(as_Widget(returnKey), "menu"),
1739 uiBackgroundMenu_ColorId); 2349 uiBackgroundMenu_ColorId);
1740 setId_Widget(addChildFlags_Widget(values, iClob(returnKey), alignLeft_WidgetFlag), 2350 setId_Widget(addChildFlags_Widget(values, iClob(returnKey), alignLeft_WidgetFlag),
1741 "prefs.returnkey"); 2351 "prefs.returnkey");
1742 } 2352 }
1743 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.animate}"))); 2353 addDialogToggle_(headings, values, "${prefs.animate}", "prefs.animate");
1744 addChild_Widget(values, iClob(makeToggle_Widget("prefs.animate")));
1745 makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values); 2354 makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values);
1746 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.smoothscroll}"))); 2355 addDialogToggle_(headings, values, "${prefs.smoothscroll}", "prefs.smoothscroll");
1747 addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll")));
1748 /* Scroll speeds. */ { 2356 /* Scroll speeds. */ {
1749 for (int type = 0; type < max_ScrollType; type++) { 2357 for (int type = 0; type < max_ScrollType; type++) {
1750 const char *typeStr = (type == mouse_ScrollType ? "mouse" : "keyboard"); 2358 const char *typeStr = (type == mouse_ScrollType ? "mouse" : "keyboard");
@@ -1764,25 +2372,21 @@ iWidget *makePreferences_Widget(void) {
1764 values, iClob(scrollSpeed), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 2372 values, iClob(scrollSpeed), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1765 } 2373 }
1766 } 2374 }
1767 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.imageloadscroll}"))); 2375 addDialogToggle_(headings, values, "${prefs.imageloadscroll}", "prefs.imageloadscroll");
1768 addChild_Widget(values, iClob(makeToggle_Widget("prefs.imageloadscroll")));
1769 if (deviceType_App() == phone_AppDeviceType) { 2376 if (deviceType_App() == phone_AppDeviceType) {
1770 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.hidetoolbarscroll}"))); 2377 addDialogToggle_(headings, values, "${prefs.hidetoolbarscroll}", "prefs.hidetoolbarscroll");
1771 addChild_Widget(values, iClob(makeToggle_Widget("prefs.hidetoolbarscroll")));
1772 } 2378 }
1773 makeTwoColumnHeading_("${heading.prefs.sizing}", headings, values); 2379 makeTwoColumnHeading_("${heading.prefs.sizing}", headings, values);
1774 addPrefsInputWithHeading_(headings, values, "prefs.uiscale", iClob(new_InputWidget(8))); 2380 addPrefsInputWithHeading_(headings, values, "prefs.uiscale", iClob(new_InputWidget(8)));
1775 if (deviceType_App() == desktop_AppDeviceType) { 2381 if (deviceType_App() == desktop_AppDeviceType) {
1776 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.retainwindow}"))); 2382 addDialogToggle_(headings, values, "${prefs.retainwindow}", "prefs.retainwindow");
1777 addChild_Widget(values, iClob(makeToggle_Widget("prefs.retainwindow")));
1778 } 2383 }
1779 } 2384 }
1780 /* Colors. */ { 2385 /* Colors. */ {
1781 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.colors}", '3', &headings, &values); 2386 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.colors}", '3', &headings, &values);
1782 makeTwoColumnHeading_("${heading.prefs.uitheme}", headings, values); 2387 makeTwoColumnHeading_("${heading.prefs.uitheme}", headings, values);
1783#if defined (iPlatformApple) || defined (iPlatformMSys) 2388#if defined (iPlatformApple) || defined (iPlatformMSys)
1784 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.ostheme}"))); 2389 addDialogToggle_(headings, values, "${prefs.ostheme}", "prefs.ostheme");
1785 addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme")));
1786#endif 2390#endif
1787 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.theme}"))); 2391 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.theme}")));
1788 iWidget *themes = new_Widget(); 2392 iWidget *themes = new_Widget();
@@ -1804,20 +2408,13 @@ iWidget *makePreferences_Widget(void) {
1804 for (int i = 0; i < 2; ++i) { 2408 for (int i = 0; i < 2; ++i) {
1805 const iBool isDark = (i == 0); 2409 const iBool isDark = (i == 0);
1806 const char *mode = isDark ? "dark" : "light"; 2410 const char *mode = isDark ? "dark" : "light";
1807 const iMenuItem themes[] = {
1808 { "${prefs.doctheme.name.colorfuldark}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, colorfulDark_GmDocumentTheme) },
1809 { "${prefs.doctheme.name.colorfullight}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, colorfulLight_GmDocumentTheme) },
1810 { "${prefs.doctheme.name.black}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, black_GmDocumentTheme) },
1811 { "${prefs.doctheme.name.gray}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, gray_GmDocumentTheme) },
1812 { "${prefs.doctheme.name.white}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, white_GmDocumentTheme) },
1813 { "${prefs.doctheme.name.sepia}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, sepia_GmDocumentTheme) },
1814 { "${prefs.doctheme.name.highcontrast}", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, highContrast_GmDocumentTheme) },
1815 };
1816 addChild_Widget(headings, iClob(makeHeading_Widget(isDark ? "${prefs.doctheme.dark}" : "${prefs.doctheme.light}"))); 2411 addChild_Widget(headings, iClob(makeHeading_Widget(isDark ? "${prefs.doctheme.dark}" : "${prefs.doctheme.light}")));
1817 iLabelWidget *button = 2412 iLabelWidget *button = makeMenuButton_LabelWidget(
1818 makeMenuButton_LabelWidget(themes[1].label, themes, iElemCount(themes)); 2413 docThemes[i][findWidestLabel_MenuItem(docThemes[i], max_GmDocumentTheme)].label,
1819// setFrameColor_Widget(findChild_Widget(as_Widget(button), "menu"), 2414 docThemes[i],
1820// uiBackgroundSelected_ColorId); 2415 max_GmDocumentTheme);
2416 // setFrameColor_Widget(findChild_Widget(as_Widget(button), "menu"),
2417 // uiBackgroundSelected_ColorId);
1821 setBackgroundColor_Widget(findChild_Widget(as_Widget(button), "menu"), uiBackgroundMenu_ColorId); 2418 setBackgroundColor_Widget(findChild_Widget(as_Widget(button), "menu"), uiBackgroundMenu_ColorId);
1822 setId_Widget(addChildFlags_Widget(values, iClob(button), alignLeft_WidgetFlag), 2419 setId_Widget(addChildFlags_Widget(values, iClob(button), alignLeft_WidgetFlag),
1823 format_CStr("prefs.doctheme.%s", mode)); 2420 format_CStr("prefs.doctheme.%s", mode));
@@ -1831,6 +2428,17 @@ iWidget *makePreferences_Widget(void) {
1831 addRadioButton_(sats, "prefs.saturation.0", "0 %", "saturation.set arg:0"); 2428 addRadioButton_(sats, "prefs.saturation.0", "0 %", "saturation.set arg:0");
1832 } 2429 }
1833 addChildFlags_Widget(values, iClob(sats), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 2430 addChildFlags_Widget(values, iClob(sats), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
2431 /* Colorize images. */ {
2432 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.imagestyle}")));
2433 iLabelWidget *button = makeMenuButton_LabelWidget(
2434 imgStyles[findWidestLabel_MenuItem(imgStyles, iElemCount(imgStyles) - 1)].label,
2435 imgStyles,
2436 iElemCount(imgStyles) - 1);
2437 setBackgroundColor_Widget(findChild_Widget(as_Widget(button), "menu"),
2438 uiBackgroundMenu_ColorId);
2439 setId_Widget(addChildFlags_Widget(values, iClob(button), alignLeft_WidgetFlag),
2440 "prefs.imagestyle");
2441 }
1834 } 2442 }
1835 /* Fonts. */ { 2443 /* Fonts. */ {
1836 setId_Widget(appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.fonts}", '4', &headings, &values), "prefs.page.fonts"); 2444 setId_Widget(appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.fonts}", '4', &headings, &values), "prefs.page.fonts");
@@ -1839,8 +2447,7 @@ iWidget *makePreferences_Widget(void) {
1839 addFontButtons_(values, "headingfont"); 2447 addFontButtons_(values, "headingfont");
1840 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.font}"))); 2448 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.font}")));
1841 addFontButtons_(values, "font"); 2449 addFontButtons_(values, "font");
1842 addChild_Widget(headings, iClob(makePadding_Widget(bigGap))); 2450 addDialogPadding_(headings, values);
1843 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
1844 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.mono}"))); 2451 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.mono}")));
1845 iWidget *mono = new_Widget(); { 2452 iWidget *mono = new_Widget(); {
1846 iWidget *tog; 2453 iWidget *tog;
@@ -1872,8 +2479,7 @@ iWidget *makePreferences_Widget(void) {
1872 updateSize_LabelWidget((iLabelWidget *) tog); 2479 updateSize_LabelWidget((iLabelWidget *) tog);
1873 } 2480 }
1874 addChildFlags_Widget(values, iClob(boldLink), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 2481 addChildFlags_Widget(values, iClob(boldLink), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1875 addChild_Widget(headings, iClob(makePadding_Widget(bigGap))); 2482 addDialogPadding_(headings, values);
1876 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
1877 /* Custom font. */ { 2483 /* Custom font. */ {
1878 iInputWidget *customFont = new_InputWidget(0); 2484 iInputWidget *customFont = new_InputWidget(0);
1879 setHint_InputWidget(customFont, "${hint.prefs.userfont}"); 2485 setHint_InputWidget(customFont, "${hint.prefs.userfont}");
@@ -1902,19 +2508,12 @@ iWidget *makePreferences_Widget(void) {
1902 addRadioButton_(quote, "prefs.quoteicon.0", "${prefs.quoteicon.line}", "quoteicon.set arg:0"); 2508 addRadioButton_(quote, "prefs.quoteicon.0", "${prefs.quoteicon.line}", "quoteicon.set arg:0");
1903 } 2509 }
1904 addChildFlags_Widget(values, iClob(quote), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 2510 addChildFlags_Widget(values, iClob(quote), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1905 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.biglede}"))); 2511 addDialogToggle_(headings, values, "${prefs.biglede}", "prefs.biglede");
1906 addChild_Widget(values, iClob(makeToggle_Widget("prefs.biglede"))); 2512 addDialogToggle_(headings, values, "${prefs.plaintext.wrap}", "prefs.plaintext.wrap");
1907 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.plaintext.wrap}"))); 2513 addDialogToggle_(headings, values, "${prefs.collapsepreonload}", "prefs.collapsepreonload");
1908 addChild_Widget(values, iClob(makeToggle_Widget("prefs.plaintext.wrap"))); 2514 addDialogPadding_(headings, values);
1909 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.collapsepreonload}"))); 2515 addDialogToggle_(headings, values, "${prefs.sideicon}", "prefs.sideicon");
1910 addChild_Widget(values, iClob(makeToggle_Widget("prefs.collapsepreonload"))); 2516 addDialogToggle_(headings, values, "${prefs.centershort}", "prefs.centershort");
1911// makeTwoColumnHeading_("${heading.prefs.widelayout}", headings, values);
1912 addChild_Widget(headings, iClob(makePadding_Widget(bigGap)));
1913 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
1914 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.sideicon}")));
1915 addChild_Widget(values, iClob(makeToggle_Widget("prefs.sideicon")));
1916 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.centershort}")));
1917 addChild_Widget(values, iClob(makeToggle_Widget("prefs.centershort")));
1918 } 2517 }
1919 /* Network. */ { 2518 /* Network. */ {
1920 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.network}", '6', &headings, &values); 2519 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.network}", '6', &headings, &values);
@@ -1961,13 +2560,37 @@ iWidget *makePreferences_Widget(void) {
1961 iClob(makeDialogButtons_Widget( 2560 iClob(makeDialogButtons_Widget(
1962 (iMenuItem[]){ { "${close}", SDLK_ESCAPE, 0, "prefs.dismiss" } }, 1))); 2561 (iMenuItem[]){ { "${close}", SDLK_ESCAPE, 0, "prefs.dismiss" } }, 1)));
1963 addChild_Widget(dlg->root->widget, iClob(dlg)); 2562 addChild_Widget(dlg->root->widget, iClob(dlg));
1964 finalizeSheet_Mobile(dlg); 2563// finalizeSheet_Mobile(dlg);
2564// arrange_Widget(dlg);
1965 setupSheetTransition_Mobile(dlg, iTrue); 2565 setupSheetTransition_Mobile(dlg, iTrue);
1966// printTree_Widget(dlg); 2566// printTree_Widget(dlg);
1967 return dlg; 2567 return dlg;
1968} 2568}
1969 2569
1970iWidget *makeBookmarkEditor_Widget(void) { 2570iWidget *makeBookmarkEditor_Widget(void) {
2571 const iMenuItem actions[] = {
2572 { "${cancel}" },
2573 { uiTextCaution_ColorEscape "${dlg.bookmark.save}", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept" }
2574 };
2575 if (isUsingPanelLayout_Mobile()) {
2576 const iMenuItem items[] = {
2577 { "title id:bmed.heading text:${heading.bookmark.edit}" },
2578 { "heading id:dlg.bookmark.url" },
2579 { "input id:bmed.url url:1 noheading:1" },
2580 { "padding" },
2581 { "input id:bmed.title text:${dlg.bookmark.title}" },
2582 { "input id:bmed.tags text:${dlg.bookmark.tags}" },
2583 { "input id:bmed.icon maxlen:1 text:${dlg.bookmark.icon}" },
2584 { "heading text:${heading.bookmark.tags}" },
2585 { "toggle id:bmed.tag.home text:${bookmark.tag.home}" },
2586 { "toggle id:bmed.tag.remote text:${bookmark.tag.remote}" },
2587 { "toggle id:bmed.tag.linksplit text:${bookmark.tag.linksplit}" },
2588 { NULL }
2589 };
2590 iWidget *dlg = makePanels_Mobile("bmed", items, actions, iElemCount(actions));
2591 setupSheetTransition_Mobile(dlg, iTrue);
2592 return dlg;
2593 }
1971 iWidget *dlg = makeSheet_Widget("bmed"); 2594 iWidget *dlg = makeSheet_Widget("bmed");
1972 setId_Widget(addChildFlags_Widget( 2595 setId_Widget(addChildFlags_Widget(
1973 dlg, 2596 dlg,
@@ -1985,28 +2608,18 @@ iWidget *makeBookmarkEditor_Widget(void) {
1985 /* Buttons for special tags. */ 2608 /* Buttons for special tags. */
1986 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); 2609 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
1987 addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); 2610 addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values)));
1988 makeTwoColumnHeading_("SPECIAL TAGS", headings, values); 2611 makeTwoColumnHeading_("${heading.bookmark.tags}", headings, values);
1989 addChild_Widget(headings, iClob(makeHeading_Widget("${bookmark.tag.home}"))); 2612 addDialogToggle_(headings, values, "${bookmark.tag.home}", "bmed.tag.home");
1990 addChild_Widget(values, iClob(makeToggle_Widget("bmed.tag.home"))); 2613 addDialogToggle_(headings, values, "${bookmark.tag.remote}", "bmed.tag.remote");
1991 addChild_Widget(headings, iClob(makeHeading_Widget("${bookmark.tag.remote}"))); 2614 addDialogToggle_(headings, values, "${bookmark.tag.linksplit}", "bmed.tag.linksplit");
1992 addChild_Widget(values, iClob(makeToggle_Widget("bmed.tag.remote")));
1993 addChild_Widget(headings, iClob(makeHeading_Widget("${bookmark.tag.linksplit}")));
1994 addChild_Widget(values, iClob(makeToggle_Widget("bmed.tag.linksplit")));
1995 arrange_Widget(dlg); 2615 arrange_Widget(dlg);
1996 for (int i = 0; i < 3; ++i) { 2616 for (int i = 0; i < 3; ++i) {
1997 as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x; 2617 as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x;
1998 } 2618 }
1999 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); 2619 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
2000 addChild_Widget( 2620 addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions))));
2001 dlg,
2002 iClob(makeDialogButtons_Widget((iMenuItem[]){ { "${cancel}", 0, 0, NULL },
2003 { uiTextCaution_ColorEscape "${dlg.bookmark.save}",
2004 SDLK_RETURN,
2005 KMOD_PRIMARY,
2006 "bmed.accept" } },
2007 2)));
2008 addChild_Widget(get_Root()->widget, iClob(dlg)); 2621 addChild_Widget(get_Root()->widget, iClob(dlg));
2009 finalizeSheet_Mobile(dlg); 2622 setupSheetTransition_Mobile(dlg, iTrue);
2010 return dlg; 2623 return dlg;
2011} 2624}
2012 2625
@@ -2060,7 +2673,6 @@ iWidget *makeBookmarkCreation_Widget(const iString *url, const iString *title, i
2060 return dlg; 2673 return dlg;
2061} 2674}
2062 2675
2063
2064static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) { 2676static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
2065 if (equal_Command(cmd, "cancel")) { 2677 if (equal_Command(cmd, "cancel")) {
2066 setupSheetTransition_Mobile(dlg, iFalse); 2678 setupSheetTransition_Mobile(dlg, iFalse);
@@ -2106,132 +2718,174 @@ static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
2106} 2718}
2107 2719
2108iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) { 2720iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) {
2109 iWidget *dlg = makeSheet_Widget("feedcfg"); 2721 const char *headingText = bookmarkId ? uiHeading_ColorEscape "${heading.feedcfg}"
2110 setId_Widget(addChildFlags_Widget( 2722 : uiHeading_ColorEscape "${heading.subscribe}";
2111 dlg, 2723 const iMenuItem actions[] = { { "${cancel}" },
2112 iClob(new_LabelWidget(bookmarkId ? uiHeading_ColorEscape "${heading.feedcfg}" 2724 { bookmarkId ? uiTextCaution_ColorEscape "${dlg.feed.save}"
2113 : uiHeading_ColorEscape "${heading.subscribe}", 2725 : uiTextCaution_ColorEscape "${dlg.feed.sub}",
2114 NULL)), 2726 SDLK_RETURN,
2115 frameless_WidgetFlag), 2727 KMOD_PRIMARY,
2116 "feedcfg.heading"); 2728 format_CStr("feedcfg.accept bmid:%d", bookmarkId) } };
2117 iWidget *headings, *values; 2729 iWidget *dlg;
2118 addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); 2730 if (isUsingPanelLayout_Mobile()) {
2119 iInputWidget *input = new_InputWidget(0); 2731 const iMenuItem typeItems[] = {
2120 addDialogInputWithHeading_(headings, values, "${dlg.feed.title}", "feedcfg.title", iClob(input)); 2732 { "button id:feedcfg.type.gemini label:dlg.feed.type.gemini", 0, 0, "feedcfg.type arg:0" },
2121 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.feed.entrytype}"))); 2733 { "button id:feedcfg.type.headings label:dlg.feed.type.headings", 0, 0, "feedcfg.type arg:1" },
2122 iWidget *types = new_Widget(); { 2734 { NULL }
2123 addRadioButton_(types, "feedcfg.type.gemini", "${dlg.feed.type.gemini}", "feedcfg.type arg:0"); 2735 };
2124 addRadioButton_(types, "feedcfg.type.headings", "${dlg.feed.type.headings}", "feedcfg.type arg:1"); 2736 dlg = makePanels_Mobile("feedcfg", (iMenuItem[]){
2125 } 2737 { format_CStr("title id:feedcfg.heading text:%s", headingText) },
2126 addChildFlags_Widget(values, iClob(types), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 2738 { "input id:feedcfg.title text:${dlg.feed.title}" },
2127 iWidget *buttons = 2739 { "radio id:dlg.feed.entrytype", 0, 0, (const void *) typeItems },
2128 addChild_Widget(dlg, 2740 { NULL }
2129 iClob(makeDialogButtons_Widget( 2741 }, actions, iElemCount(actions));
2130 (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, 2742 }
2131 { bookmarkId ? uiTextCaution_ColorEscape "${dlg.feed.save}" 2743 else {
2132 : uiTextCaution_ColorEscape "${dlg.feed.sub}", 2744 dlg = makeSheet_Widget("feedcfg");
2133 SDLK_RETURN, 2745 setId_Widget(
2134 KMOD_PRIMARY, 2746 addChildFlags_Widget(dlg, iClob(new_LabelWidget(headingText, NULL)), frameless_WidgetFlag),
2135 format_CStr("feedcfg.accept bmid:%d", bookmarkId) } }, 2747 "feedcfg.heading");
2136 2))); 2748 iWidget *headings, *values;
2137 setId_Widget(child_Widget(buttons, childCount_Widget(buttons) - 1), "feedcfg.save"); 2749 addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values)));
2138 arrange_Widget(dlg); 2750 iInputWidget *input = new_InputWidget(0);
2139 as_Widget(input)->rect.size.x = 100 * gap_UI - headings->rect.size.x; 2751 addDialogInputWithHeading_(headings, values, "${dlg.feed.title}", "feedcfg.title", iClob(input));
2140 addChild_Widget(get_Root()->widget, iClob(dlg)); 2752 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.feed.entrytype}")));
2141 finalizeSheet_Mobile(dlg); 2753 iWidget *types = new_Widget(); {
2754 addRadioButton_(types, "feedcfg.type.gemini", "${dlg.feed.type.gemini}", "feedcfg.type arg:0");
2755 addRadioButton_(types, "feedcfg.type.headings", "${dlg.feed.type.headings}", "feedcfg.type arg:1");
2756 }
2757 addChildFlags_Widget(values, iClob(types), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
2758 iWidget *buttons =
2759 addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions))));
2760 setId_Widget(child_Widget(buttons, childCount_Widget(buttons) - 1), "feedcfg.save");
2761 arrange_Widget(dlg);
2762 as_Widget(input)->rect.size.x = 100 * gap_UI - headings->rect.size.x;
2763 addChild_Widget(get_Root()->widget, iClob(dlg));
2764// finalizeSheet_Mobile(dlg);
2765 }
2142 /* Initialize. */ { 2766 /* Initialize. */ {
2143 const iBookmark *bm = bookmarkId ? get_Bookmarks(bookmarks_App(), bookmarkId) : NULL; 2767 const iBookmark *bm = bookmarkId ? get_Bookmarks(bookmarks_App(), bookmarkId) : NULL;
2144 setText_InputWidget(findChild_Widget(dlg, "feedcfg.title"), 2768 setText_InputWidget(findChild_Widget(dlg, "feedcfg.title"),
2145 bm ? &bm->title : feedTitle_DocumentWidget(document_App())); 2769 bm ? &bm->title : feedTitle_DocumentWidget(document_App()));
2146 setFlags_Widget(findChild_Widget(dlg, 2770 setFlags_Widget(findChild_Widget(dlg,
2147 hasTag_Bookmark(bm, headings_BookmarkTag) ? "feedcfg.type.headings" 2771 hasTag_Bookmark(bm, headings_BookmarkTag)
2148 : "feedcfg.type.gemini"), 2772 ? "feedcfg.type.headings"
2773 : "feedcfg.type.gemini"),
2149 selected_WidgetFlag, 2774 selected_WidgetFlag,
2150 iTrue); 2775 iTrue);
2151 setCommandHandler_Widget(dlg, handleFeedSettingCommands_); 2776 setCommandHandler_Widget(dlg, handleFeedSettingCommands_);
2152 } 2777 }
2778 setupSheetTransition_Mobile(dlg, incoming_TransitionFlag);
2153 return dlg; 2779 return dlg;
2154} 2780}
2155 2781
2156iWidget *makeIdentityCreation_Widget(void) { 2782iWidget *makeIdentityCreation_Widget(void) {
2157 iWidget *dlg = makeSheet_Widget("ident"); 2783 const iMenuItem actions[] = { { "${dlg.newident.more}", 0, 0, "ident.showmore" },
2158 setId_Widget(addChildFlags_Widget( 2784 { "---" },
2159 dlg, 2785 { "${cancel}", SDLK_ESCAPE, 0, "ident.cancel" },
2160 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.newident}", NULL)), 2786 { uiTextAction_ColorEscape "${dlg.newident.create}",
2161 frameless_WidgetFlag), 2787 SDLK_RETURN,
2162 "ident.heading"); 2788 KMOD_PRIMARY,
2163 iWidget *page = new_Widget(); 2789 "ident.accept" } };
2164 addChildFlags_Widget( 2790 iUrl url;
2165 dlg, iClob(new_LabelWidget("${dlg.newident.rsa.selfsign}", NULL)), frameless_WidgetFlag); 2791 init_Url(&url, url_DocumentWidget(document_App()));
2166 /* TODO: Use makeTwoColumnWidget_? */ 2792 const iMenuItem scopeItems[] = {
2167 addChild_Widget(dlg, iClob(page)); 2793 { format_CStr("${dlg.newident.scope.domain}:\n%s", cstr_Rangecc(url.host)), 0, 0, "ident.scope arg:0" },
2168 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); 2794 { format_CStr("${dlg.newident.scope.page}:\n%s", cstr_Rangecc(url.path)), 0, 0, "ident.scope arg:1" },
2169 iWidget *headings = addChildFlags_Widget( 2795 { "${dlg.newident.scope.none}", 0, 0, "ident.scope arg:2" },
2170 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 2796 { NULL }
2171 iWidget *values = addChildFlags_Widget( 2797 };
2172 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 2798 iWidget *dlg;
2173 setId_Widget(headings, "headings"); 2799 if (isUsingPanelLayout_Mobile()) {
2174 setId_Widget(values, "values"); 2800 dlg = makePanels_Mobile("ident", (iMenuItem[]){
2175 iInputWidget *inputs[6]; 2801 { "title id:ident.heading text:${heading.newident}" },
2176 /* Where will the new identity be active on? */ { 2802 { "label text:${dlg.newident.rsa.selfsign}" },
2177 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.newident.scope}"))); 2803 { "dropdown id:ident.scope text:${dlg.newident.scope}", 0, 0,
2178 const iMenuItem items[] = { 2804 (const void *) scopeItems },
2179 { "${dlg.newident.scope.domain}", 0, 0, "ident.scope arg:0" }, 2805 { "input id:ident.until hint:hint.newident.date maxlen:19 text:${dlg.newident.until}" },
2180 { "${dlg.newident.scope.page}", 0, 0, "ident.scope arg:1" }, 2806 //{ "padding" },
2181 { "${dlg.newident.scope.none}", 0, 0, "ident.scope arg:2" }, 2807 //{ "toggle id:ident.temp text:${dlg.newident.temp}" },
2182 }; 2808 //{ "label text:${help.ident.temp}" },
2183 setId_Widget(addChild_Widget(values, 2809 { "heading id:dlg.newident.commonname" },
2184 iClob(makeMenuButton_LabelWidget( 2810 { "input id:ident.common noheading:1" },
2185 items[0].label, items, iElemCount(items)))), 2811 { "padding collapse:1" },
2186 "ident.scope"); 2812 { "input collapse:1 id:ident.email hint:hint.newident.optional text:${dlg.newident.email}" },
2187 } 2813 { "input collapse:1 id:ident.userid hint:hint.newident.optional text:${dlg.newident.userid}" },
2188 addDialogInputWithHeading_(headings, 2814 { "input collapse:1 id:ident.domain hint:hint.newident.optional text:${dlg.newident.domain}" },
2189 values, 2815 { "input collapse:1 id:ident.org hint:hint.newident.optional text:${dlg.newident.org}" },
2190 "${dlg.newident.until}", 2816 { "input collapse:1 id:ident.country hint:hint.newident.optional text:${dlg.newident.country}" },
2191 "ident.until", 2817 { NULL }
2192 iClob(newHint_InputWidget(19, "${hint.newident.date}"))); 2818 }, actions, iElemCount(actions));
2193 addDialogInputWithHeading_(headings,
2194 values,
2195 "${dlg.newident.commonname}",
2196 "ident.common",
2197 iClob(inputs[0] = new_InputWidget(0)));
2198 /* Temporary? */ {
2199 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.newident.temp}")));
2200 iWidget *tmpGroup = new_Widget();
2201 setFlags_Widget(tmpGroup, arrangeSize_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue);
2202 addChild_Widget(tmpGroup, iClob(makeToggle_Widget("ident.temp")));
2203 setId_Widget(
2204 addChildFlags_Widget(tmpGroup,
2205 iClob(new_LabelWidget(uiTextCaution_ColorEscape warning_Icon
2206 " ${dlg.newident.notsaved}",
2207 NULL)),
2208 hidden_WidgetFlag | frameless_WidgetFlag),
2209 "ident.temp.note");
2210 addChild_Widget(values, iClob(tmpGroup));
2211 }
2212 addChildFlags_Widget(headings, iClob(makePadding_Widget(gap_UI)), collapse_WidgetFlag | hidden_WidgetFlag);
2213 addChildFlags_Widget(values, iClob(makePadding_Widget(gap_UI)), collapse_WidgetFlag | hidden_WidgetFlag);
2214 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.email}", "ident.email", iClob(inputs[1] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2215 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.userid}", "ident.userid", iClob(inputs[2] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2216 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.domain}", "ident.domain", iClob(inputs[3] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2217 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.org}", "ident.org", iClob(inputs[4] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2218 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.country}", "ident.country", iClob(inputs[5] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2219 arrange_Widget(dlg);
2220 for (size_t i = 0; i < iElemCount(inputs); ++i) {
2221 as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x;
2222 } 2819 }
2223 addChild_Widget(dlg, 2820 else {
2224 iClob(makeDialogButtons_Widget( 2821 dlg = makeSheet_Widget("ident");
2225 (iMenuItem[]){ { "${dlg.newident.more}", 0, 0, "ident.showmore" }, 2822 setId_Widget(addChildFlags_Widget(
2226 { "---", 0, 0, NULL }, 2823 dlg,
2227 { "${cancel}", SDLK_ESCAPE, 0, "ident.cancel" }, 2824 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.newident}", NULL)),
2228 { uiTextAction_ColorEscape "${dlg.newident.create}", 2825 frameless_WidgetFlag),
2229 SDLK_RETURN, 2826 "ident.heading");
2230 KMOD_PRIMARY, 2827 iWidget *page = new_Widget();
2231 "ident.accept" } }, 2828 addChildFlags_Widget(
2232 4))); 2829 dlg, iClob(new_LabelWidget("${dlg.newident.rsa.selfsign}", NULL)), frameless_WidgetFlag);
2233 addChild_Widget(get_Root()->widget, iClob(dlg)); 2830 /* TODO: Use makeTwoColumnWidget_? */
2234 finalizeSheet_Mobile(dlg); 2831 addChild_Widget(dlg, iClob(page));
2832 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
2833 iWidget *headings = addChildFlags_Widget(
2834 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
2835 iWidget *values = addChildFlags_Widget(
2836 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
2837 setId_Widget(headings, "headings");
2838 setId_Widget(values, "values");
2839 iInputWidget *inputs[6];
2840 /* Where will the new identity be active on? */ {
2841 iWidget *head = addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.newident.scope}")));
2842 iWidget *val;
2843 setId_Widget(
2844 addChild_Widget(values,
2845 val = iClob(makeMenuButton_LabelWidget(
2846 scopeItems[0].label, scopeItems, iElemCount(scopeItems)))),
2847 "ident.scope");
2848 head->sizeRef = val;
2849 }
2850 addDialogInputWithHeading_(headings,
2851 values,
2852 "${dlg.newident.until}",
2853 "ident.until",
2854 iClob(newHint_InputWidget(19, "${hint.newident.date}")));
2855 addDialogInputWithHeading_(headings,
2856 values,
2857 "${dlg.newident.commonname}",
2858 "ident.common",
2859 iClob(inputs[0] = new_InputWidget(0)));
2860 /* Temporary? */ {
2861 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.newident.temp}")));
2862 iWidget *tmpGroup = new_Widget();
2863 setFlags_Widget(tmpGroup, arrangeSize_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue);
2864 addChild_Widget(tmpGroup, iClob(makeToggle_Widget("ident.temp")));
2865 setId_Widget(
2866 addChildFlags_Widget(tmpGroup,
2867 iClob(new_LabelWidget(uiTextCaution_ColorEscape warning_Icon
2868 " ${dlg.newident.notsaved}",
2869 NULL)),
2870 hidden_WidgetFlag | frameless_WidgetFlag),
2871 "ident.temp.note");
2872 addChild_Widget(values, iClob(tmpGroup));
2873 }
2874 addChildFlags_Widget(headings, iClob(makePadding_Widget(gap_UI)), collapse_WidgetFlag | hidden_WidgetFlag);
2875 addChildFlags_Widget(values, iClob(makePadding_Widget(gap_UI)), collapse_WidgetFlag | hidden_WidgetFlag);
2876 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.email}", "ident.email", iClob(inputs[1] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2877 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.userid}", "ident.userid", iClob(inputs[2] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2878 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.domain}", "ident.domain", iClob(inputs[3] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2879 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.org}", "ident.org", iClob(inputs[4] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2880 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.country}", "ident.country", iClob(inputs[5] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2881 arrange_Widget(dlg);
2882 for (size_t i = 0; i < iElemCount(inputs); ++i) {
2883 as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x;
2884 }
2885 addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions))));
2886 addChild_Widget(get_Root()->widget, iClob(dlg));
2887 }
2888 setupSheetTransition_Mobile(dlg, iTrue);
2235 return dlg; 2889 return dlg;
2236} 2890}
2237 2891
@@ -2247,15 +2901,23 @@ static const iMenuItem languages[] = {
2247 { "${lang.pt}", 0, 0, "xlt.lang id:pt" }, 2901 { "${lang.pt}", 0, 0, "xlt.lang id:pt" },
2248 { "${lang.ru}", 0, 0, "xlt.lang id:ru" }, 2902 { "${lang.ru}", 0, 0, "xlt.lang id:ru" },
2249 { "${lang.es}", 0, 0, "xlt.lang id:es" }, 2903 { "${lang.es}", 0, 0, "xlt.lang id:es" },
2904 { NULL }
2250}; 2905};
2251 2906
2252static iBool translationHandler_(iWidget *dlg, const char *cmd) { 2907static iBool translationHandler_(iWidget *dlg, const char *cmd) {
2253 iUnused(dlg); 2908 iUnused(dlg);
2254 if (equal_Command(cmd, "xlt.lang")) { 2909 if (equal_Command(cmd, "xlt.lang")) {
2255 iLabelWidget *menuItem = pointer_Command(cmd); 2910 const iMenuItem *langItem = &languages[languageIndex_CStr(cstr_Rangecc(range_Command(cmd, "id")))];
2256 iWidget *button = parent_Widget(parent_Widget(menuItem)); 2911 iWidget *widget = pointer_Command(cmd);
2257 iAssert(isInstance_Object(button, &Class_LabelWidget)); 2912 iLabelWidget *drop;
2258 updateText_LabelWidget((iLabelWidget *) button, text_LabelWidget(menuItem)); 2913 if (flags_Widget(widget) & nativeMenu_WidgetFlag) {
2914 drop = (iLabelWidget *) parent_Widget(widget);
2915 }
2916 else {
2917 drop = (iLabelWidget *) parent_Widget(parent_Widget(widget));
2918 }
2919 iAssert(isInstance_Object(drop, &Class_LabelWidget));
2920 updateDropdownSelection_LabelWidget(drop, langItem->command);
2259 return iTrue; 2921 return iTrue;
2260 } 2922 }
2261 return iFalse; 2923 return iFalse;
@@ -2263,6 +2925,7 @@ static iBool translationHandler_(iWidget *dlg, const char *cmd) {
2263 2925
2264const char *languageId_String(const iString *menuItemLabel) { 2926const char *languageId_String(const iString *menuItemLabel) {
2265 iForIndices(i, languages) { 2927 iForIndices(i, languages) {
2928 if (!languages[i].label) break;
2266 if (!cmp_String(menuItemLabel, translateCStr_Lang(languages[i].label))) { 2929 if (!cmp_String(menuItemLabel, translateCStr_Lang(languages[i].label))) {
2267 return cstr_Rangecc(range_Command(languages[i].command, "id")); 2930 return cstr_Rangecc(range_Command(languages[i].command, "id"));
2268 } 2931 }
@@ -2272,6 +2935,7 @@ const char *languageId_String(const iString *menuItemLabel) {
2272 2935
2273int languageIndex_CStr(const char *langId) { 2936int languageIndex_CStr(const char *langId) {
2274 iForIndices(i, languages) { 2937 iForIndices(i, languages) {
2938 if (!languages[i].label) break;
2275 if (equal_Rangecc(range_Command(languages[i].command, "id"), langId)) { 2939 if (equal_Rangecc(range_Command(languages[i].command, "id"), langId)) {
2276 return (int) i; 2940 return (int) i;
2277 } 2941 }
@@ -2280,54 +2944,74 @@ int languageIndex_CStr(const char *langId) {
2280} 2944}
2281 2945
2282iWidget *makeTranslation_Widget(iWidget *parent) { 2946iWidget *makeTranslation_Widget(iWidget *parent) {
2283 iWidget *dlg = makeSheet_Widget("xlt"); 2947 const iMenuItem actions[] = {
2284 setFlags_Widget(dlg, keepOnTop_WidgetFlag, iFalse); 2948 { "${cancel}", SDLK_ESCAPE, 0, "translation.cancel" },
2285 dlg->minSize.x = 70 * gap_UI; 2949 { uiTextAction_ColorEscape "${dlg.translate}", SDLK_RETURN, 0, "translation.submit" }
2950 };
2951 iWidget *dlg;
2952 if (isUsingPanelLayout_Mobile()) {
2953 dlg = makePanelsParent_Mobile(parent, "xlt", (iMenuItem[]){
2954 { "title id:heading.translate" },
2955 { "dropdown id:xlt.from text:${dlg.translate.from}", 0, 0, (const void *) languages },
2956 { "dropdown id:xlt.to text:${dlg.translate.to}", 0, 0, (const void *) languages },
2957 { NULL }
2958 }, actions, iElemCount(actions));
2959 }
2960 else {
2961 dlg = makeSheet_Widget("xlt");
2962 setFlags_Widget(dlg, keepOnTop_WidgetFlag, iFalse);
2963 dlg->minSize.x = 70 * gap_UI;
2964 addChildFlags_Widget(
2965 dlg,
2966 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.translate}", NULL)),
2967 frameless_WidgetFlag);
2968 addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId))));
2969 iWidget *headings, *values;
2970 iWidget *page;
2971 addChild_Widget(dlg, iClob(page = makeTwoColumns_Widget(&headings, &values)));
2972 setId_Widget(page, "xlt.langs");
2973 iLabelWidget *fromLang, *toLang;
2974 const size_t numLangs = iElemCount(languages) - 1;
2975 const char *widestLabel = languages[findWidestLabel_MenuItem(languages, numLangs)].label;
2976 /* Source language. */ {
2977 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.translate.from}")));
2978 setId_Widget(addChildFlags_Widget(values,
2979 iClob(fromLang = makeMenuButton_LabelWidget(
2980 widestLabel, languages, numLangs)),
2981 alignLeft_WidgetFlag),
2982 "xlt.from");
2983 setBackgroundColor_Widget(findChild_Widget(as_Widget(fromLang), "menu"),
2984 uiBackgroundMenu_ColorId);
2985 }
2986 /* Target language. */ {
2987 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.translate.to}")));
2988 setId_Widget(addChildFlags_Widget(values,
2989 iClob(toLang = makeMenuButton_LabelWidget(
2990 widestLabel, languages, numLangs)),
2991 alignLeft_WidgetFlag),
2992 "xlt.to");
2993 setBackgroundColor_Widget(findChild_Widget(as_Widget(toLang), "menu"),
2994 uiBackgroundMenu_ColorId);
2995 }
2996 addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId))));
2997 addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions))));
2998 addChild_Widget(parent, iClob(dlg));
2999 arrange_Widget(dlg);
3000 }
3001 /* Update choices. */
3002 updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.from"),
3003 languages[prefs_App()->langFrom].command);
3004 updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.to"),
3005 languages[prefs_App()->langTo].command);
3006// updateText_LabelWidget(
3007// findChild_Widget(dlg, "xlt.from"),
3008// text_LabelWidget(child_Widget(findChild_Widget(findChild_Widget(dlg, "xlt.from"), "menu"),
3009// prefs_App()->langFrom)));
3010// updateText_LabelWidget(
3011// findChild_Widget(dlg, "xlt.to"),
3012// text_LabelWidget(child_Widget(findChild_Widget(findChild_Widget(dlg, "xlt.to"), "menu"),
3013// prefs_App()->langTo)));
2286 setCommandHandler_Widget(dlg, translationHandler_); 3014 setCommandHandler_Widget(dlg, translationHandler_);
2287 addChildFlags_Widget(dlg, 3015 setupSheetTransition_Mobile(dlg, iTrue);
2288 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.translate}", NULL)),
2289 frameless_WidgetFlag);
2290 addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId))));
2291 iWidget *headings, *values;
2292 iWidget *page;
2293 addChild_Widget(dlg, iClob(page = makeTwoColumns_Widget(&headings, &values)));
2294 setId_Widget(page, "xlt.langs");
2295 iLabelWidget *fromLang, *toLang;
2296 /* Source language. */ {
2297 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.translate.from}")));
2298 setId_Widget(
2299 addChildFlags_Widget(values,
2300 iClob(fromLang = makeMenuButton_LabelWidget(
2301 "${lang.pt}", languages, iElemCount(languages))),
2302 alignLeft_WidgetFlag),
2303 "xlt.from");
2304 iWidget *langMenu = findChild_Widget(as_Widget(fromLang), "menu");
2305 updateText_LabelWidget(fromLang,
2306 text_LabelWidget(child_Widget(langMenu, prefs_App()->langFrom)));
2307 setBackgroundColor_Widget(langMenu, uiBackgroundMenu_ColorId);
2308 }
2309 /* Target language. */ {
2310 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.translate.to}")));
2311 setId_Widget(addChildFlags_Widget(values,
2312 iClob(toLang = makeMenuButton_LabelWidget(
2313 "${lang.pt}", languages, iElemCount(languages))),
2314 alignLeft_WidgetFlag),
2315 "xlt.to");
2316 iWidget *langMenu = findChild_Widget(as_Widget(toLang), "menu");
2317 setBackgroundColor_Widget(langMenu, uiBackgroundMenu_ColorId);
2318 updateText_LabelWidget(toLang,
2319 text_LabelWidget(child_Widget(langMenu, prefs_App()->langTo)));
2320 }
2321 addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId))));
2322 addChild_Widget(
2323 dlg,
2324 iClob(makeDialogButtons_Widget(
2325 (iMenuItem[]){
2326 { "${cancel}", SDLK_ESCAPE, 0, "translation.cancel" },
2327 { uiTextAction_ColorEscape "${dlg.translate}", SDLK_RETURN, 0, "translation.submit" } },
2328 2)));
2329 addChild_Widget(parent, iClob(dlg));
2330 arrange_Widget(dlg);
2331 finalizeSheet_Mobile(dlg);
2332 return dlg; 3016 return dlg;
2333} 3017}
diff --git a/src/ui/util.h b/src/ui/util.h
index 2423f834..5d092700 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -220,18 +220,32 @@ struct Impl_MenuItem {
220 const char *label; 220 const char *label;
221 int key; 221 int key;
222 int kmods; 222 int kmods;
223 const char *command; 223 union {
224 const char *command;
225 const void *data;
226 };
224}; 227};
225 228
226iWidget * makeMenu_Widget (iWidget *parent, const iMenuItem *items, size_t n); /* returns no ref */ 229iWidget * makeMenu_Widget (iWidget *parent, const iMenuItem *items, size_t n); /* returns no ref */
227void openMenu_Widget (iWidget *, iInt2 windowCoord); 230void makeMenuItems_Widget (iWidget *menu, const iMenuItem *items, size_t n);
228void openMenuFlags_Widget(iWidget *, iInt2 windowCoord, iBool postCommands); 231void openMenu_Widget (iWidget *, iInt2 windowCoord);
229void closeMenu_Widget (iWidget *); 232void openMenuFlags_Widget (iWidget *, iInt2 windowCoord, iBool postCommands);
233void closeMenu_Widget (iWidget *);
234void releaseNativeMenu_Widget (iWidget *);
230 235
231iLabelWidget * findMenuItem_Widget (iWidget *menu, const char *command); 236size_t findWidestLabel_MenuItem (const iMenuItem *items, size_t num);
232void setMenuItemDisabled_Widget (iWidget *menu, const char *command, iBool disable); 237void setSelected_NativeMenuItem (iMenuItem *item, iBool isSelected);
233 238
234int checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see macro below */ 239iChar removeIconPrefix_String (iString *);
240
241iLabelWidget * findMenuItem_Widget (iWidget *menu, const char *command);
242iMenuItem * findNativeMenuItem_Widget (iWidget *menu, const char *commandSuffix);
243void setMenuItemDisabled_Widget (iWidget *menu, const char *command, iBool disable);
244void setMenuItemDisabledByIndex_Widget(iWidget *menu, size_t index, iBool disable);
245void setMenuItemLabel_Widget (iWidget *menu, const char *command, const char *newLabel);
246void setMenuItemLabelByIndex_Widget (iWidget *menu, size_t index, const char *newLabel);
247
248int checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see macro below */
235 249
236#define processContextMenuEvent_Widget(menu, sdlEvent, stmtEaten) \ 250#define processContextMenuEvent_Widget(menu, sdlEvent, stmtEaten) \
237 for (const int result = checkContextMenu_Widget((menu), (sdlEvent));;) { \ 251 for (const int result = checkContextMenu_Widget((menu), (sdlEvent));;) { \
@@ -239,7 +253,8 @@ int checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see mac
239 break; \ 253 break; \
240 } 254 }
241 255
242iLabelWidget * makeMenuButton_LabelWidget (const char *label, const iMenuItem *items, size_t n); 256iLabelWidget * makeMenuButton_LabelWidget (const char *label, const iMenuItem *items, size_t n);
257void updateDropdownSelection_LabelWidget (iLabelWidget *dropButton, const char *selectedCommand);
243 258
244/*-----------------------------------------------------------------------------------------------*/ 259/*-----------------------------------------------------------------------------------------------*/
245 260
@@ -268,6 +283,8 @@ void useSheetStyle_Widget (iWidget *);
268iWidget * makeDialogButtons_Widget (const iMenuItem *actions, size_t numActions); 283iWidget * makeDialogButtons_Widget (const iMenuItem *actions, size_t numActions);
269iWidget * makeTwoColumns_Widget (iWidget **headings, iWidget **values); 284iWidget * makeTwoColumns_Widget (iWidget **headings, iWidget **values);
270 285
286iLabelWidget *dialogAcceptButton_Widget (const iWidget *);
287
271iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values, 288iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values,
272 const char *labelText, const char *inputId, 289 const char *labelText, const char *inputId,
273 iInputWidget *input); 290 iInputWidget *input);
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 4f567989..6b9ee11d 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -40,6 +40,67 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
40# include "../ios.h" 40# include "../ios.h"
41#endif 41#endif
42 42
43struct Impl_WidgetDrawBuffer {
44 SDL_Texture *texture;
45 iInt2 size;
46 iBool isValid;
47 SDL_Texture *oldTarget;
48 iInt2 oldOrigin;
49};
50
51static void init_WidgetDrawBuffer(iWidgetDrawBuffer *d) {
52 d->texture = NULL;
53 d->size = zero_I2();
54 d->isValid = iFalse;
55 d->oldTarget = NULL;
56}
57
58static void deinit_WidgetDrawBuffer(iWidgetDrawBuffer *d) {
59 SDL_DestroyTexture(d->texture);
60}
61
62iDefineTypeConstruction(WidgetDrawBuffer)
63
64static void realloc_WidgetDrawBuffer(iWidgetDrawBuffer *d, SDL_Renderer *render, iInt2 size) {
65 if (!isEqual_I2(d->size, size)) {
66 d->size = size;
67 if (d->texture) {
68 SDL_DestroyTexture(d->texture);
69 }
70 d->texture = SDL_CreateTexture(render,
71 SDL_PIXELFORMAT_RGBA8888,
72 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
73 size.x,
74 size.y);
75 SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND);
76 d->isValid = iFalse;
77 }
78}
79
80static void release_WidgetDrawBuffer(iWidgetDrawBuffer *d) {
81 if (d->texture) {
82 SDL_DestroyTexture(d->texture);
83 d->texture = NULL;
84 }
85 d->size = zero_I2();
86 d->isValid = iFalse;
87}
88
89static iRect boundsForDraw_Widget_(const iWidget *d) {
90 iRect bounds = bounds_Widget(d);
91 if (d->flags & drawBackgroundToBottom_WidgetFlag) {
92 bounds.size.y = iMax(bounds.size.y, size_Root(d->root).y);
93 }
94 return bounds;
95}
96
97static iBool checkDrawBuffer_Widget_(const iWidget *d) {
98 return d->drawBuf && d->drawBuf->isValid &&
99 isEqual_I2(d->drawBuf->size, boundsForDraw_Widget_(d).size);
100}
101
102/*----------------------------------------------------------------------------------------------*/
103
43static void printInfo_Widget_(const iWidget *); 104static void printInfo_Widget_(const iWidget *);
44 105
45void releaseChildren_Widget(iWidget *d) { 106void releaseChildren_Widget(iWidget *d) {
@@ -66,6 +127,7 @@ void init_Widget(iWidget *d) {
66 d->children = NULL; 127 d->children = NULL;
67 d->parent = NULL; 128 d->parent = NULL;
68 d->commandHandler = NULL; 129 d->commandHandler = NULL;
130 d->drawBuf = NULL;
69 iZap(d->padding); 131 iZap(d->padding);
70} 132}
71 133
@@ -82,6 +144,7 @@ static void visualOffsetAnimation_Widget_(void *ptr) {
82 144
83void deinit_Widget(iWidget *d) { 145void deinit_Widget(iWidget *d) {
84 releaseChildren_Widget(d); 146 releaseChildren_Widget(d);
147 delete_WidgetDrawBuffer(d->drawBuf);
85#if 0 && !defined (NDEBUG) 148#if 0 && !defined (NDEBUG)
86 printf("widget %p (%s) deleted (on top:%d)\n", d, cstr_String(&d->id), 149 printf("widget %p (%s) deleted (on top:%d)\n", d, cstr_String(&d->id),
87 d->flags & keepOnTop_WidgetFlag ? 1 : 0); 150 d->flags & keepOnTop_WidgetFlag ? 1 : 0);
@@ -93,6 +156,16 @@ void deinit_Widget(iWidget *d) {
93 if (d->flags & visualOffset_WidgetFlag) { 156 if (d->flags & visualOffset_WidgetFlag) {
94 removeTicker_App(visualOffsetAnimation_Widget_, d); 157 removeTicker_App(visualOffsetAnimation_Widget_, d);
95 } 158 }
159 iWindow *win = get_Window();
160 if (win->lastHover == d) {
161 win->lastHover = NULL;
162 }
163 if (win->hover == d) {
164 win->hover = NULL;
165 }
166 if (d->flags & nativeMenu_WidgetFlag) {
167 releaseNativeMenu_Widget(d);
168 }
96 widgetDestroyed_Touch(d); 169 widgetDestroyed_Touch(d);
97} 170}
98 171
@@ -100,11 +173,15 @@ static void aboutToBeDestroyed_Widget_(iWidget *d) {
100 d->flags |= destroyPending_WidgetFlag; 173 d->flags |= destroyPending_WidgetFlag;
101 if (isFocused_Widget(d)) { 174 if (isFocused_Widget(d)) {
102 setFocus_Widget(NULL); 175 setFocus_Widget(NULL);
103 return; 176 //return; /* TODO: Why?! */
104 } 177 }
105 remove_Periodic(periodic_App(), d); 178 remove_Periodic(periodic_App(), d);
179 iWindow *win = get_Window();
106 if (isHover_Widget(d)) { 180 if (isHover_Widget(d)) {
107 get_Window()->hover = NULL; 181 win->hover = NULL;
182 }
183 if (win->lastHover == d) {
184 win->lastHover = NULL;
108 } 185 }
109 iForEach(ObjectList, i, d->children) { 186 iForEach(ObjectList, i, d->children) {
110 aboutToBeDestroyed_Widget_(as_Widget(i.object)); 187 aboutToBeDestroyed_Widget_(as_Widget(i.object));
@@ -151,6 +228,7 @@ void setFlags_Widget(iWidget *d, int64_t flags, iBool set) {
151 } 228 }
152 else { 229 else {
153 removeOne_PtrArray(onTop, d); 230 removeOne_PtrArray(onTop, d);
231 iAssert(indexOf_PtrArray(onTop, d) == iInvalidPos);
154 } 232 }
155 } 233 }
156 if (d->flags & arrangeWidth_WidgetFlag && 234 if (d->flags & arrangeWidth_WidgetFlag &&
@@ -196,6 +274,10 @@ iWidget *root_Widget(const iWidget *d) {
196 return d ? d->root->widget : NULL; 274 return d ? d->root->widget : NULL;
197} 275}
198 276
277iWindow *window_Widget(const iAnyObject *d) {
278 return constAs_Widget(d)->root->window;
279}
280
199void showCollapsed_Widget(iWidget *d, iBool show) { 281void showCollapsed_Widget(iWidget *d, iBool show) {
200 const iBool isVisible = !(d->flags & hidden_WidgetFlag); 282 const iBool isVisible = !(d->flags & hidden_WidgetFlag);
201 if ((isVisible && !show) || (!isVisible && show)) { 283 if ((isVisible && !show) || (!isVisible && show)) {
@@ -452,7 +534,7 @@ static void arrange_Widget_(iWidget *d) {
452 else if (d->flags & centerHorizontal_WidgetFlag) { 534 else if (d->flags & centerHorizontal_WidgetFlag) {
453 centerHorizontal_Widget_(d); 535 centerHorizontal_Widget_(d);
454 } 536 }
455 if (d->flags & resizeToParentWidth_WidgetFlag) { 537 if (d->flags & resizeToParentWidth_WidgetFlag && d->parent) {
456 iRect childBounds = zero_Rect(); 538 iRect childBounds = zero_Rect();
457 if (flags_Widget(d->parent) & arrangeWidth_WidgetFlag) { 539 if (flags_Widget(d->parent) & arrangeWidth_WidgetFlag) {
458 /* Can't go narrower than what the children require, though. */ 540 /* Can't go narrower than what the children require, though. */
@@ -462,7 +544,7 @@ static void arrange_Widget_(iWidget *d) {
462 setWidth_Widget_(d, iMaxi(width_Rect(innerRect_Widget_(d->parent)), 544 setWidth_Widget_(d, iMaxi(width_Rect(innerRect_Widget_(d->parent)),
463 width_Rect(childBounds))); 545 width_Rect(childBounds)));
464 } 546 }
465 if (d->flags & resizeToParentHeight_WidgetFlag) { 547 if (d->flags & resizeToParentHeight_WidgetFlag && d->parent) {
466 TRACE(d, "resize to parent height"); 548 TRACE(d, "resize to parent height");
467 setHeight_Widget_(d, height_Rect(innerRect_Widget_(d->parent))); 549 setHeight_Widget_(d, height_Rect(innerRect_Widget_(d->parent)));
468 } 550 }
@@ -817,9 +899,6 @@ iInt2 localToWindow_Widget(const iWidget *d, iInt2 localCoord) {
817 applyVisualOffset_Widget_(w, &pos); 899 applyVisualOffset_Widget_(w, &pos);
818 addv_I2(&window, pos); 900 addv_I2(&window, pos);
819 } 901 }
820#if defined (iPlatformMobile)
821 window.y += value_Anim(&get_Window()->rootOffset);
822#endif
823 return window; 902 return window;
824} 903}
825 904
@@ -899,15 +978,18 @@ static iBool filterEvent_Widget_(const iWidget *d, const SDL_Event *ev) {
899} 978}
900 979
901void unhover_Widget(void) { 980void unhover_Widget(void) {
902 get_Window()->hover = NULL; 981 iWidget **hover = &get_Window()->hover;
982 if (*hover) {
983 refresh_Widget(*hover);
984 }
985 *hover = NULL;
903} 986}
904 987
905iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) { 988iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
906 //iAssert(d->root == get_Root());
907 if (!d->parent) { 989 if (!d->parent) {
908 if (get_Window()->focus && get_Window()->focus->root == d->root && isKeyboardEvent_(ev)) { 990 if (window_Widget(d)->focus && window_Widget(d)->focus->root == d->root && isKeyboardEvent_(ev)) {
909 /* Root dispatches keyboard events directly to the focused widget. */ 991 /* Root dispatches keyboard events directly to the focused widget. */
910 if (dispatchEvent_Widget(get_Window()->focus, ev)) { 992 if (dispatchEvent_Widget(window_Widget(d)->focus, ev)) {
911 return iTrue; 993 return iTrue;
912 } 994 }
913 } 995 }
@@ -936,7 +1018,8 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
936 } 1018 }
937 } 1019 }
938 else if (ev->type == SDL_MOUSEMOTION && 1020 else if (ev->type == SDL_MOUSEMOTION &&
939 (!get_Window()->hover || hasParent_Widget(d, get_Window()->hover)) && 1021 ev->motion.windowID == SDL_GetWindowID(window_Widget(d)->win) &&
1022 (!window_Widget(d)->hover || hasParent_Widget(d, window_Widget(d)->hover)) &&
940 flags_Widget(d) & hover_WidgetFlag && ~flags_Widget(d) & hidden_WidgetFlag && 1023 flags_Widget(d) & hover_WidgetFlag && ~flags_Widget(d) & hidden_WidgetFlag &&
941 ~flags_Widget(d) & disabled_WidgetFlag) { 1024 ~flags_Widget(d) & disabled_WidgetFlag) {
942 if (contains_Widget(d, init_I2(ev->motion.x, ev->motion.y))) { 1025 if (contains_Widget(d, init_I2(ev->motion.x, ev->motion.y))) {
@@ -955,11 +1038,11 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
955 iReverseForEach(ObjectList, i, d->children) { 1038 iReverseForEach(ObjectList, i, d->children) {
956 iWidget *child = as_Widget(i.object); 1039 iWidget *child = as_Widget(i.object);
957 //iAssert(child->root == d->root); 1040 //iAssert(child->root == d->root);
958 if (child == get_Window()->focus && isKeyboardEvent_(ev)) { 1041 if (child == window_Widget(d)->focus && isKeyboardEvent_(ev)) {
959 continue; /* Already dispatched. */ 1042 continue; /* Already dispatched. */
960 } 1043 }
961 if (isVisible_Widget(child) && child->flags & keepOnTop_WidgetFlag) { 1044 if (isVisible_Widget(child) && child->flags & keepOnTop_WidgetFlag) {
962 /* Already dispatched. */ 1045 /* Already dispatched. */
963 continue; 1046 continue;
964 } 1047 }
965 if (dispatchEvent_Widget(child, ev)) { 1048 if (dispatchEvent_Widget(child, ev)) {
@@ -974,7 +1057,7 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
974#endif 1057#endif
975#if 0 1058#if 0
976 if (ev->type == SDL_MOUSEMOTION) { 1059 if (ev->type == SDL_MOUSEMOTION) {
977 printf("[%p] %s:'%s' (on top) ate the motion\n", 1060 printf("[%p] %s:'%s' ate the motion\n",
978 child, class_Widget(child)->name, 1061 child, class_Widget(child)->name,
979 cstr_String(id_Widget(child))); 1062 cstr_String(id_Widget(child)));
980 fflush(stdout); 1063 fflush(stdout);
@@ -1008,24 +1091,60 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
1008 return iFalse; 1091 return iFalse;
1009} 1092}
1010 1093
1094void scrollInfo_Widget(const iWidget *d, iWidgetScrollInfo *info) {
1095 iRect bounds = boundsWithoutVisualOffset_Widget(d);
1096 const iRect winRect = adjusted_Rect(safeRect_Root(d->root),
1097 zero_I2(),
1098 init_I2(0, -get_MainWindow()->keyboardHeight));
1099 info->height = bounds.size.y;
1100 info->avail = height_Rect(winRect);
1101 if (info->avail >= info->height) {
1102 info->normScroll = 0.0f;
1103 info->thumbY = 0;
1104 info->thumbHeight = 0;
1105 }
1106 else {
1107 int scroll = top_Rect(winRect) - top_Rect(bounds);
1108 info->normScroll = scroll / (float) (info->height - info->avail);
1109 info->normScroll = iClamp(info->normScroll, 0.0f, 1.0f);
1110 info->thumbHeight = iMin(info->avail / 2, info->avail * info->avail / info->height);
1111 info->thumbY = top_Rect(winRect) + (info->avail - info->thumbHeight) * info->normScroll;
1112 }
1113}
1114
1011iBool scrollOverflow_Widget(iWidget *d, int delta) { 1115iBool scrollOverflow_Widget(iWidget *d, int delta) {
1012 iRect bounds = boundsWithoutVisualOffset_Widget(d); 1116 iRect bounds = boundsWithoutVisualOffset_Widget(d);
1013 const iInt2 rootSize = size_Root(d->root); 1117 const iRect winRect = adjusted_Rect(safeRect_Root(d->root),
1014 const iRect winRect = safeRect_Root(d->root); 1118 zero_I2(),
1015 const int yTop = top_Rect(winRect); 1119 init_I2(0, -get_MainWindow()->keyboardHeight));
1016 const int yBottom = bottom_Rect(winRect); 1120 const int yTop = top_Rect(winRect);
1121 const int yBottom = bottom_Rect(winRect);
1017 if (top_Rect(bounds) >= yTop && bottom_Rect(bounds) < yBottom) { 1122 if (top_Rect(bounds) >= yTop && bottom_Rect(bounds) < yBottom) {
1018 return iFalse; /* fits inside just fine */ 1123 return iFalse; /* fits inside just fine */
1019 } 1124 }
1020 //const int safeBottom = rootSize.y - yBottom; 1125 //const int safeBottom = rootSize.y - yBottom;
1021 bounds.pos.y += delta; 1126 iRangei validPosRange = { bottom_Rect(winRect) - height_Rect(bounds), yTop };
1022 const iRangei range = { bottom_Rect(winRect) - height_Rect(bounds), yTop }; 1127 if (validPosRange.start > validPosRange.end) {
1128 validPosRange.start = validPosRange.end; /* no room to scroll */
1129 }
1130 if (delta) {
1131 if (delta < 0 && bounds.pos.y < validPosRange.start) {
1132 delta = 0;
1133 }
1134 if (delta > 0 && bounds.pos.y > validPosRange.end) {
1135 delta = 0;
1136 }
1137 bounds.pos.y += delta;
1138 if (delta < 0) {
1139 bounds.pos.y = iMax(bounds.pos.y, validPosRange.start);
1140 }
1141 else if (delta > 0) {
1142 bounds.pos.y = iMin(bounds.pos.y, validPosRange.end);
1143 }
1023// printf("range: %d ... %d\n", range.start, range.end); 1144// printf("range: %d ... %d\n", range.start, range.end);
1024 if (range.start >= range.end) {
1025 bounds.pos.y = range.end;
1026 } 1145 }
1027 else { 1146 else {
1028 bounds.pos.y = iClamp(bounds.pos.y, range.start, range.end); 1147 bounds.pos.y = iClamp(bounds.pos.y, validPosRange.start, validPosRange.end);
1029 } 1148 }
1030// if (delta >= 0) { 1149// if (delta >= 0) {
1031// bounds.pos.y = iMin(bounds.pos.y, yTop); 1150// bounds.pos.y = iMin(bounds.pos.y, yTop);
@@ -1036,7 +1155,8 @@ iBool scrollOverflow_Widget(iWidget *d, int delta) {
1036 const iInt2 newPos = windowToInner_Widget(d->parent, bounds.pos); 1155 const iInt2 newPos = windowToInner_Widget(d->parent, bounds.pos);
1037 if (!isEqual_I2(newPos, d->rect.pos)) { 1156 if (!isEqual_I2(newPos, d->rect.pos)) {
1038 d->rect.pos = newPos; 1157 d->rect.pos = newPos;
1039 refresh_Widget(d); 1158// refresh_Widget(d);
1159 postRefresh_App();
1040 } 1160 }
1041 return height_Rect(bounds) > height_Rect(winRect); 1161 return height_Rect(bounds) > height_Rect(winRect);
1042} 1162}
@@ -1077,6 +1197,9 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
1077 } 1197 }
1078 if (ev->user.code == command_UserEventCode) { 1198 if (ev->user.code == command_UserEventCode) {
1079 const char *cmd = command_UserEvent(ev); 1199 const char *cmd = command_UserEvent(ev);
1200 if (d->drawBuf && equal_Command(cmd, "theme.changed")) {
1201 d->drawBuf->isValid = iFalse;
1202 }
1080 if (d->flags & (leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag) && 1203 if (d->flags & (leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag) &&
1081 isVisible_Widget(d) && ~d->flags & disabled_WidgetFlag && 1204 isVisible_Widget(d) && ~d->flags & disabled_WidgetFlag &&
1082 equal_Command(cmd, "edgeswipe.moved")) { 1205 equal_Command(cmd, "edgeswipe.moved")) {
@@ -1130,7 +1253,7 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
1130 ev->button.x, 1253 ev->button.x,
1131 ev->button.y); 1254 ev->button.y);
1132 } 1255 }
1133 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); 1256 setCursor_Window(window_Widget(d), SDL_SYSTEM_CURSOR_ARROW);
1134 return iTrue; 1257 return iTrue;
1135 } 1258 }
1136 return iFalse; 1259 return iFalse;
@@ -1147,14 +1270,14 @@ int backgroundFadeColor_Widget(void) {
1147 } 1270 }
1148} 1271}
1149 1272
1150void drawBackground_Widget(const iWidget *d) { 1273iLocalDef iBool isDrawn_Widget_(const iWidget *d) {
1151 if (d->flags & noBackground_WidgetFlag) { 1274 return ~d->flags & hidden_WidgetFlag || d->flags & visualOffset_WidgetFlag;
1152 return; 1275}
1153 } 1276
1154 if (d->flags & hidden_WidgetFlag && ~d->flags & visualOffset_WidgetFlag) { 1277void drawLayerEffects_Widget(const iWidget *d) {
1155 return; 1278 /* Layered effects are not buffered, so they are drawn here separately. */
1156 } 1279 iAssert(isDrawn_Widget_(d));
1157 /* Popup menus have a shadowed border. */ 1280 iAssert(window_Widget(d) == get_Window());
1158 iBool shadowBorder = (d->flags & keepOnTop_WidgetFlag && ~d->flags & mouseModal_WidgetFlag) != 0; 1281 iBool shadowBorder = (d->flags & keepOnTop_WidgetFlag && ~d->flags & mouseModal_WidgetFlag) != 0;
1159 iBool fadeBackground = (d->bgColor >= 0 || d->frameColor >= 0) && d->flags & mouseModal_WidgetFlag; 1282 iBool fadeBackground = (d->bgColor >= 0 || d->frameColor >= 0) && d->flags & mouseModal_WidgetFlag;
1160 if (deviceType_App() == phone_AppDeviceType) { 1283 if (deviceType_App() == phone_AppDeviceType) {
@@ -1163,13 +1286,12 @@ void drawBackground_Widget(const iWidget *d) {
1163 shadowBorder = iFalse; 1286 shadowBorder = iFalse;
1164 } 1287 }
1165 } 1288 }
1289 const iBool isFaded = fadeBackground && ~d->flags & noFadeBackground_WidgetFlag;
1166 if (shadowBorder && ~d->flags & noShadowBorder_WidgetFlag) { 1290 if (shadowBorder && ~d->flags & noShadowBorder_WidgetFlag) {
1167 iPaint p; 1291 iPaint p;
1168 init_Paint(&p); 1292 init_Paint(&p);
1169 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30); 1293 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30);
1170 } 1294 }
1171 const iBool isFaded = fadeBackground &&
1172 ~d->flags & noFadeBackground_WidgetFlag;
1173 if (isFaded) { 1295 if (isFaded) {
1174 iPaint p; 1296 iPaint p;
1175 init_Paint(&p); 1297 init_Paint(&p);
@@ -1183,15 +1305,68 @@ void drawBackground_Widget(const iWidget *d) {
1183 fillRect_Paint(&p, rect_Root(d->root), backgroundFadeColor_Widget()); 1305 fillRect_Paint(&p, rect_Root(d->root), backgroundFadeColor_Widget());
1184 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); 1306 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
1185 } 1307 }
1308#if defined (iPlatformAppleMobile)
1309 if (d->bgColor >= 0 && d->flags & (drawBackgroundToHorizontalSafeArea_WidgetFlag |
1310 drawBackgroundToVerticalSafeArea_WidgetFlag)) {
1311 iPaint p;
1312 init_Paint(&p);
1313 const iRect rect = bounds_Widget(d);
1314 const iInt2 rootSize = size_Root(d->root);
1315 const iInt2 center = divi_I2(rootSize, 2);
1316 int top = 0, right = 0, bottom = 0, left = 0;
1317 if (d->flags & drawBackgroundToHorizontalSafeArea_WidgetFlag) {
1318 const iBool isWide = width_Rect(rect) > rootSize.x * 9 / 10;
1319 if (isWide || mid_Rect(rect).x < center.x) {
1320 left = -left_Rect(rect);
1321 }
1322 if (isWide || mid_Rect(rect).x > center.x) {
1323 right = rootSize.x - right_Rect(rect);
1324 }
1325 }
1326 if (d->flags & drawBackgroundToVerticalSafeArea_WidgetFlag) {
1327 if (top_Rect(rect) > center.y) {
1328 bottom = rootSize.y - bottom_Rect(rect);
1329 }
1330 if (bottom_Rect(rect) < center.y) {
1331 top = -top_Rect(rect);
1332 }
1333 }
1334 if (top < 0) {
1335 fillRect_Paint(&p, (iRect){ init_I2(left_Rect(rect), 0),
1336 init_I2(width_Rect(rect), top_Rect(rect)) },
1337 d->bgColor);
1338 }
1339 if (left < 0) {
1340 fillRect_Paint(&p, (iRect){ init_I2(0, top_Rect(rect)),
1341 init_I2(left_Rect(rect), height_Rect(rect)) }, d->bgColor);
1342 }
1343 if (right > 0) {
1344 fillRect_Paint(&p, (iRect){ init_I2(right_Rect(rect), top_Rect(rect)),
1345 init_I2(right, height_Rect(rect)) }, d->bgColor);
1346 }
1347// adjustEdges_Rect(&rect, iMin(0, top), iMax(0, right), iMax(0, bottom), iMin(0, left));
1348 }
1349#endif
1350}
1351
1352void drawBackground_Widget(const iWidget *d) {
1353 if (d->flags & noBackground_WidgetFlag) {
1354 return;
1355 }
1356 if (!isDrawn_Widget_(d)) {
1357 return;
1358 }
1359 /* Popup menus have a shadowed border. */
1186 if (d->bgColor >= 0 || d->frameColor >= 0) { 1360 if (d->bgColor >= 0 || d->frameColor >= 0) {
1187 iRect rect = bounds_Widget(d); 1361 iRect rect = bounds_Widget(d);
1188 if (d->flags & drawBackgroundToBottom_WidgetFlag) { 1362 if (d->flags & drawBackgroundToBottom_WidgetFlag) {
1189 rect.size.y = size_Root(d->root).y - top_Rect(rect); 1363 rect.size.y += size_Root(d->root).y; // = iMax(rect.size.y, size_Root(d->root).y - top_Rect(rect));
1190 } 1364 }
1191 iPaint p; 1365 iPaint p;
1192 init_Paint(&p); 1366 init_Paint(&p);
1193 if (d->bgColor >= 0) { 1367 if (d->bgColor >= 0) {
1194#if defined (iPlatformAppleMobile) 1368#if 0 && defined (iPlatformAppleMobile)
1369 /* TODO: This is part of the unbuffered draw (layer effects). */
1195 if (d->flags & (drawBackgroundToHorizontalSafeArea_WidgetFlag | 1370 if (d->flags & (drawBackgroundToHorizontalSafeArea_WidgetFlag |
1196 drawBackgroundToVerticalSafeArea_WidgetFlag)) { 1371 drawBackgroundToVerticalSafeArea_WidgetFlag)) {
1197 const iInt2 rootSize = size_Root(d->root); 1372 const iInt2 rootSize = size_Root(d->root);
@@ -1214,7 +1389,7 @@ void drawBackground_Widget(const iWidget *d) {
1214 top = -top_Rect(rect); 1389 top = -top_Rect(rect);
1215 } 1390 }
1216 } 1391 }
1217 adjustEdges_Rect(&rect, top, right, bottom, left); 1392 adjustEdges_Rect(&rect, iMin(0, top), iMax(0, right), iMax(0, bottom), iMin(0, left));
1218 } 1393 }
1219#endif 1394#endif
1220 fillRect_Paint(&p, rect, d->bgColor); 1395 fillRect_Paint(&p, rect, d->bgColor);
@@ -1242,8 +1417,62 @@ void drawBackground_Widget(const iWidget *d) {
1242 } 1417 }
1243} 1418}
1244 1419
1245iLocalDef iBool isDrawn_Widget_(const iWidget *d) { 1420int drawCount_;
1246 return ~d->flags & hidden_WidgetFlag || d->flags & visualOffset_WidgetFlag; 1421
1422static iBool isRoot_Widget_(const iWidget *d) {
1423 return d == d->root->widget;
1424}
1425
1426iLocalDef iBool isFullyContainedByOther_Rect(const iRect d, const iRect other) {
1427 if (isEmpty_Rect(other)) {
1428 /* Nothing is contained by empty. */
1429 return iFalse;
1430 }
1431 if (isEmpty_Rect(d)) {
1432 /* Empty is fully contained by anything. */
1433 return iTrue;
1434 }
1435 return equal_Rect(intersect_Rect(d, other), d);
1436}
1437
1438static void addToPotentiallyVisible_Widget_(const iWidget *d, iPtrArray *pvs, iRect *fullyMasked) {
1439 if (isDrawn_Widget_(d)) {
1440 iRect bounds = bounds_Widget(d);
1441 if (d->flags & drawBackgroundToBottom_WidgetFlag) {
1442 bounds.size.y += size_Root(d->root).y; // iMax(bounds.size.y, size_Root(d->root).y - top_Rect(bounds));
1443 }
1444 if (isFullyContainedByOther_Rect(bounds, *fullyMasked)) {
1445 return; /* can't be seen */
1446 }
1447 pushBack_PtrArray(pvs, d);
1448 if (d->bgColor >= 0 && ~d->flags & noBackground_WidgetFlag &&
1449 isFullyContainedByOther_Rect(*fullyMasked, bounds)) {
1450 *fullyMasked = bounds;
1451 }
1452 }
1453}
1454
1455static void findPotentiallyVisible_Widget_(const iWidget *d, iPtrArray *pvs) {
1456 iRect fullyMasked = zero_Rect();
1457 if (isRoot_Widget_(d)) {
1458 iReverseConstForEach(PtrArray, i, onTop_Root(d->root)) {
1459 const iWidget *top = i.ptr;
1460 iAssert(top->parent);
1461 addToPotentiallyVisible_Widget_(top, pvs, &fullyMasked);
1462 }
1463 }
1464 iReverseConstForEach(ObjectList, i, d->children) {
1465 const iWidget *child = i.object;
1466 if (~child->flags & keepOnTop_WidgetFlag) {
1467 addToPotentiallyVisible_Widget_(child, pvs, &fullyMasked);
1468 }
1469 }
1470}
1471
1472iLocalDef void incrementDrawCount_(const iWidget *d) {
1473 if (class_Widget(d) != &Class_Widget || d->bgColor >= 0 || d->frameColor >= 0) {
1474 drawCount_++;
1475 }
1247} 1476}
1248 1477
1249void drawChildren_Widget(const iWidget *d) { 1478void drawChildren_Widget(const iWidget *d) {
@@ -1253,21 +1482,108 @@ void drawChildren_Widget(const iWidget *d) {
1253 iConstForEach(ObjectList, i, d->children) { 1482 iConstForEach(ObjectList, i, d->children) {
1254 const iWidget *child = constAs_Widget(i.object); 1483 const iWidget *child = constAs_Widget(i.object);
1255 if (~child->flags & keepOnTop_WidgetFlag && isDrawn_Widget_(child)) { 1484 if (~child->flags & keepOnTop_WidgetFlag && isDrawn_Widget_(child)) {
1485 incrementDrawCount_(child);
1256 class_Widget(child)->draw(child); 1486 class_Widget(child)->draw(child);
1257 } 1487 }
1258 } 1488 }
1489}
1490
1491void drawRoot_Widget(const iWidget *d) {
1492 iAssert(d == d->root->widget);
1259 /* Root draws the on-top widgets on top of everything else. */ 1493 /* Root draws the on-top widgets on top of everything else. */
1260 if (d == d->root->widget) { 1494 iPtrArray pvs;
1261 iConstForEach(PtrArray, i, onTop_Root(d->root)) { 1495 init_PtrArray(&pvs);
1262 const iWidget *top = *i.value; 1496 findPotentiallyVisible_Widget_(d, &pvs);
1263 class_Widget(top)->draw(top); 1497 iReverseConstForEach(PtrArray, i, &pvs) {
1264 } 1498 incrementDrawCount_(i.ptr);
1265 } 1499 class_Widget(i.ptr)->draw(i.ptr);
1500 }
1501 deinit_PtrArray(&pvs);
1502}
1503
1504void setDrawBufferEnabled_Widget(iWidget *d, iBool enable) {
1505 if (enable && !d->drawBuf) {
1506 d->drawBuf = new_WidgetDrawBuffer();
1507 }
1508 else if (!enable && d->drawBuf) {
1509 delete_WidgetDrawBuffer(d->drawBuf);
1510 d->drawBuf = NULL;
1511 }
1512}
1513
1514static void beginBufferDraw_Widget_(const iWidget *d) {
1515 if (d->drawBuf) {
1516// printf("[%p] drawbuffer update %d\n", d, d->drawBuf->isValid);
1517 if (d->drawBuf->isValid) {
1518 iAssert(!isEqual_I2(d->drawBuf->size, boundsForDraw_Widget_(d).size));
1519// printf(" drawBuf:%dx%d boundsForDraw:%dx%d\n",
1520// d->drawBuf->size.x, d->drawBuf->size.y,
1521// boundsForDraw_Widget_(d).size.x,
1522// boundsForDraw_Widget_(d).size.y);
1523 }
1524 const iRect bounds = bounds_Widget(d);
1525 SDL_Renderer *render = renderer_Window(get_Window());
1526 d->drawBuf->oldTarget = SDL_GetRenderTarget(render);
1527 d->drawBuf->oldOrigin = origin_Paint;
1528 realloc_WidgetDrawBuffer(d->drawBuf, render, boundsForDraw_Widget_(d).size);
1529 SDL_SetRenderTarget(render, d->drawBuf->texture);
1530// SDL_SetRenderDrawColor(render, 255, 0, 0, 128);
1531 SDL_SetRenderDrawColor(render, 0, 0, 0, 0);
1532 SDL_RenderClear(render);
1533 origin_Paint = neg_I2(bounds.pos); /* with current visual offset */
1534// printf("beginBufferDraw: origin %d,%d\n", origin_Paint.x, origin_Paint.y);
1535// fflush(stdout);
1536 }
1537}
1538
1539static void endBufferDraw_Widget_(const iWidget *d) {
1540 if (d->drawBuf) {
1541 d->drawBuf->isValid = iTrue;
1542 SDL_SetRenderTarget(renderer_Window(get_Window()), d->drawBuf->oldTarget);
1543 origin_Paint = d->drawBuf->oldOrigin;
1544// printf("endBufferDraw: origin %d,%d\n", origin_Paint.x, origin_Paint.y);
1545// fflush(stdout);
1546 }
1266} 1547}
1267 1548
1268void draw_Widget(const iWidget *d) { 1549void draw_Widget(const iWidget *d) {
1269 drawBackground_Widget(d); 1550 iAssert(window_Widget(d) == get_Window());
1270 drawChildren_Widget(d); 1551 if (!isDrawn_Widget_(d)) {
1552 if (d->drawBuf) {
1553// printf("[%p] drawBuffer released\n", d);
1554 release_WidgetDrawBuffer(d->drawBuf);
1555 }
1556 return;
1557 }
1558 drawLayerEffects_Widget(d);
1559 if (!d->drawBuf || !checkDrawBuffer_Widget_(d)) {
1560 beginBufferDraw_Widget_(d);
1561 drawBackground_Widget(d);
1562 drawChildren_Widget(d);
1563 endBufferDraw_Widget_(d);
1564 }
1565 if (d->drawBuf) {
1566 //iAssert(d->drawBuf->isValid);
1567 const iRect bounds = bounds_Widget(d);
1568 SDL_RenderCopy(renderer_Window(get_Window()), d->drawBuf->texture, NULL,
1569 &(SDL_Rect){ bounds.pos.x, bounds.pos.y,
1570 d->drawBuf->size.x, d->drawBuf->size.y });
1571 }
1572 if (d->flags & overflowScrollable_WidgetFlag) {
1573 iWidgetScrollInfo info;
1574 scrollInfo_Widget(d, &info);
1575 if (info.thumbHeight > 0) {
1576 iPaint p;
1577 init_Paint(&p);
1578 const int scrollWidth = gap_UI / 2;
1579 iRect bounds = bounds_Widget(d);
1580 bounds.pos.x = right_Rect(bounds) - scrollWidth * 3;
1581 bounds.size.x = scrollWidth;
1582 bounds.pos.y = info.thumbY;
1583 bounds.size.y = info.thumbHeight;
1584 fillRect_Paint(&p, bounds, tmQuote_ColorId);
1585 }
1586 }
1271} 1587}
1272 1588
1273iAny *addChild_Widget(iWidget *d, iAnyObject *child) { 1589iAny *addChild_Widget(iWidget *d, iAnyObject *child) {
@@ -1288,6 +1604,12 @@ iAny *addChildPosFlags_Widget(iWidget *d, iAnyObject *child, enum iWidgetAddPos
1288 d->children = new_ObjectList(); 1604 d->children = new_ObjectList();
1289 } 1605 }
1290 if (addPos == back_WidgetAddPos) { 1606 if (addPos == back_WidgetAddPos) {
1607 /* Remove a redundant border flags. */
1608 if (!isEmpty_ObjectList(d->children) &&
1609 as_Widget(back_ObjectList(d->children))->flags & borderBottom_WidgetFlag &&
1610 widget->flags & borderTop_WidgetFlag) {
1611 widget->flags &= ~borderTop_WidgetFlag;
1612 }
1291 pushBack_ObjectList(d->children, widget); /* ref */ 1613 pushBack_ObjectList(d->children, widget); /* ref */
1292 } 1614 }
1293 else { 1615 else {
@@ -1402,6 +1724,7 @@ iAny *hitChild_Widget(const iWidget *d, iInt2 coord) {
1402} 1724}
1403 1725
1404iAny *findChild_Widget(const iWidget *d, const char *id) { 1726iAny *findChild_Widget(const iWidget *d, const char *id) {
1727 if (!d) return NULL;
1405 if (cmp_String(id_Widget(d), id) == 0) { 1728 if (cmp_String(id_Widget(d), id) == 0) {
1406 return iConstCast(iAny *, d); 1729 return iConstCast(iAny *, d);
1407 } 1730 }
@@ -1506,7 +1829,17 @@ iBool equalWidget_Command(const char *cmd, const iWidget *widget, const char *ch
1506 if (equal_Command(cmd, checkCommand)) { 1829 if (equal_Command(cmd, checkCommand)) {
1507 const iWidget *src = pointer_Command(cmd); 1830 const iWidget *src = pointer_Command(cmd);
1508 iAssert(!src || strstr(cmd, " ptr:")); 1831 iAssert(!src || strstr(cmd, " ptr:"));
1509 return src == widget || hasParent_Widget(src, widget); 1832 if (src == widget || hasParent_Widget(src, widget)) {
1833 return iTrue;
1834 }
1835// if (src && type_Window(window_Widget(src)) == popup_WindowType) {
1836// /* Special case: command was emitted from a popup widget. The popup root widget actually
1837// belongs to someone else. */
1838// iWidget *realParent = userData_Object(src->root->widget);
1839// iAssert(realParent);
1840// iAssert(isInstance_Object(realParent, &Class_Widget));
1841// return realParent == widget || hasParent_Widget(realParent, widget);
1842// }
1510 } 1843 }
1511 return iFalse; 1844 return iFalse;
1512} 1845}
@@ -1557,7 +1890,9 @@ iWidget *focus_Widget(void) {
1557} 1890}
1558 1891
1559void setHover_Widget(iWidget *d) { 1892void setHover_Widget(iWidget *d) {
1560 get_Window()->hover = d; 1893 iWindow *win = get_Window();
1894 iAssert(win);
1895 win->hover = d;
1561} 1896}
1562 1897
1563iWidget *hover_Widget(void) { 1898iWidget *hover_Widget(void) {
@@ -1646,6 +1981,10 @@ void postCommand_Widget(const iAnyObject *d, const char *cmd, ...) {
1646 } 1981 }
1647 if (!isGlobal) { 1982 if (!isGlobal) {
1648 iAssert(isInstance_Object(d, &Class_Widget)); 1983 iAssert(isInstance_Object(d, &Class_Widget));
1984 if (type_Window(window_Widget(d)) == popup_WindowType) {
1985 postCommandf_Root(((const iWidget *) d)->root, "cancel popup:1 ptr:%p", d);
1986 d = userData_Object(root_Widget(d));
1987 }
1649 appendFormat_String(&str, " ptr:%p", d); 1988 appendFormat_String(&str, " ptr:%p", d);
1650 } 1989 }
1651 postCommandString_Root(((const iWidget *) d)->root, &str); 1990 postCommandString_Root(((const iWidget *) d)->root, &str);
@@ -1653,11 +1992,20 @@ void postCommand_Widget(const iAnyObject *d, const char *cmd, ...) {
1653} 1992}
1654 1993
1655void refresh_Widget(const iAnyObject *d) { 1994void refresh_Widget(const iAnyObject *d) {
1995 if (!d) return;
1656 /* TODO: Could be widget specific, if parts of the tree are cached. */ 1996 /* TODO: Could be widget specific, if parts of the tree are cached. */
1657 /* TODO: The visbuffer in DocumentWidget and ListWidget could be moved to be a general 1997 /* TODO: The visbuffer in DocumentWidget and ListWidget could be moved to be a general
1658 purpose feature of Widget. */ 1998 purpose feature of Widget. */
1659 iAssert(isInstance_Object(d, &Class_Widget)); 1999 iAssert(isInstance_Object(d, &Class_Widget));
1660 iUnused(d); 2000 /* Mark draw buffers invalid. */
2001 for (const iWidget *w = d; w; w = w->parent) {
2002 if (w->drawBuf) {
2003// if (w->drawBuf->isValid) {
2004// printf("[%p] drawbuffer invalidated by %p\n", w, d); fflush(stdout);
2005// }
2006 w->drawBuf->isValid = iFalse;
2007 }
2008 }
1661 postRefresh_App(); 2009 postRefresh_App();
1662} 2010}
1663 2011
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 1a944c0a..9243c00a 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -34,7 +34,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
34#include <the_Foundation/string.h> 34#include <the_Foundation/string.h>
35#include <SDL_events.h> 35#include <SDL_events.h>
36 36
37iDeclareType(Root) /* each widget is associated with a Root */ 37iDeclareType(Root) /* each widget is associated with a Root */
38iDeclareType(Window) /* each Root is inside a Window */
38 39
39#define iDeclareWidgetClass(className) \ 40#define iDeclareWidgetClass(className) \
40 iDeclareType(className); \ 41 iDeclareType(className); \
@@ -120,6 +121,7 @@ enum iWidgetFlag {
120#define destroyPending_WidgetFlag iBit64(61) 121#define destroyPending_WidgetFlag iBit64(61)
121#define leftEdgeDraggable_WidgetFlag iBit64(62) 122#define leftEdgeDraggable_WidgetFlag iBit64(62)
122#define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */ 123#define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */
124#define nativeMenu_WidgetFlag iBit64(64)
123 125
124enum iWidgetAddPos { 126enum iWidgetAddPos {
125 back_WidgetAddPos, 127 back_WidgetAddPos,
@@ -131,6 +133,8 @@ enum iWidgetFocusDir {
131 backward_WidgetFocusDir, 133 backward_WidgetFocusDir,
132}; 134};
133 135
136iDeclareType(WidgetDrawBuffer)
137
134struct Impl_Widget { 138struct Impl_Widget {
135 iObject object; 139 iObject object;
136 iString id; 140 iString id;
@@ -148,6 +152,7 @@ struct Impl_Widget {
148 iWidget * parent; 152 iWidget * parent;
149 iBool (*commandHandler)(iWidget *, const char *); 153 iBool (*commandHandler)(iWidget *, const char *);
150 iRoot * root; 154 iRoot * root;
155 iWidgetDrawBuffer *drawBuf;
151}; 156};
152 157
153iDeclareObjectConstruction(Widget) 158iDeclareObjectConstruction(Widget)
@@ -182,6 +187,7 @@ void releaseChildren_Widget (iWidget *);
182 - inner: 0,0 is at the top left corner of the widget */ 187 - inner: 0,0 is at the top left corner of the widget */
183 188
184iWidget * root_Widget (const iWidget *); 189iWidget * root_Widget (const iWidget *);
190iWindow * window_Widget (const iAnyObject *);
185const iString * id_Widget (const iWidget *); 191const iString * id_Widget (const iWidget *);
186int64_t flags_Widget (const iWidget *); 192int64_t flags_Widget (const iWidget *);
187iRect bounds_Widget (const iWidget *); /* outer bounds */ 193iRect bounds_Widget (const iWidget *); /* outer bounds */
@@ -201,8 +207,15 @@ iAny * findFocusable_Widget (const iWidget *startFrom, enum iWidgetF
201iAny * findOverflowScrollable_Widget (iWidget *); 207iAny * findOverflowScrollable_Widget (iWidget *);
202size_t childCount_Widget (const iWidget *); 208size_t childCount_Widget (const iWidget *);
203void draw_Widget (const iWidget *); 209void draw_Widget (const iWidget *);
210void drawLayerEffects_Widget (const iWidget *);
204void drawBackground_Widget (const iWidget *); 211void drawBackground_Widget (const iWidget *);
205void drawChildren_Widget (const iWidget *); 212void drawChildren_Widget (const iWidget *);
213void drawRoot_Widget (const iWidget *); /* root only */
214void setDrawBufferEnabled_Widget (iWidget *, iBool enable);
215
216iLocalDef iBool isDrawBufferEnabled_Widget(const iWidget *d) {
217 return d && d->drawBuf;
218}
206 219
207iLocalDef int width_Widget(const iAnyObject *d) { 220iLocalDef int width_Widget(const iAnyObject *d) {
208 if (d) { 221 if (d) {
@@ -276,6 +289,18 @@ void refresh_Widget (const iAnyObject *);
276 289
277iBool equalWidget_Command (const char *cmd, const iWidget *widget, const char *checkCommand); 290iBool equalWidget_Command (const char *cmd, const iWidget *widget, const char *checkCommand);
278 291
292iDeclareType(WidgetScrollInfo)
293
294struct Impl_WidgetScrollInfo {
295 int height; /* widget's height */
296 int avail; /* available height */
297 float normScroll;
298 int thumbY; /* window coords */
299 int thumbHeight;
300};
301
302void scrollInfo_Widget (const iWidget *, iWidgetScrollInfo *info);
303
279int backgroundFadeColor_Widget (void); 304int backgroundFadeColor_Widget (void);
280 305
281void setFocus_Widget (iWidget *); 306void setFocus_Widget (iWidget *);
diff --git a/src/ui/window.c b/src/ui/window.c
index f8391ed9..066ea102 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
30#include "keys.h" 30#include "keys.h"
31#include "labelwidget.h" 31#include "labelwidget.h"
32#include "documentwidget.h" 32#include "documentwidget.h"
33#include "sidebarwidget.h"
33#include "paint.h" 34#include "paint.h"
34#include "root.h" 35#include "root.h"
35#include "touch.h" 36#include "touch.h"
@@ -57,7 +58,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
57#include "stb_image.h" 58#include "stb_image.h"
58#include "stb_image_resize.h" 59#include "stb_image_resize.h"
59 60
60static iWindow *theWindow_ = NULL; 61static iWindow * theWindow_;
62static iMainWindow *theMainWindow_;
61 63
62#if defined (iPlatformApple) || defined (iPlatformLinux) || defined (iPlatformOther) 64#if defined (iPlatformApple) || defined (iPlatformLinux) || defined (iPlatformOther)
63static float initialUiScale_ = 1.0f; 65static float initialUiScale_ = 1.0f;
@@ -67,7 +69,10 @@ static float initialUiScale_ = 1.1f;
67 69
68static iBool isOpenGLRenderer_; 70static iBool isOpenGLRenderer_;
69 71
70iDefineTypeConstructionArgs(Window, (iRect rect), rect) 72iDefineTypeConstructionArgs(Window,
73 (enum iWindowType type, iRect rect, uint32_t flags),
74 type, rect, flags)
75iDefineTypeConstructionArgs(MainWindow, (iRect rect), rect)
71 76
72/* TODO: Define menus per platform. */ 77/* TODO: Define menus per platform. */
73 78
@@ -116,6 +121,7 @@ static const iMenuItem viewMenuItems_[] = {
116static iMenuItem bookmarksMenuItems_[] = { 121static iMenuItem bookmarksMenuItems_[] = {
117 { "${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, 122 { "${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
118 { "${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, 123 { "${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
124 { "${menu.newfolder}", 0, 0, "bookmarks.addfolder" },
119 { "---", 0, 0, NULL }, 125 { "---", 0, 0, NULL },
120 { "${menu.import.links}", 0, 0, "bookmark.links confirm:1" }, 126 { "${menu.import.links}", 0, 0, "bookmark.links confirm:1" },
121 { "---", 0, 0, NULL }, 127 { "---", 0, 0, NULL },
@@ -124,6 +130,8 @@ static iMenuItem bookmarksMenuItems_[] = {
124 { "${macos.menu.bookmarks.bytime}", 0, 0, "open url:about:bookmarks?created" }, 130 { "${macos.menu.bookmarks.bytime}", 0, 0, "open url:about:bookmarks?created" },
125 { "${menu.feeds.entrylist}", 0, 0, "open url:about:feeds" }, 131 { "${menu.feeds.entrylist}", 0, 0, "open url:about:feeds" },
126 { "---", 0, 0, NULL }, 132 { "---", 0, 0, NULL },
133 { "${menu.sort.alpha}", 0, 0, "bookmarks.sort" },
134 { "---", 0, 0, NULL },
127 { "${menu.bookmarks.refresh}", 0, 0, "bookmarks.reload.remote" }, 135 { "${menu.bookmarks.refresh}", 0, 0, "bookmarks.reload.remote" },
128 { "${menu.feeds.refresh}", SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh" }, 136 { "${menu.feeds.refresh}", SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh" },
129}; 137};
@@ -169,17 +177,17 @@ int numRoots_Window(const iWindow *d) {
169 return num; 177 return num;
170} 178}
171 179
172static void windowSizeChanged_Window_(iWindow *d) { 180static void windowSizeChanged_MainWindow_(iMainWindow *d) {
173 const int numRoots = numRoots_Window(d); 181 const int numRoots = numRoots_Window(as_Window(d));
174 const iInt2 rootSize = d->size; 182 const iInt2 rootSize = d->base.size;
175 const int weights[2] = { 183 const int weights[2] = {
176 d->roots[0] ? (d->splitMode & twoToOne_WindowSplit ? 2 : 1) : 0, 184 d->base.roots[0] ? (d->splitMode & twoToOne_WindowSplit ? 2 : 1) : 0,
177 d->roots[1] ? (d->splitMode & oneToTwo_WindowSplit ? 2 : 1) : 0, 185 d->base.roots[1] ? (d->splitMode & oneToTwo_WindowSplit ? 2 : 1) : 0,
178 }; 186 };
179 const int totalWeight = weights[0] + weights[1]; 187 const int totalWeight = weights[0] + weights[1];
180 int w = 0; 188 int w = 0;
181 iForIndices(i, d->roots) { 189 iForIndices(i, d->base.roots) {
182 iRoot *root = d->roots[i]; 190 iRoot *root = d->base.roots[i];
183 if (root) { 191 if (root) {
184 iRect *rect = &root->widget->rect; 192 iRect *rect = &root->widget->rect;
185 /* Horizontal split frame. */ 193 /* Horizontal split frame. */
@@ -199,26 +207,27 @@ static void windowSizeChanged_Window_(iWindow *d) {
199 } 207 }
200} 208}
201 209
202static void setupUserInterface_Window(iWindow *d) { 210static void setupUserInterface_MainWindow(iMainWindow *d) {
203#if defined (iHaveNativeMenus) 211#if defined (iHaveNativeMenus)
204 insertMacMenus_(); 212 insertMacMenus_();
205#endif 213#endif
206 /* One root is created by default. */ 214 /* One root is created by default. */
207 d->roots[0] = new_Root(); 215 d->base.roots[0] = new_Root();
208 setCurrent_Root(d->roots[0]); 216 d->base.roots[0]->window = as_Window(d);
209 createUserInterface_Root(d->roots[0]); 217 setCurrent_Root(d->base.roots[0]);
218 createUserInterface_Root(d->base.roots[0]);
210 setCurrent_Root(NULL); 219 setCurrent_Root(NULL);
211 /* One of the roots always has keyboard input focus. */ 220 /* One of the roots always has keyboard input focus. */
212 d->keyRoot = d->roots[0]; 221 d->base.keyRoot = d->base.roots[0];
213} 222}
214 223
215static void updateSize_Window_(iWindow *d, iBool notifyAlways) { 224static void updateSize_MainWindow_(iMainWindow *d, iBool notifyAlways) {
216 iInt2 *size = &d->size; 225 iInt2 *size = &d->base.size;
217 const iInt2 oldSize = *size; 226 const iInt2 oldSize = *size;
218 SDL_GetRendererOutputSize(d->render, &size->x, &size->y); 227 SDL_GetRendererOutputSize(d->base.render, &size->x, &size->y);
219 size->y -= d->keyboardHeight; 228 size->y -= d->keyboardHeight;
220 if (notifyAlways || !isEqual_I2(oldSize, *size)) { 229 if (notifyAlways || !isEqual_I2(oldSize, *size)) {
221 windowSizeChanged_Window_(d); 230 windowSizeChanged_MainWindow_(d);
222 if (!isEqual_I2(*size, d->place.lastNotifiedSize)) { 231 if (!isEqual_I2(*size, d->place.lastNotifiedSize)) {
223 const iBool isHoriz = (d->place.lastNotifiedSize.x != size->x); 232 const iBool isHoriz = (d->place.lastNotifiedSize.x != size->x);
224 const iBool isVert = (d->place.lastNotifiedSize.y != size->y); 233 const iBool isVert = (d->place.lastNotifiedSize.y != size->y);
@@ -234,8 +243,8 @@ static void updateSize_Window_(iWindow *d, iBool notifyAlways) {
234 } 243 }
235} 244}
236 245
237void drawWhileResizing_Window(iWindow *d, int w, int h) { 246void drawWhileResizing_MainWindow(iMainWindow *d, int w, int h) {
238 draw_Window(d); 247 draw_MainWindow(d);
239} 248}
240 249
241static float pixelRatio_Window_(const iWindow *d) { 250static float pixelRatio_Window_(const iWindow *d) {
@@ -308,7 +317,7 @@ static iRoot *rootAt_Window_(const iWindow *d, iInt2 coord) {
308} 317}
309 318
310#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 319#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
311static SDL_HitTestResult hitTest_Window_(SDL_Window *win, const SDL_Point *pos, void *data) { 320static SDL_HitTestResult hitTest_MainWindow_(SDL_Window *win, const SDL_Point *pos, void *data) {
312 iWindow *d = data; 321 iWindow *d = data;
313 iAssert(d->win == win); 322 iAssert(d->win == win);
314 if (SDL_GetWindowFlags(d->win) & (SDL_WINDOW_MOUSE_CAPTURE | SDL_WINDOW_FULLSCREEN_DESKTOP)) { 323 if (SDL_GetWindowFlags(d->win) & (SDL_WINDOW_MOUSE_CAPTURE | SDL_WINDOW_FULLSCREEN_DESKTOP)) {
@@ -361,19 +370,22 @@ SDL_HitTestResult hitTest_Window(const iWindow *d, iInt2 pos) {
361#endif 370#endif
362 371
363iBool create_Window_(iWindow *d, iRect rect, uint32_t flags) { 372iBool create_Window_(iWindow *d, iRect rect, uint32_t flags) {
364 flags |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN; 373 flags |= SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN;
374 if (d->type == main_WindowType) {
375 flags |= SDL_WINDOW_RESIZABLE;
365#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 376#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
366 if (prefs_App()->customFrame) { 377 if (prefs_App()->customFrame) {
367 /* We are drawing a custom frame so hide the default one. */ 378 /* We are drawing a custom frame so hide the default one. */
368 flags |= SDL_WINDOW_BORDERLESS; 379 flags |= SDL_WINDOW_BORDERLESS;
369 } 380 }
370#endif 381#endif
382 }
371 if (SDL_CreateWindowAndRenderer( 383 if (SDL_CreateWindowAndRenderer(
372 width_Rect(rect), height_Rect(rect), flags, &d->win, &d->render)) { 384 width_Rect(rect), height_Rect(rect), flags, &d->win, &d->render)) {
373 return iFalse; 385 return iFalse;
374 } 386 }
375#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 387#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
376 if (prefs_App()->customFrame) { 388 if (type_Window(d) == main_WindowType && prefs_App()->customFrame) {
377 /* Register a handler for window hit testing (drag, resize). */ 389 /* Register a handler for window hit testing (drag, resize). */
378 SDL_SetWindowHitTest(d->win, hitTest_Window_, d); 390 SDL_SetWindowHitTest(d->win, hitTest_Window_, d);
379 SDL_SetWindowResizable(d->win, SDL_TRUE); 391 SDL_SetWindowResizable(d->win, SDL_TRUE);
@@ -397,40 +409,27 @@ static SDL_Surface *loadImage_(const iBlock *data, int resized) {
397 pixels, w, h, 8 * num, w * num, SDL_PIXELFORMAT_RGBA32); 409 pixels, w, h, 8 * num, w * num, SDL_PIXELFORMAT_RGBA32);
398} 410}
399 411
400void init_Window(iWindow *d, iRect rect) { 412void init_Window(iWindow *d, enum iWindowType type, iRect rect, uint32_t flags) {
401 theWindow_ = d; 413 d->type = type;
402 d->win = NULL; 414 d->win = NULL;
403 d->size = zero_I2(); /* will be updated below */ 415 d->size = zero_I2(); /* will be updated below */
404 iZap(d->roots); 416 d->hover = NULL;
405 d->splitMode = d->pendingSplitMode = 0; 417 d->lastHover = NULL;
406 d->pendingSplitUrl = new_String(); 418 d->mouseGrab = NULL;
407 d->hover = NULL; 419 d->focus = NULL;
408 d->mouseGrab = NULL;
409 d->focus = NULL;
410 iZap(d->cursors);
411 d->place.initialPos = rect.pos;
412 d->place.normalRect = rect;
413 d->place.lastNotifiedSize = zero_I2();
414 d->place.snap = 0;
415 d->pendingCursor = NULL; 420 d->pendingCursor = NULL;
416 d->isDrawFrozen = iTrue; 421 d->isExposed = iFalse;
417 d->isExposed = iFalse; 422 d->isMinimized = iFalse;
418 d->isMinimized = iFalse;
419 d->isInvalidated = iFalse; /* set when posting event, to avoid repeated events */ 423 d->isInvalidated = iFalse; /* set when posting event, to avoid repeated events */
420 d->isMouseInside = iTrue; 424 d->isMouseInside = iTrue;
421 d->ignoreClick = iFalse; 425 d->ignoreClick = iFalse;
422 d->focusGainedAt = 0; 426 d->focusGainedAt = 0;
423 d->keyboardHeight = 0; 427 d->presentTime = 0.0;
424 init_Anim(&d->rootOffset, 0.0f); 428 d->frameTime = SDL_GetTicks();
425 uint32_t flags = 0; 429 d->keyRoot = NULL;
426#if defined (iPlatformAppleDesktop) 430 d->borderShadow = NULL;
427 SDL_SetHint(SDL_HINT_RENDER_DRIVER, shouldDefaultToMetalRenderer_MacOS() ? "metal" : "opengl"); 431 iZap(d->roots);
428#elif defined (iPlatformAppleMobile) 432 iZap(d->cursors);
429 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal");
430#else
431 flags |= SDL_WINDOW_OPENGL;
432#endif
433 SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
434 /* First try SDL's default renderer that should be the best option. */ 433 /* First try SDL's default renderer that should be the best option. */
435 if (forceSoftwareRender_App() || !create_Window_(d, rect, flags)) { 434 if (forceSoftwareRender_App() || !create_Window_(d, rect, flags)) {
436 /* No luck, maybe software only? This should always work as long as there is a display. */ 435 /* No luck, maybe software only? This should always work as long as there is a display. */
@@ -443,36 +442,95 @@ void init_Window(iWindow *d, iRect rect) {
443 if (left_Rect(rect) >= 0 || top_Rect(rect) >= 0) { 442 if (left_Rect(rect) >= 0 || top_Rect(rect) >= 0) {
444 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); 443 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect));
445 } 444 }
445 SDL_GetRendererOutputSize(d->render, &d->size.x, &d->size.y);
446 drawBlank_Window_(d);
447 d->pixelRatio = pixelRatio_Window_(d); /* point/pixel conversion */
448 d->displayScale = displayScale_Window_(d);
449 d->uiScale = initialUiScale_;
450 /* TODO: Ratios, scales, and metrics must be window-specific, not global. */
451 setScale_Metrics(d->pixelRatio * d->displayScale * d->uiScale);
452 d->text = new_Text(d->render);
453}
454
455static void deinitRoots_Window_(iWindow *d) {
456 iRecycle();
457 iForIndices(i, d->roots) {
458 if (d->roots[i]) {
459 setCurrent_Root(d->roots[i]);
460 delete_Root(d->roots[i]);
461 d->roots[i] = NULL;
462 }
463 }
464 setCurrent_Root(NULL);
465}
466
467void deinit_Window(iWindow *d) {
468 if (d->type == popup_WindowType) {
469 removePopup_App(d);
470 }
471 deinitRoots_Window_(d);
472 delete_Text(d->text);
473 SDL_DestroyRenderer(d->render);
474 SDL_DestroyWindow(d->win);
475 iForIndices(i, d->cursors) {
476 if (d->cursors[i]) {
477 SDL_FreeCursor(d->cursors[i]);
478 }
479 }
480}
481
482void init_MainWindow(iMainWindow *d, iRect rect) {
483 theWindow_ = &d->base;
484 theMainWindow_ = d;
485 uint32_t flags = 0;
486#if defined (iPlatformAppleDesktop)
487 SDL_SetHint(SDL_HINT_RENDER_DRIVER, shouldDefaultToMetalRenderer_MacOS() ? "metal" : "opengl");
488#elif defined (iPlatformAppleMobile)
489 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal");
490#else
491 flags |= SDL_WINDOW_OPENGL;
492#endif
493 SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
494 init_Window(&d->base, main_WindowType, rect, flags);
495 d->isDrawFrozen = iTrue;
496 d->splitMode = 0;
497 d->pendingSplitMode = 0;
498 d->pendingSplitUrl = new_String();
499 d->place.initialPos = rect.pos;
500 d->place.normalRect = rect;
501 d->place.lastNotifiedSize = zero_I2();
502 d->place.snap = 0;
503 d->keyboardHeight = 0;
504#if defined(iPlatformMobile)
505 const iInt2 minSize = zero_I2(); /* windows aren't independently resizable */
506#else
446 const iInt2 minSize = init_I2(425, 325); 507 const iInt2 minSize = init_I2(425, 325);
447 SDL_SetWindowMinimumSize(d->win, minSize.x, minSize.y); 508#endif
448 SDL_SetWindowTitle(d->win, "Lagrange"); 509 SDL_SetWindowMinimumSize(d->base.win, minSize.x, minSize.y);
510 SDL_SetWindowTitle(d->base.win, "Lagrange");
449 /* Some info. */ { 511 /* Some info. */ {
450 SDL_RendererInfo info; 512 SDL_RendererInfo info;
451 SDL_GetRendererInfo(d->render, &info); 513 SDL_GetRendererInfo(d->base.render, &info);
452 isOpenGLRenderer_ = !iCmpStr(info.name, "opengl"); 514 isOpenGLRenderer_ = !iCmpStr(info.name, "opengl");
453 printf("[window] renderer: %s%s\n", info.name, 515 printf("[window] renderer: %s%s\n",
516 info.name,
454 info.flags & SDL_RENDERER_ACCELERATED ? " (accelerated)" : ""); 517 info.flags & SDL_RENDERER_ACCELERATED ? " (accelerated)" : "");
455#if !defined (NDEBUG) 518#if !defined(NDEBUG)
456 printf("[window] max texture size: %d x %d\n", 519 printf("[window] max texture size: %d x %d\n",
457 info.max_texture_width, 520 info.max_texture_width,
458 info.max_texture_height); 521 info.max_texture_height);
459 for (size_t i = 0; i < info.num_texture_formats; ++i) { 522 for (size_t i = 0; i < info.num_texture_formats; ++i) {
460 printf("[window] supported texture format: %s\n", SDL_GetPixelFormatName( 523 printf("[window] supported texture format: %s\n",
461 info.texture_formats[i])); 524 SDL_GetPixelFormatName(info.texture_formats[i]));
462 } 525 }
463#endif 526#endif
464 } 527 }
465 drawBlank_Window_(d); 528#if defined(iPlatformMsys)
466 d->pixelRatio = pixelRatio_Window_(d); /* point/pixel conversion */ 529 SDL_SetWindowMinimumSize(d->base.win, minSize.x * d->base.displayScale, minSize.y * d->base.displayScale);
467 d->displayScale = displayScale_Window_(d); 530 useExecutableIconResource_SDLWindow(d->base.win);
468 d->uiScale = initialUiScale_;
469 setScale_Metrics(d->pixelRatio * d->displayScale * d->uiScale);
470#if defined (iPlatformMsys)
471 SDL_SetWindowMinimumSize(d->win, minSize.x * d->displayScale, minSize.y * d->displayScale);
472 useExecutableIconResource_SDLWindow(d->win);
473#endif 531#endif
474#if defined (iPlatformLinux) 532#if defined (iPlatformLinux)
475 SDL_SetWindowMinimumSize(d->win, minSize.x * d->pixelRatio, minSize.y * d->pixelRatio); 533 SDL_SetWindowMinimumSize(d->win, minSize.x * d->base.pixelRatio, minSize.y * d->base.pixelRatio);
476 /* Load the window icon. */ { 534 /* Load the window icon. */ {
477 SDL_Surface *surf = loadImage_(&imageLagrange64_Embedded, 0); 535 SDL_Surface *surf = loadImage_(&imageLagrange64_Embedded, 0);
478 SDL_SetWindowIcon(d->win, surf); 536 SDL_SetWindowIcon(d->win, surf);
@@ -481,20 +539,16 @@ void init_Window(iWindow *d, iRect rect) {
481 } 539 }
482#endif 540#endif
483#if defined (iPlatformAppleMobile) 541#if defined (iPlatformAppleMobile)
484 setupWindow_iOS(d); 542 setupWindow_iOS(as_Window(d));
485#endif 543#endif
486 d->presentTime = 0.0; 544 setCurrent_Text(d->base.text);
487 d->frameTime = SDL_GetTicks(); 545 SDL_GetRendererOutputSize(d->base.render, &d->base.size.x, &d->base.size.y);
488 d->loadAnimTimer = 0; 546 setupUserInterface_MainWindow(d);
489 init_Text(d->render);
490 SDL_GetRendererOutputSize(d->render, &d->size.x, &d->size.y);
491 setupUserInterface_Window(d);
492 postCommand_App("~bindings.changed"); /* update from bindings */ 547 postCommand_App("~bindings.changed"); /* update from bindings */
493 //updateSize_Window_(d, iFalse);
494 /* Load the border shadow texture. */ { 548 /* Load the border shadow texture. */ {
495 SDL_Surface *surf = loadImage_(&imageShadow_Embedded, 0); 549 SDL_Surface *surf = loadImage_(&imageShadow_Embedded, 0);
496 d->borderShadow = SDL_CreateTextureFromSurface(d->render, surf); 550 d->base.borderShadow = SDL_CreateTextureFromSurface(d->base.render, surf);
497 SDL_SetTextureBlendMode(d->borderShadow, SDL_BLENDMODE_BLEND); 551 SDL_SetTextureBlendMode(d->base.borderShadow, SDL_BLENDMODE_BLEND);
498 free(surf->pixels); 552 free(surf->pixels);
499 SDL_FreeSurface(surf); 553 SDL_FreeSurface(surf);
500 } 554 }
@@ -504,36 +558,26 @@ void init_Window(iWindow *d, iRect rect) {
504 if (prefs_App()->customFrame) { 558 if (prefs_App()->customFrame) {
505 SDL_Surface *surf = loadImage_(&imageLagrange64_Embedded, appIconSize_Root()); 559 SDL_Surface *surf = loadImage_(&imageLagrange64_Embedded, appIconSize_Root());
506 SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); 560 SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0");
507 d->appIcon = SDL_CreateTextureFromSurface(d->render, surf); 561 d->appIcon = SDL_CreateTextureFromSurface(d->base.render, surf);
508 free(surf->pixels); 562 free(surf->pixels);
509 SDL_FreeSurface(surf); 563 SDL_FreeSurface(surf);
510 /* We need to observe non-client-area events. */ 564 /* We need to observe non-client-area events. */
511 SDL_EventState(SDL_SYSWMEVENT, SDL_TRUE); 565 SDL_EventState(SDL_SYSWMEVENT, SDL_TRUE);
512 } 566 }
513#endif 567#endif
568 SDL_HideWindow(d->base.win);
514} 569}
515 570
516void deinit_Window(iWindow *d) { 571void deinit_MainWindow(iMainWindow *d) {
517 iRecycle(); 572 deinitRoots_Window_(as_Window(d));
518 iForIndices(i, d->roots) { 573 if (theWindow_ == as_Window(d)) {
519 if (d->roots[i]) {
520 setCurrent_Root(d->roots[i]);
521 deinit_Root(d->roots[i]);
522 }
523 }
524 if (theWindow_ == d) {
525 theWindow_ = NULL; 574 theWindow_ = NULL;
526 } 575 }
527 setCurrent_Root(NULL); 576 if (theMainWindow_ == d) {
528 delete_String(d->pendingSplitUrl); 577 theMainWindow_ = NULL;
529 deinit_Text();
530 SDL_DestroyRenderer(d->render);
531 SDL_DestroyWindow(d->win);
532 iForIndices(i, d->cursors) {
533 if (d->cursors[i]) {
534 SDL_FreeCursor(d->cursors[i]);
535 }
536 } 578 }
579 delete_String(d->pendingSplitUrl);
580 deinit_Window(&d->base);
537} 581}
538 582
539SDL_Renderer *renderer_Window(const iWindow *d) { 583SDL_Renderer *renderer_Window(const iWindow *d) {
@@ -546,8 +590,8 @@ iInt2 maxTextureSize_Window(const iWindow *d) {
546 return init_I2(info.max_texture_width, info.max_texture_height); 590 return init_I2(info.max_texture_width, info.max_texture_height);
547} 591}
548 592
549iBool isFullscreen_Window(const iWindow *d) { 593iBool isFullscreen_MainWindow(const iMainWindow *d) {
550 return snap_Window(d) == fullscreen_WindowSnap; 594 return snap_MainWindow(d) == fullscreen_WindowSnap;
551} 595}
552 596
553iRoot *findRoot_Window(const iWindow *d, const iWidget *widget) { 597iRoot *findRoot_Window(const iWindow *d, const iWidget *widget) {
@@ -566,36 +610,41 @@ iRoot *otherRoot_Window(const iWindow *d, iRoot *root) {
566 return root == d->roots[0] && d->roots[1] ? d->roots[1] : d->roots[0]; 610 return root == d->roots[0] && d->roots[1] ? d->roots[1] : d->roots[0];
567} 611}
568 612
569static void invalidate_Window_(iWindow *d, iBool forced) { 613static void invalidate_MainWindow_(iMainWindow *d, iBool forced) {
570 if (d && (!d->isInvalidated || forced)) { 614 if (d && (!d->base.isInvalidated || forced)) {
571 d->isInvalidated = iTrue; 615 d->base.isInvalidated = iTrue;
572 resetFonts_Text(); 616 resetFonts_Text(text_Window(d));
573 postCommand_App("theme.changed auto:1"); /* forces UI invalidation */ 617 postCommand_App("theme.changed auto:1"); /* forces UI invalidation */
574 } 618 }
575} 619}
576 620
577void invalidate_Window(iWindow *d) { 621void invalidate_Window(iAnyWindow *d) {
578 invalidate_Window_(d, iFalse); 622 if (type_Window(d) == main_WindowType) {
623 invalidate_MainWindow_(as_MainWindow(d), iFalse);
624 }
625 else {
626 iAssert(type_Window(d) == main_WindowType);
627 }
579} 628}
580 629
581static iBool isNormalPlacement_Window_(const iWindow *d) { 630static iBool isNormalPlacement_MainWindow_(const iMainWindow *d) {
582 if (d->isDrawFrozen) return iFalse; 631 if (d->isDrawFrozen) return iFalse;
583#if defined (iPlatformApple) 632#if defined (iPlatformApple)
584 /* Maximized mode is not special on macOS. */ 633 /* Maximized mode is not special on macOS. */
585 if (snap_Window(d) == maximized_WindowSnap) { 634 if (snap_MainWindow(d) == maximized_WindowSnap) {
586 return iTrue; 635 return iTrue;
587 } 636 }
588#endif 637#endif
589 if (snap_Window(d)) return iFalse; 638 if (snap_MainWindow(d)) return iFalse;
590 return !(SDL_GetWindowFlags(d->win) & SDL_WINDOW_MINIMIZED); 639 return !(SDL_GetWindowFlags(d->base.win) & SDL_WINDOW_MINIMIZED);
591} 640}
592 641
593static iBool unsnap_Window_(iWindow *d, const iInt2 *newPos) { 642static iBool unsnap_MainWindow_(iMainWindow *d, const iInt2 *newPos) {
594#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 643#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
595 if (!prefs_App()->customFrame) { 644 if (!prefs_App()->customFrame) {
596 return iFalse; 645 return iFalse;
597 } 646 }
598 const int snap = snap_Window(d); 647 const int snap = snap_MainWindow(d);
599 if (snap == yMaximized_WindowSnap || snap == left_WindowSnap || snap == right_WindowSnap) { 648 if (snap == yMaximized_WindowSnap || snap == left_WindowSnap || snap == right_WindowSnap) {
600 if (!newPos || (d->place.lastHit == SDL_HITTEST_RESIZE_LEFT || 649 if (!newPos || (d->place.lastHit == SDL_HITTEST_RESIZE_LEFT ||
601 d->place.lastHit == SDL_HITTEST_RESIZE_RIGHT)) { 650 d->place.lastHit == SDL_HITTEST_RESIZE_RIGHT)) {
@@ -603,21 +652,21 @@ static iBool unsnap_Window_(iWindow *d, const iInt2 *newPos) {
603 } 652 }
604 if (newPos) { 653 if (newPos) {
605 SDL_Rect usable; 654 SDL_Rect usable;
606 SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(d->win), &usable); 655 SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(d->base.win), &usable);
607 /* Snap to top. */ 656 /* Snap to top. */
608 if (snap == yMaximized_WindowSnap && 657 if (snap == yMaximized_WindowSnap &&
609 iAbs(newPos->y - usable.y) < lineHeight_Text(uiContent_FontId) * 2) { 658 iAbs(newPos->y - usable.y) < lineHeight_Text(uiContent_FontId) * 2) {
610 setSnap_Window(d, redo_WindowSnap | yMaximized_WindowSnap); 659 setSnap_MainWindow(d, redo_WindowSnap | yMaximized_WindowSnap);
611 return iFalse; 660 return iFalse;
612 } 661 }
613 } 662 }
614 } 663 }
615 if (snap && snap != fullscreen_WindowSnap) { 664 if (snap && snap != fullscreen_WindowSnap) {
616 if (snap_Window(d) == yMaximized_WindowSnap && newPos) { 665 if (snap_MainWindow(d) == yMaximized_WindowSnap && newPos) {
617 d->place.normalRect.pos = *newPos; 666 d->place.normalRect.pos = *newPos;
618 } 667 }
619 //printf("unsnap\n"); fflush(stdout); 668 //printf("unsnap\n"); fflush(stdout);
620 setSnap_Window(d, none_WindowSnap); 669 setSnap_MainWindow(d, none_WindowSnap);
621 return iTrue; 670 return iTrue;
622 } 671 }
623#endif 672#endif
@@ -627,7 +676,7 @@ static iBool unsnap_Window_(iWindow *d, const iInt2 *newPos) {
627static void notifyMetricsChange_Window_(const iWindow *d) { 676static void notifyMetricsChange_Window_(const iWindow *d) {
628 /* Dynamic UI metrics change. Widgets need to update themselves. */ 677 /* Dynamic UI metrics change. Widgets need to update themselves. */
629 setScale_Metrics(d->pixelRatio * d->displayScale * d->uiScale); 678 setScale_Metrics(d->pixelRatio * d->displayScale * d->uiScale);
630 resetFonts_Text(); 679 resetFonts_Text(d->text);
631 postCommand_App("metrics.changed"); 680 postCommand_App("metrics.changed");
632} 681}
633 682
@@ -649,146 +698,178 @@ static void checkPixelRatioChange_Window_(iWindow *d) {
649} 698}
650 699
651static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) { 700static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
701 if (ev->windowID != SDL_GetWindowID(d->win)) {
702 return iFalse;
703 }
652 switch (ev->event) { 704 switch (ev->event) {
653#if defined (iPlatformDesktop)
654 case SDL_WINDOWEVENT_EXPOSED: 705 case SDL_WINDOWEVENT_EXPOSED:
655 if (!d->isExposed) { 706 d->isExposed = iTrue;
656 drawBlank_Window_(d); /* avoid showing system-provided contents */ 707 postRefresh_App();
657 d->isExposed = iTrue; 708 return iTrue;
658 } 709 case SDL_WINDOWEVENT_RESTORED:
710 case SDL_WINDOWEVENT_SHOWN:
711 postRefresh_App();
712 return iTrue;
713 case SDL_WINDOWEVENT_FOCUS_LOST:
714 /* Popup windows are currently only used for menus. */
715 closeMenu_Widget(d->roots[0]->widget);
716 return iTrue;
717 case SDL_WINDOWEVENT_LEAVE:
718 unhover_Widget();
719 d->isMouseInside = iFalse;
720 //postCommand_App("window.mouse.exited");
721// SDL_SetWindowInputFocus(mainWindow_App()->base.win);
722 printf("mouse leaves popup\n"); fflush(stdout);
723 //SDL_RaiseWindow(mainWindow_App()->base.win);
724 postRefresh_App();
725 return iTrue;
726 case SDL_WINDOWEVENT_ENTER:
727 d->isMouseInside = iTrue;
728 //postCommand_App("window.mouse.entered");
729 printf("mouse enters popup\n"); fflush(stdout);
730 return iTrue;
731 }
732 return iFalse;
733}
734
735static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent *ev) {
736 switch (ev->event) {
737#if defined(iPlatformDesktop)
738 case SDL_WINDOWEVENT_EXPOSED:
739 d->base.isExposed = iTrue;
659 /* Since we are manually controlling when to redraw the window, we are responsible 740 /* Since we are manually controlling when to redraw the window, we are responsible
660 for ensuring that window contents get redrawn after expose events. Under certain 741 for ensuring that window contents get redrawn after expose events. Under certain
661 circumstances (e.g., under openbox), not doing this would mean that the window 742 circumstances (e.g., under openbox), not doing this would mean that the window
662 is missing contents until other events trigger a refresh. */ 743 is missing contents until other events trigger a refresh. */
663 postRefresh_App(); 744 postRefresh_App();
664#if defined (LAGRANGE_ENABLE_WINDOWPOS_FIX) 745#if defined(LAGRANGE_ENABLE_WINDOWPOS_FIX)
665 if (d->place.initialPos.x >= 0) { 746 if (d->place.initialPos.x >= 0) {
666 int bx, by; 747 int bx, by;
667 SDL_GetWindowBordersSize(d->win, &by, &bx, NULL, NULL); 748 SDL_GetWindowBordersSize(d->win, &by, &bx, NULL, NULL);
668 SDL_SetWindowPosition(d->win, d->place.initialPos.x + bx, d->place.initialPos.y + by); 749 SDL_SetWindowPosition(
750 d->win, d->place.initialPos.x + bx, d->place.initialPos.y + by);
669 d->place.initialPos = init1_I2(-1); 751 d->place.initialPos = init1_I2(-1);
670 } 752 }
671#endif 753#endif
672 return iFalse; 754 return iFalse;
673 case SDL_WINDOWEVENT_MOVED: { 755 case SDL_WINDOWEVENT_MOVED: {
674 if (d->isMinimized) { 756 if (d->base.isMinimized) {
675 return iFalse; 757 return iFalse;
676 } 758 }
677 checkPixelRatioChange_Window_(d); 759 checkPixelRatioChange_Window_(as_Window(d));
678 const iInt2 newPos = init_I2(ev->data1, ev->data2); 760 const iInt2 newPos = init_I2(ev->data1, ev->data2);
679 if (isEqual_I2(newPos, init1_I2(-32000))) { /* magic! */ 761 if (isEqual_I2(newPos, init1_I2(-32000))) { /* magic! */
680 /* Maybe minimized? Seems like a Windows constant of some kind. */ 762 /* Maybe minimized? Seems like a Windows constant of some kind. */
681 d->isMinimized = iTrue; 763 d->base.isMinimized = iTrue;
682 return iFalse; 764 return iFalse;
683 } 765 }
684#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 766#if defined(LAGRANGE_ENABLE_CUSTOM_FRAME)
685 /* Set the snap position depending on where the mouse cursor is. */ 767 /* Set the snap position depending on where the mouse cursor is. */
686 if (prefs_App()->customFrame) { 768 if (prefs_App()->customFrame) {
687 SDL_Rect usable; 769 SDL_Rect usable;
688 iInt2 mouse = cursor_Win32(); /* SDL is unaware of the current cursor pos */ 770 iInt2 mouse = cursor_Win32(); /* SDL is unaware of the current cursor pos */
689 SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(d->win), &usable); 771 SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(d->base.win), &usable);
690 const iBool isTop = iAbs(mouse.y - usable.y) < gap_UI * 20; 772 const iBool isTop = iAbs(mouse.y - usable.y) < gap_UI * 20;
691 const iBool isBottom = iAbs(usable.y + usable.h - mouse.y) < gap_UI * 20; 773 const iBool isBottom = iAbs(usable.y + usable.h - mouse.y) < gap_UI * 20;
692 if (iAbs(mouse.x - usable.x) < gap_UI) { 774 if (iAbs(mouse.x - usable.x) < gap_UI) {
693 setSnap_Window(d, 775 setSnap_MainWindow(d,
694 redo_WindowSnap | left_WindowSnap | 776 redo_WindowSnap | left_WindowSnap |
695 (isTop ? topBit_WindowSnap : 0) | 777 (isTop ? topBit_WindowSnap : 0) |
696 (isBottom ? bottomBit_WindowSnap : 0)); 778 (isBottom ? bottomBit_WindowSnap : 0));
697 return iTrue; 779 return iTrue;
698 } 780 }
699 if (iAbs(mouse.x - usable.x - usable.w) < gap_UI) { 781 if (iAbs(mouse.x - usable.x - usable.w) < gap_UI) {
700 setSnap_Window(d, 782 setSnap_MainWindow(d,
701 redo_WindowSnap | right_WindowSnap | 783 redo_WindowSnap | right_WindowSnap |
702 (isTop ? topBit_WindowSnap : 0) | 784 (isTop ? topBit_WindowSnap : 0) |
703 (isBottom ? bottomBit_WindowSnap : 0)); 785 (isBottom ? bottomBit_WindowSnap : 0));
704 return iTrue; 786 return iTrue;
705 } 787 }
706 if (iAbs(mouse.y - usable.y) < 2) { 788 if (iAbs(mouse.y - usable.y) < 2) {
707 setSnap_Window(d, 789 setSnap_MainWindow(d,
708 redo_WindowSnap | (d->place.lastHit == SDL_HITTEST_RESIZE_TOP 790 redo_WindowSnap | (d->place.lastHit == SDL_HITTEST_RESIZE_TOP
709 ? yMaximized_WindowSnap 791 ? yMaximized_WindowSnap
710 : maximized_WindowSnap)); 792 : maximized_WindowSnap));
711 return iTrue; 793 return iTrue;
712 } 794 }
713 } 795 }
714#endif /* defined LAGRANGE_ENABLE_CUSTOM_FRAME */ 796#endif /* defined LAGRANGE_ENABLE_CUSTOM_FRAME */
715 //printf("MOVED: %d, %d\n", ev->data1, ev->data2); fflush(stdout); 797 if (unsnap_MainWindow_(d, &newPos)) {
716 if (unsnap_Window_(d, &newPos)) {
717 return iTrue; 798 return iTrue;
718 } 799 }
719 if (isNormalPlacement_Window_(d)) { 800 if (isNormalPlacement_MainWindow_(d)) {
720 d->place.normalRect.pos = newPos; 801 d->place.normalRect.pos = newPos;
721 //printf("normal rect set (move)\n"); fflush(stdout); 802 // printf("normal rect set (move)\n"); fflush(stdout);
722 iInt2 border = zero_I2(); 803 iInt2 border = zero_I2();
723#if !defined (iPlatformApple) 804#if !defined(iPlatformApple)
724 SDL_GetWindowBordersSize(d->win, &border.y, &border.x, NULL, NULL); 805 SDL_GetWindowBordersSize(d->base.win, &border.y, &border.x, NULL, NULL);
725#endif 806#endif
726 d->place.normalRect.pos = max_I2(zero_I2(), sub_I2(d->place.normalRect.pos, border)); 807 d->place.normalRect.pos =
808 max_I2(zero_I2(), sub_I2(d->place.normalRect.pos, border));
727 } 809 }
728 return iTrue; 810 return iTrue;
729 } 811 }
730 case SDL_WINDOWEVENT_RESIZED: 812 case SDL_WINDOWEVENT_RESIZED:
731 if (d->isMinimized) { 813 if (d->base.isMinimized) {
732 //updateSize_Window_(d, iTrue); 814 // updateSize_Window_(d, iTrue);
733 return iTrue; 815 return iTrue;
734 } 816 }
735 if (unsnap_Window_(d, NULL)) { 817 if (unsnap_MainWindow_(d, NULL)) {
736 return iTrue; 818 return iTrue;
737 } 819 }
738 if (isNormalPlacement_Window_(d)) { 820 if (isNormalPlacement_MainWindow_(d)) {
739 d->place.normalRect.size = init_I2(ev->data1, ev->data2); 821 d->place.normalRect.size = init_I2(ev->data1, ev->data2);
740 //printf("normal rect set (resize)\n"); fflush(stdout); 822 // printf("normal rect set (resize)\n"); fflush(stdout);
741 } 823 }
742 checkPixelRatioChange_Window_(d); 824 checkPixelRatioChange_Window_(as_Window(d));
743 //updateSize_Window_(d, iTrue /* we were already redrawing during the resize */);
744 postRefresh_App(); 825 postRefresh_App();
745 return iTrue; 826 return iTrue;
746 case SDL_WINDOWEVENT_RESTORED: 827 case SDL_WINDOWEVENT_RESTORED:
747 case SDL_WINDOWEVENT_SHOWN: 828 case SDL_WINDOWEVENT_SHOWN:
748 updateSize_Window_(d, iTrue); 829 updateSize_MainWindow_(d, iTrue);
749 invalidate_Window_(d, iTrue); 830 invalidate_MainWindow_(d, iTrue);
750 d->isMinimized = iFalse; 831 d->base.isMinimized = iFalse;
751 postRefresh_App(); 832 postRefresh_App();
752 return iTrue; 833 return iTrue;
753 case SDL_WINDOWEVENT_MINIMIZED: 834 case SDL_WINDOWEVENT_MINIMIZED:
754 d->isMinimized = iTrue; 835 d->base.isMinimized = iTrue;
836 return iTrue;
837#else /* if defined (!iPlatformDesktop) */
838 case SDL_WINDOWEVENT_RESIZED:
839 /* On mobile, this occurs when the display is rotated. */
840 invalidate_Window(d);
841 postRefresh_App();
755 return iTrue; 842 return iTrue;
756#endif /* defined (iPlatformDesktop) */ 843#endif
757 case SDL_WINDOWEVENT_LEAVE: 844 case SDL_WINDOWEVENT_LEAVE:
758 unhover_Widget(); 845 unhover_Widget();
759 d->isMouseInside = iFalse; 846 d->base.isMouseInside = iFalse;
760 postCommand_App("window.mouse.exited"); 847 postCommand_App("window.mouse.exited");
761 return iTrue; 848 return iTrue;
762 case SDL_WINDOWEVENT_ENTER: 849 case SDL_WINDOWEVENT_ENTER:
763 d->isMouseInside = iTrue; 850 d->base.isMouseInside = iTrue;
851 SDL_SetWindowInputFocus(d->base.win);
764 postCommand_App("window.mouse.entered"); 852 postCommand_App("window.mouse.entered");
765 return iTrue; 853 return iTrue;
766#if defined (iPlatformMobile)
767 case SDL_WINDOWEVENT_RESIZED:
768 /* On mobile, this occurs when the display is rotated. */
769 invalidate_Window(d);
770 postRefresh_App();
771 return iTrue;
772#endif
773 case SDL_WINDOWEVENT_FOCUS_GAINED: 854 case SDL_WINDOWEVENT_FOCUS_GAINED:
774 d->focusGainedAt = SDL_GetTicks(); 855 d->base.focusGainedAt = SDL_GetTicks();
775 setCapsLockDown_Keys(iFalse); 856 setCapsLockDown_Keys(iFalse);
776 postCommand_App("window.focus.gained"); 857 postCommand_App("window.focus.gained");
777 d->isExposed = iTrue; 858 d->base.isExposed = iTrue;
778#if defined (iPlatformMobile) 859#if !defined (iPlatformDesktop)
779 /* Returned to foreground, may have lost buffered content. */ 860 /* Returned to foreground, may have lost buffered content. */
780 invalidate_Window_(d, iTrue); 861 invalidate_MainWindow_(d, iTrue);
781 postCommand_App("window.unfreeze"); 862 postCommand_App("window.unfreeze");
782#endif 863#endif
783 return iFalse; 864 return iFalse;
784 case SDL_WINDOWEVENT_FOCUS_LOST: 865 case SDL_WINDOWEVENT_FOCUS_LOST:
785 postCommand_App("window.focus.lost"); 866 postCommand_App("window.focus.lost");
786#if defined (iPlatformMobile) 867#if !defined (iPlatformDesktop)
787 setFreezeDraw_Window(d, iTrue); 868 setFreezeDraw_MainWindow(d, iTrue);
788#endif 869#endif
789 return iFalse; 870 return iFalse;
790 case SDL_WINDOWEVENT_TAKE_FOCUS: 871 case SDL_WINDOWEVENT_TAKE_FOCUS:
791 SDL_SetWindowInputFocus(d->win); 872 SDL_SetWindowInputFocus(d->base.win);
792 postRefresh_App(); 873 postRefresh_App();
793 return iTrue; 874 return iTrue;
794 default: 875 default:
@@ -805,6 +886,7 @@ static void applyCursor_Window_(iWindow *d) {
805} 886}
806 887
807iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { 888iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
889 iMainWindow *mw = (type_Window(d) == main_WindowType ? as_MainWindow(d) : NULL);
808 switch (ev->type) { 890 switch (ev->type) {
809#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 891#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
810 case SDL_SYSWMEVENT: { 892 case SDL_SYSWMEVENT: {
@@ -817,21 +899,28 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
817 } 899 }
818#endif 900#endif
819 case SDL_WINDOWEVENT: { 901 case SDL_WINDOWEVENT: {
820 return handleWindowEvent_Window_(d, &ev->window); 902 if (mw) {
903 return handleWindowEvent_MainWindow_(mw, &ev->window);
904 }
905 else {
906 return handleWindowEvent_Window_(d, &ev->window);
907 }
821 } 908 }
822 case SDL_RENDER_TARGETS_RESET: 909 case SDL_RENDER_TARGETS_RESET:
823 case SDL_RENDER_DEVICE_RESET: { 910 case SDL_RENDER_DEVICE_RESET: {
824 invalidate_Window_(d, iTrue /* force full reset */); 911 if (mw) {
912 invalidate_MainWindow_(mw, iTrue /* force full reset */);
913 }
825 break; 914 break;
826 } 915 }
827 default: { 916 default: {
828 SDL_Event event = *ev; 917 SDL_Event event = *ev;
829 if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.unfreeze")) { 918 if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.unfreeze") && mw) {
830 d->isDrawFrozen = iFalse; 919 mw->isDrawFrozen = iFalse;
831 if (SDL_GetWindowFlags(d->win) & SDL_WINDOW_HIDDEN) { 920 if (SDL_GetWindowFlags(d->win) & SDL_WINDOW_HIDDEN) {
832 SDL_ShowWindow(d->win); 921 SDL_ShowWindow(d->win);
833 } 922 }
834 postRefresh_App(); 923 draw_MainWindow(mw); /* don't show a frame of placeholder content */
835 postCommand_App("media.player.update"); /* in case a player needs updating */ 924 postCommand_App("media.player.update"); /* in case a player needs updating */
836 return iTrue; 925 return iTrue;
837 } 926 }
@@ -873,7 +962,7 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
873 } 962 }
874 } 963 }
875 } 964 }
876 const iWidget *oldHover = d->hover; 965// const iWidget *oldHover = d->hover;
877 iBool wasUsed = iFalse; 966 iBool wasUsed = iFalse;
878 /* Dispatch first to the mouse-grabbed widget. */ 967 /* Dispatch first to the mouse-grabbed widget. */
879// iWidget *widget = d->root.widget; 968// iWidget *widget = d->root.widget;
@@ -913,7 +1002,7 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
913 updateMetrics_Root(d->roots[i]); 1002 updateMetrics_Root(d->roots[i]);
914 } 1003 }
915 } 1004 }
916 if (isCommand_UserEvent(&event, "lang.changed")) { 1005 if (isCommand_UserEvent(&event, "lang.changed") && mw) {
917#if defined (iHaveNativeMenus) 1006#if defined (iHaveNativeMenus)
918 /* Retranslate the menus. */ 1007 /* Retranslate the menus. */
919 removeMacMenus_(); 1008 removeMacMenus_();
@@ -927,9 +1016,6 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
927 } 1016 }
928 } 1017 }
929 } 1018 }
930 if (oldHover != d->hover) {
931 postRefresh_App();
932 }
933 if (event.type == SDL_MOUSEMOTION) { 1019 if (event.type == SDL_MOUSEMOTION) {
934 applyCursor_Window_(d); 1020 applyCursor_Window_(d);
935 } 1021 }
@@ -949,6 +1035,10 @@ iBool setKeyRoot_Window(iWindow *d, iRoot *root) {
949 return iFalse; 1035 return iFalse;
950} 1036}
951 1037
1038iLocalDef iBool isEscapeKeypress_(const SDL_Event *ev) {
1039 return (ev->type == SDL_KEYDOWN || ev->type == SDL_KEYUP) && ev->key.keysym.sym == SDLK_ESCAPE;
1040}
1041
952iBool dispatchEvent_Window(iWindow *d, const SDL_Event *ev) { 1042iBool dispatchEvent_Window(iWindow *d, const SDL_Event *ev) {
953 if (ev->type == SDL_MOUSEMOTION) { 1043 if (ev->type == SDL_MOUSEMOTION) {
954 /* Hover widget may change. */ 1044 /* Hover widget may change. */
@@ -964,12 +1054,19 @@ iBool dispatchEvent_Window(iWindow *d, const SDL_Event *ev) {
964 } 1054 }
965 if ((ev->type == SDL_KEYDOWN || ev->type == SDL_KEYUP || ev->type == SDL_TEXTINPUT) 1055 if ((ev->type == SDL_KEYDOWN || ev->type == SDL_KEYUP || ev->type == SDL_TEXTINPUT)
966 && d->keyRoot != root) { 1056 && d->keyRoot != root) {
967 continue; /* Key events go only to the root with keyboard focus. */ 1057 if (!isEscapeKeypress_(ev)) {
1058 /* Key events go only to the root with keyboard focus, with the exception
1059 of Escape that will also affect the entire window. */
1060 continue;
1061 }
968 } 1062 }
969 if (ev->type == SDL_MOUSEWHEEL && !contains_Rect(rect_Root(root), 1063 if (ev->type == SDL_MOUSEWHEEL && !contains_Rect(rect_Root(root),
970 coord_MouseWheelEvent(&ev->wheel))) { 1064 coord_MouseWheelEvent(&ev->wheel))) {
971 continue; /* Only process the event in the relevant split. */ 1065 continue; /* Only process the event in the relevant split. */
972 } 1066 }
1067 if (!root->widget) {
1068 continue;
1069 }
973 setCurrent_Root(root); 1070 setCurrent_Root(root);
974 const iBool wasUsed = dispatchEvent_Widget(root->widget, ev); 1071 const iBool wasUsed = dispatchEvent_Widget(root->widget, ev);
975 if (wasUsed) { 1072 if (wasUsed) {
@@ -1012,32 +1109,60 @@ iBool postContextClick_Window(iWindow *d, const SDL_MouseButtonEvent *ev) {
1012} 1109}
1013 1110
1014void draw_Window(iWindow *d) { 1111void draw_Window(iWindow *d) {
1112 if (SDL_GetWindowFlags(d->win) & SDL_WINDOW_HIDDEN) {
1113 return;
1114 }
1115 iPaint p;
1116 init_Paint(&p);
1117 iRoot *root = d->roots[0];
1118 setCurrent_Root(root);
1119 unsetClip_Paint(&p); /* update clip to full window */
1120 const iColor back = get_Color(uiBackground_ColorId);
1121 SDL_SetRenderDrawColor(d->render, back.r, back.g, back.b, 255);
1122 SDL_RenderClear(d->render);
1123 d->frameTime = SDL_GetTicks();
1124 if (isExposed_Window(d)) {
1125 d->isInvalidated = iFalse;
1126 extern int drawCount_;
1127 drawRoot_Widget(root->widget);
1128#if !defined (NDEBUG)
1129 draw_Text(defaultBold_FontId, safeRect_Root(root).pos, red_ColorId, "%d", drawCount_);
1130 drawCount_ = 0;
1131#endif
1132 }
1133// drawRectThickness_Paint(&p, (iRect){ zero_I2(), sub_I2(d->size, one_I2()) }, gap_UI / 4, uiSeparator_ColorId);
1134 setCurrent_Root(NULL);
1135 SDL_RenderPresent(d->render);
1136}
1137
1138void draw_MainWindow(iMainWindow *d) {
1139 /* TODO: Try to make this a specialization of `draw_Window`? */
1140 iWindow *w = as_Window(d);
1015 if (d->isDrawFrozen) { 1141 if (d->isDrawFrozen) {
1016 return; 1142 return;
1017 } 1143 }
1018//#if defined (iPlatformMobile) 1144 setCurrent_Text(d->base.text);
1019 /* Check if root needs resizing. */ { 1145 /* Check if root needs resizing. */ {
1020 iInt2 renderSize; 1146 iInt2 renderSize;
1021 SDL_GetRendererOutputSize(d->render, &renderSize.x, &renderSize.y); 1147 SDL_GetRendererOutputSize(w->render, &renderSize.x, &renderSize.y);
1022 if (!isEqual_I2(renderSize, d->size)) { 1148 if (!isEqual_I2(renderSize, w->size)) {
1023 updateSize_Window_(d, iTrue); 1149 updateSize_MainWindow_(d, iTrue);
1024 processEvents_App(postedEventsOnly_AppEventMode); 1150 processEvents_App(postedEventsOnly_AppEventMode);
1025 } 1151 }
1026 } 1152 }
1027//#endif 1153 const int winFlags = SDL_GetWindowFlags(d->base.win);
1028 const int winFlags = SDL_GetWindowFlags(d->win);
1029 const iBool gotFocus = (winFlags & SDL_WINDOW_INPUT_FOCUS) != 0; 1154 const iBool gotFocus = (winFlags & SDL_WINDOW_INPUT_FOCUS) != 0;
1030 iPaint p; 1155 iPaint p;
1031 init_Paint(&p); 1156 init_Paint(&p);
1032 /* Clear the window. The clear color is visible as a border around the window 1157 /* Clear the window. The clear color is visible as a border around the window
1033 when the custom frame is being used. */ { 1158 when the custom frame is being used. */ {
1034 setCurrent_Root(d->roots[0]); 1159 setCurrent_Root(w->roots[0]);
1035#if defined (iPlatformAppleMobile) 1160#if defined (iPlatformMobile)
1036 iColor back = get_Color(uiBackground_ColorId); 1161 iColor back = get_Color(uiBackground_ColorId);
1037 if (deviceType_App() == phone_AppDeviceType) { 1162 if (deviceType_App() == phone_AppDeviceType) {
1038 /* Page background extends to safe area, so fill it completely. */ 1163 /* Page background extends to safe area, so fill it completely. */
1039 back = get_Color(tmBackground_ColorId); 1164 back = get_Color(tmBackground_ColorId);
1040 } 1165 }
1041#else 1166#else
1042 const iColor back = get_Color(gotFocus && d->place.snap != maximized_WindowSnap && 1167 const iColor back = get_Color(gotFocus && d->place.snap != maximized_WindowSnap &&
1043 ~winFlags & SDL_WINDOW_FULLSCREEN_DESKTOP 1168 ~winFlags & SDL_WINDOW_FULLSCREEN_DESKTOP
@@ -1045,19 +1170,20 @@ void draw_Window(iWindow *d) {
1045 : uiSeparator_ColorId); 1170 : uiSeparator_ColorId);
1046#endif 1171#endif
1047 unsetClip_Paint(&p); /* update clip to full window */ 1172 unsetClip_Paint(&p); /* update clip to full window */
1048 SDL_SetRenderDrawColor(d->render, back.r, back.g, back.b, 255); 1173 SDL_SetRenderDrawColor(w->render, back.r, back.g, back.b, 255);
1049 SDL_RenderClear(d->render); 1174 SDL_RenderClear(w->render);
1050 } 1175 }
1051 /* Draw widgets. */ 1176 /* Draw widgets. */
1052 d->frameTime = SDL_GetTicks(); 1177 w->frameTime = SDL_GetTicks();
1053 if (isExposed_Window(d)) { 1178 if (isExposed_Window(w)) {
1054 d->isInvalidated = iFalse; 1179 w->isInvalidated = iFalse;
1055 iForIndices(i, d->roots) { 1180 extern int drawCount_;
1056 iRoot *root = d->roots[i]; 1181 iForIndices(i, w->roots) {
1182 iRoot *root = w->roots[i];
1057 if (root) { 1183 if (root) {
1058 setCurrent_Root(root); 1184 setCurrent_Root(root);
1059 unsetClip_Paint(&p); /* update clip to current root */ 1185 unsetClip_Paint(&p); /* update clip to current root */
1060 draw_Widget(root->widget); 1186 drawRoot_Widget(root->widget);
1061#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 1187#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
1062 /* App icon. */ 1188 /* App icon. */
1063 const iWidget *appIcon = findChild_Widget(root->widget, "winbar.icon"); 1189 const iWidget *appIcon = findChild_Widget(root->widget, "winbar.icon");
@@ -1070,14 +1196,14 @@ void draw_Window(iWindow *d) {
1070 SDL_SetTextureColorMod(d->appIcon, iconColor.r, iconColor.g, iconColor.b); 1196 SDL_SetTextureColorMod(d->appIcon, iconColor.r, iconColor.g, iconColor.b);
1071 SDL_SetTextureAlphaMod(d->appIcon, gotFocus || !isLight ? 255 : 92); 1197 SDL_SetTextureAlphaMod(d->appIcon, gotFocus || !isLight ? 255 : 92);
1072 SDL_RenderCopy( 1198 SDL_RenderCopy(
1073 d->render, 1199 w->render,
1074 d->appIcon, 1200 d->appIcon,
1075 NULL, 1201 NULL,
1076 &(SDL_Rect){ left_Rect(rect) + gap_UI * 1.25f, mid.y - size / 2, size, size }); 1202 &(SDL_Rect){ left_Rect(rect) + gap_UI * 1.25f, mid.y - size / 2, size, size });
1077 } 1203 }
1078#endif 1204#endif
1079 /* Root separator and keyboard focus indicator. */ 1205 /* Root separator and keyboard focus indicator. */
1080 if (numRoots_Window(d) > 1){ 1206 if (numRoots_Window(w) > 1){
1081 const iRect bounds = bounds_Widget(root->widget); 1207 const iRect bounds = bounds_Widget(root->widget);
1082 if (i == 1) { 1208 if (i == 1) {
1083 fillRect_Paint(&p, (iRect){ 1209 fillRect_Paint(&p, (iRect){
@@ -1085,7 +1211,7 @@ void draw_Window(iWindow *d) {
1085 init_I2(gap_UI / 4, height_Rect(bounds)) 1211 init_I2(gap_UI / 4, height_Rect(bounds))
1086 }, uiSeparator_ColorId); 1212 }, uiSeparator_ColorId);
1087 } 1213 }
1088 if (root == d->keyRoot) { 1214 if (root == w->keyRoot) {
1089 const iBool isDark = isDark_ColorTheme(colorTheme_App()); 1215 const iBool isDark = isDark_ColorTheme(colorTheme_App());
1090 fillRect_Paint(&p, (iRect){ 1216 fillRect_Paint(&p, (iRect){
1091 topLeft_Rect(bounds), 1217 topLeft_Rect(bounds),
@@ -1097,6 +1223,10 @@ void draw_Window(iWindow *d) {
1097 } 1223 }
1098 } 1224 }
1099 setCurrent_Root(NULL); 1225 setCurrent_Root(NULL);
1226#if !defined (NDEBUG)
1227 draw_Text(defaultBold_FontId, safeRect_Root(w->roots[0]).pos, red_ColorId, "%d", drawCount_);
1228 drawCount_ = 0;
1229#endif
1100 } 1230 }
1101#if 0 1231#if 0
1102 /* Text cache debugging. */ { 1232 /* Text cache debugging. */ {
@@ -1106,21 +1236,21 @@ void draw_Window(iWindow *d) {
1106 SDL_RenderCopy(d->render, glyphCache_Text(), NULL, &rect); 1236 SDL_RenderCopy(d->render, glyphCache_Text(), NULL, &rect);
1107 } 1237 }
1108#endif 1238#endif
1109 SDL_RenderPresent(d->render); 1239 SDL_RenderPresent(w->render);
1110} 1240}
1111 1241
1112void resize_Window(iWindow *d, int w, int h) { 1242void resize_MainWindow(iMainWindow *d, int w, int h) {
1113 if (w > 0 && h > 0) { 1243 if (w > 0 && h > 0) {
1114 SDL_SetWindowSize(d->win, w, h); 1244 SDL_SetWindowSize(d->base.win, w, h);
1115 updateSize_Window_(d, iFalse); 1245 updateSize_MainWindow_(d, iFalse);
1116 } 1246 }
1117 else { 1247 else {
1118 updateSize_Window_(d, iTrue); /* notify always */ 1248 updateSize_MainWindow_(d, iTrue); /* notify always */
1119 } 1249 }
1120} 1250}
1121 1251
1122void setTitle_Window(iWindow *d, const iString *title) { 1252void setTitle_MainWindow(iMainWindow *d, const iString *title) {
1123 SDL_SetWindowTitle(d->win, cstr_String(title)); 1253 SDL_SetWindowTitle(d->base.win, cstr_String(title));
1124 iLabelWidget *bar = findChild_Widget(get_Root()->widget, "winbar.title"); 1254 iLabelWidget *bar = findChild_Widget(get_Root()->widget, "winbar.title");
1125 if (bar) { 1255 if (bar) {
1126 updateText_LabelWidget(bar, title); 1256 updateText_LabelWidget(bar, title);
@@ -1128,6 +1258,9 @@ void setTitle_Window(iWindow *d, const iString *title) {
1128} 1258}
1129 1259
1130void setUiScale_Window(iWindow *d, float uiScale) { 1260void setUiScale_Window(iWindow *d, float uiScale) {
1261 if (uiScale <= 0.0f) {
1262 uiScale = 1.0f;
1263 }
1131 uiScale = iClamp(uiScale, 0.5f, 4.0f); 1264 uiScale = iClamp(uiScale, 0.5f, 4.0f);
1132 if (d) { 1265 if (d) {
1133 if (iAbs(d->uiScale - uiScale) > 0.0001f) { 1266 if (iAbs(d->uiScale - uiScale) > 0.0001f) {
@@ -1140,7 +1273,7 @@ void setUiScale_Window(iWindow *d, float uiScale) {
1140 } 1273 }
1141} 1274}
1142 1275
1143void setFreezeDraw_Window(iWindow *d, iBool freezeDraw) { 1276void setFreezeDraw_MainWindow(iMainWindow *d, iBool freezeDraw) {
1144 d->isDrawFrozen = freezeDraw; 1277 d->isDrawFrozen = freezeDraw;
1145} 1278}
1146 1279
@@ -1191,63 +1324,80 @@ iWindow *get_Window(void) {
1191 return theWindow_; 1324 return theWindow_;
1192} 1325}
1193 1326
1327void setCurrent_Window(iAnyWindow *d) {
1328 theWindow_ = d;
1329 if (type_Window(d) == main_WindowType) {
1330 theMainWindow_ = d;
1331 }
1332 if (d) {
1333 setCurrent_Text(theWindow_->text);
1334 setCurrent_Root(theWindow_->keyRoot);
1335 }
1336 else {
1337 setCurrent_Text(NULL);
1338 setCurrent_Root(NULL);
1339 }
1340}
1341
1342iMainWindow *get_MainWindow(void) {
1343 return theMainWindow_;
1344}
1345
1194iBool isOpenGLRenderer_Window(void) { 1346iBool isOpenGLRenderer_Window(void) {
1195 return isOpenGLRenderer_; 1347 return isOpenGLRenderer_;
1196} 1348}
1197 1349
1198void setKeyboardHeight_Window(iWindow *d, int height) { 1350void setKeyboardHeight_MainWindow(iMainWindow *d, int height) {
1199 if (d->keyboardHeight != height) { 1351 if (d->keyboardHeight != height) {
1200 d->keyboardHeight = height; 1352 d->keyboardHeight = height;
1201 if (height == 0) {
1202 setFlags_Anim(&d->rootOffset, easeBoth_AnimFlag, iTrue);
1203 setValue_Anim(&d->rootOffset, 0, 250);
1204 }
1205 postCommandf_App("keyboard.changed arg:%d", height); 1353 postCommandf_App("keyboard.changed arg:%d", height);
1206 postRefresh_App(); 1354 postRefresh_App();
1207 } 1355 }
1208} 1356}
1209 1357
1210void checkPendingSplit_Window(iWindow *d) { 1358void checkPendingSplit_MainWindow(iMainWindow *d) {
1211 if (d->splitMode != d->pendingSplitMode) { 1359 if (d->splitMode != d->pendingSplitMode) {
1212 setSplitMode_Window(d, d->pendingSplitMode); 1360 setSplitMode_MainWindow(d, d->pendingSplitMode);
1213 } 1361 }
1214} 1362}
1215 1363
1216void swapRoots_Window(iWindow *d) { 1364void swapRoots_MainWindow(iMainWindow *d) {
1217 if (numRoots_Window(d) == 2) { 1365 iWindow *w = as_Window(d);
1218 iSwap(iRoot *, d->roots[0], d->roots[1]); 1366 if (numRoots_Window(w) == 2) {
1219 updateSize_Window_(d, iTrue); 1367 iSwap(iRoot *, w->roots[0], w->roots[1]);
1368 updateSize_MainWindow_(d, iTrue);
1220 } 1369 }
1221} 1370}
1222 1371
1223void setSplitMode_Window(iWindow *d, int splitFlags) { 1372void setSplitMode_MainWindow(iMainWindow *d, int splitFlags) {
1224 const int splitMode = splitFlags & mode_WindowSplit; 1373 const int splitMode = splitFlags & mode_WindowSplit;
1225 if (deviceType_App() == phone_AppDeviceType) { 1374 if (deviceType_App() == phone_AppDeviceType) {
1226 /* There isn't enough room on the phone. */ 1375 /* There isn't enough room on the phone. */
1227 /* TODO: Maybe in landscape only? */ 1376 /* TODO: Maybe in landscape only? */
1228 return; 1377 return;
1229 } 1378 }
1379 iWindow *w = as_Window(d);
1230 iAssert(current_Root() == NULL); 1380 iAssert(current_Root() == NULL);
1231 if (d->splitMode != splitMode) { 1381 if (d->splitMode != splitMode) {
1232 int oldCount = numRoots_Window(d); 1382 int oldCount = numRoots_Window(w);
1233 setFreezeDraw_Window(d, iTrue); 1383 setFreezeDraw_MainWindow(d, iTrue);
1234 if (oldCount == 2 && splitMode == 0) { 1384 if (oldCount == 2 && splitMode == 0) {
1235 /* Keep references to the tabs of the second root. */ 1385 /* Keep references to the tabs of the second root. */
1236 const iDocumentWidget *curPage = document_Root(d->keyRoot); 1386 const iDocumentWidget *curPage = document_Root(w->keyRoot);
1237 if (!curPage) { 1387 if (!curPage) {
1238 /* All tabs closed on that side. */ 1388 /* All tabs closed on that side. */
1239 curPage = document_Root(otherRoot_Window(d, d->keyRoot)); 1389 curPage = document_Root(otherRoot_Window(w, w->keyRoot));
1240 } 1390 }
1241 iObjectList *tabs = listDocuments_App(d->roots[1]); 1391 iObjectList *tabs = listDocuments_App(w->roots[1]);
1242 iForEach(ObjectList, i, tabs) { 1392 iForEach(ObjectList, i, tabs) {
1243 setRoot_Widget(i.object, d->roots[0]); 1393 setRoot_Widget(i.object, w->roots[0]);
1244 } 1394 }
1245 setFocus_Widget(NULL); 1395 setFocus_Widget(NULL);
1246 delete_Root(d->roots[1]); 1396 delete_Root(w->roots[1]);
1247 d->roots[1] = NULL; 1397 w->roots[1] = NULL;
1248 d->keyRoot = d->roots[0]; 1398 w->keyRoot = w->roots[0];
1249 /* Move the deleted root's tabs to the first root. */ 1399 /* Move the deleted root's tabs to the first root. */
1250 setCurrent_Root(d->roots[0]); 1400 setCurrent_Root(w->roots[0]);
1251 iWidget *docTabs = findWidget_Root("doctabs"); 1401 iWidget *docTabs = findWidget_Root("doctabs");
1252 iForEach(ObjectList, j, tabs) { 1402 iForEach(ObjectList, j, tabs) {
1253 appendTabPage_Widget(docTabs, j.object, "", 0, 0); 1403 appendTabPage_Widget(docTabs, j.object, "", 0, 0);
@@ -1259,38 +1409,48 @@ void setSplitMode_Window(iWindow *d, int splitFlags) {
1259 } 1409 }
1260 else if (splitMode && oldCount == 1) { 1410 else if (splitMode && oldCount == 1) {
1261 /* Add a second root. */ 1411 /* Add a second root. */
1262 iDocumentWidget *moved = document_Root(d->roots[0]); 1412 iDocumentWidget *moved = document_Root(w->roots[0]);
1263 iAssert(d->roots[1] == NULL); 1413 iAssert(w->roots[1] == NULL);
1264 const iBool addToLeft = (prefs_App()->pinSplit == 2); 1414 const iBool addToLeft = (prefs_App()->pinSplit == 2);
1265 size_t newRootIndex = 1; 1415 size_t newRootIndex = 1;
1266 if (addToLeft) { 1416 if (addToLeft) {
1267 iSwap(iRoot *, d->roots[0], d->roots[1]); 1417 iSwap(iRoot *, w->roots[0], w->roots[1]);
1268 newRootIndex = 0; 1418 newRootIndex = 0;
1269 } 1419 }
1270 d->roots[newRootIndex] = new_Root(); 1420 w->roots[newRootIndex] = new_Root();
1271 d->keyRoot = d->roots[newRootIndex]; 1421 w->keyRoot = w->roots[newRootIndex];
1272 setCurrent_Root(d->roots[newRootIndex]); 1422 w->keyRoot->window = w;
1273 createUserInterface_Root(d->roots[newRootIndex]); 1423 setCurrent_Root(w->roots[newRootIndex]);
1424 createUserInterface_Root(w->roots[newRootIndex]);
1425 /* Bookmark folder state will match the old root's state. */ {
1426 for (int sb = 0; sb < 2; sb++) {
1427 const char *sbId = (sb == 0 ? "sidebar" : "sidebar2");
1428 setClosedFolders_SidebarWidget(
1429 findChild_Widget(w->roots[newRootIndex]->widget, sbId),
1430 closedFolders_SidebarWidget(
1431 findChild_Widget(w->roots[newRootIndex ^ 1]->widget, sbId)));
1432 }
1433 }
1274 if (!isEmpty_String(d->pendingSplitUrl)) { 1434 if (!isEmpty_String(d->pendingSplitUrl)) {
1275 postCommandf_Root(d->roots[newRootIndex], "open url:%s", 1435 postCommandf_Root(w->roots[newRootIndex], "open url:%s",
1276 cstr_String(d->pendingSplitUrl)); 1436 cstr_String(d->pendingSplitUrl));
1277 clear_String(d->pendingSplitUrl); 1437 clear_String(d->pendingSplitUrl);
1278 } 1438 }
1279 else if (~splitFlags & noEvents_WindowSplit) { 1439 else if (~splitFlags & noEvents_WindowSplit) {
1280 iWidget *docTabs0 = findChild_Widget(d->roots[newRootIndex ^ 1]->widget, "doctabs"); 1440 iWidget *docTabs0 = findChild_Widget(w->roots[newRootIndex ^ 1]->widget, "doctabs");
1281 iWidget *docTabs1 = findChild_Widget(d->roots[newRootIndex]->widget, "doctabs"); 1441 iWidget *docTabs1 = findChild_Widget(w->roots[newRootIndex]->widget, "doctabs");
1282 /* If the old root has multiple tabs, move the current one to the new split. */ 1442 /* If the old root has multiple tabs, move the current one to the new split. */
1283 if (tabCount_Widget(docTabs0) >= 2) { 1443 if (tabCount_Widget(docTabs0) >= 2) {
1284 int movedIndex = tabPageIndex_Widget(docTabs0, moved); 1444 int movedIndex = tabPageIndex_Widget(docTabs0, moved);
1285 removeTabPage_Widget(docTabs0, movedIndex); 1445 removeTabPage_Widget(docTabs0, movedIndex);
1286 showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax(movedIndex - 1, 0))); 1446 showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax(movedIndex - 1, 0)));
1287 iRelease(removeTabPage_Widget(docTabs1, 0)); /* delete the default tab */ 1447 iRelease(removeTabPage_Widget(docTabs1, 0)); /* delete the default tab */
1288 setRoot_Widget(as_Widget(moved), d->roots[newRootIndex]); 1448 setRoot_Widget(as_Widget(moved), w->roots[newRootIndex]);
1289 prependTabPage_Widget(docTabs1, iClob(moved), "", 0, 0); 1449 prependTabPage_Widget(docTabs1, iClob(moved), "", 0, 0);
1290 postCommandf_App("tabs.switch page:%p", moved); 1450 postCommandf_App("tabs.switch page:%p", moved);
1291 } 1451 }
1292 else { 1452 else {
1293 postCommand_Root(d->roots[newRootIndex], "navigate.home"); 1453 postCommand_Root(w->roots[newRootIndex], "navigate.home");
1294 } 1454 }
1295 } 1455 }
1296 setCurrent_Root(NULL); 1456 setCurrent_Root(NULL);
@@ -1319,26 +1479,26 @@ void setSplitMode_Window(iWindow *d, int splitFlags) {
1319 } 1479 }
1320#endif 1480#endif
1321 if (~splitFlags & noEvents_WindowSplit) { 1481 if (~splitFlags & noEvents_WindowSplit) {
1322 updateSize_Window_(d, iTrue); 1482 updateSize_MainWindow_(d, iTrue);
1323 postCommand_App("window.unfreeze"); 1483 postCommand_App("window.unfreeze");
1324 } 1484 }
1325 } 1485 }
1326} 1486}
1327 1487
1328void setSnap_Window(iWindow *d, int snapMode) { 1488void setSnap_MainWindow(iMainWindow *d, int snapMode) {
1329 if (!prefs_App()->customFrame) { 1489 if (!prefs_App()->customFrame) {
1330 if (snapMode == maximized_WindowSnap) { 1490 if (snapMode == maximized_WindowSnap) {
1331 SDL_MaximizeWindow(d->win); 1491 SDL_MaximizeWindow(d->base.win);
1332 } 1492 }
1333 else if (snapMode == fullscreen_WindowSnap) { 1493 else if (snapMode == fullscreen_WindowSnap) {
1334 SDL_SetWindowFullscreen(d->win, SDL_WINDOW_FULLSCREEN_DESKTOP); 1494 SDL_SetWindowFullscreen(d->base.win, SDL_WINDOW_FULLSCREEN_DESKTOP);
1335 } 1495 }
1336 else { 1496 else {
1337 if (snap_Window(d) == fullscreen_WindowSnap) { 1497 if (snap_MainWindow(d) == fullscreen_WindowSnap) {
1338 SDL_SetWindowFullscreen(d->win, 0); 1498 SDL_SetWindowFullscreen(d->base.win, 0);
1339 } 1499 }
1340 else { 1500 else {
1341 SDL_RestoreWindow(d->win); 1501 SDL_RestoreWindow(d->base.win);
1342 } 1502 }
1343 } 1503 }
1344 return; 1504 return;
@@ -1350,9 +1510,9 @@ void setSnap_Window(iWindow *d, int snapMode) {
1350 const int snapDist = gap_UI * 4; 1510 const int snapDist = gap_UI * 4;
1351 iRect newRect = zero_Rect(); 1511 iRect newRect = zero_Rect();
1352 SDL_Rect usable; 1512 SDL_Rect usable;
1353 SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(d->win), &usable); 1513 SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(d->base.win), &usable);
1354 if (d->place.snap == fullscreen_WindowSnap) { 1514 if (d->place.snap == fullscreen_WindowSnap) {
1355 SDL_SetWindowFullscreen(d->win, 0); 1515 SDL_SetWindowFullscreen(d->base.win, 0);
1356 } 1516 }
1357 d->place.snap = snapMode & ~redo_WindowSnap; 1517 d->place.snap = snapMode & ~redo_WindowSnap;
1358 switch (snapMode & mask_WindowSnap) { 1518 switch (snapMode & mask_WindowSnap) {
@@ -1373,8 +1533,8 @@ void setSnap_Window(iWindow *d, int snapMode) {
1373 case yMaximized_WindowSnap: 1533 case yMaximized_WindowSnap:
1374 newRect.pos.y = 0; 1534 newRect.pos.y = 0;
1375 newRect.size.y = usable.h; 1535 newRect.size.y = usable.h;
1376 SDL_GetWindowSize(d->win, &newRect.size.x, NULL); 1536 SDL_GetWindowSize(d->base.win, &newRect.size.x, NULL);
1377 SDL_GetWindowPosition(d->win, &newRect.pos.x, NULL); 1537 SDL_GetWindowPosition(d->base.win, &newRect.pos.x, NULL);
1378 /* Snap the window to left/right edges, if close by. */ 1538 /* Snap the window to left/right edges, if close by. */
1379 if (iAbs(right_Rect(newRect) - (usable.x + usable.w)) < snapDist) { 1539 if (iAbs(right_Rect(newRect) - (usable.x + usable.w)) < snapDist) {
1380 newRect.pos.x = usable.x + usable.w - width_Rect(newRect); 1540 newRect.pos.x = usable.x + usable.w - width_Rect(newRect);
@@ -1384,7 +1544,7 @@ void setSnap_Window(iWindow *d, int snapMode) {
1384 } 1544 }
1385 break; 1545 break;
1386 case fullscreen_WindowSnap: 1546 case fullscreen_WindowSnap:
1387 SDL_SetWindowFullscreen(d->win, SDL_WINDOW_FULLSCREEN_DESKTOP); 1547 SDL_SetWindowFullscreen(d->base.win, SDL_WINDOW_FULLSCREEN_DESKTOP);
1388 break; 1548 break;
1389 } 1549 }
1390 if (snapMode & (topBit_WindowSnap | bottomBit_WindowSnap)) { 1550 if (snapMode & (topBit_WindowSnap | bottomBit_WindowSnap)) {
@@ -1416,9 +1576,9 @@ void setSnap_Window(iWindow *d, int snapMode) {
1416#endif /* defined (LAGRANGE_ENABLE_CUSTOM_FRAME) */ 1576#endif /* defined (LAGRANGE_ENABLE_CUSTOM_FRAME) */
1417} 1577}
1418 1578
1419int snap_Window(const iWindow *d) { 1579int snap_MainWindow(const iMainWindow *d) {
1420 if (!prefs_App()->customFrame) { 1580 if (!prefs_App()->customFrame) {
1421 const int flags = SDL_GetWindowFlags(d->win); 1581 const int flags = SDL_GetWindowFlags(d->base.win);
1422 if (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) { 1582 if (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) {
1423 return fullscreen_WindowSnap; 1583 return fullscreen_WindowSnap;
1424 } 1584 }
@@ -1429,3 +1589,27 @@ int snap_Window(const iWindow *d) {
1429 } 1589 }
1430 return d->place.snap; 1590 return d->place.snap;
1431} 1591}
1592
1593/*----------------------------------------------------------------------------------------------*/
1594
1595iWindow *newPopup_Window(iInt2 screenPos, iWidget *rootWidget) {
1596 iWindow *win =
1597 new_Window(popup_WindowType,
1598 (iRect){ screenPos, divf_I2(rootWidget->rect.size, get_Window()->pixelRatio) },
1599 SDL_WINDOW_ALWAYS_ON_TOP |
1600#if !defined (iPlatformAppleDesktop)
1601 SDL_WINDOW_BORDERLESS |
1602#endif
1603 SDL_WINDOW_POPUP_MENU |
1604 SDL_WINDOW_SKIP_TASKBAR);
1605#if defined (iPlatformAppleDesktop)
1606 hideTitleBar_MacOS(win); /* make it a borderless window */
1607#endif
1608 iRoot *root = new_Root();
1609 win->roots[0] = root;
1610 win->keyRoot = root;
1611 root->widget = rootWidget;
1612 root->window = win;
1613 setRoot_Widget(rootWidget, root);
1614 return win;
1615}
diff --git a/src/ui/window.h b/src/ui/window.h
index 63f7e5f2..f1827931 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -29,8 +29,19 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
29#include <SDL_render.h> 29#include <SDL_render.h>
30#include <SDL_video.h> 30#include <SDL_video.h>
31 31
32enum iWindowType {
33 main_WindowType,
34 popup_WindowType,
35};
36
37iDeclareType(MainWindow)
38iDeclareType(Text)
32iDeclareType(Window) 39iDeclareType(Window)
33iDeclareTypeConstructionArgs(Window, iRect rect) 40
41iDeclareTypeConstructionArgs(Window, enum iWindowType type, iRect rect, uint32_t flags)
42iDeclareTypeConstructionArgs(MainWindow, iRect rect)
43
44typedef iAny iAnyWindow;
34 45
35enum iWindowSnap { 46enum iWindowSnap {
36 none_WindowSnap = 0, 47 none_WindowSnap = 0,
@@ -69,9 +80,8 @@ enum iWindowSplit {
69}; 80};
70 81
71struct Impl_Window { 82struct Impl_Window {
83 enum iWindowType type;
72 SDL_Window * win; 84 SDL_Window * win;
73 iWindowPlacement place;
74 iBool isDrawFrozen; /* avoids premature draws while restoring window state */
75 iBool isExposed; 85 iBool isExposed;
76 iBool isMinimized; 86 iBool isMinimized;
77 iBool isMouseInside; 87 iBool isMouseInside;
@@ -80,12 +90,8 @@ struct Impl_Window {
80 uint32_t focusGainedAt; 90 uint32_t focusGainedAt;
81 SDL_Renderer *render; 91 SDL_Renderer *render;
82 iInt2 size; 92 iInt2 size;
83 int splitMode;
84 int pendingSplitMode;
85 iString * pendingSplitUrl; /* URL to open in a newly opened split */
86 iRoot * roots[2]; /* root widget and UI state; second one is for split mode */
87 iRoot * keyRoot; /* root that has the current keyboard input focus */
88 iWidget * hover; 93 iWidget * hover;
94 iWidget * lastHover; /* cleared if deleted */
89 iWidget * mouseGrab; 95 iWidget * mouseGrab;
90 iWidget * focus; 96 iWidget * focus;
91 float pixelRatio; /* conversion between points and pixels, e.g., coords, window size */ 97 float pixelRatio; /* conversion between points and pixels, e.g., coords, window size */
@@ -93,57 +99,118 @@ struct Impl_Window {
93 float uiScale; 99 float uiScale;
94 uint32_t frameTime; 100 uint32_t frameTime;
95 double presentTime; 101 double presentTime;
96 SDL_Texture * appIcon;
97 SDL_Texture * borderShadow;
98 SDL_Cursor * cursors[SDL_NUM_SYSTEM_CURSORS]; 102 SDL_Cursor * cursors[SDL_NUM_SYSTEM_CURSORS];
99 SDL_Cursor * pendingCursor; 103 SDL_Cursor * pendingCursor;
100 int loadAnimTimer; 104 iRoot * roots[2]; /* root widget and UI state; second one is for split mode */
101 iAnim rootOffset; 105 iRoot * keyRoot; /* root that has the current keyboard input focus */
102 int keyboardHeight; /* mobile software keyboards */ 106 SDL_Texture * borderShadow;
107 iText * text;
103}; 108};
104 109
110struct Impl_MainWindow {
111 iWindow base;
112 iWindowPlacement place;
113 iBool isDrawFrozen; /* avoids premature draws while restoring window state */
114 int splitMode;
115 int pendingSplitMode;
116 iString * pendingSplitUrl; /* URL to open in a newly opened split */
117 SDL_Texture * appIcon;
118 int keyboardHeight; /* mobile software keyboards */
119};
120
121iLocalDef enum iWindowType type_Window(const iAnyWindow *d) {
122 if (d) {
123 return ((const iWindow *) d)->type;
124 }
125 return main_WindowType;
126}
127
128uint32_t id_Window (const iWindow *);
129iInt2 size_Window (const iWindow *);
130iInt2 maxTextureSize_Window (const iWindow *);
131float uiScale_Window (const iWindow *);
132iInt2 coord_Window (const iWindow *, int x, int y);
133iInt2 mouseCoord_Window (const iWindow *, int whichDevice);
134iAnyObject * hitChild_Window (const iWindow *, iInt2 coord);
135uint32_t frameTime_Window (const iWindow *);
136SDL_Renderer * renderer_Window (const iWindow *);
137int numRoots_Window (const iWindow *);
138iRoot * findRoot_Window (const iWindow *, const iWidget *widget);
139iRoot * otherRoot_Window (const iWindow *, iRoot *root);
140
105iBool processEvent_Window (iWindow *, const SDL_Event *); 141iBool processEvent_Window (iWindow *, const SDL_Event *);
106iBool dispatchEvent_Window (iWindow *, const SDL_Event *); 142iBool dispatchEvent_Window (iWindow *, const SDL_Event *);
107void invalidate_Window (iWindow *); /* discard all cached graphics */ 143void invalidate_Window (iAnyWindow *); /* discard all cached graphics */
108void draw_Window (iWindow *); 144void draw_Window (iWindow *);
109void drawWhileResizing_Window(iWindow *d, int w, int h); /* workaround for SDL bug */
110void resize_Window (iWindow *, int w, int h);
111void setTitle_Window (iWindow *, const iString *title);
112void setUiScale_Window (iWindow *, float uiScale); 145void setUiScale_Window (iWindow *, float uiScale);
113void setFreezeDraw_Window (iWindow *, iBool freezeDraw);
114iBool setKeyRoot_Window (iWindow *, iRoot *root);
115void setCursor_Window (iWindow *, int cursor); 146void setCursor_Window (iWindow *, int cursor);
116void setSnap_Window (iWindow *, int snapMode); 147iBool setKeyRoot_Window (iWindow *, iRoot *root);
117void setKeyboardHeight_Window(iWindow *, int height);
118void setSplitMode_Window (iWindow *, int splitMode);
119void showToolbars_Window (iWindow *, iBool show);
120iBool postContextClick_Window (iWindow *, const SDL_MouseButtonEvent *); 148iBool postContextClick_Window (iWindow *, const SDL_MouseButtonEvent *);
121void checkPendingSplit_Window(iWindow *);
122void swapRoots_Window (iWindow *);
123
124uint32_t id_Window (const iWindow *);
125iInt2 size_Window (const iWindow *);
126iInt2 maxTextureSize_Window (const iWindow *);
127float uiScale_Window (const iWindow *);
128iInt2 coord_Window (const iWindow *, int x, int y);
129iInt2 mouseCoord_Window (const iWindow *, int whichDevice);
130iAnyObject *hitChild_Window (const iWindow *, iInt2 coord);
131uint32_t frameTime_Window (const iWindow *);
132SDL_Renderer *renderer_Window (const iWindow *);
133int snap_Window (const iWindow *);
134iBool isFullscreen_Window (const iWindow *);
135int numRoots_Window (const iWindow *);
136iRoot * findRoot_Window (const iWindow *, const iWidget *widget);
137iRoot * otherRoot_Window (const iWindow *, iRoot *root);
138iWindow * get_Window (void);
139 149
150iWindow * get_Window (void);
140iBool isOpenGLRenderer_Window (void); 151iBool isOpenGLRenderer_Window (void);
141 152
142#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 153void setCurrent_Window (iAnyWindow *);
143SDL_HitTestResult hitTest_Window(const iWindow *d, iInt2 pos);
144#endif
145 154
146iLocalDef iBool isExposed_Window(const iWindow *d) { 155iLocalDef iBool isExposed_Window(const iWindow *d) {
147 iAssert(d); 156 iAssert(d);
148 return d->isExposed; 157 return d->isExposed;
149} 158}
159
160iLocalDef iWindow *as_Window(iAnyWindow *d) {
161 iAssert(type_Window(d) == main_WindowType || type_Window(d) == popup_WindowType);
162 return (iWindow *) d;
163}
164
165iLocalDef const iWindow *constAs_Window(const iAnyWindow *d) {
166 iAssert(type_Window(d) == main_WindowType || type_Window(d) == popup_WindowType);
167 return (const iWindow *) d;
168}
169
170iLocalDef iText *text_Window(const iAnyWindow *d) {
171 return constAs_Window(d)->text;
172}
173
174/*----------------------------------------------------------------------------------------------*/
175
176iLocalDef iWindow *asWindow_MainWindow(iMainWindow *d) {
177 iAssert(type_Window(d) == main_WindowType);
178 return &d->base;
179}
180
181void setTitle_MainWindow (iMainWindow *, const iString *title);
182void setSnap_MainWindow (iMainWindow *, int snapMode);
183void setFreezeDraw_MainWindow (iMainWindow *, iBool freezeDraw);
184void setKeyboardHeight_MainWindow (iMainWindow *, int height);
185void setSplitMode_MainWindow (iMainWindow *, int splitMode);
186void checkPendingSplit_MainWindow (iMainWindow *);
187void swapRoots_MainWindow (iMainWindow *);
188void showToolbars_MainWindow (iMainWindow *, iBool show);
189void resize_MainWindow (iMainWindow *, int w, int h);
190
191iBool processEvent_MainWindow (iMainWindow *, const SDL_Event *);
192void draw_MainWindow (iMainWindow *);
193void drawWhileResizing_MainWindow (iMainWindow *, int w, int h); /* workaround for SDL bug */
194
195int snap_MainWindow (const iMainWindow *);
196iBool isFullscreen_Window (const iMainWindow *);
197
198#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
199SDL_HitTestResult hitTest_MainWindow(const iMainWindow *d, iInt2 pos);
200#endif
201
202iMainWindow * get_MainWindow (void);
203
204iLocalDef iMainWindow *as_MainWindow(iAnyWindow *d) {
205 iAssert(type_Window(d) == main_WindowType);
206 return (iMainWindow *) d;
207}
208
209iLocalDef const iMainWindow *constAs_MainWindow(const iAnyWindow *d) {
210 iAssert(type_Window(d) == main_WindowType);
211 return (const iMainWindow *) d;
212}
213
214/*----------------------------------------------------------------------------------------------*/
215
216iWindow * newPopup_Window (iInt2 screenPos, iWidget *rootWidget);
diff --git a/src/visited.c b/src/visited.c
index 94cff492..e9f691c6 100644
--- a/src/visited.c
+++ b/src/visited.c
@@ -29,7 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
29#include <the_Foundation/ptrarray.h> 29#include <the_Foundation/ptrarray.h>
30#include <the_Foundation/sortedarray.h> 30#include <the_Foundation/sortedarray.h>
31 31
32const int maxAge_Visited = 2 * 3600 * 24 * 30; /* two months */ 32const int maxAge_Visited = 6 * 3600 * 24 * 30; /* six months */
33 33
34void init_VisitedUrl(iVisitedUrl *d) { 34void init_VisitedUrl(iVisitedUrl *d) {
35 initCurrent_Time(&d->when); 35 initCurrent_Time(&d->when);