2024-06-04 22:05:44 +02:00
|
|
|
// Copyright 2024, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2024-08-22 02:43:11 +02:00
|
|
|
import { DefaultNodeSize, FlexDirection, LayoutNode } from "./types";
|
|
|
|
import { reverseFlexDirection } from "./utils";
|
2024-06-04 22:05:44 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new node.
|
|
|
|
* @param flexDirection The flex direction for the new node.
|
|
|
|
* @param size The size for the new node.
|
|
|
|
* @param children The children for the new node.
|
|
|
|
* @param data The data for the new node.
|
|
|
|
* @returns The new node.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
export function newLayoutNode(
|
2024-06-04 22:05:44 +02:00
|
|
|
flexDirection?: FlexDirection,
|
|
|
|
size?: number,
|
2024-08-15 03:40:41 +02:00
|
|
|
children?: LayoutNode[],
|
|
|
|
data?: TabLayoutData
|
|
|
|
): LayoutNode {
|
|
|
|
const newNode: LayoutNode = {
|
2024-06-04 22:05:44 +02:00
|
|
|
id: crypto.randomUUID(),
|
2024-06-05 01:35:32 +02:00
|
|
|
flexDirection: flexDirection ?? FlexDirection.Row,
|
2024-07-03 23:31:02 +02:00
|
|
|
size: size ?? DefaultNodeSize,
|
2024-06-04 22:05:44 +02:00
|
|
|
children,
|
|
|
|
data,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!validateNode(newNode)) {
|
|
|
|
throw new Error("Invalid node");
|
|
|
|
}
|
|
|
|
return newNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds new nodes to the tree at the given index.
|
|
|
|
* @param node The parent node.
|
|
|
|
* @param idx The index to insert at.
|
|
|
|
* @param children The nodes to insert.
|
|
|
|
* @returns The updated parent node.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
export function addChildAt(node: LayoutNode, idx: number, ...children: LayoutNode[]) {
|
2024-08-15 21:24:06 +02:00
|
|
|
// console.log("adding", children, "to", node, "at index", idx);
|
2024-06-04 22:05:44 +02:00
|
|
|
if (children.length === 0) return;
|
|
|
|
|
|
|
|
if (!node.children) {
|
|
|
|
addIntermediateNode(node);
|
|
|
|
}
|
|
|
|
const childrenToAdd = children.flatMap((v) => {
|
|
|
|
if (v.flexDirection !== node.flexDirection) {
|
|
|
|
return v;
|
|
|
|
} else if (v.children) {
|
|
|
|
return v.children;
|
|
|
|
} else {
|
|
|
|
v.flexDirection = reverseFlexDirection(node.flexDirection);
|
|
|
|
return v;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (node.children.length <= idx) {
|
|
|
|
node.children.push(...childrenToAdd);
|
|
|
|
} else if (idx >= 0) {
|
|
|
|
node.children.splice(idx, 0, ...childrenToAdd);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds an intermediate node as a direct child of the given node, moving the given node's children or data into it.
|
|
|
|
*
|
|
|
|
* If the node contains children, they are moved two levels deeper to preserve their flex direction. If the node only has data, it is moved one level deeper.
|
|
|
|
* @param node The node to add the intermediate node to.
|
|
|
|
* @returns The updated node and the node that was added.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
export function addIntermediateNode(node: LayoutNode): LayoutNode {
|
|
|
|
let intermediateNode: LayoutNode;
|
2024-06-04 22:05:44 +02:00
|
|
|
|
|
|
|
if (node.data) {
|
|
|
|
intermediateNode = newLayoutNode(reverseFlexDirection(node.flexDirection), undefined, undefined, node.data);
|
|
|
|
node.children = [intermediateNode];
|
|
|
|
node.data = undefined;
|
|
|
|
} else {
|
|
|
|
const intermediateNodeInner = newLayoutNode(node.flexDirection, undefined, node.children);
|
|
|
|
intermediateNode = newLayoutNode(reverseFlexDirection(node.flexDirection), undefined, [intermediateNodeInner]);
|
|
|
|
node.children = [intermediateNode];
|
|
|
|
}
|
|
|
|
const intermediateNodeId = intermediateNode.id;
|
|
|
|
intermediateNode.id = node.id;
|
|
|
|
node.id = intermediateNodeId;
|
|
|
|
return intermediateNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attempts to remove the specified node from its parent.
|
|
|
|
* @param parent The parent node.
|
|
|
|
* @param childToRemove The node to remove.
|
2024-06-17 23:14:09 +02:00
|
|
|
* @param startingIndex The index in children to start the search from.
|
2024-06-04 22:05:44 +02:00
|
|
|
* @returns The updated parent node, or undefined if the node was not found.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
export function removeChild(parent: LayoutNode, childToRemove: LayoutNode, startingIndex: number = 0) {
|
2024-06-04 22:05:44 +02:00
|
|
|
if (!parent.children) return;
|
2024-06-17 23:14:09 +02:00
|
|
|
const idx = parent.children.indexOf(childToRemove, startingIndex);
|
2024-06-04 22:05:44 +02:00
|
|
|
if (idx === -1) return;
|
|
|
|
parent.children?.splice(idx, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds the node with the given id.
|
|
|
|
* @param node The node to search in.
|
|
|
|
* @param id The id to search for.
|
|
|
|
* @returns The node with the given id or undefined if no node with the given id was found.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
export function findNode(node: LayoutNode, id: string): LayoutNode | undefined {
|
2024-06-04 22:05:44 +02:00
|
|
|
if (node.id === id) return node;
|
|
|
|
if (!node.children) return;
|
|
|
|
for (const child of node.children) {
|
|
|
|
const result = findNode(child, id);
|
|
|
|
if (result) return result;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds the node whose children contains the node with the given id.
|
|
|
|
* @param node The node to start the search from.
|
|
|
|
* @param id The id to search for.
|
|
|
|
* @returns The parent node, or undefined if no node with the given id was found.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
export function findParent(node: LayoutNode, id: string): LayoutNode | undefined {
|
2024-06-04 22:05:44 +02:00
|
|
|
if (node.id === id || !node.children) return;
|
|
|
|
for (const child of node.children) {
|
|
|
|
if (child.id === id) return node;
|
|
|
|
const retVal = findParent(child, id);
|
|
|
|
if (retVal) return retVal;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether a node is valid.
|
|
|
|
* @param node The node to validate.
|
|
|
|
* @returns True if the node is valid, false otherwise.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
export function validateNode(node: LayoutNode): boolean {
|
2024-06-04 22:05:44 +02:00
|
|
|
if (!node.children == !node.data) {
|
|
|
|
console.error("Either children or data must be defined for node, not both");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.children?.length === 0) {
|
|
|
|
console.error("Node cannot define an empty array of children");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-08-15 03:40:41 +02:00
|
|
|
* Recursively walk the layout tree starting at the specified node. Run the specified callbacks, if any.
|
|
|
|
* @param node The node from which to start the walk.
|
|
|
|
* @param beforeWalkCallback An optional callback to run before walking a node's children.
|
|
|
|
* @param afterWalkCallback An optional callback to run after walking a node's children.
|
2024-06-04 22:05:44 +02:00
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
export function walkNodes(
|
|
|
|
node: LayoutNode,
|
|
|
|
beforeWalkCallback?: (node: LayoutNode) => void,
|
|
|
|
afterWalkCallback?: (node: LayoutNode) => void
|
|
|
|
) {
|
|
|
|
if (!node) return;
|
|
|
|
beforeWalkCallback?.(node);
|
|
|
|
node.children?.forEach((child) => walkNodes(child, beforeWalkCallback, afterWalkCallback));
|
|
|
|
afterWalkCallback?.(node);
|
2024-06-04 22:05:44 +02:00
|
|
|
}
|
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
/**
|
|
|
|
* Recursively corrects the tree to minimize nested single-child nodes, remove invalid nodes, and correct invalid flex direction order.
|
|
|
|
* @param node The node to start the balancing from.
|
|
|
|
* @param beforeWalkCallback Any optional callback to run before walking a node's children.
|
|
|
|
* @param afterWalkCallback An optional callback to run after walking a node's children.
|
|
|
|
* @returns The corrected node.
|
|
|
|
*/
|
|
|
|
export function balanceNode(
|
|
|
|
node: LayoutNode,
|
|
|
|
beforeWalkCallback?: (node: LayoutNode) => void,
|
|
|
|
afterWalkCallback?: (node: LayoutNode) => void
|
|
|
|
): LayoutNode {
|
|
|
|
walkNodes(
|
|
|
|
node,
|
|
|
|
(node) => {
|
|
|
|
beforeWalkCallback?.(node);
|
|
|
|
if (!validateNode(node)) throw new Error("Invalid node");
|
|
|
|
node.children = node.children?.flatMap((child) => {
|
|
|
|
if (child.flexDirection === node.flexDirection) {
|
|
|
|
child.flexDirection = reverseFlexDirection(node.flexDirection);
|
|
|
|
}
|
|
|
|
if (child.children?.length == 1 && child.children[0].children) {
|
|
|
|
return child.children[0].children;
|
|
|
|
}
|
|
|
|
if (child.children?.length === 0) return;
|
|
|
|
return child;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
(node) => {
|
|
|
|
node.children = node.children?.filter((v) => v);
|
|
|
|
if (node.children?.length === 1 && !node.children[0].children) {
|
|
|
|
node.data = node.children[0].data;
|
|
|
|
node.id = node.children[0].id;
|
|
|
|
node.children = undefined;
|
2024-06-04 22:05:44 +02:00
|
|
|
}
|
2024-08-15 03:40:41 +02:00
|
|
|
afterWalkCallback?.(node);
|
|
|
|
}
|
|
|
|
);
|
2024-06-04 22:05:44 +02:00
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds the first node in the tree where a new node can be inserted.
|
|
|
|
*
|
|
|
|
* This will attempt to fill each node until it has maxChildren children. If a node is full, it will move to its children and
|
|
|
|
* fill each of them until it has maxChildren children. It will ensure that each child fills evenly before moving to the next
|
|
|
|
* layer down.
|
|
|
|
*
|
|
|
|
* @param node The node to start the search from.
|
|
|
|
* @param maxChildren The maximum number of children a node can have.
|
|
|
|
* @returns The node to insert into and the index at which to insert.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
export function findNextInsertLocation(node: LayoutNode, maxChildren: number): { node: LayoutNode; index: number } {
|
2024-06-04 22:05:44 +02:00
|
|
|
const insertLoc = findNextInsertLocationHelper(node, maxChildren, 1);
|
|
|
|
return { node: insertLoc?.node, index: insertLoc?.index };
|
|
|
|
}
|
|
|
|
|
2024-08-01 06:27:46 +02:00
|
|
|
/**
|
|
|
|
* Traverse the layout tree using the supplied index array to find the node to insert at.
|
|
|
|
* @param node The node to start the search from.
|
|
|
|
* @param indexArr The array of indices to aid in the traversal.
|
|
|
|
* @returns The node to insert into and the index at which to insert.
|
|
|
|
*/
|
2024-08-15 03:40:41 +02:00
|
|
|
export function findInsertLocationFromIndexArr(
|
|
|
|
node: LayoutNode,
|
2024-08-01 06:27:46 +02:00
|
|
|
indexArr: number[]
|
2024-08-15 03:40:41 +02:00
|
|
|
): { node: LayoutNode; index: number } {
|
2024-08-01 06:27:46 +02:00
|
|
|
function normalizeIndex(index: number) {
|
|
|
|
const childrenLength = node.children?.length ?? 1;
|
|
|
|
const lastChildIndex = childrenLength - 1;
|
|
|
|
if (index < 0) {
|
|
|
|
return childrenLength - Math.max(index, -childrenLength);
|
|
|
|
}
|
|
|
|
return Math.min(index, lastChildIndex);
|
|
|
|
}
|
|
|
|
if (indexArr.length == 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const nextIndex = normalizeIndex(indexArr.shift());
|
|
|
|
if (indexArr.length == 0 || !node.children) {
|
|
|
|
return { node, index: nextIndex };
|
|
|
|
}
|
2024-08-15 03:40:41 +02:00
|
|
|
return findInsertLocationFromIndexArr(node.children[nextIndex], indexArr);
|
2024-08-01 06:27:46 +02:00
|
|
|
}
|
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
function findNextInsertLocationHelper(
|
|
|
|
node: LayoutNode,
|
2024-06-04 22:05:44 +02:00
|
|
|
maxChildren: number,
|
|
|
|
curDepth: number = 1
|
2024-08-15 03:40:41 +02:00
|
|
|
): { node: LayoutNode; index: number; depth: number } {
|
2024-06-04 22:05:44 +02:00
|
|
|
if (!node) return;
|
|
|
|
if (!node.children) return { node, index: 1, depth: curDepth };
|
2024-08-15 03:40:41 +02:00
|
|
|
let insertLocs: { node: LayoutNode; index: number; depth: number }[] = [];
|
2024-06-04 22:05:44 +02:00
|
|
|
if (node.children.length < maxChildren) {
|
|
|
|
insertLocs.push({ node, index: node.children.length, depth: curDepth });
|
|
|
|
}
|
|
|
|
for (const child of node.children.slice().reverse()) {
|
|
|
|
insertLocs.push(findNextInsertLocationHelper(child, maxChildren, curDepth + 1));
|
|
|
|
}
|
|
|
|
insertLocs = insertLocs
|
|
|
|
.filter((a) => a)
|
|
|
|
.sort((a, b) => Math.pow(a.depth, a.index + maxChildren) - Math.pow(b.depth, b.index + maxChildren));
|
|
|
|
return insertLocs[0];
|
|
|
|
}
|
2024-07-23 02:00:31 +02:00
|
|
|
|
2024-08-15 03:40:41 +02:00
|
|
|
export function totalChildrenSize(node: LayoutNode): number {
|
|
|
|
return node.children?.reduce((partialSum, child) => partialSum + child.size, 0);
|
2024-07-23 02:00:31 +02:00
|
|
|
}
|