mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Fix greedy rendering of drag preview (#68)
This commit is contained in:
parent
b8b03ea817
commit
2c6f6d917f
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>,
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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"
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user