import { safeInvoke } from '../events'; import type { DocumentId } from '../../types/identifiers'; import { CLICK_ACTIONS, DRAG_ACTIONS, LONG_PRESS_DURATION_MS, POINTER_DRAG_THRESHOLD_SQUARED, STACK_HIT_EPSILON, } from '../../constants/desktop'; export { CLICK_ACTIONS, DRAG_ACTIONS, STACK_HIT_EPSILON, POINTER_DRAG_THRESHOLD_SQUARED, LONG_PRESS_DURATION_MS }; export type ClickAction = (typeof CLICK_ACTIONS)[keyof typeof CLICK_ACTIONS]; export type DragAction = (typeof DRAG_ACTIONS)[keyof typeof DRAG_ACTIONS]; export const withinThreshold = (dx: number, dy: number, thresholdSquared: number): boolean => (dx * dx + dy * dy) <= thresholdSquared; interface PointerIntentArgs { doc: { id: string }; entryDescriptor: unknown; selectedDocumentIds: Array; metaKey: boolean; pointerButton?: number; pointerType?: string; stackHits?: string[] | null; } export interface PointerIntent { docId: DocumentId; entryDescriptor: unknown; pointerType?: string; pointerButton?: number; selectedAtDown: boolean; selectionCountAtDown: number; metaKey: boolean; clickAction: ClickAction; dragAction: DragAction; stackDocIdsForDrag: string[] | null; stackDocIdsForClick: string[] | null; stackReplaceOnClick: boolean; stackReplaceOnDrag: boolean; clickSelectionApplied: boolean; stackSelectionApplied: boolean; longPressTriggered: boolean; optimisticSelection: string[]; } export const createPointerIntent = ({ doc, entryDescriptor, selectedDocumentIds, metaKey, pointerButton, pointerType, stackHits, }: PointerIntentArgs): PointerIntent => { const alreadySelected = selectedDocumentIds.includes(doc.id); const selectionCount = Array.isArray(selectedDocumentIds) ? selectedDocumentIds.length : 0; let clickAction: ClickAction = CLICK_ACTIONS.none; let dragAction: DragAction = DRAG_ACTIONS.none; if (metaKey) { clickAction = CLICK_ACTIONS.addStack; dragAction = DRAG_ACTIONS.dragSelection; } else if (alreadySelected) { clickAction = CLICK_ACTIONS.openDetail; dragAction = selectionCount > 1 ? DRAG_ACTIONS.dragSelection : DRAG_ACTIONS.dragSelectSingle; } else { clickAction = CLICK_ACTIONS.selectSingle; dragAction = DRAG_ACTIONS.dragSelectSingle; } const stackList: string[] = Array.isArray(stackHits) && stackHits.length > 0 ? stackHits.map((value) => String(value)) : [String(doc.id)]; const stackDocIdsForClick = clickAction === CLICK_ACTIONS.addStack ? stackList : null; const stackDocIdsForDrag = metaKey ? stackList : null; // Calculate optimistic selection let optimisticSelection: string[] = []; if (metaKey) { // Additive selection (stack or single) const currentSelection = new Set(selectedDocumentIds); stackList.forEach(id => currentSelection.add(id)); optimisticSelection = Array.from(currentSelection); } else if (alreadySelected) { // Already selected: keep current selection optimisticSelection = [...selectedDocumentIds]; } else { // New single selection optimisticSelection = [doc.id]; } return { docId: doc.id, entryDescriptor, pointerType, pointerButton, selectedAtDown: alreadySelected, selectionCountAtDown: selectionCount, metaKey, clickAction, dragAction, stackDocIdsForDrag, stackDocIdsForClick, stackReplaceOnClick: clickAction === CLICK_ACTIONS.addStack, stackReplaceOnDrag: false, clickSelectionApplied: false, stackSelectionApplied: false, longPressTriggered: false, optimisticSelection, }; }; export const applyClickPlanImmediately = ({ intent, event, onEntryPointer, onDocumentStackSelect, onSelect }: { intent: PointerIntent; event?: unknown; onEntryPointer?: (descriptor: unknown, event?: unknown) => void; onDocumentStackSelect?: (docIds: string[], event?: unknown, options?: { replace?: boolean }) => void; onSelect?: (descriptor: unknown, event?: unknown) => void; }) => { switch (intent.clickAction) { case CLICK_ACTIONS.selectSingle: case CLICK_ACTIONS.addCard: if (onSelect) { safeInvoke(onSelect, intent.entryDescriptor, event); } else { safeInvoke(onEntryPointer, intent.entryDescriptor, event); } intent.clickSelectionApplied = true; break; case CLICK_ACTIONS.addStack: if (Array.isArray(intent.stackDocIdsForClick) && intent.stackDocIdsForClick.length > 0) { // Use onSelect for stack selection (batch) if (onSelect) { // Map docIds to descriptors if necessary, or just pass IDs if onSelect handles it. // The current onSelect adapter in DesktopWorkspace expects { id } objects or just IDs? // Let's assume it expects descriptors like selectSingle. const descriptors = intent.stackDocIdsForClick.map(id => ({ type: 'document', id, key: `document:${id}`, })); safeInvoke(onSelect, descriptors, event); } else { // Fallback to legacy if onSelect not provided (shouldn't happen in new flow) safeInvoke( onDocumentStackSelect, intent.stackDocIdsForClick, event, { replace: intent.stackReplaceOnClick }, ); } intent.clickSelectionApplied = true; intent.stackSelectionApplied = true; } break; case CLICK_ACTIONS.openDetail: default: intent.clickSelectionApplied = true; break; } }; export const finalizeClickSelection = ({ intent, event, onEntryPointer, onDocumentStackSelect, onSelect }: { intent: PointerIntent; event?: unknown; onEntryPointer?: (descriptor: unknown, event?: unknown) => void; onDocumentStackSelect?: (docIds: string[], event?: unknown, options?: { replace?: boolean }) => void; onSelect?: (descriptor: unknown, event?: unknown) => void; }) => { if (!intent || intent.clickSelectionApplied) { return; } applyClickPlanImmediately({ intent, event, onEntryPointer, onDocumentStackSelect, onSelect }); }; export const applyLongPressSelection = ({ intent, stackDocIds, syntheticEvent, onDocumentStackSelect }: { intent: PointerIntent; stackDocIds?: string[] | null; syntheticEvent?: unknown; onDocumentStackSelect?: (docIds: string[], event?: unknown, options?: { replace?: boolean }) => void; }) => { if (!intent) { return; } const stackCopy: string[] = Array.isArray(stackDocIds) && stackDocIds.length > 0 ? stackDocIds.map((value) => String(value)) : [String(intent.docId)]; safeInvoke(onDocumentStackSelect, stackCopy, syntheticEvent, { replace: true }); intent.clickAction = CLICK_ACTIONS.addStack; intent.dragAction = DRAG_ACTIONS.dragSelectStack; intent.stackDocIdsForClick = stackCopy; intent.stackDocIdsForDrag = stackCopy; intent.stackReplaceOnClick = true; intent.stackReplaceOnDrag = true; intent.clickSelectionApplied = true; intent.stackSelectionApplied = true; intent.longPressTriggered = true; };