ui: Add the `SwitchField` component (#34713)

Danilo Leal created

This will be useful for both the current agent panel and some other
onboarding stuff we're working on. Also ended up removing the
`SwitchWithLabel` as it was unused.

Release Notes:

- N/A

Change summary

crates/ui/src/components/toggle.rs | 166 +++++++++++++++++++++++++++----
1 file changed, 142 insertions(+), 24 deletions(-)

Detailed changes

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

@@ -543,28 +543,48 @@ impl RenderOnce for Switch {
     }
 }
 
-/// A [`Switch`] that has a [`Label`].
-#[derive(IntoElement)]
-pub struct SwitchWithLabel {
+/// # SwitchField
+///
+/// A field component that combines a label, description, and switch into one reusable component.
+///
+/// # Examples
+///
+/// ```
+/// use ui::prelude::*;
+///
+/// SwitchField::new(
+///     "feature-toggle",
+///     "Enable feature",
+///     "This feature adds new functionality to the app.",
+///     ToggleState::Unselected,
+///     |state, window, cx| {
+///         // Logic here
+///     }
+/// );
+/// ```
+#[derive(IntoElement, RegisterComponent)]
+pub struct SwitchField {
     id: ElementId,
-    label: Label,
+    label: SharedString,
+    description: SharedString,
     toggle_state: ToggleState,
     on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
     disabled: bool,
     color: SwitchColor,
 }
 
-impl SwitchWithLabel {
-    /// Creates a switch with an attached label.
+impl SwitchField {
     pub fn new(
         id: impl Into<ElementId>,
-        label: Label,
+        label: impl Into<SharedString>,
+        description: impl Into<SharedString>,
         toggle_state: impl Into<ToggleState>,
         on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
     ) -> Self {
         Self {
             id: id.into(),
-            label,
+            label: label.into(),
+            description: description.into(),
             toggle_state: toggle_state.into(),
             on_click: Arc::new(on_click),
             disabled: false,
@@ -572,43 +592,141 @@ impl SwitchWithLabel {
         }
     }
 
-    /// Sets the disabled state of the [`SwitchWithLabel`].
     pub fn disabled(mut self, disabled: bool) -> Self {
         self.disabled = disabled;
         self
     }
 
     /// Sets the color of the switch using the specified [`SwitchColor`].
+    /// This changes the color scheme of the switch when it's in the "on" state.
     pub fn color(mut self, color: SwitchColor) -> Self {
         self.color = color;
         self
     }
 }
 
-impl RenderOnce for SwitchWithLabel {
-    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+impl RenderOnce for SwitchField {
+    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
         h_flex()
             .id(SharedString::from(format!("{}-container", self.id)))
-            .gap(DynamicSpacing::Base08.rems(cx))
+            .w_full()
+            .gap_4()
+            .justify_between()
+            .flex_wrap()
             .child(
-                Switch::new(self.id.clone(), self.toggle_state)
-                    .disabled(self.disabled)
-                    .color(self.color)
-                    .on_click({
-                        let on_click = self.on_click.clone();
-                        move |checked, window, cx| {
-                            (on_click)(checked, window, cx);
-                        }
-                    }),
+                v_flex()
+                    .gap_0p5()
+                    .max_w_5_6()
+                    .child(Label::new(self.label))
+                    .child(Label::new(self.description).color(Color::Muted)),
             )
             .child(
-                div()
-                    .id(SharedString::from(format!("{}-label", self.id)))
-                    .child(self.label),
+                Switch::new(
+                    SharedString::from(format!("{}-switch", self.id)),
+                    self.toggle_state,
+                )
+                .color(self.color)
+                .disabled(self.disabled)
+                .on_click({
+                    let on_click = self.on_click.clone();
+                    move |state, window, cx| {
+                        (on_click)(state, window, cx);
+                    }
+                }),
             )
     }
 }
 
+impl Component for SwitchField {
+    fn scope() -> ComponentScope {
+        ComponentScope::Input
+    }
+
+    fn description() -> Option<&'static str> {
+        Some("A field component that combines a label, description, and switch")
+    }
+
+    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+        Some(
+            v_flex()
+                .gap_6()
+                .children(vec![
+                    example_group_with_title(
+                        "States",
+                        vec![
+                            single_example(
+                                "Unselected",
+                                SwitchField::new(
+                                    "switch_field_unselected",
+                                    "Enable notifications",
+                                    "Receive notifications when new messages arrive.",
+                                    ToggleState::Unselected,
+                                    |_, _, _| {},
+                                )
+                                .into_any_element(),
+                            ),
+                            single_example(
+                                "Selected",
+                                SwitchField::new(
+                                    "switch_field_selected",
+                                    "Enable notifications",
+                                    "Receive notifications when new messages arrive.",
+                                    ToggleState::Selected,
+                                    |_, _, _| {},
+                                )
+                                .into_any_element(),
+                            ),
+                        ],
+                    ),
+                    example_group_with_title(
+                        "Colors",
+                        vec![
+                            single_example(
+                                "Default",
+                                SwitchField::new(
+                                    "switch_field_default",
+                                    "Default color",
+                                    "This uses the default switch color.",
+                                    ToggleState::Selected,
+                                    |_, _, _| {},
+                                )
+                                .into_any_element(),
+                            ),
+                            single_example(
+                                "Accent",
+                                SwitchField::new(
+                                    "switch_field_accent",
+                                    "Accent color",
+                                    "This uses the accent color scheme.",
+                                    ToggleState::Selected,
+                                    |_, _, _| {},
+                                )
+                                .color(SwitchColor::Accent)
+                                .into_any_element(),
+                            ),
+                        ],
+                    ),
+                    example_group_with_title(
+                        "Disabled",
+                        vec![single_example(
+                            "Disabled",
+                            SwitchField::new(
+                                "switch_field_disabled",
+                                "Disabled field",
+                                "This field is disabled and cannot be toggled.",
+                                ToggleState::Selected,
+                                |_, _, _| {},
+                            )
+                            .disabled(true)
+                            .into_any_element(),
+                        )],
+                    ),
+                ])
+                .into_any_element(),
+        )
+    }
+}
+
 impl Component for Checkbox {
     fn scope() -> ComponentScope {
         ComponentScope::Input