rename skeuo to desktop

This commit is contained in:
2025-10-28 10:41:56 +01:00
parent 94578475ea
commit e150b81190
8 changed files with 1093 additions and 686 deletions
+17
View File
@@ -0,0 +1,17 @@
import React, { createContext, useContext } from 'react';
const DesktopContext = createContext(null);
export const DesktopProvider = ({ value, children }) => (
<DesktopContext.Provider value={value}>{children}</DesktopContext.Provider>
);
export const useDesktopContext = () => {
const context = useContext(DesktopContext);
if (!context) {
throw new Error('useDesktopContext must be used within a DesktopProvider');
}
return context;
};
export default DesktopContext;
+17
View File
@@ -0,0 +1,17 @@
export const preventAll = (event) => {
if (!event) {
return;
}
try {
event.preventDefault();
} catch (
// eslint-disable-next-line no-empty
error
) {}
try {
event.stopPropagation();
} catch (
// eslint-disable-next-line no-empty
error
) {}
};
+8
View File
@@ -0,0 +1,8 @@
export const clamp = (value, min, max) => {
if (value < min) return min;
if (value > max) return max;
return value;
};
export const formatTransform = (x, y, rotation = 0, scale = 1) =>
`translate3d(${x}px, ${y}px, 0) rotate(${rotation}deg) scale(${scale})`;
+248
View File
@@ -0,0 +1,248 @@
import { useCallback, useRef } from 'react';
import { useDesktopContext } from './context';
import { preventAll } from './events';
import { clamp, formatTransform } from './math';
const useDocumentDrag = () => {
const {
layoutRef,
itemRefs,
documentLookup,
ensureDocumentSize,
resolveBaseMetrics,
bringToFront,
setDraggingId,
syncLayoutSnapshot,
canvasSize,
openOverlayForDoc,
recalcVisibleDocIds,
settings,
} = useDesktopContext();
const dragStateRef = useRef(null);
const { canvasPadding, defaultCanvasWidth, defaultCanvasHeight, debugDrag } = settings;
const finishDrag = useCallback(
(pointerId) => {
const state = dragStateRef.current;
if (!state || state.pointerId !== pointerId) {
return;
}
const capturedTarget = state.capturedTarget;
if (capturedTarget && typeof capturedTarget.releasePointerCapture === 'function') {
try {
capturedTarget.releasePointerCapture(pointerId);
} catch (
// eslint-disable-next-line no-empty
error
) {}
}
dragStateRef.current = null;
setDraggingId((current) => (current === state.docId ? null : current));
syncLayoutSnapshot();
},
[setDraggingId, syncLayoutSnapshot],
);
const handlePointerDown = useCallback(
(event, docId) => {
if (debugDrag) {
console.log(
'[skeuo] handlePointerDown fired for doc',
docId,
'button',
event.button,
'pointerType',
event.pointerType,
'pointerId',
event.pointerId,
);
}
preventAll(event);
const entry = layoutRef.current.get(docId) || null;
const docKey = docId != null ? String(docId) : null;
const doc = docKey ? documentLookup.get(docKey) : null;
const { width: docWidth, height: docHeight } = ensureDocumentSize(doc);
const { baseScale } = resolveBaseMetrics(doc, docWidth, docHeight);
const normalizedBaseScale =
Number.isFinite(baseScale) && baseScale > 0 ? baseScale : 1;
const defaultCenterX = canvasPadding + docWidth / 2;
const defaultCenterY = canvasPadding + docHeight / 2;
const centerX = typeof entry?.centerX === 'number' ? entry.centerX : defaultCenterX;
const centerY = typeof entry?.centerY === 'number' ? entry.centerY : defaultCenterY;
if (entry && (entry.centerX !== centerX || entry.centerY !== centerY)) {
layoutRef.current.set(docId, { ...entry, centerX, centerY });
}
bringToFront(docId);
const capturedTarget = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
if (capturedTarget && typeof capturedTarget.setPointerCapture === 'function') {
try {
capturedTarget.setPointerCapture(event.pointerId);
} catch (
// eslint-disable-next-line no-empty
error
) {}
}
dragStateRef.current = {
docId,
pointerId: event.pointerId,
originCenterX: centerX,
originCenterY: centerY,
startX: event.clientX,
startY: event.clientY,
rotation: entry?.rotation ?? 0,
moved: false,
locked: false,
width: docWidth,
height: docHeight,
dragScale: 1,
baseScale: normalizedBaseScale,
capturedTarget,
};
setDraggingId(docId);
},
[
bringToFront,
canvasPadding,
documentLookup,
ensureDocumentSize,
layoutRef,
resolveBaseMetrics,
setDraggingId,
debugDrag,
],
);
const handlePointerMove = useCallback(
(event) => {
const state = dragStateRef.current;
if (!state) {
if (debugDrag) {
console.log('[skeuo] handlePointerMove: no drag state for pointer', event.pointerId);
}
return;
}
if (state.pointerId !== event.pointerId) {
if (debugDrag) {
console.log(
'[skeuo] handlePointerMove: pointer mismatch expected',
state.pointerId,
'got',
event.pointerId,
);
}
return;
}
preventAll(event);
if (state.locked) {
if (debugDrag) {
console.log('[skeuo] handlePointerMove: locked drag for doc', state.docId);
}
return;
}
const entry = layoutRef.current.get(state.docId);
if (!entry) {
return;
}
const deltaX = event.clientX - state.startX;
const deltaY = event.clientY - state.startY;
const nextCenterX = state.originCenterX + deltaX;
const nextCenterY = state.originCenterY + deltaY;
const docWidth = state.width;
const docHeight = state.height;
const halfWidth = docWidth / 2;
const halfHeight = docHeight / 2;
const canvasWidth = canvasSize.width || defaultCanvasWidth;
const canvasHeight = canvasSize.height || defaultCanvasHeight;
const minCenterX = canvasPadding + halfWidth;
const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - halfWidth);
const minCenterY = canvasPadding + halfHeight;
const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - halfHeight);
const clampedCenterX = clamp(nextCenterX, minCenterX, maxCenterX);
const clampedCenterY = clamp(nextCenterY, minCenterY, maxCenterY);
const prevCenterX = typeof entry.centerX === 'number' ? entry.centerX : state.originCenterX;
const prevCenterY = typeof entry.centerY === 'number' ? entry.centerY : state.originCenterY;
if (Math.abs(clampedCenterX - prevCenterX) < 0.5 && Math.abs(clampedCenterY - prevCenterY) < 0.5) {
if (debugDrag) {
console.log('[skeuo] handlePointerMove: movement under threshold for doc', state.docId);
}
return;
}
const updated = { ...entry, centerX: clampedCenterX, centerY: clampedCenterY };
layoutRef.current.set(state.docId, updated);
const node = itemRefs.current.get(state.docId);
if (node) {
node.style.transform = formatTransform(
clampedCenterX - state.width / 2,
clampedCenterY - state.height / 2,
state.rotation,
state.dragScale || 1,
);
}
state.moved = true;
if (debugDrag) {
console.log('[skeuo] handlePointerMove: moved doc', state.docId, 'to', clampedCenterX, clampedCenterY);
}
recalcVisibleDocIds();
},
[
canvasPadding,
canvasSize.height,
canvasSize.width,
defaultCanvasHeight,
defaultCanvasWidth,
itemRefs,
layoutRef,
recalcVisibleDocIds,
debugDrag,
],
);
const handlePointerUp = useCallback(
(event) => {
const state = dragStateRef.current;
if (state && state.pointerId === event.pointerId) {
const moved = Boolean(state.moved);
const docId = state.docId;
const shouldOpen = !moved && event.detail >= 2;
finishDrag(event.pointerId);
if (shouldOpen) {
const originInfo = {
rotation: state.rotation || 0,
scale: state.baseScale || 1,
width: state.width,
height: state.height,
};
openOverlayForDoc(docId, originInfo);
}
return;
}
finishDrag(event.pointerId);
},
[finishDrag, openOverlayForDoc],
);
const handlePointerCancel = useCallback(
(event) => {
finishDrag(event.pointerId);
},
[finishDrag],
);
return {
handlePointerDown,
handlePointerMove,
handlePointerUp,
handlePointerCancel,
};
};
export default useDocumentDrag;