Add SelectableTile

Nate Butler created

Change summary

Cargo.lock                                             |   1 
crates/onboarding_ui/Cargo.toml                        |   1 
crates/onboarding_ui/src/components/mod.rs             |   1 
crates/onboarding_ui/src/components/selectable_tile.rs | 170 ++++++++++++
crates/onboarding_ui/src/onboarding_ui.rs              |   1 
5 files changed, 174 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -10831,6 +10831,7 @@ dependencies = [
  "serde_json",
  "settings",
  "settings_ui",
+ "smallvec",
  "theme",
  "ui",
  "util",

crates/onboarding_ui/Cargo.toml 🔗

@@ -34,6 +34,7 @@ util.workspace = true
 vim_mode_setting.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
+smallvec.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

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

@@ -0,0 +1,170 @@
+use component::{example_group_with_title, single_example};
+use gpui::{ClickEvent, transparent_black};
+use smallvec::SmallVec;
+use ui::{prelude::*, utils::CornerSolver};
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct SelectableTile {
+    id: ElementId,
+    width: DefiniteLength,
+    height: DefiniteLength,
+    parent_focused: bool,
+    selected: bool,
+    children: SmallVec<[AnyElement; 2]>,
+    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
+}
+
+impl SelectableTile {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            width: px(120.).into(),
+            height: px(120.).into(),
+            parent_focused: false,
+            selected: false,
+            children: SmallVec::new(),
+            on_click: None,
+        }
+    }
+
+    pub fn w(mut self, width: impl Into<DefiniteLength>) -> Self {
+        self.width = width.into();
+        self
+    }
+
+    pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
+        self.height = height.into();
+        self
+    }
+
+    pub fn parent_focused(mut self, focused: bool) -> Self {
+        self.parent_focused = focused;
+        self
+    }
+
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+
+    pub fn on_click(
+        mut self,
+        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+    ) -> Self {
+        self.on_click = Some(Box::new(handler));
+        self
+    }
+}
+
+impl RenderOnce for SelectableTile {
+    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let ring_corner_radius = px(8.);
+        let ring_width = px(1.);
+        let padding = px(2.);
+        let content_border_width = px(0.);
+        let content_border_radius = CornerSolver::child_radius(
+            ring_corner_radius,
+            ring_width,
+            padding,
+            content_border_width,
+        );
+
+        let mut element = h_flex()
+            .id(self.id)
+            .w(self.width)
+            .h(self.height)
+            .overflow_hidden()
+            .rounded(ring_corner_radius)
+            .border(ring_width)
+            .border_color(if self.selected && self.parent_focused {
+                cx.theme().colors().border
+            } else if self.selected {
+                cx.theme().status().info
+            } else {
+                transparent_black()
+            })
+            .p(padding)
+            .child(
+                h_flex()
+                    .size_full()
+                    .rounded(content_border_radius)
+                    .items_center()
+                    .justify_center()
+                    .shadow_hairline()
+                    .bg(cx.theme().colors().surface_background)
+                    .children(self.children),
+            );
+
+        if let Some(on_click) = self.on_click {
+            element = element.on_click(move |event, window, cx| {
+                on_click(event, window, cx);
+            });
+        }
+
+        element
+    }
+}
+
+impl ParentElement for SelectableTile {
+    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
+        self.children.extend(elements)
+    }
+}
+
+impl Component for SelectableTile {
+    fn scope() -> ComponentScope {
+        ComponentScope::Layout
+    }
+
+    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+        let states = example_group_with_title(
+            "SelectableTile States",
+            vec![
+                single_example(
+                    "Default",
+                    SelectableTile::new("default")
+                        .w(px(120.))
+                        .h(px(120.))
+                        .parent_focused(false)
+                        .selected(false)
+                        .child(
+                            div()
+                                .p_4()
+                                .child(Icon::new(IconName::Check).size(IconSize::Medium)),
+                        )
+                        .into_any_element(),
+                ),
+                single_example(
+                    "Selected",
+                    SelectableTile::new("selected")
+                        .w(px(120.))
+                        .h(px(120.))
+                        .parent_focused(false)
+                        .selected(true)
+                        .child(
+                            div()
+                                .p_4()
+                                .child(Icon::new(IconName::Check).size(IconSize::Medium)),
+                        )
+                        .into_any_element(),
+                ),
+                single_example(
+                    "Selected & Parent Focused",
+                    SelectableTile::new("selected_focused")
+                        .w(px(120.))
+                        .h(px(120.))
+                        .parent_focused(true)
+                        .selected(true)
+                        .child(
+                            div()
+                                .p_4()
+                                .child(Icon::new(IconName::Check).size(IconSize::Medium)),
+                        )
+                        .into_any_element(),
+                ),
+            ],
+        );
+
+        Some(v_flex().p_4().gap_4().child(states).into_any_element())
+    }
+}