onboarding: Continue work on new flow (#35233)

Finn Evers created

This PR continues the work on the new and revamped onboarding flow.


Release Notes:

- N/A

Change summary

Cargo.lock                             |   1 
crates/onboarding/Cargo.toml           |   1 
crates/onboarding/src/onboarding.rs    |  36 +++
crates/onboarding/src/welcome.rs       | 276 ++++++++++++++++++++++++++++
crates/ui/src/components/keybinding.rs |   2 
5 files changed, 314 insertions(+), 2 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -11028,6 +11028,7 @@ dependencies = [
  "ui",
  "workspace",
  "workspace-hack",
+ "zed_actions",
 ]
 
 [[package]]

crates/onboarding/Cargo.toml 🔗

@@ -26,3 +26,4 @@ theme.workspace = true
 ui.workspace = true
 workspace.workspace = true
 workspace-hack.workspace = true
+zed_actions.workspace = true

crates/onboarding/src/onboarding.rs 🔗

@@ -1,3 +1,4 @@
+use crate::welcome::{ShowWelcome, WelcomePage};
 use command_palette_hooks::CommandPaletteFilter;
 use db::kvp::KEY_VALUE_STORE;
 use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
@@ -20,6 +21,8 @@ use workspace::{
     open_new, with_active_or_new_workspace,
 };
 
+mod welcome;
+
 pub struct OnBoardingFeatureFlag {}
 
 impl FeatureFlag for OnBoardingFeatureFlag {
@@ -63,12 +66,43 @@ pub fn init(cx: &mut App) {
                 .detach();
         });
     });
