toggle.rs

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