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#[derive(Debug, Clone)]
17pub struct ResultsUI {
18 cursor: u16,
19 bottom: u16,
20 height: u16, width: u16,
22 widths: Vec<u16>, 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, 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 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 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 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 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 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 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 break
278 }
279
280 start_index += 1;
281 height -= h;
282 }
283
284 }
285
286 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