1use gpui::{div, prelude::*, Div, Element, ElementId, RenderOnce, Stateful, Styled, ViewContext};
2use std::sync::Arc;
3use theme2::ActiveTheme;
4
5use crate::{Icon, IconElement, Selection, TextColor};
6
7pub type CheckHandler<V> = Arc<dyn Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync>;
8
9/// # Checkbox
10///
11/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
12/// Each checkbox works independently from other checkboxes in the list,
13/// therefore checking an additional box does not affect any other selections.
14#[derive(RenderOnce)]
15pub struct Checkbox<V: 'static> {
16 id: ElementId,
17 checked: Selection,
18 disabled: bool,
19 on_click: Option<CheckHandler<V>>,
20}
21
22impl<V: 'static> Component<V> for Checkbox<V> {
23 type Rendered = Stateful<V, Div<V>>;
24
25 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
26 let group_id = format!("checkbox_group_{:?}", self.id);
27
28 let icon = match self.checked {
29 // When selected, we show a checkmark.
30 Selection::Selected => {
31 Some(
32 IconElement::new(Icon::Check)
33 .size(crate::IconSize::Small)
34 .color(
35 // If the checkbox is disabled we change the color of the icon.
36 if self.disabled {
37 TextColor::Disabled
38 } else {
39 TextColor::Selected
40 },
41 ),
42 )
43 }
44 // In an indeterminate state, we show a dash.
45 Selection::Indeterminate => {
46 Some(
47 IconElement::new(Icon::Dash)
48 .size(crate::IconSize::Small)
49 .color(
50 // If the checkbox is disabled we change the color of the icon.
51 if self.disabled {
52 TextColor::Disabled
53 } else {
54 TextColor::Selected
55 },
56 ),
57 )
58 }
59 // When unselected, we show nothing.
60 Selection::Unselected => None,
61 };
62
63 // A checkbox could be in an indeterminate state,
64 // for example the indeterminate state could represent:
65 // - a group of options of which only some are selected
66 // - an enabled option that is no longer available
67 // - a previously agreed to license that has been updated
68 //
69 // For the sake of styles we treat the indeterminate state as selected,
70 // but it's icon will be different.
71 let selected =
72 self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
73
74 // We could use something like this to make the checkbox background when selected:
75 //
76 // ~~~rust
77 // ...
78 // .when(selected, |this| {
79 // this.bg(cx.theme().colors().element_selected)
80 // })
81 // ~~~
82 //
83 // But we use a match instead here because the checkbox might be disabled,
84 // and it could be disabled _while_ it is selected, as well as while it is not selected.
85 let (bg_color, border_color) = match (self.disabled, selected) {
86 (true, _) => (
87 cx.theme().colors().ghost_element_disabled,
88 cx.theme().colors().border_disabled,
89 ),
90 (false, true) => (
91 cx.theme().colors().element_selected,
92 cx.theme().colors().border,
93 ),
94 (false, false) => (
95 cx.theme().colors().element_background,
96 cx.theme().colors().border,
97 ),
98 };
99
100 div()
101 .id(self.id)
102 // Rather than adding `px_1()` to add some space around the checkbox,
103 // we use a larger parent element to create a slightly larger
104 // click area for the checkbox.
105 .size_5()
106 // Because we've enlarged the click area, we need to create a
107 // `group` to pass down interactivity events to the checkbox.
108 .group(group_id.clone())
109 .child(
110 div()
111 .flex()
112 // This prevent the flex element from growing
113 // or shrinking in response to any size changes
114 .flex_none()
115 // The combo of `justify_center()` and `items_center()`
116 // is used frequently to center elements in a flex container.
117 //
118 // We use this to center the icon in the checkbox.
119 .justify_center()
120 .items_center()
121 .m_1()
122 .size_4()
123 .rounded_sm()
124 .bg(bg_color)
125 .border()
126 .border_color(border_color)
127 // We only want the interactivity states to fire when we
128 // are in a checkbox that isn't disabled.
129 .when(!self.disabled, |this| {
130 // Here instead of `hover()` we use `group_hover()`
131 // to pass it the group id.
132 this.group_hover(group_id.clone(), |el| {
133 el.bg(cx.theme().colors().element_hover)
134 })
135 })
136 .children(icon),
137 )
138 .when_some(
139 self.on_click.filter(|_| !self.disabled),
140 |this, on_click| {
141 this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx))
142 },
143 )
144 }
145}
146impl<V: 'static> Checkbox<V> {
147 pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
148 Self {
149 id: id.into(),
150 checked,
151 disabled: false,
152 on_click: None,
153 }
154 }
155
156 pub fn disabled(mut self, disabled: bool) -> Self {
157 self.disabled = disabled;
158 self
159 }
160
161 pub fn on_click(
162 mut self,
163 handler: impl 'static + Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync,
164 ) -> Self {
165 self.on_click = Some(Arc::new(handler));
166 self
167 }
168
169 pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
170 let group_id = format!("checkbox_group_{:?}", self.id);
171
172 let icon = match self.checked {
173 // When selected, we show a checkmark.
174 Selection::Selected => {
175 Some(
176 IconElement::new(Icon::Check)
177 .size(crate::IconSize::Small)
178 .color(
179 // If the checkbox is disabled we change the color of the icon.
180 if self.disabled {
181 TextColor::Disabled
182 } else {
183 TextColor::Selected
184 },
185 ),
186 )
187 }
188 // In an indeterminate state, we show a dash.
189 Selection::Indeterminate => {
190 Some(
191 IconElement::new(Icon::Dash)
192 .size(crate::IconSize::Small)
193 .color(
194 // If the checkbox is disabled we change the color of the icon.
195 if self.disabled {
196 TextColor::Disabled
197 } else {
198 TextColor::Selected
199 },
200 ),
201 )
202 }
203 // When unselected, we show nothing.
204 Selection::Unselected => None,
205 };
206
207 // A checkbox could be in an indeterminate state,
208 // for example the indeterminate state could represent:
209 // - a group of options of which only some are selected
210 // - an enabled option that is no longer available
211 // - a previously agreed to license that has been updated
212 //
213 // For the sake of styles we treat the indeterminate state as selected,
214 // but it's icon will be different.
215 let selected =
216 self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
217
218 // We could use something like this to make the checkbox background when selected:
219 //
220 // ~~~rust
221 // ...
222 // .when(selected, |this| {
223 // this.bg(cx.theme().colors().element_selected)
224 // })
225 // ~~~
226 //
227 // But we use a match instead here because the checkbox might be disabled,
228 // and it could be disabled _while_ it is selected, as well as while it is not selected.
229 let (bg_color, border_color) = match (self.disabled, selected) {
230 (true, _) => (
231 cx.theme().colors().ghost_element_disabled,
232 cx.theme().colors().border_disabled,
233 ),
234 (false, true) => (
235 cx.theme().colors().element_selected,
236 cx.theme().colors().border,
237 ),
238 (false, false) => (
239 cx.theme().colors().element_background,
240 cx.theme().colors().border,
241 ),
242 };
243
244 div()
245 .id(self.id)
246 // Rather than adding `px_1()` to add some space around the checkbox,
247 // we use a larger parent element to create a slightly larger
248 // click area for the checkbox.
249 .size_5()
250 // Because we've enlarged the click area, we need to create a
251 // `group` to pass down interactivity events to the checkbox.
252 .group(group_id.clone())
253 .child(
254 div()
255 .flex()
256 // This prevent the flex element from growing
257 // or shrinking in response to any size changes
258 .flex_none()
259 // The combo of `justify_center()` and `items_center()`
260 // is used frequently to center elements in a flex container.
261 //
262 // We use this to center the icon in the checkbox.
263 .justify_center()
264 .items_center()
265 .m_1()
266 .size_4()
267 .rounded_sm()
268 .bg(bg_color)
269 .border()
270 .border_color(border_color)
271 // We only want the interactivity states to fire when we
272 // are in a checkbox that isn't disabled.
273 .when(!self.disabled, |this| {
274 // Here instead of `hover()` we use `group_hover()`
275 // to pass it the group id.
276 this.group_hover(group_id.clone(), |el| {
277 el.bg(cx.theme().colors().element_hover)
278 })
279 })
280 .children(icon),
281 )
282 .when_some(
283 self.on_click.filter(|_| !self.disabled),
284 |this, on_click| {
285 this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx))
286 },
287 )
288 }
289}
290
291#[cfg(feature = "stories")]
292pub use stories::*;
293
294#[cfg(feature = "stories")]
295mod stories {
296 use super::*;
297 use crate::{h_stack, Story};
298 use gpui::{Div, Render};
299
300 pub struct CheckboxStory;
301
302 impl Render<Self> for CheckboxStory {
303 type Element = Div<Self>;
304
305 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
306 Story::container(cx)
307 .child(Story::title_for::<_, Checkbox<Self>>(cx))
308 .child(Story::label(cx, "Default"))
309 .child(
310 h_stack()
311 .p_2()
312 .gap_2()
313 .rounded_md()
314 .border()
315 .border_color(cx.theme().colors().border)
316 .child(Checkbox::new("checkbox-enabled", Selection::Unselected))
317 .child(Checkbox::new(
318 "checkbox-intermediate",
319 Selection::Indeterminate,
320 ))
321 .child(Checkbox::new("checkbox-selected", Selection::Selected)),
322 )
323 .child(Story::label(cx, "Disabled"))
324 .child(
325 h_stack()
326 .p_2()
327 .gap_2()
328 .rounded_md()
329 .border()
330 .border_color(cx.theme().colors().border)
331 .child(
332 Checkbox::new("checkbox-disabled", Selection::Unselected)
333 .disabled(true),
334 )
335 .child(
336 Checkbox::new(
337 "checkbox-disabled-intermediate",
338 Selection::Indeterminate,
339 )
340 .disabled(true),
341 )
342 .child(
343 Checkbox::new("checkbox-disabled-selected", Selection::Selected)
344 .disabled(true),
345 ),
346 )
347 }
348 }
349}