Skip to main content

binocular/ui/
layout.rs

1use crate::app::LayoutState;
2use ratatui::layout::{Constraint, Direction, Layout, Rect};
3
4pub struct UiAreas {
5    pub search_results: Rect,
6    pub preview: Option<Rect>,
7    pub search_bar: Rect,
8    pub show_search: bool,
9}
10
11pub fn split_main_layout(area: Rect, show_preview: bool, layout: &LayoutState) -> UiAreas {
12    if show_preview && layout.preview_fullscreen {
13        return UiAreas {
14            search_bar: Rect::default(),
15            search_results: Rect::default(),
16            preview: Some(area),
17            show_search: false,
18        };
19    }
20
21    if show_preview {
22        let results_pct = 100 - layout.preview_percent;
23        let preview_pct = layout.preview_percent;
24
25        let (left_pct, right_pct) = if layout.panes_swapped {
26            (preview_pct, results_pct)
27        } else {
28            (results_pct, preview_pct)
29        };
30
31        let columns = Layout::default()
32            .direction(Direction::Horizontal)
33            .constraints([
34                Constraint::Percentage(left_pct),
35                Constraint::Percentage(right_pct),
36            ])
37            .split(area);
38
39        let (results_col, preview_col) = if layout.panes_swapped {
40            (columns[1], columns[0])
41        } else {
42            (columns[0], columns[1])
43        };
44
45        let (search_bar, search_results) =
46            split_results_column(results_col, layout.search_bar_at_bottom);
47
48        UiAreas {
49            search_bar,
50            search_results,
51            preview: Some(preview_col),
52            show_search: true,
53        }
54    } else {
55        let (search_bar, search_results) = split_results_column(area, layout.search_bar_at_bottom);
56
57        UiAreas {
58            search_bar,
59            search_results,
60            preview: None,
61            show_search: true,
62        }
63    }
64}
65
66fn split_results_column(col: Rect, bar_at_bottom: bool) -> (Rect, Rect) {
67    let parts = if bar_at_bottom {
68        Layout::default()
69            .direction(Direction::Vertical)
70            .constraints([Constraint::Min(0), Constraint::Length(3)])
71            .split(col)
72    } else {
73        Layout::default()
74            .direction(Direction::Vertical)
75            .constraints([Constraint::Length(3), Constraint::Min(0)])
76            .split(col)
77    };
78
79    if bar_at_bottom {
80        (parts[1], parts[0])
81    } else {
82        (parts[0], parts[1])
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn fullscreen_preview_hides_search_regions() {
92        let layout = LayoutState {
93            preview_fullscreen: true,
94            ..LayoutState::default()
95        };
96        let area = Rect::new(0, 0, 120, 40);
97
98        let areas = split_main_layout(area, true, &layout);
99
100        assert!(!areas.show_search);
101        assert_eq!(areas.preview, Some(area));
102    }
103
104    #[test]
105    fn hidden_preview_uses_single_search_column() {
106        let area = Rect::new(0, 0, 120, 40);
107        let areas = split_main_layout(area, false, &LayoutState::default());
108
109        assert!(areas.show_search);
110        assert!(areas.preview.is_none());
111        assert_eq!(areas.search_bar.height, 3);
112        assert_eq!(areas.search_results.height, 37);
113    }
114
115    #[test]
116    fn swapped_panes_move_preview_to_left_column() {
117        let layout = LayoutState {
118            panes_swapped: true,
119            preview_percent: 40,
120            ..LayoutState::default()
121        };
122        let area = Rect::new(0, 0, 100, 30);
123
124        let areas = split_main_layout(area, true, &layout);
125        let preview = areas.preview.expect("preview area");
126
127        assert_eq!(preview.x, 0);
128        assert_eq!(preview.width, 40);
129        assert_eq!(areas.search_results.x, 40);
130    }
131
132    #[test]
133    fn search_bar_can_move_to_bottom() {
134        let layout = LayoutState {
135            search_bar_at_bottom: true,
136            ..LayoutState::default()
137        };
138        let area = Rect::new(0, 0, 80, 20);
139
140        let areas = split_main_layout(area, false, &layout);
141
142        assert_eq!(areas.search_bar.y, 17);
143        assert_eq!(areas.search_bar.height, 3);
144        assert_eq!(areas.search_results.y, 0);
145    }
146}