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}