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) => (
124                cx.theme().colors().element_background,
125                cx.theme().colors().border,
126            ),
127        };
128
129        div()
130            // Rather than adding `px_1()` to add some space around the checkbox,
131            // we use a larger parent element to create a slightly larger
132            // click area for the checkbox.
133            .size_5()
134            // Because we've enlarged the click area, we need to create a
135            // `group` to pass down interaction events to the checkbox.
136            .group(group_id.clone())
137            .child(
138                div()
139                    .flex()
140                    // This prevent the flex element from growing
141                    // or shrinking in response to any size changes
142                    .flex_none()
143                    // The combo of `justify_center()` and `items_center()`
144                    // is used frequently to center elements in a flex container.
145                    //
146                    // We use this to center the icon in the checkbox.
147                    .justify_center()
148                    .items_center()
149                    .m_1()
150                    .size_4()
151                    .rounded_sm()
152                    .bg(bg_color)
153                    .border()
154                    .border_color(border_color)
155                    // We only want the interaction states to fire when we
156                    // are in a checkbox that isn't disabled.
157                    .when(!self.disabled, |this| {
158                        // Here instead of `hover()` we use `group_hover()`
159                        // to pass it the group id.
160                        this.group_hover(group_id.clone(), |el| {
161                            el.bg(cx.theme().colors().element_hover)
162                        })
163                    })
164                    .child(icon),
165            )
166    }
167}
168
169#[cfg(feature = "stories")]
170pub use stories::*;
171
172#[cfg(feature = "stories")]
173mod stories {
174    use super::*;
175    use crate::{h_stack, Story};
176    use gpui2::{Div, Render};
177
178    pub struct CheckboxStory;
179
180    impl Render for CheckboxStory {
181        type Element = Div<Self>;
182
183        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
184            Story::container(cx)
185                .child(Story::title_for::<_, Checkbox>(cx))
186                .child(Story::label(cx, "Default"))
187                .child(
188                    h_stack()
189                        .p_2()
190                        .gap_2()
191                        .rounded_md()
192                        .border()
193                        .border_color(cx.theme().colors().border)
194                        .child(Checkbox::new("checkbox-enabled"))
195                        .child(Checkbox::new("checkbox-intermediate").set_indeterminate())
196                        .child(Checkbox::new("checkbox-selected").toggle()),
197                )
198                .child(Story::label(cx, "Disabled"))
199                .child(
200                    h_stack()
201                        .p_2()
202                        .gap_2()
203                        .rounded_md()
204                        .border()
205                        .border_color(cx.theme().colors().border)
206                        .child(Checkbox::new("checkbox-disabled").set_disabled(true))
207                        .child(
208                            Checkbox::new("checkbox-disabled-intermediate")
209                                .set_disabled(true)
210                                .set_indeterminate(),
211                        )
212                        .child(
213                            Checkbox::new("checkbox-disabled-selected")
214                                .set_disabled(true)
215                                .toggle(),
216                        ),
217                )
218        }
219    }
220}