Respect motion settings and improve accessible theme transitions

This commit is contained in:
Samuel Onoja committed 2026-10-01 11:54:57 +01:00
1 parent 53bad8c672
commit 7a9fd5c864
13 files changed
+390 -67

No files matched your search

+68 -26
View File
@@ -44,9 +44,12 @@ impl IdleSettings {
Ok(self)
}
fn next_poll(self, elapsed: Duration, animate: bool) -> Duration {
fn next_poll(self, elapsed: Duration, fade_duration: Duration) -> Duration {
let dim_at = Duration::from_secs(self.dim_after_seconds);
if animate && self.dim_after_seconds != 0 && elapsed >= dim_at && elapsed < dim_at + Duration::from_millis(500)
if !fade_duration.is_zero()
&& self.dim_after_seconds != 0
&& elapsed >= dim_at
&& elapsed < dim_at + fade_duration
{
return Duration::from_millis(16);
}
@@ -60,16 +63,15 @@ impl IdleSettings {
.min(Duration::from_secs(1))
}
fn appearance(self, elapsed: Duration) -> (f32, bool) {
fn appearance(self, elapsed: Duration, fade_duration: Duration) -> (f32, bool) {
let sleeping = self.sleep_after_seconds != 0 && elapsed >= Duration::from_secs(self.sleep_after_seconds);
let dim = if self.dim_after_seconds == 0 {
let dim_at = Duration::from_secs(self.dim_after_seconds);
let dim = if self.dim_after_seconds == 0 || elapsed < dim_at {
0.0
} else if fade_duration.is_zero() {
0.65
} else {
elapsed
.saturating_sub(Duration::from_secs(self.dim_after_seconds))
.as_secs_f32()
.min(0.5)
* 1.3
(elapsed.saturating_sub(dim_at).as_secs_f32() / fade_duration.as_secs_f32()).min(1.0) * 0.65
};
(if sleeping { 1.0 } else { dim }, sleeping)
}
@@ -129,11 +131,11 @@ impl Ferese {
if !self.session_lock.ready_for_idle(self.space.outputs()) {
return;
}
let fade_duration = self.animation_duration(Duration::from_millis(500));
let since = self.session_lock.idle_since.get_or_insert(now);
let (mut opacity, sleeping) = self.lock_idle.appearance(now.saturating_duration_since(*since));
if !self.animations_enabled() && opacity > 0.0 && !sleeping {
opacity = 0.65;
}
let (opacity, sleeping) = self
.lock_idle
.appearance(now.saturating_duration_since(*since), fade_duration);
let changed = opacity != self.session_lock.idle_opacity || sleeping != self.session_lock.sleeping;
let was_sleeping = self.session_lock.sleeping;
self.session_lock.idle_opacity = opacity;
@@ -245,7 +247,11 @@ impl SessionLockHandler for Ferese {
.session_lock
.idle_since
.map_or(Duration::ZERO, |since| since.elapsed());
TimeoutAction::ToDuration(state.lock_idle.next_poll(elapsed, state.animations_enabled()))
TimeoutAction::ToDuration(
state
.lock_idle
.next_poll(elapsed, state.animation_duration(Duration::from_millis(500))),
)
}) {
Ok(token) => self.session_lock.idle_timer = Some(token),
Err(error) => tracing::warn!(%error, "could not schedule lock screen inactivity"),
@@ -355,24 +361,60 @@ smithay::reexports::wayland_server::delegate_dispatch!(Ferese: [ExtSessionLockSu
mod tests {
use super::*;
#[test]
fn dim_fade_respects_speed_without_changing_idle_deadlines() {
let policy = IdleSettings::default();
for (duration, half_at) in [(250, 30_125), (1000, 30_500)] {
let fade = Duration::from_millis(duration);
assert_eq!(policy.appearance(Duration::from_secs(29), fade), (0.0, false));
let halfway = policy.appearance(Duration::from_millis(half_at), fade);
assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1);
assert_eq!(
policy.next_poll(Duration::from_millis(half_at), fade),
Duration::from_millis(16)
);
assert_eq!(policy.appearance(Duration::from_secs(120), fade), (1.0, true));
}
assert_eq!(
policy.appearance(Duration::from_secs(30), Duration::ZERO),
(0.65, false)
);
assert_eq!(policy.appearance(Duration::from_secs(120), Duration::ZERO), (1.0, true));
}
#[test]
fn idle_policy_fades_then_sleeps_and_activity_resets_it() {
let policy = IdleSettings::default();
assert_eq!(policy.appearance(Duration::from_secs(29)), (0.0, false));
let halfway = policy.appearance(Duration::from_millis(30_250));
assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1);
assert_eq!(policy.appearance(Duration::from_secs(31)), (0.65, false));
assert_eq!(policy.appearance(Duration::from_secs(120)), (1.0, true));
assert_eq!(policy.appearance(Duration::ZERO), (0.0, false));
assert_eq!(
policy.next_poll(Duration::from_millis(29_998), true),
policy.appearance(Duration::from_secs(29), Duration::from_millis(500)),
(0.0, false)
);
let halfway = policy.appearance(Duration::from_millis(30_250), Duration::from_millis(500));
assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1);
assert_eq!(
policy.appearance(Duration::from_secs(31), Duration::from_millis(500)),
(0.65, false)
);
assert_eq!(
policy.appearance(Duration::from_secs(120), Duration::from_millis(500)),
(1.0, true)
);
assert_eq!(
policy.appearance(Duration::ZERO, Duration::from_millis(500)),
(0.0, false)
);
assert_eq!(
policy.next_poll(Duration::from_millis(29_998), Duration::from_millis(500)),
Duration::from_millis(2)
);
assert_eq!(
policy.next_poll(Duration::from_secs(30), true),
policy.next_poll(Duration::from_secs(30), Duration::from_millis(500)),
Duration::from_millis(16)
);
assert_eq!(policy.next_poll(Duration::from_secs(30), false), Duration::from_secs(1));
assert_eq!(
policy.next_poll(Duration::from_secs(30), Duration::ZERO),
Duration::from_secs(1)
);
}
#[test]
@@ -427,7 +469,7 @@ mod tests {
dim_after_seconds: 0,
sleep_after_seconds: 0
}
.appearance(Duration::from_secs(9999)),
.appearance(Duration::from_secs(9999), Duration::from_millis(500)),
(0.0, false)
);
assert_eq!(
@@ -435,7 +477,7 @@ mod tests {
dim_after_seconds: 0,
sleep_after_seconds: 10
}
.appearance(Duration::from_secs(10)),
.appearance(Duration::from_secs(10), Duration::from_millis(500)),
(1.0, true)
);
assert_eq!(
@@ -443,7 +485,7 @@ mod tests {
dim_after_seconds: 1,
sleep_after_seconds: 0
}
.appearance(Duration::from_secs(9999)),
.appearance(Duration::from_secs(9999), Duration::from_millis(500)),
(0.65, false)
);
assert!(
+51 -6
View File
@@ -269,12 +269,16 @@ struct ClosingAnimation {
}
impl ClosingAnimation {
fn advance(&mut self, delta: Duration) -> bool {
fn advance(&mut self, delta: Duration, animations_enabled: bool) -> bool {
if self.close_sent {
return false;
}
self.progress = (self.progress + delta.as_secs_f64() / CLOSE_ANIMATION_DURATION.as_secs_f64()).min(1.0);
self.progress = if animations_enabled {
(self.progress + delta.as_secs_f64() / CLOSE_ANIMATION_DURATION.as_secs_f64()).min(1.0)
} else {
1.0
};
if self.progress < 1.0 {
return false;
}
@@ -284,6 +288,14 @@ impl ClosingAnimation {
}
}
fn scaled_animation_duration(duration: Duration, enabled: bool, speed: f64) -> Duration {
if enabled {
duration.div_f64(speed)
} else {
Duration::ZERO
}
}
pub struct Ferese {
pub(crate) session_lock_state: SessionLockManagerState,
pub(crate) session_lock: Lock,
@@ -2125,7 +2137,7 @@ impl Ferese {
let mut ready_to_close = Vec::new();
for (id, animation) in &mut self.closing_windows {
if animation.advance(delta) {
if animation.advance(delta, self.animations_enabled) {
ready_to_close.push(*id);
} else if !animation.close_sent {
active_animation = true;
@@ -2321,6 +2333,10 @@ impl Ferese {
self.animations_enabled
}
pub(crate) fn animation_duration(&self, duration: Duration) -> Duration {
scaled_animation_duration(duration, self.animations_enabled, self.animation_speed)
}
pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) {
let Some((window, id)) = self
.window_ids
@@ -3775,6 +3791,35 @@ impl ClientData for ClientState {
#[cfg(test)]
mod tests {
#[test]
fn configured_motion_policy_scales_durations_and_reduced_motion_wins() {
for (source, milliseconds) in [
("animations { speed 2; }", 100),
("animations { speed 0.5; }", 400),
("animations { speed 2; reduced-motion #true; }", 0),
("animations { speed 0.5; enabled #false; }", 0),
] {
let config = crate::config::Config::parse_source(source).unwrap();
assert_eq!(
scaled_animation_duration(
Duration::from_millis(200),
config.animations_enabled(),
config.animation_speed().unwrap()
),
Duration::from_millis(milliseconds),
);
}
}
#[test]
fn disabling_motion_finishes_an_in_progress_close_once() {
let mut animation = ClosingAnimation::default();
assert!(!animation.advance(Duration::from_millis(35), true));
assert!(animation.advance(Duration::ZERO, false));
assert_eq!(animation.progress, 1.0);
assert!(!animation.advance(Duration::ZERO, false));
}
#[test]
fn workspace_drag_cancels_when_focus_or_target_ownership_changes() {
let mut outputs = OutputWorkspaceMap::default();
@@ -4101,11 +4146,11 @@ mod tests {
fn close_animation_delays_the_protocol_close_until_it_finishes() {
let mut animation = ClosingAnimation::default();
assert!(!animation.advance(Duration::from_millis(70)));
assert!(!animation.advance(Duration::from_millis(70), true));
assert_eq!(animation.progress, 0.5);
assert!(animation.advance(Duration::from_millis(70)));
assert!(animation.advance(Duration::from_millis(70), true));
assert!(animation.close_sent);
assert!(!animation.advance(Duration::from_secs(1)));
assert!(!animation.advance(Duration::from_secs(1), true));
}
#[test]
+38 -4
View File
@@ -24,6 +24,14 @@ use crate::{Ferese, RuntimeConfig};
const DEBOUNCE: Duration = Duration::from_millis(150);
const WALLPAPER_TIMEOUT: Duration = Duration::from_secs(2);
fn transition_progress(elapsed: Duration, duration: Duration) -> f64 {
if duration.is_zero() {
1.0
} else {
(elapsed.as_secs_f64() / duration.as_secs_f64()).clamp(0.0, 1.0)
}
}
pub(crate) fn read_source(path: &Path) -> Result<String, String> {
let mut source = String::new();
std::fs::File::open(path)
@@ -474,10 +482,18 @@ impl Ferese {
.as_ref()
.filter(|transition| transition.previous_wallpaper.is_some())
.map_or(1., |transition| {
(transition.started.elapsed().as_secs_f32() / (TRANSITION_MS as f32 / 1000.)).clamp(0., 1.)
transition_progress(transition.started.elapsed(), self.theme_transition_duration()) as f32
})
}
fn theme_transition_duration(&self) -> Duration {
if self.theme_engine.snapshot.theme.reduced_motion {
Duration::ZERO
} else {
self.animation_duration(Duration::from_millis(TRANSITION_MS))
}
}
pub(crate) fn poll_theme(&mut self) {
let now = Instant::now();
if self
@@ -535,12 +551,12 @@ impl Ferese {
.theme_engine
.last_frame
.is_none_or(|last| now.saturating_duration_since(last) >= Duration::from_millis(16));
let duration = self.theme_transition_duration();
if frame_due && let Some(transition) = &self.theme_engine.transition {
self.theme_engine.last_frame = Some(now);
let progress =
now.saturating_duration_since(transition.started).as_secs_f64() / (TRANSITION_MS as f64 / 1000.);
let progress = transition_progress(now.saturating_duration_since(transition.started), duration);
let target = &self.theme_engine.snapshot.theme;
let complete = progress >= 1. || target.reduced_motion;
let complete = progress >= 1.;
let presented = if complete {
target.clone()
} else {
@@ -617,3 +633,21 @@ impl Ferese {
Ok(self.theme_engine.value())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn theme_and_wallpaper_share_scaled_progress_and_instant_completion() {
let elapsed = Duration::from_millis(50);
assert_eq!(transition_progress(elapsed, Duration::from_millis(200)), 0.25);
assert_eq!(transition_progress(elapsed, Duration::from_millis(100)), 0.5);
assert_eq!(transition_progress(elapsed, Duration::from_millis(400)), 0.125);
assert_eq!(transition_progress(Duration::ZERO, Duration::ZERO), 1.0);
assert_eq!(
transition_progress(Duration::from_secs(1), Duration::from_millis(200)),
1.0
);
}
}