1#[allow(unused)]
2use log::debug;
3
4use ratatui::{
5 layout::{Alignment, Rect},
6 style::{Style, Stylize},
7 widgets::{Paragraph, Row, Table},
8};
9use unicode_width::UnicodeWidthStr;
10
11use crate::{
12 SSS, Selection, Selector,
13 config::{ResultsConfig, RowConnectionStyle},
14 nucleo::{Status, Worker},
15 render::Click,
16 utils::text::{clip_text_lines, fit_width, prefix_text, substitute_escaped},
17};
18
19#[derive(Debug)]
20pub struct ResultsUI {
21 cursor: u16,
22 bottom: u16,
23 height: u16, width: u16,
25 widths: Vec<u16>,
28 col: Option<usize>,
29 pub status: Status,
30 pub config: ResultsConfig,
31
32 pub cursor_disabled: bool,
33}
34
35impl ResultsUI {
36 pub fn new(config: ResultsConfig) -> Self {
37 Self {
38 cursor: 0,
39 bottom: 0,
40 col: None,
41 widths: Vec::new(),
42 status: Default::default(),
43 height: 0, width: 0,
45 config,
46 cursor_disabled: false,
47 }
48 }
49 pub fn update_dimensions(&mut self, area: &Rect) {
51 let [bw, bh] = [self.config.border.height(), self.config.border.width()];
52 self.width = area.width.saturating_sub(bw);
53 self.height = area.height.saturating_sub(bh);
54 }
55
56 pub fn table_width(&self) -> u16 {
57 self.config.column_spacing.0 * self.widths().len().saturating_sub(1) as u16
58 + self.widths.iter().sum::<u16>()
59 + self.config.border.width()
60 }
61
62 pub fn reverse(&self) -> bool {
64 self.config.reverse.is_always()
65 }
66 pub fn is_wrap(&self) -> bool {
67 self.config.wrap
68 }
69 pub fn wrap(&mut self, wrap: bool) {
70 self.config.wrap = wrap;
71 }
72
73 pub fn toggle_col(&mut self, col_idx: usize) -> bool {
76 if self.col == Some(col_idx) {
77 self.col = None
78 } else {
79 self.col = Some(col_idx);
80 }
81 self.col.is_some()
82 }
83 pub fn cycle_col(&mut self) {
84 self.col = match self.col {
85 None => self.widths.is_empty().then_some(0),
86 Some(c) => {
87 let next = c + 1;
88 if next < self.widths.len() {
89 Some(next)
90 } else {
91 None
92 }
93 }
94 };
95 }
96
97 fn scroll_padding(&self) -> u16 {
99 self.config.scroll_padding.min(self.height / 2)
100 }
101 pub fn end(&self) -> u32 {
102 self.status.matched_count.saturating_sub(1)
103 }
104
105 pub fn index(&self) -> u32 {
109 if self.cursor_disabled {
110 u32::MAX
111 } else {
112 (self.cursor + self.bottom) as u32
113 }
114 }
115 pub fn cursor_prev(&mut self) {
123 if self.cursor <= self.scroll_padding() && self.bottom > 0 {
124 self.bottom -= 1;
125 } else if self.cursor > 0 {
126 self.cursor -= 1;
127 } else if self.config.scroll_wrap {
128 self.cursor_jump(self.end());
129 }
130 }
131 pub fn cursor_next(&mut self) {
132 if self.cursor_disabled {
133 self.cursor_disabled = false
134 }
135
136 log::trace!(
137 "Cursor {} @ index {}. Status: {:?}.",
138 self.cursor,
139 self.index(),
140 self.status
141 );
142
143 if self.cursor + 1 + self.scroll_padding() >= self.height
144 && self.bottom + self.height < self.status.matched_count as u16
145 {
146 self.bottom += 1;
147 } else if self.index() < self.end() {
148 self.cursor += 1;
149 } else if self.config.scroll_wrap {
150 self.cursor_jump(0)
151 }
152 }
153
154 pub fn cursor_jump(&mut self, index: u32) {
155 self.cursor_disabled = false;
156
157 let end = self.end();
158 let index = index.min(end) as u16;
159
160 if index < self.bottom || index >= self.bottom + self.height {
161 self.bottom = (end as u16 + 1).saturating_sub(self.height).min(index);
162 self.cursor = index - self.bottom;
163 } else {
164 self.cursor = index - self.bottom;
165 }
166 }
167
168 pub fn indentation(&self) -> usize {
170 self.config.multi_prefix.width()
171 }
172 pub fn col(&self) -> Option<usize> {
173 self.col
174 }
175
176 pub fn widths(&self) -> &Vec<u16> {
179 &self.widths
180 }
181 pub fn width(&self) -> u16 {
183 self.width.saturating_sub(self.indentation() as u16)
184 }
185 pub fn match_style(&self) -> Style {
186 Style::default()
187 .fg(self.config.match_fg)
188 .add_modifier(self.config.match_modifier)
189 }
190
191 pub fn max_widths(&self) -> Vec<u16> {
192 if !self.config.wrap {
193 return vec![];
194 }
195
196 let mut widths = vec![u16::MAX; self.widths.len()];
197
198 let total: u16 = self.widths.iter().sum();
199 if total <= self.width() {
200 return vec![];
201 }
202
203 let mut available = self.width();
204 let mut scale_total = 0;
205 let mut scalable_indices = Vec::new();
206
207 for (i, &w) in self.widths.iter().enumerate() {
208 if w <= self.config.wrap_scaling_min_width {
209 available = available.saturating_sub(w);
210 } else {
211 scale_total += w;
212 scalable_indices.push(i);
213 }
214 }
215
216 for &i in &scalable_indices {
217 let old = self.widths[i];
218 let new_w = old * available / scale_total;
219 widths[i] = new_w.max(self.config.wrap_scaling_min_width);
220 }
221
222 if let Some(&last_idx) = scalable_indices.last() {
224 let used_total: u16 = widths.iter().sum();
225 if used_total < self.width() {
226 widths[last_idx] += self.width() - used_total;
227 }
228 }
229
230 widths
231 }
232
233 pub fn make_table<'a, T: SSS>(
236 &mut self,
237 worker: &'a mut Worker<T>,
238 selector: &mut Selector<T, impl Selection>,
239 matcher: &mut nucleo::Matcher,
240 click: &mut Click,
241 ) -> Table<'a> {
242 let offset = self.bottom as u32;
243 let end = (self.bottom + self.height) as u32;
244
245 let (mut results, mut widths, status) =
246 worker.results(offset, end, &self.max_widths(), self.match_style(), matcher);
247
248 let match_count = status.matched_count;
249 self.status = status;
250
251 if match_count < (self.bottom + self.cursor) as u32 && !self.cursor_disabled {
252 self.cursor_jump(match_count);
253 } else {
254 self.cursor = self.cursor.min(results.len().saturating_sub(1) as u16)
255 }
256
257 widths[0] += self.indentation() as u16;
258
259 let mut rows = vec![];
260 let mut total_height = 0;
261
262 if results.is_empty() {
263 return Table::new(rows, widths);
264 }
265
266 let cursor_result_h = results[self.cursor as usize].2;
268 let mut start_index = 0;
270
271 let cum_h_after_cursor = results[(self.cursor as usize + 1).min(results.len())..]
272 .iter()
273 .map(|(_, _, height)| height)
274 .sum::<u16>();
275
276 let cursor_should_lt = self.height - self.scroll_padding().min(cum_h_after_cursor);
277
278 if cursor_result_h >= cursor_should_lt {
279 start_index = self.cursor;
280 self.bottom += self.cursor;
281 self.cursor = 0;
282 } else
283 if let cum_h_to_cursor = results[0..=self.cursor as usize]
285 .iter()
286 .map(|(_, _, height)| height)
287 .sum::<u16>()
288 && cum_h_to_cursor > cursor_should_lt
289 {
290 start_index = 1;
291 let mut remaining_height = cum_h_to_cursor.saturating_sub(cursor_should_lt);
292
293 for (row, item, h) in results[..self.cursor as usize].iter_mut() {
294 let h = *h; if remaining_height < h {
297 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
298 clip_text_lines(t, remaining_height, !self.reverse());
299 }
300 total_height = remaining_height;
301
302 let prefix = if selector.contains(item) {
303 self.config.multi_prefix.clone().to_string()
304 } else {
305 fit_width(
306 &substitute_escaped(
307 &self.config.default_prefix,
308 &[
309 ('d', ""), ('r', ""),
311 ],
312 ),
313 self.indentation(),
314 )
315 };
316
317 prefix_text(&mut row[0], prefix);
318
319 let last_visible = widths
320 .iter()
321 .enumerate()
322 .rev()
323 .find_map(|(i, w)| (*w != 0).then_some(i));
324
325 let mut row_texts: Vec<_> = row
326 .iter()
327 .take(last_visible.map(|x| x + 1).unwrap_or(0))
328 .cloned()
329 .collect();
330 if self.config.right_align_last && row_texts.len() > 1 {
331 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
332 }
333
334 let row = Row::new(row_texts).height(remaining_height);
335 rows.push(row);
336
337 self.bottom += start_index - 1;
338 self.cursor -= start_index - 1;
339 break;
340 } else if remaining_height == h {
341 self.bottom += start_index;
342 self.cursor -= start_index;
343 break;
345 }
346
347 start_index += 1;
348 remaining_height -= h;
349 }
350 }
351
352 for (i, (mut row, item, mut height)) in
355 (start_index..).zip(results.drain(start_index as usize..))
356 {
357 if let Click::ResultPos(c) = click
358 && total_height > *c
359 {
360 let idx = offset + i as u32 - 1;
361 log::debug!("Mapped click position to index: {c} -> {idx}",);
362 *click = Click::ResultIdx(idx);
363 }
364
365 if self.height - total_height == 0 {
366 break;
367 } else if self.height - total_height < height {
368 height = self.height - total_height;
369
370 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
371 clip_text_lines(t, height, self.reverse());
372 }
373 total_height = self.height;
374 } else {
375 total_height += height;
376 }
377
378 let prefix = if selector.contains(item) {
379 self.config.multi_prefix.clone().to_string()
380 } else {
381 fit_width(
382 &substitute_escaped(
383 &self.config.default_prefix,
384 &[
385 ('d', &(i + 1).to_string()), ('r', &(i + 1 + self.bottom).to_string()), ],
388 ),
389 self.indentation(),
390 )
391 };
392
393 prefix_text(&mut row[0], prefix);
394
395 if !self.cursor_disabled && i == self.cursor {
396 row = row
397 .into_iter()
398 .enumerate()
399 .map(|(i, t)| {
400 if self.col == Some(i)
401 || (self.col.is_none()
402 && matches!(
403 self.config.row_connection_style,
404 RowConnectionStyle::Disjoint
405 ))
406 {
407 t.style(self.config.current_fg)
408 .bg(self.config.current_bg)
409 .add_modifier(self.config.current_modifier)
410 } else {
411 t
412 }
413 })
414 .collect();
415 }
416
417 let last_visible = widths
419 .iter()
420 .enumerate()
421 .rev()
422 .find_map(|(i, w)| (*w != 0).then_some(i));
423
424 let mut row_texts: Vec<_> = row
425 .iter()
426 .take(last_visible.map(|x| x + 1).unwrap_or(0))
427 .cloned()
428 .collect();
429 if self.config.right_align_last && row_texts.len() > 1 {
430 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
431 }
432
433 let mut row = Row::new(row_texts).height(height);
434
435 if i == self.cursor
436 && self.col.is_none()
437 && !matches!(
438 self.config.row_connection_style,
439 RowConnectionStyle::Disjoint
440 )
441 {
442 row = row
443 .style(self.config.current_fg)
444 .bg(self.config.current_bg)
445 .add_modifier(self.config.current_modifier)
446 }
447
448 rows.push(row);
449 }
450
451 if self.reverse() {
452 rows.reverse();
453 if total_height < self.height {
454 let spacer_height = self.height - total_height;
455 rows.insert(0, Row::new(vec![vec![]]).height(spacer_height));
456 }
457 }
458
459 self.widths = {
461 let pos = widths.iter().rposition(|&x| x != 0).map_or(0, |p| p + 1);
462 let mut widths = widths[..pos].to_vec();
463 if pos > 2 && self.config.right_align_last {
464 let used = widths.iter().take(widths.len() - 1).sum();
465 widths[pos - 1] = self.width().saturating_sub(used);
466 }
467 widths
468 };
469
470 let mut table = Table::new(rows, self.widths.clone())
472 .column_spacing(self.config.column_spacing.0)
473 .style(self.config.fg)
474 .add_modifier(self.config.modifier);
475
476 table = table.block(self.config.border.as_static_block());
477 table
478 }
479
480 pub fn make_status(&self) -> Paragraph<'_> {
481 Paragraph::new(format!(
482 "{}{}/{}",
483 " ".repeat(self.indentation()),
484 &self.status.matched_count,
485 &self.status.item_count
486 ))
487 .style(self.config.status_fg)
488 .add_modifier(self.config.status_modifier)
489 }
490}