tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-test/tests/emoji_a_color.rs Rust · 196 siq'ikuna · 6.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

1//! **¿Un emoji se pinta a color?** Hasta este test, no — en ninguna app.
2//!
3//! El hallazgo, medido antes de tocar nada (§8): con `TextBlock::simple` a
4//! 32 px, `😀` resolvía a **DejaVu Sans del sistema** y `❤️` a **Inter**, y las
5//! cuatro cadenas probadas daban **cero píxeles cromáticos**. No era el
6//! rasterizador —vello 0.9 dibuja COLR y CBDT solo, y la máquina tiene
7//! `Noto Color Emoji` que fontconfig resuelve con el alias `emoji`— sino el
8//! **orden de la pila de fuentes**: parley agrega la familia genérica `Emoji`
9//! detrás de la pila normal, así que cualquier fuente de texto que cubra el
10//! codepoint gana. DejaVu tiene contornos monocromos del bloque Emoticons e
11//! Inter tiene `❤` como dingbat.
12//!
13//! Se certifica por texto (§8): conteo de píxeles cromáticos, no una imagen.
14//! Un píxel es cromático si sus canales se separan más de 24/255 — un gris,
15//! sea cual sea su brillo, no lo es.
16//!
17//! **Los dos controles negativos son la mitad del test**, y valen más que el
18//! positivo: si el gate estuviera puesto de más, el corazón con selector de
19//! TEXTO (`U+FE0E`) también saldría rojo, y los símbolos del chrome de la UI
20//! (`⚠ ▶ ✔ ©`) se convertirían en caritas de colores. Los dos casos están acá.
21
22use llimphi_test::{with_harness, Harness, Rendered};
23use llimphi_text::peniko::Color;
24use llimphi_text::{layout_block, TextBlock, Typesetter};
25
26const ANCHO: u32 = 320;
27const ALTO: u32 = 80;
28const ORIGEN: (f64, f64) = (10.0, 10.0);
29const TAM: f32 = 32.0;
30
31/// Cuánto se pueden separar los canales de un píxel antes de que deje de ser
32/// un gris. Bien por encima del ruido del antialiasing (medido: un render sin
33/// emoji da `max_croma = 0` exacto sobre fondo y tinta neutros).
34const CROMA_MIN: u32 = 24;
35
36fn fondo() -> Color {
37    Color::from_rgba8(0, 0, 0, 255)
38}
39
40fn tinta() -> Color {
41    Color::from_rgba8(255, 255, 255, 255)
42}
43
44struct Medida {
45    /// Píxeles con algo pintado encima del fondo.
46    tinta: u32,
47    /// De ésos, los que no son grises.
48    cromaticos: u32,
49    /// La separación de canales más grande que haya en el cuadro.
50    max_croma: u32,
51    /// Píxeles claramente rojos (R por encima de G y de B).
52    rojos: u32,
53}
54
55fn medir(r: &Rendered) -> Medida {
56    let mut m = Medida { tinta: 0, cromaticos: 0, max_croma: 0, rojos: 0 };
57    for i in (0..r.rgba.len()).step_by(4) {
58        let (cr, cg, cb) = (r.rgba[i] as i32, r.rgba[i + 1] as i32, r.rgba[i + 2] as i32);
59        if cr + cg + cb > 24 {
60            m.tinta += 1;
61        }
62        let croma = (cr - cg).abs().max((cg - cb).abs()).max((cr - cb).abs()) as u32;
63        if croma > CROMA_MIN {
64            m.cromaticos += 1;
65        }
66        m.max_croma = m.max_croma.max(croma);
67        if cr - cg > 60 && cr - cb > 60 {
68            m.rojos += 1;
69        }
70    }
71    m
72}
73
74fn pintar(h: &mut Harness, texto: &str) -> Rendered {
75    let mut ts = Typesetter::new();
76    let bloque = TextBlock::simple(texto, TAM, tinta(), ORIGEN);
77    let layout = layout_block(&mut ts, &bloque);
78    h.render_con(
79        llimphi_test::llimphi_compositor::View::<()>::new(Default::default()),
80        ANCHO,
81        ALTO,
82        fondo(),
83        |escena, _m, _c, _ts| {
84            llimphi_text::draw_layout(escena, &layout, tinta(), ORIGEN);
85        },
86    )
87}
88
89/// Sin fuente de emoji en la máquina no hay nada que preguntar: el emoji cae
90/// al fallback y sale monocromo, que es el comportamiento correcto en esa
91/// máquina. El test se saltea **diciéndolo**, en vez de fallar por el entorno.
92fn hay_emoji() -> bool {
93    let hay = Typesetter::new().hay_fuente_de_emoji();
94    if !hay {
95        eprintln!(
96            "SALTEADO: esta máquina no tiene fuente de emoji (la familia genérica \
97             `emoji` de fontique no resuelve a nada). Instalá una —p. ej. \
98             Noto Color Emoji— para que este test tenga algo que medir."
99        );
100    }
101    hay
102}
103
104#[test]
105fn una_carita_se_pinta_a_color() {
106    if !hay_emoji() {
107        return;
108    }
109    with_harness(|h| {
110        let m = medir(&pintar(h, "😀"));
111        // Medido con el arreglo puesto: tinta 924, cromáticos 892, croma 235.
112        // Sin el arreglo: tinta 451, cromáticos 0, croma 0 — o sea que el
113        // umbral no es un número prudente, es un abismo. Se pide la mitad de
114        // lo medido para dejar lugar a que cambie la fuente del sistema.
115        assert!(
116            m.cromaticos > 400,
117            "la carita salió monocroma: tinta={} cromáticos={} croma_máx={} \
118             (sin el arreglo daba cromáticos=0)",
119            m.tinta,
120            m.cromaticos,
121            m.max_croma
122        );
123    });
124}
125
126#[test]
127fn el_corazon_sale_rojo_y_no_de_cualquier_color() {
128    if !hay_emoji() {
129        return;
130    }
131    with_harness(|h| {
132        // `❤` + VS16: el caso donde el selector pide presentación de color.
133        let m = medir(&pintar(h, "\u{2764}\u{FE0F}"));
134        assert!(
135            m.rojos > 200,
136            "el corazón no salió rojo: tinta={} cromáticos={} rojos={} — \
137             «hay color» no alcanza como oráculo, un corazón verde también \
138             tendría cromáticos>0",
139            m.tinta,
140            m.cromaticos,
141            m.rojos
142        );
143    });
144}
145
146#[test]
147fn el_selector_de_texto_lo_deja_monocromo() {
148    if !hay_emoji() {
149        return;
150    }
151    with_harness(|h| {
152        // `❤` + VS15 (`U+FE0E`) es el pedido EXPLÍCITO de la forma de símbolo.
153        let m = medir(&pintar(h, "\u{2764}\u{FE0E}"));
154        assert!(m.tinta > 0, "no se pintó nada; el test no probaría nada");
155        assert_eq!(
156            m.cromaticos, 0,
157            "el selector de texto se ignoró y el corazón salió de colores \
158             (tinta={} croma_máx={})",
159            m.tinta, m.max_croma
160        );
161    });
162}
163
164#[test]
165fn los_simbolos_del_chrome_siguen_monocromos() {
166    if !hay_emoji() {
167        return;
168    }
169    with_harness(|h| {
170        // Los cuatro son Extended_Pictographic —o sea que el clasificador de
171        // swash los marca como emoji— y ninguno es presentación emoji. Si el
172        // gate se aflojara a «extended pictographic», el aviso de la UI
173        // saldría como una carita amarilla.
174        let m = medir(&pintar(h, "⚠ ▶ ✔ ©"));
175        assert!(m.tinta > 0, "no se pintó nada; el test no probaría nada");
176        assert_eq!(
177            m.cromaticos, 0,
178            "un símbolo del chrome se fue a la fuente de emoji \
179             (tinta={} croma_máx={})",
180            m.tinta, m.max_croma
181        );
182    });
183}
184
185#[test]
186fn el_texto_normal_no_se_tocó() {
187    with_harness(|h| {
188        let m = medir(&pintar(h, "Hamburgefonstiv 0123"));
189        assert!(m.tinta > 0, "no se pintó nada");
190        assert_eq!(
191            m.cromaticos, 0,
192            "el texto sin emoji salió con color: croma_máx={}",
193            m.max_croma
194        );
195    });
196}