Repository navigation
Fractional Text measurement is coupled to Yoga callback order #283
Description
Summary
Fractional Text measurement currently depends on a Yoga-specific sequence of measure callback invocations.
The Runtime currently:
- measures Text using a provisional whole-cell width;
- runs
calculateLayout(); - compares the measured width with the final pixel-grid width;
- stores an override keyed by
(availableWidth, widthMode); - marks the Text node dirty and runs layout again when the line count changes.
This works with the current yoga-layout@3.2.1 callback trace, but the result can change when a compatible backend requests the same width using a different measure mode in the following pass.
The measurement result may legitimately differ between Exactly and AtMost. The problem is not that these modes should be treated as equivalent; it is that VueTUI's correction is stateful and only applies when a later callback happens to repeat the expected mode.
Reproduction
I tested VueTUI at commit 5ddb623 with yoga-layout locally replaced by the research implementation from @taffyjs/yoga PR #9 at commit 007bc33.
This is a backend-substitution experiment, not a currently supported VueTUI configuration.
cd tests/runtime
vp test run integration/layout/text-measure-parity.test.tsxTwo of eight tests fail:
expected: "buil\nd\nafter"
received: "buil\nafter"
expected: "xbuil\n d\nafter"
received: "xbuil\nafter"
In the first case, the relevant layout passes both provide availableWidth = 4.25, but use Exactly and AtMost at different points. The override recorded for one request is therefore not consumed by the other, and the final d is clipped.
The official yoga-layout@3.2.1 backend passes these tests because its callback trace happens to match the current reconciliation state.
Why this matters
A measure callback may be invoked multiple times internally during one layout calculation; that is normal.
The fragile part is that the Runtime completes a layout, stores information derived from that particular callback trace, marks the node dirty, and runs layout again expecting a compatible follow-up trace.
This creates hidden coupling to one layout engine's callback count, order, and modes.
Ink previously removed a similar renderer-owned layout/wrap/layout cycle in PR #276. However, copying Ink's current single-pass behavior directly is not sufficient: its conservative wrapping can leave a stale blank row for this fractional-width fixture. VueTUI therefore needs to establish its own terminal-cell contract rather than assuming floor() or ceil() is always correct.
Desired outcome
Investigate and prototype a Text measurement design with these properties:
- The same complete measurement input—text revision, wrapping mode, available width, and measure mode—returns the same result regardless of previous or future callback calls.
- Correctness does not depend on a particular Yoga callback count, order, or mode transition.
- Measurement and painting agree on the number of wrapped lines.
- Existing fractional-origin, fractional-offset, overflow-hidden, sub-cell, and dynamic-content cases remain covered.
- Runtime-owned
calculateLayout()pass count is observable in the regression tests.
Prefer a correct single-layout strategy. If the existing post-layout feedback pass is genuinely necessary because the final terminal-cell width cannot be known during measurement, document that constraint and replace the callback-trace-dependent override with an explicitly bounded, backend-independent mechanism.
Scope
This issue concerns only post-layout Text-width reconciliation.
The zero-content guard currently shares the same layout loop but solves a separate problem and should be evaluated independently.
Activity
fix(runtime): keep text layout single-pass