matchmaker/ui/
mod.rs

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