Hold back an inspector rebuild while a field in it has focus

A rebuild destroys the widget being typed in, and a typed value can change the
very structure that decides whether a rebuild is needed -- a field that says
how many ports a node has. That made such a field unusable: every keystroke
previewed, the preview emitted 'project', refreshValues() saw the signature
change and threw the row away. Dragging the value worked only because a drag
keeps receiving globalpointermove on the destroyed widget.

So refreshValues() now postpones a due rebuild while the focus sits inside the
inspector, and runs it as soon as the field is left. Values are safe in the
meantime: TextField.setValue ignores an edit in progress.

Two handles for that in the toolkit. FocusManager.isWithin(node) also replaces
the parent chain refreshLive() walked by hand, and changeHandlers lets a panel
notice that the focus has gone -- there was no signal for that before.

Nothing in the editor can trigger this today, since the Sequence setting that
did is gone. It is the ground for the List tab, where a row that adds or
deletes an item changes the structure while you type. The e2e check therefore
builds the case out of real parts: type into a field, set the object's parent
from outside -- the parent is part of the structure signature -- and confirm
the input survives and the rebuild follows on Escape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-10 09:11:05 +02:00
1 parent 3c76106fd2
commit 5a440ba433
6 files changed
+73 -5

No files matched your search

+36
View File
@@ -142,4 +142,40 @@ export async function inspector(t) {
await redo();
check('redo restored the rename', (await object('o2')).name === 'Renamed crate');
await shot('07-undo-redo');
// 7. A change to the *structure* of what is shown normally rebuilds the inspector, which
// destroys the widget being typed in. While a field in here has focus the rebuild has to
// wait – otherwise a field whose value decides the structure cannot be typed into at all.
await clickText('Renamed crate');
const xRow = await locate('X');
await click(xRow.x + 160, xRow.y);
// The value it already has: Escape cancels the widget but not the preview it wrote, so
// typing anything else here would leave that value behind for the later steps.
await typeText('120');
const before = await json('__gm.inspectorInfo()');
// The parent is part of the object's structure signature, so this asks for a rebuild.
await mutate("__gm.state.activeScene.objects.find(o => o.id === 'o2').parentId = 'o1'");
const during = await json('__gm.inspectorInfo()');
const stillTyping = await json(
'({ tag: document.activeElement?.tagName, value: document.activeElement?.value })',
);
check(
'a structure change while typing does not rebuild the inspector',
during.rebuilds === before.rebuilds && during.postponed,
JSON.stringify(during),
);
check(
'the field being typed in survives it',
stillTyping.tag === 'INPUT' && stillTyping.value === '120',
JSON.stringify(stillTyping),
);
await press('Escape');
await sleep(200);
const after = await json('__gm.inspectorInfo()');
check(
'leaving the field runs the postponed rebuild',
after.rebuilds === before.rebuilds + 1 && !after.postponed,
JSON.stringify(after),
);
await mutate("__gm.state.activeScene.objects.find(o => o.id === 'o2').parentId = ''");
}