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
12pub struct SlotInstance {
27 base: BaseInstance,
28}
29
30impl ImplToFromPaxAny for Slot {}
31
32#[derive(Default)]
34pub struct Slot {
35 pub index: LocalProperty<Numeric>,
37 pub is_remainder: LocalProperty<bool>,
38 pub last_node_id: LocalProperty<usize>,
39 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 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 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}