Skip to main content

ratatui_toolkit/button/
mod.rs

1//! Button component
2//!
3//! Provides clickable button widgets for UI interactions.
4
5use ratatui::layout::Rect;
6use ratatui::style::{Color, Modifier, Style};
7use ratatui::text::{Line, Span};
8
9/// A clickable button widget for the UI
10#[derive(Debug, Clone)]
11pub struct Button {
12    /// The text displayed on the button
13    pub text: String,
14    /// The area where the button is rendered (for click detection)
15    pub area: Option<Rect>,
16    /// Whether the button is currently hovered
17    pub hovered: bool,
18    /// Normal style (not hovered)
19    pub normal_style: Style,
20    /// Hover style
21    pub hover_style: Style,
22}
23
24impl Button {
25    /// Create a new button with default styling
26    pub fn new(text: impl Into<String>) -> Self {
27        Self {
28            text: text.into(),
29            area: None,
30            hovered: false,
31            normal_style: Style::default()
32                .fg(Color::Cyan)
33                .add_modifier(Modifier::BOLD),
34            hover_style: Style::default()
35                .fg(Color::Black)
36                .bg(Color::Cyan)
37                .add_modifier(Modifier::BOLD),
38        }
39    }
40
41    /// Set custom normal style
42    pub fn normal_style(mut self, style: Style) -> Self {
43        self.normal_style = style;
44        self
45    }
46
47    /// Set custom hover style
48    pub fn hover_style(mut self, style: Style) -> Self {
49        self.hover_style = style;
50        self
51    }
52
53    /// Check if a mouse click at (column, row) is within the button area
54    pub fn is_clicked(&self, column: u16, row: u16) -> bool {
55        if let Some(area) = self.area {
56            column >= area.x
57                && column < area.x + area.width
58                && row >= area.y
59                && row < area.y + area.height
60        } else {
61            false
62        }
63    }
64
65    /// Update hover state based on mouse position
66    pub fn update_hover(&mut self, column: u16, row: u16) {
67        self.hovered = self.is_clicked(column, row);
68    }
69
70    /// Render the button as a styled span
71    pub fn render(&self, panel_area: Rect, _title_prefix: &str) -> (Span<'static>, Rect) {
72        let button_text = format!(" [{}] ", self.text);
73        let button_width = button_text.len() as u16;
74        let button_x = panel_area.x + panel_area.width.saturating_sub(button_width + 2);
75        let button_y = panel_area.y;
76
77        let area = Rect {
78            x: button_x,
79            y: button_y,
80            width: button_width,
81            height: 1,
82        };
83
84        let style = if self.hovered {
85            self.hover_style
86        } else {
87            self.normal_style
88        };
89
90        (Span::styled(button_text, style), area)
91    }
92
93    /// Create a complete title line with the button on the right
94    pub fn render_with_title(&mut self, panel_area: Rect, title: &str) -> Line<'static> {
95        let (button_span, area) = self.render(panel_area, title);
96        self.area = Some(area);
97        let title_line = Line::from(vec![Span::raw(title.to_string()), button_span]);
98        title_line
99    }
100
101    /// Render button at a specific position (for multiple buttons)
102    pub fn render_at_offset(
103        &self,
104        panel_area: Rect,
105        offset_from_right: u16,
106    ) -> (Span<'static>, Rect) {
107        let button_text = format!(" [{}] ", self.text);
108        let button_width = button_text.len() as u16;
109        let button_x = panel_area.x
110            + panel_area
111                .width
112                .saturating_sub(offset_from_right + button_width + 2);
113        let button_y = panel_area.y;
114
115        let area = Rect {
116            x: button_x,
117            y: button_y,
118            width: button_width,
119            height: 1,
120        };
121
122        let style = if self.hovered {
123            self.hover_style
124        } else {
125            self.normal_style
126        };
127
128        (Span::styled(button_text, style), area)
129    }
130}
131
132impl Default for Button {
133    fn default() -> Self {
134        Self::new("Button")
135    }
136}
137
138/// Helper function to render multiple buttons in a title
139pub fn render_title_with_buttons(
140    panel_area: Rect,
141    title: &str,
142    buttons: &mut [&mut Button],
143) -> Line<'static> {
144    let mut spans = vec![Span::raw(title.to_string())];
145
146    let mut offset = 0u16;
147
148    for button in buttons.iter_mut().rev() {
149        let (button_span, area) = button.render_at_offset(panel_area, offset);
150        button.area = Some(area);
151
152        let button_width = format!(" [{}] ", button.text).len() as u16;
153        offset += button_width;
154
155        spans.insert(1, button_span);
156    }
157
158    Line::from(spans)
159}