diff --git a/niri-visual-tests/src/cases/gradient_angle.rs b/niri-visual-tests/src/cases/gradient_angle.rs index 64c953c4..39a2e4ef 100644 --- a/niri-visual-tests/src/cases/gradient_angle.rs +++ b/niri-visual-tests/src/cases/gradient_angle.rs @@ -1,8 +1,6 @@ use std::f32::consts::{FRAC_PI_2, PI}; -use std::sync::atomic::Ordering; use std::time::Duration; -use niri::animation::ANIMATION_SLOWDOWN; use niri::render_helpers::border::BorderRenderElement; use niri_config::{Color, CornerRadius, GradientInterpolation}; use smithay::backend::renderer::element::RenderElement; @@ -31,20 +29,13 @@ impl TestCase for GradientAngle { } fn advance_animations(&mut self, current_time: Duration) { - let mut delta = if self.prev_time.is_zero() { + let delta = if self.prev_time.is_zero() { Duration::ZERO } else { current_time.saturating_sub(self.prev_time) }; self.prev_time = current_time; - let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst); - if slowdown == 0. { - delta = Duration::ZERO - } else { - delta = delta.div_f64(slowdown); - } - self.angle += delta.as_secs_f32() * PI; if self.angle >= PI * 2. { diff --git a/niri-visual-tests/src/cases/gradient_area.rs b/niri-visual-tests/src/cases/gradient_area.rs index 2fa275f2..6f0cf4f3 100644 --- a/niri-visual-tests/src/cases/gradient_area.rs +++ b/niri-visual-tests/src/cases/gradient_area.rs @@ -1,8 +1,6 @@ use std::f32::consts::{FRAC_PI_4, PI}; -use std::sync::atomic::Ordering; use std::time::Duration; -use niri::animation::ANIMATION_SLOWDOWN; use niri::layout::focus_ring::FocusRing; use niri::render_helpers::border::BorderRenderElement; use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation}; @@ -43,20 +41,13 @@ impl TestCase for GradientArea { } fn advance_animations(&mut self, current_time: Duration) { - let mut delta = if self.prev_time.is_zero() { + let delta = if self.prev_time.is_zero() { Duration::ZERO } else { current_time.saturating_sub(self.prev_time) }; self.prev_time = current_time; - let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst); - if slowdown == 0. { - delta = Duration::ZERO - } else { - delta = delta.div_f64(slowdown); - } - self.progress += delta.as_secs_f32() * PI; if self.progress >= PI * 2. { diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs index f979debb..0dbe004d 100644 --- a/niri-visual-tests/src/cases/layout.rs +++ b/niri-visual-tests/src/cases/layout.rs @@ -69,7 +69,7 @@ impl Layout { let mut layout = niri::layout::Layout::with_options(clock.clone(), options); layout.add_output(output.clone()); - let start_time = clock.now(); + let start_time = clock.now_unadjusted(); Self { output, @@ -207,24 +207,25 @@ impl TestCase for Layout { self.layout.are_animations_ongoing(Some(&self.output)) || !self.steps.is_empty() } - fn advance_animations(&mut self, current_time: Duration) { + fn advance_animations(&mut self, _current_time: Duration) { + let now_unadjusted = self.clock.now_unadjusted(); let run = self .steps .keys() .copied() - .filter(|delay| self.start_time + *delay <= current_time) + .filter(|delay| self.start_time + *delay <= now_unadjusted) .collect::>(); for delay in &run { let now = self.start_time + *delay; - self.clock.set_time_override(Some(now)); - self.layout.advance_animations(now); + self.clock.set_unadjusted(now); + self.layout.advance_animations(); let f = self.steps.remove(delay).unwrap(); f(self); } - self.clock.set_time_override(None); - self.layout.advance_animations(current_time); + self.clock.set_unadjusted(now_unadjusted); + self.layout.advance_animations(); } fn render( diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs index e2637698..f50edf37 100644 --- a/niri-visual-tests/src/cases/tile.rs +++ b/niri-visual-tests/src/cases/tile.rs @@ -90,8 +90,8 @@ impl TestCase for Tile { self.tile.are_animations_ongoing() } - fn advance_animations(&mut self, current_time: Duration) { - self.tile.advance_animations(current_time); + fn advance_animations(&mut self, _current_time: Duration) { + self.tile.advance_animations(); } fn render( diff --git a/niri-visual-tests/src/main.rs b/niri-visual-tests/src/main.rs index fa035c75..59563651 100644 --- a/niri-visual-tests/src/main.rs +++ b/niri-visual-tests/src/main.rs @@ -2,15 +2,11 @@ extern crate tracing; use std::env; -use std::sync::atomic::Ordering; use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt}; use cases::Args; -use gtk::prelude::{ - AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt, -}; +use gtk::prelude::{ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt}; use gtk::{gdk, gio, glib}; -use niri::animation::ANIMATION_SLOWDOWN; use smithay_view::SmithayView; use tracing_subscriber::EnvFilter; @@ -66,20 +62,23 @@ fn on_startup(_app: &adw::Application) { fn build_ui(app: &adw::Application) { let stack = gtk::Stack::new(); + let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.); struct S { stack: gtk::Stack, + anim_adjustment: gtk::Adjustment, } impl S { fn add(&self, make: impl Fn(Args) -> T + 'static, title: &str) { - let view = SmithayView::new(make); + let view = SmithayView::new(make, &self.anim_adjustment); self.stack.add_titled(&view, None, title); } } let s = S { stack: stack.clone(), + anim_adjustment: anim_adjustment.clone(), }; s.add(Window::freeform, "Freeform Window"); @@ -133,9 +132,6 @@ fn build_ui(app: &adw::Application) { let content_headerbar = adw::HeaderBar::new(); - let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.); - anim_adjustment - .connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst)); let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment)); anim_scale.set_hexpand(true); diff --git a/niri-visual-tests/src/smithay_view.rs b/niri-visual-tests/src/smithay_view.rs index 68c5d67d..416729f0 100644 --- a/niri-visual-tests/src/smithay_view.rs +++ b/niri-visual-tests/src/smithay_view.rs @@ -1,4 +1,5 @@ use gtk::glib; +use gtk::prelude::*; use gtk::subclass::prelude::*; use smithay::utils::Size; @@ -7,13 +8,13 @@ use crate::cases::{Args, TestCase}; mod imp { use std::cell::{Cell, OnceCell, RefCell}; use std::ptr::null; + use std::time::Duration; use anyhow::{ensure, Context}; use gtk::gdk; use gtk::prelude::*; use niri::animation::Clock; use niri::render_helpers::{resources, shaders}; - use niri::utils::get_monotonic_time; use smithay::backend::egl::ffi::egl; use smithay::backend::egl::EGLContext; use smithay::backend::renderer::gles::GlesRenderer; @@ -31,7 +32,7 @@ mod imp { renderer: RefCell>>, pub make_test_case: OnceCell, test_case: RefCell>>, - clock: RefCell, + pub clock: RefCell, } #[glib::object_subclass] @@ -127,6 +128,10 @@ mod imp { let size = self.size.get(); + let frame_clock = self.obj().frame_clock().unwrap(); + let time = Duration::from_micros(frame_clock.frame_time() as u64); + self.clock.borrow_mut().set_unadjusted(time); + // Create the test case if missing. let mut case = self.test_case.borrow_mut(); let case = case.get_or_insert_with(|| { @@ -138,7 +143,7 @@ mod imp { make(args) }); - case.advance_animations(get_monotonic_time()); + case.advance_animations(self.clock.borrow_mut().now()); let rect: Rectangle = Rectangle::from_loc_and_size((0, 0), size); @@ -238,13 +243,33 @@ glib::wrapper! { } impl SmithayView { - pub fn new(make_test_case: impl Fn(Args) -> T + 'static) -> Self { + pub fn new( + make_test_case: impl Fn(Args) -> T + 'static, + anim_adjustment: >k::Adjustment, + ) -> Self { let obj: Self = glib::Object::builder().build(); let make = move |args| Box::new(make_test_case(args)) as Box; let make_test_case = Box::new(make) as _; let _ = obj.imp().make_test_case.set(make_test_case); + anim_adjustment.connect_value_changed({ + let obj = obj.downgrade(); + move |adj| { + if let Some(obj) = obj.upgrade() { + let mut clock = obj.imp().clock.borrow_mut(); + let instantly = adj.value() == 0.0; + let rate = if instantly { + 1.0 + } else { + 1.0 / adj.value().max(0.001) + }; + clock.set_rate(rate); + clock.set_complete_instantly(instantly); + } + } + }); + obj } } diff --git a/src/animation/clock.rs b/src/animation/clock.rs index 3cfa727b..30f8bd45 100644 --- a/src/animation/clock.rs +++ b/src/animation/clock.rs @@ -1,41 +1,202 @@ -use std::cell::Cell; +use std::cell::RefCell; use std::rc::Rc; use std::time::Duration; use crate::utils::get_monotonic_time; -/// Clock that can have its time value overridden. +/// Shareable lazy clock that can change rate. /// -/// Can be cloned to share the same clock. +/// The clock will fetch the time once and then retain it until explicitly cleared with +/// [`Clock::clear`]. #[derive(Debug, Default, Clone)] pub struct Clock { - time_override: Rc>>, + inner: Rc>, +} + +#[derive(Debug, Default)] +struct LazyClock { + time: Option, +} + +/// Clock that can adjust its rate. +#[derive(Debug)] +struct AdjustableClock { + inner: LazyClock, + current_time: Duration, + last_seen_time: Duration, + rate: f64, + complete_instantly: bool, } impl Clock { - /// Creates a new [`Clock`] with time override in place. - pub fn with_override(time: Duration) -> Self { + /// Creates a new clock with the given time. + pub fn with_time(time: Duration) -> Self { + let clock = AdjustableClock::new(LazyClock::with_time(time)); Self { - time_override: Rc::new(Cell::new(Some(time))), + inner: Rc::new(RefCell::new(clock)), } } - /// Sets the current time override. - pub fn set_time_override(&mut self, time: Option) { - self.time_override.set(time); + /// Returns the current time. + pub fn now(&self) -> Duration { + self.inner.borrow_mut().now() } - /// Gets the current time. - #[inline] - pub fn now(&self) -> Duration { - self.time_override.get().unwrap_or_else(get_monotonic_time) + /// Returns the underlying time not adjusted for rate change. + pub fn now_unadjusted(&self) -> Duration { + self.inner.borrow_mut().inner.now() + } + + /// Sets the unadjusted clock time. + pub fn set_unadjusted(&mut self, time: Duration) { + self.inner.borrow_mut().inner.set(time); + } + + /// Clears the stored time so it's re-fetched again next. + pub fn clear(&mut self) { + self.inner.borrow_mut().inner.clear(); + } + + /// Gets the clock rate. + pub fn rate(&self) -> f64 { + self.inner.borrow().rate() + } + + /// Sets the clock rate. + pub fn set_rate(&mut self, rate: f64) { + self.inner.borrow_mut().set_rate(rate); + } + + /// Returns whether animations should complete instantly. + pub fn should_complete_instantly(&self) -> bool { + self.inner.borrow().should_complete_instantly() + } + + /// Sets whether animations should complete instantly. + pub fn set_complete_instantly(&mut self, value: bool) { + self.inner.borrow_mut().set_complete_instantly(value); } } impl PartialEq for Clock { fn eq(&self, other: &Self) -> bool { - Rc::ptr_eq(&self.time_override, &other.time_override) + Rc::ptr_eq(&self.inner, &other.inner) } } impl Eq for Clock {} + +impl LazyClock { + pub fn with_time(time: Duration) -> Self { + Self { time: Some(time) } + } + + pub fn clear(&mut self) { + self.time = None; + } + + pub fn set(&mut self, time: Duration) { + self.time = Some(time); + } + + pub fn now(&mut self) -> Duration { + *self.time.get_or_insert_with(get_monotonic_time) + } +} + +impl AdjustableClock { + pub fn new(mut inner: LazyClock) -> Self { + let time = inner.now(); + Self { + inner, + current_time: time, + last_seen_time: time, + rate: 1., + complete_instantly: false, + } + } + + pub fn rate(&self) -> f64 { + self.rate + } + + pub fn set_rate(&mut self, rate: f64) { + self.rate = rate.clamp(0., 1000.); + } + + pub fn should_complete_instantly(&self) -> bool { + self.complete_instantly + } + + pub fn set_complete_instantly(&mut self, value: bool) { + self.complete_instantly = value; + } + + pub fn now(&mut self) -> Duration { + let time = self.inner.now(); + + if self.last_seen_time == time { + return self.current_time; + } + + if self.last_seen_time < time { + let delta = time - self.last_seen_time; + let delta = delta.mul_f64(self.rate); + self.current_time = self.current_time.saturating_add(delta); + } else { + let delta = self.last_seen_time - time; + let delta = delta.mul_f64(self.rate); + self.current_time = self.current_time.saturating_sub(delta); + } + + self.last_seen_time = time; + self.current_time + } +} + +impl Default for AdjustableClock { + fn default() -> Self { + Self::new(LazyClock::default()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frozen_clock() { + let mut clock = Clock::with_time(Duration::ZERO); + assert_eq!(clock.now(), Duration::ZERO); + + clock.set_unadjusted(Duration::from_millis(100)); + assert_eq!(clock.now(), Duration::from_millis(100)); + + clock.set_unadjusted(Duration::from_millis(200)); + assert_eq!(clock.now(), Duration::from_millis(200)); + } + + #[test] + fn rate_change() { + let mut clock = Clock::with_time(Duration::ZERO); + clock.set_rate(0.5); + + clock.set_unadjusted(Duration::from_millis(100)); + assert_eq!(clock.now_unadjusted(), Duration::from_millis(100)); + assert_eq!(clock.now(), Duration::from_millis(50)); + + clock.set_unadjusted(Duration::from_millis(200)); + assert_eq!(clock.now_unadjusted(), Duration::from_millis(200)); + assert_eq!(clock.now(), Duration::from_millis(100)); + + clock.set_unadjusted(Duration::from_millis(150)); + assert_eq!(clock.now_unadjusted(), Duration::from_millis(150)); + assert_eq!(clock.now(), Duration::from_millis(75)); + + clock.set_rate(2.0); + + clock.set_unadjusted(Duration::from_millis(250)); + assert_eq!(clock.now_unadjusted(), Duration::from_millis(250)); + assert_eq!(clock.now(), Duration::from_millis(275)); + } +} diff --git a/src/animation/mod.rs b/src/animation/mod.rs index 5efb0f16..50dfc195 100644 --- a/src/animation/mod.rs +++ b/src/animation/mod.rs @@ -2,7 +2,6 @@ use std::time::Duration; use keyframe::functions::{EaseOutCubic, EaseOutQuad}; use keyframe::EasingFunction; -use portable_atomic::{AtomicF64, Ordering}; mod spring; pub use spring::{Spring, SpringParams}; @@ -10,8 +9,6 @@ pub use spring::{Spring, SpringParams}; mod clock; pub use clock::Clock; -pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.); - #[derive(Debug, Clone)] pub struct Animation { from: f64, @@ -24,7 +21,7 @@ pub struct Animation { /// Best effort; not always exactly precise. clamped_duration: Duration, start_time: Duration, - current_time: Duration, + clock: Clock, kind: Kind, } @@ -50,23 +47,16 @@ pub enum Curve { impl Animation { pub fn new( - current_time: Duration, + clock: Clock, from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation, ) -> Self { - // Scale the velocity by slowdown to keep the touchpad gestures feeling right. - let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed); + // Scale the velocity by rate to keep the touchpad gestures feeling right. + let initial_velocity = initial_velocity / clock.rate().max(0.001); - let mut rv = Self::ease( - current_time, - from, - to, - initial_velocity, - 0, - Curve::EaseOutCubic, - ); + let mut rv = Self::ease(clock, from, to, initial_velocity, 0, Curve::EaseOutCubic); if config.off { rv.is_off = true; return rv; @@ -85,7 +75,6 @@ impl Animation { } let start_time = self.start_time; - let current_time = self.current_time; match config.kind { niri_config::AnimationKind::Spring(p) => { @@ -97,11 +86,11 @@ impl Animation { initial_velocity: self.initial_velocity, params, }; - *self = Self::spring(current_time, spring); + *self = Self::spring(self.clock.clone(), spring); } niri_config::AnimationKind::Easing(p) => { *self = Self::ease( - current_time, + self.clock.clone(), self.from, self.to, self.initial_velocity, @@ -112,27 +101,20 @@ impl Animation { } self.start_time = start_time; - self.current_time = current_time; } /// Restarts the animation using the previous config. - pub fn restarted( - &self, - current_time: Duration, - from: f64, - to: f64, - initial_velocity: f64, - ) -> Self { + pub fn restarted(&self, from: f64, to: f64, initial_velocity: f64) -> Self { if self.is_off { return self.clone(); } - // Scale the velocity by slowdown to keep the touchpad gestures feeling right. - let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed); + // Scale the velocity by rate to keep the touchpad gestures feeling right. + let initial_velocity = initial_velocity / self.clock.rate().max(0.001); match self.kind { Kind::Easing { curve } => Self::ease( - current_time, + self.clock.clone(), from, to, initial_velocity, @@ -146,7 +128,7 @@ impl Animation { initial_velocity: self.initial_velocity, params: spring.params, }; - Self::spring(current_time, spring) + Self::spring(self.clock.clone(), spring) } Kind::Deceleration { initial_velocity, @@ -154,7 +136,7 @@ impl Animation { } => { let threshold = 0.001; // FIXME Self::decelerate( - current_time, + self.clock.clone(), from, initial_velocity, deceleration_rate, @@ -165,7 +147,7 @@ impl Animation { } pub fn ease( - current_time: Duration, + clock: Clock, from: f64, to: f64, initial_velocity: f64, @@ -183,13 +165,13 @@ impl Animation { duration, // Our current curves never overshoot. clamped_duration: duration, - start_time: current_time, - current_time, + start_time: clock.now(), + clock, kind, } } - pub fn spring(current_time: Duration, spring: Spring) -> Self { + pub fn spring(clock: Clock, spring: Spring) -> Self { let _span = tracy_client::span!("Animation::spring"); let duration = spring.duration(); @@ -203,14 +185,14 @@ impl Animation { is_off: false, duration, clamped_duration, - start_time: current_time, - current_time, + start_time: clock.now(), + clock, kind, } } pub fn decelerate( - current_time: Duration, + clock: Clock, from: f64, initial_velocity: f64, deceleration_rate: f64, @@ -238,77 +220,26 @@ impl Animation { is_off: false, duration, clamped_duration: duration, - start_time: current_time, - current_time, + start_time: clock.now(), + clock, kind, } } - pub fn set_current_time(&mut self, time: Duration) { - if self.duration.is_zero() { - self.current_time = time; - return; - } - - let end_time = self.start_time + self.duration; - if end_time <= self.current_time { - return; - } - - let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed); - if slowdown <= f64::EPSILON { - // Zero slowdown will cause the animation to end right away. - self.current_time = end_time; - return; - } - - // We can't change current_time (since the incoming time values are always real-time), so - // apply the slowdown by shifting the start time to compensate. - if self.current_time <= time { - let delta = time - self.current_time; - - let max_delta = end_time - self.current_time; - let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64(); - if slowdown <= min_slowdown { - // Our slowdown value will cause the animation to end right away. - self.current_time = end_time; - return; - } - - let adjusted_delta = delta.div_f64(slowdown); - if adjusted_delta >= delta { - self.start_time -= adjusted_delta - delta; - } else { - self.start_time += delta - adjusted_delta; - } - } else { - let delta = self.current_time - time; - - let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64(); - if slowdown <= min_slowdown { - // Current time was about to jump to before the animation had started; let's just - // cancel the animation in this case. - self.current_time = end_time; - return; - } - - let adjusted_delta = delta.div_f64(slowdown); - if adjusted_delta >= delta { - self.start_time += adjusted_delta - delta; - } else { - self.start_time -= delta - adjusted_delta; - } + pub fn is_done(&self) -> bool { + if self.clock.should_complete_instantly() { + return true; } - self.current_time = time; - } - - pub fn is_done(&self) -> bool { - self.current_time >= self.start_time + self.duration + self.clock.now() >= self.start_time + self.duration } pub fn is_clamped_done(&self) -> bool { - self.current_time >= self.start_time + self.clamped_duration + if self.clock.should_complete_instantly() { + return true; + } + + self.clock.now() >= self.start_time + self.clamped_duration } pub fn value(&self) -> f64 { @@ -316,7 +247,7 @@ impl Animation { return self.to; } - let passed = self.current_time.saturating_sub(self.start_time); + let passed = self.clock.now().saturating_sub(self.start_time); match self.kind { Kind::Easing { curve } => { diff --git a/src/input/mod.rs b/src/input/mod.rs index eec8fbe4..bafa1505 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -83,11 +83,8 @@ impl State { { let _span = tracy_client::span!("process_input_event"); - // A bit of a hack, but animation end runs some logic (i.e. workspace clean-up) and it - // doesn't always trigger due to damage, etc. So run it here right before it might prove - // important. Besides, animations affect the input, so it's best to have up-to-date values - // here. - self.niri.advance_animations(get_monotonic_time()); + // Make sure some logic like workspace clean-up has a chance to run before doing actions. + self.niri.advance_animations(); if self.niri.monitors_active { // Notify the idle-notifier of activity. diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 16cc5524..facb59d3 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -314,6 +314,9 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply { let action = niri_config::Action::from(action); ctx.event_loop.insert_idle(move |state| { + // Make sure some logic like workspace clean-up has a chance to run before doing + // actions. + state.niri.advance_animations(); state.do_action(action, false); let _ = tx.send_blocking(()); }); diff --git a/src/layout/closing_window.rs b/src/layout/closing_window.rs index 744c099e..12a77f92 100644 --- a/src/layout/closing_window.rs +++ b/src/layout/closing_window.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::time::Duration; use anyhow::Context as _; use glam::{Mat3, Vec2}; @@ -138,15 +137,15 @@ impl ClosingWindow { }) } - pub fn advance_animations(&mut self, current_time: Duration) { + pub fn advance_animations(&mut self) { match &mut self.anim_state { AnimationState::Waiting { blocker, anim } => { if blocker.state() != BlockerState::Pending { - let anim = anim.restarted(current_time, 0., 1., 0.); + let anim = anim.restarted(0., 1., 0.); self.anim_state = AnimationState::Animating(anim); } } - AnimationState::Animating(anim) => anim.set_current_time(current_time), + AnimationState::Animating(_anim) => (), } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index f4e59af3..45228038 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -219,6 +219,8 @@ pub struct Layout { interactive_move: Option>, /// Clock for driving animations. clock: Clock, + /// Time that we last updated render elements for. + update_render_elements_time: Duration, /// Configurable properties of the layout. options: Rc, } @@ -447,6 +449,7 @@ impl Layout { last_active_workspace_id: HashMap::new(), interactive_move: None, clock, + update_render_elements_time: Duration::ZERO, options: Rc::new(options), } } @@ -468,6 +471,7 @@ impl Layout { last_active_workspace_id: HashMap::new(), interactive_move: None, clock, + update_render_elements_time: Duration::ZERO, options: opts, } } @@ -2194,22 +2198,22 @@ impl Layout { } } - pub fn advance_animations(&mut self, current_time: Duration) { + pub fn advance_animations(&mut self) { let _span = tracy_client::span!("Layout::advance_animations"); if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { - move_.tile.advance_animations(current_time); + move_.tile.advance_animations(); } match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { - mon.advance_animations(current_time); + mon.advance_animations(); } } MonitorSet::NoOutputs { workspaces, .. } => { for ws in workspaces { - ws.advance_animations(current_time); + ws.advance_animations(); } } } @@ -2242,6 +2246,8 @@ impl Layout { pub fn update_render_elements(&mut self, output: Option<&Output>) { let _span = tracy_client::span!("Layout::update_render_elements"); + self.update_render_elements_time = self.clock.now(); + if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move { if output.map_or(true, |output| move_.output == *output) { let pos_within_output = move_.tile_render_location(); @@ -3475,6 +3481,10 @@ impl Layout { output: &Output, target: RenderTarget, ) -> impl Iterator> { + if self.update_render_elements_time != self.clock.now() { + error!("clock moved between updating render elements and rendering"); + } + let mut rv = None; if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { @@ -3653,7 +3663,7 @@ mod tests { impl Default for Layout { fn default() -> Self { - Self::with_options(Clock::with_override(Duration::ZERO), Default::default()) + Self::with_options(Clock::with_time(Duration::ZERO), Default::default()) } } @@ -4553,14 +4563,14 @@ mod tests { layout.refresh(is_active); } Op::AdvanceAnimations { msec_delta } => { - let mut now = layout.clock.now(); + let mut now = layout.clock.now_unadjusted(); if msec_delta >= 0 { now = now.saturating_add(Duration::from_millis(msec_delta as u64)); } else { now = now.saturating_sub(Duration::from_millis(-msec_delta as u64)); } - layout.clock.set_time_override(Some(now)); - layout.advance_animations(now); + layout.clock.set_unadjusted(now); + layout.advance_animations(); } Op::MoveWorkspaceToOutput(id) => { let name = format!("output{id}"); @@ -4674,7 +4684,7 @@ mod tests { #[track_caller] fn check_ops_with_options(options: Options, ops: &[Op]) { - let mut layout = Layout::with_options(Clock::with_override(Duration::ZERO), options); + let mut layout = Layout::with_options(Clock::with_time(Duration::ZERO), options); for op in ops { op.apply(&mut layout); diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index aad921bb..52e73cce 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -184,7 +184,7 @@ impl Monitor { self.active_workspace_idx = idx; self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( - self.clock.now(), + self.clock.clone(), current_idx, idx as f64, 0., @@ -734,9 +734,8 @@ impl Monitor { Some(column.tiles[column.active_tile_idx].window()) } - pub fn advance_animations(&mut self, current_time: Duration) { + pub fn advance_animations(&mut self) { if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch { - anim.set_current_time(current_time); if anim.is_done() { self.workspace_switch = None; self.clean_up_workspaces(); @@ -744,7 +743,7 @@ impl Monitor { } for ws in &mut self.workspaces { - ws.advance_animations(current_time); + ws.advance_animations(); } } @@ -1112,7 +1111,7 @@ impl Monitor { self.active_workspace_idx = new_idx; self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( - self.clock.now(), + self.clock.clone(), gesture.current_idx, new_idx as f64, velocity, diff --git a/src/layout/opening_window.rs b/src/layout/opening_window.rs index 90e50581..0a3d4973 100644 --- a/src/layout/opening_window.rs +++ b/src/layout/opening_window.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::time::Duration; use anyhow::Context as _; use glam::{Mat3, Vec2}; @@ -41,9 +40,7 @@ impl OpenAnimation { } } - pub fn advance_animations(&mut self, current_time: Duration) { - self.anim.set_current_time(current_time); - } + pub fn advance_animations(&mut self) {} pub fn is_done(&self) -> bool { self.anim.is_done() diff --git a/src/layout/tile.rs b/src/layout/tile.rs index dd3d50af..6646999f 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -1,5 +1,4 @@ use std::rc::Rc; -use std::time::Duration; use niri_config::{Color, CornerRadius, GradientInterpolation}; use smithay::backend::allocator::Fourcc; @@ -185,7 +184,7 @@ impl Tile { let change = f64::max(change.x.abs(), change.y.abs()); if change > RESIZE_ANIMATION_THRESHOLD { let anim = Animation::new( - self.clock.now(), + self.clock.clone(), 0., 1., 0., @@ -218,29 +217,25 @@ impl Tile { self.rounded_corner_damage.set_size(window_size); } - pub fn advance_animations(&mut self, current_time: Duration) { + pub fn advance_animations(&mut self) { if let Some(open) = &mut self.open_animation { - open.advance_animations(current_time); if open.is_done() { self.open_animation = None; } } if let Some(resize) = &mut self.resize_animation { - resize.anim.set_current_time(current_time); if resize.anim.is_done() { self.resize_animation = None; } } if let Some(move_) = &mut self.move_x_animation { - move_.anim.set_current_time(current_time); if move_.anim.is_done() { self.move_x_animation = None; } } if let Some(move_) = &mut self.move_y_animation { - move_.anim.set_current_time(current_time); if move_.anim.is_done() { self.move_y_animation = None; } @@ -326,7 +321,7 @@ impl Tile { pub fn start_open_animation(&mut self) { self.open_animation = Some(OpenAnimation::new(Animation::new( - self.clock.now(), + self.clock.clone(), 0., 1., 0., @@ -353,8 +348,8 @@ impl Tile { // Preserve the previous config if ongoing. let anim = self.move_x_animation.take().map(|move_| move_.anim); let anim = anim - .map(|anim| anim.restarted(self.clock.now(), 1., 0., 0.)) - .unwrap_or_else(|| Animation::new(self.clock.now(), 1., 0., 0., config)); + .map(|anim| anim.restarted(1., 0., 0.)) + .unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config)); self.move_x_animation = Some(MoveAnimation { anim, @@ -372,8 +367,8 @@ impl Tile { // Preserve the previous config if ongoing. let anim = self.move_y_animation.take().map(|move_| move_.anim); let anim = anim - .map(|anim| anim.restarted(self.clock.now(), 1., 0., 0.)) - .unwrap_or_else(|| Animation::new(self.clock.now(), 1., 0., 0., config)); + .map(|anim| anim.restarted(1., 0., 0.)) + .unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config)); self.move_y_animation = Some(MoveAnimation { anim, diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 6432ca8b..10e2b0a4 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -84,10 +84,7 @@ pub struct Workspace { /// Any gaps, including left padding from work area left exclusive zone, is handled /// with this view offset (rather than added as a constant elsewhere in the code). This allows /// for natural handling of fullscreen windows, which must ignore work area padding. - view_offset: f64, - - /// Adjustment of the view offset, if one is currently ongoing. - view_offset_adj: Option, + view_offset: ViewOffset, /// Whether to activate the previous, rather than the next, column upon column removal. /// @@ -188,8 +185,12 @@ struct ColumnData { } #[derive(Debug)] -enum ViewOffsetAdjustment { +enum ViewOffset { + /// The view offset is static. + Static(f64), + /// The view offset is animating. Animation(Animation), + /// The view offset is controlled by the ongoing gesture. Gesture(ViewGesture), } @@ -325,17 +326,70 @@ impl OutputId { } } -impl ViewOffsetAdjustment { +impl ViewOffset { + /// Returns the current view offset. + pub fn current(&self) -> f64 { + match self { + ViewOffset::Static(offset) => *offset, + ViewOffset::Animation(anim) => anim.value(), + ViewOffset::Gesture(gesture) => gesture.current_view_offset, + } + } + + /// Returns the target view offset suitable for computing the new view offset. + pub fn target(&self) -> f64 { + match self { + ViewOffset::Static(offset) => *offset, + ViewOffset::Animation(anim) => anim.to(), + // This can be used for example if a gesture is interrupted. + ViewOffset::Gesture(gesture) => gesture.current_view_offset, + } + } + + /// Returns a view offset value suitable for saving and later restoration. + /// + /// This means that it shouldn't return an in-progress animation or gesture value. + fn static_view_offset(&self) -> f64 { + match self { + ViewOffset::Static(offset) => *offset, + // For animations we can return the final value. + ViewOffset::Animation(anim) => anim.to(), + ViewOffset::Gesture(gesture) => gesture.static_view_offset, + } + } + + pub fn is_static(&self) -> bool { + matches!(self, Self::Static(_)) + } + pub fn is_animation(&self) -> bool { matches!(self, Self::Animation(_)) } - pub fn target_view_offset(&self) -> f64 { + pub fn is_gesture(&self) -> bool { + matches!(self, Self::Gesture(_)) + } + + pub fn offset(&mut self, delta: f64) { match self { - ViewOffsetAdjustment::Animation(anim) => anim.to(), - ViewOffsetAdjustment::Gesture(gesture) => gesture.current_view_offset, + ViewOffset::Static(offset) => *offset += delta, + ViewOffset::Animation(anim) => anim.offset(delta), + ViewOffset::Gesture(_gesture) => { + // Is this needed? + error!("cancel gesture before offsetting"); + } + } + } + + pub fn cancel_gesture(&mut self) { + if let ViewOffset::Gesture(gesture) = self { + *self = ViewOffset::Static(gesture.current_view_offset); } } + + pub fn stop_anim_and_gesture(&mut self) { + *self = ViewOffset::Static(self.current()); + } } impl ColumnData { @@ -442,8 +496,7 @@ impl Workspace { data: vec![], active_column_idx: 0, interactive_resize: None, - view_offset: 0., - view_offset_adj: None, + view_offset: ViewOffset::Static(0.), activate_prev_column_on_removal: None, view_offset_before_fullscreen: None, closing_windows: vec![], @@ -484,8 +537,7 @@ impl Workspace { data: vec![], active_column_idx: 0, interactive_resize: None, - view_offset: 0., - view_offset_adj: None, + view_offset: ViewOffset::Static(0.), activate_prev_column_on_removal: None, view_offset_before_fullscreen: None, closing_windows: vec![], @@ -524,37 +576,31 @@ impl Workspace { || (self.options.always_center_single_column && self.columns.len() <= 1) } - pub fn advance_animations(&mut self, current_time: Duration) { - if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj { - anim.set_current_time(current_time); - self.view_offset = anim.value(); + pub fn advance_animations(&mut self) { + if let ViewOffset::Animation(anim) = &self.view_offset { if anim.is_done() { - self.view_offset_adj = None; + self.view_offset = ViewOffset::Static(anim.to()); } - } else if let Some(ViewOffsetAdjustment::Gesture(gesture)) = &self.view_offset_adj { - self.view_offset = gesture.current_view_offset; } for col in &mut self.columns { - col.advance_animations(current_time); + col.advance_animations(); } self.closing_windows.retain_mut(|closing| { - closing.advance_animations(current_time); + closing.advance_animations(); closing.are_animations_ongoing() }); } pub fn are_animations_ongoing(&self) -> bool { - self.view_offset_adj - .as_ref() - .is_some_and(|s| s.is_animation()) + self.view_offset.is_animation() || self.columns.iter().any(Column::are_animations_ongoing) || !self.closing_windows.is_empty() } pub fn are_transitions_ongoing(&self) -> bool { - self.view_offset_adj.is_some() + !self.view_offset.is_static() || self.columns.iter().any(Column::are_animations_ongoing) || !self.closing_windows.is_empty() } @@ -786,7 +832,7 @@ impl Workspace { fn compute_new_view_offset_fit( &self, - current_x: f64, + target_x: Option, col_x: f64, width: f64, is_fullscreen: bool, @@ -795,14 +841,10 @@ impl Workspace { return 0.; } - let final_x = if let Some(ViewOffsetAdjustment::Animation(anim)) = &self.view_offset_adj { - current_x - self.view_offset + anim.to() - } else { - current_x - }; + let target_x = target_x.unwrap_or_else(|| self.target_view_pos()); let new_offset = compute_new_view_offset( - final_x + self.working_area.loc.x, + target_x + self.working_area.loc.x, self.working_area.size.w, col_x, width, @@ -815,37 +857,41 @@ impl Workspace { fn compute_new_view_offset_centered( &self, - current_x: f64, + target_x: Option, col_x: f64, width: f64, is_fullscreen: bool, ) -> f64 { if is_fullscreen { - return self.compute_new_view_offset_fit(current_x, col_x, width, is_fullscreen); + return self.compute_new_view_offset_fit(target_x, col_x, width, is_fullscreen); } // Columns wider than the view are left-aligned (the fit code can deal with that). if self.working_area.size.w <= width { - return self.compute_new_view_offset_fit(current_x, col_x, width, is_fullscreen); + return self.compute_new_view_offset_fit(target_x, col_x, width, is_fullscreen); } -(self.working_area.size.w - width) / 2. - self.working_area.loc.x } - fn compute_new_view_offset_for_column_fit(&self, current_x: f64, idx: usize) -> f64 { + fn compute_new_view_offset_for_column_fit(&self, target_x: Option, idx: usize) -> f64 { let col = &self.columns[idx]; self.compute_new_view_offset_fit( - current_x, + target_x, self.column_x(idx), col.width(), col.is_fullscreen, ) } - fn compute_new_view_offset_for_column_centered(&self, current_x: f64, idx: usize) -> f64 { + fn compute_new_view_offset_for_column_centered( + &self, + target_x: Option, + idx: usize, + ) -> f64 { let col = &self.columns[idx]; self.compute_new_view_offset_centered( - current_x, + target_x, self.column_x(idx), col.width(), col.is_fullscreen, @@ -854,21 +900,21 @@ impl Workspace { fn compute_new_view_offset_for_column( &self, - current_x: f64, + target_x: Option, idx: usize, prev_idx: Option, ) -> f64 { if self.is_centering_focused_column() { - return self.compute_new_view_offset_for_column_centered(current_x, idx); + return self.compute_new_view_offset_for_column_centered(target_x, idx); } match self.options.center_focused_column { CenterFocusedColumn::Always => { - self.compute_new_view_offset_for_column_centered(current_x, idx) + self.compute_new_view_offset_for_column_centered(target_x, idx) } CenterFocusedColumn::OnOverflow => { let Some(prev_idx) = prev_idx else { - return self.compute_new_view_offset_for_column_fit(current_x, idx); + return self.compute_new_view_offset_for_column_fit(target_x, idx); }; // Always take the left or right neighbor of the target as the source. @@ -878,36 +924,35 @@ impl Workspace { idx.saturating_sub(1) }; - let source_x = self.column_x(source_idx); - let source_width = self.columns[source_idx].width(); + let source_col_x = self.column_x(source_idx); + let source_col_width = self.columns[source_idx].width(); - let target_x = self.column_x(idx); - let target_width = self.columns[idx].width(); + let target_col_x = self.column_x(idx); + let target_col_width = self.columns[idx].width(); - let total_width = if source_x < target_x { + let total_width = if source_col_x < target_col_x { // Source is left from target. - target_x - source_x + target_width + target_col_x - source_col_x + target_col_width } else { // Source is right from target. - source_x - target_x + source_width + source_col_x - target_col_x + source_col_width } + self.options.gaps * 2.; // If it fits together, do a normal animation, otherwise center the new column. if total_width <= self.working_area.size.w { - self.compute_new_view_offset_for_column_fit(current_x, idx) + self.compute_new_view_offset_for_column_fit(target_x, idx) } else { - self.compute_new_view_offset_for_column_centered(current_x, idx) + self.compute_new_view_offset_for_column_centered(target_x, idx) } } CenterFocusedColumn::Never => { - self.compute_new_view_offset_for_column_fit(current_x, idx) + self.compute_new_view_offset_for_column_fit(target_x, idx) } } } - fn animate_view_offset(&mut self, current_x: f64, idx: usize, new_view_offset: f64) { + fn animate_view_offset(&mut self, idx: usize, new_view_offset: f64) { self.animate_view_offset_with_config( - current_x, idx, new_view_offset, self.options.animations.horizontal_view_movement.0, @@ -916,78 +961,67 @@ impl Workspace { fn animate_view_offset_with_config( &mut self, - current_x: f64, idx: usize, new_view_offset: f64, config: niri_config::Animation, ) { + self.view_offset.cancel_gesture(); + let new_col_x = self.column_x(idx); - let old_col_x = current_x - self.view_offset; + let old_col_x = self.column_x(self.active_column_idx); let offset_delta = old_col_x - new_col_x; - self.view_offset += offset_delta; + self.view_offset.offset(offset_delta); let pixel = 1. / self.scale.fractional_scale(); - // If we're already animating towards that, don't restart it. - if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj { - // Offset the animation for the active column change. - anim.offset(offset_delta); - - let to_diff = new_view_offset - anim.to(); - if (anim.value() - self.view_offset).abs() < pixel && to_diff.abs() < pixel { - // Correct for any inaccuracy. - anim.offset(to_diff); - return; - } - } - - // If our view offset is already this, we don't need to do anything. - if (self.view_offset - new_view_offset).abs() < pixel { + // If our view offset is already this or animating towards this, we don't need to do + // anything. + let to_diff = new_view_offset - self.view_offset.target(); + if to_diff.abs() < pixel { // Correct for any inaccuracy. - self.view_offset = new_view_offset; - self.view_offset_adj = None; + self.view_offset.offset(to_diff); return; } // FIXME: also compute and use current velocity. - self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( - self.clock.now(), - self.view_offset, + self.view_offset = ViewOffset::Animation(Animation::new( + self.clock.clone(), + self.view_offset.current(), new_view_offset, 0., config, - ))); + )); } fn animate_view_offset_to_column_centered( &mut self, - current_x: f64, + target_x: Option, idx: usize, config: niri_config::Animation, ) { - let new_view_offset = self.compute_new_view_offset_for_column_centered(current_x, idx); - self.animate_view_offset_with_config(current_x, idx, new_view_offset, config); + let new_view_offset = self.compute_new_view_offset_for_column_centered(target_x, idx); + self.animate_view_offset_with_config(idx, new_view_offset, config); } fn animate_view_offset_to_column_with_config( &mut self, - current_x: f64, + target_x: Option, idx: usize, prev_idx: Option, config: niri_config::Animation, ) { - let new_view_offset = self.compute_new_view_offset_for_column(current_x, idx, prev_idx); - self.animate_view_offset_with_config(current_x, idx, new_view_offset, config); + let new_view_offset = self.compute_new_view_offset_for_column(target_x, idx, prev_idx); + self.animate_view_offset_with_config(idx, new_view_offset, config); } fn animate_view_offset_to_column( &mut self, - current_x: f64, + target_x: Option, idx: usize, prev_idx: Option, ) { self.animate_view_offset_to_column_with_config( - current_x, + target_x, idx, prev_idx, self.options.animations.horizontal_view_movement.0, @@ -1006,9 +1040,8 @@ impl Workspace { return; } - let current_x = self.view_pos(); self.animate_view_offset_to_column_with_config( - current_x, + None, idx, Some(self.active_column_idx), config, @@ -1240,14 +1273,13 @@ impl Workspace { // If this is the first window on an empty workspace, remove the effect of whatever // view_offset was left over and skip the animation. if was_empty { - self.view_offset = 0.; - self.view_offset_adj = None; + self.view_offset = ViewOffset::Static(0.); self.view_offset = - self.compute_new_view_offset_for_column(self.view_pos(), idx, None); + ViewOffset::Static(self.compute_new_view_offset_for_column(None, idx, None)); } let prev_offset = (!was_empty && idx == self.active_column_idx + 1) - .then(|| self.static_view_offset()); + .then(|| self.view_offset.static_view_offset()); let anim_config = anim_config.unwrap_or(self.options.animations.horizontal_view_movement.0); @@ -1416,15 +1448,13 @@ impl Workspace { // Restore the view offset but make sure to scroll the view in case the // previous window had resized. - let current_x = self.view_pos(); self.animate_view_offset_with_config( - current_x, self.active_column_idx, prev_offset, view_config, ); self.animate_view_offset_to_column_with_config( - current_x, + None, self.active_column_idx, None, view_config, @@ -1509,6 +1539,9 @@ impl Workspace { // the Resizing state, which can trigger this code path for a while. let resize = if offset != 0. { resize } else { None }; if let Some(resize) = resize { + // Don't bother with the gesture. + self.view_offset.cancel_gesture(); + // If this is an interactive resize commit of an active window, then we need to // either preserve the view offset or adjust it accordingly. let centered = self.is_centering_focused_column(); @@ -1518,33 +1551,24 @@ impl Workspace { // FIXME: when view_offset becomes fractional, this can be made additive too. let new_offset = -(self.working_area.size.w - width) / 2. - self.working_area.loc.x; - new_offset - self.view_offset + new_offset - self.view_offset.target() } else if resize.edges.contains(ResizeEdge::LEFT) { -offset } else { 0. }; - self.view_offset += offset; - if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj { - anim.offset(offset); - } else { - // Don't bother with the gesture. - self.view_offset_adj = None; - } + self.view_offset.offset(offset); } - if self.interactive_resize.is_none() - && !matches!(self.view_offset_adj, Some(ViewOffsetAdjustment::Gesture(_))) - { + if self.interactive_resize.is_none() && !self.view_offset.is_gesture() { // We might need to move the view to ensure the resized window is still visible. - let current_x = self.view_pos(); // Upon unfullscreening, restore the view offset. let is_fullscreen = self.columns[col_idx].tiles[tile_idx].is_fullscreen(); if was_fullscreen && !is_fullscreen { if let Some(prev_offset) = self.view_offset_before_fullscreen.take() { - self.animate_view_offset(current_x, col_idx, prev_offset); + self.animate_view_offset(col_idx, prev_offset); } } @@ -1558,7 +1582,7 @@ impl Workspace { // FIXME: we will want to skip the animation in some cases here to make continuously // resizing windows not look janky. - self.animate_view_offset_to_column_with_config(current_x, col_idx, None, config); + self.animate_view_offset_to_column_with_config(None, col_idx, None, config); } } } @@ -1574,22 +1598,16 @@ impl Workspace { return 0.; } - let current_x = self.view_pos(); + // Consider the end of an ongoing animation because that's what compute to fit does too. + let target_x = self.target_view_pos(); let new_view_offset = self.compute_new_view_offset_for_column( - current_x, + Some(target_x), column_idx, Some(self.active_column_idx), ); - // Consider the end of an ongoing animation because that's what compute to fit does too. - let final_x = if let Some(ViewOffsetAdjustment::Animation(anim)) = &self.view_offset_adj { - current_x - self.view_offset + anim.to() - } else { - current_x - }; - let new_col_x = self.column_x(column_idx); - let from_view_offset = final_x - new_col_x; + let from_view_offset = target_x - new_col_x; (from_view_offset - new_view_offset).abs() / self.working_area.size.w } @@ -1698,7 +1716,7 @@ impl Workspace { let output_scale = Scale::from(self.scale.fractional_scale()); let anim = Animation::new( - self.clock.now(), + self.clock.clone(), 0., 1., 0., @@ -1870,10 +1888,7 @@ impl Workspace { // Preserve the camera position when moving to the left. let view_offset_delta = -self.column_x(self.active_column_idx) + current_col_x; - self.view_offset += view_offset_delta; - if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj { - anim.offset(view_offset_delta); - } + self.view_offset.offset(view_offset_delta); // The column we just moved is offset by the difference between its new and old position. let new_col_x = self.column_x(new_idx); @@ -1987,7 +2002,8 @@ impl Workspace { if source_tile_was_active { // Make sure the previous (target) column is activated so the animation looks right. - self.activate_prev_column_on_removal = Some(self.static_view_offset() + offset.x); + self.activate_prev_column_on_removal = + Some(self.view_offset.static_view_offset() + offset.x); } offset.x += self.columns[source_col_idx].render_offset().x; @@ -2204,9 +2220,8 @@ impl Workspace { return; } - let center_x = self.view_pos(); self.animate_view_offset_to_column_centered( - center_x, + None, self.active_column_idx, self.options.animations.horizontal_view_movement.0, ); @@ -2216,19 +2231,11 @@ impl Workspace { } pub fn view_pos(&self) -> f64 { - self.column_x(self.active_column_idx) + self.view_offset + self.column_x(self.active_column_idx) + self.view_offset.current() } - /// Returns a view offset value suitable for saving and later restoration. - /// - /// This means that it shouldn't return an in-progress animation or gesture value. - fn static_view_offset(&self) -> f64 { - match &self.view_offset_adj { - // For animations we can return the final value. - Some(ViewOffsetAdjustment::Animation(anim)) => anim.to(), - Some(ViewOffsetAdjustment::Gesture(gesture)) => gesture.static_view_offset, - _ => self.view_offset, - } + pub fn target_view_pos(&self) -> f64 { + self.column_x(self.active_column_idx) + self.view_offset.target() } // HACK: pass a self.data iterator in manually as a workaround for the lack of method partial @@ -2398,9 +2405,9 @@ impl Workspace { // effect here. if self.columns.is_empty() { let view_offset = if self.is_centering_focused_column() { - self.compute_new_view_offset_centered(0., 0., hint_area.size.w, false) + self.compute_new_view_offset_centered(Some(0.), 0., hint_area.size.w, false) } else { - self.compute_new_view_offset_fit(0., 0., hint_area.size.w, false) + self.compute_new_view_offset_fit(Some(0.), 0., hint_area.size.w, false) }; hint_area.loc.x -= view_offset; } else { @@ -2429,10 +2436,7 @@ impl Workspace { pub fn active_tile_visual_rectangle(&self) -> Option> { let col = self.columns.get(self.active_column_idx)?; - let final_view_offset = self - .view_offset_adj - .as_ref() - .map_or(self.view_offset, |adj| adj.target_view_offset()); + let final_view_offset = self.view_offset.target(); let view_off = Point::from((-final_view_offset, 0.)); let (tile, tile_off) = col.tiles().nth(col.active_tile_idx).unwrap(); @@ -2623,7 +2627,7 @@ impl Workspace { && col_idx == self.active_column_idx && self.columns[col_idx].tiles.len() == 1 { - self.view_offset_before_fullscreen = Some(self.static_view_offset()); + self.view_offset_before_fullscreen = Some(self.view_offset.static_view_offset()); } let mut col = &mut self.columns[col_idx]; @@ -2682,7 +2686,7 @@ impl Workspace { return false; } - if self.view_offset_adj.is_some() { + if !self.view_offset.is_static() { return false; } @@ -2745,13 +2749,13 @@ impl Workspace { } let gesture = ViewGesture { - current_view_offset: self.view_offset, + current_view_offset: self.view_offset.current(), tracker: SwipeTracker::new(), - delta_from_tracker: self.view_offset, - static_view_offset: self.static_view_offset(), + delta_from_tracker: self.view_offset.current(), + static_view_offset: self.view_offset.static_view_offset(), is_touchpad, }; - self.view_offset_adj = Some(ViewOffsetAdjustment::Gesture(gesture)); + self.view_offset = ViewOffset::Gesture(gesture); } pub fn view_offset_gesture_update( @@ -2760,7 +2764,7 @@ impl Workspace { timestamp: Duration, is_touchpad: bool, ) -> Option { - let Some(ViewOffsetAdjustment::Gesture(gesture)) = &mut self.view_offset_adj else { + let ViewOffset::Gesture(gesture) = &mut self.view_offset else { return None; }; @@ -2783,7 +2787,7 @@ impl Workspace { } pub fn view_offset_gesture_end(&mut self, _cancelled: bool, is_touchpad: Option) -> bool { - let Some(ViewOffsetAdjustment::Gesture(gesture)) = &self.view_offset_adj else { + let ViewOffset::Gesture(gesture) = &mut self.view_offset else { return false; }; @@ -2806,8 +2810,7 @@ impl Workspace { let current_view_offset = pos + gesture.delta_from_tracker; if self.columns.is_empty() { - self.view_offset = current_view_offset; - self.view_offset_adj = None; + self.view_offset = ViewOffset::Static(current_view_offset); return true; } @@ -2979,7 +2982,6 @@ impl Workspace { let new_col_x = self.column_x(new_col_idx); let delta = active_col_x - new_col_x; - self.view_offset = current_view_offset + delta; if self.active_column_idx != new_col_idx { self.view_offset_before_fullscreen = None; @@ -2989,16 +2991,16 @@ impl Workspace { let target_view_offset = target_snap.view_pos - new_col_x; - self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( - self.clock.now(), + self.view_offset = ViewOffset::Animation(Animation::new( + self.clock.clone(), current_view_offset + delta, target_view_offset, velocity, self.options.animations.horizontal_view_movement.0, - ))); + )); // HACK: deal with things like snapping to the right edge of a larger-than-view window. - self.animate_view_offset_to_column(self.view_pos(), new_col_idx, None); + self.animate_view_offset_to_column(None, new_col_idx, None); true } @@ -3033,8 +3035,7 @@ impl Workspace { }; self.interactive_resize = Some(resize); - // Stop ongoing animation. - self.view_offset_adj = None; + self.view_offset.stop_anim_and_gesture(); true } @@ -3112,7 +3113,7 @@ impl Workspace { // Animate the active window into view right away. if self.columns[self.active_column_idx].contains(window) { - self.animate_view_offset_to_column(self.view_pos(), self.active_column_idx, None); + self.animate_view_offset_to_column(None, self.active_column_idx, None); } } @@ -3282,16 +3283,15 @@ impl Column { self.update_tile_sizes(animate); } - pub fn advance_animations(&mut self, current_time: Duration) { + pub fn advance_animations(&mut self) { if let Some(anim) = &mut self.move_animation { - anim.set_current_time(current_time); if anim.is_done() { self.move_animation = None; } } for tile in &mut self.tiles { - tile.advance_animations(current_time); + tile.advance_animations(); } } @@ -3335,7 +3335,7 @@ impl Column { let current_offset = self.move_animation.as_ref().map_or(0., Animation::value); self.move_animation = Some(Animation::new( - self.clock.now(), + self.clock.clone(), from_x_offset + current_offset, 0., 0., diff --git a/src/main.rs b/src/main.rs index 1d4ed56f..43fa2db6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,6 @@ use std::{env, mem}; use clap::Parser; use directories::ProjectDirs; -use niri::animation; use niri::cli::{Cli, Sub}; #[cfg(feature = "dbus")] use niri::dbus; @@ -164,13 +163,6 @@ fn main() -> Result<(), Box> { }) .unwrap_or_default(); - let slowdown = if config.animations.off { - 0. - } else { - config.animations.slowdown.clamp(0., 100.) - }; - animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed); - let spawn_at_startup = mem::take(&mut config.spawn_at_startup); *CHILD_ENV.write().unwrap() = mem::take(&mut config.environment); diff --git a/src/niri.rs b/src/niri.rs index 57050d6d..b2e17d5d 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -122,6 +122,7 @@ use crate::layer::MappedLayer; use crate::layout::tile::TileRenderElement; use crate::layout::workspace::WorkspaceId; use crate::layout::{Layout, LayoutElement as _, MonitorRenderElement}; +use crate::niri_render_elements; use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState}; use crate::protocols::gamma_control::GammaControlManagerState; use crate::protocols::mutter_x11_interop::MutterX11InteropManagerState; @@ -150,7 +151,6 @@ use crate::utils::{ make_screenshot_path, output_matches_name, output_size, send_scale_transform, write_png_rgba8, }; use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped, WindowRef}; -use crate::{animation, niri_render_elements}; const CLEAR_COLOR_LOCKED: [f32; 4] = [0.3, 0.1, 0.1, 1.]; @@ -553,12 +553,21 @@ impl State { self.refresh(); + // Advance animations to the current time (not target render time) before rendering outputs + // in order to clear completed animations and render elements. Even if we're not rendering, + // it's good to advance every now and then so the workspace clean-up and animations don't + // build up (the 1 second frame callback timer will call this line). + self.niri.advance_animations(); + self.niri.redraw_queued_outputs(&mut self.backend); { let _span = tracy_client::span!("flush_clients"); self.niri.display_handle.flush_clients().unwrap(); } + + // Clear the time so it's fetched afresh next iteration. + self.niri.clock.clear(); } fn refresh(&mut self) { @@ -1054,12 +1063,11 @@ impl State { self.niri.layout.ensure_named_workspace(ws_config); } - let slowdown = if config.animations.off { - 0. - } else { - config.animations.slowdown.clamp(0., 100.) - }; - animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed); + let rate = 1.0 / config.animations.slowdown.max(0.001); + self.niri.clock.set_rate(rate); + self.niri + .clock + .set_complete_instantly(config.animations.off); *CHILD_ENV.write().unwrap() = mem::take(&mut config.environment); @@ -1675,8 +1683,13 @@ impl Niri { let config_ = config.borrow(); let config_file_output_config = config_.outputs.clone(); - let clock = Clock::default(); - let layout = Layout::new(clock.clone(), &config_); + let mut animation_clock = Clock::default(); + + let rate = 1.0 / config_.animations.slowdown.max(0.001); + animation_clock.set_rate(rate); + animation_clock.set_complete_instantly(config_.animations.off); + + let layout = Layout::new(animation_clock.clone(), &config_); let (blocker_cleared_tx, blocker_cleared_rx) = mpsc::channel(); @@ -1804,8 +1817,9 @@ impl Niri { let mods_with_finger_scroll_binds = mods_with_finger_scroll_binds(backend.mod_key(), &config_.binds); - let screenshot_ui = ScreenshotUi::new(clock.clone(), config.clone()); - let config_error_notification = ConfigErrorNotification::new(clock.clone(), config.clone()); + let screenshot_ui = ScreenshotUi::new(animation_clock.clone(), config.clone()); + let config_error_notification = + ConfigErrorNotification::new(animation_clock.clone(), config.clone()); let mut hotkey_overlay = HotkeyOverlay::new(config.clone(), backend.mod_key()); if !config_.hotkey_overlay.skip_at_startup { @@ -1900,7 +1914,7 @@ impl Niri { display_handle, start_time: Instant::now(), is_at_startup: true, - clock, + clock: animation_clock, layout, global_space: Space::default(), @@ -3042,16 +3056,15 @@ impl Niri { } } - pub fn advance_animations(&mut self, target_time: Duration) { - self.layout.advance_animations(target_time); - self.config_error_notification - .advance_animations(target_time); - self.screenshot_ui.advance_animations(target_time); + pub fn advance_animations(&mut self) { + let _span = tracy_client::span!("Niri::advance_animations"); + + self.layout.advance_animations(); + self.config_error_notification.advance_animations(); + self.screenshot_ui.advance_animations(); for state in self.output_state.values_mut() { if let Some(transition) = &mut state.screen_transition { - // Screen transition uses real time so that it's not affected by animation slowdown. - transition.advance_animations(target_time); if transition.is_done() { state.screen_transition = None; } @@ -3263,11 +3276,13 @@ impl Niri { let target_presentation_time = state.frame_clock.next_presentation_time(); + // Freeze the clock at the target time. + self.clock.set_unadjusted(target_presentation_time); + + self.update_render_elements(Some(output)); + let mut res = RenderResult::Skipped; if self.monitors_active { - // Update from the config and advance the animations. - self.advance_animations(target_presentation_time); - let state = self.output_state.get_mut(output).unwrap(); state.unfinished_animations_remain = self.layout.are_animations_ongoing(Some(output)); state.unfinished_animations_remain |= @@ -3280,8 +3295,6 @@ impl Niri { .cursor_manager .is_current_cursor_animated(output.current_scale().integer_scale()); - self.update_render_elements(Some(output)); - // Render. res = backend.render(self, output, target_presentation_time); } @@ -4827,11 +4840,13 @@ impl Niri { Duration::from_millis(u64::from(d)) }); - // Screen transition uses real time so that it's not affected by animation slowdown. - let start_at = get_monotonic_time() + delay; for (output, from_texture) in textures { let state = self.output_state.get_mut(&output).unwrap(); - state.screen_transition = Some(ScreenTransition::new(from_texture, start_at)); + state.screen_transition = Some(ScreenTransition::new( + from_texture, + delay, + self.clock.clone(), + )); } // We don't actually need to queue a redraw because the point is to freeze the screen for a diff --git a/src/ui/config_error_notification.rs b/src/ui/config_error_notification.rs index af4f67e3..bd46aa7d 100644 --- a/src/ui/config_error_notification.rs +++ b/src/ui/config_error_notification.rs @@ -60,7 +60,7 @@ impl ConfigErrorNotification { fn animation(&self, from: f64, to: f64) -> Animation { let c = self.config.borrow(); Animation::new( - self.clock.now(), + self.clock.clone(), from, to, 0., @@ -96,11 +96,10 @@ impl ConfigErrorNotification { self.state = State::Hiding(self.animation(1., 0.)); } - pub fn advance_animations(&mut self, target_presentation_time: Duration) { + pub fn advance_animations(&mut self) { match &mut self.state { State::Hidden => (), State::Showing(anim) => { - anim.set_current_time(target_presentation_time); if anim.is_done() { let duration = if self.created_path.is_some() { // Make this quite a bit longer because it comes with a monitor modeset @@ -110,16 +109,15 @@ impl ConfigErrorNotification { } else { Duration::from_secs(4) }; - self.state = State::Shown(target_presentation_time + duration); + self.state = State::Shown(self.clock.now() + duration); } } State::Shown(deadline) => { - if target_presentation_time >= *deadline { + if self.clock.now() >= *deadline { self.hide(); } } State::Hiding(anim) => { - anim.set_current_time(target_presentation_time); if anim.is_clamped_done() { self.state = State::Hidden; } diff --git a/src/ui/screen_transition.rs b/src/ui/screen_transition.rs index 3b5c2159..7d26f85f 100644 --- a/src/ui/screen_transition.rs +++ b/src/ui/screen_transition.rs @@ -4,6 +4,7 @@ use smithay::backend::renderer::element::Kind; use smithay::backend::renderer::gles::GlesTexture; use smithay::utils::{Scale, Transform}; +use crate::animation::Clock; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement}; use crate::render_helpers::RenderTarget; @@ -17,31 +18,25 @@ pub struct ScreenTransition { from_texture: [TextureBuffer; 3], /// Monotonic time when to start the crossfade. start_at: Duration, - /// Current crossfade alpha. - alpha: f32, + /// Clock to drive animations. + clock: Clock, } impl ScreenTransition { - pub fn new(from_texture: [TextureBuffer; 3], start_at: Duration) -> Self { + pub fn new( + from_texture: [TextureBuffer; 3], + delay: Duration, + clock: Clock, + ) -> Self { Self { from_texture, - start_at, - alpha: 1., - } - } - - pub fn advance_animations(&mut self, current_time: Duration) { - if self.start_at + DURATION <= current_time { - self.alpha = 0.; - } else if self.start_at <= current_time { - self.alpha = 1. - (current_time - self.start_at).as_secs_f32() / DURATION.as_secs_f32(); - } else { - self.alpha = 1.; + start_at: clock.now_unadjusted() + delay, + clock, } } pub fn is_done(&self) -> bool { - self.alpha == 0. + self.start_at + DURATION <= self.clock.now_unadjusted() } pub fn update_render_elements(&mut self, scale: Scale, transform: Transform) { @@ -53,6 +48,17 @@ impl ScreenTransition { } pub fn render(&self, target: RenderTarget) -> PrimaryGpuTextureRenderElement { + // Screen transition ignores animation slowdown. + let now = self.clock.now_unadjusted(); + + let alpha = if self.start_at + DURATION <= now { + 0. + } else if self.start_at <= now { + 1. - (now - self.start_at).as_secs_f32() / DURATION.as_secs_f32() + } else { + 1. + }; + let idx = match target { RenderTarget::Output => 0, RenderTarget::Screencast => 1, @@ -62,7 +68,7 @@ impl ScreenTransition { PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer( self.from_texture[idx].clone(), (0., 0.), - self.alpha, + alpha, None, None, Kind::Unspecified, diff --git a/src/ui/screenshot_ui.rs b/src/ui/screenshot_ui.rs index cde9bdac..f7d1bd1a 100644 --- a/src/ui/screenshot_ui.rs +++ b/src/ui/screenshot_ui.rs @@ -3,7 +3,6 @@ use std::cmp::{max, min}; use std::collections::HashMap; use std::iter::zip; use std::rc::Rc; -use std::time::Duration; use anyhow::Context; use arrayvec::ArrayVec; @@ -185,7 +184,7 @@ impl ScreenshotUi { let open_anim = { let c = config.borrow(); - Animation::new(clock.now(), 0., 1., 0., c.animations.screenshot_ui_open.0) + Animation::new(clock.clone(), 0., 1., 0., c.animations.screenshot_ui_open.0) }; *self = Self::Open { @@ -238,13 +237,7 @@ impl ScreenshotUi { matches!(self, ScreenshotUi::Open { .. }) } - pub fn advance_animations(&mut self, current_time: Duration) { - let Self::Open { open_anim, .. } = self else { - return; - }; - - open_anim.set_current_time(current_time); - } + pub fn advance_animations(&mut self) {} pub fn are_animations_ongoing(&self) -> bool { let Self::Open { open_anim, .. } = self else {