summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.c48
-rw-r--r--src/app.h1
-rw-r--r--src/defs.h11
-rw-r--r--src/gmdocument.c19
-rw-r--r--src/ios.m20
-rw-r--r--src/lang.c9
-rw-r--r--src/ui/color.c2
-rw-r--r--src/ui/documentwidget.c34
-rw-r--r--src/ui/inputwidget.c267
-rw-r--r--src/ui/labelwidget.c14
-rw-r--r--src/ui/listwidget.c2
-rw-r--r--src/ui/lookupwidget.c2
-rw-r--r--src/ui/mobile.c795
-rw-r--r--src/ui/mobile.h32
-rw-r--r--src/ui/paint.c17
-rw-r--r--src/ui/root.c132
-rw-r--r--src/ui/scrollwidget.c2
-rw-r--r--src/ui/sidebarwidget.c97
-rw-r--r--src/ui/text.c60
-rw-r--r--src/ui/text.h7
-rw-r--r--src/ui/touch.c54
-rw-r--r--src/ui/touch.h1
-rw-r--r--src/ui/util.c728
-rw-r--r--src/ui/util.h7
-rw-r--r--src/ui/widget.c133
-rw-r--r--src/ui/widget.h8
-rw-r--r--src/ui/window.c45
27 files changed, 1557 insertions, 990 deletions
diff --git a/src/app.c b/src/app.c
index a50a4553..cc172139 100644
--- a/src/app.c
+++ b/src/app.c
@@ -146,6 +146,7 @@ iDeclareType(Ticker)
146 146
147struct Impl_Ticker { 147struct Impl_Ticker {
148 iAny *context; 148 iAny *context;
149 iRoot *root;
149 void (*callback)(iAny *); 150 void (*callback)(iAny *);
150}; 151};
151 152
@@ -411,15 +412,17 @@ static iBool loadState_App_(iApp *d) {
411 const uint8_t rootIndex = bits & 0xff; 412 const uint8_t rootIndex = bits & 0xff;
412 const uint8_t flags = bits >> 8; 413 const uint8_t flags = bits >> 8;
413 iRoot *root = d->window->roots[rootIndex]; 414 iRoot *root = d->window->roots[rootIndex];
414 if (root && deviceType_App() != phone_AppDeviceType) { 415 if (root) {
415 iSidebarWidget *sidebar = findChild_Widget(root->widget, "sidebar"); 416 iSidebarWidget *sidebar = findChild_Widget(root->widget, "sidebar");
416 iSidebarWidget *sidebar2 = findChild_Widget(root->widget, "sidebar2"); 417 iSidebarWidget *sidebar2 = findChild_Widget(root->widget, "sidebar2");
417 setWidth_SidebarWidget(sidebar, widths[0]);
418 setWidth_SidebarWidget(sidebar2, widths[1]);
419 postCommandf_Root(root, "sidebar.mode arg:%u", modes & 0xf); 418 postCommandf_Root(root, "sidebar.mode arg:%u", modes & 0xf);
420 postCommandf_Root(root, "sidebar2.mode arg:%u", modes >> 4); 419 postCommandf_Root(root, "sidebar2.mode arg:%u", modes >> 4);
421 if (flags & 1) postCommand_Root(root, "sidebar.toggle"); 420 if (deviceType_App() != phone_AppDeviceType) {
422 if (flags & 2) postCommand_Root(root, "sidebar2.toggle"); 421 setWidth_SidebarWidget(sidebar, widths[0]);
422 setWidth_SidebarWidget(sidebar2, widths[1]);
423 if (flags & 1) postCommand_Root(root, "sidebar.toggle noanim:1");
424 if (flags & 2) postCommand_Root(root, "sidebar2.toggle noanim:1");
425 }
423 } 426 }
424 } 427 }
425 else if (!memcmp(magic, magicTabDocument_App_, 4)) { 428 else if (!memcmp(magic, magicTabDocument_App_, 4)) {
@@ -776,6 +779,10 @@ static void init_App_(iApp *d, int argc, char **argv) {
776 iRelease(openCmds); 779 iRelease(openCmds);
777 } 780 }
778 fetchRemote_Bookmarks(d->bookmarks); 781 fetchRemote_Bookmarks(d->bookmarks);
782 if (deviceType_App() != desktop_AppDeviceType) {
783 /* HACK: Force a resize so widgets update their state. */
784 resize_Window(d->window, -1, -1);
785 }
779} 786}
780 787
781static void deinit_App(iApp *d) { 788static void deinit_App(iApp *d) {
@@ -1061,7 +1068,14 @@ void processEvents_App(enum iAppEventMode eventMode) {
1061 } 1068 }
1062 continue; 1069 continue;
1063 } 1070 }
1064 else if (ev.type == SDL_USEREVENT && ev.user.code == arrange_UserEventCode) { 1071 d->lastEventTime = SDL_GetTicks();
1072 if (d->isIdling) {
1073// printf("[App] ...woke up\n");
1074// fflush(stdout);
1075 }
1076 d->isIdling = iFalse;
1077#endif
1078 if (ev.type == SDL_USEREVENT && ev.user.code == arrange_UserEventCode) {
1065 printf("[App] rearrange\n"); 1079 printf("[App] rearrange\n");
1066 resize_Window(d->window, -1, -1); 1080 resize_Window(d->window, -1, -1);
1067 iForIndices(i, d->window->roots) { 1081 iForIndices(i, d->window->roots) {
@@ -1078,13 +1092,6 @@ void processEvents_App(enum iAppEventMode eventMode) {
1078// postRefresh_App(); 1092// postRefresh_App();
1079 continue; 1093 continue;
1080 } 1094 }
1081 d->lastEventTime = SDL_GetTicks();
1082 if (d->isIdling) {
1083// printf("[App] ...woke up\n");
1084// fflush(stdout);
1085 }
1086 d->isIdling = iFalse;
1087#endif
1088 gotEvents = iTrue; 1095 gotEvents = iTrue;
1089 /* Keyboard modifier mapping. */ 1096 /* Keyboard modifier mapping. */
1090 if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) { 1097 if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) {
@@ -1187,7 +1194,7 @@ static void runTickers_App_(iApp *d) {
1187 iConstForEach(Array, i, &pending->values) { 1194 iConstForEach(Array, i, &pending->values) {
1188 const iTicker *ticker = i.value; 1195 const iTicker *ticker = i.value;
1189 if (ticker->callback) { 1196 if (ticker->callback) {
1190 setCurrent_Root(findRoot_Window(d->window, ticker->context)); /* root might be NULL */ 1197 setCurrent_Root(ticker->root); /* root might be NULL */
1191 ticker->callback(ticker->context); 1198 ticker->callback(ticker->context);
1192 } 1199 }
1193 } 1200 }
@@ -1418,13 +1425,19 @@ iAny *findWidget_App(const char *id) {
1418 1425
1419void addTicker_App(iTickerFunc ticker, iAny *context) { 1426void addTicker_App(iTickerFunc ticker, iAny *context) {
1420 iApp *d = &app_; 1427 iApp *d = &app_;
1421 insert_SortedArray(&d->tickers, &(iTicker){ context, ticker }); 1428 insert_SortedArray(&d->tickers, &(iTicker){ context, get_Root(), ticker });
1429 postRefresh_App();
1430}
1431
1432void addTickerRoot_App(iTickerFunc ticker, iRoot *root, iAny *context) {
1433 iApp *d = &app_;
1434 insert_SortedArray(&d->tickers, &(iTicker){ context, root, ticker });
1422 postRefresh_App(); 1435 postRefresh_App();
1423} 1436}
1424 1437
1425void removeTicker_App(iTickerFunc ticker, iAny *context) { 1438void removeTicker_App(iTickerFunc ticker, iAny *context) {
1426 iApp *d = &app_; 1439 iApp *d = &app_;
1427 remove_SortedArray(&d->tickers, &(iTicker){ context, ticker }); 1440 remove_SortedArray(&d->tickers, &(iTicker){ context, NULL, ticker });
1428} 1441}
1429 1442
1430iMimeHooks *mimeHooks_App(void) { 1443iMimeHooks *mimeHooks_App(void) {
@@ -1507,6 +1520,7 @@ static void updateFontButton_(iLabelWidget *button, int font) {
1507 1520
1508static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { 1521static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1509 if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) { 1522 if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) {
1523 setupSheetTransition_Mobile(d, iFalse);
1510 setUiScale_Window(get_Window(), 1524 setUiScale_Window(get_Window(),
1511 toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale")))); 1525 toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale"))));
1512#if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT) 1526#if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
@@ -2381,7 +2395,7 @@ iBool handleCommand_App(const char *cmd) {
2381 iCertImportWidget *imp = new_CertImportWidget(); 2395 iCertImportWidget *imp = new_CertImportWidget();
2382 setPageContent_CertImportWidget(imp, sourceContent_DocumentWidget(document_App())); 2396 setPageContent_CertImportWidget(imp, sourceContent_DocumentWidget(document_App()));
2383 addChild_Widget(get_Root()->widget, iClob(imp)); 2397 addChild_Widget(get_Root()->widget, iClob(imp));
2384 finalizeSheet_Widget(as_Widget(imp)); 2398 finalizeSheet_Mobile(as_Widget(imp));
2385 postRefresh_App(); 2399 postRefresh_App();
2386 return iTrue; 2400 return iTrue;
2387 } 2401 }
diff --git a/src/app.h b/src/app.h
index aa647bfb..0fb6be29 100644
--- a/src/app.h
+++ b/src/app.h
@@ -110,6 +110,7 @@ typedef void (*iTickerFunc)(iAny *);
110 110
111iAny * findWidget_App (const char *id); 111iAny * findWidget_App (const char *id);
112void addTicker_App (iTickerFunc ticker, iAny *context); 112void addTicker_App (iTickerFunc ticker, iAny *context);
113void addTickerRoot_App (iTickerFunc ticker, iRoot *root, iAny *context);
113void removeTicker_App (iTickerFunc ticker, iAny *context); 114void removeTicker_App (iTickerFunc ticker, iAny *context);
114void postRefresh_App (void); 115void postRefresh_App (void);
115void postImmediateRefresh_App(void); 116void postImmediateRefresh_App(void);
diff --git a/src/defs.h b/src/defs.h
index cc485ed1..71719f7a 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -66,6 +66,7 @@ enum iFileVersion {
66#define ballotCheck_Icon "\U0001f5f9" 66#define ballotCheck_Icon "\U0001f5f9"
67#define inbox_Icon "\U0001f4e5" 67#define inbox_Icon "\U0001f4e5"
68#define book_Icon "\U0001f56e" 68#define book_Icon "\U0001f56e"
69#define bookmark_Icon "\U0001f516"
69#define folder_Icon "\U0001f4c1" 70#define folder_Icon "\U0001f4c1"
70#define openTab_Icon "\u2750" 71#define openTab_Icon "\u2750"
71#define openTabBg_Icon "\u2b1a" 72#define openTabBg_Icon "\u2b1a"
@@ -90,6 +91,16 @@ enum iFileVersion {
90#define globe_Icon "\U0001f310" 91#define globe_Icon "\U0001f310"
91#define magnifyingGlass_Icon "\U0001f50d" 92#define magnifyingGlass_Icon "\U0001f50d"
92#define midEllipsis_Icon "\u00b7\u00b7\u00b7" 93#define midEllipsis_Icon "\u00b7\u00b7\u00b7"
94#define return_Icon "\u21a9"
95
96#if defined (iPlatformApple)
97# define shift_Icon "\u21e7"
98# define shiftReturn_Icon shift_Icon return_Icon
99#else
100# define shift_Icon "Shift"
101# define shiftReturn_Icon shift_Icon " " return_Icon
102#endif
103
93 104
94/* UI labels that depend on the platform */ 105/* UI labels that depend on the platform */
95 106
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 3daa4714..67adb9cc 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -716,12 +716,25 @@ static void doLayout_GmDocument_(iGmDocument *d) {
716 meta->flags |= topLeft_GmPreMetaFlag; 716 meta->flags |= topLeft_GmPreMetaFlag;
717 } 717 }
718 } 718 }
719 /* Visited links are never bold. */ 719 float lineHeightReduction = 0.0f;
720 if (!isMono && run.linkId && linkFlags_GmDocument(d, run.linkId) & visited_GmLinkFlag) { 720 if (!isMono) {
721 run.font = paragraph_FontId; 721 /* Upper-level headings are typeset a bit tighter. */
722 if (type == heading1_GmLineType) {
723 lineHeightReduction = 0.10f;
724 }
725 else if (type == heading2_GmLineType) {
726 lineHeightReduction = 0.05f;
727 }
728 /* Visited links are never bold. */
729 if (run.linkId && linkFlags_GmDocument(d, run.linkId) & visited_GmLinkFlag) {
730 run.font = paragraph_FontId;
731 }
722 } 732 }
723 iAssert(!isEmpty_Range(&runLine)); /* must have something at this point */ 733 iAssert(!isEmpty_Range(&runLine)); /* must have something at this point */
724 while (!isEmpty_Range(&runLine)) { 734 while (!isEmpty_Range(&runLine)) {
735 if (~run.flags & startOfLine_GmRunFlag && lineHeightReduction > 0.0f) {
736 pos.y -= lineHeightReduction * lineHeight_Text(run.font);
737 }
725 run.bounds.pos = addX_I2(pos, indent * gap_Text); 738 run.bounds.pos = addX_I2(pos, indent * gap_Text);
726 const int wrapAvail = d->size.x - run.bounds.pos.x - rightMargin * gap_Text; 739 const int wrapAvail = d->size.x - run.bounds.pos.x - rightMargin * gap_Text;
727 const int avail = isWordWrapped ? wrapAvail : 0; 740 const int avail = isWordWrapped ? wrapAvail : 0;
diff --git a/src/ios.m b/src/ios.m
index 6e2fa64b..a1654df2 100644
--- a/src/ios.m
+++ b/src/ios.m
@@ -196,7 +196,7 @@ didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
196 CGRect keyboardFrame = [view convertRect:rawFrame fromView:nil]; 196 CGRect keyboardFrame = [view convertRect:rawFrame fromView:nil];
197// NSLog(@"keyboardFrame: %@", NSStringFromCGRect(keyboardFrame)); 197// NSLog(@"keyboardFrame: %@", NSStringFromCGRect(keyboardFrame));
198 iWindow *window = get_Window(); 198 iWindow *window = get_Window();
199 const iInt2 rootSize = rootSize_Window(window); 199 const iInt2 rootSize = size_Root(window->roots[0]);
200 const int keyTop = keyboardFrame.origin.y * window->pixelRatio; 200 const int keyTop = keyboardFrame.origin.y * window->pixelRatio;
201 setKeyboardHeight_Window(window, rootSize.y - keyTop); 201 setKeyboardHeight_Window(window, rootSize.y - keyTop);
202} 202}
@@ -302,6 +302,24 @@ iBool processEvent_iOS(const SDL_Event *ev) {
302 postCommandf_App("os.theme.changed dark:%d contrast:1", isSystemDarkMode_ ? 1 : 0); 302 postCommandf_App("os.theme.changed dark:%d contrast:1", isSystemDarkMode_ ? 1 : 0);
303 } 303 }
304 } 304 }
305 else if (equal_Command(cmd, "theme.changed")) {
306 if (@available(iOS 13.0, *)) {
307 /* SDL doesn't expose this as a setting, so we'll rely on a hack.
308 Adding an SDL hint for this would be a cleaner solution than calling
309 a private method. */
310 UIViewController *vc = viewController_(get_Window());
311 SEL sel = NSSelectorFromString(@"setStatusStyle:"); /* custom method */
312 if ([vc respondsToSelector:sel]) {
313 NSInvocation *call = [NSInvocation invocationWithMethodSignature:
314 [NSMethodSignature signatureWithObjCTypes:"v@:i"]];
315 [call setSelector:sel];
316 int style = isDark_ColorTheme(colorTheme_App()) ?
317 UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent;
318 [call setArgument:&style atIndex:2];
319 [call invokeWithTarget:vc];
320 }
321 }
322 }
305 } 323 }
306 return iFalse; /* allow normal processing */ 324 return iFalse; /* allow normal processing */
307} 325}
diff --git a/src/lang.c b/src/lang.c
index cd5d3120..4c05647c 100644
--- a/src/lang.c
+++ b/src/lang.c
@@ -44,6 +44,7 @@ int cmp_MsgStr_(const void *e1, const void *e2) {
44enum iPluralType { 44enum iPluralType {
45 none_PluralType, 45 none_PluralType,
46 notEqualToOne_PluralType, 46 notEqualToOne_PluralType,
47 polish_PluralType,
47 slavic_PluralType, 48 slavic_PluralType,
48}; 49};
49 50
@@ -58,6 +59,10 @@ static size_t pluralIndex_Lang_(const iLang *d, int n) {
58 switch (d->pluralType) { 59 switch (d->pluralType) {
59 case notEqualToOne_PluralType: 60 case notEqualToOne_PluralType:
60 return n != 1; 61 return n != 1;
62 case polish_PluralType:
63 return n == 1 ? 0
64 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1
65 : 2;
61 case slavic_PluralType: 66 case slavic_PluralType:
62 return n % 10 == 1 && n % 100 != 11 ? 0 67 return n % 10 == 1 && n % 100 != 11 ? 0
63 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 68 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1
@@ -81,6 +86,7 @@ static void load_Lang_(iLang *d, const char *id) {
81 : equal_CStr(id, "de") ? &blobDe_Embedded 86 : equal_CStr(id, "de") ? &blobDe_Embedded
82 : equal_CStr(id, "ia") ? &blobIa_Embedded 87 : equal_CStr(id, "ia") ? &blobIa_Embedded
83 : equal_CStr(id, "ie") ? &blobIe_Embedded 88 : equal_CStr(id, "ie") ? &blobIe_Embedded
89 : equal_CStr(id, "pl") ? &blobPl_Embedded
84 : equal_CStr(id, "sr") ? &blobSr_Embedded 90 : equal_CStr(id, "sr") ? &blobSr_Embedded
85 : equal_CStr(id, "tok") ? &blobTok_Embedded 91 : equal_CStr(id, "tok") ? &blobTok_Embedded
86 : equal_CStr(id, "zh_Hans") ? &blobZh_Hans_Embedded 92 : equal_CStr(id, "zh_Hans") ? &blobZh_Hans_Embedded
@@ -89,6 +95,9 @@ static void load_Lang_(iLang *d, const char *id) {
89 if (data == &blobRu_Embedded || data == &blobSr_Embedded) { 95 if (data == &blobRu_Embedded || data == &blobSr_Embedded) {
90 d->pluralType = slavic_PluralType; 96 d->pluralType = slavic_PluralType;
91 } 97 }
98 else if (data == &blobPl_Embedded) {
99 d->pluralType = polish_PluralType;
100 }
92 else if (data == &blobZh_Hans_Embedded || data == &blobZh_Hant_Embedded || 101 else if (data == &blobZh_Hans_Embedded || data == &blobZh_Hant_Embedded ||
93 data == &blobTok_Embedded) { 102 data == &blobTok_Embedded) {
94 d->pluralType = none_PluralType; 103 d->pluralType = none_PluralType;
diff --git a/src/ui/color.c b/src/ui/color.c
index 0a177a8a..6c51bc06 100644
--- a/src/ui/color.c
+++ b/src/ui/color.c
@@ -305,7 +305,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
305 0.4f)); 305 0.4f));
306 uiPalette_[uiMarked_ColorId ].a = 128; 306 uiPalette_[uiMarked_ColorId ].a = 128;
307 uiPalette_[uiMatching_ColorId].a = 128; 307 uiPalette_[uiMatching_ColorId].a = 128;
308 if (deviceType_App() == phone_AppDeviceType) { 308 if (deviceType_App() != desktop_AppDeviceType) {
309 copy_(uiInputBackground_ColorId, uiBackgroundSidebar_ColorId); 309 copy_(uiInputBackground_ColorId, uiBackgroundSidebar_ColorId);
310 copy_(uiInputFrame_ColorId, uiBackgroundSidebar_ColorId); 310 copy_(uiInputFrame_ColorId, uiBackgroundSidebar_ColorId);
311 copy_(uiInputFrameFocused_ColorId, uiBackgroundSidebar_ColorId); 311 copy_(uiInputFrameFocused_ColorId, uiBackgroundSidebar_ColorId);
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index d23e95a6..5dcdace6 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -1466,7 +1466,8 @@ static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t du
1466 } 1466 }
1467 /* Show and hide toolbar on scroll. */ 1467 /* Show and hide toolbar on scroll. */
1468 if (deviceType_App() == phone_AppDeviceType) { 1468 if (deviceType_App() == phone_AppDeviceType) {
1469 if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5) { 1469 const float normPos = normScrollPos_DocumentWidget_(d);
1470 if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) {
1470 showToolbars_Root(as_Widget(d)->root, offset < 0); 1471 showToolbars_Root(as_Widget(d)->root, offset < 0);
1471 } 1472 }
1472 } 1473 }
@@ -1606,6 +1607,7 @@ static void inputQueryValidator_(iInputWidget *input, void *context) {
1606 avail < 128 ? uiTextStrong_ColorId 1607 avail < 128 ? uiTextStrong_ColorId
1607 : uiTextDim_ColorId); 1608 : uiTextDim_ColorId);
1608 delete_String(url); 1609 delete_String(url);
1610 arrange_Widget(findChild_Widget(dlg, "dialogbuttons"));
1609} 1611}
1610 1612
1611static void checkResponse_DocumentWidget_(iDocumentWidget *d) { 1613static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
@@ -1637,10 +1639,28 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1637 : cstr_String(&resp->meta), 1639 : cstr_String(&resp->meta),
1638 uiTextCaution_ColorEscape "${dlg.input.send}", 1640 uiTextCaution_ColorEscape "${dlg.input.send}",
1639 format_CStr("!document.input.submit doc:%p", d)); 1641 format_CStr("!document.input.submit doc:%p", d));
1640 setId_Widget(addChildPosFlags_Widget(findChild_Widget(dlg, "dialogbuttons"), 1642 iWidget *buttons = findChild_Widget(dlg, "dialogbuttons");
1643 iLabelWidget *lineBreak;
1644 /* The line break and URL length counters are positioned differently on mobile. */
1645 if (deviceType_App() == desktop_AppDeviceType) {
1646 lineBreak = new_LabelWidget("${dlg.input.linebreak}"
1647 uiTextAction_ColorEscape
1648 " " shiftReturn_Icon,
1649 NULL);
1650 insertChildAfter_Widget(buttons, iClob(lineBreak), 0);
1651 }
1652 else {
1653 lineBreak = new_LabelWidget("${dlg.input.linebreak}", "text.insert arg:10");
1654 }
1655 setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue);
1656 setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId);
1657 setId_Widget(addChildPosFlags_Widget(buttons,
1641 iClob(new_LabelWidget("", NULL)), 1658 iClob(new_LabelWidget("", NULL)),
1642 front_WidgetAddPos, frameless_WidgetFlag), 1659 front_WidgetAddPos, frameless_WidgetFlag),
1643 "valueinput.counter"); 1660 "valueinput.counter");
1661 if (deviceType_App() != desktop_AppDeviceType) {
1662 addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos);
1663 }
1644 setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d); 1664 setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d);
1645 setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), 1665 setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"),
1646 statusCode == sensitiveInput_GmStatusCode); 1666 statusCode == sensitiveInput_GmStatusCode);
@@ -1653,7 +1673,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1653 break; 1673 break;
1654 } 1674 }
1655 case categorySuccess_GmStatusCode: 1675 case categorySuccess_GmStatusCode:
1656 reset_SmoothScroll(&d->scrollY); 1676 //reset_SmoothScroll(&d->scrollY);
1657 iRelease(d->doc); /* new content incoming */ 1677 iRelease(d->doc); /* new content incoming */
1658 d->doc = new_GmDocument(); 1678 d->doc = new_GmDocument();
1659 delete_Gempub(d->sourceGempub); 1679 delete_Gempub(d->sourceGempub);
@@ -2310,7 +2330,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2310 } 2330 }
2311 updateFetchProgress_DocumentWidget_(d); 2331 updateFetchProgress_DocumentWidget_(d);
2312 checkResponse_DocumentWidget_(d); 2332 checkResponse_DocumentWidget_(d);
2313 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); /* TODO: unless user already scrolled! */ 2333 if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) {
2334 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); /* TODO: unless user already scrolled! */
2335 }
2314 d->state = ready_RequestState; 2336 d->state = ready_RequestState;
2315 postProcessRequestContent_DocumentWidget_(d, iFalse); 2337 postProcessRequestContent_DocumentWidget_(d, iFalse);
2316 /* The response may be cached. */ 2338 /* The response may be cached. */
@@ -3081,7 +3103,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3081 pushBackN_Array(&items, 3103 pushBackN_Array(&items,
3082 (iMenuItem[]){ { "---", 0, 0, NULL }, 3104 (iMenuItem[]){ { "---", 0, 0, NULL },
3083 { "${link.copy}", 0, 0, "document.copylink" }, 3105 { "${link.copy}", 0, 0, "document.copylink" },
3084 { pin_Icon " ${link.bookmark}", 3106 { bookmark_Icon " ${link.bookmark}",
3085 0, 3107 0,
3086 0, 3108 0,
3087 format_CStr("!bookmark.add title:%s url:%s", 3109 format_CStr("!bookmark.add title:%s url:%s",
@@ -3126,7 +3148,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3126 { reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" }, 3148 { reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" },
3127 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" }, 3149 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" },
3128 { "---", 0, 0, NULL }, 3150 { "---", 0, 0, NULL },
3129 { pin_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, 3151 { bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
3130 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, 3152 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
3131 { "---", 0, 0, NULL }, 3153 { "---", 0, 0, NULL },
3132 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, 3154 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 6719fb40..784fabdd 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -42,8 +42,12 @@ static const size_t maxUndo_InputWidget_ = 64;
42 42
43static void enableEditorKeysInMenus_(iBool enable) { 43static void enableEditorKeysInMenus_(iBool enable) {
44#if defined (iPlatformAppleDesktop) 44#if defined (iPlatformAppleDesktop)
45 enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable); 45 enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable);
46 enableMenuItemsByKey_MacOS(SDLK_RIGHT, KMOD_PRIMARY, enable); 46 enableMenuItemsByKey_MacOS(SDLK_RIGHT, KMOD_PRIMARY, enable);
47 enableMenuItemsByKey_MacOS(SDLK_UP, KMOD_PRIMARY, enable);
48 enableMenuItemsByKey_MacOS(SDLK_DOWN, KMOD_PRIMARY, enable);
49 enableMenuItemsByKey_MacOS(SDLK_UP, KMOD_PRIMARY | KMOD_SHIFT, enable);
50 enableMenuItemsByKey_MacOS(SDLK_DOWN, KMOD_PRIMARY | KMOD_SHIFT, enable);
47#else 51#else
48 iUnused(enable); 52 iUnused(enable);
49#endif 53#endif
@@ -84,6 +88,7 @@ iDeclareType(InputLine)
84 88
85struct Impl_InputLine { 89struct Impl_InputLine {
86 size_t offset; /* character position from the beginning */ 90 size_t offset; /* character position from the beginning */
91 size_t len; /* length as characters */
87 iString text; /* UTF-8 */ 92 iString text; /* UTF-8 */
88}; 93};
89 94
@@ -109,6 +114,7 @@ struct Impl_InputWidget {
109 iArray text; /* iChar[] */ 114 iArray text; /* iChar[] */
110 iArray oldText; /* iChar[] */ 115 iArray oldText; /* iChar[] */
111 iArray lines; 116 iArray lines;
117 int lastUpdateWidth;
112 iString hint; 118 iString hint;
113 iString srcHint; 119 iString srcHint;
114 int leftPadding; 120 int leftPadding;
@@ -138,7 +144,26 @@ static void clearUndo_InputWidget_(iInputWidget *d) {
138 clear_Array(&d->undoStack); 144 clear_Array(&d->undoStack);
139} 145}
140 146
147iLocalDef iInt2 padding_(void) {
148 return init_I2(gap_UI / 2, gap_UI / 2);
149}
150
151static iRect contentBounds_InputWidget_(const iInputWidget *d) {
152 const iWidget *w = constAs_Widget(d);
153 // const iRect widgetBounds = bounds_Widget(w);
154 iRect bounds = adjusted_Rect(bounds_Widget(w),
155 addX_I2(padding_(), d->leftPadding),
156 neg_I2(addX_I2(padding_(), d->rightPadding)));
157 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0));
158 bounds.pos.y += padding_().y / 2;
159 if (flags_Widget(w) & extraPadding_WidgetFlag) {
160 bounds.pos.y += gap_UI;
161 }
162 return bounds;
163}
164
141static void updateCursorLine_InputWidget_(iInputWidget *d) { 165static void updateCursorLine_InputWidget_(iInputWidget *d) {
166 iWidget *w = as_Widget(d);
142 d->cursorLine = 0; 167 d->cursorLine = 0;
143 iConstForEach(Array, i, &d->lines) { 168 iConstForEach(Array, i, &d->lines) {
144 const iInputLine *line = i.value; 169 const iInputLine *line = i.value;
@@ -147,6 +172,20 @@ static void updateCursorLine_InputWidget_(iInputWidget *d) {
147 } 172 }
148 d->cursorLine = index_ArrayConstIterator(&i); 173 d->cursorLine = index_ArrayConstIterator(&i);
149 } 174 }
175 /* May need to scroll to keep the cursor visible. */
176 iWidget *flow = findOverflowScrollable_Widget(w);
177 if (flow) {
178 const iRect rootRect = { rect_Root(w->root).pos, visibleSize_Root(w->root) };
179 int yCursor = contentBounds_InputWidget_(d).pos.y +
180 lineHeight_Text(d->font) * (int) d->cursorLine;
181 const int margin = lineHeight_Text(d->font) * 3;
182 if (yCursor < top_Rect(rootRect) + margin) {
183 scrollOverflow_Widget(flow, top_Rect(rootRect) + margin - yCursor);
184 }
185 else if (yCursor > bottom_Rect(rootRect) - margin * 3 / 2) {
186 scrollOverflow_Widget(flow, bottom_Rect(rootRect) - margin * 3 / 2 - yCursor);
187 }
188 }
150} 189}
151 190
152static void showCursor_InputWidget_(iInputWidget *d) { 191static void showCursor_InputWidget_(iInputWidget *d) {
@@ -161,21 +200,6 @@ static void invalidateBuffered_InputWidget_(iInputWidget *d) {
161 } 200 }
162} 201}
163 202
164iLocalDef iInt2 padding_(void) {
165 return init_I2(gap_UI / 2, gap_UI / 2);
166}
167
168static iRect contentBounds_InputWidget_(const iInputWidget *d) {
169 const iWidget *w = constAs_Widget(d);
170 const iRect widgetBounds = bounds_Widget(w);
171 iRect bounds = adjusted_Rect(bounds_Widget(w),
172 addX_I2(padding_(), d->leftPadding),
173 neg_I2(addX_I2(padding_(), d->rightPadding)));
174 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0));
175 bounds.pos.y += padding_().y / 2;
176 return bounds;
177}
178
179static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) { 203static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) {
180 if (d->maxLen) { 204 if (d->maxLen) {
181 /* Set a fixed size based on maximum possible width of the text. */ 205 /* Set a fixed size based on maximum possible width of the text. */
@@ -219,6 +243,7 @@ static void clearLines_InputWidget_(iInputWidget *d) {
219} 243}
220 244
221static void updateLines_InputWidget_(iInputWidget *d) { 245static void updateLines_InputWidget_(iInputWidget *d) {
246 d->lastUpdateWidth = d->widget.rect.size.x;
222 clearLines_InputWidget_(d); 247 clearLines_InputWidget_(d);
223 if (d->maxLen) { 248 if (d->maxLen) {
224 /* Everything on a single line. */ 249 /* Everything on a single line. */
@@ -226,6 +251,7 @@ static void updateLines_InputWidget_(iInputWidget *d) {
226 init_InputLine(&line); 251 init_InputLine(&line);
227 iString *u8 = visText_InputWidget_(d); 252 iString *u8 = visText_InputWidget_(d);
228 set_String(&line.text, u8); 253 set_String(&line.text, u8);
254 line.len = length_String(u8);
229 delete_String(u8); 255 delete_String(u8);
230 pushBack_Array(&d->lines, &line); 256 pushBack_Array(&d->lines, &line);
231 updateCursorLine_InputWidget_(d); 257 updateCursorLine_InputWidget_(d);
@@ -249,14 +275,16 @@ static void updateLines_InputWidget_(iInputWidget *d) {
249 init_InputLine(&line); 275 init_InputLine(&line);
250 setRange_String(&line.text, part); 276 setRange_String(&line.text, part);
251 line.offset = charPos; 277 line.offset = charPos;
278 line.len = length_String(&line.text);
252 pushBack_Array(&d->lines, &line); 279 pushBack_Array(&d->lines, &line);
253 charPos += length_String(&line.text); 280 charPos += line.len;
254 content.start = endPos; 281 content.start = endPos;
255 } 282 }
256 if (isEmpty_Array(&d->lines) || endsWith_String(u8, "\n")) { 283 if (isEmpty_Array(&d->lines) || endsWith_String(u8, "\n")) {
257 /* Always at least one empty line. */ 284 /* Always at least one empty line. */
258 iInputLine line; 285 iInputLine line;
259 init_InputLine(&line); 286 init_InputLine(&line);
287 line.offset = charPos;
260 pushBack_Array(&d->lines, &line); 288 pushBack_Array(&d->lines, &line);
261 } 289 }
262 else { 290 else {
@@ -278,7 +306,7 @@ static void updateMetrics_InputWidget_(iInputWidget *d) {
278 iWidget *w = as_Widget(d); 306 iWidget *w = as_Widget(d);
279 updateSizeForFixedLength_InputWidget_(d); 307 updateSizeForFixedLength_InputWidget_(d);
280 /* Caller must arrange the width, but the height is fixed. */ 308 /* Caller must arrange the width, but the height is fixed. */
281 w->rect.size.y = contentHeight_InputWidget_(d, iTrue) + 3 * padding_().y; /* TODO: Why 3x? */ 309 w->rect.size.y = contentHeight_InputWidget_(d, iTrue) + 3.0f * padding_().y; /* TODO: Why 3x? */
282 if (flags_Widget(w) & extraPadding_WidgetFlag) { 310 if (flags_Widget(w) & extraPadding_WidgetFlag) {
283 w->rect.size.y += 2 * gap_UI; 311 w->rect.size.y += 2 * gap_UI;
284 } 312 }
@@ -316,6 +344,7 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
316 d->cursor = 0; 344 d->cursor = 0;
317 d->lastCursor = 0; 345 d->lastCursor = 0;
318 d->cursorLine = 0; 346 d->cursorLine = 0;
347 d->lastUpdateWidth = 0;
319 d->verticalMoveX = -1; /* TODO: Use this. */ 348 d->verticalMoveX = -1; /* TODO: Use this. */
320 d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag; 349 d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag;
321 iZap(d->mark); 350 iZap(d->mark);
@@ -455,31 +484,46 @@ void setContentPadding_InputWidget(iInputWidget *d, int left, int right) {
455 refresh_Widget(d); 484 refresh_Widget(d);
456} 485}
457 486
487static iBool isHintVisible_InputWidget_(const iInputWidget *d) {
488 return !isEmpty_String(&d->hint) && size_Array(&d->lines) == 1 &&
489 isEmpty_String(&line_InputWidget_(d, 0)->text);
490}
491
458static void updateBuffered_InputWidget_(iInputWidget *d) { 492static void updateBuffered_InputWidget_(iInputWidget *d) {
459 invalidateBuffered_InputWidget_(d); 493 invalidateBuffered_InputWidget_(d);
460 iString *bufText = NULL; 494 if (isHintVisible_InputWidget_(d)) {
495 d->buffered = new_TextBuf(d->font, uiAnnotation_ColorId, cstr_String(&d->hint));
496 }
497 else {
498 iString *bufText = NULL;
461#if 0 499#if 0
462 if (d->inFlags & isUrl_InputWidgetFlag && as_Widget(d)->root == win->keyRoot) { 500 if (d->inFlags & isUrl_InputWidgetFlag && as_Widget(d)->root == win->keyRoot) {
463 /* TODO: Move this omitting to `updateLines_`? */ 501 /* TODO: Move this omitting to `updateLines_`? */
464 /* Highlight the host name. */ 502 /* Highlight the host name. */
465 iUrl parts; 503 iUrl parts;
466 const iString *text = collect_String(utf32toUtf8_InputWidget_(d)); 504 const iString *text = collect_String(utf32toUtf8_InputWidget_(d));
467 init_Url(&parts, text); 505 init_Url(&parts, text);
468 if (!isEmpty_Range(&parts.host)) { 506 if (!isEmpty_Range(&parts.host)) {
469 bufText = new_String(); 507 bufText = new_String();
470 appendRange_String(bufText, (iRangecc){ constBegin_String(text), parts.host.start }); 508 appendRange_String(bufText, (iRangecc){ constBegin_String(text), parts.host.start });
471 appendCStr_String(bufText, uiTextStrong_ColorEscape); 509 appendCStr_String(bufText, uiTextStrong_ColorEscape);
472 appendRange_String(bufText, parts.host); 510 appendRange_String(bufText, parts.host);
473 appendCStr_String(bufText, restore_ColorEscape); 511 appendCStr_String(bufText, restore_ColorEscape);
474 appendRange_String(bufText, (iRangecc){ parts.host.end, constEnd_String(text) }); 512 appendRange_String(bufText, (iRangecc){ parts.host.end, constEnd_String(text) });
513 }
475 } 514 }
476 }
477#endif 515#endif
478 if (!bufText) { 516 if (!bufText) {
479 bufText = visText_InputWidget_(d); 517 bufText = visText_InputWidget_(d);
518 }
519 const int maxWidth = contentBounds_InputWidget_(d).size.x;
520 const int fg = uiInputText_ColorId;
521 const char *text = cstr_String(bufText);
522 d->buffered =
523 (d->inFlags & isUrl_InputWidgetFlag ? newBound_TextBuf(d->font, fg, maxWidth, text)
524 : newWrap_TextBuf (d->font, fg, maxWidth, text));
525 delete_String(bufText);
480 } 526 }
481 d->buffered = new_TextBuf(d->font, uiInputText_ColorId, cstr_String(bufText));
482 delete_String(bufText);
483 d->inFlags &= ~needUpdateBuffer_InputWidgetFlag; 527 d->inFlags &= ~needUpdateBuffer_InputWidgetFlag;
484} 528}
485 529
@@ -563,7 +607,11 @@ void begin_InputWidget(iInputWidget *d) {
563 } 607 }
564 updateCursorLine_InputWidget_(d); 608 updateCursorLine_InputWidget_(d);
565 SDL_StartTextInput(); 609 SDL_StartTextInput();
566 setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag, iTrue); 610 setFlags_Widget(w, selected_WidgetFlag, iTrue);
611 if (d->maxLayoutLines != iInvalidSize) {
612 /* This will extend beyond the arranged region. */
613 setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue);
614 }
567 showCursor_InputWidget_(d); 615 showCursor_InputWidget_(d);
568 refresh_Widget(w); 616 refresh_Widget(w);
569 d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d); 617 d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d);
@@ -657,6 +705,10 @@ void setCursor_InputWidget(iInputWidget *d, size_t pos) {
657 showCursor_InputWidget_(d); 705 showCursor_InputWidget_(d);
658} 706}
659 707
708iLocalDef iBool isLastLine_InputWidget_(const iInputWidget *d, const iInputLine *line) {
709 return (const void *) line == constAt_Array(&d->lines, size_Array(&d->lines) - 1);
710}
711
660static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const iInputLine *line) { 712static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const iInputLine *line) {
661 size_t index = line->offset; 713 size_t index = line->offset;
662 if (x <= 0) { 714 if (x <= 0) {
@@ -665,7 +717,7 @@ static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const
665 const char *endPos; 717 const char *endPos;
666 tryAdvanceNoWrap_Text(d->font, range_String(&line->text), x, &endPos); 718 tryAdvanceNoWrap_Text(d->font, range_String(&line->text), x, &endPos);
667 if (endPos == constEnd_String(&line->text)) { 719 if (endPos == constEnd_String(&line->text)) {
668 index += length_String(&line->text); 720 index += line->len;
669 } 721 }
670 else { 722 else {
671 /* Need to know the actual character index. */ 723 /* Need to know the actual character index. */
@@ -675,6 +727,9 @@ static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const
675 index++; 727 index++;
676 } 728 }
677 } 729 }
730 if (!isLastLine_InputWidget_(d, line) && index == line->offset + line->len) {
731 index = iMax(index - 1, line->offset);
732 }
678 return index; 733 return index;
679} 734}
680 735
@@ -690,11 +745,6 @@ static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir) {
690 newCursor = indexForRelativeX_InputWidget_(d, xPos, ++line); 745 newCursor = indexForRelativeX_InputWidget_(d, xPos, ++line);
691 } 746 }
692 if (newCursor != iInvalidPos) { 747 if (newCursor != iInvalidPos) {
693 /* Clamp it to the current line. */
694 newCursor = iMin(newCursor, line->offset + length_String(&line->text) -
695 /* last line is allowed to go to the cursorMax */
696 ((const void *) line < constAt_Array(&d->lines, numLines - 1) ? 1 : 0));
697 newCursor = iMax(newCursor, line->offset);
698 setCursor_InputWidget(d, newCursor); 748 setCursor_InputWidget(d, newCursor);
699 return iTrue; 749 return iTrue;
700 } 750 }
@@ -707,6 +757,7 @@ void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) {
707 757
708void setUrlContent_InputWidget(iInputWidget *d, iBool isUrl) { 758void setUrlContent_InputWidget(iInputWidget *d, iBool isUrl) {
709 iChangeFlags(d->inFlags, isUrl_InputWidgetFlag, isUrl); 759 iChangeFlags(d->inFlags, isUrl_InputWidgetFlag, isUrl);
760 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
710} 761}
711 762
712void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) { 763void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) {
@@ -826,7 +877,8 @@ static iInt2 textOrigin_InputWidget_(const iInputWidget *d) { //}, const char *v
826 877
827static size_t coordIndex_InputWidget_(const iInputWidget *d, iInt2 coord) { 878static size_t coordIndex_InputWidget_(const iInputWidget *d, iInt2 coord) {
828 const iInt2 pos = sub_I2(coord, contentBounds_InputWidget_(d).pos); 879 const iInt2 pos = sub_I2(coord, contentBounds_InputWidget_(d).pos);
829 const size_t lineNumber = iMin(pos.y / lineHeight_Text(d->font), (int) size_Array(&d->lines) - 1); 880 const size_t lineNumber = iMin(iMax(0, pos.y) / lineHeight_Text(d->font),
881 (int) size_Array(&d->lines) - 1);
830 const iInputLine *line = line_InputWidget_(d, lineNumber); 882 const iInputLine *line = line_InputWidget_(d, lineNumber);
831 const char *endPos; 883 const char *endPos;
832 tryAdvanceNoWrap_Text(d->font, range_String(&line->text), pos.x, &endPos); 884 tryAdvanceNoWrap_Text(d->font, range_String(&line->text), pos.x, &endPos);
@@ -880,7 +932,7 @@ static iRanges lineRange_InputWidget_(const iInputWidget *d) {
880 return (iRanges){ 0, 0 }; 932 return (iRanges){ 0, 0 };
881 } 933 }
882 const iInputLine *line = line_InputWidget_(d, d->cursorLine); 934 const iInputLine *line = line_InputWidget_(d, d->cursorLine);
883 return (iRanges){ line->offset, line->offset + length_String(&line->text) }; 935 return (iRanges){ line->offset, line->offset + line->len };
884} 936}
885 937
886static void extendRange_InputWidget_(iInputWidget *d, size_t *pos, int dir) { 938static void extendRange_InputWidget_(iInputWidget *d, size_t *pos, int dir) {
@@ -907,6 +959,9 @@ static iRect bounds_InputWidget_(const iInputWidget *d) {
907 return bounds; 959 return bounds;
908 } 960 }
909 bounds.size.y = contentHeight_InputWidget_(d, iFalse) + 3 * padding_().y; 961 bounds.size.y = contentHeight_InputWidget_(d, iFalse) + 3 * padding_().y;
962 if (w->flags & extraPadding_WidgetFlag) {
963 bounds.size.y += 2 * gap_UI;
964 }
910 return bounds; 965 return bounds;
911} 966}
912 967
@@ -958,11 +1013,19 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
958 } 1013 }
959 return iFalse; 1014 return iFalse;
960 } 1015 }
1016 else if (isCommand_UserEvent(ev, "text.insert")) {
1017 pushUndo_InputWidget_(d);
1018 deleteMarked_InputWidget_(d);
1019 insertChar_InputWidget_(d, arg_Command(command_UserEvent(ev)));
1020 contentsWereChanged_InputWidget_(d);
1021 return iTrue;
1022 }
961 else if (isMetricsChange_UserEvent(ev)) { 1023 else if (isMetricsChange_UserEvent(ev)) {
962 updateMetrics_InputWidget_(d); 1024 updateMetrics_InputWidget_(d);
963 updateLinesAndResize_InputWidget_(d); 1025 updateLinesAndResize_InputWidget_(d);
964 } 1026 }
965 else if (isResize_UserEvent(ev)) { 1027 else if (isResize_UserEvent(ev) || d->lastUpdateWidth != w->rect.size.x) {
1028 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
966 if (d->inFlags & isUrl_InputWidgetFlag) { 1029 if (d->inFlags & isUrl_InputWidgetFlag) {
967 /* Restore/omit the default scheme if necessary. */ 1030 /* Restore/omit the default scheme if necessary. */
968 setText_InputWidget(d, text_InputWidget(d)); 1031 setText_InputWidget(d, text_InputWidget(d));
@@ -1077,6 +1140,17 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1077 return iTrue; 1140 return iTrue;
1078 } 1141 }
1079 } 1142 }
1143#if defined (iPlatformApple)
1144 if (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT)) {
1145 switch (key) {
1146 case SDLK_UP:
1147 case SDLK_DOWN:
1148 setCursor_InputWidget(d, key == SDLK_UP ? 0 : curMax);
1149 refresh_Widget(d);
1150 return iTrue;
1151 }
1152 }
1153#endif
1080 d->lastCursor = d->cursor; 1154 d->lastCursor = d->cursor;
1081 switch (key) { 1155 switch (key) {
1082 case SDLK_INSERT: 1156 case SDLK_INSERT:
@@ -1086,7 +1160,8 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1086 return iTrue; 1160 return iTrue;
1087 case SDLK_RETURN: 1161 case SDLK_RETURN:
1088 case SDLK_KP_ENTER: 1162 case SDLK_KP_ENTER:
1089 if (mods == KMOD_SHIFT) { 1163 if (mods == KMOD_SHIFT || (~d->inFlags & isUrl_InputWidgetFlag &&
1164 deviceType_App() != desktop_AppDeviceType)) {
1090 pushUndo_InputWidget_(d); 1165 pushUndo_InputWidget_(d);
1091 deleteMarked_InputWidget_(d); 1166 deleteMarked_InputWidget_(d);
1092 insertChar_InputWidget_(d, '\n'); 1167 insertChar_InputWidget_(d, '\n');
@@ -1170,7 +1245,12 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1170 break; 1245 break;
1171 case SDLK_HOME: 1246 case SDLK_HOME:
1172 case SDLK_END: 1247 case SDLK_END:
1173 setCursor_InputWidget(d, key == SDLK_HOME ? lineFirst : lineLast); 1248 if (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT)) {
1249 setCursor_InputWidget(d, key == SDLK_HOME ? 0 : curMax);
1250 }
1251 else {
1252 setCursor_InputWidget(d, key == SDLK_HOME ? lineFirst : lineLast);
1253 }
1174 refresh_Widget(w); 1254 refresh_Widget(w);
1175 return iTrue; 1255 return iTrue;
1176 case SDLK_a: 1256 case SDLK_a:
@@ -1216,19 +1296,20 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1216 /* Allow focus switching. */ 1296 /* Allow focus switching. */
1217 return processEvent_Widget(as_Widget(d), ev); 1297 return processEvent_Widget(as_Widget(d), ev);
1218 case SDLK_UP: 1298 case SDLK_UP:
1219 if (moveCursorByLine_InputWidget_(d, -1)) {
1220 refresh_Widget(d);
1221 return iTrue;
1222 }
1223 /* For moving to lookup from url entry. */
1224 return processEvent_Widget(as_Widget(d), ev);
1225 case SDLK_DOWN: 1299 case SDLK_DOWN:
1226 if (moveCursorByLine_InputWidget_(d, +1)) { 1300 if (moveCursorByLine_InputWidget_(d, key == SDLK_UP ? -1 : +1)) {
1227 refresh_Widget(d); 1301 refresh_Widget(d);
1228 return iTrue; 1302 return iTrue;
1229 } 1303 }
1230 /* For moving to lookup from url entry. */ 1304 /* For moving to lookup from url entry. */
1231 return processEvent_Widget(as_Widget(d), ev); 1305 return processEvent_Widget(as_Widget(d), ev);
1306 case SDLK_PAGEUP:
1307 case SDLK_PAGEDOWN:
1308 for (int count = 0; count < 5; count++) {
1309 moveCursorByLine_InputWidget_(d, key == SDLK_PAGEUP ? -1 : +1);
1310 }
1311 refresh_Widget(d);
1312 return iTrue;
1232 } 1313 }
1233 if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) { 1314 if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) {
1234 return iFalse; 1315 return iFalse;
@@ -1248,6 +1329,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1248 return processEvent_Widget(w, ev); 1329 return processEvent_Widget(w, ev);
1249} 1330}
1250 1331
1332#if 0
1251static iBool isWhite_(const iString *str) { 1333static iBool isWhite_(const iString *str) {
1252 iConstForEach(String, i, str) { 1334 iConstForEach(String, i, str) {
1253 if (!isSpace_Char(i.value)) { 1335 if (!isSpace_Char(i.value)) {
@@ -1256,11 +1338,12 @@ static iBool isWhite_(const iString *str) {
1256 } 1338 }
1257 return iTrue; 1339 return iTrue;
1258} 1340}
1341#endif
1259 1342
1260static void draw_InputWidget_(const iInputWidget *d) { 1343static void draw_InputWidget_(const iInputWidget *d) {
1261 const iWidget *w = constAs_Widget(d); 1344 const iWidget *w = constAs_Widget(d);
1262 iRect bounds = adjusted_Rect(bounds_InputWidget_(d), padding_(), neg_I2(padding_())); 1345 iRect bounds = adjusted_Rect(bounds_InputWidget_(d), padding_(), neg_I2(padding_()));
1263 iBool isHint = iFalse; 1346 iBool isHint = isHintVisible_InputWidget_(d);
1264 const iBool isFocused = isFocused_Widget(w); 1347 const iBool isFocused = isFocused_Widget(w);
1265 const iBool isHover = isHover_Widget(w) && 1348 const iBool isHover = isHover_Widget(w) &&
1266 contains_InputWidget_(d, mouseCoord_Window(get_Window())); 1349 contains_InputWidget_(d, mouseCoord_Window(get_Window()));
@@ -1283,40 +1366,52 @@ static void draw_InputWidget_(const iInputWidget *d) {
1283 isFocused ? gap_UI / 4 : 1, 1366 isFocused ? gap_UI / 4 : 1,
1284 isFocused ? uiInputFrameFocused_ColorId 1367 isFocused ? uiInputFrameFocused_ColorId
1285 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); 1368 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId);
1286 setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), init_I2(-d->rightPadding, 0))); 1369 setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0),
1370 init_I2(-d->rightPadding, w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0)));
1287 const iRect contentBounds = contentBounds_InputWidget_(d); 1371 const iRect contentBounds = contentBounds_InputWidget_(d);
1288// const iInt2 textOrigin = textOrigin_InputWidget_(d); //, cstr_String(text)); 1372// const iInt2 textOrigin = textOrigin_InputWidget_(d); //, cstr_String(text));
1289 iInt2 drawPos = topLeft_Rect(contentBounds); 1373 iInt2 drawPos = topLeft_Rect(contentBounds);
1290 const int fg = isHint ? uiAnnotation_ColorId 1374 const int fg = isHint ? uiAnnotation_ColorId
1291 : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId 1375 : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId
1292 : uiInputText_ColorId; 1376 : uiInputText_ColorId;
1293 /* TODO: If buffered, just draw the buffered copy. */ 1377 /* If buffered, just draw the buffered copy. */
1294 iConstForEach(Array, i, &d->lines) { 1378 if (d->buffered && !isFocused) { //&& !isFocused/* && !isHint*/) {
1295 const iInputLine *line = i.value; 1379 /* Most input widgets will use this, since only one is focused at a time. */
1296 const iBool isLast = index_ArrayConstIterator(&i) == size_Array(&d->lines) - 1; 1380 draw_TextBuf(d->buffered, topLeft_Rect(contentBounds), white_ColorId);
1297 const iInputLine *nextLine = isLast ? NULL : (line + 1); 1381 }
1298 const iRanges lineRange = { line->offset, 1382 else if (isHint) {
1299 nextLine ? nextLine->offset : size_Array(&d->text) }; 1383 drawRange_Text(d->font, topLeft_Rect(contentBounds), uiAnnotation_ColorId,
1300 if (isFocused && !isEmpty_Range(&d->mark)) { 1384 range_String(&d->hint));
1301 /* Draw the selected range. */ 1385 }
1302 const iRanges mark = mark_InputWidget_(d); 1386 else {
1303 if (mark.start < lineRange.end && mark.end > lineRange.start) { 1387 iConstForEach(Array, i, &d->lines) {
1304 const int m1 = advanceN_Text(d->font, 1388 const iInputLine *line = i.value;
1305 cstr_String(&line->text), 1389 const iBool isLast = index_ArrayConstIterator(&i) == size_Array(&d->lines) - 1;
1306 iMax(lineRange.start, mark.start) - line->offset) 1390 const iInputLine *nextLine = isLast ? NULL : (line + 1);
1307 .x; 1391 const iRanges lineRange = { line->offset,
1308 const int m2 = advanceN_Text(d->font, 1392 nextLine ? nextLine->offset : size_Array(&d->text) };
1309 cstr_String(&line->text), 1393 if (isFocused && !isEmpty_Range(&d->mark)) {
1310 iMin(lineRange.end, mark.end) - line->offset) 1394 /* Draw the selected range. */
1311 .x; 1395 const iRanges mark = mark_InputWidget_(d);
1312 fillRect_Paint(&p, 1396 if (mark.start < lineRange.end && mark.end > lineRange.start) {
1313 (iRect){ addX_I2(drawPos, iMin(m1, m2)), 1397 const int m1 = advanceN_Text(d->font,
1314 init_I2(iMax(gap_UI / 3, iAbs(m2 - m1)), lineHeight_Text(d->font)) }, 1398 cstr_String(&line->text),
1315 uiMarked_ColorId); 1399 iMax(lineRange.start, mark.start) - line->offset)
1400 .x;
1401 const int m2 = advanceN_Text(d->font,
1402 cstr_String(&line->text),
1403 iMin(lineRange.end, mark.end) - line->offset)
1404 .x;
1405 fillRect_Paint(&p,
1406 (iRect){ addX_I2(drawPos, iMin(m1, m2)),
1407 init_I2(iMax(gap_UI / 3, iAbs(m2 - m1)),
1408 lineHeight_Text(d->font)) },
1409 uiMarked_ColorId);
1410 }
1316 } 1411 }
1412 drawRange_Text(d->font, drawPos, fg, range_String(&line->text));
1413 drawPos.y += lineHeight_Text(d->font);
1317 } 1414 }
1318 drawRange_Text(d->font, drawPos, fg, range_String(&line->text));
1319 drawPos.y += lineHeight_Text(d->font);
1320 } 1415 }
1321// if (d->buffered && !isFocused && !isHint) { 1416// if (d->buffered && !isFocused && !isHint) {
1322// /* Most input widgets will use this, since only one is focused at a time. */ 1417// /* Most input widgets will use this, since only one is focused at a time. */
@@ -1378,13 +1473,7 @@ static void draw_InputWidget_(const iInputWidget *d) {
1378 drawChildren_Widget(w); 1473 drawChildren_Widget(w);
1379} 1474}
1380 1475
1381//static void sizeChanged_InputWidget_(iInputWidget *d) {
1382// printf("[InputWidget] %p: size changed, updating layout\n", d);
1383// updateLinesAndResize_InputWidget_(d, iFalse);
1384//}
1385
1386iBeginDefineSubclass(InputWidget, Widget) 1476iBeginDefineSubclass(InputWidget, Widget)
1387 .processEvent = (iAny *) processEvent_InputWidget_, 1477 .processEvent = (iAny *) processEvent_InputWidget_,
1388 .draw = (iAny *) draw_InputWidget_, 1478 .draw = (iAny *) draw_InputWidget_,
1389// .sizeChanged = (iAny *) sizeChanged_InputWidget_,
1390iEndDefineSubclass(InputWidget) 1479iEndDefineSubclass(InputWidget)
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index a7aa6391..a940b0cb 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -28,6 +28,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
28#include "app.h" 28#include "app.h"
29#include "util.h" 29#include "util.h"
30#include "keys.h" 30#include "keys.h"
31#include "touch.h"
31 32
32struct Impl_LabelWidget { 33struct Impl_LabelWidget {
33 iWidget widget; 34 iWidget widget;
@@ -46,6 +47,15 @@ struct Impl_LabelWidget {
46 } flags; 47 } flags;
47}; 48};
48 49
50static iBool isHover_LabelWidget_(const iLabelWidget *d) {
51#if defined (iPlatformMobile)
52 if (!isHovering_Touch()) {
53 return iFalse;
54 }
55#endif
56 return isHover_Widget(d);
57}
58
49static iInt2 padding_LabelWidget_(const iLabelWidget *d, int corner) { 59static iInt2 padding_LabelWidget_(const iLabelWidget *d, int corner) {
50 const iWidget *w = constAs_Widget(d); 60 const iWidget *w = constAs_Widget(d);
51 const int64_t flags = flags_Widget(w); 61 const int64_t flags = flags_Widget(w);
@@ -199,7 +209,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
199 if (startsWith_String(&d->label, "\v")) { 209 if (startsWith_String(&d->label, "\v")) {
200 colorEscape = cstr_String(&d->label)[1] - asciiBase_ColorEscape; /* TODO: can be two bytes long */ 210 colorEscape = cstr_String(&d->label)[1] - asciiBase_ColorEscape; /* TODO: can be two bytes long */
201 } 211 }
202 if (isHover_Widget(w)) { 212 if (isHover_LabelWidget_(d)) {
203 if (isFrameless) { 213 if (isFrameless) {
204 *bg = uiBackgroundFramelessHover_ColorId; 214 *bg = uiBackgroundFramelessHover_ColorId;
205 *fg = uiTextFramelessHover_ColorId; 215 *fg = uiTextFramelessHover_ColorId;
@@ -257,7 +267,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
257 const int64_t flags = flags_Widget(w); 267 const int64_t flags = flags_Widget(w);
258 const iRect bounds = bounds_Widget(w); 268 const iRect bounds = bounds_Widget(w);
259 iRect rect = bounds; 269 iRect rect = bounds;
260 const iBool isHover = isHover_Widget(w); 270 const iBool isHover = isHover_LabelWidget_(d);
261 if (isButton) { 271 if (isButton) {
262 shrink_Rect(&rect, divi_I2(gap2_UI, 4)); 272 shrink_Rect(&rect, divi_I2(gap2_UI, 4));
263 adjustEdges_Rect(&rect, gap_UI / 8, 0, -gap_UI / 8, 0); 273 adjustEdges_Rect(&rect, gap_UI / 8, 0, -gap_UI / 8, 0);
diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c
index 95be44a0..a3406d48 100644
--- a/src/ui/listwidget.c
+++ b/src/ui/listwidget.c
@@ -414,7 +414,7 @@ static void draw_ListWidget_(const iListWidget *d) {
414 init_I2(blankWidth, d->itemHeight) }; 414 init_I2(blankWidth, d->itemHeight) };
415 iConstForEach(IntSet, v, &d->invalidItems) { 415 iConstForEach(IntSet, v, &d->invalidItems) {
416 const size_t index = *v.value; 416 const size_t index = *v.value;
417 if (contains_Range(&drawItems, index)) { 417 if (contains_Range(&drawItems, index) && index < size_PtrArray(&d->items)) {
418 const iListItem *item = constAt_PtrArray(&d->items, index); 418 const iListItem *item = constAt_PtrArray(&d->items, index);
419 const iRect itemRect = { init_I2(0, index * d->itemHeight - buf->origin), 419 const iRect itemRect = { init_I2(0, index * d->itemHeight - buf->origin),
420 init_I2(d->visBuf->texSize.x, d->itemHeight) }; 420 init_I2(d->visBuf->texSize.x, d->itemHeight) };
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index b06523f9..3eafd4bd 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -662,7 +662,7 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) {
662 setPos_Widget(w, windowToLocal_Widget(w, bottomLeft_Rect(bounds_Widget(url)))); 662 setPos_Widget(w, windowToLocal_Widget(w, bottomLeft_Rect(bounds_Widget(url))));
663#if defined (iPlatformAppleMobile) 663#if defined (iPlatformAppleMobile)
664 /* Adjust height based on keyboard size. */ { 664 /* Adjust height based on keyboard size. */ {
665 w->rect.size.y = visibleRootSize_Window(window).y - top_Rect(bounds_Widget(w)); 665 w->rect.size.y = visibleSize_Root(root).y - top_Rect(bounds_Widget(w));
666 if (deviceType_App() == phone_AppDeviceType) { 666 if (deviceType_App() == phone_AppDeviceType) {
667 float l, r; 667 float l, r;
668 safeAreaInsets_iOS(&l, NULL, &r, NULL); 668 safeAreaInsets_iOS(&l, NULL, &r, NULL);
diff --git a/src/ui/mobile.c b/src/ui/mobile.c
new file mode 100644
index 00000000..1f5e9758
--- /dev/null
+++ b/src/ui/mobile.c
@@ -0,0 +1,795 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#include "mobile.h"
24
25#include "app.h"
26#include "command.h"
27#include "defs.h"
28#include "inputwidget.h"
29#include "labelwidget.h"
30#include "root.h"
31#include "text.h"
32#include "widget.h"
33#include "window.h"
34
35#if defined (iPlatformAppleMobile)
36# include "ios.h"
37#endif
38
39static iBool useMobileSheetLayout_(void) {
40 return deviceType_App() != desktop_AppDeviceType;
41}
42
43static iBool isSideBySideLayout_(void) {
44 if (deviceType_App() == phone_AppDeviceType) {
45 return isLandscape_App();
46 }
47 return numRoots_Window(get_Window()) == 1;
48}
49
50static enum iFontId labelFont_(void) {
51 return deviceType_App() == phone_AppDeviceType ? defaultBig_FontId : defaultMedium_FontId;
52}
53
54static enum iFontId labelBoldFont_(void) {
55 return deviceType_App() == phone_AppDeviceType ? defaultBigBold_FontId : defaultMediumBold_FontId;
56}
57
58static void updatePanelSheetMetrics_(iWidget *sheet) {
59 iWidget *navi = findChild_Widget(sheet, "panel.navi");
60 iWidget *naviPad = child_Widget(navi, 0);
61 int naviHeight = lineHeight_Text(labelFont_()) + 4 * gap_UI;
62#if defined (iPlatformAppleMobile)
63 float left, right, top, bottom;
64 safeAreaInsets_iOS(&left, &top, &right, &bottom);
65 setPadding_Widget(sheet, left, 0, right, 0);
66 navi->rect.pos = init_I2(left, top);
67 iConstForEach(PtrArray, i, findChildren_Widget(sheet, "panel.toppad")) {
68 iWidget *pad = *i.value;
69 setFixedSize_Widget(pad, init1_I2(naviHeight));
70 }
71#endif
72 setFixedSize_Widget(navi, init_I2(-1, naviHeight));
73}
74
75static iWidget *findDetailStack_(iWidget *topPanel) {
76 return findChild_Widget(parent_Widget(topPanel), "detailstack");
77}
78
79static void unselectAllPanelButtons_(iWidget *topPanel) {
80 iForEach(ObjectList, i, children_Widget(topPanel)) {
81 if (isInstance_Object(i.object, &Class_LabelWidget)) {
82 iLabelWidget *label = i.object;
83 if (!cmp_String(command_LabelWidget(label), "panel.open")) {
84 setFlags_Widget(i.object, selected_WidgetFlag, iFalse);
85 }
86 }
87 }
88}
89
90static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) {
91 if (equal_Command(cmd, "window.resized")) {
92 const iBool isPortrait = (deviceType_App() == phone_AppDeviceType && isPortrait_App());
93 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);
97 iWidget * navi = findChild_Widget(sheet, "panel.navi");
98 iWidget * detailStack = findChild_Widget(mainDetailSplit, "detailstack");
99 const size_t numPanels = childCount_Widget(detailStack);
100 const iBool isSideBySide = isSideBySideLayout_() && numPanels > 0;
101 setFlags_Widget(mainDetailSplit, arrangeHorizontal_WidgetFlag, isSideBySide);
102 setFlags_Widget(detailStack, expand_WidgetFlag, isSideBySide);
103 setFlags_Widget(detailStack, hidden_WidgetFlag, numPanels == 0);
104 iWidget *topPanel = findChild_Widget(mainDetailSplit, "panel.top");
105 const int pad = isPortrait ? 0 : 3 * gap_UI;
106 if (isSideBySide) {
107 iAssert(topPanel);
108 topPanel->rect.size.x = (deviceType_App() == phone_AppDeviceType ?
109 safeRoot.size.x * 2 / 5 : (safeRoot.size.x / 3));
110 }
111 if (deviceType_App() == tablet_AppDeviceType) {
112 setPadding_Widget(topPanel, pad, 0, pad, pad);
113 if (numPanels == 0) {
114 setFlags_Widget(sheet, centerHorizontal_WidgetFlag, iTrue);
115 const int sheetWidth = iMin(safeRoot.size.x, safeRoot.size.y);
116 mainDetailSplit->rect.size.x = sheetWidth;
117 setFixedSize_Widget(sheet, init_I2(sheetWidth, -1));
118 setFixedSize_Widget(navi, init_I2(sheetWidth, -1));
119 }
120 }
121 iForEach(ObjectList, i, children_Widget(detailStack)) {
122 iWidget *panel = i.object;
123 setFlags_Widget(panel, edgeDraggable_WidgetFlag, !isSideBySide);
124 if (isSideBySide) {
125 setVisualOffset_Widget(panel, 0, 0, 0);
126 }
127 setPadding_Widget(panel, pad, 0, pad, pad);
128 }
129 arrange_Widget(mainDetailSplit);
130 }
131 return iFalse;
132}
133
134static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) {
135 const iBool isPortrait = !isSideBySideLayout_();
136 if (equal_Command(cmd, "panel.open")) {
137 iWidget *button = pointer_Command(cmd);
138 iWidget *panel = userData_Object(button);
139// openMenu_Widget(panel, innerToWindow_Widget(panel, zero_I2()));
140// setFlags_Widget(panel, hidden_WidgetFlag, iFalse);
141 unselectAllPanelButtons_(topPanel);
142 iForEach(ObjectList, i, children_Widget(findDetailStack_(topPanel))) {
143 iWidget *child = i.object;
144 setFlags_Widget(child, hidden_WidgetFlag | disabled_WidgetFlag, child != panel);
145 /* Animate the current panel in. */
146 if (child == panel && isPortrait) {
147 setupSheetTransition_Mobile(panel, iTrue);
148 }
149 }
150 setFlags_Widget(button, selected_WidgetFlag, iTrue);
151 return iTrue;
152 }
153 if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd) &&
154 argLabel_Command(cmd, "button") == SDL_BUTTON_X1) {
155 postCommand_App("panel.close");
156 return iTrue;
157 }
158 if (equal_Command(cmd, "panel.close")) {
159 iBool wasClosed = iFalse;
160 if (isPortrait) {
161 iForEach(ObjectList, i, children_Widget(findDetailStack_(topPanel))) {
162 iWidget *child = i.object;
163 if (!cmp_String(id_Widget(child), "panel") && isVisible_Widget(child)) {
164 // closeMenu_Widget(child);
165 setupSheetTransition_Mobile(child, iFalse);
166 setFlags_Widget(child, hidden_WidgetFlag | disabled_WidgetFlag, iTrue);
167 setFocus_Widget(NULL);
168 updateTextCStr_LabelWidget(findWidget_App("panel.back"), "Back");
169 wasClosed = iTrue;
170 }
171 }
172 }
173 unselectAllPanelButtons_(topPanel);
174 if (!wasClosed) {
175 postCommand_App("prefs.dismiss");
176 }
177 return iTrue;
178 }
179 if (equal_Command(cmd, "document.changed")) {
180 postCommand_App("prefs.dismiss");
181 return iFalse;
182 }
183 if (equal_Command(cmd, "window.resized")) {
184 // sheet > mdsplit > panel.top
185 updatePanelSheetMetrics_(parent_Widget(parent_Widget(topPanel)));
186 }
187 return iFalse;
188}
189
190static iBool isTwoColumnPage_(iWidget *d) {
191 if (cmp_String(id_Widget(d), "dialogbuttons") == 0 ||
192 cmp_String(id_Widget(d), "prefs.tabs") == 0) {
193 return iFalse;
194 }
195 if (class_Widget(d) == &Class_Widget && childCount_Widget(d) == 2) {
196 return class_Widget(child_Widget(d, 0)) == &Class_Widget &&
197 class_Widget(child_Widget(d, 1)) == &Class_Widget;
198 }
199 return iFalse;
200}
201
202static iBool isOmittedPref_(const iString *id) {
203 static const char *omittedPrefs[] = {
204 "prefs.smoothscroll",
205 "prefs.imageloadscroll",
206 "prefs.pinsplit",
207 "prefs.retainwindow",
208 "prefs.ca.file",
209 "prefs.ca.path",
210 };
211 iForIndices(i, omittedPrefs) {
212 if (cmp_String(id, omittedPrefs[i]) == 0) {
213 return iTrue;
214 }
215 }
216 return iFalse;
217}
218
219enum iPrefsElement {
220 panelTitle_PrefsElement,
221 heading_PrefsElement,
222 toggle_PrefsElement,
223 dropdown_PrefsElement,
224 radioButton_PrefsElement,
225 textInput_PrefsElement,
226};
227
228static iAnyObject *addPanelChild_(iWidget *panel, iAnyObject *child, int64_t flags,
229 enum iPrefsElement elementType,
230 enum iPrefsElement precedingElementType) {
231 /* Erase redundant/unused headings. */
232 if (precedingElementType == heading_PrefsElement &&
233 (!child || (elementType == heading_PrefsElement || elementType == radioButton_PrefsElement))) {
234 iRelease(removeChild_Widget(panel, lastChild_Widget(panel)));
235 if (!cmp_String(id_Widget(constAs_Widget(lastChild_Widget(panel))), "padding")) {
236 iRelease(removeChild_Widget(panel, lastChild_Widget(panel)));
237 }
238 }
239 if (child) {
240 /* Insert padding between different element types. */
241 if (precedingElementType != panelTitle_PrefsElement) {
242 if (elementType == heading_PrefsElement ||
243 (elementType == toggle_PrefsElement &&
244 precedingElementType != toggle_PrefsElement &&
245 precedingElementType != heading_PrefsElement) ||
246 (elementType == dropdown_PrefsElement &&
247 precedingElementType != dropdown_PrefsElement &&
248 precedingElementType != heading_PrefsElement) ||
249 (elementType == textInput_PrefsElement &&
250 precedingElementType != textInput_PrefsElement &&
251 precedingElementType != heading_PrefsElement)) {
252 addChild_Widget(panel, iClob(makePadding_Widget(lineHeight_Text(labelFont_()))));
253 }
254 }
255 if ((elementType == toggle_PrefsElement && precedingElementType != toggle_PrefsElement) ||
256 (elementType == textInput_PrefsElement && precedingElementType != textInput_PrefsElement)) {
257 flags |= borderTop_WidgetFlag;
258 }
259 return addChildFlags_Widget(panel, child, flags);
260 }
261 return NULL;
262}
263
264static void stripTrailingColon_(iLabelWidget *label) {
265 const iString *text = text_LabelWidget(label);
266 if (endsWith_String(text, ":")) {
267 iString *mod = copy_String(text);
268 removeEnd_String(mod, 1);
269 updateText_LabelWidget(label, mod);
270 delete_String(mod);
271 }
272}
273
274static iLabelWidget *makePanelButton_(const char *text, const char *command) {
275 iLabelWidget *btn = new_LabelWidget(text, command);
276 setFlags_Widget(as_Widget(btn),
277 borderBottom_WidgetFlag | alignLeft_WidgetFlag |
278 frameless_WidgetFlag | extraPadding_WidgetFlag,
279 iTrue);
280 checkIcon_LabelWidget(btn);
281 setFont_LabelWidget(btn, labelFont_());
282 setTextColor_LabelWidget(btn, uiTextStrong_ColorId);
283 setBackgroundColor_Widget(as_Widget(btn), uiBackgroundSidebar_ColorId);
284 return btn;
285}
286
287static iWidget *makeValuePadding_(iWidget *value) {
288 iInputWidget *input = isInstance_Object(value, &Class_InputWidget) ? (iInputWidget *) value : NULL;
289 if (input) {
290 setFont_InputWidget(input, labelFont_());
291 setContentPadding_InputWidget(input, 3 * gap_UI, 3 * gap_UI);
292 }
293 iWidget *pad = new_Widget();
294 setBackgroundColor_Widget(pad, uiBackgroundSidebar_ColorId);
295 setPadding_Widget(pad, 0, 1 * gap_UI, 0, 1 * gap_UI);
296 addChild_Widget(pad, iClob(value));
297 setFlags_Widget(pad,
298 borderBottom_WidgetFlag |
299 arrangeVertical_WidgetFlag |
300 resizeToParentWidth_WidgetFlag |
301 resizeWidthOfChildren_WidgetFlag |
302 arrangeHeight_WidgetFlag,
303 iTrue);
304 return pad;
305}
306
307static iWidget *makeValuePaddingWithHeading_(iLabelWidget *heading, iWidget *value) {
308 iWidget *div = new_Widget();
309 setFlags_Widget(div,
310 borderBottom_WidgetFlag | arrangeHeight_WidgetFlag |
311 resizeWidthOfChildren_WidgetFlag |
312 arrangeHorizontal_WidgetFlag, iTrue);
313 setBackgroundColor_Widget(div, uiBackgroundSidebar_ColorId);
314 setPadding_Widget(div, gap_UI, gap_UI, 4 * gap_UI, gap_UI);
315 addChildFlags_Widget(div, iClob(heading), 0);
316 //setFixedSize_Widget(as_Widget(heading), init_I2(-1, height_Widget(value)));
317 setFont_LabelWidget(heading, labelFont_());
318 setTextColor_LabelWidget(heading, uiTextStrong_ColorId);
319 if (isInstance_Object(value, &Class_InputWidget)) {
320 addChildFlags_Widget(div, iClob(value), expand_WidgetFlag);
321 }
322 else if (isInstance_Object(value, &Class_LabelWidget) &&
323 cmp_String(command_LabelWidget((iLabelWidget *) value), "toggle")) {
324 addChildFlags_Widget(div, iClob(value), expand_WidgetFlag);
325 /* TODO: This doesn't work? */
326// setCommand_LabelWidget(heading,
327// collectNewFormat_String("!%s ptr:%p",
328// cstr_String(command_LabelWidget((iLabelWidget *) value)),
329// value));
330 }
331 else {
332 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag);
333 addChild_Widget(div, iClob(value));
334 }
335 return div;
336}
337
338static iWidget *addChildPanel_(iWidget *parent, iLabelWidget *panelButton,
339 const iString *titleText) {
340 iWidget *panel = new_Widget();
341 setId_Widget(panel, "panel");
342 setUserData_Object(panelButton, panel);
343 setBackgroundColor_Widget(panel, uiBackground_ColorId);
344 setId_Widget(addChild_Widget(panel, iClob(makePadding_Widget(0))), "panel.toppad");
345 if (titleText) {
346 iLabelWidget *title =
347 addChildFlags_Widget(panel,
348 iClob(new_LabelWidget(cstr_String(titleText), NULL)),
349 alignLeft_WidgetFlag | frameless_WidgetFlag);
350 setFont_LabelWidget(title, uiLabelLargeBold_FontId);
351 setTextColor_LabelWidget(title, uiHeading_ColorId);
352 }
353 addChildFlags_Widget(parent,
354 iClob(panel),
355 focusRoot_WidgetFlag | hidden_WidgetFlag | disabled_WidgetFlag |
356 arrangeVertical_WidgetFlag | resizeWidthOfChildren_WidgetFlag |
357 arrangeHeight_WidgetFlag | overflowScrollable_WidgetFlag |
358 drawBackgroundToBottom_WidgetFlag |
359 horizontalOffset_WidgetFlag | commandOnClick_WidgetFlag);
360 return panel;
361}
362
363void finalizeSheet_Mobile(iWidget *sheet) {
364 /* The sheet contents are completely rearranged and restyled on a phone.
365 We'll set up a linear fullscreen arrangement of the widgets. Sheets are already
366 scrollable so they can be taller than the display. In hindsight, it may have been
367 easier to create phone versions of each dialog, but at least this works with any
368 future changes to the UI (..."works"). At least this way it is possible to enforce
369 a consistent styling. */
370 if (useMobileSheetLayout_() && parent_Widget(sheet) == root_Widget(sheet)) {
371 if (~flags_Widget(sheet) & keepOnTop_WidgetFlag) {
372 /* Already finalized. */
373 arrange_Widget(sheet);
374 postRefresh_App();
375 return;
376 }
377 /* Landscape Layout    Portrait Layout
378   
379 ┌─────────┬──────Detail─Stack─────┐    ┌─────────┬ ─ ─ ─ ─ ┐
380 │ │┌───────────────────┐ │    │ │Detail
381 │ ││┌──────────────────┴┐ │    │ │Stack │
382 │ │││┌──────────────────┴┐│    │ │┌──────┐
383 │ ││││ ││    │ ││┌─────┴┐│
384 │ ││││ ││    │ │││ │
385 │Top Panel││││ ││    │Top Panel│││ ││
386 │ ││││ Panels ││    │ │││Panels│
387 │ ││││ ││    │ │││ ││
388 │ │└┤│ ││    │ │││ │
389 │ │ └┤ ││    │ │└┤ ││
390 │ │ └───────────────────┘│    │ │ └──────┘
391 └─────────┴───────────────────────┘    └─────────┴ ─ ─ ─ ─ ┘
392    offscreen
393 */
394 /* Modify the top sheet to act as a fullscreen background. */
395 setPadding1_Widget(sheet, 0);
396 setBackgroundColor_Widget(sheet, uiBackground_ColorId);
397 setFlags_Widget(sheet,
398 keepOnTop_WidgetFlag |
399 parentCannotResize_WidgetFlag |
400 arrangeSize_WidgetFlag |
401 centerHorizontal_WidgetFlag |
402 arrangeVertical_WidgetFlag |
403 arrangeHorizontal_WidgetFlag |
404 overflowScrollable_WidgetFlag,
405 iFalse);
406 setFlags_Widget(sheet,
407 frameless_WidgetFlag |
408 //resizeWidthOfChildren_WidgetFlag |
409 edgeDraggable_WidgetFlag |
410 commandOnClick_WidgetFlag,
411 iTrue);
412 iPtrArray * contents = collect_PtrArray(new_PtrArray()); /* two-column pages */
413 iPtrArray * panelButtons = collect_PtrArray(new_PtrArray());
414 iWidget * prefsTabs = findChild_Widget(sheet, "prefs.tabs");
415 iWidget * dialogHeading = (prefsTabs ? NULL : child_Widget(sheet, 0));
416 const iBool isPrefs = (prefsTabs != NULL);
417 const int64_t panelButtonFlags = borderBottom_WidgetFlag | alignLeft_WidgetFlag |
418 frameless_WidgetFlag | extraPadding_WidgetFlag;
419 iWidget *mainDetailSplit = makeHDiv_Widget();
420 setCommandHandler_Widget(mainDetailSplit, mainDetailSplitHandler_);
421 setFlags_Widget(mainDetailSplit, resizeHeightOfChildren_WidgetFlag, iFalse);
422 setId_Widget(mainDetailSplit, "mdsplit");
423 iWidget *topPanel = new_Widget(); {
424 setId_Widget(topPanel, "panel.top");
425 setCommandHandler_Widget(topPanel, topPanelHandler_);
426 setFlags_Widget(topPanel,
427 arrangeVertical_WidgetFlag |
428 resizeWidthOfChildren_WidgetFlag |
429 arrangeHeight_WidgetFlag |
430 overflowScrollable_WidgetFlag |
431 commandOnClick_WidgetFlag,
432 iTrue);
433 addChild_Widget(mainDetailSplit, iClob(topPanel));
434 }
435 iWidget *detailStack = new_Widget(); {
436 setId_Widget(detailStack, "detailstack");
437 setFlags_Widget(detailStack, collapse_WidgetFlag | resizeWidthOfChildren_WidgetFlag, iTrue);
438 addChild_Widget(mainDetailSplit, iClob(detailStack));
439 }
440 addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(labelFont_()))));
441 /* Slide top panel with detail panels. */ {
442 setFlags_Widget(topPanel, refChildrenOffset_WidgetFlag, iTrue);
443 topPanel->offsetRef = detailStack;
444 }
445 if (prefsTabs) {
446 iRelease(removeChild_Widget(sheet, child_Widget(sheet, 0))); /* heading */
447 iRelease(removeChild_Widget(sheet, findChild_Widget(sheet, "dialogbuttons")));
448 /* Pull out the pages and make them panels. */
449 iWidget *pages = findChild_Widget(prefsTabs, "tabs.pages");
450 size_t pageCount = tabCount_Widget(prefsTabs);
451 for (size_t i = 0; i < pageCount; i++) {
452 iString *text = copy_String(text_LabelWidget(tabPageButton_Widget(prefsTabs, tabPage_Widget(prefsTabs, 0))));
453 iWidget *page = removeTabPage_Widget(prefsTabs, 0);
454 iWidget *pageContent = child_Widget(page, 1); /* surrounded by padding widgets */
455 pushBack_PtrArray(contents, ref_Object(pageContent));
456 iLabelWidget *panelButton;
457 pushBack_PtrArray(panelButtons,
458 addChildFlags_Widget(topPanel,
459 iClob(panelButton = makePanelButton_(
460 i == 1 ? "${heading.prefs.userinterface}" : cstr_String(text),
461 "panel.open")),
462 (i == 0 ? borderTop_WidgetFlag : 0) |
463 chevron_WidgetFlag));
464 const iChar icons[] = {
465 0x02699, /* gear */
466 0x1f4f1, /* mobile phone */
467 0x1f3a8, /* palette */
468 0x1f523,
469 0x1f5a7, /* computer network */
470 };
471 setIcon_LabelWidget(panelButton, icons[i]);
472// setFont_LabelWidget(panelButton, labelFont_());
473// setBackgroundColor_Widget(as_Widget(panelButton), uiBackgroundSidebar_ColorId);
474 iRelease(page);
475 delete_String(text);
476 }
477 destroy_Widget(prefsTabs);
478 }
479 iForEach(ObjectList, i, children_Widget(sheet)) {
480 iWidget *child = i.object;
481 if (isTwoColumnPage_(child)) {
482 pushBack_PtrArray(contents, removeChild_Widget(sheet, child));
483 }
484 else {
485 removeChild_Widget(sheet, child);
486 addChild_Widget(topPanel, child);
487 iRelease(child);
488 }
489 }
490 const iBool useSlidePanels = (size_PtrArray(contents) == size_PtrArray(panelButtons));
491 addChild_Widget(sheet, iClob(mainDetailSplit));
492 iForEach(PtrArray, j, contents) {
493 iWidget *owner = topPanel;
494 if (useSlidePanels) {
495 /* Create a new child panel. */
496 iLabelWidget *button = at_PtrArray(panelButtons, index_PtrArrayIterator(&j));
497 owner = addChildPanel_(detailStack, button,
498 collect_String(upper_String(text_LabelWidget(button))));
499 }
500 iWidget *pageContent = j.ptr;
501 iWidget *headings = child_Widget(pageContent, 0);
502 iWidget *values = child_Widget(pageContent, 1);
503 enum iPrefsElement prevElement = panelTitle_PrefsElement;
504 /* Identify the types of controls in the dialog and restyle/organize them. */
505 while (!isEmpty_ObjectList(children_Widget(headings))) {
506 iWidget *heading = child_Widget(headings, 0);
507 iWidget *value = child_Widget(values, 0);
508 removeChild_Widget(headings, heading);
509 removeChild_Widget(values, value);
510 /* Can we ignore these widgets? */
511 if (isOmittedPref_(id_Widget(value)) ||
512 (class_Widget(heading) == &Class_Widget &&
513 class_Widget(value) == &Class_Widget) /* just padding */) {
514 iRelease(heading);
515 iRelease(value);
516 continue;
517 }
518 enum iPrefsElement element = toggle_PrefsElement;
519 iLabelWidget *headingLabel = NULL;
520 iLabelWidget *valueLabel = NULL;
521 iInputWidget *valueInput = NULL;
522 const iBool isMenuButton = findChild_Widget(value, "menu") != NULL;
523 if (isInstance_Object(heading, &Class_LabelWidget)) {
524 headingLabel = (iLabelWidget *) heading;
525 stripTrailingColon_(headingLabel);
526 }
527 if (isInstance_Object(value, &Class_LabelWidget)) {
528 valueLabel = (iLabelWidget *) value;
529 setFont_LabelWidget(valueLabel, labelFont_());
530 }
531 if (isInstance_Object(value, &Class_InputWidget)) {
532 valueInput = (iInputWidget *) value;
533 setFlags_Widget(value, borderBottom_WidgetFlag, iFalse);
534 element = textInput_PrefsElement;
535 }
536 if (childCount_Widget(value) >= 2) {
537 if (isInstance_Object(child_Widget(value, 0), &Class_InputWidget)) {
538 element = textInput_PrefsElement;
539 setPadding_Widget(value, 0, 0, gap_UI, 0);
540 valueInput = child_Widget(value, 0);
541 }
542 }
543 if (valueInput) {
544 setFont_InputWidget(valueInput, labelFont_());
545 setContentPadding_InputWidget(valueInput, 3 * gap_UI, 0);
546 }
547 /* Toggles have the button on the right. */
548 if (valueLabel && cmp_String(command_LabelWidget(valueLabel), "toggle") == 0) {
549 element = toggle_PrefsElement;
550 addPanelChild_(owner,
551 iClob(makeValuePaddingWithHeading_(headingLabel, value)),
552 0,
553 element,
554 prevElement);
555 }
556 else if (valueLabel && isEmpty_String(text_LabelWidget(valueLabel))) {
557 element = heading_PrefsElement;
558 iRelease(value);
559 addPanelChild_(owner, iClob(heading), 0, element, prevElement);
560 setFont_LabelWidget(headingLabel, uiLabel_FontId);
561 }
562 else if (isMenuButton) {
563 element = dropdown_PrefsElement;
564 setFlags_Widget(value,
565 alignRight_WidgetFlag | noBackground_WidgetFlag |
566 frameless_WidgetFlag, iTrue);
567 setFlags_Widget(value, alignLeft_WidgetFlag, iFalse);
568 iWidget *pad = addPanelChild_(owner, iClob(makeValuePaddingWithHeading_(headingLabel, value)), 0,
569 element, prevElement);
570 pad->padding[2] = gap_UI;
571 }
572 else if (valueInput) {
573 addPanelChild_(owner, iClob(makeValuePaddingWithHeading_(headingLabel, value)), 0,
574 element, prevElement);
575 }
576 else {
577 if (childCount_Widget(value) >= 2) {
578 element = radioButton_PrefsElement;
579 /* Always padding before radio buttons. */
580 addChild_Widget(owner, iClob(makePadding_Widget(lineHeight_Text(labelFont_()))));
581 }
582 addChildFlags_Widget(owner, iClob(heading), borderBottom_WidgetFlag);
583 if (headingLabel) {
584 setTextColor_LabelWidget(headingLabel, uiSubheading_ColorId);
585 setText_LabelWidget(headingLabel,
586 collect_String(upper_String(text_LabelWidget(headingLabel))));
587 }
588 addPanelChild_(owner, iClob(value), 0, element, prevElement);
589 /* Radio buttons expand to fill the space. */
590 if (element == radioButton_PrefsElement) {
591 setBackgroundColor_Widget(value, uiBackgroundSidebar_ColorId);
592 setPadding_Widget(value, 4 * gap_UI, 2 * gap_UI, 4 * gap_UI, 2 * gap_UI);
593 setFlags_Widget(value, arrangeWidth_WidgetFlag, iFalse);
594 setFlags_Widget(value,
595 borderBottom_WidgetFlag |
596 resizeToParentWidth_WidgetFlag |
597 resizeWidthOfChildren_WidgetFlag,
598 iTrue);
599 iForEach(ObjectList, sub, children_Widget(value)) {
600 if (isInstance_Object(sub.object, &Class_LabelWidget)) {
601 iLabelWidget *opt = sub.object;
602 setFont_LabelWidget(opt, defaultMedium_FontId);
603 setFlags_Widget(as_Widget(opt), noBackground_WidgetFlag, iTrue);
604 }
605 }
606 }
607 }
608 prevElement = element;
609 }
610 addPanelChild_(owner, NULL, 0, 0, prevElement);
611 destroy_Widget(pageContent);
612 }
613 destroyPending_Root(sheet->root);
614 /* Additional elements for preferences. */
615 if (isPrefs) {
616 addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(labelFont_()))));
617 iLabelWidget *aboutButton = addChildFlags_Widget(topPanel,
618 iClob(makePanelButton_(planet_Icon " ${menu.about}", "panel.open")),
619 chevron_WidgetFlag);
620 addChildFlags_Widget(topPanel,
621 iClob(makePanelButton_(info_Icon " ${menu.help}", "!open url:about:help")),
622 borderTop_WidgetFlag);
623 /* The About panel. */ {
624 iWidget *panel = addChildPanel_(detailStack, aboutButton, NULL);
625 iString *msg = collectNew_String();
626 setCStr_String(msg, "Lagrange " LAGRANGE_APP_VERSION);
627#if defined (iPlatformAppleMobile)
628 appendCStr_String(msg, " (" LAGRANGE_IOS_VERSION ")");
629#endif
630 addChildFlags_Widget(panel, iClob(new_LabelWidget(cstr_String(msg), NULL)),
631 frameless_WidgetFlag);
632 addChildFlags_Widget(panel,
633 iClob(makePanelButton_(globe_Icon " By @jk@skyjake.fi",
634 "!open url:https://skyjake.fi/@jk")),
635 borderTop_WidgetFlag);
636 addChildFlags_Widget(panel,
637 iClob(makePanelButton_(clock_Icon " ${menu.releasenotes}",
638 "!open url:about:version")),
639 0);
640 addChildFlags_Widget(panel,
641 iClob(makePanelButton_(info_Icon " ${menu.aboutpages}",
642 "!open url:about:about")),
643 0);
644 addChildFlags_Widget(panel,
645 iClob(makePanelButton_(bug_Icon " ${menu.debug}",
646 "!open url:about:debug")),
647 0);
648 }
649 }
650 else {
651 setFlags_Widget(topPanel, overflowScrollable_WidgetFlag, iTrue);
652 /* Update heading style. */
653 setFont_LabelWidget((iLabelWidget *) dialogHeading, uiLabelLargeBold_FontId);
654 setFlags_Widget(dialogHeading, alignLeft_WidgetFlag, iTrue);
655 }
656 if (findChild_Widget(sheet, "valueinput.prompt")) {
657 iWidget *prompt = findChild_Widget(sheet, "valueinput.prompt");
658 setFlags_Widget(prompt, alignLeft_WidgetFlag, iTrue);
659 iInputWidget *input = findChild_Widget(sheet, "input");
660 removeChild_Widget(parent_Widget(input), input);
661 addChild_Widget(topPanel, iClob(makeValuePadding_(as_Widget(input))));
662 }
663 /* Top padding for each panel, to account for the overlaid navbar. */ {
664 setId_Widget(addChildPos_Widget(topPanel,
665 iClob(makePadding_Widget(0)), front_WidgetAddPos),
666 "panel.toppad");
667 }
668 /* Navbar. */ {
669 iWidget *navi = new_Widget();
670 setId_Widget(navi, "panel.navi");
671 setBackgroundColor_Widget(navi, uiBackground_ColorId);
672 addChild_Widget(navi, iClob(makePadding_Widget(0)));
673 iLabelWidget *back = addChildFlags_Widget(navi,
674 iClob(new_LabelWidget(leftAngle_Icon " ${panel.back}", "panel.close")),
675 noBackground_WidgetFlag | frameless_WidgetFlag |
676 alignLeft_WidgetFlag | extraPadding_WidgetFlag);
677 checkIcon_LabelWidget(back);
678 setId_Widget(as_Widget(back), "panel.back");
679 setFont_LabelWidget(back, labelFont_());
680 if (!isPrefs) {
681 /* Pick up the dialog buttons for the navbar. */
682 iWidget *buttons = findChild_Widget(sheet, "dialogbuttons");
683 iLabelWidget *cancel = findMenuItem_Widget(buttons, "cancel");
684// if (!cancel) {
685// cancel = findMenuItem_Widget(buttons, "translation.cancel");
686// }
687 if (cancel) {
688 updateText_LabelWidget(back, text_LabelWidget(cancel));
689 setCommand_LabelWidget(back, command_LabelWidget(cancel));
690 }
691 iLabelWidget *def = (iLabelWidget *) lastChild_Widget(buttons);
692 if (def && !cancel) {
693 updateText_LabelWidget(back, text_LabelWidget(def));
694 setCommand_LabelWidget(back, command_LabelWidget(def));
695 setFlags_Widget(as_Widget(back), alignLeft_WidgetFlag, iFalse);
696 setFlags_Widget(as_Widget(back), alignRight_WidgetFlag, iTrue);
697 setIcon_LabelWidget(back, 0);
698 setFont_LabelWidget(back, labelBoldFont_());
699 }
700 else if (def != cancel) {
701 removeChild_Widget(buttons, def);
702 setFont_LabelWidget(def, labelBoldFont_());
703 setFlags_Widget(as_Widget(def),
704 frameless_WidgetFlag | extraPadding_WidgetFlag |
705 noBackground_WidgetFlag, iTrue);
706 addChildFlags_Widget(as_Widget(back), iClob(def), moveToParentRightEdge_WidgetFlag);
707 updateSize_LabelWidget(def);
708 }
709 /* Action buttons are added in the bottom as extra buttons. */ {
710 iBool isFirstAction = iTrue;
711 iForEach(ObjectList, i, children_Widget(buttons)) {
712 if (isInstance_Object(i.object, &Class_LabelWidget) &&
713 i.object != cancel && i.object != def) {
714 iLabelWidget *item = i.object;
715 setBackgroundColor_Widget(i.object, uiBackgroundSidebar_ColorId);
716 setFont_LabelWidget(item, labelFont_());
717 removeChild_Widget(buttons, item);
718 addChildFlags_Widget(topPanel, iClob(item), panelButtonFlags |
719 (isFirstAction ? borderTop_WidgetFlag : 0));
720 updateSize_LabelWidget(item);
721 isFirstAction = iFalse;
722 }
723 }
724 }
725 iRelease(removeChild_Widget(parent_Widget(buttons), buttons));
726 /* Styling for remaining elements. */
727 iForEach(ObjectList, i, children_Widget(topPanel)) {
728 if (isInstance_Object(i.object, &Class_LabelWidget) &&
729 isEmpty_String(command_LabelWidget(i.object)) &&
730 isEmpty_String(id_Widget(i.object))) {
731 setFlags_Widget(i.object, alignLeft_WidgetFlag, iTrue);
732 if (font_LabelWidget(i.object) == uiLabel_FontId) {
733 setFont_LabelWidget(i.object, uiContent_FontId);
734 }
735 }
736 }
737 }
738 addChildFlags_Widget(sheet, iClob(navi),
739 drawBackgroundToVerticalSafeArea_WidgetFlag |
740 arrangeHeight_WidgetFlag | resizeWidthOfChildren_WidgetFlag |
741 resizeToParentWidth_WidgetFlag | arrangeVertical_WidgetFlag);
742 }
743 if (isPrefs && isSideBySideLayout_()) {
744 /* Show the General panel. */
745 postCommand_Widget(at_PtrArray(panelButtons, 0), "panel.open");
746 }
747 mainDetailSplitHandler_(mainDetailSplit, "window.resized"); /* make it resize the split */
748 updatePanelSheetMetrics_(sheet);
749 iAssert(sheet->parent);
750 arrange_Widget(sheet->parent);
751 postCommand_App("widget.overflow"); /* with the correct dimensions */
752 }
753 else {
754 arrange_Widget(sheet);
755 }
756 postRefresh_App();
757}
758
759void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) {
760 if (!useMobileSheetLayout_()) {
761 return;
762 }
763 const iBool isSlidePanel = (flags_Widget(sheet) & horizontalOffset_WidgetFlag) != 0;
764 if (isSlidePanel && isLandscape_App()) {
765 return;
766 }
767 if (isIncoming) {
768 setVisualOffset_Widget(sheet, isSlidePanel ? width_Widget(sheet) : height_Widget(sheet), 0, 0);
769 setVisualOffset_Widget(sheet, 0, 330, easeOut_AnimFlag | softer_AnimFlag);
770 }
771 else {
772 const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset) - 0) > 1;
773 setVisualOffset_Widget(sheet,
774 isSlidePanel ? width_Widget(sheet) : height_Widget(sheet),
775 wasDragged ? 100 : 200,
776 wasDragged ? 0 : easeIn_AnimFlag | softer_AnimFlag);
777 }
778}
779
780void setupSheetTransition_Mobile(iWidget *sheet, iBool isIncoming) {
781 if (isSideBySideLayout_()) {
782 return;
783 }
784 if (isIncoming) {
785 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iTrue);
786 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, 0, 0);
787 setVisualOffset_Widget(sheet, 0, 200, easeOut_AnimFlag);
788 }
789 else {
790 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iTrue);
791 const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset)) > 0;
792 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, wasDragged ? 100 : 200,
793 wasDragged ? 0 : easeIn_AnimFlag);
794 }
795}
diff --git a/src/ui/mobile.h b/src/ui/mobile.h
new file mode 100644
index 00000000..44134389
--- /dev/null
+++ b/src/ui/mobile.h
@@ -0,0 +1,32 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#pragma once
24
25#include <the_Foundation/defs.h>
26
27iDeclareType(Widget)
28
29void setupMenuTransition_Mobile (iWidget *menu, iBool isIncoming);
30void setupSheetTransition_Mobile (iWidget *sheet, iBool isIncoming);
31
32void finalizeSheet_Mobile (iWidget *sheet);
diff --git a/src/ui/paint.c b/src/ui/paint.c
index faaf403d..c575d5fc 100644
--- a/src/ui/paint.c
+++ b/src/ui/paint.c
@@ -62,20 +62,19 @@ void endTarget_Paint(iPaint *d) {
62} 62}
63 63
64void setClip_Paint(iPaint *d, iRect rect) { 64void setClip_Paint(iPaint *d, iRect rect) {
65 if (rect.pos.y < 0) { 65 rect = intersect_Rect(rect, rect_Root(get_Root()));
66 const int off = rect.pos.y; 66 if (isEmpty_Rect(rect)) {
67 rect.pos.y -= off; 67 rect = init_Rect(0, 0, 1, 1);
68 rect.size.y = iMax(0, rect.size.y + off);
69 }
70 if (rect.pos.x < 0) {
71 const int off = rect.pos.x;
72 rect.pos.x -= off;
73 rect.size.x = iMax(0, rect.size.x + off);
74 } 68 }
75 SDL_RenderSetClipRect(renderer_Paint_(d), (const SDL_Rect *) &rect); 69 SDL_RenderSetClipRect(renderer_Paint_(d), (const SDL_Rect *) &rect);
76} 70}
77 71
78void unsetClip_Paint(iPaint *d) { 72void unsetClip_Paint(iPaint *d) {
73 if (numRoots_Window(get_Window()) > 1) {
74 const iRect rect = rect_Root(get_Root());
75 SDL_RenderSetClipRect(renderer_Paint_(d), (const SDL_Rect *) &rect);
76 return;
77 }
79#if SDL_VERSION_ATLEAST(2, 0, 12) 78#if SDL_VERSION_ATLEAST(2, 0, 12)
80 SDL_RenderSetClipRect(renderer_Paint_(d), NULL); 79 SDL_RenderSetClipRect(renderer_Paint_(d), NULL);
81#else 80#else
diff --git a/src/ui/root.c b/src/ui/root.c
index 76ef05c4..6cf3f424 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -269,6 +269,7 @@ iAnyObject *findWidget_Root(const char *id) {
269} 269}
270 270
271void destroyPending_Root(iRoot *d) { 271void destroyPending_Root(iRoot *d) {
272 iRoot *oldRoot = current_Root();
272 setCurrent_Root(d); 273 setCurrent_Root(d);
273 iForEach(PtrSet, i, d->pendingDestruction) { 274 iForEach(PtrSet, i, d->pendingDestruction) {
274 iWidget *widget = *i.value; 275 iWidget *widget = *i.value;
@@ -282,7 +283,7 @@ void destroyPending_Root(iRoot *d) {
282 iRelease(widget); 283 iRelease(widget);
283 remove_PtrSetIterator(&i); 284 remove_PtrSetIterator(&i);
284 } 285 }
285 setCurrent_Root(NULL); 286 setCurrent_Root(oldRoot);
286} 287}
287 288
288void postArrange_Root(iRoot *d) { 289void postArrange_Root(iRoot *d) {
@@ -349,6 +350,7 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
349 } 350 }
350 else if (equal_Command(cmd, "input.resized")) { 351 else if (equal_Command(cmd, "input.resized")) {
351 /* No parent handled this, so do a full rearrangement. */ 352 /* No parent handled this, so do a full rearrangement. */
353 /* TODO: Defer this and do a single rearrangement later. */
352 arrange_Widget(root); 354 arrange_Widget(root);
353 postRefresh_App(); 355 postRefresh_App();
354 return iTrue; 356 return iTrue;
@@ -414,6 +416,7 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
414 else { 416 else {
415 addChildPos_Widget(findChild_Widget(root, "stack"), iClob(sidebar), back_WidgetAddPos); 417 addChildPos_Widget(findChild_Widget(root, "stack"), iClob(sidebar), back_WidgetAddPos);
416 setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI); 418 setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI);
419 setWidth_SidebarWidget(sidebar2, (float) width_Widget(root) / (float) gap_UI);
417 } 420 }
418 return iFalse; 421 return iFalse;
419 } 422 }
@@ -466,11 +469,11 @@ static void setReloadLabel_Root_(iRoot *d, iBool animating) {
466 const iBool isMobile = deviceType_App() != desktop_AppDeviceType; 469 const iBool isMobile = deviceType_App() != desktop_AppDeviceType;
467 iLabelWidget *label = findChild_Widget(d->widget, "reload"); 470 iLabelWidget *label = findChild_Widget(d->widget, "reload");
468 updateTextCStr_LabelWidget( 471 updateTextCStr_LabelWidget(
469 label, animating ? loadAnimationCStr_() : (isMobile ? pageMenuCStr_ : reloadCStr_)); 472 label, animating ? loadAnimationCStr_() : (/*isMobile ? pageMenuCStr_ :*/ reloadCStr_));
470 if (isMobile) { 473// if (isMobile) {
471 setCommand_LabelWidget(label, 474// setCommand_LabelWidget(label,
472 collectNewCStr_String(animating ? "navigate.reload" : "menu.open")); 475// collectNewCStr_String(animating ? "navigate.reload" : "menu.open"));
473 } 476// }
474} 477}
475 478
476static void checkLoadAnimation_Root_(iRoot *d) { 479static void checkLoadAnimation_Root_(iRoot *d) {
@@ -536,9 +539,12 @@ static iBool willPerformSearchQuery_(const iString *userInput) {
536 539
537static void updateUrlInputContentPadding_(iWidget *navBar) { 540static void updateUrlInputContentPadding_(iWidget *navBar) {
538 iInputWidget *url = findChild_Widget(navBar, "url"); 541 iInputWidget *url = findChild_Widget(navBar, "url");
539 const iWidget *indicators = findChild_Widget(navBar, "url.rightembed"); 542 const int lockWidth = width_Widget(findChild_Widget(navBar, "navbar.lock"));
540 setContentPadding_InputWidget(url, -1, 543 const int indicatorsWidth = width_Widget(findChild_Widget(navBar, "url.rightembed"));
541 width_Widget(indicators)); 544 /* The indicators widget has a padding that covers the urlButtons area. */
545 setContentPadding_InputWidget(url,
546 lockWidth - 2 * gap_UI, // * 0.75f,
547 indicatorsWidth);
542} 548}
543 549
544static void showSearchQueryIndicator_(iBool show) { 550static void showSearchQueryIndicator_(iBool show) {
@@ -585,17 +591,17 @@ static void updateNavBarSize_(iWidget *navBar) {
585 updateSize_LabelWidget(label); 591 updateSize_LabelWidget(label);
586 } 592 }
587 } 593 }
594 updateUrlInputContentPadding_(navBar);
588 /* Note that InputWidget uses the `tight` flag to adjust its inner padding. */ 595 /* Note that InputWidget uses the `tight` flag to adjust its inner padding. */
589 /* TODO: Is this redundant? See `updateMetrics_Window_()`. */ 596// const int embedButtonWidth = width_Widget(findChild_Widget(navBar, "navbar.lock"));
590 const int embedButtonWidth = width_Widget(findChild_Widget(navBar, "navbar.lock")); 597// setContentPadding_InputWidget(findChild_Widget(navBar, "url"),
591 setContentPadding_InputWidget(findChild_Widget(navBar, "url"), 598// embedButtonWidth * 0.75f,
592 embedButtonWidth * 0.75f, 599// embedButtonWidth * 0.75f);
593 embedButtonWidth * 0.75f);
594 } 600 }
595 if (isPhone) { 601 if (isPhone) {
596 static const char *buttons[] = { "navbar.back", "navbar.forward", "navbar.sidebar", 602 static const char *buttons[] = { "navbar.back", "navbar.forward", "navbar.sidebar",
597 "navbar.ident", "navbar.home", "navbar.menu" }; 603 "navbar.ident", "navbar.home", "navbar.menu" };
598 iWidget *toolBar = findWidget_App("toolbar"); 604 iWidget *toolBar = findWidget_Root("toolbar");
599 setVisualOffset_Widget(toolBar, 0, 0, 0); 605 setVisualOffset_Widget(toolBar, 0, 0, 0);
600 setFlags_Widget(toolBar, hidden_WidgetFlag, isLandscape_App()); 606 setFlags_Widget(toolBar, hidden_WidgetFlag, isLandscape_App());
601 iForIndices(i, buttons) { 607 iForIndices(i, buttons) {
@@ -901,15 +907,19 @@ void updateMetrics_Root(iRoot *d) {
901 iWidget *url = findChild_Widget(d->widget, "url"); 907 iWidget *url = findChild_Widget(d->widget, "url");
902 iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed"); 908 iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed");
903 iWidget *embedPad = findChild_Widget(navBar, "url.embedpad"); 909 iWidget *embedPad = findChild_Widget(navBar, "url.embedpad");
910 iWidget *urlButtons = findChild_Widget(navBar, "url.buttons");
904 setPadding_Widget(as_Widget(url), 0, gap_UI, 0, gap_UI); 911 setPadding_Widget(as_Widget(url), 0, gap_UI, 0, gap_UI);
905 navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */ 912 navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */
906 updateSize_LabelWidget((iLabelWidget *) lock); 913// updateSize_LabelWidget((iLabelWidget *) lock);
907 setFixedSize_Widget(embedPad, init_I2(width_Widget(lock) + gap_UI / 2, 1)); 914// updateSize_LabelWidget((iLabelWidget *) findChild_Widget(navBar, "reload"));
908 setContentPadding_InputWidget((iInputWidget *) url, width_Widget(lock) * 0.75, 915// arrange_Widget(urlButtons);
909 width_Widget(lock) * 0.75); 916 setFixedSize_Widget(embedPad, init_I2(width_Widget(urlButtons) + gap_UI / 2, 1));
917// setContentPadding_InputWidget((iInputWidget *) url, width_Widget(lock) * 0.75,
918// width_Widget(lock) * 0.75);
910 rightEmbed->rect.pos.y = gap_UI; 919 rightEmbed->rect.pos.y = gap_UI;
911 updatePadding_Root(d); 920 updatePadding_Root(d);
912 arrange_Widget(d->widget); 921 arrange_Widget(d->widget);
922 updateUrlInputContentPadding_(navBar);
913 postRefresh_App(); 923 postRefresh_App();
914} 924}
915 925
@@ -1057,7 +1067,7 @@ void createUserInterface_Root(iRoot *d) {
1057 setNoAutoMinHeight_LabelWidget(fprog, iTrue); 1067 setNoAutoMinHeight_LabelWidget(fprog, iTrue);
1058 addChildFlags_Widget(rightEmbed, 1068 addChildFlags_Widget(rightEmbed,
1059 iClob(fprog), 1069 iClob(fprog),
1060 collapse_WidgetFlag | frameless_WidgetFlag | hidden_WidgetFlag); 1070 collapse_WidgetFlag | hidden_WidgetFlag | frameless_WidgetFlag);
1061 } 1071 }
1062 /* Download progress indicator is also inside the input field, but hidden normally. */ { 1072 /* Download progress indicator is also inside the input field, but hidden normally. */ {
1063 iLabelWidget *progress = new_LabelWidget(uiTextCaution_ColorEscape "00.000 ${mb}", NULL); 1073 iLabelWidget *progress = new_LabelWidget(uiTextCaution_ColorEscape "00.000 ${mb}", NULL);
@@ -1066,7 +1076,7 @@ void createUserInterface_Root(iRoot *d) {
1066 setAlignVisually_LabelWidget(progress, iTrue); 1076 setAlignVisually_LabelWidget(progress, iTrue);
1067 setNoAutoMinHeight_LabelWidget(progress, iTrue); 1077 setNoAutoMinHeight_LabelWidget(progress, iTrue);
1068 addChildFlags_Widget( 1078 addChildFlags_Widget(
1069 rightEmbed, iClob(progress), collapse_WidgetFlag); 1079 rightEmbed, iClob(progress), collapse_WidgetFlag | hidden_WidgetFlag);
1070 } 1080 }
1071 /* Pinning indicator. */ { 1081 /* Pinning indicator. */ {
1072 iLabelWidget *pin = new_LabelWidget(uiTextAction_ColorEscape leftHalf_Icon, NULL); 1082 iLabelWidget *pin = new_LabelWidget(uiTextAction_ColorEscape leftHalf_Icon, NULL);
@@ -1076,39 +1086,44 @@ void createUserInterface_Root(iRoot *d) {
1076 setNoAutoMinHeight_LabelWidget(pin, iTrue); 1086 setNoAutoMinHeight_LabelWidget(pin, iTrue);
1077 addChildFlags_Widget(rightEmbed, 1087 addChildFlags_Widget(rightEmbed,
1078 iClob(pin), 1088 iClob(pin),
1079 collapse_WidgetFlag | tight_WidgetFlag | frameless_WidgetFlag); 1089 collapse_WidgetFlag | hidden_WidgetFlag | tight_WidgetFlag | frameless_WidgetFlag);
1090 }
1091 iWidget *urlButtons = new_Widget();
1092 setId_Widget(urlButtons, "url.buttons");
1093 setFlags_Widget(urlButtons, embedFlags | arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
1094 /* Mobile page menu. */
1095 if (deviceType_App() != desktop_AppDeviceType) {
1096 iLabelWidget *pageMenuButton;
1097 /* In a mobile layout, the reload button is replaced with the Page/Ellipsis menu. */
1098 pageMenuButton = makeMenuButton_LabelWidget(pageMenuCStr_,
1099 (iMenuItem[]){
1100 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
1101 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
1102 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" },
1103 { "---", 0, 0, NULL },
1104 { bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
1105 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
1106 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
1107 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
1108 { "---", 0, 0, NULL },
1109 { "${menu.page.copyurl}", 0, 0, "document.copylink" },
1110 { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" },
1111 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } },
1112 12);
1113 setId_Widget(as_Widget(pageMenuButton), "pagemenubutton");
1114 setFont_LabelWidget(pageMenuButton, uiContentBold_FontId);
1115 setAlignVisually_LabelWidget(pageMenuButton, iTrue);
1116 addChildFlags_Widget(urlButtons, iClob(pageMenuButton), embedFlags);
1117 updateSize_LabelWidget(pageMenuButton);
1080 } 1118 }
1081 /* Reload button. */ { 1119 /* Reload button. */ {
1082 iLabelWidget *reload; 1120 iLabelWidget *reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload");
1083 if (deviceType_App() == desktop_AppDeviceType) {
1084 reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload");
1085 }
1086 else {
1087 /* In a mobile layout, the reload button is replaced with the Page/Ellipsis menu. */
1088 reload = makeMenuButton_LabelWidget(pageMenuCStr_,
1089 (iMenuItem[]){
1090 { reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" },
1091 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" },
1092 { "---", 0, 0, NULL },
1093 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
1094 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
1095 { "---", 0, 0, NULL },
1096 { pin_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
1097 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
1098 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
1099 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
1100 { "---", 0, 0, NULL },
1101 { "${menu.page.copyurl}", 0, 0, "document.copylink" },
1102 { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" },
1103 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } },
1104 14);
1105 setFont_LabelWidget((iLabelWidget *) reload, uiContentBold_FontId);
1106 setAlignVisually_LabelWidget((iLabelWidget *) reload, iTrue);
1107 }
1108 setId_Widget(as_Widget(reload), "reload"); 1121 setId_Widget(as_Widget(reload), "reload");
1109 addChildFlags_Widget(as_Widget(url), iClob(reload), embedFlags | moveToParentRightEdge_WidgetFlag); 1122 addChildFlags_Widget(urlButtons, iClob(reload), embedFlags);
1110 updateSize_LabelWidget(reload); 1123 updateSize_LabelWidget(reload);
1111 } 1124 }
1125 addChildFlags_Widget(as_Widget(url), iClob(urlButtons), moveToParentRightEdge_WidgetFlag);
1126 arrange_Widget(urlButtons);
1112 setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad"); 1127 setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad");
1113 } 1128 }
1114 if (deviceType_App() != desktop_AppDeviceType) { 1129 if (deviceType_App() != desktop_AppDeviceType) {
@@ -1216,7 +1231,9 @@ void createUserInterface_Root(iRoot *d) {
1216 setFlags_Widget(toolBar, moveToParentBottomEdge_WidgetFlag | 1231 setFlags_Widget(toolBar, moveToParentBottomEdge_WidgetFlag |
1217 parentCannotResizeHeight_WidgetFlag | 1232 parentCannotResizeHeight_WidgetFlag |
1218 resizeWidthOfChildren_WidgetFlag | 1233 resizeWidthOfChildren_WidgetFlag |
1219 arrangeHeight_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue); 1234 arrangeHeight_WidgetFlag | arrangeHorizontal_WidgetFlag |
1235 commandOnClick_WidgetFlag |
1236 drawBackgroundToBottom_WidgetFlag, iTrue);
1220 setBackgroundColor_Widget(toolBar, tmBannerBackground_ColorId); 1237 setBackgroundColor_Widget(toolBar, tmBannerBackground_ColorId);
1221 setId_Widget(addChildFlags_Widget(toolBar, 1238 setId_Widget(addChildFlags_Widget(toolBar,
1222 iClob(newLargeIcon_LabelWidget("\U0001f870", "navigate.back")), 1239 iClob(newLargeIcon_LabelWidget("\U0001f870", "navigate.back")),
@@ -1231,7 +1248,7 @@ void createUserInterface_Root(iRoot *d) {
1231 frameless_WidgetFlag), 1248 frameless_WidgetFlag),
1232 "toolbar.ident"); 1249 "toolbar.ident");
1233 setId_Widget(addChildFlags_Widget(toolBar, 1250 setId_Widget(addChildFlags_Widget(toolBar,
1234 iClob(newLargeIcon_LabelWidget("\U0001f588", "toolbar.showview arg:-1")), 1251 iClob(newLargeIcon_LabelWidget(book_Icon, "toolbar.showview arg:-1")),
1235 frameless_WidgetFlag | commandOnClick_WidgetFlag), 1252 frameless_WidgetFlag | commandOnClick_WidgetFlag),
1236 "toolbar.view"); 1253 "toolbar.view");
1237 iLabelWidget *menuButton = makeMenuButton_LabelWidget("\U0001d362", phoneNavMenuItems_, 1254 iLabelWidget *menuButton = makeMenuButton_LabelWidget("\U0001d362", phoneNavMenuItems_,
@@ -1244,17 +1261,17 @@ void createUserInterface_Root(iRoot *d) {
1244 setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue); 1261 setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue);
1245 setTextColor_LabelWidget(i.object, tmBannerIcon_ColorId); 1262 setTextColor_LabelWidget(i.object, tmBannerIcon_ColorId);
1246 // setBackgroundColor_Widget(i.object, tmBannerSideTitle_ColorId); 1263 // setBackgroundColor_Widget(i.object, tmBannerSideTitle_ColorId);
1247 } 1264 }
1248 const iMenuItem items[] = { 1265 const iMenuItem items[] = {
1249 { pin_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" }, 1266 { book_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" },
1250 { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" }, 1267 { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" },
1251 { clock_Icon " ${sidebar.history}", 0, 0, "toolbar.showview arg:2" }, 1268 { clock_Icon " ${sidebar.history}", 0, 0, "toolbar.showview arg:2" },
1252 { page_Icon " ${toolbar.outline}", 0, 0, "toolbar.showview arg:4" }, 1269 { page_Icon " ${toolbar.outline}", 0, 0, "toolbar.showview arg:4" },
1253 }; 1270 };
1254 iWidget *menu = makeMenu_Widget(findChild_Widget(toolBar, "toolbar.view"), 1271 iWidget *menu = makeMenu_Widget(findChild_Widget(toolBar, "toolbar.view"),
1255 items, iElemCount(items)); 1272 items, iElemCount(items));
1256 setId_Widget(menu, "toolbar.menu"); /* view menu */ 1273 setId_Widget(menu, "toolbar.menu"); /* view menu */
1257 } 1274 }
1258#endif 1275#endif
1259 updatePadding_Root(d); 1276 updatePadding_Root(d);
1260 /* Global context menus. */ { 1277 /* Global context menus. */ {
@@ -1319,6 +1336,11 @@ void createUserInterface_Root(iRoot *d) {
1319 } 1336 }
1320 updateMetrics_Root(d); 1337 updateMetrics_Root(d);
1321 updateNavBarSize_(navBar); 1338 updateNavBarSize_(navBar);
1339 if (deviceType_App() == phone_AppDeviceType) {
1340 const float sidebarWidth = width_Widget(root) / (float) gap_UI;
1341 setWidth_SidebarWidget(findChild_Widget(root, "sidebar"), sidebarWidth);
1342 setWidth_SidebarWidget(findChild_Widget(root, "sidebar2"), sidebarWidth);
1343 }
1322} 1344}
1323 1345
1324void showToolbars_Root(iRoot *d, iBool show) { 1346void showToolbars_Root(iRoot *d, iBool show) {
diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c
index 32b57c69..ff5144b2 100644
--- a/src/ui/scrollwidget.c
+++ b/src/ui/scrollwidget.c
@@ -108,7 +108,7 @@ static void unfade_ScrollWidget_(iScrollWidget *d, float opacity) {
108 d->fadeStart = SDL_GetTicks() + 1000; 108 d->fadeStart = SDL_GetTicks() + 1000;
109 if (targetValue_Anim(&d->opacity) < opacity) { 109 if (targetValue_Anim(&d->opacity) < opacity) {
110 setValue_Anim(&d->opacity, opacity, 66); 110 setValue_Anim(&d->opacity, opacity, 66);
111 addTicker_App(animateOpacity_ScrollWidget_, d); 111 addTickerRoot_App(animateOpacity_ScrollWidget_, as_Widget(d)->root, d);
112 } 112 }
113 if (!d->willCheckFade && d->fadeEnabled) { 113 if (!d->willCheckFade && d->fadeEnabled) {
114 d->willCheckFade = iTrue; 114 d->willCheckFade = iTrue;
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index eae0432f..cb6dab65 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -106,7 +106,8 @@ struct Impl_SidebarWidget {
106 size_t numUnreadEntries; 106 size_t numUnreadEntries;
107 iWidget * resizer; 107 iWidget * resizer;
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}; 111};
111 112
112iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side) 113iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side)
@@ -239,7 +240,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
239 as_Widget(d), 240 as_Widget(d),
240 (iMenuItem[]){ { openTab_Icon " ${feeds.entry.newtab}", 0, 0, "feed.entry.opentab" }, 241 (iMenuItem[]){ { openTab_Icon " ${feeds.entry.newtab}", 0, 0, "feed.entry.opentab" },
241 { circle_Icon " ${feeds.entry.markread}", 0, 0, "feed.entry.toggleread" }, 242 { circle_Icon " ${feeds.entry.markread}", 0, 0, "feed.entry.toggleread" },
242 { pin_Icon " ${feeds.entry.bookmark}", 0, 0, "feed.entry.bookmark" }, 243 { bookmark_Icon " ${feeds.entry.bookmark}", 0, 0, "feed.entry.bookmark" },
243 { "---", 0, 0, NULL }, 244 { "---", 0, 0, NULL },
244 { page_Icon " ${feeds.entry.openfeed}", 0, 0, "feed.entry.openfeed" }, 245 { page_Icon " ${feeds.entry.openfeed}", 0, 0, "feed.entry.openfeed" },
245 { edit_Icon " ${feeds.edit}", 0, 0, "feed.entry.edit" }, 246 { edit_Icon " ${feeds.edit}", 0, 0, "feed.entry.edit" },
@@ -364,7 +365,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
364 as_Widget(d), 365 as_Widget(d),
365 (iMenuItem[]){ 366 (iMenuItem[]){
366 { "${menu.copyurl}", 0, 0, "history.copy" }, 367 { "${menu.copyurl}", 0, 0, "history.copy" },
367 { pin_Icon " ${sidebar.entry.bookmark}", 0, 0, "history.addbookmark" }, 368 { bookmark_Icon " ${sidebar.entry.bookmark}", 0, 0, "history.addbookmark" },
368 { "---", 0, 0, NULL }, 369 { "---", 0, 0, NULL },
369 { close_Icon " ${menu.forgeturl}", 0, 0, "history.delete" }, 370 { close_Icon " ${menu.forgeturl}", 0, 0, "history.delete" },
370 { "---", 0, 0, NULL }, 371 { "---", 0, 0, NULL },
@@ -373,10 +374,6 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
373 break; 374 break;
374 } 375 }
375 case identities_SidebarMode: { 376 case identities_SidebarMode: {
376 /* Actions. */ {
377 addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0);
378 addActionButton_SidebarWidget_(d, "${sidebar.action.ident.import}", "ident.import", 0);
379 }
380 const iString *tabUrl = url_DocumentWidget(document_App()); 377 const iString *tabUrl = url_DocumentWidget(document_App());
381 isEmpty = iTrue; 378 isEmpty = iTrue;
382 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) { 379 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) {
@@ -413,6 +410,11 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
413 iRelease(item); 410 iRelease(item);
414 isEmpty = iFalse; 411 isEmpty = iFalse;
415 } 412 }
413 /* Actions. */
414 if (!isEmpty) {
415 addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0);
416 addActionButton_SidebarWidget_(d, "${sidebar.action.ident.import}", "ident.import", 0);
417 }
416 const iMenuItem menuItems[] = { 418 const iMenuItem menuItems[] = {
417 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" }, 419 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" },
418 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" }, 420 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" },
@@ -520,15 +522,15 @@ float width_SidebarWidget(const iSidebarWidget *d) {
520} 522}
521 523
522static const char *normalModeLabels_[max_SidebarMode] = { 524static const char *normalModeLabels_[max_SidebarMode] = {
523 pin_Icon " ${sidebar.bookmarks}", 525 book_Icon " ${sidebar.bookmarks}",
524 star_Icon " ${sidebar.feeds}", 526 star_Icon " ${sidebar.feeds}",
525 clock_Icon " ${sidebar.history}", 527 clock_Icon " ${sidebar.history}",
526 person_Icon " ${sidebar.identities}", 528 person_Icon " ${sidebar.identities}",
527 page_Icon " ${sidebar.outline}", 529 page_Icon " ${sidebar.outline}",
528}; 530};
529 531
530static const char *tightModeLabels_[max_SidebarMode] = { 532static const char *tightModeLabels_[max_SidebarMode] = {
531 pin_Icon, 533 book_Icon,
532 star_Icon, 534 star_Icon,
533 clock_Icon, 535 clock_Icon,
534 person_Icon, 536 person_Icon,
@@ -576,11 +578,11 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
576 d->itemFonts[0] = uiContent_FontId; 578 d->itemFonts[0] = uiContent_FontId;
577 d->itemFonts[1] = uiContentBold_FontId; 579 d->itemFonts[1] = uiContentBold_FontId;
578#if defined (iPlatformAppleMobile) 580#if defined (iPlatformAppleMobile)
579 d->widthAsGaps = 73;
580 if (deviceType_App() == phone_AppDeviceType) { 581 if (deviceType_App() == phone_AppDeviceType) {
581 d->itemFonts[0] = defaultBig_FontId; 582 d->itemFonts[0] = defaultBig_FontId;
582 d->itemFonts[1] = defaultBigBold_FontId; 583 d->itemFonts[1] = defaultBigBold_FontId;
583 } 584 }
585 d->widthAsGaps = 73;
584#else 586#else
585 d->widthAsGaps = 60; 587 d->widthAsGaps = 60;
586#endif 588#endif
@@ -644,6 +646,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
644 "actions"); 646 "actions");
645 setBackgroundColor_Widget(d->actions, uiBackgroundSidebar_ColorId); 647 setBackgroundColor_Widget(d->actions, uiBackgroundSidebar_ColorId);
646 d->contextItem = NULL; 648 d->contextItem = NULL;
649 d->contextIndex = iInvalidPos;
647 d->blank = new_Widget(); 650 d->blank = new_Widget();
648 addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag); 651 addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag);
649 addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag); 652 addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag);
@@ -770,6 +773,7 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) {
770 for (int i = 0; i < max_SidebarMode; i++) { 773 for (int i = 0; i < max_SidebarMode; i++) {
771 iLabelWidget *button = d->modeButtons[i]; 774 iLabelWidget *button = d->modeButtons[i];
772 if (!button) continue; 775 if (!button) continue;
776 setAlignVisually_LabelWidget(button, isTight);
773 setFlags_Widget(as_Widget(button), tight_WidgetFlag, isTight); 777 setFlags_Widget(as_Widget(button), tight_WidgetFlag, isTight);
774 if (i == feeds_SidebarMode && d->numUnreadEntries) { 778 if (i == feeds_SidebarMode && d->numUnreadEntries) {
775 updateText_LabelWidget( 779 updateText_LabelWidget(
@@ -803,7 +807,7 @@ void setWidth_SidebarWidget(iSidebarWidget *d, float widthAsGaps) {
803 if (isVisible_Widget(w)) { 807 if (isVisible_Widget(w)) {
804 w->rect.size.x = width; 808 w->rect.size.x = width;
805 } 809 }
806 arrange_Widget(findWidget_App("stack")); 810 arrange_Widget(findWidget_Root("stack"));
807 checkModeButtonLayout_SidebarWidget_(d); 811 checkModeButtonLayout_SidebarWidget_(d);
808 updateItemHeight_SidebarWidget_(d); 812 updateItemHeight_SidebarWidget_(d);
809 if (!isFixedWidth && !isRefreshPending_App()) { 813 if (!isFixedWidth && !isRefreshPending_App()) {
@@ -844,7 +848,6 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c
844 isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))); 848 isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit")));
845 postCommand_App("bookmarks.changed"); 849 postCommand_App("bookmarks.changed");
846 } 850 }
847 setFlags_Widget(as_Widget(d), disabled_WidgetFlag, iFalse);
848 destroy_Widget(editor); 851 destroy_Widget(editor);
849 return iTrue; 852 return iTrue;
850 } 853 }
@@ -875,10 +878,33 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *
875 if (arg_Command(cmd) && isVisible_Widget(w)) { 878 if (arg_Command(cmd) && isVisible_Widget(w)) {
876 return iTrue; 879 return iTrue;
877 } 880 }
881 const iBool isAnimated = argLabel_Command(cmd, "noanim") == 0 &&
882 (deviceType_App() != phone_AppDeviceType);
883 int visX = 0;
884 if (isVisible_Widget(w)) {
885 visX = left_Rect(bounds_Widget(w)) - left_Rect(w->root->widget->rect);
886 }
878 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); 887 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w));
879 if (isVisible_Widget(w)) { 888 if (isVisible_Widget(w)) {
889 setFlags_Widget(w, keepOnTop_WidgetFlag, iFalse);
880 w->rect.size.x = d->widthAsGaps * gap_UI; 890 w->rect.size.x = d->widthAsGaps * gap_UI;
881 invalidate_ListWidget(d->list); 891 invalidate_ListWidget(d->list);
892 if (isAnimated) {
893 setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue);
894 setVisualOffset_Widget(w, (d->side == left_SideBarSide ? -1 : 1) * w->rect.size.x, 0, 0);
895 setVisualOffset_Widget(w, 0, 300, easeOut_AnimFlag | softer_AnimFlag);
896 }
897 }
898 else if (isAnimated) {
899 setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue);
900 if (d->side == right_SideBarSide) {
901 setVisualOffset_Widget(w, visX, 0, 0);
902 setVisualOffset_Widget(w, visX + w->rect.size.x, 300, easeOut_AnimFlag | softer_AnimFlag);
903 }
904 else {
905 setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue);
906 setVisualOffset_Widget(w, -w->rect.size.x, 300, easeOut_AnimFlag | softer_AnimFlag);
907 }
882 } 908 }
883 arrange_Widget(w->parent); 909 arrange_Widget(w->parent);
884 /* BUG: Rearranging because the arrange above didn't fully resolve the height. */ 910 /* BUG: Rearranging because the arrange above didn't fully resolve the height. */
@@ -985,11 +1011,8 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
985 itemClicked_SidebarWidget_(d, pointerLabel_Command(cmd, "item")); 1011 itemClicked_SidebarWidget_(d, pointerLabel_Command(cmd, "item"));
986 return iTrue; 1012 return iTrue;
987 } 1013 }
988 else if (isCommand_Widget(w, ev, "menu.opened")) {
989 setFlags_Widget(as_Widget(d->list), disabled_WidgetFlag, iTrue);
990 }
991 else if (isCommand_Widget(w, ev, "menu.closed")) { 1014 else if (isCommand_Widget(w, ev, "menu.closed")) {
992 setFlags_Widget(as_Widget(d->list), disabled_WidgetFlag, iFalse); 1015 // invalidateItem_ListWidget(d->list, d->contextIndex);
993 } 1016 }
994 else if (isCommand_Widget(w, ev, "bookmark.open")) { 1017 else if (isCommand_Widget(w, ev, "bookmark.open")) {
995 const iSidebarItem *item = d->contextItem; 1018 const iSidebarItem *item = d->contextItem;
@@ -1010,7 +1033,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1010 else if (isCommand_Widget(w, ev, "bookmark.edit")) { 1033 else if (isCommand_Widget(w, ev, "bookmark.edit")) {
1011 const iSidebarItem *item = d->contextItem; 1034 const iSidebarItem *item = d->contextItem;
1012 if (d->mode == bookmarks_SidebarMode && item) { 1035 if (d->mode == bookmarks_SidebarMode && item) {
1013 setFlags_Widget(w, disabled_WidgetFlag, iTrue);
1014 iWidget *dlg = makeBookmarkEditor_Widget(); 1036 iWidget *dlg = makeBookmarkEditor_Widget();
1015 setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w)))); 1037 setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w))));
1016 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); 1038 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id);
@@ -1038,7 +1060,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1038 else if (isCommand_Widget(w, ev, "bookmark.dup")) { 1060 else if (isCommand_Widget(w, ev, "bookmark.dup")) {
1039 const iSidebarItem *item = d->contextItem; 1061 const iSidebarItem *item = d->contextItem;
1040 if (d->mode == bookmarks_SidebarMode && item) { 1062 if (d->mode == bookmarks_SidebarMode && item) {
1041 setFlags_Widget(w, disabled_WidgetFlag, iTrue);
1042 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); 1063 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id);
1043 const iBool isRemote = hasTag_Bookmark(bm, remote_BookmarkTag); 1064 const iBool isRemote = hasTag_Bookmark(bm, remote_BookmarkTag);
1044 iChar icon = isRemote ? 0x1f588 : bm->icon; 1065 iChar icon = isRemote ? 0x1f588 : bm->icon;
@@ -1131,7 +1152,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1131 return iTrue; 1152 return iTrue;
1132 } 1153 }
1133 if (isCommand_Widget(w, ev, "feed.entry.edit")) { 1154 if (isCommand_Widget(w, ev, "feed.entry.edit")) {
1134 setFlags_Widget(w, disabled_WidgetFlag, iTrue);
1135 makeFeedSettings_Widget(id_Bookmark(feedBookmark)); 1155 makeFeedSettings_Widget(id_Bookmark(feedBookmark));
1136 return iTrue; 1156 return iTrue;
1137 } 1157 }
@@ -1307,6 +1327,10 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1307 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); 1327 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
1308 } 1328 }
1309 } 1329 }
1330 if (d->contextIndex != iInvalidPos) {
1331 invalidateItem_ListWidget(d->list, d->contextIndex);
1332 d->contextIndex = iInvalidPos;
1333 }
1310 } 1334 }
1311 if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) { 1335 if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) {
1312 if (ev->button.button == SDL_BUTTON_RIGHT) { 1336 if (ev->button.button == SDL_BUTTON_RIGHT) {
@@ -1315,7 +1339,12 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1315 updateMouseHover_ListWidget(d->list); 1339 updateMouseHover_ListWidget(d->list);
1316 } 1340 }
1317 if (constHoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) { 1341 if (constHoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) {
1318 d->contextItem = hoverItem_ListWidget(d->list); 1342 d->contextItem = hoverItem_ListWidget(d->list);
1343 /* Context is drawn in hover state. */
1344 if (d->contextIndex != iInvalidPos) {
1345 invalidateItem_ListWidget(d->list, d->contextIndex);
1346 }
1347 d->contextIndex = hoverItemIndex_ListWidget(d->list);
1319 /* Update menu items. */ 1348 /* Update menu items. */
1320 /* TODO: Some callback-based mechanism would be nice for updating menus right 1349 /* TODO: Some callback-based mechanism would be nice for updating menus right
1321 before they open? */ 1350 before they open? */
@@ -1427,18 +1456,30 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
1427 const iRect bounds = bounds_Widget(w); 1456 const iRect bounds = bounds_Widget(w);
1428 iPaint p; 1457 iPaint p;
1429 init_Paint(&p); 1458 init_Paint(&p);
1459 if (flags_Widget(w) & visualOffset_WidgetFlag && isVisible_Widget(w)) {
1460 fillRect_Paint(&p, boundsWithoutVisualOffset_Widget(w), tmBackground_ColorId);
1461 }
1430 draw_Widget(w); 1462 draw_Widget(w);
1431 drawVLine_Paint( 1463 if (isVisible_Widget(w)) {
1432 &p, addX_I2(topRight_Rect(bounds), -1), height_Rect(bounds), uiSeparator_ColorId); 1464 drawVLine_Paint(
1465 &p,
1466 addX_I2(d->side == left_SideBarSide ? topRight_Rect(bounds) : topLeft_Rect(bounds), -1),
1467 height_Rect(bounds),
1468 uiSeparator_ColorId);
1469 }
1433} 1470}
1434 1471
1435static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect, 1472static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1436 const iListWidget *list) { 1473 const iListWidget *list) {
1437 const iSidebarWidget *sidebar = findParentClass_Widget(constAs_Widget(list), 1474 const iSidebarWidget *sidebar = findParentClass_Widget(constAs_Widget(list),
1438 &Class_SidebarWidget); 1475 &Class_SidebarWidget);
1476 const iBool isMenuVisible = isVisible_Widget(sidebar->menu);
1439 const iBool isPressing = isMouseDown_ListWidget(list); 1477 const iBool isPressing = isMouseDown_ListWidget(list);
1440 const iBool isHover = isHover_Widget(constAs_Widget(list)) && 1478 const iBool isHover =
1441 constHoverItem_ListWidget(list) == d; 1479 (!isMenuVisible &&
1480 isHover_Widget(constAs_Widget(list)) &&
1481 constHoverItem_ListWidget(list) == d) ||
1482 (isMenuVisible && sidebar->contextItem == d);
1442 const int scrollBarWidth = scrollBarWidth_ListWidget(list); 1483 const int scrollBarWidth = scrollBarWidth_ListWidget(list);
1443#if defined (iPlatformApple) 1484#if defined (iPlatformApple)
1444 const int blankWidth = 0; 1485 const int blankWidth = 0;
@@ -1652,7 +1693,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1652 drawRange_Text( 1693 drawRange_Text(
1653 font, cPos, d->listItem.isSelected ? iconColor : metaFg, range_String(&icon)); 1694 font, cPos, d->listItem.isSelected ? iconColor : metaFg, range_String(&icon));
1654 deinit_String(&icon); 1695 deinit_String(&icon);
1655 drawRange_Text(d->listItem.isSelected ? uiContentBold_FontId : font, 1696 drawRange_Text(d->listItem.isSelected ? sidebar->itemFonts[1] : font,
1656 add_I2(cPos, init_I2(indent, 0)), 1697 add_I2(cPos, init_I2(indent, 0)),
1657 fg, 1698 fg,
1658 range_String(&d->label)); 1699 range_String(&d->label));
diff --git a/src/ui/text.c b/src/ui/text.c
index 9838fb00..889aa2e4 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -781,6 +781,7 @@ enum iRunMode {
781 permanentColorFlag_RunMode = iBit(11), 781 permanentColorFlag_RunMode = iBit(11),
782 alwaysVariableWidthFlag_RunMode = iBit(12), 782 alwaysVariableWidthFlag_RunMode = iBit(12),
783 fillBackground_RunMode = iBit(13), 783 fillBackground_RunMode = iBit(13),
784 stopAtNewline_RunMode = iBit(14), /* don't advance past \n, consider it a wrap pos */
784}; 785};
785 786
786static enum iFontId fontId_Text_(const iFont *font) { 787static enum iFontId fontId_Text_(const iFont *font) {
@@ -923,7 +924,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
923 } 924 }
924 /* TODO: Check out if `uc_wordbreak_property()` from libunistring can be used here. */ 925 /* TODO: Check out if `uc_wordbreak_property()` from libunistring can be used here. */
925 if (ch == '\n') { 926 if (ch == '\n') {
926 if (args->xposLimit > 0 && ~mode & noWrapFlag_RunMode) { 927 if (args->xposLimit > 0 && mode & stopAtNewline_RunMode) {
927 /* Stop the line here, this is a hard warp. */ 928 /* Stop the line here, this is a hard warp. */
928 if (args->continueFrom_out) { 929 if (args->continueFrom_out) {
929 *args->continueFrom_out = chPos; 930 *args->continueFrom_out = chPos;
@@ -1148,7 +1149,8 @@ iInt2 advanceRange_Text(int fontId, iRangecc text) {
1148iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos) { 1149iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos) {
1149 int advance; 1150 int advance;
1150 const int height = run_Font_(font_Text_(fontId), 1151 const int height = run_Font_(font_Text_(fontId),
1151 &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId), 1152 &(iRunArgs){ .mode = measure_RunMode | stopAtNewline_RunMode |
1153 runFlagsFromId_(fontId),
1152 .text = text, 1154 .text = text,
1153 .xposLimit = width, 1155 .xposLimit = width,
1154 .continueFrom_out = endPos, 1156 .continueFrom_out = endPos,
@@ -1161,6 +1163,7 @@ iInt2 tryAdvanceNoWrap_Text(int fontId, iRangecc text, int width, const char **e
1161 int advance; 1163 int advance;
1162 const int height = run_Font_(font_Text_(fontId), 1164 const int height = run_Font_(font_Text_(fontId),
1163 &(iRunArgs){ .mode = measure_RunMode | noWrapFlag_RunMode | 1165 &(iRunArgs){ .mode = measure_RunMode | noWrapFlag_RunMode |
1166 stopAtNewline_RunMode |
1164 runFlagsFromId_(fontId), 1167 runFlagsFromId_(fontId),
1165 .text = text, 1168 .text = text,
1166 .xposLimit = width, 1169 .xposLimit = width,
@@ -1269,7 +1272,7 @@ int drawWrapRange_Text(int fontId, iInt2 pos, int maxWidth, int color, iRangecc
1269 const iInt2 adv = tryAdvance_Text(fontId, text, maxWidth, &endp); 1272 const iInt2 adv = tryAdvance_Text(fontId, text, maxWidth, &endp);
1270 drawRange_Text(fontId, pos, color, (iRangecc){ text.start, endp }); 1273 drawRange_Text(fontId, pos, color, (iRangecc){ text.start, endp });
1271 text.start = endp; 1274 text.start = endp;
1272 pos.y += adv.y; 1275 pos.y += iMax(adv.y, lineHeight_Text(fontId));
1273 } 1276 }
1274 return pos.y; 1277 return pos.y;
1275} 1278}
@@ -1404,9 +1407,21 @@ iString *renderBlockChars_Text(const iBlock *fontData, int height, enum iTextBlo
1404 1407
1405iDefineTypeConstructionArgs(TextBuf, (int font, int color, const char *text), font, color, text) 1408iDefineTypeConstructionArgs(TextBuf, (int font, int color, const char *text), font, color, text)
1406 1409
1407void init_TextBuf(iTextBuf *d, int font, int color, const char *text) { 1410static void initWrap_TextBuf_(iTextBuf *d, int font, int color, int maxWidth, iBool doWrap, const char *text) {
1408 SDL_Renderer *render = text_.render; 1411 SDL_Renderer *render = text_.render;
1409 d->size = advance_Text(font, text); 1412 if (maxWidth == 0) {
1413 d->size = advance_Text(font, text);
1414 }
1415 else {
1416 d->size = zero_I2();
1417 iRangecc content = range_CStr(text);
1418 while (!isEmpty_Range(&content)) {
1419 const iInt2 size = (doWrap ? tryAdvance_Text(font, content, maxWidth, &content.start)
1420 : tryAdvanceNoWrap_Text(font, content, maxWidth, &content.start));
1421 d->size.x = iMax(d->size.x, size.x);
1422 d->size.y += iMax(size.y, lineHeight_Text(font));
1423 }
1424 }
1410 SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); 1425 SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0");
1411 if (d->size.x * d->size.y) { 1426 if (d->size.x * d->size.y) {
1412 d->texture = SDL_CreateTexture(render, 1427 d->texture = SDL_CreateTexture(render,
@@ -1424,17 +1439,50 @@ void init_TextBuf(iTextBuf *d, int font, int color, const char *text) {
1424 SDL_SetTextureBlendMode(text_.cache, SDL_BLENDMODE_NONE); /* blended when TextBuf is drawn */ 1439 SDL_SetTextureBlendMode(text_.cache, SDL_BLENDMODE_NONE); /* blended when TextBuf is drawn */
1425 SDL_SetRenderDrawColor(text_.render, 0, 0, 0, 0); 1440 SDL_SetRenderDrawColor(text_.render, 0, 0, 0, 0);
1426 SDL_RenderClear(text_.render); 1441 SDL_RenderClear(text_.render);
1427 draw_Text_(font, zero_I2(), color | fillBackground_ColorId, range_CStr(text)); 1442 const int fg = color | fillBackground_ColorId;
1443 iRangecc range = range_CStr(text);
1444 if (maxWidth == 0) {
1445 draw_Text_(font, zero_I2(), fg, range);
1446 }
1447 else if (doWrap) {
1448 drawWrapRange_Text(font, zero_I2(), maxWidth, fg, range);
1449 }
1450 else {
1451 iInt2 pos = zero_I2();
1452 while (!isEmpty_Range(&range)) {
1453 const char *endp;
1454 tryAdvanceNoWrap_Text(font, range, maxWidth, &endp);
1455 draw_Text_(font, pos, fg, (iRangecc){ range.start, endp });
1456 range.start = endp;
1457 pos.y += lineHeight_Text(font);
1458 }
1459 }
1428 SDL_SetTextureBlendMode(text_.cache, SDL_BLENDMODE_BLEND); 1460 SDL_SetTextureBlendMode(text_.cache, SDL_BLENDMODE_BLEND);
1429 SDL_SetRenderTarget(render, oldTarget); 1461 SDL_SetRenderTarget(render, oldTarget);
1430 SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND); 1462 SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND);
1431 } 1463 }
1432} 1464}
1433 1465
1466void init_TextBuf(iTextBuf *d, int font, int color, const char *text) {
1467 initWrap_TextBuf_(d, font, color, 0, iFalse, text);
1468}
1469
1434void deinit_TextBuf(iTextBuf *d) { 1470void deinit_TextBuf(iTextBuf *d) {
1435 SDL_DestroyTexture(d->texture); 1471 SDL_DestroyTexture(d->texture);
1436} 1472}
1437 1473
1474iTextBuf *newBound_TextBuf(int font, int color, int boundWidth, const char *text) {
1475 iTextBuf *d = iMalloc(TextBuf);
1476 initWrap_TextBuf_(d, font, color, boundWidth, iFalse, text);
1477 return d;
1478}
1479
1480iTextBuf *newWrap_TextBuf(int font, int color, int wrapWidth, const char *text) {
1481 iTextBuf *d = iMalloc(TextBuf);
1482 initWrap_TextBuf_(d, font, color, wrapWidth, iTrue, text);
1483 return d;
1484}
1485
1438void draw_TextBuf(const iTextBuf *d, iInt2 pos, int color) { 1486void draw_TextBuf(const iTextBuf *d, iInt2 pos, int color) {
1439 const iColor clr = get_Color(color); 1487 const iColor clr = get_Color(color);
1440 SDL_SetTextureColorMod(d->texture, clr.r, clr.g, clr.b); 1488 SDL_SetTextureColorMod(d->texture, clr.r, clr.g, clr.b);
diff --git a/src/ui/text.h b/src/ui/text.h
index c6091599..044ddd32 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -170,10 +170,13 @@ iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iT
170 170
171iDeclareType(TextBuf) 171iDeclareType(TextBuf)
172iDeclareTypeConstructionArgs(TextBuf, int font, int color, const char *text) 172iDeclareTypeConstructionArgs(TextBuf, int font, int color, const char *text)
173 173
174struct Impl_TextBuf { 174struct Impl_TextBuf {
175 SDL_Texture *texture; 175 SDL_Texture *texture;
176 iInt2 size; 176 iInt2 size;
177}; 177};
178 178
179void draw_TextBuf (const iTextBuf *, iInt2 pos, int color); 179iTextBuf * newBound_TextBuf(int font, int color, int boundWidth, const char *text); /* does not word wrap */
180iTextBuf * newWrap_TextBuf (int font, int color, int wrapWidth, const char *text);
181
182void draw_TextBuf (const iTextBuf *, iInt2 pos, int color);
diff --git a/src/ui/touch.c b/src/ui/touch.c
index b2c52526..74a22baf 100644
--- a/src/ui/touch.c
+++ b/src/ui/touch.c
@@ -227,7 +227,7 @@ static void dispatchButtonUp_Touch_(iFloat3 pos) {
227 227
228static void dispatchNotification_Touch_(const iTouch *d, int code) { 228static void dispatchNotification_Touch_(const iTouch *d, int code) {
229 if (d->affinity) { 229 if (d->affinity) {
230 iRoot *oldRoot = get_Root(); 230 iRoot *oldRoot = current_Root();
231 setCurrent_Root(d->affinity->root); 231 setCurrent_Root(d->affinity->root);
232 dispatchEvent_Widget(d->affinity, (SDL_Event *) &(SDL_UserEvent){ 232 dispatchEvent_Widget(d->affinity, (SDL_Event *) &(SDL_UserEvent){
233 .type = SDL_USEREVENT, 233 .type = SDL_USEREVENT,
@@ -318,6 +318,8 @@ static void update_TouchState_(void *ptr) {
318 if (pixels.x || pixels.y) { 318 if (pixels.x || pixels.y) {
319 subv_F3(&mom->accum, initI2_F3(pixels)); 319 subv_F3(&mom->accum, initI2_F3(pixels));
320 dispatchMotion_Touch_(mom->pos, 0); 320 dispatchMotion_Touch_(mom->pos, 0);
321 iAssert(mom->affinity);
322 setCurrent_Root(mom->affinity->root);
321 dispatchEvent_Widget(mom->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){ 323 dispatchEvent_Widget(mom->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){
322 .type = SDL_MOUSEWHEEL, 324 .type = SDL_MOUSEWHEEL,
323 .timestamp = nowTime, 325 .timestamp = nowTime,
@@ -334,26 +336,13 @@ static void update_TouchState_(void *ptr) {
334 } 336 }
335 /* Keep updating if interaction is still ongoing. */ 337 /* Keep updating if interaction is still ongoing. */
336 if (!isEmpty_Array(d->touches) || !isEmpty_Array(d->moms)) { 338 if (!isEmpty_Array(d->touches) || !isEmpty_Array(d->moms)) {
337 addTicker_App(update_TouchState_, ptr); 339 addTickerRoot_App(update_TouchState_, NULL, ptr);
338 } 340 }
339} 341}
340 342
341static iWidget *findOverflowScrollable_Widget_(iWidget *d) {
342 const iInt2 rootSize = size_Root(d->root);
343 for (iWidget *w = d; w; w = parent_Widget(w)) {
344 if (flags_Widget(w) & overflowScrollable_WidgetFlag) {
345 if (height_Widget(w) > rootSize.y && !hasVisibleChildOnTop_Widget(w)) {
346 return w;
347 }
348 return NULL;
349 }
350 }
351 return NULL;
352}
353
354static iWidget *findSlidePanel_Widget_(iWidget *d) { 343static iWidget *findSlidePanel_Widget_(iWidget *d) {
355 for (iWidget *w = d; w; w = parent_Widget(w)) { 344 for (iWidget *w = d; w; w = parent_Widget(w)) {
356 if (isVisible_Widget(w) && flags_Widget(w) & horizontalOffset_WidgetFlag) { 345 if (isVisible_Widget(w) && flags_Widget(w) & edgeDraggable_WidgetFlag) {
357 return w; 346 return w;
358 } 347 }
359 } 348 }
@@ -466,6 +455,8 @@ iBool processEvent_Touch(const SDL_Event *ev) {
466 if (edge == left_TouchEdge) { 455 if (edge == left_TouchEdge) {
467 dragging = findSlidePanel_Widget_(aff); 456 dragging = findSlidePanel_Widget_(aff);
468 if (dragging) { 457 if (dragging) {
458// printf("Selected for dragging: ");
459// identify_Widget(dragging);
469 setFlags_Widget(dragging, dragged_WidgetFlag, iTrue); 460 setFlags_Widget(dragging, dragged_WidgetFlag, iTrue);
470 } 461 }
471 } 462 }
@@ -492,7 +483,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
492 } 483 }
493 /* This may begin a pinch. */ 484 /* This may begin a pinch. */
494 checkNewPinch_TouchState_(d, back_Array(d->touches)); 485 checkNewPinch_TouchState_(d, back_Array(d->touches));
495 addTicker_App(update_TouchState_, d); 486 addTickerRoot_App(update_TouchState_, NULL, d);
496 } 487 }
497 else if (ev->type == SDL_FINGERMOTION) { 488 else if (ev->type == SDL_FINGERMOTION) {
498 iTouch *touch = find_TouchState_(d, fing->fingerId); 489 iTouch *touch = find_TouchState_(d, fing->fingerId);
@@ -544,7 +535,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
544 divvf_F3(&touch->accum, 6); 535 divvf_F3(&touch->accum, 6);
545 divfv_I2(&pixels, 6); 536 divfv_I2(&pixels, 6);
546 /* Allow scrolling a scrollable widget. */ 537 /* Allow scrolling a scrollable widget. */
547 iWidget *flow = findOverflowScrollable_Widget_(touch->affinity); 538 iWidget *flow = findOverflowScrollable_Widget(touch->affinity);
548 if (flow) { 539 if (flow) {
549 touch->affinity = flow; 540 touch->affinity = flow;
550 } 541 }
@@ -567,7 +558,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
567 } 558 }
568 /* Edge swipe aborted? */ 559 /* Edge swipe aborted? */
569 if (touch->edge == left_TouchEdge) { 560 if (touch->edge == left_TouchEdge) {
570 if (fing->dx < 0) { 561 if (fing->dx < 0 && x_F3(touch->pos[0]) < tapRadiusPt_ * window->pixelRatio) {
571 touch->edge = none_TouchEdge; 562 touch->edge = none_TouchEdge;
572 if (touch->edgeDragging) { 563 if (touch->edgeDragging) {
573 setFlags_Widget(touch->edgeDragging, dragged_WidgetFlag, iFalse); 564 setFlags_Widget(touch->edgeDragging, dragged_WidgetFlag, iFalse);
@@ -598,6 +589,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
598 if (pixels.x || pixels.y) { 589 if (pixels.x || pixels.y) {
599 setFocus_Widget(NULL); 590 setFocus_Widget(NULL);
600 dispatchMotion_Touch_(touch->pos[0], 0); 591 dispatchMotion_Touch_(touch->pos[0], 0);
592 setCurrent_Root(touch->affinity->root);
601 dispatchEvent_Widget(touch->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){ 593 dispatchEvent_Widget(touch->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){
602 .type = SDL_MOUSEWHEEL, 594 .type = SDL_MOUSEWHEEL,
603 .timestamp = SDL_GetTicks(), 595 .timestamp = SDL_GetTicks(),
@@ -642,17 +634,19 @@ iBool processEvent_Touch(const SDL_Event *ev) {
642 continue; 634 continue;
643 } 635 }
644 /* Edge swipes do not generate momentum. */ 636 /* Edge swipes do not generate momentum. */
637 const size_t lastIndex = iMin(touch->posCount - 1, lastIndex_Touch_);
645 const uint32_t duration = nowTime - touch->startTime; 638 const uint32_t duration = nowTime - touch->startTime;
646 const iFloat3 gestureVector = sub_F3(pos, touch->startPos); 639 const iFloat3 gestureVector = sub_F3(pos, touch->pos[lastIndex]);
647 iFloat3 velocity = zero_F3(); 640 iFloat3 velocity = zero_F3();
648 if (touch->edge && fabsf(2 * x_F3(gestureVector)) > fabsf(y_F3(gestureVector)) && 641 if (touch->edge && fabsf(2 * x_F3(gestureVector)) > fabsf(y_F3(gestureVector)) &&
649 !isStationary_Touch_(touch)) { 642 !isStationary_Touch_(touch)) {
650 dispatchClick_Touch_(touch, touch->edge == left_TouchEdge ? SDL_BUTTON_X1 643 const int swipeDir = x_F3(gestureVector) > 0 ? +1 : -1;
651 : SDL_BUTTON_X2); 644 dispatchClick_Touch_(touch,
645 touch->edge == left_TouchEdge && swipeDir > 0 ? SDL_BUTTON_X1 :
646 touch->edge == right_TouchEdge && swipeDir < 0 ? SDL_BUTTON_X2 : 0);
652 setHover_Widget(NULL); 647 setHover_Widget(NULL);
653 } 648 }
654 else { 649 else {
655 const size_t lastIndex = iMin(touch->posCount - 1, lastIndex_Touch_);
656 const uint32_t elapsed = fing->timestamp - touch->posTime[lastIndex]; 650 const uint32_t elapsed = fing->timestamp - touch->posTime[lastIndex];
657 const float minVelocity = 400.0f; 651 const float minVelocity = 400.0f;
658 if (elapsed < 150) { 652 if (elapsed < 150) {
@@ -758,6 +752,20 @@ iInt2 latestPosition_Touch(void) {
758 return touchState_()->currentTouchPos; 752 return touchState_()->currentTouchPos;
759} 753}
760 754
755iBool isHovering_Touch(void) {
756 iTouchState *d = touchState_();
757 if (numFingers_Touch() == 1) {
758 const iTouch *touch = constFront_Array(d->touches);
759 if (touch->isTapBegun && isStationary_Touch_(touch)) {
760 return iTrue;
761 }
762 if (touch->isTapAndHold) {
763 return iTrue;
764 }
765 }
766 return iFalse;
767}
768
761size_t numFingers_Touch(void) { 769size_t numFingers_Touch(void) {
762 return size_Array(touchState_()->touches); 770 return size_Array(touchState_()->touches);
763} 771}
diff --git a/src/ui/touch.h b/src/ui/touch.h
index 1a6fb350..e048224a 100644
--- a/src/ui/touch.h
+++ b/src/ui/touch.h
@@ -41,4 +41,5 @@ enum iWidgetTouchMode widgetMode_Touch (const iWidget *widget);
41void widgetDestroyed_Touch (iWidget *widget); 41void widgetDestroyed_Touch (iWidget *widget);
42 42
43iInt2 latestPosition_Touch (void); /* valid during processing of current event */ 43iInt2 latestPosition_Touch (void); /* valid during processing of current event */
44iBool isHovering_Touch (void); /* stationary touch or a long-press drag ongoing */
44size_t numFingers_Touch (void); 45size_t numFingers_Touch (void);
diff --git a/src/ui/util.c b/src/ui/util.c
index 04cdf27f..4b35f8f7 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -80,7 +80,7 @@ void toString_Sym(int key, int kmods, iString *str) {
80 appendChar_String(str, 0x2325); 80 appendChar_String(str, 0x2325);
81 } 81 }
82 if (kmods & KMOD_SHIFT) { 82 if (kmods & KMOD_SHIFT) {
83 appendChar_String(str, 0x21e7); 83 appendCStr_String(str, shift_Icon);
84 } 84 }
85 if (kmods & KMOD_GUI) { 85 if (kmods & KMOD_GUI) {
86 appendChar_String(str, 0x2318); 86 appendChar_String(str, 0x2318);
@@ -93,7 +93,7 @@ void toString_Sym(int key, int kmods, iString *str) {
93 appendCStr_String(str, "Alt+"); 93 appendCStr_String(str, "Alt+");
94 } 94 }
95 if (kmods & KMOD_SHIFT) { 95 if (kmods & KMOD_SHIFT) {
96 appendCStr_String(str, "Shift+"); 96 appendCStr_String(str, shift_Icon "+");
97 } 97 }
98 if (kmods & KMOD_GUI) { 98 if (kmods & KMOD_GUI) {
99 appendCStr_String(str, "Meta+"); 99 appendCStr_String(str, "Meta+");
@@ -138,7 +138,7 @@ void toString_Sym(int key, int kmods, iString *str) {
138 } 138 }
139 else if (key == SDLK_RETURN) { 139 else if (key == SDLK_RETURN) {
140 removePlus_(str); 140 removePlus_(str);
141 appendChar_String(str, 0x21a9); /* Leftwards arrow with a hook */ 141 appendCStr_String(str, return_Icon); /* Leftwards arrow with a hook */
142 } 142 }
143 else { 143 else {
144 appendCStr_String(str, SDL_GetKeyName(key)); 144 appendCStr_String(str, SDL_GetKeyName(key));
@@ -837,10 +837,7 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
837 if (postCommands) { 837 if (postCommands) {
838 postCommand_Widget(d, "menu.opened"); 838 postCommand_Widget(d, "menu.opened");
839 } 839 }
840 if (isPortraitPhone) { 840 setupMenuTransition_Mobile(d, iTrue);
841 setVisualOffset_Widget(d, isSlidePanel ? width_Widget(d) : height_Widget(d), 0, 0);
842 setVisualOffset_Widget(d, 0, 330, easeOut_AnimFlag | softer_AnimFlag);
843 }
844} 841}
845 842
846void closeMenu_Widget(iWidget *d) { 843void closeMenu_Widget(iWidget *d) {
@@ -851,14 +848,7 @@ void closeMenu_Widget(iWidget *d) {
851 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iTrue); 848 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iTrue);
852 postRefresh_App(); 849 postRefresh_App();
853 postCommand_Widget(d, "menu.closed"); 850 postCommand_Widget(d, "menu.closed");
854 if (isPortrait_App() && deviceType_App() == phone_AppDeviceType) { 851 setupMenuTransition_Mobile(d, iFalse);
855 const iBool wasDragged = iAbs(value_Anim(&d->visualOffset) - 0) > 1;
856 setVisualOffset_Widget(d,
857 flags_Widget(d) & horizontalOffset_WidgetFlag ?
858 width_Widget(d) : height_Widget(d),
859 wasDragged ? 100 : 200,
860 wasDragged ? 0 : easeIn_AnimFlag | softer_AnimFlag);
861 }
862} 852}
863 853
864iLabelWidget *findMenuItem_Widget(iWidget *menu, const char *command) { 854iLabelWidget *findMenuItem_Widget(iWidget *menu, const char *command) {
@@ -904,7 +894,7 @@ static iBool isTabPage_Widget_(const iWidget *tabs, const iWidget *page) {
904static void unfocusFocusInsideTabPage_(const iWidget *page) { 894static void unfocusFocusInsideTabPage_(const iWidget *page) {
905 iWidget *focus = focus_Widget(); 895 iWidget *focus = focus_Widget();
906 if (page && focus && hasParent_Widget(focus, page)) { 896 if (page && focus && hasParent_Widget(focus, page)) {
907 printf("unfocus inside page: %p\n", focus); 897// printf("unfocus inside page: %p\n", focus);
908 setFocus_Widget(NULL); 898 setFocus_Widget(NULL);
909 } 899 }
910} 900}
@@ -1127,46 +1117,6 @@ size_t tabCount_Widget(const iWidget *tabs) {
1127 1117
1128/*-----------------------------------------------------------------------------------------------*/ 1118/*-----------------------------------------------------------------------------------------------*/
1129 1119
1130static void acceptFilePath_(iWidget *dlg) {
1131 iInputWidget *input = findChild_Widget(dlg, "input");
1132 iString *path = makeAbsolute_Path(text_InputWidget(input));
1133 postCommandf_App("%s path:%s", cstr_String(id_Widget(dlg)), cstr_String(path));
1134 destroy_Widget(dlg);
1135 delete_String(path);
1136}
1137
1138iBool filePathHandler_(iWidget *dlg, const char *cmd) {
1139 iWidget *ptr = as_Widget(pointer_Command(cmd));
1140 if (equal_Command(cmd, "input.ended")) {
1141 if (hasParent_Widget(ptr, dlg)) {
1142 if (arg_Command(cmd)) {
1143 acceptFilePath_(dlg);
1144 }
1145 else {
1146 destroy_Widget(dlg);
1147 }
1148 return iTrue;
1149 }
1150 return iFalse;
1151 }
1152 else if (ptr && !hasParent_Widget(ptr, dlg)) {
1153 /* Command from outside the dialog, so dismiss the dialog. */
1154 if (!equal_Command(cmd, "focus.lost")) {
1155 destroy_Widget(dlg);
1156 }
1157 return iFalse;
1158 }
1159 else if (equal_Command(cmd, "filepath.cancel")) {
1160 end_InputWidget(findChild_Widget(dlg, "input"), iFalse);
1161 destroy_Widget(dlg);
1162 return iTrue;
1163 }
1164 else if (equal_Command(cmd, "filepath.accept")) {
1165 acceptFilePath_(dlg);
1166 return iTrue;
1167 }
1168 return iFalse;
1169}
1170 1120
1171iWidget *makeSheet_Widget(const char *id) { 1121iWidget *makeSheet_Widget(const char *id) {
1172 iWidget *sheet = new_Widget(); 1122 iWidget *sheet = new_Widget();
@@ -1183,609 +1133,6 @@ iWidget *makeSheet_Widget(const char *id) {
1183 return sheet; 1133 return sheet;
1184} 1134}
1185 1135
1186static void updateSheetPanelMetrics_(iWidget *sheet) {
1187 iWidget *navi = findChild_Widget(sheet, "panel.navi");
1188 iWidget *naviPad = child_Widget(navi, 0);
1189 int naviHeight = lineHeight_Text(defaultBig_FontId) + 4 * gap_UI;
1190#if defined (iPlatformAppleMobile)
1191 float left, right, top, bottom;
1192 safeAreaInsets_iOS(&left, &top, &right, &bottom);
1193 setPadding_Widget(sheet, left, 0, right, 0);
1194 navi->rect.pos = init_I2(left, top);
1195 iConstForEach(PtrArray, i, findChildren_Widget(sheet, "panel.toppad")) {
1196 iWidget *pad = *i.value;
1197 setFixedSize_Widget(pad, init1_I2(naviHeight));
1198 }
1199#endif
1200 setFixedSize_Widget(navi, init_I2(-1, naviHeight));
1201}
1202
1203static iBool slidePanelHandler_(iWidget *d, const char *cmd) {
1204 if (equal_Command(cmd, "panel.open")) {
1205 iWidget *button = pointer_Command(cmd);
1206 iWidget *panel = userData_Object(button);
1207 openMenu_Widget(panel, innerToWindow_Widget(panel, zero_I2()));
1208 setFlags_Widget(panel, disabled_WidgetFlag, iFalse);
1209// updateTextCStr_LabelWidget(findWidget_App("panel.back"), );
1210 return iTrue;
1211 }
1212 if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd) &&
1213 argLabel_Command(cmd, "button") == SDL_BUTTON_X1) {
1214 postCommand_App("panel.close");
1215 return iTrue;
1216 }
1217 if (equal_Command(cmd, "panel.close")) {
1218 iBool wasClosed = iFalse;
1219 iForEach(ObjectList, i, children_Widget(parent_Widget(d))) {
1220 iWidget *child = i.object;
1221 if (!cmp_String(id_Widget(child), "panel") && isVisible_Widget(child)) {
1222 closeMenu_Widget(child);
1223 setFlags_Widget(child, disabled_WidgetFlag, iTrue);
1224 setFocus_Widget(NULL);
1225 updateTextCStr_LabelWidget(findWidget_App("panel.back"), "Back");
1226 wasClosed = iTrue;
1227 }
1228 }
1229 if (!wasClosed) {
1230 postCommand_App("prefs.dismiss");
1231 }
1232 return iTrue;
1233 }
1234 if (equal_Command(cmd, "document.changed")) {
1235 postCommand_App("prefs.dismiss");
1236 return iFalse;
1237 }
1238 if (equal_Command(cmd, "window.resized")) {
1239 updateSheetPanelMetrics_(parent_Widget(d));
1240 }
1241 return iFalse;
1242}
1243
1244static iBool isTwoColumnPage_(iWidget *d) {
1245 if (cmp_String(id_Widget(d), "dialogbuttons") == 0 ||
1246 cmp_String(id_Widget(d), "prefs.tabs") == 0) {
1247 return iFalse;
1248 }
1249 if (class_Widget(d) == &Class_Widget && childCount_Widget(d) == 2) {
1250 return class_Widget(child_Widget(d, 0)) == &Class_Widget &&
1251 class_Widget(child_Widget(d, 1)) == &Class_Widget;
1252 }
1253 return iFalse;
1254}
1255
1256static iBool isOmittedPref_(const iString *id) {
1257 static const char *omittedPrefs[] = {
1258 "prefs.smoothscroll",
1259 "prefs.imageloadscroll",
1260 "prefs.retainwindow",
1261 "prefs.ca.file",
1262 "prefs.ca.path",
1263 };
1264 iForIndices(i, omittedPrefs) {
1265 if (cmp_String(id, omittedPrefs[i]) == 0) {
1266 return iTrue;
1267 }
1268 }
1269 return iFalse;
1270}
1271
1272enum iPrefsElement {
1273 panelTitle_PrefsElement,
1274 heading_PrefsElement,
1275 toggle_PrefsElement,
1276 dropdown_PrefsElement,
1277 radioButton_PrefsElement,
1278 textInput_PrefsElement,
1279};
1280
1281static iAnyObject *addPanelChild_(iWidget *panel, iAnyObject *child, int64_t flags,
1282 enum iPrefsElement elementType,
1283 enum iPrefsElement precedingElementType) {
1284 /* Erase redundant/unused headings. */
1285 if (precedingElementType == heading_PrefsElement &&
1286 (!child || (elementType == heading_PrefsElement || elementType == radioButton_PrefsElement))) {
1287 iRelease(removeChild_Widget(panel, lastChild_Widget(panel)));
1288 if (!cmp_String(id_Widget(constAs_Widget(lastChild_Widget(panel))), "padding")) {
1289 iRelease(removeChild_Widget(panel, lastChild_Widget(panel)));
1290 }
1291 }
1292 if (child) {
1293 /* Insert padding between different element types. */
1294 if (precedingElementType != panelTitle_PrefsElement) {
1295 if (elementType == heading_PrefsElement ||
1296 (elementType == toggle_PrefsElement &&
1297 precedingElementType != toggle_PrefsElement &&
1298 precedingElementType != heading_PrefsElement) ||
1299 (elementType == dropdown_PrefsElement &&
1300 precedingElementType != dropdown_PrefsElement &&
1301 precedingElementType != heading_PrefsElement) ||
1302 (elementType == textInput_PrefsElement &&
1303 precedingElementType != textInput_PrefsElement &&
1304 precedingElementType != heading_PrefsElement)) {
1305 addChild_Widget(panel, iClob(makePadding_Widget(lineHeight_Text(defaultBig_FontId))));
1306 }
1307 }
1308 if ((elementType == toggle_PrefsElement && precedingElementType != toggle_PrefsElement) ||
1309 (elementType == textInput_PrefsElement && precedingElementType != textInput_PrefsElement)) {
1310 flags |= borderTop_WidgetFlag;
1311 }
1312 return addChildFlags_Widget(panel, child, flags);
1313 }
1314 return NULL;
1315}
1316
1317static void stripTrailingColon_(iLabelWidget *label) {
1318 const iString *text = text_LabelWidget(label);
1319 if (endsWith_String(text, ":")) {
1320 iString *mod = copy_String(text);
1321 removeEnd_String(mod, 1);
1322 updateText_LabelWidget(label, mod);
1323 delete_String(mod);
1324 }
1325}
1326
1327static iLabelWidget *makePanelButton_(const char *text, const char *command) {
1328 iLabelWidget *btn = new_LabelWidget(text, command);
1329 setFlags_Widget(as_Widget(btn),
1330 borderBottom_WidgetFlag | alignLeft_WidgetFlag |
1331 frameless_WidgetFlag | extraPadding_WidgetFlag,
1332 iTrue);
1333 checkIcon_LabelWidget(btn);
1334 setFont_LabelWidget(btn, defaultBig_FontId);
1335 setTextColor_LabelWidget(btn, uiTextStrong_ColorId);
1336 setBackgroundColor_Widget(as_Widget(btn), uiBackgroundSidebar_ColorId);
1337 return btn;
1338}
1339
1340static iWidget *makeValuePadding_(iWidget *value) {
1341 iInputWidget *input = isInstance_Object(value, &Class_InputWidget) ? (iInputWidget *) value : NULL;
1342 if (input) {
1343 setFont_InputWidget(input, defaultBig_FontId);
1344 setContentPadding_InputWidget(input, 3 * gap_UI, 3 * gap_UI);
1345 }
1346 iWidget *pad = new_Widget();
1347 setBackgroundColor_Widget(pad, uiBackgroundSidebar_ColorId);
1348 setPadding_Widget(pad, 0, 1 * gap_UI, 0, 1 * gap_UI);
1349 addChild_Widget(pad, iClob(value));
1350 setFlags_Widget(pad,
1351 borderBottom_WidgetFlag |
1352 arrangeVertical_WidgetFlag |
1353 resizeToParentWidth_WidgetFlag |
1354 resizeWidthOfChildren_WidgetFlag |
1355 arrangeHeight_WidgetFlag,
1356 iTrue);
1357 return pad;
1358}
1359
1360static iWidget *makeValuePaddingWithHeading_(iLabelWidget *heading, iWidget *value) {
1361 iWidget *div = new_Widget();
1362 setFlags_Widget(div,
1363 borderBottom_WidgetFlag | arrangeHeight_WidgetFlag |
1364 resizeWidthOfChildren_WidgetFlag |
1365 arrangeHorizontal_WidgetFlag, iTrue);
1366 setBackgroundColor_Widget(div, uiBackgroundSidebar_ColorId);
1367 setPadding_Widget(div, gap_UI, gap_UI, 4 * gap_UI, gap_UI);
1368 addChildFlags_Widget(div, iClob(heading), 0);
1369 //setFixedSize_Widget(as_Widget(heading), init_I2(-1, height_Widget(value)));
1370 setFont_LabelWidget(heading, defaultBig_FontId);
1371 setTextColor_LabelWidget(heading, uiTextStrong_ColorId);
1372 if (isInstance_Object(value, &Class_InputWidget)) {
1373 addChildFlags_Widget(div, iClob(value), expand_WidgetFlag);
1374 }
1375 else {
1376 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag);
1377 addChild_Widget(div, iClob(value));
1378 }
1379 return div;
1380}
1381
1382static iWidget *addChildPanel_(iWidget *sheet, iLabelWidget *panelButton,
1383 const iString *titleText) {
1384 iWidget *owner = new_Widget();
1385 setId_Widget(owner, "panel");
1386 setUserData_Object(panelButton, owner);
1387 setBackgroundColor_Widget(owner, uiBackground_ColorId);
1388 setId_Widget(addChild_Widget(owner, iClob(makePadding_Widget(0))), "panel.toppad");
1389 if (titleText) {
1390 iLabelWidget *title =
1391 addChildFlags_Widget(owner,
1392 iClob(new_LabelWidget(cstr_String(titleText), NULL)),
1393 alignLeft_WidgetFlag | frameless_WidgetFlag);
1394 setFont_LabelWidget(title, uiLabelLargeBold_FontId);
1395 setTextColor_LabelWidget(title, uiHeading_ColorId);
1396 }
1397 addChildFlags_Widget(sheet,
1398 iClob(owner),
1399 focusRoot_WidgetFlag | hidden_WidgetFlag | disabled_WidgetFlag |
1400 arrangeVertical_WidgetFlag | resizeWidthOfChildren_WidgetFlag |
1401 arrangeHeight_WidgetFlag | overflowScrollable_WidgetFlag |
1402 horizontalOffset_WidgetFlag | commandOnClick_WidgetFlag);
1403 return owner;
1404}
1405
1406void finalizeSheet_Widget(iWidget *sheet) {
1407 /* The sheet contents are completely rearranged and restyled on a phone.
1408 We'll set up a linear fullscreen arrangement of the widgets. Sheets are already
1409 scrollable so they can be taller than the display. In hindsight, it may have been
1410 easier to create phone versions of each dialog, but at least this works with any
1411 future changes to the UI (..."works"). At least this way it is possible to enforce
1412 a consistent styling. */
1413 if (deviceType_App() == phone_AppDeviceType && parent_Widget(sheet) == root_Widget(sheet)) {
1414 if (~flags_Widget(sheet) & keepOnTop_WidgetFlag) {
1415 /* Already finalized. */
1416 arrange_Widget(sheet);
1417 postRefresh_App();
1418 return;
1419 }
1420 /* Modify the top sheet to act as a fullscreen background. */
1421 setPadding1_Widget(sheet, 0);
1422 setBackgroundColor_Widget(sheet, uiBackground_ColorId);
1423 setFlags_Widget(sheet,
1424 keepOnTop_WidgetFlag |
1425 parentCannotResize_WidgetFlag |
1426 arrangeSize_WidgetFlag |
1427 centerHorizontal_WidgetFlag |
1428 arrangeVertical_WidgetFlag |
1429 arrangeHorizontal_WidgetFlag |
1430 overflowScrollable_WidgetFlag,
1431 iFalse);
1432 setFlags_Widget(sheet,
1433 commandOnClick_WidgetFlag |
1434 frameless_WidgetFlag |
1435 resizeWidthOfChildren_WidgetFlag,
1436 iTrue);
1437 iPtrArray * contents = collect_PtrArray(new_PtrArray()); /* two-column pages */
1438 iPtrArray * panelButtons = collect_PtrArray(new_PtrArray());
1439 iWidget * prefsTabs = findChild_Widget(sheet, "prefs.tabs");
1440 iWidget * dialogHeading = (prefsTabs ? NULL : child_Widget(sheet, 0));
1441 const iBool isPrefs = (prefsTabs != NULL);
1442 const int64_t panelButtonFlags = borderBottom_WidgetFlag | alignLeft_WidgetFlag |
1443 frameless_WidgetFlag | extraPadding_WidgetFlag;
1444 iWidget *topPanel = new_Widget();
1445 setId_Widget(topPanel, "panel.top");
1446 addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(defaultBig_FontId))));
1447 if (prefsTabs) {
1448 iRelease(removeChild_Widget(sheet, child_Widget(sheet, 0))); /* heading */
1449 iRelease(removeChild_Widget(sheet, findChild_Widget(sheet, "dialogbuttons")));
1450 /* Pull out the pages and make them panels. */
1451 iWidget *pages = findChild_Widget(prefsTabs, "tabs.pages");
1452 size_t pageCount = tabCount_Widget(prefsTabs);
1453 for (size_t i = 0; i < pageCount; i++) {
1454 iString *text = copy_String(text_LabelWidget(tabPageButton_Widget(prefsTabs, tabPage_Widget(prefsTabs, 0))));
1455 iWidget *page = removeTabPage_Widget(prefsTabs, 0);
1456 iWidget *pageContent = child_Widget(page, 1); /* surrounded by padding widgets */
1457 pushBack_PtrArray(contents, ref_Object(pageContent));
1458 iLabelWidget *panelButton;
1459 pushBack_PtrArray(panelButtons,
1460 addChildFlags_Widget(topPanel,
1461 iClob(panelButton = makePanelButton_(
1462 i == 1 ? "${heading.prefs.userinterface}" : cstr_String(text),
1463 "panel.open")),
1464 (i == 0 ? borderTop_WidgetFlag : 0) |
1465 chevron_WidgetFlag));
1466 const iChar icons[] = {
1467 0x02699, /* gear */
1468 0x1f4f1, /* mobile phone */
1469 0x1f3a8, /* palette */
1470 0x1f523,
1471 0x1f5a7, /* computer network */
1472 };
1473 setIcon_LabelWidget(panelButton, icons[i]);
1474// setFont_LabelWidget(panelButton, defaultBig_FontId);
1475// setBackgroundColor_Widget(as_Widget(panelButton), uiBackgroundSidebar_ColorId);
1476 iRelease(page);
1477 delete_String(text);
1478 }
1479 destroy_Widget(prefsTabs);
1480 }
1481 iForEach(ObjectList, i, children_Widget(sheet)) {
1482 iWidget *child = i.object;
1483 if (isTwoColumnPage_(child)) {
1484 pushBack_PtrArray(contents, removeChild_Widget(sheet, child));
1485 }
1486 else {
1487 removeChild_Widget(sheet, child);
1488 addChild_Widget(topPanel, child);
1489 iRelease(child);
1490 }
1491 }
1492 const iBool useSlidePanels = (size_PtrArray(contents) == size_PtrArray(panelButtons));
1493 addChildFlags_Widget(sheet, iClob(topPanel),
1494 arrangeVertical_WidgetFlag |
1495 resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag |
1496 overflowScrollable_WidgetFlag |
1497 commandOnClick_WidgetFlag);
1498 setCommandHandler_Widget(topPanel, slidePanelHandler_);
1499 iForEach(PtrArray, j, contents) {
1500 iWidget *owner = topPanel;
1501 if (useSlidePanels) {
1502 /* Create a new child panel. */
1503 iLabelWidget *button = at_PtrArray(panelButtons, index_PtrArrayIterator(&j));
1504 owner = addChildPanel_(sheet, button,
1505 collect_String(upper_String(text_LabelWidget(button))));
1506 }
1507 iWidget *pageContent = j.ptr;
1508 iWidget *headings = child_Widget(pageContent, 0);
1509 iWidget *values = child_Widget(pageContent, 1);
1510 enum iPrefsElement prevElement = panelTitle_PrefsElement;
1511 /* Identify the types of controls in the dialog and restyle/organize them. */
1512 while (!isEmpty_ObjectList(children_Widget(headings))) {
1513 iWidget *heading = child_Widget(headings, 0);
1514 iWidget *value = child_Widget(values, 0);
1515 removeChild_Widget(headings, heading);
1516 removeChild_Widget(values, value);
1517 /* Can we ignore these widgets? */
1518 if (isOmittedPref_(id_Widget(value)) ||
1519 (class_Widget(heading) == &Class_Widget &&
1520 class_Widget(value) == &Class_Widget) /* just padding */) {
1521 iRelease(heading);
1522 iRelease(value);
1523 continue;
1524 }
1525 enum iPrefsElement element = toggle_PrefsElement;
1526 iLabelWidget *headingLabel = NULL;
1527 iLabelWidget *valueLabel = NULL;
1528 iInputWidget *valueInput = NULL;
1529 const iBool isMenuButton = findChild_Widget(value, "menu") != NULL;
1530 if (isInstance_Object(heading, &Class_LabelWidget)) {
1531 headingLabel = (iLabelWidget *) heading;
1532 stripTrailingColon_(headingLabel);
1533 }
1534 if (isInstance_Object(value, &Class_LabelWidget)) {
1535 valueLabel = (iLabelWidget *) value;
1536 setFont_LabelWidget(valueLabel, defaultBig_FontId);
1537 }
1538 if (isInstance_Object(value, &Class_InputWidget)) {
1539 valueInput = (iInputWidget *) value;
1540 setFlags_Widget(value, borderBottom_WidgetFlag, iFalse);
1541 element = textInput_PrefsElement;
1542 }
1543 if (childCount_Widget(value) >= 2) {
1544 if (isInstance_Object(child_Widget(value, 0), &Class_InputWidget)) {
1545 element = textInput_PrefsElement;
1546 setPadding_Widget(value, 0, 0, gap_UI, 0);
1547 valueInput = child_Widget(value, 0);
1548 }
1549 }
1550 if (valueInput) {
1551 setFont_InputWidget(valueInput, defaultBig_FontId);
1552 setContentPadding_InputWidget(valueInput, 3 * gap_UI, 0);
1553 }
1554 /* Toggles have the button on the right. */
1555 if (valueLabel && cmp_String(command_LabelWidget(valueLabel), "toggle") == 0) {
1556 element = toggle_PrefsElement;
1557 addPanelChild_(owner,
1558 iClob(makeValuePaddingWithHeading_(headingLabel, value)),
1559 0,
1560 element,
1561 prevElement);
1562 }
1563 else if (valueLabel && isEmpty_String(text_LabelWidget(valueLabel))) {
1564 element = heading_PrefsElement;
1565 iRelease(value);
1566 addPanelChild_(owner, iClob(heading), 0, element, prevElement);
1567 setFont_LabelWidget(headingLabel, uiLabel_FontId);
1568 }
1569 else if (isMenuButton) {
1570 element = dropdown_PrefsElement;
1571 setFlags_Widget(value,
1572 alignRight_WidgetFlag | noBackground_WidgetFlag |
1573 frameless_WidgetFlag, iTrue);
1574 setFlags_Widget(value, alignLeft_WidgetFlag, iFalse);
1575 iWidget *pad = addPanelChild_(owner, iClob(makeValuePaddingWithHeading_(headingLabel, value)), 0,
1576 element, prevElement);
1577 pad->padding[2] = gap_UI;
1578 }
1579 else if (valueInput) {
1580 addPanelChild_(owner, iClob(makeValuePaddingWithHeading_(headingLabel, value)), 0,
1581 element, prevElement);
1582 }
1583 else {
1584 if (childCount_Widget(value) >= 2) {
1585 element = radioButton_PrefsElement;
1586 /* Always padding before radio buttons. */
1587 addChild_Widget(owner, iClob(makePadding_Widget(lineHeight_Text(defaultBig_FontId))));
1588 }
1589 addChildFlags_Widget(owner, iClob(heading), borderBottom_WidgetFlag);
1590 if (headingLabel) {
1591 setTextColor_LabelWidget(headingLabel, uiSubheading_ColorId);
1592 setText_LabelWidget(headingLabel,
1593 collect_String(upper_String(text_LabelWidget(headingLabel))));
1594 }
1595 addPanelChild_(owner, iClob(value), 0, element, prevElement);
1596 /* Radio buttons expand to fill the space. */
1597 if (element == radioButton_PrefsElement) {
1598 setBackgroundColor_Widget(value, uiBackgroundSidebar_ColorId);
1599 setPadding_Widget(value, 4 * gap_UI, 2 * gap_UI, 4 * gap_UI, 2 * gap_UI);
1600 setFlags_Widget(value,
1601 borderBottom_WidgetFlag |
1602 resizeToParentWidth_WidgetFlag |
1603 resizeWidthOfChildren_WidgetFlag,
1604 iTrue);
1605 iForEach(ObjectList, sub, children_Widget(value)) {
1606 if (isInstance_Object(sub.object, &Class_LabelWidget)) {
1607 iLabelWidget *opt = sub.object;
1608 setFont_LabelWidget(opt, defaultMedium_FontId);
1609 setFlags_Widget(as_Widget(opt), noBackground_WidgetFlag, iTrue);
1610 }
1611 }
1612 }
1613 }
1614 prevElement = element;
1615 }
1616 addPanelChild_(owner, NULL, 0, 0, prevElement);
1617 destroy_Widget(pageContent);
1618 setFlags_Widget(owner, drawBackgroundToBottom_WidgetFlag, iTrue);
1619 }
1620 destroyPending_Root(sheet->root);
1621 /* Additional elements for preferences. */
1622 if (isPrefs) {
1623 addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(defaultBig_FontId))));
1624 addChildFlags_Widget(topPanel,
1625 iClob(makePanelButton_(info_Icon " ${menu.help}", "!open url:about:help")),
1626 borderTop_WidgetFlag);
1627 iLabelWidget *aboutButton = addChildFlags_Widget(topPanel,
1628 iClob(makePanelButton_(planet_Icon " ${menu.about}", "panel.open")),
1629 chevron_WidgetFlag);
1630 /* The About panel. */ {
1631 iWidget *panel = addChildPanel_(sheet, aboutButton, NULL);
1632 iString *msg = collectNew_String();
1633 setCStr_String(msg, "Lagrange " LAGRANGE_APP_VERSION);
1634#if defined (iPlatformAppleMobile)
1635 appendCStr_String(msg, " (" LAGRANGE_IOS_VERSION ")");
1636#endif
1637 addChild_Widget(panel, iClob(new_LabelWidget(cstr_String(msg), NULL)));
1638 addChildFlags_Widget(panel,
1639 iClob(makePanelButton_(globe_Icon " By @jk@skyjake.fi",
1640 "!open url:https://skyjake.fi/@jk")),
1641 borderTop_WidgetFlag);
1642 addChildFlags_Widget(panel,
1643 iClob(makePanelButton_(clock_Icon " ${menu.releasenotes}",
1644 "!open url:about:version")),
1645 0);
1646 addChildFlags_Widget(panel,
1647 iClob(makePanelButton_(info_Icon " ${menu.aboutpages}",
1648 "!open url:about:about")),
1649 0);
1650 addChildFlags_Widget(panel,
1651 iClob(makePanelButton_(bug_Icon " ${menu.debug}",
1652 "!open url:about:debug")),
1653 0);
1654 }
1655 }
1656 else {
1657 setFlags_Widget(topPanel, overflowScrollable_WidgetFlag, iTrue);
1658 /* Update heading style. */
1659 setFont_LabelWidget((iLabelWidget *) dialogHeading, uiLabelLargeBold_FontId);
1660 setFlags_Widget(dialogHeading, alignLeft_WidgetFlag, iTrue);
1661 }
1662 if (findChild_Widget(sheet, "valueinput.prompt")) {
1663 iWidget *prompt = findChild_Widget(sheet, "valueinput.prompt");
1664 setFlags_Widget(prompt, alignLeft_WidgetFlag, iTrue);
1665 iInputWidget *input = findChild_Widget(sheet, "input");
1666 removeChild_Widget(parent_Widget(input), input);
1667 addChild_Widget(topPanel, iClob(makeValuePadding_(as_Widget(input))));
1668 }
1669 /* Top padding for each panel, to account for the overlaid navbar. */ {
1670 setId_Widget(addChildPos_Widget(topPanel,
1671 iClob(makePadding_Widget(0)), front_WidgetAddPos),
1672 "panel.toppad");
1673 }
1674 /* Navbar. */ {
1675 iWidget *navi = new_Widget();
1676 setId_Widget(navi, "panel.navi");
1677 setBackgroundColor_Widget(navi, uiBackground_ColorId);
1678 addChild_Widget(navi, iClob(makePadding_Widget(0)));
1679 iLabelWidget *back = addChildFlags_Widget(navi,
1680 iClob(new_LabelWidget(leftAngle_Icon " ${panel.back}", "panel.close")),
1681 noBackground_WidgetFlag | frameless_WidgetFlag |
1682 alignLeft_WidgetFlag | extraPadding_WidgetFlag);
1683 checkIcon_LabelWidget(back);
1684 setId_Widget(as_Widget(back), "panel.back");
1685 setFont_LabelWidget(back, defaultBig_FontId);
1686 if (!isPrefs) {
1687 /* Pick up the dialog buttons for the navbar. */
1688 iWidget *buttons = findChild_Widget(sheet, "dialogbuttons");
1689 iLabelWidget *cancel = findMenuItem_Widget(buttons, "cancel");
1690// if (!cancel) {
1691// cancel = findMenuItem_Widget(buttons, "translation.cancel");
1692// }
1693 if (cancel) {
1694 updateText_LabelWidget(back, text_LabelWidget(cancel));
1695 setCommand_LabelWidget(back, command_LabelWidget(cancel));
1696 }
1697 iLabelWidget *def = (iLabelWidget *) lastChild_Widget(buttons);
1698 if (def && !cancel) {
1699 updateText_LabelWidget(back, text_LabelWidget(def));
1700 setCommand_LabelWidget(back, command_LabelWidget(def));
1701 setFlags_Widget(as_Widget(back), alignLeft_WidgetFlag, iFalse);
1702 setFlags_Widget(as_Widget(back), alignRight_WidgetFlag, iTrue);
1703 setIcon_LabelWidget(back, 0);
1704 setFont_LabelWidget(back, defaultBigBold_FontId);
1705 }
1706 else if (def != cancel) {
1707 removeChild_Widget(buttons, def);
1708 setFont_LabelWidget(def, defaultBigBold_FontId);
1709 setFlags_Widget(as_Widget(def),
1710 frameless_WidgetFlag | extraPadding_WidgetFlag |
1711 noBackground_WidgetFlag, iTrue);
1712 addChildFlags_Widget(as_Widget(back), iClob(def), moveToParentRightEdge_WidgetFlag);
1713 updateSize_LabelWidget(def);
1714 }
1715 /* Action buttons are added in the bottom as extra buttons. */ {
1716 iBool isFirstAction = iTrue;
1717 iForEach(ObjectList, i, children_Widget(buttons)) {
1718 if (isInstance_Object(i.object, &Class_LabelWidget) &&
1719 i.object != cancel && i.object != def) {
1720 iLabelWidget *item = i.object;
1721 setBackgroundColor_Widget(i.object, uiBackgroundSidebar_ColorId);
1722 setFont_LabelWidget(item, defaultBig_FontId);
1723 removeChild_Widget(buttons, item);
1724 addChildFlags_Widget(topPanel, iClob(item), panelButtonFlags |
1725 (isFirstAction ? borderTop_WidgetFlag : 0));
1726 updateSize_LabelWidget(item);
1727 isFirstAction = iFalse;
1728 }
1729 }
1730 }
1731 iRelease(removeChild_Widget(parent_Widget(buttons), buttons));
1732 /* Styling for remaining elements. */
1733 iForEach(ObjectList, i, children_Widget(topPanel)) {
1734 if (isInstance_Object(i.object, &Class_LabelWidget) &&
1735 isEmpty_String(command_LabelWidget(i.object)) &&
1736 isEmpty_String(id_Widget(i.object))) {
1737 setFlags_Widget(i.object, alignLeft_WidgetFlag, iTrue);
1738 if (font_LabelWidget(i.object) == uiLabel_FontId) {
1739 setFont_LabelWidget(i.object, uiContent_FontId);
1740 }
1741 }
1742 }
1743 }
1744 addChildFlags_Widget(sheet, iClob(navi),
1745 drawBackgroundToVerticalSafeArea_WidgetFlag |
1746 arrangeHeight_WidgetFlag | resizeWidthOfChildren_WidgetFlag |
1747 resizeToParentWidth_WidgetFlag | arrangeVertical_WidgetFlag);
1748 }
1749 updateSheetPanelMetrics_(sheet);
1750 iAssert(sheet->parent);
1751 arrange_Widget(sheet->parent);
1752 postCommand_App("widget.overflow"); /* with the correct dimensions */
1753// printTree_Widget(sheet);
1754 }
1755 else {
1756 arrange_Widget(sheet);
1757 }
1758 postRefresh_App();
1759}
1760
1761void makeFilePath_Widget(iWidget * parent,
1762 const iString *initialPath,
1763 const char * title,
1764 const char * acceptLabel,
1765 const char * command) {
1766 setFocus_Widget(NULL);
1767// processEvents_App(postedEventsOnly_AppEventMode);
1768 iWidget *dlg = makeSheet_Widget(command);
1769 setCommandHandler_Widget(dlg, filePathHandler_);
1770 addChild_Widget(parent, iClob(dlg));
1771 addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag);
1772 iInputWidget *input = addChild_Widget(dlg, iClob(new_InputWidget(0)));
1773 if (initialPath) {
1774 setText_InputWidget(input, collect_String(makeRelative_Path(initialPath)));
1775 }
1776 setId_Widget(as_Widget(input), "input");
1777 as_Widget(input)->rect.size.x = dlg->rect.size.x;
1778 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
1779 iWidget *div = new_Widget(); {
1780 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
1781 addChild_Widget(div, iClob(newKeyMods_LabelWidget("${cancel}", SDLK_ESCAPE, 0, "filepath.cancel")));
1782 addChild_Widget(div, iClob(newKeyMods_LabelWidget(acceptLabel, SDLK_RETURN, 0, "filepath.accept")));
1783 }
1784 addChild_Widget(dlg, iClob(div));
1785 finalizeSheet_Widget(dlg);
1786 setFocus_Widget(as_Widget(input));
1787}
1788
1789static void acceptValueInput_(iWidget *dlg) { 1136static void acceptValueInput_(iWidget *dlg) {
1790 const iInputWidget *input = findChild_Widget(dlg, "input"); 1137 const iInputWidget *input = findChild_Widget(dlg, "input");
1791 if (!isEmpty_String(id_Widget(dlg))) { 1138 if (!isEmpty_String(id_Widget(dlg))) {
@@ -1806,7 +1153,8 @@ static void updateValueInputWidth_(iWidget *dlg) {
1806 dlg->rect.size.x = rootSize.x; 1153 dlg->rect.size.x = rootSize.x;
1807 } 1154 }
1808 else { 1155 else {
1809 dlg->rect.size.x = iMaxi(iMaxi(rootSize.x / 2, title->rect.size.x), prompt->rect.size.x); 1156 dlg->rect.size.x =
1157 iMin(rootSize.x, iMaxi(iMaxi(100 * gap_UI, title->rect.size.x), prompt->rect.size.x));
1810 } 1158 }
1811} 1159}
1812 1160
@@ -1880,6 +1228,12 @@ iWidget *makeDialogButtons_Widget(const iMenuItem *actions, size_t numActions) {
1880 if (*label == '*' || *label == '&') { 1228 if (*label == '*' || *label == '&') {
1881 continue; /* Special value selection items for a Question dialog. */ 1229 continue; /* Special value selection items for a Question dialog. */
1882 } 1230 }
1231 if (startsWith_CStr(label, "```")) {
1232 /* Annotation. */
1233 iLabelWidget *annotation = addChild_Widget(div, iClob(new_LabelWidget(label + 3, NULL)));
1234 setTextColor_LabelWidget(annotation, uiTextAction_ColorId);
1235 continue;
1236 }
1883 if (!iCmpStr(label, "---")) { 1237 if (!iCmpStr(label, "---")) {
1884 /* Separator.*/ 1238 /* Separator.*/
1885 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); 1239 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag);
@@ -1945,7 +1299,7 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con
1945 iClob(makeDialogButtons_Widget( 1299 iClob(makeDialogButtons_Widget(
1946 (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, { acceptLabel, 0, 0, "valueinput.accept" } }, 1300 (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, { acceptLabel, 0, 0, "valueinput.accept" } },
1947 2))); 1301 2)));
1948 finalizeSheet_Widget(dlg); 1302 finalizeSheet_Mobile(dlg);
1949 if (parent) { 1303 if (parent) {
1950 setFocus_Widget(as_Widget(input)); 1304 setFocus_Widget(as_Widget(input));
1951 } 1305 }
@@ -2002,13 +1356,17 @@ iWidget *makeQuestion_Widget(const char *title, const char *msg,
2002 const iMenuItem *item = &items[i]; 1356 const iMenuItem *item = &items[i];
2003 const char first = item->label[0]; 1357 const char first = item->label[0];
2004 if (first == '*' || first == '&') { 1358 if (first == '*' || first == '&') {
2005 addChildFlags_Widget(dlg, 1359 iLabelWidget *option =
1360 addChildFlags_Widget(dlg,
2006 iClob(newKeyMods_LabelWidget(item->label + 1, 1361 iClob(newKeyMods_LabelWidget(item->label + 1,
2007 item->key, 1362 item->key,
2008 item->kmods, 1363 item->kmods,
2009 item->command)), 1364 item->command)),
2010 resizeToParentWidth_WidgetFlag | 1365 resizeToParentWidth_WidgetFlag |
2011 (first == '&' ? selected_WidgetFlag : 0)); 1366 (first == '&' ? selected_WidgetFlag : 0));
1367 if (deviceType_App() != desktop_AppDeviceType) {
1368 setFont_LabelWidget(option, defaultBig_FontId);
1369 }
2012 } 1370 }
2013 } 1371 }
2014 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); 1372 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
@@ -2016,7 +1374,7 @@ iWidget *makeQuestion_Widget(const char *title, const char *msg,
2016 addChild_Widget(dlg->root->widget, iClob(dlg)); 1374 addChild_Widget(dlg->root->widget, iClob(dlg));
2017 arrange_Widget(dlg); /* BUG: This extra arrange shouldn't be needed but the dialog won't 1375 arrange_Widget(dlg); /* BUG: This extra arrange shouldn't be needed but the dialog won't
2018 be arranged correctly unless it's here. */ 1376 be arranged correctly unless it's here. */
2019 finalizeSheet_Widget(dlg); 1377 finalizeSheet_Mobile(dlg);
2020 return dlg; 1378 return dlg;
2021} 1379}
2022 1380
@@ -2155,7 +1513,7 @@ static int cmp_MenuItem_(const void *e1, const void *e2) {
2155#endif 1513#endif
2156 1514
2157void updatePreferencesLayout_Widget(iWidget *prefs) { 1515void updatePreferencesLayout_Widget(iWidget *prefs) {
2158 if (!prefs || deviceType_App() == phone_AppDeviceType) { 1516 if (!prefs || deviceType_App() != desktop_AppDeviceType) {
2159 return; 1517 return;
2160 } 1518 }
2161 /* Doing manual layout here because the widget arranging logic isn't sophisticated enough. */ 1519 /* Doing manual layout here because the widget arranging logic isn't sophisticated enough. */
@@ -2248,14 +1606,16 @@ iWidget *makePreferences_Widget(void) {
2248 addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoverlink"))); 1606 addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoverlink")));
2249 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.archive.openindex}"))); 1607 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.archive.openindex}")));
2250 addChild_Widget(values, iClob(makeToggle_Widget("prefs.archive.openindex"))); 1608 addChild_Widget(values, iClob(makeToggle_Widget("prefs.archive.openindex")));
2251 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.pinsplit}"))); 1609 if (deviceType_App() != phone_AppDeviceType) {
2252 iWidget *pinSplit = new_Widget(); 1610 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.pinsplit}")));
2253 /* Split mode document pinning. */ { 1611 iWidget *pinSplit = new_Widget();
2254 addRadioButton_(pinSplit, "prefs.pinsplit.0", "${prefs.pinsplit.none}", "pinsplit.set arg:0"); 1612 /* Split mode document pinning. */ {
2255 addRadioButton_(pinSplit, "prefs.pinsplit.1", "${prefs.pinsplit.left}", "pinsplit.set arg:1"); 1613 addRadioButton_(pinSplit, "prefs.pinsplit.0", "${prefs.pinsplit.none}", "pinsplit.set arg:0");
2256 addRadioButton_(pinSplit, "prefs.pinsplit.2", "${prefs.pinsplit.right}", "pinsplit.set arg:2"); 1614 addRadioButton_(pinSplit, "prefs.pinsplit.1", "${prefs.pinsplit.left}", "pinsplit.set arg:1");
2257 } 1615 addRadioButton_(pinSplit, "prefs.pinsplit.2", "${prefs.pinsplit.right}", "pinsplit.set arg:2");
2258 addChildFlags_Widget(values, iClob(pinSplit), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 1616 }
1617 addChildFlags_Widget(values, iClob(pinSplit), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1618 }
2259 addChild_Widget(headings, iClob(makePadding_Widget(bigGap))); 1619 addChild_Widget(headings, iClob(makePadding_Widget(bigGap)));
2260 addChild_Widget(values, iClob(makePadding_Widget(bigGap))); 1620 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
2261 /* UI languages. */ { 1621 /* UI languages. */ {
@@ -2268,6 +1628,7 @@ iWidget *makePreferences_Widget(void) {
2268 { "${lang.fr} - fr", 0, 0, "uilang id:fr" }, 1628 { "${lang.fr} - fr", 0, 0, "uilang id:fr" },
2269 { "${lang.ia} - ia", 0, 0, "uilang id:ia" }, 1629 { "${lang.ia} - ia", 0, 0, "uilang id:ia" },
2270 { "${lang.ie} - ie", 0, 0, "uilang id:ie" }, 1630 { "${lang.ie} - ie", 0, 0, "uilang id:ie" },
1631 { "${lang.pl} - pl", 0, 0, "uilang id:pl" },
2271 { "${lang.ru} - ru", 0, 0, "uilang id:ru" }, 1632 { "${lang.ru} - ru", 0, 0, "uilang id:ru" },
2272 { "${lang.sr} - sr", 0, 0, "uilang id:sr" }, 1633 { "${lang.sr} - sr", 0, 0, "uilang id:sr" },
2273 { "${lang.tok} - tok", 0, 0, "uilang id:tok" }, 1634 { "${lang.tok} - tok", 0, 0, "uilang id:tok" },
@@ -2475,7 +1836,8 @@ iWidget *makePreferences_Widget(void) {
2475 iClob(makeDialogButtons_Widget( 1836 iClob(makeDialogButtons_Widget(
2476 (iMenuItem[]){ { "${dismiss}", SDLK_ESCAPE, 0, "prefs.dismiss" } }, 1))); 1837 (iMenuItem[]){ { "${dismiss}", SDLK_ESCAPE, 0, "prefs.dismiss" } }, 1)));
2477 addChild_Widget(dlg->root->widget, iClob(dlg)); 1838 addChild_Widget(dlg->root->widget, iClob(dlg));
2478 finalizeSheet_Widget(dlg); 1839 finalizeSheet_Mobile(dlg);
1840 setupSheetTransition_Mobile(dlg, iTrue);
2479// printTree_Widget(dlg); 1841// printTree_Widget(dlg);
2480 return dlg; 1842 return dlg;
2481} 1843}
@@ -2519,15 +1881,10 @@ iWidget *makeBookmarkEditor_Widget(void) {
2519 "bmed.accept" } }, 1881 "bmed.accept" } },
2520 2))); 1882 2)));
2521 addChild_Widget(get_Root()->widget, iClob(dlg)); 1883 addChild_Widget(get_Root()->widget, iClob(dlg));
2522 finalizeSheet_Widget(dlg); 1884 finalizeSheet_Mobile(dlg);
2523 return dlg; 1885 return dlg;
2524} 1886}
2525 1887
2526static void enableSidebars_(void) {
2527 setFlags_Widget(findWidget_App("sidebar"), disabled_WidgetFlag, iFalse);
2528 setFlags_Widget(findWidget_App("sidebar2"), disabled_WidgetFlag, iFalse);
2529}
2530
2531static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, const char *cmd) { 1888static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, const char *cmd) {
2532 if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) { 1889 if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) {
2533 if (equal_Command(cmd, "bmed.accept")) { 1890 if (equal_Command(cmd, "bmed.accept")) {
@@ -2552,8 +1909,6 @@ static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, cons
2552 postCommand_App("bookmarks.changed"); 1909 postCommand_App("bookmarks.changed");
2553 } 1910 }
2554 destroy_Widget(editor); 1911 destroy_Widget(editor);
2555 /* Sidebars are disabled when a dialog is opened. */
2556 enableSidebars_();
2557 return iTrue; 1912 return iTrue;
2558 } 1913 }
2559 return iFalse; 1914 return iFalse;
@@ -2583,8 +1938,6 @@ iWidget *makeBookmarkCreation_Widget(const iString *url, const iString *title, i
2583static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) { 1938static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
2584 if (equal_Command(cmd, "cancel")) { 1939 if (equal_Command(cmd, "cancel")) {
2585 destroy_Widget(dlg); 1940 destroy_Widget(dlg);
2586 /* Sidebars are disabled when a dialog is opened. */
2587 enableSidebars_();
2588 return iTrue; 1941 return iTrue;
2589 } 1942 }
2590 if (equal_Command(cmd, "feedcfg.accept")) { 1943 if (equal_Command(cmd, "feedcfg.accept")) {
@@ -2619,8 +1972,6 @@ static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
2619 } 1972 }
2620 postCommand_App("bookmarks.changed"); 1973 postCommand_App("bookmarks.changed");
2621 destroy_Widget(dlg); 1974 destroy_Widget(dlg);
2622 /* Sidebars are disabled when a dialog is opened. */
2623 enableSidebars_();
2624 return iTrue; 1975 return iTrue;
2625 } 1976 }
2626 return iFalse; 1977 return iFalse;
@@ -2659,7 +2010,7 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) {
2659 arrange_Widget(dlg); 2010 arrange_Widget(dlg);
2660 as_Widget(input)->rect.size.x = 100 * gap_UI - headings->rect.size.x; 2011 as_Widget(input)->rect.size.x = 100 * gap_UI - headings->rect.size.x;
2661 addChild_Widget(get_Root()->widget, iClob(dlg)); 2012 addChild_Widget(get_Root()->widget, iClob(dlg));
2662 finalizeSheet_Widget(dlg); 2013 finalizeSheet_Mobile(dlg);
2663 /* Initialize. */ { 2014 /* Initialize. */ {
2664 const iBookmark *bm = bookmarkId ? get_Bookmarks(bookmarks_App(), bookmarkId) : NULL; 2015 const iBookmark *bm = bookmarkId ? get_Bookmarks(bookmarks_App(), bookmarkId) : NULL;
2665 setText_InputWidget(findChild_Widget(dlg, "feedcfg.title"), 2016 setText_InputWidget(findChild_Widget(dlg, "feedcfg.title"),
@@ -2735,7 +2086,7 @@ iWidget *makeIdentityCreation_Widget(void) {
2735 "ident.accept" } }, 2086 "ident.accept" } },
2736 2))); 2087 2)));
2737 addChild_Widget(get_Root()->widget, iClob(dlg)); 2088 addChild_Widget(get_Root()->widget, iClob(dlg));
2738 finalizeSheet_Widget(dlg); 2089 finalizeSheet_Mobile(dlg);
2739 return dlg; 2090 return dlg;
2740} 2091}
2741 2092
@@ -2786,6 +2137,7 @@ int languageIndex_CStr(const char *langId) {
2786iWidget *makeTranslation_Widget(iWidget *parent) { 2137iWidget *makeTranslation_Widget(iWidget *parent) {
2787 iWidget *dlg = makeSheet_Widget("xlt"); 2138 iWidget *dlg = makeSheet_Widget("xlt");
2788 setFlags_Widget(dlg, keepOnTop_WidgetFlag, iFalse); 2139 setFlags_Widget(dlg, keepOnTop_WidgetFlag, iFalse);
2140 dlg->minSize.x = 70 * gap_UI;
2789 setCommandHandler_Widget(dlg, translationHandler_); 2141 setCommandHandler_Widget(dlg, translationHandler_);
2790 addChildFlags_Widget(dlg, 2142 addChildFlags_Widget(dlg,
2791 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.translate}", NULL)), 2143 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.translate}", NULL)),
@@ -2831,6 +2183,6 @@ iWidget *makeTranslation_Widget(iWidget *parent) {
2831 2))); 2183 2)));
2832 addChild_Widget(parent, iClob(dlg)); 2184 addChild_Widget(parent, iClob(dlg));
2833 arrange_Widget(dlg); 2185 arrange_Widget(dlg);
2834 finalizeSheet_Widget(dlg); 2186 finalizeSheet_Mobile(dlg);
2835 return dlg; 2187 return dlg;
2836} 2188}
diff --git a/src/ui/util.h b/src/ui/util.h
index 50845280..43aeb172 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -22,6 +22,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#pragma once 23#pragma once
24 24
25#include "mobile.h"
26
25#include <the_Foundation/string.h> 27#include <the_Foundation/string.h>
26#include <the_Foundation/rect.h> 28#include <the_Foundation/rect.h>
27#include <the_Foundation/vec2.h> 29#include <the_Foundation/vec2.h>
@@ -255,9 +257,8 @@ size_t tabCount_Widget (const iWidget *tabs);
255 257
256/*-----------------------------------------------------------------------------------------------*/ 258/*-----------------------------------------------------------------------------------------------*/
257 259
258iWidget * makeSheet_Widget (const char *id); 260iWidget * makeSheet_Widget (const char *id);
259void finalizeSheet_Widget (iWidget *sheet); 261iWidget * makeDialogButtons_Widget (const iMenuItem *actions, size_t numActions);
260iWidget * makeDialogButtons_Widget(const iMenuItem *actions, size_t numActions);
261 262
262iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values, 263iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values,
263 const char *labelText, const char *inputId, 264 const char *labelText, const char *inputId,
diff --git a/src/ui/widget.c b/src/ui/widget.c
index c1c920d2..3439fb1b 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -23,6 +23,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
23#include "widget.h" 23#include "widget.h"
24 24
25#include "app.h" 25#include "app.h"
26#include "periodic.h"
26#include "touch.h" 27#include "touch.h"
27#include "command.h" 28#include "command.h"
28#include "paint.h" 29#include "paint.h"
@@ -39,6 +40,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
39# include "../ios.h" 40# include "../ios.h"
40#endif 41#endif
41 42
43static void printInfo_Widget_(const iWidget *);
44
42void releaseChildren_Widget(iWidget *d) { 45void releaseChildren_Widget(iWidget *d) {
43 iForEach(ObjectList, i, d->children) { 46 iForEach(ObjectList, i, d->children) {
44 ((iWidget *) i.object)->parent = NULL; /* the actual reference being held */ 47 ((iWidget *) i.object)->parent = NULL; /* the actual reference being held */
@@ -55,6 +58,7 @@ void init_Widget(iWidget *d) {
55 d->rect = zero_Rect(); 58 d->rect = zero_Rect();
56 d->minSize = zero_I2(); 59 d->minSize = zero_I2();
57 d->sizeRef = NULL; 60 d->sizeRef = NULL;
61 d->offsetRef = NULL;
58 d->bgColor = none_ColorId; 62 d->bgColor = none_ColorId;
59 d->frameColor = none_ColorId; 63 d->frameColor = none_ColorId;
60 init_Anim(&d->visualOffset, 0.0f); 64 init_Anim(&d->visualOffset, 0.0f);
@@ -92,6 +96,7 @@ void deinit_Widget(iWidget *d) {
92} 96}
93 97
94static void aboutToBeDestroyed_Widget_(iWidget *d) { 98static void aboutToBeDestroyed_Widget_(iWidget *d) {
99 d->flags |= destroyPending_WidgetFlag;
95 if (isFocused_Widget(d)) { 100 if (isFocused_Widget(d)) {
96 setFocus_Widget(NULL); 101 setFocus_Widget(NULL);
97 return; 102 return;
@@ -99,6 +104,7 @@ static void aboutToBeDestroyed_Widget_(iWidget *d) {
99 if (flags_Widget(d) & keepOnTop_WidgetFlag) { 104 if (flags_Widget(d) & keepOnTop_WidgetFlag) {
100 removeOne_PtrArray(onTop_Root(d->root), d); 105 removeOne_PtrArray(onTop_Root(d->root), d);
101 } 106 }
107 remove_Periodic(periodic_App(), d);
102 if (isHover_Widget(d)) { 108 if (isHover_Widget(d)) {
103 get_Window()->hover = NULL; 109 get_Window()->hover = NULL;
104 } 110 }
@@ -149,6 +155,11 @@ void setFlags_Widget(iWidget *d, int64_t flags, iBool set) {
149 removeOne_PtrArray(onTop, d); 155 removeOne_PtrArray(onTop, d);
150 } 156 }
151 } 157 }
158 if (d->flags & arrangeWidth_WidgetFlag &&
159 d->flags & resizeToParentWidth_WidgetFlag) {
160 printf("[Widget] Conflicting flags for ");
161 identify_Widget(d);
162 }
152 } 163 }
153} 164}
154 165
@@ -208,7 +219,7 @@ void setVisualOffset_Widget(iWidget *d, int value, uint32_t span, int animFlags)
208 else { 219 else {
209 setValue_Anim(&d->visualOffset, value, span); 220 setValue_Anim(&d->visualOffset, value, span);
210 d->visualOffset.flags = animFlags; 221 d->visualOffset.flags = animFlags;
211 addTicker_App(visualOffsetAnimation_Widget_, d); 222 addTickerRoot_App(visualOffsetAnimation_Widget_, d->root, d);
212 } 223 }
213} 224}
214 225
@@ -364,10 +375,13 @@ iRect innerBounds_Widget(const iWidget *d) {
364 return ib; 375 return ib;
365} 376}
366 377
367//iLocalDef iBool isArranged_Widget_(const iWidget *d) { 378iRect innerBoundsWithoutVisualOffset_Widget(const iWidget *d) {
368// return !isCollapsed_Widget_(d) && ~d->flags & fixedPosition_WidgetFlag; 379 iRect ib = adjusted_Rect(boundsWithoutVisualOffset_Widget(d),
369//} 380 init_I2(d->padding[0], d->padding[1]),
370 381 init_I2(-d->padding[2], -d->padding[3]));
382 ib.size = max_I2(zero_I2(), ib.size);
383 return ib;
384}
371 385
372static size_t numArrangedChildren_Widget_(const iWidget *d) { 386static size_t numArrangedChildren_Widget_(const iWidget *d) {
373 size_t count = 0; 387 size_t count = 0;
@@ -469,12 +483,19 @@ static void arrange_Widget_(iWidget *d) {
469 const int expCount = numExpandingChildren_Widget_(d); 483 const int expCount = numExpandingChildren_Widget_(d);
470 TRACE(d, "%d expanding children", expCount); 484 TRACE(d, "%d expanding children", expCount);
471 /* Resize children to fill the parent widget. */ 485 /* Resize children to fill the parent widget. */
486 iAssert((d->flags & (resizeToParentWidth_WidgetFlag | arrangeWidth_WidgetFlag)) !=
487 (resizeToParentWidth_WidgetFlag | arrangeWidth_WidgetFlag));
472 if (d->flags & resizeChildren_WidgetFlag) { 488 if (d->flags & resizeChildren_WidgetFlag) {
473 const iInt2 dirs = init_I2((d->flags & resizeWidthOfChildren_WidgetFlag) != 0, 489 const iInt2 dirs = init_I2((d->flags & resizeWidthOfChildren_WidgetFlag) != 0,
474 (d->flags & resizeHeightOfChildren_WidgetFlag) != 0); 490 (d->flags & resizeHeightOfChildren_WidgetFlag) != 0);
475#if !defined (NDEBUG) 491#if !defined (NDEBUG)
476 /* Check for conflicting flags. */ 492 /* Check for conflicting flags. */
477 if (dirs.x) iAssert(~d->flags & arrangeWidth_WidgetFlag); 493 if (dirs.x) {
494 if (d->flags & arrangeWidth_WidgetFlag) {
495 identify_Widget(d);
496 }
497 iAssert(~d->flags & arrangeWidth_WidgetFlag);
498 }
478 if (dirs.y) iAssert(~d->flags & arrangeHeight_WidgetFlag); 499 if (dirs.y) iAssert(~d->flags & arrangeHeight_WidgetFlag);
479#endif 500#endif
480 TRACE(d, "resize children, x:%d y:%d (own size: %dx%d)", dirs.x, dirs.y, 501 TRACE(d, "resize children, x:%d y:%d (own size: %dx%d)", dirs.x, dirs.y,
@@ -697,26 +718,32 @@ static void arrange_Widget_(iWidget *d) {
697 TRACE(d, "END"); 718 TRACE(d, "END");
698} 719}
699 720
700#if 0 721static void resetArrangement_Widget_(iWidget *d) {
701void resetSize_Widget(iWidget *d) {
702 if (~d->flags & fixedWidth_WidgetFlag) {
703 d->rect.size.x = d->minSize.x;
704 }
705 if (~d->flags & fixedHeight_WidgetFlag) {
706 d->rect.size.y = d->minSize.y;
707 }
708 iForEach(ObjectList, i, children_Widget(d)) { 722 iForEach(ObjectList, i, children_Widget(d)) {
709 iWidget *child = as_Widget(i.object); 723 iWidget *child = as_Widget(i.object);
710 if (isArrangedSize_Widget_(child)) { 724 resetArrangement_Widget_(child);
711 resetSize_Widget(child); 725 if (isArrangedPos_Widget_(child)) {
726 if (d->flags & arrangeHorizontal_WidgetFlag) {
727 child->rect.pos.x = 0;
728 }
729 if (d->flags & resizeWidthOfChildren_WidgetFlag && child->flags & expand_WidgetFlag) {
730 child->rect.size.x = 0;
731 }
732 if (d->flags & arrangeVertical_WidgetFlag) {
733 child->rect.pos.y = 0;
734 }
735 if (d->flags & resizeHeightOfChildren_WidgetFlag && child->flags & expand_WidgetFlag) {
736 child->rect.size.y = 0;
737 }
712 } 738 }
713 } 739 }
714} 740}
715#endif
716 741
717void arrange_Widget(iWidget *d) { 742void arrange_Widget(iWidget *d) {
718 //resetSize_Widget_(d); /* back to initial default sizes */ 743 if (d) {
719 arrange_Widget_(d); 744 resetArrangement_Widget_(d); /* back to initial default sizes */
745 arrange_Widget_(d);
746 }
720} 747}
721 748
722static void applyVisualOffset_Widget_(const iWidget *d, iInt2 *pos) { 749static void applyVisualOffset_Widget_(const iWidget *d, iInt2 *pos) {
@@ -729,6 +756,16 @@ static void applyVisualOffset_Widget_(const iWidget *d, iInt2 *pos) {
729 pos->y += off; 756 pos->y += off;
730 } 757 }
731 } 758 }
759 if (d->flags & refChildrenOffset_WidgetFlag) {
760 iConstForEach(ObjectList, i, children_Widget(d->offsetRef)) {
761 const iWidget *child = i.object;
762 if (child == d) continue;
763 if (child->flags & (visualOffset_WidgetFlag | dragged_WidgetFlag)) {
764 const int invOff = size_Root(d->root).x - iRound(value_Anim(&child->visualOffset));
765 pos->x -= invOff / 4;
766 }
767 }
768 }
732} 769}
733 770
734iRect bounds_Widget(const iWidget *d) { 771iRect bounds_Widget(const iWidget *d) {
@@ -919,18 +956,23 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
919 } 956 }
920 } 957 }
921 if (class_Widget(d)->processEvent(d, ev)) { 958 if (class_Widget(d)->processEvent(d, ev)) {
959 iAssert(get_Root() == d->root);
922 return iTrue; 960 return iTrue;
923 } 961 }
924 } 962 }
963 iAssert(get_Root() == d->root);
925 return iFalse; 964 return iFalse;
926} 965}
927 966
928static iBool scrollOverflow_Widget_(iWidget *d, int delta) { 967iBool scrollOverflow_Widget(iWidget *d, int delta) {
929 iRect bounds = bounds_Widget(d); 968 iRect bounds = boundsWithoutVisualOffset_Widget(d);
930 const iInt2 rootSize = size_Root(d->root); 969 const iInt2 rootSize = size_Root(d->root);
931 const iRect winRect = safeRect_Root(d->root); 970 const iRect winRect = safeRect_Root(d->root);
932 const int yTop = top_Rect(winRect); 971 const int yTop = top_Rect(winRect);
933 const int yBottom = bottom_Rect(winRect); 972 const int yBottom = bottom_Rect(winRect);
973 if (top_Rect(bounds) >= yTop && bottom_Rect(bounds) < yBottom) {
974 return iFalse; /* fits inside just fine */
975 }
934 //const int safeBottom = rootSize.y - yBottom; 976 //const int safeBottom = rootSize.y - yBottom;
935 bounds.pos.y += delta; 977 bounds.pos.y += delta;
936 const iRangei range = { bottom_Rect(winRect) - height_Rect(bounds), yTop }; 978 const iRangei range = { bottom_Rect(winRect) - height_Rect(bounds), yTop };
@@ -978,7 +1020,7 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
978 if (!isPerPixel_MouseWheelEvent(&ev->wheel)) { 1020 if (!isPerPixel_MouseWheelEvent(&ev->wheel)) {
979 step *= lineHeight_Text(uiLabel_FontId); 1021 step *= lineHeight_Text(uiLabel_FontId);
980 } 1022 }
981 if (scrollOverflow_Widget_(d, step)) { 1023 if (scrollOverflow_Widget(d, step)) {
982 return iTrue; 1024 return iTrue;
983 } 1025 }
984 } 1026 }
@@ -987,10 +1029,11 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
987 if (d->flags & overflowScrollable_WidgetFlag && 1029 if (d->flags & overflowScrollable_WidgetFlag &&
988 ~d->flags & visualOffset_WidgetFlag && 1030 ~d->flags & visualOffset_WidgetFlag &&
989 isCommand_UserEvent(ev, "widget.overflow")) { 1031 isCommand_UserEvent(ev, "widget.overflow")) {
990 scrollOverflow_Widget_(d, 0); /* check bounds */ 1032 scrollOverflow_Widget(d, 0); /* check bounds */
991 } 1033 }
992 if (ev->user.code == command_UserEventCode && d->commandHandler && 1034 if (ev->user.code == command_UserEventCode && d->commandHandler &&
993 d->commandHandler(d, ev->user.data1)) { 1035 d->commandHandler(d, ev->user.data1)) {
1036 iAssert(get_Root() == d->root);
994 return iTrue; 1037 return iTrue;
995 } 1038 }
996 break; 1039 break;
@@ -1043,11 +1086,18 @@ void drawBackground_Widget(const iWidget *d) {
1043 init_Paint(&p); 1086 init_Paint(&p);
1044 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30); 1087 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30);
1045 } 1088 }
1046 1089 const iBool isFaded = fadeBackground &&
1047 if (fadeBackground && ~d->flags & noFadeBackground_WidgetFlag) { 1090 ~d->flags & noFadeBackground_WidgetFlag &&
1091 ~d->flags & destroyPending_WidgetFlag;
1092 if (isFaded) {
1048 iPaint p; 1093 iPaint p;
1049 init_Paint(&p); 1094 init_Paint(&p);
1050 p.alpha = 0x50; 1095 p.alpha = 0x50;
1096 if (flags_Widget(d) & (visualOffset_WidgetFlag | dragged_WidgetFlag)) {
1097 const float area = d->rect.size.x * d->rect.size.y;
1098 const float visibleArea = area_Rect(intersect_Rect(bounds_Widget(d), rect_Root(d->root)));
1099 p.alpha *= (area > 0 ? visibleArea / area : 0.0f);
1100 }
1051 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); 1101 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
1052 int fadeColor; 1102 int fadeColor;
1053 switch (colorTheme_App()) { 1103 switch (colorTheme_App()) {
@@ -1061,9 +1111,7 @@ void drawBackground_Widget(const iWidget *d) {
1061 fadeColor = gray50_ColorId; 1111 fadeColor = gray50_ColorId;
1062 break; 1112 break;
1063 } 1113 }
1064 fillRect_Paint(&p, 1114 fillRect_Paint(&p, rect_Root(d->root), fadeColor);
1065 rect_Root(d->root),
1066 fadeColor);
1067 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); 1115 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
1068 } 1116 }
1069 if (d->bgColor >= 0 || d->frameColor >= 0) { 1117 if (d->bgColor >= 0 || d->frameColor >= 0) {
@@ -1319,6 +1367,22 @@ iAny *findParentClass_Widget(const iWidget *d, const iAnyClass *class) {
1319 return i; 1367 return i;
1320} 1368}
1321 1369
1370iAny *findOverflowScrollable_Widget(iWidget *d) {
1371 const iRect rootRect = rect_Root(d->root);
1372 for (iWidget *w = d; w; w = parent_Widget(w)) {
1373 if (flags_Widget(w) & overflowScrollable_WidgetFlag) {
1374 const iRect bounds = boundsWithoutVisualOffset_Widget(w);
1375 if ((bottom_Rect(bounds) > bottom_Rect(rootRect) ||
1376 top_Rect(bounds) < top_Rect(rootRect)) &&
1377 !hasVisibleChildOnTop_Widget(w)) {
1378 return w;
1379 }
1380 return NULL;
1381 }
1382 }
1383 return NULL;
1384}
1385
1322size_t childCount_Widget(const iWidget *d) { 1386size_t childCount_Widget(const iWidget *d) {
1323 if (!d->children) return 0; 1387 if (!d->children) return 0;
1324 return size_ObjectList(d->children); 1388 return size_ObjectList(d->children);
@@ -1567,7 +1631,8 @@ static void printInfo_Widget_(const iWidget *d) {
1567 cstr_String(text_LabelWidget((const iLabelWidget *) d)), 1631 cstr_String(text_LabelWidget((const iLabelWidget *) d)),
1568 cstr_String(command_LabelWidget((const iLabelWidget *) d))); 1632 cstr_String(command_LabelWidget((const iLabelWidget *) d)));
1569 } 1633 }
1570 printf("size:%dx%d {min:%dx%d} [%d..%d %d:%d] flags:%08llx%s%s%s%s%s\n", 1634 printf("pos:%d,%d size:%dx%d {min:%dx%d} [%d..%d %d:%d] flags:%08llx%s%s%s%s%s%s%s\n",
1635 d->rect.pos.x, d->rect.pos.y,
1571 d->rect.size.x, d->rect.size.y, 1636 d->rect.size.x, d->rect.size.y,
1572 d->minSize.x, d->minSize.y, 1637 d->minSize.x, d->minSize.y,
1573 d->padding[0], d->padding[2], 1638 d->padding[0], d->padding[2],
@@ -1577,7 +1642,9 @@ static void printInfo_Widget_(const iWidget *d) {
1577 d->flags & tight_WidgetFlag ? " tight" : "", 1642 d->flags & tight_WidgetFlag ? " tight" : "",
1578 d->flags & fixedWidth_WidgetFlag ? " fixW" : "", 1643 d->flags & fixedWidth_WidgetFlag ? " fixW" : "",
1579 d->flags & fixedHeight_WidgetFlag ? " fixH" : "", 1644 d->flags & fixedHeight_WidgetFlag ? " fixH" : "",
1580 d->flags & resizeToParentWidth_WidgetFlag ? " rsPrnW" : ""); 1645 d->flags & resizeToParentWidth_WidgetFlag ? " prnW" : "",
1646 d->flags & arrangeWidth_WidgetFlag ? " aW" : "",
1647 d->flags & resizeWidthOfChildren_WidgetFlag ? " rsWChild" : "");
1581} 1648}
1582 1649
1583static void printTree_Widget_(const iWidget *d, int indent) { 1650static void printTree_Widget_(const iWidget *d, int indent) {
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 5c05e917..79d45f23 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -117,6 +117,9 @@ enum iWidgetFlag {
117#define parentCannotResizeHeight_WidgetFlag iBit64(58) 117#define parentCannotResizeHeight_WidgetFlag iBit64(58)
118#define ignoreForParentWidth_WidgetFlag iBit64(59) 118#define ignoreForParentWidth_WidgetFlag iBit64(59)
119#define noFadeBackground_WidgetFlag iBit64(60) 119#define noFadeBackground_WidgetFlag iBit64(60)
120#define destroyPending_WidgetFlag iBit64(61) /* TODO: needed? */
121#define edgeDraggable_WidgetFlag iBit64(62)
122#define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */
120 123
121enum iWidgetAddPos { 124enum iWidgetAddPos {
122 back_WidgetAddPos, 125 back_WidgetAddPos,
@@ -135,13 +138,14 @@ struct Impl_Widget {
135 iRect rect; 138 iRect rect;
136 iInt2 minSize; 139 iInt2 minSize;
137 iWidget * sizeRef; 140 iWidget * sizeRef;
141 iWidget * offsetRef;
138 int padding[4]; /* left, top, right, bottom */ 142 int padding[4]; /* left, top, right, bottom */
139 iAnim visualOffset; 143 iAnim visualOffset;
140 int bgColor; 144 int bgColor;
141 int frameColor; 145 int frameColor;
142 iObjectList *children; 146 iObjectList *children;
143 iWidget * parent; 147 iWidget * parent;
144 iBool (*commandHandler)(iWidget *, const char *); 148 iBool (*commandHandler)(iWidget *, const char *);
145 iRoot * root; 149 iRoot * root;
146}; 150};
147 151
@@ -193,6 +197,7 @@ iAny * findChild_Widget (const iWidget *, const char *id);
193const iPtrArray *findChildren_Widget (const iWidget *, const char *id); 197const iPtrArray *findChildren_Widget (const iWidget *, const char *id);
194iAny * findParentClass_Widget (const iWidget *, const iAnyClass *class); 198iAny * findParentClass_Widget (const iWidget *, const iAnyClass *class);
195iAny * findFocusable_Widget (const iWidget *startFrom, enum iWidgetFocusDir focusDir); 199iAny * findFocusable_Widget (const iWidget *startFrom, enum iWidgetFocusDir focusDir);
200iAny * findOverflowScrollable_Widget (iWidget *);
196size_t childCount_Widget (const iWidget *); 201size_t childCount_Widget (const iWidget *);
197void draw_Widget (const iWidget *); 202void draw_Widget (const iWidget *);
198void drawBackground_Widget (const iWidget *); 203void drawBackground_Widget (const iWidget *);
@@ -262,6 +267,7 @@ iAny * child_Widget (iWidget *, size_t index); /* O(n) */
262size_t childIndex_Widget (const iWidget *, const iAnyObject *child); /* O(n) */ 267size_t childIndex_Widget (const iWidget *, const iAnyObject *child); /* O(n) */
263void arrange_Widget (iWidget *); 268void arrange_Widget (iWidget *);
264void resetSize_Widget (iWidget *); 269void resetSize_Widget (iWidget *);
270iBool scrollOverflow_Widget (iWidget *, int delta); /* moves the widget */
265iBool dispatchEvent_Widget (iWidget *, const SDL_Event *); 271iBool dispatchEvent_Widget (iWidget *, const SDL_Event *);
266iBool processEvent_Widget (iWidget *, const SDL_Event *); 272iBool processEvent_Widget (iWidget *, const SDL_Event *);
267void postCommand_Widget (const iAnyObject *, const char *cmd, ...); 273void postCommand_Widget (const iAnyObject *, const char *cmd, ...);
diff --git a/src/ui/window.c b/src/ui/window.c
index 99430a05..abdc363d 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -171,19 +171,6 @@ int numRoots_Window(const iWindow *d) {
171 return num; 171 return num;
172} 172}
173 173
174static void setupUserInterface_Window(iWindow *d) {
175#if defined (iPlatformAppleDesktop)
176 insertMacMenus_();
177#endif
178 /* One root is created by default. */
179 d->roots[0] = new_Root();
180 setCurrent_Root(d->roots[0]);
181 createUserInterface_Root(d->roots[0]);
182 setCurrent_Root(NULL);
183 /* One of the roots always has keyboard input focus. */
184 d->keyRoot = d->roots[0];
185}
186
187static void windowSizeChanged_Window_(iWindow *d) { 174static void windowSizeChanged_Window_(iWindow *d) {
188 const int numRoots = numRoots_Window(d); 175 const int numRoots = numRoots_Window(d);
189 const iInt2 rootSize = d->size; 176 const iInt2 rootSize = d->size;
@@ -214,6 +201,19 @@ static void windowSizeChanged_Window_(iWindow *d) {
214 } 201 }
215} 202}
216 203
204static void setupUserInterface_Window(iWindow *d) {
205#if defined (iPlatformAppleDesktop)
206 insertMacMenus_();
207#endif
208 /* One root is created by default. */
209 d->roots[0] = new_Root();
210 setCurrent_Root(d->roots[0]);
211 createUserInterface_Root(d->roots[0]);
212 setCurrent_Root(NULL);
213 /* One of the roots always has keyboard input focus. */
214 d->keyRoot = d->roots[0];
215}
216
217static void updateSize_Window_(iWindow *d, iBool notifyAlways) { 217static void updateSize_Window_(iWindow *d, iBool notifyAlways) {
218 iInt2 *size = &d->size; 218 iInt2 *size = &d->size;
219 const iInt2 oldSize = *size; 219 const iInt2 oldSize = *size;
@@ -515,15 +515,15 @@ void init_Window(iWindow *d, iRect rect) {
515 515
516void deinit_Window(iWindow *d) { 516void deinit_Window(iWindow *d) {
517 iRecycle(); 517 iRecycle();
518 if (theWindow_ == d) {
519 theWindow_ = NULL;
520 }
521 iForIndices(i, d->roots) { 518 iForIndices(i, d->roots) {
522 if (d->roots[i]) { 519 if (d->roots[i]) {
523 setCurrent_Root(d->roots[i]); 520 setCurrent_Root(d->roots[i]);
524 deinit_Root(d->roots[i]); 521 deinit_Root(d->roots[i]);
525 } 522 }
526 } 523 }
524 if (theWindow_ == d) {
525 theWindow_ = NULL;
526 }
527 setCurrent_Root(NULL); 527 setCurrent_Root(NULL);
528 delete_String(d->pendingSplitUrl); 528 delete_String(d->pendingSplitUrl);
529 deinit_Text(); 529 deinit_Text();
@@ -761,7 +761,7 @@ static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
761#if defined (iPlatformMobile) 761#if defined (iPlatformMobile)
762 case SDL_WINDOWEVENT_RESIZED: 762 case SDL_WINDOWEVENT_RESIZED:
763 /* On mobile, this occurs when the display is rotated. */ 763 /* On mobile, this occurs when the display is rotated. */
764 invalidate_Window_(d); 764 invalidate_Window(d);
765 postRefresh_App(); 765 postRefresh_App();
766 return iTrue; 766 return iTrue;
767#endif 767#endif
@@ -772,7 +772,7 @@ static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
772 d->isExposed = iTrue; 772 d->isExposed = iTrue;
773#if defined (iPlatformMobile) 773#if defined (iPlatformMobile)
774 /* Returned to foreground, may have lost buffered content. */ 774 /* Returned to foreground, may have lost buffered content. */
775 invalidate_Window_(d); 775 invalidate_Window(d);
776 postCommand_App("window.unfreeze"); 776 postCommand_App("window.unfreeze");
777#endif 777#endif
778 return iFalse; 778 return iFalse;
@@ -995,7 +995,7 @@ void draw_Window(iWindow *d) {
995 /* Check if root needs resizing. */ { 995 /* Check if root needs resizing. */ {
996 iInt2 renderSize; 996 iInt2 renderSize;
997 SDL_GetRendererOutputSize(d->render, &renderSize.x, &renderSize.y); 997 SDL_GetRendererOutputSize(d->render, &renderSize.x, &renderSize.y);
998 if (!isEqual_I2(renderSize, d->root->rect.size)) { 998 if (!isEqual_I2(renderSize, d->size)) {
999 updateSize_Window_(d, iTrue); 999 updateSize_Window_(d, iTrue);
1000 processEvents_App(postedEventsOnly_AppEventMode); 1000 processEvents_App(postedEventsOnly_AppEventMode);
1001 } 1001 }
@@ -1007,8 +1007,13 @@ void draw_Window(iWindow *d) {
1007 init_Paint(&p); 1007 init_Paint(&p);
1008 /* Clear the window. The clear color is visible as a border around the window 1008 /* Clear the window. The clear color is visible as a border around the window
1009 when the custom frame is being used. */ { 1009 when the custom frame is being used. */ {
1010 setCurrent_Root(d->roots[0]);
1010#if defined (iPlatformAppleMobile) 1011#if defined (iPlatformAppleMobile)
1011 const iColor back = get_Color(tmBackground_ColorId); 1012 iColor back = get_Color(uiBackground_ColorId);
1013 if (deviceType_App() == phone_AppDeviceType) {
1014 /* Page background extends to safe area, so fill it completely. */
1015 back = get_Color(tmBackground_ColorId);
1016 }
1012#else 1017#else
1013 const iColor back = get_Color(gotFocus && d->place.snap != maximized_WindowSnap && 1018 const iColor back = get_Color(gotFocus && d->place.snap != maximized_WindowSnap &&
1014 ~winFlags & SDL_WINDOW_FULLSCREEN_DESKTOP 1019 ~winFlags & SDL_WINDOW_FULLSCREEN_DESKTOP