1use std::collections::HashMap;
4use std::iter;
5use std::ops::Range;
6use std::rc::Rc;
7use_RefCell!();
8
9use pax_manifest::ControlFlowRouteBranchDefinition;
10use pax_runtime_api::pax_value::ImplToFromPaxAny;
11use pax_runtime_api::{
12 borrow, borrow_mut, use_RefCell, Interpolatable, PaxValue, Property, ToPaxValue, Variable,
13};
14
15use crate::api::Layer;
16use crate::{
17 BaseInstance, ExpandedNode, InstanceFlags, InstanceNode, InstantiationArgs, RuntimeContext,
18 RuntimePropertiesStackFrame,
19};
20
21pub const INTERNAL_ROUTE_LOCATION_SYMBOL: &str = "$route_location";
23pub const INTERNAL_ROUTE_MATCH_SYMBOL: &str = "$route_match";
25const ROUTE_SYMBOL: &str = "route";
26
27#[derive(Clone, Debug, Default, PartialEq, Eq)]
32pub struct RouteLocation {
33 pub path_segments: Vec<String>,
35 pub query: HashMap<String, Vec<String>>,
37 pub fragment: Option<String>,
39}
40
41impl RouteLocation {
42 pub fn root() -> Self {
44 Self::default()
45 }
46
47 pub fn with_path_segments(&self, path_segments: Vec<String>) -> Self {
49 Self {
50 path_segments,
51 query: self.query.clone(),
52 fragment: self.fragment.clone(),
53 }
54 }
55}
56
57impl From<pax_message::RouteChangeInterruptArgs> for RouteLocation {
58 fn from(value: pax_message::RouteChangeInterruptArgs) -> Self {
59 Self {
60 path_segments: value.path_segments,
61 query: value.query,
62 fragment: value.fragment,
63 }
64 }
65}
66
67impl From<&pax_message::RouteChangeInterruptArgs> for RouteLocation {
68 fn from(value: &pax_message::RouteChangeInterruptArgs) -> Self {
69 Self {
70 path_segments: value.path_segments.clone(),
71 query: value.query.clone(),
72 fragment: value.fragment.clone(),
73 }
74 }
75}
76
77impl Interpolatable for RouteLocation {}
78
79impl ToPaxValue for RouteLocation {
80 fn to_pax_value(self) -> PaxValue {
81 PaxValue::Object(vec![
82 (
83 "path_segments".to_string(),
84 self.path_segments.to_pax_value(),
85 ),
86 ("query".to_string(), multimap_to_pax_value(&self.query)),
87 ("fragment".to_string(), self.fragment.to_pax_value()),
88 ])
89 }
90}
91
92#[derive(Clone, Debug, Default, PartialEq, Eq)]
94pub struct RouteMatch {
95 pub location: RouteLocation,
97 pub global_location: RouteLocation,
99 pub params: HashMap<String, String>,
101 pub consumed_segments: usize,
103 pub remainder: Vec<String>,
105 pub is_exact: bool,
107}
108
109impl RouteMatch {
110 pub fn remainder_location(&self) -> RouteLocation {
112 self.location.with_path_segments(self.remainder.clone())
113 }
114
115 fn default_match(location: &RouteLocation, global_location: &RouteLocation) -> Self {
116 Self {
117 location: location.clone(),
118 global_location: global_location.clone(),
119 params: HashMap::new(),
120 consumed_segments: 0,
121 remainder: location.path_segments.clone(),
122 is_exact: location.path_segments.is_empty(),
123 }
124 }
125}
126
127impl Interpolatable for RouteMatch {}
128
129impl ToPaxValue for RouteMatch {
130 fn to_pax_value(self) -> PaxValue {
131 PaxValue::Object(vec![
132 ("location".to_string(), self.location.to_pax_value()),
133 (
134 "global_location".to_string(),
135 self.global_location.to_pax_value(),
136 ),
137 ("params".to_string(), string_map_to_pax_value(&self.params)),
138 (
139 "consumed_segments".to_string(),
140 self.consumed_segments.to_pax_value(),
141 ),
142 ("remainder".to_string(), self.remainder.to_pax_value()),
143 ("is_exact".to_string(), self.is_exact.to_pax_value()),
144 ])
145 }
146}
147
148#[derive(Default)]
150pub struct RouterProperties {
151 pub input_location: Property<RouteLocation>,
153 pub global_location: Property<RouteLocation>,
155}
156
157impl ImplToFromPaxAny for RouterProperties {}
158
159impl ToPaxValue for RouterProperties {
160 fn to_pax_value(self) -> PaxValue {
161 PaxValue::Object(vec![
162 (
163 "input_location".to_string(),
164 self.input_location.to_pax_value(),
165 ),
166 (
167 "global_location".to_string(),
168 self.global_location.to_pax_value(),
169 ),
170 ])
171 }
172}
173
174#[derive(Clone, Debug, PartialEq, Eq)]
175enum RouteSegmentPattern {
176 Literal(String),
177 Param(String),
178 CatchAll,
179}
180
181#[derive(Clone, Debug)]
183pub struct CompiledRouteBranch {
184 pub default: bool,
186 pub modal: bool,
188 pattern: Vec<RouteSegmentPattern>,
189}
190
191impl CompiledRouteBranch {
192 fn from_definition(definition: &ControlFlowRouteBranchDefinition) -> Self {
193 Self {
194 default: definition.default,
195 modal: definition.modal,
196 pattern: definition
197 .path
198 .as_deref()
199 .map(compile_route_pattern)
200 .unwrap_or_default(),
201 }
202 }
203}
204
205#[derive(Clone, Debug, Default, PartialEq, Eq)]
206struct RouteSelection {
207 branch_index: Option<usize>,
208 route_match: Option<RouteMatch>,
209}
210
211pub struct RouterInstance {
212 base: BaseInstance,
213 branches: Vec<CompiledRouteBranch>,
214 branch_child_ranges: Vec<Range<usize>>,
215}
216
217impl InstanceNode for RouterInstance {
218 fn instantiate(args: InstantiationArgs) -> Rc<Self>
219 where
220 Self: Sized,
221 {
222 Rc::new(Self {
223 base: BaseInstance::new(
224 args,
225 InstanceFlags {
226 invisible_to_slot: true,
227 invisible_to_raycasting: true,
228 layer: Layer::DontCare,
229 is_component: false,
230 is_slot: false,
231 },
232 ),
233 branches: vec![],
234 branch_child_ranges: vec![],
235 })
236 }
237
238 fn handle_mount(
239 self: Rc<Self>,
240 expanded_node: &Rc<ExpandedNode>,
241 context: &Rc<RuntimeContext>,
242 ) {
243 self.handle_setup(expanded_node, context, true);
244 }
245
246 fn handle_control_flow_node_expansion(
247 self: Rc<Self>,
248 expanded_node: &Rc<ExpandedNode>,
249 context: &Rc<RuntimeContext>,
250 ) {
251 self.handle_setup(expanded_node, context, false);
252 }
253
254 fn resolve_debug(
255 &self,
256 f: &mut std::fmt::Formatter,
257 _expanded_node: Option<&ExpandedNode>,
258 ) -> std::fmt::Result {
259 f.debug_struct("Router").finish()
260 }
261
262 fn base(&self) -> &BaseInstance {
263 &self.base
264 }
265}
266
267impl RouterInstance {
268 pub fn instantiate_with_branches(
270 args: InstantiationArgs,
271 branches: Vec<CompiledRouteBranch>,
272 branch_child_ranges: Vec<Range<usize>>,
273 ) -> Rc<Self> {
274 Rc::new(Self {
275 base: BaseInstance::new(
276 args,
277 InstanceFlags {
278 invisible_to_slot: true,
279 invisible_to_raycasting: true,
280 layer: Layer::DontCare,
281 is_component: false,
282 is_slot: false,
283 },
284 ),
285 branches,
286 branch_child_ranges,
287 })
288 }
289
290 fn handle_setup(
291 self: Rc<Self>,
292 expanded_node: &Rc<ExpandedNode>,
293 context: &Rc<RuntimeContext>,
294 is_mount: bool,
295 ) {
296 let weak_ref_self = Rc::downgrade(expanded_node);
297 let cloned_self = Rc::clone(&self);
298 let cloned_context = Rc::clone(context);
299
300 let input_location =
301 expanded_node.with_properties_unwrapped(|properties: &mut RouterProperties| {
302 properties.input_location.clone()
303 });
304 let global_location =
305 expanded_node.with_properties_unwrapped(|properties: &mut RouterProperties| {
306 properties.global_location.clone()
307 });
308
309 let deps = vec![input_location.untyped(), global_location.untyped()];
310 let branches = self.branches.clone();
311 let branch_child_ranges = self.branch_child_ranges.clone();
312 let old_selection = RefCell::new(RouteSelection::default());
313
314 expanded_node
315 .children
316 .replace_with(Property::computed_with_name(
317 move || {
318 let Some(cloned_expanded_node) = weak_ref_self.upgrade() else {
319 panic!("ran evaluator after expanded node dropped (router)")
320 };
321
322 let input = input_location.get();
323 let global = global_location.get();
324 let selection = select_route_branch(&branches, &input, &global);
325
326 if *borrow!(old_selection) == selection {
327 return cloned_expanded_node.current_attached_children();
328 }
329 *borrow_mut!(old_selection) = selection.clone();
330
331 let selected_branch = selection.branch_index.and_then(|branch_index| {
332 branches
333 .get(branch_index)
334 .map(|branch| (branch_index, branch))
335 });
336 let selected_range = selected_branch
337 .and_then(|(branch_index, _)| branch_child_ranges.get(branch_index))
338 .cloned()
339 .unwrap_or(0..0);
340 let children = borrow!(cloned_self.base().get_instance_children());
341 let start = selected_range.start.min(children.len());
342 let end = selected_range.end.min(children.len());
343 let route_match = selection.route_match.clone();
344 let env = route_match
345 .clone()
346 .map(route_scope)
347 .map(|scope| cloned_expanded_node.stack.push(scope))
348 .unwrap_or_else(|| Rc::clone(&cloned_expanded_node.stack));
349 let selected_range = start..end;
350 let active_children = borrow!(cloned_expanded_node.active_children).clone();
351 let selected_children = Self::matching_children_for_range(
352 &cloned_expanded_node,
353 &active_children,
354 &children,
355 &selected_range,
356 route_match.as_ref(),
357 &env,
358 &cloned_context,
359 is_mount,
360 );
361
362 let mut new_active_children = selected_children;
363 if selected_branch
364 .map(|(_, branch)| branch.modal)
365 .unwrap_or(false)
366 {
367 for child in active_children {
368 if !new_active_children
369 .iter()
370 .any(|selected| Rc::ptr_eq(selected, &child))
371 {
372 new_active_children.push(child);
373 }
374 }
375 }
376
377 if is_mount {
378 cloned_expanded_node.attach_children(
379 new_active_children,
380 &cloned_context,
381 &cloned_expanded_node.parent_frame,
382 )
383 } else {
384 new_active_children
385 }
386 },
387 &deps,
388 &format!("router_children (node id: {})", expanded_node.id.0),
389 ));
390 }
391
392 fn matching_children_for_range(
393 expanded_node: &Rc<ExpandedNode>,
394 active_children: &[Rc<ExpandedNode>],
395 templates: &[Rc<dyn InstanceNode>],
396 range: &Range<usize>,
397 route_match: Option<&RouteMatch>,
398 env: &Rc<RuntimePropertiesStackFrame>,
399 context: &Rc<RuntimeContext>,
400 is_mount: bool,
401 ) -> Vec<Rc<ExpandedNode>> {
402 let mut selected = Vec::new();
403 for template in templates[range.clone()].iter() {
404 let active = active_children
405 .iter()
406 .find(|child| {
407 !selected.iter().any(|selected| Rc::ptr_eq(selected, child))
408 && Self::child_uses_template(child, template)
409 && Self::same_route_instance(
410 Self::child_route_match(child).as_ref(),
411 route_match,
412 )
413 })
414 .cloned();
415 let rescued = active.or_else(|| {
416 (is_mount && expanded_node.attached.get() > 0)
417 .then(|| {
418 expanded_node.rescue_exiting_child_matching(|child| {
419 Self::child_uses_template(child, template)
420 && Self::same_route_instance(
421 Self::child_route_match(child).as_ref(),
422 route_match,
423 )
424 })
425 })
426 .flatten()
427 });
428 let child = rescued.unwrap_or_else(|| {
429 expanded_node
430 .create_children_detached(
431 iter::once((Rc::clone(template), Rc::clone(env))),
432 context,
433 &Rc::downgrade(expanded_node),
434 )
435 .pop()
436 .expect("route child creation returned no child")
437 });
438 if let Some(route_match) = route_match {
439 Self::set_child_route_match(&child, route_match.clone());
440 }
441 if !is_mount && child.attached.get() == 0 {
442 child.recurse_control_flow_expansion(context);
443 }
444 selected.push(child);
445 }
446 selected
447 }
448
449 fn child_uses_template(child: &Rc<ExpandedNode>, template: &Rc<dyn InstanceNode>) -> bool {
450 let child_template = borrow!(child.instance_node).clone();
451 Rc::ptr_eq(&child_template, template)
452 }
453
454 fn child_route_match(child: &Rc<ExpandedNode>) -> Option<RouteMatch> {
455 Self::child_route_match_property(child).map(|route_match| route_match.get())
456 }
457
458 fn child_route_match_property(child: &Rc<ExpandedNode>) -> Option<Property<RouteMatch>> {
459 child
460 .stack
461 .resolve_symbol_as_erased_property(INTERNAL_ROUTE_MATCH_SYMBOL)
462 .map(Property::<RouteMatch>::new_from_untyped)
463 }
464
465 fn same_route_instance(lhs: Option<&RouteMatch>, rhs: Option<&RouteMatch>) -> bool {
466 match (lhs, rhs) {
469 (Some(lhs), Some(rhs)) => lhs.params == rhs.params,
470 (None, None) => true,
471 _ => false,
472 }
473 }
474
475 fn set_child_route_match(child: &Rc<ExpandedNode>, route_match: RouteMatch) {
476 if let Some(property) = Self::child_route_match_property(child) {
477 property.set_if_neq(route_match);
478 }
479 }
480}
481
482pub fn compile_route_branches(
483 definitions: &[ControlFlowRouteBranchDefinition],
484) -> Vec<CompiledRouteBranch> {
485 definitions
486 .iter()
487 .map(CompiledRouteBranch::from_definition)
488 .collect()
489}
490
491fn compile_route_pattern(path: &str) -> Vec<RouteSegmentPattern> {
492 if path.is_empty() {
493 panic!("Route path must not be empty");
494 }
495
496 let normalized = if path == "/" {
497 "/"
498 } else {
499 path.trim_start_matches('/').trim_end_matches('/')
500 };
501
502 if normalized.is_empty() {
503 return vec![];
504 }
505
506 let segments = normalized
507 .split('/')
508 .filter(|segment| !segment.is_empty())
509 .collect::<Vec<_>>();
510 let mut pattern = Vec::new();
511 for (index, segment) in segments.iter().enumerate() {
512 if *segment == "*" {
513 if index != segments.len() - 1 {
514 panic!("Route catch-all '*' must be terminal");
515 }
516 pattern.push(RouteSegmentPattern::CatchAll);
517 } else if let Some(param_name) = segment.strip_prefix(':') {
518 if param_name.is_empty() {
519 panic!("Route params must be named");
520 }
521 pattern.push(RouteSegmentPattern::Param(param_name.to_string()));
522 } else if segment.contains('*') {
523 panic!("Route only supports terminal '*'");
524 } else {
525 pattern.push(RouteSegmentPattern::Literal(segment.to_string()));
526 }
527 }
528
529 pattern
530}
531
532fn select_route_branch(
533 branches: &[CompiledRouteBranch],
534 input: &RouteLocation,
535 global: &RouteLocation,
536) -> RouteSelection {
537 let mut default_index = None;
538
539 for (index, branch) in branches.iter().enumerate() {
540 if branch.default {
541 default_index.get_or_insert(index);
542 continue;
543 }
544
545 if let Some(route_match) = match_route_branch(branch, input, global) {
546 return RouteSelection {
547 branch_index: Some(index),
548 route_match: Some(route_match),
549 };
550 }
551 }
552
553 default_index
554 .map(|index| RouteSelection {
555 branch_index: Some(index),
556 route_match: Some(RouteMatch::default_match(input, global)),
557 })
558 .unwrap_or_default()
559}
560
561fn match_route_branch(
562 branch: &CompiledRouteBranch,
563 input: &RouteLocation,
564 global: &RouteLocation,
565) -> Option<RouteMatch> {
566 let mut params = HashMap::new();
567 let mut input_index = 0usize;
568
569 for (pattern_index, segment_pattern) in branch.pattern.iter().enumerate() {
570 match segment_pattern {
571 RouteSegmentPattern::Literal(expected) => {
572 let actual = input.path_segments.get(input_index)?;
573 if actual != expected {
574 return None;
575 }
576 input_index += 1;
577 }
578 RouteSegmentPattern::Param(name) => {
579 let actual = input.path_segments.get(input_index)?;
580 params.insert(name.clone(), actual.clone());
581 input_index += 1;
582 }
583 RouteSegmentPattern::CatchAll => {
584 if pattern_index != branch.pattern.len() - 1 {
585 return None;
586 }
587
588 let remainder = input.path_segments[input_index..].to_vec();
589 return Some(RouteMatch {
590 location: input.clone(),
591 global_location: global.clone(),
592 params,
593 consumed_segments: input_index,
594 is_exact: remainder.is_empty(),
595 remainder,
596 });
597 }
598 }
599 }
600
601 if input_index != input.path_segments.len() {
602 return None;
603 }
604
605 Some(RouteMatch {
606 location: input.clone(),
607 global_location: global.clone(),
608 params,
609 consumed_segments: input_index,
610 remainder: vec![],
611 is_exact: true,
612 })
613}
614
615fn route_scope(route_match: RouteMatch) -> HashMap<String, Variable> {
616 let route_property = Property::new(route_match);
617 vec![
618 (
619 ROUTE_SYMBOL.to_string(),
620 Variable::new_from_typed_property(route_property.clone()),
621 ),
622 (
623 INTERNAL_ROUTE_MATCH_SYMBOL.to_string(),
624 Variable::new_from_typed_property(route_property),
625 ),
626 ]
627 .into_iter()
628 .collect()
629}
630
631fn multimap_to_pax_value(map: &HashMap<String, Vec<String>>) -> PaxValue {
632 let mut entries = map
633 .iter()
634 .map(|(key, values)| (key.clone(), values.clone().to_pax_value()))
635 .collect::<Vec<_>>();
636 entries.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0));
637 PaxValue::Object(entries)
638}
639
640fn string_map_to_pax_value(map: &HashMap<String, String>) -> PaxValue {
641 let mut entries = map
642 .iter()
643 .map(|(key, value)| (key.clone(), value.clone().to_pax_value()))
644 .collect::<Vec<_>>();
645 entries.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0));
646 PaxValue::Object(entries)
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652 use crate::api::math::{Point2, Transform2};
653 use crate::api::{CommonProperties, Layer, Size, Window};
654 use crate::{
655 BaseInstance, ComponentInstance, Globals, InstanceFlags, RuntimePropertiesStackFrame,
656 TransformAndBounds,
657 };
658 use pax_manifest::cartridge_generation::{
659 ComponentTransitionConfig, TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT,
660 TRANSITION_PHASE_IDLE,
661 };
662 use pax_manifest::ValueDefinition;
663 use pax_runtime_api::pax_value::{PaxAny, ToFromPaxAny};
664 use pax_runtime_api::{Duration, Platform, TargetInfo, OS};
665 use std::cell::RefCell;
666
667 fn route(path_segments: &[&str]) -> RouteLocation {
668 RouteLocation {
669 path_segments: path_segments
670 .iter()
671 .map(|segment| (*segment).to_string())
672 .collect(),
673 ..Default::default()
674 }
675 }
676
677 fn test_globals() -> Globals {
678 Globals {
679 elapsed_frames: Property::new(0),
680 elapsed_millis: Property::new(0),
681 viewport: Property::new(TransformAndBounds {
682 transform: Transform2::identity(),
683 bounds: (100.0, 100.0),
684 }),
685 gyro: Property::new(Default::default()),
686 accel: Property::new(Default::default()),
687 route_location: Property::new(RouteLocation::root()),
688 browser_allows_scroller_vector_layers: Property::new(true),
689 browser_allows_nested_scroller_vector_layers: Property::new(true),
690 platform: Platform::Unknown,
691 os: OS::Unknown,
692 target: TargetInfo::new(Platform::Unknown, OS::Unknown),
693 get_elapsed_millis: Rc::new(|| 0),
694 }
695 }
696
697 fn default_properties_factory() -> Box<
698 dyn Fn(
699 Rc<RuntimePropertiesStackFrame>,
700 Option<Rc<ExpandedNode>>,
701 ) -> Option<Rc<RefCell<PaxAny>>>,
702 > {
703 Box::new(|_, expanded_node| {
704 expanded_node
705 .is_none()
706 .then(|| Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
707 })
708 }
709
710 fn default_common_properties_factory() -> Box<
711 dyn Fn(
712 Rc<RuntimePropertiesStackFrame>,
713 Option<Rc<ExpandedNode>>,
714 ) -> Option<Rc<RefCell<CommonProperties>>>,
715 > {
716 Box::new(|_, expanded_node| {
717 expanded_node
718 .is_none()
719 .then(|| Rc::new(RefCell::new(CommonProperties::default())))
720 })
721 }
722
723 fn component_args(template: Option<Vec<Rc<dyn InstanceNode>>>) -> InstantiationArgs {
724 InstantiationArgs {
725 prototypical_common_properties: crate::CommonPropertiesInit::Factory(
726 default_common_properties_factory(),
727 ),
728 prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
729 handler_registry: None,
730 children: None,
731 component_template: template.map(RefCell::new),
732 component_settings: None,
733 template_node_identifier: None,
734 template_node_type_id: None,
735 template_node_selector_info: None,
736 transition_config: Default::default(),
737 properties_scope: crate::PropertiesScopeInit::None,
738 }
739 }
740
741 fn leaf_args() -> InstantiationArgs {
742 InstantiationArgs {
743 prototypical_common_properties: crate::CommonPropertiesInit::Factory(
744 default_common_properties_factory(),
745 ),
746 prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
747 handler_registry: None,
748 children: None,
749 component_template: Some(RefCell::new(Vec::new())),
750 component_settings: None,
751 template_node_identifier: None,
752 template_node_type_id: None,
753 template_node_selector_info: None,
754 transition_config: ComponentTransitionConfig {
755 has_enter: true,
756 enter_frame_count: 10,
757 has_exit: true,
758 exit_frame_count: 10,
759 timeout_ms: 5_000,
760 ..Default::default()
761 },
762 properties_scope: crate::PropertiesScopeInit::None,
763 }
764 }
765
766 fn leaf() -> Rc<dyn InstanceNode> {
767 ComponentInstance::instantiate(leaf_args())
768 }
769
770 struct HitBoxInstance {
771 base: BaseInstance,
772 }
773
774 impl InstanceNode for HitBoxInstance {
775 fn base(&self) -> &BaseInstance {
776 &self.base
777 }
778
779 fn instantiate(args: InstantiationArgs) -> Rc<Self> {
780 Rc::new(Self {
781 base: BaseInstance::new(
782 args,
783 InstanceFlags {
784 invisible_to_slot: false,
785 invisible_to_raycasting: false,
786 layer: Layer::Canvas,
787 is_component: false,
788 is_slot: false,
789 },
790 ),
791 })
792 }
793
794 fn resolve_debug(
795 &self,
796 f: &mut std::fmt::Formatter,
797 _expanded_node: Option<&ExpandedNode>,
798 ) -> std::fmt::Result {
799 write!(f, "HitBox")
800 }
801 }
802
803 fn hit_box_args(transition_config: ComponentTransitionConfig) -> InstantiationArgs {
804 InstantiationArgs {
805 prototypical_common_properties: crate::CommonPropertiesInit::Factory(Box::new(
806 |_, expanded_node| {
807 expanded_node.is_none().then(|| {
808 let mut cp = CommonProperties::default();
809 cp.width = Property::new(Some(Size::Pixels(40.into())));
810 cp.height = Property::new(Some(Size::Pixels(40.into())));
811 Rc::new(RefCell::new(cp))
812 })
813 },
814 )),
815 prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
816 handler_registry: None,
817 children: None,
818 component_template: Some(RefCell::new(Vec::new())),
819 component_settings: None,
820 template_node_identifier: None,
821 template_node_type_id: None,
822 template_node_selector_info: None,
823 transition_config,
824 properties_scope: crate::PropertiesScopeInit::None,
825 }
826 }
827
828 fn hit_box(transition_config: ComponentTransitionConfig) -> Rc<dyn InstanceNode> {
829 HitBoxInstance::instantiate(hit_box_args(transition_config))
830 }
831
832 fn dynamic_exit_duration_leaf(frames: f64) -> Rc<dyn InstanceNode> {
833 let mut args = leaf_args();
834 args.transition_config = ComponentTransitionConfig {
835 has_exit: true,
836 exit_dynamic_durations: vec![ValueDefinition::LiteralValue(PaxValue::Duration(
837 Duration::Frames(frames.into()),
838 ))],
839 timeout_ms: 5_000,
840 ..Default::default()
841 };
842 ComponentInstance::instantiate(args)
843 }
844
845 fn router_args(
846 input_location: Property<RouteLocation>,
847 global_location: Property<RouteLocation>,
848 children: Vec<Rc<dyn InstanceNode>>,
849 ) -> InstantiationArgs {
850 let input_location_for_factory = input_location.clone();
851 let global_location_for_factory = global_location.clone();
852 InstantiationArgs {
853 prototypical_common_properties: crate::CommonPropertiesInit::Factory(
854 default_common_properties_factory(),
855 ),
856 prototypical_properties: crate::PropertiesInit::Factory(Box::new(
857 move |_, expanded_node| {
858 if let Some(expanded_node) = expanded_node {
859 expanded_node.with_properties_unwrapped(
860 |properties: &mut RouterProperties| {
861 properties
862 .input_location
863 .replace_with(input_location_for_factory.clone());
864 properties
865 .global_location
866 .replace_with(global_location_for_factory.clone());
867 },
868 );
869 return None;
870 }
871
872 Some(Rc::new(RefCell::new({
873 let mut properties = RouterProperties::default();
874 properties.input_location = input_location_for_factory.clone();
875 properties.global_location = global_location_for_factory.clone();
876 properties.to_pax_any()
877 })))
878 },
879 )),
880 handler_registry: None,
881 children: Some(RefCell::new(children)),
882 component_template: None,
883 component_settings: None,
884 template_node_identifier: None,
885 template_node_type_id: None,
886 template_node_selector_info: None,
887 transition_config: Default::default(),
888 properties_scope: crate::PropertiesScopeInit::None,
889 }
890 }
891
892 fn mounted_router(
893 input_location: Property<RouteLocation>,
894 branch_definitions: Vec<ControlFlowRouteBranchDefinition>,
895 branch_child_ranges: Vec<Range<usize>>,
896 children: Vec<Rc<dyn InstanceNode>>,
897 ) -> (Rc<ExpandedNode>, Rc<ExpandedNode>, Rc<RuntimeContext>) {
898 let router: Rc<dyn InstanceNode> = RouterInstance::instantiate_with_branches(
899 router_args(input_location.clone(), input_location, children),
900 compile_route_branches(&branch_definitions),
901 branch_child_ranges,
902 );
903 let root_component =
904 ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&router)])));
905 let context = Rc::new(RuntimeContext::new(test_globals()));
906 let root = ExpandedNode::initialize_root(root_component, &context);
907 root.recurse_update(&context);
908 let router_node = root.children.get().remove(0);
909 (root, router_node, context)
910 }
911
912 fn route_param(node: &Rc<ExpandedNode>, name: &str) -> Option<String> {
913 let route = node.stack.resolve_symbol(ROUTE_SYMBOL)?.get_as_pax_value();
914 let PaxValue::Object(route_fields) = route else {
915 return None;
916 };
917 let params = route_fields
918 .into_iter()
919 .find_map(|(field, value)| (field == "params").then_some(value))?;
920 let PaxValue::Object(param_fields) = params else {
921 return None;
922 };
923 param_fields
924 .into_iter()
925 .find_map(|(field, value)| (field == name).then_some(value))
926 .and_then(|value| match value {
927 PaxValue::String(value) => Some(value),
928 _ => None,
929 })
930 }
931
932 #[test]
933 fn terminal_catch_all_preserves_remainder() {
934 let branches = compile_route_branches(&[ControlFlowRouteBranchDefinition {
935 path: Some("/settings/*".to_string()),
936 default: false,
937 modal: false,
938 child_ids: vec![],
939 metadata: None,
940 }]);
941 let input = route(&["settings", "team", "42"]);
942 let selection = select_route_branch(&branches, &input, &input);
943
944 assert_eq!(selection.branch_index, Some(0));
945 let route_match = selection.route_match.unwrap();
946 assert_eq!(route_match.consumed_segments, 1);
947 assert_eq!(
948 route_match.remainder,
949 vec!["team".to_string(), "42".to_string()]
950 );
951 assert!(!route_match.is_exact);
952 }
953
954 #[test]
955 fn params_are_captured_for_exact_routes() {
956 let branches = compile_route_branches(&[ControlFlowRouteBranchDefinition {
957 path: Some("/docs/:slug".to_string()),
958 default: false,
959 modal: false,
960 child_ids: vec![],
961 metadata: None,
962 }]);
963 let input = route(&["docs", "router"]);
964 let selection = select_route_branch(&branches, &input, &input);
965
966 assert_eq!(selection.branch_index, Some(0));
967 let route_match = selection.route_match.unwrap();
968 assert_eq!(route_match.params.get("slug"), Some(&"router".to_string()));
969 assert_eq!(route_match.remainder, Vec::<String>::new());
970 assert!(route_match.is_exact);
971 }
972
973 #[test]
974 fn relative_paths_compile_like_absolute_paths() {
975 let branches = compile_route_branches(&[ControlFlowRouteBranchDefinition {
976 path: Some("settings/*".to_string()),
977 default: false,
978 modal: false,
979 child_ids: vec![],
980 metadata: None,
981 }]);
982 let input = route(&["settings", "integrations", "logs"]);
983 let selection = select_route_branch(&branches, &input, &input);
984
985 assert_eq!(selection.branch_index, Some(0));
986 let route_match = selection.route_match.unwrap();
987 assert_eq!(
988 route_match.remainder,
989 vec!["integrations".to_string(), "logs".to_string()]
990 );
991 }
992
993 #[test]
994 fn default_branch_receives_unmatched_input() {
995 let branches = compile_route_branches(&[
996 ControlFlowRouteBranchDefinition {
997 path: Some("/docs/:slug".to_string()),
998 default: false,
999 modal: false,
1000 child_ids: vec![],
1001 metadata: None,
1002 },
1003 ControlFlowRouteBranchDefinition {
1004 path: None,
1005 default: true,
1006 modal: false,
1007 child_ids: vec![],
1008 metadata: None,
1009 },
1010 ]);
1011 let input = route(&["missing"]);
1012 let selection = select_route_branch(&branches, &input, &input);
1013
1014 assert_eq!(selection.branch_index, Some(1));
1015 let route_match = selection.route_match.unwrap();
1016 assert_eq!(route_match.remainder, vec!["missing".to_string()]);
1017 assert_eq!(route_match.location, input);
1018 }
1019
1020 #[test]
1021 fn same_branch_route_match_change_enters_new_tree_and_exits_old_tree() {
1022 let input_location = Property::new(route(&["users", "1"]));
1023 let (_root, router_node, context) = mounted_router(
1024 input_location.clone(),
1025 vec![ControlFlowRouteBranchDefinition {
1026 path: Some("/users/:id".to_string()),
1027 default: false,
1028 modal: false,
1029 child_ids: vec![],
1030 metadata: None,
1031 }],
1032 vec![0..1],
1033 vec![leaf()],
1034 );
1035
1036 let initial_active = borrow!(router_node.active_children).clone();
1037 assert_eq!(initial_active.len(), 1);
1038 assert_eq!(route_param(&initial_active[0], "id"), Some("1".to_string()));
1039
1040 input_location.set(route(&["users", "2"]));
1041 router_node.recurse_update(&context);
1042
1043 let active = borrow!(router_node.active_children).clone();
1044 let exiting = borrow!(router_node.exiting_children).clone();
1045 assert_eq!(active.len(), 1);
1046 assert_eq!(exiting.len(), 1);
1047 assert_ne!(active[0].id.0, initial_active[0].id.0);
1048 assert_eq!(route_param(&active[0], "id"), Some("2".to_string()));
1049 assert_eq!(route_param(&exiting[0], "id"), Some("1".to_string()));
1050 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1051 assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
1052 }
1053
1054 #[test]
1055 fn catch_all_reuses_route_tree_when_only_remainder_changes() {
1056 let input_location = Property::new(route(&["teams", "atlas"]));
1057 let (_root, router_node, context) = mounted_router(
1058 input_location.clone(),
1059 vec![ControlFlowRouteBranchDefinition {
1060 path: Some("/teams/:id/*".to_string()),
1061 default: false,
1062 modal: false,
1063 child_ids: vec![],
1064 metadata: None,
1065 }],
1066 vec![0..1],
1067 vec![leaf()],
1068 );
1069
1070 let initial_active = borrow!(router_node.active_children).clone();
1071 assert_eq!(initial_active.len(), 1);
1072
1073 input_location.set(route(&[
1074 "teams",
1075 "atlas",
1076 "settings",
1077 "integrations",
1078 "logs",
1079 ]));
1080 router_node.recurse_update(&context);
1081
1082 let active = borrow!(router_node.active_children).clone();
1083 assert_eq!(active.len(), 1);
1084 assert_eq!(active[0].id.0, initial_active[0].id.0);
1085 assert!(borrow!(router_node.exiting_children).is_empty());
1086 assert_eq!(
1087 RouterInstance::child_route_match(&active[0])
1088 .expect("active route should retain its match")
1089 .remainder,
1090 vec![
1091 "settings".to_string(),
1092 "integrations".to_string(),
1093 "logs".to_string(),
1094 ]
1095 );
1096 }
1097
1098 #[test]
1099 fn branch_change_transitions_each_root_of_multi_root_routes() {
1100 let input_location = Property::new(route(&["alpha"]));
1101 let (_root, router_node, context) = mounted_router(
1102 input_location.clone(),
1103 vec![
1104 ControlFlowRouteBranchDefinition {
1105 path: Some("/alpha".to_string()),
1106 default: false,
1107 modal: false,
1108 child_ids: vec![],
1109 metadata: None,
1110 },
1111 ControlFlowRouteBranchDefinition {
1112 path: Some("/beta".to_string()),
1113 default: false,
1114 modal: false,
1115 child_ids: vec![],
1116 metadata: None,
1117 },
1118 ],
1119 vec![0..2, 2..4],
1120 vec![leaf(), leaf(), leaf(), leaf()],
1121 );
1122
1123 let initial_active = borrow!(router_node.active_children).clone();
1124 assert_eq!(initial_active.len(), 2);
1125
1126 input_location.set(route(&["beta"]));
1127 router_node.recurse_update(&context);
1128
1129 let active = borrow!(router_node.active_children).clone();
1130 let exiting = borrow!(router_node.exiting_children).clone();
1131 assert_eq!(active.len(), 2);
1132 assert_eq!(exiting.len(), 2);
1133 assert!(active
1134 .iter()
1135 .all(|child| child.transition_phase.get() == TRANSITION_PHASE_ENTER));
1136 assert!(exiting
1137 .iter()
1138 .all(|child| child.transition_phase.get() == TRANSITION_PHASE_EXIT));
1139 assert!(initial_active
1140 .iter()
1141 .all(|old_child| exiting.iter().any(|child| child.id.0 == old_child.id.0)));
1142 }
1143
1144 #[test]
1145 fn branch_can_be_reselected_while_previous_instance_is_exiting() {
1146 let input_location = Property::new(route(&["alpha"]));
1147 let (_root, router_node, context) = mounted_router(
1148 input_location.clone(),
1149 vec![
1150 ControlFlowRouteBranchDefinition {
1151 path: Some("/alpha".to_string()),
1152 default: false,
1153 modal: false,
1154 child_ids: vec![],
1155 metadata: None,
1156 },
1157 ControlFlowRouteBranchDefinition {
1158 path: Some("/beta".to_string()),
1159 default: false,
1160 modal: false,
1161 child_ids: vec![],
1162 metadata: None,
1163 },
1164 ],
1165 vec![0..1, 1..2],
1166 vec![leaf(), leaf()],
1167 );
1168
1169 let initial_alpha = borrow!(router_node.active_children)[0].id;
1170
1171 input_location.set(route(&["beta"]));
1172 router_node.recurse_update(&context);
1173 assert_eq!(borrow!(router_node.active_children).len(), 1);
1174 assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1175 assert_eq!(borrow!(router_node.exiting_children)[0].id, initial_alpha);
1176
1177 input_location.set(route(&["alpha"]));
1178 router_node.recurse_update(&context);
1179
1180 let active = borrow!(router_node.active_children).clone();
1181 let exiting = borrow!(router_node.exiting_children).clone();
1182 assert_eq!(active.len(), 1);
1183 assert_eq!(active[0].id, initial_alpha);
1184 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1185 assert_eq!(exiting.len(), 1);
1186 assert_ne!(exiting[0].id, initial_alpha);
1187 }
1188
1189 #[test]
1190 fn retained_exiting_route_tree_does_not_intercept_hit_testing() {
1191 let input_location = Property::new(route(&["alpha"]));
1192 let exiting_route_config = ComponentTransitionConfig {
1193 has_exit: true,
1194 exit_frame_count: 30,
1195 timeout_ms: 5_000,
1196 ..Default::default()
1197 };
1198 let (root, router_node, context) = mounted_router(
1199 input_location.clone(),
1200 vec![
1201 ControlFlowRouteBranchDefinition {
1202 path: Some("/alpha".to_string()),
1203 default: false,
1204 modal: false,
1205 child_ids: vec![],
1206 metadata: None,
1207 },
1208 ControlFlowRouteBranchDefinition {
1209 path: Some("/beta".to_string()),
1210 default: false,
1211 modal: false,
1212 child_ids: vec![],
1213 metadata: None,
1214 },
1215 ],
1216 vec![0..1, 1..2],
1217 vec![hit_box(exiting_route_config), hit_box(Default::default())],
1218 );
1219
1220 input_location.set(route(&["beta"]));
1221 router_node.recurse_update(&context);
1222
1223 let active = borrow!(router_node.active_children).clone();
1224 assert_eq!(active.len(), 1);
1225 assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1226
1227 let hits = context.get_elements_beneath_ray(
1228 Some(root),
1229 Point2::<Window>::new(5.0, 5.0),
1230 true,
1231 vec![],
1232 false,
1233 );
1234 assert_eq!(hits.first().map(|node| node.id), Some(active[0].id));
1235 }
1236
1237 #[test]
1238 fn dynamic_exit_duration_controls_retained_route_cleanup() {
1239 let input_location = Property::new(route(&["alpha"]));
1240 let (_root, router_node, context) = mounted_router(
1241 input_location.clone(),
1242 vec![
1243 ControlFlowRouteBranchDefinition {
1244 path: Some("/alpha".to_string()),
1245 default: false,
1246 modal: false,
1247 child_ids: vec![],
1248 metadata: None,
1249 },
1250 ControlFlowRouteBranchDefinition {
1251 path: Some("/beta".to_string()),
1252 default: false,
1253 modal: false,
1254 child_ids: vec![],
1255 metadata: None,
1256 },
1257 ],
1258 vec![0..1, 1..2],
1259 vec![dynamic_exit_duration_leaf(3.0), leaf()],
1260 );
1261
1262 input_location.set(route(&["beta"]));
1263 router_node.recurse_update(&context);
1264 assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1265 let exiting_child = borrow!(router_node.exiting_children)[0].clone();
1266
1267 context.globals().elapsed_frames.set(2);
1268 context.drain_node_effects();
1269 assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1270
1271 context.globals().elapsed_frames.set(3);
1272 context.drain_node_effects();
1273 assert!(borrow!(router_node.exiting_children).is_empty());
1274 assert_eq!(exiting_child.transition_phase.get(), TRANSITION_PHASE_IDLE);
1275 assert!(!pax_runtime_api::properties::property_has_direct_outbound(
1276 &context.globals().elapsed_frames.untyped(),
1277 &exiting_child.transition_playhead.untyped(),
1278 ));
1279 assert!(!pax_runtime_api::properties::property_has_direct_outbound(
1280 &context.globals().elapsed_millis.untyped(),
1281 &exiting_child.transition_playhead_millis.untyped(),
1282 ));
1283 }
1284
1285 #[test]
1286 fn active_enter_transition_returns_to_idle_after_duration() {
1287 let input_location = Property::new(route(&["alpha"]));
1288 let (_root, router_node, context) = mounted_router(
1289 input_location.clone(),
1290 vec![
1291 ControlFlowRouteBranchDefinition {
1292 path: Some("/alpha".to_string()),
1293 default: false,
1294 modal: false,
1295 child_ids: vec![],
1296 metadata: None,
1297 },
1298 ControlFlowRouteBranchDefinition {
1299 path: Some("/beta".to_string()),
1300 default: false,
1301 modal: false,
1302 child_ids: vec![],
1303 metadata: None,
1304 },
1305 ],
1306 vec![0..1, 1..2],
1307 vec![leaf(), leaf()],
1308 );
1309
1310 input_location.set(route(&["beta"]));
1311 router_node.recurse_update(&context);
1312
1313 let active = borrow!(router_node.active_children).clone();
1314 assert_eq!(active.len(), 1);
1315 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1316
1317 context.globals().elapsed_frames.set(9);
1318 context.drain_node_effects();
1319 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1320
1321 context.globals().elapsed_frames.set(10);
1322 context.drain_node_effects();
1323 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_IDLE);
1324 }
1325
1326 #[test]
1327 fn modal_branch_keeps_previous_route_active_underneath() {
1328 let input_location = Property::new(route(&["alpha"]));
1329 let (_root, router_node, context) = mounted_router(
1330 input_location.clone(),
1331 vec![
1332 ControlFlowRouteBranchDefinition {
1333 path: Some("/alpha".to_string()),
1334 default: false,
1335 modal: false,
1336 child_ids: vec![],
1337 metadata: None,
1338 },
1339 ControlFlowRouteBranchDefinition {
1340 path: Some("/tools".to_string()),
1341 default: false,
1342 modal: true,
1343 child_ids: vec![],
1344 metadata: None,
1345 },
1346 ],
1347 vec![0..1, 1..2],
1348 vec![leaf(), leaf()],
1349 );
1350
1351 let base_id = borrow!(router_node.active_children)[0].id;
1352
1353 input_location.set(route(&["tools"]));
1354 router_node.recurse_update(&context);
1355
1356 let active = borrow!(router_node.active_children).clone();
1357 let exiting = borrow!(router_node.exiting_children).clone();
1358 assert_eq!(active.len(), 2);
1359 assert!(exiting.is_empty());
1360 assert_ne!(active[0].id, base_id);
1361 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1362 assert_eq!(active[1].id, base_id);
1363 }
1364
1365 #[test]
1366 fn dismissing_modal_reveals_previous_route_without_remounting() {
1367 let input_location = Property::new(route(&["alpha"]));
1368 let (_root, router_node, context) = mounted_router(
1369 input_location.clone(),
1370 vec![
1371 ControlFlowRouteBranchDefinition {
1372 path: Some("/alpha".to_string()),
1373 default: false,
1374 modal: false,
1375 child_ids: vec![],
1376 metadata: None,
1377 },
1378 ControlFlowRouteBranchDefinition {
1379 path: Some("/tools".to_string()),
1380 default: false,
1381 modal: true,
1382 child_ids: vec![],
1383 metadata: None,
1384 },
1385 ],
1386 vec![0..1, 1..2],
1387 vec![leaf(), leaf()],
1388 );
1389
1390 let base_id = borrow!(router_node.active_children)[0].id;
1391
1392 input_location.set(route(&["tools"]));
1393 router_node.recurse_update(&context);
1394 let modal_id = borrow!(router_node.active_children)[0].id;
1395
1396 input_location.set(route(&["alpha"]));
1397 router_node.recurse_update(&context);
1398
1399 let active = borrow!(router_node.active_children).clone();
1400 let exiting = borrow!(router_node.exiting_children).clone();
1401 assert_eq!(active.len(), 1);
1402 assert_eq!(active[0].id, base_id);
1403 assert_eq!(exiting.len(), 1);
1404 assert_eq!(exiting[0].id, modal_id);
1405 assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
1406 }
1407
1408 #[test]
1409 fn no_match_transitions_out_previous_route_tree() {
1410 let input_location = Property::new(route(&["alpha"]));
1411 let (_root, router_node, context) = mounted_router(
1412 input_location.clone(),
1413 vec![ControlFlowRouteBranchDefinition {
1414 path: Some("/alpha".to_string()),
1415 default: false,
1416 modal: false,
1417 child_ids: vec![],
1418 metadata: None,
1419 }],
1420 vec![0..1],
1421 vec![leaf()],
1422 );
1423
1424 assert_eq!(borrow!(router_node.active_children).len(), 1);
1425
1426 input_location.set(route(&["missing"]));
1427 router_node.recurse_update(&context);
1428
1429 let active = borrow!(router_node.active_children).clone();
1430 let exiting = borrow!(router_node.exiting_children).clone();
1431 assert!(active.is_empty());
1432 assert_eq!(exiting.len(), 1);
1433 assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
1434 }
1435}