Skip to main content

agent_air_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
96        .widget_areas
97        .insert(opts.sidebar_widget_id, sidebar_area);
98    // Insert sidebar at beginning of render order (renders first, behind main)
99    result.render_order.insert(0, opts.sidebar_widget_id);
100
101    result
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::themes::Theme;
108    use crate::widgets::widget_ids;
109    use ratatui::layout::Rect;
110    use std::collections::{HashMap, HashSet};
111
112    fn test_context(area: Rect) -> LayoutContext<'static> {
113        static THEME: std::sync::LazyLock<Theme> = std::sync::LazyLock::new(Theme::default);
114        LayoutContext {
115            frame_area: area,
116            show_throbber: false,
117            input_visual_lines: 1,
118            theme: &THEME,
119            active_widgets: HashSet::new(),
120        }
121    }
122
123    fn test_sizes() -> WidgetSizes {
124        WidgetSizes {
125            heights: HashMap::new(),
126            is_active: HashMap::new(),
127        }
128    }
129
130    #[test]
131    fn test_sidebar_layout() {
132        let area = Rect::new(0, 0, 100, 24);
133        let ctx = test_context(area);
134        let sizes = test_sizes();
135
136        let opts = SidebarOptions {
137            sidebar_widget_id: "file_browser",
138            sidebar_width: SidebarWidth::Fixed(30),
139            ..Default::default()
140        };
141
142        let result = compute(&ctx, &sizes, &opts);
143
144        assert!(result.widget_areas.contains_key(widget_ids::CHAT_VIEW));
145        assert!(result.widget_areas.contains_key("file_browser"));
146
147        let sidebar_area = result.widget_areas.get("file_browser").unwrap();
148        assert_eq!(sidebar_area.width, 30);
149    }
150}