Skip to main content

kui_core/
depart.rs

1//! Exit transitions: a subtree the view stopped declaring, kept as a
2//! picture and played out.
3//!
4//! A view asks for one with `NodeSpec::exit` (an [`crate::enter::Enter`]
5//! read the other way: where the slots end) together with a `transition`.
6//! When such a node is in one frame's tree and not the next, while its
7//! parent still is, its specs, contents and laid-out rects are copied out
8//! of the previous frame into this store as a **ghost**, and every later
9//! frame replays it until its transition ends. An app never calls into
10//! this module: `Core` owns the [`DepartStore`], and a driver only sees
11//! that another frame is owed while [`DepartStore::animating`] is true.
12//!
13//! A ghost is:
14//!
15//! - **frozen, not re-laid-out.** Its rects are the ones layout gave it
16//!   the last time it existed, so a dying node never fights the live
17//!   layout for space.
18//! - **in its place, and unclipped.** It is painted where the node was in
19//!   the paint order, just under the node that painted after it, so a
20//!   panel that sat under a HUD leaves under it. Its ancestors may be
21//!   gone, so it draws outside every clip they held; the clips inside the
22//!   picture stay.
23//! - **inert.** No hit region, no place in the Tab ring, no access row.
24//! - **self-easing.** Its slots are one lerp over its own clock; a spring
25//!   easing samples as ease-out.
26//!
27//! A ghost is dropped when its transition ends, when the same key comes
28//! back (the live node wins at once, so a toast dismissed and re-shown
29//! does not double), when it has gone unreplayed for a while, and when a
30//! later frame's removal needs the room. The store holds at most
31//! [`MAX_NODES`] nodes, judged per frame's removal as a whole: the oldest
32//! ghosts are evicted until the removal fits, and a removal larger than
33//! the budget on its own is not animated at all, so a list never gets
34//! half its rows sliding out and the rest blinking away. A node that goes
35//! because an ancestor went goes at once with it, unless that ancestor
36//! has an `exit` of its own.
37
38use crate::anim::Easing;
39use crate::color::Color;
40use crate::enter::Enter;
41use crate::geom::{Rect, Vec2};
42use crate::key::Key;
43use crate::resources::ImageId;
44use crate::spec::{NodeSpec, Sizing};
45use crate::tree::{NIL, NodeContent, Tree};
46
47/// The most nodes every departing subtree together may retain. The budget
48/// is over *nodes* rather than subtrees because a node is what a replayed
49/// frame pays for. A frame whose removal is larger than this gets none of
50/// it animated (every departing node vanishes at once, as a node with no
51/// `exit` does) rather than the first of them sliding out and the rest
52/// blinking. A departing subtree costs about 0.065 µs a node in the frame
53/// it leaves and 0.021 µs a node a frame while it plays, so the bound is
54/// on memory and on a removal nobody meant to animate, not on time.
55pub const MAX_NODES: usize = 4096;
56
57/// Whether a node can depart at all: it declared an `exit`, and a
58/// `transition` with a duration to run it over. The diff counts a frame's
59/// removal by this before the store copies any of it, and
60/// [`DepartStore::depart`] returns early on the same two conditions, so a
61/// subtree counted is a subtree copied.
62#[inline]
63pub(crate) fn can_depart(spec: &NodeSpec) -> bool {
64    spec.anim().exit.is_some()
65        && spec
66            .transition
67            .as_ref()
68            .is_some_and(|t| t.duration_ms > 0.0)
69}
70
71/// A departing node's content. Same leaves a live node has, in the forms
72/// that outlive the frame that made them: a `TextId` indexes the frame's
73/// text list, which is rebuilt every frame, so a ghost carries the cache
74/// key of the shaped buffer behind it instead.
75#[derive(Clone, Copy, Debug, PartialEq)]
76pub(crate) enum GhostContent {
77    Container,
78    Text {
79        cache_key: u64,
80        color: Color,
81    },
82    Edit(Key),
83    Image(ImageId, crate::resources::ImageOpts),
84    /// A stroke: `len` points from `first` in the ghost's own point list
85    /// (relative to the node's box, like the live run's), drawn `width`
86    /// wide in the colour the node's `bg` slot eases to.
87    Line {
88        first: u32,
89        len: u32,
90        width: f32,
91        dash: Option<crate::line::Cut>,
92    },
93    /// A fragment, by value: the handle and the parameters the node last
94    /// declared. The ghost re-declares them every frame it draws, so the
95    /// picture is frozen at departure while the box eases.
96    Fragment(crate::fragment::Draw),
97    /// A polygon, by value like a fragment; the fill is the colour the
98    /// node's `bg` slot eases to.
99    Polygon(crate::fragment::Draw),
100    /// A path: `len` ops from `first` in the ghost's own op list
101    /// (relative to the node's box, like the live run's), with the run's
102    /// rule, stroke width and hash, so its masks are the live node's.
103    Path {
104        first: u32,
105        len: u32,
106        rule: crate::path::FillRule,
107        stroke_w: f32,
108        dash: Option<crate::line::Cut>,
109        hash: u64,
110        angle: Option<f32>,
111    },
112}
113
114pub(crate) struct GhostNode {
115    /// Index within this ghost, `NIL` for its root.
116    pub parent: u32,
117    pub spec: NodeSpec,
118    pub content: GhostContent,
119    /// Where layout left it, logical, in viewport coordinates.
120    pub rect: Rect,
121}
122
123/// Where a departing subtree sat in the paint order: the
124/// layer, and the key of the first node painted after it in that layer
125/// that the frame which noticed it gone still declares. Its ghost is
126/// painted just under that node — and at the end of its layer once the
127/// node is gone too, or on top of everything once the layer is.
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub(crate) enum Place {
130    /// In the in-flow layer, just under `before`; at its end for `None`.
131    InFlow { before: Option<Key> },
132    /// Inside the float layer rooted at `layer`, just under `before`; at
133    /// that layer's end for `None`.
134    InLayer { layer: Key, before: Option<Key> },
135    /// A float layer of its own, just under the layer rooted at `before`;
136    /// on top of the stack for `None`.
137    Layer { before: Option<Key> },
138}
139
140impl Place {
141    /// The key a per-node ask names, for the membership mask: the node a
142    /// ghost is painted under, or for one at a layer's end the layer.
143    fn mask_key(self) -> Option<Key> {
144        match self {
145            Place::InFlow { before } | Place::Layer { before } => before,
146            Place::InLayer { layer, before } => Some(before.unwrap_or(layer)),
147        }
148    }
149
150    /// The node (or, for a whole layer, the layer) the ghost is painted
151    /// just under; `None` at the end of wherever it is.
152    fn before(self) -> Option<Key> {
153        match self {
154            Place::InFlow { before } | Place::Layer { before } | Place::InLayer { before, .. } => {
155                before
156            }
157        }
158    }
159
160    /// The same place, under something else.
161    fn with_before(self, before: Option<Key>) -> Self {
162        match self {
163            Place::InFlow { .. } => Place::InFlow { before },
164            Place::InLayer { layer, .. } => Place::InLayer { layer, before },
165            Place::Layer { .. } => Place::Layer { before },
166        }
167    }
168}
169
170/// One ask a pass makes of the replay: the slot the emission has reached.
171/// The three `Under*` asks are per node and gated by [`Replay::may_precede`];
172/// the three `*End`/`Top` asks are per layer and also sweep up every
173/// ghost of that layer whose `before` node is gone.
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175pub(crate) enum At {
176    /// Just under in-flow node `key`.
177    UnderInFlow(Key),
178    /// The end of the in-flow layer.
179    InFlowEnd,
180    /// Just under the float layer rooted at `key`.
181    UnderLayer(Key),
182    /// Just under node `key` inside a float layer.
183    UnderInLayer(Key),
184    /// The end of the float layer rooted at `key`.
185    LayerEnd(Key),
186    /// Above every layer: whatever is still unpainted.
187    Top,
188}
189
190/// One departing subtree, with what it needs to play itself out.
191pub(crate) struct Ghost {
192    pub key: Key,
193    pub nodes: Vec<GhostNode>,
194    /// The points of every `line` in the subtree, copied out of the frame
195    /// that had them (a `LineId` indexes a list that is rebuilt every
196    /// frame). Empty, and unallocated, for a subtree with no lines.
197    pub points: Vec<Vec2>,
198    /// The ops of every `path` in the subtree, on the same terms.
199    pub ops: Vec<crate::path::PathOp>,
200    /// Where it is painted, relative to the live frame.
201    pub place: Place,
202    /// Clock reading (driver seconds) of the frame that noticed it gone.
203    left_at: f64,
204    /// How long the exit runs, in seconds (the root's `transition`).
205    duration: f64,
206    easing: Easing,
207    /// Where the root's slots are headed.
208    exit: Enter,
209    /// The group opacity the departing root inherited from ancestors that
210    /// may no longer exist.
211    base_opacity: f32,
212    last_used: u64,
213}
214
215/// A ghost's root as it should be painted this frame: the eased slots, and
216/// the offset to move every rect in the subtree by.
217pub(crate) struct Playback {
218    pub offset: Vec2,
219    pub bg: Option<Color>,
220    pub radius: Option<[f32; 4]>,
221    pub size: Option<(Option<f32>, Option<f32>)>,
222    /// Multiplied into the root's own opacity.
223    pub opacity: f32,
224    pub base_opacity: f32,
225    /// The root's turn and scale, eased toward the exit's when it names
226    /// either; `None` for a root that declares none and an exit that
227    /// names none (ADR 0043).
228    pub transform: Option<(f32, f32)>,
229}
230
231impl Ghost {
232    /// Where the exit has got to at `now`: `None` once it is over.
233    fn playback(&self, now: f64) -> Option<Playback> {
234        let raw = ((now - self.left_at) / self.duration) as f32;
235        if raw >= 1.0 || raw.is_nan() {
236            return None;
237        }
238        let p = self.easing.apply(raw.max(0.0));
239        let root = &self.nodes[0].spec;
240        let lerp = |from: f32, to: f32| from + (to - from) * p;
241        let e = &self.exit;
242        Some(Playback {
243            offset: Vec2::new(e.dx * p, e.dy * p),
244            bg: e.bg.map(|to| root.style.bg.lerp(to, p)),
245            radius: e
246                .radius
247                .map(|to| root.style.radius.map(|from| lerp(from, to))),
248            size: (e.width.is_some() || e.height.is_some()).then(|| {
249                let rect = self.nodes[0].rect;
250                (
251                    e.width.and_then(Sizing::amount).map(|to| lerp(rect.w, to)),
252                    e.height.and_then(Sizing::amount).map(|to| lerp(rect.h, to)),
253                )
254            }),
255            opacity: match e.opacity {
256                Some(to) => lerp(root.style.opacity, to),
257                None => root.style.opacity,
258            },
259            base_opacity: self.base_opacity,
260            transform: {
261                let own = root.transform_spec();
262                if own.is_none() && e.rotate.is_none() && e.scale.is_none() {
263                    None
264                } else {
265                    let own = own.unwrap_or_default();
266                    Some((
267                        e.rotate.map_or(own.rotate, |to| lerp(own.rotate, to)),
268                        e.scale.map_or(own.scale, |to| lerp(own.scale, to)),
269                    ))
270                }
271            },
272        })
273    }
274}
275
276/// The departing subtrees, bounded (see [`MAX_NODES`]).
277#[derive(Default)]
278pub struct DepartStore {
279    ghosts: Vec<Ghost>,
280    /// Nodes across every ghost, kept in step with `ghosts` so the budget
281    /// is a comparison rather than a walk.
282    nodes: usize,
283    /// The core's frame counter as of `begin_frame`.
284    frame_no: u64,
285    /// Whether a ghost replayed this frame is still mid-flight.
286    active: bool,
287    /// A membership mask over the ghosts' `before` keys, so a departure
288    /// whose key no ghost sits under — every row of a mass removal — skips
289    /// the walk that would hand its place on. Never cleared: a stale bit
290    /// costs one walk, and there are at most a few hundred ghosts to walk.
291    before_mask: u64,
292    /// The ghosts' own keys, as an exact set kept in step with `ghosts`,
293    /// so a departure of a key no ghost holds skips [`Self::retire`] —
294    /// which is a pass over the whole store, and a mass removal would
295    /// otherwise pay one per row. Exact rather than a 64-bit mask like
296    /// `before_mask`: a mass removal is hundreds of distinct keys, which
297    /// saturates 64 bits and makes a mask answer "maybe" every time.
298    held: rustc_hash::FxHashSet<Key>,
299}
300
301impl DepartStore {
302    /// Starts a frame under the core's counter.
303    pub(crate) fn begin_frame(&mut self, frame_no: u64) {
304        self.frame_no = frame_no;
305        self.active = false;
306        // A ghost not replayed for a while goes — the same backstop the
307        // anim store keeps, for a driver whose clock stops moving while
308        // frames keep coming (`retain::sweep_cutoff`).
309        if !self.ghosts.is_empty()
310            && let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
311        {
312            self.drop_where(|g| g.last_used < cutoff);
313        }
314    }
315
316    /// True when a ghost replayed this frame is still mid-flight, i.e. the
317    /// driver owes another frame.
318    pub fn animating(&self) -> bool {
319        self.active
320    }
321
322    /// Whether anything is departing at all — the gate every pass that
323    /// would otherwise walk an empty store checks first.
324    pub fn is_empty(&self) -> bool {
325        self.ghosts.is_empty()
326    }
327
328    /// How many nodes are retained across every departing subtree.
329    pub fn node_count(&self) -> usize {
330        self.nodes
331    }
332
333    /// The keys of the departing roots — what a caller tests the live tree
334    /// against to notice one coming back.
335    pub fn keys(&self) -> impl Iterator<Item = Key> + '_ {
336        self.ghosts.iter().map(|g| g.key)
337    }
338
339    /// Each departing root's key and the spec it left with — what a
340    /// trace names a departure by, since its node is no longer in any
341    /// tree.
342    pub(crate) fn roots(&self) -> impl Iterator<Item = (Key, &NodeSpec)> + '_ {
343        self.ghosts
344            .iter()
345            .filter_map(|g| g.nodes.first().map(|n| (g.key, &n.spec)))
346    }
347
348    fn drop_where(&mut self, mut pred: impl FnMut(&Ghost) -> bool) {
349        let nodes = &mut self.nodes;
350        let held = &mut self.held;
351        self.ghosts.retain(|g| {
352            let drop = pred(g);
353            if drop {
354                *nodes -= g.nodes.len();
355                held.remove(&g.key);
356            }
357            !drop
358        });
359    }
360
361    /// A key the view declared again: the live node wins and its ghost is
362    /// discarded, so a dismissed and re-shown toast does not draw twice.
363    pub(crate) fn retire(&mut self, key: Key) {
364        self.drop_where(|g| g.key == key);
365    }
366
367    /// The same, for every ghost whose key is in `live` — the whole of a
368    /// frame's returns in one pass.
369    pub(crate) fn retire_returned(&mut self, live: &rustc_hash::FxHashSet<Key>) {
370        if !live.is_empty() {
371            self.drop_where(|g| live.contains(&g.key));
372        }
373    }
374
375    /// The budget, applied to a frame's removal whole: `wanted` is every node the frame's departing subtrees
376    /// would add together, counted before any of them is copied. A removal
377    /// that fits an empty store is admitted — and if the store is holding
378    /// earlier exits it has no room beside, the **oldest** ghosts go first
379    /// until it fits, since they are the ones furthest through their own
380    /// fade and the removal the user just caused is the one they are
381    /// looking at. A removal larger than [`MAX_NODES`] on its own is refused
382    /// whole: `false`, no ghost, and the caller raises `exit-budget` for
383    /// the frame. There is no partial credit either way, so a view never
384    /// gets half its rows animating and the rest blinking, and how a
385    /// removal reads cannot depend on what else the app happened to be
386    /// doing 100 ms earlier.
387    pub(crate) fn admit(&mut self, wanted: usize) -> bool {
388        if wanted > MAX_NODES {
389            return false;
390        }
391        let mut evict = 0;
392        while self.nodes + wanted > MAX_NODES {
393            // Store order is departure order, so the front is the oldest.
394            let g = &self.ghosts[evict];
395            self.nodes -= g.nodes.len();
396            self.held.remove(&g.key);
397            evict += 1;
398        }
399        if evict > 0 {
400            self.ghosts.drain(..evict);
401        }
402        true
403    }
404
405    /// Copies `root`'s subtree out of `tree` (which is the *previous*
406    /// frame's, the last one that had it) and starts its exit at `now`.
407    /// `text` is read for that same frame's list, since the subtree's text
408    /// nodes carry its ids.
409    /// The budget is not checked here: the caller has counted the frame's
410    /// whole removal and had it admitted ([`Self::admit`]) before copying
411    /// any of it, so a subtree that reaches this always fits.
412    // The two lists at the end are the frame's per-node side tables, read
413    // for the same kept frame `tree` is; a struct for the pair would name
414    // one thing that only exists here.
415    #[allow(clippy::too_many_arguments)]
416    pub(crate) fn depart(
417        &mut self,
418        tree: &Tree,
419        root: usize,
420        now: f64,
421        base_opacity: f32,
422        place: Place,
423        text: &crate::text::TextSystem,
424        lines: &crate::line::LineStore,
425        fragments: &crate::fragment::FragmentList,
426        paths: &crate::path::PathStore,
427        exit_with: Option<Enter>,
428    ) {
429        let spec = &tree.specs[root];
430        // The exit named at the removal (`Core::set_exit`, backlog F136)
431        // over the one the kept frame declared.
432        let (Some(t), Some(exit)) = (spec.transition, exit_with.or(spec.anim().exit)) else {
433            return;
434        };
435        let duration = t.duration_ms.max(0.0) as f64 / 1000.0;
436        if duration <= 0.0 {
437            return;
438        }
439        let end = tree.subtree_end(root);
440        let key = tree.keys[root];
441        // A second departure of the same key (the view showed it, dropped
442        // it, showed it and dropped it again inside one exit) replaces the
443        // first: two pictures of one node are never right. Behind `held`
444        // because `retire` walks the whole store: unguarded, a frame that
445        // drops a thousand rows pays one walk per row, which is the one
446        // quadratic left in a mass removal — 32 ms for 10k subtrees
447        // against 1.05 ms guarded, and 44% of the departing frame at
448        // today's budget. Kept rather than deleted even though the frame
449        // path cannot reach it — `collect_departures` retires a returning
450        // key before it can depart again — because that argument runs
451        // through another module's early returns, and a set lookup is a
452        // cheap thing to be wrong about.
453        if self.held.contains(&key) {
454            self.retire(key);
455        }
456        let mut points = Vec::new();
457        let mut ops = Vec::new();
458        let nodes = (root..end)
459            .map(|i| GhostNode {
460                parent: if i == root {
461                    NIL
462                } else {
463                    tree.parent[i] - root as u32
464                },
465                spec: tree.specs[i].clone(),
466                content: match tree.content[i] {
467                    NodeContent::Container => GhostContent::Container,
468                    NodeContent::Text(id) => {
469                        let (cache_key, color) = text.prev_frame_text(id);
470                        GhostContent::Text { cache_key, color }
471                    }
472                    NodeContent::Edit(k) => GhostContent::Edit(k),
473                    NodeContent::Image(id, opts) => GhostContent::Image(id, opts),
474                    // A departing grid is its box: the cells are the
475                    // frame's and go with it.
476                    NodeContent::Cells(_) => GhostContent::Container,
477                    // A departing fragment keeps painting. Its draw is
478                    // sixteen floats and a handle, fixed-size and `Copy`,
479                    // so the ghost owns a copy outright rather than
480                    // indexing a list that has to outlive the frame —
481                    // which is what the fixed sixteen buys. The copy comes
482                    // from the previous frame's list, because that is the
483                    // tree this ghost is being cut out of.
484                    NodeContent::Fragment(id) => match fragments.prev_get(id) {
485                        Some(draw) => GhostContent::Fragment(draw),
486                        None => GhostContent::Container,
487                    },
488                    NodeContent::Polygon(id) => match fragments.prev_get(id) {
489                        Some(draw) => GhostContent::Polygon(draw),
490                        None => GhostContent::Container,
491                    },
492                    NodeContent::Line(id) => {
493                        let (run, pts) = lines.prev_run(id);
494                        let first = points.len() as u32;
495                        points.extend_from_slice(pts);
496                        GhostContent::Line {
497                            first,
498                            len: pts.len() as u32,
499                            width: run.width,
500                            dash: run.dash,
501                        }
502                    }
503                    NodeContent::Path(id) => match paths.prev_run(id) {
504                        Some((run, run_ops)) => {
505                            let first = ops.len() as u32;
506                            ops.extend_from_slice(run_ops);
507                            GhostContent::Path {
508                                first,
509                                len: run_ops.len() as u32,
510                                rule: run.rule,
511                                stroke_w: run.stroke_w,
512                                dash: run.dash,
513                                hash: run.hash,
514                                angle: run.angle,
515                            }
516                        }
517                        None => GhostContent::Container,
518                    },
519                },
520                rect: Rect::from_pos_size(tree.pos[i], tree.size[i]),
521            })
522            .collect::<Vec<_>>();
523        debug_assert!(
524            self.nodes + nodes.len() <= MAX_NODES,
525            "a departure the frame did not have admitted"
526        );
527        self.nodes += nodes.len();
528        // A ghost that was painted just under this node loses its place
529        // with it, and takes the place this one is taking: the two stay in
530        // the order they had, since the store keeps departures in order.
531        if self.before_mask & (1u64 << (key.0 & 63)) != 0 {
532            for g in &mut self.ghosts {
533                if g.place.before() == Some(key) {
534                    g.place = g.place.with_before(place.before());
535                }
536            }
537        }
538        if let Some(before) = place.before() {
539            self.before_mask |= 1u64 << (before.0 & 63);
540        }
541        self.held.insert(key);
542        self.ghosts.push(Ghost {
543            key,
544            nodes,
545            points,
546            ops,
547            place,
548            left_at: now,
549            duration,
550            easing: t.curve(),
551            exit,
552            base_opacity,
553            last_used: self.frame_no,
554        });
555    }
556
557    /// Starts this frame's replay: drops the ghosts whose exit is over,
558    /// and hands the rest out with where each has got to at `now`, taken
559    /// out of the store so the emitter can borrow the rest of the core
560    /// while it paints them between the live nodes. [`Self::end_replay`]
561    /// puts them back.
562    pub(crate) fn begin_replay(&mut self, now: f64) -> Replay {
563        let frame_no = self.frame_no;
564        let nodes = &mut self.nodes;
565        let held = &mut self.held;
566        let mut plays = Vec::with_capacity(self.ghosts.len());
567        let mut mask = 0u64;
568        self.ghosts.retain_mut(|g| match g.playback(now) {
569            Some(play) => {
570                g.last_used = frame_no;
571                if let Some(k) = g.place.mask_key() {
572                    mask |= 1u64 << (k.0 & 63);
573                }
574                plays.push(play);
575                true
576            }
577            None => {
578                *nodes -= g.nodes.len();
579                held.remove(&g.key);
580                false
581            }
582        });
583        self.active = !self.ghosts.is_empty();
584        Replay {
585            painted: vec![false; self.ghosts.len()],
586            ghosts: std::mem::take(&mut self.ghosts),
587            plays,
588            mask,
589        }
590    }
591
592    /// The other half of [`Self::begin_replay`]. Nothing departs between
593    /// the two — the diff runs before the passes — so nothing can have
594    /// been pushed while the ghosts were out.
595    pub(crate) fn end_replay(&mut self, replay: Replay) {
596        debug_assert!(self.ghosts.is_empty());
597        self.ghosts = replay.ghosts;
598        // `held` is what lets `depart` skip the walk, so it has to be the
599        // ghosts' keys exactly: a key missing from it is a retire that
600        // will not happen, which is two pictures of one node. Checked here
601        // because every frame that replays passes through.
602        debug_assert_eq!(self.held.len(), self.ghosts.len());
603    }
604
605    /// Every still-running ghost handed to `emit` in store order, and the
606    /// finished ones dropped — the replay without the passes, for a test
607    /// that has no frame to paint.
608    #[cfg(test)]
609    pub(crate) fn replay(&mut self, now: f64, mut emit: impl FnMut(&Ghost, &Playback)) {
610        let mut replay = self.begin_replay(now);
611        replay.paint(At::Top, &mut emit);
612        self.end_replay(replay);
613    }
614
615    /// Drops every ghost. The frame driver has no reason to; a test that
616    /// wants a clean slate does.
617    pub fn clear(&mut self) {
618        self.ghosts.clear();
619        self.held.clear();
620        self.nodes = 0;
621        self.active = false;
622    }
623}
624
625/// One frame's ghosts, out of the store for the length of the emission
626/// passes (see [`DepartStore::begin_replay`]). The passes ask for the
627/// ghosts under each node as they reach it, and for the rest of a pass
628/// once they are through it.
629#[derive(Default)]
630pub(crate) struct Replay {
631    ghosts: Vec<Ghost>,
632    plays: Vec<Playback>,
633    /// Painted this frame already: a ghost is painted once, whichever of
634    /// the two asks finds it first.
635    painted: Vec<bool>,
636    /// A membership mask over the `before` keys, so a pass answers
637    /// "nothing under this node" with one AND for almost every node
638    /// rather than a walk over the ghosts.
639    mask: u64,
640}
641
642impl Replay {
643    pub fn is_empty(&self) -> bool {
644        self.ghosts.is_empty()
645    }
646
647    /// Whether any ghost *may* be painted just under `key`: false is
648    /// certain, true is worth the walk [`Self::paint`] makes.
649    #[inline]
650    pub fn may_precede(&self, key: Key) -> bool {
651        self.mask & (1u64 << (key.0 & 63)) != 0
652    }
653
654    /// Hands `emit` the ghosts whose place is `at` — and, for a layer's
655    /// end, every ghost of that layer not painted yet, which is where a
656    /// ghost whose `before` node is gone ends up: at the end of its layer,
657    /// still under everything above it. `At::Top` takes what is left,
658    /// which is a ghost whose whole layer is gone.
659    pub fn paint(&mut self, at: At, mut emit: impl FnMut(&Ghost, &Playback)) {
660        for i in 0..self.ghosts.len() {
661            if self.painted[i] {
662                continue;
663            }
664            let here = match (at, self.ghosts[i].place) {
665                (At::UnderInFlow(k), Place::InFlow { before }) => before == Some(k),
666                (At::InFlowEnd, Place::InFlow { .. }) => true,
667                (At::UnderLayer(k), Place::Layer { before }) => before == Some(k),
668                (At::UnderInLayer(k), Place::InLayer { before, .. }) => before == Some(k),
669                (At::LayerEnd(k), Place::InLayer { layer, .. }) => layer == k,
670                (At::Top, _) => true,
671                _ => false,
672            };
673            if !here {
674                continue;
675            }
676            self.painted[i] = true;
677            emit(&self.ghosts[i], &self.plays[i]);
678        }
679    }
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    /// The core's frame counter, stood in for: each test's frames count
687    /// from one.
688    struct Frames(u64);
689    impl Frames {
690        fn next(&mut self) -> u64 {
691            self.0 += 1;
692            self.0
693        }
694    }
695    use crate::tree::OriginId;
696
697    const IN_FLOW: Place = Place::InFlow { before: None };
698
699    fn tree_with(spec: NodeSpec, children: usize) -> Tree {
700        let mut t = Tree::new();
701        let root = t.push(
702            NIL,
703            Key::ROOT,
704            OriginId::HOST,
705            NodeSpec::default(),
706            NodeContent::Container,
707        );
708        let node = t.push(
709            root,
710            Key::ROOT.str("x"),
711            OriginId::HOST,
712            spec,
713            NodeContent::Container,
714        );
715        for i in 0..children {
716            t.push(
717                node,
718                Key::ROOT.str("x").index(i as u64),
719                OriginId::HOST,
720                NodeSpec::default(),
721                NodeContent::Container,
722            );
723        }
724        t
725    }
726
727    fn departing(spec: NodeSpec) -> NodeSpec {
728        spec.transition(100.0)
729            .exit(Enter::from(50.0, 0.0).opacity(0.0))
730    }
731
732    #[test]
733    fn a_ghost_plays_out_and_then_goes() {
734        let mut d = DepartStore::default();
735        let mut frame = Frames(0);
736        let text = crate::text::TextSystem::new();
737        let lines = crate::line::LineStore::default();
738        let fragments = crate::fragment::FragmentList::default();
739        let paths = crate::path::PathStore::default();
740        let tree = tree_with(departing(NodeSpec::column()), 2);
741        d.begin_frame(frame.next());
742        d.depart(
743            &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths, None,
744        );
745        assert_eq!(d.node_count(), 3, "the subtree, not just its root");
746
747        let mut seen = Vec::new();
748        d.begin_frame(frame.next());
749        d.replay(0.05, |_, p| seen.push(p.offset.x));
750        assert!(d.animating());
751        assert_eq!(seen.len(), 1);
752        assert!(seen[0] > 0.0 && seen[0] < 50.0, "halfway out: {}", seen[0]);
753
754        d.begin_frame(frame.next());
755        d.replay(0.2, |_, _| panic!("the exit is over"));
756        assert!(!d.animating());
757        assert!(d.is_empty());
758        assert_eq!(d.node_count(), 0);
759    }
760
761    #[test]
762    fn a_key_that_comes_back_takes_its_ghost_with_it() {
763        let mut d = DepartStore::default();
764        let mut frame = Frames(0);
765        let text = crate::text::TextSystem::new();
766        let lines = crate::line::LineStore::default();
767        let fragments = crate::fragment::FragmentList::default();
768        let paths = crate::path::PathStore::default();
769        let tree = tree_with(departing(NodeSpec::column()), 0);
770        d.begin_frame(frame.next());
771        d.depart(
772            &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths, None,
773        );
774        assert_eq!(d.keys().collect::<Vec<_>>(), vec![Key::ROOT.str("x")]);
775        d.retire(Key::ROOT.str("x"));
776        assert!(d.is_empty());
777        assert_eq!(d.node_count(), 0);
778    }
779
780    /// One key departing twice with no return in between: the second
781    /// picture replaces the first rather than joining it. The frame path
782    /// cannot produce this — `collect_departures` retires a returning key
783    /// first — so this is the only cover the `retire` inside `depart` has,
784    /// and it is what says the `held` guard (ADR 0012 decision 5) does not
785    /// skip a retire it owed.
786    #[test]
787    fn a_second_departure_of_one_key_replaces_the_first() {
788        let mut d = DepartStore::default();
789        let mut frame = Frames(0);
790        let text = crate::text::TextSystem::new();
791        let lines = crate::line::LineStore::default();
792        let fragments = crate::fragment::FragmentList::default();
793        let paths = crate::path::PathStore::default();
794        let tree = tree_with(departing(NodeSpec::column()), 2);
795        d.begin_frame(frame.next());
796        d.depart(
797            &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths, None,
798        );
799        assert_eq!(d.keys().count(), 1);
800        assert_eq!(d.node_count(), 3);
801
802        d.depart(
803            &tree, 1, 0.05, 1.0, IN_FLOW, &text, &lines, &fragments, &paths, None,
804        );
805        assert_eq!(d.keys().count(), 1, "one picture of one node, not two");
806        assert_eq!(d.node_count(), 3, "and the budget charged once for it");
807    }
808
809    #[test]
810    fn a_node_without_both_halves_never_departs() {
811        let text = crate::text::TextSystem::new();
812        let lines = crate::line::LineStore::default();
813        let fragments = crate::fragment::FragmentList::default();
814        let paths = crate::path::PathStore::default();
815        for spec in [
816            NodeSpec::column(),
817            NodeSpec::column().transition(100.0),
818            // An `exit` with no duration to run over.
819            NodeSpec::column()
820                .exit(Enter::from(10.0, 0.0))
821                .transition(0.0),
822        ] {
823            assert!(!can_depart(&spec), "and the diff never counts it");
824            let mut d = DepartStore::default();
825            let mut frame = Frames(0);
826            let tree = tree_with(spec, 0);
827            d.begin_frame(frame.next());
828            d.depart(
829                &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths, None,
830            );
831            assert!(d.is_empty());
832        }
833    }
834
835    /// `count` subtrees of 16 nodes each, keyed `ROOT[i]` from `from`,
836    /// departed one after another at `now` — the way `collect_departures`
837    /// feeds a frame's admitted removal to the store.
838    fn depart_sixteens(d: &mut DepartStore, from: u64, count: u64, now: f64) {
839        let text = crate::text::TextSystem::new();
840        let lines = crate::line::LineStore::default();
841        let fragments = crate::fragment::FragmentList::default();
842        let paths = crate::path::PathStore::default();
843        let spec = departing(NodeSpec::column());
844        for i in from..from + count {
845            let mut t = Tree::new();
846            let root = t.push(
847                NIL,
848                Key::ROOT,
849                OriginId::HOST,
850                NodeSpec::default(),
851                NodeContent::Container,
852            );
853            let node = t.push(
854                root,
855                Key::ROOT.index(i),
856                OriginId::HOST,
857                spec.clone(),
858                NodeContent::Container,
859            );
860            for c in 0..15 {
861                t.push(
862                    node,
863                    Key::ROOT.index(i).index(c),
864                    OriginId::HOST,
865                    NodeSpec::default(),
866                    NodeContent::Container,
867                );
868            }
869            d.depart(
870                &t, 1, now, 1.0, IN_FLOW, &text, &lines, &fragments, &paths, None,
871            );
872        }
873    }
874
875    /// ADR 0012 decision 2: a removal is judged whole. One larger than the
876    /// budget is refused before anything is copied, and the store is
877    /// exactly what it was.
878    #[test]
879    fn a_removal_over_the_budget_is_refused_whole() {
880        let mut d = DepartStore::default();
881        let mut frame = Frames(0);
882        d.begin_frame(frame.next());
883        assert!(d.admit(3 * 16));
884        depart_sixteens(&mut d, 0, 3, 0.0);
885        d.begin_frame(frame.next());
886        assert!(!d.admit(MAX_NODES + 1), "one node past the budget");
887        assert_eq!(d.keys().count(), 3, "and the store was not touched");
888        assert_eq!(d.node_count(), 48);
889        assert!(d.admit(MAX_NODES), "the budget itself fits an empty store");
890    }
891
892    /// ADR 0012 decision 3: a removal that fits the budget but not the
893    /// room beside earlier exits evicts those, oldest first, and exactly
894    /// as many as it needs.
895    #[test]
896    fn a_new_removal_evicts_the_oldest_ghosts_until_it_fits() {
897        let mut d = DepartStore::default();
898        let mut frame = Frames(0);
899        // Subtrees of 16 in flight, keyed ROOT[0..n], filling the store to
900        // 192 short of the budget.
901        let n = (MAX_NODES - 192) / 16;
902        d.begin_frame(frame.next());
903        assert!(d.admit(n * 16));
904        depart_sixteens(&mut d, 0, n as u64, 0.0);
905        assert_eq!(d.node_count(), MAX_NODES - 192);
906        // A frame wants 15 more of 16 = 240, 48 past the budget, so three
907        // ghosts have to go, and they are ROOT[0], [1], [2].
908        d.begin_frame(frame.next());
909        assert!(d.admit(15 * 16));
910        assert_eq!(
911            d.node_count(),
912            MAX_NODES - 192 - 48,
913            "three evicted, not four, not two"
914        );
915        assert_eq!(
916            d.keys().next(),
917            Some(Key::ROOT.index(3)),
918            "the oldest went first"
919        );
920        depart_sixteens(&mut d, 100_000, 15, 0.1);
921        assert_eq!(
922            d.node_count(),
923            MAX_NODES,
924            "full, with the new removal whole"
925        );
926        assert_eq!(d.keys().count(), n - 3 + 15);
927        assert!(d.held.contains(&Key::ROOT.index(100_014)));
928        assert!(!d.held.contains(&Key::ROOT.index(2)), "and `held` followed");
929    }
930
931    /// The case decisions 2 and 3 exist to leave alone: a removal that fits
932    /// beside what is in flight evicts nothing.
933    #[test]
934    fn a_removal_that_fits_evicts_nothing() {
935        let mut d = DepartStore::default();
936        let mut frame = Frames(0);
937        d.begin_frame(frame.next());
938        assert!(d.admit(16));
939        depart_sixteens(&mut d, 0, 1, 0.0);
940        d.begin_frame(frame.next());
941        assert!(d.admit(MAX_NODES - 16));
942        assert_eq!(d.keys().count(), 1);
943        assert_eq!(d.node_count(), 16);
944        assert!(d.admit(0), "and nothing wanted is always admitted");
945    }
946
947    #[test]
948    fn a_ghost_nobody_replays_is_swept() {
949        let mut d = DepartStore::default();
950        let mut frame = Frames(0);
951        let text = crate::text::TextSystem::new();
952        let lines = crate::line::LineStore::default();
953        let fragments = crate::fragment::FragmentList::default();
954        let paths = crate::path::PathStore::default();
955        let tree = tree_with(departing(NodeSpec::column()), 0);
956        d.begin_frame(frame.next());
957        d.depart(
958            &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths, None,
959        );
960        // Frames without a replay: the sweep runs on the 240th.
961        for _ in 0..480 {
962            d.begin_frame(frame.next());
963        }
964        assert!(d.is_empty(), "an unreplayed ghost does not live forever");
965        assert_eq!(d.node_count(), 0);
966    }
967
968    #[test]
969    fn springs_play_out_as_ease_out() {
970        let mut d = DepartStore::default();
971        let mut frame = Frames(0);
972        let text = crate::text::TextSystem::new();
973        let lines = crate::line::LineStore::default();
974        let fragments = crate::fragment::FragmentList::default();
975        let paths = crate::path::PathStore::default();
976        let spec = NodeSpec::column()
977            .transition(100.0)
978            .easing(Easing::Spring)
979            .exit(Enter::from(100.0, 0.0));
980        let tree = tree_with(spec, 0);
981        d.begin_frame(frame.next());
982        d.depart(
983            &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths, None,
984        );
985        let mut x = 0.0;
986        d.replay(0.05, |_, p| x = p.offset.x);
987        let expect = Easing::EaseOut.apply(0.5) * 100.0;
988        assert!((x - expect).abs() < 1e-3, "{x} != {expect}");
989    }
990
991    #[test]
992    fn the_exit_reads_an_enter_backwards() {
993        let mut d = DepartStore::default();
994        let mut frame = Frames(0);
995        let text = crate::text::TextSystem::new();
996        let lines = crate::line::LineStore::default();
997        let fragments = crate::fragment::FragmentList::default();
998        let paths = crate::path::PathStore::default();
999        let spec = NodeSpec::column()
1000            .bg(Color::hex(0xff0000ff))
1001            .radius(10.0)
1002            .transition(100.0)
1003            .easing(Easing::Linear)
1004            .exit(
1005                Enter::default()
1006                    .bg(Color::hex(0xff000000))
1007                    .radius(0.0)
1008                    .opacity(0.0),
1009            );
1010        let mut tree = tree_with(spec, 0);
1011        tree.size[1] = crate::geom::Size::new(40.0, 20.0);
1012        d.begin_frame(frame.next());
1013        d.depart(
1014            &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths, None,
1015        );
1016        d.replay(0.05, |_, p| {
1017            assert!((p.opacity - 0.5).abs() < 1e-4, "halfway faded");
1018            assert!((p.bg.unwrap().a - 0.5).abs() < 1e-4, "halfway transparent");
1019            assert!((p.radius.unwrap()[0] - 5.0).abs() < 1e-4, "halfway square");
1020            assert!(p.size.is_none(), "an exit that names no size resizes none");
1021        });
1022    }
1023}