agent_core/tui/layout/
split.rs

1//! Split layout template
2
3use ratatui::layout::{Constraint, Direction, Layout};
4
5use crate::tui::widgets::widget_ids;
6
7use super::types::{LayoutContext, LayoutResult, WidgetSizes};
8
9/// Options for split layout (two main areas)
10#[derive(Clone)]
11pub struct SplitOptions {
12    /// Direction of the split
13    pub direction: Direction,
14    /// Widget ID for the first (left/top) area
15    pub first_widget_id: &'static str,
16    /// Widget ID for the second (right/bottom) area
17    pub second_widget_id: &'static str,
18    /// How to split the space
19    pub split: SplitRatio,
20    /// Widget ID for input (shared below both areas)
21    pub input_widget_id: &'static str,
22    /// Widget ID for the status bar (None = no status bar)
23    pub status_bar_widget_id: Option<&'static str>,
24}
25
26/// Split ratio specification
27#[derive(Clone)]
28pub enum SplitRatio {
29    /// Equal split (50/50)
30    Equal,
31    /// Percentage for first area (remainder goes to second)
32    Percentage(u16),
33    /// Fixed size for first area
34    FirstFixed(u16),
35    /// Fixed size for second area
36    SecondFixed(u16),
37}
38
39impl Default for SplitOptions {
40    fn default() -> Self {
41        Self {
42            direction: Direction::Horizontal,
43            first_widget_id: widget_ids::CHAT_VIEW,
44            second_widget_id: "secondary",
45            split: SplitRatio::Equal,
46            input_widget_id: widget_ids::TEXT_INPUT,
47            status_bar_widget_id: Some(widget_ids::STATUS_BAR),
48        }
49    }
50}
51
52/// Compute the split layout
53pub fn compute(ctx: &LayoutContext, sizes: &WidgetSizes, opts: &SplitOptions) -> LayoutResult {
54    let mut result = LayoutResult::default();
55    let area = ctx.frame_area;
56
57    // Calculate input height
58    let input_height = if ctx.show_throbber {
59        3
60    } else {
61        (ctx.input_visual_lines as u16).max(1) + 2
62    };
63
64    let status_height = if let Some(status_id) = opts.status_bar_widget_id {
65        if sizes.is_active(status_id) {
66            sizes.height(status_id)
67        } else {
68            0
69        }
70    } else {
71        0
72    };
73
74    // First split: main content vs input/status
75    let v_chunks = Layout::default()
76        .direction(Direction::Vertical)
77        .constraints([
78            Constraint::Min(5),
79            Constraint::Length(input_height),
80            Constraint::Length(status_height),
81        ])
82        .split(area);
83
84    let content_area = v_chunks[0];
85    result.input_area = Some(v_chunks[1]);
86    result.widget_areas.insert(opts.input_widget_id, v_chunks[1]);
87
88    if let Some(status_id) = opts.status_bar_widget_id {
89        if sizes.is_active(status_id) && status_height > 0 {
90            result.widget_areas.insert(status_id, v_chunks[2]);
91            result.render_order.push(status_id);
92        }
93    }
94
95    // Split content area
96    let split_constraint = match opts.split {
97        SplitRatio::Equal => Constraint::Percentage(50),
98        SplitRatio::Percentage(p) => Constraint::Percentage(p),
99        SplitRatio::FirstFixed(w) => Constraint::Length(w),
100        SplitRatio::SecondFixed(_) => Constraint::Min(1), // Second gets fixed below
101    };
102
103    let second_constraint = match opts.split {
104        SplitRatio::SecondFixed(w) => Constraint::Length(w),
105        _ => Constraint::Min(1),
106    };
107
108    let content_chunks = Layout::default()
109        .direction(opts.direction.clone())
110        .constraints([split_constraint, second_constraint])
111        .split(content_area);
112
113    result.widget_areas.insert(opts.first_widget_id, content_chunks[0]);
114    result.widget_areas.insert(opts.second_widget_id, content_chunks[1]);
115
116    result.render_order = vec![
117        opts.first_widget_id,
118        opts.second_widget_id,
119        opts.input_widget_id,
120    ];
121
122    result
123}