Add additional row components

Nate Butler created

Change summary

crates/onboarding_ui/src/components/callout_row.rs         |  86 +++++
crates/onboarding_ui/src/components/checkbox_row.rs        | 134 ++++++++
crates/onboarding_ui/src/components/header_row.rs          |  78 ++++
crates/onboarding_ui/src/components/mod.rs                 |   4 
crates/onboarding_ui/src/components/selectable_tile_row.rs | 124 +++++++
5 files changed, 426 insertions(+)

Detailed changes

crates/onboarding_ui/src/components/callout_row.rs 🔗

@@ -0,0 +1,86 @@
+use component::{example_group_with_title, single_example};
+use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
+use smallvec::SmallVec;
+use ui::{Label, prelude::*};
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct CalloutRow {
+    title: SharedString,
+    lines: SmallVec<[SharedString; 4]>,
+}
+
+impl CalloutRow {
+    pub fn new(title: impl Into<SharedString>) -> Self {
+        Self {
+            title: title.into(),
+            lines: SmallVec::new(),
+        }
+    }
+
+    pub fn line(mut self, line: impl Into<SharedString>) -> Self {
+        self.lines.push(line.into());
+        self
+    }
+}
+
+impl RenderOnce for CalloutRow {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        div().px_2().child(
+            v_flex()
+                .p_3()
+                .gap_1()
+                .bg(cx.theme().colors().surface_background)
+                .border_1()
+                .border_color(cx.theme().colors().border_variant)
+                .rounded_md()
+                .child(Label::new(self.title).weight(gpui::FontWeight::MEDIUM))
+                .children(
+                    self.lines
+                        .into_iter()
+                        .map(|line| Label::new(line).size(LabelSize::Small).color(Color::Muted)),
+                ),
+        )
+    }
+}
+
+impl Component for CalloutRow {
+    fn scope() -> ComponentScope {
+        ComponentScope::Layout
+    }
+
+    fn sort_name() -> &'static str {
+        "RowCallout"
+    }
+
+    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+        let examples = example_group_with_title(
+            "CalloutRow Examples",
+            vec![
+                single_example(
+                    "Privacy Notice",
+                    CalloutRow::new("We don't use your code to train AI models")
+                        .line("You choose which providers you enable, and they have their own privacy policies.")
+                        .line("Read more about our privacy practices in our Privacy Policy.")
+                        .into_any_element(),
+                ),
+                single_example(
+                    "Single Line",
+                    CalloutRow::new("Important Notice")
+                        .line("This is a single line of information.")
+                        .into_any_element(),
+                ),
+                single_example(
+                    "Multi Line",
+                    CalloutRow::new("Getting Started")
+                        .line("Welcome to Zed! Here are some things to know:")
+                        .line("• Use Cmd+P to quickly open files")
+                        .line("• Use Cmd+Shift+P to access the command palette")
+                        .line("• Check out the documentation for more tips")
+                        .into_any_element(),
+                ),
+            ],
+        );
+
+        Some(v_flex().p_4().gap_4().child(examples).into_any_element())
+    }
+}

crates/onboarding_ui/src/components/checkbox_row.rs 🔗

