Detailed changes
@@ -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",
@@ -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;
@@ -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(),
),
@@ -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
@@ -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);
+ },
+ ))),
+ ),
+ ),
+ )
+ }
+}
@@ -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))
}
@@ -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 {
@@ -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)) {
@@ -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::*;
@@ -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(),
+ )
+ }
+}
@@ -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(
@@ -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(),
)