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