Skip to main content

pax_runtime/engine/
mod.rs

1use crate::constants::{PRE_RENDER_HANDLERS, TICK_HANDLERS};
2use crate::{
3    api::LocalProperty, ExpandedNodeIdentifier, RouteLocation, RuntimePropertiesStackFrame,
4    TransformAndBounds, INTERNAL_ROUTE_LOCATION_SYMBOL,
5};
6use_RefCell!();
7use std::collections::{HashMap, HashSet};
8use std::rc::Rc;
9
10use pax_message::NativeMessage;
11use pax_runtime_api::{
12    borrow, pax_value::PaxAny, use_RefCell, Accel, Event, Focus, Gyro, SelectStart, TargetInfo,
13    Variable, Viewport, Window, OS,
14};
15
16use crate::api::{KeyDown, KeyPress, KeyUp, NodeContext, RenderContext};
17
18use crate::{ComponentInstance, RuntimeContext};
19use pax_runtime_api::Platform;
20
21pub mod layer_surface;
22pub mod layer_tiling;
23pub mod node_interface;
24pub mod occlusion;
25// TODO move these to not be in engine - make separate crates?
26pub mod pax_gpu_render_context;
27pub mod piet_render_context;
28#[cfg(test)]
29mod render_lifecycle_tests;
30
31/// The atomic unit of rendering; also the container for each unique tuple of computed properties.
32/// Represents an expanded node, that is "expanded" in the context of computed properties and repeat expansion.
33/// For example, a Rectangle inside `for i in 0..3` and a `for j in 0..4` would have 12 expanded nodes representing the 12 virtual Rectangles in the
34/// rendered scene graph.
35/// `ExpandedNode`s are architecturally "type-blind" — while they store typed data e.g. inside `computed_properties` and `computed_common_properties`,
36/// they require coordinating with their "type-aware" [`InstanceNode`] to perform operations on those properties.
37mod expanded_node;
38pub use expanded_node::{
39    ExpandedNode, RuntimeResolvedPropertyColumns, RuntimeResolvedPropertyEntry,
40    RuntimeSettingsCondition, RuntimeSettingsLayer, RuntimeSettingsSignatureEntry,
41    RuntimeSettingsSource,
42};
43
44use self::node_interface::NodeLocal;
45
46fn saturating_u128_to_u64(value: u128) -> u64 {
47    value.min(u64::MAX as u128) as u64
48}
49
50struct FilteredRenderPlan {
51    dirty_nodes: HashSet<ExpandedNodeIdentifier>,
52    render_path_nodes: HashSet<ExpandedNodeIdentifier>,
53}
54
55fn viewport_info_property(
56    viewport_bounds: &LocalProperty<TransformAndBounds<NodeLocal, Window>>,
57) -> LocalProperty<Viewport> {
58    let cloned_viewport = viewport_bounds.clone();
59    let deps = [cloned_viewport.untyped()];
60    LocalProperty::computed(
61        move || {
62            let viewport = cloned_viewport.get();
63            Viewport::new(viewport.bounds.0, viewport.bounds.1)
64        },
65        &deps,
66    )
67}
68
69fn viewport_number_property(
70    viewport: &LocalProperty<Viewport>,
71    name: &str,
72    accessor: fn(Viewport) -> f64,
73) -> LocalProperty<f64> {
74    let cloned_viewport = viewport.clone();
75    let deps = [cloned_viewport.untyped()];
76    LocalProperty::computed_with_name(move || accessor(cloned_viewport.get()), &deps, name)
77}
78
79fn viewport_bool_property(
80    viewport: &LocalProperty<Viewport>,
81    name: &str,
82    accessor: fn(Viewport) -> bool,
83) -> LocalProperty<bool> {
84    let cloned_viewport = viewport.clone();
85    let deps = [cloned_viewport.untyped()];
86    LocalProperty::computed_with_name(move || accessor(cloned_viewport.get()), &deps, name)
87}
88
89#[cfg(feature = "designtime")]
90use {crate::InstanceNode, pax_designtime::DesigntimeManager, pax_runtime_api::borrow_mut};
91
92#[derive(Clone)]
93/// Engine-wide reactive globals exposed to every component frame.
94pub struct Globals {
95    pub elapsed_frames: LocalProperty<u64>,
96    pub elapsed_millis: LocalProperty<u64>,
97    pub viewport: LocalProperty<TransformAndBounds<NodeLocal, Window>>,
98    pub gyro: LocalProperty<Gyro>,
99    pub accel: LocalProperty<Accel>,
100    pub route_location: LocalProperty<RouteLocation>,
101    pub browser_allows_scroller_vector_layers: LocalProperty<bool>,
102    pub browser_allows_nested_scroller_vector_layers: LocalProperty<bool>,
103    pub platform: Platform,
104    pub os: OS,
105    pub target: TargetInfo,
106    #[cfg(feature = "designtime")]
107    pub designtime: Rc<RefCell<DesigntimeManager>>,
108    pub get_elapsed_millis: Rc<dyn Fn() -> u128>,
109}
110
111impl Globals {
112    /// Build the root stack frame containing built-in globals plus internal engine state.
113    pub fn stack_frame(&self) -> Rc<RuntimePropertiesStackFrame> {
114        let target = self.target;
115        let viewport = viewport_info_property(&self.viewport);
116
117        let target_var = Variable::new_from_typed_property(LocalProperty::new(target));
118        let viewport_var = Variable::new_from_typed_property(viewport.clone());
119        let gyro_var = Variable::new_from_typed_property(self.gyro.clone());
120        let accel_var = Variable::new_from_typed_property(self.accel.clone());
121        let elapsed_frames_var = Variable::new_from_typed_property(self.elapsed_frames.clone());
122        let elapsed_millis_var = Variable::new_from_typed_property(self.elapsed_millis.clone());
123        let route_location_var = Variable::new_from_typed_property(self.route_location.clone());
124
125        let global_scope = vec![
126            ("$target".to_string(), target_var),
127            ("$viewport".to_string(), viewport_var),
128            (
129                "$web".to_string(),
130                Variable::new_from_typed_property(LocalProperty::new(target.web)),
131            ),
132            (
133                "$native".to_string(),
134                Variable::new_from_typed_property(LocalProperty::new(target.native)),
135            ),
136            (
137                "$ios".to_string(),
138                Variable::new_from_typed_property(LocalProperty::new(target.ios)),
139            ),
140            (
141                "$iphone".to_string(),
142                Variable::new_from_typed_property(LocalProperty::new(target.iphone)),
143            ),
144            (
145                "$ipad".to_string(),
146                Variable::new_from_typed_property(LocalProperty::new(target.ipad)),
147            ),
148            (
149                "$macos".to_string(),
150                Variable::new_from_typed_property(LocalProperty::new(target.macos)),
151            ),
152            (
153                "$android".to_string(),
154                Variable::new_from_typed_property(LocalProperty::new(target.android)),
155            ),
156            (
157                "$windows".to_string(),
158                Variable::new_from_typed_property(LocalProperty::new(target.windows)),
159            ),
160            (
161                "$linux".to_string(),
162                Variable::new_from_typed_property(LocalProperty::new(target.linux)),
163            ),
164            (
165                "$mobile".to_string(),
166                Variable::new_from_typed_property(LocalProperty::new(target.mobile)),
167            ),
168            (
169                "$desktop".to_string(),
170                Variable::new_from_typed_property(LocalProperty::new(target.desktop)),
171            ),
172            (
173                "$major".to_string(),
174                Variable::new_from_typed_property(viewport_number_property(
175                    &viewport,
176                    "$major",
177                    |viewport| viewport.major,
178                )),
179            ),
180            (
181                "$minor".to_string(),
182                Variable::new_from_typed_property(viewport_number_property(
183                    &viewport,
184                    "$minor",
185                    |viewport| viewport.minor,
186                )),
187            ),
188            (
189                "$aspect".to_string(),
190                Variable::new_from_typed_property(viewport_number_property(
191                    &viewport,
192                    "$aspect",
193                    |viewport| viewport.aspect,
194                )),
195            ),
196            (
197                "$landscape".to_string(),
198                Variable::new_from_typed_property(viewport_bool_property(
199                    &viewport,
200                    "$landscape",
201                    |viewport| viewport.landscape,
202                )),
203            ),
204            (
205                "$portrait".to_string(),
206                Variable::new_from_typed_property(viewport_bool_property(
207                    &viewport,
208                    "$portrait",
209                    |viewport| viewport.portrait,
210                )),
211            ),
212            (
213                "$square".to_string(),
214                Variable::new_from_typed_property(viewport_bool_property(
215                    &viewport,
216                    "$square",
217                    |viewport| viewport.square,
218                )),
219            ),
220            ("$gyro".to_string(), gyro_var),
221            ("$accel".to_string(), accel_var),
222            ("$frames".to_string(), elapsed_frames_var),
223            ("$millis".to_string(), elapsed_millis_var),
224            (
225                INTERNAL_ROUTE_LOCATION_SYMBOL.to_string(),
226                route_location_var,
227            ),
228        ]
229        .into_iter()
230        .collect();
231
232        let root_env = RuntimePropertiesStackFrame::new(global_scope);
233        root_env
234    }
235}
236
237impl std::fmt::Debug for Globals {
238    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        f.debug_struct("Globals")
240            .field("elapsed_frames", &self.elapsed_frames)
241            .field("elapsed_millis", &self.elapsed_millis)
242            .field("viewport", &self.viewport)
243            .field("gyro", &self.gyro)
244            .field("accel", &self.accel)
245            .field("route_location", &self.route_location)
246            .field("target", &self.target)
247            .finish_non_exhaustive()
248    }
249}
250
251/// Singleton struct storing everything related to properties computation & rendering
252pub struct PaxEngine {
253    pub runtime_context: Rc<RuntimeContext>,
254    pub root_expanded_node: Option<Rc<ExpandedNode>>,
255    pub scroller_tiling_policy: layer_tiling::ScrollerTilingPolicy,
256}
257
258/// Selects the Rust state owner independently of the handler's target `NodeContext`.
259pub enum HandlerLocation {
260    /// A binding on a template element: use the component that authored the element.
261    Inline,
262    /// A component's own settings or conventional method: use the target component.
263    Component,
264}
265
266/// Runtime event handler thunk generated from template bindings.
267pub struct Handler {
268    pub function: fn(Rc<RefCell<PaxAny>>, &NodeContext, Option<PaxAny>),
269    pub location: HandlerLocation,
270}
271
272impl Handler {
273    /// Build a handler whose `self` is the component that authored the inline binding.
274    /// The context still describes the primitive or child component receiving the call.
275    pub fn new_inline_handler(
276        function: fn(Rc<RefCell<PaxAny>>, &NodeContext, Option<PaxAny>),
277    ) -> Self {
278        Handler {
279            function,
280            location: HandlerLocation::Inline,
281        }
282    }
283
284    /// Build a handler whose `self` and context both belong to the target component.
285    pub fn new_component_handler(
286        function: fn(Rc<RefCell<PaxAny>>, &NodeContext, Option<PaxAny>),
287    ) -> Self {
288        Handler {
289            function,
290            location: HandlerLocation::Component,
291        }
292    }
293}
294
295/// Map from event key to one or more handlers registered on an instance node.
296pub struct HandlerRegistry {
297    pub handlers: HashMap<String, Vec<Handler>>,
298}
299
300impl Default for HandlerRegistry {
301    fn default() -> Self {
302        HandlerRegistry {
303            handlers: HashMap::new(),
304        }
305    }
306}
307
308/// Central instance of the PaxEngine and runtime, intended to be created by a particular chassis.
309/// Contains all rendering and runtime logic.
310///
311impl PaxEngine {
312    /// Activates the mounted application exactly once. Candidate hosts defer this
313    /// until their revision is committed.
314    pub fn activate_application(&self) {
315        self.runtime_context.application.activate();
316    }
317
318    /// Stops new application work, unmounts, then begins retained resource cleanup.
319    pub fn shutdown(&mut self, reason: pax_runtime_api::StopReason) {
320        let closing = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
321            self.runtime_context.application.begin_close()
322        }));
323        if matches!(closing, Ok(false)) {
324            return;
325        }
326        let unmounted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.unmount()));
327        let stopped = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
328            self.runtime_context.application.finish_close(reason);
329        }));
330        // Teardown can run from Drop and native C entrypoints. Finish the
331        // remaining cleanup even if an application hook unwinds.
332        if closing.is_err() || unmounted.is_err() || stopped.is_err() {
333            log::error!("application teardown panicked; remaining cleanup was attempted");
334        }
335    }
336
337    #[cfg(not(feature = "designtime"))]
338    fn build_globals(
339        viewport_size: (f64, f64),
340        platform: Platform,
341        os: OS,
342        get_elapsed_millis: Box<dyn Fn() -> u128>,
343    ) -> Globals {
344        use crate::api::math::Transform2;
345        use pax_runtime_api::{properties, Functions};
346        Functions::register_all_functions();
347
348        let elapsed_frames = LocalProperty::new(0);
349        let elapsed_millis = LocalProperty::new(saturating_u128_to_u64(get_elapsed_millis()));
350        properties::register_time(&elapsed_frames);
351        properties::register_millis(&elapsed_millis);
352        Globals {
353            elapsed_frames,
354            elapsed_millis,
355            viewport: LocalProperty::new(TransformAndBounds {
356                transform: Transform2::identity(),
357                bounds: viewport_size,
358            }),
359            gyro: LocalProperty::new(Gyro::default()),
360            accel: LocalProperty::new(Accel::default()),
361            route_location: LocalProperty::new(RouteLocation::root()),
362            browser_allows_scroller_vector_layers: LocalProperty::new(true),
363            browser_allows_nested_scroller_vector_layers: LocalProperty::new(true),
364            platform,
365            os,
366            target: TargetInfo::new(platform, os),
367            get_elapsed_millis: Rc::from(get_elapsed_millis),
368        }
369    }
370
371    #[cfg(feature = "designtime")]
372    fn build_globals(
373        viewport_size: (f64, f64),
374        designtime: Rc<RefCell<DesigntimeManager>>,
375        platform: Platform,
376        os: OS,
377        get_elapsed_millis: Box<dyn Fn() -> u128>,
378    ) -> Globals {
379        use pax_runtime_api::{math::Transform2, properties, Functions};
380        Functions::register_all_functions();
381
382        let elapsed_frames = LocalProperty::new(0);
383        let elapsed_millis = LocalProperty::new(saturating_u128_to_u64(get_elapsed_millis()));
384        properties::register_time(&elapsed_frames);
385        properties::register_millis(&elapsed_millis);
386        Globals {
387            elapsed_frames,
388            elapsed_millis,
389            viewport: LocalProperty::new(TransformAndBounds {
390                transform: Transform2::identity(),
391                bounds: viewport_size,
392            }),
393            gyro: LocalProperty::new(Gyro::default()),
394            accel: LocalProperty::new(Accel::default()),
395            route_location: LocalProperty::new(RouteLocation::root()),
396            browser_allows_scroller_vector_layers: LocalProperty::new(true),
397            browser_allows_nested_scroller_vector_layers: LocalProperty::new(true),
398            platform,
399            os,
400            target: TargetInfo::new(platform, os),
401            designtime: designtime.clone(),
402            get_elapsed_millis: Rc::from(get_elapsed_millis),
403        }
404    }
405
406    #[cfg(feature = "designtime")]
407    pub fn new_empty_with_designtime(
408        viewport_size: (f64, f64),
409        designtime: Rc<RefCell<DesigntimeManager>>,
410        platform: Platform,
411        os: OS,
412        get_elapsed_millis: Box<dyn Fn() -> u128>,
413        scroller_tiling_policy: layer_tiling::ScrollerTilingPolicy,
414    ) -> Self {
415        let globals =
416            Self::build_globals(viewport_size, designtime, platform, os, get_elapsed_millis);
417        let runtime_context = Rc::new(RuntimeContext::new_empty(globals));
418        PaxEngine {
419            runtime_context,
420            root_expanded_node: None,
421            scroller_tiling_policy,
422        }
423    }
424
425    #[cfg(not(feature = "designtime"))]
426    pub fn new_empty(
427        viewport_size: (f64, f64),
428        platform: Platform,
429        os: OS,
430        get_elapsed_millis: Box<dyn Fn() -> u128>,
431        scroller_tiling_policy: layer_tiling::ScrollerTilingPolicy,
432    ) -> Self {
433        let globals = Self::build_globals(viewport_size, platform, os, get_elapsed_millis);
434        let runtime_context = Rc::new(RuntimeContext::new(globals));
435        PaxEngine {
436            runtime_context,
437            root_expanded_node: None,
438            scroller_tiling_policy,
439        }
440    }
441
442    /// Mount a root component tree into an existing runtime kernel.
443    pub fn mount_root_component(
444        &mut self,
445        main_component_instance: Rc<ComponentInstance>,
446    ) -> Rc<ExpandedNode> {
447        let _graph = self.runtime_context.property_graph.enter();
448        self.unmount();
449        let root_node = ExpandedNode::initialize_root(
450            Rc::clone(&main_component_instance),
451            &self.runtime_context,
452        );
453        self.runtime_context.register_root_expanded_node(&root_node);
454        self.root_expanded_node = Some(Rc::clone(&root_node));
455        root_node
456    }
457
458    /// Detach the mounted root component tree, leaving the runtime kernel empty.
459    pub fn unmount(&mut self) {
460        let _graph = self.runtime_context.property_graph.enter();
461        let Some(root_expanded_node) = self.root_expanded_node.take() else {
462            return;
463        };
464
465        #[cfg(feature = "designtime")]
466        let root_id = root_expanded_node.id;
467        root_expanded_node.recurse_unmount(&self.runtime_context);
468        self.runtime_context.clear_root_expanded_node();
469        self.runtime_context.layer_count.set(0);
470        self.runtime_context.clear_layer_scroller_owners();
471        self.runtime_context.set_root_scroller_id(None);
472        self.runtime_context.clear_visual_viewport_state();
473
474        #[cfg(feature = "designtime")]
475        if self
476            .runtime_context
477            .get_userland_root_expanded_node()
478            .as_ref()
479            .is_some_and(|node| node.id == root_id)
480        {
481            self.runtime_context.set_userland_root_expanded_node(None);
482        }
483    }
484
485    #[cfg(not(feature = "designtime"))]
486    pub fn new(
487        main_component_instance: Rc<ComponentInstance>,
488        viewport_size: (f64, f64),
489        platform: Platform,
490        os: OS,
491        get_elapsed_millis: Box<dyn Fn() -> u128>,
492        scroller_tiling_policy: layer_tiling::ScrollerTilingPolicy,
493    ) -> Self {
494        let mut engine = Self::new_empty(
495            viewport_size,
496            platform,
497            os,
498            get_elapsed_millis,
499            scroller_tiling_policy,
500        );
501        engine.mount_root_component(main_component_instance);
502        engine
503    }
504
505    #[cfg(feature = "designtime")]
506    pub fn new_with_designtime(
507        userland_main_component_instance: Rc<ComponentInstance>,
508        viewport_size: (f64, f64),
509        designtime: Rc<RefCell<DesigntimeManager>>,
510        platform: Platform,
511        os: OS,
512        get_elapsed_millis: Box<dyn Fn() -> u128>,
513        scroller_tiling_policy: layer_tiling::ScrollerTilingPolicy,
514    ) -> Self {
515        let mut engine = Self::new_empty_with_designtime(
516            viewport_size,
517            designtime,
518            platform,
519            os,
520            get_elapsed_millis,
521            scroller_tiling_policy,
522        );
523        engine
524            .runtime_context
525            .set_userland_root_instance_node(Some(userland_main_component_instance.clone()));
526        let root_expanded_node = engine.mount_root_component(userland_main_component_instance);
527        engine
528            .runtime_context
529            .set_userland_root_expanded_node(Some(root_expanded_node));
530        engine
531    }
532
533    #[cfg(feature = "designtime")]
534    pub fn partial_update_expanded_node(&mut self, new_instance: Rc<dyn InstanceNode>) {
535        // update the expanded nodes that just got a new instance node
536        let unique_id = new_instance
537            .base()
538            .template_node_identifier
539            .clone()
540            .expect("new instance node has unique identifier");
541
542        let nodes = self
543            .runtime_context
544            .get_expanded_nodes_by_global_ids(&unique_id);
545        for node in nodes {
546            node.recreate_with_new_data(new_instance.clone(), &self.runtime_context);
547        }
548    }
549
550    #[cfg(feature = "designtime")]
551    pub fn full_reload_userland(&mut self, new_userland_instance: Rc<dyn InstanceNode>) {
552        let node = borrow!(self.runtime_context.userland_root_expanded_node)
553            .as_ref()
554            .map(Rc::clone)
555            .unwrap();
556        *borrow_mut!(self.runtime_context.userland_frame_instance_node) =
557            Some(Rc::clone(&new_userland_instance));
558        node.fully_recreate_with_new_data(new_userland_instance.clone(), &self.runtime_context);
559    }
560
561    // NOTES: this is the order of different things being computed in recurse-expand-nodes
562    // - expanded_node instantiated from instance_node.
563
564    /// Workhorse methods of every tick.  Will be executed up to 240 Hz.
565    /// Three phases:
566    /// 1. Expand nodes & compute properties; recurse entire instance tree and evaluate ExpandedNodes, stitching
567    ///    together parent/child relationships between ExpandedNodes along the way.
568    /// 2. Compute layout (z-index & TransformAndBounds) by visiting ExpandedNode tree
569    ///    in rendering order, writing computed rendering-specific values to ExpandedNodes
570    /// 3. Render:
571    ///     a. find lowest node (last child of last node)
572    ///     b. start rendering, from lowest node on-up, throughout tree
573    pub fn tick(&mut self) -> Vec<NativeMessage> {
574        if matches!(
575            self.runtime_context.application.context().phase(),
576            pax_runtime_api::AppPhase::Closing | pax_runtime_api::AppPhase::Closed
577        ) {
578            return Vec::new();
579        }
580        let _graph = self.runtime_context.property_graph.enter();
581        self.runtime_context.property_graph.import();
582        //
583        // 1. UPDATE NODES (properties, etc.). This part we should be able to
584        // completely remove once reactive properties dirty-dag is a thing.
585        //
586        if let Some(root_expanded_node) = &self.root_expanded_node {
587            root_expanded_node.recurse_update(&self.runtime_context);
588            if self.runtime_context.has_import_settings_nodes() {
589                root_expanded_node.recurse_sync_import_settings(&self.runtime_context);
590            }
591        }
592
593        let ctx = &self.runtime_context;
594        ctx.drain_node_effects();
595        let deliveries = ctx
596            .application
597            .dispatcher()
598            .drain(|| ctx.drain_node_effects());
599        if let Err(error) = deliveries {
600            log::error!("UI completion panicked: {error}");
601            self.shutdown(pax_runtime_api::StopReason::HostClosed);
602            return Vec::new();
603        }
604        self.run_lifecycle_handlers(TICK_HANDLERS, ctx.tick_handler_nodes());
605        ctx.drain_node_effects();
606        self.run_lifecycle_handlers(PRE_RENDER_HANDLERS, ctx.pre_render_handler_nodes());
607        ctx.drain_node_effects();
608
609        let globals = ctx.globals();
610        let time = &globals.elapsed_frames;
611        time.set(time.get() + 1);
612        globals
613            .elapsed_millis
614            .set(saturating_u128_to_u64((globals.get_elapsed_millis)()));
615
616        ctx.flush_custom_events().unwrap();
617
618        // Advancing the clock or dispatching queued events can switch branches.
619        // Settle those effects before final layout and the retained render plan;
620        // otherwise a replay can clear the old subtree but omit its replacement.
621        ctx.drain_node_effects();
622        ctx.prepare_scene_geometry();
623        ctx.dispatch_viewport_proximity();
624        // Handler mutations settle for drawing, but observation samples at most once
625        // per tick. Their invalidations remain pending for the next display tick.
626        // Observation uses layout/scroll geometry, not compositing assignments.
627        // Finalize occlusion only after handlers settle, so animation plus a
628        // proximity callback does not traverse the whole scene twice in one tick.
629        ctx.drain_node_effects();
630        if ctx.take_occlusion_dirty() {
631            if let Some(root) = &self.root_expanded_node {
632                occlusion::update_node_occlusion(root, ctx);
633            }
634            ctx.drain_node_effects();
635        }
636        ctx.prepare_scene_geometry();
637        occlusion::update_native_culling(ctx, self.scroller_tiling_policy);
638        let native_messages = ctx.take_native_messages();
639        native_messages
640    }
641
642    fn run_lifecycle_handlers(&self, handler_key: &str, nodes: Vec<ExpandedNodeIdentifier>) {
643        for node_id in nodes {
644            if let Some(node) = self.runtime_context.get_expanded_node_by_eid(node_id) {
645                node.run_lifecycle_handlers(handler_key, &self.runtime_context);
646            }
647        }
648    }
649
650    fn build_filtered_render_plan(
651        &self,
652        root: &Rc<ExpandedNode>,
653        dirty_node_ids: &[ExpandedNodeIdentifier],
654    ) -> Result<FilteredRenderPlan, &'static str> {
655        let dirty_nodes: HashSet<_> = dirty_node_ids.iter().copied().collect();
656        let mut render_path_nodes = HashSet::new();
657
658        for dirty_node_id in &dirty_nodes {
659            let mut node = self
660                .runtime_context
661                .get_expanded_node_by_eid(*dirty_node_id)
662                .ok_or("missing_dirty_node")?;
663            let mut local_path = HashSet::new();
664            loop {
665                if !local_path.insert(node.id) {
666                    return Err("render_parent_cycle");
667                }
668                if node.is_unclippable() {
669                    return Err("unclippable_path");
670                }
671                render_path_nodes.insert(node.id);
672                if node.id == root.id {
673                    break;
674                }
675                node = node.render_parent_node().ok_or("missing_render_parent")?;
676            }
677        }
678
679        if !render_path_nodes.contains(&root.id) {
680            return Err("missing_root_path");
681        }
682
683        Ok(FilteredRenderPlan {
684            dirty_nodes,
685            render_path_nodes,
686        })
687    }
688
689    fn dirty_layer_can_use_targeted_replay_nodes(
690        &self,
691        layer: usize,
692        dirty_node_ids: &[ExpandedNodeIdentifier],
693        targeted_node_ids: &HashSet<u32>,
694    ) -> bool {
695        for dirty_node_id in dirty_node_ids {
696            let Some(node) = self
697                .runtime_context
698                .get_expanded_node_by_eid(*dirty_node_id)
699            else {
700                return false;
701            };
702            if node.occlusion.get().render_layer_id != layer
703                || borrow!(node.instance_node).base().flags().layer != crate::api::Layer::Canvas
704            {
705                continue;
706            }
707            if !targeted_node_ids.contains(&dirty_node_id.to_u32()) {
708                return false;
709            }
710        }
711        true
712    }
713
714    pub fn render(&mut self, rcs: &mut dyn RenderContext) {
715        if matches!(
716            self.runtime_context.application.context().phase(),
717            pax_runtime_api::AppPhase::Closing | pax_runtime_api::AppPhase::Closed
718        ) {
719            return;
720        }
721        let _graph = self.runtime_context.property_graph.enter();
722        use crate::render_instrumentation::{Phase, Span};
723        let _render_timing = Span::new(Phase::Render);
724        self.update_layer_count(rcs);
725        self.runtime_context.prepare_scene_geometry();
726
727        if !self.runtime_context.has_canvas_render_work() {
728            return;
729        }
730
731        let mut removals = self.runtime_context.take_canvas_node_removals();
732        // Resizing drops truncated layers and their retained nodes together.
733        // Retrying their old removals would keep nonexistent layers dirty forever.
734        removals.retain(|(layer, _)| *layer < self.runtime_context.layer_count.get());
735        let mut dirty_layers = self.runtime_context.dirty_canvas_layers();
736        dirty_layers.extend(removals.iter().map(|(layer, _)| *layer));
737        dirty_layers.sort_unstable();
738        dirty_layers.dedup();
739        let mut lighting_layers = dirty_layers.clone();
740        if self.runtime_context.take_scene_lighting_dirty() {
741            lighting_layers.extend(0..self.runtime_context.layer_count.get());
742            lighting_layers.sort_unstable();
743            lighting_layers.dedup();
744        }
745        for layer in &lighting_layers {
746            let collect_timing = Span::new(Phase::Lighting);
747            let lighting = self
748                .runtime_context
749                .collect_scene_lighting_for_layer(*layer);
750            drop(collect_timing);
751            let _upload_timing = Span::new(Phase::LightingUpload);
752            rcs.set_scene_lighting(*layer, &lighting);
753        }
754        // Slot/scope changes can invalidate node masks during lighting resolution.
755        // Value-only changes must not enter the full-canvas clear/replay path.
756        dirty_layers.extend(self.runtime_context.dirty_canvas_layers());
757        dirty_layers.sort_unstable();
758        dirty_layers.dedup();
759        let prepare_timing = Span::new(Phase::Prepare);
760        let removal_layers: HashSet<_> = removals.iter().map(|(layer, _)| *layer).collect();
761        let targeted_replay_node_ids = self.runtime_context.take_targeted_canvas_replay_node_ids();
762        let dirty_node_ids_before_expansion = self.runtime_context.dirty_canvas_node_ids();
763        let retains_canvas_nodes = rcs.retains_canvas_nodes();
764        let mut retained_full_replay_layers = Vec::new();
765        if retains_canvas_nodes {
766            for layer in &dirty_layers {
767                let has_retained_removal = removal_layers.contains(layer);
768                let has_targeted_conflict =
769                    targeted_replay_node_ids
770                        .get(layer)
771                        .is_some_and(|targeted_node_ids| {
772                            !self.dirty_layer_can_use_targeted_replay_nodes(
773                                *layer,
774                                &dirty_node_ids_before_expansion,
775                                targeted_node_ids,
776                            )
777                        });
778                if has_retained_removal || has_targeted_conflict {
779                    rcs.clear_targeted_replay(*layer);
780                    self.runtime_context
781                        .mark_canvas_nodes_on_layer_dirty(*layer);
782                    retained_full_replay_layers.push(*layer);
783                }
784            }
785        }
786        if !retains_canvas_nodes && (!dirty_layers.is_empty() || !removals.is_empty()) {
787            for layer in &dirty_layers {
788                let can_use_targeted_replay_nodes = targeted_replay_node_ids
789                    .get(layer)
790                    .is_some_and(|targeted_node_ids| {
791                        !removal_layers.contains(layer)
792                            && self.dirty_layer_can_use_targeted_replay_nodes(
793                                *layer,
794                                &dirty_node_ids_before_expansion,
795                                targeted_node_ids,
796                            )
797                    });
798                if !can_use_targeted_replay_nodes {
799                    rcs.clear_targeted_replay(*layer);
800                    self.runtime_context
801                        .mark_canvas_nodes_on_layer_dirty(*layer);
802                }
803            }
804        }
805        let dirty_node_ids = self.runtime_context.dirty_canvas_node_ids();
806        let has_dirty_nodes = !dirty_node_ids.is_empty();
807        let has_node_removals = !removals.is_empty();
808        if !has_dirty_nodes && !has_node_removals {
809            for layer in &dirty_layers {
810                rcs.clear(*layer);
811            }
812        } else if !retains_canvas_nodes {
813            for layer in &dirty_layers {
814                rcs.clear(*layer);
815            }
816        } else {
817            for layer in retained_full_replay_layers {
818                rcs.clear(layer);
819            }
820        }
821
822        let mut failed_removal_layers = Vec::new();
823        for (layer, node_id) in removals {
824            if !rcs.remove_node(layer, node_id) {
825                self.runtime_context
826                    .enqueue_canvas_node_removal(layer, node_id);
827                failed_removal_layers.push(layer);
828            }
829        }
830
831        // This is pretty useful during debugging - left it here since I use it often. /Sam
832        // crate::api::log(&format!("tree: {:#?}", self.root_node));
833        if has_dirty_nodes {
834            if let Some(root_expanded_node) = &self.root_expanded_node {
835                match self.build_filtered_render_plan(root_expanded_node, &dirty_node_ids) {
836                    Ok(plan) => {
837                        let mut stats = expanded_node::FilteredRenderStats::default();
838                        root_expanded_node.recurse_render_filtered(
839                            &self.runtime_context,
840                            rcs,
841                            &plan.render_path_nodes,
842                            &plan.dirty_nodes,
843                            &mut stats,
844                        );
845                        #[cfg(debug_assertions)]
846                        log::trace!(
847                            "[pax-render-filter] dirty_nodes={} path_nodes={} visited_path_nodes={} dirty_nodes_rendered={} skipped_subtrees={}",
848                            plan.dirty_nodes.len(),
849                            plan.render_path_nodes.len(),
850                            stats.path_nodes_visited,
851                            stats.dirty_nodes_rendered,
852                            stats.skipped_subtrees,
853                        );
854                    }
855                    Err(_reason) => {
856                        #[cfg(debug_assertions)]
857                        log::trace!(
858                            "[pax-render-filter] fallback={} dirty_nodes={}",
859                            _reason,
860                            dirty_node_ids.len(),
861                        );
862                        root_expanded_node.recurse_render_queue(&self.runtime_context, rcs);
863                    }
864                }
865            }
866        }
867        self.runtime_context.recurse_flush_queued_renders(rcs);
868
869        self.runtime_context.clear_all_dirty_canvases();
870        for layer in failed_removal_layers {
871            self.runtime_context.set_canvas_dirty(layer);
872        }
873
874        drop(prepare_timing);
875        let _flush_timing = Span::new(Phase::Flush);
876        for layer in lighting_layers {
877            rcs.flush(layer, Rc::clone(&self.runtime_context.dirty_canvases));
878        }
879    }
880
881    pub fn get_expanded_node(&self, id: ExpandedNodeIdentifier) -> Option<Rc<ExpandedNode>> {
882        let val = self.runtime_context.get_expanded_node_by_eid(id).clone();
883        val.map(|v| v.clone())
884    }
885
886    /// Called by chassis when viewport size changes, e.g. with native window resizes
887    pub fn set_viewport_size(&mut self, new_viewport_size: (f64, f64)) {
888        const VIEWPORT_SIZE_EPSILON: f64 = 0.001;
889        let current_viewport_size = self.runtime_context.globals().viewport.get().bounds;
890        if (current_viewport_size.0 - new_viewport_size.0).abs() <= VIEWPORT_SIZE_EPSILON
891            && (current_viewport_size.1 - new_viewport_size.1).abs() <= VIEWPORT_SIZE_EPSILON
892        {
893            return;
894        }
895
896        self.runtime_context.edit_globals(|globals| {
897            globals
898                .viewport
899                .update(|t_and_b| t_and_b.bounds = new_viewport_size);
900        });
901        self.runtime_context.invalidate_viewport_presentation();
902        self.runtime_context.mark_layer_canvas_plans_dirty();
903        self.runtime_context.mark_occlusion_dirty();
904    }
905
906    pub fn update_layer_count(&self, rcs: &mut dyn RenderContext) {
907        let curr_layer_count = self.runtime_context.layer_count.get();
908        // A renderer can be replaced or shared across successive app revisions.
909        // Its actual layer set, not process-global history, determines the resize.
910        if rcs.layers() != curr_layer_count {
911            rcs.resize_layers_to(
912                curr_layer_count,
913                Rc::clone(&self.runtime_context.dirty_canvases),
914            );
915        }
916        self.runtime_context
917            .resize_canvas_layers_to(curr_layer_count);
918    }
919
920    pub fn global_dispatch_focus(&self, args: Focus) -> bool {
921        let Some(root_expanded_node) = &self.root_expanded_node else {
922            return false;
923        };
924        let mut prevent_default = false;
925        root_expanded_node.recurse_visit_postorder(&mut |expanded_node| {
926            prevent_default |= expanded_node.dispatch_focus(
927                Event::new(args.clone()),
928                &self.runtime_context.globals(),
929                &self.runtime_context,
930            );
931        });
932        prevent_default
933    }
934
935    pub fn global_dispatch_select_start(&self, args: SelectStart) -> bool {
936        let Some(root_expanded_node) = &self.root_expanded_node else {
937            return false;
938        };
939        let mut prevent_default = false;
940        root_expanded_node.recurse_visit_postorder(&mut |expanded_node| {
941            prevent_default |= expanded_node.dispatch_select_start(
942                Event::new(args.clone()),
943                &self.runtime_context.globals(),
944                &self.runtime_context,
945            );
946        });
947        prevent_default
948    }
949
950    pub fn global_dispatch_key_down(&self, args: KeyDown) -> bool {
951        let Some(root_expanded_node) = &self.root_expanded_node else {
952            return false;
953        };
954        let mut prevent_default = false;
955        root_expanded_node.recurse_visit_postorder(&mut |expanded_node| {
956            prevent_default |= expanded_node.dispatch_key_down(
957                Event::new(args.clone()),
958                &self.runtime_context.globals(),
959                &self.runtime_context,
960            );
961        });
962        prevent_default
963    }
964
965    pub fn global_dispatch_key_up(&self, args: KeyUp) -> bool {
966        let Some(root_expanded_node) = &self.root_expanded_node else {
967            return false;
968        };
969        let mut prevent_default = false;
970        root_expanded_node.recurse_visit_postorder(&mut |expanded_node| {
971            prevent_default |= expanded_node.dispatch_key_up(
972                Event::new(args.clone()),
973                &self.runtime_context.globals(),
974                &self.runtime_context,
975            );
976        });
977        prevent_default
978    }
979
980    pub fn global_dispatch_key_press(&self, args: KeyPress) -> bool {
981        let Some(root_expanded_node) = &self.root_expanded_node else {
982            return false;
983        };
984        let mut prevent_default = false;
985        root_expanded_node.recurse_visit_postorder(&mut |expanded_node| {
986            prevent_default |= expanded_node.dispatch_key_press(
987                Event::new(args.clone()),
988                &self.runtime_context.globals(),
989                &self.runtime_context,
990            );
991        });
992        prevent_default
993    }
994
995    pub fn global_dispatch_gyro(&self, args: Gyro) -> bool {
996        let mut prevent_default = false;
997        for node_id in self.runtime_context.gyro_handler_nodes() {
998            let Some(expanded_node) = self.runtime_context.get_expanded_node_by_eid(node_id) else {
999                continue;
1000            };
1001            prevent_default |= expanded_node.dispatch_gyro(
1002                Event::new(args.clone()),
1003                &self.runtime_context.globals(),
1004                &self.runtime_context,
1005            );
1006        }
1007        prevent_default
1008    }
1009
1010    pub fn global_dispatch_accel(&self, args: Accel) -> bool {
1011        let mut prevent_default = false;
1012        for node_id in self.runtime_context.accel_handler_nodes() {
1013            let Some(expanded_node) = self.runtime_context.get_expanded_node_by_eid(node_id) else {
1014                continue;
1015            };
1016            prevent_default |= expanded_node.dispatch_accel(
1017                Event::new(args.clone()),
1018                &self.runtime_context.globals(),
1019                &self.runtime_context,
1020            );
1021        }
1022        prevent_default
1023    }
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028    use super::*;
1029    use crate::constants::{ACCEL_HANDLERS, GYRO_HANDLERS, PRE_RENDER_HANDLERS, TICK_HANDLERS};
1030    use crate::{InstanceNode, InstantiationArgs};
1031    use pax_runtime_api::pax_value::{PaxAny, PaxValue};
1032    use std::cell::RefCell;
1033    use std::sync::atomic::{AtomicUsize, Ordering};
1034
1035    static TICK_CALLS: AtomicUsize = AtomicUsize::new(0);
1036    static PRE_RENDER_CALLS: AtomicUsize = AtomicUsize::new(0);
1037    static GYRO_CALLS: AtomicUsize = AtomicUsize::new(0);
1038    static ACCEL_CALLS: AtomicUsize = AtomicUsize::new(0);
1039
1040    fn count_tick_handler(
1041        _properties: Rc<RefCell<PaxAny>>,
1042        _ctx: &NodeContext,
1043        _event: Option<PaxAny>,
1044    ) {
1045        TICK_CALLS.fetch_add(1, Ordering::SeqCst);
1046    }
1047
1048    fn count_pre_render_handler(
1049        _properties: Rc<RefCell<PaxAny>>,
1050        _ctx: &NodeContext,
1051        _event: Option<PaxAny>,
1052    ) {
1053        PRE_RENDER_CALLS.fetch_add(1, Ordering::SeqCst);
1054    }
1055
1056    fn count_gyro_handler(
1057        _properties: Rc<RefCell<PaxAny>>,
1058        _ctx: &NodeContext,
1059        _event: Option<PaxAny>,
1060    ) {
1061        GYRO_CALLS.fetch_add(1, Ordering::SeqCst);
1062    }
1063
1064    fn count_accel_handler(
1065        _properties: Rc<RefCell<PaxAny>>,
1066        _ctx: &NodeContext,
1067        _event: Option<PaxAny>,
1068    ) {
1069        ACCEL_CALLS.fetch_add(1, Ordering::SeqCst);
1070    }
1071
1072    fn empty_component() -> Rc<ComponentInstance> {
1073        <ComponentInstance as InstanceNode>::instantiate(InstantiationArgs {
1074            prototypical_common_properties: crate::CommonPropertiesInit::Default,
1075            prototypical_properties: crate::PropertiesInit::Factory(Box::new(|_, _| {
1076                Some(Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
1077            })),
1078            handler_registry: None,
1079            children: None,
1080            component_template: None,
1081            component_settings: None,
1082            template_node_identifier: None,
1083            template_node_type_id: None,
1084            template_node_selector_info: None,
1085            transition_config: Default::default(),
1086            properties_scope: crate::PropertiesScopeInit::None,
1087        })
1088    }
1089
1090    fn component_with_lifecycle_handlers() -> Rc<ComponentInstance> {
1091        let mut registry = HandlerRegistry::default();
1092        registry.handlers.insert(
1093            TICK_HANDLERS.to_string(),
1094            vec![Handler::new_inline_handler(count_tick_handler)],
1095        );
1096        registry.handlers.insert(
1097            PRE_RENDER_HANDLERS.to_string(),
1098            vec![Handler::new_inline_handler(count_pre_render_handler)],
1099        );
1100
1101        <ComponentInstance as InstanceNode>::instantiate(InstantiationArgs {
1102            prototypical_common_properties: crate::CommonPropertiesInit::Default,
1103            prototypical_properties: crate::PropertiesInit::Factory(Box::new(|_, _| {
1104                Some(Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
1105            })),
1106            handler_registry: Some(Rc::new(RefCell::new(registry))),
1107            children: None,
1108            component_template: None,
1109            component_settings: None,
1110            template_node_identifier: None,
1111            template_node_type_id: None,
1112            template_node_selector_info: None,
1113            transition_config: Default::default(),
1114            properties_scope: crate::PropertiesScopeInit::None,
1115        })
1116    }
1117
1118    fn component_with_sensor_handlers() -> Rc<ComponentInstance> {
1119        let mut registry = HandlerRegistry::default();
1120        registry.handlers.insert(
1121            GYRO_HANDLERS.to_string(),
1122            vec![Handler::new_inline_handler(count_gyro_handler)],
1123        );
1124        registry.handlers.insert(
1125            ACCEL_HANDLERS.to_string(),
1126            vec![Handler::new_inline_handler(count_accel_handler)],
1127        );
1128
1129        <ComponentInstance as InstanceNode>::instantiate(InstantiationArgs {
1130            prototypical_common_properties: crate::CommonPropertiesInit::Default,
1131            prototypical_properties: crate::PropertiesInit::Factory(Box::new(|_, _| {
1132                Some(Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
1133            })),
1134            handler_registry: Some(Rc::new(RefCell::new(registry))),
1135            children: None,
1136            component_template: None,
1137            component_settings: None,
1138            template_node_identifier: None,
1139            template_node_type_id: None,
1140            template_node_selector_info: None,
1141            transition_config: Default::default(),
1142            properties_scope: crate::PropertiesScopeInit::None,
1143        })
1144    }
1145
1146    fn object_field(value: PaxValue, field: &str) -> PaxValue {
1147        let PaxValue::Object(fields) = value else {
1148            panic!("expected object value");
1149        };
1150        fields
1151            .into_iter()
1152            .find_map(|(name, value)| (name == field).then_some(value))
1153            .unwrap_or_else(|| panic!("expected object field {field}"))
1154    }
1155
1156    #[test]
1157    fn empty_engine_can_tick_and_mount_later() {
1158        let mut engine = crate::test_support::empty_engine(
1159            (320.0, 240.0),
1160            Platform::Web,
1161            OS::Mac,
1162            Box::new(|| 0),
1163            layer_tiling::ScrollerTilingPolicy::default(),
1164        );
1165
1166        assert!(engine.root_expanded_node.is_none());
1167        assert!(engine.runtime_context.get_root_expanded_node().is_none());
1168        assert!(engine.tick().is_empty());
1169
1170        let root = engine.mount_root_component(empty_component());
1171        assert_eq!(
1172            engine.root_expanded_node.as_ref().map(|node| node.id),
1173            Some(root.id)
1174        );
1175        assert_eq!(
1176            engine
1177                .runtime_context
1178                .get_root_expanded_node()
1179                .map(|node| node.id),
1180            Some(root.id)
1181        );
1182        let _ = engine.tick();
1183    }
1184
1185    #[test]
1186    fn unmount_clears_registered_root() {
1187        let mut engine = crate::test_support::empty_engine(
1188            (320.0, 240.0),
1189            Platform::Web,
1190            OS::Mac,
1191            Box::new(|| 0),
1192            layer_tiling::ScrollerTilingPolicy::default(),
1193        );
1194
1195        engine.mount_root_component(empty_component());
1196        engine.unmount();
1197
1198        assert!(engine.root_expanded_node.is_none());
1199        assert!(engine.runtime_context.get_root_expanded_node().is_none());
1200    }
1201
1202    #[test]
1203    fn globals_expose_sensor_values_to_stack_frame() {
1204        let engine = crate::test_support::empty_engine(
1205            (320.0, 240.0),
1206            Platform::Web,
1207            OS::Mac,
1208            Box::new(|| 0),
1209            layer_tiling::ScrollerTilingPolicy::default(),
1210        );
1211        let globals = engine.runtime_context.globals();
1212        globals.gyro.set(Gyro {
1213            x: 1.0,
1214            y: 2.0,
1215            z: 3.0,
1216        });
1217        globals.accel.set(Accel {
1218            x: 4.0,
1219            y: 5.0,
1220            z: 6.0,
1221        });
1222
1223        assert_eq!(
1224            globals
1225                .stack_frame()
1226                .resolve_symbol("$gyro")
1227                .unwrap()
1228                .get_as_pax_value(),
1229            PaxValue::Object(
1230                vec![
1231                    ("x".to_string(), PaxValue::Numeric(1.0.into())),
1232                    ("y".to_string(), PaxValue::Numeric(2.0.into())),
1233                    ("z".to_string(), PaxValue::Numeric(3.0.into())),
1234                ]
1235                .into_iter()
1236                .collect(),
1237            )
1238        );
1239        assert_eq!(
1240            globals
1241                .stack_frame()
1242                .resolve_symbol("$accel")
1243                .unwrap()
1244                .get_as_pax_value(),
1245            PaxValue::Object(
1246                vec![
1247                    ("x".to_string(), PaxValue::Numeric(4.0.into())),
1248                    ("y".to_string(), PaxValue::Numeric(5.0.into())),
1249                    ("z".to_string(), PaxValue::Numeric(6.0.into())),
1250                ]
1251                .into_iter()
1252                .collect(),
1253            )
1254        );
1255    }
1256
1257    #[test]
1258    fn lifecycle_handlers_run_once_per_tick() {
1259        TICK_CALLS.store(0, Ordering::SeqCst);
1260        PRE_RENDER_CALLS.store(0, Ordering::SeqCst);
1261        let mut engine = crate::test_support::empty_engine(
1262            (320.0, 240.0),
1263            Platform::Web,
1264            OS::Mac,
1265            Box::new(|| 0),
1266            layer_tiling::ScrollerTilingPolicy::default(),
1267        );
1268
1269        engine.mount_root_component(component_with_lifecycle_handlers());
1270        let _ = engine.tick();
1271
1272        assert_eq!(TICK_CALLS.load(Ordering::SeqCst), 1);
1273        assert_eq!(PRE_RENDER_CALLS.load(Ordering::SeqCst), 1);
1274    }
1275
1276    #[test]
1277    fn sensor_handlers_dispatch_through_registered_nodes() {
1278        GYRO_CALLS.store(0, Ordering::SeqCst);
1279        ACCEL_CALLS.store(0, Ordering::SeqCst);
1280        let mut engine = crate::test_support::empty_engine(
1281            (320.0, 240.0),
1282            Platform::Web,
1283            OS::Mac,
1284            Box::new(|| 0),
1285            layer_tiling::ScrollerTilingPolicy::default(),
1286        );
1287
1288        assert!(!engine.global_dispatch_gyro(Gyro::default()));
1289        assert!(!engine.global_dispatch_accel(Accel::default()));
1290        engine.mount_root_component(component_with_sensor_handlers());
1291        assert!(!engine.global_dispatch_gyro(Gyro::default()));
1292        assert!(!engine.global_dispatch_accel(Accel::default()));
1293
1294        assert_eq!(GYRO_CALLS.load(Ordering::SeqCst), 1);
1295        assert_eq!(ACCEL_CALLS.load(Ordering::SeqCst), 1);
1296    }
1297
1298    #[test]
1299    fn globals_expose_clock_aliases_to_stack_frame() {
1300        let engine = crate::test_support::empty_engine(
1301            (320.0, 240.0),
1302            Platform::Web,
1303            OS::Mac,
1304            Box::new(|| 123),
1305            layer_tiling::ScrollerTilingPolicy::default(),
1306        );
1307        let globals = engine.runtime_context.globals();
1308        let stack = globals.stack_frame();
1309
1310        assert_eq!(
1311            stack.resolve_symbol("$frames").unwrap().get_as_pax_value(),
1312            PaxValue::Numeric(0.into())
1313        );
1314        assert_eq!(
1315            stack.resolve_symbol("$millis").unwrap().get_as_pax_value(),
1316            PaxValue::Numeric(123.into())
1317        );
1318        let old_frames_symbol = format!("{}{}", "$frames", "_elapsed");
1319        let old_millis_symbol = format!("{}{}", "$elapsed", "_millis");
1320        assert!(stack.resolve_symbol(&old_frames_symbol).is_none());
1321        assert!(stack.resolve_symbol(&old_millis_symbol).is_none());
1322    }
1323
1324    #[test]
1325    fn globals_expose_target_aliases_to_stack_frame() {
1326        let engine = crate::test_support::empty_engine(
1327            (320.0, 240.0),
1328            Platform::Native,
1329            OS::IPad,
1330            Box::new(|| 0),
1331            layer_tiling::ScrollerTilingPolicy::default(),
1332        );
1333        let globals = engine.runtime_context.globals();
1334        let stack = globals.stack_frame();
1335
1336        assert_eq!(
1337            stack.resolve_symbol("$ios").unwrap().get_as_pax_value(),
1338            PaxValue::Bool(true)
1339        );
1340        assert_eq!(
1341            stack.resolve_symbol("$ipad").unwrap().get_as_pax_value(),
1342            PaxValue::Bool(true)
1343        );
1344        assert_eq!(
1345            stack.resolve_symbol("$iphone").unwrap().get_as_pax_value(),
1346            PaxValue::Bool(false)
1347        );
1348        assert_eq!(
1349            stack.resolve_symbol("$native").unwrap().get_as_pax_value(),
1350            PaxValue::Bool(true)
1351        );
1352        assert_eq!(
1353            object_field(
1354                stack.resolve_symbol("$target").unwrap().get_as_pax_value(),
1355                "ipad"
1356            ),
1357            PaxValue::Bool(true)
1358        );
1359    }
1360
1361    #[test]
1362    fn globals_expose_viewport_orientation_aliases_to_stack_frame() {
1363        let engine = crate::test_support::empty_engine(
1364            (320.0, 240.0),
1365            Platform::Web,
1366            OS::Mac,
1367            Box::new(|| 0),
1368            layer_tiling::ScrollerTilingPolicy::default(),
1369        );
1370        let globals = engine.runtime_context.globals();
1371        let stack = globals.stack_frame();
1372
1373        assert_eq!(
1374            stack
1375                .resolve_symbol("$landscape")
1376                .unwrap()
1377                .get_as_pax_value(),
1378            PaxValue::Bool(true)
1379        );
1380        assert_eq!(
1381            stack.resolve_symbol("$major").unwrap().get_as_pax_value(),
1382            PaxValue::Numeric(320.0.into())
1383        );
1384        assert_eq!(
1385            object_field(
1386                stack
1387                    .resolve_symbol("$viewport")
1388                    .unwrap()
1389                    .get_as_pax_value(),
1390                "minor"
1391            ),
1392            PaxValue::Numeric(240.0.into())
1393        );
1394
1395        globals.viewport.update(|viewport| {
1396            viewport.bounds = (240.0, 320.0);
1397        });
1398
1399        assert_eq!(
1400            stack
1401                .resolve_symbol("$portrait")
1402                .unwrap()
1403                .get_as_pax_value(),
1404            PaxValue::Bool(true)
1405        );
1406        assert_eq!(
1407            stack.resolve_symbol("$minor").unwrap().get_as_pax_value(),
1408            PaxValue::Numeric(240.0.into())
1409        );
1410    }
1411}
1412
1413impl Drop for PaxEngine {
1414    fn drop(&mut self) {
1415        self.shutdown(pax_runtime_api::StopReason::HostClosed);
1416    }
1417}