diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 74be6344f9..3d2d9cd9db 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -3522,7 +3522,7 @@ impl Serialize for Length { /// # Returns /// /// A `DefiniteLength` representing the relative length as a fraction of the parent's size. -pub fn relative(fraction: f32) -> DefiniteLength { +pub const fn relative(fraction: f32) -> DefiniteLength { DefiniteLength::Fraction(fraction) } diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 82688e6220..21ea74f01c 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -1,42 +1,24 @@ +use std::sync::Arc; + use client::TelemetrySettings; use fs::Fs; -use gpui::{App, Entity, IntoElement, Window}; +use gpui::{App, IntoElement, Window}; use settings::{BaseKeymap, Settings, update_settings_file}; -use theme::{Appearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, ThemeSettings}; +use theme::{ + Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, + ThemeSettings, +}; use ui::{ ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px, }; use vim_mode_setting::VimModeSetting; -use crate::theme_preview::ThemePreviewTile; +use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; -/// separates theme "mode" ("dark" | "light" | "system") into two separate states -/// - appearance = "dark" | "light" -/// - "system" true/false -/// when system selected: -/// - toggling between light and dark does not change theme.mode, just which variant will be changed -/// when system not selected: -/// - toggling between light and dark does change theme.mode -/// selecting a theme preview will always change theme.["light" | "dark"] to the selected theme, -/// -/// this allows for selecting a dark and light theme option regardless of whether the mode is set to system or not -/// it does not support setting theme to a static value -fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement { let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone(); let system_appearance = theme::SystemAppearance::global(cx); - let appearance_state = window.use_state(cx, |_, _cx| { - theme_selection - .as_ref() - .and_then(|selection| selection.mode()) - .and_then(|mode| match mode { - ThemeMode::System => None, - ThemeMode::Light => Some(Appearance::Light), - ThemeMode::Dark => Some(Appearance::Dark), - }) - .unwrap_or(*system_appearance) - }); - let appearance = *appearance_state.read(cx); let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic { mode: match *system_appearance { Appearance::Light => ThemeMode::Light, @@ -45,70 +27,13 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { light: ThemeName("One Light".into()), dark: ThemeName("One Dark".into()), }); - let theme_registry = ThemeRegistry::global(cx); - let current_theme_name = theme_selection.theme(appearance); - 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, - Appearance::Dark => 1, - }; - - let theme_seed = 0xBEEF as f32; - - const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; - const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; - - let theme_names = match appearance { - Appearance::Light => LIGHT_THEMES, - Appearance::Dark => DARK_THEMES, - }; - let themes = theme_names - .map(|theme_name| theme_registry.get(theme_name)) - .map(Result::unwrap); - - let theme_previews = themes.map(|theme| { - 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| { - let fs = ::global(cx); - let theme_name = theme_name.clone(); - update_settings_file::(fs, cx, move |settings, _| { - settings.set_theme(theme_name, appearance); - }); - } - }) - }); + let theme_mode = theme_selection + .mode() + .unwrap_or_else(|| match *system_appearance { + Appearance::Light => ThemeMode::Light, + Appearance::Dark => ThemeMode::Dark, + }); return v_flex() .gap_2() @@ -116,93 +41,148 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { h_flex().justify_between().child(Label::new("Theme")).child( ToggleButtonGroup::single_row( "theme-selector-onboarding-dark-light", - [ - ToggleButtonSimple::new("Light", { - let appearance_state = appearance_state.clone(); + [ThemeMode::Light, ThemeMode::Dark, ThemeMode::System].map(|mode| { + const MODE_NAMES: [SharedString; 3] = [ + SharedString::new_static("Light"), + SharedString::new_static("Dark"), + SharedString::new_static("System"), + ]; + ToggleButtonSimple::new( + MODE_NAMES[mode as usize].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), - ], + write_mode_change(mode, cx); + }, + ) + }), ) - .selected_index(selected_index) + .selected_index(theme_mode as usize) .style(ui::ToggleButtonGroupStyle::Outlined) .button_width(rems_from_px(64.)), ), ) - .child(h_flex().gap_4().justify_between().children(theme_previews)); + .child( + h_flex() + .gap_4() + .justify_between() + .children(render_theme_previews(&theme_selection, cx)), + ); - fn write_appearance_change( - appearance_state: &Entity, - new_appearance: Appearance, + fn render_theme_previews( + theme_selection: &ThemeSelection, cx: &mut App, - ) { - let fs = ::global(cx); - appearance_state.write(cx, new_appearance); + ) -> [impl IntoElement; 3] { + let system_appearance = SystemAppearance::global(cx); + let theme_registry = ThemeRegistry::global(cx); - update_settings_file::(fs, cx, move |settings, _| { - if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) { - return; - } - let new_mode = match new_appearance { + let theme_seed = 0xBEEF as f32; + let theme_mode = theme_selection + .mode() + .unwrap_or_else(|| match *system_appearance { Appearance::Light => ThemeMode::Light, Appearance::Dark => ThemeMode::Dark, - }; - settings.set_mode(new_mode); + }); + let appearance = match theme_mode { + ThemeMode::Light => Appearance::Light, + ThemeMode::Dark => Appearance::Dark, + ThemeMode::System => *system_appearance, + }; + let current_theme_name = theme_selection.theme(appearance); + + const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; + const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; + const FAMILY_NAMES: [SharedString; 3] = [ + SharedString::new_static("One"), + SharedString::new_static("Ayu"), + SharedString::new_static("Gruvbox"), + ]; + + let theme_names = match appearance { + Appearance::Light => LIGHT_THEMES, + Appearance::Dark => DARK_THEMES, + }; + + let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap()); + + let theme_previews = [0, 1, 2].map(|index| { + let theme = &themes[index]; + 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( + h_flex() + .relative() + .w_full() + .border_2() + .border_color(colors.border_transparent) + .rounded(ThemePreviewTile::ROOT_RADIUS) + .map(|this| { + if is_selected { + this.border_color(colors.border_selected) + } else { + this.opacity(0.8).hover(|s| s.border_color(colors.border)) + } + }) + .map(|this| { + if theme_mode == ThemeMode::System { + let (light, dark) = ( + theme_registry.get(LIGHT_THEMES[index]).unwrap(), + theme_registry.get(DARK_THEMES[index]).unwrap(), + ); + this.child( + ThemePreviewTile::new(light, theme_seed) + .style(ThemePreviewStyle::SideBySide(dark)), + ) + } else { + this.child( + ThemePreviewTile::new(theme.clone(), theme_seed) + .style(ThemePreviewStyle::Bordered), + ) + } + }), + ) + .child( + Label::new(FAMILY_NAMES[index].clone()) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .on_click({ + let theme_name = theme.name.clone(); + move |_, _, cx| { + write_theme_change(theme_name.clone(), theme_mode, cx); + } + }) + }); + + theme_previews + } + + fn write_mode_change(mode: ThemeMode, cx: &mut App) { + let fs = ::global(cx); + update_settings_file::(fs, cx, move |settings, _cx| { + settings.set_mode(mode); }); } - fn toggle_system_theme_mode( - theme_selection: ThemeSelection, - appearance: Appearance, - cx: &mut App, - ) { + fn write_theme_change(theme: impl Into>, theme_mode: ThemeMode, cx: &mut App) { let fs = ::global(cx); - - update_settings_file::(fs, cx, move |settings, _| { - settings.theme = Some(match theme_selection { - ThemeSelection::Static(theme_name) => ThemeSelection::Dynamic { + let theme = theme.into(); + update_settings_file::(fs, cx, move |settings, cx| { + if theme_mode == ThemeMode::System { + settings.theme = Some(ThemeSelection::Dynamic { mode: ThemeMode::System, - light: theme_name.clone(), - dark: theme_name.clone(), - }, - ThemeSelection::Dynamic { - mode: ThemeMode::System, - light, - dark, - } => { - let mode = match appearance { - Appearance::Light => ThemeMode::Light, - Appearance::Dark => ThemeMode::Dark, - }; - ThemeSelection::Dynamic { mode, light, dark } - } - ThemeSelection::Dynamic { - mode: _, - light, - dark, - } => ThemeSelection::Dynamic { - mode: ThemeMode::System, - light, - dark, - }, - }); + light: ThemeName(theme.clone()), + dark: ThemeName(theme.clone()), + }); + } else { + let appearance = *SystemAppearance::global(cx); + settings.set_theme(theme.clone(), appearance); + } }); } } diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index d51511b7f4..53631be1c9 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -1,175 +1,300 @@ #![allow(unused, dead_code)] use gpui::{Hsla, Length}; use std::sync::Arc; -use theme::{Theme, ThemeRegistry}; +use theme::{Theme, ThemeColors, ThemeRegistry}; use ui::{ IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius, }; +#[derive(Clone, PartialEq)] +pub enum ThemePreviewStyle { + Bordered, + Borderless, + SideBySide(Arc), +} + /// Shows a preview of a theme as an abstract illustration /// of a thumbnail-sized editor. #[derive(IntoElement, RegisterComponent, Documented)] pub struct ThemePreviewTile { theme: Arc, seed: f32, + style: ThemePreviewStyle, } impl ThemePreviewTile { - pub const CORNER_RADIUS: Pixels = px(8.0); + pub const SKELETON_HEIGHT_DEFAULT: Pixels = px(2.); + pub const SIDEBAR_SKELETON_ITEM_COUNT: usize = 8; + pub const SIDEBAR_WIDTH_DEFAULT: DefiniteLength = relative(0.25); + pub const ROOT_RADIUS: Pixels = px(8.0); + pub const ROOT_BORDER: Pixels = px(2.0); + pub const ROOT_PADDING: Pixels = px(2.0); + pub const CHILD_BORDER: Pixels = px(1.0); + pub const CHILD_RADIUS: std::cell::LazyCell = std::cell::LazyCell::new(|| { + inner_corner_radius( + Self::ROOT_RADIUS, + Self::ROOT_BORDER, + Self::ROOT_PADDING, + Self::CHILD_BORDER, + ) + }); pub fn new(theme: Arc, seed: f32) -> Self { - Self { theme, seed } + Self { + theme, + seed, + style: ThemePreviewStyle::Bordered, + } + } + + pub fn style(mut self, style: ThemePreviewStyle) -> Self { + self.style = style; + self + } + + pub fn item_skeleton(w: Length, h: Length, bg: Hsla) -> impl IntoElement { + div().w(w).h(h).rounded_full().bg(bg) + } + + pub fn render_sidebar_skeleton_items( + seed: f32, + colors: &ThemeColors, + skeleton_height: impl Into + Clone, + ) -> [impl IntoElement; Self::SIDEBAR_SKELETON_ITEM_COUNT] { + let skeleton_height = skeleton_height.into(); + std::array::from_fn(|index| { + let width = { + let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5; + 0.5 + value * 0.45 + }; + Self::item_skeleton( + relative(width).into(), + skeleton_height, + colors.text.alpha(0.45), + ) + }) + } + + pub fn render_pseudo_code_skeleton( + seed: f32, + theme: Arc, + skeleton_height: impl Into, + ) -> impl IntoElement { + 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 skeleton_height = skeleton_height.into(); + + 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); + Self::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_1p5().children(lines) + } + + pub fn render_sidebar( + seed: f32, + colors: &ThemeColors, + width: impl Into + Clone, + skeleton_height: impl Into, + ) -> impl IntoElement { + div() + .h_full() + .w(width) + .border_r(px(1.)) + .border_color(colors.border_transparent) + .bg(colors.panel_background) + .child(v_flex().p_2().size_full().gap_1().children( + Self::render_sidebar_skeleton_items(seed, colors, skeleton_height.into()), + )) + } + + pub fn render_pane( + seed: f32, + theme: Arc, + skeleton_height: impl Into, + ) -> impl IntoElement { + v_flex().h_full().flex_grow().child( + div() + .size_full() + .overflow_hidden() + .bg(theme.colors().editor_background) + .p_2() + .child(Self::render_pseudo_code_skeleton( + seed, + theme, + skeleton_height.into(), + )), + ) + } + + pub fn render_editor( + seed: f32, + theme: Arc, + sidebar_width: impl Into + Clone, + skeleton_height: impl Into + Clone, + ) -> impl IntoElement { + div() + .size_full() + .flex() + .bg(theme.colors().background.alpha(1.00)) + .child(Self::render_sidebar( + seed, + theme.colors(), + sidebar_width, + skeleton_height.clone(), + )) + .child(Self::render_pane(seed, theme, skeleton_height.clone())) + } + + fn render_borderless(seed: f32, theme: Arc) -> impl IntoElement { + return Self::render_editor( + seed, + theme, + Self::SIDEBAR_WIDTH_DEFAULT, + Self::SKELETON_HEIGHT_DEFAULT, + ); + } + + fn render_border(seed: f32, theme: Arc) -> impl IntoElement { + div() + .size_full() + .p(Self::ROOT_PADDING) + .rounded(Self::ROOT_RADIUS) + .child( + div() + .size_full() + .rounded(*Self::CHILD_RADIUS) + .border(Self::CHILD_BORDER) + .border_color(theme.colors().border) + .child(Self::render_editor( + seed, + theme.clone(), + Self::SIDEBAR_WIDTH_DEFAULT, + Self::SKELETON_HEIGHT_DEFAULT, + )), + ) + } + + fn render_side_by_side( + seed: f32, + theme: Arc, + other_theme: Arc, + border_color: Hsla, + ) -> impl IntoElement { + let sidebar_width = relative(0.20); + + return div() + .size_full() + .p(Self::ROOT_PADDING) + .rounded(Self::ROOT_RADIUS) + .child( + h_flex() + .size_full() + .relative() + .rounded(*Self::CHILD_RADIUS) + .border(Self::CHILD_BORDER) + .border_color(border_color) + .overflow_hidden() + .child(div().size_full().child(Self::render_editor( + seed, + theme.clone(), + sidebar_width, + Self::SKELETON_HEIGHT_DEFAULT, + ))) + .child( + div() + .size_full() + .absolute() + .left_1_2() + .bg(other_theme.colors().editor_background) + .child(Self::render_editor( + seed, + other_theme, + sidebar_width, + Self::SKELETON_HEIGHT_DEFAULT, + )), + ), + ) + .into_any_element(); } } 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 = Self::CORNER_RADIUS; - 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(2.); - - 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( - v_flex() - .p_2() - .size_full() - .gap_1() - .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_1p5() - .children(lines) - .into_any_element() - }; - - 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); - - div() - .size_full() - .rounded(root_radius) - .p(root_padding) - .child( - div() - .size_full() - .rounded(inner_radius) - .border(child_border) - .border_color(color.border) - .bg(color.background) - .child(content), + match self.style { + ThemePreviewStyle::Bordered => { + Self::render_border(self.seed, self.theme).into_any_element() + } + ThemePreviewStyle::Borderless => { + Self::render_borderless(self.seed, self.theme).into_any_element() + } + ThemePreviewStyle::SideBySide(other_theme) => Self::render_side_by_side( + self.seed, + self.theme, + other_theme, + _cx.theme().colors().border, ) + .into_any_element(), + } } }