ca63a5d04c
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>
106 lines
3.4 KiB
TypeScript
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);
|
|
}
|
|
};
|
|
}
|