Onboarding refactor (#39724)

Andrew Farkas , dino , Lukas Wirth , Mikayla Maki , Anthony Eid , and Mikayla Maki created

<img width="1648" height="976" alt="Screenshot 2025-10-07 at 6 57 20 PM"
src="https://github.com/user-attachments/assets/ae7289c0-8820-4fdf-ae28-84fb6bd64942"
/>

Fixes #39347

Release Notes:

- Improved onboarding UI by collapsing it to a single page

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>

Change summary

Cargo.lock                                       |   5 
assets/keymaps/default-linux.json                |   3 
assets/keymaps/default-macos.json                |   5 
assets/keymaps/default-windows.json              |   3 
crates/agent_ui/src/agent_configuration.rs       |   8 
crates/gpui/src/window.rs                        |   2 
crates/onboarding/Cargo.toml                     |   5 
crates/onboarding/src/ai_setup_page.rs           | 427 ------------
crates/onboarding/src/basics_page.rs             | 244 ++++--
crates/onboarding/src/editing_page.rs            | 611 ------------------
crates/onboarding/src/onboarding.rs              | 436 ++---------
crates/onboarding/src/welcome.rs                 |  10 
crates/ui/src/components/button/button_like.rs   |  89 ++
crates/ui/src/components/button/toggle_button.rs |  64 +
crates/ui/src/components/toggle.rs               |  46 
15 files changed, 419 insertions(+), 1,539 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10560,20 +10560,15 @@ dependencies = [
 name = "onboarding"
 version = "0.1.0"
 dependencies = [
- "ai_onboarding",
  "anyhow",
  "client",
  "component",
  "db",
  "documented",
- "editor",
  "fs",
  "fuzzy",
  "git",
  "gpui",
- "itertools 0.14.0",
- "language",
- "language_model",
  "menu",
  "notifications",
  "picker",

assets/keymaps/default-linux.json 🔗

@@ -1229,9 +1229,6 @@
     "context": "Onboarding",
     "use_key_equivalents": true,
     "bindings": {
-      "ctrl-1": "onboarding::ActivateBasicsPage",
-      "ctrl-2": "onboarding::ActivateEditingPage",
-      "ctrl-3": "onboarding::ActivateAISetupPage",
       "ctrl-enter": "onboarding::Finish",
       "alt-shift-l": "onboarding::SignIn",
       "alt-shift-a": "onboarding::OpenAccount"

assets/keymaps/default-macos.json 🔗

@@ -1334,10 +1334,7 @@
     "context": "Onboarding",
     "use_key_equivalents": true,
     "bindings": {
-      "cmd-1": "onboarding::ActivateBasicsPage",
-      "cmd-2": "onboarding::ActivateEditingPage",
-      "cmd-3": "onboarding::ActivateAISetupPage",
-      "cmd-escape": "onboarding::Finish",
+      "cmd-enter": "onboarding::Finish",
       "alt-tab": "onboarding::SignIn",
       "alt-shift-a": "onboarding::OpenAccount"
     }

assets/keymaps/default-windows.json 🔗

@@ -1257,9 +1257,6 @@
     "context": "Onboarding",
     "use_key_equivalents": true,
     "bindings": {
-      "ctrl-1": "onboarding::ActivateBasicsPage",
-      "ctrl-2": "onboarding::ActivateEditingPage",
-      "ctrl-3": "onboarding::ActivateAISetupPage",
       "ctrl-enter": "onboarding::Finish",
       "alt-shift-l": "onboarding::SignIn",
       "shift-alt-a": "onboarding::OpenAccount"

crates/agent_ui/src/agent_configuration.rs 🔗

@@ -409,7 +409,7 @@ impl AgentConfiguration {
 
         SwitchField::new(
             "always-allow-tool-actions-switch",
-            "Allow running commands without asking for confirmation",
+            Some("Allow running commands without asking for confirmation"),
             Some(
                 "The agent can perform potentially destructive actions without asking for your confirmation.".into(),
             ),
@@ -429,7 +429,7 @@ impl AgentConfiguration {
 
         SwitchField::new(
             "single-file-review",
-            "Enable single-file agent reviews",
+            Some("Enable single-file agent reviews"),
             Some("Agent edits are also displayed in single-file editors for review.".into()),
             single_file_review,
             move |state, _window, cx| {
@@ -450,7 +450,7 @@ impl AgentConfiguration {
 
         SwitchField::new(
             "sound-notification",
-            "Play sound when finished generating",
+            Some("Play sound when finished generating"),
             Some(
                 "Hear a notification sound when the agent is done generating changes or needs your input.".into(),
             ),
@@ -470,7 +470,7 @@ impl AgentConfiguration {
 
         SwitchField::new(
             "modifier-send",
-            "Use modifier to submit a message",
+            Some("Use modifier to submit a message"),
             Some(
                 "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
             ),

crates/gpui/src/window.rs 🔗

@@ -58,7 +58,7 @@ mod prompts;
 use crate::util::atomic_incr_if_not_zero;
 pub use prompts::*;
 
-pub(crate) const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1024.), px(700.));
+pub(crate) const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1536.), px(864.));
 
 /// Represents the two different phases when dispatching events.
 #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]

crates/onboarding/Cargo.toml 🔗

@@ -15,20 +15,15 @@ path = "src/onboarding.rs"
 default = []
 
 [dependencies]
-ai_onboarding.workspace = true
 anyhow.workspace = true
 client.workspace = true
 component.workspace = true
 db.workspace = true
 documented.workspace = true
-editor.workspace = true
 fs.workspace = true
 fuzzy.workspace = true
 git.workspace = true
 gpui.workspace = true
-itertools.workspace = true
-language.workspace = true
-language_model.workspace = true
 menu.workspace = true
 notifications.workspace = true
 picker.workspace = true

crates/onboarding/src/ai_setup_page.rs 🔗

@@ -1,427 +0,0 @@
-use std::sync::Arc;
-
-use ai_onboarding::AiUpsellCard;
-use client::{Client, UserStore, zed_urls};
-use fs::Fs;
-use gpui::{
-    Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity,
-    Window, prelude::*,
-};
-use itertools;
-use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
-use project::DisableAiSettings;
-use settings::{Settings, update_settings_file};
-use ui::{
-    Badge, ButtonLike, Divider, KeyBinding, Modal, ModalFooter, ModalHeader, Section, SwitchField,
-    ToggleState, prelude::*, tooltip_container,
-};
-use util::ResultExt;
-use workspace::{ModalView, Workspace};
-use zed_actions::agent::OpenSettings;
-
-const FEATURED_PROVIDERS: [&str; 4] = ["anthropic", "google", "openai", "ollama"];
-
-fn render_llm_provider_section(
-    tab_index: &mut isize,
-    workspace: WeakEntity<Workspace>,
-    disabled: bool,
-    window: &mut Window,
-    cx: &mut App,
-) -> impl IntoElement {
-    v_flex()
-        .gap_4()
-        .child(
-            v_flex()
-                .child(Label::new("Or use other LLM providers").size(LabelSize::Large))
-                .child(
-                    Label::new("Bring your API keys to use the available providers with Zed's UI for free.")
-                        .color(Color::Muted),
-                ),
-        )
-        .child(render_llm_provider_card(tab_index, workspace, disabled, window, cx))
-}
-
-fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> impl IntoElement {
-    let (title, description) = if disabled {
-        (
-            "AI is disabled across Zed",
-            "Re-enable it any time in Settings.",
-        )
-    } else {
-        (
-            "Privacy is the default for Zed",
-            "Any use or storage of your data is with your explicit, single-use, opt-in consent.",
-        )
-    };
-
-    v_flex()
-        .relative()
-        .pt_2()
-        .pb_2p5()
-        .pl_3()
-        .pr_2()
-        .border_1()
-        .border_dashed()
-        .border_color(cx.theme().colors().border.opacity(0.5))
-        .bg(cx.theme().colors().surface_background.opacity(0.3))
-        .rounded_lg()
-        .overflow_hidden()
-        .child(
-            h_flex()
-                .gap_2()
-                .justify_between()
-                .child(Label::new(title))
-                .child(
-                    h_flex()
-                        .gap_1()
-                        .child(
-                            Badge::new("Privacy")
-                                .icon(IconName::ShieldCheck)
-                                .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()),
-                        )
-                        .child(
-                            Button::new("learn_more", "Learn More")
-                                .style(ButtonStyle::Outlined)
-                                .label_size(LabelSize::Small)
-                                .icon(IconName::ArrowUpRight)
-                                .icon_size(IconSize::XSmall)
-                                .icon_color(Color::Muted)
-                                .on_click(|_, _, cx| {
-                                    cx.open_url(&zed_urls::ai_privacy_and_security(cx))
-                                })
-                                .tab_index({
-                                    *tab_index += 1;
-                                    *tab_index - 1
-                                }),
-                        ),
-                ),
-        )
-        .child(
-            Label::new(description)
-                .size(LabelSize::Small)
-                .color(Color::Muted),
-        )
-}
-
-fn render_llm_provider_card(
-    tab_index: &mut isize,
-    workspace: WeakEntity<Workspace>,
-    disabled: bool,
-    _: &mut Window,
-    cx: &mut App,
-) -> impl IntoElement {
-    let registry = LanguageModelRegistry::read_global(cx);
-
-    v_flex()
-        .border_1()
-        .border_color(cx.theme().colors().border)
-        .bg(cx.theme().colors().surface_background.opacity(0.5))
-        .rounded_lg()
-        .overflow_hidden()
-        .children(itertools::intersperse_with(
-            FEATURED_PROVIDERS
-                .into_iter()
-                .flat_map(|provider_name| {
-                    registry.provider(&LanguageModelProviderId::new(provider_name))
-                })
-                .enumerate()
-                .map(|(index, provider)| {
-                    let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
-                    let is_authenticated = provider.is_authenticated(cx);
-
-                    ButtonLike::new(("onboarding-ai-setup-buttons", index))
-                        .size(ButtonSize::Large)
-                        .tab_index({
-                            *tab_index += 1;
-                            *tab_index - 1
-                        })
-                        .child(
-                            h_flex()
-                                .group(&group_name)
-                                .px_0p5()
-                                .w_full()
-                                .gap_2()
-                                .justify_between()
-                                .child(
-                                    h_flex()
-                                        .gap_1()
-                                        .child(
-                                            Icon::new(provider.icon())
-                                                .color(Color::Muted)
-                                                .size(IconSize::XSmall),
-                                        )
-                                        .child(Label::new(provider.name().0)),
-                                )
-                                .child(
-                                    h_flex()
-                                        .gap_1()
-                                        .when(!is_authenticated, |el| {
-                                            el.visible_on_hover(group_name.clone())
-                                                .child(
-                                                    Icon::new(IconName::Settings)
-                                                        .color(Color::Muted)
-                                                        .size(IconSize::XSmall),
-                                                )
-                                                .child(
-                                                    Label::new("Configure")
-                                                        .color(Color::Muted)
-                                                        .size(LabelSize::Small),
-                                                )
-                                        })
-                                        .when(is_authenticated && !disabled, |el| {
-                                            el.child(
-                                                Icon::new(IconName::Check)
-                                                    .color(Color::Success)
-                                                    .size(IconSize::XSmall),
-                                            )
-                                            .child(
-                                                Label::new("Configured")
-                                                    .color(Color::Muted)
-                                                    .size(LabelSize::Small),
-                                            )
-                                        }),
-                                ),
-                        )
-                        .on_click({
-                            let workspace = workspace.clone();
-                            move |_, window, cx| {
-                                workspace
-                                    .update(cx, |workspace, cx| {
-                                        workspace.toggle_modal(window, cx, |window, cx| {
-                                            telemetry::event!(
-                                                "Welcome AI Modal Opened",
-                                                provider = provider.name().0,
-                                            );
-
-                                            let modal = AiConfigurationModal::new(
-                                                provider.clone(),
-                                                window,
-                                                cx,
-                                            );
-                                            window.focus(&modal.focus_handle(cx));
-                                            modal
-                                        });
-                                    })
-                                    .log_err();
-                            }
-                        })
-                        .into_any_element()
-                }),
-            || Divider::horizontal().into_any_element(),
-        ))
-        .child(Divider::horizontal())
-        .child(
-            Button::new("agent_settings", "Add Many Others")
-                .size(ButtonSize::Large)
-                .icon(IconName::Plus)
-                .icon_position(IconPosition::Start)
-                .icon_color(Color::Muted)
-                .icon_size(IconSize::XSmall)
-                .on_click(|_event, window, cx| {
-                    window.dispatch_action(OpenSettings.boxed_clone(), cx)
-                })
-                .tab_index({
-                    *tab_index += 1;
-                    *tab_index - 1
-                }),
-        )
-}
-
-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 {
-    let mut tab_index = 0;
-    let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
-
-    v_flex()
-        .gap_2()
-        .child(
-            SwitchField::new(
-                "enable_ai",
-                "Enable AI features",
-                None,
-                if is_ai_disabled {
-                    ToggleState::Unselected
-                } else {
-                    ToggleState::Selected
-                },
-                |&toggle_state, _, cx| {
-                    let enabled = match toggle_state {
-                        ToggleState::Indeterminate => {
-                            return;
-                        }
-                        ToggleState::Unselected => true,
-                        ToggleState::Selected => false,
-                    };
-
-                    telemetry::event!(
-                        "Welcome AI Enabled",
-                        toggle = if enabled { "on" } else { "off" },
-                    );
-
-                    let fs = <dyn Fs>::global(cx);
-                    update_settings_file(fs, cx, move |settings, _| {
-                        settings.disable_ai = Some(enabled.into());
-                    });
-                },
-            )
-            .tab_index({
-                tab_index += 1;
-                tab_index - 1
-            }),
-        )
-        .child(render_privacy_card(&mut tab_index, is_ai_disabled, cx))
-        .child(
-            v_flex()
-                .mt_2()
-                .gap_6()
-                .child(
-                    AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
-                        .tab_index(Some({
-                            tab_index += 1;
-                            tab_index - 1
-                        })),
-                )
-                .child(render_llm_provider_section(
-                    &mut tab_index,
-                    workspace,
-                    is_ai_disabled,
-                    window,
-                    cx,
-                ))
-                .when(is_ai_disabled, |this| {
-                    this.child(
-                        div()
-                            .id("backdrop")
-                            .size_full()
-                            .absolute()
-                            .inset_0()
-                            .bg(cx.theme().colors().editor_background)
-                            .opacity(0.8)
-                            .block_mouse_except_scroll(),
-                    )
-                }),
-        )
-}
-
-struct AiConfigurationModal {
-    focus_handle: FocusHandle,
-    selected_provider: Arc<dyn LanguageModelProvider>,
-    configuration_view: AnyView,
-}
-
-impl AiConfigurationModal {
-    fn new(
-        selected_provider: Arc<dyn LanguageModelProvider>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let focus_handle = cx.focus_handle();
-        let configuration_view = selected_provider.configuration_view(
-            language_model::ConfigurationViewTargetAgent::ZedAgent,
-            window,
-            cx,
-        );
-
-        Self {
-            focus_handle,
-            configuration_view,
-            selected_provider,
-        }
-    }
-
-    fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
-        cx.emit(DismissEvent);
-    }
-}
-
-impl ModalView for AiConfigurationModal {}
-
-impl EventEmitter<DismissEvent> for AiConfigurationModal {}
-
-impl Focusable for AiConfigurationModal {
-    fn focus_handle(&self, _cx: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl Render for AiConfigurationModal {
-    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(
-                        ModalHeader::new()
-                            .icon(
-                                Icon::new(self.selected_provider.icon())
-                                    .color(Color::Muted)
-                                    .size(IconSize::Small),
-                            )
-                            .headline(self.selected_provider.name().0),
-                    )
-                    .section(Section::new().child(self.configuration_view.clone()))
-                    .footer(
-                        ModalFooter::new().end_slot(
-                            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.))),
-                                )
-                                .on_click(cx.listener(|this, _event, _window, cx| {
-                                    this.cancel(&menu::Cancel, cx)
-                                })),
-                        ),
-                    ),
-            )
-    }
-}
-
-pub struct AiPrivacyTooltip {}
-
-impl AiPrivacyTooltip {
-    pub fn new() -> Self {
-        Self {}
-    }
-}
-
-impl Render for AiPrivacyTooltip {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        const DESCRIPTION: &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(cx, move |this, _| {
-            this.child(
-                h_flex()
-                    .gap_1()
-                    .child(
-                        Icon::new(IconName::ShieldCheck)
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
-                    )
-                    .child(Label::new("Privacy First")),
-            )
-            .child(
-                div().max_w_64().child(
-                    Label::new(DESCRIPTION)
-                        .size(LabelSize::Small)
-                        .color(Color::Muted),
-                ),
-            )
-        })
-    }
-}

crates/onboarding/src/basics_page.rs 🔗

@@ -2,19 +2,23 @@ use std::sync::Arc;
 
 use client::TelemetrySettings;
 use fs::Fs;
-use gpui::{App, IntoElement};
+use gpui::{Action, App, IntoElement};
 use settings::{BaseKeymap, Settings, update_settings_file};
 use theme::{
     Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection,
     ThemeSettings,
 };
 use ui::{
-    ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup,
-    ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px,
+    ButtonLike, ParentElement as _, StatefulInteractiveElement, SwitchField, TintColor,
+    ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*,
+    rems_from_px,
 };
 use vim_mode_setting::VimModeSetting;
 
-use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile};
+use crate::{
+    ImportCursorSettings, ImportVsCodeSettings, SettingsImportState,
+    theme_preview::{ThemePreviewStyle, ThemePreviewTile},
+};
 
 const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
 const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
@@ -70,6 +74,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
                         )
                     }),
                 )
