tpt_appfront_core/component.rs
1//! Component model primitives shared across backends.
2//!
3//! `appfront`'s `#[component]` macro (re-exported as
4//! `tpt_appfront_core::component`) builds on these types to give components a
5//! real, React-like shape: typed `Props`, `children` slots, and optional
6//! memoization keyed on props equality.
7
8use crate::ui_tree::UITree;
9use std::cell::RefCell;
10use std::collections::HashMap;
11
12/// The `children` slot passed to a component that accepts them. A component
13/// declares a `children: Children<Msg>` parameter and renders the passed
14/// subtrees somewhere in its own tree:
15///
16/// ```ignore
17/// #[tpt_appfront_core::component]
18/// fn card(props: CardProps, children: Children<Msg>) -> UITree<Msg> {
19/// UITree::container(|c| {
20/// c.heading(2, &props.title);
21/// for child in children.0 {
22/// c.with(child);
23/// }
24/// })
25/// }
26/// ```
27///
28/// `Children` is just a typed `Vec<UITree<Msg>>` so a component can iterate and
29/// re-emit the slot content with full control over where it lands.
30#[derive(Debug, Clone, Default)]
31pub struct Children<Msg>(pub Vec<UITree<Msg>>);
32
33impl<Msg> Children<Msg> {
34 /// An empty slot.
35 pub fn none() -> Self {
36 Children(Vec::new())
37 }
38
39 /// Whether the slot carries no children.
40 pub fn is_empty(&self) -> bool {
41 self.0.is_empty()
42 }
43
44 /// Number of children in the slot.
45 pub fn len(&self) -> usize {
46 self.0.len()
47 }
48}
49
50thread_local! {
51 // The cache stores the last `(key, tree)` per component id. Both `P` (the
52 // memo key) and `Msg` are type-erased via `Box<dyn Any>`; `memoize` only
53 // ever downcasts to its own concrete `(P, UITree<Msg>)`, which is sound
54 // because each component id is generated once with a fixed `(P, Msg)`.
55 static MEMO_CACHE: RefCell<HashMap<u64, Box<dyn std::any::Any>>> =
56 RefCell::new(HashMap::new());
57}
58
59/// Memoize `build(key)` for the component identified by `id`. If the previous
60/// `key` for `id` is `PartialEq` to the current one, the cached `UITree<Msg>`
61/// is cloned and returned without calling `build`, preserving the previously
62/// built `Msg`-bound event closures. Otherwise `build` runs, its result is
63/// cached, and it is returned.
64///
65/// `P` must be `PartialEq + Clone + 'static`. This is the primitive behind
66/// `#[component(memo)]`.
67pub fn memoize<P, Msg, F>(id: u64, key: P, build: F) -> UITree<Msg>
68where
69 P: PartialEq + Clone + 'static,
70 Msg: Clone + 'static,
71 F: FnOnce(&P) -> UITree<Msg>,
72{
73 MEMO_CACHE.with(|cache| {
74 let mut cache = cache.borrow_mut();
75 if let Some(entry) = cache.get(&id) {
76 if let Some((prev_key, prev_tree)) =
77 entry.downcast_ref::<(P, UITree<Msg>)>()
78 {
79 if *prev_key == key {
80 return prev_tree.clone();
81 }
82 }
83 }
84 let tree = build(&key);
85 cache.insert(id, Box::new((key, tree.clone())) as Box<dyn std::any::Any>);
86 tree
87 })
88}