tawasuyu

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Definitions

⎇ main llimphi-wasm-app-sdk/src/lib.rs Rust · 313 lines · 11.2 KB History Who touched what raw

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-wasm-app-sdk — lado guest de las apps WASM Tier 3.
2//!
3//! Una app implementa [`WasmApp`]: un `Model` propio, una transición
4//! `update(&mut self, Msg)` y una vista `view(&self, &mut Ui) -> WireNode`. La
5//! UI se arma con los constructores libres de [`llimphi_wire_view`]
6//! (`col`/`row`/`text`) para lo no-interactivo y con el [`Ui`] para lo
7//! interactivo (`button`/`text_input`/`checkbox`).
8//!
9//! **Eventos con payload.** El `Ui` asigna un [`EventId`] a cada control y guarda
10//! su handler. El nodo lleva sólo el id; el host, al disparar, manda
11//! `dispatch(event_id, payload)`. El guest mira su tabla y reconstruye el `Msg`:
12//! un botón rinde su `Msg` fijo, un input rinde `f(texto_tecleado)`, un checkbox
13//! `f(nuevo_estado)`. Por eso **`Msg` ya no cruza la frontera** — sólo el id y el
14//! [`EventPayload`] —, así que `Msg` se libera de (de)serializar: basta `Clone`.
15//!
16//! [`export_wasm_app!`] genera el ABI: `wasm_init`, `wasm_view() -> u64`
17//! (ptr<<32|len del `WireNode` postcard), `wasm_dispatch(event_id, ptr, len)`,
18//! `wasm_alloc`, `wasm_free`. El `Model` y la tabla de handlers viven en un
19//! global y persisten entre frames porque el host mantiene viva la instancia.
20
21pub use llimphi_wire_view as wire;
22pub use llimphi_wire_view::{
23    col, leaf, row, spacer, text, Align, Dim, Dir, EventId, EventPayload, Justify, Rgba, TextAlign,
24    WireInput, WireNode, WireRadio, WireSelect, WireSlider, WireText,
25};
26
27/// Handler de un evento, del lado guest. El `Ui` los guarda; el dispatch los
28/// resuelve a un `Msg`.
29pub enum Handler<Msg> {
30    /// Click/tap → un `Msg` fijo.
31    Unit(Msg),
32    /// Texto tecleado → `Msg` construido con el texto nuevo.
33    Text(Box<dyn Fn(String) -> Msg>),
34    /// Toggle → `Msg` construido con el nuevo estado.
35    Toggle(Box<dyn Fn(bool) -> Msg>),
36    /// Slider → `Msg` construido con el valor nuevo.
37    Value(Box<dyn Fn(f32) -> Msg>),
38    /// Dropdown → `Msg` construido con el índice elegido.
39    Select(Box<dyn Fn(u32) -> Msg>),
40}
41
42/// Contexto de construcción de UI: asigna [`EventId`]s y acumula los handlers
43/// del frame. Se reconstruye en cada `view` y se entrega por `&mut`.
44pub struct Ui<Msg> {
45    handlers: Vec<Handler<Msg>>,
46}
47
48impl<Msg> Default for Ui<Msg> {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl<Msg> Ui<Msg> {
55    pub fn new() -> Self {
56        Self {
57            handlers: Vec::new(),
58        }
59    }
60
61    fn register(&mut self, h: Handler<Msg>) -> EventId {
62        let id = self.handlers.len() as EventId;
63        self.handlers.push(h);
64        id
65    }
66
67    /// Botón: nodo con `label` y el `Msg` a emitir al click. Encadena estilo a
68    /// gusto (`.fill(..).radius(..).size(..)`).
69    pub fn button(&mut self, label: impl Into<String>, size: f32, color: Rgba, msg: Msg) -> WireNode {
70        let id = self.register(Handler::Unit(msg));
71        text(label, size, color).on_click(id)
72    }
73
74    /// Hace clickable cualquier nodo ya construido, emitiendo `msg`.
75    pub fn clickable(&mut self, node: WireNode, msg: Msg) -> WireNode {
76        let id = self.register(Handler::Unit(msg));
77        node.on_click(id)
78    }
79
80    /// Campo de texto editable. `on_input(texto)` construye el `Msg` con el texto
81    /// nuevo en cada cambio. El `value` es la fuente de verdad (Elm): pintalo
82    /// desde tu `Model`.
83    pub fn text_input(
84        &mut self,
85        value: impl Into<String>,
86        placeholder: impl Into<String>,
87        on_input: impl Fn(String) -> Msg + 'static,
88    ) -> WireNode {
89        let id = self.register(Handler::Text(Box::new(on_input)));
90        WireNode::new()
91            .with_input(WireInput {
92                value: value.into(),
93                placeholder: placeholder.into(),
94                password: false,
95                multiline: false,
96            })
97            .on_input(id)
98    }
99
100    /// Igual que [`Self::text_input`] pero oculta el contenido (contraseñas).
101    pub fn password_input(
102        &mut self,
103        value: impl Into<String>,
104        placeholder: impl Into<String>,
105        on_input: impl Fn(String) -> Msg + 'static,
106    ) -> WireNode {
107        let id = self.register(Handler::Text(Box::new(on_input)));
108        WireNode::new()
109            .with_input(WireInput {
110                value: value.into(),
111                placeholder: placeholder.into(),
112                password: true,
113                multiline: false,
114            })
115            .on_input(id)
116    }
117
118    /// Campo multilínea: Enter inserta `\n` (en `text_input` se ignora). Para
119    /// notas/descripciones. El guest sigue siendo la fuente de verdad del texto.
120    pub fn multiline_input(
121        &mut self,
122        value: impl Into<String>,
123        placeholder: impl Into<String>,
124        on_input: impl Fn(String) -> Msg + 'static,
125    ) -> WireNode {
126        let id = self.register(Handler::Text(Box::new(on_input)));
127        WireNode::new()
128            .with_input(WireInput {
129                value: value.into(),
130                placeholder: placeholder.into(),
131                password: false,
132                multiline: true,
133            })
134            .on_input(id)
135    }
136
137    /// Checkbox. `on_toggle(nuevo_estado)` construye el `Msg`.
138    pub fn checkbox(&mut self, checked: bool, on_toggle: impl Fn(bool) -> Msg + 'static) -> WireNode {
139        let id = self.register(Handler::Toggle(Box::new(on_toggle)));
140        WireNode::new().with_toggle(checked).on_toggle(id)
141    }
142
143    /// Slider con `value` en `[min, max]`. `on_value(v)` construye el `Msg` con
144    /// el valor nuevo al clickear/arrastrar la barra.
145    pub fn slider(
146        &mut self,
147        value: f32,
148        min: f32,
149        max: f32,
150        on_value: impl Fn(f32) -> Msg + 'static,
151    ) -> WireNode {
152        let id = self.register(Handler::Value(Box::new(on_value)));
153        WireNode::new()
154            .with_slider(WireSlider { value, min, max })
155            .on_value(id)
156    }
157
158    /// Dropdown con `options` y la `selected` actual. `on_select(idx)` construye
159    /// el `Msg` con el índice elegido.
160    pub fn select(
161        &mut self,
162        options: Vec<String>,
163        selected: u32,
164        on_select: impl Fn(u32) -> Msg + 'static,
165    ) -> WireNode {
166        let id = self.register(Handler::Select(Box::new(on_select)));
167        WireNode::new()
168            .with_select(WireSelect { options, selected })
169            .on_select(id)
170    }
171
172    /// Grupo de radio: `options` siempre visibles con la `selected` marcada.
173    /// `on_choose(idx)` construye el `Msg` con la opción elegida (mismo handler
174    /// que el dropdown; sólo cambia el render). Devuelve el nodo dentro de una
175    /// columna para apilar las opciones.
176    pub fn radio(
177        &mut self,
178        options: Vec<String>,
179        selected: u32,
180        on_choose: impl Fn(u32) -> Msg + 'static,
181    ) -> WireNode {
182        let id = self.register(Handler::Select(Box::new(on_choose)));
183        WireNode::new()
184            .with_radio(WireRadio { options, selected })
185            .on_radio(id)
186    }
187
188    /// Resuelve un evento a un `Msg`. Lo usa el runtime generado por la macro.
189    pub fn resolve(&self, id: EventId, payload: EventPayload) -> Option<Msg>
190    where
191        Msg: Clone,
192    {
193        match self.handlers.get(id as usize)? {
194            Handler::Unit(m) => Some(m.clone()),
195            Handler::Text(f) => match payload {
196                EventPayload::Text(s) => Some(f(s)),
197                _ => None,
198            },
199            Handler::Toggle(f) => match payload {
200                EventPayload::Toggle(b) => Some(f(b)),
201                _ => None,
202            },
203            Handler::Value(f) => match payload {
204                EventPayload::Value(v) => Some(f(v)),
205                _ => None,
206            },
207            Handler::Select(f) => match payload {
208                EventPayload::Select(i) => Some(f(i)),
209                _ => None,
210            },
211        }
212    }
213}
214
215/// Contrato de una app WASM Tier 3. El runtime lo maneja vía
216/// [`export_wasm_app!`].
217pub trait WasmApp {
218    /// Mensajes de la app. Sólo `Clone` — no cruzan la frontera (lo hace el
219    /// `EventPayload`), así que no necesitan (de)serializar.
220    type Msg: Clone;
221
222    /// Estado inicial.
223    fn init() -> Self;
224
225    /// Transición. Muta el `Model` en sitio.
226    fn update(&mut self, msg: Self::Msg);
227
228    /// Vista. Construí lo interactivo con `ui` (asigna los EventIds).
229    fn view(&self, ui: &mut Ui<Self::Msg>) -> WireNode;
230}
231
232/// Genera el ABI del guest a partir de un tipo que implementa [`WasmApp`].
233#[macro_export]
234macro_rules! export_wasm_app {
235    ($app:ty) => {
236        // Model + la tabla de handlers del último `view`. Persisten entre frames.
237        static mut __WASM_RT: ::core::option::Option<(
238            $app,
239            $crate::Ui<<$app as $crate::WasmApp>::Msg>,
240        )> = ::core::option::Option::None;
241        static mut __WASM_OUT: ::std::vec::Vec<u8> = ::std::vec::Vec::new();
242
243        #[no_mangle]
244        pub extern "C" fn wasm_init() {
245            let app = <$app as $crate::WasmApp>::init();
246            unsafe {
247                __WASM_RT = ::core::option::Option::Some((app, $crate::Ui::new()));
248            }
249        }
250
251        #[no_mangle]
252        pub extern "C" fn wasm_view() -> u64 {
253            let rt = unsafe { __WASM_RT.as_mut().expect("wasm_view antes de wasm_init") };
254            let mut ui = $crate::Ui::new();
255            let node = $crate::WasmApp::view(&rt.0, &mut ui);
256            rt.1 = ui; // la tabla de este frame, para el próximo dispatch
257            let bytes = $crate::postcard_to_vec(&node);
258            let ptr = bytes.as_ptr() as u64;
259            let len = bytes.len() as u64;
260            unsafe {
261                __WASM_OUT = bytes;
262            }
263            (ptr << 32) | len
264        }
265
266        #[no_mangle]
267        pub extern "C" fn wasm_free(_ptr: u32, _len: u32) {
268            unsafe {
269                __WASM_OUT = ::std::vec::Vec::new();
270            }
271        }
272
273        #[no_mangle]
274        pub extern "C" fn wasm_alloc(len: u32) -> u32 {
275            let mut buf = ::std::vec![0u8; len as usize];
276            let ptr = buf.as_mut_ptr() as u32;
277            ::std::mem::forget(buf);
278            ptr
279        }
280
281        #[no_mangle]
282        pub extern "C" fn wasm_dispatch(event_id: u32, ptr: u32, len: u32) {
283            let bytes = unsafe {
284                ::std::vec::Vec::from_raw_parts(ptr as *mut u8, len as usize, len as usize)
285            };
286            let payload = match $crate::postcard_from_slice::<$crate::EventPayload>(&bytes) {
287                ::core::result::Result::Ok(p) => p,
288                ::core::result::Result::Err(_) => return,
289            };
290            unsafe {
291                if let ::core::option::Option::Some((app, ui)) = __WASM_RT.as_mut() {
292                    if let ::core::option::Option::Some(msg) = ui.resolve(event_id, payload) {
293                        $crate::WasmApp::update(app, msg);
294                    }
295                }
296            }
297        }
298    };
299}
300
301/// Helper para que la macro no exija a cada guest declarar `postcard`.
302#[doc(hidden)]
303pub fn postcard_to_vec<T: serde::Serialize>(value: &T) -> std::vec::Vec<u8> {
304    postcard::to_allocvec(value).expect("postcard encode")
305}
306
307/// Helper de decodificación para la macro.
308#[doc(hidden)]
309pub fn postcard_from_slice<T: serde::de::DeserializeOwned>(
310    bytes: &[u8],
311) -> Result<T, postcard::Error> {
312    postcard::from_bytes(bytes)
313}