use knuffel::errors::DecodeError; use knuffel::Decode as _; use crate::utils::{expect_only_children, parse_arg_node, MergeWith}; use crate::FloatOrInt; #[derive(Debug, Clone, PartialEq)] pub struct Animations { pub off: bool, pub slowdown: f64, pub workspace_switch: WorkspaceSwitchAnim, pub window_open: WindowOpenAnim, pub window_close: WindowCloseAnim, pub horizontal_view_movement: HorizontalViewMovementAnim, pub window_movement: WindowMovementAnim, pub window_resize: WindowResizeAnim, pub config_notification_open_close: ConfigNotificationOpenCloseAnim, pub exit_confirmation_open_close: ExitConfirmationOpenCloseAnim, pub screenshot_ui_open: ScreenshotUiOpenAnim, pub overview_open_close: OverviewOpenCloseAnim, pub recent_windows_close: RecentWindowsCloseAnim, } impl Default for Animations { fn default() -> Self { Self { off: false, slowdown: 1., workspace_switch: Default::default(), horizontal_view_movement: Default::default(), window_movement: Default::default(), window_open: Default::default(), window_close: Default::default(), window_resize: Default::default(), config_notification_open_close: Default::default(), exit_confirmation_open_close: Default::default(), screenshot_ui_open: Default::default(), overview_open_close: Default::default(), recent_windows_close: Default::default(), } } } #[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct AnimationsPart { #[knuffel(child)] pub off: bool, #[knuffel(child)] pub on: bool, #[knuffel(child, unwrap(argument))] pub slowdown: Option>, #[knuffel(child)] pub workspace_switch: Option, #[knuffel(child)] pub window_open: Option, #[knuffel(child)] pub window_close: Option, #[knuffel(child)] pub horizontal_view_movement: Option, #[knuffel(child)] pub window_movement: Option, #[knuffel(child)] pub window_resize: Option, #[knuffel(child)] pub config_notification_open_close: Option, #[knuffel(child)] pub exit_confirmation_open_close: Option, #[knuffel(child)] pub screenshot_ui_open: Option, #[knuffel(child)] pub overview_open_close: Option, #[knuffel(child)] pub recent_windows_close: Option, } impl MergeWith for Animations { fn merge_with(&mut self, part: &AnimationsPart) { self.off |= part.off; if part.on { self.off = false; } merge!((self, part), slowdown); // Animation properties are fairly tied together, except maybe `off`. So let's just save // ourselves the work and not merge within individual animations. merge_clone!( (self, part), workspace_switch, window_open, window_close, horizontal_view_movement, window_movement, window_resize, config_notification_open_close, exit_confirmation_open_close, screenshot_ui_open, overview_open_close, recent_windows_close, ); } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct Animation { pub off: bool, pub kind: Kind, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum Kind { Easing(EasingParams), Spring(SpringParams), } #[derive(Debug, Clone, Copy, PartialEq)] pub struct EasingParams { pub duration_ms: u32, pub curve: Curve, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum Curve { Linear, EaseOutQuad, EaseOutCubic, EaseOutExpo, CubicBezier(f64, f64, f64, f64), } #[derive(Debug, Clone, Copy, PartialEq)] pub struct SpringParams { pub damping_ratio: f64, pub stiffness: u32, pub epsilon: f64, } #[derive(Debug, Clone, Copy, PartialEq)] pub struct WorkspaceSwitchAnim(pub Animation); impl Default for WorkspaceSwitchAnim { fn default() -> Self { Self(Animation { off: false, kind: Kind::Spring(SpringParams { damping_ratio: 1., stiffness: 1000, epsilon: 0.0001, }), }) } } #[derive(Debug, Clone, PartialEq)] pub struct WindowOpenAnim { pub anim: Animation, pub custom_shader: Option, } impl Default for WindowOpenAnim { fn default() -> Self { Self { anim: Animation { off: false, kind: Kind::Easing(EasingParams { duration_ms: 150, curve: Curve::EaseOutExpo, }), }, custom_shader: None, } } } #[derive(Debug, Clone, PartialEq)] pub struct WindowCloseAnim { pub anim: Animation, pub custom_shader: Option, } impl Default for WindowCloseAnim { fn default() -> Self { Self { anim: Animation { off: false, kind: Kind::Easing(EasingParams { duration_ms: 150, curve: Curve::EaseOutQuad, }), }, custom_shader: None, } } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct HorizontalViewMovementAnim(pub Animation); impl Default for HorizontalViewMovementAnim { fn default() -> Self { Self(Animation { off: false, kind: Kind::Spring(SpringParams { damping_ratio: 1., stiffness: 800, epsilon: 0.0001, }), }) } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct WindowMovementAnim(pub Animation); impl Default for WindowMovementAnim { fn default() -> Self { Self(Animation { off: false, kind: Kind::Spring(SpringParams { damping_ratio: 1., stiffness: 800, epsilon: 0.0001, }), }) } } #[derive(Debug, Clone, PartialEq)] pub struct WindowResizeAnim { pub anim: Animation, pub custom_shader: Option, } impl Default for WindowResizeAnim { fn default() -> Self { Self { anim: Animation { off: false, kind: Kind::Spring(SpringParams { damping_ratio: 1., stiffness: 800, epsilon: 0.0001, }), }, custom_shader: None, } } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct ConfigNotificationOpenCloseAnim(pub Animation); impl Default for ConfigNotificationOpenCloseAnim { fn default() -> Self { Self(Animation { off: false, kind: Kind::Spring(SpringParams { damping_ratio: 0.6, stiffness: 1000, epsilon: 0.001, }), }) } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct ExitConfirmationOpenCloseAnim(pub Animation); impl Default for ExitConfirmationOpenCloseAnim { fn default() -> Self { Self(Animation { off: false, kind: Kind::Spring(SpringParams { damping_ratio: 0.6, stiffness: 500, epsilon: 0.01, }), }) } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct ScreenshotUiOpenAnim(pub Animation); impl Default for ScreenshotUiOpenAnim { fn default() -> Self { Self(Animation { off: false, kind: Kind::Easing(EasingParams { duration_ms: 200, curve: Curve::EaseOutQuad, }), }) } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct OverviewOpenCloseAnim(pub Animation); impl Default for OverviewOpenCloseAnim { fn default() -> Self { Self(Animation { off: false, kind: Kind::Spring(SpringParams { damping_ratio: 1., stiffness: 800, epsilon: 0.0001, }), }) } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct RecentWindowsCloseAnim(pub Animation); impl Default for RecentWindowsCloseAnim { fn default() -> Self { Self(Animation { off: false, kind: Kind::Spring(SpringParams { damping_ratio: 1., stiffness: 800, epsilon: 0.001, }), }) } } impl knuffel::Decode for WorkspaceSwitchAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for HorizontalViewMovementAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for WindowMovementAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for WindowOpenAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().anim; let mut custom_shader = None; let anim = Animation::decode_node(node, ctx, default, |child, ctx| { if &**child.node_name == "custom-shader" { custom_shader = parse_arg_node("custom-shader", child, ctx)?; Ok(true) } else { Ok(false) } })?; Ok(Self { anim, custom_shader, }) } } impl knuffel::Decode for WindowCloseAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().anim; let mut custom_shader = None; let anim = Animation::decode_node(node, ctx, default, |child, ctx| { if &**child.node_name == "custom-shader" { custom_shader = parse_arg_node("custom-shader", child, ctx)?; Ok(true) } else { Ok(false) } })?; Ok(Self { anim, custom_shader, }) } } impl knuffel::Decode for WindowResizeAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().anim; let mut custom_shader = None; let anim = Animation::decode_node(node, ctx, default, |child, ctx| { if &**child.node_name == "custom-shader" { custom_shader = parse_arg_node("custom-shader", child, ctx)?; Ok(true) } else { Ok(false) } })?; Ok(Self { anim, custom_shader, }) } } impl knuffel::Decode for ConfigNotificationOpenCloseAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for ExitConfirmationOpenCloseAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for ScreenshotUiOpenAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for OverviewOpenCloseAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for RecentWindowsCloseAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl Animation { pub fn new_off() -> Self { Self { off: true, kind: Kind::Easing(EasingParams { duration_ms: 0, curve: Curve::Linear, }), } } fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, default: Self, mut process_children: impl FnMut( &knuffel::ast::SpannedNode, &mut knuffel::decode::Context, ) -> Result>, ) -> Result> { #[derive(Default, PartialEq)] struct OptionalEasingParams { duration_ms: Option, curve: Option, } expect_only_children(node, ctx); let mut off = false; let mut easing_params = OptionalEasingParams::default(); 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 != OptionalEasingParams::default() { 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", )); } let mut iter_args = child.arguments.iter(); let val = iter_args.next().ok_or_else(|| { DecodeError::missing(child, "additional argument `curve` is required") })?; let animation_curve_string: String = knuffel::traits::DecodeScalar::decode(val, ctx)?; let animation_curve = match animation_curve_string.as_str() { "linear" => Some(Curve::Linear), "ease-out-quad" => Some(Curve::EaseOutQuad), "ease-out-cubic" => Some(Curve::EaseOutCubic), "ease-out-expo" => Some(Curve::EaseOutExpo), "cubic-bezier" => { let val = iter_args.next().ok_or_else(|| { DecodeError::missing( child, "missing x1 coordinate for cubic Bézier curve control point", ) })?; // the X axis represents time frame so it cannot be negative // or larger than 1 let x1: FloatOrInt<0, 1> = knuffel::traits::DecodeScalar::decode(val, ctx)?; let val = iter_args.next().ok_or_else(|| { DecodeError::missing( child, "missing y1 coordinate for cubic Bézier curve control point", ) })?; let y1: FloatOrInt<{ i32::MIN }, { i32::MAX }> = knuffel::traits::DecodeScalar::decode(val, ctx)?; let val = iter_args.next().ok_or_else(|| { DecodeError::missing( child, "missing x2 coordinate for cubic Bézier curve control point", ) })?; let x2: FloatOrInt<0, 1> = knuffel::traits::DecodeScalar::decode(val, ctx)?; let val = iter_args.next().ok_or_else(|| { DecodeError::missing( child, "missing y2 coordinate for cubic Bézier curve control point", ) })?; let y2: FloatOrInt<{ i32::MIN }, { i32::MAX }> = knuffel::traits::DecodeScalar::decode(val, ctx)?; Some(Curve::CubicBezier(x1.0, y1.0, x2.0, y2.0)) } unexpected_curve => { ctx.emit_error(DecodeError::unexpected( &val.literal, "argument", format!( "unexpected animation curve `{unexpected_curve}`. \ Niri only supports five animation curves: \ `ease-out-quad`, `ease-out-cubic`, `ease-out-expo`, `linear` and `cubic-bezier`." ), )); None } }; if let Some(val) = iter_args.next() { ctx.emit_error(DecodeError::unexpected( &val.literal, "argument", "unexpected argument", )); } for name in child.properties.keys() { ctx.emit_error(DecodeError::unexpected( name, "property", format!("unexpected property `{}`", name.escape_default()), )); } for child in child.children() { ctx.emit_error(DecodeError::unexpected( child, "node", format!("unexpected node `{}`", child.node_name.escape_default()), )); } easing_params.curve = animation_curve; } name_str => { if !process_children(child, ctx)? { ctx.emit_error(DecodeError::unexpected( child, "node", format!("unexpected node `{}`", name_str.escape_default()), )); } } } } let kind = if let Some(spring_params) = spring_params { // Configured spring. Kind::Spring(spring_params) } else if easing_params == OptionalEasingParams::default() { // Did not configure anything. default.kind } else { // Configured easing. let default = if let Kind::Easing(easing) = default.kind { easing } else { // Generic fallback values for when the default animation is spring, but the user // configured an easing animation. EasingParams { duration_ms: 250, curve: Curve::EaseOutCubic, } }; Kind::Easing(EasingParams { duration_ms: easing_params.duration_ms.unwrap_or(default.duration_ms), curve: easing_params.curve.unwrap_or(default.curve), }) }; Ok(Self { off, kind }) } } impl knuffel::Decode for SpringParams where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { 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, }) } }