diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index f8d74015..4c0d137b 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -318,6 +318,34 @@ impl AppSettings { self.apply_theme(Some(window), cx); } + /// Set the accent color override and re-resolve the theme + pub fn apply_accent( + &mut self, + selection: AccentSelection, + window: &mut Window, + cx: &mut Context, + ) { + self.inner.update(cx, |this, cx| { + this.accent_selection = selection; + cx.notify(); + }); + self.apply_theme(Some(window), cx); + } + + /// Set the surface treatment preference + pub fn apply_surface( + &mut self, + preference: SurfacePreference, + window: &mut Window, + cx: &mut Context, + ) { + self.inner.update(cx, |this, cx| { + this.surface_preference = preference; + cx.notify(); + }); + self.apply_theme(Some(window), cx); + } + /// Apply theme pub fn apply_theme(&mut self, window: Option<&mut Window>, cx: &mut Context) { let (selection, accent, surface, appearance) = { diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index c5106186..f2f62b4b 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -36,6 +36,10 @@ pub const TABBAR_HEIGHT: Pixels = px(36.0); pub const SIDEBAR_WIDTH: Pixels = px(240.); pub fn init(cx: &mut App) { + for issue in ThemeRegistry::active().validate() { + log::warn!("theme {}: {}", issue.variant_id, issue.message); + } + Theme::sync_system_appearance(None, cx); Theme::sync_scrollbar_appearance(cx); } diff --git a/crates/workspace/src/dialogs/settings.rs b/crates/workspace/src/dialogs/settings.rs index 08e814dc..414458fe 100644 --- a/crates/workspace/src/dialogs/settings.rs +++ b/crates/workspace/src/dialogs/settings.rs @@ -4,7 +4,7 @@ use gpui::{ Window, div, px, }; use settings::AppSettings; -use theme::{ActiveTheme, Appearance, Theme}; +use theme::{AccentPreset, AccentSelection, ActiveTheme, Appearance, SurfacePreference, Theme}; use ui::button::{Button, ButtonVariants}; use ui::group_box::{GroupBox, GroupBoxVariants}; use ui::input::{Input, InputState}; @@ -59,6 +59,8 @@ impl Render for Preferences { const SCREENING: &str = "Show an screening dialog to verify the unknown sender."; const AVATAR: &str = "Hide all avatar pictures to improve performance."; const MODE: &str = "Use the selected light or dark theme, or to follow the OS."; + const ACCENT: &str = "Override the accent color of the selected theme."; + const SURFACE: &str = "Preferred surface treatment. Frosted rendering is not applied yet."; const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages."; const RESET: &str = "Reset the theme to the default one."; @@ -66,6 +68,8 @@ impl Render for Preferences { let hide_avatar = AppSettings::get_hide_avatar(cx); let nip4e = AppSettings::get_nip4e(cx); let appearance = AppSettings::get_appearance(cx); + let accent_selection = AppSettings::get_accent_selection(cx); + let surface_preference = AppSettings::get_surface_preference(cx); v_flex() .gap_4() @@ -135,6 +139,127 @@ impl Render for Preferences { }), ), ) + .child( + h_flex() + .gap_3() + .justify_between() + .child( + v_flex() + .child(div().text_sm().child(SharedString::from("Accent"))) + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from(ACCENT)), + ), + ) + .child( + Button::new("accent") + .label(accent_selection.label()) + .ghost_alt() + .small() + .dropdown_menu(|menu, _window, _cx| { + let menu = + menu.item( + PopupMenuItem::new( + AccentSelection::ThemeDefault.label(), + ) + .on_click(|_, window, cx| { + AppSettings::global(cx).update(cx, |s, cx| { + s.apply_accent( + AccentSelection::ThemeDefault, + window, + cx, + ); + }); + }), + ); + + let mut menu = menu; + for preset in AccentPreset::ALL { + menu = menu.item( + PopupMenuItem::new(preset.label()).on_click({ + move |_, window, cx| { + AppSettings::global(cx).update( + cx, + |s, cx| { + s.apply_accent( + AccentSelection::Preset(preset), + window, + cx, + ); + }, + ); + } + }), + ); + } + menu + }), + ), + ) + .child( + h_flex() + .gap_3() + .justify_between() + .child( + v_flex() + .child(div().text_sm().child(SharedString::from("Surface"))) + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from(SURFACE)), + ), + ) + .child( + Button::new("surface") + .label(match surface_preference { + SurfacePreference::ThemeDefault => "Theme default", + SurfacePreference::Frosted => "Frosted", + SurfacePreference::Opaque => "Opaque", + }) + .ghost_alt() + .small() + .dropdown_menu(|menu, _window, _cx| { + menu.item(PopupMenuItem::new("Theme default").on_click( + |_, window, cx| { + AppSettings::global(cx).update(cx, |s, cx| { + s.apply_surface( + SurfacePreference::ThemeDefault, + window, + cx, + ); + }); + }, + )) + .item(PopupMenuItem::new("Frosted").on_click( + |_, window, cx| { + AppSettings::global(cx).update(cx, |s, cx| { + s.apply_surface( + SurfacePreference::Frosted, + window, + cx, + ); + }); + }, + )) + .item( + PopupMenuItem::new("Opaque").on_click( + |_, window, cx| { + AppSettings::global(cx).update(cx, |s, cx| { + s.apply_surface( + SurfacePreference::Opaque, + window, + cx, + ); + }); + }, + ), + ) + }), + ), + ) .child( h_flex() .gap_3() diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 9826369c..1b426b40 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -465,9 +465,23 @@ impl Workspace { fn theme_selector(&mut self, window: &mut Window, cx: &mut Context) { let appearance = cx.theme().appearance; - let variants: Vec = ThemeRegistry::active() - .variants_for(appearance) - .cloned() + let registry = ThemeRegistry::active(); + let groups: Vec<(String, Vec)> = registry + .families + .iter() + .filter_map(|family| { + let variants: Vec = family + .variants + .iter() + .filter(|variant| variant.appearance == appearance) + .cloned() + .collect(); + if variants.is_empty() { + None + } else { + Some((family.name.clone(), variants)) + } + }) .collect(); window.open_dialog(cx, move |this, _window, cx| { @@ -475,75 +489,88 @@ impl Workspace { .show_close(true) .title("Select theme") .child(v_flex().gap_2().w_full().children({ - let mut items = vec![]; + let mut items: Vec = vec![]; - for (ix, variant) in variants.iter().enumerate() { + for (family_name, variants) in groups.iter() { items.push( - h_flex() - .id(ix) - .group("") - .px_2() - .h_8() - .w_full() - .justify_between() - .rounded(cx.theme().radius) - .bg(gpui::transparent_black()) - .hover(|this| this.bg(cx.theme().hover)) - .child( - h_flex() - .gap_1p5() - .flex_1() - .text_sm() - .child(SharedString::from(variant.name.clone())) - .child( - div() - .text_xs() - .italic() - .text_color(cx.theme().text_muted) - .child(SharedString::from( - variant.family_id.clone(), - )), - ), - ) - .child( - h_flex() - .gap_1() - .invisible() - .group_hover("", |this| this.visible()) - .child( - Button::new(format!("url-{ix}")) - .icon(IconName::Link) - .ghost() - .small() - .on_click({ - let url = variant.source.url.clone(); - move |_ev, _window, cx| { - cx.open_url(&url); - } - }), - ) - .child( - Button::new(format!("set-{ix}")) - .icon(IconName::Check) - .primary() - .small() - .on_click({ - let id = variant.id.clone(); - move |_ev, window, cx| { - let settings = AppSettings::global(cx); - - settings.update(cx, |this, cx| { - this.set_theme_variant( - id.clone(), - window, - cx, - ); - }) - } - }), - ), - ), + div() + .pt_2() + .text_xs() + .italic() + .text_color(cx.theme().text_faint) + .child(SharedString::from(family_name.clone())) + .into_any_element(), ); + + for (ix, variant) in variants.iter().enumerate() { + items.push( + h_flex() + .id(ix) + .group("") + .px_2() + .h_8() + .w_full() + .justify_between() + .rounded(cx.theme().radius) + .bg(gpui::transparent_black()) + .hover(|this| this.bg(cx.theme().hover)) + .child( + h_flex() + .gap_1p5() + .flex_1() + .text_sm() + .child(SharedString::from(variant.name.clone())) + .child( + div() + .text_xs() + .italic() + .text_color(cx.theme().text_faint) + .child(SharedString::from( + variant.source.license.clone(), + )), + ), + ) + .child( + h_flex() + .gap_1() + .invisible() + .group_hover("", |this| this.visible()) + .child( + Button::new(format!("url-{ix}")) + .icon(IconName::Link) + .ghost() + .small() + .on_click({ + let url = variant.source.url.clone(); + move |_ev, _window, cx| { + cx.open_url(&url); + } + }), + ) + .child( + Button::new(format!("set-{ix}")) + .icon(IconName::Check) + .primary() + .small() + .on_click({ + let id = variant.id.clone(); + move |_ev, window, cx| { + let settings = AppSettings::global(cx); + + settings.update(cx, |this, cx| { + this.set_theme_variant( + id.clone(), + window, + cx, + ); + }) + } + }), + ), + ) + .into_any_element(), + ); + } } items