onboarding: Wire up tab index (#35659)

Closes #ISSUE

Allows tabbing through everything in all three pages. Until #35075 is
merged it is not possible to actually "click" tab focused buttons with
the keyboard.

Additionally adds an action `onboarding::Finish` and displays the
keybind. The action corresponds to both the "Skip all" and "Start
Building" buttons, with the keybind displayed similar to how it is for
the page nav buttons

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
This commit is contained in:
Ben Kunkle 2025-08-05 14:48:15 -05:00 committed by GitHub
parent 0b5592d788
commit 6b77654f66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 505 additions and 280 deletions

View file

@ -1175,7 +1175,8 @@
"bindings": { "bindings": {
"ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage", "ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage" "ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-escape": "onboarding::Finish"
} }
} }
] ]

View file

@ -1277,7 +1277,8 @@
"bindings": { "bindings": {
"cmd-1": "onboarding::ActivateBasicsPage", "cmd-1": "onboarding::ActivateBasicsPage",
"cmd-2": "onboarding::ActivateEditingPage", "cmd-2": "onboarding::ActivateEditingPage",
"cmd-3": "onboarding::ActivateAISetupPage" "cmd-3": "onboarding::ActivateAISetupPage",
"cmd-escape": "onboarding::Finish"
} }
} }
] ]

View file

@ -12,6 +12,7 @@ pub struct AiUpsellCard {
pub sign_in_status: SignInStatus, pub sign_in_status: SignInStatus,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>, pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub user_plan: Option<Plan>, pub user_plan: Option<Plan>,
pub tab_index: Option<isize>,
} }
impl AiUpsellCard { impl AiUpsellCard {
@ -28,6 +29,7 @@ impl AiUpsellCard {
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
}), }),
tab_index: None,
} }
} }
} }
@ -112,7 +114,8 @@ impl RenderOnce for AiUpsellCard {
.on_click(move |_, _window, cx| { .on_click(move |_, _window, cx| {
telemetry::event!("Start Trial Clicked", state = "post-sign-in"); telemetry::event!("Start Trial Clicked", state = "post-sign-in");
cx.open_url(&zed_urls::start_trial_url(cx)) cx.open_url(&zed_urls::start_trial_url(cx))
}), })
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
) )
.child( .child(
Label::new("No credit card required") Label::new("No credit card required")
@ -123,6 +126,7 @@ impl RenderOnce for AiUpsellCard {
_ => Button::new("sign_in", "Sign In") _ => Button::new("sign_in", "Sign In")
.full_width() .full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent)) .style(ButtonStyle::Tinted(ui::TintColor::Accent))
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
.on_click({ .on_click({
let callback = self.sign_in.clone(); let callback = self.sign_in.clone();
move |_, window, cx| { move |_, window, cx| {
@ -193,6 +197,7 @@ impl Component for AiUpsellCard {
sign_in_status: SignInStatus::SignedOut, sign_in_status: SignInStatus::SignedOut,
sign_in: Arc::new(|_, _| {}), sign_in: Arc::new(|_, _| {}),
user_plan: None, user_plan: None,
tab_index: Some(0),
} }
.into_any_element(), .into_any_element(),
), ),
@ -202,6 +207,7 @@ impl Component for AiUpsellCard {
sign_in_status: SignInStatus::SignedIn, sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}), sign_in: Arc::new(|_, _| {}),
user_plan: None, user_plan: None,
tab_index: Some(1),
} }
.into_any_element(), .into_any_element(),
), ),

View file

