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