Skip to main content

pito_footer/
confirm.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::{Position, Rect},
4    style::{Modifier, Style},
5    widgets::Widget,
6};
7
8use crate::key::Key;
9use crate::styles::{Styles, Tone};
10use crate::text::{self, Pen};
11
12const MARK: &str = "▸ ";
13const BEFORE: u16 = 2;
14const BETWEEN: u16 = 3;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum Answer {
19    Yes,
20    No,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct ConfirmKeys {
26    pub yes: &'static [Key],
27    pub no: &'static [Key],
28    pub choose: bool,
29    pub toggle: &'static [Key],
30    pub accept: &'static [Key],
31}
32
33impl ConfirmKeys {
34    pub const HEY: ConfirmKeys = ConfirmKeys {
35        yes: &[Key::Char('y'), Key::Char('Y')],
36        no: &[Key::Char('n'), Key::Char('N'), Key::Esc],
37        choose: true,
38        toggle: &[
39            Key::Left,
40            Key::Right,
41            Key::Tab,
42            Key::BackTab,
43            Key::Char('h'),
44            Key::Char('l'),
45        ],
46        accept: &[Key::Enter],
47    };
48
49    pub const ENTER: ConfirmKeys = ConfirmKeys {
50        yes: &[Key::Enter],
51        no: &[Key::Esc],
52        choose: false,
53        toggle: &[],
54        accept: &[],
55    };
56
57    pub const fn yes(mut self, keys: &'static [Key]) -> Self {
58        self.yes = keys;
59        self
60    }
61
62    pub const fn no(mut self, keys: &'static [Key]) -> Self {
63        self.no = keys;
64        self
65    }
66
67    pub const fn choose(mut self, choose: bool) -> Self {
68        self.choose = choose;
69        self
70    }
71
72    pub const fn toggle(mut self, keys: &'static [Key]) -> Self {
73        self.toggle = keys;
74        self
75    }
76
77    pub const fn accept(mut self, keys: &'static [Key]) -> Self {
78        self.accept = keys;
79        self
80    }
81}
82
83impl Default for ConfirmKeys {
84    fn default() -> Self {
85        ConfirmKeys::HEY
86    }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct Confirm {
91    yes: bool,
92    keys: ConfirmKeys,
93}
94
95impl Default for Confirm {
96    fn default() -> Self {
97        Confirm::new()
98    }
99}
100
101impl Confirm {
102    pub const fn new() -> Self {
103        Confirm {
104            yes: false,
105            keys: ConfirmKeys::HEY,
106        }
107    }
108
109    pub const fn keys(mut self, keys: ConfirmKeys) -> Self {
110        self.keys = keys;
111        self
112    }
113
114    pub const fn start(mut self, answer: Answer) -> Self {
115        self.yes = matches!(answer, Answer::Yes);
116        self
117    }
118
119    pub fn yes(&self) -> bool {
120        self.yes
121    }
122
123    pub fn choosing(&self) -> bool {
124        self.keys.choose
125    }
126
127    pub fn key(&mut self, key: Key) -> Option<Answer> {
128        if self.keys.yes.contains(&key) {
129            return Some(Answer::Yes);
130        }
131        if self.keys.no.contains(&key) {
132            return Some(Answer::No);
133        }
134        if !self.keys.choose {
135            return None;
136        }
137        if self.keys.accept.contains(&key) {
138            return Some(if self.yes { Answer::Yes } else { Answer::No });
139        }
140        if self.keys.toggle.contains(&key) {
141            self.yes = !self.yes;
142        }
143        None
144    }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148#[non_exhaustive]
149pub struct Words<'a> {
150    pub yes: &'a str,
151    pub no: &'a str,
152    pub hint: Option<&'a str>,
153}
154
155impl<'a> Words<'a> {
156    pub const fn new(yes: &'a str, no: &'a str) -> Self {
157        Words {
158            yes,
159            no,
160            hint: None,
161        }
162    }
163
164    pub const fn hint(mut self, hint: &'a str) -> Self {
165        self.hint = Some(hint);
166        self
167    }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct ConfirmBar<'a> {
172    question: &'a str,
173    confirm: Confirm,
174    words: Words<'a>,
175    rule: bool,
176    inline: bool,
177    tone: Tone,
178    styles: Option<Styles>,
179}
180
181impl<'a> ConfirmBar<'a> {
182    pub fn new(question: &'a str, confirm: &Confirm, words: Words<'a>) -> Self {
183        ConfirmBar {
184            question,
185            confirm: *confirm,
186            words,
187            rule: true,
188            inline: false,
189            tone: Tone::Ink,
190            styles: None,
191        }
192    }
193
194    pub fn rule(mut self, rule: bool) -> Self {
195        self.rule = rule;
196        self
197    }
198
199    pub fn inline(mut self, inline: bool) -> Self {
200        self.inline = inline;
201        self
202    }
203
204    pub fn tone(mut self, tone: Tone) -> Self {
205        self.tone = tone;
206        self
207    }
208
209    pub fn styles(mut self, styles: Styles) -> Self {
210        self.styles = Some(styles);
211        self
212    }
213
214    pub(crate) fn styled(mut self, fallback: Styles) -> Self {
215        self.styles = Some(self.styles.unwrap_or(fallback));
216        self
217    }
218
219    fn look(&self) -> Styles {
220        self.styles.unwrap_or_default()
221    }
222
223    pub fn height(&self) -> u16 {
224        let hint = !self.inline && self.words.hint.is_some();
225        1 + u16::from(self.rule) + u16::from(hint)
226    }
227
228    fn choices_width(&self) -> u16 {
229        if !self.confirm.keys.choose {
230            return 0;
231        }
232        BEFORE
233            .saturating_add(text::width(MARK))
234            .saturating_add(text::width(self.words.yes))
235            .saturating_add(BETWEEN)
236            .saturating_add(text::width(self.words.no))
237    }
238
239    fn question_style(&self) -> Style {
240        self.tone.style(&self.look()).add_modifier(Modifier::BOLD)
241    }
242
243    fn draw_choices(&self, pen: &mut Pen) {
244        if !self.confirm.keys.choose {
245            return;
246        }
247        pen.skip(BEFORE);
248        let styles = self.look();
249        let lit = styles.lit();
250        let muted = styles.muted;
251        if self.confirm.yes {
252            pen.put(MARK, lit);
253            pen.put(self.words.yes, lit);
254            pen.skip(BETWEEN);
255            pen.put(self.words.no, muted);
256        } else {
257            pen.put(self.words.yes, muted);
258            pen.skip(BETWEEN);
259            pen.put(MARK, lit);
260            pen.put(self.words.no, lit);
261        }
262    }
263}
264
265impl Widget for ConfirmBar<'_> {
266    fn render(self, area: Rect, buf: &mut Buffer) {
267        (&self).render(area, buf);
268    }
269}
270
271impl Widget for &ConfirmBar<'_> {
272    fn render(self, area: Rect, buf: &mut Buffer) {
273        self.paint(area, buf);
274    }
275}
276
277impl ConfirmBar<'_> {
278    pub(crate) fn paint(&self, area: Rect, buf: &mut Buffer) -> Option<Position> {
279        if area.is_empty() {
280            return None;
281        }
282        let mut end = None;
283        let mut y = area.y;
284        let styles = self.look();
285        if self.rule {
286            text::rule(buf, area, styles.rule);
287            y += 1;
288        }
289        let muted = styles.muted;
290        if let Some(mut pen) = Pen::new(buf, area, area.x, y) {
291            let choices = self.choices_width();
292            let room = area.width.saturating_sub(choices);
293            let least = text::width(self.question).min(area.width / 2).max(1);
294            if room >= least {
295                pen.clip(self.question, self.question_style(), room);
296                self.draw_choices(&mut pen);
297            } else {
298                pen.clip(self.question, self.question_style(), area.width);
299            }
300            if self.inline
301                && let Some(hint) = self.words.hint
302            {
303                pen.indented(hint, muted);
304                return Some(Position::new(pen.x, y));
305            }
306            end = Some(Position::new(pen.x, y));
307            y += 1;
308        }
309        if let Some(hint) = self.words.hint
310            && let Some(mut pen) = Pen::new(buf, area, area.x, y)
311        {
312            pen.indented(hint, muted);
313            end = Some(Position::new(pen.x, y));
314        }
315        end
316    }
317}