mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
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.
This commit is contained in:
parent
45a9a95e38
commit
0a45311f30
@ -11,7 +11,17 @@
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
|
||||
.block-header {
|
||||
.block-content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.block-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
@ -22,7 +32,8 @@
|
||||
background-color: var(--panel-bg-color);
|
||||
|
||||
.block-header-text {
|
||||
padding-left: 5px;
|
||||
padding: 0 5px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@ -32,14 +43,4 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.block-content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,26 @@ import "./block.less";
|
||||
|
||||
interface BlockProps {
|
||||
blockId: string;
|
||||
onClose: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const BlockHeader = ({ blockId, onClose }: BlockProps) => {
|
||||
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
|
||||
return (
|
||||
<div key="header" className="block-header">
|
||||
<div className="block-header-text text-fixed">
|
||||
Block [{blockId.substring(0, 8)}] {blockData.view}
|
||||
</div>
|
||||
{onClose && (
|
||||
<div className="close-button" onClick={onClose}>
|
||||
<i className="fa fa-solid fa-xmark-large" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Block = ({ blockId, onClose }: BlockProps) => {
|
||||
const blockRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -36,15 +53,7 @@ const Block = ({ blockId, onClose }: BlockProps) => {
|
||||
}
|
||||
return (
|
||||
<div className="block" ref={blockRef}>
|
||||
<div key="header" className="block-header">
|
||||
<div className="block-header-text text-fixed">
|
||||
Block [{blockId.substring(0, 8)}] {blockData.view}
|
||||
</div>
|
||||
<div className="flex-spacer" />
|
||||
<div className="close-button" onClick={onClose}>
|
||||
<i className="fa fa-solid fa-xmark-large" />
|
||||
</div>
|
||||
</div>
|
||||
<BlockHeader blockId={blockId} onClose={onClose} />
|
||||
<div key="content" className="block-content">
|
||||
<ErrorBoundary>
|
||||
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{blockElem}</React.Suspense>
|
||||
@ -54,4 +63,4 @@ const Block = ({ blockId, onClose }: BlockProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { Block };
|
||||
export { Block, BlockHeader };
|
||||
|
@ -280,13 +280,11 @@ function wrapObjectServiceCall<T>(fnName: string, ...args: any[]): Promise<T> {
|
||||
// should provide getFn if it is available (e.g. inside of a jotai atom)
|
||||
// otherwise it will use the globalStore.get function
|
||||
function getObjectValue<T>(oref: string, getFn?: jotai.Getter): T {
|
||||
console.log("getObjectValue", oref);
|
||||
let wov = waveObjectValueCache.get(oref);
|
||||
if (wov == null) {
|
||||
return null;
|
||||
}
|
||||
if (getFn == null) {
|
||||
console.log("getObjectValue", "getFn is null, using globalStore.get");
|
||||
getFn = globalStore.get;
|
||||
}
|
||||
const atomVal = getFn(wov.dataAtom);
|
||||
@ -303,14 +301,10 @@ function setObjectValue<T extends WaveObj>(value: T, setFn?: jotai.Setter, pushT
|
||||
return;
|
||||
}
|
||||
if (setFn == null) {
|
||||
console.log("setter null");
|
||||
setFn = globalStore.set;
|
||||
}
|
||||
console.log("Setting", oref, "to", value);
|
||||
setFn(wov.dataAtom, { value: value, loading: false });
|
||||
console.log("Setting", oref, "to", value, "done");
|
||||
if (pushToServer) {
|
||||
console.log("pushToServer", oref, value);
|
||||
UpdateObject(value, false);
|
||||
}
|
||||
}
|
||||
@ -340,7 +334,6 @@ export function UpdateObjectMeta(blockId: string, meta: MetadataType): Promise<v
|
||||
}
|
||||
|
||||
export function UpdateObject(waveObj: WaveObj, returnUpdates: boolean): Promise<WaveObjUpdate[]> {
|
||||
console.log("UpdateObject", waveObj, returnUpdates);
|
||||
return wrapObjectServiceCall("UpdateObject", waveObj, returnUpdates);
|
||||
}
|
||||
export {
|
||||
|
@ -21,3 +21,13 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-preview {
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
background-color: aquamarine;
|
||||
color: black;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Block } from "@/app/block/block";
|
||||
import { Block, BlockHeader } from "@/app/block/block";
|
||||
import * as WOS from "@/store/wos";
|
||||
|
||||
import { TileLayout } from "@/faraday/index";
|
||||
@ -27,6 +27,11 @@ const TabContent = ({ tabId }: { tabId: string }) => {
|
||||
return <Block blockId={tabData.blockId} onClose={onClose} />;
|
||||
}, []);
|
||||
|
||||
const renderPreview = useCallback((tabData: TabLayoutData) => {
|
||||
console.log("renderPreview", tabData);
|
||||
return <BlockHeader blockId={tabData.blockId} />;
|
||||
}, []);
|
||||
|
||||
const onNodeDelete = useCallback((data: TabLayoutData) => {
|
||||
console.log("onNodeDelete", data);
|
||||
return WOS.DeleteBlock(data.blockId);
|
||||
@ -49,6 +54,7 @@ const TabContent = ({ tabId }: { tabId: string }) => {
|
||||
<TileLayout
|
||||
key={tabId}
|
||||
renderContent={renderBlock}
|
||||
renderPreview={renderPreview}
|
||||
layoutTreeStateAtom={layoutStateAtom}
|
||||
onNodeDelete={onNodeDelete}
|
||||
/>
|
||||
|
@ -63,11 +63,12 @@ function TabBar({ workspace }: { workspace: Workspace }) {
|
||||
function Widgets() {
|
||||
const windowData = jotai.useAtomValue(atoms.waveWindow);
|
||||
const activeTabAtom = useMemo(() => {
|
||||
return WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", windowData.activetabid));
|
||||
}, [windowData.activetabid]);
|
||||
const [, dispatchLayoutStateAction] = useLayoutTreeStateReducerAtom(
|
||||
getLayoutStateAtomForTab(windowData.activetabid, activeTabAtom)
|
||||
return getLayoutStateAtomForTab(
|
||||
windowData.activetabid,
|
||||
WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", windowData.activetabid))
|
||||
);
|
||||
}, [windowData.activetabid]);
|
||||
const [, dispatchLayoutStateAction] = useLayoutTreeStateReducerAtom(activeTabAtom);
|
||||
|
||||
const addBlockToTab = useCallback(
|
||||
(blockId: string) => {
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
CSSProperties,
|
||||
ReactNode,
|
||||
RefObject,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
@ -16,6 +17,7 @@ import {
|
||||
import { useDrag, useDragLayer, useDrop } from "react-dnd";
|
||||
|
||||
import useResizeObserver from "@react-hook/resize-observer";
|
||||
import { toPng } from "html-to-image";
|
||||
import { useLayoutTreeStateReducerAtom } from "./layoutAtom.js";
|
||||
import { findNode } from "./layoutNode.js";
|
||||
import {
|
||||
@ -27,6 +29,7 @@ import {
|
||||
LayoutTreeDeleteNodeAction,
|
||||
LayoutTreeMoveNodeAction,
|
||||
LayoutTreeState,
|
||||
PreviewRenderer,
|
||||
WritableLayoutTreeStateAtom,
|
||||
} from "./model.js";
|
||||
import "./tilelayout.less";
|
||||
@ -35,11 +38,18 @@ import { FlexDirection, setTransform as createTransform, debounce, determineDrop
|
||||
export interface TileLayoutProps<T> {
|
||||
layoutTreeStateAtom: WritableLayoutTreeStateAtom<T>;
|
||||
renderContent: ContentRenderer<T>;
|
||||
renderPreview?: PreviewRenderer<T>;
|
||||
onNodeDelete?: (data: T) => Promise<void>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TileLayout = <T,>({ layoutTreeStateAtom, className, renderContent, onNodeDelete }: TileLayoutProps<T>) => {
|
||||
export const TileLayout = <T,>({
|
||||
layoutTreeStateAtom,
|
||||
className,
|
||||
renderContent,
|
||||
renderPreview,
|
||||
onNodeDelete,
|
||||
}: TileLayoutProps<T>) => {
|
||||
const overlayContainerRef = useRef<HTMLDivElement>(null);
|
||||
const displayContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -54,6 +64,7 @@ export const TileLayout = <T,>({ layoutTreeStateAtom, className, renderContent,
|
||||
const setRef = useCallback(
|
||||
(id: string, ref: RefObject<HTMLDivElement>) => {
|
||||
setNodeRefs((prev) => {
|
||||
// console.log("setRef", id, ref);
|
||||
prev.set(id, ref);
|
||||
return prev;
|
||||
});
|
||||
@ -64,6 +75,7 @@ export const TileLayout = <T,>({ layoutTreeStateAtom, className, renderContent,
|
||||
|
||||
const deleteRef = useCallback(
|
||||
(id: string) => {
|
||||
// console.log("deleteRef", id);
|
||||
if (nodeRefs.has(id)) {
|
||||
setNodeRefs((prev) => {
|
||||
prev.delete(id);
|
||||
@ -105,6 +117,7 @@ export const TileLayout = <T,>({ layoutTreeStateAtom, className, renderContent,
|
||||
|
||||
for (const leaf of layoutTreeState.leafs) {
|
||||
const leafRef = nodeRefs.get(leaf.id);
|
||||
// console.log("current leafRef", leafRef.current);
|
||||
if (leafRef?.current) {
|
||||
const leafBounding = leafRef.current.getBoundingClientRect();
|
||||
const transform = createTransform({
|
||||
@ -148,16 +161,11 @@ export const TileLayout = <T,>({ layoutTreeStateAtom, className, renderContent,
|
||||
|
||||
// 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.
|
||||
// `overlayVisible` will be disabled until after the overlay has been pushed out of view.
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const [overlayVisible, setOverlayVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setAnimate(true);
|
||||
}, 50);
|
||||
setTimeout(() => {
|
||||
setOverlayVisible(true);
|
||||
}, 30);
|
||||
}, []);
|
||||
|
||||
const onLeafClose = useCallback(
|
||||
@ -177,7 +185,8 @@ export const TileLayout = <T,>({ layoutTreeStateAtom, className, renderContent,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx("tile-layout", className, { animate, overlayVisible })}>
|
||||
<Suspense>
|
||||
<div className={clsx("tile-layout", className, { animate })}>
|
||||
<div key="display" ref={displayContainerRef} className="display-container">
|
||||
{layoutLeafTransforms &&
|
||||
layoutTreeState.leafs.map((leaf) => {
|
||||
@ -186,6 +195,7 @@ export const TileLayout = <T,>({ layoutTreeStateAtom, className, renderContent,
|
||||
key={leaf.id}
|
||||
layoutNode={leaf}
|
||||
renderContent={renderContent}
|
||||
renderPreview={renderPreview}
|
||||
transform={layoutLeafTransforms[leaf.id]}
|
||||
onLeafClose={onLeafClose}
|
||||
ready={animate}
|
||||
@ -215,12 +225,14 @@ export const TileLayout = <T,>({ layoutTreeStateAtom, className, renderContent,
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
interface TileNodeProps<T> {
|
||||
layoutNode: LayoutNode<T>;
|
||||
renderContent: ContentRenderer<T>;
|
||||
renderPreview?: PreviewRenderer<T>;
|
||||
onLeafClose: (node: LayoutNode<T>) => void;
|
||||
ready: boolean;
|
||||
transform: CSSProperties;
|
||||
@ -228,9 +240,18 @@ interface TileNodeProps<T> {
|
||||
|
||||
const dragItemType = "TILE_ITEM";
|
||||
|
||||
const TileNode = <T,>({ layoutNode, renderContent, transform, onLeafClose, ready }: TileNodeProps<T>) => {
|
||||
const TileNode = <T,>({
|
||||
layoutNode,
|
||||
renderContent,
|
||||
renderPreview,
|
||||
transform,
|
||||
onLeafClose,
|
||||
ready,
|
||||
}: TileNodeProps<T>) => {
|
||||
const tileNodeRef = useRef<HTMLDivElement>(null);
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Register the node as a draggable item.
|
||||
const [{ isDragging, dragItem }, drag, dragPreview] = useDrag(
|
||||
() => ({
|
||||
type: dragItemType,
|
||||
@ -243,16 +264,47 @@ const TileNode = <T,>({ layoutNode, renderContent, transform, onLeafClose, ready
|
||||
[layoutNode]
|
||||
);
|
||||
|
||||
// TODO: remove debug effect
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
console.log("drag start", layoutNode.id, layoutNode, dragItem);
|
||||
}
|
||||
}, [isDragging]);
|
||||
|
||||
// Generate a preview div using the provided renderPreview function. This will be placed in the DOM so we can render an image from it, but it is pushed out of view so the user will not see it.
|
||||
// No-op if not provided, meaning React-DnD will attempt to generate a preview from the DOM, which is very slow.
|
||||
const preview = useMemo(() => {
|
||||
const previewElement = renderPreview?.(layoutNode.data);
|
||||
console.log("preview", previewElement);
|
||||
return (
|
||||
<div className="tile-preview-container">
|
||||
<div className="tile-preview" ref={previewRef}>
|
||||
{previewElement}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Once the preview div is mounted, grab it and render a PNG, then register it with the DnD system. I noticed that if I call this immediately, it occasionally captures an empty HTMLElement.
|
||||
// I found a hacky workaround of just adding a timeout so the capture doesn't happen until after the first paint.
|
||||
useEffect(
|
||||
debounce(() => {
|
||||
console.log("dragPreview effect");
|
||||
if (previewRef.current) {
|
||||
toPng(previewRef.current).then((url) => {
|
||||
console.log("got preview url", url);
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
dragPreview(img);
|
||||
});
|
||||
}
|
||||
}, 50),
|
||||
[previewRef]
|
||||
);
|
||||
|
||||
// Register the tile item as a draggable component
|
||||
useEffect(() => {
|
||||
drag(tileNodeRef);
|
||||
dragPreview(tileNodeRef);
|
||||
}, [tileNodeRef]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
@ -267,11 +319,11 @@ const TileNode = <T,>({ layoutNode, renderContent, transform, onLeafClose, ready
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}, [, layoutNode.data, ready, onClose]);
|
||||
}, [layoutNode.data, ready, onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tile-node"
|
||||
className={clsx("tile-node", { dragging: isDragging })}
|
||||
ref={tileNodeRef}
|
||||
id={layoutNode.id}
|
||||
style={{
|
||||
@ -281,6 +333,7 @@ const TileNode = <T,>({ layoutNode, renderContent, transform, onLeafClose, ready
|
||||
}}
|
||||
>
|
||||
{leafContent}
|
||||
{preview}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -424,7 +477,7 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefs, style
|
||||
const overlayBoundingRect = overlayContainerRef.current.getBoundingClientRect();
|
||||
const targetBoundingRect = targetRef.current.getBoundingClientRect();
|
||||
|
||||
let placeholderTransform: CSSProperties;
|
||||
// Placeholder should be either half the height or half the width of the targetNode, depending on the flex direction of the targetNode's parent.
|
||||
const placeholderHeight =
|
||||
parentNode.flexDirection === FlexDirection.Column
|
||||
? targetBoundingRect.height / 2
|
||||
@ -433,27 +486,32 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefs, style
|
||||
parentNode.flexDirection === FlexDirection.Row
|
||||
? targetBoundingRect.width / 2
|
||||
: targetBoundingRect.width;
|
||||
|
||||
// Default to placing the placeholder in the first half of the target node.
|
||||
let placeholderTop = targetBoundingRect.top - overlayBoundingRect.top;
|
||||
let placeholderLeft = targetBoundingRect.left - overlayBoundingRect.left;
|
||||
if (action.index > targetIndex) {
|
||||
placeholderTransform = createTransform({
|
||||
top:
|
||||
targetBoundingRect.top +
|
||||
(parentNode.flexDirection === FlexDirection.Column && targetBoundingRect.height / 2) -
|
||||
overlayBoundingRect.top,
|
||||
left:
|
||||
targetBoundingRect.left +
|
||||
(parentNode.flexDirection === FlexDirection.Row && targetBoundingRect.width / 2) -
|
||||
overlayBoundingRect.left,
|
||||
width: placeholderWidth,
|
||||
height: placeholderHeight,
|
||||
});
|
||||
if (action.index >= (parentNode.children?.length ?? 1)) {
|
||||
// If there are no more nodes after the specified index, place the placeholder in the second half of the target node (either right or bottom).
|
||||
placeholderTop +=
|
||||
parentNode.flexDirection === FlexDirection.Column && targetBoundingRect.height / 2;
|
||||
placeholderLeft +=
|
||||
parentNode.flexDirection === FlexDirection.Row && targetBoundingRect.width / 2;
|
||||
} else {
|
||||
placeholderTransform = createTransform({
|
||||
top: targetBoundingRect.top - overlayBoundingRect.top,
|
||||
left: targetBoundingRect.left - overlayBoundingRect.left,
|
||||
// Otherwise, place the placeholder between the target node (the one after which it will be inserted) and the next node
|
||||
placeholderTop +=
|
||||
parentNode.flexDirection === FlexDirection.Column &&
|
||||
(3 * targetBoundingRect.height) / 4;
|
||||
placeholderLeft +=
|
||||
parentNode.flexDirection === FlexDirection.Row && (3 * targetBoundingRect.width) / 4;
|
||||
}
|
||||
}
|
||||
const placeholderTransform = createTransform({
|
||||
top: placeholderTop,
|
||||
left: placeholderLeft,
|
||||
width: placeholderWidth,
|
||||
height: placeholderHeight,
|
||||
});
|
||||
}
|
||||
|
||||
newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />;
|
||||
}
|
||||
|
@ -73,29 +73,34 @@ function getLayoutNodeWaveObjAtomFromTab<T>(
|
||||
get: Getter
|
||||
): WritableAtom<LayoutNodeWaveObj<T>, [value: LayoutNodeWaveObj<T>], void> {
|
||||
const tabValue = get(tabAtom);
|
||||
console.log("getLayoutNodeWaveObjAtomFromTab tabValue", tabValue);
|
||||
// console.log("getLayoutNodeWaveObjAtomFromTab tabValue", tabValue);
|
||||
if (!tabValue) return;
|
||||
const layoutNodeOref = WOS.makeORef("layout", tabValue.layoutNode);
|
||||
console.log("getLayoutNodeWaveObjAtomFromTab oref", layoutNodeOref);
|
||||
// console.log("getLayoutNodeWaveObjAtomFromTab oref", layoutNodeOref);
|
||||
return WOS.getWaveObjectAtom<LayoutNodeWaveObj<T>>(layoutNodeOref);
|
||||
}
|
||||
|
||||
export function withLayoutNodeAtomFromTab<T>(tabAtom: Atom<Tab>): WritableLayoutNodeAtom<T> {
|
||||
export function withLayoutStateAtomFromTab<T>(tabAtom: Atom<Tab>): WritableLayoutTreeStateAtom<T> {
|
||||
const pendingActionAtom = atom<LayoutTreeAction>(null) as PrimitiveAtom<LayoutTreeAction>;
|
||||
const generationAtom = atom(0) as PrimitiveAtom<number>;
|
||||
return atom(
|
||||
(get) => {
|
||||
console.log("get withLayoutNodeAtomFromTab", tabAtom);
|
||||
const atom = getLayoutNodeWaveObjAtomFromTab<T>(tabAtom, get);
|
||||
if (!atom) return null;
|
||||
const retVal = get(atom)?.node;
|
||||
console.log("get withLayoutNodeAtomFromTab end", retVal);
|
||||
return get(atom)?.node;
|
||||
const waveObjAtom = getLayoutNodeWaveObjAtomFromTab<T>(tabAtom, get);
|
||||
if (!waveObjAtom) return null;
|
||||
const layoutState = newLayoutTreeState(get(waveObjAtom)?.node);
|
||||
layoutState.pendingAction = get(pendingActionAtom);
|
||||
layoutState.generation = get(generationAtom);
|
||||
return layoutState;
|
||||
},
|
||||
(get, set, value) => {
|
||||
console.log("set withLayoutNodeAtomFromTab", value);
|
||||
set(pendingActionAtom, value.pendingAction);
|
||||
if (get(generationAtom) !== value.generation) {
|
||||
const waveObjAtom = getLayoutNodeWaveObjAtomFromTab<T>(tabAtom, get);
|
||||
if (!waveObjAtom) return;
|
||||
const newWaveObjAtom = { ...get(waveObjAtom), node: value };
|
||||
set(waveObjAtom, newWaveObjAtom);
|
||||
const newWaveObj = { ...get(waveObjAtom), node: value.rootNode };
|
||||
set(generationAtom, value.generation);
|
||||
set(waveObjAtom, newWaveObj);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -106,11 +111,11 @@ export function getLayoutStateAtomForTab(
|
||||
): WritableLayoutTreeStateAtom<TabLayoutData> {
|
||||
let atom = tabLayoutAtomCache.get(tabId);
|
||||
if (atom) {
|
||||
console.log("Reusing atom for tab", tabId);
|
||||
// console.log("Reusing atom for tab", tabId);
|
||||
return atom;
|
||||
}
|
||||
console.log("Creating new atom for tab", tabId);
|
||||
atom = withLayoutTreeState(withLayoutNodeAtomFromTab<TabLayoutData>(tabAtom));
|
||||
// console.log("Creating new atom for tab", tabId);
|
||||
atom = withLayoutStateAtomFromTab<TabLayoutData>(tabAtom);
|
||||
tabLayoutAtomCache.set(tabId, atom);
|
||||
return atom;
|
||||
}
|
||||
|
@ -116,10 +116,27 @@ function computeMoveNode<T>(
|
||||
|
||||
let newOperation: MoveOperation<T>;
|
||||
const parent = lazy(() => findParent(rootNode, node.id));
|
||||
const grandparent = lazy(() => findParent(rootNode, parent().id));
|
||||
const indexInParent = lazy(() => parent()?.children.findIndex((child) => node.id === child.id));
|
||||
const indexInGrandparent = lazy(() => grandparent()?.children.findIndex((child) => parent().id === child.id));
|
||||
const isRoot = rootNode.id === node.id;
|
||||
|
||||
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();
|
||||
newOperation = {
|
||||
parentId: grandparentNode.id,
|
||||
node: nodeToMove,
|
||||
index,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
case DropDirection.Top:
|
||||
if (node.flexDirection === FlexDirection.Column) {
|
||||
newOperation = { parentId: node.id, index: 0, node: nodeToMove };
|
||||
@ -140,6 +157,21 @@ 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;
|
||||
newOperation = {
|
||||
parentId: grandparentNode.id,
|
||||
node: nodeToMove,
|
||||
index,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
case DropDirection.Bottom:
|
||||
if (node.flexDirection === FlexDirection.Column) {
|
||||
newOperation = { parentId: node.id, index: 1, node: nodeToMove };
|
||||
@ -160,6 +192,21 @@ 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();
|
||||
newOperation = {
|
||||
parentId: grandparentNode.id,
|
||||
node: nodeToMove,
|
||||
index,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
case DropDirection.Left:
|
||||
if (node.flexDirection === FlexDirection.Row) {
|
||||
newOperation = { parentId: node.id, index: 0, node: nodeToMove };
|
||||
@ -173,6 +220,21 @@ 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;
|
||||
newOperation = {
|
||||
parentId: grandparentNode.id,
|
||||
node: nodeToMove,
|
||||
index,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
case DropDirection.Right:
|
||||
if (node.flexDirection === FlexDirection.Row) {
|
||||
newOperation = { parentId: node.id, index: 1, node: nodeToMove };
|
||||
@ -186,8 +248,12 @@ function computeMoveNode<T>(
|
||||
};
|
||||
}
|
||||
break;
|
||||
case DropDirection.Center:
|
||||
// TODO: handle center drop
|
||||
console.log("center drop");
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid direction");
|
||||
throw new Error(`Invalid direction: ${direction}`);
|
||||
}
|
||||
|
||||
if (newOperation) layoutTreeState.pendingAction = { type: LayoutTreeActionType.Move, ...newOperation };
|
||||
@ -217,7 +283,9 @@ function moveNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMove
|
||||
}
|
||||
|
||||
if (!parent && action.insertAtRoot) {
|
||||
if (!rootNode.children) {
|
||||
addIntermediateNode(rootNode);
|
||||
}
|
||||
addChildAt(rootNode, action.index, node);
|
||||
} else if (parent) {
|
||||
addChildAt(parent, action.index, node);
|
||||
|
@ -133,6 +133,8 @@ export type WritableLayoutTreeStateAtom<T> = WritableAtom<LayoutTreeState<T>, [v
|
||||
|
||||
export type ContentRenderer<T> = (data: T, ready: boolean, onClose?: () => void) => React.ReactNode;
|
||||
|
||||
export type PreviewRenderer<T> = (data: T) => React.ReactElement;
|
||||
|
||||
export interface LayoutNodeWaveObj<T> extends WaveObj {
|
||||
node: LayoutNode<T>;
|
||||
}
|
||||
|
@ -41,7 +41,14 @@
|
||||
}
|
||||
|
||||
.tile-node {
|
||||
visibility: hidden;
|
||||
&.dragging {
|
||||
filter: blur(8px);
|
||||
}
|
||||
|
||||
.tile-preview-container {
|
||||
position: absolute;
|
||||
top: 10000px;
|
||||
}
|
||||
}
|
||||
|
||||
&.animate {
|
||||
@ -53,12 +60,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.overlayVisible {
|
||||
.tile-node {
|
||||
visibility: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.tile-leaf,
|
||||
.overlay-leaf {
|
||||
display: flex;
|
||||
|
@ -16,6 +16,11 @@ export enum DropDirection {
|
||||
Right = 1,
|
||||
Bottom = 2,
|
||||
Left = 3,
|
||||
OuterTop = 4,
|
||||
OuterRight = 5,
|
||||
OuterBottom = 6,
|
||||
OuterLeft = 7,
|
||||
Center = 8,
|
||||
}
|
||||
|
||||
export enum FlexDirection {
|
||||
@ -38,6 +43,15 @@ export function determineDropDirection(dimensions?: Dimensions, offset?: XYCoord
|
||||
// Lies outside of the box
|
||||
if (y < 0 || y > height || x < 0 || x > width) return undefined;
|
||||
|
||||
// TODO: uncomment once center drop is supported
|
||||
// // Determines if a drop point falls within the center fifth of the box, meaning we should return Center.
|
||||
// const centerX1 = (2 * width) / 5;
|
||||
// const centerX2 = (3 * width) / 5;
|
||||
// const centerY1 = (2 * height) / 5;
|
||||
// const centerY2 = (3 * width) / 5;
|
||||
|
||||
// if (x > centerX1 && x < centerX2 && y > centerY1 && y < centerY2) return DropDirection.Center;
|
||||
|
||||
const diagonal1 = y * width - x * height;
|
||||
const diagonal2 = y * width + x * height - height * width;
|
||||
|
||||
@ -55,6 +69,16 @@ export function determineDropDirection(dimensions?: Dimensions, offset?: XYCoord
|
||||
code = 5 - code;
|
||||
}
|
||||
|
||||
// Determines whether a drop is close to an edge of the box, meaning drop direction should be OuterX, instead of X
|
||||
const xOuter1 = width / 5;
|
||||
const xOuter2 = width - width / 5;
|
||||
const yOuter1 = height / 5;
|
||||
const yOuter2 = height - height / 5;
|
||||
|
||||
if (y < yOuter1 || y > yOuter2 || x < xOuter1 || x > xOuter2) {
|
||||
code += 4;
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
|
@ -14,42 +14,83 @@ test("determineDropDirection", () => {
|
||||
const dimensions: Dimensions = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: 3,
|
||||
width: 3,
|
||||
height: 5,
|
||||
width: 5,
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 1.5,
|
||||
y: 0.5,
|
||||
x: 2.5,
|
||||
y: 1.5,
|
||||
}),
|
||||
DropDirection.Top
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 2.5,
|
||||
y: 3.5,
|
||||
}),
|
||||
DropDirection.Bottom
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 3.5,
|
||||
y: 2.5,
|
||||
}),
|
||||
DropDirection.Right
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 1.5,
|
||||
y: 2.5,
|
||||
}),
|
||||
DropDirection.Bottom
|
||||
DropDirection.Left
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 2.5,
|
||||
y: 1.5,
|
||||
y: 0.5,
|
||||
}),
|
||||
DropDirection.Right
|
||||
DropDirection.OuterTop
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 4.5,
|
||||
y: 2.5,
|
||||
}),
|
||||
DropDirection.OuterRight
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 2.5,
|
||||
y: 4.5,
|
||||
}),
|
||||
DropDirection.OuterBottom
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 0.5,
|
||||
y: 1.5,
|
||||
y: 2.5,
|
||||
}),
|
||||
DropDirection.Left
|
||||
DropDirection.OuterLeft
|
||||
);
|
||||
|
||||
// TODO: uncomment once center direction is supported
|
||||
// assert.equal(
|
||||
// determineDropDirection(dimensions, {
|
||||
// x: 2.5,
|
||||
// y: 2.5,
|
||||
// }),
|
||||
// DropDirection.Center
|
||||
// );
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 1.5,
|
||||
|
@ -57,6 +57,7 @@
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"html-to-image": "^1.11.11",
|
||||
"immer": "^10.1.1",
|
||||
"jotai": "^2.8.0",
|
||||
"monaco-editor": "^0.49.0",
|
||||
|
@ -6961,6 +6961,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-to-image@npm:^1.11.11":
|
||||
version: 1.11.11
|
||||
resolution: "html-to-image@npm:1.11.11"
|
||||
checksum: 10c0/0b6349221ad253dfca01d165c589d44341e942faf0273aab28c8b7d86ff2922d3e8e6390f57bf5ddaf6bac9a3b590a8cdaa77d52a363354796dd0e0e05eb35d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-url-attributes@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "html-url-attributes@npm:3.0.0"
|
||||
@ -11126,6 +11133,7 @@ __metadata:
|
||||
clsx: "npm:^2.1.1"
|
||||
eslint: "npm:^9.2.0"
|
||||
eslint-config-prettier: "npm:^9.1.0"
|
||||
html-to-image: "npm:^1.11.11"
|
||||
immer: "npm:^10.1.1"
|
||||
jotai: "npm:^2.8.0"
|
||||
less: "npm:^4.2.0"
|
||||
|
Loading…
Reference in New Issue
Block a user