@ -4699,6 +4699,8 @@ pub enum ElementId {
Path(Arc<std::path::Path>), Path(Arc<std::path::Path>),
/// A code location. /// A code location.
CodeLocation(core::panic::Location<'static>), CodeLocation(core::panic::Location<'static>),
/// A labeled child of an element.
NamedChild(Box<ElementId>, SharedString),
} }
impl ElementId { impl ElementId {
@ -4719,6 +4721,7 @@ impl Display for ElementId {
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?, ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
ElementId::Path(path) => write!(f, "{}", path.display())?, ElementId::Path(path) => write!(f, "{}", path.display())?,
ElementId::CodeLocation(location) => write!(f, "{}", location)?, ElementId::CodeLocation(location) => write!(f, "{}", location)?,
ElementId::NamedChild(id, name) => write!(f, "{}-{}", id, name)?,
} }
Ok(()) 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. /// A rectangle to be rendered in the window at the given position and size.
/// Passed as an argument [`Window::paint_quad`]. /// Passed as an argument [`Window::paint_quad`].
#[derive(Clone)] #[derive(Clone)]

View file

@ -1,9 +1,11 @@
use std::sync::Arc; use std::sync::Arc;
use ai_onboarding::{AiUpsellCard, SignInStatus}; use ai_onboarding::{AiUpsellCard, SignInStatus};
use client::UserStore;
use fs::Fs; use fs::Fs;
use gpui::{ 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 itertools;
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
@ -14,15 +16,14 @@ use ui::{
prelude::*, tooltip_container, prelude::*, tooltip_container,
}; };
use util::ResultExt; use util::ResultExt;
use workspace::ModalView; use workspace::{ModalView, Workspace};
use zed_actions::agent::OpenSettings; use zed_actions::agent::OpenSettings;
use crate::Onboarding;
const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"]; const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"];
fn render_llm_provider_section( fn render_llm_provider_section(
onboarding: &Onboarding, tab_index: &mut isize,
workspace: WeakEntity<Workspace>,
disabled: bool, disabled: bool,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
@ -37,10 +38,10 @@ fn render_llm_provider_section(
.color(Color::Muted), .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 = || { let privacy_badge = || {
Badge::new("Privacy") Badge::new("Privacy")
.icon(IconName::ShieldCheck) .icon(IconName::ShieldCheck)
@ -98,6 +99,10 @@ fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement {
.icon_color(Color::Muted) .icon_color(Color::Muted)
.on_click(|_, _, cx| { .on_click(|_, _, cx| {
cx.open_url("https://zed.dev/docs/ai/privacy-and-security"); 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( fn render_llm_provider_card(
onboarding: &Onboarding, tab_index: &mut isize,
workspace: WeakEntity<Workspace>,
disabled: bool, disabled: bool,
_: &mut Window, _: &mut Window,
cx: &mut App, cx: &mut App,
@ -140,6 +146,10 @@ fn render_llm_provider_card(
ButtonLike::new(("onboarding-ai-setup-buttons", index)) ButtonLike::new(("onboarding-ai-setup-buttons", index))
.size(ButtonSize::Large) .size(ButtonSize::Large)
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.child( .child(
h_flex() h_flex()
.group(&group_name) .group(&group_name)
@ -188,7 +198,7 @@ fn render_llm_provider_card(
), ),
) )
.on_click({ .on_click({
let workspace = onboarding.workspace.clone(); let workspace = workspace.clone();
move |_, window, cx| { move |_, window, cx| {
workspace workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
@ -219,57 +229,56 @@ fn render_llm_provider_card(
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.on_click(|_event, window, cx| { .on_click(|_event, window, cx| {
window.dispatch_action(OpenSettings.boxed_clone(), cx) window.dispatch_action(OpenSettings.boxed_clone(), cx)
})
.tab_index({
*tab_index += 1;
*tab_index - 1
}), }),
) )
} }
pub(crate) fn render_ai_setup_page( pub(crate) fn render_ai_setup_page(
onboarding: &Onboarding, workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> impl IntoElement { ) -> impl IntoElement {
let mut tab_index = 0;
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; 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() v_flex()
.gap_2() .gap_2()
.child(SwitchField::new( .child(
"enable_ai", SwitchField::new(
"Enable AI features", "enable_ai",
None, "Enable AI features",
if is_ai_disabled { None,
ToggleState::Unselected if is_ai_disabled {
} else { ToggleState::Unselected
ToggleState::Selected } else {
}, ToggleState::Selected
|toggle_state, _, cx| { },
let enabled = match toggle_state { |&toggle_state, _, cx| {
ToggleState::Indeterminate => { let fs = <dyn Fs>::global(cx);
return; update_settings_file::<DisableAiSettings>(
} fs,
ToggleState::Unselected => false, cx,
ToggleState::Selected => true, move |ai_settings: &mut Option<bool>, _| {
}; *ai_settings = match toggle_state {
ToggleState::Indeterminate => None,
let fs = <dyn Fs>::global(cx); ToggleState::Unselected => Some(true),
update_settings_file::<DisableAiSettings>( ToggleState::Selected => Some(false),
fs, };
cx, },
move |ai_settings: &mut Option<bool>, _| { );
*ai_settings = Some(!enabled); },
}, )
); .tab_index({
}, tab_index += 1;
)) tab_index - 1
.child(render_privacy_card(is_ai_disabled, cx)) }),
)
.child(render_privacy_card(&mut tab_index, is_ai_disabled, cx))
.child( .child(
v_flex() v_flex()
.mt_2() .mt_2()
@ -277,15 +286,31 @@ pub(crate) fn render_ai_setup_page(
.child(AiUpsellCard { .child(AiUpsellCard {
sign_in_status: SignInStatus::SignedIn, sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}), 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( .child(render_llm_provider_section(
onboarding, &mut tab_index,
workspace,
is_ai_disabled, is_ai_disabled,
window, window,
cx, 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(),
)
}),
) )
} }

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use client::TelemetrySettings; use client::TelemetrySettings;
use fs::Fs; use fs::Fs;
use gpui::{App, IntoElement, Window}; use gpui::{App, IntoElement};
use settings::{BaseKeymap, Settings, update_settings_file}; use settings::{BaseKeymap, Settings, update_settings_file};
use theme::{ use theme::{
Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection,
@ -16,7 +16,7 @@ use vim_mode_setting::VimModeSetting;
use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; 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 theme_selection = ThemeSettings::get_global(cx).theme_selection.clone();
let system_appearance = theme::SystemAppearance::global(cx); let system_appearance = theme::SystemAppearance::global(cx);
let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic { 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) .selected_index(theme_mode as usize)
.style(ui::ToggleButtonGroupStyle::Outlined) .style(ui::ToggleButtonGroupStyle::Outlined)
.button_width(rems_from_px(64.)), .button_width(rems_from_px(64.)),
@ -64,10 +65,11 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement
h_flex() h_flex()
.gap_4() .gap_4()
.justify_between() .justify_between()
.children(render_theme_previews(&theme_selection, cx)), .children(render_theme_previews(tab_index, &theme_selection, cx)),
); );
fn render_theme_previews( fn render_theme_previews(
tab_index: &mut isize,
theme_selection: &ThemeSelection, theme_selection: &ThemeSelection,
cx: &mut App, cx: &mut App,
) -> [impl IntoElement; 3] { ) -> [impl IntoElement; 3] {
@ -110,12 +112,12 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement
let colors = cx.theme().colors(); let colors = cx.theme().colors();
v_flex() v_flex()
.id(name.clone())
.w_full() .w_full()
.items_center() .items_center()
.gap_1() .gap_1()
.child( .child(
h_flex() h_flex()
.id(name.clone())
.relative() .relative()
.w_full() .w_full()
.border_2() .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)) 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| { .map(|this| {
if theme_mode == ThemeMode::System { if theme_mode == ThemeMode::System {
let (light, dark) = ( let (light, dark) = (
@ -151,12 +167,6 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement
.color(Color::Muted) .color(Color::Muted)
.size(LabelSize::Small), .size(LabelSize::Small),
) )
.on_click({
let theme_name = theme.name.clone();
move |_, _, cx| {
write_theme_change(theme_name.clone(), theme_mode, cx);
}
})
}); });
theme_previews 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) { fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
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 {
let fs = <dyn Fs>::global(cx); let fs = <dyn Fs>::global(cx);
v_flex() v_flex()
@ -225,7 +227,10 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement {
move |setting, _| setting.metrics = Some(enabled), move |setting, _| setting.metrics = Some(enabled),
); );
}}, }},
)) ).tab_index({
*tab_index += 1;
*tab_index
}))
.child(SwitchField::new( .child(SwitchField::new(
"onboarding-telemetry-crash-reports", "onboarding-telemetry-crash-reports",
"Help Fix Zed", "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) { let base_keymap = match BaseKeymap::get_global(cx) {
BaseKeymap::VSCode => Some(0), BaseKeymap::VSCode => Some(0),
BaseKeymap::JetBrains => Some(1), 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, 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 = <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() v_flex()
.gap_6() .gap_6()
.child(render_theme_section(window, cx)) .child(render_theme_section(&mut tab_index, cx))
.child( .child(render_base_keymap_section(&mut tab_index, cx))
v_flex().gap_2().child(Label::new("Base Keymap")).child( .child(render_vim_mode_switch(&mut tab_index, cx))
ToggleButtonGroup::two_rows( .child(render_telemetry_section(&mut tab_index, cx))
"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 = <dyn Fs>::global(cx);
move |selection, _, cx| {
let enabled = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
ToggleState::Indeterminate => { return; },
};
update_settings_file::<VimModeSetting>(
fs.clone(),
cx,
move |setting, _| *setting = Some(enabled),
);
}
},
))
.child(render_telemetry_section(cx))
} }

View file

@ -171,6 +171,7 @@ fn write_format_on_save(format_on_save: bool, cx: &mut App) {
} }
fn render_setting_import_button( fn render_setting_import_button(
tab_index: isize,
label: SharedString, label: SharedString,
icon_name: IconName, icon_name: IconName,
action: &dyn Action, action: &dyn Action,
@ -182,6 +183,7 @@ fn render_setting_import_button(
.full_width() .full_width()
.style(ButtonStyle::Outlined) .style(ButtonStyle::Outlined)
.size(ButtonSize::Large) .size(ButtonSize::Large)
.tab_index(tab_index)
.child( .child(
h_flex() h_flex()
.w_full() .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 import_state = SettingsImportState::global(cx);
let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [ 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)| { 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() 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)) .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 theme_settings = ThemeSettings::get_global(cx);
let ui_font_size = theme_settings.ui_font_size(cx); let ui_font_size = theme_settings.ui_font_size(cx);
let ui_font_family = theme_settings.ui_font.family.clone(); 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) .style(ButtonStyle::Outlined)
.size(ButtonSize::Medium) .size(ButtonSize::Medium)
.full_width() .full_width()
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.child( .child(
h_flex() h_flex()
.w_full() .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); 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) .style(ButtonStyle::Outlined)
.size(ButtonSize::Medium) .size(ButtonSize::Medium)
.full_width() .full_width()
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.child( .child(
h_flex() h_flex()
.w_full() .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); 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())) .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 ≠."; 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() v_flex()
.gap_5() .gap_5()
.child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) .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( .child(
SwitchField::new( SwitchField::new(
"onboarding-font-ligatures", "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); write_font_ligatures(toggle_state == &ToggleState::Selected, cx);
}, },
) )
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.tooltip(Tooltip::text(LIGATURE_TOOLTIP)), .tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
) )
.child(SwitchField::new( .child(
"onboarding-format-on-save", SwitchField::new(
"Format on Save", "onboarding-format-on-save",
Some("Format code automatically when saving.".into()), "Format on Save",
if read_format_on_save(cx) { Some("Format code automatically when saving.".into()),
ui::ToggleState::Selected if read_format_on_save(cx) {
} else { ui::ToggleState::Selected
ui::ToggleState::Unselected } else {
}, ui::ToggleState::Unselected
|toggle_state, _, cx| { },
write_format_on_save(toggle_state == &ToggleState::Selected, cx); |toggle_state, _, cx| {
}, write_format_on_save(toggle_state == &ToggleState::Selected, cx);
)) },
.child(SwitchField::new( )
"onboarding-enable-inlay-hints", .tab_index({
"Inlay Hints", *tab_index += 1;
Some("See parameter names for function and method calls inline.".into()), *tab_index - 1
if read_inlay_hints(cx) { }),
ui::ToggleState::Selected )
} else { .child(
ui::ToggleState::Unselected SwitchField::new(
}, "onboarding-enable-inlay-hints",
|toggle_state, _, cx| { "Inlay Hints",
write_inlay_hints(toggle_state == &ToggleState::Selected, cx); Some("See parameter names for function and method calls inline.".into()),
}, if read_inlay_hints(cx) {
)) ui::ToggleState::Selected
.child(SwitchField::new( } else {
"onboarding-git-blame-switch", ui::ToggleState::Unselected
"Git Blame", },
Some("See who committed each line on a given file.".into()), |toggle_state, _, cx| {
if read_git_blame(cx) { write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
ui::ToggleState::Selected },
} else { )
ui::ToggleState::Unselected .tab_index({
}, *tab_index += 1;
|toggle_state, _, cx| { *tab_index - 1
set_git_blame(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);
},
)
.tab_index({
*tab_index += 1;
*tab_index - 1
}),
)
.child( .child(
h_flex() h_flex()
.items_start() .items_start()
@ -648,6 +697,7 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In
ShowMinimap::Always => 1, ShowMinimap::Always => 1,
ShowMinimap::Never => 2, ShowMinimap::Never => 2,
}) })
.tab_index(tab_index)
.style(ToggleButtonGroupStyle::Outlined) .style(ToggleButtonGroupStyle::Outlined)
.button_width(ui::rems_from_px(64.)), .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 { pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
let mut tab_index = 0;
v_flex() v_flex()
.gap_4() .gap_4()
.child(render_import_settings_section(cx)) .child(render_import_settings_section(&mut tab_index, cx))
.child(render_popular_settings_section(window, cx)) .child(render_popular_settings_section(&mut tab_index, window, cx))
} }

View file

@ -75,6 +75,8 @@ actions!(
ActivateEditingPage, ActivateEditingPage,
/// Activates the AI Setup page. /// Activates the AI Setup page.
ActivateAISetupPage, ActivateAISetupPage,
/// Finish the onboarding process.
Finish,
] ]
); );
@ -261,40 +263,6 @@ impl Onboarding {
cx.emit(ItemEvent::UpdateTab); 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( fn render_nav_buttons(
&mut self, &mut self,
window: &mut Window, window: &mut Window,
@ -401,6 +369,13 @@ impl Onboarding {
.children(self.render_nav_buttons(window, cx)), .children(self.render_nav_buttons(window, cx)),
) )
.map(|this| { .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 { if ai_setup_page {
this.child( this.child(
ButtonLike::new("start_building") ButtonLike::new("start_building")
@ -412,23 +387,37 @@ impl Onboarding {
.w_full() .w_full()
.justify_between() .justify_between()
.child(Label::new("Start Building")) .child(Label::new("Start Building"))
.child( .child(keybinding.map_or_else(
Icon::new(IconName::Check) || {
.size(IconSize::Small), Icon::new(IconName::Check)
), .size(IconSize::Small)
.into_any_element()
},
IntoElement::into_any_element,
)),
) )
.on_click(cx.listener(|this, _, _, cx| { .on_click(|_, window, cx| {
this.go_to_welcome_page(cx); window.dispatch_action(Finish.boxed_clone(), cx);
})), }),
) )
} else { } else {
this.child( this.child(
ButtonLike::new("skip_all") ButtonLike::new("skip_all")
.size(ButtonSize::Medium) .size(ButtonSize::Medium)
.child(Label::new("Skip All").ml_1()) .child(
.on_click(cx.listener(|this, _, _, cx| { h_flex()
this.go_to_welcome_page(cx); .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 { fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
match self.selected_page { match self.selected_page {
SelectedPage::Basics => { SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(),
crate::basics_page::render_basics_page(window, cx).into_any_element()
}
SelectedPage::Editing => { SelectedPage::Editing => {
crate::editing_page::render_editing_page(window, cx).into_any_element() crate::editing_page::render_editing_page(window, cx).into_any_element()
} }
SelectedPage::AiSetup => { SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page(
crate::ai_setup_page::render_ai_setup_page(&self, window, cx).into_any_element() 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 { impl Render for Onboarding {
@ -484,11 +479,13 @@ impl Render for Onboarding {
.key_context({ .key_context({
let mut ctx = KeyContext::new_with_defaults(); let mut ctx = KeyContext::new_with_defaults();
ctx.add("Onboarding"); ctx.add("Onboarding");
ctx.add("menu");
ctx ctx
}) })
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.size_full() .size_full()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.on_action(Self::on_finish)
.on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
this.set_page(SelectedPage::Basics, cx); this.set_page(SelectedPage::Basics, cx);
})) }))
@ -498,6 +495,14 @@ impl Render for Onboarding {
.on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| { .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
this.set_page(SelectedPage::AiSetup, 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( .child(
h_flex() h_flex()
.max_w(rems_from_px(1100.)) .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( pub async fn handle_import_vscode_settings(
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
source: VsCodeSettingsSource, source: VsCodeSettingsSource,

View file

@ -412,6 +412,7 @@ where
size: ToggleButtonGroupSize, size: ToggleButtonGroupSize,
button_width: Rems, button_width: Rems,
selected_index: usize, selected_index: usize,
tab_index: Option<isize>,
} }
impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> { 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, size: ToggleButtonGroupSize::Default,
button_width: rems_from_px(100.), button_width: rems_from_px(100.),
selected_index: 0, selected_index: 0,
tab_index: None,
} }
} }
} }
@ -436,6 +438,7 @@ impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS, 2> {
size: ToggleButtonGroupSize::Default, size: ToggleButtonGroupSize::Default,
button_width: rems_from_px(100.), button_width: rems_from_px(100.),
selected_index: 0, 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.selected_index = index;
self 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 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; let entry_index = row_index * COLS + col_index;
ButtonLike::new((self.group_name, entry_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| { .when(entry_index == self.selected_index || selected, |this| {
this.toggle_state(true) this.toggle_state(true)
.selected_style(ButtonStyle::Tinted(TintColor::Accent)) .selected_style(ButtonStyle::Tinted(TintColor::Accent))

View file

@ -19,6 +19,7 @@ pub struct NumericStepper {
/// Whether to reserve space for the reset button. /// Whether to reserve space for the reset button.
reserve_space_for_reset: bool, reserve_space_for_reset: bool,
on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
tab_index: Option<isize>,
} }
impl NumericStepper { impl NumericStepper {
@ -36,6 +37,7 @@ impl NumericStepper {
on_increment: Box::new(on_increment), on_increment: Box::new(on_increment),
reserve_space_for_reset: false, reserve_space_for_reset: false,
on_reset: None, on_reset: None,
tab_index: None,
} }
} }
@ -56,6 +58,11 @@ impl NumericStepper {
self.on_reset = Some(Box::new(on_reset)); self.on_reset = Some(Box::new(on_reset));
self self
} }
pub fn tab_index(mut self, tab_index: isize) -> Self {
self.tab_index = Some(tab_index);
self
}
} }
impl RenderOnce for NumericStepper { impl RenderOnce for NumericStepper {
@ -64,6 +71,7 @@ impl RenderOnce for NumericStepper {
let icon_size = IconSize::Small; let icon_size = IconSize::Small;
let is_outlined = matches!(self.style, NumericStepperStyle::Outlined); let is_outlined = matches!(self.style, NumericStepperStyle::Outlined);
let mut tab_index = self.tab_index;
h_flex() h_flex()
.id(self.id) .id(self.id)
@ -74,6 +82,10 @@ impl RenderOnce for NumericStepper {
IconButton::new("reset", IconName::RotateCcw) IconButton::new("reset", IconName::RotateCcw)
.shape(shape) .shape(shape)
.icon_size(icon_size) .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), .on_click(on_reset),
) )
} else if self.reserve_space_for_reset { } else if self.reserve_space_for_reset {
@ -113,6 +125,12 @@ impl RenderOnce for NumericStepper {
.border_r_1() .border_r_1()
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
.child(Icon::new(IconName::Dash).size(IconSize::Small)) .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), .on_click(self.on_decrement),
) )
} else { } else {
@ -120,6 +138,10 @@ impl RenderOnce for NumericStepper {
IconButton::new("decrement", IconName::Dash) IconButton::new("decrement", IconName::Dash)
.shape(shape) .shape(shape)
.icon_size(icon_size) .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), .on_click(self.on_decrement),
) )
} }
@ -137,6 +159,12 @@ impl RenderOnce for NumericStepper {
.border_l_1() .border_l_1()
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
.child(Icon::new(IconName::Plus).size(IconSize::Small)) .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), .on_click(self.on_increment),
) )
} else { } else {
@ -144,6 +172,10 @@ impl RenderOnce for NumericStepper {
IconButton::new("increment", IconName::Dash) IconButton::new("increment", IconName::Dash)
.shape(shape) .shape(shape)
.icon_size(icon_size) .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), .on_click(self.on_increment),
) )
} }

View file

@ -424,6 +424,7 @@ pub struct Switch {
label: Option<SharedString>, label: Option<SharedString>,
key_binding: Option<KeyBinding>, key_binding: Option<KeyBinding>,
color: SwitchColor, color: SwitchColor,
tab_index: Option<isize>,
} }
impl Switch { impl Switch {
@ -437,6 +438,7 @@ impl Switch {
label: None, label: None,
key_binding: None, key_binding: None,
color: SwitchColor::default(), color: SwitchColor::default(),
tab_index: None,
} }
} }
@ -472,6 +474,11 @@ impl Switch {
self.key_binding = key_binding.into(); self.key_binding = key_binding.into();
self 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 { impl RenderOnce for Switch {
@ -501,6 +508,20 @@ impl RenderOnce for Switch {
.w(DynamicSpacing::Base32.rems(cx)) .w(DynamicSpacing::Base32.rems(cx))
.h(DynamicSpacing::Base20.rems(cx)) .h(DynamicSpacing::Base20.rems(cx))
.group(group_id.clone()) .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( .child(
h_flex() h_flex()
.when(is_on, |on| on.justify_end()) .when(is_on, |on| on.justify_end())
@ -572,6 +593,7 @@ pub struct SwitchField {
disabled: bool, disabled: bool,
color: SwitchColor, color: SwitchColor,
tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>, tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
tab_index: Option<isize>,
} }
impl SwitchField { impl SwitchField {
@ -591,6 +613,7 @@ impl SwitchField {
disabled: false, disabled: false,
color: SwitchColor::Accent, color: SwitchColor::Accent,
tooltip: None, tooltip: None,
tab_index: None,
} }
} }
@ -615,14 +638,33 @@ impl SwitchField {
self.tooltip = Some(Rc::new(tooltip)); self.tooltip = Some(Rc::new(tooltip));
self self
} }
pub fn tab_index(mut self, tab_index: isize) -> Self {
self.tab_index = Some(tab_index);
self
}
} }
impl RenderOnce for SwitchField { impl RenderOnce for SwitchField {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { 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() h_flex()
.id(SharedString::from(format!("{}-container", self.id))) .id((self.id.clone(), "container"))
.when(!self.disabled, |this| { .when(!self.disabled, |this| {
this.hover(|this| this.cursor_pointer()) this.hover(|this| this.cursor_pointer())
}) })
@ -630,25 +672,11 @@ impl RenderOnce for SwitchField {
.gap_4() .gap_4()
.justify_between() .justify_between()
.flex_wrap() .flex_wrap()
.child(match (&self.description, &tooltip) { .child(match (&self.description, tooltip) {
(Some(description), Some(tooltip)) => v_flex() (Some(description), Some(tooltip)) => v_flex()
.gap_0p5() .gap_0p5()
.max_w_5_6() .max_w_5_6()
.child( .child(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)
}),
),
)
.child(Label::new(description.clone()).color(Color::Muted)) .child(Label::new(description.clone()).color(Color::Muted))
.into_any_element(), .into_any_element(),
(Some(description), None) => v_flex() (Some(description), None) => v_flex()
@ -657,35 +685,23 @@ impl RenderOnce for SwitchField {
.child(Label::new(self.label.clone())) .child(Label::new(self.label.clone()))
.child(Label::new(description.clone()).color(Color::Muted)) .child(Label::new(description.clone()).color(Color::Muted))
.into_any_element(), .into_any_element(),
(None, Some(tooltip)) => h_flex() (None, Some(tooltip)) => tooltip.into_any_element(),
.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, None) => Label::new(self.label.clone()).into_any_element(), (None, None) => Label::new(self.label.clone()).into_any_element(),
}) })
.child( .child(
Switch::new( Switch::new((self.id.clone(), "switch"), self.toggle_state)
SharedString::from(format!("{}-switch", self.id)), .color(self.color)
self.toggle_state, .disabled(self.disabled)
) .when_some(
.color(self.color) self.tab_index.filter(|_| !self.disabled),
.disabled(self.disabled) |this, tab_index| this.tab_index(tab_index),
.on_click({ )
let on_click = self.on_click.clone(); .on_click({
move |state, window, cx| { let on_click = self.on_click.clone();
(on_click)(state, window, cx); move |state, window, cx| {
} (on_click)(state, window, cx);
}), }
}),
) )
.when(!self.disabled, |this| { .when(!self.disabled, |this| {
this.on_click({ this.on_click({