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}