Reordering shapes by dragging worked well in the vector tab. Pixel art layers and frames and tilemap layers still used up/down buttons. All three lists now share one widget built from the shape list. - SortableList in the UI toolkit, with the drag rules next to it in src/ui/sortable.ts. A row has an optional thumbnail, a name, a hint on the right and an eye toggle. Every drop is one undo step. - The shape list is a thin wrapper around it. shapeRows.ts is gone, its tests moved to the new rules. - Pixel art layers show the layer alone on the current frame, frames show the whole frame. Up/Down, Hide, Earlier and Later are gone. - Tilemap layers get the eye instead of the checkbox. The per-row up, down and delete buttons are replaced by Delete layer next to Add layer. - A row is selected on release instead of press. Selecting rebuilt the inspector or tilemap panel and cancelled a drag that started on a row not yet selected. Tests: - ./check.sh: 138 files, 2052 tests, exit 0 (before 137 / 2051). - GM_STEPS=39,40,47,48 (fresh seed): ALL PASSED, 154 checks. - 5 new e2e checks. Check total 1264 in the README is computed (1259 + 5), CI never measured 1259. - Counter-checks bite: dropMove ignoring reversed fails a unit test, a tilemap drop not applied fails the drag check, a layer thumbnail showing all layers fails the thumbnail check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
49 lines
2.1 KiB
TypeScript
49 lines
2.1 KiB
TypeScript
/**
|
||
* The arithmetic of the sortable list shared by the shape, pixel art and tilemap lists: which row
|
||
* shows which item, which gap a drop falls into, and what move that makes in the owner's order.
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { dropGap, dropMove, rowOrder } from '../../src/ui/sortable';
|
||
|
||
const ROW = 30;
|
||
|
||
describe('sortable list', () => {
|
||
it('shows a layer list upside down – the item drawn last is the top row', () => {
|
||
expect(rowOrder(['bottom', 'middle', 'top'], true)).toEqual(['top', 'middle', 'bottom']);
|
||
expect(rowOrder(['first', 'second'], false)).toEqual(['first', 'second']);
|
||
});
|
||
|
||
it('drops into the gap the pointer is nearest to', () => {
|
||
expect(dropGap(3, 2, ROW)).toBe(0);
|
||
expect(dropGap(3, 20, ROW)).toBe(1);
|
||
expect(dropGap(3, 44, ROW)).toBe(1);
|
||
expect(dropGap(3, 46, ROW)).toBe(2);
|
||
// Outside the list there is no gap: a drop above it is the first, below it the last.
|
||
expect(dropGap(3, 400, ROW)).toBe(3);
|
||
expect(dropGap(3, -40, ROW)).toBe(0);
|
||
});
|
||
|
||
it('turns a drop into a move of the model, the top row being the last item when reversed', () => {
|
||
// Three items, so the rows are the model indices [2, 1, 0]: the top row dropped at the
|
||
// bottom gap becomes the first item of the model, the one drawn first.
|
||
expect(dropMove(3, 0, 3, true)).toEqual({ from: 2, to: 0 });
|
||
expect(dropMove(3, 2, 0, true)).toEqual({ from: 0, to: 2 });
|
||
expect(dropMove(3, 1, 0, true)).toEqual({ from: 1, to: 2 });
|
||
});
|
||
|
||
it('keeps the indices as they are in a list shown in model order', () => {
|
||
// Frames: the first row is the first frame. The top row dropped at the bottom becomes the last.
|
||
expect(dropMove(3, 0, 3, false)).toEqual({ from: 0, to: 2 });
|
||
expect(dropMove(3, 2, 0, false)).toEqual({ from: 2, to: 0 });
|
||
expect(dropMove(3, 0, 2, false)).toEqual({ from: 0, to: 1 });
|
||
});
|
||
|
||
it('is no move when the row lands where it already is', () => {
|
||
for (const reversed of [true, false]) {
|
||
expect(dropMove(3, 1, 1, reversed)).toBeNull();
|
||
expect(dropMove(3, 1, 2, reversed)).toBeNull();
|
||
expect(dropMove(3, 5, 1, reversed)).toBeNull();
|
||
}
|
||
});
|
||
});
|