checkbox.rs

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