Files
vue-tui/packages/runtime-tests
Yunfei He b6dc9a0919 fix(runtime): a 0-width box wraps its text onto its own line, not drops it (Ink parity) (#103)
A 0-width text container (flexBasis=0, width=0, width="0%", a negative parsed
percent) DROPPED its text in vue where Ink wraps it onto its own row. Ink's wrapText
has no width<=0 guard: wrapAnsi("A", 0, {hard:true, trim:false}) = "\nA" (height 2),
so the glyph occupies a second row and a row-sibling renders "B\nA". vue collapsed it
to height 1 (wrapText's `width <= 0 -> [""]` guard), then the paint clamp re-collapsed
the wrap, so the sibling overwrote the text -> "B".

Fixes, all confined to the width<=0 branch:
- text-measure.ts: drop the `width <= 0 -> [""]` guard. A styled string can't go
  through wrapAnsi at width 0 (wrap-ansi@10 byte-splits SGR codes -> garbage like
  "B\n["), so the wrap/hard branch routes through a new wrapZeroWidthAnsi that
  derives its line STRUCTURE from wrapAnsi on the PLAIN (stripped) text — which is
  correct for zero-width graphemes (ZWSP/ZWNJ/ZWJ/combining/VS16/BOM, interior and
  trailing) — then re-applies SGR per grapheme via slice-ansi's slot model, keeping
  wide glyphs whole. Input is NFC-normalized first so combining sequences compose to
  match wrap-ansi (and vue's own normal-width path), not the decomposed source bytes.
- paint.ts: pad the bg to the TRUE wrap width (0), not a >=1-clamped width — a 0-width
  box pads nothing (Ink getMaxWidth=0); clamping bg-padded the empty leading wrap line
  into a stray cell that collided with a row-sibling.

A comparison-battery test locks wrapZeroWidthAnsi's plain output to wrapAnsi's
width-0 layout for ~22 inputs (zero-width, wide, emoji, ZWJ, combining decomposed +
composed, multiline). The full layout suite is byte-unchanged for all width>=1 cases.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:54:12 +08:00
..