From 8c446ecd5957aad1151c9ab8d2811b472d2ed547 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 30 Jun 2025 21:44:17 -0400 Subject: [PATCH] wip basics section --- 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(-) create mode 100644 crates/onboarding_ui/src/theme_preview.rs diff --git a/Cargo.lock b/Cargo.lock index eab5741d25c0ac25120f24821a15188aaafbc649..011f970283f8f7bf19a98d4c47e571f47382c3a9 100644 --- a/Cargo.lock +++ b/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", ] diff --git a/crates/onboarding_ui/Cargo.toml b/crates/onboarding_ui/Cargo.toml index 24766430338fbf28b415cbff8a47326547908c4f..61ee08fc2e53956be1245017a4a48ab10c9906b4 100644 --- a/crates/onboarding_ui/Cargo.toml +++ b/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 diff --git a/crates/onboarding_ui/src/onboarding_ui.rs b/crates/onboarding_ui/src/onboarding_ui.rs index accd01eb7326afe5be004013212763e866d25c0c..72d8d7f776157cc7b861bdfa5355a5b0b9bc343b 100644 --- a/crates/onboarding_ui/src/onboarding_ui.rs +++ b/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, } +impl OnboardingUI {} + impl EventEmitter 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() } diff --git a/crates/onboarding_ui/src/theme_preview.rs b/crates/onboarding_ui/src/theme_preview.rs new file mode 100644 index 0000000000000000000000000000000000000000..32780e6c62731e0e6f67f79ae825f1fee697a60f --- /dev/null +++ b/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, + selected: bool, + seed: f32, +} + +impl ThemePreviewTile { + pub fn new(theme: Arc, 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::>(); + + 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, 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::>(); + + h_flex().gap(px(2.)).ml(relative(indent)).children(blocks) + }) + .collect::>(); + + 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), + ) + } +}