Skip to main content

pito_tui/
pick.rs

1use std::borrow::Cow;
2
3use pito_footer::{Hint, Key};
4use pito_list::{Cell, Column, Keys, List, Row};
5use ratatui::{
6    Frame,
7    layout::Rect,
8    text::Line,
9    widgets::{Block, BorderType, Clear, Paragraph},
10};
11
12use crate::palette::Palette;
13use crate::text::cells;
14
15const CANCEL: &[Key] = &[Key::Esc, Key::Char('q')];
16const GAP: usize = 2;
17const NARROWEST: usize = 24;
18const HEADING: u16 = 2;
19
20#[non_exhaustive]
21pub struct Pick {
22    pub(crate) title: Cow<'static, str>,
23    pub(crate) rows: List,
24    pub(crate) hints: Vec<Hint<'static>>,
25    pub(crate) cancel: &'static [Key],
26    columns: Vec<Column<'static>>,
27    full: bool,
28}
29
30impl Pick {
31    pub fn new(
32        title: impl Into<Cow<'static, str>>,
33        options: impl IntoIterator<Item = impl Into<String>>,
34    ) -> Self {
35        let rows = options
36            .into_iter()
37            .map(|option| Row::new([Cell::new(option.into())]));
38        Pick::rows(title, rows)
39    }
40
41    pub fn rows(title: impl Into<Cow<'static, str>>, rows: impl IntoIterator<Item = Row>) -> Self {
42        Pick {
43            title: title.into(),
44            rows: List::new().with_rows(rows).keys(Keys::VIM),
45            hints: Vec::new(),
46            cancel: CANCEL,
47            columns: Vec::new(),
48            full: false,
49        }
50    }
51
52    pub fn selected(mut self, index: usize) -> Self {
53        self.rows.select(index);
54        self
55    }
56
57    pub fn hints(mut self, hints: impl IntoIterator<Item = Hint<'static>>) -> Self {
58        self.hints = hints.into_iter().collect();
59        self
60    }
61
62    pub fn keys(mut self, keys: Keys) -> Self {
63        self.rows = self.rows.keys(keys);
64        self
65    }
66
67    pub fn cancel_keys(mut self, keys: &'static [Key]) -> Self {
68        self.cancel = keys;
69        self
70    }
71
72    pub fn columns(mut self, columns: impl IntoIterator<Item = Column<'static>>) -> Self {
73        self.columns = columns.into_iter().collect();
74        self
75    }
76
77    pub fn full(mut self, full: bool) -> Self {
78        self.full = full;
79        self
80    }
81
82    fn laid(&self, count: usize) -> Vec<Column<'static>> {
83        if !self.columns.is_empty() {
84            return self.columns.clone();
85        }
86        let mut columns = vec![Column::new("", 4, 0).flex()];
87        columns.extend((1..count).map(|_| Column::new("", 0, 0).fit(40).right()));
88        columns
89    }
90
91    fn fill(&mut self, frame: &mut Frame, area: Rect, palette: &Palette, count: usize) {
92        frame.render_widget(Clear, area);
93        frame.render_widget(Block::new().style(palette.base), area);
94        let heading = Line::styled(self.title.to_string(), palette.selected);
95        let top = Rect {
96            height: area.height.min(1),
97            ..area
98        };
99        frame.render_widget(Paragraph::new(heading), top);
100        let below = area.height.min(HEADING);
101        let list = Rect {
102            y: area.y + below,
103            height: area.height - below,
104            ..area
105        };
106        let columns = self.laid(count);
107        let view = palette.view(&mut self.rows, &columns).header(false);
108        frame.render_widget(view, list);
109    }
110
111    pub(crate) fn draw(&mut self, frame: &mut Frame, area: Rect, palette: &Palette) {
112        let rows = self.rows.rows();
113        let count = rows.iter().map(|row| row.cells().len()).max().unwrap_or(1);
114        if self.full {
115            self.fill(frame, area, palette, count);
116            return;
117        }
118        let widest = rows
119            .iter()
120            .map(|row| {
121                row.cells()
122                    .iter()
123                    .map(|cell| cells(cell.text()) + GAP)
124                    .sum::<usize>()
125            })
126            .max()
127            .unwrap_or(0);
128        let wanted = (widest + 6).max(cells(&self.title) + 6).max(NARROWEST);
129        let width = u16::try_from(wanted).unwrap_or(u16::MAX).min(area.width);
130        let tall = u16::try_from(rows.len() + 2).unwrap_or(u16::MAX);
131        let height = tall.min(area.height);
132        let place = Rect::new(
133            area.x + (area.width - width) / 2,
134            area.y + (area.height - height) / 3,
135            width,
136            height,
137        );
138        frame.render_widget(Clear, place);
139        let title = Line::styled(format!(" {} ", self.title), palette.selected);
140        let block = Block::bordered()
141            .border_type(BorderType::Rounded)
142            .border_style(palette.rule)
143            .style(palette.base)
144            .title(title);
145        let inner = block.inner(place);
146        frame.render_widget(block, place);
147        let columns = self.laid(count);
148        let view = palette.view(&mut self.rows, &columns).header(false);
149        frame.render_widget(view, inner);
150    }
151}