Skip to main content

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}