2024-06-04 13:05:44 -07:00
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
2024-08-26 12:32:28 -07:00
import { DEFAULT_MAX_CHILDREN } from "./layoutTree" ;
2024-08-21 17:43:11 -07:00
import { DefaultNodeSize , FlexDirection , LayoutNode } from "./types" ;
import { reverseFlexDirection } from "./utils" ;
2024-06-04 13:05:44 -07: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-14 18:40:41 -07:00
export function newLayoutNode (
2024-06-04 13:05:44 -07:00
flexDirection? : FlexDirection ,
size? : number ,
2024-08-14 18:40:41 -07:00
children? : LayoutNode [],
data? : TabLayoutData
) : LayoutNode {
const newNode : LayoutNode = {
2024-06-04 13:05:44 -07:00
id : crypto.randomUUID (),
2024-06-04 16:35:32 -07:00
flexDirection : flexDirection ?? FlexDirection . Row ,
2024-07-03 14:31:02 -07:00
size : size ?? DefaultNodeSize ,
2024-06-04 13:05:44 -07: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-14 18:40:41 -07:00
export function addChildAt ( node : LayoutNode , idx : number , ... children : LayoutNode []) {
2024-08-15 12:24:06 -07:00
// console.log("adding", children, "to", node, "at index", idx);
2024-06-04 13:05:44 -07: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-14 18:40:41 -07:00
export function addIntermediateNode ( node : LayoutNode ) : LayoutNode {
let intermediateNode : LayoutNode ;
2024-06-04 13:05:44 -07: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 14:14:09 -07:00
* @param startingIndex The index in children to start the search from.
2024-06-04 13:05:44 -07:00
* @returns The updated parent node, or undefined if the node was not found.
*/
2024-08-14 18:40:41 -07:00
export function removeChild ( parent : LayoutNode , childToRemove : LayoutNode , startingIndex : number = 0 ) {
2024-06-04 13:05:44 -07:00
if ( ! parent . children ) return ;
2024-06-17 14:14:09 -07:00
const idx = parent . children . indexOf ( childToRemove , startingIndex );
2024-06-04 13:05:44 -07: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-14 18:40:41 -07:00
export function findNode ( node : LayoutNode , id : string ) : LayoutNode | undefined {
2024-08-27 13:48:53 -07:00
if ( ! node ) return ;
2024-06-04 13:05:44 -07: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-14 18:40:41 -07:00
export function findParent ( node : LayoutNode , id : string ) : LayoutNode | undefined {
2024-06-04 13:05:44 -07: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-14 18:40:41 -07:00
export function validateNode ( node : LayoutNode ) : boolean {
2024-06-04 13:05:44 -07: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-14 18:40:41 -07: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 13:05:44 -07:00
*/
2024-08-14 18:40:41 -07: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 13:05:44 -07:00
}
2024-08-14 18:40:41 -07: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 ) => {
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 ;
});
2024-08-27 13:25:02 -07:00
beforeWalkCallback ? .( node );
2024-08-14 18:40:41 -07:00
},
( 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 13:05:44 -07:00
}
2024-08-14 18:40:41 -07:00
afterWalkCallback ? .( node );
}
);
2024-06-04 13:05:44 -07: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-26 12:32:28 -07:00
export function findNextInsertLocation (
node : LayoutNode ,
maxChildren = DEFAULT_MAX_CHILDREN
) : { node : LayoutNode ; index : number } {
2024-06-04 13:05:44 -07:00
const insertLoc = findNextInsertLocationHelper ( node , maxChildren , 1 );
return { node : insertLoc?.node , index : insertLoc?.index };
}
2024-07-31 21:27:46 -07: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-14 18:40:41 -07:00
export function findInsertLocationFromIndexArr (
node : LayoutNode ,
2024-07-31 21:27:46 -07:00
indexArr : number []
2024-08-14 18:40:41 -07:00
) : { node : LayoutNode ; index : number } {
2024-07-31 21:27:46 -07: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-14 18:40:41 -07:00
return findInsertLocationFromIndexArr ( node . children [ nextIndex ], indexArr );
2024-07-31 21:27:46 -07:00
}
2024-08-14 18:40:41 -07:00
function findNextInsertLocationHelper (
node : LayoutNode ,
2024-06-04 13:05:44 -07:00
maxChildren : number ,
curDepth : number = 1
2024-08-14 18:40:41 -07:00
) : { node : LayoutNode ; index : number ; depth : number } {
2024-06-04 13:05:44 -07:00
if ( ! node ) return ;
if ( ! node . children ) return { node , index : 1 , depth : curDepth };
2024-08-14 18:40:41 -07:00
let insertLocs : { node : LayoutNode ; index : number ; depth : number }[] = [];
2024-06-04 13:05:44 -07: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-22 17:00:31 -07:00
2024-08-14 18:40:41 -07:00
export function totalChildrenSize ( node : LayoutNode ) : number {
return node . children ? . reduce (( partialSum , child ) => partialSum + child . size , 0 );
2024-07-22 17:00:31 -07:00
}