UNPKG

asciitorium

Version:
212 lines (211 loc) 8.94 kB
import { Component } from '../core/Component.js'; import { requestRender } from '../core/RenderScheduler.js'; import { State } from '../core/State.js'; import { AssetManager } from '../core/AssetManager.js'; export class Banner extends Component { constructor(options) { const borderPadding = options.border ? 2 : 0; // Use placeholder dimensions during loading const placeholderWidth = 12; // "Loading..." length + border const placeholderHeight = 1 + borderPadding; // Call super() with placeholder dimensions const { font, text, letterSpacing, ...componentProps } = options; super({ ...componentProps, width: options.width ?? options.style?.width ?? placeholderWidth, height: options.height ?? options.style?.height ?? placeholderHeight, }); this.isLoading = false; this.letterSpacing = 0; this.isDestroyed = false; // Track if component has been destroyed // Set letter spacing (default 0 if not provided) this.letterSpacing = letterSpacing ?? 0; // Set font and text this.font = font; // Handle text prop (string or State) if (text instanceof State) { this.textState = text; this.text = text.value; } else { this.text = text || ''; } // Set loading state this.isLoading = true; // Start async loading using AssetManager AssetManager.getFont(font) .then((fontAsset) => { if (this.isDestroyed) return; this.isLoading = false; this.loadError = undefined; this.fontAsset = fontAsset; // Recalculate dimensions based on text and font const renderedBuffer = this.renderTextWithFont(this.text, fontAsset); const textWidth = Math.max(...renderedBuffer.map((line) => line.length), 0); const textHeight = renderedBuffer.length; this.originalHeight = textHeight + borderPadding; this.originalWidth = textWidth + borderPadding; this.width = textWidth + borderPadding; this.height = textHeight + borderPadding; // Set up state subscription for reactive text if (this.textState) { this.bind(this.textState, (newValue) => { this.text = newValue; this.updateFontText(); }); } requestRender(); this.forceRenderIfNeeded(); }) .catch((error) => { if (this.isDestroyed) return; this.isLoading = false; this.loadError = error.message || 'Failed to load font'; console.error(`Banner: Failed to load font "${this.font}":`, error); requestRender(); this.forceRenderIfNeeded(); }); } forceRenderIfNeeded() { // Use the base class method for focus refresh (which also triggers render) this.notifyAppOfFocusRefresh(); } renderTextWithFont(text, fontAsset) { if (!text || text.length === 0) { return [[]]; } const height = fontAsset.height; const result = []; // Initialize result buffer with empty strings for (let y = 0; y < height; y++) { result.push([]); } // Process each character for (let i = 0; i < text.length; i++) { const char = text[i]; const glyph = fontAsset.glyphs.get(char); if (!glyph) { // Fallback: render the character itself as a single-width glyph for (let y = 0; y < height; y++) { if (y === 0) { result[y].push(char); } else { result[y].push(' '); } } } else { // Render the glyph - each line must be padded to glyph.width for (let y = 0; y < height; y++) { if (y < glyph.lines.length) { const glyphLine = glyph.lines[y]; // Add characters from the glyph line for (let x = 0; x < glyphLine.length; x++) { result[y].push(glyphLine[x]); } // Pad the rest with spaces to reach glyph.width for (let x = glyphLine.length; x < glyph.width; x++) { result[y].push(' '); } } else { // Pad entire line with spaces if glyph is shorter than font height for (let x = 0; x < glyph.width; x++) { result[y].push(' '); } } } } // Add letter spacing after each character (except the last one) if (i < text.length - 1 && this.letterSpacing > 0) { for (let y = 0; y < height; y++) { for (let s = 0; s < this.letterSpacing; s++) { result[y].push(' '); } } } } return result; } updateFontText() { if (this.isDestroyed) return; if (!this.fontAsset || !this.text) return; // Recalculate dimensions based on new text const renderedBuffer = this.renderTextWithFont(this.text, this.fontAsset); const textWidth = Math.max(...renderedBuffer.map((line) => line.length), 0); const textHeight = renderedBuffer.length; const borderPadding = this.border ? 2 : 0; this.originalHeight = textHeight + borderPadding; this.originalWidth = textWidth + borderPadding; this.width = textWidth + borderPadding; this.height = textHeight + borderPadding; // Request a re-render requestRender(); } destroy() { this.isDestroyed = true; super.destroy(); // Note: Component.destroy() automatically handles state unsubscriptions } draw() { try { const buffer = super.draw(); // Defensive check: ensure buffer was created successfully if (!buffer || buffer.length === 0) { return buffer; } const xOffset = this.border ? 1 : 0; const yOffset = this.border ? 1 : 0; const innerWidth = this.width - (this.border ? 2 : 0); const innerHeight = this.height - (this.border ? 2 : 0); // Show loading or error state if (this.isLoading || this.loadError) { const displayText = this.loadError ? `Error: ${this.loadError}` : 'Loading...'; const chars = [...displayText]; const bufferY = yOffset; if (bufferY < buffer.length) { for (let x = 0; x < Math.min(chars.length, innerWidth); x++) { const bufferX = x + xOffset; if (buffer[bufferY] && bufferX < buffer[bufferY].length) { buffer[bufferY][bufferX] = chars[x]; } } } this.buffer = buffer; return buffer; } // Font rendering mode if (this.fontAsset && this.text !== undefined) { const lines = this.renderTextWithFont(this.text, this.fontAsset); for (let y = 0; y < Math.min(lines.length, innerHeight); y++) { const line = lines[y]; const bufferY = y + yOffset; // Defensive check: ensure buffer row exists (race condition protection) if (bufferY >= buffer.length) break; for (let x = 0; x < Math.min(line.length, innerWidth); x++) { const bufferX = x + xOffset; // Defensive check: ensure buffer column exists (race condition protection) if (!buffer[bufferY] || bufferX >= buffer[bufferY].length) break; const char = line[x]; buffer[bufferY][bufferX] = char; } } this.buffer = buffer; return buffer; } return buffer; } catch (error) { // If any error occurs during draw, return empty buffer to prevent crash console.error('Banner component draw() error:', error); return super.draw(); // Return basic empty buffer } } }