onboarding: Add fast-follow adjustments (#35814)

Danilo Leal created

Release Notes:

- N/A

Change summary

assets/images/certified_user_stamp.svg           |  0 
assets/images/pro_trial_stamp.svg                |  0 
assets/images/pro_user_stamp.svg                 |  0 
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 +++++++++-------
crates/ui/src/components/button/toggle_button.rs | 44 +++++++++++
crates/ui/src/components/image.rs                |  2 
12 files changed, 153 insertions(+), 70 deletions(-)

Detailed changes

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

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

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

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<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(

crates/onboarding/src/basics_page.rs 🔗

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

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

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

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<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(),
         )
     }

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,
 }