Show where an inspector row can be dragged and answer the pointer
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Failing after 16m38s

After #310 rows could be dragged by their mark, but nothing said which
marks take a drag, and the pointer on one changed nothing but the
cursor.

- A mark that drags is now a Grip: the same widget wrapped, the same
  size, so the value column stays where it was. Once sortBlocks takes
  it, it carries six faint dots at its left edge.
- Marks that cannot drag stay plain: the variables of a placed object
  and the fields of a typed struct show no dots.
- With the pointer on a grip its dots light up and the whole block that
  would move is washed in the accent colour, a struct with its fields.
  While the block is carried the wash gives way to the dimming, and the
  grip stays lit.
- While a block is carried the other grips let the pointer pass: no
  lit dots, no wash. Every grip of the column shows grabbing instead of
  grab until the drop.
- The wash first used controlHover like list rows. In the inspector it
  is darker than the panel and vanished next to the dark fields, so it
  became the accent at 15 percent, the colour of the drop line.
- __gm.inspectorMark also reports the dots: none, idle or lit.

Tests:
- Gate on melektaus-atx: 236 files, 3576 tests (+4 in
  block-drag.test.ts, one of them a drag passing over another grip).
- e2e 22: 40 checks (+1, sum 1740). 22f-iii reads idle dots, moves the
  mouse onto the mark and reads them lit.
- Two counter-checks bite: no wash on hover fails 1 test, grips never
  made grabbable fail 3. A third: hover ignoring the drag fails 1.
- Screenshots of a lit single row and a lit struct block looked at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-04 00:11:41 +02:00
1 parent 2c19c89878
commit c74b6b1cab
10 files changed
+270 -23

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
/**
* What a sortable column shows before anything is dragged (#310): dots on every grip, and under the
* pointer the grip lit and the block that would go with it backed.
*/
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { CanvasTextMetrics, FederatedPointerEvent, Graphics } from 'pixi.js';
import { Grip, HStack, Label, sortBlocks, Theme, VStack } from '../../src/ui';
vi.mock('../../src/ui/svg', () => ({ drawSvg: () => undefined, tintSvg: (svg: string) => svg }));
beforeAll(() => {
CanvasTextMetrics.measureText = ((text: string) => ({
width: text.length * 7,
height: 14,
lines: [text],
lineWidths: [text.length * 7],
})) as never;
});
/** Three blocks: one row, two rows, one row. */
function column(sortable = true) {
const body = new VStack();
const blocks = [1, 2, 1].map((size, index) => {
const grip = new Grip(new Label(String(index), { width: 28 }));
const rows = Array.from({ length: size }, (_, i) => {
const row = new HStack();
row.minHeight = Theme.rowHeight;
if (i === 0) row.add(grip);
return body.add(row);
});
return { grip, rows };
});
body.resize(200, 400);
if (sortable) sortBlocks(blocks, () => {});
return { body, blocks };
}
const pointer = (type: string) => Object.assign(new FederatedPointerEvent(null as never), { type });
const backs = (body: VStack) => body.children.filter((c) => c instanceof Graphics);
describe('the grips of a sortable column', () => {
it('show their dots, and only where the rows may be dragged', () => {
expect(column().blocks.map((b) => b.grip.dotsShown)).toEqual(['idle', 'idle', 'idle']);
expect(column(false).blocks.map((b) => b.grip.dotsShown)).toEqual(['none', 'none', 'none']);
});
it('light up under the pointer and back the whole block, behind its rows', () => {
const { body, blocks } = column();
const grip = blocks[1]!.grip;
grip.emit('pointerover', pointer('pointerover'));
expect(grip.dotsShown).toBe('lit');
expect(blocks[0]!.grip.dotsShown).toBe('idle');
const [back] = backs(body);
expect(body.children.indexOf(back!)).toBe(0);
const bounds = (back as Graphics).bounds;
expect([bounds.minY, bounds.maxY]).toEqual([Theme.rowHeight, 3 * Theme.rowHeight]);
});
it('go back to rest when the pointer leaves', () => {
const { body, blocks } = column();
const grip = blocks[1]!.grip;
grip.emit('pointerover', pointer('pointerover'));
grip.emit('pointerout', pointer('pointerout'));
expect(grip.dotsShown).toBe('idle');
expect(backs(body)).toEqual([]);
});
it('let the pointer pass by while a block is dragged, and answer it again after', () => {
const { body, blocks } = column();
const [dragged, , passed] = blocks.map((b) => b.grip);
const at = (type: string, y: number, button = 0) =>
Object.assign(pointer(type), { button, global: { x: 0, y } });
dragged!.emit('pointerdown', at('pointerdown', 0));
dragged!.emit('globalpointermove', at('globalpointermove', 10));
passed!.emit('pointerover', pointer('pointerover'));
expect(blocks.map((b) => b.grip.dotsShown)).toEqual(['lit', 'idle', 'idle']);
// The drop line is a Graphics too; a backing would sit behind the rows.
expect(body.children[0]).not.toBeInstanceOf(Graphics);
expect(blocks.map((b) => b.grip.cursor)).toEqual(['grabbing', 'grabbing', 'grabbing']);
dragged!.emit('pointerupoutside', at('pointerupoutside', 10));
expect(blocks.map((b) => b.grip.dotsShown)).toEqual(['idle', 'idle', 'lit']);
expect(backs(body)).toHaveLength(1);
expect(blocks.map((b) => b.grip.cursor)).toEqual(['grab', 'grab', 'grab']);
});
});