diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-25 16:47:22 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:48:22 +0200 |
| commit | 8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch) | |
| tree | e9d0cafa93904ffcbec3188c37961bcc16c6422d /legacy/use-assets-selection-box.ts | |
| parent | 39ea298a9ccba3104c549c0ca079c231981811f5 (diff) | |
| download | opencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz | |
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'legacy/use-assets-selection-box.ts')
| -rw-r--r-- | legacy/use-assets-selection-box.ts | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/legacy/use-assets-selection-box.ts b/legacy/use-assets-selection-box.ts new file mode 100644 index 00000000..cb8ae2ed --- /dev/null +++ b/legacy/use-assets-selection-box.ts @@ -0,0 +1,198 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +interface SelectionBoxState { + startPos: { x: number; y: number }; + currentPos: { x: number; y: number }; + isActive: boolean; + isAdditive: boolean; + initialSelectedIds: string[]; + initialAnchorId: string | null; +} + +function isIntersecting({ + selectionRect, + itemRect, +}: { + selectionRect: DOMRect; + itemRect: DOMRect; +}) { + return !( + itemRect.right < selectionRect.left || + itemRect.left > selectionRect.right || + itemRect.bottom < selectionRect.top || + itemRect.top > selectionRect.bottom + ); +} + +function createSelectionRect({ + startPos, + currentPos, +}: { + startPos: { x: number; y: number }; + currentPos: { x: number; y: number }; +}) { + return new DOMRect( + Math.min(startPos.x, currentPos.x), + Math.min(startPos.y, currentPos.y), + Math.abs(currentPos.x - startPos.x), + Math.abs(currentPos.y - startPos.y), + ); +} + +export function useAssetsSelectionBox({ + containerRef, + getSelectableElements, + selectedIds, + selectionAnchorId, + onSelectionChange, +}: { + containerRef: React.RefObject<HTMLElement | null>; + getSelectableElements: () => Map<string, HTMLElement>; + selectedIds: string[]; + selectionAnchorId: string | null; + onSelectionChange: ({ + intersectedIds, + initialSelectedIds, + initialAnchorId, + isAdditive, + }: { + intersectedIds: string[]; + initialSelectedIds: string[]; + initialAnchorId: string | null; + isAdditive: boolean; + }) => void; +}) { + const [selectionBox, setSelectionBox] = useState<SelectionBoxState | null>( + null, + ); + const justFinishedSelectingRef = useRef(false); + const isAssetItemTarget = useCallback((target: EventTarget | null) => { + return target instanceof Element && target.closest("[data-asset-item='true']"); + }, []); + + const handleMouseDown = useCallback( + (event: React.MouseEvent<HTMLElement>) => { + if (event.button !== 0) { + return; + } + + if (isAssetItemTarget(event.target)) { + return; + } + + setSelectionBox({ + startPos: { x: event.clientX, y: event.clientY }, + currentPos: { x: event.clientX, y: event.clientY }, + isActive: false, + isAdditive: event.ctrlKey || event.metaKey, + initialSelectedIds: selectedIds, + initialAnchorId: selectionAnchorId, + }); + }, + [isAssetItemTarget, selectedIds, selectionAnchorId], + ); + + const updateSelection = useCallback( + ({ + startPos, + currentPos, + isAdditive, + initialSelectedIds, + initialAnchorId, + }: SelectionBoxState) => { + const selectionRect = createSelectionRect({ startPos, currentPos }); + const intersectedIds = [...getSelectableElements().entries()] + .filter(([, element]) => + isIntersecting({ + selectionRect, + itemRect: element.getBoundingClientRect(), + }), + ) + .map(([id]) => id); + + onSelectionChange({ + intersectedIds, + initialSelectedIds, + initialAnchorId, + isAdditive, + }); + }, + [getSelectableElements, onSelectionChange], + ); + + useEffect(() => { + if (!selectionBox) { + return; + } + + const handleMouseMove = ({ clientX, clientY }: MouseEvent) => { + const deltaX = Math.abs(clientX - selectionBox.startPos.x); + const deltaY = Math.abs(clientY - selectionBox.startPos.y); + const shouldActivate = deltaX > 5 || deltaY > 5; + const nextSelectionBox = { + ...selectionBox, + currentPos: { x: clientX, y: clientY }, + isActive: shouldActivate || selectionBox.isActive, + }; + + setSelectionBox(nextSelectionBox); + + if (!nextSelectionBox.isActive) { + return; + } + + updateSelection(nextSelectionBox); + }; + + const handleMouseUp = () => { + if (selectionBox.isActive) { + justFinishedSelectingRef.current = true; + requestAnimationFrame(() => { + justFinishedSelectingRef.current = false; + }); + } + + setSelectionBox(null); + }; + + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [selectionBox, updateSelection]); + + useEffect(() => { + if (!selectionBox) { + return; + } + + const previousBodyUserSelect = document.body.style.userSelect; + const previousContainerUserSelect = containerRef.current?.style.userSelect ?? ""; + + document.body.style.userSelect = "none"; + if (containerRef.current) { + containerRef.current.style.userSelect = "none"; + } + + return () => { + document.body.style.userSelect = previousBodyUserSelect; + if (containerRef.current) { + containerRef.current.style.userSelect = previousContainerUserSelect; + } + }; + }, [containerRef, selectionBox]); + + const shouldIgnoreClick = useCallback(() => { + return justFinishedSelectingRef.current; + }, []); + + return { + selectionBox, + isSelecting: selectionBox?.isActive ?? false, + handleMouseDown, + shouldIgnoreClick, + }; +} |
