Monorepo for Aesthetic.Computer
aesthetic.computer
1# Button Width Sizing Issue - Technical Plan
2
3## Problem Summary
4The HUD button width calculation for KidLisp syntax highlighting is not working optimally. Buttons are sometimes too wide, too narrow, or inconsistent when resizing windows.
5
6## Current State (September 6, 2025)
7
8### Key Files & Code Locations
9- **Primary File**: `/workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/disk.mjs`
10- **Button Width Logic**: Lines ~8000-8070 (makeFrame function)
11- **Text Rendering Pipeline**: `write() → text.box() → typeface.print() → $.printLine()`
12- **Supporting Files**:
13 - `type.mjs`: Typeface class with blockWidth/blockHeight properties
14 - `kidlisp.mjs`: updateHUDWithSyntaxHighlighting() function
15
16### Current Implementation Details
17
18#### Text Cleaning (Lines ~8000-8015)
19```javascript
20// Use plain text for width calculation to avoid counting color codes
21const textForWidthCalculation = currentHUDPlainTxt || currentHUDTxt;
22
23// Double-check: strip color codes directly if they're still present
24const colorCodeRegex = /\\[^\\]*\\/g;
25const cleanText = textForWidthCalculation.replace(colorCodeRegex, '');
26```
27
28#### Button Width Calculation (Lines ~8015-8035)
29```javascript
30// Use consistent scaling regardless of screen size
31const screenScale = 1.0;
32const scaledBlockWidth = tf.blockWidth * screenScale;
33
34// Calculate button width based on text content
35let maxButtonWidth;
36
37if (cleanText.includes('\n')) {
38 // Multiline: calculate bounds based on longest line with generous buffer to prevent wrapping
39 const lines = cleanText.split('\n');
40 const longestLineLength = Math.max(...lines.map(line => line.length));
41 maxButtonWidth = (longestLineLength + 10) * scaledBlockWidth; // Add larger buffer to prevent wrapping
42} else {
43 // Single line: use generous bounds to ensure text is never cut off
44 maxButtonWidth = Math.max(cleanText.length * scaledBlockWidth + scaledBlockWidth * 4, $api.screen.width);
45}
46
47// Use text.box to get proper layout
48const textBounds = $api.text.box(
49 cleanText,
50 undefined,
51 maxButtonWidth,
52 screenScale
53);
54
55// Use the width from text.box
56const textWidth = textBounds.box.width;
57const padding = scaledBlockWidth * 1.5; // 1.5 character buffer
58let w = textWidth + currentHUDScrub + padding;
59```
60
61#### Height Calculation (Lines ~8065-8070)
62```javascript
63// Use the text box height with padding - add full line height for multiline text
64const heightPadding = cleanText.includes('\n') ?
65 tf.blockHeight * screenScale + 4 : // Full line height + 4px for multiline
66 tf.blockHeight * screenScale * 0.4; // 40% for single line
67const h = textBounds.box.height + heightPadding;
68```
69
70### Font System Properties
71- **Default Font**: font_1 (monospace, defined in `/workspaces/aesthetic-computer/system/public/aesthetic.computer/disks/common/fonts.mjs`)
72- **Glyph Width**: 6px (`tf.blockWidth` from `font_1.glyphWidth`)
73- **Glyph Height**: 10px (`tf.blockHeight` from `font_1.glyphHeight`)
74- **Screen Scale**: 1.0 (fixed, no longer responsive)
75- **Global Typeface**: `tf` initialized at line 4729 in disk.mjs as `new Typeface().load()`
76
77### Font System Architecture
78
79#### Font Definition Structure (fonts.mjs)
80```javascript
81export const font_1 = {
82 glyphHeight: 10, // Natural block height (tf.blockHeight)
83 glyphWidth: 6, // Natural block width (tf.blockWidth)
84 proportional: false, // Monospace font - fixed character width
85 // Character mappings...
86 a: "lowercase/a - 2022.1.11.16.12.07",
87 // etc...
88};
89```
90
91#### Typeface Class (type.mjs)
92**Location**: `/workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/type.mjs`
93
94**Key Properties**:
95- `get blockWidth()` (line 35): Returns `this.data.glyphWidth` (6px for font_1)
96- `get blockHeight()` (line 46): Returns `this.data.glyphHeight` (10px for font_1)
97- `constructor(name = "font_1")` (line 23): Defaults to font_1, loads from fonts.mjs
98
99**Important Methods**:
100- `print()` (line 319): Main text rendering method, handles proportional vs monospace
101- `load()` (line 50): Preloads glyphs, handles different font types (BDF, microtype, etc.)
102
103#### Global Font Initialization (disk.mjs)
104**Location**: Line 4729 in `/workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/disk.mjs`
105```javascript
106if (!tf) tf = await new Typeface(/*"unifont"*/).load($commonApi.net.preload);
107```
108- `tf` is the global typeface instance used throughout the system
109- Defaults to font_1 (6px × 10px monospace bitmap font)
110- Loaded once when first piece loads
111- `tf.blockWidth` and `tf.blockHeight` are the source values for all width/height calculations
112
113#### Text Width Calculation Methods
114
115**1. Monospace (font_1 - current default)**:
116```javascript
117const textWidth = text.length * tf.blockWidth; // Simple: chars × 6px
118```
119
120**2. Proportional (MatrixChunky8, unifont)**:
121```javascript
122// Uses character-specific advance widths from font definition
123const advances = this.data?.advances || {};
124let totalWidth = 0;
125[...text].forEach(char => {
126 totalWidth += (advances[char] || defaultWidth) * scale;
127});
128```
129
130#### Font Types Available
1311. **font_1** (default): 6×10px monospace bitmap font
1322. **MatrixChunky8**: Proportional BDF font with character-specific widths
1333. **unifont**: 8×16px monospace Unicode font via BDF
1344. **microtype**: 4×5px ultra-compact font for special cases
135
136#### Text Rendering Pipeline
137```
138write() → text.box() → typeface.print() → $.printLine()
139```
1401. `write()`: Entry point for text rendering
1412. `text.box()`: Layout calculation using `tf.blockWidth * scale`
1423. `typeface.print()`: Character-by-character rendering with proper spacing
1434. `$.printLine()`: Low-level pixel rendering
144
145#### Critical Width Calculation Insight
146**The Issue**: `text.box()` defaults to total character count when bounds is undefined:
147```javascript
148// In text.box() at line 1745:
149if (bounds === undefined) bounds = (text.length + 2) * blockWidth;
150```
151**For 239-char multiline text**: `(239 + 2) * 6 = 1446px` (way too wide!)
152**Should be**: Based on longest line, not total character count
153
154#### Natural Block Width Values
155- **tf.blockWidth**: 6px (from font_1.glyphWidth)
156- **tf.blockHeight**: 10px (from font_1.glyphHeight)
157- **With scaling**: `scaledBlockWidth = tf.blockWidth * screenScale`
158- **In button calc**: Used as base unit for all width/height calculations
159
160### Known Issues
161
162#### 1. Debug Output Analysis
163Recent debug showed:
164```
165cleanText: "fade:red-blue-black-blue-red\nink (? rainbow white 0)..."
166cleanTextLength: 239
167textWidth: 1084.5 (too wide!)
168finalWidth: 1091.25
169```
170
171**Root Cause**: When `maxButtonWidth` is undefined, text.box() defaults to `(text.length + 2) * blockWidth = (239 + 2) * 4.5 = 1084.5px`
172
173#### 2. Window Resize Behavior
174- User wants consistent button size regardless of window dimensions
175- Text should never be cut off or wrapped unexpectedly
176- Current implementation removes responsive scaling but still has sizing inconsistencies
177
178#### 3. Text.box() Function Behavior (Lines 1740-1750)
179```javascript
180if (bounds === undefined) bounds = (text.length + 2) * blockWidth;
181```
182This is the source of the 1084px width - it uses total character count instead of longest line.
183
184### Architecture Decisions Made
185
186#### ✅ Preserved Features
187- Multiline text boundary support maintained
188- Original text rendering pipeline intact
189- KidLisp syntax highlighting integration working
190- Single-line vs multiline handling separation
191
192#### ✅ Fixed Issues
193- Removed responsive scaling (screenScale always 1.0)
194- Fixed duplicate variable declarations
195- Added proper height padding for multiline (full line height + 4px)
196- Improved color code stripping
197
198#### ❌ Remaining Issues
199- Multiline button width still inconsistent
200- Debug logging not comprehensive enough
201- Text wrapping edge cases not fully handled
202
203### User Requirements
2041. **Always show full text**: Never cut off or wrap unexpectedly
2052. **Consistent sizing**: Button dimensions shouldn't change on window resize
2063. **Optimal width**: Width should match longest line, not total character count
2074. **Multiline support**: Preserve existing multiline text layout capabilities
208
209### Suggested Next Steps for New Agent
210
211#### Immediate Priority
2121. **Fix multiline width calculation**: Ensure maxButtonWidth for multiline uses longest line length, not total text length
2132. **Implement better debug logging**: Log dimensions only when they change per frame
2143. **Test edge cases**: Very long lines, mixed content, empty lines
215
216#### Implementation Strategy
217```javascript
218// Proposed improved approach for multiline:
219if (cleanText.includes('\n')) {
220 const lines = cleanText.split('\n');
221 const longestLineLength = Math.max(...lines.map(line => line.length));
222 // Use actual longest line + minimal buffer, not total character count
223 maxButtonWidth = longestLineLength * scaledBlockWidth + (scaledBlockWidth * 2);
224}
225```
226
227#### Debug Logging Strategy
228```javascript
229// Track dimensions per frame and only log changes:
230const currentDimensions = { width: textBounds.box.width, height: textBounds.box.height };
231if (!window.lastButtonDimensions ||
232 window.lastButtonDimensions.width !== currentDimensions.width ||
233 window.lastButtonDimensions.height !== currentDimensions.height) {
234 // Log comprehensive button state
235 window.lastButtonDimensions = currentDimensions;
236}
237```
238
239### Test Cases to Validate
2401. **Single-line commands**: "rect 255 255 255 128" should be ~120px wide
2412. **Multiline KidLisp**: 8-line code block should size to longest line width
2423. **Window resize**: Button dimensions should remain constant
2434. **Empty lines**: Multiline with blank lines should handle correctly
2445. **Very long lines**: Lines exceeding screen width should not wrap
245
246### Context for New Agent
247- This is aesthetic-computer, a creative coding platform
248- KidLisp is the Lisp interpreter with syntax highlighting
249- HUD shows command history with colored syntax
250- Text rendering uses monospace font with precise pixel calculations
251- User prioritizes functionality over responsive design
252
253### Font System Research Locations
254**Primary Files**:
255- `/workspaces/aesthetic-computer/system/public/aesthetic.computer/disks/common/fonts.mjs` - Font definitions and glyph mappings
256- `/workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/type.mjs` - Typeface class and text rendering logic
257- `/workspaces/aesthetic-computer/system/public/aesthetic.computer/lib/disk.mjs` - Global font initialization and text.box() function
258
259**Key Research Points**:
2601. **Font Metrics**: Search for `glyphWidth`, `glyphHeight`, `blockWidth`, `blockHeight` in type.mjs and fonts.mjs
2612. **Text Layout**: Study `text.box()` function at disk.mjs:1733-1850 for width calculation logic
2623. **Proportional Fonts**: Check `advances` property in fonts.mjs for character-specific widths
2634. **Font Loading**: Examine `tf` initialization at disk.mjs:4729 and Typeface constructor
2645. **Width Calculations**: Look for `tf.blockWidth * scale` patterns throughout disk.mjs
265
266**Font System Debug Commands**:
267```javascript
268// In browser console:
269console.log("Font info:", tf.blockWidth, tf.blockHeight, tf.data);
270console.log("Font_1 definition:", fonts.font_1);
271console.log("Text box test:", $api.text.box("test", undefined, undefined, 1));
272```
273
274## Session History Summary
275- Started with buttons too wide (200px+ for short text)
276- Evolved through multiple approaches: character-based calculation, screen percentage, longest-line calculation
277- Discovered text.box() default behavior causing width inflation
278- Fixed height padding and screen scaling consistency
279- Still need optimal multiline width solution
280
281---
282*Created: September 6, 2025*
283*Status: Ready for new agent continuation*