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