checkbox.rs

  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}