summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-07-13 21:02:33 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-07-13 21:02:33 +0300
commitd939dcd1109d7c92e6290976eaf84b94984abef3 (patch)
tree8e07462d687bec980095203e4d351b5e3a9774c0
parentf3a3b7785489f1d7c782e02d2e753d012729ab80 (diff)
Drawing document RTL text runs
The base text direction of each line of text is determined when the document is laid out. When drawing runs, use this predetermined base direction.
-rw-r--r--src/gmdocument.c95
-rw-r--r--src/gmdocument.h7
-rw-r--r--src/ui/documentwidget.c29
-rw-r--r--src/ui/text.c54
-rw-r--r--src/ui/text.h2
5 files changed, 104 insertions, 83 deletions
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 34987beb..9fc270d2 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -337,7 +337,7 @@ static enum iGmDocumentTheme currentTheme_(void) {
337} 337}
338 338
339static void alignDecoration_GmRun_(iGmRun *run, iBool isCentered) { 339static void alignDecoration_GmRun_(iGmRun *run, iBool isCentered) {
340 const iRect visBounds = visualBounds_Text(run->font, run->text); 340 const iRect visBounds = visualBounds_Text(run->textParams.font, run->text);
341 const int visWidth = width_Rect(visBounds); 341 const int visWidth = width_Rect(visBounds);
342 int xAdjust = 0; 342 int xAdjust = 0;
343 if (!isCentered) { 343 if (!isCentered) {
@@ -414,12 +414,13 @@ static const int colors[max_GmLineType] = {
414 tmLinkText_ColorId, 414 tmLinkText_ColorId,
415}; 415};
416 416
417static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, int origin, int advance) { 417static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, int origin,
418 int advance, iBool isBaseRTL) {
418 iAssert(wrapRange.start <= wrapRange.end); 419 iAssert(wrapRange.start <= wrapRange.end);
419 trimEnd_Rangecc(&wrapRange); 420 trimEnd_Rangecc(&wrapRange);
420// printf("typeset: {%s}\n", cstr_Rangecc(wrapRange)); 421// printf("typeset: {%s}\n", cstr_Rangecc(wrapRange));
421 iRunTypesetter *d = wrap->context; 422 iRunTypesetter *d = wrap->context;
422 const int fontId = d->run.font; 423 const int fontId = d->run.textParams.font;
423 d->run.text = wrapRange; 424 d->run.text = wrapRange;
424 if (~d->run.flags & startOfLine_GmRunFlag && d->lineHeightReduction > 0.0f) { 425 if (~d->run.flags & startOfLine_GmRunFlag && d->lineHeightReduction > 0.0f) {
425 d->pos.y -= d->lineHeightReduction * lineHeight_Text(fontId); 426 d->pos.y -= d->lineHeightReduction * lineHeight_Text(fontId);
@@ -431,6 +432,7 @@ static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange,
431 d->run.bounds.size.y = dims.y; 432 d->run.bounds.size.y = dims.y;
432 d->run.visBounds = d->run.bounds; 433 d->run.visBounds = d->run.bounds;
433 d->run.visBounds.size.x = dims.x; 434 d->run.visBounds.size.x = dims.x;
435 d->run.textParams.isRTL = isBaseRTL;
434 pushBack_Array(&d->layout, &d->run); 436 pushBack_Array(&d->layout, &d->run);
435 d->run.flags &= ~startOfLine_GmRunFlag; 437 d->run.flags &= ~startOfLine_GmRunFlag;
436 d->pos.y += lineHeight_Text(fontId) * prefs_App()->lineSpacing; 438 d->pos.y += lineHeight_Text(fontId) * prefs_App()->lineSpacing;
@@ -520,7 +522,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
520 if (*line.end == '\r') { 522 if (*line.end == '\r') {
521 line.end--; /* trim CR always */ 523 line.end--; /* trim CR always */
522 } 524 }
523 iGmRun run = { .color = white_ColorId }; 525 iGmRun run = { .textParams = { .color = white_ColorId } };
524 enum iGmLineType type; 526 enum iGmLineType type;
525 float indent = 0.0f; 527 float indent = 0.0f;
526 /* Detect the type of the line. */ 528 /* Detect the type of the line. */
@@ -565,7 +567,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
565 } 567 }
566 } 568 }
567 trimLine_Rangecc(&line, type, isNormalized); 569 trimLine_Rangecc(&line, type, isNormalized);
568 run.font = fonts[type]; 570 run.textParams.font = fonts[type];
569 /* Remember headings for the document outline. */ 571 /* Remember headings for the document outline. */
570 if (type == heading1_GmLineType || type == heading2_GmLineType || type == heading3_GmLineType) { 572 if (type == heading1_GmLineType || type == heading2_GmLineType || type == heading3_GmLineType) {
571 pushBack_Array( 573 pushBack_Array(
@@ -586,7 +588,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
586 continue; 588 continue;
587 } 589 }
588 run.preId = preId; 590 run.preId = preId;
589 run.font = (d->format == plainText_SourceFormat ? regularMonospace_FontId : preFont); 591 run.textParams.font = (d->format == plainText_SourceFormat ? regularMonospace_FontId : preFont);
590 indent = indents[type]; 592 indent = indents[type];
591 } 593 }
592 if (addSiteBanner) { 594 if (addSiteBanner) {
@@ -601,9 +603,9 @@ static void doLayout_GmDocument_(iGmDocument *d) {
601 banner.visBounds.size.y += iMaxi(6000 * lineHeight_Text(uiLabel_FontId) / 603 banner.visBounds.size.y += iMaxi(6000 * lineHeight_Text(uiLabel_FontId) /
602 d->size.x, lineHeight_Text(uiLabel_FontId) * 5); 604 d->size.x, lineHeight_Text(uiLabel_FontId) * 5);
603 } 605 }
604 banner.font = banner_FontId; 606 banner.text = bannerText;
605 banner.text = bannerText; 607 banner.textParams.font = banner_FontId;
606 banner.color = tmBannerTitle_ColorId; 608 banner.textParams.color = tmBannerTitle_ColorId;
607 pushBack_Array(&d->layout, &banner); 609 pushBack_Array(&d->layout, &banner);
608 pos.y += height_Rect(banner.visBounds) + 610 pos.y += height_Rect(banner.visBounds) +
609 lineHeight_Text(paragraph_FontId) * prefs->lineSpacing; 611 lineHeight_Text(paragraph_FontId) * prefs->lineSpacing;
@@ -614,13 +616,13 @@ static void doLayout_GmDocument_(iGmDocument *d) {
614 if (type == quote_GmLineType && !prefs->quoteIcon) { 616 if (type == quote_GmLineType && !prefs->quoteIcon) {
615 /* For quote indicators we still need to produce a run. */ 617 /* For quote indicators we still need to produce a run. */
616 run.visBounds.pos = addX_I2(pos, indents[type] * gap_Text); 618 run.visBounds.pos = addX_I2(pos, indents[type] * gap_Text);
617 run.visBounds.size = init_I2(gap_Text, lineHeight_Text(run.font)); 619 run.visBounds.size = init_I2(gap_Text, lineHeight_Text(run.textParams.font));
618 run.bounds = zero_Rect(); /* just visual */ 620 run.bounds = zero_Rect(); /* just visual */
619 run.flags = quoteBorder_GmRunFlag | decoration_GmRunFlag; 621 run.flags = quoteBorder_GmRunFlag | decoration_GmRunFlag;
620 run.text = iNullRange; 622 run.text = iNullRange;
621 pushBack_Array(&d->layout, &run); 623 pushBack_Array(&d->layout, &run);
622 } 624 }
623 pos.y += lineHeight_Text(run.font) * prefs->lineSpacing; 625 pos.y += lineHeight_Text(run.textParams.font) * prefs->lineSpacing;
624 prevType = type; 626 prevType = type;
625 if (type != quote_GmLineType) { 627 if (type != quote_GmLineType) {
626 addQuoteIcon = prefs->quoteIcon; 628 addQuoteIcon = prefs->quoteIcon;
@@ -664,13 +666,14 @@ static void doLayout_GmDocument_(iGmDocument *d) {
664 const iGmPreMeta *meta = constAt_Array(&d->preMeta, preId - 1); 666 const iGmPreMeta *meta = constAt_Array(&d->preMeta, preId - 1);
665 if (meta->flags & folded_GmPreMetaFlag) { 667 if (meta->flags & folded_GmPreMetaFlag) {
666 const iBool isBlank = isEmpty_Range(&meta->altText); 668 const iBool isBlank = isEmpty_Range(&meta->altText);
667 iGmRun altText = { .font = paragraph_FontId, 669 iGmRun altText = {
668 .flags = (isBlank ? decoration_GmRunFlag : 0) | altText_GmRunFlag }; 670 .textParams = { .font = paragraph_FontId, .color = tmQuote_ColorId },
671 .flags = (isBlank ? decoration_GmRunFlag : 0) | altText_GmRunFlag
672 };
669 const iInt2 margin = preRunMargin_GmDocument(d, 0); 673 const iInt2 margin = preRunMargin_GmDocument(d, 0);
670 altText.color = tmQuote_ColorId;
671 altText.text = isBlank ? range_Lang(range_CStr("doc.pre.nocaption")) 674 altText.text = isBlank ? range_Lang(range_CStr("doc.pre.nocaption"))
672 : meta->altText; 675 : meta->altText;
673 iInt2 size = measureWrapRange_Text(altText.font, d->size.x - 2 * margin.x, 676 iInt2 size = measureWrapRange_Text(altText.textParams.font, d->size.x - 2 * margin.x,
674 altText.text).bounds.size; 677 altText.text).bounds.size;
675 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x, 678 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x,
676 size.y + 2 * margin.y); 679 size.y + 2 * margin.y);
@@ -689,19 +692,19 @@ static void doLayout_GmDocument_(iGmDocument *d) {
689 setRange_String(&d->title, line); 692 setRange_String(&d->title, line);
690 } 693 }
691 /* List bullet. */ 694 /* List bullet. */
692 run.color = colors[type]; 695 run.textParams.color = colors[type];
693 if (type == bullet_GmLineType) { 696 if (type == bullet_GmLineType) {
694 /* TODO: Literata bullet is broken? */ 697 /* TODO: Literata bullet is broken? */
695 iGmRun bulRun = run; 698 iGmRun bulRun = run;
696 if (prefs->font == literata_TextFont) { 699 if (prefs->font == literata_TextFont) {
697 /* Something wrong this the glyph in Literata, looks cropped. */ 700 /* Something wrong this the glyph in Literata, looks cropped. */
698 bulRun.font = defaultContentRegular_FontId; 701 bulRun.textParams.font = defaultContentRegular_FontId;
699 } 702 }
700 bulRun.color = tmQuote_ColorId; 703 bulRun.textParams.color = tmQuote_ColorId;
701 bulRun.visBounds.pos = addX_I2(pos, (indents[text_GmLineType] - 0.55f) * gap_Text); 704 bulRun.visBounds.pos = addX_I2(pos, (indents[text_GmLineType] - 0.55f) * gap_Text);
702 bulRun.visBounds.size = 705 bulRun.visBounds.size =
703 init_I2((indents[bullet_GmLineType] - indents[text_GmLineType]) * gap_Text, 706 init_I2((indents[bullet_GmLineType] - indents[text_GmLineType]) * gap_Text,
704 lineHeight_Text(bulRun.font)); 707 lineHeight_Text(bulRun.textParams.font));
705 // bulRun.visBounds.pos.x -= 4 * gap_Text - width_Rect(bulRun.visBounds) / 2; 708 // bulRun.visBounds.pos.x -= 4 * gap_Text - width_Rect(bulRun.visBounds) / 2;
706 bulRun.bounds = zero_Rect(); /* just visual */ 709 bulRun.bounds = zero_Rect(); /* just visual */
707 bulRun.text = range_CStr(bullet); 710 bulRun.text = range_CStr(bullet);
@@ -713,11 +716,11 @@ static void doLayout_GmDocument_(iGmDocument *d) {
713 if (type == quote_GmLineType && addQuoteIcon) { 716 if (type == quote_GmLineType && addQuoteIcon) {
714 addQuoteIcon = iFalse; 717 addQuoteIcon = iFalse;
715 iGmRun quoteRun = run; 718 iGmRun quoteRun = run;
716 quoteRun.font = heading1_FontId; 719 quoteRun.textParams.font = heading1_FontId;
717 quoteRun.text = range_CStr(quote); 720 quoteRun.text = range_CStr(quote);
718 quoteRun.color = tmQuoteIcon_ColorId; 721 quoteRun.textParams.color = tmQuoteIcon_ColorId;
719 iRect vis = visualBounds_Text(quoteRun.font, quoteRun.text); 722 iRect vis = visualBounds_Text(quoteRun.textParams.font, quoteRun.text);
720 quoteRun.visBounds.size = measure_Text(quoteRun.font, quote).bounds.size; 723 quoteRun.visBounds.size = measure_Text(quoteRun.textParams.font, quote).bounds.size;
721 quoteRun.visBounds.pos = 724 quoteRun.visBounds.pos =
722 add_I2(pos, 725 add_I2(pos,
723 init_I2((indents[quote_GmLineType] - 5) * gap_Text, 726 init_I2((indents[quote_GmLineType] - 5) * gap_Text,
@@ -733,7 +736,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
733 if (type == link_GmLineType) { 736 if (type == link_GmLineType) {
734 iGmRun icon = run; 737 iGmRun icon = run;
735 icon.visBounds.pos = pos; 738 icon.visBounds.pos = pos;
736 icon.visBounds.size = init_I2(indent * gap_Text, lineHeight_Text(run.font)); 739 icon.visBounds.size = init_I2(indent * gap_Text, lineHeight_Text(run.textParams.font));
737 icon.bounds = zero_Rect(); /* just visual */ 740 icon.bounds = zero_Rect(); /* just visual */
738 const iGmLink *link = constAt_PtrArray(&d->links, run.linkId - 1); 741 const iGmLink *link = constAt_PtrArray(&d->links, run.linkId - 1);
739 icon.text = range_CStr(link->flags & query_GmLinkFlag ? magnifyingGlass 742 icon.text = range_CStr(link->flags & query_GmLinkFlag ? magnifyingGlass
@@ -748,23 +751,23 @@ static void doLayout_GmDocument_(iGmDocument *d) {
748 } 751 }
749 /* TODO: List bullets needs the same centering logic. */ 752 /* TODO: List bullets needs the same centering logic. */
750 /* Special exception for the tiny bullet operator. */ 753 /* Special exception for the tiny bullet operator. */
751 icon.font = equal_Rangecc(link->labelIcon, "\u2219") ? regularMonospace_FontId 754 icon.textParams.font = equal_Rangecc(link->labelIcon, "\u2219") ? regularMonospace_FontId
752 : regular_FontId; 755 : regular_FontId;
753 alignDecoration_GmRun_(&icon, iFalse); 756 alignDecoration_GmRun_(&icon, iFalse);
754 icon.color = linkColor_GmDocument(d, run.linkId, icon_GmLinkPart); 757 icon.textParams.color = linkColor_GmDocument(d, run.linkId, icon_GmLinkPart);
755 icon.flags |= decoration_GmRunFlag; 758 icon.flags |= decoration_GmRunFlag;
756 pushBack_Array(&d->layout, &icon); 759 pushBack_Array(&d->layout, &icon);
757 } 760 }
758 run.color = colors[type]; 761 run.textParams.color = colors[type];
759 if (d->format == plainText_SourceFormat) { 762 if (d->format == plainText_SourceFormat) {
760 run.color = colors[text_GmLineType]; 763 run.textParams.color = colors[text_GmLineType];
761 } 764 }
762 /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */ 765 /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */
763// int bigCount = 0; 766// int bigCount = 0;
764 iBool isLedeParagraph = iFalse; 767 iBool isLedeParagraph = iFalse;
765 if (type == text_GmLineType && isFirstText) { 768 if (type == text_GmLineType && isFirstText) {
766 if (!isMono) run.font = firstParagraph_FontId; 769 if (!isMono) run.textParams.font = firstParagraph_FontId;
767 run.color = tmFirstParagraph_ColorId; 770 run.textParams.color = tmFirstParagraph_ColorId;
768// bigCount = 15; /* max lines -- what if the whole document is one paragraph? */ 771// bigCount = 15; /* max lines -- what if the whole document is one paragraph? */
769 isLedeParagraph = iTrue; 772 isLedeParagraph = iTrue;
770 isFirstText = iFalse; 773 isFirstText = iFalse;
@@ -811,7 +814,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
811 } 814 }
812 /* Visited links are never bold. */ 815 /* Visited links are never bold. */
813 if (run.linkId && linkFlags_GmDocument(d, run.linkId) & visited_GmLinkFlag) { 816 if (run.linkId && linkFlags_GmDocument(d, run.linkId) & visited_GmLinkFlag) {
814 rts.run.font = paragraph_FontId; 817 rts.run.textParams.font = paragraph_FontId;
815 } 818 }
816 } 819 }
817 if (!prefs->quoteIcon && type == quote_GmLineType) { 820 if (!prefs->quoteIcon && type == quote_GmLineType) {
@@ -827,15 +830,15 @@ static void doLayout_GmDocument_(iGmDocument *d) {
827 .mode = word_WrapTextMode, 830 .mode = word_WrapTextMode,
828 .wrapFunc = typesetOneLine_RunTypesetter_, 831 .wrapFunc = typesetOneLine_RunTypesetter_,
829 .context = &rts }, 832 .context = &rts },
830 run.font); 833 run.textParams.font);
831 if (!isLedeParagraph || size_Array(&rts.layout) <= maxLedeLines_) { 834 if (!isLedeParagraph || size_Array(&rts.layout) <= maxLedeLines_) {
832 commit_RunTypesetter_(&rts, d); 835 commit_RunTypesetter_(&rts, d);
833 break; 836 break;
834 } 837 }
835 clear_RunTypesetter_(&rts); 838 clear_RunTypesetter_(&rts);
836 rts.pos = pos; 839 rts.pos = pos;
837 rts.run.font = rts.fonts[text_GmLineType]; 840 rts.run.textParams.font = rts.fonts[text_GmLineType];
838 rts.run.color = colors [text_GmLineType]; 841 rts.run.textParams.color = colors[text_GmLineType];
839 isLedeParagraph = iFalse; 842 isLedeParagraph = iFalse;
840 } 843 }
841 pos = rts.pos; 844 pos = rts.pos;
@@ -869,11 +872,11 @@ static void doLayout_GmDocument_(iGmDocument *d) {
869 run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2; 872 run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;
870 run.bounds.size.y = run.visBounds.size.y; 873 run.bounds.size.y = run.visBounds.size.y;
871 } 874 }
872 run.text = iNullRange; 875 run.text = iNullRange;
873 run.font = 0; 876 run.textParams.font = 0;
874 run.color = 0; 877 run.textParams.color = 0;
875 run.mediaType = image_GmRunMediaType; 878 run.mediaType = image_GmRunMediaType;
876 run.mediaId = imageId; 879 run.mediaId = imageId;
877 pushBack_Array(&d->layout, &run); 880 pushBack_Array(&d->layout, &run);
878 pos.y += run.bounds.size.y + margin; 881 pos.y += run.bounds.size.y + margin;
879 } 882 }
@@ -888,7 +891,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
888 run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI; 891 run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;
889 run.visBounds = run.bounds; 892 run.visBounds = run.bounds;
890 run.text = iNullRange; 893 run.text = iNullRange;
891 run.color = 0; 894 run.textParams.color = 0;
892 run.mediaType = audio_GmRunMediaType; 895 run.mediaType = audio_GmRunMediaType;
893 run.mediaId = audioId; 896 run.mediaId = audioId;
894 pushBack_Array(&d->layout, &run); 897 pushBack_Array(&d->layout, &run);
@@ -905,7 +908,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
905 run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI; 908 run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;
906 run.visBounds = run.bounds; 909 run.visBounds = run.bounds;
907 run.text = iNullRange; 910 run.text = iNullRange;
908 run.color = 0; 911 run.textParams.color = 0;
909 run.mediaType = download_GmRunMediaType; 912 run.mediaType = download_GmRunMediaType;
910 run.mediaId = downloadId; 913 run.mediaId = downloadId;
911 pushBack_Array(&d->layout, &run); 914 pushBack_Array(&d->layout, &run);
@@ -1517,11 +1520,11 @@ static void markLinkRunsVisited_GmDocument_(iGmDocument *d, const iIntSet *linkI
1517 iForEach(Array, r, &d->layout) { 1520 iForEach(Array, r, &d->layout) {
1518 iGmRun *run = r.value; 1521 iGmRun *run = r.value;
1519 if (run->linkId && !run->mediaId && contains_IntSet(linkIds, run->linkId)) { 1522 if (run->linkId && !run->mediaId && contains_IntSet(linkIds, run->linkId)) {
1520 if (run->font == bold_FontId) { 1523 if (run->textParams.font == bold_FontId) {
1521 run->font = paragraph_FontId; 1524 run->textParams.font = paragraph_FontId;
1522 } 1525 }
1523 else if (run->flags & decoration_GmRunFlag) { 1526 else if (run->flags & decoration_GmRunFlag) {
1524 run->color = linkColor_GmDocument(d, run->linkId, icon_GmLinkPart); 1527 run->textParams.color = linkColor_GmDocument(d, run->linkId, icon_GmLinkPart);
1525 } 1528 }
1526 } 1529 }
1527 } 1530 }
@@ -2008,7 +2011,7 @@ iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) {
2008 return (iRangecc){ d->text.start, d->text.start }; 2011 return (iRangecc){ d->text.start, d->text.start };
2009 } 2012 }
2010 iRangecc loc; 2013 iRangecc loc;
2011 tryAdvanceNoWrap_Text(d->font, d->text, x, &loc.start); 2014 tryAdvanceNoWrap_Text(d->textParams.font, d->text, x, &loc.start);
2012 loc.end = loc.start; 2015 loc.end = loc.start;
2013 iChar ch; 2016 iChar ch;
2014 if (d->text.end != loc.start) { 2017 if (d->text.end != loc.start) {
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 0d50e6ad..9d906006 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -128,8 +128,11 @@ struct Impl_GmRun {
128 iRangecc text; 128 iRangecc text;
129 iRect bounds; /* used for hit testing, may extend to edges */ 129 iRect bounds; /* used for hit testing, may extend to edges */
130 iRect visBounds; /* actual visual bounds */ 130 iRect visBounds; /* actual visual bounds */
131 uint8_t font; 131 struct {
132 uint8_t color; 132 uint16_t color : 8;
133 uint16_t font : 7;
134 uint16_t isRTL : 1;
135 } textParams;
133 uint8_t flags; 136 uint8_t flags;
134 uint8_t mediaType; 137 uint8_t mediaType;
135 uint16_t preId; /* preformatted block ID (sequential) */ 138 uint16_t preId; /* preformatted block ID (sequential) */
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 3c6c0039..ea4909eb 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -1585,7 +1585,7 @@ static void parseUser_DocumentWidget_(iDocumentWidget *d) {
1585static void cacheRunGlyphs_(void *data, const iGmRun *run) { 1585static void cacheRunGlyphs_(void *data, const iGmRun *run) {
1586 iUnused(data); 1586 iUnused(data);
1587 if (!isEmpty_Range(&run->text)) { 1587 if (!isEmpty_Range(&run->text)) {
1588 cache_Text(run->font, run->text); 1588 cache_Text(run->textParams.font, run->text);
1589 } 1589 }
1590} 1590}
1591 1591
@@ -3910,14 +3910,14 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol
3910 contains_Range(&mark, run->text.start))) { 3910 contains_Range(&mark, run->text.start))) {
3911 int x = 0; 3911 int x = 0;
3912 if (!*isInside) { 3912 if (!*isInside) {
3913 x = measureRange_Text(run->font, 3913 x = measureRange_Text(run->textParams.font,
3914 (iRangecc){ run->text.start, iMax(run->text.start, mark.start) }) 3914 (iRangecc){ run->text.start, iMax(run->text.start, mark.start) })
3915 .advance.x; 3915 .advance.x;
3916 } 3916 }
3917 int w = width_Rect(run->visBounds) - x; 3917 int w = width_Rect(run->visBounds) - x;
3918 if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) { 3918 if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) {
3919 w = measureRange_Text( 3919 w = measureRange_Text(
3920 run->font, 3920 run->textParams.font,
3921 !*isInside ? mark 3921 !*isInside ? mark
3922 : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) }) 3922 : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) })
3923 .advance.x; 3923 .advance.x;
@@ -3973,15 +3973,15 @@ static void drawBannerRun_DrawContext_(iDrawContext *d, const iGmRun *run, iInt2
3973 iInt2 bpos = add_I2(visPos, init_I2(0, lineHeight_Text(banner_FontId) / 2)); 3973 iInt2 bpos = add_I2(visPos, init_I2(0, lineHeight_Text(banner_FontId) / 2));
3974 if (icon) { 3974 if (icon) {
3975 appendChar_String(&str, icon); 3975 appendChar_String(&str, icon);
3976 const iRect iconRect = visualBounds_Text(run->font, range_String(&str)); 3976 const iRect iconRect = visualBounds_Text(run->textParams.font, range_String(&str));
3977 drawRange_Text( 3977 drawRange_Text(
3978 run->font, 3978 run->textParams.font,
3979 addY_I2(bpos, -mid_Rect(iconRect).y + lineHeight_Text(run->font) / 2), 3979 addY_I2(bpos, -mid_Rect(iconRect).y + lineHeight_Text(run->textParams.font) / 2),
3980 tmBannerIcon_ColorId, 3980 tmBannerIcon_ColorId,
3981 range_String(&str)); 3981 range_String(&str));
3982 bpos.x += right_Rect(iconRect) + 3 * gap_Text; 3982 bpos.x += right_Rect(iconRect) + 3 * gap_Text;
3983 } 3983 }
3984 drawRange_Text(run->font, 3984 drawRange_Text(run->textParams.font,
3985 bpos, 3985 bpos,
3986 tmBannerTitle_ColorId, 3986 tmBannerTitle_ColorId,
3987 bannerText_DocumentWidget_(d->widget)); 3987 bannerText_DocumentWidget_(d->widget));
@@ -4088,7 +4088,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4088 /* Media UIs are drawn afterwards as a dynamic overlay. */ 4088 /* Media UIs are drawn afterwards as a dynamic overlay. */
4089 return; 4089 return;
4090 } 4090 }
4091 enum iColorId fg = run->color; 4091 enum iColorId fg = run->textParams.color;
4092 const iGmDocument *doc = d->widget->doc; 4092 const iGmDocument *doc = d->widget->doc;
4093 const int linkFlags = linkFlags_GmDocument(doc, run->linkId); 4093 const int linkFlags = linkFlags_GmDocument(doc, run->linkId);
4094 /* Hover state of a link. */ 4094 /* Hover state of a link. */
@@ -4152,8 +4152,11 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4152 const iInt2 margin = preRunMargin_GmDocument(doc, run->preId); 4152 const iInt2 margin = preRunMargin_GmDocument(doc, run->preId);
4153 fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId); 4153 fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId);
4154 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmQuoteIcon_ColorId); 4154 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmQuoteIcon_ColorId);
4155 drawWrapRange_Text(run->font, add_I2(visPos, margin), 4155 drawWrapRange_Text(run->textParams.font,
4156 run->visBounds.size.x - 2 * margin.x, run->color, run->text); 4156 add_I2(visPos, margin),
4157 run->visBounds.size.x - 2 * margin.x,
4158 run->textParams.color,
4159 run->text);
4157 } 4160 }
4158 else if (run->flags & siteBanner_GmRunFlag) { 4161 else if (run->flags & siteBanner_GmRunFlag) {
4159 /* Banner background. */ 4162 /* Banner background. */
@@ -4194,7 +4197,11 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4194 height_Rect(run->visBounds), 4197 height_Rect(run->visBounds),
4195 tmQuoteIcon_ColorId); 4198 tmQuoteIcon_ColorId);
4196 } 4199 }
4197 drawBoundRange_Text(run->font, visPos, width_Rect(run->visBounds), fg, run->text); 4200 drawBoundRange_Text(run->textParams.font,
4201 visPos,
4202 (run->textParams.isRTL ? -1 : 1) * width_Rect(run->visBounds),
4203 fg,
4204 run->text);
4198 runDrawn:; 4205 runDrawn:;
4199 } 4206 }
4200 /* Presentation of links. */ 4207 /* Presentation of links. */
diff --git a/src/ui/text.c b/src/ui/text.c
index e1638368..03ce46c7 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -792,7 +792,8 @@ struct Impl_AttributedRun {
792}; 792};
793 793
794iDeclareType(AttributedText) 794iDeclareType(AttributedText)
795iDeclareTypeConstructionArgs(AttributedText, iRangecc text, size_t maxLen, iFont *font, iColor fgColor) 795iDeclareTypeConstructionArgs(AttributedText, iRangecc text, size_t maxLen, iFont *font,
796 iColor fgColor, int baseDir)
796 797
797struct Impl_AttributedText { 798struct Impl_AttributedText {
798 iRangecc source; /* original source text */ 799 iRangecc source; /* original source text */
@@ -809,8 +810,9 @@ struct Impl_AttributedText {
809 iBool isBaseRTL; 810 iBool isBaseRTL;
810}; 811};
811 812
812iDefineTypeConstructionArgs(AttributedText, (iRangecc text, size_t maxLen, iFont *font, iColor fgColor), 813iDefineTypeConstructionArgs(AttributedText,
813 text, maxLen, font, fgColor) 814 (iRangecc text, size_t maxLen, iFont *font, iColor fgColor, int baseDir),
815 text, maxLen, font, fgColor, baseDir)
814 816
815static const char *sourcePtr_AttributedText_(const iAttributedText *d, int logicalPos) { 817static const char *sourcePtr_AttributedText_(const iAttributedText *d, int logicalPos) {
816 const int *logToSource = constData_Array(&d->logicalToSourceOffset); 818 const int *logToSource = constData_Array(&d->logicalToSourceOffset);
@@ -867,7 +869,7 @@ static enum iFontId fontId_Text_(const iFont *font) {
867 return (enum iFontId) (font - text_.fonts); 869 return (enum iFontId) (font - text_.fonts);
868} 870}
869 871
870static void prepare_AttributedText_(iAttributedText *d) { 872static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir) {
871 iAssert(isEmpty_Array(&d->runs)); 873 iAssert(isEmpty_Array(&d->runs));
872 size_t length = 0; 874 size_t length = 0;
873 /* Prepare the UTF-32 logical string. */ { 875 /* Prepare the UTF-32 logical string. */ {
@@ -896,7 +898,7 @@ static void prepare_AttributedText_(iAttributedText *d) {
896 data_Array(&d->logicalToVisual), 898 data_Array(&d->logicalToVisual),
897 data_Array(&d->visualToLogical), 899 data_Array(&d->visualToLogical),
898 (FriBidiLevel *) d->bidiLevels); 900 (FriBidiLevel *) d->bidiLevels);
899 d->isBaseRTL = FRIBIDI_IS_RTL(baseDir); 901 d->isBaseRTL = (overrideBaseDir == 0 ? FRIBIDI_IS_RTL(baseDir) : (overrideBaseDir < 0));
900#endif 902#endif
901 } 903 }
902 /* The mapping needs to include the terminating NULL position. */ { 904 /* The mapping needs to include the terminating NULL position. */ {
@@ -1017,7 +1019,8 @@ static void prepare_AttributedText_(iAttributedText *d) {
1017#endif 1019#endif
1018} 1020}
1019 1021
1020void init_AttributedText(iAttributedText *d, iRangecc text, size_t maxLen, iFont *font, iColor fgColor) { 1022void init_AttributedText(iAttributedText *d, iRangecc text, size_t maxLen, iFont *font, iColor fgColor,
1023 int baseDir) {
1021 d->source = text; 1024 d->source = text;
1022 d->maxLen = maxLen ? maxLen : iInvalidSize; 1025 d->maxLen = maxLen ? maxLen : iInvalidSize;
1023 d->font = font; 1026 d->font = font;
@@ -1030,7 +1033,7 @@ void init_AttributedText(iAttributedText *d, iRangecc text, size_t maxLen, iFont
1030 init_Array(&d->logicalToSourceOffset, sizeof(int)); 1033 init_Array(&d->logicalToSourceOffset, sizeof(int));
1031 d->bidiLevels = NULL; 1034 d->bidiLevels = NULL;
1032 d->isBaseRTL = iFalse; 1035 d->isBaseRTL = iFalse;
1033 prepare_AttributedText_(d); 1036 prepare_AttributedText_(d, baseDir);
1034} 1037}
1035 1038
1036void deinit_AttributedText(iAttributedText *d) { 1039void deinit_AttributedText(iAttributedText *d) {
@@ -1181,7 +1184,7 @@ static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) {
1181 iArray glyphIndices; 1184 iArray glyphIndices;
1182 init_Array(&glyphIndices, sizeof(uint32_t)); 1185 init_Array(&glyphIndices, sizeof(uint32_t));
1183 iAttributedText attrText; 1186 iAttributedText attrText;
1184 init_AttributedText(&attrText, text, 0, d, (iColor){}); 1187 init_AttributedText(&attrText, text, 0, d, (iColor){}, 0);
1185 /* We use AttributedText here so the font lookup matches the behavior during text drawing -- 1188 /* We use AttributedText here so the font lookup matches the behavior during text drawing --
1186 glyphs may be selected from a font that's different than `d`. */ 1189 glyphs may be selected from a font that's different than `d`. */
1187 const iChar *logicalText = constData_Array(&attrText.logical); 1190 const iChar *logicalText = constData_Array(&attrText.logical);
@@ -1229,9 +1232,10 @@ struct Impl_RunArgs {
1229 size_t maxLen; /* max characters to process */ 1232 size_t maxLen; /* max characters to process */
1230 iInt2 pos; 1233 iInt2 pos;
1231 iWrapText * wrap; 1234 iWrapText * wrap;
1232 int xposLimit; /* hard limit for wrapping */ 1235 int xposLimit; /* hard limit for wrapping */
1233 int xposLayoutBound; /* visible bound for layout purposes; does not affect wrapping */ 1236 int xposLayoutBound; /* visible bound for layout purposes; does not affect wrapping */
1234 int color; 1237 int color;
1238 int baseDir;
1235 /* TODO: Cleanup using TextMetrics 1239 /* TODO: Cleanup using TextMetrics
1236 Use TextMetrics output pointer instead of return value & cursorAdvance_out. */ 1240 Use TextMetrics output pointer instead of return value & cursorAdvance_out. */
1237 iInt2 * cursorAdvance_out; 1241 iInt2 * cursorAdvance_out;
@@ -1239,13 +1243,13 @@ struct Impl_RunArgs {
1239 int * runAdvance_out; 1243 int * runAdvance_out;
1240}; 1244};
1241 1245
1242static iBool notify_WrapText_(iWrapText *d, const char *ending, int origin, int advance) { 1246static iBool notify_WrapText_(iWrapText *d, const char *ending, int origin, int advance, iBool isBaseRTL) {
1243 if (d && d->wrapFunc && d->wrapRange_.start) { 1247 if (d && d->wrapFunc && d->wrapRange_.start) {
1244 /* `wrapRange_` uses logical indices. */ 1248 /* `wrapRange_` uses logical indices. */
1245 const char *end = ending ? ending : d->wrapRange_.end; 1249 const char *end = ending ? ending : d->wrapRange_.end;
1246 iRangecc range = { d->wrapRange_.start, end }; 1250 iRangecc range = { d->wrapRange_.start, end };
1247 iAssert(range.start <= range.end); 1251 iAssert(range.start <= range.end);
1248 const iBool result = d->wrapFunc(d, range, origin, advance); 1252 const iBool result = d->wrapFunc(d, range, origin, advance, isBaseRTL);
1249 if (result) { 1253 if (result) {
1250 d->wrapRange_.start = end; 1254 d->wrapRange_.start = end;
1251 } 1255 }
@@ -1380,7 +1384,8 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1380 font is used and other attributes such as color. (HarfBuzz shaping is done 1384 font is used and other attributes such as color. (HarfBuzz shaping is done
1381 with one specific font.) */ 1385 with one specific font.) */
1382 iAttributedText attrText; 1386 iAttributedText attrText;
1383 init_AttributedText(&attrText, args->text, args->maxLen, d, get_Color(args->color)); 1387 init_AttributedText(&attrText, args->text, args->maxLen, d, get_Color(args->color),
1388 args->baseDir);
1384 if (args->wrap) { 1389 if (args->wrap) {
1385 /* TODO: Duplicated args? */ 1390 /* TODO: Duplicated args? */
1386 iAssert(equalRange_Rangecc(args->wrap->text, args->text)); 1391 iAssert(equalRange_Rangecc(args->wrap->text, args->text));
@@ -1390,7 +1395,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1390 const iChar *logicalText = constData_Array(&attrText.logical); 1395 const iChar *logicalText = constData_Array(&attrText.logical);
1391 const iChar *visualText = constData_Array(&attrText.visual); 1396 const iChar *visualText = constData_Array(&attrText.visual);
1392 const int * logToVis = constData_Array(&attrText.logicalToVisual); 1397 const int * logToVis = constData_Array(&attrText.logicalToVisual);
1393 const int * visToLog = constData_Array(&attrText.visualToLogical); 1398// const int * visToLog = constData_Array(&attrText.visualToLogical);
1394 const size_t runCount = size_Array(&attrText.runs); 1399 const size_t runCount = size_Array(&attrText.runs);
1395 iArray buffers; 1400 iArray buffers;
1396 init_Array(&buffers, sizeof(iGlyphBuffer)); 1401 init_Array(&buffers, sizeof(iGlyphBuffer));
@@ -1580,7 +1585,8 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1580 if (!notify_WrapText_(args->wrap, 1585 if (!notify_WrapText_(args->wrap,
1581 sourcePtr_AttributedText_(&attrText, wrapResumePos), 1586 sourcePtr_AttributedText_(&attrText, wrapResumePos),
1582 origin, 1587 origin,
1583 iRound(wrapAdvance))) { 1588 iRound(wrapAdvance),
1589 attrText.isBaseRTL)) {
1584 willAbortDueToWrap = iTrue; 1590 willAbortDueToWrap = iTrue;
1585 } 1591 }
1586 xCursor = origin; 1592 xCursor = origin;
@@ -1743,8 +1749,9 @@ static int runFlagsFromId_(enum iFontId fontId) {
1743 return runFlags; 1749 return runFlags;
1744} 1750}
1745 1751
1746static iBool cbAdvanceOneLine_(iWrapText *d, iRangecc range, int origin, int advance) { 1752static iBool cbAdvanceOneLine_(iWrapText *d, iRangecc range, int origin, int advance,
1747 iUnused(origin, advance); 1753 iBool isBaseRTL) {
1754 iUnused(origin, advance, isBaseRTL);
1748 *((const char **) d->context) = range.end; 1755 *((const char **) d->context) = range.end;
1749 return iFalse; /* just one line */ 1756 return iFalse; /* just one line */
1750} 1757}
@@ -1785,9 +1792,9 @@ iTextMetrics measureN_Text(int fontId, const char *text, size_t n) {
1785} 1792}
1786 1793
1787static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text, size_t maxLen) { 1794static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text, size_t maxLen) {
1788 iText *d = &text_; 1795 iText * d = &text_;
1789 iFont *font = font_Text_(fontId); 1796 iFont * font = font_Text_(fontId);
1790 const iColor clr = get_Color(color & mask_ColorId); 1797 const iColor clr = get_Color(color & mask_ColorId);
1791 SDL_SetTextureColorMod(d->cache, clr.r, clr.g, clr.b); 1798 SDL_SetTextureColorMod(d->cache, clr.r, clr.g, clr.b);
1792 run_Font_(font, 1799 run_Font_(font,
1793 &(iRunArgs){ .mode = draw_RunMode | 1800 &(iRunArgs){ .mode = draw_RunMode |
@@ -1798,7 +1805,8 @@ static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color,
1798 .maxLen = maxLen, 1805 .maxLen = maxLen,
1799 .pos = pos, 1806 .pos = pos,
1800 .xposLayoutBound = xposBound, 1807 .xposLayoutBound = xposBound,
1801 .color = color & mask_ColorId }); 1808 .color = color & mask_ColorId,
1809 .baseDir = xposBound ? iSign(xposBound - pos.x) : 0 });
1802} 1810}
1803 1811
1804static void drawBounded_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text) { 1812static void drawBounded_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text) {
diff --git a/src/ui/text.h b/src/ui/text.h
index 341f94da..4630b9f6 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -205,7 +205,7 @@ struct Impl_WrapText {
205 iRangecc text; 205 iRangecc text;
206 int maxWidth; 206 int maxWidth;
207 enum iWrapTextMode mode; 207 enum iWrapTextMode mode;
208 iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, int origin, int advance); 208 iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, int origin, int advance, iBool isBaseRTL);
209 void * context; 209 void * context;
210 /* internal */ 210 /* internal */
211 iRangecc wrapRange_; 211 iRangecc wrapRange_;