fix(paint): backgroundColor now visible with borders and text
Root causes: - drawBorder wrote plain characters, overwriting fillBackground's ANSI codes - renderTextWithInlineStyles spread node.props over inherited bg, but undefined props from Vue clobbered the inherited backgroundColor - Child text nodes did not inherit backgroundColor from parent Box Fixes: - drawBorder applies backgroundColor (and borderColor) to border chars - Filter out undefined props before merging in renderTextWithInlineStyles - Pass inheritedBg through the paint tree to text nodes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
import chalk from "chalk";
|
||||||
|
import { afterEach, beforeEach, expect, test } from "vite-plus/test";
|
||||||
|
import { render } from "@vue-tui/testing";
|
||||||
|
import { Box, Text } from "@vue-tui/runtime";
|
||||||
|
|
||||||
|
const BG_BLUE = "\x1b[44m";
|
||||||
|
|
||||||
|
let prevLevel: typeof chalk.level;
|
||||||
|
beforeEach(() => {
|
||||||
|
prevLevel = chalk.level;
|
||||||
|
chalk.level = 1;
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
chalk.level = prevLevel;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Box backgroundColor produces ANSI background codes", async () => {
|
||||||
|
const { frames } = await render(
|
||||||
|
() => <Box backgroundColor="blue" width={5} height={1} />,
|
||||||
|
{ columns: 10 },
|
||||||
|
);
|
||||||
|
expect(frames.at(-1)).toContain(BG_BLUE);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Box backgroundColor survives border rendering", async () => {
|
||||||
|
const { frames } = await render(
|
||||||
|
() => <Box backgroundColor="blue" borderStyle="single" width={6} height={3} />,
|
||||||
|
{ columns: 10 },
|
||||||
|
);
|
||||||
|
const raw = frames.at(-1)!;
|
||||||
|
expect(raw).toContain(BG_BLUE);
|
||||||
|
expect(raw).toContain("┌");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("child Text inherits backgroundColor from parent Box", async () => {
|
||||||
|
const { frames } = await render(
|
||||||
|
() => (
|
||||||
|
<Box backgroundColor="blue" width={10} height={1}>
|
||||||
|
<Text>hello</Text>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
{ columns: 20 },
|
||||||
|
);
|
||||||
|
const raw = frames.at(-1)!;
|
||||||
|
expect(raw).toContain("hello");
|
||||||
|
expect(raw).toContain(BG_BLUE);
|
||||||
|
});
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"@vitejs/plugin-vue-jsx": "catalog:",
|
"@vitejs/plugin-vue-jsx": "catalog:",
|
||||||
"@vue-tui/runtime": "workspace:*",
|
"@vue-tui/runtime": "workspace:*",
|
||||||
"@vue-tui/testing": "workspace:*",
|
"@vue-tui/testing": "workspace:*",
|
||||||
|
"chalk": "catalog:",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite-plus": "^0.1.20",
|
"vite-plus": "^0.1.20",
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
|
|||||||
@@ -91,7 +91,10 @@ function placeLine(grid: string[][], x: number, y: number, line: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTextWithInlineStyles(node: TuiText | TuiVirtualText, acc: TextProps = {}): string {
|
function renderTextWithInlineStyles(node: TuiText | TuiVirtualText, acc: TextProps = {}): string {
|
||||||
const merged: TextProps = { ...acc, ...node.props };
|
const defined = Object.fromEntries(
|
||||||
|
Object.entries(node.props).filter(([, v]) => v !== undefined),
|
||||||
|
);
|
||||||
|
const merged: TextProps = { ...acc, ...defined };
|
||||||
let out = "";
|
let out = "";
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
if (child.type === "text-leaf") {
|
if (child.type === "text-leaf") {
|
||||||
@@ -120,28 +123,31 @@ function drawBorder(
|
|||||||
if (!chars) return;
|
if (!chars) return;
|
||||||
if (w < 2 || h < 2) return;
|
if (w < 2 || h < 2) return;
|
||||||
|
|
||||||
// Per-edge toggles default to true when borderStyle is set.
|
|
||||||
const top = props["borderTop"] !== false;
|
const top = props["borderTop"] !== false;
|
||||||
const bottom = props["borderBottom"] !== false;
|
const bottom = props["borderBottom"] !== false;
|
||||||
const left = props["borderLeft"] !== false;
|
const left = props["borderLeft"] !== false;
|
||||||
const right = props["borderRight"] !== false;
|
const right = props["borderRight"] !== false;
|
||||||
|
|
||||||
// Corners require both adjacent edges to be enabled; otherwise the
|
const borderColor = props["borderColor"] as string | undefined;
|
||||||
// adjacent edge character is used as a "stub" so the visible edge still
|
const bgColor = props["backgroundColor"] as string | undefined;
|
||||||
// terminates cleanly.
|
const colorProps: TextProps = {};
|
||||||
|
if (borderColor) colorProps.color = borderColor;
|
||||||
|
if (bgColor) colorProps.backgroundColor = bgColor;
|
||||||
|
const colorize = (s: string) => (borderColor || bgColor ? applyChalk(s, colorProps) : s);
|
||||||
|
|
||||||
if (top) {
|
if (top) {
|
||||||
const tl = left ? chars.topLeft : chars.top;
|
const tl = left ? chars.topLeft : chars.top;
|
||||||
const tr = right ? chars.topRight : chars.top;
|
const tr = right ? chars.topRight : chars.top;
|
||||||
output.write(x, y, [tl + chars.top.repeat(w - 2) + tr], transformers);
|
output.write(x, y, [colorize(tl + chars.top.repeat(w - 2) + tr)], transformers);
|
||||||
}
|
}
|
||||||
if (bottom) {
|
if (bottom) {
|
||||||
const bl = left ? chars.bottomLeft : chars.bottom;
|
const bl = left ? chars.bottomLeft : chars.bottom;
|
||||||
const br = right ? chars.bottomRight : chars.bottom;
|
const br = right ? chars.bottomRight : chars.bottom;
|
||||||
output.write(x, y + h - 1, [bl + chars.bottom.repeat(w - 2) + br], transformers);
|
output.write(x, y + h - 1, [colorize(bl + chars.bottom.repeat(w - 2) + br)], transformers);
|
||||||
}
|
}
|
||||||
for (let i = 1; i < h - 1; i++) {
|
for (let i = 1; i < h - 1; i++) {
|
||||||
if (left) output.write(x, y + i, [chars.left], transformers);
|
if (left) output.write(x, y + i, [colorize(chars.left)], transformers);
|
||||||
if (right) output.write(x + w - 1, y + i, [chars.right], transformers);
|
if (right) output.write(x + w - 1, y + i, [colorize(chars.right)], transformers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +181,7 @@ function paintNode(
|
|||||||
x0: number,
|
x0: number,
|
||||||
y0: number,
|
y0: number,
|
||||||
transformers: Transformer[],
|
transformers: Transformer[],
|
||||||
|
inheritedBg?: string,
|
||||||
): void {
|
): void {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case "root": {
|
case "root": {
|
||||||
@@ -187,18 +194,20 @@ function paintNode(
|
|||||||
const y = y0 + layout.top;
|
const y = y0 + layout.top;
|
||||||
const w = Math.max(0, Math.floor(layout.width));
|
const w = Math.max(0, Math.floor(layout.width));
|
||||||
const h = Math.max(0, Math.floor(layout.height));
|
const h = Math.max(0, Math.floor(layout.height));
|
||||||
|
const bg = (node.props["backgroundColor"] as string | undefined) ?? inheritedBg;
|
||||||
if (node.props["backgroundColor"]) {
|
if (node.props["backgroundColor"]) {
|
||||||
fillBackground(output, x, y, w, h, node.props["backgroundColor"], transformers);
|
fillBackground(output, x, y, w, h, node.props["backgroundColor"], transformers);
|
||||||
}
|
}
|
||||||
if (node.props["borderStyle"]) {
|
if (node.props["borderStyle"]) {
|
||||||
drawBorder(output, x, y, w, h, node.props, transformers);
|
drawBorder(output, x, y, w, h, node.props, transformers);
|
||||||
}
|
}
|
||||||
for (const child of node.children) paintNode(child, output, x, y, transformers);
|
for (const child of node.children) paintNode(child, output, x, y, transformers, bg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "text": {
|
||||||
const layout = node.yoga.getComputedLayout();
|
const layout = node.yoga.getComputedLayout();
|
||||||
const text = renderTextWithInlineStyles(node);
|
const bgProps: TextProps = inheritedBg ? { backgroundColor: inheritedBg } : {};
|
||||||
|
const text = renderTextWithInlineStyles(node, bgProps);
|
||||||
const wrapped = wrapText(
|
const wrapped = wrapText(
|
||||||
text,
|
text,
|
||||||
Math.max(1, Math.floor(layout.width)),
|
Math.max(1, Math.floor(layout.width)),
|
||||||
@@ -214,7 +223,7 @@ function paintNode(
|
|||||||
}
|
}
|
||||||
case "transform": {
|
case "transform": {
|
||||||
const next = [...transformers, node.transform];
|
const next = [...transformers, node.transform];
|
||||||
for (const child of node.children) paintNode(child, output, x0, y0, next);
|
for (const child of node.children) paintNode(child, output, x0, y0, next, inheritedBg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case "virtual-text":
|
case "virtual-text":
|
||||||
|
|||||||
Generated
+6
@@ -12,6 +12,9 @@ catalogs:
|
|||||||
'@vitejs/plugin-vue-jsx':
|
'@vitejs/plugin-vue-jsx':
|
||||||
specifier: ^5.1.5
|
specifier: ^5.1.5
|
||||||
version: 5.1.5
|
version: 5.1.5
|
||||||
|
chalk:
|
||||||
|
specifier: ^5.6.2
|
||||||
|
version: 5.6.2
|
||||||
vite-plus:
|
vite-plus:
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 0.1.22
|
version: 0.1.22
|
||||||
@@ -186,6 +189,9 @@ importers:
|
|||||||
'@vue-tui/testing':
|
'@vue-tui/testing':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../testing
|
version: link:../testing
|
||||||
|
chalk:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 5.6.2
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user