Skip to main content

pax_runtime/
properties.rs

1use crate::api::math::Point2;
2use crate::api::Window;
3use crate::constants::{ACCEL_HANDLERS, GYRO_HANDLERS, PRE_RENDER_HANDLERS, TICK_HANDLERS};
4use pax_language::interpreter::property_resolution::IdentifierResolver;
5use pax_manifest::cartridge_generation::{TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT};
6use pax_manifest::UniqueTemplateNodeIdentifier;
7use pax_message::{NativeMessage, SafeAreaInsets, ScreenshotData};
8use pax_runtime_api::properties::{
9    drain_effects, drain_effects_with_report, property_has_direct_outbound,
10    property_outbound_debug_names, register_effect_property, register_effect_property_with_name,
11    UntypedProperty,
12};
13use pax_runtime_api::{
14    borrow, borrow_mut, use_RefCell, Event, Interpolatable, LightShape, LocalProperty, MouseOut,
15    MouseOver, RenderContext, SceneLight, SceneLighting, Variable,
16};
17use_RefCell!();
18use kurbo::{Affine, Point};
19use std::cell::Cell;
20use std::collections::{HashMap, HashSet};
21use std::hash::Hash;
22use std::rc::{Rc, Weak};
23use std::sync::OnceLock;
24use std::time::Instant;
25
26use crate::{ExpandedNode, Globals};
27
28#[cfg(feature = "designtime")]
29use crate::{ComponentInstance, InstanceNode};
30
31fn effect_drain_instrumentation_enabled() -> bool {
32    static ENABLED: OnceLock<bool> = OnceLock::new();
33    *ENABLED.get_or_init(|| {
34        std::env::var("PAX_EFFECT_DRAIN_INSTRUMENTATION")
35            .map(|value| {
36                let value = value.to_ascii_lowercase();
37                matches!(value.as_str(), "1" | "true" | "yes" | "on")
38            })
39            .unwrap_or(false)
40    })
41}
42
43fn format_effect_top(top_effects: &[(String, usize)]) -> String {
44    top_effects
45        .iter()
46        .map(|(name, count)| format!("{count}x {name}"))
47        .collect::<Vec<_>>()
48        .join("; ")
49}
50
51fn summarize_debug_names(names: Vec<String>) -> Vec<(String, usize)> {
52    let mut counts = HashMap::new();
53    for name in names {
54        let category = name
55            .split_once(" (")
56            .map(|(category, _)| category)
57            .unwrap_or(&name)
58            .to_owned();
59        *counts.entry(category).or_insert(0) += 1;
60    }
61    let mut counts = counts.into_iter().collect::<Vec<_>>();
62    counts.sort_by(|(left_name, left_count), (right_name, right_count)| {
63        right_count
64            .cmp(left_count)
65            .then_with(|| left_name.cmp(right_name))
66    });
67    counts
68}
69
70impl Interpolatable for ExpandedNodeIdentifier {}
71
72fn transform_scene_light_for_layer(
73    mut light: SceneLight,
74    source_to_root: Affine,
75    root_to_target: Affine,
76) -> SceneLight {
77    if matches!(light.shape, LightShape::Point) {
78        let point = Point::new(light.position.x, light.position.y);
79        let point = root_to_target * (source_to_root * point);
80        light.position.x = point.x;
81        light.position.y = point.y;
82    }
83    light
84}
85
86#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
87/// Stable runtime identifier assigned to an expanded node.
88pub struct ExpandedNodeIdentifier(pub u32);
89
90impl ExpandedNodeIdentifier {
91    /// Convert to the integer id passed across chassis message boundaries.
92    pub fn to_u32(&self) -> u32 {
93        self.0
94    }
95}
96
97/// Shared context for properties pass recursion
98pub struct RuntimeContext {
99    pub application: pax_runtime_api::application::ApplicationInstance,
100    /// Owner-thread bridge for application properties.
101    pub property_graph: pax_runtime_api::PropertyGraph,
102    next_uid: Cell<ExpandedNodeIdentifier>,
103    messages: RefCell<Vec<NativeMessage>>,
104    globals: RefCell<Globals>,
105    root_expanded_node: RefCell<Weak<ExpandedNode>>,
106    #[cfg(feature = "designtime")]
107    pub userland_frame_instance_node: RefCell<Option<Rc<dyn InstanceNode>>>,
108    #[cfg(feature = "designtime")]
109    pub userland_root_expanded_node: RefCell<Option<Rc<ExpandedNode>>>,
110    node_cache: RefCell<NodeCache>,
111    last_topmost_element: RefCell<Weak<ExpandedNode>>,
112    active_touch_targets: RefCell<HashMap<i64, ExpandedNodeIdentifier>>,
113    queued_custom_events: RefCell<Vec<(Rc<ExpandedNode>, &'static str)>>,
114    queued_renders: RefCell<Vec<Rc<ExpandedNode>>>,
115    pub layer_count: Cell<usize>,
116    pub dirty_canvases: Rc<RefCell<Vec<bool>>>,
117    dirty_canvas_nodes: RefCell<HashSet<ExpandedNodeIdentifier>>,
118    scene_geometry: RefCell<crate::scene_geometry::SceneGeometry>,
119    pub(crate) viewport_proximity: RefCell<crate::viewport_proximity::ViewportProximityState>,
120    canvas_node_light_masks: RefCell<HashMap<ExpandedNodeIdentifier, u32>>,
121    scene_lighting_nodes: RefCell<HashSet<ExpandedNodeIdentifier>>,
122    scene_lighting_dirty: Cell<bool>,
123    // Slot order and lexical owners determine masks; light color/position do not.
124    layer_light_scopes:
125        RefCell<HashMap<usize, Vec<(ExpandedNodeIdentifier, Option<ExpandedNodeIdentifier>)>>>,
126    #[cfg(test)]
127    lighting_mask_node_visits: Cell<usize>,
128    lighting_overflow_counts: RefCell<HashMap<usize, usize>>,
129    // Nodes dirty solely because a physical tile needs replay. An independent
130    // invalidation removes eligibility even if the node also intersects that tile.
131    targeted_canvas_replay_node_ids: RefCell<HashMap<usize, HashSet<u32>>>,
132    removed_canvas_nodes: RefCell<Vec<(usize, u32)>>,
133    occlusion_dirty: Cell<bool>,
134    pub(crate) occlusion_state: RefCell<crate::engine::occlusion::OcclusionState>,
135    layer_canvas_plan_generation: Cell<u32>,
136    canvas_drawable_layers: RefCell<HashSet<usize>>,
137    import_settings_node_count: Cell<usize>,
138    tick_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
139    pre_render_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
140    gyro_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
141    accel_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
142    screenshot_map: Rc<RefCell<HashMap<u32, ScreenshotData>>>,
143    scroller_surface_states: RefCell<HashMap<u32, ScrollerSurfaceState>>,
144    layer_scroller_owners: RefCell<HashMap<usize, ExpandedNodeIdentifier>>,
145    root_scroller_id: Cell<Option<u32>>,
146    visual_viewport_state: Cell<Option<VisualViewportState>>,
147    safe_area_insets: LocalProperty<SafeAreaInsets>,
148}
149
150#[derive(Clone, Copy, Debug, Default)]
151/// Last-known scroll state for a native or browser-owned scroller surface.
152pub struct ScrollerSurfaceState {
153    pub viewport_width: f64,
154    pub viewport_height: f64,
155    pub content_width: f64,
156    pub content_height: f64,
157    pub scroll_x: f64,
158    pub scroll_y: f64,
159    pub presentation_scroll_x: f64,
160    pub presentation_scroll_y: f64,
161    pub clip_content: bool,
162}
163
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
165/// Coarse classification of changes to a native or browser-owned scroller surface.
166pub enum ScrollerSurfaceStateChange {
167    Unchanged,
168    ScrollOnly,
169    Structural,
170}
171
172impl ScrollerSurfaceState {
173    fn close_enough(lhs: f64, rhs: f64) -> bool {
174        (lhs - rhs).abs() <= 1e-4
175    }
176
177    fn structural_eq(&self, other: &Self) -> bool {
178        Self::close_enough(self.viewport_width, other.viewport_width)
179            && Self::close_enough(self.viewport_height, other.viewport_height)
180            && Self::close_enough(self.content_width, other.content_width)
181            && Self::close_enough(self.content_height, other.content_height)
182            && self.clip_content == other.clip_content
183    }
184
185    fn scroll_eq(&self, other: &Self) -> bool {
186        Self::close_enough(self.scroll_x, other.scroll_x)
187            && Self::close_enough(self.scroll_y, other.scroll_y)
188            && Self::close_enough(self.presentation_scroll_x, other.presentation_scroll_x)
189            && Self::close_enough(self.presentation_scroll_y, other.presentation_scroll_y)
190    }
191}
192
193#[derive(Clone, Copy, Debug, Default)]
194/// Browser visual viewport state used when page scrolling participates in root scroller behavior.
195pub struct VisualViewportState {
196    pub width: f64,
197    pub height: f64,
198    pub offset_x: f64,
199    pub offset_y: f64,
200    pub page_scroll_x: f64,
201    pub page_scroll_y: f64,
202}
203
204struct NodeCache {
205    eid_to_node: HashMap<ExpandedNodeIdentifier, Rc<ExpandedNode>>,
206    uni_to_eid: HashMap<UniqueTemplateNodeIdentifier, Vec<ExpandedNodeIdentifier>>,
207}
208
209impl NodeCache {
210    fn new() -> Self {
211        Self {
212            eid_to_node: Default::default(),
213            uni_to_eid: Default::default(),
214        }
215    }
216
217    // Add this node to all relevant constant lookup cache structures
218    fn add_to_cache(&mut self, node: &Rc<ExpandedNode>) {
219        self.eid_to_node.insert(node.id, Rc::clone(&node));
220        let uni = borrow!(node.instance_node)
221            .base()
222            .template_node_identifier
223            .clone();
224        if let Some(uni) = uni {
225            self.uni_to_eid.entry(uni).or_default().push(node.id);
226        }
227    }
228
229    // Remove this node from all relevant constant lookup cache structures
230    fn remove_from_cache(&mut self, node: &Rc<ExpandedNode>) {
231        self.eid_to_node.remove(&node.id);
232        if let Some(uni) = &borrow!(node.instance_node).base().template_node_identifier {
233            self.uni_to_eid
234                .entry(uni.clone())
235                .or_default()
236                .retain(|&n| n != node.id);
237        }
238    }
239}
240
241impl RuntimeContext {
242    #[cfg(not(feature = "designtime"))]
243    /// Create a runtime context for normal app execution.
244    pub fn new(globals: Globals) -> Self {
245        let application =
246            pax_runtime_api::application::ApplicationInstance::for_runtime(globals.target);
247        Self {
248            property_graph: application.property_graph(),
249            application,
250            next_uid: Cell::new(ExpandedNodeIdentifier(0)),
251            messages: RefCell::new(Vec::new()),
252            globals: RefCell::new(globals),
253            root_expanded_node: RefCell::new(Weak::new()),
254            node_cache: RefCell::new(NodeCache::new()),
255            active_touch_targets: Default::default(),
256            queued_custom_events: Default::default(),
257            queued_renders: Default::default(),
258            layer_count: Cell::default(),
259            last_topmost_element: Default::default(),
260            dirty_canvases: Default::default(),
261            dirty_canvas_nodes: Default::default(),
262            scene_geometry: Default::default(),
263            viewport_proximity: Default::default(),
264            canvas_node_light_masks: Default::default(),
265            scene_lighting_nodes: Default::default(),
266            scene_lighting_dirty: Default::default(),
267            layer_light_scopes: Default::default(),
268            #[cfg(test)]
269            lighting_mask_node_visits: Cell::new(0),
270            lighting_overflow_counts: Default::default(),
271            targeted_canvas_replay_node_ids: Default::default(),
272            removed_canvas_nodes: Default::default(),
273            occlusion_dirty: Cell::new(true),
274            occlusion_state: Default::default(),
275            layer_canvas_plan_generation: Cell::new(1),
276            canvas_drawable_layers: Default::default(),
277            import_settings_node_count: Cell::new(0),
278            tick_handler_nodes: Default::default(),
279            pre_render_handler_nodes: Default::default(),
280            gyro_handler_nodes: Default::default(),
281            accel_handler_nodes: Default::default(),
282            screenshot_map: Default::default(),
283            scroller_surface_states: Default::default(),
284            layer_scroller_owners: Default::default(),
285            root_scroller_id: Cell::new(None),
286            visual_viewport_state: Cell::new(None),
287            safe_area_insets: LocalProperty::default(),
288        }
289    }
290
291    #[cfg(feature = "designtime")]
292    /// Create a runtime context with the userland component tracked for designtime tools.
293    pub fn new(globals: Globals, userland: Rc<ComponentInstance>) -> Self {
294        let application =
295            pax_runtime_api::application::ApplicationInstance::for_runtime(globals.target);
296        Self {
297            property_graph: application.property_graph(),
298            application,
299            next_uid: Cell::new(ExpandedNodeIdentifier(0)),
300            messages: RefCell::new(Vec::new()),
301            globals: RefCell::new(globals),
302            root_expanded_node: RefCell::new(Weak::new()),
303            userland_frame_instance_node: RefCell::new(Some(userland)),
304            userland_root_expanded_node: Default::default(),
305            node_cache: RefCell::new(NodeCache::new()),
306            active_touch_targets: Default::default(),
307            queued_custom_events: Default::default(),
308            queued_renders: Default::default(),
309            layer_count: Cell::default(),
310            last_topmost_element: Default::default(),
311            dirty_canvases: Default::default(),
312            dirty_canvas_nodes: Default::default(),
313            scene_geometry: Default::default(),
314            viewport_proximity: Default::default(),
315            canvas_node_light_masks: Default::default(),
316            scene_lighting_nodes: Default::default(),
317            scene_lighting_dirty: Default::default(),
318            layer_light_scopes: Default::default(),
319            #[cfg(test)]
320            lighting_mask_node_visits: Cell::new(0),
321            lighting_overflow_counts: Default::default(),
322            targeted_canvas_replay_node_ids: Default::default(),
323            removed_canvas_nodes: Default::default(),
324            occlusion_dirty: Cell::new(true),
325            occlusion_state: Default::default(),
326            layer_canvas_plan_generation: Cell::new(1),
327            canvas_drawable_layers: Default::default(),
328            import_settings_node_count: Cell::new(0),
329            tick_handler_nodes: Default::default(),
330            pre_render_handler_nodes: Default::default(),
331            gyro_handler_nodes: Default::default(),
332            accel_handler_nodes: Default::default(),
333            screenshot_map: Default::default(),
334            scroller_surface_states: Default::default(),
335            layer_scroller_owners: Default::default(),
336            root_scroller_id: Cell::new(None),
337            visual_viewport_state: Cell::new(None),
338            safe_area_insets: LocalProperty::default(),
339        }
340    }
341
342    #[cfg(feature = "designtime")]
343    /// Create a runtime context before any userland tree has been mounted.
344    pub fn new_empty(globals: Globals) -> Self {
345        let application =
346            pax_runtime_api::application::ApplicationInstance::for_runtime(globals.target);
347        Self {
348            property_graph: application.property_graph(),
349            application,
350            next_uid: Cell::new(ExpandedNodeIdentifier(0)),
351            messages: RefCell::new(Vec::new()),
352            globals: RefCell::new(globals),
353            root_expanded_node: RefCell::new(Weak::new()),
354            userland_frame_instance_node: RefCell::new(None),
355            userland_root_expanded_node: Default::default(),
356            node_cache: RefCell::new(NodeCache::new()),
357            active_touch_targets: Default::default(),
358            queued_custom_events: Default::default(),
359            queued_renders: Default::default(),
360            layer_count: Cell::default(),
361            last_topmost_element: Default::default(),
362            dirty_canvases: Default::default(),
363            dirty_canvas_nodes: Default::default(),
364            scene_geometry: Default::default(),
365            viewport_proximity: Default::default(),
366            canvas_node_light_masks: Default::default(),
367            scene_lighting_nodes: Default::default(),
368            scene_lighting_dirty: Default::default(),
369            layer_light_scopes: Default::default(),
370            #[cfg(test)]
371            lighting_mask_node_visits: Cell::new(0),
372            lighting_overflow_counts: Default::default(),
373            targeted_canvas_replay_node_ids: Default::default(),
374            removed_canvas_nodes: Default::default(),
375            occlusion_dirty: Cell::new(true),
376            occlusion_state: Default::default(),
377            layer_canvas_plan_generation: Cell::new(1),
378            canvas_drawable_layers: Default::default(),
379            import_settings_node_count: Cell::new(0),
380            tick_handler_nodes: Default::default(),
381            pre_render_handler_nodes: Default::default(),
382            gyro_handler_nodes: Default::default(),
383            accel_handler_nodes: Default::default(),
384            screenshot_map: Default::default(),
385            scroller_surface_states: Default::default(),
386            layer_scroller_owners: Default::default(),
387            root_scroller_id: Cell::new(None),
388            visual_viewport_state: Cell::new(None),
389            safe_area_insets: LocalProperty::default(),
390        }
391    }
392
393    /// Store the root expanded node after it has been initialized.
394    pub fn register_root_expanded_node(&self, root: &Rc<ExpandedNode>) {
395        *borrow_mut!(self.root_expanded_node) = Rc::downgrade(root);
396    }
397
398    /// Clear the registered root expanded node.
399    pub fn clear_root_expanded_node(&self) {
400        *borrow_mut!(self.root_expanded_node) = Weak::new();
401        let mut state = self.occlusion_state.borrow_mut();
402        let stats = state.stats;
403        *state = Default::default();
404        state.stats = stats;
405    }
406
407    /// Add a node to runtime lookup caches.
408    pub fn add_to_cache(&self, node: &Rc<ExpandedNode>) {
409        borrow_mut!(self.node_cache).add_to_cache(node);
410        self.refresh_scene_lighting_node(node);
411        self.register_node_lifecycle_handlers(node);
412        if borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas {
413            // Initial drawing belongs to mount, independent of whether lighting
414            // happens to assign a retained mask to this primitive.
415            self.mark_canvas_node_dirty(node.id);
416            if !node.is_render_source() {
417                self.set_canvas_dirty(node.occlusion.get().render_layer_id);
418            }
419        }
420        if node.is_import_settings_node() {
421            self.import_settings_node_count
422                .set(self.import_settings_node_count.get() + 1);
423        }
424        self.mark_occlusion_dirty();
425    }
426
427    /// Remove a node from runtime lookup caches.
428    pub fn remove_from_cache(&self, node: &Rc<ExpandedNode>) {
429        borrow_mut!(self.node_cache).remove_from_cache(node);
430        if borrow_mut!(self.scene_lighting_nodes).remove(&node.id) {
431            // Ancestor lights can affect canvases other than their own layer.
432            self.set_all_canvases_dirty();
433        }
434        self.unregister_viewport_proximity(node.id.to_u32());
435        borrow_mut!(self.scene_geometry).remove(node.id.to_u32());
436        self.occlusion_state.borrow_mut().remove(node.id.to_u32());
437        borrow_mut!(self.canvas_node_light_masks).remove(&node.id);
438        borrow_mut!(self.active_touch_targets).retain(|_, target| *target != node.id);
439        self.unregister_node_lifecycle_handlers(node.id);
440        if node.is_import_settings_node() {
441            self.import_settings_node_count
442                .set(self.import_settings_node_count.get().saturating_sub(1));
443        }
444        self.mark_occlusion_dirty();
445    }
446
447    pub fn has_import_settings_nodes(&self) -> bool {
448        self.import_settings_node_count.get() > 0
449    }
450
451    pub(crate) fn refresh_scene_lighting_node(&self, node: &ExpandedNode) {
452        let mut contributors = borrow_mut!(self.scene_lighting_nodes);
453        let changed = if borrow!(node.instance_node).has_scene_lighting() {
454            contributors.insert(node.id)
455        } else {
456            contributors.remove(&node.id)
457        };
458        if changed {
459            self.set_all_canvases_dirty();
460        }
461    }
462
463    pub fn register_node_effect_property(
464        &self,
465        node: ExpandedNodeIdentifier,
466        prop: &LocalProperty<()>,
467    ) {
468        self.register_node_effect_property_named(node, prop, "node effect");
469    }
470
471    pub fn register_node_effect_property_named(
472        &self,
473        node: ExpandedNodeIdentifier,
474        prop: &LocalProperty<()>,
475        effect_name: &str,
476    ) {
477        if effect_drain_instrumentation_enabled() {
478            let debug_name = self.node_effect_debug_name(node, effect_name);
479            register_effect_property_with_name(prop, &debug_name);
480        } else {
481            register_effect_property(prop);
482        }
483    }
484
485    pub fn register_expanded_node_effect_property_named(
486        &self,
487        node: &Rc<ExpandedNode>,
488        prop: &LocalProperty<()>,
489        effect_name: &str,
490    ) {
491        if effect_drain_instrumentation_enabled() {
492            let debug_name = Self::expanded_node_effect_debug_name(node, effect_name);
493            register_effect_property_with_name(prop, &debug_name);
494        } else {
495            register_effect_property(prop);
496        }
497    }
498
499    pub fn register_node_effect(
500        &self,
501        node: ExpandedNodeIdentifier,
502        dependencies: &[UntypedProperty],
503        effect: impl Fn() + 'static,
504    ) -> LocalProperty<()> {
505        let prop = LocalProperty::computed(effect, dependencies);
506        self.register_node_effect_property_named(node, &prop, "registered node effect");
507        prop
508    }
509
510    pub fn drain_node_effects(&self) {
511        let _graph = self.property_graph.enter();
512        self.property_graph.import();
513        const MAX_NODE_EFFECTS_PER_TICK: usize = 100_000;
514        if !effect_drain_instrumentation_enabled() {
515            let drained = drain_effects(MAX_NODE_EFFECTS_PER_TICK);
516            if drained == MAX_NODE_EFFECTS_PER_TICK {
517                log::warn!(
518                    "reactive drain hit {} evaluations in one tick; deferring remaining work",
519                    MAX_NODE_EFFECTS_PER_TICK
520                );
521            }
522            return;
523        }
524
525        let start = Instant::now();
526        let report = drain_effects_with_report(MAX_NODE_EFFECTS_PER_TICK);
527        let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
528        if report.budget_exhausted {
529            log::warn!(
530                "reactive drain hit {} evaluations in one tick; deferring remaining work",
531                MAX_NODE_EFFECTS_PER_TICK
532            );
533        }
534        if report.popped > 0 || report.cutoffs_evaluated > 0 {
535            let globals = self.globals();
536            let frame_dependents = summarize_debug_names(property_outbound_debug_names(
537                &globals.elapsed_frames.untyped(),
538            ));
539            let millis_dependents = summarize_debug_names(property_outbound_debug_names(
540                &globals.elapsed_millis.untyped(),
541            ));
542            let frame_clock = globals.elapsed_frames.untyped();
543            let millis_clock = globals.elapsed_millis.untyped();
544            let (entering, exiting, enter_cleanup, exit_cleanup, stale_clock_nodes) = {
545                let nodes = borrow!(self.node_cache);
546                nodes.eid_to_node.values().fold(
547                    (0, 0, 0, 0, Vec::new()),
548                    |(entering, exiting, enter_cleanup, exit_cleanup, mut stale_clock_nodes),
549                     node| {
550                        let phase = node.transition_phase.get();
551                        if phase != TRANSITION_PHASE_ENTER
552                            && phase != TRANSITION_PHASE_EXIT
553                            && (property_has_direct_outbound(
554                                &frame_clock,
555                                &node.transition_playhead.untyped(),
556                            ) || property_has_direct_outbound(
557                                &millis_clock,
558                                &node.transition_playhead_millis.untyped(),
559                            ))
560                        {
561                            stale_clock_nodes
562                                .push(Self::expanded_node_effect_debug_name(node, "idle clock"));
563                        }
564                        (
565                            entering + usize::from(phase == TRANSITION_PHASE_ENTER),
566                            exiting + usize::from(phase == TRANSITION_PHASE_EXIT),
567                            enter_cleanup + usize::from(node.enter_cleanup_active.get()),
568                            exit_cleanup + usize::from(node.exit_cleanup_active.get()),
569                            stale_clock_nodes,
570                        )
571                    },
572                )
573            };
574            println!(
575                "[PaxEffectDrain] ran={} popped={} clean={} unregistered={} missing={} remaining={} cutoffs={} suppressed={} propagated={} remaining_cutoffs={} elapsed_ms={:.3} transitions=enter:{}/exit:{}/enter_cleanup:{}/exit_cleanup:{} stale_clocks=[{}] frame_deps=[{}] millis_deps=[{}] effects=[{}] cutoff_nodes=[{}]",
576                report.ran,
577                report.popped,
578                report.skipped_clean,
579                report.skipped_unregistered,
580                report.skipped_missing,
581                report.remaining,
582                report.cutoffs_evaluated,
583                report.cutoffs_suppressed,
584                report.cutoffs_propagated,
585                report.remaining_cutoffs,
586                elapsed_ms,
587                entering,
588                exiting,
589                enter_cleanup,
590                exit_cleanup,
591                stale_clock_nodes.join("; "),
592                format_effect_top(&frame_dependents),
593                format_effect_top(&millis_dependents),
594                format_effect_top(&report.top_effects),
595                format_effect_top(&report.top_cutoffs),
596            );
597        }
598    }
599
600    fn node_effect_debug_name(&self, node: ExpandedNodeIdentifier, effect_name: &str) -> String {
601        let node = borrow!(self.node_cache).eid_to_node.get(&node).cloned();
602        if let Some(node) = node {
603            Self::expanded_node_effect_debug_name(&node, effect_name)
604        } else {
605            format!("{effect_name} node={node:?}")
606        }
607    }
608
609    fn expanded_node_effect_debug_name(node: &Rc<ExpandedNode>, effect_name: &str) -> String {
610        let instance_node = borrow!(node.instance_node);
611        let base = instance_node.base();
612        let type_name = base
613            .template_node_type_id
614            .as_ref()
615            .map(|type_id| {
616                type_id
617                    .get_pascal_identifier()
618                    .unwrap_or_else(|| type_id.to_string())
619            })
620            .unwrap_or_else(|| "<unknown>".to_owned());
621
622        format!("{effect_name} type={type_name}")
623    }
624
625    pub fn tick_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
626        borrow!(self.tick_handler_nodes).clone()
627    }
628
629    pub fn pre_render_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
630        borrow!(self.pre_render_handler_nodes).clone()
631    }
632
633    pub fn gyro_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
634        borrow!(self.gyro_handler_nodes).clone()
635    }
636
637    pub fn accel_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
638        borrow!(self.accel_handler_nodes).clone()
639    }
640
641    fn register_node_lifecycle_handlers(&self, node: &Rc<ExpandedNode>) {
642        let registry = borrow!(node.instance_node)
643            .base()
644            .handler_registry
645            .as_ref()
646            .map(Rc::clone);
647        let Some(registry) = registry else {
648            return;
649        };
650        let handlers = &borrow!(registry).handlers;
651        if handlers.get(TICK_HANDLERS).is_some_and(|h| !h.is_empty()) {
652            self.register_lifecycle_handler(&self.tick_handler_nodes, node.id);
653        }
654        if handlers
655            .get(PRE_RENDER_HANDLERS)
656            .is_some_and(|h| !h.is_empty())
657        {
658            self.register_lifecycle_handler(&self.pre_render_handler_nodes, node.id);
659        }
660        if handlers.get(GYRO_HANDLERS).is_some_and(|h| !h.is_empty()) {
661            self.register_lifecycle_handler(&self.gyro_handler_nodes, node.id);
662        }
663        if handlers.get(ACCEL_HANDLERS).is_some_and(|h| !h.is_empty()) {
664            self.register_lifecycle_handler(&self.accel_handler_nodes, node.id);
665        }
666    }
667
668    fn unregister_node_lifecycle_handlers(&self, id: ExpandedNodeIdentifier) {
669        borrow_mut!(self.tick_handler_nodes).retain(|node_id| *node_id != id);
670        borrow_mut!(self.pre_render_handler_nodes).retain(|node_id| *node_id != id);
671        borrow_mut!(self.gyro_handler_nodes).retain(|node_id| *node_id != id);
672        borrow_mut!(self.accel_handler_nodes).retain(|node_id| *node_id != id);
673    }
674
675    fn register_lifecycle_handler(
676        &self,
677        handler_nodes: &RefCell<Vec<ExpandedNodeIdentifier>>,
678        id: ExpandedNodeIdentifier,
679    ) {
680        let mut handler_nodes = borrow_mut!(handler_nodes);
681        if !handler_nodes.contains(&id) {
682            handler_nodes.push(id);
683        }
684    }
685
686    /// Look up an expanded node by runtime id.
687    pub fn get_expanded_node_by_eid(&self, id: ExpandedNodeIdentifier) -> Option<Rc<ExpandedNode>> {
688        borrow!(self.node_cache).eid_to_node.get(&id).cloned()
689    }
690
691    /// Route a touch sequence to the node hit at touch-down, even after the finger moves away.
692    pub fn capture_touch_target(&self, identifier: i64, target: ExpandedNodeIdentifier) {
693        borrow_mut!(self.active_touch_targets).insert(identifier, target);
694    }
695
696    /// Resolve the node captured for an active touch sequence.
697    pub fn captured_touch_target(&self, identifier: i64) -> Option<Rc<ExpandedNode>> {
698        let target = borrow!(self.active_touch_targets)
699            .get(&identifier)
700            .copied()?;
701        self.get_expanded_node_by_eid(target)
702    }
703
704    /// Release and resolve the node captured for a completed touch sequence.
705    pub fn release_touch_target(&self, identifier: i64) -> Option<Rc<ExpandedNode>> {
706        let target = borrow_mut!(self.active_touch_targets).remove(&identifier)?;
707        self.get_expanded_node_by_eid(target)
708    }
709
710    /// Store a screenshot payload delivered by the chassis.
711    pub fn load_screenshot(&self, id: u32, data: ScreenshotData) -> bool {
712        borrow_mut!(self.screenshot_map).insert(id, data);
713        true
714    }
715
716    /// Shared screenshot capture map keyed by request id.
717    pub fn get_screenshot_map(&self) -> Rc<RefCell<HashMap<u32, ScreenshotData>>> {
718        Rc::clone(&self.screenshot_map)
719    }
720
721    /// Remember browser-owned scroller state for native compositing and scroll transforms.
722    pub fn set_scroller_surface_state(
723        &self,
724        id: u32,
725        state: ScrollerSurfaceState,
726    ) -> ScrollerSurfaceStateChange {
727        let change = {
728            let mut states = borrow_mut!(self.scroller_surface_states);
729            let change = match states.get(&id) {
730                Some(previous) if previous.structural_eq(&state) && previous.scroll_eq(&state) => {
731                    ScrollerSurfaceStateChange::Unchanged
732                }
733                Some(previous) if previous.structural_eq(&state) => {
734                    ScrollerSurfaceStateChange::ScrollOnly
735                }
736                _ => ScrollerSurfaceStateChange::Structural,
737            };
738            if change != ScrollerSurfaceStateChange::Unchanged {
739                states.insert(id, state);
740            }
741            change
742        };
743
744        match change {
745            ScrollerSurfaceStateChange::Unchanged => {}
746            ScrollerSurfaceStateChange::ScrollOnly => {
747                self.mark_occlusion_presentation_dirty(id);
748                self.invalidate_viewport_presentation();
749                self.mark_layer_canvas_plans_dirty();
750                self.mark_scroller_content_layers_dirty(id);
751            }
752            ScrollerSurfaceStateChange::Structural => {
753                self.invalidate_viewport_presentation();
754                self.mark_layer_canvas_plans_dirty();
755                self.mark_occlusion_dirty();
756            }
757        }
758
759        change
760    }
761
762    /// Update hot scroll offsets for an existing native scroller surface without touching
763    /// structural state.
764    pub fn update_scroller_surface_scroll(
765        &self,
766        id: u32,
767        scroll_x: f64,
768        scroll_y: f64,
769        presentation_scroll_x: f64,
770        presentation_scroll_y: f64,
771    ) -> ScrollerSurfaceStateChange {
772        let change = {
773            let mut states = borrow_mut!(self.scroller_surface_states);
774            let Some(state) = states.get_mut(&id) else {
775                return ScrollerSurfaceStateChange::Unchanged;
776            };
777            let next = ScrollerSurfaceState {
778                scroll_x,
779                scroll_y,
780                presentation_scroll_x,
781                presentation_scroll_y,
782                ..*state
783            };
784            let change = if state.scroll_eq(&next) {
785                ScrollerSurfaceStateChange::Unchanged
786            } else {
787                ScrollerSurfaceStateChange::ScrollOnly
788            };
789            if change != ScrollerSurfaceStateChange::Unchanged {
790                *state = next;
791            }
792            change
793        };
794
795        if change == ScrollerSurfaceStateChange::ScrollOnly {
796            self.mark_occlusion_presentation_dirty(id);
797            self.invalidate_viewport_presentation();
798            self.mark_layer_canvas_plans_dirty();
799        }
800
801        change
802    }
803
804    /// Remove cached scroller surface state.
805    pub fn remove_scroller_surface_state(&self, id: u32) {
806        borrow_mut!(self.scroller_surface_states).remove(&id);
807        self.invalidate_viewport_presentation();
808        self.mark_layer_canvas_plans_dirty();
809        self.mark_occlusion_dirty();
810    }
811
812    /// Fetch cached scroller surface state by node id.
813    pub fn get_scroller_surface_state(&self, id: u32) -> Option<ScrollerSurfaceState> {
814        borrow!(self.scroller_surface_states).get(&id).cloned()
815    }
816
817    /// Fetch the presentation scroll offset for a native scroller surface, falling back to the
818    /// authoritative scroll position when presentation scroll is unavailable.
819    pub fn get_scroller_surface_scroll(&self, id: u32) -> Option<(f64, f64)> {
820        borrow!(self.scroller_surface_states).get(&id).map(|state| {
821            let scroll_x = if state.presentation_scroll_x.is_finite() {
822                state.presentation_scroll_x
823            } else {
824                state.scroll_x
825            };
826            let scroll_y = if state.presentation_scroll_y.is_finite() {
827                state.presentation_scroll_y
828            } else {
829                state.scroll_y
830            };
831            (scroll_x, scroll_y)
832        })
833    }
834
835    pub(crate) fn scroller_content_presentation_transform(&self, node: &ExpandedNode) -> Affine {
836        fn clamp_offset(value: f64, content: f64, viewport: f64) -> f64 {
837            if content <= viewport || !value.is_finite() {
838                return 0.0;
839            }
840            value.max(0.0).min((content - viewport).max(0.0))
841        }
842
843        let root_delegates_to_page_scroll = self.get_root_scroller_id() == Some(node.id.to_u32())
844            && self.get_visual_viewport_state().is_some();
845        if root_delegates_to_page_scroll {
846            return Affine::IDENTITY;
847        }
848
849        let instance_node = borrow!(node.instance_node);
850        let (scroll_x, scroll_y) = if self.get_root_scroller_id() == Some(node.id.to_u32()) {
851            if let Some(visual) = self.get_visual_viewport_state() {
852                let visual_x = visual.page_scroll_x + visual.offset_x;
853                let visual_y = visual.page_scroll_y + visual.offset_y;
854                if visual_x.is_finite() && visual_y.is_finite() {
855                    if let Some(state) = self.get_scroller_surface_state(node.id.to_u32()) {
856                        let viewport_width = if visual.width.is_finite() {
857                            visual.width
858                        } else {
859                            state.viewport_width
860                        };
861                        let viewport_height = if visual.height.is_finite() {
862                            visual.height
863                        } else {
864                            state.viewport_height
865                        };
866                        (
867                            clamp_offset(visual_x, state.content_width, viewport_width),
868                            clamp_offset(visual_y, state.content_height, viewport_height),
869                        )
870                    } else {
871                        (visual_x, visual_y)
872                    }
873                } else {
874                    self.get_scroller_surface_scroll(node.id.to_u32())
875                        .or_else(|| instance_node.resolve_scroll_offset(node))
876                        .unwrap_or((0.0, 0.0))
877                }
878            } else {
879                self.get_scroller_surface_scroll(node.id.to_u32())
880                    .or_else(|| instance_node.resolve_scroll_offset(node))
881                    .unwrap_or((0.0, 0.0))
882            }
883        } else {
884            self.get_scroller_surface_scroll(node.id.to_u32())
885                .or_else(|| instance_node.resolve_scroll_offset(node))
886                .unwrap_or((0.0, 0.0))
887        };
888
889        if scroll_x.abs() <= f64::EPSILON && scroll_y.abs() <= f64::EPSILON {
890            return Affine::IDENTITY;
891        }
892
893        let world_transform = Affine::from(node.transform_and_bounds.get().transform);
894        let inverse_world = Affine::from(node.transform_and_bounds.get().transform.inverse());
895        world_transform * Affine::translate((-scroll_x, -scroll_y)) * inverse_world
896    }
897
898    /// Resolve the browser/native presentation transform inherited by a node from ancestor
899    /// scrollers. Layout transforms remain in content coordinates, while events arrive in the
900    /// scrolled window coordinates that the user sees.
901    pub(crate) fn presentation_scroll_transform_for_node(&self, node: &ExpandedNode) -> Affine {
902        let mut ancestors = Vec::new();
903        let mut current = node.render_parent_node();
904        while let Some(parent) = current {
905            current = parent.render_parent_node();
906            ancestors.push(parent);
907        }
908        ancestors.reverse();
909
910        ancestors
911            .into_iter()
912            .filter(|ancestor| borrow!(ancestor.instance_node).scrolls_content(ancestor))
913            .fold(Affine::IDENTITY, |transform, scroller| {
914                transform * self.scroller_content_presentation_transform(&scroller)
915            })
916    }
917
918    /// Clear render-layer-to-scroller ownership before recomputing occlusion.
919    pub fn clear_layer_scroller_owners(&self) {
920        borrow_mut!(self.layer_scroller_owners).clear();
921        self.mark_layer_canvas_plans_dirty();
922    }
923
924    /// Record that a render layer is owned by a particular scroller.
925    pub fn register_layer_scroller_owner(
926        &self,
927        layer_id: usize,
928        scroller_id: ExpandedNodeIdentifier,
929    ) {
930        borrow_mut!(self.layer_scroller_owners).insert(layer_id, scroller_id);
931        self.mark_layer_canvas_plans_dirty();
932    }
933
934    /// Find the scroller that owns a render layer, when one exists.
935    pub fn get_layer_scroller_owner(&self, layer_id: usize) -> Option<ExpandedNodeIdentifier> {
936        borrow!(self.layer_scroller_owners).get(&layer_id).copied()
937    }
938
939    fn mark_scroller_content_layers_dirty(&self, scroller_id: u32) {
940        let layers = borrow!(self.layer_scroller_owners)
941            .iter()
942            .filter_map(|(layer, owner)| (owner.to_u32() == scroller_id).then_some(*layer))
943            .collect::<Vec<_>>();
944        for layer in layers {
945            self.set_canvas_dirty(layer);
946        }
947    }
948
949    /// Mark which node currently delegates root scrolling behavior to the page.
950    pub fn set_root_scroller_id(&self, id: Option<u32>) {
951        self.root_scroller_id.set(id);
952        self.invalidate_viewport_presentation();
953        self.mark_layer_canvas_plans_dirty();
954        self.mark_occlusion_dirty();
955    }
956
957    /// Current page-scroll-backed root scroller id.
958    pub fn get_root_scroller_id(&self) -> Option<u32> {
959        self.root_scroller_id.get()
960    }
961
962    /// Live native safe-area data used by opt-in layout primitives. Defaults to zero.
963    pub fn safe_area_insets(&self) -> LocalProperty<SafeAreaInsets> {
964        self.safe_area_insets.clone()
965    }
966
967    /// Update native safe-area data without changing the viewport or root layout.
968    pub fn set_safe_area_insets(&self, insets: SafeAreaInsets) {
969        let valid = |value: f64| {
970            if value.is_finite() {
971                value.max(0.0)
972            } else {
973                0.0
974            }
975        };
976        self.safe_area_insets.set_if_neq(SafeAreaInsets {
977            top: valid(insets.top),
978            right: valid(insets.right),
979            bottom: valid(insets.bottom),
980            left: valid(insets.left),
981        });
982    }
983
984    /// Cache the browser visual viewport state for root scroller math.
985    pub fn set_visual_viewport_state(&self, state: VisualViewportState) {
986        self.visual_viewport_state.set(Some(state));
987        self.invalidate_viewport_presentation();
988        self.mark_layer_canvas_plans_dirty();
989        self.mark_occlusion_dirty();
990    }
991
992    /// Clear cached visual viewport state.
993    pub fn clear_visual_viewport_state(&self) {
994        self.visual_viewport_state.set(None);
995        self.invalidate_viewport_presentation();
996        self.mark_layer_canvas_plans_dirty();
997        self.mark_occlusion_dirty();
998    }
999
1000    /// Return cached browser visual viewport state, if available.
1001    pub fn get_visual_viewport_state(&self) -> Option<VisualViewportState> {
1002        self.visual_viewport_state.get()
1003    }
1004
1005    /// Ensure the dirty-canvas table has entries up to the requested layer count.
1006    pub fn resize_canvas_layers_to(&self, id: usize) {
1007        let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
1008        let old_len = dirty_canvases.len();
1009        dirty_canvases.resize(id, false);
1010        for dirty in dirty_canvases.iter_mut().skip(old_len) {
1011            *dirty = true;
1012        }
1013        if old_len != id {
1014            self.mark_layer_canvas_plans_dirty();
1015        }
1016    }
1017
1018    pub fn mark_layer_canvas_plans_dirty(&self) {
1019        // Render-layer reassignment does not change observed layout/scroll
1020        // geometry. Presentation setters invalidate observation explicitly.
1021        let next = self.layer_canvas_plan_generation.get().wrapping_add(1);
1022        self.layer_canvas_plan_generation.set(next.max(1));
1023    }
1024
1025    pub fn layer_canvas_plan_generation(&self) -> u32 {
1026        self.layer_canvas_plan_generation.get()
1027    }
1028
1029    /// Replace the set of render layers that currently contain canvas drawables.
1030    pub fn set_canvas_drawable_layers(&self, layers: HashSet<usize>) {
1031        let mut current = borrow_mut!(self.canvas_drawable_layers);
1032        if *current == layers {
1033            return;
1034        }
1035        *current = layers;
1036        self.mark_layer_canvas_plans_dirty();
1037    }
1038
1039    /// Return whether a render layer currently has canvas work to paint.
1040    pub fn layer_has_canvas_drawables(&self, layer: usize) -> bool {
1041        borrow!(self.canvas_drawable_layers).contains(&layer)
1042    }
1043
1044    /// Mark every canvas layer clean.
1045    pub fn clear_all_dirty_canvases(&self) {
1046        let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
1047        for v in dirty_canvases.iter_mut() {
1048            *v = false;
1049        }
1050    }
1051
1052    /// Mark a canvas layer dirty.
1053    pub fn set_canvas_dirty(&self, id: usize) {
1054        let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
1055        if let Some(v) = dirty_canvases.get_mut(id) {
1056            *v = true;
1057        }
1058    }
1059
1060    /// Check whether a canvas layer needs redraw.
1061    pub fn is_canvas_dirty(&self, id: &usize) -> bool {
1062        *borrow!(self.dirty_canvases).get(*id).unwrap_or(&true)
1063    }
1064
1065    pub fn dirty_canvas_layers(&self) -> Vec<usize> {
1066        borrow!(self.dirty_canvases)
1067            .iter()
1068            .enumerate()
1069            .filter_map(|(index, dirty)| dirty.then_some(index))
1070            .collect()
1071    }
1072
1073    pub fn has_dirty_canvas_layers(&self) -> bool {
1074        borrow!(self.dirty_canvases).iter().any(|dirty| *dirty)
1075    }
1076
1077    pub fn has_dirty_canvas_nodes(&self) -> bool {
1078        !borrow!(self.dirty_canvas_nodes).is_empty()
1079    }
1080
1081    pub fn has_canvas_node_removals(&self) -> bool {
1082        !borrow!(self.removed_canvas_nodes).is_empty()
1083    }
1084
1085    pub fn has_canvas_render_work(&self) -> bool {
1086        self.has_dirty_canvas_layers()
1087            || self.has_canvas_node_removals()
1088            || self.scene_lighting_dirty.get()
1089    }
1090
1091    /// Schedule lighting resolution without invalidating canvas geometry or contents.
1092    /// The retained renderer decides which surfaces depend on the changed lights.
1093    pub fn mark_scene_lighting_dirty(&self) {
1094        self.scene_lighting_dirty.set(true);
1095    }
1096
1097    pub(crate) fn take_scene_lighting_dirty(&self) -> bool {
1098        self.scene_lighting_dirty.replace(false)
1099    }
1100
1101    pub fn mark_occlusion_dirty(&self) {
1102        self.occlusion_dirty.set(true);
1103        self.occlusion_state.borrow_mut().structural = true;
1104        // Reuse structural invalidation for layer ownership and LightFrame ancestry.
1105        // Geometry/scroll-only invalidation deliberately leaves these masks valid.
1106        borrow_mut!(self.layer_light_scopes).clear();
1107        if !borrow!(self.scene_lighting_nodes).is_empty()
1108            || !borrow!(self.canvas_node_light_masks).is_empty()
1109        {
1110            self.set_all_canvases_dirty();
1111        }
1112    }
1113
1114    /// Invalidate native compositing after a node's paint or coverage changes.
1115    /// Detached source changes reconcile their owning mask in the visible scene.
1116    pub fn mark_node_occlusion_dirty(&self, mut id: u32) {
1117        loop {
1118            // Source records are cached for capture but absent from visible
1119            // compositing. Reconcile their owning mask, including nested sources,
1120            // so source animation does not look like a missing scene node.
1121            borrow_mut!(self.scene_geometry).invalidate_existing(id);
1122            let owner = self
1123                .get_expanded_node_by_eid(ExpandedNodeIdentifier(id))
1124                .and_then(|node| node.render_source_owner());
1125            let Some(owner) = owner else {
1126                break;
1127            };
1128            id = owner.to_u32();
1129        }
1130        self.occlusion_dirty.set(true);
1131        self.occlusion_state.borrow_mut().dirty.insert(id);
1132    }
1133
1134    fn mark_occlusion_presentation_dirty(&self, id: u32) {
1135        self.occlusion_dirty.set(true);
1136        self.occlusion_state.borrow_mut().scrolled.insert(id);
1137    }
1138
1139    /// Cumulative work performed by native compositing, including conservative rebuilds.
1140    pub fn occlusion_stats(&self) -> crate::engine::occlusion::OcclusionStats {
1141        self.occlusion_state.borrow().stats
1142    }
1143
1144    pub fn take_occlusion_dirty(&self) -> bool {
1145        let dirty = self.occlusion_dirty.get();
1146        self.occlusion_dirty.set(false);
1147        dirty
1148    }
1149
1150    pub fn set_all_canvases_dirty(&self) {
1151        let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
1152        for v in dirty_canvases.iter_mut() {
1153            *v = true;
1154        }
1155    }
1156
1157    pub fn mark_canvas_node_dirty(&self, id: ExpandedNodeIdentifier) {
1158        borrow_mut!(self.scene_geometry).invalidate(id.to_u32());
1159        let owner = self
1160            .get_expanded_node_by_eid(id)
1161            .and_then(|node| node.render_source_owner())
1162            .and_then(|id| self.get_expanded_node_by_eid(id));
1163        if let Some(owner) = owner {
1164            owner.changed_listener.invalidate();
1165            return;
1166        }
1167        borrow_mut!(self.dirty_canvas_nodes).insert(id);
1168        #[cfg(feature = "shaders")]
1169        {
1170            // Filter support can reach neighboring tiles beyond a leaf's original coverage. Include
1171            // the owning boundaries in damage/replay selection; native coverage remains unchanged.
1172            let mut parent = self
1173                .get_expanded_node_by_eid(id)
1174                .and_then(|n| n.render_parent_node());
1175            while let Some(node) = parent {
1176                if node.shader_effect_bound.get() {
1177                    borrow_mut!(self.dirty_canvas_nodes).insert(node.id);
1178                    for ids in borrow_mut!(self.targeted_canvas_replay_node_ids).values_mut() {
1179                        ids.remove(&node.id.to_u32());
1180                    }
1181                }
1182                parent = node.render_parent_node();
1183            }
1184        }
1185
1186        // Replay is surface-local; an independent content/animation change must
1187        // also reach surviving tiles, even when this node intersects the replay.
1188        for ids in borrow_mut!(self.targeted_canvas_replay_node_ids).values_mut() {
1189            ids.remove(&id.to_u32());
1190        }
1191    }
1192
1193    pub fn clear_canvas_node_dirty(&self, id: &ExpandedNodeIdentifier) {
1194        borrow_mut!(self.dirty_canvas_nodes).remove(id);
1195    }
1196
1197    pub fn is_canvas_node_dirty(&self, id: &ExpandedNodeIdentifier) -> bool {
1198        borrow!(self.dirty_canvas_nodes).contains(id)
1199    }
1200
1201    pub fn dirty_canvas_node_ids(&self) -> Vec<ExpandedNodeIdentifier> {
1202        borrow!(self.dirty_canvas_nodes).iter().copied().collect()
1203    }
1204
1205    pub fn mark_canvas_nodes_on_layer_dirty(&self, layer: usize) {
1206        if let Some(ids) = borrow_mut!(self.targeted_canvas_replay_node_ids).get_mut(&layer) {
1207            ids.clear();
1208        }
1209        let node_cache = borrow!(self.node_cache);
1210        let dirty_nodes = &mut *borrow_mut!(self.dirty_canvas_nodes);
1211        for node in node_cache.eid_to_node.values() {
1212            if !node.is_render_source()
1213                && node.occlusion.get().render_layer_id == layer
1214                && borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas
1215            {
1216                dirty_nodes.insert(node.id);
1217            }
1218        }
1219    }
1220
1221    /// Return the direct-light membership mask resolved for a retained canvas node.
1222    pub fn canvas_node_light_mask(&self, id: ExpandedNodeIdentifier) -> u32 {
1223        borrow!(self.canvas_node_light_masks)
1224            .get(&id)
1225            .copied()
1226            .unwrap_or(0)
1227    }
1228
1229    /// Resolve registered direct/ambient contributors in the target layer's coordinates.
1230    ///
1231    /// Light values remain live on every render; retained node masks are rebuilt only
1232    /// when selected light slots, lexical scope, or scene structure changes. Contributors
1233    /// are independent of viewport visibility, and an unlit scene needs no node scan.
1234    pub fn collect_scene_lighting_for_layer(&self, layer: usize) -> SceneLighting {
1235        if borrow!(self.scene_lighting_nodes).is_empty()
1236            && borrow!(self.canvas_node_light_masks).is_empty()
1237        {
1238            borrow_mut!(self.lighting_overflow_counts).remove(&layer);
1239            return SceneLighting::with_default_ambient(Vec::new());
1240        }
1241        let layer_to_root_transforms = self.layer_to_root_transforms(layer);
1242        let root_to_target = layer_to_root_transforms[&layer].inverse();
1243        let node_cache = borrow!(self.node_cache);
1244        let mut scoped_lights = Vec::new();
1245        let mut topmost_ambient = None;
1246
1247        for id in borrow!(self.scene_lighting_nodes).iter() {
1248            let Some(node) = node_cache.eid_to_node.get(id) else {
1249                continue;
1250            };
1251            if node.is_render_source() {
1252                continue;
1253            }
1254            let occlusion = node.occlusion.get();
1255            let source_layer = occlusion.render_layer_id;
1256            let Some(source_to_root) = layer_to_root_transforms.get(&source_layer) else {
1257                continue;
1258            };
1259
1260            let (light, ambient) = {
1261                let instance_node = borrow!(node.instance_node);
1262                (
1263                    instance_node.resolve_scene_light(node, self),
1264                    instance_node.resolve_scene_ambient_light(node, self),
1265                )
1266            };
1267            if let Some(light) = light {
1268                if light.enabled {
1269                    scoped_lights.push((
1270                        occlusion.z_index,
1271                        node.id,
1272                        Self::nearest_light_frame(node),
1273                        transform_scene_light_for_layer(light, *source_to_root, root_to_target),
1274                    ));
1275                }
1276            }
1277            if let Some(ambient) = ambient {
1278                let should_replace = topmost_ambient
1279                    .as_ref()
1280                    .map(|(z_index, id, _)| (occlusion.z_index, node.id) >= (*z_index, *id))
1281                    .unwrap_or(true);
1282                if should_replace {
1283                    topmost_ambient = Some((occlusion.z_index, node.id, ambient));
1284                }
1285            }
1286        }
1287
1288        scoped_lights
1289            .sort_by(|left, right| right.0.cmp(&left.0).then_with(|| left.1.cmp(&right.1)));
1290        let overflow = scoped_lights
1291            .len()
1292            .saturating_sub(SceneLighting::MAX_LIGHTS);
1293        scoped_lights.truncate(SceneLighting::MAX_LIGHTS);
1294
1295        {
1296            let mut overflow_counts = borrow_mut!(self.lighting_overflow_counts);
1297            if overflow == 0 {
1298                overflow_counts.remove(&layer);
1299            } else if overflow_counts.insert(layer, overflow) != Some(overflow) {
1300                log::warn!(
1301                    "canvas layer {layer} has more than {} enabled lights; omitting {overflow}",
1302                    SceneLighting::MAX_LIGHTS
1303                );
1304            }
1305        }
1306
1307        let scopes = scoped_lights
1308            .iter()
1309            .map(|(_, id, frame, _)| (*id, *frame))
1310            .collect::<Vec<_>>();
1311        let scopes_changed = borrow!(self.layer_light_scopes).get(&layer) != Some(&scopes);
1312        if scopes_changed {
1313            let mut masks = borrow_mut!(self.canvas_node_light_masks);
1314            // Empty masks are implicit. An unlit/ambient-only scene never needs a
1315            // recipient scan, unless retained masks from earlier lights need clearing.
1316            if !scopes.is_empty() || !masks.is_empty() {
1317                let mut dirty_nodes = borrow_mut!(self.dirty_canvas_nodes);
1318                for node in node_cache.eid_to_node.values() {
1319                    #[cfg(test)]
1320                    self.lighting_mask_node_visits
1321                        .set(self.lighting_mask_node_visits.get() + 1);
1322                    if node.occlusion.get().render_layer_id != layer
1323                        || borrow!(node.instance_node).base().flags().layer
1324                            != crate::api::Layer::Canvas
1325                    {
1326                        continue;
1327                    }
1328                    let mask = if scopes.is_empty() {
1329                        0
1330                    } else {
1331                        let ancestry = Self::light_frame_ancestry(node);
1332                        scopes
1333                            .iter()
1334                            .enumerate()
1335                            .fold(0u32, |mask, (slot, (_, owner))| {
1336                                if light_reaches_frame_ancestry(owner.as_ref(), &ancestry) {
1337                                    mask | (1u32 << slot)
1338                                } else {
1339                                    mask
1340                                }
1341                            })
1342                    };
1343                    let previous = if mask == 0 {
1344                        masks.remove(&node.id)
1345                    } else {
1346                        masks.insert(node.id, mask)
1347                    }
1348                    .unwrap_or(0);
1349                    if previous != mask {
1350                        self.set_canvas_dirty(layer);
1351                        dirty_nodes.insert(node.id);
1352                        if let Some(ids) =
1353                            borrow_mut!(self.targeted_canvas_replay_node_ids).get_mut(&layer)
1354                        {
1355                            ids.remove(&node.id.to_u32());
1356                        }
1357                    }
1358                }
1359            }
1360            borrow_mut!(self.layer_light_scopes).insert(layer, scopes);
1361        }
1362        drop(node_cache);
1363
1364        let lights = scoped_lights
1365            .into_iter()
1366            .map(|(_, _, _, light)| light)
1367            .collect::<Vec<_>>();
1368
1369        if let Some((_, _, ambient)) = topmost_ambient {
1370            SceneLighting {
1371                active: true,
1372                ambient_is_authored: true,
1373                ambient,
1374                lights,
1375            }
1376        } else {
1377            SceneLighting::with_default_ambient(lights)
1378        }
1379    }
1380
1381    fn nearest_light_frame(node: &ExpandedNode) -> Option<ExpandedNodeIdentifier> {
1382        let mut current = node.render_parent_node();
1383        while let Some(parent) = current {
1384            if borrow!(parent.instance_node).establishes_light_frame() {
1385                return Some(parent.id);
1386            }
1387            current = parent.render_parent_node();
1388        }
1389        None
1390    }
1391
1392    fn light_frame_ancestry(node: &ExpandedNode) -> HashSet<ExpandedNodeIdentifier> {
1393        let mut frames = HashSet::new();
1394        let mut current = node.render_parent_node();
1395        while let Some(parent) = current {
1396            if borrow!(parent.instance_node).establishes_light_frame() {
1397                frames.insert(parent.id);
1398            }
1399            current = parent.render_parent_node();
1400        }
1401        frames
1402    }
1403
1404    fn layer_to_root_transforms(&self, layer: usize) -> HashMap<usize, Affine> {
1405        let mut transforms = HashMap::new();
1406        transforms.insert(layer, self.layer_to_root_transform(layer));
1407
1408        let mut current_layer = layer;
1409        let mut visited = HashSet::new();
1410        while let Some(owner_id) = self.get_layer_scroller_owner(current_layer) {
1411            if !visited.insert(current_layer) {
1412                break;
1413            }
1414            let Some(owner_node) = self.get_expanded_node_by_eid(owner_id) else {
1415                break;
1416            };
1417            let owner_layer = owner_node.occlusion.get().render_layer_id;
1418            transforms
1419                .entry(owner_layer)
1420                .or_insert_with(|| self.layer_to_root_transform(owner_layer));
1421            if owner_layer == current_layer {
1422                break;
1423            }
1424            current_layer = owner_layer;
1425        }
1426
1427        transforms
1428    }
1429
1430    fn layer_to_root_transform(&self, layer: usize) -> Affine {
1431        let mut transform = Affine::IDENTITY;
1432        let mut current_layer = layer;
1433        let mut visited = HashSet::new();
1434
1435        while let Some(owner_id) = self.get_layer_scroller_owner(current_layer) {
1436            if !visited.insert(current_layer) {
1437                break;
1438            }
1439            let Some(owner_node) = self.get_expanded_node_by_eid(owner_id) else {
1440                break;
1441            };
1442            let owner_layer = owner_node.occlusion.get().render_layer_id;
1443            let owner_transform = self.canvas_surface_transform_for_node(&owner_node);
1444            let (scroll_x, scroll_y) = self
1445                .get_scroller_surface_scroll(owner_node.id.to_u32())
1446                .or_else(|| borrow!(owner_node.instance_node).resolve_scroll_offset(&owner_node))
1447                .unwrap_or((0.0, 0.0));
1448            transform = owner_transform * Affine::translate((-scroll_x, -scroll_y)) * transform;
1449
1450            if owner_layer == current_layer {
1451                break;
1452            }
1453            current_layer = owner_layer;
1454        }
1455
1456        transform
1457    }
1458
1459    /// Resolve a node's local coordinates into its owning canvas's content space.
1460    /// Scroller-owned layers use the registered owner's origin; root layers use
1461    /// window coordinates. Detached mask sources retain their scene transform until
1462    /// capture projection. Physical tile offsets are applied later by the renderer.
1463    pub fn canvas_surface_transform_for_node(&self, node: &ExpandedNode) -> Affine {
1464        let transform = Affine::from(node.transform_and_bounds.get().transform);
1465        if node.is_render_source() {
1466            return transform;
1467        }
1468        let own_layer = node.occlusion.get().render_layer_id;
1469        // Structural Frame/Mask ancestors can retain a different/default layer
1470        // without owning a canvas. Using them as the origin displaces content
1471        // relative to clips and alpha masks in the actual Scroller surface.
1472        self.get_layer_scroller_owner(own_layer)
1473            .and_then(|owner| self.get_expanded_node_by_eid(owner))
1474            .map_or(transform, |owner| {
1475                Affine::from(owner.transform_and_bounds.get().transform.inverse()) * transform
1476            })
1477    }
1478
1479    pub(crate) fn invalidate_scene_geometry(&self, id: u32) {
1480        borrow_mut!(self.scene_geometry).invalidate(id);
1481    }
1482
1483    pub(crate) fn remove_observation_geometry(&self, id: u32) {
1484        if let Some(node) = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(id)) {
1485            let instance = borrow!(node.instance_node);
1486            if instance.base().flags().layer != crate::api::Layer::DontCare
1487                || instance.materializes_native_surface(&node)
1488            {
1489                return;
1490            }
1491        }
1492        borrow_mut!(self.scene_geometry).remove(id);
1493    }
1494
1495    /// Prepare geometry dirtied by scene changes. Surface replay alone does not invalidate it.
1496    pub fn prepare_scene_geometry(&self) {
1497        let dirty = borrow_mut!(self.scene_geometry).take_dirty();
1498        for id in dirty {
1499            // A primitive may have requested another pending record while preparing.
1500            if borrow!(self.scene_geometry).get(id).is_some() {
1501                continue;
1502            }
1503            if let Some(node) = self.get_expanded_node_by_eid(ExpandedNodeIdentifier(id)) {
1504                if borrow!(node.instance_node).base().flags().layer != crate::api::Layer::DontCare
1505                    || borrow!(node.instance_node).materializes_native_surface(&node)
1506                    || self.viewport_dependency(id)
1507                {
1508                    self.prepare_canvas_geometry(&node);
1509                } else {
1510                    borrow_mut!(self.scene_geometry).remove(id);
1511                }
1512            }
1513        }
1514    }
1515
1516    fn prepare_canvas_geometry(
1517        &self,
1518        node: &ExpandedNode,
1519    ) -> Rc<crate::scene_geometry::PreparedCanvasGeometry> {
1520        let tab = node.transform_and_bounds.get();
1521        let local = borrow!(node.instance_node).prepare_canvas_geometry(node);
1522        let mut prepared = crate::scene_geometry::PreparedCanvasGeometry::new(
1523            node.occlusion.get().render_layer_id,
1524            tab.bounds,
1525            self.canvas_surface_transform_for_node(node),
1526            local,
1527        );
1528        prepared.world_transform = Affine::from(tab.transform);
1529        // Detached mask sources reuse prepared geometry without entering visible
1530        // replay or native-compositing indexes.
1531        prepared.is_canvas = !node.is_render_source()
1532            && borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas;
1533        prepared.is_native = !node.is_render_source()
1534            && (matches!(
1535                borrow!(node.instance_node).base().flags().layer,
1536                crate::api::Layer::Native | crate::api::Layer::NativeNonOccluding
1537            ) || borrow!(node.instance_node).materializes_native_surface(node));
1538        borrow_mut!(self.scene_geometry).insert(node.id.to_u32(), prepared)
1539    }
1540
1541    /// Obtain the same prepared record used by spatial replay selection.
1542    pub fn canvas_geometry_for_node(
1543        &self,
1544        node: &ExpandedNode,
1545    ) -> Rc<crate::scene_geometry::PreparedCanvasGeometry> {
1546        let cached = borrow!(self.scene_geometry).get(node.id.to_u32());
1547        cached.unwrap_or_else(|| self.prepare_canvas_geometry(node))
1548    }
1549
1550    /// Work counters for geometry preparation and spatial queries.
1551    pub fn scene_geometry_stats(&self) -> crate::scene_geometry::SceneGeometryStats {
1552        borrow!(self.scene_geometry).stats
1553    }
1554
1555    /// Select canvas nodes using settled runtime geometry, even before their first draw.
1556    /// Call after draining scene effects; querying does not advance layout or dispatch events.
1557    pub fn canvas_nodes_intersecting(
1558        &self,
1559        layer: usize,
1560        regions: &[kurbo::Rect],
1561    ) -> Option<Vec<u32>> {
1562        self.prepare_scene_geometry();
1563        borrow_mut!(self.scene_geometry).query(layer, regions)
1564    }
1565
1566    pub(crate) fn compositing_nodes_intersecting(
1567        &self,
1568        layer: usize,
1569        regions: &[kurbo::Rect],
1570        native: bool,
1571    ) -> Vec<u32> {
1572        borrow_mut!(self.scene_geometry).query_exact(layer, regions, native)
1573    }
1574
1575    /// Apply a backend's replay region request through the shared scene index.
1576    pub fn request_canvas_replay(&self, update: pax_runtime_api::ReplayCanvasLayerUpdate) {
1577        if let Some(ids) = update
1578            .regions
1579            .as_deref()
1580            .and_then(|regions| self.canvas_nodes_intersecting(update.layer, regions))
1581        {
1582            self.mark_targeted_canvas_replay_nodes(update.layer, &ids);
1583            self.mark_canvas_nodes_on_layer_dirty_by_id(update.layer, &ids);
1584        } else {
1585            self.mark_canvas_nodes_on_layer_dirty(update.layer);
1586        }
1587        self.set_canvas_dirty(update.layer);
1588    }
1589
1590    pub fn mark_canvas_nodes_on_layer_dirty_by_id(&self, layer: usize, node_ids: &[u32]) {
1591        let node_cache = borrow!(self.node_cache);
1592        let dirty_nodes = &mut *borrow_mut!(self.dirty_canvas_nodes);
1593        for node_id in node_ids {
1594            let id = ExpandedNodeIdentifier(*node_id);
1595            let Some(node) = node_cache.eid_to_node.get(&id) else {
1596                continue;
1597            };
1598            if node.occlusion.get().render_layer_id == layer
1599                && borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas
1600            {
1601                dirty_nodes.insert(id);
1602            }
1603        }
1604    }
1605
1606    pub fn mark_targeted_canvas_replay_nodes(&self, layer: usize, node_ids: &[u32]) {
1607        let mut targeted = borrow_mut!(self.targeted_canvas_replay_node_ids);
1608        let layer_nodes = targeted.entry(layer).or_default();
1609        let dirty = borrow!(self.dirty_canvas_nodes);
1610        // A second region request may include nodes already dirtied by replay.
1611        // Never relabel an independently dirty node as replay-only, though: its
1612        // changed pixels need updating outside the requested region as well.
1613        for id in node_ids {
1614            if !dirty.contains(&ExpandedNodeIdentifier(*id)) {
1615                layer_nodes.insert(*id);
1616            }
1617        }
1618    }
1619
1620    pub fn take_targeted_canvas_replay_node_ids(&self) -> HashMap<usize, HashSet<u32>> {
1621        let targeted = &mut *borrow_mut!(self.targeted_canvas_replay_node_ids);
1622        std::mem::take(targeted)
1623    }
1624
1625    pub fn mark_all_canvas_nodes_dirty(&self) {
1626        for ids in borrow_mut!(self.targeted_canvas_replay_node_ids).values_mut() {
1627            ids.clear();
1628        }
1629        let node_cache = borrow!(self.node_cache);
1630        let dirty_nodes = &mut *borrow_mut!(self.dirty_canvas_nodes);
1631        for node in node_cache.eid_to_node.values() {
1632            if !node.is_render_source()
1633                && borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas
1634            {
1635                dirty_nodes.insert(node.id);
1636            }
1637        }
1638    }
1639
1640    pub fn enqueue_canvas_node_removal(&self, layer: usize, node_id: u32) {
1641        borrow_mut!(self.removed_canvas_nodes).push((layer, node_id));
1642    }
1643
1644    pub fn take_canvas_node_removals(&self) -> Vec<(usize, u32)> {
1645        let mut removals = borrow_mut!(self.removed_canvas_nodes);
1646        std::mem::take(&mut *removals)
1647    }
1648
1649    /// Finds all ExpandedNodes with the CommonProperty#id matching the provided string
1650    pub fn get_expanded_nodes_by_id(&self, id: &str) -> Vec<Rc<ExpandedNode>> {
1651        //v0 limitation: currently an O(n) lookup cost (could be made O(1) with an id->expandednode cache)
1652        borrow!(self.node_cache)
1653            .eid_to_node
1654            .values()
1655            .filter(|val| {
1656                let common_props = val.get_common_properties();
1657                let common_props = borrow!(common_props);
1658                common_props.id.get().is_some_and(|i| i == id)
1659            })
1660            .cloned()
1661            .collect()
1662    }
1663
1664    /// Finds all ExpandedNodes with corresponding UniqueTemplateNodeIdentifier
1665    pub fn get_expanded_nodes_by_global_ids(
1666        &self,
1667        uni: &UniqueTemplateNodeIdentifier,
1668    ) -> Vec<Rc<ExpandedNode>> {
1669        let node_cache = borrow!(self.node_cache);
1670        node_cache
1671            .uni_to_eid
1672            .get(uni)
1673            .map(|eids| {
1674                let mut nodes = vec![];
1675                for e in eids {
1676                    if let Some(node) = node_cache.eid_to_node.get(e) {
1677                        nodes.push(Rc::clone(node));
1678                    } else {
1679                        log::warn!(
1680                            "failed to find node in engine for expanded node identifier: {:?}",
1681                            e
1682                        );
1683                    }
1684                }
1685                nodes
1686            })
1687            .unwrap_or_default()
1688    }
1689
1690    /// Simple 2D raycasting: the coordinates of the ray represent a
1691    /// ray running orthogonally to the view plane, intersecting at
1692    /// the specified point `ray`.  Areas outside of clipping bounds will
1693    /// not register a `hit`, nor will elements that suppress input events.
1694    pub fn get_elements_beneath_ray(
1695        &self,
1696        root: Option<Rc<ExpandedNode>>,
1697        ray: Point2<Window>,
1698        limit_one: bool,
1699        mut accum: Vec<Rc<ExpandedNode>>,
1700        hit_invisible: bool,
1701    ) -> Vec<Rc<ExpandedNode>> {
1702        //Traverse all elements in render tree sorted by z-index (highest-to-lowest)
1703        //First: check whether events are suppressed
1704        //Next: check whether ancestral clipping bounds (hit_test) are satisfied
1705        //Finally: check whether element itself satisfies hit_test(ray)
1706
1707        let Some(root_node) = root.or_else(|| borrow!(self.root_expanded_node).upgrade()) else {
1708            return accum;
1709        };
1710        let mut to_process = vec![(root_node, false, Affine::IDENTITY, false)];
1711        while let Some((node, clipped, active_scroll_transform, retained_for_exit)) =
1712            to_process.pop()
1713        {
1714            if node.is_render_source() {
1715                continue;
1716            }
1717            // make sure slot sources are updated for this node
1718            node.compute_flattened_projected_children();
1719            if !hit_invisible
1720                && (retained_for_exit || node.transition_phase.get() == TRANSITION_PHASE_EXIT)
1721            {
1722                continue;
1723            }
1724            // Browser-composited scrollers move descendants outside the engine transform tree.
1725            // Fold active scroll offsets into hit-testing so event rays line up with presented content.
1726            let (scroll_transform, clips_content) = {
1727                let instance_node = borrow!(node.instance_node);
1728                let scrolls_content = instance_node.scrolls_content(&node);
1729                let clips_content = instance_node.clips_content(&node);
1730                drop(instance_node);
1731                (
1732                    if scrolls_content {
1733                        self.scroller_content_presentation_transform(&node)
1734                    } else {
1735                        Affine::IDENTITY
1736                    },
1737                    clips_content,
1738                )
1739            };
1740            let descendant_scroll_transform = active_scroll_transform * scroll_transform;
1741            let hit = node.ray_cast_test(active_scroll_transform.inverse() * ray);
1742            if hit && !clipped {
1743                if hit_invisible
1744                    || !borrow!(node.instance_node)
1745                        .base()
1746                        .flags()
1747                        .invisible_to_raycasting
1748                {
1749                    //We only care about the topmost node getting hit, and the element
1750                    //pool is ordered by z-index so we can just resolve the whole
1751                    //calculation when we find the first matching node
1752                    if limit_one {
1753                        return vec![node];
1754                    }
1755                    accum.push(Rc::clone(&node));
1756                }
1757            }
1758            let clipped = clipped || (!hit && clips_content);
1759            let retained_children = borrow!(node.exiting_children).clone();
1760            to_process.extend(
1761                node.children
1762                    .get()
1763                    .iter()
1764                    .cloned()
1765                    .map(|v| {
1766                        let retained_for_exit = retained_children
1767                            .iter()
1768                            .any(|retained| Rc::ptr_eq(retained, &v));
1769                        let cp = v.get_common_properties();
1770                        let unclippable = borrow!(cp).unclippable.get().unwrap_or(false);
1771                        (
1772                            v,
1773                            clipped && !unclippable,
1774                            descendant_scroll_transform,
1775                            retained_for_exit,
1776                        )
1777                    })
1778                    .rev(),
1779            )
1780        }
1781        accum
1782    }
1783    /// Alias for `get_elements_beneath_ray` with `limit_one = true`
1784    pub fn get_topmost_element_beneath_ray(
1785        self: &Rc<Self>,
1786        ray: Point2<Window>,
1787    ) -> Option<Rc<ExpandedNode>> {
1788        let res = self.get_elements_beneath_ray(None, ray, true, vec![], false);
1789        let Some(new_topmost) = res
1790            .into_iter()
1791            .next()
1792            .or_else(|| borrow!(self.root_expanded_node).upgrade())
1793        else {
1794            *borrow_mut!(self.last_topmost_element) = Weak::new();
1795            return None;
1796        };
1797
1798        // Send mouse over/out events if the hit element is different than last.
1799        // Use template ancestry rather than containing-component ancestry so
1800        // wrappers around slotted content can own hover affordances.
1801        let last_topmost = borrow!(self.last_topmost_element).upgrade();
1802        if Some(new_topmost.id) != last_topmost.as_ref().map(|n| n.id) {
1803            let (leaving, entering) = find_template_paths_to_common_ancestor(
1804                &last_topmost,
1805                &Some(Rc::clone(&new_topmost)),
1806            );
1807            for leave in leaving {
1808                leave.dispatch_mouse_out(Event::new(MouseOut {}), &self.globals(), self);
1809            }
1810            for enter in entering {
1811                enter.dispatch_mouse_over(Event::new(MouseOver {}), &self.globals(), self);
1812            }
1813            *borrow_mut!(self.last_topmost_element) = Rc::downgrade(&new_topmost);
1814        }
1815        Some(new_topmost)
1816    }
1817
1818    pub fn gen_uid(&self) -> ExpandedNodeIdentifier {
1819        let val = self.next_uid.get();
1820        let next_val = ExpandedNodeIdentifier(val.0 + 1);
1821        self.next_uid.set(next_val);
1822        val
1823    }
1824
1825    pub fn enqueue_native_message(&self, message: NativeMessage) {
1826        borrow_mut!(self.messages).push(message)
1827    }
1828
1829    pub fn take_native_messages(&self) -> Vec<NativeMessage> {
1830        let mut messages = borrow_mut!(self.messages);
1831        std::mem::take(&mut *messages)
1832    }
1833
1834    pub fn globals(&self) -> Globals {
1835        borrow!(self.globals).clone()
1836    }
1837
1838    pub fn edit_globals(&self, f: impl Fn(&mut Globals)) {
1839        let mut globals = borrow_mut!(self.globals);
1840        f(&mut globals);
1841    }
1842
1843    pub fn queue_custom_event(&self, source_expanded_node: Rc<ExpandedNode>, name: &'static str) {
1844        let mut queued_custom_events = borrow_mut!(self.queued_custom_events);
1845        queued_custom_events.push((source_expanded_node, name));
1846    }
1847
1848    pub fn flush_custom_events(self: &Rc<Self>) -> Result<(), String> {
1849        let mut queued_custom_event = borrow_mut!(self.queued_custom_events);
1850        let to_flush: Vec<_> = std::mem::take(queued_custom_event.as_mut());
1851        for (target, ident) in to_flush {
1852            target.dispatch_custom_event(ident, self)?;
1853        }
1854        Ok(())
1855    }
1856
1857    #[cfg(feature = "designtime")]
1858    pub fn get_userland_root_expanded_node(&self) -> Option<Rc<ExpandedNode>> {
1859        borrow!(self.userland_root_expanded_node).clone()
1860    }
1861
1862    #[cfg(feature = "designtime")]
1863    pub fn get_userland_root_instance_node(&self) -> Option<Rc<dyn InstanceNode>> {
1864        borrow!(self.userland_frame_instance_node).clone()
1865    }
1866
1867    #[cfg(feature = "designtime")]
1868    pub fn set_userland_root_expanded_node(&self, root: Option<Rc<ExpandedNode>>) {
1869        *borrow_mut!(self.userland_root_expanded_node) = root;
1870    }
1871
1872    #[cfg(feature = "designtime")]
1873    pub fn set_userland_root_instance_node(&self, node: Option<Rc<dyn InstanceNode>>) {
1874        *borrow_mut!(self.userland_frame_instance_node) = node;
1875    }
1876
1877    pub fn get_root_expanded_node(&self) -> Option<Rc<ExpandedNode>> {
1878        borrow!(self.root_expanded_node).upgrade()
1879    }
1880
1881    pub fn queue_render(&self, expanded_node: Rc<ExpandedNode>) {
1882        borrow_mut!(self.queued_renders).push(expanded_node);
1883    }
1884
1885    pub fn recurse_flush_queued_renders(self: &Rc<RuntimeContext>, rcs: &mut dyn RenderContext) {
1886        while !borrow!(self.queued_renders).is_empty() {
1887            for n in std::mem::take(&mut *borrow_mut!(self.queued_renders)) {
1888                n.recurse_render(self, rcs);
1889            }
1890        }
1891    }
1892}
1893
1894fn find_template_paths_to_common_ancestor(
1895    last_topmost: &Option<Rc<ExpandedNode>>,
1896    new_topmost: &Option<Rc<ExpandedNode>>,
1897) -> (Vec<Rc<ExpandedNode>>, Vec<Rc<ExpandedNode>>) {
1898    let mut last_path = template_path_from_root(last_topmost);
1899    let mut new_path = template_path_from_root(new_topmost);
1900    let common_prefix_len = last_path
1901        .iter()
1902        .zip(new_path.iter())
1903        .take_while(|(last, new)| last.id == new.id)
1904        .count();
1905
1906    // Remove common ancestors from both paths.
1907    last_path.drain(0..common_prefix_len);
1908    new_path.drain(0..common_prefix_len);
1909
1910    (last_path, new_path)
1911}
1912
1913fn template_path_from_root(node: &Option<Rc<ExpandedNode>>) -> Vec<Rc<ExpandedNode>> {
1914    let mut path = Vec::new();
1915    let mut current = node.clone();
1916    while let Some(node) = current {
1917        path.push(Rc::clone(&node));
1918        current = node.template_parent.upgrade();
1919    }
1920    path.reverse();
1921    path
1922}
1923
1924/// Data structure for a single frame of our runtime stack, including
1925/// a reference to its parent frame and `properties` for
1926/// runtime evaluation, e.g. of Expressions.  `RuntimePropertiesStackFrame`s also track
1927/// timeline playhead position.
1928///
1929/// `Component`s push `RuntimePropertiesStackFrame`s before computing properties and pop them after computing, thus providing a
1930/// hierarchical store of node-relevant data that can be bound to symbols in expressions.
1931
1932pub struct RuntimePropertiesStackFrame {
1933    symbols_within_frame: HashMap<String, Variable>,
1934    parent: Option<Rc<RuntimePropertiesStackFrame>>,
1935}
1936
1937impl RuntimePropertiesStackFrame {
1938    pub fn new(symbols_within_frame: HashMap<String, Variable>) -> Rc<Self> {
1939        Rc::new(Self {
1940            symbols_within_frame,
1941            parent: None,
1942        })
1943    }
1944
1945    pub fn push(self: &Rc<Self>, symbols_within_frame: HashMap<String, Variable>) -> Rc<Self> {
1946        Rc::new(RuntimePropertiesStackFrame {
1947            symbols_within_frame,
1948            parent: Some(Rc::clone(self)),
1949        })
1950    }
1951
1952    pub fn pop(self: &Rc<Self>) -> Option<Rc<Self>> {
1953        self.parent.clone()
1954    }
1955
1956    pub fn resolve_symbol_as_variable(&self, symbol: &str) -> Option<Variable> {
1957        if let Some(e) = self.symbols_within_frame.get(&clean_symbol(symbol)) {
1958            Some(e.clone())
1959        } else {
1960            self.parent.as_ref()?.resolve_symbol_as_variable(symbol)
1961        }
1962    }
1963
1964    pub fn resolve_symbol_as_erased_property(&self, symbol: &str) -> Option<UntypedProperty> {
1965        if let Some(e) = self.symbols_within_frame.get(&clean_symbol(symbol)) {
1966            Some(e.clone().get_untyped_property().clone())
1967        } else {
1968            self.parent
1969                .as_ref()?
1970                .resolve_symbol_as_erased_property(symbol)
1971        }
1972    }
1973
1974    pub fn resolve_symbol(&self, symbol: &str) -> Option<Variable> {
1975        self.symbols_within_frame
1976            .get(&clean_symbol(symbol))
1977            .cloned()
1978            .or_else(|| self.parent.as_ref()?.resolve_symbol(symbol))
1979    }
1980}
1981
1982fn clean_symbol(symbol: &str) -> String {
1983    symbol.replace("self.", "").replace("this.", "")
1984}
1985
1986impl IdentifierResolver for RuntimePropertiesStackFrame {
1987    fn resolve(&self, name: String) -> Result<Variable, String> {
1988        self.resolve_symbol(&name)
1989            .ok_or_else(|| format!("Could not resolve symbol {}", name))
1990    }
1991}
1992
1993fn light_reaches_frame_ancestry(
1994    owner_frame: Option<&ExpandedNodeIdentifier>,
1995    frame_ancestry: &HashSet<ExpandedNodeIdentifier>,
1996) -> bool {
1997    owner_frame.is_none_or(|frame| frame_ancestry.contains(frame))
1998}
1999
2000/// Data structure used for dynamic injection of values
2001/// into Expressions, maintaining a pointer e.g. to the current
2002/// stack frame to enable evaluation of properties & dependencies
2003pub struct ExpressionContext {
2004    pub stack_frame: Rc<RuntimePropertiesStackFrame>,
2005}
2006
2007#[cfg(test)]
2008mod light_scope_tests {
2009    use super::*;
2010    use crate::api::math::Transform2;
2011    use crate::{
2012        BaseInstance, CommonPropertiesInit, ComponentInstance, InstanceFlags, InstanceNode,
2013        InstantiationArgs, PropertiesInit, PropertiesScopeInit, RouteLocation, TransformAndBounds,
2014    };
2015    use pax_runtime_api::pax_value::{PaxAny, PaxValue};
2016    use pax_runtime_api::{Layer, Platform, SceneAmbientLight, TargetInfo, OS};
2017    use std::fmt;
2018
2019    #[derive(Clone, Copy)]
2020    enum TestLightingRole {
2021        Frame,
2022        Light(f64),
2023        Ambient(f64),
2024        Surface,
2025    }
2026
2027    struct TestLightingNode {
2028        base: BaseInstance,
2029        role: TestLightingRole,
2030        enabled: Cell<bool>,
2031        intensity_multiplier: Cell<f64>,
2032        resolutions: Cell<usize>,
2033    }
2034
2035    impl TestLightingNode {
2036        fn new(
2037            role: TestLightingRole,
2038            children: Vec<Rc<dyn InstanceNode>>,
2039        ) -> Rc<TestLightingNode> {
2040            Rc::new(Self {
2041                base: BaseInstance::new(
2042                    node_args(children),
2043                    InstanceFlags {
2044                        invisible_to_slot: false,
2045                        invisible_to_raycasting: true,
2046                        layer: match role {
2047                            TestLightingRole::Frame => Layer::DontCare,
2048                            TestLightingRole::Light(_)
2049                            | TestLightingRole::Ambient(_)
2050                            | TestLightingRole::Surface => Layer::Canvas,
2051                        },
2052                        is_component: false,
2053                        is_slot: false,
2054                    },
2055                ),
2056                role,
2057                enabled: Cell::new(true),
2058                intensity_multiplier: Cell::new(1.0),
2059                resolutions: Cell::new(0),
2060            })
2061        }
2062
2063        fn as_instance(self: &Rc<Self>) -> Rc<dyn InstanceNode> {
2064            self.clone()
2065        }
2066
2067        fn set_enabled(&self, enabled: bool) {
2068            self.enabled.set(enabled);
2069        }
2070    }
2071
2072    impl InstanceNode for TestLightingNode {
2073        fn has_scene_lighting(&self) -> bool {
2074            matches!(
2075                self.role,
2076                TestLightingRole::Light(_) | TestLightingRole::Ambient(_)
2077            )
2078        }
2079
2080        fn instantiate(args: InstantiationArgs) -> Rc<Self>
2081        where
2082            Self: Sized,
2083        {
2084            Rc::new(Self {
2085                base: BaseInstance::new(
2086                    args,
2087                    InstanceFlags {
2088                        invisible_to_slot: false,
2089                        invisible_to_raycasting: true,
2090                        layer: Layer::Canvas,
2091                        is_component: false,
2092                        is_slot: false,
2093                    },
2094                ),
2095                role: TestLightingRole::Surface,
2096                enabled: Cell::new(true),
2097                intensity_multiplier: Cell::new(1.0),
2098                resolutions: Cell::new(0),
2099            })
2100        }
2101
2102        fn resolve_debug(
2103            &self,
2104            f: &mut fmt::Formatter,
2105            _expanded_node: Option<&ExpandedNode>,
2106        ) -> fmt::Result {
2107            f.debug_struct("TestLightingNode").finish()
2108        }
2109
2110        fn resolve_scene_light(
2111            &self,
2112            _expanded_node: &ExpandedNode,
2113            _context: &RuntimeContext,
2114        ) -> Option<SceneLight> {
2115            assert!(self.has_scene_lighting());
2116            self.resolutions.set(self.resolutions.get() + 1);
2117            match self.role {
2118                TestLightingRole::Light(intensity) if self.enabled.get() => Some(SceneLight {
2119                    intensity: intensity * self.intensity_multiplier.get(),
2120                    ..Default::default()
2121                }),
2122                _ => None,
2123            }
2124        }
2125
2126        fn resolve_scene_ambient_light(
2127            &self,
2128            _expanded_node: &ExpandedNode,
2129            _context: &RuntimeContext,
2130        ) -> Option<SceneAmbientLight> {
2131            assert!(self.has_scene_lighting());
2132            self.resolutions.set(self.resolutions.get() + 1);
2133            match self.role {
2134                TestLightingRole::Ambient(intensity) if self.enabled.get() => {
2135                    Some(SceneAmbientLight {
2136                        intensity,
2137                        ..Default::default()
2138                    })
2139                }
2140                _ => None,
2141            }
2142        }
2143
2144        fn establishes_light_frame(&self) -> bool {
2145            matches!(self.role, TestLightingRole::Frame)
2146        }
2147
2148        fn base(&self) -> &BaseInstance {
2149            &self.base
2150        }
2151    }
2152
2153    struct ScopedLightingFixture {
2154        context: Rc<RuntimeContext>,
2155        _root: Rc<ExpandedNode>,
2156        root_light: Rc<TestLightingNode>,
2157        outer_light: Rc<TestLightingNode>,
2158        ambient: Rc<TestLightingNode>,
2159        root_surface: Rc<ExpandedNode>,
2160        outer_surface: Rc<ExpandedNode>,
2161        nested_surface: Rc<ExpandedNode>,
2162        sibling_surface: Rc<ExpandedNode>,
2163        sibling_frame: Rc<ExpandedNode>,
2164    }
2165
2166    fn test_globals() -> Globals {
2167        Globals {
2168            elapsed_frames: LocalProperty::new(0),
2169            elapsed_millis: LocalProperty::new(0),
2170            viewport: LocalProperty::new(TransformAndBounds {
2171                transform: Transform2::identity(),
2172                bounds: (100.0, 100.0),
2173            }),
2174            gyro: LocalProperty::new(Default::default()),
2175            accel: LocalProperty::new(Default::default()),
2176            route_location: LocalProperty::new(RouteLocation::root()),
2177            browser_allows_scroller_vector_layers: LocalProperty::new(true),
2178            browser_allows_nested_scroller_vector_layers: LocalProperty::new(true),
2179            platform: Platform::Unknown,
2180            os: OS::Unknown,
2181            target: TargetInfo::new(Platform::Unknown, OS::Unknown),
2182            #[cfg(feature = "designtime")]
2183            designtime: crate::test_support::designtime(),
2184            get_elapsed_millis: Rc::new(|| 0),
2185        }
2186    }
2187
2188    fn properties_factory() -> crate::PropertiesFactory {
2189        Box::new(|_, expanded_node| {
2190            expanded_node
2191                .is_none()
2192                .then(|| Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
2193        })
2194    }
2195
2196    fn node_args(children: Vec<Rc<dyn InstanceNode>>) -> InstantiationArgs {
2197        InstantiationArgs {
2198            prototypical_common_properties: CommonPropertiesInit::Default,
2199            prototypical_properties: PropertiesInit::Factory(properties_factory()),
2200            handler_registry: None,
2201            children: Some(RefCell::new(children)),
2202            component_template: None,
2203            component_settings: None,
2204            template_node_identifier: None,
2205            template_node_type_id: None,
2206            template_node_selector_info: None,
2207            transition_config: Default::default(),
2208            properties_scope: PropertiesScopeInit::None,
2209        }
2210    }
2211
2212    fn component_args(template: Vec<Rc<dyn InstanceNode>>) -> InstantiationArgs {
2213        InstantiationArgs {
2214            prototypical_common_properties: CommonPropertiesInit::Default,
2215            prototypical_properties: PropertiesInit::Factory(properties_factory()),
2216            handler_registry: None,
2217            children: None,
2218            component_template: Some(RefCell::new(template)),
2219            component_settings: None,
2220            template_node_identifier: None,
2221            template_node_type_id: None,
2222            template_node_selector_info: None,
2223            transition_config: Default::default(),
2224            properties_scope: PropertiesScopeInit::None,
2225        }
2226    }
2227
2228    fn mount_test_tree(
2229        children: Vec<Rc<dyn InstanceNode>>,
2230    ) -> (Rc<RuntimeContext>, Rc<ExpandedNode>) {
2231        let root_component = ComponentInstance::instantiate(component_args(children));
2232        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
2233        let root = ExpandedNode::initialize_root(root_component, &context);
2234        root.recurse_update(&context);
2235        crate::engine::occlusion::update_node_occlusion(&root, &context);
2236        (context, root)
2237    }
2238
2239    fn clear_dirty_canvas_nodes(context: &RuntimeContext) {
2240        for id in context.dirty_canvas_node_ids() {
2241            context.clear_canvas_node_dirty(&id);
2242        }
2243    }
2244
2245    fn scoped_lighting_fixture() -> ScopedLightingFixture {
2246        let nested_light = TestLightingNode::new(TestLightingRole::Light(3.0), Vec::new());
2247        let nested_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2248        let nested_frame = TestLightingNode::new(
2249            TestLightingRole::Frame,
2250            vec![nested_light.as_instance(), nested_surface.as_instance()],
2251        );
2252
2253        let sibling_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2254        let sibling_frame =
2255            TestLightingNode::new(TestLightingRole::Frame, vec![sibling_surface.as_instance()]);
2256
2257        let outer_light = TestLightingNode::new(TestLightingRole::Light(2.0), Vec::new());
2258        let outer_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2259        let outer_frame = TestLightingNode::new(
2260            TestLightingRole::Frame,
2261            vec![
2262                outer_light.as_instance(),
2263                outer_surface.as_instance(),
2264                nested_frame.as_instance(),
2265                sibling_frame.as_instance(),
2266            ],
2267        );
2268
2269        let root_light = TestLightingNode::new(TestLightingRole::Light(1.0), Vec::new());
2270        let root_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2271        let ambient = TestLightingNode::new(TestLightingRole::Ambient(0.72), Vec::new());
2272        ambient.set_enabled(false);
2273
2274        let (context, root) = mount_test_tree(vec![
2275            root_light.as_instance(),
2276            root_surface.as_instance(),
2277            outer_frame.as_instance(),
2278            ambient.as_instance(),
2279        ]);
2280
2281        let root_children = root.children.get();
2282        let root_surface_node = root_children[1].clone();
2283        let outer_frame_node = root_children[2].clone();
2284        let outer_children = outer_frame_node.children.get();
2285        let outer_surface_node = outer_children[1].clone();
2286        let nested_frame_node = outer_children[2].clone();
2287        let sibling_frame_node = outer_children[3].clone();
2288        let nested_children = nested_frame_node.children.get();
2289        let nested_surface_node = nested_children[1].clone();
2290        let sibling_surface_node = sibling_frame_node.children.get()[0].clone();
2291
2292        ScopedLightingFixture {
2293            context,
2294            _root: root,
2295            root_light,
2296            outer_light,
2297            ambient,
2298            root_surface: root_surface_node,
2299            outer_surface: outer_surface_node,
2300            nested_surface: nested_surface_node,
2301            sibling_surface: sibling_surface_node,
2302            sibling_frame: sibling_frame_node,
2303        }
2304    }
2305
2306    #[test]
2307    fn root_lights_reach_every_frame_ancestry() {
2308        let ancestry = HashSet::from([ExpandedNodeIdentifier(10), ExpandedNodeIdentifier(20)]);
2309        assert!(light_reaches_frame_ancestry(None, &ancestry));
2310    }
2311
2312    #[test]
2313    fn framed_lights_reach_only_descendants_of_their_owner_frame() {
2314        let outer_frame = ExpandedNodeIdentifier(10);
2315        let nested_frame = ExpandedNodeIdentifier(20);
2316        let sibling_frame = ExpandedNodeIdentifier(30);
2317        let nested_ancestry = HashSet::from([outer_frame, nested_frame]);
2318
2319        assert!(light_reaches_frame_ancestry(
2320            Some(&outer_frame),
2321            &nested_ancestry
2322        ));
2323        assert!(light_reaches_frame_ancestry(
2324            Some(&nested_frame),
2325            &nested_ancestry
2326        ));
2327        assert!(!light_reaches_frame_ancestry(
2328            Some(&sibling_frame),
2329            &nested_ancestry
2330        ));
2331    }
2332
2333    #[test]
2334    fn mounted_tree_resolves_root_nested_and_sibling_light_masks() {
2335        let fixture = scoped_lighting_fixture();
2336
2337        let lighting = fixture.context.collect_scene_lighting_for_layer(0);
2338
2339        assert_eq!(
2340            lighting
2341                .lights
2342                .iter()
2343                .map(|light| light.intensity)
2344                .collect::<Vec<_>>(),
2345            vec![1.0, 2.0, 3.0]
2346        );
2347        assert!(!lighting.ambient_is_authored);
2348        assert_eq!(
2349            fixture
2350                .context
2351                .canvas_node_light_mask(fixture.root_surface.id),
2352            0b001
2353        );
2354        assert_eq!(
2355            fixture
2356                .context
2357                .canvas_node_light_mask(fixture.outer_surface.id),
2358            0b011
2359        );
2360        assert_eq!(
2361            fixture
2362                .context
2363                .canvas_node_light_mask(fixture.nested_surface.id),
2364            0b111
2365        );
2366        assert_eq!(
2367            fixture
2368                .context
2369                .canvas_node_light_mask(fixture.sibling_surface.id),
2370            0b011
2371        );
2372    }
2373
2374    #[test]
2375    fn unlit_scene_does_not_resolve_nonproviders_or_store_zero_masks() {
2376        let surfaces = (0..1000)
2377            .map(|_| TestLightingNode::new(TestLightingRole::Surface, Vec::new()))
2378            .collect::<Vec<_>>();
2379        let (context, _root) =
2380            mount_test_tree(surfaces.iter().map(TestLightingNode::as_instance).collect());
2381        clear_dirty_canvas_nodes(&context);
2382        for _ in 0..100 {
2383            let lighting = context.collect_scene_lighting_for_layer(0);
2384            assert!(lighting.lights.is_empty());
2385            assert!(!lighting.ambient_is_authored);
2386            assert_eq!(
2387                lighting.ambient.intensity,
2388                SceneLighting::DEFAULT_AMBIENT_INTENSITY
2389            );
2390        }
2391        assert!(surfaces
2392            .iter()
2393            .all(|surface| surface.resolutions.get() == 0));
2394        assert!(borrow!(context.canvas_node_light_masks).is_empty());
2395        assert!(context.dirty_canvas_node_ids().is_empty());
2396    }
2397
2398    #[test]
2399    fn disabled_provider_enable_remove_and_remount_refresh_memberships() {
2400        let light = TestLightingNode::new(TestLightingRole::Light(2.0), Vec::new());
2401        light.set_enabled(false);
2402        let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2403        let (context, root) = mount_test_tree(vec![light.as_instance(), surface.as_instance()]);
2404        let children = root.children.get();
2405        let light_node = children[0].clone();
2406        let surface_node = &children[1];
2407        assert!(context
2408            .collect_scene_lighting_for_layer(0)
2409            .lights
2410            .is_empty());
2411        light.set_enabled(true);
2412        assert_eq!(context.collect_scene_lighting_for_layer(0).lights.len(), 1);
2413        assert_eq!(context.canvas_node_light_mask(surface_node.id), 1);
2414        clear_dirty_canvas_nodes(&context);
2415        light_node.clone().recurse_unmount(&context);
2416        assert!(context
2417            .collect_scene_lighting_for_layer(0)
2418            .lights
2419            .is_empty());
2420        assert_eq!(context.canvas_node_light_mask(surface_node.id), 0);
2421        assert!(context.is_canvas_node_dirty(&surface_node.id));
2422        assert!(borrow!(context.scene_lighting_nodes).is_empty());
2423        assert!(borrow!(context.canvas_node_light_masks).is_empty());
2424        light_node.recurse_mount(&context);
2425        assert_eq!(context.collect_scene_lighting_for_layer(0).lights.len(), 1);
2426        assert_eq!(context.canvas_node_light_mask(surface_node.id), 1);
2427    }
2428
2429    #[test]
2430    fn template_replacement_refreshes_provider_capability_without_remount() {
2431        let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2432        let (context, root) = mount_test_tree(vec![surface.as_instance()]);
2433        let node = root.children.get()[0].clone();
2434        assert!(context
2435            .collect_scene_lighting_for_layer(0)
2436            .lights
2437            .is_empty());
2438        let light = TestLightingNode::new(TestLightingRole::Light(3.0), Vec::new());
2439        node.recreate_with_new_data(light.as_instance(), &context);
2440        assert_eq!(
2441            context.collect_scene_lighting_for_layer(0).lights[0].intensity,
2442            3.0
2443        );
2444        node.recreate_with_new_data(surface.as_instance(), &context);
2445        assert!(context
2446            .collect_scene_lighting_for_layer(0)
2447            .lights
2448            .is_empty());
2449        assert_eq!(context.canvas_node_light_mask(node.id), 0);
2450        assert!(borrow!(context.scene_lighting_nodes).is_empty());
2451    }
2452
2453    #[test]
2454    fn slot_reallocation_and_render_reparenting_refresh_retained_masks() {
2455        let fixture = scoped_lighting_fixture();
2456        fixture.context.collect_scene_lighting_for_layer(0);
2457        clear_dirty_canvas_nodes(&fixture.context);
2458
2459        fixture.outer_light.set_enabled(false);
2460        let lighting = fixture.context.collect_scene_lighting_for_layer(0);
2461
2462        assert_eq!(
2463            lighting
2464                .lights
2465                .iter()
2466                .map(|light| light.intensity)
2467                .collect::<Vec<_>>(),
2468            vec![1.0, 3.0]
2469        );
2470        assert_eq!(
2471            fixture
2472                .context
2473                .canvas_node_light_mask(fixture.root_surface.id),
2474            0b01
2475        );
2476        assert_eq!(
2477            fixture
2478                .context
2479                .canvas_node_light_mask(fixture.outer_surface.id),
2480            0b01
2481        );
2482        assert_eq!(
2483            fixture
2484                .context
2485                .canvas_node_light_mask(fixture.nested_surface.id),
2486            0b11
2487        );
2488        assert!(fixture
2489            .context
2490            .is_canvas_node_dirty(&fixture.outer_surface.id));
2491        assert!(fixture
2492            .context
2493            .is_canvas_node_dirty(&fixture.nested_surface.id));
2494        assert!(!fixture
2495            .context
2496            .is_canvas_node_dirty(&fixture.root_surface.id));
2497
2498        clear_dirty_canvas_nodes(&fixture.context);
2499        *borrow_mut!(fixture.nested_surface.render_parent) = Rc::downgrade(&fixture.sibling_frame);
2500        fixture.context.mark_occlusion_dirty();
2501        fixture.context.collect_scene_lighting_for_layer(0);
2502
2503        assert_eq!(
2504            fixture
2505                .context
2506                .canvas_node_light_mask(fixture.nested_surface.id),
2507            0b01
2508        );
2509        assert!(fixture
2510            .context
2511            .is_canvas_node_dirty(&fixture.nested_surface.id));
2512    }
2513
2514    #[test]
2515    fn default_and_authored_ambient_keep_empty_direct_masks_distinct() {
2516        let fixture = scoped_lighting_fixture();
2517        fixture.root_light.set_enabled(false);
2518
2519        let default_ambient = fixture.context.collect_scene_lighting_for_layer(0);
2520        assert!(default_ambient.active);
2521        assert!(!default_ambient.ambient_is_authored);
2522        assert_eq!(
2523            fixture
2524                .context
2525                .canvas_node_light_mask(fixture.root_surface.id),
2526            0
2527        );
2528        assert_eq!(
2529            default_ambient.ambient.intensity,
2530            SceneLighting::DEFAULT_AMBIENT_INTENSITY
2531        );
2532
2533        fixture.ambient.set_enabled(true);
2534        let authored_ambient = fixture.context.collect_scene_lighting_for_layer(0);
2535        assert!(authored_ambient.active);
2536        assert!(authored_ambient.ambient_is_authored);
2537        assert_eq!(authored_ambient.ambient.intensity, 0.72);
2538        assert_eq!(
2539            fixture
2540                .context
2541                .canvas_node_light_mask(fixture.root_surface.id),
2542            0
2543        );
2544    }
2545
2546    #[test]
2547    fn light_overflow_selection_is_deterministic_and_recovers() {
2548        let lights = (0..10)
2549            .map(|index| {
2550                TestLightingNode::new(TestLightingRole::Light((index + 1) as f64), Vec::new())
2551            })
2552            .collect::<Vec<_>>();
2553        let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2554        let mut templates = lights
2555            .iter()
2556            .rev()
2557            .map(TestLightingNode::as_instance)
2558            .collect::<Vec<_>>();
2559        templates.push(surface.as_instance());
2560        let (context, root) = mount_test_tree(templates);
2561        let expanded = root.children.get();
2562        let surface_node = expanded[10].clone();
2563
2564        let first = context.collect_scene_lighting_for_layer(0);
2565        let second = context.collect_scene_lighting_for_layer(0);
2566        let expected = vec![10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0];
2567        assert_eq!(
2568            first
2569                .lights
2570                .iter()
2571                .map(|light| light.intensity)
2572                .collect::<Vec<_>>(),
2573            expected
2574        );
2575        assert_eq!(first.lights, second.lights);
2576        assert_eq!(context.canvas_node_light_mask(surface_node.id), 0xff);
2577        assert_eq!(borrow!(context.lighting_overflow_counts).get(&0), Some(&2));
2578
2579        lights[0].set_enabled(false);
2580        lights[1].set_enabled(false);
2581        let recovered = context.collect_scene_lighting_for_layer(0);
2582        assert_eq!(
2583            recovered
2584                .lights
2585                .iter()
2586                .map(|light| light.intensity)
2587                .collect::<Vec<_>>(),
2588            expected
2589        );
2590        assert!(!borrow!(context.lighting_overflow_counts).contains_key(&0));
2591    }
2592
2593    #[test]
2594    fn unlit_scene_does_no_lighting_scan_regardless_of_scene_size() {
2595        let surfaces = (0..1024)
2596            .map(|_| TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance())
2597            .collect();
2598        let (context, _root) = mount_test_tree(surfaces);
2599        clear_dirty_canvas_nodes(&context);
2600        for _ in 0..20 {
2601            context.set_canvas_dirty(0);
2602            let lighting = context.collect_scene_lighting_for_layer(0);
2603            assert!(lighting.lights.is_empty());
2604            assert!(!lighting.ambient_is_authored);
2605        }
2606        assert_eq!(context.lighting_mask_node_visits.get(), 0);
2607        assert!(borrow!(context.canvas_node_light_masks).is_empty());
2608        assert!(context.dirty_canvas_node_ids().is_empty());
2609    }
2610
2611    #[test]
2612    fn lighting_masks_survive_geometry_and_value_changes_but_not_scope_changes() {
2613        let fixture = scoped_lighting_fixture();
2614        fixture.context.collect_scene_lighting_for_layer(0);
2615        clear_dirty_canvas_nodes(&fixture.context);
2616        let visits = fixture.context.lighting_mask_node_visits.get();
2617        fixture.root_light.intensity_multiplier.set(4.0);
2618        fixture
2619            .context
2620            .mark_node_occlusion_dirty(fixture.outer_surface.id.to_u32());
2621        let lighting = fixture.context.collect_scene_lighting_for_layer(0);
2622        assert_eq!(lighting.lights[0].intensity, 4.0);
2623        assert_eq!(fixture.context.lighting_mask_node_visits.get(), visits);
2624        assert!(fixture.context.dirty_canvas_node_ids().is_empty());
2625
2626        // Move the root light into a sibling frame: slot identity is stable but
2627        // its lexical reach changes. Production reparenting invalidates structure.
2628        let root_light = fixture._root.children.get()[0].clone();
2629        *borrow_mut!(root_light.render_parent) = Rc::downgrade(&fixture.sibling_frame);
2630        fixture.context.mark_occlusion_dirty();
2631        fixture.context.collect_scene_lighting_for_layer(0);
2632        assert_eq!(
2633            fixture
2634                .context
2635                .canvas_node_light_mask(fixture.root_surface.id),
2636            0
2637        );
2638        assert_eq!(
2639            fixture
2640                .context
2641                .canvas_node_light_mask(fixture.sibling_surface.id),
2642            0b011
2643        );
2644        assert!(fixture
2645            .context
2646            .is_canvas_node_dirty(&fixture.root_surface.id));
2647        assert!(fixture.context.lighting_mask_node_visits.get() > visits);
2648    }
2649
2650    #[test]
2651    fn disabled_light_stays_registered_and_last_light_removal_clears_masks() {
2652        let light = TestLightingNode::new(TestLightingRole::Light(2.0), Vec::new());
2653        light.set_enabled(false);
2654        let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2655        let (context, root) = mount_test_tree(vec![light.as_instance(), surface.as_instance()]);
2656        let nodes = root.children.get();
2657        assert!(context
2658            .collect_scene_lighting_for_layer(0)
2659            .lights
2660            .is_empty());
2661        assert_eq!(context.lighting_mask_node_visits.get(), 0);
2662        light.set_enabled(true);
2663        assert_eq!(context.collect_scene_lighting_for_layer(0).lights.len(), 1);
2664        assert_eq!(context.canvas_node_light_mask(nodes[1].id), 1);
2665        clear_dirty_canvas_nodes(&context);
2666        context.clear_all_dirty_canvases();
2667        nodes[0].clone().recurse_unmount(&context);
2668        assert!(context.is_canvas_dirty(&0));
2669        assert!(context
2670            .collect_scene_lighting_for_layer(0)
2671            .lights
2672            .is_empty());
2673        assert_eq!(context.canvas_node_light_mask(nodes[1].id), 0);
2674        assert!(context.is_canvas_node_dirty(&nodes[1].id));
2675        assert!(borrow!(context.scene_lighting_nodes).is_empty());
2676        let visits = context.lighting_mask_node_visits.get();
2677        context.collect_scene_lighting_for_layer(0);
2678        assert_eq!(context.lighting_mask_node_visits.get(), visits);
2679    }
2680
2681    #[test]
2682    fn ambient_only_scene_never_builds_direct_masks() {
2683        let ambient = TestLightingNode::new(TestLightingRole::Ambient(0.72), Vec::new());
2684        let (context, _root) = mount_test_tree(vec![
2685            ambient.as_instance(),
2686            TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance(),
2687        ]);
2688        assert!(
2689            context
2690                .collect_scene_lighting_for_layer(0)
2691                .ambient_is_authored
2692        );
2693        ambient.set_enabled(false);
2694        assert!(
2695            !context
2696                .collect_scene_lighting_for_layer(0)
2697                .ambient_is_authored
2698        );
2699        ambient.set_enabled(true);
2700        assert_eq!(
2701            context
2702                .collect_scene_lighting_for_layer(0)
2703                .ambient
2704                .intensity,
2705            0.72
2706        );
2707        assert_eq!(context.lighting_mask_node_visits.get(), 0);
2708    }
2709
2710    #[test]
2711    fn replacement_refreshes_lighting_capability_in_both_directions() {
2712        let (context, root) = mount_test_tree(vec![TestLightingNode::new(
2713            TestLightingRole::Surface,
2714            Vec::new(),
2715        )
2716        .as_instance()]);
2717        let node = root.children.get()[0].clone();
2718        context.collect_scene_lighting_for_layer(0);
2719        node.recreate_with_new_data(
2720            TestLightingNode::new(TestLightingRole::Light(5.0), Vec::new()).as_instance(),
2721            &context,
2722        );
2723        assert_eq!(
2724            context.collect_scene_lighting_for_layer(0).lights[0].intensity,
2725            5.0
2726        );
2727        node.recreate_with_new_data(
2728            TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance(),
2729            &context,
2730        );
2731        assert!(context
2732            .collect_scene_lighting_for_layer(0)
2733            .lights
2734            .is_empty());
2735        assert_eq!(context.canvas_node_light_mask(node.id), 0);
2736    }
2737
2738    #[test]
2739    fn ancestor_lights_follow_scroll_without_rescanning_and_clear_after_layer_move() {
2740        let light = TestLightingNode::new(TestLightingRole::Light(1.0), Vec::new());
2741        let (context, root) = mount_test_tree(vec![
2742            light.as_instance(),
2743            TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance(),
2744            TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance(),
2745        ]);
2746        let nodes = root.children.get();
2747        let surface = &nodes[2];
2748        let mut occlusion = surface.occlusion.get();
2749        occlusion.render_layer_id = 1;
2750        surface.occlusion.set(occlusion);
2751        context.register_layer_scroller_owner(1, nodes[1].id);
2752        context.set_scroller_surface_state(
2753            nodes[1].id.to_u32(),
2754            ScrollerSurfaceState {
2755                viewport_width: 100.0,
2756                viewport_height: 100.0,
2757                content_width: 100.0,
2758                content_height: 1000.0,
2759                clip_content: true,
2760                ..Default::default()
2761            },
2762        );
2763        let before = context.collect_scene_lighting_for_layer(1);
2764        let visits = context.lighting_mask_node_visits.get();
2765        context.set_scroller_surface_state(
2766            nodes[1].id.to_u32(),
2767            ScrollerSurfaceState {
2768                scroll_y: 300.0,
2769                presentation_scroll_y: 300.0,
2770                ..context
2771                    .get_scroller_surface_state(nodes[1].id.to_u32())
2772                    .unwrap()
2773            },
2774        );
2775        let after = context.collect_scene_lighting_for_layer(1);
2776        assert_eq!(
2777            after.lights[0].position.y - before.lights[0].position.y,
2778            300.0
2779        );
2780        assert_eq!(context.canvas_node_light_mask(surface.id), 1);
2781        assert_eq!(context.lighting_mask_node_visits.get(), visits);
2782
2783        // Move the recipient into an unrelated layer. It must lose its old mask
2784        // even if the previous layer is collected before the new one.
2785        occlusion.render_layer_id = 2;
2786        surface.occlusion.set(occlusion);
2787        context.mark_occlusion_dirty();
2788        context.collect_scene_lighting_for_layer(1);
2789        assert!(context
2790            .collect_scene_lighting_for_layer(2)
2791            .lights
2792            .is_empty());
2793        assert_eq!(context.canvas_node_light_mask(surface.id), 0);
2794        context.clear_all_dirty_canvases();
2795        nodes[0].clone().recurse_unmount(&context);
2796        assert!(context.is_canvas_dirty(&1));
2797    }
2798
2799    #[test]
2800    fn detached_source_changes_preserve_incremental_scene_reconciliation() {
2801        let (context, root) = mount_test_tree(vec![TestLightingNode::new(
2802            TestLightingRole::Surface,
2803            Vec::new(),
2804        )
2805        .as_instance()]);
2806        let owner = root.children.get()[0].clone();
2807        let sources = owner.create_children_detached(
2808            vec![(
2809                TestLightingNode::new(TestLightingRole::Surface, Vec::new()).as_instance(),
2810                owner.stack.clone(),
2811            )],
2812            &context,
2813            &Rc::downgrade(&owner),
2814        );
2815        let source = owner
2816            .attach_sidecar_children(sources, &context, &owner.parent_frame)
2817            .remove(0);
2818        source.mount_as_render_source(&owner, &context);
2819        root.recurse_update(&context);
2820        crate::engine::occlusion::update_node_occlusion(&root, &context);
2821        let before = context.occlusion_stats();
2822        source
2823            .get_common_properties()
2824            .borrow()
2825            .x
2826            .set(Some(pax_runtime_api::Size::Pixels(15.0.into())));
2827        root.recurse_update(&context);
2828        crate::engine::occlusion::update_node_occlusion(&root, &context);
2829        let after = context.occlusion_stats();
2830        assert_eq!(after.rebuilds, before.rebuilds);
2831        assert!(after.records_updated > before.records_updated);
2832        root.recurse_unmount(&context);
2833    }
2834
2835    #[test]
2836    fn canvas_surface_transform_uses_the_layer_owner_not_intermediate_frames() {
2837        let leaf = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2838        let mask = TestLightingNode::new(TestLightingRole::Frame, vec![leaf.as_instance()]);
2839        let inner = TestLightingNode::new(TestLightingRole::Frame, vec![mask.as_instance()]);
2840        let outer = TestLightingNode::new(TestLightingRole::Frame, vec![inner.as_instance()]);
2841        let (context, root) = mount_test_tree(vec![outer.as_instance()]);
2842        let outer = root.children.get()[0].clone();
2843        let inner = outer.children.get()[0].clone();
2844        let mask = inner.children.get()[0].clone();
2845        let leaf = mask.children.get()[0].clone();
2846
2847        let outer_transform = Affine::translate((40.0, 70.0)) * Affine::scale(1.5);
2848        let inner_local = Affine::translate((25.0, 35.0)) * Affine::rotate(0.3);
2849        let inner_transform = outer_transform * inner_local;
2850        let mask_transform = inner_transform * Affine::translate((60.0, 90.0));
2851        let leaf_local = Affine::translate((8.0, 12.0));
2852        let leaf_transform = mask_transform * leaf_local;
2853        for (node, transform, layer, parent_frame) in [
2854            (&outer, outer_transform, 0, None),
2855            (&inner, inner_transform, 1, Some(outer.id)),
2856            // Like a Mask in a Scroller, this structural frame stays on layer zero.
2857            (&mask, mask_transform, 0, Some(inner.id)),
2858            (&leaf, leaf_transform, 2, Some(mask.id)),
2859        ] {
2860            node.transform_and_bounds
2861                .replace_with(LocalProperty::new(TransformAndBounds {
2862                    transform: Transform2::new(transform.as_coeffs()),
2863                    bounds: (100.0, 100.0),
2864                }));
2865            let mut occlusion = node.occlusion.get();
2866            occlusion.render_layer_id = layer;
2867            node.occlusion.set(occlusion);
2868            node.parent_frame
2869                .replace_with(LocalProperty::new(parent_frame));
2870        }
2871        context.register_layer_scroller_owner(1, outer.id);
2872        context.register_layer_scroller_owner(2, inner.id);
2873
2874        let assert_transform = |actual: Affine, expected: Affine| {
2875            for (actual, expected) in actual.as_coeffs().into_iter().zip(expected.as_coeffs()) {
2876                assert!((actual - expected).abs() < 1e-9, "{actual} != {expected}");
2877            }
2878        };
2879        assert_transform(
2880            context.canvas_surface_transform_for_node(&leaf),
2881            Affine::translate((60.0, 90.0)) * leaf_local,
2882        );
2883        assert_transform(context.layer_to_root_transform(2), inner_transform);
2884
2885        // A root canvas may also contain structural frames from another layer.
2886        // Without a surface owner its coordinates must remain in window space.
2887        context.clear_layer_scroller_owners();
2888        assert_transform(
2889            context.canvas_surface_transform_for_node(&leaf),
2890            leaf_transform,
2891        );
2892    }
2893
2894    #[test]
2895    fn touch_capture_releases_and_clears_removed_targets() {
2896        let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2897        let (context, root) = mount_test_tree(vec![surface.as_instance()]);
2898        let surface_node = root.children.get()[0].clone();
2899
2900        context.capture_touch_target(41, surface_node.id);
2901        assert!(Rc::ptr_eq(
2902            &context.captured_touch_target(41).unwrap(),
2903            &surface_node
2904        ));
2905        assert!(Rc::ptr_eq(
2906            &context.release_touch_target(41).unwrap(),
2907            &surface_node
2908        ));
2909        assert!(context.captured_touch_target(41).is_none());
2910
2911        context.capture_touch_target(42, surface_node.id);
2912        context.remove_from_cache(&surface_node);
2913        assert!(context.captured_touch_target(42).is_none());
2914        assert!(context.release_touch_target(42).is_none());
2915    }
2916}