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