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
245impl From<&crate::theme::Theme> for ButtonStyle {
246 fn from(theme: &crate::theme::Theme) -> Self {
247 let p = &theme.palette;
248 Self {
249 variant: ButtonVariant::SingleLine,
250 focused_fg: p.highlight_fg,
251 focused_bg: p.highlight_bg,
252 unfocused_fg: p.text,
253 unfocused_bg: Color::DarkGray,
254 disabled_fg: p.text_disabled,
255 pressed_fg: p.pressed_fg,
256 pressed_bg: p.pressed_bg,
257 toggled_fg: p.highlight_fg,
258 toggled_bg: p.success,
259 }
260 }
261}
262
263pub struct Button<'a> {
267 label: &'a str,
268 icon: Option<&'a str>,
269 state: &'a ButtonState,
270 style: ButtonStyle,
271 focus_id: FocusId,
272 alignment: Alignment,
273}
274
275impl<'a> Button<'a> {
276 pub fn new(label: &'a str, state: &'a ButtonState) -> Self {
283 Self {
284 label,
285 icon: None,
286 state,
287 style: ButtonStyle::default(),
288 focus_id: FocusId::default(),
289 alignment: Alignment::Center,
290 }
291 }
292
293 pub fn icon(mut self, icon: &'a str) -> Self {
295 self.icon = Some(icon);
296 self
297 }
298
299 pub fn style(mut self, style: ButtonStyle) -> Self {
301 self.style = style;
302 self
303 }
304
305 pub fn theme(self, theme: &crate::theme::Theme) -> Self {
307 self.style(ButtonStyle::from(theme))
308 }
309
310 pub fn variant(mut self, variant: ButtonVariant) -> Self {
312 self.style.variant = variant;
313 self
314 }
315
316 pub fn focus_id(mut self, id: FocusId) -> Self {
318 self.focus_id = id;
319 self
320 }
321
322 pub fn alignment(mut self, alignment: Alignment) -> Self {
324 self.alignment = alignment;
325 self
326 }
327
328 fn current_style(&self) -> Style {
330 if !self.state.enabled {
331 Style::default().fg(self.style.disabled_fg)
332 } else if self.state.pressed {
333 Style::default()
334 .fg(self.style.pressed_fg)
335 .bg(self.style.pressed_bg)
336 } else if self.style.variant == ButtonVariant::Toggle && self.state.toggled {
337 Style::default()
338 .fg(self.style.toggled_fg)
339 .bg(self.style.toggled_bg)
340 .add_modifier(Modifier::BOLD)
341 } else if self.state.focused {
342 Style::default()
343 .fg(self.style.focused_fg)
344 .bg(self.style.focused_bg)
345 .add_modifier(Modifier::BOLD)
346 } else {
347 Style::default()
348 .fg(self.style.unfocused_fg)
349 .bg(self.style.unfocused_bg)
350 }
351 }
352
353 fn build_text(&self) -> String {
355 match self.style.variant {
356 ButtonVariant::SingleLine | ButtonVariant::Toggle => {
357 if let Some(icon) = self.icon {
358 format!(" {} {} ", icon, self.label)
359 } else {
360 format!(" {} ", self.label)
361 }
362 }
363 ButtonVariant::Block | ButtonVariant::IconText | ButtonVariant::Minimal => {
364 if let Some(icon) = self.icon {
365 format!("{} {}", icon, self.label)
366 } else {
367 self.label.to_string()
368 }
369 }
370 }
371 }
372
373 pub fn min_width(&self) -> u16 {
375 let text = self.build_text();
376 let text_len = text.chars().count() as u16;
377
378 match self.style.variant {
379 ButtonVariant::Block => text_len + 4, _ => text_len,
381 }
382 }
383
384 pub fn min_height(&self) -> u16 {
386 match self.style.variant {
387 ButtonVariant::Block => 3, _ => 1,
389 }
390 }
391
392 pub fn render_stateful(self, area: Rect, buf: &mut Buffer) -> ClickRegion<ButtonAction> {
418 let click_area = match self.style.variant {
419 ButtonVariant::Block => area,
420 _ => Rect::new(area.x, area.y, self.min_width().min(area.width), 1),
421 };
422
423 self.render(area, buf);
424
425 ClickRegion::new(click_area, ButtonAction::Click)
426 }
427
428 pub fn render_with_registry<D: Clone>(
468 self,
469 area: Rect,
470 buf: &mut Buffer,
471 registry: &mut ClickRegionRegistry<D>,
472 data: D,
473 ) {
474 let region = self.render_stateful(area, buf);
475 registry.register(region.area, data);
476 }
477}
478
479impl Widget for Button<'_> {
480 fn render(self, area: Rect, buf: &mut Buffer) {
481 let style = self.current_style();
482 let text = self.build_text();
483
484 match self.style.variant {
485 ButtonVariant::SingleLine | ButtonVariant::Toggle | ButtonVariant::Minimal => {
486 let line = Line::from(Span::styled(text, style));
487 let paragraph = Paragraph::new(line).alignment(self.alignment);
488 paragraph.render(area, buf);
489 }
490
491 ButtonVariant::Block => {
492 let block = Block::default().borders(Borders::ALL).border_style(style);
493
494 let inner = block.inner(area);
495 block.render(area, buf);
496
497 let paragraph = Paragraph::new(text).style(style).alignment(self.alignment);
498 paragraph.render(inner, buf);
499 }
500
501 ButtonVariant::IconText => {
502 let line = Line::from(Span::styled(text, style));
503 let paragraph = Paragraph::new(line);
504 paragraph.render(area, buf);
505 }
506 }
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn test_state_default() {
516 let state = ButtonState::default();
517 assert!(!state.focused);
518 assert!(!state.pressed);
519 assert!(state.enabled);
520 assert!(!state.toggled);
521 }
522
523 #[test]
524 fn test_state_enabled() {
525 let state = ButtonState::enabled();
526 assert!(state.enabled);
527 assert!(!state.focused);
528 }
529
530 #[test]
531 fn test_state_disabled() {
532 let state = ButtonState::disabled();
533 assert!(!state.enabled);
534 }
535
536 #[test]
537 fn test_state_toggled() {
538 let state = ButtonState::toggled(true);
539 assert!(state.toggled);
540 assert!(state.enabled);
541 }
542
543 #[test]
544 fn test_toggle() {
545 let mut state = ButtonState::enabled();
546 assert!(!state.toggled);
547
548 state.toggle();
549 assert!(state.toggled);
550
551 state.toggle();
552 assert!(!state.toggled);
553 }
554
555 #[test]
556 fn test_toggle_disabled() {
557 let mut state = ButtonState::disabled();
558 state.toggled = false;
559
560 state.toggle();
561 assert!(!state.toggled); }
563
564 #[test]
565 fn test_button_text_single_line() {
566 let state = ButtonState::enabled();
567 let button = Button::new("Click", &state).variant(ButtonVariant::SingleLine);
568
569 assert_eq!(button.build_text(), " Click ");
570 }
571
572 #[test]
573 fn test_button_text_with_icon() {
574 let state = ButtonState::enabled();
575 let button = Button::new("Save", &state).icon("💾");
576
577 assert_eq!(button.build_text(), " 💾 Save ");
578 }
579
580 #[test]
581 fn test_button_min_width() {
582 let state = ButtonState::enabled();
583
584 let button = Button::new("OK", &state).variant(ButtonVariant::SingleLine);
585 assert_eq!(button.min_width(), 4); let button = Button::new("OK", &state).variant(ButtonVariant::Block);
588 assert_eq!(button.min_width(), 6); }
590
591 #[test]
592 fn test_button_min_height() {
593 let state = ButtonState::enabled();
594
595 let button = Button::new("OK", &state).variant(ButtonVariant::SingleLine);
596 assert_eq!(button.min_height(), 1);
597
598 let button = Button::new("OK", &state).variant(ButtonVariant::Block);
599 assert_eq!(button.min_height(), 3);
600 }
601
602 #[test]
603 fn test_render_stateful() {
604 let state = ButtonState::enabled();
605 let button = Button::new("Test", &state);
606
607 let area = Rect::new(5, 3, 20, 1);
608 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 10));
609
610 let click_region = button.render_stateful(area, &mut buffer);
611
612 assert_eq!(click_region.area.x, 5);
613 assert_eq!(click_region.area.y, 3);
614 assert_eq!(click_region.data, ButtonAction::Click);
615 }
616
617 #[test]
618 fn test_render_with_registry() {
619 use crate::traits::ClickRegionRegistry;
620
621 let state = ButtonState::enabled();
622 let button = Button::new("Click", &state);
623 let area = Rect::new(5, 3, 20, 1);
624 let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 10));
625 let mut registry: ClickRegionRegistry<&str> = ClickRegionRegistry::new();
626
627 button.render_with_registry(area, &mut buffer, &mut registry, "test_button");
628
629 assert_eq!(registry.len(), 1);
631
632 assert_eq!(registry.handle_click(5, 3), Some(&"test_button"));
634
635 assert_eq!(registry.handle_click(100, 100), None);
637 }
638
639 #[test]
640 fn test_render_with_registry_multiple_buttons() {
641 use crate::traits::ClickRegionRegistry;
642
643 let mut registry: ClickRegionRegistry<usize> = ClickRegionRegistry::new();
644 let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 10));
645
646 let state = ButtonState::enabled();
648
649 let button1 = Button::new("OK", &state);
650 button1.render_with_registry(Rect::new(0, 0, 10, 1), &mut buffer, &mut registry, 0);
651
652 let button2 = Button::new("Cancel", &state);
653 button2.render_with_registry(Rect::new(15, 0, 12, 1), &mut buffer, &mut registry, 1);
654
655 let button3 = Button::new("Help", &state);
656 button3.render_with_registry(Rect::new(30, 0, 10, 1), &mut buffer, &mut registry, 2);
657
658 assert_eq!(registry.len(), 3);
660
661 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);
668 }
669
670 #[test]
671 fn test_style_presets() {
672 let primary = ButtonStyle::primary();
673 assert_eq!(primary.focused_bg, Color::Blue);
674
675 let danger = ButtonStyle::danger();
676 assert_eq!(danger.focused_bg, Color::Red);
677
678 let success = ButtonStyle::success();
679 assert_eq!(success.focused_bg, Color::Green);
680 }
681
682 #[test]
683 fn test_style_builder() {
684 let style = ButtonStyle::default()
685 .variant(ButtonVariant::Toggle)
686 .focused(Color::White, Color::Cyan)
687 .toggled(Color::Black, Color::Magenta);
688
689 assert_eq!(style.variant, ButtonVariant::Toggle);
690 assert_eq!(style.focused_fg, Color::White);
691 assert_eq!(style.focused_bg, Color::Cyan);
692 assert_eq!(style.toggled_fg, Color::Black);
693 assert_eq!(style.toggled_bg, Color::Magenta);
694 }
695
696 #[test]
697 fn test_current_style_states() {
698 let state = ButtonState::disabled();
700 let button = Button::new("Test", &state);
701 let style = button.current_style();
702 assert_eq!(style.fg, Some(button.style.disabled_fg));
703
704 let mut state = ButtonState::enabled();
706 state.focused = true;
707 let button = Button::new("Test", &state);
708 let style = button.current_style();
709 assert_eq!(style.fg, Some(button.style.focused_fg));
710 assert_eq!(style.bg, Some(button.style.focused_bg));
711
712 let mut state = ButtonState::enabled();
714 state.toggled = true;
715 let button = Button::new("Test", &state).variant(ButtonVariant::Toggle);
716 let style = button.current_style();
717 assert_eq!(style.fg, Some(button.style.toggled_fg));
718 assert_eq!(style.bg, Some(button.style.toggled_bg));
719 }
720}