gioser

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Definitions

⎇ main llimphi-jaula/examples/huesped_demo.rs Rust · 217 lines · 7.8 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//! **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}