checkbox.rs

  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}