onboarding ui: Add theme preview tiles and button functionality to basic page (#35413)

This PR polishes and adds functionality to the onboarding UI with a
focus on the basic page. It added theme preview tiles, got the Vim,
telemetry, crash reporting, and sign-in button working.

The theme preview component was moved to the UI crate and it now can
have a click handler on it.

Finally, this commit also changed `client::User.github_login` and
`client::UserStore.by_github_login` to use `SharedStrings` instead of
`Strings`. This change was made because user.github_login was cloned in
several areas including the UI, and was cast to a shared string in some
cases too.

Release Notes:

- N/A

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
This commit is contained in:
Anthony Eid 2025-07-31 14:40:41 -04:00 committed by GitHub
parent b59f992928
commit c6947ee4f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 295 additions and 83 deletions

3
Cargo.lock generated
View file

@ -10923,6 +10923,7 @@ name = "onboarding"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"client",
"command_palette_hooks", "command_palette_hooks",
"db", "db",
"editor", "editor",
@ -10937,6 +10938,7 @@ dependencies = [
"theme", "theme",
"ui", "ui",
"util", "util",
"vim_mode_setting",
"workspace", "workspace",
"workspace-hack", "workspace-hack",
"zed_actions", "zed_actions",
@ -18594,7 +18596,6 @@ dependencies = [
"serde", "serde",
"settings", "settings",
"telemetry", "telemetry",
"theme",
"ui", "ui",
"util", "util",
"vim_mode_setting", "vim_mode_setting",

View file

@ -126,7 +126,7 @@ impl ChannelMembership {
proto::channel_member::Kind::Member => 0, proto::channel_member::Kind::Member => 0,
proto::channel_member::Kind::Invitee => 1, proto::channel_member::Kind::Invitee => 1,
}, },
username_order: self.user.github_login.as_str(), username_order: &self.user.github_login,
} }
} }
} }

View file

