import type { MosaicNode } from "react-mosaic-component"; import type { LayoutConfig } from "@/types/app"; /** * Statistics about the layout tree structure */ export interface LayoutStats { /** Number of horizontal splits in the tree */ rowSplits: number; /** Number of vertical splits in the tree */ columnSplits: number; /** Maximum depth of the tree */ depth: number; /** Total number of windows (leaf nodes) */ windowCount: number; } /** * Information about a leaf node in the layout tree */ export interface LeafInfo { /** The window ID (leaf node value) */ leafId: string; /** Depth of this leaf in the tree (root = 0) */ depth: number; /** Direction of the parent split (null if no parent) */ parentDirection: "row" | "column" | null; } /** * Analyzes the layout tree and returns statistics * Used by smart direction algorithm to balance splits */ export function analyzeLayoutStats( node: MosaicNode | null, ): LayoutStats { if (node === null) { return { rowSplits: 0, columnSplits: 0, depth: 0, windowCount: 0 }; } if (typeof node === "string") { // Leaf node (window ID) return { rowSplits: 0, columnSplits: 0, depth: 0, windowCount: 1 }; } // Branch node - recursively analyze children const firstStats = analyzeLayoutStats(node.first); const secondStats = analyzeLayoutStats(node.second); return { rowSplits: firstStats.rowSplits + secondStats.rowSplits + (node.direction === "row" ? 1 : 0), columnSplits: firstStats.columnSplits + secondStats.columnSplits + (node.direction === "column" ? 1 : 0), depth: Math.max(firstStats.depth, secondStats.depth) + 1, windowCount: firstStats.windowCount + secondStats.windowCount, }; } /** * Finds all leaf nodes in the tree with their depth and parent direction */ export function findAllLeaves( node: MosaicNode | null, depth: number = 0, parentDirection: "row" | "column" | null = null, ): LeafInfo[] { if (node === null) { return []; } // Leaf node - return info if (typeof node === "string") { return [{ leafId: node, depth, parentDirection }]; } // Branch node - recursively find leaves in children const leftLeaves = findAllLeaves(node.first, depth + 1, node.direction); const rightLeaves = findAllLeaves(node.second, depth + 1, node.direction); return [...leftLeaves, ...rightLeaves]; } /** * Finds the shallowest leaf in the tree as a heuristic for largest screen space * Accurate for balanced splits (default 50%), may be suboptimal for very unbalanced splits * If multiple leaves at same depth, returns first one encountered (deterministic) */ export function findShallowstLeaf( node: MosaicNode | null, ): LeafInfo | null { const leaves = findAllLeaves(node); if (leaves.length === 0) return null; // Find minimum depth const minDepth = Math.min(...leaves.map((leaf) => leaf.depth)); // Return first leaf at minimum depth return leaves.find((leaf) => leaf.depth === minDepth) || null; } /** * Replaces a specific leaf node with a split containing the leaf + new window * * @param node - Current node in tree traversal * @param targetLeafId - ID of leaf to replace * @param newWindowId - ID of new window to insert * @param direction - Direction of the new split * @param splitPercentage - Split percentage for new split * @param position - Where to place new window ('first' or 'second') * @returns New tree with split at target leaf, or original tree if leaf not found */ export function replaceLeafWithSplit( node: MosaicNode | null, targetLeafId: string, newWindowId: string, direction: "row" | "column", splitPercentage: number, position: "first" | "second" = "second", ): MosaicNode | null { if (node === null) return null; // Leaf node - check if this is our target if (typeof node === "string") { if (node === targetLeafId) { // Found target! Replace with split const [first, second] = position === "first" ? [newWindowId, targetLeafId] // New window first : [targetLeafId, newWindowId]; // New window second (default) return { direction, first, second, splitPercentage, }; } // Not our target, return unchanged return node; } // Branch node - recursively check children const newFirst = replaceLeafWithSplit( node.first, targetLeafId, newWindowId, direction, splitPercentage, position, ); const newSecond = replaceLeafWithSplit( node.second, targetLeafId, newWindowId, direction, splitPercentage, position, ); // Return new branch with potentially updated children return { direction: node.direction, first: newFirst!, second: newSecond!, splitPercentage: node.splitPercentage, }; } /** * Calculates split direction for balanced insertion * Rotates parent direction 90° (row→column, column→row) * This creates a checkerboard pattern for more balanced layouts */ export function calculateBalancedDirection( parentDirection: "row" | "column" | null, ): "row" | "column" { if (parentDirection === null) { return "row"; // Default to horizontal for first split } // Rotate 90 degrees return parentDirection === "row" ? "column" : "row"; } /** * Inserts a new window into the layout tree according to the layout configuration * * Smart mode uses shallowest-leaf algorithm for balanced/dwindle-style layouts: * - Finds the leaf node at minimum depth (heuristic for largest visual space) * - Splits that leaf with direction rotated 90° from parent (row→column, column→row) * - Creates checkerboard-like balanced layouts, progressively equalizing space * - Works best with balanced split percentages (default 50%) * * This is NOT a spiral/fibonacci layout (which maintains a main window). * It creates equal-ish space distribution similar to "dwindle" mode in other WMs. * * @param currentLayout - The current layout tree (null if no windows yet) * @param newWindowId - The ID of the new window to insert * @param config - Layout configuration specifying how to insert the window * @returns The new layout tree with the window inserted */ export function insertWindow( currentLayout: MosaicNode | null, newWindowId: string, config: LayoutConfig, ): MosaicNode { // First window - just return the window ID as leaf node if (currentLayout === null) { return newWindowId; } // Smart mode: Use improved shallowest-leaf algorithm if (config.insertionMode === "smart") { // Find shallowest leaf to split (largest screen space) const leafInfo = findShallowstLeaf(currentLayout); if (!leafInfo) { // Shouldn't happen, but fallback to simple root insertion console.warn("[Layout] No leaf found, falling back to root insertion"); return { direction: "row", first: currentLayout, second: newWindowId, splitPercentage: config.splitPercentage, }; } // Determine split direction by rotating parent's direction const direction = calculateBalancedDirection(leafInfo.parentDirection); // Replace that leaf with a split const newLayout = replaceLeafWithSplit( currentLayout, leafInfo.leafId, newWindowId, direction, config.splitPercentage, config.insertionPosition, ); return newLayout || newWindowId; // Fallback if replacement failed } // Row/Column modes: Simple root-level wrapping (old behavior) const direction = config.insertionMode === "row" ? "row" : config.insertionMode === "column" ? "column" : "row"; // Determine which side gets the new window const [firstNode, secondNode] = config.insertionPosition === "first" ? [newWindowId, currentLayout] // New window on left/top : [currentLayout, newWindowId]; // New window on right/bottom (default) // Create split node with new window return { direction, first: firstNode, second: secondNode, splitPercentage: config.splitPercentage, }; }