tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ e74800d9dab0bd8fcc82d36f776628ad45cedfd3
llimphi-compositor/src/view.rs
Historia
Quién tocó qué
crudo
feat: llimphi standalone — framework UI soberano extraído del monorepo
1use super::*;
2
3impl<Msg> View<Msg> {
4 pub fn new(style: Style) -> Self {
5 Self {
6 style,
7 fill: None,
8 hover_fill: None,
9 radius: 0.0,
10 text: None,
11 image: None,
12 painter: None,
13 gpu_painter: None,
14 on_pointer_enter: None,
15 on_pointer_leave: None,
16 on_click: None,
17 on_click_at: None,
18 on_right_click: None,
19 on_right_click_at: None,
20 on_middle_click: None,
21 drag: None,
22 drag_at: None,
23 drag_payload: None,
24 on_drop: None,
25 drop_hover_fill: None,
26 clip: false,
27 on_scroll: None,
28 focusable: None,
29 alpha: None,
30 transform: None,
31 tooltip: None,
32 children: Vec::new(),
33 }
34 }
35
36 /// Asocia un texto de **tooltip** a este nodo. Llimphi sólo lo transporta
37 /// hasta el [`MountedNode`](crate::MountedNode); el consumidor decide cómo
38 /// mostrarlo (un overlay del runtime, una surface popup del cliente) tras
39 /// localizar el nodo bajo el cursor con el hit-test de hover.
40 pub fn tooltip(mut self, text: impl Into<String>) -> Self {
41 self.tooltip = Some(text.into());
42 self
43 }
44
45 /// Registra un handler de rueda local: si el cursor está sobre este
46 /// nodo cuando la rueda gira, el runtime lo invoca con el delta
47 /// `(dx, dy)` en líneas lógicas ANTES de caer al `App::on_wheel`
48 /// global. Devolver `Some(Msg)` consume el evento. Es la base de las
49 /// áreas de scroll autocontenidas (`llimphi-widget-scroll`).
50 pub fn on_scroll<F>(mut self, handler: F) -> Self
51 where
52 F: Fn(f32, f32) -> Option<Msg> + Send + Sync + 'static,
53 {
54 self.on_scroll = Some(Arc::new(handler));
55 self
56 }
57
58 /// Marca este nodo como enfocable con el id opaco `id`. El runtime lo
59 /// incluye en el orden de Tab (pre-orden del árbol) y le da foco al
60 /// clickearlo; cada cambio de foco se notifica vía `App::on_focus`.
61 /// El caller pinta el focus-ring comparando el id contra el foco que
62 /// guardó en su `Model`.
63 pub fn focusable(mut self, id: u64) -> Self {
64 self.focusable = Some(id);
65 self
66 }
67
68 /// Aplica una transformación afín 2D a este nodo y todo su subtree,
69 /// **alrededor del centro de su rect** (CSS `transform-origin: 50%
70 /// 50%`). El centro se resuelve en `paint` contra el layout computado;
71 /// el caller sólo provee el afín "local" (producto de sus
72 /// `rotate`/`scale`/`translate`). Nodos anidados componen en el
73 /// espacio ya transformado del padre. Pensado para `transform` y
74 /// `@keyframes` CSS de puriy. `Affine::IDENTITY` equivale a no setear.
75 pub fn transform(mut self, xf: Affine) -> Self {
76 self.transform = Some(xf);
77 self
78 }
79
80 pub fn fill(mut self, color: Color) -> Self {
81 self.fill = Some(color);
82 self
83 }
84
85 /// Opacidad uniforme aplicada a este nodo y todos sus descendientes
86 /// vía `scene.push_layer(Mix::Normal, a, …)`. Pensado para fade-in/out
87 /// de overlays, toasts y modales sin tener que tunear el alpha de
88 /// cada color del subtree. Valores fuera de `[0.0, 1.0]` se clampean.
89 /// Hace que el subtree se componga en una capa intermedia — usar sólo
90 /// cuando sea necesario (no es gratuito).
91 pub fn alpha(mut self, a: f32) -> Self {
92 self.alpha = Some(a.clamp(0.0, 1.0));
93 self
94 }
95
96 /// Color a usar cuando el cursor está sobre este nodo. Habilita
97 /// el hit-test de hover sobre el nodo.
98 pub fn hover_fill(mut self, color: Color) -> Self {
99 self.hover_fill = Some(color);
100 self
101 }
102
103 /// Marca este nodo como draggable. Mientras el usuario sostenga el
104 /// botón izquierdo sobre él, el runtime llama `handler(Move, dx, dy)`
105 /// por cada `CursorMoved` (dx/dy = delta desde el evento anterior) y
106 /// `handler(End, 0, 0)` al soltar. Sobreescribe `on_click` para este
107 /// nodo: un nodo es draggable **o** clickable.
108 pub fn draggable<F>(mut self, handler: F) -> Self
109 where
110 F: Fn(DragPhase, f32, f32) -> Option<Msg> + Send + Sync + 'static,
111 {
112 self.drag = Some(Arc::new(handler));
113 self
114 }
115
116 /// Como `draggable`, pero el handler también recibe la posición
117 /// inicial del press relativa al rect del nodo `(initial_lx,
118 /// initial_ly)`. Útil cuando el caller necesita resolver qué
119 /// entidad bajo el cursor inició el drag (Conceptos, lemmings,
120 /// nodos de un grafo, etc.). Gana sobre `draggable` si ambos están.
121 pub fn draggable_at<F>(mut self, handler: F) -> Self
122 where
123 F: Fn(DragPhase, f32, f32, f32, f32) -> Option<Msg> + Send + Sync + 'static,
124 {
125 self.drag_at = Some(Arc::new(handler));
126 self
127 }
128
129 /// Declara el payload `u64` que viaja con el drag de este nodo. Los
130 /// drop targets bajo cursor al soltar reciben este valor en su
131 /// `on_drop`. Sin payload, los drop targets no reaccionan (útil para
132 /// drags de "resize/scroll" que no representan transferencia).
133 pub fn drag_payload(mut self, payload: u64) -> Self {
134 self.drag_payload = Some(payload);
135 self
136 }
137
138 /// Marca este nodo como drop target. El runtime invoca `handler(payload)`
139 /// cuando un drag termina sobre el rect de este nodo y el origen del
140 /// drag declaró un payload. Si devuelve `Some(Msg)`, se dispatchea al
141 /// `update` antes del `DragPhase::End` del origen.
142 pub fn on_drop<F>(mut self, handler: F) -> Self
143 where
144 F: Fn(u64) -> Option<Msg> + Send + Sync + 'static,
145 {
146 self.on_drop = Some(Arc::new(handler));
147 self
148 }
149
150 /// Color de relleno cuando un drag activo está hovereando este drop
151 /// target. Análogo a `hover_fill` pero solo aplica mientras dura un
152 /// drag. Útil para resaltar el destino válido.
153 pub fn drop_hover_fill(mut self, color: Color) -> Self {
154 self.drop_hover_fill = Some(color);
155 self
156 }
157
158 pub fn radius(mut self, r: f64) -> Self {
159 self.radius = r;
160 self
161 }
162
163 pub fn text(mut self, content: impl Into<String>, size_px: f32, color: Color) -> Self {
164 self.text = Some(TextSpec {
165 content: content.into(),
166 size_px,
167 color,
168 alignment: llimphi_text::Alignment::Center,
169 italic: false,
170 font_family: None,
171 line_height: 1.2,
172 runs: None,
173 });
174 self
175 }
176
177 pub fn text_aligned(
178 mut self,
179 content: impl Into<String>,
180 size_px: f32,
181 color: Color,
182 alignment: llimphi_text::Alignment,
183 ) -> Self {
184 self.text = Some(TextSpec {
185 content: content.into(),
186 size_px,
187 color,
188 alignment,
189 italic: false,
190 font_family: None,
191 line_height: 1.2,
192 runs: None,
193 });
194 self
195 }
196
197 /// Como `text_aligned` pero con un flag `italic`. Si la fuente activa
198 /// no tiene variante italic, parley aplica synthesizing.
199 pub fn text_aligned_italic(
200 mut self,
201 content: impl Into<String>,
202 size_px: f32,
203 color: Color,
204 alignment: llimphi_text::Alignment,
205 italic: bool,
206 ) -> Self {
207 self.text = Some(TextSpec {
208 content: content.into(),
209 size_px,
210 color,
211 alignment,
212 italic,
213 font_family: None,
214 line_height: 1.2,
215 runs: None,
216 });
217 self
218 }
219
220 /// Como `text_aligned_italic` pero con font-family explícito.
221 /// La cadena se pasa como `parley::FontStack::Source` (acepta listas
222 /// CSS con fallbacks).
223 pub fn text_aligned_full(
224 mut self,
225 content: impl Into<String>,
226 size_px: f32,
227 color: Color,
228 alignment: llimphi_text::Alignment,
229 italic: bool,
230 font_family: Option<String>,
231 ) -> Self {
232 self.text = Some(TextSpec {
233 content: content.into(),
234 size_px,
235 color,
236 alignment,
237 italic,
238 font_family,
239 line_height: 1.2,
240 runs: None,
241 });
242 self
243 }
244
245 /// Texto **multicolor** en una sola pasada de shaping: `content` se pinta
246 /// con `default_color` y cada `(start_byte, end_byte, color)` de `runs`
247 /// sobreescribe su rango (offsets en bytes). Pensado para syntax
248 /// highlighting — un nodo por línea en vez de uno por token. Anclado
249 /// arriba-izquierda (sin centrado vertical); el caller dimensiona el rect.
250 pub fn text_runs(
251 mut self,
252 content: impl Into<String>,
253 size_px: f32,
254 default_color: Color,
255 runs: Vec<(usize, usize, Color)>,
256 alignment: llimphi_text::Alignment,
257 ) -> Self {
258 self.text = Some(TextSpec {
259 content: content.into(),
260 size_px,
261 color: default_color,
262 alignment,
263 italic: false,
264 font_family: None,
265 line_height: 1.2,
266 runs: Some(runs),
267 });
268 self
269 }
270
271 /// Sobreescribe el múltiplo de interlínea del texto ya seteado (default
272 /// 1.2). No-op si el nodo no tiene texto. Pensado para puriy, que pasa
273 /// el `line-height` computado de CSS para que medición y pintado usen
274 /// el mismo valor.
275 pub fn line_height(mut self, mult: f32) -> Self {
276 if let Some(t) = self.text.as_mut() {
277 t.line_height = mult;
278 }
279 self
280 }
281
282 pub fn on_click(mut self, msg: Msg) -> Self {
283 self.on_click = Some(msg);
284 self
285 }
286
287 /// Dispatch `msg` cuando el cursor entra al rect del nodo
288 /// (transición no-hover → hover). Sólo emite una vez por entrada —
289 /// el runtime no repite el msg si el cursor se mueve dentro del rect.
290 pub fn on_pointer_enter(mut self, msg: Msg) -> Self {
291 self.on_pointer_enter = Some(msg);
292 self
293 }
294
295 /// Dispatch `msg` cuando el cursor sale del rect del nodo.
296 pub fn on_pointer_leave(mut self, msg: Msg) -> Self {
297 self.on_pointer_leave = Some(msg);
298 self
299 }
300
301 /// Como `on_click`, pero el handler recibe `(local_x, local_y,
302 /// rect_w, rect_h)` — la posición del cursor relativa al rect del
303 /// nodo más las dimensiones actuales del nodo. Útil para canvas
304 /// elements que necesitan saber dónde fue el click para convertirlo
305 /// a coordenadas de mundo. Sobrescribe `on_click` para este nodo
306 /// si ambos están presentes.
307 pub fn on_click_at<F>(mut self, handler: F) -> Self
308 where
309 F: Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync + 'static,
310 {
311 self.on_click_at = Some(Arc::new(handler));
312 self
313 }
314
315 /// Declara el `Msg` a emitir cuando el usuario hace click derecho
316 /// sobre este nodo. Para menús contextuales, conviene pasar un
317 /// `Msg::OpenMenu { ... }` y dejar que el modelo guarde la
318 /// posición; el overlay se abre vía [`App::view_overlay`].
319 pub fn on_right_click(mut self, msg: Msg) -> Self {
320 self.on_right_click = Some(msg);
321 self
322 }
323
324 /// Variante posicional de [`Self::on_right_click`]. El handler recibe
325 /// `(local_x, local_y, rect_w, rect_h)` para que un nodo "grilla"
326 /// pueda resolver internamente qué subcelda recibió el click. La
327 /// posición está relativa al rect del nodo.
328 pub fn on_right_click_at<F>(mut self, handler: F) -> Self
329 where
330 F: Fn(f32, f32, f32, f32) -> Option<Msg> + Send + Sync + 'static,
331 {
332 self.on_right_click_at = Some(Arc::new(handler));
333 self
334 }
335
336 /// Declara el `Msg` a emitir cuando el usuario hace click con el
337 /// botón del medio (rueda presionada). Usado típicamente para abrir
338 /// links en pestaña nueva — igual que Ctrl+Click pero más rápido.
339 pub fn on_middle_click(mut self, msg: Msg) -> Self {
340 self.on_middle_click = Some(msg);
341 self
342 }
343
344 /// Pinta `image` dentro del rect del nodo, centrada y escalada
345 /// preservando aspect ratio. Re-exporta `peniko::Image` vía
346 /// `llimphi_raster::peniko::Image` — el caller decodifica los
347 /// bytes con el crate `image` (u otro) y construye el `Image`
348 /// con `Blob<u8>` + `ImageFormat::Rgba8`.
349 pub fn image(mut self, image: Image) -> Self {
350 self.image = Some(image);
351 self
352 }
353
354 /// Registra una closure de pintura custom. El runtime la invoca
355 /// con `(&mut vello::Scene, &mut Typesetter, PaintRect)` durante
356 /// el paint del nodo. La closure es responsable de pintar
357 /// primitivas custom dentro del rect; no debe dejar `push_layer`
358 /// sin par. Soporte para canvas elements estilo
359 /// dominium/pluma/cosmos.
360 pub fn paint_with<F>(mut self, painter: F) -> Self
361 where
362 F: Fn(&mut vello::Scene, &mut llimphi_text::Typesetter, PaintRect)
363 + Send
364 + Sync
365 + 'static,
366 {
367 self.painter = Some(Arc::new(painter));
368 self
369 }
370
371 /// Registra una closure de pintura GPU directo. La closure recibe
372 /// `(&Device, &Queue, &mut CommandEncoder, &TextureView, PaintRect, (viewport_w, viewport_h))`
373 /// y debe escribir sobre el `TextureView` con `LoadOp::Load` (no
374 /// clear) para preservar la pasada vello previa. El último
375 /// argumento es el tamaño en pixels de la `TextureView` destino
376 /// (la intermedia del frame) — necesario para calcular NDC sin
377 /// asumir un viewport fijo. Ver [`GpuPaintFn`] para semántica
378 /// completa, contexto y orden de pintura.
379 pub fn gpu_paint_with<F>(mut self, painter: F) -> Self
380 where
381 F: Fn(
382 &wgpu::Device,
383 &wgpu::Queue,
384 &mut wgpu::CommandEncoder,
385 &wgpu::TextureView,
386 PaintRect,
387 (u32, u32),
388 ) + Send
389 + Sync
390 + 'static,
391 {
392 self.gpu_painter = Some(Arc::new(painter));
393 self
394 }
395
396 /// Recorta los hijos al rect de este nodo (paint y hit-test). Útil
397 /// para paneles con contenido virtualizado que no debe sangrar a
398 /// vecinos (listas, scrollers, viewers).
399 pub fn clip(mut self, enabled: bool) -> Self {
400 self.clip = enabled;
401 self
402 }
403
404 pub fn children(mut self, children: Vec<View<Msg>>) -> Self {
405 self.children = children;
406 self
407 }
408}