tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos
1use super::*;
2
3// ─────────────────────────────────────────────────────────────────────────
4// Bloque 25 — Memo de view() (enfoque A: clone-splice). Un `View::memo(key,
5// deps, builder)` es un subárbol cuyo `View` NO se reconstruye si `deps` (ni el
6// tamaño del slot) cambiaron: el builder no se llama, y el `Mounted` cacheado se
7// splica en el árbol principal. El subárbol vive en un `LayoutTree` AISLADO que
8// taffy cachea → 0 re-solve. Ahorra view-build + mount + re-solve del subárbol;
9// paga un `clone` liviano (los handlers son `Arc`). Paint/hit-test quedan
10// INTACTOS (el splice produce un `Mounted` plano normal). Correctitud probada
11// por el arnés (Mounted memoizado == fresco).
12// ─────────────────────────────────────────────────────────────────────────
13
14/// Registro de subárboles memoizados (Bloque 25). Vive en el runtime entre
15/// frames. Por key: `deps` + tamaño del slot con que se computó + el
16/// `LayoutTree` aislado (cacheado por taffy) + el `Mounted` local + sus rects
17/// locales.
18pub struct MemoRegistry<Msg> {
19 entries: hashbrown::HashMap<u64, MemoEntry<Msg>>,
20 seen: hashbrown::HashSet<u64>,
21}
22
23pub(crate) struct MemoEntry<Msg> {
24 deps: u64,
25 avail: (f32, f32),
26 layout: LayoutTree,
27 nodes: Vec<MountedNode<Msg>>,
28 rects: Vec<llimphi_layout::Rect>,
29}
30
31impl<Msg> Default for MemoRegistry<Msg> {
32 fn default() -> Self {
33 Self {
34 entries: hashbrown::HashMap::new(),
35 seen: hashbrown::HashSet::new(),
36 }
37 }
38}
39
40impl<Msg> MemoRegistry<Msg> {
41 pub fn new() -> Self {
42 Self::default()
43 }
44 /// Subárboles memoizados vivos.
45 pub fn len(&self) -> usize {
46 self.entries.len()
47 }
48 pub fn is_empty(&self) -> bool {
49 self.entries.is_empty()
50 }
51}
52
53/// `true` si el árbol tiene algún nodo `memo` (fast-path para el runtime).
54pub fn has_memo<Msg>(view: &View<Msg>) -> bool {
55 view.rare.as_ref().is_some_and(|r| r.memo.is_some())
56 || view.children.iter().any(has_memo)
57}
58
59/// Extrae los builders de los nodos `memo` del `view` (dejándolos como
60/// marcadores `builder: None`), en pre-orden. El runtime los pasa a
61/// [`splice_memos`] tras el layout. Debe correr ANTES de `mount`.
62pub fn take_memo_builders<Msg>(
63 view: &mut View<Msg>,
64 out: &mut Vec<(u64, u64, MemoBuilderFn<Msg>, crate::Style)>,
65) {
66 if view
67 .rare
68 .as_ref()
69 .and_then(|r| r.memo.as_ref())
70 .is_some_and(|m| m.builder.is_some())
71 {
72 // Capturamos el `Style` del nodo memo: el subárbol aislado se computa
73 // como `container(style) -> content`, igual que el fresco, para que el
74 // contenido se estire dentro del box del memo (no se dimensione como raíz).
75 let style = view.style.clone();
76 let m = view.rare.as_mut().and_then(|r| r.memo.as_mut()).expect("memo");
77 let b = m.builder.take().expect("builder");
78 out.push((m.key, m.deps, b, style));
79 }
80 for c in &mut view.children {
81 take_memo_builders(c, out);
82 }
83}
84
85/// Splica los subárboles memoizados en el `Mounted`/`ComputedLayout` ya
86/// computados: para cada memo, si `deps` y el tamaño del slot no cambiaron reusa
87/// el `Mounted` cacheado (sin llamar el builder); si cambiaron, llama el builder,
88/// monta+computa el subárbol en su `LayoutTree` aislado y lo cachea. Luego clona
89/// los nodos cacheados con `NodeId` sintéticos (rango alto, colisión-proof),
90/// offsetea sus rects a la posición del marcador, y los inserta como hijos del
91/// marcador. `measure` mide una hoja de texto (misma que el compute principal).
92pub fn splice_memos<Msg, F>(
93 mounted: &mut Mounted<Msg>,
94 computed: &mut ComputedLayout,
95 pending: &[(u64, u64, MemoBuilderFn<Msg>, crate::Style)],
96 reg: &mut MemoRegistry<Msg>,
97 mut measure: F,
98) where
99 Msg: Clone,
100 F: FnMut(
101 &TextMeasure,
102 llimphi_layout::taffy::Size<Option<f32>>,
103 llimphi_layout::taffy::Size<llimphi_layout::taffy::AvailableSpace>,
104 ) -> llimphi_layout::taffy::Size<f32>,
105{
106 reg.seen.clear();
107 let mut synth: u64 = u64::MAX;
108 for (key, deps, builder, style) in pending {
109 reg.seen.insert(*key);
110 // Re-scan del marcador: los índices se corren tras cada splice.
111 let Some(mi) = mounted.nodes.iter().position(|n| n.memo_ref == Some(*key)) else {
112 continue;
113 };
114 let Some(slot) = computed.get(mounted.nodes[mi].id) else {
115 continue;
116 };
117 let abs = (slot.x, slot.y);
118 let avail = (slot.w, slot.h);
119
120 // Reuso si deps y tamaño del slot coinciden; si no, rebuild.
121 let hit = reg
122 .entries
123 .get(key)
124 .is_some_and(|e| e.deps == *deps && e.avail == avail);
125 if !hit {
126 // El aislado replica EXACTAMENTE el fresco: `container(style) ->
127 // content`. El `container` (índice 0) == el marcador; se computa con
128 // el mismo slot que el marcador, así el `content` se estira igual.
129 let root_view = View::new(style.clone()).children(vec![builder()]);
130 let mut layout = reg
131 .entries
132 .remove(key)
133 .map(|e| e.layout)
134 .unwrap_or_default();
135 layout.clear();
136 let sub_mounted = mount(&mut layout, root_view);
137 let sub_computed = {
138 let tm = &sub_mounted.text_measures;
139 layout
140 .compute_with_measure(sub_mounted.root, avail, |nid, k, a| match tm.get(&nid) {
141 Some(t) => measure(t, k, a),
142 None => llimphi_layout::taffy::Size {
143 width: 0.0,
144 height: 0.0,
145 },
146 })
147 .expect("layout memo")
148 };
149 // Origen del container (índice 0) para hacer los rects RELATIVOS a él
150 // (luego se offsetean al `abs` del marcador).
151 let origin = sub_computed
152 .get(sub_mounted.nodes[0].id)
153 .map(|r| (r.x, r.y))
154 .unwrap_or((0.0, 0.0));
155 let rects = sub_mounted
156 .nodes
157 .iter()
158 .map(|n| {
159 let r = sub_computed.get(n.id).unwrap_or(llimphi_layout::Rect {
160 x: 0.0,
161 y: 0.0,
162 w: 0.0,
163 h: 0.0,
164 });
165 llimphi_layout::Rect {
166 x: r.x - origin.0,
167 y: r.y - origin.1,
168 w: r.w,
169 h: r.h,
170 }
171 })
172 .collect();
173 reg.entries.insert(
174 *key,
175 MemoEntry {
176 deps: *deps,
177 avail,
178 layout,
179 nodes: sub_mounted.nodes,
180 rects,
181 },
182 );
183 }
184
185 let e = reg.entries.get(key).expect("insertado o reusado");
186 // Salteamos el índice 0 (el container == el marcador, ya en el árbol
187 // principal): sólo splicamos su CONTENIDO (`[1..]`).
188 let sub_count = e.nodes.len().saturating_sub(1);
189 if sub_count == 0 {
190 continue;
191 }
192 let base = mi + 1;
193 let mut cloned: Vec<MountedNode<Msg>> = Vec::with_capacity(sub_count);
194 for (i, n) in e.nodes.iter().enumerate().skip(1) {
195 let mut c = n.clone();
196 let sid = NodeId::from(synth);
197 synth -= 1;
198 c.id = sid;
199 // Índice local `i` (≥1) → absoluto `mi + i`; subtree_end local + mi.
200 c.subtree_end = n.subtree_end + mi;
201 let r = e.rects[i];
202 computed.rects.insert(
203 sid,
204 llimphi_layout::Rect {
205 x: r.x + abs.0,
206 y: r.y + abs.1,
207 w: r.w,
208 h: r.h,
209 },
210 );
211 cloned.push(c);
212 }
213 // Corre los subtree_end existentes que abarcaban el marcador (>mi).
214 for n in mounted.nodes.iter_mut() {
215 if n.subtree_end > mi {
216 n.subtree_end += sub_count;
217 }
218 }
219 mounted.nodes.splice(base..base, cloned);
220 }
221 // GC de las keys que no aparecieron este frame.
222 let seen = ®.seen;
223 reg.entries.retain(|k, _| seen.contains(k));
224}
225