summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.c30
-rw-r--r--src/feeds.c7
-rw-r--r--src/gmdocument.c86
-rw-r--r--src/gmrequest.c6
-rw-r--r--src/gmutil.c62
-rw-r--r--src/gmutil.h2
-rw-r--r--src/gopher.c10
-rw-r--r--src/gopher.h2
-rw-r--r--src/ios.m32
-rw-r--r--src/macos.m13
-rw-r--r--src/prefs.c4
-rw-r--r--src/resources.c4
-rw-r--r--src/resources.h2
-rw-r--r--src/ui/color.c27
-rw-r--r--src/ui/documentwidget.c58
-rw-r--r--src/ui/indicatorwidget.c66
-rw-r--r--src/ui/inputwidget.c20
-rw-r--r--src/ui/mobile.c4
-rw-r--r--src/ui/root.c31
-rw-r--r--src/ui/root.h2
-rw-r--r--src/ui/sidebarwidget.c5
-rw-r--r--src/ui/text.c42
-rw-r--r--src/ui/text.h2
-rw-r--r--src/ui/touch.c5
-rw-r--r--src/ui/window.c3
25 files changed, 341 insertions, 184 deletions
diff --git a/src/app.c b/src/app.c
index 4045610e..3e6e9cdc 100644
--- a/src/app.c
+++ b/src/app.c
@@ -124,7 +124,7 @@ struct Impl_App {
124 iMimeHooks * mimehooks; 124 iMimeHooks * mimehooks;
125 iGmCerts * certs; 125 iGmCerts * certs;
126 iVisited * visited; 126 iVisited * visited;
127 iBookmarks * bookmarks; 127 iBookmarks * bookmarks;
128 iMainWindow *window; 128 iMainWindow *window;
129 iPtrArray popupWindows; 129 iPtrArray popupWindows;
130 iSortedArray tickers; /* per-frame callbacks, used for animations */ 130 iSortedArray tickers; /* per-frame callbacks, used for animations */
@@ -333,7 +333,9 @@ static const char *dataDir_App_(void) {
333 333
334static const char *downloadDir_App_(void) { 334static const char *downloadDir_App_(void) {
335#if defined (iPlatformAndroidMobile) 335#if defined (iPlatformAndroidMobile)
336 return concatPath_CStr(SDL_AndroidGetInternalStoragePath(), "Downloads"); 336 const char *dir = concatPath_CStr(SDL_AndroidGetExternalStoragePath(), "Downloads");
337 makeDirs_Path(collectNewCStr_String(dir));
338 return dir;
337#endif 339#endif
338#if defined (iPlatformLinux) || defined (iPlatformOther) 340#if defined (iPlatformLinux) || defined (iPlatformOther)
339 /* Parse user-dirs.dirs using the `xdg-user-dir` tool. */ 341 /* Parse user-dirs.dirs using the `xdg-user-dir` tool. */
@@ -759,7 +761,7 @@ static void init_App_(iApp *d, int argc, char **argv) {
759 d->isSuspended = iFalse; 761 d->isSuspended = iFalse;
760 d->tempFilesPendingDeletion = new_StringSet(); 762 d->tempFilesPendingDeletion = new_StringSet();
761 init_CommandLine(&d->args, argc, argv); 763 init_CommandLine(&d->args, argc, argv);
762 /* Where was the app started from? We ask SDL first because the command line alone 764 /* Where was the app started from? We ask SDL first because the command line alone
763 cannot be relied on (behavior differs depending on OS). */ { 765 cannot be relied on (behavior differs depending on OS). */ {
764 char *exec = SDL_GetBasePath(); 766 char *exec = SDL_GetBasePath();
765 if (exec) { 767 if (exec) {
@@ -1268,7 +1270,7 @@ static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *ev
1268 /* SDL regression circa 2.0.18? SDL_PollEvent() doesn't always return 1270 /* SDL regression circa 2.0.18? SDL_PollEvent() doesn't always return
1269 events posted immediately beforehand. Waiting with a very short timeout 1271 events posted immediately beforehand. Waiting with a very short timeout
1270 seems to work better. */ 1272 seems to work better. */
1271#if defined (iPlatformLinux) 1273#if defined (iPlatformLinux) && SDL_VERSION_ATLEAST(2, 0, 18)
1272 return SDL_WaitEventTimeout(event, 1); 1274 return SDL_WaitEventTimeout(event, 1);
1273#else 1275#else
1274 return SDL_PollEvent(event); 1276 return SDL_PollEvent(event);
@@ -1289,9 +1291,10 @@ void processEvents_App(enum iAppEventMode eventMode) {
1289 iRoot *oldCurrentRoot = current_Root(); /* restored afterwards */ 1291 iRoot *oldCurrentRoot = current_Root(); /* restored afterwards */
1290 SDL_Event ev; 1292 SDL_Event ev;
1291 iBool gotEvents = iFalse; 1293 iBool gotEvents = iFalse;
1294 iBool gotRefresh = iFalse;
1292 iPtrArray windows; 1295 iPtrArray windows;
1293 init_PtrArray(&windows); 1296 init_PtrArray(&windows);
1294 while (nextEvent_App_(d, eventMode, &ev)) { 1297 while (nextEvent_App_(d, gotRefresh ? postedEventsOnly_AppEventMode : eventMode, &ev)) {
1295#if defined (iPlatformAppleMobile) 1298#if defined (iPlatformAppleMobile)
1296 if (processEvent_iOS(&ev)) { 1299 if (processEvent_iOS(&ev)) {
1297 continue; 1300 continue;
@@ -1362,6 +1365,10 @@ void processEvents_App(enum iAppEventMode eventMode) {
1362 dispatchCommands_Periodic(&d->periodic); 1365 dispatchCommands_Periodic(&d->periodic);
1363 continue; 1366 continue;
1364 } 1367 }
1368 if (ev.type == SDL_USEREVENT && ev.user.code == refresh_UserEventCode) {
1369 gotRefresh = iTrue;
1370 continue;
1371 }
1365#if defined (LAGRANGE_ENABLE_IDLE_SLEEP) 1372#if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1366 if (ev.type == SDL_USEREVENT && ev.user.code == asleep_UserEventCode) { 1373 if (ev.type == SDL_USEREVENT && ev.user.code == asleep_UserEventCode) {
1367 if (SDL_GetTicks() - d->lastEventTime > idleThreshold_App_ && 1374 if (SDL_GetTicks() - d->lastEventTime > idleThreshold_App_ &&
@@ -1391,6 +1398,16 @@ void processEvents_App(enum iAppEventMode eventMode) {
1391 ev.key.keysym.mod = mapMods_Keys(ev.key.keysym.mod & ~KMOD_CAPS); 1398 ev.key.keysym.mod = mapMods_Keys(ev.key.keysym.mod & ~KMOD_CAPS);
1392 } 1399 }
1393#if defined (iPlatformAndroidMobile) 1400#if defined (iPlatformAndroidMobile)
1401 /* Use the system Back button to close panels, if they're open. */
1402 if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_AC_BACK) {
1403 SDL_UserEvent panelBackCmd = { .type = SDL_USEREVENT,
1404 .code = command_UserEventCode,
1405 .data1 = iDupStr("panel.close"),
1406 .data2 = d->window->base.keyRoot };
1407 if (dispatchEvent_Window(&d->window->base, (SDL_Event *) &panelBackCmd)) {
1408 continue; /* Was handled by someone. */
1409 }
1410 }
1394 /* Ignore all mouse events; just use touch. */ 1411 /* Ignore all mouse events; just use touch. */
1395 if (ev.type == SDL_MOUSEBUTTONDOWN || 1412 if (ev.type == SDL_MOUSEBUTTONDOWN ||
1396 ev.type == SDL_MOUSEBUTTONUP || 1413 ev.type == SDL_MOUSEBUTTONUP ||
@@ -2074,7 +2091,6 @@ iDocumentWidget *document_Command(const char *cmd) {
2074} 2091}
2075 2092
2076iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNew) { 2093iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNew) {
2077 //iApp *d = &app_;
2078 iWidget *tabs = findWidget_Root("doctabs"); 2094 iWidget *tabs = findWidget_Root("doctabs");
2079 setFlags_Widget(tabs, hidden_WidgetFlag, iFalse); 2095 setFlags_Widget(tabs, hidden_WidgetFlag, iFalse);
2080 iWidget *newTabButton = findChild_Widget(tabs, "newtab"); 2096 iWidget *newTabButton = findChild_Widget(tabs, "newtab");
@@ -2090,6 +2106,7 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe
2090 iRelease(doc); /* now owned by the tabs */ 2106 iRelease(doc); /* now owned by the tabs */
2091 addTabCloseButton_Widget(tabs, as_Widget(doc), "tabs.close"); 2107 addTabCloseButton_Widget(tabs, as_Widget(doc), "tabs.close");
2092 addChild_Widget(findChild_Widget(tabs, "tabs.buttons"), iClob(newTabButton)); 2108 addChild_Widget(findChild_Widget(tabs, "tabs.buttons"), iClob(newTabButton));
2109 showOrHideNewTabButton_Root(tabs->root);
2093 if (switchToNew) { 2110 if (switchToNew) {
2094 postCommandf_App("tabs.switch page:%p", doc); 2111 postCommandf_App("tabs.switch page:%p", doc);
2095 } 2112 }
@@ -2840,6 +2857,7 @@ iBool handleCommand_App(const char *cmd) {
2840 return iTrue; 2857 return iTrue;
2841 } 2858 }
2842 iDocumentWidget *doc = document_Command(cmd); 2859 iDocumentWidget *doc = document_Command(cmd);
2860 iAssert(doc);
2843 iDocumentWidget *origin = doc; 2861 iDocumentWidget *origin = doc;
2844 if (hasLabel_Command(cmd, "origin")) { 2862 if (hasLabel_Command(cmd, "origin")) {
2845 iDocumentWidget *cmdOrig = findWidget_App(cstr_Command(cmd, "origin")); 2863 iDocumentWidget *cmdOrig = findWidget_App(cstr_Command(cmd, "origin"));
diff --git a/src/feeds.c b/src/feeds.c
index a8cbf47a..7b679dc1 100644
--- a/src/feeds.c
+++ b/src/feeds.c
@@ -275,7 +275,8 @@ static void save_Feeds_(iFeeds *d) {
275 if (open_File(f, write_FileMode | text_FileMode)) { 275 if (open_File(f, write_FileMode | text_FileMode)) {
276 lock_Mutex(d->mtx); 276 lock_Mutex(d->mtx);
277 iString *str = new_String(); 277 iString *str = new_String();
278 format_String(str, "%llu\n# Feeds\n", integralSeconds_Time(&d->lastRefreshedAt)); 278 format_String(str, "%llu\n# Feeds\n", (unsigned long long)
279 integralSeconds_Time(&d->lastRefreshedAt));
279 write_File(f, utf8_String(str)); 280 write_File(f, utf8_String(str));
280 /* Index of feeds for IDs. */ { 281 /* Index of feeds for IDs. */ {
281 iConstForEach(PtrArray, i, listSubscriptions_()) { 282 iConstForEach(PtrArray, i, listSubscriptions_()) {
@@ -296,8 +297,8 @@ static void save_Feeds_(iFeeds *d) {
296 } 297 }
297 format_String(str, "%x\n%llu\n%llu\n%s\n%s\n", 298 format_String(str, "%x\n%llu\n%llu\n%s\n%s\n",
298 entry->bookmarkId, 299 entry->bookmarkId,
299 integralSeconds_Time(&entry->posted), 300 (unsigned long long) integralSeconds_Time(&entry->posted),
300 integralSeconds_Time(&entry->discovered), 301 (unsigned long long) integralSeconds_Time(&entry->discovered),
301 cstr_String(&entry->url), 302 cstr_String(&entry->url),
302 cstr_String(&entry->title)); 303 cstr_String(&entry->title));
303 write_File(f, utf8_String(str)); 304 write_File(f, utf8_String(str));
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 19230392..b5e71e21 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -547,9 +547,11 @@ static void clear_RunTypesetter_(iRunTypesetter *d) {
547 clear_Array(&d->layout); 547 clear_Array(&d->layout);
548} 548}
549 549
550static void commit_RunTypesetter_(iRunTypesetter *d, iGmDocument *doc) { 550static size_t commit_RunTypesetter_(iRunTypesetter *d, iGmDocument *doc) {
551 const size_t n = size_Array(&d->layout);
551 pushBackN_Array(&doc->layout, constData_Array(&d->layout), size_Array(&d->layout)); 552 pushBackN_Array(&doc->layout, constData_Array(&d->layout), size_Array(&d->layout));
552 clear_RunTypesetter_(d); 553 clear_RunTypesetter_(d);
554 return n;
553} 555}
554 556
555static const int maxLedeLines_ = 10; 557static const int maxLedeLines_ = 10;
@@ -611,6 +613,10 @@ static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange,
611} 613}
612 614
613static void doLayout_GmDocument_(iGmDocument *d) { 615static void doLayout_GmDocument_(iGmDocument *d) {
616 static iRegExp *ansiPattern_;
617 if (!ansiPattern_) {
618 ansiPattern_ = makeAnsiEscapePattern_Text(iTrue /* with ESC */);
619 }
614 const iPrefs *prefs = prefs_App(); 620 const iPrefs *prefs = prefs_App();
615 const iBool isMono = isForcedMonospace_GmDocument_(d); 621 const iBool isMono = isForcedMonospace_GmDocument_(d);
616 const iBool isGopher = isGopher_GmDocument_(d); 622 const iBool isGopher = isGopher_GmDocument_(d);
@@ -618,8 +624,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
618 const iBool isVeryNarrow = d->size.x <= 70 * gap_Text; 624 const iBool isVeryNarrow = d->size.x <= 70 * gap_Text;
619 const iBool isExtremelyNarrow = d->size.x <= 60 * gap_Text; 625 const iBool isExtremelyNarrow = d->size.x <= 60 * gap_Text;
620 const iBool isFullWidthImages = (d->outsideMargin < 5 * gap_UI); 626 const iBool isFullWidthImages = (d->outsideMargin < 5 * gap_UI);
621// const iBool isDarkBg = isDark_GmDocumentTheme( 627
622// isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
623 initTheme_GmDocument_(d); 628 initTheme_GmDocument_(d);
624 d->isLayoutInvalidated = iFalse; 629 d->isLayoutInvalidated = iFalse;
625 /* TODO: Collect these parameters into a GmTheme. */ 630 /* TODO: Collect these parameters into a GmTheme. */
@@ -657,7 +662,6 @@ static void doLayout_GmDocument_(iGmDocument *d) {
657 const iArray *oldPreMeta = collect_Array(copy_Array(&d->preMeta)); /* remember fold states */ 662 const iArray *oldPreMeta = collect_Array(copy_Array(&d->preMeta)); /* remember fold states */
658 clear_Array(&d->preMeta); 663 clear_Array(&d->preMeta);
659 clear_String(&d->title); 664 clear_String(&d->title);
660// clear_String(&d->bannerText);
661 if (d->size.x <= 0 || isEmpty_String(&d->source)) { 665 if (d->size.x <= 0 || isEmpty_String(&d->source)) {
662 return; 666 return;
663 } 667 }
@@ -671,7 +675,6 @@ static void doLayout_GmDocument_(iGmDocument *d) {
671 int preFont = preformatted_FontId; 675 int preFont = preformatted_FontId;
672 uint16_t preId = 0; 676 uint16_t preId = 0;
673 iBool enableIndents = iFalse; 677 iBool enableIndents = iFalse;
674// iBool addSiteBanner = d->bannerType != none_GmDocumentBanner;
675 const iBool isNormalized = isNormalized_GmDocument_(d); 678 const iBool isNormalized = isNormalized_GmDocument_(d);
676 enum iGmLineType prevType = text_GmLineType; 679 enum iGmLineType prevType = text_GmLineType;
677 enum iGmLineType prevNonBlankType = text_GmLineType; 680 enum iGmLineType prevNonBlankType = text_GmLineType;
@@ -755,7 +758,6 @@ static void doLayout_GmDocument_(iGmDocument *d) {
755 if (d->format == gemini_SourceFormat && 758 if (d->format == gemini_SourceFormat &&
756 startsWithSc_Rangecc(line, "```", &iCaseSensitive)) { 759 startsWithSc_Rangecc(line, "```", &iCaseSensitive)) {
757 isPreformat = iFalse; 760 isPreformat = iFalse;
758// addSiteBanner = iFalse; /* overrides the banner */
759 continue; 761 continue;
760 } 762 }
761 run.mediaType = max_MediaType; /* preformatted block */ 763 run.mediaType = max_MediaType; /* preformatted block */
@@ -763,28 +765,6 @@ static void doLayout_GmDocument_(iGmDocument *d) {
763 run.font = (d->format == plainText_SourceFormat ? plainText_FontId : preFont); 765 run.font = (d->format == plainText_SourceFormat ? plainText_FontId : preFont);
764 indent = indents[type]; 766 indent = indents[type];
765 } 767 }
766#if 0
767 if (addSiteBanner) {
768 addSiteBanner = iFalse;
769 const iRangecc bannerText = urlHost_String(&d->url);
770 if (!isEmpty_Range(&bannerText)) {
771 setRange_String(&d->bannerText, bannerText);
772 iGmRun banner = { .flags = decoration_GmRunFlag | siteBanner_GmRunFlag };
773 banner.bounds = zero_Rect();
774 banner.visBounds = init_Rect(0, 0, d->size.x, lineHeight_Text(banner_FontId) * 2);
775 if (d->bannerType == certificateWarning_GmDocumentBanner) {
776 banner.visBounds.size.y += iMaxi(6000 * lineHeight_Text(uiLabel_FontId) /
777 d->size.x, lineHeight_Text(uiLabel_FontId) * 5);
778 }
779 banner.text = bannerText;
780 banner.font = banner_FontId;
781 banner.color = tmBannerTitle_ColorId;
782 pushBack_Array(&d->layout, &banner);
783 pos.y += height_Rect(banner.visBounds) +
784 1.5f * lineHeight_Text(paragraph_FontId) * prefs->lineSpacing;
785 }
786 }
787#endif
788 /* Empty lines don't produce text runs. */ 768 /* Empty lines don't produce text runs. */
789 if (isEmpty_Range(&line)) { 769 if (isEmpty_Range(&line)) {
790 if (type == quote_GmLineType && !prefs->quoteIcon) { 770 if (type == quote_GmLineType && !prefs->quoteIcon) {
@@ -865,6 +845,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
865 if ((type == heading1_GmLineType || type == heading2_GmLineType) && 845 if ((type == heading1_GmLineType || type == heading2_GmLineType) &&
866 isEmpty_String(&d->title)) { 846 isEmpty_String(&d->title)) {
867 setRange_String(&d->title, line); 847 setRange_String(&d->title, line);
848 /* Get rid of ANSI escapes. */
849 replaceRegExp_String(&d->title, ansiPattern_, "", NULL, NULL);
868 } 850 }
869 /* List bullet. */ 851 /* List bullet. */
870 if (type == bullet_GmLineType) { 852 if (type == bullet_GmLineType) {
@@ -964,6 +946,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
964 } 946 }
965 } 947 }
966 iAssert(!isEmpty_Range(&line)); /* must have something at this point */ 948 iAssert(!isEmpty_Range(&line)); /* must have something at this point */
949 size_t numRunsAdded = 0;
967 /* Typeset the paragraph. */ { 950 /* Typeset the paragraph. */ {
968 iRunTypesetter rts; 951 iRunTypesetter rts;
969 init_RunTypesetter_(&rts); 952 init_RunTypesetter_(&rts);
@@ -1036,7 +1019,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
1036 : 1.0f); 1019 : 1.0f);
1037 } 1020 }
1038 } 1021 }
1039 commit_RunTypesetter_(&rts, d); 1022 numRunsAdded = commit_RunTypesetter_(&rts, d);
1040 break; 1023 break;
1041 } 1024 }
1042 /* Try again... */ 1025 /* Try again... */
@@ -1050,6 +1033,11 @@ static void doLayout_GmDocument_(iGmDocument *d) {
1050 deinit_RunTypesetter_(&rts); 1033 deinit_RunTypesetter_(&rts);
1051 } 1034 }
1052 /* Flag the end of line, too. */ 1035 /* Flag the end of line, too. */
1036 if (numRunsAdded == 0) {
1037 pos.y += lineHeight_Text(run.font) * prefs->lineSpacing;
1038 followsBlank = iTrue;
1039 continue;
1040 }
1053 iGmRun *lastRun = back_Array(&d->layout); 1041 iGmRun *lastRun = back_Array(&d->layout);
1054 lastRun->flags |= endOfLine_GmRunFlag; 1042 lastRun->flags |= endOfLine_GmRunFlag;
1055 if (lastRun->linkId && lastRun->flags & startOfLine_GmRunFlag) { 1043 if (lastRun->linkId && lastRun->flags & startOfLine_GmRunFlag) {
@@ -1301,7 +1289,7 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
1301 0x203b, 0x2042, 0x205c, 0x2182, 0x25ed, 0x2600, 0x2601, 0x2604, 0x2605, 0x2606, 1289 0x203b, 0x2042, 0x205c, 0x2182, 0x25ed, 0x2600, 0x2601, 0x2604, 0x2605, 0x2606,
1302 0x265c, 0x265e, 0x2690, 0x2691, 0x2693, 0x2698, 0x2699, 0x26f0, 0x270e, 0x2728, 1290 0x265c, 0x265e, 0x2690, 0x2691, 0x2693, 0x2698, 0x2699, 0x26f0, 0x270e, 0x2728,
1303 0x272a, 0x272f, 0x2731, 0x2738, 0x273a, 0x273e, 0x2740, 0x2742, 0x2744, 0x2748, 1291 0x272a, 0x272f, 0x2731, 0x2738, 0x273a, 0x273e, 0x2740, 0x2742, 0x2744, 0x2748,
1304 0x274a, 0x2751, 0x2756, 0x2766, 0x27bd, 0x27c1, 0x27d0, 0x2b19, 0x1f300, 0x1f303, 1292 0x274a, 0x2318, 0x2756, 0x2766, 0x27bd, 0x27c1, 0x27d0, 0x2b19, 0x1f300, 0x1f303,
1305 0x1f306, 0x1f308, 0x1f30a, 0x1f319, 0x1f31f, 0x1f320, 0x1f340, 0x1f4cd, 0x1f4e1, 0x1f531, 1293 0x1f306, 0x1f308, 0x1f30a, 0x1f319, 0x1f31f, 0x1f320, 0x1f340, 0x1f4cd, 0x1f4e1, 0x1f531,
1306 0x1f533, 0x1f657, 0x1f659, 0x1f665, 0x1f668, 0x1f66b, 0x1f78b, 0x1f796, 0x1f79c, 1294 0x1f533, 0x1f657, 0x1f659, 0x1f665, 0x1f668, 0x1f66b, 0x1f78b, 0x1f796, 0x1f79c,
1307 }; 1295 };
@@ -1951,44 +1939,6 @@ void setUrl_GmDocument(iGmDocument *d, const iString *url) {
1951 } 1939 }
1952} 1940}
1953 1941
1954int replaceRegExp_String(iString *d, const iRegExp *regexp, const char *replacement,
1955 void (*matchHandler)(void *, const iRegExpMatch *),
1956 void *context) {
1957 iRegExpMatch m;
1958 iString result;
1959 int numMatches = 0;
1960 const char *pos = constBegin_String(d);
1961 init_RegExpMatch(&m);
1962 init_String(&result);
1963 while (matchString_RegExp(regexp, d, &m)) {
1964 appendRange_String(&result, (iRangecc){ pos, begin_RegExpMatch(&m) });
1965 /* Replace any capture group back-references. */
1966 for (const char *ch = replacement; *ch; ch++) {
1967 if (*ch == '\\') {
1968 ch++;
1969 if (*ch == '\\') {
1970 appendCStr_String(&result, "\\");
1971 }
1972 else if (*ch >= '0' && *ch <= '9') {
1973 appendRange_String(&result, capturedRange_RegExpMatch(&m, *ch - '0'));
1974 }
1975 }
1976 else {
1977 appendData_Block(&result.chars, ch, 1);
1978 }
1979 }
1980 if (matchHandler) {
1981 matchHandler(context, &m);
1982 }
1983 pos = end_RegExpMatch(&m);
1984 numMatches++;
1985 }
1986 appendRange_String(&result, (iRangecc){ pos, constEnd_String(d) });
1987 set_String(d, &result);
1988 deinit_String(&result);
1989 return numMatches;
1990}
1991
1992iDeclareType(PendingLink) 1942iDeclareType(PendingLink)
1993struct Impl_PendingLink { 1943struct Impl_PendingLink {
1994 iString *url; 1944 iString *url;
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 3d5a4aef..82c232e1 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -358,6 +358,12 @@ static const iBlock *aboutPageSource_(iRangecc path, iRangecc query) {
358 if (equalCase_Rangecc(path, "version")) { 358 if (equalCase_Rangecc(path, "version")) {
359 return &blobVersion_Resources; 359 return &blobVersion_Resources;
360 } 360 }
361 if (equalCase_Rangecc(path, "version-1.5")) {
362 return &blobVersion_1_5_Resources;
363 }
364 if (equalCase_Rangecc(path, "version-0.13")) {
365 return &blobVersion_0_13_Resources;
366 }
361 if (equalCase_Rangecc(path, "debug")) { 367 if (equalCase_Rangecc(path, "debug")) {
362 return utf8_String(debugInfo_App()); 368 return utf8_String(debugInfo_App());
363 } 369 }
diff --git a/src/gmutil.c b/src/gmutil.c
index 98e4d4d6..e59e6649 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -131,6 +131,16 @@ static iRangecc prevPathSeg_(const char *end, const char *start) {
131 return seg; 131 return seg;
132} 132}
133 133
134void stripUrlPort_String(iString *d) {
135 iUrl parts;
136 init_Url(&parts, d);
137 if (!isEmpty_Range(&parts.port)) {
138 /* Always preceded by a colon. */
139 remove_Block(&d->chars, parts.port.start - 1 - constBegin_String(d),
140 size_Range(&parts.port) + 1);
141 }
142}
143
134void stripDefaultUrlPort_String(iString *d) { 144void stripDefaultUrlPort_String(iString *d) {
135 iUrl parts; 145 iUrl parts;
136 init_Url(&parts, d); 146 init_Url(&parts, d);
@@ -248,6 +258,9 @@ iRangecc urlRoot_String(const iString *d) {
248 else { 258 else {
249 iUrl parts; 259 iUrl parts;
250 init_Url(&parts, d); 260 init_Url(&parts, d);
261 if (equalCase_Rangecc(parts.scheme, "about")) {
262 return (iRangecc){ constBegin_String(d), parts.path.start };
263 }
251 rootEnd = parts.path.start; 264 rootEnd = parts.path.start;
252 } 265 }
253 return (iRangecc){ constBegin_String(d), rootEnd }; 266 return (iRangecc){ constBegin_String(d), rootEnd };
@@ -681,6 +694,17 @@ const iString *withSpacesEncoded_String(const iString *d) {
681 return d; 694 return d;
682} 695}
683 696
697const iString *withScheme_String(const iString *d, const char *scheme) {
698 iUrl parts;
699 init_Url(&parts, d);
700 if (!equalCase_Rangecc(parts.scheme, scheme)) {
701 iString *repl = collectNewCStr_String(scheme);
702 appendRange_String(repl, (iRangecc){ parts.scheme.end, constEnd_String(d) });
703 return repl;
704 }
705 return d;
706}
707
684const iString *canonicalUrl_String(const iString *d) { 708const iString *canonicalUrl_String(const iString *d) {
685 /* The "canonical" form, used for internal storage and comparisons, is: 709 /* The "canonical" form, used for internal storage and comparisons, is:
686 - all non-reserved characters decoded (i.e., it's an IRI) 710 - all non-reserved characters decoded (i.e., it's an IRI)
@@ -880,3 +904,41 @@ const iGmError *get_GmError(enum iGmStatusCode code) {
880 iAssert(errors_[0].code == unknownStatusCode_GmStatusCode); 904 iAssert(errors_[0].code == unknownStatusCode_GmStatusCode);
881 return &errors_[0].err; /* unknown */ 905 return &errors_[0].err; /* unknown */
882} 906}
907
908int replaceRegExp_String(iString *d, const iRegExp *regexp, const char *replacement,
909 void (*matchHandler)(void *, const iRegExpMatch *),
910 void *context) {
911 iRegExpMatch m;
912 iString result;
913 int numMatches = 0;
914 const char *pos = constBegin_String(d);
915 init_RegExpMatch(&m);
916 init_String(&result);
917 while (matchString_RegExp(regexp, d, &m)) {
918 appendRange_String(&result, (iRangecc){ pos, begin_RegExpMatch(&m) });
919 /* Replace any capture group back-references. */
920 for (const char *ch = replacement; *ch; ch++) {
921 if (*ch == '\\') {
922 ch++;
923 if (*ch == '\\') {
924 appendCStr_String(&result, "\\");
925 }
926 else if (*ch >= '0' && *ch <= '9') {
927 appendRange_String(&result, capturedRange_RegExpMatch(&m, *ch - '0'));
928 }
929 }
930 else {
931 appendData_Block(&result.chars, ch, 1);
932 }
933 }
934 if (matchHandler) {
935 matchHandler(context, &m);
936 }
937 pos = end_RegExpMatch(&m);
938 numMatches++;
939 }
940 appendRange_String(&result, (iRangecc){ pos, constEnd_String(d) });
941 set_String(d, &result);
942 deinit_String(&result);
943 return numMatches;
944}
diff --git a/src/gmutil.h b/src/gmutil.h
index 15bb7b2e..1594afc4 100644
--- a/src/gmutil.h
+++ b/src/gmutil.h
@@ -127,6 +127,7 @@ iBool isKnownScheme_Rangecc (iRangecc scheme); /* any URI scheme */
127iBool isKnownUrlScheme_Rangecc(iRangecc scheme); /* URL schemes only */ 127iBool isKnownUrlScheme_Rangecc(iRangecc scheme); /* URL schemes only */
128void punyEncodeDomain_Rangecc(iRangecc domain, iString *encoded_out); 128void punyEncodeDomain_Rangecc(iRangecc domain, iString *encoded_out);
129void punyEncodeUrlHost_String(iString *absoluteUrl); 129void punyEncodeUrlHost_String(iString *absoluteUrl);
130void stripUrlPort_String (iString *);
130void stripDefaultUrlPort_String(iString *); 131void stripDefaultUrlPort_String(iString *);
131const iString * urlFragmentStripped_String(const iString *); 132const iString * urlFragmentStripped_String(const iString *);
132const iString * urlQueryStripped_String (const iString *); 133const iString * urlQueryStripped_String (const iString *);
@@ -138,6 +139,7 @@ const char * makeFileUrl_CStr (const char *localFilePath);
138iString * localFilePathFromUrl_String(const iString *); 139iString * localFilePathFromUrl_String(const iString *);
139void urlEncodeSpaces_String (iString *); 140void urlEncodeSpaces_String (iString *);
140const iString * withSpacesEncoded_String(const iString *); 141const iString * withSpacesEncoded_String(const iString *);
142const iString * withScheme_String (const iString *, const char *scheme); /* replace URI scheme */
141const iString * canonicalUrl_String (const iString *); 143const iString * canonicalUrl_String (const iString *);
142 144
143const char * mediaType_Path (const iString *path); 145const char * mediaType_Path (const iString *path);
diff --git a/src/gopher.c b/src/gopher.c
index 008a7743..0e34fe6a 100644
--- a/src/gopher.c
+++ b/src/gopher.c
@@ -299,3 +299,13 @@ iBool processResponse_Gopher(iGopher *d, const iBlock *data) {
299 } 299 }
300 return changed; 300 return changed;
301} 301}
302
303void setUrlItemType_Gopher(iString *url, char itemType) {
304 iUrl parts;
305 init_Url(&parts, url);
306 if (equalCase_Rangecc(parts.scheme, "gopher")) {
307 if (parts.path.start && size_Range(&parts.path) >= 2) {
308 ((char *) parts.path.start)[1] = itemType;
309 }
310 }
311}
diff --git a/src/gopher.h b/src/gopher.h
index 3ad7e374..3cad0c21 100644
--- a/src/gopher.h
+++ b/src/gopher.h
@@ -44,3 +44,5 @@ iDeclareTypeConstruction(Gopher)
44void open_Gopher (iGopher *, const iString *url); 44void open_Gopher (iGopher *, const iString *url);
45iBool processResponse_Gopher (iGopher *, const iBlock *data); 45iBool processResponse_Gopher (iGopher *, const iBlock *data);
46void cancel_Gopher (iGopher *); 46void cancel_Gopher (iGopher *);
47
48void setUrlItemType_Gopher (iString *url, char itemType);
diff --git a/src/ios.m b/src/ios.m
index 82596ffd..68c03827 100644
--- a/src/ios.m
+++ b/src/ios.m
@@ -163,7 +163,8 @@ API_AVAILABLE(ios(13.0))
163 163
164/*----------------------------------------------------------------------------------------------*/ 164/*----------------------------------------------------------------------------------------------*/
165 165
166@interface AppState : NSObject<UIDocumentPickerDelegate, UITextFieldDelegate, UITextViewDelegate> { 166@interface AppState : NSObject<UIDocumentPickerDelegate, UITextFieldDelegate, UITextViewDelegate,
167 UIScrollViewDelegate> {
167 iString *fileBeingSaved; 168 iString *fileBeingSaved;
168 iString *pickFileCommand; 169 iString *pickFileCommand;
169 iSystemTextInput *sysCtrl; 170 iSystemTextInput *sysCtrl;
@@ -173,6 +174,7 @@ API_AVAILABLE(ios(13.0))
173@end 174@end
174 175
175static AppState *appState_; 176static AppState *appState_;
177static UIScrollView *statusBarTapper_; /* dummy scroll view just for getting notified of taps */
176 178
177@implementation AppState 179@implementation AppState
178 180
@@ -310,8 +312,15 @@ replacementString:(NSString *)string {
310 notifyChange_SystemTextInput_(sysCtrl); 312 notifyChange_SystemTextInput_(sysCtrl);
311} 313}
312 314
315- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView {
316 postCommand_App("scroll.top smooth:1");
317 return NO;
318}
319
313@end 320@end
314 321
322/*----------------------------------------------------------------------------------------------*/
323
315static void enableMouse_(iBool yes) { 324static void enableMouse_(iBool yes) {
316 SDL_EventState(SDL_MOUSEBUTTONDOWN, yes); 325 SDL_EventState(SDL_MOUSEBUTTONDOWN, yes);
317 SDL_EventState(SDL_MOUSEMOTION, yes); 326 SDL_EventState(SDL_MOUSEMOTION, yes);
@@ -426,6 +435,19 @@ void setupWindow_iOS(iWindow *window) {
426 UIViewController *ctl = viewController_(window); 435 UIViewController *ctl = viewController_(window);
427 isSystemDarkMode_ = isDarkMode_(window); 436 isSystemDarkMode_ = isDarkMode_(window);
428 postCommandf_App("~os.theme.changed dark:%d contrast:1", isSystemDarkMode_ ? 1 : 0); 437 postCommandf_App("~os.theme.changed dark:%d contrast:1", isSystemDarkMode_ ? 1 : 0);
438 /* A hack to get notified on status bar taps. We create a thin dummy UIScrollView
439 that occupies the top of the screen where the status bar is located. */ {
440 CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
441 statusBarTapper_ = [[UIScrollView alloc] initWithFrame:statusBarFrame];
442// [statusBarTapper_ setBackgroundColor:[UIColor greenColor]]; /* to see where it is */
443 [statusBarTapper_ setShowsVerticalScrollIndicator:NO];
444 [statusBarTapper_ setShowsHorizontalScrollIndicator:NO];
445 [statusBarTapper_ setContentSize:(CGSize){ 10000, 10000 }];
446 [statusBarTapper_ setContentOffset:(CGPoint){ 0, 1000 }];
447 [statusBarTapper_ setScrollsToTop:YES];
448 [statusBarTapper_ setDelegate:appState_];
449 [ctl.view addSubview:statusBarTapper_];
450 }
429} 451}
430 452
431void playHapticEffect_iOS(enum iHapticEffect effect) { 453void playHapticEffect_iOS(enum iHapticEffect effect) {
@@ -443,6 +465,14 @@ void playHapticEffect_iOS(enum iHapticEffect effect) {
443} 465}
444 466
445iBool processEvent_iOS(const SDL_Event *ev) { 467iBool processEvent_iOS(const SDL_Event *ev) {
468 if (ev->type == SDL_DISPLAYEVENT) {
469 if (deviceType_App() == phone_AppDeviceType) {
470 [statusBarTapper_ setHidden:(ev->display.data1 == SDL_ORIENTATION_LANDSCAPE ||
471 ev->display.data1 == SDL_ORIENTATION_LANDSCAPE_FLIPPED)];
472 }
473 [statusBarTapper_ setFrame:[UIApplication sharedApplication].statusBarFrame];
474 return iFalse;
475 }
446 if (ev->type == SDL_WINDOWEVENT) { 476 if (ev->type == SDL_WINDOWEVENT) {
447 if (ev->window.event == SDL_WINDOWEVENT_RESTORED) { 477 if (ev->window.event == SDL_WINDOWEVENT_RESTORED) {
448 const iBool isDark = isDarkMode_(get_Window()); 478 const iBool isDark = isDarkMode_(get_Window());
diff --git a/src/macos.m b/src/macos.m
index 4ad267c1..191842f6 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -72,10 +72,10 @@ static NSString *currentSystemAppearance_(void) {
72} 72}
73 73
74iBool shouldDefaultToMetalRenderer_MacOS(void) { 74iBool shouldDefaultToMetalRenderer_MacOS(void) {
75 /* TODO: Test if SDL 2.0.16 works better (no stutters with Metal?). */
76 return iFalse; /*
77 const iInt2 ver = macVer_(); 75 const iInt2 ver = macVer_();
78 return ver.x > 10 || ver.y > 13;*/ 76 SDL_DisplayMode dispMode;
77 SDL_GetDesktopDisplayMode(0, &dispMode);
78 return dispMode.refresh_rate > 60 && (ver.x > 10 || ver.y > 13);
79} 79}
80 80
81static void ignoreImmediateKeyDownEvents_(void) { 81static void ignoreImmediateKeyDownEvents_(void) {
@@ -436,6 +436,10 @@ static iBool processScrollWheelEvent_(NSEvent *event) {
436 const iBool isInertia = (event.momentumPhase & (NSEventPhaseBegan | NSEventPhaseChanged)) != 0; 436 const iBool isInertia = (event.momentumPhase & (NSEventPhaseBegan | NSEventPhaseChanged)) != 0;
437 const iBool isEnded = event.scrollingDeltaX == 0.0f && event.scrollingDeltaY == 0.0f && !isInertia; 437 const iBool isEnded = event.scrollingDeltaX == 0.0f && event.scrollingDeltaY == 0.0f && !isInertia;
438 const iWindow *win = &get_MainWindow()->base; 438 const iWindow *win = &get_MainWindow()->base;
439 if (event.window != nsWindow_(win->win)) {
440 /* Not the main window. */
441 return iFalse;
442 }
439 if (isPerPixel) { 443 if (isPerPixel) {
440 /* On macOS 12.1, stopping ongoing inertia scroll with a tap seems to sometimes produce 444 /* On macOS 12.1, stopping ongoing inertia scroll with a tap seems to sometimes produce
441 spurious large scroll events. */ 445 spurious large scroll events. */
@@ -525,7 +529,6 @@ static iBool processScrollWheelEvent_(NSEvent *event) {
525 ev.wheel.y = iSign(ev.wheel.y); 529 ev.wheel.y = iSign(ev.wheel.y);
526 } 530 }
527#endif 531#endif
528
529 return iTrue; 532 return iTrue;
530} 533}
531 534
@@ -734,7 +737,7 @@ enum iColorId removeColorEscapes_String(iString *d) {
734static NSString *cleanString_(const iString *ansiEscapedText) { 737static NSString *cleanString_(const iString *ansiEscapedText) {
735 iString mod; 738 iString mod;
736 initCopy_String(&mod, ansiEscapedText); 739 initCopy_String(&mod, ansiEscapedText);
737 iRegExp *ansi = makeAnsiEscapePattern_Text(); 740 iRegExp *ansi = makeAnsiEscapePattern_Text(iTrue /* with ESC */);
738 replaceRegExp_String(&mod, ansi, "", NULL, NULL); 741 replaceRegExp_String(&mod, ansi, "", NULL, NULL);
739 iRelease(ansi); 742 iRelease(ansi);
740 NSString *clean = [NSString stringWithUTF8String:cstr_String(&mod)]; 743 NSString *clean = [NSString stringWithUTF8String:cstr_String(&mod)];
diff --git a/src/prefs.c b/src/prefs.c
index 6164ca25..13a1dab7 100644
--- a/src/prefs.c
+++ b/src/prefs.c
@@ -25,8 +25,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
25#include <the_Foundation/fileinfo.h> 25#include <the_Foundation/fileinfo.h>
26#include <assert.h> 26#include <assert.h>
27 27
28static_assert(offsetof(iPrefs, plainTextWrap) == offsetof(iPrefs, bools[plainTextWrap_PrefsBool]), 28_Static_assert(offsetof(iPrefs, plainTextWrap) == offsetof(iPrefs, bools[plainTextWrap_PrefsBool]),
29 "memory layout mismatch (needs struct packing?)"); 29 "memory layout mismatch (needs struct packing?)");
30 30
31void init_Prefs(iPrefs *d) { 31void init_Prefs(iPrefs *d) {
32 iForIndices(i, d->strings) { 32 iForIndices(i, d->strings) {
diff --git a/src/resources.c b/src/resources.c
index ae85463a..fa7485ce 100644
--- a/src/resources.c
+++ b/src/resources.c
@@ -33,6 +33,8 @@ iBlock blobAbout_Resources;
33iBlock blobHelp_Resources; 33iBlock blobHelp_Resources;
34iBlock blobLagrange_Resources; 34iBlock blobLagrange_Resources;
35iBlock blobLicense_Resources; 35iBlock blobLicense_Resources;
36iBlock blobVersion_0_13_Resources;
37iBlock blobVersion_1_5_Resources;
36iBlock blobVersion_Resources; 38iBlock blobVersion_Resources;
37iBlock blobArghelp_Resources; 39iBlock blobArghelp_Resources;
38iBlock blobCs_Resources; 40iBlock blobCs_Resources;
@@ -76,6 +78,8 @@ static struct {
76 { &blobVersion_Resources, "about/android-version.gmi" }, 78 { &blobVersion_Resources, "about/android-version.gmi" },
77#else 79#else
78 { &blobHelp_Resources, "about/help.gmi" }, 80 { &blobHelp_Resources, "about/help.gmi" },
81 { &blobVersion_0_13_Resources, "about/version-0.13.gmi" },
82 { &blobVersion_1_5_Resources, "about/version-1.5.gmi" },
79 { &blobVersion_Resources, "about/version.gmi" }, 83 { &blobVersion_Resources, "about/version.gmi" },
80#endif 84#endif
81 { &blobArghelp_Resources, "arg-help.txt" }, 85 { &blobArghelp_Resources, "arg-help.txt" },
diff --git a/src/resources.h b/src/resources.h
index 3852cc3e..e440fda3 100644
--- a/src/resources.h
+++ b/src/resources.h
@@ -35,6 +35,8 @@ extern iBlock blobAbout_Resources;
35extern iBlock blobHelp_Resources; 35extern iBlock blobHelp_Resources;
36extern iBlock blobLagrange_Resources; 36extern iBlock blobLagrange_Resources;
37extern iBlock blobLicense_Resources; 37extern iBlock blobLicense_Resources;
38extern iBlock blobVersion_0_13_Resources;
39extern iBlock blobVersion_1_5_Resources;
38extern iBlock blobVersion_Resources; 40extern iBlock blobVersion_Resources;
39extern iBlock blobArghelp_Resources; 41extern iBlock blobArghelp_Resources;
40extern iBlock blobCs_Resources; 42extern iBlock blobCs_Resources;
diff --git a/src/ui/color.c b/src/ui/color.c
index 3c2f0339..824342ae 100644
--- a/src/ui/color.c
+++ b/src/ui/color.c
@@ -868,23 +868,16 @@ void ansiColors_Color(iRangecc escapeSequence, int fgDefault, int bgDefault,
868 case 97: 868 case 97:
869 fg = ansi8BitColors_[8 + arg - 90]; 869 fg = ansi8BitColors_[8 + arg - 90];
870 break; 870 break;
871 } 871 case 100:
872 } 872 case 101:
873 /* Ensure legibility if only the foreground color is set. */ 873 case 102:
874 if (fg.a) { 874 case 103:
875 const iHSLColor themeBg = get_HSLColor(tmBackground_ColorId); 875 case 104:
876 const float bgLuminance = luma_Color(get_Color(tmBackground_ColorId)); 876 case 105:
877 if (bgLuminance > 0.4f) { 877 case 106:
878 float dim = (bgLuminance - 0.4f); 878 case 107:
879 fg.r *= 0.5f * dim; 879 bg = ansi8BitColors_[8 + arg - 100];
880 fg.g *= 0.5f * dim; 880 break;
881 fg.b *= 0.5f * dim;
882 }
883 if (themeBg.sat > 0.15f && themeBg.lum >= 0.5f) {
884 iHSLColor fgHsl = hsl_Color(fg);
885 fgHsl.hue = themeBg.hue;
886 fgHsl.lum = themeBg.lum * 0.5f;
887 fg = rgb_HSLColor(fgHsl);
888 } 881 }
889 } 882 }
890 if (fg.a && fg_out) { 883 if (fg.a && fg_out) {
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 6a535882..25559890 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
36#include "gmdocument.h" 36#include "gmdocument.h"
37#include "gmrequest.h" 37#include "gmrequest.h"
38#include "gmutil.h" 38#include "gmutil.h"
39#include "gopher.h"
39#include "history.h" 40#include "history.h"
40#include "indicatorwidget.h" 41#include "indicatorwidget.h"
41#include "inputwidget.h" 42#include "inputwidget.h"
@@ -921,6 +922,7 @@ static void documentRunsInvalidated_DocumentView_(iDocumentView *d) {
921 d->hoverPre = NULL; 922 d->hoverPre = NULL;
922 d->hoverAltPre = NULL; 923 d->hoverAltPre = NULL;
923 d->hoverLink = NULL; 924 d->hoverLink = NULL;
925 clear_PtrArray(&d->visibleMedia);
924 iZap(d->visibleRuns); 926 iZap(d->visibleRuns);
925 iZap(d->renderRuns); 927 iZap(d->renderRuns);
926} 928}
@@ -2756,6 +2758,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
2756 } 2758 }
2757 d->flags |= drawDownloadCounter_DocumentWidgetFlag; 2759 d->flags |= drawDownloadCounter_DocumentWidgetFlag;
2758 clear_PtrSet(d->view.invalidRuns); 2760 clear_PtrSet(d->view.invalidRuns);
2761 documentRunsInvalidated_DocumentWidget_(d);
2759 deinit_String(&str); 2762 deinit_String(&str);
2760 return; 2763 return;
2761 } 2764 }
@@ -2899,10 +2902,14 @@ static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) {
2899 add_Banner(d->banner, warning_BannerType, none_GmStatusCode, title, str); 2902 add_Banner(d->banner, warning_BannerType, none_GmStatusCode, title, str);
2900 } 2903 }
2901 /* Warnings related to page contents. */ 2904 /* Warnings related to page contents. */
2902 const int dismissed = 2905 int dismissed =
2903 value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)), 2906 value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)),
2904 dismissWarnings_SiteSpecKey) | 2907 dismissWarnings_SiteSpecKey) |
2905 (!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0); 2908 (!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0);
2909 /* File pages don't allow dismissing warnings, so skip it. */
2910 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {
2911 dismissed |= ansiEscapes_GmDocumentWarning;
2912 }
2906 const int warnings = warnings_GmDocument(d->view.doc) & ~dismissed; 2913 const int warnings = warnings_GmDocument(d->view.doc) & ~dismissed;
2907 if (warnings & missingGlyphs_GmDocumentWarning) { 2914 if (warnings & missingGlyphs_GmDocumentWarning) {
2908 add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL); 2915 add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL);
@@ -4068,14 +4075,12 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
4068 return iTrue; 4075 return iTrue;
4069 } 4076 }
4070 else if (equal_Command(cmd, "valueinput.cancelled") && 4077 else if (equal_Command(cmd, "valueinput.cancelled") &&
4071 equal_Rangecc(range_Command(cmd, "id"), "document.input.submit") && document_App() == d) { 4078 equal_Rangecc(range_Command(cmd, "id"), "!document.input.submit") && document_App() == d) {
4072 postCommand_Root(get_Root(), "navigate.back"); 4079 postCommand_Root(get_Root(), "navigate.back");
4073 return iTrue; 4080 return iTrue;
4074 } 4081 }
4075 else if (equalWidget_Command(cmd, w, "document.request.updated") && 4082 else if (equalWidget_Command(cmd, w, "document.request.updated") &&
4076 id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) { 4083 id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) {
4077// set_Block(&d->sourceContent, &lockResponse_GmRequest(d->request)->body);
4078// unlockResponse_GmRequest(d->request);
4079 if (document_App() == d) { 4084 if (document_App() == d) {
4080 updateFetchProgress_DocumentWidget_(d); 4085 updateFetchProgress_DocumentWidget_(d);
4081 } 4086 }
@@ -4301,6 +4306,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
4301 else if (equal_Command(cmd, "navigate.parent") && document_App() == d) { 4306 else if (equal_Command(cmd, "navigate.parent") && document_App() == d) {
4302 iUrl parts; 4307 iUrl parts;
4303 init_Url(&parts, d->mod.url); 4308 init_Url(&parts, d->mod.url);
4309 if (endsWith_Rangecc(parts.path, "/index.gmi")) {
4310 parts.path.end -= 9; /* This is the default index page. */
4311 }
4304 /* Remove the last path segment. */ 4312 /* Remove the last path segment. */
4305 if (size_Range(&parts.path) > 1) { 4313 if (size_Range(&parts.path) > 1) {
4306 if (parts.path.end[-1] == '/') { 4314 if (parts.path.end[-1] == '/') {
@@ -4310,14 +4318,42 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
4310 if (parts.path.end[-1] == '/') break; 4318 if (parts.path.end[-1] == '/') break;
4311 parts.path.end--; 4319 parts.path.end--;
4312 } 4320 }
4313 postCommandf_Root(w->root, 4321 iString *parentUrl = collectNewRange_String((iRangecc){ constBegin_String(d->mod.url),
4314 "open url:%s", 4322 parts.path.end });
4315 cstr_Rangecc((iRangecc){ constBegin_String(d->mod.url), parts.path.end })); 4323 /* Always go to a gophermap. */
4324 setUrlItemType_Gopher(parentUrl, '1');
4325 /* Hierarchical navigation doesn't make sense with Titan. */
4326 if (startsWith_String(parentUrl, "titan://")) {
4327 /* We have no way of knowing if the corresponding URL is valid for Gemini,
4328 but let's try anyway. */
4329 set_String(parentUrl, withScheme_String(parentUrl, "gemini"));
4330 stripUrlPort_String(parentUrl);
4331 }
4332 if (!cmpCase_String(parentUrl, "about:")) {
4333 setCStr_String(parentUrl, "about:about");
4334 }
4335 postCommandf_Root(w->root, "open url:%s", cstr_String(parentUrl));
4316 } 4336 }
4317 return iTrue; 4337 return iTrue;
4318 } 4338 }
4319 else if (equal_Command(cmd, "navigate.root") && document_App() == d) { 4339 else if (equal_Command(cmd, "navigate.root") && document_App() == d) {
4320 postCommandf_Root(w->root, "open url:%s/", cstr_Rangecc(urlRoot_String(d->mod.url))); 4340 iString *rootUrl = collectNewRange_String(urlRoot_String(d->mod.url));
4341 /* Always go to a gophermap. */
4342 setUrlItemType_Gopher(rootUrl, '1');
4343 /* Hierarchical navigation doesn't make sense with Titan. */
4344 if (startsWith_String(rootUrl, "titan://")) {
4345 /* We have no way of knowing if the corresponding URL is valid for Gemini,
4346 but let's try anyway. */
4347 set_String(rootUrl, withScheme_String(rootUrl, "gemini"));
4348 stripUrlPort_String(rootUrl);
4349 }
4350 if (!cmpCase_String(rootUrl, "about:")) {
4351 setCStr_String(rootUrl, "about:about");
4352 }
4353 else {
4354 appendCStr_String(rootUrl, "/");
4355 }
4356 postCommandf_Root(w->root, "open url:%s", cstr_String(rootUrl));
4321 return iTrue; 4357 return iTrue;
4322 } 4358 }
4323 else if (equalWidget_Command(cmd, w, "scroll.moved")) { 4359 else if (equalWidget_Command(cmd, w, "scroll.moved")) {
@@ -4340,6 +4376,12 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
4340 return iTrue; 4376 return iTrue;
4341 } 4377 }
4342 else if (equal_Command(cmd, "scroll.top") && document_App() == d) { 4378 else if (equal_Command(cmd, "scroll.top") && document_App() == d) {
4379 if (argLabel_Command(cmd, "smooth")) {
4380 stopWidgetMomentum_Touch(w);
4381 smoothScroll_DocumentView_(&d->view, -pos_SmoothScroll(&d->view.scrollY), 500);
4382 d->view.scrollY.flags |= muchSofter_AnimFlag;
4383 return iTrue;
4384 }
4343 init_Anim(&d->view.scrollY.pos, 0); 4385 init_Anim(&d->view.scrollY.pos, 0);
4344 invalidate_VisBuf(d->view.visBuf); 4386 invalidate_VisBuf(d->view.visBuf);
4345 clampScroll_DocumentView_(&d->view); 4387 clampScroll_DocumentView_(&d->view);
diff --git a/src/ui/indicatorwidget.c b/src/ui/indicatorwidget.c
index bc0bd0fa..e16550ff 100644
--- a/src/ui/indicatorwidget.c
+++ b/src/ui/indicatorwidget.c
@@ -28,32 +28,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
28 28
29#include <SDL_timer.h> 29#include <SDL_timer.h>
30 30
31static int timerId_; /* common timer for all indicators */ 31struct Impl_IndicatorWidget {
32static int animCount_; /* number of animating indicators */
33
34static uint32_t postRefresh_(uint32_t interval, void *context) {
35 iUnused(context);
36 postRefresh_App();
37 return interval;
38}
39
40static void startTimer_(void) {
41 animCount_++;
42 if (!timerId_) {
43 timerId_ = SDL_AddTimer(1000 / 60, postRefresh_, NULL);
44 }
45}
46
47static void stopTimer_(void) {
48 iAssert(animCount_ > 0);
49 if (--animCount_ == 0) {
50 iAssert(timerId_);
51 SDL_RemoveTimer(timerId_);
52 timerId_ = 0;
53 }
54}
55
56struct Impl_IndicatorWidget{
57 iWidget widget; 32 iWidget widget;
58 iAnim pos; 33 iAnim pos;
59}; 34};
@@ -64,6 +39,14 @@ iLocalDef iBool isActive_IndicatorWidget_(const iIndicatorWidget *d) {
64 return isSelected_Widget(d); 39 return isSelected_Widget(d);
65} 40}
66 41
42static void animate_IndicatorWidget_(void *ptr) {
43 iIndicatorWidget *d = ptr;
44 if (!isFinished_Anim(&d->pos)) {
45 addTickerRoot_App(animate_IndicatorWidget_, d->widget.root, ptr);
46 }
47 postRefresh_App();
48}
49
67static void setActive_IndicatorWidget_(iIndicatorWidget *d, iBool set) { 50static void setActive_IndicatorWidget_(iIndicatorWidget *d, iBool set) {
68 setFlags_Widget(as_Widget(d), selected_WidgetFlag, set); 51 setFlags_Widget(as_Widget(d), selected_WidgetFlag, set);
69} 52}
@@ -75,22 +58,8 @@ void init_IndicatorWidget(iIndicatorWidget *d) {
75 setFlags_Widget(w, unhittable_WidgetFlag, iTrue); 58 setFlags_Widget(w, unhittable_WidgetFlag, iTrue);
76} 59}
77 60
78static void startTimer_IndicatorWidget_(iIndicatorWidget *d) {
79 if (!isActive_IndicatorWidget_(d)) {
80 startTimer_();
81 setActive_IndicatorWidget_(d, iTrue);
82 }
83}
84
85static void stopTimer_IndicatorWidget_(iIndicatorWidget *d) {
86 if (isActive_IndicatorWidget_(d)) {
87 stopTimer_();
88 setActive_IndicatorWidget_(d, iFalse);
89 }
90}
91
92void deinit_IndicatorWidget(iIndicatorWidget *d) { 61void deinit_IndicatorWidget(iIndicatorWidget *d) {
93 stopTimer_IndicatorWidget_(d); 62 removeTicker_App(animate_IndicatorWidget_, d);
94} 63}
95 64
96static iBool isCompleted_IndicatorWidget_(const iIndicatorWidget *d) { 65static iBool isCompleted_IndicatorWidget_(const iIndicatorWidget *d) {
@@ -116,12 +85,7 @@ void draw_IndicatorWidget_(const iIndicatorWidget *d) {
116 85
117iBool processEvent_IndicatorWidget_(iIndicatorWidget *d, const SDL_Event *ev) { 86iBool processEvent_IndicatorWidget_(iIndicatorWidget *d, const SDL_Event *ev) {
118 iWidget *w = &d->widget; 87 iWidget *w = &d->widget;
119 if (ev->type == SDL_USEREVENT && ev->user.code == refresh_UserEventCode) { 88 if (isCommand_SDLEvent(ev)) {
120 if (isFinished_Anim(&d->pos)) {
121 stopTimer_IndicatorWidget_(d);
122 }
123 }
124 else if (isCommand_SDLEvent(ev)) {
125 const char *cmd = command_UserEvent(ev); 89 const char *cmd = command_UserEvent(ev);
126 if (startsWith_CStr(cmd, "document.request.")) { 90 if (startsWith_CStr(cmd, "document.request.")) {
127 if (pointerLabel_Command(cmd, "doc") == parent_Widget(w)) { 91 if (pointerLabel_Command(cmd, "doc") == parent_Widget(w)) {
@@ -130,23 +94,23 @@ iBool processEvent_IndicatorWidget_(iIndicatorWidget *d, const SDL_Event *ev) {
130 setValue_Anim(&d->pos, 0, 0); 94 setValue_Anim(&d->pos, 0, 0);
131 setValue_Anim(&d->pos, 0.75f, 4000); 95 setValue_Anim(&d->pos, 0.75f, 4000);
132 setFlags_Anim(&d->pos, easeOut_AnimFlag, iTrue); 96 setFlags_Anim(&d->pos, easeOut_AnimFlag, iTrue);
133 startTimer_IndicatorWidget_(d); 97 animate_IndicatorWidget_(d);
134 } 98 }
135 else if (equal_Command(cmd, "finished")) { 99 else if (equal_Command(cmd, "finished")) {
136 if (value_Anim(&d->pos) > 0.01f) { 100 if (value_Anim(&d->pos) > 0.01f) {
137 setValue_Anim(&d->pos, 1.0f, 250); 101 setValue_Anim(&d->pos, 1.0f, 250);
138 setFlags_Anim(&d->pos, easeOut_AnimFlag, iFalse); 102 setFlags_Anim(&d->pos, easeOut_AnimFlag, iFalse);
139 startTimer_IndicatorWidget_(d); 103 animate_IndicatorWidget_(d);
140 } 104 }
141 else { 105 else {
142 setValue_Anim(&d->pos, 0, 0); 106 setValue_Anim(&d->pos, 0, 0);
143 stopTimer_IndicatorWidget_(d); 107 animate_IndicatorWidget_(d);
144 refresh_Widget(d); 108 refresh_Widget(d);
145 } 109 }
146 } 110 }
147 else if (equal_Command(cmd, "cancelled")) { 111 else if (equal_Command(cmd, "cancelled")) {
148 setValue_Anim(&d->pos, 0, 0); 112 setValue_Anim(&d->pos, 0, 0);
149 stopTimer_IndicatorWidget_(d); 113 animate_IndicatorWidget_(d);
150 refresh_Widget(d); 114 refresh_Widget(d);
151 } 115 }
152 } 116 }
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 9261da0c..aa55f3f0 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -742,15 +742,17 @@ static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, int doStart) {
742#else /* using a system-provided text control */ 742#else /* using a system-provided text control */
743 743
744static void updateAllLinesAndResizeHeight_InputWidget_(iInputWidget *d) { 744static void updateAllLinesAndResizeHeight_InputWidget_(iInputWidget *d) {
745 /* Rewrap the buffered text and resize accordingly. */ 745 if (width_Widget(d) >= minWidth_InputWidget_) {
746 iWrapText wt = wrap_InputWidget_(d, 0); 746 /* Rewrap the buffered text and resize accordingly. */
747 /* TODO: Set max lines limit for WrapText. */ 747 iWrapText wt = wrap_InputWidget_(d, 0);
748 const int height = measure_WrapText(&wt, d->font).bounds.size.y; 748 /* TODO: Set max lines limit for WrapText. */
749 /* We use this to store the number wrapped lines for determining widget height. */ 749 const int height = measure_WrapText(&wt, d->font).bounds.size.y;
750 d->visWrapLines.start = 0; 750 /* We use this to store the number wrapped lines for determining widget height. */
751 d->visWrapLines.end = iMax(d->minWrapLines, 751 d->visWrapLines.start = 0;
752 iMin(d->maxWrapLines, height / lineHeight_Text(d->font))); 752 d->visWrapLines.end = iMax(d->minWrapLines,
753 updateMetrics_InputWidget_(d); 753 iMin(d->maxWrapLines, height / lineHeight_Text(d->font)));
754 updateMetrics_InputWidget_(d);
755 }
754} 756}
755 757
756#endif 758#endif
diff --git a/src/ui/mobile.c b/src/ui/mobile.c
index cf955423..aefeebc6 100644
--- a/src/ui/mobile.c
+++ b/src/ui/mobile.c
@@ -43,7 +43,7 @@ const iToolbarActionSpec toolbarActions_Mobile[max_ToolbarAction] = {
43 { home_Icon, "${menu.home}", "navigate.home" }, 43 { home_Icon, "${menu.home}", "navigate.home" },
44 { upArrow_Icon, "${menu.parent}", "navigate.parent" }, 44 { upArrow_Icon, "${menu.parent}", "navigate.parent" },
45 { reload_Icon, "${menu.reload}", "navigate.reload" }, 45 { reload_Icon, "${menu.reload}", "navigate.reload" },
46 { openTab_Icon, "${menu.newtab}", "tabs.new" }, 46 { add_Icon, "${menu.newtab}", "tabs.new" },
47 { close_Icon, "${menu.closetab}", "tabs.close" }, 47 { close_Icon, "${menu.closetab}", "tabs.close" },
48 { bookmark_Icon, "${menu.page.bookmark}", "bookmark.add" }, 48 { bookmark_Icon, "${menu.page.bookmark}", "bookmark.add" },
49 { globe_Icon, "${menu.page.translate}", "document.translate" }, 49 { globe_Icon, "${menu.page.translate}", "document.translate" },
@@ -940,7 +940,7 @@ void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) {
940 } 940 }
941 const int maxOffset = isHorizPanel ? width_Widget(sheet) 941 const int maxOffset = isHorizPanel ? width_Widget(sheet)
942 : isPortraitPhone_App() ? height_Widget(sheet) 942 : isPortraitPhone_App() ? height_Widget(sheet)
943 : (12 * gap_UI); 943 : (6 * gap_UI);
944 if (isIncoming) { 944 if (isIncoming) {
945 setVisualOffset_Widget(sheet, maxOffset, 0, 0); 945 setVisualOffset_Widget(sheet, maxOffset, 0, 0);
946 setVisualOffset_Widget(sheet, 0, 330, easeOut_AnimFlag | softer_AnimFlag); 946 setVisualOffset_Widget(sheet, 0, 330, easeOut_AnimFlag | softer_AnimFlag);
diff --git a/src/ui/root.c b/src/ui/root.c
index 5c4296cf..6e187313 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -703,6 +703,20 @@ void updateToolbarColors_Root(iRoot *d) {
703#endif 703#endif
704} 704}
705 705
706void showOrHideNewTabButton_Root(iRoot *d) {
707 iWidget *tabs = findChild_Widget(d->widget, "doctabs");
708 iWidget *newTabButton = findChild_Widget(tabs, "newtab");
709 iBool hide = iFalse;
710 iForIndices(i, prefs_App()->navbarActions) {
711 if (prefs_App()->navbarActions[i] == newTab_ToolbarAction) {
712 hide = iTrue;
713 break;
714 }
715 }
716 setFlags_Widget(newTabButton, hidden_WidgetFlag, hide);
717 arrange_Widget(findChild_Widget(tabs, "tabs.buttons"));
718}
719
706void notifyVisualOffsetChange_Root(iRoot *d) { 720void notifyVisualOffsetChange_Root(iRoot *d) {
707 if (d && (d->didAnimateVisualOffsets || d->didChangeArrangement)) { 721 if (d && (d->didAnimateVisualOffsets || d->didChangeArrangement)) {
708 iNotifyAudience(d, visualOffsetsChanged, RootVisualOffsetsChanged); 722 iNotifyAudience(d, visualOffsetsChanged, RootVisualOffsetsChanged);
@@ -848,6 +862,7 @@ static void updateNavBarActions_(iWidget *navBar) {
848 } 862 }
849 iEndCollect(); 863 iEndCollect();
850 } 864 }
865 showOrHideNewTabButton_Root(navBar->root);
851} 866}
852 867
853static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { 868static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
@@ -1312,8 +1327,7 @@ void createUserInterface_Root(iRoot *d) {
1312#if defined (iPlatformApple) 1327#if defined (iPlatformApple)
1313 addUnsplitButton_(navBar); 1328 addUnsplitButton_(navBar);
1314#endif 1329#endif
1315 iWidget *navBack; 1330 setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(backArrow_Icon, 0, 0, "navigate.back")), collapse_WidgetFlag), "navbar.action1");
1316 setId_Widget(navBack = addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(backArrow_Icon, 0, 0, "navigate.back")), collapse_WidgetFlag), "navbar.action1");
1317 setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(forwardArrow_Icon, 0, 0, "navigate.forward")), collapse_WidgetFlag), "navbar.action2"); 1331 setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(forwardArrow_Icon, 0, 0, "navigate.forward")), collapse_WidgetFlag), "navbar.action2");
1318 /* Button for toggling the left sidebar. */ 1332 /* Button for toggling the left sidebar. */
1319 setId_Widget(addChildFlags_Widget( 1333 setId_Widget(addChildFlags_Widget(
@@ -1497,6 +1511,16 @@ void createUserInterface_Root(iRoot *d) {
1497 /* On PC platforms, the close buttons are generally on the top right. */ 1511 /* On PC platforms, the close buttons are generally on the top right. */
1498 addUnsplitButton_(navBar); 1512 addUnsplitButton_(navBar);
1499#endif 1513#endif
1514 if (deviceType_App() == tablet_AppDeviceType) {
1515 /* Ensure that all navbar buttons match the height of the input field.
1516 This is required because touch input fields are given extra padding,
1517 making them taller than buttons by default. */
1518 iForEach(ObjectList, i, children_Widget(navBar)) {
1519 if (isInstance_Object(i.object, &Class_LabelWidget)) {
1520 as_Widget(i.object)->sizeRef = as_Widget(url);
1521 }
1522 }
1523 }
1500 } 1524 }
1501 /* Tab bar. */ { 1525 /* Tab bar. */ {
1502 iWidget *mainStack = new_Widget(); 1526 iWidget *mainStack = new_Widget();
@@ -1517,7 +1541,7 @@ void createUserInterface_Root(iRoot *d) {
1517 } 1541 }
1518 setId_Widget( 1542 setId_Widget(
1519 addChildFlags_Widget(buttons, iClob(newIcon_LabelWidget(add_Icon, 0, 0, "tabs.new")), 1543 addChildFlags_Widget(buttons, iClob(newIcon_LabelWidget(add_Icon, 0, 0, "tabs.new")),
1520 moveToParentRightEdge_WidgetFlag), 1544 moveToParentRightEdge_WidgetFlag | collapse_WidgetFlag),
1521 "newtab"); 1545 "newtab");
1522 } 1546 }
1523 /* Sidebars. */ { 1547 /* Sidebars. */ {
@@ -1528,6 +1552,7 @@ void createUserInterface_Root(iRoot *d) {
1528 addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos); 1552 addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos);
1529 iSidebarWidget *sidebar2 = new_SidebarWidget(right_SidebarSide); 1553 iSidebarWidget *sidebar2 = new_SidebarWidget(right_SidebarSide);
1530 addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos); 1554 addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos);
1555 setFlags_Widget(as_Widget(sidebar2), disabledWhenHidden_WidgetFlag, iTrue);
1531 } 1556 }
1532 else { 1557 else {
1533 /* Sidebar is a slide-over sheet. */ 1558 /* Sidebar is a slide-over sheet. */
diff --git a/src/ui/root.h b/src/ui/root.h
index 7e831be3..a81ebdf7 100644
--- a/src/ui/root.h
+++ b/src/ui/root.h
@@ -43,6 +43,8 @@ void updatePadding_Root (iRoot *); /* TODO: is part of m
43void dismissPortraitPhoneSidebars_Root (iRoot *); 43void dismissPortraitPhoneSidebars_Root (iRoot *);
44void showToolbar_Root (iRoot *, iBool show); 44void showToolbar_Root (iRoot *, iBool show);
45void updateToolbarColors_Root (iRoot *); 45void updateToolbarColors_Root (iRoot *);
46void showOrHideNewTabButton_Root (iRoot *);
47
46void notifyVisualOffsetChange_Root (iRoot *); 48void notifyVisualOffsetChange_Root (iRoot *);
47 49
48iInt2 size_Root (const iRoot *); 50iInt2 size_Root (const iRoot *);
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index f5beb785..16677f9e 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -286,7 +286,8 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
286 iZap(on); 286 iZap(on);
287 size_t numItems = 0; 287 size_t numItems = 0;
288 isEmpty = iTrue; 288 isEmpty = iTrue;
289 iConstForEach(PtrArray, i, listEntries_Feeds()) { 289 const iPtrArray *feedEntries = listEntries_Feeds();
290 iConstForEach(PtrArray, i, feedEntries) {
290 const iFeedEntry *entry = i.ptr; 291 const iFeedEntry *entry = i.ptr;
291 if (isHidden_FeedEntry(entry)) { 292 if (isHidden_FeedEntry(entry)) {
292 continue; /* A hidden entry. */ 293 continue; /* A hidden entry. */
@@ -350,7 +351,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
350 } 351 }
351 /* Actions. */ 352 /* Actions. */
352 if (!isMobile) { 353 if (!isMobile) {
353 if (!keepActions && !isEmpty) { 354 if (!keepActions && !isEmpty_PtrArray(feedEntries)) {
354 addActionButton_SidebarWidget_(d, 355 addActionButton_SidebarWidget_(d,
355 check_Icon 356 check_Icon
356 " ${sidebar.action.feeds.markallread}", 357 " ${sidebar.action.feeds.markallread}",
diff --git a/src/ui/text.c b/src/ui/text.c
index 7bb418eb..200108ed 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -390,8 +390,12 @@ static void deinitCache_Text_(iText *d) {
390 SDL_DestroyTexture(d->cache); 390 SDL_DestroyTexture(d->cache);
391} 391}
392 392
393iRegExp *makeAnsiEscapePattern_Text(void) { 393iRegExp *makeAnsiEscapePattern_Text(iBool includeEscChar) {
394 return new_RegExp("[[()][?]?([0-9;AB]*?)([ABCDEFGHJKSTfhilmn])", 0); 394 const char *pattern = "\x1b[[()][?]?([0-9;AB]*?)([ABCDEFGHJKSTfhilmn])";
395 if (!includeEscChar) {
396 pattern++;
397 }
398 return new_RegExp(pattern, 0);
395} 399}
396 400
397void init_Text(iText *d, SDL_Renderer *render) { 401void init_Text(iText *d, SDL_Renderer *render) {
@@ -399,7 +403,7 @@ void init_Text(iText *d, SDL_Renderer *render) {
399 activeText_ = d; 403 activeText_ = d;
400 init_Array(&d->fonts, sizeof(iFont)); 404 init_Array(&d->fonts, sizeof(iFont));
401 d->contentFontSize = contentScale_Text_; 405 d->contentFontSize = contentScale_Text_;
402 d->ansiEscape = makeAnsiEscapePattern_Text(); 406 d->ansiEscape = makeAnsiEscapePattern_Text(iFalse /* no ESC */);
403 d->baseFontId = -1; 407 d->baseFontId = -1;
404 d->baseFgColorId = -1; 408 d->baseFgColorId = -1;
405 d->missingGlyphs = iFalse; 409 d->missingGlyphs = iFalse;
@@ -697,6 +701,34 @@ struct Impl_AttributedRun {
697 701
698static iColor fgColor_AttributedRun_(const iAttributedRun *d) { 702static iColor fgColor_AttributedRun_(const iAttributedRun *d) {
699 if (d->fgColor_.a) { 703 if (d->fgColor_.a) {
704 /* Ensure legibility if only the foreground color is set. */
705 if (!d->bgColor_.a) {
706 iColor fg = d->fgColor_;
707 const iHSLColor themeBg = get_HSLColor(tmBackground_ColorId);
708 const float bgLuminance = luma_Color(get_Color(tmBackground_ColorId));
709 /* TODO: Actually this should check if the FG is too close to the BG, and
710 either darken or brighten the FG. Now it only accounts for nearly black/white
711 backgrounds. */
712 if (bgLuminance < 0.1f) {
713 /* Background is dark. Lighten the foreground. */
714 iHSLColor fgHsl = hsl_Color(fg);
715 fgHsl.lum = iMax(0.2f, fgHsl.lum);
716 return rgb_HSLColor(fgHsl);
717 }
718 if (bgLuminance > 0.4f) {
719 float dim = (bgLuminance - 0.4f);
720 fg.r *= 1.0f * dim;
721 fg.g *= 1.0f * dim;
722 fg.b *= 1.0f * dim;
723 }
724 if (themeBg.sat > 0.15f && themeBg.lum >= 0.5f) {
725 iHSLColor fgHsl = hsl_Color(fg);
726 fgHsl.hue = themeBg.hue;
727 fgHsl.lum = themeBg.lum * 0.5f;
728 fg = rgb_HSLColor(fgHsl);
729 }
730 return fg;
731 }
700 return d->fgColor_; 732 return d->fgColor_;
701 } 733 }
702 if (d->attrib.fgColorId == none_ColorId) { 734 if (d->attrib.fgColorId == none_ColorId) {
@@ -1559,7 +1591,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1559 iAssert(xAdvance >= 0); 1591 iAssert(xAdvance >= 0);
1560 if (wrapMode == word_WrapTextMode) { 1592 if (wrapMode == word_WrapTextMode) {
1561 /* When word wrapping, only consider certain places breakable. */ 1593 /* When word wrapping, only consider certain places breakable. */
1562 if ((prevCh == '-' || prevCh == '/') && !isPunct_Char(ch)) { 1594 if ((prevCh == '-' || prevCh == '/' || prevCh == '\\') && !isPunct_Char(ch)) {
1563 safeBreakPos = logPos; 1595 safeBreakPos = logPos;
1564 breakAdvance = wrapAdvance; 1596 breakAdvance = wrapAdvance;
1565 breakRunIndex = runIndex; 1597 breakRunIndex = runIndex;
@@ -1960,6 +1992,7 @@ static iBool cbAdvanceOneLine_(iWrapText *d, iRangecc range, iTextAttrib attrib,
1960} 1992}
1961 1993
1962iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos) { 1994iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos) {
1995 *endPos = text.end;
1963 iWrapText wrap = { .mode = word_WrapTextMode, 1996 iWrapText wrap = { .mode = word_WrapTextMode,
1964 .text = text, 1997 .text = text,
1965 .maxWidth = width, 1998 .maxWidth = width,
@@ -1974,6 +2007,7 @@ iInt2 tryAdvanceNoWrap_Text(int fontId, iRangecc text, int width, const char **e
1974 *endPos = text.start; 2007 *endPos = text.start;
1975 return zero_I2(); 2008 return zero_I2();
1976 } 2009 }
2010 *endPos = text.end;
1977 /* "NoWrap" means words aren't wrapped; the line is broken at nearest character. */ 2011 /* "NoWrap" means words aren't wrapped; the line is broken at nearest character. */
1978 iWrapText wrap = { .mode = anyCharacter_WrapTextMode, 2012 iWrapText wrap = { .mode = anyCharacter_WrapTextMode,
1979 .text = text, 2013 .text = text,
diff --git a/src/ui/text.h b/src/ui/text.h
index c8bb6f85..b952df84 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -235,7 +235,7 @@ enum iTextBlockMode { quadrants_TextBlockMode, shading_TextBlockMode };
235iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iTextBlockMode, 235iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iTextBlockMode,
236 const iString *text); 236 const iString *text);
237 237
238iRegExp * makeAnsiEscapePattern_Text (void); 238iRegExp * makeAnsiEscapePattern_Text (iBool includeEscChar);
239 239
240/*-----------------------------------------------------------------------------------------------*/ 240/*-----------------------------------------------------------------------------------------------*/
241 241
diff --git a/src/ui/touch.c b/src/ui/touch.c
index 20ccf7b8..a178a913 100644
--- a/src/ui/touch.c
+++ b/src/ui/touch.c
@@ -638,11 +638,14 @@ iBool processEvent_Touch(const SDL_Event *ev) {
638 pixels.x = 0; 638 pixels.x = 0;
639 } 639 }
640#if 0 640#if 0
641 printf("%p (%s) py: %i wy: %f acc: %f edge: %d\n", 641 static uint32_t lastTime = 0;
642 printf("%u :: %p (%s) py: %i wy: %f acc: %f edge: %d\n",
643 nowTime - lastTime,
642 touch->affinity, 644 touch->affinity,
643 class_Widget(touch->affinity)->name, 645 class_Widget(touch->affinity)->name,
644 pixels.y, y_F3(amount), y_F3(touch->accum), 646 pixels.y, y_F3(amount), y_F3(touch->accum),
645 touch->edge); 647 touch->edge);
648 lastTime = nowTime;
646#endif 649#endif
647 if (pixels.x || pixels.y) { 650 if (pixels.x || pixels.y) {
648 //setFocus_Widget(NULL); 651 //setFocus_Widget(NULL);
diff --git a/src/ui/window.c b/src/ui/window.c
index af36bb22..13abc5fa 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -1001,10 +1001,11 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
1001 default: { 1001 default: {
1002 SDL_Event event = *ev; 1002 SDL_Event event = *ev;
1003 if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.unfreeze") && mw) { 1003 if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.unfreeze") && mw) {
1004 mw->isDrawFrozen = iFalse;
1005 if (SDL_GetWindowFlags(d->win) & SDL_WINDOW_HIDDEN) { 1004 if (SDL_GetWindowFlags(d->win) & SDL_WINDOW_HIDDEN) {
1005 mw->isDrawFrozen = iTrue; /* don't trigger a redraw now */
1006 SDL_ShowWindow(d->win); 1006 SDL_ShowWindow(d->win);
1007 } 1007 }
1008 mw->isDrawFrozen = iFalse;
1008 draw_MainWindow(mw); /* don't show a frame of placeholder content */ 1009 draw_MainWindow(mw); /* don't show a frame of placeholder content */
1009 postCommand_App("media.player.update"); /* in case a player needs updating */ 1010 postCommand_App("media.player.update"); /* in case a player needs updating */
1010 return iTrue; 1011 return iTrue;