From 0d207769b988758faaf52a13f3c2fa704354a032 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Tue, 22 Sep 2026 18:11:42 +0200 Subject: [PATCH] Focused text fields show their outline on every edge (#202) The focus outline of a text field was only visible at its corners. The mask that clips the text covered the whole field, border included, so the 1 px stroke was cut away along every straight edge. Only the rounded corners bent inwards far enough to survive. With the larger radius the new theme tried, this became obvious. - TextField puts text, selection and caret into their own container. Only that container is masked. Background and outline are not. - NumberField and the rename field in lists inherit the fix. - The theme radius goes back to 2 px. - New debug hook __gm.focusedRect() returns where the focused widget sits, so a check can read its border. Tests: - ./check.sh: 141 files, 2080 tests, exit 0 - GM_STEPS=2: ALL PASSED, 24 checks (one new) - The new check in 2c reads the middle of all four edges of the focused name field. All four must be the focus blue. - Counter-check with the old full-field mask: the check fails, all four edges read #24272e. - Editor check total is now 1286 (1285 + 1), computed, not measured. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- src/editor/debugHook.ts | 5 +++++ src/ui/Theme.ts | 2 +- src/ui/widgets/TextField.ts | 17 +++++++---------- tools/e2e/02-inspector.mjs | 15 +++++++++++++++ 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f92b2175..73883ebe 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh The browser suite and the three example games need a machine with a GPU: ```bash -node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1285 checks (GM_STEPS=20 for one step) +node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1286 checks (GM_STEPS=20 for one step) # every tool signs in first – see the Accounts page node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs diff --git a/src/editor/debugHook.ts b/src/editor/debugHook.ts index 45d927f7..669e3d5b 100644 --- a/src/editor/debugHook.ts +++ b/src/editor/debugHook.ts @@ -210,6 +210,11 @@ export function installDebugHook(app: Application, state: EditorState, editor: E inspectorField: fieldOf, /** The inspector panel on screen – what tells whether a row of it can be clicked at all. */ inspectorRect: () => rectOf(inspector), + /** Where the focused widget sits on screen, or `null` – for checks on its focus border. */ + focusedRect: () => { + const widget = ui().focus.focused; + return widget ? rectOf(widget) : null; + }, inspectorInfo: () => inspector.debugInfo(), textColor, overlayTexts, diff --git a/src/ui/Theme.ts b/src/ui/Theme.ts index e37242f6..15270b92 100644 --- a/src/ui/Theme.ts +++ b/src/ui/Theme.ts @@ -12,7 +12,7 @@ export const Theme = { controlHeight: 24, padding: 8, gap: 4, - radius: 8, + radius: 2, fieldPadding: 4, checkboxSize: 16, scrollbarWidth: 4, diff --git a/src/ui/widgets/TextField.ts b/src/ui/widgets/TextField.ts index b2f48b38..98910d95 100644 --- a/src/ui/widgets/TextField.ts +++ b/src/ui/widgets/TextField.ts @@ -1,4 +1,4 @@ -import { CanvasTextMetrics, type FederatedPointerEvent, Graphics, Text } from 'pixi.js'; +import { CanvasTextMetrics, Container, type FederatedPointerEvent, Graphics, Text } from 'pixi.js'; import { pressCount } from '../clicks'; import { drawControlBox } from '../draw'; import type { TextEditTarget } from '../KeyboardBridge'; @@ -66,15 +66,12 @@ export class TextField extends Widget implements TextEditTarget { this.minWidth = options.width ?? 80; this.textNode = new Text({ text: value, style: textStyle(options.style ?? 'body') }); this.placeholderNode = new Text({ text: this.placeholder, style: textStyle('muted') }); - this.addChild( - this.bg, - this.selectionShape, - this.placeholderNode, - this.textNode, - this.caret, - this.clipShape, - ); - this.mask = this.clipShape; + // Only the contents are clipped: a mask on the whole field cut its border away along the + // straight edges and left it showing at the rounded corners alone. + const content = new Container(); + content.addChild(this.selectionShape, this.placeholderNode, this.textNode, this.caret); + content.mask = this.clipShape; + this.addChild(this.bg, content, this.clipShape); this.trackHover(); this.on('pointerdown', (e: FederatedPointerEvent) => this.handlePointerDown(e)); diff --git a/tools/e2e/02-inspector.mjs b/tools/e2e/02-inspector.mjs index c2c35d28..8fd9bc8a 100644 --- a/tools/e2e/02-inspector.mjs +++ b/tools/e2e/02-inspector.mjs @@ -63,6 +63,21 @@ export async function inspector(t) { await press('a', { ctrl: true }); await typeText('Renamed crate'); await shot('03-typing'); + // The focus border runs all the way round, not only through the rounded corners: the field's + // clip once took in its own border and cut the straight edges away. + const field = await json(`__gm.focusedRect()`); + const edges = field + ? await json( + `[[${field.x + field.w / 2}, ${field.y}], [${field.x + field.w / 2}, ${field.y + field.h - 1}],` + + ` [${field.x}, ${field.y + field.h / 2}], [${field.x + field.w - 1}, ${field.y + field.h / 2}]]` + + `.map(([x, y]) => __gm.pixelAt(Math.round(x), Math.round(y)))`, + ) + : []; + check( + 'the focused field is outlined along all four edges', + edges.length === 4 && edges.every((c) => c === '#4c8dff'), + JSON.stringify({ field, edges }), + ); await press('Enter'); check( 'typing + Enter renamed the object',