Add the `SwitchWithLabel` component (#22314)

Danilo Leal created

<img width="800" alt="Screenshot 2024-12-20 at 8 31 14 PM"
src="https://github.com/user-attachments/assets/1d7bd10a-0db3-41e4-9f59-977cc2ab137c"
/>

Release Notes:

- N/A

Change summary

crates/ui/src/components/toggle.rs    | 75 +++++++++++++++++++++++++++++
crates/workspace/src/theme_preview.rs | 15 +++--
2 files changed, 83 insertions(+), 7 deletions(-)

Detailed changes

crates/ui/src/components/toggle.rs 🔗

@@ -282,6 +282,52 @@ impl RenderOnce for Switch {
     }
 }
 
+/// A [`Switch`] that has a [`Label`].
+#[derive(IntoElement)]
+pub struct SwitchWithLabel {
+    id: ElementId,
+    label: Label,
+    checked: ToggleState,
+    on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
+}
+
+impl SwitchWithLabel {
+    pub fn new(
+        id: impl Into<ElementId>,
+        label: Label,
+        checked: ToggleState,
+        on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            label,
+            checked,
+            on_click: Arc::new(on_click),
+        }
+    }
+}
+
+impl RenderOnce for SwitchWithLabel {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        h_flex()
+            .gap(DynamicSpacing::Base08.rems(cx))
+            .child(Switch::new(self.id.clone(), self.checked).on_click({
+                let on_click = self.on_click.clone();
+                move |checked, cx| {
+                    (on_click)(checked, cx);
+                }
+            }))
+            .child(
+                div()
+                    .id(SharedString::from(format!("{}-label", self.id)))
+                    .on_click(move |_event, cx| {
+                        (self.on_click)(&self.checked.inverse(), cx);
+                    })
+                    .child(self.label),
+            )
+    }
+}
+
 impl ComponentPreview for Checkbox {
     fn description() -> impl Into<Option<&'static str>> {
         "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
@@ -407,3 +453,32 @@ impl ComponentPreview for CheckboxWithLabel {
         ])]
     }
 }
+
+impl ComponentPreview for SwitchWithLabel {
+    fn description() -> impl Into<Option<&'static str>> {
+        "A switch with an associated label, allowing users to select an option while providing a descriptive text."
+    }
+
+    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
+        vec![example_group(vec![
+            single_example(
+                "Off",
+                SwitchWithLabel::new(
+                    "switch_with_label_unselected",
+                    Label::new("Always save on quit"),
+                    ToggleState::Unselected,
+                    |_, _| {},
+                ),
+            ),
+            single_example(
+                "On",
+                SwitchWithLabel::new(
+                    "switch_with_label_selected",
+                    Label::new("Always save on quit"),
+                    ToggleState::Selected,
+                    |_, _| {},
+                ),
+            ),
+        ])]
+    }
+}

crates/workspace/src/theme_preview.rs 🔗

@@ -6,7 +6,7 @@ use ui::{
     element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
     Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
     Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile,
-    IconDecoration, Indicator, Switch, Table, TintColor, Tooltip,
+    IconDecoration, Indicator, Switch, SwitchWithLabel, Table, TintColor, Tooltip,
 };
 
 use crate::{Item, Workspace};
@@ -369,16 +369,17 @@ impl ThemePreview {
             .overflow_scroll()
             .size_full()
             .gap_2()
-            .child(Switch::render_component_previews(cx))
-            .child(ContentGroup::render_component_previews(cx))
-            .child(IconDecoration::render_component_previews(cx))
-            .child(DecoratedIcon::render_component_previews(cx))
+            .child(Button::render_component_previews(cx))
             .child(Checkbox::render_component_previews(cx))
             .child(CheckboxWithLabel::render_component_previews(cx))
+            .child(ContentGroup::render_component_previews(cx))
+            .child(DecoratedIcon::render_component_previews(cx))
             .child(Facepile::render_component_previews(cx))
-            .child(Button::render_component_previews(cx))
-            .child(Indicator::render_component_previews(cx))
             .child(Icon::render_component_previews(cx))
+            .child(IconDecoration::render_component_previews(cx))
+            .child(Indicator::render_component_previews(cx))
+            .child(Switch::render_component_previews(cx))
+            .child(SwitchWithLabel::render_component_previews(cx))
             .child(Table::render_component_previews(cx))
     }