summaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-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
12 files changed, 166 insertions, 99 deletions
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;