import type { FederatedPointerEvent } from 'pixi.js'; /** Longest gap between two presses that still reads as one double-click. */ const DOUBLE_MS = 400; /** How far the pointer may travel between them. */ const SLOP = 6; /** When and where a press happened. */ export interface Press { time: number; x: number; y: number; } export const pressOf = (e: FederatedPointerEvent): Press => ({ time: e.timeStamp, x: e.global.x, y: e.global.y, }); /** Whether `e` comes soon enough and close enough after `first` to make a double-click with it. */ export function followsPress(first: Press, e: FederatedPointerEvent): boolean { const gap = e.timeStamp - first.time; return gap >= 0 && gap <= DOUBLE_MS && Math.hypot(e.global.x - first.x, e.global.y - first.y) <= SLOP; } interface LastPress extends Press { key: unknown; button: number; count: number; } let last: LastPress = { key: null, button: -1, time: 0, x: 0, y: 0, count: 0 }; /** * Press count in a row (2 on a double-click), since Pixi leaves `detail` at 0 on `pointerdown`. `key` * must survive a rebuild of its widget: the first click's selection can recreate the list rows. */ export function pressCount(e: FederatedPointerEvent, key: unknown): number { const repeat = key === last.key && e.button === last.button && followsPress(last, e); const count = repeat ? last.count + 1 : 1; last = { ...pressOf(e), key, button: e.button, // A finished double-click ends the run: folding a row open and shut again is two // double-clicks, and the second must count as one rather than as clicks three and four. count: count === 2 ? 0 : count, }; return count; }