toggle.rs

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