+                .size(ToggleButtonGroupSize::Medium)
                 .tab_index(tab_index)
                 .selected_index(theme_mode as usize)
                 .style(ui::ToggleButtonGroupStyle::Outlined)
@@ -220,91 +225,87 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
         .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("Anonymous usage data helps us build the right features and improve your experience.".into()),
-            if TelemetrySettings::get_global(cx).metrics {
-                ui::ToggleState::Selected
-            } else {
-                ui::ToggleState::Unselected
-            },
-            {
-            let fs = fs.clone();
-            move |selection, _, cx| {
-                let enabled = match selection {
-                    ToggleState::Selected => true,
-                    ToggleState::Unselected => false,
-                    ToggleState::Indeterminate => { return; },
-                };
-
-                update_settings_file(
-                    fs.clone(),
-                    cx,
-                    move |setting, _| {
-                        setting.telemetry.get_or_insert_default().metrics = Some(enabled);
-                    }
-                    ,
-                );
-
-                // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
-                // and can fix it in a timely manner to respect a user's choice.
-                telemetry::event!("Welcome Page Telemetry Metrics Toggled",
-                    options = if enabled {
-                        "on"
-                    } else {
-                        "off"
+        .child(
+            SwitchField::new(
+                "onboarding-telemetry-metrics",
+                None::<&str>,
+                Some("Help improve Zed by sending anonymous usage data".into()),
+                if TelemetrySettings::get_global(cx).metrics {
+                    ui::ToggleState::Selected
+                } else {
+                    ui::ToggleState::Unselected
+                },
+                {
+                    let fs = fs.clone();
+                    move |selection, _, cx| {
+                        let enabled = match selection {
+                            ToggleState::Selected => true,
+                            ToggleState::Unselected => false,
+                            ToggleState::Indeterminate => {
+                                return;
+                            }
+                        };
+
+                        update_settings_file(fs.clone(), cx, move |setting, _| {
+                            setting.telemetry.get_or_insert_default().metrics = Some(enabled);
+                        });
+
+                        // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
+                        // and can fix it in a timely manner to respect a user's choice.
+                        telemetry::event!(
+                            "Welcome Page Telemetry Metrics Toggled",
+                            options = if enabled { "on" } else { "off" }
+                        );
                     }
-                );
+                },
+            )
+            .tab_index({
+                *tab_index += 1;
+                *tab_index
+            }),
+        )
+        .child(
+            SwitchField::new(
+                "onboarding-telemetry-crash-reports",
+                None::<&str>,
+                Some(
+                    "Help fix Zed by sending crash reports so we can fix critical issues fast"
+                        .into(),
+                ),
+                if TelemetrySettings::get_global(cx).diagnostics {
+                    ui::ToggleState::Selected
+                } else {
+                    ui::ToggleState::Unselected
+                },
+                {
+                    let fs = fs.clone();
+                    move |selection, _, cx| {
+                        let enabled = match selection {
+                            ToggleState::Selected => true,
+                            ToggleState::Unselected => false,
+                            ToggleState::Indeterminate => {
+                                return;
+                            }
+                        };
 
-            }},
-        ).tab_index({
-            *tab_index += 1;
-            *tab_index
-        }))
-        .child(SwitchField::new(
-            "onboarding-telemetry-crash-reports",
-            "Help Fix Zed",
-            Some("Send crash reports so we can fix critical issues fast.".into()),
-            if TelemetrySettings::get_global(cx).diagnostics {
-                ui::ToggleState::Selected
-            } else {
-                ui::ToggleState::Unselected
-            },
-            {
-                let fs = fs.clone();
-                move |selection, _, cx| {
-                    let enabled = match selection {
-                        ToggleState::Selected => true,
-                        ToggleState::Unselected => false,
-                        ToggleState::Indeterminate => { return; },
-                    };
-
-                    update_settings_file(
-                        fs.clone(),
-                        cx,
-                        move |setting, _| {
+                        update_settings_file(fs.clone(), cx, move |setting, _| {
                             setting.telemetry.get_or_insert_default().diagnostics = Some(enabled);
-                        },
-
-                    );
-
-                    // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
-                    // and can fix it in a timely manner to respect a user's choice.
-                    telemetry::event!("Welcome Page Telemetry Diagnostics Toggled",
-                        options = if enabled {
-                            "on"
-                        } else {
-                            "off"
-                        }
-                    );
-                }
-            }
-        ).tab_index({
-                    *tab_index += 1;
-                    *tab_index
-                }))
+                        });
+
+                        // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
+                        // and can fix it in a timely manner to respect a user's choice.
+                        telemetry::event!(
+                            "Welcome Page Telemetry Diagnostics Toggled",
+                            options = if enabled { "on" } else { "off" }
+                        );
+                    }
+                },
+            )
+            .tab_index({
+                *tab_index += 1;
+                *tab_index
+            }),
+        )
 }
 
 fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
