Overview
Jarvis uses a binary split tree to manage pane layout within the application window. Every visible pane occupies a leaf node in the tree, and interior nodes describe how their two children are divided — either horizontally (side by side) or vertically (top and bottom).
The tiling subsystem (jarvis-tiling crate) is intentionally decoupled from rendering and platform windowing, making it a pure-logic system that can be tested independently.
Binary Split Tree
Data Model
pub enum SplitNode {
Leaf {
pane_id : u32
},
Split {
direction : Direction , // Horizontal or Vertical
ratio : f64 , // 0.0 .. 1.0, space for first child
first : Box < SplitNode >,
second : Box < SplitNode >,
},
}
pub enum Direction {
Horizontal , // Children side by side (left | right)
Vertical , // Children stacked (top / bottom)
}
Visual Example
This tree produces:
+------------------+------------------+
| | |
| | Pane 2 |
| Pane 1 | |
| +------------------+
| | |
| | Pane 3 |
| | |
+------------------+------------------+
Core Operations
Splitting
Closing Panes
Focus Management
Zoom Mode
Resizing
Keyboard Resize
Adjust split ratios with arrow keys:
pub fn resize ( & mut self , direction : Direction , delta : i32 ) -> bool {
let delta_f = delta as f64 * 0.05 ; // 5% per step
self . tree . adjust_ratio ( self . focused, delta_f )
}
Example :
Initial ratio: 0.5 (50% / 50%)
Resize right (+1): 0.55 (55% / 45%)
Resize right (+1): 0.60 (60% / 40%)
Resize left (-1): 0.55 (55% / 45%)
Ratios are clamped to [0.1, 0.9] to prevent invisible panes.
Mouse Drag Resize
Split Border Structure
pub struct SplitBorder {
pub direction : Direction , // Axis the split divides
pub position : f64 , // Pixel position of divider
pub start : f64 , // Start of divider span
pub end : f64 , // End of divider span
pub first_pane : u32 , // Pane ID from first subtree
pub second_pane : u32 , // Pane ID from second subtree
pub bounds : Rect , // Bounding rect of split region
}
Each split node produces one border. The compute_borders() function walks the tree recursively.
Hit Testing
const HIT_ZONE : f64 = 6.0 ; // 6 pixels on each side
for border in borders {
let dist = match border . direction {
Horizontal => ( cursor . x - border . position) . abs (),
Vertical => ( cursor . y - border . position) . abs (),
};
if dist < HIT_ZONE && cursor_in_span ( border ) {
return Some ( border );
}
}
Swap Panes
Exchange positions with a neighbor:
pub fn swap ( & mut self , direction : Direction ) -> bool {
if let Some ( neighbor ) = self . tree . find_neighbor ( self . focused, direction ) {
self . tree . swap_panes ( self . focused, neighbor )
} else {
false
}
}
Visual Example :
Before swap_panes(1, 2):
Split(H, 0.5)
/ \\
Leaf(1) Leaf(2)
After:
Split(H, 0.5)
/ \\
Leaf(2) Leaf(1)
The tree structure (directions and ratios) is preserved; only pane IDs swap.
Layout Engine
Algorithm
Converts the split tree into pixel rectangles:
Calculation Details
Visual Example: Gap and Padding
Viewport: 800 x 600
outer_padding = 10
gap = 8
+---------- 800 ----------+
| padding = 10 |
| +--- 780 x 580 ------+ |
| | | gap | | |
| | Pane 1 | (8px) | P2 | |
| | 386 | |386 | |
| +--------+-------+----+ |
| |
+--------------------------+
Available = 780 - 8 = 772
Each pane = 772 * 0.5 = 386
Configuration
pub struct LayoutEngine {
pub gap : u32 , // Pixels between panes
pub outer_padding : u32 , // Screen-edge padding
pub min_pane_size : f64 , // Minimum dimension (default 50.0)
}
[ layout ]
panel_gap = 6 # 0-20 pixels
outer_padding = 0 # 0-40 pixels
Pane Stacks (Tabs)
A single leaf position can host multiple panes as a stack (tabbed interface).
Structure
pub struct PaneStack {
panes : Vec < u32 >, // Ordered list of pane IDs
active_index : usize , // Index of visible pane
}
Operations
TilingManager Integration
// Add a new chat pane to the current stack
tiling . push_to_stack ( PaneKind :: Chat , "Chat Room" );
// Cycle through tabs
tiling . cycle_stack_next ();
tiling . cycle_stack_prev ();
TilingManager API
State
pub struct TilingManager {
tree : SplitNode , // Split tree root
panes : HashMap < u32 , Pane >, // Pane registry
stacks : HashMap < u32 , PaneStack >, // Tab stacks at leaves
focused : u32 , // Currently focused pane
zoomed : Option < u32 >, // Zoomed pane (if any)
layout_engine : LayoutEngine , // Gap, padding, min size
next_id : u32 , // Auto-incrementing ID
}
Initialization
// Create manager with one terminal pane
let mut tiling = TilingManager :: new ();
// Or with custom layout engine
let engine = LayoutEngine {
gap : 8 ,
outer_padding : 10 ,
min_pane_size : 100.0 ,
};
let mut tiling = TilingManager :: with_layout ( engine );
Accessors
Method Returns focused_id()Currently focused pane ID is_zoomed()Whether zoom mode is active zoomed_id()Some(id) of zoomed panepane_count()Total number of panes pane(id)Option<&Pane> for given IDtree()Reference to split tree root gap()Current inter-pane gap outer_padding()Current outer padding ordered_pane_ids()Pane IDs in visual order (DFS)
Command Dispatch
pub enum TilingCommand {
SplitHorizontal ,
SplitVertical ,
Close ,
Resize ( Direction , i32 ),
Swap ( Direction ),
FocusNext ,
FocusPrev ,
FocusDirection ( Direction ),
Zoom ,
}
// Execute any command
tiling . execute ( TilingCommand :: SplitHorizontal );
tiling . execute ( TilingCommand :: Resize ( Direction :: Horizontal , 2 ));
WebView Bounds Synchronization
Each pane corresponds to a wry WebView that needs position/size updates.
Sync Pipeline
When Sync Triggers
NewPane (split + create WebView)
ClosePane (remove pane + destroy WebView)
SplitHorizontal / SplitVertical
ZoomPane (toggle zoom)
ResizePane (keyboard resize)
SwapPane
Mouse drag resize (every cursor move)
Window resize events
Coordinate Conversion
pub fn tiling_rect_to_wry ( rect : & Rect ) -> wry :: Rect {
wry :: Rect {
position : Position :: Logical ( LogicalPosition :: new ( rect . x, rect . y)),
size : Size :: Logical ( LogicalSize :: new ( rect . width, rect . height)),
}
}
UI Chrome Integration
Content Rect Calculation
The tiling viewport excludes UI chrome:
// Get logical window size
let ( w , h ) = get_logical_window_size ();
// Subtract tab bar and status bar
let content = ui_chrome . content_rect ( w , h );
// Use as tiling viewport
let layout = tiling . compute_layout ( content );
Example
Window: 1280 x 800
Tab bar: 32px
Status bar: 24px
Content rect:
x: 0
y: 32
width: 1280
height: 744 (800 - 32 - 24)
The platform module provides a WindowManager trait for external app windows:
pub trait WindowManager : Send + Sync {
fn list_windows ( & self ) -> Result < Vec < ExternalWindow >>;
fn set_window_frame ( & self , window_id : WindowId , frame : Rect ) -> Result <()>;
fn focus_window ( & self , window_id : WindowId ) -> Result <()>;
fn set_minimized ( & self , window_id : WindowId , minimized : bool ) -> Result <()>;
fn watch_windows (
& self ,
callback : Box < dyn Fn ( WindowEvent ) + Send >
) -> Result < WatchHandle >;
}
macOS : CoreGraphics-based implementation
Windows / Linux : Stub implementations (returns NoopWindowManager)
Usage
let window_mgr = create_window_manager () ? ;
// List all windows
for window in window_mgr . list_windows () ? {
println! ( "{}: {}" , window . id, window . title);
}
// Tile an external window
let rect = Rect { x : 0.0 , y : 0.0 , width : 800.0 , height : 600.0 };
window_mgr . set_window_frame ( window_id , rect ) ? ;
Configuration Reference
[ layout ]
panel_gap = 6 # Pixels between panes (0-20)
border_radius = 8 # Corner rounding (0-20)
padding = 10 # Inner pane padding (0-40)
max_panels = 5 # Max simultaneous panels (1-10)
default_panel_width = 0.72 # Default width fraction (0.3-1.0)
scrollbar_width = 3 # Scrollbar width (1-10)
border_width = 0.0 # Pane border width (0.0-3.0)
outer_padding = 0 # Screen-edge padding (0-40)
inactive_opacity = 1.0 # Opacity for unfocused panes (0.0-1.0)
[ opacity ]
background = 1.0
panel = 0.85
orb = 1.0
hex_grid = 0.8
hud = 1.0
Testing
The tiling crate has comprehensive unit tests:
# Run all tiling tests
cargo test -p jarvis-tiling
# Run specific test module
cargo test -p jarvis-tiling tree::tests
cargo test -p jarvis-tiling layout::tests
Test Coverage
Tree operations (split, remove, swap, ratio adjustment, traversal)
Layout computation (single pane, splits with gap, nested splits)
Border computation and hit testing
Stack operations (push, remove, cycle, serialization)
Manager integration (split/close lifecycle, focus cycling, zoom)
Next Steps