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//! **El primer huésped de verdad: una app que corre aparte y se pinta adentro.**
2//!
3//! Hasta acá el cable estaba probado por partes —el dibujo bit a bit, el marco,
4//! los eventos, la vida— y ninguna de esas pruebas enciende una pantalla. Esto
5//! es el lazo cerrado: un proceso que compone su interfaz con el compositor de
6//! siempre, la manda por el cable, y recibe los clics de vuelta.
7//!
8//! No es `nada` todavía (eso es F6b, y está medido en `SDD-JAULA-LINUX.md`).
9//! Es lo más chico que ejercita **las cuatro cosas que pueden fallar**:
10//!
11//! 1. **Texto**, que es lo único que obliga a preguntar del otro lado — sin él,
12//! el medidor no se usa y el cable parecería andar aunque estuviera roto.
13//! 2. **Clics**, con hit-test del lado del huésped sobre su propio layout.
14//! 3. **Redimensionado**, que es el otro camino por el que la interfaz cambia.
15//! 4. **Salir**, para que el anfitrión tenga algo que ver morir.
16//!
17//! ```bash
18//! cargo run --manifest-path 02_ruway/llimphi/llimphi-jaula/Cargo.toml \
19//! --no-default-features --features std --example huesped_demo -- <socket>
20//! ```
21//!
22//! Normalmente no se corre a mano: lo lanza el anfitrión
23//! (`llimphi-widget-tablado`, ejemplo `tablado_demo`).
24
25use std::cell::RefCell;
26use std::rc::Rc;
27
28use llimphi_cable::{DelAnfitrion, Huesped};
29use llimphi_jaula::taffy::prelude::{length, percent, FlexDirection, Rect, Size, Style};
30use llimphi_jaula::taffy::AlignItems;
31use llimphi_jaula::{medidor, peniko::Color, Jaula, View};
32use llimphi_text::Alignment;
33
34/// Lo que el huésped sabe de sí mismo. Deliberadamente mínimo: lo que se prueba
35/// es el cable, no la app.
36struct Modelo {
37 ancho: f32,
38 alto: f32,
39 elegida: usize,
40 clics: u32,
41 /// Cuántas medidas pidió. Se pinta: es el instrumento del que habla el SDD,
42 /// y verlo en la pantalla es más honesto que confiar en que alguien lo mire
43 /// en un log.
44 medidas: u64,
45}
46
47#[derive(Clone, Debug)]
48enum Msg {
49 Elegir(usize),
50}
51
52const FILAS: [&str; 5] = [
53 "primera fila — con acentos: ñandú, café",
54 "segunda fila",
55 "tercera fila, un poco más larga que las otras",
56 "cuarta",
57 "quinta fila",
58];
59
60fn vista(m: &Modelo) -> View<Msg> {
61 let mut hijos = vec![cabecera(m)];
62 for (i, texto) in FILAS.iter().enumerate() {
63 hijos.push(fila(i, texto, i == m.elegida));
64 }
65 View::new(Style {
66 flex_direction: FlexDirection::Column,
67 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
68 padding: Rect {
69 left: length(10.0),
70 right: length(10.0),
71 top: length(8.0),
72 bottom: length(8.0),
73 },
74 gap: Size { width: length(0.0), height: length(4.0) },
75 ..Default::default()
76 })
77 .fill(Color::from_rgba8(24, 26, 32, 255))
78 .children(hijos)
79}
80
81fn cabecera(m: &Modelo) -> View<Msg> {
82 // El texto lleva números que cambian: si el cable se congelara, la pantalla
83 // lo diría sola en vez de parecer una interfaz quieta.
84 View::new(Style {
85 size: Size { width: percent(1.0_f32), height: length(20.0) },
86 flex_shrink: 0.0,
87 align_items: Some(AlignItems::CENTER),
88 ..Default::default()
89 })
90 .text_aligned(
91 format!(
92 "huésped · {} clics · {} medidas · {}×{}",
93 m.clics, m.medidas, m.ancho as i32, m.alto as i32
94 ),
95 12.0,
96 Color::from_rgba8(230, 232, 240, 255),
97 Alignment::Start,
98 )
99}
100
101fn fila(i: usize, texto: &str, elegida: bool) -> View<Msg> {
102 let fondo = if elegida {
103 Color::from_rgba8(52, 84, 140, 255)
104 } else {
105 Color::from_rgba8(34, 37, 45, 255)
106 };
107 View::new(Style {
108 size: Size { width: percent(1.0_f32), height: length(22.0) },
109 flex_shrink: 0.0,
110 align_items: Some(AlignItems::CENTER),
111 padding: Rect {
112 left: length(8.0),
113 right: length(8.0),
114 top: length(0.0),
115 bottom: length(0.0),
116 },
117 ..Default::default()
118 })
119 .fill(fondo)
120 .on_click(Msg::Elegir(i))
121 .text_aligned(
122 texto.to_string(),
123 11.5,
124 Color::from_rgba8(215, 218, 226, 255),
125 Alignment::Start,
126 )
127}
128
129fn main() {
130 let ruta = std::env::args().nth(1).unwrap_or_else(|| {
131 eprintln!("uso: huesped_demo <ruta-del-socket>");
132 std::process::exit(2);
133 });
134
135 let flujo = match std::os::unix::net::UnixStream::connect(&ruta) {
136 Ok(f) => f,
137 Err(e) => {
138 eprintln!("huésped: no pude conectarme a {ruta}: {e}");
139 std::process::exit(1);
140 }
141 };
142
143 // El cable se comparte entre el medidor —que corre ADENTRO de `fotograma`—
144 // y el bucle, que manda el cuadro después. `RefCell` y no un `Mutex` porque
145 // esto es un solo hilo: las dos tomas nunca se anidan, y si algún día se
146 // anidaran es mejor que reviente acá que que se trabe.
147 let cable = Rc::new(RefCell::new(Huesped::new(flujo)));
148
149 let medidor_cable = Rc::clone(&cable);
150 let mut jaula = Jaula::new(640.0, 400.0, move |p| {
151 // El marshalling es el de la jaula, compartido con el camino de wawa:
152 // si el huésped armara su propia petición, un campo nuevo del estilo
153 // quedaría sin viajar de un solo lado.
154 medidor::medir_con(p, |peticion, salida| {
155 let pedido = match postcard::from_bytes(peticion) {
156 Ok(p) => p,
157 Err(_) => return -1,
158 };
159 match medidor_cable.borrow_mut().medir(&pedido) {
160 Ok(m) => {
161 salida.copy_from_slice(&m.a_bytes());
162 0
163 }
164 Err(_) => -1,
165 }
166 })
167 .unwrap_or(llimphi_text::wire::Medida { width: 0.0, full_width: 0.0, height: 0.0 })
168 });
169
170 let mut modelo = Modelo { ancho: 640.0, alto: 400.0, elegida: 0, clics: 0, medidas: 0 };
171
172 loop {
173 let montado = jaula.fotograma(vista(&modelo));
174 modelo.medidas = cable.borrow().medidas_pedidas();
175 if cable.borrow_mut().enviar_cuadro(jaula.escena()).is_err() {
176 // El anfitrión se fue. Salir es lo correcto: seguir componiendo
177 // contra un caño muerto es gastar una CPU para nadie.
178 return;
179 }
180
181 // Se BLOQUEA esperando. Un bucle que compusiera sin parar quemaría un
182 // núcleo entero para pintar siempre lo mismo.
183 let evento = match cable.borrow_mut().esperar_evento() {
184 Ok(e) => e,
185 Err(_) => return,
186 };
187 match evento {
188 DelAnfitrion::Puntero { x, y, apretado } if apretado => {
189 // El hit-test lo hace el huésped, contra SU layout. El anfitrión
190 // no sabe —ni tiene por qué— qué hay en cada píxel.
191 if let Some(Msg::Elegir(i)) = jaula.click(&montado, x, y) {
192 modelo.elegida = i;
193 modelo.clics += 1;
194 }
195 }
196 DelAnfitrion::Tamano { ancho, alto } => {
197 jaula.redimensionar(ancho, alto);
198 modelo.ancho = ancho;
199 modelo.alto = alto;
200 }
201 DelAnfitrion::Adios => return,
202 // Lo demás todavía no lo usa esta demo. Se ignora explícitamente en
203 // vez de con un `_ => {}` mudo, para que la próxima persona vea qué
204 // llega y no está atendido.
205 DelAnfitrion::Puntero { .. }
206 | DelAnfitrion::PunteroSalio
207 | DelAnfitrion::Tecla(_)
208 | DelAnfitrion::Ime(_)
209 | DelAnfitrion::Rueda(_)
210 // Esta demo es una lista de colores, no un navegador: «pararse en
211 // una ruta» no significa nada acá. El huésped que SÍ la atiende es
212 // `nahual-jailed`, que la traduce a su `TreeSelect`.
213 | DelAnfitrion::Ir(_)
214 | DelAnfitrion::Medida(_) => {}
215 }
216 }
217}