Skip to main content

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}