matchmaker/ui/
results.rs

1use ratatui::{
2    layout::Rect,
3    style::{Style, Stylize},
4    widgets::{Paragraph, Row, Table},
5};
6use unicode_width::UnicodeWidthStr;
7
8use crate::{
9    MMItem, Selection, SelectionSet,
10    config::ResultsConfig,
11    nucleo::{Status, Worker},
12    utils::text::{clip_text_lines, fit_width, prefix_text, substitute_escaped},
13};
14
15// todo: possible to store rows in here?
16#[derive(Debug, Clone)]
17pub struct ResultsUI {
18    cursor: u16,
19    bottom: u16,
20    height: u16,      // actual height
21    width: u16,
22    widths: Vec<u16>, // not sure how to support it yet
23    col: Option<usize>,
24    pub status: Status,
25    pub config: ResultsConfig,
26}
27
28impl ResultsUI {
29    pub fn new(config: ResultsConfig) -> Self {
30        Self {
31            cursor: 0,
32            bottom: 0,
33            col: None,
34            widths: Vec::new(),
35            status: Default::default(),
36            height: 0, // uninitialized, so be sure to call update_dimensions
37            width: 0,
38            config,
39        }
40    }
41
42    pub fn indentation(&self) -> usize {
43        self.config.multi_prefix.width()
44    }
45
46    pub fn col(&self) -> Option<usize> {
47        self.col
48    }
49
50    pub fn widths(&self) -> &Vec<u16> {
51        &self.widths
52    }
53
54    pub fn width(&self) -> u16 {
55        self.width.saturating_sub(self.indentation() as u16)
56    }
57
58    // todo: support cooler things like only showing/outputting a specific column/cycling columns
59    pub fn toggle_col(&mut self, col_idx: usize) -> bool {
60        if self.col == Some(col_idx) {
61            self.col = None
62        } else {
63            self.col = Some(col_idx);
64        }
65        self.col.is_some()
66    }
67
68    pub fn match_style(&self) -> Style {
69        Style::default()
70        .fg(self.config.match_fg)
71        .add_modifier(self.config.match_modifier)
72    }
73
74    pub fn wrap(&mut self, wrap: bool) {
75        self.config.wrap = wrap;
76    }
77
78    pub fn is_wrap(&self) -> bool {
79        self.config.wrap
80    }
81
82    pub fn cycle_col(&mut self) {
83        self.col = match self.col {
84            None => {
85                if !self.widths.is_empty() { Some(0) } else { None }
86            }
87            Some(c) => {
88                let next = c + 1;
89                if next < self.widths.len() {
90                    Some(next)
91                } else {
92                    None
93                }
94            }
95        };
96    }
97
98    pub fn reverse(&self) -> bool {
99        self.config.reverse.unwrap()
100    }
101
102    fn scroll_padding(&self) -> u16 {
103        self.config.scroll_padding.min(self.height / 2)
104    }
105
106    // as given by ratatui area
107    pub fn update_dimensions(&mut self, area: &Rect) {
108        let border = self.config.border.height();
109        self.width = area.width.saturating_sub(border);
110        self.height = area.height.saturating_sub(border);
111    }
112
113    pub fn cursor_prev(&mut self) -> bool {
114        if self.cursor <= self.scroll_padding() && self.bottom > 0 {
115            self.bottom -= 1;
116        } else if self.cursor > 0 {
117            self.cursor -= 1;
118            return self.cursor == 1;
119        } else if self.config.scroll_wrap {
120            self.cursor_jump(self.end());
121        }
122        false
123    }
124    pub fn cursor_next(&mut self) -> bool {
125        if self.cursor + 1 + self.scroll_padding() >= self.height
126        && self.bottom + self.height < self.status.matched_count as u16
127        {
128            self.bottom += 1;
129        } else if self.index() < self.end() {
130            self.cursor += 1;
131            if self.index() == self.end() {
132                return true;
133            }
134        } else if self.config.scroll_wrap {
135            self.cursor_jump(0)
136        }
137        false
138    }
139    pub fn cursor_jump(&mut self, index: u32) {
140        let end = self.end();
141        let index = index.min(end) as u16;
142
143        if index < self.bottom || index >= self.bottom + self.height {
144            self.bottom = (end as u16 + 1).saturating_sub(self.height).min(index);
145            self.cursor = index - self.bottom;
146        } else {
147            self.cursor = index - self.bottom;
148        }
149    }
150    pub fn end(&self) -> u32 {
151        self.status.matched_count.saturating_sub(1)
152    }
153    pub fn index(&self) -> u32 {
154        (self.cursor + self.bottom) as u32
155    }
156
157    pub fn max_widths(&self) -> Vec<u16> {
158        if ! self.config.wrap {
159            return vec![];
160        }
161
162        let mut widths = vec![u16::MAX; self.widths.len()];
163
164        let total: u16 = self.widths.iter().sum();
165        if total <= self.width() {
166            return vec![];
167        }
168
169        let mut available = self.width();
170        let mut scale_total = 0;
171        let mut scalable_indices = Vec::new();
172
173        for (i, &w) in self.widths.iter().enumerate() {
174            if w <= 5 {
175                available = available.saturating_sub(w);
176            } else {
177                scale_total += w;
178                scalable_indices.push(i);
179            }
180        }
181
182        for &i in &scalable_indices {
183            let old = self.widths[i];
184            let new_w = old * available / scale_total;
185            widths[i] = new_w.max(5);
186        }
187
188        // give remainder to the last scalable column
189        if let Some(&last_idx) = scalable_indices.last() {
190            let used_total: u16 = widths.iter().sum();
191            if used_total < self.width() {
192                widths[last_idx] += self.width() - used_total;
193            }
194        }
195
196        widths
197    }
198
199    // this updates the internal status, so be sure to call make_status afterward
200    // some janky wrapping is implemented, dunno whats causing flickering, padding is fixed going down only
201    pub fn make_table<'a, T: MMItem, C: 'a>(
202        &'a mut self,
203        worker: &'a mut Worker<T, C>,
204        selections: &mut SelectionSet<T, impl Selection>,
205        matcher: &mut nucleo::Matcher,
206    ) -> Table<'a> {
207        let offset = self.bottom as u32;
208        let end = (self.bottom + self.height) as u32;
209
210        let (mut results, mut widths, status) = worker.results(offset, end, &self.max_widths(), self.match_style(), matcher);
211
212        if status.matched_count < (self.bottom + self.cursor) as u32 {
213            self.cursor_jump(status.matched_count);
214        }
215
216        widths[0] += self.indentation() as u16;
217
218        self.status = status;
219
220        let mut rows = vec![];
221        let mut total_height = 0;
222
223        if results.is_empty() {
224            return Table::new(rows, widths)
225        }
226
227        // debug!("sb: {}, {}, {}, {}", self.bottom, self.cursor, total_height, self.height);
228
229
230        let cursor_result_h = results[self.cursor as usize].2;
231        let mut start_index = 0;
232
233        let cursor_should_above = self.height - self.scroll_padding();
234
235        if cursor_result_h >= cursor_should_above {
236            start_index = self.cursor;
237            self.bottom += self.cursor;
238            self.cursor = 0;
239        } else if let cursor_cum_h = results[0..=self.cursor as usize].iter().map(|(_, _, height)| height).sum::<u16>() && cursor_cum_h > cursor_should_above && self.bottom + self.height < self.status.matched_count as u16 {
240            start_index = 1;
241            let mut height = cursor_cum_h - cursor_should_above;
242            for (row, item, h) in results[..self.cursor as usize].iter_mut() {
243                let h = *h;
244
245                if height < h {
246                    for (_, t) in row.iter_mut().enumerate().filter(|(i, _) | widths[*i] != 0 ) {
247                        clip_text_lines(t, height, !self.reverse());
248                    }
249                    total_height += height;
250
251                    let prefix = if selections.contains(item) {
252                        self.config.multi_prefix.clone().to_string()
253                    } else {
254                        fit_width(
255                            &substitute_escaped(
256                                &self.config.default_prefix,
257                                &[('d', &(start_index - 1).to_string()), ('r', &self.index().to_string())],
258                            ),
259                            self.indentation(),
260                        )
261                    };
262
263                    prefix_text(&mut row[0], prefix);
264
265                    let row = Row::from_iter(row.clone().into_iter().enumerate().filter_map(|(i, v) | (widths[i] != 0).then_some(v) )).height(height);
266                    // debug!("1: {} {:?} {}", start_index, row, h_exceedance);
267
268                    rows.push(row);
269
270                    self.bottom += start_index - 1;
271                    self.cursor -= start_index - 1;
272                    break
273                } else if height == h {
274                    self.bottom += start_index;
275                    self.cursor -= start_index;
276                    // debug!("2: {} {}", start_index, h);
277                    break
278                }
279
280                start_index += 1;
281                height -= h;
282            }
283
284        }
285
286        // debug!("si: {start_index}, {}, {}, {}", self.bottom, self.cursor, total_height);
287
288        for (i, (mut row, item, mut height)) in (start_index..).zip(results.drain(start_index as usize..)) {
289            if self.height - total_height == 0 {
290                break
291            } else if self.height - total_height < height {
292                height = self.height - total_height;
293
294                for (_, t) in row.iter_mut().enumerate().filter(|(i, _) | widths[*i] != 0 ) {
295                    clip_text_lines(t, height, self.reverse());
296                }
297                total_height = self.height;
298            } else {
299                total_height += height;
300            }
301
302            let prefix = if selections.contains(item) {
303                self.config.multi_prefix.clone().to_string()
304            } else {
305                fit_width(
306                    &substitute_escaped(
307                        &self.config.default_prefix,
308                        &[('d', &i.to_string()), ('r', &self.index().to_string())],
309                    ),
310                    self.indentation(),
311                )
312            };
313
314            prefix_text(&mut row[0], prefix);
315
316            if i == self.cursor {
317                row = row
318                .into_iter()
319                .enumerate()
320                .map(|(i, t)| {
321                    if self.col.is_none_or(|a| i == a) {
322                        t.style(self.config.current_fg)
323                        .bg(self.config.current_bg)
324                        .add_modifier(self.config.current_modifier)
325                    } else {
326                        t
327                    }
328                })
329                .collect();
330            }
331
332            let row = Row::from_iter(row.into_iter().enumerate().filter_map(|(i, v) | (widths[i] != 0).then_some(v) )).height(height);
333
334            rows.push(row);
335        }
336
337
338        if self.reverse() {
339            rows.reverse();
340            if total_height < self.height {
341                let spacer_height = self.height - total_height;
342                rows.insert(0, Row::new(vec![vec![]]).height(spacer_height));
343            }
344        }
345
346        self.widths = {
347            let pos = widths.iter().rposition(|&x| x != 0).map_or(0, |p| p + 1);
348            widths[..pos].to_vec()
349        };
350
351
352        let mut table = Table::new(rows, self.widths.clone()).column_spacing(self.config.column_spacing.0);
353
354        table = table.block(self.config.border.as_block()).style(self.config.fg).add_modifier(self.config.modifier);
355        table
356    }
357
358    pub fn make_status(&self) -> Paragraph<'_> {
359        Paragraph::new(format!(
360            "  {}/{}",
361            &self.status.matched_count, &self.status.item_count
362        ))
363        .style(self.config.count_fg)
364        .add_modifier(self.config.count_modifier)
365    }
366}
367