test(runtime): pin absolute-child position to the padding box; fix wording
Addresses PR review: the containing block for an absolutely-positioned child is the **padding box** (inside the borders), not the "border-box". Verified by running yoga (an abs child at top:0/left:0 insets by the border only, never by padding — confirmed across border/padding combos) and the real Ink/vue-tui renderers (X lands at the inner-border edge, byte-identical in both). - Tighten the regression test: exact-frame assertions instead of `toContain`, including a border+padding case that distinguishes the padding box from the content box — the assertion that would have caught the original wording slip (presence-only assertions could not). - Correct "containing block (border-box)" -> "padding box (inside the borders)" in paint.ts, layout-guards.ts, and ink-divergences.md. (The unrelated "border-box-like" *sizing* notes are correct and left as-is.) No runtime behavior change; the code already used yoga's computed position. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -410,9 +410,9 @@ different runtime behavior, ownership rule, or out-of-contract handling.
|
||||
dimension is zero. Paint applies the same inner-content gate, so the in-flow child subtree
|
||||
neither reserves invisible rows nor writes glyphs outside a nonexistent content area.
|
||||
Border and background are still painted as far as the outer area permits.
|
||||
**Absolutely-positioned children are exempt** — their region is the containing block
|
||||
(border-box), not the content rect, so they still lay out and paint (clipped only by
|
||||
`overflow:hidden`), matching Ink. Positive-size content areas keep the
|
||||
**Absolutely-positioned children are exempt** — their containing block is the padding box
|
||||
(inside the borders), not the content rect, so they still lay out and paint (clipped only
|
||||
by `overflow:hidden`), matching Ink. Positive-size content areas keep the
|
||||
existing overflow behavior; this is not a blanket `overflow:hidden`.
|
||||
- **Layout model guidance:** primitive `Box` should preserve the Yoga/flexbox model rather
|
||||
than paper over it with ad-hoc layout corrections. Defaults such as `flexShrink: 1` are
|
||||
|
||||
@@ -5,13 +5,18 @@ import { Box, Text } from "@vue-tui/runtime";
|
||||
|
||||
// A Box whose inner content area collapses to zero must still paint its
|
||||
// position:"absolute" children — an absolutely-positioned child is placed
|
||||
// against the containing block (border-box), not the (nonexistent) content
|
||||
// against its containing block, the **padding box** (the area inside the
|
||||
// borders; padding itself does NOT inset it), not the (nonexistent) content
|
||||
// rect, so the zero-content guard must not suppress it. Ink v7.0.4 paints
|
||||
// these (verified by running real Ink: a w=2 h=2 single-border box with an
|
||||
// absolute child renders "┌┐#\n└X"); vue-tui previously suppressed ALL
|
||||
// children, including absolute ones, rendering "┌┐#\n└┘".
|
||||
// these (verified by running real Ink); vue-tui previously suppressed ALL
|
||||
// children. Flow children stay suppressed (the blessed degenerate-box
|
||||
// divergence).
|
||||
//
|
||||
// Assertions use exact frames (not `toContain`) so they pin WHERE the child
|
||||
// lands — the padding-box edge — distinguishing it from the border box and
|
||||
// the content box.
|
||||
|
||||
test("absolute child paints when border eats the whole content area (w=2 h=2)", async () => {
|
||||
test("absolute child paints at the padding-box edge when content area is zero (w=2 h=2)", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box>
|
||||
@@ -25,7 +30,27 @@ test("absolute child paints when border eats the whole content area (w=2 h=2)",
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
expect(lastFrame()).toContain("X");
|
||||
// X lands at the inner-border (padding-box) corner — it replaces the `┘`.
|
||||
expect(lastFrame()).toBe("┌┐#\n└X");
|
||||
});
|
||||
|
||||
test("absolute child positions against the padding box, not the content box", async () => {
|
||||
// Discriminator: with border=1 AND padding=1, the padding-box edge is at
|
||||
// row 1 / col 1 (inside the border, before padding). A content-box containing
|
||||
// block would instead put X at row 2 / col 2. Verified byte-identical in real
|
||||
// Ink v7.0.4. This is the assertion that would have caught a "border-box" vs
|
||||
// "padding-box" mistake.
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box width={7} height={5} borderStyle="single" padding={1}>
|
||||
<Box position="absolute" top={0} left={0}>
|
||||
<Text>X</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
expect(lastFrame()).toBe("┌─────┐\n│X │\n│ │\n│ │\n└─────┘");
|
||||
});
|
||||
|
||||
test("flow (non-absolute) child stays suppressed when the content area is zero", async () => {
|
||||
@@ -42,5 +67,5 @@ test("flow (non-absolute) child stays suppressed when the content area is zero",
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
expect(lastFrame()).not.toContain("Y");
|
||||
expect(lastFrame()).toBe("┌┐#\n└┘");
|
||||
});
|
||||
|
||||
@@ -64,9 +64,10 @@ function applyZeroContentGuards(node: TuiNode, guarded: Map<YogaNode, number>):
|
||||
const inner = getBoxInnerSize(node);
|
||||
if (inner.width === 0 || inner.height === 0) {
|
||||
for (const child of node.children) {
|
||||
// Absolutely-positioned children are placed against the containing
|
||||
// block (border-box), not the content rect, so the zero-content guard
|
||||
// must not hide them — Ink lays them out and paints them regardless.
|
||||
// Absolutely-positioned children are placed against their containing
|
||||
// block — the padding box (inside the borders) — not the content rect,
|
||||
// so the zero-content guard must not hide them; Ink lays them out and
|
||||
// paints them regardless.
|
||||
if (hasYoga(child) && child.yoga.getPositionType() === Yoga.POSITION_TYPE_ABSOLUTE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -685,8 +685,9 @@ function paintNode(
|
||||
const contentMetrics = getBoxContentMetrics(node, w, h);
|
||||
// A Box with no inner content area has no legal paint region for FLOW
|
||||
// children. Absolutely-positioned children, though, are placed against
|
||||
// the containing block (border-box), not the content rect — Ink still
|
||||
// paints them — so paint just those and keep flow children suppressed.
|
||||
// their containing block — the padding box (inside the borders) — not the
|
||||
// content rect, so Ink still paints them; paint just those and keep flow
|
||||
// children suppressed.
|
||||
if (contentMetrics.width === 0 || contentMetrics.height === 0) {
|
||||
for (const child of node.children) {
|
||||
const childYoga = (child as { yoga?: { getPositionType?: () => number } }).yoga;
|
||||
|
||||
Reference in New Issue
Block a user