tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
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;