matchmaker/ui/
mod.rs

1mod input;
2mod preview;
3mod results;
4mod display;
5mod overlay;
6pub use display::DisplayUI;
7pub use input::InputUI;
8pub use preview::PreviewUI;
9pub use results::ResultsUI;
10pub use overlay::*;
11
12pub use ratatui::{
13    layout::{Constraint, Direction, Layout, Rect},
14    widgets::Table,
15    Frame
16}; // reexport for convenience
17
18use crate::{
19    SSS, Selection, Selector, config::{
20        DisplayConfig, InputConfig, PreviewLayoutSetting, RenderConfig, ResultsConfig, TerminalLayoutSettings, UiConfig
21    }, nucleo::Worker, preview::Preview, tui::Tui
22};
23// UI
24pub struct UI {
25    pub layout: Option<TerminalLayoutSettings>,
26    pub area: Rect, // unused
27    pub config: UiConfig
28}
29
30impl UI {
31    pub fn new<'a, T: SSS, S: Selection, W: std::io::Write>(
32        mut config: RenderConfig,
33        matcher: &'a mut nucleo::Matcher,
34        worker: Worker<T>,
35        selection_set: Selector<T, S>,
36        view: Option<Preview>,
37        tui: &mut Tui<W>,
38    ) -> (Self, PickerUI<'a, T, S>, Option<PreviewUI>) {
39        if config.results.reverse.is_none() {
40            config.results.reverse = Some(
41                tui.is_fullscreen() && tui.area.y < tui.area.height / 2 // reverse if fullscreen + cursor is in lower half of the screen
42            );
43        }
44
45        let ui = Self {
46            layout: tui.config.layout.clone(),
47            area: tui.area,
48            config: config.ui
49        };
50
51        let picker = PickerUI::new(config.results, config.input, config.header, config.footer, matcher, worker, selection_set);
52
53        let preview = if let Some(view) = view {
54            Some(PreviewUI::new(view, config.preview))
55        } else {
56            None
57        };
58
59        (ui, picker, preview)
60    }
61
62    pub fn update_dimensions(&mut self, area: Rect) {
63        self.area = area;
64    }
65
66    pub fn make_ui(&self) -> ratatui::widgets::Block<'_> {
67        self.config.border.as_block()
68    }
69
70    pub fn inner_area(&self, area: &Rect) -> Rect {
71        Rect {
72            x: area.x + self.config.border.left(),
73            y: area.y + self.config.border.top(),
74            width: area.width.saturating_sub(self.config.border.width()),
75            height: area.height.saturating_sub(self.config.border.height()),
76        }
77    }
78}
79
80pub struct PickerUI<'a, T: SSS, S: Selection> {
81    pub results: ResultsUI,
82    pub input: InputUI,
83    pub header: DisplayUI,
84    pub footer: DisplayUI,
85    pub matcher: &'a mut nucleo::Matcher,
86    pub selections: Selector<T, S>,
87    pub worker: Worker<T>,
88}
89
90impl<'a, T: SSS, S: Selection> PickerUI<'a, T, S> {
91    pub fn new(
92        results_config: ResultsConfig,
93        input_config: InputConfig,
94        header_config: DisplayConfig,
95        footer_config: DisplayConfig,
96        matcher: &'a mut nucleo::Matcher,
97        worker: Worker<T>,
98        selections: Selector<T, S>,
99    ) -> Self {
100        Self {
101            results: ResultsUI::new(results_config),
102            input: InputUI::new(input_config),
103            header: DisplayUI::new(header_config),
104            footer: DisplayUI::new(footer_config),
105            matcher,
106            selections,
107            worker,
108        }
109    }
110
111    pub fn layout(&self, area: Rect) -> [Rect; 5] {
112        let PickerUI { input, header, footer, .. } = self;
113
114        let mut constraints = [
115        Constraint::Length(1 + input.config.border.height()), // input
116        Constraint::Length(1), // status
117        Constraint::Length(header.height()),
118        Constraint::Fill(1), // results
119        Constraint::Length(footer.height()),
120        ];
121
122        if self.reverse() {
123            constraints.reverse();
124        }
125
126        let chunks = Layout::default()
127        .direction(Direction::Vertical)
128        .constraints(constraints)
129        .split(area);
130
131        if self.reverse() {
132            [chunks[4], chunks[3], chunks[2], chunks[1], chunks[0]]
133        } else {
134            [chunks[0], chunks[1], chunks[2], chunks[3], chunks[4]]
135        }
136    }
137}
138
139impl<'a, T: SSS, O: Selection> PickerUI<'a, T, O> {
140    pub fn make_table(&mut self) -> Table<'_> {
141        self.results
142        .make_table(&mut self.worker, &mut self.selections, self.matcher)
143    }
144
145    pub fn update(&mut self) {
146        self.worker.find(&self.input.input);
147    }
148
149    // creation from UI ensures Some
150    pub fn reverse(&self) -> bool {
151        self.results.reverse()
152    }
153}
154
155impl PreviewLayoutSetting {
156    pub fn split(&self, area: Rect) -> [Rect; 2] {
157        use crate::config::Side;
158        use ratatui::layout::{Constraint, Direction, Layout};
159
160        let direction = match self.side {
161            Side::Left | Side::Right => Direction::Horizontal,
162            Side::Top | Side::Bottom => Direction::Vertical,
163        };
164
165        let side_first = matches!(self.side, Side::Left | Side::Top);
166
167        let total = if matches!(direction, Direction::Horizontal) {
168            area.width
169        } else {
170            area.height
171        };
172
173        let p = self.percentage.inner();
174
175        let mut side_size = if p != 0 { total * p / 100 } else { 0 };
176
177        let min = if self.min < 0 {
178            total.saturating_sub((-self.min) as u16)
179        } else {
180            self.min as u16
181        };
182
183        let max = if self.max < 0 {
184            total.saturating_sub((-self.max) as u16)
185        } else {
186            self.max as u16
187        };
188
189        side_size = side_size.clamp(min, max);
190
191        let side_constraint = Constraint::Length(side_size);
192
193        let constraints = if side_first {
194            [side_constraint, Constraint::Min(0)]
195        } else {
196            [Constraint::Min(0), side_constraint]
197        };
198
199        let chunks = Layout::default()
200        .direction(direction)
201        .constraints(constraints)
202        .split(area);
203
204        if side_first {
205            [chunks[0], chunks[1]]
206        } else {
207            [chunks[1], chunks[0]]
208        }
209    }
210}