1use ratatui::{
28 buffer::Buffer,
29 layout::{Alignment, Rect},
30 style::{Color, Modifier, Style},
31 text::{Line, Span},
32 widgets::{Block, Borders, Paragraph, Widget},
33};
34
35use crate::traits::{ClickRegion, ClickRegionRegistry, FocusId};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ButtonAction {
40 Click,
42}
43
44#[derive(Debug, Clone)]
46pub struct ButtonState {
47 pub focused: bool,
49 pub pressed: bool,
51 pub enabled: bool,
53 pub toggled: bool,
55}
56
57impl Default for ButtonState {
58 fn default() -> Self {
59 Self {
60 focused: false,
61 pressed: false,
62 enabled: true,
63 toggled: false,
64 }
65 }
66}
67
68impl ButtonState {
69 pub fn enabled() -> Self {
71 Self {
72 enabled: true,
73 ..Default::default()
74 }
75 }
76
77 pub fn disabled() -> Self {
79 Self {
80 enabled: false,
81 ..Default::default()
82 }
83 }
84
85 pub fn toggled(toggled: bool) -> Self {
87 Self {
88 toggled,
89 enabled: true,
90 ..Default::default()
91 }
92 }
93
94 pub fn set_focused(&mut self, focused: bool) {
96 self.focused = focused;
97 }
98
99 pub fn set_pressed(&mut self, pressed: bool) {
101 self.pressed = pressed;
102 }
103
104 pub fn set_enabled(&mut self, enabled: bool) {
106 self.enabled = enabled;
107 }
108
109 pub fn toggle(&mut self) {
111 if self.enabled {
112 self.toggled = !self.toggled;
113 }
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
119pub enum ButtonVariant {
120 #[default]
122 SingleLine,
123 Block,
125 IconText,
127 Toggle,
129 Minimal,
131}
132
133#[derive(Debug, Clone)]
135pub struct ButtonStyle {
136 pub variant: ButtonVariant,
138 pub focused_fg: Color,
140 pub focused_bg: Color,
142 pub unfocused_fg: Color,
144 pub unfocused_bg: Color,
146 pub disabled_fg: Color,
148 pub pressed_fg: Color,
150 pub pressed_bg: Color,
152 pub toggled_fg: Color,
154 pub toggled_bg: Color,
156}
157
158impl Default for ButtonStyle {
159 fn default() -> Self {
160 Self {
161 variant: ButtonVariant::SingleLine,
162 focused_fg: Color::Black,
163 focused_bg: Color::Yellow,
164 unfocused_fg: Color::White,
165 unfocused_bg: Color::DarkGray,
166 disabled_fg: Color::DarkGray,
167 pressed_fg: Color::Black,
168 pressed_bg: Color::White,
169 toggled_fg: Color::Black,
170 toggled_bg: Color::Green,
171 }
172 }
173}
174
175impl ButtonStyle {
176 pub fn new(variant: ButtonVariant) -> Self {
178 Self {
179 variant,
180 ..Default::default()
181 }
182 }
183
184 pub fn variant(mut self, variant: ButtonVariant) -> Self {
186 self.variant = variant;
187 self
188 }
189
190 pub fn focused(mut self, fg: Color, bg: Color) -> Self {
192 self.focused_fg = fg;
193 self.focused_bg = bg;
194 self
195 }
196
197 pub fn unfocused(mut self, fg: Color, bg: Color) -> Self {
199 self.unfocused_fg = fg;
200 self.unfocused_bg = bg;
201 self
202 }
203
204 pub fn toggled(mut self, fg: Color, bg: Color) -> Self {
206 self.toggled_fg = fg;
207 self.toggled_bg = bg;
208 self
209 }
210
211 pub fn primary() -> Self {
213 Self {
214 focused_fg: Color::White,
215 focused_bg: Color::Blue,
216 unfocused_fg: Color::White,
217 unfocused_bg: Color::Rgb(50, 100, 200),
218 ..Default::default()
219 }
220 }
221
222 pub fn danger() -> Self {
224 Self {
225 focused_fg: Color::White,
226 focused_bg: Color::Red,
227 unfocused_fg: Color::White,
228 unfocused_bg: Color::Rgb(150, 50, 50),
229 ..Default::default()
230 }
231 }
232
233 pub fn success() -> Self {
235 Self {
236 focused_fg: Color::White,
237 focused_bg: Color::Green,
238 unfocused_fg: Color::White,
239 unfocused_bg: Color::Rgb(50, 150, 50),
240 ..Default::default()
241 }
242 }
243}
244
245pub struct Button<'a> {
249 label: &'a str,
250 icon: Option<&'a str>,
251 state: &'a ButtonState,
252 style: ButtonStyle,
253 focus_id: FocusId,
254 alignment: Alignment,
255}
256
257impl<'a> Button<'a> {
258 pub fn new(label: &'a str, state: &'a ButtonState) -> Self {
265 Self {
266 label,
267 icon: None,
268 state,
269 style: ButtonStyle::default(),
270 focus_id: FocusId::default(),
271 alignment: Alignment::Center,
272 }
273 }
274
275 pub fn icon(mut self, icon: &'a str) -> Self {
277 self.icon = Some(icon);
278 self
279 }
280
281 pub fn style(mut self, style: ButtonStyle) -> Self {
283 self.style = style;
284 self
285 }
286
287 pub fn variant(mut self, variant: ButtonVariant) -> Self {
289 self.style.variant = variant;
290 self
291 }
292
293 pub fn focus_id(mut self, id: FocusId) -> Self {
295 self.focus_id = id;
296 self
297 }
298
299 pub fn alignment(mut self, alignment: Alignment) -> Self {
301 self.alignment = alignment;
302 self
303 }
304
305 fn current_style(&self) -> Style {
307 if !self.state.enabled {
308 Style::default().fg(self.style.disabled_fg)
309 } else if self.state.pressed {
310 Style::default()
311 .fg(self.style.pressed_fg)
312 .bg(self.style.pressed_bg)
313 } else if self.style.variant == ButtonVariant::Toggle && self.state.toggled {
314 Style::default()
315 .fg(self.style.toggled_fg)
316 .bg(self.style.toggled_bg)
317 .add_modifier(Modifier::BOLD)
318 } else if self.state.focused {
319 Style::default()
320 .fg(self.style.focused_fg)
321 .bg(self.style.focused_bg)
322 .add_modifier(Modifier::BOLD)
323 } else {
324 Style::default()
325 .fg(self.style.unfocused_fg)
326 .bg(self.style.unfocused_bg)
327 }
328 }
329
330 fn build_text(&self) -> String {
332 match self.style.variant {
333 ButtonVariant::SingleLine | ButtonVariant::Toggle => {
334 if let Some(icon) = self.icon {
335 format!(" {} {} ", icon, self.label)
336 } else {
337 format!(" {} ", self.label)
338 }
339 }
340 ButtonVariant::Block | ButtonVariant::IconText | ButtonVariant::Minimal => {
341 if let Some(icon) = self.icon {
342 format!("{} {}", icon, self.label)
343 } else {
344 self.label.to_string()
345 }
346 }
347 }
348 }
349
350 pub fn min_width(&self) -> u16 {
352 let text = self.build_text();
353 let text_len = text.chars().count() as u16;
354
355 match self.style.variant {
356 ButtonVariant::Block => text_len + 4, _ => text_len,
358 }
359 }
360
361 pub fn min_height(&self) -> u16 {
363 match self.style.variant {
364 ButtonVariant::Block => 3, _ => 1,
366 }
367 }
368
369 pub fn render_stateful(self, area: Rect, buf: &mut Buffer) -> ClickRegion<ButtonAction> {
395 let click_area = match self.style.variant {
396 ButtonVariant::Block => area,
397 _ => Rect::new(area.x, area.y, area.width, 1),
398 };
399
400 self.render(area, buf);
401
402 ClickRegion::new(click_area, ButtonAction::Click)
403 }
404
405 pub fn render_with_registry<D: Clone>(
445 self,
446 area: Rect,
447 buf: &mut Buffer,
448 registry: &mut ClickRegionRegistry<D>,
449 data: D,
450 ) {
451 let region = self.render_stateful(area, buf);
452 registry.register(region.area, data);
453 }
454}
455
456impl Widget for Button<'_> {
457 fn render(self, area: Rect, buf: &mut Buffer) {
458 let style = self.current_style();
459 let text = self.build_text();
460
461 match self.style.variant {
462 ButtonVariant::SingleLine | ButtonVariant::Toggle | ButtonVariant::Minimal => {
463 let line = Line::from(Span::styled(text, style));
464 let paragraph = Paragraph::new(line).alignment(self.alignment);
465 paragraph.render(area, buf);
466 }
467
468 ButtonVariant::Block => {
469 let block = Block::default().borders(Borders::ALL).border_style(style);
470
471 let inner = block.inner(area);
472 block.render(area, buf);
473
474 let paragraph = Paragraph::new(text).style(style).alignment(self.alignment);
475 paragraph.render(inner, buf);
476 }
477
478 ButtonVariant::IconText => {
479 let line = Line::from(Span::styled(text, style));
480 let paragraph = Paragraph::new(line);
481 paragraph.render(area, buf);
482 }
483 }
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn test_state_default() {
493 let state = ButtonState::default();
494 assert!(!state.focused);
495 assert!(!state.pressed);
496 assert!(state.enabled);
497 assert!(!state.toggled);
498 }
499
500 #[test]
501 fn test_state_enabled() {
502 let state = ButtonState::enabled();
503 assert!(state.enabled);
504 assert!(!state.focused);
505 }
506
507 #[test]
508 fn test_state_disabled() {
509 let state = ButtonState::disabled();
510 assert!(!state.enabled);
511 }
512
513 #[test]
514 fn test_state_toggled() {
515 let state = ButtonState::toggled(true);
516 assert!(state.toggled);
517 assert!(state.enabled);
518 }
519
520 #[test]
521 fn test_toggle() {
522 let mut state = ButtonState::enabled();
523 assert!(!state.toggled);
524
525 state.toggle();
526 assert!(state.toggled);
527
528 state.toggle();
529 assert!(!state.toggled);
530 }
531
532 #[test]
533 fn test_toggle_disabled() {
534 let mut state = ButtonState::disabled();
535 state.toggled = false;
536
537 state.toggle();
538 assert!(!state.toggled); }
540
541 #[test]
542 fn test_button_text_single_line() {
543 let state = ButtonState::enabled();
544 let button = Button::new("Click", &state).variant(ButtonVariant::SingleLine);
545
546 assert_eq!(button.build_text(), " Click ");
547 }
548
549 #[test]
550 fn test_button_text_with_icon() {
551 let state = ButtonState::enabled();
552 let button = Button::new("Save", &state).icon("💾");
553
554 assert_eq!(button.build_text(), " 💾 Save ");
555 }
556
557 #[test]
558 fn test_button_min_width() {
559 let state = ButtonState::enabled();
560
561 let button = Button::new("OK", &state).variant(ButtonVariant::SingleLine);
562 assert_eq!(button.min_width(), 4); let button = Button::new("OK", &state).variant(ButtonVariant::Block);
565 assert_eq!(button.min_width(), 6); }
567
568 #[test]
569 fn test_button_min_height() {
570 let state = ButtonState::enabled();
571
572 let button = Button::new("OK", &state).variant(ButtonVariant::SingleLine);
573 assert_eq!(button.min_height(), 1);
574
575 let button = Button::new("OK", &state).variant(ButtonVariant::Block);
576 assert_eq!(button.min_height(), 3);
577 }
578
579 #[test]
580 fn test_render_stateful() {
581 let state = ButtonState::enabled();
582 let button = Button::new("Test", &state);
583
584 let area = Rect::new(5, 3, 20, 1);
585 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 10));
586
587 let click_region = button.render_stateful(area, &mut buffer);
588
589 assert_eq!(click_region.area.x, 5);
590 assert_eq!(click_region.area.y, 3);
591 assert_eq!(click_region.data, ButtonAction::Click);
592 }
593
594 #[test]
595 fn test_render_with_registry() {
596 use crate::traits::ClickRegionRegistry;
597
598 let state = ButtonState::enabled();
599 let button = Button::new("Click", &state);
600 let area = Rect::new(5, 3, 20, 1);
601 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 10));
602 let mut registry: ClickRegionRegistry<&str> = ClickRegionRegistry::new();
603
604 button.render_with_registry(area, &mut buffer, &mut registry, "test_button");
605
606 assert_eq!(registry.len(), 1);
608
609 assert_eq!(registry.handle_click(5, 3), Some(&"test_button"));
611
612 assert_eq!(registry.handle_click(100, 100), None);
614 }
615
616 #[test]
617 fn test_render_with_registry_multiple_buttons() {
618 use crate::traits::ClickRegionRegistry;
619
620 let mut registry: ClickRegionRegistry<usize> = ClickRegionRegistry::new();
621 let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 10));
622
623 let state = ButtonState::enabled();
625
626 let button1 = Button::new("OK", &state);
627 button1.render_with_registry(Rect::new(0, 0, 10, 1), &mut buffer, &mut registry, 0);
628
629 let button2 = Button::new("Cancel", &state);
630 button2.render_with_registry(Rect::new(15, 0, 12, 1), &mut buffer, &mut registry, 1);
631
632 let button3 = Button::new("Help", &state);
633 button3.render_with_registry(Rect::new(30, 0, 10, 1), &mut buffer, &mut registry, 2);
634
635 assert_eq!(registry.len(), 3);
637
638 assert_eq!(registry.handle_click(2, 0), Some(&0)); assert_eq!(registry.handle_click(18, 0), Some(&1)); assert_eq!(registry.handle_click(32, 0), Some(&2)); assert_eq!(registry.handle_click(12, 0), None);
645 }
646
647 #[test]
648 fn test_style_presets() {
649 let primary = ButtonStyle::primary();
650 assert_eq!(primary.focused_bg, Color::Blue);
651
652 let danger = ButtonStyle::danger();
653 assert_eq!(danger.focused_bg, Color::Red);
654
655 let success = ButtonStyle::success();
656 assert_eq!(success.focused_bg, Color::Green);
657 }
658
659 #[test]
660 fn test_style_builder() {
661 let style = ButtonStyle::default()
662 .variant(ButtonVariant::Toggle)
663 .focused(Color::White, Color::Cyan)
664 .toggled(Color::Black, Color::Magenta);
665
666 assert_eq!(style.variant, ButtonVariant::Toggle);
667 assert_eq!(style.focused_fg, Color::White);
668 assert_eq!(style.focused_bg, Color::Cyan);
669 assert_eq!(style.toggled_fg, Color::Black);
670 assert_eq!(style.toggled_bg, Color::Magenta);
671 }
672
673 #[test]
674 fn test_current_style_states() {
675 let state = ButtonState::disabled();
677 let button = Button::new("Test", &state);
678 let style = button.current_style();
679 assert_eq!(style.fg, Some(button.style.disabled_fg));
680
681 let mut state = ButtonState::enabled();
683 state.focused = true;
684 let button = Button::new("Test", &state);
685 let style = button.current_style();
686 assert_eq!(style.fg, Some(button.style.focused_fg));
687 assert_eq!(style.bg, Some(button.style.focused_bg));
688
689 let mut state = ButtonState::enabled();
691 state.toggled = true;
692 let button = Button::new("Test", &state).variant(ButtonVariant::Toggle);
693 let style = button.current_style();
694 assert_eq!(style.fg, Some(button.style.toggled_fg));
695 assert_eq!(style.bg, Some(button.style.toggled_bg));
696 }
697}