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