Files
vue-tui/packages/runtime/src/host/layout-guards.ts
T
Yunfei He ca63a5d04c refactor(runtime)!: prefix host primitive tags with tui- (align to Ink, drop *Impl)
The renderer's intrinsic elements were named with bare words (box/text/static/
transform/virtual-text), which collide with the same-named public components: a
template `<box>` PascalCase-resolves to `<Box>` under vue-tsc (no isCustomElement
at the type layer), forcing the BoxImpl/TextImpl/StaticImpl workaround.

Prefix the 5 host elements to `tui-*` (mirroring Ink's `ink-box`/`ink-text`):
the prefix + hyphen keeps them in their own namespace, so the components keep
their real names (Box/Text/Static) with no self-recursion — the *Impl rename is
removed. root/text-leaf/comment stay unprefixed (not template tags, not elements).

Mechanics: renamed the TuiNode discriminant literals + factories first, then let
vue-tsc enumerate all 145 stale `node.type === "box"` comparisons (the type-
driven finder also kept `position: "static"` and the ansi-tokenizer's separate
`type: "text"` union untouched). Updated createElement cases, HOST_TAGS,
the .vue templates, transform.ts h(), and raw `h("box")` host-op tests.

Two non-type-checked contaminations the sed caused were caught by tests and fixed:
- patchProp's `key === "transform"` (the PROP name, not the node type) must stay
  "transform" — the sed wrongly prefixed it, dropping the transform fn (identity).
- text-measure's `token.type === "text"` is an AnsiToken, not a TuiNode — reverted.

BREAKING CHANGE: the internal host element names are now tui-box/tui-text/
tui-virtual-text/tui-static/tui-transform. Public components (Box/Text/Static/
Spacer/Newline/Transform) and their props/types are unchanged; only raw host-op
callers (h("box") -> h("tui-box")) are affected.

vp run ready green: fmt, lint 0/0, vue-tsc, tests (runtime 350, integration 1161,
PTY 129).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:49:08 +08:00

106 lines
3.4 KiB
TypeScript

import Yoga from "yoga-layout";
import type { Direction, Node as YogaNode } from "yoga-layout";
import type { TuiBox, TuiNode, TuiRoot, TuiStatic, TuiText, TuiTransform } from "./nodes.ts";
type YogaCarrier = TuiRoot | TuiBox | TuiText | TuiStatic | TuiTransform;
type ContainerWithChildren = TuiRoot | TuiBox | TuiText | TuiStatic | TuiTransform;
function hasYoga(node: TuiNode): node is YogaCarrier {
return (
node.type === "root" ||
node.type === "tui-box" ||
node.type === "tui-text" ||
node.type === "tui-static" ||
node.type === "tui-transform"
);
}
function hasChildren(node: TuiNode): node is ContainerWithChildren {
return (
node.type === "root" ||
node.type === "tui-box" ||
node.type === "tui-text" ||
node.type === "tui-static" ||
node.type === "tui-transform"
);
}
function getBoxInnerSize(node: TuiBox): { width: number; height: number } {
const layout = node.yoga.getComputedLayout();
const width = Math.max(0, Math.floor(layout.width));
const height = Math.max(0, Math.floor(layout.height));
const left =
node.yoga.getComputedBorder(Yoga.EDGE_LEFT) + node.yoga.getComputedPadding(Yoga.EDGE_LEFT);
const right =
node.yoga.getComputedBorder(Yoga.EDGE_RIGHT) + node.yoga.getComputedPadding(Yoga.EDGE_RIGHT);
const top =
node.yoga.getComputedBorder(Yoga.EDGE_TOP) + node.yoga.getComputedPadding(Yoga.EDGE_TOP);
const bottom =
node.yoga.getComputedBorder(Yoga.EDGE_BOTTOM) + node.yoga.getComputedPadding(Yoga.EDGE_BOTTOM);
return {
width: Math.max(0, Math.floor(width - left - right)),
height: Math.max(0, Math.floor(height - top - bottom)),
};
}
function hideYogaChild(child: TuiNode, guarded: Map<YogaNode, number>): boolean {
if (!hasYoga(child)) return false;
if (guarded.has(child.yoga)) return false;
const display = child.yoga.getDisplay();
if (display === Yoga.DISPLAY_NONE) return false;
guarded.set(child.yoga, display);
child.yoga.setDisplay(Yoga.DISPLAY_NONE);
return true;
}
function applyZeroContentGuards(node: TuiNode, guarded: Map<YogaNode, number>): boolean {
if (hasYoga(node) && node.yoga.getDisplay() === Yoga.DISPLAY_NONE) return false;
let changed = false;
if (node.type === "tui-box") {
const inner = getBoxInnerSize(node);
if (inner.width === 0 || inner.height === 0) {
for (const child of node.children) {
// 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;
}
changed = hideYogaChild(child, guarded) || changed;
}
return changed;
}
}
if (!hasChildren(node)) return changed;
for (const child of node.children) {
changed = applyZeroContentGuards(child, guarded) || changed;
}
return changed;
}
export function calculateLayoutWithContentGuards(
root: TuiRoot,
width?: number,
height?: number,
direction: Direction = Yoga.DIRECTION_LTR,
): () => void {
const guarded = new Map<YogaNode, number>();
for (;;) {
root.yoga.calculateLayout(width, height, direction);
if (!applyZeroContentGuards(root, guarded)) break;
}
return () => {
for (const [node, display] of [...guarded].reverse()) {
node.setDisplay(display);
}
};
}