Skip to main content

kui_core/
slots.rs

1//! The animatable slots an entrance or a keyframe stop may name (width,
2//! height, bg, radius, opacity) as one value.
3//!
4//! You rarely build a [`Slots`] directly: [`crate::enter::Enter`] (where
5//! a node starts on first sight) and [`crate::keyframes::Keyframe`] (a
6//! stop in a cycle) are this plus one field each, carry the same builders,
7//! and deref to it, so `enter.bg` reads the slot.
8//!
9//! ```rust
10//! use kui_core::{Color, Enter};
11//!
12//! let enter = Enter::from(-40.0, 0.0).bg(Color::WHITE).opacity(0.0);
13//! assert_eq!(enter.opacity, Some(0.0));
14//! assert_eq!(enter.bg, Some(Color::WHITE));
15//! assert!(enter.width.is_none());
16//! ```
17
18use crate::anim::Slot;
19use crate::color::Color;
20use crate::spec::Sizing;
21use crate::value::Value;
22
23/// The slots, each optional: one left out is not part of the entrance or
24/// the stop, and keeps whatever the node itself declares.
25#[derive(Clone, Copy, Debug, Default, PartialEq)]
26pub struct Slots {
27    /// Only the amount animates, in the form the node's own `width`
28    /// declares (a `fit` width never moves).
29    pub width: Option<Sizing>,
30    pub height: Option<Sizing>,
31    pub bg: Option<Color>,
32    /// All four corners.
33    pub radius: Option<f32>,
34    /// Group opacity, 0..=1.
35    pub opacity: Option<f32>,
36}
37
38impl Slots {
39    pub fn width(mut self, width: Sizing) -> Self {
40        self.width = Some(width);
41        self
42    }
43
44    pub fn height(mut self, height: Sizing) -> Self {
45        self.height = Some(height);
46        self
47    }
48
49    pub fn bg(mut self, bg: Color) -> Self {
50        self.bg = Some(bg);
51        self
52    }
53
54    pub fn radius(mut self, radius: f32) -> Self {
55        self.radius = Some(radius);
56        self
57    }
58
59    pub fn opacity(mut self, opacity: f32) -> Self {
60        self.opacity = Some(opacity.clamp(0.0, 1.0));
61        self
62    }
63
64    /// Reads one field of a stop or an entrance from plain data, in the
65    /// form the prop itself takes (sizings as a number, `"grow"`, `"50%"`,
66    /// `{grow}` / `{percent}`; colours as `0xRRGGBBAA` or `"#hex"`).
67    /// `Ok(false)` when `name` is none of the five, so the caller can read
68    /// its own fields after. Every binding funnels through here, so the
69    /// shape is the same in JSX, Lua and C. With `refs`, a `$name` in a
70    /// colour or length slot resolves through it, and one that misses
71    /// leaves the slot unnamed, remembered on the refs.
72    pub(crate) fn parse_field(
73        &mut self,
74        name: &str,
75        v: &Value,
76        refs: Option<&mut crate::tokens::NameRefs<'_>>,
77    ) -> Result<bool, String> {
78        let num = |what: &str| {
79            v.as_float()
80                .map(|n| n as f32)
81                .ok_or_else(|| format!("{what} must be a number"))
82        };
83        if let Some(refs) = refs {
84            let hit = match name {
85                "width" => refs
86                    .length_ref(v)
87                    .map(|px| self.width = px.map(Sizing::Fixed)),
88                "height" => refs
89                    .length_ref(v)
90                    .map(|px| self.height = px.map(Sizing::Fixed)),
91                "bg" => refs.color_ref(v).map(|c| self.bg = c),
92                "radius" => refs.length_ref(v).map(|r| self.radius = r),
93                _ => None,
94            };
95            if hit.is_some() {
96                return Ok(true);
97            }
98        }
99        match name {
100            // An expression the full table refused leaves the slot
101            // unset, as a prop is left undeclared.
102            "width" => self.width = kept(sizing_value(v))?,
103            "height" => self.height = kept(sizing_value(v))?,
104            "bg" => self.bg = Some(color_value(v)?),
105            "radius" => self.radius = Some(num("radius")?),
106            "opacity" => self.opacity = Some(num("opacity")?.clamp(0.0, 1.0)),
107            _ => return Ok(false),
108        }
109        Ok(true)
110    }
111
112    /// The slot's value as the four lanes a tween carries, or `None` when
113    /// the slot is not named — or is a sizing with no amount to animate.
114    /// The slots a transition tweens that no entrance or stop can name
115    /// (border, position, shadow) answer `None` too. Inlined: it is asked
116    /// once per slot per transitioning node per frame.
117    #[inline]
118    pub(crate) fn lanes(&self, slot: Slot) -> Option<[f32; 4]> {
119        match slot {
120            Slot::Width => self.width.and_then(Sizing::amount).map(one),
121            Slot::Height => self.height.and_then(Sizing::amount).map(one),
122            Slot::Bg => self.bg.map(Color::lanes),
123            Slot::Radius => self.radius.map(|r| [r; 4]),
124            Slot::Opacity => self.opacity.map(one),
125            Slot::Border | Slot::Pos | Slot::Shadow | Slot::ShadowColor => None,
126        }
127    }
128}
129
130/// A scalar in a tween's four lanes: the value, then nothing.
131#[inline]
132pub(crate) fn one(v: f32) -> [f32; 4] {
133    [v, 0.0, 0.0, 0.0]
134}
135
136/// The five delegating builders on a type with a `slots: Slots` field, so
137/// `Enter::from(..).bg(..)` and `Keyframe::default().at(..).bg(..)` read
138/// the same and are written once.
139macro_rules! slot_builders {
140    ($ty:ty) => {
141        impl $ty {
142            /// A [`crate::spec::Sizing`], or a number of px, as on a spec.
143            #[inline]
144            pub fn width(mut self, width: impl Into<crate::spec::Sizing>) -> Self {
145                self.slots = self.slots.width(width.into());
146                self
147            }
148
149            /// A [`crate::spec::Sizing`], or a number of px, as on a spec.
150            #[inline]
151            pub fn height(mut self, height: impl Into<crate::spec::Sizing>) -> Self {
152                self.slots = self.slots.height(height.into());
153                self
154            }
155
156            /// `width(Sizing::GROW)`, as on a spec.
157            #[inline]
158            pub fn grow_width(self) -> Self {
159                self.width(crate::spec::Sizing::GROW)
160            }
161
162            /// `height(Sizing::GROW)`, as on a spec.
163            #[inline]
164            pub fn grow_height(self) -> Self {
165                self.height(crate::spec::Sizing::GROW)
166            }
167
168            pub fn bg(mut self, bg: crate::color::Color) -> Self {
169                self.slots = self.slots.bg(bg);
170                self
171            }
172
173            pub fn radius(mut self, radius: f32) -> Self {
174                self.slots = self.slots.radius(radius);
175                self
176            }
177
178            pub fn opacity(mut self, opacity: f32) -> Self {
179                self.slots = self.slots.opacity(opacity);
180                self
181            }
182        }
183
184        impl std::ops::Deref for $ty {
185            type Target = crate::slots::Slots;
186            fn deref(&self) -> &crate::slots::Slots {
187                &self.slots
188            }
189        }
190
191        impl std::ops::DerefMut for $ty {
192            fn deref_mut(&mut self) -> &mut crate::slots::Slots {
193                &mut self.slots
194            }
195        }
196    };
197}
198pub(crate) use slot_builders;
199
200/// `Some` of what parsed, `None` for a size expression the full table
201/// refused ([`crate::calc::is_full`]), the error otherwise.
202fn kept<T>(r: Result<T, String>) -> Result<Option<T>, String> {
203    match r {
204        Ok(v) => Ok(Some(v)),
205        Err(e) if crate::calc::is_full(&e) => Ok(None),
206        Err(e) => Err(e),
207    }
208}
209
210/// A sizing from plain data, in the forms the prop takes.
211pub(crate) fn sizing_value(v: &Value) -> Result<Sizing, String> {
212    match v {
213        Value::Int(_) | Value::Float(_) => Ok(Sizing::Fixed(v.as_float().unwrap_or(0.0) as f32)),
214        Value::Str(s) => crate::schema::sizing_str(s),
215        Value::Map(_) => {
216            if let Some(g) = v.get_float("grow") {
217                Ok(Sizing::Grow(g as f32))
218            } else if let Some(p) = v.get_float("percent") {
219                // JS's spelling, as `"50%"` and a size expression's
220                // `{ percent: 50 }` read it.
221                Ok(Sizing::Percent(p as f32 / 100.0))
222            } else if let Some(p) = v.get_float("pct") {
223                // The Lua spelling.
224                Ok(Sizing::Percent(p as f32 / 100.0))
225            } else {
226                // A size expression as data.
227                crate::calc::sizing_value(v).map_err(|e| {
228                    format!("sizing object needs grow, percent or a size expression: {e}")
229                })
230            }
231        }
232        _ => Err("bad sizing (fit | grow | number | \"N%\" | a size expression)".into()),
233    }
234}
235
236/// A colour from plain data: `0xRRGGBBAA` or `"#hex"`.
237pub(crate) fn color_value(v: &Value) -> Result<Color, String> {
238    match v {
239        Value::Int(n) => Ok(crate::schema::color_num(*n as u32)),
240        Value::Float(n) => Ok(crate::schema::color_num(*n as u32)),
241        Value::Str(s) => crate::schema::color_hex_str(s),
242        _ => Err("color must be a 0xRRGGBBAA number or \"#hex\" string".into()),
243    }
244}