/** * 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 { blockGap, dropGap, dropMove, dropSlot, edgeScroll, gapLineY, 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(); } }); }); describe('blocks of rows (#310)', () => { // A one-row variable, a struct of four rows, another one-row variable, with gaps between. const SPANS = [ { top: 0, bottom: 24 }, { top: 28, bottom: 140 }, { top: 144, bottom: 168 }, ]; it('drops past every block whose middle the pointer has crossed, however tall it is', () => { expect(blockGap(-10, SPANS)).toBe(0); expect(blockGap(11, SPANS)).toBe(0); expect(blockGap(13, SPANS)).toBe(1); // Inside the tall block but above its middle (84): still in front of it. expect(blockGap(80, SPANS)).toBe(1); expect(blockGap(90, SPANS)).toBe(2); expect(blockGap(500, SPANS)).toBe(3); }); it('draws the line between the blocks, on the edge at either end', () => { expect(gapLineY(0, SPANS)).toBe(0); expect(gapLineY(1, SPANS)).toBe(26); expect(gapLineY(2, SPANS)).toBe(142); expect(gapLineY(3, SPANS)).toBe(168); }); it('turns the drop into the place the model takes, or none where the block stays', () => { expect(dropSlot(500, SPANS, 0)).toBe(2); expect(dropSlot(-10, SPANS, 2)).toBe(0); expect(dropSlot(90, SPANS, 0)).toBe(1); expect(dropSlot(80, SPANS, 1)).toBeNull(); expect(dropSlot(150, SPANS, 1)).toBeNull(); }); it('scrolls at the edges only, faster the deeper the pointer goes into them', () => { expect(edgeScroll(200, 100, 400)).toBe(0); expect(edgeScroll(118, 100, 400)).toBe(-3); expect(edgeScroll(100, 100, 400)).toBe(-12); expect(edgeScroll(40, 100, 400)).toBe(-12); expect(edgeScroll(490, 100, 400)).toBe(7); }); });