Monorepo for Aesthetic.Computer aesthetic.computer

Button Width Sizing Issue - Technical Plan#

Problem Summary#

The HUD button width calculation for KidLisp syntax highlighting is not working optimally. Buttons are sometimes too wide, too narrow, or inconsistent when resizing windows.

Current State (September 6, 2025)#

Key Files & Code Locations#

  • Primary File: /workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/disk.mjs
  • Button Width Logic: Lines ~8000-8070 (makeFrame function)
  • Text Rendering Pipeline: write() → text.box() → typeface.print() → $.printLine()
  • Supporting Files:
    • type.mjs: Typeface class with blockWidth/blockHeight properties
    • kidlisp.mjs: updateHUDWithSyntaxHighlighting() function

Current Implementation Details#

Text Cleaning (Lines ~8000-8015)#

// Use plain text for width calculation to avoid counting color codes
const textForWidthCalculation = currentHUDPlainTxt || currentHUDTxt;

// Double-check: strip color codes directly if they're still present
const colorCodeRegex = /\\[^\\]*\\/g;
const cleanText = textForWidthCalculation.replace(colorCodeRegex, '');

Button Width Calculation (Lines ~8015-8035)#

// Use consistent scaling regardless of screen size
const screenScale = 1.0;
const scaledBlockWidth = tf.blockWidth * screenScale;

// Calculate button width based on text content
let maxButtonWidth;

if (cleanText.includes('\n')) {
  // Multiline: calculate bounds based on longest line with generous buffer to prevent wrapping
  const lines = cleanText.split('\n');
  const longestLineLength = Math.max(...lines.map(line => line.length));
  maxButtonWidth = (longestLineLength + 10) * scaledBlockWidth; // Add larger buffer to prevent wrapping
} else {
  // Single line: use generous bounds to ensure text is never cut off
  maxButtonWidth = Math.max(cleanText.length * scaledBlockWidth + scaledBlockWidth * 4, $api.screen.width);
}

// Use text.box to get proper layout
const textBounds = $api.text.box(
  cleanText,
  undefined,
  maxButtonWidth,
  screenScale
);

// Use the width from text.box
const textWidth = textBounds.box.width;
const padding = scaledBlockWidth * 1.5; // 1.5 character buffer
let w = textWidth + currentHUDScrub + padding;

Height Calculation (Lines ~8065-8070)#

// Use the text box height with padding - add full line height for multiline text
const heightPadding = cleanText.includes('\n') ? 
  tf.blockHeight * screenScale + 4 : // Full line height + 4px for multiline
  tf.blockHeight * screenScale * 0.4; // 40% for single line
const h = textBounds.box.height + heightPadding;

Font System Properties#

  • Default Font: font_1 (monospace, defined in /workspaces/aesthetic-computer/system/public/aesthetic.computer/disks/common/fonts.mjs)
  • Glyph Width: 6px (tf.blockWidth from font_1.glyphWidth)
  • Glyph Height: 10px (tf.blockHeight from font_1.glyphHeight)
  • Screen Scale: 1.0 (fixed, no longer responsive)
  • Global Typeface: tf initialized at line 4729 in disk.mjs as new Typeface().load()

Font System Architecture#

Font Definition Structure (fonts.mjs)#

export const font_1 = {
  glyphHeight: 10,           // Natural block height (tf.blockHeight)
  glyphWidth: 6,             // Natural block width (tf.blockWidth) 
  proportional: false,       // Monospace font - fixed character width
  // Character mappings...
  a: "lowercase/a - 2022.1.11.16.12.07",
  // etc...
};

Typeface Class (type.mjs)#

Location: /workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/type.mjs

Key Properties:

  • get blockWidth() (line 35): Returns this.data.glyphWidth (6px for font_1)
  • get blockHeight() (line 46): Returns this.data.glyphHeight (10px for font_1)
  • constructor(name = "font_1") (line 23): Defaults to font_1, loads from fonts.mjs

Important Methods:

  • print() (line 319): Main text rendering method, handles proportional vs monospace
  • load() (line 50): Preloads glyphs, handles different font types (BDF, microtype, etc.)

Global Font Initialization (disk.mjs)#

Location: Line 4729 in /workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/disk.mjs

if (!tf) tf = await new Typeface(/*"unifont"*/).load($commonApi.net.preload);
  • tf is the global typeface instance used throughout the system
  • Defaults to font_1 (6px × 10px monospace bitmap font)
  • Loaded once when first piece loads
  • tf.blockWidth and tf.blockHeight are the source values for all width/height calculations

Text Width Calculation Methods#

1. Monospace (font_1 - current default):

const textWidth = text.length * tf.blockWidth; // Simple: chars × 6px

2. Proportional (MatrixChunky8, unifont):

// Uses character-specific advance widths from font definition
const advances = this.data?.advances || {};
let totalWidth = 0;
[...text].forEach(char => {
  totalWidth += (advances[char] || defaultWidth) * scale;
});

Font Types Available#

  1. font_1 (default): 6×10px monospace bitmap font
  2. MatrixChunky8: Proportional BDF font with character-specific widths
  3. unifont: 8×16px monospace Unicode font via BDF
  4. microtype: 4×5px ultra-compact font for special cases

Text Rendering Pipeline#

write() → text.box() → typeface.print() → $.printLine()
  1. write(): Entry point for text rendering
  2. text.box(): Layout calculation using tf.blockWidth * scale
  3. typeface.print(): Character-by-character rendering with proper spacing
  4. $.printLine(): Low-level pixel rendering

