1#![allow(missing_docs)]
2
3use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
4use std::sync::Arc;
5
6use crate::prelude::*;
7use crate::utils::is_light;
8use crate::{Color, Icon, IconName, ToggleState};
9
10/// Creates a new checkbox
11pub fn checkbox(id: impl Into<ElementId>, toggle_state: ToggleState) -> Checkbox {
12 Checkbox::new(id, toggle_state)
13}
14
15/// Creates a new switch
16pub fn switch(id: impl Into<ElementId>, toggle_state: ToggleState) -> Switch {
17 Switch::new(id, toggle_state)
18}
19
20/// # Checkbox
21///
22/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
23/// Each checkbox works independently from other checkboxes in the list,
24/// therefore checking an additional box does not affect any other selections.
25#[derive(IntoElement)]
26pub struct Checkbox {
27 id: ElementId,
28 toggle_state: ToggleState,
29 disabled: bool,
30 on_click: Option<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
31}
32
33impl Checkbox {
34 pub fn new(id: impl Into<ElementId>, checked: ToggleState) -> Self {
35 Self {
36 id: id.into(),
37 toggle_state: checked,
38 disabled: false,
39 on_click: None,
40 }
41 }
42
43 pub fn disabled(mut self, disabled: bool) -> Self {
44 self.disabled = disabled;
45 self
46 }
47
48 pub fn on_click(
49 mut self,
50 handler: impl Fn(&ToggleState, &mut WindowContext) + 'static,
51 ) -> Self {
52 self.on_click = Some(Box::new(handler));
53 self
54 }
55}
56
57impl RenderOnce for Checkbox {
58 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
59 let group_id = format!("checkbox_group_{:?}", self.id);
60
61 let icon = match self.toggle_state {
62 ToggleState::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
63 if self.disabled {
64 Color::Disabled
65 } else {
66 Color::Selected
67 },
68 )),
69 ToggleState::Indeterminate => Some(
70 Icon::new(IconName::Dash)
71 .size(IconSize::Small)
72 .color(if self.disabled {
73 Color::Disabled
74 } else {
75 Color::Selected
76 }),
77 ),
78 ToggleState::Unselected => None,
79 };
80
81 let selected = self.toggle_state == ToggleState::Selected
82 || self.toggle_state == ToggleState::Indeterminate;
83
84 let (bg_color, border_color) = match (self.disabled, selected) {
85 (true, _) => (
86 cx.theme().colors().ghost_element_disabled,
87 cx.theme().colors().border_disabled,
88 ),
89 (false, true) => (
90 cx.theme().colors().element_selected,
91 cx.theme().colors().border,
92 ),
93 (false, false) => (
94 cx.theme().colors().element_background,
95 cx.theme().colors().border,
96 ),
97 };
98
99 h_flex()
100 .id(self.id)
101 .justify_center()
102 .items_center()
103 .size(DynamicSpacing::Base20.rems(cx))
104 .group(group_id.clone())
105 .child(
106 div()
107 .flex()
108 .flex_none()
109 .justify_center()
110 .items_center()
111 .m(DynamicSpacing::Base04.px(cx))
112 .size(DynamicSpacing::Base16.rems(cx))
113 .rounded_sm()
114 .bg(bg_color)
115 .border_1()
116 .border_color(border_color)
117 .when(!self.disabled, |this| {
118 this.group_hover(group_id.clone(), |el| {
119 el.bg(cx.theme().colors().element_hover)
120 })
121 })
122 .children(icon),
123 )
124 .when_some(
125 self.on_click.filter(|_| !self.disabled),
126 |this, on_click| {
127 this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
128 },
129 )
130 }
131}
132
133/// A [`Checkbox`] that has a [`Label`].
134#[derive(IntoElement)]
135pub struct CheckboxWithLabel {
136 id: ElementId,
137 label: Label,
138 checked: ToggleState,
139 on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
140}
141
142impl CheckboxWithLabel {
143 pub fn new(
144 id: impl Into<ElementId>,
145 label: Label,
146 checked: ToggleState,
147 on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
148 ) -> Self {
149 Self {
150 id: id.into(),
151 label,
152 checked,
153 on_click: Arc::new(on_click),
154 }
155 }
156}
157
158impl RenderOnce for CheckboxWithLabel {
159 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
160 h_flex()
161 .gap(DynamicSpacing::Base08.rems(cx))
162 .child(Checkbox::new(self.id.clone(), self.checked).on_click({
163 let on_click = self.on_click.clone();
164 move |checked, cx| {
165 (on_click)(checked, cx);
166 }
167 }))
168 .child(
169 div()
170 .id(SharedString::from(format!("{}-label", self.id)))
171 .on_click(move |_event, cx| {
172 (self.on_click)(&self.checked.inverse(), cx);
173 })
174 .child(self.label),
175 )
176 }
177}
178
179/// # Switch
180///
181/// Switches are used to represent opposite states, such as enabled or disabled.
182#[derive(IntoElement)]
183pub struct Switch {
184 id: ElementId,
185 toggle_state: ToggleState,
186 disabled: bool,
187 on_click: Option<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
188}
189
190impl Switch {
191 pub fn new(id: impl Into<ElementId>, state: ToggleState) -> Self {
192 Self {
193 id: id.into(),
194 toggle_state: state,
195 disabled: false,
196 on_click: None,
197 }
198 }
199
200 pub fn disabled(mut self, disabled: bool) -> Self {
201 self.disabled = disabled;
202 self
203 }
204
205 pub fn on_click(
206 mut self,
207 handler: impl Fn(&ToggleState, &mut WindowContext) + 'static,
208 ) -> Self {
209 self.on_click = Some(Box::new(handler));
210 self
211 }
212}
213
214impl RenderOnce for Switch {
215 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
216 let is_on = self.toggle_state == ToggleState::Selected;
217 let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 };
218 let base_color = cx.theme().colors().text;
219
220 let bg_color = if is_on {
221 cx.theme()
222 .colors()
223 .element_background
224 .blend(base_color.opacity(0.08))
225 } else {
226 cx.theme().colors().element_background
227 };
228 let thumb_color = base_color.opacity(0.8);
229 let thumb_hover_color = base_color;
230 let border_color = cx.theme().colors().border_variant;
231 // Lighter themes need higher contrast borders
232 let border_hover_color = if is_on {
233 border_color.blend(base_color.opacity(0.16 * adjust_ratio))
234 } else {
235 border_color.blend(base_color.opacity(0.05 * adjust_ratio))
236 };
237 let thumb_opacity = match (is_on, self.disabled) {
238 (_, true) => 0.2,
239 (true, false) => 1.0,
240 (false, false) => 0.5,
241 };
242
243 let group_id = format!("switch_group_{:?}", self.id);
244
245 h_flex()
246 .id(self.id)
247 .items_center()
248 .w(DynamicSpacing::Base32.rems(cx))
249 .h(DynamicSpacing::Base20.rems(cx))
250 .group(group_id.clone())
251 .child(
252 h_flex()
253 .when(is_on, |on| on.justify_end())
254 .when(!is_on, |off| off.justify_start())
255 .items_center()
256 .size_full()
257 .rounded_full()
258 .px(DynamicSpacing::Base02.px(cx))
259 .bg(bg_color)
260 .border_1()
261 .border_color(border_color)
262 .when(!self.disabled, |this| {
263 this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color))
264 })
265 .child(
266 div()
267 .size(DynamicSpacing::Base12.rems(cx))
268 .rounded_full()
269 .bg(thumb_color)
270 .when(!self.disabled, |this| {
271 this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color))
272 })
273 .opacity(thumb_opacity),
274 ),
275 )
276 .when_some(
277 self.on_click.filter(|_| !self.disabled),
278 |this, on_click| {
279 this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
280 },
281 )
282 }
283}
284
285impl ComponentPreview for Checkbox {
286 fn description() -> impl Into<Option<&'static str>> {
287 "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
288 }
289
290 fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
291 vec![
292 example_group_with_title(
293 "Default",
294 vec![
295 single_example(
296 "Unselected",
297 Checkbox::new("checkbox_unselected", ToggleState::Unselected),
298 ),
299 single_example(
300 "Indeterminate",
301 Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
302 ),
303 single_example(
304 "Selected",
305 Checkbox::new("checkbox_selected", ToggleState::Selected),
306 ),
307 ],
308 ),
309 example_group_with_title(
310 "Disabled",
311 vec![
312 single_example(
313 "Unselected",
314 Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
315 .disabled(true),
316 ),
317 single_example(
318 "Indeterminate",
319 Checkbox::new(
320 "checkbox_disabled_indeterminate",
321 ToggleState::Indeterminate,
322 )
323 .disabled(true),
324 ),
325 single_example(
326 "Selected",
327 Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
328 .disabled(true),
329 ),
330 ],
331 ),
332 ]
333 }
334}
335
336impl ComponentPreview for Switch {
337 fn description() -> impl Into<Option<&'static str>> {
338 "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
339 }
340
341 fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
342 vec![
343 example_group_with_title(
344 "Default",
345 vec![
346 single_example(
347 "Off",
348 Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _cx| {}),
349 ),
350 single_example(
351 "On",
352 Switch::new("switch_on", ToggleState::Selected).on_click(|_, _cx| {}),
353 ),
354 ],
355 ),
356 example_group_with_title(
357 "Disabled",
358 vec![
359 single_example(
360 "Off",
361 Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
362 ),
363 single_example(
364 "On",
365 Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
366 ),
367 ],
368 ),
369 ]
370 }
371}
372
373impl ComponentPreview for CheckboxWithLabel {
374 fn description() -> impl Into<Option<&'static str>> {
375 "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
376 }
377
378 fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
379 vec![example_group(vec![
380 single_example(
381 "Unselected",
382 CheckboxWithLabel::new(
383 "checkbox_with_label_unselected",
384 Label::new("Always save on quit"),
385 ToggleState::Unselected,
386 |_, _| {},
387 ),
388 ),
389 single_example(
390 "Indeterminate",
391 CheckboxWithLabel::new(
392 "checkbox_with_label_indeterminate",
393 Label::new("Always save on quit"),
394 ToggleState::Indeterminate,
395 |_, _| {},
396 ),
397 ),
398 single_example(
399 "Selected",
400 CheckboxWithLabel::new(
401 "checkbox_with_label_selected",
402 Label::new("Always save on quit"),
403 ToggleState::Selected,
404 |_, _| {},
405 ),
406 ),
407 ])]
408 }
409}