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