@@ -372,8 +373,8 @@ 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? Use our first-class implementation of Vim Mode.".into()),
+        Some("Vim Mode"),
+        Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()),
         toggle_state,
         {
             let fs = <dyn Fs>::global(cx);
@@ -402,12 +403,79 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
     })
 }
 
+fn render_setting_import_button(
+    tab_index: isize,
+    label: SharedString,
+    action: &dyn Action,
+    imported: bool,
+) -> impl IntoElement + 'static {
+    let action = action.boxed_clone();
+    h_flex().w_full().child(
+        ButtonLike::new(label.clone())
+            .style(ButtonStyle::OutlinedTransparent)
+            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+            .toggle_state(imported)
+            .size(ButtonSize::Medium)
+            .tab_index(tab_index)
+            .child(
+                h_flex()
+                    .w_full()
+                    .justify_between()
+                    .when(imported, |this| {
+                        this.child(Icon::new(IconName::Check).color(Color::Success))
+                    })
+                    .child(Label::new(label.clone()).mx_2().size(LabelSize::Small)),
+            )
+            .on_click(move |_, window, cx| {
+                telemetry::event!("Welcome Import Settings", import_source = label,);
+                window.dispatch_action(action.boxed_clone(), cx);
+            }),
+    )
+}
+
+fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
+    let import_state = SettingsImportState::global(cx);
+    let imports: [(SharedString, &dyn Action, bool); 2] = [
+        (
+            "VS Code".into(),
+            &ImportVsCodeSettings { skip_prompt: false },
+            import_state.vscode,
+        ),
+        (
+            "Cursor".into(),
+            &ImportCursorSettings { skip_prompt: false },
+            import_state.cursor,
+        ),
+    ];
+
+    let [vscode, cursor] = imports.map(|(label, action, imported)| {
+        *tab_index += 1;
+        render_setting_import_button(*tab_index - 1, label, action, imported)
+    });
+
+    h_flex()
+        .child(
+            v_flex()
+                .gap_0p5()
+                .max_w_5_6()
+                .child(Label::new("Import Settings"))
+                .child(
+                    Label::new("Automatically pull your settings from other editors")
+                        .color(Color::Muted),
+                ),
+        )
+        .child(div().w_full())
+        .child(h_flex().gap_1().child(vscode).child(cursor))
+}
+
 pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement {
     let mut tab_index = 0;
     v_flex()
+        .id("basics-page")
         .gap_6()
         .child(render_theme_section(&mut tab_index, cx))
         .child(render_base_keymap_section(&mut tab_index, cx))
+        .child(render_import_settings_section(&mut tab_index, cx))
         .child(render_vim_mode_switch(&mut tab_index, cx))
         .child(render_telemetry_section(&mut tab_index, cx))
 }

crates/onboarding/src/editing_page.rs 🔗

