Skip to main content

matchmaker/ui/
mod.rs

1mod display;
2mod input;
3mod overlay;
4mod preview;
5mod results;
6pub use display::*;
7pub use input::*;
8pub use overlay::*;
9pub use preview::*;
10pub use results::*;
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, PreviewLayout, RenderConfig, ResultsConfig, StatusConfig,
22        TerminalLayoutSettings, UiConfig,
23    },
24    nucleo::Worker,
25    preview::Preview,
26    render::Click,
27    tui::Tui,
28};
29// UI
30pub struct UI {
31    pub layout: Option<TerminalLayoutSettings>,
32    area: Rect, // unused
33    pub config: UiConfig,
34}
35
36// requires columns > 1
37impl UI {
38    pub fn new<'a, T: SSS, S: Selection, W: std::io::Write>(
39        mut config: RenderConfig,
40        matcher: &'a mut nucleo::Matcher,
41        worker: Worker<T>,
42        selection_set: Selector<T, S>,
43        view: Option<Preview>,
44        tui: &mut Tui<W>,
45        hidden_columns: Vec<bool>,
46    ) -> (Self, PickerUI<'a, T, S>, DisplayUI, Option<PreviewUI>) {
47        assert!(!worker.columns.is_empty());
48
49        if config.results.reverse.is_none() {
50            config.results.reverse = (
51                tui.is_fullscreen() && tui.area.y < tui.area.height / 2
52                // reverse if fullscreen + cursor is in lower half of the screen
53            )
54            .into()
55        }
56
57        let ui_area = [
58            tui.area.width.saturating_sub(config.ui.border.width()),
59            tui.area.height.saturating_sub(config.ui.border.height()),
60        ];
61
62        let area = Rect {
63            x: tui.area.x + config.ui.border.left(),
64            y: tui.area.y + config.ui.border.top(),
65            width: ui_area[0],
66            height: ui_area[1],
67        };
68
69        let ui = Self {
70            layout: tui.config.layout.clone(),
71            area,
72            config: config.ui,
73        };
74
75        let mut picker = PickerUI::new(
76            config.results,
77            config.status,
78            config.input,
79            config.header,
80            matcher,
81            worker,
82            selection_set,
83        );
84        picker.results.hidden_columns(hidden_columns);
85
86        let preview = if let Some(view) = view {
87            Some(PreviewUI::new(view, config.preview, ui_area))
88        } else {
89            None
90        };
91
92        let footer = DisplayUI::new(config.footer);
93
94        (ui, picker, footer, preview)
95    }
96
97    pub fn update_dimensions(&mut self, area: Rect) {
98        let border = &self.config.border;
99
100        self.area = Rect {
101            x: area.x + border.left(),
102            y: area.y + border.top(),
103            width: area.width.saturating_sub(border.width()),
104            height: area.height.saturating_sub(border.height()),
105        };
106    }
107
108    pub fn make_ui(&self) -> ratatui::widgets::Block<'_> {
109        self.config.border.as_block()
110    }
111
112    pub fn area(&self) -> Rect {
113        self.area
114    }
115
116    pub fn compute_area(&self, area: &Rect) -> Rect {
117        Rect {
118            x: area.x + self.config.border.left(),
119            y: area.y + self.config.border.top(),
120            width: area.width.saturating_sub(self.config.border.width()),
121            height: area.height.saturating_sub(self.config.border.height()),
122        }
123    }
124
125    pub fn full_area(&self) -> Rect {
126        Rect {
127            x: self.area.x - self.config.border.left(),
128            y: self.area.y - self.config.border.top(),
129            width: self.area.width + self.config.border.width(),
130            height: self.area.height + self.config.border.height(),
131        }
132    }
133}
134
135pub struct PickerUI<'a, T: SSS, S: Selection> {
136    pub results: ResultsUI,
137    pub input: InputUI,
138    pub header: DisplayUI,
139    pub matcher: &'a mut nucleo::Matcher,
140    pub selector: Selector<T, S>,
141    pub worker: Worker<T>,
142}
143
144impl<'a, T: SSS, S: Selection> PickerUI<'a, T, S> {
145    pub fn new(
146        results_config: ResultsConfig,
147        status_config: StatusConfig,
148        input_config: InputConfig,
149        header_config: DisplayConfig,
150        matcher: &'a mut nucleo::Matcher,
151        worker: Worker<T>,
152        selections: Selector<T, S>,
153    ) -> Self {
154        Self {
155            results: ResultsUI::new(results_config, status_config),
156            input: InputUI::new(input_config),
157            header: DisplayUI::new(header_config),
158            matcher,
159            selector: selections,
160            worker,
161        }
162    }
163
164    pub fn layout(&self, area: Rect) -> [Rect; 4] {
165        let PickerUI {
166            input,
167            header,
168            results,
169            ..
170        } = self;
171
172        let mut constraints = [
173            Constraint::Length(1 + input.config.border.height()), // input
174            Constraint::Length(results.status_config.show as u16), // status
175            Constraint::Length(header.height()),
176            Constraint::Fill(1), // results
177        ];
178
179        if self.reverse() {
180            constraints.reverse();
181        }
182
183        let chunks = Layout::default()
184            .direction(Direction::Vertical)
185            .constraints(constraints)
186            .split(area);
187
188        std::array::from_fn(|i| {
189            chunks[if self.reverse() {
190                chunks.len() - i - 1
191            } else {
192                i
193            }]
194        })
195    }
196}
197
198impl<'a, T: SSS, O: Selection> PickerUI<'a, T, O> {
199    pub fn make_table(&mut self, click: &mut Click) -> (Table<'_>, u16) {
200        let cursor_byte = self.input.byte_index(self.input.cursor() as usize);
201        let active_column = self.worker.query.active_column_index(cursor_byte);
202
203        let table = self.results.make_table(
204            active_column,
205            &mut self.worker,
206            &mut self.selector,
207            self.matcher,
208            click,
209        );
210        let width = self.results.table_width();
211        (table, width)
212    }
213
214    pub fn update(&mut self) {
215        self.worker.find(&self.input.input);
216    }
217    pub fn tick(&mut self) {
218        self.worker.find(&self.input.input);
219    }
220
221    // creation from UI ensures Some
222    pub fn reverse(&self) -> bool {
223        self.results.reverse()
224    }
225}
226
227impl PreviewLayout {
228    pub fn split(&self, area: Rect) -> [Rect; 2] {
229        use crate::config::Side;
230        use ratatui::layout::{Constraint, Direction, Layout};
231
232        let direction = match self.side {
233            Side::Left | Side::Right => Direction::Horizontal,
234            Side::Top | Side::Bottom => Direction::Vertical,
235        };
236
237        let side_first = matches!(self.side, Side::Left | Side::Top);
238
239        let total = if matches!(direction, Direction::Horizontal) {
240            area.width
241        } else {
242            area.height
243        };
244
245        let min = if self.min < 0 {
246            total.saturating_sub((-self.min) as u16)
247        } else {
248            self.min as u16
249        };
250
251        let max = if self.max < 0 {
252            total.saturating_sub((-self.max) as u16)
253        } else {
254            self.max as u16
255        };
256
257        let side_size = if min <= max {
258            self.percentage.compute_clamped(total, min, max)
259        } else {
260            log::error!("PreviewLayout min > max: {min} > {max}. Ignoring max.");
261            self.percentage.compute_clamped(total, min, 0)
262        };
263
264        let side_constraint = Constraint::Length(side_size);
265
266        let constraints = if side_first {
267            [side_constraint, Constraint::Min(0)]
268        } else {
269            [Constraint::Min(0), side_constraint]
270        };
271
272        let chunks = Layout::default()
273            .direction(direction)
274            .constraints(constraints)
275            .split(area);
276
277        if side_first {
278            [chunks[0], chunks[1]]
279        } else {
280            [chunks[1], chunks[0]]
281        }
282    }
283}