Lagrange [work/v1.8]

Text attributes that change inside a run

=> 6b931c95725eef2ebb7e831c4017d3d67b33294f

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) {
         1.333,
         1.666,
         2.000,
-        0.568,
+        0.650, //0.568,
         0.710, /* calibration: fits the Lagrange title screen with Normal line width */
     };
     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)
 
 /*----------------------------------------------------------------------------------------------*/
 
+iDeclareType(GmTheme)
+
+struct Impl_GmTheme {
+    int colors[max_GmLineType];
+    int fonts[max_GmLineType];
+};
+
+/*----------------------------------------------------------------------------------------------*/
+
 struct Impl_GmDocument {
     iObject object;
     enum iSourceFormat format;
@@ -91,6 +100,7 @@ struct Impl_GmDocument {
     iString   title; /* the first top-level title */
     iArray    headings;
     iArray    preMeta; /* metadata about preformatted blocks */
+    iGmTheme  theme;
     uint32_t  themeSeed;
     iChar     siteIcon;
     iMedia *  media;
@@ -101,6 +111,51 @@ struct Impl_GmDocument {
 
 iDefineObjectConstruction(GmDocument)
 
+static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {
+    const iRangecc scheme = urlScheme_String(&d->url);
+    if (equalCase_Rangecc(scheme, "gemini")) {
+        return prefs_App()->monospaceGemini;
+    }
+    if (equalCase_Rangecc(scheme, "gopher") ||
+        equalCase_Rangecc(scheme, "finger")) {
+        return prefs_App()->monospaceGopher;
+    }
+    return iFalse;
+}
+
+static void initTheme_GmDocument_(iGmDocument *d) {
+    static const int defaultColors[max_GmLineType] = {
+        tmParagraph_ColorId,
+        tmParagraph_ColorId, /* bullet */
+        tmPreformatted_ColorId,
+        tmQuote_ColorId,
+        tmHeading1_ColorId,
+        tmHeading2_ColorId,
+        tmHeading3_ColorId,
+        tmLinkText_ColorId,
+    };
+    iGmTheme *theme = &d->theme;
+    memcpy(theme->colors, defaultColors, sizeof(theme->colors));
+    const iPrefs *prefs    = prefs_App();
+    const iBool   isMono   = isForcedMonospace_GmDocument_(d);
+    const iBool   isDarkBg = isDark_GmDocumentTheme(
+        isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
+    const enum iFontId headingFont = isMono ? documentMonospace_FontId : documentHeading_FontId;
+    const enum iFontId bodyFont    = isMono ? documentMonospace_FontId : documentBody_FontId;
+    theme->fonts[text_GmLineType] = FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize);
+    theme->fonts[bullet_GmLineType] = FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize);
+    theme->fonts[preformatted_GmLineType] = preformatted_FontId;
+    theme->fonts[quote_GmLineType] = isMono ? monospaceParagraph_FontId : quote_FontId;
+    theme->fonts[heading1_GmLineType] = FONT_ID(headingFont, bold_FontStyle, contentHuge_FontSize);
+    theme->fonts[heading2_GmLineType] = FONT_ID(headingFont, bold_FontStyle, contentLarge_FontSize);
+    theme->fonts[heading3_GmLineType] = FONT_ID(headingFont, regular_FontStyle, contentBig_FontSize);
+    theme->fonts[link_GmLineType] = FONT_ID(
+        bodyFont,
+        ((isDarkBg && prefs->boldLinkDark) || (!isDarkBg && prefs->boldLinkLight)) ? semiBold_FontStyle
+                                                                                   : regular_FontStyle,
+        contentRegular_FontSize);
+}
+
 static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) {
     if (d->format == plainText_SourceFormat) {
         return text_GmLineType;
@@ -318,18 +373,6 @@ static iBool isGopher_GmDocument_(const iGmDocument *d) {
             equalCase_Rangecc(scheme, "finger"));
 }
 
-static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {
-    const iRangecc scheme = urlScheme_String(&d->url);
-    if (equalCase_Rangecc(scheme, "gemini")) {
-        return prefs_App()->monospaceGemini;
-    }
-    if (equalCase_Rangecc(scheme, "gopher") ||
-        equalCase_Rangecc(scheme, "finger")) {
-        return prefs_App()->monospaceGopher;
-    }
-    return iFalse;
-}
-
 static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo,
                                               uint16_t linkId) {
     iGmLink *link = at_PtrArray(&d->links, linkId - 1);
@@ -402,7 +445,8 @@ struct Impl_RunTypesetter {
     int    rightMargin;
     iBool  isWordWrapped;
     iBool  isPreformat;
-    const int *fonts;
+    int    baseFont;
+    int    baseColor;
 };
     
 static void init_RunTypesetter_(iRunTypesetter *d) {
@@ -425,39 +469,47 @@ static void commit_RunTypesetter_(iRunTypesetter *d, iGmDocument *doc) {
 
 static const int maxLedeLines_ = 10;
 
-static const int colors[max_GmLineType] = {
-    tmParagraph_ColorId,
-    tmParagraph_ColorId,
-    tmPreformatted_ColorId,
-    tmQuote_ColorId,
-    tmHeading1_ColorId,
-    tmHeading2_ColorId,
-    tmHeading3_ColorId,
-    tmLinkText_ColorId,
-};
+static int applyAttributes_RunTypesetter_(iRunTypesetter *d, iTextAttrib attrib) {
+    /* WARNING: This is duplicated in run_Font_(). Make sure they behave identically. */
+    if (attrib.bold) {
+        d->run.font = fontWithStyle_Text(d->baseFont, bold_FontStyle);
+        d->run.color = tmFirstParagraph_ColorId;
+    }
+    else if (attrib.italic) {
+        d->run.font = fontWithStyle_Text(d->baseFont, italic_FontStyle);
+    }
+    else if (attrib.monospace) {
+        d->run.font = fontWithFamily_Text(d->baseFont, monospace_FontId);
+        d->run.color = tmPreformatted_ColorId;
+    }
+    else {
+        d->run.font  = d->baseFont;
+        d->run.color = d->baseColor;
+    }
+}
 
-static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, int origin,
-                                           int advance, iBool isBaseRTL) {
+static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, iTextAttrib attrib,
+                                           int origin, int advance) {
     iAssert(wrapRange.start <= wrapRange.end);
     trimEnd_Rangecc(&wrapRange);
 //    printf("typeset: {%s}\n", cstr_Rangecc(wrapRange));
     iRunTypesetter *d = wrap->context;
-    const int fontId = d->run.font;
     d->run.text = wrapRange;
+    applyAttributes_RunTypesetter_(d, attrib);
     if (~d->run.flags & startOfLine_GmRunFlag && d->lineHeightReduction > 0.0f) {
-        d->pos.y -= d->lineHeightReduction * lineHeight_Text(fontId);
+        d->pos.y -= d->lineHeightReduction * lineHeight_Text(d->baseFont);
     }
     d->run.bounds.pos = addX_I2(d->pos, origin + d->indent);
-    const iInt2 dims = init_I2(advance, lineHeight_Text(fontId));
+    const iInt2 dims = init_I2(advance, lineHeight_Text(d->baseFont));
     iChangeFlags(d->run.flags, wide_GmRunFlag, (d->isPreformat && dims.x > d->layoutWidth));
     d->run.bounds.size.x    = iMax(wrap->maxWidth, dims.x) - origin; /* Extends to the right edge for selection. */
     d->run.bounds.size.y    = dims.y;
     d->run.visBounds        = d->run.bounds;
     d->run.visBounds.size.x = dims.x;
-    d->run.isRTL = isBaseRTL;
+    d->run.isRTL            = attrib.isBaseRTL;
     pushBack_Array(&d->layout, &d->run);
     d->run.flags &= ~startOfLine_GmRunFlag;
-    d->pos.y += lineHeight_Text(fontId) * prefs_App()->lineSpacing;
+    d->pos.y += lineHeight_Text(d->baseFont) * prefs_App()->lineSpacing;
     return iTrue; /* continue to next wrapped line */
 }
 
@@ -469,25 +521,10 @@ static void doLayout_GmDocument_(iGmDocument *d) {
     const iBool   isVeryNarrow      = d->size.x <= 70 * gap_Text;
     const iBool   isExtremelyNarrow = d->size.x <= 60 * gap_Text;
     const iBool   isFullWidthImages = (d->outsideMargin < 5 * gap_UI);
-    const iBool   isDarkBg          = isDark_GmDocumentTheme(
-        isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
+//    const iBool   isDarkBg          = isDark_GmDocumentTheme(
+//        isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
+    initTheme_GmDocument_(d);
     /* TODO: Collect these parameters into a GmTheme. */
-    const enum iFontId headingFont = isMono ? documentMonospace_FontId : documentHeading_FontId;
-    const enum iFontId bodyFont    = isMono ? documentMonospace_FontId : documentBody_FontId;
-    const int fonts[max_GmLineType] = {
-        FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize), /* text */
-        FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize), /* bullet */
-        preformatted_FontId,                                           /* pre */
-        isMono ? monospaceParagraph_FontId : quote_FontId,             /* quote */
-        FONT_ID(headingFont, bold_FontStyle, contentHuge_FontSize),    /* h1 */
-        FONT_ID(headingFont, bold_FontStyle, contentLarge_FontSize),   /* h2 */
-        FONT_ID(headingFont, regular_FontStyle, contentBig_FontSize),  /* h3 */
-        FONT_ID(bodyFont,
-                ((isDarkBg && prefs->boldLinkDark) || (!isDarkBg && prefs->boldLinkLight))
-                             ? semiBold_FontStyle
-                             : regular_FontStyle,
-                contentRegular_FontSize) /* link */
-    };
     float indents[max_GmLineType] = { 5, 10, 5, isNarrow ? 5 : 10, 0, 0, 0, 5 };
     if (isExtremelyNarrow) {
         /* Further reduce the margins. */
@@ -598,7 +635,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
                 }
             }
             trimLine_Rangecc(&line, type, isNormalized);
-            run.font = fonts[type];
+            run.font = d->theme.fonts[type];
             /* Remember headings for the document outline. */
             if (type == heading1_GmLineType || type == heading2_GmLineType || type == heading3_GmLineType) {
                 pushBack_Array(
@@ -618,7 +655,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
                 addSiteBanner = iFalse; /* overrides the banner */
                 continue;
             }
-            run.preId = preId;
+            run.mediaType = max_MediaType; /* preformatted block */
+            run.mediaId = preId;
             run.font = (d->format == plainText_SourceFormat ? plainText_FontId : preFont);
             indent = indents[type];
         }
@@ -650,7 +688,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
                 run.visBounds.size = init_I2(gap_Text, lineHeight_Text(run.font));
                 run.bounds         = zero_Rect(); /* just visual */
                 run.text           = iNullRange;
-                run.flags     = quoteBorder_GmRunFlag | decoration_GmRunFlag;
+                run.flags          = quoteBorder_GmRunFlag | decoration_GmRunFlag;
                 pushBack_Array(&d->layout, &run);
             }
             pos.y += lineHeight_Text(run.font) * prefs->lineSpacing;
@@ -708,7 +746,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
                                                    altText.text).bounds.size;
                 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x,
                                                                size.y + 2 * margin.y);
