Onboarding refactor (#39724)
<img width="1648" height="976" alt="Screenshot 2025-10-07 at 6 57 20 PM" src="https://github.com/user-attachments/assets/ae7289c0-8820-4fdf-ae28-84fb6bd64942" /> Fixes #39347 Release Notes: - Improved onboarding UI by collapsing it to a single page --------- Co-authored-by: dino <dinojoaocosta@gmail.com> Co-authored-by: Lukas Wirth <me@lukaswirth.dev> Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com> Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
co-authored by
dino
Lukas Wirth
Mikayla Maki
Anthony Eid
Mikayla Maki
parent
4dbd186485
commit
ba937d16e7
Generated
-5
@@ -10560,20 +10560,15 @@ dependencies = [
|
||||
name = "onboarding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai_onboarding",
|
||||
"anyhow",
|
||||
"client",
|
||||
"component",
|
||||
"db",
|
||||
"documented",
|
||||
"editor",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"language_model",
|
||||
"menu",
|
||||
"notifications",
|
||||
"picker",
|
||||
|
||||
@@ -1229,9 +1229,6 @@
|
||||
"context": "Onboarding",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-1": "onboarding::ActivateBasicsPage",
|
||||
"ctrl-2": "onboarding::ActivateEditingPage",
|
||||
"ctrl-3": "onboarding::ActivateAISetupPage",
|
||||
"ctrl-enter": "onboarding::Finish",
|
||||
"alt-shift-l": "onboarding::SignIn",
|
||||
"alt-shift-a": "onboarding::OpenAccount"
|
||||
|
||||
@@ -1334,10 +1334,7 @@
|
||||
"context": "Onboarding",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-1": "onboarding::ActivateBasicsPage",
|
||||
"cmd-2": "onboarding::ActivateEditingPage",
|
||||
"cmd-3": "onboarding::ActivateAISetupPage",
|
||||
"cmd-escape": "onboarding::Finish",
|
||||
"cmd-enter": "onboarding::Finish",
|
||||
"alt-tab": "onboarding::SignIn",
|
||||
"alt-shift-a": "onboarding::OpenAccount"
|
||||
}
|
||||
|
||||
@@ -1257,9 +1257,6 @@
|
||||
"context": "Onboarding",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-1": "onboarding::ActivateBasicsPage",
|
||||
"ctrl-2": "onboarding::ActivateEditingPage",
|
||||
"ctrl-3": "onboarding::ActivateAISetupPage",
|
||||
"ctrl-enter": "onboarding::Finish",
|
||||
"alt-shift-l": "onboarding::SignIn",
|
||||
"shift-alt-a": "onboarding::OpenAccount"
|
||||
|
||||
@@ -409,7 +409,7 @@ impl AgentConfiguration {
|
||||
|
||||
SwitchField::new(
|
||||
"always-allow-tool-actions-switch",
|
||||
"Allow running commands without asking for confirmation",
|
||||
Some("Allow running commands without asking for confirmation"),
|
||||
Some(
|
||||
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
|
||||
),
|
||||
@@ -429,7 +429,7 @@ impl AgentConfiguration {
|
||||
|
||||
SwitchField::new(
|
||||
"single-file-review",
|
||||
"Enable single-file agent reviews",
|
||||
Some("Enable single-file agent reviews"),
|
||||
Some("Agent edits are also displayed in single-file editors for review.".into()),
|
||||
single_file_review,
|
||||
move |state, _window, cx| {
|
||||
@@ -450,7 +450,7 @@ impl AgentConfiguration {
|
||||
|
||||
SwitchField::new(
|
||||
"sound-notification",
|
||||
"Play sound when finished generating",
|
||||
Some("Play sound when finished generating"),
|
||||
Some(
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
|
||||
),
|
||||
@@ -470,7 +470,7 @@ impl AgentConfiguration {
|
||||
|
||||
SwitchField::new(
|
||||
"modifier-send",
|
||||
"Use modifier to submit a message",
|
||||
Some("Use modifier to submit a message"),
|
||||
Some(
|
||||
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
|
||||
),
|
||||
|
||||
@@ -58,7 +58,7 @@ mod prompts;
|
||||
use crate::util::atomic_incr_if_not_zero;
|
||||
pub use prompts::*;
|
||||
|
||||
pub(crate) const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1024.), px(700.));
|
||||
pub(crate) const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1536.), px(864.));
|
||||
|
||||
/// Represents the two different phases when dispatching events.
|
||||
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
|
||||
|
||||
@@ -15,20 +15,15 @@ path = "src/onboarding.rs"
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
component.workspace = true
|
||||
db.workspace = true
|
||||
documented.workspace = true
|
||||
editor.workspace = true
|
||||
fs.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
menu.workspace = true
|
||||
notifications.workspace = true
|
||||
picker.workspace = true
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ai_onboarding::AiUpsellCard;
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity,
|
||||
Window, prelude::*,
|
||||
};
|
||||
use itertools;
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use project::DisableAiSettings;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{
|
||||
Badge, ButtonLike, Divider, KeyBinding, Modal, ModalFooter, ModalHeader, Section, SwitchField,
|
||||
ToggleState, prelude::*, tooltip_container,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace};
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
const FEATURED_PROVIDERS: [&str; 4] = ["anthropic", "google", "openai", "ollama"];
|
||||
|
||||
fn render_llm_provider_section(
|
||||
tab_index: &mut isize,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
disabled: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.child(Label::new("Or use other LLM providers").size(LabelSize::Large))
|
||||
.child(
|
||||
Label::new("Bring your API keys to use the available providers with Zed's UI for free.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(render_llm_provider_card(tab_index, workspace, disabled, window, cx))
|
||||
}
|
||||
|
||||
fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> impl IntoElement {
|
||||
let (title, description) = if disabled {
|
||||
(
|
||||
"AI is disabled across Zed",
|
||||
"Re-enable it any time in Settings.",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"Privacy is the default for Zed",
|
||||
"Any use or storage of your data is with your explicit, single-use, opt-in consent.",
|
||||
)
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.relative()
|
||||
.pt_2()
|
||||
.pb_2p5()
|
||||
.pl_3()
|
||||
.pr_2()
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(cx.theme().colors().surface_background.opacity(0.3))
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new(title))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Badge::new("Privacy")
|
||||
.icon(IconName::ShieldCheck)
|
||||
.tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()),
|
||||
)
|
||||
.child(
|
||||
Button::new("learn_more", "Learn More")
|
||||
.style(ButtonStyle::Outlined)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url(&zed_urls::ai_privacy_and_security(cx))
|
||||
})
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index - 1
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new(description)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_llm_provider_card(
|
||||
tab_index: &mut isize,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
disabled: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().surface_background.opacity(0.5))
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.children(itertools::intersperse_with(
|
||||
FEATURED_PROVIDERS
|
||||
.into_iter()
|
||||
.flat_map(|provider_name| {
|
||||
registry.provider(&LanguageModelProviderId::new(provider_name))
|
||||
})
|
||||
.enumerate()
|
||||
.map(|(index, provider)| {
|
||||
let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
|
||||
let is_authenticated = provider.is_authenticated(cx);
|
||||
|
||||
ButtonLike::new(("onboarding-ai-setup-buttons", index))
|
||||
.size(ButtonSize::Large)
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index - 1
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.group(&group_name)
|
||||
.px_0p5()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(provider.icon())
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(Label::new(provider.name().0)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(!is_authenticated, |el| {
|
||||
el.visible_on_hover(group_name.clone())
|
||||
.child(
|
||||
Icon::new(IconName::Settings)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new("Configure")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.when(is_authenticated && !disabled, |el| {
|
||||
el.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Success)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new("Configured")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let workspace = workspace.clone();
|
||||
move |_, window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
telemetry::event!(
|
||||
"Welcome AI Modal Opened",
|
||||
provider = provider.name().0,
|
||||
);
|
||||
|
||||
let modal = AiConfigurationModal::new(
|
||||
provider.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window.focus(&modal.focus_handle(cx));
|
||||
modal
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
|| Divider::horizontal().into_any_element(),
|
||||
))
|
||||
.child(Divider::horizontal())
|
||||
.child(
|
||||
Button::new("agent_settings", "Add Many Others")
|
||||
.size(ButtonSize::Large)
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(OpenSettings.boxed_clone(), cx)
|
||||
})
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index - 1
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_ai_setup_page(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let mut tab_index = 0;
|
||||
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"enable_ai",
|
||||
"Enable AI features",
|
||||
None,
|
||||
if is_ai_disabled {
|
||||
ToggleState::Unselected
|
||||
} else {
|
||||
ToggleState::Selected
|
||||
},
|
||||
|&toggle_state, _, cx| {
|
||||
let enabled = match toggle_state {
|
||||
ToggleState::Indeterminate => {
|
||||
return;
|
||||
}
|
||||
ToggleState::Unselected => true,
|
||||
ToggleState::Selected => false,
|
||||
};
|
||||
|
||||
telemetry::event!(
|
||||
"Welcome AI Enabled",
|
||||
toggle = if enabled { "on" } else { "off" },
|
||||
);
|
||||
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
settings.disable_ai = Some(enabled.into());
|
||||
});
|
||||
},
|
||||
)
|
||||
.tab_index({
|
||||
tab_index += 1;
|
||||
tab_index - 1
|
||||
}),
|
||||
)
|
||||
.child(render_privacy_card(&mut tab_index, is_ai_disabled, cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_6()
|
||||
.child(
|
||||
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
|
||||
.tab_index(Some({
|
||||
tab_index += 1;
|
||||
tab_index - 1
|
||||
})),
|
||||
)
|
||||
.child(render_llm_provider_section(
|
||||
&mut tab_index,
|
||||
workspace,
|
||||
is_ai_disabled,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(is_ai_disabled, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.id("backdrop")
|
||||
.size_full()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.opacity(0.8)
|
||||
.block_mouse_except_scroll(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
struct AiConfigurationModal {
|
||||
focus_handle: FocusHandle,
|
||||
selected_provider: Arc<dyn LanguageModelProvider>,
|
||||
configuration_view: AnyView,
|
||||
}
|
||||
|
||||
impl AiConfigurationModal {
|
||||
fn new(
|
||||
selected_provider: Arc<dyn LanguageModelProvider>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let configuration_view = selected_provider.configuration_view(
|
||||
language_model::ConfigurationViewTargetAgent::ZedAgent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
configuration_view,
|
||||
selected_provider,
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for AiConfigurationModal {}
|
||||
|
||||
impl EventEmitter<DismissEvent> for AiConfigurationModal {}
|
||||
|
||||
impl Focusable for AiConfigurationModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AiConfigurationModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("OnboardingAiConfigurationModal")
|
||||
.w(rems(34.))
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
|
||||
)
|
||||
.child(
|
||||
Modal::new("onboarding-ai-setup-modal", None)
|
||||
.header(
|
||||
ModalHeader::new()
|
||||
.icon(
|
||||
Icon::new(self.selected_provider.icon())
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.headline(self.selected_provider.name().0),
|
||||
)
|
||||
.section(Section::new().child(self.configuration_view.clone()))
|
||||
.footer(
|
||||
ModalFooter::new().end_slot(
|
||||
Button::new("ai-onb-modal-Done", "Done")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
&self.focus_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.cancel(&menu::Cancel, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AiPrivacyTooltip {}
|
||||
|
||||
impl AiPrivacyTooltip {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AiPrivacyTooltip {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
const DESCRIPTION: &str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. ";
|
||||
|
||||
tooltip_container(cx, move |this, _| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ShieldCheck)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("Privacy First")),
|
||||
)
|
||||
.child(
|
||||
div().max_w_64().child(
|
||||
Label::new(DESCRIPTION)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,23 @@ use std::sync::Arc;
|
||||
|
||||
use client::TelemetrySettings;
|
||||
use fs::Fs;
|
||||
use gpui::{App, IntoElement};
|
||||
use gpui::{Action, App, IntoElement};
|
||||
use settings::{BaseKeymap, Settings, update_settings_file};
|
||||
use theme::{
|
||||
Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection,
|
||||
ThemeSettings,
|
||||
};
|
||||
use ui::{
|
||||
ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup,
|
||||
ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px,
|
||||
ButtonLike, ParentElement as _, StatefulInteractiveElement, SwitchField, TintColor,
|
||||
ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*,
|
||||
rems_from_px,
|
||||
};
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
|
||||
use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile};
|
||||
use crate::{
|
||||
ImportCursorSettings, ImportVsCodeSettings, SettingsImportState,
|
||||
theme_preview::{ThemePreviewStyle, ThemePreviewTile},
|
||||
};
|
||||
|
||||
const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
|
||||
const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
|
||||
@@ -70,6 +74,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
|
||||
)
|
||||
}),
|
||||
)
|
||||
.size(ToggleButtonGroupSize::Medium)
|
||||
.tab_index(tab_index)
|
||||
.selected_index(theme_mode as usize)
|
||||
.style(ui::ToggleButtonGroupStyle::Outlined)
|
||||
@@ -220,91 +225,87 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
|
||||
.gap_4()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant.opacity(0.5))
|
||||
.child(Label::new("Telemetry").size(LabelSize::Large))
|
||||
.child(SwitchField::new(
|
||||
"onboarding-telemetry-metrics",
|
||||
"Help Improve Zed",
|
||||
Some("Anonymous usage data helps us build the right features and improve your experience.".into()),
|
||||
if TelemetrySettings::get_global(cx).metrics {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |selection, _, cx| {
|
||||
let enabled = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected => false,
|
||||
ToggleState::Indeterminate => { return; },
|
||||
};
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"onboarding-telemetry-metrics",
|
||||
None::<&str>,
|
||||
Some("Help improve Zed by sending anonymous usage data".into()),
|
||||
if TelemetrySettings::get_global(cx).metrics {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |selection, _, cx| {
|
||||
let enabled = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected => false,
|
||||
ToggleState::Indeterminate => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
update_settings_file(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |setting, _| {
|
||||
setting.telemetry.get_or_insert_default().metrics = Some(enabled);
|
||||
update_settings_file(fs.clone(), cx, move |setting, _| {
|
||||
setting.telemetry.get_or_insert_default().metrics = Some(enabled);
|
||||
});
|
||||
|
||||
// This telemetry event shouldn't fire when it's off. If it does we'll be alerted
|
||||
// and can fix it in a timely manner to respect a user's choice.
|
||||
telemetry::event!(
|
||||
"Welcome Page Telemetry Metrics Toggled",
|
||||
options = if enabled { "on" } else { "off" }
|
||||
);
|
||||
}
|
||||
,
|
||||
);
|
||||
},
|
||||
)
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"onboarding-telemetry-crash-reports",
|
||||
None::<&str>,
|
||||
Some(
|
||||
"Help fix Zed by sending crash reports so we can fix critical issues fast"
|
||||
.into(),
|
||||
),
|
||||
if TelemetrySettings::get_global(cx).diagnostics {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |selection, _, cx| {
|
||||
let enabled = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected => false,
|
||||
ToggleState::Indeterminate => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// This telemetry event shouldn't fire when it's off. If it does we'll be alerted
|
||||
// and can fix it in a timely manner to respect a user's choice.
|
||||
telemetry::event!("Welcome Page Telemetry Metrics Toggled",
|
||||
options = if enabled {
|
||||
"on"
|
||||
} else {
|
||||
"off"
|
||||
}
|
||||
);
|
||||
|
||||
}},
|
||||
).tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index
|
||||
}))
|
||||
.child(SwitchField::new(
|
||||
"onboarding-telemetry-crash-reports",
|
||||
"Help Fix Zed",
|
||||
Some("Send crash reports so we can fix critical issues fast.".into()),
|
||||
if TelemetrySettings::get_global(cx).diagnostics {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |selection, _, cx| {
|
||||
let enabled = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected => false,
|
||||
ToggleState::Indeterminate => { return; },
|
||||
};
|
||||
|
||||
update_settings_file(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |setting, _| {
|
||||
update_settings_file(fs.clone(), cx, move |setting, _| {
|
||||
setting.telemetry.get_or_insert_default().diagnostics = Some(enabled);
|
||||
},
|
||||
});
|
||||
|
||||
);
|
||||
|
||||
// This telemetry event shouldn't fire when it's off. If it does we'll be alerted
|
||||
// and can fix it in a timely manner to respect a user's choice.
|
||||
telemetry::event!("Welcome Page Telemetry Diagnostics Toggled",
|
||||
options = if enabled {
|
||||
"on"
|
||||
} else {
|
||||
"off"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
).tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index
|
||||
}))
|
||||
// This telemetry event shouldn't fire when it's off. If it does we'll be alerted
|
||||
// and can fix it in a timely manner to respect a user's choice.
|
||||
telemetry::event!(
|
||||
"Welcome Page Telemetry Diagnostics Toggled",
|
||||
options = if enabled { "on" } else { "off" }
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
|
||||
@@ -372,8 +373,8 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
|
||||
};
|
||||
SwitchField::new(
|
||||
"onboarding-vim-mode",
|
||||
"Vim Mode",
|
||||
Some("Coming from Neovim? Use our first-class implementation of Vim Mode.".into()),
|
||||
Some("Vim Mode"),
|
||||
Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()),
|
||||
toggle_state,
|
||||
{
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
@@ -402,12 +403,79 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
|
||||
})
|
||||
}
|
||||
|
||||
fn render_setting_import_button(
|
||||
tab_index: isize,
|
||||
label: SharedString,
|
||||
action: &dyn Action,
|
||||
imported: bool,
|
||||
) -> impl IntoElement + 'static {
|
||||
let action = action.boxed_clone();
|
||||
h_flex().w_full().child(
|
||||
ButtonLike::new(label.clone())
|
||||
.style(ButtonStyle::OutlinedTransparent)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.toggle_state(imported)
|
||||
.size(ButtonSize::Medium)
|
||||
.tab_index(tab_index)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.when(imported, |this| {
|
||||
this.child(Icon::new(IconName::Check).color(Color::Success))
|
||||
})
|
||||
.child(Label::new(label.clone()).mx_2().size(LabelSize::Small)),
|
||||
)
|
||||
.on_click(move |_, window, cx| {
|
||||
telemetry::event!("Welcome Import Settings", import_source = label,);
|
||||
window.dispatch_action(action.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
|
||||
let import_state = SettingsImportState::global(cx);
|
||||
let imports: [(SharedString, &dyn Action, bool); 2] = [
|
||||
(
|
||||
"VS Code".into(),
|
||||
&ImportVsCodeSettings { skip_prompt: false },
|
||||
import_state.vscode,
|
||||
),
|
||||
(
|
||||
"Cursor".into(),
|
||||
&ImportCursorSettings { skip_prompt: false },
|
||||
import_state.cursor,
|
||||
),
|
||||
];
|
||||
|
||||
let [vscode, cursor] = imports.map(|(label, action, imported)| {
|
||||
*tab_index += 1;
|
||||
render_setting_import_button(*tab_index - 1, label, action, imported)
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new("Import Settings"))
|
||||
.child(
|
||||
Label::new("Automatically pull your settings from other editors")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(div().w_full())
|
||||
.child(h_flex().gap_1().child(vscode).child(cursor))
|
||||
}
|
||||
|
||||
pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement {
|
||||
let mut tab_index = 0;
|
||||
v_flex()
|
||||
.id("basics-page")
|
||||
.gap_6()
|
||||
.child(render_theme_section(&mut tab_index, cx))
|
||||
.child(render_base_keymap_section(&mut tab_index, cx))
|
||||
.child(render_import_settings_section(&mut tab_index, cx))
|
||||
.child(render_vim_mode_switch(&mut tab_index, cx))
|
||||
.child(render_telemetry_section(&mut tab_index, cx))
|
||||
}
|
||||
|
||||
@@ -1,611 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::{EditorSettings, ShowMinimap};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, FontFeatures, IntoElement, Pixels, SharedString, Window};
|
||||
use language::language_settings::{AllLanguageSettings, FormatOnSave};
|
||||
use project::project_settings::ProjectSettings;
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use theme::{FontFamilyName, ThemeSettings};
|
||||
use ui::{
|
||||
ButtonLike, PopoverMenu, SwitchField, ToggleButtonGroup, ToggleButtonGroupStyle,
|
||||
ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
|
||||
};
|
||||
use ui_input::{NumberField, font_picker};
|
||||
|
||||
use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
|
||||
|
||||
fn read_show_mini_map(cx: &App) -> ShowMinimap {
|
||||
editor::EditorSettings::get_global(cx).minimap.show
|
||||
}
|
||||
|
||||
fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
// This is used to speed up the UI
|
||||
// the UI reads the current values to get what toggle state to show on buttons
|
||||
// there's a slight delay if we just call update_settings_file so we manually set
|
||||
// the value here then call update_settings file to get around the delay
|
||||
let mut curr_settings = EditorSettings::get_global(cx).clone();
|
||||
curr_settings.minimap.show = show;
|
||||
EditorSettings::override_global(curr_settings, cx);
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
telemetry::event!(
|
||||
"Welcome Minimap Clicked",
|
||||
from = settings.editor.minimap.clone().unwrap_or_default(),
|
||||
to = show
|
||||
);
|
||||
settings.editor.minimap.get_or_insert_default().show = Some(show);
|
||||
});
|
||||
}
|
||||
|
||||
fn read_inlay_hints(cx: &App) -> bool {
|
||||
AllLanguageSettings::get_global(cx)
|
||||
.defaults
|
||||
.inlay_hints
|
||||
.enabled
|
||||
}
|
||||
|
||||
fn write_inlay_hints(enabled: bool, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
let mut curr_settings = AllLanguageSettings::get_global(cx).clone();
|
||||
curr_settings.defaults.inlay_hints.enabled = enabled;
|
||||
AllLanguageSettings::override_global(curr_settings, cx);
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _cx| {
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.defaults
|
||||
.inlay_hints
|
||||
.get_or_insert_default()
|
||||
.enabled = Some(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
fn read_git_blame(cx: &App) -> bool {
|
||||
ProjectSettings::get_global(cx).git.inline_blame.enabled
|
||||
}
|
||||
|
||||
fn write_git_blame(enabled: bool, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
let mut curr_settings = ProjectSettings::get_global(cx).clone();
|
||||
curr_settings.git.inline_blame.enabled = enabled;
|
||||
ProjectSettings::override_global(curr_settings, cx);
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
settings
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.inline_blame
|
||||
.get_or_insert_default()
|
||||
.enabled = Some(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
fn write_ui_font_family(font: SharedString, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
telemetry::event!(
|
||||
"Welcome Font Changed",
|
||||
type = "ui font",
|
||||
old = settings.theme.ui_font_family,
|
||||
new = font
|
||||
);
|
||||
settings.theme.ui_font_family = Some(FontFamilyName(font.into()));
|
||||
});
|
||||
}
|
||||
|
||||
fn write_ui_font_size(size: Pixels, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
settings.theme.ui_font_size = Some(size.into());
|
||||
});
|
||||
}
|
||||
|
||||
fn write_buffer_font_size(size: Pixels, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
settings.theme.buffer_font_size = Some(size.into());
|
||||
});
|
||||
}
|
||||
|
||||
fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
telemetry::event!(
|
||||
"Welcome Font Changed",
|
||||
type = "editor font",
|
||||
old = settings.theme.buffer_font_family,
|
||||
new = font_family
|
||||
);
|
||||
|
||||
settings.theme.buffer_font_family = Some(FontFamilyName(font_family.into()));
|
||||
});
|
||||
}
|
||||
|
||||
fn read_font_ligatures(cx: &App) -> bool {
|
||||
ThemeSettings::get_global(cx)
|
||||
.buffer_font
|
||||
.features
|
||||
.is_calt_enabled()
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn write_font_ligatures(enabled: bool, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let bit = if enabled { 1 } else { 0 };
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let mut features = settings
|
||||
.theme
|
||||
.buffer_font_features
|
||||
.as_mut()
|
||||
.map(|features| features.tag_value_list().to_vec())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
|
||||
features[calt_index].1 = bit;
|
||||
} else {
|
||||
features.push(("calt".into(), bit));
|
||||
}
|
||||
|
||||
settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features)));
|
||||
});
|
||||
}
|
||||
|
||||
fn read_format_on_save(cx: &App) -> bool {
|
||||
match AllLanguageSettings::get_global(cx).defaults.format_on_save {
|
||||
FormatOnSave::On => true,
|
||||
FormatOnSave::Off => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_format_on_save(format_on_save: bool, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
settings.project.all_languages.defaults.format_on_save = Some(match format_on_save {
|
||||
true => FormatOnSave::On,
|
||||
false => FormatOnSave::Off,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_setting_import_button(
|
||||
tab_index: isize,
|
||||
label: SharedString,
|
||||
icon_name: IconName,
|
||||
action: &dyn Action,
|
||||
imported: bool,
|
||||
) -> impl IntoElement {
|
||||
let action = action.boxed_clone();
|
||||
h_flex().w_full().child(
|
||||
ButtonLike::new(label.clone())
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Large)
|
||||
.tab_index(tab_index)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.px_1()
|
||||
.child(
|
||||
Icon::new(icon_name)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(Label::new(label.clone())),
|
||||
)
|
||||
.when(imported, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Success)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(Label::new("Imported").size(LabelSize::Small)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(move |_, window, cx| {
|
||||
telemetry::event!("Welcome Import Settings", import_source = label,);
|
||||
window.dispatch_action(action.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_import_settings_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
|
||||
let import_state = SettingsImportState::global(cx);
|
||||
let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [
|
||||
(
|
||||
"VS Code".into(),
|
||||
IconName::EditorVsCode,
|
||||
&ImportVsCodeSettings { skip_prompt: false },
|
||||
import_state.vscode,
|
||||
),
|
||||
(
|
||||
"Cursor".into(),
|
||||
IconName::EditorCursor,
|
||||
&ImportCursorSettings { skip_prompt: false },
|
||||
import_state.cursor,
|
||||
),
|
||||
];
|
||||
|
||||
let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| {
|
||||
*tab_index += 1;
|
||||
render_setting_import_button(*tab_index - 1, label, icon_name, action, imported)
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.child(Label::new("Import Settings").size(LabelSize::Large))
|
||||
.child(
|
||||
Label::new("Automatically pull your settings from other editors.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(h_flex().w_full().gap_4().child(vscode).child(cursor))
|
||||
}
|
||||
|
||||
fn render_font_customization_section(
|
||||
tab_index: &mut isize,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let ui_font_size = theme_settings.ui_font_size(cx);
|
||||
let ui_font_family = theme_settings.ui_font.family.clone();
|
||||
let buffer_font_family = theme_settings.buffer_font.family.clone();
|
||||
let buffer_font_size = theme_settings.buffer_font_size(cx);
|
||||
|
||||
let ui_font_picker =
|
||||
cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx));
|
||||
|
||||
let buffer_font_picker = cx.new(|cx| {
|
||||
font_picker(
|
||||
buffer_font_family.clone(),
|
||||
write_buffer_font_family,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let ui_font_handle = ui::PopoverMenuHandle::default();
|
||||
let buffer_font_handle = ui::PopoverMenuHandle::default();
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Label::new("UI Font"))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
PopoverMenu::new("ui-font-picker")
|
||||
.menu({
|
||||
let ui_font_picker = ui_font_picker;
|
||||
move |_window, _cx| Some(ui_font_picker.clone())
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("ui-font-family-button")
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.full_width()
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index - 1
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new(ui_font_family))
|
||||
.child(
|
||||
Icon::new(IconName::ChevronUpDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
),
|
||||
)
|
||||
.full_width(true)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(4.0),
|
||||
})
|
||||
.with_handle(ui_font_handle),
|
||||
)
|
||||
.child(font_picker_stepper(
|
||||
"ui-font-size",
|
||||
&ui_font_size,
|
||||
tab_index,
|
||||
write_ui_font_size,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Label::new("Editor Font"))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
PopoverMenu::new("buffer-font-picker")
|
||||
.menu({
|
||||
let buffer_font_picker = buffer_font_picker;
|
||||
move |_window, _cx| Some(buffer_font_picker.clone())
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("buffer-font-family-button")
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.full_width()
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index - 1
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new(buffer_font_family))
|
||||
.child(
|
||||
Icon::new(IconName::ChevronUpDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
),
|
||||
)
|
||||
.full_width(true)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(4.0),
|
||||
})
|
||||
.with_handle(buffer_font_handle),
|
||||
)
|
||||
.child(font_picker_stepper(
|
||||
"buffer-font-size",
|
||||
&buffer_font_size,
|
||||
tab_index,
|
||||
write_buffer_font_size,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn font_picker_stepper(
|
||||
id: &'static str,
|
||||
font_size: &Pixels,
|
||||
tab_index: &mut isize,
|
||||
write_font_size: fn(Pixels, &mut App),
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> NumberField<u32> {
|
||||
window.with_id(id, |window| {
|
||||
let optimistic_font_size: gpui::Entity<Option<u32>> = window.use_state(cx, |_, _| None);
|
||||
optimistic_font_size.update(cx, |optimistic_font_size, _| {
|
||||
if let Some(optimistic_font_size_val) = optimistic_font_size {
|
||||
if *optimistic_font_size_val == u32::from(font_size) {
|
||||
*optimistic_font_size = None;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let stepper_font_size = optimistic_font_size
|
||||
.read(cx)
|
||||
.unwrap_or_else(|| font_size.into());
|
||||
|
||||
NumberField::new(
|
||||
SharedString::new(format!("{}-stepper", id)),
|
||||
stepper_font_size,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.on_change(move |new_value, _, cx| {
|
||||
optimistic_font_size.write(cx, Some(*new_value));
|
||||
write_font_size(Pixels::from(*new_value), cx);
|
||||
})
|
||||
.format(|value| format!("{value}px"))
|
||||
.tab_index({
|
||||
*tab_index += 2;
|
||||
*tab_index - 2
|
||||
})
|
||||
.min(6)
|
||||
.max(32)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_popular_settings_section(
|
||||
tab_index: &mut isize,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
const LIGATURE_TOOLTIP: &str =
|
||||
"Font ligatures combine two characters into one. For example, turning != into ≠.";
|
||||
|
||||
v_flex()
|
||||
.pt_6()
|
||||
.gap_4()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant.opacity(0.5))
|
||||
.child(Label::new("Popular Settings").size(LabelSize::Large))
|
||||
.child(render_font_customization_section(tab_index, window, cx))
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"onboarding-font-ligatures",
|
||||
"Font Ligatures",
|
||||
Some("Combine text characters into their associated symbols.".into()),
|
||||
if read_font_ligatures(cx) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
|toggle_state, _, cx| {
|
||||
let enabled = toggle_state == &ToggleState::Selected;
|
||||
telemetry::event!(
|
||||
"Welcome Font Ligature",
|
||||
options = if enabled { "on" } else { "off" },
|
||||
);
|
||||
|
||||
write_font_ligatures(enabled, cx);
|
||||
},
|
||||
)
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index - 1
|
||||
})
|
||||
.tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
|
||||
)
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"onboarding-format-on-save",
|
||||
"Format on Save",
|
||||
Some("Format code automatically when saving.".into()),
|
||||
if read_format_on_save(cx) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
|toggle_state, _, cx| {
|
||||
let enabled = toggle_state == &ToggleState::Selected;
|
||||
telemetry::event!(
|
||||
"Welcome Format On Save Changed",
|
||||
options = if enabled { "on" } else { "off" },
|
||||
);
|
||||
|
||||
write_format_on_save(enabled, cx);
|
||||
},
|
||||
)
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index - 1
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"onboarding-enable-inlay-hints",
|
||||
"Inlay Hints",
|
||||
Some("See parameter names for function and method calls inline.".into()),
|
||||
if read_inlay_hints(cx) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
|toggle_state, _, cx| {
|
||||
let enabled = toggle_state == &ToggleState::Selected;
|
||||
telemetry::event!(
|
||||
"Welcome Inlay Hints Changed",
|
||||
options = if enabled { "on" } else { "off" },
|
||||
);
|
||||
|
||||
write_inlay_hints(enabled, cx);
|
||||
},
|
||||
)
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index - 1
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"onboarding-git-blame-switch",
|
||||
"Inline Git Blame",
|
||||
Some("See who committed each line on a given file.".into()),
|
||||
if read_git_blame(cx) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
|toggle_state, _, cx| {
|
||||
let enabled = toggle_state == &ToggleState::Selected;
|
||||
telemetry::event!(
|
||||
"Welcome Git Blame Changed",
|
||||
options = if enabled { "on" } else { "off" },
|
||||
);
|
||||
|
||||
write_git_blame(enabled, cx);
|
||||
},
|
||||
)
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index - 1
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex().child(Label::new("Minimap")).child(
|
||||
Label::new("See a high-level overview of your source code.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
ToggleButtonGroup::single_row(
|
||||
"onboarding-show-mini-map",
|
||||
[
|
||||
ToggleButtonSimple::new("Auto", |_, _, cx| {
|
||||
write_show_mini_map(ShowMinimap::Auto, cx);
|
||||
})
|
||||
.tooltip(Tooltip::text(
|
||||
"Show the minimap if the editor's scrollbar is visible.",
|
||||
)),
|
||||
ToggleButtonSimple::new("Always", |_, _, cx| {
|
||||
write_show_mini_map(ShowMinimap::Always, cx);
|
||||
}),
|
||||
ToggleButtonSimple::new("Never", |_, _, cx| {
|
||||
write_show_mini_map(ShowMinimap::Never, cx);
|
||||
}),
|
||||
],
|
||||
)
|
||||
.selected_index(match read_show_mini_map(cx) {
|
||||
ShowMinimap::Auto => 0,
|
||||
ShowMinimap::Always => 1,
|
||||
ShowMinimap::Never => 2,
|
||||
})
|
||||
.tab_index(tab_index)
|
||||
.style(ToggleButtonGroupStyle::Outlined)
|
||||
.width(ui::rems_from_px(3. * 64.)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let mut tab_index = 0;
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(render_import_settings_section(&mut tab_index, cx))
|
||||
.child(render_popular_settings_section(&mut tab_index, window, cx))
|
||||
}
|
||||
@@ -14,8 +14,8 @@ use serde::Deserialize;
|
||||
use settings::{SettingsStore, VsCodeSettingsSource};
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
Avatar, ButtonLike, FluentBuilder, Headline, KeyBinding, ParentElement as _,
|
||||
StatefulInteractiveElement, Vector, VectorName, WithScrollbar, prelude::*, rems_from_px,
|
||||
KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName,
|
||||
WithScrollbar as _, prelude::*, rems_from_px,
|
||||
};
|
||||
pub use ui_input::font_picker;
|
||||
use workspace::{
|
||||
@@ -26,10 +26,8 @@ use workspace::{
|
||||
open_new, register_serializable_item, with_active_or_new_workspace,
|
||||
};
|
||||
|
||||
mod ai_setup_page;
|
||||
mod base_keymap_picker;
|
||||
mod basics_page;
|
||||
mod editing_page;
|
||||
pub mod multibuffer_hint;
|
||||
mod theme_preview;
|
||||
mod welcome;
|
||||
@@ -66,12 +64,6 @@ actions!(
|
||||
actions!(
|
||||
onboarding,
|
||||
[
|
||||
/// Activates the Basics page.
|
||||
ActivateBasicsPage,
|
||||
/// Activates the Editing page.
|
||||
ActivateEditingPage,
|
||||
/// Activates the AI Setup page.
|
||||
ActivateAISetupPage,
|
||||
/// Finish the onboarding process.
|
||||
Finish,
|
||||
/// Sign in while in the onboarding flow.
|
||||
@@ -216,27 +208,9 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SelectedPage {
|
||||
Basics,
|
||||
Editing,
|
||||
AiSetup,
|
||||
}
|
||||
|
||||
impl SelectedPage {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
SelectedPage::Basics => "Basics",
|
||||
SelectedPage::Editing => "Editing",
|
||||
SelectedPage::AiSetup => "AI Setup",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Onboarding {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
selected_page: SelectedPage,
|
||||
user_store: Entity<UserStore>,
|
||||
scroll_handle: ScrollHandle,
|
||||
_settings_subscription: Subscription,
|
||||
@@ -259,7 +233,6 @@ impl Onboarding {
|
||||
workspace: workspace.weak_handle(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
selected_page: SelectedPage::Basics,
|
||||
user_store: workspace.user_store().clone(),
|
||||
_settings_subscription: cx
|
||||
.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||
@@ -267,228 +240,8 @@ impl Onboarding {
|
||||
})
|
||||
}
|
||||
|
||||
fn set_page(
|
||||
&mut self,
|
||||
page: SelectedPage,
|
||||
clicked: Option<&'static str>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(click) = clicked {
|
||||
telemetry::event!(
|
||||
"Welcome Tab Clicked",
|
||||
from = self.selected_page.name(),
|
||||
to = page.name(),
|
||||
clicked = click,
|
||||
);
|
||||
}
|
||||
|
||||
self.selected_page = page;
|
||||
self.scroll_handle.set_offset(Default::default());
|
||||
cx.notify();
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
fn render_nav_buttons(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> [impl IntoElement; 3] {
|
||||
let pages = [
|
||||
SelectedPage::Basics,
|
||||
SelectedPage::Editing,
|
||||
SelectedPage::AiSetup,
|
||||
];
|
||||
|
||||
let text = ["Basics", "Editing", "AI Setup"];
|
||||
|
||||
let actions: [&dyn Action; 3] = [
|
||||
&ActivateBasicsPage,
|
||||
&ActivateEditingPage,
|
||||
&ActivateAISetupPage,
|
||||
];
|
||||
|
||||
let mut binding = actions.map(|action| {
|
||||
KeyBinding::for_action_in(action, &self.focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.)))
|
||||
});
|
||||
|
||||
pages.map(|page| {
|
||||
let i = page as usize;
|
||||
let selected = self.selected_page == page;
|
||||
h_flex()
|
||||
.id(text[i])
|
||||
.relative()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.justify_between()
|
||||
.rounded_sm()
|
||||
.when(selected, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.h_4()
|
||||
.w_px()
|
||||
.bg(cx.theme().colors().text_accent)
|
||||
.absolute()
|
||||
.left_0(),
|
||||
)
|
||||
})
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.child(Label::new(text[i]).map(|this| {
|
||||
if selected {
|
||||
this.color(Color::Default)
|
||||
} else {
|
||||
this.color(Color::Muted)
|
||||
}
|
||||
}))
|
||||
.child(binding[i].take().map_or(
|
||||
gpui::Empty.into_any_element(),
|
||||
IntoElement::into_any_element,
|
||||
))
|
||||
.on_click(cx.listener(move |this, click_event, _, cx| {
|
||||
let click = match click_event {
|
||||
gpui::ClickEvent::Mouse(_) => "mouse",
|
||||
gpui::ClickEvent::Keyboard(_) => "keyboard",
|
||||
};
|
||||
|
||||
this.set_page(page, Some(click), cx);
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
fn render_nav(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.h_full()
|
||||
.w(rems_from_px(220.))
|
||||
.flex_shrink_0()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
h_flex()
|
||||
.px_2()
|
||||
.gap_4()
|
||||
.child(Vector::square(VectorName::ZedLogo, rems(2.5)))
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
Headline::new("Welcome to Zed").size(HeadlineSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new("The editor for what's next")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.italic(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.py_4()
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().colors().border_variant.opacity(0.5))
|
||||
.gap_1()
|
||||
.children(self.render_nav_buttons(window, cx)),
|
||||
)
|
||||
.map(|this| {
|
||||
if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
this.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.ml_2()
|
||||
.gap_2()
|
||||
.max_w_full()
|
||||
.w_full()
|
||||
.child(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(
|
||||
Label::new(user.github_login.clone())
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
ButtonLike::new("open_account")
|
||||
.size(ButtonSize::Medium)
|
||||
.child(
|
||||
h_flex()
|
||||
.ml_1()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new("Open Account"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenAccount,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| {
|
||||
kb.size(rems_from_px(12.))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
OpenAccount.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
ButtonLike::new("sign_in")
|
||||
.size(ButtonSize::Medium)
|
||||
.child(
|
||||
h_flex()
|
||||
.ml_1()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new("Sign In"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&SignIn,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
),
|
||||
)
|
||||
.on_click(|_, window, cx| {
|
||||
telemetry::event!("Welcome Sign In Clicked");
|
||||
window.dispatch_action(SignIn.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child({
|
||||
Button::new("start_building", "Start Building")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Medium)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&Finish, &self.focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(|_, window, cx| {
|
||||
telemetry::event!("Welcome Start Building Clicked");
|
||||
window.dispatch_action(Finish.boxed_clone(), cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
|
||||
telemetry::event!("Welcome Skip Clicked");
|
||||
telemetry::event!("Finish Setup");
|
||||
go_to_welcome_page(cx);
|
||||
}
|
||||
|
||||
@@ -509,29 +262,14 @@ impl Onboarding {
|
||||
cx.open_url(&zed_urls::account_url(cx))
|
||||
}
|
||||
|
||||
fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let client = Client::global(cx);
|
||||
|
||||
match self.selected_page {
|
||||
SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(),
|
||||
SelectedPage::Editing => {
|
||||
crate::editing_page::render_editing_page(window, cx).into_any_element()
|
||||
}
|
||||
SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page(
|
||||
self.workspace.clone(),
|
||||
self.user_store.clone(),
|
||||
client,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
fn render_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
crate::basics_page::render_basics_page(cx).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Onboarding {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
div()
|
||||
.image_cache(gpui::retain_all("onboarding-page"))
|
||||
.key_context({
|
||||
let mut ctx = KeyContext::new_with_defaults();
|
||||
@@ -545,15 +283,6 @@ impl Render for Onboarding {
|
||||
.on_action(Self::on_finish)
|
||||
.on_action(Self::handle_sign_in)
|
||||
.on_action(Self::handle_open_account)
|
||||
.on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
|
||||
this.set_page(SelectedPage::Basics, Some("action"), cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| {
|
||||
this.set_page(SelectedPage::Editing, Some("action"), cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
|
||||
this.set_page(SelectedPage::AiSetup, Some("action"), cx);
|
||||
}))
|
||||
.on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
|
||||
window.focus_next();
|
||||
cx.notify();
|
||||
@@ -563,35 +292,68 @@ impl Render for Onboarding {
|
||||
cx.notify();
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.max_w(rems_from_px(1100.))
|
||||
.max_h(rems_from_px(850.))
|
||||
div()
|
||||
.max_w(Rems(48.0))
|
||||
.w_full()
|
||||
.mx_auto()
|
||||
.size_full()
|
||||
.m_auto()
|
||||
.py_20()
|
||||
.px_12()
|
||||
.items_start()
|
||||
.gap_12()
|
||||
.child(self.render_nav(window, cx))
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.m_auto()
|
||||
.id("page-content")
|
||||
.gap_6()
|
||||
.size_full()
|
||||
.pr_6()
|
||||
.max_w_full()
|
||||
.min_w_0()
|
||||
.p_12()
|
||||
.border_color(cx.theme().colors().border_variant.opacity(0.5))
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
v_flex()
|
||||
.id("page-content")
|
||||
.size_full()
|
||||
.max_w_full()
|
||||
.min_w_0()
|
||||
.pl_12()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant.opacity(0.5))
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_page(window, cx))
|
||||
.track_scroll(&self.scroll_handle),
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.child(Vector::square(VectorName::ZedLogo, rems(2.5)))
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
Headline::new("Welcome to Zed")
|
||||
.size(HeadlineSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new("The editor for what's next")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.italic(),
|
||||
),
|
||||
)
|
||||
.child(div().w_full())
|
||||
.child({
|
||||
Button::new("finish_setup", "Finish Setup")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.width(Rems(12.0))
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&Finish,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Finish.boxed_clone(), cx);
|
||||
})
|
||||
})
|
||||
.pb_6()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant.opacity(0.5)),
|
||||
)
|
||||
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
|
||||
),
|
||||
.child(self.render_page(cx))
|
||||
.track_scroll(&self.scroll_handle),
|
||||
)
|
||||
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -628,7 +390,6 @@ impl Item for Onboarding {
|
||||
Some(cx.new(|cx| Onboarding {
|
||||
workspace: self.workspace.clone(),
|
||||
user_store: self.user_store.clone(),
|
||||
selected_page: self.selected_page,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||
@@ -814,25 +575,10 @@ impl workspace::SerializableItem for Onboarding {
|
||||
cx: &mut App,
|
||||
) -> gpui::Task<gpui::Result<Entity<Self>>> {
|
||||
window.spawn(cx, async move |cx| {
|
||||
if let Some(page_number) =
|
||||
if let Some(_) =
|
||||
persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)?
|
||||
{
|
||||
let page = match page_number {
|
||||
0 => Some(SelectedPage::Basics),
|
||||
1 => Some(SelectedPage::Editing),
|
||||
2 => Some(SelectedPage::AiSetup),
|
||||
_ => None,
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let onboarding_page = Onboarding::new(workspace, cx);
|
||||
if let Some(page) = page {
|
||||
zlog::info!("Onboarding page {page:?} loaded");
|
||||
onboarding_page.update(cx, |onboarding_page, cx| {
|
||||
onboarding_page.set_page(page, None, cx);
|
||||
})
|
||||
}
|
||||
onboarding_page
|
||||
})
|
||||
workspace.update(cx, |workspace, cx| Onboarding::new(workspace, cx))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("No onboarding page to deserialize"))
|
||||
}
|
||||
@@ -848,10 +594,10 @@ impl workspace::SerializableItem for Onboarding {
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> Option<gpui::Task<gpui::Result<()>>> {
|
||||
let workspace_id = workspace.database_id()?;
|
||||
let page_number = self.selected_page as u16;
|
||||
|
||||
Some(cx.background_spawn(async move {
|
||||
persistence::ONBOARDING_PAGES
|
||||
.save_onboarding_page(item_id, workspace_id, page_number)
|
||||
.save_onboarding_page(item_id, workspace_id)
|
||||
.await
|
||||
}))
|
||||
}
|
||||
@@ -874,17 +620,32 @@ mod persistence {
|
||||
impl Domain for OnboardingPagesDb {
|
||||
const NAME: &str = stringify!(OnboardingPagesDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE onboarding_pages (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
page_number INTEGER,
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql!(
|
||||
CREATE TABLE onboarding_pages (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
page_number INTEGER,
|
||||
|
||||
PRIMARY KEY(workspace_id, item_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)];
|
||||
PRIMARY KEY(workspace_id, item_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE onboarding_pages_2 (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
|
||||
PRIMARY KEY(workspace_id, item_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
INSERT INTO onboarding_pages_2 SELECT workspace_id, item_id FROM onboarding_pages;
|
||||
DROP TABLE onboarding_pages;
|
||||
ALTER TABLE onboarding_pages_2 RENAME TO onboarding_pages;
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
|
||||
@@ -893,11 +654,10 @@ mod persistence {
|
||||
query! {
|
||||
pub async fn save_onboarding_page(
|
||||
item_id: workspace::ItemId,
|
||||
workspace_id: workspace::WorkspaceId,
|
||||
page_number: u16
|
||||
workspace_id: workspace::WorkspaceId
|
||||
) -> Result<()> {
|
||||
INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id, page_number)
|
||||
VALUES (?, ?, ?)
|
||||
INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id)
|
||||
VALUES (?, ?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,8 +665,8 @@ mod persistence {
|
||||
pub fn get_onboarding_page(
|
||||
item_id: workspace::ItemId,
|
||||
workspace_id: workspace::WorkspaceId
|
||||
) -> Result<Option<u16>> {
|
||||
SELECT page_number
|
||||
) -> Result<Option<workspace::ItemId>> {
|
||||
SELECT item_id
|
||||
FROM onboarding_pages
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ impl SectionEntry {
|
||||
}
|
||||
|
||||
pub struct WelcomePage {
|
||||
first_paint: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
@@ -168,6 +169,10 @@ impl WelcomePage {
|
||||
|
||||
impl Render for WelcomePage {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.first_paint {
|
||||
window.request_animation_frame();
|
||||
self.first_paint = false;
|
||||
}
|
||||
let (first_section, second_section) = CONTENT;
|
||||
let first_section_entries = first_section.entries.len();
|
||||
let last_index = first_section_entries + second_section.entries.len();
|
||||
@@ -311,7 +316,10 @@ impl WelcomePage {
|
||||
cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
WelcomePage { focus_handle }
|
||||
WelcomePage {
|
||||
first_paint: true,
|
||||
focus_handle,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,9 @@ pub enum ButtonStyle {
|
||||
/// a fully transparent button.
|
||||
Outlined,
|
||||
|
||||
/// Transparent button that always has an outline.
|
||||
OutlinedTransparent,
|
||||
|
||||
/// A more de-emphasized version of the outlined button.
|
||||
OutlinedGhost,
|
||||
|
||||
@@ -149,11 +152,38 @@ pub enum ButtonStyle {
|
||||
Transparent,
|
||||
}
|
||||
|
||||
/// Rounding for a button that may have straight edges.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub(crate) enum ButtonLikeRounding {
|
||||
All,
|
||||
Left,
|
||||
Right,
|
||||
pub(crate) struct ButtonLikeRounding {
|
||||
/// Top-left corner rounding
|
||||
pub top_left: bool,
|
||||
/// Top-right corner rounding
|
||||
pub top_right: bool,
|
||||
/// Bottom-right corner rounding
|
||||
pub bottom_right: bool,
|
||||
/// Bottom-left corner rounding
|
||||
pub bottom_left: bool,
|
||||
}
|
||||
|
||||
impl ButtonLikeRounding {
|
||||
pub const ALL: Self = Self {
|
||||
top_left: true,
|
||||
top_right: true,
|
||||
bottom_right: true,
|
||||
bottom_left: true,
|
||||
};
|
||||
pub const LEFT: Self = Self {
|
||||
top_left: true,
|
||||
top_right: false,
|
||||
bottom_right: false,
|
||||
bottom_left: true,
|
||||
};
|
||||
pub const RIGHT: Self = Self {
|
||||
top_left: false,
|
||||
top_right: true,
|
||||
bottom_right: true,
|
||||
bottom_left: false,
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -198,6 +228,12 @@ impl ButtonStyle {
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_background,
|
||||
border_color: cx.theme().colors().border_variant,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::OutlinedGhost => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: cx.theme().colors().border_variant,
|
||||
@@ -249,6 +285,12 @@ impl ButtonStyle {
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_hover,
|
||||
border_color: cx.theme().colors().border,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::OutlinedGhost => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: cx.theme().colors().border,
|
||||
@@ -293,6 +335,12 @@ impl ButtonStyle {
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_active,
|
||||
border_color: cx.theme().colors().border_variant,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::OutlinedGhost => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: cx.theme().colors().border_variant,
|
||||
@@ -332,6 +380,12 @@ impl ButtonStyle {
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_background,
|
||||
border_color: cx.theme().colors().border,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::OutlinedGhost => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: cx.theme().colors().border,
|
||||
@@ -374,6 +428,12 @@ impl ButtonStyle {
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_disabled,
|
||||
border_color: cx.theme().colors().border_disabled,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::OutlinedGhost => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: cx.theme().colors().border_disabled,
|
||||
@@ -455,7 +515,7 @@ impl ButtonLike {
|
||||
width: None,
|
||||
height: None,
|
||||
size: ButtonSize::Default,
|
||||
rounding: Some(ButtonLikeRounding::All),
|
||||
rounding: Some(ButtonLikeRounding::ALL),
|
||||
tooltip: None,
|
||||
hoverable_tooltip: None,
|
||||
children: SmallVec::new(),
|
||||
@@ -469,15 +529,15 @@ impl ButtonLike {
|
||||
}
|
||||
|
||||
pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
|
||||
Self::new(id).rounding(ButtonLikeRounding::Left)
|
||||
Self::new(id).rounding(ButtonLikeRounding::LEFT)
|
||||
}
|
||||
|
||||
pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
|
||||
Self::new(id).rounding(ButtonLikeRounding::Right)
|
||||
Self::new(id).rounding(ButtonLikeRounding::RIGHT)
|
||||
}
|
||||
|
||||
pub fn new_rounded_all(id: impl Into<ElementId>) -> Self {
|
||||
Self::new(id).rounding(ButtonLikeRounding::All)
|
||||
Self::new(id).rounding(ButtonLikeRounding::ALL)
|
||||
}
|
||||
|
||||
pub fn opacity(mut self, opacity: f32) -> Self {
|
||||
@@ -630,14 +690,17 @@ impl RenderOnce for ButtonLike {
|
||||
.when(
|
||||
matches!(
|
||||
self.style,
|
||||
ButtonStyle::Outlined | ButtonStyle::OutlinedGhost
|
||||
ButtonStyle::Outlined
|
||||
| ButtonStyle::OutlinedTransparent
|
||||
| ButtonStyle::OutlinedGhost
|
||||
),
|
||||
|this| this.border_1(),
|
||||
)
|
||||
.when_some(self.rounding, |this, rounding| match rounding {
|
||||
ButtonLikeRounding::All => this.rounded_sm(),
|
||||
ButtonLikeRounding::Left => this.rounded_l_sm(),
|
||||
ButtonLikeRounding::Right => this.rounded_r_sm(),
|
||||
.when_some(self.rounding, |this, rounding| {
|
||||
this.when(rounding.top_left, |this| this.rounded_tl_sm())
|
||||
.when(rounding.top_right, |this| this.rounded_tr_sm())
|
||||
.when(rounding.bottom_right, |this| this.rounded_br_sm())
|
||||
.when(rounding.bottom_left, |this| this.rounded_bl_sm())
|
||||
})
|
||||
.gap(DynamicSpacing::Base04.rems(cx))
|
||||
.map(|this| match self.size {
|
||||
|
||||
@@ -6,15 +6,41 @@ use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, Tooltip,
|
||||
|
||||
/// The position of a [`ToggleButton`] within a group of buttons.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ToggleButtonPosition {
|
||||
/// The toggle button is first in the group.
|
||||
First,
|
||||
pub struct ToggleButtonPosition {
|
||||
/// The toggle button is one of the leftmost of the group.
|
||||
leftmost: bool,
|
||||
/// The toggle button is one of the rightmost of the group.
|
||||
rightmost: bool,
|
||||
/// The toggle button is one of the topmost of the group.
|
||||
topmost: bool,
|
||||
/// The toggle button is one of the bottommost of the group.
|
||||
bottommost: bool,
|
||||
}
|
||||
|
||||
/// The toggle button is in the middle of the group (i.e., it is not the first or last toggle button).
|
||||
Middle,
|
||||
impl ToggleButtonPosition {
|
||||
pub const HORIZONTAL_FIRST: Self = Self {
|
||||
leftmost: true,
|
||||
..Self::HORIZONTAL_MIDDLE
|
||||
};
|
||||
pub const HORIZONTAL_MIDDLE: Self = Self {
|
||||
leftmost: false,
|
||||
rightmost: false,
|
||||
topmost: true,
|
||||
bottommost: true,
|
||||
};
|
||||
pub const HORIZONTAL_LAST: Self = Self {
|
||||
rightmost: true,
|
||||
..Self::HORIZONTAL_MIDDLE
|
||||
};
|
||||
|
||||
/// The toggle button is last in the group.
|
||||
Last,
|
||||
pub(crate) fn to_rounding(self) -> ButtonLikeRounding {
|
||||
ButtonLikeRounding {
|
||||
top_left: self.topmost && self.leftmost,
|
||||
top_right: self.topmost && self.rightmost,
|
||||
bottom_right: self.bottommost && self.rightmost,
|
||||
bottom_left: self.bottommost && self.leftmost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
@@ -46,15 +72,15 @@ impl ToggleButton {
|
||||
}
|
||||
|
||||
pub fn first(self) -> Self {
|
||||
self.position_in_group(ToggleButtonPosition::First)
|
||||
self.position_in_group(ToggleButtonPosition::HORIZONTAL_FIRST)
|
||||
}
|
||||
|
||||
pub fn middle(self) -> Self {
|
||||
self.position_in_group(ToggleButtonPosition::Middle)
|
||||
self.position_in_group(ToggleButtonPosition::HORIZONTAL_MIDDLE)
|
||||
}
|
||||
|
||||
pub fn last(self) -> Self {
|
||||
self.position_in_group(ToggleButtonPosition::Last)
|
||||
self.position_in_group(ToggleButtonPosition::HORIZONTAL_LAST)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +179,8 @@ impl RenderOnce for ToggleButton {
|
||||
};
|
||||
|
||||
self.base
|
||||
.when_some(self.position_in_group, |this, position| match position {
|
||||
ToggleButtonPosition::First => this.rounding(ButtonLikeRounding::Left),
|
||||
ToggleButtonPosition::Middle => this.rounding(None),
|
||||
ToggleButtonPosition::Last => this.rounding(ButtonLikeRounding::Right),
|
||||
.when_some(self.position_in_group, |this, position| {
|
||||
this.rounding(position.to_rounding())
|
||||
})
|
||||
.child(
|
||||
Label::new(self.label)
|
||||
@@ -535,7 +559,15 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
|
||||
|
||||
ButtonLike::new((group_name.clone(), entry_index))
|
||||
.full_width()
|
||||
.rounding(None)
|
||||
.rounding(Some(
|
||||
ToggleButtonPosition {
|
||||
leftmost: col_index == 0,
|
||||
rightmost: col_index == COLS - 1,
|
||||
topmost: row_index == 0,
|
||||
bottommost: row_index == ROWS - 1,
|
||||
}
|
||||
.to_rounding(),
|
||||
))
|
||||
.when_some(self.tab_index, |this, tab_index| {
|
||||
this.tab_index(tab_index + entry_index as isize)
|
||||
})
|
||||
|
||||
@@ -585,7 +585,7 @@ impl RenderOnce for Switch {
|
||||
///
|
||||
/// let switch_field = SwitchField::new(
|
||||
/// "feature-toggle",
|
||||
/// "Enable feature",
|
||||
/// Some("Enable feature"),
|
||||
/// Some("This feature adds new functionality to the app.".into()),
|
||||
/// ToggleState::Unselected,
|
||||
/// |state, window, cx| {
|
||||
@@ -596,7 +596,7 @@ impl RenderOnce for Switch {
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct SwitchField {
|
||||
id: ElementId,
|
||||
label: SharedString,
|
||||
label: Option<SharedString>,
|
||||
description: Option<SharedString>,
|
||||
toggle_state: ToggleState,
|
||||
on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
|
||||
@@ -609,14 +609,14 @@ pub struct SwitchField {
|
||||
impl SwitchField {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
label: impl Into<SharedString>,
|
||||
label: Option<impl Into<SharedString>>,
|
||||
description: Option<SharedString>,
|
||||
toggle_state: impl Into<ToggleState>,
|
||||
on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
label: label.map(Into::into),
|
||||
description,
|
||||
toggle_state: toggle_state.into(),
|
||||
on_click: Arc::new(on_click),
|
||||
@@ -657,11 +657,11 @@ impl SwitchField {
|
||||
|
||||
impl RenderOnce for SwitchField {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let tooltip = self.tooltip.map(|tooltip_fn| {
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(Label::new(self.label.clone()))
|
||||
.child(
|
||||
let tooltip = self
|
||||
.tooltip
|
||||
.zip(self.label.clone())
|
||||
.map(|(tooltip_fn, label)| {
|
||||
h_flex().gap_0p5().child(Label::new(label)).child(
|
||||
IconButton::new("tooltip_button", IconName::Info)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -673,7 +673,7 @@ impl RenderOnce for SwitchField {
|
||||
})
|
||||
.on_click(|_, _, _| {}), // Intentional empty on click handler so that clicking on the info tooltip icon doesn't trigger the switch toggle
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.id((self.id.clone(), "container"))
|
||||
@@ -694,11 +694,17 @@ impl RenderOnce for SwitchField {
|
||||
(Some(description), None) => v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new(self.label.clone()))
|
||||
.when_some(self.label, |this, label| this.child(Label::new(label)))
|
||||
.child(Label::new(description.clone()).color(Color::Muted))
|
||||
.into_any_element(),
|
||||
(None, Some(tooltip)) => tooltip.into_any_element(),
|
||||
(None, None) => Label::new(self.label.clone()).into_any_element(),
|
||||
(None, None) => {
|
||||
if let Some(label) = self.label.clone() {
|
||||
Label::new(label).into_any_element()
|
||||
} else {
|
||||
gpui::Empty.into_any_element()
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(
|
||||
Switch::new((self.id.clone(), "switch"), self.toggle_state)
|
||||
@@ -748,7 +754,7 @@ impl Component for SwitchField {
|
||||
"Unselected",
|
||||
SwitchField::new(
|
||||
"switch_field_unselected",
|
||||
"Enable notifications",
|
||||
Some("Enable notifications"),
|
||||
Some("Receive notifications when new messages arrive.".into()),
|
||||
ToggleState::Unselected,
|
||||
|_, _, _| {},
|
||||
@@ -759,7 +765,7 @@ impl Component for SwitchField {
|
||||
"Selected",
|
||||
SwitchField::new(
|
||||
"switch_field_selected",
|
||||
"Enable notifications",
|
||||
Some("Enable notifications"),
|
||||
Some("Receive notifications when new messages arrive.".into()),
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
@@ -775,7 +781,7 @@ impl Component for SwitchField {
|
||||
"Default",
|
||||
SwitchField::new(
|
||||
"switch_field_default",
|
||||
"Default color",
|
||||
Some("Default color"),
|
||||
Some("This uses the default switch color.".into()),
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
@@ -786,7 +792,7 @@ impl Component for SwitchField {
|
||||
"Accent",
|
||||
SwitchField::new(
|
||||
"switch_field_accent",
|
||||
"Accent color",
|
||||
Some("Accent color"),
|
||||
Some("This uses the accent color scheme.".into()),
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
@@ -802,7 +808,7 @@ impl Component for SwitchField {
|
||||
"Disabled",
|
||||
SwitchField::new(
|
||||
"switch_field_disabled",
|
||||
"Disabled field",
|
||||
Some("Disabled field"),
|
||||
Some("This field is disabled and cannot be toggled.".into()),
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
@@ -817,7 +823,7 @@ impl Component for SwitchField {
|
||||
"No Description",
|
||||
SwitchField::new(
|
||||
"switch_field_disabled",
|
||||
"Disabled field",
|
||||
Some("Disabled field"),
|
||||
None,
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
@@ -832,7 +838,7 @@ impl Component for SwitchField {
|
||||
"Tooltip with Description",
|
||||
SwitchField::new(
|
||||
"switch_field_tooltip_with_desc",
|
||||
"Nice Feature",
|
||||
Some("Nice Feature"),
|
||||
Some("Enable advanced configuration options.".into()),
|
||||
ToggleState::Unselected,
|
||||
|_, _, _| {},
|
||||
@@ -844,7 +850,7 @@ impl Component for SwitchField {
|
||||
"Tooltip without Description",
|
||||
SwitchField::new(
|
||||
"switch_field_tooltip_no_desc",
|
||||
"Nice Feature",
|
||||
Some("Nice Feature"),
|
||||
None,
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
|
||||
Reference in New Issue
Block a user