Skip to main content

aimux_server/
layout.rs

1use crate::session::Layout;
2
3#[derive(Debug, Clone, Copy, PartialEq)]
4pub struct PanePosition {
5    pub x: u16,
6    pub y: u16,
7    pub width: u16,
8    pub height: u16,
9}
10
11pub fn compute_layout(
12    layout: &Layout,
13    pane_count: usize,
14    area_width: u16,
15    area_height: u16,
16) -> Vec<PanePosition> {
17    if pane_count == 0 {
18        return Vec::new();
19    }
20    if pane_count == 1 {
21        return vec![PanePosition {
22            x: 0,
23            y: 0,
24            width: area_width,
25            height: area_height,
26        }];
27    }
28
29    match layout {
30        Layout::Single => vec![PanePosition {
31            x: 0,
32            y: 0,
33            width: area_width,
34            height: area_height,
35        }],
36        Layout::EvenHorizontal => compute_even_horizontal(pane_count, area_width, area_height),
37        Layout::EvenVertical => compute_even_vertical(pane_count, area_width, area_height),
38        Layout::MainVertical => compute_main_vertical(pane_count, area_width, area_height),
39        Layout::Tiled => compute_tiled(pane_count, area_width, area_height),
40        Layout::Custom(ref tree) => crate::layout_dsl::compute_tree_layout(tree, area_width, area_height),
41    }
42}
43
44fn compute_even_horizontal(
45    pane_count: usize,
46    area_width: u16,
47    area_height: u16,
48) -> Vec<PanePosition> {
49    let n = pane_count as u16;
50    let borders = n - 1;
51    let usable = area_width.saturating_sub(borders);
52    let base_width = usable / n;
53    let remainder = usable % n;
54
55    let mut positions = Vec::with_capacity(pane_count);
56    let mut x = 0u16;
57    for i in 0..n {
58        // Last pane absorbs remainder.
59        let w = if i == n - 1 {
60            base_width + remainder
61        } else {
62            base_width
63        };
64        positions.push(PanePosition {
65            x,
66            y: 0,
67            width: w,
68            height: area_height,
69        });
70        x += w + 1; // +1 for border gap
71    }
72    positions
73}
74
75fn compute_even_vertical(
76    pane_count: usize,
77    area_width: u16,
78    area_height: u16,
79) -> Vec<PanePosition> {
80    let n = pane_count as u16;
81    let borders = n - 1;
82    let usable = area_height.saturating_sub(borders);
83    let base_height = usable / n;
84    let remainder = usable % n;
85
86    let mut positions = Vec::with_capacity(pane_count);
87    let mut y = 0u16;
88    for i in 0..n {
89        let h = if i == n - 1 {
90            base_height + remainder
91        } else {
92            base_height
93        };
94        positions.push(PanePosition {
95            x: 0,
96            y,
97            width: area_width,
98            height: h,
99        });
100        y += h + 1;
101    }
102    positions
103}
104
105fn compute_main_vertical(
106    pane_count: usize,
107    area_width: u16,
108    area_height: u16,
109) -> Vec<PanePosition> {
110    let left_width = area_width / 2;
111    let right_width = area_width.saturating_sub(left_width).saturating_sub(1);
112
113    let mut positions = Vec::with_capacity(pane_count);
114
115    // Main pane on the left.
116    positions.push(PanePosition {
117        x: 0,
118        y: 0,
119        width: left_width,
120        height: area_height,
121    });
122
123    // Remaining panes stacked on the right.
124    let right_count = (pane_count - 1) as u16;
125    if right_count == 0 {
126        return positions;
127    }
128
129    let right_x = left_width + 1; // +1 for vertical border
130    let borders = right_count - 1;
131    let usable_height = area_height.saturating_sub(borders);
132    let base_height = usable_height / right_count;
133    let remainder = usable_height % right_count;
134
135    let mut y = 0u16;
136    for i in 0..right_count {
137        let h = if i == right_count - 1 {
138            base_height + remainder
139        } else {
140            base_height
141        };
142        positions.push(PanePosition {
143            x: right_x,
144            y,
145            width: right_width,
146            height: h,
147        });
148        y += h + 1;
149    }
150    positions
151}
152
153fn compute_tiled(pane_count: usize, area_width: u16, area_height: u16) -> Vec<PanePosition> {
154    let n = pane_count;
155    let cols = (n as f64).sqrt().ceil() as usize;
156    let rows = n.div_ceil(cols);
157
158    let row_borders = (rows as u16).saturating_sub(1);
159    let usable_height = area_height.saturating_sub(row_borders);
160    let base_row_height = usable_height / rows as u16;
161
162    let mut positions = Vec::with_capacity(n);
163    let mut pane_idx = 0;
164    let mut y = 0u16;
165
166    for row in 0..rows {
167        let panes_in_row = if row == rows - 1 {
168            n - pane_idx // last row gets remaining panes
169        } else {
170            cols
171        };
172
173        let row_height = if row == rows - 1 {
174            // Last row absorbs height remainder.
175            area_height.saturating_sub(y)
176        } else {
177            base_row_height
178        };
179
180        // Compute widths for this row (last row may have fewer panes, so wider).
181        let row_col_borders = (panes_in_row as u16).saturating_sub(1);
182        let row_usable_width = area_width.saturating_sub(row_col_borders);
183        let col_width = row_usable_width / panes_in_row as u16;
184        let width_remainder = row_usable_width % panes_in_row as u16;
185
186        let mut x = 0u16;
187        for col in 0..panes_in_row {
188            let w = if col == panes_in_row - 1 {
189                col_width + width_remainder
190            } else {
191                col_width
192            };
193            positions.push(PanePosition {
194                x,
195                y,
196                width: w,
197                height: row_height,
198            });
199            x += w + 1;
200            pane_idx += 1;
201        }
202
203        y += row_height + 1; // +1 for horizontal border
204    }
205    positions
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn single_fills_area() {
214        let result = compute_layout(&Layout::Single, 1, 80, 24);
215        assert_eq!(result.len(), 1);
216        assert_eq!(
217            result[0],
218            PanePosition {
219                x: 0,
220                y: 0,
221                width: 80,
222                height: 24,
223            }
224        );
225    }
226
227    #[test]
228    fn even_horizontal_two_panes() {
229        let result = compute_layout(&Layout::EvenHorizontal, 2, 81, 24);
230        assert_eq!(result.len(), 2);
231        assert_eq!(
232            result[0],
233            PanePosition {
234                x: 0,
235                y: 0,
236                width: 40,
237                height: 24,
238            }
239        );
240        assert_eq!(
241            result[1],
242            PanePosition {
243                x: 41,
244                y: 0,
245                width: 40,
246                height: 24,
247            }
248        );
249    }
250
251    #[test]
252    fn even_horizontal_three_panes() {
253        let result = compute_layout(&Layout::EvenHorizontal, 3, 80, 24);
254        assert_eq!(result.len(), 3);
255        // 2 borders, usable = 78, base = 26, last gets 26
256        let total_width: u16 = result.iter().map(|p| p.width).sum();
257        assert_eq!(total_width, 80 - 2); // area_width minus 2 borders
258        assert_eq!(result[0].x, 0);
259        assert_eq!(result[1].x, result[0].width + 1);
260        assert_eq!(result[2].x, result[1].x + result[1].width + 1);
261    }
262
263    #[test]
264    fn even_vertical_two_panes() {
265        let result = compute_layout(&Layout::EvenVertical, 2, 80, 25);
266        assert_eq!(result.len(), 2);
267        assert_eq!(
268            result[0],
269            PanePosition {
270                x: 0,
271                y: 0,
272                width: 80,
273                height: 12,
274            }
275        );
276        assert_eq!(
277            result[1],
278            PanePosition {
279                x: 0,
280                y: 13,
281                width: 80,
282                height: 12,
283            }
284        );
285    }
286
287    #[test]
288    fn main_vertical_three_panes() {
289        let result = compute_layout(&Layout::MainVertical, 3, 80, 24);
290        assert_eq!(result.len(), 3);
291        // Left pane: 50% = 40 cols
292        assert_eq!(result[0].x, 0);
293        assert_eq!(result[0].width, 40);
294        assert_eq!(result[0].height, 24);
295        // Right panes: x = 41, width = 80 - 40 - 1 = 39
296        assert_eq!(result[1].x, 41);
297        assert_eq!(result[1].width, 39);
298        assert_eq!(result[2].x, 41);
299        assert_eq!(result[2].width, 39);
300        // Stacked vertically with 1 border: usable = 23, each 11, last 12
301        assert_eq!(result[1].y, 0);
302        assert_eq!(result[1].height, 11);
303        assert_eq!(result[2].y, 12);
304        assert_eq!(result[2].height, 12);
305    }
306
307    #[test]
308    fn tiled_four_panes() {
309        // sqrt(4) = 2 cols, 2 rows
310        let result = compute_layout(&Layout::Tiled, 4, 81, 25);
311        assert_eq!(result.len(), 4);
312        // 2 cols: usable_w = 81-1 = 80, each 40
313        // 2 rows: usable_h = 25-1 = 24, each 12
314        assert_eq!(
315            result[0],
316            PanePosition {
317                x: 0,
318                y: 0,
319                width: 40,
320                height: 12,
321            }
322        );
323        assert_eq!(
324            result[1],
325            PanePosition {
326                x: 41,
327                y: 0,
328                width: 40,
329                height: 12,
330            }
331        );
332        assert_eq!(
333            result[2],
334            PanePosition {
335                x: 0,
336                y: 13,
337                width: 40,
338                height: 12,
339            }
340        );
341        assert_eq!(
342            result[3],
343            PanePosition {
344                x: 41,
345                y: 13,
346                width: 40,
347                height: 12,
348            }
349        );
350    }
351
352    #[test]
353    fn tiled_five_panes() {
354        // ceil(sqrt(5)) = 3 cols, ceil(5/3) = 2 rows
355        let result = compute_layout(&Layout::Tiled, 5, 80, 25);
356        assert_eq!(result.len(), 5);
357        // Row 0: 3 panes, border gaps = 2, usable = 78, each 26
358        assert_eq!(result[0].y, 0);
359        assert_eq!(result[1].y, 0);
360        assert_eq!(result[2].y, 0);
361        assert_eq!(result[0].width, 26);
362        assert_eq!(result[1].width, 26);
363        assert_eq!(result[2].width, 26);
364        // Row 1: 2 panes (fills full width), border gaps = 1, usable = 79, each 39, last 40
365        let row1_y = result[3].y;
366        assert_eq!(result[3].y, row1_y);
367        assert_eq!(result[4].y, row1_y);
368        assert_eq!(result[3].width, 39);
369        assert_eq!(result[4].width, 40);
370    }
371
372    #[test]
373    fn zero_panes_returns_empty() {
374        let result = compute_layout(&Layout::EvenHorizontal, 0, 80, 24);
375        assert!(result.is_empty());
376    }
377
378    #[test]
379    fn one_pane_any_layout_fills_area() {
380        for layout in [
381            Layout::Single,
382            Layout::EvenHorizontal,
383            Layout::EvenVertical,
384            Layout::MainVertical,
385            Layout::Tiled,
386        ] {
387            let result = compute_layout(&layout, 1, 120, 40);
388            assert_eq!(result.len(), 1, "layout: {:?}", layout);
389            assert_eq!(
390                result[0],
391                PanePosition {
392                    x: 0,
393                    y: 0,
394                    width: 120,
395                    height: 40,
396                },
397                "layout: {:?}",
398                layout,
399            );
400        }
401    }
402
403    #[test]
404    fn small_terminal_no_overflow() {
405        // 3x3 with 3 panes horizontal: usable = 1, each pane 0 wide — degenerate but no panic
406        let result = compute_layout(&Layout::EvenHorizontal, 3, 3, 3);
407        assert_eq!(result.len(), 3);
408        // No position should overflow u16
409        for pos in &result {
410            assert!(pos.x <= 3);
411            assert!(pos.y <= 3);
412        }
413    }
414
415    #[test]
416    fn small_terminal_tiled() {
417        let result = compute_layout(&Layout::Tiled, 4, 5, 5);
418        assert_eq!(result.len(), 4);
419        for pos in &result {
420            assert!(pos.x < 10);
421            assert!(pos.y < 10);
422        }
423    }
424
425    #[test]
426    fn even_horizontal_widths_contiguous() {
427        // Verify panes + borders exactly fill the width.
428        for pane_count in 2..=6 {
429            let width = 100u16;
430            let result = compute_layout(&Layout::EvenHorizontal, pane_count, width, 24);
431            let total: u16 = result.iter().map(|p| p.width).sum::<u16>()
432                + (pane_count as u16 - 1); // borders
433            assert_eq!(total, width, "pane_count={}", pane_count);
434        }
435    }
436
437    #[test]
438    fn even_vertical_heights_contiguous() {
439        for pane_count in 2..=6 {
440            let height = 50u16;
441            let result = compute_layout(&Layout::EvenVertical, pane_count, 80, height);
442            let total: u16 = result.iter().map(|p| p.height).sum::<u16>()
443                + (pane_count as u16 - 1);
444            assert_eq!(total, height, "pane_count={}", pane_count);
445        }
446    }
447}