Skip to main content

pax_runtime/
repeat.rs

1use std::collections::{HashMap, HashSet};
2use std::iter;
3use std::rc::Rc;
4use_RefCell!();
5
6use pax_language::Computable;
7use pax_manifest::ExpressionInfo;
8use pax_runtime_api::CoercionRules;
9use pax_runtime_api::{
10    borrow, borrow_mut, use_RefCell, ImplToFromPaxAny, LocalProperty, PaxValue, ToPaxValue,
11    Variable,
12};
13
14use crate::api::Layer;
15use crate::{
16    BaseInstance, ExpandedNode, InstanceFlags, InstanceNode, InstantiationArgs, RuntimeContext,
17    RuntimePropertiesStackFrame,
18};
19
20mod value_eq;
21
22fn update_repeat_bindings(
23    index: &LocalProperty<usize>,
24    item: &LocalProperty<PaxValue>,
25    i: usize,
26    value: PaxValue,
27) {
28    index.set_if_neq(i);
29    // PaxValue's language equality is approximate. Invalidation requires exact
30    // representation equality, otherwise small animation/data edits disappear.
31    if !item.read(|previous| value_eq::same_value(previous, &value)) {
32        item.set(value);
33    }
34}
35
36const INTERNAL_REPEAT_INDEX_SYMBOL: &str = "$repeat_index";
37const INTERNAL_REPEAT_ELEMENT_SYMBOL: &str = "$repeat_element";
38const INTERNAL_REPEAT_KEY_SYMBOL: &str = "$repeat_key";
39
40/// A special "control-flow" primitive associated with the `for` statement.
41/// Repeat allows for nodes to be rendered dynamically per data specified in `source_expression`.
42/// That is: for a `source_expression` of length `n`, `Repeat` will render its
43/// template `n` times, each with an embedded component context (`RepeatItem`)
44/// with an index `i` and a pointer to that relevant datum `source_expression[i]`
45pub struct RepeatInstance {
46    pub base: BaseInstance,
47}
48
49impl ImplToFromPaxAny for RepeatProperties {}
50///Contains modal _vec_ and _range_ variants, describing whether the Repeat source
51///is encoded as a `Vec<T>` (where T is a `PaxValue` properties type) or as a `Range<isize>`
52#[derive(Default)]
53pub struct RepeatProperties {
54    pub source_expression: LocalProperty<PaxValue>,
55    pub iterator_i_symbol: LocalProperty<Option<String>>,
56    pub iterator_elem_symbol: LocalProperty<Option<String>>,
57    pub repeat_key_expression: Option<ExpressionInfo>,
58}
59
60impl ToPaxValue for RepeatProperties {
61    fn to_pax_value(self) -> PaxValue {
62        PaxValue::Object(
63            vec![
64                (
65                    "source_expression".to_string(),
66                    self.source_expression.to_pax_value(),
67                ),
68                (
69                    "iterator_i_symbol".to_string(),
70                    self.iterator_i_symbol.to_pax_value(),
71                ),
72                (
73                    "iterator_elem_symbol".to_string(),
74                    self.iterator_elem_symbol.to_pax_value(),
75                ),
76            ]
77            .into_iter()
78            .collect(),
79        )
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::api::math::Transform2;
87    use crate::api::CommonProperties;
88    use crate::{ComponentInstance, ExpandedNode, Globals, RouteLocation, TransformAndBounds};
89    use pax_language::interpreter::property_resolution::IdentifierResolver;
90    use pax_language::parse_pax_expression;
91    use pax_manifest::cartridge_generation::{
92        ComponentTransitionConfig, TRANSITION_PHASE_ENTER, TRANSITION_PHASE_EXIT,
93        TRANSITION_PHASE_IDLE,
94    };
95    use pax_runtime_api::pax_value::{PaxAny, ToFromPaxAny};
96    use pax_runtime_api::{borrow, CoercionRules, Numeric, Platform, Size, TargetInfo, OS};
97    use std::cell::RefCell;
98
99    fn test_globals() -> Globals {
100        Globals {
101            elapsed_frames: LocalProperty::new(0),
102            elapsed_millis: LocalProperty::new(0),
103            viewport: LocalProperty::new(TransformAndBounds {
104                transform: Transform2::identity(),
105                bounds: (100.0, 100.0),
106            }),
107            gyro: LocalProperty::new(Default::default()),
108            accel: LocalProperty::new(Default::default()),
109            route_location: LocalProperty::new(RouteLocation::root()),
110            browser_allows_scroller_vector_layers: LocalProperty::new(true),
111            browser_allows_nested_scroller_vector_layers: LocalProperty::new(true),
112            platform: Platform::Unknown,
113            os: OS::Unknown,
114            target: TargetInfo::new(Platform::Unknown, OS::Unknown),
115            #[cfg(feature = "designtime")]
116            designtime: crate::test_support::designtime(),
117            get_elapsed_millis: Rc::new(|| 0),
118        }
119    }
120
121    fn default_properties_factory() -> Box<
122        dyn Fn(
123            Rc<RuntimePropertiesStackFrame>,
124            Option<Rc<ExpandedNode>>,
125        ) -> Option<Rc<RefCell<PaxAny>>>,
126    > {
127        Box::new(|_, expanded_node| {
128            expanded_node
129                .is_none()
130                .then(|| Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::default()))))
131        })
132    }
133
134    fn default_common_properties_factory() -> Box<
135        dyn Fn(
136            Rc<RuntimePropertiesStackFrame>,
137            Option<Rc<ExpandedNode>>,
138        ) -> Option<Rc<RefCell<CommonProperties>>>,
139    > {
140        Box::new(|_, expanded_node| {
141            expanded_node
142                .is_none()
143                .then(|| Rc::new(RefCell::new(CommonProperties::default())))
144        })
145    }
146
147    fn component_args(template: Option<Vec<Rc<dyn InstanceNode>>>) -> InstantiationArgs {
148        InstantiationArgs {
149            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
150                default_common_properties_factory(),
151            ),
152            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
153            handler_registry: None,
154            children: None,
155            component_template: template.map(RefCell::new),
156            component_settings: None,
157            template_node_identifier: None,
158            template_node_type_id: None,
159            template_node_selector_info: None,
160            transition_config: Default::default(),
161            properties_scope: crate::PropertiesScopeInit::None,
162        }
163    }
164
165    fn leaf_args(transition_config: ComponentTransitionConfig) -> InstantiationArgs {
166        InstantiationArgs {
167            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
168                default_common_properties_factory(),
169            ),
170            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
171            handler_registry: None,
172            children: None,
173            component_template: Some(RefCell::new(Vec::new())),
174            component_settings: None,
175            template_node_identifier: None,
176            template_node_type_id: None,
177            template_node_selector_info: None,
178            transition_config,
179            properties_scope: crate::PropertiesScopeInit::None,
180        }
181    }
182
183    fn repeat_args(
184        source: LocalProperty<PaxValue>,
185        children: Vec<Rc<dyn InstanceNode>>,
186    ) -> InstantiationArgs {
187        repeat_args_with_key(source, children, true)
188    }
189
190    fn unkeyed_repeat_args(
191        source: LocalProperty<PaxValue>,
192        children: Vec<Rc<dyn InstanceNode>>,
193    ) -> InstantiationArgs {
194        repeat_args_with_key(source, children, false)
195    }
196
197    fn repeat_args_with_key(
198        source: LocalProperty<PaxValue>,
199        children: Vec<Rc<dyn InstanceNode>>,
200        use_key: bool,
201    ) -> InstantiationArgs {
202        let key_expression = ExpressionInfo::new(parse_pax_expression("item.id").unwrap());
203        let source_for_factory = source.clone();
204        InstantiationArgs {
205            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
206                default_common_properties_factory(),
207            ),
208            prototypical_properties: crate::PropertiesInit::Factory(Box::new(
209                move |_, expanded_node| {
210                    expanded_node.is_none().then(|| {
211                        Rc::new(RefCell::new(
212                            RepeatProperties {
213                                source_expression: source_for_factory.clone(),
214                                iterator_i_symbol: LocalProperty::new(Some("i".to_string())),
215                                iterator_elem_symbol: LocalProperty::new(Some("item".to_string())),
216                                repeat_key_expression: use_key.then(|| key_expression.clone()),
217                            }
218                            .to_pax_any(),
219                        ))
220                    })
221                },
222            )),
223            handler_registry: None,
224            children: Some(RefCell::new(children)),
225            component_template: None,
226            component_settings: None,
227            template_node_identifier: None,
228            template_node_type_id: None,
229            template_node_selector_info: None,
230            transition_config: Default::default(),
231            properties_scope: crate::PropertiesScopeInit::None,
232        }
233    }
234
235    fn positioned_leaf_args() -> InstantiationArgs {
236        InstantiationArgs {
237            prototypical_common_properties: crate::CommonPropertiesInit::Factory(Box::new(
238                |env, expanded_node| {
239                    if expanded_node.is_some() {
240                        return None;
241                    }
242                    let i_untyped = env.resolve_symbol_as_erased_property("i").unwrap();
243                    let i = LocalProperty::<usize>::new_from_untyped(i_untyped.clone());
244                    let deps = [i_untyped];
245
246                    let mut cp = CommonProperties::default();
247                    cp.x = LocalProperty::computed(
248                        move || Some(Size::Pixels((i.get() as f64 * 10.0).into())),
249                        &deps,
250                    );
251                    cp.width = LocalProperty::new(Some(Size::Pixels(10.into())));
252                    cp.height = LocalProperty::new(Some(Size::Pixels(10.into())));
253                    Some(Rc::new(RefCell::new(cp)))
254                },
255            )),
256            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
257            handler_registry: None,
258            children: None,
259            component_template: Some(RefCell::new(Vec::new())),
260            component_settings: None,
261            template_node_identifier: None,
262            template_node_type_id: None,
263            template_node_selector_info: None,
264            transition_config: Default::default(),
265            properties_scope: crate::PropertiesScopeInit::None,
266        }
267    }
268
269    fn transitioned_positioned_leaf_args() -> InstantiationArgs {
270        let mut args = positioned_leaf_args();
271        args.transition_config = ComponentTransitionConfig {
272            has_enter: true,
273            enter_frame_count: 10,
274            has_exit: true,
275            exit_frame_count: 10,
276            timeout_ms: 5_000,
277            ..Default::default()
278        };
279        args
280    }
281
282    fn expression_positioned_leaf_args(expr: &str) -> InstantiationArgs {
283        let expression = ExpressionInfo::new(parse_pax_expression(expr).unwrap());
284        InstantiationArgs {
285            prototypical_common_properties: crate::CommonPropertiesInit::Factory(Box::new(
286                move |env, expanded_node| {
287                    if expanded_node.is_some() {
288                        return None;
289                    }
290                    let mut deps = Vec::new();
291                    for dependency in &expression.dependencies {
292                        let property = env
293                            .resolve_symbol_as_erased_property(dependency)
294                            .unwrap_or_else(|| panic!("missing dependency: {dependency}"));
295                        deps.push(property);
296                    }
297
298                    let expression_for_eval = expression.clone();
299                    let env_for_eval = Rc::clone(&env);
300                    let mut cp = CommonProperties::default();
301                    cp.x = LocalProperty::computed(
302                        move || {
303                            let env_for_eval: Rc<dyn IdentifierResolver> = env_for_eval.clone();
304                            expression_for_eval
305                                .expression
306                                .compute(env_for_eval)
307                                .ok()
308                                .and_then(|value| Size::try_coerce(value).ok())
309                        },
310                        &deps,
311                    );
312                    cp.width = LocalProperty::new(Some(Size::Pixels(10.into())));
313                    cp.height = LocalProperty::new(Some(Size::Pixels(10.into())));
314                    Some(Rc::new(RefCell::new(cp)))
315                },
316            )),
317            prototypical_properties: crate::PropertiesInit::Factory(default_properties_factory()),
318            handler_registry: None,
319            children: None,
320            component_template: Some(RefCell::new(Vec::new())),
321            component_settings: None,
322            template_node_identifier: None,
323            template_node_type_id: None,
324            template_node_selector_info: None,
325            transition_config: Default::default(),
326            properties_scope: crate::PropertiesScopeInit::None,
327        }
328    }
329
330    fn transitioned_expression_positioned_leaf_args(expr: &str) -> InstantiationArgs {
331        let mut args = expression_positioned_leaf_args(expr);
332        args.transition_config = ComponentTransitionConfig {
333            has_enter: true,
334            enter_frame_count: 10,
335            has_exit: true,
336            exit_frame_count: 10,
337            timeout_ms: 5_000,
338            ..Default::default()
339        };
340        args
341    }
342
343    fn item(id: &str) -> PaxValue {
344        item_with_x(id, 0.0)
345    }
346
347    fn item_with_x(id: &str, x: f64) -> PaxValue {
348        PaxValue::Object(
349            vec![
350                ("id".to_string(), PaxValue::String(id.to_string())),
351                ("x".to_string(), x.to_pax_value()),
352            ]
353            .into_iter()
354            .collect(),
355        )
356    }
357
358    fn source(ids: &[&str]) -> PaxValue {
359        PaxValue::Vec(ids.iter().map(|id| item(id)).collect())
360    }
361
362    fn source_with_x(entries: &[(&str, f64)]) -> PaxValue {
363        PaxValue::Vec(entries.iter().map(|(id, x)| item_with_x(id, *x)).collect())
364    }
365
366    fn ids(nodes: &[Rc<ExpandedNode>]) -> Vec<u32> {
367        nodes.iter().map(|node| node.id.0).collect()
368    }
369
370    fn mounted_repeat(source: LocalProperty<PaxValue>) -> (Rc<ExpandedNode>, Rc<RuntimeContext>) {
371        let leaf: Rc<dyn InstanceNode> =
372            ComponentInstance::instantiate(leaf_args(Default::default()));
373        let repeat: Rc<dyn InstanceNode> =
374            RepeatInstance::instantiate(repeat_args(source, vec![leaf]));
375        let root = ComponentInstance::instantiate(component_args(Some(vec![repeat])));
376        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
377        let root = ExpandedNode::initialize_root(root, &context);
378        root.recurse_update(&context);
379        context.drain_node_effects();
380        (root, context)
381    }
382
383    fn binding_probe(
384        child: &ExpandedNode,
385        symbol: &str,
386    ) -> (LocalProperty<()>, Rc<std::cell::Cell<usize>>) {
387        let property = child
388            .stack
389            .resolve_symbol_as_erased_property(symbol)
390            .unwrap();
391        let runs = Rc::new(std::cell::Cell::new(0));
392        let count = Rc::clone(&runs);
393        let probe = LocalProperty::computed(move || count.set(count.get() + 1), &[property]);
394        probe.get();
395        (probe, runs)
396    }
397
398    #[test]
399    fn keyed_repeat_only_notifies_changed_items_and_indices() {
400        let source_property = LocalProperty::new(source_with_x(&[("a", 1.0), ("b", 2.0)]));
401        let (root, context) = mounted_repeat(source_property.clone());
402        let repeat = root.children.get().remove(0);
403        let children = repeat.children.get();
404        let probes: Vec<_> = children
405            .iter()
406            .flat_map(|child| [binding_probe(child, "item"), binding_probe(child, "i")])
407            .collect();
408        let counts = || {
409            probes
410                .iter()
411                .map(|(probe, runs)| {
412                    probe.get();
413                    runs.get()
414                })
415                .collect::<Vec<_>>()
416        };
417
418        source_property.set(source_with_x(&[("a", 1.0), ("b", 2.0)]));
419        context.drain_node_effects();
420        assert_eq!(ids(&repeat.children.get()), ids(&children));
421        assert_eq!(
422            counts(),
423            [1, 1, 1, 1],
424            "identical source must not notify retained items"
425        );
426
427        source_property.set(source_with_x(&[("a", 1.0 + 1e-8), ("b", 2.0)]));
428        context.drain_node_effects();
429        assert_eq!(
430            counts(),
431            [2, 1, 1, 1],
432            "even sub-epsilon edits must propagate"
433        );
434
435        source_property.set(source_with_x(&[("b", 2.0), ("a", 1.0 + 1e-8)]));
436        context.drain_node_effects();
437        assert_eq!(
438            ids(&repeat.children.get()),
439            [children[1].id.0, children[0].id.0]
440        );
441        assert_eq!(counts(), [2, 2, 1, 2], "reordering only changes indices");
442    }
443
444    #[test]
445    fn keyed_repeat_reuses_reordered_children_and_exits_removed_keys() {
446        let source_property = LocalProperty::new(source(&["a", "b", "c"]));
447        let leaf_transition_config = ComponentTransitionConfig {
448            has_exit: true,
449            exit_frame_count: 30,
450            timeout_ms: 5_000,
451            ..Default::default()
452        };
453        let leaf: Rc<dyn InstanceNode> =
454            ComponentInstance::instantiate(leaf_args(leaf_transition_config));
455        let repeat: Rc<dyn InstanceNode> =
456            RepeatInstance::instantiate(repeat_args(source_property.clone(), vec![leaf]));
457        let root_component =
458            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
459        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
460        let root = ExpandedNode::initialize_root(root_component, &context);
461
462        root.recurse_update(&context);
463        let repeat_node = root.children.get().remove(0);
464        let initial = repeat_node.children.get();
465        assert_eq!(initial.len(), 3);
466        let initial_ids = ids(&initial);
467
468        source_property.set(source(&["c", "a", "b"]));
469        root.recurse_update(&context);
470        let reordered_active = borrow!(repeat_node.active_children).clone();
471        assert_eq!(
472            ids(&reordered_active),
473            vec![initial_ids[2], initial_ids[0], initial_ids[1]]
474        );
475        assert!(borrow!(repeat_node.exiting_children).is_empty());
476
477        source_property.set(source(&["c", "d", "a"]));
478        root.recurse_update(&context);
479        let active = borrow!(repeat_node.active_children).clone();
480        let exiting = borrow!(repeat_node.exiting_children).clone();
481
482        assert_eq!(active.len(), 3);
483        assert_eq!(exiting.len(), 1);
484        assert_eq!(active[0].id.0, initial_ids[2]);
485        assert_ne!(active[1].id.0, initial_ids[0]);
486        assert_ne!(active[1].id.0, initial_ids[1]);
487        assert_ne!(active[1].id.0, initial_ids[2]);
488        assert_eq!(active[2].id.0, initial_ids[0]);
489        assert_eq!(exiting[0].id.0, initial_ids[1]);
490        assert_eq!(exiting[0].transition_phase.get(), TRANSITION_PHASE_EXIT);
491
492        source_property.set(source(&["c", "b", "a"]));
493        root.recurse_update(&context);
494        let active = borrow!(repeat_node.active_children).clone();
495        assert_eq!(active[1].id.0, initial_ids[1]);
496        assert_eq!(borrow!(repeat_node.mounted_children).len(), 4);
497        assert_eq!(active[1].transition_phase.get(), TRANSITION_PHASE_IDLE);
498    }
499
500    #[test]
501    fn retained_children_keep_parent_bindings_on_data_updates_and_reorder() {
502        let source_property = LocalProperty::new(source_with_x(&[("a", 1.0), ("b", 2.0)]));
503        let (root, context) = mounted_repeat(source_property.clone());
504        let repeat = root.children.get().remove(0);
505        let children = repeat.children.get();
506        let initial: Vec<_> = children
507            .iter()
508            .map(|c| c.parent_binding_rebuilds.get())
509            .collect();
510        for value in [
511            source_with_x(&[("a", 3.0), ("b", 2.0)]),
512            source_with_x(&[("b", 2.0), ("a", 3.0)]),
513        ] {
514            source_property.set(value);
515            context.drain_node_effects();
516            assert_eq!(
517                children
518                    .iter()
519                    .map(|c| c.parent_binding_rebuilds.get())
520                    .collect::<Vec<_>>(),
521                initial
522            );
523        }
524        source_property.set(source_with_x(&[("b", 2.0), ("a", 3.0), ("c", 4.0)]));
525        context.drain_node_effects();
526        assert_eq!(
527            children
528                .iter()
529                .map(|c| c.parent_binding_rebuilds.get())
530                .collect::<Vec<_>>(),
531            initial
532        );
533        assert_eq!(repeat.children.get().len(), 3);
534    }
535
536    #[test]
537    fn equal_valued_parent_sources_are_not_interchangeable() {
538        let (root, context) = mounted_repeat(LocalProperty::new(source(&["a"])));
539        let repeat = root.children.get().remove(0);
540        let children = repeat.children.get();
541        let child = &children[0];
542        let first = LocalProperty::new(None);
543        let second = LocalProperty::new(None);
544        repeat.attach_children(children.clone(), &context, &first);
545        let n = child.parent_binding_rebuilds.get();
546        repeat.attach_children(children.clone(), &context, &first);
547        assert_eq!(child.parent_binding_rebuilds.get(), n);
548        repeat.attach_children(children.clone(), &context, &second);
549        assert_eq!(child.parent_binding_rebuilds.get(), n + 1);
550        first.set(Some(root.id));
551        assert_eq!(
552            child.parent_frame.get(),
553            None,
554            "old frame source must be disconnected"
555        );
556        second.set(Some(repeat.id));
557        assert_eq!(child.parent_frame.get(), Some(repeat.id));
558
559        // Existing subscriptions must follow parent values without reattachment.
560        let cp = repeat.get_common_properties();
561        borrow!(cp)
562            .opacity
563            .set(Some(pax_runtime_api::Opacity::from(0.25)));
564        assert_eq!(child.computed_opacity.get(), 0.25);
565        assert_eq!(child.parent_binding_rebuilds.get(), n + 1);
566    }
567
568    #[test]
569    fn data_only_reconciliation_skips_structure_and_churn_releases_properties() {
570        let source_property = LocalProperty::new(source_with_x(&[("a", 1.0), ("b", 2.0)]));
571        let (root, context) = mounted_repeat(source_property.clone());
572        let repeat = root.children.get().remove(0);
573        let counts = || {
574            (
575                repeat.layout_hull_rebuilds.get(),
576                repeat.structural_children_updates.get(),
577            )
578        };
579        let initial = counts();
580        source_property.set(source_with_x(&[("a", 8.0), ("b", 2.0)]));
581        context.drain_node_effects();
582        assert_eq!(counts(), initial);
583        let settled = pax_runtime_api::properties::property_table_total_properties_count();
584        for _ in 0..40 {
585            source_property.set(source_with_x(&[("a", 8.0), ("b", 2.0), ("c", 3.0)]));
586            context.drain_node_effects();
587            source_property.set(source_with_x(&[("a", 8.0), ("b", 2.0)]));
588            context.drain_node_effects();
589            assert_eq!(
590                pax_runtime_api::properties::property_table_total_properties_count(),
591                settled
592            );
593        }
594    }
595
596    #[test]
597    fn nested_repeats_do_not_reconcile_unchanged_sibling_payloads() {
598        let data = |x| {
599            PaxValue::Vec(vec![
600                PaxValue::Object(vec![
601                    ("id".into(), PaxValue::String("a".into())),
602                    ("parts".into(), source_with_x(&[("part", x)])),
603                ]),
604                PaxValue::Object(vec![
605                    ("id".into(), PaxValue::String("b".into())),
606                    ("parts".into(), source_with_x(&[("part", 2.0)])),
607                ]),
608            ])
609        };
610        let source_property = LocalProperty::new(data(1.0));
611        let source_evaluations = Rc::new(std::cell::Cell::new(0));
612        let evaluations = source_evaluations.clone();
613        let leaf: Rc<dyn InstanceNode> =
614            ComponentInstance::instantiate(leaf_args(Default::default()));
615        let mut inner_args = repeat_args(LocalProperty::new(PaxValue::default()), vec![leaf]);
616        inner_args.prototypical_properties =
617            crate::PropertiesInit::Factory(Box::new(move |env, node| {
618                if node.is_some() {
619                    return None;
620                }
621                let outer_item = LocalProperty::<PaxValue>::new_from_untyped(
622                    env.resolve_symbol_as_erased_property("item").unwrap(),
623                );
624                let dep = outer_item.untyped();
625                let evaluations = evaluations.clone();
626                let source_expression = LocalProperty::computed(
627                    move || {
628                        evaluations.set(evaluations.get() + 1);
629                        let PaxValue::Object(fields) = outer_item.get() else {
630                            panic!("expected item object")
631                        };
632                        fields
633                            .into_iter()
634                            .find(|(name, _)| name == "parts")
635                            .unwrap()
636                            .1
637                    },
638                    &[dep],
639                );
640                Some(Rc::new(RefCell::new(
641                    RepeatProperties {
642                        source_expression,
643                        iterator_i_symbol: LocalProperty::new(Some("i".into())),
644                        iterator_elem_symbol: LocalProperty::new(Some("item".into())),
645                        repeat_key_expression: Some(ExpressionInfo::new(
646                            parse_pax_expression("item.id").unwrap(),
647                        )),
648                    }
649                    .to_pax_any(),
650                )))
651            }));
652        let inner: Rc<dyn InstanceNode> = RepeatInstance::instantiate(inner_args);
653        let outer: Rc<dyn InstanceNode> =
654            RepeatInstance::instantiate(repeat_args(source_property.clone(), vec![inner]));
655        let root = ComponentInstance::instantiate(component_args(Some(vec![outer])));
656        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
657        let root = ExpandedNode::initialize_root(root, &context);
658        root.recurse_update(&context);
659        context.drain_node_effects();
660        let outer = root.children.get().remove(0);
661        let inners = outer.children.get();
662        let first = inners[0].children.get()[0].clone();
663        let other = inners[1].children.get()[0].clone();
664        let (first_probe, first_runs) = binding_probe(&first, "item");
665        let (other_probe, other_runs) = binding_probe(&other, "item");
666        let evaluations = source_evaluations.get();
667        source_property.set(data(1.0));
668        context.drain_node_effects();
669        assert_eq!(source_evaluations.get(), evaluations);
670        source_property.set(data(1.0 + 1e-8));
671        context.drain_node_effects();
672        first_probe.get();
673        other_probe.get();
674        assert_eq!(source_evaluations.get(), evaluations + 1);
675        assert_eq!((first_runs.get(), other_runs.get()), (2, 1));
676        assert_eq!(inners[0].children.get()[0].id, first.id);
677        assert_eq!(inners[1].children.get()[0].id, other.id);
678    }
679
680    #[test]
681    fn parent_values_resize_reactively_and_replaced_sources_rebind() {
682        let (root, context) = mounted_repeat(LocalProperty::new(source(&["a"])));
683        let repeat = root.children.get().remove(0);
684        let children = repeat.children.get();
685        let child = &children[0];
686        let parent_cp = repeat.get_common_properties();
687        let child_cp = child.get_common_properties();
688        borrow!(parent_cp).width.set(Some(Size::Pixels(100.into())));
689        borrow!(child_cp).width.set(Some(Size::Percent(50.into())));
690        assert_eq!(child.transform_and_bounds.get().bounds.0, 50.0);
691        let rebuilds = child.parent_binding_rebuilds.get();
692        borrow!(parent_cp).width.set(Some(Size::Pixels(200.into())));
693        assert_eq!(child.transform_and_bounds.get().bounds.0, 100.0);
694        repeat.attach_children(children.clone(), &context, &repeat.parent_frame);
695        assert_eq!(child.parent_binding_rebuilds.get(), rebuilds);
696
697        let old_opacity = borrow!(child_cp).opacity.clone();
698        let new_opacity = LocalProperty::new(old_opacity.get());
699        borrow_mut!(child_cp).opacity = new_opacity.clone();
700        repeat.attach_children(children.clone(), &context, &repeat.parent_frame);
701        assert_eq!(child.parent_binding_rebuilds.get(), rebuilds + 1);
702        old_opacity.set(Some(0.25.into()));
703        assert_eq!(child.computed_opacity.get(), 1.0);
704        new_opacity.set(Some(0.75.into()));
705        assert_eq!(child.computed_opacity.get(), 0.75);
706
707        let template = borrow!(child.instance_node).clone();
708        child.recreate_with_new_data(template, &context);
709        context.drain_node_effects();
710        let rebuilt = child.parent_binding_rebuilds.get();
711        repeat.attach_children(children.clone(), &context, &repeat.parent_frame);
712        assert_eq!(
713            child.parent_binding_rebuilds.get(),
714            rebuilt + 1,
715            "reload must invalidate the binding fast path"
716        );
717    }
718
719    #[test]
720    fn repeat_exit_partition_and_final_removal_are_structural_changes() {
721        let source_property = LocalProperty::new(source(&["a"]));
722        let config = ComponentTransitionConfig {
723            has_exit: true,
724            exit_frame_count: 5,
725            timeout_ms: 5_000,
726            ..Default::default()
727        };
728        let leaf: Rc<dyn InstanceNode> = ComponentInstance::instantiate(leaf_args(config));
729        let repeat: Rc<dyn InstanceNode> =
730            RepeatInstance::instantiate(repeat_args(source_property.clone(), vec![leaf]));
731        let root = ComponentInstance::instantiate(component_args(Some(vec![repeat])));
732        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
733        let root = ExpandedNode::initialize_root(root, &context);
734        root.recurse_update(&context);
735        context.drain_node_effects();
736        let repeat = root.children.get().remove(0);
737        let id = repeat.children.get()[0].id;
738        let before_exit = repeat.structural_children_updates.get();
739        source_property.set(source(&[]));
740        context.drain_node_effects();
741        assert_eq!(repeat.children.get()[0].id, id);
742        assert!(
743            repeat.structural_children_updates.get() > before_exit,
744            "active-to-exiting affects projection even with identical rendered IDs"
745        );
746        let before_prune = repeat.structural_children_updates.get();
747        context.globals().elapsed_frames.set(10);
748        context.drain_node_effects();
749        assert!(repeat.children.get().is_empty());
750        assert!(repeat.structural_children_updates.get() > before_prune);
751
752        // Cleanup and a new selection in the same tick must both be observed.
753        source_property.set(source(&["a"]));
754        context.drain_node_effects();
755        source_property.set(source(&[]));
756        context.drain_node_effects();
757        context.globals().elapsed_frames.set(20);
758        source_property.set(source(&["b"]));
759        context.drain_node_effects();
760        assert_eq!(repeat.children.get().len(), 1);
761        assert!(borrow!(repeat.exiting_children).is_empty());
762    }
763
764    #[test]
765    fn external_key_dependencies_and_invalid_key_fallback_remain_reactive() {
766        let source_property = LocalProperty::new(source(&["a"]));
767        let leaf: Rc<dyn InstanceNode> =
768            ComponentInstance::instantiate(leaf_args(Default::default()));
769        let mut args = repeat_args(source_property.clone(), vec![leaf]);
770        let source_copy = source_property.clone();
771        args.prototypical_properties = crate::PropertiesInit::Factory(Box::new(move |_, node| {
772            node.is_none().then(|| {
773                Rc::new(RefCell::new(
774                    RepeatProperties {
775                        source_expression: source_copy.clone(),
776                        iterator_i_symbol: LocalProperty::new(Some("i".into())),
777                        iterator_elem_symbol: LocalProperty::new(Some("item".into())),
778                        repeat_key_expression: Some(ExpressionInfo::new(
779                            parse_pax_expression("$frames").unwrap(),
780                        )),
781                    }
782                    .to_pax_any(),
783                ))
784            })
785        }));
786        let repeat: Rc<dyn InstanceNode> = RepeatInstance::instantiate(args);
787        let root = ComponentInstance::instantiate(component_args(Some(vec![repeat])));
788        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
789        let root = ExpandedNode::initialize_root(root, &context);
790        root.recurse_update(&context);
791        context.drain_node_effects();
792        let repeat = root.children.get().remove(0);
793        let first_id = repeat.children.get()[0].id;
794        context.globals().elapsed_frames.set(1);
795        context.drain_node_effects();
796        assert_ne!(repeat.children.get()[0].id, first_id);
797
798        let invalid = PaxValue::Object(vec![("id".into(), PaxValue::Bool(true))]);
799        let data = PaxValue::Vec(vec![item("same"), item("same"), invalid]);
800        let source_property = LocalProperty::new(data.clone());
801        let (root, context) = mounted_repeat(source_property.clone());
802        let repeat = root.children.get().remove(0);
803        let first_ids = ids(&repeat.children.get());
804        assert_eq!(first_ids.iter().collect::<HashSet<_>>().len(), 3);
805        source_property.set(data);
806        context.drain_node_effects();
807        assert_eq!(ids(&repeat.children.get()), first_ids);
808    }
809
810    #[test]
811    fn keyed_groups_keep_all_template_roots_together_on_reorder() {
812        let source_property = LocalProperty::new(source(&["a", "b"]));
813        let leaf: Rc<dyn InstanceNode> =
814            ComponentInstance::instantiate(leaf_args(Default::default()));
815        let repeat: Rc<dyn InstanceNode> = RepeatInstance::instantiate(repeat_args(
816            source_property.clone(),
817            vec![leaf.clone(), leaf],
818        ));
819        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
820        let root = ExpandedNode::initialize_root(
821            ComponentInstance::instantiate(component_args(Some(vec![repeat]))),
822            &context,
823        );
824        root.recurse_update(&context);
825        context.drain_node_effects();
826        let repeat = root.children.get().remove(0);
827        let initial = ids(&repeat.children.get());
828        assert_eq!(initial.len(), 4);
829        source_property.set(source(&["b", "a"]));
830        context.drain_node_effects();
831        assert_eq!(
832            ids(&repeat.children.get()),
833            [initial[2], initial[3], initial[0], initial[1]]
834        );
835    }
836
837    #[test]
838    fn detached_mask_style_repeat_binds_inserted_children_and_releases_last_child() {
839        let source_property = LocalProperty::new(source(&[]));
840        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
841        let root = ExpandedNode::initialize_root(
842            ComponentInstance::instantiate(component_args(None)),
843            &context,
844        );
845        let mut args = leaf_args(Default::default());
846        args.prototypical_common_properties =
847            crate::CommonPropertiesInit::Factory(Box::new(|_, node| {
848                node.is_none().then(|| {
849                    let mut cp = CommonProperties::default();
850                    cp.width = LocalProperty::new(Some(Size::Percent(50.into())));
851                    cp.height = LocalProperty::new(Some(Size::Percent(50.into())));
852                    Rc::new(RefCell::new(cp))
853                })
854            }));
855        let leaf: Rc<dyn InstanceNode> = ComponentInstance::instantiate(args);
856        let repeat: Rc<dyn InstanceNode> =
857            RepeatInstance::instantiate(repeat_args(source_property.clone(), vec![leaf]));
858        let sidecar = root.create_children_detached(
859            iter::once((repeat, root.stack.clone())),
860            &context,
861            &Rc::downgrade(&root),
862        );
863        let sidecar = root.attach_sidecar_children(sidecar, &context, &root.parent_frame);
864        let repeat = &sidecar[0];
865        repeat.recurse_control_flow_expansion(&context);
866        context.drain_node_effects();
867        assert!(repeat.children.get().is_empty());
868        source_property.set(source(&["ring"]));
869        let children = repeat.children.get();
870        repeat.attach_children(children.clone(), &context, &repeat.parent_frame);
871        context.drain_node_effects();
872        assert_eq!(
873            children[0].attached.get(),
874            0,
875            "mask geometry must not acquire visible mount lifecycle"
876        );
877        assert_eq!(children[0].transform_and_bounds.get().bounds, (50.0, 50.0));
878        context.globals().viewport.set(TransformAndBounds {
879            transform: Transform2::identity(),
880            bounds: (300.0, 200.0),
881        });
882        context.drain_node_effects();
883        assert_eq!(
884            children[0].transform_and_bounds.get().bounds,
885            (150.0, 100.0)
886        );
887        let rebuilds = children[0].parent_binding_rebuilds.get();
888        source_property.set(source(&["ring"]));
889        repeat.attach_children(repeat.children.get(), &context, &repeat.parent_frame);
890        assert_eq!(children[0].parent_binding_rebuilds.get(), rebuilds);
891        source_property.set(source(&[]));
892        repeat.attach_children(repeat.children.get(), &context, &repeat.parent_frame);
893        context.drain_node_effects();
894        assert!(repeat.children.get().is_empty());
895        assert!(borrow!(repeat.mounted_children).is_empty());
896    }
897
898    #[test]
899    fn unkeyed_repeat_rescues_a_position_readded_during_exit() {
900        let source_property = LocalProperty::new(source(&["a", "b"]));
901        let leaf_transition_config = ComponentTransitionConfig {
902            has_enter: true,
903            enter_frame_count: 10,
904            has_exit: true,
905            exit_frame_count: 30,
906            timeout_ms: 5_000,
907            ..Default::default()
908        };
909        let leaf: Rc<dyn InstanceNode> =
910            ComponentInstance::instantiate(leaf_args(leaf_transition_config));
911        let repeat: Rc<dyn InstanceNode> =
912            RepeatInstance::instantiate(unkeyed_repeat_args(source_property.clone(), vec![leaf]));
913        let root_component =
914            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
915        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
916        let root = ExpandedNode::initialize_root(root_component, &context);
917
918        root.recurse_update(&context);
919        let repeat_node = root.children.get().remove(0);
920        let second_id = borrow!(repeat_node.active_children)[1].id;
921
922        source_property.set(source(&["a"]));
923        root.recurse_update(&context);
924        assert_eq!(borrow!(repeat_node.exiting_children)[0].id, second_id);
925
926        source_property.set(source(&["a", "b"]));
927        root.recurse_update(&context);
928        assert_eq!(borrow!(repeat_node.active_children)[1].id, second_id);
929        assert!(borrow!(repeat_node.exiting_children).is_empty());
930        assert_eq!(
931            borrow!(repeat_node.active_children)[1]
932                .transition_phase
933                .get(),
934            TRANSITION_PHASE_ENTER
935        );
936    }
937
938    #[test]
939    fn keyed_repeat_assigns_distinct_scope_bindings_per_child() {
940        let source_property = LocalProperty::new(source(&["a", "b", "c"]));
941        let leaf: Rc<dyn InstanceNode> =
942            ComponentInstance::instantiate(leaf_args(Default::default()));
943        let repeat: Rc<dyn InstanceNode> =
944            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
945        let root_component =
946            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
947        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
948        let root = ExpandedNode::initialize_root(root_component, &context);
949
950        root.recurse_update(&context);
951        let repeat_node = root.children.get().remove(0);
952        let children = repeat_node.children.get();
953        assert_eq!(children.len(), 3);
954
955        let indices: Vec<_> = children
956            .iter()
957            .map(|child| {
958                child
959                    .stack
960                    .resolve_symbol("i")
961                    .and_then(|variable| Numeric::try_coerce(variable.get_as_pax_value()).ok())
962                    .map(|value| value.to_int())
963            })
964            .collect();
965        assert_eq!(indices, vec![Some(0), Some(1), Some(2)]);
966    }
967
968    #[test]
969    fn keyed_repeat_assigns_distinct_element_bindings_per_child() {
970        let source_property =
971            LocalProperty::new(source_with_x(&[("a", 0.0), ("b", 10.0), ("c", 20.0)]));
972        let leaf: Rc<dyn InstanceNode> =
973            ComponentInstance::instantiate(leaf_args(Default::default()));
974        let repeat: Rc<dyn InstanceNode> =
975            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
976        let root_component =
977            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
978        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
979        let root = ExpandedNode::initialize_root(root_component, &context);
980
981        root.recurse_update(&context);
982        let repeat_node = root.children.get().remove(0);
983        let children = repeat_node.children.get();
984        assert_eq!(children.len(), 3);
985
986        let xs: Vec<_> = children
987            .iter()
988            .map(|child| {
989                child
990                    .stack
991                    .resolve_symbol("item")
992                    .map(|variable| variable.get_as_pax_value())
993                    .and_then(|value| match value {
994                        PaxValue::Object(fields) => fields
995                            .into_iter()
996                            .find_map(|(name, value)| (name == "x").then_some(value)),
997                        _ => None,
998                    })
999                    .and_then(|value| Numeric::try_coerce(value).ok())
1000                    .map(|value| value.to_float())
1001            })
1002            .collect();
1003
1004        assert_eq!(xs, vec![Some(0.0), Some(10.0), Some(20.0)]);
1005    }
1006
1007    #[test]
1008    fn keyed_repeat_resolves_distinct_common_properties_per_child() {
1009        let source_property = LocalProperty::new(source(&["a", "b", "c"]));
1010        let leaf: Rc<dyn InstanceNode> = ComponentInstance::instantiate(positioned_leaf_args());
1011        let repeat: Rc<dyn InstanceNode> =
1012            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
1013        let root_component =
1014            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
1015        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
1016        let root = ExpandedNode::initialize_root(root_component, &context);
1017
1018        root.recurse_update(&context);
1019        let repeat_node = root.children.get().remove(0);
1020        let children = repeat_node.children.get();
1021        assert_eq!(children.len(), 3);
1022
1023        let xs: Vec<_> = children
1024            .iter()
1025            .map(|child| {
1026                borrow!(child.get_common_properties())
1027                    .x
1028                    .get()
1029                    .map(|size| size.expect_pixels().to_float())
1030            })
1031            .collect();
1032
1033        assert_eq!(xs, vec![Some(0.0), Some(10.0), Some(20.0)]);
1034    }
1035
1036    #[test]
1037    fn keyed_repeat_resolves_distinct_expression_common_properties_from_element_binding() {
1038        let source_property =
1039            LocalProperty::new(source_with_x(&[("a", 0.0), ("b", 10.0), ("c", 20.0)]));
1040        let leaf: Rc<dyn InstanceNode> =
1041            ComponentInstance::instantiate(expression_positioned_leaf_args("(item.x)px"));
1042        let repeat: Rc<dyn InstanceNode> =
1043            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
1044        let root_component =
1045            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
1046        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
1047        let root = ExpandedNode::initialize_root(root_component, &context);
1048
1049        root.recurse_update(&context);
1050        let repeat_node = root.children.get().remove(0);
1051        let children = repeat_node.children.get();
1052        assert_eq!(children.len(), 3);
1053
1054        let xs: Vec<_> = children
1055            .iter()
1056            .map(|child| {
1057                borrow!(child.get_common_properties())
1058                    .x
1059                    .get()
1060                    .map(|size| size.expect_pixels().to_float())
1061            })
1062            .collect();
1063
1064        assert_eq!(xs, vec![Some(0.0), Some(10.0), Some(20.0)]);
1065    }
1066
1067    #[test]
1068    fn keyed_repeat_resolves_distinct_common_properties_for_transition_components() {
1069        let source_property = LocalProperty::new(source(&["a", "b", "c"]));
1070        let leaf: Rc<dyn InstanceNode> =
1071            ComponentInstance::instantiate(transitioned_positioned_leaf_args());
1072        let repeat: Rc<dyn InstanceNode> =
1073            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
1074        let root_component =
1075            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
1076        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
1077        let root = ExpandedNode::initialize_root(root_component, &context);
1078
1079        root.recurse_update(&context);
1080        let repeat_node = root.children.get().remove(0);
1081        let children = repeat_node.children.get();
1082        assert_eq!(children.len(), 3);
1083
1084        let xs: Vec<_> = children
1085            .iter()
1086            .map(|child| {
1087                borrow!(child.get_common_properties())
1088                    .x
1089                    .get()
1090                    .map(|size| size.expect_pixels().to_float())
1091            })
1092            .collect();
1093
1094        assert_eq!(xs, vec![Some(0.0), Some(10.0), Some(20.0)]);
1095    }
1096
1097    #[test]
1098    fn keyed_repeat_resolves_expression_common_properties_for_transition_components() {
1099        let source_property =
1100            LocalProperty::new(source_with_x(&[("a", 0.0), ("b", 10.0), ("c", 20.0)]));
1101        let leaf: Rc<dyn InstanceNode> = ComponentInstance::instantiate(
1102            transitioned_expression_positioned_leaf_args("(item.x)px"),
1103        );
1104        let repeat: Rc<dyn InstanceNode> =
1105            RepeatInstance::instantiate(repeat_args(source_property, vec![leaf]));
1106        let root_component =
1107            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
1108        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
1109        let root = ExpandedNode::initialize_root(root_component, &context);
1110
1111        root.recurse_update(&context);
1112        let repeat_node = root.children.get().remove(0);
1113        let children = repeat_node.children.get();
1114        assert_eq!(children.len(), 3);
1115
1116        let xs: Vec<_> = children
1117            .iter()
1118            .map(|child| {
1119                borrow!(child.get_common_properties())
1120                    .x
1121                    .get()
1122                    .map(|size| size.expect_pixels().to_float())
1123            })
1124            .collect();
1125
1126        assert_eq!(xs, vec![Some(0.0), Some(10.0), Some(20.0)]);
1127    }
1128
1129    #[test]
1130    fn unkeyed_repeat_stale_element_binding_defaults_after_source_shrinks() {
1131        let source_property = LocalProperty::new(source_with_x(&[("a", 0.0), ("b", 10.0)]));
1132        let leaf: Rc<dyn InstanceNode> =
1133            ComponentInstance::instantiate(leaf_args(Default::default()));
1134        let repeat: Rc<dyn InstanceNode> =
1135            RepeatInstance::instantiate(unkeyed_repeat_args(source_property.clone(), vec![leaf]));
1136        let root_component =
1137            ComponentInstance::instantiate(component_args(Some(vec![Rc::clone(&repeat)])));
1138        let context = Rc::new(crate::test_support::runtime_context(test_globals()));
1139        let root = ExpandedNode::initialize_root(root_component, &context);
1140
1141        root.recurse_update(&context);
1142        let repeat_node = root.children.get().remove(0);
1143        let children = repeat_node.children.get();
1144        assert_eq!(children.len(), 2);
1145
1146        let stale_item = children[1].stack.resolve_symbol("item").unwrap();
1147        source_property.set(source_with_x(&[("a", 0.0)]));
1148
1149        assert_eq!(stale_item.get_as_pax_value(), PaxValue::default());
1150    }
1151}
1152
1153#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1154enum RepeatKey {
1155    String(String),
1156    Int(i64),
1157    Synthetic(usize),
1158}
1159
1160#[derive(Clone)]
1161struct RepeatChildGroup {
1162    key: RepeatKey,
1163    elem: LocalProperty<PaxValue>,
1164    i: LocalProperty<usize>,
1165    children: Vec<Rc<ExpandedNode>>,
1166}
1167
1168/// Per-iteration bindings exposed inside a `for` template body.
1169pub struct RepeatItem {
1170    pub elem: LocalProperty<PaxValue>,
1171    pub i: LocalProperty<usize>,
1172}
1173
1174impl ToPaxValue for RepeatItem {
1175    fn to_pax_value(self) -> PaxValue {
1176        PaxValue::Object(
1177            vec![
1178                ("elem".to_string(), self.elem.get().to_pax_value()),
1179                ("i".to_string(), self.i.get().to_pax_value()),
1180            ]
1181            .into_iter()
1182            .collect(),
1183        )
1184    }
1185}
1186
1187impl InstanceNode for RepeatInstance {
1188    fn instantiate(args: InstantiationArgs) -> Rc<Self>
1189    where
1190        Self: Sized,
1191    {
1192        Rc::new(Self {
1193            base: BaseInstance::new(
1194                args,
1195                InstanceFlags {
1196                    invisible_to_slot: true,
1197                    invisible_to_raycasting: true,
1198                    layer: Layer::DontCare,
1199                    is_component: false,
1200                    is_slot: false,
1201                },
1202            ),
1203        })
1204    }
1205
1206    fn resolve_debug(
1207        &self,
1208        f: &mut std::fmt::Formatter,
1209        _expanded_node: Option<&ExpandedNode>,
1210    ) -> std::fmt::Result {
1211        f.debug_struct("Repeat").finish()
1212    }
1213
1214    fn base(&self) -> &BaseInstance {
1215        &self.base
1216    }
1217
1218    fn handle_mount(
1219        self: Rc<Self>,
1220        expanded_node: &Rc<ExpandedNode>,
1221        context: &Rc<RuntimeContext>,
1222    ) {
1223        self.handle_setup(expanded_node, context, true);
1224    }
1225
1226    fn handle_control_flow_node_expansion(
1227        self: Rc<Self>,
1228        expanded_node: &Rc<ExpandedNode>,
1229        context: &Rc<RuntimeContext>,
1230    ) {
1231        self.handle_setup(expanded_node, context, false);
1232    }
1233}
1234
1235impl RepeatInstance {
1236    fn handle_setup(
1237        self: Rc<Self>,
1238        expanded_node: &Rc<ExpandedNode>,
1239        context: &Rc<RuntimeContext>,
1240        is_mount: bool,
1241    ) {
1242        // No-op: wait with creating child-nodes until update tick, since the
1243        // condition has then been evaluated
1244        let weak_ref_self = Rc::downgrade(expanded_node);
1245        let cloned_self = Rc::clone(&self);
1246        let cloned_context = Rc::clone(context);
1247        let source_expression =
1248            expanded_node.with_properties_unwrapped(|properties: &mut RepeatProperties| {
1249                properties.source_expression.clone()
1250            });
1251
1252        let i_symbol =
1253            expanded_node.with_properties_unwrapped(|properties: &mut RepeatProperties| {
1254                properties.iterator_i_symbol.clone()
1255            });
1256        let elem_symbol =
1257            expanded_node.with_properties_unwrapped(|properties: &mut RepeatProperties| {
1258                properties.iterator_elem_symbol.clone()
1259            });
1260        let repeat_key_expression =
1261            expanded_node.with_properties_unwrapped(|properties: &mut RepeatProperties| {
1262                properties.repeat_key_expression.clone()
1263            });
1264
1265        let mut deps = vec![
1266            source_expression.untyped(),
1267            i_symbol.untyped(),
1268            elem_symbol.untyped(),
1269        ];
1270        if let Some(repeat_key_expression) = &repeat_key_expression {
1271            for dependency in &repeat_key_expression.dependencies {
1272                if let Some(property) = expanded_node
1273                    .stack
1274                    .resolve_symbol_as_erased_property(dependency)
1275                {
1276                    deps.push(property);
1277                }
1278            }
1279        }
1280
1281        let last_length = Rc::new(RefCell::new(0));
1282        let last_elem_sym = Rc::new(RefCell::new(None));
1283        let last_i_sym = Rc::new(RefCell::new(None));
1284        let cached_children: Rc<RefCell<Vec<Rc<ExpandedNode>>>> = Default::default();
1285        let keyed_groups: Rc<RefCell<Vec<RepeatChildGroup>>> = Default::default();
1286
1287        let children = LocalProperty::computed_with_name(
1288            move || {
1289                let Some(cloned_expanded_node) = weak_ref_self.upgrade() else {
1290                    panic!("ran evaluator after expanded node dropped (repeat elem)")
1291                };
1292                let source_len = source_expression.read(Self::source_len);
1293                if let Some(repeat_key_expression) = &repeat_key_expression {
1294                    let i_symbol_value = i_symbol.get();
1295                    let elem_symbol_value = elem_symbol.get();
1296                    let symbols_changed = i_symbol_value != *borrow!(last_i_sym)
1297                        || elem_symbol_value != *borrow!(last_elem_sym);
1298                    *borrow_mut!(last_i_sym) = i_symbol_value.clone();
1299                    *borrow_mut!(last_elem_sym) = elem_symbol_value.clone();
1300                    if symbols_changed {
1301                        borrow_mut!(keyed_groups).clear();
1302                    }
1303                    return Self::compute_keyed_children(
1304                        &cloned_expanded_node,
1305                        &cloned_context,
1306                        Rc::clone(&cloned_self),
1307                        &source_expression,
1308                        i_symbol_value,
1309                        elem_symbol_value,
1310                        repeat_key_expression,
1311                        Rc::clone(&keyed_groups),
1312                        source_len,
1313                        is_mount,
1314                        !symbols_changed,
1315                    );
1316                }
1317
1318                if source_len == *borrow!(last_length)
1319                    && i_symbol.read(|i| i == &*borrow!(last_i_sym))
1320                    && elem_symbol.read(|e| e == &*borrow!(last_elem_sym))
1321                {
1322                    return if cloned_expanded_node.attached.get() > 0 {
1323                        cloned_expanded_node.current_attached_children()
1324                    } else {
1325                        borrow!(cached_children).clone()
1326                    };
1327                }
1328                let i_symbol_value = i_symbol.get();
1329                let elem_symbol_value = elem_symbol.get();
1330                let symbols_unchanged = i_symbol_value == *borrow!(last_i_sym)
1331                    && elem_symbol_value == *borrow!(last_elem_sym);
1332                *borrow_mut!(last_length) = source_len;
1333                *borrow_mut!(last_i_sym) = i_symbol_value.clone();
1334                *borrow_mut!(last_elem_sym) = elem_symbol_value.clone();
1335
1336                let template_children = borrow!(cloned_self.base().get_instance_children()).clone();
1337                let active_children = borrow!(cloned_expanded_node.active_children).clone();
1338                let mut selected_children = Vec::new();
1339                for i in 0..source_len {
1340                    let property_i = LocalProperty::new(i);
1341                    let cp_source_expression = source_expression.clone();
1342                    let property_elem = LocalProperty::computed_with_name(
1343                        move || cp_source_expression.read(|source| Self::source_elem(source, i)),
1344                        &[source_expression.untyped()],
1345                        "repeat elem",
1346                    );
1347                    let scope = Self::repeat_scope(
1348                        i_symbol_value.clone(),
1349                        elem_symbol_value.clone(),
1350                        property_i,
1351                        property_elem,
1352                    );
1353                    let new_env = cloned_expanded_node.stack.push(scope);
1354                    for (template_offset, template) in template_children.iter().enumerate() {
1355                        let flat_index = i * template_children.len() + template_offset;
1356                        let active = symbols_unchanged
1357                            .then(|| active_children.get(flat_index))
1358                            .flatten()
1359                            .filter(|child| {
1360                                let child_template = borrow!(child.instance_node).clone();
1361                                Rc::ptr_eq(&child_template, template)
1362                            })
1363                            .cloned();
1364                        let rescued = active.or_else(|| {
1365                            (symbols_unchanged
1366                                && is_mount
1367                                && cloned_expanded_node.attached.get() > 0)
1368                                .then(|| {
1369                                    cloned_expanded_node.rescue_exiting_child_matching(|child| {
1370                                        let child_template = borrow!(child.instance_node).clone();
1371                                        let template_matches =
1372                                            Rc::ptr_eq(&child_template, template);
1373                                        let index_matches = child
1374                                            .stack
1375                                            .resolve_symbol_as_erased_property(
1376                                                INTERNAL_REPEAT_INDEX_SYMBOL,
1377                                            )
1378                                            .map(LocalProperty::<usize>::new_from_untyped)
1379                                            .map(|index| index.get() == i)
1380                                            .unwrap_or(true);
1381                                        template_matches && index_matches
1382                                    })
1383                                })
1384                                .flatten()
1385                        });
1386                        let child = rescued.unwrap_or_else(|| {
1387                            cloned_expanded_node
1388                                .create_children_detached(
1389                                    iter::once((Rc::clone(template), Rc::clone(&new_env))),
1390                                    &cloned_context,
1391                                    &Rc::downgrade(&cloned_expanded_node),
1392                                )
1393                                .pop()
1394                                .expect("repeat child creation returned no child")
1395                        });
1396                        if !is_mount && child.attached.get() == 0 {
1397                            child.recurse_control_flow_expansion(&cloned_context);
1398                        }
1399                        selected_children.push(child);
1400                    }
1401                }
1402                let ret = if is_mount {
1403                    cloned_expanded_node.attach_children(
1404                        selected_children,
1405                        &cloned_context,
1406                        &cloned_expanded_node.parent_frame,
1407                    )
1408                } else {
1409                    selected_children
1410                };
1411                *borrow_mut!(cached_children) = ret.clone();
1412                ret
1413            },
1414            &deps,
1415            &format!("repeat_children (node id: {})", expanded_node.id.0),
1416        );
1417        expanded_node.children.replace_with(children);
1418    }
1419
1420    fn source_len(source: &PaxValue) -> usize {
1421        if let PaxValue::Range(start, end) = source {
1422            (isize::try_coerce(*end.clone()).unwrap() - isize::try_coerce(*start.clone()).unwrap())
1423                as usize
1424        } else if let PaxValue::Vec(v) = source {
1425            v.len()
1426        } else {
1427            log::warn!("source is not a vec");
1428            0
1429        }
1430    }
1431
1432    fn source_elem(source: &PaxValue, i: usize) -> PaxValue {
1433        if let PaxValue::Range(start, _) = source {
1434            let start = isize::try_coerce(*start.clone()).unwrap();
1435            (start + i as isize).to_pax_value()
1436        } else if let PaxValue::Vec(v) = source {
1437            v.get(i).cloned().unwrap_or_default()
1438        } else {
1439            log::warn!("source is not a vec");
1440            Default::default()
1441        }
1442    }
1443
1444    fn repeat_scope(
1445        i_symbol: Option<String>,
1446        elem_symbol: Option<String>,
1447        property_i: LocalProperty<usize>,
1448        property_elem: LocalProperty<PaxValue>,
1449    ) -> HashMap<String, Variable> {
1450        let mut scope = HashMap::new();
1451        if let Some(i_symbol) = i_symbol {
1452            scope.insert(
1453                i_symbol,
1454                Variable::new_from_typed_property(property_i.clone()),
1455            );
1456        }
1457        if let Some(elem_symbol) = elem_symbol {
1458            scope.insert(
1459                elem_symbol,
1460                Variable::new_from_typed_property(property_elem.clone()),
1461            );
1462        }
1463        scope.insert(
1464            INTERNAL_REPEAT_INDEX_SYMBOL.to_string(),
1465            Variable::new_from_typed_property(property_i),
1466        );
1467        scope.insert(
1468            INTERNAL_REPEAT_ELEMENT_SYMBOL.to_string(),
1469            Variable::new_from_typed_property(property_elem),
1470        );
1471        scope
1472    }
1473
1474    fn encoded_repeat_key(key: &RepeatKey) -> String {
1475        match key {
1476            RepeatKey::String(value) => format!("string:{value}"),
1477            RepeatKey::Int(value) => format!("int:{value}"),
1478            RepeatKey::Synthetic(value) => format!("synthetic:{value}"),
1479        }
1480    }
1481
1482    fn repeat_scope_with_key(
1483        i_symbol: Option<String>,
1484        elem_symbol: Option<String>,
1485        property_i: LocalProperty<usize>,
1486        property_elem: LocalProperty<PaxValue>,
1487        key: &RepeatKey,
1488    ) -> HashMap<String, Variable> {
1489        let mut scope = Self::repeat_scope(i_symbol, elem_symbol, property_i, property_elem);
1490        scope.insert(
1491            INTERNAL_REPEAT_KEY_SYMBOL.to_string(),
1492            Variable::new_from_typed_property(LocalProperty::new(Self::encoded_repeat_key(key))),
1493        );
1494        scope
1495    }
1496
1497    fn repeat_key_from_value(value: PaxValue, index: usize) -> Option<RepeatKey> {
1498        match value {
1499            PaxValue::String(value) => Some(RepeatKey::String(value)),
1500            PaxValue::Numeric(value) if !value.is_float() => Some(RepeatKey::Int(value.to_int())),
1501            _ => {
1502                log::warn!(
1503                    "repeat key for index {} must evaluate to a string or integer; using positional fallback",
1504                    index
1505                );
1506                None
1507            }
1508        }
1509    }
1510
1511    fn evaluate_key(
1512        key_expression: &ExpressionInfo,
1513        env: Rc<RuntimePropertiesStackFrame>,
1514        index: usize,
1515    ) -> Option<RepeatKey> {
1516        key_expression
1517            .expression
1518            .compute(env)
1519            .map(|value| Self::repeat_key_from_value(value, index))
1520            .unwrap_or_else(|err| {
1521                log::warn!(
1522                    "failed to compute repeat key for index {}: {}; using positional fallback",
1523                    index,
1524                    err
1525                );
1526                None
1527            })
1528    }
1529
1530    fn compute_keyed_children(
1531        expanded_node: &Rc<ExpandedNode>,
1532        context: &Rc<RuntimeContext>,
1533        repeat: Rc<RepeatInstance>,
1534        source_expression: &LocalProperty<PaxValue>,
1535        i_symbol_value: Option<String>,
1536        elem_symbol_value: Option<String>,
1537        key_expression: &ExpressionInfo,
1538        keyed_groups: Rc<RefCell<Vec<RepeatChildGroup>>>,
1539        source_len: usize,
1540        is_mount: bool,
1541        allow_rescue: bool,
1542    ) -> Vec<Rc<ExpandedNode>> {
1543        let mut old_groups_by_key: HashMap<RepeatKey, RepeatChildGroup> = borrow_mut!(keyed_groups)
1544            .drain(..)
1545            .map(|group| (group.key.clone(), group))
1546            .collect();
1547        let mut new_groups = Vec::new();
1548        let mut new_children = Vec::new();
1549        let mut used_keys = HashSet::new();
1550        let template_children = repeat.base().get_instance_children();
1551
1552        for i in 0..source_len {
1553            let elem = source_expression.read(|source| Self::source_elem(source, i));
1554            let key_probe_i = LocalProperty::new(i);
1555            let key_probe_elem = LocalProperty::new(elem.clone());
1556            let key_probe_scope = Self::repeat_scope(
1557                i_symbol_value.clone(),
1558                elem_symbol_value.clone(),
1559                key_probe_i,
1560                key_probe_elem,
1561            );
1562            let key_probe_env = expanded_node.stack.push(key_probe_scope);
1563            let evaluated_key = Self::evaluate_key(key_expression, key_probe_env, i);
1564            let key = match evaluated_key {
1565                Some(key) if used_keys.insert(key.clone()) => key,
1566                Some(key) => {
1567                    log::warn!(
1568                        "duplicate repeat key {:?} at index {}; using positional fallback",
1569                        key,
1570                        i
1571                    );
1572                    RepeatKey::Synthetic(i)
1573                }
1574                None => RepeatKey::Synthetic(i),
1575            };
1576
1577            let group = if let Some(group) = old_groups_by_key.remove(&key) {
1578                update_repeat_bindings(&group.i, &group.elem, i, elem);
1579                group
1580            } else if allow_rescue && is_mount {
1581                Self::rescue_keyed_group(
1582                    expanded_node,
1583                    context,
1584                    &template_children,
1585                    i_symbol_value.clone(),
1586                    elem_symbol_value.clone(),
1587                    &key,
1588                    i,
1589                    elem.clone(),
1590                )
1591                .unwrap_or_else(|| {
1592                    Self::create_keyed_group(
1593                        expanded_node,
1594                        context,
1595                        &template_children,
1596                        i_symbol_value.clone(),
1597                        elem_symbol_value.clone(),
1598                        key.clone(),
1599                        i,
1600                        elem,
1601                        is_mount,
1602                    )
1603                })
1604            } else {
1605                Self::create_keyed_group(
1606                    expanded_node,
1607                    context,
1608                    &template_children,
1609                    i_symbol_value.clone(),
1610                    elem_symbol_value.clone(),
1611                    key.clone(),
1612                    i,
1613                    elem,
1614                    is_mount,
1615                )
1616            };
1617
1618            new_children.extend(group.children.iter().cloned());
1619            new_groups.push(group);
1620        }
1621
1622        *borrow_mut!(keyed_groups) = new_groups;
1623        if is_mount {
1624            expanded_node.attach_children(new_children, context, &expanded_node.parent_frame)
1625        } else {
1626            new_children
1627        }
1628    }
1629
1630    fn create_keyed_group(
1631        expanded_node: &Rc<ExpandedNode>,
1632        context: &Rc<RuntimeContext>,
1633        template_children: &RefCell<Vec<Rc<dyn InstanceNode>>>,
1634        i_symbol: Option<String>,
1635        elem_symbol: Option<String>,
1636        key: RepeatKey,
1637        i: usize,
1638        elem: PaxValue,
1639        is_mount: bool,
1640    ) -> RepeatChildGroup {
1641        let property_i = LocalProperty::new(i);
1642        let property_elem = LocalProperty::new(elem);
1643        let scope = Self::repeat_scope_with_key(
1644            i_symbol,
1645            elem_symbol,
1646            property_i.clone(),
1647            property_elem.clone(),
1648            &key,
1649        );
1650        let new_env = expanded_node.stack.push(scope);
1651        let children = expanded_node.create_children_detached(
1652            borrow!(template_children)
1653                .clone()
1654                .into_iter()
1655                .zip(iter::repeat(new_env)),
1656            context,
1657            &Rc::downgrade(expanded_node),
1658        );
1659        if !is_mount {
1660            for child in &children {
1661                child.recurse_control_flow_expansion(context);
1662            }
1663        }
1664        RepeatChildGroup {
1665            key,
1666            elem: property_elem,
1667            i: property_i,
1668            children,
1669        }
1670    }
1671
1672    fn rescue_keyed_group(
1673        expanded_node: &Rc<ExpandedNode>,
1674        context: &Rc<RuntimeContext>,
1675        template_children: &RefCell<Vec<Rc<dyn InstanceNode>>>,
1676        i_symbol: Option<String>,
1677        elem_symbol: Option<String>,
1678        key: &RepeatKey,
1679        i: usize,
1680        elem: PaxValue,
1681    ) -> Option<RepeatChildGroup> {
1682        let encoded_key = Self::encoded_repeat_key(key);
1683        let templates = borrow!(template_children).clone();
1684        let mut children = templates
1685            .iter()
1686            .map(|template| {
1687                expanded_node.rescue_exiting_child_matching(|child| {
1688                    let child_template = borrow!(child.instance_node).clone();
1689                    Rc::ptr_eq(&child_template, template)
1690                        && child
1691                            .stack
1692                            .resolve_symbol_as_erased_property(INTERNAL_REPEAT_KEY_SYMBOL)
1693                            .map(LocalProperty::<String>::new_from_untyped)
1694                            .map(|key| key.get() == encoded_key)
1695                            .unwrap_or(false)
1696                })
1697            })
1698            .collect::<Vec<_>>();
1699        let seed = children.iter().flatten().next()?.clone();
1700        let property_i = seed
1701            .stack
1702            .resolve_symbol_as_erased_property(INTERNAL_REPEAT_INDEX_SYMBOL)
1703            .map(LocalProperty::<usize>::new_from_untyped)?;
1704        let property_elem = seed
1705            .stack
1706            .resolve_symbol_as_erased_property(INTERNAL_REPEAT_ELEMENT_SYMBOL)
1707            .map(LocalProperty::<PaxValue>::new_from_untyped)?;
1708        update_repeat_bindings(&property_i, &property_elem, i, elem);
1709        let env = expanded_node.stack.push(Self::repeat_scope_with_key(
1710            i_symbol,
1711            elem_symbol,
1712            property_i.clone(),
1713            property_elem.clone(),
1714            key,
1715        ));
1716        let resolved_children = children
1717            .drain(..)
1718            .zip(templates)
1719            .map(|(child, template)| {
1720                child.unwrap_or_else(|| {
1721                    expanded_node
1722                        .create_children_detached(
1723                            iter::once((template, Rc::clone(&env))),
1724                            context,
1725                            &Rc::downgrade(expanded_node),
1726                        )
1727                        .pop()
1728                        .expect("repeat child creation returned no child")
1729                })
1730            })
1731            .collect();
1732        Some(RepeatChildGroup {
1733            key: key.clone(),
1734            elem: property_elem,
1735            i: property_i,
1736            children: resolved_children,
1737        })
1738    }
1739}