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}
226
227impl Ghost {
228 /// Where the exit has got to at `now`: `None` once it is over.
229 fn playback(&self, now: f64) -> Option<Playback> {
230 let raw = ((now - self.left_at) / self.duration) as f32;
231 if raw >= 1.0 || raw.is_nan() {
232 return None;
233 }
234 let p = self.easing.apply(raw.max(0.0));
235 let root = &self.nodes[0].spec;
236 let lerp = |from: f32, to: f32| from + (to - from) * p;
237 let e = &self.exit;
238 Some(Playback {
239 offset: Vec2::new(e.dx * p, e.dy * p),
240 bg: e.bg.map(|to| root.style.bg.lerp(to, p)),
241 radius: e
242 .radius
243 .map(|to| root.style.radius.map(|from| lerp(from, to))),
244 size: (e.width.is_some() || e.height.is_some()).then(|| {
245 let rect = self.nodes[0].rect;
246 (
247 e.width.and_then(Sizing::amount).map(|to| lerp(rect.w, to)),
248 e.height.and_then(Sizing::amount).map(|to| lerp(rect.h, to)),
249 )
250 }),
251 opacity: match e.opacity {
252 Some(to) => lerp(root.style.opacity, to),
253 None => root.style.opacity,
254 },
255 base_opacity: self.base_opacity,
256 })
257 }
258}
259
260/// The departing subtrees, bounded (see [`MAX_NODES`]).
261#[derive(Default)]
262pub struct DepartStore {
263 ghosts: Vec<Ghost>,
264 /// Nodes across every ghost, kept in step with `ghosts` so the budget
265 /// is a comparison rather than a walk.
266 nodes: usize,
267 /// The core's frame counter as of `begin_frame`.
268 frame_no: u64,
269 /// Whether a ghost replayed this frame is still mid-flight.
270 active: bool,
271 /// A membership mask over the ghosts' `before` keys, so a departure
272 /// whose key no ghost sits under — every row of a mass removal — skips
273 /// the walk that would hand its place on. Never cleared: a stale bit
274 /// costs one walk, and there are at most a few hundred ghosts to walk.
275 before_mask: u64,
276 /// The ghosts' own keys, as an exact set kept in step with `ghosts`,
277 /// so a departure of a key no ghost holds skips [`Self::retire`] —
278 /// which is a pass over the whole store, and a mass removal would
279 /// otherwise pay one per row. Exact rather than a 64-bit mask like
280 /// `before_mask`: a mass removal is hundreds of distinct keys, which
281 /// saturates 64 bits and makes a mask answer "maybe" every time.
282 held: rustc_hash::FxHashSet<Key>,
283}
284
285impl DepartStore {
286 /// Starts a frame under the core's counter.
287 pub(crate) fn begin_frame(&mut self, frame_no: u64) {
288 self.frame_no = frame_no;
289 self.active = false;
290 // A ghost not replayed for a while goes — the same backstop the
291 // anim store keeps, for a driver whose clock stops moving while
292 // frames keep coming (`retain::sweep_cutoff`).
293 if !self.ghosts.is_empty()
294 && let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
295 {
296 self.drop_where(|g| g.last_used < cutoff);
297 }
298 }
299
300 /// True when a ghost replayed this frame is still mid-flight, i.e. the
301 /// driver owes another frame.
302 pub fn animating(&self) -> bool {
303 self.active
304 }
305
306 /// Whether anything is departing at all — the gate every pass that
307 /// would otherwise walk an empty store checks first.
308 pub fn is_empty(&self) -> bool {
309 self.ghosts.is_empty()
310 }
311
312 /// How many nodes are retained across every departing subtree.
313 pub fn node_count(&self) -> usize {
314 self.nodes
315 }
316
317 /// The keys of the departing roots — what a caller tests the live tree
318 /// against to notice one coming back.
319 pub fn keys(&self) -> impl Iterator<Item = Key> + '_ {
320 self.ghosts.iter().map(|g| g.key)
321 }
322
323 /// Each departing root's key and the spec it left with — what a
324 /// trace names a departure by, since its node is no longer in any
325 /// tree.
326 pub(crate) fn roots(&self) -> impl Iterator<Item = (Key, &NodeSpec)> + '_ {
327 self.ghosts
328 .iter()
329 .filter_map(|g| g.nodes.first().map(|n| (g.key, &n.spec)))
330 }
331
332 fn drop_where(&mut self, mut pred: impl FnMut(&Ghost) -> bool) {
333 let nodes = &mut self.nodes;
334 let held = &mut self.held;
335 self.ghosts.retain(|g| {
336 let drop = pred(g);
337 if drop {
338 *nodes -= g.nodes.len();
339 held.remove(&g.key);
340 }
341 !drop
342 });
343 }
344
345 /// A key the view declared again: the live node wins and its ghost is
346 /// discarded, so a dismissed and re-shown toast does not draw twice.
347 pub(crate) fn retire(&mut self, key: Key) {
348 self.drop_where(|g| g.key == key);
349 }
350
351 /// The same, for every ghost whose key is in `live` — the whole of a
352 /// frame's returns in one pass.
353 pub(crate) fn retire_returned(&mut self, live: &rustc_hash::FxHashSet<Key>) {
354 if !live.is_empty() {
355 self.drop_where(|g| live.contains(&g.key));
356 }
357 }
358
359 /// The budget, applied to a frame's removal whole: `wanted` is every node the frame's departing subtrees
360 /// would add together, counted before any of them is copied. A removal
361 /// that fits an empty store is admitted — and if the store is holding
362 /// earlier exits it has no room beside, the **oldest** ghosts go first
363 /// until it fits, since they are the ones furthest through their own
364 /// fade and the removal the user just caused is the one they are
365 /// looking at. A removal larger than [`MAX_NODES`] on its own is refused
366 /// whole: `false`, no ghost, and the caller raises `exit-budget` for
367 /// the frame. There is no partial credit either way, so a view never
368 /// gets half its rows animating and the rest blinking, and how a
369 /// removal reads cannot depend on what else the app happened to be
370 /// doing 100 ms earlier.
371 pub(crate) fn admit(&mut self, wanted: usize) -> bool {
372 if wanted > MAX_NODES {
373 return false;
374 }
375 let mut evict = 0;
376 while self.nodes + wanted > MAX_NODES {
377 // Store order is departure order, so the front is the oldest.
378 let g = &self.ghosts[evict];
379 self.nodes -= g.nodes.len();
380 self.held.remove(&g.key);
381 evict += 1;
382 }
383 if evict > 0 {
384 self.ghosts.drain(..evict);
385 }
386 true
387 }
388
389 /// Copies `root`'s subtree out of `tree` (which is the *previous*
390 /// frame's, the last one that had it) and starts its exit at `now`.
391 /// `text` is read for that same frame's list, since the subtree's text
392 /// nodes carry its ids.
393 /// The budget is not checked here: the caller has counted the frame's
394 /// whole removal and had it admitted ([`Self::admit`]) before copying
395 /// any of it, so a subtree that reaches this always fits.
396 // The two lists at the end are the frame's per-node side tables, read
397 // for the same kept frame `tree` is; a struct for the pair would name
398 // one thing that only exists here.
399 #[allow(clippy::too_many_arguments)]
400 pub(crate) fn depart(
401 &mut self,
402 tree: &Tree,
403 root: usize,
404 now: f64,
405 base_opacity: f32,
406 place: Place,
407 text: &crate::text::TextSystem,
408 lines: &crate::line::LineStore,
409 fragments: &crate::fragment::FragmentList,
410 paths: &crate::path::PathStore,
411 ) {
412 let spec = &tree.specs[root];
413 let (Some(t), Some(exit)) = (spec.transition, spec.anim().exit) else {
414 return;
415 };
416 let duration = t.duration_ms.max(0.0) as f64 / 1000.0;
417 if duration <= 0.0 {
418 return;
419 }
420 let end = tree.subtree_end(root);
421 let key = tree.keys[root];
422 // A second departure of the same key (the view showed it, dropped
423 // it, showed it and dropped it again inside one exit) replaces the
424 // first: two pictures of one node are never right. Behind `held`
425 // because `retire` walks the whole store: unguarded, a frame that
426 // drops a thousand rows pays one walk per row, which is the one
427 // quadratic left in a mass removal — 32 ms for 10k subtrees
428 // against 1.05 ms guarded, and 44% of the departing frame at
429 // today's budget. Kept rather than deleted even though the frame
430 // path cannot reach it — `collect_departures` retires a returning
431 // key before it can depart again — because that argument runs
432 // through another module's early returns, and a set lookup is a
433 // cheap thing to be wrong about.
434 if self.held.contains(&key) {
435 self.retire(key);
436 }
437 let mut points = Vec::new();
438 let mut ops = Vec::new();
439 let nodes = (root..end)
440 .map(|i| GhostNode {
441 parent: if i == root {
442 NIL
443 } else {
444 tree.parent[i] - root as u32
445 },
446 spec: tree.specs[i].clone(),
447 content: match tree.content[i] {
448 NodeContent::Container => GhostContent::Container,
449 NodeContent::Text(id) => {
450 let (cache_key, color) = text.prev_frame_text(id);
451 GhostContent::Text { cache_key, color }
452 }
453 NodeContent::Edit(k) => GhostContent::Edit(k),
454 NodeContent::Image(id, opts) => GhostContent::Image(id, opts),
455 // A departing grid is its box: the cells are the
456 // frame's and go with it.
457 NodeContent::Cells(_) => GhostContent::Container,
458 // A departing fragment keeps painting. Its draw is
459 // sixteen floats and a handle, fixed-size and `Copy`,
460 // so the ghost owns a copy outright rather than
461 // indexing a list that has to outlive the frame —
462 // which is what the fixed sixteen buys. The copy comes
463 // from the previous frame's list, because that is the
464 // tree this ghost is being cut out of.
465 NodeContent::Fragment(id) => match fragments.prev_get(id) {
466 Some(draw) => GhostContent::Fragment(draw),
467 None => GhostContent::Container,
468 },
469 NodeContent::Polygon(id) => match fragments.prev_get(id) {
470 Some(draw) => GhostContent::Polygon(draw),
471 None => GhostContent::Container,
472 },
473 NodeContent::Line(id) => {
474 let (run, pts) = lines.prev_run(id);
475 let first = points.len() as u32;
476 points.extend_from_slice(pts);
477 GhostContent::Line {
478 first,
479 len: pts.len() as u32,
480 width: run.width,
481 dash: run.dash,
482 }
483 }
484 NodeContent::Path(id) => match paths.prev_run(id) {
485 Some((run, run_ops)) => {
486 let first = ops.len() as u32;
487 ops.extend_from_slice(run_ops);
488 GhostContent::Path {
489 first,
490 len: run_ops.len() as u32,
491 rule: run.rule,
492 stroke_w: run.stroke_w,
493 dash: run.dash,
494 hash: run.hash,
495 angle: run.angle,
496 }
497 }
498 None => GhostContent::Container,
499 },
500 },
501 rect: Rect::from_pos_size(tree.pos[i], tree.size[i]),
502 })
503 .collect::<Vec<_>>();
504 debug_assert!(
505 self.nodes + nodes.len() <= MAX_NODES,
506 "a departure the frame did not have admitted"
507 );
508 self.nodes += nodes.len();
509 // A ghost that was painted just under this node loses its place
510 // with it, and takes the place this one is taking: the two stay in
511 // the order they had, since the store keeps departures in order.
512 if self.before_mask & (1u64 << (key.0 & 63)) != 0 {
513 for g in &mut self.ghosts {
514 if g.place.before() == Some(key) {
515 g.place = g.place.with_before(place.before());
516 }
517 }
518 }
519 if let Some(before) = place.before() {
520 self.before_mask |= 1u64 << (before.0 & 63);
521 }
522 self.held.insert(key);
523 self.ghosts.push(Ghost {
524 key,
525 nodes,
526 points,
527 ops,
528 place,
529 left_at: now,
530 duration,
531 easing: t.curve(),
532 exit,
533 base_opacity,
534 last_used: self.frame_no,
535 });
536 }
537
538 /// Starts this frame's replay: drops the ghosts whose exit is over,
539 /// and hands the rest out with where each has got to at `now`, taken
540 /// out of the store so the emitter can borrow the rest of the core
541 /// while it paints them between the live nodes. [`Self::end_replay`]
542 /// puts them back.
543 pub(crate) fn begin_replay(&mut self, now: f64) -> Replay {
544 let frame_no = self.frame_no;
545 let nodes = &mut self.nodes;
546 let held = &mut self.held;
547 let mut plays = Vec::with_capacity(self.ghosts.len());
548 let mut mask = 0u64;
549 self.ghosts.retain_mut(|g| match g.playback(now) {
550 Some(play) => {
551 g.last_used = frame_no;
552 if let Some(k) = g.place.mask_key() {
553 mask |= 1u64 << (k.0 & 63);
554 }
555 plays.push(play);
556 true
557 }
558 None => {
559 *nodes -= g.nodes.len();
560 held.remove(&g.key);
561 false
562 }
563 });
564 self.active = !self.ghosts.is_empty();
565 Replay {
566 painted: vec![false; self.ghosts.len()],
567 ghosts: std::mem::take(&mut self.ghosts),
568 plays,
569 mask,
570 }
571 }
572
573 /// The other half of [`Self::begin_replay`]. Nothing departs between
574 /// the two — the diff runs before the passes — so nothing can have
575 /// been pushed while the ghosts were out.
576 pub(crate) fn end_replay(&mut self, replay: Replay) {
577 debug_assert!(self.ghosts.is_empty());
578 self.ghosts = replay.ghosts;
579 // `held` is what lets `depart` skip the walk, so it has to be the
580 // ghosts' keys exactly: a key missing from it is a retire that
581 // will not happen, which is two pictures of one node. Checked here
582 // because every frame that replays passes through.
583 debug_assert_eq!(self.held.len(), self.ghosts.len());
584 }
585
586 /// Every still-running ghost handed to `emit` in store order, and the
587 /// finished ones dropped — the replay without the passes, for a test
588 /// that has no frame to paint.
589 #[cfg(test)]
590 pub(crate) fn replay(&mut self, now: f64, mut emit: impl FnMut(&Ghost, &Playback)) {
591 let mut replay = self.begin_replay(now);
592 replay.paint(At::Top, &mut emit);
593 self.end_replay(replay);
594 }
595
596 /// Drops every ghost. The frame driver has no reason to; a test that
597 /// wants a clean slate does.
598 pub fn clear(&mut self) {
599 self.ghosts.clear();
600 self.held.clear();
601 self.nodes = 0;
602 self.active = false;
603 }
604}
605
606/// One frame's ghosts, out of the store for the length of the emission
607/// passes (see [`DepartStore::begin_replay`]). The passes ask for the
608/// ghosts under each node as they reach it, and for the rest of a pass
609/// once they are through it.
610#[derive(Default)]
611pub(crate) struct Replay {
612 ghosts: Vec<Ghost>,
613 plays: Vec<Playback>,
614 /// Painted this frame already: a ghost is painted once, whichever of
615 /// the two asks finds it first.
616 painted: Vec<bool>,
617 /// A membership mask over the `before` keys, so a pass answers
618 /// "nothing under this node" with one AND for almost every node
619 /// rather than a walk over the ghosts.
620 mask: u64,
621}
622
623impl Replay {
624 pub fn is_empty(&self) -> bool {
625 self.ghosts.is_empty()
626 }
627
628 /// Whether any ghost *may* be painted just under `key`: false is
629 /// certain, true is worth the walk [`Self::paint`] makes.
630 #[inline]
631 pub fn may_precede(&self, key: Key) -> bool {
632 self.mask & (1u64 << (key.0 & 63)) != 0
633 }
634
635 /// Hands `emit` the ghosts whose place is `at` — and, for a layer's
636 /// end, every ghost of that layer not painted yet, which is where a
637 /// ghost whose `before` node is gone ends up: at the end of its layer,
638 /// still under everything above it. `At::Top` takes what is left,
639 /// which is a ghost whose whole layer is gone.
640 pub fn paint(&mut self, at: At, mut emit: impl FnMut(&Ghost, &Playback)) {
641 for i in 0..self.ghosts.len() {
642 if self.painted[i] {
643 continue;
644 }
645 let here = match (at, self.ghosts[i].place) {
646 (At::UnderInFlow(k), Place::InFlow { before }) => before == Some(k),
647 (At::InFlowEnd, Place::InFlow { .. }) => true,
648 (At::UnderLayer(k), Place::Layer { before }) => before == Some(k),
649 (At::UnderInLayer(k), Place::InLayer { before, .. }) => before == Some(k),
650 (At::LayerEnd(k), Place::InLayer { layer, .. }) => layer == k,
651 (At::Top, _) => true,
652 _ => false,
653 };
654 if !here {
655 continue;
656 }
657 self.painted[i] = true;
658 emit(&self.ghosts[i], &self.plays[i]);
659 }
660 }
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666
667 /// The core's frame counter, stood in for: each test's frames count
668 /// from one.
669 struct Frames(u64);
670 impl Frames {
671 fn next(&mut self) -> u64 {
672 self.0 += 1;
673 self.0
674 }
675 }
676 use crate::tree::OriginId;
677
678 const IN_FLOW: Place = Place::InFlow { before: None };
679
680 fn tree_with(spec: NodeSpec, children: usize) -> Tree {
681 let mut t = Tree::new();
682 let root = t.push(
683 NIL,
684 Key::ROOT,
685 OriginId::HOST,
686 NodeSpec::default(),
687 NodeContent::Container,
688 );
689 let node = t.push(
690 root,
691 Key::ROOT.str("x"),
692 OriginId::HOST,
693 spec,
694 NodeContent::Container,
695 );
696 for i in 0..children {
697 t.push(
698 node,
699 Key::ROOT.str("x").index(i as u64),
700 OriginId::HOST,
701 NodeSpec::default(),
702 NodeContent::Container,
703 );
704 }
705 t
706 }
707
708 fn departing(spec: NodeSpec) -> NodeSpec {
709 spec.transition(100.0)
710 .exit(Enter::from(50.0, 0.0).opacity(0.0))
711 }
712
713 #[test]
714 fn a_ghost_plays_out_and_then_goes() {
715 let mut d = DepartStore::default();
716 let mut frame = Frames(0);
717 let text = crate::text::TextSystem::new();
718 let lines = crate::line::LineStore::default();
719 let fragments = crate::fragment::FragmentList::default();
720 let paths = crate::path::PathStore::default();
721 let tree = tree_with(departing(NodeSpec::column()), 2);
722 d.begin_frame(frame.next());
723 d.depart(
724 &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
725 );
726 assert_eq!(d.node_count(), 3, "the subtree, not just its root");
727
728 let mut seen = Vec::new();
729 d.begin_frame(frame.next());
730 d.replay(0.05, |_, p| seen.push(p.offset.x));
731 assert!(d.animating());
732 assert_eq!(seen.len(), 1);
733 assert!(seen[0] > 0.0 && seen[0] < 50.0, "halfway out: {}", seen[0]);
734
735 d.begin_frame(frame.next());
736 d.replay(0.2, |_, _| panic!("the exit is over"));
737 assert!(!d.animating());
738 assert!(d.is_empty());
739 assert_eq!(d.node_count(), 0);
740 }
741
742 #[test]
743 fn a_key_that_comes_back_takes_its_ghost_with_it() {
744 let mut d = DepartStore::default();
745 let mut frame = Frames(0);
746 let text = crate::text::TextSystem::new();
747 let lines = crate::line::LineStore::default();
748 let fragments = crate::fragment::FragmentList::default();
749 let paths = crate::path::PathStore::default();
750 let tree = tree_with(departing(NodeSpec::column()), 0);
751 d.begin_frame(frame.next());
752 d.depart(
753 &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
754 );
755 assert_eq!(d.keys().collect::<Vec<_>>(), vec![Key::ROOT.str("x")]);
756 d.retire(Key::ROOT.str("x"));
757 assert!(d.is_empty());
758 assert_eq!(d.node_count(), 0);
759 }
760
761 /// One key departing twice with no return in between: the second
762 /// picture replaces the first rather than joining it. The frame path
763 /// cannot produce this — `collect_departures` retires a returning key
764 /// first — so this is the only cover the `retire` inside `depart` has,
765 /// and it is what says the `held` guard (ADR 0012 decision 5) does not
766 /// skip a retire it owed.
767 #[test]
768 fn a_second_departure_of_one_key_replaces_the_first() {
769 let mut d = DepartStore::default();
770 let mut frame = Frames(0);
771 let text = crate::text::TextSystem::new();
772 let lines = crate::line::LineStore::default();
773 let fragments = crate::fragment::FragmentList::default();
774 let paths = crate::path::PathStore::default();
775 let tree = tree_with(departing(NodeSpec::column()), 2);
776 d.begin_frame(frame.next());
777 d.depart(
778 &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
779 );
780 assert_eq!(d.keys().count(), 1);
781 assert_eq!(d.node_count(), 3);
782
783 d.depart(
784 &tree, 1, 0.05, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
785 );
786 assert_eq!(d.keys().count(), 1, "one picture of one node, not two");
787 assert_eq!(d.node_count(), 3, "and the budget charged once for it");
788 }
789
790 #[test]
791 fn a_node_without_both_halves_never_departs() {
792 let text = crate::text::TextSystem::new();
793 let lines = crate::line::LineStore::default();
794 let fragments = crate::fragment::FragmentList::default();
795 let paths = crate::path::PathStore::default();
796 for spec in [
797 NodeSpec::column(),
798 NodeSpec::column().transition(100.0),
799 // An `exit` with no duration to run over.
800 NodeSpec::column()
801 .exit(Enter::from(10.0, 0.0))
802 .transition(0.0),
803 ] {
804 assert!(!can_depart(&spec), "and the diff never counts it");
805 let mut d = DepartStore::default();
806 let mut frame = Frames(0);
807 let tree = tree_with(spec, 0);
808 d.begin_frame(frame.next());
809 d.depart(
810 &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
811 );
812 assert!(d.is_empty());
813 }
814 }
815
816 /// `count` subtrees of 16 nodes each, keyed `ROOT[i]` from `from`,
817 /// departed one after another at `now` — the way `collect_departures`
818 /// feeds a frame's admitted removal to the store.
819 fn depart_sixteens(d: &mut DepartStore, from: u64, count: u64, now: f64) {
820 let text = crate::text::TextSystem::new();
821 let lines = crate::line::LineStore::default();
822 let fragments = crate::fragment::FragmentList::default();
823 let paths = crate::path::PathStore::default();
824 let spec = departing(NodeSpec::column());
825 for i in from..from + count {
826 let mut t = Tree::new();
827 let root = t.push(
828 NIL,
829 Key::ROOT,
830 OriginId::HOST,
831 NodeSpec::default(),
832 NodeContent::Container,
833 );
834 let node = t.push(
835 root,
836 Key::ROOT.index(i),
837 OriginId::HOST,
838 spec.clone(),
839 NodeContent::Container,
840 );
841 for c in 0..15 {
842 t.push(
843 node,
844 Key::ROOT.index(i).index(c),
845 OriginId::HOST,
846 NodeSpec::default(),
847 NodeContent::Container,
848 );
849 }
850 d.depart(&t, 1, now, 1.0, IN_FLOW, &text, &lines, &fragments, &paths);
851 }
852 }
853
854 /// ADR 0012 decision 2: a removal is judged whole. One larger than the
855 /// budget is refused before anything is copied, and the store is
856 /// exactly what it was.
857 #[test]
858 fn a_removal_over_the_budget_is_refused_whole() {
859 let mut d = DepartStore::default();
860 let mut frame = Frames(0);
861 d.begin_frame(frame.next());
862 assert!(d.admit(3 * 16));
863 depart_sixteens(&mut d, 0, 3, 0.0);
864 d.begin_frame(frame.next());
865 assert!(!d.admit(MAX_NODES + 1), "one node past the budget");
866 assert_eq!(d.keys().count(), 3, "and the store was not touched");
867 assert_eq!(d.node_count(), 48);
868 assert!(d.admit(MAX_NODES), "the budget itself fits an empty store");
869 }
870
871 /// ADR 0012 decision 3: a removal that fits the budget but not the
872 /// room beside earlier exits evicts those, oldest first, and exactly
873 /// as many as it needs.
874 #[test]
875 fn a_new_removal_evicts_the_oldest_ghosts_until_it_fits() {
876 let mut d = DepartStore::default();
877 let mut frame = Frames(0);
878 // Subtrees of 16 in flight, keyed ROOT[0..n], filling the store to
879 // 192 short of the budget.
880 let n = (MAX_NODES - 192) / 16;
881 d.begin_frame(frame.next());
882 assert!(d.admit(n * 16));
883 depart_sixteens(&mut d, 0, n as u64, 0.0);
884 assert_eq!(d.node_count(), MAX_NODES - 192);
885 // A frame wants 15 more of 16 = 240, 48 past the budget, so three
886 // ghosts have to go, and they are ROOT[0], [1], [2].
887 d.begin_frame(frame.next());
888 assert!(d.admit(15 * 16));
889 assert_eq!(
890 d.node_count(),
891 MAX_NODES - 192 - 48,
892 "three evicted, not four, not two"
893 );
894 assert_eq!(
895 d.keys().next(),
896 Some(Key::ROOT.index(3)),
897 "the oldest went first"
898 );
899 depart_sixteens(&mut d, 100_000, 15, 0.1);
900 assert_eq!(
901 d.node_count(),
902 MAX_NODES,
903 "full, with the new removal whole"
904 );
905 assert_eq!(d.keys().count(), n - 3 + 15);
906 assert!(d.held.contains(&Key::ROOT.index(100_014)));
907 assert!(!d.held.contains(&Key::ROOT.index(2)), "and `held` followed");
908 }
909
910 /// The case decisions 2 and 3 exist to leave alone: a removal that fits
911 /// beside what is in flight evicts nothing.
912 #[test]
913 fn a_removal_that_fits_evicts_nothing() {
914 let mut d = DepartStore::default();
915 let mut frame = Frames(0);
916 d.begin_frame(frame.next());
917 assert!(d.admit(16));
918 depart_sixteens(&mut d, 0, 1, 0.0);
919 d.begin_frame(frame.next());
920 assert!(d.admit(MAX_NODES - 16));
921 assert_eq!(d.keys().count(), 1);
922 assert_eq!(d.node_count(), 16);
923 assert!(d.admit(0), "and nothing wanted is always admitted");
924 }
925
926 #[test]
927 fn a_ghost_nobody_replays_is_swept() {
928 let mut d = DepartStore::default();
929 let mut frame = Frames(0);
930 let text = crate::text::TextSystem::new();
931 let lines = crate::line::LineStore::default();
932 let fragments = crate::fragment::FragmentList::default();
933 let paths = crate::path::PathStore::default();
934 let tree = tree_with(departing(NodeSpec::column()), 0);
935 d.begin_frame(frame.next());
936 d.depart(
937 &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
938 );
939 // Frames without a replay: the sweep runs on the 240th.
940 for _ in 0..480 {
941 d.begin_frame(frame.next());
942 }
943 assert!(d.is_empty(), "an unreplayed ghost does not live forever");
944 assert_eq!(d.node_count(), 0);
945 }
946
947 #[test]
948 fn springs_play_out_as_ease_out() {
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 spec = NodeSpec::column()
956 .transition(100.0)
957 .easing(Easing::Spring)
958 .exit(Enter::from(100.0, 0.0));
959 let tree = tree_with(spec, 0);
960 d.begin_frame(frame.next());
961 d.depart(
962 &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
963 );
964 let mut x = 0.0;
965 d.replay(0.05, |_, p| x = p.offset.x);
966 let expect = Easing::EaseOut.apply(0.5) * 100.0;
967 assert!((x - expect).abs() < 1e-3, "{x} != {expect}");
968 }
969
970 #[test]
971 fn the_exit_reads_an_enter_backwards() {
972 let mut d = DepartStore::default();
973 let mut frame = Frames(0);
974 let text = crate::text::TextSystem::new();
975 let lines = crate::line::LineStore::default();
976 let fragments = crate::fragment::FragmentList::default();
977 let paths = crate::path::PathStore::default();
978 let spec = NodeSpec::column()
979 .bg(Color::hex(0xff0000ff))
980 .radius(10.0)
981 .transition(100.0)
982 .easing(Easing::Linear)
983 .exit(
984 Enter::default()
985 .bg(Color::hex(0xff000000))
986 .radius(0.0)
987 .opacity(0.0),
988 );
989 let mut tree = tree_with(spec, 0);
990 tree.size[1] = crate::geom::Size::new(40.0, 20.0);
991 d.begin_frame(frame.next());
992 d.depart(
993 &tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
994 );
995 d.replay(0.05, |_, p| {
996 assert!((p.opacity - 0.5).abs() < 1e-4, "halfway faded");
997 assert!((p.bg.unwrap().a - 0.5).abs() < 1e-4, "halfway transparent");
998 assert!((p.radius.unwrap()[0] - 5.0).abs() < 1e-4, "halfway square");
999 assert!(p.size.is_none(), "an exit that names no size resizes none");
1000 });
1001 }
1002}