gioser

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Sutichaykuna

⎇ main llimphi-jaula/src/lib.rs Rust · 174 siq'ikuna · 7.0 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

1// =============================================================================
2//  llimphi-jaula :: la cáscara del lado APP  (W4 de 03_ukupacha/wawa/PLAN-LLIMPHI.md)
3// -----------------------------------------------------------------------------
4//  `llimphi-wawa` es la cáscara del kernel: recibe una escena y la ejecuta. Ésta
5//  es la del otro lado de la frontera: compone una escena y la manda.
6//
7//      View<Msg> → mount → layout → paint → EscenaWire → postcard → …
8//      …………………… frontera WASM ……………………
9//                                        … → shapear nativo → rasterizar
10//
11//  Las dos corren el MISMO `llimphi-compositor` — el mismo mount, el mismo
12//  layout de taffy, el mismo `render.rs` de 4.000 líneas. Lo único que cambia es
13//  el alias `Lienzo` y, con él, el `Typesetter`. Ése es el resultado de W4 que
14//  vale la pena decir en voz alta: **no hay un compositor para apps y otro para
15//  el kernel**.
16//
17//  ## Las dos cosas que esta cáscara hace y la del kernel no
18//
19//  **Pregunta las medidas.** taffy no puede dimensionar una etiqueta sin saber
20//  cuánto mide, y medir es shapear — justo lo que no debe pasar acá adentro (el
21//  30,3x del intérprete se comería el 72% del fotograma, W4.1). Así que el
22//  medidor es un argumento: en wawa envuelve un `sys_`, en un test envuelve un
23//  `Typesetter` de parley de verdad. Ver `llimphi_text::wire`.
24//
25//  **Se queda con el árbol.** El `Mounted` no cruza la frontera, así que el
26//  hit-test se resuelve de este lado: el kernel manda una coordenada y la app
27//  contesta qué se tocó. Es lo contrario de la cáscara del kernel, que no puede
28//  hacer hit-test justamente porque el árbol se quedó acá.
29// =============================================================================
30
31#![cfg_attr(not(any(feature = "std", test)), no_std)]
32
33extern crate alloc;
34
35use alloc::collections::BTreeSet;
36use llimphi_compositor::Mounted;
37use llimphi_layout::{ComputedLayout, LayoutTree};
38
39/// Cómo preguntarle las medidas al kernel — el otro lado de W4.
40pub mod medidor;
41
42pub use llimphi_compositor::View;
43pub use llimphi_layout::taffy;
44pub use llimphi_layout::Style;
45pub use llimphi_text;
46pub use llimphi_text::peniko;
47pub use llimphi_text::peniko::Color;
48pub use llimphi_text::wire::{Medida, Peticion, Typesetter};
49pub use llimphi_wire_escena as wire;
50pub use llimphi_wire_escena::EscenaWire;
51
52/// La cáscara de la app: el estado que sobrevive entre fotogramas.
53pub struct Jaula {
54    typesetter: Typesetter,
55    escena: EscenaWire,
56    computado: Option<ComputedLayout>,
57    ancho: f32,
58    alto: f32,
59    /// **Qué huellas de imagen ya le mandamos al anfitrión.**
60    ///
61    /// Las órdenes de imagen viajan por huella y los píxeles aparte; la escena
62    /// se arma de cero en cada fotograma y no sabe nada de la conexión, así que
63    /// la memoria de «esto ya lo mandé» tiene que vivir acá, que es lo único
64    /// que dura lo que dura el caño. Sin ella cada cuadro remandaría todas las
65    /// miniaturas visibles — con veintiocho celdas eso son megabytes por
66    /// fotograma para pintar exactamente lo mismo.
67    blobs_enviados: BTreeSet<u64>,
68}
69
70impl Jaula {
71    /// Una jaula que mide preguntando del otro lado de la frontera.
72    ///
73    /// El medidor NO es opcional en la práctica: sin él el layout es una
74    /// estimación grosera. Se pide por constructor para que olvidarlo sea
75    /// imposible en vez de ser un default silencioso.
76    pub fn new<F>(ancho: f32, alto: f32, medidor: F) -> Self
77    where
78        F: FnMut(&Peticion<'_>) -> Medida + 'static,
79    {
80        Self {
81            typesetter: Typesetter::con_medidor(medidor),
82            escena: EscenaWire::new(),
83            computado: None,
84            ancho,
85            alto,
86            blobs_enviados: BTreeSet::new(),
87        }
88    }
89
90    /// Cuántas medidas salieron de la estimación de emergencia en vez del
91    /// medidor. Distinto de cero = el layout de esas etiquetas es aproximado.
92    pub fn medidas_estimadas(&self) -> usize {
93        self.typesetter.medidas_estimadas()
94    }
95
96    /// Redimensiona el viewport.
97    pub fn redimensionar(&mut self, ancho: f32, alto: f32) {
98        self.ancho = ancho;
99        self.alto = alto;
100    }
101
102    /// Corre un fotograma: monta, layoutea preguntando las medidas, y graba la
103    /// escena para mandar.
104    ///
105    /// Devuelve el `Mounted` por la misma razón que [`llimphi_wawa::Cascara`]:
106    /// es genérico en `Msg` y la cáscara no lo es. Acá además hace falta
107    /// conservarlo, porque el hit-test se resuelve de este lado.
108    pub fn fotograma<Msg: Clone>(&mut self, vista: View<Msg>) -> Mounted<Msg> {
109        let mut arbol = LayoutTree::new();
110        let montado = llimphi_compositor::mount(&mut arbol, vista);
111
112        let ts = &mut self.typesetter;
113        let tmap = &montado.text_measures;
114        let computado = arbol
115            .compute_with_measure(montado.root, (self.ancho, self.alto), |nid, k, a| {
116                match tmap.get(&nid) {
117                    Some(tm) => llimphi_compositor::measure_text_node(ts, tm, k, a),
118                    None => taffy::Size {
119                        width: 0.0,
120                        height: 0.0,
121                    },
122                }
123            })
124            .unwrap_or_default();
125
126        self.escena.reset();
127        llimphi_compositor::paint(
128            &mut self.escena,
129            &montado,
130            &computado,
131            &mut self.typesetter,
132            None,
133            None,
134        );
135        self.computado = Some(computado);
136        montado
137    }
138
139    /// La escena del último fotograma, lista para serializar y mandar.
140    pub fn escena(&self) -> &EscenaWire {
141        &self.escena
142    }
143
144    /// **La escena lista para mandar**: la del último fotograma, con los blobs
145    /// de imagen que el anfitrión **todavía no tiene**.
146    ///
147    /// Es la que hay que usar para enviar; [`Self::escena`] sigue devolviendo la
148    /// cruda para quien quiera mirarla (tests, volcados) sin consumir la memoria
149    /// de lo enviado. Llamarla dos veces por fotograma deja la segunda sin
150    /// blobs, que es correcto: ya salieron en la primera.
151    pub fn escena_para_enviar(&mut self) -> &EscenaWire {
152        let enviados = &mut self.blobs_enviados;
153        self.escena.retener_blobs(|h| enviados.insert(h));
154        &self.escena
155    }
156
157    /// El layout del último fotograma, contra el que probar los clicks.
158    pub fn computado(&self) -> Option<&ComputedLayout> {
159        self.computado.as_ref()
160    }
161
162    /// El `Msg` del nodo clickeable bajo `(x, y)`.
163    ///
164    /// El hit-test vive de ESTE lado a propósito: el árbol nunca cruzó la
165    /// frontera. El kernel manda una coordenada; qué hay debajo lo sabe la app.
166    pub fn click<Msg: Clone>(&self, montado: &Mounted<Msg>, x: f32, y: f32) -> Option<Msg> {
167        let computado = self.computado.as_ref()?;
168        let idx = llimphi_compositor::hit_test_click(montado, computado, x, y)?;
169        montado.nodes.get(idx).and_then(|n| n.on_click.clone())
170    }
171}
172
173#[cfg(test)]
174mod pruebas;