Focused text fields show their outline on every edge (#202)
game-maker / suite (push) Failing after 1m15s
game-maker / gate (push) Successful in 2m15s

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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-22 18:11:42 +02:00
1 parent 940a78e8ee
commit 0d207769b9
5 files changed
+29 -12

No files matched your search

+1 -1
View File
@@ -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: The browser suite and the three example games need a machine with a GPU:
```bash ```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 # every tool signs in first – see the Accounts page
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
+5
View File
@@ -210,6 +210,11 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
inspectorField: fieldOf, inspectorField: fieldOf,
/** The inspector panel on screen – what tells whether a row of it can be clicked at all. */ /** The inspector panel on screen – what tells whether a row of it can be clicked at all. */
inspectorRect: () => rectOf(inspector), 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(), inspectorInfo: () => inspector.debugInfo(),
textColor, textColor,
overlayTexts, overlayTexts,
+1 -1
View File
@@ -12,7 +12,7 @@ export const Theme = {
controlHeight: 24, controlHeight: 24,
padding: 8, padding: 8,
gap: 4, gap: 4,
radius: 8, radius: 2,
fieldPadding: 4, fieldPadding: 4,
checkboxSize: 16, checkboxSize: 16,
scrollbarWidth: 4, scrollbarWidth: 4,
+7 -10
View File
@@ -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 { pressCount } from '../clicks';
import { drawControlBox } from '../draw'; import { drawControlBox } from '../draw';
import type { TextEditTarget } from '../KeyboardBridge'; import type { TextEditTarget } from '../KeyboardBridge';
@@ -66,15 +66,12 @@ export class TextField extends Widget implements TextEditTarget {
this.minWidth = options.width ?? 80; this.minWidth = options.width ?? 80;
this.textNode = new Text({ text: value, style: textStyle(options.style ?? 'body') }); this.textNode = new Text({ text: value, style: textStyle(options.style ?? 'body') });
this.placeholderNode = new Text({ text: this.placeholder, style: textStyle('muted') }); this.placeholderNode = new Text({ text: this.placeholder, style: textStyle('muted') });
this.addChild( // Only the contents are clipped: a mask on the whole field cut its border away along the
this.bg, // straight edges and left it showing at the rounded corners alone.
this.selectionShape, const content = new Container();
this.placeholderNode, content.addChild(this.selectionShape, this.placeholderNode, this.textNode, this.caret);
this.textNode, content.mask = this.clipShape;
this.caret, this.addChild(this.bg, content, this.clipShape);
this.clipShape,
);
this.mask = this.clipShape;
this.trackHover(); this.trackHover();
this.on('pointerdown', (e: FederatedPointerEvent) => this.handlePointerDown(e)); this.on('pointerdown', (e: FederatedPointerEvent) => this.handlePointerDown(e));
+15
View File
@@ -63,6 +63,21 @@ export async function inspector(t) {
await press('a', { ctrl: true }); await press('a', { ctrl: true });
await typeText('Renamed crate'); await typeText('Renamed crate');
await shot('03-typing'); 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'); await press('Enter');
check( check(
'typing + Enter renamed the object', 'typing + Enter renamed the object',