gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ e74800d9dab0bd8fcc82d36f776628ad45cedfd3 llimphi-compositor/src/view.rs Rust · 408 líneas · 14.3 KB Historia Quién tocó qué crudo

feat: llimphi standalone — framework UI soberano extraído del monorepo e65e9cc62 · Sergio ·

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}