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#[derive(Debug, Clone)]
17pub struct ResultsUI {
18 cursor: u16,
19 bottom: u16,
20 height: u16, widths: Vec<u16>, 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, 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 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 }
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 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 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}