fix(text-editor): reserve hit-box padding in editing preview so the text box doesn't jump on done#849
Conversation
The in-editor text preview (RoundedBackgroundTextField) passes enableHitBoxCorrection: false, so its rounded background hugs the glyphs, while the finished layer (LayerWidgetTextItem) renders the same RoundedBackgroundText with enableHitBoxCorrection: true. The rounded background box therefore grows and shifts (becoming left/right symmetric) the moment editing completes — a visible WYSIWYG mismatch between typing and the placed text layer. Reserve the same symmetric hit-box padding while editing: enable the correction on the preview background and inset the editable glyphs by the matching line-height ratio (0.3 horizontal, 0.1 vertical), so the box is identical during editing and after completion. Adds a widget test locking the preview background height to the finished layer's.
hm21
left a comment
There was a problem hiding this comment.
Thanks for working on that PR. The core fix is correct and well-motivated: the editing preview used enableHitBoxCorrection: false while the finished layer (LayerWidgetTextItem) uses true, so the rounded box grew on Done. Flipping the preview to true and reserving the matching vertical room fixes the vertical jump cleanly, and the new test proves exactly that. flutter analyze is clean apart from two lint infos, and the test passes.
However, one change needs rework before merge. The horizontal room is reserved on the wrong side: the new Padding shrinks the editable TextField's wrap width, while the background keeps the full maxTextWidth. So the editable glyphs wrap at a narrower column than the background box and the finished layer, reintroducing a jump-on-done (line-count change) of the same class this PR targets — only horizontal. I reproduced this with a throwaway widget test (details in the inline comment).
Verdict: needs changes — couple the two wrap widths (inline comment), then the rest are small cleanups: undisposed TextPainter, test not covering the Padding half of the fix, a mislabeled test comment, and two 80-char lint infos.
| required double hitBoxHorizontal, | ||
| required double hitBoxVertical, | ||
| }) { | ||
| return Padding( |
There was a problem hiding this comment.
Reintroduces a jump-on-done — this time horizontal.
This Padding(horizontal: hitBoxHorizontal) shrinks the TextField's layout width by 2*hitBoxHorizontal (~0.6·lineHeight, ~28px at fontSize 40), but the background RoundedBackgroundText.rich still lays its glyphs out at the full maxTextWidth (L190) and only grows the CustomPaint outward. The two sides no longer agree on the wrap column: the editable text is reserved by shrinking inward, the background by growing outward at an unchanged wrap width.
I verified it with a widget test sweeping maxWidth (default TextEditorConfigs, wrapping on), measuring the EditableText vs the finished RoundedBackgroundText:
maxWidth=200 editableH=240 finishedH=128 <- editable ~4 lines, box/finished ~2 lines
maxWidth=220 editableH=240 finishedH=128
maxWidth=240 editableH=120 finishedH=128 <- back in sync
For a band of widths the visible text wraps into more lines than the box that should hug it (a mismatch while editing), and the line count changes on Done. This was not present before the PR, since the editable text used the full width.
Fix: make both layouts wrap at the same column — e.g. subtract 2*hitBoxHorizontal from the background's maxTextWidth (L190), or give the TextField an explicit width of maxTextWidth - 2*hitBoxHorizontal instead of an outer Padding. Please add a near-boundary wrapping test as well, not just the single-line height case.
| /// same way [RoundedBackgroundText] lays the text out, so the hit-box padding | ||
| /// reserved here matches the rectangle the painter draws. | ||
| double _preferredLineHeight(double fontSize) { | ||
| final painter = TextPainter( |
There was a problem hiding this comment.
_preferredLineHeight creates this TextPainter, calls layout(), and returns without dispose(). It runs from build() on every keystroke and scroll tick (both listeners call setState), leaking one native ui.Paragraph per rebuild until GC.
final h = painter.preferredLineHeight;
painter.dispose();
return h;Optionally memoize on (fontSize, style, leadingDistribution, textDirection) — none of them change per keystroke, so recomputing the layout every frame is redundant.
… line height Addresses review feedback: - Horizontal room was reserved on the wrong side: the editable Padding shrank the TextField's wrap width by 2*hitBox while the background kept the full maxTextWidth, so they broke into different line counts (a horizontal jump-on-done). Subtract 2*hitBox from the background maxTextWidth so both wrap at the same column. - Dispose the TextPainter in _preferredLineHeight and memoize it (build runs every keystroke/scroll tick; inputs are unchanged per frame). - Tests: assert the editable glyph inset (the Padding half of the fix); add a wrap-column sweep across widths (editable line count via TextPainter vs the background box line count); reword the reference-render comment; keep lines within 80 chars.
|
Thanks for the thorough review — the horizontal-wrap catch was spot on. Pushed a follow-up addressing every point: Horizontal jump (blocking). You're right: the Undisposed Test didn't cover the Mislabeled comment. Reworded: the reference tree is described as a bare 80-char infos. Reflowed; |
|
LGTM thanks for the thorough writeup 🙏 |
Problem
When adding a text layer, the rounded background box looks different while typing vs after pressing done: during editing the box hugs the glyphs (the trailing/leading side looks clipped and asymmetric), and the moment editing completes the box grows and becomes left/right symmetric.
Root cause
The in-editor preview and the finished layer render the same
RoundedBackgroundTextbut reserve different room for the painter's padding:LayerWidgetTextItemrendersRoundedBackgroundText(..., enableHitBoxCorrection: true), soRoundedBackgroundTextgrows itsCustomPaintsize bylineHeight * 0.3(horizontal) /lineHeight * 0.1(vertical) on each side and offsets the painter, keeping the always-drawn background padding fully inside the box.RoundedBackgroundTextField._buildBackgroundText()buildsRoundedBackgroundText.rich(...)withoutenableHitBoxCorrection(defaults tofalse). The background isPositioned(left: 0, right: 0), so it is tied to the stack width (the glyph width) and theRoundedBackgroundTextPainterpadding (rawHeight * 0.3/rawHeight * 0.1, always drawn) has no reserved room and gets clipped — asymmetrically.So the transition from editing → placed layer visibly changes the box geometry.
Fix
Make the editing preview reserve the same padding as the finished layer:
_buildBackgroundText()passesenableHitBoxCorrection: true.lineHeight * 0.3horizontal,lineHeight * 0.1vertical) so they stay centered inside the now-symmetric box and align with the finished layer.lineHeightis computed the same wayRoundedBackgroundTextlays the text out (preferredLineHeightwith the configuredleadingDistribution).No public API change; single widget file.
Test
Adds a widget test (
test/features/text_editor/rounded_background_text_field_hitbox_test.dart) that locks the preview background box height to the finished layer's height for the same text/style. Without the fix the preview is~0.2 * lineHeightshorter; with it, they match.