tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
1//! `llimphi-widget-qr` — pinta una matriz de código QR.
2//!
3//! **Existe porque había tres copias del mismo pintor.** `qillqa-mesa-llimphi`,
4//! el módulo Mesa de `agora-app` y el diente `Mi carné` pintaban cada uno su
5//! versión, y la de `agora-app` lo dice en su propio comentario: «la misma
6//! composición que `qillqa-mesa-llimphi::qr_painter`». Ese es el patrón que
7//! `shared/qr` ya había cortado del lado de la codificación —matriz y PNG, una
8//! sola vez— y que del lado del dibujo seguía abierto.
9//!
10//! Y las tres copias no eran idénticas, que es el costo real de duplicar: sólo
11//! **una** tenía el solape de medio píxel entre módulos. Sin él el redondeo deja
12//! hilos blancos y **el lector falla** — un defecto arreglado en un lugar y
13//! vivo en los otros dos. Acá está una vez y arreglado.
14//!
15//! **Data-agnóstico**, como el resto de los widgets: recibe el lado y los
16//! módulos, no un tipo de `shared/qr`. Quien tenga una `QrMatriz` pasa
17//! `(m.ancho, &m.modulos)`; quien la tenga de otra forma, también.
18
19#![forbid(unsafe_code)]
20#![no_std]
21
22extern crate alloc;
23
24use alloc::vec::Vec;
25use llimphi_compositor::llimphi_layout::taffy::prelude::{length, percent, Size, Style};
26use llimphi_compositor::peniko::Color;
27use llimphi_compositor::View;
28
29/// Módulos de **zona tranquila** alrededor del código.
30///
31/// Cuatro es lo que pide la norma. Una de las tres copias usaba dos, que es
32/// menos margen del que muchos lectores necesitan para encontrar el símbolo
33/// sobre un fondo que no controlan.
34pub const ZONA_TRANQUILA: usize = 4;
35
36/// La vista del QR, con alto fijo en píxeles.
37///
38/// El código se pinta **siempre blanco sobre negro, sin teñir con la paleta**:
39/// un lector de QR no sabe de temas, y un código en los colores del tema oscuro
40/// es un cuadrado bonito que no se lee.
41pub fn qr_view<Msg: Clone + Send + Sync + 'static>(
42 ancho: usize,
43 modulos: &[bool],
44 alto_px: f32,
45) -> View<Msg> {
46 qr_view_ext(ancho, modulos, alto_px, ZONA_TRANQUILA)
47}
48
49/// Como [`qr_view`], eligiendo la zona tranquila.
50pub fn qr_view_ext<Msg: Clone + Send + Sync + 'static>(
51 ancho: usize,
52 modulos: &[bool],
53 alto_px: f32,
54 borde: usize,
55) -> View<Msg> {
56 let modulos: Vec<bool> = modulos.to_vec();
57 View::new(Style {
58 size: Size { width: percent(1.0_f32), height: length(alto_px) },
59 flex_shrink: 0.0,
60 ..Default::default()
61 })
62 // Es una imagen y su contenido no se puede leer en voz alta: el lector de
63 // pantalla necesita que alguien diga qué hay adentro, y quien lo sabe es el
64 // llamador. Acá sólo se declara el rol.
65 .role(llimphi_compositor::Role::Image)
66 .paint_with(move |scene, _ts, rect| {
67 use llimphi_compositor::peniko::kurbo::{Affine, Rect};
68 use llimphi_compositor::peniko::Fill;
69
70 if ancho == 0 || modulos.len() < ancho * ancho {
71 return;
72 }
73 let lado_mod = (ancho + borde * 2) as f64;
74 // Cuadrado y centrado: un QR estirado no se lee.
75 let lado_px = rect.w.min(rect.h) as f64;
76 let paso = lado_px / lado_mod;
77 let x0 = rect.x as f64 + (rect.w as f64 - lado_px) * 0.5;
78 let y0 = rect.y as f64 + (rect.h as f64 - lado_px) * 0.5;
79
80 scene.fill(
81 Fill::NonZero,
82 Affine::IDENTITY,
83 Color::WHITE,
84 None,
85 &Rect::new(x0, y0, x0 + lado_px, y0 + lado_px),
86 );
87 for my in 0..ancho {
88 for mx in 0..ancho {
89 if !modulos[my * ancho + mx] {
90 continue;
91 }
92 let x = x0 + (mx + borde) as f64 * paso;
93 let y = y0 + (my + borde) as f64 * paso;
94 // **El medio píxel de solape.** Sin él el redondeo deja hilos
95 // blancos entre módulos y el lector no engancha el símbolo. Es
96 // el arreglo que sólo una de las tres copias tenía.
97 scene.fill(
98 Fill::NonZero,
99 Affine::IDENTITY,
100 Color::BLACK,
101 None,
102 &Rect::new(x, y, x + paso + 0.5, y + paso + 0.5),
103 );
104 }
105 }
106 })
107}