tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1// =============================================================================
2// escena :: la grabadora de órdenes de dibujo
3// -----------------------------------------------------------------------------
4// `vello::Scene` es un BUFFER CODIFICADO: cada `fill`/`stroke` no dibuja nada,
5// apila bytes que después la GPU consume de una. `vello_cpu::RenderContext`, en
6// cambio, es IMPERATIVO y con estado: se le fija transform/paint/fill-rule y se
7// le pide `fill_path` ya mismo. Esa es toda la diferencia entre los dos mundos,
8// y es el trabajo que el plan (W1) señalaba: «vello_cpu cubre fill/stroke/clip/
9// Pixmap casi 1:1; NO tiene `Scene` — ahí está el trabajo».
10//
11// `Escena` cierra esa brecha grabando. Expone los OCHO métodos que
12// `llimphi-compositor` usa de `vello::Scene`, con la misma firma, y guarda cada
13// llamada como una `Orden`. Después `crate::pintor` las reproduce contra un
14// `RenderContext`. Grabar en vez de dibujar al vuelo no es un rodeo: es lo que
15// hace que `append` —componer una escena hija dentro de la madre, que el
16// compositor usa para reciclar subárboles cacheados— sea posible del todo.
17//
18// Las órdenes hablan en tipos de `peniko`/`kurbo`, que son LOS MISMOS que usa
19// vello (ambos resuelven a peniko 0.6). No hay conversión ni copia semántica en
20// el medio: `Color`, `Affine`, `BezPath` cruzan tal cual.
21// =============================================================================
22
23use alloc::vec::Vec;
24
25use vello_cpu::kurbo::{Affine, BezPath, Rect, RoundedRect, Shape, Stroke};
26use vello_cpu::peniko::{BlendMode, Brush, BrushRef, Color, Fill};
27
28/// Tolerancia de aplanado al convertir una forma arbitraria en `BezPath`.
29///
30/// Es la misma que vello codifica de fábrica para sus formas (0,1 px). Más fino
31/// no se nota en pantalla y cuesta segmentos; más grueso se ve en los radios.
32const TOLERANCIA: f64 = 0.1;
33
34/// Una forma grabada.
35///
36/// Se conservan los casos rápidos en su tipo original en vez de aplanar todo a
37/// `BezPath`: `vello_cpu` tiene `fill_rect`/`stroke_rect` dedicados, y el
38/// compositor de Llimphi dibuja rectángulos y rectángulos redondeados casi
39/// siempre (cajas, bordes, recortes). Aplanar eso a curvas sería pagar
40/// segmentos por nada en el 90% de los nodos.
41#[derive(Clone, Debug)]
42pub enum Forma {
43 /// Rectángulo recto — el camino barato.
44 Rect(Rect),
45 /// Rectángulo redondeado — se aplana al reproducir, pero se graba compacto.
46 Redondeado(RoundedRect),
47 /// Cualquier otra cosa, ya aplanada a curvas de Bézier.
48 Camino(BezPath),
49}
50
51impl Forma {
52 /// Graba una forma cualquiera, quedándose con la representación más barata.
53 ///
54 /// `kurbo::Shape` ofrece `as_rect`/`as_rounded_rect` justamente para esto:
55 /// preguntar sin castear ni conocer el tipo concreto.
56 pub fn de(forma: &impl Shape) -> Self {
57 if let Some(r) = forma.as_rect() {
58 Self::Rect(r)
59 } else if let Some(rr) = forma.as_rounded_rect() {
60 Self::Redondeado(rr)
61 } else {
62 Self::Camino(forma.into_path(TOLERANCIA))
63 }
64 }
65
66 /// La forma como camino de Bézier. Para el recorte de capa, donde
67 /// `vello_cpu` sólo acepta `BezPath`.
68 pub fn a_camino(&self) -> BezPath {
69 match self {
70 Self::Rect(r) => r.into_path(TOLERANCIA),
71 Self::Redondeado(rr) => rr.into_path(TOLERANCIA),
72 Self::Camino(c) => c.clone(),
73 }
74 }
75}
76
77/// Una orden de dibujo grabada, con todo lo que hace falta para reproducirla.
78///
79/// El pincel viaja **en propiedad** (`Brush`, no `BrushRef`): la escena
80/// sobrevive a quien la grabó —es justamente el punto de `append`— así que no
81/// puede quedarse con préstamos.
82#[derive(Clone, Debug)]
83pub enum Orden {
84 /// `Scene::fill`.
85 Rellenar {
86 regla: Fill,
87 xf: Affine,
88 pincel: Brush,
89 xf_pincel: Option<Affine>,
90 forma: Forma,
91 },
92 /// `Scene::stroke`.
93 Trazar {
94 estilo: Stroke,
95 xf: Affine,
96 pincel: Brush,
97 xf_pincel: Option<Affine>,
98 forma: Forma,
99 },
100 /// `Scene::push_layer`.
101 AbrirCapa {
102 regla: Fill,
103 mezcla: BlendMode,
104 alfa: f32,
105 xf: Affine,
106 recorte: Forma,
107 },
108 /// `Scene::push_luminance_mask_layer`.
109 AbrirMascaraLuminancia {
110 regla: Fill,
111 alfa: f32,
112 xf: Affine,
113 recorte: Forma,
114 },
115 /// `Scene::pop_layer`.
116 CerrarCapa,
117 /// `Scene::draw_image`.
118 Imagen {
119 imagen: vello_cpu::peniko::ImageBrush,
120 xf: Affine,
121 },
122 /// `Scene::draw_blurred_rounded_rect`.
123 RectDifuso {
124 xf: Affine,
125 rect: Rect,
126 color: Color,
127 radio: f64,
128 desvio: f64,
129 },
130 /// Una corrida de glifos ya shapeados. El equivalente de `Scene::draw_glyphs`.
131 Glifos {
132 fuente: vello_cpu::peniko::FontData,
133 tamano: f32,
134 xf: Affine,
135 pincel: Brush,
136 /// Ajustar los contornos a la grilla de píxeles. En tamaños de UI
137 /// (12-14 px) es la diferencia entre texto nítido y texto borroso.
138 hint: bool,
139 glifos: Vec<Glifo>,
140 },
141}
142
143/// Un glifo posicionado: qué dibujar y dónde, en coordenadas de la corrida.
144///
145/// Es un tipo propio y no el de vello ni el de `glifo` a propósito: la escena es
146/// el punto donde los dos mundos se tocan, y hacerla hablar el vocabulario de
147/// uno de los dos la ataría a él. Los tres campos son los mismos en ambos, así
148/// que la traducción es un `map` en el borde.
149#[derive(Clone, Copy, Debug, PartialEq)]
150pub struct Glifo {
151 /// Índice del glifo **dentro de la fuente**. No es un code point Unicode:
152 /// el shaping ya resolvió ligaduras, bidi y selección de cara.
153 pub id: u32,
154 /// Desplazamiento horizontal dentro de la corrida.
155 pub x: f32,
156 /// Desplazamiento vertical dentro de la corrida (línea base).
157 pub y: f32,
158}
159
160/// La escena grabada: una lista plana de órdenes, en orden de emisión.
161///
162/// Deliberadamente plana y `Clone`: reproducir es recorrerla, y `append` es
163/// concatenar. No hay árbol que recorrer ni punteros que arreglar.
164#[derive(Clone, Debug, Default)]
165pub struct Escena {
166 ordenes: Vec<Orden>,
167}
168
169impl Escena {
170 /// Una escena vacía.
171 pub fn new() -> Self {
172 Self::default()
173 }
174
175 /// Descarta todo lo grabado conservando la capacidad ya reservada.
176 ///
177 /// Es lo que se llama entre fotogramas: el bucle de Llimphi vuelve a pintar
178 /// la vista entera cada vez, y realocar el vector en cada frame sería
179 /// regalar trabajo al asignador —que en wawa es un `linked_list_allocator`
180 /// de kernel, no un malloc con arenas—.
181 pub fn reset(&mut self) {
182 self.ordenes.clear();
183 }
184
185 /// Las órdenes grabadas, en orden. Lo que consume `crate::pintor`.
186 pub fn ordenes(&self) -> &[Orden] {
187 &self.ordenes
188 }
189
190 /// Cuántas órdenes tiene. Útil para tests y para medir escenas.
191 pub fn len(&self) -> usize {
192 self.ordenes.len()
193 }
194
195 /// ¿No se grabó nada?
196 pub fn is_empty(&self) -> bool {
197 self.ordenes.is_empty()
198 }
199
200 // -------------------------------------------------------------------------
201 // Los ocho métodos que `llimphi-compositor` usa de `vello::Scene`.
202 // Las firmas son las de vello 0.7, a propósito: el objetivo es que el
203 // compositor no note con cuál de los dos está hablando.
204 // -------------------------------------------------------------------------
205
206 /// Rellena una forma. Espeja `vello::Scene::fill`.
207 pub fn fill<'b>(
208 &mut self,
209 style: Fill,
210 transform: Affine,
211 brush: impl Into<BrushRef<'b>>,
212 brush_transform: Option<Affine>,
213 shape: &impl Shape,
214 ) {
215 self.ordenes.push(Orden::Rellenar {
216 regla: style,
217 xf: transform,
218 pincel: brush.into().to_owned(),
219 xf_pincel: brush_transform,
220 forma: Forma::de(shape),
221 });
222 }
223
224 /// Traza el contorno de una forma. Espeja `vello::Scene::stroke`.
225 pub fn stroke<'b>(
226 &mut self,
227 style: &Stroke,
228 transform: Affine,
229 brush: impl Into<BrushRef<'b>>,
230 brush_transform: Option<Affine>,
231 shape: &impl Shape,
232 ) {
233 self.ordenes.push(Orden::Trazar {
234 estilo: style.clone(),
235 xf: transform,
236 pincel: brush.into().to_owned(),
237 xf_pincel: brush_transform,
238 forma: Forma::de(shape),
239 });
240 }
241
242 /// Abre una capa recortada. Espeja `vello::Scene::push_layer`.
243 pub fn push_layer(
244 &mut self,
245 clip_style: Fill,
246 blend: impl Into<BlendMode>,
247 alpha: f32,
248 transform: Affine,
249 clip: &impl Shape,
250 ) {
251 self.ordenes.push(Orden::AbrirCapa {
252 regla: clip_style,
253 mezcla: blend.into(),
254 alfa: alpha.clamp(0.0, 1.0),
255 xf: transform,
256 recorte: Forma::de(clip),
257 });
258 }
259
260 /// Abre una capa que enmascara por luminancia.
261 /// Espeja `vello::Scene::push_luminance_mask_layer`.
262 pub fn push_luminance_mask_layer(
263 &mut self,
264 clip_style: Fill,
265 alpha: f32,
266 transform: Affine,
267 clip: &impl Shape,
268 ) {
269 self.ordenes.push(Orden::AbrirMascaraLuminancia {
270 regla: clip_style,
271 alfa: alpha.clamp(0.0, 1.0),
272 xf: transform,
273 recorte: Forma::de(clip),
274 });
275 }
276
277 /// Cierra la última capa abierta. Espeja `vello::Scene::pop_layer`.
278 pub fn pop_layer(&mut self) {
279 self.ordenes.push(Orden::CerrarCapa);
280 }
281
282 /// Dibuja una imagen. Espeja `vello::Scene::draw_image`.
283 pub fn draw_image(&mut self, image: &vello_cpu::peniko::ImageBrush, transform: Affine) {
284 self.ordenes.push(Orden::Imagen {
285 imagen: image.clone(),
286 xf: transform,
287 });
288 }
289
290 /// Dibuja un rectángulo redondeado con desenfoque gaussiano — la sombra.
291 /// Espeja `vello::Scene::draw_blurred_rounded_rect`.
292 pub fn draw_blurred_rounded_rect(
293 &mut self,
294 transform: Affine,
295 rect: Rect,
296 brush: Color,
297 radius: f64,
298 std_dev: f64,
299 ) {
300 self.ordenes.push(Orden::RectDifuso {
301 xf: transform,
302 rect,
303 color: brush,
304 radio: radius,
305 desvio: std_dev,
306 });
307 }
308
309 /// Dibuja una corrida de glifos ya shapeados.
310 ///
311 /// **No espeja la firma de `vello::Scene::draw_glyphs`, y es a propósito.**
312 /// La de vello devuelve un *builder* (`DrawGlyphs`) que se va configurando y
313 /// se cierra con `.draw(style, glyphs)`. Reproducir ese builder acá sería
314 /// maquinaria para nadie: el único que llama a este método es código propio
315 /// —el puente de `llimphi-text` hacia el backend CPU—, no el DSL de widgets.
316 /// Los ocho métodos que sí espejan firma lo hacen porque `llimphi-compositor`
317 /// los invoca tal cual; éste no está en ese conjunto.
318 pub fn draw_glyphs<'b>(
319 &mut self,
320 fuente: &vello_cpu::peniko::FontData,
321 tamano: f32,
322 brush: impl Into<BrushRef<'b>>,
323 transform: Affine,
324 hint: bool,
325 glifos: impl IntoIterator<Item = Glifo>,
326 ) {
327 let glifos: Vec<Glifo> = glifos.into_iter().collect();
328 if glifos.is_empty() {
329 return;
330 }
331 self.ordenes.push(Orden::Glifos {
332 fuente: fuente.clone(),
333 tamano,
334 xf: transform,
335 pincel: brush.into().to_owned(),
336 hint,
337 glifos,
338 });
339 }
340
341 /// Incorpora una escena hija, opcionalmente transformada.
342 /// Espeja `vello::Scene::append`.
343 ///
344 /// Es la razón de ser de la grabación. El compositor cachea el dibujo de un
345 /// subárbol que no cambió y lo vuelve a pegar trasladado al origen nuevo;
346 /// sin escena grabada eso exigiría re-ejecutar el subárbol entero.
347 pub fn append(&mut self, other: &Self, transform: Option<Affine>) {
348 match transform {
349 None => self.ordenes.extend_from_slice(&other.ordenes),
350 Some(t) => self
351 .ordenes
352 .extend(other.ordenes.iter().map(|o| trasladar(o, t))),
353 }
354 }
355}
356
357/// Aplica una transformación a una orden ya grabada.
358///
359/// La transformación se compone POR IZQUIERDA (`t * xf`), igual que hace vello
360/// al concatenar encodings: la de la madre se aplica DESPUÉS de la de la hija,
361/// que es lo que uno espera al meter un dibujo dentro de otro ya posicionado.
362/// La transformación del pincel se deja como está: es relativa a la de la
363/// forma, no al mundo, y componerla dos veces desplazaría los gradientes.
364fn trasladar(orden: &Orden, t: Affine) -> Orden {
365 let mut o = orden.clone();
366 match &mut o {
367 Orden::Rellenar { xf, .. }
368 | Orden::Trazar { xf, .. }
369 | Orden::AbrirCapa { xf, .. }
370 | Orden::AbrirMascaraLuminancia { xf, .. }
371 | Orden::Imagen { xf, .. }
372 | Orden::RectDifuso { xf, .. }
373 | Orden::Glifos { xf, .. } => *xf = t * *xf,
374 Orden::CerrarCapa => {}
375 }
376 o
377}