wip basics section

Nate Butler created

Change summary

Cargo.lock                                |   4 
crates/onboarding_ui/Cargo.toml           |   4 
crates/onboarding_ui/src/onboarding_ui.rs | 416 +++++++++++++++++++++---
crates/onboarding_ui/src/theme_preview.rs | 200 ++++++++++++
4 files changed, 570 insertions(+), 54 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10828,13 +10828,15 @@ dependencies = [
  "gpui",
  "menu",
  "project",
+ "serde_json",
  "settings",
  "settings_ui",
  "theme",
  "ui",
  "util",
+ "vim_mode_setting",
+ "welcome",
  "workspace",
- "workspace-hack",
  "zed_actions",
 ]
 

crates/onboarding_ui/Cargo.toml 🔗

@@ -16,6 +16,7 @@ test-support = []
 
 [dependencies]
 anyhow.workspace = true
+welcome.workspace = true
 client.workspace = true
 command_palette_hooks.workspace = true
 component.workspace = true
@@ -24,12 +25,13 @@ feature_flags.workspace = true
 gpui.workspace = true
 menu.workspace = true
 project.workspace = true
+serde_json.workspace = true
 settings.workspace = true
 settings_ui.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
+vim_mode_setting.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 

crates/onboarding_ui/src/onboarding_ui.rs 🔗

@@ -1,20 +1,27 @@
 #![allow(unused, dead_code)]
 mod persistence;
+mod theme_preview;
 
-use client::Client;
+use client::{Client, TelemetrySettings};
 use command_palette_hooks::CommandPaletteFilter;
 use feature_flags::FeatureFlagAppExt as _;
 use gpui::{
-    Entity, EventEmitter, FocusHandle, Focusable, KeyBinding, Task, WeakEntity, actions, prelude::*,
+    Entity, EventEmitter, FocusHandle, Focusable, KeyBinding, Task, UpdateGlobal, WeakEntity,
+    actions, prelude::*, svg,
 };
 use menu;
 use persistence::ONBOARDING_DB;
 
 use project::Project;
+use serde_json;
+use settings::{Settings, SettingsStore};
 use settings_ui::SettingsUiFeatureFlag;
 use std::sync::Arc;
-use ui::{ListItem, Vector, VectorName, prelude::*};
+use theme::{Theme, ThemeRegistry, ThemeSettings};
+use ui::{ListItem, ToggleState, Vector, VectorName, prelude::*};
 use util::ResultExt;
