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, PaxValue, Property, 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: Property<bool>,
28    pub conditional_branches: Vec<Property<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(Property::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::{Platform, Property, TargetInfo, OS};
240    use std::cell::RefCell;
241
242    fn test_globals() -> Globals {
243        Globals {
244            elapsed_frames: Property::new(0),
245            elapsed_millis: Property::new(0),
246            viewport: Property::new(TransformAndBounds {
247                transform: Transform2::identity(),
248                bounds: (100.0, 100.0),
249            }),
250            gyro: Property::new(Default::default()),
251            accel: Property::new(Default::default()),
252            route_location: Property::new(RouteLocation::root()),
253            browser_allows_scroller_vector_layers: Property::new(true),
254            browser_allows_nested_scroller_vector_layers: Property::new(true),
255            platform: Platform::Unknown,
256            os: OS::Unknown,
257            target: TargetInfo::new(Platform::Unknown, OS::Unknown),
258            get_elapsed_millis: Rc::new(|| 0),
259        }
260    }
261
262    fn default_properties_factory() -> Box<
263        dyn Fn(
264            Rc<RuntimePropertiesStackFrame>,
265            Option<Rc<ExpandedNode>>,
266        ) -> Option<Rc<RefCell<PaxAny>>>,
267    > {
268        Box::new(|_, expanded_node| {
269            expanded_node
270                .is_none()
271                .then(|| Rc::new(RefCell::new(PaxAny::Builtin(Default::default()))))
272        })
273    }
274
275    fn default_common_properties_factory() -> Box<
276        dyn Fn(
277            Rc<RuntimePropertiesStackFrame>,
278            Option<Rc<ExpandedNode>>,
279        ) -> Option<Rc<RefCell<CommonProperties>>>,
280    > {
281        Box::new(|_, expanded_node| {
282            expanded_node
283                .is_none()
284                .then(|| Rc::new(RefCell::new(CommonProperties::default())))
285        })
286    }
287
288    fn component_args(template: Option<Vec<Rc<dyn InstanceNode>>>) -> InstantiationArgs {
289        InstantiationArgs {
290            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
291                default_common_properties_factory(),
292            ),
293            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
294            handler_registry: None,
295            children: None,
296            component_template: template.map(RefCell::new),
297            component_settings: None,
298            template_node_identifier: None,
299            template_node_type_id: None,
300            template_node_selector_info: None,
301            transition_config: Default::default(),
302            properties_scope: crate::PropertiesScopeInit::None,
303        }
304    }
305
306    fn transition_leaf() -> Rc<dyn InstanceNode> {
307        let mut args = component_args(Some(Vec::new()));
308        args.transition_config = ComponentTransitionConfig {
309            has_enter: true,
310            enter_frame_count: 10,
311            has_exit: true,
312            exit_frame_count: 10,
313            timeout_ms: 5_000,
314            ..Default::default()
315        };
316        ComponentInstance::instantiate(args)
317    }
318
319    fn mounted_conditional(
320        condition: Property<bool>,
321    ) -> (Rc<ExpandedNode>, Rc<ExpandedNode>, Rc<RuntimeContext>) {
322        let conditional: Rc<dyn InstanceNode> =
323            ConditionalInstance::instantiate(conditional_args(condition, vec![transition_leaf()]));
324        let root_component =
325            ComponentInstance::instantiate(component_args(Some(vec![conditional])));
326        let context = Rc::new(RuntimeContext::new(test_globals()));
327        let root = ExpandedNode::initialize_root(root_component, &context);
328        root.recurse_update(&context);
329        let conditional_node = root.children.get().remove(0);
330        (root, conditional_node, context)
331    }
332
333    fn conditional_args(
334        condition: Property<bool>,
335        children: Vec<Rc<dyn InstanceNode>>,
336    ) -> InstantiationArgs {
337        InstantiationArgs {
338            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
339                default_common_properties_factory(),
340            ),
341            prototypical_properties: crate::PropertiesInit::Factory(Box::new(
342                move |_, expanded_node| {
343                    expanded_node.is_none().then(|| {
344                        Rc::new(RefCell::new(
345                            ConditionalProperties {
346                                boolean_expression: condition.clone(),
347                                conditional_branches: Vec::new(),
348                            }
349                            .to_pax_any(),
350                        ))
351                    })
352                },
353            )),
354            handler_registry: None,
355            children: Some(RefCell::new(children)),
356            component_template: None,
357            component_settings: None,
358            template_node_identifier: None,
359            template_node_type_id: None,
360            template_node_selector_info: None,
361            transition_config: Default::default(),
362            properties_scope: crate::PropertiesScopeInit::None,
363        }
364    }
365
366    struct TestLeaf {
367        base: BaseInstance,
368    }
369
370    impl InstanceNode for TestLeaf {
371        fn instantiate(args: InstantiationArgs) -> Rc<Self>
372        where
373            Self: Sized,
374        {
375            Rc::new(Self {
376                base: BaseInstance::new(
377                    args,
378                    InstanceFlags {
379                        invisible_to_slot: false,
380                        invisible_to_raycasting: true,
381                        layer: Layer::DontCare,
382                        is_component: false,
383                        is_slot: false,
384                    },
385                ),
386            })
387        }
388
389        fn resolve_debug(
390            &self,
391            f: &mut std::fmt::Formatter,
392            _expanded_node: Option<&ExpandedNode>,
393        ) -> std::fmt::Result {
394            f.debug_struct("TestLeaf").finish()
395        }
396
397        fn base(&self) -> &BaseInstance {
398            &self.base
399        }
400    }
401
402    #[test]
403    fn detached_conditional_keeps_cached_children_on_same_branch_recompute() {
404        let condition = Property::new(true);
405        let leaf: Rc<dyn InstanceNode> = TestLeaf::instantiate(component_args(Some(Vec::new())));
406        let conditional =
407            ConditionalInstance::instantiate(conditional_args(condition.clone(), vec![leaf]));
408        let root_component = ComponentInstance::instantiate(component_args(None));
409        let context = Rc::new(RuntimeContext::new(test_globals()));
410        let root = ExpandedNode::initialize_root(root_component, &context);
411
412        let detached = root
413            .create_children_detached(
414                vec![(
415                    conditional.clone() as Rc<dyn InstanceNode>,
416                    Rc::clone(&root.stack),
417                )],
418                &context,
419                &Rc::downgrade(&root),
420            )
421            .remove(0);
422
423        conditional.clone().handle_setup(&detached, &context, false);
424
425        let initial = detached.children.get();
426        assert_eq!(initial.len(), 1);
427        let initial_id = initial[0].id;
428
429        condition.set(true);
430        let recomputed = detached.children.get();
431        assert_eq!(recomputed.len(), 1);
432        assert_eq!(recomputed[0].id, initial_id);
433    }
434
435    #[test]
436    fn conditional_prunes_completed_exit_and_can_reenter_during_exit() {
437        let condition = Property::new(true);
438        let (root, conditional_node, context) = mounted_conditional(condition.clone());
439        let initial_id = borrow!(conditional_node.active_children)[0].id;
440
441        condition.set(false);
442        root.recurse_update(&context);
443        assert!(borrow!(conditional_node.active_children).is_empty());
444        assert_eq!(borrow!(conditional_node.exiting_children).len(), 1);
445        assert_eq!(
446            borrow!(conditional_node.exiting_children)[0]
447                .transition_phase
448                .get(),
449            TRANSITION_PHASE_EXIT
450        );
451
452        context.globals().elapsed_frames.set(5);
453        condition.set(true);
454        root.recurse_update(&context);
455
456        let active = borrow!(conditional_node.active_children).clone();
457        assert_eq!(active.len(), 1);
458        assert_eq!(active[0].id, initial_id);
459        assert_eq!(active[0].transition_phase.get(), TRANSITION_PHASE_ENTER);
460        assert!(borrow!(conditional_node.exiting_children).is_empty());
461
462        context.globals().elapsed_frames.set(10);
463        context.drain_node_effects();
464        assert!(borrow!(conditional_node.exiting_children).is_empty());
465        assert_eq!(conditional_node.children.get().len(), 1);
466    }
467
468    #[test]
469    fn conditional_survives_repeated_rapid_toggles_during_transitions() {
470        let condition = Property::new(true);
471        let (root, conditional_node, context) = mounted_conditional(condition.clone());
472        let stable_id = borrow!(conditional_node.active_children)[0].id;
473
474        for frame in 1..=6 {
475            context.globals().elapsed_frames.set(frame);
476            condition.set(frame % 2 == 0);
477            root.recurse_update(&context);
478
479            let expected_active = usize::from(frame % 2 == 0);
480            assert_eq!(
481                borrow!(conditional_node.active_children).len(),
482                expected_active
483            );
484            assert_eq!(borrow!(conditional_node.mounted_children).len(), 1);
485            assert_eq!(borrow!(conditional_node.mounted_children)[0].id, stable_id);
486        }
487
488        context.globals().elapsed_frames.set(20);
489        context.drain_node_effects();
490        assert_eq!(borrow!(conditional_node.active_children).len(), 1);
491        assert!(borrow!(conditional_node.exiting_children).is_empty());
492    }
493
494    #[test]
495    fn conditional_refreshes_computed_children_after_exit_cleanup() {
496        let condition = Property::new(true);
497        let (_root, conditional_node, context) = mounted_conditional(condition.clone());
498
499        condition.set(false);
500        let _ = conditional_node.children.get();
501        context.globals().elapsed_frames.set(10);
502        context.drain_node_effects();
503
504        assert!(borrow!(conditional_node.active_children).is_empty());
505        assert!(borrow!(conditional_node.exiting_children).is_empty());
506        assert!(conditional_node.children.get().is_empty());
507    }
508}