1use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
2
3use crate::prelude::*;
4use crate::{Color, Icon, IconName, Selection};
5
6/// # Checkbox
7///
8/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
9/// Each checkbox works independently from other checkboxes in the list,
10/// therefore checking an additional box does not affect any other selections.
11#[derive(IntoElement)]
12pub struct Checkbox {
13 id: ElementId,
14 checked: Selection,
15 disabled: bool,
16 on_click: Option<Box<dyn Fn(&Selection, &mut WindowContext) + 'static>>,
17}
18
19impl Checkbox {
20 pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
21 Self {
22 id: id.into(),
23 checked,
24 disabled: false,
25 on_click: None,
26 }
27 }
28
29 pub fn disabled(mut self, disabled: bool) -> Self {
30 self.disabled = disabled;
31 self
32 }
33
34 pub fn on_click(mut self, handler: impl Fn(&Selection, &mut WindowContext) + 'static) -> Self {
35 self.on_click = Some(Box::new(handler));
36 self
37 }
38}
39
40impl RenderOnce for Checkbox {
41 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
42 let group_id = format!("checkbox_group_{:?}", self.id);
43
44 let icon = match self.checked {
45 Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
46 if self.disabled {
47 Color::Disabled
48 } else {
49 Color::Selected
50 },
51 )),
52 Selection::Indeterminate => Some(
53 Icon::new(IconName::Dash)
54 .size(IconSize::Small)
55 .color(if self.disabled {
56 Color::Disabled
57 } else {
58 Color::Selected
59 }),
60 ),
61 Selection::Unselected => None,
62 };
63
64 // A checkbox could be in an indeterminate state,
65 // for example the indeterminate state could represent:
66 // - a group of options of which only some are selected
67 // - an enabled option that is no longer available
68 // - a previously agreed to license that has been updated
69 //
70 // For the sake of styles we treat the indeterminate state as selected,
71 // but its icon will be different.
72 let selected =
73 self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
74
75 // We could use something like this to make the checkbox background when selected:
76 //
77 // ```rs
78 // ...
79 // .when(selected, |this| {
80 // this.bg(cx.theme().colors().element_selected)
81 // })
82 // ```
83 //
84 // But we use a match instead here because the checkbox might be disabled,
85 // and it could be disabled _while_ it is selected, as well as while it is not selected.
86 let (bg_color, border_color) = match (self.disabled, selected) {
87 (true, _) => (
88 cx.theme().colors().ghost_element_disabled,
89 cx.theme().colors().border_disabled,
90 ),
91 (false, true) => (
92 cx.theme().colors().element_selected,
93 cx.theme().colors().border,
94 ),
95 (false, false) => (
96 cx.theme().colors().element_background,
97 cx.theme().colors().border,
98 ),
99 };
100
101 h_flex()
102 .id(self.id)
103 .justify_center()
104 .items_center()
105 // Rather than adding `px_1()` to add some space around the checkbox,
106 // we use a larger parent element to create a slightly larger
107 // click area for the checkbox.
108 .size_5()
109 // Because we've enlarged the click area, we need to create a
110 // `group` to pass down interactivity events to the checkbox.
111 .group(group_id.clone())
112 .child(
113 div()
114 .flex()
115 // This prevent the flex element from growing
116 // or shrinking in response to any size changes
117 .flex_none()
118 // The combo of `justify_center()` and `items_center()`
119 // is used frequently to center elements in a flex container.
120 //
121 // We use this to center the icon in the checkbox.
122 .justify_center()
123 .items_center()
124 .m_1()
125 .size_4()
126 .rounded_sm()
127 .bg(bg_color)
128 .border()
129 .border_color(border_color)
130 // We only want the interactivity states to fire when we
131 // are in a checkbox that isn't disabled.
132 .when(!self.disabled, |this| {
133 // Here instead of `hover()` we use `group_hover()`
134 // to pass it the group id.
135 this.group_hover(group_id.clone(), |el| {
136 el.bg(cx.theme().colors().element_hover)
137 })
138 })
139 .children(icon),
140 )
141 .when_some(
142 self.on_click.filter(|_| !self.disabled),
143 |this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
144 )
145 }
146}