matchmaker/ui/
mod.rs

1mod display;
2mod input;
3mod overlay;
4mod preview;
5mod results;
6pub use display::DisplayUI;
7pub use input::InputUI;
8pub use overlay::*;
9pub use preview::PreviewUI;
10pub use results::ResultsUI;
11
12pub use ratatui::{
13    Frame,
14    layout::{Constraint, Direction, Layout, Rect},
15    widgets::Table,
16}; // reexport for convenience
17
18use crate::{
19    SSS, Selection, Selector,
20    config::{
21        DisplayConfig, InputConfig, PreviewLayoutSetting, RenderConfig, ResultsConfig,
22        TerminalLayoutSettings, UiConfig,
23    },
24    nucleo::Worker,
25    preview::Preview,
26    tui::Tui,
27};
28// UI
29pub struct UI {
30    pub layout: Option<TerminalLayoutSettings>,
31    pub area: Rect, // unused
32    pub config: UiConfig,
33}
34
35impl UI {
36    pub fn new<'a, T: SSS, S: Selection, W: std::io::Write>(
37        mut config: RenderConfig,
38        matcher: &'a mut nucleo::Matcher,
39        worker: Worker<T>,
40        selection_set: Selector<T, S>,
41        view: Option<Preview>,
42        tui: &mut Tui<W>,
43    ) -> (Self, PickerUI<'a, T, S>, Option<PreviewUI>) {
44        if config.results.reverse.is_none() {
45            config.results.reverse = Some(
46                tui.is_fullscreen() && tui.area.y < tui.area.height / 2, // reverse if fullscreen + cursor is in lower half of the screen
47            );
48        }
49
50        let ui = Self {
51            layout: tui.config.layout.clone(),
52            area: tui.area,
53            config: config.ui,
54        };
55
56        let picker = PickerUI::new(
57            config.results,
58            config.input,
59            config.header,
60            config.footer,
61            matcher,
62            worker,
63            selection_set,
64        );
65
66        let preview = if let Some(view) = view {
67            Some(PreviewUI::new(view, config.preview))
68        } else {
69            None
70        };
71
72        (ui, picker, preview)
73    }
74
75    pub fn update_dimensions(&mut self, area: Rect) {
76        self.area = area;
77    }
78
79    pub fn make_ui(&self) -> ratatui::widgets::Block<'_> {
80        self.config.border.as_block()
81    }
82
83    pub fn inner_area(&self, area: &Rect) -> Rect {
84        Rect {
85            x: area.x + self.config.border.left(),
86            y: area.y + self.config.border.top(),
87            width: area.width.saturating_sub(self.config.border.width()),
88            height: area.height.saturating_sub(self.config.border.height()),
89        }
90    }
91}
92
93pub struct PickerUI<'a, T: SSS, S: Selection> {
94    pub results: ResultsUI,
95    pub input: InputUI,
96    pub header: DisplayUI,
97    pub footer: DisplayUI,
98    pub matcher: &'a mut nucleo::Matcher,
99    pub selections: Selector<T, S>,
100    pub worker: Worker<T>,
101}
102
103impl<'a, T: SSS, S: Selection> PickerUI<'a, T, S> {
104    pub fn new(
105        results_config: ResultsConfig,
106        input_config: InputConfig,
107        header_config: DisplayConfig,
108        footer_config: DisplayConfig,
109        matcher: &'a mut nucleo::Matcher,
110        worker: Worker<T>,
111        selections: Selector<T, S>,
112    ) -> Self {
113        Self {
114            results: ResultsUI::new(results_config),
115            input: InputUI::new(input_config),
116            header: DisplayUI::new(header_config),
117            footer: DisplayUI::new(footer_config),
118            matcher,
119            selections,
120            worker,
121        }
122    }
123
124    pub fn layout(&self, area: Rect) -> [Rect; 5] {
125        let PickerUI {
126            input,
127            header,
128            footer,
129            ..
130        } = self;
131
132        let mut constraints = [
133            Constraint::Length(1 + input.config.border.height()), // input
134            Constraint::Length(1),                                // status
135            Constraint::Length(header.height()),
136            Constraint::Fill(1), // results
137            Constraint::Length(footer.height()),
138        ];
139
140        if self.reverse() {
141            constraints.reverse();
142        }
143
144        let chunks = Layout::default()
145            .direction(Direction::Vertical)
146            .constraints(constraints)
147            .split(area);
148
149        if self.reverse() {
150            [chunks[4], chunks[3], chunks[2], chunks[1], chunks[0]]
151        } else {
152            [chunks[0], chunks[1], chunks[2], chunks[3], chunks[4]]
153        }
154    }
155}
156
157impl<'a, T: SSS, O: Selection> PickerUI<'a, T, O> {
158    pub fn make_table(&mut self) -> Table<'_> {
159        self.results
160            .make_table(&mut self.worker, &mut self.selections, self.matcher)
161    }
162
163    pub fn update(&mut self) {
164        self.worker.find(&self.input.input);
165    }
166
167    // creation from UI ensures Some
168    pub fn reverse(&self) -> bool {
169        self.results.reverse()
170    }
171}
172
173impl PreviewLayoutSetting {
174    pub fn split(&self, area: Rect) -> [Rect; 2] {
175        use crate::config::Side;
176        use ratatui::layout::{Constraint, Direction, Layout};
177
178        let direction = match self.side {
179            Side::Left | Side::Right => Direction::Horizontal,
180            Side::Top | Side::Bottom => Direction::Vertical,
181        };
182
183        let side_first = matches!(self.side, Side::Left | Side::Top);
184
185        let total = if matches!(direction, Direction::Horizontal) {
186            area.width
187        } else {
188            area.height
189        };
190
191        let p = self.percentage.inner();
192
193        let mut side_size = if p != 0 { total * p / 100 } else { 0 };
194
195        let min = if self.min < 0 {
196            total.saturating_sub((-self.min) as u16)
197        } else {
198            self.min as u16
199        };
200
201        let max = if self.max < 0 {
202            total.saturating_sub((-self.max) as u16)
203        } else {
204            self.max as u16
205        };
206
207        side_size = side_size.clamp(min, max);
208
209        let side_constraint = Constraint::Length(side_size);
210
211        let constraints = if side_first {
212            [side_constraint, Constraint::Min(0)]
213        } else {
214            [Constraint::Min(0), side_constraint]
215        };
216
217        let chunks = Layout::default()
218            .direction(direction)
219            .constraints(constraints)
220            .split(area);
221
222        if side_first {
223            [chunks[0], chunks[1]]
224        } else {
225            [chunks[1], chunks[0]]
226        }
227    }
228}