Skip to main content

ratatui_toolkit/
button.rs

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