tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main llimphi-compositor/src/render/memo.rs Rust · 225 siq'ikuna · 8.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

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 = &reg.seen;
223    reg.entries.retain(|k, _| seen.contains(k));
224}
225