Detailed changes
@@ -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"
}
}
]
@@ -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"
}
}
]
@@ -12,6 +12,7 @@ pub struct AiUpsellCard {
pub sign_in_status: SignInStatus,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub user_plan: Option<Plan>,
+ pub tab_index: Option<isize>,
}
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(),
),
@@ -4699,6 +4699,8 @@ pub enum ElementId {
Path(Arc<std::path::Path>),
/// A code location.
CodeLocation(core::panic::Location<'static>),
+ /// A labeled child of an element.
+ NamedChild(Box<ElementId>, 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<T: Into<SharedString>> 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)]
@@ -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<Workspace>,
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<Workspace>,
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<Workspace>,
+ user_store: Entity<UserStore>,
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 = <dyn Fs>::global(cx);
- update_settings_file::<DisableAiSettings>(
- fs,
- cx,
- move |ai_settings: &mut Option<bool>, _| {
- *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 = <dyn Fs>::global(cx);
+ update_settings_file::<DisableAiSettings>(
+ fs,
+ cx,
+ move |ai_settings: &mut Option<bool>, _| {
+ *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(),
+ )
+ }),
)
}
@@ -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 = <dyn Fs>::global(cx);
-
- update_settings_file::<BaseKeymap>(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 = <dyn 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,
};
- 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)
- ),
+ 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);
+ }),
+ ],
)
- .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 = <dyn Fs>::global(cx);
- move |selection, _, cx| {
- let enabled = match selection {
- ToggleState::Selected => true,
- ToggleState::Unselected => false,
- ToggleState::Indeterminate => { return; },
- };
+ .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),
+ );
- update_settings_file::<VimModeSetting>(
- fs.clone(),
- cx,
- move |setting, _| *setting = Some(enabled),
- );
- }
- },
- ))
- .child(render_telemetry_section(cx))
+ fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
+ let fs = <dyn Fs>::global(cx);
+
+ update_settings_file::<BaseKeymap>(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 = <dyn Fs>::global(cx);
+ move |&selection, _, cx| {
+ update_settings_file::<VimModeSetting>(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(&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))
}
@@ -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))
}
@@ -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::<Onboarding>()?;
- 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::<WelcomePage>()?;
- 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<Self>) -> 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::<Onboarding>()?;
+ 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::<WelcomePage>()?;
+ 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<Workspace>,
source: VsCodeSettingsSource,
@@ -412,6 +412,7 @@ where
size: ToggleButtonGroupSize,
button_width: Rems,
selected_index: usize,
+ tab_index: Option<isize>,
}
impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> {
@@ -423,6 +424,7 @@ impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> {
size: ToggleButtonGroupSize::Default,
button_width: rems_from_px(100.),
selected_index: 0,
+ tab_index: None,
}
}
}
@@ -436,6 +438,7 @@ impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS, 2> {
size: ToggleButtonGroupSize::Default,
button_width: rems_from_px(100.),
selected_index: 0,
+ tab_index: None,
}
}
}
@@ -460,6 +463,15 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> ToggleButtonGroup<T
self.selected_index = index;
self
}
+
+ /// Sets the tab index for the toggle button group.
+ /// The tab index is set to the initial value provided, then the
+ /// value is incremented by the number of buttons in the group.
+ pub fn tab_index(mut self, tab_index: &mut isize) -> Self {
+ self.tab_index = Some(*tab_index);
+ *tab_index += (COLS * ROWS) as isize;
+ self
+ }
}
impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
@@ -479,6 +491,9 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> 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))
@@ -19,6 +19,7 @@ pub struct NumericStepper {
/// Whether to reserve space for the reset button.
reserve_space_for_reset: bool,
on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
+ tab_index: Option<isize>,
}
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),
)
}
@@ -424,6 +424,7 @@ pub struct Switch {
label: Option<SharedString>,
key_binding: Option<KeyBinding>,
color: SwitchColor,
+ tab_index: Option<isize>,
}
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<isize>) -> 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<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
+ tab_index: Option<isize>,
}
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({