Skip to main content

pax_runtime/engine/
expanded_node.rs

1use crate::api::TextInput;
2use crate::node_interface::NodeLocal;
3use pax_language::Computable;
4use pax_runtime_api::pax_value::{ImplToFromPaxAny, PaxAny, ToFromPaxAny};
5use pax_runtime_api::{
6    borrow, borrow_mut, use_RefCell, AsyncError, AsyncScope, Focus, Interpolatable, Layer,
7    LocalProperty, NativeLiquidGlassScope, OpacityScope, PaxValue, Percent, SelectStart, Variable,
8};
9
10use crate::api::math::Point2;
11use crate::constants::{
12    ACCEL_HANDLERS, BUTTON_CLICK_HANDLERS, CHECKBOX_CHANGE_HANDLERS, CLICK_HANDLERS,
13    CONTEXT_MENU_HANDLERS, DOUBLE_CLICK_HANDLERS, DROP_HANDLERS, FOCUSED_HANDLERS, GYRO_HANDLERS,
14    KEY_DOWN_HANDLERS, KEY_PRESS_HANDLERS, KEY_UP_HANDLERS, MOUSE_DOWN_HANDLERS,
15    MOUSE_MOVE_HANDLERS, MOUSE_OUT_HANDLERS, MOUSE_OVER_HANDLERS, MOUSE_UP_HANDLERS,
16    PHOTO_PICKER_CHANGE_HANDLERS, SCROLL_HANDLERS, SELECT_START_HANDLERS, SLIDER_CHANGE_HANDLERS,
17    TAP_HANDLERS, TEXTBOX_CHANGE_HANDLERS, TEXTBOX_INPUT_HANDLERS, TEXT_INPUT_HANDLERS,
18    TOUCH_CANCEL_HANDLERS, TOUCH_END_HANDLERS, TOUCH_MOVE_HANDLERS, TOUCH_START_HANDLERS,
19    WHEEL_HANDLERS,
20};
21use_RefCell!();
22use crate::cartridge::evaluate_timeline_duration;
23use crate::{ExpandedNodeIdentifier, Globals, LayoutHull, LayoutProperties, TransformAndBounds};
24use core::fmt;
25use std::cell::Cell;
26use std::collections::{BTreeMap, HashMap, HashSet};
27use std::rc::{Rc, Weak};
28
29// Keep the source handles alive, not the parent node. Equality of current
30// values is insufficient: a different source must establish a new subscription.
31#[derive(Clone)]
32struct ParentBindingSources {
33    parent: ExpandedNodeIdentifier,
34    properties: Vec<pax_runtime_api::properties::UntypedProperty>,
35}
36
37impl ParentBindingSources {
38    fn same_as(&self, other: &Self) -> bool {
39        self.parent == other.parent
40            && self.properties.len() == other.properties.len()
41            && self
42                .properties
43                .iter()
44                .zip(&other.properties)
45                .all(|(a, b)| a.get_id() == b.get_id())
46    }
47}
48
49#[derive(PartialEq)]
50struct ChildStructure {
51    binding_generation: u64,
52    rendered: Vec<ExpandedNodeIdentifier>,
53    active: Vec<ExpandedNodeIdentifier>,
54    exiting: Vec<ExpandedNodeIdentifier>,
55}
56
57use crate::api::{
58    Accel, Axis, ButtonClick, CheckboxChange, Click, CommonProperties, ContextMenu, DoubleClick,
59    Drop, Event, Gyro, KeyDown, KeyPress, KeyUp, LayoutRole, MouseDown, MouseMove, MouseOut,
60    MouseOver, MouseUp, NodeContext, PhotoPickerChange, RenderContext, Scroll, Size, SliderChange,
61    TextboxChange, TextboxInput, TouchCancel, TouchEnd, TouchMove, TouchStart, Wheel, Window,
62};
63use pax_manifest::cartridge_generation::{
64    TRANSITION_GENERATION_SYMBOL, TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT,
65    TRANSITION_PHASE_IDLE, TRANSITION_PHASE_SYMBOL, TRANSITION_PLAYHEAD_MILLIS_SYMBOL,
66    TRANSITION_PLAYHEAD_SYMBOL, TRANSITION_TAKEOVER_SYMBOL,
67};
68use pax_manifest::{
69    ExpressionInfo, LocationInfo, SelectorExpr, SettingsBlockElement, TypeId,
70    UniqueTemplateNodeIdentifier, ValueDefinition,
71};
72
73use crate::{
74    add_symmetric_padding_to_content_layout_hull, apply_container_frame, apply_padding_frame,
75    compute_tab, ComponentInstance, ContainerFrame, HandlerLocation, InstanceNode, InstanceNodePtr,
76    ReceivedChildrenSource, RuntimeContext, RuntimePropertiesStackFrame,
77};
78
79#[derive(Clone)]
80pub struct RuntimeSettingsLayer {
81    pub provider_id: ExpandedNodeIdentifier,
82    pub provider_type_id: TypeId,
83    /// The provider component's lexical scope. Imported settings expressions
84    /// must resolve `self` and provider properties here, even though their
85    /// resulting values are applied to a different node.
86    pub provider_stack: Rc<RuntimePropertiesStackFrame>,
87    pub settings: Vec<SettingsBlockElement>,
88    pub transition: Option<LocalProperty<Option<crate::SettingsTransitionConfig>>>,
89}
90
91impl fmt::Debug for RuntimeSettingsLayer {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.debug_struct("RuntimeSettingsLayer")
94            .field("provider_id", &self.provider_id)
95            .field("provider_type_id", &self.provider_type_id)
96            .field("provider_stack", &"<runtime scope>")
97            .field("settings", &self.settings)
98            .finish()
99    }
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct RuntimeSettingsSignatureEntry {
104    pub transition_enabled: bool,
105    pub provider_id: ExpandedNodeIdentifier,
106    pub provider_type_id: TypeId,
107}
108
109// Discovery runs every frame. Keep only handles until the ordered provider
110// signature changes; copying settings also copies their entire expression trees.
111struct DiscoveredSettingsProvider {
112    node: Rc<ExpandedNode>,
113    transition: Option<LocalProperty<Option<crate::SettingsTransitionConfig>>>,
114}
115
116impl DiscoveredSettingsProvider {
117    fn signature(&self) -> RuntimeSettingsSignatureEntry {
118        RuntimeSettingsSignatureEntry {
119            transition_enabled: self
120                .transition
121                .as_ref()
122                .and_then(|policy| policy.get())
123                .is_some(),
124            provider_id: self.node.id,
125            provider_type_id: borrow!(self.node.instance_node)
126                .base()
127                .template_node_type_id
128                .clone()
129                .unwrap_or_default(),
130        }
131    }
132
133    fn materialize(self) -> RuntimeSettingsLayer {
134        let instance = borrow!(self.node.instance_node);
135        #[cfg(test)]
136        self.node
137            .settings_layer_materializations
138            .set(self.node.settings_layer_materializations.get() + 1);
139        RuntimeSettingsLayer {
140            provider_id: self.node.id,
141            provider_type_id: instance
142                .base()
143                .template_node_type_id
144                .clone()
145                .unwrap_or_default(),
146            provider_stack: self
147                .node
148                .stack
149                .push(borrow!(self.node.properties_scope).clone()),
150            settings: instance
151                .base()
152                .component_settings
153                .as_ref()
154                .expect("discovered provider has settings")
155                .clone(),
156            transition: self.transition,
157        }
158    }
159}
160
161#[derive(Clone, Debug, PartialEq, Eq)]
162pub enum RuntimeSettingsSource {
163    ComponentSettings,
164    ImportedLayer {
165        provider_id: ExpandedNodeIdentifier,
166        provider_type_id: TypeId,
167    },
168    Inline,
169}
170
171#[derive(Clone, Debug, Default)]
172pub struct RuntimeSettingsCondition {
173    pub positive: Vec<ExpressionInfo>,
174    pub negative: Vec<ExpressionInfo>,
175}
176
177#[derive(Clone)]
178pub struct RuntimeResolvedPropertyEntry {
179    pub transition: Option<LocalProperty<Option<crate::SettingsTransitionConfig>>>,
180    pub source: RuntimeSettingsSource,
181    pub selector: Option<SelectorExpr>,
182    pub source_location: Option<LocationInfo>,
183    /// Overrides the receiving node's scope when this value came from an
184    /// imported settings provider.
185    pub source_stack: Option<Rc<RuntimePropertiesStackFrame>>,
186    pub value: ValueDefinition,
187    pub condition: Option<RuntimeSettingsCondition>,
188    pub axis_index: Option<usize>,
189}
190
191impl fmt::Debug for RuntimeResolvedPropertyEntry {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        f.debug_struct("RuntimeResolvedPropertyEntry")
194            .field("source", &self.source)
195            .field("selector", &self.selector)
196            .field("source_location", &self.source_location)
197            .field(
198                "source_stack",
199                &self.source_stack.as_ref().map(|_| "<runtime scope>"),
200            )
201            .field("value", &self.value)
202            .field("condition", &self.condition)
203            .field("axis_index", &self.axis_index)
204            .finish()
205    }
206}
207
208pub type RuntimeResolvedPropertyColumns = BTreeMap<String, Vec<RuntimeResolvedPropertyEntry>>;
209
210#[derive(Clone, Copy)]
211enum PointerActivationSource {
212    Mouse,
213    Touch,
214}
215
216#[derive(Clone, Copy, Debug, Default)]
217pub(crate) struct FilteredRenderStats {
218    pub path_nodes_visited: usize,
219    pub dirty_nodes_rendered: usize,
220    pub skipped_subtrees: usize,
221}
222
223/// This runtime owner stays on the UI thread. Transfer properties or delivery
224/// handles to workers instead.
225///
226/// ```compile_fail
227/// fn requires_send<T: Send>() {}
228/// requires_send::<pax_runtime::ExpandedNode>();
229/// ```
230#[derive(Clone)]
231pub struct ExpandedNode {
232    property_binding_scope: RefCell<pax_runtime_api::properties::BindingScope>,
233    async_scope: RefCell<Option<AsyncScope>>,
234    #[allow(dead_code)]
235    /// Unique ID of this expanded node, roughly encoding an address in the tree, where the first u32 is the instance ID
236    /// and the subsequent u32s represent addresses within an expanded tree via Repeat.
237    pub id: ExpandedNodeIdentifier,
238
239    /// Pointer to the unexpanded `instance_node` underlying this ExpandedNode
240    pub instance_node: RefCell<InstanceNodePtr>,
241
242    /// Pointer (`Weak` to avoid Rc cycle memory leaks) to the ExpandedNode
243    /// rendered directly above this one.
244    pub render_parent: RefCell<Weak<ExpandedNode>>,
245
246    /// Pointer (`Weak` to avoid Rc cycle memory leaks) to the ExpandedNode
247    /// in the template directly above this one.
248    pub template_parent: Weak<ExpandedNode>,
249
250    /// Id of closest frame present in the node tree.
251    /// included as a parameter on AnyCreatePatch when
252    /// creating a native element to know what clipping context
253    /// to attach to
254    pub parent_frame: LocalProperty<Option<ExpandedNodeIdentifier>>,
255
256    /// Nearest active native liquid-glass scope inherited by descendants.
257    pub liquid_glass_scope: LocalProperty<Option<NativeLiquidGlassScope>>,
258
259    /// Reference to the _component for which this `ExpandedNode` is a template member._ Used at least for
260    /// resolving projected children for `slot`. `Option`al because the very root instance node (root component, root instance node)
261    /// has a corollary "root component expanded node."  That very root expanded node _does not have_ a containing ExpandedNode component,
262    /// thus `containing_component` is `Option`al.
263    pub containing_component: Weak<ExpandedNode>,
264
265    /// Persistent clone of the state of the `PropertiesTreeShared#runtime_properties_stack` at the time that this node was expanded (this is expected to remain immutable
266    /// through the lifetime of the program after the initial expansion; however, if that constraint changes, this should be
267    /// explicitly updated to accommodate.)
268    pub stack: Rc<RuntimePropertiesStackFrame>,
269    pub(crate) stores: crate::store::NodeStores,
270
271    /// Pointers to the ExpandedNode beneath this one. Used for rendering
272    /// recursion and other render-tree traversals.
273    pub children: LocalProperty<Vec<Rc<ExpandedNode>>>,
274
275    /// The concrete render-tree children currently attached beneath this node.
276    ///
277    /// This may temporarily include children retained for exit transitions, so
278    /// it should not be confused with the semantic payload exposed through
279    /// `NodeContext::received_children`.
280    pub mounted_children: RefCell<Vec<Rc<ExpandedNode>>>,
281
282    /// Children that represent the currently selected control-flow/template output.
283    ///
284    /// When this node's received-child source is `Owned`, this is the internal
285    /// source for `NodeContext::received_children`.
286    pub active_children: RefCell<Vec<Rc<ExpandedNode>>>,
287    /// Reactive mirror of `active_children` for consumers that need the active
288    /// semantic set.
289    pub active_children_view: LocalProperty<Vec<Rc<ExpandedNode>>>,
290
291    /// Children retained only so exit transitions can finish before unmount.
292    ///
293    /// When this node's received-child source is `Owned`, this is the internal
294    /// source for `NodeContext::retained_received_children`.
295    pub exiting_children: RefCell<Vec<Rc<ExpandedNode>>>,
296    /// Retention token captured when each exiting child is queued. Rescuing a
297    /// child advances its token so stale cleanup work cannot unmount it.
298    exiting_child_generations: RefCell<HashMap<ExpandedNodeIdentifier, u64>>,
299    /// Reactive mirror of `exiting_children` for consumers that need retained exits.
300    pub exiting_children_view: LocalProperty<Vec<Rc<ExpandedNode>>>,
301
302    /// Auxiliary children are not presented directly. Mask sources have a normal
303    /// logical mount lifetime; other sidecar consumers choose their own lifetime.
304    pub sidecar_children: RefCell<Vec<Rc<ExpandedNode>>>,
305
306    /// Mask receiving this node's visual output, rather than the visible scene.
307    /// The identifier avoids keeping the owner alive through its descendants.
308    render_source_owner: Cell<Option<ExpandedNodeIdentifier>>,
309
310    /// Each ExpandedNode has a unique "stamp" of computed properties
311    pub properties: RefCell<Rc<RefCell<PaxAny>>>,
312
313    /// Each ExpandedNode has unique, computed `CommonProperties`
314    pub common_properties: RefCell<Rc<RefCell<CommonProperties>>>,
315    /// Measured bounds reported by chassis/native layout or by container-owned bottom-up layout.
316    /// When width/height are omitted, these values are used as the fallback concrete size.
317    pub measured_size: LocalProperty<Option<(f64, f64)>>,
318    /// Selector-facing metadata used by runtime and designtime queries.
319    pub selector_metadata: RefCell<RuntimeSelectorMetadata>,
320
321    /// The layout information (width, height, transform) used to render this node.
322    /// computed property based on parent bounds + common properties
323    pub transform_and_bounds: LocalProperty<TransformAndBounds<NodeLocal, Window>>,
324    /// Engine-owned subtree hull used by container measurement.
325    pub subtree_layout_hull: LocalProperty<LayoutHull>,
326    /// Optional container-assigned virtual wrapper frame applied before this node's own layout.
327    pub container_frame: LocalProperty<Option<ContainerFrame>>,
328
329    /// The accumulated opacity inherited from render ancestors and this node's
330    /// own common opacity value.
331    pub computed_opacity: LocalProperty<f64>,
332    /// Authored opacity scopes retained independently of their multiplied world opacity.
333    /// Keeping the factors avoids division by zero and detects changes under a hidden ancestor.
334    pub computed_opacity_scopes: LocalProperty<Vec<OpacityScope>>,
335
336    /// For nodes that own projection, tracks projected children in their
337    /// expanded, non-collapsed form.
338    ///
339    /// Projection is a transport detail used to deliver received payload into a
340    /// node's encapsulated implementation, typically for `slot(...)`.
341    /// Repeat/conditional descendants are still present here.
342    pub expanded_projected_children: RefCell<Option<Vec<Rc<ExpandedNode>>>>,
343    /// Flattened version of the projected children above, where repeat and
344    /// conditionals are recursively replaced by their current outputs.
345    ///
346    /// This is the raw projected family, not necessarily the same thing as the
347    /// semantic `received_children` view seen by all consumers.
348    pub expanded_and_flattened_projected_children: LocalProperty<Vec<Rc<ExpandedNode>>>,
349    /// Number of expanded and flattened projected children.
350    pub flattened_projected_children_count: LocalProperty<usize>,
351
352    /// Flag that is > 0 if this node is part of the root tree. If it is,
353    /// updates to this nodes children also marks them as attached (+1), triggering
354    /// mount and dismount on addition/removal. This is needed mainly for slot,
355    /// since an entire "shadow tree" needs to be expanded and updated for
356    /// each slot child, but only the ones that have a "connected" slot should
357    /// trigger mount/dismount updates
358    pub attached: Cell<u32>,
359
360    /// Logical render-layer assignment plus z-order for the current render tree pass.
361    ///
362    /// Layer 0 is the root surface stack, and non-root layers are reserved for scroller-owned
363    /// vector islands.
364    pub occlusion: LocalProperty<Occlusion>,
365
366    /// Hash of the last native occlusion mask emitted for this node.
367    pub native_mask_hash: Cell<u64>,
368
369    /// Last browser-owned descendant content layer published for scroller-style native hosts.
370    pub browser_content_layer_id: Cell<Option<u32>>,
371
372    /// Hash of the last presentation metadata emitted for clipping/scrolling chassis consumers.
373    pub presentation_cache_hash: Cell<u64>,
374
375    /// A map of all properties available on this expanded node.
376    /// Used by the RuntimePropertiesStackFrame to resolve symbols.
377    pub properties_scope: RefCell<HashMap<String, Variable>>,
378
379    /// The flattened index of this node in its container (if this container
380    /// cares about slot children, ex: component, path).
381    pub slot_index: LocalProperty<Option<usize>>,
382
383    /// property used to "freeze" (stop firing tick) on a node and all it's children
384    pub suspended: LocalProperty<bool>,
385
386    /// used by native elements to trigger sending of native messages
387    /// used by canvas elements to dirtify their canvas
388    pub changed_listener: LocalProperty<()>,
389    selector_classes_listener: LocalProperty<()>,
390
391    /// Tracks whether this node's occlusion-affecting inputs changed.
392    pub occlusion_listener: LocalProperty<()>,
393
394    /// Pulls the node's children property only when its upstream dependencies changed.
395    pub children_listener: LocalProperty<()>,
396    /// Rebinds `subtree_layout_hull` when the child list changes.
397    pub subtree_layout_hull_listener: LocalProperty<()>,
398    parent_binding_sources: RefCell<Option<ParentBindingSources>>,
399    layout_binding_generation: Cell<u64>,
400    #[cfg(test)]
401    pub(crate) parent_binding_rebuilds: Cell<usize>,
402    #[cfg(test)]
403    pub(crate) layout_hull_rebuilds: Cell<usize>,
404    #[cfg(test)]
405    pub(crate) structural_children_updates: Cell<usize>,
406    /// Reactive content-measurement effect used by autosize-style container features.
407    pub content_measurement_listener: LocalProperty<()>,
408    /// Rebinds `content_measurement_listener` when the content child list changes.
409    pub content_measurement_rebind_listener: LocalProperty<()>,
410    /// Guards one-time setup of the reactive content-measurement listener pair.
411    pub content_measurement_bound: Cell<bool>,
412    #[cfg(feature = "shaders")]
413    pub(crate) shader_effect_bound: Cell<bool>,
414    /// Cached update traversal state. Structural effects mark ancestors dirty when child
415    /// ownership changes; the update pass clears this for subtrees with no remaining
416    /// imperative update work.
417    subtree_requires_non_reactive_update: Cell<bool>,
418
419    /// Dirty signal emitted when the projected child set changes structurally.
420    pub projected_children_changed: LocalProperty<()>,
421
422    /// Dirty signal emitted when active slot sites or slot-index expressions
423    /// may change the component-local projection resolution.
424    pub slot_projection_changed: LocalProperty<()>,
425
426    /// subscription properties: added to this expanded node by calling ctx.subscribe in a node event handler
427    pub subscriptions: RefCell<Vec<LocalProperty<()>>>,
428
429    /// Current lifecycle transition phase for this node.
430    pub transition_phase: LocalProperty<u64>,
431    /// Monotonic identity for each lifecycle transition run.
432    pub transition_generation: LocalProperty<u64>,
433    /// Whether the current run directly reversed the opposite lifecycle phase.
434    pub transition_takeover: LocalProperty<bool>,
435    /// Class membership captured at lifecycle-transition start. Ordinary settings continue to
436    /// follow live classes, while transition tracks keep the selector membership that launched
437    /// the current run.
438    pub transition_selector_classes: RefCell<Option<Vec<String>>>,
439    /// Parent-owned exit retention generation for stale cleanup protection.
440    exit_retention_generation: Cell<u64>,
441    /// Frame at which the active lifecycle transition began.
442    pub transition_origin_frame: LocalProperty<u64>,
443    /// Millisecond clock value at which the active lifecycle transition began.
444    pub transition_origin_millis: LocalProperty<u64>,
445    /// Local playhead, in frames, for the active lifecycle transition.
446    pub transition_playhead: LocalProperty<f64>,
447    /// Local playhead, in milliseconds, for the active lifecycle transition.
448    pub transition_playhead_millis: LocalProperty<f64>,
449    /// Effect property used to stop enter-transition clock dependencies.
450    pub enter_cleanup_listener: LocalProperty<()>,
451    /// Whether enter cleanup should do work on frame ticks.
452    pub enter_cleanup_active: Cell<bool>,
453    /// Wall-clock start for exit timeout enforcement.
454    pub exit_started_millis: Cell<Option<u128>>,
455    /// Whether this node has already warned about truncating its current exit transition.
456    exit_timeout_warning_emitted: Cell<bool>,
457    /// Effect property used to release deferred exit children.
458    pub exit_cleanup_listener: LocalProperty<()>,
459    /// Whether exit cleanup should do work on frame ticks.
460    pub exit_cleanup_active: Cell<bool>,
461    /// Imported provider layers currently active for this component instance.
462    pub imported_settings_layers: RefCell<Vec<RuntimeSettingsLayer>>,
463    #[cfg(test)]
464    pub(crate) settings_layer_materializations: Cell<usize>,
465    pub import_settings_transition:
466        RefCell<Option<LocalProperty<Option<crate::SettingsTransitionConfig>>>>,
467    pub(crate) settings_motion: Rc<RefCell<HashMap<String, Box<dyn std::any::Any>>>>,
468    pub(crate) settings_birth_frame: u64,
469    /// Ordered provider-node ids used to detect when the imported layer stack changed.
470    pub imported_settings_signature: RefCell<Vec<RuntimeSettingsSignatureEntry>>,
471    /// Layered property entries ordered from lowest to highest precedence.
472    pub resolved_property_columns: RefCell<RuntimeResolvedPropertyColumns>,
473    /// Winning property entry per key after layering and inline override.
474    pub resolved_property_provenance: RefCell<BTreeMap<String, RuntimeResolvedPropertyEntry>>,
475    /// Requests a materialization pass that resets properties removed from the prior columns.
476    pub reset_removed_runtime_properties: Cell<bool>,
477}
478
479#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
480pub struct Occlusion {
481    /// Logical render layer: 0 for the root surface stack, >0 for scroller-owned vector islands.
482    pub render_layer_id: usize,
483    pub z_index: i32,
484    // this is used to perform last patches logic,
485    // and is updated to reflect this nodes parent_frame
486    // when occlusion is calculated
487    pub parent_frame: Option<u32>,
488}
489
490impl Interpolatable for Occlusion {}
491
492impl ImplToFromPaxAny for ExpandedNode {}
493impl Interpolatable for ExpandedNode {}
494
495#[derive(Clone)]
496/// Live selector identity for one expanded runtime node.
497pub struct RuntimeSelectorMetadata {
498    /// Concrete element/component type used by type selectors.
499    pub type_id: TypeId,
500    /// Reactive node id used by id selectors.
501    pub id: LocalProperty<Option<String>>,
502    /// Reactive, normalized class names in left-to-right cascade order.
503    pub classes: LocalProperty<Vec<String>>,
504}
505
506impl RuntimeSelectorMetadata {
507    fn from_base(
508        base: &crate::rendering::BaseInstance,
509        common_properties: &Rc<RefCell<CommonProperties>>,
510        stack: &Rc<RuntimePropertiesStackFrame>,
511    ) -> Self {
512        let classes = base
513            .template_node_selector_info
514            .as_ref()
515            .and_then(|info| {
516                info.class_binding.as_ref().map(|binding| {
517                    build_selector_classes_property(binding, info.source_location.as_ref(), stack)
518                })
519            })
520            .unwrap_or_else(|| LocalProperty::new_with_name(Vec::new(), "selector classes"));
521
522        Self {
523            type_id: base.template_node_type_id.clone().unwrap_or_default(),
524            id: common_properties.borrow().id.clone(),
525            classes,
526        }
527    }
528
529    pub fn matches(&self, selector: &SelectorExpr) -> bool {
530        match selector {
531            SelectorExpr::Id(id) => self.id.get().as_deref() == Some(id.as_str()),
532            SelectorExpr::Class(class_name) => self
533                .classes
534                .get()
535                .into_iter()
536                .any(|node_class| node_class == *class_name),
537            SelectorExpr::Type(type_name) => {
538                self.type_id.to_string() == *type_name
539                    || self.type_id.get_pascal_identifier().as_deref() == Some(type_name.as_str())
540            }
541        }
542    }
543}
544
545fn warn_selector_class_once(
546    warnings: &Rc<RefCell<HashSet<String>>>,
547    key: String,
548    message: impl FnOnce() -> String,
549) {
550    if warnings.borrow_mut().insert(key) {
551        log::warn!("{}", message());
552    }
553}
554
555fn normalize_selector_classes(
556    value: PaxValue,
557    warnings: &Rc<RefCell<HashSet<String>>>,
558    source_location: Option<&LocationInfo>,
559) -> Vec<String> {
560    let location = source_location
561        .map(|location| {
562            format!(
563                " at {}:{}",
564                location.start_line_col.0, location.start_line_col.1
565            )
566        })
567        .unwrap_or_default();
568    let candidates = match value {
569        PaxValue::String(value) => vec![value],
570        PaxValue::Vec(values) => values
571            .into_iter()
572            .filter_map(|value| match value {
573                PaxValue::String(value) => Some(value),
574                other => {
575                    warn_selector_class_once(warnings, format!("list-type:{other}"), || {
576                        format!("ignoring non-string value in class list{location}: {other}")
577                    });
578                    None
579                }
580            })
581            .collect(),
582        other => {
583            warn_selector_class_once(warnings, format!("binding-type:{other}"), || {
584                format!(
585                    "class binding{location} must evaluate to String or Vec<String>; received {other}"
586                )
587            });
588            return Vec::new();
589        }
590    };
591
592    let mut seen = HashSet::new();
593    let mut normalized = candidates
594        .into_iter()
595        .rev()
596        .filter(|class_name| {
597            if class_name.is_empty() {
598                return false;
599            }
600            if !matches!(
601                SelectorExpr::parse(&format!(".{class_name}")),
602                Ok(SelectorExpr::Class(_))
603            ) {
604                warn_selector_class_once(warnings, format!("invalid-name:{class_name}"), || {
605                    format!("ignoring invalid class name '{class_name}'{location}")
606                });
607                return false;
608            }
609            seen.insert(class_name.clone())
610        })
611        .collect::<Vec<_>>();
612    normalized.reverse();
613    normalized
614}
615
616fn build_selector_classes_property(
617    binding: &ValueDefinition,
618    source_location: Option<&LocationInfo>,
619    stack: &Rc<RuntimePropertiesStackFrame>,
620) -> LocalProperty<Vec<String>> {
621    let warnings = Rc::new(RefCell::new(HashSet::new()));
622    let source_location = source_location.cloned();
623    match binding {
624        ValueDefinition::LiteralValue(value) => LocalProperty::new_with_name(
625            normalize_selector_classes(value.clone(), &warnings, source_location.as_ref()),
626            "selector classes",
627        ),
628        ValueDefinition::Expression(info) => {
629            let dependencies = info
630                .dependencies
631                .iter()
632                .filter_map(|dependency| {
633                    stack
634                        .resolve_symbol_as_erased_property(dependency)
635                        .or_else(|| {
636                            log::warn!("failed to resolve class binding symbol {dependency}");
637                            None
638                        })
639                })
640                .collect::<Vec<_>>();
641            let expression = info.expression.clone();
642            let expression_label = expression.to_string();
643            let stack = Rc::clone(stack);
644            let warnings = Rc::clone(&warnings);
645            LocalProperty::computed_with_name(
646                move || match expression.compute(stack.clone()) {
647                    Ok(value) => {
648                        normalize_selector_classes(value, &warnings, source_location.as_ref())
649                    }
650                    Err(error) => {
651                        warn_selector_class_once(
652                            &warnings,
653                            format!("evaluation:{expression_label}:{error:?}"),
654                            || {
655                                let location = source_location
656                                    .as_ref()
657                                    .map(|location| {
658                                        format!(
659                                            " at {}:{}",
660                                            location.start_line_col.0, location.start_line_col.1
661                                        )
662                                    })
663                                    .unwrap_or_default();
664                                format!(
665                                    "failed to evaluate class binding `{expression_label}`{location}: {error:?}"
666                                )
667                            },
668                        );
669                        Vec::new()
670                    }
671                },
672                &dependencies,
673                "selector classes",
674            )
675        }
676        other => {
677            log::warn!("unsupported class binding definition: {other}");
678            LocalProperty::new_with_name(Vec::new(), "selector classes")
679        }
680    }
681}
682
683#[cfg(test)]
684mod selector_class_tests {
685    use super::{build_selector_classes_property, normalize_selector_classes};
686    use crate::RuntimePropertiesStackFrame;
687    use pax_language::parse_pax_expression;
688    use pax_manifest::{ExpressionInfo, ValueDefinition};
689    use pax_runtime_api::{LocalProperty, PaxValue, Variable};
690    use std::cell::RefCell;
691    use std::collections::{HashMap, HashSet};
692    use std::rc::Rc;
693
694    #[test]
695    fn normalizes_order_duplicates_and_invalid_names() {
696        let warnings = Rc::new(RefCell::new(HashSet::new()));
697        let classes = normalize_selector_classes(
698            PaxValue::Vec(vec![
699                PaxValue::String("base".to_string()),
700                PaxValue::String("bad name".to_string()),
701                PaxValue::String("active".to_string()),
702                PaxValue::String("base".to_string()),
703                PaxValue::Numeric(1.into()),
704            ]),
705            &warnings,
706            None,
707        );
708
709        assert_eq!(classes, vec!["active", "base"]);
710        assert_eq!(warnings.borrow().len(), 2);
711    }
712
713    #[test]
714    fn expression_backed_classes_track_string_and_vector_properties() {
715        let constant_binding = ValueDefinition::Expression(ExpressionInfo::new(
716            parse_pax_expression(r#""temporary""#).unwrap(),
717        ));
718        let constant_classes = build_selector_classes_property(
719            &constant_binding,
720            None,
721            &RuntimePropertiesStackFrame::new(HashMap::new()),
722        );
723        assert_eq!(constant_classes.get(), vec!["temporary"]);
724
725        let scalar = LocalProperty::new("base".to_string());
726        let stack = RuntimePropertiesStackFrame::new(HashMap::from([(
727            "current_class".to_string(),
728            Variable::new_from_typed_property(scalar.clone()),
729        )]));
730        let scalar_binding = ValueDefinition::Expression(ExpressionInfo::new(
731            parse_pax_expression("current_class").unwrap(),
732        ));
733        let scalar_classes = build_selector_classes_property(&scalar_binding, None, &stack);
734        assert_eq!(scalar_classes.get(), vec!["base"]);
735        scalar.set("active".to_string());
736        assert_eq!(scalar_classes.get(), vec!["active"]);
737
738        let vector = LocalProperty::new(vec!["base".to_string(), "selected".to_string()]);
739        let stack = RuntimePropertiesStackFrame::new(HashMap::from([(
740            "current_classes".to_string(),
741            Variable::new_from_typed_property(vector.clone()),
742        )]));
743        let vector_binding = ValueDefinition::Expression(ExpressionInfo::new(
744            parse_pax_expression("current_classes").unwrap(),
745        ));
746        let vector_classes = build_selector_classes_property(&vector_binding, None, &stack);
747        assert_eq!(vector_classes.get(), vec!["base", "selected"]);
748        vector.set(Vec::new());
749        assert!(vector_classes.get().is_empty());
750    }
751}
752
753macro_rules! dispatch_event_handler {
754    ($fn_name:ident, $arg_type:ty, $handler_key:ident, $recurse:expr) => {
755        pub fn $fn_name(
756            self: &Rc<Self>,
757            event: Event<$arg_type>,
758            globals: &Globals,
759            ctx: &Rc<RuntimeContext>,
760        ) -> bool {
761            if self.is_render_source() {
762                return event.cancelled();
763            }
764            self.run_event_handlers_for_key($handler_key, &event, ctx);
765
766            if $recurse {
767                if let Some(parent) = self.template_parent.upgrade() {
768                    return parent.$fn_name(event, globals, ctx);
769                }
770            }
771            event.cancelled()
772        }
773    };
774}
775
776impl ExpandedNode {
777    /// Whether this live node supplies a mask rather than visible scene content.
778    pub fn is_render_source(&self) -> bool {
779        self.render_source_owner.get().is_some()
780    }
781
782    /// Initialize a detached source using ordinary component lifetime machinery.
783    /// Presentation and native-resource allocation remain disabled for this tree.
784    pub fn mount_as_render_source(self: &Rc<Self>, owner: &Rc<Self>, context: &Rc<RuntimeContext>) {
785        assert_eq!(
786            self.attached.get(),
787            0,
788            "a mask cannot borrow a mounted scene node"
789        );
790        self.render_source_owner.set(Some(owner.id));
791        self.recurse_mount(context);
792    }
793
794    pub(crate) fn render_source_owner(&self) -> Option<ExpandedNodeIdentifier> {
795        self.render_source_owner.get()
796    }
797
798    fn suppresses_native_presentation(&self) -> bool {
799        self.is_render_source() && borrow!(self.instance_node).base().flags().layer == Layer::Native
800    }
801
802    pub fn initialize_root(template: Rc<ComponentInstance>, ctx: &Rc<RuntimeContext>) -> Rc<Self> {
803        let root_node = Self::new(
804            template,
805            ctx.globals().stack_frame(),
806            ctx,
807            Weak::new(),
808            Weak::new(),
809        );
810        root_node.bind_to_parent_bounds(ctx);
811        Rc::clone(&root_node).recurse_mount(ctx);
812        root_node
813    }
814
815    fn has_event_handlers(&self, handler_key: &str) -> bool {
816        borrow!(self.instance_node)
817            .base()
818            .get_handler_registry()
819            .is_some_and(|registry| {
820                borrow!(*registry)
821                    .handlers
822                    .get(handler_key)
823                    .is_some_and(|handlers| !handlers.is_empty())
824            })
825    }
826
827    fn handler_owner(self: &Rc<Self>, location: &HandlerLocation) -> Rc<Self> {
828        // Ownership follows the authored component, not the render parent.
829        // Resolve current state at dispatch time so reload cannot leave a stale handle.
830        match location {
831            HandlerLocation::Component => self.clone(),
832            HandlerLocation::Inline => self
833                .containing_component
834                .upgrade()
835                .unwrap_or_else(|| self.clone()),
836        }
837    }
838
839    fn run_event_handlers_for_key<T: Clone + 'static>(
840        self: &Rc<Self>,
841        handler_key: &str,
842        event: &Event<T>,
843        ctx: &Rc<RuntimeContext>,
844    ) {
845        if self.is_render_source() {
846            return;
847        }
848        // Input can retain a hover/focus target across a tree replacement. Its
849        // final platform event must not re-enter a detached binding lifetime.
850        if self.attached.get() == 0 {
851            return;
852        }
853        let _graph = ctx.property_graph.enter();
854        if let Some(registry) = borrow!(self.instance_node).base().get_handler_registry() {
855            let borrowed_registry = &borrow!(*registry);
856            if let Some(handlers) = borrowed_registry.handlers.get(handler_key) {
857                let context = self.get_node_context(ctx);
858                for handler in handlers {
859                    let owner = self.handler_owner(&handler.location);
860                    if owner.attached.get() == 0 {
861                        continue;
862                    }
863                    // LocalProperty bindings created by a handler belong to the
864                    // component whose state it mutates. The context's async
865                    // lifetime still identifies the exact event target node.
866                    let _scope = owner.property_binding_scope.borrow().enter();
867                    let properties = Rc::clone(&*borrow!(owner.properties));
868                    (handler.function)(properties, &context, Some(event.clone().to_pax_any()));
869                }
870            }
871        }
872    }
873
874    fn run_lifecycle_handlers_for_key(
875        self: &Rc<Self>,
876        handler_key: &str,
877        context: &Rc<RuntimeContext>,
878    ) {
879        if let Some(registry) = borrow!(self.instance_node).base().get_handler_registry() {
880            if let Some(handlers) = borrow!(registry).handlers.get(handler_key) {
881                for handler in handlers {
882                    // Refresh target-local layout for each callback, then enter
883                    // the authored owner's scope for any bindings it creates.
884                    let target_context = self.get_node_context(context);
885                    let owner = self.handler_owner(&handler.location);
886                    let _scope = owner.property_binding_scope.borrow().enter();
887                    // Final unmount marks ancestors detached before invoking
888                    // children; their state and binding scopes are still live here.
889                    (handler.function)(
890                        Rc::clone(&*borrow!(owner.properties)),
891                        &target_context,
892                        None,
893                    );
894                }
895            }
896        }
897    }
898
899    fn run_viewport_event_handlers<T: Clone + 'static>(
900        self: &Rc<Self>,
901        key: &str,
902        event: Event<T>,
903        ctx: &Rc<RuntimeContext>,
904    ) {
905        let handlers: Vec<_> = borrow!(self.instance_node)
906            .base()
907            .get_handler_registry()
908            .map(|registry| {
909                borrow!(registry)
910                    .handlers
911                    .get(key)
912                    .map(|handlers| {
913                        handlers
914                            .iter()
915                            .map(|h| (h.function, matches!(h.location, HandlerLocation::Component)))
916                            .collect()
917                    })
918                    .unwrap_or_default()
919            })
920            .unwrap_or_default();
921        for (function, component) in handlers {
922            if self.attached.get() == 0 || self.suspended.get() {
923                break;
924            }
925            let properties = if component {
926                Rc::clone(&borrow!(self.properties))
927            } else {
928                self.containing_component
929                    .upgrade()
930                    .map(|owner| Rc::clone(&borrow!(owner.properties)))
931                    .unwrap_or_else(|| Rc::clone(&borrow!(self.properties)))
932            };
933            function(
934                properties,
935                &self.get_node_context(ctx),
936                Some(event.clone().to_pax_any()),
937            );
938        }
939    }
940
941    pub(crate) fn dispatch_viewport_proximity_enter(
942        self: &Rc<Self>,
943        event: Event<crate::api::ViewportProximityEnter>,
944        ctx: &Rc<RuntimeContext>,
945    ) {
946        self.run_viewport_event_handlers(
947            crate::constants::VIEWPORT_PROXIMITY_ENTER_HANDLERS,
948            event,
949            ctx,
950        );
951    }
952    pub(crate) fn dispatch_viewport_proximity_change(
953        self: &Rc<Self>,
954        event: Event<crate::api::ViewportProximityChange>,
955        ctx: &Rc<RuntimeContext>,
956    ) {
957        self.run_viewport_event_handlers(
958            crate::constants::VIEWPORT_PROXIMITY_CHANGE_HANDLERS,
959            event,
960            ctx,
961        );
962    }
963    pub(crate) fn dispatch_viewport_proximity_exit(
964        self: &Rc<Self>,
965        event: Event<crate::api::ViewportProximityExit>,
966        ctx: &Rc<RuntimeContext>,
967    ) {
968        self.run_viewport_event_handlers(
969            crate::constants::VIEWPORT_PROXIMITY_EXIT_HANDLERS,
970            event,
971            ctx,
972        );
973    }
974
975    fn new(
976        template: Rc<dyn InstanceNode>,
977        env: Rc<RuntimePropertiesStackFrame>,
978        context: &Rc<RuntimeContext>,
979        containing_component: Weak<ExpandedNode>,
980        parent: Weak<ExpandedNode>,
981    ) -> Rc<Self> {
982        let _graph = context.property_graph.enter();
983        let property_binding_scope = context.property_graph.binding_scope();
984        let _scope = property_binding_scope.enter();
985        let transition_config = template.base().transition_config().clone();
986        let has_transition_bindings = transition_config.has_enter || transition_config.has_exit;
987        let transition_phase =
988            LocalProperty::new_with_name(TRANSITION_PHASE_IDLE, "transition phase");
989        let transition_generation = LocalProperty::new_with_name(0, "transition generation");
990        let transition_takeover = LocalProperty::new_with_name(false, "transition takeover");
991        let transition_origin_frame = LocalProperty::new_with_name(
992            context.globals().elapsed_frames.get(),
993            "transition origin",
994        );
995        let transition_origin_millis = LocalProperty::new_with_name(
996            context.globals().elapsed_millis.get(),
997            "transition origin millis",
998        );
999        let transition_playhead = LocalProperty::new_with_name(0.0, "transition playhead");
1000        let transition_playhead_millis =
1001            LocalProperty::new_with_name(0.0, "transition playhead millis");
1002
1003        let env = if has_transition_bindings {
1004            env.push(
1005                vec![
1006                    (
1007                        TRANSITION_PHASE_SYMBOL.to_string(),
1008                        Variable::new_from_typed_property(transition_phase.clone()),
1009                    ),
1010                    (
1011                        TRANSITION_GENERATION_SYMBOL.to_string(),
1012                        Variable::new_from_typed_property(transition_generation.clone()),
1013                    ),
1014                    (
1015                        TRANSITION_TAKEOVER_SYMBOL.to_string(),
1016                        Variable::new_from_typed_property(transition_takeover.clone()),
1017                    ),
1018                    (
1019                        TRANSITION_PLAYHEAD_SYMBOL.to_string(),
1020                        Variable::new_from_typed_property(transition_playhead.clone()),
1021                    ),
1022                    (
1023                        TRANSITION_PLAYHEAD_MILLIS_SYMBOL.to_string(),
1024                        Variable::new_from_typed_property(transition_playhead_millis.clone()),
1025                    ),
1026                ]
1027                .into_iter()
1028                .collect(),
1029            )
1030        } else {
1031            env
1032        };
1033
1034        // Legacy callbacks may inspect pre-bound state. Local self-timelines
1035        // also need sibling double-binding aliases installed before capturing
1036        // their scope. Keep their pre-node phase until alias installation is a
1037        // separate descriptor operation; ordinary structured nodes just allocate.
1038        let legacy_initialization = template.base().requires_pre_node_binding();
1039        let properties_init = &template.base().instance_prototypical_properties;
1040        let common_init = &template.base().instance_prototypical_common_properties;
1041        let properties = if legacy_initialization {
1042            properties_init.materialize(env.clone(), None).unwrap()
1043        } else {
1044            properties_init.allocate(env.clone())
1045        };
1046        let common_properties = if legacy_initialization {
1047            common_init.materialize(env.clone(), None).unwrap()
1048        } else {
1049            common_init.allocate(env.clone())
1050        };
1051        let selector_metadata =
1052            RuntimeSelectorMetadata::from_base(template.base(), &common_properties, &env);
1053
1054        let property_scope = if legacy_initialization {
1055            let mut scope = borrow!(*common_properties).retrieve_property_scope();
1056            scope.extend(template.base().properties_scope.build(properties.clone()));
1057            scope
1058        } else {
1059            // Publish after binding so double-binding aliases are final and
1060            // conversion-adapter properties are allocated only once.
1061            HashMap::new()
1062        };
1063
1064        let id = context.gen_uid();
1065        let render_source_owner = parent
1066            .upgrade()
1067            .and_then(|parent| parent.render_source_owner.get());
1068        let res = Rc::new(ExpandedNode {
1069            property_binding_scope: RefCell::new(property_binding_scope),
1070            async_scope: RefCell::new(None),
1071            id,
1072            stores: crate::store::NodeStores::new(parent.upgrade().as_deref()),
1073            stack: env,
1074            instance_node: RefCell::new(Rc::clone(&template)),
1075            attached: Cell::new(0),
1076            properties: RefCell::new(properties),
1077            common_properties: RefCell::new(common_properties),
1078            measured_size: LocalProperty::default(),
1079            selector_metadata: RefCell::new(selector_metadata),
1080
1081            // these two refer to their rendering parent, not their
1082            // template parent
1083            render_parent: Default::default(),
1084            parent_frame: Default::default(),
1085            liquid_glass_scope: Default::default(),
1086            template_parent: parent,
1087
1088            containing_component,
1089            children: LocalProperty::new_with_name(
1090                Vec::new(),
1091                &format!("node children (node id: {})", id.0),
1092            ),
1093            mounted_children: RefCell::new(Vec::new()),
1094            active_children: RefCell::new(Vec::new()),
1095            active_children_view: LocalProperty::new(Vec::new()),
1096            exiting_children: RefCell::new(Vec::new()),
1097            exiting_child_generations: RefCell::new(HashMap::new()),
1098            exiting_children_view: LocalProperty::new(Vec::new()),
1099            sidecar_children: RefCell::new(Vec::new()),
1100            render_source_owner: Cell::new(render_source_owner),
1101            transform_and_bounds: LocalProperty::new(TransformAndBounds::default()),
1102            subtree_layout_hull: LocalProperty::new(LayoutHull::default()),
1103            container_frame: LocalProperty::new(None),
1104            computed_opacity: LocalProperty::new(1.0),
1105            computed_opacity_scopes: LocalProperty::new(Vec::new()),
1106            expanded_projected_children: Default::default(),
1107            expanded_and_flattened_projected_children: Default::default(),
1108            flattened_projected_children_count: LocalProperty::new(0),
1109            occlusion: LocalProperty::new(Occlusion::default()),
1110            native_mask_hash: Cell::new(0),
1111            browser_content_layer_id: Cell::new(None),
1112            presentation_cache_hash: Cell::new(0),
1113            properties_scope: RefCell::new(property_scope),
1114            slot_index: LocalProperty::default(),
1115            suspended: LocalProperty::new(false),
1116            changed_listener: LocalProperty::default(),
1117            selector_classes_listener: LocalProperty::default(),
1118            occlusion_listener: LocalProperty::default(),
1119            children_listener: LocalProperty::default(),
1120            subtree_layout_hull_listener: LocalProperty::default(),
1121            parent_binding_sources: RefCell::new(None),
1122            layout_binding_generation: Cell::new(0),
1123            #[cfg(test)]
1124            parent_binding_rebuilds: Cell::new(0),
1125            #[cfg(test)]
1126            layout_hull_rebuilds: Cell::new(0),
1127            #[cfg(test)]
1128            structural_children_updates: Cell::new(0),
1129            content_measurement_listener: LocalProperty::default(),
1130            content_measurement_rebind_listener: LocalProperty::default(),
1131            content_measurement_bound: Cell::new(false),
1132            #[cfg(feature = "shaders")]
1133            shader_effect_bound: Cell::new(false),
1134            subtree_requires_non_reactive_update: Cell::new(true),
1135            projected_children_changed: LocalProperty::default(),
1136            slot_projection_changed: LocalProperty::default(),
1137            subscriptions: Default::default(),
1138            transition_phase,
1139            transition_generation,
1140            transition_takeover,
1141            transition_selector_classes: RefCell::new(None),
1142            exit_retention_generation: Cell::new(0),
1143            transition_origin_frame,
1144            transition_origin_millis,
1145            transition_playhead,
1146            transition_playhead_millis,
1147            enter_cleanup_listener: LocalProperty::default(),
1148            enter_cleanup_active: Cell::new(false),
1149            exit_started_millis: Cell::new(None),
1150            exit_timeout_warning_emitted: Cell::new(false),
1151            exit_cleanup_listener: LocalProperty::default(),
1152            exit_cleanup_active: Cell::new(false),
1153            imported_settings_layers: RefCell::new(Vec::new()),
1154            #[cfg(test)]
1155            settings_layer_materializations: Cell::new(0),
1156            import_settings_transition: RefCell::new(None),
1157            settings_motion: Rc::new(RefCell::new(HashMap::new())),
1158            settings_birth_frame: context.globals().elapsed_frames.get(),
1159            imported_settings_signature: RefCell::new(Vec::new()),
1160            resolved_property_columns: RefCell::new(BTreeMap::new()),
1161            resolved_property_provenance: RefCell::new(BTreeMap::new()),
1162            reset_removed_runtime_properties: Cell::new(false),
1163        });
1164        template.base().bind_properties(&res);
1165        if legacy_initialization {
1166            let common_properties = Rc::clone(&*borrow!(res.common_properties));
1167            *res.selector_metadata.borrow_mut() =
1168                RuntimeSelectorMetadata::from_base(template.base(), &common_properties, &res.stack);
1169        }
1170        res.refresh_properties_scope(&template);
1171        res.bind_selector_classes_listener(context);
1172        res.bind_occlusion_listener(context);
1173        res.bind_children_listener(context);
1174        res.bind_subtree_layout_hull(context);
1175        res
1176    }
1177
1178    pub fn recreate_with_new_data(
1179        self: &Rc<Self>,
1180        template: Rc<dyn InstanceNode>,
1181        context: &Rc<RuntimeContext>,
1182    ) {
1183        let _graph = context.property_graph.enter();
1184        let _scope = self.property_binding_scope.borrow().enter();
1185        // A replacement may change bindings or clipping dependencies. Revoke the
1186        // old generation before it can receive more of a frozen event batch.
1187        context.unregister_viewport_proximity(self.id.to_u32());
1188        let previous = self.stores.has_providers().then(|| {
1189            borrow!(self.properties_scope)
1190                .iter()
1191                .filter(|(name, _)| !name.starts_with('$'))
1192                .map(|(name, value)| (name.clone(), value.get_untyped_property().get_id()))
1193                .collect::<HashMap<_, _>>()
1194        });
1195        *borrow_mut!(self.instance_node) = Rc::clone(&template);
1196        template.base().bind_properties(self);
1197        self.refresh_properties_scope(&template);
1198        if let Some(previous) = previous {
1199            // Stores can retain any exposed property handle. Recreate the
1200            // provider and its consumers if a template edit changes an alias.
1201            let current: HashMap<_, _> = borrow!(self.properties_scope)
1202                .iter()
1203                .filter(|(name, _)| !name.starts_with('$'))
1204                .map(|(name, value)| (name.clone(), value.get_untyped_property().get_id()))
1205                .collect();
1206            if current != previous {
1207                self.fully_recreate_with_new_data(template, context);
1208                return;
1209            }
1210        }
1211        let common_properties = Rc::clone(&*borrow!(self.common_properties));
1212        *self.selector_metadata.borrow_mut() =
1213            RuntimeSelectorMetadata::from_base(template.base(), &common_properties, &self.stack);
1214        self.refresh_properties_scope(&template);
1215        self.bind_selector_classes_listener(context);
1216        self.reapply_runtime_settings(context);
1217        self.bind_to_parent_bounds(context);
1218        self.bind_occlusion_listener(context);
1219        self.mark_non_reactive_update_subtree_dirty();
1220        context.mark_occlusion_dirty();
1221        context.set_canvas_dirty(self.occlusion.get().render_layer_id);
1222        if self.attached.get() > 0 {
1223            context.refresh_scene_lighting_node(self);
1224            context.viewport_structure_changed();
1225            context.register_viewport_proximity(self);
1226            context.invalidate_scene_geometry(self.id.to_u32());
1227        }
1228    }
1229
1230    pub fn fully_recreate_with_new_data(
1231        self: &Rc<Self>,
1232        template: Rc<dyn InstanceNode>,
1233        context: &Rc<RuntimeContext>,
1234    ) {
1235        let _graph = context.property_graph.enter();
1236        let _scope = self.property_binding_scope.borrow().enter();
1237        Rc::clone(self).recurse_unmount(context);
1238        *borrow_mut!(self.instance_node) = template;
1239        self.prepare_remount(context);
1240        let _scope = self.property_binding_scope.borrow().enter();
1241
1242        self.bind_to_parent_bounds(context);
1243        self.mark_non_reactive_update_subtree_dirty();
1244        context.mark_occlusion_dirty();
1245        Rc::clone(self).recurse_mount(context);
1246        context.drain_node_effects();
1247        self.recurse_emit_mount_updates();
1248    }
1249
1250    fn prepare_remount(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
1251        let template = Rc::clone(&*borrow!(self.instance_node));
1252        let new_expanded_node = Self::new(
1253            template.clone(),
1254            Rc::clone(&self.stack),
1255            context,
1256            Weak::clone(&self.containing_component),
1257            Weak::clone(&self.template_parent),
1258        );
1259        *self.property_binding_scope.borrow_mut() =
1260            new_expanded_node.property_binding_scope.borrow().clone();
1261        let _scope = self.property_binding_scope.borrow().enter();
1262        *borrow_mut!(self.instance_node) = Rc::clone(&*borrow!(new_expanded_node.instance_node));
1263        *borrow_mut!(self.properties) = Rc::clone(&*borrow!(new_expanded_node.properties));
1264        *borrow_mut!(self.properties_scope) = borrow!(new_expanded_node.properties_scope).clone();
1265        *borrow_mut!(self.common_properties) =
1266            Rc::clone(&*borrow!(new_expanded_node.common_properties));
1267        *self.selector_metadata.borrow_mut() = new_expanded_node.selector_metadata.borrow().clone();
1268        self.occlusion.set(Default::default());
1269
1270        self.bind_to_parent_bounds(context);
1271        self.bind_selector_classes_listener(context);
1272        self.bind_occlusion_listener(context);
1273        self.bind_children_listener(context);
1274        self.bind_subtree_layout_hull(context);
1275        self.stores.mount(self.template_parent.upgrade().as_deref());
1276    }
1277
1278    fn recurse_emit_mount_updates(self: &Rc<Self>) {
1279        // `fully_recreate_with_new_data` can remount native-backed nodes after the main
1280        // update traversal has already consumed `changed_listener`. Emit the initial
1281        // native updates here so the chassis does not paint a frame with skeletal defaults.
1282        self.changed_listener.get();
1283        self.occlusion_listener.get();
1284        for child in self.children.get().iter() {
1285            child.recurse_emit_mount_updates();
1286        }
1287        for child in borrow!(self.sidecar_children).iter() {
1288            child.recurse_emit_mount_updates();
1289        }
1290    }
1291
1292    fn refresh_properties_scope(self: &Rc<Self>, template: &Rc<dyn InstanceNode>) {
1293        let common_properties = Rc::clone(&*borrow!(self.common_properties));
1294        let mut refreshed_scope = borrow!(*common_properties).retrieve_property_scope();
1295        refreshed_scope.extend(
1296            template
1297                .base()
1298                .properties_scope
1299                .build(Rc::clone(&*borrow!(self.properties))),
1300        );
1301        *borrow_mut!(self.properties_scope) = refreshed_scope;
1302    }
1303
1304    /// Returns whether this node is a descendant of the ExpandedNode described by `other_expanded_node_id` (id)
1305    /// Currently requires traversing linked list of ancestry, incurring a O(log(n)) cost for a tree of `n` elements.
1306    /// This could be mitigated with caching/memoization, perhaps by storing a HashSet on each ExpandedNode describing its ancestry chain.
1307    pub fn is_descendant_of(&self, other_expanded_node_id: &ExpandedNodeIdentifier) -> bool {
1308        if let Some(parent) = borrow!(self.render_parent).upgrade() {
1309            // We have a parent — if it matches the ID, this node is indeed an ancestor of other_expanded_node_id.  Otherwise, recurse upward.
1310            if parent.id.eq(other_expanded_node_id) {
1311                true
1312            } else {
1313                parent.is_descendant_of(other_expanded_node_id)
1314            }
1315        } else {
1316            false
1317        }
1318    }
1319
1320    pub fn create_children_detached(
1321        self: &Rc<Self>,
1322        templates: impl IntoIterator<Item = (Rc<dyn InstanceNode>, Rc<RuntimePropertiesStackFrame>)>,
1323        context: &Rc<RuntimeContext>,
1324        template_parent: &Weak<ExpandedNode>,
1325    ) -> Vec<Rc<ExpandedNode>> {
1326        let containing_component = if borrow!(self.instance_node).base().flags().is_component {
1327            Rc::downgrade(&self)
1328        } else {
1329            Weak::clone(&self.containing_component)
1330        };
1331
1332        let mut children = Vec::new();
1333
1334        for (template, env) in templates {
1335            children.push(Self::new(
1336                template,
1337                env,
1338                context,
1339                Weak::clone(&containing_component),
1340                Weak::clone(&template_parent),
1341            ));
1342        }
1343        children
1344    }
1345
1346    pub(crate) fn current_attached_children(&self) -> Vec<Rc<ExpandedNode>> {
1347        borrow!(self.mounted_children).clone()
1348    }
1349
1350    pub(crate) fn rescue_exiting_child_matching(
1351        self: &Rc<Self>,
1352        predicate: impl Fn(&Rc<ExpandedNode>) -> bool,
1353    ) -> Option<Rc<ExpandedNode>> {
1354        let position = borrow!(self.exiting_children).iter().position(predicate)?;
1355        let child = borrow_mut!(self.exiting_children).remove(position);
1356        child
1357            .exit_retention_generation
1358            .set(child.exit_retention_generation.get().wrapping_add(1));
1359        borrow_mut!(self.exiting_child_generations).remove(&child.id);
1360        if borrow!(self.exiting_children).is_empty() {
1361            self.exit_cleanup_active.set(false);
1362            self.exit_cleanup_listener
1363                .replace_with(LocalProperty::new_with_name((), "exit transition cleanup"));
1364        }
1365        Some(child)
1366    }
1367
1368    fn sync_mounted_children_from_active_and_exiting(&self) -> Vec<Rc<ExpandedNode>> {
1369        let active_children = borrow!(self.active_children).clone();
1370        let exiting_children = borrow!(self.exiting_children).clone();
1371        if !self
1372            .active_children_view
1373            .read(|old| Self::same_children(old, &active_children))
1374        {
1375            self.active_children_view.set(active_children.clone());
1376        }
1377        if !self
1378            .exiting_children_view
1379            .read(|old| Self::same_children(old, &exiting_children))
1380        {
1381            self.exiting_children_view.set(exiting_children.clone());
1382        }
1383        let mut combined = exiting_children;
1384        combined.extend(active_children);
1385        *borrow_mut!(self.mounted_children) = combined.clone();
1386        combined
1387    }
1388
1389    fn has_child(children: &[Rc<ExpandedNode>], target: &Rc<ExpandedNode>) -> bool {
1390        children.iter().any(|child| Rc::ptr_eq(child, target))
1391    }
1392
1393    fn same_children(a: &[Rc<ExpandedNode>], b: &[Rc<ExpandedNode>]) -> bool {
1394        a.len() == b.len() && a.iter().zip(b).all(|(a, b)| Rc::ptr_eq(a, b))
1395    }
1396
1397    fn detach_child_for_reparent(&self, target: &Rc<ExpandedNode>) {
1398        let mut removed = false;
1399        {
1400            let active_children = &mut *borrow_mut!(self.active_children);
1401            let old_len = active_children.len();
1402            active_children.retain(|child| !Rc::ptr_eq(child, target));
1403            removed |= active_children.len() != old_len;
1404        }
1405        {
1406            let exiting_children = &mut *borrow_mut!(self.exiting_children);
1407            let old_len = exiting_children.len();
1408            exiting_children.retain(|child| !Rc::ptr_eq(child, target));
1409            removed |= exiting_children.len() != old_len;
1410        }
1411        if removed {
1412            self.sync_mounted_children_from_active_and_exiting();
1413            self.mark_non_reactive_update_subtree_dirty();
1414        }
1415    }
1416
1417    fn template_node_identifier(&self) -> Option<UniqueTemplateNodeIdentifier> {
1418        borrow!(self.instance_node)
1419            .base()
1420            .template_node_identifier
1421            .clone()
1422    }
1423
1424    fn transition_source_matches(
1425        sources: &[UniqueTemplateNodeIdentifier],
1426        own_identifier: Option<&UniqueTemplateNodeIdentifier>,
1427        requested_source: Option<&UniqueTemplateNodeIdentifier>,
1428    ) -> bool {
1429        if let Some(source) = requested_source {
1430            return sources.contains(source);
1431        }
1432        if sources.is_empty() {
1433            return true;
1434        }
1435        own_identifier
1436            .map(|identifier| sources.contains(identifier))
1437            .unwrap_or(false)
1438    }
1439
1440    fn has_exit_transition_for_source(
1441        &self,
1442        requested_source: Option<&UniqueTemplateNodeIdentifier>,
1443    ) -> bool {
1444        let instance_node = borrow!(self.instance_node);
1445        let transition_config = instance_node.base().transition_config();
1446        transition_config.has_exit
1447            && Self::transition_source_matches(
1448                &transition_config.exit_sources,
1449                instance_node.base().template_node_identifier.as_ref(),
1450                requested_source,
1451            )
1452    }
1453
1454    fn mark_non_reactive_update_subtree_dirty(&self) {
1455        if self.subtree_requires_non_reactive_update.replace(true) {
1456            return;
1457        }
1458        if let Some(parent) = borrow!(self.render_parent).upgrade() {
1459            parent.mark_non_reactive_update_subtree_dirty();
1460        }
1461    }
1462
1463    fn start_self_enter_transition_for_source(
1464        self: &Rc<Self>,
1465        context: &Rc<RuntimeContext>,
1466        requested_source: Option<&UniqueTemplateNodeIdentifier>,
1467    ) -> bool {
1468        let instance_node = borrow!(self.instance_node);
1469        let transition_config = instance_node.base().transition_config();
1470        if !transition_config.has_enter
1471            || !Self::transition_source_matches(
1472                &transition_config.enter_sources,
1473                instance_node.base().template_node_identifier.as_ref(),
1474                requested_source,
1475            )
1476        {
1477            return false;
1478        }
1479        drop(instance_node);
1480        self.begin_transition_run(context, TRANSITION_PHASE_ENTER);
1481        self.exit_started_millis.set(None);
1482        self.exit_timeout_warning_emitted.set(false);
1483        self.enable_enter_cleanup_listener(context);
1484        true
1485    }
1486
1487    fn start_self_enter_transition(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
1488        let started = self.start_self_enter_transition_for_source(context, None);
1489        if started {
1490            if let Some(source) = self.template_node_identifier() {
1491                self.start_bound_enter_transitions(context, &source);
1492            }
1493        }
1494    }
1495
1496    fn start_self_exit_transition_for_source(
1497        self: &Rc<Self>,
1498        context: &Rc<RuntimeContext>,
1499        requested_source: Option<&UniqueTemplateNodeIdentifier>,
1500    ) -> bool {
1501        if !self.has_exit_transition_for_source(requested_source) {
1502            return false;
1503        }
1504        self.begin_transition_run(context, TRANSITION_PHASE_EXIT);
1505        self.disable_enter_cleanup_listener();
1506        self.exit_started_millis
1507            .set(Some((context.globals().get_elapsed_millis)()));
1508        self.exit_timeout_warning_emitted.set(false);
1509        true
1510    }
1511
1512    fn start_self_exit_transition(self: &Rc<Self>, context: &Rc<RuntimeContext>) -> bool {
1513        let started = self.start_self_exit_transition_for_source(context, None);
1514        if started {
1515            if let Some(source) = self.template_node_identifier() {
1516                self.start_bound_exit_transitions(context, &source);
1517            }
1518        }
1519        started
1520    }
1521
1522    fn begin_transition_run(self: &Rc<Self>, context: &Rc<RuntimeContext>, phase: u64) {
1523        let previous_phase = self.transition_phase.get();
1524        let is_takeover = matches!(
1525            (previous_phase, phase),
1526            (TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT)
1527                | (TRANSITION_PHASE_EXIT, TRANSITION_PHASE_ENTER)
1528        );
1529        self.transition_origin_frame
1530            .set(context.globals().elapsed_frames.get());
1531        self.transition_origin_millis
1532            .set(context.globals().elapsed_millis.get());
1533        self.transition_takeover.set(is_takeover);
1534        self.transition_generation
1535            .set(self.transition_generation.get().wrapping_add(1));
1536        self.activate_transition_clock(context);
1537        self.transition_phase.set(phase);
1538        *self.transition_selector_classes.borrow_mut() =
1539            Some(self.selector_metadata.borrow().classes.get());
1540        self.reset_removed_runtime_properties.set(true);
1541        self.reapply_runtime_settings(context);
1542    }
1543
1544    fn activate_transition_clock(&self, context: &Rc<RuntimeContext>) {
1545        let elapsed_frames = context.globals().elapsed_frames.clone();
1546        let origin_frame = self.transition_origin_frame.clone();
1547        let elapsed_frames_for_playhead = elapsed_frames.clone();
1548        let origin_frame_for_playhead = origin_frame.clone();
1549        self.transition_playhead
1550            .replace_with(LocalProperty::computed_with_name(
1551                move || {
1552                    elapsed_frames_for_playhead
1553                        .get()
1554                        .saturating_sub(origin_frame_for_playhead.get()) as f64
1555                },
1556                &[elapsed_frames.untyped(), origin_frame.untyped()],
1557                "transition playhead",
1558            ));
1559
1560        let elapsed_millis = context.globals().elapsed_millis.clone();
1561        let origin_millis = self.transition_origin_millis.clone();
1562        let elapsed_millis_for_playhead = elapsed_millis.clone();
1563        let origin_millis_for_playhead = origin_millis.clone();
1564        self.transition_playhead_millis
1565            .replace_with(LocalProperty::computed_with_name(
1566                move || {
1567                    elapsed_millis_for_playhead
1568                        .get()
1569                        .saturating_sub(origin_millis_for_playhead.get()) as f64
1570                },
1571                &[elapsed_millis.untyped(), origin_millis.untyped()],
1572                "transition playhead millis",
1573            ));
1574    }
1575
1576    fn deactivate_transition_clock(&self) {
1577        self.transition_playhead
1578            .replace_with(LocalProperty::new_with_name(0.0, "transition playhead"));
1579        self.transition_playhead_millis
1580            .replace_with(LocalProperty::new_with_name(
1581                0.0,
1582                "transition playhead millis",
1583            ));
1584    }
1585
1586    fn start_bound_enter_transitions(
1587        self: &Rc<Self>,
1588        context: &Rc<RuntimeContext>,
1589        source: &UniqueTemplateNodeIdentifier,
1590    ) {
1591        if let Some(containing_component) = self.containing_component.upgrade() {
1592            containing_component.start_descendant_enter_transitions_for_source(context, source);
1593        }
1594    }
1595
1596    fn start_bound_exit_transitions(
1597        self: &Rc<Self>,
1598        context: &Rc<RuntimeContext>,
1599        source: &UniqueTemplateNodeIdentifier,
1600    ) {
1601        if let Some(containing_component) = self.containing_component.upgrade() {
1602            containing_component.start_descendant_exit_transitions_for_source(context, source);
1603        }
1604    }
1605
1606    fn start_descendant_enter_transitions_for_source(
1607        self: &Rc<Self>,
1608        context: &Rc<RuntimeContext>,
1609        source: &UniqueTemplateNodeIdentifier,
1610    ) {
1611        for child in borrow!(self.mounted_children).iter() {
1612            child.start_enter_transition_for_source_recursive(context, source);
1613        }
1614        for child in borrow!(self.sidecar_children).iter() {
1615            child.start_enter_transition_for_source_recursive(context, source);
1616        }
1617    }
1618
1619    fn start_descendant_exit_transitions_for_source(
1620        self: &Rc<Self>,
1621        context: &Rc<RuntimeContext>,
1622        source: &UniqueTemplateNodeIdentifier,
1623    ) {
1624        for child in borrow!(self.mounted_children).iter() {
1625            child.start_exit_transition_for_source_recursive(context, source);
1626        }
1627        for child in borrow!(self.sidecar_children).iter() {
1628            child.start_exit_transition_for_source_recursive(context, source);
1629        }
1630    }
1631
1632    fn start_enter_transition_for_source_recursive(
1633        self: &Rc<Self>,
1634        context: &Rc<RuntimeContext>,
1635        source: &UniqueTemplateNodeIdentifier,
1636    ) {
1637        if self.template_node_identifier().as_ref() != Some(source) {
1638            self.start_self_enter_transition_for_source(context, Some(source));
1639        }
1640        for child in borrow!(self.mounted_children).iter() {
1641            child.start_enter_transition_for_source_recursive(context, source);
1642        }
1643        for child in borrow!(self.sidecar_children).iter() {
1644            child.start_enter_transition_for_source_recursive(context, source);
1645        }
1646    }
1647
1648    fn trigger_bound_enter_transitions_for_mounted_sources_recursive(
1649        self: &Rc<Self>,
1650        context: &Rc<RuntimeContext>,
1651    ) {
1652        if let Some(source) = self.template_node_identifier() {
1653            let should_trigger = {
1654                let instance_node = borrow!(self.instance_node);
1655                instance_node
1656                    .base()
1657                    .transition_config()
1658                    .enter_sources
1659                    .contains(&source)
1660            };
1661            if should_trigger {
1662                self.start_bound_enter_transitions(context, &source);
1663            }
1664        }
1665        for child in borrow!(self.mounted_children).iter() {
1666            child.trigger_bound_enter_transitions_for_mounted_sources_recursive(context);
1667        }
1668        for child in borrow!(self.sidecar_children).iter() {
1669            child.trigger_bound_enter_transitions_for_mounted_sources_recursive(context);
1670        }
1671    }
1672
1673    fn start_exit_transition_for_source_recursive(
1674        self: &Rc<Self>,
1675        context: &Rc<RuntimeContext>,
1676        source: &UniqueTemplateNodeIdentifier,
1677    ) {
1678        if self.template_node_identifier().as_ref() != Some(source) {
1679            self.start_self_exit_transition_for_source(context, Some(source));
1680        }
1681        for child in borrow!(self.mounted_children).iter() {
1682            child.start_exit_transition_for_source_recursive(context, source);
1683        }
1684        for child in borrow!(self.sidecar_children).iter() {
1685            child.start_exit_transition_for_source_recursive(context, source);
1686        }
1687    }
1688
1689    fn start_exit_transition_tree(self: &Rc<Self>, context: &Rc<RuntimeContext>) -> bool {
1690        let mut started = self.start_self_exit_transition(context);
1691        for child in borrow!(self.mounted_children).iter() {
1692            started |= child.start_exit_transition_tree(context);
1693        }
1694        started
1695    }
1696
1697    fn start_enter_transition_tree(self: &Rc<Self>, context: &Rc<RuntimeContext>) -> bool {
1698        let was_exiting = self.transition_phase.get() == TRANSITION_PHASE_EXIT;
1699        let started_here = self.start_self_enter_transition_for_source(context, None);
1700        if was_exiting && !started_here {
1701            self.transition_phase.set(TRANSITION_PHASE_IDLE);
1702            self.transition_takeover.set(false);
1703            self.deactivate_transition_clock();
1704            self.exit_started_millis.set(None);
1705            self.transition_selector_classes.borrow_mut().take();
1706            self.reset_removed_runtime_properties.set(true);
1707            self.reapply_runtime_settings(context);
1708        }
1709        let mut started = started_here;
1710        for child in borrow!(self.mounted_children).iter() {
1711            started |= child.start_enter_transition_tree(context);
1712        }
1713        if started_here {
1714            if let Some(source) = self.template_node_identifier() {
1715                self.start_bound_enter_transitions(context, &source);
1716            }
1717        }
1718        started
1719    }
1720
1721    fn retain_exiting_child(
1722        self: &Rc<Self>,
1723        child: Rc<ExpandedNode>,
1724        context: &Rc<RuntimeContext>,
1725    ) {
1726        if Self::has_child(&borrow!(self.exiting_children), &child) {
1727            return;
1728        }
1729        let generation = child.exit_retention_generation.get().wrapping_add(1);
1730        child.exit_retention_generation.set(generation);
1731        borrow_mut!(self.exiting_child_generations).insert(child.id, generation);
1732        borrow_mut!(self.exiting_children).push(child);
1733        self.enable_exit_cleanup_listener(context);
1734    }
1735
1736    fn self_exit_transition_complete(&self, context: &Rc<RuntimeContext>) -> bool {
1737        if self.transition_phase.get() != TRANSITION_PHASE_EXIT {
1738            return true;
1739        }
1740        let instance_node = borrow!(self.instance_node);
1741        let base = instance_node.base();
1742        let transition_config = base.transition_config().clone();
1743        let template_node_identifier = base.template_node_identifier.clone();
1744        let template_node_type_id = base.template_node_type_id.clone();
1745        drop(instance_node);
1746        let duration_complete = self.transition_duration_complete(
1747            transition_config.exit_frame_count,
1748            transition_config.exit_millis_count,
1749            &transition_config.exit_dynamic_durations,
1750        );
1751        let timeout_complete = self
1752            .exit_started_millis
1753            .get()
1754            .map(|started| {
1755                (context.globals().get_elapsed_millis)().saturating_sub(started)
1756                    >= transition_config.timeout_ms as u128
1757            })
1758            .unwrap_or(false);
1759        if timeout_complete
1760            && !duration_complete
1761            && !self.exit_timeout_warning_emitted.replace(true)
1762        {
1763            log::warn!(
1764                "Exit transition timed out after {}ms and will be truncated for node {:?}{}{}. Check @out/@transition duration, percent keyframes, and dynamic duration expressions.",
1765                transition_config.timeout_ms,
1766                self.id,
1767                template_node_identifier
1768                    .as_ref()
1769                    .map(|identifier| format!(" template={}", identifier))
1770                    .unwrap_or_default(),
1771                template_node_type_id
1772                    .as_ref()
1773                    .map(|type_id| format!(" type={}", type_id))
1774                    .unwrap_or_default(),
1775            );
1776        }
1777        duration_complete || timeout_complete
1778    }
1779
1780    fn transition_duration_complete(
1781        &self,
1782        frame_count: u64,
1783        millis_count: Option<u64>,
1784        dynamic_durations: &[ValueDefinition],
1785    ) -> bool {
1786        let mut frame_count = frame_count as f64;
1787        let mut millis_count = millis_count.map(|value| value as f64).unwrap_or_default();
1788        let mut unresolved_dynamic_duration = false;
1789
1790        for duration_definition in dynamic_durations {
1791            match evaluate_timeline_duration(duration_definition, &self.stack) {
1792                Some(duration) if duration.is_frame_based() => {
1793                    frame_count = frame_count.max(duration.as_frames_f64().max(0.0).ceil());
1794                }
1795                Some(duration) => {
1796                    millis_count = millis_count.max(duration.as_milliseconds_f64().max(0.0).ceil());
1797                }
1798                None => unresolved_dynamic_duration = true,
1799            }
1800        }
1801
1802        if unresolved_dynamic_duration
1803            && frame_count <= f64::EPSILON
1804            && millis_count <= f64::EPSILON
1805        {
1806            frame_count = 100.0;
1807        }
1808
1809        let frame_complete =
1810            frame_count <= f64::EPSILON || self.transition_playhead.get() >= frame_count;
1811        let millis_complete =
1812            millis_count <= f64::EPSILON || self.transition_playhead_millis.get() >= millis_count;
1813        frame_complete && millis_complete
1814    }
1815
1816    fn self_enter_transition_complete(&self) -> bool {
1817        if self.transition_phase.get() != TRANSITION_PHASE_ENTER {
1818            return true;
1819        }
1820        let instance_node = borrow!(self.instance_node);
1821        let transition_config = instance_node.base().transition_config().clone();
1822        drop(instance_node);
1823        self.transition_duration_complete(
1824            transition_config.enter_frame_count,
1825            transition_config.enter_millis_count,
1826            &transition_config.enter_dynamic_durations,
1827        )
1828    }
1829
1830    fn enable_enter_cleanup_listener(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
1831        if self.enter_cleanup_active.get() {
1832            return;
1833        }
1834        self.enter_cleanup_active.set(true);
1835        let weak_self = Rc::downgrade(self);
1836        let runtime_context = Rc::clone(context);
1837        let elapsed_frames = context.globals().elapsed_frames.clone();
1838        let elapsed_frames_dep = elapsed_frames.untyped();
1839        self.enter_cleanup_listener
1840            .replace_with(LocalProperty::computed_with_name(
1841                move || {
1842                    let _ = elapsed_frames.get();
1843                    if let Some(node) = weak_self.upgrade() {
1844                        node.complete_self_enter_transition_if_needed(&runtime_context);
1845                    }
1846                },
1847                &[elapsed_frames_dep],
1848                "enter transition cleanup",
1849            ));
1850        context.register_expanded_node_effect_property_named(
1851            self,
1852            &self.enter_cleanup_listener,
1853            "enter transition cleanup",
1854        );
1855    }
1856
1857    fn complete_self_enter_transition_if_needed(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
1858        if !self.enter_cleanup_active.get() {
1859            return;
1860        }
1861        if self.transition_phase.get() != TRANSITION_PHASE_ENTER {
1862            self.disable_enter_cleanup_listener();
1863            return;
1864        }
1865        if self.self_enter_transition_complete() {
1866            self.transition_phase.set(TRANSITION_PHASE_IDLE);
1867            self.deactivate_transition_clock();
1868            self.disable_enter_cleanup_listener();
1869            self.transition_selector_classes.borrow_mut().take();
1870            self.reset_removed_runtime_properties.set(true);
1871            self.reapply_runtime_settings(context);
1872        }
1873    }
1874
1875    fn disable_enter_cleanup_listener(&self) {
1876        self.enter_cleanup_active.set(false);
1877        self.enter_cleanup_listener
1878            .replace_with(LocalProperty::new_with_name((), "enter transition cleanup"));
1879    }
1880
1881    fn exit_transition_tree_complete(self: &Rc<Self>, context: &Rc<RuntimeContext>) -> bool {
1882        self.self_exit_transition_complete(context)
1883            && borrow!(self.mounted_children)
1884                .iter()
1885                .all(|child| child.exit_transition_tree_complete(context))
1886    }
1887
1888    fn enable_exit_cleanup_listener(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
1889        if self.exit_cleanup_active.get() {
1890            return;
1891        }
1892        self.exit_cleanup_active.set(true);
1893        let weak_self = Rc::downgrade(self);
1894        let cloned_context = Rc::clone(context);
1895        let elapsed_frames = context.globals().elapsed_frames.clone();
1896        let elapsed_frames_dep = elapsed_frames.untyped();
1897        self.exit_cleanup_listener
1898            .replace_with(LocalProperty::computed_with_name(
1899                move || {
1900                    let _ = elapsed_frames.get();
1901                    if let Some(node) = weak_self.upgrade() {
1902                        node.prune_completed_exit_children(&cloned_context);
1903                    }
1904                },
1905                &[elapsed_frames_dep],
1906                "exit transition cleanup",
1907            ));
1908        context.register_expanded_node_effect_property_named(
1909            self,
1910            &self.exit_cleanup_listener,
1911            "exit transition cleanup",
1912        );
1913    }
1914
1915    fn prune_completed_exit_children(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
1916        let exiting = std::mem::take(&mut *borrow_mut!(self.exiting_children));
1917        if exiting.is_empty() {
1918            self.exit_cleanup_active.set(false);
1919            self.exit_cleanup_listener
1920                .replace_with(LocalProperty::new_with_name((), "exit transition cleanup"));
1921            return;
1922        }
1923
1924        let mut retained = Vec::new();
1925        let active = borrow!(self.active_children).clone();
1926        for child in exiting {
1927            let expected_generation = borrow_mut!(self.exiting_child_generations).remove(&child.id);
1928            if Self::has_child(&active, &child)
1929                || expected_generation != Some(child.exit_retention_generation.get())
1930            {
1931                continue;
1932            }
1933            if child.exit_transition_tree_complete(context) {
1934                child.recurse_unmount(context);
1935            } else {
1936                borrow_mut!(self.exiting_child_generations)
1937                    .insert(child.id, child.exit_retention_generation.get());
1938                retained.push(child);
1939            }
1940        }
1941        *borrow_mut!(self.exiting_children) = retained;
1942        self.sync_mounted_children_from_active_and_exiting();
1943        // `children` is computed for control-flow nodes. A direct write here can
1944        // mark a pending branch update clean if cleanup and selection change in
1945        // the same frame, so invalidate it and let its evaluator reconcile.
1946        self.children.invalidate();
1947        context.mark_occlusion_dirty();
1948
1949        if borrow!(self.exiting_children).is_empty() {
1950            self.exit_cleanup_active.set(false);
1951            self.exit_cleanup_listener
1952                .replace_with(LocalProperty::new_with_name((), "exit transition cleanup"));
1953        }
1954    }
1955
1956    pub fn attach_children(
1957        self: &Rc<Self>,
1958        new_children: Vec<Rc<ExpandedNode>>,
1959        context: &Rc<RuntimeContext>,
1960        parent_frame: &LocalProperty<Option<ExpandedNodeIdentifier>>,
1961    ) -> Vec<Rc<ExpandedNode>> {
1962        self.attach_children_with_liquid_glass_scope(
1963            new_children,
1964            context,
1965            parent_frame,
1966            &self.liquid_glass_scope,
1967        )
1968    }
1969
1970    pub fn attach_children_with_liquid_glass_scope(
1971        self: &Rc<Self>,
1972        new_children: Vec<Rc<ExpandedNode>>,
1973        context: &Rc<RuntimeContext>,
1974        parent_frame: &LocalProperty<Option<ExpandedNodeIdentifier>>,
1975        liquid_glass_scope: &LocalProperty<Option<NativeLiquidGlassScope>>,
1976    ) -> Vec<Rc<ExpandedNode>> {
1977        let mut bindings_changed = false;
1978        for child in new_children.iter() {
1979            let previous_parent = borrow!(child.render_parent).upgrade();
1980            let is_reparented = child.attached.get() > 0
1981                && previous_parent
1982                    .as_ref()
1983                    .is_some_and(|parent| parent.id != self.id);
1984            if is_reparented {
1985                child.mark_canvas_subtree_dirty_for_reparent(context, true);
1986                if let Some(previous_parent) = previous_parent.as_ref() {
1987                    previous_parent.detach_child_for_reparent(child);
1988                }
1989            }
1990
1991            bindings_changed |=
1992                child.ensure_parent_bindings(self, context, parent_frame, liquid_glass_scope);
1993            if is_reparented {
1994                child.rebind_parent_bounds_for_mounted_subtree(context);
1995                child.mark_canvas_subtree_dirty_for_reparent(context, false);
1996            }
1997        }
1998        // Reconciliation still evaluates item data and parent context before
1999        // this fast path. Exiting children remain managed by their cleanup lane.
2000        if Self::same_children(&borrow!(self.active_children), &new_children)
2001            && (self.attached.get() == 0
2002                || new_children.iter().all(|child| child.attached.get() > 0))
2003        {
2004            if bindings_changed {
2005                self.mark_non_reactive_update_subtree_dirty();
2006            }
2007            return self.current_attached_children();
2008        }
2009        let mut newly_mounted_children = Vec::new();
2010        if self.attached.get() > 0 {
2011            let old_active_children = borrow!(self.active_children).clone();
2012            for child in old_active_children.iter() {
2013                if Self::has_child(&new_children, child) {
2014                    continue;
2015                }
2016                if child.transition_phase.get() == TRANSITION_PHASE_EXIT {
2017                    if child.exit_transition_tree_complete(context) {
2018                        Rc::clone(child).recurse_unmount(context);
2019                    } else {
2020                        self.retain_exiting_child(Rc::clone(child), context);
2021                    }
2022                    continue;
2023                }
2024                if child.start_exit_transition_tree(context) {
2025                    self.retain_exiting_child(Rc::clone(child), context);
2026                } else {
2027                    Rc::clone(child).recurse_unmount(context);
2028                }
2029            }
2030            for child in new_children.iter() {
2031                if !Self::has_child(&old_active_children, child) || child.attached.get() == 0 {
2032                    if child.attached.get() > 0 {
2033                        child.start_enter_transition_tree(context);
2034                    } else {
2035                        Rc::clone(child).recurse_mount(context);
2036                        newly_mounted_children.push(Rc::clone(child));
2037                    }
2038                }
2039            }
2040        }
2041        *borrow_mut!(self.active_children) = new_children;
2042        let mounted = self.sync_mounted_children_from_active_and_exiting();
2043        for child in newly_mounted_children {
2044            child.trigger_bound_enter_transitions_for_mounted_sources_recursive(context);
2045        }
2046        self.mark_non_reactive_update_subtree_dirty();
2047        mounted
2048    }
2049
2050    pub fn attach_sidecar_children(
2051        self: &Rc<Self>,
2052        new_children: Vec<Rc<ExpandedNode>>,
2053        context: &Rc<RuntimeContext>,
2054        parent_frame: &LocalProperty<Option<ExpandedNodeIdentifier>>,
2055    ) -> Vec<Rc<ExpandedNode>> {
2056        for child in new_children.iter() {
2057            child.ensure_parent_bindings(self, context, parent_frame, &self.liquid_glass_scope);
2058        }
2059        *borrow_mut!(self.sidecar_children) = new_children.clone();
2060        self.mark_non_reactive_update_subtree_dirty();
2061        new_children
2062    }
2063
2064    fn ensure_parent_bindings(
2065        self: &Rc<Self>,
2066        parent: &Rc<Self>,
2067        context: &Rc<RuntimeContext>,
2068        frame: &LocalProperty<Option<ExpandedNodeIdentifier>>,
2069        glass: &LocalProperty<Option<NativeLiquidGlassScope>>,
2070    ) -> bool {
2071        if let Some(owner) = parent.render_source_owner.get() {
2072            self.render_source_owner.set(Some(owner));
2073        }
2074        let properties = {
2075            let parent_cp = parent.get_common_properties();
2076            let parent_cp = borrow!(parent_cp);
2077            let cp = self.get_common_properties();
2078            let cp = borrow!(cp);
2079            vec![
2080                frame.untyped(),
2081                glass.untyped(),
2082                parent.suspended.untyped(),
2083                parent.transform_and_bounds.untyped(),
2084                parent.computed_opacity.untyped(),
2085                parent.computed_opacity_scopes.untyped(),
2086                parent_cp.padding_x.untyped(),
2087                parent_cp.padding_y.untyped(),
2088                self.container_frame.untyped(),
2089                self.measured_size.untyped(),
2090                cp._suspended.untyped(),
2091                cp.opacity.untyped(),
2092                cp.transform.untyped(),
2093                cp.width.untyped(),
2094                cp.height.untyped(),
2095                cp.x.untyped(),
2096                cp.y.untyped(),
2097                cp.anchor_x.untyped(),
2098                cp.anchor_y.untyped(),
2099                cp.scale_x.untyped(),
2100                cp.scale_y.untyped(),
2101                cp.skew_x.untyped(),
2102                cp.skew_y.untyped(),
2103                cp.rotate.untyped(),
2104            ]
2105        };
2106        let sources = ParentBindingSources {
2107            parent: parent.id,
2108            properties,
2109        };
2110        let same_parent = borrow!(self.render_parent)
2111            .upgrade()
2112            .is_some_and(|old| Rc::ptr_eq(&old, parent));
2113        if same_parent
2114            && borrow!(self.parent_binding_sources)
2115                .as_ref()
2116                .is_some_and(|old| old.same_as(&sources))
2117        {
2118            return false;
2119        }
2120        if self.attached.get() > 0 && context.viewport_dependency(self.id.to_u32()) {
2121            context.viewport_structure_changed();
2122        }
2123        *borrow_mut!(self.render_parent) = Rc::downgrade(parent);
2124        let frame = frame.clone();
2125        let deps = [frame.untyped()];
2126        self.parent_frame
2127            .replace_with(LocalProperty::computed(move || frame.get(), &deps));
2128        let glass = glass.clone();
2129        let deps = [glass.untyped()];
2130        self.liquid_glass_scope
2131            .replace_with(LocalProperty::computed(move || glass.get(), &deps));
2132        self.inherit_suspend(parent);
2133        self.bind_to_parent_bounds(context);
2134        *borrow_mut!(self.parent_binding_sources) = Some(sources);
2135        true
2136    }
2137
2138    fn rebind_parent_bounds_for_mounted_subtree(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
2139        self.bind_to_parent_bounds(context);
2140        self.bind_occlusion_listener(context);
2141        let mounted_children = borrow!(self.mounted_children).clone();
2142        for child in mounted_children {
2143            child.rebind_parent_bounds_for_mounted_subtree(context);
2144        }
2145        let sidecar_children = borrow!(self.sidecar_children).clone();
2146        for child in sidecar_children {
2147            child.rebind_parent_bounds_for_mounted_subtree(context);
2148        }
2149    }
2150
2151    fn mark_canvas_subtree_dirty_for_reparent(
2152        self: &Rc<Self>,
2153        context: &Rc<RuntimeContext>,
2154        queue_retained_removal: bool,
2155    ) {
2156        let layer = self.occlusion.get().render_layer_id;
2157        context.set_canvas_dirty(layer);
2158        if borrow!(self.instance_node).base().flags().layer == Layer::Canvas {
2159            if queue_retained_removal {
2160                context.enqueue_canvas_node_removal(layer, self.id.to_u32());
2161            }
2162            context.mark_canvas_node_dirty(self.id);
2163        }
2164        context.mark_occlusion_dirty();
2165
2166        let mounted_children = borrow!(self.mounted_children).clone();
2167        for child in mounted_children {
2168            child.mark_canvas_subtree_dirty_for_reparent(context, queue_retained_removal);
2169        }
2170        let sidecar_children = borrow!(self.sidecar_children).clone();
2171        for child in sidecar_children {
2172            child.mark_canvas_subtree_dirty_for_reparent(context, queue_retained_removal);
2173        }
2174    }
2175
2176    fn bind_to_parent_bounds(self: &Rc<Self>, ctx: &Rc<RuntimeContext>) {
2177        // Explicit reload/reparent rebuilds invalidate the attachment fast path.
2178        self.parent_binding_sources.borrow_mut().take();
2179        self.layout_binding_generation
2180            .set(self.layout_binding_generation.get().wrapping_add(1));
2181        self.subtree_layout_hull_listener.invalidate();
2182        self.children_listener.invalidate();
2183        #[cfg(test)]
2184        self.parent_binding_rebuilds
2185            .set(self.parent_binding_rebuilds.get() + 1);
2186        let render_parent = borrow!(self.render_parent).upgrade();
2187        let parent_transform_and_bounds = render_parent
2188            .as_ref()
2189            .map(|n| n.transform_and_bounds.clone())
2190            .unwrap_or_else(|| ctx.globals().viewport);
2191        let (parent_padding_x, parent_padding_y) = render_parent
2192            .as_ref()
2193            .map(|n| {
2194                let common_props = n.get_common_properties();
2195                let common_props = borrow!(common_props);
2196                (
2197                    common_props.padding_x.clone(),
2198                    common_props.padding_y.clone(),
2199                )
2200            })
2201            .unwrap_or_default();
2202        let container_frame = self.container_frame.clone();
2203        let deps = [
2204            parent_transform_and_bounds.untyped(),
2205            parent_padding_x.untyped(),
2206            parent_padding_y.untyped(),
2207            container_frame.untyped(),
2208        ];
2209        let effective_parent_transform_and_bounds = LocalProperty::computed(
2210            move || {
2211                apply_container_frame(
2212                    apply_padding_frame(
2213                        parent_transform_and_bounds.get(),
2214                        parent_padding_x.get(),
2215                        parent_padding_y.get(),
2216                    ),
2217                    container_frame.get(),
2218                )
2219            },
2220            &deps,
2221        );
2222        let common_props = borrow!(self.common_properties);
2223        let extra_transform = borrow!(common_props).transform.clone();
2224
2225        let transform_and_bounds = compute_tab(
2226            self.layout_properties(),
2227            extra_transform,
2228            effective_parent_transform_and_bounds,
2229        );
2230        self.transform_and_bounds.replace_with(transform_and_bounds);
2231
2232        let parent_opacity = borrow!(self.render_parent)
2233            .upgrade()
2234            .map(|n| n.computed_opacity.clone())
2235            .unwrap_or_else(|| LocalProperty::new(1.0));
2236        let common_props = self.get_common_properties();
2237        let self_opacity = borrow!(common_props).opacity.clone();
2238        let parent_scopes = render_parent
2239            .as_ref()
2240            .map(|n| n.computed_opacity_scopes.clone())
2241            .unwrap_or_else(|| LocalProperty::new(Vec::new()));
2242        let local_opacity = self_opacity.clone();
2243        let node_id = self.id.to_u32();
2244        let scope_deps = [parent_scopes.untyped(), local_opacity.untyped()];
2245        self.computed_opacity_scopes
2246            .replace_with(LocalProperty::computed(
2247                move || {
2248                    let mut scopes = parent_scopes.get();
2249                    if let Some(opacity) = local_opacity.get() {
2250                        scopes.push(OpacityScope {
2251                            node_id,
2252                            opacity: opacity.to_float_0_1().clamp(0.0, 1.0) as f32,
2253                        });
2254                    }
2255                    scopes
2256                },
2257                &scope_deps,
2258            ));
2259        let deps = [parent_opacity.untyped(), self_opacity.untyped()];
2260        self.computed_opacity.replace_with(LocalProperty::computed(
2261            move || {
2262                let parent = parent_opacity.get().clamp(0.0, 1.0);
2263                let local = self_opacity
2264                    .get()
2265                    .unwrap_or_default()
2266                    .to_float_0_1()
2267                    .clamp(0.0, 1.0);
2268                (parent * local).clamp(0.0, 1.0)
2269            },
2270            &deps,
2271        ));
2272    }
2273
2274    fn bind_occlusion_listener(self: &Rc<Self>, ctx: &Rc<RuntimeContext>) {
2275        let instance_node = borrow!(self.instance_node);
2276        let mut deps: Vec<_> = borrow!(self.properties_scope)
2277            .iter()
2278            .filter(|(name, _)| instance_node.property_requires_occlusion_recompute(name))
2279            .map(|(_, v)| v.get_untyped_property().clone())
2280            .collect();
2281        drop(instance_node);
2282
2283        // Structural child changes are handled by children_listener after
2284        // reconciliation; data-only repeat invalidation is not an occlusion change.
2285        deps.extend([
2286            self.transform_and_bounds.untyped(),
2287            self.computed_opacity.untyped(),
2288            self.liquid_glass_scope.untyped(),
2289            self.get_common_properties().borrow().unclippable.untyped(),
2290        ]);
2291
2292        let context = Rc::clone(ctx);
2293        let id = self.id.to_u32();
2294        self.occlusion_listener
2295            .replace_with(LocalProperty::computed_with_name(
2296                move || {
2297                    context.mark_node_occlusion_dirty(id);
2298                },
2299                &deps,
2300                "occlusion listener",
2301            ));
2302        ctx.register_expanded_node_effect_property_named(
2303            self,
2304            &self.occlusion_listener,
2305            "occlusion listener",
2306        );
2307    }
2308
2309    fn reapply_runtime_settings(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
2310        let instance = Rc::clone(&*borrow!(self.instance_node));
2311        instance.base().bind_properties(self);
2312        self.reset_removed_runtime_properties.set(false);
2313        self.refresh_properties_scope(&instance);
2314        self.mark_non_reactive_update_subtree_dirty();
2315        context.mark_occlusion_dirty();
2316        context.set_canvas_dirty(self.occlusion.get().render_layer_id);
2317    }
2318
2319    fn bind_selector_classes_listener(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
2320        let classes = self.selector_metadata.borrow().classes.clone();
2321        let previous = Rc::new(RefCell::new(classes.get()));
2322        let weak_self = Rc::downgrade(self);
2323        let runtime_context = Rc::clone(context);
2324        self.selector_classes_listener
2325            .replace_with(LocalProperty::computed_with_name(
2326                move || {
2327                    let current = classes.get();
2328                    if *previous.borrow() == current {
2329                        return;
2330                    }
2331                    *previous.borrow_mut() = current;
2332                    let Some(node) = weak_self.upgrade() else {
2333                        return;
2334                    };
2335                    node.reset_removed_runtime_properties.set(true);
2336                    node.reapply_runtime_settings(&runtime_context);
2337                },
2338                &[self.selector_metadata.borrow().classes.untyped()],
2339                "selector classes listener",
2340            ));
2341        context.register_expanded_node_effect_property_named(
2342            self,
2343            &self.selector_classes_listener,
2344            "selector classes listener",
2345        );
2346    }
2347
2348    fn bind_children_listener(self: &Rc<Self>, ctx: &Rc<RuntimeContext>) {
2349        let deps = [self.children.untyped()];
2350        let weak_self = Rc::downgrade(self);
2351        let context = Rc::clone(ctx);
2352        let previous = RefCell::new(None);
2353        self.children_listener
2354            .replace_with(LocalProperty::computed_with_name(
2355                move || {
2356                    let Some(node) = weak_self.upgrade() else {
2357                        return;
2358                    };
2359                    if !node.child_structure_changed(&previous) {
2360                        return;
2361                    }
2362                    #[cfg(test)]
2363                    node.structural_children_updates
2364                        .set(node.structural_children_updates.get() + 1);
2365                    if borrow!(node.instance_node).base().flags().is_component
2366                        || borrow!(node.expanded_projected_children).is_some()
2367                    {
2368                        node.compute_flattened_projected_children();
2369                    }
2370                    // Projected if/for nodes are flattened out of the render
2371                    // tree. Their structural changes must refresh the receiving
2372                    // component's input list, even when it was previously empty.
2373                    if borrow!(node.instance_node).base().flags().invisible_to_slot {
2374                        let mut ancestor = node.template_parent.upgrade();
2375                        while let Some(parent) = ancestor {
2376                            if borrow!(parent.expanded_projected_children).is_some() {
2377                                parent.compute_flattened_projected_children();
2378                                break;
2379                            }
2380                            if !borrow!(parent.instance_node)
2381                                .base()
2382                                .flags()
2383                                .invisible_to_slot
2384                            {
2385                                break;
2386                            }
2387                            ancestor = parent.template_parent.upgrade();
2388                        }
2389                    }
2390                    context.mark_occlusion_dirty();
2391
2392                    let (is_slot, is_component) = {
2393                        let instance = borrow!(node.instance_node);
2394                        let flags = instance.base().flags();
2395                        (flags.is_slot, flags.is_component)
2396                    };
2397                    if !is_slot {
2398                        if is_component {
2399                            node.slot_projection_changed.set(());
2400                        } else if let Some(containing_component) =
2401                            node.containing_component.upgrade()
2402                        {
2403                            containing_component.slot_projection_changed.set(());
2404                        }
2405                    }
2406                },
2407                &deps,
2408                "children listener",
2409            ));
2410        ctx.register_expanded_node_effect_property_named(
2411            self,
2412            &self.children_listener,
2413            "children listener",
2414        );
2415    }
2416
2417    fn bind_subtree_layout_hull(self: &Rc<Self>, ctx: &Rc<RuntimeContext>) {
2418        self.rebind_subtree_layout_hull();
2419        let deps = [self.children.untyped()];
2420        let weak_self = Rc::downgrade(self);
2421        let previous = RefCell::new(None);
2422        self.subtree_layout_hull_listener
2423            .replace_with(LocalProperty::computed_with_name(
2424                move || {
2425                    let Some(node) = weak_self.upgrade() else {
2426                        return;
2427                    };
2428                    if node.child_structure_changed(&previous) {
2429                        node.rebind_subtree_layout_hull();
2430                    }
2431                },
2432                &deps,
2433                "subtree layout hull listener",
2434            ));
2435        ctx.register_expanded_node_effect_property_named(
2436            self,
2437            &self.subtree_layout_hull_listener,
2438            "subtree layout hull listener",
2439        );
2440    }
2441    fn child_structure_changed(&self, previous: &RefCell<Option<ChildStructure>>) -> bool {
2442        // Always evaluate first: reconciliation may update data, rescue an exit,
2443        // or remove a completed exit even when the source keys are unchanged.
2444        let children = self.children.get();
2445        let ids = |children: &[Rc<ExpandedNode>]| {
2446            children.iter().map(|child| child.id).collect::<Vec<_>>()
2447        };
2448        // Slot projection distinguishes active from exiting children even when
2449        // their combined render order is unchanged at the start of an exit.
2450        let current = ChildStructure {
2451            binding_generation: self.layout_binding_generation.get(),
2452            rendered: ids(&children),
2453            active: ids(&borrow!(self.active_children)),
2454            exiting: ids(&borrow!(self.exiting_children)),
2455        };
2456        let mut previous = previous.borrow_mut();
2457        if previous.as_ref() == Some(&current) {
2458            return false;
2459        }
2460        *previous = Some(current);
2461        true
2462    }
2463
2464    fn rebind_subtree_layout_hull(self: &Rc<Self>) {
2465        #[cfg(test)]
2466        self.layout_hull_rebuilds
2467            .set(self.layout_hull_rebuilds.get() + 1);
2468        let self_transform_and_bounds = self.transform_and_bounds.clone();
2469        let layout_properties = self.layout_properties();
2470        let common_props = self.get_common_properties();
2471        let (width, height, x, y, padding_x, padding_y) = {
2472            let common_props = borrow!(common_props);
2473            (
2474                common_props.width.clone(),
2475                common_props.height.clone(),
2476                common_props.x.clone(),
2477                common_props.y.clone(),
2478                common_props.padding_x.clone(),
2479                common_props.padding_y.clone(),
2480            )
2481        };
2482        let children = self.children.get();
2483        let has_children = !children.is_empty();
2484        let bounds_name = format!("layout bounds (node id: {})", self.id.0);
2485        let bounds_transform_and_bounds = self_transform_and_bounds.clone();
2486        let bounds = LocalProperty::computed_with_cutoff_and_name(
2487            move || bounds_transform_and_bounds.get().bounds,
2488            &[self_transform_and_bounds.untyped()],
2489            <(f64, f64)>::eq,
2490            &bounds_name,
2491        );
2492        let mut own_deps = vec![bounds.untyped()];
2493        if has_children {
2494            own_deps.extend([width.untyped(), height.untyped(), x.untyped(), y.untyped()]);
2495        } else {
2496            own_deps.push(layout_properties.untyped());
2497        }
2498
2499        let own_hull_name = format!("own layout hull (node id: {})", self.id.0);
2500        let own_hull = LocalProperty::computed_with_cutoff_and_name(
2501            move || {
2502                let bounds = bounds.get();
2503                let (contributes_x, contributes_y) = if has_children {
2504                    (
2505                        layout_axis_can_contribute_from_parts(width.get(), x.get()),
2506                        layout_axis_can_contribute_from_parts(height.get(), y.get()),
2507                    )
2508                } else {
2509                    let layout_properties = layout_properties.get();
2510                    (
2511                        layout_axis_can_contribute(&layout_properties, Axis::X),
2512                        layout_axis_can_contribute(&layout_properties, Axis::Y),
2513                    )
2514                };
2515                LayoutHull::from_axis_ranges(
2516                    contributes_x.then_some((0.0, bounds.0)),
2517                    contributes_y.then_some((0.0, bounds.1)),
2518                )
2519            },
2520            &own_deps,
2521            crate::layout_hulls_equivalent,
2522            &own_hull_name,
2523        );
2524
2525        if !has_children {
2526            self.subtree_layout_hull.replace_with(own_hull);
2527            return;
2528        }
2529
2530        let mut projected_child_hulls = Vec::with_capacity(children.len());
2531        for child in children.iter() {
2532            let child_cp = child.get_common_properties();
2533            let child_layout_role = borrow!(child_cp).layout_role.clone();
2534            let projection_name = format!(
2535                "projected child layout hull (parent id: {}, child id: {})",
2536                self.id.0, child.id.0
2537            );
2538            projected_child_hulls.push(crate::projected_child_layout_hull_property(
2539                self_transform_and_bounds.clone(),
2540                padding_x.clone(),
2541                padding_y.clone(),
2542                child.transform_and_bounds.clone(),
2543                child.subtree_layout_hull.clone(),
2544                child_layout_role,
2545                &projection_name,
2546            ));
2547        }
2548
2549        let mut deps = vec![own_hull.untyped(), padding_x.untyped(), padding_y.untyped()];
2550        deps.extend(projected_child_hulls.iter().map(LocalProperty::untyped));
2551
2552        let property_name = format!("subtree layout hull (node id: {})", self.id.0);
2553        self.subtree_layout_hull
2554            .replace_with(LocalProperty::computed_with_cutoff_and_name(
2555                move || {
2556                    let mut hull = own_hull.get();
2557                    let mut children_hull = LayoutHull::default();
2558                    for projected_hull in &projected_child_hulls {
2559                        children_hull = children_hull.union(projected_hull.get());
2560                    }
2561                    hull = hull.union(add_symmetric_padding_to_content_layout_hull(
2562                        children_hull,
2563                        padding_x.get(),
2564                        padding_y.get(),
2565                    ));
2566
2567                    hull
2568                },
2569                &deps,
2570                crate::layout_hulls_equivalent,
2571                &property_name,
2572            ));
2573    }
2574
2575    pub fn inherit_suspend(self: &Rc<Self>, node: &Rc<Self>) {
2576        self.parent_binding_sources.borrow_mut().take();
2577        let cp = self.get_common_properties();
2578        let self_suspended = borrow!(cp)._suspended.clone();
2579        let parent_suspended = node.suspended.clone();
2580        let deps = [parent_suspended.untyped(), self_suspended.untyped()];
2581        self.suspended.replace_with(LocalProperty::computed(
2582            move || {
2583                self_suspended
2584                    .get()
2585                    .unwrap_or_else(|| parent_suspended.get())
2586            },
2587            &deps,
2588        ));
2589    }
2590
2591    pub fn generate_children(
2592        self: &Rc<Self>,
2593        templates: impl IntoIterator<Item = (Rc<dyn InstanceNode>, Rc<RuntimePropertiesStackFrame>)>,
2594        context: &Rc<RuntimeContext>,
2595        parent_frame: &LocalProperty<Option<ExpandedNodeIdentifier>>,
2596        is_mount: bool,
2597    ) -> Vec<Rc<ExpandedNode>> {
2598        self.generate_children_with_liquid_glass_scope(
2599            templates,
2600            context,
2601            parent_frame,
2602            &self.liquid_glass_scope,
2603            is_mount,
2604        )
2605    }
2606
2607    pub fn generate_children_with_liquid_glass_scope(
2608        self: &Rc<Self>,
2609        templates: impl IntoIterator<Item = (Rc<dyn InstanceNode>, Rc<RuntimePropertiesStackFrame>)>,
2610        context: &Rc<RuntimeContext>,
2611        parent_frame: &LocalProperty<Option<ExpandedNodeIdentifier>>,
2612        liquid_glass_scope: &LocalProperty<Option<NativeLiquidGlassScope>>,
2613        is_mount: bool,
2614    ) -> Vec<Rc<ExpandedNode>> {
2615        let new_children = self.create_children_detached(templates, context, &Rc::downgrade(&self));
2616        let res = if is_mount {
2617            self.attach_children_with_liquid_glass_scope(
2618                new_children,
2619                context,
2620                parent_frame,
2621                liquid_glass_scope,
2622            )
2623        } else {
2624            for child in new_children.iter() {
2625                child.recurse_control_flow_expansion(context);
2626            }
2627
2628            new_children
2629        };
2630        res
2631    }
2632
2633    /// Run tick or pre-render handlers on this node unless it is detached or suspended.
2634    /// Inline handlers receive their authored component's state and this node's context.
2635    pub fn run_lifecycle_handlers(
2636        self: &Rc<Self>,
2637        handler_key: &str,
2638        context: &Rc<RuntimeContext>,
2639    ) {
2640        if self.attached.get() == 0 || self.suspended.get() {
2641            return;
2642        }
2643        let _graph = context.property_graph.enter();
2644        let _scope = self.property_binding_scope.borrow().enter();
2645        self.run_lifecycle_handlers_for_key(handler_key, context);
2646    }
2647
2648    pub fn recurse_update(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
2649        // Settle queued reactive effects before checking which subtrees still need
2650        // imperative work, then flush any effects produced by that non-reactive pass.
2651        context.drain_node_effects();
2652        self.recurse_update_mounted(context);
2653        context.drain_node_effects();
2654    }
2655
2656    fn recurse_update_mounted(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
2657        if self.attached.get() == 0 || !self.subtree_requires_non_reactive_update.get() {
2658            return;
2659        }
2660        let _graph = context.property_graph.enter();
2661        let _scope = self.property_binding_scope.borrow().enter();
2662        let instance_node = Rc::clone(&*borrow!(self.instance_node));
2663        if !self.suppresses_native_presentation()
2664            && instance_node.requires_non_reactive_update(self)
2665        {
2666            instance_node.update(&self, context);
2667        }
2668        let mut subtree_requires_non_reactive_update = !self.suppresses_native_presentation()
2669            && Rc::clone(&*borrow!(self.instance_node)).requires_non_reactive_update(self);
2670        let children = borrow!(self.mounted_children).clone();
2671        for child in children.iter() {
2672            child.recurse_update_mounted(context);
2673            subtree_requires_non_reactive_update |=
2674                child.subtree_requires_non_reactive_update.get();
2675        }
2676        let sidecar_children = borrow!(self.sidecar_children).clone();
2677        for child in sidecar_children.iter() {
2678            child.recurse_update_mounted(context);
2679            subtree_requires_non_reactive_update |=
2680                child.subtree_requires_non_reactive_update.get();
2681        }
2682        self.subtree_requires_non_reactive_update
2683            .set(subtree_requires_non_reactive_update);
2684    }
2685
2686    pub fn recurse_sync_import_settings(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
2687        if borrow!(self.instance_node).base().flags().is_component {
2688            self.sync_imported_settings(context);
2689        }
2690        for child in self.children.get().iter() {
2691            child.recurse_sync_import_settings(context);
2692        }
2693        for child in borrow!(self.sidecar_children).iter() {
2694            child.recurse_sync_import_settings(context);
2695        }
2696    }
2697
2698    fn recurse_reapply_runtime_settings_for_component_owner(
2699        self: &Rc<Self>,
2700        owner_component_id: ExpandedNodeIdentifier,
2701        context: &Rc<RuntimeContext>,
2702    ) {
2703        if self.is_import_settings_node() {
2704            return;
2705        }
2706
2707        let owned_by_component = self
2708            .containing_component
2709            .upgrade()
2710            .map(|component| component.id == owner_component_id)
2711            .unwrap_or(false);
2712
2713        if owned_by_component {
2714            self.reset_removed_runtime_properties.set(true);
2715            let instance = borrow!(self.instance_node).clone();
2716            self.recreate_with_new_data(instance, context);
2717        }
2718
2719        let children = self.children.get();
2720        for child in children.iter() {
2721            child.recurse_reapply_runtime_settings_for_component_owner(owner_component_id, context);
2722        }
2723
2724        let sidecar_children = borrow!(self.sidecar_children).clone();
2725        for child in sidecar_children.iter() {
2726            child.recurse_reapply_runtime_settings_for_component_owner(owner_component_id, context);
2727        }
2728    }
2729
2730    pub fn recurse_control_flow_expansion(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
2731        borrow!(self.instance_node)
2732            .clone()
2733            .handle_control_flow_node_expansion(&self, context);
2734    }
2735
2736    pub fn recurse_mount(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
2737        let _graph = context.property_graph.enter();
2738        if self.attached.get() == 0 && self.stores.is_closed() {
2739            self.prepare_remount(context);
2740        }
2741        self.property_binding_scope.borrow().resume();
2742        let _scope = self.property_binding_scope.borrow().enter();
2743        if self.attached.get() == 0 {
2744            self.stores.mount(self.template_parent.upgrade().as_deref());
2745            // All bindings are installed before mount. Materialize their local
2746            // views now so shared snapshots seen by handlers are initialized.
2747            let scope: Vec<_> = borrow!(self.properties_scope).values().cloned().collect();
2748            for property in scope {
2749                property.get_as_pax_value();
2750            }
2751            // Materialize projected children before mount so Slot can resolve them.
2752            borrow!(self.instance_node)
2753                .clone()
2754                .handle_setup_projected_children(&self, context);
2755
2756            // Pre-mount pass to make sure projected children are expanded and we compute the correct flattened list.
2757            if let Some(projected_children) = borrow!(self.expanded_projected_children).as_ref() {
2758                for projected_child in projected_children {
2759                    projected_child.recurse_control_flow_expansion(context);
2760                }
2761                // This is needed to resolve slot connections in a single tick.
2762                self.compute_flattened_projected_children();
2763            }
2764            self.attached.set(self.attached.get() + 1);
2765            self.start_self_enter_transition(context);
2766            context.add_to_cache(&self);
2767            self.run_lifecycle_handlers_for_key("mount", context);
2768            // Native source leaves have no GPU coverage. Keep their properties
2769            // available to component logic, but create no native surfaces or
2770            // native update subscriptions.
2771            if !self.suppresses_native_presentation() {
2772                borrow!(self.instance_node)
2773                    .clone()
2774                    .handle_mount(&self, context);
2775            }
2776            context.register_expanded_node_effect_property_named(
2777                self,
2778                &self.changed_listener,
2779                "changed listener",
2780            );
2781            context.register_viewport_proximity(self);
2782        }
2783    }
2784
2785    pub fn async_scope(&self, context: &Rc<RuntimeContext>) -> Result<AsyncScope, AsyncError> {
2786        if self.attached.get() == 0 {
2787            return Err(AsyncError::Closed);
2788        }
2789        if let Some(scope) = self.async_scope.borrow().as_ref() {
2790            return Ok(scope.clone());
2791        }
2792        let scope = context.application.context().async_scope()?.child()?;
2793        let binding = self.property_binding_scope.borrow().clone();
2794        scope.set_callback_context(move |callback| binding.with_graph(callback));
2795        *self.async_scope.borrow_mut() = Some(scope.clone());
2796        Ok(scope)
2797    }
2798
2799    pub fn recurse_unmount(self: Rc<Self>, context: &Rc<RuntimeContext>) {
2800        if self.attached.get() != 1 {
2801            return;
2802        }
2803        let _graph = context.property_graph.enter();
2804        let _scope = self.property_binding_scope.borrow().enter();
2805        // WARNING: do NOT make recurse_unmount result in expr evaluation,
2806        // in this case: do not refer to self.children expression.
2807        // expr evaluation in this context can trigger get's of "old data", ie try to get
2808        // an index of a for loop source that doesn't exist anymore
2809        if self.attached.get() == 1 {
2810            if let Some(scope) = self.async_scope.borrow_mut().take() {
2811                scope.cancel();
2812            }
2813            self.attached.set(self.attached.get() - 1);
2814            context.remove_from_cache(&self);
2815            for child in borrow!(self.mounted_children).iter() {
2816                Rc::clone(child).recurse_unmount(context);
2817            }
2818            if !self.suppresses_native_presentation() {
2819                borrow!(self.instance_node).handle_unmount(&self, context);
2820            }
2821            self.run_lifecycle_handlers_for_key("unmount", context);
2822
2823            if !self.is_render_source()
2824                && self.instance_node.borrow().base().flags().layer == Layer::Canvas
2825            {
2826                context.enqueue_canvas_node_removal(
2827                    self.occlusion.get().render_layer_id,
2828                    self.id.to_u32(),
2829                );
2830                context.set_canvas_dirty(self.occlusion.get().render_layer_id);
2831            }
2832
2833            // Needed because occlusion updates are only sent on diffs so we reset it when unmounting
2834            self.occlusion.set(Default::default());
2835            self.browser_content_layer_id.set(None);
2836            self.changed_listener.replace_with(LocalProperty::default());
2837            self.selector_classes_listener
2838                .replace_with(LocalProperty::default());
2839            borrow_mut!(self.subscriptions).clear();
2840            borrow_mut!(self.active_children).clear();
2841            borrow_mut!(self.exiting_children).clear();
2842            borrow_mut!(self.exiting_child_generations).clear();
2843            borrow_mut!(self.mounted_children).clear();
2844            borrow_mut!(self.settings_motion).clear();
2845            borrow_mut!(self.import_settings_transition).take();
2846            borrow_mut!(self.imported_settings_layers).clear();
2847            borrow_mut!(self.imported_settings_signature).clear();
2848            borrow_mut!(self.resolved_property_columns).clear();
2849            borrow_mut!(self.resolved_property_provenance).clear();
2850            self.reset_removed_runtime_properties.set(false);
2851            self.active_children_view.set(Vec::new());
2852            self.exiting_children_view.set(Vec::new());
2853            self.transition_phase.set(TRANSITION_PHASE_IDLE);
2854            self.transition_takeover.set(false);
2855            self.transition_selector_classes.borrow_mut().take();
2856            self.deactivate_transition_clock();
2857            self.enter_cleanup_active.set(false);
2858            self.enter_cleanup_listener
2859                .replace_with(LocalProperty::new_with_name((), "enter transition cleanup"));
2860            self.exit_cleanup_active.set(false);
2861            self.exit_cleanup_listener
2862                .replace_with(LocalProperty::default());
2863            self.stores.close();
2864            // Final unmount releases component-owned state without evaluating
2865            // bindings against potentially removed repeat items. Remount allocates
2866            // fresh properties; externally shared handles remain their owner's.
2867            self.children.replace_with(LocalProperty::new(Vec::new()));
2868            self.children_listener
2869                .replace_with(LocalProperty::default());
2870            self.occlusion_listener
2871                .replace_with(LocalProperty::default());
2872            self.subtree_layout_hull_listener
2873                .replace_with(LocalProperty::default());
2874            self.content_measurement_listener
2875                .replace_with(LocalProperty::default());
2876            self.content_measurement_rebind_listener
2877                .replace_with(LocalProperty::default());
2878            self.content_measurement_bound.set(false);
2879            borrow_mut!(self.sidecar_children).clear();
2880            borrow_mut!(self.expanded_projected_children).take();
2881            self.expanded_and_flattened_projected_children
2882                .set(Vec::new());
2883            self.flattened_projected_children_count.set(0);
2884            self.parent_binding_sources.borrow_mut().take();
2885            self.transform_and_bounds
2886                .replace_with(LocalProperty::default());
2887            self.computed_opacity.replace_with(LocalProperty::new(1.0));
2888            self.subtree_layout_hull
2889                .replace_with(LocalProperty::default());
2890            self.measured_size.set(None);
2891            *borrow_mut!(self.common_properties) =
2892                Rc::new(RefCell::new(CommonProperties::default()));
2893            self.selector_metadata.borrow_mut().id = LocalProperty::default();
2894            self.selector_metadata.borrow_mut().classes = LocalProperty::default();
2895            borrow_mut!(self.properties_scope).clear();
2896            *borrow_mut!(self.properties) =
2897                Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default())));
2898            self.property_binding_scope.borrow().close();
2899        }
2900    }
2901
2902    pub fn is_import_settings_node(&self) -> bool {
2903        borrow!(self.instance_node)
2904            .base()
2905            .template_node_type_id
2906            .as_ref()
2907            .and_then(|type_id| type_id.get_pascal_identifier())
2908            .as_deref()
2909            == Some("ImportSettings")
2910    }
2911
2912    pub fn is_in_import_settings_subtree(&self) -> bool {
2913        let mut current = borrow!(self.render_parent).upgrade();
2914        while let Some(node) = current {
2915            if node.is_import_settings_node() {
2916                return true;
2917            }
2918            current = borrow!(node.render_parent).upgrade();
2919        }
2920        false
2921    }
2922
2923    fn collect_imported_settings_from_node(
2924        node: &Rc<Self>,
2925        in_import_settings: bool,
2926        descend_components: bool,
2927        providers: &mut Vec<DiscoveredSettingsProvider>,
2928        transition: Option<LocalProperty<Option<crate::SettingsTransitionConfig>>>,
2929    ) {
2930        if node.is_import_settings_node() {
2931            let sidecar_children = borrow!(node.sidecar_children).clone();
2932            for child in sidecar_children.iter() {
2933                Self::collect_imported_settings_from_node(
2934                    child,
2935                    true,
2936                    true,
2937                    providers,
2938                    transition
2939                        .clone()
2940                        .or_else(|| node.import_settings_transition.borrow().clone()),
2941                );
2942            }
2943            return;
2944        }
2945
2946        if borrow!(node.instance_node).base().flags().is_component {
2947            if in_import_settings {
2948                if borrow!(node.instance_node)
2949                    .base()
2950                    .component_settings
2951                    .as_ref()
2952                    .is_some_and(|settings| !settings.is_empty())
2953                {
2954                    providers.push(DiscoveredSettingsProvider {
2955                        node: Rc::clone(node),
2956                        transition: transition.clone(),
2957                    });
2958                }
2959                let children = node.children.get();
2960                for child in children.iter() {
2961                    Self::collect_imported_settings_from_node(
2962                        child,
2963                        false,
2964                        true,
2965                        providers,
2966                        transition.clone(),
2967                    );
2968                }
2969                let sidecar_children = borrow!(node.sidecar_children).clone();
2970                for child in sidecar_children.iter() {
2971                    Self::collect_imported_settings_from_node(
2972                        child,
2973                        false,
2974                        true,
2975                        providers,
2976                        transition.clone(),
2977                    );
2978                }
2979                return;
2980            }
2981            if !descend_components {
2982                return;
2983            }
2984        }
2985
2986        let children = node.children.get();
2987        for child in children.iter() {
2988            Self::collect_imported_settings_from_node(
2989                child,
2990                in_import_settings,
2991                descend_components,
2992                providers,
2993                transition.clone(),
2994            );
2995        }
2996        let sidecar_children = borrow!(node.sidecar_children).clone();
2997        for child in sidecar_children.iter() {
2998            Self::collect_imported_settings_from_node(
2999                child,
3000                in_import_settings,
3001                descend_components,
3002                providers,
3003                transition.clone(),
3004            );
3005        }
3006    }
3007
3008    pub fn sync_imported_settings(self: &Rc<Self>, context: &Rc<RuntimeContext>) {
3009        let mut providers = Vec::new();
3010        let children = self.children.get();
3011        for child in children.iter() {
3012            Self::collect_imported_settings_from_node(child, false, false, &mut providers, None);
3013        }
3014        let sidecar_children = borrow!(self.sidecar_children).clone();
3015        for child in sidecar_children.iter() {
3016            Self::collect_imported_settings_from_node(child, false, false, &mut providers, None);
3017        }
3018
3019        let signature = providers
3020            .iter()
3021            .map(DiscoveredSettingsProvider::signature)
3022            .collect::<Vec<_>>();
3023        if *borrow!(self.imported_settings_signature) == signature {
3024            return;
3025        }
3026
3027        *borrow_mut!(self.imported_settings_signature) = signature;
3028        *borrow_mut!(self.imported_settings_layers) = providers
3029            .into_iter()
3030            .map(DiscoveredSettingsProvider::materialize)
3031            .collect();
3032
3033        let root_children = self.children.get();
3034        for child in root_children.iter() {
3035            if child.is_import_settings_node() {
3036                continue;
3037            }
3038            child.recurse_reapply_runtime_settings_for_component_owner(self.id, context);
3039            child.recurse_emit_mount_updates();
3040        }
3041    }
3042
3043    pub fn recurse_render_queue(
3044        self: &Rc<Self>,
3045        ctx: &Rc<RuntimeContext>,
3046        rcs: &mut dyn RenderContext,
3047    ) {
3048        if self.is_render_source() {
3049            return;
3050        }
3051        let cp = self.get_common_properties();
3052        let cp = borrow!(cp);
3053        if cp.unclippable.get().unwrap_or(false) {
3054            ctx.queue_render(Rc::clone(&self));
3055        } else {
3056            self.recurse_render(ctx, rcs);
3057        }
3058    }
3059
3060    pub(crate) fn render_parent_node(&self) -> Option<Rc<ExpandedNode>> {
3061        borrow!(self.render_parent).upgrade()
3062    }
3063
3064    pub(crate) fn is_unclippable(&self) -> bool {
3065        let cp = self.get_common_properties();
3066        let cp = borrow!(cp);
3067        cp.unclippable.get().unwrap_or(false)
3068    }
3069
3070    pub(crate) fn recurse_render_filtered(
3071        self: &Rc<Self>,
3072        ctx: &Rc<RuntimeContext>,
3073        rcs: &mut dyn RenderContext,
3074        render_path_nodes: &HashSet<ExpandedNodeIdentifier>,
3075        dirty_nodes: &HashSet<ExpandedNodeIdentifier>,
3076        stats: &mut FilteredRenderStats,
3077    ) {
3078        if self.is_render_source() {
3079            return;
3080        }
3081        stats.path_nodes_visited += 1;
3082        #[cfg(feature = "shaders")]
3083        if self.render_shader_effect(ctx, rcs) {
3084            return;
3085        }
3086        borrow!(self.instance_node).handle_pre_render(&self, ctx, rcs);
3087        for child in self.children.get().iter().rev() {
3088            if render_path_nodes.contains(&child.id) {
3089                child.recurse_render_filtered(ctx, rcs, render_path_nodes, dirty_nodes, stats);
3090            } else {
3091                stats.skipped_subtrees += 1;
3092            }
3093        }
3094        if dirty_nodes.contains(&self.id) {
3095            borrow!(self.instance_node).render(&self, ctx, rcs);
3096            stats.dirty_nodes_rendered += 1;
3097        }
3098        borrow!(self.instance_node).handle_post_render(&self, ctx, rcs);
3099    }
3100
3101    pub fn recurse_render(self: &Rc<Self>, ctx: &Rc<RuntimeContext>, rcs: &mut dyn RenderContext) {
3102        if self.is_render_source() {
3103            return;
3104        }
3105        #[cfg(feature = "shaders")]
3106        if self.render_shader_effect(ctx, rcs) {
3107            return;
3108        }
3109        borrow!(self.instance_node).handle_pre_render(&self, ctx, rcs);
3110        for child in self.children.get().iter().rev() {
3111            child.recurse_render_queue(ctx, rcs);
3112        }
3113        borrow!(self.instance_node).render(&self, ctx, rcs);
3114        borrow!(self.instance_node).handle_post_render(&self, ctx, rcs);
3115    }
3116
3117    /// Records a detached source using ordinary vector hooks without presenting it or allocating
3118    /// native surfaces. Structural components retain their normal back-to-front child order.
3119    pub fn recurse_render_alpha_source(
3120        self: &Rc<Self>,
3121        ctx: &Rc<RuntimeContext>,
3122        rcs: &mut dyn RenderContext,
3123    ) {
3124        assert!(self.is_render_source() && rcs.alpha_source_layer().is_some());
3125        let mut deferred = Vec::new();
3126        self.render_alpha_source_inner(ctx, rcs, &mut deferred);
3127        let mut next = 0;
3128        while next < deferred.len() {
3129            let node = Rc::clone(&deferred[next]);
3130            next += 1;
3131            node.render_alpha_source_inner(ctx, rcs, &mut deferred);
3132        }
3133    }
3134
3135    fn render_alpha_source_inner(
3136        self: &Rc<Self>,
3137        ctx: &Rc<RuntimeContext>,
3138        rcs: &mut dyn RenderContext,
3139        deferred: &mut Vec<Rc<ExpandedNode>>,
3140    ) {
3141        #[cfg(feature = "shaders")]
3142        assert!(
3143            borrow!(self.instance_node).shader_effect(self).is_none(),
3144            "shader effects inside detached mask sources are unsupported"
3145        );
3146        let supported = borrow!(self.instance_node).supports_alpha_source_render();
3147        let structural = {
3148            let instance = borrow!(self.instance_node);
3149            let flags = instance.base().flags();
3150            flags.layer == Layer::DontCare || flags.is_component
3151        };
3152        if !supported && !structural {
3153            return;
3154        }
3155        if supported {
3156            borrow!(self.instance_node).handle_pre_render(self, ctx, rcs);
3157        }
3158        for child in self.children.get().iter().rev() {
3159            if child.is_unclippable() {
3160                deferred.push(Rc::clone(child));
3161            } else {
3162                child.render_alpha_source_inner(ctx, rcs, deferred);
3163            }
3164        }
3165        if supported {
3166            borrow!(self.instance_node).render(self, ctx, rcs);
3167            borrow!(self.instance_node).handle_post_render(self, ctx, rcs);
3168        }
3169    }
3170
3171    /// Manages unpacking an `Rc<RefCell<PaxValue>>`, downcasting into
3172    /// the parameterized `target_type`, and executing a provided closure `body` in the
3173    /// context of that unwrapped variant (including support for mutable operations),
3174    /// the closure is executed.  Used at least by calculating properties in `expand_node` and
3175    /// passing `&mut self` into event handlers (where the typed `self` is retrieved from an instance of `PaxValue`)
3176    pub fn with_properties_unwrapped<T: ToFromPaxAny, R>(
3177        &self,
3178        callback: impl FnOnce(&mut T) -> R,
3179    ) -> R {
3180        self.try_with_properties_unwrapped(callback)
3181            .expect("properties not of expected type")
3182    }
3183
3184    pub fn try_with_properties_unwrapped<T: ToFromPaxAny, R>(
3185        &self,
3186        callback: impl FnOnce(&mut T) -> R,
3187    ) -> Option<R> {
3188        // Borrow the contents of the RefCell mutably.
3189        let properties = borrow_mut!(self.properties);
3190        let mut borrowed = borrow_mut!(properties);
3191        // Downcast the unwrapped value to the specified `target_type` (or panic)
3192        let Ok(mut val) = T::mut_from_pax_any(&mut *borrowed) else {
3193            return None;
3194        };
3195        self.property_binding_scope
3196            .borrow()
3197            .with_graph(|| Some(callback(&mut val)))
3198    }
3199
3200    pub fn recurse_visit_postorder(self: &Rc<Self>, func: &mut impl FnMut(&Rc<Self>)) {
3201        // NOTE: This is to make sure the projected children list is updated before trying to access children,
3202        // to make stacker/scroller behave correctly when the number of children is dynamic.
3203        self.compute_flattened_projected_children();
3204        for child in self.children.get().iter().rev() {
3205            child.recurse_visit_postorder(func)
3206        }
3207        func(self);
3208    }
3209
3210    pub fn get_node_context(self: &Rc<Self>, ctx: &Rc<RuntimeContext>) -> NodeContext {
3211        let globals = ctx.globals();
3212        let viewport = super::viewport_info_property(&globals.viewport);
3213        let target = LocalProperty::new(globals.target);
3214        let t_and_b = self.transform_and_bounds.clone();
3215        let deps = [t_and_b.untyped()];
3216        let bounds_name = format!("node context bounds (node id: {})", self.id.0);
3217        let bounds_self = LocalProperty::computed_with_cutoff_and_name(
3218            move || t_and_b.get().bounds,
3219            &deps,
3220            <(f64, f64)>::eq,
3221            &bounds_name,
3222        );
3223        let render_parent = borrow!(self.render_parent).upgrade();
3224        let t_and_b_parent = render_parent
3225            .as_ref()
3226            .map(|parent| parent.transform_and_bounds.clone())
3227            .unwrap_or_else(|| globals.viewport.clone());
3228        let (parent_padding_x, parent_padding_y) = render_parent
3229            .as_ref()
3230            .map(|parent| {
3231                let common_props = parent.get_common_properties();
3232                let common_props = borrow!(common_props);
3233                (
3234                    common_props.padding_x.clone(),
3235                    common_props.padding_y.clone(),
3236                )
3237            })
3238            .unwrap_or_default();
3239        let deps = [
3240            t_and_b_parent.untyped(),
3241            parent_padding_x.untyped(),
3242            parent_padding_y.untyped(),
3243        ];
3244        let bounds_parent = LocalProperty::computed(
3245            move || {
3246                apply_padding_frame(
3247                    t_and_b_parent.get(),
3248                    parent_padding_x.get(),
3249                    parent_padding_y.get(),
3250                )
3251                .bounds
3252            },
3253            &deps,
3254        );
3255
3256        let owns_projected_children = borrow!(self.instance_node).base().flags().is_component
3257            || borrow!(self.expanded_projected_children).is_some();
3258
3259        let projected_children_count = if owns_projected_children {
3260            self.flattened_projected_children_count.clone()
3261        } else {
3262            self.containing_component
3263                .upgrade()
3264                .map(|v| v.flattened_projected_children_count.clone())
3265                .unwrap_or_default()
3266        };
3267
3268        let projected_children = if owns_projected_children {
3269            self.expanded_and_flattened_projected_children.clone()
3270        } else {
3271            self.containing_component
3272                .upgrade()
3273                .map(|v| v.expanded_and_flattened_projected_children.clone())
3274                .unwrap_or_default()
3275        };
3276
3277        let projected_children_changed = if owns_projected_children {
3278            self.projected_children_changed.clone()
3279        } else {
3280            self.containing_component
3281                .upgrade()
3282                .map(|v| v.projected_children_changed.clone())
3283                .unwrap_or_default()
3284        };
3285        // Normalize semantic payload for container consumers. This hides the
3286        // engine transport distinction (`Owned` vs `Projected`) behind the
3287        // single concept "received children".
3288        let received_children = match borrow!(self.instance_node).received_children_source() {
3289            ReceivedChildrenSource::Owned => self.active_children_view.clone(),
3290            ReceivedChildrenSource::Projected => {
3291                if owns_projected_children {
3292                    self.expanded_and_flattened_projected_children.clone()
3293                } else {
3294                    self.containing_component
3295                        .upgrade()
3296                        .map(|v| v.expanded_and_flattened_projected_children.clone())
3297                        .unwrap_or_default()
3298                }
3299            }
3300        };
3301        let received_children_count_source = received_children.clone();
3302        let received_children_count = LocalProperty::computed(
3303            move || received_children_count_source.get().len(),
3304            &[received_children.untyped()],
3305        );
3306        let received_children_signal = received_children.clone();
3307        let received_children_changed = LocalProperty::computed(
3308            move || {
3309                let _ = received_children_signal.get();
3310            },
3311            &[received_children.untyped()],
3312        );
3313        // Normalize the corresponding exit-retained payload set. These nodes
3314        // are no longer active content, but containers may still need them for
3315        // ghosting or overlay placement while `@out` transitions finish.
3316        let retained_received_children =
3317            match borrow!(self.instance_node).received_children_source() {
3318                ReceivedChildrenSource::Owned => self.exiting_children_view.clone(),
3319                ReceivedChildrenSource::Projected
3320                    if borrow!(self.expanded_projected_children).is_some() =>
3321                {
3322                    self.exiting_children_view.clone()
3323                }
3324                ReceivedChildrenSource::Projected => LocalProperty::default(),
3325            };
3326        let retained_received_children_signal = retained_received_children.clone();
3327        let retained_received_children_changed = LocalProperty::computed(
3328            move || {
3329                let _ = retained_received_children_signal.get();
3330            },
3331            &[retained_received_children.untyped()],
3332        );
3333
3334        let last_frame = Rc::new(RefCell::new(globals.elapsed_frames.get()));
3335        let suspended = self.suspended.clone();
3336        let elapsed_frames = globals.elapsed_frames.clone();
3337        let elapsed_millis = globals.elapsed_millis.clone();
3338        let deps = [elapsed_frames.untyped(), suspended.untyped()];
3339        // TODO: this still triggers the dirty dag dependencies of elapsed
3340        // frames even if the value is the same. Try to make it not trigger
3341        // dependencides when frozen
3342        let elapsed_frames_frozen_if_suspended = LocalProperty::computed(
3343            move || {
3344                if suspended.get() {
3345                    *borrow!(last_frame)
3346                } else {
3347                    let val = elapsed_frames.get();
3348                    *borrow_mut!(last_frame) = val;
3349                    val
3350                }
3351            },
3352            &deps,
3353        );
3354        // PREF: possibly change this to just take a reference to expanded node, and
3355        // expose methods to retrieve these values from the expanded noe when
3356        // needed, instead of re-creating/copying
3357        NodeContext {
3358            slot_index: self.slot_index.clone(),
3359            expression_stack: Rc::clone(&self.stack),
3360            store_generation: self.stores.generation(),
3361            expanded_node: Rc::downgrade(&self),
3362            containing_component: Weak::clone(&self.containing_component),
3363            elapsed_frames: elapsed_frames_frozen_if_suspended,
3364            elapsed_millis,
3365            gyro: globals.gyro.clone(),
3366            accel: globals.accel.clone(),
3367            bounds_self,
3368            bounds_parent,
3369            measured_size: self.measured_size.clone(),
3370            subtree_layout_hull: self.subtree_layout_hull.clone(),
3371            runtime_context: ctx.clone(),
3372            platform: globals.platform.clone(),
3373            os: globals.os.clone(),
3374            target,
3375            viewport,
3376            get_elapsed_millis: globals.get_elapsed_millis,
3377            projected_children_count,
3378            projected_children,
3379            node_transform_and_bounds: self.transform_and_bounds.get(),
3380            projected_children_changed,
3381            received_children,
3382            received_children_count,
3383            received_children_changed,
3384            retained_received_children,
3385            retained_received_children_changed,
3386            #[cfg(feature = "designtime")]
3387            designtime: globals.designtime.clone(),
3388        }
3389    }
3390
3391    pub fn get_common_properties(&self) -> Rc<RefCell<CommonProperties>> {
3392        Rc::clone(&*borrow!(self.common_properties))
3393    }
3394
3395    pub fn is_layout_breakout(&self) -> bool {
3396        let common_props = self.get_common_properties();
3397        let is_breakout = borrow!(common_props).layout_role.get() == Some(LayoutRole::Breakout);
3398        is_breakout
3399    }
3400
3401    /// Determines whether the provided ray, orthogonal to the view plane,
3402    /// intersects this `ExpandedNode`.
3403    pub fn ray_cast_test(&self, ray: Point2<Window>) -> bool {
3404        if self.is_render_source() {
3405            return false;
3406        }
3407        let cp = borrow!(self.common_properties);
3408
3409        // skip raycast if false
3410        if !borrow!(&*cp)._raycastable.get().unwrap_or(true) {
3411            return false;
3412        }
3413        drop(cp);
3414        borrow!(self.instance_node).ray_cast_test(self, ray)
3415    }
3416
3417    pub fn compute_flattened_projected_children(&self) {
3418        // All of this should ideally be reactively updated,
3419        // but currently doesn't exist a way to "listen to"
3420        // an entire node tree, and generate the flattened list
3421        // only when changed.
3422        if let Some(projected_children) = borrow!(self.expanded_projected_children).as_ref() {
3423            let new_flattened = flatten_expanded_nodes_for_projection(projected_children);
3424            let old_and_new_filtered_same =
3425                self.expanded_and_flattened_projected_children
3426                    .read(|flattened| {
3427                        flattened
3428                            .iter()
3429                            .map(|n| n.id)
3430                            .eq(new_flattened.iter().map(|n| n.id))
3431                    });
3432
3433            if !old_and_new_filtered_same {
3434                self.flattened_projected_children_count
3435                    .set(new_flattened.len());
3436                self.expanded_and_flattened_projected_children
3437                    .set(new_flattened);
3438                for (i, projected_child) in self
3439                    .expanded_and_flattened_projected_children
3440                    .get()
3441                    .iter()
3442                    .enumerate()
3443                {
3444                    if projected_child.slot_index.get() != Some(i) {
3445                        projected_child.slot_index.set(Some(i));
3446                    };
3447                }
3448            }
3449        }
3450    }
3451
3452    dispatch_event_handler!(dispatch_scroll, Scroll, SCROLL_HANDLERS, true);
3453    dispatch_event_handler!(dispatch_touch_start, TouchStart, TOUCH_START_HANDLERS, true);
3454
3455    dispatch_event_handler!(dispatch_touch_move, TouchMove, TOUCH_MOVE_HANDLERS, true);
3456    dispatch_event_handler!(dispatch_touch_end, TouchEnd, TOUCH_END_HANDLERS, true);
3457    dispatch_event_handler!(
3458        dispatch_touch_cancel,
3459        TouchCancel,
3460        TOUCH_CANCEL_HANDLERS,
3461        true
3462    );
3463    dispatch_event_handler!(dispatch_key_down, KeyDown, KEY_DOWN_HANDLERS, false);
3464    dispatch_event_handler!(dispatch_key_up, KeyUp, KEY_UP_HANDLERS, false);
3465    dispatch_event_handler!(dispatch_key_press, KeyPress, KEY_PRESS_HANDLERS, false);
3466    dispatch_event_handler!(
3467        dispatch_checkbox_change,
3468        CheckboxChange,
3469        CHECKBOX_CHANGE_HANDLERS,
3470        true
3471    );
3472    dispatch_event_handler!(
3473        dispatch_slider_change,
3474        SliderChange,
3475        SLIDER_CHANGE_HANDLERS,
3476        true
3477    );
3478    dispatch_event_handler!(
3479        dispatch_textbox_change,
3480        TextboxChange,
3481        TEXTBOX_CHANGE_HANDLERS,
3482        true
3483    );
3484    dispatch_event_handler!(dispatch_text_input, TextInput, TEXT_INPUT_HANDLERS, true);
3485    dispatch_event_handler!(
3486        dispatch_textbox_input,
3487        TextboxInput,
3488        TEXTBOX_INPUT_HANDLERS,
3489        true
3490    );
3491    dispatch_event_handler!(
3492        dispatch_button_click,
3493        ButtonClick,
3494        BUTTON_CLICK_HANDLERS,
3495        true
3496    );
3497    dispatch_event_handler!(
3498        dispatch_photo_picker_change,
3499        PhotoPickerChange,
3500        PHOTO_PICKER_CHANGE_HANDLERS,
3501        true
3502    );
3503    dispatch_event_handler!(dispatch_mouse_down, MouseDown, MOUSE_DOWN_HANDLERS, true);
3504    dispatch_event_handler!(dispatch_mouse_up, MouseUp, MOUSE_UP_HANDLERS, true);
3505    dispatch_event_handler!(dispatch_mouse_move, MouseMove, MOUSE_MOVE_HANDLERS, true);
3506    dispatch_event_handler!(dispatch_mouse_over, MouseOver, MOUSE_OVER_HANDLERS, false);
3507    dispatch_event_handler!(dispatch_mouse_out, MouseOut, MOUSE_OUT_HANDLERS, false);
3508    dispatch_event_handler!(
3509        dispatch_double_click,
3510        DoubleClick,
3511        DOUBLE_CLICK_HANDLERS,
3512        true
3513    );
3514    dispatch_event_handler!(
3515        dispatch_context_menu,
3516        ContextMenu,
3517        CONTEXT_MENU_HANDLERS,
3518        true
3519    );
3520    fn dispatch_pointer_activation(
3521        self: &Rc<Self>,
3522        event: Event<Click>,
3523        source: PointerActivationSource,
3524        ctx: &Rc<RuntimeContext>,
3525    ) -> bool {
3526        let has_click = self.has_event_handlers(CLICK_HANDLERS);
3527        let has_tap = self.has_event_handlers(TAP_HANDLERS);
3528        let handler_key = match (has_click, has_tap, source) {
3529            (true, true, PointerActivationSource::Mouse) => Some(CLICK_HANDLERS),
3530            (true, true, PointerActivationSource::Touch) => Some(TAP_HANDLERS),
3531            (true, false, _) => Some(CLICK_HANDLERS),
3532            (false, true, _) => Some(TAP_HANDLERS),
3533            (false, false, _) => None,
3534        };
3535
3536        if let Some(handler_key) = handler_key {
3537            self.run_event_handlers_for_key(handler_key, &event, ctx);
3538        }
3539
3540        if let Some(parent) = self.template_parent.upgrade() {
3541            return parent.dispatch_pointer_activation(event, source, ctx);
3542        }
3543        event.cancelled()
3544    }
3545
3546    pub fn dispatch_click(
3547        self: &Rc<Self>,
3548        event: Event<Click>,
3549        _globals: &Globals,
3550        ctx: &Rc<RuntimeContext>,
3551    ) -> bool {
3552        self.dispatch_pointer_activation(event, PointerActivationSource::Mouse, ctx)
3553    }
3554
3555    pub fn dispatch_tap(
3556        self: &Rc<Self>,
3557        event: Event<Click>,
3558        _globals: &Globals,
3559        ctx: &Rc<RuntimeContext>,
3560    ) -> bool {
3561        self.dispatch_pointer_activation(event, PointerActivationSource::Touch, ctx)
3562    }
3563
3564    dispatch_event_handler!(dispatch_wheel, Wheel, WHEEL_HANDLERS, true);
3565    dispatch_event_handler!(dispatch_drop, Drop, DROP_HANDLERS, true);
3566    dispatch_event_handler!(dispatch_gyro, Gyro, GYRO_HANDLERS, false);
3567    dispatch_event_handler!(dispatch_accel, Accel, ACCEL_HANDLERS, false);
3568    dispatch_event_handler!(dispatch_focus, Focus, FOCUSED_HANDLERS, false);
3569    dispatch_event_handler!(
3570        dispatch_select_start,
3571        SelectStart,
3572        SELECT_START_HANDLERS,
3573        false
3574    );
3575
3576    pub fn dispatch_custom_event(
3577        self: &Rc<Self>,
3578        identifier: &str,
3579        ctx: &Rc<RuntimeContext>,
3580    ) -> Result<(), String> {
3581        if self.attached.get() == 0 {
3582            return Ok(());
3583        }
3584        let _graph = ctx.property_graph.enter();
3585        let component_origin_instance = borrow!(self.instance_node);
3586        let registry = component_origin_instance
3587            .base()
3588            .handler_registry
3589            .as_ref()
3590            .ok_or_else(|| "no registry present".to_owned())?;
3591
3592        let parent_component = self
3593            .containing_component
3594            .upgrade()
3595            .ok_or_else(|| "can't dispatch from root (has no parent)".to_owned())?;
3596        if parent_component.attached.get() == 0 {
3597            return Ok(());
3598        }
3599        let _scope = parent_component.property_binding_scope.borrow().enter();
3600        let properties = borrow!(parent_component.properties);
3601
3602        for handler in borrow!(registry)
3603            .handlers
3604            .get(identifier)
3605            .expect("presence should have been checked when added to custom_event_queue")
3606        {
3607            (handler.function)(Rc::clone(&*properties), &self.get_node_context(ctx), None)
3608        }
3609        Ok(())
3610    }
3611
3612    // Sets measured bounds when the chassis or container layout resolves them empirically.
3613    pub fn set_measured_size(self: &Rc<ExpandedNode>, width: f64, height: f64) {
3614        self.measured_size.set(Some((width, height)));
3615    }
3616
3617    // Backward-compatible alias used by chassis interrupt plumbing.
3618    pub fn chassis_resize_request(self: &Rc<ExpandedNode>, width: f64, height: f64) {
3619        self.set_measured_size(width, height);
3620    }
3621
3622    /// Helper method that returns a collection of common properties
3623    /// related to layout (position, size, scale, anchor, etc),
3624    pub fn layout_properties(self: &Rc<ExpandedNode>) -> LocalProperty<LayoutProperties> {
3625        let common_props = self.get_common_properties();
3626        let common_props = borrow!(common_props);
3627        let cp_width = common_props.width.clone();
3628        let cp_height = common_props.height.clone();
3629        let cp_transform = common_props.transform.clone();
3630        let cp_anchor_x = common_props.anchor_x.clone();
3631        let cp_anchor_y = common_props.anchor_y.clone();
3632        let cp_scale_x = common_props.scale_x.clone();
3633        let cp_scale_y = common_props.scale_y.clone();
3634        let cp_skew_x = common_props.skew_x.clone();
3635        let cp_skew_y = common_props.skew_y.clone();
3636        let cp_rotate = common_props.rotate.clone();
3637        let cp_x = common_props.x.clone();
3638        let cp_y = common_props.y.clone();
3639        let measured_size = self.measured_size.clone();
3640        let deps = [
3641            cp_width.untyped(),
3642            cp_height.untyped(),
3643            cp_transform.untyped(),
3644            cp_anchor_x.untyped(),
3645            cp_anchor_y.untyped(),
3646            cp_scale_x.untyped(),
3647            cp_scale_y.untyped(),
3648            cp_skew_x.untyped(),
3649            cp_skew_y.untyped(),
3650            cp_rotate.untyped(),
3651            cp_x.untyped(),
3652            cp_y.untyped(),
3653            measured_size.untyped(),
3654        ];
3655
3656        LocalProperty::computed(
3657            move || {
3658                // Used for auto sized text, might be used for other things later
3659                let fallback = measured_size.get();
3660                let (w_fallback, h_fallback) = match fallback {
3661                    Some((wf, hf)) => (Some(wf), Some(hf)),
3662                    None => (None, None),
3663                };
3664
3665                LayoutProperties {
3666                    x: cp_x.get(),
3667                    y: cp_y.get(),
3668                    width: cp_width
3669                        .get()
3670                        .or(w_fallback.map(|v| Size::Pixels(v.into()))),
3671                    height: cp_height
3672                        .get()
3673                        .or(h_fallback.map(|v| Size::Pixels(v.into()))),
3674                    rotate: cp_rotate.get(),
3675                    // TODO make the common prop only accept percent
3676                    scale_x: cp_scale_x
3677                        .get()
3678                        .map(|v| Percent((100.0 * v.expect_percent()).into())),
3679                    scale_y: cp_scale_y
3680                        .get()
3681                        .map(|v| Percent((100.0 * v.expect_percent()).into())),
3682                    anchor_x: cp_anchor_x.get(),
3683                    anchor_y: cp_anchor_y.get(),
3684                    skew_x: cp_skew_x.get(),
3685                    skew_y: cp_skew_y.get(),
3686                }
3687            },
3688            &deps,
3689        )
3690    }
3691}
3692
3693fn layout_axis_can_contribute(layout: &LayoutProperties, axis: Axis) -> bool {
3694    let size = match axis {
3695        Axis::X => layout.width,
3696        Axis::Y => layout.height,
3697    };
3698    let position = match axis {
3699        Axis::X => layout.x,
3700        Axis::Y => layout.y,
3701    };
3702
3703    size.is_some_and(|size| !size_depends_on_parent(size))
3704        && !position.is_some_and(size_depends_on_parent)
3705}
3706
3707fn layout_axis_can_contribute_from_parts(size: Option<Size>, position: Option<Size>) -> bool {
3708    size.is_some_and(|size| !size_depends_on_parent(size))
3709        && !position.is_some_and(size_depends_on_parent)
3710}
3711
3712fn size_depends_on_parent(size: Size) -> bool {
3713    match size {
3714        Size::Pixels(_) => false,
3715        Size::Percent(percent) => percent.to_float().abs() > f64::EPSILON,
3716        Size::Combined(_, percent) => percent.to_float().abs() > f64::EPSILON,
3717    }
3718}
3719
3720/// Given a projected child list, flatten away "slot-invisible" nodes (namely, `if` and `for`)
3721/// and return a top-level list of renderable projected children.
3722fn flatten_expanded_nodes_for_projection(nodes: &[Rc<ExpandedNode>]) -> Vec<Rc<ExpandedNode>> {
3723    let mut result: Vec<Rc<ExpandedNode>> = vec![];
3724    for node in nodes {
3725        if borrow!(node.instance_node).base().flags().invisible_to_slot {
3726            result.extend(flatten_expanded_nodes_for_projection(
3727                node.children
3728                    .get()
3729                    .clone()
3730                    .into_iter()
3731                    .collect::<Vec<_>>()
3732                    .as_slice(),
3733            ));
3734        } else {
3735            result.push(Rc::clone(&node))
3736        }
3737    }
3738    result
3739}
3740
3741impl std::fmt::Debug for ExpandedNode {
3742    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3743        //see: https://users.rust-lang.org/t/reusing-an-fmt-formatter/8531/4
3744        //maybe this utility should be moved to a more accessible place?
3745        pub struct Fmt<F>(pub F)
3746        where
3747            F: Fn(&mut fmt::Formatter) -> fmt::Result;
3748
3749        impl<F> fmt::Debug for Fmt<F>
3750        where
3751            F: Fn(&mut fmt::Formatter) -> fmt::Result,
3752        {
3753            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3754                (self.0)(f)
3755            }
3756        }
3757
3758        f.debug_struct("ExpandedNode")
3759            .field(
3760                "instance_node",
3761                &Fmt(|f| borrow!(self.instance_node).resolve_debug(f, Some(self))),
3762            )
3763            .field("id", &self.id)
3764            .field("common_properties", &borrow!(self.common_properties))
3765            .field("transform_and_bounds", &self.transform_and_bounds)
3766            .field("children", &self.children.get().iter().collect::<Vec<_>>())
3767            .field(
3768                "projected_children",
3769                &self
3770                    .expanded_and_flattened_projected_children
3771                    .get()
3772                    .iter()
3773                    .map(|v| v.id)
3774                    .collect::<Vec<_>>(),
3775            )
3776            .field("render_placement", &self.occlusion.get())
3777            .field(
3778                "containing_component",
3779                &self.containing_component.upgrade().map(|v| v.id.clone()),
3780            )
3781            .finish()
3782    }
3783}