summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-03-28 22:04:24 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-03-28 22:04:24 +0300
commitde3463f5d9ac3a8b7c0f3e23ff11ade2b19fdb68 (patch)
tree195d147125afb9db3a4b9775849aea30017dd876
parent3a881ec009c9c8b6030d2f9bdf404423108e3019 (diff)
Folding preformatted blocks and showing alt text
The alt text of preformatted blocks is shown on mouse hover. The blocks can be clicked to toggle folding. IssueID #180
-rw-r--r--src/gmdocument.c137
-rw-r--r--src/gmdocument.h20
-rw-r--r--src/ui/color.h3
-rw-r--r--src/ui/documentwidget.c119
4 files changed, 215 insertions, 64 deletions
diff --git a/src/gmdocument.c b/src/gmdocument.c
index f1471f0f..874a117e 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#include "gmdocument.h" 23#include "gmdocument.h"
24#include "gmutil.h" 24#include "gmutil.h"
25#include "lang.h"
25#include "ui/color.h" 26#include "ui/color.h"
26#include "ui/text.h" 27#include "ui/text.h"
27#include "ui/metrics.h" 28#include "ui/metrics.h"
@@ -82,6 +83,7 @@ struct Impl_GmDocument {
82 iString bannerText; 83 iString bannerText;
83 iString title; /* the first top-level title */ 84 iString title; /* the first top-level title */
84 iArray headings; 85 iArray headings;
86 iArray preMeta; /* metadata about preformatted blocks */
85 uint32_t themeSeed; 87 uint32_t themeSeed;
86 iChar siteIcon; 88 iChar siteIcon;
87 iMedia * media; 89 iMedia * media;
@@ -143,19 +145,21 @@ static int lastVisibleRunBottom_GmDocument_(const iGmDocument *d) {
143 return 0; 145 return 0;
144} 146}
145 147
146static iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *start, int font) { 148static iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *start, int font,
149 iRangecc *contents, const char **endPos) {
147 const iRangecc content = { start, constEnd_String(&d->source) }; 150 const iRangecc content = { start, constEnd_String(&d->source) };
148 iRangecc line = iNullRange; 151 iRangecc line = iNullRange;
149 nextSplit_Rangecc(content, "\n", &line); 152 nextSplit_Rangecc(content, "\n", &line);
150 iAssert(startsWith_Rangecc(line, "```")); 153 iAssert(startsWith_Rangecc(line, "```"));
151 iRangecc preBlock = { line.end + 1, line.end + 1 }; 154 *contents = (iRangecc){ line.end + 1, line.end + 1 };
152 while (nextSplit_Rangecc(content, "\n", &line)) { 155 while (nextSplit_Rangecc(content, "\n", &line)) {
153 if (startsWith_Rangecc(line, "```")) { 156 if (startsWith_Rangecc(line, "```")) {
157 if (endPos) *endPos = line.end;
154 break; 158 break;
155 } 159 }
156 preBlock.end = line.end; 160 contents->end = line.end;
157 } 161 }
158 return measureRange_Text(font, preBlock); 162 return measureRange_Text(font, *contents);
159} 163}
160 164
161static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) { 165static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) {
@@ -279,8 +283,8 @@ static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {
279 return iFalse; 283 return iFalse;
280} 284}
281 285
282static void linkContentLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo, 286static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo,
283 uint16_t linkId) { 287 uint16_t linkId) {
284 iGmLink *link = at_PtrArray(&d->links, linkId - 1); 288 iGmLink *link = at_PtrArray(&d->links, linkId - 1);
285 link->flags |= content_GmLinkFlag; 289 link->flags |= content_GmLinkFlag;
286 if (mediaInfo && mediaInfo->isPermanent) { 290 if (mediaInfo && mediaInfo->isPermanent) {
@@ -370,6 +374,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
370 clear_Array(&d->layout); 374 clear_Array(&d->layout);
371 clearLinks_GmDocument_(d); 375 clearLinks_GmDocument_(d);
372 clear_Array(&d->headings); 376 clear_Array(&d->headings);
377 const iArray *oldPreMeta = collect_Array(copy_Array(&d->preMeta)); /* remember fold states */
378 clear_Array(&d->preMeta);
373 clear_String(&d->title); 379 clear_String(&d->title);
374 clear_String(&d->bannerText); 380 clear_String(&d->bannerText);
375 if (d->size.x <= 0 || isEmpty_String(&d->source)) { 381 if (d->size.x <= 0 || isEmpty_String(&d->source)) {
@@ -381,7 +387,6 @@ static void doLayout_GmDocument_(iGmDocument *d) {
381 iBool isFirstText = prefs->bigFirstParagraph; 387 iBool isFirstText = prefs->bigFirstParagraph;
382 iBool addQuoteIcon = prefs->quoteIcon; 388 iBool addQuoteIcon = prefs->quoteIcon;
383 iBool isPreformat = iFalse; 389 iBool isPreformat = iFalse;
384 iRangecc preAltText = iNullRange; /* TODO: alt text is being ignored */
385 int preFont = preformatted_FontId; 390 int preFont = preformatted_FontId;
386 uint16_t preId = 0; 391 uint16_t preId = 0;
387 iBool enableIndents = iFalse; 392 iBool enableIndents = iFalse;
@@ -408,17 +413,26 @@ static void doLayout_GmDocument_(iGmDocument *d) {
408 } 413 }
409 indent = indents[type]; 414 indent = indents[type];
410 if (type == preformatted_GmLineType) { 415 if (type == preformatted_GmLineType) {
416 /* Begin a new preformatted block. */
411 isPreformat = iTrue; 417 isPreformat = iTrue;
412 preId++; 418 const size_t preIndex = preId++;
413 preFont = preformatted_FontId; 419 preFont = preformatted_FontId;
414 /* Use a smaller font if the block contents are wide. */ 420 /* Use a smaller font if the block contents are wide. */
415 if (measurePreformattedBlock_GmDocument_(d, line.start, preFont).x > 421 iGmPreMeta meta = { .bounds = line };
422 meta.pixelRect.size = measurePreformattedBlock_GmDocument_(
423 d, line.start, preFont, &meta.contents, &meta.bounds.end);
424 if (meta.pixelRect.size.x >
416 d->size.x /*- indents[preformatted_GmLineType] * gap_Text*/) { 425 d->size.x /*- indents[preformatted_GmLineType] * gap_Text*/) {
417 preFont = preformattedSmall_FontId; 426 preFont = preformattedSmall_FontId;
418 } 427 }
419 trimLine_Rangecc(&line, type, isNormalized); 428 trimLine_Rangecc(&line, type, isNormalized);
420 preAltText = line; 429 meta.altText = line; /* without the ``` */
421 /* TODO: store and link the alt text to this run */ 430 /* Reuse previous state. */
431 if (preIndex < size_Array(oldPreMeta)) {
432 meta.flags = constValue_Array(oldPreMeta, preIndex, iGmPreMeta).flags &
433 folded_GmPreMetaFlag;
434 }
435 pushBack_Array(&d->preMeta, &meta);
422 continue; 436 continue;
423 } 437 }
424 else if (type == link_GmLineType) { 438 else if (type == link_GmLineType) {
@@ -446,7 +460,6 @@ static void doLayout_GmDocument_(iGmDocument *d) {
446 if (d->format == gemini_GmDocumentFormat && 460 if (d->format == gemini_GmDocumentFormat &&
447 startsWithSc_Rangecc(line, "```", &iCaseSensitive)) { 461 startsWithSc_Rangecc(line, "```", &iCaseSensitive)) {
448 isPreformat = iFalse; 462 isPreformat = iFalse;
449 preAltText = iNullRange;
450 addSiteBanner = iFalse; /* overrides the banner */ 463 addSiteBanner = iFalse; /* overrides the banner */
451 continue; 464 continue;
452 } 465 }
@@ -523,6 +536,30 @@ static void doLayout_GmDocument_(iGmDocument *d) {
523 pos.y += required - delta; 536 pos.y += required - delta;
524 } 537 }
525 } 538 }
539 /* Folded blocks are represented by a single run with the alt text. */
540 if (isPreformat) {
541 const iGmPreMeta *meta = constAt_Array(&d->preMeta, preId - 1);
542 if (meta->flags & folded_GmPreMetaFlag) {
543 const iBool isBlank = isEmpty_Range(&meta->altText);
544 iGmRun altText = { .font = paragraph_FontId,
545 .flags = (isBlank ? decoration_GmRunFlag : 0) | altText_GmRunFlag };
546 const iInt2 margin = preRunMargin_GmDocument(d, 0);
547 altText.color = tmQuote_ColorId;
548 altText.text = isBlank ? range_Lang(range_CStr("doc.pre.nocaption"))
549 : meta->altText;
550 iInt2 size = advanceWrapRange_Text(altText.font, d->size.x - 2 * margin.x,
551 altText.text);
552 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x,
553 size.y + 2 * margin.y);
554 altText.preId = preId;
555 pushBack_Array(&d->layout, &altText);
556 pos.y += height_Rect(altText.bounds);
557 contentLine = meta->bounds; /* Skip the whole thing. */
558 isPreformat = iFalse;
559 prevType = preformatted_GmLineType;
560 continue;
561 }
562 }
526 /* Save the document title (first high-level heading). */ 563 /* Save the document title (first high-level heading). */
527 if ((type == heading1_GmLineType || type == heading2_GmLineType) && 564 if ((type == heading1_GmLineType || type == heading2_GmLineType) &&
528 isEmpty_String(&d->title)) { 565 isEmpty_String(&d->title)) {
@@ -632,14 +669,27 @@ static void doLayout_GmDocument_(iGmDocument *d) {
632 type == quote_GmLineType ? 4 : 0); 669 type == quote_GmLineType ? 4 : 0);
633 const iBool isWordWrapped = 670 const iBool isWordWrapped =
634 (d->format == plainText_GmDocumentFormat ? prefs->plainTextWrap : !isPreformat); 671 (d->format == plainText_GmDocumentFormat ? prefs->plainTextWrap : !isPreformat);
672 if (isPreformat) {
673 /* Remember the top left coordinates of the block (first line of block). */
674 iGmPreMeta *meta = at_Array(&d->preMeta, preId - 1);
675 if (~meta->flags & topLeft_GmPreMetaFlag) {
676 meta->pixelRect.pos = pos; //, indent * gap_Text);
677 meta->flags |= topLeft_GmPreMetaFlag;
678 }
679 /* Collapse indentation if too wide. */
680 if (width_Rect(meta->pixelRect) > d->size.x - (indent + rightMargin) * gap_Text) {
681 indent = 0;
682 }
683 }
635 iAssert(!isEmpty_Range(&runLine)); /* must have something at this point */ 684 iAssert(!isEmpty_Range(&runLine)); /* must have something at this point */
636 while (!isEmpty_Range(&runLine)) { 685 while (!isEmpty_Range(&runLine)) {
637 run.bounds.pos = addX_I2(pos, indent * gap_Text); 686 run.bounds.pos = addX_I2(pos, indent * gap_Text);
638 const int avail = isWordWrapped ? d->size.x - run.bounds.pos.x - rightMargin * gap_Text : 0; 687 const int wrapAvail = d->size.x - run.bounds.pos.x - rightMargin * gap_Text;
688 const int avail = isWordWrapped ? wrapAvail : 0;
639 const char *contPos; 689 const char *contPos;
640 const iInt2 dims = tryAdvance_Text(run.font, runLine, avail, &contPos); 690 const iInt2 dims = tryAdvance_Text(run.font, runLine, avail, &contPos);
641 iChangeFlags(run.flags, wide_GmRunFlag, (isPreformat && dims.x > d->size.x)); 691 iChangeFlags(run.flags, wide_GmRunFlag, (isPreformat && dims.x > d->size.x));
642 run.bounds.size.x = iMax(avail, dims.x); /* Extends to the right edge for selection. */ 692 run.bounds.size.x = iMax(wrapAvail, dims.x); /* Extends to the right edge for selection. */
643 run.bounds.size.y = dims.y; 693 run.bounds.size.y = dims.y;
644 run.visBounds = run.bounds; 694 run.visBounds = run.bounds;
645 run.visBounds.size.x = dims.x; 695 run.visBounds.size.x = dims.x;
@@ -671,7 +721,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
671 iGmMediaInfo img; 721 iGmMediaInfo img;
672 imageInfo_Media(d->media, imageId, &img); 722 imageInfo_Media(d->media, imageId, &img);
673 const iInt2 imgSize = imageSize_Media(d->media, imageId); 723 const iInt2 imgSize = imageSize_Media(d->media, imageId);
674 linkContentLaidOut_GmDocument_(d, &img, run.linkId); 724 linkContentWasLaidOut_GmDocument_(d, &img, run.linkId);
675 const int margin = lineHeight_Text(paragraph_FontId) / 2; 725 const int margin = lineHeight_Text(paragraph_FontId) / 2;
676 pos.y += margin; 726 pos.y += margin;
677 run.bounds.pos = pos; 727 run.bounds.pos = pos;
@@ -699,7 +749,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
699 else if (audioId) { 749 else if (audioId) {
700 iGmMediaInfo info; 750 iGmMediaInfo info;
701 audioInfo_Media(d->media, audioId, &info); 751 audioInfo_Media(d->media, audioId, &info);
702 linkContentLaidOut_GmDocument_(d, &info, run.linkId); 752 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);
703 const int margin = lineHeight_Text(paragraph_FontId) / 2; 753 const int margin = lineHeight_Text(paragraph_FontId) / 2;
704 pos.y += margin; 754 pos.y += margin;
705 run.bounds.pos = pos; 755 run.bounds.pos = pos;
@@ -716,7 +766,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
716 else if (downloadId) { 766 else if (downloadId) {
717 iGmMediaInfo info; 767 iGmMediaInfo info;
718 downloadInfo_Media(d->media, downloadId, &info); 768 downloadInfo_Media(d->media, downloadId, &info);
719 linkContentLaidOut_GmDocument_(d, &info, run.linkId); 769 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);
720 const int margin = lineHeight_Text(paragraph_FontId) / 2; 770 const int margin = lineHeight_Text(paragraph_FontId) / 2;
721 pos.y += margin; 771 pos.y += margin;
722 run.bounds.pos = pos; 772 run.bounds.pos = pos;
@@ -773,6 +823,7 @@ void init_GmDocument(iGmDocument *d) {
773 init_String(&d->bannerText); 823 init_String(&d->bannerText);
774 init_String(&d->title); 824 init_String(&d->title);
775 init_Array(&d->headings, sizeof(iGmHeading)); 825 init_Array(&d->headings, sizeof(iGmHeading));
826 init_Array(&d->preMeta, sizeof(iGmPreMeta));
776 d->themeSeed = 0; 827 d->themeSeed = 0;
777 d->siteIcon = 0; 828 d->siteIcon = 0;
778 d->media = new_Media(); 829 d->media = new_Media();
@@ -784,6 +835,7 @@ void deinit_GmDocument(iGmDocument *d) {
784 deinit_String(&d->title); 835 deinit_String(&d->title);
785 clearLinks_GmDocument_(d); 836 clearLinks_GmDocument_(d);
786 deinit_PtrArray(&d->links); 837 deinit_PtrArray(&d->links);
838 deinit_Array(&d->preMeta);
787 deinit_Array(&d->headings); 839 deinit_Array(&d->headings);
788 deinit_Array(&d->layout); 840 deinit_Array(&d->layout);
789 deinit_String(&d->localHost); 841 deinit_String(&d->localHost);
@@ -804,6 +856,7 @@ void reset_GmDocument(iGmDocument *d) {
804 clearLinks_GmDocument_(d); 856 clearLinks_GmDocument_(d);
805 clear_Array(&d->layout); 857 clear_Array(&d->layout);
806 clear_Array(&d->headings); 858 clear_Array(&d->headings);
859 clear_Array(&d->preMeta);
807 clear_String(&d->url); 860 clear_String(&d->url);
808 clear_String(&d->localHost); 861 clear_String(&d->localHost);
809 d->themeSeed = 0; 862 d->themeSeed = 0;
@@ -815,6 +868,8 @@ static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {
815 set_Color(tmBannerSideTitle_ColorId, 868 set_Color(tmBannerSideTitle_ColorId,
816 mix_Color(get_Color(tmBannerTitle_ColorId), get_Color(tmBackground_ColorId), 869 mix_Color(get_Color(tmBannerTitle_ColorId), get_Color(tmBackground_ColorId),
817 theme == colorfulDark_GmDocumentTheme ? 0.55f : 0)); 870 theme == colorfulDark_GmDocumentTheme ? 0.55f : 0));
871 set_Color(tmAltTextBackground_ColorId, mix_Color(get_Color(tmQuoteIcon_ColorId),
872 get_Color(tmBackground_ColorId), 0.85f));
818 if (theme == colorfulDark_GmDocumentTheme) { 873 if (theme == colorfulDark_GmDocumentTheme) {
819 /* Ensure paragraph text and link text aren't too similarly colored. */ 874 /* Ensure paragraph text and link text aren't too similarly colored. */
820 if (delta_Color(get_Color(tmLinkText_ColorId), get_Color(tmParagraph_ColorId)) < 100) { 875 if (delta_Color(get_Color(tmLinkText_ColorId), get_Color(tmParagraph_ColorId)) < 100) {
@@ -824,35 +879,6 @@ static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {
824 } 879 }
825 set_Color(tmLinkCustomIconVisited_ColorId, 880 set_Color(tmLinkCustomIconVisited_ColorId,
826 mix_Color(get_Color(tmLinkIconVisited_ColorId), get_Color(tmLinkIcon_ColorId), 0.5f)); 881 mix_Color(get_Color(tmLinkIconVisited_ColorId), get_Color(tmLinkIcon_ColorId), 0.5f));
827#if 0
828 set_Color(tmOutlineHeadingAbove_ColorId, get_Color(white_ColorId));
829 set_Color(tmOutlineHeadingBelow_ColorId, get_Color(black_ColorId));
830 switch (theme) {
831 case colorfulDark_GmDocumentTheme:
832 set_Color(tmOutlineHeadingBelow_ColorId, get_Color(tmBannerTitle_ColorId));
833 if (equal_Color(get_Color(tmOutlineHeadingAbove_ColorId),
834 get_Color(tmOutlineHeadingBelow_ColorId))) {
835 set_Color(tmOutlineHeadingBelow_ColorId, get_Color(tmHeading3_ColorId));
836 }
837 break;
838 case colorfulLight_GmDocumentTheme:
839 case sepia_GmDocumentTheme:
840 set_Color(tmOutlineHeadingAbove_ColorId, get_Color(black_ColorId));
841 set_Color(tmOutlineHeadingBelow_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(black_ColorId), 0.6f));
842 break;
843 case gray_GmDocumentTheme:
844 set_Color(tmOutlineHeadingBelow_ColorId, get_Color(gray75_ColorId));
845 break;
846 case white_GmDocumentTheme:
847 set_Color(tmOutlineHeadingBelow_ColorId, mix_Color(get_Color(tmBannerIcon_ColorId), get_Color(white_ColorId), 0.6f));
848 break;
849 case highContrast_GmDocumentTheme:
850 set_Color(tmOutlineHeadingAbove_ColorId, get_Color(black_ColorId));
851 break;
852 default:
853 break;
854 }
855#endif
856} 882}
857 883
858static void updateIconBasedOnUrl_GmDocument_(iGmDocument *d) { 884static void updateIconBasedOnUrl_GmDocument_(iGmDocument *d) {
@@ -1406,6 +1432,20 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width) {
1406 setWidth_GmDocument(d, width); /* re-do layout */ 1432 setWidth_GmDocument(d, width); /* re-do layout */
1407} 1433}
1408 1434
1435void foldPre_GmDocument(iGmDocument *d, uint16_t preId) {
1436 if (preId > 0 && preId <= size_Array(&d->preMeta)) {
1437 iGmPreMeta *meta = at_Array(&d->preMeta, preId - 1);
1438 meta->flags ^= folded_GmPreMetaFlag;
1439 }
1440}
1441
1442const iGmPreMeta *preMeta_GmDocument(const iGmDocument *d, uint16_t preId) {
1443 if (preId > 0 && preId <= size_Array(&d->preMeta)) {
1444 return constAt_Array(&d->preMeta, preId - 1);
1445 }
1446 return NULL;
1447}
1448
1409void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRenderFunc render, 1449void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRenderFunc render,
1410 void *context) { 1450 void *context) {
1411 iBool isInside = iFalse; 1451 iBool isInside = iFalse;
@@ -1684,4 +1724,9 @@ iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) {
1684 return loc; 1724 return loc;
1685} 1725}
1686 1726
1727iInt2 preRunMargin_GmDocument(const iGmDocument *d, uint16_t preId) {
1728 iUnused(d, preId);
1729 return init_I2(3 * gap_Text, 2 * gap_Text);
1730}
1731
1687iDefineClass(GmDocument) 1732iDefineClass(GmDocument)
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 57c19e9a..14c5c85d 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
32#include <the_Foundation/time.h> 32#include <the_Foundation/time.h>
33 33
34iDeclareType(GmHeading) 34iDeclareType(GmHeading)
35iDeclareType(GmPreMeta)
35iDeclareType(GmRun) 36iDeclareType(GmRun)
36 37
37enum iGmLineType { 38enum iGmLineType {
@@ -89,6 +90,20 @@ struct Impl_GmHeading {
89 int level; /* 0, 1, 2 */ 90 int level; /* 0, 1, 2 */
90}; 91};
91 92
93enum iGmPreMetaFlag {
94 folded_GmPreMetaFlag = 0x1,
95 topLeft_GmPreMetaFlag = 0x2,
96};
97
98struct Impl_GmPreMeta {
99 iRangecc bounds; /* including ``` markers */
100 iRangecc altText; /* range in source */
101 iRangecc contents; /* just the content lines */
102 int flags;
103 /* TODO: refactor old code to incorporate wide scroll handling here */
104 iRect pixelRect;
105};
106
92enum iGmRunFlags { 107enum iGmRunFlags {
93 decoration_GmRunFlag = iBit(1), /* not part of the source */ 108 decoration_GmRunFlag = iBit(1), /* not part of the source */
94 startOfLine_GmRunFlag = iBit(2), 109 startOfLine_GmRunFlag = iBit(2),
@@ -97,6 +112,7 @@ enum iGmRunFlags {
97 quoteBorder_GmRunFlag = iBit(5), 112 quoteBorder_GmRunFlag = iBit(5),
98 wide_GmRunFlag = iBit(6), /* horizontally scrollable */ 113 wide_GmRunFlag = iBit(6), /* horizontally scrollable */
99 footer_GmRunFlag = iBit(7), 114 footer_GmRunFlag = iBit(7),
115 altText_GmRunFlag = iBit(8),
100}; 116};
101 117
102enum iGmRunMediaType { 118enum iGmRunMediaType {
@@ -150,6 +166,7 @@ void setWidth_GmDocument (iGmDocument *, int width);
150void redoLayout_GmDocument (iGmDocument *); 166void redoLayout_GmDocument (iGmDocument *);
151void setUrl_GmDocument (iGmDocument *, const iString *url); 167void setUrl_GmDocument (iGmDocument *, const iString *url);
152void setSource_GmDocument (iGmDocument *, const iString *source, int width); 168void setSource_GmDocument (iGmDocument *, const iString *source, int width);
169void foldPre_GmDocument (iGmDocument *, uint16_t preId);
153 170
154void reset_GmDocument (iGmDocument *); /* free images */ 171void reset_GmDocument (iGmDocument *); /* free images */
155 172
@@ -194,4 +211,5 @@ const iTime * linkTime_GmDocument (const iGmDocument *, iGmLinkId linkId);
194iBool isMediaLink_GmDocument (const iGmDocument *, iGmLinkId linkId); 211iBool isMediaLink_GmDocument (const iGmDocument *, iGmLinkId linkId);
195const iString * title_GmDocument (const iGmDocument *); 212const iString * title_GmDocument (const iGmDocument *);
196iChar siteIcon_GmDocument (const iGmDocument *); 213iChar siteIcon_GmDocument (const iGmDocument *);
197 214const iGmPreMeta *preMeta_GmDocument (const iGmDocument *, uint16_t preId);
215iInt2 preRunMargin_GmDocument (const iGmDocument *, uint16_t preId);
diff --git a/src/ui/color.h b/src/ui/color.h
index 057ca42d..cd2f95d3 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -115,7 +115,8 @@ enum iColorId {
115 uiTextAppTitle_ColorId, 115 uiTextAppTitle_ColorId,
116 uiBackgroundSidebar_ColorId, 116 uiBackgroundSidebar_ColorId,
117 uiBackgroundMenu_ColorId, 117 uiBackgroundMenu_ColorId,
118 tmLinkCustomIconVisited_ColorId, 118 tmLinkCustomIconVisited_ColorId, /* derived from other theme colors */
119 tmAltTextBackground_ColorId, /* derived from other theme colors */
119 120
120 /* content theme colors */ 121 /* content theme colors */
121 tmFirst_ColorId, 122 tmFirst_ColorId,
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 6bb16a93..93297c70 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -202,7 +202,8 @@ struct Impl_DocumentWidget {
202 iRangecc foundMark; 202 iRangecc foundMark;
203 int pageMargin; 203 int pageMargin;
204 iPtrArray visibleLinks; 204 iPtrArray visibleLinks;
205 iPtrArray visibleWideRuns; /* scrollable blocks */ 205 iPtrArray visiblePre;
206 iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */
206 iArray wideRunOffsets; 207 iArray wideRunOffsets;
207 iAnim animWideRunOffset; 208 iAnim animWideRunOffset;
208 uint16_t animWideRunId; 209 uint16_t animWideRunId;
@@ -211,6 +212,8 @@ struct Impl_DocumentWidget {
211 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ 212 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */
212 float grabbedStartVolume; 213 float grabbedStartVolume;
213 int mediaTimer; 214 int mediaTimer;
215 const iGmRun * hoverPre; /* for clicking */
216 const iGmRun * hoverAltPre; /* for drawing alt text */
214 const iGmRun * hoverLink; 217 const iGmRun * hoverLink;
215 const iGmRun * contextLink; 218 const iGmRun * contextLink;
216 const iGmRun * firstVisibleRun; 219 const iGmRun * firstVisibleRun;
@@ -220,6 +223,7 @@ struct Impl_DocumentWidget {
220 float initNormScrollY; 223 float initNormScrollY;
221 iAnim scrollY; 224 iAnim scrollY;
222 iAnim sideOpacity; 225 iAnim sideOpacity;
226 iAnim altTextOpacity;
223 iScrollWidget *scroll; 227 iScrollWidget *scroll;
224 iWidget * menu; 228 iWidget * menu;
225 iWidget * playerMenu; 229 iWidget * playerMenu;
@@ -260,6 +264,8 @@ void init_DocumentWidget(iDocumentWidget *d) {
260 d->selectMark = iNullRange; 264 d->selectMark = iNullRange;
261 d->foundMark = iNullRange; 265 d->foundMark = iNullRange;
262 d->pageMargin = 5; 266 d->pageMargin = 5;
267 d->hoverPre = NULL;
268 d->hoverAltPre = NULL;
263 d->hoverLink = NULL; 269 d->hoverLink = NULL;
264 d->contextLink = NULL; 270 d->contextLink = NULL;
265 d->firstVisibleRun = NULL; 271 d->firstVisibleRun = NULL;
@@ -267,12 +273,14 @@ void init_DocumentWidget(iDocumentWidget *d) {
267 d->visBuf = new_VisBuf(); 273 d->visBuf = new_VisBuf();
268 d->invalidRuns = new_PtrSet(); 274 d->invalidRuns = new_PtrSet();
269 init_Anim(&d->sideOpacity, 0); 275 init_Anim(&d->sideOpacity, 0);
276 init_Anim(&d->altTextOpacity, 0);
270 d->sourceStatus = none_GmStatusCode; 277 d->sourceStatus = none_GmStatusCode;
271 init_String(&d->sourceHeader); 278 init_String(&d->sourceHeader);
272 init_String(&d->sourceMime); 279 init_String(&d->sourceMime);
273 init_Block(&d->sourceContent, 0); 280 init_Block(&d->sourceContent, 0);
274 iZap(d->sourceTime); 281 iZap(d->sourceTime);
275 init_PtrArray(&d->visibleLinks); 282 init_PtrArray(&d->visibleLinks);
283 init_PtrArray(&d->visiblePre);
276 init_PtrArray(&d->visibleWideRuns); 284 init_PtrArray(&d->visibleWideRuns);
277 init_Array(&d->wideRunOffsets, sizeof(int)); 285 init_Array(&d->wideRunOffsets, sizeof(int));
278 init_PtrArray(&d->visibleMedia); 286 init_PtrArray(&d->visibleMedia);
@@ -322,6 +330,7 @@ void deinit_DocumentWidget(iDocumentWidget *d) {
322 deinit_Array(&d->wideRunOffsets); 330 deinit_Array(&d->wideRunOffsets);
323 deinit_PtrArray(&d->visibleMedia); 331 deinit_PtrArray(&d->visibleMedia);
324 deinit_PtrArray(&d->visibleWideRuns); 332 deinit_PtrArray(&d->visibleWideRuns);
333 deinit_PtrArray(&d->visiblePre);
325 deinit_PtrArray(&d->visibleLinks); 334 deinit_PtrArray(&d->visibleLinks);
326 delete_Block(d->certFingerprint); 335 delete_Block(d->certFingerprint);
327 delete_String(d->certSubject); 336 delete_String(d->certSubject);
@@ -419,8 +428,11 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
419 } 428 }
420 d->lastVisibleRun = run; 429 d->lastVisibleRun = run;
421 } 430 }
422 if (run->preId && run->flags & wide_GmRunFlag) { 431 if (run->preId) {
423 pushBack_PtrArray(&d->visibleWideRuns, run); 432 pushBack_PtrArray(&d->visiblePre, run);
433 if (run->flags & wide_GmRunFlag) {
434 pushBack_PtrArray(&d->visibleWideRuns, run);
435 }
424 } 436 }
425 if (run->mediaType == audio_GmRunMediaType || run->mediaType == download_GmRunMediaType) { 437 if (run->mediaType == audio_GmRunMediaType || run->mediaType == download_GmRunMediaType) {
426 iAssert(run->mediaId); 438 iAssert(run->mediaId);
@@ -501,10 +513,18 @@ static void invalidateWideRunsWithNonzeroOffset_DocumentWidget_(iDocumentWidget
501 } 513 }
502} 514}
503 515
516static void animate_DocumentWidget_(void *ticker) {
517 iDocumentWidget *d = ticker;
518 if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->altTextOpacity)) {
519 addTicker_App(animate_DocumentWidget_, d);
520 }
521}
522
504static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { 523static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
505 const iWidget *w = constAs_Widget(d); 524 const iWidget *w = constAs_Widget(d);
506 const iRect docBounds = documentBounds_DocumentWidget_(d); 525 const iRect docBounds = documentBounds_DocumentWidget_(d);
507 const iGmRun * oldHoverLink = d->hoverLink; 526 const iGmRun * oldHoverLink = d->hoverLink;
527 d->hoverPre = NULL;
508 d->hoverLink = NULL; 528 d->hoverLink = NULL;
509 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), value_Anim(&d->scrollY)); 529 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), value_Anim(&d->scrollY));
510 if (isHover_Widget(w) && (~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) && 530 if (isHover_Widget(w) && (~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) &&
@@ -525,11 +545,31 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
525 if (d->hoverLink) { 545 if (d->hoverLink) {
526 invalidateLink_DocumentWidget_(d, d->hoverLink->linkId); 546 invalidateLink_DocumentWidget_(d, d->hoverLink->linkId);
527 } 547 }
528 refresh_Widget(as_Widget(d)); 548 refresh_Widget(w);
549 }
550 if (isHover_Widget(w)) {
551 iConstForEach(PtrArray, j, &d->visiblePre) {
552 const iGmRun *run = j.ptr;
553 if (contains_Rect(run->bounds, hoverPos)) {
554 d->hoverPre = run;
555 d->hoverAltPre = run;
556 break;
557 }
558 }
559 }
560 if (!d->hoverPre && targetValue_Anim(&d->altTextOpacity) > 0.5f) {
561 setValue_Anim(&d->altTextOpacity, 0.0f, 300);
562 animate_DocumentWidget_(d);
563 refresh_Widget(w);
564 }
565 else if (d->hoverPre && targetValue_Anim(&d->altTextOpacity) < 0.5f) {
566 setValue_Anim(&d->altTextOpacity, 1.0f, 0);
567 refresh_Widget(w);
529 } 568 }
530 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) { 569 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) {
531 setCursor_Window(get_Window(), 570 setCursor_Window(get_Window(),
532 d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM); 571 d->hoverLink || d->hoverPre ? SDL_SYSTEM_CURSOR_HAND
572 : SDL_SYSTEM_CURSOR_IBEAM);
533 if (d->hoverLink && 573 if (d->hoverLink &&
534 linkFlags_GmDocument(d->doc, d->hoverLink->linkId) & permanent_GmLinkFlag) { 574 linkFlags_GmDocument(d->doc, d->hoverLink->linkId) & permanent_GmLinkFlag) {
535 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); /* not dismissable */ 575 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); /* not dismissable */
@@ -537,13 +577,6 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
537 } 577 }
538} 578}
539 579
540static void animate_DocumentWidget_(void *ticker) {
541 iDocumentWidget *d = ticker;
542 if (!isFinished_Anim(&d->sideOpacity)) {
543 addTicker_App(animate_DocumentWidget_, d);
544 }
545}
546
547static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimated) { 580static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimated) {
548 float opacity = 0.0f; 581 float opacity = 0.0f;
549 const iGmRun *banner = siteBanner_GmDocument(d->doc); 582 const iGmRun *banner = siteBanner_GmDocument(d->doc);
@@ -649,6 +682,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
649 docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); 682 docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0);
650 clear_PtrArray(&d->visibleLinks); 683 clear_PtrArray(&d->visibleLinks);
651 clear_PtrArray(&d->visibleWideRuns); 684 clear_PtrArray(&d->visibleWideRuns);
685 clear_PtrArray(&d->visiblePre);
652 clear_PtrArray(&d->visibleMedia); 686 clear_PtrArray(&d->visibleMedia);
653 const iRangecc oldHeading = currentHeading_DocumentWidget_(d); 687 const iRangecc oldHeading = currentHeading_DocumentWidget_(d);
654 /* Scan for visible runs. */ { 688 /* Scan for visible runs. */ {
@@ -765,6 +799,8 @@ static iRangecc bannerText_DocumentWidget_(const iDocumentWidget *d) {
765static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { 799static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {
766 d->foundMark = iNullRange; 800 d->foundMark = iNullRange;
767 d->selectMark = iNullRange; 801 d->selectMark = iNullRange;
802 d->hoverPre = NULL;
803 d->hoverAltPre = NULL;
768 d->hoverLink = NULL; 804 d->hoverLink = NULL;
769 d->contextLink = NULL; 805 d->contextLink = NULL;
770 d->firstVisibleRun = NULL; 806 d->firstVisibleRun = NULL;
@@ -1199,6 +1235,18 @@ static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos,
1199 } 1235 }
1200} 1236}
1201 1237
1238static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) {
1239 d->hoverPre = NULL;
1240 d->hoverAltPre = NULL;
1241 d->selectMark = iNullRange;
1242 foldPre_GmDocument(d->doc, preId);
1243 redoLayout_GmDocument(d->doc);
1244 scroll_DocumentWidget_(d, 0);
1245 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window()));
1246 invalidate_DocumentWidget_(d);
1247 refresh_Widget(as_Widget(d));
1248}
1249
1202static void checkResponse_DocumentWidget_(iDocumentWidget *d) { 1250static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1203 if (!d->request) { 1251 if (!d->request) {
1204 return; 1252 return;
@@ -2681,6 +2729,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2681 } 2729 }
2682 if (!isMoved_Click(&d->click)) { 2730 if (!isMoved_Click(&d->click)) {
2683 setFocus_Widget(NULL); 2731 setFocus_Widget(NULL);
2732 if (d->hoverPre) {
2733 togglePreFold_DocumentWidget_(d, d->hoverPre->preId);
2734 return iTrue;
2735 }
2684 if (d->hoverLink) { 2736 if (d->hoverLink) {
2685 const iGmLinkId linkId = d->hoverLink->linkId; 2737 const iGmLinkId linkId = d->hoverLink->linkId;
2686 const int linkFlags = linkFlags_GmDocument(d->doc, linkId); 2738 const int linkFlags = linkFlags_GmDocument(d->doc, linkId);
@@ -2994,7 +3046,14 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
2994 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */ 3046 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */
2995 } 3047 }
2996 } 3048 }
2997 if (run->flags & siteBanner_GmRunFlag) { 3049 if (run->flags & altText_GmRunFlag) {
3050 const iInt2 margin = preRunMargin_GmDocument(doc, run->preId);
3051 fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmAltTextBackground_ColorId);
3052 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmQuoteIcon_ColorId);
3053 drawWrapRange_Text(run->font, add_I2(visPos, margin),
3054 run->visBounds.size.x - 2 * margin.x, run->color, run->text);
3055 }
3056 else if (run->flags & siteBanner_GmRunFlag) {
2998 /* Banner background. */ 3057 /* Banner background. */
2999 iRect bannerBack = initCorners_Rect(topLeft_Rect(d->widgetBounds), 3058 iRect bannerBack = initCorners_Rect(topLeft_Rect(d->widgetBounds),
3000 init_I2(right_Rect(bounds_Widget(constAs_Widget(d->widget))), 3059 init_I2(right_Rect(bounds_Widget(constAs_Widget(d->widget))),
@@ -3157,8 +3216,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3157 } 3216 }
3158 } 3217 }
3159 } 3218 }
3160// drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId); 3219 drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId);
3161// drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId); 3220 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId);
3162} 3221}
3163 3222
3164static int drawSideRect_(iPaint *p, iRect rect) { 3223static int drawSideRect_(iPaint *p, iRect rect) {
@@ -3392,7 +3451,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
3392 hasSiteBanner_GmDocument(d->doc) ? tmBannerBackground_ColorId 3451 hasSiteBanner_GmDocument(d->doc) ? tmBannerBackground_ColorId
3393 : tmBackground_ColorId); 3452 : tmBackground_ColorId);
3394 } 3453 }
3395 const int yBottom = yTop + size_GmDocument(d->doc).y; 3454 const int yBottom = yTop + size_GmDocument(d->doc).y + 1;
3396 if (yBottom < bottom_Rect(bounds)) { 3455 if (yBottom < bottom_Rect(bounds)) {
3397 fillRect_Paint(&ctx.paint, 3456 fillRect_Paint(&ctx.paint,
3398 init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom), 3457 init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom),
@@ -3412,6 +3471,34 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
3412 drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId); 3471 drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId);
3413 } 3472 }
3414 draw_Widget(w); 3473 draw_Widget(w);
3474 /* Alt text. */
3475 const float altTextOpacity = value_Anim(&d->altTextOpacity);
3476 if (d->hoverAltPre && altTextOpacity > 0) {
3477 const iGmPreMeta *meta = preMeta_GmDocument(d->doc, d->hoverAltPre->preId);
3478 if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag &&
3479 !isEmpty_Range(&meta->altText)) {
3480 const int margin = 2 * gap_UI;
3481 const int altFont = uiLabel_FontId;
3482 const int wrap = docBounds.size.x - 2 * margin;
3483 iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos),
3484 -value_Anim(&d->scrollY));
3485 const iInt2 textSize = advanceWrapRange_Text(altFont, wrap, meta->altText);
3486 pos.y -= textSize.y + gap_UI;
3487 pos.y = iMax(pos.y, top_Rect(bounds));
3488 const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) };
3489 ctx.paint.alpha = altTextOpacity * 255;
3490 if (altTextOpacity < 1) {
3491 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
3492 }
3493 fillRect_Paint(&ctx.paint, altRect, tmAltTextBackground_ColorId);
3494 drawRect_Paint(&ctx.paint, altRect, tmQuoteIcon_ColorId);
3495 setOpacity_Text(altTextOpacity);
3496 drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap,
3497 tmQuote_ColorId, meta->altText);
3498 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
3499 setOpacity_Text(1.0f);
3500 }
3501 }
3415} 3502}
3416 3503
3417/*----------------------------------------------------------------------------------------------*/ 3504/*----------------------------------------------------------------------------------------------*/