111 lines
3.2 KiB
TypeScript
111 lines
3.2 KiB
TypeScript
import type { Component } from "../tui";
|
|
import { applyBackgroundToLine, padding, replaceTabs, visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
|
|
/**
|
|
* Text component - displays multi-line text with word wrapping
|
|
*/
|
|
export class Text implements Component {
|
|
#text: string;
|
|
#paddingX: number; // Left/right padding
|
|
#paddingY: number; // Top/bottom padding
|
|
#customBgFn?: (text: string) => string;
|
|
|
|
// Cache for rendered output
|
|
#cachedText?: string;
|
|
#cachedWidth?: number;
|
|
#cachedLines?: string[];
|
|
|
|
constructor(text: string = "", paddingX: number = 1, paddingY: number = 1, customBgFn?: (text: string) => string) {
|
|
this.#text = text;
|
|
this.#paddingX = paddingX;
|
|
this.#paddingY = paddingY;
|
|
this.#customBgFn = customBgFn;
|
|
}
|
|
|
|
getText(): string {
|
|
return this.#text;
|
|
}
|
|
|
|
setText(text: string): void {
|
|
this.#text = text;
|
|
this.#cachedText = undefined;
|
|
this.#cachedWidth = undefined;
|
|
this.#cachedLines = undefined;
|
|
}
|
|
|
|
setCustomBgFn(customBgFn?: (text: string) => string): void {
|
|
this.#customBgFn = customBgFn;
|
|
this.#cachedText = undefined;
|
|
this.#cachedWidth = undefined;
|
|
this.#cachedLines = undefined;
|
|
}
|
|
|
|
invalidate(): void {
|
|
this.#cachedText = undefined;
|
|
this.#cachedWidth = undefined;
|
|
this.#cachedLines = undefined;
|
|
}
|
|
|
|
render(width: number): string[] {
|
|
// Check cache
|
|
if (this.#cachedLines && this.#cachedText === this.#text && this.#cachedWidth === width) {
|
|
return this.#cachedLines;
|
|
}
|
|
|
|
// Don't render anything if there's no actual text
|
|
if (!this.#text || this.#text.trim() === "") {
|
|
const result: string[] = [];
|
|
this.#cachedText = this.#text;
|
|
this.#cachedWidth = width;
|
|
this.#cachedLines = result;
|
|
return result;
|
|
}
|
|
|
|
// Replace tabs with 3 spaces
|
|
const normalizedText = replaceTabs(this.#text);
|
|
|
|
// Calculate content width (subtract left/right margins)
|
|
const contentWidth = Math.max(1, width - this.#paddingX * 2);
|
|
|
|
// Wrap text (this preserves ANSI codes but does NOT pad)
|
|
const wrappedLines = wrapTextWithAnsi(normalizedText, contentWidth);
|
|
|
|
// Add margins and background to each line
|
|
const leftMargin = padding(this.#paddingX);
|
|
const rightMargin = padding(this.#paddingX);
|
|
const contentLines: string[] = [];
|
|
|
|
for (const line of wrappedLines) {
|
|
// Add margins
|
|
const lineWithMargins = leftMargin + line + rightMargin;
|
|
|
|
// Apply background if specified (this also pads to full width)
|
|
if (this.#customBgFn) {
|
|
contentLines.push(applyBackgroundToLine(lineWithMargins, width, this.#customBgFn));
|
|
} else {
|
|
// No background - just pad to width with spaces
|
|
const visibleLen = visibleWidth(lineWithMargins);
|
|
const paddingNeeded = Math.max(0, width - visibleLen);
|
|
contentLines.push(lineWithMargins + padding(paddingNeeded));
|
|
}
|
|
}
|
|
|
|
// Add top/bottom padding (empty lines)
|
|
const emptyLine = padding(width);
|
|
const emptyLines: string[] = [];
|
|
for (let i = 0; i < this.#paddingY; i++) {
|
|
const line = this.#customBgFn ? applyBackgroundToLine(emptyLine, width, this.#customBgFn) : emptyLine;
|
|
emptyLines.push(line);
|
|
}
|
|
|
|
const result = [...emptyLines, ...contentLines, ...emptyLines];
|
|
|
|
// Update cache
|
|
this.#cachedText = this.#text;
|
|
this.#cachedWidth = width;
|
|
this.#cachedLines = result;
|
|
|
|
return result.length > 0 ? result : [""];
|
|
}
|
|
}
|