Critical Width Calculation Insight#

The Issue: text.box() defaults to total character count when bounds is undefined:

// In text.box() at line 1745:
if (bounds === undefined) bounds = (text.length + 2) * blockWidth;

For 239-char multiline text: (239 + 2) * 6 = 1446px (way too wide!) Should be: Based on longest line, not total character count

Natural Block Width Values#

  • tf.blockWidth: 6px (from font_1.glyphWidth)
  • tf.blockHeight: 10px (from font_1.glyphHeight)
  • With scaling: scaledBlockWidth = tf.blockWidth * screenScale
  • In button calc: Used as base unit for all width/height calculations

Known Issues#

1. Debug Output Analysis#

Recent debug showed:

cleanText: "fade:red-blue-black-blue-red\nink (? rainbow white 0)..."
cleanTextLength: 239
textWidth: 1084.5 (too wide!)
finalWidth: 1091.25

Root Cause: When maxButtonWidth is undefined, text.box() defaults to (text.length + 2) * blockWidth = (239 + 2) * 4.5 = 1084.5px

2. Window Resize Behavior#

  • User wants consistent button size regardless of window dimensions
  • Text should never be cut off or wrapped unexpectedly
  • Current implementation removes responsive scaling but still has sizing inconsistencies

3. Text.box() Function Behavior (Lines 1740-1750)#

if (bounds === undefined) bounds = (text.length + 2) * blockWidth;

This is the source of the 1084px width - it uses total character count instead of longest line.

Architecture Decisions Made#

✅ Preserved Features#

  • Multiline text boundary support maintained
  • Original text rendering pipeline intact
  • KidLisp syntax highlighting integration working
  • Single-line vs multiline handling separation

✅ Fixed Issues#

  • Removed responsive scaling (screenScale always 1.0)
  • Fixed duplicate variable declarations
  • Added proper height padding for multiline (full line height + 4px)
  • Improved color code stripping

❌ Remaining Issues#

  • Multiline button width still inconsistent
  • Debug logging not comprehensive enough
  • Text wrapping edge cases not fully handled

User Requirements#

  1. Always show full text: Never cut off or wrap unexpectedly
  2. Consistent sizing: Button dimensions shouldn't change on window resize
  3. Optimal width: Width should match longest line, not total character count
  4. Multiline support: Preserve existing multiline text layout capabilities

Suggested Next Steps for New Agent#

Immediate Priority#

  1. Fix multiline width calculation: Ensure maxButtonWidth for multiline uses longest line length, not total text length
  2. Implement better debug logging: Log dimensions only when they change per frame
  3. Test edge cases: Very long lines, mixed content, empty lines

Implementation Strategy#

// Proposed improved approach for multiline:
if (cleanText.includes('\n')) {
  const lines = cleanText.split('\n');
  const longestLineLength = Math.max(...lines.map(line => line.length));
  // Use actual longest line + minimal buffer, not total character count
  maxButtonWidth = longestLineLength * scaledBlockWidth + (scaledBlockWidth * 2);
}

Debug Logging Strategy#

// Track dimensions per frame and only log changes:
const currentDimensions = { width: textBounds.box.width, height: textBounds.box.height };
if (!window.lastButtonDimensions || 
    window.lastButtonDimensions.width !== currentDimensions.width ||
    window.lastButtonDimensions.height !== currentDimensions.height) {
  // Log comprehensive button state
  window.lastButtonDimensions = currentDimensions;
}

Test Cases to Validate#

  1. Single-line commands: "rect 255 255 255 128" should be ~120px wide
  2. Multiline KidLisp: 8-line code block should size to longest line width
  3. Window resize: Button dimensions should remain constant
  4. Empty lines: Multiline with blank lines should handle correctly
  5. Very long lines: Lines exceeding screen width should not wrap

Context for New Agent#

  • This is aesthetic-computer, a creative coding platform
  • KidLisp is the Lisp interpreter with syntax highlighting
  • HUD shows command history with colored syntax
  • Text rendering uses monospace font with precise pixel calculations
  • User prioritizes functionality over responsive design

Font System Research Locations#

Primary Files:

  • /workspaces/aesthetic-computer/system/public/aesthetic.computer/disks/common/fonts.mjs - Font definitions and glyph mappings
  • /workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/type.mjs - Typeface class and text rendering logic
  • /workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/disk.mjs - Global font initialization and text.box() function

Key Research Points:

  1. Font Metrics: Search for glyphWidth, glyphHeight, blockWidth, blockHeight in type.mjs and fonts.mjs
  2. Text Layout: Study text.box() function at disk.mjs:1733-1850 for width calculation logic
  3. Proportional Fonts: Check advances property in fonts.mjs for character-specific widths
  4. Font Loading: Examine tf initialization at disk.mjs:4729 and Typeface constructor
  5. Width Calculations: Look for tf.blockWidth * scale patterns throughout disk.mjs

Font System Debug Commands:

// In browser console:
console.log("Font info:", tf.blockWidth, tf.blockHeight, tf.data);
console.log("Font_1 definition:", fonts.font_1);
console.log("Text box test:", $api.text.box("test", undefined, undefined, 1));

Session History Summary#

  • Started with buttons too wide (200px+ for short text)
  • Evolved through multiple approaches: character-based calculation, screen percentage, longest-line calculation
  • Discovered text.box() default behavior causing width inflation
  • Fixed height padding and screen scaling consistency
  • Still need optimal multiline width solution

Created: September 6, 2025 Status: Ready for new agent continuation