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, 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 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 label: Option<SharedString>,
277 key_binding: Option<KeyBinding>,
278}
279
280impl Switch {
281 /// Creates a new [`Switch`].
282 pub fn new(id: impl Into<ElementId>, state: ToggleState) -> Self {
283 Self {
284 id: id.into(),
285 toggle_state: state,
286 disabled: false,
287 on_click: None,
288 label: None,
289 key_binding: None,
290 }
291 }
292
293 /// Sets the disabled state of the [`Switch`].
294 pub fn disabled(mut self, disabled: bool) -> Self {
295 self.disabled = disabled;
296 self
297 }
298
299 /// Binds a handler to the [`Switch`] that will be called when clicked.
300 pub fn on_click(
301 mut self,
302 handler: impl Fn(&ToggleState, &mut WindowContext) + 'static,
303 ) -> Self {
304 self.on_click = Some(Box::new(handler));
305 self
306 }
307
308 /// Sets the label of the [`Switch`].
309 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
310 self.label = Some(label.into());
311 self
312 }
313
314 /// Display the keybinding that triggers the switch action.
315 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
316 self.key_binding = key_binding.into();
317 self
318 }
319}
320
321impl RenderOnce for Switch {
322 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
323 let is_on = self.toggle_state == ToggleState::Selected;
324 let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 };
325 let base_color = cx.theme().colors().text;
326
327 let bg_color = if is_on {
328 cx.theme()
329 .colors()
330 .element_background
331 .blend(base_color.opacity(0.08))
332 } else {
333 cx.theme().colors().element_background
334 };
335 let thumb_color = base_color.opacity(0.8);
336 let thumb_hover_color = base_color;
337 let border_color = cx.theme().colors().border_variant;
338 // Lighter themes need higher contrast borders
339 let border_hover_color = if is_on {
340 border_color.blend(base_color.opacity(0.16 * adjust_ratio))
341 } else {
342 border_color.blend(base_color.opacity(0.05 * adjust_ratio))
343 };
344 let thumb_opacity = match (is_on, self.disabled) {
345 (_, true) => 0.2,
346 (true, false) => 1.0,
347 (false, false) => 0.5,
348 };
349
350 let group_id = format!("switch_group_{:?}", self.id);
351
352 let switch = h_flex()
353 .w(DynamicSpacing::Base32.rems(cx))
354 .h(DynamicSpacing::Base20.rems(cx))
355 .group(group_id.clone())
356 .child(
357 h_flex()
358 .when(is_on, |on| on.justify_end())
359 .when(!is_on, |off| off.justify_start())
360 .items_center()
361 .size_full()
362 .rounded_full()
363 .px(DynamicSpacing::Base02.px(cx))
364 .bg(bg_color)
365 .border_1()
366 .border_color(border_color)
367 .when(!self.disabled, |this| {
368 this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color))
369 })
370 .child(
371 div()
372 .size(DynamicSpacing::Base12.rems(cx))
373 .rounded_full()
374 .bg(thumb_color)
375 .when(!self.disabled, |this| {
376 this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color))
377 })
378 .opacity(thumb_opacity),
379 ),
380 );
381
382 h_flex()
383 .id(self.id)
384 .gap(DynamicSpacing::Base06.rems(cx))
385 .child(switch)
386 .when_some(
387 self.on_click.filter(|_| !self.disabled),
388 |this, on_click| {
389 this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
390 },
391 )
392 .when_some(self.label, |this, label| {
393 this.child(Label::new(label).size(LabelSize::Small))
394 })
395 .children(self.key_binding)
396 }
397}
398
399impl ComponentPreview for Checkbox {
400 fn description() -> impl Into<Option<&'static str>> {
401 "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
402 }
403
404 fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
405 vec![
406 example_group_with_title(
407 "Default",
408 vec![
409 single_example(
410 "Unselected",
411 Checkbox::new("checkbox_unselected", ToggleState::Unselected),
412 ),
413 single_example(
414 "Indeterminate",
415 Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
416 ),
417 single_example(
418 "Selected",
419 Checkbox::new("checkbox_selected", ToggleState::Selected),
420 ),
421 ],
422 ),
423 example_group_with_title(
424 "Default (Filled)",
425 vec![
426 single_example(
427 "Unselected",
428 Checkbox::new("checkbox_unselected", ToggleState::Unselected).fill(),
429 ),
430 single_example(
431 "Indeterminate",
432 Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate).fill(),
433 ),
434 single_example(
435 "Selected",
436 Checkbox::new("checkbox_selected", ToggleState::Selected).fill(),
437 ),
438 ],
439 ),
440 example_group_with_title(
441 "ElevationBased",
442 vec![
443 single_example(
444 "Unselected",
445 Checkbox::new("checkbox_unfilled_unselected", ToggleState::Unselected)
446 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
447 ),
448 single_example(
449 "Indeterminate",
450 Checkbox::new(
451 "checkbox_unfilled_indeterminate",
452 ToggleState::Indeterminate,
453 )
454 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
455 ),
456 single_example(
457 "Selected",
458 Checkbox::new("checkbox_unfilled_selected", ToggleState::Selected)
459 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
460 ),
461 ],
462 ),
463 example_group_with_title(
464 "ElevationBased (Filled)",
465 vec![
466 single_example(
467 "Unselected",
468 Checkbox::new("checkbox_filled_unselected", ToggleState::Unselected)
469 .fill()
470 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
471 ),
472 single_example(
473 "Indeterminate",
474 Checkbox::new("checkbox_filled_indeterminate", ToggleState::Indeterminate)
475 .fill()
476 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
477 ),
478 single_example(
479 "Selected",
480 Checkbox::new("checkbox_filled_selected", ToggleState::Selected)
481 .fill()
482 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
483 ),
484 ],
485 ),
486 example_group_with_title(
487 "Custom Color",
488 vec![
489 single_example(
490 "Unselected",
491 Checkbox::new("checkbox_custom_unselected", ToggleState::Unselected)
492 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
493 ),
494 single_example(
495 "Indeterminate",
496 Checkbox::new("checkbox_custom_indeterminate", ToggleState::Indeterminate)
497 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
498 ),
499 single_example(
500 "Selected",
501 Checkbox::new("checkbox_custom_selected", ToggleState::Selected)
502 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
503 ),
504 ],
505 ),
506 example_group_with_title(
507 "Custom Color (Filled)",
508 vec![
509 single_example(
510 "Unselected",
511 Checkbox::new("checkbox_custom_filled_unselected", ToggleState::Unselected)
512 .fill()
513 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
514 ),
515 single_example(
516 "Indeterminate",
517 Checkbox::new(
518 "checkbox_custom_filled_indeterminate",
519 ToggleState::Indeterminate,
520 )
521 .fill()
522 .style(ToggleStyle::Custom(hsla(
523 142.0 / 360.,
524 0.68,
525 0.45,
526 0.7,
527 ))),
528 ),
529 single_example(
530 "Selected",
531 Checkbox::new("checkbox_custom_filled_selected", ToggleState::Selected)
532 .fill()
533 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
534 ),
535 ],
536 ),
537 example_group_with_title(
538 "Disabled",
539 vec![
540 single_example(
541 "Unselected",
542 Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
543 .disabled(true),
544 ),
545 single_example(
546 "Indeterminate",
547 Checkbox::new(
548 "checkbox_disabled_indeterminate",
549 ToggleState::Indeterminate,
550 )
551 .disabled(true),
552 ),
553 single_example(
554 "Selected",
555 Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
556 .disabled(true),
557 ),
558 ],
559 ),
560 example_group_with_title(
561 "Disabled (Filled)",
562 vec![
563 single_example(
564 "Unselected",
565 Checkbox::new(
566 "checkbox_disabled_filled_unselected",
567 ToggleState::Unselected,
568 )
569 .fill()
570 .disabled(true),
571 ),
572 single_example(
573 "Indeterminate",
574 Checkbox::new(
575 "checkbox_disabled_filled_indeterminate",
576 ToggleState::Indeterminate,
577 )
578 .fill()
579 .disabled(true),
580 ),
581 single_example(
582 "Selected",
583 Checkbox::new("checkbox_disabled_filled_selected", ToggleState::Selected)
584 .fill()
585 .disabled(true),
586 ),
587 ],
588 ),
589 ]
590 }
591}
592
593impl ComponentPreview for Switch {
594 fn description() -> impl Into<Option<&'static str>> {
595 "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
596 }
597
598 fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
599 vec![
600 example_group_with_title(
601 "Default",
602 vec![
603 single_example(
604 "Off",
605 Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _cx| {}),
606 ),
607 single_example(
608 "On",
609 Switch::new("switch_on", ToggleState::Selected).on_click(|_, _cx| {}),
610 ),
611 ],
612 ),
613 example_group_with_title(
614 "Disabled",
615 vec![
616 single_example(
617 "Off",
618 Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
619 ),
620 single_example(
621 "On",
622 Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
623 ),
624 ],
625 ),
626 example_group_with_title(
627 "Label Permutations",
628 vec![
629 single_example(
630 "Label",
631 Switch::new("switch_with_label", ToggleState::Selected)
632 .label("Always save on quit"),
633 ),
634 single_example(
635 "Keybinding",
636 Switch::new("switch_with_label", ToggleState::Selected)
637 .key_binding(theme_preview_keybinding("cmd-shift-e")),
638 ),
639 ],
640 ),
641 ]
642 }
643}
644
645impl ComponentPreview for CheckboxWithLabel {
646 fn description() -> impl Into<Option<&'static str>> {
647 "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
648 }
649
650 fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
651 vec![example_group(vec![
652 single_example(
653 "Unselected",
654 CheckboxWithLabel::new(
655 "checkbox_with_label_unselected",
656 Label::new("Always save on quit"),
657 ToggleState::Unselected,
658 |_, _| {},
659 ),
660 ),
661 single_example(
662 "Indeterminate",
663 CheckboxWithLabel::new(
664 "checkbox_with_label_indeterminate",
665 Label::new("Always save on quit"),
666 ToggleState::Indeterminate,
667 |_, _| {},
668 ),
669 ),
670 single_example(
671 "Selected",
672 CheckboxWithLabel::new(
673 "checkbox_with_label_selected",
674 Label::new("Always save on quit"),
675 ToggleState::Selected,
676 |_, _| {},
677 ),
678 ),
679 ])]
680 }
681}