From 2c6f6d917fb821ca6f9350edbf84d7e4f8150597 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Fri, 21 Jun 2024 10:18:35 -0700 Subject: [PATCH] Fix greedy rendering of drag preview (#68) --- frontend/app/tab/tabcontent.tsx | 9 ++- frontend/faraday/lib/TileLayout.stories.tsx | 11 ++- frontend/faraday/lib/TileLayout.tsx | 90 +++++++++++---------- package.json | 1 + yarn.lock | 10 +++ 5 files changed, 76 insertions(+), 45 deletions(-) diff --git a/frontend/app/tab/tabcontent.tsx b/frontend/app/tab/tabcontent.tsx index d1c3a19de..6f9410d17 100644 --- a/frontend/app/tab/tabcontent.tsx +++ b/frontend/app/tab/tabcontent.tsx @@ -10,6 +10,7 @@ import { TileLayout } from "@/faraday/index"; import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom"; import { useAtomValue } from "jotai"; import { useCallback, useMemo } from "react"; +import { getApi } from "../store/global"; import "./tabcontent.less"; const TabContent = ({ tabId }: { tabId: string }) => { @@ -27,7 +28,6 @@ const TabContent = ({ tabId }: { tabId: string }) => { onClose: () => void, dragHandleRef: React.RefObject ) => { - // console.log("renderBlock", tabData); if (!tabData.blockId || !ready) { return null; } @@ -37,15 +37,17 @@ const TabContent = ({ tabId }: { tabId: string }) => { ); const renderPreview = useCallback((tabData: TabLayoutData) => { - console.log("renderPreview", tabData); return ; }, []); const onNodeDelete = useCallback((data: TabLayoutData) => { - console.log("onNodeDelete", data); return services.ObjectService.DeleteBlock(data.blockId); }, []); + const getCursorPoint = useCallback(() => { + return getApi().getCursorPoint(); + }, []); + if (tabLoading) { return ; } @@ -66,6 +68,7 @@ const TabContent = ({ tabId }: { tabId: string }) => { renderPreview={renderPreview} layoutTreeStateAtom={layoutStateAtom} onNodeDelete={onNodeDelete} + getCursorPoint={getCursorPoint} /> ); diff --git a/frontend/faraday/lib/TileLayout.stories.tsx b/frontend/faraday/lib/TileLayout.stories.tsx index 1a8c99842..ebcafb3d6 100644 --- a/frontend/faraday/lib/TileLayout.stories.tsx +++ b/frontend/faraday/lib/TileLayout.stories.tsx @@ -26,7 +26,16 @@ const meta = { name: "Hello world!", }) ), - renderContent: renderTestData, + renderContent: ( + data: TestData, + _ready: boolean, + _onClose: () => void, + dragHandleRef: React.RefObject + ) => ( +
+ {renderTestData(data)} +
+ ), renderPreview: renderTestData, }, component: TileLayout, diff --git a/frontend/faraday/lib/TileLayout.tsx b/frontend/faraday/lib/TileLayout.tsx index e9b335a94..6aba83ee4 100644 --- a/frontend/faraday/lib/TileLayout.tsx +++ b/frontend/faraday/lib/TileLayout.tsx @@ -1,7 +1,6 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { getApi } from "@/app/store/global"; import useResizeObserver from "@react-hook/resize-observer"; import clsx from "clsx"; import { toPng } from "html-to-image"; @@ -19,6 +18,7 @@ import React, { } from "react"; import { DropTargetMonitor, useDrag, useDragLayer, useDrop } from "react-dnd"; import { debounce, throttle } from "throttle-debounce"; +import { useDevicePixelRatio } from "use-device-pixel-ratio"; import { useLayoutTreeStateReducerAtom } from "./layoutAtom"; import { findNode } from "./layoutNode"; import { @@ -59,6 +59,12 @@ export interface TileLayoutProps { * The class name to use for the top-level div of the tile layout. */ className?: string; + + /** + * A callback for getting the cursor point in reference to the current window. This removes Electron as a runtime dependency, allowing for better integration with Storybook. + * @returns The cursor position relative to the current window. + */ + getCursorPoint?: () => Point; } const DragPreviewWidth = 300; @@ -70,6 +76,7 @@ export const TileLayout = ({ renderContent, renderPreview, onNodeDelete, + getCursorPoint, }: TileLayoutProps) => { const overlayContainerRef = useRef(null); const displayContainerRef = useRef(null); @@ -121,7 +128,7 @@ export const TileLayout = ({ // because that conflicts with the DnD layer. useEffect( debounce(100, () => { - const cursorPoint = getApi().getCursorPoint(); + const cursorPoint = getCursorPoint?.() ?? dragClientOffset; if (cursorPoint && displayContainerRef.current) { const displayContainerRect = displayContainerRef.current.getBoundingClientRect(); const normalizedX = cursorPoint.x - displayContainerRect.x; @@ -324,9 +331,9 @@ const DisplayNode = ({ const tileNodeRef = useRef(null); const dragHandleRef = useRef(null); const previewRef = useRef(null); - const hasImagePreviewSetRef = useRef(false); - // Register the node as a draggable item. + const devicePixelRatio = useDevicePixelRatio(); + const [{ isDragging }, drag, dragPreview] = useDrag( () => ({ type: dragItemType, @@ -338,42 +345,55 @@ const DisplayNode = ({ [layoutNode] ); - const previewElement = renderPreview?.(layoutNode.data); - const previewWidth = DragPreviewWidth; - const previewHeight = DragPreviewHeight; - const previewTransform = `scale(${1 / window.devicePixelRatio})`; + const [previewElementGeneration, setPreviewElementGeneration] = useState(0); + const previewElement = useMemo(() => { + setPreviewElementGeneration(previewElementGeneration + 1); + return ( +
+
+ {renderPreview?.(layoutNode.data)} +
+
+ ); + }, [renderPreview, devicePixelRatio]); + const [previewImage, setPreviewImage] = useState(null); - // we set the drag preview on load to be the HTML element - // later, on pointerenter, we generate a static png preview to use instead (for performance) - useEffect(() => { - if (!hasImagePreviewSetRef.current) { - dragPreview(previewRef.current); - } - }, []); + const [previewImageGeneration, setPreviewImageGeneration] = useState(0); const generatePreviewImage = useCallback(() => { - let offsetX = (DragPreviewWidth * window.devicePixelRatio - DragPreviewWidth) / 2 + 10; - let offsetY = (DragPreviewHeight * window.devicePixelRatio - DragPreviewHeight) / 2 + 10; - if (previewImage != null) { + const offsetX = (DragPreviewWidth * devicePixelRatio - DragPreviewWidth) / 2 + 10; + const offsetY = (DragPreviewHeight * devicePixelRatio - DragPreviewHeight) / 2 + 10; + if (previewImage !== null && previewElementGeneration === previewImageGeneration) { dragPreview(previewImage, { offsetY, offsetX }); } else if (previewRef.current) { + setPreviewImageGeneration(previewElementGeneration); toPng(previewRef.current).then((url) => { const img = new Image(); img.src = url; - img.onload = () => { - hasImagePreviewSetRef.current = true; - setPreviewImage(img); - dragPreview(img, { offsetY, offsetX }); - }; + setPreviewImage(img); + dragPreview(img, { offsetY, offsetX }); }); } - }, [previewRef, previewImage, dragPreview]); + }, [ + dragPreview, + previewRef.current, + previewElementGeneration, + previewImageGeneration, + previewImage, + devicePixelRatio, + ]); // Register the tile item as a draggable component useEffect(() => { - if (dragHandleRef.current) { - drag(dragHandleRef); - } - }, [ready]); + drag(dragHandleRef); + }, [drag, dragHandleRef.current]); const onClose = useCallback(() => { onLeafClose(layoutNode); @@ -402,19 +422,7 @@ const DisplayNode = ({ onPointerEnter={generatePreviewImage} > {leafContent} -
-
- {previewElement} -
-
+ {previewElement} ); }; diff --git a/package.json b/package.json index 8a9656ec1..e12c8d6ad 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "remark-gfm": "^4.0.0", "rxjs": "^7.8.1", "throttle-debounce": "^5.0.0", + "use-device-pixel-ratio": "^1.1.2", "uuid": "^9.0.1" }, "packageManager": "yarn@4.3.0" diff --git a/yarn.lock b/yarn.lock index 41b3dfe57..beb84cc71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12330,6 +12330,7 @@ __metadata: tsx: "npm:^4.15.4" typescript: "npm:^5.4.5" typescript-eslint: "npm:^7.8.0" + use-device-pixel-ratio: "npm:^1.1.2" uuid: "npm:^9.0.1" vite: "npm:^5.0.0" vite-plugin-static-copy: "npm:^1.0.5" @@ -12886,6 +12887,15 @@ __metadata: languageName: node linkType: hard +"use-device-pixel-ratio@npm:^1.1.2": + version: 1.1.2 + resolution: "use-device-pixel-ratio@npm:1.1.2" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/125d3f75b82de0dd754ad2c930a4441e2845cfbacfa6d818857b6991da60dc4b41d33a791ff5b84725a10616d84c7beb6a75ef489c63b3f24fa910779a284099 + languageName: node + linkType: hard + "use-sidecar@npm:^1.1.2": version: 1.1.2 resolution: "use-sidecar@npm:1.1.2"