1use gpui::{div, hsla, prelude::*, AnyView, ElementId, Hsla, IntoElement, Styled, WindowContext};
2use std::sync::Arc;
3
4use crate::utils::is_light;
5use crate::{prelude::*, ElevationIndex};
6use crate::{Color, Icon, IconName, ToggleState};
7
8// TODO: Checkbox, CheckboxWithLabel, Switch, SwitchWithLabel all could 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 WindowContext) + 'static>>,
45 filled: bool,
46 style: ToggleStyle,
47 tooltip: Option<Box<dyn Fn(&mut WindowContext) -> 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 WindowContext) + '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 WindowContext) -> AnyView + 'static) -> Self {
99 self.tooltip = Some(Box::new(tooltip));
100 self
101 }
102}
103
104impl Checkbox {
105 fn bg_color(&self, cx: &WindowContext) -> 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: &WindowContext) -> 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, cx: &mut WindowContext) -> 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 |_, cx| on_click(&self.toggle_state.inverse(), cx))
185 },
186 )
187 .when_some(self.tooltip, |this, tooltip| {
188 this.tooltip(move |cx| tooltip(cx))
189 })
190 }
191}
192
193/// A [`Checkbox`] that has a [`Label`].
194#[derive(IntoElement)]
195pub struct CheckboxWithLabel {
196 id: ElementId,
197 label: Label,
198 checked: ToggleState,
199 on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
200 filled: bool,
201 style: ToggleStyle,
202}
203
204impl CheckboxWithLabel {
205 /// Creates a checkbox with an attached label.
206 pub fn new(
207 id: impl Into<ElementId>,
208 label: Label,
209 checked: ToggleState,
210 on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
211 ) -> Self {
212 Self {
213 id: id.into(),
214 label,
215 checked,
216 on_click: Arc::new(on_click),
217 filled: false,
218 style: ToggleStyle::default(),
219 }
220 }
221
222 /// Sets the style of the checkbox using the specified [`ToggleStyle`].
223 pub fn style(mut self, style: ToggleStyle) -> Self {
224 self.style = style;
225 self
226 }
227
228 /// Match the style of the checkbox to the current elevation using [`ToggleStyle::ElevationBased`].
229 pub fn elevation(mut self, elevation: ElevationIndex) -> Self {
230 self.style = ToggleStyle::ElevationBased(elevation);
231 self
232 }
233
234 /// Sets the `fill` setting of the checkbox, indicating whether it should be filled.
235 pub fn fill(mut self) -> Self {
236 self.filled = true;
237 self
238 }
239}
240
241impl RenderOnce for CheckboxWithLabel {
242 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
243 h_flex()
244 .gap(DynamicSpacing::Base08.rems(cx))
245 .child(
246 Checkbox::new(self.id.clone(), self.checked)
247 .style(self.style)
248 .when(self.filled, Checkbox::fill)
249 .on_click({
250 let on_click = self.on_click.clone();
251 move |checked, cx| {
252 (on_click)(checked, cx);
253 }
254 }),
255 )
256 .child(
257 div()
258 .id(SharedString::from(format!("{}-label", self.id)))
259 .on_click(move |_event, cx| {
260 (self.on_click)(&self.checked.inverse(), cx);
261 })
262 .child(self.label),
263 )
264 }
265}
266
267/// # Switch
268///
269/// Switches are used to represent opposite states, such as enabled or disabled.
270#[derive(IntoElement)]
271pub struct Switch {
272 id: ElementId,
273 toggle_state: ToggleState,
274 disabled: bool,
275 on_click: Option<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
276}
277
278impl Switch {
279 /// Creates a new [`Switch`].
280 pub fn new(id: impl Into<ElementId>, state: ToggleState) -> Self {
281 Self {
282 id: id.into(),
283 toggle_state: state,
284 disabled: false,
285 on_click: None,
286 }
287 }
288
289 /// Sets the disabled state of the [`Switch`].
290 pub fn disabled(mut self, disabled: bool) -> Self {
291 self.disabled = disabled;
292 self
293 }
294
295 /// Binds a handler to the [`Switch`] that will be called when clicked.
296 pub fn on_click(
297 mut self,
298 handler: impl Fn(&ToggleState, &mut WindowContext) + 'static,
299 ) -> Self {
300 self.on_click = Some(Box::new(handler));
301 self
302 }
303}
304
305impl RenderOnce for Switch {
306 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
307 let is_on = self.toggle_state == ToggleState::Selected;
308 let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 };
309 let base_color = cx.theme().colors().text;
310
311 let bg_color = if is_on {
312 cx.theme()
313 .colors()
314 .element_background
315 .blend(base_color.opacity(0.08))
316 } else {
317 cx.theme().colors().element_background
318 };
319 let thumb_color = base_color.opacity(0.8);
320 let thumb_hover_color = base_color;
321 let border_color = cx.theme().colors().border_variant;
322 // Lighter themes need higher contrast borders
323 let border_hover_color = if is_on {
324 border_color.blend(base_color.opacity(0.16 * adjust_ratio))
325 } else {
326 border_color.blend(base_color.opacity(0.05 * adjust_ratio))
327 };
328 let thumb_opacity = match (is_on, self.disabled) {
329 (_, true) => 0.2,
330 (true, false) => 1.0,
331 (false, false) => 0.5,
332 };
333
334 let group_id = format!("switch_group_{:?}", self.id);
335
336 h_flex()
337 .id(self.id)
338 .items_center()
339 .w(DynamicSpacing::Base32.rems(cx))
340 .h(DynamicSpacing::Base20.rems(cx))
341 .group(group_id.clone())
342 .child(
343 h_flex()
344 .when(is_on, |on| on.justify_end())
345 .when(!is_on, |off| off.justify_start())
346 .items_center()
347 .size_full()
348 .rounded_full()
349 .px(DynamicSpacing::Base02.px(cx))
350 .bg(bg_color)
351 .border_1()
352 .border_color(border_color)
353 .when(!self.disabled, |this| {
354 this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color))
355 })
356 .child(
357 div()
358 .size(DynamicSpacing::Base12.rems(cx))
359 .rounded_full()
360 .bg(thumb_color)
361 .when(!self.disabled, |this| {
362 this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color))
363 })
364 .opacity(thumb_opacity),
365 ),
366 )
367 .when_some(
368 self.on_click.filter(|_| !self.disabled),
369 |this, on_click| {
370 this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
371 },
372 )
373 }
374}
375
376/// A [`Switch`] that has a [`Label`].
377#[derive(IntoElement)]
378pub struct SwitchWithLabel {
379 id: ElementId,
380 label: Label,
381 checked: ToggleState,
382 on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
383}
384
385impl SwitchWithLabel {
386 /// Creates a switch with an attached label.
387 pub fn new(
388 id: impl Into<ElementId>,
389 label: Label,
390 checked: ToggleState,
391 on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
392 ) -> Self {
393 Self {
394 id: id.into(),
395 label,
396 checked,
397 on_click: Arc::new(on_click),
398 }
399 }
400}
401
402impl RenderOnce for SwitchWithLabel {
403 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
404 h_flex()
405 .gap(DynamicSpacing::Base08.rems(cx))
406 .child(Switch::new(self.id.clone(), self.checked).on_click({
407 let on_click = self.on_click.clone();
408 move |checked, cx| {
409 (on_click)(checked, cx);
410 }
411 }))
412 .child(
413 div()
414 .id(SharedString::from(format!("{}-label", self.id)))
415 .on_click(move |_event, cx| {
416 (self.on_click)(&self.checked.inverse(), cx);
417 })
418 .child(self.label),
419 )
420 }
421}
422
423impl ComponentPreview for Checkbox {
424 fn description() -> impl Into<Option<&'static str>> {
425 "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
426 }
427
428 fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
429 vec![
430 example_group_with_title(
431 "Default",
432 vec![
433 single_example(
434 "Unselected",
435 Checkbox::new("checkbox_unselected", ToggleState::Unselected),
436 ),
437 single_example(
438 "Indeterminate",
439 Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
440 ),
441 single_example(
442 "Selected",
443 Checkbox::new("checkbox_selected", ToggleState::Selected),
444 ),
445 ],
446 ),
447 example_group_with_title(
448 "Default (Filled)",
449 vec![
450 single_example(
451 "Unselected",
452 Checkbox::new("checkbox_unselected", ToggleState::Unselected).fill(),
453 ),
454 single_example(
455 "Indeterminate",
456 Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate).fill(),
457 ),
458 single_example(
459 "Selected",
460 Checkbox::new("checkbox_selected", ToggleState::Selected).fill(),
461 ),
462 ],
463 ),
464 example_group_with_title(
465 "ElevationBased",
466 vec![
467 single_example(
468 "Unselected",
469 Checkbox::new("checkbox_unfilled_unselected", ToggleState::Unselected)
470 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
471 ),
472 single_example(
473 "Indeterminate",
474 Checkbox::new(
475 "checkbox_unfilled_indeterminate",
476 ToggleState::Indeterminate,
477 )
478 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
479 ),
480 single_example(
481 "Selected",
482 Checkbox::new("checkbox_unfilled_selected", ToggleState::Selected)
483 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
484 ),
485 ],
486 ),
487 example_group_with_title(
488 "ElevationBased (Filled)",
489 vec![
490 single_example(
491 "Unselected",
492 Checkbox::new("checkbox_filled_unselected", ToggleState::Unselected)
493 .fill()
494 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
495 ),
496 single_example(
497 "Indeterminate",
498 Checkbox::new("checkbox_filled_indeterminate", ToggleState::Indeterminate)
499 .fill()
500 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
501 ),
502 single_example(
503 "Selected",
504 Checkbox::new("checkbox_filled_selected", ToggleState::Selected)
505 .fill()
506 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
507 ),
508 ],
509 ),
510 example_group_with_title(
511 "Custom Color",
512 vec![
513 single_example(
514 "Unselected",
515 Checkbox::new("checkbox_custom_unselected", ToggleState::Unselected)
516 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
517 ),
518 single_example(
519 "Indeterminate",
520 Checkbox::new("checkbox_custom_indeterminate", ToggleState::Indeterminate)
521 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
522 ),
523 single_example(
524 "Selected",
525 Checkbox::new("checkbox_custom_selected", ToggleState::Selected)
526 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
527 ),
528 ],
529 ),
530 example_group_with_title(
531 "Custom Color (Filled)",
532 vec![
533 single_example(
534 "Unselected",
535 Checkbox::new("checkbox_custom_filled_unselected", ToggleState::Unselected)
536 .fill()
537 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
538 ),
539 single_example(
540 "Indeterminate",
541 Checkbox::new(
542 "checkbox_custom_filled_indeterminate",
543 ToggleState::Indeterminate,
544 )
545 .fill()
546 .style(ToggleStyle::Custom(hsla(
547 142.0 / 360.,
548 0.68,
549 0.45,
550 0.7,
551 ))),
552 ),
553 single_example(
554 "Selected",
555 Checkbox::new("checkbox_custom_filled_selected", ToggleState::Selected)
556 .fill()
557 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
558 ),
559 ],
560 ),
561 example_group_with_title(
562 "Disabled",
563 vec![
564 single_example(
565 "Unselected",
566 Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
567 .disabled(true),
568 ),
569 single_example(
570 "Indeterminate",
571 Checkbox::new(
572 "checkbox_disabled_indeterminate",
573 ToggleState::Indeterminate,
574 )
575 .disabled(true),
576 ),
577 single_example(
578 "Selected",
579 Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
580 .disabled(true),
581 ),
582 ],
583 ),
584 example_group_with_title(
585 "Disabled (Filled)",
586 vec![
587 single_example(
588 "Unselected",
589 Checkbox::new(
590 "checkbox_disabled_filled_unselected",
591 ToggleState::Unselected,
592 )
593 .fill()
594 .disabled(true),
595 ),
596 single_example(
597 "Indeterminate",
598 Checkbox::new(
599 "checkbox_disabled_filled_indeterminate",
600 ToggleState::Indeterminate,
601 )
602 .fill()
603 .disabled(true),
604 ),
605 single_example(
606 "Selected",
607 Checkbox::new("checkbox_disabled_filled_selected", ToggleState::Selected)
608 .fill()
609 .disabled(true),
610 ),
611 ],
612 ),
613 ]
614 }
615}
616
617impl ComponentPreview for Switch {
618 fn description() -> impl Into<Option<&'static str>> {
619 "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
620 }
621
622 fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
623 vec![
624 example_group_with_title(
625 "Default",
626 vec![
627 single_example(
628 "Off",
629 Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _cx| {}),
630 ),
631 single_example(
632 "On",
633 Switch::new("switch_on", ToggleState::Selected).on_click(|_, _cx| {}),
634 ),
635 ],
636 ),
637 example_group_with_title(
638 "Disabled",
639 vec![
640 single_example(
641 "Off",
642 Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
643 ),
644 single_example(
645 "On",
646 Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
647 ),
648 ],
649 ),
650 ]
651 }
652}
653
654impl ComponentPreview for CheckboxWithLabel {
655 fn description() -> impl Into<Option<&'static str>> {
656 "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
657 }
658
659 fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
660 vec![example_group(vec![
661 single_example(
662 "Unselected",
663 CheckboxWithLabel::new(
664 "checkbox_with_label_unselected",
665 Label::new("Always save on quit"),
666 ToggleState::Unselected,
667 |_, _| {},
668 ),
669 ),
670 single_example(
671 "Indeterminate",
672 CheckboxWithLabel::new(
673 "checkbox_with_label_indeterminate",
674 Label::new("Always save on quit"),
675 ToggleState::Indeterminate,
676 |_, _| {},
677 ),
678 ),
679 single_example(
680 "Selected",
681 CheckboxWithLabel::new(
682 "checkbox_with_label_selected",
683 Label::new("Always save on quit"),
684 ToggleState::Selected,
685 |_, _| {},
686 ),
687 ),
688 ])]
689 }
690}
691
692impl ComponentPreview for SwitchWithLabel {
693 fn description() -> impl Into<Option<&'static str>> {
694 "A switch with an associated label, allowing users to select an option while providing a descriptive text."
695 }
696
697 fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
698 vec![example_group(vec![
699 single_example(
700 "Off",
701 SwitchWithLabel::new(
702 "switch_with_label_unselected",
703 Label::new("Always save on quit"),
704 ToggleState::Unselected,
705 |_, _| {},
706 ),
707 ),
708 single_example(
709 "On",
710 SwitchWithLabel::new(
711 "switch_with_label_selected",
712 Label::new("Always save on quit"),
713 ToggleState::Selected,
714 |_, _| {},
715 ),
716 ),
717 ])]
718 }
719}