1use gpui::{
2 AnyElement, AnyView, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, prelude::*,
3};
4use std::sync::Arc;
5
6use crate::utils::is_light;
7use crate::{Color, Icon, IconName, ToggleState};
8use crate::{ElevationIndex, KeyBinding, prelude::*};
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, RegisterComponent)]
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(_) => cx.theme().colors().border,
143 ToggleStyle::Custom(color) => color.opacity(0.3),
144 }
145 }
146
147 /// container size
148 pub fn container_size() -> Pixels {
149 px(20.0)
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 {
159 Color::Selected
160 };
161 let icon = match self.toggle_state {
162 ToggleState::Selected => {
163 if self.placeholder {
164 None
165 } else {
166 Some(
167 Icon::new(IconName::Check)
168 .size(IconSize::Small)
169 .color(color),
170 )
171 }
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 let hover_border_color = border_color.alpha(0.7);
182
183 let size = Self::container_size();
184
185 let checkbox = h_flex()
186 .id(self.id.clone())
187 .justify_center()
188 .items_center()
189 .size(size)
190 .group(group_id.clone())
191 .child(
192 div()
193 .flex()
194 .flex_none()
195 .justify_center()
196 .items_center()
197 .m_1()
198 .size_4()
199 .rounded_xs()
200 .bg(bg_color)
201 .border_1()
202 .border_color(border_color)
203 .when(self.disabled, |this| this.cursor_not_allowed())
204 .when(self.disabled, |this| {
205 this.bg(cx.theme().colors().element_disabled.opacity(0.6))
206 })
207 .when(!self.disabled, |this| {
208 this.group_hover(group_id.clone(), |el| el.border_color(hover_border_color))
209 })
210 .when(self.placeholder, |this| {
211 this.child(
212 div()
213 .flex_none()
214 .rounded_full()
215 .bg(color.color(cx).alpha(0.5))
216 .size(px(4.)),
217 )
218 })
219 .children(icon),
220 );
221
222 h_flex()
223 .id(self.id)
224 .gap(DynamicSpacing::Base06.rems(cx))
225 .child(checkbox)
226 .when_some(
227 self.on_click.filter(|_| !self.disabled),
228 |this, on_click| {
229 this.on_click(move |_, window, cx| {
230 on_click(&self.toggle_state.inverse(), window, cx)
231 })
232 },
233 )
234 // TODO: Allow label size to be different from default.
235 // TODO: Allow label color to be different from muted.
236 .when_some(self.label, |this, label| {
237 this.child(Label::new(label).color(Color::Muted))
238 })
239 .when_some(self.tooltip, |this, tooltip| {
240 this.tooltip(move |window, cx| tooltip(window, cx))
241 })
242 }
243}
244
245/// A [`Checkbox`] that has a [`Label`].
246#[derive(IntoElement, RegisterComponent)]
247pub struct CheckboxWithLabel {
248 id: ElementId,
249 label: Label,
250 checked: ToggleState,
251 on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
252 filled: bool,
253 style: ToggleStyle,
254 checkbox_position: IconPosition,
255}
256
257// TODO: Remove `CheckboxWithLabel` now that `label` is a method of `Checkbox`.
258impl CheckboxWithLabel {
259 /// Creates a checkbox with an attached label.
260 pub fn new(
261 id: impl Into<ElementId>,
262 label: Label,
263 checked: ToggleState,
264 on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
265 ) -> Self {
266 Self {
267 id: id.into(),
268 label,
269 checked,
270 on_click: Arc::new(on_click),
271 filled: false,
272 style: ToggleStyle::default(),
273 checkbox_position: IconPosition::Start,
274 }
275 }
276
277 /// Sets the style of the checkbox using the specified [`ToggleStyle`].
278 pub fn style(mut self, style: ToggleStyle) -> Self {
279 self.style = style;
280 self
281 }
282
283 /// Match the style of the checkbox to the current elevation using [`ToggleStyle::ElevationBased`].
284 pub fn elevation(mut self, elevation: ElevationIndex) -> Self {
285 self.style = ToggleStyle::ElevationBased(elevation);
286 self
287 }
288
289 /// Sets the `fill` setting of the checkbox, indicating whether it should be filled.
290 pub fn fill(mut self) -> Self {
291 self.filled = true;
292 self
293 }
294
295 pub fn checkbox_position(mut self, position: IconPosition) -> Self {
296 self.checkbox_position = position;
297 self
298 }
299}
300
301impl RenderOnce for CheckboxWithLabel {
302 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
303 h_flex()
304 .gap(DynamicSpacing::Base08.rems(cx))
305 .when(self.checkbox_position == IconPosition::Start, |this| {
306 this.child(
307 Checkbox::new(self.id.clone(), self.checked)
308 .style(self.style.clone())
309 .when(self.filled, Checkbox::fill)
310 .on_click({
311 let on_click = self.on_click.clone();
312 move |checked, window, cx| {
313 (on_click)(checked, window, cx);
314 }
315 }),
316 )
317 })
318 .child(
319 div()
320 .id(SharedString::from(format!("{}-label", self.id)))
321 .on_click({
322 let on_click = self.on_click.clone();
323 move |_event, window, cx| {
324 (on_click)(&self.checked.inverse(), window, cx);
325 }
326 })
327 .child(self.label),
328 )
329 .when(self.checkbox_position == IconPosition::End, |this| {
330 this.child(
331 Checkbox::new(self.id.clone(), self.checked)
332 .style(self.style)
333 .when(self.filled, Checkbox::fill)
334 .on_click(move |checked, window, cx| {
335 (self.on_click)(checked, window, cx);
336 }),
337 )
338 })
339 }
340}
341
342/// # Switch
343///
344/// Switches are used to represent opposite states, such as enabled or disabled.
345#[derive(IntoElement, RegisterComponent)]
346pub struct Switch {
347 id: ElementId,
348 toggle_state: ToggleState,
349 disabled: bool,
350 on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>,
351 label: Option<SharedString>,
352 key_binding: Option<KeyBinding>,
353}
354
355impl Switch {
356 /// Creates a new [`Switch`].
357 pub fn new(id: impl Into<ElementId>, state: ToggleState) -> Self {
358 Self {
359 id: id.into(),
360 toggle_state: state,
361 disabled: false,
362 on_click: None,
363 label: None,
364 key_binding: None,
365 }
366 }
367
368 /// Sets the disabled state of the [`Switch`].
369 pub fn disabled(mut self, disabled: bool) -> Self {
370 self.disabled = disabled;
371 self
372 }
373
374 /// Binds a handler to the [`Switch`] that will be called when clicked.
375 pub fn on_click(
376 mut self,
377 handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
378 ) -> Self {
379 self.on_click = Some(Box::new(handler));
380 self
381 }
382
383 /// Sets the label of the [`Switch`].
384 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
385 self.label = Some(label.into());
386 self
387 }
388
389 /// Display the keybinding that triggers the switch action.
390 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
391 self.key_binding = key_binding.into();
392 self
393 }
394}
395
396impl RenderOnce for Switch {
397 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
398 let is_on = self.toggle_state == ToggleState::Selected;
399 let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 };
400 let base_color = cx.theme().colors().text;
401
402 let bg_color = if is_on {
403 cx.theme()
404 .colors()
405 .element_background
406 .blend(base_color.opacity(0.08))
407 } else {
408 cx.theme().colors().element_background
409 };
410 let thumb_color = base_color.opacity(0.8);
411 let thumb_hover_color = base_color;
412 let border_color = cx.theme().colors().border_variant;
413 // Lighter themes need higher contrast borders
414 let border_hover_color = if is_on {
415 border_color.blend(base_color.opacity(0.16 * adjust_ratio))
416 } else {
417 border_color.blend(base_color.opacity(0.05 * adjust_ratio))
418 };
419 let thumb_opacity = match (is_on, self.disabled) {
420 (_, true) => 0.2,
421 (true, false) => 1.0,
422 (false, false) => 0.5,
423 };
424
425 let group_id = format!("switch_group_{:?}", self.id);
426
427 let switch = h_flex()
428 .w(DynamicSpacing::Base32.rems(cx))
429 .h(DynamicSpacing::Base20.rems(cx))
430 .group(group_id.clone())
431 .child(
432 h_flex()
433 .when(is_on, |on| on.justify_end())
434 .when(!is_on, |off| off.justify_start())
435 .items_center()
436 .size_full()
437 .rounded_full()
438 .px(DynamicSpacing::Base02.px(cx))
439 .bg(bg_color)
440 .border_1()
441 .border_color(border_color)
442 .when(!self.disabled, |this| {
443 this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color))
444 })
445 .child(
446 div()
447 .size(DynamicSpacing::Base12.rems(cx))
448 .rounded_full()
449 .bg(thumb_color)
450 .when(!self.disabled, |this| {
451 this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color))
452 })
453 .opacity(thumb_opacity),
454 ),
455 );
456
457 h_flex()
458 .id(self.id)
459 .gap(DynamicSpacing::Base06.rems(cx))
460 .cursor_pointer()
461 .child(switch)
462 .when_some(
463 self.on_click.filter(|_| !self.disabled),
464 |this, on_click| {
465 this.on_click(move |_, window, cx| {
466 on_click(&self.toggle_state.inverse(), window, cx)
467 })
468 },
469 )
470 .when_some(self.label, |this, label| {
471 this.child(Label::new(label).size(LabelSize::Small))
472 })
473 .children(self.key_binding)
474 }
475}
476
477/// A [`Switch`] that has a [`Label`].
478#[derive(IntoElement)]
479pub struct SwitchWithLabel {
480 id: ElementId,
481 label: Label,
482 toggle_state: ToggleState,
483 on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
484 disabled: bool,
485}
486
487impl SwitchWithLabel {
488 /// Creates a switch with an attached label.
489 pub fn new(
490 id: impl Into<ElementId>,
491 label: Label,
492 toggle_state: impl Into<ToggleState>,
493 on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
494 ) -> Self {
495 Self {
496 id: id.into(),
497 label,
498 toggle_state: toggle_state.into(),
499 on_click: Arc::new(on_click),
500 disabled: false,
501 }
502 }
503
504 /// Sets the disabled state of the [`SwitchWithLabel`].
505 pub fn disabled(mut self, disabled: bool) -> Self {
506 self.disabled = disabled;
507 self
508 }
509}
510
511impl RenderOnce for SwitchWithLabel {
512 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
513 h_flex()
514 .id(SharedString::from(format!("{}-container", self.id)))
515 .gap(DynamicSpacing::Base08.rems(cx))
516 .child(
517 Switch::new(self.id.clone(), self.toggle_state)
518 .disabled(self.disabled)
519 .on_click({
520 let on_click = self.on_click.clone();
521 move |checked, window, cx| {
522 (on_click)(checked, window, cx);
523 }
524 }),
525 )
526 .child(
527 div()
528 .id(SharedString::from(format!("{}-label", self.id)))
529 .child(self.label),
530 )
531 }
532}
533
534impl Component for Checkbox {
535 fn scope() -> ComponentScope {
536 ComponentScope::Input
537 }
538
539 fn description() -> Option<&'static str> {
540 Some("A checkbox component that can be used for multiple choice selections")
541 }
542
543 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
544 Some(
545 v_flex()
546 .gap_6()
547 .children(vec![
548 example_group_with_title(
549 "States",
550 vec![
551 single_example(
552 "Unselected",
553 Checkbox::new("checkbox_unselected", ToggleState::Unselected)
554 .into_any_element(),
555 ),
556 single_example(
557 "Placeholder",
558 Checkbox::new("checkbox_indeterminate", ToggleState::Selected)
559 .placeholder(true)
560 .into_any_element(),
561 ),
562 single_example(
563 "Indeterminate",
564 Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate)
565 .into_any_element(),
566 ),
567 single_example(
568 "Selected",
569 Checkbox::new("checkbox_selected", ToggleState::Selected)
570 .into_any_element(),
571 ),
572 ],
573 ),
574 example_group_with_title(
575 "Styles",
576 vec![
577 single_example(
578 "Default",
579 Checkbox::new("checkbox_default", ToggleState::Selected)
580 .into_any_element(),
581 ),
582 single_example(
583 "Filled",
584 Checkbox::new("checkbox_filled", ToggleState::Selected)
585 .fill()
586 .into_any_element(),
587 ),
588 single_example(
589 "ElevationBased",
590 Checkbox::new("checkbox_elevation", ToggleState::Selected)
591 .style(ToggleStyle::ElevationBased(
592 ElevationIndex::EditorSurface,
593 ))
594 .into_any_element(),
595 ),
596 single_example(
597 "Custom Color",
598 Checkbox::new("checkbox_custom", ToggleState::Selected)
599 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7)))
600 .into_any_element(),
601 ),
602 ],
603 ),
604 example_group_with_title(
605 "Disabled",
606 vec![
607 single_example(
608 "Unselected",
609 Checkbox::new(
610 "checkbox_disabled_unselected",
611 ToggleState::Unselected,
612 )
613 .disabled(true)
614 .into_any_element(),
615 ),
616 single_example(
617 "Selected",
618 Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
619 .disabled(true)
620 .into_any_element(),
621 ),
622 ],
623 ),
624 example_group_with_title(
625 "With Label",
626 vec![single_example(
627 "Default",
628 Checkbox::new("checkbox_with_label", ToggleState::Selected)
629 .label("Always save on quit")
630 .into_any_element(),
631 )],
632 ),
633 ])
634 .into_any_element(),
635 )
636 }
637}
638
639impl Component for Switch {
640 fn scope() -> ComponentScope {
641 ComponentScope::Input
642 }
643
644 fn description() -> Option<&'static str> {
645 Some("A switch component that represents binary states like on/off")
646 }
647
648 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
649 Some(
650 v_flex()
651 .gap_6()
652 .children(vec![
653 example_group_with_title(
654 "States",
655 vec![
656 single_example(
657 "Off",
658 Switch::new("switch_off", ToggleState::Unselected)
659 .on_click(|_, _, _cx| {})
660 .into_any_element(),
661 ),
662 single_example(
663 "On",
664 Switch::new("switch_on", ToggleState::Selected)
665 .on_click(|_, _, _cx| {})
666 .into_any_element(),
667 ),
668 ],
669 ),
670 example_group_with_title(
671 "Disabled",
672 vec![
673 single_example(
674 "Off",
675 Switch::new("switch_disabled_off", ToggleState::Unselected)
676 .disabled(true)
677 .into_any_element(),
678 ),
679 single_example(
680 "On",
681 Switch::new("switch_disabled_on", ToggleState::Selected)
682 .disabled(true)
683 .into_any_element(),
684 ),
685 ],
686 ),
687 example_group_with_title(
688 "With Label",
689 vec![
690 single_example(
691 "Label",
692 Switch::new("switch_with_label", ToggleState::Selected)
693 .label("Always save on quit")
694 .into_any_element(),
695 ),
696 // TODO: Where did theme_preview_keybinding go?
697 // single_example(
698 // "Keybinding",
699 // Switch::new("switch_with_keybinding", ToggleState::Selected)
700 // .key_binding(theme_preview_keybinding("cmd-shift-e"))
701 // .into_any_element(),
702 // ),
703 ],
704 ),
705 ])
706 .into_any_element(),
707 )
708 }
709}
710
711impl Component for CheckboxWithLabel {
712 fn scope() -> ComponentScope {
713 ComponentScope::Input
714 }
715
716 fn description() -> Option<&'static str> {
717 Some("A checkbox component with an attached label")
718 }
719
720 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
721 Some(
722 v_flex()
723 .gap_6()
724 .children(vec![example_group_with_title(
725 "States",
726 vec![
727 single_example(
728 "Unselected",
729 CheckboxWithLabel::new(
730 "checkbox_with_label_unselected",
731 Label::new("Always save on quit"),
732 ToggleState::Unselected,
733 |_, _, _| {},
734 )
735 .into_any_element(),
736 ),
737 single_example(
738 "Indeterminate",
739 CheckboxWithLabel::new(
740 "checkbox_with_label_indeterminate",
741 Label::new("Always save on quit"),
742 ToggleState::Indeterminate,
743 |_, _, _| {},
744 )
745 .into_any_element(),
746 ),
747 single_example(
748 "Selected",
749 CheckboxWithLabel::new(
750 "checkbox_with_label_selected",
751 Label::new("Always save on quit"),
752 ToggleState::Selected,
753 |_, _, _| {},
754 )
755 .into_any_element(),
756 ),
757 ],
758 )])
759 .into_any_element(),
760 )
761 }
762}