toggle.rs

  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}