desktop selection

This commit is contained in:
2025-11-02 13:12:36 +01:00
parent 5bf64fc5f3
commit 9ddaa59155
9 changed files with 240 additions and 40 deletions
+100 -11
View File
@@ -2,6 +2,7 @@ import { useCallback, useRef } from 'react';
import { useDesktopContext } from './context';
import { preventAll } from './events';
import { clamp, formatTransform } from './math';
import usePointerTap from '../ui/usePointerTap';
const DRAG_HYSTERESIS_PX = 4;
const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX;
@@ -36,8 +37,33 @@ const useDocumentDrag = () => {
recalcVisibleDocIds,
settings,
containerRef,
onDocumentOpen,
onInspectDocument,
} = useDesktopContext();
const tapHandler = usePointerTap({
delay: 220,
onSingle: ({ data, event }) => {
if (!data || !data.docId) {
return;
}
if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
return;
}
openOverlayForDoc(data.docId, data.originInfo);
},
onDouble: ({ data }) => {
if (!data || !data.docId) {
return;
}
if (typeof onInspectDocument === 'function') {
onInspectDocument(data.docId);
return;
}
onDocumentOpen?.(data.docId);
},
});
const dragStateRef = useRef(null);
const inertiaAnimationsRef = useRef(new Map());
const { canvasPadding, defaultCanvasWidth, defaultCanvasHeight, debugDrag } = settings;
@@ -197,7 +223,10 @@ const useDocumentDrag = () => {
Number.isFinite(baseScale) && baseScale > 0 ? baseScale : 1;
const defaultCenterX = canvasPadding + docWidth / 2;
const defaultCenterY = canvasPadding + docHeight / 2;
bringToFront(docId);
const metaPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
if (!metaPressed) {
bringToFront(docId);
}
const entry = layoutRef.current.get(docId) || null;
const centerX = typeof entry?.centerX === 'number' ? entry.centerX : defaultCenterX;
const centerY = typeof entry?.centerY === 'number' ? entry.centerY : defaultCenterY;
@@ -338,8 +367,8 @@ const useDocumentDrag = () => {
const rotatedOffsetY =
state.localPointerOffsetX * sinRot + state.localPointerOffsetY * cosRot;
const desiredCenterX = pointerCanvasX - rotatedOffsetX;
const desiredCenterY = pointerCanvasY - rotatedOffsetY;
const previousCenterX = Number.isFinite(entry.centerX) ? entry.centerX : state.originCenterX;
const previousCenterY = Number.isFinite(entry.centerY) ? entry.centerY : state.originCenterY;
const absCos = Math.abs(cosRot);
const absSin = Math.abs(sinRot);
@@ -351,6 +380,18 @@ const useDocumentDrag = () => {
const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - rotatedHalfWidth);
const minCenterY = canvasPadding + rotatedHalfHeight;
const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - rotatedHalfHeight);
const pointerRelativePrevX = pointerCanvasX - previousCenterX;
const pointerRelativePrevY = pointerCanvasY - previousCenterY;
const cosInversePrev = Math.cos(-rotationRad);
const sinInversePrev = Math.sin(-rotationRad);
const pointerLocalPrevX = pointerRelativePrevX * cosInversePrev - pointerRelativePrevY * sinInversePrev;
const pointerLocalPrevY = pointerRelativePrevX * sinInversePrev + pointerRelativePrevY * cosInversePrev;
const pointerInsideRelativeToPrev =
Math.abs(pointerLocalPrevX) <= halfWidth && Math.abs(pointerLocalPrevY) <= halfHeight;
const desiredCenterX = pointerCanvasX - rotatedOffsetX;
const desiredCenterY = pointerCanvasY - rotatedOffsetY;
const clampedCenterX = clamp(desiredCenterX, minCenterX, maxCenterX);
const clampedCenterY = clamp(desiredCenterY, minCenterY, maxCenterY);
@@ -363,19 +404,30 @@ const useDocumentDrag = () => {
state.moved = true;
}
let currentCenterX = clampedCenterX;
let currentCenterY = clampedCenterY;
const updated = { ...entry, centerX: currentCenterX, centerY: currentCenterY };
layoutRef.current.set(state.docId, updated);
const collidedWithHorizontalEdge =
Math.abs(desiredCenterY - clampedCenterY) > EDGE_COLLISION_THRESHOLD;
const collidedWithVerticalEdge =
Math.abs(desiredCenterX - clampedCenterX) > EDGE_COLLISION_THRESHOLD;
const collidedWithEdge = collidedWithHorizontalEdge || collidedWithVerticalEdge;
let currentCenterX = clampedCenterX;
let currentCenterY = clampedCenterY;
if (collidedWithEdge && !pointerInsideRelativeToPrev) {
currentCenterX = previousCenterX;
currentCenterY = previousCenterY;
}
const updated = { ...entry, centerX: currentCenterX, centerY: currentCenterY };
layoutRef.current.set(state.docId, updated);
const offsetX = pointerCanvasX - currentCenterX;
const offsetY = pointerCanvasY - currentCenterY;
const cosInverseCurrent = Math.cos(-rotationRad);
const sinInverseCurrent = Math.sin(-rotationRad);
const pointerLocalX = offsetX * cosInverseCurrent - offsetY * sinInverseCurrent;
const pointerLocalY = offsetX * sinInverseCurrent + offsetY * cosInverseCurrent;
const pointerInsideCard =
Math.abs(pointerLocalX) <= halfWidth && Math.abs(pointerLocalY) <= halfHeight;
const currentTimestamp =
typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)
@@ -415,6 +467,14 @@ const useDocumentDrag = () => {
state.restRotation = nextRest;
}
dampingOverride = ANGULAR_DAMPING + EDGE_COLLISION_EXTRA_DAMPING;
if (!pointerInsideCard) {
torque = 0;
currentCenterX = entry?.centerX ?? currentCenterX;
currentCenterY = entry?.centerY ?? currentCenterY;
if (updated.centerX !== currentCenterX || updated.centerY !== currentCenterY) {
layoutRef.current.set(state.docId, { ...updated, centerX: currentCenterX, centerY: currentCenterY });
}
}
}
state.lastClientX = event.clientX;
@@ -522,6 +582,20 @@ const useDocumentDrag = () => {
}
}
}
const rotationForOffsetDeg = state.rotation || 0;
const rotationForOffsetRad = (rotationForOffsetDeg * Math.PI) / 180;
const cosInverse = Math.cos(-rotationForOffsetRad);
const sinInverse = Math.sin(-rotationForOffsetRad);
const pointerRelativeX = pointerCanvasX - currentCenterX;
const pointerRelativeY = pointerCanvasY - currentCenterY;
const updatedLocalOffsetX = pointerRelativeX * cosInverse - pointerRelativeY * sinInverse;
const updatedLocalOffsetY = pointerRelativeX * sinInverse + pointerRelativeY * cosInverse;
if (!collidedWithEdge || pointerInsideCard) {
state.localPointerOffsetX = updatedLocalOffsetX;
state.localPointerOffsetY = updatedLocalOffsetY;
}
if (debugDrag) {
console.log('[desk] handlePointerMove: moved doc', state.docId, 'to', currentCenterX, currentCenterY);
}
@@ -563,20 +637,35 @@ const useDocumentDrag = () => {
}
const docId = state.docId;
bringToFront(docId);
const metaPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
if (!metaPressed) {
bringToFront(docId);
}
const originInfo = {
rotation: state.rotation || 0,
scale: state.baseScale || 1,
width: state.width,
height: state.height,
};
const docKey = docId != null ? String(docId) : null;
const doc = docKey ? documentLookup.get(docKey) : null;
tapHandler(event, {
docId,
originInfo,
docTitle: doc?.title || 'document',
});
finishDrag(event.pointerId);
openOverlayForDoc(docId, originInfo);
return;
}
finishDrag(event.pointerId);
},
[bringToFront, finishDrag, openOverlayForDoc, startInertiaAnimation],
[
bringToFront,
documentLookup,
finishDrag,
startInertiaAnimation,
tapHandler,
],
);
const handlePointerCancel = useCallback(