summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.c12
-rw-r--r--src/gmdocument.c36
-rw-r--r--src/gmdocument.h22
-rw-r--r--src/gopher.c4
-rw-r--r--src/history.c11
-rw-r--r--src/history.h1
-rw-r--r--src/lang.c2
-rw-r--r--src/ui/documentwidget.c6
-rw-r--r--src/ui/text.c2
-rw-r--r--src/ui/text_simple.c16
-rw-r--r--src/ui/util.c24
-rw-r--r--src/ui/widget.c96
12 files changed, 160 insertions, 72 deletions
diff --git a/src/app.c b/src/app.c
index a9ec6a26..9e636651 100644
--- a/src/app.c
+++ b/src/app.c
@@ -2123,6 +2123,12 @@ void resetFonts_App(void) {
2123 } 2123 }
2124} 2124}
2125 2125
2126static void invalidateCachedDocuments_App_(void) {
2127 iForEach(ObjectList, i, iClob(listDocuments_App(NULL))) {
2128 invalidateCachedLayout_History(history_DocumentWidget(i.object));
2129 }
2130}
2131
2126iBool handleCommand_App(const char *cmd) { 2132iBool handleCommand_App(const char *cmd) {
2127 iApp *d = &app_; 2133 iApp *d = &app_;
2128 const iBool isFrozen = !d->window || d->window->isDrawFrozen; 2134 const iBool isFrozen = !d->window || d->window->isDrawFrozen;
@@ -2287,7 +2293,10 @@ iBool handleCommand_App(const char *cmd) {
2287 if (!isFrozen) { 2293 if (!isFrozen) {
2288 setFreezeDraw_MainWindow(get_MainWindow(), iTrue); /* no intermediate draws before docs updated */ 2294 setFreezeDraw_MainWindow(get_MainWindow(), iTrue); /* no intermediate draws before docs updated */
2289 } 2295 }
2290 d->prefs.zoomPercent = arg_Command(cmd); 2296 if (arg_Command(cmd) != d->prefs.zoomPercent) {
2297 d->prefs.zoomPercent = arg_Command(cmd);
2298 invalidateCachedDocuments_App_();
2299 }
2291 setDocumentFontSize_Text(text_Window(d->window), (float) d->prefs.zoomPercent / 100.0f); 2300 setDocumentFontSize_Text(text_Window(d->window), (float) d->prefs.zoomPercent / 100.0f);
2292 if (!isFrozen) { 2301 if (!isFrozen) {
2293 postCommand_App("font.changed"); 2302 postCommand_App("font.changed");
@@ -2304,6 +2313,7 @@ iBool handleCommand_App(const char *cmd) {
2304 delta /= 2; 2313 delta /= 2;
2305 } 2314 }
2306 d->prefs.zoomPercent = iClamp(d->prefs.zoomPercent + delta, 50, 200); 2315 d->prefs.zoomPercent = iClamp(d->prefs.zoomPercent + delta, 50, 200);
2316 invalidateCachedDocuments_App_();
2307 setDocumentFontSize_Text(text_Window(d->window), (float) d->prefs.zoomPercent / 100.0f); 2317 setDocumentFontSize_Text(text_Window(d->window), (float) d->prefs.zoomPercent / 100.0f);
2308 if (!isFrozen) { 2318 if (!isFrozen) {
2309 postCommand_App("font.changed"); 2319 postCommand_App("font.changed");
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 8b24ce29..0027bdb3 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -161,10 +161,9 @@ struct Impl_GmDocument {
161 iInt2 size; 161 iInt2 size;
162 int outsideMargin; 162 int outsideMargin;
163 iBool enableCommandLinks; /* `about:command?` only allowed on selected pages */ 163 iBool enableCommandLinks; /* `about:command?` only allowed on selected pages */
164 iBool isLayoutInvalidated;
164 iArray layout; /* contents of source, laid out in document space */ 165 iArray layout; /* contents of source, laid out in document space */
165 iPtrArray links; 166 iPtrArray links;
166// enum iGmDocumentBanner bannerType;
167// iString bannerText;
168 iString title; /* the first top-level title */ 167 iString title; /* the first top-level title */
169 iArray headings; 168 iArray headings;
170 iArray preMeta; /* metadata about preformatted blocks */ 169 iArray preMeta; /* metadata about preformatted blocks */
@@ -607,6 +606,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
607// const iBool isDarkBg = isDark_GmDocumentTheme( 606// const iBool isDarkBg = isDark_GmDocumentTheme(
608// isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight); 607// isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
609 initTheme_GmDocument_(d); 608 initTheme_GmDocument_(d);
609 d->isLayoutInvalidated = iFalse;
610 /* TODO: Collect these parameters into a GmTheme. */ 610 /* TODO: Collect these parameters into a GmTheme. */
611 float indents[max_GmLineType] = { 5, 10, 5, isNarrow ? 5 : 10, 0, 0, 0, 5 }; 611 float indents[max_GmLineType] = { 5, 10, 5, isNarrow ? 5 : 10, 0, 0, 0, 5 };
612 if (isExtremelyNarrow) { 612 if (isExtremelyNarrow) {
@@ -968,6 +968,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
968 ? 4 : 0) * gap_Text; 968 ? 4 : 0) * gap_Text;
969 } 969 }
970 if (!isMono) { 970 if (!isMono) {
971#if 0
971 /* Upper-level headings are typeset a bit tighter. */ 972 /* Upper-level headings are typeset a bit tighter. */
972 if (type == heading1_GmLineType) { 973 if (type == heading1_GmLineType) {
973 rts.lineHeightReduction = 0.10f; 974 rts.lineHeightReduction = 0.10f;
@@ -975,6 +976,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
975 else if (type == heading2_GmLineType) { 976 else if (type == heading2_GmLineType) {
976 rts.lineHeightReduction = 0.06f; 977 rts.lineHeightReduction = 0.06f;
977 } 978 }
979#endif
978 /* Visited links are never bold. */ 980 /* Visited links are never bold. */
979 if (run.linkId && !prefs->boldLinkVisited && 981 if (run.linkId && !prefs->boldLinkVisited &&
980 linkFlags_GmDocument(d, run.linkId) & visited_GmLinkFlag) { 982 linkFlags_GmDocument(d, run.linkId) & visited_GmLinkFlag) {
@@ -1142,13 +1144,12 @@ void init_GmDocument(iGmDocument *d) {
1142 init_String(&d->source); 1144 init_String(&d->source);
1143 init_String(&d->url); 1145 init_String(&d->url);
1144 init_String(&d->localHost); 1146 init_String(&d->localHost);
1145// d->bannerType = siteDomain_GmDocumentBanner;
1146 d->outsideMargin = 0; 1147 d->outsideMargin = 0;
1147 d->size = zero_I2(); 1148 d->size = zero_I2();
1148 d->enableCommandLinks = iFalse; 1149 d->enableCommandLinks = iFalse;
1150 d->isLayoutInvalidated = iFalse;
1149 init_Array(&d->layout, sizeof(iGmRun)); 1151 init_Array(&d->layout, sizeof(iGmRun));
1150 init_PtrArray(&d->links); 1152 init_PtrArray(&d->links);
1151// init_String(&d->bannerText);
1152 init_String(&d->title); 1153 init_String(&d->title);
1153 init_Array(&d->headings, sizeof(iGmHeading)); 1154 init_Array(&d->headings, sizeof(iGmHeading));
1154 init_Array(&d->preMeta, sizeof(iGmPreMeta)); 1155 init_Array(&d->preMeta, sizeof(iGmPreMeta));
@@ -1164,7 +1165,6 @@ void init_GmDocument(iGmDocument *d) {
1164void deinit_GmDocument(iGmDocument *d) { 1165void deinit_GmDocument(iGmDocument *d) {
1165 iReleasePtr(&d->openURLs); 1166 iReleasePtr(&d->openURLs);
1166 delete_Media(d->media); 1167 delete_Media(d->media);
1167// deinit_String(&d->bannerText);
1168 deinit_String(&d->title); 1168 deinit_String(&d->title);
1169 clearLinks_GmDocument_(d); 1169 clearLinks_GmDocument_(d);
1170 deinit_PtrArray(&d->links); 1170 deinit_PtrArray(&d->links);
@@ -1720,22 +1720,28 @@ void setFormat_GmDocument(iGmDocument *d, enum iSourceFormat format) {
1720 d->format = format; 1720 d->format = format;
1721} 1721}
1722 1722
1723#if 0 1723void setWidth_GmDocument(iGmDocument *d, int width, int canvasWidth) {
1724void setBanner_GmDocument(iGmDocument *d, enum iGmDocumentBanner type) { 1724 d->size.x = width;
1725 d->bannerType = type; 1725 d->outsideMargin = iMax(0, (canvasWidth - width) / 2); /* distance to edge of the canvas */
1726 doLayout_GmDocument_(d); /* TODO: just flag need-layout and do it later */
1726} 1727}
1727#endif
1728 1728
1729void setWidth_GmDocument(iGmDocument *d, int width, int outsideMargin) { 1729iBool updateWidth_GmDocument(iGmDocument *d, int width, int canvasWidth) {
1730 d->size.x = width; 1730 if (d->size.x != width || d->isLayoutInvalidated) {
1731 d->outsideMargin = outsideMargin; /* distance to edge of the viewport */ 1731 setWidth_GmDocument(d, width, canvasWidth);
1732 doLayout_GmDocument_(d); /* TODO: just flag need-layout and do it later */ 1732 return iTrue;
1733 }
1734 return iFalse;
1733} 1735}
1734 1736
1735void redoLayout_GmDocument(iGmDocument *d) { 1737void redoLayout_GmDocument(iGmDocument *d) {
1736 doLayout_GmDocument_(d); 1738 doLayout_GmDocument_(d);
1737} 1739}
1738 1740
1741void invalidateLayout_GmDocument(iGmDocument *d) {
1742 d->isLayoutInvalidated = iTrue;
1743}
1744
1739static void markLinkRunsVisited_GmDocument_(iGmDocument *d, const iIntSet *linkIds) { 1745static void markLinkRunsVisited_GmDocument_(iGmDocument *d, const iIntSet *linkIds) {
1740 iForEach(Array, r, &d->layout) { 1746 iForEach(Array, r, &d->layout) {
1741 iGmRun *run = r.value; 1747 iGmRun *run = r.value;
@@ -2080,7 +2086,7 @@ static void convertMarkdownToGemtext_GmDocument_(iGmDocument *d) {
2080 d->format = gemini_SourceFormat; 2086 d->format = gemini_SourceFormat;
2081} 2087}
2082 2088
2083void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int outsideMargin, 2089void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int canvasWidth,
2084 enum iGmDocumentUpdate updateType) { 2090 enum iGmDocumentUpdate updateType) {
2085 /* TODO: This API has been set up to allow partial/progressive updating of the content. 2091 /* TODO: This API has been set up to allow partial/progressive updating of the content.
2086 Currently the entire source is replaced every time, though. */ 2092 Currently the entire source is replaced every time, though. */
@@ -2122,7 +2128,7 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int
2122 if (isNormalized_GmDocument_(d)) { 2128 if (isNormalized_GmDocument_(d)) {
2123 normalize_GmDocument(d); 2129 normalize_GmDocument(d);
2124 } 2130 }
2125 setWidth_GmDocument(d, width, outsideMargin); /* re-do layout */ 2131 setWidth_GmDocument(d, width, canvasWidth); /* re-do layout */
2126} 2132}
2127 2133
2128void foldPre_GmDocument(iGmDocument *d, uint16_t preId) { 2134void foldPre_GmDocument(iGmDocument *d, uint16_t preId) {
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 444520c6..58fc3db3 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -120,10 +120,8 @@ enum iGmRunFlags {
120 decoration_GmRunFlag = iBit(1), /* not part of the source */ 120 decoration_GmRunFlag = iBit(1), /* not part of the source */
121 startOfLine_GmRunFlag = iBit(2), 121 startOfLine_GmRunFlag = iBit(2),
122 endOfLine_GmRunFlag = iBit(3), 122 endOfLine_GmRunFlag = iBit(3),
123// siteBanner_GmRunFlag = iBit(4), /* area reserved for the site banner */
124 quoteBorder_GmRunFlag = iBit(5), 123 quoteBorder_GmRunFlag = iBit(5),
125 wide_GmRunFlag = iBit(6), /* horizontally scrollable */ 124 wide_GmRunFlag = iBit(6), /* horizontally scrollable */
126// footer_GmRunFlag = iBit(7),
127 altText_GmRunFlag = iBit(8), 125 altText_GmRunFlag = iBit(8),
128}; 126};
129 127
@@ -177,13 +175,6 @@ enum iGmDocumentWarning {
177 missingGlyphs_GmDocumentWarning = iBit(2), 175 missingGlyphs_GmDocumentWarning = iBit(2),
178}; 176};
179 177
180/*
181enum iGmDocumentBanner {
182 none_GmDocumentBanner,
183 siteDomain_GmDocumentBanner,
184 certificateWarning_GmDocumentBanner,
185};*/
186
187enum iGmDocumentUpdate { 178enum iGmDocumentUpdate {
188 partial_GmDocumentUpdate, /* appending more content */ 179 partial_GmDocumentUpdate, /* appending more content */
189 final_GmDocumentUpdate, /* process all lines, including the last one if not terminated */ 180 final_GmDocumentUpdate, /* process all lines, including the last one if not terminated */
@@ -191,12 +182,13 @@ enum iGmDocumentUpdate {
191 182
192void setThemeSeed_GmDocument (iGmDocument *, const iBlock *seed); 183void setThemeSeed_GmDocument (iGmDocument *, const iBlock *seed);
193void setFormat_GmDocument (iGmDocument *, enum iSourceFormat format); 184void setFormat_GmDocument (iGmDocument *, enum iSourceFormat format);
194//void setBanner_GmDocument (iGmDocument *, enum iGmDocumentBanner type); 185void setWidth_GmDocument (iGmDocument *, int width, int canvasWidth);
195void setWidth_GmDocument (iGmDocument *, int width, int outsideMargin); 186iBool updateWidth_GmDocument (iGmDocument *, int width, int canvasWidth);
196void redoLayout_GmDocument (iGmDocument *); 187void redoLayout_GmDocument (iGmDocument *);
188void invalidateLayout_GmDocument(iGmDocument *); /* will have to be redone later */
197iBool updateOpenURLs_GmDocument(iGmDocument *); 189iBool updateOpenURLs_GmDocument(iGmDocument *);
198void setUrl_GmDocument (iGmDocument *, const iString *url); 190void setUrl_GmDocument (iGmDocument *, const iString *url);
199void setSource_GmDocument (iGmDocument *, const iString *source, int width, int outsideMargin, 191void setSource_GmDocument (iGmDocument *, const iString *source, int width, int canvasWidth,
200 enum iGmDocumentUpdate updateType); 192 enum iGmDocumentUpdate updateType);
201void foldPre_GmDocument (iGmDocument *, uint16_t preId); 193void foldPre_GmDocument (iGmDocument *, uint16_t preId);
202 194
@@ -204,8 +196,6 @@ void updateVisitedLinks_GmDocument (iGmDocument *); /* check all links for
204void invalidatePalette_GmDocument (iGmDocument *); 196void invalidatePalette_GmDocument (iGmDocument *);
205void makePaletteGlobal_GmDocument (const iGmDocument *); /* copies document colors to the global palette */ 197void makePaletteGlobal_GmDocument (const iGmDocument *); /* copies document colors to the global palette */
206 198
207//void reset_GmDocument (iGmDocument *); /* free images */
208
209typedef void (*iGmDocumentRenderFunc)(void *, const iGmRun *); 199typedef void (*iGmDocumentRenderFunc)(void *, const iGmRun *);
210 200
211iMedia * media_GmDocument (iGmDocument *); 201iMedia * media_GmDocument (iGmDocument *);
@@ -219,10 +209,6 @@ const iGmRun * renderProgressive_GmDocument(const iGmDocument *d, const iGmRun
219 iRangei visRangeY, iGmDocumentRenderFunc render, 209 iRangei visRangeY, iGmDocumentRenderFunc render,
220 void *context); 210 void *context);
221iInt2 size_GmDocument (const iGmDocument *); 211iInt2 size_GmDocument (const iGmDocument *);
222//const iGmRun * siteBanner_GmDocument (const iGmDocument *);
223//iBool hasSiteBanner_GmDocument (const iGmDocument *);
224//enum iGmDocumentBanner bannerType_GmDocument(const iGmDocument *);
225//const iString * bannerText_GmDocument (const iGmDocument *);
226const iArray * headings_GmDocument (const iGmDocument *); /* array of GmHeadings */ 212const iArray * headings_GmDocument (const iGmDocument *); /* array of GmHeadings */
227const iString * source_GmDocument (const iGmDocument *); 213const iString * source_GmDocument (const iGmDocument *);
228size_t memorySize_GmDocument (const iGmDocument *); /* bytes */ 214size_t memorySize_GmDocument (const iGmDocument *); /* bytes */
diff --git a/src/gopher.c b/src/gopher.c
index ac5fe560..008a7743 100644
--- a/src/gopher.c
+++ b/src/gopher.c
@@ -103,10 +103,10 @@ static iBool convertSource_Gopher_(iGopher *d) {
103 for (;;) { 103 for (;;) {
104 /* Find the end of the line. */ 104 /* Find the end of the line. */
105 iRangecc line = { body.start, body.start }; 105 iRangecc line = { body.start, body.start };
106 while (line.end < body.end - 1 && !isCRLFLineTerminator_(line.end)) { 106 while (line.end < body.end - 1 && !isLineTerminator_(line.end)) {
107 line.end++; 107 line.end++;
108 } 108 }
109 if (line.end >= body.end - 1 || !isCRLFLineTerminator_(line.end)) { 109 if (line.end >= body.end - 1 || !isLineTerminator_(line.end)) {
110 /* Not a complete line. More may be coming later. */ 110 /* Not a complete line. More may be coming later. */
111 break; 111 break;
112 } 112 }
diff --git a/src/history.c b/src/history.c
index 208c239d..7185912f 100644
--- a/src/history.c
+++ b/src/history.c
@@ -440,6 +440,17 @@ void clearCache_History(iHistory *d) {
440 unlock_Mutex(d->mtx); 440 unlock_Mutex(d->mtx);
441} 441}
442 442
443void invalidateCachedLayout_History(iHistory *d) {
444 lock_Mutex(d->mtx);
445 iForEach(Array, i, &d->recent) {
446 iRecentUrl *url = i.value;
447 if (url->cachedDoc) {
448 invalidateLayout_GmDocument(url->cachedDoc);
449 }
450 }
451 unlock_Mutex(d->mtx);
452}
453
443size_t pruneLeastImportant_History(iHistory *d) { 454size_t pruneLeastImportant_History(iHistory *d) {
444 size_t delta = 0; 455 size_t delta = 0;
445 size_t chosen = iInvalidPos; 456 size_t chosen = iInvalidPos;
diff --git a/src/history.h b/src/history.h
index 7dad72df..d3daae80 100644
--- a/src/history.h
+++ b/src/history.h
@@ -76,6 +76,7 @@ void clearCache_History (iHistory *);
76size_t pruneLeastImportant_History (iHistory *); 76size_t pruneLeastImportant_History (iHistory *);
77size_t pruneLeastImportantMemory_History (iHistory *); 77size_t pruneLeastImportantMemory_History (iHistory *);
78void invalidateTheme_History (iHistory *); /* theme has changed, cached contents need updating */ 78void invalidateTheme_History (iHistory *); /* theme has changed, cached contents need updating */
79void invalidateCachedLayout_History (iHistory *);
79 80
80iBool atLatest_History (const iHistory *); 81iBool atLatest_History (const iHistory *);
81iBool atOldest_History (const iHistory *); 82iBool atOldest_History (const iHistory *);
diff --git a/src/lang.c b/src/lang.c
index 017937cf..18c7e374 100644
--- a/src/lang.c
+++ b/src/lang.c
@@ -84,6 +84,7 @@ static void clear_Lang_(iLang *d) {
84 84
85static void load_Lang_(iLang *d, const char *id) { 85static void load_Lang_(iLang *d, const char *id) {
86 /* Load compiled language strings from a resource blob. */ 86 /* Load compiled language strings from a resource blob. */
87 /* TODO: How about an array for these? (id, blob, pluralType) */
87 iUnused(id); 88 iUnused(id);
88 const iBlock *data = equal_CStr(id, "fi") ? &blobFi_Resources 89 const iBlock *data = equal_CStr(id, "fi") ? &blobFi_Resources
89 : equal_CStr(id, "fr") ? &blobFr_Resources 90 : equal_CStr(id, "fr") ? &blobFr_Resources
@@ -93,6 +94,7 @@ static void load_Lang_(iLang *d, const char *id) {
93 : equal_CStr(id, "es_MX") ? &blobEs_MX_Resources 94 : equal_CStr(id, "es_MX") ? &blobEs_MX_Resources
94 : equal_CStr(id, "de") ? &blobDe_Resources 95 : equal_CStr(id, "de") ? &blobDe_Resources
95 : equal_CStr(id, "gl") ? &blobGl_Resources 96 : equal_CStr(id, "gl") ? &blobGl_Resources
97 : equal_CStr(id, "hu") ? &blobHu_Embedded
96 : equal_CStr(id, "ia") ? &blobIa_Resources 98 : equal_CStr(id, "ia") ? &blobIa_Resources
97 : equal_CStr(id, "ie") ? &blobIe_Resources 99 : equal_CStr(id, "ie") ? &blobIe_Resources
98 : equal_CStr(id, "isv") ? &blobIsv_Resources 100 : equal_CStr(id, "isv") ? &blobIsv_Resources
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 79bfea7b..8c24d4a9 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -1702,6 +1702,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1702 } 1702 }
1703 if (cachedDoc) { 1703 if (cachedDoc) {
1704 replaceDocument_DocumentWidget_(d, cachedDoc); 1704 replaceDocument_DocumentWidget_(d, cachedDoc);
1705 updateWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d), width_Widget(d));
1705 } 1706 }
1706 else if (setSource) { 1707 else if (setSource) {
1707 setSource_DocumentWidget(d, &str); 1708 setSource_DocumentWidget(d, &str);
@@ -2498,7 +2499,7 @@ static iBool updateDocumentWidthRetainingScrollPosition_DocumentWidget_(iDocumen
2498 /* TODO: First *fully* visible run? */ 2499 /* TODO: First *fully* visible run? */
2499 voffset = visibleRange_DocumentWidget_(d).start - top_Rect(run->visBounds); 2500 voffset = visibleRange_DocumentWidget_(d).start - top_Rect(run->visBounds);
2500 } 2501 }
2501 setWidth_GmDocument(d->doc, newWidth, (width_Widget(d) - newWidth) / 2); 2502 setWidth_GmDocument(d->doc, newWidth, width_Widget(d));
2502 setWidth_Banner(d->banner, newWidth); 2503 setWidth_Banner(d->banner, newWidth);
2503 documentRunsInvalidated_DocumentWidget_(d); 2504 documentRunsInvalidated_DocumentWidget_(d);
2504 if (runLoc && !keepCenter) { 2505 if (runLoc && !keepCenter) {
@@ -2734,6 +2735,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2734 } 2735 }
2735 else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed") || 2736 else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed") ||
2736 equal_Command(cmd, "keyroot.changed")) { 2737 equal_Command(cmd, "keyroot.changed")) {
2738 if (equal_Command(cmd, "font.changed")) {
2739 invalidateCachedLayout_History(d->mod.history);
2740 }
2737 /* Alt/Option key may be involved in window size changes. */ 2741 /* Alt/Option key may be involved in window size changes. */
2738 setLinkNumberMode_DocumentWidget_(d, iFalse); 2742 setLinkNumberMode_DocumentWidget_(d, iFalse);
2739 d->phoneToolbar = findWidget_App("toolbar"); 2743 d->phoneToolbar = findWidget_App("toolbar");
diff --git a/src/ui/text.c b/src/ui/text.c
index e762b891..3805c666 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -2114,7 +2114,7 @@ iTextMetrics draw_WrapText(iWrapText *d, int fontId, iInt2 pos, int color) {
2114 const int width = d->mode == word_WrapTextMode 2114 const int width = d->mode == word_WrapTextMode
2115 ? tryAdvance_Text(fontId, text, d->maxWidth, &endPos).x 2115 ? tryAdvance_Text(fontId, text, d->maxWidth, &endPos).x
2116 : tryAdvanceNoWrap_Text(fontId, text, d->maxWidth, &endPos).x; 2116 : tryAdvanceNoWrap_Text(fontId, text, d->maxWidth, &endPos).x;
2117 notify_WrapText_(d, endPos, 0, width, iFalse); 2117 notify_WrapText_(d, endPos, (iTextAttrib){ .colorId = color }, 0, width);
2118 drawRange_Text(fontId, pos, color, (iRangecc){ text.start, endPos }); 2118 drawRange_Text(fontId, pos, color, (iRangecc){ text.start, endPos });
2119 text.start = endPos; 2119 text.start = endPos;
2120 pos.y += lineHeight_Text(fontId); 2120 pos.y += lineHeight_Text(fontId);
diff --git a/src/ui/text_simple.c b/src/ui/text_simple.c
index 8b1de64a..81fb94a5 100644
--- a/src/ui/text_simple.c
+++ b/src/ui/text_simple.c
@@ -61,6 +61,7 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
61 and other non-complex LTR scripts. Composed glyphs are not supported (must rely on text 61 and other non-complex LTR scripts. Composed glyphs are not supported (must rely on text
62 being in a pre-composed form). This algorithm is used if HarfBuzz is not available. */ 62 being in a pre-composed form). This algorithm is used if HarfBuzz is not available. */
63 const iInt2 orig = args->pos; 63 const iInt2 orig = args->pos;
64 iTextAttrib attrib = { .colorId = args->color };
64 iRect bounds = { orig, init_I2(0, d->height) }; 65 iRect bounds = { orig, init_I2(0, d->height) };
65 float xpos = orig.x; 66 float xpos = orig.x;
66 float xposMax = xpos; 67 float xposMax = xpos;
@@ -86,7 +87,7 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
86// } 87// }
87 const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); 88 const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2());
88 const iBool checkHitChar = wrap && wrap->hitChar; 89 const iBool checkHitChar = wrap && wrap->hitChar;
89 const iBool isMonospaced = d->isMonospaced && !(mode & alwaysVariableWidthFlag_RunMode); 90 const iBool isMonospaced = isMonospaced_Font(d) && !(mode & alwaysVariableWidthFlag_RunMode);
90 if (isMonospaced) { 91 if (isMonospaced) {
91 monoAdvance = glyph_Font_(d, 'M')->advance; 92 monoAdvance = glyph_Font_(d, 'M')->advance;
92 } 93 }
@@ -166,7 +167,7 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
166 /* TODO: Check out if `uc_wordbreak_property()` from libunistring can be used here. */ 167 /* TODO: Check out if `uc_wordbreak_property()` from libunistring can be used here. */
167 if (ch == '\n') { 168 if (ch == '\n') {
168 /* Notify about the wrap. */ 169 /* Notify about the wrap. */
169 if (!notify_WrapText_(wrap, chPos, 0, iMax(xpos, xposExtend) - orig.x, iFalse)) { 170 if (!notify_WrapText_(wrap, chPos, attrib, 0, iMax(xpos, xposExtend) - orig.x)) {
170 break; 171 break;
171 } 172 }
172 lastWordEnd = NULL; 173 lastWordEnd = NULL;
@@ -246,7 +247,7 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
246 wrapPos = iMin(wrapPos, args->text.end); 247 wrapPos = iMin(wrapPos, args->text.end);
247 advance = wrapAdvance; 248 advance = wrapAdvance;
248 } 249 }
249 if (!notify_WrapText_(wrap, wrapPos, 0, advance, iFalse)) { 250 if (!notify_WrapText_(wrap, wrapPos, attrib, 0, advance)) {
250 break; 251 break;
251 } 252 }
252 lastWordEnd = NULL; 253 lastWordEnd = NULL;
@@ -282,8 +283,7 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
282 } 283 }
283 /* Symbols and emojis are NOT monospaced, so must conform when the primary font 284 /* Symbols and emojis are NOT monospaced, so must conform when the primary font
284 is monospaced. Except with Japanese script, that's larger than the normal monospace. */ 285 is monospaced. Except with Japanese script, that's larger than the normal monospace. */
285 const iBool useMonoAdvance = 286 const iBool useMonoAdvance = monoAdvance > 0; // && !isJapanese_FontId(fontId_Text_(glyph->font));
286 monoAdvance > 0 && !isJapanese_FontId(fontId_Text_(glyph->font));
287 const float advance = (useMonoAdvance && glyph->advance > 0 ? monoAdvance : glyph->advance); 287 const float advance = (useMonoAdvance && glyph->advance > 0 ? monoAdvance : glyph->advance);
288 if (!isMeasuring_(mode) && ch != 0x20 /* don't bother rendering spaces */) { 288 if (!isMeasuring_(mode) && ch != 0x20 /* don't bother rendering spaces */) {
289 if (useMonoAdvance && dst.w > advance && glyph->font != d && !isEmoji) { 289 if (useMonoAdvance && dst.w > advance && glyph->font != d && !isEmoji) {
@@ -328,9 +328,9 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
328 if (enableKerning_Text && next) { 328 if (enableKerning_Text && next) {
329 const uint32_t nextGlyphIndex = glyphIndex_Font_(glyph->font, next); 329 const uint32_t nextGlyphIndex = glyphIndex_Font_(glyph->font, next);
330 int kern = stbtt_GetGlyphKernAdvance( 330 int kern = stbtt_GetGlyphKernAdvance(
331 &glyph->font->font, index_Glyph_(glyph), nextGlyphIndex); 331 &glyph->font->fontFile->stbInfo, index_Glyph_(glyph), nextGlyphIndex);
332 /* Nunito needs some kerning fixes. */ 332 /* Nunito needs some kerning fixes. */
333 if (glyph->font->family == nunito_TextFont) { 333 if (glyph->font->fontSpec->flags & fixNunitoKerning_FontSpecFlag) {
334 if (ch == 'W' && (next == 'i' || next == 'h')) { 334 if (ch == 'W' && (next == 'i' || next == 'h')) {
335 kern = -30; 335 kern = -30;
336 } 336 }
@@ -362,7 +362,7 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
362 break; 362 break;
363 } 363 }
364 } 364 }
365 notify_WrapText_(wrap, chPos, 0, xpos - orig.x, iFalse); 365 notify_WrapText_(wrap, chPos, attrib, 0, xpos - orig.x);
366 if (checkHitChar && wrap->hitChar == args->text.end) { 366 if (checkHitChar && wrap->hitChar == args->text.end) {
367 wrap->hitAdvance_out = sub_I2(init_I2(xpos, ypos), orig); 367 wrap->hitAdvance_out = sub_I2(init_I2(xpos, ypos), orig);
368 } 368 }
diff --git a/src/ui/util.c b/src/ui/util.c
index 6d0453ff..5a58551b 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1023,11 +1023,13 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) {
1023 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue); 1023 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue);
1024 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); 1024 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse);
1025 arrange_Widget(d); /* need to know the height */ 1025 arrange_Widget(d); /* need to know the height */
1026 iBool allowOverflow = iFalse;
1026 /* A vertical offset determined by a possible selected label in the menu. */ { 1027 /* A vertical offset determined by a possible selected label in the menu. */ {
1027 iConstForEach(ObjectList, child, children_Widget(d)) { 1028 iConstForEach(ObjectList, child, children_Widget(d)) {
1028 const iWidget *item = constAs_Widget(child.object); 1029 const iWidget *item = constAs_Widget(child.object);
1029 if (flags_Widget(item) & selected_WidgetFlag) { 1030 if (flags_Widget(item) & selected_WidgetFlag) {
1030 windowCoord.y -= item->rect.pos.y; 1031 windowCoord.y -= item->rect.pos.y;
1032 allowOverflow = iTrue;
1031 } 1033 }
1032 } 1034 }
1033 } 1035 }
@@ -1129,11 +1131,13 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) {
1129 rightExcess += r; 1131 rightExcess += r;
1130 } 1132 }
1131#endif 1133#endif
1132 if (bottomExcess > 0 && (!isPortraitPhone || !isSlidePanel)) { 1134 if (!allowOverflow) {
1133 d->rect.pos.y -= bottomExcess; 1135 if (bottomExcess > 0 && (!isPortraitPhone || !isSlidePanel)) {
1134 } 1136 d->rect.pos.y -= bottomExcess;
1135 if (topExcess > 0) { 1137 }
1136 d->rect.pos.y += topExcess; 1138 if (topExcess > 0) {
1139 d->rect.pos.y += topExcess;
1140 }
1137 } 1141 }
1138 if (rightExcess > 0) { 1142 if (rightExcess > 0) {
1139 d->rect.pos.x -= rightExcess; 1143 d->rect.pos.x -= rightExcess;
@@ -1455,12 +1459,17 @@ void addTabCloseButton_Widget(iWidget *tabs, const iWidget *page, const char *co
1455 iLabelWidget *tabButton = tabButtonForPage_Widget_(tabs, page); 1459 iLabelWidget *tabButton = tabButtonForPage_Widget_(tabs, page);
1456 setPadding_Widget(as_Widget(tabButton), 0, 0, 0, gap_UI / 4); 1460 setPadding_Widget(as_Widget(tabButton), 0, 0, 0, gap_UI / 4);
1457 setFlags_Widget(as_Widget(tabButton), arrangeVertical_WidgetFlag | resizeHeightOfChildren_WidgetFlag, iTrue); 1461 setFlags_Widget(as_Widget(tabButton), arrangeVertical_WidgetFlag | resizeHeightOfChildren_WidgetFlag, iTrue);
1462#if defined (iPlatformApple)
1463 const int64_t edge = moveToParentLeftEdge_WidgetFlag;
1464#else
1465 const int64_t edge = moveToParentRightEdge_WidgetFlag;
1466#endif
1458 iLabelWidget *close = addChildFlags_Widget( 1467 iLabelWidget *close = addChildFlags_Widget(
1459 as_Widget(tabButton), 1468 as_Widget(tabButton),
1460 iClob(new_LabelWidget(close_Icon, 1469 iClob(new_LabelWidget(close_Icon,
1461 format_CStr("%s id:%s", command, cstr_String(id_Widget(page))))), 1470 format_CStr("%s id:%s", command, cstr_String(id_Widget(page))))),
1462 moveToParentRightEdge_WidgetFlag | tight_WidgetFlag | frameless_WidgetFlag | 1471 edge | tight_WidgetFlag | frameless_WidgetFlag | noBackground_WidgetFlag |
1463 noBackground_WidgetFlag | hidden_WidgetFlag | visibleOnParentHover_WidgetFlag); 1472 hidden_WidgetFlag | visibleOnParentHover_WidgetFlag);
1464 if (deviceType_App() != desktop_AppDeviceType) { 1473 if (deviceType_App() != desktop_AppDeviceType) {
1465 setFlags_Widget(as_Widget(close), 1474 setFlags_Widget(as_Widget(close),
1466 hidden_WidgetFlag | visibleOnParentHover_WidgetFlag, iFalse); 1475 hidden_WidgetFlag | visibleOnParentHover_WidgetFlag, iFalse);
@@ -2198,6 +2207,7 @@ iWidget *makePreferences_Widget(void) {
2198 { "${lang.fi} - fi", 0, 0, "uilang id:fi" }, 2207 { "${lang.fi} - fi", 0, 0, "uilang id:fi" },
2199 { "${lang.fr} - fr", 0, 0, "uilang id:fr" }, 2208 { "${lang.fr} - fr", 0, 0, "uilang id:fr" },
2200 { "${lang.gl} - gl", 0, 0, "uilang id:gl" }, 2209 { "${lang.gl} - gl", 0, 0, "uilang id:gl" },
2210 { "${lang.hu} - hu", 0, 0, "uilang id:hu" },
2201 { "${lang.ia} - ia", 0, 0, "uilang id:ia" }, 2211 { "${lang.ia} - ia", 0, 0, "uilang id:ia" },
2202 { "${lang.ie} - ie", 0, 0, "uilang id:ie" }, 2212 { "${lang.ie} - ie", 0, 0, "uilang id:ie" },
2203 { "${lang.isv} - isv", 0, 0, "uilang id:isv" }, 2213 { "${lang.isv} - isv", 0, 0, "uilang id:isv" },
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 8a7127a2..b509cbe2 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -34,6 +34,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
34#include <the_Foundation/ptrarray.h> 34#include <the_Foundation/ptrarray.h>
35#include <the_Foundation/ptrset.h> 35#include <the_Foundation/ptrset.h>
36#include <SDL_mouse.h> 36#include <SDL_mouse.h>
37#include <SDL_timer.h>
37#include <stdarg.h> 38#include <stdarg.h>
38 39
39#if defined (iPlatformAppleMobile) 40#if defined (iPlatformAppleMobile)
@@ -1136,18 +1137,36 @@ void scrollInfo_Widget(const iWidget *d, iWidgetScrollInfo *info) {
1136 } 1137 }
1137} 1138}
1138 1139
1139iBool scrollOverflow_Widget(iWidget *d, int delta) { 1140static iBool isOverflowScrollPossible_Widget_(const iWidget *d, int delta) {
1141 if (~d->flags & overflowScrollable_WidgetFlag) {
1142 return iFalse;
1143 }
1140 iRect bounds = boundsWithoutVisualOffset_Widget(d); 1144 iRect bounds = boundsWithoutVisualOffset_Widget(d);
1141 const iRect winRect = adjusted_Rect(safeRect_Root(d->root), 1145 const iRect winRect = adjusted_Rect(safeRect_Root(d->root),
1142 zero_I2(), 1146 zero_I2(),
1143 init_I2(0, -get_MainWindow()->keyboardHeight)); 1147 init_I2(0, -get_MainWindow()->keyboardHeight));
1144 const int yTop = top_Rect(winRect); 1148 const int yTop = top_Rect(winRect);
1145 const int yBottom = bottom_Rect(winRect); 1149 const int yBottom = bottom_Rect(winRect);
1146 if (top_Rect(bounds) >= yTop && bottom_Rect(bounds) < yBottom) { 1150 if (delta == 0) {
1147 return iFalse; /* fits inside just fine */ 1151 if (top_Rect(bounds) >= yTop && bottom_Rect(bounds) <= yBottom) {
1152 return iFalse; /* fits inside just fine */
1153 }
1154 }
1155 else if (delta > 0) {
1156 return top_Rect(bounds) < yTop;
1157 }
1158 return bottom_Rect(bounds) > yBottom;
1159}
1160
1161iBool scrollOverflow_Widget(iWidget *d, int delta) {
1162 if (!isOverflowScrollPossible_Widget_(d, delta)) {
1163 return iFalse;
1148 } 1164 }
1149 //const int safeBottom = rootSize.y - yBottom; 1165 iRect bounds = boundsWithoutVisualOffset_Widget(d);
1150 iRangei validPosRange = { bottom_Rect(winRect) - height_Rect(bounds), yTop }; 1166 const iRect winRect = adjusted_Rect(safeRect_Root(d->root),
1167 zero_I2(),
1168 init_I2(0, -get_MainWindow()->keyboardHeight));
1169 iRangei validPosRange = { bottom_Rect(winRect) - height_Rect(bounds), top_Rect(winRect) };
1151 if (validPosRange.start > validPosRange.end) { 1170 if (validPosRange.start > validPosRange.end) {
1152 validPosRange.start = validPosRange.end; /* no room to scroll */ 1171 validPosRange.start = validPosRange.end; /* no room to scroll */
1153 } 1172 }
@@ -1170,21 +1189,29 @@ iBool scrollOverflow_Widget(iWidget *d, int delta) {
1170 else { 1189 else {
1171 bounds.pos.y = iClamp(bounds.pos.y, validPosRange.start, validPosRange.end); 1190 bounds.pos.y = iClamp(bounds.pos.y, validPosRange.start, validPosRange.end);
1172 } 1191 }
1173// if (delta >= 0) {
1174// bounds.pos.y = iMin(bounds.pos.y, yTop);
1175// }
1176// else {
1177// bounds.pos.y = iMax(bounds.pos.y, );
1178// }
1179 const iInt2 newPos = windowToInner_Widget(d->parent, bounds.pos); 1192 const iInt2 newPos = windowToInner_Widget(d->parent, bounds.pos);
1180 if (!isEqual_I2(newPos, d->rect.pos)) { 1193 if (!isEqual_I2(newPos, d->rect.pos)) {
1181 d->rect.pos = newPos; 1194 d->rect.pos = newPos;
1182// refresh_Widget(d);
1183 postRefresh_App(); 1195 postRefresh_App();
1184 } 1196 }
1185 return height_Rect(bounds) > height_Rect(winRect); 1197 return height_Rect(bounds) > height_Rect(winRect);
1186} 1198}
1187 1199
1200static uint32_t lastHoverOverflowMotionTime_;
1201
1202static void overflowHoverAnimation_(iAny *widget) {
1203 iWindow *win = window_Widget(widget);
1204 iInt2 coord = mouseCoord_Window(win, 0);
1205 /* A motion event will cause an overflow window to scroll. */
1206 SDL_MouseMotionEvent ev = {
1207 .type = SDL_MOUSEMOTION,
1208 .windowID = SDL_GetWindowID(win->win),
1209 .x = coord.x / win->pixelRatio,
1210 .y = coord.y / win->pixelRatio,
1211 };
1212 SDL_PushEvent((SDL_Event *) &ev);
1213}
1214
1188iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) { 1215iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
1189 if (d->flags & commandOnClick_WidgetFlag && 1216 if (d->flags & commandOnClick_WidgetFlag &&
1190 (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) && 1217 (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) &&
@@ -1202,14 +1229,45 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
1202 postCommand_Widget(d, "mouse.moved coord:%d %d", ev->motion.x, ev->motion.y); 1229 postCommand_Widget(d, "mouse.moved coord:%d %d", ev->motion.x, ev->motion.y);
1203 return iTrue; 1230 return iTrue;
1204 } 1231 }
1205 else if (d->flags & overflowScrollable_WidgetFlag && ev->type == SDL_MOUSEWHEEL && 1232 else if (d->flags & overflowScrollable_WidgetFlag && ~d->flags & visualOffset_WidgetFlag) {
1206 ~d->flags & visualOffset_WidgetFlag) { 1233 if (ev->type == SDL_MOUSEWHEEL) {
1207 int step = ev->wheel.y; 1234 int step = ev->wheel.y;
1208 if (!isPerPixel_MouseWheelEvent(&ev->wheel)) { 1235 if (!isPerPixel_MouseWheelEvent(&ev->wheel)) {
1209 step *= lineHeight_Text(uiLabel_FontId); 1236 step *= lineHeight_Text(uiLabel_FontId);
1237 }
1238 if (scrollOverflow_Widget(d, step)) {
1239 return iTrue;
1240 }
1210 } 1241 }
1211 if (scrollOverflow_Widget(d, step)) { 1242 else if (ev->type == SDL_MOUSEMOTION && ev->motion.which != SDL_TOUCH_MOUSEID &&
1212 return iTrue; 1243 ev->motion.y >= 0) {
1244 /* TODO: Motion events occur frequently. Maybe it would help if these were handled
1245 via audiences that specifically register to listen for motion, to minimize the
1246 number of widgets that need to process them. */
1247 const int hoverScrollLimit = 2 * lineHeight_Text(default_FontId);
1248 float speed = 0.0f;
1249 if (ev->motion.y < hoverScrollLimit) {
1250 speed = (hoverScrollLimit - ev->motion.y) / (float) hoverScrollLimit;
1251 }
1252 else {
1253 const int bottomLimit = bottom_Rect(rect_Root(d->root)) - hoverScrollLimit;
1254 if (ev->motion.y > bottomLimit ) {
1255 speed = -(ev->motion.y - bottomLimit) / (float) hoverScrollLimit;
1256 }
1257 }
1258 if (speed != 0.0f && isOverflowScrollPossible_Widget_(d, speed > 0 ? 1 : -1)) {
1259 const uint32_t nowTime = SDL_GetTicks();
1260 uint32_t elapsed = nowTime - lastHoverOverflowMotionTime_;
1261 if (elapsed > 100) {
1262 elapsed = 16;
1263 }
1264 int step = elapsed * gap_UI / 16 * iClamp(speed, -1.0f, 1.0f);
1265 if (step != 0) {
1266 lastHoverOverflowMotionTime_ = nowTime;
1267 scrollOverflow_Widget(d, step);
1268 }
1269 addTicker_App(overflowHoverAnimation_, d);
1270 }
1213 } 1271 }
1214 } 1272 }
1215 switch (ev->type) { 1273 switch (ev->type) {