@@ -0,0 +1,134 @@
+use component::{example_group_with_title, single_example};
+use gpui::StatefulInteractiveElement as _;
+use gpui::{AnyElement, App, ClickEvent, IntoElement, RenderOnce, Window};
+use ui::prelude::*;
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct CheckboxRow {
+    label: SharedString,
+    description: Option<SharedString>,
+    checked: bool,
+    on_click: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
+}
+
+impl CheckboxRow {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            description: None,
+            checked: false,
+            on_click: None,
+        }
+    }
+
+    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
+        self.description = Some(description.into());
+        self
+    }
+
+    pub fn checked(mut self, checked: bool) -> Self {
+        self.checked = checked;
+        self
+    }
+
+    pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
+        self.on_click = Some(Box::new(handler));
+        self
+    }
+}
+
+impl RenderOnce for CheckboxRow {
+    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let checked = self.checked;
+        let on_click = self.on_click;
+
+        let checkbox = gpui::div()
+            .w_4()
+            .h_4()
+            .rounded_sm()
+            .border_1()
+            .border_color(cx.theme().colors().border)
+            .when(checked, |this| {
+                this.bg(cx.theme().colors().element_selected)
+                    .border_color(cx.theme().colors().border_selected)
+            })
+            .hover(|this| this.bg(cx.theme().colors().element_hover))
+            .child(gpui::div().when(checked, |this| {
+                this.size_full()
+                    .flex()
+                    .items_center()
+                    .justify_center()
+                    .child(Icon::new(IconName::Check))
+            }));
+
+        let main_row = if let Some(on_click) = on_click {
+            gpui::div()
+                .id("checkbox-row")
+                .h_flex()
+                .gap_2()
+                .items_center()
+                .child(checkbox)
+                .child(Label::new(self.label))
+                .cursor_pointer()
+                .on_click(move |_event, window, cx| on_click(window, cx))
+        } else {
+            gpui::div()
+                .id("checkbox-row")
+                .h_flex()
+                .gap_2()
+                .items_center()
+                .child(checkbox)
+                .child(Label::new(self.label))
+        };
+
+        v_flex()
+            .px_5()
+            .py_1()
+            .gap_1()
+            .child(main_row)
+            .when_some(self.description, |this, desc| {
+                this.child(
+                    gpui::div()
+                        .ml_6()
+                        .child(Label::new(desc).size(LabelSize::Small).color(Color::Muted)),
+                )
+            })
+    }
+}
+
+impl Component for CheckboxRow {
+    fn scope() -> ComponentScope {
+        ComponentScope::Layout
+    }
+
+    fn sort_name() -> &'static str {
+        "RowCheckbox"
+    }
+
+    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+        let examples = example_group_with_title(
+            "CheckboxRow Examples",
+            vec![
+                single_example(
+                    "Unchecked",
+                    CheckboxRow::new("Enable Vim Mode").into_any_element(),
+                ),
+                single_example(
+                    "Checked",
+                    CheckboxRow::new("Send Crash Reports")
+                        .checked(true)
+                        .into_any_element(),
+                ),
+                single_example(
+                    "With Description",
+                    CheckboxRow::new("Send Telemetry")
+                        .description("Help improve Zed by sending anonymous usage data")
+                        .checked(true)
+                        .into_any_element(),
+                ),
+            ],
+        );
+
+        Some(v_flex().p_4().gap_4().child(examples).into_any_element())
+    }
+}

crates/onboarding_ui/src/components/header_row.rs 🔗

@@ -0,0 +1,78 @@
+use component::{example_group_with_title, single_example};
+use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
+use ui::{Label, prelude::*};
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct HeaderRow {
+    label: SharedString,
+    end_slot: Option<AnyElement>,
+}
+
+impl HeaderRow {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            end_slot: None,
+        }
+    }
+
+    pub fn end_slot(mut self, slot: impl IntoElement) -> Self {
+        self.end_slot = Some(slot.into_any_element());
+        self
+    }
+}
+
+impl RenderOnce for HeaderRow {
+    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+        h_flex()
+            .h(px(32.))
+            .w_full()
+            .px_5()
+            .justify_between()
+            .child(Label::new(self.label))
+            .when_some(self.end_slot, |this, slot| this.child(slot))
+    }
+}
+
+impl Component for HeaderRow {
+    fn scope() -> ComponentScope {
+        ComponentScope::Layout
+    }
+
+    fn sort_name() -> &'static str {
+        "RowHeader"
+    }
+
+    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+        let examples = example_group_with_title(
+            "HeaderRow Examples",
+            vec![
+                single_example(
+                    "Simple Header",
+                    HeaderRow::new("Pick a Theme").into_any_element(),
+                ),
+                single_example(
+                    "Header with Button",
+                    HeaderRow::new("Pick a Theme")
+                        .end_slot(
+                            Button::new("more_themes", "More Themes")
+                                .style(ButtonStyle::Subtle)
+                                .color(Color::Muted),
+                        )
+                        .into_any_element(),
+                ),
+                single_example(
+                    "Header with Icon Button",
+                    HeaderRow::new("Settings")
+                        .end_slot(
+                            IconButton::new("refresh", IconName::RotateCw)
+                                .style(ButtonStyle::Subtle),
+                        )
+                        .into_any_element(),
+                ),
+            ],
+        );
+
+        Some(v_flex().p_4().gap_4().child(examples).into_any_element())
+    }
+}

