rdi_core/spec.rs
1//! Animation specification types.
2//!
3//! The engine consumes a `Vec<IconAnimationSpec>` and an
4//! [`AnimationOptions`]; both are plain data with no callbacks.
5
6use crate::curve::Curve;
7use crate::duration::Duration;
8use crate::geometry::Point;
9use crate::id::IconId;
10use crate::overlay_plan::OverlayRenderOptions;
11
12/// Reusable movement, effect-strength and duration recommendations.
13///
14/// Pass these fields to animation/effect constructors explicitly. A preset
15/// does not compile shaders, change their progress clock or start playback.
16/// The default is ease-in-out movement, smooth 15 percent strength fades and
17/// a fixed two-second duration. This value is available on every platform.
18#[derive(Clone, Debug)]
19pub struct AnimationPreset {
20 /// Suggested position interpolation, reusable for either axis.
21 pub movement: Curve,
22 /// Suggested effect strength; Effect still enforces clean endpoints.
23 pub envelope: Curve,
24 /// Suggested fixed or distance-based timing policy.
25 pub duration: Duration,
26}
27
28impl Default for AnimationPreset {
29 fn default() -> Self {
30 Self {
31 movement: Curve::ease_in_out(),
32 envelope: Curve::keyframes(
33 vec![
34 crate::Keyframe::new(0.0, 0.0),
35 crate::Keyframe::new(0.15, 1.0),
36 crate::Keyframe::new(0.85, 1.0),
37 crate::Keyframe::new(1.0, 0.0),
38 ],
39 crate::KeyframeInterp::SmoothStep,
40 ).expect("valid default preset envelope"),
41 duration: Duration::fixed(std::time::Duration::from_secs(2)),
42 }
43 }
44}
45
46/// Per-icon animation description.
47///
48/// X and Y are two independent axis animations that share the same
49/// normalised time `t = elapsed / duration ∈ [0, 1]`, so both axes tick in
50/// lock-step but can use different curves.
51#[derive(Clone, Debug)]
52pub struct IconAnimationSpec {
53 pub id: IconId,
54 pub target: Point,
55 pub duration: Duration,
56 pub curve_x: Curve,
57 pub curve_y: Curve,
58 pub effect: Option<crate::Effect>,
59}
60
61impl IconAnimationSpec {
62 /// Convenience constructor that reuses the same curve for both axes.
63 pub fn new(id: IconId, target: Point, duration: Duration, curve: Curve) -> Self {
64 Self {
65 id,
66 target,
67 duration,
68 curve_x: curve.clone(),
69 curve_y: curve,
70 effect: None,
71 }
72 }
73
74 /// Constructor with distinct X/Y curves.
75 pub fn with_axes(
76 id: IconId,
77 target: Point,
78 duration: Duration,
79 curve_x: Curve,
80 curve_y: Curve,
81 ) -> Self {
82 Self {
83 id,
84 target,
85 duration,
86 curve_x,
87 curve_y,
88 effect: None,
89 }
90 }
91}
92
93/// Global options that apply to every icon in an animation batch.
94#[derive(Clone, Copy, Debug, Default, PartialEq)]
95pub struct AnimationOptions {
96 /// Align destinations to available grid cells before preparation. Protects
97 /// stationary icons and resolves target conflicts; never changes Shell flags.
98 /// Defaults to false. Failures occur before artwork, flags or position writes.
99 pub snap_to_grid: bool,
100
101 /// Tick rate in Hz. Defaults to 100 (matching the legacy 10 ms step).
102 pub tick_hz: Option<u32>,
103
104 /// Distance threshold in pixels below which an icon is considered "at
105 /// target" and dropped from the active set. Defaults to `10`
106 /// (matching the legacy `POSITION_ERROR_VALUE`).
107 pub position_tolerance_px: Option<i32>,
108
109 /// Optional folder-flag operation applied before the animation starts.
110 pub before_flags: Option<FolderFlagOp>,
111
112 /// Optional folder-flag operation applied after the animation ends
113 /// (regardless of success or user-triggered stop).
114 pub after_flags: Option<FolderFlagOp>,
115
116 /// Force the engine to skip the overlay entirely and take the
117 /// "loud warning + direct teleport" fallback path.
118 ///
119 /// Lets tests and the `animate_direct` demo binary exercise the
120 /// fallback without needing a real overlay failure. Defaults to
121 /// `false` — normal callers get the overlay animation.
122 pub force_fallback: bool,
123
124 /// Feature toggles for the overlay renderer's decoration passes
125 /// (label text, shortcut-arrow overlay, admin shield overlay).
126 /// Defaults to [`OverlayRenderOptions::all_enabled`] — every
127 /// decoration Explorer draws is drawn on the overlay too.
128 pub render_options: OverlayRenderOptions,
129}
130
131/// Folder-flag mutation semantics, mirroring the legacy `ISF_*` modifiers.
132#[derive(Copy, Clone, Debug, PartialEq, Eq)]
133pub enum FolderFlagOp {
134 /// OR-set semantics — matches legacy `set_desktop_flags`.
135 ///
136 /// The engine will call `SetCurrentFolderFlags(flags, 0xFFFF_FFFF)`.
137 Set(u32),
138
139 /// Exactly-set semantics — matches legacy `exactly_set_desktop_flags`.
140 ///
141 /// The engine will call `SetCurrentFolderFlags(build_true_mask(flags), flags)`.
142 Exactly(u32),
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::{Duration, IconId};
149
150 #[test]
151 fn animation_preset_defaults_are_independent() {
152 use crate::AnimationCurve;
153 let mut preset = AnimationPreset::default();
154 assert_eq!(preset.movement, Curve::ease_in_out());
155 assert_eq!(preset.duration.resolve(Point::ZERO, Point::ZERO).unwrap(), std::time::Duration::from_secs(2));
156 for (progress, expected) in [(0.0, 0.0), (0.15, 1.0), (0.5, 1.0), (0.85, 1.0), (1.0, 0.0)] {
157 assert_eq!(preset.envelope.eval(progress), expected);
158 }
159 preset.movement = Curve::linear();
160 assert_eq!(AnimationPreset::default().movement, Curve::ease_in_out());
161 fn assert_send_sync<Value: Send + Sync>() {}
162 assert_send_sync::<AnimationPreset>();
163 }
164
165 #[test]
166 fn spec_new_shares_curve_between_axes() {
167 let c = Curve::linear();
168 let s = IconAnimationSpec::new(
169 IconId::from("x"),
170 Point::new(10, 20),
171 Duration::fixed(std::time::Duration::from_millis(500)),
172 c.clone(),
173 );
174 assert_eq!(s.curve_x, c);
175 assert_eq!(s.curve_y, c);
176 }
177
178 #[test]
179 fn options_default_leaves_everything_none() {
180 let o = AnimationOptions::default();
181 assert!(o.tick_hz.is_none());
182 assert!(o.position_tolerance_px.is_none());
183 assert!(o.before_flags.is_none());
184 assert!(o.after_flags.is_none());
185 assert!(!o.force_fallback);
186 assert_eq!(o.render_options, OverlayRenderOptions::all_enabled());
187 }
188}