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}