matchmaker/ui/
results.rs

1use ratatui::{
2    layout::Rect,
3    style::Stylize,
4    widgets::{Paragraph, Row, Table},
5};
6use unicode_width::UnicodeWidthStr;
7
8use crate::{
9    PickerItem, Selection, SelectionSet,
10    config::ResultsConfig,
11    nucleo::worker::{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    widths: Vec<u16>, // not sure how to support it yet
22    col: Option<usize>,
23    pub status: Status,
24    pub config: ResultsConfig,
25}
26
27impl ResultsUI {
28    pub fn new(config: ResultsConfig) -> Self {
29        Self {
30            cursor: 0,
31            bottom: 0,
32            col: None,
33            widths: Vec::new(),
34            status: Default::default(),
35            height: 0, // uninitialized, so be sure to call update_dimensions
36            config,
37        }
38    }
39
40    pub fn col(&self) -> Option<usize> {
41        self.col.clone()
42    }
43
44    pub fn widths(&self) -> &Vec<u16> {
45        &self.widths
46    }
47
48    // todo: support cooler things like only showing/outputting a specific column/cycling columns
49    pub fn toggle_col(&mut self, col_idx: usize) -> bool {
50        if self.col.map_or(false, |x| x == col_idx) {
51            self.col = None
52        } else {
53            self.col = Some(col_idx);
54            // if col_idx < self.widths.len() {
55            //     self.col = Some(col_idx)
56            // } else {
57            //     warn!("Tried to set col = {col_idx} but widths = {}, ignoring", self.widths.len())
58            // }
59        }
60        self.col.is_some()
61    }
62
63    pub fn reverse(&self) -> bool {
64        self.config.reverse.unwrap()
65    }
66
67    fn scroll_padding(&self) -> u16 {
68        self.config.scroll_padding.min(self.height / 2)
69    }
70
71    // as given by ratatui area
72    pub fn update_dimensions(&mut self, area: &Rect) {
73        let mut height = area.height;
74        height -= self.config.border.height();
75        self.height = height;
76    }
77
78    pub fn cursor_prev(&mut self) -> bool {
79        if self.cursor <= self.scroll_padding() && self.bottom > 0 {
80            self.bottom -= 1;
81        } else if self.cursor > 0 {
82            self.cursor -= 1;
83            return self.cursor == 1;
84        } else if self.config.scroll_wrap {
85            self.cursor_jump(self.end());
86        }
87        false
88    }
89    pub fn cursor_next(&mut self) -> bool {
90        if self.cursor + 1 + self.scroll_padding() >= self.height
91            && self.bottom + self.height < self.status.matched_count as u16
92        {
93            self.bottom += 1;
94        } else if self.index() < self.end() {
95            self.cursor += 1;
96            if self.index() == self.end() {
97                return true;
98            }
99        } else if self.config.scroll_wrap {
100            self.cursor_jump(0)
101        }
102        false
103    }
104    pub fn cursor_jump(&mut self, index: u32) {
105        let end = self.end();
106        let index = index.min(end) as u16;
107
108        if index < self.bottom || index >= self.bottom + self.height {
109            self.bottom = (end as u16 + 1).saturating_sub(self.height).min(index);
110            self.cursor = index - self.bottom;
111        } else {
112            self.cursor = index - self.bottom;
113        }
114    }
115
116    pub fn end(&self) -> u32 {
117        self.status.matched_count.saturating_sub(1)
118    }
119
120    pub fn index(&self) -> u32 {
121        (self.cursor + self.bottom) as u32
122    }
123
124    // this updates the internal status, so be sure to call make_status afterward
125    pub fn make_table<'a, T: PickerItem, C: 'a>(
126        &'a mut self,
127        worker: &'a mut Worker<T, C>,
128        selections: &mut SelectionSet<T, impl Selection>,
129        matcher: &mut nucleo::Matcher,
130    ) -> Table<'a> {
131        let offset = self.bottom as u32;
132        let end = (self.bottom + self.height) as u32;
133
134        let (results, mut widths, status) = worker.results(offset, end, matcher);
135
136        if status.matched_count < (self.bottom + self.cursor) as u32 {
137            self.cursor_jump(status.matched_count);
138        }
139
140        self.status = status;
141
142        widths[0] += self.config.multi_prefix.width() as u16;
143
144        let mut rows = vec![];
145        let mut total_height = 0;
146
147        for (i, (mut row, item, height)) in results.into_iter().enumerate() {
148            total_height += height;
149            if total_height > self.height {
150                clip_text_lines(&mut row[0], self.height - total_height, self.reverse());
151                total_height = self.height;
152            }
153
154            let prefix = if selections.contains(item) {
155                self.config.multi_prefix.clone()
156            } else {
157                fit_width(
158                    &substitute_escaped(
159                        &self.config.default_prefix,
160                        &[('d', &i.to_string()), ('r', &self.index().to_string())],
161                    ),
162                    self.config.multi_prefix.width(),
163                )
164            };
165
166            prefix_text(&mut row[0], prefix);
167
168            if i as u16 == self.cursor {
169                row = row
170                    .into_iter()
171                    .enumerate()
172                    .map(|(i, t)| {
173                        if self.col.map_or(true, |a| i == a) {
174                            t.style(self.config.current_fg)
175                                .bg(self.config.current_bg)
176                                .add_modifier(self.config.current_modifier)
177                        } else {
178                            t
179                        }
180                    })
181                    .collect();
182            }
183
184            let row = Row::new(row);
185            rows.push(row);
186        }
187
188        if self.reverse() {
189            rows.reverse();
190            if total_height < self.height {
191                let spacer_height = self.height - total_height;
192                rows.insert(0, Row::new(vec![vec![]]).height(spacer_height));
193            }
194        }
195
196        self.widths = {
197            let pos = widths.iter().rposition(|&x| x != 0).map_or(0, |p| p + 1);
198            widths[..pos].to_vec()
199        };
200
201        let mut table = Table::new(rows, self.widths.clone()).column_spacing(self.config.column_spacing.0);
202
203        table = table.block(self.config.border.as_block());
204        table
205    }
206
207    pub fn make_status(&self) -> Paragraph<'_> {
208        let input = Paragraph::new(format!(
209            "  {}/{}",
210            &self.status.matched_count, &self.status.item_count
211        ))
212        .style(self.config.count_fg)
213        .add_modifier(self.config.count_modifier);
214
215        input
216    }
217}