tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-wire-view/src/lib.rs Rust · 441 siq'ikuna · 12.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! llimphi-wire-view — IR serializable de un subset de `View<Msg>`.
2//!
3//! Un `WireNode` espeja la forma de `llimphi_compositor::View`: un árbol de
4//! cajas con dirección flex, dimensiones, padding, relleno, radio, un texto
5//! opcional y handlers. La diferencia clave: los callbacks **no cruzan la
6//! frontera WASM**. En lugar de un `Fn() -> Msg`, cada nodo interactivo carga
7//! `on_click: Option<Vec<u8>>` — los bytes postcard del `Msg` del guest. El
8//! host rebota esos bytes a `dispatch` del guest, que los decodifica y corre
9//! su `update`. Como en Llimphi `on_click` ya es `Option<Msg>` por valor (no
10//! una clausura), el mapeo es directo.
11//!
12//! `no_std + alloc`: el mismo IR sirve para un guest no_std (incluso una app
13//! wawa) y para el host con std.
14
15#![no_std]
16
17extern crate alloc;
18
19use alloc::string::String;
20use alloc::vec::Vec;
21use serde::{Deserialize, Serialize};
22
23/// Color RGBA de 8 bits por canal — el mismo formato que `Color::from_rgba8`.
24pub type Rgba = [u8; 4];
25
26/// Dimensión de un eje. Espeja `taffy::Dimension` en su subset útil.
27#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
28pub enum Dim {
29    /// Tamaño automático (contenido).
30    Auto,
31    /// Píxeles lógicos.
32    Px(f32),
33    /// Fracción del padre, `0.0..=1.0`.
34    Pct(f32),
35}
36
37impl Default for Dim {
38    fn default() -> Self {
39        Dim::Auto
40    }
41}
42
43/// Dirección del eje principal flex.
44#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
45pub enum Dir {
46    /// Caja simple (hoja o stack vertical). Se materializa como columna.
47    Block,
48    /// Fila — hijos en horizontal.
49    Row,
50    /// Columna — hijos en vertical.
51    Column,
52}
53
54impl Default for Dir {
55    fn default() -> Self {
56        Dir::Block
57    }
58}
59
60/// Alineación cruzada (`align-items`).
61#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
62pub enum Align {
63    Start,
64    Center,
65    End,
66    Stretch,
67}
68
69/// Distribución en el eje principal (`justify-content`).
70#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
71pub enum Justify {
72    Start,
73    Center,
74    End,
75    SpaceBetween,
76    SpaceAround,
77}
78
79/// Alineación horizontal del texto dentro de su caja.
80#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize)]
81pub enum TextAlign {
82    #[default]
83    Start,
84    Center,
85    End,
86}
87
88/// Texto a rasterizar dentro de un nodo.
89#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
90pub struct WireText {
91    pub content: String,
92    pub size: f32,
93    pub color: Rgba,
94    pub align: TextAlign,
95    pub italic: bool,
96}
97
98/// Identificador de un handler de evento. El guest lo asigna por frame (en su
99/// `view`); el host lo rebota en `dispatch(event_id, payload)` y el guest mira
100/// su tabla para reconstruir el `Msg`. Reemplaza el viejo modelo de "bytes del
101/// Msg en el nodo": ahora el nodo lleva sólo el id, así un evento puede acarrear
102/// un payload (texto tecleado, estado de un toggle) que el host inyecta.
103pub type EventId = u32;
104
105/// Lo que un evento acarrea de vuelta al guest. El host lo serializa (postcard)
106/// y lo pasa a `dispatch`; el guest lo entrega al handler del `event_id`.
107#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
108pub enum EventPayload {
109    /// Click/tap — sin datos. Handler `Unit(Msg)`.
110    Click,
111    /// Texto nuevo de un campo editable. Handler `Text(Fn(String)->Msg)`.
112    Text(String),
113    /// Nuevo estado de un checkbox/switch. Handler `Toggle(Fn(bool)->Msg)`.
114    Toggle(bool),
115    /// Nuevo valor de un slider. Handler `Value(Fn(f32)->Msg)`.
116    Value(f32),
117    /// Índice de la opción elegida en un dropdown. Handler `Select(Fn(u32)->Msg)`.
118    Select(u32),
119}
120
121/// Campo de texto editable. El `value` es la fuente de verdad del guest; el host
122/// lo pinta y, al teclear, emite `on_input` con el texto nuevo (modelo Elm: el
123/// host no guarda estado de edición, sólo notifica).
124#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
125pub struct WireInput {
126    pub value: String,
127    pub placeholder: String,
128    /// Oculta el contenido (●●●) — para contraseñas.
129    pub password: bool,
130    /// Acepta varias líneas: Enter inserta `\n` en vez de ignorarse. El guest
131    /// sigue siendo la fuente de verdad del texto (modelo value-driven).
132    pub multiline: bool,
133}
134
135/// Slider: barra con un valor en `[min, max]`. Al clickear/arrastrar, el host
136/// emite `on_value` con el valor nuevo según la posición.
137#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
138pub struct WireSlider {
139    pub value: f32,
140    pub min: f32,
141    pub max: f32,
142}
143
144/// Dropdown: lista de opciones con una seleccionada. El host pinta el header con
145/// la opción actual; al elegir otra, emite `on_select` con su índice.
146#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
147pub struct WireSelect {
148    pub options: Vec<String>,
149    pub selected: u32,
150}
151
152/// Grupo de radio: lista siempre visible con una opción marcada. Al clickear
153/// otra, emite `on_radio` con su índice (mismo payload que el dropdown,
154/// `Select(idx)`). Difiere del dropdown sólo en el render: todas las opciones se
155/// ven a la vez con un indicador ◉/○, sin estado de apertura.
156#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
157pub struct WireRadio {
158    pub options: Vec<String>,
159    pub selected: u32,
160}
161
162/// Nodo del árbol. Recursivo, igual que `View<Msg>`. Construir con el builder
163/// encadenable o con los constructores libres (`col`, `row`, `text`, `leaf`).
164/// Los nodos interactivos los arma el `Ui` del SDK, que asigna los `EventId`.
165#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
166pub struct WireNode {
167    pub dir: Dir,
168    pub width: Dim,
169    pub height: Dim,
170    pub grow: f32,
171    /// Gap entre hijos (ambos ejes), en px lógicos.
172    pub gap: f32,
173    /// Padding `[top, right, bottom, left]`, en px lógicos.
174    pub padding: [f32; 4],
175    pub align: Option<Align>,
176    pub justify: Option<Justify>,
177    pub fill: Option<Rgba>,
178    pub radius: f32,
179    pub text: Option<WireText>,
180    /// Campo editable, si este nodo es un input.
181    pub input: Option<WireInput>,
182    /// Estado de un checkbox, si este nodo es uno.
183    pub toggle: Option<bool>,
184    /// Slider, si este nodo es uno.
185    pub slider: Option<WireSlider>,
186    /// Dropdown, si este nodo es uno.
187    pub select: Option<WireSelect>,
188    /// Grupo de radio, si este nodo es uno.
189    pub radio: Option<WireRadio>,
190    /// Handler de click (evento sin payload).
191    pub on_click: Option<EventId>,
192    /// Handler del texto tecleado en `input`.
193    pub on_input: Option<EventId>,
194    /// Handler del cambio de `toggle`.
195    pub on_toggle: Option<EventId>,
196    /// Handler del valor de `slider`.
197    pub on_value: Option<EventId>,
198    /// Handler de la opción elegida en `select`.
199    pub on_select: Option<EventId>,
200    /// Handler de la opción elegida en `radio`.
201    pub on_radio: Option<EventId>,
202    pub children: Vec<WireNode>,
203}
204
205impl WireNode {
206    pub fn new() -> Self {
207        Self::default()
208    }
209
210    pub fn dir(mut self, d: Dir) -> Self {
211        self.dir = d;
212        self
213    }
214
215    pub fn width(mut self, d: Dim) -> Self {
216        self.width = d;
217        self
218    }
219
220    pub fn height(mut self, d: Dim) -> Self {
221        self.height = d;
222        self
223    }
224
225    pub fn size(mut self, w: Dim, h: Dim) -> Self {
226        self.width = w;
227        self.height = h;
228        self
229    }
230
231    pub fn grow(mut self, g: f32) -> Self {
232        self.grow = g;
233        self
234    }
235
236    pub fn gap(mut self, g: f32) -> Self {
237        self.gap = g;
238        self
239    }
240
241    /// Padding uniforme en los cuatro lados.
242    pub fn pad(mut self, p: f32) -> Self {
243        self.padding = [p; 4];
244        self
245    }
246
247    /// Padding por lado `[top, right, bottom, left]`.
248    pub fn padding(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
249        self.padding = [top, right, bottom, left];
250        self
251    }
252
253    pub fn align(mut self, a: Align) -> Self {
254        self.align = Some(a);
255        self
256    }
257
258    pub fn justify(mut self, j: Justify) -> Self {
259        self.justify = Some(j);
260        self
261    }
262
263    pub fn fill(mut self, color: Rgba) -> Self {
264        self.fill = Some(color);
265        self
266    }
267
268    pub fn radius(mut self, r: f32) -> Self {
269        self.radius = r;
270        self
271    }
272
273    /// Fija el texto del nodo (alineación `Start`, sin itálica).
274    pub fn label(mut self, content: impl Into<String>, size: f32, color: Rgba) -> Self {
275        self.text = Some(WireText {
276            content: content.into(),
277            size,
278            color,
279            align: TextAlign::Start,
280            italic: false,
281        });
282        self
283    }
284
285    /// Alinea el texto ya fijado (no-op si no hay texto).
286    pub fn text_align(mut self, align: TextAlign) -> Self {
287        if let Some(t) = self.text.as_mut() {
288            t.align = align;
289        }
290        self
291    }
292
293    /// Asocia el `EventId` del handler de click (lo asigna el `Ui` del SDK).
294    pub fn on_click(mut self, id: EventId) -> Self {
295        self.on_click = Some(id);
296        self
297    }
298
299    /// Marca el nodo como campo editable.
300    pub fn with_input(mut self, input: WireInput) -> Self {
301        self.input = Some(input);
302        self
303    }
304
305    /// `EventId` del handler que recibe el texto tecleado.
306    pub fn on_input(mut self, id: EventId) -> Self {
307        self.on_input = Some(id);
308        self
309    }
310
311    /// Marca el nodo como checkbox con el estado dado.
312    pub fn with_toggle(mut self, checked: bool) -> Self {
313        self.toggle = Some(checked);
314        self
315    }
316
317    /// `EventId` del handler que recibe el nuevo estado del checkbox.
318    pub fn on_toggle(mut self, id: EventId) -> Self {
319        self.on_toggle = Some(id);
320        self
321    }
322
323    /// Marca el nodo como slider.
324    pub fn with_slider(mut self, slider: WireSlider) -> Self {
325        self.slider = Some(slider);
326        self
327    }
328
329    /// `EventId` del handler que recibe el valor nuevo del slider.
330    pub fn on_value(mut self, id: EventId) -> Self {
331        self.on_value = Some(id);
332        self
333    }
334
335    /// Marca el nodo como dropdown.
336    pub fn with_select(mut self, select: WireSelect) -> Self {
337        self.select = Some(select);
338        self
339    }
340
341    /// `EventId` del handler que recibe el índice elegido en el dropdown.
342    pub fn on_select(mut self, id: EventId) -> Self {
343        self.on_select = Some(id);
344        self
345    }
346
347    /// Marca el nodo como grupo de radio.
348    pub fn with_radio(mut self, radio: WireRadio) -> Self {
349        self.radio = Some(radio);
350        self
351    }
352
353    /// `EventId` del handler que recibe el índice elegido en el grupo de radio.
354    pub fn on_radio(mut self, id: EventId) -> Self {
355        self.on_radio = Some(id);
356        self
357    }
358
359    pub fn children(mut self, children: Vec<WireNode>) -> Self {
360        self.children = children;
361        self
362    }
363
364    pub fn child(mut self, child: WireNode) -> Self {
365        self.children.push(child);
366        self
367    }
368}
369
370/// Columna con hijos.
371pub fn col(children: Vec<WireNode>) -> WireNode {
372    WireNode::new().dir(Dir::Column).children(children)
373}
374
375/// Fila con hijos.
376pub fn row(children: Vec<WireNode>) -> WireNode {
377    WireNode::new().dir(Dir::Row).children(children)
378}
379
380/// Nodo de texto.
381pub fn text(content: impl Into<String>, size: f32, color: Rgba) -> WireNode {
382    WireNode::new().label(content, size, color)
383}
384
385/// Caja vacía (contenedor o separador). Usar `.grow(1.0)` para spacer flexible.
386pub fn leaf() -> WireNode {
387    WireNode::new()
388}
389
390/// Separador flexible (`flex-grow: 1`).
391pub fn spacer() -> WireNode {
392    WireNode::new().grow(1.0)
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use alloc::vec;
399
400    #[test]
401    fn round_trip_postcard() {
402        let tree = col(vec![
403            text("0", 160.0, [230, 240, 250, 255]).grow(1.0),
404            WireNode::new()
405                .with_input(WireInput {
406                    value: "hola".into(),
407                    placeholder: "nombre…".into(),
408                    password: false,
409                    multiline: false,
410                })
411                .on_input(7),
412            row(vec![
413                text("+1", 28.0, [10, 30, 20, 255])
414                    .fill([60, 200, 130, 255])
415                    .radius(12.0)
416                    .on_click(1),
417                text("reset", 22.0, [30, 10, 10, 255])
418                    .fill([220, 80, 80, 255])
419                    .radius(12.0)
420                    .on_click(2),
421            ])
422            .gap(16.0)
423            .justify(Justify::Center),
424        ])
425        .pad(32.0)
426        .fill([20, 24, 32, 255]);
427
428        let bytes = postcard::to_allocvec(&tree).unwrap();
429        let back: WireNode = postcard::from_bytes(&bytes).unwrap();
430        assert_eq!(tree, back);
431        assert_eq!(back.children[1].on_input, Some(7));
432        assert_eq!(back.children[2].children[0].on_click, Some(1));
433
434        // EventPayload también round-trippea.
435        let p = EventPayload::Text("hola".into());
436        assert_eq!(
437            postcard::from_bytes::<EventPayload>(&postcard::to_allocvec(&p).unwrap()).unwrap(),
438            p
439        );
440    }
441}