13 KiB
Wave Terminal Layout System Architecture
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.
Overview
The layout system manages a tree of LayoutNode objects that represent the hierarchical structure of content. Each node can either be:
- Leaf node: Contains actual content (block data)
- Container node: Contains child nodes with a specific flex direction
The system uses CSS Flexbox for positioning but maintains its own tree structure for state management, drag-and-drop operations, and complex layout manipulations.
Core Architecture
File Structure
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
Key Data Structures
LayoutNode
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:
- Either
dataORchildrenmust be defined, never both - Leaf nodes have
data, container nodes havechildren - All nodes have a
flexDirectionthat determines layout axis sizerepresents the relative flex size within the parent
LayoutTreeState
The 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:
- Incremented on every state change
- Used for optimistic updates and conflict resolution
- Prevents stale state overwrites
NodeModel
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
}
Core Classes
LayoutModel
The central orchestrator that manages the entire layout system:
Key Responsibilities:
- Maintains tree state through Jotai atoms
- Processes layout actions (move, resize, insert, delete)
- Computes layout positions and transforms
- Manages drag-and-drop operations
- Handles resize operations
- Provides node models for React components
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
}
Layout Actions
The system uses a comprehensive action system for all modifications:
Action Types
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
}
Action Flow
- User Interaction → Action triggered
- Action Validation → Check if operation is valid
- Tree Modification → Update
LayoutTreeState - State Propagation → Update Jotai atoms
- Layout Computation → Recalculate positions
- React Re-render → Update UI
Example: Move Operation
// 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
};
Drag and Drop System
The layout system implements a sophisticated drag-and-drop interface using react-dnd.
Drop Direction Logic
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:
- Inner zones (Top/Right/Bottom/Left): Insert within the target node
- Outer zones: Insert in the target's parent
- Center: Swap nodes
Drag Preview
The system generates drag previews by:
- Rendering content to an off-screen element
- Converting to PNG using
html-to-image - Using the image as the drag preview
Resize System
Resize Handles
Resize 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
}
Resize Operation
- Handle Drag Start → Store resize context
- Drag Move → Compute new sizes based on cursor position
- Throttled Updates → Update node sizes (10ms throttle)
- Drag End → Commit final sizes
Layout Computation
The system computes absolute positions from the tree structure:
Process
- Tree Walk → Traverse from root to leaves
- Flexbox Simulation → Calculate container and child sizes
- Position Calculation → Compute absolute positions
- Transform Generation → Create CSS transforms
- Handle Positioning → Place resize handles between nodes
Key Functions
updateTreeHelper()- Main layout computationcomputeNodeFromProps()- Individual node positioningsetTransform()- CSS transform generation
Node Management
Node Operations
The 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
Tree Balancing
The system automatically optimizes the tree structure:
- Removes unnecessary intermediate nodes
- Flattens single-child containers
- Ensures valid flex directions
State Synchronization
Frontend ↔ Backend Sync
The layout state synchronizes with the backend through:
layoutAtom.ts- Jotai atom that wraps backend state- Generation tracking - Prevents state conflicts
- Pending actions - Backend-initiated changes
- Leaf order - Frontend-computed ordering sent to backend
Atom Structure
const 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));
}
}
);
Special Features
Magnification
Nodes can be magnified to take up the full layout space:
- Magnified nodes appear above others (higher z-index)
- Only one node can be magnified at a time
- Animation smoothly transitions between normal and magnified states
Ephemeral Nodes
Temporary nodes that aren't part of the persistent tree:
- Used for preview/temporary content
- Automatically cleaned up
- Appear above the normal layout
Focus Management
- One node can be focused at a time
- Focus affects keyboard navigation
- Integrates with the terminal's block focus system
Integration Points
React Integration
Hooks:
useTileLayout()- Main hook for layout setupuseNodeModel()- Get node model for componentuseDebouncedNodeInnerRect()- Animated positioning
Content Rendering
The 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>;
}
Performance Optimizations
- Memoization - Extensive use of
React.memo()anduseMemo() - Throttling - Resize and drag operations throttled to 10-50ms
- Transform-based positioning - Uses CSS transforms for performance
- Split atoms - Jotai
splitAtom()for efficient array updates - Selective re-rendering - Only affected components re-render
Common Patterns
Adding New Actions
- Define action type in
types.ts - Implement handler in
layoutTree.ts - Add case to
LayoutModel.treeReducer() - Update generation and call
updateTree()
Extending Node Properties
- Add to
LayoutNodeAdditionalPropsintypes.ts - Compute in
updateTreeHelper() - Access via
nodeModel.additionalProps
Custom Layout Behaviors
Override or extend layout computation by:
- Modifying
computeNodeFromProps() - Adding custom CSS transforms
- Implementing special handling in action reducers
Error Handling
The system includes extensive validation:
- Node structure validation
- Action parameter checking
- Tree consistency checks
- Graceful degradation on errors
Testing
The layout system includes comprehensive tests:
layoutNode.test.ts- Node operationslayoutTree.test.ts- Tree operationsutils.test.ts- Utility functions
Debugging
For debugging layout issues:
- Check
treeState.generationfor state changes - Inspect
additionalPropsfor computed layout data - Use browser dev tools to examine CSS transforms
- Enable console logging in action reducers
The layout system is complex but well-structured, providing a powerful foundation for Wave Terminal's dynamic layout capabilities.