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    //   inherited: [-]   (value is unset and falls back to a lower layer)
81    const CHIP_WIDTH: u16 = 3;
82
83    let line = if state.inherited {
84        // Neutral chip: the value is inherited/unset, so we deliberately avoid
85        // a definite checked/unchecked glyph that could be read as the user
86        // having set it off (issue #2345).
87        Line::from(vec![
88            Span::styled(padded_label, Style::default().fg(label_color)),
89            Span::styled(": ", Style::default().fg(label_color)),
90            Span::styled("[", Style::default().fg(bracket_color)),
91            Span::styled("-", Style::default().fg(bracket_color)),
92            Span::styled("]", Style::default().fg(bracket_color)),
93        ])
94    } else if state.checked {
95        Line::from(vec![
96            Span::styled(padded_label, Style::default().fg(label_color)),
97            Span::styled(": ", Style::default().fg(label_color)),
98            Span::styled("[", Style::default().fg(bracket_color)),
99            Span::styled(
100                "v",
101                Style::default()
102                    .fg(check_color)
103                    .add_modifier(ratatui::style::Modifier::BOLD),
104            ),
105            Span::styled("]", Style::default().fg(bracket_color)),
106        ])
107    } else {
108        Line::from(vec![
109            Span::styled(padded_label, Style::default().fg(label_color)),
110            Span::styled(": ", Style::default().fg(label_color)),
111            Span::styled("[", Style::default().fg(bracket_color)),
112            Span::styled(" ", Style::default().fg(bracket_color)),
113            Span::styled("]", Style::default().fg(bracket_color)),
114        ])
115    };
116
117    let paragraph = Paragraph::new(line);
118    frame.render_widget(paragraph, area);
119
120    // Chip position after label (label + ": ").
121    let label_overhead = actual_label_width.saturating_add(2);
122    let checkbox_start = area.x.saturating_add(label_overhead);
123    let chip_avail = area.width.saturating_sub(label_overhead.min(area.width));
124    let checkbox_area = Rect::new(checkbox_start, area.y, CHIP_WIDTH.min(chip_avail), 1);
125
126    // Full area is label + ": " + chip
127    let full_width = (actual_label_width + 2 + CHIP_WIDTH).min(area.width);
128    let full_area = Rect::new(area.x, area.y, full_width, 1);
129
130    ToggleLayout {
131        checkbox_area,
132        full_area,
133    }
134}