Skip to main content

pax_runtime/
container.rs

1//! Container-facing child ontology and geometry seams.
2//!
3//! The runtime carries several child concepts, but container consumers should
4//! reason about them on two axes:
5//!
6//! 1. Semantic role
7//!    - `received_children`: the payload a node received from its caller
8//!    - encapsulated implementation children: the node's own private template or
9//!      primitive-assembled structure
10//! 2. Lifecycle slice of the received payload
11//!    - active received children: present in `NodeContext::received_children`
12//!    - retained exiting received children: present in
13//!      `NodeContext::retained_received_children`
14//!
15//! Engine details such as projection still exist, but they are transport
16//! mechanisms rather than the primary semantic abstraction.
17
18use crate::api::math::Transform2;
19use crate::api::{borrow, NodeContext, Property, Size};
20use crate::node_interface::NodeLocal;
21use crate::{
22    apply_padding_frame, layout_hulls_equivalent, project_child_layout_hull_to_parent_space,
23    resolve_padded_autosize_axis, ExpandedNode, LayoutHull,
24};
25use pax_runtime_api::{properties::UntypedProperty, Interpolatable};
26use std::rc::Rc;
27
28const MEASURED_SIZE_EPSILON: f64 = 1e-9;
29
30/// Trait for nodes that semantically interpret child content.
31///
32/// Containers should treat `NodeContext::received_children` as their canonical
33/// payload set and `NodeContext::retained_received_children` as the set of
34/// exit-retained payload nodes that may still need placement or transition
35/// handling.
36///
37/// Encapsulated implementation children remain a private detail of the node's
38/// own template or primitive assembly and should generally not drive container
39/// layout logic.
40///
41/// Containers can call this from their existing mount logic to install reactive
42/// behavior on top of those normalized views.
43pub trait Container {
44    fn bind_container(&self, _ctx: &NodeContext) {}
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48#[doc(hidden)]
49pub enum ContentMeasurementGeometry {
50    /// Measure child content without container-assigned placement.
51    Intrinsic,
52    /// Measure child content after placement in the container's local space.
53    Placed,
54}
55
56/// Measure the aggregate layout hull contributed by received content in the
57/// container's local coordinate space.
58///
59/// Empty content is treated as a zero-sized valid hull so autosized containers
60/// can collapse to `0x0` when they have no children.
61pub fn measure_content_children_layout_hull(ctx: &NodeContext) -> LayoutHull {
62    let content_children = ctx.received_children.get();
63    if content_children.is_empty() {
64        return LayoutHull::from_axis_ranges(Some((0.0, 0.0)), Some((0.0, 0.0)));
65    }
66
67    let (padding_x, padding_y) = node_padding(ctx);
68    let content_transform_and_bounds =
69        apply_padding_frame(ctx.node_transform_and_bounds, padding_x, padding_y);
70
71    let mut hull: Option<LayoutHull> = None;
72    for child in content_children.iter() {
73        if child.is_layout_breakout() {
74            // Breakout nodes stay parent-local but are intentionally excluded
75            // from the parent's measured hull.
76            continue;
77        }
78        let projected_hull = project_child_layout_hull_to_parent_space(
79            content_transform_and_bounds,
80            child.transform_and_bounds.get(),
81            child.subtree_layout_hull.get(),
82        );
83        hull = Some(match hull {
84            Some(existing) => existing.union(projected_hull),
85            None => projected_hull,
86        });
87    }
88
89    hull.unwrap_or_else(|| LayoutHull::from_axis_ranges(Some((0.0, 0.0)), Some((0.0, 0.0))))
90}
91
92/// Measure forward autosize extents from received content.
93pub fn measure_content_children_forward_extents(ctx: &NodeContext) -> (Option<f64>, Option<f64>) {
94    let hull = measure_content_children_layout_hull(ctx);
95    (hull.forward_extent_x(), hull.forward_extent_y())
96}
97
98/// Resolve one axis of autosize given the public `autosize` toggle plus an optional override.
99pub fn resolve_axis_autosize(
100    autosize: bool,
101    axis_override: Option<bool>,
102    default_when_enabled: bool,
103) -> bool {
104    axis_override.unwrap_or(autosize && default_when_enabled)
105}
106
107/// Resolve a node's measured size from its received content.
108///
109/// Explicit axes keep their current container bounds; implicit axes use the
110/// measured forward extents when they are valid. If an implicit axis cannot be
111/// measured safely, this returns `None` so the caller can fall back.
112pub fn resolve_content_autosize_measurement(
113    ctx: &NodeContext,
114    width_explicit: bool,
115    height_explicit: bool,
116) -> Option<(f64, f64)> {
117    resolve_content_autosize_measurement_with_axes(ctx, width_explicit, height_explicit, true, true)
118}
119
120/// Resolve a node's measured size from received content with explicit per-axis autosize control.
121pub fn resolve_content_autosize_measurement_with_axes(
122    ctx: &NodeContext,
123    width_explicit: bool,
124    height_explicit: bool,
125    autosize_width: bool,
126    autosize_height: bool,
127) -> Option<(f64, f64)> {
128    if width_explicit && height_explicit {
129        return None;
130    }
131    if !autosize_width && !autosize_height {
132        return None;
133    }
134
135    let bounds = ctx.bounds_self.get();
136    let (content_width, content_height) = measure_content_children_forward_extents(ctx);
137    let (padding_x, padding_y) = node_padding(ctx);
138
139    Some((
140        if width_explicit || !autosize_width {
141            bounds.0
142        } else {
143            resolve_padded_autosize_axis(content_width?, padding_x)?
144        },
145        if height_explicit || !autosize_height {
146            bounds.1
147        } else {
148            resolve_padded_autosize_axis(content_height?, padding_y)?
149        },
150    ))
151}
152
153fn node_padding(ctx: &NodeContext) -> (Option<Size>, Option<Size>) {
154    let Some(node) = ctx.expanded_node.upgrade() else {
155        return (None, None);
156    };
157    let common_props = node.get_common_properties();
158    let common_props = borrow!(common_props);
159    (common_props.padding_x.get(), common_props.padding_y.get())
160}
161
162/// Update `measured_size` from received content when autosize is enabled.
163pub fn sync_content_autosize(expanded_node: &Rc<ExpandedNode>, ctx: &NodeContext, enabled: bool) {
164    sync_content_autosize_with_axes(expanded_node, ctx, enabled, enabled);
165}
166
167/// Update `measured_size` from received content with explicit per-axis autosize control.
168pub fn sync_content_autosize_with_axes(
169    expanded_node: &Rc<ExpandedNode>,
170    ctx: &NodeContext,
171    autosize_width: bool,
172    autosize_height: bool,
173) {
174    let measured_size = if autosize_width || autosize_height {
175        let common_props = expanded_node.get_common_properties();
176        let common_props = borrow!(common_props);
177        let width_explicit = common_props.width.get().is_some();
178        let height_explicit = common_props.height.get().is_some();
179        drop(common_props);
180
181        resolve_content_autosize_measurement_with_axes(
182            ctx,
183            width_explicit,
184            height_explicit,
185            autosize_width,
186            autosize_height,
187        )
188    } else {
189        None
190    };
191
192    match measured_size {
193        Some(measured_size) => {
194            let previous = expanded_node.measured_size.get();
195            if measured_size_needs_update(previous, measured_size) {
196                expanded_node.set_measured_size(measured_size.0, measured_size.1);
197            }
198        }
199        None => {
200            if expanded_node.measured_size.get().is_some() {
201                expanded_node.measured_size.set(None);
202            }
203        }
204    }
205}
206
207pub fn measured_size_needs_update(previous: Option<(f64, f64)>, next: (f64, f64)) -> bool {
208    match previous {
209        Some((previous_width, previous_height)) => {
210            axis_needs_update(previous_width, next.0) || axis_needs_update(previous_height, next.1)
211        }
212        None => true,
213    }
214}
215
216fn axis_needs_update(previous: f64, next: f64) -> bool {
217    if previous == next {
218        return false;
219    }
220    if !previous.is_finite() || !next.is_finite() {
221        return previous.to_bits() != next.to_bits();
222    }
223    (previous - next).abs() > MEASURED_SIZE_EPSILON
224}
225
226fn rebind_content_measurement_effect<F>(
227    expanded_node: &Rc<ExpandedNode>,
228    runtime_context: &Rc<crate::RuntimeContext>,
229    listener_name: &'static str,
230    geometry: ContentMeasurementGeometry,
231    extra_deps: &[UntypedProperty],
232    effect: F,
233) where
234    F: Fn(&Rc<ExpandedNode>, &NodeContext) + Clone + 'static,
235{
236    let common_props = expanded_node.get_common_properties();
237    let (width_prop, height_prop, padding_x_prop, padding_y_prop) = {
238        let common_props = borrow!(common_props);
239        (
240            common_props.width.clone(),
241            common_props.height.clone(),
242            common_props.padding_x.clone(),
243            common_props.padding_y.clone(),
244        )
245    };
246    let node_ctx = expanded_node.get_node_context(runtime_context);
247    let mut deps = vec![
248        node_ctx.bounds_self.untyped(),
249        width_prop.untyped(),
250        height_prop.untyped(),
251        padding_x_prop.untyped(),
252        padding_y_prop.untyped(),
253    ];
254    deps.extend(extra_deps.iter().cloned());
255
256    let placed_measurement = if geometry == ContentMeasurementGeometry::Placed {
257        let node_transform_and_bounds = expanded_node.transform_and_bounds.clone();
258        let mut projected_child_hulls = Vec::new();
259        for child in node_ctx.received_children.get().iter() {
260            let child_cp = child.get_common_properties();
261            let child_layout_role = borrow!(child_cp).layout_role.clone();
262            let projection_name = format!(
263                "{listener_name} projected child layout hull (parent id: {}, child id: {})",
264                expanded_node.id.0, child.id.0
265            );
266            projected_child_hulls.push(crate::projected_child_layout_hull_property(
267                node_transform_and_bounds.clone(),
268                padding_x_prop.clone(),
269                padding_y_prop.clone(),
270                child.transform_and_bounds.clone(),
271                child.subtree_layout_hull.clone(),
272                child_layout_role,
273                &projection_name,
274            ));
275        }
276
277        let measurement_deps = projected_child_hulls
278            .iter()
279            .map(Property::untyped)
280            .collect::<Vec<_>>();
281
282        let measurement_name = format!("{listener_name} placed measurement");
283        let measurement = Property::computed_with_cutoff_and_name(
284            move || {
285                if projected_child_hulls.is_empty() {
286                    return LayoutHull::from_axis_ranges(Some((0.0, 0.0)), Some((0.0, 0.0)));
287                }
288                let hull = projected_child_hulls
289                    .iter()
290                    .fold(LayoutHull::default(), |hull, child| hull.union(child.get()));
291                if hull.x_range().is_none() && hull.y_range().is_none() {
292                    LayoutHull::from_axis_ranges(Some((0.0, 0.0)), Some((0.0, 0.0)))
293                } else {
294                    hull
295                }
296            },
297            &measurement_deps,
298            layout_hulls_equivalent,
299            &measurement_name,
300        );
301        deps.push(measurement.untyped());
302        Some(measurement)
303    } else {
304        for child in node_ctx.received_children.get().iter() {
305            let child_cp = child.get_common_properties();
306            deps.push(borrow!(child_cp).layout_role.untyped());
307            deps.push(child.subtree_layout_hull.untyped());
308        }
309        None
310    };
311
312    let weak_node = Rc::downgrade(expanded_node);
313    let runtime_context = Rc::clone(runtime_context);
314    let bounds_self = node_ctx.bounds_self.clone();
315    expanded_node
316        .content_measurement_listener
317        .replace_with(Property::computed_with_name(
318            move || {
319                let _ = bounds_self.get();
320                if let Some(measurement) = &placed_measurement {
321                    let _ = measurement.get();
322                }
323                let Some(node) = weak_node.upgrade() else {
324                    return;
325                };
326                let node_ctx = node.get_node_context(&runtime_context);
327                effect(&node, &node_ctx);
328            },
329            &deps,
330            listener_name,
331        ));
332}
333
334/// Bind a reactive content-measurement effect to this node.
335///
336/// The effect is re-evaluated after the tree update pass, before occlusion and
337/// layer-plan generation, and its dependency list is rebound whenever the
338/// normalized received-child list changes.
339pub fn bind_content_measurement_effect<F>(
340    expanded_node: &Rc<ExpandedNode>,
341    ctx: &NodeContext,
342    listener_name: &'static str,
343    geometry: ContentMeasurementGeometry,
344    extra_deps: &[UntypedProperty],
345    effect: F,
346) where
347    F: Fn(&Rc<ExpandedNode>, &NodeContext) + Clone + 'static,
348{
349    if expanded_node.content_measurement_bound.replace(true) {
350        return;
351    }
352
353    rebind_content_measurement_effect(
354        expanded_node,
355        &ctx.runtime_context,
356        listener_name,
357        geometry,
358        extra_deps,
359        effect.clone(),
360    );
361    ctx.runtime_context
362        .register_expanded_node_effect_property_named(
363            expanded_node,
364            &expanded_node.content_measurement_listener,
365            listener_name,
366        );
367
368    let weak_node = Rc::downgrade(expanded_node);
369    let runtime_context = Rc::clone(&ctx.runtime_context);
370    let extra_deps = extra_deps.to_vec();
371    let rebind_name = format!("{listener_name} rebind");
372    expanded_node
373        .content_measurement_rebind_listener
374        .replace_with(Property::computed_with_name(
375            move || {
376                let Some(node) = weak_node.upgrade() else {
377                    return;
378                };
379                rebind_content_measurement_effect(
380                    &node,
381                    &runtime_context,
382                    listener_name,
383                    geometry,
384                    &extra_deps,
385                    effect.clone(),
386                );
387            },
388            &[ctx.received_children_changed.untyped()],
389            &rebind_name,
390        ));
391    ctx.runtime_context
392        .register_expanded_node_effect_property_named(
393            expanded_node,
394            &expanded_node.content_measurement_rebind_listener,
395            &rebind_name,
396        );
397}
398
399/// Engine-internal selector for which child family should be normalized into
400/// `NodeContext::received_children`.
401///
402/// This is a provenance selector, not the semantic API surface.
403///
404/// `Owned` means the node's received payload already lives in its active child
405/// tree.
406///
407/// `Projected` means the node receives payload from its caller and the runtime
408/// threads that payload through projection so it can be consumed by `slot(...)`
409/// within the node's encapsulated implementation.
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411pub enum ReceivedChildrenSource {
412    Owned,
413    Projected,
414}
415
416/// Parent-local frame assigned by a container to one of its received children.
417///
418/// This behaves like a virtual wrapper node inside the parent: the frame's
419/// transform is composed onto the parent transform and its bounds become the
420/// child container bounds.
421#[derive(Clone, Copy, Debug, Default, PartialEq)]
422pub struct ContainerFrame {
423    pub transform: Transform2<NodeLocal, NodeLocal>,
424    pub bounds: (f64, f64),
425}
426
427impl Interpolatable for ContainerFrame {
428    fn interpolate(&self, other: &Self, t: f64) -> Self {
429        Self {
430            transform: self.transform.interpolate(&other.transform, t),
431            bounds: self.bounds.interpolate(&other.bounds, t),
432        }
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use crate::api::math::{Transform2, Vector2};
439    use crate::api::{CommonProperties, Layer, LayoutRole, Size};
440    use crate::{
441        bind_content_measurement_effect, measured_size_needs_update, sync_content_autosize,
442        sync_content_autosize_with_axes, BaseInstance, ComponentInstance, ContainerFrame,
443        ContentMeasurementGeometry, ExpandedNode, Globals, InstanceFlags, InstanceNode,
444        InstantiationArgs, RouteLocation, RuntimeContext, RuntimePropertiesStackFrame,
445        TransformAndBounds,
446    };
447    use pax_runtime_api::pax_value::PaxAny;
448    use pax_runtime_api::{Platform, Property, TargetInfo, OS};
449    use std::cell::{Cell, RefCell};
450    use std::fmt;
451    use std::rc::Rc;
452
453    fn test_globals() -> Globals {
454        Globals {
455            elapsed_frames: Property::new(0),
456            elapsed_millis: Property::new(0),
457            viewport: Property::new(TransformAndBounds {
458                transform: Transform2::identity(),
459                bounds: (100.0, 100.0),
460            }),
461            gyro: Property::new(Default::default()),
462            accel: Property::new(Default::default()),
463            route_location: Property::new(RouteLocation::root()),
464            browser_allows_scroller_vector_layers: Property::new(true),
465            browser_allows_nested_scroller_vector_layers: Property::new(true),
466            platform: Platform::Unknown,
467            os: OS::Unknown,
468            target: TargetInfo::new(Platform::Unknown, OS::Unknown),
469            get_elapsed_millis: Rc::new(|| 0),
470        }
471    }
472
473    fn default_properties_factory() -> Box<
474        dyn Fn(
475            Rc<RuntimePropertiesStackFrame>,
476            Option<Rc<ExpandedNode>>,
477        ) -> Option<Rc<RefCell<PaxAny>>>,
478    > {
479        Box::new(|_, expanded_node| {
480            expanded_node
481                .is_none()
482                .then(|| Rc::new(RefCell::new(PaxAny::Builtin(Default::default()))))
483        })
484    }
485
486    fn default_common_properties_factory() -> Box<
487        dyn Fn(
488            Rc<RuntimePropertiesStackFrame>,
489            Option<Rc<ExpandedNode>>,
490        ) -> Option<Rc<RefCell<CommonProperties>>>,
491    > {
492        Box::new(|_, expanded_node| {
493            expanded_node
494                .is_none()
495                .then(|| Rc::new(RefCell::new(CommonProperties::default())))
496        })
497    }
498
499    fn component_args(
500        template: Option<Vec<Rc<dyn InstanceNode>>>,
501        children: Option<Vec<Rc<dyn InstanceNode>>>,
502    ) -> InstantiationArgs {
503        InstantiationArgs {
504            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
505                default_common_properties_factory(),
506            ),
507            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
508            handler_registry: None,
509            children: children.map(RefCell::new),
510            component_template: template.map(RefCell::new),
511            component_settings: None,
512            template_node_identifier: None,
513            template_node_type_id: None,
514            template_node_selector_info: None,
515            transition_config: Default::default(),
516            properties_scope: crate::PropertiesScopeInit::None,
517        }
518    }
519
520    fn direct_node_args(children: Vec<Rc<dyn InstanceNode>>) -> InstantiationArgs {
521        InstantiationArgs {
522            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
523                default_common_properties_factory(),
524            ),
525            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
526            handler_registry: None,
527            children: Some(RefCell::new(children)),
528            component_template: None,
529            component_settings: None,
530            template_node_identifier: None,
531            template_node_type_id: None,
532            template_node_selector_info: None,
533            transition_config: Default::default(),
534            properties_scope: crate::PropertiesScopeInit::None,
535        }
536    }
537
538    struct TestDirectNode {
539        base: BaseInstance,
540    }
541
542    impl InstanceNode for TestDirectNode {
543        fn instantiate(args: InstantiationArgs) -> Rc<Self>
544        where
545            Self: Sized,
546        {
547            Rc::new(Self {
548                base: BaseInstance::new(
549                    args,
550                    InstanceFlags {
551                        invisible_to_slot: false,
552                        invisible_to_raycasting: true,
553                        layer: Layer::DontCare,
554                        is_component: false,
555                        is_slot: false,
556                    },
557                ),
558            })
559        }
560
561        fn resolve_debug(
562            &self,
563            f: &mut fmt::Formatter,
564            _expanded_node: Option<&ExpandedNode>,
565        ) -> fmt::Result {
566            f.debug_struct("TestDirectNode").finish()
567        }
568
569        fn base(&self) -> &BaseInstance {
570            &self.base
571        }
572    }
573
574    #[test]
575    fn owned_received_children_reflect_active_children() {
576        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
577        let direct: Rc<dyn InstanceNode> =
578            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
579        let root_component =
580            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
581        let context = Rc::new(RuntimeContext::new(test_globals()));
582        let root = ExpandedNode::initialize_root(root_component, &context);
583
584        root.recurse_update(&context);
585        let direct_node = root.children.get().first().cloned().unwrap();
586        let node_ctx = direct_node.get_node_context(&context);
587        let received_children = node_ctx.received_children.get();
588
589        assert_eq!(received_children.len(), 1);
590        assert_eq!(received_children[0].id, direct_node.children.get()[0].id);
591        assert_eq!(node_ctx.received_children_count.get(), 1);
592        assert!(node_ctx.retained_received_children.get().is_empty());
593    }
594
595    #[test]
596    fn projected_received_children_reflect_flattened_projected_children() {
597        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
598        let slotted_component: Rc<dyn InstanceNode> =
599            ComponentInstance::instantiate(component_args(Some(Vec::new()), Some(vec![leaf])));
600        let root_component = ComponentInstance::instantiate(component_args(
601            Some(vec![Rc::clone(&slotted_component)]),
602            None,
603        ));
604        let context = Rc::new(RuntimeContext::new(test_globals()));
605        let root = ExpandedNode::initialize_root(root_component, &context);
606
607        root.recurse_update(&context);
608        let component_node = root.children.get().first().cloned().unwrap();
609        let node_ctx = component_node.get_node_context(&context);
610        let received_children = node_ctx.received_children.get();
611
612        assert_eq!(received_children.len(), 1);
613        assert_eq!(
614            received_children[0].id,
615            component_node
616                .expanded_and_flattened_projected_children
617                .get()[0]
618                .id
619        );
620        assert_eq!(node_ctx.received_children_count.get(), 1);
621        assert!(node_ctx.retained_received_children.get().is_empty());
622    }
623
624    #[test]
625    fn sync_content_autosize_collapses_empty_content() {
626        let direct: Rc<dyn InstanceNode> =
627            TestDirectNode::instantiate(direct_node_args(Vec::new()));
628        let root_component =
629            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
630        let context = Rc::new(RuntimeContext::new(test_globals()));
631        let root = ExpandedNode::initialize_root(root_component, &context);
632
633        root.recurse_update(&context);
634        let direct_node = root.children.get().first().cloned().unwrap();
635        let node_ctx = direct_node.get_node_context(&context);
636
637        sync_content_autosize(&direct_node, &node_ctx, true);
638
639        assert_eq!(direct_node.measured_size.get(), Some((0.0, 0.0)));
640    }
641
642    #[test]
643    fn sync_content_autosize_with_axes_keeps_unmanaged_axis_at_layout_bounds() {
644        let direct: Rc<dyn InstanceNode> =
645            TestDirectNode::instantiate(direct_node_args(Vec::new()));
646        let root_component =
647            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
648        let context = Rc::new(RuntimeContext::new(test_globals()));
649        let root = ExpandedNode::initialize_root(root_component, &context);
650
651        root.recurse_update(&context);
652        let direct_node = root.children.get().first().cloned().unwrap();
653        let node_ctx = direct_node.get_node_context(&context);
654
655        sync_content_autosize_with_axes(&direct_node, &node_ctx, false, true);
656
657        assert_eq!(direct_node.measured_size.get(), Some((100.0, 0.0)));
658    }
659
660    #[test]
661    fn measured_size_update_ignores_float_jitter() {
662        assert!(!measured_size_needs_update(
663            Some((836.8000000000001, 228.6585365853658)),
664            (836.8000000000001, 228.65853658536582),
665        ));
666        assert!(measured_size_needs_update(
667            Some((836.8000000000001, 228.6585365853658)),
668            (836.8000000000001, 228.6585375853658),
669        ));
670        assert!(measured_size_needs_update(None, (0.0, 0.0)));
671    }
672
673    #[test]
674    fn placed_measurement_ignores_ancestor_translation_but_observes_child_position() {
675        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
676        let direct: Rc<dyn InstanceNode> =
677            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
678        let root_component =
679            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
680        let context = Rc::new(RuntimeContext::new(test_globals()));
681        let root = ExpandedNode::initialize_root(root_component, &context);
682        root.recurse_update(&context);
683        let direct_node = root.children.get().first().cloned().unwrap();
684        let child = direct_node.children.get().first().cloned().unwrap();
685        let child_common = child.get_common_properties();
686        child_common
687            .borrow()
688            .width
689            .set(Some(Size::Pixels(20.into())));
690        child_common
691            .borrow()
692            .height
693            .set(Some(Size::Pixels(20.into())));
694        child_common.borrow().x.set(Some(Size::Pixels(0.into())));
695        child_common.borrow().y.set(Some(Size::Pixels(0.into())));
696        context.drain_node_effects();
697
698        let effect_runs = Rc::new(Cell::new(0));
699        let effect_runs_for_effect = Rc::clone(&effect_runs);
700        let node_ctx = direct_node.get_node_context(&context);
701        bind_content_measurement_effect(
702            &direct_node,
703            &node_ctx,
704            "placed measurement test",
705            ContentMeasurementGeometry::Placed,
706            &[],
707            move |_, _| effect_runs_for_effect.set(effect_runs_for_effect.get() + 1),
708        );
709        context.drain_node_effects();
710        let baseline = effect_runs.get();
711
712        let viewport = context.globals().viewport;
713        viewport.set(TransformAndBounds {
714            transform: Transform2::translate(Vector2::new(25.0, 15.0)),
715            bounds: (100.0, 100.0),
716        });
717        context.drain_node_effects();
718        assert_eq!(effect_runs.get(), baseline);
719
720        child_common.borrow().x.set(Some(Size::Pixels(12.into())));
721        context.drain_node_effects();
722        assert_eq!(effect_runs.get(), baseline + 1);
723    }
724
725    #[test]
726    fn intrinsic_measurement_ignores_assigned_frame_but_observes_child_size() {
727        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
728        let direct: Rc<dyn InstanceNode> =
729            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
730        let root_component =
731            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
732        let context = Rc::new(RuntimeContext::new(test_globals()));
733        let root = ExpandedNode::initialize_root(root_component, &context);
734        root.recurse_update(&context);
735        let direct_node = root.children.get().first().cloned().unwrap();
736        let child = direct_node.children.get().first().cloned().unwrap();
737        let child_common = child.get_common_properties();
738        child_common
739            .borrow()
740            .width
741            .set(Some(Size::Pixels(20.into())));
742        child_common
743            .borrow()
744            .height
745            .set(Some(Size::Pixels(20.into())));
746        child_common.borrow().x.set(Some(Size::Pixels(0.into())));
747        child_common.borrow().y.set(Some(Size::Pixels(0.into())));
748        context.drain_node_effects();
749
750        let effect_runs = Rc::new(Cell::new(0));
751        let effect_runs_for_effect = Rc::clone(&effect_runs);
752        let node_ctx = direct_node.get_node_context(&context);
753        bind_content_measurement_effect(
754            &direct_node,
755            &node_ctx,
756            "intrinsic measurement test",
757            ContentMeasurementGeometry::Intrinsic,
758            &[],
759            move |_, _| effect_runs_for_effect.set(effect_runs_for_effect.get() + 1),
760        );
761        context.drain_node_effects();
762        let baseline = effect_runs.get();
763        let baseline_hull = child.subtree_layout_hull.get();
764
765        child.container_frame.set(Some(ContainerFrame {
766            transform: Transform2::translate(Vector2::new(20.0, 10.0)),
767            bounds: (100.0, 100.0),
768        }));
769        context.drain_node_effects();
770        assert_eq!(effect_runs.get(), baseline);
771
772        child_common
773            .borrow()
774            .width
775            .set(Some(Size::Pixels(40.into())));
776        context.drain_node_effects();
777        assert_ne!(child.subtree_layout_hull.get(), baseline_hull);
778        assert_eq!(effect_runs.get(), baseline + 1);
779    }
780
781    #[test]
782    fn sync_content_autosize_includes_padding() {
783        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
784        let direct: Rc<dyn InstanceNode> =
785            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
786        let root_component =
787            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
788        let context = Rc::new(RuntimeContext::new(test_globals()));
789        let root = ExpandedNode::initialize_root(root_component, &context);
790
791        root.recurse_update(&context);
792        let direct_node = root.children.get().first().cloned().unwrap();
793        let child = direct_node.children.get().first().cloned().unwrap();
794        child.set_measured_size(50.0, 60.0);
795        let direct_common_props = direct_node.get_common_properties();
796        direct_common_props
797            .borrow()
798            .padding_x
799            .set(Some(Size::Pixels(5.into())));
800        direct_common_props
801            .borrow()
802            .padding_y
803            .set(Some(Size::Pixels(10.into())));
804
805        let node_ctx = direct_node.get_node_context(&context);
806        sync_content_autosize(&direct_node, &node_ctx, true);
807
808        assert_eq!(direct_node.measured_size.get(), Some((60.0, 80.0)));
809    }
810
811    #[test]
812    fn subtree_layout_hull_includes_trailing_padding_for_ancestor_measurement() {
813        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
814        let direct: Rc<dyn InstanceNode> =
815            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
816        let root_component =
817            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
818        let context = Rc::new(RuntimeContext::new(test_globals()));
819        let root = ExpandedNode::initialize_root(root_component, &context);
820
821        root.recurse_update(&context);
822        let direct_node = root.children.get().first().cloned().unwrap();
823        let child = direct_node.children.get().first().cloned().unwrap();
824        child.set_measured_size(50.0, 60.0);
825        let direct_common_props = direct_node.get_common_properties();
826        direct_common_props
827            .borrow()
828            .padding_x
829            .set(Some(Size::Pixels(5.into())));
830        direct_common_props
831            .borrow()
832            .padding_y
833            .set(Some(Size::Pixels(10.into())));
834
835        root.recurse_update(&context);
836        context.drain_node_effects();
837
838        let hull = direct_node.subtree_layout_hull.get();
839
840        assert_eq!(hull.x_range(), Some((5.0, 60.0)));
841        assert_eq!(hull.y_range(), Some((10.0, 80.0)));
842        assert_eq!(hull.forward_extent_x(), Some(60.0));
843        assert_eq!(hull.forward_extent_y(), Some(80.0));
844    }
845
846    #[test]
847    fn subtree_layout_hull_solves_percent_padding_from_content_hull() {
848        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
849        let direct: Rc<dyn InstanceNode> =
850            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
851        let root_component =
852            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
853        let context = Rc::new(RuntimeContext::new(test_globals()));
854        let root = ExpandedNode::initialize_root(root_component, &context);
855
856        root.recurse_update(&context);
857        let direct_node = root.children.get().first().cloned().unwrap();
858        let child = direct_node.children.get().first().cloned().unwrap();
859        child.set_measured_size(60.0, 80.0);
860        direct_node.set_measured_size(333.0, 333.0);
861        let direct_common_props = direct_node.get_common_properties();
862        direct_common_props
863            .borrow()
864            .padding_x
865            .set(Some(Size::Percent(20.into())));
866        direct_common_props
867            .borrow()
868            .padding_y
869            .set(Some(Size::Percent(10.into())));
870
871        root.recurse_update(&context);
872        context.drain_node_effects();
873
874        let hull = direct_node.subtree_layout_hull.get();
875
876        assert_eq!(hull.forward_extent_x(), Some(100.0));
877        assert_eq!(hull.forward_extent_y(), Some(100.0));
878        assert!((hull.x_range().unwrap().0 - 20.0).abs() < 1e-9);
879        assert!((hull.y_range().unwrap().0 - 10.0).abs() < 1e-9);
880    }
881
882    #[test]
883    fn sync_content_autosize_solves_percent_padding() {
884        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
885        let direct: Rc<dyn InstanceNode> =
886            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
887        let root_component =
888            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
889        let context = Rc::new(RuntimeContext::new(test_globals()));
890        let root = ExpandedNode::initialize_root(root_component, &context);
891
892        root.recurse_update(&context);
893        let direct_node = root.children.get().first().cloned().unwrap();
894        let child = direct_node.children.get().first().cloned().unwrap();
895        child.set_measured_size(60.0, 80.0);
896        let direct_common_props = direct_node.get_common_properties();
897        direct_common_props
898            .borrow()
899            .padding_x
900            .set(Some(Size::Percent(20.into())));
901        direct_common_props
902            .borrow()
903            .padding_y
904            .set(Some(Size::Percent(10.into())));
905
906        let node_ctx = direct_node.get_node_context(&context);
907        sync_content_autosize(&direct_node, &node_ctx, true);
908
909        assert_eq!(direct_node.measured_size.get(), Some((100.0, 100.0)));
910    }
911
912    #[test]
913    fn sync_content_autosize_ignores_breakout_children() {
914        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
915        let direct: Rc<dyn InstanceNode> =
916            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
917        let root_component =
918            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
919        let context = Rc::new(RuntimeContext::new(test_globals()));
920        let root = ExpandedNode::initialize_root(root_component, &context);
921
922        root.recurse_update(&context);
923        let direct_node = root.children.get().first().cloned().unwrap();
924        let child = direct_node.children.get().first().cloned().unwrap();
925        child.set_measured_size(50.0, 60.0);
926        let child_common_props = child.get_common_properties();
927        child_common_props
928            .borrow()
929            .layout_role
930            .set(Some(LayoutRole::Breakout));
931
932        let node_ctx = direct_node.get_node_context(&context);
933        sync_content_autosize(&direct_node, &node_ctx, true);
934
935        assert_eq!(direct_node.measured_size.get(), Some((0.0, 0.0)));
936    }
937}