tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1//! **`nada` adentro de una jaula: el editor entero corriendo en otro proceso.**
2//!
3//! Es el último ladrillo de `SDD-JAULA-LINUX.md`. Todo lo que hay debajo ya
4//! estaba probado por partes —el dibujo bit a bit, el cable, el tablado, los
5//! eventos, la vida del huésped, y el editor compilando contra el backend `wire`—
6//! y esto es lo que las junta: el **mismo** `nada` que corre suelto en el
7//! escritorio, componiendo hacia un cable en vez de hacia una ventana.
8//!
9//! ```bash
10//! cargo run --manifest-path 02_ruway/llimphi/llimphi-jaula/Cargo.toml \
11//! --no-default-features --features std --example nada_enjaulado -- <socket> [raíz]
12//! ```
13//!
14//! Normalmente lo lanza el anfitrión, que le pasa el socket.
15//!
16//! ## Lo que este archivo NO hace, y es el punto
17//!
18//! No reimplementa nada del editor. No hay una vista, ni un `update`, ni un
19//! manejo de teclas propios: se llaman los de `nada`, que son los mismos que
20//! usa el binario de escritorio. Si esto tuviera lógica de editor adentro, todo
21//! el trabajo de las siete fases anteriores habría sido al pedo — el objetivo es
22//! que la app se vea **igual** enjaulada que suelta.
23//!
24//! Lo único propio son las tres piezas de la frontera:
25//!
26//! 1. **El medidor**: el typesetter de la jaula no shapea, pregunta.
27//! 2. **El despachador**: `nada` manda sus efectos a un `Despachador`; acá ese
28//! trait lo cumple una cola que el bucle drena, en vez del bucle de winit.
29//! 3. **El bucle**: componer, mandar el cuadro, esperar, aplicar.
30
31use std::cell::RefCell;
32use std::collections::VecDeque;
33use std::rc::Rc;
34use std::sync::{Arc, Mutex};
35
36use llimphi_cable::{DelAnfitrion, Huesped};
37use llimphi_jaula::{medidor, Jaula};
38
39/// El despachador del huésped: una **cola** que el bucle drena.
40///
41/// En el escritorio, `nada` le manda sus efectos al `Handle` de winit, que los
42/// mete en la cola de eventos de la ventana. Acá no hay ventana: la cola es
43/// ésta, y el bucle la vacía en cada vuelta. Para `nada` no cambia nada —
44/// habla con el trait, no con quien lo cumple.
45///
46/// `Arc<Mutex<…>>` y no `Rc<RefCell<…>>` porque el trait pide `Send + Sync`: el
47/// trabajo de fondo ([`Despachador::lanzar`]) corre en un hilo de verdad.
48#[derive(Clone, Default)]
49struct Cola(Arc<Mutex<VecDeque<nada::Msg>>>);
50
51impl llimphi_despacho::Despachador<nada::Msg> for Cola {
52 fn despachar(&self, msg: nada::Msg) {
53 // Si el mutex está envenenado, un efecto perdido no justifica tirar el
54 // editor abajo: se descarta y se sigue. El síntoma sería un guardado que
55 // no aparece, no una pantalla negra.
56 if let Ok(mut c) = self.0.lock() {
57 c.push_back(msg);
58 }
59 }
60
61 /// Guardar un archivo va a un hilo, como en el escritorio. El default del
62 /// trait lo correría inline y bloquearía el fotograma.
63 fn lanzar(&self, trabajo: Box<dyn FnOnce() -> nada::Msg + Send + 'static>) {
64 let cola = self.clone();
65 std::thread::spawn(move || {
66 llimphi_despacho::Despachador::despachar(&cola, trabajo());
67 });
68 }
69}
70
71impl Cola {
72 fn sacar(&self) -> Option<nada::Msg> {
73 self.0.lock().ok()?.pop_front()
74 }
75}
76
77fn main() {
78 let mut args = std::env::args().skip(1);
79 let Some(ruta) = args.next() else {
80 eprintln!("uso: nada_enjaulado <ruta-del-socket> [raíz-del-proyecto]");
81 std::process::exit(2);
82 };
83 let raiz = args
84 .next()
85 .map(std::path::PathBuf::from)
86 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| ".".into()));
87
88 let flujo = match std::os::unix::net::UnixStream::connect(&ruta) {
89 Ok(f) => f,
90 Err(e) => {
91 eprintln!("nada enjaulado: no pude conectarme a {ruta}: {e}");
92 std::process::exit(1);
93 }
94 };
95
96 // El cable se comparte entre el medidor —que corre ADENTRO de `fotograma`—
97 // y el bucle, que manda el cuadro después. Un solo hilo: las dos tomas nunca
98 // se anidan.
99 let cable = Rc::new(RefCell::new(Huesped::new(flujo)));
100
101 let medidor_cable = Rc::clone(&cable);
102 let (mut ancho, mut alto) = nada::TAMANO_INICIAL;
103 let mut jaula = Jaula::new(ancho as f32, alto as f32, move |p| {
104 // El marshalling es el compartido con el camino de wawa: si el huésped
105 // armara su propia petición, un campo nuevo del estilo quedaría sin
106 // viajar de un solo lado y el texto se mediría distinto según quién
107 // hospeda.
108 medidor::medir_con(p, |peticion, salida| {
109 let Ok(pedido) = postcard::from_bytes(peticion) else { return -1 };
110 match medidor_cable.borrow_mut().medir(&pedido) {
111 Ok(m) => {
112 salida.copy_from_slice(&m.a_bytes());
113 0
114 }
115 Err(_) => -1,
116 }
117 })
118 .unwrap_or(llimphi_text::wire::Medida { width: 0.0, full_width: 0.0, height: 0.0 })
119 });
120
121 // **El editor de verdad.** El LSP arranca apagado: levantar un
122 // `rust-analyzer` es un gesto explícito, igual que en el módulo de shuma.
123 let mut modelo = nada::Model::nuevo(nada::Opciones::en(raiz));
124 let cola = Cola::default();
125 modelo.despacho = Arc::new(cola.clone());
126
127 loop {
128 // 1) Componer con la vista de `nada` — la misma que pinta el binario
129 // suelto— y mandar el cuadro.
130 let montado = jaula.fotograma(nada::vista_raiz(&modelo));
131 if cable.borrow_mut().enviar_cuadro(jaula.escena()).is_err() {
132 return; // el anfitrión se fue
133 }
134
135 // 2) Esperar. Un bucle que compusiera sin parar quemaría un núcleo para
136 // pintar siempre lo mismo.
137 let evento = match cable.borrow_mut().esperar_evento() {
138 Ok(e) => e,
139 Err(_) => return,
140 };
141
142 let mut pendientes: Vec<nada::Msg> = Vec::new();
143 match evento {
144 // El hit-test lo hace el huésped contra SU layout: el anfitrión no
145 // sabe —ni tiene por qué— qué hay en cada píxel.
146 DelAnfitrion::Puntero { x, y, apretado } if apretado => {
147 if let Some(msg) = jaula.click(&montado, x, y) {
148 pendientes.push(msg);
149 }
150 }
151 DelAnfitrion::Tecla(ev) => {
152 // Las teclas las traduce `nada`, con la misma función que usa el
153 // binario de escritorio. Acá no hay un mapeo paralelo.
154 if let Some(msg) = nada::keys::handle_key(&modelo, &ev) {
155 pendientes.push(msg);
156 }
157 }
158 DelAnfitrion::Ime(ev) => pendientes.push(nada::Msg::EditIme(ev)),
159 DelAnfitrion::Rueda(d) => {
160 // `Msg::Scroll` pasó a llevar líneas fraccionarias (el trackpad
161 // manda deltas chicos y redondearlos a entero perdía el gesto).
162 // Esto compilaba con `i32` y se quedó atrás: el ejemplo vive en
163 // otro workspace, así que el `cargo check` del repo nunca lo mira.
164 let lineas = d.y * 3.0;
165 if lineas != 0.0 {
166 pendientes.push(nada::Msg::Scroll(lineas));
167 }
168 }
169 DelAnfitrion::Tamano { ancho: a, alto: h } => {
170 ancho = a as u32;
171 alto = h as u32;
172 jaula.redimensionar(a, h);
173 pendientes.push(nada::Msg::WinResized(ancho, alto));
174 }
175 DelAnfitrion::Adios => return,
176 DelAnfitrion::Puntero { .. } | DelAnfitrion::PunteroSalio => {}
177 // El editor no tiene un verbo de «pararse en esta ruta» que el
178 // anfitrión pueda pedirle: su navegación es el árbol propio. Se
179 // ignora explícitamente. El huésped que SÍ la atiende es
180 // `nahual-jailed`, que la traduce a su `TreeSelect`.
181 DelAnfitrion::Ir(_) => {}
182 // La medida se contesta adentro del medidor; si llega acá es que
183 // sobró una, y descartarla es lo correcto.
184 DelAnfitrion::Medida(_) => {}
185 }
186
187 // 3) Aplicar lo del evento **y todo lo que los efectos hayan encolado**.
188 // El drenaje va después y en bucle porque un `update` puede despachar
189 // otro `Msg` —el LSP contestando, el guardado terminando— y dejarlo
190 // para la vuelta siguiente sería pintar un cuadro que ya está viejo.
191 // El despacho se clona ANTES del bucle: `dispatch` consume el modelo, así
192 // que no se le puede pedir prestado un campo en la misma expresión.
193 let despacho: nada::Despacho = modelo.despacho.clone();
194 for msg in pendientes {
195 modelo = nada::update::dispatch(modelo, msg, &despacho);
196 }
197 while let Some(msg) = cola.sacar() {
198 modelo = nada::update::dispatch(modelo, msg, &despacho);
199 }
200 }
201}