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    pub 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 = Self {
58            layout: tui.config.layout.clone(),
59            area: tui.area,
60            config: config.ui,
61        };
62
63        let mut picker = PickerUI::new(
64            config.results,
65            config.status,
66            config.input,
67            config.header,
68            matcher,
69            worker,
70            selection_set,
71        );
72        picker.results.hidden_columns(hidden_columns);
73
74        let ui_area = [
75            tui.area.width.saturating_sub(ui.config.border.width()),
76            tui.area.height.saturating_sub(ui.config.border.height()),
77        ];
78        let preview = if let Some(view) = view {
79            Some(PreviewUI::new(view, config.preview, ui_area))
80        } else {
81            None
82        };
83
84        let footer = DisplayUI::new(config.footer);
85
86        (ui, picker, footer, preview)
87    }
88
89    pub fn update_dimensions(&mut self, area: Rect) {
90        self.area = area;
91    }
92
93    pub fn make_ui(&self) -> ratatui::widgets::Block<'_> {
94        self.config.border.as_block()
95    }
96
97    pub fn inner_area(&self, area: &Rect) -> Rect {
98        Rect {
99            x: area.x + self.config.border.left(),
100            y: area.y + self.config.border.top(),
101            width: area.width.saturating_sub(self.config.border.width()),
102            height: area.height.saturating_sub(self.config.border.height()),
103        }
104    }
105}
106
107pub struct PickerUI<'a, T: SSS, S: Selection> {
108    pub results: ResultsUI,
109    pub input: InputUI,
110    pub header: DisplayUI,
111    pub matcher: &'a mut nucleo::Matcher,
112    pub selector: Selector<T, S>,
113    pub worker: Worker<T>,
114}
115
116impl<'a, T: SSS, S: Selection> PickerUI<'a, T, S> {
117    pub fn new(
118        results_config: ResultsConfig,
119        status_config: StatusConfig,
120        input_config: InputConfig,
121        header_config: DisplayConfig,
122        matcher: &'a mut nucleo::Matcher,
123        worker: Worker<T>,
124        selections: Selector<T, S>,
125    ) -> Self {
126        Self {
127            results: ResultsUI::new(results_config, status_config),
128            input: InputUI::new(input_config),
129            header: DisplayUI::new(header_config),
130            matcher,
131            selector: selections,
132            worker,
133        }
134    }
135
136    pub fn layout(&self, area: Rect) -> [Rect; 4] {
137        let PickerUI {
138            input,
139            header,
140            results,
141            ..
142        } = self;
143
144        let mut constraints = [
145            Constraint::Length(1 + input.config.border.height()), // input
146            Constraint::Length(results.status_config.show as u16), // status
147            Constraint::Length(header.height()),
148            Constraint::Fill(1), // results
149        ];
150
151        if self.reverse() {
152            constraints.reverse();
153        }
154
155        let chunks = Layout::default()
156            .direction(Direction::Vertical)
157            .constraints(constraints)
158            .split(area);
159
160        std::array::from_fn(|i| {
161            chunks[if self.reverse() {
162                chunks.len() - i - 1
163            } else {
164                i
165            }]
166        })
167    }
168}
169
170impl<'a, T: SSS, O: Selection> PickerUI<'a, T, O> {
171    pub fn make_table(&mut self, click: &mut Click) -> (Table<'_>, u16) {
172        let table =
173            self.results
174                .make_table(&mut self.worker, &mut self.selector, self.matcher, click);
175        let width = self.results.table_width();
176        (table, width)
177    }
178
179    pub fn update(&mut self) {
180        self.worker.find(&self.input.input);
181    }
182    pub fn tick(&mut self) {
183        self.worker.find(&self.input.input);
184    }
185
186    // creation from UI ensures Some
187    pub fn reverse(&self) -> bool {
188        self.results.reverse()
189    }
190}
191
192impl PreviewLayout {
193    pub fn split(&self, area: Rect) -> [Rect; 2] {
194        use crate::config::Side;
195        use ratatui::layout::{Constraint, Direction, Layout};
196
197        let direction = match self.side {
198            Side::Left | Side::Right => Direction::Horizontal,
199            Side::Top | Side::Bottom => Direction::Vertical,
200        };
201
202        let side_first = matches!(self.side, Side::Left | Side::Top);
203
204        let total = if matches!(direction, Direction::Horizontal) {
205            area.width
206        } else {
207            area.height
208        };
209
210        let p = self.percentage.inner();
211
212        let mut side_size = if p != 0 { total * p / 100 } else { 0 };
213
214        let min = if self.min < 0 {
215            total.saturating_sub((-self.min) as u16)
216        } else {
217            self.min as u16
218        };
219
220        let max = if self.max < 0 {
221            total.saturating_sub((-self.max) as u16)
222        } else {
223            self.max as u16
224        };
225
226        side_size = side_size.clamp(min, max);
227
228        let side_constraint = Constraint::Length(side_size);
229
230        let constraints = if side_first {
231            [side_constraint, Constraint::Min(0)]
232        } else {
233            [Constraint::Min(0), side_constraint]
234        };
235
236        let chunks = Layout::default()
237            .direction(direction)
238            .constraints(constraints)
239            .split(area);
240
241        if side_first {
242            [chunks[0], chunks[1]]
243        } else {
244            [chunks[1], chunks[0]]
245        }
246    }
247}