onboarding: Add the AI page (#35351)

This PR starts the work on the AI onboarding page as well as the
configuration modal

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
Finn Evers 2025-08-01 16:43:59 +02:00 committed by GitHub
parent e5c6a596a9
commit b01d1872cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 550 additions and 63 deletions

4
Cargo.lock generated
View file

@ -10923,6 +10923,7 @@ dependencies = [
name = "onboarding"
version = "0.1.0"
dependencies = [
"ai_onboarding",
"anyhow",
"client",
"command_palette_hooks",
@ -10933,7 +10934,10 @@ dependencies = [
"feature_flags",
"fs",
"gpui",
"itertools 0.14.0",
"language",
"language_model",
"menu",
"project",
"schemars",
"serde",

View file

@ -406,7 +406,9 @@ impl AgentConfiguration {
SwitchField::new(
"always-allow-tool-actions-switch",
"Allow running commands without asking for confirmation",
"The agent can perform potentially destructive actions without asking for your confirmation.",
Some(
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
),
always_allow_tool_actions,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@ -424,7 +426,7 @@ impl AgentConfiguration {
SwitchField::new(
"single-file-review",
"Enable single-file agent reviews",
"Agent edits are also displayed in single-file editors for review.",
Some("Agent edits are also displayed in single-file editors for review.".into()),
single_file_review,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@ -442,7 +444,9 @@ impl AgentConfiguration {
SwitchField::new(
"sound-notification",
"Play sound when finished generating",
"Hear a notification sound when the agent is done generating changes or needs your input.",
Some(
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
),
play_sound_when_agent_done,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@ -460,7 +464,9 @@ impl AgentConfiguration {
SwitchField::new(
"modifier-send",
"Use modifier to submit a message",
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.",
Some(
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(),
),
use_modifier_to_send,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;

View file

@ -1,6 +1,7 @@
use std::sync::Arc;
use client::{Client, zed_urls};
use cloud_llm_client::Plan;
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Divider, List, Vector, VectorName, prelude::*};
@ -10,13 +11,15 @@ use crate::{BulletItem, SignInStatus};
pub struct AiUpsellCard {
pub sign_in_status: SignInStatus,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub user_plan: Option<Plan>,
}
impl AiUpsellCard {
pub fn new(client: Arc<Client>) -> Self {
pub fn new(client: Arc<Client>, user_plan: Option<Plan>) -> Self {
let status = *client.status().borrow();
Self {
user_plan,
sign_in_status: status.into(),
sign_in: Arc::new(move |_window, cx| {
cx.spawn({
@ -34,6 +37,7 @@ impl AiUpsellCard {
impl RenderOnce for AiUpsellCard {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let pro_section = v_flex()
.flex_grow()
.w_full()
.gap_1()
.child(
@ -56,6 +60,7 @@ impl RenderOnce for AiUpsellCard {
);
let free_section = v_flex()
.flex_grow()
.w_full()
.gap_1()
.child(
@ -71,7 +76,7 @@ impl RenderOnce for AiUpsellCard {
)
.child(
List::new()
.child(BulletItem::new("50 prompts with the Claude models"))
.child(BulletItem::new("50 prompts with Claude models"))
.child(BulletItem::new("2,000 accepted edit predictions")),
);
@ -132,22 +137,28 @@ impl RenderOnce for AiUpsellCard {
v_flex()
.relative()
.p_6()
.pt_4()
.p_4()
.pt_3()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_lg()
.overflow_hidden()
.child(grid_bg)
.child(gradient_bg)
.child(Headline::new("Try Zed AI"))
.child(Label::new(DESCRIPTION).color(Color::Muted).mb_2())
.child(Label::new("Try Zed AI").size(LabelSize::Large))
.child(
div()
.max_w_3_4()
.mb_2()
.child(Label::new(DESCRIPTION).color(Color::Muted)),
)
.child(
h_flex()
.w_full()
.mt_1p5()
.mb_2p5()
.items_start()
.gap_12()
.gap_6()
.child(free_section)
.child(pro_section),
)
@ -183,6 +194,7 @@ impl Component for AiUpsellCard {
AiUpsellCard {
sign_in_status: SignInStatus::SignedOut,
sign_in: Arc::new(|_, _| {}),
user_plan: None,
}
.into_any_element(),
),
@ -191,6 +203,7 @@ impl Component for AiUpsellCard {
AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
user_plan: None,
}
.into_any_element(),
),

View file

@ -16,6 +16,7 @@ default = []
[dependencies]
anyhow.workspace = true
ai_onboarding.workspace = true
client.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
@ -25,7 +26,10 @@ editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
menu.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true

View file

@ -0,0 +1,362 @@
use std::sync::Arc;
use ai_onboarding::{AiUpsellCard, SignInStatus};
use client::DisableAiSettings;
use fs::Fs;
use gpui::{
Action, AnyView, App, DismissEvent, EventEmitter, FocusHandle, Focusable, Window, prelude::*,
};
use itertools;
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use settings::{Settings, update_settings_file};
use ui::{
Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState,
prelude::*,
};
use workspace::ModalView;
use util::ResultExt;
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,
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(onboarding, disabled, window, cx))
}
fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement {
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()
.map(|this| {
if disabled {
this.child(
h_flex()
.gap_2()
.justify_between()
.child(
h_flex()
.gap_1()
.child(Label::new("AI is disabled across Zed"))
.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::XSmall),
),
)
.child(Badge::new("PRIVACY").icon(IconName::FileLock)),
)
.child(
Label::new("Re-enable it any time in Settings.")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("We don't train models using your data"))
.child(
h_flex()
.gap_1()
.child(Badge::new("Privacy").icon(IconName::FileLock))
.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(
"https://zed.dev/docs/ai/privacy-and-security",
);
}),
),
),
)
.child(
Label::new(
"Feel confident in the security and privacy of your projects using Zed.",
)
.size(LabelSize::Small)
.color(Color::Muted),
)
}
})
}
fn render_llm_provider_card(
onboarding: &Onboarding,
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)
.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 = onboarding.workspace.clone();
move |_, window, cx| {
workspace
.update(cx, |workspace, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
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)
}),
)
}
pub(crate) fn render_ai_setup_page(
onboarding: &Onboarding,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
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(
v_flex()
.mt_2()
.gap_6()
.child(AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
user_plan: onboarding.cloud_user_store.read(cx).plan(),
})
.child(render_llm_provider_section(
onboarding,
is_ai_disabled,
window,
cx,
))
.when(is_ai_disabled, |this| this.child(backdrop)),
)
}
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(window, cx);
Self {
focus_handle,
configuration_view,
selected_provider,
}
}
}
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()
.w(rems(34.))
.elevation_3(cx)
.track_focus(&self.focus_handle)
.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(
h_flex()
.gap_1()
.child(
Button::new("onboarding-closing-cancel", "Cancel")
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(Button::new("save-btn", "Done").on_click(cx.listener(
|_, _, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx);
cx.emit(DismissEvent);
},
))),
),
),
)
}
}

View file

@ -242,7 +242,7 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement {
.child(SwitchField::new(
"onboarding-telemetry-metrics",
"Help Improve Zed",
"Sending anonymous usage data helps us build the right features and create the best experience.",
Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()),
if TelemetrySettings::get_global(cx).metrics {
ui::ToggleState::Selected
} else {
@ -267,7 +267,7 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement {
.child(SwitchField::new(
"onboarding-telemetry-crash-reports",
"Help Fix Zed",
"Send crash reports so we can fix critical issues fast.",
Some("Send crash reports so we can fix critical issues fast.".into()),
if TelemetrySettings::get_global(cx).diagnostics {
ui::ToggleState::Selected
} else {
@ -338,10 +338,10 @@ pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl Into
.style(ui::ToggleButtonGroupStyle::Outlined)
),
)
.child(v_flex().justify_center().child(div().h_0().child("hack").invisible()).child(SwitchField::new(
.child(SwitchField::new(
"onboarding-vim-mode",
"Vim Mode",
"Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.",
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 {
@ -363,6 +363,6 @@ pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl Into
);
}
},
)))
))
.child(render_telemetry_section(cx))
}

View file

@ -349,7 +349,7 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In
.child(SwitchField::new(
"onboarding-enable-inlay-hints",
"Inlay Hints",
"See parameter names for function and method calls inline.",
Some("See parameter names for function and method calls inline.".into()),
if read_inlay_hints(cx) {
ui::ToggleState::Selected
} else {
@ -362,7 +362,7 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In
.child(SwitchField::new(
"onboarding-git-blame-switch",
"Git Blame",
"See who committed each line on a given file.",
Some("See who committed each line on a given file.".into()),
if read_git_blame(cx) {
ui::ToggleState::Selected
} else {

View file

@ -1,5 +1,5 @@
use crate::welcome::{ShowWelcome, WelcomePage};
use client::{Client, UserStore};
use client::{Client, CloudUserStore, UserStore};
use command_palette_hooks::CommandPaletteFilter;
use db::kvp::KEY_VALUE_STORE;
use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
@ -25,6 +25,7 @@ use workspace::{
open_new, with_active_or_new_workspace,
};
mod ai_setup_page;
mod basics_page;
mod editing_page;
mod theme_preview;
@ -78,11 +79,7 @@ pub fn init(cx: &mut App) {
if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx);
} else {
let settings_page = Onboarding::new(
workspace.weak_handle(),
workspace.user_store().clone(),
cx,
);
let settings_page = Onboarding::new(workspace, cx);
workspace.add_item_to_active_pane(
Box::new(settings_page),
None,
@ -198,8 +195,7 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
|workspace, window, cx| {
{
workspace.toggle_dock(DockPosition::Left, window, cx);
let onboarding_page =
Onboarding::new(workspace.weak_handle(), workspace.user_store().clone(), cx);
let onboarding_page = Onboarding::new(workspace, cx);
workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
window.focus(&onboarding_page.focus_handle(cx));
@ -224,21 +220,19 @@ struct Onboarding {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
selected_page: SelectedPage,
cloud_user_store: Entity<CloudUserStore>,
user_store: Entity<UserStore>,
_settings_subscription: Subscription,
}
impl Onboarding {
fn new(
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
cx: &mut App,
) -> Entity<Self> {
fn new(workspace: &Workspace, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
workspace,
user_store,
workspace: workspace.weak_handle(),
focus_handle: cx.focus_handle(),
selected_page: SelectedPage::Basics,
cloud_user_store: workspace.app_state().cloud_user_store.clone(),
user_store: workspace.user_store().clone(),
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
})
}
@ -391,13 +385,11 @@ impl Onboarding {
SelectedPage::Editing => {
crate::editing_page::render_editing_page(window, cx).into_any_element()
}
SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(),
SelectedPage::AiSetup => {
crate::ai_setup_page::render_ai_setup_page(&self, window, cx).into_any_element()
}
}
}
fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().child("ai setup page")
}
}
impl Render for Onboarding {
@ -418,7 +410,9 @@ impl Render for Onboarding {
.gap_12()
.child(self.render_nav(window, cx))
.child(
div()
v_flex()
.max_w_full()
.min_w_0()
.pl_12()
.border_l_1()
.border_color(cx.theme().colors().border_variant.opacity(0.5))
@ -458,11 +452,9 @@ impl Item for Onboarding {
_: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>> {
Some(Onboarding::new(
self.workspace.clone(),
self.user_store.clone(),
cx,
))
self.workspace
.update(cx, |workspace, cx| Onboarding::new(workspace, cx))
.ok()
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {

View file

@ -1,4 +1,5 @@
mod avatar;
mod badge;
mod banner;
mod button;
mod callout;
@ -41,6 +42,7 @@ mod tooltip;
mod stories;
pub use avatar::*;
pub use badge::*;
pub use banner::*;
pub use button::*;
pub use callout::*;

View file

@ -0,0 +1,71 @@
use crate::Divider;
use crate::DividerColor;
use crate::component_prelude::*;
use crate::prelude::*;
use gpui::{AnyElement, IntoElement, SharedString, Window};
#[derive(IntoElement, RegisterComponent)]
pub struct Badge {
label: SharedString,
icon: IconName,
}
impl Badge {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
icon: IconName::Check,
}
}
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = icon;
self
}
}
impl RenderOnce for Badge {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.h_full()
.gap_1()
.pl_1()
.pr_2()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().element_background)
.rounded_sm()
.overflow_hidden()
.child(
Icon::new(self.icon)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Divider::vertical().color(DividerColor::Border))
.child(
Label::new(self.label.clone())
.size(LabelSize::XSmall)
.buffer_font(cx)
.ml_1(),
)
}
}
impl Component for Badge {
fn scope() -> ComponentScope {
ComponentScope::DataDisplay
}
fn description() -> Option<&'static str> {
Some(
"A compact, labeled component with optional icon for displaying status, categories, or metadata.",
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
single_example("Basic Badge", Badge::new("Default").into_any_element())
.into_any_element(),
)
}
}

View file

@ -1,5 +1,5 @@
use crate::{
Clickable, Color, DynamicSpacing, Headline, HeadlineSize, IconButton, IconButtonShape,
Clickable, Color, DynamicSpacing, Headline, HeadlineSize, Icon, IconButton, IconButtonShape,
IconName, Label, LabelCommon, LabelSize, h_flex, v_flex,
};
use gpui::{prelude::FluentBuilder, *};
@ -92,6 +92,7 @@ impl RenderOnce for Modal {
#[derive(IntoElement)]
pub struct ModalHeader {
icon: Option<Icon>,
headline: Option<SharedString>,
description: Option<SharedString>,
children: SmallVec<[AnyElement; 2]>,
@ -108,6 +109,7 @@ impl Default for ModalHeader {
impl ModalHeader {
pub fn new() -> Self {
Self {
icon: None,
headline: None,
description: None,
children: SmallVec::new(),
@ -116,6 +118,11 @@ impl ModalHeader {
}
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
/// Set the headline of the modal.
///
/// This will insert the headline as the first item
@ -179,12 +186,17 @@ impl RenderOnce for ModalHeader {
)
})
.child(
v_flex().flex_1().children(children).when_some(
self.description,
|this, description| {
v_flex()
.flex_1()
.child(
h_flex()
.gap_1()
.when_some(self.icon, |this, icon| this.child(icon))
.children(children),
)
.when_some(self.description, |this, description| {
this.child(Label::new(description).color(Color::Muted).mb_2())
},
),
}),
)
.when(self.show_dismiss_button, |this| {
this.child(

View file

@ -566,7 +566,7 @@ impl RenderOnce for Switch {
pub struct SwitchField {
id: ElementId,
label: SharedString,
description: SharedString,
description: Option<SharedString>,
toggle_state: ToggleState,
on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
disabled: bool,
@ -577,14 +577,14 @@ impl SwitchField {
pub fn new(
id: impl Into<ElementId>,
label: impl Into<SharedString>,
description: 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(),
description: description.into(),
description: description,
toggle_state: toggle_state.into(),
on_click: Arc::new(on_click),
disabled: false,
@ -592,6 +592,11 @@ impl SwitchField {
}
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
@ -616,13 +621,15 @@ impl RenderOnce for SwitchField {
.gap_4()
.justify_between()
.flex_wrap()
.child(
v_flex()
.child(match &self.description {
Some(description) => v_flex()
.gap_0p5()
.max_w_5_6()
.child(Label::new(self.label))
.child(Label::new(self.description).color(Color::Muted)),
)
.child(Label::new(self.label.clone()))
.child(Label::new(description.clone()).color(Color::Muted))
.into_any_element(),
None => Label::new(self.label.clone()).into_any_element(),
})
.child(
Switch::new(
SharedString::from(format!("{}-switch", self.id)),
@ -671,7 +678,7 @@ impl Component for SwitchField {
SwitchField::new(
"switch_field_unselected",
"Enable notifications",
"Receive notifications when new messages arrive.",
Some("Receive notifications when new messages arrive.".into()),
ToggleState::Unselected,
|_, _, _| {},
)
@ -682,7 +689,7 @@ impl Component for SwitchField {
SwitchField::new(
"switch_field_selected",
"Enable notifications",
"Receive notifications when new messages arrive.",
Some("Receive notifications when new messages arrive.".into()),
ToggleState::Selected,
|_, _, _| {},
)
@ -698,7 +705,7 @@ impl Component for SwitchField {
SwitchField::new(
"switch_field_default",
"Default color",
"This uses the default switch color.",
Some("This uses the default switch color.".into()),
ToggleState::Selected,
|_, _, _| {},
)
@ -709,7 +716,7 @@ impl Component for SwitchField {
SwitchField::new(
"switch_field_accent",
"Accent color",
"This uses the accent color scheme.",
Some("This uses the accent color scheme.".into()),
ToggleState::Selected,
|_, _, _| {},
)
@ -725,7 +732,7 @@ impl Component for SwitchField {
SwitchField::new(
"switch_field_disabled",
"Disabled field",
"This field is disabled and cannot be toggled.",
Some("This field is disabled and cannot be toggled.".into()),
ToggleState::Selected,
|_, _, _| {},
)
@ -733,6 +740,20 @@ impl Component for SwitchField {
.into_any_element(),
)],
),
example_group_with_title(
"No Description",
vec![single_example(
"No Description",
SwitchField::new(
"switch_field_disabled",
"Disabled field",
None,
ToggleState::Selected,
|_, _, _| {},
)
.into_any_element(),
)],
),
])
.into_any_element(),
)