Preserve live dimming settings and stabilize theme transition borders

This commit is contained in:
Samuel Onoja committed 2026-10-01 13:49:36 +01:00
1 parent dec4d54bd4
commit 9e966f81a0
9 files changed
+102 -30

No files matched your search

+6 -14
View File
@@ -94,16 +94,13 @@ impl Config {
pub(crate) fn resolved_theme_settings( pub(crate) fn resolved_theme_settings(
theme: &ferese_config::theme::ResolvedTheme, theme: &ferese_config::theme::ResolvedTheme,
inactive_dim: InactiveDimSettings,
) -> Result<ThemeSettings, ConfigError> { ) -> Result<ThemeSettings, ConfigError> {
let config = Self { let config = Self {
theme: serde_json::from_value(serde_json::to_value(&theme.tokens).unwrap()) theme: serde_json::from_value(serde_json::to_value(&theme.tokens).unwrap())
.expect("resolved theme matches runtime schema"), .expect("resolved theme matches runtime schema"),
..Self::default() ..Self::default()
}; };
let mut settings = config.theme_settings()?; config.theme_settings()
settings.inactive_dim = inactive_dim;
Ok(settings)
} }
pub(crate) fn runtime_config(&self) -> Result<crate::RuntimeConfig, ConfigError> { pub(crate) fn runtime_config(&self) -> Result<crate::RuntimeConfig, ConfigError> {
@@ -126,6 +123,7 @@ impl Config {
bindings, bindings,
window_rules: self.window_rules()?, window_rules: self.window_rules()?,
theme_settings: self.theme_settings()?, theme_settings: self.theme_settings()?,
inactive_dim: self.inactive_dim_settings()?,
default_column_width: self.default_column_width()?, default_column_width: self.default_column_width()?,
scrolling_focus_strategy: self.scrolling_focus_strategy(), scrolling_focus_strategy: self.scrolling_focus_strategy(),
column_width_presets: self.width_presets()?, column_width_presets: self.width_presets()?,
@@ -622,11 +620,6 @@ mod tests {
bar_background_color: RgbaColor([17.0 / 255.0, 24.0 / 255.0, 33.0 / 255.0, 1.0,]), bar_background_color: RgbaColor([17.0 / 255.0, 24.0 / 255.0, 33.0 / 255.0, 1.0,]),
text_primary_color: RgbaColor([244.0 / 255.0, 247.0 / 255.0, 251.0 / 255.0, 1.0]), text_primary_color: RgbaColor([244.0 / 255.0, 247.0 / 255.0, 251.0 / 255.0, 1.0]),
shell_opacity: 0.78, shell_opacity: 0.78,
inactive_dim: InactiveDimSettings {
enabled: false,
amount: 0.15,
duration_ms: 150.0
},
window_radius: 12.0, window_radius: 12.0,
shadow_offset_y: -2.0, shadow_offset_y: -2.0,
shadow_blur: 24.0, shadow_blur: 24.0,
@@ -756,15 +749,14 @@ mod tests {
#[test] #[test]
fn inactive_dimming_is_opt_in_and_configurable() { fn inactive_dimming_is_opt_in_and_configurable() {
let defaults = parse("").theme_settings().unwrap().inactive_dim; let defaults = parse("").inactive_dim_settings().unwrap();
assert!(!defaults.enabled); assert!(!defaults.enabled);
assert_eq!(defaults.amount, 0.15); assert_eq!(defaults.amount, 0.15);
assert_eq!(defaults.duration_ms, 150.0); assert_eq!(defaults.duration_ms, 150.0);
let settings = let settings =
parse("appearance {\n inactive-dim {\n enabled #true\n amount 0.25\n duration-ms 100\n }\n}\n") parse("appearance {\n inactive-dim {\n enabled #true\n amount 0.25\n duration-ms 100\n }\n}\n")
.theme_settings() .inactive_dim_settings()
.unwrap() .unwrap();
.inactive_dim;
assert!(settings.enabled); assert!(settings.enabled);
assert_eq!(settings.amount, 0.25); assert_eq!(settings.amount, 0.25);
assert_eq!(settings.duration_ms, 100.0); assert_eq!(settings.duration_ms, 100.0);
@@ -777,7 +769,7 @@ mod tests {
] { ] {
assert!( assert!(
Config::parse_source(&format!("appearance {{ inactive-dim {{ {key} {value}; }} }}")) Config::parse_source(&format!("appearance {{ inactive-dim {{ {key} {value}; }} }}"))
.and_then(|config| config.theme_settings()) .and_then(|config| config.runtime_config())
.is_err() .is_err()
); );
} }
+11 -9
View File
@@ -1,6 +1,17 @@
use super::*; use super::*;
impl Config { impl Config {
pub(crate) fn inactive_dim_settings(&self) -> Result<InactiveDimSettings, ConfigError> {
Ok(InactiveDimSettings {
enabled: self.appearance.inactive_dim.enabled,
amount: unit_theme_value(self.appearance.inactive_dim.amount, "appearance.inactive_dim.amount")?,
duration_ms: nonnegative_theme_value(
self.appearance.inactive_dim.duration_ms,
"appearance.inactive_dim.duration_ms",
)?,
})
}
pub fn theme_settings(&self) -> Result<ThemeSettings, ConfigError> { pub fn theme_settings(&self) -> Result<ThemeSettings, ConfigError> {
let border_width = nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?; let border_width = nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?;
let focus_ring_width = let focus_ring_width =
@@ -39,14 +50,6 @@ impl Config {
"surface.bar.background", "surface.bar.background",
)?, )?,
shell_opacity: unit_theme_value(self.theme.material.opacity, "material.opacity")?, shell_opacity: unit_theme_value(self.theme.material.opacity, "material.opacity")?,
inactive_dim: InactiveDimSettings {
enabled: self.appearance.inactive_dim.enabled,
amount: unit_theme_value(self.appearance.inactive_dim.amount, "appearance.inactive_dim.amount")?,
duration_ms: nonnegative_theme_value(
self.appearance.inactive_dim.duration_ms,
"appearance.inactive_dim.duration_ms",
)?,
},
window_radius, window_radius,
shadow_offset_y, shadow_offset_y,
shadow_blur, shadow_blur,
@@ -210,7 +213,6 @@ pub struct ThemeSettings {
pub bar_background_color: RgbaColor, pub bar_background_color: RgbaColor,
pub text_primary_color: RgbaColor, pub text_primary_color: RgbaColor,
pub shell_opacity: f64, pub shell_opacity: f64,
pub inactive_dim: InactiveDimSettings,
pub window_radius: f64, pub window_radius: f64,
pub shadow_offset_y: f64, pub shadow_offset_y: f64,
pub shadow_blur: f64, pub shadow_blur: f64,
+56
View File
@@ -194,6 +194,62 @@ mod tests {
} }
} }
#[test]
fn live_reload_keeps_dimming_policy_independent_of_theme_transitions() {
if std::env::var_os("FERESE_DIM_RELOAD_TEST_CHILD").is_none() {
use std::os::unix::fs::PermissionsExt;
let directory = Directory::new();
std::fs::set_permissions(&directory.0, std::fs::Permissions::from_mode(0o700)).unwrap();
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"reload::tests::live_reload_keeps_dimming_policy_independent_of_theme_transitions",
"--nocapture",
])
.env("FERESE_DIM_RELOAD_TEST_CHILD", "1")
.env("XDG_RUNTIME_DIR", &directory.0)
.env("XDG_CONFIG_HOME", &directory.0)
.env_remove("FERESE_SOCKET")
.output()
.unwrap();
assert!(
output.status.success(),
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
return;
}
let source = "appearance { inactive-dim { enabled #true; amount 0.25; }; }";
let directory = Directory::new();
let (_, runtime, candidate) = crate::theme::prepare(source, &directory.0).unwrap();
let mut event_loop = smithay::reexports::calloop::EventLoop::try_new().unwrap();
let display = smithay::reexports::wayland_server::Display::new().unwrap();
let mut state = crate::Ferese::new(&mut event_loop, display, runtime).unwrap();
state.config_source = Some(source.into());
state.theme_engine.snapshot.theme = candidate.theme.clone();
state.theme_engine.snapshot.presented = candidate.theme;
for enabled in [false, true, false] {
let source = format!("appearance {{ inactive-dim {{ enabled #{enabled}; amount 0.25; }}; }}");
state.reload_config_source(source).unwrap();
assert_eq!(state.inactive_dim.enabled, enabled);
let opacity = crate::dimming::target(
state.inactive_dim,
Some(ferese_layout::WindowId(1)),
ferese_layout::WindowId(2),
false,
);
assert_eq!(opacity, if enabled { 0.25 } else { 0.0 });
}
state
.reload_config_source("appearance { inactive-dim { enabled #false; }; }; theme { mode \"light\"; }".into())
.unwrap();
state.poll_theme();
assert!(!state.inactive_dim.enabled);
}
#[test] #[test]
fn runtime_validation_rejects_bad_edits_before_application() { fn runtime_validation_rejects_bad_edits_before_application() {
for source in [ for source in [
+4
View File
@@ -372,6 +372,7 @@ pub struct Ferese {
window_rules: Vec<WindowRule>, window_rules: Vec<WindowRule>,
window_rules_applied: HashSet<WindowId>, window_rules_applied: HashSet<WindowId>,
pub(crate) theme_settings: ThemeSettings, pub(crate) theme_settings: ThemeSettings,
pub(crate) inactive_dim: crate::config::InactiveDimSettings,
animations_enabled: bool, animations_enabled: bool,
animation_speed: f64, animation_speed: f64,
spring_config: SpringConfig, spring_config: SpringConfig,
@@ -450,6 +451,7 @@ pub struct RuntimeConfig {
pub workspace_auto_back_and_forth: bool, pub workspace_auto_back_and_forth: bool,
pub window_rules: Vec<WindowRule>, pub window_rules: Vec<WindowRule>,
pub theme_settings: ThemeSettings, pub theme_settings: ThemeSettings,
pub inactive_dim: crate::config::InactiveDimSettings,
pub default_column_width: ColumnWidth, pub default_column_width: ColumnWidth,
pub scrolling_focus_strategy: ViewportFocusStrategy, pub scrolling_focus_strategy: ViewportFocusStrategy,
pub column_width_presets: Vec<ColumnWidth>, pub column_width_presets: Vec<ColumnWidth>,
@@ -602,6 +604,7 @@ impl Ferese {
window_rules: config.window_rules, window_rules: config.window_rules,
window_rules_applied: HashSet::new(), window_rules_applied: HashSet::new(),
theme_settings: config.theme_settings, theme_settings: config.theme_settings,
inactive_dim: config.inactive_dim,
animations_enabled: config.animations_enabled, animations_enabled: config.animations_enabled,
animation_speed: config.animation_speed, animation_speed: config.animation_speed,
spring_config: config.spring_config, spring_config: config.spring_config,
@@ -739,6 +742,7 @@ impl Ferese {
self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings); self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings);
let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules); let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules);
self.theme_settings = config.theme_settings; self.theme_settings = config.theme_settings;
self.inactive_dim = config.inactive_dim;
self.column_width_presets = config.column_width_presets; self.column_width_presets = config.column_width_presets;
self.animations_enabled = config.animations_enabled; self.animations_enabled = config.animations_enabled;
if !self.animations_enabled { if !self.animations_enabled {
+1 -1
View File
@@ -69,7 +69,7 @@ impl Ferese {
active active
}); });
let dim_settings = self.theme_settings.inactive_dim; let dim_settings = self.inactive_dim;
let duration = if self.animations_enabled { let duration = if self.animations_enabled {
dim_settings.duration_ms dim_settings.duration_ms
} else { } else {
+1 -2
View File
@@ -562,8 +562,7 @@ impl Ferese {
} else { } else {
transition.from.transition(target, progress) transition.from.transition(target, progress)
}; };
self.theme_settings = Config::resolved_theme_settings(&presented, self.theme_settings.inactive_dim) self.theme_settings = Config::resolved_theme_settings(&presented).expect("validated resolved theme");
.expect("validated resolved theme");
self.theme_engine.snapshot.presented = presented; self.theme_engine.snapshot.presented = presented;
self.theme_engine.publish(); self.theme_engine.publish();
self.backdrop_generation = self.backdrop_generation.wrapping_add(1); self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
+3 -3
View File
@@ -300,7 +300,7 @@ pub(super) fn workspace_selector_style(
background: if active { background: if active {
Some(Background::Color(color_with_opacity(shell_theme.accent, 0.16))) Some(Background::Color(color_with_opacity(shell_theme.accent, 0.16)))
} else if occupied { } else if occupied {
Some(Background::Color(color_with_opacity(shell_theme.text_primary, 0.04))) Some(Background::Color(color_with_opacity(shell_theme.border, 0.3)))
} else { } else {
None None
}, },
@@ -326,9 +326,9 @@ pub(super) fn bar_style(theme: ShellTheme, compositor_material: bool) -> contain
pub(super) fn bar_group_style(theme: ShellTheme) -> container::Style { pub(super) fn bar_group_style(theme: ShellTheme) -> container::Style {
container::Style { container::Style {
background: Some(Background::Color(color_with_opacity(theme.text_primary, 0.035))), background: Some(Background::Color(color_with_opacity(theme.border, 0.25))),
border: Border { border: Border {
color: color_with_opacity(theme.text_muted, 0.12), color: color(theme.border),
width: 1.0, width: 1.0,
radius: theme.material_radius.min(16.0).into(), radius: theme.material_radius.min(16.0).into(),
}, },
+19
View File
@@ -1069,6 +1069,25 @@ mod tests {
assert_eq!(configured_font(None), cosmic::font::default()); assert_eq!(configured_font(None), cosmic::font::default());
} }
#[test]
fn bar_decoration_does_not_follow_transitional_text_contrast() {
let theme = config::ShellTheme::default();
let contrasted = config::ShellTheme {
text_primary: [0, 0, 0, 255],
text_muted: [255, 255, 255, 255],
..theme
};
let before = bar_group_style(theme);
let after = bar_group_style(contrasted);
assert_eq!(before.border, after.border);
assert_eq!(before.background, after.background);
assert_eq!(before.border.color, color(theme.border));
assert_eq!(
workspace_selector_style(false, false, true, theme).background,
workspace_selector_style(false, false, true, contrasted).background,
);
}
#[test] #[test]
fn workspace_selector_has_distinct_active_paint() { fn workspace_selector_has_distinct_active_paint() {
let theme = ShellTheme::default(); let theme = ShellTheme::default();
+1 -1
View File
@@ -218,7 +218,7 @@ impl FereseShell {
text_color: Some(primary), text_color: Some(primary),
icon_color: Some(primary), icon_color: Some(primary),
border: Border { border: Border {
color: color_with_opacity(theme.text_muted, 0.18 * p), color: color_with_opacity(theme.border, p),
width: 1.0, width: 1.0,
radius: theme.material_radius.into(), radius: theme.material_radius.into(),
}, },