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