Skip to main content

fresh/view/controls/toggle/
render.rs

1//! Toggle rendering functions
2
3use ratatui::layout::Rect;
4use ratatui::style::Style;
5use ratatui::text::{Line, Span};
6use ratatui::widgets::Paragraph;
7use ratatui::Frame;
8
9use super::{FocusState, ToggleColors, ToggleLayout, ToggleState};
10
11/// Render a toggle control
12///
13/// # Arguments
14/// * `frame` - The ratatui frame to render to
15/// * `area` - Rectangle where the toggle should be rendered
16/// * `state` - The toggle state
17/// * `colors` - Colors for rendering
18///
19/// # Returns
20/// Layout information for hit testing
21pub fn render_toggle(
22    frame: &mut Frame,
23    area: Rect,
24    state: &ToggleState,
25    colors: &ToggleColors,
26) -> ToggleLayout {
27    render_toggle_aligned(frame, area, state, colors, None)
28}
29
30/// Render a toggle control with optional label width alignment
31///
32/// # Arguments
33/// * `frame` - The ratatui frame to render to
34/// * `area` - Rectangle where the toggle should be rendered
35/// * `state` - The toggle state
36/// * `colors` - Colors for rendering
37/// * `label_width` - Optional minimum label width for alignment
38///
39/// # Returns
40/// Layout information for hit testing
41pub fn render_toggle_aligned(
42    frame: &mut Frame,
43    area: Rect,
44    state: &ToggleState,
45    colors: &ToggleColors,
46    label_width: Option<u16>,
47) -> ToggleLayout {
48    if area.height == 0 || area.width < 4 {
49        return ToggleLayout {
50            checkbox_area: Rect::default(),
51            full_area: area,
52        };
53    }
54
55    // When focused/hovered the chip sits on top of the row's highlight bg
56    // (settings_selected_bg / menu_hover_bg). Use `focused_fg` for the
57    // checkmark too — themes guarantee `focused_fg` contrasts with
58    // `focused` (their bg), whereas `checkmark` is green-ish in most
59    // themes and collides with green-tinted highlights (e.g. Nostalgia).
60    let (bracket_color, check_color, label_color) = match state.focus {
61        FocusState::Normal => (colors.bracket, colors.checkmark, colors.label),
62        FocusState::Focused => (colors.focused_fg, colors.focused_fg, colors.focused_fg),
63        FocusState::Hovered => (colors.focused_fg, colors.focused_fg, colors.focused_fg),
64        FocusState::Disabled => (colors.disabled, colors.disabled, colors.disabled),
65    };
66
67    // Format: "Label: [v]" / "Label: [ ]" with optional padding.
68    let actual_label_width = label_width.unwrap_or(state.label.len() as u16);
69    let padded_label = format!(
70        "{:width$}",
71        state.label,
72        width = actual_label_width as usize
73    );
74
75    // Compact checkbox glyph — matches the widget framework's
76    // `[v]` / `[ ]` convention so an empty checkbox is not visually
77    // confusable with an empty text input.
78    //   checked:   [v]
79    //   unchecked: [ ]
80    const CHIP_WIDTH: u16 = 3;
81
82    let line = if state.checked {
83        Line::from(vec![
84            Span::styled(padded_label, Style::default().fg(label_color)),
85            Span::styled(": ", Style::default().fg(label_color)),
86            Span::styled("[", Style::default().fg(bracket_color)),
87            Span::styled(
88                "v",
89                Style::default()
90                    .fg(check_color)
91                    .add_modifier(ratatui::style::Modifier::BOLD),
92            ),
93            Span::styled("]", Style::default().fg(bracket_color)),
94        ])
95    } else {
96        Line::from(vec![
97            Span::styled(padded_label, Style::default().fg(label_color)),
98            Span::styled(": ", Style::default().fg(label_color)),
99            Span::styled("[", Style::default().fg(bracket_color)),
100            Span::styled(" ", Style::default().fg(bracket_color)),
101            Span::styled("]", Style::default().fg(bracket_color)),
102        ])
103    };
104
105    let paragraph = Paragraph::new(line);
106    frame.render_widget(paragraph, area);
107
108    // Chip position after label (label + ": ").
109    let label_overhead = actual_label_width.saturating_add(2);
110    let checkbox_start = area.x.saturating_add(label_overhead);
111    let chip_avail = area.width.saturating_sub(label_overhead.min(area.width));
112    let checkbox_area = Rect::new(checkbox_start, area.y, CHIP_WIDTH.min(chip_avail), 1);
113
114    // Full area is label + ": " + chip
115    let full_width = (actual_label_width + 2 + CHIP_WIDTH).min(area.width);
116    let full_area = Rect::new(area.x, area.y, full_width, 1);
117
118    ToggleLayout {
119        checkbox_area,
120        full_area,
121    }
122}