Add preview for Checkbox with Label (#20448)

Nate Butler created

Add previews for Checkbox with Label.

Merge checkbox components.

Release Notes:

- N/A

Change summary

crates/storybook/src/story_selector.rs                   |   2 
crates/ui/src/components/checkbox.rs                     |  89 +++++++
crates/ui/src/components/checkbox/checkbox.rs            | 114 ----------
crates/ui/src/components/checkbox/checkbox_with_label.rs |  51 ----
crates/ui/src/components/stories.rs                      |   2 
crates/ui/src/components/stories/checkbox.rs             |  47 ----
crates/ui/src/traits/component_preview.rs                |   4 
crates/workspace/src/theme_preview.rs                    |   3 
8 files changed, 90 insertions(+), 222 deletions(-)

Detailed changes

crates/storybook/src/story_selector.rs 🔗

@@ -16,7 +16,6 @@ pub enum ComponentStory {
     AutoHeightEditor,
     Avatar,
     Button,
-    Checkbox,
     CollabNotification,
     ContextMenu,
     Cursor,
@@ -52,7 +51,6 @@ impl ComponentStory {
             Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(),
             Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(),
             Self::Button => cx.new_view(|_| ui::ButtonStory).into(),
-            Self::Checkbox => cx.new_view(|_| ui::CheckboxStory).into(),
             Self::CollabNotification => cx
                 .new_view(|_| collab_ui::notifications::CollabNotificationStory)
                 .into(),

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

@@ -1,7 +1,4 @@
 #![allow(missing_docs)]
-mod checkbox_with_label;
-
-pub use checkbox_with_label::*;
 
 use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
 
@@ -163,3 +160,89 @@ impl ComponentPreview for Checkbox {
         ]
     }
 }
+
+use std::sync::Arc;
+
+/// A [`Checkbox`] that has a [`Label`].
+#[derive(IntoElement)]
+pub struct CheckboxWithLabel {
+    id: ElementId,
+    label: Label,
+    checked: Selection,
+    on_click: Arc<dyn Fn(&Selection, &mut WindowContext) + 'static>,
+}
+
+impl CheckboxWithLabel {
+    pub fn new(
+        id: impl Into<ElementId>,
+        label: Label,
+        checked: Selection,
+        on_click: impl Fn(&Selection, &mut WindowContext) + 'static,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            label,
+            checked,
+            on_click: Arc::new(on_click),
+        }
+    }
+}
+
+impl RenderOnce for CheckboxWithLabel {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        h_flex()
+            .gap(Spacing::Large.rems(cx))
+            .child(Checkbox::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 CheckboxWithLabel {
+    fn description() -> impl Into<Option<&'static str>> {
+        "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
+    }
+
+    fn examples() -> Vec<ComponentExampleGroup<Self>> {
+        vec![example_group(vec![
+            single_example(
+                "Unselected",
+                CheckboxWithLabel::new(
+                    "checkbox_with_label_unselected",
+                    Label::new("Always save on quit"),
+                    Selection::Unselected,
+                    |_, _| {},
+                ),
+            ),
+            single_example(
+                "Indeterminate",
+                CheckboxWithLabel::new(
+                    "checkbox_with_label_indeterminate",
+                    Label::new("Always save on quit"),
+                    Selection::Indeterminate,
+                    |_, _| {},
+                ),
+            ),
+            single_example(
+                "Selected",
+                CheckboxWithLabel::new(
+                    "checkbox_with_label_selected",
+                    Label::new("Always save on quit"),
+                    Selection::Selected,
+                    |_, _| {},
+                ),
+            ),
+        ])]
+    }
+}

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

@@ -1,114 +0,0 @@
-#![allow(missing_docs)]
-
-use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
-
-use crate::prelude::*;
-use crate::{Color, Icon, IconName, Selection};
-
-/// # Checkbox
-///
-/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
-/// Each checkbox works independently from other checkboxes in the list,
-/// therefore checking an additional box does not affect any other selections.
-#[derive(IntoElement)]
-pub struct Checkbox {
-    id: ElementId,
-    checked: Selection,
-    disabled: bool,
-    on_click: Option<Box<dyn Fn(&Selection, &mut WindowContext) + 'static>>,
-}
-
-impl Checkbox {
-    pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
-        Self {
-            id: id.into(),
-            checked,
-            disabled: false,
-            on_click: None,
-        }
-    }
-
-    pub fn disabled(mut self, disabled: bool) -> Self {
-        self.disabled = disabled;
-        self
-    }
-
-    pub fn on_click(mut self, handler: impl Fn(&Selection, &mut WindowContext) + 'static) -> Self {
-        self.on_click = Some(Box::new(handler));
-        self
-    }
-}
-
-impl RenderOnce for Checkbox {
-    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        let group_id = format!("checkbox_group_{:?}", self.id);
-
-        let icon = match self.checked {
-            Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
-                if self.disabled {
-                    Color::Disabled
-                } else {
-                    Color::Selected
-                },
-            )),
-            Selection::Indeterminate => Some(
-                Icon::new(IconName::Dash)
-                    .size(IconSize::Small)
-                    .color(if self.disabled {
-                        Color::Disabled
-                    } else {
-                        Color::Selected
-                    }),
-            ),
-            Selection::Unselected => None,
-        };
-
-        let selected =
-            self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
-
-        let (bg_color, border_color) = match (self.disabled, selected) {
-            (true, _) => (
-                cx.theme().colors().ghost_element_disabled,
-                cx.theme().colors().border_disabled,
-            ),
-            (false, true) => (
-                cx.theme().colors().element_selected,
-                cx.theme().colors().border,
-            ),
-            (false, false) => (
-                cx.theme().colors().element_background,
-                cx.theme().colors().border,
-            ),
-        };
-
-        h_flex()
-            .id(self.id)
-            .justify_center()
-            .items_center()
-            .size(crate::styles::custom_spacing(cx, 20.))
-            .group(group_id.clone())
-            .child(
-                div()
-                    .flex()
-                    .flex_none()
-                    .justify_center()
-                    .items_center()
-                    .m(Spacing::Small.px(cx))
-                    .size(crate::styles::custom_spacing(cx, 16.))
-                    .rounded_sm()
-                    .bg(bg_color)
-                    .border_1()
-                    .border_color(border_color)
-                    .when(!self.disabled, |this| {
-                        this.group_hover(group_id.clone(), |el| {
-                            el.bg(cx.theme().colors().element_hover)
-                        })
-                    })
-                    .children(icon),
-            )
-            .when_some(
-                self.on_click.filter(|_| !self.disabled),
-                |this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
-            )
-    }
-}

