aiprompts/layout.md
The Wave Terminal layout system is a sophisticated tile-based layout engine built with React, TypeScript, and Jotai state management. It provides a flexible, drag-and-drop interface for arranging terminal blocks and other content in complex layouts.
The layout system manages a tree of LayoutNode objects that represent the hierarchical structure of content. Each node can either be:
The system uses CSS Flexbox for positioning but maintains its own tree structure for state management, drag-and-drop operations, and complex layout manipulations.
frontend/layout/lib/
├── TileLayout.tsx # Main React component
├── layoutAtom.ts # Jotai state management
├── layoutModel.ts # Core model class
├── layoutModelHooks.ts # React hooks for integration
├── layoutNode.ts # Node manipulation functions
├── layoutTree.ts # Tree operation functions
├── nodeRefMap.ts # DOM reference tracking
├── types.ts # Type definitions
├── utils.ts # Utility functions
└── tilelayout.scss # Styling
The fundamental building block of the layout system:
interface LayoutNode {
id: string; // Unique identifier
data?: TabLayoutData; // Content data (only for leaf nodes)
children?: LayoutNode[]; // Child nodes (only for containers)
flexDirection: FlexDirection; // "row" or "column"
size: number; // Flex size (0-100)
}
Key Rules:
data OR children must be defined, never bothdata, container nodes have childrenflexDirection that determines layout axissize represents the relative flex size within the parentThe complete state of the layout:
interface LayoutTreeState {
rootNode: LayoutNode; // Root of the tree
focusedNodeId?: string; // Currently focused node
magnifiedNodeId?: string; // Currently magnified node
leafOrder?: LeafOrderEntry[]; // Computed leaf ordering
pendingBackendActions: LayoutActionData[]; // Actions from backend
generation: number; // State version number
}
Generation System:
Runtime model for individual nodes, providing React-friendly state:
interface NodeModel {
additionalProps: Atom<LayoutNodeAdditionalProps>;
innerRect: Atom<CSSProperties>;
blockNum: Atom<number>;
nodeId: string;
blockId: string;
isFocused: Atom<boolean>;
isMagnified: Atom<boolean>;
isEphemeral: Atom<boolean>;
toggleMagnify: () => void;
focusNode: () => void;
onClose: () => void;
dragHandleRef?: React.RefObject<HTMLDivElement>;
// ... additional state and methods
}
The central orchestrator that manages the entire layout system:
Key Responsibilities:
State Management:
class LayoutModel {
treeStateAtom: WritableLayoutTreeStateAtom; // Persistent state
leafs: PrimitiveAtom<LayoutNode[]>; // Computed leaf nodes
additionalProps: PrimitiveAtom<Record<string, LayoutNodeAdditionalProps>>;
pendingTreeAction: AtomWithThrottle<LayoutTreeAction>;
activeDrag: PrimitiveAtom<boolean>;
// ... many more atoms for different aspects
}
Action Processing: The model uses a reducer pattern to process actions:
treeReducer(action: LayoutTreeAction) {
switch (action.type) {
case LayoutTreeActionType.Move:
moveNode(this.treeState, action);
break;
case LayoutTreeActionType.InsertNode:
insertNode(this.treeState, action);
break;
// ... handle all action types
}
this.updateTree(); // Recompute derived state
}
The system uses a comprehensive action system for all modifications:
enum LayoutTreeActionType {
ComputeMove = "computemove", // Preview move operation
Move = "move", // Execute move
Swap = "swap", // Swap two nodes
ResizeNode = "resize", // Resize node(s)
InsertNode = "insert", // Insert new node
InsertNodeAtIndex = "insertatindex", // Insert at specific index
DeleteNode = "delete", // Remove node
FocusNode = "focus", // Change focus
MagnifyNodeToggle = "magnify", // Toggle magnification
SplitHorizontal = "splithorizontal", // Split horizontally
SplitVertical = "splitvertical", // Split vertically
// ... more actions
}
LayoutTreeState// 1. Compute operation during drag
const computeAction: LayoutTreeComputeMoveNodeAction = {
type: LayoutTreeActionType.ComputeMove,
nodeId: targetNodeId,
nodeToMoveId: draggedNodeId,
direction: DropDirection.Right
};
// 2. Execute on drop
const moveAction: LayoutTreeMoveNodeAction = {
type: LayoutTreeActionType.Move,
parentId: newParentId,
index: insertIndex,
node: nodeToMove
};
The layout system implements a sophisticated drag-and-drop interface using react-dnd.
When dragging over a node, the system determines drop direction based on cursor position:
enum DropDirection {
Top = 0, Right = 1, Bottom = 2, Left = 3,
OuterTop = 4, OuterRight = 5, OuterBottom = 6, OuterLeft = 7,
Center = 8
}
Drop Zones:
The system generates drag previews by:
html-to-imageResize handles are dynamically positioned between adjacent nodes:
interface ResizeHandleProps {
id: string;
parentNodeId: string;
parentIndex: number;
centerPx: number; // Handle position
transform: CSSProperties; // CSS positioning
flexDirection: FlexDirection; // Handle orientation
}
The system computes absolute positions from the tree structure:
updateTreeHelper() - Main layout computationcomputeNodeFromProps() - Individual node positioningsetTransform() - CSS transform generationThe layoutNode.ts file provides core node manipulation:
// Create new node
newLayoutNode(flexDirection?, size?, children?, data?)
// Tree traversal
findNode(node, id)
findParent(node, id)
walkNodes(node, beforeCallback?, afterCallback?)
// Modifications
addChildAt(node, index, ...children)
removeChild(parent, childToRemove)
balanceNode(node) // Optimize tree structure
The system automatically optimizes the tree structure:
The layout state synchronizes with the backend through:
layoutAtom.ts - Jotai atom that wraps backend stateconst layoutTreeStateAtom = atom(
(get) => {
// Read from backend
const layoutState = get(backendLayoutStateAtom);
return transformToTreeState(layoutState);
},
(get, set, treeState) => {
// Write to backend
if (generationNewer(treeState)) {
set(backendLayoutStateAtom, transformFromTreeState(treeState));
}
}
);
Nodes can be magnified to take up the full layout space:
Temporary nodes that aren't part of the persistent tree:
Hooks:
useTileLayout() - Main hook for layout setupuseNodeModel() - Get node model for componentuseDebouncedNodeInnerRect() - Animated positioningThe layout system is content-agnostic through render callbacks:
interface TileLayoutContents {
renderContent: (nodeModel: NodeModel) => React.ReactNode;
renderPreview?: (nodeModel: NodeModel) => React.ReactElement;
onNodeDelete?: (data: TabLayoutData) => Promise<void>;
}
React.memo() and useMemo()splitAtom() for efficient array updatestypes.tslayoutTree.tsLayoutModel.treeReducer()updateTree()LayoutNodeAdditionalProps in types.tsupdateTreeHelper()nodeModel.additionalPropsOverride or extend layout computation by:
computeNodeFromProps()The system includes extensive validation:
The layout system includes comprehensive tests:
layoutNode.test.ts - Node operationslayoutTree.test.ts - Tree operationsutils.test.ts - Utility functionsFor debugging layout issues:
treeState.generation for state changesadditionalProps for computed layout dataThe layout system is complex but well-structured, providing a powerful foundation for Wave Terminal's dynamic layout capabilities.