agent_core/tui/layout/
template.rs1use 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
11pub enum LayoutTemplate {
16 Standard(StandardOptions),
18
19 Sidebar(SidebarOptions),
21
22 Split(SplitOptions),
24
25 Minimal(MinimalOptions),
27
28 Custom(Box<dyn LayoutProvider>),
30
31 CustomFn(LayoutFn),
33}
34
35impl LayoutTemplate {
36 pub fn standard() -> Self {
40 Self::Standard(StandardOptions::default())
41 }
42
43 pub fn with_panels() -> Self {
45 Self::Standard(StandardOptions::default())
46 }
47
48 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 pub fn minimal() -> Self {
59 Self::Minimal(MinimalOptions::default())
60 }
61
62 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 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 pub fn custom<P: LayoutProvider>(provider: P) -> Self {
84 Self::Custom(Box::new(provider))
85 }
86
87 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 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}