Skip to main content

pax_runtime/engine/
mod.rs

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