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;
25pub mod pax_gpu_render_context;
27pub mod piet_render_context;
28#[cfg(test)]
29mod render_lifecycle_tests;
30
31mod 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)]
93pub 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 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
251pub 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
258pub enum HandlerLocation {
260 Inline,
262 Component,
264}
265
266pub struct Handler {
268 pub function: fn(Rc<RefCell<PaxAny>>, &NodeContext, Option<PaxAny>),
269 pub location: HandlerLocation,
270}
271
272impl Handler {
273 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 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
295pub 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
308impl PaxEngine {
312 pub fn activate_application(&self) {
315 self.runtime_context.application.activate();
316 }
317
318 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 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 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 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 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 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 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 ctx.drain_node_effects();
622 ctx.prepare_scene_geometry();
623 ctx.dispatch_viewport_proximity();
624 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 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 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 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 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 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}