Derive theme gradients and add solid paint controls

This commit is contained in:
Samuel Onoja committed 2026-10-04 09:03:28 +01:00
1 parent 20395007a5
commit 831ac3a77f
18 files changed
+604 -20

No files matched your search

+1
View File
@@ -126,6 +126,7 @@ impl Appearance {
text: self.text,
muted: self.text.scale_alpha(0.55),
accent: self.accent,
accent_gradient: None,
on_accent: self.on_accent,
radius: self.radius,
error: Color::from_rgb8(235, 98, 98),
+1
View File
@@ -59,6 +59,7 @@ impl Appearance {
text: self.text,
muted: self.muted,
accent: self.accent,
accent_gradient: None,
on_accent: self.on_accent,
radius: self.radius,
error: Color::from_rgb8(235, 98, 98),
+8
View File
@@ -954,6 +954,14 @@ impl App {
fn change(&mut self, edit: Edit) -> Task<Message> {
match self.draft.edit(&edit) {
Ok(()) => {
// A shared style selection applies to both appearances while
// retaining their authored gradient endpoints.
for unset in visuals::paint_style_overrides(&self.draft, &edit) {
if self.draft.edit(&unset).is_ok() {
self.pending.push(unset);
}
}
// When a gradient exists, changing Accent also changes its
// leading stop; otherwise the visible focus border would stay
// on the old palette despite the control saying it changed.
+14
View File
@@ -742,6 +742,20 @@ pub fn fields(page: Page) -> Vec<Field> {
"Focus rings and selected controls. Use a hex color.",
"#3D7BE6",
),
choice(
"theme.focus_ring.style",
"Accent style",
"Window focus borders and selected controls. Gradients use the theme colors unless overridden.",
"auto",
&[("auto", "Gradient"), ("solid", "Solid")],
),
choice(
"theme.border.style",
"Inactive border style",
"Gradients retain the border color's transparency.",
"auto",
&[("auto", "Gradient"), ("solid", "Solid")],
),
choice(
"theme.material.style",
"Surface style",
+53
View File
@@ -137,6 +137,21 @@ pub fn toggle_split(snapshot: &Snapshot, enabled: bool, active: ferese_config::t
edits
}
pub fn paint_style_overrides(snapshot: &Snapshot, edit: &Edit) -> Vec<Edit> {
let Edit::Set(path, _) = edit else { return vec![] };
let section = match path.as_str() {
"theme.focus_ring.style" => "focus_ring",
"theme.border.style" => "border",
_ => return vec![],
};
["light", "dark"]
.into_iter()
.map(|kind| format!("theme.{kind}.{section}.style"))
.filter(|path| snapshot.item(path).is_some())
.map(Edit::Unset)
.collect()
}
#[cfg(test)]
pub fn preset(index: usize) -> Vec<Edit> {
let Some(preset) = PRESETS.get(index) else {
@@ -244,6 +259,44 @@ mod tests {
snapshot
}
#[test]
fn shared_style_controls_replace_scoped_styles_without_deleting_endpoints() {
let mut snapshot = Snapshot::parse(
r##"theme {
family catppuccin
light { focus-ring { style solid; gradient { from "#123456"; to "#654321"; }; }; }
dark { focus-ring { style solid; gradient { from "#ABCDEF"; to "#FEDCBA"; }; }; }
}"##
.into(),
)
.unwrap();
for (style, enabled) in [("auto", true), ("solid", false), ("auto", true)] {
let edit = set("theme.focus_ring.style", style);
for unset in paint_style_overrides(&snapshot, &edit) {
snapshot.edit(&unset).unwrap();
}
snapshot.edit(&edit).unwrap();
for (mode, from) in [("light", "#123456"), ("dark", "#ABCDEF")] {
snapshot.edit(&set("theme.mode", mode)).unwrap();
let theme = ferese_config::theme::resolve(
&snapshot.doc,
std::path::Path::new("/config"),
"2026-09-30T12:00:00Z".parse().unwrap(),
|_| unreachable!(),
)
.unwrap()
.theme;
assert_eq!(theme.tokens.focus_ring.gradient.is_some(), enabled);
if enabled {
assert_eq!(theme.tokens.focus_ring.gradient.unwrap().from, from);
}
assert!(snapshot.item(&format!("theme.{mode}.focus_ring.gradient")).is_some());
}
}
}
fn contrast(a: Color, b: Color) -> f32 {
ferese_theme::contrast(a, b)
}
+5
View File
@@ -51,6 +51,7 @@ pub(crate) struct ShellTheme {
pub(crate) text_primary: [u8; 4],
pub(crate) text_muted: [u8; 4],
pub(crate) accent: [u8; 4],
pub(crate) accent_gradient: Option<cosmic::iced::gradient::Linear>,
pub(crate) on_accent: [u8; 4],
pub(crate) border: [u8; 4],
pub(crate) shadow: [u8; 4],
@@ -80,6 +81,7 @@ impl Default for ShellTheme {
text_primary: [244, 247, 251, 255],
text_muted: [135, 147, 162, 255],
accent: [61, 123, 230, 255],
accent_gradient: None,
on_accent: [244, 247, 251, 255],
border: [255, 255, 255, 24],
shadow: [0, 0, 0, 85],
@@ -111,6 +113,7 @@ impl ShellTheme {
text: color(self.text_primary),
muted: color(self.text_muted),
accent: color(self.accent),
accent_gradient: self.accent_gradient,
on_accent: color(self.on_accent),
radius: self.material_radius,
error: cosmic::iced::Color::from_rgb8(235, 98, 98),
@@ -401,6 +404,7 @@ impl ShellConfig {
self.theme = shell_theme(&config);
self.theme.appearance = theme.appearance;
self.theme.high_contrast = theme.accessibility.increase_contrast;
self.theme.accent_gradient = ferese_theme::Palette::from_resolved(theme).accent_gradient;
self.theme.material_radius = theme.tokens.geometry.shell_radius as f32;
self.theme.bar_radius = self.theme.material_radius;
self.font_family = Some(theme.tokens.typography.font_family.clone());
@@ -447,6 +451,7 @@ fn shell_theme(theme: &ThemeConfig) -> ShellTheme {
text_primary: parse_color(&theme.colors.text_primary).unwrap_or(defaults.text_primary),
text_muted: parse_color(&theme.colors.text_muted).unwrap_or(defaults.text_muted),
accent: parse_color(&theme.colors.accent).unwrap_or(defaults.accent),
accent_gradient: defaults.accent_gradient,
on_accent: parse_color(&theme.colors.on_accent).unwrap_or(defaults.on_accent),
border: parse_color(&theme.colors.border).unwrap_or(defaults.border),
shadow: parse_color(&theme.colors.shadow).unwrap_or(defaults.shadow),