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, 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, MouseOut, MouseOver,
15    Property, RenderContext, SceneLight, SceneLighting, Store, Variable,
16};
17use_RefCell!();
18use kurbo::{Affine, Point};
19use std::any::{Any, TypeId};
20use std::cell::Cell;
21use std::collections::{HashMap, HashSet};
22use std::hash::Hash;
23use std::rc::{Rc, Weak};
24use std::sync::OnceLock;
25use std::time::Instant;
26
27use crate::{ExpandedNode, Globals};
28
29#[cfg(feature = "designtime")]
30use crate::{ComponentInstance, InstanceNode};
31
32fn effect_drain_instrumentation_enabled() -> bool {
33    static ENABLED: OnceLock<bool> = OnceLock::new();
34    *ENABLED.get_or_init(|| {
35        std::env::var("PAX_EFFECT_DRAIN_INSTRUMENTATION")
36            .map(|value| {
37                let value = value.to_ascii_lowercase();
38                matches!(value.as_str(), "1" | "true" | "yes" | "on")
39            })
40            .unwrap_or(false)
41    })
42}
43
44fn format_effect_top(top_effects: &[(String, usize)]) -> String {
45    top_effects
46        .iter()
47        .map(|(name, count)| format!("{count}x {name}"))
48        .collect::<Vec<_>>()
49        .join("; ")
50}
51
52fn summarize_debug_names(names: Vec<String>) -> Vec<(String, usize)> {
53    let mut counts = HashMap::new();
54    for name in names {
55        let category = name
56            .split_once(" (")
57            .map(|(category, _)| category)
58            .unwrap_or(&name)
59            .to_owned();
60        *counts.entry(category).or_insert(0) += 1;
61    }
62    let mut counts = counts.into_iter().collect::<Vec<_>>();
63    counts.sort_by(|(left_name, left_count), (right_name, right_count)| {
64        right_count
65            .cmp(left_count)
66            .then_with(|| left_name.cmp(right_name))
67    });
68    counts
69}
70
71impl Interpolatable for ExpandedNodeIdentifier {}
72
73fn transform_scene_light_for_layer(
74    mut light: SceneLight,
75    source_to_root: Affine,
76    root_to_target: Affine,
77) -> SceneLight {
78    if matches!(light.shape, LightShape::Point) {
79        let point = Point::new(light.position.x, light.position.y);
80        let point = root_to_target * (source_to_root * point);
81        light.position.x = point.x;
82        light.position.y = point.y;
83    }
84    light
85}
86
87#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
88/// Stable runtime identifier assigned to an expanded node.
89pub struct ExpandedNodeIdentifier(pub u32);
90
91impl ExpandedNodeIdentifier {
92    /// Convert to the integer id passed across chassis message boundaries.
93    pub fn to_u32(&self) -> u32 {
94        self.0
95    }
96}
97
98/// Shared context for properties pass recursion
99pub struct RuntimeContext {
100    next_uid: Cell<ExpandedNodeIdentifier>,
101    messages: RefCell<Vec<NativeMessage>>,
102    globals: RefCell<Globals>,
103    root_expanded_node: RefCell<Weak<ExpandedNode>>,
104    #[cfg(feature = "designtime")]
105    pub userland_frame_instance_node: RefCell<Option<Rc<dyn InstanceNode>>>,
106    #[cfg(feature = "designtime")]
107    pub userland_root_expanded_node: RefCell<Option<Rc<ExpandedNode>>>,
108    node_cache: RefCell<NodeCache>,
109    last_topmost_element: RefCell<Weak<ExpandedNode>>,
110    active_touch_targets: RefCell<HashMap<i64, ExpandedNodeIdentifier>>,
111    queued_custom_events: RefCell<Vec<(Rc<ExpandedNode>, &'static str)>>,
112    queued_renders: RefCell<Vec<Rc<ExpandedNode>>>,
113    pub layer_count: Cell<usize>,
114    pub dirty_canvases: Rc<RefCell<Vec<bool>>>,
115    dirty_canvas_nodes: RefCell<HashSet<ExpandedNodeIdentifier>>,
116    canvas_node_light_masks: RefCell<HashMap<ExpandedNodeIdentifier, u32>>,
117    lighting_overflow_counts: RefCell<HashMap<usize, usize>>,
118    targeted_canvas_replay_node_ids: RefCell<HashMap<usize, HashSet<u32>>>,
119    removed_canvas_nodes: RefCell<Vec<(usize, u32)>>,
120    occlusion_dirty: Cell<bool>,
121    layer_canvas_plan_generation: Cell<u32>,
122    canvas_drawable_layers: RefCell<HashSet<usize>>,
123    import_settings_node_count: Cell<usize>,
124    tick_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
125    pre_render_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
126    gyro_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
127    accel_handler_nodes: RefCell<Vec<ExpandedNodeIdentifier>>,
128    screenshot_map: Rc<RefCell<HashMap<u32, ScreenshotData>>>,
129    scroller_surface_states: RefCell<HashMap<u32, ScrollerSurfaceState>>,
130    layer_scroller_owners: RefCell<HashMap<usize, ExpandedNodeIdentifier>>,
131    root_scroller_id: Cell<Option<u32>>,
132    visual_viewport_state: Cell<Option<VisualViewportState>>,
133}
134
135#[derive(Clone, Copy, Debug, Default)]
136/// Last-known scroll state for a native or browser-owned scroller surface.
137pub struct ScrollerSurfaceState {
138    pub viewport_width: f64,
139    pub viewport_height: f64,
140    pub content_width: f64,
141    pub content_height: f64,
142    pub scroll_x: f64,
143    pub scroll_y: f64,
144    pub presentation_scroll_x: f64,
145    pub presentation_scroll_y: f64,
146    pub clip_content: bool,
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150/// Coarse classification of changes to a native or browser-owned scroller surface.
151pub enum ScrollerSurfaceStateChange {
152    Unchanged,
153    ScrollOnly,
154    Structural,
155}
156
157impl ScrollerSurfaceState {
158    fn close_enough(lhs: f64, rhs: f64) -> bool {
159        (lhs - rhs).abs() <= 1e-4
160    }
161
162    fn structural_eq(&self, other: &Self) -> bool {
163        Self::close_enough(self.viewport_width, other.viewport_width)
164            && Self::close_enough(self.viewport_height, other.viewport_height)
165            && Self::close_enough(self.content_width, other.content_width)
166            && Self::close_enough(self.content_height, other.content_height)
167            && self.clip_content == other.clip_content
168    }
169
170    fn scroll_eq(&self, other: &Self) -> bool {
171        Self::close_enough(self.scroll_x, other.scroll_x)
172            && Self::close_enough(self.scroll_y, other.scroll_y)
173            && Self::close_enough(self.presentation_scroll_x, other.presentation_scroll_x)
174            && Self::close_enough(self.presentation_scroll_y, other.presentation_scroll_y)
175    }
176}
177
178#[derive(Clone, Copy, Debug, Default)]
179/// Browser visual viewport state used when page scrolling participates in root scroller behavior.
180pub struct VisualViewportState {
181    pub width: f64,
182    pub height: f64,
183    pub offset_x: f64,
184    pub offset_y: f64,
185    pub page_scroll_x: f64,
186    pub page_scroll_y: f64,
187}
188
189struct NodeCache {
190    eid_to_node: HashMap<ExpandedNodeIdentifier, Rc<ExpandedNode>>,
191    uni_to_eid: HashMap<UniqueTemplateNodeIdentifier, Vec<ExpandedNodeIdentifier>>,
192}
193
194impl NodeCache {
195    fn new() -> Self {
196        Self {
197            eid_to_node: Default::default(),
198            uni_to_eid: Default::default(),
199        }
200    }
201
202    // Add this node to all relevant constant lookup cache structures
203    fn add_to_cache(&mut self, node: &Rc<ExpandedNode>) {
204        self.eid_to_node.insert(node.id, Rc::clone(&node));
205        let uni = borrow!(node.instance_node)
206            .base()
207            .template_node_identifier
208            .clone();
209        if let Some(uni) = uni {
210            self.uni_to_eid.entry(uni).or_default().push(node.id);
211        }
212    }
213
214    // Remove this node from all relevant constant lookup cache structures
215    fn remove_from_cache(&mut self, node: &Rc<ExpandedNode>) {
216        self.eid_to_node.remove(&node.id);
217        if let Some(uni) = &borrow!(node.instance_node).base().template_node_identifier {
218            self.uni_to_eid
219                .entry(uni.clone())
220                .or_default()
221                .retain(|&n| n != node.id);
222        }
223    }
224}
225
226impl RuntimeContext {
227    #[cfg(not(feature = "designtime"))]
228    /// Create a runtime context for normal app execution.
229    pub fn new(globals: Globals) -> Self {
230        Self {
231            next_uid: Cell::new(ExpandedNodeIdentifier(0)),
232            messages: RefCell::new(Vec::new()),
233            globals: RefCell::new(globals),
234            root_expanded_node: RefCell::new(Weak::new()),
235            node_cache: RefCell::new(NodeCache::new()),
236            active_touch_targets: Default::default(),
237            queued_custom_events: Default::default(),
238            queued_renders: Default::default(),
239            layer_count: Cell::default(),
240            last_topmost_element: Default::default(),
241            dirty_canvases: Default::default(),
242            dirty_canvas_nodes: Default::default(),
243            canvas_node_light_masks: Default::default(),
244            lighting_overflow_counts: Default::default(),
245            targeted_canvas_replay_node_ids: Default::default(),
246            removed_canvas_nodes: Default::default(),
247            occlusion_dirty: Cell::new(true),
248            layer_canvas_plan_generation: Cell::new(1),
249            canvas_drawable_layers: Default::default(),
250            import_settings_node_count: Cell::new(0),
251            tick_handler_nodes: Default::default(),
252            pre_render_handler_nodes: Default::default(),
253            gyro_handler_nodes: Default::default(),
254            accel_handler_nodes: Default::default(),
255            screenshot_map: Default::default(),
256            scroller_surface_states: Default::default(),
257            layer_scroller_owners: Default::default(),
258            root_scroller_id: Cell::new(None),
259            visual_viewport_state: Cell::new(None),
260        }
261    }
262
263    #[cfg(feature = "designtime")]
264    /// Create a runtime context with the userland component tracked for designtime tools.
265    pub fn new(globals: Globals, userland: Rc<ComponentInstance>) -> Self {
266        Self {
267            next_uid: Cell::new(ExpandedNodeIdentifier(0)),
268            messages: RefCell::new(Vec::new()),
269            globals: RefCell::new(globals),
270            root_expanded_node: RefCell::new(Weak::new()),
271            userland_frame_instance_node: RefCell::new(Some(userland)),
272            userland_root_expanded_node: Default::default(),
273            node_cache: RefCell::new(NodeCache::new()),
274            active_touch_targets: Default::default(),
275            queued_custom_events: Default::default(),
276            queued_renders: Default::default(),
277            layer_count: Cell::default(),
278            last_topmost_element: Default::default(),
279            dirty_canvases: Default::default(),
280            dirty_canvas_nodes: Default::default(),
281            canvas_node_light_masks: Default::default(),
282            lighting_overflow_counts: Default::default(),
283            targeted_canvas_replay_node_ids: Default::default(),
284            removed_canvas_nodes: Default::default(),
285            occlusion_dirty: Cell::new(true),
286            layer_canvas_plan_generation: Cell::new(1),
287            canvas_drawable_layers: Default::default(),
288            import_settings_node_count: Cell::new(0),
289            tick_handler_nodes: Default::default(),
290            pre_render_handler_nodes: Default::default(),
291            gyro_handler_nodes: Default::default(),
292            accel_handler_nodes: Default::default(),
293            screenshot_map: Default::default(),
294            scroller_surface_states: Default::default(),
295            layer_scroller_owners: Default::default(),
296            root_scroller_id: Cell::new(None),
297            visual_viewport_state: Cell::new(None),
298        }
299    }
300
301    #[cfg(feature = "designtime")]
302    /// Create a runtime context before any userland tree has been mounted.
303    pub fn new_empty(globals: Globals) -> Self {
304        Self {
305            next_uid: Cell::new(ExpandedNodeIdentifier(0)),
306            messages: RefCell::new(Vec::new()),
307            globals: RefCell::new(globals),
308            root_expanded_node: RefCell::new(Weak::new()),
309            userland_frame_instance_node: RefCell::new(None),
310            userland_root_expanded_node: Default::default(),
311            node_cache: RefCell::new(NodeCache::new()),
312            active_touch_targets: Default::default(),
313            queued_custom_events: Default::default(),
314            queued_renders: Default::default(),
315            layer_count: Cell::default(),
316            last_topmost_element: Default::default(),
317            dirty_canvases: Default::default(),
318            dirty_canvas_nodes: Default::default(),
319            canvas_node_light_masks: Default::default(),
320            lighting_overflow_counts: Default::default(),
321            targeted_canvas_replay_node_ids: Default::default(),
322            removed_canvas_nodes: Default::default(),
323            occlusion_dirty: Cell::new(true),
324            layer_canvas_plan_generation: Cell::new(1),
325            canvas_drawable_layers: Default::default(),
326            import_settings_node_count: Cell::new(0),
327            tick_handler_nodes: Default::default(),
328            pre_render_handler_nodes: Default::default(),
329            gyro_handler_nodes: Default::default(),
330            accel_handler_nodes: Default::default(),
331            screenshot_map: Default::default(),
332            scroller_surface_states: Default::default(),
333            layer_scroller_owners: Default::default(),
334            root_scroller_id: Cell::new(None),
335            visual_viewport_state: Cell::new(None),
336        }
337    }
338
339    /// Store the root expanded node after it has been initialized.
340    pub fn register_root_expanded_node(&self, root: &Rc<ExpandedNode>) {
341        *borrow_mut!(self.root_expanded_node) = Rc::downgrade(root);
342    }
343
344    /// Clear the registered root expanded node.
345    pub fn clear_root_expanded_node(&self) {
346        *borrow_mut!(self.root_expanded_node) = Weak::new();
347    }
348
349    /// Add a node to runtime lookup caches.
350    pub fn add_to_cache(&self, node: &Rc<ExpandedNode>) {
351        borrow_mut!(self.node_cache).add_to_cache(node);
352        self.register_node_lifecycle_handlers(node);
353        if node.is_import_settings_node() {
354            self.import_settings_node_count
355                .set(self.import_settings_node_count.get() + 1);
356        }
357        self.mark_occlusion_dirty();
358    }
359
360    /// Remove a node from runtime lookup caches.
361    pub fn remove_from_cache(&self, node: &Rc<ExpandedNode>) {
362        borrow_mut!(self.node_cache).remove_from_cache(node);
363        borrow_mut!(self.canvas_node_light_masks).remove(&node.id);
364        borrow_mut!(self.active_touch_targets).retain(|_, target| *target != node.id);
365        self.unregister_node_lifecycle_handlers(node.id);
366        if node.is_import_settings_node() {
367            self.import_settings_node_count
368                .set(self.import_settings_node_count.get().saturating_sub(1));
369        }
370        self.mark_occlusion_dirty();
371    }
372
373    pub fn has_import_settings_nodes(&self) -> bool {
374        self.import_settings_node_count.get() > 0
375    }
376
377    pub fn register_node_effect_property(&self, node: ExpandedNodeIdentifier, prop: &Property<()>) {
378        self.register_node_effect_property_named(node, prop, "node effect");
379    }
380
381    pub fn register_node_effect_property_named(
382        &self,
383        node: ExpandedNodeIdentifier,
384        prop: &Property<()>,
385        effect_name: &str,
386    ) {
387        if effect_drain_instrumentation_enabled() {
388            let debug_name = self.node_effect_debug_name(node, effect_name);
389            register_effect_property_with_name(prop, &debug_name);
390        } else {
391            register_effect_property(prop);
392        }
393    }
394
395    pub fn register_expanded_node_effect_property_named(
396        &self,
397        node: &Rc<ExpandedNode>,
398        prop: &Property<()>,
399        effect_name: &str,
400    ) {
401        if effect_drain_instrumentation_enabled() {
402            let debug_name = Self::expanded_node_effect_debug_name(node, effect_name);
403            register_effect_property_with_name(prop, &debug_name);
404        } else {
405            register_effect_property(prop);
406        }
407    }
408
409    pub fn register_node_effect(
410        &self,
411        node: ExpandedNodeIdentifier,
412        dependencies: &[UntypedProperty],
413        effect: impl Fn() + 'static,
414    ) -> Property<()> {
415        let prop = Property::computed(effect, dependencies);
416        self.register_node_effect_property_named(node, &prop, "registered node effect");
417        prop
418    }
419
420    pub fn drain_node_effects(&self) {
421        const MAX_NODE_EFFECTS_PER_TICK: usize = 100_000;
422        if !effect_drain_instrumentation_enabled() {
423            let drained = drain_effects(MAX_NODE_EFFECTS_PER_TICK);
424            if drained == MAX_NODE_EFFECTS_PER_TICK {
425                log::warn!(
426                    "reactive drain hit {} evaluations in one tick; deferring remaining work",
427                    MAX_NODE_EFFECTS_PER_TICK
428                );
429            }
430            return;
431        }
432
433        let start = Instant::now();
434        let report = drain_effects_with_report(MAX_NODE_EFFECTS_PER_TICK);
435        let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
436        if report.budget_exhausted {
437            log::warn!(
438                "reactive drain hit {} evaluations in one tick; deferring remaining work",
439                MAX_NODE_EFFECTS_PER_TICK
440            );
441        }
442        if report.popped > 0 || report.cutoffs_evaluated > 0 {
443            let globals = self.globals();
444            let frame_dependents = summarize_debug_names(property_outbound_debug_names(
445                &globals.elapsed_frames.untyped(),
446            ));
447            let millis_dependents = summarize_debug_names(property_outbound_debug_names(
448                &globals.elapsed_millis.untyped(),
449            ));
450            let frame_clock = globals.elapsed_frames.untyped();
451            let millis_clock = globals.elapsed_millis.untyped();
452            let (entering, exiting, enter_cleanup, exit_cleanup, stale_clock_nodes) = {
453                let nodes = borrow!(self.node_cache);
454                nodes.eid_to_node.values().fold(
455                    (0, 0, 0, 0, Vec::new()),
456                    |(entering, exiting, enter_cleanup, exit_cleanup, mut stale_clock_nodes),
457                     node| {
458                        let phase = node.transition_phase.get();
459                        if phase != TRANSITION_PHASE_ENTER
460                            && phase != TRANSITION_PHASE_EXIT
461                            && (property_has_direct_outbound(
462                                &frame_clock,
463                                &node.transition_playhead.untyped(),
464                            ) || property_has_direct_outbound(
465                                &millis_clock,
466                                &node.transition_playhead_millis.untyped(),
467                            ))
468                        {
469                            stale_clock_nodes
470                                .push(Self::expanded_node_effect_debug_name(node, "idle clock"));
471                        }
472                        (
473                            entering + usize::from(phase == TRANSITION_PHASE_ENTER),
474                            exiting + usize::from(phase == TRANSITION_PHASE_EXIT),
475                            enter_cleanup + usize::from(node.enter_cleanup_active.get()),
476                            exit_cleanup + usize::from(node.exit_cleanup_active.get()),
477                            stale_clock_nodes,
478                        )
479                    },
480                )
481            };
482            println!(
483                "[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=[{}]",
484                report.ran,
485                report.popped,
486                report.skipped_clean,
487                report.skipped_unregistered,
488                report.skipped_missing,
489                report.remaining,
490                report.cutoffs_evaluated,
491                report.cutoffs_suppressed,
492                report.cutoffs_propagated,
493                report.remaining_cutoffs,
494                elapsed_ms,
495                entering,
496                exiting,
497                enter_cleanup,
498                exit_cleanup,
499                stale_clock_nodes.join("; "),
500                format_effect_top(&frame_dependents),
501                format_effect_top(&millis_dependents),
502                format_effect_top(&report.top_effects),
503                format_effect_top(&report.top_cutoffs),
504            );
505        }
506    }
507
508    fn node_effect_debug_name(&self, node: ExpandedNodeIdentifier, effect_name: &str) -> String {
509        let node = borrow!(self.node_cache).eid_to_node.get(&node).cloned();
510        if let Some(node) = node {
511            Self::expanded_node_effect_debug_name(&node, effect_name)
512        } else {
513            format!("{effect_name} node={node:?}")
514        }
515    }
516
517    fn expanded_node_effect_debug_name(node: &Rc<ExpandedNode>, effect_name: &str) -> String {
518        let instance_node = borrow!(node.instance_node);
519        let base = instance_node.base();
520        let type_name = base
521            .template_node_type_id
522            .as_ref()
523            .map(|type_id| {
524                type_id
525                    .get_pascal_identifier()
526                    .unwrap_or_else(|| type_id.to_string())
527            })
528            .unwrap_or_else(|| "<unknown>".to_owned());
529
530        format!("{effect_name} type={type_name}")
531    }
532
533    pub fn tick_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
534        borrow!(self.tick_handler_nodes).clone()
535    }
536
537    pub fn pre_render_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
538        borrow!(self.pre_render_handler_nodes).clone()
539    }
540
541    pub fn gyro_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
542        borrow!(self.gyro_handler_nodes).clone()
543    }
544
545    pub fn accel_handler_nodes(&self) -> Vec<ExpandedNodeIdentifier> {
546        borrow!(self.accel_handler_nodes).clone()
547    }
548
549    fn register_node_lifecycle_handlers(&self, node: &Rc<ExpandedNode>) {
550        let registry = borrow!(node.instance_node)
551            .base()
552            .handler_registry
553            .as_ref()
554            .map(Rc::clone);
555        let Some(registry) = registry else {
556            return;
557        };
558        let handlers = &borrow!(registry).handlers;
559        if handlers.get(TICK_HANDLERS).is_some_and(|h| !h.is_empty()) {
560            self.register_lifecycle_handler(&self.tick_handler_nodes, node.id);
561        }
562        if handlers
563            .get(PRE_RENDER_HANDLERS)
564            .is_some_and(|h| !h.is_empty())
565        {
566            self.register_lifecycle_handler(&self.pre_render_handler_nodes, node.id);
567        }
568        if handlers.get(GYRO_HANDLERS).is_some_and(|h| !h.is_empty()) {
569            self.register_lifecycle_handler(&self.gyro_handler_nodes, node.id);
570        }
571        if handlers.get(ACCEL_HANDLERS).is_some_and(|h| !h.is_empty()) {
572            self.register_lifecycle_handler(&self.accel_handler_nodes, node.id);
573        }
574    }
575
576    fn unregister_node_lifecycle_handlers(&self, id: ExpandedNodeIdentifier) {
577        borrow_mut!(self.tick_handler_nodes).retain(|node_id| *node_id != id);
578        borrow_mut!(self.pre_render_handler_nodes).retain(|node_id| *node_id != id);
579        borrow_mut!(self.gyro_handler_nodes).retain(|node_id| *node_id != id);
580        borrow_mut!(self.accel_handler_nodes).retain(|node_id| *node_id != id);
581    }
582
583    fn register_lifecycle_handler(
584        &self,
585        handler_nodes: &RefCell<Vec<ExpandedNodeIdentifier>>,
586        id: ExpandedNodeIdentifier,
587    ) {
588        let mut handler_nodes = borrow_mut!(handler_nodes);
589        if !handler_nodes.contains(&id) {
590            handler_nodes.push(id);
591        }
592    }
593
594    /// Look up an expanded node by runtime id.
595    pub fn get_expanded_node_by_eid(&self, id: ExpandedNodeIdentifier) -> Option<Rc<ExpandedNode>> {
596        borrow!(self.node_cache).eid_to_node.get(&id).cloned()
597    }
598
599    /// Route a touch sequence to the node hit at touch-down, even after the finger moves away.
600    pub fn capture_touch_target(&self, identifier: i64, target: ExpandedNodeIdentifier) {
601        borrow_mut!(self.active_touch_targets).insert(identifier, target);
602    }
603
604    /// Resolve the node captured for an active touch sequence.
605    pub fn captured_touch_target(&self, identifier: i64) -> Option<Rc<ExpandedNode>> {
606        let target = borrow!(self.active_touch_targets)
607            .get(&identifier)
608            .copied()?;
609        self.get_expanded_node_by_eid(target)
610    }
611
612    /// Release and resolve the node captured for a completed touch sequence.
613    pub fn release_touch_target(&self, identifier: i64) -> Option<Rc<ExpandedNode>> {
614        let target = borrow_mut!(self.active_touch_targets).remove(&identifier)?;
615        self.get_expanded_node_by_eid(target)
616    }
617
618    /// Store a screenshot payload delivered by the chassis.
619    pub fn load_screenshot(&self, id: u32, data: ScreenshotData) -> bool {
620        borrow_mut!(self.screenshot_map).insert(id, data);
621        true
622    }
623
624    /// Shared screenshot capture map keyed by request id.
625    pub fn get_screenshot_map(&self) -> Rc<RefCell<HashMap<u32, ScreenshotData>>> {
626        Rc::clone(&self.screenshot_map)
627    }
628
629    /// Remember browser-owned scroller state for native compositing and scroll transforms.
630    pub fn set_scroller_surface_state(
631        &self,
632        id: u32,
633        state: ScrollerSurfaceState,
634    ) -> ScrollerSurfaceStateChange {
635        let change = {
636            let mut states = borrow_mut!(self.scroller_surface_states);
637            let change = match states.get(&id) {
638                Some(previous) if previous.structural_eq(&state) && previous.scroll_eq(&state) => {
639                    ScrollerSurfaceStateChange::Unchanged
640                }
641                Some(previous) if previous.structural_eq(&state) => {
642                    ScrollerSurfaceStateChange::ScrollOnly
643                }
644                _ => ScrollerSurfaceStateChange::Structural,
645            };
646            if change != ScrollerSurfaceStateChange::Unchanged {
647                states.insert(id, state);
648            }
649            change
650        };
651
652        match change {
653            ScrollerSurfaceStateChange::Unchanged => {}
654            ScrollerSurfaceStateChange::ScrollOnly => {
655                self.mark_layer_canvas_plans_dirty();
656                self.mark_scroller_content_layers_dirty(id);
657            }
658            ScrollerSurfaceStateChange::Structural => {
659                self.mark_layer_canvas_plans_dirty();
660                self.mark_occlusion_dirty();
661            }
662        }
663
664        change
665    }
666
667    /// Update hot scroll offsets for an existing native scroller surface without touching
668    /// structural state.
669    pub fn update_scroller_surface_scroll(
670        &self,
671        id: u32,
672        scroll_x: f64,
673        scroll_y: f64,
674        presentation_scroll_x: f64,
675        presentation_scroll_y: f64,
676    ) -> ScrollerSurfaceStateChange {
677        let change = {
678            let mut states = borrow_mut!(self.scroller_surface_states);
679            let Some(state) = states.get_mut(&id) else {
680                return ScrollerSurfaceStateChange::Unchanged;
681            };
682            let next = ScrollerSurfaceState {
683                scroll_x,
684                scroll_y,
685                presentation_scroll_x,
686                presentation_scroll_y,
687                ..*state
688            };
689            let change = if state.scroll_eq(&next) {
690                ScrollerSurfaceStateChange::Unchanged
691            } else {
692                ScrollerSurfaceStateChange::ScrollOnly
693            };
694            if change != ScrollerSurfaceStateChange::Unchanged {
695                *state = next;
696            }
697            change
698        };
699
700        if change == ScrollerSurfaceStateChange::ScrollOnly {
701            self.mark_layer_canvas_plans_dirty();
702        }
703
704        change
705    }
706
707    /// Remove cached scroller surface state.
708    pub fn remove_scroller_surface_state(&self, id: u32) {
709        borrow_mut!(self.scroller_surface_states).remove(&id);
710        self.mark_layer_canvas_plans_dirty();
711        self.mark_occlusion_dirty();
712    }
713
714    /// Fetch cached scroller surface state by node id.
715    pub fn get_scroller_surface_state(&self, id: u32) -> Option<ScrollerSurfaceState> {
716        borrow!(self.scroller_surface_states).get(&id).cloned()
717    }
718
719    /// Fetch the presentation scroll offset for a native scroller surface, falling back to the
720    /// authoritative scroll position when presentation scroll is unavailable.
721    pub fn get_scroller_surface_scroll(&self, id: u32) -> Option<(f64, f64)> {
722        borrow!(self.scroller_surface_states).get(&id).map(|state| {
723            let scroll_x = if state.presentation_scroll_x.is_finite() {
724                state.presentation_scroll_x
725            } else {
726                state.scroll_x
727            };
728            let scroll_y = if state.presentation_scroll_y.is_finite() {
729                state.presentation_scroll_y
730            } else {
731                state.scroll_y
732            };
733            (scroll_x, scroll_y)
734        })
735    }
736
737    fn scroller_content_presentation_transform(&self, node: &ExpandedNode) -> Affine {
738        fn clamp_offset(value: f64, content: f64, viewport: f64) -> f64 {
739            if content <= viewport || !value.is_finite() {
740                return 0.0;
741            }
742            value.max(0.0).min((content - viewport).max(0.0))
743        }
744
745        let root_delegates_to_page_scroll = self.get_root_scroller_id() == Some(node.id.to_u32())
746            && self.get_visual_viewport_state().is_some();
747        if root_delegates_to_page_scroll {
748            return Affine::IDENTITY;
749        }
750
751        let instance_node = borrow!(node.instance_node);
752        let (scroll_x, scroll_y) = if self.get_root_scroller_id() == Some(node.id.to_u32()) {
753            if let Some(visual) = self.get_visual_viewport_state() {
754                let visual_x = visual.page_scroll_x + visual.offset_x;
755                let visual_y = visual.page_scroll_y + visual.offset_y;
756                if visual_x.is_finite() && visual_y.is_finite() {
757                    if let Some(state) = self.get_scroller_surface_state(node.id.to_u32()) {
758                        let viewport_width = if visual.width.is_finite() {
759                            visual.width
760                        } else {
761                            state.viewport_width
762                        };
763                        let viewport_height = if visual.height.is_finite() {
764                            visual.height
765                        } else {
766                            state.viewport_height
767                        };
768                        (
769                            clamp_offset(visual_x, state.content_width, viewport_width),
770                            clamp_offset(visual_y, state.content_height, viewport_height),
771                        )
772                    } else {
773                        (visual_x, visual_y)
774                    }
775                } else {
776                    self.get_scroller_surface_scroll(node.id.to_u32())
777                        .or_else(|| instance_node.resolve_scroll_offset(node))
778                        .unwrap_or((0.0, 0.0))
779                }
780            } else {
781                self.get_scroller_surface_scroll(node.id.to_u32())
782                    .or_else(|| instance_node.resolve_scroll_offset(node))
783                    .unwrap_or((0.0, 0.0))
784            }
785        } else {
786            self.get_scroller_surface_scroll(node.id.to_u32())
787                .or_else(|| instance_node.resolve_scroll_offset(node))
788                .unwrap_or((0.0, 0.0))
789        };
790
791        if scroll_x.abs() <= f64::EPSILON && scroll_y.abs() <= f64::EPSILON {
792            return Affine::IDENTITY;
793        }
794
795        let world_transform = Affine::from(node.transform_and_bounds.get().transform);
796        let inverse_world = Affine::from(node.transform_and_bounds.get().transform.inverse());
797        world_transform * Affine::translate((-scroll_x, -scroll_y)) * inverse_world
798    }
799
800    /// Resolve the browser/native presentation transform inherited by a node from ancestor
801    /// scrollers. Layout transforms remain in content coordinates, while events arrive in the
802    /// scrolled window coordinates that the user sees.
803    pub(crate) fn presentation_scroll_transform_for_node(&self, node: &ExpandedNode) -> Affine {
804        let mut ancestors = Vec::new();
805        let mut current = node.render_parent_node();
806        while let Some(parent) = current {
807            current = parent.render_parent_node();
808            ancestors.push(parent);
809        }
810        ancestors.reverse();
811
812        ancestors
813            .into_iter()
814            .filter(|ancestor| borrow!(ancestor.instance_node).scrolls_content(ancestor))
815            .fold(Affine::IDENTITY, |transform, scroller| {
816                transform * self.scroller_content_presentation_transform(&scroller)
817            })
818    }
819
820    /// Clear render-layer-to-scroller ownership before recomputing occlusion.
821    pub fn clear_layer_scroller_owners(&self) {
822        borrow_mut!(self.layer_scroller_owners).clear();
823        self.mark_layer_canvas_plans_dirty();
824    }
825
826    /// Record that a render layer is owned by a particular scroller.
827    pub fn register_layer_scroller_owner(
828        &self,
829        layer_id: usize,
830        scroller_id: ExpandedNodeIdentifier,
831    ) {
832        borrow_mut!(self.layer_scroller_owners).insert(layer_id, scroller_id);
833        self.mark_layer_canvas_plans_dirty();
834    }
835
836    /// Find the scroller that owns a render layer, when one exists.
837    pub fn get_layer_scroller_owner(&self, layer_id: usize) -> Option<ExpandedNodeIdentifier> {
838        borrow!(self.layer_scroller_owners).get(&layer_id).copied()
839    }
840
841    fn mark_scroller_content_layers_dirty(&self, scroller_id: u32) {
842        let layers = borrow!(self.layer_scroller_owners)
843            .iter()
844            .filter_map(|(layer, owner)| (owner.to_u32() == scroller_id).then_some(*layer))
845            .collect::<Vec<_>>();
846        for layer in layers {
847            self.set_canvas_dirty(layer);
848        }
849    }
850
851    /// Mark which node currently delegates root scrolling behavior to the page.
852    pub fn set_root_scroller_id(&self, id: Option<u32>) {
853        self.root_scroller_id.set(id);
854        self.mark_layer_canvas_plans_dirty();
855        self.mark_occlusion_dirty();
856    }
857
858    /// Current page-scroll-backed root scroller id.
859    pub fn get_root_scroller_id(&self) -> Option<u32> {
860        self.root_scroller_id.get()
861    }
862
863    /// Cache the browser visual viewport state for root scroller math.
864    pub fn set_visual_viewport_state(&self, state: VisualViewportState) {
865        self.visual_viewport_state.set(Some(state));
866        self.mark_layer_canvas_plans_dirty();
867        self.mark_occlusion_dirty();
868    }
869
870    /// Clear cached visual viewport state.
871    pub fn clear_visual_viewport_state(&self) {
872        self.visual_viewport_state.set(None);
873        self.mark_layer_canvas_plans_dirty();
874        self.mark_occlusion_dirty();
875    }
876
877    /// Return cached browser visual viewport state, if available.
878    pub fn get_visual_viewport_state(&self) -> Option<VisualViewportState> {
879        self.visual_viewport_state.get()
880    }
881
882    /// Ensure the dirty-canvas table has entries up to the requested layer count.
883    pub fn resize_canvas_layers_to(&self, id: usize) {
884        let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
885        let old_len = dirty_canvases.len();
886        dirty_canvases.resize(id, false);
887        for dirty in dirty_canvases.iter_mut().skip(old_len) {
888            *dirty = true;
889        }
890        if old_len != id {
891            self.mark_layer_canvas_plans_dirty();
892        }
893    }
894
895    pub fn mark_layer_canvas_plans_dirty(&self) {
896        let next = self.layer_canvas_plan_generation.get().wrapping_add(1);
897        self.layer_canvas_plan_generation.set(next.max(1));
898    }
899
900    pub fn layer_canvas_plan_generation(&self) -> u32 {
901        self.layer_canvas_plan_generation.get()
902    }
903
904    /// Replace the set of render layers that currently contain canvas drawables.
905    pub fn set_canvas_drawable_layers(&self, layers: HashSet<usize>) {
906        let mut current = borrow_mut!(self.canvas_drawable_layers);
907        if *current == layers {
908            return;
909        }
910        *current = layers;
911        self.mark_layer_canvas_plans_dirty();
912    }
913
914    /// Return whether a render layer currently has canvas work to paint.
915    pub fn layer_has_canvas_drawables(&self, layer: usize) -> bool {
916        borrow!(self.canvas_drawable_layers).contains(&layer)
917    }
918
919    /// Mark every canvas layer clean.
920    pub fn clear_all_dirty_canvases(&self) {
921        let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
922        for v in dirty_canvases.iter_mut() {
923            *v = false;
924        }
925    }
926
927    /// Mark a canvas layer dirty.
928    pub fn set_canvas_dirty(&self, id: usize) {
929        let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
930        if let Some(v) = dirty_canvases.get_mut(id) {
931            *v = true;
932        }
933    }
934
935    /// Check whether a canvas layer needs redraw.
936    pub fn is_canvas_dirty(&self, id: &usize) -> bool {
937        *borrow!(self.dirty_canvases).get(*id).unwrap_or(&true)
938    }
939
940    pub fn dirty_canvas_layers(&self) -> Vec<usize> {
941        borrow!(self.dirty_canvases)
942            .iter()
943            .enumerate()
944            .filter_map(|(index, dirty)| dirty.then_some(index))
945            .collect()
946    }
947
948    pub fn has_dirty_canvas_layers(&self) -> bool {
949        borrow!(self.dirty_canvases).iter().any(|dirty| *dirty)
950    }
951
952    pub fn has_dirty_canvas_nodes(&self) -> bool {
953        !borrow!(self.dirty_canvas_nodes).is_empty()
954    }
955
956    pub fn has_canvas_node_removals(&self) -> bool {
957        !borrow!(self.removed_canvas_nodes).is_empty()
958    }
959
960    pub fn has_canvas_render_work(&self) -> bool {
961        self.has_dirty_canvas_layers() || self.has_canvas_node_removals()
962    }
963
964    pub fn mark_occlusion_dirty(&self) {
965        self.occlusion_dirty.set(true);
966    }
967
968    pub fn take_occlusion_dirty(&self) -> bool {
969        let dirty = self.occlusion_dirty.get();
970        self.occlusion_dirty.set(false);
971        dirty
972    }
973
974    pub fn set_all_canvases_dirty(&self) {
975        let mut dirty_canvases = borrow_mut!(self.dirty_canvases);
976        for v in dirty_canvases.iter_mut() {
977            *v = true;
978        }
979    }
980
981    pub fn mark_canvas_node_dirty(&self, id: ExpandedNodeIdentifier) {
982        borrow_mut!(self.dirty_canvas_nodes).insert(id);
983    }
984
985    pub fn clear_canvas_node_dirty(&self, id: &ExpandedNodeIdentifier) {
986        borrow_mut!(self.dirty_canvas_nodes).remove(id);
987    }
988
989    pub fn is_canvas_node_dirty(&self, id: &ExpandedNodeIdentifier) -> bool {
990        borrow!(self.dirty_canvas_nodes).contains(id)
991    }
992
993    pub fn dirty_canvas_node_ids(&self) -> Vec<ExpandedNodeIdentifier> {
994        borrow!(self.dirty_canvas_nodes).iter().copied().collect()
995    }
996
997    pub fn mark_canvas_nodes_on_layer_dirty(&self, layer: usize) {
998        let node_cache = borrow!(self.node_cache);
999        let dirty_nodes = &mut *borrow_mut!(self.dirty_canvas_nodes);
1000        for node in node_cache.eid_to_node.values() {
1001            if node.occlusion.get().render_layer_id == layer
1002                && borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas
1003            {
1004                dirty_nodes.insert(node.id);
1005            }
1006        }
1007    }
1008
1009    /// Return the direct-light membership mask resolved for a retained canvas node.
1010    pub fn canvas_node_light_mask(&self, id: ExpandedNodeIdentifier) -> u32 {
1011        borrow!(self.canvas_node_light_masks)
1012            .get(&id)
1013            .copied()
1014            .unwrap_or(0)
1015    }
1016
1017    pub fn collect_scene_lighting_for_layer(&self, layer: usize) -> SceneLighting {
1018        let layer_to_root_transforms = self.layer_to_root_transforms(layer);
1019        let root_to_target = layer_to_root_transforms[&layer].inverse();
1020        let node_cache = borrow!(self.node_cache);
1021        let mut scoped_lights = Vec::new();
1022        let mut topmost_ambient = None;
1023
1024        for node in node_cache.eid_to_node.values() {
1025            let occlusion = node.occlusion.get();
1026            let source_layer = occlusion.render_layer_id;
1027            let Some(source_to_root) = layer_to_root_transforms.get(&source_layer) else {
1028                continue;
1029            };
1030
1031            let (light, ambient) = {
1032                let instance_node = borrow!(node.instance_node);
1033                (
1034                    instance_node.resolve_scene_light(node, self),
1035                    instance_node.resolve_scene_ambient_light(node, self),
1036                )
1037            };
1038            if let Some(light) = light {
1039                if light.enabled {
1040                    scoped_lights.push((
1041                        occlusion.z_index,
1042                        node.id,
1043                        Self::nearest_light_frame(node),
1044                        transform_scene_light_for_layer(light, *source_to_root, root_to_target),
1045                    ));
1046                }
1047            }
1048            if let Some(ambient) = ambient {
1049                let should_replace = topmost_ambient
1050                    .as_ref()
1051                    .map(|(z_index, id, _)| (occlusion.z_index, node.id) >= (*z_index, *id))
1052                    .unwrap_or(true);
1053                if should_replace {
1054                    topmost_ambient = Some((occlusion.z_index, node.id, ambient));
1055                }
1056            }
1057        }
1058
1059        scoped_lights
1060            .sort_by(|left, right| right.0.cmp(&left.0).then_with(|| left.1.cmp(&right.1)));
1061        let overflow = scoped_lights
1062            .len()
1063            .saturating_sub(SceneLighting::MAX_LIGHTS);
1064        scoped_lights.truncate(SceneLighting::MAX_LIGHTS);
1065
1066        {
1067            let mut overflow_counts = borrow_mut!(self.lighting_overflow_counts);
1068            if overflow == 0 {
1069                overflow_counts.remove(&layer);
1070            } else if overflow_counts.insert(layer, overflow) != Some(overflow) {
1071                log::warn!(
1072                    "canvas layer {layer} has more than {} enabled lights; omitting {overflow}",
1073                    SceneLighting::MAX_LIGHTS
1074                );
1075            }
1076        }
1077
1078        let mut resolved_masks = Vec::new();
1079        for node in node_cache.eid_to_node.values() {
1080            if node.occlusion.get().render_layer_id != layer
1081                || borrow!(node.instance_node).base().flags().layer != crate::api::Layer::Canvas
1082            {
1083                continue;
1084            }
1085
1086            let frame_ancestry = Self::light_frame_ancestry(node);
1087            let mask = scoped_lights.iter().enumerate().fold(
1088                0u32,
1089                |mask, (slot, (_, _, owner_frame, _))| {
1090                    if light_reaches_frame_ancestry(owner_frame.as_ref(), &frame_ancestry) {
1091                        mask | (1u32 << slot)
1092                    } else {
1093                        mask
1094                    }
1095                },
1096            );
1097            resolved_masks.push((node.id, mask));
1098        }
1099        drop(node_cache);
1100
1101        let mut changed_nodes = Vec::new();
1102        {
1103            let mut masks = borrow_mut!(self.canvas_node_light_masks);
1104            for (node_id, mask) in resolved_masks {
1105                if masks.insert(node_id, mask) != Some(mask) {
1106                    changed_nodes.push(node_id);
1107                }
1108            }
1109        }
1110        borrow_mut!(self.dirty_canvas_nodes).extend(changed_nodes);
1111
1112        let lights = scoped_lights
1113            .into_iter()
1114            .map(|(_, _, _, light)| light)
1115            .collect::<Vec<_>>();
1116
1117        if let Some((_, _, ambient)) = topmost_ambient {
1118            SceneLighting {
1119                active: true,
1120                ambient_is_authored: true,
1121                ambient,
1122                lights,
1123            }
1124        } else {
1125            SceneLighting::with_default_ambient(lights)
1126        }
1127    }
1128
1129    fn nearest_light_frame(node: &ExpandedNode) -> Option<ExpandedNodeIdentifier> {
1130        let mut current = node.render_parent_node();
1131        while let Some(parent) = current {
1132            if borrow!(parent.instance_node).establishes_light_frame() {
1133                return Some(parent.id);
1134            }
1135            current = parent.render_parent_node();
1136        }
1137        None
1138    }
1139
1140    fn light_frame_ancestry(node: &ExpandedNode) -> HashSet<ExpandedNodeIdentifier> {
1141        let mut frames = HashSet::new();
1142        let mut current = node.render_parent_node();
1143        while let Some(parent) = current {
1144            if borrow!(parent.instance_node).establishes_light_frame() {
1145                frames.insert(parent.id);
1146            }
1147            current = parent.render_parent_node();
1148        }
1149        frames
1150    }
1151
1152    fn layer_to_root_transforms(&self, layer: usize) -> HashMap<usize, Affine> {
1153        let mut transforms = HashMap::new();
1154        transforms.insert(layer, self.layer_to_root_transform(layer));
1155
1156        let mut current_layer = layer;
1157        let mut visited = HashSet::new();
1158        while let Some(owner_id) = self.get_layer_scroller_owner(current_layer) {
1159            if !visited.insert(current_layer) {
1160                break;
1161            }
1162            let Some(owner_node) = self.get_expanded_node_by_eid(owner_id) else {
1163                break;
1164            };
1165            let owner_layer = owner_node.occlusion.get().render_layer_id;
1166            transforms
1167                .entry(owner_layer)
1168                .or_insert_with(|| self.layer_to_root_transform(owner_layer));
1169            if owner_layer == current_layer {
1170                break;
1171            }
1172            current_layer = owner_layer;
1173        }
1174
1175        transforms
1176    }
1177
1178    fn layer_to_root_transform(&self, layer: usize) -> Affine {
1179        let mut transform = Affine::IDENTITY;
1180        let mut current_layer = layer;
1181        let mut visited = HashSet::new();
1182
1183        while let Some(owner_id) = self.get_layer_scroller_owner(current_layer) {
1184            if !visited.insert(current_layer) {
1185                break;
1186            }
1187            let Some(owner_node) = self.get_expanded_node_by_eid(owner_id) else {
1188                break;
1189            };
1190            let owner_layer = owner_node.occlusion.get().render_layer_id;
1191            let owner_transform = self.canvas_surface_transform_for_node(&owner_node);
1192            let (scroll_x, scroll_y) = self
1193                .get_scroller_surface_scroll(owner_node.id.to_u32())
1194                .or_else(|| borrow!(owner_node.instance_node).resolve_scroll_offset(&owner_node))
1195                .unwrap_or((0.0, 0.0));
1196            transform = owner_transform * Affine::translate((-scroll_x, -scroll_y)) * transform;
1197
1198            if owner_layer == current_layer {
1199                break;
1200            }
1201            current_layer = owner_layer;
1202        }
1203
1204        transform
1205    }
1206
1207    fn canvas_surface_transform_for_node(&self, node: &ExpandedNode) -> Affine {
1208        let transform = Affine::from(node.transform_and_bounds.get().transform);
1209        let own_layer = node.occlusion.get().render_layer_id;
1210        let mut parent_frame_id = node.parent_frame.get();
1211        while let Some(current_parent_frame_id) = parent_frame_id {
1212            let Some(parent_frame) = self.get_expanded_node_by_eid(current_parent_frame_id) else {
1213                break;
1214            };
1215            if parent_frame.occlusion.get().render_layer_id != own_layer {
1216                return Affine::from(parent_frame.transform_and_bounds.get().transform.inverse())
1217                    * transform;
1218            }
1219            parent_frame_id = parent_frame.parent_frame.get();
1220        }
1221        transform
1222    }
1223
1224    pub fn mark_canvas_nodes_on_layer_dirty_by_id(&self, layer: usize, node_ids: &[u32]) {
1225        let node_cache = borrow!(self.node_cache);
1226        let dirty_nodes = &mut *borrow_mut!(self.dirty_canvas_nodes);
1227        for node_id in node_ids {
1228            let id = ExpandedNodeIdentifier(*node_id);
1229            let Some(node) = node_cache.eid_to_node.get(&id) else {
1230                continue;
1231            };
1232            if node.occlusion.get().render_layer_id == layer
1233                && borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas
1234            {
1235                dirty_nodes.insert(id);
1236            }
1237        }
1238    }
1239
1240    pub fn mark_targeted_canvas_replay_nodes(&self, layer: usize, node_ids: &[u32]) {
1241        let mut targeted = borrow_mut!(self.targeted_canvas_replay_node_ids);
1242        let layer_nodes = targeted.entry(layer).or_default();
1243        layer_nodes.extend(node_ids.iter().copied());
1244    }
1245
1246    pub fn take_targeted_canvas_replay_node_ids(&self) -> HashMap<usize, HashSet<u32>> {
1247        let targeted = &mut *borrow_mut!(self.targeted_canvas_replay_node_ids);
1248        std::mem::take(targeted)
1249    }
1250
1251    pub fn mark_all_canvas_nodes_dirty(&self) {
1252        let node_cache = borrow!(self.node_cache);
1253        let dirty_nodes = &mut *borrow_mut!(self.dirty_canvas_nodes);
1254        for node in node_cache.eid_to_node.values() {
1255            if borrow!(node.instance_node).base().flags().layer == crate::api::Layer::Canvas {
1256                dirty_nodes.insert(node.id);
1257            }
1258        }
1259    }
1260
1261    pub fn enqueue_canvas_node_removal(&self, layer: usize, node_id: u32) {
1262        borrow_mut!(self.removed_canvas_nodes).push((layer, node_id));
1263    }
1264
1265    pub fn take_canvas_node_removals(&self) -> Vec<(usize, u32)> {
1266        let mut removals = borrow_mut!(self.removed_canvas_nodes);
1267        std::mem::take(&mut *removals)
1268    }
1269
1270    /// Finds all ExpandedNodes with the CommonProperty#id matching the provided string
1271    pub fn get_expanded_nodes_by_id(&self, id: &str) -> Vec<Rc<ExpandedNode>> {
1272        //v0 limitation: currently an O(n) lookup cost (could be made O(1) with an id->expandednode cache)
1273        borrow!(self.node_cache)
1274            .eid_to_node
1275            .values()
1276            .filter(|val| {
1277                let common_props = val.get_common_properties();
1278                let common_props = borrow!(common_props);
1279                common_props.id.get().is_some_and(|i| i == id)
1280            })
1281            .cloned()
1282            .collect()
1283    }
1284
1285    /// Finds all ExpandedNodes with corresponding UniqueTemplateNodeIdentifier
1286    pub fn get_expanded_nodes_by_global_ids(
1287        &self,
1288        uni: &UniqueTemplateNodeIdentifier,
1289    ) -> Vec<Rc<ExpandedNode>> {
1290        let node_cache = borrow!(self.node_cache);
1291        node_cache
1292            .uni_to_eid
1293            .get(uni)
1294            .map(|eids| {
1295                let mut nodes = vec![];
1296                for e in eids {
1297                    if let Some(node) = node_cache.eid_to_node.get(e) {
1298                        nodes.push(Rc::clone(node));
1299                    } else {
1300                        log::warn!(
1301                            "failed to find node in engine for expanded node identifier: {:?}",
1302                            e
1303                        );
1304                    }
1305                }
1306                nodes
1307            })
1308            .unwrap_or_default()
1309    }
1310
1311    /// Simple 2D raycasting: the coordinates of the ray represent a
1312    /// ray running orthogonally to the view plane, intersecting at
1313    /// the specified point `ray`.  Areas outside of clipping bounds will
1314    /// not register a `hit`, nor will elements that suppress input events.
1315    pub fn get_elements_beneath_ray(
1316        &self,
1317        root: Option<Rc<ExpandedNode>>,
1318        ray: Point2<Window>,
1319        limit_one: bool,
1320        mut accum: Vec<Rc<ExpandedNode>>,
1321        hit_invisible: bool,
1322    ) -> Vec<Rc<ExpandedNode>> {
1323        //Traverse all elements in render tree sorted by z-index (highest-to-lowest)
1324        //First: check whether events are suppressed
1325        //Next: check whether ancestral clipping bounds (hit_test) are satisfied
1326        //Finally: check whether element itself satisfies hit_test(ray)
1327
1328        let Some(root_node) = root.or_else(|| borrow!(self.root_expanded_node).upgrade()) else {
1329            return accum;
1330        };
1331        let mut to_process = vec![(root_node, false, Affine::IDENTITY, false)];
1332        while let Some((node, clipped, active_scroll_transform, retained_for_exit)) =
1333            to_process.pop()
1334        {
1335            // make sure slot sources are updated for this node
1336            node.compute_flattened_projected_children();
1337            if !hit_invisible
1338                && (retained_for_exit || node.transition_phase.get() == TRANSITION_PHASE_EXIT)
1339            {
1340                continue;
1341            }
1342            // Browser-composited scrollers move descendants outside the engine transform tree.
1343            // Fold active scroll offsets into hit-testing so event rays line up with presented content.
1344            let (scroll_transform, clips_content) = {
1345                let instance_node = borrow!(node.instance_node);
1346                let scrolls_content = instance_node.scrolls_content(&node);
1347                let clips_content = instance_node.clips_content(&node);
1348                drop(instance_node);
1349                (
1350                    if scrolls_content {
1351                        self.scroller_content_presentation_transform(&node)
1352                    } else {
1353                        Affine::IDENTITY
1354                    },
1355                    clips_content,
1356                )
1357            };
1358            let descendant_scroll_transform = active_scroll_transform * scroll_transform;
1359            let hit = node.ray_cast_test(active_scroll_transform.inverse() * ray);
1360            if hit && !clipped {
1361                if hit_invisible
1362                    || !borrow!(node.instance_node)
1363                        .base()
1364                        .flags()
1365                        .invisible_to_raycasting
1366                {
1367                    //We only care about the topmost node getting hit, and the element
1368                    //pool is ordered by z-index so we can just resolve the whole
1369                    //calculation when we find the first matching node
1370                    if limit_one {
1371                        return vec![node];
1372                    }
1373                    accum.push(Rc::clone(&node));
1374                }
1375            }
1376            let clipped = clipped || (!hit && clips_content);
1377            let retained_children = borrow!(node.exiting_children).clone();
1378            to_process.extend(
1379                node.children
1380                    .get()
1381                    .iter()
1382                    .cloned()
1383                    .map(|v| {
1384                        let retained_for_exit = retained_children
1385                            .iter()
1386                            .any(|retained| Rc::ptr_eq(retained, &v));
1387                        let cp = v.get_common_properties();
1388                        let unclippable = borrow!(cp).unclippable.get().unwrap_or(false);
1389                        (
1390                            v,
1391                            clipped && !unclippable,
1392                            descendant_scroll_transform,
1393                            retained_for_exit,
1394                        )
1395                    })
1396                    .rev(),
1397            )
1398        }
1399        accum
1400    }
1401    /// Alias for `get_elements_beneath_ray` with `limit_one = true`
1402    pub fn get_topmost_element_beneath_ray(
1403        self: &Rc<Self>,
1404        ray: Point2<Window>,
1405    ) -> Option<Rc<ExpandedNode>> {
1406        let res = self.get_elements_beneath_ray(None, ray, true, vec![], false);
1407        let Some(new_topmost) = res
1408            .into_iter()
1409            .next()
1410            .or_else(|| borrow!(self.root_expanded_node).upgrade())
1411        else {
1412            *borrow_mut!(self.last_topmost_element) = Weak::new();
1413            return None;
1414        };
1415
1416        // Send mouse over/out events if the hit element is different than last.
1417        // Use template ancestry rather than containing-component ancestry so
1418        // wrappers around slotted content can own hover affordances.
1419        let last_topmost = borrow!(self.last_topmost_element).upgrade();
1420        if Some(new_topmost.id) != last_topmost.as_ref().map(|n| n.id) {
1421            let (leaving, entering) = find_template_paths_to_common_ancestor(
1422                &last_topmost,
1423                &Some(Rc::clone(&new_topmost)),
1424            );
1425            for leave in leaving {
1426                leave.dispatch_mouse_out(Event::new(MouseOut {}), &self.globals(), self);
1427            }
1428            for enter in entering {
1429                enter.dispatch_mouse_over(Event::new(MouseOver {}), &self.globals(), self);
1430            }
1431            *borrow_mut!(self.last_topmost_element) = Rc::downgrade(&new_topmost);
1432        }
1433        Some(new_topmost)
1434    }
1435
1436    pub fn gen_uid(&self) -> ExpandedNodeIdentifier {
1437        let val = self.next_uid.get();
1438        let next_val = ExpandedNodeIdentifier(val.0 + 1);
1439        self.next_uid.set(next_val);
1440        val
1441    }
1442
1443    pub fn enqueue_native_message(&self, message: NativeMessage) {
1444        borrow_mut!(self.messages).push(message)
1445    }
1446
1447    pub fn take_native_messages(&self) -> Vec<NativeMessage> {
1448        let mut messages = borrow_mut!(self.messages);
1449        std::mem::take(&mut *messages)
1450    }
1451
1452    pub fn globals(&self) -> Globals {
1453        borrow!(self.globals).clone()
1454    }
1455
1456    pub fn edit_globals(&self, f: impl Fn(&mut Globals)) {
1457        let mut globals = borrow_mut!(self.globals);
1458        f(&mut globals);
1459    }
1460
1461    pub fn queue_custom_event(&self, source_expanded_node: Rc<ExpandedNode>, name: &'static str) {
1462        let mut queued_custom_events = borrow_mut!(self.queued_custom_events);
1463        queued_custom_events.push((source_expanded_node, name));
1464    }
1465
1466    pub fn flush_custom_events(self: &Rc<Self>) -> Result<(), String> {
1467        let mut queued_custom_event = borrow_mut!(self.queued_custom_events);
1468        let to_flush: Vec<_> = std::mem::take(queued_custom_event.as_mut());
1469        for (target, ident) in to_flush {
1470            target.dispatch_custom_event(ident, self)?;
1471        }
1472        Ok(())
1473    }
1474
1475    #[cfg(feature = "designtime")]
1476    pub fn get_userland_root_expanded_node(&self) -> Option<Rc<ExpandedNode>> {
1477        borrow!(self.userland_root_expanded_node).clone()
1478    }
1479
1480    #[cfg(feature = "designtime")]
1481    pub fn get_userland_root_instance_node(&self) -> Option<Rc<dyn InstanceNode>> {
1482        borrow!(self.userland_frame_instance_node).clone()
1483    }
1484
1485    #[cfg(feature = "designtime")]
1486    pub fn set_userland_root_expanded_node(&self, root: Option<Rc<ExpandedNode>>) {
1487        *borrow_mut!(self.userland_root_expanded_node) = root;
1488    }
1489
1490    #[cfg(feature = "designtime")]
1491    pub fn set_userland_root_instance_node(&self, node: Option<Rc<dyn InstanceNode>>) {
1492        *borrow_mut!(self.userland_frame_instance_node) = node;
1493    }
1494
1495    pub fn get_root_expanded_node(&self) -> Option<Rc<ExpandedNode>> {
1496        borrow!(self.root_expanded_node).upgrade()
1497    }
1498
1499    pub fn queue_render(&self, expanded_node: Rc<ExpandedNode>) {
1500        borrow_mut!(self.queued_renders).push(expanded_node);
1501    }
1502
1503    pub fn recurse_flush_queued_renders(self: &Rc<RuntimeContext>, rcs: &mut dyn RenderContext) {
1504        while !borrow!(self.queued_renders).is_empty() {
1505            for n in std::mem::take(&mut *borrow_mut!(self.queued_renders)) {
1506                n.recurse_render(self, rcs);
1507            }
1508        }
1509    }
1510}
1511
1512fn find_template_paths_to_common_ancestor(
1513    last_topmost: &Option<Rc<ExpandedNode>>,
1514    new_topmost: &Option<Rc<ExpandedNode>>,
1515) -> (Vec<Rc<ExpandedNode>>, Vec<Rc<ExpandedNode>>) {
1516    let mut last_path = template_path_from_root(last_topmost);
1517    let mut new_path = template_path_from_root(new_topmost);
1518    let common_prefix_len = last_path
1519        .iter()
1520        .zip(new_path.iter())
1521        .take_while(|(last, new)| last.id == new.id)
1522        .count();
1523
1524    // Remove common ancestors from both paths.
1525    last_path.drain(0..common_prefix_len);
1526    new_path.drain(0..common_prefix_len);
1527
1528    (last_path, new_path)
1529}
1530
1531fn template_path_from_root(node: &Option<Rc<ExpandedNode>>) -> Vec<Rc<ExpandedNode>> {
1532    let mut path = Vec::new();
1533    let mut current = node.clone();
1534    while let Some(node) = current {
1535        path.push(Rc::clone(&node));
1536        current = node.template_parent.upgrade();
1537    }
1538    path.reverse();
1539    path
1540}
1541
1542/// Data structure for a single frame of our runtime stack, including
1543/// a reference to its parent frame and `properties` for
1544/// runtime evaluation, e.g. of Expressions.  `RuntimePropertiesStackFrame`s also track
1545/// timeline playhead position.
1546///
1547/// `Component`s push `RuntimePropertiesStackFrame`s before computing properties and pop them after computing, thus providing a
1548/// hierarchical store of node-relevant data that can be bound to symbols in expressions.
1549
1550pub struct RuntimePropertiesStackFrame {
1551    symbols_within_frame: HashMap<String, Variable>,
1552    local_stores: Rc<RefCell<HashMap<TypeId, Box<dyn Any>>>>,
1553    parent: Option<Rc<RuntimePropertiesStackFrame>>,
1554}
1555
1556impl RuntimePropertiesStackFrame {
1557    pub fn new(symbols_within_frame: HashMap<String, Variable>) -> Rc<Self> {
1558        Rc::new(Self {
1559            symbols_within_frame,
1560            local_stores: Default::default(),
1561            parent: None,
1562        })
1563    }
1564
1565    pub fn push(self: &Rc<Self>, symbols_within_frame: HashMap<String, Variable>) -> Rc<Self> {
1566        Rc::new(RuntimePropertiesStackFrame {
1567            symbols_within_frame,
1568            local_stores: Default::default(),
1569            parent: Some(Rc::clone(self)),
1570        })
1571    }
1572
1573    pub fn pop(self: &Rc<Self>) -> Option<Rc<Self>> {
1574        self.parent.clone()
1575    }
1576
1577    pub fn insert_stack_local_store<T: Store>(&self, store: T) {
1578        let type_id = TypeId::of::<T>();
1579        borrow_mut!(self.local_stores).insert(type_id, Box::new(store));
1580    }
1581
1582    pub fn peek_stack_local_store<T: Store, V>(
1583        self: &Rc<Self>,
1584        f: impl FnOnce(&mut T) -> V,
1585    ) -> Result<V, String> {
1586        let mut current = Rc::clone(self);
1587        let type_id = TypeId::of::<T>();
1588
1589        while !borrow!(current.local_stores).contains_key(&type_id) {
1590            current = current
1591                .parent
1592                .clone()
1593                .ok_or_else(|| format!("couldn't find store in local stack"))?;
1594        }
1595        let v = {
1596            let mut stores = borrow_mut!(current.local_stores);
1597            let store = stores.get_mut(&type_id).unwrap().downcast_mut().unwrap();
1598            f(store)
1599        };
1600        Ok(v)
1601    }
1602
1603    pub fn resolve_symbol_as_variable(&self, symbol: &str) -> Option<Variable> {
1604        if let Some(e) = self.symbols_within_frame.get(&clean_symbol(symbol)) {
1605            Some(e.clone())
1606        } else {
1607            self.parent.as_ref()?.resolve_symbol_as_variable(symbol)
1608        }
1609    }
1610
1611    pub fn resolve_symbol_as_erased_property(&self, symbol: &str) -> Option<UntypedProperty> {
1612        if let Some(e) = self.symbols_within_frame.get(&clean_symbol(symbol)) {
1613            Some(e.clone().get_untyped_property().clone())
1614        } else {
1615            self.parent
1616                .as_ref()?
1617                .resolve_symbol_as_erased_property(symbol)
1618        }
1619    }
1620
1621    pub fn resolve_symbol(&self, symbol: &str) -> Option<Variable> {
1622        self.symbols_within_frame
1623            .get(&clean_symbol(symbol))
1624            .cloned()
1625            .or_else(|| self.parent.as_ref()?.resolve_symbol(symbol))
1626    }
1627}
1628
1629fn clean_symbol(symbol: &str) -> String {
1630    symbol.replace("self.", "").replace("this.", "")
1631}
1632
1633impl IdentifierResolver for RuntimePropertiesStackFrame {
1634    fn resolve(&self, name: String) -> Result<Variable, String> {
1635        self.resolve_symbol(&name)
1636            .ok_or_else(|| format!("Could not resolve symbol {}", name))
1637    }
1638}
1639
1640fn light_reaches_frame_ancestry(
1641    owner_frame: Option<&ExpandedNodeIdentifier>,
1642    frame_ancestry: &HashSet<ExpandedNodeIdentifier>,
1643) -> bool {
1644    owner_frame.is_none_or(|frame| frame_ancestry.contains(frame))
1645}
1646
1647/// Data structure used for dynamic injection of values
1648/// into Expressions, maintaining a pointer e.g. to the current
1649/// stack frame to enable evaluation of properties & dependencies
1650pub struct ExpressionContext {
1651    pub stack_frame: Rc<RuntimePropertiesStackFrame>,
1652}
1653
1654#[cfg(test)]
1655mod light_scope_tests {
1656    use super::*;
1657    use crate::api::math::Transform2;
1658    use crate::{
1659        BaseInstance, CommonPropertiesInit, ComponentInstance, InstanceFlags, InstanceNode,
1660        InstantiationArgs, PropertiesInit, PropertiesScopeInit, RouteLocation, TransformAndBounds,
1661    };
1662    use pax_runtime_api::pax_value::{PaxAny, PaxValue};
1663    use pax_runtime_api::{Layer, Platform, SceneAmbientLight, TargetInfo, OS};
1664    use std::fmt;
1665
1666    #[derive(Clone, Copy)]
1667    enum TestLightingRole {
1668        Frame,
1669        Light(f64),
1670        Ambient(f64),
1671        Surface,
1672    }
1673
1674    struct TestLightingNode {
1675        base: BaseInstance,
1676        role: TestLightingRole,
1677        enabled: Cell<bool>,
1678    }
1679
1680    impl TestLightingNode {
1681        fn new(
1682            role: TestLightingRole,
1683            children: Vec<Rc<dyn InstanceNode>>,
1684        ) -> Rc<TestLightingNode> {
1685            Rc::new(Self {
1686                base: BaseInstance::new(
1687                    node_args(children),
1688                    InstanceFlags {
1689                        invisible_to_slot: false,
1690                        invisible_to_raycasting: true,
1691                        layer: match role {
1692                            TestLightingRole::Frame => Layer::DontCare,
1693                            TestLightingRole::Light(_)
1694                            | TestLightingRole::Ambient(_)
1695                            | TestLightingRole::Surface => Layer::Canvas,
1696                        },
1697                        is_component: false,
1698                        is_slot: false,
1699                    },
1700                ),
1701                role,
1702                enabled: Cell::new(true),
1703            })
1704        }
1705
1706        fn as_instance(self: &Rc<Self>) -> Rc<dyn InstanceNode> {
1707            self.clone()
1708        }
1709
1710        fn set_enabled(&self, enabled: bool) {
1711            self.enabled.set(enabled);
1712        }
1713    }
1714
1715    impl InstanceNode for TestLightingNode {
1716        fn instantiate(args: InstantiationArgs) -> Rc<Self>
1717        where
1718            Self: Sized,
1719        {
1720            Rc::new(Self {
1721                base: BaseInstance::new(
1722                    args,
1723                    InstanceFlags {
1724                        invisible_to_slot: false,
1725                        invisible_to_raycasting: true,
1726                        layer: Layer::Canvas,
1727                        is_component: false,
1728                        is_slot: false,
1729                    },
1730                ),
1731                role: TestLightingRole::Surface,
1732                enabled: Cell::new(true),
1733            })
1734        }
1735
1736        fn resolve_debug(
1737            &self,
1738            f: &mut fmt::Formatter,
1739            _expanded_node: Option<&ExpandedNode>,
1740        ) -> fmt::Result {
1741            f.debug_struct("TestLightingNode").finish()
1742        }
1743
1744        fn resolve_scene_light(
1745            &self,
1746            _expanded_node: &ExpandedNode,
1747            _context: &RuntimeContext,
1748        ) -> Option<SceneLight> {
1749            match self.role {
1750                TestLightingRole::Light(intensity) if self.enabled.get() => Some(SceneLight {
1751                    intensity,
1752                    ..Default::default()
1753                }),
1754                _ => None,
1755            }
1756        }
1757
1758        fn resolve_scene_ambient_light(
1759            &self,
1760            _expanded_node: &ExpandedNode,
1761            _context: &RuntimeContext,
1762        ) -> Option<SceneAmbientLight> {
1763            match self.role {
1764                TestLightingRole::Ambient(intensity) if self.enabled.get() => {
1765                    Some(SceneAmbientLight {
1766                        intensity,
1767                        ..Default::default()
1768                    })
1769                }
1770                _ => None,
1771            }
1772        }
1773
1774        fn establishes_light_frame(&self) -> bool {
1775            matches!(self.role, TestLightingRole::Frame)
1776        }
1777
1778        fn base(&self) -> &BaseInstance {
1779            &self.base
1780        }
1781    }
1782
1783    struct ScopedLightingFixture {
1784        context: Rc<RuntimeContext>,
1785        _root: Rc<ExpandedNode>,
1786        root_light: Rc<TestLightingNode>,
1787        outer_light: Rc<TestLightingNode>,
1788        ambient: Rc<TestLightingNode>,
1789        root_surface: Rc<ExpandedNode>,
1790        outer_surface: Rc<ExpandedNode>,
1791        nested_surface: Rc<ExpandedNode>,
1792        sibling_surface: Rc<ExpandedNode>,
1793        sibling_frame: Rc<ExpandedNode>,
1794    }
1795
1796    fn test_globals() -> Globals {
1797        Globals {
1798            elapsed_frames: Property::new(0),
1799            elapsed_millis: Property::new(0),
1800            viewport: Property::new(TransformAndBounds {
1801                transform: Transform2::identity(),
1802                bounds: (100.0, 100.0),
1803            }),
1804            gyro: Property::new(Default::default()),
1805            accel: Property::new(Default::default()),
1806            route_location: Property::new(RouteLocation::root()),
1807            browser_allows_scroller_vector_layers: Property::new(true),
1808            browser_allows_nested_scroller_vector_layers: Property::new(true),
1809            platform: Platform::Unknown,
1810            os: OS::Unknown,
1811            target: TargetInfo::new(Platform::Unknown, OS::Unknown),
1812            get_elapsed_millis: Rc::new(|| 0),
1813        }
1814    }
1815
1816    fn properties_factory() -> crate::PropertiesFactory {
1817        Box::new(|_, expanded_node| {
1818            expanded_node
1819                .is_none()
1820                .then(|| Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
1821        })
1822    }
1823
1824    fn node_args(children: Vec<Rc<dyn InstanceNode>>) -> InstantiationArgs {
1825        InstantiationArgs {
1826            prototypical_common_properties: CommonPropertiesInit::Default,
1827            prototypical_properties: PropertiesInit::Factory(properties_factory()),
1828            handler_registry: None,
1829            children: Some(RefCell::new(children)),
1830            component_template: None,
1831            component_settings: None,
1832            template_node_identifier: None,
1833            template_node_type_id: None,
1834            template_node_selector_info: None,
1835            transition_config: Default::default(),
1836            properties_scope: PropertiesScopeInit::None,
1837        }
1838    }
1839
1840    fn component_args(template: Vec<Rc<dyn InstanceNode>>) -> InstantiationArgs {
1841        InstantiationArgs {
1842            prototypical_common_properties: CommonPropertiesInit::Default,
1843            prototypical_properties: PropertiesInit::Factory(properties_factory()),
1844            handler_registry: None,
1845            children: None,
1846            component_template: Some(RefCell::new(template)),
1847            component_settings: None,
1848            template_node_identifier: None,
1849            template_node_type_id: None,
1850            template_node_selector_info: None,
1851            transition_config: Default::default(),
1852            properties_scope: PropertiesScopeInit::None,
1853        }
1854    }
1855
1856    fn mount_test_tree(
1857        children: Vec<Rc<dyn InstanceNode>>,
1858    ) -> (Rc<RuntimeContext>, Rc<ExpandedNode>) {
1859        let root_component = ComponentInstance::instantiate(component_args(children));
1860        let context = Rc::new(RuntimeContext::new(test_globals()));
1861        let root = ExpandedNode::initialize_root(root_component, &context);
1862        root.recurse_update(&context);
1863        crate::engine::occlusion::update_node_occlusion(&root, &context);
1864        (context, root)
1865    }
1866
1867    fn clear_dirty_canvas_nodes(context: &RuntimeContext) {
1868        for id in context.dirty_canvas_node_ids() {
1869            context.clear_canvas_node_dirty(&id);
1870        }
1871    }
1872
1873    fn scoped_lighting_fixture() -> ScopedLightingFixture {
1874        let nested_light = TestLightingNode::new(TestLightingRole::Light(3.0), Vec::new());
1875        let nested_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
1876        let nested_frame = TestLightingNode::new(
1877            TestLightingRole::Frame,
1878            vec![nested_light.as_instance(), nested_surface.as_instance()],
1879        );
1880
1881        let sibling_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
1882        let sibling_frame =
1883            TestLightingNode::new(TestLightingRole::Frame, vec![sibling_surface.as_instance()]);
1884
1885        let outer_light = TestLightingNode::new(TestLightingRole::Light(2.0), Vec::new());
1886        let outer_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
1887        let outer_frame = TestLightingNode::new(
1888            TestLightingRole::Frame,
1889            vec![
1890                outer_light.as_instance(),
1891                outer_surface.as_instance(),
1892                nested_frame.as_instance(),
1893                sibling_frame.as_instance(),
1894            ],
1895        );
1896
1897        let root_light = TestLightingNode::new(TestLightingRole::Light(1.0), Vec::new());
1898        let root_surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
1899        let ambient = TestLightingNode::new(TestLightingRole::Ambient(0.72), Vec::new());
1900        ambient.set_enabled(false);
1901
1902        let (context, root) = mount_test_tree(vec![
1903            root_light.as_instance(),
1904            root_surface.as_instance(),
1905            outer_frame.as_instance(),
1906            ambient.as_instance(),
1907        ]);
1908
1909        let root_children = root.children.get();
1910        let root_surface_node = root_children[1].clone();
1911        let outer_frame_node = root_children[2].clone();
1912        let outer_children = outer_frame_node.children.get();
1913        let outer_surface_node = outer_children[1].clone();
1914        let nested_frame_node = outer_children[2].clone();
1915        let sibling_frame_node = outer_children[3].clone();
1916        let nested_children = nested_frame_node.children.get();
1917        let nested_surface_node = nested_children[1].clone();
1918        let sibling_surface_node = sibling_frame_node.children.get()[0].clone();
1919
1920        ScopedLightingFixture {
1921            context,
1922            _root: root,
1923            root_light,
1924            outer_light,
1925            ambient,
1926            root_surface: root_surface_node,
1927            outer_surface: outer_surface_node,
1928            nested_surface: nested_surface_node,
1929            sibling_surface: sibling_surface_node,
1930            sibling_frame: sibling_frame_node,
1931        }
1932    }
1933
1934    #[test]
1935    fn root_lights_reach_every_frame_ancestry() {
1936        let ancestry = HashSet::from([ExpandedNodeIdentifier(10), ExpandedNodeIdentifier(20)]);
1937        assert!(light_reaches_frame_ancestry(None, &ancestry));
1938    }
1939
1940    #[test]
1941    fn framed_lights_reach_only_descendants_of_their_owner_frame() {
1942        let outer_frame = ExpandedNodeIdentifier(10);
1943        let nested_frame = ExpandedNodeIdentifier(20);
1944        let sibling_frame = ExpandedNodeIdentifier(30);
1945        let nested_ancestry = HashSet::from([outer_frame, nested_frame]);
1946
1947        assert!(light_reaches_frame_ancestry(
1948            Some(&outer_frame),
1949            &nested_ancestry
1950        ));
1951        assert!(light_reaches_frame_ancestry(
1952            Some(&nested_frame),
1953            &nested_ancestry
1954        ));
1955        assert!(!light_reaches_frame_ancestry(
1956            Some(&sibling_frame),
1957            &nested_ancestry
1958        ));
1959    }
1960
1961    #[test]
1962    fn mounted_tree_resolves_root_nested_and_sibling_light_masks() {
1963        let fixture = scoped_lighting_fixture();
1964
1965        let lighting = fixture.context.collect_scene_lighting_for_layer(0);
1966
1967        assert_eq!(
1968            lighting
1969                .lights
1970                .iter()
1971                .map(|light| light.intensity)
1972                .collect::<Vec<_>>(),
1973            vec![1.0, 2.0, 3.0]
1974        );
1975        assert!(!lighting.ambient_is_authored);
1976        assert_eq!(
1977            fixture
1978                .context
1979                .canvas_node_light_mask(fixture.root_surface.id),
1980            0b001
1981        );
1982        assert_eq!(
1983            fixture
1984                .context
1985                .canvas_node_light_mask(fixture.outer_surface.id),
1986            0b011
1987        );
1988        assert_eq!(
1989            fixture
1990                .context
1991                .canvas_node_light_mask(fixture.nested_surface.id),
1992            0b111
1993        );
1994        assert_eq!(
1995            fixture
1996                .context
1997                .canvas_node_light_mask(fixture.sibling_surface.id),
1998            0b011
1999        );
2000    }
2001
2002    #[test]
2003    fn slot_reallocation_and_render_reparenting_refresh_retained_masks() {
2004        let fixture = scoped_lighting_fixture();
2005        fixture.context.collect_scene_lighting_for_layer(0);
2006        clear_dirty_canvas_nodes(&fixture.context);
2007
2008        fixture.outer_light.set_enabled(false);
2009        let lighting = fixture.context.collect_scene_lighting_for_layer(0);
2010
2011        assert_eq!(
2012            lighting
2013                .lights
2014                .iter()
2015                .map(|light| light.intensity)
2016                .collect::<Vec<_>>(),
2017            vec![1.0, 3.0]
2018        );
2019        assert_eq!(
2020            fixture
2021                .context
2022                .canvas_node_light_mask(fixture.root_surface.id),
2023            0b01
2024        );
2025        assert_eq!(
2026            fixture
2027                .context
2028                .canvas_node_light_mask(fixture.outer_surface.id),
2029            0b01
2030        );
2031        assert_eq!(
2032            fixture
2033                .context
2034                .canvas_node_light_mask(fixture.nested_surface.id),
2035            0b11
2036        );
2037        assert!(fixture
2038            .context
2039            .is_canvas_node_dirty(&fixture.outer_surface.id));
2040        assert!(fixture
2041            .context
2042            .is_canvas_node_dirty(&fixture.nested_surface.id));
2043        assert!(!fixture
2044            .context
2045            .is_canvas_node_dirty(&fixture.root_surface.id));
2046
2047        clear_dirty_canvas_nodes(&fixture.context);
2048        *borrow_mut!(fixture.nested_surface.render_parent) = Rc::downgrade(&fixture.sibling_frame);
2049        fixture.context.collect_scene_lighting_for_layer(0);
2050
2051        assert_eq!(
2052            fixture
2053                .context
2054                .canvas_node_light_mask(fixture.nested_surface.id),
2055            0b01
2056        );
2057        assert!(fixture
2058            .context
2059            .is_canvas_node_dirty(&fixture.nested_surface.id));
2060    }
2061
2062    #[test]
2063    fn default_and_authored_ambient_keep_empty_direct_masks_distinct() {
2064        let fixture = scoped_lighting_fixture();
2065        fixture.root_light.set_enabled(false);
2066
2067        let default_ambient = fixture.context.collect_scene_lighting_for_layer(0);
2068        assert!(default_ambient.active);
2069        assert!(!default_ambient.ambient_is_authored);
2070        assert_eq!(
2071            fixture
2072                .context
2073                .canvas_node_light_mask(fixture.root_surface.id),
2074            0
2075        );
2076        assert_eq!(
2077            default_ambient.ambient.intensity,
2078            SceneLighting::DEFAULT_AMBIENT_INTENSITY
2079        );
2080
2081        fixture.ambient.set_enabled(true);
2082        let authored_ambient = fixture.context.collect_scene_lighting_for_layer(0);
2083        assert!(authored_ambient.active);
2084        assert!(authored_ambient.ambient_is_authored);
2085        assert_eq!(authored_ambient.ambient.intensity, 0.72);
2086        assert_eq!(
2087            fixture
2088                .context
2089                .canvas_node_light_mask(fixture.root_surface.id),
2090            0
2091        );
2092    }
2093
2094    #[test]
2095    fn light_overflow_selection_is_deterministic_and_recovers() {
2096        let lights = (0..10)
2097            .map(|index| {
2098                TestLightingNode::new(TestLightingRole::Light((index + 1) as f64), Vec::new())
2099            })
2100            .collect::<Vec<_>>();
2101        let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2102        let mut templates = lights
2103            .iter()
2104            .rev()
2105            .map(TestLightingNode::as_instance)
2106            .collect::<Vec<_>>();
2107        templates.push(surface.as_instance());
2108        let (context, root) = mount_test_tree(templates);
2109        let expanded = root.children.get();
2110        let surface_node = expanded[10].clone();
2111
2112        let first = context.collect_scene_lighting_for_layer(0);
2113        let second = context.collect_scene_lighting_for_layer(0);
2114        let expected = vec![10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0];
2115        assert_eq!(
2116            first
2117                .lights
2118                .iter()
2119                .map(|light| light.intensity)
2120                .collect::<Vec<_>>(),
2121            expected
2122        );
2123        assert_eq!(first.lights, second.lights);
2124        assert_eq!(context.canvas_node_light_mask(surface_node.id), 0xff);
2125        assert_eq!(borrow!(context.lighting_overflow_counts).get(&0), Some(&2));
2126
2127        lights[0].set_enabled(false);
2128        lights[1].set_enabled(false);
2129        let recovered = context.collect_scene_lighting_for_layer(0);
2130        assert_eq!(
2131            recovered
2132                .lights
2133                .iter()
2134                .map(|light| light.intensity)
2135                .collect::<Vec<_>>(),
2136            expected
2137        );
2138        assert!(!borrow!(context.lighting_overflow_counts).contains_key(&0));
2139    }
2140
2141    #[test]
2142    fn touch_capture_releases_and_clears_removed_targets() {
2143        let surface = TestLightingNode::new(TestLightingRole::Surface, Vec::new());
2144        let (context, root) = mount_test_tree(vec![surface.as_instance()]);
2145        let surface_node = root.children.get()[0].clone();
2146
2147        context.capture_touch_target(41, surface_node.id);
2148        assert!(Rc::ptr_eq(
2149            &context.captured_touch_target(41).unwrap(),
2150            &surface_node
2151        ));
2152        assert!(Rc::ptr_eq(
2153            &context.release_touch_target(41).unwrap(),
2154            &surface_node
2155        ));
2156        assert!(context.captured_touch_target(41).is_none());
2157
2158        context.capture_touch_target(42, surface_node.id);
2159        context.remove_from_cache(&surface_node);
2160        assert!(context.captured_touch_target(42).is_none());
2161        assert!(context.release_touch_target(42).is_none());
2162    }
2163}