toggle.rs

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