From e1ff98965a4aec72418165424a0183e596df1ecb Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 6 Jul 2021 07:04:29 +0300 Subject: 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. --- src/gmdocument.c | 10 +- src/ui/documentwidget.c | 30 +-- src/ui/inputwidget.c | 17 +- src/ui/labelwidget.c | 8 +- src/ui/lookupwidget.c | 2 +- src/ui/mediaui.c | 6 +- src/ui/sidebarwidget.c | 8 +- src/ui/text.c | 524 +++++++++++++++++++++++++++++++++++------------- src/ui/text.h | 34 +++- src/ui/translation.c | 2 +- src/ui/util.c | 4 +- 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 } contents->end = line.end; } - return measureRange_Text(font, *contents); + return measureRange_Text(font, *contents).bounds.size; } static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) { @@ -483,7 +483,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { if (meta.pixelRect.size.x > d->size.x - (enableIndents ? indents[preformatted_GmLineType] : 0) * gap_Text) { preFont = preformattedSmall_FontId; - meta.pixelRect.size = measureRange_Text(preFont, meta.contents); + meta.pixelRect.size = measureRange_Text(preFont, meta.contents).bounds.size; } trimLine_Rangecc(&line, type, isNormalized); meta.altText = line; /* without the ``` */ @@ -614,8 +614,8 @@ static void doLayout_GmDocument_(iGmDocument *d) { altText.color = tmQuote_ColorId; altText.text = isBlank ? range_Lang(range_CStr("doc.pre.nocaption")) : meta->altText; - iInt2 size = advanceWrapRange_Text(altText.font, d->size.x - 2 * margin.x, - altText.text); + iInt2 size = measureWrapRange_Text(altText.font, d->size.x - 2 * margin.x, + altText.text).bounds.size; altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x, size.y + 2 * margin.y); altText.preId = preId; @@ -661,7 +661,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { quoteRun.text = range_CStr(quote); quoteRun.color = tmQuoteIcon_ColorId; iRect vis = visualBounds_Text(quoteRun.font, quoteRun.text); - quoteRun.visBounds.size = advance_Text(quoteRun.font, quote); + quoteRun.visBounds.size = measure_Text(quoteRun.font, quote).bounds.size; quoteRun.visBounds.pos = add_I2(pos, 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) { prependChar_String(text, siteIcon); prependCStr_String(text, escape_Color(uiIcon_ColorId)); } - const int width = advanceRange_Text(default_FontId, range_String(text)).x; + const int width = measureRange_Text(default_FontId, range_String(text)).advance.x; if (width <= avail || isEmpty_StringArray(title)) { updateText_LabelWidget(tabButton, text); @@ -953,7 +953,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { const char *endPos; tryAdvanceNoWrap_Text(default_FontId, range_String(text), - avail - advance_Text(default_FontId, "...").x, + avail - measure_Text(default_FontId, "...").advance.x, &endPos); updateText_LabelWidget( tabButton, @@ -3898,17 +3898,17 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol contains_Range(&mark, run->text.start))) { int x = 0; if (!*isInside) { - x = advanceRange_Text(run->font, + x = measureRange_Text(run->font, (iRangecc){ run->text.start, iMax(run->text.start, mark.start) }) - .x; + .advance.x; } int w = width_Rect(run->visBounds) - x; if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) { - w = advanceRange_Text( + w = measureRange_Text( run->font, !*isInside ? mark : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) }) - .x; + .advance.x; *isInside = iFalse; } else { @@ -4013,8 +4013,8 @@ static void drawBannerRun_DrawContext_(iDrawContext *d, const iGmRun *run, iInt2 appendCStr_String(&str, "\n"); appendCStr_String(&str, cstr_Lang("dlg.certwarn.domain.expired")); } - const iInt2 dims = advanceWrapRange_Text( - uiContent_FontId, width_Rect(rect) - 16 * gap_UI, range_String(&str)); + const iInt2 dims = measureWrapRange_Text( + uiContent_FontId, width_Rect(rect) - 16 * gap_UI, range_String(&str)).bounds.size; const int warnHeight = run->visBounds.size.y - domainHeight; const int yOff = (lineHeight_Text(uiLabelLarge_FontId) - lineHeight_Text(uiContent_FontId)) / 2; @@ -4224,7 +4224,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { appendFormat_String( &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); } - const iInt2 size = measureRange_Text(metaFont, range_String(&text)); + const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size; fillRect_Paint( &d->paint, (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) { append_String(&str, collect_String(format_Date(&date, "%b %d"))); } if (!isEmpty_String(&str)) { - const iInt2 textSize = measure_Text(metaFont, cstr_String(&str)); + const iInt2 textSize = measure_Text(metaFont, cstr_String(&str)).bounds.size; int tx = topRight_Rect(linkRect).x; const char *msg = cstr_String(&str); if (tx + textSize.x > right_Rect(d->widgetBounds)) { @@ -4301,7 +4301,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { fillRect_Paint(&d->paint, (iRect){ init_I2(tx, top_Rect(linkRect)), textSize }, uiBackground_ColorId); msg += 4; /* skip the space and dash */ - tx += measure_Text(metaFont, " \u2014").x / 2; + tx += measure_Text(metaFont, " \u2014").advance.x / 2; } drawAlign_Text(metaFont, init_I2(tx, top_Rect(linkRect)), @@ -4359,8 +4359,8 @@ static void updateSideIconBuf_DocumentWidget_(const iDocumentWidget *d) { /* Determine the required size. */ iInt2 bufSize = init1_I2(minBannerSize); if (isHeadingVisible) { - const iInt2 headingSize = advanceWrapRange_Text(heading3_FontId, avail, - currentHeading_DocumentWidget_(d)); + const iInt2 headingSize = measureWrapRange_Text(heading3_FontId, avail, + currentHeading_DocumentWidget_(d)).bounds.size; if (headingSize.x > 0) { bufSize.y += gap_Text + headingSize.y; bufSize.x = iMax(bufSize.x, headingSize.x); @@ -4736,7 +4736,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { if (prefs_App()->hoverLink && d->hoverLink) { const int font = uiLabel_FontId; const iRangecc linkUrl = range_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId)); - const iInt2 size = measureRange_Text(font, linkUrl); + const iInt2 size = measureRange_Text(font, linkUrl).bounds.size; const iRect linkRect = { addY_I2(bottomLeft_Rect(bounds), -size.y), addX_I2(size, 2 * gap_UI) }; fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId); @@ -4757,7 +4757,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { const int wrap = docBounds.size.x - 2 * margin; iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos), -pos_SmoothScroll(&d->scrollY)); - const iInt2 textSize = advanceWrapRange_Text(altFont, wrap, meta->altText); + const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size; pos.y -= textSize.y + gap_UI; pos.y = iMax(pos.y, top_Rect(bounds)); 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) { int extraHeight = (flags_Widget(as_Widget(d)) & extraPadding_WidgetFlag ? extraPaddingHeight_ : 0); setFixedSize_Widget( as_Widget(d), - add_I2(measure_Text(d->font, cstr_Block(content)), + add_I2(measure_Text(d->font, cstr_Block(content)).bounds.size, init_I2(6 * gap_UI + d->leftPadding + d->rightPadding, 2 * gap_UI + extraHeight))); delete_Block(content); @@ -747,7 +747,7 @@ static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir) { const iInputLine *line = line_InputWidget_(d, d->cursorLine); - int xPos = advanceN_Text(d->font, cstr_String(&line->text), d->cursor - line->offset).x; + int xPos = measureN_Text(d->font, cstr_String(&line->text), d->cursor - line->offset).advance.x; size_t newCursor = iInvalidPos; const size_t numLines = size_Array(&d->lines); if (dir < 0 && d->cursorLine > 0) { @@ -1400,14 +1400,14 @@ static void draw_InputWidget_(const iInputWidget *d) { /* Draw the selected range. */ const iRanges mark = mark_InputWidget_(d); if (mark.start < lineRange.end && mark.end > lineRange.start) { - const int m1 = advanceN_Text(d->font, + const int m1 = measureN_Text(d->font, cstr_String(&line->text), iMax(lineRange.start, mark.start) - line->offset) - .x; - const int m2 = advanceN_Text(d->font, + .advance.x; + const int m2 = measureN_Text(d->font, cstr_String(&line->text), iMin(lineRange.end, mark.end) - line->offset) - .x; + .advance.x; fillRect_Paint(&p, (iRect){ addX_I2(drawPos, iMin(m1, m2)), init_I2(iMax(gap_UI / 3, iAbs(m2 - m1)), @@ -1437,7 +1437,8 @@ static void draw_InputWidget_(const iInputWidget *d) { else { initCStr_String(&cur, " "); } - curSize = addX_I2(advance_Text(d->font, cstr_String(&cur)), iMin(2, gap_UI / 4)); + curSize = addX_I2(measure_Text(d->font, cstr_String(&cur)).bounds.size, + iMin(2, gap_UI / 4)); } else { /* Bar cursor. */ @@ -1447,7 +1448,7 @@ static void draw_InputWidget_(const iInputWidget *d) { const iString * text = &curLine->text; /* The `gap_UI` offsets below are a hack. They are used because for some reason the cursor rect and the glyph inside don't quite position like during `run_Text_()`. */ - const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(text), d->cursor - curLine->offset); + const iInt2 prefixSize = measureN_Text(d->font, cstr_String(text), d->cursor - curLine->offset).bounds.size; const iInt2 curPos = addX_I2(addY_I2(contentBounds.pos, lineHeight_Text(d->font) * d->cursorLine), prefixSize.x + (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) { if (d->flags.wrap) { if (flags_Widget(w) & fixedHeight_WidgetFlag) { /* Calculate a new height based on the wrapping. */ - w->rect.size.y = advanceWrapRange_Text( + w->rect.size.y = measureWrapRange_Text( d->font, innerBounds_Widget(w).size.x, range_String(&d->label)) - .y; + .bounds.size.y; } } } @@ -386,13 +386,13 @@ static void sizeChanged_LabelWidget_(iLabelWidget *d) { iInt2 defaultSize_LabelWidget(const iLabelWidget *d) { const iWidget *w = constAs_Widget(d); const int64_t flags = flags_Widget(w); - iInt2 size = add_I2(measure_Text(d->font, cstr_String(&d->label)), + iInt2 size = add_I2(measure_Text(d->font, cstr_String(&d->label)).bounds.size, add_I2(padding_LabelWidget_(d, 0), padding_LabelWidget_(d, 2))); if ((flags & drawKey_WidgetFlag) && d->key) { iString str; init_String(&str); keyStr_LabelWidget_(d, &str); - size.x += 2 * gap_UI + measure_Text(uiShortcuts_FontId, cstr_String(&str)).x; + size.x += 2 * gap_UI + measure_Text(uiShortcuts_FontId, cstr_String(&str)).bounds.size.x; deinit_String(&str); } 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 ? permanent_ColorId | (isPressing || isCursor ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId) : d->fg; - const iInt2 size = measureRange_Text(d->font, range_String(&d->text)); + const iInt2 size = measureRange_Text(d->font, range_String(&d->text)).bounds.size; iInt2 pos = init_I2(left_Rect(rect) + 3 * gap_UI, mid_Rect(rect).y - size.y / 2); if (d->listItem.isSeparator) { 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) { appendChar_String(&num, ':'); appendChar_String(&num, sevenSegmentDigit_ + (secs / 10) % 10); appendChar_String(&num, sevenSegmentDigit_ + (secs % 10)); - iInt2 size = advanceRange_Text(font, range_String(&num)); + iInt2 size = measureRange_Text(font, range_String(&num)).bounds.size; if (align == right_Alignment) { pos.x -= size.x; } @@ -151,7 +151,7 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) { drawHLine_Paint(p, init_I2(s1, yMid), part, bright); drawHLine_Paint(p, init_I2(s1 + part, yMid), scrubMax - part, dim); const char *dot = "\u23fa"; - const int dotWidth = advance_Text(uiLabel_FontId, dot).x; + const int dotWidth = measure_Text(uiLabel_FontId, dot).advance.x; draw_Text(uiLabel_FontId, init_I2(s1 * (1.0f - normPos) + s2 * normPos - dotWidth / 2, yMid - lineHeight_Text(uiLabel_FontId) / 2), @@ -223,7 +223,7 @@ static void drawSevenSegmentBytes_(iInt2 pos, int color, size_t numBytes) { } } const int font = uiLabel_FontId; - const iInt2 dims = advanceRange_Text(font, range_String(&digits)); + const iInt2 dims = measureRange_Text(font, range_String(&digits)).bounds.size; drawRange_Text(font, addX_I2(pos, -dims.x), color, range_String(&digits)); deinit_String(&digits); } 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) { iMaxi(d->maxButtonLabelWidth, 3 * gap_UI + measure_Text(font_LabelWidget(d->modeButtons[i]), translateCStr_Lang(normalModeLabels_[i])) - .x); + .bounds.size.x); } } updateItemHeight_SidebarWidget_(d); @@ -1672,8 +1672,8 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect, } /* Select the layout based on how the title fits. */ int metaFg = isPressing ? fg : uiSubheading_ColorId; - iInt2 titleSize = advanceRange_Text(titleFont, range_String(&d->label)); - const iInt2 metaSize = advanceRange_Text(uiLabel_FontId, range_String(&d->meta)); + iInt2 titleSize = measureRange_Text(titleFont, range_String(&d->label)).bounds.size; + const iInt2 metaSize = measureRange_Text(uiLabel_FontId, range_String(&d->meta)).bounds.size; pos.x += iconPad; const int avail = width_Rect(itemRect) - iconPad - 3 * gap_UI; 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, pos.y += (itemHeight - h2 - h2) / 2; draw_Text( uiLabel_FontId, addY_I2(pos, h2 - h1 - gap_UI / 8), metaFg, "%s \u2014 ", cstr_String(&d->meta)); - int skip = metaSize.x + advance_Text(uiLabel_FontId, " \u2014 ").x; + int skip = metaSize.x + measure_Text(uiLabel_FontId, " \u2014 ").advance.x; iInt2 cur = addX_I2(pos, skip); const char *endPos; 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 { iRangei visual; /* UTF-32 codepoint indices in the visual-order text */ iFont * font; iColor fgColor; - int lineBreaks; + iBool isLineBreak; }; iDeclareType(AttributedText) @@ -819,7 +819,7 @@ static void finishRun_AttributedText_(iAttributedText *d, iAttributedRun *run, i finishedRun.visual.end = endAt; if (!isEmpty_Range(&finishedRun.visual)) { pushBack_Array(&d->runs, &finishedRun); - run->lineBreaks = 0; + run->isLineBreak = iFalse; } run->visual.start = endAt; } @@ -930,8 +930,10 @@ static void prepare_AttributedText_(iAttributedText *d) { } if (ch == '\n') { finishRun_AttributedText_(d, &run, pos); - run.visual.start = pos + 1; - run.lineBreaks++; + /* A separate run for the newline. */ + run.visual.start = pos; + run.isLineBreak = iTrue; + finishRun_AttributedText_(d, &run, pos + 1); continue; } if (isControl_Char_(ch)) { @@ -1131,12 +1133,11 @@ static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) { const iChar *visualText = constData_Array(&attrText.visual); iConstForEach(Array, i, &attrText.runs) { const iAttributedRun *run = i.value; - //for (const char *chPos = run->text.start; chPos != run->text.end; ) { + if (run->isLineBreak) { + continue; + } for (int pos = run->visual.start; pos < run->visual.end; pos++) { const iChar ch = visualText[pos]; -// const char *oldPos = chPos; -// const iChar ch = nextChar_(&chPos, text.end); -// if (chPos == oldPos) break; /* don't get stuck */ if (!isSpace_Char(ch) && !isControl_Char_(ch)) { const uint32_t glyphIndex = glyphIndex_Font_(d, ch); if (glyphIndex) { @@ -1179,6 +1180,8 @@ struct Impl_RunArgs { int xposLimit; /* hard limit for wrapping */ int xposLayoutBound; /* visible bound for layout purposes; does not affect wrapping */ int color; + /* TODO: Use TextMetrics output pointer instead of return value & cursorAdvance_out */ + iInt2 * cursorAdvance_out; const char ** continueFrom_out; int * runAdvance_out; }; @@ -1225,19 +1228,79 @@ float horizKern_Font_(iFont *d, uint32_t glyph1, uint32_t glyph2) { } #if defined (LAGRANGE_ENABLE_HARFBUZZ) -static float glyphRunRightEdge_Font_(const iFont *d, const hb_glyph_position_t *glyphPos, size_t count) { + +iDeclareType(GlyphBuffer) + +struct Impl_GlyphBuffer { + hb_buffer_t * hb; + iFont * font; + const iChar * visualText; + hb_glyph_info_t * glyphInfo; + hb_glyph_position_t *glyphPos; + unsigned int glyphCount; +}; + +static void init_GlyphBuffer_(iGlyphBuffer *d, iFont *font, const iChar *visualText) { + d->hb = hb_buffer_create(); + d->font = font; + d->visualText = visualText; + d->glyphInfo = NULL; + d->glyphPos = NULL; + d->glyphCount = 0; +} + +static void deinit_GlyphBuffer_(iGlyphBuffer *d) { + hb_buffer_destroy(d->hb); +} + +static void shape_GlyphBuffer_(iGlyphBuffer *d) { + if (!d->glyphInfo) { + hb_shape(d->font->hbFont, d->hb, NULL, 0); + d->glyphInfo = hb_buffer_get_glyph_infos(d->hb, &d->glyphCount); + d->glyphPos = hb_buffer_get_glyph_positions(d->hb, &d->glyphCount); + } +} + +static float advance_GlyphBuffer_(const iGlyphBuffer *d, iRangei wrapVisRange) { float x = 0.0f; - for (unsigned int i = 0; i < count; i++) { - const float xAdvance = d->xScale * glyphPos[i].x_advance; - x += xAdvance; + for (unsigned int i = 0; i < d->glyphCount; i++) { + const int visPos = d->glyphInfo[i].cluster; + if (visPos < wrapVisRange.start) { + continue; + } + if (visPos >= wrapVisRange.end) { + break; + } + x += d->font->xScale * d->glyphPos[i].x_advance; + if (i + 1 < d->glyphCount) { + x += horizKern_Font_(d->font, + d->glyphInfo[i].codepoint, + d->glyphInfo[i + 1].codepoint); + } } return x; } +static void evenMonospaceAdvances_GlyphBuffer_(iGlyphBuffer *d, iFont *baseFont) { + shape_GlyphBuffer_(d); + const float monoAdvance = glyph_Font_(baseFont, 'M')->advance; + for (unsigned int i = 0; i < d->glyphCount; ++i) { + const hb_glyph_info_t *info = d->glyphInfo + i; + if (d->glyphPos[i].x_advance > 0 && d->font != baseFont) { + const iChar ch = d->visualText[info->cluster]; + if (isPictograph_Char(ch) || isEmoji_Char(ch)) { + const float dw = d->font->xScale * d->glyphPos[i].x_advance - monoAdvance; + d->glyphPos[i].x_offset -= dw / 2 / d->font->xScale - 1; + d->glyphPos[i].x_advance -= dw / d->font->xScale - 1; + } + } + } +} + static iRect run_Font_(iFont *d, const iRunArgs *args) { const int mode = args->mode; - iRect bounds = zero_Rect(); const iInt2 orig = args->pos; + iRect bounds = { orig, init_I2(0, d->height) }; float xCursor = 0.0f; float yCursor = 0.0f; float xCursorMax = 0.0f; @@ -1247,7 +1310,6 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { if (args->continueFrom_out) { *args->continueFrom_out = args->text.end; } - hb_buffer_t *hbBuf = hb_buffer_create(); /* Split the text into a number of attributed runs that specify exactly which font is used and other attributes such as color. (HarfBuzz shaping is done with one specific font.) */ @@ -1259,64 +1321,171 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { /* Initialize the wrap range. */ args->wrap->wrapRange_ = args->text; } - int wrapResumePos = -1; + const iChar *visualText = constData_Array(&attrText.visual); + const size_t runCount = size_Array(&attrText.runs); + iArray buffers; + init_Array(&buffers, sizeof(iGlyphBuffer)); + resize_Array(&buffers, runCount); + /* Prepare the HarfBuzz buffers. They will be lazily shaped when needed. */ + iConstForEach(Array, i, &attrText.runs) { + const iAttributedRun *run = i.value; + iGlyphBuffer *buf = at_Array(&buffers, index_ArrayConstIterator(&i)); + init_GlyphBuffer_(buf, run->font, visualText); + for (int pos = run->visual.start; pos < run->visual.end; pos++) { + hb_buffer_add(buf->hb, visualText[pos], pos); + } + hb_buffer_set_content_type(buf->hb, HB_BUFFER_CONTENT_TYPE_UNICODE); + hb_buffer_set_direction(buf->hb, HB_DIRECTION_LTR); + /* hb_buffer_set_script(hbBuf, HB_SCRIPT_LATIN); */ /* will be autodetected */ + } + if (isMonospaced) { + /* Fit borrowed glyphs into the expected monospacing. */ + for (size_t runIndex = 0; runIndex < runCount; runIndex++) { + evenMonospaceAdvances_GlyphBuffer_(at_Array(&buffers, runIndex), d); + } + } iBool willAbortDueToWrap = iFalse; const iRangecc sourceText = attrText.source; - const iChar * visualText = constData_Array(&attrText.visual); const int * visToSource = constData_Array(&attrText.visualToSourceOffset); - for (size_t runIndex = 0; runIndex < size_Array(&attrText.runs); runIndex++) { - const iAttributedRun *run = at_Array(&attrText.runs, runIndex); - iRangei runVisual = run->visual; - if (wrapResumePos >= 0) { - xCursor = 0.0f; - yCursor += d->height; - runVisual.start = wrapResumePos; - wrapResumePos = NULL; - } - else if (run->lineBreaks) { - for (int i = 0; i < run->lineBreaks; i++) { - /* One callback for each "wrapped" line even if they're empty. */ - if (!notify_WrapText_(args->wrap, - sourcePtr_AttributedText_(&attrText, run->visual.start), - iRound(xCursor))) { + const size_t visSize = size_Array(&attrText.visual); + iRanges wrapRuns = { 0, runCount }; + iRangei wrapVisRange = { 0, visSize }; + int wrapResumePos = visSize; + size_t wrapResumeRunIndex = runCount; + while (!isEmpty_Range(&wrapRuns)) { + float wrapAdvance = 0.0f; + if (args->wrap && args->wrap->maxWidth > 0) { + iAssert(wrapVisRange.end == visSize); + /* Determine ends of wrapRuns and wrapVisRange. */ + for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) { + const iAttributedRun *run = at_Array(&attrText.runs, runIndex); + if (run->isLineBreak) { + wrapVisRange.end = run->visual.start; + wrapResumePos = run->visual.end; + wrapRuns.end = runIndex; + wrapResumeRunIndex = runIndex + 1; + yCursor += d->height; break; } - xCursor = 0.0f; - yCursor += d->height; + wrapResumeRunIndex = runCount; + wrapResumePos = visSize; + iGlyphBuffer *buf = at_Array(&buffers, runIndex); + iAssert(run->font == buf->font); + shape_GlyphBuffer_(buf); + int safeBreakPos = -1; + iChar prevCh = 0; + for (unsigned int i = 0; i < buf->glyphCount; i++) { + const hb_glyph_info_t *info = &buf->glyphInfo[i]; + const hb_codepoint_t glyphId = info->codepoint; + const int visPos = info->cluster; + if (visPos < wrapVisRange.start) { + continue; + } + const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId); + const int glyphFlags = hb_glyph_info_get_glyph_flags(info); + const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; + const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; + if (args->wrap->mode == word_WrapTextMode) { + /* When word wrapping, only consider certain places breakable. */ + const iChar ch = visualText[info->cluster]; + if ((ch >= 128 || !ispunct(ch)) && (prevCh == '-' || prevCh == '/')) { + safeBreakPos = visPos; + // isSoftHyphenBreak = iFalse; + } + else if (isSpace_Char(ch)) { + safeBreakPos = visPos; + // isSoftHyphenBreak = iFalse; + } + prevCh = ch; + } + else { + if (~glyphFlags & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) { + safeBreakPos = visPos; + } + } + /* Out of room? */ + if (wrapAdvance + xOffset + glyph->d[0].x + glyph->rect[0].size.x > + args->wrap->maxWidth) { + if (safeBreakPos >= 0) { + wrapVisRange.end = safeBreakPos; + } + else { + wrapVisRange.end = visPos; + } + wrapResumePos = wrapVisRange.end; + while (wrapResumePos < visSize && isSpace_Char(visualText[wrapResumePos])) { + wrapResumePos++; /* skip space */ + } + wrapRuns.end = runIndex + 1; /* still includes this run */ + wrapResumeRunIndex = runIndex; /* ...but continue from the same one */ + break; + } + wrapAdvance += xAdvance; + /* Additional kerning tweak. It would be better to use HarfBuzz font callbacks, + but they don't seem to get called? */ + if (i + 1 < buf->glyphCount) { + wrapAdvance += horizKern_Font_(buf->font, + glyphId, + buf->glyphInfo[i + 1].codepoint); + } + } } } - const iRangecc runSource = sourceRange_AttributedText_(&attrText, runVisual); - hb_buffer_clear_contents(hbBuf); - iBool isRunRTL = iFalse; - /* Cluster values are used to determine offset inside the UTF-8 source string. */ - //hb_buffer_set_cluster_level(hbBuf, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); - for (int pos = runVisual.start; pos < runVisual.end; pos++) { - hb_buffer_add(hbBuf, visualText[pos], pos); + else { + /* Calculate total advance without wrapping. */ + for (size_t i = wrapRuns.start; i < wrapRuns.end; i++) { + wrapAdvance += advance_GlyphBuffer_(at_Array(&buffers, i), wrapVisRange); + } } - hb_buffer_set_content_type(hbBuf, HB_BUFFER_CONTENT_TYPE_UNICODE); - hb_buffer_set_direction(hbBuf, HB_DIRECTION_LTR); - /* hb_buffer_set_script(hbBuf, HB_SCRIPT_LATIN); */ /* will be autodetected */ - //hb_buffer_set_language(hbBuf, hb_language_from_string("en", -1)); /* TODO: language from document/UI, if known */ - hb_shape(run->font->hbFont, hbBuf, NULL, 0); /* TODO: Specify features, too? */ - unsigned int glyphCount = 0; - const hb_glyph_info_t * glyphInfo = hb_buffer_get_glyph_infos(hbBuf, &glyphCount); - hb_glyph_position_t * glyphPos = hb_buffer_get_glyph_positions(hbBuf, &glyphCount); - int breakPos = -1; -// iBool isSoftHyphenBreak = iFalse; - /* Fit foreign glyphs into the expected monospacing. */ - if (isMonospaced) { - for (unsigned int i = 0; i < glyphCount; ++i) { - const hb_glyph_info_t *info = &glyphInfo[i]; - if (glyphPos[i].x_advance > 0 && run->font != d) { - const iChar ch = visualText[info->cluster]; - if (isPictograph_Char(ch) || isEmoji_Char(ch)) { - const float dw = run->font->xScale * glyphPos[i].x_advance - monoAdvance; - glyphPos[i].x_offset -= dw / 2 / run->font->xScale - 1; - glyphPos[i].x_advance -= dw / run->font->xScale - 1; + /* Make a callback for each wrapped line. */ + if (!notify_WrapText_(args->wrap, +// sourcePtr_AttributedText_(&attrText, wrapVisRange.end), + sourcePtr_AttributedText_(&attrText, wrapResumePos), + iRound(wrapAdvance))) { + willAbortDueToWrap = iTrue; + } +#if 0 + if (wrapResumePos >= 0) { + xCursor = 0.0f; + yCursor += d->height; + runVisual.start = wrapResumePos; + wrapResumePos = NULL; + } + else if (run->lineBreaks) { + for (int i = 0; i < run->lineBreaks; i++) { + /* One callback for each "wrapped" line even if they're empty. */ + if (!notify_WrapText_(args->wrap, + sourcePtr_AttributedText_(&attrText, run->visual.start), + iRound(xCursor))) { + break; } + xCursor = 0.0f; + yCursor += d->height; } } - } + const iRangecc runSource = sourceRange_AttributedText_(&attrText, runVisual); + hb_buffer_clear_contents(hbBuf); + iBool isRunRTL = attrText.bidiLevels != NULL; + /* Cluster values are used to determine offset inside the UTF-8 source string. */ + //hb_buffer_set_cluster_level(hbBuf, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); + for (int pos = runVisual.start; pos < runVisual.end; pos++) { + hb_buffer_add(hbBuf, visualText[pos], pos); + if (attrText.bidiLevels) { + const char lev = attrText.bidiLevels[pos]; + if (!FRIBIDI_IS_NEUTRAL(lev) && !FRIBIDI_IS_RTL(lev)) { + isRunRTL = iFalse; + } + } + } + hb_buffer_set_content_type(hbBuf, HB_BUFFER_CONTENT_TYPE_UNICODE); + hb_buffer_set_direction(hbBuf, HB_DIRECTION_LTR); + /* hb_buffer_set_script(hbBuf, HB_SCRIPT_LATIN); */ /* will be autodetected */ + //hb_buffer_set_language(hbBuf, hb_language_from_string("en", -1)); /* TODO: language from document/UI, if known */ + hb_shape(run->font->hbFont, hbBuf, NULL, 0); /* TODO: Specify features, too? */ + unsigned int glyphCount = 0; + const hb_glyph_info_t * glyphInfo = hb_buffer_get_glyph_infos(hbBuf, &glyphCount); + hb_glyph_position_t * glyphPos = hb_buffer_get_glyph_positions(hbBuf, &glyphCount); +// iBool isSoftHyphenBreak = iFalse; /* Check if this run needs to be wrapped. If so, we'll draw the portion that fits on the line, and re-run the loop resuming the run from the wrap point. */ if (args->wrap && args->wrap->maxWidth > 0) { @@ -1379,25 +1548,48 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { xCursor += args->xposLayoutBound - orig.x - glyphRunRightEdge_Font_(run->font, glyphPos, glyphCount); } - /* We have determined a possible wrap position inside the text run, so now we can - draw the glyphs. */ - for (unsigned int i = 0; i < glyphCount; i++) { - const hb_glyph_info_t *info = &glyphInfo[i]; - const hb_codepoint_t glyphId = info->codepoint; - const int visPos = info->cluster; - const float xOffset = run->font->xScale * glyphPos[i].x_offset; - const float yOffset = run->font->yScale * glyphPos[i].y_offset; - const float xAdvance = run->font->xScale * glyphPos[i].x_advance; - const float yAdvance = run->font->yScale * glyphPos[i].y_advance; - const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId); - const float xf = xCursor + xOffset; - const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0; - if (visPos == breakPos) { - runIndex--; /* stay on the same run */ - wrapResumePos = visPos; - break; +#endif + /* TODO: Alignment. */ + xCursor = 0.0f; + /* We have determined a possible wrap position and alignment for the work runs, + so now we can process the glyphs. */ + for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) { + const iAttributedRun *run = at_Array(&attrText.runs, runIndex); + if (run->isLineBreak) { + xCursor = 0.0f; + yCursor += d->height; + continue; } - /* Draw the glyph. */ { + iGlyphBuffer *buf = at_Array(&buffers, runIndex); + shape_GlyphBuffer_(buf); + iAssert(run->font == buf->font); + iRangei runVisual = run->visual; + /* Process all the glyphs. */ + for (unsigned int i = 0; i < buf->glyphCount; i++) { + const hb_glyph_info_t *info = &buf->glyphInfo[i]; + const hb_codepoint_t glyphId = info->codepoint; + const int visPos = info->cluster; + if (visPos < wrapVisRange.start) { + continue; + } + if (visPos >= wrapVisRange.end) { + break; + } + const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; + const float yOffset = run->font->yScale * buf->glyphPos[i].y_offset; + const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; + const float yAdvance = run->font->yScale * buf->glyphPos[i].y_advance; + const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId); + const float xf = xCursor + xOffset; + const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0; +#if 0 + if (visPos == breakPos) { + runIndex--; /* stay on the same run */ + wrapResumePos = visPos; + break; + } +#endif + /* Output position for the glyph. */ SDL_Rect dst = { orig.x + xCursor + xOffset + glyph->d[hoff].x, orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y, glyph->rect[hoff].size.x, @@ -1414,7 +1606,8 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { bounds.size.x = iMax(bounds.size.x, dst.x + dst.w - orig.x); bounds.size.y = iMax(bounds.size.y, yCursor + glyph->font->height); } - if (mode & draw_RunMode && visualText[visPos] != 0x20) { + if (mode & draw_RunMode && visualText[visPos] > 0x20) { + /* Draw the glyph. */ if (!isRasterized_Glyph_(glyph, hoff)) { cacheSingleGlyph_Font_(run->font, glyphId); /* may cause cache reset */ glyph = glyphByIndex_Font_(run->font, glyphId); @@ -1437,29 +1630,38 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { } SDL_RenderCopy(text_.render, text_.cache, &src, &dst); } + xCursor += xAdvance; + yCursor += yAdvance; + /* Additional kerning tweak. It would be better to use HarfBuzz font callbacks, + but they don't seem to get called? */ + if (i + 1 < buf->glyphCount) { + xCursor += horizKern_Font_(run->font, glyphId, buf->glyphInfo[i + 1].codepoint); + } + xCursorMax = iMax(xCursorMax, xCursor); } - xCursor += xAdvance; - yCursor += yAdvance; - /* Additional kerning tweak. It would be better to use HarfBuzz font callbacks, - but they don't seem to get called? */ - if (i + 1 < glyphCount) { - xCursor += horizKern_Font_(run->font, glyphId, glyphInfo[i + 1].codepoint); - } - xCursorMax = iMax(xCursorMax, xCursor); } if (willAbortDueToWrap) { break; } + wrapRuns.start = wrapResumeRunIndex; + wrapRuns.end = runCount; + wrapVisRange.start = wrapResumePos; + wrapVisRange.end = visSize; + yCursor += d->height; } - if (args->wrap) { - args->wrap->cursor_out = init_I2(xCursor, yCursor); + if (args->cursorAdvance_out) { + *args->cursorAdvance_out = init_I2(xCursor, yCursor); } +#if 0 /* The final, unbroken line. */ notify_WrapText_(args->wrap, NULL, xCursor); +#endif if (args->runAdvance_out) { *args->runAdvance_out = xCursorMax; } - hb_buffer_destroy(hbBuf); + iForEach(Array, b, &buffers) { + deinit_GlyphBuffer_(b.value); + } deinit_AttributedText(&attrText); return bounds; } @@ -1476,12 +1678,27 @@ int lineHeight_Text(int fontId) { return font_Text_(fontId)->height; } +#if 0 iInt2 measureRange_Text(int fontId, iRangecc text) { if (isEmpty_Range(&text)) { return init_I2(0, lineHeight_Text(fontId)); } return run_Font_(font_Text_(fontId), &(iRunArgs){ .mode = measure_RunMode, .text = text }).size; } +#endif + +iTextMetrics measureRange_Text(int fontId, iRangecc text) { + if (isEmpty_Range(&text)) { + return (iTextMetrics){ init_Rect(0, 0, 0, lineHeight_Text(fontId)), zero_I2() }; + } + iTextMetrics tm; + tm.bounds = run_Font_(font_Text_(fontId), &(iRunArgs){ + .mode = measure_RunMode, + .text = text, + .cursorAdvance_out = &tm.advance + }); + return tm; +} iRect visualBounds_Text(int fontId, iRangecc text) { return run_Font_(font_Text_(fontId), @@ -1491,9 +1708,11 @@ iRect visualBounds_Text(int fontId, iRangecc text) { }); } +#if 0 iInt2 measure_Text(int fontId, const char *text) { return measureRange_Text(fontId, range_CStr(text)); } +#endif void cache_Text(int fontId, iRangecc text) { cacheTextGlyphs_Font_(font_Text_(fontId), text); @@ -1507,6 +1726,7 @@ static int runFlagsFromId_(enum iFontId fontId) { return runFlags; } +#if 0 iInt2 advanceRange_Text(int fontId, iRangecc text) { int advance; const int height = run_Font_(font_Text_(fontId), @@ -1516,19 +1736,22 @@ iInt2 advanceRange_Text(int fontId, iRangecc text) { .size.y; return init_I2(advance, height); } +#endif static iBool cbAdvanceOneLine_(iWrapText *d, iRangecc range, int advance) { - *((const char **) d->context) = iMin(skipSpace_CStr(range.end), d->text.end); + *((const char **) d->context) = range.end; // iMin(skipSpace_CStr(range.end), d->text.end); return iFalse; /* just one line */ } iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos) { - iWrapText wrap = { .mode = word_WrapTextMode, - .text = text, .maxWidth = width, - .wrapFunc = cbAdvanceOneLine_, .context = endPos }; - /* The return value is expected to be the horizontal/vertical bounds, while WrapText - returns the actual advanced cursor position. */ - return addY_I2(advance_WrapText(&wrap, fontId), lineHeight_Text(fontId)); + /* FIXME: Get rid of this. Caller could use WrapText directly? */ + iWrapText wrap = { .mode = word_WrapTextMode, + .text = text, + .maxWidth = width, + .wrapFunc = cbAdvanceOneLine_, + .context = endPos }; + /* The return value is expected to be the horizontal/vertical bounds. */ + return measure_WrapText(&wrap, fontId).bounds.size; #if 0 int advance; @@ -1546,42 +1769,29 @@ iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos) iInt2 tryAdvanceNoWrap_Text(int fontId, iRangecc text, int width, const char **endPos) { /* TODO: "NoWrap" means words aren't wrapped; the line is broken at nearest character. */ - iWrapText wrap = { .mode = anyCharacter_WrapTextMode, - .text = text, .maxWidth = width, - .wrapFunc = cbAdvanceOneLine_, .context = endPos }; - const int x = advance_WrapText(&wrap, fontId).x; - return init_I2(x, lineHeight_Text(fontId)); + iWrapText wrap = { .mode = anyCharacter_WrapTextMode, + .text = text, + .maxWidth = width, + .wrapFunc = cbAdvanceOneLine_, + .context = endPos }; + return measure_WrapText(&wrap, fontId).bounds.size; } -iInt2 advance_WrapText(iWrapText *d, int fontId) { - run_Font_(font_Text_(fontId), - &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId), - .text = d->text, - .wrap = d - }); - return d->cursor_out; -} - -iRect measure_WrapText(iWrapText *d, int fontId) { - return run_Font_(font_Text_(fontId), - &(iRunArgs){ .mode = measure_RunMode | visualFlag_RunMode | runFlagsFromId_(fontId), - .text = d->text, - .wrap = d - }); -} - -void draw_WrapText(iWrapText *d, int fontId, iInt2 pos, int color) { - run_Font_(font_Text_(fontId), - &(iRunArgs){ .mode = draw_RunMode | runFlagsFromId_(fontId) | - (color & permanent_ColorId ? permanentColorFlag_RunMode : 0) | - (color & fillBackground_ColorId ? fillBackground_RunMode : 0), - .text = d->text, - .pos = pos, - .wrap = d, - .color = color, - }); +iTextMetrics measureN_Text(int fontId, const char *text, size_t n) { + if (n == 0) { + return (iTextMetrics){ init_Rect(0, 0, 0, lineHeight_Text(fontId)), + zero_I2() }; + } + iTextMetrics tm; + tm.bounds = run_Font_(font_Text_(fontId), + &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId), + .text = range_CStr(text), + .maxLen = n, + .cursorAdvance_out = &tm.advance }); + return tm; } +#if 0 iInt2 advance_Text(int fontId, const char *text) { return advanceRange_Text(fontId, range_CStr(text)); } @@ -1591,13 +1801,14 @@ iInt2 advanceN_Text(int fontId, const char *text, size_t n) { return init_I2(0, lineHeight_Text(fontId)); } int advance; - run_Font_(font_Text_(fontId), + int height = run_Font_(font_Text_(fontId), &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId), .text = range_CStr(text), .maxLen = n, - .runAdvance_out = &advance }); - return init_I2(advance, lineHeight_Text(fontId)); + .runAdvance_out = &advance }).size.y; + return init_I2(advance, height); } +#endif static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text, size_t maxLen) { iText *d = &text_; @@ -1633,10 +1844,10 @@ void drawAlign_Text(int fontId, iInt2 pos, int color, enum iAlignment align, con va_end(args); } if (align == center_Alignment) { - pos.x -= measure_Text(fontId, cstr_Block(&chars)).x / 2; + pos.x -= measure_Text(fontId, cstr_Block(&chars)).bounds.size.x / 2; } else if (align == right_Alignment) { - pos.x -= measure_Text(fontId, cstr_Block(&chars)).x; + pos.x -= measure_Text(fontId, cstr_Block(&chars)).bounds.size.x; } draw_Text_(fontId, pos, color, range_Block(&chars)); deinit_Block(&chars); @@ -1678,6 +1889,12 @@ void drawOutline_Text(int fontId, iInt2 pos, int outlineColor, int fillColor, iR } } +iTextMetrics measureWrapRange_Text(int fontId, int maxWidth, iRangecc text) { + iWrapText wrap = { .text = text, .maxWidth = maxWidth, .mode = word_WrapTextMode }; + return measure_WrapText(&wrap, fontId); +} + +#if 0 iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) { iInt2 size = zero_I2(); const char *endp; @@ -1689,6 +1906,7 @@ iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) { } return size; } +#endif void drawBoundRange_Text(int fontId, iInt2 pos, int boundWidth, int color, iRangecc text) { /* 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 void drawCenteredRange_Text(int fontId, iRect rect, iBool alignVisual, int color, iRangecc text) { iRect textBounds = alignVisual ? visualBounds_Text(fontId, text) - : (iRect){ zero_I2(), advanceRange_Text(fontId, text) }; + : measureRange_Text(fontId, text).bounds; textBounds.pos = sub_I2(mid_Rect(rect), mid_Rect(textBounds)); textBounds.pos.x = iMax(textBounds.pos.x, left_Rect(rect)); /* keep left edge visible */ draw_Text_(fontId, textBounds.pos, color, text); } +#if 0 +iInt2 advance_WrapText(iWrapText *d, int fontId) { + const iRect bounds = run_Font_(font_Text_(fontId), + &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId), + .text = d->text, + .wrap = d }); + return bounds.size; +} +#endif + +iTextMetrics measure_WrapText(iWrapText *d, int fontId) { + iTextMetrics tm; + tm.bounds = run_Font_(font_Text_(fontId), + &(iRunArgs){ .mode = measure_RunMode | runFlagsFromId_(fontId), + .text = d->text, + .wrap = d, + .cursorAdvance_out = &tm.advance }); + return tm; +} + +void draw_WrapText(iWrapText *d, int fontId, iInt2 pos, int color) { + run_Font_(font_Text_(fontId), + &(iRunArgs){ .mode = draw_RunMode | runFlagsFromId_(fontId) | + (color & permanent_ColorId ? permanentColorFlag_RunMode : 0) | + (color & fillBackground_ColorId ? fillBackground_RunMode : 0), + .text = d->text, + .pos = pos, + .wrap = d, + .color = color, + }); +} + SDL_Texture *glyphCache_Text(void) { return text_.cache; } @@ -1868,7 +2118,7 @@ iDefineTypeConstructionArgs(TextBuf, (int font, int color, const char *text), fo static void initWrap_TextBuf_(iTextBuf *d, int font, int color, int maxWidth, iBool doWrap, const char *text) { SDL_Renderer *render = text_.render; if (maxWidth == 0) { - d->size = advance_Text(font, text); + d->size = measure_Text(font, text).bounds.size; } else { 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 void resetFonts_Text (void); int lineHeight_Text (int fontId); -iInt2 measure_Text (int fontId, const char *text); -iInt2 measureRange_Text (int fontId, iRangecc text); +//iInt2 measure_Text (int fontId, const char *text); +//iInt2 measureRange_Text (int fontId, iRangecc text); iRect visualBounds_Text (int fontId, iRangecc text); -iInt2 advance_Text (int fontId, const char *text); -iInt2 advanceN_Text (int fontId, const char *text, size_t n); /* `n` in characters */ -iInt2 advanceRange_Text (int fontId, iRangecc text); -iInt2 advanceWrapRange_Text (int fontId, int maxWidth, iRangecc text); +//iInt2 advance_Text (int fontId, const char *text); +//iInt2 advanceN_Text (int fontId, const char *text, size_t n); /* `n` in characters */ +//iInt2 advanceRange_Text (int fontId, iRangecc text); +//iInt2 advanceWrapRange_Text (int fontId, int maxWidth, iRangecc text); + +iDeclareType(TextMetrics) + +struct Impl_TextMetrics { + iRect bounds; /* logical bounds: multiples of line height, horiz. advance */ + iInt2 advance; /* cursor offset */ +}; + +iTextMetrics measureRange_Text (int fontId, iRangecc text); +iTextMetrics measureWrapRange_Text (int fontId, int maxWidth, iRangecc text); +iTextMetrics measureN_Text (int fontId, const char *text, size_t n); /* `n` in characters */ + +iLocalDef iTextMetrics measure_Text(int fontId, const char *text) { + return measureRange_Text(fontId, range_CStr(text)); +} iInt2 tryAdvance_Text (int fontId, iRangecc text, int width, const char **endPos); iInt2 tryAdvanceNoWrap_Text (int fontId, iRangecc text, int width, const char **endPos); @@ -196,15 +211,12 @@ struct Impl_WrapText { enum iWrapTextMode mode; iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, int advance); void * context; - /* output */ - iInt2 cursor_out; /* internal */ iRangecc wrapRange_; }; -iRect measure_WrapText (iWrapText *, int fontId); -iInt2 advance_WrapText (iWrapText *, int fontId); -void draw_WrapText (iWrapText *, int fontId, iInt2 pos, int color); +iTextMetrics measure_WrapText (iWrapText *, int fontId); +void draw_WrapText (iWrapText *, int fontId, iInt2 pos, int color); SDL_Texture * glyphCache_Text (void); 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) { spr->color = 0; init_String(&spr->text); appendChar_String(&spr->text, chars[i]); - spr->xoff = (width - advanceRange_Text(d->font, range_String(&spr->text)).x) / 2; + spr->xoff = (width - measureRange_Text(d->font, range_String(&spr->text)).advance.x) / 2; spr->size = init_I2(width, lineHeight_Text(d->font)); x += width + gap; } 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) { size_t widestPos = iInvalidPos; iConstForEach(Array, i, uiLangs) { const int width = - advance_Text(uiLabel_FontId, + measure_Text(uiLabel_FontId, translateCStr_Lang(((const iMenuItem *) i.value)->label)) - .x; + .advance.x; if (widestPos == iInvalidPos || width > widest) { widest = width; widestPos = index_ArrayConstIterator(&i); -- cgit v1.2.3