use crate::anim::Easing;
use crate::color::Color;
use crate::enter::Enter;
use crate::geom::{Rect, Vec2};
use crate::key::Key;
use crate::resources::ImageId;
use crate::spec::{NodeSpec, Sizing};
use crate::tree::{NIL, NodeContent, Tree};
pub const MAX_NODES: usize = 4096;
#[inline]
pub(crate) fn can_depart(spec: &NodeSpec) -> bool {
spec.anim().exit.is_some()
&& spec
.transition
.as_ref()
.is_some_and(|t| t.duration_ms > 0.0)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum GhostContent {
Container,
Text {
cache_key: u64,
color: Color,
},
Edit(Key),
Image(ImageId, crate::resources::ImageOpts),
Line {
first: u32,
len: u32,
width: f32,
dash: Option<crate::line::Cut>,
},
Fragment(crate::fragment::Draw),
Polygon(crate::fragment::Draw),
Path {
first: u32,
len: u32,
rule: crate::path::FillRule,
stroke_w: f32,
dash: Option<crate::line::Cut>,
hash: u64,
angle: Option<f32>,
},
}
pub(crate) struct GhostNode {
pub parent: u32,
pub spec: NodeSpec,
pub content: GhostContent,
pub rect: Rect,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Place {
InFlow { before: Option<Key> },
InLayer { layer: Key, before: Option<Key> },
Layer { before: Option<Key> },
}
impl Place {
fn mask_key(self) -> Option<Key> {
match self {
Place::InFlow { before } | Place::Layer { before } => before,
Place::InLayer { layer, before } => Some(before.unwrap_or(layer)),
}
}
fn before(self) -> Option<Key> {
match self {
Place::InFlow { before } | Place::Layer { before } | Place::InLayer { before, .. } => {
before
}
}
}
fn with_before(self, before: Option<Key>) -> Self {
match self {
Place::InFlow { .. } => Place::InFlow { before },
Place::InLayer { layer, .. } => Place::InLayer { layer, before },
Place::Layer { .. } => Place::Layer { before },
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum At {
UnderInFlow(Key),
InFlowEnd,
UnderLayer(Key),
UnderInLayer(Key),
LayerEnd(Key),
Top,
}
pub(crate) struct Ghost {
pub key: Key,
pub nodes: Vec<GhostNode>,
pub points: Vec<Vec2>,
pub ops: Vec<crate::path::PathOp>,
pub place: Place,
left_at: f64,
duration: f64,
easing: Easing,
exit: Enter,
base_opacity: f32,
last_used: u64,
}
pub(crate) struct Playback {
pub offset: Vec2,
pub bg: Option<Color>,
pub radius: Option<[f32; 4]>,
pub size: Option<(Option<f32>, Option<f32>)>,
pub opacity: f32,
pub base_opacity: f32,
}
impl Ghost {
fn playback(&self, now: f64) -> Option<Playback> {
let raw = ((now - self.left_at) / self.duration) as f32;
if raw >= 1.0 || raw.is_nan() {
return None;
}
let p = self.easing.apply(raw.max(0.0));
let root = &self.nodes[0].spec;
let lerp = |from: f32, to: f32| from + (to - from) * p;
let e = &self.exit;
Some(Playback {
offset: Vec2::new(e.dx * p, e.dy * p),
bg: e.bg.map(|to| root.style.bg.lerp(to, p)),
radius: e
.radius
.map(|to| root.style.radius.map(|from| lerp(from, to))),
size: (e.width.is_some() || e.height.is_some()).then(|| {
let rect = self.nodes[0].rect;
(
e.width.and_then(Sizing::amount).map(|to| lerp(rect.w, to)),
e.height.and_then(Sizing::amount).map(|to| lerp(rect.h, to)),
)
}),
opacity: match e.opacity {
Some(to) => lerp(root.style.opacity, to),
None => root.style.opacity,
},
base_opacity: self.base_opacity,
})
}
}
#[derive(Default)]
pub struct DepartStore {
ghosts: Vec<Ghost>,
nodes: usize,
frame_no: u64,
active: bool,
before_mask: u64,
held: rustc_hash::FxHashSet<Key>,
}
impl DepartStore {
pub(crate) fn begin_frame(&mut self, frame_no: u64) {
self.frame_no = frame_no;
self.active = false;
if !self.ghosts.is_empty()
&& let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
{
self.drop_where(|g| g.last_used < cutoff);
}
}
pub fn animating(&self) -> bool {
self.active
}
pub fn is_empty(&self) -> bool {
self.ghosts.is_empty()
}
pub fn node_count(&self) -> usize {
self.nodes
}
pub fn keys(&self) -> impl Iterator<Item = Key> + '_ {
self.ghosts.iter().map(|g| g.key)
}
pub(crate) fn roots(&self) -> impl Iterator<Item = (Key, &NodeSpec)> + '_ {
self.ghosts
.iter()
.filter_map(|g| g.nodes.first().map(|n| (g.key, &n.spec)))
}
fn drop_where(&mut self, mut pred: impl FnMut(&Ghost) -> bool) {
let nodes = &mut self.nodes;
let held = &mut self.held;
self.ghosts.retain(|g| {
let drop = pred(g);
if drop {
*nodes -= g.nodes.len();
held.remove(&g.key);
}
!drop
});
}
pub(crate) fn retire(&mut self, key: Key) {
self.drop_where(|g| g.key == key);
}
pub(crate) fn retire_returned(&mut self, live: &rustc_hash::FxHashSet<Key>) {
if !live.is_empty() {
self.drop_where(|g| live.contains(&g.key));
}
}
pub(crate) fn admit(&mut self, wanted: usize) -> bool {
if wanted > MAX_NODES {
return false;
}
let mut evict = 0;
while self.nodes + wanted > MAX_NODES {
let g = &self.ghosts[evict];
self.nodes -= g.nodes.len();
self.held.remove(&g.key);
evict += 1;
}
if evict > 0 {
self.ghosts.drain(..evict);
}
true
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn depart(
&mut self,
tree: &Tree,
root: usize,
now: f64,
base_opacity: f32,
place: Place,
text: &crate::text::TextSystem,
lines: &crate::line::LineStore,
fragments: &crate::fragment::FragmentList,
paths: &crate::path::PathStore,
) {
let spec = &tree.specs[root];
let (Some(t), Some(exit)) = (spec.transition, spec.anim().exit) else {
return;
};
let duration = t.duration_ms.max(0.0) as f64 / 1000.0;
if duration <= 0.0 {
return;
}
let end = tree.subtree_end(root);
let key = tree.keys[root];
if self.held.contains(&key) {
self.retire(key);
}
let mut points = Vec::new();
let mut ops = Vec::new();
let nodes = (root..end)
.map(|i| GhostNode {
parent: if i == root {
NIL
} else {
tree.parent[i] - root as u32
},
spec: tree.specs[i].clone(),
content: match tree.content[i] {
NodeContent::Container => GhostContent::Container,
NodeContent::Text(id) => {
let (cache_key, color) = text.prev_frame_text(id);
GhostContent::Text { cache_key, color }
}
NodeContent::Edit(k) => GhostContent::Edit(k),
NodeContent::Image(id, opts) => GhostContent::Image(id, opts),
NodeContent::Cells(_) => GhostContent::Container,
NodeContent::Fragment(id) => match fragments.prev_get(id) {
Some(draw) => GhostContent::Fragment(draw),
None => GhostContent::Container,
},
NodeContent::Polygon(id) => match fragments.prev_get(id) {
Some(draw) => GhostContent::Polygon(draw),
None => GhostContent::Container,
},
NodeContent::Line(id) => {
let (run, pts) = lines.prev_run(id);
let first = points.len() as u32;
points.extend_from_slice(pts);
GhostContent::Line {
first,
len: pts.len() as u32,
width: run.width,
dash: run.dash,
}
}
NodeContent::Path(id) => match paths.prev_run(id) {
Some((run, run_ops)) => {
let first = ops.len() as u32;
ops.extend_from_slice(run_ops);
GhostContent::Path {
first,
len: run_ops.len() as u32,
rule: run.rule,
stroke_w: run.stroke_w,
dash: run.dash,
hash: run.hash,
angle: run.angle,
}
}
None => GhostContent::Container,
},
},
rect: Rect::from_pos_size(tree.pos[i], tree.size[i]),
})
.collect::<Vec<_>>();
debug_assert!(
self.nodes + nodes.len() <= MAX_NODES,
"a departure the frame did not have admitted"
);
self.nodes += nodes.len();
if self.before_mask & (1u64 << (key.0 & 63)) != 0 {
for g in &mut self.ghosts {
if g.place.before() == Some(key) {
g.place = g.place.with_before(place.before());
}
}
}
if let Some(before) = place.before() {
self.before_mask |= 1u64 << (before.0 & 63);
}
self.held.insert(key);
self.ghosts.push(Ghost {
key,
nodes,
points,
ops,
place,
left_at: now,
duration,
easing: t.curve(),
exit,
base_opacity,
last_used: self.frame_no,
});
}
pub(crate) fn begin_replay(&mut self, now: f64) -> Replay {
let frame_no = self.frame_no;
let nodes = &mut self.nodes;
let held = &mut self.held;
let mut plays = Vec::with_capacity(self.ghosts.len());
let mut mask = 0u64;
self.ghosts.retain_mut(|g| match g.playback(now) {
Some(play) => {
g.last_used = frame_no;
if let Some(k) = g.place.mask_key() {
mask |= 1u64 << (k.0 & 63);
}
plays.push(play);
true
}
None => {
*nodes -= g.nodes.len();
held.remove(&g.key);
false
}
});
self.active = !self.ghosts.is_empty();
Replay {
painted: vec![false; self.ghosts.len()],
ghosts: std::mem::take(&mut self.ghosts),
plays,
mask,
}
}
pub(crate) fn end_replay(&mut self, replay: Replay) {
debug_assert!(self.ghosts.is_empty());
self.ghosts = replay.ghosts;
debug_assert_eq!(self.held.len(), self.ghosts.len());
}
#[cfg(test)]
pub(crate) fn replay(&mut self, now: f64, mut emit: impl FnMut(&Ghost, &Playback)) {
let mut replay = self.begin_replay(now);
replay.paint(At::Top, &mut emit);
self.end_replay(replay);
}
pub fn clear(&mut self) {
self.ghosts.clear();
self.held.clear();
self.nodes = 0;
self.active = false;
}
}
#[derive(Default)]
pub(crate) struct Replay {
ghosts: Vec<Ghost>,
plays: Vec<Playback>,
painted: Vec<bool>,
mask: u64,
}
impl Replay {
pub fn is_empty(&self) -> bool {
self.ghosts.is_empty()
}
#[inline]
pub fn may_precede(&self, key: Key) -> bool {
self.mask & (1u64 << (key.0 & 63)) != 0
}
pub fn paint(&mut self, at: At, mut emit: impl FnMut(&Ghost, &Playback)) {
for i in 0..self.ghosts.len() {
if self.painted[i] {
continue;
}
let here = match (at, self.ghosts[i].place) {
(At::UnderInFlow(k), Place::InFlow { before }) => before == Some(k),
(At::InFlowEnd, Place::InFlow { .. }) => true,
(At::UnderLayer(k), Place::Layer { before }) => before == Some(k),
(At::UnderInLayer(k), Place::InLayer { before, .. }) => before == Some(k),
(At::LayerEnd(k), Place::InLayer { layer, .. }) => layer == k,
(At::Top, _) => true,
_ => false,
};
if !here {
continue;
}
self.painted[i] = true;
emit(&self.ghosts[i], &self.plays[i]);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Frames(u64);
impl Frames {
fn next(&mut self) -> u64 {
self.0 += 1;
self.0
}
}
use crate::tree::OriginId;
const IN_FLOW: Place = Place::InFlow { before: None };
fn tree_with(spec: NodeSpec, children: usize) -> Tree {
let mut t = Tree::new();
let root = t.push(
NIL,
Key::ROOT,
OriginId::HOST,
NodeSpec::default(),
NodeContent::Container,
);
let node = t.push(
root,
Key::ROOT.str("x"),
OriginId::HOST,
spec,
NodeContent::Container,
);
for i in 0..children {
t.push(
node,
Key::ROOT.str("x").index(i as u64),
OriginId::HOST,
NodeSpec::default(),
NodeContent::Container,
);
}
t
}
fn departing(spec: NodeSpec) -> NodeSpec {
spec.transition(100.0)
.exit(Enter::from(50.0, 0.0).opacity(0.0))
}
#[test]
fn a_ghost_plays_out_and_then_goes() {
let mut d = DepartStore::default();
let mut frame = Frames(0);
let text = crate::text::TextSystem::new();
let lines = crate::line::LineStore::default();
let fragments = crate::fragment::FragmentList::default();
let paths = crate::path::PathStore::default();
let tree = tree_with(departing(NodeSpec::column()), 2);
d.begin_frame(frame.next());
d.depart(
&tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
);
assert_eq!(d.node_count(), 3, "the subtree, not just its root");
let mut seen = Vec::new();
d.begin_frame(frame.next());
d.replay(0.05, |_, p| seen.push(p.offset.x));
assert!(d.animating());
assert_eq!(seen.len(), 1);
assert!(seen[0] > 0.0 && seen[0] < 50.0, "halfway out: {}", seen[0]);
d.begin_frame(frame.next());
d.replay(0.2, |_, _| panic!("the exit is over"));
assert!(!d.animating());
assert!(d.is_empty());
assert_eq!(d.node_count(), 0);
}
#[test]
fn a_key_that_comes_back_takes_its_ghost_with_it() {
let mut d = DepartStore::default();
let mut frame = Frames(0);
let text = crate::text::TextSystem::new();
let lines = crate::line::LineStore::default();
let fragments = crate::fragment::FragmentList::default();
let paths = crate::path::PathStore::default();
let tree = tree_with(departing(NodeSpec::column()), 0);
d.begin_frame(frame.next());
d.depart(
&tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
);
assert_eq!(d.keys().collect::<Vec<_>>(), vec![Key::ROOT.str("x")]);
d.retire(Key::ROOT.str("x"));
assert!(d.is_empty());
assert_eq!(d.node_count(), 0);
}
#[test]
fn a_second_departure_of_one_key_replaces_the_first() {
let mut d = DepartStore::default();
let mut frame = Frames(0);
let text = crate::text::TextSystem::new();
let lines = crate::line::LineStore::default();
let fragments = crate::fragment::FragmentList::default();
let paths = crate::path::PathStore::default();
let tree = tree_with(departing(NodeSpec::column()), 2);
d.begin_frame(frame.next());
d.depart(
&tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
);
assert_eq!(d.keys().count(), 1);
assert_eq!(d.node_count(), 3);
d.depart(
&tree, 1, 0.05, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
);
assert_eq!(d.keys().count(), 1, "one picture of one node, not two");
assert_eq!(d.node_count(), 3, "and the budget charged once for it");
}
#[test]
fn a_node_without_both_halves_never_departs() {
let text = crate::text::TextSystem::new();
let lines = crate::line::LineStore::default();
let fragments = crate::fragment::FragmentList::default();
let paths = crate::path::PathStore::default();
for spec in [
NodeSpec::column(),
NodeSpec::column().transition(100.0),
NodeSpec::column()
.exit(Enter::from(10.0, 0.0))
.transition(0.0),
] {
assert!(!can_depart(&spec), "and the diff never counts it");
let mut d = DepartStore::default();
let mut frame = Frames(0);
let tree = tree_with(spec, 0);
d.begin_frame(frame.next());
d.depart(
&tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
);
assert!(d.is_empty());
}
}
fn depart_sixteens(d: &mut DepartStore, from: u64, count: u64, now: f64) {
let text = crate::text::TextSystem::new();
let lines = crate::line::LineStore::default();
let fragments = crate::fragment::FragmentList::default();
let paths = crate::path::PathStore::default();
let spec = departing(NodeSpec::column());
for i in from..from + count {
let mut t = Tree::new();
let root = t.push(
NIL,
Key::ROOT,
OriginId::HOST,
NodeSpec::default(),
NodeContent::Container,
);
let node = t.push(
root,
Key::ROOT.index(i),
OriginId::HOST,
spec.clone(),
NodeContent::Container,
);
for c in 0..15 {
t.push(
node,
Key::ROOT.index(i).index(c),
OriginId::HOST,
NodeSpec::default(),
NodeContent::Container,
);
}
d.depart(&t, 1, now, 1.0, IN_FLOW, &text, &lines, &fragments, &paths);
}
}
#[test]
fn a_removal_over_the_budget_is_refused_whole() {
let mut d = DepartStore::default();
let mut frame = Frames(0);
d.begin_frame(frame.next());
assert!(d.admit(3 * 16));
depart_sixteens(&mut d, 0, 3, 0.0);
d.begin_frame(frame.next());
assert!(!d.admit(MAX_NODES + 1), "one node past the budget");
assert_eq!(d.keys().count(), 3, "and the store was not touched");
assert_eq!(d.node_count(), 48);
assert!(d.admit(MAX_NODES), "the budget itself fits an empty store");
}
#[test]
fn a_new_removal_evicts_the_oldest_ghosts_until_it_fits() {
let mut d = DepartStore::default();
let mut frame = Frames(0);
let n = (MAX_NODES - 192) / 16;
d.begin_frame(frame.next());
assert!(d.admit(n * 16));
depart_sixteens(&mut d, 0, n as u64, 0.0);
assert_eq!(d.node_count(), MAX_NODES - 192);
d.begin_frame(frame.next());
assert!(d.admit(15 * 16));
assert_eq!(
d.node_count(),
MAX_NODES - 192 - 48,
"three evicted, not four, not two"
);
assert_eq!(
d.keys().next(),
Some(Key::ROOT.index(3)),
"the oldest went first"
);
depart_sixteens(&mut d, 100_000, 15, 0.1);
assert_eq!(
d.node_count(),
MAX_NODES,
"full, with the new removal whole"
);
assert_eq!(d.keys().count(), n - 3 + 15);
assert!(d.held.contains(&Key::ROOT.index(100_014)));
assert!(!d.held.contains(&Key::ROOT.index(2)), "and `held` followed");
}
#[test]
fn a_removal_that_fits_evicts_nothing() {
let mut d = DepartStore::default();
let mut frame = Frames(0);
d.begin_frame(frame.next());
assert!(d.admit(16));
depart_sixteens(&mut d, 0, 1, 0.0);
d.begin_frame(frame.next());
assert!(d.admit(MAX_NODES - 16));
assert_eq!(d.keys().count(), 1);
assert_eq!(d.node_count(), 16);
assert!(d.admit(0), "and nothing wanted is always admitted");
}
#[test]
fn a_ghost_nobody_replays_is_swept() {
let mut d = DepartStore::default();
let mut frame = Frames(0);
let text = crate::text::TextSystem::new();
let lines = crate::line::LineStore::default();
let fragments = crate::fragment::FragmentList::default();
let paths = crate::path::PathStore::default();
let tree = tree_with(departing(NodeSpec::column()), 0);
d.begin_frame(frame.next());
d.depart(
&tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
);
for _ in 0..480 {
d.begin_frame(frame.next());
}
assert!(d.is_empty(), "an unreplayed ghost does not live forever");
assert_eq!(d.node_count(), 0);
}
#[test]
fn springs_play_out_as_ease_out() {
let mut d = DepartStore::default();
let mut frame = Frames(0);
let text = crate::text::TextSystem::new();
let lines = crate::line::LineStore::default();
let fragments = crate::fragment::FragmentList::default();
let paths = crate::path::PathStore::default();
let spec = NodeSpec::column()
.transition(100.0)
.easing(Easing::Spring)
.exit(Enter::from(100.0, 0.0));
let tree = tree_with(spec, 0);
d.begin_frame(frame.next());
d.depart(
&tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
);
let mut x = 0.0;
d.replay(0.05, |_, p| x = p.offset.x);
let expect = Easing::EaseOut.apply(0.5) * 100.0;
assert!((x - expect).abs() < 1e-3, "{x} != {expect}");
}
#[test]
fn the_exit_reads_an_enter_backwards() {
let mut d = DepartStore::default();
let mut frame = Frames(0);
let text = crate::text::TextSystem::new();
let lines = crate::line::LineStore::default();
let fragments = crate::fragment::FragmentList::default();
let paths = crate::path::PathStore::default();
let spec = NodeSpec::column()
.bg(Color::hex(0xff0000ff))
.radius(10.0)
.transition(100.0)
.easing(Easing::Linear)
.exit(
Enter::default()
.bg(Color::hex(0xff000000))
.radius(0.0)
.opacity(0.0),
);
let mut tree = tree_with(spec, 0);
tree.size[1] = crate::geom::Size::new(40.0, 20.0);
d.begin_frame(frame.next());
d.depart(
&tree, 1, 0.0, 1.0, IN_FLOW, &text, &lines, &fragments, &paths,
);
d.replay(0.05, |_, p| {
assert!((p.opacity - 0.5).abs() < 1e-4, "halfway faded");
assert!((p.bg.unwrap().a - 0.5).abs() < 1e-4, "halfway transparent");
assert!((p.radius.unwrap()[0] - 5.0).abs() < 1e-4, "halfway square");
assert!(p.size.is_none(), "an exit that names no size resizes none");
});
}
}