checkbox.rs

  1use gpui::{div, prelude::*, Component, ElementId, 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(Component)]
 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> Checkbox<V> {
 23    pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
 24        Self {
 25            id: id.into(),
 26            checked,
 27            disabled: false,
 28            on_click: None,
 29        }
 30    }
 31
 32    pub fn disabled(mut self, disabled: bool) -> Self {
 33        self.disabled = disabled;
 34        self
 35    }
 36
 37    pub fn on_click(
 38        mut self,
 39        handler: impl 'static + Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync,
 40    ) -> Self {
 41        self.on_click = Some(Arc::new(handler));
 42        self
 43    }
 44
 45    pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
 46        let group_id = format!("checkbox_group_{:?}", self.id);
 47
 48        let icon = match self.checked {
 49            // When selected, we show a checkmark.
 50            Selection::Selected => {
 51                Some(
 52                    IconElement::new(Icon::Check)
 53                        .size(crate::IconSize::Small)
 54                        .color(
 55                            // If the checkbox is disabled we change the color of the icon.
 56                            if self.disabled {
 57                                TextColor::Disabled
 58                            } else {
 59                                TextColor::Selected
 60                            },
 61                        ),
 62                )
 63            }
 64            // In an indeterminate state, we show a dash.
 65            Selection::Indeterminate => {
 66                Some(
 67                    IconElement::new(Icon::Dash)
 68                        .size(crate::IconSize::Small)
 69                        .color(
 70                            // If the checkbox is disabled we change the color of the icon.
 71                            if self.disabled {
 72                                TextColor::Disabled
 73                            } else {
 74                                TextColor::Selected
 75                            },
 76                        ),
 77                )
 78            }
 79            // When unselected, we show nothing.
 80            Selection::Unselected => None,
 81        };
 82
 83        // A checkbox could be in an indeterminate state,
 84        // for example the indeterminate state could represent:
 85        //  - a group of options of which only some are selected
 86        //  - an enabled option that is no longer available
 87        //  - a previously agreed to license that has been updated
 88        //
 89        // For the sake of styles we treat the indeterminate state as selected,
 90        // but it's icon will be different.
 91        let selected =
 92            self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
 93
 94        // We could use something like this to make the checkbox background when selected:
 95        //
 96        // ~~~rust
 97        // ...
 98        // .when(selected, |this| {
 99        //     this.bg(cx.theme().colors().element_selected)
100        // })
101        // ~~~
102        //
103        // But we use a match instead here because the checkbox might be disabled,
104        // and it could be disabled _while_ it is selected, as well as while it is not selected.
105        let (bg_color, border_color) = match (self.disabled, selected) {
106            (true, _) => (
107                cx.theme().colors().ghost_element_disabled,
108                cx.theme().colors().border_disabled,
109            ),
110            (false, true) => (
111                cx.theme().colors().element_selected,
112                cx.theme().colors().border,
113            ),
114            (false, false) => (
115                cx.theme().colors().element_background,
116                cx.theme().colors().border,
117            ),
118        };
119
120        div()
121            .id(self.id)
122            // Rather than adding `px_1()` to add some space around the checkbox,
123            // we use a larger parent element to create a slightly larger
124            // click area for the checkbox.
125            .size_5()
126            // Because we've enlarged the click area, we need to create a
127            // `group` to pass down interactivity events to the checkbox.
128            .group(group_id.clone())
129            .child(
130                div()
131                    .flex()
132                    // This prevent the flex element from growing
133                    // or shrinking in response to any size changes
134                    .flex_none()
135                    // The combo of `justify_center()` and `items_center()`
136                    // is used frequently to center elements in a flex container.
137                    //
138                    // We use this to center the icon in the checkbox.
139                    .justify_center()
140                    .items_center()
141                    .m_1()
142                    .size_4()
143                    .rounded_sm()
144                    .bg(bg_color)
145                    .border()
146                    .border_color(border_color)
147                    // We only want the interactivity states to fire when we
148                    // are in a checkbox that isn't disabled.
149                    .when(!self.disabled, |this| {
150                        // Here instead of `hover()` we use `group_hover()`
151                        // to pass it the group id.
152                        this.group_hover(group_id.clone(), |el| {
153                            el.bg(cx.theme().colors().element_hover)
154                        })
155                    })
156                    .children(icon),
157            )
158            .when_some(
159                self.on_click.filter(|_| !self.disabled),
160                |this, on_click| {
161                    this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx))
162                },
163            )
164    }
165}
166
167#[cfg(feature = "stories")]
168pub use stories::*;
169
170#[cfg(feature = "stories")]
171mod stories {
172    use super::*;
173    use crate::{h_stack, Story};
174    use gpui::{Div, Render};
175
176    pub struct CheckboxStory;
177
178    impl Render for CheckboxStory {
179        type Element = Div<Self>;
180
181        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
182            Story::container(cx)
183                .child(Story::title_for::<_, Checkbox<Self>>(cx))
184                .child(Story::label(cx, "Default"))
185                .child(
186                    h_stack()
187                        .p_2()
188                        .gap_2()
189                        .rounded_md()
190                        .border()
191                        .border_color(cx.theme().colors().border)
192                        .child(Checkbox::new("checkbox-enabled", Selection::Unselected))
193                        .child(Checkbox::new(
194                            "checkbox-intermediate",
195                            Selection::Indeterminate,
196                        ))
197                        .child(Checkbox::new("checkbox-selected", Selection::Selected)),
198                )
199                .child(Story::label(cx, "Disabled"))
200                .child(
201                    h_stack()
202                        .p_2()
203                        .gap_2()
204                        .rounded_md()
205                        .border()
206                        .border_color(cx.theme().colors().border)
207                        .child(
208                            Checkbox::new("checkbox-disabled", Selection::Unselected)
209                                .disabled(true),
210                        )
211                        .child(
212                            Checkbox::new(
213                                "checkbox-disabled-intermediate",
214                                Selection::Indeterminate,
215                            )
216                            .disabled(true),
217                        )
218                        .child(
219                            Checkbox::new("checkbox-disabled-selected", Selection::Selected)
220                                .disabled(true),
221                        ),
222                )
223        }
224    }
225}