1use std::{borrow::Cow, sync::Arc};
2
3use ratatui::layout::Rect;
4
5use crate::{Key, MAX_COLUMNS, Row, row::Kind};
6
7const PAGE: usize = 10;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub struct Keys {
12 pub up: &'static [Key],
13 pub down: &'static [Key],
14 pub page_up: &'static [Key],
15 pub page_down: &'static [Key],
16 pub first: &'static [Key],
17 pub last: &'static [Key],
18 pub open: &'static [Key],
19}
20
21impl Keys {
22 pub const fn new() -> Self {
23 Keys {
24 up: &[Key::Up],
25 down: &[Key::Down],
26 page_up: &[Key::PageUp],
27 page_down: &[Key::PageDown],
28 first: &[Key::Home],
29 last: &[Key::End],
30 open: &[Key::Enter],
31 }
32 }
33
34 pub const VIM: Keys = Keys {
35 up: &[Key::Up, Key::Char('k')],
36 down: &[Key::Down, Key::Char('j')],
37 page_up: &[Key::PageUp, Key::Ctrl('u')],
38 page_down: &[Key::PageDown, Key::Ctrl('d')],
39 first: &[Key::Home, Key::Char('g')],
40 last: &[Key::End, Key::Char('G')],
41 open: &[Key::Enter],
42 };
43
44 pub const fn up(mut self, keys: &'static [Key]) -> Self {
45 self.up = keys;
46 self
47 }
48
49 pub const fn down(mut self, keys: &'static [Key]) -> Self {
50 self.down = keys;
51 self
52 }
53
54 pub const fn page_up(mut self, keys: &'static [Key]) -> Self {
55 self.page_up = keys;
56 self
57 }
58
59 pub const fn page_down(mut self, keys: &'static [Key]) -> Self {
60 self.page_down = keys;
61 self
62 }
63
64 pub const fn first(mut self, keys: &'static [Key]) -> Self {
65 self.first = keys;
66 self
67 }
68
69 pub const fn last(mut self, keys: &'static [Key]) -> Self {
70 self.last = keys;
71 self
72 }
73
74 pub const fn open(mut self, keys: &'static [Key]) -> Self {
75 self.open = keys;
76 self
77 }
78}
79
80impl Default for Keys {
81 fn default() -> Self {
82 Keys::new()
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[non_exhaustive]
88pub enum Step {
89 Pass,
90 Held,
91 Moved,
92 Open(usize),
93}
94
95pub trait Source {
96 fn len(&self) -> usize;
97
98 fn row(&self, index: usize) -> Cow<'_, Row>;
99
100 fn is_empty(&self) -> bool {
101 self.len() == 0
102 }
103
104 fn selectable(&self, index: usize) -> bool {
105 self.row(index).selectable()
106 }
107
108 fn mark_width(&self) -> u16 {
109 0
110 }
111}
112
113macro_rules! rows {
114 ($($source:ty),*) => {$(
115 impl Source for $source {
116 fn len(&self) -> usize {
117 <[Row]>::len(self)
118 }
119
120 fn row(&self, index: usize) -> Cow<'_, Row> {
121 Cow::Borrowed(&self[index])
122 }
123
124 fn selectable(&self, index: usize) -> bool {
125 self[index].selectable()
126 }
127
128 fn mark_width(&self) -> u16 {
129 self.iter().map(Row::mark_width).max().unwrap_or(0)
130 }
131 }
132 )*};
133}
134
135rows!(Vec<Row>, Arc<Vec<Row>>, Arc<[Row]>);
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
138#[non_exhaustive]
139pub enum Paging {
140 #[default]
141 Cursor,
142 View,
143}
144
145#[derive(Debug, Clone)]
146pub struct List<S = Vec<Row>> {
147 source: S,
148 selected: usize,
149 top: usize,
150 page: usize,
151 mark_width: u16,
152 touched: Option<(usize, u16)>,
153 rescan: bool,
154 range: Option<(usize, usize)>,
155 keys: Keys,
156 paging: Paging,
157 placed: [(usize, u16, u16); MAX_COLUMNS],
158 placed_len: usize,
159 shown: Option<(usize, usize, usize)>,
160 cut: bool,
161 bar: bool,
162}
163
164impl Default for List {
165 fn default() -> Self {
166 List::new()
167 }
168}
169
170impl List {
171 pub fn new() -> Self {
172 List::from_source(Vec::new())
173 }
174
175 pub fn with_rows(mut self, rows: impl IntoIterator<Item = Row>) -> Self {
176 self.set_rows(rows);
177 self
178 }
179
180 pub fn set_rows(&mut self, rows: impl IntoIterator<Item = Row>) {
181 self.source.clear();
182 self.mark_width = 0;
183 self.touched = None;
184 self.rescan = false;
185 for row in rows {
186 self.push(row);
187 }
188 self.settle();
189 }
190
191 pub fn push(&mut self, row: Row) {
192 self.mark_width = self.mark_width.max(row.mark_width());
193 self.source.push(row);
194 }
195
196 pub fn clear(&mut self) {
197 self.source.clear();
198 self.mark_width = 0;
199 self.touched = None;
200 self.rescan = false;
201 self.selected = 0;
202 self.top = 0;
203 }
204
205 pub fn rows(&self) -> &[Row] {
206 &self.source
207 }
208
209 pub fn selected_row(&self) -> Option<&Row> {
210 self.source.get(self.selected()?)
211 }
212
213 pub fn row_mut(&mut self, index: usize) -> Option<&mut Row> {
214 self.settle_marks();
215 let before = self.source.get(index)?.mark_width();
216 self.touched = Some((index, before));
217 self.source.get_mut(index)
218 }
219}
220
221impl<S: Source> List<S> {
222 pub fn from_source(source: S) -> Self {
223 let mark_width = source.mark_width();
224 List {
225 source,
226 selected: 0,
227 top: 0,
228 page: PAGE,
229 mark_width,
230 touched: None,
231 rescan: false,
232 range: None,
233 keys: Keys::new(),
234 paging: Paging::Cursor,
235 placed: [(0, 0, 0); MAX_COLUMNS],
236 placed_len: 0,
237 shown: None,
238 cut: false,
239 bar: false,
240 }
241 }
242
243 pub fn keys(mut self, keys: Keys) -> Self {
244 self.keys = keys;
245 self
246 }
247
248 pub fn paging(mut self, paging: Paging) -> Self {
249 self.paging = paging;
250 self
251 }
252
253 pub fn source(&self) -> &S {
254 &self.source
255 }
256
257 pub fn set_source(&mut self, source: S) {
258 self.source = source;
259 self.mark_width = self.source.mark_width();
260 self.touched = None;
261 self.rescan = false;
262 self.settle();
263 }
264
265 pub fn update(&mut self, change: impl FnOnce(&mut S)) {
266 change(&mut self.source);
267 self.mark_width = self.source.mark_width();
268 self.touched = None;
269 self.rescan = false;
270 self.settle();
271 }
272
273 fn settle(&mut self) {
274 let last = self.source.len().saturating_sub(1);
275 self.selected = self.selected.min(last);
276 self.top = self.top.min(last);
277 }
278
279 pub fn len(&self) -> usize {
280 self.source.len()
281 }
282
283 pub fn is_empty(&self) -> bool {
284 self.source.is_empty()
285 }
286
287 pub fn selected(&self) -> Option<usize> {
288 let last = self.source.len().checked_sub(1)?;
289 let at = self.selected.min(last);
290 self.next(at).or_else(|| self.prev(at))
291 }
292
293 pub fn select(&mut self, index: usize) {
294 let at = index.min(self.source.len().saturating_sub(1));
295 self.selected = self.next(at).or_else(|| self.prev(at)).unwrap_or(at);
296 }
297
298 pub fn select_range(&mut self, range: Option<(usize, usize)>) {
299 self.range = range;
300 }
301
302 pub fn range(&self) -> Option<(usize, usize)> {
303 self.range
304 }
305
306 pub(crate) fn in_range(&self, index: usize) -> bool {
307 self.range
308 .is_some_and(|(from, to)| (from.min(to)..=from.max(to)).contains(&index))
309 }
310
311 pub fn top(&self) -> usize {
312 self.top
313 }
314
315 pub fn scroll_to(&mut self, top: usize) {
316 self.top = top.min(self.source.len().saturating_sub(1));
317 }
318
319 pub fn page(&self) -> usize {
320 self.page
321 }
322
323 pub fn columns(&self) -> &[(usize, u16, u16)] {
324 &self.placed[..self.placed_len]
325 }
326
327 pub fn shown(&self) -> Option<(usize, usize, usize)> {
328 self.shown
329 }
330
331 pub fn position(&self) -> Option<(usize, usize)> {
332 Some((self.selected()?.saturating_add(1), self.source.len()))
333 }
334
335 pub(crate) fn drawn(&mut self, shown: Option<(usize, usize, usize)>, cut: bool, bar: bool) {
336 self.shown = shown;
337 self.cut = cut;
338 self.bar = bar;
339 }
340
341 pub(crate) fn place(&mut self, placed: impl IntoIterator<Item = (usize, u16, u16)>) {
342 self.placed_len = 0;
343 for (slot, column) in self.placed.iter_mut().zip(placed) {
344 *slot = column;
345 self.placed_len += 1;
346 }
347 }
348
349 pub(crate) fn mark_width(&mut self) -> u16 {
350 self.settle_marks();
351 if self.rescan {
352 self.mark_width = self.source.mark_width();
353 self.rescan = false;
354 }
355 self.mark_width
356 }
357
358 fn settle_marks(&mut self) {
359 let Some((index, before)) = self.touched.take() else {
360 return;
361 };
362 let after = if index < self.source.len() {
363 self.source.row(index).mark_width()
364 } else {
365 0
366 };
367 if after >= self.mark_width {
368 self.mark_width = after;
369 self.rescan = false;
370 } else if before >= self.mark_width {
371 self.rescan = true;
372 }
373 }
374
375 pub(crate) fn row(&self, index: usize) -> Cow<'_, Row> {
376 self.source.row(index)
377 }
378
379 fn next(&self, from: usize) -> Option<usize> {
380 (from..self.source.len()).find(|&at| self.source.selectable(at))
381 }
382
383 fn prev(&self, from: usize) -> Option<usize> {
384 let end = from.saturating_add(1).min(self.source.len());
385 (0..end).rev().find(|&at| self.source.selectable(at))
386 }
387
388 fn go(&mut self, to: impl FnOnce(&Self, usize) -> Option<usize>) -> Step {
389 let Some(from) = self.selected() else {
390 return Step::Held;
391 };
392 self.selected = from;
393 match to(self, from) {
394 Some(to) if to != from => {
395 self.selected = to;
396 Step::Moved
397 }
398 _ => Step::Held,
399 }
400 }
401
402 pub fn up(&mut self) -> Step {
403 self.go(|list, from| list.prev(from.checked_sub(1)?))
404 }
405
406 pub fn down(&mut self) -> Step {
407 self.go(|list, from| list.next(from.saturating_add(1)))
408 }
409
410 pub fn page_up(&mut self) -> Step {
411 self.flip(|list, from| {
412 let to = from.saturating_sub(list.page);
413 list.prev(to).or_else(|| list.next(to))
414 })
415 }
416
417 pub fn page_down(&mut self) -> Step {
418 self.flip(|list, from| {
419 let to = from
420 .saturating_add(list.page)
421 .min(list.source.len().saturating_sub(1));
422 list.next(to).or_else(|| list.prev(to))
423 })
424 }
425
426 fn flip(&mut self, to: impl FnOnce(&Self, usize) -> Option<usize>) -> Step {
427 let from = self.selected();
428 let step = self.go(to);
429 if let (Step::Moved, Paging::View, Some(from)) = (step, self.paging, from) {
430 let to = self.selected;
431 self.top = if to > from {
432 self.top.saturating_add(to - from)
433 } else {
434 self.top.saturating_sub(from - to)
435 };
436 }
437 step
438 }
439
440 pub fn first(&mut self) -> Step {
441 self.go(|list, _| list.next(0))
442 }
443
444 pub fn last(&mut self) -> Step {
445 self.go(|list, _| list.prev(list.source.len().saturating_sub(1)))
446 }
447
448 pub fn key(&mut self, key: Key) -> Step {
449 let keys = self.keys;
450 if keys.up.contains(&key) {
451 self.up()
452 } else if keys.down.contains(&key) {
453 self.down()
454 } else if keys.page_up.contains(&key) {
455 self.page_up()
456 } else if keys.page_down.contains(&key) {
457 self.page_down()
458 } else if keys.first.contains(&key) {
459 self.first()
460 } else if keys.last.contains(&key) {
461 self.last()
462 } else if keys.open.contains(&key) {
463 match self.selected() {
464 Some(index) => Step::Open(index),
465 None => Step::Held,
466 }
467 } else {
468 Step::Pass
469 }
470 }
471
472 pub fn hit(&self, area: Rect, header: bool, column: u16, row: u16) -> Option<usize> {
473 let first = area.y.saturating_add(u16::from(header));
474 let right = area.right().saturating_sub(u16::from(self.bar));
475 let bottom = area.bottom().saturating_sub(u16::from(self.cut));
476 if column < area.x || column >= right || row < first || row >= bottom {
477 return None;
478 }
479 let index = self.top.saturating_add(usize::from(row - first));
480 (index < self.source.len() && self.source.selectable(index)).then_some(index)
481 }
482
483 fn heading_above(&self, at: usize, height: usize) -> usize {
484 let reach = at.saturating_sub(height.saturating_sub(1));
485 let mut head = at;
486 for above in (reach..at).rev() {
487 let row = self.source.row(above);
488 match row.kind {
489 Kind::Item => break,
490 Kind::Heading => head = above,
491 Kind::Blank => {}
492 }
493 }
494 head
495 }
496
497 pub(crate) fn follow(&mut self, height: usize, tail: bool) -> Option<usize> {
498 if height > 0 {
499 self.page = height.saturating_sub(1).max(1);
500 }
501 let len = self.source.len();
502 if height == 0 || len == 0 {
503 self.top = 0;
504 return None;
505 }
506 let lines = len + usize::from(tail);
507 let Some(selected) = self.selected() else {
508 self.top = self.top.min(lines.saturating_sub(height));
509 return None;
510 };
511 self.selected = selected;
512 if selected < self.top {
513 self.top = selected;
514 }
515 let last = selected + 1 == len;
516 let bottom = selected + 1 + usize::from(last && tail);
517 if bottom > self.top.saturating_add(height) {
518 self.top = bottom - height;
519 }
520 self.top = self.top.min(self.heading_above(selected, height));
521 self.top = self.top.min(lines.saturating_sub(height));
522 Some(selected)
523 }
524}