Skip to main content

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