agent_core/tui/layout/
template.rs

1//! Layout template enum and implementations
2
3use ratatui::layout::{Direction, Rect};
4
5use super::minimal::{self, MinimalOptions};
6use super::sidebar::{self, SidebarOptions, SidebarWidth};
7use super::split::{self, SplitOptions};
8use super::standard::{self, StandardOptions};
9use super::types::{LayoutContext, LayoutFn, LayoutProvider, LayoutResult, WidgetSizes};
10
11/// Layout templates with customization options
12///
13/// Templates provide common layout patterns. Use `Custom` or `CustomFn`
14/// for full control with ratatui.
15pub enum LayoutTemplate {
16    /// Standard vertical layout: chat (fills), panels, input, status bar
17    Standard(StandardOptions),
18
19    /// Sidebar layout: main content + sidebar
20    Sidebar(SidebarOptions),
21
22    /// Split layout: two main areas side by side or stacked
23    Split(SplitOptions),
24
25    /// Minimal layout: just chat and input, no status bar
26    Minimal(MinimalOptions),
27
28    /// Custom layout using a LayoutProvider implementation
29    Custom(Box<dyn LayoutProvider>),
30
31    /// Custom layout using a closure
32    CustomFn(LayoutFn),
33}
34
35impl LayoutTemplate {
36    // --- Constructors ---
37
38    /// Create a standard layout with default options
39    pub fn standard() -> Self {
40        Self::Standard(StandardOptions::default())
41    }
42
43    /// Create a standard layout with panels (permission, question, slash popup)
44    pub fn with_panels() -> Self {
45        Self::Standard(StandardOptions::default())
46    }
47
48    /// Create a sidebar layout
49    pub fn with_sidebar(sidebar_widget_id: &'static str, width: impl Into<SidebarWidth>) -> Self {
50        Self::Sidebar(SidebarOptions {
51            sidebar_widget_id,
52            sidebar_width: width.into(),
53            ..Default::default()
54        })
55    }
56
57    /// Create a minimal layout (no status bar, no panels)
58    pub fn minimal() -> Self {
59        Self::Minimal(MinimalOptions::default())
60    }
61
62    /// Create a horizontal split layout
63    pub fn split_horizontal(left_widget_id: &'static str, right_widget_id: &'static str) -> Self {
64        Self::Split(SplitOptions {
65            direction: Direction::Horizontal,
66            first_widget_id: left_widget_id,
67            second_widget_id: right_widget_id,
68            ..Default::default()
69        })
70    }
71
72    /// Create a vertical split layout
73    pub fn split_vertical(top_widget_id: &'static str, bottom_widget_id: &'static str) -> Self {
74        Self::Split(SplitOptions {
75            direction: Direction::Vertical,
76            first_widget_id: top_widget_id,
77            second_widget_id: bottom_widget_id,
78            ..Default::default()
79        })
80    }
81
82    /// Create a custom layout using a LayoutProvider
83    pub fn custom<P: LayoutProvider>(provider: P) -> Self {
84        Self::Custom(Box::new(provider))
85    }
86
87    /// Create a custom layout using a closure
88    pub fn custom_fn<F>(f: F) -> Self
89    where
90        F: Fn(Rect, &LayoutContext, &WidgetSizes) -> LayoutResult + Send + Sync + 'static,
91    {
92        Self::CustomFn(Box::new(f))
93    }
94
95    // --- Compute Layout ---
96
97    /// Compute the layout for the given context
98    pub fn compute(&self, ctx: &LayoutContext, sizes: &WidgetSizes) -> LayoutResult {
99        match self {
100            Self::Standard(opts) => standard::compute(ctx, sizes, opts),
101            Self::Sidebar(opts) => sidebar::compute(ctx, sizes, opts),
102            Self::Split(opts) => split::compute(ctx, sizes, opts),
103            Self::Minimal(opts) => minimal::compute(ctx, sizes, opts),
104            Self::Custom(provider) => provider.compute(ctx, sizes),
105            Self::CustomFn(f) => f(ctx.frame_area, ctx, sizes),
106        }
107    }
108}
109
110impl Default for LayoutTemplate {
111    fn default() -> Self {
112        Self::with_panels()
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::tui::layout::helpers;
120    use crate::tui::themes::Theme;
121    use ratatui::layout::{Constraint, Rect};
122    use std::collections::{HashMap, HashSet};
123
124    fn test_context(area: Rect) -> LayoutContext<'static> {
125        static THEME: std::sync::LazyLock<Theme> = std::sync::LazyLock::new(Theme::default);
126        LayoutContext {
127            frame_area: area,
128            show_throbber: false,
129            input_visual_lines: 1,
130            theme: &THEME,
131            active_widgets: HashSet::new(),
132        }
133    }
134
135    fn test_sizes() -> WidgetSizes {
136        WidgetSizes {
137            heights: HashMap::new(),
138            is_active: HashMap::new(),
139        }
140    }
141
142    #[test]
143    fn test_custom_fn_layout() {
144        let area = Rect::new(0, 0, 80, 24);
145        let ctx = test_context(area);
146        let sizes = test_sizes();
147
148        let template = LayoutTemplate::custom_fn(|area, _ctx, _sizes| {
149            let chunks = helpers::vstack(
150                area,
151                &[Constraint::Percentage(80), Constraint::Percentage(20)],
152            );
153
154            let mut result = LayoutResult::default();
155            result.widget_areas.insert("custom_main", chunks[0]);
156            result.widget_areas.insert("custom_footer", chunks[1]);
157            result.render_order = vec!["custom_main", "custom_footer"];
158            result
159        });
160
161        let result = template.compute(&ctx, &sizes);
162
163        assert!(result.widget_areas.contains_key("custom_main"));
164        assert!(result.widget_areas.contains_key("custom_footer"));
165    }
166}