Skip to main content

pito_list/
view.rs

1use std::{fmt::Write, iter};
2
3use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
4
5use crate::{
6    Bar, Column, Count, List, Place, Row, Source,
7    bar::{Stack, thumb},
8    row::{Kind, Paint},
9    text::{self, blank, put, put_runs},
10};
11
12pub const CURSOR: &str = "▌ ";
13pub const MAX_COLUMNS: usize = 32;
14const GAP: u16 = 2;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct Styles {
19    pub(crate) selected: Style,
20    pub(crate) ink: Style,
21    pub(crate) faint: Style,
22    pub(crate) base: Style,
23    pub(crate) header: Option<Style>,
24    pub(crate) header_key: Option<Style>,
25    pub(crate) cursor: Option<Style>,
26    pub(crate) keep: bool,
27    pub(crate) track: Option<Style>,
28    pub(crate) thumb: Option<Style>,
29    pub(crate) count: Option<Style>,
30}
31
32impl Styles {
33    pub const fn new() -> Self {
34        Styles {
35            selected: Style::new(),
36            ink: Style::new(),
37            faint: Style::new(),
38            base: Style::new(),
39            header: None,
40            header_key: None,
41            cursor: None,
42            keep: false,
43            track: None,
44            thumb: None,
45            count: None,
46        }
47    }
48
49    pub const fn selected(mut self, style: Style) -> Self {
50        self.selected = style;
51        self
52    }
53
54    pub const fn ink(mut self, style: Style) -> Self {
55        self.ink = style;
56        self
57    }
58
59    pub const fn faint(mut self, style: Style) -> Self {
60        self.faint = style;
61        self
62    }
63
64    pub const fn base(mut self, style: Style) -> Self {
65        self.base = style;
66        self
67    }
68
69    pub const fn header(mut self, style: Style) -> Self {
70        self.header = Some(style);
71        self
72    }
73
74    pub const fn header_key(mut self, style: Style) -> Self {
75        self.header_key = Some(style);
76        self
77    }
78
79    pub const fn cursor(mut self, style: Style) -> Self {
80        self.cursor = Some(style);
81        self
82    }
83
84    pub const fn keep_colours(mut self, keep: bool) -> Self {
85        self.keep = keep;
86        self
87    }
88
89    pub const fn track(mut self, style: Style) -> Self {
90        self.track = Some(style);
91        self
92    }
93
94    pub const fn thumb(mut self, style: Style) -> Self {
95        self.thumb = Some(style);
96        self
97    }
98
99    pub const fn count(mut self, style: Style) -> Self {
100        self.count = Some(style);
101        self
102    }
103
104    fn resolve(&self, paint: Paint) -> Style {
105        match paint {
106            Paint::Ink => self.ink,
107            Paint::Faint => self.faint,
108            Paint::Own(style) => style,
109        }
110    }
111}
112
113impl Default for Styles {
114    fn default() -> Self {
115        Styles::new()
116    }
117}
118
119struct Layout {
120    widths: [u16; MAX_COLUMNS],
121    kept: [bool; MAX_COLUMNS],
122    right: [bool; MAX_COLUMNS],
123    count: usize,
124    flex: usize,
125}
126
127fn small(value: usize) -> u16 {
128    u16::try_from(value).unwrap_or(u16::MAX)
129}
130
131impl Layout {
132    fn new(columns: &[Column], width: u16, lead: u16, gap: u16) -> Self {
133        let count = columns.len().min(MAX_COLUMNS);
134        let mut layout = Layout {
135            widths: [0; MAX_COLUMNS],
136            kept: [false; MAX_COLUMNS],
137            right: [false; MAX_COLUMNS],
138            count,
139            flex: 0,
140        };
141        for (right, column) in layout.right.iter_mut().zip(columns) {
142            *right = column.right;
143        }
144        if count == 0 {
145            return layout;
146        }
147        layout.flex = columns[..count]
148            .iter()
149            .position(|column| column.flex)
150            .unwrap_or(count - 1);
151        layout.kept[..count].fill(true);
152        while layout.need(columns, lead, gap) > usize::from(width) {
153            let mut pick: Option<usize> = None;
154            for at in 0..count {
155                if at != layout.flex && layout.kept[at] && !columns[at].pinned {
156                    match pick {
157                        Some(held) if columns[held].rank > columns[at].rank => {}
158                        _ => pick = Some(at),
159                    }
160                }
161            }
162            match pick {
163                Some(at) => layout.kept[at] = false,
164                None => break,
165            }
166        }
167        layout
168    }
169
170    fn need(&self, columns: &[Column], lead: u16, gap: u16) -> usize {
171        let mut total = usize::from(lead).saturating_add(usize::from(columns[self.flex].min));
172        for (at, column) in columns.iter().enumerate().take(self.count) {
173            if at != self.flex && self.kept[at] {
174                total = total
175                    .saturating_add(usize::from(column.min))
176                    .saturating_add(usize::from(gap));
177            }
178        }
179        total
180    }
181
182    fn fits(&self, columns: &[Column], at: usize) -> bool {
183        at != self.flex && self.kept[at] && columns[at].fit.is_some()
184    }
185
186    fn size(&mut self, columns: &[Column], width: u16, lead: u16, gap: u16, fitted: &[u16]) {
187        if self.count == 0 {
188            return;
189        }
190        let width = usize::from(width);
191        let mut extra = width.saturating_sub(self.need(columns, lead, gap));
192        let mut used = usize::from(lead);
193        for (at, column) in columns.iter().enumerate().take(self.count) {
194            if at == self.flex || !self.kept[at] {
195                continue;
196            }
197            let min = usize::from(column.min);
198            let preferred = match column.fit {
199                Some(cap) => fitted[at].min(cap),
200                None => column.preferred,
201            };
202            let grow = usize::from(preferred).saturating_sub(min).min(extra);
203            extra -= grow;
204            let size = min + grow;
205            self.widths[at] = small(size);
206            used = used.saturating_add(size).saturating_add(usize::from(gap));
207        }
208        self.widths[self.flex] = small(width.saturating_sub(used));
209    }
210
211    fn places(&self, from: u16, edge: u16, gap: u16) -> impl Iterator<Item = (usize, u16, u16)> {
212        (0..self.count)
213            .filter(|&at| self.kept[at])
214            .scan(from, move |x, at| {
215                let start = *x;
216                *x = start.saturating_add(self.widths[at]).saturating_add(gap);
217                Some((at, start, self.widths[at].min(edge.saturating_sub(start))))
218            })
219            .filter(|&(_, _, width)| width > 0)
220    }
221}
222
223fn fitted<S: Source>(
224    list: &List<S>,
225    columns: &[Column],
226    layout: &Layout,
227    header: bool,
228    lines: usize,
229) -> [u16; MAX_COLUMNS] {
230    let mut widths = [0; MAX_COLUMNS];
231    let count = layout.count;
232    if !(0..count).any(|at| layout.fits(columns, at)) {
233        return widths;
234    }
235    if header {
236        for (at, width) in widths.iter_mut().enumerate().take(count) {
237            if layout.fits(columns, at) {
238                *width = text::cells(columns[at].title);
239            }
240        }
241    }
242    let from = list.top();
243    let to = from.saturating_add(lines).min(list.len());
244    for index in from..to {
245        let row = list.row(index);
246        if row.kind != Kind::Item {
247            continue;
248        }
249        for (at, cell) in row.cells.iter().enumerate().take(count) {
250            if !layout.kept[at] {
251                continue;
252            }
253            if cell.rest {
254                break;
255            }
256            if layout.fits(columns, at) {
257                let cells = cell.pieces().fold(0usize, |total, (text, _)| {
258                    total.saturating_add(text::width(text))
259                });
260                widths[at] = widths[at].max(small(cells));
261            }
262        }
263    }
264    widths
265}
266
267fn columns_line<'r, I>(
268    buf: &mut Buffer,
269    clip: Rect,
270    y: u16,
271    from: u16,
272    layout: &Layout,
273    gap: u16,
274    mut cell: impl FnMut(usize) -> Option<(I, bool)>,
275) -> u16
276where
277    I: Iterator<Item = (&'r str, Style)> + Clone,
278{
279    let mut x = from;
280    let mut ink = 0;
281    for at in 0..layout.count {
282        if !layout.kept[at] {
283            continue;
284        }
285        let size = layout.widths[at];
286        if let Some((runs, rest)) = cell(at) {
287            if rest {
288                let room = clip.right().saturating_sub(x);
289                return ink.max(put_runs(buf, clip, x, y, room, runs, layout.right[at]));
290            }
291            ink = ink.max(put_runs(buf, clip, x, y, size, runs, layout.right[at]));
292        }
293        x = x.saturating_add(size).saturating_add(gap);
294    }
295    ink
296}
297
298#[derive(Debug)]
299pub struct ListView<'a, S = Vec<Row>> {
300    list: &'a mut List<S>,
301    columns: &'a [Column<'a>],
302    styles: Styles,
303    cursor: &'a str,
304    gap: u16,
305    header: bool,
306    key: Option<usize>,
307    end: Option<&'a str>,
308    empty: Option<&'a str>,
309    bar: Option<Bar<'a>>,
310    count: Option<Count<'a>>,
311}
312
313impl<'a, S: Source> ListView<'a, S> {
314    pub fn new(list: &'a mut List<S>, columns: &'a [Column<'a>]) -> Self {
315        ListView {
316            list,
317            columns,
318            styles: Styles::new(),
319            cursor: CURSOR,
320            gap: GAP,
321            header: false,
322            key: None,
323            end: None,
324            empty: None,
325            bar: None,
326            count: None,
327        }
328    }
329
330    pub fn scrollbar(mut self, bar: Option<Bar<'a>>) -> Self {
331        self.bar = bar;
332        self
333    }
334
335    pub fn count(mut self, count: Option<Count<'a>>) -> Self {
336        self.count = count;
337        self
338    }
339
340    pub fn styles(mut self, styles: Styles) -> Self {
341        self.styles = styles;
342        self
343    }
344
345    pub fn cursor(mut self, cursor: &'a str) -> Self {
346        self.cursor = cursor;
347        self
348    }
349
350    pub fn gap(mut self, gap: u16) -> Self {
351        self.gap = gap;
352        self
353    }
354
355    pub fn header(mut self, header: bool) -> Self {
356        self.header = header;
357        self
358    }
359
360    pub fn key_column(mut self, column: Option<usize>) -> Self {
361        self.key = column;
362        self
363    }
364
365    pub fn end(mut self, end: Option<&'a str>) -> Self {
366        self.end = end;
367        self
368    }
369
370    pub fn empty(mut self, empty: Option<&'a str>) -> Self {
371        self.empty = empty;
372        self
373    }
374}
375
376struct Line<'a> {
377    styles: Styles,
378    chosen: Style,
379    layout: Layout,
380    gap: u16,
381    indent: u16,
382    lead: u16,
383    cursor: &'a str,
384    mark_width: u16,
385    area: Rect,
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389enum Lit {
390    No,
391    Range,
392    Cursor,
393}
394
395impl Line<'_> {
396    fn paint(&self, paint: Paint, chosen: bool) -> Style {
397        match (chosen, self.styles.keep) {
398            (false, _) => self.styles.resolve(paint),
399            (true, false) => self.styles.selected,
400            (true, true) => self.styles.resolve(paint).patch(self.styles.selected),
401        }
402    }
403
404    fn row(&self, buf: &mut Buffer, y: u16, row: &Row, lit: Lit) -> u16 {
405        match row.kind {
406            Kind::Item => self.item(buf, y, row, lit),
407            Kind::Heading => self.heading(buf, y, row),
408            Kind::Blank => 0,
409        }
410    }
411
412    fn item(&self, buf: &mut Buffer, y: u16, row: &Row, lit: Lit) -> u16 {
413        let area = self.area;
414        let mut x = area.x;
415        let room = area.width;
416        let chosen = lit != Lit::No;
417        let mut ink = 0;
418        if chosen {
419            blank(buf, area, x, y, room, self.chosen);
420        }
421        if lit == Lit::Cursor {
422            let style = self.styles.cursor.unwrap_or(self.styles.selected);
423            ink = put(buf, area, x, y, self.indent, self.cursor, style);
424        }
425        x = x.saturating_add(self.indent);
426        if let Some(mark) = &row.mark {
427            ink = ink.max(put(
428                buf,
429                area,
430                x,
431                y,
432                self.mark_width,
433                &mark.text,
434                self.paint(mark.paint, chosen),
435            ));
436        }
437        x = x.saturating_add(self.lead - self.indent);
438        let cells = columns_line(buf, area, y, x, &self.layout, self.gap, |at| {
439            let cell = row.cells.get(at)?;
440            let runs = cell
441                .pieces()
442                .map(move |(text, paint)| (text, self.paint(paint, chosen)));
443            Some((runs, cell.rest))
444        });
445        ink.max(cells)
446    }
447
448    fn heading(&self, buf: &mut Buffer, y: u16, row: &Row) -> u16 {
449        let Some(cell) = row.cells.first() else {
450            return 0;
451        };
452        let x = self.area.x.saturating_add(self.indent);
453        let room = self.area.width.saturating_sub(self.indent);
454        let runs = cell
455            .pieces()
456            .map(|(text, paint)| (text, self.styles.resolve(paint)));
457        put_runs(buf, self.area, x, y, room, runs, false)
458    }
459
460    fn header(&self, buf: &mut Buffer, wide: Rect, columns: &[Column], key: Option<usize>) {
461        let styles = self.styles;
462        let y = wide.y;
463        if let Some(style) = styles.header {
464            blank(buf, wide, wide.x, y, wide.width, styles.base.patch(style));
465        }
466        let title = styles.header.unwrap_or(styles.faint);
467        let x = self.area.x.saturating_add(self.lead);
468        columns_line(buf, self.area, y, x, &self.layout, self.gap, |at| {
469            let style = match styles.header_key {
470                Some(style) if key == Some(at) => style,
471                _ => title,
472            };
473            Some((iter::once((columns[at].title, style)), false))
474        });
475    }
476
477    fn note(&self, buf: &mut Buffer, y: u16, text: &str) -> u16 {
478        let x = self.area.x.saturating_add(self.indent);
479        let room = self.area.width.saturating_sub(self.indent);
480        put(buf, self.area, x, y, room, text, self.styles.faint)
481    }
482
483    fn bar(&self, buf: &mut Buffer, bar: &Bar, x: u16, y: u16, top: usize, lines: usize) {
484        let height = usize::from(self.area.bottom().saturating_sub(y));
485        let unit = if bar.halves.is_some() { 2 } else { 1 };
486        let (start, size) = thumb(top, lines, height, unit);
487        let end = start.saturating_add(size);
488        let track = self.styles.track.unwrap_or(self.styles.faint);
489        let lit = self
490            .styles
491            .thumb
492            .or(self.styles.cursor)
493            .unwrap_or(self.styles.selected);
494        let clip = Rect {
495            x,
496            width: 1,
497            ..self.area
498        };
499        for offset in 0..height {
500            let from = offset * unit;
501            let upper = (start..end).contains(&from);
502            let lower = (start..end).contains(&(from + unit - 1));
503            let at = y.saturating_add(small(offset));
504            match bar.glyph(upper, lower) {
505                Some(glyph) => put(buf, clip, x, at, 1, glyph, lit),
506                None => put(buf, clip, x, at, 1, bar.track, track),
507            };
508        }
509    }
510}
511
512fn label(
513    buf: &mut Buffer,
514    count: &Count,
515    shown: (usize, usize, usize),
516    style: Style,
517    line: Rect,
518    free: Option<u16>,
519) {
520    let mut text = Stack::new();
521    let (first, last, total) = shown;
522    if write!(text, "{}", count.label(first, last, total)).is_err() {
523        return;
524    }
525    let text = text.text();
526    if let Some(free) = free
527        && line.right().saturating_sub(text::cells(text)) < free
528    {
529        return;
530    }
531    let runs = iter::once((text, style));
532    put_runs(buf, line, line.x, line.y, line.width, runs, true);
533}
534
535impl<S: Source> Widget for ListView<'_, S> {
536    fn render(self, area: Rect, buf: &mut Buffer) {
537        let area = area.intersection(buf.area);
538        if area.is_empty() {
539            self.list.place(iter::empty());
540            self.list.drawn(None, false, false);
541            return;
542        }
543        let ListView {
544            list,
545            columns,
546            styles,
547            cursor,
548            gap,
549            header,
550            key,
551            end,
552            empty,
553            bar,
554            count,
555        } = self;
556        for y in area.top()..area.bottom() {
557            blank(buf, area, area.x, y, area.width, styles.base);
558        }
559        let indent = text::cells(cursor);
560        let mark_width = list.mark_width();
561        let lead = indent
562            .saturating_add(mark_width)
563            .saturating_add(u16::from(mark_width > 0));
564        let y = area.y.saturating_add(u16::from(header));
565        let body = area.bottom().saturating_sub(y);
566        let len = list.len();
567        let lines = if len == 0 {
568            0
569        } else {
570            len.saturating_add(usize::from(end.is_some()))
571        };
572        let over = body > 0 && lines > usize::from(body);
573        let count = count.filter(|_| over);
574        let cut = count.is_some_and(|count| count.place == Place::Line && body > 1);
575        let bar = bar.filter(|_| over && area.width > 1);
576        let rows = Rect {
577            width: area.width - u16::from(bar.is_some()),
578            height: area.height - u16::from(cut),
579            ..area
580        };
581        let height = usize::from(rows.bottom().saturating_sub(y));
582        let selected = list.follow(height, end.is_some());
583        let mut layout = Layout::new(columns, rows.width, lead, gap);
584        let fitted = fitted(list, columns, &layout, header, height);
585        layout.size(columns, rows.width, lead, gap, &fitted);
586        list.place(layout.places(rows.x.saturating_add(lead), rows.right(), gap));
587        let line = Line {
588            styles,
589            chosen: styles.base.patch(styles.selected),
590            layout,
591            gap,
592            indent,
593            lead,
594            cursor,
595            mark_width,
596            area: rows,
597        };
598        if header {
599            line.header(buf, Rect { height: 1, ..area }, columns, key);
600        }
601        if list.is_empty() {
602            list.drawn(None, false, false);
603            if let Some(text) = empty
604                && height > 0
605            {
606                line.note(buf, y, text);
607            }
608            return;
609        }
610        let top = list.top();
611        let mut ink = 0;
612        for offset in 0..height {
613            let index = top.saturating_add(offset);
614            let at = y.saturating_add(small(offset));
615            if index < len {
616                let lit = if selected == Some(index) {
617                    Lit::Cursor
618                } else if list.in_range(index) {
619                    Lit::Range
620                } else {
621                    Lit::No
622                };
623                ink = line.row(buf, at, &list.row(index), lit);
624                continue;
625            }
626            ink = 0;
627            if let Some(text) = end
628                && index == len
629            {
630                ink = line.note(buf, at, text);
631            }
632            break;
633        }
634        let on = len.saturating_sub(top).min(height);
635        let shown = (on > 0).then(|| (top + 1, top + on, len));
636        list.drawn(shown, cut, bar.is_some());
637        if let Some(bar) = bar {
638            line.bar(buf, &bar, area.right() - 1, y, top, lines);
639        }
640        let (Some(count), Some(shown)) = (count, shown) else {
641            return;
642        };
643        let style = styles.count.unwrap_or(styles.faint);
644        let (at, free) = match count.place {
645            Place::Line if cut => (
646                Rect {
647                    y: rows.bottom(),
648                    height: 1,
649                    ..area
650                },
651                None,
652            ),
653            _ => {
654                let last = y.saturating_add(small(height)).saturating_sub(1);
655                let free = if ink == 0 {
656                    rows.x
657                } else {
658                    ink.saturating_add(1)
659                };
660                (
661                    Rect {
662                        y: last,
663                        height: 1,
664                        ..rows
665                    },
666                    Some(free),
667                )
668            }
669        };
670        label(buf, &count, shown, style, at, free);
671    }
672}