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