1use ratatui::layout::{Constraint, Direction, Layout, Rect};
2
3pub struct AppLayout {
10 pub sidebar: Rect,
11 pub main_content: Rect,
12 pub status_bar: Rect,
13 pub help_bar: Rect,
14}
15
16const SIDEBAR_WIDTH_WIDE: u16 = 20;
18const SIDEBAR_WIDTH_MEDIUM: u16 = 16;
20const SIDEBAR_WIDTH_NARROW: u16 = 12;
22
23const STATUS_BAR_HEIGHT: u16 = 1;
25const HELP_BAR_HEIGHT: u16 = 1;
27
28const MIN_CONTENT_WIDTH: u16 = 10;
30
31pub fn compute_layout(area: Rect) -> AppLayout {
44 let vertical = Layout::default()
46 .direction(Direction::Vertical)
47 .constraints([
48 Constraint::Min(3),
49 Constraint::Length(STATUS_BAR_HEIGHT),
50 Constraint::Length(HELP_BAR_HEIGHT),
51 ])
52 .split(area);
53
54 let main_area = vertical[0];
55 let status_bar = vertical[1];
56 let help_bar = vertical[2];
57
58 let sidebar_width = compute_sidebar_width(main_area.width);
60
61 let horizontal = Layout::default()
62 .direction(Direction::Horizontal)
63 .constraints([
64 Constraint::Length(sidebar_width),
65 Constraint::Min(MIN_CONTENT_WIDTH),
66 ])
67 .split(main_area);
68
69 let sidebar = horizontal[0];
70 let main_content = horizontal[1];
71
72 AppLayout {
73 sidebar,
74 main_content,
75 status_bar,
76 help_bar,
77 }
78}
79
80fn compute_sidebar_width(terminal_width: u16) -> u16 {
87 if terminal_width > 80 {
88 SIDEBAR_WIDTH_WIDE
89 } else if terminal_width > 40 {
90 SIDEBAR_WIDTH_MEDIUM
91 } else {
92 SIDEBAR_WIDTH_NARROW
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn compute_layout_standard_terminal() {
102 let area = Rect::new(0, 0, 120, 40);
103 let layout = compute_layout(area);
104 assert_eq!(layout.sidebar.width, 20);
105 assert_eq!(layout.main_content.x, 20);
106 assert_eq!(layout.status_bar.height, 1);
107 assert_eq!(layout.help_bar.height, 1);
108 assert_eq!(layout.status_bar.y + 1, layout.help_bar.y);
109 }
110
111 #[test]
112 fn compute_layout_small_terminal() {
113 let area = Rect::new(0, 0, 50, 20);
114 let layout = compute_layout(area);
115 assert_eq!(layout.sidebar.width, 16);
116 assert!(layout.main_content.width > 0);
117 }
118
119 #[test]
120 fn compute_layout_very_small_terminal() {
121 let area = Rect::new(0, 0, 30, 10);
122 let layout = compute_layout(area);
123 assert_eq!(layout.sidebar.width, 12);
124 assert!(layout.main_content.width > 0);
125 }
126
127 #[test]
128 fn compute_layout_minimum_viable() {
129 let area = Rect::new(0, 0, 20, 5);
130 let layout = compute_layout(area);
131 assert!(layout.sidebar.width > 0 || layout.main_content.width > 0);
133 }
134
135 #[test]
136 fn compute_layout_deterministic() {
137 let area = Rect::new(0, 0, 100, 30);
139 let layout1 = compute_layout(area);
140 let layout2 = compute_layout(area);
141 assert_eq!(layout1.sidebar, layout2.sidebar);
142 assert_eq!(layout1.main_content, layout2.main_content);
143 assert_eq!(layout1.status_bar, layout2.status_bar);
144 assert_eq!(layout1.help_bar, layout2.help_bar);
145 }
146
147 #[test]
148 fn status_bar_is_second_to_last_line() {
149 let area = Rect::new(0, 0, 80, 24);
150 let layout = compute_layout(area);
151 assert_eq!(layout.status_bar.y, area.height - 2);
153 assert_eq!(layout.help_bar.y, area.height - 1);
154 }
155
156 #[test]
157 fn help_bar_is_last_line() {
158 let area = Rect::new(0, 0, 80, 24);
159 let layout = compute_layout(area);
160 assert_eq!(layout.help_bar.y + layout.help_bar.height, area.height);
161 }
162
163 #[test]
164 fn sidebar_width_constants() {
165 assert_eq!(compute_sidebar_width(120), SIDEBAR_WIDTH_WIDE);
166 assert_eq!(compute_sidebar_width(81), SIDEBAR_WIDTH_WIDE);
167 assert_eq!(compute_sidebar_width(80), SIDEBAR_WIDTH_MEDIUM);
168 assert_eq!(compute_sidebar_width(41), SIDEBAR_WIDTH_MEDIUM);
169 assert_eq!(compute_sidebar_width(40), SIDEBAR_WIDTH_NARROW);
170 assert_eq!(compute_sidebar_width(20), SIDEBAR_WIDTH_NARROW);
171 }
172
173 #[test]
174 fn main_content_fills_remaining_width() {
175 let area = Rect::new(0, 0, 100, 30);
176 let layout = compute_layout(area);
177 assert_eq!(layout.sidebar.width + layout.main_content.width, area.width);
178 }
179}