From 93cee2994ab9ccf59a09f61d5b8acf6cd937d654 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Sat, 23 Nov 2024 11:27:27 +0300 Subject: Refactor animations to take explicit current time --- src/animation/clock.rs | 41 ++++++++++++ src/animation/mod.rs | 80 +++++++++++++++--------- src/input/mod.rs | 3 +- src/layout/closing_window.rs | 3 +- src/layout/mod.rs | 120 +++++++++++++++++++++++++++++++----- src/layout/monitor.rs | 20 +++++- src/layout/tile.rs | 25 +++++--- src/layout/workspace.rs | 46 ++++++++++---- src/niri.rs | 15 ++++- src/ui/config_error_notification.rs | 14 ++++- src/ui/screenshot_ui.rs | 17 +++-- 11 files changed, 306 insertions(+), 78 deletions(-) create mode 100644 src/animation/clock.rs (limited to 'src') diff --git a/src/animation/clock.rs b/src/animation/clock.rs new file mode 100644 index 00000000..3cfa727b --- /dev/null +++ b/src/animation/clock.rs @@ -0,0 +1,41 @@ +use std::cell::Cell; +use std::rc::Rc; +use std::time::Duration; + +use crate::utils::get_monotonic_time; + +/// Clock that can have its time value overridden. +/// +/// Can be cloned to share the same clock. +#[derive(Debug, Default, Clone)] +pub struct Clock { + time_override: Rc>>, +} + +impl Clock { + /// Creates a new [`Clock`] with time override in place. + pub fn with_override(time: Duration) -> Self { + Self { + time_override: Rc::new(Cell::new(Some(time))), + } + } + + /// Sets the current time override. + pub fn set_time_override(&mut self, time: Option) { + self.time_override.set(time); + } + + /// Gets the current time. + #[inline] + pub fn now(&self) -> Duration { + self.time_override.get().unwrap_or_else(get_monotonic_time) + } +} + +impl PartialEq for Clock { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.time_override, &other.time_override) + } +} + +impl Eq for Clock {} diff --git a/src/animation/mod.rs b/src/animation/mod.rs index 780cb3a8..5efb0f16 100644 --- a/src/animation/mod.rs +++ b/src/animation/mod.rs @@ -4,11 +4,12 @@ use keyframe::functions::{EaseOutCubic, EaseOutQuad}; use keyframe::EasingFunction; use portable_atomic::{AtomicF64, Ordering}; -use crate::utils::get_monotonic_time; - mod spring; pub use spring::{Spring, SpringParams}; +mod clock; +pub use clock::Clock; + pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.); #[derive(Debug, Clone)] @@ -48,11 +49,24 @@ pub enum Curve { } impl Animation { - pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self { + pub fn new( + current_time: Duration, + 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); - let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic); + let mut rv = Self::ease( + current_time, + from, + to, + initial_velocity, + 0, + Curve::EaseOutCubic, + ); if config.off { rv.is_off = true; return rv; @@ -83,10 +97,11 @@ impl Animation { initial_velocity: self.initial_velocity, params, }; - *self = Self::spring(spring); + *self = Self::spring(current_time, spring); } niri_config::AnimationKind::Easing(p) => { *self = Self::ease( + current_time, self.from, self.to, self.initial_velocity, @@ -101,7 +116,13 @@ impl Animation { } /// Restarts the animation using the previous config. - pub fn restarted(&self, from: f64, to: f64, initial_velocity: f64) -> Self { + pub fn restarted( + &self, + current_time: Duration, + from: f64, + to: f64, + initial_velocity: f64, + ) -> Self { if self.is_off { return self.clone(); } @@ -111,6 +132,7 @@ impl Animation { match self.kind { Kind::Easing { curve } => Self::ease( + current_time, from, to, initial_velocity, @@ -124,23 +146,32 @@ impl Animation { initial_velocity: self.initial_velocity, params: spring.params, }; - Self::spring(spring) + Self::spring(current_time, spring) } Kind::Deceleration { initial_velocity, deceleration_rate, } => { let threshold = 0.001; // FIXME - Self::decelerate(from, initial_velocity, deceleration_rate, threshold) + Self::decelerate( + current_time, + from, + initial_velocity, + deceleration_rate, + threshold, + ) } } } - pub fn ease(from: f64, to: f64, initial_velocity: f64, duration_ms: u64, curve: Curve) -> Self { - // FIXME: ideally we shouldn't use current time here because animations started within the - // same frame cycle should have the same start time to be synchronized. - let now = get_monotonic_time(); - + pub fn ease( + current_time: Duration, + from: f64, + to: f64, + initial_velocity: f64, + duration_ms: u64, + curve: Curve, + ) -> Self { let duration = Duration::from_millis(duration_ms); let kind = Kind::Easing { curve }; @@ -152,19 +183,15 @@ impl Animation { duration, // Our current curves never overshoot. clamped_duration: duration, - start_time: now, - current_time: now, + start_time: current_time, + current_time, kind, } } - pub fn spring(spring: Spring) -> Self { + pub fn spring(current_time: Duration, spring: Spring) -> Self { let _span = tracy_client::span!("Animation::spring"); - // FIXME: ideally we shouldn't use current time here because animations started within the - // same frame cycle should have the same start time to be synchronized. - let now = get_monotonic_time(); - let duration = spring.duration(); let clamped_duration = spring.clamped_duration().unwrap_or(duration); let kind = Kind::Spring(spring); @@ -176,22 +203,19 @@ impl Animation { is_off: false, duration, clamped_duration, - start_time: now, - current_time: now, + start_time: current_time, + current_time, kind, } } pub fn decelerate( + current_time: Duration, from: f64, initial_velocity: f64, deceleration_rate: f64, threshold: f64, ) -> Self { - // FIXME: ideally we shouldn't use current time here because animations started within the - // same frame cycle should have the same start time to be synchronized. - let now = get_monotonic_time(); - let duration_s = if initial_velocity == 0. { 0. } else { @@ -214,8 +238,8 @@ impl Animation { is_off: false, duration, clamped_duration: duration, - start_time: now, - current_time: now, + start_time: current_time, + current_time, kind, } } diff --git a/src/input/mod.rs b/src/input/mod.rs index 976f51db..eec8fbe4 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -3024,6 +3024,7 @@ pub fn mods_with_finger_scroll_binds(comp_mod: CompositorMod, binds: &Binds) -> #[cfg(test)] mod tests { use super::*; + use crate::animation::Clock; #[test] fn bindings_suppress_keys() { @@ -3042,7 +3043,7 @@ mod tests { let comp_mod = CompositorMod::Super; let mut suppressed_keys = HashSet::new(); - let screenshot_ui = ScreenshotUi::new(Default::default()); + let screenshot_ui = ScreenshotUi::new(Clock::default(), Default::default()); let disable_power_key_handling = false; // The key_code we pick is arbitrary, the only thing diff --git a/src/layout/closing_window.rs b/src/layout/closing_window.rs index 8bbf0258..744c099e 100644 --- a/src/layout/closing_window.rs +++ b/src/layout/closing_window.rs @@ -142,8 +142,7 @@ impl ClosingWindow { match &mut self.anim_state { AnimationState::Waiting { blocker, anim } => { if blocker.state() != BlockerState::Pending { - let mut anim = anim.restarted(0., 1., 0.); - anim.set_current_time(current_time); + let anim = anim.restarted(current_time, 0., 1., 0.); self.anim_state = AnimationState::Animating(anim); } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index e5cfea67..f4e59af3 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -52,6 +52,7 @@ use workspace::WorkspaceId; pub use self::monitor::MonitorRenderElement; use self::monitor::{Monitor, WorkspaceSwitch}; use self::workspace::{compute_working_area, Column, ColumnWidth, InsertHint, OutputId, Workspace}; +use crate::animation::Clock; use crate::layout::workspace::InsertPosition; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; @@ -216,6 +217,8 @@ pub struct Layout { last_active_workspace_id: HashMap, /// Ongoing interactive move. interactive_move: Option>, + /// Clock for driving animations. + clock: Clock, /// Configurable properties of the layout. options: Rc, } @@ -433,27 +436,30 @@ impl Options { } impl Layout { - pub fn new(config: &Config) -> Self { - Self::with_options_and_workspaces(config, Options::from_config(config)) + pub fn new(clock: Clock, config: &Config) -> Self { + Self::with_options_and_workspaces(clock, config, Options::from_config(config)) } - pub fn with_options(options: Options) -> Self { + pub fn with_options(clock: Clock, options: Options) -> Self { Self { monitor_set: MonitorSet::NoOutputs { workspaces: vec![] }, is_active: true, last_active_workspace_id: HashMap::new(), interactive_move: None, + clock, options: Rc::new(options), } } - fn with_options_and_workspaces(config: &Config, options: Options) -> Self { + fn with_options_and_workspaces(clock: Clock, config: &Config, options: Options) -> Self { let opts = Rc::new(options); let workspaces = config .workspaces .iter() - .map(|ws| Workspace::new_with_config_no_outputs(Some(ws.clone()), opts.clone())) + .map(|ws| { + Workspace::new_with_config_no_outputs(Some(ws.clone()), clock.clone(), opts.clone()) + }) .collect(); Self { @@ -461,6 +467,7 @@ impl Layout { is_active: true, last_active_workspace_id: HashMap::new(), interactive_move: None, + clock, options: opts, } } @@ -522,13 +529,18 @@ impl Layout { } // Make sure there's always an empty workspace. - workspaces.push(Workspace::new(output.clone(), self.options.clone())); + workspaces.push(Workspace::new( + output.clone(), + self.clock.clone(), + self.options.clone(), + )); for ws in &mut workspaces { ws.set_output(Some(output.clone())); } - let mut monitor = Monitor::new(output, workspaces, self.options.clone()); + let mut monitor = + Monitor::new(output, workspaces, self.clock.clone(), self.options.clone()); monitor.active_workspace_idx = active_workspace_idx.unwrap_or(0); monitors.push(monitor); @@ -540,7 +552,11 @@ impl Layout { } MonitorSet::NoOutputs { mut workspaces } => { // We know there are no empty workspaces there, so add one. - workspaces.push(Workspace::new(output.clone(), self.options.clone())); + workspaces.push(Workspace::new( + output.clone(), + self.clock.clone(), + self.options.clone(), + )); let ws_id_to_activate = self.last_active_workspace_id.remove(&output.name()); let mut active_workspace_idx = 0; @@ -553,7 +569,8 @@ impl Layout { } } - let mut monitor = Monitor::new(output, workspaces, self.options.clone()); + let mut monitor = + Monitor::new(output, workspaces, self.clock.clone(), self.options.clone()); monitor.active_workspace_idx = active_workspace_idx; MonitorSet::Normal { @@ -785,7 +802,10 @@ impl Layout { let ws = if let Some(ws) = workspaces.get_mut(0) { ws } else { - workspaces.push(Workspace::new_no_outputs(self.options.clone())); + workspaces.push(Workspace::new_no_outputs( + self.clock.clone(), + self.options.clone(), + )); &mut workspaces[0] }; ws.add_window(None, window, true, width, is_full_width); @@ -1992,6 +2012,8 @@ impl Layout { move_win_id = Some(window_id.clone()); } InteractiveMoveState::Moving(move_) => { + assert_eq!(self.clock, move_.tile.clock); + let scale = move_.output.current_scale().fractional_scale(); let options = Options::clone(&self.options).adjusted_for_scale(scale); assert_eq!( @@ -2026,6 +2048,8 @@ impl Layout { "with no outputs there cannot be empty unnamed workspaces" ); + assert_eq!(self.clock, workspace.clock); + assert_eq!( workspace.base_options, self.options, "workspace base options must be synchronized with layout" @@ -2070,6 +2094,7 @@ impl Layout { ); assert!(monitor.active_workspace_idx < monitor.workspaces.len()); + assert_eq!(self.clock, monitor.clock); assert_eq!( monitor.options, self.options, "monitor options must be synchronized with layout" @@ -2135,6 +2160,8 @@ impl Layout { // exists. for workspace in &monitor.workspaces { + assert_eq!(self.clock, workspace.clock); + assert_eq!( workspace.base_options, self.options, "workspace options must be synchronized with layout" @@ -2326,6 +2353,7 @@ impl Layout { return; } + let clock = self.clock.clone(); let options = self.options.clone(); match &mut self.monitor_set { @@ -2349,6 +2377,7 @@ impl Layout { let ws = Workspace::new_with_config( mon.output.clone(), Some(ws_config.clone()), + clock, options, ); mon.workspaces.insert(0, ws); @@ -2357,7 +2386,8 @@ impl Layout { mon.clean_up_workspaces(); } MonitorSet::NoOutputs { workspaces } => { - let ws = Workspace::new_with_config_no_outputs(Some(ws_config.clone()), options); + let ws = + Workspace::new_with_config_no_outputs(Some(ws_config.clone()), clock, options); workspaces.insert(0, ws); } } @@ -3161,7 +3191,10 @@ impl Layout { let ws = if let Some(ws) = workspaces.get_mut(0) { ws } else { - workspaces.push(Workspace::new_no_outputs(self.options.clone())); + workspaces.push(Workspace::new_no_outputs( + self.clock.clone(), + self.options.clone(), + )); &mut workspaces[0] }; @@ -3620,7 +3653,7 @@ mod tests { impl Default for Layout { fn default() -> Self { - Self::with_options(Default::default()) + Self::with_options(Clock::with_override(Duration::ZERO), Default::default()) } } @@ -3854,6 +3887,16 @@ mod tests { prop_oneof![Just(1.), Just(1.5), Just(2.),] } + fn arbitrary_msec_delta() -> impl Strategy { + prop_oneof![ + 1 => Just(-1000), + 2 => Just(-10), + 1 => Just(0), + 2 => Just(10), + 6 => Just(1000), + ] + } + #[derive(Debug, Clone, Copy, Arbitrary)] enum Op { AddOutput(#[proptest(strategy = "1..=5usize")] usize), @@ -3997,6 +4040,10 @@ mod tests { Refresh { is_active: bool, }, + AdvanceAnimations { + #[proptest(strategy = "arbitrary_msec_delta()")] + msec_delta: i32, + }, MoveWorkspaceToOutput(#[proptest(strategy = "1..=5u8")] u8), ViewOffsetGestureBegin { #[proptest(strategy = "1..=5usize")] @@ -4505,6 +4552,16 @@ mod tests { Op::Refresh { is_active } => { layout.refresh(is_active); } + Op::AdvanceAnimations { msec_delta } => { + let mut now = layout.clock.now(); + 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); + } Op::MoveWorkspaceToOutput(id) => { let name = format!("output{id}"); let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { @@ -4617,7 +4674,7 @@ mod tests { #[track_caller] fn check_ops_with_options(options: Options, ops: &[Op]) { - let mut layout = Layout::with_options(options); + let mut layout = Layout::with_options(Clock::with_override(Duration::ZERO), options); for op in ops { op.apply(&mut layout); @@ -5440,7 +5497,7 @@ mod tests { config.layout.border.off = false; config.layout.border.width = FloatOrInt(2.); - let mut layout = Layout::new(&config); + let mut layout = Layout::new(Clock::default(), &config); Op::AddWindow { id: 1, @@ -5460,7 +5517,7 @@ mod tests { let mut config = Config::default(); config.layout.preset_window_heights = vec![PresetSize::Fixed(1), PresetSize::Fixed(2)]; - let mut layout = Layout::new(&config); + let mut layout = Layout::new(Clock::default(), &config); let ops = [ Op::AddOutput(1), @@ -5752,6 +5809,37 @@ mod tests { check_ops(&ops); } + #[test] + fn interactive_move_onto_last_workspace() { + let ops = [ + Op::AddOutput(1), + Op::AddWindow { + id: 0, + bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), + min_max_size: Default::default(), + }, + Op::InteractiveMoveBegin { + window: 0, + output_idx: 1, + px: 0., + py: 0., + }, + Op::InteractiveMoveUpdate { + window: 0, + dx: 1000., + dy: 0., + output_idx: 1, + px: 0., + py: 0., + }, + Op::FocusWorkspaceDown, + Op::AdvanceAnimations { msec_delta: 1000 }, + Op::InteractiveMoveEnd { window: 0 }, + ]; + + check_ops(&ops); + } + #[test] fn output_active_workspace_is_preserved() { let ops = [ diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index aecad889..aad921bb 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -15,7 +15,7 @@ use super::workspace::{ WorkspaceRenderElement, }; use super::{LayoutElement, Options}; -use crate::animation::Animation; +use crate::animation::{Animation, Clock}; use crate::input::swipe_tracker::SwipeTracker; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::RenderTarget; @@ -45,6 +45,8 @@ pub struct Monitor { pub(super) previous_workspace_id: Option, /// In-progress switch between workspaces. pub(super) workspace_switch: Option, + /// Clock for driving animations. + pub(super) clock: Clock, /// Configurable properties of the layout. pub(super) options: Rc, } @@ -94,7 +96,12 @@ impl WorkspaceSwitch { } impl Monitor { - pub fn new(output: Output, workspaces: Vec>, options: Rc) -> Self { + pub fn new( + output: Output, + workspaces: Vec>, + clock: Clock, + options: Rc, + ) -> Self { Self { output_name: output.name(), output, @@ -102,6 +109,7 @@ impl Monitor { active_workspace_idx: 0, previous_workspace_id: None, workspace_switch: None, + clock, options, } } @@ -151,7 +159,11 @@ impl Monitor { } pub fn add_workspace_bottom(&mut self) { - let ws = Workspace::new(self.output.clone(), self.options.clone()); + let ws = Workspace::new( + self.output.clone(), + self.clock.clone(), + self.options.clone(), + ); self.workspaces.push(ws); } @@ -172,6 +184,7 @@ impl Monitor { self.active_workspace_idx = idx; self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( + self.clock.now(), current_idx, idx as f64, 0., @@ -1099,6 +1112,7 @@ impl Monitor { self.active_workspace_idx = new_idx; self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( + self.clock.now(), gesture.current_idx, new_idx as f64, velocity, diff --git a/src/layout/tile.rs b/src/layout/tile.rs index ddb9bd8c..dd3d50af 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -13,7 +13,7 @@ use super::{ LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options, RESIZE_ANIMATION_THRESHOLD, }; -use crate::animation::Animation; +use crate::animation::{Animation, Clock}; use crate::niri_render_elements; use crate::render_helpers::border::BorderRenderElement; use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage}; @@ -76,6 +76,9 @@ pub struct Tile { /// Scale of the output the tile is on (and rounds its sizes to). scale: f64, + /// Clock for driving animations. + pub(super) clock: Clock, + /// Configurable properties of the layout. pub(super) options: Rc, } @@ -110,7 +113,7 @@ struct MoveAnimation { } impl Tile { - pub fn new(window: W, scale: f64, options: Rc) -> Self { + pub fn new(window: W, scale: f64, clock: Clock, options: Rc) -> Self { let rules = window.rules(); let border_config = rules.border.resolve_against(options.border); let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into()); @@ -130,6 +133,7 @@ impl Tile { unmap_snapshot: None, rounded_corner_damage: Default::default(), scale, + clock, options, } } @@ -180,7 +184,13 @@ impl Tile { let change = self.window.size().to_f64().to_point() - size_from.to_point(); let change = f64::max(change.x.abs(), change.y.abs()); if change > RESIZE_ANIMATION_THRESHOLD { - let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.anim); + let anim = Animation::new( + self.clock.now(), + 0., + 1., + 0., + self.options.animations.window_resize.anim, + ); self.resize_animation = Some(ResizeAnimation { anim, size_from, @@ -316,6 +326,7 @@ impl Tile { pub fn start_open_animation(&mut self) { self.open_animation = Some(OpenAnimation::new(Animation::new( + self.clock.now(), 0., 1., 0., @@ -342,8 +353,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(1., 0., 0.)) - .unwrap_or_else(|| Animation::new(1., 0., 0., config)); + .map(|anim| anim.restarted(self.clock.now(), 1., 0., 0.)) + .unwrap_or_else(|| Animation::new(self.clock.now(), 1., 0., 0., config)); self.move_x_animation = Some(MoveAnimation { anim, @@ -361,8 +372,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(1., 0., 0.)) - .unwrap_or_else(|| Animation::new(1., 0., 0., config)); + .map(|anim| anim.restarted(self.clock.now(), 1., 0., 0.)) + .unwrap_or_else(|| Animation::new(self.clock.now(), 1., 0., 0., config)); self.move_y_animation = Some(MoveAnimation { anim, diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index b26dfcc1..6432ca8b 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -19,7 +19,7 @@ use super::closing_window::{ClosingWindow, ClosingWindowRenderElement}; use super::insert_hint_element::{InsertHintElement, InsertHintRenderElement}; use super::tile::{Tile, TileRenderElement, TileRenderSnapshot}; use super::{ConfigureIntent, InteractiveResizeData, LayoutElement, Options, RemovedTile}; -use crate::animation::Animation; +use crate::animation::{Animation, Clock}; use crate::input::swipe_tracker::SwipeTracker; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; @@ -113,6 +113,9 @@ pub struct Workspace { /// Insert hint element for rendering. insert_hint_element: InsertHintElement, + /// Clock for driving animations. + pub(super) clock: Clock, + /// Configurable properties of the layout as received from the parent monitor. pub(super) base_options: Rc, @@ -293,6 +296,9 @@ pub struct Column { /// Scale of the output the column is on (and rounds its sizes to). scale: f64, + /// Clock for driving animations. + clock: Clock, + /// Configurable properties of the layout. options: Rc, } @@ -403,13 +409,14 @@ impl TileData { } impl Workspace { - pub fn new(output: Output, options: Rc) -> Self { - Self::new_with_config(output, None, options) + pub fn new(output: Output, clock: Clock, options: Rc) -> Self { + Self::new_with_config(output, None, clock, options) } pub fn new_with_config( output: Output, config: Option, + clock: Clock, base_options: Rc, ) -> Self { let original_output = config @@ -442,6 +449,7 @@ impl Workspace { closing_windows: vec![], insert_hint: None, insert_hint_element: InsertHintElement::new(options.insert_hint), + clock, base_options, options, name: config.map(|c| c.name.0), @@ -451,6 +459,7 @@ impl Workspace { pub fn new_with_config_no_outputs( config: Option, + clock: Clock, base_options: Rc, ) -> Self { let original_output = OutputId( @@ -482,6 +491,7 @@ impl Workspace { closing_windows: vec![], insert_hint: None, insert_hint_element: InsertHintElement::new(options.insert_hint), + clock, base_options, options, name: config.map(|c| c.name.0), @@ -489,8 +499,8 @@ impl Workspace { } } - pub fn new_no_outputs(options: Rc) -> Self { - Self::new_with_config_no_outputs(None, options) + pub fn new_no_outputs(clock: Clock, options: Rc) -> Self { + Self::new_with_config_no_outputs(None, clock, options) } pub fn id(&self) -> WorkspaceId { @@ -941,6 +951,7 @@ impl Workspace { // FIXME: also compute and use current velocity. self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( + self.clock.now(), self.view_offset, new_view_offset, 0., @@ -1100,7 +1111,12 @@ impl Workspace { width: ColumnWidth, is_full_width: bool, ) { - let tile = Tile::new(window, self.scale.fractional_scale(), self.options.clone()); + let tile = Tile::new( + window, + self.scale.fractional_scale(), + self.clock.clone(), + self.options.clone(), + ); self.add_tile(col_idx, tile, activate, width, is_full_width, None); } @@ -1118,7 +1134,6 @@ impl Workspace { self.view_size, self.working_area, self.scale.fractional_scale(), - self.options.clone(), width, is_full_width, true, @@ -1682,7 +1697,13 @@ impl Workspace { ) { let output_scale = Scale::from(self.scale.fractional_scale()); - let anim = Animation::new(0., 1., 0., self.options.animations.window_close.anim); + let anim = Animation::new( + self.clock.now(), + 0., + 1., + 0., + self.options.animations.window_close.anim, + ); let blocker = if self.options.disable_transactions { TransactionBlocker::completed() @@ -1725,6 +1746,7 @@ impl Workspace { for (column, data) in zip(&self.columns, &self.data) { assert!(Rc::ptr_eq(&self.options, &column.options)); + assert_eq!(self.clock, column.clock); assert_eq!(self.scale.fractional_scale(), column.scale); column.verify_invariants(); @@ -2619,7 +2641,6 @@ impl Workspace { self.view_size, self.working_area, self.scale.fractional_scale(), - self.options.clone(), removed.width, removed.is_full_width, false, @@ -2969,6 +2990,7 @@ 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(), current_view_offset + delta, target_view_offset, velocity, @@ -3174,7 +3196,6 @@ impl Column { view_size: Size, working_area: Rectangle, scale: f64, - options: Rc, width: ColumnWidth, is_full_width: bool, animate_resize: bool, @@ -3190,7 +3211,8 @@ impl Column { view_size, working_area, scale, - options, + clock: tile.clock.clone(), + options: tile.options.clone(), }; let is_pending_fullscreen = tile.window().is_pending_fullscreen(); @@ -3313,6 +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(), from_x_offset + current_offset, 0., 0., @@ -3716,6 +3739,7 @@ impl Column { let mut total_min_height = 0.; for (tile, data) in zip(&self.tiles, &self.data) { assert!(Rc::ptr_eq(&self.options, &tile.options)); + assert_eq!(self.clock, tile.clock); assert_eq!(self.scale, tile.scale()); assert_eq!(self.is_fullscreen, tile.window().is_pending_fullscreen()); diff --git a/src/niri.rs b/src/niri.rs index 7a579761..57050d6d 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -100,6 +100,7 @@ use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState; use smithay::wayland::xdg_activation::XdgActivationState; use smithay::wayland::xdg_foreign::XdgForeignState; +use crate::animation::Clock; use crate::backend::tty::SurfaceDmabufFeedback; use crate::backend::{Backend, RenderResult, Tty, Winit}; use crate::cursor::{CursorManager, CursorTextureCache, RenderCursor, XCursor}; @@ -179,6 +180,9 @@ pub struct Niri { /// Whether the at-startup=true window rules are active. pub is_at_startup: bool, + /// Clock for driving animations. + pub clock: Clock, + // Each workspace corresponds to a Space. Each workspace generally has one Output mapped to it, // however it may have none (when there are no outputs connected) or multiple (when mirroring). pub layout: Layout, @@ -1671,7 +1675,8 @@ impl Niri { let config_ = config.borrow(); let config_file_output_config = config_.outputs.clone(); - let layout = Layout::new(&config_); + let clock = Clock::default(); + let layout = Layout::new(clock.clone(), &config_); let (blocker_cleared_tx, blocker_cleared_rx) = mpsc::channel(); @@ -1799,8 +1804,8 @@ impl Niri { let mods_with_finger_scroll_binds = mods_with_finger_scroll_binds(backend.mod_key(), &config_.binds); - let screenshot_ui = ScreenshotUi::new(config.clone()); - let config_error_notification = ConfigErrorNotification::new(config.clone()); + let screenshot_ui = ScreenshotUi::new(clock.clone(), config.clone()); + let config_error_notification = ConfigErrorNotification::new(clock.clone(), config.clone()); let mut hotkey_overlay = HotkeyOverlay::new(config.clone(), backend.mod_key()); if !config_.hotkey_overlay.skip_at_startup { @@ -1895,6 +1900,7 @@ impl Niri { display_handle, start_time: Instant::now(), is_at_startup: true, + clock, layout, global_space: Space::default(), @@ -3044,6 +3050,7 @@ impl Niri { 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; @@ -4819,6 +4826,8 @@ impl Niri { let delay = delay_ms.map_or(screen_transition::DELAY, |d| { 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(); diff --git a/src/ui/config_error_notification.rs b/src/ui/config_error_notification.rs index c4ca3b69..af4f67e3 100644 --- a/src/ui/config_error_notification.rs +++ b/src/ui/config_error_notification.rs @@ -14,7 +14,7 @@ use smithay::output::Output; use smithay::reexports::gbm::Format as Fourcc; use smithay::utils::{Point, Transform}; -use crate::animation::Animation; +use crate::animation::{Animation, Clock}; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement}; @@ -35,6 +35,7 @@ pub struct ConfigErrorNotification { // notification. created_path: Option, + clock: Clock, config: Rc>, } @@ -46,18 +47,25 @@ enum State { } impl ConfigErrorNotification { - pub fn new(config: Rc>) -> Self { + pub fn new(clock: Clock, config: Rc>) -> Self { Self { state: State::Hidden, buffers: RefCell::new(HashMap::new()), created_path: None, + clock, config, } } fn animation(&self, from: f64, to: f64) -> Animation { let c = self.config.borrow(); - Animation::new(from, to, 0., c.animations.config_notification_open_close.0) + Animation::new( + self.clock.now(), + from, + to, + 0., + c.animations.config_notification_open_close.0, + ) } pub fn show_created(&mut self, created_path: PathBuf) { diff --git a/src/ui/screenshot_ui.rs b/src/ui/screenshot_ui.rs index 0e2006a1..cde9bdac 100644 --- a/src/ui/screenshot_ui.rs +++ b/src/ui/screenshot_ui.rs @@ -20,7 +20,7 @@ use smithay::input::keyboard::{Keysym, ModifiersState}; use smithay::output::{Output, WeakOutput}; use smithay::utils::{Physical, Point, Rectangle, Scale, Size, Transform}; -use crate::animation::Animation; +use crate::animation::{Animation, Clock}; use crate::niri_render_elements; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; @@ -49,6 +49,7 @@ const TEXT_SHOW_P: &str = pub enum ScreenshotUi { Closed { last_selection: Option<(WeakOutput, Rectangle)>, + clock: Clock, config: Rc>, }, Open { @@ -57,6 +58,7 @@ pub enum ScreenshotUi { mouse_down: bool, show_pointer: bool, open_anim: Animation, + clock: Clock, config: Rc>, }, } @@ -86,9 +88,10 @@ niri_render_elements! { } impl ScreenshotUi { - pub fn new(config: Rc>) -> Self { + pub fn new(clock: Clock, config: Rc>) -> Self { Self::Closed { last_selection: None, + clock, config, } } @@ -106,6 +109,7 @@ impl ScreenshotUi { let Self::Closed { last_selection, + clock, config, } = self else { @@ -181,7 +185,7 @@ impl ScreenshotUi { let open_anim = { let c = config.borrow(); - Animation::new(0., 1., 0., c.animations.screenshot_ui_open.0) + Animation::new(clock.now(), 0., 1., 0., c.animations.screenshot_ui_open.0) }; *self = Self::Open { @@ -190,6 +194,7 @@ impl ScreenshotUi { mouse_down: false, show_pointer: true, open_anim, + clock: clock.clone(), config: config.clone(), }; @@ -200,7 +205,10 @@ impl ScreenshotUi { pub fn close(&mut self) -> bool { let Self::Open { - selection, config, .. + selection, + clock, + config, + .. } = self else { return false; @@ -213,6 +221,7 @@ impl ScreenshotUi { *self = Self::Closed { last_selection, + clock: clock.clone(), config: config.clone(), }; -- cgit