Unified node model to pass data from layout to blocks (#259)

This adds a new NodeModel, which can be passed from the TileLayout to
contained blocks. It contains all the layout data that the block should
care about, including focus status, whether a drag operation is
underway, whether the node is magnified, etc.

This also adds a focus stack for the layout, which will let the focus
switch to the last-focused node when the currently-focused one is
closed.

This also addresses a regression in the resize handles that caused them
to be offset from the cursor when dragged.

---------

Co-authored-by: sawka <mike.sawka@gmail.com>
This commit is contained in:
Evan Simkowitz
2024-08-26 11:56:00 -07:00
committed by GitHub
co-authored by sawka
parent 8e5a4a457c
commit 164afeeb66
19 changed files with 596 additions and 573 deletions
+8 -2
View File
@@ -5,10 +5,10 @@ import { TileLayout } from "./lib/TileLayout";
import { LayoutModel } from "./lib/layoutModel";
import {
deleteLayoutModelForTab,
getLayoutModelForActiveTab,
getLayoutModelForTab,
getLayoutModelForTabById,
useLayoutModel,
useLayoutNode,
} from "./lib/layoutModelHooks";
import { newLayoutNode } from "./lib/layoutNode";
import type {
@@ -19,6 +19,7 @@ import type {
LayoutTreeCommitPendingAction,
LayoutTreeComputeMoveNodeAction,
LayoutTreeDeleteNodeAction,
LayoutTreeFocusNodeAction,
LayoutTreeInsertNodeAction,
LayoutTreeInsertNodeAtIndexAction,
LayoutTreeMagnifyNodeToggleAction,
@@ -27,12 +28,15 @@ import type {
LayoutTreeSetPendingAction,
LayoutTreeStateSetter,
LayoutTreeSwapNodeAction,
NodeModel,
PreviewRenderer,
} from "./lib/types";
import { DropDirection, LayoutTreeActionType, NavigateDirection } from "./lib/types";
export {
deleteLayoutModelForTab,
DropDirection,
getLayoutModelForActiveTab,
getLayoutModelForTab,
getLayoutModelForTabById,
LayoutModel,
@@ -41,7 +45,6 @@ export {
newLayoutNode,
TileLayout,
useLayoutModel,
useLayoutNode,
};
export type {
ContentRenderer,
@@ -51,6 +54,7 @@ export type {
LayoutTreeCommitPendingAction,
LayoutTreeComputeMoveNodeAction,
LayoutTreeDeleteNodeAction,
LayoutTreeFocusNodeAction,
LayoutTreeInsertNodeAction,
LayoutTreeInsertNodeAtIndexAction,
LayoutTreeMagnifyNodeToggleAction,
@@ -59,4 +63,6 @@ export type {
LayoutTreeSetPendingAction,
LayoutTreeStateSetter,
LayoutTreeSwapNodeAction,
NodeModel,
PreviewRenderer,
};
+41 -68
View File
@@ -19,7 +19,7 @@ import { DropTargetMonitor, XYCoord, useDrag, useDragLayer, useDrop } from "reac
import { debounce, throttle } from "throttle-debounce";
import { useDevicePixelRatio } from "use-device-pixel-ratio";
import { LayoutModel } from "./layoutModel";
import { useLayoutNode, useTileLayout } from "./layoutModelHooks";
import { useNodeModel, useTileLayout } from "./layoutModelHooks";
import "./tilelayout.less";
import {
LayoutNode,
@@ -53,7 +53,6 @@ const DragPreviewHeight = 300;
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);
@@ -85,7 +84,7 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
}
}
}),
[getCursorPoint, generation]
[getCursorPoint]
);
// Effect to detect when the cursor leaves the TileLayout hit trap so we can remove any placeholders. This cannot be done using pointer capture
@@ -115,7 +114,7 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
>
<div key="display" ref={layoutModel.displayContainerRef} className="display-container">
<ResizeHandleWrapper layoutModel={layoutModel} />
<DisplayNodesWrapper contents={contents} layoutModel={layoutModel} />
<DisplayNodesWrapper layoutModel={layoutModel} />
</div>
<Placeholder key="placeholder" layoutModel={layoutModel} style={{ top: 10000, ...overlayTransform }} />
<OverlayNodeWrapper layoutModel={layoutModel} />
@@ -131,19 +130,15 @@ interface DisplayNodesWrapperProps {
* The layout tree state.
*/
layoutModel: LayoutModel;
/**
* contains callbacks and information about the contents (or styling) of of the TileLayout
*/
contents: TileLayoutContents;
}
const DisplayNodesWrapper = ({ layoutModel, contents }: DisplayNodesWrapperProps) => {
const DisplayNodesWrapper = ({ layoutModel }: DisplayNodesWrapperProps) => {
const leafs = useAtomValue(layoutModel.leafs);
return useMemo(
() =>
leafs.map((leaf) => {
return <DisplayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} contents={contents} />;
leafs.map((node) => {
return <DisplayNode key={node.id} layoutModel={layoutModel} node={node} />;
}),
[leafs]
);
@@ -154,12 +149,7 @@ interface DisplayNodeProps {
/**
* The leaf node object, containing the data needed to display the leaf contents to the user.
*/
layoutNode: LayoutNode;
/**
* contains callbacks and information about the contents (or styling) of of the TileLayout
*/
contents: TileLayoutContents;
node: LayoutNode;
}
const dragItemType = "TILE_ITEM";
@@ -167,26 +157,23 @@ const dragItemType = "TILE_ITEM";
/**
* The draggable and displayable portion of a leaf node in a layout tree.
*/
const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) => {
const DisplayNode = ({ layoutModel, node }: DisplayNodeProps) => {
const nodeModel = useNodeModel(layoutModel, node);
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 addlProps = useAtomValue(nodeModel.additionalProps);
const devicePixelRatio = useDevicePixelRatio();
const [{ isDragging }, drag, dragPreview] = useDrag(
() => ({
type: dragItemType,
item: () => layoutNode,
item: () => node,
canDrag: () => !addlProps?.isMagnifiedNode,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[layoutNode, addlProps]
[node, addlProps]
);
const [previewElementGeneration, setPreviewElementGeneration] = useState(0);
@@ -203,11 +190,11 @@ const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) =>
transform: `scale(${1 / devicePixelRatio})`,
}}
>
{contents.renderPreview?.(layoutNode.data)}
{layoutModel.renderPreview?.(nodeModel)}
</div>
</div>
);
}, [contents.renderPreview, devicePixelRatio, layoutNode.data]);
}, [devicePixelRatio, nodeModel]);
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
const [previewImageGeneration, setPreviewImageGeneration] = useState(0);
@@ -232,31 +219,20 @@ const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) =>
previewImageGeneration,
previewImage,
devicePixelRatio,
layoutNode.data,
]);
// Register the display node as a draggable item
useEffect(() => {
drag(dragHandleRef);
}, [drag, dragHandleRef.current]);
const leafContent = useMemo(() => {
return (
layoutNode.data && (
<div key="leaf" className="tile-leaf">
{contents.renderContent(
layoutNode.data,
globalReady,
addlProps?.isMagnifiedNode ?? false,
activeDrag,
() => layoutModel.magnifyNodeToggle(layoutNode),
() => layoutModel.closeNode(layoutNode),
dragHandleRef
)}
</div>
)
<div key="leaf" className="tile-leaf">
{layoutModel.renderContent(nodeModel)}
</div>
);
}, [layoutNode, globalReady, activeDrag, addlProps]);
}, [nodeModel]);
// Register the display node as a draggable item
useEffect(() => {
drag(nodeModel.dragHandleRef);
}, [drag, nodeModel.dragHandleRef.current]);
return (
<div
@@ -266,7 +242,7 @@ const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) =>
"last-magnified": addlProps?.isLastMagnifiedNode,
})}
ref={tileNodeRef}
id={layoutNode.id}
id={node.id}
style={addlProps?.transform}
onPointerEnter={generatePreviewImage}
onPointerOver={(event) => event.stopPropagation()}
@@ -281,14 +257,14 @@ interface OverlayNodeWrapperProps {
layoutModel: LayoutModel;
}
const OverlayNodeWrapper = ({ layoutModel }: OverlayNodeWrapperProps) => {
const OverlayNodeWrapper = memo(({ layoutModel }: OverlayNodeWrapperProps) => {
const leafs = useAtomValue(layoutModel.leafs);
const overlayTransform = useAtomValue(layoutModel.overlayTransform);
const overlayNodes = useMemo(
() =>
leafs.map((leaf) => {
return <OverlayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} />;
leafs.map((node) => {
return <OverlayNode key={node.id} layoutModel={layoutModel} node={node} />;
}),
[leafs]
);
@@ -298,34 +274,31 @@ const OverlayNodeWrapper = ({ layoutModel }: OverlayNodeWrapperProps) => {
{overlayNodes}
</div>
);
};
});
interface OverlayNodeProps {
/**
* The layout node object corresponding to the OverlayNode.
*/
layoutNode: LayoutNode;
/**
* The layout tree state.
*/
layoutModel: LayoutModel;
node: LayoutNode;
}
/**
* 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
* dimensions of the corresponding DisplayNode for each LayoutTreeState leaf.
*/
const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
const additionalProps = useLayoutNode(layoutModel, layoutNode);
const OverlayNode = memo(({ node, layoutModel }: OverlayNodeProps) => {
const nodeModel = useNodeModel(layoutModel, node);
const additionalProps = useAtomValue(nodeModel.additionalProps);
const overlayRef = useRef<HTMLDivElement>(null);
const generation = useAtomValue(layoutModel.generationAtom);
const [, drop] = useDrop(
() => ({
accept: dragItemType,
canDrop: (_, monitor) => {
const dragItem = monitor.getItem<LayoutNode>();
if (monitor.isOver({ shallow: true }) && dragItem?.id !== layoutNode.id) {
if (monitor.isOver({ shallow: true }) && dragItem?.id !== node.id) {
return true;
}
return false;
@@ -346,7 +319,7 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
offset.y -= containerRect.y;
layoutModel.treeReducer({
type: LayoutTreeActionType.ComputeMove,
node: layoutNode,
node: node,
nodeToMove: dragItem,
direction: determineDropDirection(additionalProps.rect, offset),
} as LayoutTreeComputeMoveNodeAction);
@@ -358,7 +331,7 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
}
}),
}),
[layoutNode, generation, additionalProps, layoutModel.displayContainerRef]
[node, additionalProps, layoutModel.displayContainerRef]
);
// Register the overlay node as a drop target
@@ -366,27 +339,27 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
drop(overlayRef);
}, []);
return <div ref={overlayRef} className="overlay-node" id={layoutNode.id} style={additionalProps?.transform} />;
};
return <div ref={overlayRef} className="overlay-node" id={node.id} style={additionalProps?.transform} />;
});
interface ResizeHandleWrapperProps {
layoutModel: LayoutModel;
}
const ResizeHandleWrapper = ({ layoutModel }: ResizeHandleWrapperProps) => {
const ResizeHandleWrapper = memo(({ layoutModel }: ResizeHandleWrapperProps) => {
const resizeHandles = useAtomValue(layoutModel.resizeHandles) as Atom<ResizeHandleProps>[];
return resizeHandles.map((resizeHandleAtom, i) => (
<ResizeHandle key={`resize-handle-${i}`} layoutModel={layoutModel} resizeHandleAtom={resizeHandleAtom} />
));
};
});
interface ResizeHandleComponentProps {
resizeHandleAtom: Atom<ResizeHandleProps>;
layoutModel: LayoutModel;
}
const ResizeHandle = ({ resizeHandleAtom, layoutModel }: ResizeHandleComponentProps) => {
const ResizeHandle = memo(({ resizeHandleAtom, layoutModel }: ResizeHandleComponentProps) => {
const resizeHandleProps = useAtomValue(resizeHandleAtom);
const resizeHandleRef = useRef<HTMLDivElement>(null);
@@ -436,7 +409,7 @@ const ResizeHandle = ({ resizeHandleAtom, layoutModel }: ResizeHandleComponentPr
<div className="line" />
</div>
);
};
});
interface PlaceholderProps {
/**
+2 -4
View File
@@ -17,7 +17,6 @@ function getLayoutStateAtomFromTab(tabAtom: Atom<Tab>, get: Getter): WritableWav
export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayoutTreeStateAtom {
if (layoutStateAtomMap.has(tabAtom)) {
// console.log("found atom");
return layoutStateAtomMap.get(tabAtom);
}
const generationAtom = atom(1);
@@ -26,9 +25,9 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayo
const stateAtom = getLayoutStateAtomFromTab(tabAtom, get);
if (!stateAtom) return;
const layoutStateData = get(stateAtom);
// console.log("layoutStateData", layoutStateData);
const layoutTreeState: LayoutTreeState = {
rootNode: layoutStateData?.rootnode,
focusedNodeId: layoutStateData?.focusednodeid,
magnifiedNodeId: layoutStateData?.magnifiednodeid,
generation: get(generationAtom),
};
@@ -37,12 +36,11 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayo
(get, set, value) => {
if (get(generationAtom) < value.generation) {
const stateAtom = getLayoutStateAtomFromTab(tabAtom, get);
// console.log("setting new atom val", value);
if (!stateAtom) return;
const waveObjVal = get(stateAtom);
// console.log("waveObjVal", waveObjVal);
waveObjVal.rootnode = value.rootNode;
waveObjVal.magnifiednodeid = value.magnifiedNodeId;
waveObjVal.focusednodeid = value.focusedNodeId;
set(generationAtom, value.generation);
set(stateAtom, waveObjVal);
}
File diff suppressed because it is too large Load Diff
+10 -7
View File
@@ -1,13 +1,13 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { globalStore, WOS } from "@/app/store/global";
import { atoms, globalStore, WOS } from "@/app/store/global";
import useResizeObserver from "@react-hook/resize-observer";
import { Atom, useAtomValue } from "jotai";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { withLayoutTreeStateAtomFromTab } from "./layoutAtom";
import { LayoutModel } from "./layoutModel";
import { LayoutNode, LayoutNodeAdditionalProps, TileLayoutContents } from "./types";
import { LayoutNode, NodeModel, TileLayoutContents } from "./types";
const layoutModelMap: Map<string, LayoutModel> = new Map();
@@ -34,6 +34,11 @@ export function getLayoutModelForTabById(tabId: string) {
return getLayoutModelForTab(tabAtom);
}
export function getLayoutModelForActiveTab() {
const tabId = globalStore.get(atoms.activeTabId);
return getLayoutModelForTabById(tabId);
}
export function deleteLayoutModelForTab(tabId: string) {
if (layoutModelMap.has(tabId)) layoutModelMap.delete(tabId);
}
@@ -51,8 +56,6 @@ export function useTileLayout(tabAtom: Atom<Tab>, tileContent: TileLayoutContent
return layoutModel;
}
export function useLayoutNode(layoutModel: LayoutModel, layoutNode: LayoutNode): LayoutNodeAdditionalProps {
const [addlPropsAtom] = useState(layoutModel.getNodeAdditionalPropertiesAtom(layoutNode.id));
const addlProps = useAtomValue(addlPropsAtom);
return addlProps;
export function useNodeModel(layoutModel: LayoutModel, layoutNode: LayoutNode): NodeModel {
return layoutModel.getNodeModel(layoutNode);
}
+48 -35
View File
@@ -18,6 +18,7 @@ import {
LayoutTreeActionType,
LayoutTreeComputeMoveNodeAction,
LayoutTreeDeleteNodeAction,
LayoutTreeFocusNodeAction,
LayoutTreeInsertNodeAction,
LayoutTreeInsertNodeAtIndexAction,
LayoutTreeMagnifyNodeToggleAction,
@@ -37,7 +38,6 @@ import {
export function computeMoveNode(layoutState: LayoutTreeState, computeInsertAction: LayoutTreeComputeMoveNodeAction) {
const rootNode = layoutState.rootNode;
const { node, nodeToMove, direction } = computeInsertAction;
// console.log("computeInsertOperation start", layoutState.rootNode, node, nodeToMove, direction);
if (direction === undefined) {
console.warn("No direction provided for insertItemInDirection");
return;
@@ -179,14 +179,12 @@ export function computeMoveNode(layoutState: LayoutTreeState, computeInsertActio
}
break;
case DropDirection.Center:
// console.log("center drop", rootNode, node, nodeToMove);
if (node.id !== rootNode.id && nodeToMove.id !== rootNode.id) {
const swapAction: LayoutTreeSwapNodeAction = {
type: LayoutTreeActionType.Swap,
node1Id: node.id,
node2Id: nodeToMove.id,
};
// console.log("swapAction", swapAction);
return swapAction;
} else {
console.warn("cannot swap");
@@ -209,7 +207,6 @@ export function computeMoveNode(layoutState: LayoutTreeState, computeInsertActio
export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNodeAction) {
const rootNode = layoutState.rootNode;
// console.log("moveNode", action, layoutState.rootNode);
if (!action) {
console.error("no move node action provided");
return;
@@ -223,8 +220,6 @@ export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNod
const parent = findNode(rootNode, action.parentId);
const oldParent = findParent(rootNode, action.node.id);
// console.log(node, parent, oldParent);
let startingIndex = 0;
// If moving under the same parent, we need to make sure that we are removing the child from its old position, not its new one.
@@ -256,6 +251,7 @@ export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNod
if (oldParent) {
removeChild(oldParent, node, startingIndex);
}
layoutState.generation++;
}
export function insertNode(layoutState: LayoutTreeState, action: LayoutTreeInsertNodeAction) {
@@ -265,13 +261,15 @@ export function insertNode(layoutState: LayoutTreeState, action: LayoutTreeInser
}
if (!layoutState.rootNode) {
layoutState.rootNode = action.node;
return;
}
const insertLoc = findNextInsertLocation(layoutState.rootNode, 5);
addChildAt(insertLoc.node, insertLoc.index, action.node);
if (action.magnified) {
layoutState.magnifiedNodeId = action.node.id;
} else {
const insertLoc = findNextInsertLocation(layoutState.rootNode, 5);
addChildAt(insertLoc.node, insertLoc.index, action.node);
if (action.magnified) {
layoutState.magnifiedNodeId = action.node.id;
}
layoutState.focusedNodeId = action.node.id;
}
layoutState.generation++;
}
export function insertNodeAtIndex(layoutState: LayoutTreeState, action: LayoutTreeInsertNodeAtIndexAction) {
@@ -281,22 +279,22 @@ export function insertNodeAtIndex(layoutState: LayoutTreeState, action: LayoutTr
}
if (!layoutState.rootNode) {
layoutState.rootNode = action.node;
return;
}
const insertLoc = findInsertLocationFromIndexArr(layoutState.rootNode, action.indexArr);
if (!insertLoc) {
console.error("insertNodeAtIndex unable to find insert location");
return;
}
addChildAt(insertLoc.node, insertLoc.index + 1, action.node);
if (action.magnified) {
layoutState.magnifiedNodeId = action.node.id;
} else {
const insertLoc = findInsertLocationFromIndexArr(layoutState.rootNode, action.indexArr);
if (!insertLoc) {
console.error("insertNodeAtIndex unable to find insert location");
return;
}
addChildAt(insertLoc.node, insertLoc.index + 1, action.node);
if (action.magnified) {
layoutState.magnifiedNodeId = action.node.id;
}
layoutState.focusedNodeId = action.node.id;
}
layoutState.generation++;
}
export function swapNode(layoutState: LayoutTreeState, action: LayoutTreeSwapNodeAction) {
// console.log("swapNode", layoutState, action);
if (!action.node1Id || !action.node2Id) {
console.error("invalid swapNode action, both node1 and node2 must be defined");
return;
@@ -325,10 +323,10 @@ export function swapNode(layoutState: LayoutTreeState, action: LayoutTreeSwapNod
parentNode1.children[parentNode1Index] = node2;
parentNode2.children[parentNode2Index] = node1;
layoutState.generation++;
}
export function deleteNode(layoutState: LayoutTreeState, action: LayoutTreeDeleteNodeAction) {
// console.log("deleteNode", layoutState, action);
if (!action?.nodeId) {
console.error("no delete node action provided");
return;
@@ -339,20 +337,23 @@ export function deleteNode(layoutState: LayoutTreeState, action: LayoutTreeDelet
}
if (layoutState.rootNode.id === action.nodeId) {
layoutState.rootNode = undefined;
return;
}
const parent = findParent(layoutState.rootNode, action.nodeId);
if (parent) {
const node = parent.children.find((child) => child.id === action.nodeId);
removeChild(parent, node);
// console.log("node deleted", parent, node);
} else {
console.error("unable to delete node, not found in tree");
const parent = findParent(layoutState.rootNode, action.nodeId);
if (parent) {
const node = parent.children.find((child) => child.id === action.nodeId);
removeChild(parent, node);
if (layoutState.focusedNodeId === node.id) {
layoutState.focusedNodeId = undefined;
}
} else {
console.error("unable to delete node, not found in tree");
}
}
layoutState.generation++;
}
export function resizeNode(layoutState: LayoutTreeState, action: LayoutTreeResizeNodeAction) {
// console.log("resizeNode", layoutState, action);
if (!action.resizeOperations) {
console.error("invalid resizeNode operation. nodeSizes array must be defined.");
}
@@ -364,10 +365,20 @@ export function resizeNode(layoutState: LayoutTreeState, action: LayoutTreeResiz
const node = findNode(layoutState.rootNode, resize.nodeId);
node.size = resize.size;
}
layoutState.generation++;
}
export function focusNode(layoutState: LayoutTreeState, action: LayoutTreeFocusNodeAction) {
if (!action.nodeId) {
console.error("invalid focusNode operation, nodeId must be defined.");
return;
}
layoutState.focusedNodeId = action.nodeId;
layoutState.generation++;
}
export function magnifyNodeToggle(layoutState: LayoutTreeState, action: LayoutTreeMagnifyNodeToggleAction) {
// console.log("magnifyNodeToggle", layoutState, action);
if (!action.nodeId) {
console.error("invalid magnifyNodeToggle operation. nodeId must be defined.");
return;
@@ -380,5 +391,7 @@ export function magnifyNodeToggle(layoutState: LayoutTreeState, action: LayoutTr
layoutState.magnifiedNodeId = undefined;
} else {
layoutState.magnifiedNodeId = action.nodeId;
layoutState.focusedNodeId = action.nodeId;
}
layoutState.generation++;
}
+34 -13
View File
@@ -1,13 +1,13 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { WritableAtom } from "jotai";
import { Atom, WritableAtom } from "jotai";
import { CSSProperties } from "react";
export enum NavigateDirection {
Top = 0,
Up = 0,
Right = 1,
Bottom = 2,
Down = 2,
Left = 3,
}
@@ -67,6 +67,7 @@ export enum LayoutTreeActionType {
InsertNode = "insert",
InsertNodeAtIndex = "insertatindex",
DeleteNode = "delete",
FocusNode = "focus",
MagnifyNodeToggle = "magnify",
}
@@ -207,6 +208,18 @@ export interface LayoutTreeResizeNodeAction extends LayoutTreeAction {
resizeOperations: ResizeNodeOperation[];
}
/**
* Action for focusing a node from the layout tree.
*/
export interface LayoutTreeFocusNodeAction extends LayoutTreeAction {
type: LayoutTreeActionType.FocusNode;
/**
* The id of the node to focus;
*/
nodeId: string;
}
/**
* Action for toggling magnification of a node from the layout tree.
*/
@@ -234,23 +247,16 @@ export type LayoutTreeStateSetter = (value: LayoutState) => void;
export type LayoutTreeState = {
rootNode: LayoutNode;
focusedNodeId?: string;
magnifiedNodeId?: string;
generation: number;
};
export type WritableLayoutTreeStateAtom = WritableAtom<LayoutTreeState, [value: LayoutTreeState], void>;
export type ContentRenderer = (
data: TabLayoutData,
ready: boolean,
isMagnified: boolean,
disablePointerEvents: boolean,
onMagnifyToggle: () => void,
onClose: () => void,
dragHandleRef: React.RefObject<HTMLDivElement>
) => React.ReactNode;
export type ContentRenderer = (nodeModel: NodeModel) => React.ReactNode;
export type PreviewRenderer = (data: TabLayoutData) => React.ReactElement;
export type PreviewRenderer = (nodeModel: NodeModel) => React.ReactElement;
export const DefaultNodeSize = 10;
@@ -307,3 +313,18 @@ export interface LayoutNodeAdditionalProps {
isMagnifiedNode?: boolean;
isLastMagnifiedNode?: boolean;
}
export interface NodeModel {
additionalProps: Atom<LayoutNodeAdditionalProps>;
blockNum: Atom<number>;
nodeId: string;
blockId: string;
isFocused: Atom<boolean>;
isMagnified: Atom<boolean>;
ready: Atom<boolean>;
disablePointerEvents: Atom<boolean>;
toggleMagnify: () => void;
focusNode: () => void;
onClose: () => void;
dragHandleRef?: React.RefObject<HTMLDivElement>;
}
+21 -1
View File
@@ -3,7 +3,7 @@
import { CSSProperties } from "react";
import { XYCoord } from "react-dnd";
import { DropDirection, FlexDirection } from "./types";
import { DropDirection, FlexDirection, NavigateDirection } from "./types";
export function reverseFlexDirection(flexDirection: FlexDirection): FlexDirection {
return flexDirection === FlexDirection.Row ? FlexDirection.Column : FlexDirection.Row;
@@ -82,3 +82,23 @@ export function setTransform(
position: "absolute",
};
}
export function getCenter(dimensions: Dimensions): Point {
return {
x: dimensions.left + dimensions.width / 2,
y: dimensions.top + dimensions.height / 2,
};
}
export function navigateDirectionToOffset(direction: NavigateDirection): Point {
switch (direction) {
case NavigateDirection.Up:
return { x: 0, y: -1 };
case NavigateDirection.Down:
return { x: 0, y: 1 };
case NavigateDirection.Left:
return { x: -1, y: 0 };
case NavigateDirection.Right:
return { x: 1, y: 0 };
}
}