Let a text field select from right to left with the mouse (#236)
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Successful in 9m36s

Dragging from right to left in a canvas text field only moved the
cursor. The hidden input behind the field got every selection sorted
and without a direction. Its selectionchange echo then handed back the
sorted range, and the anchor jumped to the mouse on the first move.

- KeyboardBridge sets the selection with 'backward' when the focus
  lies before the anchor, and reads selectionDirection when the input
  reports a change.
- TextField passes anchor and focus through unchanged. Its own
  sorting is gone.
- Shift+Left runs through the same path. The cursor should now sit at
  the moving end there too. This is not tested on its own.

Tests:
- ./check.sh: 167 files, 2435 tests, CHECK EXIT 0
- e2e 2: ALL PASSED, 25 checks. New check 2d-ii drags right to left
  in the name field and reads start 0, end 13, direction backward.
- Counter-check without the direction: 2d-ii fails with end 2, the
  reported symptom.
- e2e check total in README goes to 1401 (computed, not run in full).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-26 15:30:33 +02:00
1 parent 74b9c60661
commit 1b3cb625a3
4 files changed
+43 -23

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:
```bash
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1400 checks (GM_STEPS=20 for one step)
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1401 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
+19 -12
View File
@@ -5,7 +5,8 @@
*/
export interface TextEditTarget {
onBridgeInput(value: string, selectionStart: number, selectionEnd: number): void;
/** `anchor` is where the selection started, `focus` where the caret is; focus < anchor is backward. */
onBridgeInput(value: string, anchor: number, focus: number): void;
/** Called for navigation keys (Enter, Escape, Tab, arrows …). Return true to consume. */
onBridgeKey(event: KeyboardEvent): boolean;
onBridgeBlur(): void;
@@ -40,8 +41,14 @@ export class KeyboardBridge {
document.body.appendChild(input);
this.input = input;
const sync = () =>
this.target?.onBridgeInput(input.value, input.selectionStart ?? 0, input.selectionEnd ?? 0);
// The input keeps its range ordered; the direction says which end is the anchor. Reporting
// the ordered range lost the anchor of a right-to-left drag on the first selectionchange.
const sync = () => {
const start = input.selectionStart ?? 0;
const end = input.selectionEnd ?? 0;
const backward = input.selectionDirection === 'backward';
this.target?.onBridgeInput(input.value, backward ? end : start, backward ? start : end);
};
input.addEventListener('input', sync);
input.addEventListener('keyup', sync);
document.addEventListener('selectionchange', () => {
@@ -62,17 +69,12 @@ export class KeyboardBridge {
}
/** Mirrors the field's value into the input and grabs DOM focus. */
attach(
target: TextEditTarget,
value: string,
selectionStart = value.length,
selectionEnd = selectionStart,
): void {
attach(target: TextEditTarget, value: string, anchor = value.length, focus = anchor): void {
this.suppressBlur = true;
this.target = target;
this.input.value = value;
this.input.focus({ preventScroll: true });
this.input.setSelectionRange(selectionStart, selectionEnd);
this.select(anchor, focus);
this.suppressBlur = false;
}
@@ -83,9 +85,14 @@ export class KeyboardBridge {
}
/** Pushes a value the canvas side changed into the input, so the next keystroke edits that text. */
setValue(value: string, selectionStart = value.length, selectionEnd = selectionStart): void {
setValue(value: string, anchor = value.length, focus = anchor): void {
this.input.value = value;
if (document.activeElement === this.input) this.input.setSelectionRange(selectionStart, selectionEnd);
if (document.activeElement === this.input) this.select(anchor, focus);
}
private select(anchor: number, focus: number): void {
const direction = focus < anchor ? 'backward' : 'forward';
this.input.setSelectionRange(Math.min(anchor, focus), Math.max(anchor, focus), direction);
}
/** Ignored when another field attached meanwhile, so a late blur cannot steal it. */
+5 -10
View File
@@ -167,23 +167,18 @@ export class TextField extends Widget implements TextEditTarget {
private setSelection(start: number, end: number, pushToBridge: boolean): void {
this.selStart = clamp(start, 0, this.valueText.length);
this.selEnd = clamp(end, 0, this.valueText.length);
if (pushToBridge && this.editing) {
// The DOM only takes an ordered range; selStart/selEnd keep the anchor/focus order a
// drag selection needs.
const lo = Math.min(this.selStart, this.selEnd);
const hi = Math.max(this.selStart, this.selEnd);
ui().keyboard.setValue(this.valueText, lo, hi);
}
// selStart/selEnd are anchor/focus; the bridge keeps that order across its selectionchange echo.
if (pushToBridge && this.editing) ui().keyboard.setValue(this.valueText, this.selStart, this.selEnd);
this.caretVisible = true;
this.invalidate();
}
/** Adopts the input's value and selection verbatim; fires `onInput` only when the text changed. */
onBridgeInput(value: string, selectionStart: number, selectionEnd: number): void {
onBridgeInput(value: string, anchor: number, focus: number): void {
const changed = value !== this.valueText;
this.valueText = value;
this.selStart = selectionStart;
this.selEnd = selectionEnd;
this.selStart = anchor;
this.selEnd = focus;
this.caretVisible = true;
this.invalidate();
if (changed) this.onInput?.(value);
+18
View File
@@ -105,6 +105,24 @@ export async function inspector(t) {
JSON.stringify((await object('o2')).name),
);
// 2d-ii. A drag from right to left selects the whole name (#236). The hidden input reported its
// range ordered, and its selectionchange echo made the caret the anchor after the first move.
await click(nameLabel.x + 160, nameLabel.y);
const nameField = await json('__gm.focusedRect()');
await press('Escape');
const midY = nameField.y + nameField.h / 2;
await drag(nameField.x + nameField.w - 4, midY, nameField.x + 3, midY);
const backward = await json(
'({ start: document.activeElement?.selectionStart, end: document.activeElement?.selectionEnd,' +
' direction: document.activeElement?.selectionDirection })',
);
await press('Escape');
check(
'dragging from right to left selects the whole name',
backward.start === 0 && backward.end === 'Renamed crate'.length && backward.direction === 'backward',
JSON.stringify(backward),
);
// 2e. Scrub the X field (right of the "X" label) by 100 px.
const xLabel = await locate('X');
await drag(xLabel.x + 160, xLabel.y, xLabel.x + 260, xLabel.y);