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}