checkbox.rs

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