feat(text): make text layout style-aware so rich text wraps correctly - #909
Draft
wouterlucas wants to merge 2 commits into
Draft
wouterlucas wants to merge 2 commits into
wouterlucas wants to merge 2 commits into
Conversation
wouterlucas
force-pushed
the
fix/rich-text-styled-advance
branch
from
September 18, 2026 21:14
023abc0 to
e6bf739
Compare
Line breaking measured every substring with the base font, so a bold run that fits under regular metrics but overflows in bold wrapped in the wrong place, and a line's reported width understated its drawn extent. The previous change corrected the drawn extent after the fact; this corrects the measurement that the wrapping decision itself is based on. MeasureTextFn gains an optional `start`, the absolute index of the substring within the text passed to mapTextLayout. The layout engine now threads that offset through wrapLine, the three wrap strategies, and the per-character scans in truncateLineEnd, truncateWord and splitWord. The parameter is optional and a negative value means "not locatable in the source", which is what separators and the overflow suffix pass, so the plain text path and existing callers are unaffected. Each renderer supplies a measure function closed over its parsed spans rather than the layout engine learning about rich text: Canvas switches the shared measure font per style run, SDF applies the synthetic bold advance and italic overhang. Splitting at span boundaries preserves kerning within a run and drops it across one, where it does not apply. The line width correction is kept, since a line's width is still summed from word widths plus base-font separators and a line-final italic overhang sits outside any word, but it now just calls the same style- aware measure function instead of duplicating the span walk. Note that measurement is no longer a pure function of the substring, so any future memoisation must key on the offset as well as the text.
wouterlucas
force-pushed
the
feat/style-aware-text-layout
branch
from
September 18, 2026 21:17
307035d to
44d9803
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 3 of 4. Base is #908 — review that first; this diff is isolated against it.
Problem
MeasureTextFnhad no style parameter, so line breaking measured every substring with the base font. A bold run that fits under regular metrics but overflows in bold wrapped in the wrong place, and a line reported a width narrower than its drawn extent.PR 2 corrected the drawn extent after the fact. This corrects the measurement the wrapping decision is based on.
Approach
MeasureTextFngains an optionalstart: the absolute index of the substring within the text passed tomapTextLayout. The layout engine threads it throughwrapLine, the three wrap strategies, and the per-character scans intruncateLineEnd,truncateWordandsplitWord.The parameter is optional and a negative value means "not locatable in the source", which is what separators and the overflow suffix pass. The plain text path and existing callers are unaffected.
The layout engine does not learn about rich text. Each renderer supplies a measure function closed over its own parsed spans:
Splitting at span boundaries preserves kerning within a run and drops it across one, where it does not apply.
Note on the retained width correction
PR 2 introduced
correctStyledLineWidthsin both renderers. It is kept rather than deleted, because a line width is still summed from word widths plus base-font separator widths, and a line-final italic overhang sits outside any word — so the accumulated width is not exactly the drawn extent.It now just calls the same style-aware measure function instead of duplicating the span walk, which removes roughly 100 lines of near-duplicate logic across the two renderers.
Reviewer caution
Measurement is no longer a pure function of the substring — it depends on the offset too. Any future memoisation must key on
(start, len), not the string alone. Called out in the commit message.Testing
tsc --noEmitclean, 331 tests pass, eslint 0 errors. Adds 6 tests asserting that every located substring genuinely lives at the offset reported, that the overflow suffix is measured unstyled, that a styled run forces an earlier break, that a styled run off the line does not perturb it, that styled lines report greater width, and that mid-wordbreak-allsplits respect styled characters.