add accent color and surface preferences to settings

This commit is contained in:
2026-10-08 08:56:57 +07:00
parent f4b435b160
commit 02b9cc238f
4 changed files with 254 additions and 70 deletions
+28
View File
@@ -318,6 +318,34 @@ impl AppSettings {
self.apply_theme(Some(window), cx); 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>,
) {
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>,
) {
self.inner.update(cx, |this, cx| {
this.surface_preference = preference;
cx.notify();
});
self.apply_theme(Some(window), cx);
}
/// Apply theme /// Apply theme
pub fn apply_theme(&mut self, window: Option<&mut Window>, cx: &mut Context<Self>) { pub fn apply_theme(&mut self, window: Option<&mut Window>, cx: &mut Context<Self>) {
let (selection, accent, surface, appearance) = { let (selection, accent, surface, appearance) = {
+4
View File
@@ -36,6 +36,10 @@ pub const TABBAR_HEIGHT: Pixels = px(36.0);
pub const SIDEBAR_WIDTH: Pixels = px(240.); pub const SIDEBAR_WIDTH: Pixels = px(240.);
pub fn init(cx: &mut App) { 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_system_appearance(None, cx);
Theme::sync_scrollbar_appearance(cx); Theme::sync_scrollbar_appearance(cx);
} }
+126 -1
View File
@@ -4,7 +4,7 @@ use gpui::{
Window, div, px, Window, div, px,
}; };
use settings::AppSettings; use settings::AppSettings;
use theme::{ActiveTheme, Appearance, Theme}; use theme::{AccentPreset, AccentSelection, ActiveTheme, Appearance, SurfacePreference, Theme};
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::group_box::{GroupBox, GroupBoxVariants}; use ui::group_box::{GroupBox, GroupBoxVariants};
use ui::input::{Input, InputState}; 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 SCREENING: &str = "Show an screening dialog to verify the unknown sender.";
const AVATAR: &str = "Hide all avatar pictures to improve performance."; 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 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 NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages.";
const RESET: &str = "Reset the theme to the default one."; 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 hide_avatar = AppSettings::get_hide_avatar(cx);
let nip4e = AppSettings::get_nip4e(cx); let nip4e = AppSettings::get_nip4e(cx);
let appearance = AppSettings::get_appearance(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() v_flex()
.gap_4() .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( .child(
h_flex() h_flex()
.gap_3() .gap_3()
+96 -69
View File
@@ -465,9 +465,23 @@ impl Workspace {
fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let appearance = cx.theme().appearance; let appearance = cx.theme().appearance;
let variants: Vec<ThemeVariant> = ThemeRegistry::active() let registry = ThemeRegistry::active();
.variants_for(appearance) let groups: Vec<(String, Vec<ThemeVariant>)> = registry
.cloned() .families
.iter()
.filter_map(|family| {
let variants: Vec<ThemeVariant> = family
.variants
.iter()
.filter(|variant| variant.appearance == appearance)
.cloned()
.collect();
if variants.is_empty() {
None
} else {
Some((family.name.clone(), variants))
}
})
.collect(); .collect();
window.open_dialog(cx, move |this, _window, cx| { window.open_dialog(cx, move |this, _window, cx| {
@@ -475,75 +489,88 @@ impl Workspace {
.show_close(true) .show_close(true)
.title("Select theme") .title("Select theme")
.child(v_flex().gap_2().w_full().children({ .child(v_flex().gap_2().w_full().children({
let mut items = vec![]; let mut items: Vec<gpui::AnyElement> = vec![];
for (ix, variant) in variants.iter().enumerate() { for (family_name, variants) in groups.iter() {
items.push( items.push(
h_flex() div()
.id(ix) .pt_2()
.group("") .text_xs()
.px_2() .italic()
.h_8() .text_color(cx.theme().text_faint)
.w_full() .child(SharedString::from(family_name.clone()))
.justify_between() .into_any_element(),
.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,
);
})
}
}),
),
),
); );
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 items