gioser

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Definitions

⎇ main llimphi-jaula/examples/nada_enjaulado.rs Rust · 201 lines · 8.7 KB History Who touched what raw

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

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}