aboutsummaryrefslogtreecommitdiff
path: root/niri-config/src
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-03-05 13:32:30 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-03-05 13:32:52 +0400
commitae89b2e514fd4c22a38c2dce258707c369ca944a (patch)
tree743448567dbc92843b46fc677b39336df3406a58 /niri-config/src
parent732f7f6f33595924654d23a380cc2a48bf9f3257 (diff)
downloadniri-ae89b2e514fd4c22a38c2dce258707c369ca944a.tar.gz
niri-ae89b2e514fd4c22a38c2dce258707c369ca944a.tar.bz2
niri-ae89b2e514fd4c22a38c2dce258707c369ca944a.zip
Implement spring animations
Diffstat (limited to 'niri-config/src')
-rw-r--r--niri-config/src/lib.rs331
1 files changed, 310 insertions, 21 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()