import type { DropSpot } from './widgets/ListView'; /** * How a sortable list maps between what is on screen and what its owner holds. A layer list shows the * drawing order upside down – the item drawn last is the topmost row – so every index has two * readings, and dropping a dragged row has a third: the place it lands in is counted with the row * still in the list, while the model takes it out first. * * Kept apart from the widgets so the arithmetic can be measured without a canvas. */ /** The ids as the list shows them, top first. `reversed` puts the model's last item on top. */ export function rowOrder(ids: readonly T[], reversed: boolean): T[] { return reversed ? [...ids].reverse() : [...ids]; } /** The gap a row dropped at `y` falls into, 0…count – the boundary above row 0 being 0. */ export function dropGap(count: number, y: number, rowHeight: number): number { if (rowHeight <= 0) return 0; return Math.max(0, Math.min(count, Math.round(y / rowHeight))); } /** * The move a drop makes, in model indices, or null when the row would land where it already is. * `fromRow` and `gap` are in display positions; a gap below the dragged row counts that row itself, * which the model does not once it has taken it out. */ export function dropMove( count: number, fromRow: number, gap: number, reversed: boolean, ): { from: number; to: number } | null { if (fromRow < 0 || fromRow >= count) return null; const toRow = gap > fromRow ? gap - 1 : gap; if (toRow === fromRow || toRow < 0 || toRow >= count) return null; if (!reversed) return { from: fromRow, to: toRow }; const last = count - 1; return { from: last - fromRow, to: last - toRow }; } /** * Where a dragged row lands, counted in the list it has *left* – which is what the move command * takes. A drop on the middle of a row takes that row's place: the list is flat, so there is no * "inside" for it to mean, and without this a drag downwards onto the next row would cancel itself * out. The gaps above and below a row are insert points, and one below the dragged row shifts up * by one once the row is out. `-1` is "nothing to do". */ export function dropIndex(from: number, spot: DropSpot, length: number): number { const last = length - 1; if (from < 0 || from > last) return -1; if (spot.at === 'end') return last; const target = Number(spot.targetId); if (!Number.isInteger(target) || target < 0 || target > last) return -1; if (spot.at === 'onto') return target; const insert = target + (spot.at === 'after' ? 1 : 0); return Math.min(last, insert > from ? insert - 1 : insert); } /** The rows of one block, top to bottom – a struct with its fields, an item with its nested list. */ export interface BlockSpan { top: number; bottom: number; } /** * The gap a block dropped at `y` falls into, 0…count: past every block whose middle is above `y`. * Blocks differ in height, so the middle of each is what the pointer has to cross. */ export function blockGap(y: number, spans: readonly BlockSpan[]): number { return spans.filter((span) => (span.top + span.bottom) / 2 < y).length; } /** Where the line for `gap` is drawn: halfway between the blocks around it, on the edge at either end. */ export function gapLineY(gap: number, spans: readonly BlockSpan[]): number { const above = spans[gap - 1]; const below = spans[gap]; if (above && below) return (above.bottom + below.top) / 2; return above?.bottom ?? below?.top ?? 0; } /** The place block `from` moves to when dropped at `y`, or null where it would stay. */ export function dropSlot(y: number, spans: readonly BlockSpan[], from: number): number | null { return dropMove(spans.length, from, blockGap(y, spans), false)?.to ?? null; } /** * How far a scrolled column moves this frame while a drag holds the pointer near its top or bottom: * the deeper into the `edge` band, the faster, up to `step`. Positive scrolls down. */ export function edgeScroll(y: number, top: number, height: number, edge = 24, step = 12): number { const into = (depth: number) => Math.ceil((Math.min(edge, depth) / edge) * step); if (y < top + edge) return -into(top + edge - y); if (y > top + height - edge) return into(y - (top + height - edge)); return 0; }