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: [✓]" 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    // Chip-style toggle. Both states render at the same width so the
76    // surrounding layout doesn't shift when the value flips.
77    //   checked:   [ ✓ ACTIVE ]
78    //   unchecked: [          ]
79    const CHIP_INNER: &str = "          "; // 10 spaces, same width as " ✓ ACTIVE "
80    const CHIP_WIDTH: u16 = 12; // "[" + 10 + "]"
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("✓", Style::default().fg(_check_color)),
88            Span::styled(" ACTIVE ", Style::default().fg(_check_color)),
89            Span::styled("]", Style::default().fg(bracket_color)),
90        ])
91    } else {
92        Line::from(vec![
93            Span::styled(padded_label, Style::default().fg(label_color)),
94            Span::styled(": ", Style::default().fg(label_color)),
95            Span::styled("[", Style::default().fg(bracket_color)),
96            Span::styled(CHIP_INNER, Style::default().fg(bracket_color)),
97            Span::styled("]", Style::default().fg(bracket_color)),
98        ])
99    };
100
101    let paragraph = Paragraph::new(line);
102    frame.render_widget(paragraph, area);
103
104    // Chip position after label
105    let checkbox_start = area.x + actual_label_width + 2; // label + ": "
106    let checkbox_area = Rect::new(checkbox_start, area.y, CHIP_WIDTH.min(area.width), 1);
107
108    // Full area is label + ": " + chip
109    let full_width = (actual_label_width + 2 + CHIP_WIDTH).min(area.width);
110    let full_area = Rect::new(area.x, area.y, full_width, 1);
111
112    ToggleLayout {
113        checkbox_area,
114        full_area,
115    }
116}