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