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 propertieskidlisp.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.blockWidthfromfont_1.glyphWidth) - Glyph Height: 10px (
tf.blockHeightfromfont_1.glyphHeight) - Screen Scale: 1.0 (fixed, no longer responsive)
- Global Typeface:
tfinitialized at line 4729 in disk.mjs asnew 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): Returnsthis.data.glyphWidth(6px for font_1)get blockHeight()(line 46): Returnsthis.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 monospaceload()(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);
tfis the global typeface instance used throughout the system- Defaults to font_1 (6px × 10px monospace bitmap font)
- Loaded once when first piece loads
tf.blockWidthandtf.blockHeightare 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#
- font_1 (default): 6×10px monospace bitmap font
- MatrixChunky8: Proportional BDF font with character-specific widths
- unifont: 8×16px monospace Unicode font via BDF
- microtype: 4×5px ultra-compact font for special cases
Text Rendering Pipeline#
write() → text.box() → typeface.print() → $.printLine()
write(): Entry point for text renderingtext.box(): Layout calculation usingtf.blockWidth * scaletypeface.print(): Character-by-character rendering with proper spacing$.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#
- Always show full text: Never cut off or wrap unexpectedly
- Consistent sizing: Button dimensions shouldn't change on window resize
- Optimal width: Width should match longest line, not total character count
- Multiline support: Preserve existing multiline text layout capabilities
Suggested Next Steps for New Agent#
Immediate Priority#
- Fix multiline width calculation: Ensure maxButtonWidth for multiline uses longest line length, not total text length
- Implement better debug logging: Log dimensions only when they change per frame
- 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#
- Single-line commands: "rect 255 255 255 128" should be ~120px wide
- Multiline KidLisp: 8-line code block should size to longest line width
- Window resize: Button dimensions should remain constant
- Empty lines: Multiline with blank lines should handle correctly
- 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:
- Font Metrics: Search for
glyphWidth,glyphHeight,blockWidth,blockHeightin type.mjs and fonts.mjs - Text Layout: Study
text.box()function at disk.mjs:1733-1850 for width calculation logic - Proportional Fonts: Check
advancesproperty in fonts.mjs for character-specific widths - Font Loading: Examine
tfinitialization at disk.mjs:4729 and Typeface constructor - Width Calculations: Look for
tf.blockWidth * scalepatterns 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