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