Skip to main content

pax_runtime_api/
layout.rs

1//! Layout units, axes, and common properties shared by renderable nodes.
2
3use super::*;
4
5/// A spatial size value that can be either a concrete pixel value like `25px`, a percent of parent bounds like `50%`,
6/// or an additive/subtractive combination of the two like `(100% - 10px)`.
7#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Hash)]
8#[serde(crate = "crate::serde")]
9pub enum Size {
10    /// Concrete pixel length, such as `25px`.
11    Pixels(Numeric),
12    /// Percent length relative to the relevant parent bound, such as `50%`.
13    Percent(Numeric),
14    /// Additive pixel and percent components, such as `100% - 10px`.
15    Combined(Numeric, Numeric),
16}
17
18impl Display for Size {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            Size::Pixels(val) => write!(f, "{}px", val),
22            Size::Percent(val) => write!(f, "{}%", val),
23            Size::Combined(pix, per) => write!(f, "{}px + {}%", pix, per),
24        }
25    }
26}
27
28impl Neg for Size {
29    type Output = Size;
30    fn neg(self) -> Self::Output {
31        match self {
32            Size::Pixels(pix) => Size::Pixels(-pix),
33            Size::Percent(per) => Size::Percent(-per),
34            Size::Combined(pix, per) => Size::Combined(-pix, -per),
35        }
36    }
37}
38
39impl Add for Size {
40    type Output = Size;
41    fn add(self, rhs: Self) -> Self::Output {
42        let mut pixel_component: Numeric = Default::default();
43        let mut percent_component: Numeric = Default::default();
44
45        [self, rhs].iter().for_each(|size| match size {
46            Size::Pixels(s) => pixel_component = pixel_component + *s,
47            Size::Percent(s) => percent_component = percent_component + *s,
48            Size::Combined(s0, s1) => {
49                pixel_component = pixel_component + *s0;
50                percent_component = percent_component + *s1;
51            }
52        });
53
54        Size::Combined(pixel_component, percent_component)
55    }
56}
57
58impl Add<Percent> for Size {
59    type Output = Size;
60    fn add(self, rhs: Percent) -> Self::Output {
61        self + Size::Percent(rhs.0)
62    }
63}
64
65impl Sub<Percent> for Size {
66    type Output = Size;
67    fn sub(self, rhs: Percent) -> Self::Output {
68        self - Size::Percent(rhs.0)
69    }
70}
71
72impl Add<Size> for Percent {
73    type Output = Size;
74    fn add(self, rhs: Size) -> Self::Output {
75        Size::Percent(self.0) + rhs
76    }
77}
78
79impl Sub<Size> for Percent {
80    type Output = Size;
81    fn sub(self, rhs: Size) -> Self::Output {
82        Size::Percent(self.0) - rhs
83    }
84}
85
86impl Sub for Size {
87    type Output = Size;
88    fn sub(self, rhs: Self) -> Self::Output {
89        let mut pixel_component: Numeric = Default::default();
90        let mut percent_component: Numeric = Default::default();
91
92        let sizes = [(self, 1), (rhs, -1)];
93        for (size, multiplier) in sizes.iter() {
94            match size {
95                Size::Pixels(s) => {
96                    pixel_component = pixel_component + *s * Numeric::from(*multiplier)
97                }
98                Size::Percent(s) => {
99                    percent_component = percent_component + *s * Numeric::from(*multiplier)
100                }
101                Size::Combined(s0, s1) => {
102                    pixel_component = pixel_component + *s0 * Numeric::from(*multiplier);
103                    percent_component = percent_component + *s1 * Numeric::from(*multiplier);
104                }
105            }
106        }
107
108        Size::Combined(pixel_component, percent_component)
109    }
110}
111
112impl Size {
113    #[allow(non_snake_case)]
114    /// Returns a zero-pixel size.
115    pub fn ZERO() -> Self {
116        Size::Pixels(Numeric::F64(0.0))
117    }
118
119    /// Returns the wrapped percent value normalized as a float, such that 100% => 1.0.
120    /// Panics if wrapped type is not a percentage.
121    pub fn expect_percent(&self) -> f64 {
122        match &self {
123            Size::Percent(val) => val.to_float() / 100.0,
124            Size::Pixels(val) => {
125                log::warn!("Percentage value expected but stored value was pixel.");
126                val.to_float() / 100.0
127            }
128            Size::Combined(_, percent) => {
129                log::warn!("Percentage value expected but stored value was a combination.");
130                percent.to_float() / 100.0
131            }
132        }
133    }
134
135    /// Returns the pixel value
136    /// Panics if wrapped type is not pixels.
137    pub fn expect_pixels(&self) -> Numeric {
138        match &self {
139            Size::Pixels(val) => val.clone(),
140            Size::Percent(val) => {
141                log::warn!("Pixel value expected but stored value was percentage.");
142                val.clone()
143            }
144            Size::Combined(pixels, _) => {
145                log::warn!("Pixel value expected but stored value was a combination.");
146                pixels.clone()
147            }
148        }
149    }
150}
151
152/// Model of 2D cartesian axes, used to disambiguate calculations that depend on axis direction (e.g. width vs height)
153#[derive(Clone, Copy)]
154pub enum Axis {
155    /// Horizontal axis.
156    X,
157    /// Vertical axis.
158    Y,
159}
160
161/// Controls whether a node participates in parent layout measurement.
162#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
163#[serde(crate = "crate::serde")]
164pub enum LayoutRole {
165    /// Normal layout behavior. The node contributes to parent hulls, autosize, and flow.
166    #[default]
167    Default,
168    /// Parent-local positioning that does not contribute to parent hulls, autosize, or flow.
169    ///
170    /// `Breakout` remains in the normal render, hit-test, scroll, and clipping trees; it does
171    /// not portal above ancestor frames, masks, or scrollers.
172    Breakout,
173}
174
175impl Interpolatable for LayoutRole {}
176impl HelperFunctions for LayoutRole {}
177
178impl Size {
179    /// Evaluate a Size in the context of `bounds` and a target `axis`.
180    /// Returns a `Pixel` value as a simple f64; calculates `Percent` with respect to `bounds` & `axis`
181    pub fn evaluate(&self, bounds: (f64, f64), axis: Axis) -> f64 {
182        let target_bound = match axis {
183            Axis::X => bounds.0,
184            Axis::Y => bounds.1,
185        };
186        match &self {
187            Size::Pixels(num) => num.to_float(),
188            Size::Percent(num) => target_bound * (num.to_float() / 100.0),
189            Size::Combined(pixel_component, percent_component) => {
190                //first calc percent, then add pixel
191                (target_bound * (percent_component.to_float() / 100.0)) + pixel_component.to_float()
192            }
193        }
194    }
195}
196
197// Compiler-facing metadata for one common property.
198#[derive(Debug, Default, Serialize, Deserialize)]
199pub struct CommonProperty {
200    // Compiler-facing property name.
201    name: String,
202    // Compiler-facing type spelling.
203    property_type: String,
204    // Whether the property may be omitted without falling back to a concrete runtime value.
205    optional: bool,
206}
207
208/// Properties shared by every renderable Pax node.
209///
210/// Each property here is special-cased by the compiler when parsing element properties,
211/// for example `<SomeElement width={...} />`.
212#[derive(Debug, Default, Clone)]
213pub struct CommonProperties {
214    /// Optional stable node identifier.
215    pub id: Property<Option<String>>,
216    /// Horizontal position.
217    pub x: Property<Option<Size>>,
218    /// Vertical position.
219    pub y: Property<Option<Size>>,
220    /// Symmetric inner spacing applied to this node's child layout area on the x axis.
221    pub padding_x: Property<Option<Size>>,
222    /// Symmetric inner spacing applied to this node's child layout area on the y axis.
223    pub padding_y: Property<Option<Size>>,
224    /// Horizontal extent.
225    pub width: Property<Option<Size>>,
226    /// Vertical extent.
227    pub height: Property<Option<Size>>,
228    /// Horizontal transform origin, relative to the node's own bounds.
229    pub anchor_x: Property<Option<Size>>,
230    /// Vertical transform origin, relative to the node's own bounds.
231    pub anchor_y: Property<Option<Size>>,
232    /// Horizontal scale coefficient.
233    // TODO: change scale to Percent (can't be px).
234    pub scale_x: Property<Option<Size>>,
235    /// Vertical scale coefficient.
236    pub scale_y: Property<Option<Size>>,
237    /// Horizontal skew.
238    pub skew_x: Property<Option<Rotation>>,
239    /// Vertical skew.
240    pub skew_y: Property<Option<Rotation>>,
241    /// Rotation around the z axis.
242    pub rotate: Property<Option<Rotation>>,
243    /// Full composed transform.
244    pub transform: Property<Option<Transform2D>>,
245    /// Node opacity, applied to the node and its descendants.
246    pub opacity: Property<Option<Opacity>>,
247    /// Controls whether this node participates in parent layout measurement and flow.
248    pub layout_role: Property<Option<LayoutRole>>,
249    /// Allows a node to render outside an ancestor clipping frame.
250    pub unclippable: Property<Option<bool>>,
251    // Internal hit-testing override, used by generated components and tooling.
252    pub _raycastable: Property<Option<bool>>,
253    // Internal suspension flag used by runtime/designtime systems.
254    pub _suspended: Property<Option<bool>>,
255}
256
257impl CommonProperties {
258    // Returns default Rust expressions for compiler-generated common-property initialization.
259    pub fn get_default_properties_literal() -> Vec<(String, String)> {
260        Self::get_property_identifiers()
261            .iter()
262            .map(|id| {
263                if id.0 == "transform" {
264                    (
265                        id.0.to_string(),
266                        "Transform2D::default_wrapped()".to_string(),
267                    )
268                } else {
269                    (id.0.to_string(), "Default::default()".to_string())
270                }
271            })
272            .collect()
273    }
274
275    // Returns common-property names paired with compiler-facing type spellings.
276    pub fn get_property_identifiers() -> Vec<(String, String)> {
277        COMMON_PROPERTIES_TYPE
278            .iter()
279            .map(|(c, t)| (c.to_string(), t.to_string()))
280            .collect()
281    }
282
283    // Returns common-property metadata in a form consumed by compiler codegen.
284    pub fn get_as_common_property() -> Vec<CommonProperty> {
285        Self::get_property_identifiers()
286            .iter()
287            .map(|id| CommonProperty {
288                name: id.0.to_string(),
289                property_type: id.1.to_string(),
290                optional: (id.0 == "transform" || id.0 == "width" || id.0 == "height"),
291            })
292            .collect()
293    }
294
295    // Exposes common properties as PAXEL variables for generated scopes.
296    pub fn retrieve_property_scope(&self) -> HashMap<String, Variable> {
297        let CommonProperties {
298            id,
299            x,
300            y,
301            padding_x,
302            padding_y,
303            width,
304            height,
305            anchor_x,
306            anchor_y,
307            scale_x,
308            scale_y,
309            skew_x,
310            skew_y,
311            rotate,
312            transform,
313            opacity,
314            layout_role,
315            unclippable,
316            _raycastable,
317            _suspended,
318            // NOTE: remember to add an entry to the hashmap bellow as well
319        } = self;
320
321        HashMap::from([
322            (
323                "id".to_string(),
324                Variable::new_from_typed_property(id.clone()),
325            ),
326            (
327                "x".to_string(),
328                Variable::new_from_typed_property(x.clone()),
329            ),
330            (
331                "y".to_string(),
332                Variable::new_from_typed_property(y.clone()),
333            ),
334            (
335                "padding_x".to_string(),
336                Variable::new_from_typed_property(padding_x.clone()),
337            ),
338            (
339                "padding_y".to_string(),
340                Variable::new_from_typed_property(padding_y.clone()),
341            ),
342            (
343                "scale_x".to_string(),
344                Variable::new_from_typed_property(scale_x.clone()),
345            ),
346            (
347                "scale_y".to_string(),
348                Variable::new_from_typed_property(scale_y.clone()),
349            ),
350            (
351                "skew_x".to_string(),
352                Variable::new_from_typed_property(skew_x.clone()),
353            ),
354            (
355                "skew_y".to_string(),
356                Variable::new_from_typed_property(skew_y.clone()),
357            ),
358            (
359                "rotate".to_string(),
360                Variable::new_from_typed_property(rotate.clone()),
361            ),
362            (
363                "anchor_x".to_string(),
364                Variable::new_from_typed_property(anchor_x.clone()),
365            ),
366            (
367                "anchor_y".to_string(),
368                Variable::new_from_typed_property(anchor_y.clone()),
369            ),
370            (
371                "transform".to_string(),
372                Variable::new_from_typed_property(transform.clone()),
373            ),
374            (
375                "opacity".to_string(),
376                Variable::new_from_typed_property(opacity.clone()),
377            ),
378            (
379                "layout_role".to_string(),
380                Variable::new_from_typed_property(layout_role.clone()),
381            ),
382            (
383                "width".to_string(),
384                Variable::new_from_typed_property(width.clone()),
385            ),
386            (
387                "height".to_string(),
388                Variable::new_from_typed_property(height.clone()),
389            ),
390            (
391                "unclippable".to_string(),
392                Variable::new_from_typed_property(unclippable.clone()),
393            ),
394            (
395                "_raycastable".to_string(),
396                Variable::new_from_typed_property(_raycastable.clone()),
397            ),
398            (
399                "_suspended".to_string(),
400                Variable::new_from_typed_property(_suspended.clone()),
401            ),
402        ])
403    }
404}
405
406impl Into<Rotation> for Size {
407    fn into(self) -> Rotation {
408        if let Size::Percent(pix) = self {
409            Rotation::Percent(pix)
410        } else {
411            panic!("Tried to coerce a pixel value into a rotation value; try `%` or `rad` instead of `px`.")
412        }
413    }
414}
415
416impl Size {
417    /// Resolves this size against a parent extent in pixels.
418    pub fn get_pixels(&self, parent: f64) -> f64 {
419        match &self {
420            Self::Pixels(p) => p.to_float(),
421            Self::Percent(p) => parent * (p.to_float() / 100.0),
422            Self::Combined(pix, per) => (parent * (per.to_float() / 100.0)) + pix.to_float(),
423        }
424    }
425}
426
427impl Interpolatable for Size {
428    fn interpolate(&self, other: &Self, t: f64) -> Self {
429        fn components(size: &Size) -> (Numeric, Numeric) {
430            match size {
431                Size::Pixels(pixels) => (*pixels, Numeric::F64(0.0)),
432                Size::Percent(percent) => (Numeric::F64(0.0), *percent),
433                Size::Combined(pixels, percent) => (*pixels, *percent),
434            }
435        }
436
437        fn from_components(pixels: Numeric, percent: Numeric) -> Size {
438            let has_pixels = pixels.to_float().abs() > f64::EPSILON;
439            let has_percent = percent.to_float().abs() > f64::EPSILON;
440            match (has_pixels, has_percent) {
441                (true, true) => Size::Combined(pixels, percent),
442                (true, false) => Size::Pixels(pixels),
443                (false, true) => Size::Percent(percent),
444                (false, false) => Size::Pixels(Numeric::F64(0.0)),
445            }
446        }
447
448        let (self_pixels, self_percent) = components(self);
449        let (other_pixels, other_percent) = components(other);
450        let t = Numeric::F64(t);
451
452        from_components(
453            self_pixels + ((other_pixels - self_pixels) * t),
454            self_percent + ((other_percent - self_percent) * t),
455        )
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[test]
464    fn size_interpolation_blends_pixel_and_percent_components() {
465        let start = Size::Pixels(0.into());
466        let end = Size::Percent(100.into());
467        let midpoint = start.interpolate(&end, 0.5);
468
469        assert!((midpoint.get_pixels(200.0) - 100.0).abs() < 0.0001);
470    }
471
472    #[test]
473    fn size_interpolation_can_settle_from_percent_offset_to_pixel_base() {
474        let start = Size::Combined(0.into(), 100.into());
475        let end = Size::Pixels(0.into());
476
477        assert!((start.interpolate(&end, 0.5).get_pixels(200.0) - 100.0).abs() < 0.0001);
478        assert!(start.interpolate(&end, 1.0).get_pixels(200.0).abs() < 0.0001);
479    }
480}
481
482impl Default for Size {
483    fn default() -> Self {
484        Self::Percent(Numeric::F64(100.0))
485    }
486}
487
488impl Mul for Size {
489    type Output = Size;
490
491    fn mul(self, rhs: Self) -> Self::Output {
492        match self {
493            Size::Pixels(pix0) => {
494                match rhs {
495                    //multiplying two pixel values adds them,
496                    //in the sense of multiplying two affine translations.
497                    //this might be wildly unexpected in some cases, so keep an eye on this and
498                    //revisit whether to support Percent values in anchor calcs (could rescind)
499                    Size::Pixels(pix1) => Size::Pixels(pix0 + pix1),
500                    Size::Percent(per1) => Size::Pixels(pix0 * per1),
501                    Size::Combined(pix1, per1) => Size::Pixels((pix0 * per1) + pix0 + pix1),
502                }
503            }
504            Size::Percent(per0) => match rhs {
505                Size::Pixels(pix1) => Size::Pixels(per0 * pix1),
506                Size::Percent(per1) => Size::Percent(per0 * per1),
507                Size::Combined(pix1, per1) => Size::Pixels((per0 * pix1) + (per0 * per1)),
508            },
509            Size::Combined(pix0, per0) => match rhs {
510                Size::Pixels(pix1) => Size::Pixels((pix0 * per0) + pix1),
511                Size::Percent(per1) => Size::Percent(pix0 * per0 * per1),
512                Size::Combined(pix1, per1) => Size::Pixels((pix0 * per0) + (pix1 * per1)),
513            },
514        }
515    }
516}