Fix greedy rendering of drag preview (#68)

This commit is contained in:
Evan Simkowitz 2024-06-21 10:18:35 -07:00 committed by GitHub
parent b8b03ea817
commit 2c6f6d917f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 76 additions and 45 deletions

View File

@ -10,6 +10,7 @@ import { TileLayout } from "@/faraday/index";
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom"; import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { getApi } from "../store/global";
import "./tabcontent.less"; import "./tabcontent.less";
const TabContent = ({ tabId }: { tabId: string }) => { const TabContent = ({ tabId }: { tabId: string }) => {
@ -27,7 +28,6 @@ const TabContent = ({ tabId }: { tabId: string }) => {
onClose: () => void, onClose: () => void,
dragHandleRef: React.RefObject<HTMLDivElement> dragHandleRef: React.RefObject<HTMLDivElement>
) => { ) => {
// console.log("renderBlock", tabData);
if (!tabData.blockId || !ready) { if (!tabData.blockId || !ready) {
return null; return null;
} }
@ -37,15 +37,17 @@ const TabContent = ({ tabId }: { tabId: string }) => {
); );
const renderPreview = useCallback((tabData: TabLayoutData) => { const renderPreview = useCallback((tabData: TabLayoutData) => {
console.log("renderPreview", tabData);
return <BlockFrame blockId={tabData.blockId} preview={true} />; return <BlockFrame blockId={tabData.blockId} preview={true} />;
}, []); }, []);
const onNodeDelete = useCallback((data: TabLayoutData) => { const onNodeDelete = useCallback((data: TabLayoutData) => {
console.log("onNodeDelete", data);
return services.ObjectService.DeleteBlock(data.blockId); return services.ObjectService.DeleteBlock(data.blockId);
}, []); }, []);
const getCursorPoint = useCallback(() => {
return getApi().getCursorPoint();
}, []);
if (tabLoading) { if (tabLoading) {
return <CenteredLoadingDiv />; return <CenteredLoadingDiv />;
} }
@ -66,6 +68,7 @@ const TabContent = ({ tabId }: { tabId: string }) => {
renderPreview={renderPreview} renderPreview={renderPreview}
layoutTreeStateAtom={layoutStateAtom} layoutTreeStateAtom={layoutStateAtom}
onNodeDelete={onNodeDelete} onNodeDelete={onNodeDelete}
getCursorPoint={getCursorPoint}
/> />
</div> </div>
); );

View File

@ -26,7 +26,16 @@ const meta = {
name: "Hello world!", name: "Hello world!",
}) })
), ),
renderContent: renderTestData, renderContent: (
data: TestData,
_ready: boolean,
_onClose: () => void,
dragHandleRef: React.RefObject<HTMLDivElement>
) => (
<div ref={dragHandleRef} className="test-content" style={{ width: "100%", height: "100%" }}>
{renderTestData(data)}
</div>
),
renderPreview: renderTestData, renderPreview: renderTestData,
}, },
component: TileLayout<TestData>, component: TileLayout<TestData>,

View File

