From 7ccf8c2f8cbcd0945df1c6931de93956a9b1b6e3 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 28 Jul 2025 23:10:28 +0200 Subject: [PATCH] onboarding: Continue work on new flow (#35233) This PR continues the work on the new and revamped onboarding flow. Release Notes: - N/A --- 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(-) create mode 100644 crates/onboarding/src/welcome.rs diff --git a/Cargo.lock b/Cargo.lock index 8d9a622655..25196fc349 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11028,6 +11028,7 @@ dependencies = [ "ui", "workspace", "workspace-hack", + "zed_actions", ] [[package]] diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 693e39d4ca..6ec8f8b162 100644 --- a/crates/onboarding/Cargo.toml +++ b/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 diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index dfdea1ca5b..b675ed2dd7 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/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::()); + + 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::(|_, window, cx| { let Some(window) = window else { return; }; - let onboarding_actions = [std::any::TypeId::of::()]; + let onboarding_actions = [ + std::any::TypeId::of::(), + std::any::TypeId::of::(), + ]; CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_action_types(&onboarding_actions); diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs new file mode 100644 index 0000000000..2ed44cf2ce --- /dev/null +++ b/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 { + title: &'static str, + entries: [SectionEntry; COLS], +} + +impl Section { + 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) -> 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) -> Entity { + let this = cx.new(|cx| WelcomePage { + focus_handle: cx.focus_handle(), + }); + + this + } +} + +impl EventEmitter 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, + _: &mut Window, + _: &mut Context, + ) -> Option> { + None + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { + f(*event) + } +} diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 1d91492f26..5779093ccc 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/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 { let key_binding = window.highest_precedence_binding_for_action_in(action, focus)?;