onboarding: Add design adjustments (#35480)

Danilo Leal and Anthony created

Release Notes:

- N/A

---------

Co-authored-by: Anthony <anthony@zed.dev>

Change summary

assets/icons/editor_atom.svg                     |   1 
assets/icons/editor_cursor.svg                   |   9 +
assets/icons/editor_emacs.svg                    |   2 
assets/icons/editor_jet_brains.svg               |   1 
assets/icons/editor_sublime.svg                  |   5 
assets/icons/editor_vs_code.svg                  |   3 
assets/icons/shield_check.svg                    |   4 
crates/icons/src/icons.rs                        |   7 
crates/onboarding/src/ai_setup_page.rs           |  31 +--
crates/onboarding/src/basics_page.rs             | 145 ++++++++---------
crates/onboarding/src/editing_page.rs            |   4 
crates/onboarding/src/onboarding.rs              |  67 +++++++
crates/onboarding/src/theme_preview.rs           |  39 +---
crates/onboarding/src/welcome.rs                 |  66 +++++++
crates/ui/src/components/badge.rs                |   9 
crates/ui/src/components/button/button_like.rs   |   4 
crates/ui/src/components/button/toggle_button.rs | 128 ++++++++++-----
17 files changed, 338 insertions(+), 187 deletions(-)

Detailed changes

assets/icons/editor_cursor.svg 🔗

@@ -0,0 +1,9 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path opacity="0.6" d="M3.5 11V5.5L8.5 8L3.5 11Z" fill="black"/>
+<path opacity="0.4" d="M8.5 14L3.5 11L8.5 8V14Z" fill="black"/>
+<path opacity="0.6" d="M8.5 5.5H3.5L8.5 2.5L8.5 5.5Z" fill="black"/>
+<path opacity="0.8" d="M8.5 5.5V2.5L13.5 5.5H8.5Z" fill="black"/>
+<path opacity="0.2" d="M13.5 11L8.5 14L11 9.5L13.5 11Z" fill="black"/>
+<path opacity="0.5" d="M13.5 11L11 9.5L13.5 5V11Z" fill="black"/>
+<path d="M3.5 11V5L8.5 2.11325L13.5 5V11L8.5 13.8868L3.5 11Z" stroke="black"/>
+</svg>

assets/icons/editor_emacs.svg 🔗

@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_2716_663)">

assets/icons/editor_sublime.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0945 8.01611C13.0945 7.87619 12.9911 7.79551 12.8642 7.8356L4.13456 10.6038C4.00742 10.6441 3.90427 10.7904 3.90427 10.9301V13.7593C3.90427 13.8992 4.00742 13.9801 4.13456 13.9398L12.8642 11.1719C12.9911 11.1315 13.0945 10.9852 13.0945 10.8453V8.01611Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.90427 7.92597C3.90427 8.06588 4.00742 8.21218 4.13456 8.25252L12.8655 11.0209C12.9926 11.0613 13.0958 10.9803 13.0958 10.8407V8.01124C13.0958 7.87158 12.9926 7.72529 12.8655 7.68494L4.13456 4.91652C4.00742 4.87618 3.90427 4.95686 3.90427 5.09677V7.92597Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0945 2.20248C13.0945 2.06256 12.9911 1.98163 12.8642 2.02197L4.13456 4.78988C4.00742 4.83022 3.90427 4.97652 3.90427 5.11644V7.94563C3.90427 8.08554 4.00742 8.16622 4.13456 8.12614L12.8642 5.35797C12.9911 5.31763 13.0945 5.17133 13.0945 5.03167V2.20248Z" fill="black"/>
+</svg>

assets/icons/editor_vs_code.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0094 13.9181C11.1984 13.9917 11.4139 13.987 11.6047 13.8952L14.0753 12.7064C14.3349 12.5814 14.5 12.3187 14.5 12.0305V3.9696C14.5 3.68136 14.3349 3.41862 14.0753 3.2937L11.6047 2.10485C11.3543 1.98438 11.0614 2.01389 10.8416 2.17363C10.8102 2.19645 10.7803 2.22193 10.7523 2.25001L6.02261 6.56498L3.96246 5.00115C3.77068 4.85558 3.50244 4.86751 3.32432 5.02953L2.66356 5.63059C2.44569 5.82877 2.44544 6.17152 2.66302 6.37004L4.44965 8.00001L2.66302 9.62998C2.44544 9.82849 2.44569 10.1713 2.66356 10.3694L3.32432 10.9705C3.50244 11.1325 3.77068 11.1444 3.96246 10.9989L6.02261 9.43504L10.7523 13.75C10.8271 13.8249 10.915 13.8812 11.0094 13.9181ZM11.5018 5.27587L7.91309 8.00001L11.5018 10.7241V5.27587Z" fill="black"/>
+</svg>

assets/icons/shield_check.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.0001 8.62505C13.0001 11.75 10.8126 13.3125 8.21266 14.2187C8.07651 14.2648 7.92862 14.2626 7.79392 14.2125C5.18771 13.3125 3.00024 11.75 3.00024 8.62505V4.25012C3.00024 4.08436 3.06609 3.92539 3.1833 3.80818C3.30051 3.69098 3.45948 3.62513 3.62523 3.62513C4.87521 3.62513 6.43769 2.87514 7.52517 1.92516C7.65758 1.81203 7.82601 1.74988 8.00016 1.74988C8.17431 1.74988 8.34275 1.81203 8.47515 1.92516C9.56889 2.88139 11.1251 3.62513 12.3751 3.62513C12.5408 3.62513 12.6998 3.69098 12.817 3.80818C12.9342 3.92539 13.0001 4.08436 13.0001 4.25012V8.62505Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 8.00002L7.33333 9.33335L10 6.66669" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

crates/icons/src/icons.rs 🔗

@@ -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,

crates/onboarding/src/ai_setup_page.rs 🔗

@@ -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(

crates/onboarding/src/basics_page.rs 🔗

@@ -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)
             ),
         )

crates/onboarding/src/editing_page.rs 🔗

@@ -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),
                                     )

crates/onboarding/src/onboarding.rs 🔗

@@ -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()))

crates/onboarding/src/theme_preview.rs 🔗

@@ -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);
 

crates/onboarding/src/welcome.rs 🔗

@@ -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())

crates/ui/src/components/badge.rs 🔗

@@ -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())
     }
 }
 

crates/ui/src/components/button/button_like.rs 🔗

@@ -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))
                 }

crates/ui/src/components/button/toggle_button.rs 🔗

@@ -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 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()
-            })
-        });
+        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;
+
+                    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