Skip to main content

tpt_appfront_core/
static_tree.rs

1//! Compile-time memoization of *static* `UITree` subtrees.
2//!
3//! A "static" subtree is one whose structure and every text/attribute value is
4//! known at compile time (no `{ expr }` interpolation in the `view!` macro).
5//! Because it never changes between renders, there is no reason to rebuild it
6//! every frame — so [`static_node`] builds it exactly once and returns a `clone`
7//! of the cached instance on every later call.
8//!
9//! This is the concrete payoff of the `#[appfront::component]` /
10//! `appfront::view!` `is_dynamic` analysis: instead of only *flagging*
11//! dynamic-ness as a hint, the codegen emits `static_node(...)` calls for the
12//! provably-static parts of the tree, so backends that rebuild the `UITree`
13//! every frame (canvas' immediate-mode `build_ui`, DOM hydration) skip that
14//! work for inert content.
15
16use crate::UITree;
17use std::any::Any;
18use std::cell::RefCell;
19use std::collections::HashMap;
20use std::rc::Rc;
21
22thread_local! {
23    /// `id -> cached tree` for the process. Keyed by a per-node unique id the
24    /// macro synthesizes (the address of a generated `static` sentinel), so two
25    /// distinct `view!` invocations can never collide.
26    static CACHE: RefCell<HashMap<u64, Rc<dyn Any>>> = RefCell::new(HashMap::new());
27}
28
29/// Builds `build()` exactly once and caches the result; subsequent calls return
30/// a `clone()` of the cached `UITree<Msg>`. `id` must be a stable, globally
31/// unique identifier for this static subtree — the macro generates one per
32/// static node (the address of a synthesized `static` sentinel), never a bare
33/// counter, because a bare counter would collide across separate `view!` calls
34/// sharing this one cache.
35///
36/// Panics on the (impossible) case that `id` was reused with a different `Msg`,
37/// because the cache stores `Rc<dyn Any>` keyed by `id` and a type mismatch
38/// would mean the macro assigned the same id twice.
39pub fn static_node<Msg: Clone + 'static>(
40    id: u64,
41    build: impl FnOnce() -> UITree<Msg>,
42) -> UITree<Msg> {
43    CACHE.with(|cache| {
44        let mut cache = cache.borrow_mut();
45        if let Some(existing) = cache.get(&id) {
46            let rc = existing.clone();
47            if let Ok(tree) = rc.downcast::<UITree<Msg>>() {
48                return (*tree).clone();
49            }
50            // Same id, different type — the macro generated a duplicate id.
51            panic!("appfront static_tree: duplicate static node id {id}");
52        }
53        let tree = build();
54        cache.insert(id, Rc::new(tree.clone()));
55        tree
56    })
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::{NodeKind, UITree};
63
64    #[test]
65    fn build_runs_exactly_once_per_id() {
66        let calls = std::rc::Rc::new(std::cell::Cell::new(0usize));
67        let calls2 = std::rc::Rc::clone(&calls);
68        let built = static_node(1, || {
69            calls2.set(calls2.get() + 1);
70            UITree::<()>::container(|c| {
71                c.text("hello");
72            })
73        });
74        // First call must have built it.
75        assert_eq!(calls.get(), 1);
76        assert!(matches!(built.kind, NodeKind::Container { .. }));
77
78        // Subsequent calls return clones without rebuilding.
79        for _ in 0..5 {
80            let _again = static_node(1, || {
81                panic!("build must not run again for a cached id");
82                #[allow(unreachable_code)]
83                UITree::<()>::container(|c| {
84                    c.text("never");
85                })
86            });
87        }
88        assert_eq!(calls.get(), 1);
89    }
90
91    #[test]
92    fn distinct_ids_cache_independently() {
93        let a = static_node(100, || {
94            UITree::<()>::container(|c| {
95                c.text("a");
96            })
97        });
98        let b = static_node(200, || {
99            UITree::<()>::container(|c| {
100                c.text("b");
101            })
102        });
103        match (&a.kind, &b.kind) {
104            (NodeKind::Container { children: ca }, NodeKind::Container { children: cb }) => {
105                match (&ca[0].kind, &cb[0].kind) {
106                    (NodeKind::Text { text: ta }, NodeKind::Text { text: tb }) => {
107                        assert_eq!(ta, "a");
108                        assert_eq!(tb, "b");
109                    }
110                    _ => panic!("expected text nodes"),
111                }
112            }
113            _ => panic!("expected container nodes"),
114        }
115    }
116
117    #[test]
118    fn cached_tree_is_independent_clone() {
119        let first = static_node(300, || {
120            UITree::<()>::container(|c| {
121                c.text("x");
122            })
123        });
124        let second = static_node(300, || {
125            panic!("build must not run again for a cached id");
126            #[allow(unreachable_code)]
127            UITree::<()>::container(|c| {
128                c.text("should-not-build");
129            })
130        });
131        // Both reference the same cached instance's data after cloning, so the
132        // content matches the first build, not the (never-run) second closure.
133        assert_eq!(format!("{first:?}"), format!("{second:?}"));
134    }
135}