Respect motion settings and improve accessible theme transitions
This commit is contained in:
1 parent
53bad8c672
commit
7a9fd5c864
13 files changed
+390
-67
No files matched your search
@@ -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
@@ -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
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user