crates/ui/src/components/checkbox/checkbox_with_label.rs 🔗

@@ -1,51 +0,0 @@
-#![allow(missing_docs)]
-
-use std::sync::Arc;
-
-use crate::{prelude::*, Checkbox};
-
-/// A [`Checkbox`] that has a [`Label`].
-#[derive(IntoElement)]
-pub struct CheckboxWithLabel {
-    id: ElementId,
-    label: Label,
-    checked: Selection,
-    on_click: Arc<dyn Fn(&Selection, &mut WindowContext) + 'static>,
-}
-
-impl CheckboxWithLabel {
-    pub fn new(
-        id: impl Into<ElementId>,
-        label: Label,
-        checked: Selection,
-        on_click: impl Fn(&Selection, &mut WindowContext) + 'static,
-    ) -> Self {
-        Self {
-            id: id.into(),
-            label,
-            checked,
-            on_click: Arc::new(on_click),
-        }
-    }
-}
-
-impl RenderOnce for CheckboxWithLabel {
-    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        h_flex()
-            .gap(Spacing::Large.rems(cx))
-            .child(Checkbox::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),
-            )
-    }
-}

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

@@ -3,7 +3,6 @@
 #![allow(missing_docs)]
 mod avatar;
 mod button;
-mod checkbox;
 mod context_menu;
 mod disclosure;
 mod icon;
@@ -20,7 +19,6 @@ mod tool_strip;
 
 pub use avatar::*;
 pub use button::*;
-pub use checkbox::*;
 pub use context_menu::*;
 pub use disclosure::*;
 pub use icon::*;

crates/ui/src/components/stories/checkbox.rs 🔗

@@ -1,47 +0,0 @@
-use gpui::{Render, ViewContext};
-use story::Story;
-
-use crate::prelude::*;
-use crate::{h_flex, Checkbox};
-
-pub struct CheckboxStory;
-
-impl Render for CheckboxStory {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        Story::container()
-            .child(Story::title_for::<Checkbox>())
-            .child(Story::label("Default"))
-            .child(
-                h_flex()
-                    .p_2()
-                    .gap_2()
-                    .rounded_md()
-                    .border_1()
-                    .border_color(cx.theme().colors().border)
-                    .child(Checkbox::new("checkbox-enabled", Selection::Unselected))
-                    .child(Checkbox::new(
-                        "checkbox-intermediate",
-                        Selection::Indeterminate,
-                    ))
-                    .child(Checkbox::new("checkbox-selected", Selection::Selected)),
-            )
-            .child(Story::label("Disabled"))
-            .child(
-                h_flex()
-                    .p_2()
-                    .gap_2()
-                    .rounded_md()
-                    .border_1()
-                    .border_color(cx.theme().colors().border)
-                    .child(Checkbox::new("checkbox-disabled", Selection::Unselected).disabled(true))
-                    .child(
-                        Checkbox::new("checkbox-disabled-intermediate", Selection::Indeterminate)
-                            .disabled(true),
-                    )
-                    .child(
-                        Checkbox::new("checkbox-disabled-selected", Selection::Selected)
-                            .disabled(true),
-                    ),
-            )
-    }
-}

crates/ui/src/traits/component_preview.rs 🔗

@@ -9,9 +9,9 @@ pub enum ExampleLabelSide {
     Left,
     /// Right side
     Right,
+    #[default]
     /// Top side
     Top,
-    #[default]
     /// Bottom side
     Bottom,
 }
@@ -81,7 +81,7 @@ pub trait ComponentPreview: IntoElement {
         v_flex()
             .gap_2()
             .when_some(group.title, |this, title| {
-                this.child(Headline::new(title).size(HeadlineSize::Small))
+                this.child(Label::new(title).size(LabelSize::Small))
             })
             .child(
                 h_flex()

crates/workspace/src/theme_preview.rs 🔗

@@ -5,7 +5,7 @@ use theme::all_theme_colors;
 use ui::{
     element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
     Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
-    Checkbox, ElevationIndex, Facepile, Indicator, Table, TintColor, Tooltip,
+    Checkbox, CheckboxWithLabel, ElevationIndex, Facepile, Indicator, Table, TintColor, Tooltip,
 };
 
 use crate::{Item, Workspace};
@@ -510,6 +510,7 @@ impl ThemePreview {
             .size_full()
             .gap_2()
             .child(Checkbox::render_component_previews(cx))
+            .child(CheckboxWithLabel::render_component_previews(cx))
             .child(Facepile::render_component_previews(cx))
             .child(Button::render_component_previews(cx))
             .child(Indicator::render_component_previews(cx))