crates/onboarding_ui/src/components/selectable_tile_row.rs 🔗

@@ -0,0 +1,124 @@
+use super::selectable_tile::SelectableTile;
+use component::{example_group_with_title, single_example};
+use gpui::{
+    AnyElement, App, IntoElement, RenderOnce, StatefulInteractiveElement, Window, prelude::*,
+};
+use smallvec::SmallVec;
+use ui::{Label, prelude::*};
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct SelectableTileRow {
+    gap: Pixels,
+    tiles: SmallVec<[SelectableTile; 8]>,
+}
+
+impl SelectableTileRow {
+    pub fn new() -> Self {
+        Self {
+            gap: px(12.),
+            tiles: SmallVec::new(),
+        }
+    }
+
+    pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
+        self.gap = gap.into();
+        self
+    }
+
+    pub fn tile(mut self, tile: SelectableTile) -> Self {
+        self.tiles.push(tile);
+        self
+    }
+}
+
+impl RenderOnce for SelectableTileRow {
+    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+        h_flex().w_full().px_5().gap(self.gap).children(self.tiles)
+    }
+}
+
+impl Component for SelectableTileRow {
+    fn scope() -> ComponentScope {
+        ComponentScope::Layout
+    }
+
+    fn sort_name() -> &'static str {
+        "RowSelectableTile"
+    }
+
+    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+        let examples = example_group_with_title(
+            "SelectableTileRow Examples",
+            vec![
+                single_example(
+                    "Theme Tiles",
+                    SelectableTileRow::new()
+                        .gap(px(12.))
+                        .tile(
+                            SelectableTile::new("tile1", px(100.), px(80.))
+                                .selected(true)
+                                .child(
+                                    div()
+                                        .size_full()
+                                        .bg(gpui::red())
+                                        .flex()
+                                        .items_center()
+                                        .justify_center()
+                                        .child(Label::new("Dark")),
+                                ),
+                        )
+                        .tile(
+                            SelectableTile::new("tile2", px(100.), px(80.)).child(
+                                div()
+                                    .size_full()
+                                    .bg(gpui::green())
+                                    .flex()
+                                    .items_center()
+                                    .justify_center()
+                                    .child(Label::new("Light")),
+                            ),
+                        )
+                        .tile(
+                            SelectableTile::new("tile3", px(100.), px(80.))
+                                .parent_focused(true)
+                                .child(
+                                    div()
+                                        .size_full()
+                                        .bg(gpui::blue())
+                                        .flex()
+                                        .items_center()
+                                        .justify_center()
+                                        .child(Label::new("Auto")),
+                                ),
+                        )
+                        .into_any_element(),
+                ),
+                single_example(
+                    "Icon Tiles",
+                    SelectableTileRow::new()
+                        .gap(px(8.))
+                        .tile(
+                            SelectableTile::new("icon1", px(48.), px(48.))
+                                .selected(true)
+                                .child(Icon::new(IconName::Code).size(IconSize::Medium)),
+                        )
+                        .tile(
+                            SelectableTile::new("icon2", px(48.), px(48.))
+                                .child(Icon::new(IconName::Terminal).size(IconSize::Medium)),
+                        )
+                        .tile(
+                            SelectableTile::new("icon3", px(48.), px(48.))
+                                .child(Icon::new(IconName::FileCode).size(IconSize::Medium)),
+                        )
+                        .tile(
+                            SelectableTile::new("icon4", px(48.), px(48.))
+                                .child(Icon::new(IconName::Settings).size(IconSize::Medium)),
+                        )
+                        .into_any_element(),
+                ),
+            ],
+        );
+
+        Some(v_flex().p_4().gap_4().child(examples).into_any_element())
+    }
+}