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}