gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ 92dd0e5bfb2ca4456006da99437f4100e156d856 MANUAL.md Markdown · 1041 líneas · 48.0 KB Historia crudo

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

1# Manual de Llimphi
2
3> Motor gráfico soberano de gioser. `wgpu` + `vello` + `taffy` + `parley`,
4> bucle Elm `input → update → view → layout → raster → present`.
5> Reemplazo total de GPUI (extinto 2026-05-26): toda app gráfica de la suite
6> corre sobre Llimphi.
7
8Este documento es la **referencia de uso** orientada a humanos y a IA.
9Está organizado para salto directo: cada capa, widget y módulo trae su API
10real (firmas copiadas del código). Para el **porqué** arquitectónico ver
11[`SDD.md`](SDD.md); para la regla de concurrencia ver
12[`COMPUTO-FUERA-DEL-HILO-UI.md`](COMPUTO-FUERA-DEL-HILO-UI.md).
13
14---
15
16## Índice
17
181. [Modelo mental en 60 segundos](#1-modelo-mental-en-60-segundos)
192. [Arquitectura — las capas](#2-arquitectura--las-capas)
203. [Quickstart — la app mínima](#3-quickstart--la-app-mínima)
214. [El trait `App` (bucle Elm)](#4-el-trait-app-bucle-elm)
225. [`Handle` — efectos y concurrencia](#5-handle--efectos-y-concurrencia)
236. [`View<Msg>` — el DSL declarativo](#6-viewmsg--el-dsl-declarativo)
247. [Layout (`taffy` / `Style`)](#7-layout-taffy--style)
258. [Eventos e interacción](#8-eventos-e-interacción)
269. [Texto](#9-texto)
2710. [Canvas custom y GPU directo](#10-canvas-custom-y-gpu-directo)
2811. [Theme y paletas](#11-theme-y-paletas)
2912. [Capas base (hal · raster · text · motion · icons · surface)](#12-capas-base)
3013. [Catálogo de widgets](#13-catálogo-de-widgets)
3114. [Catálogo de módulos](#14-catálogo-de-módulos)
3215. [`llimphi-workspace` — chasis tipo tmux](#15-llimphi-workspace--chasis-tipo-tmux)
3316. [Reglas duras y gotchas](#16-reglas-duras-y-gotchas)
3417. [Comandos y demos](#17-comandos-y-demos)
3518. [Cheat-sheet](#18-cheat-sheet)
3619. [Índice de crates](#19-índice-de-crates)
37
38---
39
40## 1. Modelo mental en 60 segundos
41
42Llimphi es **Elm sobre la GPU**. Una app es un tipo que implementa el trait
43`App` con cuatro piezas:
44
45- `Model` — estado **inmutable** de la app.
46- `Msg` — todo lo que puede pasar (`Clone + Send`).
47- `update(model, msg, handle) -> model` — transición **pura** que devuelve un
48  modelo nuevo.
49- `view(&model) -> View<Msg>` — función **pura** que describe la pantalla como
50  un árbol de `View`.
51
52El runtime hace el bucle: un evento (click/tecla/rueda) produce un `Msg`,
53`update` deriva el nuevo `Model`, `view` reconstruye el árbol, `taffy` calcula
54las cajas, `vello` rasteriza, y se hace swap del frame. **No hay mutabilidad
55compartida, no hay vDOM ajeno, no hay callbacks imperativos**: declarás qué se
56ve y qué `Msg` emite cada nodo.
57
58```
59   evento ─▶ Msg ─▶ update(model,msg) ─▶ model' ─▶ view(model') ─▶ View<Msg>
60                                                                      │
61   present ◀─ raster(vello) ◀─ layout(taffy) ◀──────────────────────┘
62```
63
64Tres reglas de oro:
651. **`view` es pura** — no muta nada, sólo lee el modelo y arma el árbol.
662. **Cómputo pesado va a un worker** vía `Handle::spawn`, nunca síncrono en
67   `update`/`init`/handlers (congela la ventana → "Not Responding").
683. **Widgets son visuales y stateless**; el estado vive en tu `Model`.
69   **Módulos** sí encapsulan estado + comportamiento.
70
71---
72
73## 2. Arquitectura — las capas
74
75```
764. llimphi-ui ........... runtime winit del bucle Elm (App, Handle, run, KeyEvent)
77   └ llimphi-compositor . árbol View<Msg>, mount sobre taffy, paint, hit-test (winit-free)
783. llimphi-layout ....... motor de layout (taffy: flexbox + grid)
792. llimphi-raster ....... rasterizador vectorial (vello) + backend GPU directo
801. llimphi-text ......... shaping + fuentes (parley): bidi, ligaduras, CJK/emoji
810. llimphi-hal .......... abstracción de superficie (wgpu + winit / framebuffer)
82```
83
84El **split compositor/runtime** (2026-05-31) es importante: `llimphi-compositor`
85es *winit-free* (sólo `View`, `mount`, `paint`, hit-test). `llimphi-ui` lo corre
86sobre winit y **re-exporta todo el compositor**, así escribís `llimphi_ui::View`
87sin enterarte del split. Esto habilita un futuro runtime sobre el framebuffer
88del kernel `wawa` reusando el mismo compositor.
89
90Auxiliares: `llimphi-theme` (paletas), `llimphi-motion` (tweens),
91`llimphi-icons` (iconos vectoriales), `llimphi-surface` (texturas externas),
92`llimphi-workspace` (chasis tmux), `llimphi-gallery` (showcase).
93
94Catálogo: **~45 widgets** (visuales) + **10 módulos** (features con estado).
95
96---
97
98## 3. Quickstart — la app mínima
99
100```rust
101use llimphi_ui::llimphi_layout::taffy::prelude::*;
102use llimphi_ui::llimphi_raster::peniko::Color;
103use llimphi_ui::{App, Handle, View};
104
105#[derive(Clone)]
106enum Msg { Increment, Reset }
107
108struct Counter;
109
110impl App for Counter {
111    type Model = u32;
112    type Msg = Msg;
113
114    fn title() -> &'static str { "llimphi · counter" }
115
116    fn init(_: &Handle<Self::Msg>) -> Self::Model { 0 }
117
118    fn update(model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
119        match msg {
120            Msg::Increment => model.saturating_add(1),
121            Msg::Reset => 0,
122        }
123    }
124
125    fn view(model: &Self::Model) -> View<Self::Msg> {
126        let boton = View::new(Style {
127            size: Size { width: length(160.0), height: length(56.0) },
128            align_items: Some(AlignItems::Center),
129            justify_content: Some(JustifyContent::Center),
130            ..Default::default()
131        })
132        .fill(Color::from_rgba8(60, 200, 130, 255))
133        .radius(12.0)
134        .text("+1", 28.0, Color::from_rgba8(10, 30, 20, 255))
135        .on_click(Msg::Increment);
136
137        View::new(Style {
138            flex_direction: FlexDirection::Column,
139            size: Size { width: percent(1.0), height: percent(1.0) },
140            align_items: Some(AlignItems::Center),
141            justify_content: Some(JustifyContent::Center),
142            gap: Size { width: length(0.0), height: length(24.0) },
143            ..Default::default()
144        })
145        .fill(Color::from_rgba8(20, 24, 32, 255))
146        .children(vec![
147            View::new(Style::default()).text(model.to_string(), 160.0, Color::WHITE),
148            boton,
149        ])
150    }
151}
152
153fn main() { llimphi_ui::run::<Counter>(); }
154```
155
156`Cargo.toml`:
157```toml
158[dependencies]
159llimphi-ui    = { workspace = true }
160llimphi-theme = { workspace = true }
161# + los widgets/modules que uses:
162# llimphi-widget-button = { workspace = true }
163```
164
165Corre con `cargo run -p <tu-crate> --release`. El ejemplo vivo está en
166`llimphi-ui/examples/counter.rs`.
167
168---
169
170## 4. El trait `App` (bucle Elm)
171
172Definido en `llimphi-ui/src/lib.rs`. El estado es inmutable; cada evento
173produce un `Model` nuevo.
174
175```rust
176pub trait App: 'static {
177    type Model: 'static;
178    type Msg: Clone + Send + 'static;
179
180    fn init(handle: &Handle<Self::Msg>) -> Self::Model;
181    fn update(model: Self::Model, msg: Self::Msg, handle: &Handle<Self::Msg>) -> Self::Model;
182    fn view(model: &Self::Model) -> View<Self::Msg>;
183
184    // --- Todo lo de abajo tiene default; sobreescribí lo que necesites ---
185
186    fn on_key(_model: &Self::Model, _event: &KeyEvent) -> Option<Self::Msg> { None }
187
188    fn on_wheel(_model: &Self::Model, _delta: WheelDelta,
189                _cursor: (f32, f32), _modifiers: Modifiers) -> Option<Self::Msg> { None }
190
191    /// Capa de overlay (menús, modales, popovers). Si devuelve `Some`, se pinta
192    /// encima y clicks/hover van EXCLUSIVAMENTE a ella (el fondo queda "bajo
193    /// vidrio"). La transición la maneja tu Model.
194    fn view_overlay(_model: &Self::Model) -> Option<View<Self::Msg>> { None }
195
196    /// Drag&drop de archivos desde el file manager. Un evento por archivo.
197    fn on_file_drop(_model: &Self::Model, _path: std::path::PathBuf) -> Option<Self::Msg> { None }
198
199    /// El foco cambió (Tab/Shift+Tab o click sobre un nodo `focusable`). El
200    /// runtime administra el foco; guardás `id` en tu Model para pintar el ring
201    /// y rutear el teclado. Ver §8 (Foco y teclado).
202    fn on_focus(_model: &Self::Model, _id: Option<u64>) -> Option<Self::Msg> { None }
203
204    /// IME (composición de texto: CJK, acentos muertos, emoji). Opt-in vía
205    /// `ime_allowed()` para no robarle el texto a las apps que sólo leen
206    /// `on_key`. Flujo: Enabled → Preedit* → Commit/Disabled. Ver §8 (IME).
207    fn ime_allowed() -> bool { false }
208    fn on_ime(_model: &Self::Model, _event: &ImeEvent) -> Option<Self::Msg> { None }
209    /// Área del caret en px físicos para ubicar la ventana de candidatos.
210    fn ime_cursor_area(_model: &Self::Model) -> Option<(f32, f32, f32, f32)> { None }
211
212    fn title() -> &'static str { "llimphi" }
213    fn app_id() -> Option<&'static str> { None }   // app_id del xdg-toplevel en Wayland
214    fn initial_size() -> (u32, u32) { (960, 540) }
215}
216```
217
218Punto de entrada: `pub fn run<A: App>()` — corre hasta que el usuario cierre la
219ventana o la app llame `Handle::quit`.
220
221**Eventos de teclado** (`KeyEvent`):
222```rust
223pub struct KeyEvent {
224    pub key: Key,                 // re-export de winit; usar NamedKey para teclas especiales
225    pub state: KeyState,          // Pressed | Released
226    pub text: Option<String>,     // texto resultante con IME/modifiers; None para flechas etc.
227    pub modifiers: Modifiers,     // { shift, ctrl, alt, meta }
228    pub repeat: bool,
229}
230```
231`Key` y `NamedKey` se re-exportan desde `llimphi_ui`.
232
233**Rueda** (`WheelDelta { x, y }`): normalizado a "líneas". Convención CSS:
234`y` positivo = scroll hacia abajo.
235
236---
237
238## 5. `Handle` — efectos y concurrencia
239
240`Handle<Msg>` es `Send + Clone`. Llega a `init` y `update`. Es el único modo
241legítimo de producir efectos sin romper la pureza de la transición.
242
243```rust
244impl<Msg: Send + 'static> Handle<Msg> {
245    pub fn quit(&self);                 // cierra la ventana / termina el bucle
246    pub fn dispatch(&self, msg: Msg);   // encola un Msg para el próximo turno
247    pub fn spawn<F: FnOnce() -> Msg + Send + 'static>(&self, f: F);   // worker; su Msg reentra al update
248    pub fn spawn_periodic<F: Fn() -> Msg + Send + 'static>(&self, period: Duration, f: F);  // tick periódico
249    pub fn for_test() -> Self;          // handle "muerto" para tests sin event loop
250}
251```
252
253- **`spawn`** — trabajo bloqueante (IO, PAM, parse, efemérides). El `Msg` que
254  devuelve la closure se entrega al `update` en el hilo de UI. **Este es el
255  patrón obligatorio para todo cómputo pesado** (§16).
256- **`spawn_periodic`** — feeds a intervalos: ticks de simulación (~11 Hz en
257  dominium), polling, animaciones por reloj. El thread muere cuando se cierra
258  el event loop.
259
260---
261
262## 6. `View<Msg>` — el DSL declarativo
263
264Un `View` = `Style` de taffy + relleno + texto/imagen/painter + handlers +
265hijos. Todo se arma con builders encadenables (`self -> Self`). Definido en
266`llimphi-compositor/src/view.rs`.
267
268```rust
269View::new(style: Style) -> View<Msg>
270```
271
272### Apariencia
273| Método | Efecto |
274|---|---|
275| `.fill(Color)` | color de fondo |
276| `.hover_fill(Color)` | color al pasar el cursor (habilita hit-test de hover) |
277| `.radius(f64)` | esquinas redondeadas |
278| `.alpha(f32)` | opacidad de todo el subtree `[0,1]` (capa intermedia — no gratis) |
279| `.transform(Affine)` | afín 2D alrededor del centro del rect (estilo CSS `transform-origin:50% 50%`) |
280| `.clip(bool)` | recorta hijos al rect (paint + hit-test) |
281| `.image(Image)` | pinta `peniko::Image` centrada, preservando aspect ratio |
282| `.children(Vec<View<Msg>>)` | hijos |
283
284### Texto (ver §9)
285```rust
286.text(content, size_px, color)                            // centrado
287.text_aligned(content, size_px, color, Alignment)
288.text_aligned_italic(content, size_px, color, Alignment, italic)
289.text_aligned_full(content, size_px, color, Alignment, italic, font_family: Option<String>)
290.text_runs(content, size_px, default_color, runs: Vec<(usize,usize,Color)>, Alignment) // multicolor 1-pasada
291.line_height(mult)                                        // override interlínea (default 1.2)
292```
293
294### Interacción (ver §8)
295```rust
296.on_click(Msg)
297.on_click_at(|lx, ly, w, h| -> Option<Msg>)     // posición local + tamaño del rect
298.on_right_click(Msg) / .on_right_click_at(...)
299.on_middle_click(Msg)
300.on_pointer_enter(Msg) / .on_pointer_leave(Msg)
301.draggable(|phase: DragPhase, dx, dy| -> Option<Msg>)
302.draggable_at(|phase, dx, dy, lx0, ly0| -> Option<Msg>)   // + posición inicial del press
303.drag_payload(u64)                                        // payload que viaja con el drag
304.on_drop(|payload: u64| -> Option<Msg>)                   // este nodo es drop target
305.drop_hover_fill(Color)                                   // resaltado mientras un drag lo sobrevuela
306.on_scroll(|dx, dy| -> Option<Msg>)                       // rueda local (antes del on_wheel global)
307.focusable(u64)                                           // nodo enfocable por Tab/click (id opaco)
308```
309
310### Pintura custom (ver §10)
311```rust
312.paint_with(|scene: &mut vello::Scene, ts: &mut Typesetter, rect: PaintRect| { ... })
313.gpu_paint_with(|device, queue, encoder, view, rect: PaintRect, (vp_w, vp_h)| { ... })
314```
315
316Notas clave:
317- **Un nodo es draggable *o* clickable**, no ambos: `draggable` sobreescribe
318  `on_click`.
319- Las variantes `*_at` ganan sobre las simples si ambas están.
320- `PaintRect { x, y, w, h }` es el rect **absoluto** del nodo en píxeles físicos.
321- `DragPhase` = `Move` (un evento por `CursorMoved`, `dx/dy` = delta **desde el
322  evento anterior**, no acumulado) | `End` (al soltar).
323
324---
325
326## 7. Layout (`taffy` / `Style`)
327
328`Style` es el `taffy::Style` directo, re-exportado vía
329`llimphi_ui::llimphi_layout::taffy::prelude::*`. Es Flexbox + CSS Grid puro.
330
331Campos más usados:
332```rust
333Style {
334    flex_direction: FlexDirection::Row | Column,
335    size:    Size { width, height },          // length(px) | percent(0..1) | Dimension::auto()
336    min_size, max_size,
337    flex_grow: f32, flex_shrink: f32,
338    align_items:     Some(AlignItems::{Start,Center,End,Stretch}),
339    justify_content: Some(JustifyContent::{Start,Center,End,SpaceBetween,...}),
340    gap:     Size { width, height },
341    padding: Rect { left, right, top, bottom },   // con length(px)
342    margin:  Rect { ... },
343    ..Default::default()
344}
345```
346
347Helpers de `prelude`: `length(px)`, `percent(frac)`, `auto()`, `Dimension`,
348`Size`, `Rect`, `FlexDirection`, `AlignItems`, `JustifyContent`.
349
350`llimphi-layout` además expone:
351- `LayoutTree::new()` / `.clear()` (reuso entre frames), `.leaf(style)`,
352  `.node(style, &children)`, `.compute(...)`, `.compute_with_measure(F)`.
353- `Rect { x, y, w, h }` y `ComputedLayout { rects: HashMap<NodeId, Rect> }`.
354
355En el 99% de los casos no tocás `LayoutTree` a mano: lo maneja el runtime al
356montar tu `View`. Sólo armás `Style`s.
357
358---
359
360## 8. Eventos e interacción
361
362| Quiero… | Cómo |
363|---|---|
364| Botón / fila clickable | `.on_click(Msg)` (+ `.hover_fill` para feedback) |
365| Saber dónde se clickeó (canvas) | `.on_click_at(\|lx,ly,w,h\| ...)` → convertir a coords de mundo |
366| Menú contextual | `.on_right_click(Msg::OpenMenu{..})`, guardar pos en Model, abrir en `view_overlay` |
367| Abrir en pestaña nueva | `.on_middle_click(Msg)` |
368| Preview al pasar el mouse | `.on_pointer_enter(Msg)` / `.on_pointer_leave(Msg)` |
369| Resize de panel | `.draggable(\|phase,dx,dy\| ...)` acumulando delta en el Model |
370| Arrastrar entidad de un canvas | `.draggable_at(\|phase,dx,dy,lx0,ly0\| ...)` |
371| Drag&drop entre zonas | origen: `.drag_payload(id)`; destino: `.on_drop(\|id\| ...)` + `.drop_hover_fill` |
372| Scroll global | `App::on_wheel(model, delta, cursor, mods)` |
373| Área de scroll | widget `scroll_y(...)` (autocontenido) o `.on_scroll(\|dx,dy\| ...)` por nodo |
374| Teclado | `App::on_key(model, &KeyEvent) -> Option<Msg>` |
375| Foco / Tab | `.focusable(id)` en los nodos + `App::on_focus(model, id)` (ver abajo) |
376| IME (CJK, acentos) | `App::ime_allowed() -> true` + `App::on_ime(model, &ImeEvent)` (ver abajo) |
377| Drop de archivos del SO | `App::on_file_drop(model, path)` |
378
379**Patrón overlay** (menús/modales): el modelo guarda "menú abierto sí/no".
380Mientras esté abierto, `view_overlay` devuelve `Some(view)`; clicks fuera se
381cierran envolviendo los items en un scrim a pantalla completa con
382`on_click = DismissOverlay`. Cuando el modelo dice cerrado, `view_overlay`
383devuelve `None`.
384
385**Scroll** (widget `llimphi-widget-scroll`). `scroll_y(offset, content_len,
386viewport_len, content, on_scroll, &palette)` arma un viewport clipeado +
387contenido desplazado `-offset` + barra arrastrable. Es **stateless**: el offset
388vive en tu Model. `on_scroll(delta_px)` (rueda y arrastre) emite un delta a
389sumar; clampealo con `scroll::clamp_offset` en tu `update`. Helpers:
390`ensure_visible(offset, vp, item_top, item_h)` para llevar la selección a la
391vista (teclado); `approach(cur, target, factor)` para scroll suave/inercia
392(driveado por `Handle::spawn_periodic`).
393
394**Foco y teclado.** Marcá los nodos navegables con `.focusable(id)` (id `u64`
395que vos elegís). El runtime es la **única fuente de verdad** del foco: lo mueve
396con Tab/Shift+Tab en orden de árbol (envolviendo) y al clickear un nodo
397enfocable, y te avisa con `App::on_focus(model, Option<u64>)`. Guardás el id en
398tu Model para (a) pintar el ring (`if model.focus == Some(id) { .fill(accent) }`
399en `view`) y (b) rutear el teclado al campo activo desde `on_key`. No setees el
400foco por tu cuenta vía Msg: quedaría desincronizado del runtime.
401
402**IME** (composición de texto). Opt-in: `ime_allowed() -> true`. Con IME activo
403el texto compuesto **no** llega por `KeyEvent.text` sino por `on_ime`:
404`ImeEvent::Enabled` → uno o más `Preedit{text, cursor}` (texto en composición, a
405pintar subrayado en el caret) → `Commit(text)` (insertá como tecleado) o
406`Disabled`. Reportá el área del caret con `ime_cursor_area(model)` para ubicar
407la ventana de candidatos (CJK) junto al cursor.
408
409---
410
411## 9. Texto
412
413`TextSpec` (en compositor) describe el texto de un nodo:
414```rust
415pub struct TextSpec {
416    pub content: String,
417    pub size_px: f32,
418    pub color: Color,
419    pub alignment: Alignment,          // Start | Center | End | Justify
420    pub italic: bool,
421    pub font_family: Option<String>,   // string CSS con fallbacks
422    pub line_height: f32,              // múltiplo; default 1.2
423    pub runs: Option<Vec<(usize, usize, Color)>>,  // color por rango de BYTES
424}
425```
426
427- `Center` es el default (apto para labels). Para editores/párrafos usar
428  `.text_aligned(..., Alignment::Start)`.
429- **Multicolor en una sola pasada de shaping**: `.text_runs(...)` colorea
430  rangos de bytes — es la base del syntax highlighting (un nodo por línea, no
431  por token). Anclado arriba-izquierda; el caller dimensiona el rect.
432- El runtime mide el texto con parley durante el layout (`compute_with_measure`)
433  para que taffy reserve el alto real del texto envuelto a varias líneas
434  (evita "textos aplastados").
435- Shaping completo: bidi, ligaduras, kerning, fallback CJK/emoji vía fontique.
436
437---
438
439## 10. Canvas custom y GPU directo
440
441Dos hooks para pintar primitivas no expresables como composición de `View`s.
442Conviven en el mismo árbol; el runtime pinta **toda la pasada vello primero**,
443luego los `gpu_painter` en orden DFS.
444
445### `paint_with` — vía vello (el default)
446```rust
447.paint_with(|scene: &mut vello::Scene, ts: &mut Typesetter, rect: PaintRect| {
448    // dibujar BezPath, kurbo, texto con `ts`, etc. dentro de `rect`.
449    // NO dejar push_layer sin pop_layer; NO resetear la scene.
450})
451```
452Para: dominium-canvas, osciloscopios de pluma, charts de cosmos, pineal.
453Bueno hasta ~500 K primitivos por frame (rebuild) o ~2 M (Scene reusada).
454
455### `gpu_paint_with` — sube vertex buffers directo a wgpu, salta vello
456```rust
457.gpu_paint_with(|device, queue, encoder, view, rect: PaintRect, (vp_w, vp_h)| {
458    // abrir begin_render_pass con LoadOp::Load (NO clear) para preservar vello.
459    // (vp_w, vp_h) = tamaño en px de la TextureView destino, para calcular NDC.
460})
461```
462Para volumen masivo: starfield Gaia de cosmos, particles de tinkuy, viewport de
463nakui, pineal denso. Rango 100 K – 10 M+ primitivos. **No** soporta texto ni AA
464fino ni múltiples grosores de stroke por flush. Para texto encima de un render
465GPU, usar `view_overlay` (segunda Scene vello).
466
467### ¿Cuándo cada uno?
468| Pregunta | vello (`paint_with`) | GPU directo (`gpu_paint_with`) |
469|---|---|---|
470| Primitivos/frame | < ~500 K rebuild / < ~2 M Scene reusada | 100 K – 10 M+ |
471| ¿Cambian cada frame? | sí, rebuild barato | mejor estático (buffer persistente) |
472| Curvas Bezier | nativas | hay que teselar |
473| Texto | sí | no |
474| AA fino | sí (analítico) | no (sin MSAA) |
475
476**Default: `paint_with`** salvo que ya midas que el volumen lo justifica
477(factores ~11× a 1M en GPU mid sólo en el régimen persistente). El backend GPU
478expone `GpuPipelines`/`GpuBatch` en `llimphi-raster` (§12).
479
480---
481
482## 11. Theme y paletas
483
484`llimphi-theme::Theme` es un struct de slots semánticos de color. Cuatro presets
485`const`: `Theme::dark()` (default), `light()`, `aurora()`, `sunset()`.
486
487```rust
488pub struct Theme {
489    pub name: &'static str,
490    // fondos
491    pub bg_app, bg_panel, bg_panel_alt, bg_input, bg_input_focus,
492    pub bg_button, bg_button_hover, bg_selected, bg_row_hover: Color,
493    // texto
494    pub fg_text, fg_muted, fg_placeholder, fg_destructive: Color,
495    // bordes y acento
496    pub border, border_focus, accent: Color,
497}
498
499Theme::all() -> Vec<Theme>                 // orden de rotación canónico
500Theme::by_name(name) -> Option<Theme>
501Theme::next_after(current_name) -> Theme   // para el theme-switcher
502```
503
504Tokens auxiliares en el mismo crate:
505- `motion::{FAST=80ms, NORMAL=160ms, SLOW=320ms}` + `ease_out_cubic`,
506  `ease_in_out_cubic`, `linear`.
507- `alpha::{SCRIM, GLASS_PANEL, DISABLED, HINT}` (constantes `u8`).
508- `radius::{XS=2, SM=4, MD=8, LG=12, XL=20}` (`f64`).
509
510**Patrón de widgets**: cada widget define su `XxxPalette` con
511`Palette::from_theme(&theme)`. Tu app guarda un `Theme` en el Model, deriva las
512paletas que necesita en `view`, y se las pasa a los widgets. Para cambiar de
513tema, el `theme-switcher` emite `Msg(next_theme)` y reconstruís todo.
514
515---
516
517## 12. Capas base
518
519### `llimphi-hal` — superficie
520```rust
521Hal::new(compatible_surface: Option<&wgpu::Surface>) -> Result<Hal, HalError>   // async
522trait Surface { fn size(); fn resize(w,h); fn acquire() -> Result<Frame,_>; fn present(frame, hal); }
523WinitSurface::new(hal, window: Arc<Window>) -> Result<Self, HalError>
524Frame::view() -> &wgpu::TextureView;  Frame::size() -> (u32,u32)
525```
526`Hal::new` pide adapter `Backends::PRIMARY` (Vulkan) y cae a `all()` sólo si no
527hay — **no volver a `InstanceDescriptor::default()`**: el backend GL de Mesa
528sobre Wayland segfaultea en el teardown. El runtime de `llimphi-ui` ya maneja
529todo esto; sólo tocás HAL si escribís un runtime nuevo.
530
531### `llimphi-raster` — rasterización
532```rust
533Renderer::new(hal) -> Result<Renderer,_>
534Renderer::render(&mut self, hal, scene: &vello::Scene, frame: &Frame, base_color: Color)
535// GPU directo:
536GpuPipelines::new(device, color_format) -> Self   // campos: lines, tris, rects, bind_layout
537GpuBatch::new(&pipelines)
538  .line_width(w) .add_line(p0,p1,color) .add_polyline(&pts,color)
539  .add_tri(a,b,c, ca,cb,cc) .add_tri_list(&verts,color) .add_rect(x,y,w,h,color)
540  .primitive_count() -> u32
541  .flush(device, queue, encoder, view, viewport, load_op)
542```
543Re-exporta `vello` y `peniko` (`Color`, `Image`, `Fill`, etc.).
544
545### `llimphi-text` — shaping
546```rust
547Typesetter::new()                          // una por proceso (FontContext es caro)
548  .layout(text, size_px, max_width, alignment, line_height, italic, font_family) -> Layout<()>
549  .layout_runs(text, size_px, default_color, &runs, alignment, line_height) -> Layout<RunBrush>
550TextBlock::simple(text, size_px, color, origin)
551layout_block(ts, &block) / measure(ts, &block) -> Measurement
552draw_layout(scene, &layout, color, origin) / draw_layout_runs(scene, &layout, origin)
553Alignment::{Start, Center, End, Justify}
554```
555
556### `llimphi-motion` — tweens
557```rust
558trait Lerp { fn lerp(self, other, t: f32) -> Self; }   // impl para f32,f64,(f32,f32),(f64,f64),Color
559Tween::new(from, to, duration, easing: fn(f32)->f32)   // o Tween::idle(value)
560tween.value() / .progress() / .done()
561animate(handle, duration, make_msg)                    // arranca los ticks del tween
562```
563Patrón: guardás `Tween<T>` en el Model, `animate(...)` en el update, la `view`
564lee `tween.value()` cada repaint. El tween se auto-termina.
565
566### `llimphi-icons` — iconos vectoriales (~23, grid 24×24)
567```rust
568Icon::{File, Folder, Save, Plus, Minus, X, Check, Edit, Trash, ChevronUp/Down/Left/Right,
569       Home, Search, Info, Warning, Error, Bell, Settings, More, ...}
570icon_view(Icon, color, stroke_width) -> View<Msg>
571paint_icon(scene, rect, icon, color, stroke_width)     // dentro de un paint_with
572```
573`stroke_width` en unidades del grid 24×24 (1.6 es armónico).
574
575### `llimphi-surface` — texturas externas
576```rust
577ExternalSurface::new(device, queue)        // barato de clonar (Arc<Mutex> interno)
578  .upload(&rgba, w, h)                      // desde otro hilo/decoder/cámara
579  .view(style) -> View<Msg>                 // blittea a su rect en el árbol Elm
580  .blit(queue, encoder, dst_view, rect, viewport)   // o manual desde gpu_paint_with
581```
582
583---
584
585## 13. Catálogo de widgets
586
587Los widgets son **funciones puras** que devuelven `View<Msg>` (o specs que se
588convierten a `View`). Son **stateless**: el estado vive en tu Model. Convención:
589cada uno trae `XxxPalette::from_theme(&Theme)`. Crates en
590`widgets/<nombre>/`, dep `llimphi-widget-<nombre>`.
591
592### Controles
593
594**button** — `button_view(label, &ButtonPalette, on_click: Msg) -> View`;
595`button_styled(label, style, alignment, &palette, on_click)`.
596
597**field** — wrapper de formulario (label + helper/error + requerido).
598`field_view(FieldSpec { label, control: View<Msg>, required, helper, error, palette })`.
599
600**text-input** — input single-line **con estado** `TextInputState`
601(`new()`/`masked()`, `text()`, `set_text()`, `apply_key(&KeyEvent) -> bool`,
602soporta undo/redo + selección con Shift). Render:
603`text_input_view(&state, placeholder, focused, &palette, on_focus: Msg)`.
604
605**text-area** — multilínea con estado `TextAreaState` (Enter = newline, sin
606auto-submit). `text_area_view(&state, placeholder, focused, body_height, &palette, on_focus)`.
607
608**slider** — sin estado. `slider_view(label, value, min, max, &palette,
609on_change: Fn(DragPhase, delta_value) -> Option<Msg>)`. El delta viene en
610unidades, no píxeles.
611
612**switch** — `switch_view(progress: f32 [0..1], on_toggle: Msg, &palette)`. La
613app guarda el `bool` y opcionalmente anima `progress` con un `Tween`.
614
615**segmented** — N opciones exclusivas. `segmented_view(&[&str], selected: usize,
616make_msg: Fn(usize)->Msg, &palette)`.
617
618**progress** — `linear_progress_view(progress, track, fill, height)` y
619`radial_progress_view(progress, track, fill, stroke_ratio)`. Sin eventos.
620
621**spinner** — `spinner_view(color, stroke_ratio, speed_rev_per_sec)`. Animado por
622reloj absoluto; requiere redraws periódicos (`spawn_periodic`).
623
624**badge** — `count_badge_view(count, BadgeKind)` ("99+" si ≥100) y
625`dot_badge_view(BadgeKind)`. `BadgeKind::{Info,Success,Warning,Error,Neutral}`.
626
627**avatar** — `avatar_view(name, size_px)`: círculo determinista (color por hash
628del nombre + inicial).
629
630**tooltip** — render puro. `tooltip_view(TooltipSpec { anchor, viewport, side:
631Side, text, palette })`. Se monta en `view_overlay`; la app controla visibilidad
632con `on_pointer_enter/leave`.
633
634**empty** — empty-state. `empty_view(Icon, title, description: Option<&str>, &palette)`.
635
636**skeleton** — placeholder con shimmer. `skeleton_view`, `skeleton_box_view(w,h,..)`,
637`skeleton_line_view(w,..)`. Requiere redraws periódicos.
638
639**banner** — tira de status. `banner_view(BannerKind::{Info,Success,Warning,Error}, message)`.
640
641### Contenedores y layout
642
643**panel** — chrome (gradiente + hairline accent). `panel_view(children, PanelStyle)`;
644`PanelStyle::{from_theme, from_theme_large, neutral}`. `panel_signature_painter(style)`
645para reusar el look en un `paint_with`.
646
647**card** — `card_view(children, CardOptions { accent, padding, gap, radius, signature }, &CardPalette)`.
648
649**stat-card** — métrica de dashboard. `stat_card_view(label, value, description,
650accent, &recent_items, &palette)`.
651
652**tabs** — `tabs_view(TabsSpec { labels, active: usize, on_select: Fn(usize)->Msg,
653content: View<Msg>, tab_height, palette, tab_width })`. Selección la maneja la app.
654
655**splitter** — divisor draggable de 2 panes. `splitter_two(Direction::{Row,Column},
656a, a_size, b, b_size, on_resize: Fn(DragPhase, delta)->Option<Msg>, &palette)`.
657`PaneSize::{Fixed(px), Flex}`. La app acumula el delta en su Model.
658
659**scroll** — área de scroll vertical con barra arrastrable. `scroll_y(offset,
660content_len, viewport_len, content, on_scroll: Fn(delta_px)->Msg, &palette)`.
661Stateless (offset en el Model); rueda autocontenida. Helpers: `clamp_offset`,
662`ensure_visible` (selección a la vista), `approach` (scroll suave). Ver §8.
663
664**tiled** — grilla auto cols×rows de tiles con title bar. `tiled_view(tiles, &palette)`,
665`tiled_view_cols(tiles, cols, &palette)`, y variantes `*_reorderable*` con
666`on_reorder: Fn(from, to)->Option<Msg>` (drag-to-swap por la title bar). `TileSpec { label, content }`.
667
668**panes** — árbol binario BSP tipo tmux. La app guarda un `Layout`:
669```rust
670Layout::single(id) / Layout::Split { axis: Axis, ratio, first, second }
671layout.split(target, new, axis) / .without(target) / .resize(&path, delta) / .leaves()
672panes_view(&layout, focused: PaneId, leaf: FnMut(PaneId)->View, on_resize: Fn(Vec<Side>,DragPhase,delta)->Option<Msg>,
673           on_focus: Fn(PaneId)->Msg, &palette)
674```
675
676**grid** — grilla 2D virtualizada. `ventana_visible(total, vp_w, vp_h, scroll_fila,
677&metrics) -> VisibleWindow` para virtualizar, luego `grid_view(GridSpec { cells:
678Vec<GridCell { content, label, selected, on_click }>, cols, metrics, caption, ... })`.
679
680**list** — lista vertical virtualizada. `list_view(ListSpec { rows: Vec<ListRow {
681label, selected, on_click }>, total, caption, truncated_hint, row_height, palette })`.
682La app prefiltra las filas visibles.
683
684**tree** — árbol expand/collapse. `tree_view(TreeSpec { rows: Vec<TreeRow { label,
685depth, has_children, expanded, selected, on_toggle, on_select }>, row_height,
686indent_px, palette })`. La app aplana el árbol según nodos expandidos.
687
688**navigator** — navegador data-agnóstico de nodos en dos modos conmutables
689(**árbol** ↔ **grafo**, reusa tree + nodegraph). Render-only: la app guarda
690`expanded`/`selected`/`mode`. Pasa un bosque de `NavNode { id: u64, label,
691kind: NavKind (Monad|Group|Dir|File|Other), children }` y callbacks por id.
692```rust
693navigator_view(NavSpec { roots, mode: NavMode::{Tree,Graph}, selected, palette, guides },
694    is_expanded: Fn(u64)->bool, on_toggle: Fn(u64)->Msg,
695    on_select: Fn(u64)->Msg, on_context: Option<Fn(u64)->Msg>)
696// árbol: click selecciona, chevron expande, icono por kind. grafo: cables de
697// contención padre→hijo, arrastrar selecciona, right-click abre. Pensado para
698// el sidebar de Mónadas/archivos de pata, pero no sabe de nouser.
699```
700
701**app-header** — `app_header(label, actions: Vec<View<Msg>>, &palette)`.
702
703**status-bar** — `status_bar_view(left, center, right, &palette)` con
704`StatusSegment::text(..).with_icon(Icon).clickable(Msg).emphasized()`.
705
706**breadcrumb** — `breadcrumb_view(&[&str], make_msg: Fn(usize)->Msg, &palette)`
707(el último segmento no es clickable).
708
709**modal** — diálogo centrado con scrim. `modal_view(ModalSpec { title, body:
710View<Msg>, buttons: Vec<ModalButton>, size, viewport, on_dismiss, palette })`.
711`ModalButton::{primary, cancel, destructive}(label, msg)`. Se monta en `view_overlay`.
712
713**toast** — notificaciones efímeras bottom-right. La app guarda `Vec<Toast>`
714(`Toast::{info,success,warning,error}(id, text, duration)`), filtra
715`is_alive(now)`, y `toast_stack_view(&toasts, viewport, make_dismiss: Fn(u64)->Msg)`.
716
717**splash** — splash de arranque (cuatro cuadrantes andinos). `splash_view(started_at:
718Instant, bg, fg_text)`; basado en tiempo, requiere redraws.
719
720### Ricos / interactivos
721
722**nodegraph** — lienzo de nodos + cables Bezier. Sin estado (la app guarda
723posiciones y `Wire`s).
724```rust
725NodeSpec { id: NodeId(u32), label, x, y, inputs: Vec<String>, outputs: Vec<String> }
726Wire { from_node, from_output: PinIdx(u16), to_node, to_input }
727nodegraph_view(&nodes, &wires, &palette, &metrics,
728    on_drag_node: Fn(NodeId, DragPhase, dx, dy)->Option<Msg>,
729    on_connect:   Fn(NodeId, PinIdx, NodeId, PinIdx)->Option<Msg>)
730// + nodegraph_view_ex (right-click) y nodegraph_view_styled (tints por nodo/cable)
731```
732
733**timeline** — scrub clickeable. `timeline_view(progress: f32, &palette,
734on_seek: Fn(f32 [0..1])->Option<Msg>)`.
735
736**text-editor** — editor IDE (capa visual sobre el core agnóstico). La app guarda
737`EditorState`:
738```rust
739EditorState::new(); .text(); .set_text(s); .has_selection(); .can_undo()/.can_redo();
740.add_cursor_at(line,col);  .apply_key_with_clipboard(&KeyEvent, &mut dyn Clipboard) -> ApplyResult;
741.ensure_caret_visible(visible_lines)
742// nota: `metrics` se pasa POR VALOR; el callback es on_pointer: Fn(PointerEvent)->Option<Msg>
743text_editor_view(&state, &EditorPalette, metrics: EditorMetrics, visible_lines: usize, on_pointer)
744text_editor_view_highlighted(&state, &palette, metrics, visible_lines, language: Language, on_pointer)
745text_editor_view_full(&state, &palette, metrics, visible_lines, language, match_ranges: &[(usize,usize)], on_pointer)
746syntax_palette_dark(&theme) -> SyntaxPalette   // en lib.rs del widget
747```
748
749**text-editor-core** — núcleo **agnóstico** (sin GPU, sin Llimphi; sólo
750`peniko::Color`). Reutilizable en TUI/web/headless. Tipos clave:
751- `Buffer` (sobre `ropey`): `from_str`, `text`, `insert(offset,s)`, `delete(s,e)`,
752  `offset_to_pos`, `pos_to_offset`, `slice`, `line(n)`.
753- `Pos { line, col }`, `Selection { anchor, caret }`, `Cursor { caret, anchor:
754  Option, desired_col }` con `move_left/right/up/down/word_left/...`,
755  `selection_range(&buf)`, `collapse`.
756- Ops: `replace_selection`, `delete_backward/forward`, `indent_or_insert_tab`,
757  `insert_newline_auto_indent` → devuelven `EditDelta { start, removed, inserted,
758  cursor_before, cursor_after }` con `.apply()/.undo()`.
759- `UndoStack`: `push(delta)`, `undo/redo(&mut buf, &mut cursor) -> bool`, `can_undo/redo`.
760- `FindState { query, case_sensitive }`: `all_matches`, `find_next`, `find_prev`.
761- Matching de brackets: `find_bracket_pair(&buf, &cursor) -> Option<(Pos, Pos)>`, `Direction`.
762- `Clipboard` (trait `get/set`), `MemClipboard`, `NullClipboard`.
763- `Diagnostic { range: DiagnosticRange { start: Pos, end: Pos }, severity: Severity,
764  message: String, source: Option<String> }` (+ ctors `error(..)`, `warning(..)`);
765  `Severity::{Error, Warning, Information, Hint}`.
766- Highlight tree-sitter: `Language::{Plain, Rust, Python, Wat}`
767  (+ `Language::from_cell_language(s)`); `Highlighter::new(lang)` con
768  `.highlight(&mut self, source: &str) -> Vec<Vec<Span>>` (un `Vec<Span>` **por
769  línea**), `.set_language(lang)`, `.language()`; helpers de módulo
770  `invalidate_tree_cache(lang)` y `apply_pending_edits(lang, &edits)` para el
771  caché incremental. `TokenKind`, `Span`, `SyntaxPalette::color(kind)`.
772
773**text-editor-lsp** — cliente LSP por stdin/stdout. `trait LspClient` (fire-and-forget
774`request_*` + lecturas de caché `latest_*`/`clear_*`): completions, hover,
775definition, references, rename, formatting, signature help, document symbols.
776`RustAnalyzerClient::start(workspace_root)`; `NoopLspClient` para tests.
777
778**clipboard** — portapapeles del sistema vía `arboard`. `SystemClipboard::new()`,
779`is_available()`, impl `Clipboard`. No-op silencioso si no hay display (CI headless).
780
781**menubar** — barra de menú mac-style. `menubar_view(&MenuBarSpec { menu: &AppMenu,
782open: Option<usize>, theme, viewport, height, on_open: Fn(Option<usize>)->Msg,
783on_command: Fn(&str)->Msg })`; dropdown en `view_overlay` con `menubar_overlay(spec)`
784o `menubar_overlay_animated(spec, active, appear)`. Navegación por teclado:
785`menubar_nav`, `menubar_command_at`.
786
787**edit-menu** — menú estándar de edición sobre un editor.
788`EditFlags::from_editor(&state, masked)`, `edit_context_menu(anchor, viewport,
789&theme, flags, on_action: Fn(EditAction)->Msg, on_dismiss)` →
790`ContextMenuSpec`. `apply(&mut state, EditAction, &mut clipboard) -> ApplyResult`.
791`EditAction::{Undo,Redo,Cut,Copy,Paste,Delete,SelectAll}`.
792
793**context-menu** — menú contextual genérico (look "webpage"). `ContextMenuItem::
794action(label).with_shortcut(..).icon(..).disabled().destructive().submenu(children)`
795o `::separator()`. `context_menu_view(ContextMenuSpec { anchor, viewport, header,
796items, active, on_pick: Fn(usize)->Msg, on_dismiss, palette })`; `context_menu_view_ex`
797con submenús/animación. Se monta en `view_overlay` con scrim.
798
799**theme-switcher** — `theme_switcher_view(&current: &Theme, on_change: Fn(Theme)->Msg)`
800(+ `_styled`/`_flex`). Cicla `Theme::next_after`.
801
802**shortcuts-help** — overlay "?" con atajos agrupados. `shortcuts_help_view(
803ShortcutsHelpSpec { title, groups: Vec<ShortcutGroup { title, entries:
804Vec<ShortcutEntry { keys, description }> }>, viewport, on_dismiss, palette })`.
805
806**wawa-mark** — sello vectorial del SO wawa. `wawa_mark_view(&WawaMarkPalette)`;
807`paint_mark(scene, rect, &palette)` para canvas custom. Usar en contenedor cuadrado.
808
809---
810
811## 14. Catálogo de módulos
812
813Los módulos encapsulan **estado + comportamiento** (a diferencia de los widgets).
814Todos siguen el mismo contrato:
815
816```
817State  +  Msg  +  Action  +  apply(state, msg, ...) -> Action
818                            +  on_key(state, &KeyEvent) -> Option<Msg>
819                            +  open_shortcut(&KeyEvent) -> bool
820                            +  view(state, ..., to_host: F) -> View<HostMsg>
821                            +  Palette
822```
823
824La app guarda `Option<ModuleState>` (o el state directo, p. ej. bookmarks),
825rutea el atajo de apertura con `open_shortcut`, rutea teclas con `on_key`, aplica
826`Msg`s con `apply`, y monta el `view` pasando un mapeo `to_host: Fn(ModuleMsg) ->
827HostMsg`. Cuando `apply` devuelve una `Action` (p. ej. `Invoke(id)`, `OpenAt{..}`,
828`GoTo{..}`), la app ejecuta el efecto. Crates en `modules/<nombre>/`.
829
830| Módulo | Atajo | Acción que devuelve | Propósito |
831|---|---|---|---|
832| **command-palette** | `Ctrl+Shift+P` | `Invoke(String)` | paleta de comandos fuzzy. El host declara `&[Command]` |
833| **file-picker** | `Ctrl+P` | `Open(PathBuf)` | fuzzy file picker; host pasa `&[PathBuf]` + `root` |
834| **fif** (find-in-files) | `Ctrl+Shift+F` | `OpenAt{path,line,col}`, `Searched{..}`, `Replaced{..}` | buscar/reemplazar; dual-view (dialog + barra). `search()` / `replace_all()` hacen el I/O |
835| **diff-viewer** | `Ctrl+Shift+D` | — | diff side-by-side. `DiffState::new(before_label, after_label, before, after)` computa con `similar` |
836| **mini-map** | `Ctrl+Shift+M` | `JumpTo(line)` | minimapa del buffer; agnóstico del editor (recibe `Snapshot`) |
837| **bookmarks** | `Ctrl+Alt+B` toggle, `Ctrl+Shift+B` lista, `Ctrl+Alt+N/P` nav | `JumpTo{path,line}` | marcadores per-file persistentes (state directo, no Option) |
838| **symbol-outline** | `Ctrl+Shift+O` | `GoTo{line,col}` | outline de símbolos; host arma `Vec<SymbolItem>` (LSP/tree-sitter/custom) |
839| **selector** | — | — | abstracción portátil abrir/guardar: `trait Selector` (`HostSelector` con PathBuf, `WawaSelector` placeholder content-addressed) |
840| **plugin-host** | — | `OpenAt{..}`, `SetStatus(..)` | runtime WASM (wasmi) con permisos por bitfield; `PluginHost::load_from_dir`/`invoke(id, cap, args)` |
841| **shuma-term** | `` Ctrl+` `` | `SetStatus(..)` | terminal integrada. `spawn(cwd)` lanza PTY (`shuma_exec`), `vt100::Parser` renderiza; `Tick` drena el PTY |
842
843Patrón típico de integración (command-palette):
844```rust
845struct Model { palette: Option<PaletteState>, commands: Vec<Command>, /* … */ }
846enum Msg { Palette(PaletteMsg), /* … */ }
847
848// on_key:
849if command_palette::open_shortcut(ev) { return Some(Msg::Palette(PaletteMsg::Open)); }
850if let Some(_) = &model.palette { return command_palette::on_key(p, ev).map(Msg::Palette); }
851
852// update:
853Msg::Palette(m) => {
854    if let Some(state) = model.palette.as_mut() {
855        match command_palette::apply(state, m, &model.commands) {
856            PaletteAction::Invoke(id) => { /* ejecutar comando id */ model.palette = None; }
857            PaletteAction::Close => model.palette = None,
858            PaletteAction::None => {}
859        }
860    }
861}
862
863// view_overlay:
864model.palette.as_ref().map(|s|
865    command_palette::view(s, &model.commands, &palette, Msg::Palette))
866```
867
868---
869
870## 15. `llimphi-workspace` — chasis tipo tmux
871
872Monta cualquier componente en un layout intercambiable con splits resizables
873(máquina de estados de foco/split/cierre + chrome estándar). Construido sobre
874`llimphi-widget-panes`.
875
876```rust
877Workspace::new()
878  .focused() -> PaneId         .count()      .leaves() -> Vec<PaneId>     .layout() -> &Layout
879  .focus(id)  .split(Axis) -> PaneId   .close() -> Option<PaneId>   .resize(&path, delta)
880  .apply(WsMsg) -> WsEffect
881
882enum WsMsg { Focus(PaneId), Split(Axis), Close, Resize(Vec<Side>, f32) }
883enum WsEffect { None, Created(PaneId), Closed(PaneId) }
884
885workspace_view(&ws, &WorkspacePalette,
886    leaf: FnMut(PaneId)->View<Host>,           // materializa el contenido de cada panel
887    lift: Fn(WsMsg)->Host)                      // sube los Msg del chasis a tu Msg
888```
889
890Patrón: `enum Msg { Ws(WsMsg), Panel(PaneId, PanelMsg) }`. En `update`,
891`ws.apply(msg)` te avisa con `WsEffect::{Created,Closed}(id)` para que crees o
892destruyas el estado del panel correspondiente.
893
894---
895
896## 16. Reglas duras y gotchas
897
898### 🔴 Cómputo pesado fuera del hilo de UI (PRIORIDAD URGENTE)
899Ningún `update`/`init`/handler puede ejecutar trabajo **síncrono** pesado
900(efemérides, simulación, IO, parse, embeddings, layout de árboles grandes).
901Bloquea el hilo → "Not Responding". **`init` corre dentro de `resumed`, después
902de crear la ventana**, así que un cómputo pesado ahí ya congela una ventana
903visible.
904
905Patrón (referencia: `cosmos-app-llimphi`):
906```rust
907// Model: Option<Resultado> (None = "calculando…") + flag dirty + contador de generación.
908struct Model { x: Option<Resultado>, x_dirty: bool, x_gen: u64 }
909enum Msg { XComputed(u64, Arc<Resultado>) }
910
911// al FINAL de update() (que tiene el Handle):
912if m.x_dirty {
913    m.x_dirty = false;
914    m.x_gen = m.x_gen.wrapping_add(1);
915    let (gen, input) = (m.x_gen, m.input.clone());     // sólo lo que el worker necesita (Send)
916    handle.spawn(move || Msg::XComputed(gen, Arc::new(compute(&input))));
917}
918// al recibir: aplicar SÓLO si la generación sigue vigente (evita que un
919// resultado tardío pise a uno más nuevo en drags/toggles rápidos).
920Msg::XComputed(gen, x) => if gen == m.x_gen {
921    m.x = Some(Arc::try_unwrap(x).unwrap_or_else(|a| (*a).clone()));
922}
923```
924La **generación** es imprescindible si el recálculo se dispara seguido. Ver
925[`COMPUTO-FUERA-DEL-HILO-UI.md`](COMPUTO-FUERA-DEL-HILO-UI.md) y su checklist por app.
926
927### Otras
928- **Solvers iterativos** (Newton/bisección): cota dura `for _ in 0..N`, nunca
929  `loop {}` con corte pegado al epsilon de f64 — en debug no converge → loop
930  infinito.
931- **Backend GPU**: preferir Vulkan (`Backends::PRIMARY`); el GL de Mesa sobre
932  Wayland segfaultea en el teardown. Ya está hecho en `Hal::new`, no revertir.
933- **Un nodo es draggable o clickable**, no ambos.
934- **`alpha` y `clip`** crean capas intermedias: tienen costo, usar sólo cuando
935  hace falta.
936- **`paint_with`** no debe dejar `push_layer` sin `pop_layer` ni resetear la
937  Scene.
938- **Hit-test respeta `.transform()`**: un nodo rotado/escalado/trasladado recibe
939  los clicks donde se ve pintado (el runtime invierte el afín acumulado). Lo que
940  **no** se ajusta todavía: la posición local que reciben los handlers `*_at` se
941  reporta en coords de pantalla, no en el espacio local del nodo transformado.
942- **GPUI está extinto**: no agregar dependencias ni código GPUI (regla §3 de
943  `CLAUDE.md`).
944- **Texto en regla pesada**: crear un `Typesetter` por frame es caro
945  (`FontContext::new` enumera fuentes del sistema). El runtime ya cachea uno y lo
946  pasa a `paint_with`.
947
948---
949
950## 17. Comandos y demos
951
952```bash
953cargo check --workspace                              # smoke test mínimo (debe pasar siempre)
954cargo run -p <crate> --release                       # correr una app
955cargo run -p <crate> --example <demo> --release      # correr un demo
956
957# demos del propio framework:
958cargo run -p llimphi-ui      --example counter --release   # bucle Elm completo
959cargo run -p llimphi-ui      --example editor  --release   # text field + teclado
960cargo run -p llimphi-ui      --example gpu_paint_demo --release
961cargo run -p llimphi-gallery --release                     # showcase de TODO el kit
962cargo run -p nada            --release                     # editor real para ejercitar widgets
963
964# benchmark GPU directo vs vello:
965cargo run -p llimphi-gpu-bench --release
966```
967
968`llimphi-gallery` (`src/main.rs`, ~967 líneas) es la **referencia viva** del
969patrón completo: `Model`/`Msg`/`init`/`update`/`view`/`view_overlay` con overlays
970mutuamente excluyentes (modal > atajos > toasts > context-menu > dropdown).
971Controles: click en switches/segments; "Mostrar toast"/"Abrir modal"; `?` abre
972atajos; `Esc` cierra el overlay activo.
973
974---
975
976## 18. Cheat-sheet
977
978```rust
979// ── App mínima ──────────────────────────────────────────────
980impl App for X { type Model; type Msg; init; update; view; }
981llimphi_ui::run::<X>();
982
983// ── Nodo ────────────────────────────────────────────────────
984View::new(Style{ flex_direction, size, gap, padding, align_items, justify_content, ..default() })
985    .fill(c).hover_fill(c).radius(r).clip(b).alpha(a).transform(xf)
986    .text(s, px, c) | .text_aligned(s,px,c,al) | .text_runs(s,px,c,runs,al)
987    .image(img) | .paint_with(|scene,ts,rect|{}) | .gpu_paint_with(|d,q,enc,view,rect,vp|{})
988    .on_click(m) | .on_click_at(|lx,ly,w,h|) | .on_right_click(m) | .on_middle_click(m)
989    .on_pointer_enter(m) | .on_pointer_leave(m)
990    .draggable(|ph,dx,dy|) | .draggable_at(|ph,dx,dy,lx0,ly0|)
991    .drag_payload(id) | .on_drop(|id|) | .drop_hover_fill(c)
992    .children(vec![..])
993
994// ── Efectos ─────────────────────────────────────────────────
995handle.spawn(|| Msg::Done(compute()));          // worker → reentra al update
996handle.spawn_periodic(dur, || Msg::Tick);       // feed periódico
997handle.dispatch(Msg::X);  handle.quit();
998
999// ── Estilo de layout (taffy prelude) ────────────────────────
1000length(px)  percent(0..1)  Dimension::auto()
1001FlexDirection::{Row,Column}  AlignItems::{Start,Center,End,Stretch}
1002JustifyContent::{Start,Center,End,SpaceBetween}
1003
1004// ── Theme ───────────────────────────────────────────────────
1005Theme::dark()/light()/aurora()/sunset();  Theme::next_after(name);  XxxPalette::from_theme(&t)
1006
1007// ── Overlay (menús/modales) ─────────────────────────────────
1008fn view_overlay(m) -> Option<View<Msg>> { if m.open { Some(menu) } else { None } }
1009```
1010
1011---
1012
1013## 19. Índice de crates
1014
1015**Framework** (`02_ruway/llimphi/`):
1016`llimphi-hal` · `llimphi-raster` · `llimphi-text` · `llimphi-layout` ·
1017`llimphi-compositor` · `llimphi-ui` · `llimphi-theme` · `llimphi-motion` ·
1018`llimphi-icons` · `llimphi-surface` · `llimphi-workspace` · `llimphi-gallery` ·
1019`llimphi-gpu-bench`.
1020
1021**Widgets** (`widgets/`, dep `llimphi-widget-<n>`): app-header · avatar · badge ·
1022banner · breadcrumb · button · card · clipboard · context-menu · edit-menu ·
1023empty · field · gallery · grid · list · menubar · modal · navigator · nodegraph ·
1024panel · panes · progress · segmented · shortcuts-help · skeleton · slider · splash ·
1025splitter · stat-card · status-bar · switch · tabs · text-area · text-editor ·
1026text-editor-core · text-editor-lsp · text-input · theme-switcher · tiled ·
1027timeline · toast · tooltip · tree · wawa-mark.
1028
1029**Módulos** (`modules/`): bookmarks · command-palette · diff-viewer · fif ·
1030file-picker · mini-map · plugin-host · selector · shuma-term · symbol-outline.
1031
1032**Android** (`android/`): clear-screen-android · vello-hello-android ·
1033vello-text-android.
1034
1035---
1036
1037> Documentos hermanos: [`SDD.md`](SDD.md) (diseño y roadmap),
1038> [`COMPUTO-FUERA-DEL-HILO-UI.md`](COMPUTO-FUERA-DEL-HILO-UI.md) (regla de
1039> concurrencia), [`README.md`](README.md) / [`LEEME.md`](LEEME.md) (overview).
1040> Las firmas de este manual reflejan el código al 2026-06-01; ante divergencia,
1041> la fuente autoritativa es el `lib.rs` de cada crate.

MANUAL.md

Manual de Llimphi

Motor gráfico soberano de gioser. wgpu + vello + taffy + parley, bucle Elm input → update → view → layout → raster → present. Reemplazo total de GPUI (extinto 2026-05-26): toda app gráfica de la suite corre sobre Llimphi.

Este documento es la referencia de uso orientada a humanos y a IA. Está organizado para salto directo: cada capa, widget y módulo trae su API real (firmas copiadas del código). Para el porqué arquitectónico ver SDD.md; para la regla de concurrencia ver COMPUTO-FUERA-DEL-HILO-UI.md.


Índice

  1. Modelo mental en 60 segundos
  2. Arquitectura — las capas
  3. Quickstart — la app mínima
  4. El trait App (bucle Elm)
  5. Handle — efectos y concurrencia
  6. View<Msg> — el DSL declarativo
  7. Layout (taffy / Style)
  8. Eventos e interacción
  9. Texto
  10. Canvas custom y GPU directo
  11. Theme y paletas
  12. Capas base (hal · raster · text · motion · icons · surface)
  13. Catálogo de widgets
  14. Catálogo de módulos
  15. llimphi-workspace — chasis tipo tmux
  16. Reglas duras y gotchas
  17. Comandos y demos
  18. Cheat-sheet
  19. Índice de crates

1. Modelo mental en 60 segundos

Llimphi es Elm sobre la GPU. Una app es un tipo que implementa el trait App con cuatro piezas:

El runtime hace el bucle: un evento (click/tecla/rueda) produce un Msg, update deriva el nuevo Model, view reconstruye el árbol, taffy calcula las cajas, vello rasteriza, y se hace swap del frame. No hay mutabilidad compartida, no hay vDOM ajeno, no hay callbacks imperativos: declarás qué se ve y qué Msg emite cada nodo.

   evento ─▶ Msg ─▶ update(model,msg) ─▶ model' ─▶ view(model') ─▶ View<Msg>
                                                                      │
   present ◀─ raster(vello) ◀─ layout(taffy) ◀──────────────────────┘

Tres reglas de oro:

  1. view es pura — no muta nada, sólo lee el modelo y arma el árbol.
  2. Cómputo pesado va a un worker vía Handle::spawn, nunca síncrono en update/init/handlers (congela la ventana → "Not Responding").
  3. Widgets son visuales y stateless; el estado vive en tu Model. Módulos sí encapsulan estado + comportamiento.

2. Arquitectura — las capas

4. llimphi-ui ........... runtime winit del bucle Elm (App, Handle, run, KeyEvent)
   └ llimphi-compositor . árbol View<Msg>, mount sobre taffy, paint, hit-test (winit-free)
3. llimphi-layout ....... motor de layout (taffy: flexbox + grid)
2. llimphi-raster ....... rasterizador vectorial (vello) + backend GPU directo
1. llimphi-text ......... shaping + fuentes (parley): bidi, ligaduras, CJK/emoji
0. llimphi-hal .......... abstracción de superficie (wgpu + winit / framebuffer)

El split compositor/runtime (2026-05-31) es importante: llimphi-compositor es winit-free (sólo View, mount, paint, hit-test). llimphi-ui lo corre sobre winit y re-exporta todo el compositor, así escribís llimphi_ui::View sin enterarte del split. Esto habilita un futuro runtime sobre el framebuffer del kernel wawa reusando el mismo compositor.

Auxiliares: llimphi-theme (paletas), llimphi-motion (tweens), llimphi-icons (iconos vectoriales), llimphi-surface (texturas externas), llimphi-workspace (chasis tmux), llimphi-gallery (showcase).

Catálogo: ~45 widgets (visuales) + 10 módulos (features con estado).


3. Quickstart — la app mínima

use llimphi_ui::llimphi_layout::taffy::prelude::*;
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::{App, Handle, View};

#[derive(Clone)]
enum Msg { Increment, Reset }

struct Counter;

impl App for Counter {
    type Model = u32;
    type Msg = Msg;

    fn title() -> &'static str { "llimphi · counter" }

    fn init(_: &Handle<Self::Msg>) -> Self::Model { 0 }

    fn update(model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
        match msg {
            Msg::Increment => model.saturating_add(1),
            Msg::Reset => 0,
        }
    }

    fn view(model: &Self::Model) -> View<Self::Msg> {
        let boton = View::new(Style {
            size: Size { width: length(160.0), height: length(56.0) },
            align_items: Some(AlignItems::Center),
            justify_content: Some(JustifyContent::Center),
            ..Default::default()
        })
        .fill(Color::from_rgba8(60, 200, 130, 255))
        .radius(12.0)
        .text("+1", 28.0, Color::from_rgba8(10, 30, 20, 255))
        .on_click(Msg::Increment);

        View::new(Style {
            flex_direction: FlexDirection::Column,
            size: Size { width: percent(1.0), height: percent(1.0) },
            align_items: Some(AlignItems::Center),
            justify_content: Some(JustifyContent::Center),
            gap: Size { width: length(0.0), height: length(24.0) },
            ..Default::default()
        })
        .fill(Color::from_rgba8(20, 24, 32, 255))
        .children(vec![
            View::new(Style::default()).text(model.to_string(), 160.0, Color::WHITE),
            boton,
        ])
    }
}

fn main() { llimphi_ui::run::<Counter>(); }

Cargo.toml:

[dependencies]
llimphi-ui    = { workspace = true }
llimphi-theme = { workspace = true }
# + los widgets/modules que uses:
# llimphi-widget-button = { workspace = true }

Corre con cargo run -p <tu-crate> --release. El ejemplo vivo está en llimphi-ui/examples/counter.rs.


4. El trait App (bucle Elm)

Definido en llimphi-ui/src/lib.rs. El estado es inmutable; cada evento produce un Model nuevo.

pub trait App: 'static {
    type Model: 'static;
    type Msg: Clone + Send + 'static;

    fn init(handle: &Handle<Self::Msg>) -> Self::Model;
    fn update(model: Self::Model, msg: Self::Msg, handle: &Handle<Self::Msg>) -> Self::Model;
    fn view(model: &Self::Model) -> View<Self::Msg>;

    // --- Todo lo de abajo tiene default; sobreescribí lo que necesites ---

    fn on_key(_model: &Self::Model, _event: &KeyEvent) -> Option<Self::Msg> { None }

    fn on_wheel(_model: &Self::Model, _delta: WheelDelta,
                _cursor: (f32, f32), _modifiers: Modifiers) -> Option<Self::Msg> { None }

    /// Capa de overlay (menús, modales, popovers). Si devuelve `Some`, se pinta
    /// encima y clicks/hover van EXCLUSIVAMENTE a ella (el fondo queda "bajo
    /// vidrio"). La transición la maneja tu Model.
    fn view_overlay(_model: &Self::Model) -> Option<View<Self::Msg>> { None }

    /// Drag&drop de archivos desde el file manager. Un evento por archivo.
    fn on_file_drop(_model: &Self::Model, _path: std::path::PathBuf) -> Option<Self::Msg> { None }

    /// El foco cambió (Tab/Shift+Tab o click sobre un nodo `focusable`). El
    /// runtime administra el foco; guardás `id` en tu Model para pintar el ring
    /// y rutear el teclado. Ver §8 (Foco y teclado).
    fn on_focus(_model: &Self::Model, _id: Option<u64>) -> Option<Self::Msg> { None }

    /// IME (composición de texto: CJK, acentos muertos, emoji). Opt-in vía
    /// `ime_allowed()` para no robarle el texto a las apps que sólo leen
    /// `on_key`. Flujo: Enabled → Preedit* → Commit/Disabled. Ver §8 (IME).
    fn ime_allowed() -> bool { false }
    fn on_ime(_model: &Self::Model, _event: &ImeEvent) -> Option<Self::Msg> { None }
    /// Área del caret en px físicos para ubicar la ventana de candidatos.
    fn ime_cursor_area(_model: &Self::Model) -> Option<(f32, f32, f32, f32)> { None }

    fn title() -> &'static str { "llimphi" }
    fn app_id() -> Option<&'static str> { None }   // app_id del xdg-toplevel en Wayland
    fn initial_size() -> (u32, u32) { (960, 540) }
}

Punto de entrada: pub fn run<A: App>() — corre hasta que el usuario cierre la ventana o la app llame Handle::quit.

Eventos de teclado (KeyEvent):

pub struct KeyEvent {
    pub key: Key,                 // re-export de winit; usar NamedKey para teclas especiales
    pub state: KeyState,          // Pressed | Released
    pub text: Option<String>,     // texto resultante con IME/modifiers; None para flechas etc.
    pub modifiers: Modifiers,     // { shift, ctrl, alt, meta }
    pub repeat: bool,
}

Key y NamedKey se re-exportan desde llimphi_ui.

Rueda (WheelDelta { x, y }): normalizado a "líneas". Convención CSS: y positivo = scroll hacia abajo.


5. Handle — efectos y concurrencia

Handle<Msg> es Send + Clone. Llega a init y update. Es el único modo legítimo de producir efectos sin romper la pureza de la transición.

impl<Msg: Send + 'static> Handle<Msg> {
    pub fn quit(&self);                 // cierra la ventana / termina el bucle
    pub fn dispatch(&self, msg: Msg);   // encola un Msg para el próximo turno
    pub fn spawn<F: FnOnce() -> Msg + Send + 'static>(&self, f: F);   // worker; su Msg reentra al update
    pub fn spawn_periodic<F: Fn() -> Msg + Send + 'static>(&self, period: Duration, f: F);  // tick periódico
    pub fn for_test() -> Self;          // handle "muerto" para tests sin event loop
}

6. View<Msg> — el DSL declarativo

Un View = Style de taffy + relleno + texto/imagen/painter + handlers + hijos. Todo se arma con builders encadenables (self -> Self). Definido en llimphi-compositor/src/view.rs.

View::new(style: Style) -> View<Msg>

Apariencia

MétodoEfecto
.fill(Color)color de fondo
.hover_fill(Color)color al pasar el cursor (habilita hit-test de hover)
.radius(f64)esquinas redondeadas
.alpha(f32)opacidad de todo el subtree [0,1] (capa intermedia — no gratis)
.transform(Affine)afín 2D alrededor del centro del rect (estilo CSS transform-origin:50% 50%)
.clip(bool)recorta hijos al rect (paint + hit-test)
.image(Image)pinta peniko::Image centrada, preservando aspect ratio
.children(Vec<View<Msg>>)hijos

Texto (ver §9)

.text(content, size_px, color)                            // centrado
.text_aligned(content, size_px, color, Alignment)
.text_aligned_italic(content, size_px, color, Alignment, italic)
.text_aligned_full(content, size_px, color, Alignment, italic, font_family: Option<String>)
.text_runs(content, size_px, default_color, runs: Vec<(usize,usize,Color)>, Alignment) // multicolor 1-pasada
.line_height(mult)                                        // override interlínea (default 1.2)

Interacción (ver §8)

.on_click(Msg)
.on_click_at(|lx, ly, w, h| -> Option<Msg>)     // posición local + tamaño del rect
.on_right_click(Msg) / .on_right_click_at(...)
.on_middle_click(Msg)
.on_pointer_enter(Msg) / .on_pointer_leave(Msg)
.draggable(|phase: DragPhase, dx, dy| -> Option<Msg>)
.draggable_at(|phase, dx, dy, lx0, ly0| -> Option<Msg>)   // + posición inicial del press
.drag_payload(u64)                                        // payload que viaja con el drag
.on_drop(|payload: u64| -> Option<Msg>)                   // este nodo es drop target
.drop_hover_fill(Color)                                   // resaltado mientras un drag lo sobrevuela
.on_scroll(|dx, dy| -> Option<Msg>)                       // rueda local (antes del on_wheel global)
.focusable(u64)                                           // nodo enfocable por Tab/click (id opaco)

Pintura custom (ver §10)

.paint_with(|scene: &mut vello::Scene, ts: &mut Typesetter, rect: PaintRect| { ... })
.gpu_paint_with(|device, queue, encoder, view, rect: PaintRect, (vp_w, vp_h)| { ... })

Notas clave:


7. Layout (taffy / Style)

Style es el taffy::Style directo, re-exportado vía llimphi_ui::llimphi_layout::taffy::prelude::*. Es Flexbox + CSS Grid puro.

Campos más usados:

Style {
    flex_direction: FlexDirection::Row | Column,
    size:    Size { width, height },          // length(px) | percent(0..1) | Dimension::auto()
    min_size, max_size,
    flex_grow: f32, flex_shrink: f32,
    align_items:     Some(AlignItems::{Start,Center,End,Stretch}),
    justify_content: Some(JustifyContent::{Start,Center,End,SpaceBetween,...}),
    gap:     Size { width, height },
    padding: Rect { left, right, top, bottom },   // con length(px)
    margin:  Rect { ... },
    ..Default::default()
}

Helpers de prelude: length(px), percent(frac), auto(), Dimension, Size, Rect, FlexDirection, AlignItems, JustifyContent.

llimphi-layout además expone:

En el 99% de los casos no tocás LayoutTree a mano: lo maneja el runtime al montar tu View. Sólo armás Styles.


8. Eventos e interacción

Quiero…Cómo
Botón / fila clickable.on_click(Msg) (+ .hover_fill para feedback)
Saber dónde se clickeó (canvas).on_click_at(|lx,ly,w,h| ...) → convertir a coords de mundo
Menú contextual.on_right_click(Msg::OpenMenu{..}), guardar pos en Model, abrir en view_overlay
Abrir en pestaña nueva.on_middle_click(Msg)
Preview al pasar el mouse.on_pointer_enter(Msg) / .on_pointer_leave(Msg)
Resize de panel.draggable(|phase,dx,dy| ...) acumulando delta en el Model
Arrastrar entidad de un canvas.draggable_at(|phase,dx,dy,lx0,ly0| ...)
Drag&drop entre zonasorigen: .drag_payload(id); destino: .on_drop(|id| ...) + .drop_hover_fill
Scroll globalApp::on_wheel(model, delta, cursor, mods)
Área de scrollwidget scroll_y(...) (autocontenido) o .on_scroll(|dx,dy| ...) por nodo
TecladoApp::on_key(model, &KeyEvent) -> Option<Msg>
Foco / Tab.focusable(id) en los nodos + App::on_focus(model, id) (ver abajo)
IME (CJK, acentos)App::ime_allowed() -> true + App::on_ime(model, &ImeEvent) (ver abajo)
Drop de archivos del SOApp::on_file_drop(model, path)

Patrón overlay (menús/modales): el modelo guarda "menú abierto sí/no". Mientras esté abierto, view_overlay devuelve Some(view); clicks fuera se cierran envolviendo los items en un scrim a pantalla completa con on_click = DismissOverlay. Cuando el modelo dice cerrado, view_overlay devuelve None.

Scroll (widget llimphi-widget-scroll). scroll_y(offset, content_len, viewport_len, content, on_scroll, &palette) arma un viewport clipeado + contenido desplazado -offset + barra arrastrable. Es stateless: el offset vive en tu Model. on_scroll(delta_px) (rueda y arrastre) emite un delta a sumar; clampealo con scroll::clamp_offset en tu update. Helpers: ensure_visible(offset, vp, item_top, item_h) para llevar la selección a la vista (teclado); approach(cur, target, factor) para scroll suave/inercia (driveado por Handle::spawn_periodic).

Foco y teclado. Marcá los nodos navegables con .focusable(id) (id u64 que vos elegís). El runtime es la única fuente de verdad del foco: lo mueve con Tab/Shift+Tab en orden de árbol (envolviendo) y al clickear un nodo enfocable, y te avisa con App::on_focus(model, Option<u64>). Guardás el id en tu Model para (a) pintar el ring (if model.focus == Some(id) { .fill(accent) } en view) y (b) rutear el teclado al campo activo desde on_key. No setees el foco por tu cuenta vía Msg: quedaría desincronizado del runtime.

IME (composición de texto). Opt-in: ime_allowed() -> true. Con IME activo el texto compuesto no llega por KeyEvent.text sino por on_ime: ImeEvent::Enabled → uno o más Preedit{text, cursor} (texto en composición, a pintar subrayado en el caret) → Commit(text) (insertá como tecleado) o Disabled. Reportá el área del caret con ime_cursor_area(model) para ubicar la ventana de candidatos (CJK) junto al cursor.


9. Texto

TextSpec (en compositor) describe el texto de un nodo:

pub struct TextSpec {
    pub content: String,
    pub size_px: f32,
    pub color: Color,
    pub alignment: Alignment,          // Start | Center | End | Justify
    pub italic: bool,
    pub font_family: Option<String>,   // string CSS con fallbacks
    pub line_height: f32,              // múltiplo; default 1.2
    pub runs: Option<Vec<(usize, usize, Color)>>,  // color por rango de BYTES
}

10. Canvas custom y GPU directo

Dos hooks para pintar primitivas no expresables como composición de Views. Conviven en el mismo árbol; el runtime pinta toda la pasada vello primero, luego los gpu_painter en orden DFS.

paint_with — vía vello (el default)

.paint_with(|scene: &mut vello::Scene, ts: &mut Typesetter, rect: PaintRect| {
    // dibujar BezPath, kurbo, texto con `ts`, etc. dentro de `rect`.
    // NO dejar push_layer sin pop_layer; NO resetear la scene.
})

Para: dominium-canvas, osciloscopios de pluma, charts de cosmos, pineal. Bueno hasta ~500 K primitivos por frame (rebuild) o ~2 M (Scene reusada).

gpu_paint_with — sube vertex buffers directo a wgpu, salta vello

.gpu_paint_with(|device, queue, encoder, view, rect: PaintRect, (vp_w, vp_h)| {
    // abrir begin_render_pass con LoadOp::Load (NO clear) para preservar vello.
    // (vp_w, vp_h) = tamaño en px de la TextureView destino, para calcular NDC.
})

Para volumen masivo: starfield Gaia de cosmos, particles de tinkuy, viewport de nakui, pineal denso. Rango 100 K – 10 M+ primitivos. No soporta texto ni AA fino ni múltiples grosores de stroke por flush. Para texto encima de un render GPU, usar view_overlay (segunda Scene vello).

¿Cuándo cada uno?

Preguntavello (paint_with)GPU directo (gpu_paint_with)
Primitivos/frame< ~500 K rebuild / < ~2 M Scene reusada100 K – 10 M+
¿Cambian cada frame?sí, rebuild baratomejor estático (buffer persistente)
Curvas Beziernativashay que teselar
Textosíno
AA finosí (analítico)no (sin MSAA)

Default: paint_with salvo que ya midas que el volumen lo justifica (factores ~11× a 1M en GPU mid sólo en el régimen persistente). El backend GPU expone GpuPipelines/GpuBatch en llimphi-raster (§12).


11. Theme y paletas

llimphi-theme::Theme es un struct de slots semánticos de color. Cuatro presets const: Theme::dark() (default), light(), aurora(), sunset().

pub struct Theme {
    pub name: &'static str,
    // fondos
    pub bg_app, bg_panel, bg_panel_alt, bg_input, bg_input_focus,
    pub bg_button, bg_button_hover, bg_selected, bg_row_hover: Color,
    // texto
    pub fg_text, fg_muted, fg_placeholder, fg_destructive: Color,
    // bordes y acento
    pub border, border_focus, accent: Color,
}

Theme::all() -> Vec<Theme>                 // orden de rotación canónico
Theme::by_name(name) -> Option<Theme>
Theme::next_after(current_name) -> Theme   // para el theme-switcher

Tokens auxiliares en el mismo crate:

Patrón de widgets: cada widget define su XxxPalette con Palette::from_theme(&theme). Tu app guarda un Theme en el Model, deriva las paletas que necesita en view, y se las pasa a los widgets. Para cambiar de tema, el theme-switcher emite Msg(next_theme) y reconstruís todo.


12. Capas base

llimphi-hal — superficie

Hal::new(compatible_surface: Option<&wgpu::Surface>) -> Result<Hal, HalError>   // async
trait Surface { fn size(); fn resize(w,h); fn acquire() -> Result<Frame,_>; fn present(frame, hal); }
WinitSurface::new(hal, window: Arc<Window>) -> Result<Self, HalError>
Frame::view() -> &wgpu::TextureView;  Frame::size() -> (u32,u32)

Hal::new pide adapter Backends::PRIMARY (Vulkan) y cae a all() sólo si no hay — no volver a InstanceDescriptor::default(): el backend GL de Mesa sobre Wayland segfaultea en el teardown. El runtime de llimphi-ui ya maneja todo esto; sólo tocás HAL si escribís un runtime nuevo.

llimphi-raster — rasterización

Renderer::new(hal) -> Result<Renderer,_>
Renderer::render(&mut self, hal, scene: &vello::Scene, frame: &Frame, base_color: Color)
// GPU directo:
GpuPipelines::new(device, color_format) -> Self   // campos: lines, tris, rects, bind_layout
GpuBatch::new(&pipelines)
  .line_width(w) .add_line(p0,p1,color) .add_polyline(&pts,color)
  .add_tri(a,b,c, ca,cb,cc) .add_tri_list(&verts,color) .add_rect(x,y,w,h,color)
  .primitive_count() -> u32
  .flush(device, queue, encoder, view, viewport, load_op)

Re-exporta vello y peniko (Color, Image, Fill, etc.).

llimphi-text — shaping

Typesetter::new()                          // una por proceso (FontContext es caro)
  .layout(text, size_px, max_width, alignment, line_height, italic, font_family) -> Layout<()>
  .layout_runs(text, size_px, default_color, &runs, alignment, line_height) -> Layout<RunBrush>
TextBlock::simple(text, size_px, color, origin)
layout_block(ts, &block) / measure(ts, &block) -> Measurement
draw_layout(scene, &layout, color, origin) / draw_layout_runs(scene, &layout, origin)
Alignment::{Start, Center, End, Justify}

llimphi-motion — tweens

trait Lerp { fn lerp(self, other, t: f32) -> Self; }   // impl para f32,f64,(f32,f32),(f64,f64),Color
Tween::new(from, to, duration, easing: fn(f32)->f32)   // o Tween::idle(value)
tween.value() / .progress() / .done()
animate(handle, duration, make_msg)                    // arranca los ticks del tween

Patrón: guardás Tween<T> en el Model, animate(...) en el update, la view lee tween.value() cada repaint. El tween se auto-termina.

llimphi-icons — iconos vectoriales (~23, grid 24×24)

Icon::{File, Folder, Save, Plus, Minus, X, Check, Edit, Trash, ChevronUp/Down/Left/Right,
       Home, Search, Info, Warning, Error, Bell, Settings, More, ...}
icon_view(Icon, color, stroke_width) -> View<Msg>
paint_icon(scene, rect, icon, color, stroke_width)     // dentro de un paint_with

stroke_width en unidades del grid 24×24 (1.6 es armónico).

llimphi-surface — texturas externas

ExternalSurface::new(device, queue)        // barato de clonar (Arc<Mutex> interno)
  .upload(&rgba, w, h)                      // desde otro hilo/decoder/cámara
  .view(style) -> View<Msg>                 // blittea a su rect en el árbol Elm
  .blit(queue, encoder, dst_view, rect, viewport)   // o manual desde gpu_paint_with

13. Catálogo de widgets

Los widgets son funciones puras que devuelven View<Msg> (o specs que se convierten a View). Son stateless: el estado vive en tu Model. Convención: cada uno trae XxxPalette::from_theme(&Theme). Crates en widgets/<nombre>/, dep llimphi-widget-<nombre>.

Controles

button — button_view(label, &ButtonPalette, on_click: Msg) -> View; button_styled(label, style, alignment, &palette, on_click).

field — wrapper de formulario (label + helper/error + requerido). field_view(FieldSpec { label, control: View<Msg>, required, helper, error, palette }).

text-input — input single-line con estado TextInputState (new()/masked(), text(), set_text(), apply_key(&KeyEvent) -> bool, soporta undo/redo + selección con Shift). Render: text_input_view(&state, placeholder, focused, &palette, on_focus: Msg).

text-area — multilínea con estado TextAreaState (Enter = newline, sin auto-submit). text_area_view(&state, placeholder, focused, body_height, &palette, on_focus).

slider — sin estado. slider_view(label, value, min, max, &palette, on_change: Fn(DragPhase, delta_value) -> Option<Msg>). El delta viene en unidades, no píxeles.

switch — switch_view(progress: f32 [0..1], on_toggle: Msg, &palette). La app guarda el bool y opcionalmente anima progress con un Tween.

segmented — N opciones exclusivas. segmented_view(&[&str], selected: usize, make_msg: Fn(usize)->Msg, &palette).

progress — linear_progress_view(progress, track, fill, height) y radial_progress_view(progress, track, fill, stroke_ratio). Sin eventos.

spinner — spinner_view(color, stroke_ratio, speed_rev_per_sec). Animado por reloj absoluto; requiere redraws periódicos (spawn_periodic).

badge — count_badge_view(count, BadgeKind) ("99+" si ≥100) y dot_badge_view(BadgeKind). BadgeKind::{Info,Success,Warning,Error,Neutral}.

avatar — avatar_view(name, size_px): círculo determinista (color por hash del nombre + inicial).

tooltip — render puro. tooltip_view(TooltipSpec { anchor, viewport, side: Side, text, palette }). Se monta en view_overlay; la app controla visibilidad con on_pointer_enter/leave.

empty — empty-state. empty_view(Icon, title, description: Option<&str>, &palette).

skeleton — placeholder con shimmer. skeleton_view, skeleton_box_view(w,h,..), skeleton_line_view(w,..). Requiere redraws periódicos.

banner — tira de status. banner_view(BannerKind::{Info,Success,Warning,Error}, message).

Contenedores y layout

panel — chrome (gradiente + hairline accent). panel_view(children, PanelStyle); PanelStyle::{from_theme, from_theme_large, neutral}. panel_signature_painter(style) para reusar el look en un paint_with.

card — card_view(children, CardOptions { accent, padding, gap, radius, signature }, &CardPalette).

stat-card — métrica de dashboard. stat_card_view(label, value, description, accent, &recent_items, &palette).

tabs — tabs_view(TabsSpec { labels, active: usize, on_select: Fn(usize)->Msg, content: View<Msg>, tab_height, palette, tab_width }). Selección la maneja la app.

splitter — divisor draggable de 2 panes. splitter_two(Direction::{Row,Column}, a, a_size, b, b_size, on_resize: Fn(DragPhase, delta)->Option<Msg>, &palette). PaneSize::{Fixed(px), Flex}. La app acumula el delta en su Model.

scroll — área de scroll vertical con barra arrastrable. scroll_y(offset, content_len, viewport_len, content, on_scroll: Fn(delta_px)->Msg, &palette). Stateless (offset en el Model); rueda autocontenida. Helpers: clamp_offset, ensure_visible (selección a la vista), approach (scroll suave). Ver §8.

tiled — grilla auto cols×rows de tiles con title bar. tiled_view(tiles, &palette), tiled_view_cols(tiles, cols, &palette), y variantes *_reorderable* con on_reorder: Fn(from, to)->Option<Msg> (drag-to-swap por la title bar). TileSpec { label, content }.

panes — árbol binario BSP tipo tmux. La app guarda un Layout:

Layout::single(id) / Layout::Split { axis: Axis, ratio, first, second }
layout.split(target, new, axis) / .without(target) / .resize(&path, delta) / .leaves()
panes_view(&layout, focused: PaneId, leaf: FnMut(PaneId)->View, on_resize: Fn(Vec<Side>,DragPhase,delta)->Option<Msg>,
           on_focus: Fn(PaneId)->Msg, &palette)

grid — grilla 2D virtualizada. ventana_visible(total, vp_w, vp_h, scroll_fila, &metrics) -> VisibleWindow para virtualizar, luego grid_view(GridSpec { cells: Vec<GridCell { content, label, selected, on_click }>, cols, metrics, caption, ... }).

list — lista vertical virtualizada. list_view(ListSpec { rows: Vec<ListRow { label, selected, on_click }>, total, caption, truncated_hint, row_height, palette }). La app prefiltra las filas visibles.

tree — árbol expand/collapse. tree_view(TreeSpec { rows: Vec<TreeRow { label, depth, has_children, expanded, selected, on_toggle, on_select }>, row_height, indent_px, palette }). La app aplana el árbol según nodos expandidos.

navigator — navegador data-agnóstico de nodos en dos modos conmutables (árbol ↔ grafo, reusa tree + nodegraph). Render-only: la app guarda expanded/selected/mode. Pasa un bosque de NavNode { id: u64, label, kind: NavKind (Monad|Group|Dir|File|Other), children } y callbacks por id.

navigator_view(NavSpec { roots, mode: NavMode::{Tree,Graph}, selected, palette, guides },
    is_expanded: Fn(u64)->bool, on_toggle: Fn(u64)->Msg,
    on_select: Fn(u64)->Msg, on_context: Option<Fn(u64)->Msg>)
// árbol: click selecciona, chevron expande, icono por kind. grafo: cables de
// contención padre→hijo, arrastrar selecciona, right-click abre. Pensado para
// el sidebar de Mónadas/archivos de pata, pero no sabe de nouser.

app-header — app_header(label, actions: Vec<View<Msg>>, &palette).

status-bar — status_bar_view(left, center, right, &palette) con StatusSegment::text(..).with_icon(Icon).clickable(Msg).emphasized().

breadcrumb — breadcrumb_view(&[&str], make_msg: Fn(usize)->Msg, &palette) (el último segmento no es clickable).

modal — diálogo centrado con scrim. modal_view(ModalSpec { title, body: View<Msg>, buttons: Vec<ModalButton>, size, viewport, on_dismiss, palette }). ModalButton::{primary, cancel, destructive}(label, msg). Se monta en view_overlay.

toast — notificaciones efímeras bottom-right. La app guarda Vec<Toast> (Toast::{info,success,warning,error}(id, text, duration)), filtra is_alive(now), y toast_stack_view(&toasts, viewport, make_dismiss: Fn(u64)->Msg).

splash — splash de arranque (cuatro cuadrantes andinos). splash_view(started_at: Instant, bg, fg_text); basado en tiempo, requiere redraws.

Ricos / interactivos

nodegraph — lienzo de nodos + cables Bezier. Sin estado (la app guarda posiciones y Wires).

NodeSpec { id: NodeId(u32), label, x, y, inputs: Vec<String>, outputs: Vec<String> }
Wire { from_node, from_output: PinIdx(u16), to_node, to_input }
nodegraph_view(&nodes, &wires, &palette, &metrics,
    on_drag_node: Fn(NodeId, DragPhase, dx, dy)->Option<Msg>,
    on_connect:   Fn(NodeId, PinIdx, NodeId, PinIdx)->Option<Msg>)
// + nodegraph_view_ex (right-click) y nodegraph_view_styled (tints por nodo/cable)

timeline — scrub clickeable. timeline_view(progress: f32, &palette, on_seek: Fn(f32 [0..1])->Option<Msg>).

text-editor — editor IDE (capa visual sobre el core agnóstico). La app guarda EditorState:

EditorState::new(); .text(); .set_text(s); .has_selection(); .can_undo()/.can_redo();
.add_cursor_at(line,col);  .apply_key_with_clipboard(&KeyEvent, &mut dyn Clipboard) -> ApplyResult;
.ensure_caret_visible(visible_lines)
// nota: `metrics` se pasa POR VALOR; el callback es on_pointer: Fn(PointerEvent)->Option<Msg>
text_editor_view(&state, &EditorPalette, metrics: EditorMetrics, visible_lines: usize, on_pointer)
text_editor_view_highlighted(&state, &palette, metrics, visible_lines, language: Language, on_pointer)
text_editor_view_full(&state, &palette, metrics, visible_lines, language, match_ranges: &[(usize,usize)], on_pointer)
syntax_palette_dark(&theme) -> SyntaxPalette   // en lib.rs del widget

text-editor-core — núcleo agnóstico (sin GPU, sin Llimphi; sólo peniko::Color). Reutilizable en TUI/web/headless. Tipos clave:

text-editor-lsp — cliente LSP por stdin/stdout. trait LspClient (fire-and-forget request_* + lecturas de caché latest_*/clear_*): completions, hover, definition, references, rename, formatting, signature help, document symbols. RustAnalyzerClient::start(workspace_root); NoopLspClient para tests.

clipboard — portapapeles del sistema vía arboard. SystemClipboard::new(), is_available(), impl Clipboard. No-op silencioso si no hay display (CI headless).

menubar — barra de menú mac-style. menubar_view(&MenuBarSpec { menu: &AppMenu, open: Option<usize>, theme, viewport, height, on_open: Fn(Option<usize>)->Msg, on_command: Fn(&str)->Msg }); dropdown en view_overlay con menubar_overlay(spec) o menubar_overlay_animated(spec, active, appear). Navegación por teclado: menubar_nav, menubar_command_at.

edit-menu — menú estándar de edición sobre un editor. EditFlags::from_editor(&state, masked), edit_context_menu(anchor, viewport, &theme, flags, on_action: Fn(EditAction)->Msg, on_dismiss) → ContextMenuSpec. apply(&mut state, EditAction, &mut clipboard) -> ApplyResult. EditAction::{Undo,Redo,Cut,Copy,Paste,Delete,SelectAll}.

context-menu — menú contextual genérico (look "webpage"). ContextMenuItem:: action(label).with_shortcut(..).icon(..).disabled().destructive().submenu(children) o ::separator(). context_menu_view(ContextMenuSpec { anchor, viewport, header, items, active, on_pick: Fn(usize)->Msg, on_dismiss, palette }); context_menu_view_ex con submenús/animación. Se monta en view_overlay con scrim.

theme-switcher — theme_switcher_view(&current: &Theme, on_change: Fn(Theme)->Msg) (+ _styled/_flex). Cicla Theme::next_after.

shortcuts-help — overlay "?" con atajos agrupados. shortcuts_help_view( ShortcutsHelpSpec { title, groups: Vec<ShortcutGroup { title, entries: Vec<ShortcutEntry { keys, description }> }>, viewport, on_dismiss, palette }).

wawa-mark — sello vectorial del SO wawa. wawa_mark_view(&WawaMarkPalette); paint_mark(scene, rect, &palette) para canvas custom. Usar en contenedor cuadrado.


14. Catálogo de módulos

Los módulos encapsulan estado + comportamiento (a diferencia de los widgets). Todos siguen el mismo contrato:

State  +  Msg  +  Action  +  apply(state, msg, ...) -> Action
                            +  on_key(state, &KeyEvent) -> Option<Msg>
                            +  open_shortcut(&KeyEvent) -> bool
                            +  view(state, ..., to_host: F) -> View<HostMsg>
                            +  Palette

La app guarda Option<ModuleState> (o el state directo, p. ej. bookmarks), rutea el atajo de apertura con open_shortcut, rutea teclas con on_key, aplica Msgs con apply, y monta el view pasando un mapeo to_host: Fn(ModuleMsg) -> HostMsg. Cuando apply devuelve una Action (p. ej. Invoke(id), OpenAt{..}, GoTo{..}), la app ejecuta el efecto. Crates en modules/<nombre>/.

MóduloAtajoAcción que devuelvePropósito
command-paletteCtrl+Shift+PInvoke(String)paleta de comandos fuzzy. El host declara &[Command]
file-pickerCtrl+POpen(PathBuf)fuzzy file picker; host pasa &[PathBuf] + root
fif (find-in-files)Ctrl+Shift+FOpenAt{path,line,col}, Searched{..}, Replaced{..}buscar/reemplazar; dual-view (dialog + barra). search() / replace_all() hacen el I/O
diff-viewerCtrl+Shift+D—diff side-by-side. DiffState::new(before_label, after_label, before, after) computa con similar
mini-mapCtrl+Shift+MJumpTo(line)minimapa del buffer; agnóstico del editor (recibe Snapshot)
bookmarksCtrl+Alt+B toggle, Ctrl+Shift+B lista, Ctrl+Alt+N/P navJumpTo{path,line}marcadores per-file persistentes (state directo, no Option)
symbol-outlineCtrl+Shift+OGoTo{line,col}outline de símbolos; host arma Vec<SymbolItem> (LSP/tree-sitter/custom)
selector——abstracción portátil abrir/guardar: trait Selector (HostSelector con PathBuf, WawaSelector placeholder content-addressed)
plugin-host—OpenAt{..}, SetStatus(..)runtime WASM (wasmi) con permisos por bitfield; PluginHost::load_from_dir/invoke(id, cap, args)
shuma-termCtrl+`SetStatus(..)terminal integrada. spawn(cwd) lanza PTY (shuma_exec), vt100::Parser renderiza; Tick drena el PTY

Patrón típico de integración (command-palette):

struct Model { palette: Option<PaletteState>, commands: Vec<Command>, /* … */ }
enum Msg { Palette(PaletteMsg), /* … */ }

// on_key:
if command_palette::open_shortcut(ev) { return Some(Msg::Palette(PaletteMsg::Open)); }
if let Some(_) = &model.palette { return command_palette::on_key(p, ev).map(Msg::Palette); }

// update:
Msg::Palette(m) => {
    if let Some(state) = model.palette.as_mut() {
        match command_palette::apply(state, m, &model.commands) {
            PaletteAction::Invoke(id) => { /* ejecutar comando id */ model.palette = None; }
            PaletteAction::Close => model.palette = None,
            PaletteAction::None => {}
        }
    }
}

// view_overlay:
model.palette.as_ref().map(|s|
    command_palette::view(s, &model.commands, &palette, Msg::Palette))

15. llimphi-workspace — chasis tipo tmux

Monta cualquier componente en un layout intercambiable con splits resizables (máquina de estados de foco/split/cierre + chrome estándar). Construido sobre llimphi-widget-panes.

Workspace::new()
  .focused() -> PaneId         .count()      .leaves() -> Vec<PaneId>     .layout() -> &Layout
  .focus(id)  .split(Axis) -> PaneId   .close() -> Option<PaneId>   .resize(&path, delta)
  .apply(WsMsg) -> WsEffect

enum WsMsg { Focus(PaneId), Split(Axis), Close, Resize(Vec<Side>, f32) }
enum WsEffect { None, Created(PaneId), Closed(PaneId) }

workspace_view(&ws, &WorkspacePalette,
    leaf: FnMut(PaneId)->View<Host>,           // materializa el contenido de cada panel
    lift: Fn(WsMsg)->Host)                      // sube los Msg del chasis a tu Msg

Patrón: enum Msg { Ws(WsMsg), Panel(PaneId, PanelMsg) }. En update, ws.apply(msg) te avisa con WsEffect::{Created,Closed}(id) para que crees o destruyas el estado del panel correspondiente.


16. Reglas duras y gotchas

🔴 Cómputo pesado fuera del hilo de UI (PRIORIDAD URGENTE)

Ningún update/init/handler puede ejecutar trabajo síncrono pesado (efemérides, simulación, IO, parse, embeddings, layout de árboles grandes). Bloquea el hilo → "Not Responding". init corre dentro de resumed, después de crear la ventana, así que un cómputo pesado ahí ya congela una ventana visible.

Patrón (referencia: cosmos-app-llimphi):

// Model: Option<Resultado> (None = "calculando…") + flag dirty + contador de generación.
struct Model { x: Option<Resultado>, x_dirty: bool, x_gen: u64 }
enum Msg { XComputed(u64, Arc<Resultado>) }

// al FINAL de update() (que tiene el Handle):
if m.x_dirty {
    m.x_dirty = false;
    m.x_gen = m.x_gen.wrapping_add(1);
    let (gen, input) = (m.x_gen, m.input.clone());     // sólo lo que el worker necesita (Send)
    handle.spawn(move || Msg::XComputed(gen, Arc::new(compute(&input))));
}
// al recibir: aplicar SÓLO si la generación sigue vigente (evita que un
// resultado tardío pise a uno más nuevo en drags/toggles rápidos).
Msg::XComputed(gen, x) => if gen == m.x_gen {
    m.x = Some(Arc::try_unwrap(x).unwrap_or_else(|a| (*a).clone()));
}

La generación es imprescindible si el recálculo se dispara seguido. Ver COMPUTO-FUERA-DEL-HILO-UI.md y su checklist por app.

Otras


17. Comandos y demos

cargo check --workspace                              # smoke test mínimo (debe pasar siempre)
cargo run -p <crate> --release                       # correr una app
cargo run -p <crate> --example <demo> --release      # correr un demo

# demos del propio framework:
cargo run -p llimphi-ui      --example counter --release   # bucle Elm completo
cargo run -p llimphi-ui      --example editor  --release   # text field + teclado
cargo run -p llimphi-ui      --example gpu_paint_demo --release
cargo run -p llimphi-gallery --release                     # showcase de TODO el kit
cargo run -p nada            --release                     # editor real para ejercitar widgets

# benchmark GPU directo vs vello:
cargo run -p llimphi-gpu-bench --release

llimphi-gallery (src/main.rs, ~967 líneas) es la referencia viva del patrón completo: Model/Msg/init/update/view/view_overlay con overlays mutuamente excluyentes (modal > atajos > toasts > context-menu > dropdown). Controles: click en switches/segments; "Mostrar toast"/"Abrir modal"; ? abre atajos; Esc cierra el overlay activo.


18. Cheat-sheet

// ── App mínima ──────────────────────────────────────────────
impl App for X { type Model; type Msg; init; update; view; }
llimphi_ui::run::<X>();

// ── Nodo ────────────────────────────────────────────────────
View::new(Style{ flex_direction, size, gap, padding, align_items, justify_content, ..default() })
    .fill(c).hover_fill(c).radius(r).clip(b).alpha(a).transform(xf)
    .text(s, px, c) | .text_aligned(s,px,c,al) | .text_runs(s,px,c,runs,al)
    .image(img) | .paint_with(|scene,ts,rect|{}) | .gpu_paint_with(|d,q,enc,view,rect,vp|{})
    .on_click(m) | .on_click_at(|lx,ly,w,h|) | .on_right_click(m) | .on_middle_click(m)
    .on_pointer_enter(m) | .on_pointer_leave(m)
    .draggable(|ph,dx,dy|) | .draggable_at(|ph,dx,dy,lx0,ly0|)
    .drag_payload(id) | .on_drop(|id|) | .drop_hover_fill(c)
    .children(vec![..])

// ── Efectos ─────────────────────────────────────────────────
handle.spawn(|| Msg::Done(compute()));          // worker → reentra al update
handle.spawn_periodic(dur, || Msg::Tick);       // feed periódico
handle.dispatch(Msg::X);  handle.quit();

// ── Estilo de layout (taffy prelude) ────────────────────────
length(px)  percent(0..1)  Dimension::auto()
FlexDirection::{Row,Column}  AlignItems::{Start,Center,End,Stretch}
JustifyContent::{Start,Center,End,SpaceBetween}

// ── Theme ───────────────────────────────────────────────────
Theme::dark()/light()/aurora()/sunset();  Theme::next_after(name);  XxxPalette::from_theme(&t)

// ── Overlay (menús/modales) ─────────────────────────────────
fn view_overlay(m) -> Option<View<Msg>> { if m.open { Some(menu) } else { None } }

19. Índice de crates

Framework (02_ruway/llimphi/): llimphi-hal · llimphi-raster · llimphi-text · llimphi-layout · llimphi-compositor · llimphi-ui · llimphi-theme · llimphi-motion · llimphi-icons · llimphi-surface · llimphi-workspace · llimphi-gallery · llimphi-gpu-bench.

Widgets (widgets/, dep llimphi-widget-<n>): app-header · avatar · badge · banner · breadcrumb · button · card · clipboard · context-menu · edit-menu · empty · field · gallery · grid · list · menubar · modal · navigator · nodegraph · panel · panes · progress · segmented · shortcuts-help · skeleton · slider · splash · splitter · stat-card · status-bar · switch · tabs · text-area · text-editor · text-editor-core · text-editor-lsp · text-input · theme-switcher · tiled · timeline · toast · tooltip · tree · wawa-mark.

Módulos (modules/): bookmarks · command-palette · diff-viewer · fif · file-picker · mini-map · plugin-host · selector · shuma-term · symbol-outline.

Android (android/): clear-screen-android · vello-hello-android · vello-text-android.


Documentos hermanos: SDD.md (diseño y roadmap), COMPUTO-FUERA-DEL-HILO-UI.md (regla de concurrencia), README.md / LEEME.md (overview). Las firmas de este manual reflejan el código al 2026-06-01; ante divergencia, la fuente autoritativa es el lib.rs de cada crate.