telar_ui_core/slots.rs
1use std::any::Any;
2use std::rc::Rc;
3
4use layout_core::LayoutError;
5
6use crate::layout_item::LayoutItem;
7
8/// The children a component receives from its call site, grouped by slot. A bare child lands in the
9/// default slot (`None`); a child written with `slot:"name"` lands in that named slot. Inside the
10/// component, the `children` placeholder drains the default slot and `children name:"x"` drains the
11/// `"x"` slot — each in call-site order. Draining is one-shot: a slot placeholder consumes its
12/// children, so referencing the same slot twice yields an empty list the second time.
13#[derive(Default)]
14pub struct Slots {
15 items: Vec<(Option<&'static str>, Box<dyn LayoutItem>)>,
16}
17
18impl Slots {
19 pub fn new() -> Self {
20 Self::default()
21 }
22
23 pub fn push(&mut self, name: Option<&'static str>, item: Box<dyn LayoutItem>) {
24 self.items.push((name, item));
25 }
26
27 /// Appends `items` as default (unnamed) children. What every generated component call site does with the
28 /// children it collected, so the emitter writes one call instead of a hand-rolled loop 418 times over.
29 pub fn extend_default(&mut self, items: impl IntoIterator<Item = Box<dyn LayoutItem>>) {
30 self.items
31 .extend(items.into_iter().map(|item| (None, item)));
32 }
33
34 /// How many children are still undrained, across every slot.
35 pub fn len(&self) -> usize {
36 self.items.len()
37 }
38
39 /// Whether the call site passed no children at all — what a compound component checks before falling back
40 /// to whatever it shows when it was given nothing.
41 pub fn is_empty(&self) -> bool {
42 self.items.is_empty()
43 }
44
45 /// Drains the default (unnamed) children in call-site order.
46 pub fn take_default(&mut self) -> Vec<Box<dyn LayoutItem>> {
47 self.take_matching(|n| n.is_none())
48 }
49
50 /// Drains the children assigned to the named slot `name`, in call-site order.
51 pub fn take(&mut self, name: &str) -> Vec<Box<dyn LayoutItem>> {
52 self.take_matching(|n| *n == Some(name))
53 }
54
55 fn take_matching(
56 &mut self,
57 pred: impl Fn(&Option<&'static str>) -> bool,
58 ) -> Vec<Box<dyn LayoutItem>> {
59 let mut taken = Vec::new();
60 let mut rest = Vec::new();
61 for (name, item) in std::mem::take(&mut self.items) {
62 if pred(&name) {
63 taken.push(item);
64 } else {
65 rest.push((name, item));
66 }
67 }
68 self.items = rest;
69 taken
70 }
71}
72
73/// A component's markup children, **not yet built**.
74///
75/// [`Slots`] is the list a call site already made; this is the recipe for making it. The difference is the
76/// whole of what a compound component needs, and it comes from one fact about how a tree is assembled here:
77/// a child is an argument, so it is constructed *before* the parent it is passed to. A `Select.Item` that
78/// wanted to know which select it belongs to, what is currently chosen, or what to call when it is picked,
79/// was asking a question about something that did not exist yet.
80///
81/// Handed the recipe instead, the parent builds its context first and then runs the recipe inside it, so a
82/// child reaches the parent through [`use_context`](crate::use_context) rather than through props threaded
83/// down by hand. The recipe is `Fn`, not `FnOnce`, for a second reason that is not theoretical: a dropdown
84/// rebuilds its rows every time the panel opens, so the children have to be makeable more than once.
85#[derive(Clone)]
86pub struct Children(Rc<dyn Fn() -> Result<Slots, LayoutError>>);
87
88impl Children {
89 pub fn new(build: impl Fn() -> Result<Slots, LayoutError> + 'static) -> Self {
90 Self(Rc::new(build))
91 }
92
93 /// Builds the children with `context` visible to every one of them, and to anything they build in turn.
94 ///
95 /// The scope is nested, so a select inside a select's own row shadows the outer one rather than
96 /// colliding with it, and it closes when this returns — a widget built afterwards sees nothing.
97 pub fn build_with<T: Any + 'static>(&self, context: T) -> Result<Slots, LayoutError> {
98 services_core::Scope::with(|| {
99 // The scope is fresh, so the only way this fails is a caller providing the same type twice into
100 // one scope, which is a bug in the component rather than anything its call site can cause.
101 let _ = services_core::provide(context);
102 (self.0)()
103 })
104 }
105
106 /// Builds the children with no context of their own, for a component that has nothing to tell them.
107 pub fn build(&self) -> Result<Slots, LayoutError> {
108 (self.0)()
109 }
110}
111
112impl Default for Children {
113 fn default() -> Self {
114 Self::new(|| Ok(Slots::new()))
115 }
116}
117
118impl From<Slots> for Children {
119 /// For a caller holding children it already built — a test, or a component forwarding what it was given.
120 /// The recipe hands them out once and is empty on any later call, since a built widget cannot be made twice.
121 fn from(slots: Slots) -> Self {
122 let cell = std::cell::RefCell::new(Some(slots));
123 Self::new(move || Ok(cell.borrow_mut().take().unwrap_or_default()))
124 }
125}
126
127/// The nearest enclosing value of type `T` that a parent provided, or `None` outside any such parent.
128///
129/// The read half of [`Children::build_with`]. `None` is the honest answer for a piece used on its own — an
130/// item outside any menu — and a compound component's pieces should say so rather than panicking, since the
131/// call site that made the mistake is markup, not Rust.
132pub fn use_context<T: Any + Clone + 'static>() -> Option<T> {
133 services_core::try_inject::<T>()
134}
135
136#[cfg(test)]
137mod tests {
138 use std::cell::RefCell;
139 use std::rc::Rc;
140
141 use layout_core::LayoutStyle;
142
143 use super::*;
144 use crate::container::Container;
145 use crate::context::reset_layout_runtime;
146 use crate::layout_item::box_item;
147
148 #[derive(Clone)]
149 struct Menu(&'static str);
150
151 /// A child records what it could see of its parent while it was being built.
152 fn spy(seen: Rc<RefCell<Vec<Option<&'static str>>>>) -> Children {
153 Children::new(move || {
154 seen.borrow_mut().push(use_context::<Menu>().map(|m| m.0));
155 let mut slots = Slots::new();
156 slots.push(None, box_item(Container::new(LayoutStyle::new(), vec![])?));
157 Ok(slots)
158 })
159 }
160
161 /// The inversion the whole type exists for. A child is an argument, so it is normally constructed before
162 /// its parent — build it from a recipe instead and the parent gets to exist first.
163 #[test]
164 fn a_child_built_from_the_recipe_can_see_the_parent_making_it() {
165 reset_layout_runtime();
166 let seen = Rc::new(RefCell::new(Vec::new()));
167 let children = spy(seen.clone());
168
169 let slots = children.build_with(Menu("edit")).unwrap();
170 assert_eq!(*seen.borrow(), vec![Some("edit")]);
171 assert_eq!(slots.len(), 1, "and it is still a child, not just a reader");
172 }
173
174 /// A piece used on its own gets `None` rather than a panic: the mistake was made in markup, and a
175 /// component that dies on it reports it as a crash in Rust nobody wrote.
176 #[test]
177 fn a_child_outside_any_parent_sees_nothing() {
178 reset_layout_runtime();
179 let seen = Rc::new(RefCell::new(Vec::new()));
180 spy(seen.clone()).build().unwrap();
181 assert_eq!(*seen.borrow(), vec![None]);
182 }
183
184 /// The context closes with the build. A widget made afterwards is not inside that menu, and must not
185 /// find it lying around.
186 #[test]
187 fn the_context_does_not_outlive_the_build_that_opened_it() {
188 reset_layout_runtime();
189 let seen = Rc::new(RefCell::new(Vec::new()));
190 let children = spy(seen.clone());
191 children.build_with(Menu("edit")).unwrap();
192 assert_eq!(use_context::<Menu>().map(|m| m.0), None);
193 }
194
195 /// Nesting is what a submenu is, and the inner one has to win inside itself without disturbing the outer.
196 #[test]
197 fn a_nested_parent_shadows_the_one_it_sits_in() {
198 reset_layout_runtime();
199 let inner_seen = Rc::new(RefCell::new(Vec::new()));
200 let inner = spy(inner_seen.clone());
201 let outer_seen = Rc::new(RefCell::new(Vec::new()));
202 let outer = {
203 let outer_seen = outer_seen.clone();
204 Children::new(move || {
205 outer_seen
206 .borrow_mut()
207 .push(use_context::<Menu>().map(|m| m.0));
208 inner.build_with(Menu("submenu"))?;
209 // Read again after the inner scope closed, which is where a stack that popped wrongly shows.
210 outer_seen
211 .borrow_mut()
212 .push(use_context::<Menu>().map(|m| m.0));
213 Ok(Slots::new())
214 })
215 };
216
217 outer.build_with(Menu("edit")).unwrap();
218 assert_eq!(*inner_seen.borrow(), vec![Some("submenu")]);
219 assert_eq!(*outer_seen.borrow(), vec![Some("edit"), Some("edit")]);
220 }
221
222 /// The reason the recipe is `Fn` and not `FnOnce`: a dropdown throws its rows away and remakes them every
223 /// time the panel opens, so children that could only be built once would come back empty on the second open.
224 #[test]
225 fn the_recipe_can_be_run_more_than_once() {
226 reset_layout_runtime();
227 let seen = Rc::new(RefCell::new(Vec::new()));
228 let children = spy(seen.clone());
229
230 for _ in 0..3 {
231 assert_eq!(children.build_with(Menu("edit")).unwrap().len(), 1);
232 }
233 assert_eq!(seen.borrow().len(), 3, "a fresh set of rows each time");
234 }
235}