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