diff --git a/compositor/src/config.rs b/compositor/src/config.rs index 9c2fa7e..1feaedf 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -5,18 +5,64 @@ use std::fs; use std::io; use std::path::PathBuf; +use ferese_animation::SpringConfig; use ferese_core::LayoutMode; use ferese_layout::ColumnWidth; use serde::Deserialize; #[derive(Debug, Default, Deserialize)] pub struct Config { + #[serde(default)] + animations: AnimationsConfig, #[serde(default)] layout: LayoutConfig, #[serde(default)] scrolling: ScrollingConfig, } +#[derive(Debug, Deserialize)] +struct AnimationsConfig { + #[serde(default = "enabled_by_default")] + enabled: bool, + #[serde(default)] + reduced_motion: bool, + #[serde(default = "default_animation_speed")] + speed: f64, + #[serde(default)] + spring: SpringSettings, +} + +impl Default for AnimationsConfig { + fn default() -> Self { + Self { + enabled: true, + reduced_motion: false, + speed: 1.0, + spring: SpringSettings::default(), + } + } +} + +#[derive(Debug, Deserialize)] +struct SpringSettings { + #[serde(default = "default_spring_mass")] + mass: f64, + #[serde(default = "default_spring_stiffness")] + stiffness: f64, + #[serde(default = "default_spring_damping")] + damping: f64, +} + +impl Default for SpringSettings { + fn default() -> Self { + Self { + mass: default_spring_mass(), + stiffness: default_spring_stiffness(), + damping: default_spring_damping(), + } + } +} + #[derive(Debug, Default, Deserialize)] struct LayoutConfig { mode: Option, @@ -57,6 +103,10 @@ pub enum ConfigError { field: &'static str, value: String, }, + InvalidAnimationValue { + field: &'static str, + value: f64, + }, } impl fmt::Display for ConfigError { @@ -72,6 +122,9 @@ impl fmt::Display for ConfigError { formatter, "invalid scrolling.{field} {value}; expected a positive number or \"full\"" ), + Self::InvalidAnimationValue { field, value } => { + write!(formatter, "invalid animations.{field} value {value}") + } } } } @@ -81,7 +134,7 @@ impl Error for ConfigError { match self { Self::Read { source, .. } => Some(source), Self::Parse { source, .. } => Some(source), - Self::InvalidColumnWidth { .. } => None, + Self::InvalidColumnWidth { .. } | Self::InvalidAnimationValue { .. } => None, } } } @@ -133,6 +186,62 @@ impl Config { .map(|value| parse_column_width(value, "width_presets")) .collect() } + + pub fn animations_enabled(&self) -> bool { + self.animations.enabled && !self.animations.reduced_motion + } + + pub fn animation_speed(&self) -> Result { + positive_animation_value(self.animations.speed, "speed") + } + + pub fn spring_config(&self) -> Result { + let mass = positive_animation_value(self.animations.spring.mass, "spring.mass")?; + let stiffness = + positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?; + let damping = self.animations.spring.damping; + if !damping.is_finite() || damping < 0.0 { + return Err(ConfigError::InvalidAnimationValue { + field: "spring.damping", + value: damping, + }); + } + + Ok(SpringConfig { + mass, + stiffness, + damping, + ..SpringConfig::default() + }) + } +} + +fn positive_animation_value(value: f64, field: &'static str) -> Result { + if value.is_finite() && value > 0.0 { + Ok(value) + } else { + Err(ConfigError::InvalidAnimationValue { field, value }) + } +} + +const fn enabled_by_default() -> bool { + true +} + +const fn default_animation_speed() -> f64 { + 1.0 +} + +const fn default_spring_mass() -> f64 { + 1.0 +} + +const fn default_spring_stiffness() -> f64 { + 700.0 +} + +const fn default_spring_damping() -> f64 { + 53.0 } fn parse_column_width( @@ -222,4 +331,32 @@ mod tests { assert!(zero.default_column_width().is_err()); assert!(unknown.default_column_width().is_err()); } + + #[test] + fn parses_animation_policy_and_reduced_motion() { + let config = parse( + "[animations]\nspeed = 1.5\nreduced_motion = true\n\n[animations.spring]\nmass = 2.0\nstiffness = 500.0\ndamping = 40.0", + ); + + assert!(!config.animations_enabled()); + assert_eq!(config.animation_speed().unwrap(), 1.5); + assert_eq!( + config.spring_config().unwrap(), + SpringConfig { + mass: 2.0, + stiffness: 500.0, + damping: 40.0, + ..SpringConfig::default() + } + ); + } + + #[test] + fn rejects_invalid_animation_numbers() { + let speed = parse("[animations]\nspeed = 0.0"); + let damping = parse("[animations.spring]\ndamping = -1.0"); + + assert!(speed.animation_speed().is_err()); + assert!(damping.spring_config().is_err()); + } } diff --git a/compositor/src/main.rs b/compositor/src/main.rs index c3e7fbc..b4fab40 100644 --- a/compositor/src/main.rs +++ b/compositor/src/main.rs @@ -12,7 +12,7 @@ mod winit; use std::{error::Error, io, process::Command}; use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; -pub use state::Ferese; +pub use state::{Ferese, RuntimeConfig}; use tracing::{info, warn}; use crate::backends::LaunchConfig; @@ -26,13 +26,15 @@ fn main() -> Result<(), Box> { let config = Config::load()?; let mut event_loop = EventLoop::try_new()?; let display = Display::new()?; - let mut state = Ferese::new( - &mut event_loop, - display, - config.layout_mode(), - config.default_column_width()?, - config.width_presets()?, - )?; + let runtime = RuntimeConfig { + layout_mode: config.layout_mode(), + default_column_width: config.default_column_width()?, + column_width_presets: config.width_presets()?, + animations_enabled: config.animations_enabled(), + animation_speed: config.animation_speed()?, + spring_config: config.spring_config()?, + }; + let mut state = Ferese::new(&mut event_loop, display, runtime)?; backends::init(launch.backend, &mut event_loop, &mut state)?; // SAFETY: the backend has already read the host display and Ferese is diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 67553e8..a1abbee 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -85,6 +85,9 @@ pub struct Ferese { pub window_geometry: HashMap, pub focused_window: Option, column_width_presets: Vec, + animations_enabled: bool, + animation_speed: f64, + spring_config: SpringConfig, pub cursor_status: CursorImageStatus, pub(crate) cursor_theme: xcursor::CursorTheme, pub(crate) named_cursors: HashMap, @@ -125,6 +128,15 @@ pub struct Ferese { pub xdg_shell_state: XdgShellState, } +pub struct RuntimeConfig { + pub layout_mode: LayoutMode, + pub default_column_width: ColumnWidth, + pub column_width_presets: Vec, + pub animations_enabled: bool, + pub animation_speed: f64, + pub spring_config: SpringConfig, +} + impl Ferese { pub fn register_output(&mut self, output: &Output, identity: String) { let Some(geometry) = self.space.output_geometry(output) else { @@ -272,9 +284,7 @@ impl Ferese { pub fn new( event_loop: &mut EventLoop<'static, Self>, display: Display, - default_layout_mode: LayoutMode, - default_column_width: ColumnWidth, - column_width_presets: Vec, + config: RuntimeConfig, ) -> Result> { let display_handle = display.handle(); crate::handlers::screencopy::init_global(&display_handle); @@ -323,14 +333,17 @@ impl Ferese { display_handle, loop_signal: event_loop.get_signal(), space: Space::default(), - workspaces: WorkspaceSet::new(default_layout_mode, default_column_width), + workspaces: WorkspaceSet::new(config.layout_mode, config.default_column_width), output_workspaces: OutputWorkspaceMap::default(), output_ids: HashMap::new(), output_identity_ids: HashMap::new(), window_ids: HashMap::new(), window_geometry: HashMap::new(), focused_window: None, - column_width_presets, + column_width_presets: config.column_width_presets, + animations_enabled: config.animations_enabled, + animation_speed: config.animation_speed, + spring_config: config.spring_config, cursor_status: CursorImageStatus::default_named(), cursor_theme, named_cursors, @@ -733,6 +746,9 @@ impl Ferese { .entry(id) .or_insert_with(|| WindowGeometry::new(rect, committed_size)); let requested_size = geometry.set_logical_target(rect, now); + if !self.animations_enabled { + geometry.advance(Duration::ZERO, self.spring_config, false); + } let visual = geometry.visual.current; let location = (visual.x.round() as i32, visual.y.round() as i32); @@ -805,6 +821,7 @@ impl Ferese { } fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool { + let delta = delta.mul_f64(self.animation_speed); let windows = self .space .elements() @@ -822,7 +839,8 @@ impl Ferese { continue; }; - active_animation |= geometry.advance(delta, SpringConfig::default(), true); + active_animation |= + geometry.advance(delta, self.spring_config, self.animations_enabled); if let Some(size) = geometry.client.expire_wait(self.start_time.elapsed()) { tracing::warn!( ?id,