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, LocalProperty, NodeContext, 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(LocalProperty::untyped)
280            .collect::<Vec<_>>();
281
282        let measurement_name = format!("{listener_name} placed measurement");
283        let measurement = LocalProperty::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(LocalProperty::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(LocalProperty::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            // Rebinding reads the received-child list itself. Depending on its
389            // unit-valued notifier without reading that notifier leaves it
390            // dirty, which can suppress invalidation on subsequent changes.
391            &[ctx.received_children.untyped()],
392            &rebind_name,
393        ));
394    ctx.runtime_context
395        .register_expanded_node_effect_property_named(
396            expanded_node,
397            &expanded_node.content_measurement_rebind_listener,
398            &rebind_name,
399        );
400}
401
402/// Engine-internal selector for which child family should be normalized into
403/// `NodeContext::received_children`.
404///
405/// This is a provenance selector, not the semantic API surface.
406///
407/// `Owned` means the node's received payload already lives in its active child
408/// tree.
409///
410/// `Projected` means the node receives payload from its caller and the runtime
411/// threads that payload through projection so it can be consumed by `slot(...)`
412/// within the node's encapsulated implementation.
413#[derive(Clone, Copy, Debug, PartialEq, Eq)]
414pub enum ReceivedChildrenSource {
415    Owned,
416    Projected,
417}
418
419/// Parent-local frame assigned by a container to one of its received children.
420///
421/// This behaves like a virtual wrapper node inside the parent: the frame's
422/// transform is composed onto the parent transform and its bounds become the
423/// child container bounds.
424#[derive(Clone, Copy, Debug, Default, PartialEq)]
425pub struct ContainerFrame {
426    pub transform: Transform2<NodeLocal, NodeLocal>,
427    pub bounds: (f64, f64),
428}
429
430impl Interpolatable for ContainerFrame {
431    fn interpolate(&self, other: &Self, t: f64) -> Self {
432        Self {
433            transform: self.transform.interpolate(&other.transform, t),
434            bounds: self.bounds.interpolate(&other.bounds, t),
435        }
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use crate::api::math::{Transform2, Vector2};
442    use crate::api::{CommonProperties, Layer, LayoutRole, Size};
443    use crate::{
444        bind_content_measurement_effect, measured_size_needs_update, sync_content_autosize,
445        sync_content_autosize_with_axes, BaseInstance, ComponentInstance, ContainerFrame,
446        ContentMeasurementGeometry, ExpandedNode, Globals, InstanceFlags, InstanceNode,
447        InstantiationArgs, RouteLocation, RuntimePropertiesStackFrame, TransformAndBounds,
448    };
449    use pax_runtime_api::pax_value::PaxAny;
450    use pax_runtime_api::{LocalProperty, Platform, TargetInfo, OS};
451    use std::cell::{Cell, RefCell};
452    use std::fmt;
453    use std::rc::Rc;
454
455    fn test_globals() -> Globals {
456        Globals {
457            elapsed_frames: LocalProperty::new(0),
458            elapsed_millis: LocalProperty::new(0),
459            viewport: LocalProperty::new(TransformAndBounds {
460                transform: Transform2::identity(),
461                bounds: (100.0, 100.0),
462            }),
463            gyro: LocalProperty::new(Default::default()),
464            accel: LocalProperty::new(Default::default()),
465            route_location: LocalProperty::new(RouteLocation::root()),
466            browser_allows_scroller_vector_layers: LocalProperty::new(true),
467            browser_allows_nested_scroller_vector_layers: LocalProperty::new(true),
468            platform: Platform::Unknown,
469            os: OS::Unknown,
470            target: TargetInfo::new(Platform::Unknown, OS::Unknown),
471            #[cfg(feature = "designtime")]
472            designtime: crate::test_support::designtime(),
473            get_elapsed_millis: Rc::new(|| 0),
474        }
475    }
476
477    fn default_properties_factory() -> Box<
478        dyn Fn(
479            Rc<RuntimePropertiesStackFrame>,
480            Option<Rc<ExpandedNode>>,
481        ) -> Option<Rc<RefCell<PaxAny>>>,
482    > {
483        Box::new(|_, expanded_node| {
484            expanded_node
485                .is_none()
486                .then(|| Rc::new(RefCell::new(PaxAny::Builtin(Default::default()))))
487        })
488    }
489
490    fn default_common_properties_factory() -> Box<
491        dyn Fn(
492            Rc<RuntimePropertiesStackFrame>,
493            Option<Rc<ExpandedNode>>,
494        ) -> Option<Rc<RefCell<CommonProperties>>>,
495    > {
496        Box::new(|_, expanded_node| {
497            expanded_node
498                .is_none()
499                .then(|| Rc::new(RefCell::new(CommonProperties::default())))
500        })
501    }
502
503    fn component_args(
504        template: Option<Vec<Rc<dyn InstanceNode>>>,
505        children: Option<Vec<Rc<dyn InstanceNode>>>,
506    ) -> InstantiationArgs {
507        InstantiationArgs {
508            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
509                default_common_properties_factory(),
510            ),
511            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
512            handler_registry: None,
513            children: children.map(RefCell::new),
514            component_template: template.map(RefCell::new),
515            component_settings: None,
516            template_node_identifier: None,
517            template_node_type_id: None,
518            template_node_selector_info: None,
519            transition_config: Default::default(),
520            properties_scope: crate::PropertiesScopeInit::None,
521        }
522    }
523
524    fn direct_node_args(children: Vec<Rc<dyn InstanceNode>>) -> InstantiationArgs {
525        InstantiationArgs {
526            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
527                default_common_properties_factory(),
528            ),
529            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
530            handler_registry: None,
531            children: Some(RefCell::new(children)),
532            component_template: None,
533            component_settings: None,
534            template_node_identifier: None,
535            template_node_type_id: None,
536            template_node_selector_info: None,
537            transition_config: Default::default(),
538            properties_scope: crate::PropertiesScopeInit::None,
539        }
540    }
541
542    struct TestDirectNode {
543        base: BaseInstance,
544    }
545
546    impl InstanceNode for TestDirectNode {
547        fn instantiate(args: InstantiationArgs) -> Rc<Self>
548        where
549            Self: Sized,
550        {
551            Rc::new(Self {
552                base: BaseInstance::new(
553                    args,
554                    InstanceFlags {
555                        invisible_to_slot: false,
556                        invisible_to_raycasting: true,
557                        layer: Layer::DontCare,
558                        is_component: false,
559                        is_slot: false,
560                    },
561                ),
562            })
563        }
564
565        fn resolve_debug(
566            &self,
567            f: &mut fmt::Formatter,
568            _expanded_node: Option<&ExpandedNode>,
569        ) -> fmt::Result {
570            f.debug_struct("TestDirectNode").finish()
571        }
572
573        fn base(&self) -> &BaseInstance {
574            &self.base
575        }
576    }
577
578    #[test]
579    fn owned_received_children_reflect_active_children() {
580        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
581        let direct: Rc<dyn InstanceNode> =
582            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
583        let root_component =
584            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
585        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
586        let root = ExpandedNode::initialize_root(root_component, &context);
587
588        root.recurse_update(&context);
589        let direct_node = root.children.get().first().cloned().unwrap();
590        let node_ctx = direct_node.get_node_context(&context);
591        let received_children = node_ctx.received_children.get();
592
593        assert_eq!(received_children.len(), 1);
594        assert_eq!(received_children[0].id, direct_node.children.get()[0].id);
595        assert_eq!(node_ctx.received_children_count.get(), 1);
596        assert!(node_ctx.retained_received_children.get().is_empty());
597    }
598
599    #[test]
600    fn projected_received_children_reflect_flattened_projected_children() {
601        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
602        let slotted_component: Rc<dyn InstanceNode> =
603            ComponentInstance::instantiate(component_args(Some(Vec::new()), Some(vec![leaf])));
604        let root_component = ComponentInstance::instantiate(component_args(
605            Some(vec![Rc::clone(&slotted_component)]),
606            None,
607        ));
608        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
609        let root = ExpandedNode::initialize_root(root_component, &context);
610
611        root.recurse_update(&context);
612        let component_node = root.children.get().first().cloned().unwrap();
613        let node_ctx = component_node.get_node_context(&context);
614        let received_children = node_ctx.received_children.get();
615
616        assert_eq!(received_children.len(), 1);
617        assert_eq!(
618            received_children[0].id,
619            component_node
620                .expanded_and_flattened_projected_children
621                .get()[0]
622                .id
623        );
624        assert_eq!(node_ctx.received_children_count.get(), 1);
625        assert!(node_ctx.retained_received_children.get().is_empty());
626    }
627
628    #[test]
629    fn sync_content_autosize_collapses_empty_content() {
630        let direct: Rc<dyn InstanceNode> =
631            TestDirectNode::instantiate(direct_node_args(Vec::new()));
632        let root_component =
633            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
634        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
635        let root = ExpandedNode::initialize_root(root_component, &context);
636
637        root.recurse_update(&context);
638        let direct_node = root.children.get().first().cloned().unwrap();
639        let node_ctx = direct_node.get_node_context(&context);
640
641        sync_content_autosize(&direct_node, &node_ctx, true);
642
643        assert_eq!(direct_node.measured_size.get(), Some((0.0, 0.0)));
644    }
645
646    #[test]
647    fn sync_content_autosize_with_axes_keeps_unmanaged_axis_at_layout_bounds() {
648        let direct: Rc<dyn InstanceNode> =
649            TestDirectNode::instantiate(direct_node_args(Vec::new()));
650        let root_component =
651            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
652        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
653        let root = ExpandedNode::initialize_root(root_component, &context);
654
655        root.recurse_update(&context);
656        let direct_node = root.children.get().first().cloned().unwrap();
657        let node_ctx = direct_node.get_node_context(&context);
658
659        sync_content_autosize_with_axes(&direct_node, &node_ctx, false, true);
660
661        assert_eq!(direct_node.measured_size.get(), Some((100.0, 0.0)));
662    }
663
664    #[test]
665    fn measured_size_update_ignores_float_jitter() {
666        assert!(!measured_size_needs_update(
667            Some((836.8000000000001, 228.6585365853658)),
668            (836.8000000000001, 228.65853658536582),
669        ));
670        assert!(measured_size_needs_update(
671            Some((836.8000000000001, 228.6585365853658)),
672            (836.8000000000001, 228.6585375853658),
673        ));
674        assert!(measured_size_needs_update(None, (0.0, 0.0)));
675    }
676
677    #[test]
678    fn placed_measurement_ignores_ancestor_translation_but_observes_child_position() {
679        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
680        let direct: Rc<dyn InstanceNode> =
681            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
682        let root_component =
683            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
684        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
685        let root = ExpandedNode::initialize_root(root_component, &context);
686        root.recurse_update(&context);
687        let direct_node = root.children.get().first().cloned().unwrap();
688        let child = direct_node.children.get().first().cloned().unwrap();
689        let child_common = child.get_common_properties();
690        child_common
691            .borrow()
692            .width
693            .set(Some(Size::Pixels(20.into())));
694        child_common
695            .borrow()
696            .height
697            .set(Some(Size::Pixels(20.into())));
698        child_common.borrow().x.set(Some(Size::Pixels(0.into())));
699        child_common.borrow().y.set(Some(Size::Pixels(0.into())));
700        context.drain_node_effects();
701
702        let effect_runs = Rc::new(Cell::new(0));
703        let effect_runs_for_effect = Rc::clone(&effect_runs);
704        let node_ctx = direct_node.get_node_context(&context);
705        bind_content_measurement_effect(
706            &direct_node,
707            &node_ctx,
708            "placed measurement test",
709            ContentMeasurementGeometry::Placed,
710            &[],
711            move |_, _| effect_runs_for_effect.set(effect_runs_for_effect.get() + 1),
712        );
713        context.drain_node_effects();
714        let baseline = effect_runs.get();
715
716        let viewport = context.globals().viewport;
717        viewport.set(TransformAndBounds {
718            transform: Transform2::translate(Vector2::new(25.0, 15.0)),
719            bounds: (100.0, 100.0),
720        });
721        context.drain_node_effects();
722        assert_eq!(effect_runs.get(), baseline);
723
724        child_common.borrow().x.set(Some(Size::Pixels(12.into())));
725        context.drain_node_effects();
726        assert_eq!(effect_runs.get(), baseline + 1);
727    }
728
729    #[test]
730    fn intrinsic_measurement_ignores_assigned_frame_but_observes_child_size() {
731        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
732        let direct: Rc<dyn InstanceNode> =
733            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
734        let root_component =
735            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
736        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
737        let root = ExpandedNode::initialize_root(root_component, &context);
738        root.recurse_update(&context);
739        let direct_node = root.children.get().first().cloned().unwrap();
740        let child = direct_node.children.get().first().cloned().unwrap();
741        let child_common = child.get_common_properties();
742        child_common
743            .borrow()
744            .width
745            .set(Some(Size::Pixels(20.into())));
746        child_common
747            .borrow()
748            .height
749            .set(Some(Size::Pixels(20.into())));
750        child_common.borrow().x.set(Some(Size::Pixels(0.into())));
751        child_common.borrow().y.set(Some(Size::Pixels(0.into())));
752        context.drain_node_effects();
753
754        let effect_runs = Rc::new(Cell::new(0));
755        let effect_runs_for_effect = Rc::clone(&effect_runs);
756        let node_ctx = direct_node.get_node_context(&context);
757        bind_content_measurement_effect(
758            &direct_node,
759            &node_ctx,
760            "intrinsic measurement test",
761            ContentMeasurementGeometry::Intrinsic,
762            &[],
763            move |_, _| effect_runs_for_effect.set(effect_runs_for_effect.get() + 1),
764        );
765        context.drain_node_effects();
766        let baseline = effect_runs.get();
767        let baseline_hull = child.subtree_layout_hull.get();
768
769        child.container_frame.set(Some(ContainerFrame {
770            transform: Transform2::translate(Vector2::new(20.0, 10.0)),
771            bounds: (100.0, 100.0),
772        }));
773        context.drain_node_effects();
774        assert_eq!(effect_runs.get(), baseline);
775
776        child_common
777            .borrow()
778            .width
779            .set(Some(Size::Pixels(40.into())));
780        context.drain_node_effects();
781        assert_ne!(child.subtree_layout_hull.get(), baseline_hull);
782        assert_eq!(effect_runs.get(), baseline + 1);
783    }
784
785    #[test]
786    fn sync_content_autosize_includes_padding() {
787        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
788        let direct: Rc<dyn InstanceNode> =
789            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
790        let root_component =
791            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
792        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
793        let root = ExpandedNode::initialize_root(root_component, &context);
794
795        root.recurse_update(&context);
796        let direct_node = root.children.get().first().cloned().unwrap();
797        let child = direct_node.children.get().first().cloned().unwrap();
798        child.set_measured_size(50.0, 60.0);
799        let direct_common_props = direct_node.get_common_properties();
800        direct_common_props
801            .borrow()
802            .padding_x
803            .set(Some(Size::Pixels(5.into())));
804        direct_common_props
805            .borrow()
806            .padding_y
807            .set(Some(Size::Pixels(10.into())));
808
809        let node_ctx = direct_node.get_node_context(&context);
810        sync_content_autosize(&direct_node, &node_ctx, true);
811
812        assert_eq!(direct_node.measured_size.get(), Some((60.0, 80.0)));
813    }
814
815    #[test]
816    fn subtree_layout_hull_includes_trailing_padding_for_ancestor_measurement() {
817        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
818        let direct: Rc<dyn InstanceNode> =
819            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
820        let root_component =
821            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
822        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
823        let root = ExpandedNode::initialize_root(root_component, &context);
824
825        root.recurse_update(&context);
826        let direct_node = root.children.get().first().cloned().unwrap();
827        let child = direct_node.children.get().first().cloned().unwrap();
828        child.set_measured_size(50.0, 60.0);
829        let direct_common_props = direct_node.get_common_properties();
830        direct_common_props
831            .borrow()
832            .padding_x
833            .set(Some(Size::Pixels(5.into())));
834        direct_common_props
835            .borrow()
836            .padding_y
837            .set(Some(Size::Pixels(10.into())));
838
839        root.recurse_update(&context);
840        context.drain_node_effects();
841
842        let hull = direct_node.subtree_layout_hull.get();
843
844        assert_eq!(hull.x_range(), Some((5.0, 60.0)));
845        assert_eq!(hull.y_range(), Some((10.0, 80.0)));
846        assert_eq!(hull.forward_extent_x(), Some(60.0));
847        assert_eq!(hull.forward_extent_y(), Some(80.0));
848    }
849
850    #[test]
851    fn subtree_layout_hull_solves_percent_padding_from_content_hull() {
852        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
853        let direct: Rc<dyn InstanceNode> =
854            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
855        let root_component =
856            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
857        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
858        let root = ExpandedNode::initialize_root(root_component, &context);
859
860        root.recurse_update(&context);
861        let direct_node = root.children.get().first().cloned().unwrap();
862        let child = direct_node.children.get().first().cloned().unwrap();
863        child.set_measured_size(60.0, 80.0);
864        direct_node.set_measured_size(333.0, 333.0);
865        let direct_common_props = direct_node.get_common_properties();
866        direct_common_props
867            .borrow()
868            .padding_x
869            .set(Some(Size::Percent(20.into())));
870        direct_common_props
871            .borrow()
872            .padding_y
873            .set(Some(Size::Percent(10.into())));
874
875        root.recurse_update(&context);
876        context.drain_node_effects();
877
878        let hull = direct_node.subtree_layout_hull.get();
879
880        assert_eq!(hull.forward_extent_x(), Some(100.0));
881        assert_eq!(hull.forward_extent_y(), Some(100.0));
882        assert!((hull.x_range().unwrap().0 - 20.0).abs() < 1e-9);
883        assert!((hull.y_range().unwrap().0 - 10.0).abs() < 1e-9);
884    }
885
886    #[test]
887    fn sync_content_autosize_solves_percent_padding() {
888        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
889        let direct: Rc<dyn InstanceNode> =
890            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
891        let root_component =
892            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
893        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
894        let root = ExpandedNode::initialize_root(root_component, &context);
895
896        root.recurse_update(&context);
897        let direct_node = root.children.get().first().cloned().unwrap();
898        let child = direct_node.children.get().first().cloned().unwrap();
899        child.set_measured_size(60.0, 80.0);
900        let direct_common_props = direct_node.get_common_properties();
901        direct_common_props
902            .borrow()
903            .padding_x
904            .set(Some(Size::Percent(20.into())));
905        direct_common_props
906            .borrow()
907            .padding_y
908            .set(Some(Size::Percent(10.into())));
909
910        let node_ctx = direct_node.get_node_context(&context);
911        sync_content_autosize(&direct_node, &node_ctx, true);
912
913        assert_eq!(direct_node.measured_size.get(), Some((100.0, 100.0)));
914    }
915
916    #[test]
917    fn sync_content_autosize_ignores_breakout_children() {
918        let leaf: Rc<dyn InstanceNode> = TestDirectNode::instantiate(direct_node_args(Vec::new()));
919        let direct: Rc<dyn InstanceNode> =
920            TestDirectNode::instantiate(direct_node_args(vec![leaf]));
921        let root_component =
922            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&direct)]), None));
923        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
924        let root = ExpandedNode::initialize_root(root_component, &context);
925
926        root.recurse_update(&context);
927        let direct_node = root.children.get().first().cloned().unwrap();
928        let child = direct_node.children.get().first().cloned().unwrap();
929        child.set_measured_size(50.0, 60.0);
930        let child_common_props = child.get_common_properties();
931        child_common_props
932            .borrow()
933            .layout_role
934            .set(Some(LayoutRole::Breakout));
935
936        let node_ctx = direct_node.get_node_context(&context);
937        sync_content_autosize(&direct_node, &node_ctx, true);
938
939        assert_eq!(direct_node.measured_size.get(), Some((0.0, 0.0)));
940    }
941}