@@ -1,611 +0,0 @@
-use std::sync::Arc;
-
-use editor::{EditorSettings, ShowMinimap};
-use fs::Fs;
-use gpui::{Action, App, FontFeatures, IntoElement, Pixels, SharedString, Window};
-use language::language_settings::{AllLanguageSettings, FormatOnSave};
-use project::project_settings::ProjectSettings;
-use settings::{Settings as _, update_settings_file};
-use theme::{FontFamilyName, ThemeSettings};
-use ui::{
-    ButtonLike, PopoverMenu, SwitchField, ToggleButtonGroup, ToggleButtonGroupStyle,
-    ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
-};
-use ui_input::{NumberField, font_picker};
-
-use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
-
-fn read_show_mini_map(cx: &App) -> ShowMinimap {
-    editor::EditorSettings::get_global(cx).minimap.show
-}
-
-fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    // This is used to speed up the UI
-    // the UI reads the current values to get what toggle state to show on buttons
-    // there's a slight delay if we just call update_settings_file so we manually set
-    // the value here then call update_settings file to get around the delay
-    let mut curr_settings = EditorSettings::get_global(cx).clone();
-    curr_settings.minimap.show = show;
-    EditorSettings::override_global(curr_settings, cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        telemetry::event!(
-            "Welcome Minimap Clicked",
-            from = settings.editor.minimap.clone().unwrap_or_default(),
-            to = show
-        );
-        settings.editor.minimap.get_or_insert_default().show = Some(show);
-    });
-}
-
-fn read_inlay_hints(cx: &App) -> bool {
-    AllLanguageSettings::get_global(cx)
-        .defaults
-        .inlay_hints
-        .enabled
-}
-
-fn write_inlay_hints(enabled: bool, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    let mut curr_settings = AllLanguageSettings::get_global(cx).clone();
-    curr_settings.defaults.inlay_hints.enabled = enabled;
-    AllLanguageSettings::override_global(curr_settings, cx);
-
-    update_settings_file(fs, cx, move |settings, _cx| {
-        settings
-            .project
-            .all_languages
-            .defaults
-            .inlay_hints
-            .get_or_insert_default()
-            .enabled = Some(enabled);
-    });
-}
-
-fn read_git_blame(cx: &App) -> bool {
-    ProjectSettings::get_global(cx).git.inline_blame.enabled
-}
-
-fn write_git_blame(enabled: bool, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    let mut curr_settings = ProjectSettings::get_global(cx).clone();
-    curr_settings.git.inline_blame.enabled = enabled;
-    ProjectSettings::override_global(curr_settings, cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        settings
-            .git
-            .get_or_insert_default()
-            .inline_blame
-            .get_or_insert_default()
-            .enabled = Some(enabled);
-    });
-}
-
-fn write_ui_font_family(font: SharedString, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        telemetry::event!(
-            "Welcome Font Changed",
-            type = "ui font",
-            old = settings.theme.ui_font_family,
-            new = font
-        );
-        settings.theme.ui_font_family = Some(FontFamilyName(font.into()));
-    });
-}
-
-fn write_ui_font_size(size: Pixels, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        settings.theme.ui_font_size = Some(size.into());
-    });
-}
-
-fn write_buffer_font_size(size: Pixels, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        settings.theme.buffer_font_size = Some(size.into());
-    });
-}
-
-fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        telemetry::event!(
-            "Welcome Font Changed",
-            type = "editor font",
-            old = settings.theme.buffer_font_family,
-            new = font_family
-        );
-
-        settings.theme.buffer_font_family = Some(FontFamilyName(font_family.into()));
-    });
-}
-
-fn read_font_ligatures(cx: &App) -> bool {
-    ThemeSettings::get_global(cx)
-        .buffer_font
-        .features
-        .is_calt_enabled()
-        .unwrap_or(true)
-}
-
-fn write_font_ligatures(enabled: bool, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-    let bit = if enabled { 1 } else { 0 };
-
-    update_settings_file(fs, cx, move |settings, _| {
-        let mut features = settings
-            .theme
-            .buffer_font_features
-            .as_mut()
-            .map(|features| features.tag_value_list().to_vec())
-            .unwrap_or_default();
-
-        if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
-            features[calt_index].1 = bit;
-        } else {
-            features.push(("calt".into(), bit));
-        }
-
-        settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features)));
-    });
-}
-
-fn read_format_on_save(cx: &App) -> bool {
-    match AllLanguageSettings::get_global(cx).defaults.format_on_save {
-        FormatOnSave::On => true,
-        FormatOnSave::Off => false,
-    }
-}
-
-fn write_format_on_save(format_on_save: bool, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        settings.project.all_languages.defaults.format_on_save = Some(match format_on_save {
-            true => FormatOnSave::On,
-            false => FormatOnSave::Off,
-        });
-    });
-}
-
-fn render_setting_import_button(
-    tab_index: isize,
-    label: SharedString,
-    icon_name: IconName,
-    action: &dyn Action,
-    imported: bool,
-) -> impl IntoElement {
-    let action = action.boxed_clone();
-    h_flex().w_full().child(
-        ButtonLike::new(label.clone())
-            .full_width()
-            .style(ButtonStyle::Outlined)
-            .size(ButtonSize::Large)
-            .tab_index(tab_index)
-            .child(
-                h_flex()
-                    .w_full()
-                    .justify_between()
-                    .child(
-                        h_flex()
-                            .gap_1p5()
-                            .px_1()
-                            .child(
-                                Icon::new(icon_name)
-                                    .color(Color::Muted)
-                                    .size(IconSize::XSmall),
-                            )
-                            .child(Label::new(label.clone())),
-                    )
-                    .when(imported, |this| {
-                        this.child(
-                            h_flex()
-                                .gap_1p5()
-                                .child(
-                                    Icon::new(IconName::Check)
-                                        .color(Color::Success)
-                                        .size(IconSize::XSmall),
-                                )
-                                .child(Label::new("Imported").size(LabelSize::Small)),
-                        )
-                    }),
-            )
-            .on_click(move |_, window, cx| {
-                telemetry::event!("Welcome Import Settings", import_source = label,);
-                window.dispatch_action(action.boxed_clone(), cx);
-            }),
-    )
-}
-
-fn render_import_settings_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
-    let import_state = SettingsImportState::global(cx);
-    let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [
-        (
-            "VS Code".into(),
-            IconName::EditorVsCode,
-            &ImportVsCodeSettings { skip_prompt: false },
-            import_state.vscode,
-        ),
-        (
-            "Cursor".into(),
-            IconName::EditorCursor,
-            &ImportCursorSettings { skip_prompt: false },
-            import_state.cursor,
-        ),
-    ];
-
-    let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| {
-        *tab_index += 1;
-        render_setting_import_button(*tab_index - 1, label, icon_name, action, imported)
-    });
-
-    v_flex()
-        .gap_4()
-        .child(
-            v_flex()
-                .child(Label::new("Import Settings").size(LabelSize::Large))
-                .child(
-                    Label::new("Automatically pull your settings from other editors.")
-                        .color(Color::Muted),
-                ),
-        )
-        .child(h_flex().w_full().gap_4().child(vscode).child(cursor))
-}
-
-fn render_font_customization_section(
-    tab_index: &mut isize,
-    window: &mut Window,
-    cx: &mut App,
-) -> impl IntoElement {
-    let theme_settings = ThemeSettings::get_global(cx);
-    let ui_font_size = theme_settings.ui_font_size(cx);
-    let ui_font_family = theme_settings.ui_font.family.clone();
-    let buffer_font_family = theme_settings.buffer_font.family.clone();
-    let buffer_font_size = theme_settings.buffer_font_size(cx);
-
-    let ui_font_picker =
-        cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx));
-
-    let buffer_font_picker = cx.new(|cx| {
-        font_picker(
-            buffer_font_family.clone(),
-            write_buffer_font_family,
-            window,
-            cx,
-        )
-    });
-
-    let ui_font_handle = ui::PopoverMenuHandle::default();
-    let buffer_font_handle = ui::PopoverMenuHandle::default();
-
-    h_flex()
-        .w_full()
-        .gap_4()
-        .child(
-            v_flex()
-                .w_full()
-                .gap_1()
-                .child(Label::new("UI Font"))
-                .child(
-                    h_flex()
-                        .w_full()
-                        .justify_between()
-                        .gap_2()
-                        .child(
-                            PopoverMenu::new("ui-font-picker")
-                                .menu({
-                                    let ui_font_picker = ui_font_picker;
-                                    move |_window, _cx| Some(ui_font_picker.clone())
-                                })
-                                .trigger(
-                                    ButtonLike::new("ui-font-family-button")
-                                        .style(ButtonStyle::Outlined)
-                                        .size(ButtonSize::Medium)
-                                        .full_width()
-                                        .tab_index({
-                                            *tab_index += 1;
-                                            *tab_index - 1
-                                        })
-                                        .child(
-                                            h_flex()
-                                                .w_full()
-                                                .justify_between()
-                                                .child(Label::new(ui_font_family))
-                                                .child(
-                                                    Icon::new(IconName::ChevronUpDown)
-                                                        .color(Color::Muted)
-                                                        .size(IconSize::XSmall),
-                                                ),
-                                        ),
-                                )
-                                .full_width(true)
-                                .anchor(gpui::Corner::TopLeft)
-                                .offset(gpui::Point {
-                                    x: px(0.0),
-                                    y: px(4.0),
-                                })
-                                .with_handle(ui_font_handle),
-                        )
-                        .child(font_picker_stepper(
-                            "ui-font-size",
-                            &ui_font_size,
-                            tab_index,
-                            write_ui_font_size,
-                            window,
-                            cx,
-                        )),
-                ),
-        )
-        .child(
-            v_flex()
-                .w_full()
-                .gap_1()
-                .child(Label::new("Editor Font"))
-                .child(
-                    h_flex()
-                        .w_full()
-                        .justify_between()
-                        .gap_2()
-                        .child(
-                            PopoverMenu::new("buffer-font-picker")
-                                .menu({
-                                    let buffer_font_picker = buffer_font_picker;
-                                    move |_window, _cx| Some(buffer_font_picker.clone())
-                                })
-                                .trigger(
-                                    ButtonLike::new("buffer-font-family-button")
-                                        .style(ButtonStyle::Outlined)
-                                        .size(ButtonSize::Medium)
-                                        .full_width()
-                                        .tab_index({
-                                            *tab_index += 1;
-                                            *tab_index - 1
-                                        })
-                                        .child(
-                                            h_flex()
-                                                .w_full()
-                                                .justify_between()
-                                                .child(Label::new(buffer_font_family))
-                                                .child(
-                                                    Icon::new(IconName::ChevronUpDown)
-                                                        .color(Color::Muted)
-                                                        .size(IconSize::XSmall),
-                                                ),
-                                        ),
-                                )
-                                .full_width(true)
-                                .anchor(gpui::Corner::TopLeft)
-                                .offset(gpui::Point {
-                                    x: px(0.0),
-                                    y: px(4.0),
-                                })
-                                .with_handle(buffer_font_handle),
-                        )
-                        .child(font_picker_stepper(
-                            "buffer-font-size",
-                            &buffer_font_size,
-                            tab_index,
-                            write_buffer_font_size,
-                            window,
-                            cx,
-                        )),
-                ),
-        )
-}
-
-fn font_picker_stepper(
-    id: &'static str,
-    font_size: &Pixels,
-    tab_index: &mut isize,
-    write_font_size: fn(Pixels, &mut App),
-    window: &mut Window,
-    cx: &mut App,
-) -> NumberField<u32> {
-    window.with_id(id, |window| {
-        let optimistic_font_size: gpui::Entity<Option<u32>> = window.use_state(cx, |_, _| None);
-        optimistic_font_size.update(cx, |optimistic_font_size, _| {
-            if let Some(optimistic_font_size_val) = optimistic_font_size {
-                if *optimistic_font_size_val == u32::from(font_size) {
-                    *optimistic_font_size = None;
-                }
-            }
-        });
-
-        let stepper_font_size = optimistic_font_size
-            .read(cx)
-            .unwrap_or_else(|| font_size.into());
-
-        NumberField::new(
-            SharedString::new(format!("{}-stepper", id)),
-            stepper_font_size,
-            window,
-            cx,
-        )
-        .on_change(move |new_value, _, cx| {
-            optimistic_font_size.write(cx, Some(*new_value));
-            write_font_size(Pixels::from(*new_value), cx);
-        })
-        .format(|value| format!("{value}px"))
-        .tab_index({
-            *tab_index += 2;
-            *tab_index - 2
-        })
-        .min(6)
-        .max(32)
-    })
-}
-
-fn render_popular_settings_section(
-    tab_index: &mut isize,
-    window: &mut Window,
-    cx: &mut App,
-) -> impl IntoElement {
-    const LIGATURE_TOOLTIP: &str =
-        "Font ligatures combine two characters into one. For example, turning != into ≠.";
-
-    v_flex()
-        .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(
-                "onboarding-font-ligatures",
-                "Font Ligatures",
-                Some("Combine text characters into their associated symbols.".into()),
-                if read_font_ligatures(cx) {
-                    ui::ToggleState::Selected
-                } else {
-                    ui::ToggleState::Unselected
-                },
-                |toggle_state, _, cx| {
-                    let enabled = toggle_state == &ToggleState::Selected;
-                    telemetry::event!(
-                        "Welcome Font Ligature",
-                        options = if enabled { "on" } else { "off" },
-                    );
-
-                    write_font_ligatures(enabled, cx);
-                },
-            )
-            .tab_index({
-                *tab_index += 1;
-                *tab_index - 1
-            })
-            .tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
-        )
-        .child(
-            SwitchField::new(
-                "onboarding-format-on-save",
-                "Format on Save",
-                Some("Format code automatically when saving.".into()),
-                if read_format_on_save(cx) {
-                    ui::ToggleState::Selected
-                } else {
-                    ui::ToggleState::Unselected
-                },
-                |toggle_state, _, cx| {
-                    let enabled = toggle_state == &ToggleState::Selected;
-                    telemetry::event!(
-                        "Welcome Format On Save Changed",
-                        options = if enabled { "on" } else { "off" },
-                    );
-
-                    write_format_on_save(enabled, cx);
-                },
-            )
-            .tab_index({
-                *tab_index += 1;
-                *tab_index - 1
-            }),
-        )
-        .child(
-            SwitchField::new(
-                "onboarding-enable-inlay-hints",
-                "Inlay Hints",
-                Some("See parameter names for function and method calls inline.".into()),
-                if read_inlay_hints(cx) {
-                    ui::ToggleState::Selected
-                } else {
-                    ui::ToggleState::Unselected
-                },
-                |toggle_state, _, cx| {
-                    let enabled = toggle_state == &ToggleState::Selected;
-                    telemetry::event!(
-                        "Welcome Inlay Hints Changed",
-                        options = if enabled { "on" } else { "off" },
-                    );
-
-                    write_inlay_hints(enabled, cx);
-                },
-            )
-            .tab_index({
-                *tab_index += 1;
-                *tab_index - 1
-            }),
-        )
-        .child(
-            SwitchField::new(
-                "onboarding-git-blame-switch",
-                "Inline Git Blame",
-                Some("See who committed each line on a given file.".into()),
-                if read_git_blame(cx) {
-                    ui::ToggleState::Selected
-                } else {
-                    ui::ToggleState::Unselected
-                },
-                |toggle_state, _, cx| {
-                    let enabled = toggle_state == &ToggleState::Selected;
-                    telemetry::event!(
-                        "Welcome Git Blame Changed",
-                        options = if enabled { "on" } else { "off" },
-                    );
-
-                    write_git_blame(enabled, cx);
-                },
-            )
-            .tab_index({
-                *tab_index += 1;
-                *tab_index - 1
-            }),
-        )
-        .child(
-            h_flex()
-                .items_start()
-                .justify_between()
-                .child(
-                    v_flex().child(Label::new("Minimap")).child(
-                        Label::new("See a high-level overview of your source code.")
-                            .color(Color::Muted),
-                    ),
-                )
-                .child(
-                    ToggleButtonGroup::single_row(
-                        "onboarding-show-mini-map",
-                        [
-                            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);
-                            }),
-                            ToggleButtonSimple::new("Never", |_, _, cx| {
-                                write_show_mini_map(ShowMinimap::Never, cx);
-                            }),
-                        ],
-                    )
-                    .selected_index(match read_show_mini_map(cx) {
-                        ShowMinimap::Auto => 0,
-                        ShowMinimap::Always => 1,
-                        ShowMinimap::Never => 2,
-                    })
-                    .tab_index(tab_index)
-                    .style(ToggleButtonGroupStyle::Outlined)
-                    .width(ui::rems_from_px(3. * 64.)),
-                ),
-        )
-}
-
-pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
-    let mut tab_index = 0;
-    v_flex()
-        .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 🔗

