checkbox.rs

  1use gpui::{div, prelude::*, Div, Element, ElementId, RenderOnce, Stateful, Styled, ViewContext};
  2use std::sync::Arc;
  3use theme2::ActiveTheme;
  4
  5use crate::{Icon, IconElement, Selection, TextColor};
  6
  7pub type CheckHandler<V> = Arc<dyn Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync>;
  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<V: 'static> {
 16    id: ElementId,
 17    checked: Selection,
 18    disabled: bool,
 19    on_click: Option<CheckHandler<V>>,
 20}
 21
 22impl<V: 'static> Component<V> for Checkbox<V> {
 23    type Rendered = Stateful<V, Div<V>>;
 24
 25    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> 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                                TextColor::Disabled
 38                            } else {
 39                                TextColor::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                                TextColor::Disabled
 53                            } else {
 54                                TextColor::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| {
141                    this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx))
142                },
143            )
144    }
145}
146impl<V: 'static> Checkbox<V> {
147    pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
148        Self {
149            id: id.into(),
150            checked,
151            disabled: false,
152            on_click: None,
153        }
154    }
155
156    pub fn disabled(mut self, disabled: bool) -> Self {
157        self.disabled = disabled;
158        self
159    }
160
161    pub fn on_click(
162        mut self,
163        handler: impl 'static + Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync,
164    ) -> Self {
165        self.on_click = Some(Arc::new(handler));
166        self
167    }
168
169    pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
170        let group_id = format!("checkbox_group_{:?}", self.id);
171
172        let icon = match self.checked {
173            // When selected, we show a checkmark.
174            Selection::Selected => {
175                Some(
176                    IconElement::new(Icon::Check)
177                        .size(crate::IconSize::Small)
178                        .color(
179                            // If the checkbox is disabled we change the color of the icon.
180                            if self.disabled {
181                                TextColor::Disabled
182                            } else {
183                                TextColor::Selected
184                            },
185                        ),
186                )
187            }
188            // In an indeterminate state, we show a dash.
189            Selection::Indeterminate => {
190                Some(
191                    IconElement::new(Icon::Dash)
192                        .size(crate::IconSize::Small)
193                        .color(
194                            // If the checkbox is disabled we change the color of the icon.
195                            if self.disabled {
196                                TextColor::Disabled
197                            } else {
198                                TextColor::Selected
199                            },
200                        ),
201                )
202            }
203            // When unselected, we show nothing.
204            Selection::Unselected => None,
205        };
206
207        // A checkbox could be in an indeterminate state,
208        // for example the indeterminate state could represent:
209        //  - a group of options of which only some are selected
210        //  - an enabled option that is no longer available
211        //  - a previously agreed to license that has been updated
212        //
213        // For the sake of styles we treat the indeterminate state as selected,
214        // but it's icon will be different.
215        let selected =
216            self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
217
218        // We could use something like this to make the checkbox background when selected:
219        //
220        // ~~~rust
221        // ...
222        // .when(selected, |this| {
223        //     this.bg(cx.theme().colors().element_selected)
224        // })
225        // ~~~
226        //
227        // But we use a match instead here because the checkbox might be disabled,
228        // and it could be disabled _while_ it is selected, as well as while it is not selected.
229        let (bg_color, border_color) = match (self.disabled, selected) {
230            (true, _) => (
231                cx.theme().colors().ghost_element_disabled,
232                cx.theme().colors().border_disabled,
233            ),
234            (false, true) => (
235                cx.theme().colors().element_selected,
236                cx.theme().colors().border,
237            ),
238            (false, false) => (
239                cx.theme().colors().element_background,
240                cx.theme().colors().border,
241            ),
242        };
243
244        div()
245            .id(self.id)
246            // Rather than adding `px_1()` to add some space around the checkbox,
247            // we use a larger parent element to create a slightly larger
248            // click area for the checkbox.
249            .size_5()
250            // Because we've enlarged the click area, we need to create a
251            // `group` to pass down interactivity events to the checkbox.
252            .group(group_id.clone())
253            .child(
254                div()
255                    .flex()
256                    // This prevent the flex element from growing
257                    // or shrinking in response to any size changes
258                    .flex_none()
259                    // The combo of `justify_center()` and `items_center()`
260                    // is used frequently to center elements in a flex container.
261                    //
262                    // We use this to center the icon in the checkbox.
263                    .justify_center()
264                    .items_center()
265                    .m_1()
266                    .size_4()
267                    .rounded_sm()
268                    .bg(bg_color)
269                    .border()
270                    .border_color(border_color)
271                    // We only want the interactivity states to fire when we
272                    // are in a checkbox that isn't disabled.
273                    .when(!self.disabled, |this| {
274                        // Here instead of `hover()` we use `group_hover()`
275                        // to pass it the group id.
276                        this.group_hover(group_id.clone(), |el| {
277                            el.bg(cx.theme().colors().element_hover)
278                        })
279                    })
280                    .children(icon),
281            )
282            .when_some(
283                self.on_click.filter(|_| !self.disabled),
284                |this, on_click| {
285                    this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx))
286                },
287            )
288    }
289}
290
291#[cfg(feature = "stories")]
292pub use stories::*;
293
294#[cfg(feature = "stories")]
295mod stories {
296    use super::*;
297    use crate::{h_stack, Story};
298    use gpui::{Div, Render};
299
300    pub struct CheckboxStory;
301
302    impl Render<Self> for CheckboxStory {
303        type Element = Div<Self>;
304
305        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
306            Story::container(cx)
307                .child(Story::title_for::<_, Checkbox<Self>>(cx))
308                .child(Story::label(cx, "Default"))
309                .child(
310                    h_stack()
311                        .p_2()
312                        .gap_2()
313                        .rounded_md()
314                        .border()
315                        .border_color(cx.theme().colors().border)
316                        .child(Checkbox::new("checkbox-enabled", Selection::Unselected))
317                        .child(Checkbox::new(
318                            "checkbox-intermediate",
319                            Selection::Indeterminate,
320                        ))
321                        .child(Checkbox::new("checkbox-selected", Selection::Selected)),
322                )
323                .child(Story::label(cx, "Disabled"))
324                .child(
325                    h_stack()
326                        .p_2()
327                        .gap_2()
328                        .rounded_md()
329                        .border()
330                        .border_color(cx.theme().colors().border)
331                        .child(
332                            Checkbox::new("checkbox-disabled", Selection::Unselected)
333                                .disabled(true),
334                        )
335                        .child(
336                            Checkbox::new(
337                                "checkbox-disabled-intermediate",
338                                Selection::Indeterminate,
339                            )
340                            .disabled(true),
341                        )
342                        .child(
343                            Checkbox::new("checkbox-disabled-selected", Selection::Selected)
344                                .disabled(true),
345                        ),
346                )
347        }
348    }
349}