1use gpui::{div, hsla, prelude::*, AnyView, ElementId, Hsla, IntoElement, Styled, Window};
2use std::sync::Arc;
3
4use crate::utils::is_light;
5use crate::{prelude::*, ElevationIndex, KeyBinding};
6use crate::{Color, Icon, IconName, ToggleState};
7
8// TODO: Checkbox, CheckboxWithLabel, and Switch could all be
9// restructured to use a ToggleLike, similar to Button/Buttonlike, Label/Labellike
10
11/// Creates a new checkbox.
12pub fn checkbox(id: impl Into<ElementId>, toggle_state: ToggleState) -> Checkbox {
13 Checkbox::new(id, toggle_state)
14}
15
16/// Creates a new switch.
17pub fn switch(id: impl Into<ElementId>, toggle_state: ToggleState) -> Switch {
18 Switch::new(id, toggle_state)
19}
20
21/// The visual style of a toggle.
22#[derive(Debug, Default, Clone, PartialEq, Eq)]
23pub enum ToggleStyle {
24 /// Toggle has a transparent background
25 #[default]
26 Ghost,
27 /// Toggle has a filled background based on the
28 /// elevation index of the parent container
29 ElevationBased(ElevationIndex),
30 /// A custom style using a color to tint the toggle
31 Custom(Hsla),
32}
33
34/// # Checkbox
35///
36/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
37/// Each checkbox works independently from other checkboxes in the list,
38/// therefore checking an additional box does not affect any other selections.
39#[derive(IntoElement)]
40pub struct Checkbox {
41 id: ElementId,
42 toggle_state: ToggleState,
43 disabled: bool,
44 on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>,
45 filled: bool,
46 style: ToggleStyle,
47 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
48}
49
50impl Checkbox {
51 /// Creates a new [`Checkbox`].
52 pub fn new(id: impl Into<ElementId>, checked: ToggleState) -> Self {
53 Self {
54 id: id.into(),
55 toggle_state: checked,
56 disabled: false,
57 on_click: None,
58 filled: false,
59 style: ToggleStyle::default(),
60 tooltip: None,
61 }
62 }
63
64 /// Sets the disabled state of the [`Checkbox`].
65 pub fn disabled(mut self, disabled: bool) -> Self {
66 self.disabled = disabled;
67 self
68 }
69
70 /// Binds a handler to the [`Checkbox`] that will be called when clicked.
71 pub fn on_click(
72 mut self,
73 handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
74 ) -> Self {
75 self.on_click = Some(Box::new(handler));
76 self
77 }
78
79 /// Sets the `fill` setting of the checkbox, indicating whether it should be filled.
80 pub fn fill(mut self) -> Self {
81 self.filled = true;
82 self
83 }
84
85 /// Sets the style of the checkbox using the specified [`ToggleStyle`].
86 pub fn style(mut self, style: ToggleStyle) -> Self {
87 self.style = style;
88 self
89 }
90
91 /// Match the style of the checkbox to the current elevation using [`ToggleStyle::ElevationBased`].
92 pub fn elevation(mut self, elevation: ElevationIndex) -> Self {
93 self.style = ToggleStyle::ElevationBased(elevation);
94 self
95 }
96
97 /// Sets the tooltip for the checkbox.
98 pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
99 self.tooltip = Some(Box::new(tooltip));
100 self
101 }
102}
103
104impl Checkbox {
105 fn bg_color(&self, cx: &App) -> Hsla {
106 let style = self.style.clone();
107 match (style, self.filled) {
108 (ToggleStyle::Ghost, false) => cx.theme().colors().ghost_element_background,
109 (ToggleStyle::Ghost, true) => cx.theme().colors().element_background,
110 (ToggleStyle::ElevationBased(_), false) => gpui::transparent_black(),
111 (ToggleStyle::ElevationBased(elevation), true) => elevation.darker_bg(cx),
112 (ToggleStyle::Custom(_), false) => gpui::transparent_black(),
113 (ToggleStyle::Custom(color), true) => color.opacity(0.2),
114 }
115 }
116
117 fn border_color(&self, cx: &App) -> Hsla {
118 if self.disabled {
119 return cx.theme().colors().border_disabled;
120 }
121
122 match self.style.clone() {
123 ToggleStyle::Ghost => cx.theme().colors().border_variant,
124 ToggleStyle::ElevationBased(elevation) => elevation.on_elevation_bg(cx),
125 ToggleStyle::Custom(color) => color.opacity(0.3),
126 }
127 }
128}
129
130impl RenderOnce for Checkbox {
131 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
132 let group_id = format!("checkbox_group_{:?}", self.id);
133 let icon = match self.toggle_state {
134 ToggleState::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
135 if self.disabled {
136 Color::Disabled
137 } else {
138 Color::Selected
139 },
140 )),
141 ToggleState::Indeterminate => Some(
142 Icon::new(IconName::Dash)
143 .size(IconSize::Small)
144 .color(if self.disabled {
145 Color::Disabled
146 } else {
147 Color::Selected
148 }),
149 ),
150 ToggleState::Unselected => None,
151 };
152
153 let bg_color = self.bg_color(cx);
154 let border_color = self.border_color(cx);
155
156 h_flex()
157 .id(self.id)
158 .justify_center()
159 .items_center()
160 .size(DynamicSpacing::Base20.rems(cx))
161 .group(group_id.clone())
162 .child(
163 div()
164 .flex()
165 .flex_none()
166 .justify_center()
167 .items_center()
168 .m(DynamicSpacing::Base04.px(cx))
169 .size(DynamicSpacing::Base16.rems(cx))
170 .rounded_sm()
171 .bg(bg_color)
172 .border_1()
173 .border_color(border_color)
174 .when(!self.disabled, |this| {
175 this.group_hover(group_id.clone(), |el| {
176 el.bg(cx.theme().colors().element_hover)
177 })
178 })
179 .children(icon),
180 )
181 .when_some(
182 self.on_click.filter(|_| !self.disabled),
183 |this, on_click| {
184 this.on_click(move |_, window, cx| {
185 on_click(&self.toggle_state.inverse(), window, cx)
186 })
187 },
188 )
189 .when_some(self.tooltip, |this, tooltip| {
190 this.tooltip(move |window, cx| tooltip(window, cx))
191 })
192 }
193}
194
195/// A [`Checkbox`] that has a [`Label`].
196#[derive(IntoElement)]
197pub struct CheckboxWithLabel {
198 id: ElementId,
199 label: Label,
200 checked: ToggleState,
201 on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
202 filled: bool,
203 style: ToggleStyle,
204}
205
206impl CheckboxWithLabel {
207 /// Creates a checkbox with an attached label.
208 pub fn new(
209 id: impl Into<ElementId>,
210 label: Label,
211 checked: ToggleState,
212 on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
213 ) -> Self {
214 Self {
215 id: id.into(),
216 label,
217 checked,
218 on_click: Arc::new(on_click),
219 filled: false,
220 style: ToggleStyle::default(),
221 }
222 }
223
224 /// Sets the style of the checkbox using the specified [`ToggleStyle`].
225 pub fn style(mut self, style: ToggleStyle) -> Self {
226 self.style = style;
227 self
228 }
229
230 /// Match the style of the checkbox to the current elevation using [`ToggleStyle::ElevationBased`].
231 pub fn elevation(mut self, elevation: ElevationIndex) -> Self {
232 self.style = ToggleStyle::ElevationBased(elevation);
233 self
234 }
235
236 /// Sets the `fill` setting of the checkbox, indicating whether it should be filled.
237 pub fn fill(mut self) -> Self {
238 self.filled = true;
239 self
240 }
241}
242
243impl RenderOnce for CheckboxWithLabel {
244 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
245 h_flex()
246 .gap(DynamicSpacing::Base08.rems(cx))
247 .child(
248 Checkbox::new(self.id.clone(), self.checked)
249 .style(self.style)
250 .when(self.filled, Checkbox::fill)
251 .on_click({
252 let on_click = self.on_click.clone();
253 move |checked, window, cx| {
254 (on_click)(checked, window, cx);
255 }
256 }),
257 )
258 .child(
259 div()
260 .id(SharedString::from(format!("{}-label", self.id)))
261 .on_click(move |_event, window, cx| {
262 (self.on_click)(&self.checked.inverse(), window, cx);
263 })
264 .child(self.label),
265 )
266 }
267}
268
269/// # Switch
270///
271/// Switches are used to represent opposite states, such as enabled or disabled.
272#[derive(IntoElement)]
273pub struct Switch {
274 id: ElementId,
275 toggle_state: ToggleState,
276 disabled: bool,
277 on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>,
278 label: Option<SharedString>,
279 key_binding: Option<KeyBinding>,
280}
281
282impl Switch {
283 /// Creates a new [`Switch`].
284 pub fn new(id: impl Into<ElementId>, state: ToggleState) -> Self {
285 Self {
286 id: id.into(),
287 toggle_state: state,
288 disabled: false,
289 on_click: None,
290 label: None,
291 key_binding: None,
292 }
293 }
294
295 /// Sets the disabled state of the [`Switch`].
296 pub fn disabled(mut self, disabled: bool) -> Self {
297 self.disabled = disabled;
298 self
299 }
300
301 /// Binds a handler to the [`Switch`] that will be called when clicked.
302 pub fn on_click(
303 mut self,
304 handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
305 ) -> Self {
306 self.on_click = Some(Box::new(handler));
307 self
308 }
309
310 /// Sets the label of the [`Switch`].
311 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
312 self.label = Some(label.into());
313 self
314 }
315
316 /// Display the keybinding that triggers the switch action.
317 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
318 self.key_binding = key_binding.into();
319 self
320 }
321}
322
323impl RenderOnce for Switch {
324 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
325 let is_on = self.toggle_state == ToggleState::Selected;
326 let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 };
327 let base_color = cx.theme().colors().text;
328
329 let bg_color = if is_on {
330 cx.theme()
331 .colors()
332 .element_background
333 .blend(base_color.opacity(0.08))
334 } else {
335 cx.theme().colors().element_background
336 };
337 let thumb_color = base_color.opacity(0.8);
338 let thumb_hover_color = base_color;
339 let border_color = cx.theme().colors().border_variant;
340 // Lighter themes need higher contrast borders
341 let border_hover_color = if is_on {
342 border_color.blend(base_color.opacity(0.16 * adjust_ratio))
343 } else {
344 border_color.blend(base_color.opacity(0.05 * adjust_ratio))
345 };
346 let thumb_opacity = match (is_on, self.disabled) {
347 (_, true) => 0.2,
348 (true, false) => 1.0,
349 (false, false) => 0.5,
350 };
351
352 let group_id = format!("switch_group_{:?}", self.id);
353
354 let switch = h_flex()
355 .w(DynamicSpacing::Base32.rems(cx))
356 .h(DynamicSpacing::Base20.rems(cx))
357 .group(group_id.clone())
358 .child(
359 h_flex()
360 .when(is_on, |on| on.justify_end())
361 .when(!is_on, |off| off.justify_start())
362 .items_center()
363 .size_full()
364 .rounded_full()
365 .px(DynamicSpacing::Base02.px(cx))
366 .bg(bg_color)
367 .border_1()
368 .border_color(border_color)
369 .when(!self.disabled, |this| {
370 this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color))
371 })
372 .child(
373 div()
374 .size(DynamicSpacing::Base12.rems(cx))
375 .rounded_full()
376 .bg(thumb_color)
377 .when(!self.disabled, |this| {
378 this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color))
379 })
380 .opacity(thumb_opacity),
381 ),
382 );
383
384 h_flex()
385 .id(self.id)
386 .gap(DynamicSpacing::Base06.rems(cx))
387 .child(switch)
388 .when_some(
389 self.on_click.filter(|_| !self.disabled),
390 |this, on_click| {
391 this.on_click(move |_, window, cx| {
392 on_click(&self.toggle_state.inverse(), window, cx)
393 })
394 },
395 )
396 .when_some(self.label, |this, label| {
397 this.child(Label::new(label).size(LabelSize::Small))
398 })
399 .children(self.key_binding)
400 }
401}
402
403impl ComponentPreview for Checkbox {
404 fn description() -> impl Into<Option<&'static str>> {
405 "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
406 }
407
408 fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
409 vec![
410 example_group_with_title(
411 "Default",
412 vec![
413 single_example(
414 "Unselected",
415 Checkbox::new("checkbox_unselected", ToggleState::Unselected),
416 ),
417 single_example(
418 "Indeterminate",
419 Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
420 ),
421 single_example(
422 "Selected",
423 Checkbox::new("checkbox_selected", ToggleState::Selected),
424 ),
425 ],
426 ),
427 example_group_with_title(
428 "Default (Filled)",
429 vec![
430 single_example(
431 "Unselected",
432 Checkbox::new("checkbox_unselected", ToggleState::Unselected).fill(),
433 ),
434 single_example(
435 "Indeterminate",
436 Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate).fill(),
437 ),
438 single_example(
439 "Selected",
440 Checkbox::new("checkbox_selected", ToggleState::Selected).fill(),
441 ),
442 ],
443 ),
444 example_group_with_title(
445 "ElevationBased",
446 vec![
447 single_example(
448 "Unselected",
449 Checkbox::new("checkbox_unfilled_unselected", ToggleState::Unselected)
450 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
451 ),
452 single_example(
453 "Indeterminate",
454 Checkbox::new(
455 "checkbox_unfilled_indeterminate",
456 ToggleState::Indeterminate,
457 )
458 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
459 ),
460 single_example(
461 "Selected",
462 Checkbox::new("checkbox_unfilled_selected", ToggleState::Selected)
463 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
464 ),
465 ],
466 ),
467 example_group_with_title(
468 "ElevationBased (Filled)",
469 vec![
470 single_example(
471 "Unselected",
472 Checkbox::new("checkbox_filled_unselected", ToggleState::Unselected)
473 .fill()
474 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
475 ),
476 single_example(
477 "Indeterminate",
478 Checkbox::new("checkbox_filled_indeterminate", ToggleState::Indeterminate)
479 .fill()
480 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
481 ),
482 single_example(
483 "Selected",
484 Checkbox::new("checkbox_filled_selected", ToggleState::Selected)
485 .fill()
486 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
487 ),
488 ],
489 ),
490 example_group_with_title(
491 "Custom Color",
492 vec![
493 single_example(
494 "Unselected",
495 Checkbox::new("checkbox_custom_unselected", ToggleState::Unselected)
496 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
497 ),
498 single_example(
499 "Indeterminate",
500 Checkbox::new("checkbox_custom_indeterminate", ToggleState::Indeterminate)
501 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
502 ),
503 single_example(
504 "Selected",
505 Checkbox::new("checkbox_custom_selected", ToggleState::Selected)
506 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
507 ),
508 ],
509 ),
510 example_group_with_title(
511 "Custom Color (Filled)",
512 vec![
513 single_example(
514 "Unselected",
515 Checkbox::new("checkbox_custom_filled_unselected", ToggleState::Unselected)
516 .fill()
517 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
518 ),
519 single_example(
520 "Indeterminate",
521 Checkbox::new(
522 "checkbox_custom_filled_indeterminate",
523 ToggleState::Indeterminate,
524 )
525 .fill()
526 .style(ToggleStyle::Custom(hsla(
527 142.0 / 360.,
528 0.68,
529 0.45,
530 0.7,
531 ))),
532 ),
533 single_example(
534 "Selected",
535 Checkbox::new("checkbox_custom_filled_selected", ToggleState::Selected)
536 .fill()
537 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
538 ),
539 ],
540 ),
541 example_group_with_title(
542 "Disabled",
543 vec![
544 single_example(
545 "Unselected",
546 Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
547 .disabled(true),
548 ),
549 single_example(
550 "Indeterminate",
551 Checkbox::new(
552 "checkbox_disabled_indeterminate",
553 ToggleState::Indeterminate,
554 )
555 .disabled(true),
556 ),
557 single_example(
558 "Selected",
559 Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
560 .disabled(true),
561 ),
562 ],
563 ),
564 example_group_with_title(
565 "Disabled (Filled)",
566 vec![
567 single_example(
568 "Unselected",
569 Checkbox::new(
570 "checkbox_disabled_filled_unselected",
571 ToggleState::Unselected,
572 )
573 .fill()
574 .disabled(true),
575 ),
576 single_example(
577 "Indeterminate",
578 Checkbox::new(
579 "checkbox_disabled_filled_indeterminate",
580 ToggleState::Indeterminate,
581 )
582 .fill()
583 .disabled(true),
584 ),
585 single_example(
586 "Selected",
587 Checkbox::new("checkbox_disabled_filled_selected", ToggleState::Selected)
588 .fill()
589 .disabled(true),
590 ),
591 ],
592 ),
593 ]
594 }
595}
596
597impl ComponentPreview for Switch {
598 fn description() -> impl Into<Option<&'static str>> {
599 "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
600 }
601
602 fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
603 vec![
604 example_group_with_title(
605 "Default",
606 vec![
607 single_example(
608 "Off",
609 Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _, _cx| {}),
610 ),
611 single_example(
612 "On",
613 Switch::new("switch_on", ToggleState::Selected).on_click(|_, _, _cx| {}),
614 ),
615 ],
616 ),
617 example_group_with_title(
618 "Disabled",
619 vec![
620 single_example(
621 "Off",
622 Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
623 ),
624 single_example(
625 "On",
626 Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
627 ),
628 ],
629 ),
630 example_group_with_title(
631 "Label Permutations",
632 vec![
633 single_example(
634 "Label",
635 Switch::new("switch_with_label", ToggleState::Selected)
636 .label("Always save on quit"),
637 ),
638 single_example(
639 "Keybinding",
640 Switch::new("switch_with_label", ToggleState::Selected)
641 .key_binding(theme_preview_keybinding("cmd-shift-e")),
642 ),
643 ],
644 ),
645 ]
646 }
647}
648
649impl ComponentPreview for CheckboxWithLabel {
650 fn description() -> impl Into<Option<&'static str>> {
651 "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
652 }
653
654 fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
655 vec![example_group(vec![
656 single_example(
657 "Unselected",
658 CheckboxWithLabel::new(
659 "checkbox_with_label_unselected",
660 Label::new("Always save on quit"),
661 ToggleState::Unselected,
662 |_, _, _| {},
663 ),
664 ),
665 single_example(
666 "Indeterminate",
667 CheckboxWithLabel::new(
668 "checkbox_with_label_indeterminate",
669 Label::new("Always save on quit"),
670 ToggleState::Indeterminate,
671 |_, _, _| {},
672 ),
673 ),
674 single_example(
675 "Selected",
676 CheckboxWithLabel::new(
677 "checkbox_with_label_selected",
678 Label::new("Always save on quit"),
679 ToggleState::Selected,
680 |_, _, _| {},
681 ),
682 ),
683 ])]
684 }
685}