Skip to main content

pax_runtime/
slot.rs

1use std::collections::HashSet;
2use std::rc::{Rc, Weak};
3use_RefCell!();
4
5use pax_runtime_api::{borrow, use_RefCell, ImplToFromPaxAny, Numeric, Property};
6
7use crate::api::Layer;
8use crate::{
9    BaseInstance, ExpandedNode, InstanceFlags, InstanceNode, InstantiationArgs, RuntimeContext,
10};
11
12/// A special "control-flow" primitive (a la `yield` or perhaps `goto`) that
13/// renders projected payload into a node's encapsulated implementation.
14///
15/// `Slot` relies on raw `projected_children` being present on the runtime stack
16/// and will not render any content if there are none. Projection is the engine
17/// transport mechanism; semantic container logic should usually reason in terms
18/// of `received_children` instead.
19///
20/// Consider a Stacker:  the owner of a Stacker passes the Stacker some nodes to render
21/// inside the cells of the Stacker.  To the owner of the Stacker, those nodes might seem like
22/// received children. Inside Stacker's encapsulated implementation, those
23/// received children travel as projected children until `Slot` becomes their
24/// rendered home. This same technique is portable and applicable elsewhere via
25/// `Slot`.
26pub struct SlotInstance {
27    base: BaseInstance,
28}
29
30impl ImplToFromPaxAny for Slot {}
31
32///Contains the index value for slot, either a literal or an expression.
33#[derive(Default)]
34pub struct Slot {
35    // HACK: these two properties are being used in update:
36    pub index: Property<Numeric>,
37    pub is_remainder: Property<bool>,
38    pub last_node_id: Property<usize>,
39    // to compute this:
40    pub showing_node: Property<Weak<ExpandedNode>>,
41}
42
43#[derive(Clone, Copy)]
44enum SlotProjection {
45    Index(i64),
46    Remainder,
47}
48
49impl InstanceNode for SlotInstance {
50    fn instantiate(args: InstantiationArgs) -> Rc<Self>
51    where
52        Self: Sized,
53    {
54        Rc::new(Self {
55            base: BaseInstance::new(
56                args,
57                InstanceFlags {
58                    invisible_to_slot: false,
59                    invisible_to_raycasting: true,
60                    layer: Layer::DontCare,
61                    is_component: false,
62                    is_slot: true,
63                },
64            ),
65        })
66    }
67
68    fn handle_mount(
69        self: Rc<Self>,
70        expanded_node: &Rc<ExpandedNode>,
71        context: &Rc<RuntimeContext>,
72    ) {
73        let weak_ref_self = Rc::downgrade(expanded_node);
74        let cloned_context = Rc::clone(context);
75
76        let containing = expanded_node
77            .containing_component
78            .upgrade()
79            .expect("slot to have a containing component");
80
81        let nodes = containing.expanded_and_flattened_projected_children.clone();
82        let projected_children_listener = containing.projected_children_changed.clone();
83        let slot_projection_listener = containing.slot_projection_changed.clone();
84
85        let (index, is_remainder) =
86            expanded_node.with_properties_unwrapped(|properties: &mut Slot| {
87                (properties.index.clone(), properties.is_remainder.clone())
88            });
89
90        if !is_remainder.get() {
91            let index_for_listener = index.clone();
92            let slot_projection_listener = slot_projection_listener.clone();
93            expanded_node
94                .changed_listener
95                .replace_with(Property::computed_with_name(
96                    move || {
97                        let _ = index_for_listener.get();
98                        slot_projection_listener.set(());
99                    },
100                    &[index.untyped()],
101                    &format!("slot index listener (node id: {})", expanded_node.id.0),
102                ));
103        }
104
105        let deps = vec![
106            index.untyped(),
107            is_remainder.untyped(),
108            nodes.untyped(),
109            slot_projection_listener.untyped(),
110        ];
111
112        expanded_node
113            .children
114            .replace_with(Property::computed_with_name(
115                move || {
116                    let Some(cloned_expanded_node) = weak_ref_self.upgrade() else {
117                        panic!("ran evaluator after expanded node dropped (repeat elem)")
118                    };
119                    let children =
120                        compute_slot_projection(&cloned_expanded_node, &containing, nodes.get());
121                    let ret = cloned_expanded_node.attach_children(
122                        children,
123                        &cloned_context,
124                        &cloned_expanded_node.parent_frame,
125                    );
126                    projected_children_listener.set(());
127                    ret
128                },
129                &deps,
130                &format!("projected_children (node id: {})", expanded_node.id.0),
131            ));
132    }
133
134    fn resolve_debug(
135        &self,
136        f: &mut std::fmt::Formatter,
137        _expanded_node: Option<&ExpandedNode>,
138    ) -> std::fmt::Result {
139        f.debug_struct("Slot").finish()
140    }
141
142    fn base(&self) -> &BaseInstance {
143        &self.base
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::api::math::Transform2;
151    use crate::api::CommonProperties;
152    use crate::{
153        ComponentInstance, Globals, ReceivedChildrenSource, RouteLocation,
154        RuntimePropertiesStackFrame, TransformAndBounds,
155    };
156    use pax_runtime_api::borrow_mut;
157    use pax_runtime_api::pax_value::{PaxAny, PaxValue, ToFromPaxAny};
158    use pax_runtime_api::{Platform, TargetInfo, OS};
159    use std::cell::RefCell;
160    use std::iter;
161
162    fn test_globals() -> Globals {
163        Globals {
164            elapsed_frames: Property::new(0),
165            elapsed_millis: Property::new(0),
166            viewport: Property::new(TransformAndBounds {
167                transform: Transform2::identity(),
168                bounds: (100.0, 100.0),
169            }),
170            gyro: Property::new(Default::default()),
171            accel: Property::new(Default::default()),
172            route_location: Property::new(RouteLocation::root()),
173            browser_allows_scroller_vector_layers: Property::new(true),
174            browser_allows_nested_scroller_vector_layers: Property::new(true),
175            platform: Platform::Unknown,
176            os: OS::Unknown,
177            target: TargetInfo::new(Platform::Unknown, OS::Unknown),
178            get_elapsed_millis: Rc::new(|| 0),
179        }
180    }
181
182    fn default_common_properties_factory() -> Box<
183        dyn Fn(
184            Rc<RuntimePropertiesStackFrame>,
185            Option<Rc<ExpandedNode>>,
186        ) -> Option<Rc<RefCell<CommonProperties>>>,
187    > {
188        Box::new(|_, expanded_node| {
189            expanded_node
190                .is_none()
191                .then(|| Rc::new(RefCell::new(CommonProperties::default())))
192        })
193    }
194
195    fn empty_properties_factory() -> Box<
196        dyn Fn(
197            Rc<RuntimePropertiesStackFrame>,
198            Option<Rc<ExpandedNode>>,
199        ) -> Option<Rc<RefCell<PaxAny>>>,
200    > {
201        Box::new(|_, expanded_node| {
202            expanded_node
203                .is_none()
204                .then(|| Rc::new(RefCell::new(PaxAny::Builtin(Default::default()))))
205        })
206    }
207
208    fn component_args(
209        template: Option<Vec<Rc<dyn InstanceNode>>>,
210        children: Option<Vec<Rc<dyn InstanceNode>>>,
211    ) -> InstantiationArgs {
212        InstantiationArgs {
213            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
214                default_common_properties_factory(),
215            ),
216            prototypical_properties: crate::PropertiesInit::Factory(empty_properties_factory()),
217            handler_registry: None,
218            children: children.map(RefCell::new),
219            component_template: template.map(RefCell::new),
220            component_settings: None,
221            template_node_identifier: None,
222            template_node_type_id: None,
223            template_node_selector_info: None,
224            transition_config: Default::default(),
225            properties_scope: crate::PropertiesScopeInit::None,
226        }
227    }
228
229    fn leaf_args(label: &'static str) -> InstantiationArgs {
230        InstantiationArgs {
231            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
232                default_common_properties_factory(),
233            ),
234            prototypical_properties: crate::PropertiesInit::Factory(Box::new(
235                move |_, expanded_node| {
236                    expanded_node.is_none().then(|| {
237                        Rc::new(RefCell::new(PaxAny::Builtin(PaxValue::String(
238                            label.to_string(),
239                        ))))
240                    })
241                },
242            )),
243            handler_registry: None,
244            children: None,
245            component_template: None,
246            component_settings: None,
247            template_node_identifier: None,
248            template_node_type_id: None,
249            template_node_selector_info: None,
250            transition_config: Default::default(),
251            properties_scope: crate::PropertiesScopeInit::None,
252        }
253    }
254
255    fn slot_args(index: Option<Property<Numeric>>) -> InstantiationArgs {
256        InstantiationArgs {
257            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
258                default_common_properties_factory(),
259            ),
260            prototypical_properties: crate::PropertiesInit::Factory(Box::new(
261                move |_, expanded_node| {
262                    expanded_node.is_none().then(|| {
263                        let mut slot = Slot::default();
264                        match &index {
265                            Some(index) => slot.index = index.clone(),
266                            None => slot.is_remainder = Property::new(true),
267                        }
268                        Rc::new(RefCell::new(slot.to_pax_any()))
269                    })
270                },
271            )),
272            handler_registry: None,
273            children: None,
274            component_template: None,
275            component_settings: None,
276            template_node_identifier: None,
277            template_node_type_id: None,
278            template_node_selector_info: None,
279            transition_config: Default::default(),
280            properties_scope: crate::PropertiesScopeInit::None,
281        }
282    }
283
284    fn container_args(children: Vec<Rc<dyn InstanceNode>>) -> InstantiationArgs {
285        InstantiationArgs {
286            prototypical_common_properties: crate::CommonPropertiesInit::Factory(
287                default_common_properties_factory(),
288            ),
289            prototypical_properties: crate::PropertiesInit::Factory(empty_properties_factory()),
290            handler_registry: None,
291            children: Some(RefCell::new(children)),
292            component_template: None,
293            component_settings: None,
294            template_node_identifier: None,
295            template_node_type_id: None,
296            template_node_selector_info: None,
297            transition_config: Default::default(),
298            properties_scope: crate::PropertiesScopeInit::None,
299        }
300    }
301
302    struct TestContainer {
303        base: BaseInstance,
304    }
305
306    impl InstanceNode for TestContainer {
307        fn instantiate(args: InstantiationArgs) -> Rc<Self>
308        where
309            Self: Sized,
310        {
311            Rc::new(Self {
312                base: BaseInstance::new(
313                    args,
314                    InstanceFlags {
315                        invisible_to_slot: false,
316                        invisible_to_raycasting: true,
317                        layer: Layer::DontCare,
318                        is_component: false,
319                        is_slot: false,
320                    },
321                ),
322            })
323        }
324
325        fn handle_mount(
326            self: Rc<Self>,
327            expanded_node: &Rc<ExpandedNode>,
328            context: &Rc<RuntimeContext>,
329        ) {
330            let env = Rc::clone(&expanded_node.stack);
331            let children = borrow!(self.base().get_instance_children());
332            let children_with_env = children.iter().cloned().zip(iter::repeat(env));
333            let new_children = expanded_node.generate_children(
334                children_with_env,
335                context,
336                &expanded_node.parent_frame,
337                true,
338            );
339            expanded_node.children.set(new_children);
340        }
341
342        fn resolve_debug(
343            &self,
344            f: &mut std::fmt::Formatter,
345            _expanded_node: Option<&ExpandedNode>,
346        ) -> std::fmt::Result {
347            f.debug_struct("TestContainer").finish()
348        }
349
350        fn base(&self) -> &BaseInstance {
351            &self.base
352        }
353    }
354
355    struct TestLeaf {
356        base: BaseInstance,
357    }
358
359    impl InstanceNode for TestLeaf {
360        fn instantiate(args: InstantiationArgs) -> Rc<Self>
361        where
362            Self: Sized,
363        {
364            Rc::new(Self {
365                base: BaseInstance::new(
366                    args,
367                    InstanceFlags {
368                        invisible_to_slot: false,
369                        invisible_to_raycasting: false,
370                        layer: Layer::DontCare,
371                        is_component: false,
372                        is_slot: false,
373                    },
374                ),
375            })
376        }
377
378        fn resolve_debug(
379            &self,
380            f: &mut std::fmt::Formatter,
381            _expanded_node: Option<&ExpandedNode>,
382        ) -> std::fmt::Result {
383            f.debug_struct("TestLeaf").finish()
384        }
385
386        fn base(&self) -> &BaseInstance {
387            &self.base
388        }
389    }
390
391    struct TestProjectedContainer {
392        base: BaseInstance,
393    }
394
395    impl InstanceNode for TestProjectedContainer {
396        fn instantiate(args: InstantiationArgs) -> Rc<Self>
397        where
398            Self: Sized,
399        {
400            Rc::new(Self {
401                base: BaseInstance::new(
402                    args,
403                    InstanceFlags {
404                        invisible_to_slot: false,
405                        invisible_to_raycasting: true,
406                        layer: Layer::DontCare,
407                        is_component: false,
408                        is_slot: false,
409                    },
410                ),
411            })
412        }
413
414        fn received_children_source(&self) -> ReceivedChildrenSource {
415            ReceivedChildrenSource::Projected
416        }
417
418        fn handle_setup_projected_children(
419            self: Rc<Self>,
420            expanded_node: &Rc<ExpandedNode>,
421            context: &Rc<RuntimeContext>,
422        ) {
423            if let Some(containing_component) = expanded_node.containing_component.upgrade() {
424                let env = Rc::clone(&expanded_node.stack);
425                let children = borrow!(self.base().get_instance_children());
426                let children_with_env = children.iter().cloned().zip(iter::repeat(env));
427                let new_projected_children = containing_component.create_children_detached(
428                    children_with_env,
429                    context,
430                    &Rc::downgrade(expanded_node),
431                );
432                *borrow_mut!(expanded_node.expanded_projected_children) =
433                    Some(new_projected_children);
434            }
435        }
436
437        fn handle_mount(
438            self: Rc<Self>,
439            expanded_node: &Rc<ExpandedNode>,
440            context: &Rc<RuntimeContext>,
441        ) {
442            let weak_ref_self = Rc::downgrade(expanded_node);
443            let cloned_context = Rc::clone(context);
444            let parent_frame = expanded_node.parent_frame.clone();
445            let projected_children = expanded_node
446                .expanded_and_flattened_projected_children
447                .clone();
448            let deps = [projected_children.untyped()];
449            expanded_node
450                .children
451                .replace_with(Property::computed_with_name(
452                    move || {
453                        let Some(node) = weak_ref_self.upgrade() else {
454                            panic!("ran evaluator after expanded node dropped")
455                        };
456                        node.attach_children(
457                            projected_children.get(),
458                            &cloned_context,
459                            &parent_frame,
460                        )
461                    },
462                    &deps,
463                    &format!(
464                        "test_projected_container_children (node id: {})",
465                        expanded_node.id.0
466                    ),
467                ));
468            let _ = expanded_node.children.get();
469        }
470
471        fn update(self: Rc<Self>, expanded_node: &Rc<ExpandedNode>, _context: &Rc<RuntimeContext>) {
472            expanded_node.compute_flattened_projected_children();
473        }
474
475        fn resolve_debug(
476            &self,
477            f: &mut std::fmt::Formatter,
478            _expanded_node: Option<&ExpandedNode>,
479        ) -> std::fmt::Result {
480            f.debug_struct("TestProjectedContainer").finish()
481        }
482
483        fn base(&self) -> &BaseInstance {
484            &self.base
485        }
486    }
487
488    fn labels(nodes: &[Rc<ExpandedNode>]) -> Vec<String> {
489        nodes
490            .iter()
491            .map(|node| {
492                node.with_properties_unwrapped(|label: &mut PaxValue| match label {
493                    PaxValue::String(label) => label.clone(),
494                    _ => panic!("expected string label"),
495                })
496            })
497            .collect()
498    }
499
500    #[test]
501    fn indexed_slot_forwards_through_a_component() {
502        let slot: Rc<dyn InstanceNode> =
503            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(0)))));
504        let forwarding = forwarding_component(vec![slot]);
505        let (root, context) = projection_fixture(vec![forwarding]);
506        root.recurse_update(&context);
507
508        let owner = root.children.get()[0].clone();
509        let sites = active_slot_sites(&owner);
510        assert_eq!(sites.len(), 1, "discover the caller's forwarded slot");
511        assert_eq!(labels(&sites[0].children.get()), vec!["red"]);
512    }
513
514    fn forwarding_component(children: Vec<Rc<dyn InstanceNode>>) -> Rc<dyn InstanceNode> {
515        // Like Scroller, this component has its own projection sites, distinct
516        // from any caller-owned slots passed as its content.
517        let own_slot: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
518        ComponentInstance::instantiate(component_args(Some(vec![own_slot]), Some(children)))
519    }
520
521    fn projection_fixture(
522        template: Vec<Rc<dyn InstanceNode>>,
523    ) -> (Rc<ExpandedNode>, Rc<RuntimeContext>) {
524        let content = ["red", "orange", "yellow"]
525            .into_iter()
526            .map(|label| TestLeaf::instantiate(leaf_args(label)) as Rc<dyn InstanceNode>)
527            .collect();
528        let owner = ComponentInstance::instantiate(component_args(Some(template), Some(content)));
529        let root = ComponentInstance::instantiate(component_args(Some(vec![owner]), None));
530        let context = Rc::new(RuntimeContext::new(test_globals()));
531        (ExpandedNode::initialize_root(root, &context), context)
532    }
533
534    #[test]
535    fn forwarded_slots_preserve_owner_remainder_and_dynamic_index() {
536        let index = Property::new(Numeric::I64(1));
537        let selected: Rc<dyn InstanceNode> =
538            SlotInstance::instantiate(slot_args(Some(index.clone())));
539        let remainder: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
540        let nested = forwarding_component(vec![forwarding_component(vec![selected, remainder])]);
541        let (root, context) = projection_fixture(vec![nested]);
542        root.recurse_update(&context);
543
544        let owner = root.children.get()[0].clone();
545        let sites = active_slot_sites(&owner);
546        assert_eq!(sites.len(), 2, "nested components' own slots are excluded");
547        let original = owner.expanded_and_flattened_projected_children.get();
548        assert_eq!(labels(&sites[0].children.get()), vec!["orange"]);
549        assert_eq!(labels(&sites[1].children.get()), vec!["red", "yellow"]);
550
551        index.set(Numeric::I64(0));
552        root.recurse_update(&context);
553        assert_eq!(labels(&sites[0].children.get()), vec!["red"]);
554        assert_eq!(labels(&sites[1].children.get()), vec!["orange", "yellow"]);
555        assert_eq!(sites[0].children.get()[0].id, original[0].id);
556        for child in &original {
557            assert_eq!(child.attached.get(), 1, "forwarding must not double mount");
558            assert_eq!(
559                child.containing_component.upgrade().unwrap().id,
560                root.id,
561                "content retains the lexical owner that supplied it"
562            );
563        }
564    }
565
566    #[test]
567    fn conditional_forwarded_slot_releases_content_to_remainder() {
568        let visible = Property::new(true);
569        let selected: Rc<dyn InstanceNode> =
570            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(0)))));
571        let mut args = container_args(vec![selected]);
572        let condition = visible.clone();
573        args.prototypical_properties =
574            crate::PropertiesInit::Factory(Box::new(move |_, expanded_node| {
575                expanded_node.is_none().then(|| {
576                    Rc::new(RefCell::new(
577                        crate::conditional::ConditionalProperties {
578                            boolean_expression: condition.clone(),
579                            conditional_branches: vec![],
580                        }
581                        .to_pax_any(),
582                    ))
583                })
584            }));
585        let conditional = crate::conditional::ConditionalInstance::instantiate(args);
586        let forwarding = forwarding_component(vec![conditional]);
587        let remainder: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
588        let (root, context) = projection_fixture(vec![forwarding, remainder]);
589        root.recurse_update(&context);
590        let owner = root.children.get()[0].clone();
591        let sites = active_slot_sites(&owner);
592        assert_eq!(sites.len(), 2);
593        let remainder = sites[1].clone();
594        assert_eq!(labels(&remainder.children.get()), vec!["orange", "yellow"]);
595
596        visible.set(false);
597        root.recurse_update(&context);
598        assert_eq!(active_slot_sites(&owner).len(), 1);
599        assert_eq!(
600            labels(&remainder.children.get()),
601            vec!["red", "orange", "yellow"]
602        );
603
604        visible.set(true);
605        root.recurse_update(&context);
606        let sites = active_slot_sites(&owner);
607        assert_eq!(sites.len(), 2);
608        assert_eq!(labels(&sites[0].children.get()), vec!["red"]);
609        assert_eq!(labels(&remainder.children.get()), vec!["orange", "yellow"]);
610    }
611
612    #[test]
613    fn forwarded_duplicate_does_not_steal_an_earlier_slot() {
614        let first: Rc<dyn InstanceNode> =
615            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(0)))));
616        let duplicate: Rc<dyn InstanceNode> =
617            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(0)))));
618        let remainder: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
619        let nested = forwarding_component(vec![duplicate, remainder]);
620        let (root, context) = projection_fixture(vec![first, nested]);
621        root.recurse_update(&context);
622
623        let owner = root.children.get()[0].clone();
624        let sites = active_slot_sites(&owner);
625        assert_eq!(sites.len(), 3);
626        assert_eq!(labels(&sites[0].children.get()), vec!["red"]);
627        assert!(sites[1].children.get().is_empty());
628        assert!(sites[1].mounted_children.borrow().is_empty());
629        assert_eq!(labels(&sites[2].children.get()), vec!["orange", "yellow"]);
630    }
631
632    #[test]
633    fn remainder_slot_receives_unconsumed_projected_children() {
634        let slot_zero: Rc<dyn InstanceNode> =
635            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(0)))));
636        let remainder_slot: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
637        let nested: Rc<dyn InstanceNode> =
638            TestContainer::instantiate(container_args(vec![remainder_slot]));
639        let host: Rc<dyn InstanceNode> =
640            TestContainer::instantiate(container_args(vec![slot_zero, nested]));
641        let projected_children = ["red", "orange", "yellow", "green"]
642            .into_iter()
643            .map(|label| TestLeaf::instantiate(leaf_args(label)) as Rc<dyn InstanceNode>)
644            .collect::<Vec<_>>();
645        let slotted_component: Rc<dyn InstanceNode> = ComponentInstance::instantiate(
646            component_args(Some(vec![host]), Some(projected_children)),
647        );
648        let root_component =
649            ComponentInstance::instantiate(component_args(Some(vec![slotted_component]), None));
650        let context = Rc::new(RuntimeContext::new(test_globals()));
651        let root = ExpandedNode::initialize_root(root_component, &context);
652
653        root.recurse_update(&context);
654
655        let component = root.children.get()[0].clone();
656        let host = component.children.get()[0].clone();
657        let slot_zero = host.children.get()[0].clone();
658        let nested = host.children.get()[1].clone();
659        let remainder_slot = nested.children.get()[0].clone();
660
661        assert_eq!(labels(&slot_zero.children.get()), vec!["red"]);
662        assert_eq!(
663            labels(&remainder_slot.children.get()),
664            vec!["orange", "yellow", "green"]
665        );
666    }
667
668    #[test]
669    fn remainder_slot_reacts_to_dynamic_prior_index() {
670        let selected = Property::new(Numeric::I64(2));
671        let featured_slot: Rc<dyn InstanceNode> =
672            SlotInstance::instantiate(slot_args(Some(selected.clone())));
673        let remainder_slot: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
674        let host: Rc<dyn InstanceNode> =
675            TestContainer::instantiate(container_args(vec![featured_slot, remainder_slot]));
676        let projected_children = ["red", "orange", "yellow", "green"]
677            .into_iter()
678            .map(|label| TestLeaf::instantiate(leaf_args(label)) as Rc<dyn InstanceNode>)
679            .collect::<Vec<_>>();
680        let slotted_component: Rc<dyn InstanceNode> = ComponentInstance::instantiate(
681            component_args(Some(vec![host]), Some(projected_children)),
682        );
683        let root_component =
684            ComponentInstance::instantiate(component_args(Some(vec![slotted_component]), None));
685        let context = Rc::new(RuntimeContext::new(test_globals()));
686        let root = ExpandedNode::initialize_root(root_component, &context);
687
688        root.recurse_update(&context);
689        let component = root.children.get()[0].clone();
690        let host = component.children.get()[0].clone();
691        let featured_slot = host.children.get()[0].clone();
692        let remainder_slot = host.children.get()[1].clone();
693        assert_eq!(labels(&featured_slot.children.get()), vec!["yellow"]);
694        assert_eq!(
695            labels(&remainder_slot.children.get()),
696            vec!["red", "orange", "green"]
697        );
698
699        selected.set(Numeric::I64(0));
700        root.recurse_update(&context);
701
702        assert_eq!(labels(&featured_slot.children.get()), vec!["red"]);
703        assert_eq!(
704            labels(&remainder_slot.children.get()),
705            vec!["orange", "yellow", "green"]
706        );
707    }
708
709    #[test]
710    fn duplicate_explicit_slot_renders_empty_and_remainder_excludes_consumed_child() {
711        let first_slot: Rc<dyn InstanceNode> =
712            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(1)))));
713        let duplicate_slot: Rc<dyn InstanceNode> =
714            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(1)))));
715        let remainder_slot: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
716        let host: Rc<dyn InstanceNode> = TestContainer::instantiate(container_args(vec![
717            first_slot,
718            duplicate_slot,
719            remainder_slot,
720        ]));
721        let projected_children = ["red", "orange", "yellow", "green"]
722            .into_iter()
723            .map(|label| TestLeaf::instantiate(leaf_args(label)) as Rc<dyn InstanceNode>)
724            .collect::<Vec<_>>();
725        let slotted_component: Rc<dyn InstanceNode> = ComponentInstance::instantiate(
726            component_args(Some(vec![host]), Some(projected_children)),
727        );
728        let root_component =
729            ComponentInstance::instantiate(component_args(Some(vec![slotted_component]), None));
730        let context = Rc::new(RuntimeContext::new(test_globals()));
731        let root = ExpandedNode::initialize_root(root_component, &context);
732
733        root.recurse_update(&context);
734
735        let component = root.children.get()[0].clone();
736        let host = component.children.get()[0].clone();
737        let first_slot = host.children.get()[0].clone();
738        let duplicate_slot = host.children.get()[1].clone();
739        let remainder_slot = host.children.get()[2].clone();
740
741        assert_eq!(labels(&first_slot.children.get()), vec!["orange"]);
742        assert!(duplicate_slot.children.get().is_empty());
743        assert_eq!(
744            labels(&remainder_slot.children.get()),
745            vec!["red", "yellow", "green"]
746        );
747    }
748
749    #[test]
750    fn duplicate_slot_toggle_rehomes_projected_child_once() {
751        let first_index = Property::new(Numeric::I64(0));
752        let second_index = Property::new(Numeric::I64(0));
753        let first_slot: Rc<dyn InstanceNode> =
754            SlotInstance::instantiate(slot_args(Some(first_index)));
755        let duplicate_slot: Rc<dyn InstanceNode> =
756            SlotInstance::instantiate(slot_args(Some(second_index.clone())));
757        let remainder_slot: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
758        let host: Rc<dyn InstanceNode> = TestContainer::instantiate(container_args(vec![
759            first_slot,
760            duplicate_slot,
761            remainder_slot,
762        ]));
763        let projected_children = ["green", "blue"]
764            .into_iter()
765            .map(|label| TestLeaf::instantiate(leaf_args(label)) as Rc<dyn InstanceNode>)
766            .collect::<Vec<_>>();
767        let slotted_component: Rc<dyn InstanceNode> = ComponentInstance::instantiate(
768            component_args(Some(vec![host]), Some(projected_children)),
769        );
770        let root_component =
771            ComponentInstance::instantiate(component_args(Some(vec![slotted_component]), None));
772        let context = Rc::new(RuntimeContext::new(test_globals()));
773        let root = ExpandedNode::initialize_root(root_component, &context);
774        root.recurse_update(&context);
775
776        let component = root.children.get()[0].clone();
777        let host = component.children.get()[0].clone();
778        let first_slot = host.children.get()[0].clone();
779        let duplicate_slot = host.children.get()[1].clone();
780        let remainder_slot = host.children.get()[2].clone();
781
782        assert_eq!(labels(&first_slot.children.get()), vec!["green"]);
783        assert!(duplicate_slot.children.get().is_empty());
784        assert_eq!(labels(&remainder_slot.children.get()), vec!["blue"]);
785
786        second_index.set(Numeric::I64(1));
787        root.recurse_update(&context);
788
789        assert_eq!(labels(&first_slot.children.get()), vec!["green"]);
790        assert_eq!(labels(&duplicate_slot.children.get()), vec!["blue"]);
791        assert!(remainder_slot.children.get().is_empty());
792
793        second_index.set(Numeric::I64(0));
794        root.recurse_update(&context);
795
796        assert_eq!(labels(&first_slot.children.get()), vec!["green"]);
797        assert!(duplicate_slot.children.get().is_empty());
798        assert_eq!(labels(&remainder_slot.children.get()), vec!["blue"]);
799        assert!(duplicate_slot.mounted_children.borrow().is_empty());
800        assert_eq!(remainder_slot.mounted_children.borrow().len(), 1);
801    }
802
803    #[test]
804    fn reparenting_slot_child_clears_previous_owner_lists_before_previous_owner_recomputes() {
805        let first_slot: Rc<dyn InstanceNode> =
806            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(0)))));
807        let second_slot: Rc<dyn InstanceNode> =
808            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(0)))));
809        let remainder_slot: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
810        let host: Rc<dyn InstanceNode> = TestContainer::instantiate(container_args(vec![
811            first_slot,
812            second_slot,
813            remainder_slot,
814        ]));
815        let projected_children = ["green", "blue"]
816            .into_iter()
817            .map(|label| TestLeaf::instantiate(leaf_args(label)) as Rc<dyn InstanceNode>)
818            .collect::<Vec<_>>();
819        let slotted_component: Rc<dyn InstanceNode> = ComponentInstance::instantiate(
820            component_args(Some(vec![host]), Some(projected_children)),
821        );
822        let root_component =
823            ComponentInstance::instantiate(component_args(Some(vec![slotted_component]), None));
824        let context = Rc::new(RuntimeContext::new(test_globals()));
825        let root = ExpandedNode::initialize_root(root_component, &context);
826        root.recurse_update(&context);
827
828        let component = root.children.get()[0].clone();
829        let host = component.children.get()[0].clone();
830        let second_slot = host.children.get()[1].clone();
831        let remainder_slot = host.children.get()[2].clone();
832        let projected_child = remainder_slot.children.get()[0].clone();
833
834        second_slot.attach_children(
835            vec![Rc::clone(&projected_child)],
836            &context,
837            &second_slot.parent_frame,
838        );
839
840        assert!(remainder_slot.active_children.borrow().is_empty());
841        assert!(remainder_slot.mounted_children.borrow().is_empty());
842        assert_eq!(second_slot.mounted_children.borrow().len(), 1);
843        assert_eq!(projected_child.attached.get(), 1);
844
845        remainder_slot.attach_children(Vec::new(), &context, &remainder_slot.parent_frame);
846
847        assert_eq!(second_slot.mounted_children.borrow().len(), 1);
848        assert_eq!(projected_child.attached.get(), 1);
849    }
850
851    #[test]
852    fn out_of_range_explicit_slot_renders_empty_without_consuming_remainder() {
853        let out_of_range_slot: Rc<dyn InstanceNode> =
854            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(8)))));
855        let remainder_slot: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
856        let host: Rc<dyn InstanceNode> =
857            TestContainer::instantiate(container_args(vec![out_of_range_slot, remainder_slot]));
858        let projected_children = ["red", "orange", "yellow", "green"]
859            .into_iter()
860            .map(|label| TestLeaf::instantiate(leaf_args(label)) as Rc<dyn InstanceNode>)
861            .collect::<Vec<_>>();
862        let slotted_component: Rc<dyn InstanceNode> = ComponentInstance::instantiate(
863            component_args(Some(vec![host]), Some(projected_children)),
864        );
865        let root_component =
866            ComponentInstance::instantiate(component_args(Some(vec![slotted_component]), None));
867        let context = Rc::new(RuntimeContext::new(test_globals()));
868        let root = ExpandedNode::initialize_root(root_component, &context);
869
870        root.recurse_update(&context);
871
872        let component = root.children.get()[0].clone();
873        let host = component.children.get()[0].clone();
874        let out_of_range_slot = host.children.get()[0].clone();
875        let remainder_slot = host.children.get()[1].clone();
876
877        assert!(out_of_range_slot.children.get().is_empty());
878        assert_eq!(
879            labels(&remainder_slot.children.get()),
880            vec!["red", "orange", "yellow", "green"]
881        );
882    }
883
884    #[test]
885    fn remainder_slot_before_explicit_slot_consumes_all_remaining_children() {
886        let remainder_slot: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
887        let later_explicit_slot: Rc<dyn InstanceNode> =
888            SlotInstance::instantiate(slot_args(Some(Property::new(Numeric::I64(0)))));
889        let host: Rc<dyn InstanceNode> =
890            TestContainer::instantiate(container_args(vec![remainder_slot, later_explicit_slot]));
891        let projected_children = ["red", "orange", "yellow", "green"]
892            .into_iter()
893            .map(|label| TestLeaf::instantiate(leaf_args(label)) as Rc<dyn InstanceNode>)
894            .collect::<Vec<_>>();
895        let slotted_component: Rc<dyn InstanceNode> = ComponentInstance::instantiate(
896            component_args(Some(vec![host]), Some(projected_children)),
897        );
898        let root_component =
899            ComponentInstance::instantiate(component_args(Some(vec![slotted_component]), None));
900        let context = Rc::new(RuntimeContext::new(test_globals()));
901        let root = ExpandedNode::initialize_root(root_component, &context);
902
903        root.recurse_update(&context);
904
905        let component = root.children.get()[0].clone();
906        let host = component.children.get()[0].clone();
907        let remainder_slot = host.children.get()[0].clone();
908        let later_explicit_slot = host.children.get()[1].clone();
909
910        assert_eq!(
911            labels(&remainder_slot.children.get()),
912            vec!["red", "orange", "yellow", "green"]
913        );
914        assert!(later_explicit_slot.children.get().is_empty());
915    }
916
917    #[test]
918    fn remainder_slot_is_discovered_inside_projected_container_children() {
919        let remainder_slot: Rc<dyn InstanceNode> = SlotInstance::instantiate(slot_args(None));
920        let projected_container: Rc<dyn InstanceNode> =
921            TestProjectedContainer::instantiate(container_args(vec![remainder_slot]));
922        let projected_children = ["red", "orange", "yellow", "green"]
923            .into_iter()
924            .map(|label| TestLeaf::instantiate(leaf_args(label)) as Rc<dyn InstanceNode>)
925            .collect::<Vec<_>>();
926        let slotted_component: Rc<dyn InstanceNode> = ComponentInstance::instantiate(
927            component_args(Some(vec![projected_container]), Some(projected_children)),
928        );
929        let root_component =
930            ComponentInstance::instantiate(component_args(Some(vec![slotted_component]), None));
931        let context = Rc::new(RuntimeContext::new(test_globals()));
932        let root = ExpandedNode::initialize_root(root_component, &context);
933
934        root.recurse_update(&context);
935
936        let component = root.children.get()[0].clone();
937        let projected_container = component.children.get()[0].clone();
938        let remainder_slot = projected_container.children.get()[0].clone();
939
940        assert_eq!(
941            labels(&remainder_slot.children.get()),
942            vec!["red", "orange", "yellow", "green"]
943        );
944    }
945}
946
947fn compute_slot_projection(
948    current_slot: &Rc<ExpandedNode>,
949    containing_component: &Rc<ExpandedNode>,
950    projected_children: Vec<Rc<ExpandedNode>>,
951) -> Vec<Rc<ExpandedNode>> {
952    let slot_sites = active_slot_sites(containing_component);
953    let mut consumed = HashSet::new();
954
955    for slot_site in slot_sites {
956        let is_current = slot_site.id == current_slot.id;
957        match slot_projection(&slot_site) {
958            SlotProjection::Index(index) => {
959                let Some(index) = normalize_slot_index(index) else {
960                    if is_current {
961                        log::warn!("slot index {} is negative; rendering no slot child", index);
962                        return Vec::new();
963                    }
964                    continue;
965                };
966
967                if index >= projected_children.len() {
968                    if is_current {
969                        log::warn!(
970                            "slot index {} is out of range for {} projected children; rendering no slot child",
971                            index,
972                            projected_children.len()
973                        );
974                        return Vec::new();
975                    }
976                    continue;
977                }
978
979                if consumed.contains(&index) {
980                    if is_current {
981                        log::warn!(
982                            "slot index {} was already consumed by an earlier slot projection; rendering no slot child",
983                            index
984                        );
985                        return Vec::new();
986                    }
987                    continue;
988                }
989
990                consumed.insert(index);
991                if is_current {
992                    return vec![Rc::clone(&projected_children[index])];
993                }
994            }
995            SlotProjection::Remainder => {
996                let remainder_indices = (0..projected_children.len())
997                    .filter(|index| !consumed.contains(index))
998                    .collect::<Vec<_>>();
999                if is_current {
1000                    return remainder_indices
1001                        .into_iter()
1002                        .map(|index| Rc::clone(&projected_children[index]))
1003                        .collect();
1004                }
1005                consumed.extend(remainder_indices);
1006            }
1007        }
1008    }
1009
1010    Vec::new()
1011}
1012
1013fn normalize_slot_index(index: i64) -> Option<usize> {
1014    (index >= 0).then_some(index as usize)
1015}
1016
1017fn slot_projection(slot_node: &ExpandedNode) -> SlotProjection {
1018    slot_node.with_properties_unwrapped(|properties: &mut Slot| {
1019        if properties.is_remainder.get() {
1020            SlotProjection::Remainder
1021        } else {
1022            SlotProjection::Index(properties.index.get().to_int())
1023        }
1024    })
1025}
1026
1027fn active_slot_sites(containing_component: &Rc<ExpandedNode>) -> Vec<Rc<ExpandedNode>> {
1028    let mut slot_sites = Vec::new();
1029    for child in containing_component.children.get() {
1030        collect_active_slot_sites(&child, containing_component.id, &mut slot_sites);
1031    }
1032    slot_sites
1033}
1034
1035fn collect_active_slot_sites(
1036    node: &Rc<ExpandedNode>,
1037    containing_component_id: crate::ExpandedNodeIdentifier,
1038    slot_sites: &mut Vec<Rc<ExpandedNode>>,
1039) {
1040    let instance = borrow!(node.instance_node);
1041    let flags = instance.base().flags();
1042    if flags.is_slot {
1043        if node
1044            .containing_component
1045            .upgrade()
1046            .is_some_and(|component| component.id == containing_component_id)
1047        {
1048            slot_sites.push(Rc::clone(node));
1049        }
1050        return;
1051    }
1052
1053    if flags.is_component && node.id != containing_component_id {
1054        drop(instance);
1055        // A nested component owns its implementation, but not the content the
1056        // caller passed to it. Forwarded slot sites keep that caller's lexical
1057        // ownership (e.g. Carousel -> Scroller -> slot(i)). Walk the projected
1058        // input, not the nested template or its own projection sites.
1059        let projected = borrow!(node.expanded_projected_children).clone();
1060        for child in projected.iter().flatten() {
1061            collect_active_slot_sites(child, containing_component_id, slot_sites);
1062        }
1063        return;
1064    }
1065    drop(instance);
1066
1067    for child in node.children.get() {
1068        collect_active_slot_sites(&child, containing_component_id, slot_sites);
1069    }
1070}