import type { Component } from "../tui.js"; import { applyBackgroundToLine, visibleWidth } from "../utils.js"; type RenderCache = { childLines: string[]; width: number; bgSample: string | undefined; lines: string[]; }; /** * 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 cache?: RenderCache; 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.cache = undefined; } private matchCache(width: number, childLines: string[], bgSample: string | undefined): boolean { const cache = this.cache; return ( !!cache && cache.width === width && cache.bgSample === bgSample && cache.childLines.length === childLines.length && cache.childLines.every((line, i) => line === childLines[i]) ); } 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 if (this.matchCache(width, childLines, bgSample)) { return this.cache!.lines; } // 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.cache = { childLines, width, bgSample, lines: 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; } }