Skip to main content

kui_core/
enter.rs

1//! Entrance transitions: where a node's animatable slots start on the
2//! first frame it is seen.
3//!
4//! Without one, a node's first sight snaps into place. `NodeSpec::enter`
5//! takes an [`Enter`] naming where the slots start instead (`dx`/`dy` for
6//! the position, plus width, height, bg, radius and opacity), and they
7//! ease from there to what the view declares over the node's
8//! `transition`. `NodeSpec::exit` takes the same type read the other way:
9//! where the slots end after the view stops declaring the node.
10//!
11//! ```rust
12//! use kui_core::{Core, Enter, NodeSpec, Size};
13//!
14//! let mut core = Core::new();
15//! let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
16//! // A toast that rises 24 px and fades in over 200 ms when it first appears,
17//! // and does the reverse when the view stops declaring it.
18//! ui.leaf_keyed(
19//!     "toast",
20//!     NodeSpec::row()
21//!         .size(200.0, 40.0)
22//!         .transition(200.0)
23//!         .enter(Enter::from(0.0, 24.0).opacity(0.0))
24//!         .exit(Enter::from(0.0, 24.0).opacity(0.0)),
25//! );
26//! ui.finish();
27//! ```
28//!
29//! `dx`/`dy` move the node's position in place, subtree and all; a node
30//! with `enter` but no `slide` eases only its entrance, and a later layout
31//! move still snaps. Slots a `keyframes` stop names are sampled, not
32//! tweened, so `enter` leaves them alone. A node that leaves and comes
33//! back enters again.
34
35use crate::slots::{Slots, slot_builders};
36use crate::value::Value;
37
38/// Where a node's slots start on first sight. Every field is optional: a
39/// slot `enter` doesn't name simply snaps as it always did. Derefs to its
40/// [`Slots`], so `enter.bg` reads the slot.
41#[derive(Clone, Copy, Debug, Default, PartialEq)]
42pub struct Enter {
43    /// Position offset (logical px) the node eases in from — `dx: -300`
44    /// slides in from the left.
45    pub dx: f32,
46    pub dy: f32,
47    /// Width, height, bg, radius and opacity — the slots a keyframe stop
48    /// names too.
49    pub slots: Slots,
50}
51
52slot_builders!(Enter);
53
54impl Enter {
55    /// Slide in from `dx`/`dy` px away.
56    pub fn from(dx: f32, dy: f32) -> Self {
57        Self {
58            dx,
59            dy,
60            ..Self::default()
61        }
62    }
63
64    pub fn offset(mut self, dx: f32, dy: f32) -> Self {
65        self.dx = dx;
66        self.dy = dy;
67        self
68    }
69
70    /// Whether the entrance moves the node's position.
71    pub fn offsets(&self) -> bool {
72        self.dx != 0.0 || self.dy != 0.0
73    }
74}
75
76/// An entrance from plain data: a map with any of `dx`, `dy`, `width`,
77/// `height`, `bg`, `radius`, `opacity`, in the forms the props themselves take (the
78/// same shapes a keyframe stop accepts). Every binding funnels `enter`
79/// through here, so the shape is the same in JSX, Lua and C.
80pub fn parse(v: &Value) -> Result<Enter, String> {
81    parse_with(v, None)
82}
83
84/// [`parse`] with a token lookup, as `keyframes::parse_with`: a `$name`
85/// in `width`, `height`, `bg` or `radius` resolves, and a miss leaves the
86/// slot unnamed and is remembered on the refs.
87pub fn parse_with(
88    v: &Value,
89    mut refs: Option<&mut crate::tokens::NameRefs<'_>>,
90) -> Result<Enter, String> {
91    let Value::Map(fields) = v else {
92        return Err("enter must be an object".into());
93    };
94    let mut e = Enter::default();
95    for (k, v) in fields {
96        let bad = |what: &str| format!("enter: {what}");
97        if e.slots
98            .parse_field(k, v, refs.as_deref_mut())
99            .map_err(|e| bad(&e))?
100        {
101            continue;
102        }
103        let num = |what: &str| {
104            v.as_float()
105                .map(|n| n as f32)
106                .ok_or_else(|| bad(&format!("{what} must be a number")))
107        };
108        match k.as_str() {
109            "dx" => e.dx = num("dx")?,
110            "dy" => e.dy = num("dy")?,
111            other => return Err(bad(&format!("unknown field {other:?}"))),
112        }
113    }
114    Ok(e)
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::color::Color;
121    use crate::spec::Sizing;
122
123    #[test]
124    fn parses_prop_shaped_values() {
125        let e = parse(&Value::map([
126            ("dx", Value::Int(-40)),
127            ("dy", Value::Float(2.5)),
128            ("width", Value::map([("grow", Value::Int(0))])),
129            ("height", Value::str("50%")),
130            ("bg", Value::str("#ff000000")),
131            ("radius", Value::Int(3)),
132            ("opacity", Value::Float(0.0)),
133        ]))
134        .unwrap();
135        assert_eq!(
136            e,
137            Enter::from(-40.0, 2.5)
138                .width(Sizing::Grow(0.0))
139                .height(Sizing::Percent(0.5))
140                .bg(Color::hex(0xff000000))
141                .radius(3.0)
142                .opacity(0.0)
143        );
144        assert!(e.offsets());
145        assert!(!Enter::default().bg(Color::WHITE).offsets());
146    }
147
148    #[test]
149    fn rejects_bad_shapes() {
150        let bad = |v: Value| parse(&v).unwrap_err();
151        assert!(bad(Value::List(vec![])).contains("object"));
152        assert!(bad(Value::map([("dx", Value::str("far"))])).contains("dx must be a number"));
153        assert!(bad(Value::map([("colour", Value::str("#fff"))])).contains("unknown field"));
154        assert!(bad(Value::map([("width", Value::str("wide"))])).contains("bad sizing"));
155    }
156}