summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/fontpack.c2
-rw-r--r--src/gmdocument.c200
-rw-r--r--src/gmdocument.h30
-rw-r--r--src/ui/documentwidget.c40
-rw-r--r--src/ui/inputwidget.c18
-rw-r--r--src/ui/text.c196
-rw-r--r--src/ui/text.h26
7 files changed, 331 insertions, 181 deletions
diff --git a/src/fontpack.c b/src/fontpack.c
index fb1c98ee..9baedc0e 100644
--- a/src/fontpack.c
+++ b/src/fontpack.c
@@ -48,7 +48,7 @@ float scale_FontSize(enum iFontSize size) {
48 1.333, 48 1.333,
49 1.666, 49 1.666,
50 2.000, 50 2.000,
51 0.568, 51 0.650, //0.568,
52 0.710, /* calibration: fits the Lagrange title screen with Normal line width */ 52 0.710, /* calibration: fits the Lagrange title screen with Normal line width */
53 }; 53 };
54 if (size < 0 || size >= max_FontSize) { 54 if (size < 0 || size >= max_FontSize) {
diff --git a/src/gmdocument.c b/src/gmdocument.c
index f0d9bf08..5c2a849e 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -75,6 +75,15 @@ iDefineTypeConstruction(GmLink)
75 75
76/*----------------------------------------------------------------------------------------------*/ 76/*----------------------------------------------------------------------------------------------*/
77 77
78iDeclareType(GmTheme)
79
80struct Impl_GmTheme {
81 int colors[max_GmLineType];
82 int fonts[max_GmLineType];
83};
84
85/*----------------------------------------------------------------------------------------------*/
86
78struct Impl_GmDocument { 87struct Impl_GmDocument {
79 iObject object; 88 iObject object;
80 enum iSourceFormat format; 89 enum iSourceFormat format;
@@ -91,6 +100,7 @@ struct Impl_GmDocument {
91 iString title; /* the first top-level title */ 100 iString title; /* the first top-level title */
92 iArray headings; 101 iArray headings;
93 iArray preMeta; /* metadata about preformatted blocks */ 102 iArray preMeta; /* metadata about preformatted blocks */
103 iGmTheme theme;
94 uint32_t themeSeed; 104 uint32_t themeSeed;
95 iChar siteIcon; 105 iChar siteIcon;
96 iMedia * media; 106 iMedia * media;
@@ -101,6 +111,51 @@ struct Impl_GmDocument {
101 111
102iDefineObjectConstruction(GmDocument) 112iDefineObjectConstruction(GmDocument)
103 113
114static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {
115 const iRangecc scheme = urlScheme_String(&d->url);
116 if (equalCase_Rangecc(scheme, "gemini")) {
117 return prefs_App()->monospaceGemini;
118 }
119 if (equalCase_Rangecc(scheme, "gopher") ||
120 equalCase_Rangecc(scheme, "finger")) {
121 return prefs_App()->monospaceGopher;
122 }
123 return iFalse;
124}
125
126static void initTheme_GmDocument_(iGmDocument *d) {
127 static const int defaultColors[max_GmLineType] = {
128 tmParagraph_ColorId,
129 tmParagraph_ColorId, /* bullet */
130 tmPreformatted_ColorId,
131 tmQuote_ColorId,
132 tmHeading1_ColorId,
133 tmHeading2_ColorId,
134 tmHeading3_ColorId,
135 tmLinkText_ColorId,
136 };
137 iGmTheme *theme = &d->theme;
138 memcpy(theme->colors, defaultColors, sizeof(theme->colors));
139 const iPrefs *prefs = prefs_App();
140 const iBool isMono = isForcedMonospace_GmDocument_(d);
141 const iBool isDarkBg = isDark_GmDocumentTheme(
142 isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
143 const enum iFontId headingFont = isMono ? documentMonospace_FontId : documentHeading_FontId;
144 const enum iFontId bodyFont = isMono ? documentMonospace_FontId : documentBody_FontId;
145 theme->fonts[text_GmLineType] = FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize);
146 theme->fonts[bullet_GmLineType] = FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize);
147 theme->fonts[preformatted_GmLineType] = preformatted_FontId;
148 theme->fonts[quote_GmLineType] = isMono ? monospaceParagraph_FontId : quote_FontId;
149 theme->fonts[heading1_GmLineType] = FONT_ID(headingFont, bold_FontStyle, contentHuge_FontSize);
150 theme->fonts[heading2_GmLineType] = FONT_ID(headingFont, bold_FontStyle, contentLarge_FontSize);
151 theme->fonts[heading3_GmLineType] = FONT_ID(headingFont, regular_FontStyle, contentBig_FontSize);
152 theme->fonts[link_GmLineType] = FONT_ID(
153 bodyFont,
154 ((isDarkBg && prefs->boldLinkDark) || (!isDarkBg && prefs->boldLinkLight)) ? semiBold_FontStyle
155 : regular_FontStyle,
156 contentRegular_FontSize);
157}
158
104static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) { 159static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) {
105 if (d->format == plainText_SourceFormat) { 160 if (d->format == plainText_SourceFormat) {
106 return text_GmLineType; 161 return text_GmLineType;
@@ -318,18 +373,6 @@ static iBool isGopher_GmDocument_(const iGmDocument *d) {
318 equalCase_Rangecc(scheme, "finger")); 373 equalCase_Rangecc(scheme, "finger"));
319} 374}
320 375
321static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {
322 const iRangecc scheme = urlScheme_String(&d->url);
323 if (equalCase_Rangecc(scheme, "gemini")) {
324 return prefs_App()->monospaceGemini;
325 }
326 if (equalCase_Rangecc(scheme, "gopher") ||
327 equalCase_Rangecc(scheme, "finger")) {
328 return prefs_App()->monospaceGopher;
329 }
330 return iFalse;
331}
332
333static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo, 376static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo,
334 uint16_t linkId) { 377 uint16_t linkId) {
335 iGmLink *link = at_PtrArray(&d->links, linkId - 1); 378 iGmLink *link = at_PtrArray(&d->links, linkId - 1);
@@ -402,7 +445,8 @@ struct Impl_RunTypesetter {
402 int rightMargin; 445 int rightMargin;
403 iBool isWordWrapped; 446 iBool isWordWrapped;
404 iBool isPreformat; 447 iBool isPreformat;
405 const int *fonts; 448 int baseFont;
449 int baseColor;
406}; 450};
407 451
408static void init_RunTypesetter_(iRunTypesetter *d) { 452static void init_RunTypesetter_(iRunTypesetter *d) {
@@ -425,39 +469,47 @@ static void commit_RunTypesetter_(iRunTypesetter *d, iGmDocument *doc) {
425 469
426static const int maxLedeLines_ = 10; 470static const int maxLedeLines_ = 10;
427 471
428static const int colors[max_GmLineType] = { 472static int applyAttributes_RunTypesetter_(iRunTypesetter *d, iTextAttrib attrib) {
429 tmParagraph_ColorId, 473 /* WARNING: This is duplicated in run_Font_(). Make sure they behave identically. */
430 tmParagraph_ColorId, 474 if (attrib.bold) {
431 tmPreformatted_ColorId, 475 d->run.font = fontWithStyle_Text(d->baseFont, bold_FontStyle);
432 tmQuote_ColorId, 476 d->run.color = tmFirstParagraph_ColorId;
433 tmHeading1_ColorId, 477 }
434 tmHeading2_ColorId, 478 else if (attrib.italic) {
435 tmHeading3_ColorId, 479 d->run.font = fontWithStyle_Text(d->baseFont, italic_FontStyle);
436 tmLinkText_ColorId, 480 }
437}; 481 else if (attrib.monospace) {
482 d->run.font = fontWithFamily_Text(d->baseFont, monospace_FontId);
483 d->run.color = tmPreformatted_ColorId;
484 }
485 else {
486 d->run.font = d->baseFont;
487 d->run.color = d->baseColor;
488 }
489}
438 490
439static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, int origin, 491static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, iTextAttrib attrib,
440 int advance, iBool isBaseRTL) { 492 int origin, int advance) {
441 iAssert(wrapRange.start <= wrapRange.end); 493 iAssert(wrapRange.start <= wrapRange.end);
442 trimEnd_Rangecc(&wrapRange); 494 trimEnd_Rangecc(&wrapRange);
443// printf("typeset: {%s}\n", cstr_Rangecc(wrapRange)); 495// printf("typeset: {%s}\n", cstr_Rangecc(wrapRange));
444 iRunTypesetter *d = wrap->context; 496 iRunTypesetter *d = wrap->context;
445 const int fontId = d->run.font;
446 d->run.text = wrapRange; 497 d->run.text = wrapRange;
498 applyAttributes_RunTypesetter_(d, attrib);
447 if (~d->run.flags & startOfLine_GmRunFlag && d->lineHeightReduction > 0.0f) { 499 if (~d->run.flags & startOfLine_GmRunFlag && d->lineHeightReduction > 0.0f) {
448 d->pos.y -= d->lineHeightReduction * lineHeight_Text(fontId); 500 d->pos.y -= d->lineHeightReduction * lineHeight_Text(d->baseFont);
449 } 501 }
450 d->run.bounds.pos = addX_I2(d->pos, origin + d->indent); 502 d->run.bounds.pos = addX_I2(d->pos, origin + d->indent);
451 const iInt2 dims = init_I2(advance, lineHeight_Text(fontId)); 503 const iInt2 dims = init_I2(advance, lineHeight_Text(d->baseFont));
452 iChangeFlags(d->run.flags, wide_GmRunFlag, (d->isPreformat && dims.x > d->layoutWidth)); 504 iChangeFlags(d->run.flags, wide_GmRunFlag, (d->isPreformat && dims.x > d->layoutWidth));
453 d->run.bounds.size.x = iMax(wrap->maxWidth, dims.x) - origin; /* Extends to the right edge for selection. */ 505 d->run.bounds.size.x = iMax(wrap->maxWidth, dims.x) - origin; /* Extends to the right edge for selection. */
454 d->run.bounds.size.y = dims.y; 506 d->run.bounds.size.y = dims.y;
455 d->run.visBounds = d->run.bounds; 507 d->run.visBounds = d->run.bounds;
456 d->run.visBounds.size.x = dims.x; 508 d->run.visBounds.size.x = dims.x;
457 d->run.isRTL = isBaseRTL; 509 d->run.isRTL = attrib.isBaseRTL;
458 pushBack_Array(&d->layout, &d->run); 510 pushBack_Array(&d->layout, &d->run);
459 d->run.flags &= ~startOfLine_GmRunFlag; 511 d->run.flags &= ~startOfLine_GmRunFlag;
460 d->pos.y += lineHeight_Text(fontId) * prefs_App()->lineSpacing; 512 d->pos.y += lineHeight_Text(d->baseFont) * prefs_App()->lineSpacing;
461 return iTrue; /* continue to next wrapped line */ 513 return iTrue; /* continue to next wrapped line */
462} 514}
463 515
@@ -469,25 +521,10 @@ static void doLayout_GmDocument_(iGmDocument *d) {
469 const iBool isVeryNarrow = d->size.x <= 70 * gap_Text; 521 const iBool isVeryNarrow = d->size.x <= 70 * gap_Text;
470 const iBool isExtremelyNarrow = d->size.x <= 60 * gap_Text; 522 const iBool isExtremelyNarrow = d->size.x <= 60 * gap_Text;
471 const iBool isFullWidthImages = (d->outsideMargin < 5 * gap_UI); 523 const iBool isFullWidthImages = (d->outsideMargin < 5 * gap_UI);
472 const iBool isDarkBg = isDark_GmDocumentTheme( 524// const iBool isDarkBg = isDark_GmDocumentTheme(
473 isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight); 525// isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
526 initTheme_GmDocument_(d);
474 /* TODO: Collect these parameters into a GmTheme. */ 527 /* TODO: Collect these parameters into a GmTheme. */
475 const enum iFontId headingFont = isMono ? documentMonospace_FontId : documentHeading_FontId;
476 const enum iFontId bodyFont = isMono ? documentMonospace_FontId : documentBody_FontId;
477 const int fonts[max_GmLineType] = {
478 FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize), /* text */
479 FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize), /* bullet */
480 preformatted_FontId, /* pre */
481 isMono ? monospaceParagraph_FontId : quote_FontId, /* quote */
482 FONT_ID(headingFont, bold_FontStyle, contentHuge_FontSize), /* h1 */
483 FONT_ID(headingFont, bold_FontStyle, contentLarge_FontSize), /* h2 */
484 FONT_ID(headingFont, regular_FontStyle, contentBig_FontSize), /* h3 */
485 FONT_ID(bodyFont,
486 ((isDarkBg && prefs->boldLinkDark) || (!isDarkBg && prefs->boldLinkLight))
487 ? semiBold_FontStyle
488 : regular_FontStyle,
489 contentRegular_FontSize) /* link */
490 };
491 float indents[max_GmLineType] = { 5, 10, 5, isNarrow ? 5 : 10, 0, 0, 0, 5 }; 528 float indents[max_GmLineType] = { 5, 10, 5, isNarrow ? 5 : 10, 0, 0, 0, 5 };
492 if (isExtremelyNarrow) { 529 if (isExtremelyNarrow) {
493 /* Further reduce the margins. */ 530 /* Further reduce the margins. */
@@ -598,7 +635,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
598 } 635 }
599 } 636 }
600 trimLine_Rangecc(&line, type, isNormalized); 637 trimLine_Rangecc(&line, type, isNormalized);
601 run.font = fonts[type]; 638 run.font = d->theme.fonts[type];
602 /* Remember headings for the document outline. */ 639 /* Remember headings for the document outline. */
603 if (type == heading1_GmLineType || type == heading2_GmLineType || type == heading3_GmLineType) { 640 if (type == heading1_GmLineType || type == heading2_GmLineType || type == heading3_GmLineType) {
604 pushBack_Array( 641 pushBack_Array(
@@ -618,7 +655,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
618 addSiteBanner = iFalse; /* overrides the banner */ 655 addSiteBanner = iFalse; /* overrides the banner */
619 continue; 656 continue;
620 } 657 }
621 run.preId = preId; 658 run.mediaType = max_MediaType; /* preformatted block */
659 run.mediaId = preId;
622 run.font = (d->format == plainText_SourceFormat ? plainText_FontId : preFont); 660 run.font = (d->format == plainText_SourceFormat ? plainText_FontId : preFont);
623 indent = indents[type]; 661 indent = indents[type];
624 } 662 }
@@ -650,7 +688,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
650 run.visBounds.size = init_I2(gap_Text, lineHeight_Text(run.font)); 688 run.visBounds.size = init_I2(gap_Text, lineHeight_Text(run.font));
651 run.bounds = zero_Rect(); /* just visual */ 689 run.bounds = zero_Rect(); /* just visual */
652 run.text = iNullRange; 690 run.text = iNullRange;
653 run.flags = quoteBorder_GmRunFlag | decoration_GmRunFlag; 691 run.flags = quoteBorder_GmRunFlag | decoration_GmRunFlag;
654 pushBack_Array(&d->layout, &run); 692 pushBack_Array(&d->layout, &run);
655 } 693 }
656 pos.y += lineHeight_Text(run.font) * prefs->lineSpacing; 694 pos.y += lineHeight_Text(run.font) * prefs->lineSpacing;
@@ -708,7 +746,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
708 altText.text).bounds.size; 746 altText.text).bounds.size;
709 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x, 747 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x,
710 size.y + 2 * margin.y); 748 size.y + 2 * margin.y);
711 altText.preId = preId; 749 altText.mediaType = max_MediaType; /* preformatted */
750 altText.mediaId = preId;
712 pushBack_Array(&d->layout, &altText); 751 pushBack_Array(&d->layout, &altText);
713 pos.y += height_Rect(altText.bounds); 752 pos.y += height_Rect(altText.bounds);
714 contentLine = meta->bounds; /* Skip the whole thing. */ 753 contentLine = meta->bounds; /* Skip the whole thing. */
@@ -723,7 +762,6 @@ static void doLayout_GmDocument_(iGmDocument *d) {
723 setRange_String(&d->title, line); 762 setRange_String(&d->title, line);
724 } 763 }
725 /* List bullet. */ 764 /* List bullet. */
726 run.color = colors[type];
727 if (type == bullet_GmLineType) { 765 if (type == bullet_GmLineType) {
728 /* TODO: Literata bullet is broken? */ 766 /* TODO: Literata bullet is broken? */
729 iGmRun bulRun = run; 767 iGmRun bulRun = run;
@@ -795,18 +833,17 @@ static void doLayout_GmDocument_(iGmDocument *d) {
795 icon.flags |= decoration_GmRunFlag; 833 icon.flags |= decoration_GmRunFlag;
796 pushBack_Array(&d->layout, &icon); 834 pushBack_Array(&d->layout, &icon);
797 } 835 }
798 run.color = colors[type]; 836 run.lineType = type;
837 run.color = d->theme.colors[type];
799 if (d->format == plainText_SourceFormat) { 838 if (d->format == plainText_SourceFormat) {
800 run.color = colors[text_GmLineType]; 839 run.color = d->theme.colors[text_GmLineType];
801 } 840 }
802 /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */ 841 /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */
803// int bigCount = 0; 842// int bigCount = 0;
804 iBool isLedeParagraph = iFalse;
805 if (type == text_GmLineType && isFirstText) { 843 if (type == text_GmLineType && isFirstText) {
806 if (!isMono) run.font = firstParagraph_FontId; 844 if (!isMono) run.font = firstParagraph_FontId;
807 run.color = tmFirstParagraph_ColorId; 845 run.color = tmFirstParagraph_ColorId;
808// bigCount = 15; /* max lines -- what if the whole document is one paragraph? */ 846 run.isLede = iTrue;
809 isLedeParagraph = iTrue;
810 isFirstText = iFalse; 847 isFirstText = iFalse;
811 } 848 }
812 else if (type != heading1_GmLineType) { 849 else if (type != heading1_GmLineType) {
@@ -826,7 +863,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
826 init_RunTypesetter_(&rts); 863 init_RunTypesetter_(&rts);
827 rts.run = run; 864 rts.run = run;
828 rts.pos = pos; 865 rts.pos = pos;
829 rts.fonts = fonts; 866 //rts.fonts = fonts;
830 rts.isWordWrapped = (d->format == plainText_SourceFormat ? prefs->plainTextWrap 867 rts.isWordWrapped = (d->format == plainText_SourceFormat ? prefs->plainTextWrap
831 : !isPreformat); 868 : !isPreformat);
832 rts.isPreformat = isPreformat; 869 rts.isPreformat = isPreformat;
@@ -859,6 +896,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
859 } 896 }
860 for (;;) { /* need to retry if the font needs changing */ 897 for (;;) { /* need to retry if the font needs changing */
861 rts.run.flags |= startOfLine_GmRunFlag; 898 rts.run.flags |= startOfLine_GmRunFlag;
899 rts.baseFont = rts.run.font;
900 rts.baseColor = rts.run.color;
862 iWrapText wrapText = { .text = line, 901 iWrapText wrapText = { .text = line,
863 .maxWidth = rts.isWordWrapped 902 .maxWidth = rts.isWordWrapped
864 ? d->size.x - run.bounds.pos.x - 903 ? d->size.x - run.bounds.pos.x -
@@ -868,7 +907,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
868 .wrapFunc = typesetOneLine_RunTypesetter_, 907 .wrapFunc = typesetOneLine_RunTypesetter_,
869 .context = &rts }; 908 .context = &rts };
870 measure_WrapText(&wrapText, rts.run.font); 909 measure_WrapText(&wrapText, rts.run.font);
871 if (!isLedeParagraph || size_Array(&rts.layout) <= maxLedeLines_) { 910 if (!rts.run.isLede || size_Array(&rts.layout) <= maxLedeLines_) {
872 if (wrapText.baseDir < 0) { 911 if (wrapText.baseDir < 0) {
873 /* Right-aligned paragraphs need margins and decorations to be flipped. */ 912 /* Right-aligned paragraphs need margins and decorations to be flipped. */
874 iForEach(Array, pr, &rts.layout) { 913 iForEach(Array, pr, &rts.layout) {
@@ -891,11 +930,12 @@ static void doLayout_GmDocument_(iGmDocument *d) {
891 commit_RunTypesetter_(&rts, d); 930 commit_RunTypesetter_(&rts, d);
892 break; 931 break;
893 } 932 }
933 /* Try again... */
894 clear_RunTypesetter_(&rts); 934 clear_RunTypesetter_(&rts);
895 rts.pos = pos; 935 rts.pos = pos;
896 rts.run.font = rts.fonts[text_GmLineType]; 936 rts.run.font = rts.baseFont = d->theme.fonts[text_GmLineType];
897 rts.run.color = colors[text_GmLineType]; 937 rts.run.color = rts.baseColor = d->theme.colors[text_GmLineType];
898 isLedeParagraph = iFalse; 938 rts.run.isLede = iFalse;
899 } 939 }
900 pos = rts.pos; 940 pos = rts.pos;
901 deinit_RunTypesetter_(&rts); 941 deinit_RunTypesetter_(&rts);
@@ -996,7 +1036,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
996 /* TODO: Store the dimensions and ranges for later access. */ 1036 /* TODO: Store the dimensions and ranges for later access. */
997 iForEach(Array, i, &d->layout) { 1037 iForEach(Array, i, &d->layout) {
998 iGmRun *run = i.value; 1038 iGmRun *run = i.value;
999 if (run->preId && run->flags & wide_GmRunFlag) { 1039 if (preId_GmRun(run) && run->flags & wide_GmRunFlag) {
1000 iGmRunRange block = findPreformattedRange_GmDocument(d, run); 1040 iGmRunRange block = findPreformattedRange_GmDocument(d, run);
1001 for (const iGmRun *j = block.start; j != block.end; j++) { 1041 for (const iGmRun *j = block.start; j != block.end; j++) {
1002 iConstCast(iGmRun *, j)->flags |= wide_GmRunFlag; 1042 iConstCast(iGmRun *, j)->flags |= wide_GmRunFlag;
@@ -1800,11 +1840,11 @@ static void flushPendingLinks_(iArray *links, const iString *source, iString *ou
1800static void convertMarkdownToGemtext_GmDocument_(iGmDocument *d) { 1840static void convertMarkdownToGemtext_GmDocument_(iGmDocument *d) {
1801 iAssert(d->format == markdown_SourceFormat); 1841 iAssert(d->format == markdown_SourceFormat);
1802 /* Get rid of indented preformats. */ { 1842 /* Get rid of indented preformats. */ {
1803 iArray *pendingLinks = collectNew_Array(sizeof(iPendingLink)); 1843 iArray *pendingLinks = collectNew_Array(sizeof(iPendingLink));
1804 const iRegExp *imageLinkPattern = iClob(new_RegExp("\n?!\\[(.+)\\]\\(([^)]+)\\)\n?", 0)); 1844 const iRegExp *imageLinkPattern = iClob(new_RegExp("\n?!\\[(.+)\\]\\(([^)]+)\\)\n?", 0));
1805 const iRegExp *linkPattern = iClob(new_RegExp("\\[(.+?)\\]\\(([^)]+)\\)", 0)); 1845 const iRegExp *linkPattern = iClob(new_RegExp("\\[(.+?)\\]\\(([^)]+)\\)", 0));
1806 const iRegExp *namedLinkPattern = iClob(new_RegExp("\\[(.+?)\\]\\[(.+?)\\]", 0)); 1846 const iRegExp *namedLinkPattern = iClob(new_RegExp("\\[(.+?)\\]\\[(.+?)\\]", 0));
1807 const iRegExp *namePattern = iClob(new_RegExp("\\s*\\[(.+?)\\]\\s*:\\s*([^\n]+)", 0)); 1847 const iRegExp *namePattern = iClob(new_RegExp("\\s*\\[(.+?)\\]\\s*:\\s*([^\n]+)", 0));
1808 iString result; 1848 iString result;
1809 init_String(&result); 1849 init_String(&result);
1810 iRangecc line = iNullRange; 1850 iRangecc line = iNullRange;
@@ -1865,7 +1905,7 @@ static void convertMarkdownToGemtext_GmDocument_(iGmDocument *d) {
1865 replaceRegExp_String(&ln, imageLinkPattern, "\n=> \\2 \\1\n", NULL, NULL); 1905 replaceRegExp_String(&ln, imageLinkPattern, "\n=> \\2 \\1\n", NULL, NULL);
1866 replaceRegExp_String(&ln, namedLinkPattern, "\\1", addPendingNamedLink_, pendingLinks); 1906 replaceRegExp_String(&ln, namedLinkPattern, "\\1", addPendingNamedLink_, pendingLinks);
1867 replaceRegExp_String(&ln, linkPattern, "\\1", addPendingLink_, pendingLinks); 1907 replaceRegExp_String(&ln, linkPattern, "\\1", addPendingLink_, pendingLinks);
1868 replaceRegExp_String(&ln, iClob(new_RegExp("(?<!`)`([^`]*?)`(?!`)", 0)), "\x1b[4m\\1\x1b[0m", NULL, NULL); 1908 replaceRegExp_String(&ln, iClob(new_RegExp("(?<!`)`([^`]+?)`(?!`)", 0)), "\x1b[4m\\1\x1b[0m", NULL, NULL);
1869 append_String(&result, &ln); 1909 append_String(&result, &ln);
1870 deinit_String(&ln); 1910 deinit_String(&ln);
1871 } 1911 }
@@ -2054,17 +2094,17 @@ iRangecc findTextBefore_GmDocument(const iGmDocument *d, const iString *text, co
2054} 2094}
2055 2095
2056iGmRunRange findPreformattedRange_GmDocument(const iGmDocument *d, const iGmRun *run) { 2096iGmRunRange findPreformattedRange_GmDocument(const iGmDocument *d, const iGmRun *run) {
2057 iAssert(run->preId); 2097 iAssert(preId_GmRun(run));
2058 iGmRunRange range = { run, run }; 2098 iGmRunRange range = { run, run };
2059 /* Find the beginning. */ 2099 /* Find the beginning. */
2060 while (range.start > (const iGmRun *) constData_Array(&d->layout)) { 2100 while (range.start > (const iGmRun *) constData_Array(&d->layout)) {
2061 const iGmRun *prev = range.start - 1; 2101 const iGmRun *prev = range.start - 1;
2062 if (prev->preId != run->preId) break; 2102 if (preId_GmRun(prev) != preId_GmRun(run)) break;
2063 range.start = prev; 2103 range.start = prev;
2064 } 2104 }
2065 /* Find the ending. */ 2105 /* Find the ending. */
2066 while (range.end < (const iGmRun *) constEnd_Array(&d->layout)) { 2106 while (range.end < (const iGmRun *) constEnd_Array(&d->layout)) {
2067 if (range.end->preId != run->preId) break; 2107 if (preId_GmRun(range.end) != preId_GmRun(run)) break;
2068 range.end++; 2108 range.end++;
2069 } 2109 }
2070 return range; 2110 return range;
@@ -2240,6 +2280,22 @@ iChar siteIcon_GmDocument(const iGmDocument *d) {
2240 return d->siteIcon; 2280 return d->siteIcon;
2241} 2281}
2242 2282
2283void runBaseAttributes_GmDocument(const iGmDocument *d, const iGmRun *run, int *fontId_out,
2284 int *colorId_out) {
2285 /* Font and color according to the line type. These are needed because each GmRun is
2286 a segment of a paragraph, and if the font or color changes inside the run, each wrapped
2287 segment needs to know both the current font/color and ALSO the base font/color, so
2288 the default attributes can be restored. */
2289 if (run->isLede) {
2290 *fontId_out = firstParagraph_FontId;
2291 *colorId_out = tmFirstParagraph_ColorId;
2292 }
2293 else {
2294 *fontId_out = fontWithSize_Text(d->theme.fonts[run->lineType], run->font % max_FontSize); /* retain size */
2295 *colorId_out = d->theme.colors[run->lineType];
2296 }
2297}
2298
2243iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) { 2299iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) {
2244 if (pos.y < top_Rect(d->bounds)) { 2300 if (pos.y < top_Rect(d->bounds)) {
2245 return (iRangecc){ d->text.start, d->text.start }; 2301 return (iRangecc){ d->text.start, d->text.start };
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 20bc9890..6ad7efdc 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -140,16 +140,13 @@ struct Impl_GmRun {
140 uint32_t color : 7; /* see max_ColorId */ 140 uint32_t color : 7; /* see max_ColorId */
141 141
142 uint32_t font : 10; 142 uint32_t font : 10;
143 uint32_t mediaType : 3; 143 uint32_t mediaType : 3; /* note: max_MediaType means preformatted block */
144 uint32_t mediaId : 9; /* zero if not an image */ 144 uint32_t lineType : 3;
145 uint32_t preId : 10; /* preformatted block ID (sequential); merge with mediaId? */ 145 uint32_t mediaId : 15; /* zero if not an image */
146 uint32_t isLede : 1;
146 }; 147 };
147}; 148};
148 149
149iLocalDef iMediaId mediaId_GmRun(const iGmRun *d) {
150 return (iMediaId){ .type = d->mediaType, .id = d->mediaId };
151}
152
153iDeclareType(GmRunRange) 150iDeclareType(GmRunRange)
154 151
155struct Impl_GmRunRange { 152struct Impl_GmRunRange {
@@ -157,7 +154,20 @@ struct Impl_GmRunRange {
157 const iGmRun *end; 154 const iGmRun *end;
158}; 155};
159 156
160iRangecc findLoc_GmRun (const iGmRun *, iInt2 pos); 157iLocalDef iBool isMedia_GmRun(const iGmRun *d) {
158 return d->mediaType > 0 && d->mediaType < max_MediaType;
159}
160iLocalDef iMediaId mediaId_GmRun(const iGmRun *d) {
161 if (d->mediaType < max_MediaType) {
162 return (iMediaId){ .type = d->mediaType, .id = d->mediaId };
163 }
164 return iInvalidMediaId;
165}
166iLocalDef uint32_t preId_GmRun(const iGmRun *d) {
167 return d->mediaType == max_MediaType ? d->mediaId : 0;
168}
169
170iRangecc findLoc_GmRun (const iGmRun *, iInt2 pos);
161 171
162iDeclareClass(GmDocument) 172iDeclareClass(GmDocument)
163iDeclareObjectConstruction(GmDocument) 173iDeclareObjectConstruction(GmDocument)
@@ -215,6 +225,9 @@ iRangecc findText_GmDocument (const iGmDocument *, const
215iRangecc findTextBefore_GmDocument (const iGmDocument *, const iString *text, const char *before); 225iRangecc findTextBefore_GmDocument (const iGmDocument *, const iString *text, const char *before);
216iGmRunRange findPreformattedRange_GmDocument (const iGmDocument *, const iGmRun *run); 226iGmRunRange findPreformattedRange_GmDocument (const iGmDocument *, const iGmRun *run);
217 227
228void runBaseAttributes_GmDocument (const iGmDocument *, const iGmRun *run,
229 int *fontId_out, int *colorId_out);
230
218enum iGmLinkPart { 231enum iGmLinkPart {
219 icon_GmLinkPart, 232 icon_GmLinkPart,
220 text_GmLinkPart, 233 text_GmLinkPart,
@@ -241,3 +254,4 @@ const iGmPreMeta *preMeta_GmDocument (const iGmDocument *, uint16_t preId);
241iInt2 preRunMargin_GmDocument (const iGmDocument *, uint16_t preId); 254iInt2 preRunMargin_GmDocument (const iGmDocument *, uint16_t preId);
242iBool preIsFolded_GmDocument (const iGmDocument *, uint16_t preId); 255iBool preIsFolded_GmDocument (const iGmDocument *, uint16_t preId);
243iBool preHasAltText_GmDocument(const iGmDocument *, uint16_t preId); 256iBool preHasAltText_GmDocument(const iGmDocument *, uint16_t preId);
257
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 8c87ba1a..8b2d6a5a 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -569,7 +569,7 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
569 } 569 }
570 d->visibleRuns.end = run; 570 d->visibleRuns.end = run;
571 } 571 }
572 if (run->preId) { 572 if (preId_GmRun(run)) {
573 pushBack_PtrArray(&d->visiblePre, run); 573 pushBack_PtrArray(&d->visiblePre, run);
574 if (run->flags & wide_GmRunFlag) { 574 if (run->flags & wide_GmRunFlag) {
575 pushBack_PtrArray(&d->visibleWideRuns, run); 575 pushBack_PtrArray(&d->visibleWideRuns, run);
@@ -635,14 +635,14 @@ static void invalidateVisibleLinks_DocumentWidget_(iDocumentWidget *d) {
635} 635}
636 636
637static int runOffset_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { 637static int runOffset_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) {
638 if (run->preId && run->flags & wide_GmRunFlag) { 638 if (preId_GmRun(run) && run->flags & wide_GmRunFlag) {
639 if (d->animWideRunId == run->preId) { 639 if (d->animWideRunId == preId_GmRun(run)) {
640 return -value_Anim(&d->animWideRunOffset); 640 return -value_Anim(&d->animWideRunOffset);
641 } 641 }
642 const size_t numOffsets = size_Array(&d->wideRunOffsets); 642 const size_t numOffsets = size_Array(&d->wideRunOffsets);
643 const int *offsets = constData_Array(&d->wideRunOffsets); 643 const int *offsets = constData_Array(&d->wideRunOffsets);
644 if (run->preId <= numOffsets) { 644 if (preId_GmRun(run) <= numOffsets) {
645 return -offsets[run->preId - 1]; 645 return -offsets[preId_GmRun(run) - 1];
646 } 646 }
647 } 647 }
648 return 0; 648 return 0;
@@ -731,7 +731,7 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
731 } 731 }
732 } 732 }
733 else if (d->hoverPre && 733 else if (d->hoverPre &&
734 preHasAltText_GmDocument(d->doc, d->hoverPre->preId) && 734 preHasAltText_GmDocument(d->doc, preId_GmRun(d->hoverPre)) &&
735 ~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { 735 ~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) {
736 setValueSpeed_Anim(&d->altTextOpacity, 1.0f, 1.5f); 736 setValueSpeed_Anim(&d->altTextOpacity, 1.0f, 1.5f);
737 if (!isFinished_Anim(&d->altTextOpacity)) { 737 if (!isFinished_Anim(&d->altTextOpacity)) {
@@ -1812,10 +1812,10 @@ static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos,
1812 maxWidth = iMax(maxWidth, width_Rect(r->visBounds)); 1812 maxWidth = iMax(maxWidth, width_Rect(r->visBounds));
1813 } 1813 }
1814 const int maxOffset = maxWidth - documentWidth_DocumentWidget_(d) + d->pageMargin * gap_UI; 1814 const int maxOffset = maxWidth - documentWidth_DocumentWidget_(d) + d->pageMargin * gap_UI;
1815 if (size_Array(&d->wideRunOffsets) <= run->preId) { 1815 if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) {
1816 resize_Array(&d->wideRunOffsets, run->preId + 1); 1816 resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1);
1817 } 1817 }
1818 int *offset = at_Array(&d->wideRunOffsets, run->preId - 1); 1818 int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1);
1819 const int oldOffset = *offset; 1819 const int oldOffset = *offset;
1820 *offset = iClamp(*offset + delta, 0, maxOffset); 1820 *offset = iClamp(*offset + delta, 0, maxOffset);
1821 /* Make sure the whole block gets redraw. */ 1821 /* Make sure the whole block gets redraw. */
@@ -1828,8 +1828,8 @@ static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos,
1828 d->foundMark = iNullRange; 1828 d->foundMark = iNullRange;
1829 } 1829 }
1830 if (duration) { 1830 if (duration) {
1831 if (d->animWideRunId != run->preId || isFinished_Anim(&d->animWideRunOffset)) { 1831 if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) {
1832 d->animWideRunId = run->preId; 1832 d->animWideRunId = preId_GmRun(run);
1833 init_Anim(&d->animWideRunOffset, oldOffset); 1833 init_Anim(&d->animWideRunOffset, oldOffset);
1834 } 1834 }
1835 setValueEased_Anim(&d->animWideRunOffset, *offset, duration); 1835 setValueEased_Anim(&d->animWideRunOffset, *offset, duration);
@@ -3814,7 +3814,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3814 } 3814 }
3815 /* Fold/unfold a preformatted block. */ 3815 /* Fold/unfold a preformatted block. */
3816 if (~d->flags & selecting_DocumentWidgetFlag && d->hoverPre && 3816 if (~d->flags & selecting_DocumentWidgetFlag && d->hoverPre &&
3817 preIsFolded_GmDocument(d->doc, d->hoverPre->preId)) { 3817 preIsFolded_GmDocument(d->doc, preId_GmRun(d->hoverPre))) {
3818 return iTrue; 3818 return iTrue;
3819 } 3819 }
3820 /* Begin selecting a range of text. */ 3820 /* Begin selecting a range of text. */
@@ -3925,7 +3925,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3925 } 3925 }
3926 } 3926 }
3927 if (d->hoverPre) { 3927 if (d->hoverPre) {
3928 togglePreFold_DocumentWidget_(d, d->hoverPre->preId); 3928 togglePreFold_DocumentWidget_(d, preId_GmRun(d->hoverPre));
3929 return iTrue; 3929 return iTrue;
3930 } 3930 }
3931 if (d->hoverLink) { 3931 if (d->hoverLink) {
@@ -4124,7 +4124,7 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol
4124 4124
4125static void drawMark_DrawContext_(void *context, const iGmRun *run) { 4125static void drawMark_DrawContext_(void *context, const iGmRun *run) {
4126 iDrawContext *d = context; 4126 iDrawContext *d = context;
4127 if (run->mediaType == none_MediaType) { 4127 if (!isMedia_GmRun(run)) {
4128 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark); 4128 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark);
4129 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark); 4129 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark);
4130 } 4130 }
@@ -4249,7 +4249,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4249 } 4249 }
4250 return; 4250 return;
4251 } 4251 }
4252 else if (run->mediaType) { 4252 else if (isMedia_GmRun(run)) {
4253 /* Media UIs are drawn afterwards as a dynamic overlay. */ 4253 /* Media UIs are drawn afterwards as a dynamic overlay. */
4254 return; 4254 return;
4255 } 4255 }
@@ -4317,7 +4317,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4317 } 4317 }
4318 } 4318 }
4319 if (run->flags & altText_GmRunFlag) { 4319 if (run->flags & altText_GmRunFlag) {
4320 const iInt2 margin = preRunMargin_GmDocument(doc, run->preId); 4320 const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run));
4321 fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId); 4321 fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId);
4322 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmQuoteIcon_ColorId); 4322 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmQuoteIcon_ColorId);
4323 drawWrapRange_Text(run->font, 4323 drawWrapRange_Text(run->font,
@@ -4368,11 +4368,17 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4368 height_Rect(run->visBounds), 4368 height_Rect(run->visBounds),
4369 tmQuoteIcon_ColorId); 4369 tmQuoteIcon_ColorId);
4370 } 4370 }
4371 /* Base attributes. */ {
4372 int f, c;
4373 runBaseAttributes_GmDocument(doc, run, &f, &c);
4374 setBaseAttributes_Text(f, c);
4375 }
4371 drawBoundRange_Text(run->font, 4376 drawBoundRange_Text(run->font,
4372 visPos, 4377 visPos,
4373 (run->isRTL ? -1 : 1) * width_Rect(run->visBounds), 4378 (run->isRTL ? -1 : 1) * width_Rect(run->visBounds),
4374 fg, 4379 fg,
4375 run->text); 4380 run->text);
4381 setBaseAttributes_Text(-1, -1);
4376 runDrawn:; 4382 runDrawn:;
4377 } 4383 }
4378 /* Presentation of links. */ 4384 /* Presentation of links. */
@@ -4944,7 +4950,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
4944 /* Alt text. */ 4950 /* Alt text. */
4945 const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5; 4951 const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5;
4946 if (d->hoverAltPre && altTextOpacity > 0) { 4952 if (d->hoverAltPre && altTextOpacity > 0) {
4947 const iGmPreMeta *meta = preMeta_GmDocument(d->doc, d->hoverAltPre->preId); 4953 const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre));
4948 if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag && 4954 if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag &&
4949 !isEmpty_Range(&meta->altText)) { 4955 !isEmpty_Range(&meta->altText)) {
4950 const int margin = 3 * gap_UI / 2; 4956 const int margin = 3 * gap_UI / 2;
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 874cf2b5..e20d6f17 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -2245,31 +2245,33 @@ struct Impl_MarkPainter {
2245 iRect lastMarkRect; 2245 iRect lastMarkRect;
2246}; 2246};
2247 2247
2248static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, int origin, int advance, 2248static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, iTextAttrib attrib,
2249 iBool isBaseRTL) { 2249 int origin, int advance) {
2250 iUnused(isBaseRTL);
2251 iMarkPainter *mp = wrapText->context; 2250 iMarkPainter *mp = wrapText->context;
2252 const iRanges mark = mp->mark; 2251 const iRanges mark = mp->mark;
2253 if (isEmpty_Range(&mark)) { 2252 if (isEmpty_Range(&mark)) {
2254 return iTrue; /* nothing marked */ 2253 return iTrue; /* nothing marked */
2255 } 2254 }
2255 int fontId = mp->d->font;
2256 /* TODO: Apply attrib on the font */
2256 const char *cstr = cstr_String(&mp->line->text); 2257 const char *cstr = cstr_String(&mp->line->text);
2257 const iRanges lineRange = { 2258 const iRanges lineRange = {
2258 wrappedText.start - cstr + mp->line->range.start, 2259 wrappedText.start - cstr + mp->line->range.start,
2259 wrappedText.end - cstr + mp->line->range.start 2260 wrappedText.end - cstr + mp->line->range.start
2260 }; 2261 };
2262 const int lineHeight = lineHeight_Text(mp->d->font);
2261 if (mark.end <= lineRange.start || mark.start >= lineRange.end) { 2263 if (mark.end <= lineRange.start || mark.start >= lineRange.end) {
2262 mp->pos.y += lineHeight_Text(mp->d->font); 2264 mp->pos.y += lineHeight;
2263 return iTrue; /* outside of mark */ 2265 return iTrue; /* outside of mark */
2264 } 2266 }
2265 iRect rect = { addX_I2(mp->pos, origin), init_I2(advance, lineHeight_Text(mp->d->font)) }; 2267 iRect rect = { addX_I2(mp->pos, origin), init_I2(advance, lineHeight) };
2266 if (mark.end < lineRange.end) { 2268 if (mark.end < lineRange.end) {
2267 /* Calculate where the mark ends. */ 2269 /* Calculate where the mark ends. */
2268 const iRangecc markedPrefix = { 2270 const iRangecc markedPrefix = {
2269 wrappedText.start, 2271 wrappedText.start,
2270 wrappedText.start + mark.end - lineRange.start 2272 wrappedText.start + mark.end - lineRange.start
2271 }; 2273 };
2272 rect.size.x = measureRange_Text(mp->d->font, markedPrefix).advance.x; 2274 rect.size.x = measureRange_Text(fontId, markedPrefix).advance.x;
2273 } 2275 }
2274 if (mark.start > lineRange.start) { 2276 if (mark.start > lineRange.start) {
2275 /* Calculate where the mark starts. */ 2277 /* Calculate where the mark starts. */
@@ -2277,10 +2279,10 @@ static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, int or
2277 wrappedText.start, 2279 wrappedText.start,
2278 wrappedText.start + mark.start - lineRange.start 2280 wrappedText.start + mark.start - lineRange.start
2279 }; 2281 };
2280 adjustEdges_Rect(&rect, 0, 0, 0, measureRange_Text(mp->d->font, unmarkedPrefix).advance.x); 2282 adjustEdges_Rect(&rect, 0, 0, 0, measureRange_Text(fontId, unmarkedPrefix).advance.x);
2281 } 2283 }
2282 rect.size.x = iMax(gap_UI / 3, rect.size.x); 2284 rect.size.x = iMax(gap_UI / 3, rect.size.x);
2283 mp->pos.y += lineHeight_Text(mp->d->font); 2285 mp->pos.y += lineHeight;
2284 fillRect_Paint(mp->paint, rect, uiMarked_ColorId | opaque_ColorId); 2286 fillRect_Paint(mp->paint, rect, uiMarked_ColorId | opaque_ColorId);
2285 if (deviceType_App() != desktop_AppDeviceType) { 2287 if (deviceType_App() != desktop_AppDeviceType) {
2286 if (isEmpty_Rect(mp->firstMarkRect)) mp->firstMarkRect = rect; 2288 if (isEmpty_Rect(mp->firstMarkRect)) mp->firstMarkRect = rect;
diff --git a/src/ui/text.c b/src/ui/text.c
index fd865fbd..52c6c4e0 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -262,6 +262,8 @@ struct Impl_Text {
262 SDL_Palette * grayscale; 262 SDL_Palette * grayscale;
263 SDL_Palette * blackAndWhite; /* unsmoothed glyph palette */ 263 SDL_Palette * blackAndWhite; /* unsmoothed glyph palette */
264 iRegExp * ansiEscape; 264 iRegExp * ansiEscape;
265 int baseFontId; /* base attributes (for restoring via escapes) */
266 int baseColorId;
265}; 267};
266 268
267iDefineTypeConstructionArgs(Text, (SDL_Renderer *render), render) 269iDefineTypeConstructionArgs(Text, (SDL_Renderer *render), render)
@@ -537,10 +539,10 @@ void init_Text(iText *d, SDL_Renderer *render) {
537 iText *oldActive = activeText_; 539 iText *oldActive = activeText_;
538 activeText_ = d; 540 activeText_ = d;
539 init_Array(&d->fonts, sizeof(iFont)); 541 init_Array(&d->fonts, sizeof(iFont));
540// d->contentFont = nunito_TextFont;
541// d->headingFont = nunito_TextFont;
542 d->contentFontSize = contentScale_Text_; 542 d->contentFontSize = contentScale_Text_;
543 d->ansiEscape = new_RegExp("[[()]([0-9;AB]*)m", 0); 543 d->ansiEscape = new_RegExp("[[()]([0-9;AB]*)m", 0);
544 d->baseFontId = -1;
545 d->baseColorId = -1;
544 d->render = render; 546 d->render = render;
545 /* A grayscale palette for rasterized glyphs. */ { 547 /* A grayscale palette for rasterized glyphs. */ {
546 SDL_Color colors[256]; 548 SDL_Color colors[256];
@@ -581,13 +583,10 @@ void setOpacity_Text(float opacity) {
581 SDL_SetTextureAlphaMod(activeText_->cache, iClamp(opacity, 0.0f, 1.0f) * 255 + 0.5f); 583 SDL_SetTextureAlphaMod(activeText_->cache, iClamp(opacity, 0.0f, 1.0f) * 255 + 0.5f);
582} 584}
583 585
584//void setFont_Text(iText *d, int fontId, const char *fontSpecId) { 586void setBaseAttributes_Text(int fontId, int colorId) {
585// setupFontVariants_Text_(d, findSpec_Fonts(fontSpecId), fontId); 587 activeText_->baseFontId = fontId;
586// if (d->contentFont != font) { 588 activeText_->baseColorId = colorId;
587// d->contentFont = font; 589}
588// resetFonts_Text(d);
589// }
590//}
591 590
592void setDocumentFontSize_Text(iText *d, float fontSizeFactor) { 591void setDocumentFontSize_Text(iText *d, float fontSizeFactor) {
593 fontSizeFactor *= contentScale_Text_; 592 fontSizeFactor *= contentScale_Text_;
@@ -855,25 +854,45 @@ static iBool isControl_Char_(iChar c) {
855iDeclareType(AttributedRun) 854iDeclareType(AttributedRun)
856 855
857struct Impl_AttributedRun { 856struct Impl_AttributedRun {
858 iRangei logical; /* UTF-32 codepoint indices in the logical-order text */ 857 iRangei logical; /* UTF-32 codepoint indices in the logical-order text */
859 iFont * font; 858 iTextAttrib attrib;
860 iColor fgColor; 859 iFont *font;
860 iColor fgColor_; /* any RGB color; A > 0 */
861 struct { 861 struct {
862 uint8_t isLineBreak : 1; 862 uint8_t isLineBreak : 1;
863 uint8_t isRTL : 1; 863// uint8_t isRTL : 1;
864 uint8_t isArabic : 1; /* Arabic script detected */ 864 uint8_t isArabic : 1; /* Arabic script detected */
865 } flags; 865 } flags;
866}; 866};
867 867
868static iColor fgColor_AttributedRun_(const iAttributedRun *d) {
869 if (d->fgColor_.a) {
870 return d->fgColor_;
871 }
872 if (d->attrib.colorId == none_ColorId) {
873 return (iColor){ 255, 255, 255, 255 };
874 }
875 return get_Color(d->attrib.colorId);
876}
877
878static void setFgColor_AttributedRun_(iAttributedRun *d, int colorId) {
879 d->attrib.colorId = colorId;
880 d->fgColor_.a = 0;
881}
882
868iDeclareType(AttributedText) 883iDeclareType(AttributedText)
869iDeclareTypeConstructionArgs(AttributedText, iRangecc text, size_t maxLen, iFont *font, 884iDeclareTypeConstructionArgs(AttributedText, iRangecc text, size_t maxLen, iFont *font,
870 iColor fgColor, int baseDir, iChar overrideChar) 885 int colorId, int baseDir, iFont *baseFont, int baseColorId,
886 iChar overrideChar)
871 887
872struct Impl_AttributedText { 888struct Impl_AttributedText {
873 iRangecc source; /* original source text */ 889 iRangecc source; /* original source text */
874 size_t maxLen; 890 size_t maxLen;
875 iFont * font; 891 iFont * font;
876 iColor fgColor; 892 int colorId;
893 iFont * baseFont;
894 int baseColorId;
895 iBool isBaseRTL;
877 iArray runs; 896 iArray runs;
878 iArray logical; /* UTF-32 text in logical order (mixed directions; matches source) */ 897 iArray logical; /* UTF-32 text in logical order (mixed directions; matches source) */
879 iArray visual; /* UTF-32 text in visual order (LTR) */ 898 iArray visual; /* UTF-32 text in visual order (LTR) */
@@ -881,13 +900,14 @@ struct Impl_AttributedText {
881 iArray visualToLogical; 900 iArray visualToLogical;
882 iArray logicalToSourceOffset; /* map logical character to an UTF-8 offset in the source text */ 901 iArray logicalToSourceOffset; /* map logical character to an UTF-8 offset in the source text */
883 char * bidiLevels; 902 char * bidiLevels;
884 iBool isBaseRTL;
885}; 903};
886 904
887iDefineTypeConstructionArgs(AttributedText, 905iDefineTypeConstructionArgs(AttributedText,
888 (iRangecc text, size_t maxLen, iFont *font, iColor fgColor, 906 (iRangecc text, size_t maxLen, iFont *font, int colorId,
889 int baseDir, iChar overrideChar), 907 int baseDir, iFont *baseFont, int baseColorId,
890 text, maxLen, font, fgColor, baseDir, overrideChar) 908 iChar overrideChar),
909 text, maxLen, font, colorId, baseDir, baseFont, baseColorId,
910 overrideChar)
891 911
892static const char *sourcePtr_AttributedText_(const iAttributedText *d, int logicalPos) { 912static const char *sourcePtr_AttributedText_(const iAttributedText *d, int logicalPos) {
893 const int *logToSource = constData_Array(&d->logicalToSourceOffset); 913 const int *logToSource = constData_Array(&d->logicalToSourceOffset);
@@ -916,16 +936,22 @@ static void finishRun_AttributedText_(iAttributedText *d, iAttributedRun *run, i
916 run->logical.start = endAt; 936 run->logical.start = endAt;
917} 937}
918 938
919static iFont *withStyle_Font_(const iFont *d, enum iFontStyle styleId) { 939int fontWithSize_Text(int font, enum iFontSize sizeId) {
920 const int fontId = (fontId_Text_(d) / maxVariants_Fonts) * maxVariants_Fonts; 940 const int familyId = (font / maxVariants_Fonts) * maxVariants_Fonts;
921 const int sizeId = sizeId_Text_(d); 941 const int styleId = (font / max_FontSize) % max_FontStyle;
922 return font_Text_(FONT_ID(fontId, styleId, sizeId)); 942 return FONT_ID(familyId, styleId, sizeId);
943}
944
945int fontWithStyle_Text(int font, enum iFontStyle styleId) {
946 const int familyId = (font / maxVariants_Fonts) * maxVariants_Fonts;
947 const int sizeId = font % max_FontSize;
948 return FONT_ID(familyId, styleId, sizeId);
923} 949}
924 950
925static iFont *withFontId_Font_(const iFont *d, enum iFontId fontId) { 951int fontWithFamily_Text(int font, enum iFontId familyId) {
926 const int styleId = styleId_Text_(d); 952 const int styleId = (font / max_FontSize) % max_FontStyle;
927 const int sizeId = sizeId_Text_(d); 953 const int sizeId = font % max_FontSize;
928 return font_Text_(FONT_ID(fontId, styleId, sizeId)); 954 return FONT_ID(familyId, styleId, sizeId);
929} 955}
930 956
931static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iChar overrideChar) { 957static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iChar overrideChar) {
@@ -982,15 +1008,17 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
982 pushBack_Array(&d->logicalToVisual, &(int){ length }); 1008 pushBack_Array(&d->logicalToVisual, &(int){ length });
983 pushBack_Array(&d->visualToLogical, &(int){ length }); 1009 pushBack_Array(&d->visualToLogical, &(int){ length });
984 } 1010 }
985 iAttributedRun run = { .logical = { 0, length }, 1011 iAttributedRun run = {
986 .font = d->font, 1012 .logical = { 0, length },
987 .fgColor = d->fgColor }; 1013 .attrib = { .colorId = d->colorId, .isBaseRTL = d->isBaseRTL },
988 const int * logToSource = constData_Array(&d->logicalToSourceOffset); 1014 .font = d->font,
1015 };
1016 const int *logToSource = constData_Array(&d->logicalToSourceOffset);
989 const int * logToVis = constData_Array(&d->logicalToVisual); 1017 const int * logToVis = constData_Array(&d->logicalToVisual);
990 const iChar * logicalText = constData_Array(&d->logical); 1018 const iChar * logicalText = constData_Array(&d->logical);
991 iBool isRTL = d->isBaseRTL; 1019 iBool isRTL = d->isBaseRTL;
992 int numNonSpace = 0; 1020 int numNonSpace = 0;
993 iFont * activeFont = d->font; 1021 iFont * attribFont = d->font;
994 for (int pos = 0; pos < length; pos++) { 1022 for (int pos = 0; pos < length; pos++) {
995 const iChar ch = logicalText[pos]; 1023 const iChar ch = logicalText[pos];
996#if defined (LAGRANGE_ENABLE_FRIBIDI) 1024#if defined (LAGRANGE_ENABLE_FRIBIDI)
@@ -1010,7 +1038,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
1010#else 1038#else
1011 const iBool isNeutral = iTrue; 1039 const iBool isNeutral = iTrue;
1012#endif 1040#endif
1013 run.flags.isRTL = isRTL; 1041 run.attrib.isRTL = isRTL;
1014 if (ch == 0x1b) { /* ANSI escape. */ 1042 if (ch == 0x1b) { /* ANSI escape. */
1015 pos++; 1043 pos++;
1016 const char *srcPos = d->source.start + logToSource[pos]; 1044 const char *srcPos = d->source.start + logToSource[pos];
@@ -1020,21 +1048,36 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
1020 if (match_RegExp(activeText_->ansiEscape, srcPos, d->source.end - srcPos, &m)) { 1048 if (match_RegExp(activeText_->ansiEscape, srcPos, d->source.end - srcPos, &m)) {
1021 finishRun_AttributedText_(d, &run, pos - 1); 1049 finishRun_AttributedText_(d, &run, pos - 1);
1022 const iRangecc sequence = capturedRange_RegExpMatch(&m, 1); 1050 const iRangecc sequence = capturedRange_RegExpMatch(&m, 1);
1051 /* TODO: Bold/italic attributes are assumed to be inside body text.
1052 We don't know what the current text style is supposed to be.
1053 That should be an additional attribute passed to WrapText, or a feature of
1054 WrapText that can be called both from here and in the run typesetter.
1055 The styling here is hardcoded to match `typesetOneLine_RunTypesetter_()`. */
1023 if (equal_Rangecc(sequence, "1")) { 1056 if (equal_Rangecc(sequence, "1")) {
1024 activeFont = withStyle_Font_(activeFont, bold_FontStyle); 1057 run.attrib.bold = iTrue;
1058 if (d->baseColorId == tmParagraph_ColorId) {
1059 setFgColor_AttributedRun_(&run, tmFirstParagraph_ColorId);
1060 }
1061 attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), bold_FontStyle));
1025 } 1062 }
1026 else if (equal_Rangecc(sequence, "3")) { 1063 else if (equal_Rangecc(sequence, "3")) {
1027 activeFont = withStyle_Font_(activeFont, italic_FontStyle); 1064 run.attrib.italic = iTrue;
1065 attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), italic_FontStyle));
1028 } 1066 }
1029 else if (equal_Rangecc(sequence, "4")) { 1067 else if (equal_Rangecc(sequence, "4")) {
1030 activeFont = withFontId_Font_(activeFont, monospace_FontId); 1068 run.attrib.monospace = iTrue;
1069 setFgColor_AttributedRun_(&run, tmPreformatted_ColorId);
1070 attribFont = font_Text_(fontWithFamily_Text(fontId_Text_(d->baseFont), monospace_FontId));
1031 } 1071 }
1032 else if (equal_Rangecc(sequence, "0")) { 1072 else if (equal_Rangecc(sequence, "0")) {
1033 activeFont = d->font; /* restore original */ 1073 run.attrib.bold = iFalse;
1034 run.fgColor = d->fgColor; 1074 run.attrib.italic = iFalse;
1075 run.attrib.monospace = iFalse;
1076 attribFont = d->baseFont;
1077 setFgColor_AttributedRun_(&run, d->baseColorId);
1035 } 1078 }
1036 else { 1079 else {
1037 run.fgColor = ansiForeground_Color(sequence, tmParagraph_ColorId); 1080 run.fgColor_ = ansiForeground_Color(sequence, tmParagraph_ColorId);
1038 } 1081 }
1039 pos += length_Rangecc(capturedRange_RegExpMatch(&m, 0)); 1082 pos += length_Rangecc(capturedRange_RegExpMatch(&m, 0));
1040 iAssert(logToSource[pos] == end_RegExpMatch(&m) - d->source.start); 1083 iAssert(logToSource[pos] == end_RegExpMatch(&m) - d->source.start);
@@ -1056,7 +1099,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
1056 colorNum = esc - asciiBase_ColorEscape; 1099 colorNum = esc - asciiBase_ColorEscape;
1057 } 1100 }
1058 run.logical.start = pos + 1; 1101 run.logical.start = pos + 1;
1059 run.fgColor = (colorNum >= 0 ? get_Color(colorNum) : d->fgColor); 1102 setFgColor_AttributedRun_(&run, colorNum >= 0 ? colorNum : d->colorId);
1060 continue; 1103 continue;
1061 } 1104 }
1062 if (ch == '\n') { 1105 if (ch == '\n') {
@@ -1077,7 +1120,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
1077 } 1120 }
1078 continue; 1121 continue;
1079 } 1122 }
1080 iFont *currentFont = activeFont; 1123 iFont *currentFont = attribFont;
1081 if (run.font->fontSpec->flags & arabic_FontSpecFlag && isPunct_Char(ch)) { 1124 if (run.font->fontSpec->flags & arabic_FontSpecFlag && isPunct_Char(ch)) {
1082 currentFont = run.font; /* remain as Arabic for whitespace */ 1125 currentFont = run.font; /* remain as Arabic for whitespace */
1083 } 1126 }
@@ -1113,12 +1156,15 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
1113#endif 1156#endif
1114} 1157}
1115 1158
1116void init_AttributedText(iAttributedText *d, iRangecc text, size_t maxLen, iFont *font, iColor fgColor, 1159void init_AttributedText(iAttributedText *d, iRangecc text, size_t maxLen, iFont *font, int colorId,
1117 int baseDir, iChar overrideChar) { 1160 int baseDir, iFont *baseFont, int baseColorId, iChar overrideChar) {
1118 d->source = text; 1161 d->source = text;
1119 d->maxLen = maxLen ? maxLen : iInvalidSize; 1162 d->maxLen = maxLen ? maxLen : iInvalidSize;
1120 d->font = font; 1163 d->font = font;
1121 d->fgColor = fgColor; 1164 d->colorId = colorId;
1165 d->baseFont = baseFont;
1166 d->baseColorId = baseColorId;
1167 d->isBaseRTL = iFalse;
1122 init_Array(&d->runs, sizeof(iAttributedRun)); 1168 init_Array(&d->runs, sizeof(iAttributedRun));
1123 init_Array(&d->logical, sizeof(iChar)); 1169 init_Array(&d->logical, sizeof(iChar));
1124 init_Array(&d->visual, sizeof(iChar)); 1170 init_Array(&d->visual, sizeof(iChar));
@@ -1126,7 +1172,6 @@ void init_AttributedText(iAttributedText *d, iRangecc text, size_t maxLen, iFont
1126 init_Array(&d->visualToLogical, sizeof(int)); 1172 init_Array(&d->visualToLogical, sizeof(int));
1127 init_Array(&d->logicalToSourceOffset, sizeof(int)); 1173 init_Array(&d->logicalToSourceOffset, sizeof(int));
1128 d->bidiLevels = NULL; 1174 d->bidiLevels = NULL;
1129 d->isBaseRTL = iFalse;
1130 prepare_AttributedText_(d, baseDir, overrideChar); 1175 prepare_AttributedText_(d, baseDir, overrideChar);
1131} 1176}
1132 1177
@@ -1278,7 +1323,7 @@ static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) {
1278 iArray glyphIndices; 1323 iArray glyphIndices;
1279 init_Array(&glyphIndices, sizeof(uint32_t)); 1324 init_Array(&glyphIndices, sizeof(uint32_t));
1280 iAttributedText attrText; 1325 iAttributedText attrText;
1281 init_AttributedText(&attrText, text, 0, d, (iColor){}, 0, 0); 1326 init_AttributedText(&attrText, text, 0, d, none_ColorId, 0, d, none_ColorId, 0);
1282 /* We use AttributedText here so the font lookup matches the behavior during text drawing -- 1327 /* We use AttributedText here so the font lookup matches the behavior during text drawing --
1283 glyphs may be selected from a font that's different than `d`. */ 1328 glyphs may be selected from a font that's different than `d`. */
1284 const iChar *logicalText = constData_Array(&attrText.logical); 1329 const iChar *logicalText = constData_Array(&attrText.logical);
@@ -1333,17 +1378,17 @@ struct Impl_RunArgs {
1333 /* TODO: Cleanup using TextMetrics 1378 /* TODO: Cleanup using TextMetrics
1334 Use TextMetrics output pointer instead of return value & cursorAdvance_out. */ 1379 Use TextMetrics output pointer instead of return value & cursorAdvance_out. */
1335 iInt2 * cursorAdvance_out; 1380 iInt2 * cursorAdvance_out;
1336// const char ** continueFrom_out;
1337 int * runAdvance_out; 1381 int * runAdvance_out;
1338}; 1382};
1339 1383
1340static iBool notify_WrapText_(iWrapText *d, const char *ending, int origin, int advance, iBool isBaseRTL) { 1384static iBool notify_WrapText_(iWrapText *d, const char *ending, iTextAttrib attrib,
1385 int origin, int advance) {
1341 if (d && d->wrapFunc && d->wrapRange_.start) { 1386 if (d && d->wrapFunc && d->wrapRange_.start) {
1342 /* `wrapRange_` uses logical indices. */ 1387 /* `wrapRange_` uses logical indices. */
1343 const char *end = ending ? ending : d->wrapRange_.end; 1388 const char *end = ending ? ending : d->wrapRange_.end;
1344 iRangecc range = { d->wrapRange_.start, end }; 1389 iRangecc range = { d->wrapRange_.start, end };
1345 iAssert(range.start <= range.end); 1390 iAssert(range.start <= range.end);
1346 const iBool result = d->wrapFunc(d, range, origin, advance, isBaseRTL); 1391 const iBool result = d->wrapFunc(d, range, attrib, origin, advance);
1347 if (result) { 1392 if (result) {
1348 d->wrapRange_.start = end; 1393 d->wrapRange_.start = end;
1349 } 1394 }
@@ -1470,8 +1515,11 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1470 font is used and other attributes such as color. (HarfBuzz shaping is done 1515 font is used and other attributes such as color. (HarfBuzz shaping is done
1471 with one specific font.) */ 1516 with one specific font.) */
1472 iAttributedText attrText; 1517 iAttributedText attrText;
1473 init_AttributedText(&attrText, args->text, args->maxLen, d, get_Color(args->color), 1518 init_AttributedText(&attrText, args->text, args->maxLen, d, args->color,
1474 args->baseDir, wrap ? wrap->overrideChar : 0); 1519 args->baseDir,
1520 activeText_->baseFontId >= 0 ? font_Text_(activeText_->baseFontId) : d,
1521 activeText_->baseColorId,
1522 wrap ? wrap->overrideChar : 0);
1475 if (wrap) { 1523 if (wrap) {
1476 wrap->baseDir = attrText.isBaseRTL ? -1 : +1; 1524 wrap->baseDir = attrText.isBaseRTL ? -1 : +1;
1477 /* TODO: Duplicated args? */ 1525 /* TODO: Duplicated args? */
@@ -1522,6 +1570,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1522 iRangei wrapPosRange = { 0, textLen }; 1570 iRangei wrapPosRange = { 0, textLen };
1523 int wrapResumePos = textLen; /* logical position where next line resumes */ 1571 int wrapResumePos = textLen; /* logical position where next line resumes */
1524 size_t wrapResumeRunIndex = runCount; /* index of run where next line resumes */ 1572 size_t wrapResumeRunIndex = runCount; /* index of run where next line resumes */
1573 iTextAttrib attrib = { .colorId = args->color, .isBaseRTL = attrText.isBaseRTL };
1574 iTextAttrib wrapAttrib = attrib;
1575 iTextAttrib lastAttrib = attrib;
1525 const int layoutBound = (wrap ? wrap->maxWidth : 0); 1576 const int layoutBound = (wrap ? wrap->maxWidth : 0);
1526 iBool isFirst = iTrue; 1577 iBool isFirst = iTrue;
1527 const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); 1578 const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2());
@@ -1545,6 +1596,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1545 /* Determine ends of wrapRuns and wrapVisRange. */ 1596 /* Determine ends of wrapRuns and wrapVisRange. */
1546 for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) { 1597 for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) {
1547 const iAttributedRun *run = at_Array(&attrText.runs, runIndex); 1598 const iAttributedRun *run = at_Array(&attrText.runs, runIndex);
1599 /* Update the attributes. */
1548 if (run->flags.isLineBreak) { 1600 if (run->flags.isLineBreak) {
1549 if (checkHitChar && 1601 if (checkHitChar &&
1550 wrap->hitChar == sourcePtr_AttributedText_(&attrText, run->logical.start)) { 1602 wrap->hitChar == sourcePtr_AttributedText_(&attrText, run->logical.start)) {
@@ -1554,7 +1606,6 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1554 wrapResumePos = run->logical.end; 1606 wrapResumePos = run->logical.end;
1555 wrapRuns.end = runIndex; 1607 wrapRuns.end = runIndex;
1556 wrapResumeRunIndex = runIndex + 1; 1608 wrapResumeRunIndex = runIndex + 1;
1557 //yCursor += d->height;
1558 break; 1609 break;
1559 } 1610 }
1560 wrapResumeRunIndex = runCount; 1611 wrapResumeRunIndex = runCount;
@@ -1564,8 +1615,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1564 shape_GlyphBuffer_(buf); 1615 shape_GlyphBuffer_(buf);
1565 int safeBreakPos = -1; 1616 int safeBreakPos = -1;
1566 iChar prevCh = 0; 1617 iChar prevCh = 0;
1618 lastAttrib = run->attrib;
1567 for (unsigned int ir = 0; ir < buf->glyphCount; ir++) { 1619 for (unsigned int ir = 0; ir < buf->glyphCount; ir++) {
1568 const int i = (run->flags.isRTL ? buf->glyphCount - ir - 1 : ir); 1620 const int i = (run->attrib.isRTL ? buf->glyphCount - ir - 1 : ir);
1569 const hb_glyph_info_t *info = &buf->glyphInfo[i]; 1621 const hb_glyph_info_t *info = &buf->glyphInfo[i];
1570 const hb_codepoint_t glyphId = info->codepoint; 1622 const hb_codepoint_t glyphId = info->codepoint;
1571 const int logPos = info->cluster; 1623 const int logPos = info->cluster;
@@ -1603,10 +1655,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1603 prevCh = ch; 1655 prevCh = ch;
1604 } 1656 }
1605 else { 1657 else {
1606 //if (~glyphFlags & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) { 1658 safeBreakPos = logPos;
1607 safeBreakPos = logPos; 1659 breakAdvance = wrapAdvance;
1608 breakAdvance = wrapAdvance; 1660 wrapAttrib = run->attrib;
1609 //}
1610 } 1661 }
1611 if (isHitPointOnThisLine) { 1662 if (isHitPointOnThisLine) {
1612 if (wrap->hitPoint.x >= orig.x + wrapAdvance && 1663 if (wrap->hitPoint.x >= orig.x + wrapAdvance &&
@@ -1638,7 +1689,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1638 wrapPosRange.end = logPos; 1689 wrapPosRange.end = logPos;
1639 breakAdvance = wrapAdvance; 1690 breakAdvance = wrapAdvance;
1640 } 1691 }
1641 wrapResumePos = wrapPosRange.end; 1692 wrapResumePos = wrapPosRange.end;
1642 if (args->wrap->mode != anyCharacter_WrapTextMode) { 1693 if (args->wrap->mode != anyCharacter_WrapTextMode) {
1643 while (wrapResumePos < textLen && isSpace_Char(logicalText[wrapResumePos])) { 1694 while (wrapResumePos < textLen && isSpace_Char(logicalText[wrapResumePos])) {
1644 wrapResumePos++; /* skip space */ 1695 wrapResumePos++; /* skip space */
@@ -1688,7 +1739,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1688 for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) { 1739 for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) {
1689 const iAttributedRun *run = at_Array(&attrText.runs, runIndex); 1740 const iAttributedRun *run = at_Array(&attrText.runs, runIndex);
1690 if (!attrText.isBaseRTL) { /* left-to-right */ 1741 if (!attrText.isBaseRTL) { /* left-to-right */
1691 if (run->flags.isRTL) { 1742 if (run->attrib.isRTL) {
1692 if (oppositeInsertIndex == iInvalidPos) { 1743 if (oppositeInsertIndex == iInvalidPos) {
1693 oppositeInsertIndex = size_Array(&runOrder); 1744 oppositeInsertIndex = size_Array(&runOrder);
1694 } 1745 }
@@ -1700,7 +1751,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1700 } 1751 }
1701 } 1752 }
1702 else { /* right-to-left */ 1753 else { /* right-to-left */
1703 if (!run->flags.isRTL) { 1754 if (!run->attrib.isRTL) {
1704 if (oppositeInsertIndex == iInvalidPos) { 1755 if (oppositeInsertIndex == iInvalidPos) {
1705 oppositeInsertIndex = 0; 1756 oppositeInsertIndex = 0;
1706 } 1757 }
@@ -1739,11 +1790,12 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1739 if (wrap && wrap->wrapFunc && 1790 if (wrap && wrap->wrapFunc &&
1740 !notify_WrapText_(args->wrap, 1791 !notify_WrapText_(args->wrap,
1741 sourcePtr_AttributedText_(&attrText, wrapResumePos), 1792 sourcePtr_AttributedText_(&attrText, wrapResumePos),
1793 wrapAttrib,
1742 origin, 1794 origin,
1743 iRound(wrapAdvance), 1795 iRound(wrapAdvance))) {
1744 attrText.isBaseRTL)) {
1745 willAbortDueToWrap = iTrue; 1796 willAbortDueToWrap = iTrue;
1746 } 1797 }
1798 wrapAttrib = lastAttrib;
1747 xCursor = origin; 1799 xCursor = origin;
1748 /* We have determined a possible wrap position and alignment for the work runs, 1800 /* We have determined a possible wrap position and alignment for the work runs,
1749 so now we can process the glyphs. */ 1801 so now we can process the glyphs. */
@@ -1796,8 +1848,8 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1796 orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y, 1848 orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y,
1797 glyph->rect[hoff].size.x, 1849 glyph->rect[hoff].size.x,
1798 glyph->rect[hoff].size.y }; 1850 glyph->rect[hoff].size.y };
1799 if (run->font->height < d->height) { 1851 if (run->font->height < attrText.baseFont->height) {
1800 dst.y += d->baseline - run->font->baseline; 1852 dst.y += attrText.baseFont->baseline - run->font->baseline;
1801 } 1853 }
1802 if (mode & visualFlag_RunMode) { 1854 if (mode & visualFlag_RunMode) {
1803 if (isEmpty_Rect(bounds)) { 1855 if (isEmpty_Rect(bounds)) {
@@ -1819,7 +1871,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1819 iAssert(isRasterized_Glyph_(glyph, hoff)); 1871 iAssert(isRasterized_Glyph_(glyph, hoff));
1820 } 1872 }
1821 if (~mode & permanentColorFlag_RunMode) { 1873 if (~mode & permanentColorFlag_RunMode) {
1822 const iColor clr = run->fgColor; 1874 const iColor clr = fgColor_AttributedRun_(run);
1823 SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b); 1875 SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b);
1824 if (args->mode & fillBackground_RunMode) { 1876 if (args->mode & fillBackground_RunMode) {
1825 SDL_SetRenderDrawColor(activeText_->render, clr.r, clr.g, clr.b, 0); 1877 SDL_SetRenderDrawColor(activeText_->render, clr.r, clr.g, clr.b, 0);
@@ -1939,9 +1991,9 @@ static int runFlagsFromId_(enum iFontId fontId) {
1939 return runFlags; 1991 return runFlags;
1940} 1992}
1941 1993
1942static iBool cbAdvanceOneLine_(iWrapText *d, iRangecc range, int origin, int advance, 1994static iBool cbAdvanceOneLine_(iWrapText *d, iRangecc range, iTextAttrib attrib, int origin,
1943 iBool isBaseRTL) { 1995 int advance) {
1944 iUnused(origin, advance, isBaseRTL); 1996 iUnused(attrib, origin, advance);
1945 *((const char **) d->context) = range.end; 1997 *((const char **) d->context) = range.end;
1946 return iFalse; /* just one line */ 1998 return iFalse; /* just one line */
1947} 1999}
diff --git a/src/ui/text.h b/src/ui/text.h
index ac59e7c8..51f7754d 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -120,6 +120,9 @@ void resetFonts_Text (iText *);
120int lineHeight_Text (int fontId); 120int lineHeight_Text (int fontId);
121float emRatio_Text (int fontId); /* em advance to line height ratio */ 121float emRatio_Text (int fontId); /* em advance to line height ratio */
122iRect visualBounds_Text (int fontId, iRangecc text); 122iRect visualBounds_Text (int fontId, iRangecc text);
123int fontWithSize_Text (int fontId, enum iFontSize sizeId);
124int fontWithStyle_Text (int fontId, enum iFontStyle styleId);
125int fontWithFamily_Text (int fontId, enum iFontId familyId);
123 126
124iDeclareType(TextMetrics) 127iDeclareType(TextMetrics)
125 128
@@ -149,9 +152,10 @@ enum iAlignment {
149 right_Alignment, 152 right_Alignment,
150}; 153};
151 154
152void setOpacity_Text (float opacity); 155void setOpacity_Text (float opacity);
156void setBaseAttributes_Text (int fontId, int colorId); /* current "normal" text attributes */
153 157
154void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */ 158void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */
155 159
156void draw_Text (int fontId, iInt2 pos, int color, const char *text, ...); 160void draw_Text (int fontId, iInt2 pos, int color, const char *text, ...);
157void drawAlign_Text (int fontId, iInt2 pos, int color, enum iAlignment align, const char *text, ...); 161void drawAlign_Text (int fontId, iInt2 pos, int color, enum iAlignment align, const char *text, ...);
@@ -173,12 +177,28 @@ enum iWrapTextMode {
173 word_WrapTextMode, 177 word_WrapTextMode,
174}; 178};
175 179
180iDeclareType(TextAttrib)
181
182/* Initial attributes at the start of a text string. These may be modified by control
183 sequences inside a text run. */
184struct Impl_TextAttrib {
185 int16_t colorId;
186 struct {
187 uint16_t bold : 1;
188 uint16_t italic : 1;
189 uint16_t monospace : 1;
190 uint16_t isBaseRTL : 1;
191 uint16_t isRTL : 1;
192 };
193};
194
176struct Impl_WrapText { 195struct Impl_WrapText {
177 /* arguments */ 196 /* arguments */
178 iRangecc text; 197 iRangecc text;
179 int maxWidth; 198 int maxWidth;
180 enum iWrapTextMode mode; 199 enum iWrapTextMode mode;
181 iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, int origin, int advance, iBool isBaseRTL); 200 iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, iTextAttrib attrib, int origin,
201 int advance);
182 void * context; 202 void * context;
183 iChar overrideChar; /* use this for all characters instead of the real ones */ 203 iChar overrideChar; /* use this for all characters instead of the real ones */
184 int baseDir; /* set to +1 for LTR, -1 for RTL */ 204 int baseDir; /* set to +1 for LTR, -1 for RTL */