@@ -14,8 +14,8 @@ use serde::Deserialize;
 use settings::{SettingsStore, VsCodeSettingsSource};
 use std::sync::Arc;
 use ui::{
-    Avatar, ButtonLike, FluentBuilder, Headline, KeyBinding, ParentElement as _,
-    StatefulInteractiveElement, Vector, VectorName, WithScrollbar, prelude::*, rems_from_px,
+    KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName,
+    WithScrollbar as _, prelude::*, rems_from_px,
 };
 pub use ui_input::font_picker;
 use workspace::{
@@ -26,10 +26,8 @@ use workspace::{
     open_new, register_serializable_item, with_active_or_new_workspace,
 };
 
-mod ai_setup_page;
 mod base_keymap_picker;
 mod basics_page;
-mod editing_page;
 pub mod multibuffer_hint;
 mod theme_preview;
 mod welcome;
@@ -66,12 +64,6 @@ actions!(
 actions!(
     onboarding,
     [
-        /// Activates the Basics page.
-        ActivateBasicsPage,
-        /// Activates the Editing page.
-        ActivateEditingPage,
-        /// Activates the AI Setup page.
-        ActivateAISetupPage,
         /// Finish the onboarding process.
         Finish,
         /// Sign in while in the onboarding flow.
@@ -216,27 +208,9 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
     )
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum SelectedPage {
-    Basics,
-    Editing,
-    AiSetup,
-}
-
-impl SelectedPage {
-    fn name(&self) -> &'static str {
-        match self {
-            SelectedPage::Basics => "Basics",
-            SelectedPage::Editing => "Editing",
-            SelectedPage::AiSetup => "AI Setup",
-        }
-    }
-}
-
 struct Onboarding {
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
-    selected_page: SelectedPage,
     user_store: Entity<UserStore>,
     scroll_handle: ScrollHandle,
     _settings_subscription: Subscription,
@@ -259,7 +233,6 @@ impl Onboarding {
                 workspace: workspace.weak_handle(),
                 focus_handle: cx.focus_handle(),
                 scroll_handle: ScrollHandle::new(),
-                selected_page: SelectedPage::Basics,
                 user_store: workspace.user_store().clone(),
                 _settings_subscription: cx
                     .observe_global::<SettingsStore>(move |_, cx| cx.notify()),
@@ -267,228 +240,8 @@ impl Onboarding {
         })
     }
 
-    fn set_page(
-        &mut self,
-        page: SelectedPage,
-        clicked: Option<&'static str>,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(click) = clicked {
-            telemetry::event!(
-                "Welcome Tab Clicked",
-                from = self.selected_page.name(),
-                to = page.name(),
-                clicked = click,
-            );
-        }
-
-        self.selected_page = page;
-        self.scroll_handle.set_offset(Default::default());
-        cx.notify();
-        cx.emit(ItemEvent::UpdateTab);
-    }
-
-    fn render_nav_buttons(
-        &mut self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> [impl IntoElement; 3] {
-        let pages = [
-            SelectedPage::Basics,
-            SelectedPage::Editing,
-            SelectedPage::AiSetup,
-        ];
-
-        let text = ["Basics", "Editing", "AI Setup"];
-
-        let actions: [&dyn Action; 3] = [
-            &ActivateBasicsPage,
-            &ActivateEditingPage,
-            &ActivateAISetupPage,
-        ];
-
-        let mut binding = actions.map(|action| {
-            KeyBinding::for_action_in(action, &self.focus_handle, window, cx)
-                .map(|kb| kb.size(rems_from_px(12.)))
-        });
-
-        pages.map(|page| {
-            let i = page as usize;
-            let selected = self.selected_page == page;
-            h_flex()
-                .id(text[i])
-                .relative()
-                .w_full()
-                .gap_2()
-                .px_2()
-                .py_0p5()
-                .justify_between()
-                .rounded_sm()
-                .when(selected, |this| {
-                    this.child(
-                        div()
-                            .h_4()
-                            .w_px()
-                            .bg(cx.theme().colors().text_accent)
-                            .absolute()
-                            .left_0(),
-                    )
-                })
-                .hover(|style| style.bg(cx.theme().colors().element_hover))
-                .child(Label::new(text[i]).map(|this| {
-                    if selected {
-                        this.color(Color::Default)
-                    } else {
-                        this.color(Color::Muted)
-                    }
-                }))
-                .child(binding[i].take().map_or(
-                    gpui::Empty.into_any_element(),
-                    IntoElement::into_any_element,
-                ))
-                .on_click(cx.listener(move |this, click_event, _, cx| {
-                    let click = match click_event {
-                        gpui::ClickEvent::Mouse(_) => "mouse",
-                        gpui::ClickEvent::Keyboard(_) => "keyboard",
-                    };
-
-                    this.set_page(page, Some(click), cx);
-                }))
-        })
-    }
-
-    fn render_nav(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        v_flex()
-            .h_full()
-            .w(rems_from_px(220.))
-            .flex_shrink_0()
-            .gap_4()
-            .justify_between()
-            .child(
-                v_flex()
-                    .gap_6()
-                    .child(
-                        h_flex()
-                            .px_2()
-                            .gap_4()
-                            .child(Vector::square(VectorName::ZedLogo, rems(2.5)))
-                            .child(
-                                v_flex()
-                                    .child(
-                                        Headline::new("Welcome to Zed").size(HeadlineSize::Small),
-                                    )
-                                    .child(
-                                        Label::new("The editor for what's next")
-                                            .color(Color::Muted)
-                                            .size(LabelSize::Small)
-                                            .italic(),
-                                    ),
-                            ),
-                    )
-                    .child(
-                        v_flex()
-                            .gap_4()
-                            .child(
-                                v_flex()
-                                    .py_4()
-                                    .border_y_1()
-                                    .border_color(cx.theme().colors().border_variant.opacity(0.5))
-                                    .gap_1()
-                                    .children(self.render_nav_buttons(window, cx)),
-                            )
-                            .map(|this| {
-                                if let Some(user) = self.user_store.read(cx).current_user() {
-                                    this.child(
-                                        v_flex()
-                                            .gap_1()
-                                            .child(
-                                                h_flex()
-                                                    .ml_2()
-                                                    .gap_2()
-                                                    .max_w_full()
-                                                    .w_full()
-                                                    .child(Avatar::new(user.avatar_uri.clone()))
-                                                    .child(
-                                                        Label::new(user.github_login.clone())
-                                                            .truncate(),
-                                                    ),
-                                            )
-                                            .child(
-                                                ButtonLike::new("open_account")
-                                                    .size(ButtonSize::Medium)
-                                                    .child(
-                                                        h_flex()
-                                                            .ml_1()
-                                                            .w_full()
-                                                            .justify_between()
-                                                            .child(Label::new("Open Account"))
-                                                            .children(
-                                                                KeyBinding::for_action_in(
-                                                                    &OpenAccount,
-                                                                    &self.focus_handle,
-                                                                    window,
-                                                                    cx,
-                                                                )
-                                                                .map(|kb| {
-                                                                    kb.size(rems_from_px(12.))
-                                                                }),
-                                                            ),
-                                                    )
-                                                    .on_click(|_, window, cx| {
-                                                        window.dispatch_action(
-                                                            OpenAccount.boxed_clone(),
-                                                            cx,
-                                                        );
-                                                    }),
-                                            ),
-                                    )
-                                } else {
-                                    this.child(
-                                        ButtonLike::new("sign_in")
-                                            .size(ButtonSize::Medium)
-                                            .child(
-                                                h_flex()
-                                                    .ml_1()
-                                                    .w_full()
-                                                    .justify_between()
-                                                    .child(Label::new("Sign In"))
-                                                    .children(
-                                                        KeyBinding::for_action_in(
-                                                            &SignIn,
-                                                            &self.focus_handle,
-                                                            window,
-                                                            cx,
-                                                        )
-                                                        .map(|kb| kb.size(rems_from_px(12.))),
-                                                    ),
-                                            )
-                                            .on_click(|_, window, cx| {
-                                                telemetry::event!("Welcome Sign In Clicked");
-                                                window.dispatch_action(SignIn.boxed_clone(), cx);
-                                            }),
-                                    )
-                                }
-                            }),
-                    ),
-            )
-            .child({
-                Button::new("start_building", "Start Building")
-                    .full_width()
-                    .style(ButtonStyle::Outlined)
-                    .size(ButtonSize::Medium)
-                    .key_binding(
-                        KeyBinding::for_action_in(&Finish, &self.focus_handle, window, cx)
-                            .map(|kb| kb.size(rems_from_px(12.))),
-                    )
-                    .on_click(|_, window, cx| {
-                        telemetry::event!("Welcome Start Building Clicked");
-                        window.dispatch_action(Finish.boxed_clone(), cx);
-                    })
-            })
-    }
-
     fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
-        telemetry::event!("Welcome Skip Clicked");
+        telemetry::event!("Finish Setup");
         go_to_welcome_page(cx);
     }
 
@@ -509,29 +262,14 @@ impl Onboarding {
         cx.open_url(&zed_urls::account_url(cx))
     }
 
-    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 => {
-                crate::editing_page::render_editing_page(window, cx).into_any_element()
-            }
-            SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page(
-                self.workspace.clone(),
-                self.user_store.clone(),
-                client,
-                window,
-                cx,
-            )
-            .into_any_element(),
-        }
+    fn render_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
+        crate::basics_page::render_basics_page(cx).into_any_element()
     }
 }
 
 impl Render for Onboarding {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        h_flex()
+        div()
             .image_cache(gpui::retain_all("onboarding-page"))
             .key_context({
                 let mut ctx = KeyContext::new_with_defaults();
@@ -545,15 +283,6 @@ impl Render for Onboarding {
             .on_action(Self::on_finish)
             .on_action(Self::handle_sign_in)
             .on_action(Self::handle_open_account)
-            .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
-                this.set_page(SelectedPage::Basics, Some("action"), cx);
-            }))
-            .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| {
-                this.set_page(SelectedPage::Editing, Some("action"), cx);
-            }))
-            .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
-                this.set_page(SelectedPage::AiSetup, Some("action"), cx);
-            }))
             .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
                 window.focus_next();
                 cx.notify();
