From bfa4bb259e4526186286c006504242ec8e9bab01 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 19 Jun 2024 11:15:14 -0700 Subject: [PATCH] Clear a drag placeholder if the user drags an item out of the layout's hit trap (#61) --- emain/emain.ts | 11 ++++ emain/preload.ts | 1 + frontend/faraday/lib/TileLayout.tsx | 91 ++++++++++++++++++++--------- frontend/faraday/lib/layoutState.ts | 20 ++----- frontend/faraday/lib/utils.ts | 2 +- frontend/types/custom.d.ts | 13 +++++ 6 files changed, 94 insertions(+), 44 deletions(-) diff --git a/emain/emain.ts b/emain/emain.ts index b99da2219..80dbcb36d 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -272,6 +272,17 @@ electron.ipcMain.on("isDevServer", (event) => { event.returnValue = isDevServer; }); +electron.ipcMain.on("getCursorPoint", (event) => { + const window = electron.BrowserWindow.fromWebContents(event.sender); + const screenPoint = electron.screen.getCursorScreenPoint(); + const windowRect = window.getContentBounds(); + const retVal: Point = { + x: screenPoint.x - windowRect.x, + y: screenPoint.y - windowRect.y, + }; + event.returnValue = retVal; +}); + (async () => { const startTs = Date.now(); const instanceLock = electronApp.requestSingleInstanceLock(); diff --git a/emain/preload.ts b/emain/preload.ts index 8e8b14f60..fdef8d936 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -6,4 +6,5 @@ let { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("api", { isDev: () => ipcRenderer.sendSync("isDev"), isDevServer: () => ipcRenderer.sendSync("isDevServer"), + getCursorPoint: () => ipcRenderer.sendSync("getCursorPoint"), }); diff --git a/frontend/faraday/lib/TileLayout.tsx b/frontend/faraday/lib/TileLayout.tsx index e5a537cfe..85ae5c4c2 100644 --- a/frontend/faraday/lib/TileLayout.tsx +++ b/frontend/faraday/lib/TileLayout.tsx @@ -15,9 +15,11 @@ import React, { useRef, useState, } from "react"; -import { useDrag, useDragLayer, useDrop } from "react-dnd"; +import { DropTargetMonitor, useDrag, useDragLayer, useDrop } from "react-dnd"; +import { getApi } from "@/app/store/global"; import useResizeObserver from "@react-hook/resize-observer"; +import { debounce, throttle } from "throttle-debounce"; import { useLayoutTreeStateReducerAtom } from "./layoutAtom"; import { findNode } from "./layoutNode"; import { @@ -34,7 +36,7 @@ import { WritableLayoutTreeStateAtom, } from "./model"; import "./tilelayout.less"; -import { Dimensions, FlexDirection, setTransform as createTransform, debounce, determineDropDirection } from "./utils"; +import { Dimensions, FlexDirection, setTransform as createTransform, determineDropDirection } from "./utils"; export interface TileLayoutProps { /** @@ -77,9 +79,9 @@ export const TileLayout = ({ const [nodeRefs, setNodeRefs] = useState>>(new Map()); const [nodeRefsGen, setNodeRefsGen] = useState(0); - useEffect(() => { - console.log("layoutTreeState changed", layoutTreeState); - }, [layoutTreeState]); + // useEffect(() => { + // console.log("layoutTreeState changed", layoutTreeState); + // }, [layoutTreeState]); const setRef = useCallback( (id: string, ref: RefObject) => { @@ -108,32 +110,57 @@ export const TileLayout = ({ }, [nodeRefs, setNodeRefs] ); - const [overlayTransform, setOverlayTransform] = useState(); const [layoutLeafTransforms, setLayoutLeafTransforms] = useState>({}); - const activeDrag = useDragLayer((monitor) => monitor.isDragging()); + const { activeDrag, dragClientOffset } = useDragLayer((monitor) => ({ + activeDrag: monitor.isDragging(), + dragClientOffset: monitor.getClientOffset(), + })); + + // Effect to detect when the cursor leaves the TileLayout hit trap so we can remove any placeholders. This cannot be done using pointer capture + // because that conflicts with the DnD layer. + useEffect( + debounce(100, () => { + const cursorPoint = getApi().getCursorPoint(); + console.log(cursorPoint); + if (cursorPoint && displayContainerRef.current) { + const displayContainerRect = displayContainerRef.current.getBoundingClientRect(); + const normalizedX = cursorPoint.x - displayContainerRect.x; + const normalizedY = cursorPoint.y - displayContainerRect.y; + if ( + normalizedX <= 0 || + normalizedX >= displayContainerRect.width || + normalizedY <= 0 || + normalizedY >= displayContainerRect.height + ) { + dispatch({ type: LayoutTreeActionType.ClearPendingAction }); + } + } + }), + [dragClientOffset] + ); /** * Callback to update the transforms on the displayed leafs and move the overlay over the display layer when dragging. */ const updateTransforms = useCallback( - debounce(() => { + debounce(30, () => { if (overlayContainerRef.current && displayContainerRef.current) { const displayBoundingRect = displayContainerRef.current.getBoundingClientRect(); - console.log("displayBoundingRect", displayBoundingRect); + // console.log("displayBoundingRect", displayBoundingRect); const overlayBoundingRect = overlayContainerRef.current.getBoundingClientRect(); const newLayoutLeafTransforms: Record = {}; - console.log( - "nodeRefs", - nodeRefs, - "layoutLeafs", - layoutTreeState.leafs, - "layoutTreeState", - layoutTreeState - ); + // console.log( + // "nodeRefs", + // nodeRefs, + // "layoutLeafs", + // layoutTreeState.leafs, + // "layoutTreeState", + // layoutTreeState + // ); for (const leaf of layoutTreeState.leafs) { const leafRef = nodeRefs.get(leaf.id); @@ -155,7 +182,7 @@ export const TileLayout = ({ setLayoutLeafTransforms(newLayoutLeafTransforms); const newOverlayOffset = displayBoundingRect.top + 2 * displayBoundingRect.height; - console.log("overlayOffset", newOverlayOffset); + // console.log("overlayOffset", newOverlayOffset); setOverlayTransform( createTransform( { @@ -168,7 +195,7 @@ export const TileLayout = ({ ) ); } - }, 30), + }), [activeDrag, overlayContainerRef, displayContainerRef, layoutTreeState.leafs, nodeRefsGen] ); @@ -179,6 +206,12 @@ export const TileLayout = ({ useResizeObserver(overlayContainerRef, () => updateTransforms()); + const onPointerLeave = useCallback(() => { + if (activeDrag) { + dispatch({ type: LayoutTreeActionType.ClearPendingAction }); + } + }, [activeDrag, dispatch]); + // Ensure that we don't see any jostling in the layout when we're rendering it the first time. // `animate` will be disabled until after the transforms have all applied the first time. const [animate, setAnimate] = useState(false); @@ -190,23 +223,23 @@ export const TileLayout = ({ const onLeafClose = useCallback( async (node: LayoutNode) => { - console.log("onLeafClose", node); + // console.log("onLeafClose", node); const deleteAction: LayoutTreeDeleteNodeAction = { type: LayoutTreeActionType.DeleteNode, nodeId: node.id, }; - console.log("calling dispatch", deleteAction); + // console.log("calling dispatch", deleteAction); dispatch(deleteAction); - console.log("calling onNodeDelete", node); + // console.log("calling onNodeDelete", node); await onNodeDelete?.(node.data); - console.log("node deleted"); + // console.log("node deleted"); }, [onNodeDelete, dispatch] ); return ( -
+
{layoutLeafTransforms && layoutTreeState.leafs.map((leaf) => { @@ -431,18 +464,18 @@ const OverlayNode = ({ layoutNode, layoutTreeState, dispatch, setRef, delete return false; }, drop: (_, monitor) => { - console.log("drop start", layoutNode.id, layoutTreeState.pendingAction); + // console.log("drop start", layoutNode.id, layoutTreeState.pendingAction); if (!monitor.didDrop() && layoutTreeState.pendingAction) { dispatch({ type: LayoutTreeActionType.CommitPendingAction, }); } }, - hover: (_, monitor) => { + hover: throttle(30, (_, monitor: DropTargetMonitor) => { if (monitor.isOver({ shallow: true })) { if (monitor.canDrop()) { const dragItem = monitor.getItem>(); - console.log("computing operation", layoutNode, dragItem, layoutTreeState.pendingAction); + // console.log("computing operation", layoutNode, dragItem, layoutTreeState.pendingAction); dispatch({ type: LayoutTreeActionType.ComputeMove, node: layoutNode, @@ -458,7 +491,7 @@ const OverlayNode = ({ layoutNode, layoutTreeState, dispatch, setRef, delete }); } } - }, + }), }), [overlayRef.current, layoutNode, layoutTreeState, dispatch] ); @@ -612,7 +645,7 @@ const Placeholder = ({ layoutTreeState, overlayContainerRef, nodeRefs, style } case LayoutTreeActionType.Swap: { const action = layoutTreeState.pendingAction as LayoutTreeSwapNodeAction; - console.log("placeholder for swap", action); + // console.log("placeholder for swap", action); const targetNode = action.node1; const targetRef = nodeRefs.get(targetNode?.id); if (targetRef?.current) { diff --git a/frontend/faraday/lib/layoutState.ts b/frontend/faraday/lib/layoutState.ts index b55d9718e..8a27de7af 100644 --- a/frontend/faraday/lib/layoutState.ts +++ b/frontend/faraday/lib/layoutState.ts @@ -116,7 +116,7 @@ function computeMoveNode( ) { const rootNode = layoutTreeState.rootNode; const { node, nodeToMove, direction } = computeInsertAction; - console.log("computeInsertOperation start", layoutTreeState.rootNode, node, nodeToMove, direction); + // console.log("computeInsertOperation start", layoutTreeState.rootNode, node, nodeToMove, direction); if (direction === undefined) { console.warn("No direction provided for insertItemInDirection"); return; @@ -141,10 +141,8 @@ function computeMoveNode( switch (direction) { case DropDirection.OuterTop: if (node.flexDirection === FlexDirection.Column) { - console.log("outer top column"); const grandparentNode = grandparent(); if (grandparentNode) { - console.log("has grandparent", grandparentNode); const index = indexInGrandparent(); newMoveOperation = { parentId: grandparentNode.id, @@ -176,10 +174,8 @@ function computeMoveNode( break; case DropDirection.OuterBottom: if (node.flexDirection === FlexDirection.Column) { - console.log("outer bottom column"); const grandparentNode = grandparent(); if (grandparentNode) { - console.log("has grandparent", grandparentNode); const index = indexInGrandparent() + 1; newMoveOperation = { parentId: grandparentNode.id, @@ -211,10 +207,8 @@ function computeMoveNode( break; case DropDirection.OuterLeft: if (node.flexDirection === FlexDirection.Row) { - console.log("outer left row"); const grandparentNode = grandparent(); if (grandparentNode) { - console.log("has grandparent", grandparentNode); const index = indexInGrandparent(); newMoveOperation = { parentId: grandparentNode.id, @@ -239,10 +233,8 @@ function computeMoveNode( break; case DropDirection.OuterRight: if (node.flexDirection === FlexDirection.Row) { - console.log("outer right row"); const grandparentNode = grandparent(); if (grandparentNode) { - console.log("has grandparent", grandparentNode); const index = indexInGrandparent() + 1; newMoveOperation = { parentId: grandparentNode.id, @@ -266,14 +258,14 @@ function computeMoveNode( } break; case DropDirection.Center: - console.log("center drop", rootNode, node, nodeToMove); + // console.log("center drop", rootNode, node, nodeToMove); if (node.id !== rootNode.id && nodeToMove.id !== rootNode.id) { const swapAction: LayoutTreeSwapNodeAction = { type: LayoutTreeActionType.Swap, node1: node, node2: nodeToMove, }; - console.log("swapAction", swapAction); + // console.log("swapAction", swapAction); layoutTreeState.pendingAction = swapAction; return; } else { @@ -301,7 +293,7 @@ function clearPendingAction(layoutTreeState: LayoutTreeState) { function moveNode(layoutTreeState: LayoutTreeState, action: LayoutTreeMoveNodeAction) { const rootNode = layoutTreeState.rootNode; - console.log("moveNode", action, layoutTreeState.rootNode); + // console.log("moveNode", action, layoutTreeState.rootNode); if (!action) { console.error("no move node action provided"); return; @@ -397,7 +389,7 @@ function swapNode(layoutTreeState: LayoutTreeState, action: LayoutTreeSwap } function deleteNode(layoutTreeState: LayoutTreeState, action: LayoutTreeDeleteNodeAction) { - console.log("deleteNode", layoutTreeState, action); + // console.log("deleteNode", layoutTreeState, action); if (!action?.nodeId) { console.error("no delete node action provided"); return; @@ -415,7 +407,7 @@ function deleteNode(layoutTreeState: LayoutTreeState, action: LayoutTreeDe if (parent) { const node = parent.children.find((child) => child.id === action.nodeId); removeChild(parent, node); - console.log("node deleted", parent, node); + // console.log("node deleted", parent, node); } else { console.error("unable to delete node, not found in tree"); } diff --git a/frontend/faraday/lib/utils.ts b/frontend/faraday/lib/utils.ts index 35a5483b7..d6c6f0fb1 100644 --- a/frontend/faraday/lib/utils.ts +++ b/frontend/faraday/lib/utils.ts @@ -33,7 +33,7 @@ export function reverseFlexDirection(flexDirection: FlexDirection): FlexDirectio } export function determineDropDirection(dimensions?: Dimensions, offset?: XYCoord | null): DropDirection | undefined { - console.log("determineDropDirection", dimensions, offset); + // console.log("determineDropDirection", dimensions, offset); if (!offset || !dimensions) return undefined; const { width, height, left, top } = dimensions; let { x, y } = offset; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 1326e2590..c8ca77f19 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -7,8 +7,21 @@ declare global { }; type ElectronApi = { + /** + * Determines whether the current app instance is a development build. + * @returns True if the current app instance is a development build. + */ isDev: () => boolean; + /** + * Determines whether the current app instance is hosted in a Vite dev server. + * @returns True if the current app instance is hosted in a Vite dev server. + */ isDevServer: () => boolean; + /** + * Get a point value representing the cursor's position relative to the calling BrowserWindow + * @returns A point value. + */ + getCursorPoint: () => Electron.Point; }; type SubjectWithRef = rxjs.Subject & { refCount: number; release: () => void };