Skip to main content

ccf_gpui_widgets/widgets/
checkbox.rs

1//! Checkbox widget
2//!
3//! A simple checkbox with optional label. Supports keyboard interaction
4//! (Space/Enter to toggle) and mouse clicks.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use ccf_gpui_widgets::widgets::Checkbox;
10//!
11//! let checkbox = cx.new(|cx| {
12//!     Checkbox::new(cx)
13//!         .with_checked(true)
14//!         .label("Enable feature")
15//! });
16//!
17//! // Subscribe to changes
18//! cx.subscribe(&checkbox, |this, _checkbox, event: &CheckboxEvent, cx| {
19//!     if let CheckboxEvent::Change(checked) = event {
20//!         println!("Checkbox is now: {}", checked);
21//!     }
22//! }).detach();
23//! ```
24
25use gpui::prelude::*;
26use gpui::*;
27
28use crate::theme::{get_theme_or, Theme};
29use super::focus_navigation::{handle_tab_navigation, with_focus_actions, EnabledCursorExt};
30
31/// Events emitted by Checkbox
32#[derive(Clone, Debug)]
33pub enum CheckboxEvent {
34    /// Checkbox state changed.
35    /// The boolean indicates the new checked state: `true` = checked, `false` = unchecked.
36    Change(bool),
37}
38
39/// Checkbox widget
40pub struct Checkbox {
41    checked: bool,
42    label: Option<SharedString>,
43    focus_handle: FocusHandle,
44    custom_theme: Option<Theme>,
45    /// Whether the widget is enabled (interactive)
46    enabled: bool,
47}
48
49impl EventEmitter<CheckboxEvent> for Checkbox {}
50
51impl Focusable for Checkbox {
52    fn focus_handle(&self, _cx: &App) -> FocusHandle {
53        self.focus_handle.clone()
54    }
55}
56
57impl Checkbox {
58    /// Create a new checkbox
59    pub fn new(cx: &mut Context<Self>) -> Self {
60        Self {
61            checked: false,
62            label: None,
63            focus_handle: cx.focus_handle().tab_stop(true),
64            custom_theme: None,
65            enabled: true,
66        }
67    }
68
69    /// Set initial checked state (builder pattern)
70    #[must_use]
71    pub fn with_checked(mut self, value: bool) -> Self {
72        self.checked = value;
73        self
74    }
75
76    /// Set label text (builder pattern)
77    #[must_use]
78    pub fn label(mut self, text: impl Into<SharedString>) -> Self {
79        self.label = Some(text.into());
80        self
81    }
82
83    /// Set custom theme (builder pattern)
84    #[must_use]
85    pub fn theme(mut self, theme: Theme) -> Self {
86        self.custom_theme = Some(theme);
87        self
88    }
89
90    /// Set enabled state (builder pattern)
91    #[must_use]
92    pub fn with_enabled(mut self, enabled: bool) -> Self {
93        self.enabled = enabled;
94        self
95    }
96
97    /// Get current checked state
98    pub fn is_checked(&self) -> bool {
99        self.checked
100    }
101
102    /// Set checked state programmatically
103    pub fn set_checked(&mut self, checked: bool, cx: &mut Context<Self>) {
104        if self.checked != checked {
105            self.checked = checked;
106            cx.emit(CheckboxEvent::Change(checked));
107            cx.notify();
108        }
109    }
110
111    /// Get the focus handle
112    pub fn focus_handle(&self) -> &FocusHandle {
113        &self.focus_handle
114    }
115
116    /// Check if the checkbox is enabled
117    pub fn is_enabled(&self) -> bool {
118        self.enabled
119    }
120
121    /// Set enabled state programmatically
122    pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
123        if self.enabled != enabled {
124            self.enabled = enabled;
125            cx.notify();
126        }
127    }
128
129    /// Set label text programmatically
130    pub fn set_label(&mut self, label: impl Into<SharedString>, cx: &mut Context<Self>) {
131        self.label = Some(label.into());
132        cx.notify();
133    }
134
135    /// Clear the label
136    pub fn clear_label(&mut self, cx: &mut Context<Self>) {
137        self.label = None;
138        cx.notify();
139    }
140
141    fn toggle(&mut self, cx: &mut Context<Self>) {
142        self.checked = !self.checked;
143        cx.emit(CheckboxEvent::Change(self.checked));
144        cx.notify();
145    }
146}
147
148impl Render for Checkbox {
149    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
150        let theme = get_theme_or(cx, self.custom_theme.as_ref());
151        let checked = self.checked;
152        let label = self.label.clone();
153        let focus_handle = self.focus_handle.clone();
154        let is_focused = self.focus_handle.is_focused(window);
155        let enabled = self.enabled;
156
157        with_focus_actions(
158            div()
159                .id("ccf_checkbox")
160                .track_focus(&focus_handle)
161                .tab_stop(enabled),
162            cx,
163        )
164        .on_key_down(cx.listener(move |checkbox, event: &KeyDownEvent, window, cx| {
165            if !checkbox.enabled {
166                return;
167            }
168            if handle_tab_navigation(event, window) {
169                return;
170            }
171            if matches!(event.keystroke.key.as_str(), "space" | "enter") {
172                checkbox.toggle(cx);
173            }
174        }))
175        .flex()
176        .flex_row()
177        .gap_2()
178        .items_center()
179        .py_1()
180        .px_1()
181        .rounded_sm()
182        .cursor_for_enabled(enabled)
183            .border_2()
184            .border_color(if is_focused && enabled { rgb(theme.border_focus) } else { rgba(0x00000000) })
185            .when(enabled, |d| {
186                d.on_mouse_down(MouseButton::Left, cx.listener(|checkbox, _event, window, cx| {
187                    checkbox.focus_handle.focus(window);
188                    checkbox.toggle(cx);
189                }))
190            })
191            .child(
192                // Checkbox box
193                div()
194                    .w(px(20.))
195                    .h(px(20.))
196                    .border_1()
197                    .rounded_sm()
198                    .flex()
199                    .items_center()
200                    .justify_center()
201                    .when(!enabled, |d| {
202                        // Disabled styling
203                        d.bg(rgb(theme.disabled_bg))
204                            .border_color(rgb(theme.disabled_bg))
205                            .when(checked, |d| {
206                                d.child(
207                                    div()
208                                        .text_color(rgb(theme.disabled_text))
209                                        .text_sm()
210                                        .child("✓")
211                                )
212                            })
213                    })
214                    .when(enabled && checked, |d| {
215                        d.bg(rgb(theme.primary))
216                            .border_color(rgb(theme.primary))
217                            .child(
218                                div()
219                                    .text_color(rgb(theme.text_black))
220                                    .text_sm()
221                                    .child("✓")
222                            )
223                    })
224                    .when(enabled && !checked, |d| {
225                        d.bg(rgb(theme.bg_input))
226                            .border_color(rgb(theme.border_input))
227                            .hover(|d| d.bg(rgb(theme.bg_input_hover)))
228                    })
229            )
230            .when_some(label, |d, label_text| {
231                d.child(
232                    div()
233                        .text_sm()
234                        .font_weight(FontWeight::SEMIBOLD)
235                        .when(enabled, |d| d.text_color(rgb(theme.text_label)))
236                        .when(!enabled, |d| d.text_color(rgb(theme.disabled_text)))
237                        .child(label_text)
238                )
239            })
240    }
241}