# llimphi-jaula The **app-side** shell: runs the Llimphi loop inside a WASM sandbox and emits an [`EscenaWire`](../llimphi-wire-escena/README.md) instead of rasterizing. It is the twin of [`llimphi-wawa`](../llimphi-wawa/README.md), on the other side of the boundary: that one receives and executes, this one composes and ships. Phase W4 of [`03_ukupacha/wawa/PLAN-LLIMPHI.md`](../../../03_ukupacha/wawa/PLAN-LLIMPHI.md). ``` View → mount → layout → paint → EscenaWire → postcard → … …………………………… WASM boundary …………………………… … → native shaping → rasterize ``` ## The thing to understand Both shells run the **same `llimphi-compositor`** — same mount, same taffy layout, same 4,000-line `render.rs`. The only difference is the `Lienzo` alias and, with it, the `Typesetter`. That is the W4 result worth saying out loud: **there is no app compositor and kernel compositor.** A widget written once works on Linux, inside the kernel, and inside a sandbox. ## Two things this one does that the kernel shell doesn't ### It asks for measurements taffy cannot size a label without knowing how big it is, and measuring means shaping — precisely what must not happen inside the sandbox: the interpreter's 30.3x would eat 72% of the frame (W4.1). So the measurer is an **argument**: ```rust let mut jaula = Jaula::new(320.0, 72.0, |p: &Peticion| { // On wawa: a `sys_medir_texto` crossing to parley on the native side. // In a test: a real parley `Typesetter`. medir_del_otro_lado(p) }); let montado = jaula.fotograma(vista()); let bytes = postcard::to_allocvec(jaula.escena())?; ``` With no measurer it does not return zero — a silent zero would collapse the whole layout and the screen would come out blank while "working". It estimates coarsely **and counts**: a non-zero `jaula.medidas_estimadas()` means that layout is approximate. On wawa that measurer already exists — it is [`medidor::wawa`](src/medidor.rs), wrapping the `sys_medir_texto` syscall: ```rust let mut jaula = Jaula::new(ancho, alto, llimphi_jaula::medidor::wawa); ``` `sys_medir_texto` is **not permission-gated**: measuring text is not a privilege, it is arithmetic over a font, and every app that draws needs it. What it does carry is a quota, because the kernel pays for the shaping outside the app's `fuel`. Off wasm the measurer returns `None` rather than an invented number. ### It keeps the tree The `Mounted` never crosses the boundary, so hit-testing resolves here: the kernel sends a coordinate and the app answers what was hit. ```rust if let Some(msg) = jaula.click(&montado, x, y) { modelo = App::update(modelo, msg); } ``` The opposite of the kernel shell, which **cannot** hit-test precisely because the tree stayed here. ## Why it lives outside the workspace It enables `llimphi-text/wire`, a backend mutually exclusive with `vello` (what the rest of the repo uses by default). Cargo's feature unification would turn on both and trip the `compile_error!` `llimphi-text` carries to catch exactly that — the same trap `llimphi-wawa` already hit. Both being outside isn't friction: it's the design. They are two processes on two sides of a boundary. ```bash cargo test --manifest-path 02_ruway/llimphi/llimphi-jaula/Cargo.toml \ --no-default-features --features std cargo check --manifest-path 02_ruway/llimphi/llimphi-jaula/Cargo.toml \ --target wasm32-unknown-unknown --no-default-features --features libm ``` ## The fixture Since encoder and replayer cannot be compiled together, the test seam is the same as the production one: **bytes**. ```bash cargo run --example emitir_fixture --no-default-features --features std ``` It composes a view with the real compositor and drops the bytes in `../llimphi-wawa/pruebas/fixture_jaula.postcard`, where the `la_escena_que_emitio_el_compositor_llega_a_pixeles` test **and the kernel's boot path** pick them up. If anyone changes the wire format on one side only, those two will say so.