fix(runtime): paint position:absolute children in zero-content boxes

The zero-content-area guard (layout-guards + paint.ts) suppressed ALL
children of a Box whose inner content rect collapsed to zero, including
position:"absolute" children. An absolutely-positioned child is placed
against the containing block (border-box), not the content rect, so Ink
v7.0.4 paints it (verified by running real Ink: a w=2 h=2 single-border
box with an absolute child renders "┌┐#\n└X"); vue-tui dropped it,
rendering "┌┐#\n└┘".

- layout-guards: exempt POSITION_TYPE_ABSOLUTE children from the hide loop
  so they keep their layout.
- paint: move overflow-clip setup above the zero-content early-return and,
  in that branch, paint only absolute children (still clipped by
  overflow:hidden, matching Ink) while keeping in-flow children suppressed
  (the blessed degenerate-box divergence).

Flow-child suppression and overflow:hidden clipping both stay Ink-aligned
(verified byte-identical against real Ink). Known limitation: an absolute
descendant nested under a suppressed in-flow child is still dropped (the
flow ancestor is removed from layout) — scoped to direct absolute children.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-06-08 22:59:55 +08:00
parent 9b7e9dfb26
commit 0d50fb7e40
4 changed files with 82 additions and 14 deletions
@@ -0,0 +1,46 @@
import { defineComponent } from "vue";
import { expect, test } from "vite-plus/test";
import { render } from "@vue-tui/testing";
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
// 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└┘".
test("absolute child paints when border eats the whole content area (w=2 h=2)", async () => {
const { lastFrame } = await render(
defineComponent(() => () => (
<Box>
<Box width={2} height={2} borderStyle="single">
<Box position="absolute" top={0} left={0}>
<Text>X</Text>
</Box>
</Box>
<Text>#</Text>
</Box>
)),
{ columns: 100 },
);
expect(lastFrame()).toContain("X");
});
test("flow (non-absolute) child stays suppressed when the content area is zero", async () => {
// The blessed degenerate-box divergence: a normal flow child in a zero-content
// box does NOT paint (avoids Ink's zero-width-text leak). This must stay true.
const { lastFrame } = await render(
defineComponent(() => () => (
<Box>
<Box width={2} height={2} borderStyle="single">
<Text>Y</Text>
</Box>
<Text>#</Text>
</Box>
)),
{ columns: 100 },
);
expect(lastFrame()).not.toContain("Y");
});