Skip to main content

pax_runtime/
conditional.rs

1use std::{iter, ops::Range, rc::Rc};
2use_RefCell!();
3
4use pax_runtime_api::pax_value::ImplToFromPaxAny;
5use pax_runtime_api::{borrow, borrow_mut, use_RefCell, LocalProperty, PaxValue, ToPaxValue};
6
7use crate::api::Layer;
8use crate::{
9    BaseInstance, ExpandedNode, InstanceFlags, InstanceNode, InstantiationArgs, RuntimeContext,
10};
11
12/// A special "control-flow" primitive, Conditional (`if`) allows for a
13/// subtree of a component template to be rendered conditionally,
14/// based on the value of the property `boolean_expression`.
15/// The Pax compiler handles ConditionalInstance specially
16/// with the `if` syntax in templates.
17pub struct ConditionalInstance {
18    base: BaseInstance,
19    branch_child_ranges: Vec<Range<usize>>,
20}
21
22impl ImplToFromPaxAny for ConditionalProperties {}
23
24///Contains the expression of a conditional, evaluated as an expression.
25#[derive(Default)]
26pub struct ConditionalProperties {
27    pub boolean_expression: LocalProperty<bool>,
28    pub conditional_branches: Vec<LocalProperty<bool>>,
29}
30
31impl ToPaxValue for ConditionalProperties {
32    fn to_pax_value(self) -> PaxValue {
33        PaxValue::Object(
34            vec![
35                (
36                    "boolean_expression".to_string(),
37                    self.boolean_expression.to_pax_value(),
38                ),
39                (
40                    "conditional_branches".to_string(),
41                    self.conditional_branches.to_pax_value(),
42                ),
43            ]
44            .into_iter()
45            .collect(),
46        )
47    }
48}
49
50impl InstanceNode for ConditionalInstance {
51    fn instantiate(args: InstantiationArgs) -> Rc<Self>
52    where
53        Self: Sized,
54    {
55        Rc::new(Self {
56            base: BaseInstance::new(
57                args,
58                InstanceFlags {
59                    invisible_to_slot: true,
60                    invisible_to_raycasting: true,
61                    layer: Layer::DontCare,
62                    is_component: false,
63                    is_slot: false,
64                },
65            ),
66            branch_child_ranges: vec![],
67        })
68    }
69
70    fn handle_mount(
71        self: Rc<Self>,
72        expanded_node: &Rc<ExpandedNode>,
73        context: &Rc<RuntimeContext>,
74    ) {
75        self.handle_setup(expanded_node, context, true);
76    }
77
78    fn handle_control_flow_node_expansion(
79        self: Rc<Self>,
80        expanded_node: &Rc<ExpandedNode>,
81        context: &Rc<RuntimeContext>,
82    ) {
83        self.handle_setup(expanded_node, context, false);
84    }
85
86    fn resolve_debug(
87        &self,
88        f: &mut std::fmt::Formatter,
89        _expanded_node: Option<&ExpandedNode>,
90    ) -> std::fmt::Result {
91        f.debug_struct("Conditional").finish()
92    }
93
94    fn base(&self) -> &BaseInstance {
95        &self.base
96    }
97}
98
99impl ConditionalInstance {
100    pub fn instantiate_with_branch_child_ranges(
101        args: InstantiationArgs,
102        branch_child_ranges: Vec<Range<usize>>,
103    ) -> Rc<Self> {
104        Rc::new(Self {
105            base: BaseInstance::new(
106                args,
107                InstanceFlags {
108                    invisible_to_slot: true,
109                    invisible_to_raycasting: true,
110                    layer: Layer::DontCare,
111                    is_component: false,
112                    is_slot: false,
113                },
114            ),
115            branch_child_ranges,
116        })
117    }
118
119    fn handle_setup(
120        self: Rc<Self>,
121        expanded_node: &Rc<ExpandedNode>,
122        context: &Rc<RuntimeContext>,
123        is_mount: bool,
124    ) {
125        let weak_ref_self = Rc::downgrade(expanded_node);
126        let cloned_self = Rc::clone(&self);
127        let cloned_context = Rc::clone(context);
128
129        let boolean_expression =
130            expanded_node.with_properties_unwrapped(|properties: &mut ConditionalProperties| {
131                properties.boolean_expression.clone()
132            });
133        let conditional_branches =
134            expanded_node.with_properties_unwrapped(|properties: &mut ConditionalProperties| {
135                properties.conditional_branches.clone()
136            });
137        let branch_conditions = if conditional_branches.is_empty() {
138            vec![boolean_expression]
139        } else {
140            conditional_branches
141        };
142
143        let deps = branch_conditions
144            .iter()
145            .map(|condition| condition.untyped())
146            .collect::<Vec<_>>();
147        let branch_child_ranges = self.branch_child_ranges.clone();
148
149        let old_active_branch = RefCell::new(None::<Option<usize>>);
150        let cached_children = RefCell::new(Vec::new());
151        expanded_node
152            .children
153            .replace_with(LocalProperty::computed_with_name(
154                move || {
155                    let Some(cloned_expanded_node) = weak_ref_self.upgrade() else {
156                        panic!("ran evaluator after expanded node dropped (conditional elem)")
157                    };
158                    let active_branch = branch_conditions
159                        .iter()
160                        .enumerate()
161                        .find_map(|(index, condition)| condition.get().then_some(index));
162                    if *borrow!(old_active_branch) == Some(active_branch) {
163                        return if cloned_expanded_node.attached.get() > 0 {
164                            cloned_expanded_node.current_attached_children()
165                        } else {
166                            borrow!(cached_children).clone()
167                        };
168                    }
169                    *borrow_mut!(old_active_branch) = Some(active_branch);
170
171                    let children = borrow!(cloned_self.base().get_instance_children());
172                    let effective_branch_ranges = if branch_child_ranges.is_empty() {
173                        vec![0..children.len()]
174                    } else {
175                        branch_child_ranges.clone()
176                    };
177                    let selected_range = active_branch
178                        .and_then(|index| effective_branch_ranges.get(index).cloned())
179                        .unwrap_or(0..0);
180                    let start = selected_range.start.min(children.len());
181                    let end = selected_range.end.min(children.len());
182                    let env = Rc::clone(&cloned_expanded_node.stack);
183                    let mut selected_children = Vec::new();
184                    for template in children[start..end].iter() {
185                        let rescued = (is_mount && cloned_expanded_node.attached.get() > 0)
186                            .then(|| {
187                                cloned_expanded_node.rescue_exiting_child_matching(|child| {
188                                    let child_template = borrow!(child.instance_node).clone();
189                                    Rc::ptr_eq(&child_template, template)
190                                })
191                            })
192                            .flatten();
193                        let child = rescued.unwrap_or_else(|| {
194                            cloned_expanded_node
195                                .create_children_detached(
196                                    iter::once((Rc::clone(template), Rc::clone(&env))),
197                                    &cloned_context,
198                                    &Rc::downgrade(&cloned_expanded_node),
199                                )
200                                .pop()
201                                .expect("conditional child creation returned no child")
202                        });
203                        if !is_mount && child.attached.get() == 0 {
204                            child.recurse_control_flow_expansion(&cloned_context);
205                        }
206                        selected_children.push(child);
207                    }
208                    let ret = if is_mount {
209                        cloned_expanded_node.attach_children(
210                            selected_children,
211                            &cloned_context,
212                            &cloned_expanded_node.parent_frame,
213                        )
214                    } else {
215                        selected_children
216                    };
217                    *borrow_mut!(cached_children) = ret.clone();
218                    ret
219                },
220                &deps,
221                &format!("conditional_children (node id: {})", expanded_node.id.0),
222            ));
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use crate::api::math::Transform2;
230    use crate::api::CommonProperties;
231    use crate::{
232        BaseInstance, ComponentInstance, Globals, InstanceFlags, RouteLocation,
233        RuntimePropertiesStackFrame, TransformAndBounds,
234    };
235    use pax_manifest::cartridge_generation::{
236        ComponentTransitionConfig, TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT,
237    };
238    use pax_runtime_api::pax_value::{PaxAny, ToFromPaxAny};
239    use pax_runtime_api::{LocalProperty, Platform, TargetInfo, OS};
240    use std::cell::RefCell;
241
242    fn test_globals() -> Globals {
243        Globals {
244            elapsed_frames: LocalProperty::new(0),
245            elapsed_millis: LocalProperty::new(0),
246            viewport: LocalProperty::new(TransformAndBounds {
247                transform: Transform2::identity(),
248                bounds: (100.0, 100.0),
249            }),
250            gyro: LocalProperty::new(Default::default()),
251            accel: LocalProperty::new(Default::default()),
252            route_location: LocalProperty::new(RouteLocation::root()),
253            browser_allows_scroller_vector_layers: LocalProperty::new(true),
254            browser_allows_nested_scroller_vector_layers: LocalProperty::new(true),
255            platform: Platform::Unknown,
256            os: OS::Unknown,
257            target: TargetInfo::new(Platform::Unknown, OS::Unknown),
258            #[cfg(feature = "designtime")]
259            designtime: crate::test_support::designtime(),
260            get_elapsed_millis: Rc::new(|| 0),
261        }
262    }
263
264    fn default_properties_factory() -> Box<
265        dyn Fn(
266            Rc<RuntimePropertiesStackFrame>,
267            Option<Rc<ExpandedNode>>,
268        ) -> Option<Rc<RefCell<PaxAny>>>,
269    > {
270        Box::new(|_, expanded_node| {
271            expanded_node
272                .is_none()
273                .then(|| Rc::new(RefCell::new(PaxAny::Builtin(Default::default()))))
274        })
275    }
276
277    fn default_common_properties_factory() -> Box<
278        dyn Fn(
279            Rc<RuntimePropertiesStackFrame>,
280            Option<Rc<ExpandedNode>>,
281        ) -> Option<Rc<RefCell<CommonProperties>>>,
282    > {
283        Box::new(|_, expanded_node| {
284            expanded_node
285                .is_none()
286                .then(|| Rc::new(RefCell::new(CommonProperties::default())))
287        })
288    }
289
290    fn component_args(template: Option<Vec<Rc<dyn InstanceNode>>>) -> InstantiationArgs {
291        InstantiationArgs {
292            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
293                default_common_properties_factory(),
294            ),
295            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
296            handler_registry: None,
297            children: None,
298            component_template: template.map(RefCell::new),
299            component_settings: None,
300            template_node_identifier: None,
301            template_node_type_id: None,
302            template_node_selector_info: None,
303            transition_config: Default::default(),
304            properties_scope: crate::PropertiesScopeInit::None,
305        }
306    }
307
308    fn transition_leaf() -> Rc<dyn InstanceNode> {
309        let mut args = component_args(Some(Vec::new()));
310        args.transition_config = ComponentTransitionConfig {
311            has_enter: true,
312            enter_frame_count: 10,
313            has_exit: true,
314            exit_frame_count: 10,
315            timeout_ms: 5_000,
316            ..Default::default()
317        };
318        ComponentInstance::instantiate(args)
319    }
320
321    fn mounted_conditional(
322        condition: LocalProperty<bool>,
323    ) -> (Rc<ExpandedNode>, Rc<ExpandedNode>, Rc<RuntimeContext>) {
324        let conditional: Rc<dyn InstanceNode> =
325            ConditionalInstance::instantiate(conditional_args(condition, vec![transition_leaf()]));
326        let root_component =
327            ComponentInstance::instantiate(component_args(Some(vec![conditional])));
328        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
329        let root = ExpandedNode::initialize_root(root_component, &context);
330        root.recurse_update(&context);
331        let conditional_node = root.children.get().remove(0);
332        (root, conditional_node, context)
333    }
334
335    fn conditional_args(
336        condition: LocalProperty<bool>,
337        children: Vec<Rc<dyn InstanceNode>>,
338    ) -> InstantiationArgs {
339        InstantiationArgs {
340            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
341                default_common_properties_factory(),
342            ),
343            prototypical_properties: crate::PropertiesInit::Factory(Box::new(
344                move |_, expanded_node| {
345                    expanded_node.is_none().then(|| {
346                        Rc::new(RefCell::new(
347                            ConditionalProperties {
348                                boolean_expression: condition.clone(),
349                                conditional_branches: Vec::new(),
350                            }
351                            .to_pax_any(),
352                        ))
353                    })
354                },
355            )),
356            handler_registry: None,
357            children: Some(RefCell::new(children)),
358            component_template: None,
359            component_settings: None,
360            template_node_identifier: None,
361            template_node_type_id: None,
362            template_node_selector_info: None,
363            transition_config: Default::default(),
364            properties_scope: crate::PropertiesScopeInit::None,
365        }
366    }
367
368    struct TestLeaf {
369        base: BaseInstance,
370    }
371
372    impl InstanceNode for TestLeaf {
373        fn instantiate(args: InstantiationArgs) -> Rc<Self>
374        where
375            Self: Sized,
376        {
377            Rc::new(Self {
378                base: BaseInstance::new(
379                    args,
380                    InstanceFlags {
381                        invisible_to_slot: false,
382                        invisible_to_raycasting: true,
383                        layer: Layer::DontCare,
384                        is_component: false,
385                        is_slot: false,
386                    },
387                ),
388            })
389        }
390
391        fn resolve_debug(
392            &self,
393            f: &mut std::fmt::Formatter,
394            _expanded_node: Option<&ExpandedNode>,
395        ) -> std::fmt::Result {
396            f.debug_struct("TestLeaf").finish()
397        }
398
399        fn base(&self) -> &BaseInstance {
400            &self.base
401        }
402    }
403
404    #[test]
405    fn detached_conditional_keeps_cached_children_on_same_branch_recompute() {
406        let condition = LocalProperty::new(true);
407        let leaf: Rc<dyn InstanceNode> = TestLeaf::instantiate(component_args(Some(Vec::new())));
408        let conditional =
409            ConditionalInstance::instantiate(conditional_args(condition.clone(), vec![leaf]));
410        let root_component = ComponentInstance::instantiate(component_args(None));
411        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
412        let root = ExpandedNode::initialize_root(root_component, &context);
413
414        let detached = root
415            .create_children_detached(
416                vec![(
417                    conditional.clone() as Rc<dyn InstanceNode>,
418                    Rc::clone(&root.stack),
419                )],
420                &context,
421                &Rc::downgrade(&root),
422            )
423            .remove(0);
424
425        conditional.clone().handle_setup(&detached, &context, false);
426
427        let initial = detached.children.get();
428        assert_eq!(initial.len(), 1);
429        let initial_id = initial[0].id;
430
431        condition.set(true);
432        let recomputed = detached.children.get();
433        assert_eq!(recomputed.len(), 1);
434        assert_eq!(recomputed[0].id, initial_id);
435    }
436
437    #[test]
438    fn conditional_prunes_completed_exit_and_can_reenter_during_exit() {
439        let condition = LocalProperty::new(true);
440        let (root, conditional_node, context) = mounted_conditional(condition.clone());
441        let initial_id = borrow!(conditional_node.active_children)[0].id;
442
443        condition.set(false);
444        root.recurse_update(&context);
445        assert!(borrow!(conditional_node.active_children).is_empty());
446        assert_eq!(borrow!(conditional_node.exiting_children).len(), 1);
447        assert_eq!(
448            borrow!(conditional_node.exiting_children)[0]
449                .transition_phase
450                .get(),
451            TRANSITION_PHASE_EXIT
452        );
453
454        context.globals().elapsed_frames.set(5);
455        condition.set(true);
456        root.recurse_update(&context);
457
458        let active = borrow!(conditional_node.active_children).clone();
459        assert_eq!(active.len(), 1);
460        assert_eq!(active[0].id, initial_id);
461        assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
462        assert!(borrow!(conditional_node.exiting_children).is_empty());
463
464        context.globals().elapsed_frames.set(10);
465        context.drain_node_effects();
466        assert!(borrow!(conditional_node.exiting_children).is_empty());
467        assert_eq!(conditional_node.children.get().len(), 1);
468    }
469
470    #[test]
471    fn conditional_survives_repeated_rapid_toggles_during_transitions() {
472        let condition = LocalProperty::new(true);
473        let (root, conditional_node, context) = mounted_conditional(condition.clone());
474        let stable_id = borrow!(conditional_node.active_children)[0].id;
475
476        for frame in 1..=6 {
477            context.globals().elapsed_frames.set(frame);
478            condition.set(frame % 2 == 0);
479            root.recurse_update(&context);
480
481            let expected_active = usize::from(frame % 2 == 0);
482            assert_eq!(
483                borrow!(conditional_node.active_children).len(),
484                expected_active
485            );
486            assert_eq!(borrow!(conditional_node.mounted_children).len(), 1);
487            assert_eq!(borrow!(conditional_node.mounted_children)[0].id, stable_id);
488        }
489
490        context.globals().elapsed_frames.set(20);
491        context.drain_node_effects();
492        assert_eq!(borrow!(conditional_node.active_children).len(), 1);
493        assert!(borrow!(conditional_node.exiting_children).is_empty());
494    }
495
496    #[test]
497    fn conditional_refreshes_computed_children_after_exit_cleanup() {
498        let condition = LocalProperty::new(true);
499        let (_root, conditional_node, context) = mounted_conditional(condition.clone());
500
501        condition.set(false);
502        let _ = conditional_node.children.get();
503        context.globals().elapsed_frames.set(10);
504        context.drain_node_effects();
505
506        assert!(borrow!(conditional_node.active_children).is_empty());
507        assert!(borrow!(conditional_node.exiting_children).is_empty());
508        assert!(conditional_node.children.get().is_empty());
509    }
510}