Skip to main content

guise/input/
checkbox.rs

1//! `Checkbox` — a controlled boolean toggle with an optional label.
2
3use gpui::prelude::*;
4use gpui::{div, px, App, ClickEvent, ElementId, FontWeight, IntoElement, SharedString, Window};
5
6use super::{control_box_size, ClickHandler};
7use crate::devtools::Probed;
8use crate::reactive::Binding;
9use crate::theme::{theme, ColorName, Size};
10
11/// A controlled checkbox. Pass `checked` and a change
12/// handler (via `cx.listener`); the parent view owns the value. Or hand it a
13/// [`Binding`] via [`Checkbox::bind`] and skip the handler.
14#[derive(IntoElement)]
15pub struct Checkbox {
16    id: ElementId,
17    checked: bool,
18    indeterminate: bool,
19    label: Option<SharedString>,
20    size: Size,
21    color: ColorName,
22    disabled: bool,
23    binding: Option<Binding<bool>>,
24    on_change: Option<ClickHandler>,
25}
26
27impl Checkbox {
28    pub fn new(id: impl Into<ElementId>) -> Self {
29        Checkbox {
30            id: id.into(),
31            checked: false,
32            indeterminate: false,
33            label: None,
34            size: Size::Sm,
35            color: ColorName::Blue,
36            disabled: false,
37            binding: None,
38            on_change: None,
39        }
40    }
41
42    pub fn checked(mut self, checked: bool) -> Self {
43        self.checked = checked;
44        self
45    }
46
47    pub fn indeterminate(mut self, indeterminate: bool) -> Self {
48        self.indeterminate = indeterminate;
49        self
50    }
51
52    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
53        self.label = Some(label.into());
54        self
55    }
56
57    pub fn size(mut self, size: Size) -> Self {
58        self.size = size;
59        self
60    }
61
62    pub fn color(mut self, color: ColorName) -> Self {
63        self.color = color;
64        self
65    }
66
67    pub fn disabled(mut self, disabled: bool) -> Self {
68        self.disabled = disabled;
69        self
70    }
71
72    /// Two-way bind the checked state. Overrides `checked`; clicks write the
73    /// toggled value back through the binding, then run any `on_change`.
74    pub fn bind(mut self, binding: Binding<bool>) -> Self {
75        self.binding = Some(binding);
76        self
77    }
78
79    pub fn on_change(
80        mut self,
81        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
82    ) -> Self {
83        self.on_change = Some(Box::new(handler));
84        self
85    }
86}
87
88impl RenderOnce for Checkbox {
89    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
90        let t = theme(cx);
91        let checked = self.binding.as_ref().map_or(self.checked, |b| b.get(cx));
92        let on = checked || self.indeterminate;
93        let accent = t.color(self.color, t.primary_shade());
94        let box_size = control_box_size(self.size);
95
96        let mut check = div()
97            .w(px(box_size))
98            .h(px(box_size))
99            .flex()
100            .items_center()
101            .justify_center()
102            .rounded(px(t.radius(Size::Xs) + 2.0))
103            .text_size(px(box_size * 0.7))
104            .font_weight(FontWeight::BOLD);
105        if on {
106            check = check
107                .bg(accent.hsla())
108                .text_color(accent.contrasting().hsla())
109                .child(SharedString::new_static(if self.indeterminate {
110                    "\u{2212}"
111                } else {
112                    "\u{2713}"
113                }));
114        } else {
115            check = check
116                .bg(t.surface().hsla())
117                .border_1()
118                .border_color(t.border().hsla());
119        }
120
121        let mut row = div()
122            .id(self.id)
123            .flex()
124            .items_center()
125            .gap(px(8.0))
126            .child(check);
127        if let Some(label) = self.label {
128            row = row.child(
129                div()
130                    .text_size(px(t.font_size(self.size)))
131                    .text_color(t.text().hsla())
132                    .child(label),
133            );
134        }
135
136        let element = if self.disabled {
137            row.opacity(0.5)
138        } else {
139            if self.binding.is_some() || self.on_change.is_some() {
140                let binding = self.binding;
141                let handler = self.on_change;
142                let next = !checked;
143                row = row.on_click(move |ev, window, cx| {
144                    if let Some(binding) = &binding {
145                        binding.set(cx, next);
146                    }
147                    if let Some(handler) = &handler {
148                        handler(ev, window, cx);
149                    }
150                });
151            }
152            row
153        };
154
155        element
156            .probe("Checkbox")
157            .attr("size", self.size.label())
158            .attr_if("checked", self.checked)
159            .attr_if("indeterminate", self.indeterminate)
160            .attr_if("disabled", self.disabled)
161    }
162}