summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-07-06 07:04:29 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-07-06 07:04:29 +0300
commite1ff98965a4aec72418165424a0183e596df1ecb (patch)
treee01f245348ba39d2a2eb5fdffe5a8600e5928b96
parent5915a8509827bbad99731b0e8451f484bff1ac61 (diff)
Text run measurement API change; bug fixes
The distinction between measure_Text and advance_Text was not very clear. Now there are only measure_Text functions that return both the bounds and the cursor position advancement, and the appropriate metrics are used by the caller.
-rw-r--r--src/gmdocument.c10
-rw-r--r--src/ui/documentwidget.c30
-rw-r--r--src/ui/inputwidget.c17
-rw-r--r--src/ui/labelwidget.c8
-rw-r--r--src/ui/lookupwidget.c2
-rw-r--r--src/ui/mediaui.c6
-rw-r--r--src/ui/sidebarwidget.c8
-rw-r--r--src/ui/text.c524
-rw-r--r--src/ui/text.h34
-rw-r--r--src/ui/translation.c2
-rw-r--r--src/ui/util.c4
11 files changed, 454 insertions, 191 deletions
diff --git a/src/gmdocument.c b/src/gmdocument.c
index cde6f4c9..d49cccd9 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -166,7 +166,7 @@ static iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const ch
166 } 166 }
167 contents->end = line.end; 167 contents->end = line.end;
168 } 168 }
169 return measureRange_Text(font, *contents); 169 return measureRange_Text(font, *contents).bounds.size;
170} 170}
171 171
172static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) { 172static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) {
@@ -483,7 +483,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
483 if (meta.pixelRect.size.x > 483 if (meta.pixelRect.size.x >
484 d->size.x - (enableIndents ? indents[preformatted_GmLineType] : 0) * gap_Text) { 484 d->size.x - (enableIndents ? indents[preformatted_GmLineType] : 0) * gap_Text) {
485 preFont = preformattedSmall_FontId; 485 preFont = preformattedSmall_FontId;
486 meta.pixelRect.size = measureRange_Text(preFont, meta.contents); 486 meta.pixelRect.size = measureRange_Text(preFont, meta.contents).bounds.size;
487 } 487 }
488 trimLine_Rangecc(&line, type, isNormalized); 488 trimLine_Rangecc(&line, type, isNormalized);
489 meta.altText = line; /* without the ``` */ 489 meta.altText = line; /* without the ``` */
@@ -614,8 +614,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
614 altText.color = tmQuote_ColorId; 614 altText.color = tmQuote_ColorId;
615 altText.text = isBlank ? range_Lang(range_CStr("doc.pre.nocaption")) 615 altText.text = isBlank ? range_Lang(range_CStr("doc.pre.nocaption"))
616 : meta->altText; 616 : meta->altText;
617 iInt2 size = advanceWrapRange_Text(altText.font, d->size.x - 2 * margin.x, 617 iInt2 size = measureWrapRange_Text(altText.font, d->size.x - 2 * margin.x,
618 altText.text); 618 altText.text).bounds.size;
619 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x, 619 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x,
620 size.y + 2 * margin.y); 620 size.y + 2 * margin.y);
621 altText.preId = preId; 621 altText.preId = preId;
@@ -661,7 +661,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
661 quoteRun.text = range_CStr(quote); 661 quoteRun.text = range_CStr(quote);
662 quoteRun.color = tmQuoteIcon_ColorId; 662 quoteRun.color = tmQuoteIcon_ColorId;
663 iRect vis = visualBounds_Text(quoteRun.font, quoteRun.text); 663 iRect vis = visualBounds_Text(quoteRun.font, quoteRun.text);
664 quoteRun.visBounds.size = advance_Text(quoteRun.font, quote); 664 quoteRun.visBounds.size = measure_Text(quoteRun.font, quote).bounds.size;
665 quoteRun.visBounds.pos = 665 quoteRun.visBounds.pos =
666 add_I2(pos, 666 add_I2(pos,
667 init_I2((indents[quote_GmLineType] - 5) * gap_Text, 667 init_I2((indents[quote_GmLineType] - 5) * gap_Text,
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 6a2173af..5bded6c6 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -942,7 +942,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
942 prependChar_String(text, siteIcon); 942 prependChar_String(text, siteIcon);
943 prependCStr_String(text, escape_Color(uiIcon_ColorId)); 943 prependCStr_String(text, escape_Color(uiIcon_ColorId));
944 } 944 }
945 const int width = advanceRange_Text(default_FontId, range_String(text)).x; 945 const int width = measureRange_Text(default_FontId, range_String(text)).advance.x;
946 if (width <= avail || 946 if (width <= avail ||
947 isEmpty_StringArray(title)) { 947 isEmpty_StringArray(title)) {
948 updateText_LabelWidget(tabButton, text); 948 updateText_LabelWidget(tabButton, text);
@@ -953,7 +953,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
953 const char *endPos; 953 const char *endPos;
954 tryAdvanceNoWrap_Text(default_FontId, 954 tryAdvanceNoWrap_Text(default_FontId,
955 range_String(text), 955 range_String(text),
956 avail - advance_Text(default_FontId, "...").x, 956 avail - measure_Text(default_FontId, "...").advance.x,
957 &endPos); 957 &endPos);
958 updateText_LabelWidget( 958 updateText_LabelWidget(
959 tabButton, 959 tabButton,
@@ -3898,17 +3898,17 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol
3898 contains_Range(&mark, run->text.start))) { 3898 contains_Range(&mark, run->text.start))) {
3899 int x = 0; 3899 int x = 0;
3900 if (!*isInside) { 3900 if (!*isInside) {
3901 x = advanceRange_Text(run->font, 3901 x = measureRange_Text(run->font,
3902 (iRangecc){ run->text.start, iMax(run->text.start, mark.start) }) 3902 (iRangecc){ run->text.start, iMax(run->text.start, mark.start) })
3903 .x; 3903 .advance.x;
3904 } 3904 }
3905 int w = width_Rect(run->visBounds) - x; 3905 int w = width_Rect(run->visBounds) - x;
3906 if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) { 3906 if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) {
3907 w = advanceRange_Text( 3907 w = measureRange_Text(
3908 run->font, 3908 run->font,
3909 !*isInside ? mark 3909 !*isInside ? mark
3910 : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) }) 3910 : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) })
3911 .x; 3911 .advance.x;
3912 *isInside = iFalse; 3912 *isInside = iFalse;
3913 } 3913 }
3914 else { 3914 else {
@@ -4013,8 +4013,8 @@ static void drawBannerRun_DrawContext_(iDrawContext *d, const iGmRun *run, iInt2
4013 appendCStr_String(&str, "\n"); 4013 appendCStr_String(&str, "\n");
4014 appendCStr_String(&str, cstr_Lang("dlg.certwarn.domain.expired")); 4014 appendCStr_String(&str, cstr_Lang("dlg.certwarn.domain.expired"));
4015 } 4015 }
4016 const iInt2 dims = advanceWrapRange_Text( 4016 const iInt2 dims = measureWrapRange_Text(
4017 uiContent_FontId, width_Rect(rect) - 16 * gap_UI, range_String(&str)); 4017 uiContent_FontId, width_Rect(rect) - 16 * gap_UI, range_String(&str)).bounds.size;
4018 const int warnHeight = run->visBounds.size.y - domainHeight; 4018 const int warnHeight = run->visBounds.size.y - domainHeight;
4019 const int yOff = (lineHeight_Text(uiLabelLarge_FontId) - 4019 const int yOff = (lineHeight_Text(uiLabelLarge_FontId) -
4020 lineHeight_Text(uiContent_FontId)) / 2; 4020 lineHeight_Text(uiContent_FontId)) / 2;
@@ -4224,7 +4224,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4224 appendFormat_String( 4224 appendFormat_String(
4225 &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); 4225 &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : "");
4226 } 4226 }
4227 const iInt2 size = measureRange_Text(metaFont, range_String(&text)); 4227 const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size;
4228 fillRect_Paint( 4228 fillRect_Paint(
4229 &d->paint, 4229 &d->paint,
4230 (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)), 4230 (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)),
@@ -4293,7 +4293,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4293 append_String(&str, collect_String(format_Date(&date, "%b %d"))); 4293 append_String(&str, collect_String(format_Date(&date, "%b %d")));
4294 } 4294 }
4295 if (!isEmpty_String(&str)) { 4295 if (!isEmpty_String(&str)) {
4296 const iInt2 textSize = measure_Text(metaFont, cstr_String(&str)); 4296 const iInt2 textSize = measure_Text(metaFont, cstr_String(&str)).bounds.size;
4297 int tx = topRight_Rect(linkRect).x; 4297 int tx = topRight_Rect(linkRect).x;
4298 const char *msg = cstr_String(&str); 4298 const char *msg = cstr_String(&str);
4299 if (tx + textSize.x > right_Rect(d->widgetBounds)) { 4299 if (tx + textSize.x > right_Rect(d->widgetBounds)) {
@@ -4301,7 +4301,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4301 fillRect_Paint(&d->paint, (iRect){ init_I2(tx, top_Rect(linkRect)), textSize }, 4301 fillRect_Paint(&d->paint, (iRect){ init_I2(tx, top_Rect(linkRect)), textSize },
4302 uiBackground_ColorId); 4302 uiBackground_ColorId);
4303 msg += 4; /* skip the space and dash */ 4303 msg += 4; /* skip the space and dash */
4304 tx += measure_Text(metaFont, " \u2014").x / 2; 4304 tx += measure_Text(metaFont, " \u2014").advance.x / 2;
4305 } 4305 }
4306 drawAlign_Text(metaFont, 4306 drawAlign_Text(metaFont,
4307 init_I2(tx, top_Rect(linkRect)), 4307 init_I2(tx, top_Rect(linkRect)),
@@ -4359,8 +4359,8 @@ static void updateSideIconBuf_DocumentWidget_(const iDocumentWidget *d) {
4359 /* Determine the required size. */ 4359 /* Determine the required size. */
4360 iInt2 bufSize = init1_I2(minBannerSize); 4360 iInt2 bufSize = init1_I2(minBannerSize);
4361 if (isHeadingVisible) { 4361 if (isHeadingVisible) {
4362 const iInt2 headingSize = advanceWrapRange_Text(heading3_FontId, avail, 4362 const iInt2 headingSize = measureWrapRange_Text(heading3_FontId, avail,
4363 currentHeading_DocumentWidget_(d)); 4363 currentHeading_DocumentWidget_(d)).bounds.size;
4364 if (headingSize.x > 0) { 4364 if (headingSize.x > 0) {
4365 bufSize.y += gap_Text + headingSize.y; 4365 bufSize.y += gap_Text + headingSize.y;
4366 bufSize.x = iMax(bufSize.x, headingSize.x); 4366 bufSize.x = iMax(bufSize.x, headingSize.x);
@@ -4736,7 +4736,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
4736 if (prefs_App()->hoverLink && d->hoverLink) { 4736 if (prefs_App()->hoverLink && d->hoverLink) {
4737 const int font = uiLabel_FontId; 4737 const int font = uiLabel_FontId;
4738 const iRangecc linkUrl = range_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId)); 4738 const iRangecc linkUrl = range_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId));
4739 const iInt2 size = measureRange_Text(font, linkUrl); 4739 const iInt2 size = measureRange_Text(font, linkUrl).bounds.size;
4740 const iRect linkRect = { addY_I2(bottomLeft_Rect(bounds), -size.y), 4740 const iRect linkRect = { addY_I2(bottomLeft_Rect(bounds), -size.y),
4741 addX_I2(size, 2 * gap_UI) }; 4741 addX_I2(size, 2 * gap_UI) };
4742 fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId); 4742 fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId);
@@ -4757,7 +4757,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
4757 const int wrap = docBounds.size.x - 2 * margin; 4757 const int wrap = docBounds.size.x - 2 * margin;
4758 iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos), 4758 iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos),
4759 -pos_SmoothScroll(&d->scrollY)); 4759 -pos_SmoothScroll(&d->scrollY));
4760 const iInt2 textSize = advanceWrapRange_Text(altFont, wrap, meta->altText); 4760 const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size;
4761 pos.y -= textSize.y + gap_UI; 4761 pos.y -= textSize.y + gap_UI;
4762 pos.y = iMax(pos.y, top_Rect(bounds)); 4762 pos.y = iMax(pos.y, top_Rect(bounds));
4763 const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) }; 4763 const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) };
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 8ea21cff..e65af417 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -211,7 +211,7 @@ static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) {
211 int extraHeight = (flags_Widget(as_Widget(d)) & extraPadding_WidgetFlag ? extraPaddingHeight_ : 0); 211 int extraHeight = (flags_Widget(as_Widget(d)) & extraPadding_WidgetFlag ? extraPaddingHeight_ : 0);
212 setFixedSize_Widget( 212 setFixedSize_Widget(
213 as_Widget(d), 213 as_Widget(d),
214 add_I2(measure_Text(d->font, cstr_Block(content)), 214 add_I2(measure_Text(d->font, cstr_Block(content)).bounds.size,
215 init_I2(6 * gap_UI + d->leftPadding + d->rightPadding, 215 init_I2(6 * gap_UI + d->leftPadding + d->rightPadding,
216 2 * gap_UI + extraHeight))); 216 2 * gap_UI + extraHeight)));
217 delete_Block(content); 217 delete_Block(content);
@@ -747,7 +747,7 @@ static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const
747 747
748static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir) { 748static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir) {
749 const iInputLine *line = line_InputWidget_(d, d->cursorLine); 749 const iInputLine *line = line_InputWidget_(d, d->cursorLine);
750 int xPos = advanceN_Text(d->font, cstr_String(&line->text), d->cursor - line->offset).x; 750 int xPos = measureN_Text(d->font, cstr_String(&line->text), d->cursor - line->offset).advance.x;
751 size_t newCursor = iInvalidPos; 751 size_t newCursor = iInvalidPos;
752 const size_t numLines = size_Array(&d->lines); 752 const size_t numLines = size_Array(&d->lines);
753 if (dir < 0 && d->cursorLine > 0) { 753 if (dir < 0 && d->cursorLine > 0) {
@@ -1400,14 +1400,14 @@ static void draw_InputWidget_(const iInputWidget *d) {
1400 /* Draw the selected range. */ 1400 /* Draw the selected range. */
1401 const iRanges mark = mark_InputWidget_(d); 1401 const iRanges mark = mark_InputWidget_(d);
1402 if (mark.start < lineRange.end && mark.end > lineRange.start) { 1402 if (mark.start < lineRange.end && mark.end > lineRange.start) {
1403 const int m1 = advanceN_Text(d->font, 1403 const int m1 = measureN_Text(d->font,
1404 cstr_String(&line->text), 1404 cstr_String(&line->text),
1405 iMax(lineRange.start, mark.start) - line->offset) 1405 iMax(lineRange.start, mark.start) - line->offset)
1406 .x; 1406 .advance.x;
1407 const int m2 = advanceN_Text(d->font, 1407 const int m2 = measureN_Text(d->font,
1408 cstr_String(&line->text), 1408 cstr_String(&line->text),
1409 iMin(lineRange.end, mark.end) - line->offset) 1409 iMin(lineRange.end, mark.end) - line->offset)
1410 .x; 1410 .advance.x;
1411 fillRect_Paint(&p, 1411 fillRect_Paint(&p,
1412 (iRect){ addX_I2(drawPos, iMin(m1, m2)), 1412 (iRect){ addX_I2(drawPos, iMin(m1, m2)),
1413 init_I2(iMax(gap_UI / 3, iAbs(m2 - m1)), 1413 init_I2(iMax(gap_UI / 3, iAbs(m2 - m1)),
@@ -1437,7 +1437,8 @@ static void draw_InputWidget_(const iInputWidget *d) {
1437 else { 1437 else {
1438 initCStr_String(&cur, " "); 1438 initCStr_String(&cur, " ");
1439 } 1439 }
1440 curSize = addX_I2(advance_Text(d->font, cstr_String(&cur)), iMin(2, gap_UI / 4)); 1440 curSize = addX_I2(measure_Text(d->font, cstr_String(&cur)).bounds.size,
1441 iMin(2, gap_UI / 4));
1441 } 1442 }
1442 else { 1443 else {
1443 /* Bar cursor. */ 1444 /* Bar cursor. */
@@ -1447,7 +1448,7 @@ static void draw_InputWidget_(const iInputWidget *d) {
1447 const iString * text = &curLine->text; 1448 const iString * text = &curLine->text;
1448 /* The `gap_UI` offsets below are a hack. They are used because for some reason the 1449 /* The `gap_UI` offsets below are a hack. They are used because for some reason the
1449 cursor rect and the glyph inside don't quite position like during `run_Text_()`. */ 1450 cursor rect and the glyph inside don't quite position like during `run_Text_()`. */
1450 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(text), d->cursor - curLine->offset); 1451 const iInt2 prefixSize = measureN_Text(d->font, cstr_String(text), d->cursor - curLine->offset).bounds.size;
1451 const iInt2 curPos = addX_I2(addY_I2(contentBounds.pos, lineHeight_Text(d->font) * d->cursorLine), 1452 const iInt2 curPos = addX_I2(addY_I2(contentBounds.pos, lineHeight_Text(d->font) * d->cursorLine),
1452 prefixSize.x + 1453 prefixSize.x +
1453 (d->mode == insert_InputMode ? -curSize.x / 2 : 0)); 1454 (d->mode == insert_InputMode ? -curSize.x / 2 : 0));
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index b68ab793..7d6bac67 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -376,9 +376,9 @@ static void sizeChanged_LabelWidget_(iLabelWidget *d) {
376 if (d->flags.wrap) { 376 if (d->flags.wrap) {
377 if (flags_Widget(w) & fixedHeight_WidgetFlag) { 377 if (flags_Widget(w) & fixedHeight_WidgetFlag) {
378 /* Calculate a new height based on the wrapping. */ 378 /* Calculate a new height based on the wrapping. */
379 w->rect.size.y = advanceWrapRange_Text( 379 w->rect.size.y = measureWrapRange_Text(
380 d->font, innerBounds_Widget(w).size.x, range_String(&d->label)) 380 d->font, innerBounds_Widget(w).size.x, range_String(&d->label))
381 .y; 381 .bounds.size.y;
382 } 382 }
383 } 383 }
384} 384}
@@ -386,13 +386,13 @@ static void sizeChanged_LabelWidget_(iLabelWidget *d) {
386iInt2 defaultSize_LabelWidget(const iLabelWidget *d) { 386iInt2 defaultSize_LabelWidget(const iLabelWidget *d) {
387 const iWidget *w = constAs_Widget(d); 387 const iWidget *w = constAs_Widget(d);
388 const int64_t flags = flags_Widget(w); 388 const int64_t flags = flags_Widget(w);
389 iInt2 size = add_I2(measure_Text(d->font, cstr_String(&d->label)), 389 iInt2 size = add_I2(measure_Text(d->font, cstr_String(&d->label)).bounds.size,
390 add_I2(padding_LabelWidget_(d, 0), padding_LabelWidget_(d, 2))); 390 add_I2(padding_LabelWidget_(d, 0), padding_LabelWidget_(d, 2)));
391 if ((flags & drawKey_WidgetFlag) && d->key) { 391 if ((flags & drawKey_WidgetFlag) && d->key) {
392 iString str; 392 iString str;
393 init_String(&str); 393 init_String(&str);
394 keyStr_LabelWidget_(d, &str); 394 keyStr_LabelWidget_(d, &str);
395 size.x += 2 * gap_UI + measure_Text(uiShortcuts_FontId, cstr_String(&str)).x; 395 size.x += 2 * gap_UI + measure_Text(uiShortcuts_FontId, cstr_String(&str)).bounds.size.x;
396 deinit_String(&str); 396 deinit_String(&str);
397 } 397 }
398 size.x += iconPadding_LabelWidget_(d); 398 size.x += iconPadding_LabelWidget_(d);
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index 254aad93..a0a507ca 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -121,7 +121,7 @@ static void draw_LookupItem_(iLookupItem *d, iPaint *p, iRect rect, const iListW
121 ? permanent_ColorId | (isPressing || isCursor ? uiTextPressed_ColorId 121 ? permanent_ColorId | (isPressing || isCursor ? uiTextPressed_ColorId
122 : uiTextFramelessHover_ColorId) 122 : uiTextFramelessHover_ColorId)
123 : d->fg; 123 : d->fg;
124 const iInt2 size = measureRange_Text(d->font, range_String(&d->text)); 124 const iInt2 size = measureRange_Text(d->font, range_String(&d->text)).bounds.size;
125 iInt2 pos = init_I2(left_Rect(rect) + 3 * gap_UI, mid_Rect(rect).y - size.y / 2); 125 iInt2 pos = init_I2(left_Rect(rect) + 3 * gap_UI, mid_Rect(rect).y - size.y / 2);
126 if (d->listItem.isSeparator) { 126 if (d->listItem.isSeparator) {
127 pos.y = bottom_Rect(rect) - lineHeight_Text(d->font); 127 pos.y = bottom_Rect(rect) - lineHeight_Text(d->font);
diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c
index fa09b214..22924876 100644
--- a/src/ui/mediaui.c
+++ b/src/ui/mediaui.c
@@ -98,7 +98,7 @@ static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) {
98 appendChar_String(&num, ':'); 98 appendChar_String(&num, ':');
99 appendChar_String(&num, sevenSegmentDigit_ + (secs / 10) % 10); 99 appendChar_String(&num, sevenSegmentDigit_ + (secs / 10) % 10);
100 appendChar_String(&num, sevenSegmentDigit_ + (secs % 10)); 100 appendChar_String(&num, sevenSegmentDigit_ + (secs % 10));
101 iInt2 size = advanceRange_Text(font, range_String(&num)); 101 iInt2 size = measureRange_Text(font, range_String(&num)).bounds.size;
102 if (align == right_Alignment) { 102 if (align == right_Alignment) {
103 pos.x -= size.x; 103 pos.x -= size.x;
104 } 104 }
@@ -151,7 +151,7 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
151 drawHLine_Paint(p, init_I2(s1, yMid), part, bright); 151 drawHLine_Paint(p, init_I2(s1, yMid), part, bright);
152 drawHLine_Paint(p, init_I2(s1 + part, yMid), scrubMax - part, dim); 152 drawHLine_Paint(p, init_I2(s1 + part, yMid), scrubMax - part, dim);
153 const char *dot = "\u23fa"; 153 const char *dot = "\u23fa";
154 const int dotWidth = advance_Text(uiLabel_FontId, dot).x; 154 const int dotWidth = measure_Text(uiLabel_FontId, dot).advance.x;
155 draw_Text(uiLabel_FontId, 155 draw_Text(uiLabel_FontId,
156 init_I2(s1 * (1.0f - normPos) + s2 * normPos - dotWidth / 2, 156 init_I2(s1 * (1.0f - normPos) + s2 * normPos - dotWidth / 2,
157 yMid - lineHeight_Text(uiLabel_FontId) / 2), 157 yMid - lineHeight_Text(uiLabel_FontId) / 2),
@@ -223,7 +223,7 @@ static void drawSevenSegmentBytes_(iInt2 pos, int color, size_t numBytes) {
223 } 223 }
224 } 224 }
225 const int font = uiLabel_FontId; 225 const int font = uiLabel_FontId;
226 const iInt2 dims = advanceRange_Text(font, range_String(&digits)); 226 const iInt2 dims = measureRange_Text(font, range_String(&digits)).bounds.size;
227 drawRange_Text(font, addX_I2(pos, -dims.x), color, range_String(&digits)); 227 drawRange_Text(font, addX_I2(pos, -dims.x), color, range_String(&digits));
228 deinit_String(&digits); 228 deinit_String(&digits);
229} 229}
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 4c2a9d64..767847ac 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -618,7 +618,7 @@ static void updateMetrics_SidebarWidget_(iSidebarWidget *d) {
618 iMaxi(d->maxButtonLabelWidth, 618 iMaxi(d->maxButtonLabelWidth,
619 3 * gap_UI + measure_Text(font_LabelWidget(d->modeButtons[i]), 619 3 * gap_UI + measure_Text(font_LabelWidget(d->modeButtons[i]),
620 translateCStr_Lang(normalModeLabels_[i])) 620 translateCStr_Lang(normalModeLabels_[i]))
621 .x); 621 .bounds.size.x);
622 } 622 }
623 } 623 }
624 updateItemHeight_SidebarWidget_(d); 624 updateItemHeight_SidebarWidget_(d);
@@ -1672,8 +1672,8 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1672 } 1672 }
1673 /* Select the layout based on how the title fits. */ 1673 /* Select the layout based on how the title fits. */
1674 int metaFg = isPressing ? fg : uiSubheading_ColorId; 1674 int metaFg = isPressing ? fg : uiSubheading_ColorId;
1675 iInt2 titleSize = advanceRange_Text(titleFont, range_String(&d->label)); 1675 iInt2 titleSize = measureRange_Text(titleFont, range_String(&d->label)).bounds.size;
1676 const iInt2 metaSize = advanceRange_Text(uiLabel_FontId, range_String(&d->meta)); 1676 const iInt2 metaSize = measureRange_Text(uiLabel_FontId, range_String(&d->meta)).bounds.size;
1677 pos.x += iconPad; 1677 pos.x += iconPad;
1678 const int avail = width_Rect(itemRect) - iconPad - 3 * gap_UI; 1678 const int avail = width_Rect(itemRect) - iconPad - 3 * gap_UI;
1679 const int labelFg = isPressing ? fg : (isUnread ? uiTextStrong_ColorId : uiText_ColorId); 1679 const int labelFg = isPressing ? fg : (isUnread ? uiTextStrong_ColorId : uiText_ColorId);
@@ -1682,7 +1682,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1682 pos.y += (itemHeight - h2 - h2) / 2; 1682 pos.y += (itemHeight - h2 - h2) / 2;
1683 draw_Text( 1683 draw_Text(
1684 uiLabel_FontId, addY_I2(pos, h2 - h1 - gap_UI / 8), metaFg, "%s \u2014 ", cstr_String(&d->meta)); 1684 uiLabel_FontId, addY_I2(pos, h2 - h1 - gap_UI / 8), metaFg, "%s \u2014 ", cstr_String(&d->meta));
1685 int skip = metaSize.x + advance_Text(uiLabel_FontId, " \u2014 ").x; 1685 int skip = metaSize.x + measure_Text(uiLabel_FontId, " \u2014 ").advance.x;
1686 iInt2 cur = addX_I2(pos, skip); 1686 iInt2 cur = addX_I2(pos, skip);
1687 const char *endPos; 1687 const char *endPos;
1688 tryAdvance_Text(titleFont, range_String(&d->label), avail - skip, &endPos); 1688 tryAdvance_Text(titleFont, range_String(&d->label), avail - skip, &endPos);
diff --git a/src/ui/text.c b/src/ui/text.c
index 73b521fa..1c509735 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -776,7 +776,7 @@ struct Impl_AttributedRun {
776 iRangei visual; /* UTF-32 codepoint indices in the visual-order text */ 776 iRangei visual; /* UTF-32 codepoint indices in the visual-order text */
777 iFont * font; 777 iFont * font;
778 iColor fgColor; 778 iColor fgColor;
779 int lineBreaks; 779 iBool isLineBreak;
780}; 780};
781 781
782iDeclareType(AttributedText) 782iDeclareType(AttributedText)
@@ -819,7 +819,7 @@ static void finishRun_AttributedText_(iAttributedText *d, iAttributedRun *run, i
819 finishedRun.visual.end = endAt; 819 finishedRun.visual.end = endAt;
820 if (!isEmpty_Range(&finishedRun.visual)) { 820 if (!isEmpty_Range(&finishedRun.visual)) {
821 pushBack_Array(&d->runs, &finishedRun); 821 pushBack_Array(&d->runs, &finishedRun);
822 run->lineBreaks = 0; 822 run->isLineBreak = iFalse;
823 } 823 }
824 run->visual.start = endAt; 824 run->visual.start = endAt;
825} 825}
@@ -930,8 +930,10 @@ static void prepare_AttributedText_(iAttributedText *d) {
930 } 930 }
931 if (ch == '\n') { 931 if (ch == '\n') {
932 finishRun_AttributedText_(d, &run, pos); 932 finishRun_AttributedText_(d, &run, pos);
933 run.visual.start = pos + 1; 933 /* A separate run for the newline. */
934 run.lineBreaks++; 934 run.visual.start = pos;
935 run.isLineBreak = iTrue;
936 finishRun_AttributedText_(d, &run, pos + 1);
935 continue; 937 continue;
936 } 938 }
937 if (isControl_Char_(ch)) { 939 if (isControl_Char_(ch)) {
@@ -1131,12 +1133,11 @@ static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) {
1131 const iChar *visualText = constData_Array(&attrText.visual); 1133 const iChar *visualText = constData_Array(&attrText.visual);
1132 iConstForEach(Array, i, &attrText.runs) { 1134 iConstForEach(Array, i, &attrText.runs) {
1133 const iAttributedRun *run = i.value; 1135 const iAttributedRun *run = i.value;
1134 //for (const char *chPos = run->text.start; chPos != run->text.end; ) { 1136 if (run->isLineBreak) {
1137 continue;
1138 }
1135 for (int pos = run->visual.start; pos < run->visual.end; pos++) { 1139 for (int pos = run->visual.start; pos < run->visual.end; pos++) {
1136 const iChar ch = visualText[pos]; 1140 const iChar ch = visualText[pos];
1137// const char *oldPos = chPos;
1138// const iChar ch = nextChar_(&chPos, text.end);
1139// if (chPos == oldPos) break; /* don't get stuck */
1140 if (!isSpace_Char(ch) && !isControl_Char_(ch)) { 1141 if (!isSpace_Char(ch) && !isControl_Char_(ch)) {
1141 const uint32_t glyphIndex = glyphIndex_Font_(d, ch); 1142 const uint32_t glyphIndex = glyphIndex_Font_(d, ch);
1142 if (glyphIndex) { 1143 if (glyphIndex) {
@@ -1179,6 +1180,8 @@ struct Impl_RunArgs {
1179 int xposLimit; /* hard limit for wrapping */ 1180 int xposLimit; /* hard limit for wrapping */
1180 int xposLayoutBound; /* visible bound for layout purposes; does not affect wrapping */ 1181 int xposLayoutBound; /* visible bound for layout purposes; does not affect wrapping */
1181 int color; 1182 int color;
1183 /* TODO: Use TextMetrics output pointer instead of return value & cursorAdvance_out */
1184 iInt2 * cursorAdvance_out;
1182 const char ** continueFrom_out; 1185 const char ** continueFrom_out;
1183 int * runAdvance_out; 1186 int * runAdvance_out;
1184}; 1187};
@@ -1225,19 +1228,79 @@ float horizKern_Font_(iFont *d, uint32_t glyph1, uint32_t glyph2) {
1225} 1228}
1226 1229
1227#if defined (LAGRANGE_ENABLE_HARFBUZZ) 1230#if defined (LAGRANGE_ENABLE_HARFBUZZ)
1228static float glyphRunRightEdge_Font_(const iFont *d, const hb_glyph_position_t *glyphPos, size_t count) { 1231
1232iDeclareType(GlyphBuffer)
1233
1234struct Impl_GlyphBuffer {
1235 hb_buffer_t * hb;
1236 iFont * font;
1237 const iChar * visualText;
1238 hb_glyph_info_t * glyphInfo;
1239 hb_glyph_position_t *glyphPos;
1240 unsigned int glyphCount;
1241};
1242
1243static void init_GlyphBuffer_(iGlyphBuffer *d, iFont *font, const iChar *visualText) {
1244 d->hb = hb_buffer_create();
1245 d->font = font;
1246 d->visualText = visualText;
1247 d->glyphInfo = NULL;
1248 d->glyphPos = NULL;
1249 d->glyphCount = 0;
1250}
1251
1252static void deinit_GlyphBuffer_(iGlyphBuffer *d) {
1253 hb_buffer_destroy(d->hb);
1254}
1255
1256static void shape_GlyphBuffer_(iGlyphBuffer *d) {
1257 if (!d->glyphInfo) {
1258 hb_shape(d->font->hbFont, d->hb, NULL, 0);
1259 d->glyphInfo = hb_buffer_get_glyph_infos(d->hb, &d->glyphCount);
1260 d->glyphPos = hb_buffer_get_glyph_positions(d->hb, &d->glyphCount);
1261 }
1262}
1263
1264static float advance_GlyphBuffer_(const iGlyphBuffer *d, iRangei wrapVisRange) {
1229 float x = 0.0f; 1265 float x = 0.0f;
1230 for (unsigned int i = 0; i < count; i++) { 1266 for (unsigned int i = 0; i < d->glyphCount; i++) {
1231 const float xAdvance = d->xScale * glyphPos[i].x_advance; 1267 const int visPos = d->glyphInfo[i].cluster;
1232 x += xAdvance; 1268 if (visPos < wrapVisRange.start) {
1269 continue;
1270 }
1271 if (visPos >= wrapVisRange.end) {
1272 break;
1273 }
1274 x += d->font->xScale * d->glyphPos[i].x_advance;
1275 if (i + 1 < d->glyphCount) {
1276 x += horizKern_Font_(d->font,
1277 d->glyphInfo[i].codepoint,
1278 d->glyphInfo[i + 1].codepoint);
1279 }
1233 } 1280 }
1234 return x; 1281 return x;
1235} 1282}
1236 1283
1284static void evenMonospaceAdvances_GlyphBuffer_(iGlyphBuffer *d, iFont *baseFont) {
1285 shape_GlyphBuffer_(d);
1286 const float monoAdvance = glyph_Font_(baseFont, 'M')->advance;
1287 for (unsigned int i = 0; i < d->glyphCount; ++i) {
1288 const hb_glyph_info_t *info = d->glyphInfo + i;
1289 if (d->glyphPos[i].x_advance > 0 && d->font != baseFont) {
1290 const iChar ch = d->visualText[info->cluster];
1291 if (isPictograph_Char(ch) || isEmoji_Char(ch)) {
1292 const float dw = d->font->xScale * d->glyphPos[i].x_advance - monoAdvance;
1293 d->glyphPos[i].x_offset -= dw / 2 / d->font->xScale - 1;
1294 d->glyphPos[i].x_advance -= dw / d->font->xScale - 1;
1295 }
1296 }
1297 }
1298}
1299
1237static iRect run_Font_(iFont *d, const iRunArgs *args) { 1300static iRect run_Font_(iFont *d, const iRunArgs *args) {
1238 const int mode = args->mode; 1301 const int mode = args->mode;
1239 iRect bounds = zero_Rect();
1240 const iInt2 orig = args->pos; 1302 const iInt2 orig = args->pos;
1303 iRect bounds = { orig, init_I2(0, d->height) };
1241 float xCursor = 0.0f; 1304 float xCursor = 0.0f;
1242 float yCursor = 0.0f; 1305 float yCursor = 0.0f;
1243 float xCursorMax = 0.0f; 1306 float xCursorMax = 0.0f;
@@ -1247,7 +1310,6 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1247 if (args->continueFrom_out) { 1310 if (args->continueFrom_out) {
1248 *args->continueFrom_out = args->text.end; 1311 *args->continueFrom_out = args->text.end;
1249 } 1312 }
1250 hb_buffer_t *hbBuf = hb_buffer_create();
1251 /* Split the text into a number of attributed runs that specify exactly which 1313 /* Split the text into a number of attributed runs that specify exactly which
1252 font is used and other attributes such as color. (HarfBuzz shaping is done 1314 font is used and other attributes such as color. (HarfBuzz shaping is done
1253 with one specific font.) */ 1315 with one specific font.) */
@@ -1259,64 +1321,171 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1259 /* Initialize the wrap range. */ 1321 /* Initialize the wrap range. */
1260 args->wrap->wrapRange_ = args->text; 1322 args->wrap->wrapRange_ = args->text;
1261 } 1323 }
1262 int wrapResumePos = -1; 1324 const iChar *visualText = constData_Array(&attrText.visual);
1325 const size_t runCount = size_Array(&attrText.runs);
1326 iArray buffers;
1327 init_Array(&buffers, sizeof(iGlyphBuffer));
1328 resize_Array(&buffers, runCount);
1329 /* Prepare the HarfBuzz buffers. They will be lazily shaped when needed. */
1330 iConstForEach(Array, i, &attrText.runs) {
1331 const iAttributedRun *run = i.value;
1332 iGlyphBuffer *buf = at_Array(&buffers, index_ArrayConstIterator(&i));
1333 init_GlyphBuffer_(buf, run->font, visualText);
1334 for (int pos = run->visual.start; pos < run->visual.end; pos++) {
1335 hb_buffer_add(buf->hb, visualText[pos], pos);
1336 }
1337 hb_buffer_set_content_type(buf->hb, HB_BUFFER_CONTENT_TYPE_UNICODE);
1338 hb_buffer_set_direction(buf->hb, HB_DIRECTION_LTR);
1339 /* hb_buffer_set_script(hbBuf, HB_SCRIPT_LATIN); */ /* will be autodetected */
1340 }
1341 if (isMonospaced) {
1342 /* Fit borrowed glyphs into the expected monospacing. */
1343 for (size_t runIndex = 0; runIndex < runCount; runIndex++) {
1344 evenMonospaceAdvances_GlyphBuffer_(at_Array(&buffers, runIndex), d);
1345 }
1346 }
1263 iBool willAbortDueToWrap = iFalse; 1347 iBool willAbortDueToWrap = iFalse;
1264 const iRangecc sourceText = attrText.source; 1348 const iRangecc sourceText = attrText.source;
1265 const iChar * visualText = constData_Array(&attrText.visual);
1266 const int * visToSource = constData_Array(&attrText.visualToSourceOffset); 1349 const int * visToSource = constData_Array(&attrText.visualToSourceOffset);
1267 for (size_t runIndex = 0; runIndex < size_Array(&attrText.runs); runIndex++) { 1350 const size_t visSize = size_Array(&attrText.visual);
1268 const iAttributedRun *run = at_Array(&attrText.runs, runIndex); 1351 iRanges wrapRuns = { 0, runCount };
1269 iRangei runVisual = run->visual; 1352 iRangei wrapVisRange = { 0, visSize };
1270 if (wrapResumePos >= 0) { 1353 int wrapResumePos = visSize;
1271 xCursor = 0.0f; 1354 size_t wrapResumeRunIndex = runCount;
1272 yCursor += d->height; 1355 while (!isEmpty_Range(&wrapRuns)) {
1273 runVisual.start = wrapResumePos; 1356 float wrapAdvance = 0.0f;
1274 wrapResumePos = NULL; 1357 if (args->wrap && args->wrap->maxWidth > 0) {
1275 } 1358 iAssert(wrapVisRange.end == visSize);
1276 else if (run->lineBreaks) { 1359 /* Determine ends of wrapRuns and wrapVisRange. */
1277 for (int i = 0; i < run->lineBreaks; i++) { 1360 for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) {
1278 /* One callback for each "wrapped" line even if they're empty. */ 1361 const iAttributedRun *run = at_Array(&attrText.runs, runIndex);
1279 if (!notify_WrapText_(args->wrap, 1362 if (run->isLineBreak) {
1280 sourcePtr_AttributedText_(&attrText, run->visual.start), 1363 wrapVisRange.end = run->visual.start;
1281 iRound(xCursor))) { 1364 wrapResumePos = run->visual.end;
1365 wrapRuns.end = runIndex;
1366 wrapResumeRunIndex = runIndex + 1;
1367 yCursor += d->height;
1282 break; 1368 break;
1283 } 1369 }
1284 xCursor = 0.0f; 1370 wrapResumeRunIndex = runCount;
1285 yCursor += d->height; 1371 wrapResumePos = visSize;
1372 iGlyphBuffer *buf = at_Array(&buffers, runIndex);
1373 iAssert(run->font == buf->font);
1374 shape_GlyphBuffer_(buf);
1375 int safeBreakPos = -1;
1376 iChar prevCh = 0;
1377 for (unsigned int i = 0; i < buf->glyphCount; i++) {
1378 const hb_glyph_info_t *info = &buf->glyphInfo[i];
1379 const hb_codepoint_t glyphId = info->codepoint;
1380 const int visPos = info->cluster;
1381 if (visPos < wrapVisRange.start) {
1382 continue;
1383 }
1384 const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId);
1385 const int glyphFlags = hb_glyph_info_get_glyph_flags(info);
1386 const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset;
1387 const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance;
1388 if (args->wrap->mode == word_WrapTextMode) {
1389 /* When word wrapping, only consider certain places breakable. */
1390 const iChar ch = visualText[info->cluster];
1391 if ((ch >= 128 || !ispunct(ch)) && (prevCh == '-' || prevCh == '/')) {
1392 safeBreakPos = visPos;
1393 // isSoftHyphenBreak = iFalse;
1394 }
1395 else if (isSpace_Char(ch)) {
1396 safeBreakPos = visPos;
1397 // isSoftHyphenBreak = iFalse;
1398 }
1399 prevCh = ch;
1400 }
1401 else {
1402 if (~glyphFlags & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) {
1403 safeBreakPos = visPos;
1404 }
1405 }
1406 /* Out of room? */
1407 if (wrapAdvance + xOffset + glyph->d[0].x + glyph->rect[0].size.x >
1408 args->wrap->maxWidth) {
1409 if (safeBreakPos >= 0) {
1410 wrapVisRange.end = safeBreakPos;
1411 }
1412 else {
1413 wrapVisRange.end = visPos;
1414 }
1415 wrapResumePos = wrapVisRange.end;
1416 while (wrapResumePos < visSize && isSpace_Char(visualText[wrapResumePos])) {
1417 wrapResumePos++; /* skip space */
1418 }
1419 wrapRuns.end = runIndex + 1; /* still includes this run */
1420 wrapResumeRunIndex = runIndex; /* ...but continue from the same one */
1421 break;
1422 }
1423 wrapAdvance += xAdvance;
1424 /* Additional kerning tweak. It would be better to use HarfBuzz font callbacks,
1425 but they don't seem to get called? */
1426 if (i + 1 < buf->glyphCount) {
1427 wrapAdvance += horizKern_Font_(buf->font,
1428 glyphId,
1429 buf->glyphInfo[i + 1].codepoint);
1430 }
1431 }
1286 } 1432 }
1287 } 1433 }
1288 const iRangecc runSource = sourceRange_AttributedText_(&attrText, runVisual); 1434 else {
1289 hb_buffer_clear_contents(hbBuf); 1435 /* Calculate total advance without wrapping. */
1290 iBool isRunRTL = iFalse; 1436 for (size_t i = wrapRuns.start; i < wrapRuns.end; i++) {
1291 /* Cluster values are used to determine offset inside the UTF-8 source string. */ 1437 wrapAdvance += advance_GlyphBuffer_(at_Array(&buffers, i), wrapVisRange);
1292 //hb_buffer_set_cluster_level(hbBuf, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); 1438 }
1293 for (int pos = runVisual.start; pos < runVisual.end; pos++) {
1294 hb_buffer_add(hbBuf, visualText[pos], pos);
1295 } 1439 }
1296 hb_buffer_set_content_type(hbBuf, HB_BUFFER_CONTENT_TYPE_UNICODE); 1440 /* Make a callback for each wrapped line. */
1297 hb_buffer_set_direction(hbBuf, HB_DIRECTION_LTR); 1441 if (!notify_WrapText_(args->wrap,
1298 /* hb_buffer_set_script(hbBuf, HB_SCRIPT_LATIN); */ /* will be autodetected */ 1442// sourcePtr_AttributedText_(&attrText, wrapVisRange.end),
1299 //hb_buffer_set_language(hbBuf, hb_language_from_string("en", -1)); /* TODO: language from document/UI, if known */ 1443 sourcePtr_AttributedText_(&attrText, wrapResumePos),
1300 hb_shape(run->font->hbFont, hbBuf, NULL, 0); /* TODO: Specify features, too? */ 1444 iRound(wrapAdvance))) {
1301 unsigned int glyphCount = 0; 1445 willAbortDueToWrap = iTrue;
1302 const hb_glyph_info_t * glyphInfo = hb_buffer_get_glyph_infos(hbBuf, &glyphCount); 1446 }
1303 hb_glyph_position_t * glyphPos = hb_buffer_get_glyph_positions(hbBuf, &glyphCount); 1447#if 0
1304 int breakPos = -1; 1448 if (wrapResumePos >= 0) {
1305// iBool isSoftHyphenBreak = iFalse; 1449 xCursor = 0.0f;
1306 /* Fit foreign glyphs into the expected monospacing. */ 1450 yCursor += d->height;
1307 if (isMonospaced) { 1451 runVisual.start = wrapResumePos;
1308 for (unsigned int i = 0; i < glyphCount; ++i) { 1452 wrapResumePos = NULL;
1309 const hb_glyph_info_t *info = &glyphInfo[i]; 1453 }
1310 if (glyphPos[i].x_advance > 0 && run->font != d) { 1454 else if (run->lineBreaks) {
1311 const iChar ch = visualText[info->cluster]; 1455 for (int i = 0; i < run->lineBreaks; i++) {
1312 if (isPictograph_Char(ch) || isEmoji_Char(ch)) { 1456 /* One callback for each "wrapped" line even if they're empty. */
1313 const float dw = run->font->xScale * glyphPos[i].x_advance - monoAdvance; 1457 if (!notify_WrapText_(args->wrap,
1314 glyphPos[i].x_offset -= dw / 2 / run->font->xScale - 1; 1458 sourcePtr_AttributedText_(&attrText, run->visual.start),
1315 glyphPos[i].x_advance -= dw / run->font->xScale - 1; 1459 iRound(xCursor))) {
1460 break;
1316 } 1461 }
1462 xCursor = 0.0f;
1463 yCursor += d->height;
1317 } 1464 }
1318 } 1465 }
1319 } 1466 const iRangecc runSource = sourceRange_AttributedText_(&attrText, runVisual);
1467 hb_buffer_clear_contents(hbBuf);
1468 iBool isRunRTL = attrText.bidiLevels != NULL;
1469 /* Cluster values are used to determine offset inside the UTF-8 source string. */
1470 //hb_buffer_set_cluster_level(hbBuf, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS);
1471 for (int pos = runVisual.start; pos < runVisual.end; pos++) {
1472 hb_buffer_add(hbBuf, visualText[pos], pos);
1473 if (attrText.bidiLevels) {
1474 const char lev = attrText.bidiLevels[pos];
1475 if (!FRIBIDI_IS_NEUTRAL(lev) && !FRIBIDI_IS_RTL(lev)) {
1476 isRunRTL = iFalse;
1477 }
1478 }
1479 }
1480 hb_buffer_set_content_type(hbBuf, HB_BUFFER_CONTENT_TYPE_UNICODE);
1481 hb_buffer_set_direction(hbBuf, HB_DIRECTION_LTR);
1482 /* hb_buffer_set_script(hbBuf, HB_SCRIPT_LATIN); */ /* will be autodetected */
1483 //hb_buffer_set_language(hbBuf, hb_language_from_string("en", -1)); /* TODO: language from document/UI, if known */
1484 hb_shape(run->font->hbFont, hbBuf, NULL, 0); /* TODO: Specify features, too? */
1485 unsigned int glyphCount = 0;
1486 const hb_glyph_info_t * glyphInfo = hb_buffer_get_glyph_infos(hbBuf, &glyphCount);
1487 hb_glyph_position_t * glyphPos = hb_buffer_get_glyph_positions(hbBuf, &glyphCount);
1488// iBool isSoftHyphenBreak = iFalse;
1320 /* Check if this run needs to be wrapped. If so, we'll draw the portion that fits on 1489 /* Check if this run needs to be wrapped. If so, we'll draw the portion that fits on
1321 the line, and re-run the loop resuming the run from the wrap point. */ 1490 the line, and re-run the loop resuming the run from the wrap point. */
1322 if (args->wrap && args->wrap->maxWidth > 0) { 1491 if (args->wrap && args->wrap->maxWidth > 0) {
@@ -1379,25 +1548,48 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1379 xCursor += args->xposLayoutBound - orig.x - 1548 xCursor += args->xposLayoutBound - orig.x -
1380 glyphRunRightEdge_Font_(run->font, glyphPos, glyphCount); 1549 glyphRunRightEdge_Font_(run->font, glyphPos, glyphCount);
1381 } 1550 }
1382 /* We have determined a possible wrap position inside the text run, so now we can 1551#endif
1383 draw the glyphs. */ 1552 /* TODO: Alignment. */
1384 for (unsigned int i = 0; i < glyphCount; i++) { 1553 xCursor = 0.0f;
1385 const hb_glyph_info_t *info = &glyphInfo[i]; 1554 /* We have determined a possible wrap position and alignment for the work runs,
1386 const hb_codepoint_t glyphId = info->codepoint; 1555 so now we can process the glyphs. */
1387 const int visPos = info->cluster; 1556 for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) {
1388 const float xOffset = run->font->xScale * glyphPos[i].x_offset; 1557 const iAttributedRun *run = at_Array(&attrText.runs, runIndex);
1389 const float yOffset = run->font->yScale * glyphPos[i].y_offset; 1558 if (run->isLineBreak) {
1390 const float xAdvance = run->font->xScale * glyphPos[i].x_advance; 1559 xCursor = 0.0f;
1391 const float yAdvance = run->font->yScale * glyphPos[i].y_advance; 1560 yCursor += d->height;
1392 const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId); 1561 continue;
1393 const float xf = xCursor + xOffset;
1394 const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0;
1395 if (visPos == breakPos) {
1396 runIndex--; /* stay on the same run */
1397 wrapResumePos = visPos;
1398 break;
1399 } 1562 }
1400 /* Draw the glyph. */ { 1563 iGlyphBuffer *buf = at_Array(&buffers, runIndex);
1564 shape_GlyphBuffer_(buf);
1565 iAssert(run->font == buf->font);
1566 iRangei runVisual = run->visual;
1567 /* Process all the glyphs. */
1568 for (unsigned int i = 0; i < buf->glyphCount; i++) {
1569 const hb_glyph_info_t *info = &buf->glyphInfo[i];
1570 const hb_codepoint_t glyphId = info->codepoint;
1571 const int visPos = info->cluster;
1572 if (visPos < wrapVisRange.start) {
1573 continue;
1574 }
1575 if (visPos >= wrapVisRange.end) {
1576 break;
1577 }
1578 const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset;
1579 const float yOffset = run->font->yScale * buf->glyphPos[i].y_offset;
1580 const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance;
1581 const float yAdvance = run->font->yScale * buf->glyphPos[i].y_advance;
1582 const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId);
1583 const float xf = xCursor + xOffset;
1584 const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0;
1585#if 0
1586 if (visPos == breakPos) {
1587 runIndex--; /* stay on the same run */
1588 wrapResumePos = visPos;
1589 break;
1590 }
1591#endif
1592 /* Output position for the glyph. */
1401 SDL_Rect dst = { orig.x + xCursor + xOffset + glyph->d[hoff].x, 1593 SDL_Rect dst = { orig.x + xCursor + xOffset + glyph->d[hoff].x,
1402 orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y, 1594 orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y,
1403 glyph->rect[hoff].size.x, 1595 glyph->rect[hoff].size.x,
@@ -1414,7 +1606,8 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1414 bounds.size.x = iMax(bounds.size.x, dst.x + dst.w - orig.x); 1606 bounds.size.x = iMax(bounds.size.x, dst.x + dst.w - orig.x);
1415 bounds.size.y = iMax(bounds.size.y, yCursor + glyph->font->height); 1607 bounds.size.y = iMax(bounds.size.y, yCursor + glyph->font->height);
1416 } 1608 }
1417 if (mode & draw_RunMode && visualText[visPos] != 0x20) { 1609 if (mode & draw_RunMode && visualText[visPos] > 0x20) {
1610 /* Draw the glyph. */
1418 if (!isRasterized_Glyph_(glyph, hoff)) { 1611 if (!isRasterized_Glyph_(glyph, hoff)) {
1419 cacheSingleGlyph_Font_(run->font, glyphId); /* may cause cache reset */ 1612 cacheSingleGlyph_Font_(run->font, glyphId); /* may cause cache reset */
1420 glyph = glyphByIndex_Font_(run->font, glyphId); 1613 glyph = glyphByIndex_Font_(run->font, glyphId);
@@ -1437,29 +1630,38 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1437 } 1630 }
1438 SDL_RenderCopy(text_.render, text_.cache, &src, &dst); 1631 SDL_RenderCopy(text_.render, text_.cache, &src, &dst);
1439 } 1632 }
1633 xCursor += xAdvance;
1634 yCursor += yAdvance;
1635 /* Additional kerning tweak. It would be better to use HarfBuzz font callbacks,
1636 but they don't seem to get called? */
1637 if (i + 1 < buf->glyphCount) {
1638 xCursor += horizKern_Font_(run->font, glyphId, buf->glyphInfo[i + 1].codepoint);
1639 }
1640 xCursorMax = iMax(xCursorMax, xCursor);
1440 } 1641 }
1441 xCursor += xAdvance;
1442 yCursor += yAdvance;
1443 /* Additional kerning tweak. It would be better to use HarfBuzz font callbacks,
1444 but they don't seem to get called? */
1445 if (i + 1 < glyphCount) {
1446 xCursor += horizKern_Font_(run->font, glyphId, glyphInfo[i + 1].codepoint);
1447 }
1448 xCursorMax = iMax(xCursorMax, xCursor);
1449 } 1642 }
1450 if (willAbortDueToWrap) { 1643 if (willAbortDueToWrap) {
1451 break; 1644 break;
1452 } 1645 }
1646 wrapRuns.start = wrapResumeRunIndex;
1647 wrapRuns.end = runCount;
1648 wrapVisRange.start = wrapResumePos;
1649 wrapVisRange.end = visSize;
1650 yCursor += d->height;
1453 } 1651 }
1454 if (args->wrap) { 1652 if (args->cursorAdvance_out) {
1455 args->wrap->cursor_out = init_I2(xCursor, yCursor); 1653 *args->cursorAdvance_out = init_I2(xCursor, yCursor);
1456 } 1654 }
1655#if 0
1457 /* The final, unbroken line. */ 1656 /* The final, unbroken line. */
1458 notify_WrapText_(args->wrap, NULL, xCursor); 1657 notify_WrapText_(args->wrap, NULL, xCursor);
1658#endif
1459 if (args->runAdvance_out) { 1659 if (args->runAdvance_out) {
1460 *args->runAdvance_out = xCursorMax; 1660 *args->runAdvance_out = xCursorMax;
1461 } 1661 }
1462 hb_buffer_destroy(hbBuf); 1662 iForEach(Array, b, &buffers) {
1663 deinit_GlyphBuffer_(b.value);
1664 }
1463 deinit_AttributedText(&attrText); 1665 deinit_AttributedText(&attrText);
1464 return bounds; 1666 return bounds;
1465} 1667}
@@ -1476,12 +1678,27 @@ int lineHeight_Text(int fontId) {
1476 return font_Text_(fontId)->height; 1678 return font_Text_(fontId)->height;
1477} 1679}
1478 1680
1681#if 0
1479iInt2 measureRange_Text(int fontId, iRangecc text) { 1682iInt2 measureRange_Text(int fontId, iRangecc text) {
1480 if (isEmpty_Range(&text)) { 1683 if (isEmpty_Range(&text)) {
1481 return init_I2(0, lineHeight_Text(fontId)); 1684 return init_I2(0, lineHeight_Text(fontId));
1482 } 1685 }
1483 return run_Font_(font_Text_(fontId), &(iRunArgs){ .mode = measure_RunMode, .text = text }).size; 1686 return run_Font_(font_Text_(fontId), &(iRunArgs){ .mode = measure_RunMode, .text = text }).size;
1484} 1687}
1688#endif
1689
1690iTextMetrics measureRange_Text(int fontId, iRangecc text) {
1691 if (isEmpty_Range(&text)) {
1692 return (iTextMetrics){ init_Rect(0, 0, 0, lineHeight_Text(fontId)), zero_I2() };
1693 }
1694 iTextMetrics tm;
1695 tm.bounds = run_Font_(font_Text_(fontId), &(iRunArgs){
1696 .mode = measure_RunMode,
1697 .text = text,
1698 .cursorAdvance_out = &tm.advance
1699 });
1700 return tm;
1701}
1485 1702
1486iRect visualBounds_Text(int fontId, iRangecc text) { 1703iRect visualBounds_Text(int fontId, iRangecc text) {
1487 return run_Font_(font_Text_(fontId), 1704 return run_Font_(font_Text_(fontId),
@@ -1491,9 +1708,11 @@ iRect visualBounds_Text(int fontId, iRangecc text) {
1491 }); 1708 });
1492} 1709}
1493 1710
1711#if 0
1494iInt2 measure_Text(int fontId, const char *text) { 1712iInt2 measure_Text(int fontId, const char *text) {
1495 return measureRange_Text(fontId, range_CStr(text)); 1713 return measureRange_Text(fontId, range_CStr(text));
1496} 1714}
1715#endif
1497 1716
1498void cache_Text(int fontId, iRangecc text) { 1717void cache_Text(int fontId, iRangecc text) {
1499 cacheTextGlyphs_Font_(font_Text_(fontId), text); 1718 cacheTextGlyphs_Font_(font_Text_(fontId), text);
@@ -1507,6 +1726,7 @@ static int runFlagsFromId_(enum iFontId fontId) {
1507 return runFlags; 1726 return runFlags;
1508} 1727}
1509 1728
1729#if 0
1510iInt2 advanceRange_Text(int fontId, iRangecc text) { 1730iInt2 advanceRange_Text(int fontId, iRangecc text) {
1511 int advance; 1731 int advance;
1512 const int height = run_Font_(font_Text_(fontId), 1732 const int height = run_Font_(font_Text_(fontId),
@@ -1516,19 +1736,22 @@ iInt2 advanceRange_Text(int fontId, iRangecc text) {
1516 .size.y; 1736 .size.y;
1517 return init_I2(advance, height); 1737 return init_I2(advance, height);
1518} 1738}
1739#endif
1519 1740
1520static iBool cbAdvanceOneLine_(iWrapText *d, iRangecc range, int advance) { 1741static iBool cbAdvanceOneLine_(iWrapText *d, iRangecc range, int advance) {
1521 *((const char **) d->context) = iMin(skipSpace_CStr(range.end), d->text.end); 1742 *((const char **) d->context) = range.end; // iMin(skipSpace_CStr(range.end), d->text.end);
1522 return iFalse; /* just one line */ 1743 return iFalse; /* just one line */
1523} 1744}
1524 1745
1525iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos) { 1746iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos) {
1526 iWrapText wrap = { .mode = word_WrapTextMode, 1747 /* FIXME: Get rid of this. Caller could use WrapText directly? */
1527 .text = text, .maxWidth = width, 1748 iWrapText wrap = { .mode = word_WrapTextMode,
1528 .wrapFunc = cbAdvanceOneLine_, .context = endPos }; 1749 .text = text,
1529 /* The return value is expected to be the horizontal/vertical bounds, while WrapText 1750 .maxWidth = width,
1530 returns the actual advanced cursor position. */ 1751 .wrapFunc = cbAdvanceOneLine_,
1531 return addY_I2(advance_WrapText(&wrap, fontId), lineHeight_Text(fontId)); 1752 .context = endPos };
1753 /* The return value is expected to be the horizontal/vertical bounds. */
1754 return measure_WrapText(&wrap, fontId).bounds.size;
1532 1755
1533#if 0 1756#if 0
1534 int advance; 1757 int advance;
@@ -1546,42 +1769,29 @@ iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos)
1546 1769
1547iInt2 tryAdvanceNoWrap_Text(int fontId, iRangecc text, int width, const char **endPos) { 1770iInt2 tryAdvanceNoWrap_Text(int fontId, iRangecc text, int width, const char **endPos) {
1548 /* TODO: "NoWrap" means words aren't wrapped; the line is broken at nearest character. */ 1771 /* TODO: "NoWrap" means words aren't wrapped; the line is broken at nearest character. */
1549 iWrapText wrap = { .mode = anyCharacter_WrapTextMode, 1772 iWrapText wrap = { .mode = anyCharacter_WrapTextMode,
1550 .text = text, .maxWidth = width, 1773 .text = text,
1551 .wrapFunc = cbAdvanceOneLine_, .context = endPos }; 1774 .maxWidth = width,
1552 const int x = advance_WrapText(&wrap, fontId).x; 1775 .wrapFunc = cbAdvanceOneLine_,
1553 return init_I2(x, lineHeight_Text(fontId)); 1776 .context = endPos };
1777 return measure_WrapText(&wrap, fontId).bounds.size;
1554} 1778}
1555 1779
1556iInt2 advance_WrapText(iWrapText *d, int fontId) { 1780iTextMetrics measureN_Text(int fontId, const char *text, size_t n) {
1557 run_Font_(font_Text_(fontId), 1781 if (n == 0) {
1558 &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId), 1782 return (iTextMetrics){ init_Rect(0, 0, 0, lineHeight_Text(fontId)),
1559 .text = d->text, 1783 zero_I2() };
1560 .wrap = d 1784 }
1561 }); 1785 iTextMetrics tm;
1562 return d->cursor_out; 1786 tm.bounds = run_Font_(font_Text_(fontId),
1563} 1787 &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId),
1564 1788 .text = range_CStr(text),
1565iRect measure_WrapText(iWrapText *d, int fontId) { 1789 .maxLen = n,
1566 return run_Font_(font_Text_(fontId), 1790 .cursorAdvance_out = &tm.advance });
1567 &(iRunArgs){ .mode = measure_RunMode | visualFlag_RunMode | runFlagsFromId_(fontId), 1791 return tm;
1568 .text = d->text,
1569 .wrap = d
1570 });
1571}
1572
1573void draw_WrapText(iWrapText *d, int fontId, iInt2 pos, int color) {
1574 run_Font_(font_Text_(fontId),
1575 &(iRunArgs){ .mode = draw_RunMode | runFlagsFromId_(fontId) |
1576 (color & permanent_ColorId ? permanentColorFlag_RunMode : 0) |
1577 (color & fillBackground_ColorId ? fillBackground_RunMode : 0),
1578 .text = d->text,
1579 .pos = pos,
1580 .wrap = d,
1581 .color = color,
1582 });
1583} 1792}
1584 1793
1794#if 0
1585iInt2 advance_Text(int fontId, const char *text) { 1795iInt2 advance_Text(int fontId, const char *text) {
1586 return advanceRange_Text(fontId, range_CStr(text)); 1796 return advanceRange_Text(fontId, range_CStr(text));
1587} 1797}
@@ -1591,13 +1801,14 @@ iInt2 advanceN_Text(int fontId, const char *text, size_t n) {
1591 return init_I2(0, lineHeight_Text(fontId)); 1801 return init_I2(0, lineHeight_Text(fontId));
1592 } 1802 }
1593 int advance; 1803 int advance;
1594 run_Font_(font_Text_(fontId), 1804 int height = run_Font_(font_Text_(fontId),
1595 &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId), 1805 &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId),
1596 .text = range_CStr(text), 1806 .text = range_CStr(text),
1597 .maxLen = n, 1807 .maxLen = n,
1598 .runAdvance_out = &advance }); 1808 .runAdvance_out = &advance }).size.y;
1599 return init_I2(advance, lineHeight_Text(fontId)); 1809 return init_I2(advance, height);
1600} 1810}
1811#endif
1601 1812
1602static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text, size_t maxLen) { 1813static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text, size_t maxLen) {
1603 iText *d = &text_; 1814 iText *d = &text_;
@@ -1633,10 +1844,10 @@ void drawAlign_Text(int fontId, iInt2 pos, int color, enum iAlignment align, con
1633 va_end(args); 1844 va_end(args);
1634 } 1845 }
1635 if (align == center_Alignment) { 1846 if (align == center_Alignment) {
1636 pos.x -= measure_Text(fontId, cstr_Block(&chars)).x / 2; 1847 pos.x -= measure_Text(fontId, cstr_Block(&chars)).bounds.size.x / 2;
1637 } 1848 }
1638 else if (align == right_Alignment) { 1849 else if (align == right_Alignment) {
1639 pos.x -= measure_Text(fontId, cstr_Block(&chars)).x; 1850 pos.x -= measure_Text(fontId, cstr_Block(&chars)).bounds.size.x;
1640 } 1851 }
1641 draw_Text_(fontId, pos, color, range_Block(&chars)); 1852 draw_Text_(fontId, pos, color, range_Block(&chars));
1642 deinit_Block(&chars); 1853 deinit_Block(&chars);
@@ -1678,6 +1889,12 @@ void drawOutline_Text(int fontId, iInt2 pos, int outlineColor, int fillColor, iR
1678 } 1889 }
1679} 1890}
1680 1891
1892iTextMetrics measureWrapRange_Text(int fontId, int maxWidth, iRangecc text) {
1893 iWrapText wrap = { .text = text, .maxWidth = maxWidth, .mode = word_WrapTextMode };
1894 return measure_WrapText(&wrap, fontId);
1895}
1896
1897#if 0
1681iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) { 1898iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) {
1682 iInt2 size = zero_I2(); 1899 iInt2 size = zero_I2();
1683 const char *endp; 1900 const char *endp;
@@ -1689,6 +1906,7 @@ iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) {
1689 } 1906 }
1690 return size; 1907 return size;
1691} 1908}
1909#endif
1692 1910
1693void drawBoundRange_Text(int fontId, iInt2 pos, int boundWidth, int color, iRangecc text) { 1911void drawBoundRange_Text(int fontId, iInt2 pos, int boundWidth, int color, iRangecc text) {
1694 /* This function is used together with text that has already been wrapped, so we'll know 1912 /* This function is used together with text that has already been wrapped, so we'll know
@@ -1746,12 +1964,44 @@ void drawCenteredOutline_Text(int fontId, iRect rect, iBool alignVisual, int out
1746 1964
1747void drawCenteredRange_Text(int fontId, iRect rect, iBool alignVisual, int color, iRangecc text) { 1965void drawCenteredRange_Text(int fontId, iRect rect, iBool alignVisual, int color, iRangecc text) {
1748 iRect textBounds = alignVisual ? visualBounds_Text(fontId, text) 1966 iRect textBounds = alignVisual ? visualBounds_Text(fontId, text)
1749 : (iRect){ zero_I2(), advanceRange_Text(fontId, text) }; 1967 : measureRange_Text(fontId, text).bounds;
1750 textBounds.pos = sub_I2(mid_Rect(rect), mid_Rect(textBounds)); 1968 textBounds.pos = sub_I2(mid_Rect(rect), mid_Rect(textBounds));
1751 textBounds.pos.x = iMax(textBounds.pos.x, left_Rect(rect)); /* keep left edge visible */ 1969 textBounds.pos.x = iMax(textBounds.pos.x, left_Rect(rect)); /* keep left edge visible */
1752 draw_Text_(fontId, textBounds.pos, color, text); 1970 draw_Text_(fontId, textBounds.pos, color, text);
1753} 1971}
1754 1972
1973#if 0
1974iInt2 advance_WrapText(iWrapText *d, int fontId) {
1975 const iRect bounds = run_Font_(font_Text_(fontId),
1976 &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId),
1977 .text = d->text,
1978 .wrap = d });
1979 return bounds.size;
1980}
1981#endif
1982
1983iTextMetrics measure_WrapText(iWrapText *d, int fontId) {
1984 iTextMetrics tm;
1985 tm.bounds = run_Font_(font_Text_(fontId),
1986 &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId),
1987 .text = d->text,
1988 .wrap = d,
1989 .cursorAdvance_out = &tm.advance });
1990 return tm;
1991}
1992
1993void draw_WrapText(iWrapText *d, int fontId, iInt2 pos, int color) {
1994 run_Font_(font_Text_(fontId),
1995 &(iRunArgs){ .mode = draw_RunMode | runFlagsFromId_(fontId) |
1996 (color & permanent_ColorId ? permanentColorFlag_RunMode : 0) |
1997 (color & fillBackground_ColorId ? fillBackground_RunMode : 0),
1998 .text = d->text,
1999 .pos = pos,
2000 .wrap = d,
2001 .color = color,
2002 });
2003}
2004
1755SDL_Texture *glyphCache_Text(void) { 2005SDL_Texture *glyphCache_Text(void) {
1756 return text_.cache; 2006 return text_.cache;
1757} 2007}
@@ -1868,7 +2118,7 @@ iDefineTypeConstructionArgs(TextBuf, (int font, int color, const char *text), fo
1868static void initWrap_TextBuf_(iTextBuf *d, int font, int color, int maxWidth, iBool doWrap, const char *text) { 2118static void initWrap_TextBuf_(iTextBuf *d, int font, int color, int maxWidth, iBool doWrap, const char *text) {
1869 SDL_Renderer *render = text_.render; 2119 SDL_Renderer *render = text_.render;
1870 if (maxWidth == 0) { 2120 if (maxWidth == 0) {
1871 d->size = advance_Text(font, text); 2121 d->size = measure_Text(font, text).bounds.size;
1872 } 2122 }
1873 else { 2123 else {
1874 d->size = zero_I2(); 2124 d->size = zero_I2();
diff --git a/src/ui/text.h b/src/ui/text.h
index ac9db405..0209e10a 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -148,13 +148,28 @@ void setContentFontSize_Text (float fontSizeFactor); /* affects all except `d
148void resetFonts_Text (void); 148void resetFonts_Text (void);
149 149
150int lineHeight_Text (int fontId); 150int lineHeight_Text (int fontId);
151iInt2 measure_Text (int fontId, const char *text); 151//iInt2 measure_Text (int fontId, const char *text);
152iInt2 measureRange_Text (int fontId, iRangecc text); 152//iInt2 measureRange_Text (int fontId, iRangecc text);
153iRect visualBounds_Text (int fontId, iRangecc text); 153iRect visualBounds_Text (int fontId, iRangecc text);
154iInt2 advance_Text (int fontId, const char *text); 154//iInt2 advance_Text (int fontId, const char *text);
155iInt2 advanceN_Text (int fontId, const char *text, size_t n); /* `n` in characters */ 155//iInt2 advanceN_Text (int fontId, const char *text, size_t n); /* `n` in characters */
156iInt2 advanceRange_Text (int fontId, iRangecc text); 156//iInt2 advanceRange_Text (int fontId, iRangecc text);
157iInt2 advanceWrapRange_Text (int fontId, int maxWidth, iRangecc text); 157//iInt2 advanceWrapRange_Text (int fontId, int maxWidth, iRangecc text);
158
159iDeclareType(TextMetrics)
160
161struct Impl_TextMetrics {
162 iRect bounds; /* logical bounds: multiples of line height, horiz. advance */
163 iInt2 advance; /* cursor offset */
164};
165
166iTextMetrics measureRange_Text (int fontId, iRangecc text);
167iTextMetrics measureWrapRange_Text (int fontId, int maxWidth, iRangecc text);
168iTextMetrics measureN_Text (int fontId, const char *text, size_t n); /* `n` in characters */
169
170iLocalDef iTextMetrics measure_Text(int fontId, const char *text) {
171 return measureRange_Text(fontId, range_CStr(text));
172}
158 173
159iInt2 tryAdvance_Text (int fontId, iRangecc text, int width, const char **endPos); 174iInt2 tryAdvance_Text (int fontId, iRangecc text, int width, const char **endPos);
160iInt2 tryAdvanceNoWrap_Text (int fontId, iRangecc text, int width, const char **endPos); 175iInt2 tryAdvanceNoWrap_Text (int fontId, iRangecc text, int width, const char **endPos);
@@ -196,15 +211,12 @@ struct Impl_WrapText {
196 enum iWrapTextMode mode; 211 enum iWrapTextMode mode;
197 iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, int advance); 212 iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, int advance);
198 void * context; 213 void * context;
199 /* output */
200 iInt2 cursor_out;
201 /* internal */ 214 /* internal */
202 iRangecc wrapRange_; 215 iRangecc wrapRange_;
203}; 216};
204 217
205iRect measure_WrapText (iWrapText *, int fontId); 218iTextMetrics measure_WrapText (iWrapText *, int fontId);
206iInt2 advance_WrapText (iWrapText *, int fontId); 219void draw_WrapText (iWrapText *, int fontId, iInt2 pos, int color);
207void draw_WrapText (iWrapText *, int fontId, iInt2 pos, int color);
208 220
209SDL_Texture * glyphCache_Text (void); 221SDL_Texture * glyphCache_Text (void);
210 222
diff --git a/src/ui/translation.c b/src/ui/translation.c
index 88edc48b..3ffa961b 100644
--- a/src/ui/translation.c
+++ b/src/ui/translation.c
@@ -80,7 +80,7 @@ void init_TranslationProgressWidget(iTranslationProgressWidget *d) {
80 spr->color = 0; 80 spr->color = 0;
81 init_String(&spr->text); 81 init_String(&spr->text);
82 appendChar_String(&spr->text, chars[i]); 82 appendChar_String(&spr->text, chars[i]);
83 spr->xoff = (width - advanceRange_Text(d->font, range_String(&spr->text)).x) / 2; 83 spr->xoff = (width - measureRange_Text(d->font, range_String(&spr->text)).advance.x) / 2;
84 spr->size = init_I2(width, lineHeight_Text(d->font)); 84 spr->size = init_I2(width, lineHeight_Text(d->font));
85 x += width + gap; 85 x += width + gap;
86 } 86 }
diff --git a/src/ui/util.c b/src/ui/util.c
index e0b05a44..570bae85 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1648,9 +1648,9 @@ iWidget *makePreferences_Widget(void) {
1648 size_t widestPos = iInvalidPos; 1648 size_t widestPos = iInvalidPos;
1649 iConstForEach(Array, i, uiLangs) { 1649 iConstForEach(Array, i, uiLangs) {
1650 const int width = 1650 const int width =
1651 advance_Text(uiLabel_FontId, 1651 measure_Text(uiLabel_FontId,
1652 translateCStr_Lang(((const iMenuItem *) i.value)->label)) 1652 translateCStr_Lang(((const iMenuItem *) i.value)->label))
1653 .x; 1653 .advance.x;
1654 if (widestPos == iInvalidPos || width > widest) { 1654 if (widestPos == iInvalidPos || width > widest) {
1655 widest = width; 1655 widest = width;
1656 widestPos = index_ArrayConstIterator(&i); 1656 widestPos = index_ArrayConstIterator(&i);