Merge remote-tracking branch 'origin/main' into project_panel/open_split

This commit is contained in:
Cesar 2025-08-05 16:22:41 -04:00
commit 730816d56f
16 changed files with 574 additions and 290 deletions

View file

@ -24,7 +24,6 @@ self-hosted-runner:
- buildjet-8vcpu-ubuntu-2204-arm
- buildjet-16vcpu-ubuntu-2204-arm
- buildjet-32vcpu-ubuntu-2204-arm
- buildjet-64vcpu-ubuntu-2204-arm
# Self Hosted Runners
- self-mini-macos
- self-32vcpu-windows-2022

View file

@ -650,7 +650,7 @@ jobs:
timeout-minutes: 60
name: Linux arm64 release bundle
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
- buildjet-32vcpu-ubuntu-2204-arm
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')

View file

@ -168,7 +168,7 @@ jobs:
name: Create a Linux *.tar.gz bundle for ARM
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
- buildjet-32vcpu-ubuntu-2204-arm
needs: tests
steps:
- name: Checkout repo

View file

@ -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"
}
}
]

View file

@ -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"
}
}
]

View file

@ -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(),
),

View file

@ -36,11 +36,18 @@ pub enum AnthropicModelMode {
pub enum Model {
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
ClaudeOpus4,
#[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")]
ClaudeOpus4_1,
#[serde(
rename = "claude-opus-4-thinking",
alias = "claude-opus-4-thinking-latest"
)]
ClaudeOpus4Thinking,
#[serde(
rename = "claude-opus-4-1-thinking",
alias = "claude-opus-4-1-thinking-latest"
)]
ClaudeOpus4_1Thinking,
#[default]
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4,
@ -91,10 +98,18 @@ impl Model {
}
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-opus-4-1-thinking") {
return Ok(Self::ClaudeOpus4_1Thinking);
}
if id.starts_with("claude-opus-4-thinking") {
return Ok(Self::ClaudeOpus4Thinking);
}
if id.starts_with("claude-opus-4-1") {
return Ok(Self::ClaudeOpus4_1);
}
if id.starts_with("claude-opus-4") {
return Ok(Self::ClaudeOpus4);
}
@ -141,7 +156,9 @@ impl Model {
pub fn id(&self) -> &str {
match self {
Self::ClaudeOpus4 => "claude-opus-4-latest",
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
@ -159,6 +176,7 @@ impl Model {
pub fn request_id(&self) -> &str {
match self {
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
@ -173,7 +191,9 @@ impl Model {
pub fn display_name(&self) -> &str {
match self {
Self::ClaudeOpus4 => "Claude Opus 4",
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
@ -192,7 +212,9 @@ impl Model {
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
@ -215,7 +237,9 @@ impl Model {
pub fn max_token_count(&self) -> u64 {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
@ -232,7 +256,9 @@ impl Model {
pub fn max_output_tokens(&self) -> u64 {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
@ -249,7 +275,9 @@ impl Model {
pub fn default_temperature(&self) -> f32 {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
@ -269,6 +297,7 @@ impl Model {
pub fn mode(&self) -> AnthropicModelMode {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4_1
| Self::ClaudeSonnet4
| Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
@ -277,6 +306,7 @@ impl Model {
| Self::Claude3Sonnet
| Self::Claude3Haiku => AnthropicModelMode::Default,
Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4Thinking
| Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),

View file

@ -32,11 +32,18 @@ pub enum Model {
ClaudeSonnet4Thinking,
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
ClaudeOpus4,
#[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")]
ClaudeOpus4_1,
#[serde(
rename = "claude-opus-4-thinking",
alias = "claude-opus-4-thinking-latest"
)]
ClaudeOpus4Thinking,
#[serde(
rename = "claude-opus-4-1-thinking",
alias = "claude-opus-4-1-thinking-latest"
)]
ClaudeOpus4_1Thinking,
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
Claude3_5SonnetV2,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
@ -147,7 +154,9 @@ impl Model {
Model::ClaudeSonnet4 => "claude-4-sonnet",
Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking",
Model::ClaudeOpus4 => "claude-4-opus",
Model::ClaudeOpus4_1 => "claude-4-opus-1",
Model::ClaudeOpus4Thinking => "claude-4-opus-thinking",
Model::ClaudeOpus4_1Thinking => "claude-4-opus-1-thinking",
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
Model::Claude3Opus => "claude-3-opus",
@ -208,6 +217,9 @@ impl Model {
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => {
"anthropic.claude-opus-4-20250514-v1:0"
}
Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
"anthropic.claude-opus-4-1-20250805-v1:0"
}
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
@ -266,7 +278,9 @@ impl Model {
Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::ClaudeOpus4 => "Claude Opus 4",
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3Opus => "Claude 3 Opus",
@ -330,8 +344,10 @@ impl Model {
| Self::Claude3_7Sonnet
| Self::ClaudeSonnet4
| Self::ClaudeOpus4
| Self::ClaudeOpus4_1
| Self::ClaudeSonnet4Thinking
| Self::ClaudeOpus4Thinking => 200_000,
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking => 200_000,
Self::AmazonNovaPremier => 1_000_000,
Self::PalmyraWriterX5 => 1_000_000,
Self::PalmyraWriterX4 => 128_000,
@ -348,7 +364,9 @@ impl Model {
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeOpus4
| Model::ClaudeOpus4Thinking => 128_000,
| Model::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Model::ClaudeOpus4_1Thinking => 128_000,
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
Self::Custom {
max_output_tokens, ..
@ -366,6 +384,8 @@ impl Model {
| Self::Claude3_7Sonnet
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking => 1.0,
Self::Custom {
@ -387,6 +407,8 @@ impl Model {
| Self::Claude3_7SonnetThinking
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Haiku => true,
@ -420,7 +442,9 @@ impl Model {
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking => true,
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking => true,
// Custom models - check if they have cache configuration
Self::Custom {
@ -440,7 +464,9 @@ impl Model {
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking => Some(BedrockModelCacheConfiguration {
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking => Some(BedrockModelCacheConfiguration {
max_cache_anchors: 4,
min_total_token: 1024,
}),
@ -467,9 +493,11 @@ impl Model {
Model::ClaudeSonnet4Thinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
Model::ClaudeOpus4Thinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => {
BedrockModelMode::Thinking {
budget_tokens: Some(4096),
}
}
_ => BedrockModelMode::Default,
}
}
@ -518,6 +546,8 @@ impl Model {
| Model::ClaudeSonnet4Thinking
| Model::ClaudeOpus4
| Model::ClaudeOpus4Thinking
| Model::ClaudeOpus4_1
| Model::ClaudeOpus4_1Thinking
| Model::Claude3Haiku
| Model::Claude3Opus
| Model::Claude3Sonnet

View file

@ -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)]

View file

@ -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(),
)
}),
)
}

View file

@ -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,
};
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()
.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 = <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))
.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))
}

View file

@ -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))
}

View file

@ -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,

View file

@ -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))

View file

@ -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),
)
}

View file

@ -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({