From 070f7dbe1a283cc6d765f5fc8a8c3d78ba5ed425 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Thu, 7 Aug 2025 19:01:52 -0300
Subject: [PATCH] onboarding: Add fast-follow adjustments (#35814)
Release Notes:
- N/A
---
assets/images/certified_user_stamp.svg | 1 -
assets/images/pro_trial_stamp.svg | 2 +-
assets/images/pro_user_stamp.svg | 1 +
assets/keymaps/default-linux.json | 10 ++-
assets/keymaps/default-macos.json | 10 ++-
crates/ai_onboarding/src/ai_upsell_card.rs | 2 +-
crates/onboarding/src/ai_setup_page.rs | 65 +++++++++++--------
crates/onboarding/src/basics_page.rs | 12 ++--
crates/onboarding/src/editing_page.rs | 17 +++--
crates/onboarding/src/onboarding.rs | 61 +++++++++--------
.../ui/src/components/button/toggle_button.rs | 44 ++++++++++++-
crates/ui/src/components/image.rs | 2 +-
12 files changed, 155 insertions(+), 72 deletions(-)
delete mode 100644 assets/images/certified_user_stamp.svg
create mode 100644 assets/images/pro_user_stamp.svg
diff --git a/assets/images/certified_user_stamp.svg b/assets/images/certified_user_stamp.svg
deleted file mode 100644
index 7e65c4fc9d..0000000000
--- a/assets/images/certified_user_stamp.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/images/pro_trial_stamp.svg b/assets/images/pro_trial_stamp.svg
index 501de88a48..a3f9095120 100644
--- a/assets/images/pro_trial_stamp.svg
+++ b/assets/images/pro_trial_stamp.svg
@@ -1 +1 @@
-
+
diff --git a/assets/images/pro_user_stamp.svg b/assets/images/pro_user_stamp.svg
new file mode 100644
index 0000000000..d037a9e833
--- /dev/null
+++ b/assets/images/pro_user_stamp.svg
@@ -0,0 +1 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 567580a9c6..c436b1a8fb 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -1103,6 +1103,13 @@
"ctrl-enter": "menu::Confirm"
}
},
+ {
+ "context": "OnboardingAiConfigurationModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel"
+ }
+ },
{
"context": "Diagnostics",
"use_key_equivalents": true,
@@ -1179,7 +1186,8 @@
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
- "ctrl-escape": "onboarding::Finish"
+ "ctrl-escape": "onboarding::Finish",
+ "alt-tab": "onboarding::SignIn"
}
}
]
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 1c2ad3a006..960bac1479 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -1205,6 +1205,13 @@
"cmd-enter": "menu::Confirm"
}
},
+ {
+ "context": "OnboardingAiConfigurationModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel"
+ }
+ },
{
"context": "Diagnostics",
"use_key_equivalents": true,
@@ -1281,7 +1288,8 @@
"cmd-1": "onboarding::ActivateBasicsPage",
"cmd-2": "onboarding::ActivateEditingPage",
"cmd-3": "onboarding::ActivateAISetupPage",
- "cmd-escape": "onboarding::Finish"
+ "cmd-escape": "onboarding::Finish",
+ "alt-tab": "onboarding::SignIn"
}
}
]
diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs
index 65d3866273..e9639ca075 100644
--- a/crates/ai_onboarding/src/ai_upsell_card.rs
+++ b/crates/ai_onboarding/src/ai_upsell_card.rs
@@ -137,7 +137,7 @@ impl RenderOnce for AiUpsellCard {
.size(rems_from_px(72.))
.child(
Vector::new(
- VectorName::CertifiedUserStamp,
+ VectorName::ProUserStamp,
rems_from_px(72.),
rems_from_px(72.),
)
diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs
index 6099745c40..00f2d5fc8b 100644
--- a/crates/onboarding/src/ai_setup_page.rs
+++ b/crates/onboarding/src/ai_setup_page.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
-use ai_onboarding::{AiUpsellCard, SignInStatus};
-use client::UserStore;
+use ai_onboarding::AiUpsellCard;
+use client::{Client, UserStore};
use fs::Fs;
use gpui::{
Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity,
@@ -12,8 +12,8 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod
use project::DisableAiSettings;
use settings::{Settings, update_settings_file};
use ui::{
- Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState,
- prelude::*, tooltip_container,
+ Badge, ButtonLike, Divider, KeyBinding, Modal, ModalFooter, ModalHeader, Section, SwitchField,
+ ToggleState, prelude::*, tooltip_container,
};
use util::ResultExt;
use workspace::{ModalView, Workspace};
@@ -88,7 +88,7 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i
h_flex()
.gap_2()
.justify_between()
- .child(Label::new("We don't train models using your data"))
+ .child(Label::new("Privacy is the default for Zed"))
.child(
h_flex().gap_1().child(privacy_badge()).child(
Button::new("learn_more", "Learn More")
@@ -109,7 +109,7 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i
)
.child(
Label::new(
- "Feel confident in the security and privacy of your projects using Zed.",
+ "Any use or storage of your data is with your explicit, single-use, opt-in consent.",
)
.size(LabelSize::Small)
.color(Color::Muted),
@@ -240,6 +240,7 @@ fn render_llm_provider_card(
pub(crate) fn render_ai_setup_page(
workspace: WeakEntity,
user_store: Entity,
+ client: Arc,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
@@ -283,15 +284,16 @@ pub(crate) fn render_ai_setup_page(
v_flex()
.mt_2()
.gap_6()
- .child(AiUpsellCard {
- sign_in_status: SignInStatus::SignedIn,
- sign_in: Arc::new(|_, _| {}),
- account_too_young: user_store.read(cx).account_too_young(),
- user_plan: user_store.read(cx).plan(),
- tab_index: Some({
+ .child({
+ let mut ai_upsell_card =
+ AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx);
+
+ ai_upsell_card.tab_index = Some({
tab_index += 1;
tab_index - 1
- }),
+ });
+
+ ai_upsell_card
})
.child(render_llm_provider_section(
&mut tab_index,
@@ -336,6 +338,10 @@ impl AiConfigurationModal {
selected_provider,
}
}
+
+ fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context) {
+ cx.emit(DismissEvent);
+ }
}
impl ModalView for AiConfigurationModal {}
@@ -349,11 +355,15 @@ impl Focusable for AiConfigurationModal {
}
impl Render for AiConfigurationModal {
- fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
v_flex()
+ .key_context("OnboardingAiConfigurationModal")
.w(rems(34.))
.elevation_3(cx)
.track_focus(&self.focus_handle)
+ .on_action(
+ cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
+ )
.child(
Modal::new("onboarding-ai-setup-modal", None)
.header(
@@ -368,18 +378,19 @@ impl Render for AiConfigurationModal {
.section(Section::new().child(self.configuration_view.clone()))
.footer(
ModalFooter::new().end_slot(
- h_flex()
- .gap_1()
- .child(
- Button::new("onboarding-closing-cancel", "Cancel")
- .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
+ Button::new("ai-onb-modal-Done", "Done")
+ .key_binding(
+ KeyBinding::for_action_in(
+ &menu::Cancel,
+ &self.focus_handle.clone(),
+ window,
+ cx,
+ )
+ .map(|kb| kb.size(rems_from_px(12.))),
)
- .child(Button::new("save-btn", "Done").on_click(cx.listener(
- |_, _, window, cx| {
- window.dispatch_action(menu::Confirm.boxed_clone(), cx);
- cx.emit(DismissEvent);
- },
- ))),
+ .on_click(cx.listener(|this, _event, _window, cx| {
+ this.cancel(&menu::Cancel, cx)
+ })),
),
),
)
@@ -396,7 +407,7 @@ impl AiPrivacyTooltip {
impl Render for AiPrivacyTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
- const DESCRIPTION: &'static str = "One of Zed's most important principles is transparency. This is why we are and value open-source so much. And it wouldn't be any different with AI.";
+ const DESCRIPTION: &'static str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. ";
tooltip_container(window, cx, move |this, _, _| {
this.child(
@@ -407,7 +418,7 @@ impl Render for AiPrivacyTooltip {
.size(IconSize::Small)
.color(Color::Muted),
)
- .child(Label::new("Privacy Principle")),
+ .child(Label::new("Privacy First")),
)
.child(
div().max_w_64().child(
diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs
index a4e4028051..a19a21fddf 100644
--- a/crates/onboarding/src/basics_page.rs
+++ b/crates/onboarding/src/basics_page.rs
@@ -201,12 +201,15 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
let fs = ::global(cx);
v_flex()
+ .pt_6()
.gap_4()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant.opacity(0.5))
.child(Label::new("Telemetry").size(LabelSize::Large))
.child(SwitchField::new(
"onboarding-telemetry-metrics",
"Help Improve Zed",
- Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()),
+ Some("Anonymous usage data helps us build the right features and improve your experience.".into()),
if TelemetrySettings::get_global(cx).metrics {
ui::ToggleState::Selected
} else {
@@ -294,7 +297,7 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE
ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
write_keymap_base(BaseKeymap::Emacs, cx);
}),
- ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| {
+ ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| {
write_keymap_base(BaseKeymap::Cursor, cx);
}),
],
@@ -326,10 +329,7 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
SwitchField::new(
"onboarding-vim-mode",
"Vim Mode",
- Some(
- "Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back."
- .into(),
- ),
+ Some("Coming from Neovim? Use our first-class implementation of Vim Mode.".into()),
toggle_state,
{
let fs = ::global(cx);
diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs
index a8f0265b6b..8b4293db0d 100644
--- a/crates/onboarding/src/editing_page.rs
+++ b/crates/onboarding/src/editing_page.rs
@@ -584,11 +584,15 @@ fn render_popular_settings_section(
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
- const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠.";
+ const LIGATURE_TOOLTIP: &'static str =
+ "Font ligatures combine two characters into one. For example, turning =/= into ≠.";
v_flex()
- .gap_5()
- .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8())
+ .pt_6()
+ .gap_4()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant.opacity(0.5))
+ .child(Label::new("Popular Settings").size(LabelSize::Large))
.child(render_font_customization_section(tab_index, window, cx))
.child(
SwitchField::new(
@@ -683,7 +687,10 @@ fn render_popular_settings_section(
[
ToggleButtonSimple::new("Auto", |_, _, cx| {
write_show_mini_map(ShowMinimap::Auto, cx);
- }),
+ })
+ .tooltip(Tooltip::text(
+ "Show the minimap if the editor's scrollbar is visible.",
+ )),
ToggleButtonSimple::new("Always", |_, _, cx| {
write_show_mini_map(ShowMinimap::Always, cx);
}),
@@ -707,7 +714,7 @@ fn render_popular_settings_section(
pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
let mut tab_index = 0;
v_flex()
- .gap_4()
+ .gap_6()
.child(render_import_settings_section(&mut tab_index, cx))
.child(render_popular_settings_section(&mut tab_index, window, cx))
}
diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs
index c4d2b6847c..98f61df97b 100644
--- a/crates/onboarding/src/onboarding.rs
+++ b/crates/onboarding/src/onboarding.rs
@@ -77,6 +77,8 @@ actions!(
ActivateAISetupPage,
/// Finish the onboarding process.
Finish,
+ /// Sign in while in the onboarding flow.
+ SignIn
]
);
@@ -376,6 +378,7 @@ impl Onboarding {
cx,
)
.map(|kb| kb.size(rems_from_px(12.)));
+
if ai_setup_page {
this.child(
ButtonLike::new("start_building")
@@ -387,14 +390,7 @@ impl Onboarding {
.w_full()
.justify_between()
.child(Label::new("Start Building"))
- .child(keybinding.map_or_else(
- || {
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .into_any_element()
- },
- IntoElement::into_any_element,
- )),
+ .children(keybinding),
)
.on_click(|_, window, cx| {
window.dispatch_action(Finish.boxed_clone(), cx);
@@ -409,11 +405,10 @@ impl Onboarding {
.ml_1()
.w_full()
.justify_between()
- .child(Label::new("Skip All"))
- .child(keybinding.map_or_else(
- || gpui::Empty.into_any_element(),
- IntoElement::into_any_element,
- )),
+ .child(
+ Label::new("Skip All").color(Color::Muted),
+ )
+ .children(keybinding),
)
.on_click(|_, window, cx| {
window.dispatch_action(Finish.boxed_clone(), cx);
@@ -435,23 +430,39 @@ impl Onboarding {
Button::new("sign_in", "Sign In")
.full_width()
.style(ButtonStyle::Outlined)
+ .size(ButtonSize::Medium)
+ .key_binding(
+ KeyBinding::for_action_in(&SignIn, &self.focus_handle, window, cx)
+ .map(|kb| kb.size(rems_from_px(12.))),
+ )
.on_click(|_, window, cx| {
- let client = Client::global(cx);
- window
- .spawn(cx, async move |cx| {
- client
- .sign_in_with_optional_connect(true, &cx)
- .await
- .notify_async_err(cx);
- })
- .detach();
+ window.dispatch_action(SignIn.boxed_clone(), cx);
})
.into_any_element()
},
)
}
+ fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
+ go_to_welcome_page(cx);
+ }
+
+ fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) {
+ let client = Client::global(cx);
+
+ window
+ .spawn(cx, async move |cx| {
+ client
+ .sign_in_with_optional_connect(true, &cx)
+ .await
+ .notify_async_err(cx);
+ })
+ .detach();
+ }
+
fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement {
+ let client = Client::global(cx);
+
match self.selected_page {
SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(),
SelectedPage::Editing => {
@@ -460,16 +471,13 @@ impl Onboarding {
SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page(
self.workspace.clone(),
self.user_store.clone(),
+ client,
window,
cx,
)
.into_any_element(),
}
}
-
- fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
- go_to_welcome_page(cx);
- }
}
impl Render for Onboarding {
@@ -486,6 +494,7 @@ impl Render for Onboarding {
.size_full()
.bg(cx.theme().colors().editor_background)
.on_action(Self::on_finish)
+ .on_action(Self::handle_sign_in)
.on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
this.set_page(SelectedPage::Basics, cx);
}))
diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs
index 6fbf834667..91defa730b 100644
--- a/crates/ui/src/components/button/toggle_button.rs
+++ b/crates/ui/src/components/button/toggle_button.rs
@@ -1,6 +1,8 @@
+use std::rc::Rc;
+
use gpui::{AnyView, ClickEvent};
-use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, prelude::*};
+use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, Tooltip, prelude::*};
/// The position of a [`ToggleButton`] within a group of buttons.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
@@ -301,6 +303,7 @@ pub struct ButtonConfiguration {
icon: Option,
on_click: Box,
selected: bool,
+ tooltip: Option AnyView>>,
}
mod private {
@@ -315,6 +318,7 @@ pub struct ToggleButtonSimple {
label: SharedString,
on_click: Box,
selected: bool,
+ tooltip: Option AnyView>>,
}
impl ToggleButtonSimple {
@@ -326,6 +330,7 @@ impl ToggleButtonSimple {
label: label.into(),
on_click: Box::new(on_click),
selected: false,
+ tooltip: None,
}
}
@@ -333,6 +338,11 @@ impl ToggleButtonSimple {
self.selected = selected;
self
}
+
+ pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
+ self.tooltip = Some(Rc::new(tooltip));
+ self
+ }
}
impl private::ToggleButtonStyle for ToggleButtonSimple {}
@@ -344,6 +354,7 @@ impl ButtonBuilder for ToggleButtonSimple {
icon: None,
on_click: self.on_click,
selected: self.selected,
+ tooltip: self.tooltip,
}
}
}
@@ -353,6 +364,7 @@ pub struct ToggleButtonWithIcon {
icon: IconName,
on_click: Box,
selected: bool,
+ tooltip: Option AnyView>>,
}
impl ToggleButtonWithIcon {
@@ -366,6 +378,7 @@ impl ToggleButtonWithIcon {
icon,
on_click: Box::new(on_click),
selected: false,
+ tooltip: None,
}
}
@@ -373,6 +386,11 @@ impl ToggleButtonWithIcon {
self.selected = selected;
self
}
+
+ pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
+ self.tooltip = Some(Rc::new(tooltip));
+ self
+ }
}
impl private::ToggleButtonStyle for ToggleButtonWithIcon {}
@@ -384,6 +402,7 @@ impl ButtonBuilder for ToggleButtonWithIcon {
icon: Some(self.icon),
on_click: self.on_click,
selected: self.selected,
+ tooltip: self.tooltip,
}
}
}
@@ -486,11 +505,13 @@ impl RenderOnce
icon,
on_click,
selected,
+ tooltip,
} = button.into_configuration();
let entry_index = row_index * COLS + col_index;
ButtonLike::new((self.group_name, entry_index))
+ .rounding(None)
.when_some(self.tab_index, |this, tab_index| {
this.tab_index(tab_index + entry_index as isize)
})
@@ -498,7 +519,6 @@ impl RenderOnce
this.toggle_state(true)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
})
- .rounding(None)
.when(self.style == ToggleButtonGroupStyle::Filled, |button| {
button.style(ButtonStyle::Filled)
})
@@ -527,6 +547,9 @@ impl RenderOnce
|this| this.color(Color::Accent),
)),
)
+ .when_some(tooltip, |this, tooltip| {
+ this.tooltip(move |window, cx| tooltip(window, cx))
+ })
.on_click(on_click)
.into_any_element()
})
@@ -920,6 +943,23 @@ impl Component
),
],
)])
+ .children(vec![single_example(
+ "With Tooltips",
+ ToggleButtonGroup::single_row(
+ "with_tooltips",
+ [
+ ToggleButtonSimple::new("First", |_, _, _| {})
+ .tooltip(Tooltip::text("This is a tooltip. Hello!")),
+ ToggleButtonSimple::new("Second", |_, _, _| {})
+ .tooltip(Tooltip::text("This is a tooltip. Hey?")),
+ ToggleButtonSimple::new("Third", |_, _, _| {})
+ .tooltip(Tooltip::text("This is a tooltip. Get out of here now!")),
+ ],
+ )
+ .selected_index(1)
+ .button_width(rems_from_px(100.))
+ .into_any_element(),
+ )])
.into_any_element(),
)
}
diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs
index 18f804abe9..09c3bbeb94 100644
--- a/crates/ui/src/components/image.rs
+++ b/crates/ui/src/components/image.rs
@@ -14,10 +14,10 @@ use crate::prelude::*;
#[strum(serialize_all = "snake_case")]
pub enum VectorName {
AiGrid,
- CertifiedUserStamp,
DebuggerGrid,
Grid,
ProTrialStamp,
+ ProUserStamp,
ZedLogo,
ZedXCopilot,
}