Skip to main content

fresh/view/controls/toggle/
mod.rs

1//! Toggle (checkbox) control for boolean values
2//!
3//! Renders as: `Label: [x]` or `Label: [ ]`
4//!
5//! This module provides a complete toggle component with:
6//! - State management (`ToggleState`)
7//! - Rendering (`render_toggle`, `render_toggle_aligned`)
8//! - Input handling (`ToggleState::handle_mouse`, `handle_key`)
9//! - Layout/hit testing (`ToggleLayout`)
10
11mod input;
12mod render;
13
14use ratatui::layout::Rect;
15use ratatui::style::Color;
16
17pub use input::ToggleEvent;
18pub use render::{render_toggle, render_toggle_aligned};
19
20use super::FocusState;
21
22/// State for a toggle control
23#[derive(Debug, Clone)]
24pub struct ToggleState {
25    /// Current value
26    pub checked: bool,
27    /// Label displayed next to the toggle
28    pub label: String,
29    /// Focus state
30    pub focus: FocusState,
31}
32
33impl ToggleState {
34    /// Create a new toggle state
35    pub fn new(checked: bool, label: impl Into<String>) -> Self {
36        Self {
37            checked,
38            label: label.into(),
39            focus: FocusState::Normal,
40        }
41    }
42
43    /// Set the focus state
44    pub fn with_focus(mut self, focus: FocusState) -> Self {
45        self.focus = focus;
46        self
47    }
48
49    /// Check if the toggle is enabled
50    pub fn is_enabled(&self) -> bool {
51        self.focus != FocusState::Disabled
52    }
53
54    /// Toggle the value
55    pub fn toggle(&mut self) {
56        if self.is_enabled() {
57            self.checked = !self.checked;
58        }
59    }
60}
61
62/// Colors for the toggle control
63#[derive(Debug, Clone, Copy)]
64pub struct ToggleColors {
65    /// Checkbox bracket color
66    pub bracket: Color,
67    /// Checkmark color when checked
68    pub checkmark: Color,
69    /// Label text color
70    pub label: Color,
71    /// Focused highlight background color
72    pub focused: Color,
73    /// Focused highlight foreground color (text on focused background)
74    pub focused_fg: Color,
75    /// Disabled color
76    pub disabled: Color,
77}
78
79impl Default for ToggleColors {
80    fn default() -> Self {
81        Self {
82            bracket: Color::Gray,
83            checkmark: Color::Green,
84            label: Color::White,
85            focused: Color::Cyan,
86            focused_fg: Color::Black,
87            disabled: Color::DarkGray,
88        }
89    }
90}
91
92impl ToggleColors {
93    /// Create colors from theme
94    pub fn from_theme(theme: &crate::view::theme::Theme) -> Self {
95        Self {
96            bracket: theme.line_number_fg,
97            checkmark: theme.diagnostic_info_fg,
98            label: theme.editor_fg,
99            focused: theme.settings_selected_bg,
100            focused_fg: theme.settings_selected_fg,
101            disabled: theme.line_number_fg,
102        }
103    }
104}
105
106/// Layout information returned after rendering for hit testing
107#[derive(Debug, Clone, Copy, Default)]
108pub struct ToggleLayout {
109    /// The checkbox area (clickable)
110    pub checkbox_area: Rect,
111    /// The full control area including label
112    pub full_area: Rect,
113}
114
115impl ToggleLayout {
116    /// Check if a point is within the clickable area
117    pub fn contains(&self, x: u16, y: u16) -> bool {
118        x >= self.full_area.x
119            && x < self.full_area.x + self.full_area.width
120            && y >= self.full_area.y
121            && y < self.full_area.y + self.full_area.height
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use ratatui::backend::TestBackend;
129    use ratatui::Terminal;
130
131    fn test_frame<F>(width: u16, height: u16, f: F)
132    where
133        F: FnOnce(&mut ratatui::Frame, Rect),
134    {
135        let backend = TestBackend::new(width, height);
136        let mut terminal = Terminal::new(backend).unwrap();
137        terminal
138            .draw(|frame| {
139                let area = Rect::new(0, 0, width, height);
140                f(frame, area);
141            })
142            .unwrap();
143    }
144
145    #[test]
146    fn test_toggle_checked() {
147        test_frame(20, 1, |frame, area| {
148            let state = ToggleState::new(true, "Enable");
149            let colors = ToggleColors::default();
150            let layout = render_toggle(frame, area, &state, &colors);
151
152            assert_eq!(layout.checkbox_area.width, 3);
153            assert_eq!(layout.full_area.width, 11); // "Enable: [x]"
154        });
155    }
156
157    #[test]
158    fn test_toggle_unchecked() {
159        test_frame(20, 1, |frame, area| {
160            let state = ToggleState::new(false, "Enable");
161            let colors = ToggleColors::default();
162            let layout = render_toggle(frame, area, &state, &colors);
163
164            assert_eq!(layout.checkbox_area.width, 3);
165        });
166    }
167
168    #[test]
169    fn test_toggle_click_detection() {
170        test_frame(20, 1, |frame, area| {
171            let state = ToggleState::new(true, "Enable");
172            let colors = ToggleColors::default();
173            let layout = render_toggle(frame, area, &state, &colors);
174
175            // Click on checkbox
176            assert!(layout.contains(0, 0));
177            assert!(layout.contains(2, 0));
178
179            // Click on label
180            assert!(layout.contains(5, 0));
181
182            // Click outside
183            assert!(!layout.contains(15, 0));
184        });
185    }
186
187    #[test]
188    fn test_toggle_state_toggle() {
189        let mut state = ToggleState::new(false, "Test");
190        assert!(!state.checked);
191
192        state.toggle();
193        assert!(state.checked);
194
195        state.toggle();
196        assert!(!state.checked);
197    }
198
199    #[test]
200    fn test_toggle_disabled_no_toggle() {
201        let mut state = ToggleState::new(false, "Test").with_focus(FocusState::Disabled);
202        state.toggle();
203        assert!(!state.checked); // Should not change
204    }
205
206    #[test]
207    fn test_toggle_narrow_area() {
208        test_frame(2, 1, |frame, area| {
209            let state = ToggleState::new(true, "Enable");
210            let colors = ToggleColors::default();
211            let layout = render_toggle(frame, area, &state, &colors);
212
213            // Should still have some layout even if truncated
214            assert!(layout.full_area.width <= area.width);
215        });
216    }
217}