Skip to main content

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

MethodReturns
focused_id()Currently focused pane ID
is_zoomed()Whether zoom mode is active
zoomed_id()Some(id) of zoomed pane
pane_count()Total number of panes
pane(id)Option<&Pane> for given ID
tree()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)

Platform Window Management

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>;
}

Platform Support

  • 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

Renderer

GPU rendering pipeline and visual effects

Keybindings

Configure tiling keybindings

Configuration

Complete TOML configuration reference

Crates

Detailed crate documentation

Build docs developers (and LLMs) love