1use std::{io::stdout, ops::ControlFlow, time::Duration};
17
18use color_eyre::Result;
19use ratatui::{
20 buffer::Buffer,
21 crossterm::{
22 event::{
23 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
24 MouseEventKind,
25 },
26 execute,
27 },
28 layout::{Constraint, Layout, Rect},
29 style::{Color, Style},
30 text::Line,
31 widgets::{Paragraph, Widget},
32 DefaultTerminal, Frame,
33};
34
35fn main() -> Result<()> {
36 color_eyre::install()?;
37 let terminal = ratatui::init();
38 execute!(stdout(), EnableMouseCapture)?;
39 let app_result = run(terminal);
40 ratatui::restore();
41 if let Err(err) = execute!(stdout(), DisableMouseCapture) {
42 eprintln!("Error disabling mouse capture: {err}");
43 }
44 app_result
45}
46
47#[derive(Debug, Clone)]
49struct Button<'a> {
50 label: Line<'a>,
51 theme: Theme,
52 state: State,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56enum State {
57 Normal,
58 Selected,
59 Active,
60}
61
62#[derive(Debug, Clone, Copy)]
63struct Theme {
64 text: Color,
65 background: Color,
66 highlight: Color,
67 shadow: Color,
68}
69
70const BLUE: Theme = Theme {
71 text: Color::Rgb(16, 24, 48),
72 background: Color::Rgb(48, 72, 144),
73 highlight: Color::Rgb(64, 96, 192),
74 shadow: Color::Rgb(32, 48, 96),
75};
76
77const RED: Theme = Theme {
78 text: Color::Rgb(48, 16, 16),
79 background: Color::Rgb(144, 48, 48),
80 highlight: Color::Rgb(192, 64, 64),
81 shadow: Color::Rgb(96, 32, 32),
82};
83
84const GREEN: Theme = Theme {
85 text: Color::Rgb(16, 48, 16),
86 background: Color::Rgb(48, 144, 48),
87 highlight: Color::Rgb(64, 192, 64),
88 shadow: Color::Rgb(32, 96, 32),
89};
90
91impl<'a> Button<'a> {
93 pub fn new<T: Into<Line<'a>>>(label: T) -> Self {
94 Button {
95 label: label.into(),
96 theme: BLUE,
97 state: State::Normal,
98 }
99 }
100
101 pub const fn theme(mut self, theme: Theme) -> Self {
102 self.theme = theme;
103 self
104 }
105
106 pub const fn state(mut self, state: State) -> Self {
107 self.state = state;
108 self
109 }
110}
111
112impl<'a> Widget for Button<'a> {
113 #[allow(clippy::cast_possible_truncation)]
114 fn render(self, area: Rect, buf: &mut Buffer) {
115 let (background, text, shadow, highlight) = self.colors();
116 buf.set_style(area, Style::new().bg(background).fg(text));
117
118 if area.height > 2 {
120 buf.set_string(
121 area.x,
122 area.y,
123 "▔".repeat(area.width as usize),
124 Style::new().fg(highlight).bg(background),
125 );
126 }
127 if area.height > 1 {
129 buf.set_string(
130 area.x,
131 area.y + area.height - 1,
132 "▁".repeat(area.width as usize),
133 Style::new().fg(shadow).bg(background),
134 );
135 }
136 buf.set_line(
138 area.x + (area.width.saturating_sub(self.label.width() as u16)) / 2,
139 area.y + (area.height.saturating_sub(1)) / 2,
140 &self.label,
141 area.width,
142 );
143 }
144}
145
146impl Button<'_> {
147 const fn colors(&self) -> (Color, Color, Color, Color) {
148 let theme = self.theme;
149 match self.state {
150 State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight),
151 State::Selected => (theme.highlight, theme.text, theme.shadow, theme.highlight),
152 State::Active => (theme.background, theme.text, theme.highlight, theme.shadow),
153 }
154 }
155}
156
157fn run(mut terminal: DefaultTerminal) -> Result<()> {
158 let mut selected_button: usize = 0;
159 let mut button_states = [State::Selected, State::Normal, State::Normal];
160 loop {
161 terminal.draw(|frame| draw(frame, button_states))?;
162 if !event::poll(Duration::from_millis(100))? {
163 continue;
164 }
165 match event::read()? {
166 Event::Key(key) => {
167 if key.kind != event::KeyEventKind::Press {
168 continue;
169 }
170 if handle_key_event(key, &mut button_states, &mut selected_button).is_break() {
171 break;
172 }
173 }
174 Event::Mouse(mouse) => {
175 handle_mouse_event(mouse, &mut button_states, &mut selected_button);
176 }
177 _ => (),
178 }
179 }
180 Ok(())
181}
182
183fn draw(frame: &mut Frame, states: [State; 3]) {
184 let vertical = Layout::vertical([
185 Constraint::Length(1),
186 Constraint::Max(3),
187 Constraint::Length(1),
188 Constraint::Min(0), ]);
190 let [title, buttons, help, _] = vertical.areas(frame.area());
191
192 frame.render_widget(
193 Paragraph::new("Custom Widget Example (mouse enabled)"),
194 title,
195 );
196 render_buttons(frame, buttons, states);
197 frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help);
198}
199
200fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) {
201 let horizontal = Layout::horizontal([
202 Constraint::Length(15),
203 Constraint::Length(15),
204 Constraint::Length(15),
205 Constraint::Min(0), ]);
207 let [red, green, blue, _] = horizontal.areas(area);
208
209 frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
210 frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);
211 frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), blue);
212}
213
214fn handle_key_event(
215 key: event::KeyEvent,
216 button_states: &mut [State; 3],
217 selected_button: &mut usize,
218) -> ControlFlow<()> {
219 match key.code {
220 KeyCode::Char('q') => return ControlFlow::Break(()),
221 KeyCode::Left | KeyCode::Char('h') => {
222 button_states[*selected_button] = State::Normal;
223 *selected_button = selected_button.saturating_sub(1);
224 button_states[*selected_button] = State::Selected;
225 }
226 KeyCode::Right | KeyCode::Char('l') => {
227 button_states[*selected_button] = State::Normal;
228 *selected_button = selected_button.saturating_add(1).min(2);
229 button_states[*selected_button] = State::Selected;
230 }
231 KeyCode::Char(' ') => {
232 if button_states[*selected_button] == State::Active {
233 button_states[*selected_button] = State::Normal;
234 } else {
235 button_states[*selected_button] = State::Active;
236 }
237 }
238 _ => (),
239 }
240 ControlFlow::Continue(())
241}
242
243fn handle_mouse_event(
244 mouse: MouseEvent,
245 button_states: &mut [State; 3],
246 selected_button: &mut usize,
247) {
248 match mouse.kind {
249 MouseEventKind::Moved => {
250 let old_selected_button = *selected_button;
251 *selected_button = match mouse.column {
252 x if x < 15 => 0,
253 x if x < 30 => 1,
254 _ => 2,
255 };
256 if old_selected_button != *selected_button {
257 if button_states[old_selected_button] != State::Active {
258 button_states[old_selected_button] = State::Normal;
259 }
260 if button_states[*selected_button] != State::Active {
261 button_states[*selected_button] = State::Selected;
262 }
263 }
264 }
265 MouseEventKind::Down(MouseButton::Left) => {
266 if button_states[*selected_button] == State::Active {
267 button_states[*selected_button] = State::Normal;
268 } else {
269 button_states[*selected_button] = State::Active;
270 }
271 }
272 _ => (),
273 }
274}