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//! Una lista navegable en el framebuffer, **sin compositor y sin GPU**.
2//!
3//! No es una demo bonita: es el arnés que prueba que el camino entero llega al
4//! panel. Por eso los colores son planos y conocidos —se cuentan píxeles del
5//! otro lado de la frontera, con `screendump`, igual que hace
6//! `scripts/wawa-cromo-qemu.py` con el cromo del kernel.
7//!
8//! cargo run -p llimphi-drm --example lista (necesita una consola libre)
9//!
10//! Se sale con `q`.
11
12use llimphi_drm::peniko::Color;
13use llimphi_drm::taffy::prelude::{length, percent, FlexDirection, Size, Style};
14use llimphi_drm::{correr, Key, KeyEvent, NamedKey, Pantalla, Programa, View};
15
16/// Los tres colores del arnés. Planos y separados a propósito: el veredicto es
17/// un conteo de píxeles exactos, y dos tonos parecidos no se distinguirían del
18/// ruido de un rasterizador.
19const FONDO: Color = Color::from_rgba8(18, 18, 32, 255);
20const FILA: Color = Color::from_rgba8(40, 44, 60, 255);
21const ELEGIDA: Color = Color::from_rgba8(90, 160, 120, 255);
22
23struct Lista;
24
25#[derive(Clone)]
26enum Msg {
27 Arriba,
28 Abajo,
29 Salir,
30}
31
32struct Modelo {
33 elegido: usize,
34 salir: bool,
35}
36
37impl Programa for Lista {
38 type Model = Modelo;
39 type Msg = Msg;
40
41 fn init() -> Modelo {
42 Modelo { elegido: 0, salir: false }
43 }
44
45 fn update(mut m: Modelo, msg: Msg) -> Modelo {
46 match msg {
47 Msg::Arriba => m.elegido = m.elegido.saturating_sub(1),
48 Msg::Abajo => m.elegido = (m.elegido + 1).min(2),
49 Msg::Salir => m.salir = true,
50 }
51 m
52 }
53
54 fn view(m: &Modelo) -> View<Msg> {
55 let filas: Vec<View<Msg>> = (0..3)
56 .map(|i| {
57 View::new(Style {
58 size: Size { width: percent(0.8_f32), height: length(80.0_f32) },
59 ..Default::default()
60 })
61 .fill(if i == m.elegido { ELEGIDA } else { FILA })
62 })
63 .collect();
64 View::new(Style {
65 flex_direction: FlexDirection::Column,
66 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
67 gap: Size { width: length(0.0_f32), height: length(20.0_f32) },
68 ..Default::default()
69 })
70 .fill(FONDO)
71 .children(filas)
72 }
73
74 fn on_key(_m: &Modelo, ev: &KeyEvent) -> Option<Msg> {
75 match &ev.key {
76 Key::Named(NamedKey::ArrowUp) => Some(Msg::Arriba),
77 Key::Named(NamedKey::ArrowDown) => Some(Msg::Abajo),
78 Key::Character(c) if c == "q" => Some(Msg::Salir),
79 _ => None,
80 }
81 }
82
83 fn termino(m: &Modelo) -> bool {
84 m.salir
85 }
86}
87
88/// Decir algo donde se pueda LEER desde afuera.
89///
90/// La salida estándar de un programa lanzado por el getty es su tty —acá tty1,
91/// la pantalla gráfica—, así que un `println!` no llega a la consola serie y
92/// quien mira el arranque desde afuera no ve nada. `/dev/kmsg` es el ring del
93/// kernel, y el kernel lo replica a la consola del `console=` del cmdline. Es
94/// el mismo camino que usa `arje-zero` para sus huellas de arranque.
95fn decir(linea: &str) {
96 use std::io::Write;
97 println!("{linea}");
98 // `/dev/console` es el que apunta el último `console=` del cmdline —en el
99 // medio, el puerto serie—, y es lo único que se lee desde afuera cuando la
100 // pantalla es justamente lo que se está probando.
101 for destino in ["/dev/console", "/dev/kmsg"] {
102 if let Ok(mut f) = std::fs::OpenOptions::new().write(true).open(destino) {
103 let _ = writeln!(f, "{linea}");
104 }
105 }
106}
107
108fn main() {
109 decir("lista :: arranco");
110 let mut pantalla = match Pantalla::abrir() {
111 Ok(p) => p,
112 Err(e) => {
113 // Se dice por qué y se sale con código: en un arranque headless esto
114 // es lo único que queda en el log del serial.
115 decir(&format!("lista :: ERROR no pude abrir la pantalla: {e}"));
116 std::process::exit(2);
117 }
118 };
119 let (w, h) = pantalla.tamano();
120 // El tamaño va al serial: es lo que permite saber, desde afuera, contra qué
121 // resolución hay que contar los píxeles del pantallazo.
122 decir(&format!("lista :: pantalla {w}x{h}"));
123 if let Err(e) = correr::<Lista>(&mut pantalla) {
124 decir(&format!("lista :: ERROR {e}"));
125 std::process::exit(1);
126 }
127}