@ -55,7 +55,7 @@ pub struct ParticipantIndex(pub u32);
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct User { pub struct User {
pub id: UserId, pub id: UserId,
pub github_login: String, pub github_login: SharedString,
pub avatar_uri: SharedUri, pub avatar_uri: SharedUri,
pub name: Option<String>, pub name: Option<String>,
} }
@ -107,7 +107,7 @@ pub enum ContactRequestStatus {
pub struct UserStore { pub struct UserStore {
users: HashMap<u64, Arc<User>>, users: HashMap<u64, Arc<User>>,
by_github_login: HashMap<String, u64>, by_github_login: HashMap<SharedString, u64>,
participant_indices: HashMap<u64, ParticipantIndex>, participant_indices: HashMap<u64, ParticipantIndex>,
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>, update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_plan: Option<proto::Plan>, current_plan: Option<proto::Plan>,
@ -904,7 +904,7 @@ impl UserStore {
let mut missing_user_ids = Vec::new(); let mut missing_user_ids = Vec::new();
for id in user_ids { for id in user_ids {
if let Some(github_login) = self.get_cached_user(id).map(|u| u.github_login.clone()) { if let Some(github_login) = self.get_cached_user(id).map(|u| u.github_login.clone()) {
ret.insert(id, github_login.into()); ret.insert(id, github_login);
} else { } else {
missing_user_ids.push(id) missing_user_ids.push(id)
} }
@ -925,7 +925,7 @@ impl User {
fn new(message: proto::User) -> Arc<Self> { fn new(message: proto::User) -> Arc<Self> {
Arc::new(User { Arc::new(User {
id: message.id, id: message.id,
github_login: message.github_login, github_login: message.github_login.into(),
avatar_uri: message.avatar_url.into(), avatar_uri: message.avatar_url.into(),
name: message.name, name: message.name,
}) })

View file

@ -38,12 +38,12 @@ fn room_participants(room: &Entity<Room>, cx: &mut TestAppContext) -> RoomPartic
let mut remote = room let mut remote = room
.remote_participants() .remote_participants()
.values() .values()
.map(|participant| participant.user.github_login.clone()) .map(|participant| participant.user.github_login.clone().to_string())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut pending = room let mut pending = room
.pending_participants() .pending_participants()
.iter() .iter()
.map(|user| user.github_login.clone()) .map(|user| user.github_login.clone().to_string())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
remote.sort(); remote.sort();
pending.sort(); pending.sort();

View file

@ -1881,7 +1881,7 @@ async fn test_active_call_events(
vec![room::Event::RemoteProjectShared { vec![room::Event::RemoteProjectShared {
owner: Arc::new(User { owner: Arc::new(User {
id: client_a.user_id().unwrap(), id: client_a.user_id().unwrap(),
github_login: "user_a".to_string(), github_login: "user_a".into(),
avatar_uri: "avatar_a".into(), avatar_uri: "avatar_a".into(),
name: None, name: None,
}), }),
@ -1900,7 +1900,7 @@ async fn test_active_call_events(
vec![room::Event::RemoteProjectShared { vec![room::Event::RemoteProjectShared {
owner: Arc::new(User { owner: Arc::new(User {
id: client_b.user_id().unwrap(), id: client_b.user_id().unwrap(),
github_login: "user_b".to_string(), github_login: "user_b".into(),
avatar_uri: "avatar_b".into(), avatar_uri: "avatar_b".into(),
name: None, name: None,
}), }),
@ -6079,7 +6079,7 @@ async fn test_contacts(
.iter() .iter()
.map(|contact| { .map(|contact| {
( (
contact.user.github_login.clone(), contact.user.github_login.clone().to_string(),
if contact.online { "online" } else { "offline" }, if contact.online { "online" } else { "offline" },
if contact.busy { "busy" } else { "free" }, if contact.busy { "busy" } else { "free" },
) )

View file

@ -696,17 +696,17 @@ impl TestClient {
current: store current: store
.contacts() .contacts()
.iter() .iter()
.map(|contact| contact.user.github_login.clone()) .map(|contact| contact.user.github_login.clone().to_string())
.collect(), .collect(),
outgoing_requests: store outgoing_requests: store
.outgoing_contact_requests() .outgoing_contact_requests()
.iter() .iter()
.map(|user| user.github_login.clone()) .map(|user| user.github_login.clone().to_string())
.collect(), .collect(),
incoming_requests: store incoming_requests: store
.incoming_contact_requests() .incoming_contact_requests()
.iter() .iter()
.map(|user| user.github_login.clone()) .map(|user| user.github_login.clone().to_string())
.collect(), .collect(),
}) })
} }

View file

@ -940,7 +940,7 @@ impl CollabPanel {
room.read(cx).local_participant().role == proto::ChannelRole::Admin room.read(cx).local_participant().role == proto::ChannelRole::Admin
}); });
ListItem::new(SharedString::from(user.github_login.clone())) ListItem::new(user.github_login.clone())
.start_slot(Avatar::new(user.avatar_uri.clone())) .start_slot(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone())) .child(Label::new(user.github_login.clone()))
.toggle_state(is_selected) .toggle_state(is_selected)
@ -2583,7 +2583,7 @@ impl CollabPanel {
) -> impl IntoElement { ) -> impl IntoElement {
let online = contact.online; let online = contact.online;
let busy = contact.busy || calling; let busy = contact.busy || calling;
let github_login = SharedString::from(contact.user.github_login.clone()); let github_login = contact.user.github_login.clone();
let item = ListItem::new(github_login.clone()) let item = ListItem::new(github_login.clone())
.indent_level(1) .indent_level(1)
.indent_step_size(px(20.)) .indent_step_size(px(20.))
@ -2662,7 +2662,7 @@ impl CollabPanel {
is_selected: bool, is_selected: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let github_login = SharedString::from(user.github_login.clone()); let github_login = user.github_login.clone();
let user_id = user.id; let user_id = user.id;
let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user); let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user);
let color = if is_response_pending { let color = if is_response_pending {

View file

@ -2416,7 +2416,7 @@ impl GitPanel {
.committer_name .committer_name
.clone() .clone()
.or_else(|| participant.user.name.clone()) .or_else(|| participant.user.name.clone())
.unwrap_or_else(|| participant.user.github_login.clone()); .unwrap_or_else(|| participant.user.github_login.clone().to_string());
new_co_authors.push((name.clone(), email.clone())) new_co_authors.push((name.clone(), email.clone()))
} }
} }
@ -2436,7 +2436,7 @@ impl GitPanel {
.name .name
.clone() .clone()
.or_else(|| user.name.clone()) .or_else(|| user.name.clone())
.unwrap_or_else(|| user.github_login.clone()); .unwrap_or_else(|| user.github_login.clone().to_string());
Some((name, email)) Some((name, email))
} }

View file

@ -16,6 +16,7 @@ default = []
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
client.workspace = true
command_palette_hooks.workspace = true command_palette_hooks.workspace = true
db.workspace = true db.workspace = true
editor.workspace = true editor.workspace = true
@ -30,6 +31,7 @@ settings.workspace = true
theme.workspace = true theme.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
vim_mode_setting.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
workspace.workspace = true workspace.workspace = true
zed_actions.workspace = true zed_actions.workspace = true

View file

@ -1,16 +1,28 @@
use fs::Fs; use std::sync::Arc;
use gpui::{App, IntoElement, Window};
use settings::{Settings, update_settings_file};
use theme::{ThemeMode, ThemeSettings};
use ui::{SwitchField, ToggleButtonGroup, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*};
fn read_theme_selection(cx: &App) -> ThemeMode { use client::TelemetrySettings;
use fs::Fs;
use gpui::{App, IntoElement};
use settings::{BaseKeymap, Settings, update_settings_file};
use theme::{Appearance, SystemAppearance, ThemeMode, ThemeSettings};
use ui::{
SwitchField, ThemePreviewTile, ToggleButtonGroup, ToggleButtonSimple, ToggleButtonWithIcon,
prelude::*,
};
use vim_mode_setting::VimModeSetting;
use crate::Onboarding;
fn read_theme_selection(cx: &App) -> (ThemeMode, SharedString) {
let settings = ThemeSettings::get_global(cx); let settings = ThemeSettings::get_global(cx);
settings (
.theme_selection settings
.as_ref() .theme_selection
.and_then(|selection| selection.mode()) .as_ref()
.unwrap_or_default() .and_then(|selection| selection.mode())
.unwrap_or_default(),
settings.active_theme.name.clone(),
)
} }
fn write_theme_selection(theme_mode: ThemeMode, cx: &App) { fn write_theme_selection(theme_mode: ThemeMode, cx: &App) {
@ -21,9 +33,15 @@ fn write_theme_selection(theme_mode: ThemeMode, cx: &App) {
}); });
} }
fn render_theme_section(cx: &mut App) -> impl IntoElement { fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
let theme_mode = read_theme_selection(cx); let fs = <dyn Fs>::global(cx);
update_settings_file::<BaseKeymap>(fs, cx, move |setting, _| {
*setting = Some(keymap_base);
});
}
fn render_theme_section(theme_mode: ThemeMode) -> impl IntoElement {
h_flex().justify_between().child(Label::new("Theme")).child( h_flex().justify_between().child(Label::new("Theme")).child(
ToggleButtonGroup::single_row( ToggleButtonGroup::single_row(
"theme-selector-onboarding", "theme-selector-onboarding",
@ -49,55 +67,160 @@ fn render_theme_section(cx: &mut App) -> impl IntoElement {
) )
} }
fn render_telemetry_section() -> impl IntoElement { fn render_telemetry_section(fs: Arc<dyn Fs>, cx: &App) -> impl IntoElement {
v_flex() v_flex()
.gap_3()
.gap_4()
.child(Label::new("Telemetry").size(LabelSize::Large)) .child(Label::new("Telemetry").size(LabelSize::Large))
.child(SwitchField::new( .child(SwitchField::new(
"vim_mode", "onboarding-telemetry-metrics",
"Help Improve Zed", "Help Improve Zed",
"Sending anonymous usage data helps us build the right features and create the best experience.", "Sending anonymous usage data helps us build the right features and create the best experience.",
ui::ToggleState::Selected, 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::<TelemetrySettings>(
fs.clone(),
cx,
move |setting, _| setting.metrics = Some(enabled),
);
}},
)) ))
.child(SwitchField::new( .child(SwitchField::new(
"vim_mode", "onboarding-telemetry-crash-reports",
"Help Fix Zed", "Help Fix Zed",
"Send crash reports so we can fix critical issues fast.", "Send crash reports so we can fix critical issues fast.",
ui::ToggleState::Selected, 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::<TelemetrySettings>(
fs.clone(),
cx,
move |setting, _| setting.diagnostics = Some(enabled),
);
}
}
)) ))
} }
pub(crate) fn render_basics_page(_: &mut Window, cx: &mut App) -> impl IntoElement { pub(crate) fn render_basics_page(onboarding: &Onboarding, cx: &mut App) -> impl IntoElement {
let (theme_mode, active_theme_name) = read_theme_selection(cx);
let themes = match theme_mode {
ThemeMode::Dark => &onboarding.dark_themes,
ThemeMode::Light => &onboarding.light_themes,
ThemeMode::System => match SystemAppearance::global(cx).0 {
Appearance::Light => &onboarding.light_themes,
Appearance::Dark => &onboarding.dark_themes,
},
};
let base_keymap = match BaseKeymap::get_global(cx) {
BaseKeymap::VSCode => Some(0),
BaseKeymap::JetBrains => Some(1),
BaseKeymap::SublimeText => Some(2),
BaseKeymap::Atom => Some(3),
BaseKeymap::Emacs => Some(4),
BaseKeymap::Cursor => Some(5),
BaseKeymap::TextMate | BaseKeymap::None => None,
};
v_flex() v_flex()
.gap_6() .gap_6()
.child(render_theme_section(cx)) .child(render_theme_section(theme_mode))
.child(h_flex().children(
themes.iter().map(|theme| {
ThemePreviewTile::new(theme.clone(), active_theme_name == theme.name, 0.48)
.on_click({
let theme_name = theme.name.clone();
let fs = onboarding.fs.clone();
move |_, _, cx| {
let theme_name = theme_name.clone();
update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
settings.set_theme(theme_name.to_string(), SystemAppearance::global(cx).0);
});
}
})
})
))
.child( .child(
v_flex().gap_2().child(Label::new("Base Keymap")).child( v_flex().gap_2().child(Label::new("Base Keymap")).child(
ToggleButtonGroup::two_rows( ToggleButtonGroup::two_rows(
"multiple_row_test", "multiple_row_test",
[ [
ToggleButtonWithIcon::new("VS Code", IconName::AiZed, |_, _, _| {}), ToggleButtonWithIcon::new("VS Code", IconName::AiZed, |_, _, cx| {
ToggleButtonWithIcon::new("Jetbrains", IconName::AiZed, |_, _, _| {}), write_keymap_base(BaseKeymap::VSCode, cx);
ToggleButtonWithIcon::new("Sublime Text", IconName::AiZed, |_, _, _| {}), }),
ToggleButtonWithIcon::new("Jetbrains", IconName::AiZed, |_, _, cx| {
write_keymap_base(BaseKeymap::JetBrains, cx);
}),
ToggleButtonWithIcon::new("Sublime Text", IconName::AiZed, |_, _, cx| {
write_keymap_base(BaseKeymap::SublimeText, cx);
}),
], ],
[ [
ToggleButtonWithIcon::new("Atom", IconName::AiZed, |_, _, _| {}), ToggleButtonWithIcon::new("Atom", IconName::AiZed, |_, _, cx| {
ToggleButtonWithIcon::new("Emacs", IconName::AiZed, |_, _, _| {}), write_keymap_base(BaseKeymap::Atom, cx);
ToggleButtonWithIcon::new("Cursor (Beta)", IconName::AiZed, |_, _, _| {}), }),
ToggleButtonWithIcon::new("Emacs", IconName::AiZed, |_, _, cx| {
write_keymap_base(BaseKeymap::Emacs, cx);
}),
ToggleButtonWithIcon::new("Cursor (Beta)", IconName::AiZed, |_, _, cx| {
write_keymap_base(BaseKeymap::Cursor, cx);
}),
], ],
) )
.when_some(base_keymap, |this, base_keymap| this.selected_index(base_keymap))
.button_width(rems_from_px(230.)) .button_width(rems_from_px(230.))
.style(ui::ToggleButtonGroupStyle::Outlined) .style(ui::ToggleButtonGroupStyle::Outlined)
), ),
) )
.child(v_flex().justify_center().child(div().h_0().child("hack").invisible()).child(SwitchField::new( .child(v_flex().justify_center().child(div().h_0().child("hack").invisible()).child(SwitchField::new(
"vim_mode", "onboarding-vim-mode",
"Vim Mode", "Vim Mode",
"Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.", "Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.",
ui::ToggleState::Selected, if VimModeSetting::get_global(cx).0 {
|_, _, _| {}, ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
{
let fs = onboarding.fs.clone();
move |selection, _, cx| {
let enabled = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
ToggleState::Indeterminate => { return; },
};
update_settings_file::<VimModeSetting>(
fs.clone(),
cx,
move |setting, _| *setting = Some(enabled),
);
}
},
))) )))
.child(render_telemetry_section()) .child(render_telemetry_section(onboarding.fs.clone(), cx))
} }

View file

@ -1,4 +1,5 @@
use crate::welcome::{ShowWelcome, WelcomePage}; use crate::welcome::{ShowWelcome, WelcomePage};
use client::{Client, UserStore};
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
@ -12,11 +13,13 @@ use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use settings::{SettingsStore, VsCodeSettingsSource}; use settings::{SettingsStore, VsCodeSettingsSource};
use std::sync::Arc; use std::sync::Arc;
use ui::{FluentBuilder, KeyBinding, Vector, VectorName, prelude::*, rems_from_px}; use theme::{Theme, ThemeRegistry};
use ui::{Avatar, FluentBuilder, KeyBinding, Vector, VectorName, prelude::*, rems_from_px};
use workspace::{ use workspace::{
AppState, Workspace, WorkspaceId, AppState, Workspace, WorkspaceId,
dock::DockPosition, dock::DockPosition,
item::{Item, ItemEvent}, item::{Item, ItemEvent},
notifications::NotifyResultExt as _,
open_new, with_active_or_new_workspace, open_new, with_active_or_new_workspace,
}; };
@ -72,7 +75,11 @@ pub fn init(cx: &mut App) {
if let Some(existing) = existing { if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx); workspace.activate_item(&existing, true, true, window, cx);
} else { } else {
let settings_page = Onboarding::new(workspace.weak_handle(), cx); let settings_page = Onboarding::new(
workspace.weak_handle(),
workspace.user_store().clone(),
cx,
);
workspace.add_item_to_active_pane( workspace.add_item_to_active_pane(
Box::new(settings_page), Box::new(settings_page),
None, None,
@ -188,7 +195,8 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
|workspace, window, cx| { |workspace, window, cx| {
{ {
workspace.toggle_dock(DockPosition::Left, window, cx); workspace.toggle_dock(DockPosition::Left, window, cx);
let onboarding_page = Onboarding::new(workspace.weak_handle(), cx); let onboarding_page =
Onboarding::new(workspace.weak_handle(), workspace.user_store().clone(), cx);
workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx); workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
window.focus(&onboarding_page.focus_handle(cx)); window.focus(&onboarding_page.focus_handle(cx));
@ -211,17 +219,51 @@ enum SelectedPage {
struct Onboarding { struct Onboarding {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
light_themes: [Arc<Theme>; 3],
dark_themes: [Arc<Theme>; 3],
focus_handle: FocusHandle, focus_handle: FocusHandle,
selected_page: SelectedPage, selected_page: SelectedPage,
fs: Arc<dyn Fs>,
user_store: Entity<UserStore>,
_settings_subscription: Subscription, _settings_subscription: Subscription,
} }
impl Onboarding { impl Onboarding {
fn new(workspace: WeakEntity<Workspace>, cx: &mut App) -> Entity<Self> { fn new(
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
cx: &mut App,
) -> Entity<Self> {
let theme_registry = ThemeRegistry::global(cx);
let one_dark = theme_registry
.get("One Dark")
.expect("Default themes are always present");
let ayu_dark = theme_registry
.get("Ayu Dark")
.expect("Default themes are always present");
let gruvbox_dark = theme_registry
.get("Gruvbox Dark")
.expect("Default themes are always present");
let one_light = theme_registry
.get("One Light")
.expect("Default themes are always present");
let ayu_light = theme_registry
.get("Ayu Light")
.expect("Default themes are always present");
let gruvbox_light = theme_registry
.get("Gruvbox Light")
.expect("Default themes are always present");
cx.new(|cx| Self { cx.new(|cx| Self {
workspace, workspace,
user_store,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
light_themes: [one_light, ayu_light, gruvbox_light],
dark_themes: [one_dark, ayu_dark, gruvbox_dark],
selected_page: SelectedPage::Basics, selected_page: SelectedPage::Basics,
fs: <dyn Fs>::global(cx),
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()), _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
}) })
} }
@ -339,16 +381,37 @@ impl Onboarding {
), ),
) )
.child( .child(
Button::new("sign_in", "Sign In") if let Some(user) = self.user_store.read(cx).current_user() {
.style(ButtonStyle::Outlined) h_flex()
.full_width(), .gap_2()
.child(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
.into_any_element()
} else {
Button::new("sign_in", "Sign In")
.style(ButtonStyle::Outlined)
.full_width()
.on_click(|_, window, cx| {
let client = Client::global(cx);
window
.spawn(cx, async move |cx| {
client
.authenticate_and_connect(true, &cx)
.await
.into_response()
.notify_async_err(cx);
})
.detach();
})
.into_any_element()
},
) )
} }
fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
match self.selected_page { match self.selected_page {
SelectedPage::Basics => { SelectedPage::Basics => {
crate::basics_page::render_basics_page(window, cx).into_any_element() crate::basics_page::render_basics_page(&self, cx).into_any_element()
} }
SelectedPage::Editing => { SelectedPage::Editing => {
crate::editing_page::render_editing_page(window, cx).into_any_element() crate::editing_page::render_editing_page(window, cx).into_any_element()
@ -420,7 +483,11 @@ impl Item for Onboarding {
_: &mut Window, _: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<Entity<Self>> { ) -> Option<Entity<Self>> {
Some(Onboarding::new(self.workspace.clone(), cx)) Some(Onboarding::new(
self.workspace.clone(),
self.user_store.clone(),
cx,
))
} }
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {

View file

@ -34,6 +34,7 @@ mod stack;
mod sticky_items; mod sticky_items;
mod tab; mod tab;
mod tab_bar; mod tab_bar;
mod theme_preview;
mod toggle; mod toggle;
mod tooltip; mod tooltip;
@ -76,6 +77,7 @@ pub use stack::*;
pub use sticky_items::*; pub use sticky_items::*;
pub use tab::*; pub use tab::*;
pub use tab_bar::*; pub use tab_bar::*;
pub use theme_preview::*;
pub use toggle::*; pub use toggle::*;
pub use tooltip::*; pub use tooltip::*;

View file

@ -431,15 +431,17 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
{ {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let entries = self.rows.into_iter().enumerate().map(|(row_index, row)| { let entries = self.rows.into_iter().enumerate().map(|(row_index, row)| {
row.into_iter().enumerate().map(move |(index, button)| { row.into_iter().enumerate().map(move |(col_index, button)| {
let ButtonConfiguration { let ButtonConfiguration {
label, label,
icon, icon,
on_click, on_click,
} = button.into_configuration(); } = button.into_configuration();
ButtonLike::new((self.group_name, row_index * COLS + index)) let entry_index = row_index * COLS + col_index;
.when(index == self.selected_index, |this| {
ButtonLike::new((self.group_name, entry_index))
.when(entry_index == self.selected_index, |this| {
this.toggle_state(true) this.toggle_state(true)
.selected_style(ButtonStyle::Tinted(TintColor::Accent)) .selected_style(ButtonStyle::Tinted(TintColor::Accent))
}) })
@ -451,10 +453,12 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
h_flex() h_flex()
.min_w(self.button_width) .min_w(self.button_width)
.gap_1p5() .gap_1p5()
.px_3()
.py_1()
.justify_center() .justify_center()
.when_some(icon, |this, icon| { .when_some(icon, |this, icon| {
this.child(Icon::new(icon).size(IconSize::XSmall).map(|this| { this.child(Icon::new(icon).size(IconSize::XSmall).map(|this| {
if index == self.selected_index { if entry_index == self.selected_index {
this.color(Color::Accent) this.color(Color::Accent)
} else { } else {
this.color(Color::Muted) this.color(Color::Muted)
@ -462,9 +466,11 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
})) }))
}) })
.child( .child(
Label::new(label).when(index == self.selected_index, |this| { Label::new(label)
this.color(Color::Accent) .size(LabelSize::Small)
}), .when(entry_index == self.selected_index, |this| {
this.color(Color::Accent)
}),
), ),
) )
.on_click(on_click) .on_click(on_click)

View file

@ -1,10 +1,7 @@
#![allow(unused, dead_code)] use crate::{component_prelude::Documented, prelude::*, utils::inner_corner_radius};
use gpui::{Hsla, Length}; use gpui::{App, ClickEvent, Hsla, IntoElement, Length, RenderOnce, Window};
use std::sync::Arc; use std::{rc::Rc, sync::Arc};
use theme::{Theme, ThemeRegistry}; use theme::{Theme, ThemeRegistry};
use ui::{
IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius,
};
/// Shows a preview of a theme as an abstract illustration /// Shows a preview of a theme as an abstract illustration
/// of a thumbnail-sized editor. /// of a thumbnail-sized editor.
@ -12,6 +9,7 @@ use ui::{
pub struct ThemePreviewTile { pub struct ThemePreviewTile {
theme: Arc<Theme>, theme: Arc<Theme>,
selected: bool, selected: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
seed: f32, seed: f32,
} }
@ -19,8 +17,9 @@ impl ThemePreviewTile {
pub fn new(theme: Arc<Theme>, selected: bool, seed: f32) -> Self { pub fn new(theme: Arc<Theme>, selected: bool, seed: f32) -> Self {
Self { Self {
theme, theme,
selected,
seed, seed,
selected,
on_click: None,
} }
} }
@ -28,10 +27,18 @@ impl ThemePreviewTile {
self.selected = selected; self.selected = selected;
self self
} }
pub fn on_click(
mut self,
listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some(Rc::new(listener));
self
}
} }
impl RenderOnce for ThemePreviewTile { impl RenderOnce for ThemePreviewTile {
fn render(self, _window: &mut ui::Window, _cx: &mut ui::App) -> impl IntoElement { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let color = self.theme.colors(); let color = self.theme.colors();
let root_radius = px(8.0); let root_radius = px(8.0);
@ -181,6 +188,13 @@ impl RenderOnce for ThemePreviewTile {
let content = div().size_full().flex().child(sidebar).child(pane); let content = div().size_full().flex().child(sidebar).child(pane);
div() div()
// Note: If two theme preview tiles are rendering the same theme they'll share an ID
// this will mean on hover and on click events will be shared between them
.id(SharedString::from(self.theme.id.clone()))
.when_some(self.on_click.clone(), |this, on_click| {
this.on_click(move |event, window, cx| on_click(event, window, cx))
.hover(|style| style.cursor_pointer().border_color(color.element_hover))
})
.size_full() .size_full()
.rounded(root_radius) .rounded(root_radius)
.p(root_padding) .p(root_padding)
@ -261,7 +275,7 @@ impl Component for ThemePreviewTile {
themes_to_preview themes_to_preview
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, theme)| { .map(|(_, theme)| {
div().w(px(200.)).h(px(140.)).child(ThemePreviewTile::new( div().w(px(200.)).h(px(140.)).child(ThemePreviewTile::new(
theme.clone(), theme.clone(),
false, false,

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
AnyElement, AnyView, ClickEvent, CursorStyle, ElementId, Hsla, IntoElement, Styled, Window, AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla,
div, hsla, prelude::*, prelude::*,
}; };
use std::sync::Arc; use std::sync::Arc;
@ -610,7 +610,7 @@ impl RenderOnce for SwitchField {
h_flex() h_flex()
.id(SharedString::from(format!("{}-container", self.id))) .id(SharedString::from(format!("{}-container", self.id)))
.when(!self.disabled, |this| { .when(!self.disabled, |this| {
this.hover(|this| this.cursor(CursorStyle::PointingHand)) this.hover(|this| this.cursor_pointer())
}) })
.w_full() .w_full()
.gap_4() .gap_4()

View file

@ -29,7 +29,6 @@ project.workspace = true
serde.workspace = true serde.workspace = true
settings.workspace = true settings.workspace = true
telemetry.workspace = true telemetry.workspace = true
theme.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
vim_mode_setting.workspace = true vim_mode_setting.workspace = true

View file

@ -21,7 +21,6 @@ pub use multibuffer_hint::*;
mod base_keymap_picker; mod base_keymap_picker;
mod multibuffer_hint; mod multibuffer_hint;
mod welcome_ui;
actions!( actions!(
welcome, welcome,

View file

@ -1 +0,0 @@
mod theme_preview;