co-mono/packages/tui/src/components/box.ts

134 lines
3.1 KiB
TypeScript

import type { Component } from "../tui.js";
import { applyBackgroundToLine, visibleWidth } from "../utils.js";
/**
* Box component - a container that applies padding and background to all children
*/
export class Box implements Component {
children: Component[] = [];
private paddingX: number;
private paddingY: number;
private bgFn?: (text: string) => string;
// Cache for rendered output
private cachedWidth?: number;
private cachedChildLines?: string;
private cachedBgSample?: string;
private cachedLines?: string[];
constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {
this.paddingX = paddingX;
this.paddingY = paddingY;
this.bgFn = bgFn;
}
addChild(component: Component): void {
this.children.push(component);
this.invalidateCache();
}
removeChild(component: Component): void {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
this.invalidateCache();
}
}
clear(): void {
this.children = [];
this.invalidateCache();
}
setBgFn(bgFn?: (text: string) => string): void {
this.bgFn = bgFn;
// Don't invalidate here - we'll detect bgFn changes by sampling output
}
private invalidateCache(): void {
this.cachedWidth = undefined;
this.cachedChildLines = undefined;
this.cachedBgSample = undefined;
this.cachedLines = undefined;
}
invalidate(): void {
this.invalidateCache();
for (const child of this.children) {
child.invalidate?.();
}
}
render(width: number): string[] {
if (this.children.length === 0) {
return [];
}
const contentWidth = Math.max(1, width - this.paddingX * 2);
const leftPad = " ".repeat(this.paddingX);
// Render all children
const childLines: string[] = [];
for (const child of this.children) {
const lines = child.render(contentWidth);
for (const line of lines) {
childLines.push(leftPad + line);
}
}
if (childLines.length === 0) {
return [];
}
// Check if bgFn output changed by sampling
const bgSample = this.bgFn ? this.bgFn("test") : undefined;
// Check cache validity
const childLinesKey = childLines.join("\n");
if (
this.cachedLines &&
this.cachedWidth === width &&
this.cachedChildLines === childLinesKey &&
this.cachedBgSample === bgSample
) {
return this.cachedLines;
}
// Apply background and padding
const result: string[] = [];
// Top padding
for (let i = 0; i < this.paddingY; i++) {
result.push(this.applyBg("", width));
}
// Content
for (const line of childLines) {
result.push(this.applyBg(line, width));
}
// Bottom padding
for (let i = 0; i < this.paddingY; i++) {
result.push(this.applyBg("", width));
}
// Update cache
this.cachedWidth = width;
this.cachedChildLines = childLinesKey;
this.cachedBgSample = bgSample;
this.cachedLines = result;
return result;
}
private applyBg(line: string, width: number): string {
const visLen = visibleWidth(line);
const padNeeded = Math.max(0, width - visLen);
const padded = line + " ".repeat(padNeeded);
if (this.bgFn) {
return applyBackgroundToLine(padded, width, this.bgFn);
}
return padded;
}
}