Say what a button without words does when the pointer rests (#209)
The top bar, the magnet, the eye and many marks of the inspector are
pictures without a caption. Whoever did not know one could only try
it. The toolkit had no hint on hover, which also forced the point
menu of #195 to be words. The answers in the issue settle the open
points: a cut tab name shows in full, shortcuts are part of the
text, the point menu stays as it is.
- Button takes a tooltip, a text or a function asked each time. It
shows on a small sign once the pointer has rested for half a
second.
- There is one sign for the page, a TooltipLayer on the overlay
above the menus. A control reports each pointer move and the
pointer leaving, the layer does the waiting.
- A press, a key, the wheel or a blur of the window takes the sign
away. initUI listens for them on the window.
- placeTooltip in ui/tooltip.ts is the rule for the place as a pure
function: under the control and flush left, flush right at the
right border, above the control at the bottom.
- toolbarTip says what the five buttons of the top bar do right now.
Undo and redo name their step from CommandStack.undoLabel and
redoLabel, as in "Undo Paint tiles (Ctrl+Z)".
- A SortableList reports its own marks with a key per mark: the eye,
the duplicate mark, the cross and the plus.
- The magnet, both fullscreen buttons, the eye of the scene tab, the
overflow of the tab strip, the plus of the assets panel, the marks
of the inspector rows and the nine anchors of the resize dialog
carry a text.
Decided here:
- Every button whose text is cut short shows the whole text, not
only a tab.
- A row action of the inspector names the one thing a click does. A
button that opens a menu says only Reset, Apply or Size from the
picture.
- A button just clicked stays quiet until the pointer has left it.
- A greyed out button shows no sign.
- The switches of the eye popup, the cross of a document tab and the
tools of pixel art have none. The tools carry words, other than
the issue assumes.
- The marks of a list say Show or Hide, Duplicate, Remove and Add,
whatever the list holds.
Found on the way:
- Signing out empties the overlay and would have destroyed a sign on
screen with it. The editor takes the tooltip away first. This has
no test of its own.
- The text hooks locate and has skip the sign, or it would answer
for the button under it.
Tests:
- Gate on melektaus-atx: 210 files, 3209 tests (3176 + 33), exit 0
- e2e 46 on melektaus-atx: 20 checks, all pass
- e2e 1-14 on melektaus-atx: 508 checks, all pass
- e2e 1-9,14,39,40,44-48: 685 pass, 2 fail, both in module 14. It
measures tabs that modules 10 to 13 open, and the chain 1-14 is
green. The cut chain was not run on the state before.
- No full run and no smokes. The sum of 1667 (1664 + 3) is reckoned,
1664 is what CI counted for 3452899.
- 33 unit tests in tests/ui/tooltip.test.ts
- Three e2e checks in block 46h of module 46: the redo button after
the wait, taking the sign away, turning at the right border
- 50 unit counter-probes and four browser counter-probes all fail
their tests. Three of the unit ones did not at first and got
sharper tests.
- Not checked in the browser: the sign of a cut tab, the marks of
the lists, turning upwards at the bottom, each other button alone
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
345289982b
commit
ec2fd9084f
27 files changed
+831
-15
No files matched your search
@@ -136,6 +136,97 @@ export async function topbar(t) {
|
||||
await json('__gm.state.project.name'),
|
||||
);
|
||||
|
||||
// 46h. The icon buttons say what they do (#209). The pointer has to rest for the sign to show; it
|
||||
// names the step redo would take and the keys, and sits under its button, inside the window.
|
||||
const tooltip = () => json('__gm.tooltip()');
|
||||
const redoAt = await json('__gm.toolbarAt("redo")');
|
||||
const redoLabel = await json('__gm.state.commands.redoLabel');
|
||||
await t.mouse('mouseMoved', redoAt.x - 1, redoAt.y);
|
||||
await t.mouse('mouseMoved', redoAt.x, redoAt.y);
|
||||
const atOnce = await tooltip();
|
||||
await waitUntil('__gm.tooltip() !== null', { label: 'the tooltip of the redo button' });
|
||||
const redoTip = await tooltip();
|
||||
const windowSize = await json('({ w: window.innerWidth, h: window.innerHeight })');
|
||||
await shot('46h-tooltip');
|
||||
check(
|
||||
'resting on the redo button shows "Redo <step> (Ctrl+Y)" under it, after a wait and not at once',
|
||||
atOnce === null &&
|
||||
typeof redoLabel === 'string' &&
|
||||
redoLabel.length > 0 &&
|
||||
redoTip.text === `Redo ${redoLabel} (Ctrl+Y)` &&
|
||||
redoTip.box.y >= redoTip.anchor.y + redoTip.anchor.h &&
|
||||
redoTip.box.x === Math.round(redoTip.anchor.x) &&
|
||||
redoTip.box.x + redoTip.box.w <= windowSize.w &&
|
||||
(await has(redoTip.text)) === false,
|
||||
JSON.stringify({ atOnce, redoLabel, redoTip }),
|
||||
);
|
||||
// Off the button it goes. The wheel takes it away as well, and the button stays quiet until the
|
||||
// pointer has left it; a click does the same, here with the menu opening under it.
|
||||
await t.mouse('mouseMoved', redoAt.x + 90, redoAt.y);
|
||||
await sleep(100);
|
||||
const afterLeaving = await tooltip();
|
||||
const menuAt = await json('__gm.toolbarAt("menu")');
|
||||
const restOnMenu = async () => {
|
||||
await t.mouse('mouseMoved', menuAt.x - 1, menuAt.y);
|
||||
await t.mouse('mouseMoved', menuAt.x, menuAt.y);
|
||||
await waitUntil('__gm.tooltip() !== null', { label: 'the tooltip of the menu button' });
|
||||
return tooltip();
|
||||
};
|
||||
const menuTip = await restOnMenu();
|
||||
await t.wheel(menuAt.x, menuAt.y, 40);
|
||||
const afterWheel = await tooltip();
|
||||
await t.mouse('mouseMoved', menuAt.x + 1, menuAt.y);
|
||||
await sleep(800);
|
||||
const afterWaiting = await tooltip();
|
||||
await t.mouse('mouseMoved', menuAt.x + 200, menuAt.y);
|
||||
const again = await restOnMenu();
|
||||
await clickToolbar('menu');
|
||||
const afterClick = await tooltip();
|
||||
const menuOpen = (await json('__gm.menuRows()')) !== null;
|
||||
await press('Escape');
|
||||
check(
|
||||
'the tooltip goes when the pointer leaves, and the wheel or a click takes it away until the pointer comes back',
|
||||
afterLeaving === null &&
|
||||
menuTip.text === 'Menu' &&
|
||||
afterWheel === null &&
|
||||
afterWaiting === null &&
|
||||
again.text === 'Menu' &&
|
||||
afterClick === null &&
|
||||
menuOpen === true,
|
||||
JSON.stringify({ afterLeaving, menuTip, afterWheel, afterWaiting, again, afterClick, menuOpen }),
|
||||
);
|
||||
// At the right border the sign turns left: flush with the right edge of its button. The button
|
||||
// nearest that border is the reset of an inspector row, which an override brings up.
|
||||
const edgeObject = await json(`(() => {
|
||||
const p = __gm.state.project;
|
||||
const scene = __gm.state.activeScene;
|
||||
const o = scene.objects.find((o) => p.assets.find((a) => a.id === o.assetId)?.type === 'sprite');
|
||||
return o ? { scene: scene.id, id: o.id, overrides: o.overrides } : null;
|
||||
})()`);
|
||||
const setOverrides = (overrides) =>
|
||||
t.mutate(
|
||||
`__gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(edgeObject.id)}).overrides = ${JSON.stringify(overrides)}`,
|
||||
);
|
||||
await setOverrides({ ...edgeObject.overrides, width: 77 });
|
||||
await t.select({ kind: 'object', holderId: edgeObject.scene, objectId: edgeObject.id });
|
||||
const resetAt = await t.findIcon('reset', false);
|
||||
await t.mouse('mouseMoved', resetAt.x - 1, resetAt.y);
|
||||
await t.mouse('mouseMoved', resetAt.x, resetAt.y);
|
||||
await waitUntil('__gm.tooltip() !== null', { label: 'the tooltip of the reset button' });
|
||||
const edgeTip = await tooltip();
|
||||
await shot('46h-tooltip-edge');
|
||||
check(
|
||||
'a tooltip that would leave the window on the right turns left, and names the one thing its button does',
|
||||
edgeTip.text === 'Reset to asset' &&
|
||||
edgeTip.anchor.x + edgeTip.box.w > windowSize.w &&
|
||||
edgeTip.box.x + edgeTip.box.w === Math.round(edgeTip.anchor.x + edgeTip.anchor.w) &&
|
||||
edgeTip.box.x + edgeTip.box.w <= windowSize.w,
|
||||
JSON.stringify({ edgeTip, windowSize }),
|
||||
);
|
||||
await t.mouse('mouseMoved', windowSize.w / 2, windowSize.h / 2);
|
||||
await setOverrides(edgeObject.overrides);
|
||||
await t.select(null);
|
||||
|
||||
// 46f. The symbol scale in the same dialog: 1 by default, typed down to 0.5, and every icon of the
|
||||
// scene drawn at half its side (18 → 9 world units), undone in one step.
|
||||
const iconSize = () =>
|
||||
|
||||
Reference in new issue
Block a user