-                altText.preId = preId;
+                altText.mediaType = max_MediaType; /* preformatted */
+                altText.mediaId = preId;
                 pushBack_Array(&d->layout, &altText);
                 pos.y += height_Rect(altText.bounds);
                 contentLine = meta->bounds; /* Skip the whole thing. */
@@ -723,7 +762,6 @@ static void doLayout_GmDocument_(iGmDocument *d) {
             setRange_String(&d->title, line);
         }
         /* List bullet. */
-        run.color = colors[type];
         if (type == bullet_GmLineType) {
             /* TODO: Literata bullet is broken? */
             iGmRun bulRun = run;
@@ -795,18 +833,17 @@ static void doLayout_GmDocument_(iGmDocument *d) {
             icon.flags |= decoration_GmRunFlag;
             pushBack_Array(&d->layout, &icon);
         }
-        run.color = colors[type];
+        run.lineType = type;
+        run.color    = d->theme.colors[type];
         if (d->format == plainText_SourceFormat) {
-            run.color = colors[text_GmLineType];
+            run.color = d->theme.colors[text_GmLineType];
         }
         /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */
 //        int bigCount = 0;
-        iBool isLedeParagraph = iFalse;
         if (type == text_GmLineType && isFirstText) {
             if (!isMono) run.font = firstParagraph_FontId;
-            run.color = tmFirstParagraph_ColorId;
-//            bigCount = 15; /* max lines -- what if the whole document is one paragraph? */
-            isLedeParagraph = iTrue;
+            run.color   = tmFirstParagraph_ColorId;
+            run.isLede  = iTrue;
             isFirstText = iFalse;
         }
         else if (type != heading1_GmLineType) {
@@ -826,7 +863,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
             init_RunTypesetter_(&rts);
             rts.run           = run;
             rts.pos           = pos;
-            rts.fonts         = fonts;
+            //rts.fonts         = fonts;
             rts.isWordWrapped = (d->format == plainText_SourceFormat ? prefs->plainTextWrap
                                                                      : !isPreformat);
             rts.isPreformat   = isPreformat;
@@ -859,6 +896,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
             }
             for (;;) { /* need to retry if the font needs changing */
                 rts.run.flags |= startOfLine_GmRunFlag;
+                rts.baseFont  = rts.run.font;
+                rts.baseColor = rts.run.color;
                 iWrapText wrapText = { .text     = line,
                                        .maxWidth = rts.isWordWrapped
                                                        ? d->size.x - run.bounds.pos.x -
@@ -868,7 +907,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
                                        .wrapFunc = typesetOneLine_RunTypesetter_,
                                        .context  = &rts };
                 measure_WrapText(&wrapText, rts.run.font);
-                if (!isLedeParagraph || size_Array(&rts.layout) <= maxLedeLines_) {
+                if (!rts.run.isLede || size_Array(&rts.layout) <= maxLedeLines_) {
                     if (wrapText.baseDir < 0) {
                         /* Right-aligned paragraphs need margins and decorations to be flipped. */
                         iForEach(Array, pr, &rts.layout) {
@@ -891,11 +930,12 @@ static void doLayout_GmDocument_(iGmDocument *d) {
                     commit_RunTypesetter_(&rts, d);
                     break;
                 }
+                /* Try again... */
                 clear_RunTypesetter_(&rts);
                 rts.pos         = pos;
-                rts.run.font    = rts.fonts[text_GmLineType];
-                rts.run.color   = colors[text_GmLineType];
-                isLedeParagraph = iFalse;
+                rts.run.font    = rts.baseFont  = d->theme.fonts[text_GmLineType];
+                rts.run.color   = rts.baseColor = d->theme.colors[text_GmLineType];
+                rts.run.isLede  = iFalse;
             }
             pos = rts.pos;
             deinit_RunTypesetter_(&rts);
@@ -996,7 +1036,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
         /* TODO: Store the dimensions and ranges for later access. */
         iForEach(Array, i, &d->layout) {
             iGmRun *run = i.value;
-            if (run->preId && run->flags & wide_GmRunFlag) {
+            if (preId_GmRun(run) && run->flags & wide_GmRunFlag) {
                 iGmRunRange block = findPreformattedRange_GmDocument(d, run);
                 for (const iGmRun *j = block.start; j != block.end; j++) {
                     iConstCast(iGmRun *, j)->flags |= wide_GmRunFlag;
@@ -1800,11 +1840,11 @@ static void flushPendingLinks_(iArray *links, const iString *source, iString *ou
 static void convertMarkdownToGemtext_GmDocument_(iGmDocument *d) {
     iAssert(d->format == markdown_SourceFormat);
     /* Get rid of indented preformats. */ {
-        iArray *pendingLinks = collectNew_Array(sizeof(iPendingLink));
+        iArray        *pendingLinks     = collectNew_Array(sizeof(iPendingLink));
         const iRegExp *imageLinkPattern = iClob(new_RegExp("\n?!\\[(.+)\\]\\(([^)]+)\\)\n?", 0));
-        const iRegExp *linkPattern = iClob(new_RegExp("\\[(.+?)\\]\\(([^)]+)\\)", 0));
+        const iRegExp *linkPattern      = iClob(new_RegExp("\\[(.+?)\\]\\(([^)]+)\\)", 0));
         const iRegExp *namedLinkPattern = iClob(new_RegExp("\\[(.+?)\\]\\[(.+?)\\]", 0));
-        const iRegExp *namePattern = iClob(new_RegExp("\\s*\\[(.+?)\\]\\s*:\\s*([^\n]+)", 0));
+        const iRegExp *namePattern      = iClob(new_RegExp("\\s*\\[(.+?)\\]\\s*:\\s*([^\n]+)", 0));
         iString result;
         init_String(&result);
         iRangecc line = iNullRange;
@@ -1865,7 +1905,7 @@ static void convertMarkdownToGemtext_GmDocument_(iGmDocument *d) {
                 replaceRegExp_String(&ln, imageLinkPattern, "\n=> \\2 \\1\n", NULL, NULL);
                 replaceRegExp_String(&ln, namedLinkPattern, "\\1", addPendingNamedLink_, pendingLinks);
                 replaceRegExp_String(&ln, linkPattern, "\\1", addPendingLink_, pendingLinks);
-                replaceRegExp_String(&ln, iClob(new_RegExp("(?preId);
+    iAssert(preId_GmRun(run));
     iGmRunRange range = { run, run };
     /* Find the beginning. */
     while (range.start > (const iGmRun *) constData_Array(&d->layout)) {
         const iGmRun *prev = range.start - 1;
-        if (prev->preId != run->preId) break;
+        if (preId_GmRun(prev) != preId_GmRun(run)) break;
         range.start = prev;
     }
     /* Find the ending. */
     while (range.end < (const iGmRun *) constEnd_Array(&d->layout)) {
-        if (range.end->preId != run->preId) break;
+        if (preId_GmRun(range.end) != preId_GmRun(run)) break;
         range.end++;
     }
     return range;
@@ -2240,6 +2280,22 @@ iChar siteIcon_GmDocument(const iGmDocument *d) {
     return d->siteIcon;
 }
 
+void runBaseAttributes_GmDocument(const iGmDocument *d, const iGmRun *run, int *fontId_out,
+                                  int *colorId_out) {
+    /* Font and color according to the line type. These are needed because each GmRun is
+       a segment of a paragraph, and if the font or color changes inside the run, each wrapped
+       segment needs to know both the current font/color and ALSO the base font/color, so
+       the default attributes can be restored. */
+    if (run->isLede) {
+        *fontId_out  = firstParagraph_FontId;
+        *colorId_out = tmFirstParagraph_ColorId;
+    }
+    else {
+        *fontId_out  = fontWithSize_Text(d->theme.fonts[run->lineType], run->font % max_FontSize); /* retain size */
+        *colorId_out = d->theme.colors[run->lineType];
+    }
+}
+
 iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) {
     if (pos.y < top_Rect(d->bounds)) {
         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 {
         uint32_t color     : 7; /* see max_ColorId */
 
         uint32_t font      : 10;
-        uint32_t mediaType : 3;
-        uint32_t mediaId   : 9; /* zero if not an image */
-        uint32_t preId     : 10; /* preformatted block ID (sequential); merge with mediaId? */
+        uint32_t mediaType : 3; /* note: max_MediaType means preformatted block */
+        uint32_t lineType  : 3;
+        uint32_t mediaId   : 15; /* zero if not an image */
+        uint32_t isLede    : 1;
     };
 };
 
-iLocalDef iMediaId mediaId_GmRun(const iGmRun *d) {
-    return (iMediaId){ .type = d->mediaType, .id = d->mediaId };
-}
-
 iDeclareType(GmRunRange)
 
 struct Impl_GmRunRange {
@@ -157,7 +154,20 @@ struct Impl_GmRunRange {
     const iGmRun *end;
 };
 
-iRangecc    findLoc_GmRun   (const iGmRun *, iInt2 pos);
+iLocalDef iBool isMedia_GmRun(const iGmRun *d) {
+    return d->mediaType > 0 && d->mediaType < max_MediaType;
+}
+iLocalDef iMediaId mediaId_GmRun(const iGmRun *d) {
+    if (d->mediaType < max_MediaType) {
+        return (iMediaId){ .type = d->mediaType, .id = d->mediaId };
+    }
+    return iInvalidMediaId;
+}
+iLocalDef uint32_t preId_GmRun(const iGmRun *d) {
+    return d->mediaType == max_MediaType ? d->mediaId : 0;
+}
+
+iRangecc    findLoc_GmRun           (const iGmRun *, iInt2 pos);
 
 iDeclareClass(GmDocument)
 iDeclareObjectConstruction(GmDocument)
@@ -215,6 +225,9 @@ iRangecc        findText_GmDocument                 (const iGmDocument *, const
 iRangecc        findTextBefore_GmDocument           (const iGmDocument *, const iString *text, const char *before);
 iGmRunRange     findPreformattedRange_GmDocument    (const iGmDocument *, const iGmRun *run);
 
+void            runBaseAttributes_GmDocument        (const iGmDocument *, const iGmRun *run,
+                                                     int *fontId_out, int *colorId_out);
+
 enum iGmLinkPart {
     icon_GmLinkPart,
     text_GmLinkPart,
@@ -241,3 +254,4 @@ const iGmPreMeta *preMeta_GmDocument    (const iGmDocument *, uint16_t preId);
 iInt2           preRunMargin_GmDocument (const iGmDocument *, uint16_t preId);
 iBool           preIsFolded_GmDocument  (const iGmDocument *, uint16_t preId);
 iBool           preHasAltText_GmDocument(const iGmDocument *, uint16_t preId);
+
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) {
         }
         d->visibleRuns.end = run;
     }
-    if (run->preId) {
+    if (preId_GmRun(run)) {
         pushBack_PtrArray(&d->visiblePre, run);
         if (run->flags & wide_GmRunFlag) {
             pushBack_PtrArray(&d->visibleWideRuns, run);
@@ -635,14 +635,14 @@ static void invalidateVisibleLinks_DocumentWidget_(iDocumentWidget *d) {
 }
 
 static int runOffset_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) {
-    if (run->preId && run->flags & wide_GmRunFlag) {
-        if (d->animWideRunId == run->preId) {
+    if (preId_GmRun(run) && run->flags & wide_GmRunFlag) {
+        if (d->animWideRunId == preId_GmRun(run)) {
             return -value_Anim(&d->animWideRunOffset);
         }
         const size_t numOffsets = size_Array(&d->wideRunOffsets);
         const int *offsets = constData_Array(&d->wideRunOffsets);
-        if (run->preId <= numOffsets) {
-            return -offsets[run->preId - 1];
+        if (preId_GmRun(run) <= numOffsets) {
+            return -offsets[preId_GmRun(run) - 1];
         }
     }
     return 0;
@@ -731,7 +731,7 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
         }
     }
     else if (d->hoverPre &&
-             preHasAltText_GmDocument(d->doc, d->hoverPre->preId) &&
+             preHasAltText_GmDocument(d->doc, preId_GmRun(d->hoverPre)) &&
              ~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) {
         setValueSpeed_Anim(&d->altTextOpacity, 1.0f, 1.5f);
         if (!isFinished_Anim(&d->altTextOpacity)) {
@@ -1812,10 +1812,10 @@ static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos,
                 maxWidth = iMax(maxWidth, width_Rect(r->visBounds));
             }
             const int maxOffset = maxWidth - documentWidth_DocumentWidget_(d) + d->pageMargin * gap_UI;
-            if (size_Array(&d->wideRunOffsets) <= run->preId) {
-                resize_Array(&d->wideRunOffsets, run->preId + 1);
+            if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) {
+                resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1);
             }
-            int *offset = at_Array(&d->wideRunOffsets, run->preId - 1);
+            int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1);
             const int oldOffset = *offset;
             *offset = iClamp(*offset + delta, 0, maxOffset);
             /* Make sure the whole block gets redraw. */
@@ -1828,8 +1828,8 @@ static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos,
                 d->foundMark  = iNullRange;
             }
             if (duration) {
-                if (d->animWideRunId != run->preId || isFinished_Anim(&d->animWideRunOffset)) {
-                    d->animWideRunId = run->preId;
+                if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) {
+                    d->animWideRunId = preId_GmRun(run);
                     init_Anim(&d->animWideRunOffset, oldOffset);
                 }
                 setValueEased_Anim(&d->animWideRunOffset, *offset, duration);
@@ -3814,7 +3814,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
             }
             /* Fold/unfold a preformatted block. */
             if (~d->flags & selecting_DocumentWidgetFlag && d->hoverPre &&
-                preIsFolded_GmDocument(d->doc, d->hoverPre->preId)) {
+                preIsFolded_GmDocument(d->doc, preId_GmRun(d->hoverPre))) {
                 return iTrue;
             }
             /* Begin selecting a range of text. */
@@ -3925,7 +3925,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
                     }
                 }
                 if (d->hoverPre) {
-                    togglePreFold_DocumentWidget_(d, d->hoverPre->preId);
+                    togglePreFold_DocumentWidget_(d, preId_GmRun(d->hoverPre));
                     return iTrue;
                 }
                 if (d->hoverLink) {
@@ -4124,7 +4124,7 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol
 
 static void drawMark_DrawContext_(void *context, const iGmRun *run) {
     iDrawContext *d = context;
-    if (run->mediaType == none_MediaType) {
+    if (!isMedia_GmRun(run)) {
         fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark);
         fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark);
     }
@@ -4249,7 +4249,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
         }
         return;
     }
-    else if (run->mediaType) {
+    else if (isMedia_GmRun(run)) {
         /* Media UIs are drawn afterwards as a dynamic overlay. */
         return;
     }
@@ -4317,7 +4317,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
         }
     }
     if (run->flags & altText_GmRunFlag) {
-        const iInt2 margin = preRunMargin_GmDocument(doc, run->preId);
+        const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run));
         fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId);
         drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmQuoteIcon_ColorId);
         drawWrapRange_Text(run->font,
@@ -4368,11 +4368,17 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
                             height_Rect(run->visBounds),
                             tmQuoteIcon_ColorId);
         }
+        /* Base attributes. */ {
+            int f, c;
+            runBaseAttributes_GmDocument(doc, run, &f, &c);
+            setBaseAttributes_Text(f, c);
+        }
         drawBoundRange_Text(run->font,
                             visPos,
                             (run->isRTL ? -1 : 1) * width_Rect(run->visBounds),
                             fg,
                             run->text);
+        setBaseAttributes_Text(-1, -1);
     runDrawn:;
     }
     /* Presentation of links. */
@@ -4944,7 +4950,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
     /* Alt text. */
     const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5;
     if (d->hoverAltPre && altTextOpacity > 0) {
-        const iGmPreMeta *meta = preMeta_GmDocument(d->doc, d->hoverAltPre->preId);
+        const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre));
         if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag &&
             !isEmpty_Range(&meta->altText)) {
             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 {
     iRect               lastMarkRect;
 };
 
-static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, int origin, int advance,
-                               iBool isBaseRTL) {
-    iUnused(isBaseRTL);
+static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, iTextAttrib attrib,
+                               int origin, int advance) {
     iMarkPainter *mp = wrapText->context;
     const iRanges mark = mp->mark;
     if (isEmpty_Range(&mark)) {
         return iTrue; /* nothing marked */
     }
+    int fontId = mp->d->font;
+    /* TODO: Apply attrib on the font */
     const char *cstr = cstr_String(&mp->line->text);
     const iRanges lineRange = {
         wrappedText.start - cstr + mp->line->range.start,
         wrappedText.end   - cstr + mp->line->range.start
     };
+    const int lineHeight = lineHeight_Text(mp->d->font);
     if (mark.end <= lineRange.start || mark.start >= lineRange.end) {
-        mp->pos.y += lineHeight_Text(mp->d->font);
+        mp->pos.y += lineHeight;
         return iTrue; /* outside of mark */
     }
-    iRect rect = { addX_I2(mp->pos, origin), init_I2(advance, lineHeight_Text(mp->d->font)) };
+    iRect rect = { addX_I2(mp->pos, origin), init_I2(advance, lineHeight) };
     if (mark.end < lineRange.end) {
         /* Calculate where the mark ends. */
         const iRangecc markedPrefix = {
             wrappedText.start,
             wrappedText.start + mark.end - lineRange.start
         };
-        rect.size.x = measureRange_Text(mp->d->font, markedPrefix).advance.x;
+        rect.size.x = measureRange_Text(fontId, markedPrefix).advance.x;
     }
     if (mark.start > lineRange.start) {
         /* Calculate where the mark starts. */
@@ -2277,10 +2279,10 @@ static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, int or
             wrappedText.start,
             wrappedText.start + mark.start - lineRange.start
         };
-        adjustEdges_Rect(&rect, 0, 0, 0, measureRange_Text(mp->d->font, unmarkedPrefix).advance.x);
+        adjustEdges_Rect(&rect, 0, 0, 0, measureRange_Text(fontId, unmarkedPrefix).advance.x);
     }
     rect.size.x = iMax(gap_UI / 3, rect.size.x);
-    mp->pos.y += lineHeight_Text(mp->d->font);
+    mp->pos.y += lineHeight;
     fillRect_Paint(mp->paint, rect, uiMarked_ColorId | opaque_ColorId);
     if (deviceType_App() != desktop_AppDeviceType) {
         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 {
     SDL_Palette *  grayscale;
     SDL_Palette *  blackAndWhite; /* unsmoothed glyph palette */
     iRegExp *      ansiEscape;
+    int            baseFontId; /* base attributes (for restoring via escapes) */
+    int            baseColorId;
 };
 
 iDefineTypeConstructionArgs(Text, (SDL_Renderer *render), render)
@@ -537,10 +539,10 @@ void init_Text(iText *d, SDL_Renderer *render) {
     iText *oldActive = activeText_;
     activeText_ = d;
     init_Array(&d->fonts, sizeof(iFont));
-//    d->contentFont     = nunito_TextFont;
-//    d->headingFont     = nunito_TextFont;
     d->contentFontSize = contentScale_Text_;
     d->ansiEscape      = new_RegExp("[[()]([0-9;AB]*)m", 0);
+    d->baseFontId      = -1;
+    d->baseColorId     = -1;
     d->render          = render;
     /* A grayscale palette for rasterized glyphs. */ {
         SDL_Color colors[256];
@@ -581,13 +583,10 @@ void setOpacity_Text(float opacity) {
     SDL_SetTextureAlphaMod(activeText_->cache, iClamp(opacity, 0.0f, 1.0f) * 255 + 0.5f);
 }
 
-//void setFont_Text(iText *d, int fontId, const char *fontSpecId) {
-//    setupFontVariants_Text_(d, findSpec_Fonts(fontSpecId), fontId);
-//    if (d->contentFont != font) {
-//        d->contentFont = font;
-//        resetFonts_Text(d);
-//    }
-//}
+void setBaseAttributes_Text(int fontId, int colorId) {
+    activeText_->baseFontId  = fontId;
+    activeText_->baseColorId = colorId;
+}
 
 void setDocumentFontSize_Text(iText *d, float fontSizeFactor) {
     fontSizeFactor *= contentScale_Text_;
@@ -855,25 +854,45 @@ static iBool isControl_Char_(iChar c) {
 iDeclareType(AttributedRun)
 
 struct Impl_AttributedRun {
-    iRangei   logical; /* UTF-32 codepoint indices in the logical-order text */
-    iFont *   font;
-    iColor    fgColor;
+    iRangei     logical; /* UTF-32 codepoint indices in the logical-order text */
+    iTextAttrib attrib;
+    iFont      *font;
+    iColor      fgColor_; /* any RGB color; A > 0 */
     struct {
         uint8_t isLineBreak : 1;
-        uint8_t isRTL       : 1;
-        uint8_t isArabic    : 1; /* Arabic script detected */
+//        uint8_t isRTL : 1;
+        uint8_t isArabic : 1; /* Arabic script detected */
     } flags;
 };
 
+static iColor fgColor_AttributedRun_(const iAttributedRun *d) {
+    if (d->fgColor_.a) {
+        return d->fgColor_;
+    }
+    if (d->attrib.colorId == none_ColorId) {
+        return (iColor){ 255, 255, 255, 255 };
+    }
+    return get_Color(d->attrib.colorId);
+}
+
+static void setFgColor_AttributedRun_(iAttributedRun *d, int colorId) {
+    d->attrib.colorId = colorId;
+    d->fgColor_.a = 0;
+}
+
 iDeclareType(AttributedText)
 iDeclareTypeConstructionArgs(AttributedText, iRangecc text, size_t maxLen, iFont *font,
-                             iColor fgColor, int baseDir, iChar overrideChar)
+                             int colorId, int baseDir, iFont *baseFont, int baseColorId,
+                             iChar overrideChar)
 
 struct Impl_AttributedText {
     iRangecc source; /* original source text */
     size_t   maxLen;
     iFont *  font;
-    iColor   fgColor;
+    int      colorId;
+    iFont *  baseFont;
+    int      baseColorId;
+    iBool    isBaseRTL;
     iArray   runs;
     iArray   logical;         /* UTF-32 text in logical order (mixed directions; matches source) */
     iArray   visual;          /* UTF-32 text in visual order (LTR) */
@@ -881,13 +900,14 @@ struct Impl_AttributedText {
     iArray   visualToLogical;
     iArray   logicalToSourceOffset; /* map logical character to an UTF-8 offset in the source text */
     char *   bidiLevels;
-    iBool    isBaseRTL;
 };
 
 iDefineTypeConstructionArgs(AttributedText,
-                            (iRangecc text, size_t maxLen, iFont *font, iColor fgColor,
-                             int baseDir, iChar overrideChar),
-                            text, maxLen, font, fgColor, baseDir, overrideChar)
+                            (iRangecc text, size_t maxLen, iFont *font, int colorId,
+                             int baseDir, iFont *baseFont, int baseColorId,
+                             iChar overrideChar),
+                            text, maxLen, font, colorId, baseDir, baseFont, baseColorId,
+                            overrideChar)
 
 static const char *sourcePtr_AttributedText_(const iAttributedText *d, int logicalPos) {
     const int *logToSource = constData_Array(&d->logicalToSourceOffset);
@@ -916,16 +936,22 @@ static void finishRun_AttributedText_(iAttributedText *d, iAttributedRun *run, i
     run->logical.start = endAt;
 }
 
-static iFont *withStyle_Font_(const iFont *d, enum iFontStyle styleId) {
-    const int fontId = (fontId_Text_(d) / maxVariants_Fonts) * maxVariants_Fonts;
-    const int sizeId = sizeId_Text_(d);
-    return font_Text_(FONT_ID(fontId, styleId, sizeId));
+int fontWithSize_Text(int font, enum iFontSize sizeId) {
+    const int familyId = (font / maxVariants_Fonts) * maxVariants_Fonts;
+    const int styleId  = (font / max_FontSize) % max_FontStyle;
+    return FONT_ID(familyId, styleId, sizeId);
+}
+
+int fontWithStyle_Text(int font, enum iFontStyle styleId) {
+    const int familyId = (font / maxVariants_Fonts) * maxVariants_Fonts;
+    const int sizeId   = font % max_FontSize;
+    return FONT_ID(familyId, styleId, sizeId);
 }
 
-static iFont *withFontId_Font_(const iFont *d, enum iFontId fontId) {
-    const int styleId = styleId_Text_(d);
-    const int sizeId = sizeId_Text_(d);
-    return font_Text_(FONT_ID(fontId, styleId, sizeId));
+int fontWithFamily_Text(int font, enum iFontId familyId) {
+    const int styleId = (font / max_FontSize) % max_FontStyle;
+    const int sizeId  = font % max_FontSize;
+    return FONT_ID(familyId, styleId, sizeId);
 }
 
 static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iChar overrideChar) {
@@ -982,15 +1008,17 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
         pushBack_Array(&d->logicalToVisual, &(int){ length });
         pushBack_Array(&d->visualToLogical, &(int){ length });
     }
-    iAttributedRun run         = { .logical = { 0, length },
-                                   .font    = d->font,
-                                   .fgColor = d->fgColor };
-    const int *    logToSource = constData_Array(&d->logicalToSourceOffset);
+    iAttributedRun run = {
+        .logical = { 0, length },
+        .attrib  = { .colorId = d->colorId, .isBaseRTL = d->isBaseRTL },
+        .font    = d->font,
+    };
+    const int     *logToSource = constData_Array(&d->logicalToSourceOffset);
     const int *    logToVis    = constData_Array(&d->logicalToVisual);
     const iChar *  logicalText = constData_Array(&d->logical);
     iBool          isRTL       = d->isBaseRTL;
     int            numNonSpace = 0;
-    iFont *        activeFont  = d->font;
+    iFont *        attribFont  = d->font;
     for (int pos = 0; pos < length; pos++) {
         const iChar ch = logicalText[pos];
 #if defined (LAGRANGE_ENABLE_FRIBIDI)
@@ -1010,7 +1038,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
 #else
         const iBool isNeutral = iTrue;
 #endif
-        run.flags.isRTL = isRTL;
+        run.attrib.isRTL = isRTL;
         if (ch == 0x1b) { /* ANSI escape. */
             pos++;
             const char *srcPos = d->source.start + logToSource[pos];
@@ -1020,21 +1048,36 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
             if (match_RegExp(activeText_->ansiEscape, srcPos, d->source.end - srcPos, &m)) {
                 finishRun_AttributedText_(d, &run, pos - 1);
                 const iRangecc sequence = capturedRange_RegExpMatch(&m, 1);
+                /* TODO: Bold/italic attributes are assumed to be inside body text.
+                   We don't know what the current text style is supposed to be.
+                   That should be an additional attribute passed to WrapText, or a feature of
+                   WrapText that can be called both from here and in the run typesetter.
+                   The styling here is hardcoded to match `typesetOneLine_RunTypesetter_()`. */
                 if (equal_Rangecc(sequence, "1")) {
-                    activeFont = withStyle_Font_(activeFont, bold_FontStyle);
+                    run.attrib.bold = iTrue;
+                    if (d->baseColorId == tmParagraph_ColorId) {
+                        setFgColor_AttributedRun_(&run, tmFirstParagraph_ColorId);
+                    }
+                    attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), bold_FontStyle));
                 }
                 else if (equal_Rangecc(sequence, "3")) {
-                    activeFont = withStyle_Font_(activeFont, italic_FontStyle);
+                    run.attrib.italic = iTrue;
+                    attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), italic_FontStyle));
                 }
                 else if (equal_Rangecc(sequence, "4")) {
-                    activeFont = withFontId_Font_(activeFont, monospace_FontId);
+                    run.attrib.monospace = iTrue;
+                    setFgColor_AttributedRun_(&run, tmPreformatted_ColorId);
+                    attribFont = font_Text_(fontWithFamily_Text(fontId_Text_(d->baseFont), monospace_FontId));
                 }
                 else if (equal_Rangecc(sequence, "0")) {
-                    activeFont = d->font; /* restore original */
-                    run.fgColor = d->fgColor;
+                    run.attrib.bold = iFalse;
+                    run.attrib.italic = iFalse;
+                    run.attrib.monospace = iFalse;
+                    attribFont = d->baseFont;
+                    setFgColor_AttributedRun_(&run, d->baseColorId);
                 }
                 else {
-                    run.fgColor = ansiForeground_Color(sequence, tmParagraph_ColorId);
+                    run.fgColor_ = ansiForeground_Color(sequence, tmParagraph_ColorId);
                 }
                 pos += length_Rangecc(capturedRange_RegExpMatch(&m, 0));
                 iAssert(logToSource[pos] == end_RegExpMatch(&m) - d->source.start);
@@ -1056,7 +1099,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
                 colorNum = esc - asciiBase_ColorEscape;
             }
             run.logical.start = pos + 1;
-            run.fgColor = (colorNum >= 0 ? get_Color(colorNum) : d->fgColor);
+            setFgColor_AttributedRun_(&run, colorNum >= 0 ? colorNum : d->colorId);
             continue;
         }
         if (ch == '\n') {
@@ -1077,7 +1120,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
             }
             continue;
         }
-        iFont *currentFont = activeFont;
+        iFont *currentFont = attribFont;
         if (run.font->fontSpec->flags & arabic_FontSpecFlag && isPunct_Char(ch)) {
             currentFont = run.font; /* remain as Arabic for whitespace */
         }
@@ -1113,12 +1156,15 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
 #endif
 }
 
-void init_AttributedText(iAttributedText *d, iRangecc text, size_t maxLen, iFont *font, iColor fgColor,
-                         int baseDir, iChar overrideChar) {
-    d->source  = text;
-    d->maxLen  = maxLen ? maxLen : iInvalidSize;
-    d->font    = font;
-    d->fgColor = fgColor;
+void init_AttributedText(iAttributedText *d, iRangecc text, size_t maxLen, iFont *font, int colorId,
+                         int baseDir, iFont *baseFont, int baseColorId, iChar overrideChar) {
+    d->source      = text;
+    d->maxLen      = maxLen ? maxLen : iInvalidSize;
+    d->font        = font;
+    d->colorId     = colorId;
+    d->baseFont    = baseFont;
+    d->baseColorId = baseColorId;
+    d->isBaseRTL   = iFalse;
     init_Array(&d->runs, sizeof(iAttributedRun));
     init_Array(&d->logical, sizeof(iChar));
     init_Array(&d->visual, sizeof(iChar));
@@ -1126,7 +1172,6 @@ void init_AttributedText(iAttributedText *d, iRangecc text, size_t maxLen, iFont
     init_Array(&d->visualToLogical, sizeof(int));
     init_Array(&d->logicalToSourceOffset, sizeof(int));
     d->bidiLevels = NULL;
-    d->isBaseRTL = iFalse;
     prepare_AttributedText_(d, baseDir, overrideChar);
 }
 
@@ -1278,7 +1323,7 @@ static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) {
     iArray glyphIndices;
     init_Array(&glyphIndices, sizeof(uint32_t));
     iAttributedText attrText;
-    init_AttributedText(&attrText, text, 0, d, (iColor){}, 0, 0);
+    init_AttributedText(&attrText, text, 0, d, none_ColorId, 0, d, none_ColorId, 0);
     /* We use AttributedText here so the font lookup matches the behavior during text drawing --
        glyphs may be selected from a font that's different than `d`. */
     const iChar *logicalText = constData_Array(&attrText.logical);
@@ -1333,17 +1378,17 @@ struct Impl_RunArgs {
     /* TODO: Cleanup using TextMetrics
        Use TextMetrics output pointer instead of return value & cursorAdvance_out. */
     iInt2 *       cursorAdvance_out;
-//    const char ** continueFrom_out;
     int *         runAdvance_out;
 };
 
-static iBool notify_WrapText_(iWrapText *d, const char *ending, int origin, int advance, iBool isBaseRTL) {
+static iBool notify_WrapText_(iWrapText *d, const char *ending, iTextAttrib attrib,
+                              int origin, int advance) {
     if (d && d->wrapFunc && d->wrapRange_.start) {
         /* `wrapRange_` uses logical indices. */
         const char *end   = ending ? ending : d->wrapRange_.end;
         iRangecc    range = { d->wrapRange_.start, end };
         iAssert(range.start <= range.end);
-        const iBool result = d->wrapFunc(d, range, origin, advance, isBaseRTL);
+        const iBool result = d->wrapFunc(d, range, attrib, origin, advance);
         if (result) {
             d->wrapRange_.start = end;
         }
@@ -1470,8 +1515,11 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
        font is used and other attributes such as color. (HarfBuzz shaping is done
        with one specific font.) */
     iAttributedText attrText;
-    init_AttributedText(&attrText, args->text, args->maxLen, d, get_Color(args->color),
-                        args->baseDir, wrap ? wrap->overrideChar : 0);
+    init_AttributedText(&attrText, args->text, args->maxLen, d, args->color,
+                        args->baseDir,
+                        activeText_->baseFontId >= 0 ? font_Text_(activeText_->baseFontId) : d,
+                        activeText_->baseColorId,
+                        wrap ? wrap->overrideChar : 0);
     if (wrap) {
         wrap->baseDir = attrText.isBaseRTL ? -1 : +1;
         /* TODO: Duplicated args? */
@@ -1522,6 +1570,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
     iRangei      wrapPosRange       = { 0, textLen };
     int          wrapResumePos      = textLen;  /* logical position where next line resumes */
     size_t       wrapResumeRunIndex = runCount; /* index of run where next line resumes */
+    iTextAttrib  attrib             = { .colorId = args->color, .isBaseRTL = attrText.isBaseRTL };
+    iTextAttrib  wrapAttrib         = attrib;
+    iTextAttrib  lastAttrib         = attrib;
     const int    layoutBound        = (wrap ? wrap->maxWidth : 0);
     iBool        isFirst            = iTrue;
     const iBool  checkHitPoint      = wrap && !isEqual_I2(wrap->hitPoint, zero_I2());
@@ -1545,6 +1596,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
             /* Determine ends of wrapRuns and wrapVisRange. */
             for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) {
                 const iAttributedRun *run = at_Array(&attrText.runs, runIndex);
+                /* Update the attributes. */
                 if (run->flags.isLineBreak) {
                     if (checkHitChar &&
                         wrap->hitChar == sourcePtr_AttributedText_(&attrText, run->logical.start)) {
@@ -1554,7 +1606,6 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
                     wrapResumePos      = run->logical.end;
                     wrapRuns.end       = runIndex;
                     wrapResumeRunIndex = runIndex + 1;
-                    //yCursor += d->height;
                     break;
                 }
                 wrapResumeRunIndex = runCount;
@@ -1564,8 +1615,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
                 shape_GlyphBuffer_(buf);
                 int safeBreakPos = -1;
                 iChar prevCh = 0;
+                lastAttrib = run->attrib;                
                 for (unsigned int ir = 0; ir < buf->glyphCount; ir++) {
-                    const int i = (run->flags.isRTL ? buf->glyphCount - ir - 1 : ir);
+                    const int i = (run->attrib.isRTL ? buf->glyphCount - ir - 1 : ir);
                     const hb_glyph_info_t *info    = &buf->glyphInfo[i];
                     const hb_codepoint_t   glyphId = info->codepoint;
                     const int              logPos  = info->cluster;
@@ -1603,10 +1655,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
                         prevCh = ch;
                     }
                     else {
-                        //if (~glyphFlags & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) {
-                            safeBreakPos = logPos;
-                            breakAdvance = wrapAdvance;
-                        //}
+                        safeBreakPos = logPos;
+                        breakAdvance = wrapAdvance;
+                        wrapAttrib   = run->attrib;
                     }
                     if (isHitPointOnThisLine) {
                         if (wrap->hitPoint.x >= orig.x + wrapAdvance &&
@@ -1638,7 +1689,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
                             wrapPosRange.end = logPos;
                             breakAdvance     = wrapAdvance;
                         }
-                        wrapResumePos      = wrapPosRange.end;
+                        wrapResumePos = wrapPosRange.end;
                         if (args->wrap->mode != anyCharacter_WrapTextMode) {
                             while (wrapResumePos < textLen && isSpace_Char(logicalText[wrapResumePos])) {
                                 wrapResumePos++; /* skip space */
@@ -1688,7 +1739,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
             for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) {
                 const iAttributedRun *run = at_Array(&attrText.runs, runIndex);
                 if (!attrText.isBaseRTL) { /* left-to-right */
-                    if (run->flags.isRTL) {
+                    if (run->attrib.isRTL) {
                         if (oppositeInsertIndex == iInvalidPos) {
                             oppositeInsertIndex = size_Array(&runOrder);
                         }
@@ -1700,7 +1751,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
                     }
                 }
                 else { /* right-to-left */
-                    if (!run->flags.isRTL) {
+                    if (!run->attrib.isRTL) {
                         if (oppositeInsertIndex == iInvalidPos) {
                             oppositeInsertIndex = 0;
                         }
@@ -1739,11 +1790,12 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
         if (wrap && wrap->wrapFunc &&
             !notify_WrapText_(args->wrap,
                               sourcePtr_AttributedText_(&attrText, wrapResumePos),
+                              wrapAttrib,
                               origin,
-                              iRound(wrapAdvance),
-                              attrText.isBaseRTL)) {
+                              iRound(wrapAdvance))) {
             willAbortDueToWrap = iTrue;
         }
+        wrapAttrib = lastAttrib;
         xCursor = origin;
         /* We have determined a possible wrap position and alignment for the work runs,
            so now we can process the glyphs. */
@@ -1796,8 +1848,8 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
                                  orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y,
                                  glyph->rect[hoff].size.x,
                                  glyph->rect[hoff].size.y };
-                if (run->font->height < d->height) {
-                    dst.y += d->baseline - run->font->baseline;
+                if (run->font->height < attrText.baseFont->height) {
+                    dst.y += attrText.baseFont->baseline - run->font->baseline;
                 }
                 if (mode & visualFlag_RunMode) {
                     if (isEmpty_Rect(bounds)) {
@@ -1819,7 +1871,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
                         iAssert(isRasterized_Glyph_(glyph, hoff));
                     }
                     if (~mode & permanentColorFlag_RunMode) {
-                        const iColor clr = run->fgColor;
+                        const iColor clr = fgColor_AttributedRun_(run);
                         SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b);
                         if (args->mode & fillBackground_RunMode) {
                             SDL_SetRenderDrawColor(activeText_->render, clr.r, clr.g, clr.b, 0);
@@ -1939,9 +1991,9 @@ static int runFlagsFromId_(enum iFontId fontId) {
     return runFlags;
 }
 
-static iBool cbAdvanceOneLine_(iWrapText *d, iRangecc range, int origin, int advance,
-                               iBool isBaseRTL) {
-    iUnused(origin, advance, isBaseRTL);
+static iBool cbAdvanceOneLine_(iWrapText *d, iRangecc range, iTextAttrib attrib, int origin,
+                               int advance) {
+    iUnused(attrib, origin, advance);
     *((const char **) d->context) = range.end;
     return iFalse; /* just one line */
 }
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 *);
 int     lineHeight_Text         (int fontId);
 float   emRatio_Text            (int fontId); /* em advance to line height ratio */
 iRect   visualBounds_Text       (int fontId, iRangecc text);
+int     fontWithSize_Text       (int fontId, enum iFontSize sizeId);
+int     fontWithStyle_Text      (int fontId, enum iFontStyle styleId);
+int     fontWithFamily_Text     (int fontId, enum iFontId familyId);
 
 iDeclareType(TextMetrics)
 
@@ -149,9 +152,10 @@ enum iAlignment {
     right_Alignment,
 };
 
-void    setOpacity_Text     (float opacity);
+void    setOpacity_Text         (float opacity);
+void    setBaseAttributes_Text  (int fontId, int colorId); /* current "normal" text attributes */
 
-void    cache_Text          (int fontId, iRangecc text); /* pre-render glyphs */
+void    cache_Text              (int fontId, iRangecc text); /* pre-render glyphs */
 
 void    draw_Text               (int fontId, iInt2 pos, int color, const char *text, ...);
 void    drawAlign_Text          (int fontId, iInt2 pos, int color, enum iAlignment align, const char *text, ...);
@@ -173,12 +177,28 @@ enum iWrapTextMode {
     word_WrapTextMode,
 };
 
+iDeclareType(TextAttrib)
+    
+/* Initial attributes at the start of a text string. These may be modified by control
+   sequences inside a text run. */
+struct Impl_TextAttrib {
+    int16_t colorId;
+    struct {
+        uint16_t bold      : 1;
+        uint16_t italic    : 1;
+        uint16_t monospace : 1;
+        uint16_t isBaseRTL : 1;
+        uint16_t isRTL     : 1;
+    }; 
+};
+
 struct Impl_WrapText {
     /* arguments */
     iRangecc    text;
     int         maxWidth;
     enum iWrapTextMode mode;
-    iBool     (*wrapFunc)(iWrapText *, iRangecc wrappedText, int origin, int advance, iBool isBaseRTL);
+    iBool     (*wrapFunc)(iWrapText *, iRangecc wrappedText, iTextAttrib attrib, int origin,
+                          int advance);
     void *      context;
     iChar       overrideChar; /* use this for all characters instead of the real ones */
     int         baseDir; /* set to +1 for LTR, -1 for RTL */
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.8/cdiff/6b931c95725eef2ebb7e831c4017d3d67b33294f
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
95.208341 milliseconds
Gemini-to-HTML Time
2.361696 milliseconds

This content has been proxied by September (3851b).