@@ -563,35 +292,68 @@ impl Render for Onboarding {
                 cx.notify();
             }))
             .child(
-                h_flex()
-                    .max_w(rems_from_px(1100.))
-                    .max_h(rems_from_px(850.))
+                div()
+                    .max_w(Rems(48.0))
+                    .w_full()
+                    .mx_auto()
                     .size_full()
-                    .m_auto()
-                    .py_20()
-                    .px_12()
-                    .items_start()
-                    .gap_12()
-                    .child(self.render_nav(window, cx))
+                    .gap_6()
                     .child(
-                        div()
+                        v_flex()
+                            .m_auto()
+                            .id("page-content")
+                            .gap_6()
                             .size_full()
-                            .pr_6()
+                            .max_w_full()
+                            .min_w_0()
+                            .p_12()
+                            .border_color(cx.theme().colors().border_variant.opacity(0.5))
+                            .overflow_y_scroll()
                             .child(
-                                v_flex()
-                                    .id("page-content")
-                                    .size_full()
-                                    .max_w_full()
-                                    .min_w_0()
-                                    .pl_12()
-                                    .border_l_1()
-                                    .border_color(cx.theme().colors().border_variant.opacity(0.5))
-                                    .overflow_y_scroll()
-                                    .child(self.render_page(window, cx))
-                                    .track_scroll(&self.scroll_handle),
+                                h_flex()
+                                    .w_full()
+                                    .gap_4()
+                                    .child(Vector::square(VectorName::ZedLogo, rems(2.5)))
+                                    .child(
+                                        v_flex()
+                                            .child(
+                                                Headline::new("Welcome to Zed")
+                                                    .size(HeadlineSize::Small),
+                                            )
+                                            .child(
+                                                Label::new("The editor for what's next")
+                                                    .color(Color::Muted)
+                                                    .size(LabelSize::Small)
+                                                    .italic(),
+                                            ),
+                                    )
+                                    .child(div().w_full())
+                                    .child({
+                                        Button::new("finish_setup", "Finish Setup")
+                                            .style(ButtonStyle::Filled)
+                                            .size(ButtonSize::Large)
+                                            .width(Rems(12.0))
+                                            .key_binding(
+                                                KeyBinding::for_action_in(
+                                                    &Finish,
+                                                    &self.focus_handle,
+                                                    window,
+                                                    cx,
+                                                )
+                                                .map(|kb| kb.size(rems_from_px(12.))),
+                                            )
+                                            .on_click(|_, window, cx| {
+                                                window.dispatch_action(Finish.boxed_clone(), cx);
+                                            })
+                                    })
+                                    .pb_6()
+                                    .border_b_1()
+                                    .border_color(cx.theme().colors().border_variant.opacity(0.5)),
                             )
-                            .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
-                    ),
+                            .child(self.render_page(cx))
+                            .track_scroll(&self.scroll_handle),
+                    )
+                    .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
             )
     }
 }
