checkbox.rs

  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}