summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-05-11 12:56:14 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-05-11 12:56:25 +0300
commit9f7a9058d3593d09a8fad9cd42b59f8a15873837 (patch)
tree75efa83e07e2130f45308e9bff627b569086fa3a
parenta13543aa922647ea2f8cc40cb9f3f797df8758df (diff)
Cache GmDocuments in memory
Navigation history keeps final GmDocuments in memory for quicker restore when navigating; no need to redo layout. Changed the color escape to Vertical Tab so Carriage Returns can be left in the source, reducing need to normalize spaces.
-rw-r--r--src/gmdocument.c69
-rw-r--r--src/gmdocument.h15
-rw-r--r--src/history.c72
-rw-r--r--src/history.h3
-rw-r--r--src/macos.m2
-rw-r--r--src/media.c5
-rw-r--r--src/media.h2
-rw-r--r--src/ui/color.c6
-rw-r--r--src/ui/color.h40
-rw-r--r--src/ui/documentwidget.c52
-rw-r--r--src/ui/labelwidget.c2
-rw-r--r--src/ui/text.c4
12 files changed, 203 insertions, 69 deletions
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 466169fc..da99fd0d 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -47,7 +47,7 @@ iBool isDark_GmDocumentTheme(enum iGmDocumentTheme d) {
47iDeclareType(GmLink) 47iDeclareType(GmLink)
48 48
49struct Impl_GmLink { 49struct Impl_GmLink {
50 iString url; 50 iString url; /* resolved */
51 iRangecc urlRange; /* URL in the source */ 51 iRangecc urlRange; /* URL in the source */
52 iRangecc labelRange; /* label in the source */ 52 iRangecc labelRange; /* label in the source */
53 iRangecc labelIcon; /* special icon defined in the label text */ 53 iRangecc labelIcon; /* special icon defined in the label text */
@@ -74,8 +74,9 @@ iDefineTypeConstruction(GmLink)
74struct Impl_GmDocument { 74struct Impl_GmDocument {
75 iObject object; 75 iObject object;
76 enum iGmDocumentFormat format; 76 enum iGmDocumentFormat format;
77 iString source; 77 iString unormSource; /* unnormalized source */
78 iString url; /* for resolving relative links */ 78 iString source; /* normalized source */
79 iString url; /* for resolving relative links */
79 iString localHost; 80 iString localHost;
80 iInt2 size; 81 iInt2 size;
81 iArray layout; /* contents of source, laid out in document space */ 82 iArray layout; /* contents of source, laid out in document space */
@@ -437,7 +438,10 @@ static void doLayout_GmDocument_(iGmDocument *d) {
437 isFirstText = iFalse; 438 isFirstText = iFalse;
438 } 439 }
439 while (nextSplit_Rangecc(content, "\n", &contentLine)) { 440 while (nextSplit_Rangecc(content, "\n", &contentLine)) {
440 iRangecc line = contentLine; /* `line` will be trimmed later; would confuse nextSplit */ 441 iRangecc line = contentLine; /* `line` will be trimmed; modifying would confuse `nextSplit_Rangecc` */
442 if (*line.end == '\r') {
443 line.end--; /* trim CR always */
444 }
441 iGmRun run = { .color = white_ColorId }; 445 iGmRun run = { .color = white_ColorId };
442 enum iGmLineType type; 446 enum iGmLineType type;
443 float indent = 0.0f; 447 float indent = 0.0f;
@@ -844,10 +848,13 @@ static void doLayout_GmDocument_(iGmDocument *d) {
844 } 848 }
845 } 849 }
846 } 850 }
851 printf("[GmDocument] layout size: %zu runs (%zu bytes)\n",
852 size_Array(&d->layout), size_Array(&d->layout) * sizeof(iGmRun));
847} 853}
848 854
849void init_GmDocument(iGmDocument *d) { 855void init_GmDocument(iGmDocument *d) {
850 d->format = gemini_GmDocumentFormat; 856 d->format = gemini_GmDocumentFormat;
857 init_String(&d->unormSource);
851 init_String(&d->source); 858 init_String(&d->source);
852 init_String(&d->url); 859 init_String(&d->url);
853 init_String(&d->localHost); 860 init_String(&d->localHost);
@@ -878,6 +885,7 @@ void deinit_GmDocument(iGmDocument *d) {
878 deinit_String(&d->localHost); 885 deinit_String(&d->localHost);
879 deinit_String(&d->url); 886 deinit_String(&d->url);
880 deinit_String(&d->source); 887 deinit_String(&d->source);
888 deinit_String(&d->unormSource);
881} 889}
882 890
883iMedia *media_GmDocument(iGmDocument *d) { 891iMedia *media_GmDocument(iGmDocument *d) {
@@ -888,6 +896,7 @@ const iMedia *constMedia_GmDocument(const iGmDocument *d) {
888 return d->media; 896 return d->media;
889} 897}
890 898
899#if 0
891void reset_GmDocument(iGmDocument *d) { 900void reset_GmDocument(iGmDocument *d) {
892 clear_Media(d->media); 901 clear_Media(d->media);
893 clearLinks_GmDocument_(d); 902 clearLinks_GmDocument_(d);
@@ -896,8 +905,11 @@ void reset_GmDocument(iGmDocument *d) {
896 clear_Array(&d->preMeta); 905 clear_Array(&d->preMeta);
897 clear_String(&d->url); 906 clear_String(&d->url);
898 clear_String(&d->localHost); 907 clear_String(&d->localHost);
908 clear_String(&d->source);
909 clear_String(&d->unormSource);
899 d->themeSeed = 0; 910 d->themeSeed = 0;
900} 911}
912#endif
901 913
902static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { 914static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {
903 set_Color(tmQuoteIcon_ColorId, 915 set_Color(tmQuoteIcon_ColorId,
@@ -1418,6 +1430,8 @@ static void normalize_GmDocument(iGmDocument *d) {
1418 isPreformat = iTrue; /* Cannot be turned off. */ 1430 isPreformat = iTrue; /* Cannot be turned off. */
1419 } 1431 }
1420 const int preTabWidth = 4; /* TODO: user-configurable parameter */ 1432 const int preTabWidth = 4; /* TODO: user-configurable parameter */
1433 iBool wasNormalized = iFalse;
1434 iBool hasTabs = iFalse;
1421 while (nextSplit_Rangecc(src, "\n", &line)) { 1435 while (nextSplit_Rangecc(src, "\n", &line)) {
1422 if (isPreformat) { 1436 if (isPreformat) {
1423 /* Replace any tab characters with spaces for visualization. */ 1437 /* Replace any tab characters with spaces for visualization. */
@@ -1428,10 +1442,16 @@ static void normalize_GmDocument(iGmDocument *d) {
1428 while (numSpaces-- > 0) { 1442 while (numSpaces-- > 0) {
1429 appendCStrN_String(normalized, " ", 1); 1443 appendCStrN_String(normalized, " ", 1);
1430 } 1444 }
1445 hasTabs = iTrue;
1446 wasNormalized = iTrue;
1431 } 1447 }
1432 else if (*ch != '\r') { 1448 else if (*ch != '\v') {
1433 appendCStrN_String(normalized, ch, 1); 1449 appendCStrN_String(normalized, ch, 1);
1434 } 1450 }
1451 else {
1452 hasTabs = iTrue;
1453 wasNormalized = iTrue;
1454 }
1435 } 1455 }
1436 appendCStr_String(normalized, "\n"); 1456 appendCStr_String(normalized, "\n");
1437 if (d->format == gemini_GmDocumentFormat && 1457 if (d->format == gemini_GmDocumentFormat &&
@@ -1450,7 +1470,10 @@ static void normalize_GmDocument(iGmDocument *d) {
1450 int spaceCount = 0; 1470 int spaceCount = 0;
1451 for (const char *ch = line.start; ch != line.end; ch++) { 1471 for (const char *ch = line.start; ch != line.end; ch++) {
1452 char c = *ch; 1472 char c = *ch;
1453 if (c == '\r') continue; 1473 if (c == '\v') {
1474 wasNormalized = iTrue;
1475 continue;
1476 }
1454 if (isNormalizableSpace_(c)) { 1477 if (isNormalizableSpace_(c)) {
1455 if (isPrevSpace) { 1478 if (isPrevSpace) {
1456 if (++spaceCount == 8) { 1479 if (++spaceCount == 8) {
@@ -1459,9 +1482,13 @@ static void normalize_GmDocument(iGmDocument *d) {
1459 popBack_Block(&normalized->chars); 1482 popBack_Block(&normalized->chars);
1460 pushBack_Block(&normalized->chars, '\t'); 1483 pushBack_Block(&normalized->chars, '\t');
1461 } 1484 }
1485 wasNormalized = iTrue;
1462 continue; /* skip repeated spaces */ 1486 continue; /* skip repeated spaces */
1463 } 1487 }
1464 c = ' '; 1488 if (c != ' ') {
1489 c = ' ';
1490 wasNormalized = iTrue;
1491 }
1465 isPrevSpace = iTrue; 1492 isPrevSpace = iTrue;
1466 } 1493 }
1467 else { 1494 else {
@@ -1472,7 +1499,13 @@ static void normalize_GmDocument(iGmDocument *d) {
1472 } 1499 }
1473 appendCStr_String(normalized, "\n"); 1500 appendCStr_String(normalized, "\n");
1474 } 1501 }
1502 printf("hasTabs: %d\n", hasTabs);
1503 printf("wasNormalized: %d\n", wasNormalized);
1504 fflush(stdout);
1475 set_String(&d->source, collect_String(normalized)); 1505 set_String(&d->source, collect_String(normalized));
1506 printf("orig:%zu norm:%zu\n", size_String(&d->unormSource), size_String(&d->source));
1507 /* normalized source has an extra newline at the end */
1508 iAssert(wasNormalized || equal_String(&d->unormSource, &d->source));
1476} 1509}
1477 1510
1478void setUrl_GmDocument(iGmDocument *d, const iString *url) { 1511void setUrl_GmDocument(iGmDocument *d, const iString *url) {
@@ -1483,8 +1516,18 @@ void setUrl_GmDocument(iGmDocument *d, const iString *url) {
1483 updateIconBasedOnUrl_GmDocument_(d); 1516 updateIconBasedOnUrl_GmDocument_(d);
1484} 1517}
1485 1518
1486void setSource_GmDocument(iGmDocument *d, const iString *source, int width) { 1519void setSource_GmDocument(iGmDocument *d, const iString *source, int width,
1487 set_String(&d->source, source); 1520 enum iGmDocumentUpdate updateType) {
1521 printf("[GmDocument] source update (%zu bytes), width:%d, final:%d\n",
1522 size_String(source), width, updateType == final_GmDocumentUpdate);
1523 if (size_String(source) == size_String(&d->unormSource)) {
1524 iAssert(equal_String(source, &d->unormSource));
1525 printf("[GmDocument] source is unchanged!\n");
1526 return; /* Nothing to do. */
1527 }
1528 set_String(&d->unormSource, source);
1529 /* Normalize. */
1530 set_String(&d->source, &d->unormSource);
1488 if (isNormalized_GmDocument_(d)) { 1531 if (isNormalized_GmDocument_(d)) {
1489 normalize_GmDocument(d); 1532 normalize_GmDocument(d);
1490 } 1533 }
@@ -1586,6 +1629,14 @@ const iString *source_GmDocument(const iGmDocument *d) {
1586 return &d->source; 1629 return &d->source;
1587} 1630}
1588 1631
1632size_t memorySize_GmDocument(const iGmDocument *d) {
1633 return size_String(&d->unormSource) +
1634 size_String(&d->source) +
1635 size_Array(&d->layout) * sizeof(iGmRun) +
1636 size_Array(&d->links) * sizeof(iGmLink) +
1637 memorySize_Media(d->media);
1638}
1639
1589iRangecc findText_GmDocument(const iGmDocument *d, const iString *text, const char *start) { 1640iRangecc findText_GmDocument(const iGmDocument *d, const iString *text, const char *start) {
1590 const char * src = constBegin_String(&d->source); 1641 const char * src = constBegin_String(&d->source);
1591 const size_t startPos = (start ? start - src : 0); 1642 const size_t startPos = (start ? start - src : 0);
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 574f0acf..1e54a16a 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -125,12 +125,12 @@ enum iGmRunMediaType {
125 125
126struct Impl_GmRun { 126struct Impl_GmRun {
127 iRangecc text; 127 iRangecc text;
128 iRect bounds; /* used for hit testing, may extend to edges */
129 iRect visBounds; /* actual visual bounds */
128 uint8_t font; 130 uint8_t font;
129 uint8_t color; 131 uint8_t color;
130 uint8_t flags; 132 uint8_t flags;
131 uint8_t mediaType; 133 uint8_t mediaType;
132 iRect bounds; /* used for hit testing, may extend to edges */
133 iRect visBounds; /* actual visual bounds */
134 uint16_t preId; /* preformatted block ID (sequential) */ 134 uint16_t preId; /* preformatted block ID (sequential) */
135 iGmLinkId linkId; /* zero for non-links */ 135 iGmLinkId linkId; /* zero for non-links */
136 uint16_t mediaId; /* zero if not an image */ 136 uint16_t mediaId; /* zero if not an image */
@@ -160,6 +160,11 @@ enum iGmDocumentBanner {
160 certificateWarning_GmDocumentBanner, 160 certificateWarning_GmDocumentBanner,
161}; 161};
162 162
163enum iGmDocumentUpdate {
164 partial_GmDocumentUpdate, /* appending more content */
165 final_GmDocumentUpdate, /* process all lines, including the last one if not terminated */
166};
167
163void setThemeSeed_GmDocument (iGmDocument *, const iBlock *seed); 168void setThemeSeed_GmDocument (iGmDocument *, const iBlock *seed);
164void setFormat_GmDocument (iGmDocument *, enum iGmDocumentFormat format); 169void setFormat_GmDocument (iGmDocument *, enum iGmDocumentFormat format);
165void setBanner_GmDocument (iGmDocument *, enum iGmDocumentBanner type); 170void setBanner_GmDocument (iGmDocument *, enum iGmDocumentBanner type);
@@ -167,10 +172,11 @@ void setWidth_GmDocument (iGmDocument *, int width);
167void redoLayout_GmDocument (iGmDocument *); 172void redoLayout_GmDocument (iGmDocument *);
168iBool updateOpenURLs_GmDocument(iGmDocument *); 173iBool updateOpenURLs_GmDocument(iGmDocument *);
169void setUrl_GmDocument (iGmDocument *, const iString *url); 174void setUrl_GmDocument (iGmDocument *, const iString *url);
170void setSource_GmDocument (iGmDocument *, const iString *source, int width); 175void setSource_GmDocument (iGmDocument *, const iString *source, int width,
176 enum iGmDocumentUpdate updateType);
171void foldPre_GmDocument (iGmDocument *, uint16_t preId); 177void foldPre_GmDocument (iGmDocument *, uint16_t preId);
172 178
173void reset_GmDocument (iGmDocument *); /* free images */ 179//void reset_GmDocument (iGmDocument *); /* free images */
174 180
175typedef void (*iGmDocumentRenderFunc)(void *, const iGmRun *); 181typedef void (*iGmDocumentRenderFunc)(void *, const iGmRun *);
176 182
@@ -190,6 +196,7 @@ enum iGmDocumentBanner bannerType_GmDocument(const iGmDocument *);
190const iString * bannerText_GmDocument (const iGmDocument *); 196const iString * bannerText_GmDocument (const iGmDocument *);
191const iArray * headings_GmDocument (const iGmDocument *); /* array of GmHeadings */ 197const iArray * headings_GmDocument (const iGmDocument *); /* array of GmHeadings */
192const iString * source_GmDocument (const iGmDocument *); 198const iString * source_GmDocument (const iGmDocument *);
199size_t memorySize_GmDocument (const iGmDocument *); /* bytes */
193 200
194iRangecc findText_GmDocument (const iGmDocument *, const iString *text, const char *start); 201iRangecc findText_GmDocument (const iGmDocument *, const iString *text, const char *start);
195iRangecc findTextBefore_GmDocument (const iGmDocument *, const iString *text, const char *before); 202iRangecc findTextBefore_GmDocument (const iGmDocument *, const iString *text, const char *before);
diff --git a/src/history.c b/src/history.c
index 9f4e415b..58ffa5c4 100644
--- a/src/history.c
+++ b/src/history.c
@@ -34,11 +34,13 @@ static const size_t maxStack_History_ = 50; /* back/forward navigable items */
34 34
35void init_RecentUrl(iRecentUrl *d) { 35void init_RecentUrl(iRecentUrl *d) {
36 init_String(&d->url); 36 init_String(&d->url);
37 d->normScrollY = 0; 37 d->normScrollY = 0;
38 d->cachedResponse = NULL; 38 d->cachedResponse = NULL;
39 d->cachedDoc = NULL;
39} 40}
40 41
41void deinit_RecentUrl(iRecentUrl *d) { 42void deinit_RecentUrl(iRecentUrl *d) {
43 iRelease(d->cachedDoc);
42 deinit_String(&d->url); 44 deinit_String(&d->url);
43 delete_GmResponse(d->cachedResponse); 45 delete_GmResponse(d->cachedResponse);
44} 46}
@@ -48,11 +50,29 @@ iDefineTypeConstruction(RecentUrl)
48iRecentUrl *copy_RecentUrl(const iRecentUrl *d) { 50iRecentUrl *copy_RecentUrl(const iRecentUrl *d) {
49 iRecentUrl *copy = new_RecentUrl(); 51 iRecentUrl *copy = new_RecentUrl();
50 set_String(&copy->url, &d->url); 52 set_String(&copy->url, &d->url);
51 copy->normScrollY = d->normScrollY; 53 copy->normScrollY = d->normScrollY;
52 copy->cachedResponse = d->cachedResponse ? copy_GmResponse(d->cachedResponse) : NULL; 54 copy->cachedResponse = d->cachedResponse ? copy_GmResponse(d->cachedResponse) : NULL;
55 copy->cachedDoc = ref_Object(d->cachedDoc);
53 return copy; 56 return copy;
54} 57}
55 58
59size_t cacheSize_RecentUrl(const iRecentUrl *d) {
60 size_t size = 0;
61 if (d->cachedResponse) {
62 size += size_String(&d->cachedResponse->meta);
63 size += size_Block(&d->cachedResponse->body);
64 }
65 return size;
66}
67
68size_t memorySize_RecentUrl(const iRecentUrl *d) {
69 size_t size = cacheSize_RecentUrl(d);
70 if (d->cachedDoc) {
71 size += memorySize_GmDocument(d->cachedDoc);
72 }
73 return size;
74}
75
56/*----------------------------------------------------------------------------------------------*/ 76/*----------------------------------------------------------------------------------------------*/
57 77
58struct Impl_History { 78struct Impl_History {
@@ -92,20 +112,31 @@ iString *debugInfo_History(const iHistory *d) {
92 iString *str = new_String(); 112 iString *str = new_String();
93 format_String(str, 113 format_String(str,
94 "```\n" 114 "```\n"
95 "Idx | Size | SP%% | URL\n" 115 "Idx | Cache | Memory | SP%% | URL\n"
96 "----+---------+-----+-----\n"); 116 "----+---------+----------+-----+-----\n");
97 size_t totalSize = 0; 117 size_t totalCache = 0;
118 size_t totalMemory = 0;
98 iConstForEach(Array, i, &d->recent) { 119 iConstForEach(Array, i, &d->recent) {
99 const iRecentUrl *item = i.value; 120 const iRecentUrl *item = i.value;
100 appendFormat_String( 121 appendFormat_String(
101 str, " %2zu | ", size_Array(&d->recent) - index_ArrayConstIterator(&i) - 1); 122 str, " %2zu | ", size_Array(&d->recent) - index_ArrayConstIterator(&i) - 1);
102 if (item->cachedResponse) { 123 const size_t cacheSize = cacheSize_RecentUrl(item);
103 appendFormat_String(str, "%7zu", size_Block(&item->cachedResponse->body)); 124 const size_t memSize = memorySize_RecentUrl(item);
104 totalSize += size_Block(&item->cachedResponse->body); 125 if (cacheSize) {
126 appendFormat_String(str, "%7zu", cacheSize);
127 totalCache += cacheSize;
105 } 128 }
106 else { 129 else {
107 appendFormat_String(str, " --"); 130 appendFormat_String(str, " --");
108 } 131 }
132 appendCStr_String(str, " | ");
133 if (memSize) {
134 appendFormat_String(str, "%8zu", memSize);
135 totalMemory += memSize;
136 }
137 else {
138 appendFormat_String(str, " --");
139 }
109 appendFormat_String(str, 140 appendFormat_String(str,
110 " | %3d | %s\n", 141 " | %3d | %s\n",
111 iRound(100.0f * item->normScrollY), 142 iRound(100.0f * item->normScrollY),
@@ -114,8 +145,10 @@ iString *debugInfo_History(const iHistory *d) {
114 appendFormat_String(str, "\n```\n"); 145 appendFormat_String(str, "\n```\n");
115 appendFormat_String(str, 146 appendFormat_String(str,
116 "Total cached data: %.3f MB\n" 147 "Total cached data: %.3f MB\n"
148 "Total memory usage: %.3f MB\n"
117 "Navigation position: %zu\n\n", 149 "Navigation position: %zu\n\n",
118 totalSize / 1.0e6f, 150 totalCache / 1.0e6f,
151 totalMemory / 1.0e6f,
119 d->recentPos); 152 d->recentPos);
120 return str; 153 return str;
121} 154}
@@ -301,14 +334,22 @@ void setCachedResponse_History(iHistory *d, const iGmResponse *response) {
301 unlock_Mutex(d->mtx); 334 unlock_Mutex(d->mtx);
302} 335}
303 336
337void setCachedDocument_History(iHistory *d, iGmDocument *doc) {
338 lock_Mutex(d->mtx);
339 iRecentUrl *item = mostRecentUrl_History(d);
340 if (item && item->cachedDoc != doc) {
341 iRelease(item->cachedDoc);
342 item->cachedDoc = ref_Object(doc);
343 }
344 unlock_Mutex(d->mtx);
345}
346
304size_t cacheSize_History(const iHistory *d) { 347size_t cacheSize_History(const iHistory *d) {
305 size_t cached = 0; 348 size_t cached = 0;
306 lock_Mutex(d->mtx); 349 lock_Mutex(d->mtx);
307 iConstForEach(Array, i, &d->recent) { 350 iConstForEach(Array, i, &d->recent) {
308 const iRecentUrl *url = i.value; 351 const iRecentUrl *url = i.value;
309 if (url->cachedResponse) { 352 cached += cacheSize_RecentUrl(url);
310 cached += size_Block(&url->cachedResponse->body);
311 }
312 } 353 }
313 unlock_Mutex(d->mtx); 354 unlock_Mutex(d->mtx);
314 return cached; 355 return cached;
@@ -335,9 +376,9 @@ size_t pruneLeastImportant_History(iHistory *d) {
335 lock_Mutex(d->mtx); 376 lock_Mutex(d->mtx);
336 iConstForEach(Array, i, &d->recent) { 377 iConstForEach(Array, i, &d->recent) {
337 const iRecentUrl *url = i.value; 378 const iRecentUrl *url = i.value;
338 if (url->cachedResponse) { 379 if (url->cachedResponse || url->cachedDoc) {
339 const double urlScore = 380 const double urlScore =
340 size_Block(&url->cachedResponse->body) * 381 cacheSize_RecentUrl(url) *
341 pow(secondsSince_Time(&now, &url->cachedResponse->when) / 60.0, 1.25); 382 pow(secondsSince_Time(&now, &url->cachedResponse->when) / 60.0, 1.25);
342 if (urlScore > score) { 383 if (urlScore > score) {
343 chosen = index_ArrayConstIterator(&i); 384 chosen = index_ArrayConstIterator(&i);
@@ -347,9 +388,10 @@ size_t pruneLeastImportant_History(iHistory *d) {
347 } 388 }
348 if (chosen != iInvalidPos) { 389 if (chosen != iInvalidPos) {
349 iRecentUrl *url = at_Array(&d->recent, chosen); 390 iRecentUrl *url = at_Array(&d->recent, chosen);
350 delta = size_Block(&url->cachedResponse->body); 391 delta = cacheSize_RecentUrl(url);
351 delete_GmResponse(url->cachedResponse); 392 delete_GmResponse(url->cachedResponse);
352 url->cachedResponse = NULL; 393 url->cachedResponse = NULL;
394 iReleasePtr(&url->cachedDoc);
353 } 395 }
354 unlock_Mutex(d->mtx); 396 unlock_Mutex(d->mtx);
355 return delta; 397 return delta;
diff --git a/src/history.h b/src/history.h
index 164a61d6..1acf7049 100644
--- a/src/history.h
+++ b/src/history.h
@@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#pragma once 23#pragma once
24 24
25#include "gmdocument.h"
25#include "gmrequest.h" 26#include "gmrequest.h"
26 27
27#include <the_Foundation/ptrarray.h> 28#include <the_Foundation/ptrarray.h>
@@ -37,6 +38,7 @@ struct Impl_RecentUrl {
37 iString url; 38 iString url;
38 float normScrollY; /* normalized to document height */ 39 float normScrollY; /* normalized to document height */
39 iGmResponse *cachedResponse; /* kept in memory for quicker back navigation */ 40 iGmResponse *cachedResponse; /* kept in memory for quicker back navigation */
41 iGmDocument *cachedDoc; /* cached copy of the presentation: layout and media (not serialized) */
40}; 42};
41 43
42/*----------------------------------------------------------------------------------------------*/ 44/*----------------------------------------------------------------------------------------------*/
@@ -51,6 +53,7 @@ void clear_History (iHistory *);
51void add_History (iHistory *, const iString *url); 53void add_History (iHistory *, const iString *url);
52void replace_History (iHistory *, const iString *url); 54void replace_History (iHistory *, const iString *url);
53void setCachedResponse_History (iHistory *, const iGmResponse *response); 55void setCachedResponse_History (iHistory *, const iGmResponse *response);
56void setCachedDocument_History (iHistory *, iGmDocument *doc);
54iBool goBack_History (iHistory *); 57iBool goBack_History (iHistory *);
55iBool goForward_History (iHistory *); 58iBool goForward_History (iHistory *);
56iRecentUrl *recentUrl_History (iHistory *, size_t pos); 59iRecentUrl *recentUrl_History (iHistory *, size_t pos);
diff --git a/src/macos.m b/src/macos.m
index 61507b6f..709a97f7 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -479,7 +479,7 @@ void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *
479 [menu setAutoenablesItems:NO]; 479 [menu setAutoenablesItems:NO];
480 for (size_t i = 0; i < count; ++i) { 480 for (size_t i = 0; i < count; ++i) {
481 const char *label = translateCStr_Lang(items[i].label); 481 const char *label = translateCStr_Lang(items[i].label);
482 if (label[0] == '\r') { 482 if (label[0] == '\v') {
483 /* Skip the formatting escape. */ 483 /* Skip the formatting escape. */
484 label += 2; 484 label += 2;
485 } 485 }
diff --git a/src/media.c b/src/media.c
index 1313b7da..2ec2109d 100644
--- a/src/media.c
+++ b/src/media.c
@@ -261,6 +261,11 @@ void clear_Media(iMedia *d) {
261 clear_PtrArray(&d->downloads); 261 clear_PtrArray(&d->downloads);
262} 262}
263 263
264size_t memorySize_Media(const iMedia *d) {
265 /* TODO: Calculate the actual memory use. */
266 return 0;
267}
268
264iBool setDownloadUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) { 269iBool setDownloadUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) {
265 iGmDownload *dl = NULL; 270 iGmDownload *dl = NULL;
266 iMediaId existing = findLinkDownload_Media(d, linkId); 271 iMediaId existing = findLinkDownload_Media(d, linkId);
diff --git a/src/media.h b/src/media.h
index 7cc941d0..c6973b79 100644
--- a/src/media.h
+++ b/src/media.h
@@ -50,6 +50,8 @@ void clear_Media (iMedia *);
50iBool setDownloadUrl_Media (iMedia *, uint16_t linkId, const iString *url); 50iBool setDownloadUrl_Media (iMedia *, uint16_t linkId, const iString *url);
51iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags); 51iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);
52 52
53size_t memorySize_Media (const iMedia *);
54
53iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId); 55iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId);
54iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out); 56iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out);
55iInt2 imageSize_Media (const iMedia *, iMediaId imageId); 57iInt2 imageSize_Media (const iMedia *, iMediaId imageId);
diff --git a/src/ui/color.c b/src/ui/color.c
index 6cbbdf28..0a177a8a 100644
--- a/src/ui/color.c
+++ b/src/ui/color.c
@@ -468,12 +468,12 @@ const char *escape_Color(int color) {
468 return esc[color]; 468 return esc[color];
469 } 469 }
470 /* TODO: Conflict with format strings! "%" (37) may be used as the color value. */ 470 /* TODO: Conflict with format strings! "%" (37) may be used as the color value. */
471 /* Double-\r is used for range extension. */ 471 /* Double-\v is used for range extension. */
472 if (color + asciiBase_ColorEscape > 127) { 472 if (color + asciiBase_ColorEscape > 127) {
473 iAssert(color - asciiExtended_ColorEscape + asciiBase_ColorEscape <= 127); 473 iAssert(color - asciiExtended_ColorEscape + asciiBase_ColorEscape <= 127);
474 return format_CStr("\r\r%c", color - asciiExtended_ColorEscape + asciiBase_ColorEscape); 474 return format_CStr("\v\v%c", color - asciiExtended_ColorEscape + asciiBase_ColorEscape);
475 } 475 }
476 return format_CStr("\r%c", color + asciiBase_ColorEscape); 476 return format_CStr("\v%c", color + asciiBase_ColorEscape);
477} 477}
478 478
479iHSLColor setSat_HSLColor(iHSLColor d, float sat) { 479iHSLColor setSat_HSLColor(iHSLColor d, float sat) {
diff --git a/src/ui/color.h b/src/ui/color.h
index d2fa3c00..aafc1794 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -187,26 +187,26 @@ iLocalDef iBool isRegularText_ColorId(enum iColorId d) {
187#define asciiBase_ColorEscape 33 187#define asciiBase_ColorEscape 33
188#define asciiExtended_ColorEscape (128 - asciiBase_ColorEscape) 188#define asciiExtended_ColorEscape (128 - asciiBase_ColorEscape)
189 189
190#define restore_ColorEscape "\r\x24" /* ASCII Cancel */ 190#define restore_ColorEscape "\v\x24" /* ASCII Cancel */
191#define black_ColorEscape "\r!" 191#define black_ColorEscape "\v!"
192#define gray25_ColorEscape "\r\"" 192#define gray25_ColorEscape "\v\""
193#define gray50_ColorEscape "\r#" 193#define gray50_ColorEscape "\v#"
194#define gray75_ColorEscape "\r$" 194#define gray75_ColorEscape "\v$"
195#define white_ColorEscape "\r%" 195#define white_ColorEscape "\v%"
196#define brown_ColorEscape "\r&" 196#define brown_ColorEscape "\v&"
197#define orange_ColorEscape "\r'" 197#define orange_ColorEscape "\v'"
198#define teal_ColorEscape "\r(" 198#define teal_ColorEscape "\v("
199#define cyan_ColorEscape "\r)" 199#define cyan_ColorEscape "\v)"
200#define yellow_ColorEscape "\r*" 200#define yellow_ColorEscape "\v*"
201#define red_ColorEscape "\r+" 201#define red_ColorEscape "\v+"
202#define magenta_ColorEscape "\r," 202#define magenta_ColorEscape "\v,"
203#define blue_ColorEscape "\r-" 203#define blue_ColorEscape "\v-"
204#define green_ColorEscape "\r." 204#define green_ColorEscape "\v."
205#define uiText_ColorEscape "\r4" 205#define uiText_ColorEscape "\v4"
206#define uiTextAction_ColorEscape "\r<" 206#define uiTextAction_ColorEscape "\v<"
207#define uiTextCaution_ColorEscape "\r=" 207#define uiTextCaution_ColorEscape "\v="
208#define uiTextStrong_ColorEscape "\r:" 208#define uiTextStrong_ColorEscape "\v:"
209#define uiHeading_ColorEscape "\rR" 209#define uiHeading_ColorEscape "\vR"
210 210
211iDeclareType(Color) 211iDeclareType(Color)
212iDeclareType(HSLColor) 212iDeclareType(HSLColor)
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 0314757f..048f8ce4 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -975,13 +975,11 @@ iBool isPinned_DocumentWidget_(const iDocumentWidget *d) {
975 975
976static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) { 976static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) {
977 iWidget *w = as_Widget(d); 977 iWidget *w = as_Widget(d);
978 showCollapsed_Widget(findChild_Widget(root_Widget(as_Widget(d)), "document.pinned"), 978 showCollapsed_Widget(findChild_Widget(root_Widget(w), "document.pinned"),
979 isPinned_DocumentWidget_(d)); 979 isPinned_DocumentWidget_(d));
980} 980}
981 981
982void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { 982static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) {
983 setUrl_GmDocument(d->doc, d->mod.url);
984 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d));
985 documentRunsInvalidated_DocumentWidget_(d); 983 documentRunsInvalidated_DocumentWidget_(d);
986 updateWindowTitle_DocumentWidget_(d); 984 updateWindowTitle_DocumentWidget_(d);
987 updateVisible_DocumentWidget_(d); 985 updateVisible_DocumentWidget_(d);
@@ -997,7 +995,23 @@ void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
997 d->flags |= otherRootByDefault_DocumentWidgetFlag; 995 d->flags |= otherRootByDefault_DocumentWidgetFlag;
998 } 996 }
999 } 997 }
1000 showOrHidePinningIndicator_DocumentWidget_(d); 998 showOrHidePinningIndicator_DocumentWidget_(d);
999}
1000
1001void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
1002 setUrl_GmDocument(d->doc, d->mod.url);
1003 setSource_GmDocument(d->doc,
1004 source,
1005 documentWidth_DocumentWidget_(d),
1006 isFinished_GmRequest(d->request) ? final_GmDocumentUpdate
1007 : partial_GmDocumentUpdate);
1008 documentWasChanged_DocumentWidget_(d);
1009}
1010
1011static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) {
1012 iRelease(d->doc);
1013 d->doc = ref_Object(newDoc);
1014 documentWasChanged_DocumentWidget_(d);
1001} 1015}
1002 1016
1003static void updateTheme_DocumentWidget_(iDocumentWidget *d) { 1017static void updateTheme_DocumentWidget_(iDocumentWidget *d) {
@@ -1063,13 +1077,13 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1063 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner); 1077 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner);
1064 setFormat_GmDocument(d->doc, gemini_GmDocumentFormat); 1078 setFormat_GmDocument(d->doc, gemini_GmDocumentFormat);
1065 translate_Lang(src); 1079 translate_Lang(src);
1080 d->state = ready_RequestState;
1066 setSource_DocumentWidget(d, src); 1081 setSource_DocumentWidget(d, src);
1067 updateTheme_DocumentWidget_(d); 1082 updateTheme_DocumentWidget_(d);
1068 reset_SmoothScroll(&d->scrollY); 1083 reset_SmoothScroll(&d->scrollY);
1069 init_Anim(&d->sideOpacity, 0); 1084 init_Anim(&d->sideOpacity, 0);
1070 init_Anim(&d->altTextOpacity, 0); 1085 init_Anim(&d->altTextOpacity, 0);
1071 resetWideRuns_DocumentWidget_(d); 1086 resetWideRuns_DocumentWidget_(d);
1072 d->state = ready_RequestState;
1073} 1087}
1074 1088
1075static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) { 1089static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) {
@@ -1174,7 +1188,9 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool
1174 } 1188 }
1175} 1189}
1176 1190
1177static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response, 1191static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1192 const iGmResponse *response,
1193 iGmDocument *cachedDoc,
1178 const iBool isInitialUpdate) { 1194 const iBool isInitialUpdate) {
1179 if (d->state == ready_RequestState) { 1195 if (d->state == ready_RequestState) {
1180 return; 1196 return;
@@ -1247,6 +1263,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1247 docFormat = gemini_GmDocumentFormat; 1263 docFormat = gemini_GmDocumentFormat;
1248 setRange_String(&d->sourceMime, param); 1264 setRange_String(&d->sourceMime, param);
1249 const iGmLinkId imgLinkId = 1; /* there's only the one link */ 1265 const iGmLinkId imgLinkId = 1; /* there's only the one link */
1266 /* TODO: Do the image loading in `postProcessRequestContent_DocumentWidget_()` */
1250 if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) { 1267 if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) {
1251 const char *linkTitle = 1268 const char *linkTitle =
1252 startsWith_String(mimeStr, "image/") ? "Image" : "Audio"; 1269 startsWith_String(mimeStr, "image/") ? "Image" : "Audio";
@@ -1300,7 +1317,10 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1300 collect_String(decode_Block(&str.chars, cstr_Rangecc(charset)))); 1317 collect_String(decode_Block(&str.chars, cstr_Rangecc(charset))));
1301 } 1318 }
1302 } 1319 }
1303 if (setSource) { 1320 if (cachedDoc) {
1321 replaceDocument_DocumentWidget_(d, cachedDoc);
1322 }
1323 else if (setSource) {
1304 setSource_DocumentWidget(d, &str); 1324 setSource_DocumentWidget(d, &str);
1305 } 1325 }
1306 deinit_String(&str); 1326 deinit_String(&str);
@@ -1386,7 +1406,8 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1386 clear_ObjectList(d->media); 1406 clear_ObjectList(d->media);
1387 delete_Gempub(d->sourceGempub); 1407 delete_Gempub(d->sourceGempub);
1388 d->sourceGempub = NULL; 1408 d->sourceGempub = NULL;
1389 reset_GmDocument(d->doc); 1409 iRelease(d->doc);
1410 d->doc = new_GmDocument();
1390 resetWideRuns_DocumentWidget_(d); 1411 resetWideRuns_DocumentWidget_(d);
1391 d->state = fetching_RequestState; 1412 d->state = fetching_RequestState;
1392 /* Do the fetch. */ { 1413 /* Do the fetch. */ {
@@ -1397,10 +1418,11 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1397 d->sourceStatus = success_GmStatusCode; 1418 d->sourceStatus = success_GmStatusCode;
1398 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached")); 1419 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
1399 set_Block(&d->sourceContent, &resp->body); 1420 set_Block(&d->sourceContent, &resp->body);
1400 updateDocument_DocumentWidget_(d, resp, iTrue); 1421 updateDocument_DocumentWidget_(d, resp, recent->cachedDoc, iTrue);
1401 postProcessRequestContent_DocumentWidget_(d, iTrue); 1422 setCachedDocument_History(d->mod.history, d->doc);
1402 } 1423 }
1403 d->state = ready_RequestState; 1424 d->state = ready_RequestState;
1425 postProcessRequestContent_DocumentWidget_(d, iTrue);
1404 init_Anim(&d->altTextOpacity, 0); 1426 init_Anim(&d->altTextOpacity, 0);
1405 reset_SmoothScroll(&d->scrollY); 1427 reset_SmoothScroll(&d->scrollY);
1406 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); 1428 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y);
@@ -1587,11 +1609,12 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1587 } 1609 }
1588 case categorySuccess_GmStatusCode: 1610 case categorySuccess_GmStatusCode:
1589 reset_SmoothScroll(&d->scrollY); 1611 reset_SmoothScroll(&d->scrollY);
1590 reset_GmDocument(d->doc); /* new content incoming */ 1612 iRelease(d->doc); /* new content incoming */
1613 d->doc = new_GmDocument();
1591 delete_Gempub(d->sourceGempub); 1614 delete_Gempub(d->sourceGempub);
1592 d->sourceGempub = NULL; 1615 d->sourceGempub = NULL;
1593 resetWideRuns_DocumentWidget_(d); 1616 resetWideRuns_DocumentWidget_(d);
1594 updateDocument_DocumentWidget_(d, resp, iTrue); 1617 updateDocument_DocumentWidget_(d, resp, NULL, iTrue);
1595 break; 1618 break;
1596 case categoryRedirect_GmStatusCode: 1619 case categoryRedirect_GmStatusCode:
1597 if (isEmpty_String(&resp->meta)) { 1620 if (isEmpty_String(&resp->meta)) {
@@ -1642,7 +1665,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1642 switch (category_GmStatusCode(statusCode)) { 1665 switch (category_GmStatusCode(statusCode)) {
1643 case categorySuccess_GmStatusCode: 1666 case categorySuccess_GmStatusCode:
1644 /* More content available. */ 1667 /* More content available. */
1645 updateDocument_DocumentWidget_(d, resp, iFalse); 1668 updateDocument_DocumentWidget_(d, resp, NULL, iFalse);
1646 break; 1669 break;
1647 default: 1670 default:
1648 break; 1671 break;
@@ -2258,6 +2281,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2258 (startsWithCase_String(meta_GmRequest(d->request), "text/") || 2281 (startsWithCase_String(meta_GmRequest(d->request), "text/") ||
2259 !cmp_String(&d->sourceMime, mimeType_Gempub))) { 2282 !cmp_String(&d->sourceMime, mimeType_Gempub))) {
2260 setCachedResponse_History(d->mod.history, lockResponse_GmRequest(d->request)); 2283 setCachedResponse_History(d->mod.history, lockResponse_GmRequest(d->request));
2284 setCachedDocument_History(d->mod.history, d->doc); /* keeps a ref */
2261 unlockResponse_GmRequest(d->request); 2285 unlockResponse_GmRequest(d->request);
2262 } 2286 }
2263 } 2287 }
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index 5d0f0041..a7aa6391 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -196,7 +196,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
196 } 196 }
197 } 197 }
198 int colorEscape = none_ColorId; 198 int colorEscape = none_ColorId;
199 if (startsWith_String(&d->label, "\r")) { 199 if (startsWith_String(&d->label, "\v")) {
200 colorEscape = cstr_String(&d->label)[1] - asciiBase_ColorEscape; /* TODO: can be two bytes long */ 200 colorEscape = cstr_String(&d->label)[1] - asciiBase_ColorEscape; /* TODO: can be two bytes long */
201 } 201 }
202 if (isHover_Widget(w)) { 202 if (isHover_Widget(w)) {
diff --git a/src/ui/text.c b/src/ui/text.c
index 9532e35a..8cf4464e 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -946,10 +946,10 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
946 prevCh = 0; 946 prevCh = 0;
947 continue; 947 continue;
948 } 948 }
949 if (ch == '\r') { /* color change */ 949 if (ch == '\v') { /* color change */
950 iChar esc = nextChar_(&chPos, args->text.end); 950 iChar esc = nextChar_(&chPos, args->text.end);
951 int colorNum = args->color; 951 int colorNum = args->color;
952 if (esc == '\r') { /* Extended range. */ 952 if (esc == '\v') { /* Extended range. */
953 esc = nextChar_(&chPos, args->text.end) + asciiExtended_ColorEscape; 953 esc = nextChar_(&chPos, args->text.end) + asciiExtended_ColorEscape;
954 colorNum = esc - asciiBase_ColorEscape; 954 colorNum = esc - asciiBase_ColorEscape;
955 } 955 }