diff options
| -rw-r--r-- | niri-config/src/lib.rs | 331 | ||||
| -rw-r--r-- | resources/default-config.kdl | 42 | ||||
| -rw-r--r-- | src/animation/mod.rs | 151 | ||||
| -rw-r--r-- | src/animation/spring.rs | 137 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 4 | ||||
| -rw-r--r-- | src/layout/tile.rs | 1 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 4 | ||||
| -rw-r--r-- | src/ui/config_error_notification.rs | 5 |
8 files changed, 635 insertions, 40 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 07fc244d..6d104827 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -527,50 +527,95 @@ impl Default for Animations { } } -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Animation { - #[knuffel(child)] pub off: bool, - #[knuffel(child, unwrap(argument))] - pub duration_ms: Option<u32>, - #[knuffel(child, unwrap(argument))] - pub curve: Option<AnimationCurve>, + pub kind: AnimationKind, } impl Animation { pub const fn unfilled() -> Self { Self { off: false, - duration_ms: None, - curve: None, + kind: AnimationKind::Easing(EasingParams::unfilled()), } } pub const fn default() -> Self { Self { off: false, - duration_ms: Some(250), - curve: Some(AnimationCurve::EaseOutCubic), + kind: AnimationKind::Easing(EasingParams::default()), } } pub const fn default_workspace_switch() -> Self { - Self::default() + Self { + off: false, + kind: AnimationKind::Spring(SpringParams { + damping_ratio: 1., + stiffness: 1000, + epsilon: 0.0001, + }), + } } pub const fn default_horizontal_view_movement() -> Self { - Self::default() + Self { + off: false, + kind: AnimationKind::Spring(SpringParams { + damping_ratio: 1., + stiffness: 800, + epsilon: 0.0001, + }), + } } pub const fn default_config_notification_open_close() -> Self { - Self::default() + Self { + off: false, + kind: AnimationKind::Spring(SpringParams { + damping_ratio: 0.6, + stiffness: 1000, + epsilon: 0.001, + }), + } } pub const fn default_window_open() -> Self { Self { - duration_ms: Some(150), - curve: Some(AnimationCurve::EaseOutExpo), - ..Self::default() + off: false, + kind: AnimationKind::Easing(EasingParams { + duration_ms: Some(150), + curve: Some(AnimationCurve::EaseOutExpo), + }), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum AnimationKind { + Easing(EasingParams), + Spring(SpringParams), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct EasingParams { + pub duration_ms: Option<u32>, + pub curve: Option<AnimationCurve>, +} + +impl EasingParams { + pub const fn unfilled() -> Self { + Self { + duration_ms: None, + curve: None, + } + } + + pub const fn default() -> Self { + Self { + duration_ms: Some(250), + curve: Some(AnimationCurve::EaseOutCubic), } } } @@ -581,6 +626,13 @@ pub enum AnimationCurve { EaseOutExpo, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct SpringParams { + pub damping_ratio: f64, + pub stiffness: u32, + pub epsilon: f64, +} + #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)] pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>); @@ -1012,6 +1064,229 @@ where } } +fn parse_arg_node<S: knuffel::traits::ErrorSpan, T: knuffel::traits::DecodeScalar<S>>( + name: &str, + node: &knuffel::ast::SpannedNode<S>, + ctx: &mut knuffel::decode::Context<S>, +) -> Result<T, DecodeError<S>> { + let mut iter_args = node.arguments.iter(); + let val = iter_args.next().ok_or_else(|| { + DecodeError::missing(node, format!("additional argument `{name}` is required")) + })?; + + let value = knuffel::traits::DecodeScalar::decode(val, ctx)?; + + if let Some(val) = iter_args.next() { + ctx.emit_error(DecodeError::unexpected( + &val.literal, + "argument", + "unexpected argument", + )); + } + for name in node.properties.keys() { + ctx.emit_error(DecodeError::unexpected( + name, + "property", + format!("unexpected property `{}`", name.escape_default()), + )); + } + for child in node.children() { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("unexpected node `{}`", child.node_name.escape_default()), + )); + } + + Ok(value) +} + +impl<S> knuffel::Decode<S> for Animation +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode<S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<Self, DecodeError<S>> { + expect_only_children(node, ctx); + + let mut off = false; + let mut easing_params = EasingParams::unfilled(); + let mut spring_params = None; + + for child in node.children() { + match &**child.node_name { + "off" => { + knuffel::decode::check_flag_node(child, ctx); + if off { + ctx.emit_error(DecodeError::unexpected( + &child.node_name, + "node", + "duplicate node `off`, single node expected", + )); + } else { + off = true; + } + } + "spring" => { + if easing_params != EasingParams::unfilled() { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + "cannot set both spring and easing parameters at once", + )); + } + if spring_params.is_some() { + ctx.emit_error(DecodeError::unexpected( + &child.node_name, + "node", + "duplicate node `spring`, single node expected", + )); + } + + spring_params = Some(SpringParams::decode_node(child, ctx)?); + } + "duration-ms" => { + if spring_params.is_some() { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + "cannot set both spring and easing parameters at once", + )); + } + if easing_params.duration_ms.is_some() { + ctx.emit_error(DecodeError::unexpected( + &child.node_name, + "node", + "duplicate node `duration-ms`, single node expected", + )); + } + + easing_params.duration_ms = Some(parse_arg_node("duration-ms", child, ctx)?); + } + "curve" => { + if spring_params.is_some() { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + "cannot set both spring and easing parameters at once", + )); + } + if easing_params.curve.is_some() { + ctx.emit_error(DecodeError::unexpected( + &child.node_name, + "node", + "duplicate node `curve`, single node expected", + )); + } + + easing_params.curve = Some(parse_arg_node("curve", child, ctx)?); + } + name_str => { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("unexpected node `{}`", name_str.escape_default()), + )); + } + } + } + + let kind = if let Some(spring_params) = spring_params { + AnimationKind::Spring(spring_params) + } else { + AnimationKind::Easing(easing_params) + }; + + Ok(Self { off, kind }) + } +} + +impl<S> knuffel::Decode<S> for SpringParams +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode<S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<Self, DecodeError<S>> { + if let Some(type_name) = &node.type_name { + ctx.emit_error(DecodeError::unexpected( + type_name, + "type name", + "no type name expected for this node", + )); + } + if let Some(val) = node.arguments.first() { + ctx.emit_error(DecodeError::unexpected( + &val.literal, + "argument", + "unexpected argument", + )); + } + for child in node.children() { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("unexpected node `{}`", child.node_name.escape_default()), + )); + } + + let mut damping_ratio = None; + let mut stiffness = None; + let mut epsilon = None; + for (name, val) in &node.properties { + match &***name { + "damping-ratio" => { + damping_ratio = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?); + } + "stiffness" => { + stiffness = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?); + } + "epsilon" => { + epsilon = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?); + } + name_str => { + ctx.emit_error(DecodeError::unexpected( + name, + "property", + format!("unexpected property `{}`", name_str.escape_default()), + )); + } + } + } + let damping_ratio = damping_ratio + .ok_or_else(|| DecodeError::missing(node, "property `damping-ratio` is required"))?; + let stiffness = stiffness + .ok_or_else(|| DecodeError::missing(node, "property `stiffness` is required"))?; + let epsilon = + epsilon.ok_or_else(|| DecodeError::missing(node, "property `epsilon` is required"))?; + + if !(0.1..=10.).contains(&damping_ratio) { + ctx.emit_error(DecodeError::conversion( + node, + "damping-ratio must be between 0.1 and 10.0", + )); + } + if stiffness < 1 { + ctx.emit_error(DecodeError::conversion(node, "stiffness must be >= 1")); + } + if !(0.00001..=0.1).contains(&epsilon) { + ctx.emit_error(DecodeError::conversion( + node, + "epsilon must be between 0.00001 and 0.1", + )); + } + + Ok(SpringParams { + damping_ratio, + stiffness, + epsilon, + }) + } +} + impl<S> knuffel::Decode<S> for Binds where S: knuffel::traits::ErrorSpan, @@ -1345,12 +1620,16 @@ mod tests { animations { slowdown 2.0 - workspace-switch { off; } + workspace-switch { + spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 + } horizontal-view-movement { duration-ms 100 curve "ease-out-expo" } + + window-open { off; } } environment { @@ -1507,12 +1786,22 @@ mod tests { animations: Animations { slowdown: 2., workspace_switch: Animation { - off: true, - ..Animation::unfilled() + off: false, + kind: AnimationKind::Spring(SpringParams { + damping_ratio: 1., + stiffness: 1000, + epsilon: 0.0001, + }), }, horizontal_view_movement: Animation { - duration_ms: Some(100), - curve: Some(AnimationCurve::EaseOutExpo), + off: false, + kind: AnimationKind::Easing(EasingParams { + duration_ms: Some(100), + curve: Some(AnimationCurve::EaseOutExpo), + }), + }, + window_open: Animation { + off: true, ..Animation::unfilled() }, ..Default::default() diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 616cd48f..bacf7983 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -254,18 +254,48 @@ animations { // slowdown 3.0 // You can configure all individual animations. - // Available settings are the same for all of them: + // Available settings are the same for all of them. // - off disables the animation. + // + // Niri supports two animation types: easing and spring. + // + // Easing has the following settings: // - duration-ms sets the duration of the animation in milliseconds. // - curve sets the easing curve. Currently, available curves // are "ease-out-cubic" and "ease-out-expo". + // + // Spring animations work better with touchpad gestures, because they + // take into account the velocity of your fingers as you release the swipe. + // The parameters are less obvious and generally should be tuned + // with trial and error. Notably, you cannot directly set the duration. + // You can use this app to help visualize how the spring parameters + // change the animation: https://flathub.org/apps/app.drey.Elastic + // + // A spring animation is configured like this: + // - spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 + // + // The damping ratio goes from 0.1 to 10.0 and has the following properties: + // - below 1.0: underdamped spring, will oscillate in the end. + // - above 1.0: overdamped spring, won't oscillate. + // - 1.0: critically damped spring, comes to rest in minimum possible time + // without oscillations. + // + // However, even with damping ratio = 1.0 the spring animation may oscillate + // if "launched" with enough velocity from a touchpad swipe. + // + // Lower stiffness will result in a slower animation more prone to oscillation. + // + // Set epsilon to a lower value if the animation "jumps" in the end. + // + // The spring mass is hardcoded to 1.0 and cannot be changed. Instead, change + // stiffness proportionally. E.g. increasing mass by 2x is the same as + // decreasing stiffness by 2x. // Animation when switching workspaces up and down, // including after the touchpad gesture. workspace-switch { // off - // duration-ms 250 - // curve "ease-out-cubic" + // spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 } // All horizontal camera view movement: @@ -275,8 +305,7 @@ animations { // - And so on. horizontal-view-movement { // off - // duration-ms 250 - // curve "ease-out-cubic" + // spring damping-ratio=1.0 stiffness=800 epsilon=0.0001 } // Window opening animation. Note that this one has different defaults. @@ -290,8 +319,7 @@ animations { // open/close animation. config-notification-open-close { // off - // duration-ms 250 - // curve "ease-out-cubic" + // spring damping-ratio=0.6 stiffness=1000 epsilon=0.001 } } diff --git a/src/animation/mod.rs b/src/animation/mod.rs index 1b928435..7984b0d9 100644 --- a/src/animation/mod.rs +++ b/src/animation/mod.rs @@ -6,6 +6,9 @@ use portable_atomic::{AtomicF64, Ordering}; use crate::utils::get_monotonic_time; +mod spring; +pub use spring::{Spring, SpringParams}; + pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.); #[derive(Debug)] @@ -15,7 +18,19 @@ pub struct Animation { duration: Duration, start_time: Duration, current_time: Duration, - curve: Curve, + kind: Kind, +} + +#[derive(Debug, Clone, Copy)] +enum Kind { + Easing { + curve: Curve, + }, + Spring(Spring), + Deceleration { + initial_velocity: f64, + deceleration_rate: f64, + }, } #[derive(Debug, Clone, Copy)] @@ -28,21 +43,116 @@ impl Animation { pub fn new( from: f64, to: f64, + initial_velocity: f64, config: niri_config::Animation, default: niri_config::Animation, ) -> Self { + if config.off { + return Self::ease(from, to, 0, Curve::EaseOutCubic); + } + + // Resolve defaults. + let (kind, easing_defaults) = match (config.kind, default.kind) { + // Configured spring. + (configured @ niri_config::AnimationKind::Spring(_), _) => (configured, None), + // Configured nothing, defaults spring. + ( + niri_config::AnimationKind::Easing(easing), + defaults @ niri_config::AnimationKind::Spring(_), + ) if easing == niri_config::EasingParams::unfilled() => (defaults, None), + // Configured easing or nothing, defaults easing. + ( + configured @ niri_config::AnimationKind::Easing(_), + niri_config::AnimationKind::Easing(defaults), + ) => (configured, Some(defaults)), + // Configured easing, defaults spring. + ( + configured @ niri_config::AnimationKind::Easing(_), + niri_config::AnimationKind::Spring(_), + ) => (configured, None), + }; + + match kind { + niri_config::AnimationKind::Spring(p) => { + let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon); + + let spring = Spring { + from, + to, + initial_velocity, + params, + }; + Self::spring(spring) + } + niri_config::AnimationKind::Easing(p) => { + let defaults = easing_defaults.unwrap_or(niri_config::EasingParams::default()); + let duration_ms = p.duration_ms.or(defaults.duration_ms).unwrap(); + let curve = Curve::from(p.curve.or(defaults.curve).unwrap()); + Self::ease(from, to, u64::from(duration_ms), curve) + } + } + } + + pub fn ease(from: f64, to: 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(); + + let duration = Duration::from_millis(duration_ms); + let kind = Kind::Easing { curve }; + + Self { + from, + to, + duration, + start_time: now, + current_time: now, + kind, + } + } + + pub fn spring(spring: Spring) -> 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 = spring.duration(); + let kind = Kind::Spring(spring); + + Self { + from: spring.from, + to: spring.to, + duration, + start_time: now, + current_time: now, + kind, + } + } + + pub fn decelerate( + 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_ms = if config.off { - 0 + let duration_s = if initial_velocity == 0. { + 0. } else { - config.duration_ms.unwrap_or(default.duration_ms.unwrap()) + let coeff = 1000. * deceleration_rate.ln(); + (-coeff * threshold / initial_velocity.abs()).ln() / coeff }; - let duration = Duration::from_millis(u64::from(duration_ms)); + let duration = Duration::from_secs_f64(duration_s); + + let to = from - initial_velocity / (1000. * deceleration_rate.ln()); - let curve = Curve::from(config.curve.unwrap_or(default.curve.unwrap())); + let kind = Kind::Deceleration { + initial_velocity, + deceleration_rate, + }; Self { from, @@ -50,7 +160,7 @@ impl Animation { duration, start_time: now, current_time: now, - curve, + kind, } } @@ -118,10 +228,29 @@ impl Animation { } pub fn value(&self) -> f64 { - let passed = (self.current_time - self.start_time).as_secs_f64(); - let total = self.duration.as_secs_f64(); - let x = (passed / total).clamp(0., 1.); - self.curve.y(x) * (self.to - self.from) + self.from + if self.is_done() { + return self.to; + } + + let passed = self.current_time - self.start_time; + + match self.kind { + Kind::Easing { curve } => { + let passed = passed.as_secs_f64(); + let total = self.duration.as_secs_f64(); + let x = (passed / total).clamp(0., 1.); + curve.y(x) * (self.to - self.from) + self.from + } + Kind::Spring(spring) => spring.value_at(passed), + Kind::Deceleration { + initial_velocity, + deceleration_rate, + } => { + let passed = passed.as_secs_f64(); + let coeff = 1000. * deceleration_rate.ln(); + self.from + (deceleration_rate.powf(1000. * passed) - 1.) / coeff * initial_velocity + } + } } pub fn to(&self) -> f64 { diff --git a/src/animation/spring.rs b/src/animation/spring.rs new file mode 100644 index 00000000..6e100f05 --- /dev/null +++ b/src/animation/spring.rs @@ -0,0 +1,137 @@ +use std::time::Duration; + +#[derive(Debug, Clone, Copy)] +pub struct SpringParams { + pub damping: f64, + pub mass: f64, + pub stiffness: f64, + pub epsilon: f64, +} + +#[derive(Debug, Clone, Copy)] +pub struct Spring { + pub from: f64, + pub to: f64, + pub initial_velocity: f64, + pub params: SpringParams, +} + +impl SpringParams { + pub fn new(damping_ratio: f64, stiffness: f64, epsilon: f64) -> Self { + let damping_ratio = damping_ratio.max(0.); + let stiffness = stiffness.max(0.); + let epsilon = epsilon.max(0.); + + let mass = 1.; + let critical_damping = 2. * (mass * stiffness).sqrt(); + let damping = damping_ratio * critical_damping; + + Self { + damping, + mass, + stiffness, + epsilon, + } + } +} + +impl Spring { + pub fn value_at(&self, t: Duration) -> f64 { + self.oscillate(t.as_secs_f64()) + } + + // Based on libadwaita (LGPL-2.1-or-later): + // https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.4.4/src/adw-spring-animation.c, + // which itself is based on (MIT): + // https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBSpringAnimation.m + /// Computes and returns the duration until the spring is at rest. + pub fn duration(&self) -> Duration { + const DELTA: f64 = 0.001; + + let beta = self.params.damping / (2. * self.params.mass); + + if beta.abs() <= f64::EPSILON || beta < 0. { + return Duration::MAX; + } + + let omega0 = (self.params.stiffness / self.params.mass).sqrt(); + + // As first ansatz for the overdamped solution, + // and general estimation for the oscillating ones + // we take the value of the envelope when it's < epsilon. + let mut x0 = -self.params.epsilon.ln() / beta; + + // f64::EPSILON is too small for this specific comparison, so we use + // f32::EPSILON even though it's doubles. + if (beta - omega0).abs() <= f64::from(f32::EPSILON) || beta < omega0 { + return Duration::from_secs_f64(x0); + } + + // Since the overdamped solution decays way slower than the envelope + // we need to use the value of the oscillation itself. + // Newton's root finding method is a good candidate in this particular case: + // https://en.wikipedia.org/wiki/Newton%27s_method + let mut y0 = self.oscillate(x0); + let m = (self.oscillate(x0 + DELTA) - y0) / DELTA; + + let mut x1 = (self.to - y0 + m * x0) / m; + let mut y1 = self.oscillate(x1); + + let mut i = 0; + while (self.to - y1).abs() > self.params.epsilon { + if i > 1000 { + return Duration::ZERO; + } + + x0 = x1; + y0 = y1; + + let m = (self.oscillate(x0 + DELTA) - y0) / DELTA; + + x1 = (self.to - y0 + m * x0) / m; + y1 = self.oscillate(x1); + i += 1; + } + + Duration::from_secs_f64(x1) + } + + /// Returns the spring position at a given time in seconds. + fn oscillate(&self, t: f64) -> f64 { + let b = self.params.damping; + let m = self.params.mass; + let k = self.params.stiffness; + let v0 = self.initial_velocity; + + let beta = b / (2. * m); + let omega0 = (k / m).sqrt(); + + let x0 = self.from - self.to; + + let envelope = (-beta * t).exp(); + + // Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x) + // for the differential equation m*ẍ+b*ẋ+kx = 0 + + // f64::EPSILON is too small for this specific comparison, so we use + // f32::EPSILON even though it's doubles. + if (beta - omega0).abs() <= f64::from(f32::EPSILON) { + // Critically damped. + self.to + envelope * (x0 + (beta * x0 + v0) * t) + } else if beta < omega0 { + // Underdamped. + let omega1 = ((omega0 * omega0) - (beta * beta)).sqrt(); + + self.to + + envelope + * (x0 * (omega1 * t).cos() + ((beta * x0 + v0) / omega1) * (omega1 * t).sin()) + } else { + // Overdamped. + let omega2 = ((beta * beta) - (omega0 * omega0)).sqrt(); + + self.to + + envelope + * (x0 * (omega2 * t).cosh() + ((beta * x0 + v0) / omega2) * (omega2 * t).sinh()) + } + } +} diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index ac9f3ff9..f2e4c30e 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -94,6 +94,7 @@ impl<W: LayoutElement> Monitor<W> { return; } + // FIXME: also compute and use current velocity. let current_idx = self .workspace_switch .as_ref() @@ -105,6 +106,7 @@ impl<W: LayoutElement> Monitor<W> { self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( current_idx, idx as f64, + 0., self.options.animations.workspace_switch, niri_config::Animation::default_workspace_switch(), ))); @@ -781,6 +783,7 @@ impl<W: LayoutElement> Monitor<W> { return true; } + let velocity = gesture.tracker.velocity() / WORKSPACE_GESTURE_MOVEMENT; let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT; let min = gesture.center_idx.saturating_sub(1) as f64; @@ -792,6 +795,7 @@ impl<W: LayoutElement> Monitor<W> { self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( gesture.current_idx, new_idx as f64, + velocity, self.options.animations.workspace_switch, niri_config::Animation::default_workspace_switch(), ))); diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 816c92e1..505a6ccd 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -111,6 +111,7 @@ impl<W: LayoutElement> Tile<W> { self.open_animation = Some(Animation::new( 0., 1., + 0., self.options.animations.window_open, niri_config::Animation::default_window_open(), )); diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 5475862c..ac31cdfb 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -436,9 +436,11 @@ impl<W: LayoutElement> Workspace<W> { return; } + // FIXME: also compute and use current velocity. self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( self.view_offset as f64, new_view_offset as f64, + 0., self.options.animations.horizontal_view_movement, niri_config::Animation::default_horizontal_view_movement(), ))); @@ -1272,6 +1274,7 @@ impl<W: LayoutElement> Workspace<W> { // effort and bug potential. let norm_factor = self.working_area.size.w as f64 / VIEW_GESTURE_WORKING_AREA_MOVEMENT; + let velocity = gesture.tracker.velocity() * norm_factor; let pos = gesture.tracker.pos() * norm_factor; let current_view_offset = pos + gesture.delta_from_tracker; @@ -1420,6 +1423,7 @@ impl<W: LayoutElement> Workspace<W> { self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( current_view_offset + delta, target_view_offset as f64, + velocity, self.options.animations.horizontal_view_movement, niri_config::Animation::default_horizontal_view_movement(), ))); diff --git a/src/ui/config_error_notification.rs b/src/ui/config_error_notification.rs index 12d92b85..b9e68e8e 100644 --- a/src/ui/config_error_notification.rs +++ b/src/ui/config_error_notification.rs @@ -62,6 +62,7 @@ impl ConfigErrorNotification { Animation::new( from, to, + 0., c.animations.config_notification_open_close, niri_config::Animation::default_config_notification_open_close(), ) @@ -118,7 +119,9 @@ impl ConfigErrorNotification { } State::Hiding(anim) => { anim.set_current_time(target_presentation_time); - if anim.is_done() { + // HACK: prevent bounciness on hiding. This is better done with a clamp property on + // the spring animation. + if anim.is_done() || anim.value() <= 0. { self.state = State::Hidden; } } |
