Clear a drag placeholder if the user drags an item out of the layout's hit trap (#61)

This commit is contained in:
Evan Simkowitz 2024-06-19 11:15:14 -07:00 committed by GitHub
parent 9c8ab4f555
commit bfa4bb259e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 94 additions and 44 deletions

View File

@ -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();

View File

@ -6,4 +6,5 @@ let { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {
isDev: () => ipcRenderer.sendSync("isDev"),
isDevServer: () => ipcRenderer.sendSync("isDevServer"),
getCursorPoint: () => ipcRenderer.sendSync("getCursorPoint"),
});

View File

@ -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<T> {
/**
@ -77,9 +79,9 @@ export const TileLayout = <T,>({
const [nodeRefs, setNodeRefs] = useState<Map<string, RefObject<HTMLDivElement>>>(new Map());
const [nodeRefsGen, setNodeRefsGen] = useState<number>(0);
useEffect(() => {
console.log("layoutTreeState changed", layoutTreeState);
}, [layoutTreeState]);
// useEffect(() => {
// console.log("layoutTreeState changed", layoutTreeState);
// }, [layoutTreeState]);
const setRef = useCallback(
(id: string, ref: RefObject<HTMLDivElement>) => {
@ -108,32 +110,57 @@ export const TileLayout = <T,>({
},
[nodeRefs, setNodeRefs]
);
const [overlayTransform, setOverlayTransform] = useState<CSSProperties>();
const [layoutLeafTransforms, setLayoutLeafTransforms] = useState<Record<string, CSSProperties>>({});
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<string, CSSProperties> = {};
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 = <T,>({
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 = <T,>({
)
);
}
}, 30),
}),
[activeDrag, overlayContainerRef, displayContainerRef, layoutTreeState.leafs, nodeRefsGen]
);
@ -179,6 +206,12 @@ export const TileLayout = <T,>({
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 = <T,>({
const onLeafClose = useCallback(
async (node: LayoutNode<T>) => {
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 (
<Suspense>
<div className={clsx("tile-layout", className, { animate })}>
<div className={clsx("tile-layout", className, { animate })} onPointerOut={onPointerLeave}>
<div key="display" ref={displayContainerRef} className="display-container">
{layoutLeafTransforms &&
layoutTreeState.leafs.map((leaf) => {
@ -431,18 +464,18 @@ const OverlayNode = <T,>({ 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<unknown, unknown>) => {
if (monitor.isOver({ shallow: true })) {
if (monitor.canDrop()) {
const dragItem = monitor.getItem<LayoutNode<T>>();
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 = <T,>({ layoutNode, layoutTreeState, dispatch, setRef, delete
});
}
}
},
}),
}),
[overlayRef.current, layoutNode, layoutTreeState, dispatch]
);
@ -612,7 +645,7 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefs, style
}
case LayoutTreeActionType.Swap: {
const action = layoutTreeState.pendingAction as LayoutTreeSwapNodeAction<T>;
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) {

View File

@ -116,7 +116,7 @@ function computeMoveNode<T>(
) {
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<T>(
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<T>(
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<T>(
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<T>(
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<T>(
}
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<T> = {
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<any>) {
function moveNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMoveNodeAction<T>) {
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<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeSwap
}
function deleteNode<T>(layoutTreeState: LayoutTreeState<T>, 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<T>(layoutTreeState: LayoutTreeState<T>, 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");
}

View File

@ -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;

View File

@ -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<T> = rxjs.Subject<T> & { refCount: number; release: () => void };