onboarding: Add design adjustments (#35480)

Release Notes:

- N/A

---------

Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
Danilo Leal
2025-08-01 15:08:15 -03:00
committed by GitHub
co-authored by Anthony
parent b31f893408
commit faa45c53d7
17 changed files with 348 additions and 185 deletions
+7
View File
@@ -107,6 +107,12 @@ pub enum IconName {
Disconnected,
DocumentText,
Download,
EditorAtom,
EditorCursor,
EditorEmacs,
EditorJetBrains,
EditorSublime,
EditorVsCode,
Ellipsis,
EllipsisVertical,
Envelope,
@@ -229,6 +235,7 @@ pub enum IconName {
Server,
Settings,
SettingsAlt,
ShieldCheck,
Shift,
Slash,
SlashSquare,
+14 -17
View File
@@ -43,6 +43,8 @@ fn render_llm_provider_section(
}
fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement {
let privacy_badge = || Badge::new("Privacy").icon(IconName::ShieldCheck);
v_flex()
.relative()
.pt_2()
@@ -71,7 +73,7 @@ fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement {
.size(IconSize::XSmall),
),
)
.child(Badge::new("PRIVACY").icon(IconName::FileLock)),
.child(privacy_badge()),
)
.child(
Label::new("Re-enable it any time in Settings.")
@@ -85,22 +87,17 @@ fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement {
.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",
);
}),
),
h_flex().gap_1().child(privacy_badge()).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(
+65 -80
View File
@@ -48,7 +48,11 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme_registry = ThemeRegistry::global(cx);
let current_theme_name = theme_selection.theme(appearance);
let theme_mode = theme_selection.mode();
let theme_mode = theme_selection.mode().unwrap_or_default();
// let theme_mode = theme_selection.mode();
// TODO: Clean this up once the "System" button inside the
// toggle button group is done
let selected_index = match appearance {
Appearance::Light => 0,
@@ -72,8 +76,28 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
let is_selected = theme.name == current_theme_name;
let name = theme.name.clone();
let colors = cx.theme().colors();
v_flex()
.id(name.clone())
.w_full()
.items_center()
.gap_1()
.child(
div()
.w_full()
.border_2()
.border_color(colors.border_transparent)
.rounded(ThemePreviewTile::CORNER_RADIUS)
.map(|this| {
if is_selected {
this.border_color(colors.border_selected)
} else {
this.opacity(0.8).hover(|s| s.border_color(colors.border))
}
})
.child(ThemePreviewTile::new(theme.clone(), theme_seed)),
)
.child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
.on_click({
let theme_name = theme.name.clone();
move |_, _, cx| {
@@ -84,84 +108,45 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
});
}
})
.flex_1()
.child(
div()
.border_2()
.border_color(colors.border_transparent)
.rounded(ThemePreviewTile::CORNER_RADIUS)
.hover(|mut style| {
if !is_selected {
style.border_color = Some(colors.element_hover);
}
style
})
.when(is_selected, |this| {
this.border_color(colors.border_selected)
})
.cursor_pointer()
.child(ThemePreviewTile::new(theme, theme_seed)),
)
.child(
h_flex()
.justify_center()
.items_baseline()
.child(Label::new(name).color(Color::Muted)),
)
});
return v_flex()
.gap_2()
.child(
h_flex().justify_between().child(Label::new("Theme")).child(
h_flex()
.gap_2()
.child(
ToggleButtonGroup::single_row(
"theme-selector-onboarding-dark-light",
[
ToggleButtonSimple::new("Light", {
let appearance_state = appearance_state.clone();
move |_, _, cx| {
write_appearance_change(
&appearance_state,
Appearance::Light,
cx,
);
}
}),
ToggleButtonSimple::new("Dark", {
let appearance_state = appearance_state.clone();
move |_, _, cx| {
write_appearance_change(
&appearance_state,
Appearance::Dark,
cx,
);
}
}),
],
)
.selected_index(selected_index)
.style(ui::ToggleButtonGroupStyle::Outlined)
.button_width(rems_from_px(64.)),
)
.child(
ToggleButtonGroup::single_row(
"theme-selector-onboarding-system",
[ToggleButtonSimple::new("System", {
let theme = theme_selection.clone();
move |_, _, cx| {
toggle_system_theme_mode(theme.clone(), appearance, cx);
}
})],
)
.selected_index((theme_mode != Some(ThemeMode::System)) as usize)
.style(ui::ToggleButtonGroupStyle::Outlined)
.button_width(rems_from_px(64.)),
),
ToggleButtonGroup::single_row(
"theme-selector-onboarding-dark-light",
[
ToggleButtonSimple::new("Light", {
let appearance_state = appearance_state.clone();
move |_, _, cx| {
write_appearance_change(&appearance_state, Appearance::Light, cx);
}
}),
ToggleButtonSimple::new("Dark", {
let appearance_state = appearance_state.clone();
move |_, _, cx| {
write_appearance_change(&appearance_state, Appearance::Dark, cx);
}
}),
// TODO: Properly put the System back as a button within this group
// Currently, given "System" is not an option in the Appearance enum,
// this button doesn't get selected
ToggleButtonSimple::new("System", {
let theme = theme_selection.clone();
move |_, _, cx| {
toggle_system_theme_mode(theme.clone(), appearance, cx);
}
})
.selected(theme_mode == ThemeMode::System),
],
)
.selected_index(selected_index)
.style(ui::ToggleButtonGroupStyle::Outlined)
.button_width(rems_from_px(64.)),
),
)
.child(h_flex().justify_between().children(theme_previews));
.child(h_flex().gap_4().justify_between().children(theme_previews));
fn write_appearance_change(
appearance_state: &Entity<Appearance>,
@@ -210,7 +195,6 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
};
ThemeSelection::Dynamic { mode, light, dark }
}
ThemeSelection::Dynamic {
mode: _,
light,
@@ -311,30 +295,31 @@ pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl Into
ToggleButtonGroup::two_rows(
"multiple_row_test",
[
ToggleButtonWithIcon::new("VS Code", IconName::AiZed, |_, _, cx| {
ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
write_keymap_base(BaseKeymap::VSCode, cx);
}),
ToggleButtonWithIcon::new("Jetbrains", IconName::AiZed, |_, _, cx| {
ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| {
write_keymap_base(BaseKeymap::JetBrains, cx);
}),
ToggleButtonWithIcon::new("Sublime Text", IconName::AiZed, |_, _, cx| {
ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {
write_keymap_base(BaseKeymap::SublimeText, cx);
}),
],
[
ToggleButtonWithIcon::new("Atom", IconName::AiZed, |_, _, cx| {
ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| {
write_keymap_base(BaseKeymap::Atom, cx);
}),
ToggleButtonWithIcon::new("Emacs", IconName::AiZed, |_, _, cx| {
ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
write_keymap_base(BaseKeymap::Emacs, cx);
}),
ToggleButtonWithIcon::new("Cursor (Beta)", IconName::AiZed, |_, _, cx| {
ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| {
write_keymap_base(BaseKeymap::Cursor, cx);
}),
],
)
.when_some(base_keymap, |this, base_keymap| this.selected_index(base_keymap))
.button_width(rems_from_px(230.))
.button_width(rems_from_px(216.))
.size(ui::ToggleButtonGroupSize::Medium)
.style(ui::ToggleButtonGroupStyle::Outlined)
),
)
+2 -2
View File
@@ -143,7 +143,7 @@ fn render_import_settings_section() -> impl IntoElement {
.gap_1p5()
.px_1()
.child(
Icon::new(IconName::Sparkle)
Icon::new(IconName::EditorVsCode)
.color(Color::Muted)
.size(IconSize::XSmall),
)
@@ -169,7 +169,7 @@ fn render_import_settings_section() -> impl IntoElement {
.gap_1p5()
.px_1()
.child(
Icon::new(IconName::Sparkle)
Icon::new(IconName::EditorCursor)
.color(Color::Muted)
.size(IconSize::XSmall),
)
+64 -3
View File
@@ -14,8 +14,8 @@ use serde::Deserialize;
use settings::{SettingsStore, VsCodeSettingsSource};
use std::sync::Arc;
use ui::{
Avatar, FluentBuilder, Headline, KeyBinding, ParentElement as _, StatefulInteractiveElement,
Vector, VectorName, prelude::*, rems_from_px,
Avatar, ButtonLike, FluentBuilder, Headline, KeyBinding, ParentElement as _,
StatefulInteractiveElement, Vector, VectorName, prelude::*, rems_from_px,
};
use workspace::{
AppState, Workspace, WorkspaceId,
@@ -344,12 +344,73 @@ impl Onboarding {
.into_element(),
]),
)
.child(Button::new("skip_all", "Skip All")),
.child(
ButtonLike::new("skip_all")
.child(Label::new("Skip All").ml_1())
.on_click(|_, _, cx| {
with_active_or_new_workspace(
cx,
|workspace, window, cx| {
let Some((onboarding_id, onboarding_idx)) =
workspace
.active_pane()
.read(cx)
.items()
.enumerate()
.find_map(|(idx, item)| {
let _ =
item.downcast::<Onboarding>()?;
Some((item.item_id(), idx))
})
else {
return;
};
workspace.active_pane().update(cx, |pane, cx| {
// Get the index here to get around the borrow checker
let idx = pane.items().enumerate().find_map(
|(idx, item)| {
let _ =
item.downcast::<WelcomePage>()?;
Some(idx)
},
);
if let Some(idx) = idx {
pane.activate_item(
idx, true, true, window, cx,
);
} else {
let item =
Box::new(WelcomePage::new(window, cx));
pane.add_item(
item,
true,
true,
Some(onboarding_idx),
window,
cx,
);
}
pane.remove_item(
onboarding_id,
false,
false,
window,
cx,
);
});
},
);
}),
),
),
)
.child(
if let Some(user) = self.user_store.read(cx).current_user() {
h_flex()
.pl_1p5()
.gap_2()
.child(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
+12 -27
View File
@@ -35,7 +35,7 @@ impl RenderOnce for ThemePreviewTile {
let item_skeleton = |w: Length, h: Pixels, bg: Hsla| div().w(w).h(h).rounded_full().bg(bg);
let skeleton_height = px(4.);
let skeleton_height = px(2.);
let sidebar_seeded_width = |seed: f32, index: usize| {
let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5;
@@ -62,12 +62,10 @@ impl RenderOnce for ThemePreviewTile {
.border_color(color.border_transparent)
.bg(color.panel_background)
.child(
div()
v_flex()
.p_2()
.flex()
.flex_col()
.size_full()
.gap(px(4.))
.gap_1()
.children(sidebar_skeleton),
);
@@ -143,32 +141,19 @@ impl RenderOnce for ThemePreviewTile {
v_flex()
.size_full()
.p_1()
.gap(px(6.))
.gap_1p5()
.children(lines)
.into_any_element()
};
let pane = div()
.h_full()
.flex_grow()
.flex()
.flex_col()
// .child(
// div()
// .w_full()
// .border_color(color.border)
// .border_b(px(1.))
// .h(relative(0.1))
// .bg(color.tab_bar_background),
// )
.child(
div()
.size_full()
.overflow_hidden()
.bg(color.editor_background)
.p_2()
.child(pseudo_code_skeleton(self.theme.clone(), self.seed)),
);
let pane = v_flex().h_full().flex_grow().child(
div()
.size_full()
.overflow_hidden()
.bg(color.editor_background)
.p_2()
.child(pseudo_code_skeleton(self.theme.clone(), self.seed)),
);
let content = div().size_full().flex().child(sidebar).child(pane);
+63 -3
View File
@@ -4,11 +4,14 @@ use gpui::{
};
use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
use workspace::{
NewFile, Open, Workspace, WorkspaceId,
NewFile, Open, WorkspaceId,
item::{Item, ItemEvent},
with_active_or_new_workspace,
};
use zed_actions::{Extensions, OpenSettings, agent, command_palette};
use crate::{Onboarding, OpenOnboarding};
actions!(
zed,
[
@@ -216,7 +219,64 @@ impl Render for WelcomePage {
div().child(
Button::new("welcome-exit", "Return to Setup")
.full_width()
.label_size(LabelSize::XSmall),
.label_size(LabelSize::XSmall)
.on_click(|_, window, cx| {
window.dispatch_action(
OpenOnboarding.boxed_clone(),
cx,
);
with_active_or_new_workspace(cx, |workspace, window, cx| {
let Some((welcome_id, welcome_idx)) = workspace
.active_pane()
.read(cx)
.items()
.enumerate()
.find_map(|(idx, item)| {
let _ = item.downcast::<WelcomePage>()?;
Some((item.item_id(), idx))
})
else {
return;
};
workspace.active_pane().update(cx, |pane, cx| {
// Get the index here to get around the borrow checker
let idx = pane.items().enumerate().find_map(
|(idx, item)| {
let _ =
item.downcast::<Onboarding>()?;
Some(idx)
},
);
if let Some(idx) = idx {
pane.activate_item(
idx, true, true, window, cx,
);
} else {
let item =
Box::new(Onboarding::new(workspace, cx));
pane.add_item(
item,
true,
true,
Some(welcome_idx),
window,
cx,
);
}
pane.remove_item(
welcome_id,
false,
false,
window,
cx,
);
});
});
}),
),
),
),
@@ -227,7 +287,7 @@ impl Render for WelcomePage {
}
impl WelcomePage {
pub fn new(window: &mut Window, cx: &mut Context<Workspace>) -> Entity<Self> {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
+2 -7
View File
@@ -32,7 +32,7 @@ impl RenderOnce for Badge {
.pl_1()
.pr_2()
.border_1()
.border_color(cx.theme().colors().border)
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(cx.theme().colors().element_background)
.rounded_sm()
.overflow_hidden()
@@ -42,12 +42,7 @@ impl RenderOnce for Badge {
.color(Color::Muted),
)
.child(Divider::vertical().color(DividerColor::Border))
.child(
Label::new(self.label.clone())
.size(LabelSize::XSmall)
.buffer_font(cx)
.ml_1(),
)
.child(Label::new(self.label.clone()).size(LabelSize::Small).ml_1())
}
}
@@ -358,6 +358,7 @@ impl ButtonStyle {
#[derive(Default, PartialEq, Clone, Copy)]
pub enum ButtonSize {
Large,
Medium,
#[default]
Default,
Compact,
@@ -368,6 +369,7 @@ impl ButtonSize {
pub fn rems(self) -> Rems {
match self {
ButtonSize::Large => rems_from_px(32.),
ButtonSize::Medium => rems_from_px(28.),
ButtonSize::Default => rems_from_px(22.),
ButtonSize::Compact => rems_from_px(18.),
ButtonSize::None => rems_from_px(16.),
@@ -573,7 +575,7 @@ impl RenderOnce for ButtonLike {
})
.gap(DynamicSpacing::Base04.rems(cx))
.map(|this| match self.size {
ButtonSize::Large => this.px(DynamicSpacing::Base06.rems(cx)),
ButtonSize::Large | ButtonSize::Medium => this.px(DynamicSpacing::Base06.rems(cx)),
ButtonSize::Default | ButtonSize::Compact => {
this.px(DynamicSpacing::Base04.rems(cx))
}
@@ -295,6 +295,7 @@ pub struct ButtonConfiguration {
label: SharedString,
icon: Option<IconName>,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
selected: bool,
}
mod private {
@@ -308,6 +309,7 @@ pub trait ButtonBuilder: 'static + private::ToggleButtonStyle {
pub struct ToggleButtonSimple {
label: SharedString,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
selected: bool,
}
impl ToggleButtonSimple {
@@ -318,8 +320,14 @@ impl ToggleButtonSimple {
Self {
label: label.into(),
on_click: Box::new(on_click),
selected: false,
}
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl private::ToggleButtonStyle for ToggleButtonSimple {}
@@ -330,6 +338,7 @@ impl ButtonBuilder for ToggleButtonSimple {
label: self.label,
icon: None,
on_click: self.on_click,
selected: self.selected,
}
}
}
@@ -338,6 +347,7 @@ pub struct ToggleButtonWithIcon {
label: SharedString,
icon: IconName,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
selected: bool,
}
impl ToggleButtonWithIcon {
@@ -350,8 +360,14 @@ impl ToggleButtonWithIcon {
label: label.into(),
icon,
on_click: Box::new(on_click),
selected: false,
}
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl private::ToggleButtonStyle for ToggleButtonWithIcon {}
@@ -362,6 +378,7 @@ impl ButtonBuilder for ToggleButtonWithIcon {
label: self.label,
icon: Some(self.icon),
on_click: self.on_click,
selected: self.selected,
}
}
}
@@ -373,6 +390,12 @@ pub enum ToggleButtonGroupStyle {
Outlined,
}
#[derive(Clone, Copy, PartialEq)]
pub enum ToggleButtonGroupSize {
Default,
Medium,
}
#[derive(IntoElement)]
pub struct ToggleButtonGroup<T, const COLS: usize = 3, const ROWS: usize = 1>
where
@@ -381,6 +404,7 @@ where
group_name: &'static str,
rows: [[T; COLS]; ROWS],
style: ToggleButtonGroupStyle,
size: ToggleButtonGroupSize,
button_width: Rems,
selected_index: usize,
}
@@ -391,6 +415,7 @@ impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> {
group_name,
rows: [buttons],
style: ToggleButtonGroupStyle::Transparent,
size: ToggleButtonGroupSize::Default,
button_width: rems_from_px(100.),
selected_index: 0,
}
@@ -403,6 +428,7 @@ impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS, 2> {
group_name,
rows: [first_row, second_row],
style: ToggleButtonGroupStyle::Transparent,
size: ToggleButtonGroupSize::Default,
button_width: rems_from_px(100.),
selected_index: 0,
}
@@ -415,6 +441,11 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> ToggleButtonGroup<T
self
}
pub fn size(mut self, size: ToggleButtonGroupSize) -> Self {
self.size = size;
self
}
pub fn button_width(mut self, button_width: Rems) -> Self {
self.button_width = button_width;
self
@@ -430,53 +461,56 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
for ToggleButtonGroup<T, COLS, ROWS>
{
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let entries = self.rows.into_iter().enumerate().map(|(row_index, row)| {
row.into_iter().enumerate().map(move |(col_index, button)| {
let ButtonConfiguration {
label,
icon,
on_click,
} = button.into_configuration();
let entries =
self.rows.into_iter().enumerate().map(|(row_index, row)| {
row.into_iter().enumerate().map(move |(col_index, button)| {
let ButtonConfiguration {
label,
icon,
on_click,
selected,
} = button.into_configuration();
let entry_index = row_index * COLS + col_index;
let entry_index = row_index * COLS + col_index;
ButtonLike::new((self.group_name, entry_index))
.when(entry_index == self.selected_index, |this| {
this.toggle_state(true)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
})
.rounding(None)
.when(self.style == ToggleButtonGroupStyle::Filled, |button| {
button.style(ButtonStyle::Filled)
})
.child(
h_flex()
.min_w(self.button_width)
.gap_1p5()
.px_3()
.py_1()
.justify_center()
.when_some(icon, |this, icon| {
this.child(Icon::new(icon).size(IconSize::XSmall).map(|this| {
if entry_index == self.selected_index {
this.color(Color::Accent)
} else {
this.color(Color::Muted)
}
}))
})
.child(
Label::new(label)
.size(LabelSize::Small)
.when(entry_index == self.selected_index, |this| {
this.color(Color::Accent)
}),
),
)
.on_click(on_click)
.into_any_element()
})
});
ButtonLike::new((self.group_name, entry_index))
.when(entry_index == self.selected_index || selected, |this| {
this.toggle_state(true)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
})
.rounding(None)
.when(self.style == ToggleButtonGroupStyle::Filled, |button| {
button.style(ButtonStyle::Filled)
})
.when(self.size == ToggleButtonGroupSize::Medium, |button| {
button.size(ButtonSize::Medium)
})
.child(
h_flex()
.min_w(self.button_width)
.gap_1p5()
.px_3()
.py_1()
.justify_center()
.when_some(icon, |this, icon| {
this.py_2()
.child(Icon::new(icon).size(IconSize::XSmall).map(|this| {
if entry_index == self.selected_index || selected {
this.color(Color::Accent)
} else {
this.color(Color::Muted)
}
}))
})
.child(Label::new(label).size(LabelSize::Small).when(
entry_index == self.selected_index || selected,
|this| this.color(Color::Accent),
)),
)
.on_click(on_click)
.into_any_element()
})
});
let border_color = cx.theme().colors().border.opacity(0.6);
let is_outlined_or_filled = self.style == ToggleButtonGroupStyle::Outlined