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