onboarding: Add fast-follow adjustments (#35814)

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-08-07 19:01:52 -03:00 committed by GitHub
parent 106d4cfce9
commit 070f7dbe1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 155 additions and 72 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -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"
}
}
]

View file

@ -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"
}
}
]

View file

@ -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.),
)

View file

@ -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<Workspace>,
user_store: Entity<UserStore>,
client: Arc<Client>,
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<Self>) {
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<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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<Self>) -> 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(

View file

@ -201,12 +201,15 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
let fs = <dyn 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 = <dyn Fs>::global(cx);

View file

@ -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))
}

View file

@ -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<Self>) -> 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);
}))

View file

@ -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<IconName>,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
selected: bool,
tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
}
mod private {
@ -315,6 +318,7 @@ pub struct ToggleButtonSimple {
label: SharedString,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
selected: bool,
tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> 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<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
selected: bool,
tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> 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<T: ButtonBuilder, const COLS: usize, const ROWS: usize> 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<T: ButtonBuilder, const COLS: usize, const ROWS: usize> 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<T: ButtonBuilder, const COLS: usize, const ROWS: usize> 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<T: ButtonBuilder, const COLS: usize, const ROWS: usize> 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(),
)
}

View file

@ -14,10 +14,10 @@ use crate::prelude::*;
#[strum(serialize_all = "snake_case")]
pub enum VectorName {
AiGrid,
CertifiedUserStamp,
DebuggerGrid,
Grid,
ProTrialStamp,
ProUserStamp,
ZedLogo,
ZedXCopilot,
}