gioser

tawasuyu/llimphi

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

Rust 124Markdown 82TOML 78Shell 12149 Sutichaykuna

⎇ main llimphi-jaula/examples/vidriera_activa.rs Rust · 255 siq'ikuna · 8.5 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1//! **La vidriera que HACE algo, corriendo en su propio proceso.**
2//!
3//! `SDD-CARAS.md` §5.2, fase C6. La vidriera de nivel 1 es un documento firmado
4//! que la plaza pinta (C2); ésta es la de nivel 2: cuando la página tiene que
5//! **responder** —un catálogo que filtra, un turno que se pide— la forma de la
6//! casa no es un navegador, es **una jaula**.
7//!
8//! Lo que este huésped demuestra, y es todo el punto:
9//!
10//! 1. **Recibe la vidriera por argumento y la verifica él mismo.** No confía en
11//!    quien lo lanzó: si la firma no cierra, lo dice y no pinta nada más.
12//! 2. **Pinta y recibe clics** sin que la plaza sepa qué hay en cada píxel — el
13//!    hit-test es de este lado, contra su propio layout.
14//! 3. **No puede hacer nada más.** No tiene red, ni disco, ni la identidad de
15//!    nadie: le llegaron bytes por `argv` y un socket que sólo habla el cable.
16//!    Eso no es una promesa de este archivo — es lo que hay.
17//!
18//! ```bash
19//! cargo build --manifest-path 02_ruway/llimphi/llimphi-jaula/Cargo.toml \
20//!     --no-default-features --features std --example vidriera_activa
21//! ```
22//!
23//! Normalmente no se corre a mano: lo lanza la plaza (`agora-app`, diente
24//! Vidriera, «abrirla como programa»).
25//!
26//! **Los textos que se ven son los de la vidriera**, que son datos del que la
27//! publicó. Lo único propio del huésped es una línea de números —clics, medidas—
28//! que es el instrumento, no prosa: por eso no pasa por `rimay-localize`, igual
29//! que su hermano `huesped_demo`. Lo que la plaza dice **sobre** este huésped sí
30//! está en los tres idiomas, y vive del lado de la plaza.
31
32use std::cell::RefCell;
33use std::rc::Rc;
34
35use agora_vidriera::{Donde, Paquete};
36use llimphi_cable::{DelAnfitrion, Huesped};
37use llimphi_jaula::taffy::prelude::{length, percent, FlexDirection, Rect, Size, Style};
38use llimphi_jaula::taffy::AlignItems;
39use llimphi_jaula::{medidor, peniko::Color, Jaula, View};
40use llimphi_text::Alignment;
41
42const FONDO: Color = Color::from_rgba8(22, 24, 30, 255);
43const TINTA: Color = Color::from_rgba8(228, 231, 238, 255);
44const TENUE: Color = Color::from_rgba8(150, 156, 170, 255);
45const ELEGIDA: Color = Color::from_rgba8(52, 84, 140, 255);
46const FILA: Color = Color::from_rgba8(32, 35, 43, 255);
47
48struct Modelo {
49    /// La vidriera que se está mostrando, o el motivo por el que no hay ninguna.
50    /// Se guarda **ya verificada**: `Paquete::de_texto` no devuelve otra cosa.
51    que_hay: Result<Paquete, String>,
52    /// Qué línea de «lo que dice» está elegida. Es el catálogo que filtra, en su
53    /// forma más chica: el huésped tiene estado propio y la plaza no se entera.
54    elegida: usize,
55    clics: u32,
56    medidas: u64,
57    ancho: f32,
58    alto: f32,
59}
60
61#[derive(Clone, Debug)]
62enum Msg {
63    Elegir(usize),
64}
65
66/// Las líneas de «lo que dice». Un párrafo suelto es una sola; varios renglones
67/// son varios. No se inventa una estructura que el documento no tiene.
68fn lineas(p: &Paquete) -> Vec<String> {
69    let dice = &p.vidriera.core.dice;
70    let ls: Vec<String> = dice
71        .lines()
72        .map(str::trim)
73        .filter(|l| !l.is_empty())
74        .map(str::to_string)
75        .collect();
76    if ls.is_empty() {
77        vec![dice.clone()]
78    } else {
79        ls
80    }
81}
82
83fn vista(m: &Modelo) -> View<Msg> {
84    let mut hijos: Vec<View<Msg>> = Vec::new();
85    match &m.que_hay {
86        Err(motivo) => {
87            hijos.push(texto(motivo.clone(), 13.0, Color::from_rgba8(220, 140, 120, 255), 20.0));
88        }
89        Ok(p) => {
90            hijos.push(texto(p.vidriera.core.nombre.clone(), 19.0, TINTA, 26.0));
91            let donde = match (&p.vidriera.core.donde, &p.reservada) {
92                (_, Some(r)) => r.core.direccion.clone(),
93                (Donde::Zona(z), None) => z.clone(),
94                (Donde::NoDice, None) => String::new(),
95            };
96            if !donde.is_empty() {
97                hijos.push(texto(donde, 12.0, TENUE, 18.0));
98            }
99            for (i, l) in lineas(p).into_iter().enumerate() {
100                hijos.push(fila(i, l, i == m.elegida));
101            }
102        }
103    }
104    hijos.push(instrumento(m));
105
106    View::new(Style {
107        flex_direction: FlexDirection::Column,
108        size: Size {
109            width: percent(1.0_f32),
110            height: percent(1.0_f32),
111        },
112        padding: Rect {
113            left: length(12.0),
114            right: length(12.0),
115            top: length(10.0),
116            bottom: length(8.0),
117        },
118        gap: Size {
119            width: length(0.0),
120            height: length(5.0),
121        },
122        ..Default::default()
123    })
124    .fill(FONDO)
125    .children(hijos)
126}
127
128fn texto(t: String, tam: f32, color: Color, alto: f32) -> View<Msg> {
129    View::new(Style {
130        size: Size {
131            width: percent(1.0_f32),
132            height: length(alto),
133        },
134        flex_shrink: 0.0,
135        align_items: Some(AlignItems::CENTER),
136        ..Default::default()
137    })
138    .text_aligned(t, tam, color, Alignment::Start)
139}
140
141fn fila(i: usize, t: String, elegida: bool) -> View<Msg> {
142    View::new(Style {
143        size: Size {
144            width: percent(1.0_f32),
145            height: length(22.0),
146        },
147        flex_shrink: 0.0,
148        align_items: Some(AlignItems::CENTER),
149        padding: Rect {
150            left: length(8.0),
151            right: length(8.0),
152            top: length(0.0),
153            bottom: length(0.0),
154        },
155        ..Default::default()
156    })
157    .fill(if elegida { ELEGIDA } else { FILA })
158    .on_click(Msg::Elegir(i))
159    .text_aligned(t, 12.0, TINTA, Alignment::Start)
160}
161
162/// La línea de números. Cambia con cada clic: si el cable se congelara, la
163/// pantalla lo diría sola en vez de parecer una interfaz quieta.
164fn instrumento(m: &Modelo) -> View<Msg> {
165    texto(
166        format!(
167            "{} clics · {} medidas · {}×{}",
168            m.clics, m.medidas, m.ancho as i32, m.alto as i32
169        ),
170        10.5,
171        TENUE,
172        16.0,
173    )
174}
175
176fn main() {
177    let ruta = std::env::args().nth(1).unwrap_or_else(|| {
178        eprintln!("uso: vidriera_activa <ruta-del-socket> [vidriera-en-hex]");
179        std::process::exit(2);
180    });
181    // La vidriera llega por argumento. **Se verifica acá**: el huésped no confía
182    // en quien lo lanzó, igual que no confiaría en un servidor.
183    let que_hay = match std::env::args().nth(2) {
184        Some(hex) => Paquete::de_texto(&hex)
185            .map_err(|e| format!("esta vidriera no entra: {e:?}")),
186        None => Err("no me pasaron ninguna vidriera".to_string()),
187    };
188
189    let flujo = match std::os::unix::net::UnixStream::connect(&ruta) {
190        Ok(f) => f,
191        Err(e) => {
192            eprintln!("vidriera_activa: no pude conectarme a {ruta}: {e}");
193            std::process::exit(1);
194        }
195    };
196
197    let cable = Rc::new(RefCell::new(Huesped::new(flujo)));
198    let medidor_cable = Rc::clone(&cable);
199    let mut jaula = Jaula::new(560.0, 360.0, move |p| {
200        medidor::medir_con(p, |peticion, salida| {
201            let pedido = match postcard::from_bytes(peticion) {
202                Ok(p) => p,
203                Err(_) => return -1,
204            };
205            match medidor_cable.borrow_mut().medir(&pedido) {
206                Ok(m) => {
207                    salida.copy_from_slice(&m.a_bytes());
208                    0
209                }
210                Err(_) => -1,
211            }
212        })
213        .unwrap_or(llimphi_text::wire::Medida {
214            width: 0.0,
215            full_width: 0.0,
216            height: 0.0,
217        })
218    });
219
220    let mut modelo = Modelo {
221        que_hay,
222        elegida: usize::MAX,
223        clics: 0,
224        medidas: 0,
225        ancho: 560.0,
226        alto: 360.0,
227    };
228
229    loop {
230        let montado = jaula.fotograma(vista(&modelo));
231        modelo.medidas = cable.borrow().medidas_pedidas();
232        if cable.borrow_mut().enviar_cuadro(jaula.escena()).is_err() {
233            return;
234        }
235        let evento = match cable.borrow_mut().esperar_evento() {
236            Ok(e) => e,
237            Err(_) => return,
238        };
239        match evento {
240            DelAnfitrion::Puntero { x, y, apretado } if apretado => {
241                if let Some(Msg::Elegir(i)) = jaula.click(&montado, x, y) {
242                    modelo.elegida = i;
243                    modelo.clics += 1;
244                }
245            }
246            DelAnfitrion::Tamano { ancho, alto } => {
247                jaula.redimensionar(ancho, alto);
248                modelo.ancho = ancho;
249                modelo.alto = alto;
250            }
251            DelAnfitrion::Adios => return,
252            _ => {}
253        }
254    }
255}