waveterm/frontend/faraday/lib/layoutState.ts

347 lines
12 KiB
TypeScript
Raw Normal View History

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import {
addChildAt,
addIntermediateNode,
balanceNode,
findNextInsertLocation,
findNode,
findParent,
removeChild,
} from "./layoutNode.js";
import {
LayoutNode,
LayoutTreeAction,
LayoutTreeActionType,
LayoutTreeComputeMoveNodeAction,
LayoutTreeDeleteNodeAction,
LayoutTreeInsertNodeAction,
LayoutTreeMoveNodeAction,
LayoutTreeState,
MoveOperation,
} from "./model.js";
import { DropDirection, FlexDirection, lazy } from "./utils.js";
/**
* Initializes a layout tree state.
* @param rootNode The root node for the tree.
* @returns The state of the tree.
*
* @template T The type of data associated with the nodes of the tree.
*/
export function newLayoutTreeState<T>(rootNode: LayoutNode<T>): LayoutTreeState<T> {
const { node: balancedRootNode, leafs } = balanceNode(rootNode);
return {
rootNode: balancedRootNode,
leafs,
pendingAction: undefined,
generation: 0,
};
}
/**
* Performs a specified action on the layout tree state. Uses Immer Produce internally to resolve deep changes to the tree.
*
* @param layoutTreeState The state of the tree.
* @param action The action to perform.
*
* @template T The type of data associated with the nodes of the tree.
* @returns The new state of the tree.
*/
export function layoutTreeStateReducer<T>(
layoutTreeState: LayoutTreeState<T>,
action: LayoutTreeAction
): LayoutTreeState<T> {
layoutTreeStateReducerInner(layoutTreeState, action);
return layoutTreeState;
}
/**
* Helper function for layoutTreeStateReducer.
* @param layoutTreeState The state of the tree.
* @param action The action to perform.
* @see layoutTreeStateReducer
* @template T The type of data associated with the nodes of the tree.
*/
function layoutTreeStateReducerInner<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeAction) {
switch (action.type) {
case LayoutTreeActionType.ComputeMove:
computeMoveNode(layoutTreeState, action as LayoutTreeComputeMoveNodeAction<T>);
break;
case LayoutTreeActionType.CommitPendingAction:
if (!layoutTreeState?.pendingAction) {
console.error("unable to commit pending action, does not exist");
break;
}
layoutTreeStateReducerInner(layoutTreeState, layoutTreeState.pendingAction);
break;
case LayoutTreeActionType.Move:
moveNode(layoutTreeState, action as LayoutTreeMoveNodeAction<T>);
layoutTreeState.generation++;
break;
case LayoutTreeActionType.InsertNode:
insertNode(layoutTreeState, action as LayoutTreeInsertNodeAction<T>);
layoutTreeState.generation++;
break;
case LayoutTreeActionType.DeleteNode:
deleteNode(layoutTreeState, action as LayoutTreeDeleteNodeAction);
layoutTreeState.generation++;
break;
default: {
console.error("Invalid reducer action", layoutTreeState, action);
}
}
}
/**
* Computes an operation for inserting a new node into the tree in the given direction relative to the specified node.
*
* @param layoutTreeState The state of the tree.
* @param computeInsertAction The operation to compute.
*
* @template T The type of data associated with the nodes of the tree.
*/
function computeMoveNode<T>(
layoutTreeState: LayoutTreeState<T>,
computeInsertAction: LayoutTreeComputeMoveNodeAction<T>
) {
const rootNode = layoutTreeState.rootNode;
const { node, nodeToMove, direction } = computeInsertAction;
console.log("computeInsertOperation start", layoutTreeState.rootNode, node, nodeToMove, direction);
if (direction === undefined) {
console.warn("No direction provided for insertItemInDirection");
return;
}
let newOperation: MoveOperation<T>;
const parent = lazy(() => findParent(rootNode, node.id));
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
const grandparent = lazy(() => findParent(rootNode, parent().id));
const indexInParent = lazy(() => parent()?.children.findIndex((child) => node.id === child.id));
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
const indexInGrandparent = lazy(() => grandparent()?.children.findIndex((child) => parent().id === child.id));
const isRoot = rootNode.id === node.id;
switch (direction) {
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
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 };
} else {
if (isRoot)
newOperation = {
node: nodeToMove,
index: 0,
insertAtRoot: true,
};
const parentNode = parent();
if (parentNode)
newOperation = {
parentId: parentNode.id,
index: indexInParent() ?? 0,
node: nodeToMove,
};
}
break;
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
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 };
} else {
if (isRoot)
newOperation = {
node: nodeToMove,
index: 1,
insertAtRoot: true,
};
const parentNode = parent();
if (parentNode)
newOperation = {
parentId: parentNode.id,
index: indexInParent() + 1,
node: nodeToMove,
};
}
break;
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
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 };
} else {
const parentNode = parent();
if (parentNode)
newOperation = {
parentId: parentNode.id,
index: indexInParent(),
node: nodeToMove,
};
}
break;
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
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 };
} else {
const parentNode = parent();
if (parentNode)
newOperation = {
parentId: parentNode.id,
index: indexInParent() + 1,
node: nodeToMove,
};
}
break;
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
case DropDirection.Center:
// TODO: handle center drop
console.log("center drop");
break;
default:
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
throw new Error(`Invalid direction: ${direction}`);
}
if (newOperation) layoutTreeState.pendingAction = { type: LayoutTreeActionType.Move, ...newOperation };
}
function moveNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMoveNodeAction<T>) {
const rootNode = layoutTreeState.rootNode;
console.log("moveNode", action, layoutTreeState.rootNode);
if (!action) {
console.error("no move node action provided");
return;
}
if (action.parentId && action.insertAtRoot) {
console.error("parent and insertAtRoot cannot both be defined in a move node action");
return;
}
let node = findNode(rootNode, action.node.id) ?? action.node;
let parent = findNode(rootNode, action.parentId);
let oldParent = findParent(rootNode, action.node.id);
console.log(node, parent, oldParent);
if (!parent && action.insertAtRoot) {
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
if (!rootNode.children) {
addIntermediateNode(rootNode);
}
addChildAt(rootNode, action.index, node);
} else if (parent) {
addChildAt(parent, action.index, node);
} else {
throw new Error("Invalid InsertOperation");
}
// Remove nodeToInsert from its old parent
if (oldParent) {
removeChild(oldParent, node);
}
const { node: newRootNode, leafs } = balanceNode(layoutTreeState.rootNode);
layoutTreeState.rootNode = newRootNode;
layoutTreeState.leafs = leafs;
layoutTreeState.pendingAction = undefined;
}
function insertNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeInsertNodeAction<T>) {
if (!action?.node) {
console.error("no insert node action provided");
return;
}
if (!layoutTreeState.rootNode) {
const { node: balancedNode, leafs } = balanceNode(action.node);
layoutTreeState.rootNode = balancedNode;
layoutTreeState.leafs = leafs;
return;
}
const insertLoc = findNextInsertLocation(layoutTreeState.rootNode, 5);
addChildAt(insertLoc.node, insertLoc.index, action.node);
const { node: newRootNode, leafs } = balanceNode(layoutTreeState.rootNode);
layoutTreeState.rootNode = newRootNode;
layoutTreeState.leafs = leafs;
}
function deleteNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeDeleteNodeAction) {
console.log("deleteNode", layoutTreeState, action);
if (!action?.nodeId) {
console.error("no delete node action provided");
return;
}
if (!layoutTreeState.rootNode) {
console.error("no root node");
return;
}
if (layoutTreeState.rootNode.id === action.nodeId) {
layoutTreeState.rootNode = undefined;
layoutTreeState.leafs = undefined;
return;
}
const parent = findParent(layoutTreeState.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 { node: newRootNode, leafs } = balanceNode(layoutTreeState.rootNode);
layoutTreeState.rootNode = newRootNode;
layoutTreeState.leafs = leafs;
}