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