+use vim_mode_setting::VimModeSetting;
+use welcome::BaseKeymap;
 use workspace::{
     Workspace, WorkspaceId,
     item::{Item, ItemEvent, SerializableItem},
@@ -133,6 +140,8 @@ pub struct OnboardingUI {
     client: Arc<Client>,
 }
 
+impl OnboardingUI {}
+
 impl EventEmitter<ItemEvent> for OnboardingUI {}
 
 impl Focusable for OnboardingUI {
@@ -286,7 +295,7 @@ impl OnboardingUI {
                 };
                 // Bounds checking for page items
                 let max_items = match self.current_page {
-                    OnboardingPage::Basics => 3,  // 3 buttons
+                    OnboardingPage::Basics => 14, // 4 themes + 7 keymaps + 3 checkboxes
                     OnboardingPage::Editing => 3, // 3 buttons
                     OnboardingPage::AiSetup => 2, // Will have 2 items
                     OnboardingPage::Welcome => 1, // Will have 1 item
@@ -329,7 +338,7 @@ impl OnboardingUI {
                 };
                 // Bounds checking for page items
                 let max_items = match self.current_page {
-                    OnboardingPage::Basics => 3,  // 3 buttons
+                    OnboardingPage::Basics => 14, // 4 themes + 7 keymaps + 3 checkboxes
                     OnboardingPage::Editing => 3, // 3 buttons
                     OnboardingPage::AiSetup => 2, // Will have 2 items
                     OnboardingPage::Welcome => 1, // Will have 1 item
@@ -390,16 +399,16 @@ impl OnboardingUI {
                 match self.current_page {
                     OnboardingPage::Basics => {
                         match item_index {
-                            0 => {
-                                // Open file action
+                            0..=3 => {
+                                // Theme selection
                                 cx.notify();
                             }
-                            1 => {
-                                // Create project action
+                            4..=10 => {
+                                // Keymap selection
                                 cx.notify();
                             }
-                            2 => {
-                                // Explore UI action
+                            11..=13 => {
+                                // Checkbox toggles (handled by their own listeners)
                                 cx.notify();
                             }
                             _ => {}
@@ -688,59 +697,362 @@ impl OnboardingUI {
         let focused_item = self.page_focus[page_index].0;
         let is_page_focused = self.focus_area == FocusArea::PageContent;
 
+        use theme_preview::ThemePreviewTile;
+
+        // Get available themes
+        let theme_registry = ThemeRegistry::default_global(cx);
+        let theme_names = theme_registry.list_names();
+        let current_theme = cx.theme().clone();
+
+        // For demo, we'll show 4 themes
+
         v_flex()
+            .id("theme-selector")
             .h_full()
             .w_full()
-            .items_center()
-            .justify_center()
-            .gap_4()
-            .child(
-                Label::new("Welcome to Zed!")
-                    .size(LabelSize::Large)
-                    .color(Color::Default),
-            )
+            .p_6()
+            .gap_6()
+            .overflow_y_scroll()
+            // Theme selector section
             .child(
-                Label::new("Let's get you started with the basics")
-                    .size(LabelSize::Default)
-                    .color(Color::Muted),
+                v_flex()
+                    .gap_3()
+                    .child(
+                        h_flex()
+                            .justify_between()
+                            .child(Label::new("Pick a Theme").size(LabelSize::Large))
+                            .child(
+                                Button::new("more_themes", "More Themes")
+                                    .style(ButtonStyle::Subtle)
+                                    .color(Color::Muted)
+                                    .on_click(cx.listener(|_, _, window, cx| {
+                                        // TODO: Open theme selector
+                                        cx.notify();
+                                    })),
+                            ),
+                    )
+                    .child(
+                        h_flex()
+                            .gap_3()
+                            .children(
+                                vec![
+                                    ("One Dark", "one-dark"),
+                                    ("Gruvbox Dark", "gruvbox-dark"),
+                                    ("One Light", "one-light"),
+                                    ("Gruvbox Light", "gruvbox-light"),
+                                ]
+                                .into_iter()
+                                .enumerate()
+                                .map(|(i, (label, theme_name))| {
+                                    let is_selected = current_theme.name == *theme_name;
+                                    let is_focused = is_page_focused && focused_item == i;
+
+                                    v_flex()
+                                        .gap_2()
+                                        .child(
+                                            div()
+                                                .id("theme-item")
+                                                .when(is_focused, |this| {
+                                                    this.border_2().border_color(
+                                                        cx.theme().colors().border_focused,
+                                                    )
+                                                })
+                                                .rounded_md()
+                                                .p_1()
+                                                .id(("theme", i))
+                                                .child(
+                                                    if let Ok(theme) =
+                                                        theme_registry.get(theme_name)
+                                                    {
+                                                        ThemePreviewTile::new(
+                                                            theme,
+                                                            is_selected,
+                                                            0.5,
+                                                        )
+                                                        .into_any_element()
+                                                    } else {
+                                                        div()
+                                                            .w(px(200.))
+                                                            .h(px(120.))
+                                                            .bg(cx
+                                                                .theme()
+                                                                .colors()
+                                                                .surface_background)
+                                                            .rounded_md()
+                                                            .into_any_element()
+                                                    },
+                                                )
+                                                .on_click(cx.listener(
+                                                    move |this, _, window, cx| {
+                                                        SettingsStore::update_global(cx, move |store, cx| {
+                                                            let mut settings = store.raw_user_settings().clone();
+                                                            settings["theme"] = serde_json::json!(theme_name);
+                                                            store.set_user_settings(&settings.to_string(), cx).ok();
+                                                        });
+                                                        cx.notify();
+                                                    },
+                                                )),
+                                        )
+                                        .child(Label::new(label).size(LabelSize::Small).color(
+                                            if is_selected {
+                                                Color::Default
+                                            } else {
+                                                Color::Muted
+                                            },
+                                        ))
+                                },
+                            )),
+                    ),
             )
+            // Keymap selector section
             .child(
                 v_flex()
-                    .gap_2()
+                    .gap_3()
                     .mt_4()
+                    .child(Label::new("Pick a Keymap").size(LabelSize::Large))
                     .child(
-                        Button::new("open_file", "Open a File")
-                            .style(ButtonStyle::Filled)
-                            .when(is_page_focused && focused_item == 0, |this| {
-                                this.color(Color::Accent)
+                        h_flex().gap_2().children(
+                            vec![
+                                ("Zed", VectorName::ZedLogo, 4),
+                                ("Atom", VectorName::ZedLogo, 5),
+                                ("JetBrains", VectorName::ZedLogo, 6),
+                                ("Sublime", VectorName::ZedLogo, 7),
+                                ("VSCode", VectorName::ZedLogo, 8),
+                                ("Emacs", VectorName::ZedLogo, 9),
+                                ("TextMate", VectorName::ZedLogo, 10),
+                            ]
+                            .into_iter()
+                            .map(|(label, icon, index)| {
+                                let is_focused = is_page_focused && focused_item == index;
+                                let current_keymap = BaseKeymap::get_global(cx).to_string();
+                                let is_selected = current_keymap == label;
+
+                                v_flex()
+                                    .gap_1()
+                                    .items_center()
+                                    .child(
+                                        div()
+                                            .id(("keymap", index))
+                                            .p_3()
+                                            .rounded_md()
+                                            .bg(cx.theme().colors().element_background)
+                                            .border_1()
+                                            .border_color(if is_selected {
+                                                cx.theme().colors().border_selected
+                                            } else {
+                                                cx.theme().colors().border
+                                            })
+                                            .when(is_focused, |this| {
+                                                this.border_color(
+                                                    cx.theme().colors().border_focused,
+                                                )
+                                            })
+                                            .when(is_selected, |this| {
+                                                this.bg(cx.theme().colors().element_selected)
+                                            })
+                                            .hover(|this| {
+                                                this.bg(cx.theme().colors().element_hover)
+                                            })
+                                            .child(
+                                                Vector::new(icon, rems(2.), rems(2.))
+                                                    .color(Color::Muted),
+                                            )
+                                            .on_click(cx.listener(move |this, _, window, cx| {
+                                                SettingsStore::update_global(cx, move |store, cx| {
+                                                    let base_keymap = match label {
+                                                        "Zed" => "None",
+                                                        "Atom" => "Atom",
+                                                        "JetBrains" => "JetBrains",
+                                                        "Sublime" => "SublimeText",
+                                                        "VSCode" => "VSCode",
+                                                        "Emacs" => "Emacs",
+                                                        "TextMate" => "TextMate",
+                                                        _ => "VSCode",
+                                                    };
+                                                    let mut settings = store.raw_user_settings().clone();
+                                                    settings["base_keymap"] = serde_json::json!(base_keymap);
+                                                    store.set_user_settings(&settings.to_string(), cx).ok();
+                                                });
+                                                cx.notify();
+                                            })),
+                                    )
+                                    .child(
+                                        Label::new(label)
+                                            .size(LabelSize::Small)
+                                            .color(if is_selected {
+                                                Color::Default
+                                            } else {
+                                                Color::Muted
+                                            }),
+                                    )
                             })
-                            .on_click(cx.listener(|_, _, _, cx| {
-                                // TODO: Trigger open file action
-                                cx.notify();
-                            })),
-                    )
-                    .child(
-                        Button::new("create_project", "Create a Project")
-                            .style(ButtonStyle::Filled)
-                            .when(is_page_focused && focused_item == 1, |this| {
-                                this.color(Color::Accent)
+                        ),
+                    ),
+            )
+            // Settings checkboxes
+            .child(
+                v_flex()
+                    .gap_3()
+                    .mt_6()
+                    .child({
+                        let vim_enabled = VimModeSetting::get_global(cx).0;
+                        h_flex()
+                            .id("vim_mode_container")
+                            .gap_2()
+                            .items_center()
+                            .p_1()
+                            .rounded_md()
+                            .when(is_page_focused && focused_item == 11, |this| {
+                                this.border_2()
+                                    .border_color(cx.theme().colors().border_focused)
                             })
-                            .on_click(cx.listener(|_, _, _, cx| {
-                                // TODO: Trigger create project action
-                                cx.notify();
-                            })),
-                    )
-                    .child(
-                        Button::new("explore_ui", "Explore the UI")
-                            .style(ButtonStyle::Filled)
-                            .when(is_page_focused && focused_item == 2, |this| {
-                                this.color(Color::Accent)
+                            .child(
+                                div()
+                                    .id("vim_mode_checkbox")
+                                    .w_4()
+                                    .h_4()
+                                    .rounded_sm()
+                                    .border_1()
+                                    .border_color(cx.theme().colors().border)
+                                    .when(vim_enabled, |this| {
+                                        this.bg(cx.theme().colors().element_selected)
+                                            .border_color(cx.theme().colors().border_selected)
+                                    })
+                                    .hover(|this| this.bg(cx.theme().colors().element_hover))
+                                    .child(
+                                        div().when(vim_enabled, |this| {
+                                            this.size_full()
+                                                .flex()
+                                                .items_center()
+                                                .justify_center()
+                                                .child(
+                                                    svg()
+                                                        .path("M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z")
+                                                        .size_3()
+                                                        .text_color(cx.theme().colors().icon),
+                                                )
+                                        })
+                                    ),
+                            )
+                            .child(Label::new("Enable Vim Mode"))
+                            .cursor_pointer()
+                            .on_click(cx.listener(move |this, _, _window, cx| {
+                                let current = VimModeSetting::get_global(cx).0;
+                                SettingsStore::update_global(cx, move |store, cx| {
+                                    let mut settings = store.raw_user_settings().clone();
+                                    settings["vim_mode"] = serde_json::json!(!current);
+                                    store.set_user_settings(&settings.to_string(), cx).ok();
+                                });
+                            }))
+                    })
+                    .child({
+                        let crash_reports_enabled = TelemetrySettings::get_global(cx).diagnostics;
+                        h_flex()
+                            .id("crash_reports_container")
+                            .gap_2()
+                            .items_center()
+                            .p_1()
+                            .rounded_md()
+                            .when(is_page_focused && focused_item == 12, |this| {
+                                this.border_2()
+                                    .border_color(cx.theme().colors().border_focused)
                             })
-                            .on_click(cx.listener(|_, _, _, cx| {
-                                // TODO: Trigger explore UI action
-                                cx.notify();
-                            })),
-                    ),
+                            .child(
+                                div()
+                                    .id("crash_reports_checkbox")
+                                    .w_4()
+                                    .h_4()
+                                    .rounded_sm()
+                                    .border_1()
+                                    .border_color(cx.theme().colors().border)
+                                    .when(crash_reports_enabled, |this| {
+                                        this.bg(cx.theme().colors().element_selected)
+                                            .border_color(cx.theme().colors().border_selected)
+                                    })
+                                    .hover(|this| this.bg(cx.theme().colors().element_hover))
+                                    .child(
+                                        div().when(crash_reports_enabled, |this| {
+                                            this.size_full()
+                                                .flex()
+                                                .items_center()
+                                                .justify_center()
+                                                .child(
+                                                    svg()
+                                                        .path("M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z")
+                                                        .size_3()
+                                                        .text_color(cx.theme().colors().icon),
+                                                )
+                                        })
+                                    ),
+                            )
+                            .child(Label::new("Send Crash Reports"))
+                            .cursor_pointer()
+                            .on_click(cx.listener(move |this, _, _window, cx| {
+                                let current = TelemetrySettings::get_global(cx).diagnostics;
+                                SettingsStore::update_global(cx, move |store, cx| {
+                                    let mut settings = store.raw_user_settings().clone();
+                                    if settings.get("telemetry").is_none() {
+                                        settings["telemetry"] = serde_json::json!({});
+                                    }
+                                    settings["telemetry"]["diagnostics"] = serde_json::json!(!current);
+                                    store.set_user_settings(&settings.to_string(), cx).ok();
+                                });
+                            }))
+                    })
+                    .child({
+                        let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
+                        h_flex()
+                            .id("telemetry_container")
+                            .gap_2()
+                            .items_center()
+                            .p_1()
+                            .rounded_md()
+                            .when(is_page_focused && focused_item == 13, |this| {
+                                this.border_2()
+                                    .border_color(cx.theme().colors().border_focused)
+                            })
+                            .child(
+                                div()
+                                    .id("telemetry_checkbox")
+                                    .w_4()
+                                    .h_4()
+                                    .rounded_sm()
+                                    .border_1()
+                                    .border_color(cx.theme().colors().border)
+                                    .when(telemetry_enabled, |this| {
+                                        this.bg(cx.theme().colors().element_selected)
+                                            .border_color(cx.theme().colors().border_selected)
+                                    })
+                                    .hover(|this| this.bg(cx.theme().colors().element_hover))
+                                    .child(
+                                        div().when(telemetry_enabled, |this| {
+                                            this.size_full()
+                                                .flex()
+                                                .items_center()
+                                                .justify_center()
+                                                .child(
+                                                    svg()
+                                                        .path("M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z")
+                                                        .size_3()
+                                                        .text_color(cx.theme().colors().icon),
+                                                )
+                                        })
+                                    ),
+                            )
+                            .child(Label::new("Send Telemetry"))
+                            .cursor_pointer()
+                            .on_click(cx.listener(move |this, _, _window, cx| {
+                                let current = TelemetrySettings::get_global(cx).metrics;
+                                SettingsStore::update_global(cx, move |store, cx| {
+                                    let mut settings = store.raw_user_settings().clone();
+                                    if settings.get("telemetry").is_none() {
+                                        settings["telemetry"] = serde_json::json!({});
+                                    }
+                                    settings["telemetry"]["metrics"] = serde_json::json!(!current);
+                                    store.set_user_settings(&settings.to_string(), cx).ok();
+                                });
+                            }))
+                    }),
             )
             .into_any_element()
     }

crates/onboarding_ui/src/theme_preview.rs 🔗

@@ -0,0 +1,200 @@
+#![allow(unused, dead_code)]
+use gpui::{Hsla, Length};
+use std::sync::Arc;
+use theme::{Theme, ThemeRegistry};
+use ui::{IntoElement, RenderOnce, prelude::*, utils::inner_corner_radius};
+
+/// Shows a preview of a theme as an abstract illustration
+/// of a thumbnail-sized editor.
+#[derive(IntoElement)]
+pub struct ThemePreviewTile {
+    theme: Arc<Theme>,
+    selected: bool,
+    seed: f32,
+}
+
+impl ThemePreviewTile {
+    pub fn new(theme: Arc<Theme>, selected: bool, seed: f32) -> Self {
+        Self {
+            theme,
+            selected,
+            seed,
+        }
+    }
+
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+}
+
+impl RenderOnce for ThemePreviewTile {
+    fn render(self, _window: &mut ui::Window, _cx: &mut ui::App) -> impl IntoElement {
+        let color = self.theme.colors();
+
+        let root_radius = px(8.0);
+        let root_border = px(2.0);
+        let root_padding = px(2.0);
+        let child_border = px(1.0);
+        let inner_radius =
+            inner_corner_radius(root_radius, root_border, root_padding, child_border);
+
+        let item_skeleton = |w: Length, h: Pixels, bg: Hsla| div().w(w).h(h).rounded_full().bg(bg);
+
+        let skeleton_height = px(4.);
+
+        let sidebar_seeded_width = |seed: f32, index: usize| {
+            let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5;
+            0.5 + value * 0.45
+        };
+
+        let sidebar_skeleton_items = 8;
+
+        let sidebar_skeleton = (0..sidebar_skeleton_items)
+            .map(|i| {
+                let width = sidebar_seeded_width(self.seed, i);
+                item_skeleton(
+                    relative(width).into(),
+                    skeleton_height,
+                    color.text.alpha(0.45),
+                )
+            })
+            .collect::<Vec<_>>();
+
+        let sidebar = div()
+            .h_full()
+            .w(relative(0.25))
+            .border_r(px(1.))
+            .border_color(color.border_transparent)
+            .bg(color.panel_background)
+            .child(
+                div()
+                    .p_2()
+                    .flex()
+                    .flex_col()
+                    .size_full()
+                    .gap(px(4.))
+                    .children(sidebar_skeleton),
+            );
+
+        let pseudo_code_skeleton = |theme: Arc<Theme>, seed: f32| -> AnyElement {
+            let colors = theme.colors();
+            let syntax = theme.syntax();
+
+            let keyword_color = syntax.get("keyword").color;
+            let function_color = syntax.get("function").color;
+            let string_color = syntax.get("string").color;
+            let comment_color = syntax.get("comment").color;
+            let variable_color = syntax.get("variable").color;
+            let type_color = syntax.get("type").color;
+            let punctuation_color = syntax.get("punctuation").color;
+
+            let syntax_colors = [
+                keyword_color,
+                function_color,
+                string_color,
+                variable_color,
+                type_color,
+                punctuation_color,
+                comment_color,
+            ];
+
+            let line_width = |line_idx: usize, block_idx: usize| -> f32 {
+                let val = (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin()
+                    * 0.5
+                    + 0.5;
+                0.05 + val * 0.2
+            };
+
+            let indentation = |line_idx: usize| -> f32 {
+                let step = line_idx % 6;
+                if step < 3 {
+                    step as f32 * 0.1
+                } else {
+                    (5 - step) as f32 * 0.1
+                }
+            };
+
+            let pick_color = |line_idx: usize, block_idx: usize| -> Hsla {
+                let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin()
+                    * 3.5)
+                    .abs() as usize
+                    % syntax_colors.len();
+                syntax_colors[idx].unwrap_or(colors.text)
+            };
+
+            let line_count = 13;
+
+            let lines = (0..line_count)
+                .map(|line_idx| {
+                    let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5)
+                        * 3.0)
+                        .round() as usize
+                        + 2;
+
+                    let indent = indentation(line_idx);
+
+                    let blocks = (0..block_count)
+                        .map(|block_idx| {
+                            let width = line_width(line_idx, block_idx);
+                            let color = pick_color(line_idx, block_idx);
+                            item_skeleton(relative(width).into(), skeleton_height, color)
+                        })
+                        .collect::<Vec<_>>();
+
+                    h_flex().gap(px(2.)).ml(relative(indent)).children(blocks)
+                })
+                .collect::<Vec<_>>();
+
+            v_flex()
+                .size_full()
+                .p_1()
+                .gap(px(6.))
+                .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 content = div().size_full().flex().child(sidebar).child(pane);
+
+        div()
+            .size_full()
+            .rounded(root_radius)
+            .p(root_padding)
+            .border(root_border)
+            .border_color(color.border_transparent)
+            .when(self.selected, |this| {
+                this.border_color(color.border_selected)
+            })
+            .child(
+                div()
+                    .size_full()
+                    .rounded(inner_radius)
+                    .border(child_border)
+                    .border_color(color.border)
+                    .bg(color.background)
+                    .child(content),
+            )
+    }
+}