checkbox.rs

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