+
+    cx.on_action(|_: &ShowWelcome, cx| {
+        with_active_or_new_workspace(cx, |workspace, window, cx| {
+            workspace
+                .with_local_workspace(window, cx, |workspace, window, cx| {
+                    let existing = workspace
+                        .active_pane()
+                        .read(cx)
+                        .items()
+                        .find_map(|item| item.downcast::<WelcomePage>());
+
+                    if let Some(existing) = existing {
+                        workspace.activate_item(&existing, true, true, window, cx);
+                    } else {
+                        let settings_page = WelcomePage::new(cx);
+                        workspace.add_item_to_active_pane(
+                            Box::new(settings_page),
+                            None,
+                            true,
+                            window,
+                            cx,
+                        )
+                    }
+                })
+                .detach();
+        });
+    });
+
     cx.observe_new::<Workspace>(|_, window, cx| {
         let Some(window) = window else {
             return;
         };
 
-        let onboarding_actions = [std::any::TypeId::of::<OpenOnboarding>()];
+        let onboarding_actions = [
+            std::any::TypeId::of::<OpenOnboarding>(),
+            std::any::TypeId::of::<ShowWelcome>(),
+        ];
 
         CommandPaletteFilter::update_global(cx, |filter, _cx| {
             filter.hide_action_types(&onboarding_actions);

crates/onboarding/src/welcome.rs 🔗

@@ -0,0 +1,276 @@
+use gpui::{
+    Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
+    NoAction, ParentElement, Render, Styled, Window, actions,
+};
+use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
+use workspace::{
+    NewFile, Open, Workspace, WorkspaceId,
+    item::{Item, ItemEvent},
+};
+use zed_actions::{Extensions, OpenSettings, command_palette};
+
+actions!(
+    zed,
+    [
+        /// Show the Zed welcome screen
+        ShowWelcome
+    ]
+);
+
+const CONTENT: (Section<4>, Section<3>) = (
+    Section {
+        title: "Get Started",
+        entries: [
+            SectionEntry {
+                icon: IconName::Plus,
+                title: "New File",
+                action: &NewFile,
+            },
+            SectionEntry {
+                icon: IconName::FolderOpen,
+                title: "Open Project",
+                action: &Open,
+            },
+            SectionEntry {
+                // TODO: use proper icon
+                icon: IconName::Download,
+                title: "Clone a Repo",
+                // TODO: use proper action
+                action: &NoAction,
+            },
+            SectionEntry {
+                icon: IconName::ListCollapse,
+                title: "Open Command Palette",
+                action: &command_palette::Toggle,
+            },
+        ],
+    },
+    Section {
+        title: "Configure",
+        entries: [
+            SectionEntry {
+                icon: IconName::Settings,
+                title: "Open Settings",
+                action: &OpenSettings,
+            },
+            SectionEntry {
+                icon: IconName::ZedAssistant,
+                title: "View AI Settings",
+                // TODO: use proper action
+                action: &NoAction,
+            },
+            SectionEntry {
+                icon: IconName::Blocks,
+                title: "Explore Extensions",
+                action: &Extensions {
+                    category_filter: None,
+                    id: None,
+                },
+            },
+        ],
+    },
+);
+
+struct Section<const COLS: usize> {
+    title: &'static str,
+    entries: [SectionEntry; COLS],
+}
+
+impl<const COLS: usize> Section<COLS> {
+    fn render(
+        self,
+        index_offset: usize,
+        focus: &FocusHandle,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> impl IntoElement {
+        v_flex()
+            .min_w_full()
+            .gap_2()
+            .child(
+                h_flex()
+                    .px_1()
+                    .gap_4()
+                    .child(
+                        Label::new(self.title.to_ascii_uppercase())
+                            .buffer_font(cx)
+                            .color(Color::Muted)
+                            .size(LabelSize::XSmall),
+                    )
+                    .child(Divider::horizontal().color(DividerColor::Border)),
+            )
+            .children(
+                self.entries
+                    .iter()
+                    .enumerate()
+                    .map(|(index, entry)| entry.render(index_offset + index, &focus, window, cx)),
+            )
+    }
+}
+
+struct SectionEntry {
+    icon: IconName,
+    title: &'static str,
+    action: &'static dyn Action,
+}
+
+impl SectionEntry {
+    fn render(
+        &self,
+        button_index: usize,
+        focus: &FocusHandle,
+        window: &Window,
+        cx: &App,
+    ) -> impl IntoElement {
+        ButtonLike::new(("onboarding-button-id", button_index))
+            .full_width()
+            .child(
+                h_flex()
+                    .w_full()
+                    .gap_1()
+                    .justify_between()
+                    .child(
+                        h_flex()
+                            .gap_2()
+                            .child(
+                                Icon::new(self.icon)
+                                    .color(Color::Muted)
+                                    .size(IconSize::XSmall),
+                            )
+                            .child(Label::new(self.title)),
+                    )
+                    .children(KeyBinding::for_action_in(self.action, focus, window, cx)),
+            )
+            .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
+    }
+}
+
+pub struct WelcomePage {
+    focus_handle: FocusHandle,
+}
+
+impl Render for WelcomePage {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let (first_section, second_entries) = CONTENT;
+        let first_section_entries = first_section.entries.len();
+
+        h_flex()
+            .size_full()
+            .justify_center()
+            .overflow_hidden()
+            .bg(cx.theme().colors().editor_background)
+            .key_context("Welcome")
+            .track_focus(&self.focus_handle(cx))
+            .child(
+                h_flex()
+                    .px_12()
+                    .py_40()
+                    .size_full()
+                    .relative()
+                    .max_w(px(1100.))
+                    .child(
+                        div()
+                            .size_full()
+                            .max_w_128()
+                            .mx_auto()
+                            .child(
+                                h_flex()
+                                    .w_full()
+                                    .justify_center()
+                                    .gap_4()
+                                    .child(Vector::square(VectorName::ZedLogo, rems(2.)))
+                                    .child(
+                                        div().child(Headline::new("Welcome to Zed")).child(
+                                            Label::new("The editor for what's next")
+                                                .size(LabelSize::Small)
+                                                .color(Color::Muted)
+                                                .italic(),
+                                        ),
+                                    ),
+                            )
+                            .child(
+                                v_flex()
+                                    .mt_12()
+                                    .gap_8()
+                                    .child(first_section.render(
+                                        Default::default(),
+                                        &self.focus_handle,
+                                        window,
+                                        cx,
+                                    ))
+                                    .child(second_entries.render(
+                                        first_section_entries,
+                                        &self.focus_handle,
+                                        window,
+                                        cx,
+                                    ))
+                                    .child(
+                                        h_flex()
+                                            .w_full()
+                                            .pt_4()
+                                            .justify_center()
+                                            // We call this a hack
+                                            .rounded_b_xs()
+                                            .border_t_1()
+                                            .border_color(DividerColor::Border.hsla(cx))
+                                            .border_dashed()
+                                            .child(
+                                                div().child(
+                                                    Button::new("welcome-exit", "Return to Setup")
+                                                        .full_width()
+                                                        .label_size(LabelSize::XSmall),
+                                                ),
+                                            ),
+                                    ),
+                            ),
+                    ),
+            )
+    }
+}
+
+impl WelcomePage {
+    pub fn new(cx: &mut Context<Workspace>) -> Entity<Self> {
+        let this = cx.new(|cx| WelcomePage {
+            focus_handle: cx.focus_handle(),
+        });
+
+        this
+    }
+}
+
+impl EventEmitter<ItemEvent> for WelcomePage {}
+
+impl Focusable for WelcomePage {
+    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Item for WelcomePage {
+    type Event = ItemEvent;
+
+    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+        "Welcome".into()
+    }
+
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        Some("New Welcome Page Opened")
+    }
+
+    fn show_toolbar(&self) -> bool {
+        false
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: Option<WorkspaceId>,
+        _: &mut Window,
+        _: &mut Context<Self>,
+    ) -> Option<Entity<Self>> {
+        None
+    }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+        f(*event)
+    }
+}

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

@@ -44,7 +44,7 @@ impl KeyBinding {
     pub fn for_action_in(
         action: &dyn Action,
         focus: &FocusHandle,
-        window: &mut Window,
+        window: &Window,
         cx: &App,
     ) -> Option<Self> {
         let key_binding = window.highest_precedence_binding_for_action_in(action, focus)?;