agent_core_tui/layout/
standard.rs1use ratatui::layout::{Constraint, Direction, Layout};
4
5use crate::widgets::widget_ids;
6
7use super::types::{LayoutContext, LayoutResult, WidgetSizes};
8
9#[derive(Clone)]
11pub struct StandardOptions {
12 pub main_widget_id: &'static str,
14 pub input_widget_id: &'static str,
16 pub panel_widget_ids: Vec<&'static str>,
18 pub popup_widget_ids: Vec<&'static str>,
20 pub overlay_widget_ids: Vec<&'static str>,
22 pub min_main_height: u16,
24 pub fixed_input_height: Option<u16>,
26 pub status_bar_widget_id: Option<&'static str>,
28}
29
30impl Default for StandardOptions {
31 fn default() -> Self {
32 Self {
33 main_widget_id: widget_ids::CHAT_VIEW,
34 input_widget_id: widget_ids::TEXT_INPUT,
35 panel_widget_ids: vec![
36 widget_ids::PERMISSION_PANEL,
37 widget_ids::BATCH_PERMISSION_PANEL,
38 widget_ids::QUESTION_PANEL,
39 ],
40 popup_widget_ids: vec![widget_ids::SLASH_POPUP],
41 overlay_widget_ids: vec![
42 widget_ids::THEME_PICKER,
43 widget_ids::SESSION_PICKER,
44 ],
45 min_main_height: 5,
46 fixed_input_height: None,
47 status_bar_widget_id: Some(widget_ids::STATUS_BAR),
48 }
49 }
50}
51
52pub fn compute(ctx: &LayoutContext, sizes: &WidgetSizes, opts: &StandardOptions) -> LayoutResult {
54 let mut result = LayoutResult::default();
55 let area = ctx.frame_area;
56
57 let input_height = opts.fixed_input_height.unwrap_or_else(|| {
59 if ctx.show_throbber {
60 3
61 } else {
62 (ctx.input_visual_lines as u16).max(1) + 2
63 }
64 });
65
66 let panel_height: u16 = opts
68 .panel_widget_ids
69 .iter()
70 .filter(|id| sizes.is_active(id))
71 .map(|id| sizes.height(id))
72 .sum();
73
74 let popup_height: u16 = opts
76 .popup_widget_ids
77 .iter()
78 .filter(|id| sizes.is_active(id))
79 .map(|id| sizes.height(id))
80 .sum();
81
82 let mut constraints = vec![Constraint::Min(opts.min_main_height)]; if panel_height > 0 {
86 constraints.push(Constraint::Length(panel_height));
87 }
88
89 if popup_height > 0 {
90 constraints.push(Constraint::Length(popup_height));
91 }
92
93 constraints.push(Constraint::Length(input_height)); if let Some(status_id) = opts.status_bar_widget_id {
97 if sizes.is_active(status_id) {
98 constraints.push(Constraint::Length(sizes.height(status_id)));
99 }
100 }
101
102 let chunks = Layout::default()
104 .direction(Direction::Vertical)
105 .constraints(constraints)
106 .split(area);
107
108 let mut chunk_idx = 0;
110
111 result.widget_areas.insert(opts.main_widget_id, chunks[chunk_idx]);
113 result.render_order.push(opts.main_widget_id);
114 chunk_idx += 1;
115
116 if panel_height > 0 {
118 let active_panels: Vec<_> = opts
119 .panel_widget_ids
120 .iter()
121 .filter(|id| sizes.is_active(id))
122 .collect();
123
124 if active_panels.len() == 1 {
125 result.widget_areas.insert(active_panels[0], chunks[chunk_idx]);
126 result.render_order.push(active_panels[0]);
127 } else if !active_panels.is_empty() {
128 let panel_constraints: Vec<_> = active_panels
130 .iter()
131 .map(|id| Constraint::Length(sizes.height(id)))
132 .collect();
133 let panel_chunks = Layout::default()
134 .direction(Direction::Vertical)
135 .constraints(panel_constraints)
136 .split(chunks[chunk_idx]);
137
138 for (i, id) in active_panels.iter().enumerate() {
139 result.widget_areas.insert(id, panel_chunks[i]);
140 result.render_order.push(id);
141 }
142 }
143 chunk_idx += 1;
144 }
145
146 if popup_height > 0 {
148 let active_popups: Vec<_> = opts
149 .popup_widget_ids
150 .iter()
151 .filter(|id| sizes.is_active(id))
152 .collect();
153
154 for id in active_popups {
155 result.widget_areas.insert(id, chunks[chunk_idx]);
156 result.render_order.push(id);
157 }
158 chunk_idx += 1;
159 }
160
161 result.widget_areas.insert(opts.input_widget_id, chunks[chunk_idx]);
163 result.input_area = Some(chunks[chunk_idx]);
164 result.render_order.push(opts.input_widget_id);
165 chunk_idx += 1;
166
167 if let Some(status_id) = opts.status_bar_widget_id {
169 if sizes.is_active(status_id) {
170 result.widget_areas.insert(status_id, chunks[chunk_idx]);
171 result.render_order.push(status_id);
172 }
173 }
174
175 for id in &opts.overlay_widget_ids {
177 if sizes.is_active(id) {
178 result.widget_areas.insert(id, area);
179 result.render_order.push(id);
180 }
181 }
182
183 result
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::themes::Theme;
190 use std::collections::{HashMap, HashSet};
191
192 fn test_context(area: ratatui::layout::Rect) -> LayoutContext<'static> {
193 static THEME: std::sync::LazyLock<Theme> = std::sync::LazyLock::new(Theme::default);
194 LayoutContext {
195 frame_area: area,
196 show_throbber: false,
197 input_visual_lines: 1,
198 theme: &THEME,
199 active_widgets: HashSet::new(),
200 }
201 }
202
203 fn test_sizes() -> WidgetSizes {
204 WidgetSizes {
205 heights: HashMap::new(),
206 is_active: HashMap::new(),
207 }
208 }
209
210 #[test]
211 fn test_standard_layout() {
212 use ratatui::layout::Rect;
213
214 let area = Rect::new(0, 0, 80, 24);
215 let ctx = test_context(area);
216 let sizes = test_sizes();
217
218 let result = compute(&ctx, &sizes, &StandardOptions::default());
219
220 assert!(result.widget_areas.contains_key(widget_ids::CHAT_VIEW));
221 assert!(result.widget_areas.contains_key(widget_ids::TEXT_INPUT));
222 }
225}