Skip to main content

pito_list/
list.rs

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}