2024-06-04 22:05:44 +02:00
|
|
|
// Copyright 2024, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
import clsx from "clsx";
|
2024-06-19 08:44:53 +02:00
|
|
|
import { toPng } from "html-to-image";
|
2024-08-15 03:40:41 +02:00
|
|
|
import { Atom, useAtomValue, useSetAtom } from "jotai";
|
2024-06-12 02:42:10 +02:00
|
|
|
import React, {
|
2024-06-07 02:58:37 +02:00
|
|
|
CSSProperties,
|
|
|
|
ReactNode,
|
Implement outer drop direction, add rudimentary drag preview image rendering (#29)
This PR adds support for Outer variants of each DropDirection.
When calculating the drop direction, the cursor position is calculated
relevant to the box over which it is hovering. The following diagram
shows how drop directions are calculated. The colored in center is
currently not supported, it is assigned to the top, bottom, left, right
direction for now, though it will ultimately be its own distinct
direction.
![IMG_3505](https://github.com/wavetermdev/thenextwave/assets/16651283/a7ea7387-b95d-4831-9e29-d3225b824c97)
When an outer drop direction is provided for a move operation, if the
reference node flexes in the same axis as the drop direction, the new
node will be inserted at the same level as the parent of the reference
node. If the reference node flexes in a different direction or the
reference node does not have a grandparent, the operation will fall back
to its non-Outer variant.
This also removes some chatty debug statements, adds a blur to the
currently-dragging node to indicate that it cannot be dropped onto, and
simplifies the deriving of the layout state atom from the tab atom so
there's no longer another intermediate derived atom for the layout node.
This also adds rudimentary support for rendering custom preview images
for any tile being dragged. Right now, this is a simple block containing
the block ID, but this can be anything. This resolves an issue where
letting React-DnD generate its own previews could take up to a half
second, and would block dragging until complete. For Monaco, this was
outright failing.
It also fixes an issue where the tile layout could animate on first
paint. Now, I use React Suspense to prevent the layout from displaying
until all the children have loaded.
2024-06-11 22:03:41 +02:00
|
|
|
Suspense,
|
2024-07-03 23:31:02 +02:00
|
|
|
memo,
|
2024-06-07 02:58:37 +02:00
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useMemo,
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from "react";
|
2024-07-31 21:49:38 +02:00
|
|
|
import { DropTargetMonitor, XYCoord, useDrag, useDragLayer, useDrop } from "react-dnd";
|
2024-06-19 20:15:14 +02:00
|
|
|
import { debounce, throttle } from "throttle-debounce";
|
2024-06-21 19:18:35 +02:00
|
|
|
import { useDevicePixelRatio } from "use-device-pixel-ratio";
|
2024-08-15 04:43:25 +02:00
|
|
|
import { LayoutModel } from "./layoutModel";
|
2024-08-15 03:40:41 +02:00
|
|
|
import { useLayoutNode, useTileLayout } from "./layoutModelHooks";
|
2024-06-04 22:05:44 +02:00
|
|
|
import "./tilelayout.less";
|
2024-08-15 04:43:25 +02:00
|
|
|
import {
|
|
|
|
LayoutNode,
|
|
|
|
LayoutTreeActionType,
|
|
|
|
LayoutTreeComputeMoveNodeAction,
|
|
|
|
ResizeHandleProps,
|
|
|
|
TileLayoutContents,
|
|
|
|
} from "./types";
|
2024-08-15 03:40:41 +02:00
|
|
|
import { determineDropDirection } from "./utils";
|
2024-06-04 22:05:44 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
export interface TileLayoutProps {
|
2024-06-26 18:31:43 +02:00
|
|
|
/**
|
|
|
|
* The atom containing the layout tree state.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
tabAtom: Atom<Tab>;
|
2024-06-26 18:31:43 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* callbacks and information about the contents (or styling) of the TileLayout or contents
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
contents: TileLayoutContents;
|
2024-06-21 21:32:38 +02:00
|
|
|
|
2024-06-21 19:18:35 +02:00
|
|
|
/**
|
|
|
|
* 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;
|
2024-06-04 22:05:44 +02:00
|
|
|
}
|
|
|
|
|
2024-06-19 08:44:53 +02:00
|
|
|
const DragPreviewWidth = 300;
|
|
|
|
const DragPreviewHeight = 300;
|
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutProps) {
|
|
|
|
const layoutModel = useTileLayout(tabAtom, contents);
|
|
|
|
const generation = useAtomValue(layoutModel.generationAtom);
|
|
|
|
const overlayTransform = useAtomValue(layoutModel.overlayTransform);
|
|
|
|
const setActiveDrag = useSetAtom(layoutModel.activeDrag);
|
|
|
|
const setReady = useSetAtom(layoutModel.ready);
|
|
|
|
const isResizing = useAtomValue(layoutModel.isResizing);
|
2024-06-04 22:05:44 +02:00
|
|
|
|
2024-06-19 20:15:14 +02:00
|
|
|
const { activeDrag, dragClientOffset } = useDragLayer((monitor) => ({
|
|
|
|
activeDrag: monitor.isDragging(),
|
|
|
|
dragClientOffset: monitor.getClientOffset(),
|
|
|
|
}));
|
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
useEffect(() => {
|
|
|
|
setActiveDrag(activeDrag);
|
|
|
|
}, [setActiveDrag, activeDrag]);
|
|
|
|
|
2024-07-31 21:49:38 +02:00
|
|
|
const checkForCursorBounds = useCallback(
|
|
|
|
debounce(100, (dragClientOffset: XYCoord) => {
|
|
|
|
const cursorPoint = dragClientOffset ?? getCursorPoint?.();
|
2024-08-15 03:40:41 +02:00
|
|
|
if (cursorPoint && layoutModel.displayContainerRef?.current) {
|
|
|
|
const displayContainerRect = layoutModel.displayContainerRef.current.getBoundingClientRect();
|
2024-06-19 20:15:14 +02:00
|
|
|
const normalizedX = cursorPoint.x - displayContainerRect.x;
|
|
|
|
const normalizedY = cursorPoint.y - displayContainerRect.y;
|
|
|
|
if (
|
|
|
|
normalizedX <= 0 ||
|
|
|
|
normalizedX >= displayContainerRect.width ||
|
|
|
|
normalizedY <= 0 ||
|
|
|
|
normalizedY >= displayContainerRect.height
|
|
|
|
) {
|
2024-08-15 03:40:41 +02:00
|
|
|
layoutModel.treeReducer({ type: LayoutTreeActionType.ClearPendingAction });
|
2024-06-19 20:15:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}),
|
2024-08-15 03:40:41 +02:00
|
|
|
[getCursorPoint, generation]
|
2024-06-19 20:15:14 +02:00
|
|
|
);
|
2024-06-04 22:05:44 +02:00
|
|
|
|
2024-07-31 21:49:38 +02:00
|
|
|
// 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(() => checkForCursorBounds(dragClientOffset), [dragClientOffset]);
|
|
|
|
|
2024-06-04 22:05:44 +02:00
|
|
|
// 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);
|
|
|
|
useEffect(() => {
|
2024-08-15 03:40:41 +02:00
|
|
|
setReady(false);
|
2024-06-04 22:05:44 +02:00
|
|
|
setTimeout(() => {
|
|
|
|
setAnimate(true);
|
2024-08-15 03:40:41 +02:00
|
|
|
setReady(true);
|
2024-06-04 22:05:44 +02:00
|
|
|
}, 50);
|
|
|
|
}, []);
|
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
const tileStyle = useMemo(
|
|
|
|
() => ({ "--gap-size-px": `${layoutModel.gapSizePx}px` }) as CSSProperties,
|
|
|
|
[layoutModel.gapSizePx]
|
2024-07-30 19:59:53 +02:00
|
|
|
);
|
|
|
|
|
2024-06-04 22:05:44 +02:00
|
|
|
return (
|
Implement outer drop direction, add rudimentary drag preview image rendering (#29)
This PR adds support for Outer variants of each DropDirection.
When calculating the drop direction, the cursor position is calculated
relevant to the box over which it is hovering. The following diagram
shows how drop directions are calculated. The colored in center is
currently not supported, it is assigned to the top, bottom, left, right
direction for now, though it will ultimately be its own distinct
direction.
![IMG_3505](https://github.com/wavetermdev/thenextwave/assets/16651283/a7ea7387-b95d-4831-9e29-d3225b824c97)
When an outer drop direction is provided for a move operation, if the
reference node flexes in the same axis as the drop direction, the new
node will be inserted at the same level as the parent of the reference
node. If the reference node flexes in a different direction or the
reference node does not have a grandparent, the operation will fall back
to its non-Outer variant.
This also removes some chatty debug statements, adds a blur to the
currently-dragging node to indicate that it cannot be dropped onto, and
simplifies the deriving of the layout state atom from the tab atom so
there's no longer another intermediate derived atom for the layout node.
This also adds rudimentary support for rendering custom preview images
for any tile being dragged. Right now, this is a simple block containing
the block ID, but this can be anything. This resolves an issue where
letting React-DnD generate its own previews could take up to a half
second, and would block dragging until complete. For Monaco, this was
outright failing.
It also fixes an issue where the tile layout could animate on first
paint. Now, I use React Suspense to prevent the layout from displaying
until all the children have loaded.
2024-06-11 22:03:41 +02:00
|
|
|
<Suspense>
|
2024-08-15 03:40:41 +02:00
|
|
|
<div
|
|
|
|
className={clsx("tile-layout", contents.className, { animate: animate && !isResizing })}
|
|
|
|
style={tileStyle}
|
|
|
|
>
|
|
|
|
<div key="display" ref={layoutModel.displayContainerRef} className="display-container">
|
|
|
|
<ResizeHandleWrapper layoutModel={layoutModel} />
|
2024-08-17 09:02:08 +02:00
|
|
|
<DisplayNodesWrapper contents={contents} layoutModel={layoutModel} />
|
Implement outer drop direction, add rudimentary drag preview image rendering (#29)
This PR adds support for Outer variants of each DropDirection.
When calculating the drop direction, the cursor position is calculated
relevant to the box over which it is hovering. The following diagram
shows how drop directions are calculated. The colored in center is
currently not supported, it is assigned to the top, bottom, left, right
direction for now, though it will ultimately be its own distinct
direction.
![IMG_3505](https://github.com/wavetermdev/thenextwave/assets/16651283/a7ea7387-b95d-4831-9e29-d3225b824c97)
When an outer drop direction is provided for a move operation, if the
reference node flexes in the same axis as the drop direction, the new
node will be inserted at the same level as the parent of the reference
node. If the reference node flexes in a different direction or the
reference node does not have a grandparent, the operation will fall back
to its non-Outer variant.
This also removes some chatty debug statements, adds a blur to the
currently-dragging node to indicate that it cannot be dropped onto, and
simplifies the deriving of the layout state atom from the tab atom so
there's no longer another intermediate derived atom for the layout node.
This also adds rudimentary support for rendering custom preview images
for any tile being dragged. Right now, this is a simple block containing
the block ID, but this can be anything. This resolves an issue where
letting React-DnD generate its own previews could take up to a half
second, and would block dragging until complete. For Monaco, this was
outright failing.
It also fixes an issue where the tile layout could animate on first
paint. Now, I use React Suspense to prevent the layout from displaying
until all the children have loaded.
2024-06-11 22:03:41 +02:00
|
|
|
</div>
|
2024-08-15 03:40:41 +02:00
|
|
|
<Placeholder key="placeholder" layoutModel={layoutModel} style={{ top: 10000, ...overlayTransform }} />
|
|
|
|
<OverlayNodeWrapper layoutModel={layoutModel} />
|
2024-06-04 22:05:44 +02:00
|
|
|
</div>
|
Implement outer drop direction, add rudimentary drag preview image rendering (#29)
This PR adds support for Outer variants of each DropDirection.
When calculating the drop direction, the cursor position is calculated
relevant to the box over which it is hovering. The following diagram
shows how drop directions are calculated. The colored in center is
currently not supported, it is assigned to the top, bottom, left, right
direction for now, though it will ultimately be its own distinct
direction.
![IMG_3505](https://github.com/wavetermdev/thenextwave/assets/16651283/a7ea7387-b95d-4831-9e29-d3225b824c97)
When an outer drop direction is provided for a move operation, if the
reference node flexes in the same axis as the drop direction, the new
node will be inserted at the same level as the parent of the reference
node. If the reference node flexes in a different direction or the
reference node does not have a grandparent, the operation will fall back
to its non-Outer variant.
This also removes some chatty debug statements, adds a blur to the
currently-dragging node to indicate that it cannot be dropped onto, and
simplifies the deriving of the layout state atom from the tab atom so
there's no longer another intermediate derived atom for the layout node.
This also adds rudimentary support for rendering custom preview images
for any tile being dragged. Right now, this is a simple block containing
the block ID, but this can be anything. This resolves an issue where
letting React-DnD generate its own previews could take up to a half
second, and would block dragging until complete. For Monaco, this was
outright failing.
It also fixes an issue where the tile layout could animate on first
paint. Now, I use React Suspense to prevent the layout from displaying
until all the children have loaded.
2024-06-11 22:03:41 +02:00
|
|
|
</Suspense>
|
2024-06-04 22:05:44 +02:00
|
|
|
);
|
2024-06-26 21:22:27 +02:00
|
|
|
}
|
|
|
|
|
2024-07-03 23:31:02 +02:00
|
|
|
export const TileLayout = memo(TileLayoutComponent) as typeof TileLayoutComponent;
|
2024-06-26 18:31:43 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
interface DisplayNodesWrapperProps {
|
2024-06-26 18:31:43 +02:00
|
|
|
/**
|
|
|
|
* The layout tree state.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
layoutModel: LayoutModel;
|
2024-06-26 18:31:43 +02:00
|
|
|
/**
|
|
|
|
* contains callbacks and information about the contents (or styling) of of the TileLayout
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
contents: TileLayoutContents;
|
2024-06-26 18:31:43 +02:00
|
|
|
}
|
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
const DisplayNodesWrapper = ({ layoutModel, contents }: DisplayNodesWrapperProps) => {
|
2024-08-22 02:43:11 +02:00
|
|
|
const leafs = useAtomValue(layoutModel.leafs);
|
2024-08-15 03:40:41 +02:00
|
|
|
|
|
|
|
return useMemo(
|
|
|
|
() =>
|
2024-08-22 02:43:11 +02:00
|
|
|
leafs.map((leaf) => {
|
2024-08-15 21:24:06 +02:00
|
|
|
return <DisplayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} contents={contents} />;
|
2024-08-15 03:40:41 +02:00
|
|
|
}),
|
2024-08-22 02:43:11 +02:00
|
|
|
[leafs]
|
2024-08-15 03:40:41 +02:00
|
|
|
);
|
|
|
|
};
|
2024-06-04 22:05:44 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
interface DisplayNodeProps {
|
|
|
|
layoutModel: LayoutModel;
|
2024-06-14 04:33:06 +02:00
|
|
|
/**
|
|
|
|
* The leaf node object, containing the data needed to display the leaf contents to the user.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
layoutNode: LayoutNode;
|
2024-06-26 18:31:43 +02:00
|
|
|
|
2024-06-14 04:33:06 +02:00
|
|
|
/**
|
2024-06-26 18:31:43 +02:00
|
|
|
* contains callbacks and information about the contents (or styling) of of the TileLayout
|
2024-06-14 04:33:06 +02:00
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
contents: TileLayoutContents;
|
2024-06-04 22:05:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const dragItemType = "TILE_ITEM";
|
|
|
|
|
2024-06-14 04:33:06 +02:00
|
|
|
/**
|
2024-06-14 04:36:06 +02:00
|
|
|
* The draggable and displayable portion of a leaf node in a layout tree.
|
2024-06-14 04:33:06 +02:00
|
|
|
*/
|
2024-08-15 21:24:06 +02:00
|
|
|
const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) => {
|
2024-08-15 03:40:41 +02:00
|
|
|
const tileNodeRef = useRef<HTMLDivElement>(null);
|
|
|
|
const dragHandleRef = useRef<HTMLDivElement>(null);
|
|
|
|
const previewRef = useRef<HTMLDivElement>(null);
|
|
|
|
const addlProps = useLayoutNode(layoutModel, layoutNode);
|
|
|
|
const activeDrag = useAtomValue(layoutModel.activeDrag);
|
|
|
|
const globalReady = useAtomValue(layoutModel.ready);
|
|
|
|
|
|
|
|
const devicePixelRatio = useDevicePixelRatio();
|
|
|
|
|
|
|
|
const [{ isDragging }, drag, dragPreview] = useDrag(
|
|
|
|
() => ({
|
|
|
|
type: dragItemType,
|
|
|
|
item: () => layoutNode,
|
2024-08-21 05:14:14 +02:00
|
|
|
canDrag: () => !addlProps?.isMagnifiedNode,
|
2024-08-15 03:40:41 +02:00
|
|
|
collect: (monitor) => ({
|
|
|
|
isDragging: monitor.isDragging(),
|
2024-06-04 22:05:44 +02:00
|
|
|
}),
|
2024-08-15 03:40:41 +02:00
|
|
|
}),
|
2024-08-21 05:14:14 +02:00
|
|
|
[layoutNode, addlProps]
|
2024-08-15 03:40:41 +02:00
|
|
|
);
|
2024-06-04 22:05:44 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
const [previewElementGeneration, setPreviewElementGeneration] = useState(0);
|
|
|
|
const previewElement = useMemo(() => {
|
|
|
|
setPreviewElementGeneration(previewElementGeneration + 1);
|
|
|
|
return (
|
|
|
|
<div key="preview" className="tile-preview-container">
|
|
|
|
<div
|
|
|
|
className="tile-preview"
|
|
|
|
ref={previewRef}
|
|
|
|
style={{
|
|
|
|
width: DragPreviewWidth,
|
|
|
|
height: DragPreviewHeight,
|
|
|
|
transform: `scale(${1 / devicePixelRatio})`,
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{contents.renderPreview?.(layoutNode.data)}
|
2024-07-30 19:59:53 +02:00
|
|
|
</div>
|
2024-08-15 03:40:41 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}, [contents.renderPreview, devicePixelRatio, layoutNode.data]);
|
|
|
|
|
|
|
|
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
|
|
|
|
const [previewImageGeneration, setPreviewImageGeneration] = useState(0);
|
|
|
|
const generatePreviewImage = useCallback(() => {
|
|
|
|
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;
|
|
|
|
setPreviewImage(img);
|
|
|
|
dragPreview(img, { offsetY, offsetX });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, [
|
|
|
|
dragPreview,
|
|
|
|
previewRef.current,
|
|
|
|
previewElementGeneration,
|
|
|
|
previewImageGeneration,
|
|
|
|
previewImage,
|
|
|
|
devicePixelRatio,
|
|
|
|
layoutNode.data,
|
|
|
|
]);
|
2024-07-30 19:59:53 +02:00
|
|
|
|
2024-08-15 23:53:13 +02:00
|
|
|
// Register the display node as a draggable item
|
2024-08-15 03:40:41 +02:00
|
|
|
useEffect(() => {
|
|
|
|
drag(dragHandleRef);
|
|
|
|
}, [drag, dragHandleRef.current]);
|
|
|
|
|
|
|
|
const leafContent = useMemo(() => {
|
|
|
|
return (
|
|
|
|
layoutNode.data && (
|
|
|
|
<div key="leaf" className="tile-leaf">
|
|
|
|
{contents.renderContent(
|
|
|
|
layoutNode.data,
|
|
|
|
globalReady,
|
2024-08-21 05:14:14 +02:00
|
|
|
addlProps?.isMagnifiedNode ?? false,
|
2024-08-15 03:40:41 +02:00
|
|
|
activeDrag,
|
2024-08-15 21:24:06 +02:00
|
|
|
() => layoutModel.magnifyNodeToggle(layoutNode),
|
2024-08-15 03:40:41 +02:00
|
|
|
() => layoutModel.closeNode(layoutNode),
|
|
|
|
dragHandleRef
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
);
|
2024-08-21 05:14:14 +02:00
|
|
|
}, [layoutNode, globalReady, activeDrag, addlProps]);
|
2024-07-30 19:59:53 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
return (
|
|
|
|
<div
|
2024-08-15 21:24:06 +02:00
|
|
|
className={clsx("tile-node", {
|
|
|
|
dragging: isDragging,
|
2024-08-21 05:14:14 +02:00
|
|
|
magnified: addlProps?.isMagnifiedNode,
|
2024-08-15 22:45:45 +02:00
|
|
|
"last-magnified": addlProps?.isLastMagnifiedNode,
|
2024-08-15 21:24:06 +02:00
|
|
|
})}
|
2024-08-15 03:40:41 +02:00
|
|
|
ref={tileNodeRef}
|
|
|
|
id={layoutNode.id}
|
|
|
|
style={addlProps?.transform}
|
|
|
|
onPointerEnter={generatePreviewImage}
|
|
|
|
onPointerOver={(event) => event.stopPropagation()}
|
|
|
|
>
|
|
|
|
{leafContent}
|
|
|
|
{previewElement}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2024-07-30 19:59:53 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
interface OverlayNodeWrapperProps {
|
|
|
|
layoutModel: LayoutModel;
|
|
|
|
}
|
2024-07-30 19:59:53 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
const OverlayNodeWrapper = ({ layoutModel }: OverlayNodeWrapperProps) => {
|
2024-08-22 02:43:11 +02:00
|
|
|
const leafs = useAtomValue(layoutModel.leafs);
|
2024-08-15 03:40:41 +02:00
|
|
|
const overlayTransform = useAtomValue(layoutModel.overlayTransform);
|
2024-07-30 19:59:53 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
const overlayNodes = useMemo(
|
|
|
|
() =>
|
2024-08-22 02:43:11 +02:00
|
|
|
leafs.map((leaf) => {
|
2024-08-15 03:40:41 +02:00
|
|
|
return <OverlayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} />;
|
|
|
|
}),
|
2024-08-22 02:43:11 +02:00
|
|
|
[leafs]
|
2024-08-15 03:40:41 +02:00
|
|
|
);
|
2024-06-04 22:05:44 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
return (
|
|
|
|
<div key="overlay" className="overlay-container" style={{ top: 10000, ...overlayTransform }}>
|
|
|
|
{overlayNodes}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2024-06-04 22:05:44 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
interface OverlayNodeProps {
|
2024-06-14 04:33:06 +02:00
|
|
|
/**
|
|
|
|
* The layout node object corresponding to the OverlayNode.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
layoutNode: LayoutNode;
|
2024-06-14 04:33:06 +02:00
|
|
|
/**
|
|
|
|
* The layout tree state.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
layoutModel: LayoutModel;
|
2024-06-04 22:05:44 +02:00
|
|
|
}
|
|
|
|
|
2024-06-14 04:33:06 +02:00
|
|
|
/**
|
|
|
|
* An overlay representing the true flexbox layout of the LayoutTreeState. This holds the drop targets for moving around nodes and is used to calculate the
|
2024-06-14 04:36:06 +02:00
|
|
|
* dimensions of the corresponding DisplayNode for each LayoutTreeState leaf.
|
2024-06-14 04:33:06 +02:00
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
|
|
|
|
const additionalProps = useLayoutNode(layoutModel, layoutNode);
|
2024-06-04 22:05:44 +02:00
|
|
|
const overlayRef = useRef<HTMLDivElement>(null);
|
2024-08-15 03:40:41 +02:00
|
|
|
const generation = useAtomValue(layoutModel.generationAtom);
|
2024-07-03 23:31:02 +02:00
|
|
|
|
2024-06-04 22:05:44 +02:00
|
|
|
const [, drop] = useDrop(
|
|
|
|
() => ({
|
|
|
|
accept: dragItemType,
|
|
|
|
canDrop: (_, monitor) => {
|
2024-08-15 03:40:41 +02:00
|
|
|
const dragItem = monitor.getItem<LayoutNode>();
|
2024-06-04 22:05:44 +02:00
|
|
|
if (monitor.isOver({ shallow: true }) && dragItem?.id !== layoutNode.id) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
drop: (_, monitor) => {
|
2024-08-15 23:53:13 +02:00
|
|
|
if (!monitor.didDrop()) {
|
|
|
|
layoutModel.onDrop();
|
2024-06-04 22:05:44 +02:00
|
|
|
}
|
|
|
|
},
|
2024-08-15 23:53:13 +02:00
|
|
|
hover: throttle(50, (_, monitor: DropTargetMonitor<unknown, unknown>) => {
|
2024-06-19 01:03:00 +02:00
|
|
|
if (monitor.isOver({ shallow: true })) {
|
2024-08-15 23:53:13 +02:00
|
|
|
if (monitor.canDrop() && layoutModel.displayContainerRef?.current && additionalProps?.rect) {
|
2024-08-15 03:40:41 +02:00
|
|
|
const dragItem = monitor.getItem<LayoutNode>();
|
|
|
|
// console.log("computing operation", layoutNode, dragItem, additionalProps.rect);
|
|
|
|
const offset = monitor.getClientOffset();
|
2024-08-15 23:53:13 +02:00
|
|
|
const containerRect = layoutModel.displayContainerRef.current.getBoundingClientRect();
|
|
|
|
offset.x -= containerRect.x;
|
|
|
|
offset.y -= containerRect.y;
|
2024-08-15 03:40:41 +02:00
|
|
|
layoutModel.treeReducer({
|
2024-06-19 01:03:00 +02:00
|
|
|
type: LayoutTreeActionType.ComputeMove,
|
|
|
|
node: layoutNode,
|
|
|
|
nodeToMove: dragItem,
|
2024-08-15 23:53:13 +02:00
|
|
|
direction: determineDropDirection(additionalProps.rect, offset),
|
2024-08-15 03:40:41 +02:00
|
|
|
} as LayoutTreeComputeMoveNodeAction);
|
2024-06-19 01:03:00 +02:00
|
|
|
} else {
|
2024-08-15 03:40:41 +02:00
|
|
|
layoutModel.treeReducer({
|
2024-06-19 01:03:00 +02:00
|
|
|
type: LayoutTreeActionType.ClearPendingAction,
|
|
|
|
});
|
|
|
|
}
|
2024-06-04 22:05:44 +02:00
|
|
|
}
|
2024-06-19 20:15:14 +02:00
|
|
|
}),
|
2024-06-04 22:05:44 +02:00
|
|
|
}),
|
2024-08-15 23:53:13 +02:00
|
|
|
[layoutNode, generation, additionalProps, layoutModel.displayContainerRef]
|
2024-06-04 22:05:44 +02:00
|
|
|
);
|
|
|
|
|
2024-08-15 23:53:13 +02:00
|
|
|
// Register the overlay node as a drop target
|
2024-06-04 22:05:44 +02:00
|
|
|
useEffect(() => {
|
2024-08-15 03:40:41 +02:00
|
|
|
drop(overlayRef);
|
|
|
|
}, []);
|
2024-07-03 23:31:02 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
return <div ref={overlayRef} className="overlay-node" id={layoutNode.id} style={additionalProps?.transform} />;
|
|
|
|
};
|
2024-06-04 22:05:44 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
interface ResizeHandleWrapperProps {
|
|
|
|
layoutModel: LayoutModel;
|
|
|
|
}
|
2024-06-04 22:05:44 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
const ResizeHandleWrapper = ({ layoutModel }: ResizeHandleWrapperProps) => {
|
|
|
|
const resizeHandles = useAtomValue(layoutModel.resizeHandles) as Atom<ResizeHandleProps>[];
|
2024-07-03 23:31:02 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
return resizeHandles.map((resizeHandleAtom, i) => (
|
|
|
|
<ResizeHandle key={`resize-handle-${i}`} layoutModel={layoutModel} resizeHandleAtom={resizeHandleAtom} />
|
|
|
|
));
|
2024-06-04 22:05:44 +02:00
|
|
|
};
|
2024-06-07 02:58:37 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
interface ResizeHandleComponentProps {
|
|
|
|
resizeHandleAtom: Atom<ResizeHandleProps>;
|
|
|
|
layoutModel: LayoutModel;
|
2024-07-03 23:31:02 +02:00
|
|
|
}
|
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
const ResizeHandle = ({ resizeHandleAtom, layoutModel }: ResizeHandleComponentProps) => {
|
|
|
|
const resizeHandleProps = useAtomValue(resizeHandleAtom);
|
2024-07-03 23:31:02 +02:00
|
|
|
const resizeHandleRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
// The pointer currently captured, or undefined.
|
|
|
|
const [trackingPointer, setTrackingPointer] = useState<number>(undefined);
|
|
|
|
|
|
|
|
// Calculates the new size of the two nodes on either side of the handle, based on the position of the cursor
|
|
|
|
const handlePointerMove = useCallback(
|
2024-07-04 00:31:39 +02:00
|
|
|
throttle(10, (event: React.PointerEvent<HTMLDivElement>) => {
|
|
|
|
if (trackingPointer === event.pointerId) {
|
|
|
|
const { clientX, clientY } = event;
|
2024-08-15 03:40:41 +02:00
|
|
|
layoutModel.onResizeMove(resizeHandleProps, clientX, clientY);
|
2024-07-03 23:31:02 +02:00
|
|
|
}
|
2024-07-04 00:31:39 +02:00
|
|
|
}),
|
2024-08-15 03:40:41 +02:00
|
|
|
[trackingPointer, layoutModel.onResizeMove, resizeHandleProps]
|
2024-07-03 23:31:02 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
// We want to use pointer capture so the operation continues even if the pointer leaves the bounds of the handle
|
|
|
|
function onPointerDown(event: React.PointerEvent<HTMLDivElement>) {
|
|
|
|
resizeHandleRef.current?.setPointerCapture(event.pointerId);
|
|
|
|
}
|
|
|
|
|
|
|
|
// This indicates that we're ready to start tracking the resize operation via the pointer
|
|
|
|
function onPointerCapture(event: React.PointerEvent<HTMLDivElement>) {
|
|
|
|
setTrackingPointer(event.pointerId);
|
|
|
|
}
|
|
|
|
|
|
|
|
// We want to wait a bit before committing the pending resize operation in case some events haven't arrived yet.
|
|
|
|
const onPointerRelease = useCallback(
|
|
|
|
debounce(30, (event: React.PointerEvent<HTMLDivElement>) => {
|
|
|
|
setTrackingPointer(undefined);
|
2024-08-15 03:40:41 +02:00
|
|
|
layoutModel.onResizeEnd();
|
2024-07-03 23:31:02 +02:00
|
|
|
}),
|
2024-08-15 03:40:41 +02:00
|
|
|
[layoutModel]
|
2024-07-03 23:31:02 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
ref={resizeHandleRef}
|
2024-08-15 03:40:41 +02:00
|
|
|
className={clsx("resize-handle", `flex-${resizeHandleProps.flexDirection}`)}
|
2024-07-03 23:31:02 +02:00
|
|
|
onPointerDown={onPointerDown}
|
|
|
|
onGotPointerCapture={onPointerCapture}
|
|
|
|
onLostPointerCapture={onPointerRelease}
|
2024-08-15 03:40:41 +02:00
|
|
|
style={resizeHandleProps.transform}
|
2024-07-04 00:31:39 +02:00
|
|
|
onPointerMove={handlePointerMove}
|
2024-07-03 23:31:02 +02:00
|
|
|
>
|
|
|
|
<div className="line" />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
interface PlaceholderProps {
|
2024-06-14 04:33:06 +02:00
|
|
|
/**
|
|
|
|
* The layout tree state.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
layoutModel: LayoutModel;
|
2024-06-14 04:33:06 +02:00
|
|
|
/**
|
|
|
|
* Any styling to apply to the placeholder container div.
|
|
|
|
*/
|
2024-06-07 02:58:37 +02:00
|
|
|
style: React.CSSProperties;
|
|
|
|
}
|
|
|
|
|
2024-06-14 04:33:06 +02:00
|
|
|
/**
|
|
|
|
* An overlay to preview pending actions on the layout tree.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
const Placeholder = memo(({ layoutModel, style }: PlaceholderProps) => {
|
2024-06-07 02:58:37 +02:00
|
|
|
const [placeholderOverlay, setPlaceholderOverlay] = useState<ReactNode>(null);
|
2024-08-15 03:40:41 +02:00
|
|
|
const placeholderTransform = useAtomValue(layoutModel.placeholderTransform);
|
2024-07-31 21:49:38 +02:00
|
|
|
|
|
|
|
useEffect(() => {
|
2024-08-15 03:40:41 +02:00
|
|
|
if (placeholderTransform) {
|
|
|
|
setPlaceholderOverlay(<div className="placeholder" style={placeholderTransform} />);
|
|
|
|
} else {
|
|
|
|
setPlaceholderOverlay(null);
|
|
|
|
}
|
|
|
|
}, [placeholderTransform]);
|
2024-06-07 02:58:37 +02:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="placeholder-container" style={style}>
|
|
|
|
{placeholderOverlay}
|
|
|
|
</div>
|
|
|
|
);
|
2024-07-31 21:49:38 +02:00
|
|
|
});
|