ratatui_widgets/
button.rs

1#![allow(unused)]
2
3use ratatui::{prelude::*, widgets::Widget};
4
5use crate::events::*;
6
7#[derive(Debug, Clone)]
8pub struct Button<'text> {
9    text: Text<'text>,
10    theme: Theme,
11    state: State,
12}
13
14#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
15pub enum State {
16    #[default]
17    Normal,
18    Selected,
19    Pressed,
20}
21
22#[derive(Debug, Clone, Copy)]
23pub struct Theme {
24    normal_text: Color,
25    normal_background: Color,
26    selected_text: Color,
27    selected_background: Color,
28    pressed_text: Color,
29    pressed_background: Color,
30    highlight: Color,
31    shadow: Color,
32}
33
34impl Default for Theme {
35    fn default() -> Self {
36        themes::NORMAL
37    }
38}
39
40/// Config
41impl<'text> Button<'text> {
42    pub fn new<T: Into<Text<'text>>>(text: T) -> Self {
43        Self {
44            text: text.into(),
45            theme: Theme::default(),
46            state: State::default(),
47        }
48    }
49
50    pub fn with_theme(mut self, theme: Theme) -> Self {
51        self.theme = theme;
52        self
53    }
54}
55
56impl EventHandler for Button<'_> {
57    fn handle_key(&mut self, key_event: KeyPressedEvent) {
58        match key_event.key {
59            Key::Char(' ') | Key::Enter => self.toggle_press(),
60            _ => {}
61        }
62    }
63}
64
65impl Button<'_> {
66    pub fn toggle_press(&mut self) {
67        match self.state {
68            State::Normal => self.press(),
69            State::Selected => self.press(),
70            State::Pressed => self.select(),
71        }
72    }
73
74    pub fn press(&mut self) {
75        self.state = State::Pressed;
76    }
77
78    pub fn normal(&mut self) {
79        self.state = State::Normal;
80    }
81
82    pub fn select(&mut self) {
83        self.state = State::Selected;
84    }
85}
86
87impl Widget for &Button<'_> {
88    fn render(self, area: Rect, buf: &mut Buffer) {
89        let theme = self.theme;
90
91        // these are wrong
92        let fg = match self.state {
93            State::Normal => theme.normal_text,
94            State::Selected => theme.selected_text,
95            State::Pressed => theme.pressed_text,
96        };
97        let bg = match self.state {
98            State::Normal => theme.normal_background,
99            State::Selected => theme.selected_background,
100            State::Pressed => theme.pressed_background,
101        };
102        let (top, bottom) = if self.state == State::Pressed {
103            (theme.shadow, theme.highlight)
104        } else {
105            (theme.highlight, theme.shadow)
106        };
107
108        buf.set_style(area, (fg, bg));
109
110        let rows = area.rows().collect::<Vec<_>>();
111        let last_index = rows.len().saturating_sub(1);
112        let (first, middle, last) = match rows.len() {
113            0 | 1 => (None, &rows[..], None),
114            2 => (None, &rows[..last_index], Some(rows[last_index])),
115            _ => (Some(rows[0]), &rows[1..last_index], Some(rows[last_index])),
116        };
117
118        // render top line if there's enough space
119        if let Some(first) = first {
120            "▔"
121                .repeat(area.width as usize)
122                .fg(top)
123                .bg(bg)
124                .render(first, buf);
125        }
126        // render bottom line if there's enough space
127        if let Some(last) = last {
128            "▁"
129                .repeat(area.width as usize)
130                .fg(bottom)
131                .bg(bg)
132                .render(last, buf);
133        }
134        self.text.clone().centered().render(middle[0], buf);
135    }
136}
137
138pub mod themes {
139    use super::Theme;
140    use ratatui::style::palette::tailwind;
141
142    pub const NORMAL: Theme = Theme {
143        normal_text: tailwind::GRAY.c200,
144        normal_background: tailwind::GRAY.c800,
145        selected_text: tailwind::GRAY.c100,
146        selected_background: tailwind::GRAY.c700,
147        pressed_text: tailwind::GRAY.c300,
148        pressed_background: tailwind::GRAY.c900,
149        highlight: tailwind::GRAY.c600,
150        shadow: tailwind::GRAY.c950,
151    };
152
153    pub const RED: Theme = Theme {
154        normal_text: tailwind::RED.c200,
155        normal_background: tailwind::RED.c800,
156        selected_text: tailwind::RED.c100,
157        selected_background: tailwind::RED.c700,
158        pressed_text: tailwind::RED.c300,
159        pressed_background: tailwind::RED.c900,
160        highlight: tailwind::RED.c600,
161        shadow: tailwind::RED.c950,
162    };
163
164    pub const GREEN: Theme = Theme {
165        normal_text: tailwind::GREEN.c200,
166        normal_background: tailwind::GREEN.c800,
167        selected_text: tailwind::GREEN.c100,
168        selected_background: tailwind::GREEN.c700,
169        pressed_text: tailwind::GREEN.c300,
170        pressed_background: tailwind::GREEN.c900,
171        highlight: tailwind::GREEN.c600,
172        shadow: tailwind::GREEN.c950,
173    };
174
175    pub const BLUE: Theme = Theme {
176        normal_text: tailwind::BLUE.c200,
177        normal_background: tailwind::BLUE.c800,
178        selected_text: tailwind::BLUE.c100,
179        selected_background: tailwind::BLUE.c700,
180        pressed_text: tailwind::BLUE.c300,
181        pressed_background: tailwind::BLUE.c900,
182        highlight: tailwind::BLUE.c600,
183        shadow: tailwind::BLUE.c950,
184    };
185}