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//! **¿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}