@@ -628,7 +390,6 @@ impl Item for Onboarding {
         Some(cx.new(|cx| Onboarding {
             workspace: self.workspace.clone(),
             user_store: self.user_store.clone(),
-            selected_page: self.selected_page,
             scroll_handle: ScrollHandle::new(),
             focus_handle: cx.focus_handle(),
             _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
@@ -814,25 +575,10 @@ impl workspace::SerializableItem for Onboarding {
         cx: &mut App,
     ) -> gpui::Task<gpui::Result<Entity<Self>>> {
         window.spawn(cx, async move |cx| {
-            if let Some(page_number) =
+            if let Some(_) =
                 persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)?
             {
-                let page = match page_number {
-                    0 => Some(SelectedPage::Basics),
-                    1 => Some(SelectedPage::Editing),
-                    2 => Some(SelectedPage::AiSetup),
-                    _ => None,
-                };
-                workspace.update(cx, |workspace, cx| {
-                    let onboarding_page = Onboarding::new(workspace, cx);
-                    if let Some(page) = page {
-                        zlog::info!("Onboarding page {page:?} loaded");
-                        onboarding_page.update(cx, |onboarding_page, cx| {
-                            onboarding_page.set_page(page, None, cx);
-                        })
-                    }
-                    onboarding_page
-                })
+                workspace.update(cx, |workspace, cx| Onboarding::new(workspace, cx))
             } else {
                 Err(anyhow::anyhow!("No onboarding page to deserialize"))
             }
@@ -848,10 +594,10 @@ impl workspace::SerializableItem for Onboarding {
         cx: &mut ui::Context<Self>,
     ) -> Option<gpui::Task<gpui::Result<()>>> {
         let workspace_id = workspace.database_id()?;
-        let page_number = self.selected_page as u16;
+
         Some(cx.background_spawn(async move {
             persistence::ONBOARDING_PAGES
-                .save_onboarding_page(item_id, workspace_id, page_number)
+                .save_onboarding_page(item_id, workspace_id)
                 .await
         }))
     }
@@ -874,17 +620,32 @@ mod persistence {
     impl Domain for OnboardingPagesDb {
         const NAME: &str = stringify!(OnboardingPagesDb);
 
-        const MIGRATIONS: &[&str] = &[sql!(
-                    CREATE TABLE onboarding_pages (
-                        workspace_id INTEGER,
-                        item_id INTEGER UNIQUE,
-                        page_number INTEGER,
-
-                        PRIMARY KEY(workspace_id, item_id),
-                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
-                        ON DELETE CASCADE
-                    ) STRICT;
-        )];
+        const MIGRATIONS: &[&str] = &[
+            sql!(
+                        CREATE TABLE onboarding_pages (
+                            workspace_id INTEGER,
+                            item_id INTEGER UNIQUE,
+                            page_number INTEGER,
+
+                            PRIMARY KEY(workspace_id, item_id),
+                            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                            ON DELETE CASCADE
+                        ) STRICT;
+            ),
+            sql!(
+                        CREATE TABLE onboarding_pages_2 (
+                            workspace_id INTEGER,
+                            item_id INTEGER UNIQUE,
+
+                            PRIMARY KEY(workspace_id, item_id),
+                            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                            ON DELETE CASCADE
+                        ) STRICT;
+                        INSERT INTO onboarding_pages_2 SELECT workspace_id, item_id FROM onboarding_pages;
+                        DROP TABLE onboarding_pages;
+                        ALTER TABLE onboarding_pages_2 RENAME TO onboarding_pages;
+            ),
+        ];
     }
 
     db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
@@ -893,11 +654,10 @@ mod persistence {
         query! {
             pub async fn save_onboarding_page(
                 item_id: workspace::ItemId,
-                workspace_id: workspace::WorkspaceId,
-                page_number: u16
+                workspace_id: workspace::WorkspaceId
             ) -> Result<()> {
-                INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id, page_number)
-                VALUES (?, ?, ?)
+                INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id)
+                VALUES (?, ?)
             }
         }
 
@@ -905,8 +665,8 @@ mod persistence {
             pub fn get_onboarding_page(
                 item_id: workspace::ItemId,
                 workspace_id: workspace::WorkspaceId
-            ) -> Result<Option<u16>> {
-                SELECT page_number
+            ) -> Result<Option<workspace::ItemId>> {
+                SELECT item_id
                 FROM onboarding_pages
                 WHERE item_id = ? AND workspace_id = ?
             }

crates/onboarding/src/welcome.rs 🔗

@@ -151,6 +151,7 @@ impl SectionEntry {
 }
 
 pub struct WelcomePage {
+    first_paint: bool,
     focus_handle: FocusHandle,
 }
 
@@ -168,6 +169,10 @@ impl WelcomePage {
 
 impl Render for WelcomePage {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        if self.first_paint {
+            window.request_animation_frame();
+            self.first_paint = false;
+        }
         let (first_section, second_section) = CONTENT;
         let first_section_entries = first_section.entries.len();
         let last_index = first_section_entries + second_section.entries.len();
@@ -311,7 +316,10 @@ impl WelcomePage {
             cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
                 .detach();
 
-            WelcomePage { focus_handle }
+            WelcomePage {
+                first_paint: true,
+                focus_handle,
+            }
         })
     }
 }

crates/ui/src/components/button/button_like.rs 🔗

@@ -135,6 +135,9 @@ pub enum ButtonStyle {
     /// a fully transparent button.
     Outlined,
 
+    /// Transparent button that always has an outline.
+    OutlinedTransparent,
+
     /// A more de-emphasized version of the outlined button.
     OutlinedGhost,
 
@@ -149,11 +152,38 @@ pub enum ButtonStyle {
     Transparent,
 }
 
+/// Rounding for a button that may have straight edges.
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub(crate) enum ButtonLikeRounding {
-    All,
-    Left,
-    Right,
+pub(crate) struct ButtonLikeRounding {
+    /// Top-left corner rounding
+    pub top_left: bool,
+    /// Top-right corner rounding
+    pub top_right: bool,
+    /// Bottom-right corner rounding
+    pub bottom_right: bool,
+    /// Bottom-left corner rounding
+    pub bottom_left: bool,
+}
+
+impl ButtonLikeRounding {
+    pub const ALL: Self = Self {
+        top_left: true,
+        top_right: true,
+        bottom_right: true,
+        bottom_left: true,
+    };
+    pub const LEFT: Self = Self {
+        top_left: true,
+        top_right: false,
+        bottom_right: false,
+        bottom_left: true,
+    };
+    pub const RIGHT: Self = Self {
+        top_left: false,
+        top_right: true,
+        bottom_right: true,
+        bottom_left: false,
+    };
 }
 
 #[derive(Debug, Clone)]
@@ -198,6 +228,12 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
+                background: cx.theme().colors().ghost_element_background,
+                border_color: cx.theme().colors().border_variant,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
             ButtonStyle::OutlinedGhost => ButtonLikeStyles {
                 background: transparent_black(),
                 border_color: cx.theme().colors().border_variant,
@@ -249,6 +285,12 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
+                background: cx.theme().colors().ghost_element_hover,
+                border_color: cx.theme().colors().border,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
             ButtonStyle::OutlinedGhost => ButtonLikeStyles {
                 background: transparent_black(),
                 border_color: cx.theme().colors().border,
@@ -293,6 +335,12 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
+                background: cx.theme().colors().ghost_element_active,
+                border_color: cx.theme().colors().border_variant,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
             ButtonStyle::OutlinedGhost => ButtonLikeStyles {
                 background: transparent_black(),
                 border_color: cx.theme().colors().border_variant,
@@ -332,6 +380,12 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
+                background: cx.theme().colors().ghost_element_background,
+                border_color: cx.theme().colors().border,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
             ButtonStyle::OutlinedGhost => ButtonLikeStyles {
                 background: transparent_black(),
                 border_color: cx.theme().colors().border,
@@ -374,6 +428,12 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
+                background: cx.theme().colors().ghost_element_disabled,
+                border_color: cx.theme().colors().border_disabled,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
             ButtonStyle::OutlinedGhost => ButtonLikeStyles {
                 background: transparent_black(),
                 border_color: cx.theme().colors().border_disabled,
@@ -455,7 +515,7 @@ impl ButtonLike {
             width: None,
             height: None,
             size: ButtonSize::Default,
-            rounding: Some(ButtonLikeRounding::All),
+            rounding: Some(ButtonLikeRounding::ALL),
             tooltip: None,
             hoverable_tooltip: None,
             children: SmallVec::new(),
@@ -469,15 +529,15 @@ impl ButtonLike {
     }
 
     pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
-        Self::new(id).rounding(ButtonLikeRounding::Left)
+        Self::new(id).rounding(ButtonLikeRounding::LEFT)
     }
 
     pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
-        Self::new(id).rounding(ButtonLikeRounding::Right)
+        Self::new(id).rounding(ButtonLikeRounding::RIGHT)
     }
 
     pub fn new_rounded_all(id: impl Into<ElementId>) -> Self {
-        Self::new(id).rounding(ButtonLikeRounding::All)
+        Self::new(id).rounding(ButtonLikeRounding::ALL)
     }
 
     pub fn opacity(mut self, opacity: f32) -> Self {
@@ -630,14 +690,17 @@ impl RenderOnce for ButtonLike {
             .when(
                 matches!(
                     self.style,
-                    ButtonStyle::Outlined | ButtonStyle::OutlinedGhost
+                    ButtonStyle::Outlined
+                        | ButtonStyle::OutlinedTransparent
+                        | ButtonStyle::OutlinedGhost
                 ),
                 |this| this.border_1(),
             )
-            .when_some(self.rounding, |this, rounding| match rounding {
-                ButtonLikeRounding::All => this.rounded_sm(),
-                ButtonLikeRounding::Left => this.rounded_l_sm(),
-                ButtonLikeRounding::Right => this.rounded_r_sm(),
+            .when_some(self.rounding, |this, rounding| {
+                this.when(rounding.top_left, |this| this.rounded_tl_sm())
+                    .when(rounding.top_right, |this| this.rounded_tr_sm())
+                    .when(rounding.bottom_right, |this| this.rounded_br_sm())
+                    .when(rounding.bottom_left, |this| this.rounded_bl_sm())
             })
             .gap(DynamicSpacing::Base04.rems(cx))
             .map(|this| match self.size {

crates/ui/src/components/button/toggle_button.rs 🔗

@@ -6,15 +6,41 @@ use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, Tooltip,
 
 /// The position of a [`ToggleButton`] within a group of buttons.
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum ToggleButtonPosition {
-    /// The toggle button is first in the group.
-    First,
-
-    /// The toggle button is in the middle of the group (i.e., it is not the first or last toggle button).
-    Middle,
+pub struct ToggleButtonPosition {
+    /// The toggle button is one of the leftmost of the group.
+    leftmost: bool,
+    /// The toggle button is one of the rightmost of the group.
+    rightmost: bool,
+    /// The toggle button is one of the topmost of the group.
+    topmost: bool,
+    /// The toggle button is one of the bottommost of the group.
+    bottommost: bool,
+}
 
-    /// The toggle button is last in the group.
-    Last,
+impl ToggleButtonPosition {
+    pub const HORIZONTAL_FIRST: Self = Self {
+        leftmost: true,
+        ..Self::HORIZONTAL_MIDDLE
+    };
+    pub const HORIZONTAL_MIDDLE: Self = Self {
+        leftmost: false,
+        rightmost: false,
+        topmost: true,
+        bottommost: true,
+    };
+    pub const HORIZONTAL_LAST: Self = Self {
+        rightmost: true,
+        ..Self::HORIZONTAL_MIDDLE
+    };
+
+    pub(crate) fn to_rounding(self) -> ButtonLikeRounding {
+        ButtonLikeRounding {
+            top_left: self.topmost && self.leftmost,
+            top_right: self.topmost && self.rightmost,
+            bottom_right: self.bottommost && self.rightmost,
+            bottom_left: self.bottommost && self.leftmost,
+        }
+    }
 }
 
 #[derive(IntoElement, RegisterComponent)]
@@ -46,15 +72,15 @@ impl ToggleButton {
     }
 
     pub fn first(self) -> Self {
-        self.position_in_group(ToggleButtonPosition::First)
+        self.position_in_group(ToggleButtonPosition::HORIZONTAL_FIRST)
     }
 
     pub fn middle(self) -> Self {
-        self.position_in_group(ToggleButtonPosition::Middle)
+        self.position_in_group(ToggleButtonPosition::HORIZONTAL_MIDDLE)
     }
 
     pub fn last(self) -> Self {
-        self.position_in_group(ToggleButtonPosition::Last)
+        self.position_in_group(ToggleButtonPosition::HORIZONTAL_LAST)
     }
 }
 
@@ -153,10 +179,8 @@ impl RenderOnce for ToggleButton {
         };
 
         self.base
-            .when_some(self.position_in_group, |this, position| match position {
-                ToggleButtonPosition::First => this.rounding(ButtonLikeRounding::Left),
-                ToggleButtonPosition::Middle => this.rounding(None),
-                ToggleButtonPosition::Last => this.rounding(ButtonLikeRounding::Right),
+            .when_some(self.position_in_group, |this, position| {
+                this.rounding(position.to_rounding())
             })
             .child(
                 Label::new(self.label)
@@ -535,7 +559,15 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
 
                     ButtonLike::new((group_name.clone(), entry_index))
                         .full_width()
-                        .rounding(None)
+                        .rounding(Some(
+                            ToggleButtonPosition {
+                                leftmost: col_index == 0,
+                                rightmost: col_index == COLS - 1,
+                                topmost: row_index == 0,
+                                bottommost: row_index == ROWS - 1,
+                            }
+                            .to_rounding(),
+                        ))
                         .when_some(self.tab_index, |this, tab_index| {
                             this.tab_index(tab_index + entry_index as isize)
                         })

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

@@ -585,7 +585,7 @@ impl RenderOnce for Switch {
 ///
 /// let switch_field = SwitchField::new(
 ///     "feature-toggle",
-///     "Enable feature",
+///     Some("Enable feature"),
 ///     Some("This feature adds new functionality to the app.".into()),
 ///     ToggleState::Unselected,
 ///     |state, window, cx| {
@@ -596,7 +596,7 @@ impl RenderOnce for Switch {
 #[derive(IntoElement, RegisterComponent)]
 pub struct SwitchField {
     id: ElementId,
-    label: SharedString,
+    label: Option<SharedString>,
     description: Option<SharedString>,
     toggle_state: ToggleState,
     on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
@@ -609,14 +609,14 @@ pub struct SwitchField {
 impl SwitchField {
     pub fn new(
         id: impl Into<ElementId>,
-        label: impl Into<SharedString>,
+        label: Option<impl Into<SharedString>>,
         description: Option<SharedString>,
         toggle_state: impl Into<ToggleState>,
         on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
     ) -> Self {
         Self {
             id: id.into(),
-            label: label.into(),
+            label: label.map(Into::into),
             description,
             toggle_state: toggle_state.into(),
             on_click: Arc::new(on_click),
@@ -657,11 +657,11 @@ impl SwitchField {
 
 impl RenderOnce for SwitchField {
     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
-        let tooltip = self.tooltip.map(|tooltip_fn| {
-            h_flex()
-                .gap_0p5()
-                .child(Label::new(self.label.clone()))
-                .child(
+        let tooltip = self
+            .tooltip
+            .zip(self.label.clone())
+            .map(|(tooltip_fn, label)| {
+                h_flex().gap_0p5().child(Label::new(label)).child(
                     IconButton::new("tooltip_button", IconName::Info)
                         .icon_size(IconSize::XSmall)
                         .icon_color(Color::Muted)
@@ -673,7 +673,7 @@ impl RenderOnce for SwitchField {
                         })
                         .on_click(|_, _, _| {}), // Intentional empty on click handler so that clicking on the info tooltip icon doesn't trigger the switch toggle
                 )
-        });
+            });
 
         h_flex()
             .id((self.id.clone(), "container"))
@@ -694,11 +694,17 @@ impl RenderOnce for SwitchField {
                 (Some(description), None) => v_flex()
                     .gap_0p5()
                     .max_w_5_6()
-                    .child(Label::new(self.label.clone()))
+                    .when_some(self.label, |this, label| this.child(Label::new(label)))
                     .child(Label::new(description.clone()).color(Color::Muted))
                     .into_any_element(),
                 (None, Some(tooltip)) => tooltip.into_any_element(),
-                (None, None) => Label::new(self.label.clone()).into_any_element(),
+                (None, None) => {
+                    if let Some(label) = self.label.clone() {
+                        Label::new(label).into_any_element()
+                    } else {
+                        gpui::Empty.into_any_element()
+                    }
+                }
             })
             .child(
                 Switch::new((self.id.clone(), "switch"), self.toggle_state)
@@ -748,7 +754,7 @@ impl Component for SwitchField {
                                 "Unselected",
                                 SwitchField::new(
                                     "switch_field_unselected",
-                                    "Enable notifications",
+                                    Some("Enable notifications"),
                                     Some("Receive notifications when new messages arrive.".into()),
                                     ToggleState::Unselected,
                                     |_, _, _| {},
@@ -759,7 +765,7 @@ impl Component for SwitchField {
                                 "Selected",
                                 SwitchField::new(
                                     "switch_field_selected",
-                                    "Enable notifications",
+                                    Some("Enable notifications"),
                                     Some("Receive notifications when new messages arrive.".into()),
                                     ToggleState::Selected,
                                     |_, _, _| {},
@@ -775,7 +781,7 @@ impl Component for SwitchField {
                                 "Default",
                                 SwitchField::new(
                                     "switch_field_default",
-                                    "Default color",
+                                    Some("Default color"),
                                     Some("This uses the default switch color.".into()),
                                     ToggleState::Selected,
                                     |_, _, _| {},
@@ -786,7 +792,7 @@ impl Component for SwitchField {
                                 "Accent",
                                 SwitchField::new(
                                     "switch_field_accent",
-                                    "Accent color",
+                                    Some("Accent color"),
                                     Some("This uses the accent color scheme.".into()),
                                     ToggleState::Selected,
                                     |_, _, _| {},
@@ -802,7 +808,7 @@ impl Component for SwitchField {
                             "Disabled",
                             SwitchField::new(
                                 "switch_field_disabled",
-                                "Disabled field",
+                                Some("Disabled field"),
                                 Some("This field is disabled and cannot be toggled.".into()),
                                 ToggleState::Selected,
                                 |_, _, _| {},
@@ -817,7 +823,7 @@ impl Component for SwitchField {
                             "No Description",
                             SwitchField::new(
                                 "switch_field_disabled",
-                                "Disabled field",
+                                Some("Disabled field"),
                                 None,
                                 ToggleState::Selected,
                                 |_, _, _| {},
@@ -832,7 +838,7 @@ impl Component for SwitchField {
                                 "Tooltip with Description",
                                 SwitchField::new(
                                     "switch_field_tooltip_with_desc",
-                                    "Nice Feature",
+                                    Some("Nice Feature"),
                                     Some("Enable advanced configuration options.".into()),
                                     ToggleState::Unselected,
                                     |_, _, _| {},
@@ -844,7 +850,7 @@ impl Component for SwitchField {
                                 "Tooltip without Description",
                                 SwitchField::new(
                                     "switch_field_tooltip_no_desc",
-                                    "Nice Feature",
+                                    Some("Nice Feature"),
                                     None,
                                     ToggleState::Selected,
                                     |_, _, _| {},