Skip to main content

fresh/view/controls/button/
render.rs

1//! Button rendering functions
2
3use ratatui::layout::Rect;
4use ratatui::style::{Modifier, Style};
5use ratatui::text::{Line, Span};
6use ratatui::widgets::Paragraph;
7use ratatui::Frame;
8
9use super::{ButtonColors, ButtonLayout, ButtonState, FocusState};
10
11/// Render a button control
12///
13/// # Arguments
14/// * `frame` - The ratatui frame to render to
15/// * `area` - Rectangle where the button should be rendered
16/// * `state` - The button state
17/// * `colors` - Colors for rendering
18///
19/// # Returns
20/// Layout information for hit testing
21pub fn render_button(
22    frame: &mut Frame,
23    area: Rect,
24    state: &ButtonState,
25    colors: &ButtonColors,
26) -> ButtonLayout {
27    if area.height == 0 || area.width < 4 {
28        return ButtonLayout::default();
29    }
30
31    let (text_color, border_color, bg_color) = match state.focus {
32        FocusState::Normal => {
33            if state.pressed {
34                (colors.text, colors.border, Some(colors.pressed_bg))
35            } else {
36                (colors.text, colors.border, None)
37            }
38        }
39        FocusState::Focused => {
40            if state.pressed {
41                (colors.text, colors.focused, Some(colors.pressed_bg))
42            } else {
43                (colors.focused, colors.focused, None)
44            }
45        }
46        FocusState::Hovered => {
47            // Hover uses dedicated hover color from theme
48            (colors.hovered, colors.hovered, None)
49        }
50        FocusState::Disabled => (colors.disabled, colors.disabled, None),
51    };
52
53    // Calculate button width: "[ " + label + " ]"
54    let button_width = (state.label.len() + 4) as u16;
55    let actual_width = button_width.min(area.width);
56
57    // Truncate label if needed
58    let max_label_len = actual_width.saturating_sub(4) as usize;
59    let display_label: String = state.label.chars().take(max_label_len).collect();
60
61    let mut style = Style::default().fg(text_color);
62    if let Some(bg) = bg_color {
63        style = style.bg(bg);
64    }
65    if state.focus == FocusState::Focused {
66        style = style.add_modifier(Modifier::BOLD);
67    }
68
69    let line = Line::from(vec![
70        Span::styled("[", Style::default().fg(border_color)),
71        Span::raw(" "),
72        Span::styled(&display_label, style),
73        Span::raw(" "),
74        Span::styled("]", Style::default().fg(border_color)),
75    ]);
76
77    let button_area = Rect::new(area.x, area.y, actual_width, 1);
78    let paragraph = Paragraph::new(line);
79    frame.render_widget(paragraph, button_area);
80
81    ButtonLayout { button_area }
82}
83
84/// Render a row of buttons with equal spacing
85///
86/// # Arguments
87/// * `frame` - The ratatui frame to render to
88/// * `area` - Rectangle where the buttons should be rendered
89/// * `buttons` - Slice of (state, colors) tuples for each button
90/// * `gap` - Space between buttons
91///
92/// # Returns
93/// Layout information for each button
94pub fn render_button_row(
95    frame: &mut Frame,
96    area: Rect,
97    buttons: &[(&ButtonState, &ButtonColors)],
98    gap: u16,
99) -> Vec<ButtonLayout> {
100    if buttons.is_empty() || area.height == 0 {
101        return Vec::new();
102    }
103
104    let mut layouts = Vec::with_capacity(buttons.len());
105    let mut x = area.x;
106
107    for (state, colors) in buttons {
108        let button_width = (state.label.len() + 4) as u16;
109        if x + button_width > area.x + area.width {
110            break;
111        }
112
113        let button_area = Rect::new(x, area.y, button_width, 1);
114        let layout = render_button(frame, button_area, state, colors);
115        layouts.push(layout);
116
117        x += button_width + gap;
118    }
119
120    layouts
121}