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, LocalProperty, PaxValue, 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: LocalProperty<RouteLocation>,
153 pub global_location: LocalProperty<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(LocalProperty::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<LocalProperty<RouteMatch>> {
459 child
460 .stack
461 .resolve_symbol_as_erased_property(INTERNAL_ROUTE_MATCH_SYMBOL)
462 .map(LocalProperty::<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 = LocalProperty::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: LocalProperty::new(0),
680 elapsed_millis: LocalProperty::new(0),
681 viewport: LocalProperty::new(TransformAndBounds {
682 transform: Transform2::identity(),
683 bounds: (100.0, 100.0),
684 }),
685 gyro: LocalProperty::new(Default::default()),
686 accel: LocalProperty::new(Default::default()),
687 route_location: LocalProperty::new(RouteLocation::root()),
688 browser_allows_scroller_vector_layers: LocalProperty::new(true),
689 browser_allows_nested_scroller_vector_layers: LocalProperty::new(true),
690 platform: Platform::Unknown,
691 os: OS::Unknown,
692 target: TargetInfo::new(Platform::Unknown, OS::Unknown),
693 #[cfg(feature = "designtime")]
694 designtime: crate::test_support::designtime(),
695 get_elapsed_millis: Rc::new(|| 0),
696 }
697 }
698
699 fn default_properties_factory() -> Box<
700 dyn Fn(
701 Rc<RuntimePropertiesStackFrame>,
702 Option<Rc<ExpandedNode>>,
703 ) -> Option<Rc<RefCell<PaxAny>>>,
704 > {
705 Box::new(|_, expanded_node| {
706 expanded_node
707 .is_none()
708 .then(|| Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
709 })
710 }
711
712 fn default_common_properties_factory() -> Box<
713 dyn Fn(
714 Rc<RuntimePropertiesStackFrame>,
715 Option<Rc<ExpandedNode>>,
716 ) -> Option<Rc<RefCell<CommonProperties>>>,
717 > {
718 Box::new(|_, expanded_node| {
719 expanded_node
720 .is_none()
721 .then(|| Rc::new(RefCell::new(CommonProperties::default())))
722 })
723 }
724
725 fn component_args(template: Option<Vec<Rc<dyn InstanceNode>>>) -> InstantiationArgs {
726 InstantiationArgs {
727 prototypical_common_properties: crate::CommonPropertiesInit::Factory(
728 default_common_properties_factory(),
729 ),
730 prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
731 handler_registry: None,
732 children: None,
733 component_template: template.map(RefCell::new),
734 component_settings: None,
735 template_node_identifier: None,
736 template_node_type_id: None,
737 template_node_selector_info: None,
738 transition_config: Default::default(),
739 properties_scope: crate::PropertiesScopeInit::None,
740 }
741 }
742
743 fn leaf_args() -> InstantiationArgs {
744 InstantiationArgs {
745 prototypical_common_properties: crate::CommonPropertiesInit::Factory(
746 default_common_properties_factory(),
747 ),
748 prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
749 handler_registry: None,
750 children: None,
751 component_template: Some(RefCell::new(Vec::new())),
752 component_settings: None,
753 template_node_identifier: None,
754 template_node_type_id: None,
755 template_node_selector_info: None,
756 transition_config: ComponentTransitionConfig {
757 has_enter: true,
758 enter_frame_count: 10,
759 has_exit: true,
760 exit_frame_count: 10,
761 timeout_ms: 5_000,
762 ..Default::default()
763 },
764 properties_scope: crate::PropertiesScopeInit::None,
765 }
766 }
767
768 fn leaf() -> Rc<dyn InstanceNode> {
769 ComponentInstance::instantiate(leaf_args())
770 }
771
772 struct HitBoxInstance {
773 base: BaseInstance,
774 }
775
776 impl InstanceNode for HitBoxInstance {
777 fn base(&self) -> &BaseInstance {
778 &self.base
779 }
780
781 fn instantiate(args: InstantiationArgs) -> Rc<Self> {
782 Rc::new(Self {
783 base: BaseInstance::new(
784 args,
785 InstanceFlags {
786 invisible_to_slot: false,
787 invisible_to_raycasting: false,
788 layer: Layer::Canvas,
789 is_component: false,
790 is_slot: false,
791 },
792 ),
793 })
794 }
795
796 fn resolve_debug(
797 &self,
798 f: &mut std::fmt::Formatter,
799 _expanded_node: Option<&ExpandedNode>,
800 ) -> std::fmt::Result {
801 write!(f, "HitBox")
802 }
803 }
804
805 fn hit_box_args(transition_config: ComponentTransitionConfig) -> InstantiationArgs {
806 InstantiationArgs {
807 prototypical_common_properties: crate::CommonPropertiesInit::Factory(Box::new(
808 |_, expanded_node| {
809 expanded_node.is_none().then(|| {
810 let mut cp = CommonProperties::default();
811 cp.width = LocalProperty::new(Some(Size::Pixels(40.into())));
812 cp.height = LocalProperty::new(Some(Size::Pixels(40.into())));
813 Rc::new(RefCell::new(cp))
814 })
815 },
816 )),
817 prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
818 handler_registry: None,
819 children: None,
820 component_template: Some(RefCell::new(Vec::new())),
821 component_settings: None,
822 template_node_identifier: None,
823 template_node_type_id: None,
824 template_node_selector_info: None,
825 transition_config,
826 properties_scope: crate::PropertiesScopeInit::None,
827 }
828 }
829
830 fn hit_box(transition_config: ComponentTransitionConfig) -> Rc<dyn InstanceNode> {
831 HitBoxInstance::instantiate(hit_box_args(transition_config))
832 }
833
834 fn dynamic_exit_duration_leaf(frames: f64) -> Rc<dyn InstanceNode> {
835 let mut args = leaf_args();
836 args.transition_config = ComponentTransitionConfig {
837 has_exit: true,
838 exit_dynamic_durations: vec![ValueDefinition::LiteralValue(PaxValue::Duration(
839 Duration::Frames(frames.into()),
840 ))],
841 timeout_ms: 5_000,
842 ..Default::default()
843 };
844 ComponentInstance::instantiate(args)
845 }
846
847 fn router_args(
848 input_location: LocalProperty<RouteLocation>,
849 global_location: LocalProperty<RouteLocation>,
850 children: Vec<Rc<dyn InstanceNode>>,
851 ) -> InstantiationArgs {
852 let input_location_for_factory = input_location.clone();
853 let global_location_for_factory = global_location.clone();
854 InstantiationArgs {
855 prototypical_common_properties: crate::CommonPropertiesInit::Factory(
856 default_common_properties_factory(),
857 ),
858 prototypical_properties: crate::PropertiesInit::Factory(Box::new(
859 move |_, expanded_node| {
860 if let Some(expanded_node) = expanded_node {
861 expanded_node.with_properties_unwrapped(
862 |properties: &mut RouterProperties| {
863 properties
864 .input_location
865 .replace_with(input_location_for_factory.clone());
866 properties
867 .global_location
868 .replace_with(global_location_for_factory.clone());
869 },
870 );
871 return None;
872 }
873
874 Some(Rc::new(RefCell::new({
875 let mut properties = RouterProperties::default();
876 properties.input_location = input_location_for_factory.clone();
877 properties.global_location = global_location_for_factory.clone();
878 properties.to_pax_any()
879 })))
880 },
881 )),
882 handler_registry: None,
883 children: Some(RefCell::new(children)),
884 component_template: None,
885 component_settings: None,
886 template_node_identifier: None,
887 template_node_type_id: None,
888 template_node_selector_info: None,
889 transition_config: Default::default(),
890 properties_scope: crate::PropertiesScopeInit::None,
891 }
892 }
893
894 fn mounted_router(
895 input_location: LocalProperty<RouteLocation>,
896 branch_definitions: Vec<ControlFlowRouteBranchDefinition>,
897 branch_child_ranges: Vec<Range<usize>>,
898 children: Vec<Rc<dyn InstanceNode>>,
899 ) -> (Rc<ExpandedNode>, Rc<ExpandedNode>, Rc<RuntimeContext>) {
900 let router: Rc<dyn InstanceNode> = RouterInstance::instantiate_with_branches(
901 router_args(input_location.clone(), input_location, children),
902 compile_route_branches(&branch_definitions),
903 branch_child_ranges,
904 );
905 let root_component =
906 ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&router)])));
907 let context = Rc::new(crate::test_support::runtime_context(test_globals()));
908 let root = ExpandedNode::initialize_root(root_component, &context);
909 root.recurse_update(&context);
910 let router_node = root.children.get().remove(0);
911 (root, router_node, context)
912 }
913
914 fn route_param(node: &Rc<ExpandedNode>, name: &str) -> Option<String> {
915 let route = node.stack.resolve_symbol(ROUTE_SYMBOL)?.get_as_pax_value();
916 let PaxValue::Object(route_fields) = route else {
917 return None;
918 };
919 let params = route_fields
920 .into_iter()
921 .find_map(|(field, value)| (field == "params").then_some(value))?;
922 let PaxValue::Object(param_fields) = params else {
923 return None;
924 };
925 param_fields
926 .into_iter()
927 .find_map(|(field, value)| (field == name).then_some(value))
928 .and_then(|value| match value {
929 PaxValue::String(value) => Some(value),
930 _ => None,
931 })
932 }
933
934 #[test]
935 fn terminal_catch_all_preserves_remainder() {
936 let branches = compile_route_branches(&[ControlFlowRouteBranchDefinition {
937 path: Some("/settings/*".to_string()),
938 default: false,
939 modal: false,
940 child_ids: vec![],
941 metadata: None,
942 }]);
943 let input = route(&["settings", "team", "42"]);
944 let selection = select_route_branch(&branches, &input, &input);
945
946 assert_eq!(selection.branch_index, Some(0));
947 let route_match = selection.route_match.unwrap();
948 assert_eq!(route_match.consumed_segments, 1);
949 assert_eq!(
950 route_match.remainder,
951 vec!["team".to_string(), "42".to_string()]
952 );
953 assert!(!route_match.is_exact);
954 }
955
956 #[test]
957 fn params_are_captured_for_exact_routes() {
958 let branches = compile_route_branches(&[ControlFlowRouteBranchDefinition {
959 path: Some("/docs/:slug".to_string()),
960 default: false,
961 modal: false,
962 child_ids: vec![],
963 metadata: None,
964 }]);
965 let input = route(&["docs", "router"]);
966 let selection = select_route_branch(&branches, &input, &input);
967
968 assert_eq!(selection.branch_index, Some(0));
969 let route_match = selection.route_match.unwrap();
970 assert_eq!(route_match.params.get("slug"), Some(&"router".to_string()));
971 assert_eq!(route_match.remainder, Vec::<String>::new());
972 assert!(route_match.is_exact);
973 }
974
975 #[test]
976 fn relative_paths_compile_like_absolute_paths() {
977 let branches = compile_route_branches(&[ControlFlowRouteBranchDefinition {
978 path: Some("settings/*".to_string()),
979 default: false,
980 modal: false,
981 child_ids: vec![],
982 metadata: None,
983 }]);
984 let input = route(&["settings", "integrations", "logs"]);
985 let selection = select_route_branch(&branches, &input, &input);
986
987 assert_eq!(selection.branch_index, Some(0));
988 let route_match = selection.route_match.unwrap();
989 assert_eq!(
990 route_match.remainder,
991 vec!["integrations".to_string(), "logs".to_string()]
992 );
993 }
994
995 #[test]
996 fn default_branch_receives_unmatched_input() {
997 let branches = compile_route_branches(&[
998 ControlFlowRouteBranchDefinition {
999 path: Some("/docs/:slug".to_string()),
1000 default: false,
1001 modal: false,
1002 child_ids: vec![],
1003 metadata: None,
1004 },
1005 ControlFlowRouteBranchDefinition {
1006 path: None,
1007 default: true,
1008 modal: false,
1009 child_ids: vec![],
1010 metadata: None,
1011 },
1012 ]);
1013 let input = route(&["missing"]);
1014 let selection = select_route_branch(&branches, &input, &input);
1015
1016 assert_eq!(selection.branch_index, Some(1));
1017 let route_match = selection.route_match.unwrap();
1018 assert_eq!(route_match.remainder, vec!["missing".to_string()]);
1019 assert_eq!(route_match.location, input);
1020 }
1021
1022 #[test]
1023 fn same_branch_route_match_change_enters_new_tree_and_exits_old_tree() {
1024 let input_location = LocalProperty::new(route(&["users", "1"]));
1025 let (_root, router_node, context) = mounted_router(
1026 input_location.clone(),
1027 vec![ControlFlowRouteBranchDefinition {
1028 path: Some("/users/:id".to_string()),
1029 default: false,
1030 modal: false,
1031 child_ids: vec![],
1032 metadata: None,
1033 }],
1034 vec![0..1],
1035 vec![leaf()],
1036 );
1037
1038 let initial_active = borrow!(router_node.active_children).clone();
1039 assert_eq!(initial_active.len(), 1);
1040 assert_eq!(route_param(&initial_active[0], "id"), Some("1".to_string()));
1041
1042 input_location.set(route(&["users", "2"]));
1043 router_node.recurse_update(&context);
1044
1045 let active = borrow!(router_node.active_children).clone();
1046 let exiting = borrow!(router_node.exiting_children).clone();
1047 assert_eq!(active.len(), 1);
1048 assert_eq!(exiting.len(), 1);
1049 assert_ne!(active[0].id.0, initial_active[0].id.0);
1050 assert_eq!(route_param(&active[0], "id"), Some("2".to_string()));
1051 assert_eq!(route_param(&exiting[0], "id"), Some("1".to_string()));
1052 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1053 assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
1054 }
1055
1056 #[test]
1057 fn catch_all_reuses_route_tree_when_only_remainder_changes() {
1058 let input_location = LocalProperty::new(route(&["teams", "atlas"]));
1059 let (_root, router_node, context) = mounted_router(
1060 input_location.clone(),
1061 vec![ControlFlowRouteBranchDefinition {
1062 path: Some("/teams/:id/*".to_string()),
1063 default: false,
1064 modal: false,
1065 child_ids: vec![],
1066 metadata: None,
1067 }],
1068 vec![0..1],
1069 vec![leaf()],
1070 );
1071
1072 let initial_active = borrow!(router_node.active_children).clone();
1073 assert_eq!(initial_active.len(), 1);
1074
1075 input_location.set(route(&[
1076 "teams",
1077 "atlas",
1078 "settings",
1079 "integrations",
1080 "logs",
1081 ]));
1082 router_node.recurse_update(&context);
1083
1084 let active = borrow!(router_node.active_children).clone();
1085 assert_eq!(active.len(), 1);
1086 assert_eq!(active[0].id.0, initial_active[0].id.0);
1087 assert!(borrow!(router_node.exiting_children).is_empty());
1088 assert_eq!(
1089 RouterInstance::child_route_match(&active[0])
1090 .expect("active route should retain its match")
1091 .remainder,
1092 vec![
1093 "settings".to_string(),
1094 "integrations".to_string(),
1095 "logs".to_string(),
1096 ]
1097 );
1098 }
1099
1100 #[test]
1101 fn branch_change_transitions_each_root_of_multi_root_routes() {
1102 let input_location = LocalProperty::new(route(&["alpha"]));
1103 let (_root, router_node, context) = mounted_router(
1104 input_location.clone(),
1105 vec![
1106 ControlFlowRouteBranchDefinition {
1107 path: Some("/alpha".to_string()),
1108 default: false,
1109 modal: false,
1110 child_ids: vec![],
1111 metadata: None,
1112 },
1113 ControlFlowRouteBranchDefinition {
1114 path: Some("/beta".to_string()),
1115 default: false,
1116 modal: false,
1117 child_ids: vec![],
1118 metadata: None,
1119 },
1120 ],
1121 vec![0..2, 2..4],
1122 vec![leaf(), leaf(), leaf(), leaf()],
1123 );
1124
1125 let initial_active = borrow!(router_node.active_children).clone();
1126 assert_eq!(initial_active.len(), 2);
1127
1128 input_location.set(route(&["beta"]));
1129 router_node.recurse_update(&context);
1130
1131 let active = borrow!(router_node.active_children).clone();
1132 let exiting = borrow!(router_node.exiting_children).clone();
1133 assert_eq!(active.len(), 2);
1134 assert_eq!(exiting.len(), 2);
1135 assert!(active
1136 .iter()
1137 .all(|child| child.transition_phase.get() == TRANSITION_PHASE_ENTER));
1138 assert!(exiting
1139 .iter()
1140 .all(|child| child.transition_phase.get() == TRANSITION_PHASE_EXIT));
1141 assert!(initial_active
1142 .iter()
1143 .all(|old_child| exiting.iter().any(|child| child.id.0 == old_child.id.0)));
1144 }
1145
1146 #[test]
1147 fn branch_can_be_reselected_while_previous_instance_is_exiting() {
1148 let input_location = LocalProperty::new(route(&["alpha"]));
1149 let (_root, router_node, context) = mounted_router(
1150 input_location.clone(),
1151 vec![
1152 ControlFlowRouteBranchDefinition {
1153 path: Some("/alpha".to_string()),
1154 default: false,
1155 modal: false,
1156 child_ids: vec![],
1157 metadata: None,
1158 },
1159 ControlFlowRouteBranchDefinition {
1160 path: Some("/beta".to_string()),
1161 default: false,
1162 modal: false,
1163 child_ids: vec![],
1164 metadata: None,
1165 },
1166 ],
1167 vec![0..1, 1..2],
1168 vec![leaf(), leaf()],
1169 );
1170
1171 let initial_alpha = borrow!(router_node.active_children)[0].id;
1172
1173 input_location.set(route(&["beta"]));
1174 router_node.recurse_update(&context);
1175 assert_eq!(borrow!(router_node.active_children).len(), 1);
1176 assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1177 assert_eq!(borrow!(router_node.exiting_children)[0].id, initial_alpha);
1178
1179 input_location.set(route(&["alpha"]));
1180 router_node.recurse_update(&context);
1181
1182 let active = borrow!(router_node.active_children).clone();
1183 let exiting = borrow!(router_node.exiting_children).clone();
1184 assert_eq!(active.len(), 1);
1185 assert_eq!(active[0].id, initial_alpha);
1186 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1187 assert_eq!(exiting.len(), 1);
1188 assert_ne!(exiting[0].id, initial_alpha);
1189 }
1190
1191 #[test]
1192 fn retained_exiting_route_tree_does_not_intercept_hit_testing() {
1193 let input_location = LocalProperty::new(route(&["alpha"]));
1194 let exiting_route_config = ComponentTransitionConfig {
1195 has_exit: true,
1196 exit_frame_count: 30,
1197 timeout_ms: 5_000,
1198 ..Default::default()
1199 };
1200 let (root, router_node, context) = mounted_router(
1201 input_location.clone(),
1202 vec![
1203 ControlFlowRouteBranchDefinition {
1204 path: Some("/alpha".to_string()),
1205 default: false,
1206 modal: false,
1207 child_ids: vec![],
1208 metadata: None,
1209 },
1210 ControlFlowRouteBranchDefinition {
1211 path: Some("/beta".to_string()),
1212 default: false,
1213 modal: false,
1214 child_ids: vec![],
1215 metadata: None,
1216 },
1217 ],
1218 vec![0..1, 1..2],
1219 vec![hit_box(exiting_route_config), hit_box(Default::default())],
1220 );
1221
1222 input_location.set(route(&["beta"]));
1223 router_node.recurse_update(&context);
1224
1225 let active = borrow!(router_node.active_children).clone();
1226 assert_eq!(active.len(), 1);
1227 assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1228
1229 let hits = context.get_elements_beneath_ray(
1230 Some(root),
1231 Point2::<Window>::new(5.0, 5.0),
1232 true,
1233 vec![],
1234 false,
1235 );
1236 assert_eq!(hits.first().map(|node| node.id), Some(active[0].id));
1237 }
1238
1239 #[test]
1240 fn dynamic_exit_duration_controls_retained_route_cleanup() {
1241 let input_location = LocalProperty::new(route(&["alpha"]));
1242 let (_root, router_node, context) = mounted_router(
1243 input_location.clone(),
1244 vec![
1245 ControlFlowRouteBranchDefinition {
1246 path: Some("/alpha".to_string()),
1247 default: false,
1248 modal: false,
1249 child_ids: vec![],
1250 metadata: None,
1251 },
1252 ControlFlowRouteBranchDefinition {
1253 path: Some("/beta".to_string()),
1254 default: false,
1255 modal: false,
1256 child_ids: vec![],
1257 metadata: None,
1258 },
1259 ],
1260 vec![0..1, 1..2],
1261 vec![dynamic_exit_duration_leaf(3.0), leaf()],
1262 );
1263
1264 input_location.set(route(&["beta"]));
1265 router_node.recurse_update(&context);
1266 assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1267 let exiting_child = borrow!(router_node.exiting_children)[0].clone();
1268
1269 context.globals().elapsed_frames.set(2);
1270 context.drain_node_effects();
1271 assert_eq!(borrow!(router_node.exiting_children).len(), 1);
1272
1273 context.globals().elapsed_frames.set(3);
1274 context.drain_node_effects();
1275 assert!(borrow!(router_node.exiting_children).is_empty());
1276 assert_eq!(exiting_child.transition_phase.get(), TRANSITION_PHASE_IDLE);
1277 assert!(!pax_runtime_api::properties::property_has_direct_outbound(
1278 &context.globals().elapsed_frames.untyped(),
1279 &exiting_child.transition_playhead.untyped(),
1280 ));
1281 assert!(!pax_runtime_api::properties::property_has_direct_outbound(
1282 &context.globals().elapsed_millis.untyped(),
1283 &exiting_child.transition_playhead_millis.untyped(),
1284 ));
1285 }
1286
1287 #[test]
1288 fn active_enter_transition_returns_to_idle_after_duration() {
1289 let input_location = LocalProperty::new(route(&["alpha"]));
1290 let (_root, router_node, context) = mounted_router(
1291 input_location.clone(),
1292 vec![
1293 ControlFlowRouteBranchDefinition {
1294 path: Some("/alpha".to_string()),
1295 default: false,
1296 modal: false,
1297 child_ids: vec![],
1298 metadata: None,
1299 },
1300 ControlFlowRouteBranchDefinition {
1301 path: Some("/beta".to_string()),
1302 default: false,
1303 modal: false,
1304 child_ids: vec![],
1305 metadata: None,
1306 },
1307 ],
1308 vec![0..1, 1..2],
1309 vec![leaf(), leaf()],
1310 );
1311
1312 input_location.set(route(&["beta"]));
1313 router_node.recurse_update(&context);
1314
1315 let active = borrow!(router_node.active_children).clone();
1316 assert_eq!(active.len(), 1);
1317 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1318
1319 context.globals().elapsed_frames.set(9);
1320 context.drain_node_effects();
1321 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1322
1323 context.globals().elapsed_frames.set(10);
1324 context.drain_node_effects();
1325 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_IDLE);
1326 }
1327
1328 #[test]
1329 fn modal_branch_keeps_previous_route_active_underneath() {
1330 let input_location = LocalProperty::new(route(&["alpha"]));
1331 let (_root, router_node, context) = mounted_router(
1332 input_location.clone(),
1333 vec![
1334 ControlFlowRouteBranchDefinition {
1335 path: Some("/alpha".to_string()),
1336 default: false,
1337 modal: false,
1338 child_ids: vec![],
1339 metadata: None,
1340 },
1341 ControlFlowRouteBranchDefinition {
1342 path: Some("/tools".to_string()),
1343 default: false,
1344 modal: true,
1345 child_ids: vec![],
1346 metadata: None,
1347 },
1348 ],
1349 vec![0..1, 1..2],
1350 vec![leaf(), leaf()],
1351 );
1352
1353 let base_id = borrow!(router_node.active_children)[0].id;
1354
1355 input_location.set(route(&["tools"]));
1356 router_node.recurse_update(&context);
1357
1358 let active = borrow!(router_node.active_children).clone();
1359 let exiting = borrow!(router_node.exiting_children).clone();
1360 assert_eq!(active.len(), 2);
1361 assert!(exiting.is_empty());
1362 assert_ne!(active[0].id, base_id);
1363 assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
1364 assert_eq!(active[1].id, base_id);
1365 }
1366
1367 #[test]
1368 fn dismissing_modal_reveals_previous_route_without_remounting() {
1369 let input_location = LocalProperty::new(route(&["alpha"]));
1370 let (_root, router_node, context) = mounted_router(
1371 input_location.clone(),
1372 vec![
1373 ControlFlowRouteBranchDefinition {
1374 path: Some("/alpha".to_string()),
1375 default: false,
1376 modal: false,
1377 child_ids: vec![],
1378 metadata: None,
1379 },
1380 ControlFlowRouteBranchDefinition {
1381 path: Some("/tools".to_string()),
1382 default: false,
1383 modal: true,
1384 child_ids: vec![],
1385 metadata: None,
1386 },
1387 ],
1388 vec![0..1, 1..2],
1389 vec![leaf(), leaf()],
1390 );
1391
1392 let base_id = borrow!(router_node.active_children)[0].id;
1393
1394 input_location.set(route(&["tools"]));
1395 router_node.recurse_update(&context);
1396 let modal_id = borrow!(router_node.active_children)[0].id;
1397
1398 input_location.set(route(&["alpha"]));
1399 router_node.recurse_update(&context);
1400
1401 let active = borrow!(router_node.active_children).clone();
1402 let exiting = borrow!(router_node.exiting_children).clone();
1403 assert_eq!(active.len(), 1);
1404 assert_eq!(active[0].id, base_id);
1405 assert_eq!(exiting.len(), 1);
1406 assert_eq!(exiting[0].id, modal_id);
1407 assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
1408 }
1409
1410 #[test]
1411 fn no_match_transitions_out_previous_route_tree() {
1412 let input_location = LocalProperty::new(route(&["alpha"]));
1413 let (_root, router_node, context) = mounted_router(
1414 input_location.clone(),
1415 vec![ControlFlowRouteBranchDefinition {
1416 path: Some("/alpha".to_string()),
1417 default: false,
1418 modal: false,
1419 child_ids: vec![],
1420 metadata: None,
1421 }],
1422 vec![0..1],
1423 vec![leaf()],
1424 );
1425
1426 assert_eq!(borrow!(router_node.active_children).len(), 1);
1427
1428 input_location.set(route(&["missing"]));
1429 router_node.recurse_update(&context);
1430
1431 let active = borrow!(router_node.active_children).clone();
1432 let exiting = borrow!(router_node.exiting_children).clone();
1433 assert!(active.is_empty());
1434 assert_eq!(exiting.len(), 1);
1435 assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
1436 }
1437}