dinamika_core/timeline/action.rs
1//! The unit of timeline composition: [`Action`] (a tween, pause, parallel or
2//! sequence), combinators over it and the flattening of the tree into a flat list
3//! of tweens ([`flatten`]).
4//!
5//! # Auto-registration on the timeline
6//!
7//! An action built from a registered shape (see [`Shape::on`]) remembers its
8//! timeline and adds **itself** to it on drop (`Drop`) — unless a combinator has
9//! "taken" it ([`parallel`]/[`sequence`]/…) or it was added explicitly via
10//! `tl.*`. So a single animation can simply be written as a statement expression:
11//! `title.content("Hi").smooth(0.5, Easing::CubicInOut);`. Absorption of children
12//! by a combinator and explicit addition mark the action as accounted for
13//! (`registered`), so it is not registered again on drop.
14//!
15//! [`Shape::on`]: crate::Shape::on
16
17use std::cell::Cell;
18use std::rc::{Rc, Weak};
19
20use super::tween::TweenObj;
21use super::TimelineState;
22
23/// The unit of timeline composition: a tween, pause, parallel or sequence.
24///
25/// Created via [`Signal::tween_to`](crate::Signal::tween_to), [`pause`],
26/// [`parallel`], [`sequence`] and the [`Action::then`] / [`Action::with`]
27/// combinators.
28pub struct Action {
29 kind: ActionKind,
30 /// The timeline for auto-registration on drop. `None` for actions built
31 /// without a shape (for example [`Signal::tween_to`](crate::Signal::tween_to))
32 /// or from an unregistered shape: such actions do not add themselves to the
33 /// timeline.
34 timeline: Option<Weak<TimelineState>>,
35 /// The action is already accounted for (added to the timeline explicitly or
36 /// absorbed by a combinator) — its `Drop` registers nothing.
37 registered: Cell<bool>,
38}
39
40enum ActionKind {
41 Tween(Rc<dyn TweenObj>),
42 Wait(f64),
43 /// A sequence with an extra `gap` pause between elements.
44 Seq(Vec<Action>, f64),
45 /// Parallel execution.
46 Par(Vec<Action>),
47}
48
49impl Action {
50 /// Builds an action from an [`ActionKind`], not bound to a timeline and not
51 /// accounted for.
52 fn new(kind: ActionKind) -> Action {
53 Action { kind, timeline: None, registered: Cell::new(false) }
54 }
55
56 /// Wraps a finished tween leaf in an [`Action`] (called from
57 /// [`new_tween`](super::new_tween), as well as from the text animations that
58 /// implement [`TweenObj`] directly).
59 pub(crate) fn from_tween(tween: Rc<dyn TweenObj>) -> Action {
60 Action::new(ActionKind::Tween(tween))
61 }
62
63 /// Binds the action to a timeline so it auto-registers on drop. Called by the
64 /// shape's property handles (`over`/`smooth`/…). Ignores an empty (dead)
65 /// reference — an action built from an unregistered shape stays "free".
66 pub(crate) fn attach_timeline(&mut self, tl: Weak<TimelineState>) {
67 if tl.strong_count() > 0 {
68 self.timeline = Some(tl);
69 }
70 }
71
72 /// Marks the action as accounted for: its `Drop` registers nothing anymore.
73 /// Called on absorption by a combinator and on explicit addition to the
74 /// timeline.
75 pub(super) fn mark_registered(&self) {
76 self.registered.set(true);
77 }
78}
79
80impl Drop for Action {
81 fn drop(&mut self) {
82 if self.registered.get() {
83 return;
84 }
85 // A free action with a "live" timeline goes to its end. We mark ourselves
86 // as accounted for and move the contents into a separate (already
87 // accounted for) action so nothing is doubled on a further drop.
88 self.registered.set(true);
89 if let Some(state) = self.timeline.as_ref().and_then(Weak::upgrade) {
90 let detached = Action {
91 kind: std::mem::replace(&mut self.kind, ActionKind::Wait(0.0)),
92 timeline: None,
93 registered: Cell::new(true),
94 };
95 state.append(detached);
96 }
97 }
98}
99
100/// Assembles a compound action from children: marks each child as accounted for
101/// (so it does not register itself), inherits the timeline from the first bound
102/// child (so the resulting wrapper auto-registers) and wraps the children in
103/// `kind`.
104fn combine(items: Vec<Action>, kind: impl FnOnce(Vec<Action>) -> ActionKind) -> Action {
105 let timeline = items.iter().find_map(|a| a.timeline.clone());
106 for it in &items {
107 it.mark_registered();
108 }
109 Action { kind: kind(items), timeline, registered: Cell::new(false) }
110}
111
112/// A pause of the given duration (in seconds).
113pub fn pause(seconds: f64) -> Action {
114 Action::new(ActionKind::Wait(seconds.max(0.0)))
115}
116
117/// Simultaneous execution of all the actions. The duration is the maximum of the
118/// nested ones.
119pub fn parallel(items: impl IntoIterator<Item = Action>) -> Action {
120 let items: Vec<Action> = items.into_iter().collect();
121 // Text edits merge by the shared text-stage cell, highlight ones by the shared
122 // highlight-stage cell; the cells differ, so the two passes are independent.
123 merge_overlapping(
124 &items,
125 |t| t.morph_group(),
126 |t| t.morph_from(),
127 |t| t.morph_new(),
128 |t, old, new| t.rebase(old, new),
129 );
130 merge_overlapping(
131 &items,
132 |t| t.highlight_group(),
133 |t| t.highlight_from(),
134 |t| t.highlight_to(),
135 |t, old, new| t.highlight_rebase(old.clone(), new.clone()),
136 );
137 combine(items, ActionKind::Par)
138}
139
140/// Merges overlapping edits of the SAME shape in a parallel into a common morph.
141///
142/// Applies to both text (content) and highlighting: such edits share the stage
143/// cell and overwrite each other every frame, so without common endpoints the
144/// first one "leaks" — its "from" already contains the result of the neighboring
145/// edits (the committed state is mutated immediately on each edit) and is visible
146/// before the animation starts. Here the endpoints of each edit in the group are
147/// reset to common ones — the base (the state before the whole group) and the
148/// final (after the whole group). Then `prepend(..).smooth()` and
149/// `append(..).smooth()` in one parallel morph the original text into the final
150/// one consistently, and several `highlight(..).over(..)` highlight their ranges
151/// together from a common base — without flickering edges before the start.
152///
153/// The group is specified by the key `key` (the identity of the shared cell), the
154/// transition endpoints are read via `from`/`to`, and the reset is via `rebase`.
155/// The base and final are derived from the group's own endpoints regardless of
156/// element order: the edits form a chain `old→new` (each "to" is the "from" of the
157/// next), so the base is the single "from" that is not among the neighbors' "to",
158/// and the final is the single "to" that is not among the "from"s. If the chain
159/// does not converge uniquely (a non-homogeneous group), the edits are left as is.
160///
161/// Only the parallel's direct tween children (the simultaneous ones) are affected;
162/// nested sequences keep their sequential semantics.
163fn merge_overlapping<T: Clone + PartialEq>(
164 items: &[Action],
165 key: impl Fn(&Rc<dyn TweenObj>) -> Option<*const ()>,
166 from: impl Fn(&Rc<dyn TweenObj>) -> Option<T>,
167 to: impl Fn(&Rc<dyn TweenObj>) -> Option<T>,
168 rebase: impl Fn(&Rc<dyn TweenObj>, &T, &T),
169) {
170 // Group the edit tweens by the shared stage cell (the shape's identity).
171 let mut groups: Vec<(*const (), Vec<&Rc<dyn TweenObj>>)> = Vec::new();
172 for it in items {
173 let tween = match &it.kind {
174 ActionKind::Tween(t) => t,
175 _ => continue,
176 };
177 let k = match key(tween) {
178 Some(k) => k,
179 None => continue,
180 };
181 match groups.iter_mut().find(|(g, _)| *g == k) {
182 Some((_, group)) => group.push(tween),
183 None => groups.push((k, vec![tween])),
184 }
185 }
186
187 for (_, group) in &groups {
188 if group.len() < 2 {
189 continue;
190 }
191 let olds: Vec<T> = group.iter().filter_map(|t| from(t)).collect();
192 let news: Vec<T> = group.iter().filter_map(|t| to(t)).collect();
193 if olds.len() != group.len() || news.len() != group.len() {
194 continue; // a non-homogeneous group — leave it alone
195 }
196 let bases: Vec<&T> = olds.iter().filter(|o| !news.contains(o)).collect();
197 let finals: Vec<&T> = news.iter().filter(|n| !olds.contains(n)).collect();
198 if bases.len() == 1 && finals.len() == 1 {
199 for t in group {
200 rebase(t, bases[0], finals[0]);
201 }
202 }
203 }
204}
205
206/// Sequential execution one after another (with no gaps).
207pub fn sequence(items: impl IntoIterator<Item = Action>) -> Action {
208 combine(items.into_iter().collect(), |v| ActionKind::Seq(v, 0.0))
209}
210
211/// A cascade: sequential execution of actions with a `gap`-second pause between
212/// neighbors. Put inside the animations that should follow each other in a "wave".
213pub fn cascade(items: impl IntoIterator<Item = Action>, gap: f64) -> Action {
214 let gap = gap.max(0.0);
215 combine(items.into_iter().collect(), move |v| ActionKind::Seq(v, gap))
216}
217
218/// An action that starts after `seconds` seconds.
219pub fn delay(seconds: f64, action: Action) -> Action {
220 sequence(vec![pause(seconds), action])
221}
222
223impl Action {
224 /// Run this action, then `next` (sequentially).
225 pub fn then(self, next: Action) -> Action {
226 sequence(vec![self, next])
227 }
228
229 /// Run this action simultaneously with `other`.
230 pub fn with(self, other: Action) -> Action {
231 parallel(vec![self, other])
232 }
233
234 /// Start this action after `seconds` seconds.
235 pub fn after(self, seconds: f64) -> Action {
236 delay(seconds, self)
237 }
238}
239
240/// Flattens the action tree into a flat list of tweens with their absolute start
241/// time. Returns the total duration of the subtree.
242///
243/// Side effect: sets each tween's absolute start time via
244/// [`TweenObj::set_start`]. For a pure duration computation (without mutating
245/// tweens) use [`duration_of`].
246pub(super) fn flatten(action: &Action, base: f64, out: &mut Vec<Rc<dyn TweenObj>>) -> f64 {
247 match &action.kind {
248 ActionKind::Tween(t) => {
249 t.set_start(base);
250 out.push(t.clone());
251 t.duration()
252 }
253 ActionKind::Wait(d) => *d,
254 ActionKind::Seq(items, gap) => {
255 let mut offset = 0.0;
256 let n = items.len();
257 for (i, it) in items.iter().enumerate() {
258 offset += flatten(it, base + offset, out);
259 if i + 1 < n {
260 offset += *gap;
261 }
262 }
263 offset
264 }
265 ActionKind::Par(items) => {
266 let mut max = 0.0_f64;
267 for it in items {
268 max = max.max(flatten(it, base, out));
269 }
270 max
271 }
272 }
273}
274
275/// Purely computes the duration of the subtree without changing the tweens' state
276/// (unlike [`flatten`], which also sets the start time). Used in
277/// [`Timeline::duration`](super::Timeline::duration), which is semantically a
278/// getter.
279pub(super) fn duration_of(action: &Action) -> f64 {
280 match &action.kind {
281 ActionKind::Tween(t) => t.duration(),
282 ActionKind::Wait(d) => *d,
283 ActionKind::Seq(items, gap) => {
284 let mut total = 0.0;
285 let n = items.len();
286 for (i, it) in items.iter().enumerate() {
287 total += duration_of(it);
288 if i + 1 < n {
289 total += *gap;
290 }
291 }
292 total
293 }
294 ActionKind::Par(items) => items.iter().map(duration_of).fold(0.0_f64, f64::max),
295 }
296}