@ -1,7 +1,6 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { getApi } from "@/app/store/global";
import useResizeObserver from "@react-hook/resize-observer"; import useResizeObserver from "@react-hook/resize-observer";
import clsx from "clsx"; import clsx from "clsx";
import { toPng } from "html-to-image"; import { toPng } from "html-to-image";
@ -19,6 +18,7 @@ import React, {
} from "react"; } from "react";
import { DropTargetMonitor, useDrag, useDragLayer, useDrop } from "react-dnd"; import { DropTargetMonitor, useDrag, useDragLayer, useDrop } from "react-dnd";
import { debounce, throttle } from "throttle-debounce"; import { debounce, throttle } from "throttle-debounce";
import { useDevicePixelRatio } from "use-device-pixel-ratio";
import { useLayoutTreeStateReducerAtom } from "./layoutAtom"; import { useLayoutTreeStateReducerAtom } from "./layoutAtom";
import { findNode } from "./layoutNode"; import { findNode } from "./layoutNode";
import { import {
@ -59,6 +59,12 @@ export interface TileLayoutProps<T> {
* The class name to use for the top-level div of the tile layout. * The class name to use for the top-level div of the tile layout.
*/ */
className?: string; 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; const DragPreviewWidth = 300;
@ -70,6 +76,7 @@ export const TileLayout = <T,>({
renderContent, renderContent,
renderPreview, renderPreview,
onNodeDelete, onNodeDelete,
getCursorPoint,
}: TileLayoutProps<T>) => { }: TileLayoutProps<T>) => {
const overlayContainerRef = useRef<HTMLDivElement>(null); const overlayContainerRef = useRef<HTMLDivElement>(null);
const displayContainerRef = useRef<HTMLDivElement>(null); const displayContainerRef = useRef<HTMLDivElement>(null);
@ -121,7 +128,7 @@ export const TileLayout = <T,>({
// because that conflicts with the DnD layer. // because that conflicts with the DnD layer.
useEffect( useEffect(
debounce(100, () => { debounce(100, () => {
const cursorPoint = getApi().getCursorPoint(); const cursorPoint = getCursorPoint?.() ?? dragClientOffset;
if (cursorPoint && displayContainerRef.current) { if (cursorPoint && displayContainerRef.current) {
const displayContainerRect = displayContainerRef.current.getBoundingClientRect(); const displayContainerRect = displayContainerRef.current.getBoundingClientRect();
const normalizedX = cursorPoint.x - displayContainerRect.x; const normalizedX = cursorPoint.x - displayContainerRect.x;
@ -324,9 +331,9 @@ const DisplayNode = <T,>({
const tileNodeRef = useRef<HTMLDivElement>(null); const tileNodeRef = useRef<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null); const dragHandleRef = useRef<HTMLDivElement>(null);
const previewRef = useRef<HTMLDivElement>(null); const previewRef = useRef<HTMLDivElement>(null);
const hasImagePreviewSetRef = useRef(false);
// Register the node as a draggable item. const devicePixelRatio = useDevicePixelRatio();
const [{ isDragging }, drag, dragPreview] = useDrag( const [{ isDragging }, drag, dragPreview] = useDrag(
() => ({ () => ({
type: dragItemType, type: dragItemType,
@ -338,42 +345,55 @@ const DisplayNode = <T,>({
[layoutNode] [layoutNode]
); );
const previewElement = renderPreview?.(layoutNode.data); const [previewElementGeneration, setPreviewElementGeneration] = useState(0);
const previewWidth = DragPreviewWidth; const previewElement = useMemo(() => {
const previewHeight = DragPreviewHeight; setPreviewElementGeneration(previewElementGeneration + 1);
const previewTransform = `scale(${1 / window.devicePixelRatio})`; return (
<div key="preview" className="tile-preview-container">
<div
className="tile-preview"
ref={previewRef}
style={{
width: DragPreviewWidth,
height: DragPreviewHeight,
transform: `scale(${1 / devicePixelRatio})`,
}}
>
{renderPreview?.(layoutNode.data)}
</div>
</div>
);
}, [renderPreview, devicePixelRatio]);
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null); const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
// we set the drag preview on load to be the HTML element const [previewImageGeneration, setPreviewImageGeneration] = useState(0);
// later, on pointerenter, we generate a static png preview to use instead (for performance)
useEffect(() => {
if (!hasImagePreviewSetRef.current) {
dragPreview(previewRef.current);
}
}, []);
const generatePreviewImage = useCallback(() => { const generatePreviewImage = useCallback(() => {
let offsetX = (DragPreviewWidth * window.devicePixelRatio - DragPreviewWidth) / 2 + 10; const offsetX = (DragPreviewWidth * devicePixelRatio - DragPreviewWidth) / 2 + 10;
let offsetY = (DragPreviewHeight * window.devicePixelRatio - DragPreviewHeight) / 2 + 10; const offsetY = (DragPreviewHeight * devicePixelRatio - DragPreviewHeight) / 2 + 10;
if (previewImage != null) { if (previewImage !== null && previewElementGeneration === previewImageGeneration) {
dragPreview(previewImage, { offsetY, offsetX }); dragPreview(previewImage, { offsetY, offsetX });
} else if (previewRef.current) { } else if (previewRef.current) {
setPreviewImageGeneration(previewElementGeneration);
toPng(previewRef.current).then((url) => { toPng(previewRef.current).then((url) => {
const img = new Image(); const img = new Image();
img.src = url; img.src = url;
img.onload = () => { setPreviewImage(img);
hasImagePreviewSetRef.current = true; 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 // Register the tile item as a draggable component
useEffect(() => { useEffect(() => {
if (dragHandleRef.current) { drag(dragHandleRef);
drag(dragHandleRef); }, [drag, dragHandleRef.current]);
}
}, [ready]);
const onClose = useCallback(() => { const onClose = useCallback(() => {
onLeafClose(layoutNode); onLeafClose(layoutNode);
@ -402,19 +422,7 @@ const DisplayNode = <T,>({
onPointerEnter={generatePreviewImage} onPointerEnter={generatePreviewImage}
> >
{leafContent} {leafContent}
<div key="preview" className="tile-preview-container"> {previewElement}
<div
className="tile-preview"
ref={previewRef}
style={{
width: previewWidth,
height: previewHeight,
transform: previewTransform,
}}
>
{previewElement}
</div>
</div>
</div> </div>
); );
}; };

View File

@ -76,6 +76,7 @@
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"throttle-debounce": "^5.0.0", "throttle-debounce": "^5.0.0",
"use-device-pixel-ratio": "^1.1.2",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },
"packageManager": "yarn@4.3.0" "packageManager": "yarn@4.3.0"

View File

@ -12330,6 +12330,7 @@ __metadata:
tsx: "npm:^4.15.4" tsx: "npm:^4.15.4"
typescript: "npm:^5.4.5" typescript: "npm:^5.4.5"
typescript-eslint: "npm:^7.8.0" typescript-eslint: "npm:^7.8.0"
use-device-pixel-ratio: "npm:^1.1.2"
uuid: "npm:^9.0.1" uuid: "npm:^9.0.1"
vite: "npm:^5.0.0" vite: "npm:^5.0.0"
vite-plugin-static-copy: "npm:^1.0.5" vite-plugin-static-copy: "npm:^1.0.5"
@ -12886,6 +12887,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "use-sidecar@npm:^1.1.2":
version: 1.1.2 version: 1.1.2
resolution: "use-sidecar@npm:1.1.2" resolution: "use-sidecar@npm:1.1.2"