aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--niri-config/src/lib.rs331
-rw-r--r--resources/default-config.kdl42
-rw-r--r--src/animation/mod.rs151
-rw-r--r--src/animation/spring.rs137
-rw-r--r--src/layout/monitor.rs4
-rw-r--r--src/layout/tile.rs1
-rw-r--r--src/layout/workspace.rs4
-rw-r--r--src/ui/config_error_notification.rs5
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;
}
}