1use gpui::{div, prelude::*, Div, Element, ElementId, RenderOnce, Styled, WindowContext};
2
3use theme2::ActiveTheme;
4
5use crate::{Color, Icon, IconElement, Selection};
6
7pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
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 {
16 id: ElementId,
17 checked: Selection,
18 disabled: bool,
19 on_click: Option<CheckHandler>,
20}
21
22impl Component for Checkbox {
23 type Rendered = gpui::Stateful<Div>;
24
25 fn render(self, cx: &mut WindowContext) -> 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 Color::Disabled
38 } else {
39 Color::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 Color::Disabled
53 } else {
54 Color::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| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
141 )
142 }
143}
144impl Checkbox {
145 pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
146 Self {
147 id: id.into(),
148 checked,
149 disabled: false,
150 on_click: None,
151 }
152 }
153
154 pub fn disabled(mut self, disabled: bool) -> Self {
155 self.disabled = disabled;
156 self
157 }
158
159 pub fn on_click(
160 mut self,
161 handler: impl 'static + Fn(&Selection, &mut WindowContext) + Send + Sync,
162 ) -> Self {
163 self.on_click = Some(Box::new(handler));
164 self
165 }
166
167 pub fn render(self, cx: &mut WindowContext) -> impl Element {
168 let group_id = format!("checkbox_group_{:?}", self.id);
169
170 let icon = match self.checked {
171 // When selected, we show a checkmark.
172 Selection::Selected => {
173 Some(
174 IconElement::new(Icon::Check)
175 .size(crate::IconSize::Small)
176 .color(
177 // If the checkbox is disabled we change the color of the icon.
178 if self.disabled {
179 Color::Disabled
180 } else {
181 Color::Selected
182 },
183 ),
184 )
185 }
186 // In an indeterminate state, we show a dash.
187 Selection::Indeterminate => {
188 Some(
189 IconElement::new(Icon::Dash)
190 .size(crate::IconSize::Small)
191 .color(
192 // If the checkbox is disabled we change the color of the icon.
193 if self.disabled {
194 Color::Disabled
195 } else {
196 Color::Selected
197 },
198 ),
199 )
200 }
201 // When unselected, we show nothing.
202 Selection::Unselected => None,
203 };
204
205 // A checkbox could be in an indeterminate state,
206 // for example the indeterminate state could represent:
207 // - a group of options of which only some are selected
208 // - an enabled option that is no longer available
209 // - a previously agreed to license that has been updated
210 //
211 // For the sake of styles we treat the indeterminate state as selected,
212 // but it's icon will be different.
213 let selected =
214 self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
215
216 // We could use something like this to make the checkbox background when selected:
217 //
218 // ~~~rust
219 // ...
220 // .when(selected, |this| {
221 // this.bg(cx.theme().colors().element_selected)
222 // })
223 // ~~~
224 //
225 // But we use a match instead here because the checkbox might be disabled,
226 // and it could be disabled _while_ it is selected, as well as while it is not selected.
227 let (bg_color, border_color) = match (self.disabled, selected) {
228 (true, _) => (
229 cx.theme().colors().ghost_element_disabled,
230 cx.theme().colors().border_disabled,
231 ),
232 (false, true) => (
233 cx.theme().colors().element_selected,
234 cx.theme().colors().border,
235 ),
236 (false, false) => (
237 cx.theme().colors().element_background,
238 cx.theme().colors().border,
239 ),
240 };
241
242 div()
243 .id(self.id)
244 // Rather than adding `px_1()` to add some space around the checkbox,
245 // we use a larger parent element to create a slightly larger
246 // click area for the checkbox.
247 .size_5()
248 // Because we've enlarged the click area, we need to create a
249 // `group` to pass down interactivity events to the checkbox.
250 .group(group_id.clone())
251 .child(
252 div()
253 .flex()
254 // This prevent the flex element from growing
255 // or shrinking in response to any size changes
256 .flex_none()
257 // The combo of `justify_center()` and `items_center()`
258 // is used frequently to center elements in a flex container.
259 //
260 // We use this to center the icon in the checkbox.
261 .justify_center()
262 .items_center()
263 .m_1()
264 .size_4()
265 .rounded_sm()
266 .bg(bg_color)
267 .border()
268 .border_color(border_color)
269 // We only want the interactivity states to fire when we
270 // are in a checkbox that isn't disabled.
271 .when(!self.disabled, |this| {
272 // Here instead of `hover()` we use `group_hover()`
273 // to pass it the group id.
274 this.group_hover(group_id.clone(), |el| {
275 el.bg(cx.theme().colors().element_hover)
276 })
277 })
278 .children(icon),
279 )
280 .when_some(
281 self.on_click.filter(|_| !self.disabled),
282 |this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
283 )
284 }
285}