agent_core/tui/layout/
sidebar.rs

1//! Sidebar layout template
2
3use ratatui::layout::{Constraint, Direction, Layout};
4
5use super::standard::{self, StandardOptions};
6use super::types::{LayoutContext, LayoutResult, WidgetSizes};
7
8/// Options for sidebar layout
9#[derive(Clone)]
10pub struct SidebarOptions {
11    /// Options for the main content area
12    pub main_options: StandardOptions,
13    /// Widget ID for the sidebar
14    pub sidebar_widget_id: &'static str,
15    /// Width of the sidebar
16    pub sidebar_width: SidebarWidth,
17    /// Position of the sidebar
18    pub sidebar_position: SidebarPosition,
19}
20
21/// Sidebar width specification
22#[derive(Clone)]
23pub enum SidebarWidth {
24    /// Fixed width in columns
25    Fixed(u16),
26    /// Percentage of total width
27    Percentage(u16),
28    /// Minimum width (sidebar gets this, main gets rest)
29    Min(u16),
30}
31
32impl From<u16> for SidebarWidth {
33    fn from(width: u16) -> Self {
34        Self::Fixed(width)
35    }
36}
37
38/// Sidebar position
39#[derive(Clone, Copy, Default)]
40pub enum SidebarPosition {
41    Left,
42    #[default]
43    Right,
44}
45
46impl Default for SidebarOptions {
47    fn default() -> Self {
48        Self {
49            main_options: StandardOptions::default(),
50            sidebar_widget_id: "sidebar",
51            sidebar_width: SidebarWidth::Fixed(30),
52            sidebar_position: SidebarPosition::Right,
53        }
54    }
55}
56
57/// Compute the sidebar layout
58pub fn compute(ctx: &LayoutContext, sizes: &WidgetSizes, opts: &SidebarOptions) -> LayoutResult {
59    let area = ctx.frame_area;
60
61    // Compute sidebar width constraint
62    let sidebar_constraint = match opts.sidebar_width {
63        SidebarWidth::Fixed(w) => Constraint::Length(w),
64        SidebarWidth::Percentage(p) => Constraint::Percentage(p),
65        SidebarWidth::Min(w) => Constraint::Min(w),
66    };
67
68    // Split horizontally
69    let h_constraints = match opts.sidebar_position {
70        SidebarPosition::Left => vec![sidebar_constraint, Constraint::Min(1)],
71        SidebarPosition::Right => vec![Constraint::Min(1), sidebar_constraint],
72    };
73
74    let h_chunks = Layout::default()
75        .direction(Direction::Horizontal)
76        .constraints(h_constraints)
77        .split(area);
78
79    let (main_area, sidebar_area) = match opts.sidebar_position {
80        SidebarPosition::Left => (h_chunks[1], h_chunks[0]),
81        SidebarPosition::Right => (h_chunks[0], h_chunks[1]),
82    };
83
84    // Compute main area layout using standard layout
85    let main_ctx = LayoutContext {
86        frame_area: main_area,
87        show_throbber: ctx.show_throbber,
88        input_visual_lines: ctx.input_visual_lines,
89        theme: ctx.theme,
90        active_widgets: ctx.active_widgets.clone(),
91    };
92    let mut result = standard::compute(&main_ctx, sizes, &opts.main_options);
93
94    // Add sidebar
95    result.widget_areas.insert(opts.sidebar_widget_id, sidebar_area);
96    // Insert sidebar at beginning of render order (renders first, behind main)
97    result.render_order.insert(0, opts.sidebar_widget_id);
98
99    result
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::tui::themes::Theme;
106    use crate::tui::widgets::widget_ids;
107    use ratatui::layout::Rect;
108    use std::collections::{HashMap, HashSet};
109
110    fn test_context(area: Rect) -> LayoutContext<'static> {
111        static THEME: std::sync::LazyLock<Theme> = std::sync::LazyLock::new(Theme::default);
112        LayoutContext {
113            frame_area: area,
114            show_throbber: false,
115            input_visual_lines: 1,
116            theme: &THEME,
117            active_widgets: HashSet::new(),
118        }
119    }
120
121    fn test_sizes() -> WidgetSizes {
122        WidgetSizes {
123            heights: HashMap::new(),
124            is_active: HashMap::new(),
125        }
126    }
127
128    #[test]
129    fn test_sidebar_layout() {
130        let area = Rect::new(0, 0, 100, 24);
131        let ctx = test_context(area);
132        let sizes = test_sizes();
133
134        let opts = SidebarOptions {
135            sidebar_widget_id: "file_browser",
136            sidebar_width: SidebarWidth::Fixed(30),
137            ..Default::default()
138        };
139
140        let result = compute(&ctx, &sizes, &opts);
141
142        assert!(result.widget_areas.contains_key(widget_ids::CHAT_VIEW));
143        assert!(result.widget_areas.contains_key("file_browser"));
144
145        let sidebar_area = result.widget_areas.get("file_browser").unwrap();
146        assert_eq!(sidebar_area.width, 30);
147    }
148}