fix(runtime): re-measure text when wrap changes at runtime (#193)

The `wrap` prop changes a <Text> node's MEASURED height (the yoga measure
func reads el.props.wrap to pick wrap/truncate/hard layout) but is NOT a
yoga prop, so a runtime wrap-only change took the generic STYLE_PROPS
branch in patchProp: it stored the new value into el.props and called
onCommit() WITHOUT markTextDirty(el). Yoga kept the OLD wrap mode's cached
height while paint rendered with the NEW wrap, so layout and paint
disagreed -- stale blank rows on wrap->truncate, overflow / overwritten
siblings on truncate->wrap.

Mark the text node dirty when the changed STYLE_PROP is `wrap` on a
tui-text node so yoga re-measures. `wrap` is the only STYLE_PROP that
affects measured dimensions (the rest are paint-only), so it is the sole
case.

Verified Ink v7.0.4 has the identical latent bug -- its applyStyles
ignores textWrap and never markDirty()s, so a wrap-only change goes stale
there too. Recorded as a blessed divergence in ink-divergences.md; the fix
matches the layout Ink produces whenever its measure func is invalidated.

Tests (text-wrap-remeasure.test.tsx) reproduce both directions:
RED produced Ink's stale frame ("aaaa …\n\n\nZZZZ"), GREEN the correct
re-measured layout.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-06-15 01:57:52 +08:00
committed by GitHub
parent e4f181f888
commit bf9d9d4a4a
3 changed files with 113 additions and 0 deletions
+15
View File
@@ -480,6 +480,21 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
}
} else if (STYLE_PROPS.has(key)) {
(el as { props: Record<string, unknown> }).props[key] = next;
// `wrap` is the one STYLE_PROP that changes a text node's MEASURED height
// (the measure func reads el.props.wrap to pick wrap/truncate/hard layout)
// yet is NOT a yoga prop — so it skips applyYogaProp and never invalidates
// yoga's cached measurement above. Without re-marking dirty, yoga keeps the
// OLD wrap mode's height while paint renders with the NEW wrap → layout and
// paint disagree (stale blank rows on wrap→truncate; overflow / overwritten
// siblings on truncate→wrap). markTextDirty forces a re-measure. Every other
// STYLE_PROP here (color/bold/border colors/…) is paint-only and never alters
// measured dimensions, so `wrap` is the sole case. NOTE: this also fixes a
// latent bug present in Ink v7.0.4 itself — Ink's applyStyles ignores
// textWrap and never markDirty()s, so a wrap-only change goes stale there too
// (a deliberate divergence pending a human vouch; see ink-divergences.md).
if (key === "wrap" && el.type === "tui-text") {
markTextDirty(el);
}
} else if (key === "aria-role" || key === "ariaRole") {
if (el.type === "tui-box") {
el.internal_accessibility ??= {};