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: LocalProperty<Option<String>>,
216    /// Horizontal position.
217    pub x: LocalProperty<Option<Size>>,
218    /// Vertical position.
219    pub y: LocalProperty<Option<Size>>,
220    /// Symmetric inner spacing applied to this node's child layout area on the x axis.
221    pub padding_x: LocalProperty<Option<Size>>,
222    /// Symmetric inner spacing applied to this node's child layout area on the y axis.
223    pub padding_y: LocalProperty<Option<Size>>,
224    /// Horizontal extent.
225    pub width: LocalProperty<Option<Size>>,
226    /// Vertical extent.
227    pub height: LocalProperty<Option<Size>>,
228    /// Horizontal transform origin, relative to the node's own bounds.
229    pub anchor_x: LocalProperty<Option<Size>>,
230    /// Vertical transform origin, relative to the node's own bounds.
231    pub anchor_y: LocalProperty<Option<Size>>,
232    /// Horizontal scale coefficient.
233    // TODO: change scale to Percent (can't be px).
234    pub scale_x: LocalProperty<Option<Size>>,
235    /// Vertical scale coefficient.
236    pub scale_y: LocalProperty<Option<Size>>,
237    /// Horizontal skew.
238    pub skew_x: LocalProperty<Option<Rotation>>,
239    /// Vertical skew.
240    pub skew_y: LocalProperty<Option<Rotation>>,
241    /// Rotation around the z axis.
242    pub rotate: LocalProperty<Option<Rotation>>,
243    /// Full composed transform.
244    pub transform: LocalProperty<Option<Transform2D>>,
245    /// Opacity of this node's subtree. WGPU and browser Piet compose each canvas
246    /// portion before fading it; live native surfaces currently fade separately.
247    pub opacity: LocalProperty<Option<Opacity>>,
248    /// Controls whether this node participates in parent layout measurement and flow.
249    pub layout_role: LocalProperty<Option<LayoutRole>>,
250    /// Allows a node to render outside an ancestor clipping frame.
251    pub unclippable: LocalProperty<Option<bool>>,
252    // Internal hit-testing override, used by generated components and tooling.
253    pub _raycastable: LocalProperty<Option<bool>>,
254    // Internal suspension flag used by runtime/designtime systems.
255    pub _suspended: LocalProperty<Option<bool>>,
256}
257
258impl CommonProperties {
259    // Returns default Rust expressions for compiler-generated common-property initialization.
260    pub fn get_default_properties_literal() -> Vec<(String, String)> {
261        Self::get_property_identifiers()
262            .iter()
263            .map(|id| {
264                if id.0 == "transform" {
265                    (
266                        id.0.to_string(),
267                        "Transform2D::default_wrapped()".to_string(),
268                    )
269                } else {
270                    (id.0.to_string(), "Default::default()".to_string())
271                }
272            })
273            .collect()
274    }
275
276    // Returns common-property names paired with compiler-facing type spellings.
277    pub fn get_property_identifiers() -> Vec<(String, String)> {
278        COMMON_PROPERTIES_TYPE
279            .iter()
280            .map(|(c, t)| (c.to_string(), t.to_string()))
281            .collect()
282    }
283
284    // Returns common-property metadata in a form consumed by compiler codegen.
285    pub fn get_as_common_property() -> Vec<CommonProperty> {
286        Self::get_property_identifiers()
287            .iter()
288            .map(|id| CommonProperty {
289                name: id.0.to_string(),
290                property_type: id.1.to_string(),
291                optional: (id.0 == "transform" || id.0 == "width" || id.0 == "height"),
292            })
293            .collect()
294    }
295
296    // Exposes common properties as PAXEL variables for generated scopes.
297    pub fn retrieve_property_scope(&self) -> HashMap<String, Variable> {
298        let CommonProperties {
299            id,
300            x,
301            y,
302            padding_x,
303            padding_y,
304            width,
305            height,
306            anchor_x,
307            anchor_y,
308            scale_x,
309            scale_y,
310            skew_x,
311            skew_y,
312            rotate,
313            transform,
314            opacity,
315            layout_role,
316            unclippable,
317            _raycastable,
318            _suspended,
319            // NOTE: remember to add an entry to the hashmap bellow as well
320        } = self;
321
322        HashMap::from([
323            (
324                "id".to_string(),
325                Variable::new_from_typed_property(id.clone()),
326            ),
327            (
328                "x".to_string(),
329                Variable::new_from_typed_property(x.clone()),
330            ),
331            (
332                "y".to_string(),
333                Variable::new_from_typed_property(y.clone()),
334            ),
335            (
336                "padding_x".to_string(),
337                Variable::new_from_typed_property(padding_x.clone()),
338            ),
339            (
340                "padding_y".to_string(),
341                Variable::new_from_typed_property(padding_y.clone()),
342            ),
343            (
344                "scale_x".to_string(),
345                Variable::new_from_typed_property(scale_x.clone()),
346            ),
347            (
348                "scale_y".to_string(),
349                Variable::new_from_typed_property(scale_y.clone()),
350            ),
351            (
352                "skew_x".to_string(),
353                Variable::new_from_typed_property(skew_x.clone()),
354            ),
355            (
356                "skew_y".to_string(),
357                Variable::new_from_typed_property(skew_y.clone()),
358            ),
359            (
360                "rotate".to_string(),
361                Variable::new_from_typed_property(rotate.clone()),
362            ),
363            (
364                "anchor_x".to_string(),
365                Variable::new_from_typed_property(anchor_x.clone()),
366            ),
367            (
368                "anchor_y".to_string(),
369                Variable::new_from_typed_property(anchor_y.clone()),
370            ),
371            (
372                "transform".to_string(),
373                Variable::new_from_typed_property(transform.clone()),
374            ),
375            (
376                "opacity".to_string(),
377                Variable::new_from_typed_property(opacity.clone()),
378            ),
379            (
380                "layout_role".to_string(),
381                Variable::new_from_typed_property(layout_role.clone()),
382            ),
383            (
384                "width".to_string(),
385                Variable::new_from_typed_property(width.clone()),
386            ),
387            (
388                "height".to_string(),
389                Variable::new_from_typed_property(height.clone()),
390            ),
391            (
392                "unclippable".to_string(),
393                Variable::new_from_typed_property(unclippable.clone()),
394            ),
395            (
396                "_raycastable".to_string(),
397                Variable::new_from_typed_property(_raycastable.clone()),
398            ),
399            (
400                "_suspended".to_string(),
401                Variable::new_from_typed_property(_suspended.clone()),
402            ),
403        ])
404    }
405}
406
407impl Into<Rotation> for Size {
408    fn into(self) -> Rotation {
409        if let Size::Percent(pix) = self {
410            Rotation::Percent(pix)
411        } else {
412            panic!("Tried to coerce a pixel value into a rotation value; try `%` or `rad` instead of `px`.")
413        }
414    }
415}
416
417impl Size {
418    /// Resolves this size against a parent extent in pixels.
419    pub fn get_pixels(&self, parent: f64) -> f64 {
420        match &self {
421            Self::Pixels(p) => p.to_float(),
422            Self::Percent(p) => parent * (p.to_float() / 100.0),
423            Self::Combined(pix, per) => (parent * (per.to_float() / 100.0)) + pix.to_float(),
424        }
425    }
426}
427
428impl Interpolatable for Size {
429    fn interpolate(&self, other: &Self, t: f64) -> Self {
430        fn components(size: &Size) -> (Numeric, Numeric) {
431            match size {
432                Size::Pixels(pixels) => (*pixels, Numeric::F64(0.0)),
433                Size::Percent(percent) => (Numeric::F64(0.0), *percent),
434                Size::Combined(pixels, percent) => (*pixels, *percent),
435            }
436        }
437
438        fn from_components(pixels: Numeric, percent: Numeric) -> Size {
439            let has_pixels = pixels.to_float().abs() > f64::EPSILON;
440            let has_percent = percent.to_float().abs() > f64::EPSILON;
441            match (has_pixels, has_percent) {
442                (true, true) => Size::Combined(pixels, percent),
443                (true, false) => Size::Pixels(pixels),
444                (false, true) => Size::Percent(percent),
445                (false, false) => Size::Pixels(Numeric::F64(0.0)),
446            }
447        }
448
449        let (self_pixels, self_percent) = components(self);
450        let (other_pixels, other_percent) = components(other);
451        let t = Numeric::F64(t);
452
453        from_components(
454            self_pixels + ((other_pixels - self_pixels) * t),
455            self_percent + ((other_percent - self_percent) * t),
456        )
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn size_interpolation_blends_pixel_and_percent_components() {
466        let start = Size::Pixels(0.into());
467        let end = Size::Percent(100.into());
468        let midpoint = start.interpolate(&end, 0.5);
469
470        assert!((midpoint.get_pixels(200.0) - 100.0).abs() < 0.0001);
471    }
472
473    #[test]
474    fn size_interpolation_can_settle_from_percent_offset_to_pixel_base() {
475        let start = Size::Combined(0.into(), 100.into());
476        let end = Size::Pixels(0.into());
477
478        assert!((start.interpolate(&end, 0.5).get_pixels(200.0) - 100.0).abs() < 0.0001);
479        assert!(start.interpolate(&end, 1.0).get_pixels(200.0).abs() < 0.0001);
480    }
481}
482
483impl Default for Size {
484    fn default() -> Self {
485        Self::Percent(Numeric::F64(100.0))
486    }
487}
488
489impl Mul for Size {
490    type Output = Size;
491
492    fn mul(self, rhs: Self) -> Self::Output {
493        match self {
494            Size::Pixels(pix0) => {
495                match rhs {
496                    //multiplying two pixel values adds them,
497                    //in the sense of multiplying two affine translations.
498                    //this might be wildly unexpected in some cases, so keep an eye on this and
499                    //revisit whether to support Percent values in anchor calcs (could rescind)
500                    Size::Pixels(pix1) => Size::Pixels(pix0 + pix1),
501                    Size::Percent(per1) => Size::Pixels(pix0 * per1),
502                    Size::Combined(pix1, per1) => Size::Pixels((pix0 * per1) + pix0 + pix1),
503                }
504            }
505            Size::Percent(per0) => match rhs {
506                Size::Pixels(pix1) => Size::Pixels(per0 * pix1),
507                Size::Percent(per1) => Size::Percent(per0 * per1),
508                Size::Combined(pix1, per1) => Size::Pixels((per0 * pix1) + (per0 * per1)),
509            },
510            Size::Combined(pix0, per0) => match rhs {
511                Size::Pixels(pix1) => Size::Pixels((pix0 * per0) + pix1),
512                Size::Percent(per1) => Size::Percent(pix0 * per0 * per1),
513                Size::Combined(pix1, per1) => Size::Pixels((pix0 * per0) + (pix1 * per1)),
514            },
515        }
516    }
517}