A list property in the inspector was a single text field holding a comma
separated line -- unreadable past a handful of items, and it could not
carry anything but text. It becomes a button ("3 items") that opens a
sixth centre tab, where every item is a row built by the very widget the
inspector would build for that item kind.
The renderer was free: a PropertyDef of kind 'list' already carries
`items`, so item i becomes a synthetic def of that kind -- number, text,
boolean and colour rows appear without a new branch.
The way into the tab goes through the state (EditorState.listSlot,
openList(), event 'list'), like every other tab switch here; a tab may
not call showTab itself. ListSlot lives next to Selection rather than in
the tab folder, or state.ts would have to import upwards.
A row's handlers must not close over a snapshot of the list: they outlive
a commit, since the tab only rebuilds when the length changes, and the
second edited row would otherwise revive the value the first replaced.
showTab switched five `visible` lines by hand, where a sixth tab is
silently forgotten; it is a Record<CentreTab, Widget> now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99 lines
3.9 KiB
JavaScript
99 lines
3.9 KiB
JavaScript
/**
|
||
* Labels must be shortened to fit their control, not written past it. Runs last because it
|
||
* renames assets to absurd lengths to provoke the case.
|
||
*
|
||
* `__gm.textOverflows()` walks every visible text and reports the ones drawn wider than their
|
||
* widget; against a build without the fix it reported the graph picker at 106 px over.
|
||
*/
|
||
|
||
/** A name far wider than any 200 px picker, so a control that does not truncate must spill. */
|
||
const LONG_NAME = 'Player movement with jumping and coyote time';
|
||
|
||
export async function overflow(t) {
|
||
const { check, sleep, json, evaluate, shot, mutate } = t;
|
||
|
||
const pickerWidths = async () => {
|
||
const out = {};
|
||
for (const tab of ['graph', 'texture', 'container']) {
|
||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
||
await sleep(300);
|
||
out[tab] = await json(`__gm.${tab}.pickerWidth()`);
|
||
}
|
||
return out;
|
||
};
|
||
const before = await pickerWidths();
|
||
|
||
await mutate(`
|
||
for (const asset of __gm.state.project.assets) {
|
||
if (asset.type === 'behavior' || asset.type === 'texture' || asset.type === 'container')
|
||
asset.name = ${JSON.stringify(LONG_NAME)};
|
||
}
|
||
__gm.state.select(null)`);
|
||
await sleep(600);
|
||
|
||
// 21. The pickers keep the width they were given. `width` used to set only a minimum, so a
|
||
// picker grew with whatever name it happened to show and the header reflowed on load.
|
||
const after = await pickerWidths();
|
||
for (const tab of ['graph', 'texture', 'container']) {
|
||
check(
|
||
`the ${tab} picker keeps its width when the name gets long`,
|
||
before[tab] === 200 && after[tab] === 200,
|
||
`before ${before[tab]}, after ${after[tab]}`,
|
||
);
|
||
}
|
||
|
||
// 21a. Every tab, because each one owns a different set of controls.
|
||
for (const tab of ['scene', 'graph', 'texture', 'game', 'container', 'list']) {
|
||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
||
await sleep(400);
|
||
const spills = await json('__gm.textOverflows()');
|
||
check(
|
||
`no label is drawn past its control in the ${tab} tab`,
|
||
spills.length === 0,
|
||
JSON.stringify(spills),
|
||
);
|
||
}
|
||
|
||
// 21b. Shortened, not simply dropped: the value must still be readable and marked as cut.
|
||
await evaluate("__gm.showTab('graph')");
|
||
await sleep(400);
|
||
const shown = (await json('__gm.textOverflows(true)')).find((e) => e.text.startsWith('Player movement'));
|
||
check(
|
||
'a value too wide for its dropdown is cut with an ellipsis',
|
||
Boolean(shown) && shown.text.endsWith('…') && shown.text.length > 8,
|
||
JSON.stringify(shown),
|
||
);
|
||
await shot('36-long-labels');
|
||
|
||
// 21c. An inspector full of dropdowns – the parent row and the enum fields – with a selection
|
||
// whose asset name is now overlong too.
|
||
await evaluate("__gm.showTab('scene')");
|
||
await sleep(300);
|
||
await evaluate("__gm.state.select({ kind: 'object', objectId: 'otex' })");
|
||
await sleep(500);
|
||
const inspector = await json('__gm.textOverflows()');
|
||
check('no label spills in the inspector', inspector.length === 0, JSON.stringify(inspector));
|
||
|
||
// 21d. The other direction: a control narrow enough to swallow its whole label draws nothing
|
||
// at all, which the overflow walk cannot see – an empty text spills over no edge.
|
||
for (const tab of ['scene', 'graph', 'texture', 'container', 'list']) {
|
||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
||
await sleep(400);
|
||
const dropped = await json('__gm.labelsDropped()');
|
||
check(
|
||
`no button is left without its label in the ${tab} tab`,
|
||
dropped.length === 0,
|
||
JSON.stringify(dropped),
|
||
);
|
||
}
|
||
// The narrowest buttons in the editor sit on a behaviour's variable rows.
|
||
await evaluate("__gm.state.select({ kind: 'asset', assetId: 'mover' })");
|
||
await sleep(400);
|
||
const onVariables = await json('__gm.labelsDropped()');
|
||
check(
|
||
'the variable rows keep their buttons labelled',
|
||
onVariables.length === 0,
|
||
JSON.stringify(onVariables),
|
||
);
|
||
}
|