checkbox.rs

  1use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
  2
  3use crate::prelude::*;
  4use crate::{Color, Icon, IconName, Selection};
  5
  6/// # Checkbox
  7///
  8/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
  9/// Each checkbox works independently from other checkboxes in the list,
 10/// therefore checking an additional box does not affect any other selections.
 11#[derive(IntoElement)]
 12pub struct Checkbox {
 13    id: ElementId,
 14    checked: Selection,
 15    disabled: bool,
 16    on_click: Option<Box<dyn Fn(&Selection, &mut WindowContext) + 'static>>,
 17}
 18
 19impl Checkbox {
 20    pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
 21        Self {
 22            id: id.into(),
 23            checked,
 24            disabled: false,
 25            on_click: None,
 26        }
 27    }
 28
 29    pub fn disabled(mut self, disabled: bool) -> Self {
 30        self.disabled = disabled;
 31        self
 32    }
 33
 34    pub fn on_click(mut self, handler: impl Fn(&Selection, &mut WindowContext) + 'static) -> Self {
 35        self.on_click = Some(Box::new(handler));
 36        self
 37    }
 38}
 39
 40impl RenderOnce for Checkbox {
 41    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
 42        let group_id = format!("checkbox_group_{:?}", self.id);
 43
 44        let icon = match self.checked {
 45            Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
 46                if self.disabled {
 47                    Color::Disabled
 48                } else {
 49                    Color::Selected
 50                },
 51            )),
 52            Selection::Indeterminate => Some(
 53                Icon::new(IconName::Dash)
 54                    .size(IconSize::Small)
 55                    .color(if self.disabled {
 56                        Color::Disabled
 57                    } else {
 58                        Color::Selected
 59                    }),
 60            ),
 61            Selection::Unselected => None,
 62        };
 63
 64        // A checkbox could be in an indeterminate state,
 65        // for example the indeterminate state could represent:
 66        //  - a group of options of which only some are selected
 67        //  - an enabled option that is no longer available
 68        //  - a previously agreed to license that has been updated
 69        //
 70        // For the sake of styles we treat the indeterminate state as selected,
 71        // but its icon will be different.
 72        let selected =
 73            self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
 74
 75        // We could use something like this to make the checkbox background when selected:
 76        //
 77        // ```rs
 78        // ...
 79        // .when(selected, |this| {
 80        //     this.bg(cx.theme().colors().element_selected)
 81        // })
 82        // ```
 83        //
 84        // But we use a match instead here because the checkbox might be disabled,
 85        // and it could be disabled _while_ it is selected, as well as while it is not selected.
 86        let (bg_color, border_color) = match (self.disabled, selected) {
 87            (true, _) => (
 88                cx.theme().colors().ghost_element_disabled,
 89                cx.theme().colors().border_disabled,
 90            ),
 91            (false, true) => (
 92                cx.theme().colors().element_selected,
 93                cx.theme().colors().border,
 94            ),
 95            (false, false) => (
 96                cx.theme().colors().element_background,
 97                cx.theme().colors().border,
 98            ),
 99        };
100
101        h_flex()
102            .id(self.id)
103            .justify_center()
104            .items_center()
105            // Rather than adding `px_1()` to add some space around the checkbox,
106            // we use a larger parent element to create a slightly larger
107            // click area for the checkbox.
108            .size_5()
109            // Because we've enlarged the click area, we need to create a
110            // `group` to pass down interactivity events to the checkbox.
111            .group(group_id.clone())
112            .child(
113                div()
114                    .flex()
115                    // This prevent the flex element from growing
116                    // or shrinking in response to any size changes
117                    .flex_none()
118                    // The combo of `justify_center()` and `items_center()`
119                    // is used frequently to center elements in a flex container.
120                    //
121                    // We use this to center the icon in the checkbox.
122                    .justify_center()
123                    .items_center()
124                    .m_1()
125                    .size_4()
126                    .rounded_sm()
127                    .bg(bg_color)
128                    .border()
129                    .border_color(border_color)
130                    // We only want the interactivity states to fire when we
131                    // are in a checkbox that isn't disabled.
132                    .when(!self.disabled, |this| {
133                        // Here instead of `hover()` we use `group_hover()`
134                        // to pass it the group id.
135                        this.group_hover(group_id.clone(), |el| {
136                            el.bg(cx.theme().colors().element_hover)
137                        })
138                    })
139                    .children(icon),
140            )
141            .when_some(
142                self.on_click.filter(|_| !self.disabled),
143                |this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
144            )
145    }
146}