diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index ef5354e82d..81f5c695a2 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1175,7 +1175,8 @@ "bindings": { "ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-2": "onboarding::ActivateEditingPage", - "ctrl-3": "onboarding::ActivateAISetupPage" + "ctrl-3": "onboarding::ActivateAISetupPage", + "ctrl-escape": "onboarding::Finish" } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 3287e50acb..69958fd1f8 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1277,7 +1277,8 @@ "bindings": { "cmd-1": "onboarding::ActivateBasicsPage", "cmd-2": "onboarding::ActivateEditingPage", - "cmd-3": "onboarding::ActivateAISetupPage" + "cmd-3": "onboarding::ActivateAISetupPage", + "cmd-escape": "onboarding::Finish" } } ] diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 2408b6aa37..89a782a7c2 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -12,6 +12,7 @@ pub struct AiUpsellCard { pub sign_in_status: SignInStatus, pub sign_in: Arc, pub user_plan: Option, + pub tab_index: Option, } impl AiUpsellCard { @@ -28,6 +29,7 @@ impl AiUpsellCard { }) .detach_and_log_err(cx); }), + tab_index: None, } } } @@ -112,7 +114,8 @@ impl RenderOnce for AiUpsellCard { .on_click(move |_, _window, cx| { telemetry::event!("Start Trial Clicked", state = "post-sign-in"); cx.open_url(&zed_urls::start_trial_url(cx)) - }), + }) + .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)), ) .child( Label::new("No credit card required") @@ -123,6 +126,7 @@ impl RenderOnce for AiUpsellCard { _ => Button::new("sign_in", "Sign In") .full_width() .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) .on_click({ let callback = self.sign_in.clone(); move |_, window, cx| { @@ -193,6 +197,7 @@ impl Component for AiUpsellCard { sign_in_status: SignInStatus::SignedOut, sign_in: Arc::new(|_, _| {}), user_plan: None, + tab_index: Some(0), } .into_any_element(), ), @@ -202,6 +207,7 @@ impl Component for AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), user_plan: None, + tab_index: Some(1), } .into_any_element(), ), diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 6ebb1cac40..9e4c1c26c5 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4699,6 +4699,8 @@ pub enum ElementId { Path(Arc), /// A code location. CodeLocation(core::panic::Location<'static>), + /// A labeled child of an element. + NamedChild(Box, SharedString), } impl ElementId { @@ -4719,6 +4721,7 @@ impl Display for ElementId { ElementId::Uuid(uuid) => write!(f, "{}", uuid)?, ElementId::Path(path) => write!(f, "{}", path.display())?, ElementId::CodeLocation(location) => write!(f, "{}", location)?, + ElementId::NamedChild(id, name) => write!(f, "{}-{}", id, name)?, } Ok(()) @@ -4809,6 +4812,12 @@ impl From<(&'static str, u32)> for ElementId { } } +impl> From<(ElementId, T)> for ElementId { + fn from((id, name): (ElementId, T)) -> Self { + ElementId::NamedChild(Box::new(id), name.into()) + } +} + /// A rectangle to be rendered in the window at the given position and size. /// Passed as an argument [`Window::paint_quad`]. #[derive(Clone)] diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index b5dda7601f..098907870b 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -1,9 +1,11 @@ use std::sync::Arc; use ai_onboarding::{AiUpsellCard, SignInStatus}; +use client::UserStore; use fs::Fs; use gpui::{ - Action, AnyView, App, DismissEvent, EventEmitter, FocusHandle, Focusable, Window, prelude::*, + Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, + Window, prelude::*, }; use itertools; use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; @@ -14,15 +16,14 @@ use ui::{ prelude::*, tooltip_container, }; use util::ResultExt; -use workspace::ModalView; +use workspace::{ModalView, Workspace}; use zed_actions::agent::OpenSettings; -use crate::Onboarding; - const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"]; fn render_llm_provider_section( - onboarding: &Onboarding, + tab_index: &mut isize, + workspace: WeakEntity, disabled: bool, window: &mut Window, cx: &mut App, @@ -37,10 +38,10 @@ fn render_llm_provider_section( .color(Color::Muted), ), ) - .child(render_llm_provider_card(onboarding, disabled, window, cx)) + .child(render_llm_provider_card(tab_index, workspace, disabled, window, cx)) } -fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { +fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> impl IntoElement { let privacy_badge = || { Badge::new("Privacy") .icon(IconName::ShieldCheck) @@ -98,6 +99,10 @@ fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { .icon_color(Color::Muted) .on_click(|_, _, cx| { cx.open_url("https://zed.dev/docs/ai/privacy-and-security"); + }) + .tab_index({ + *tab_index += 1; + *tab_index - 1 }), ), ), @@ -114,7 +119,8 @@ fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { } fn render_llm_provider_card( - onboarding: &Onboarding, + tab_index: &mut isize, + workspace: WeakEntity, disabled: bool, _: &mut Window, cx: &mut App, @@ -140,6 +146,10 @@ fn render_llm_provider_card( ButtonLike::new(("onboarding-ai-setup-buttons", index)) .size(ButtonSize::Large) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) .child( h_flex() .group(&group_name) @@ -188,7 +198,7 @@ fn render_llm_provider_card( ), ) .on_click({ - let workspace = onboarding.workspace.clone(); + let workspace = workspace.clone(); move |_, window, cx| { workspace .update(cx, |workspace, cx| { @@ -219,57 +229,56 @@ fn render_llm_provider_card( .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( - onboarding: &Onboarding, + workspace: WeakEntity, + user_store: Entity, window: &mut Window, cx: &mut App, ) -> impl IntoElement { + let mut tab_index = 0; let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; - let backdrop = div() - .id("backdrop") - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().editor_background) - .opacity(0.8) - .block_mouse_except_scroll(); - 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 => false, - ToggleState::Selected => true, - }; - - let fs = ::global(cx); - update_settings_file::( - fs, - cx, - move |ai_settings: &mut Option, _| { - *ai_settings = Some(!enabled); - }, - ); - }, - )) - .child(render_privacy_card(is_ai_disabled, cx)) + .child( + SwitchField::new( + "enable_ai", + "Enable AI features", + None, + if is_ai_disabled { + ToggleState::Unselected + } else { + ToggleState::Selected + }, + |&toggle_state, _, cx| { + let fs = ::global(cx); + update_settings_file::( + fs, + cx, + move |ai_settings: &mut Option, _| { + *ai_settings = match toggle_state { + ToggleState::Indeterminate => None, + ToggleState::Unselected => Some(true), + ToggleState::Selected => Some(false), + }; + }, + ); + }, + ) + .tab_index({ + tab_index += 1; + tab_index - 1 + }), + ) + .child(render_privacy_card(&mut tab_index, is_ai_disabled, cx)) .child( v_flex() .mt_2() @@ -277,15 +286,31 @@ pub(crate) fn render_ai_setup_page( .child(AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), - user_plan: onboarding.user_store.read(cx).plan(), + user_plan: user_store.read(cx).plan(), + tab_index: Some({ + tab_index += 1; + tab_index - 1 + }), }) .child(render_llm_provider_section( - onboarding, + &mut tab_index, + workspace, is_ai_disabled, window, cx, )) - .when(is_ai_disabled, |this| this.child(backdrop)), + .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(), + ) + }), ) } diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 21ea74f01c..a4e4028051 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use client::TelemetrySettings; use fs::Fs; -use gpui::{App, IntoElement, Window}; +use gpui::{App, IntoElement}; use settings::{BaseKeymap, Settings, update_settings_file}; use theme::{ Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, @@ -16,7 +16,7 @@ use vim_mode_setting::VimModeSetting; use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; -fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone(); let system_appearance = theme::SystemAppearance::global(cx); let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic { @@ -55,6 +55,7 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement ) }), ) + .tab_index(tab_index) .selected_index(theme_mode as usize) .style(ui::ToggleButtonGroupStyle::Outlined) .button_width(rems_from_px(64.)), @@ -64,10 +65,11 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement h_flex() .gap_4() .justify_between() - .children(render_theme_previews(&theme_selection, cx)), + .children(render_theme_previews(tab_index, &theme_selection, cx)), ); fn render_theme_previews( + tab_index: &mut isize, theme_selection: &ThemeSelection, cx: &mut App, ) -> [impl IntoElement; 3] { @@ -110,12 +112,12 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement let colors = cx.theme().colors(); v_flex() - .id(name.clone()) .w_full() .items_center() .gap_1() .child( h_flex() + .id(name.clone()) .relative() .w_full() .border_2() @@ -128,6 +130,20 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement this.opacity(0.8).hover(|s| s.border_color(colors.border)) } }) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) + .focus(|mut style| { + style.border_color = Some(colors.border_focused); + style + }) + .on_click({ + let theme_name = theme.name.clone(); + move |_, _, cx| { + write_theme_change(theme_name.clone(), theme_mode, cx); + } + }) .map(|this| { if theme_mode == ThemeMode::System { let (light, dark) = ( @@ -151,12 +167,6 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement .color(Color::Muted) .size(LabelSize::Small), ) - .on_click({ - let theme_name = theme.name.clone(); - move |_, _, cx| { - write_theme_change(theme_name.clone(), theme_mode, cx); - } - }) }); theme_previews @@ -187,15 +197,7 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement } } -fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) { - let fs = ::global(cx); - - update_settings_file::(fs, cx, move |setting, _| { - *setting = Some(keymap_base); - }); -} - -fn render_telemetry_section(cx: &App) -> impl IntoElement { +fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement { let fs = ::global(cx); v_flex() @@ -225,7 +227,10 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement { move |setting, _| setting.metrics = Some(enabled), ); }}, - )) + ).tab_index({ + *tab_index += 1; + *tab_index + })) .child(SwitchField::new( "onboarding-telemetry-crash-reports", "Help Fix Zed", @@ -251,10 +256,13 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement { ); } } - )) + ).tab_index({ + *tab_index += 1; + *tab_index + })) } -pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { let base_keymap = match BaseKeymap::get_global(cx) { BaseKeymap::VSCode => Some(0), BaseKeymap::JetBrains => Some(1), @@ -265,67 +273,89 @@ pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl Into BaseKeymap::TextMate | BaseKeymap::None => None, }; + return v_flex().gap_2().child(Label::new("Base Keymap")).child( + ToggleButtonGroup::two_rows( + "base_keymap_selection", + [ + ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| { + write_keymap_base(BaseKeymap::VSCode, cx); + }), + ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| { + write_keymap_base(BaseKeymap::JetBrains, cx); + }), + ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| { + write_keymap_base(BaseKeymap::SublimeText, cx); + }), + ], + [ + ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| { + write_keymap_base(BaseKeymap::Atom, cx); + }), + ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| { + write_keymap_base(BaseKeymap::Emacs, cx); + }), + ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| { + write_keymap_base(BaseKeymap::Cursor, cx); + }), + ], + ) + .when_some(base_keymap, |this, base_keymap| { + this.selected_index(base_keymap) + }) + .tab_index(tab_index) + .button_width(rems_from_px(216.)) + .size(ui::ToggleButtonGroupSize::Medium) + .style(ui::ToggleButtonGroupStyle::Outlined), + ); + + fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |setting, _| { + *setting = Some(keymap_base); + }); + } +} + +fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { + let toggle_state = if VimModeSetting::get_global(cx).0 { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }; + SwitchField::new( + "onboarding-vim-mode", + "Vim Mode", + Some( + "Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back." + .into(), + ), + toggle_state, + { + let fs = ::global(cx); + move |&selection, _, cx| { + update_settings_file::(fs.clone(), cx, move |setting, _| { + *setting = match selection { + ToggleState::Selected => Some(true), + ToggleState::Unselected => Some(false), + ToggleState::Indeterminate => None, + } + }); + } + }, + ) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) +} + +pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement { + let mut tab_index = 0; v_flex() .gap_6() - .child(render_theme_section(window, cx)) - .child( - v_flex().gap_2().child(Label::new("Base Keymap")).child( - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| { - write_keymap_base(BaseKeymap::VSCode, cx); - }), - ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| { - write_keymap_base(BaseKeymap::JetBrains, cx); - }), - ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| { - write_keymap_base(BaseKeymap::SublimeText, cx); - }), - ], - [ - ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| { - write_keymap_base(BaseKeymap::Atom, cx); - }), - ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| { - write_keymap_base(BaseKeymap::Emacs, cx); - }), - ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| { - write_keymap_base(BaseKeymap::Cursor, cx); - }), - ], - ) - .when_some(base_keymap, |this, base_keymap| this.selected_index(base_keymap)) - .button_width(rems_from_px(216.)) - .size(ui::ToggleButtonGroupSize::Medium) - .style(ui::ToggleButtonGroupStyle::Outlined) - ), - ) - .child(SwitchField::new( - "onboarding-vim-mode", - "Vim Mode", - Some("Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.".into()), - if VimModeSetting::get_global(cx).0 { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - { - let fs = ::global(cx); - 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 = Some(enabled), - ); - } - }, - )) - .child(render_telemetry_section(cx)) + .child(render_theme_section(&mut tab_index, cx)) + .child(render_base_keymap_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 index 6dd272745a..a8f0265b6b 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -171,6 +171,7 @@ fn write_format_on_save(format_on_save: bool, cx: &mut App) { } fn render_setting_import_button( + tab_index: isize, label: SharedString, icon_name: IconName, action: &dyn Action, @@ -182,6 +183,7 @@ fn render_setting_import_button( .full_width() .style(ButtonStyle::Outlined) .size(ButtonSize::Large) + .tab_index(tab_index) .child( h_flex() .w_full() @@ -214,7 +216,7 @@ fn render_setting_import_button( ) } -fn render_import_settings_section(cx: &App) -> impl IntoElement { +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] = [ ( @@ -232,7 +234,8 @@ fn render_import_settings_section(cx: &App) -> impl IntoElement { ]; let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| { - render_setting_import_button(label, icon_name, action, imported) + *tab_index += 1; + render_setting_import_button(*tab_index - 1, label, icon_name, action, imported) }); v_flex() @@ -248,7 +251,11 @@ fn render_import_settings_section(cx: &App) -> impl IntoElement { .child(h_flex().w_full().gap_4().child(vscode).child(cursor)) } -fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement { +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(); @@ -294,6 +301,10 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl .style(ButtonStyle::Outlined) .size(ButtonSize::Medium) .full_width() + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) .child( h_flex() .w_full() @@ -325,7 +336,11 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl write_ui_font_size(ui_font_size + px(1.), cx); }, ) - .style(ui::NumericStepperStyle::Outlined), + .style(ui::NumericStepperStyle::Outlined) + .tab_index({ + *tab_index += 2; + *tab_index - 2 + }), ), ), ) @@ -350,6 +365,10 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl .style(ButtonStyle::Outlined) .size(ButtonSize::Medium) .full_width() + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) .child( h_flex() .w_full() @@ -381,7 +400,11 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl write_buffer_font_size(buffer_font_size + px(1.), cx); }, ) - .style(ui::NumericStepperStyle::Outlined), + .style(ui::NumericStepperStyle::Outlined) + .tab_index({ + *tab_index += 2; + *tab_index - 2 + }), ), ), ) @@ -556,13 +579,17 @@ fn font_picker( .max_height(Some(rems(20.).into())) } -fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_popular_settings_section( + tab_index: &mut isize, + window: &mut Window, + cx: &mut App, +) -> impl IntoElement { const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠."; v_flex() .gap_5() .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) - .child(render_font_customization_section(window, cx)) + .child(render_font_customization_section(tab_index, window, cx)) .child( SwitchField::new( "onboarding-font-ligatures", @@ -577,47 +604,69 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In write_font_ligatures(toggle_state == &ToggleState::Selected, 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| { - write_format_on_save(toggle_state == &ToggleState::Selected, cx); - }, - )) - .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| { - write_inlay_hints(toggle_state == &ToggleState::Selected, cx); - }, - )) - .child(SwitchField::new( - "onboarding-git-blame-switch", - "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| { - set_git_blame(toggle_state == &ToggleState::Selected, cx); - }, - )) + .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| { + write_format_on_save(toggle_state == &ToggleState::Selected, 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| { + write_inlay_hints(toggle_state == &ToggleState::Selected, cx); + }, + ) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }), + ) + .child( + SwitchField::new( + "onboarding-git-blame-switch", + "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| { + set_git_blame(toggle_state == &ToggleState::Selected, cx); + }, + ) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }), + ) .child( h_flex() .items_start() @@ -648,6 +697,7 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In ShowMinimap::Always => 1, ShowMinimap::Never => 2, }) + .tab_index(tab_index) .style(ToggleButtonGroupStyle::Outlined) .button_width(ui::rems_from_px(64.)), ), @@ -655,8 +705,9 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In } pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { + let mut tab_index = 0; v_flex() .gap_4() - .child(render_import_settings_section(cx)) - .child(render_popular_settings_section(window, cx)) + .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 42e75ac2f8..c4d2b6847c 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -75,6 +75,8 @@ actions!( ActivateEditingPage, /// Activates the AI Setup page. ActivateAISetupPage, + /// Finish the onboarding process. + Finish, ] ); @@ -261,40 +263,6 @@ impl Onboarding { cx.emit(ItemEvent::UpdateTab); } - fn go_to_welcome_page(&self, cx: &mut App) { - with_active_or_new_workspace(cx, |workspace, window, cx| { - let Some((onboarding_id, onboarding_idx)) = workspace - .active_pane() - .read(cx) - .items() - .enumerate() - .find_map(|(idx, item)| { - let _ = item.downcast::()?; - Some((item.item_id(), idx)) - }) - else { - return; - }; - - workspace.active_pane().update(cx, |pane, cx| { - // Get the index here to get around the borrow checker - let idx = pane.items().enumerate().find_map(|(idx, item)| { - let _ = item.downcast::()?; - Some(idx) - }); - - if let Some(idx) = idx { - pane.activate_item(idx, true, true, window, cx); - } else { - let item = Box::new(WelcomePage::new(window, cx)); - pane.add_item(item, true, true, Some(onboarding_idx), window, cx); - } - - pane.remove_item(onboarding_id, false, false, window, cx); - }); - }); - } - fn render_nav_buttons( &mut self, window: &mut Window, @@ -401,6 +369,13 @@ impl Onboarding { .children(self.render_nav_buttons(window, cx)), ) .map(|this| { + let keybinding = KeyBinding::for_action_in( + &Finish, + &self.focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))); if ai_setup_page { this.child( ButtonLike::new("start_building") @@ -412,23 +387,37 @@ impl Onboarding { .w_full() .justify_between() .child(Label::new("Start Building")) - .child( - Icon::new(IconName::Check) - .size(IconSize::Small), - ), + .child(keybinding.map_or_else( + || { + Icon::new(IconName::Check) + .size(IconSize::Small) + .into_any_element() + }, + IntoElement::into_any_element, + )), ) - .on_click(cx.listener(|this, _, _, cx| { - this.go_to_welcome_page(cx); - })), + .on_click(|_, window, cx| { + window.dispatch_action(Finish.boxed_clone(), cx); + }), ) } else { this.child( ButtonLike::new("skip_all") .size(ButtonSize::Medium) - .child(Label::new("Skip All").ml_1()) - .on_click(cx.listener(|this, _, _, cx| { - this.go_to_welcome_page(cx); - })), + .child( + h_flex() + .ml_1() + .w_full() + .justify_between() + .child(Label::new("Skip All")) + .child(keybinding.map_or_else( + || gpui::Empty.into_any_element(), + IntoElement::into_any_element, + )), + ) + .on_click(|_, window, cx| { + window.dispatch_action(Finish.boxed_clone(), cx); + }), ) } }), @@ -464,17 +453,23 @@ impl Onboarding { fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { match self.selected_page { - SelectedPage::Basics => { - crate::basics_page::render_basics_page(window, cx).into_any_element() - } + 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, window, cx).into_any_element() - } + SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page( + self.workspace.clone(), + self.user_store.clone(), + window, + cx, + ) + .into_any_element(), } } + + fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { + go_to_welcome_page(cx); + } } impl Render for Onboarding { @@ -484,11 +479,13 @@ impl Render for Onboarding { .key_context({ let mut ctx = KeyContext::new_with_defaults(); ctx.add("Onboarding"); + ctx.add("menu"); ctx }) .track_focus(&self.focus_handle) .size_full() .bg(cx.theme().colors().editor_background) + .on_action(Self::on_finish) .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { this.set_page(SelectedPage::Basics, cx); })) @@ -498,6 +495,14 @@ impl Render for Onboarding { .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| { this.set_page(SelectedPage::AiSetup, cx); })) + .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| { + window.focus_next(); + cx.notify(); + })) + .on_action(cx.listener(|_, _: &menu::SelectPrevious, window, cx| { + window.focus_prev(); + cx.notify(); + })) .child( h_flex() .max_w(rems_from_px(1100.)) @@ -561,6 +566,40 @@ impl Item for Onboarding { } } +fn go_to_welcome_page(cx: &mut App) { + with_active_or_new_workspace(cx, |workspace, window, cx| { + let Some((onboarding_id, onboarding_idx)) = workspace + .active_pane() + .read(cx) + .items() + .enumerate() + .find_map(|(idx, item)| { + let _ = item.downcast::()?; + Some((item.item_id(), idx)) + }) + else { + return; + }; + + workspace.active_pane().update(cx, |pane, cx| { + // Get the index here to get around the borrow checker + let idx = pane.items().enumerate().find_map(|(idx, item)| { + let _ = item.downcast::()?; + Some(idx) + }); + + if let Some(idx) = idx { + pane.activate_item(idx, true, true, window, cx); + } else { + let item = Box::new(WelcomePage::new(window, cx)); + pane.add_item(item, true, true, Some(onboarding_idx), window, cx); + } + + pane.remove_item(onboarding_id, false, false, window, cx); + }); + }); +} + pub async fn handle_import_vscode_settings( workspace: WeakEntity, source: VsCodeSettingsSource, diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index d4d47da9b6..6fbf834667 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -412,6 +412,7 @@ where size: ToggleButtonGroupSize, button_width: Rems, selected_index: usize, + tab_index: Option, } impl ToggleButtonGroup { @@ -423,6 +424,7 @@ impl ToggleButtonGroup { size: ToggleButtonGroupSize::Default, button_width: rems_from_px(100.), selected_index: 0, + tab_index: None, } } } @@ -436,6 +438,7 @@ impl ToggleButtonGroup { size: ToggleButtonGroupSize::Default, button_width: rems_from_px(100.), selected_index: 0, + tab_index: None, } } } @@ -460,6 +463,15 @@ impl ToggleButtonGroup Self { + self.tab_index = Some(*tab_index); + *tab_index += (COLS * ROWS) as isize; + self + } } impl RenderOnce @@ -479,6 +491,9 @@ impl RenderOnce let entry_index = row_index * COLS + col_index; ButtonLike::new((self.group_name, entry_index)) + .when_some(self.tab_index, |this, tab_index| { + this.tab_index(tab_index + entry_index as isize) + }) .when(entry_index == self.selected_index || selected, |this| { this.toggle_state(true) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs index 0ec7111a02..2ddb86d9a0 100644 --- a/crates/ui/src/components/numeric_stepper.rs +++ b/crates/ui/src/components/numeric_stepper.rs @@ -19,6 +19,7 @@ pub struct NumericStepper { /// Whether to reserve space for the reset button. reserve_space_for_reset: bool, on_reset: Option>, + tab_index: Option, } impl NumericStepper { @@ -36,6 +37,7 @@ impl NumericStepper { on_increment: Box::new(on_increment), reserve_space_for_reset: false, on_reset: None, + tab_index: None, } } @@ -56,6 +58,11 @@ impl NumericStepper { self.on_reset = Some(Box::new(on_reset)); self } + + pub fn tab_index(mut self, tab_index: isize) -> Self { + self.tab_index = Some(tab_index); + self + } } impl RenderOnce for NumericStepper { @@ -64,6 +71,7 @@ impl RenderOnce for NumericStepper { let icon_size = IconSize::Small; let is_outlined = matches!(self.style, NumericStepperStyle::Outlined); + let mut tab_index = self.tab_index; h_flex() .id(self.id) @@ -74,6 +82,10 @@ impl RenderOnce for NumericStepper { IconButton::new("reset", IconName::RotateCcw) .shape(shape) .icon_size(icon_size) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1) + }) .on_click(on_reset), ) } else if self.reserve_space_for_reset { @@ -113,6 +125,12 @@ impl RenderOnce for NumericStepper { .border_r_1() .border_color(cx.theme().colors().border_variant) .child(Icon::new(IconName::Dash).size(IconSize::Small)) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1).focus(|style| { + style.bg(cx.theme().colors().element_hover) + }) + }) .on_click(self.on_decrement), ) } else { @@ -120,6 +138,10 @@ impl RenderOnce for NumericStepper { IconButton::new("decrement", IconName::Dash) .shape(shape) .icon_size(icon_size) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1) + }) .on_click(self.on_decrement), ) } @@ -137,6 +159,12 @@ impl RenderOnce for NumericStepper { .border_l_1() .border_color(cx.theme().colors().border_variant) .child(Icon::new(IconName::Plus).size(IconSize::Small)) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1).focus(|style| { + style.bg(cx.theme().colors().element_hover) + }) + }) .on_click(self.on_increment), ) } else { @@ -144,6 +172,10 @@ impl RenderOnce for NumericStepper { IconButton::new("increment", IconName::Dash) .shape(shape) .icon_size(icon_size) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1) + }) .on_click(self.on_increment), ) } diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index a3a3f23889..53df4767b0 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -424,6 +424,7 @@ pub struct Switch { label: Option, key_binding: Option, color: SwitchColor, + tab_index: Option, } impl Switch { @@ -437,6 +438,7 @@ impl Switch { label: None, key_binding: None, color: SwitchColor::default(), + tab_index: None, } } @@ -472,6 +474,11 @@ impl Switch { self.key_binding = key_binding.into(); self } + + pub fn tab_index(mut self, tab_index: impl Into) -> Self { + self.tab_index = Some(tab_index.into()); + self + } } impl RenderOnce for Switch { @@ -501,6 +508,20 @@ impl RenderOnce for Switch { .w(DynamicSpacing::Base32.rems(cx)) .h(DynamicSpacing::Base20.rems(cx)) .group(group_id.clone()) + .border_1() + .p(px(1.0)) + .border_color(cx.theme().colors().border_transparent) + .rounded_full() + .id((self.id.clone(), "switch")) + .when_some( + self.tab_index.filter(|_| !self.disabled), + |this, tab_index| { + this.tab_index(tab_index).focus(|mut style| { + style.border_color = Some(cx.theme().colors().border_focused); + style + }) + }, + ) .child( h_flex() .when(is_on, |on| on.justify_end()) @@ -572,6 +593,7 @@ pub struct SwitchField { disabled: bool, color: SwitchColor, tooltip: Option AnyView>>, + tab_index: Option, } impl SwitchField { @@ -591,6 +613,7 @@ impl SwitchField { disabled: false, color: SwitchColor::Accent, tooltip: None, + tab_index: None, } } @@ -615,14 +638,33 @@ impl SwitchField { self.tooltip = Some(Rc::new(tooltip)); self } + + pub fn tab_index(mut self, tab_index: isize) -> Self { + self.tab_index = Some(tab_index); + self + } } impl RenderOnce for SwitchField { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let tooltip = self.tooltip; + let tooltip = self.tooltip.map(|tooltip_fn| { + h_flex() + .gap_0p5() + .child(Label::new(self.label.clone())) + .child( + IconButton::new("tooltip_button", IconName::Info) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .shape(crate::IconButtonShape::Square) + .tooltip({ + let tooltip = tooltip_fn.clone(); + move |window, cx| tooltip(window, cx) + }), + ) + }); h_flex() - .id(SharedString::from(format!("{}-container", self.id))) + .id((self.id.clone(), "container")) .when(!self.disabled, |this| { this.hover(|this| this.cursor_pointer()) }) @@ -630,25 +672,11 @@ impl RenderOnce for SwitchField { .gap_4() .justify_between() .flex_wrap() - .child(match (&self.description, &tooltip) { + .child(match (&self.description, tooltip) { (Some(description), Some(tooltip)) => v_flex() .gap_0p5() .max_w_5_6() - .child( - h_flex() - .gap_0p5() - .child(Label::new(self.label.clone())) - .child( - IconButton::new("tooltip_button", IconName::Info) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .shape(crate::IconButtonShape::Square) - .tooltip({ - let tooltip = tooltip.clone(); - move |window, cx| tooltip(window, cx) - }), - ), - ) + .child(tooltip) .child(Label::new(description.clone()).color(Color::Muted)) .into_any_element(), (Some(description), None) => v_flex() @@ -657,35 +685,23 @@ impl RenderOnce for SwitchField { .child(Label::new(self.label.clone())) .child(Label::new(description.clone()).color(Color::Muted)) .into_any_element(), - (None, Some(tooltip)) => h_flex() - .gap_0p5() - .child(Label::new(self.label.clone())) - .child( - IconButton::new("tooltip_button", IconName::Info) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .shape(crate::IconButtonShape::Square) - .tooltip({ - let tooltip = tooltip.clone(); - move |window, cx| tooltip(window, cx) - }), - ) - .into_any_element(), + (None, Some(tooltip)) => tooltip.into_any_element(), (None, None) => Label::new(self.label.clone()).into_any_element(), }) .child( - Switch::new( - SharedString::from(format!("{}-switch", self.id)), - self.toggle_state, - ) - .color(self.color) - .disabled(self.disabled) - .on_click({ - let on_click = self.on_click.clone(); - move |state, window, cx| { - (on_click)(state, window, cx); - } - }), + Switch::new((self.id.clone(), "switch"), self.toggle_state) + .color(self.color) + .disabled(self.disabled) + .when_some( + self.tab_index.filter(|_| !self.disabled), + |this, tab_index| this.tab_index(tab_index), + ) + .on_click({ + let on_click = self.on_click.clone(); + move |state, window, cx| { + (on_click)(state, window, cx); + } + }), ) .when(!self.disabled, |this| { this.on_click({