summaryrefslogtreecommitdiff
path: root/src
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 /src
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
Diffstat (limited to 'src')
-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/*----------------------------------------------------------------------------------------------*/