Skip to main content

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}