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, rotate, scale) 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    /// The node's turn, in turns clockwise (ADR 0043).
37    pub rotate: Option<f32>,
38    /// The node's uniform scale about its pivot.
39    pub scale: Option<f32>,
40}
41
42impl Slots {
43    pub fn width(mut self, width: Sizing) -> Self {
44        self.width = Some(width);
45        self
46    }
47
48    pub fn height(mut self, height: Sizing) -> Self {
49        self.height = Some(height);
50        self
51    }
52
53    pub fn bg(mut self, bg: Color) -> Self {
54        self.bg = Some(bg);
55        self
56    }
57
58    pub fn radius(mut self, radius: f32) -> Self {
59        self.radius = Some(radius);
60        self
61    }
62
63    pub fn opacity(mut self, opacity: f32) -> Self {
64        self.opacity = Some(opacity.clamp(0.0, 1.0));
65        self
66    }
67
68    pub fn rotate(mut self, turns: f32) -> Self {
69        self.rotate = Some(turns);
70        self
71    }
72
73    pub fn scale(mut self, scale: f32) -> Self {
74        self.scale = Some(scale);
75        self
76    }
77
78    /// Reads one field of a stop or an entrance from plain data, in the
79    /// form the prop itself takes (sizings as a number, `"grow"`, `"50%"`,
80    /// `{grow}` / `{percent}`; colours as `0xRRGGBBAA` or `"#hex"`).
81    /// `Ok(false)` when `name` is none of the seven, so the caller can read
82    /// its own fields after. Every binding funnels through here, so the
83    /// shape is the same in JSX, Lua and C. With `refs`, a `$name` in a
84    /// colour or length slot resolves through it, and one that misses
85    /// leaves the slot unnamed, remembered on the refs.
86    pub(crate) fn parse_field(
87        &mut self,
88        name: &str,
89        v: &Value,
90        refs: Option<&mut crate::tokens::NameRefs<'_>>,
91    ) -> Result<bool, String> {
92        let num = |what: &str| {
93            v.as_float()
94                .map(|n| n as f32)
95                .ok_or_else(|| format!("{what} must be a number"))
96        };
97        if let Some(refs) = refs {
98            let hit = match name {
99                "width" => refs
100                    .length_ref(v)
101                    .map(|px| self.width = px.map(Sizing::Fixed)),
102                "height" => refs
103                    .length_ref(v)
104                    .map(|px| self.height = px.map(Sizing::Fixed)),
105                "bg" => refs.color_ref(v).map(|c| self.bg = c),
106                "radius" => refs.length_ref(v).map(|r| self.radius = r),
107                _ => None,
108            };
109            if hit.is_some() {
110                return Ok(true);
111            }
112        }
113        match name {
114            // An expression the full table refused leaves the slot
115            // unset, as a prop is left undeclared.
116            "width" => self.width = kept(sizing_value(v))?,
117            "height" => self.height = kept(sizing_value(v))?,
118            "bg" => self.bg = Some(color_value(v)?),
119            "radius" => self.radius = Some(num("radius")?),
120            "opacity" => self.opacity = Some(num("opacity")?.clamp(0.0, 1.0)),
121            "rotate" => self.rotate = Some(num("rotate")?),
122            "scale" => self.scale = Some(num("scale")?),
123            _ => return Ok(false),
124        }
125        Ok(true)
126    }
127
128    /// The slot's value as the four lanes a tween carries, or `None` when
129    /// the slot is not named — or is a sizing with no amount to animate.
130    /// The slots a transition tweens that no entrance or stop can name
131    /// (border, position, shadow) answer `None` too. Inlined: it is asked
132    /// once per slot per transitioning node per frame.
133    #[inline]
134    pub(crate) fn lanes(&self, slot: Slot) -> Option<[f32; 4]> {
135        match slot {
136            Slot::Width => self.width.and_then(Sizing::amount).map(one),
137            Slot::Height => self.height.and_then(Sizing::amount).map(one),
138            Slot::Bg => self.bg.map(Color::lanes),
139            Slot::Radius => self.radius.map(|r| [r; 4]),
140            Slot::Opacity => self.opacity.map(one),
141            // A lane left out is the node's own, which the caller knows
142            // and this does not: `transform_lanes`.
143            Slot::Transform => None,
144            Slot::Border | Slot::Pos | Slot::Shadow | Slot::ShadowColor => None,
145        }
146    }
147
148    /// The transform slot's lanes — `[rotate, scale, 0, 0]` — when the
149    /// stop or the entrance names either, each lane it leaves out taken
150    /// from `base`, the node's own value: a stop that names only `rotate`
151    /// does not shrink the box to a scale of nothing.
152    #[inline]
153    pub(crate) fn transform_lanes(&self, base: [f32; 4]) -> Option<[f32; 4]> {
154        if self.rotate.is_none() && self.scale.is_none() {
155            return None;
156        }
157        // A value that is not a finite number is the base's, as unnamed.
158        let pick = |v: Option<f32>, b: f32| v.filter(|v| v.is_finite()).unwrap_or(b);
159        Some([
160            pick(self.rotate, base[0]),
161            pick(self.scale, base[1]),
162            0.0,
163            0.0,
164        ])
165    }
166}
167
168/// A scalar in a tween's four lanes: the value, then nothing.
169#[inline]
170pub(crate) fn one(v: f32) -> [f32; 4] {
171    [v, 0.0, 0.0, 0.0]
172}
173
174/// The seven delegating builders on a type with a `slots: Slots` field, so
175/// `Enter::from(..).bg(..)` and `Keyframe::default().at(..).bg(..)` read
176/// the same and are written once.
177macro_rules! slot_builders {
178    ($ty:ty) => {
179        impl $ty {
180            /// A [`crate::spec::Sizing`], or a number of px, as on a spec.
181            #[inline]
182            pub fn width(mut self, width: impl Into<crate::spec::Sizing>) -> Self {
183                self.slots = self.slots.width(width.into());
184                self
185            }
186
187            /// A [`crate::spec::Sizing`], or a number of px, as on a spec.
188            #[inline]
189            pub fn height(mut self, height: impl Into<crate::spec::Sizing>) -> Self {
190                self.slots = self.slots.height(height.into());
191                self
192            }
193
194            /// `width(Sizing::GROW)`, as on a spec.
195            #[inline]
196            pub fn grow_width(self) -> Self {
197                self.width(crate::spec::Sizing::GROW)
198            }
199
200            /// `height(Sizing::GROW)`, as on a spec.
201            #[inline]
202            pub fn grow_height(self) -> Self {
203                self.height(crate::spec::Sizing::GROW)
204            }
205
206            pub fn bg(mut self, bg: crate::color::Color) -> Self {
207                self.slots = self.slots.bg(bg);
208                self
209            }
210
211            pub fn radius(mut self, radius: f32) -> Self {
212                self.slots = self.slots.radius(radius);
213                self
214            }
215
216            /// A turn in turns clockwise (ADR 0043), as on a spec.
217            pub fn rotate(mut self, turns: f32) -> Self {
218                self.slots = self.slots.rotate(turns);
219                self
220            }
221
222            /// A uniform scale about the node's pivot, as on a spec.
223            pub fn scale(mut self, scale: f32) -> Self {
224                self.slots = self.slots.scale(scale);
225                self
226            }
227
228            pub fn opacity(mut self, opacity: f32) -> Self {
229                self.slots = self.slots.opacity(opacity);
230                self
231            }
232        }
233
234        impl std::ops::Deref for $ty {
235            type Target = crate::slots::Slots;
236            fn deref(&self) -> &crate::slots::Slots {
237                &self.slots
238            }
239        }
240
241        impl std::ops::DerefMut for $ty {
242            fn deref_mut(&mut self) -> &mut crate::slots::Slots {
243                &mut self.slots
244            }
245        }
246    };
247}
248pub(crate) use slot_builders;
249
250/// `Some` of what parsed, `None` for a size expression the full table
251/// refused ([`crate::calc::is_full`]), the error otherwise.
252fn kept<T>(r: Result<T, String>) -> Result<Option<T>, String> {
253    match r {
254        Ok(v) => Ok(Some(v)),
255        Err(e) if crate::calc::is_full(&e) => Ok(None),
256        Err(e) => Err(e),
257    }
258}
259
260/// A sizing from plain data, in the forms the prop takes.
261pub(crate) fn sizing_value(v: &Value) -> Result<Sizing, String> {
262    match v {
263        Value::Int(_) | Value::Float(_) => Ok(Sizing::Fixed(v.as_float().unwrap_or(0.0) as f32)),
264        Value::Str(s) => crate::schema::sizing_str(s),
265        Value::Map(_) => {
266            if let Some(g) = v.get_float("grow") {
267                Ok(Sizing::Grow(g as f32))
268            } else if let Some(p) = v.get_float("percent") {
269                // JS's spelling, as `"50%"` and a size expression's
270                // `{ percent: 50 }` read it.
271                Ok(Sizing::Percent(p as f32 / 100.0))
272            } else if let Some(p) = v.get_float("pct") {
273                // The Lua spelling.
274                Ok(Sizing::Percent(p as f32 / 100.0))
275            } else {
276                // A size expression as data.
277                crate::calc::sizing_value(v).map_err(|e| {
278                    format!("sizing object needs grow, percent or a size expression: {e}")
279                })
280            }
281        }
282        _ => Err("bad sizing (fit | grow | number | \"N%\" | a size expression)".into()),
283    }
284}
285
286/// A colour from plain data: `0xRRGGBBAA` or `"#hex"`.
287pub(crate) fn color_value(v: &Value) -> Result<Color, String> {
288    match v {
289        Value::Int(n) => Ok(crate::schema::color_num(*n as u32)),
290        Value::Float(n) => Ok(crate::schema::color_num(*n as u32)),
291        Value::Str(s) => crate::schema::color_hex_str(s),
292        _ => Err("color must be a 0xRRGGBBAA number or \"#hex\" string".into()),
293    }
294}