From cc3b0d419856eb30eca7d8f85c7166085a2aeaf3 Mon Sep 17 00:00:00 2001 From: Andrew Farkas <6060305+HactarCE@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:47:25 -0400 Subject: [PATCH] Onboarding refactor (#39724) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2025-10-07 at 6 57 20 PM Fixes #39347 Release Notes: - Improved onboarding UI by collapsing it to a single page --------- Co-authored-by: dino Co-authored-by: Lukas Wirth Co-authored-by: Mikayla Maki Co-authored-by: Anthony Eid Co-authored-by: Mikayla Maki --- 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 +- .../ui/src/components/button/button_like.rs | 89 ++- .../ui/src/components/button/toggle_button.rs | 64 +- crates/ui/src/components/toggle.rs | 46 +- 15 files changed, 419 insertions(+), 1539 deletions(-) delete mode 100644 crates/onboarding/src/ai_setup_page.rs delete mode 100644 crates/onboarding/src/editing_page.rs diff --git a/Cargo.lock b/Cargo.lock index b908039e5356768fab720780b52451561629216e..32441f83ae6107627b79db07b536ec9943b8c7d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10535,20 +10535,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", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 2611078df14cc618390e6f04432dd0bf113f3885..dfe9920f7136d1410aaa67079f1f45700425d6a5 100644 --- a/assets/keymaps/default-linux.json +++ b/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" diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index f2528766c064ed74207cef9ee27d22cd8d5d8cbd..d5649d9d6c29118113191f689edee6584f26f777 100644 --- a/assets/keymaps/default-macos.json +++ b/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" } diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index a923457b5d73270ffab84fc3a3f5b3cc92ce4e1a..56517b34567db7ab2d35984cb78fa9424c188301 100644 --- a/assets/keymaps/default-windows.json +++ b/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" diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 3581baf4ec62b746a27bd78bade2d9e85ade069a..bd68271c3cc042f98c205148095124cbf9fab89a 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/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(), ), diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 855759279b7c1af177bd445950960b4ee8f8bf2d..aecac1fc770e56990dbf6ac4118d835f25d5766e 100644 --- a/crates/gpui/src/window.rs +++ b/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 = size(px(1024.), px(700.)); +pub(crate) const DEFAULT_WINDOW_SIZE: Size = size(px(1536.), px(864.)); /// Represents the two different phases when dispatching events. #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index f51a04cc7452ec1616401218e01e904039183751..2e9797f717b446177efa08713489aed49892c8c8 100644 --- a/crates/onboarding/Cargo.toml +++ b/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 diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs deleted file mode 100644 index 6acc8aab389c4563f2302ae3a71934676669c130..0000000000000000000000000000000000000000 --- a/crates/onboarding/src/ai_setup_page.rs +++ /dev/null @@ -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, - 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, - 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, - user_store: Entity, - client: Arc, - 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 = ::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, - configuration_view: AnyView, -} - -impl AiConfigurationModal { - fn new( - selected_provider: Arc, - window: &mut Window, - cx: &mut Context, - ) -> 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) { - cx.emit(DismissEvent); - } -} - -impl ModalView for AiConfigurationModal {} - -impl EventEmitter 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) -> 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) -> 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), - ), - ) - }) - } -} diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 99af251dfefcab5fd4dba6f82e02531d3c849433..31547115bd547bbf945f466de232cb9e7dd7724e 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/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"]; @@ -78,6 +82,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) @@ -228,91 +233,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 { @@ -380,8 +381,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 = ::global(cx); @@ -410,12 +411,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)) } diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs deleted file mode 100644 index 4fd968faaff8ae60ca5f862dfd13d8a3562f4c89..0000000000000000000000000000000000000000 --- a/crates/onboarding/src/editing_page.rs +++ /dev/null @@ -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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::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 { - window.with_id(id, |window| { - let optimistic_font_size: gpui::Entity> = 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)) -} diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index ab47eef8b75f632936f8272d5f608d713956599f..70e1524ab4e7d432a5fcbef1e18385bab1320d83 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/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, cx: &mut App) -> Task &'static str { - match self { - SelectedPage::Basics => "Basics", - SelectedPage::Editing => "Editing", - SelectedPage::AiSetup => "AI Setup", - } - } -} - struct Onboarding { workspace: WeakEntity, focus_handle: FocusHandle, - selected_page: SelectedPage, user_store: Entity, 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::(move |_, cx| cx.notify()), @@ -267,228 +240,8 @@ impl Onboarding { }) } - fn set_page( - &mut self, - page: SelectedPage, - clicked: Option<&'static str>, - cx: &mut Context, - ) { - 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, - ) -> [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) -> 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) -> 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) -> 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) -> 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::(move |_, cx| cx.notify()), @@ -814,25 +575,10 @@ impl workspace::SerializableItem for Onboarding { cx: &mut App, ) -> gpui::Task>> { 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, ) -> Option>> { 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> { - SELECT page_number + ) -> Result> { + SELECT item_id FROM onboarding_pages WHERE item_id = ? AND workspace_id = ? } diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 9c4714c6424569c6416051505d8aeca5b026128e..b5daef156849e4f44ce0945631cbc07e26f16bb1 100644 --- a/crates/onboarding/src/welcome.rs +++ b/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) -> 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, + } }) } } diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 5e0026d5c49a27ce0c17dded5a52ce7431291cca..40c75f5918fe1e70c3d52374dce2a463314b5cc7 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/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) -> Self { - Self::new(id).rounding(ButtonLikeRounding::Left) + Self::new(id).rounding(ButtonLikeRounding::LEFT) } pub fn new_rounded_right(id: impl Into) -> Self { - Self::new(id).rounding(ButtonLikeRounding::Right) + Self::new(id).rounding(ButtonLikeRounding::RIGHT) } pub fn new_rounded_all(id: impl Into) -> 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 { diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index 36f1972cf9ad8a9a7eac92e8b2648db78f806347..2a3db701d15d12361ebe623d8d56fa35ae0016a7 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/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 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) }) diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index ae74f76b9cef8075c07dfe45e27b735e98939f8f..8d582c11e77f4469bb959ec656c9d6800603a72e 100644 --- a/crates/ui/src/components/toggle.rs +++ b/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, description: Option, toggle_state: ToggleState, on_click: Arc, @@ -609,14 +609,14 @@ pub struct SwitchField { impl SwitchField { pub fn new( id: impl Into, - label: impl Into, + label: Option>, description: Option, toggle_state: impl Into, 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, |_, _, _| {},