tawasuyu

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Definiciones

⎇ main llimphi-text/src/emoji.rs Rust · 198 líneas · 6.8 KB Historia Quién tocó qué crudo

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

1//! **Presentación emoji** — qué se pinta a color y qué se pinta como símbolo.
2//!
3//! Existe por un número medido (§8): antes de esto, `😀` resolvía a DejaVu Sans
4//! y `❤️` a Inter, y **ninguna app de la suite pintaba un emoji a color** —
5//! cero píxeles cromáticos. No era el rasterizador: vello 0.9 dibuja COLR y
6//! CBDT solo, y fontconfig resuelve `Noto Color Emoji` con el alias `emoji`.
7//! Era el ORDEN de la pila de fuentes (ver [`crate::rangos_emoji`]).
8//!
9//! ## Por qué no alcanza con lo que ya trae swash
10//!
11//! El agrupador de clusters de swash —el mismo que parley usa para decidir
12//! `is_emoji`— clasifica por **Extended_Pictographic**, que es más ancho que
13//! «se pinta a color». Medido acá: `⚠ ▶ ✔ © ‼ ☀ ⤴` dan `extpict = true`. Si
14//! se los mandara a la fuente de emoji, los símbolos del chrome de la UI
15//! saldrían como caritas de colores — una regresión visible, y en la dirección
16//! contraria a la que este arreglo viene a corregir.
17//!
18//! Lo que separa una cosa de la otra es la propiedad Unicode
19//! **`Emoji_Presentation`**, y no se puede deducir de las otras dos: es una
20//! tabla. Ésta.
21//!
22//! ## De dónde salen estos rangos
23//!
24//! Derivados de `emoji-test.txt` de **Unicode 16.0** (2024-08-14) con la regla
25//! que define UTS #51: en una línea `fully-qualified`, un emoji de **un solo
26//! codepoint** tiene `Emoji_Presentation = Yes`; si su forma completa lleva
27//! `U+FE0F` detrás, tiene `No`. Salieron 1.208 codepoints en 81 rangos, con
28//! **cero** solapamiento entre los dos conjuntos — que es el control de que la
29//! regla se aplicó bien.
30//!
31//! Se les suman a mano dos bloques de **componentes**, que nunca aparecen
32//! sueltos en ese archivo (siempre van dentro de una secuencia) pero se pintan
33//! a color: los indicadores regionales de las banderas (`U+1F1E6..=U+1F1FF`) y
34//! los modificadores de tono de piel (`U+1F3FB..=U+1F3FF`).
35//!
36//! Para regenerar con una versión nueva de Unicode, bajar `emoji-test.txt` y:
37//!
38//! ```text
39//! yes = {cp for una línea fully-qualified de UN codepoint}
40//! no  = {cp for una línea fully-qualified de (cp, FE0F)}
41//! assert not (yes & no)            # el control
42//! yes |= 1F1E6..=1F1FF | 1F3FB..=1F3FF
43//! ```
44//!
45//! El único costo de que esta tabla envejezca es que un emoji **nuevo** salga
46//! monocromo, que es exactamente lo que pasaba con todos antes de existir.
47
48/// Rangos (inclusivos) de codepoints con `Emoji_Presentation = Yes`.
49/// Ordenados y disjuntos: se buscan por bisección.
50const PRESENTACION_EMOJI: &[(u32, u32)] = &[
51    (0x231A, 0x231B),
52    (0x23E9, 0x23EC),
53    (0x23F0, 0x23F0),
54    (0x23F3, 0x23F3),
55    (0x25FD, 0x25FE),
56    (0x2614, 0x2615),
57    (0x2648, 0x2653),
58    (0x267F, 0x267F),
59    (0x2693, 0x2693),
60    (0x26A1, 0x26A1),
61    (0x26AA, 0x26AB),
62    (0x26BD, 0x26BE),
63    (0x26C4, 0x26C5),
64    (0x26CE, 0x26CE),
65    (0x26D4, 0x26D4),
66    (0x26EA, 0x26EA),
67    (0x26F2, 0x26F3),
68    (0x26F5, 0x26F5),
69    (0x26FA, 0x26FA),
70    (0x26FD, 0x26FD),
71    (0x2705, 0x2705),
72    (0x270A, 0x270B),
73    (0x2728, 0x2728),
74    (0x274C, 0x274C),
75    (0x274E, 0x274E),
76    (0x2753, 0x2755),
77    (0x2757, 0x2757),
78    (0x2795, 0x2797),
79    (0x27B0, 0x27B0),
80    (0x27BF, 0x27BF),
81    (0x2B1B, 0x2B1C),
82    (0x2B50, 0x2B50),
83    (0x2B55, 0x2B55),
84    (0x1F004, 0x1F004),
85    (0x1F0CF, 0x1F0CF),
86    (0x1F18E, 0x1F18E),
87    (0x1F191, 0x1F19A),
88    (0x1F1E6, 0x1F1FF),
89    (0x1F201, 0x1F201),
90    (0x1F21A, 0x1F21A),
91    (0x1F22F, 0x1F22F),
92    (0x1F232, 0x1F236),
93    (0x1F238, 0x1F23A),
94    (0x1F250, 0x1F251),
95    (0x1F300, 0x1F320),
96    (0x1F32D, 0x1F335),
97    (0x1F337, 0x1F37C),
98    (0x1F37E, 0x1F393),
99    (0x1F3A0, 0x1F3CA),
100    (0x1F3CF, 0x1F3D3),
101    (0x1F3E0, 0x1F3F0),
102    (0x1F3F4, 0x1F3F4),
103    (0x1F3F8, 0x1F43E),
104    (0x1F440, 0x1F440),
105    (0x1F442, 0x1F4FC),
106    (0x1F4FF, 0x1F53D),
107    (0x1F54B, 0x1F54E),
108    (0x1F550, 0x1F567),
109    (0x1F57A, 0x1F57A),
110    (0x1F595, 0x1F596),
111    (0x1F5A4, 0x1F5A4),
112    (0x1F5FB, 0x1F64F),
113    (0x1F680, 0x1F6C5),
114    (0x1F6CC, 0x1F6CC),
115    (0x1F6D0, 0x1F6D2),
116    (0x1F6D5, 0x1F6D7),
117    (0x1F6DC, 0x1F6DF),
118    (0x1F6EB, 0x1F6EC),
119    (0x1F6F4, 0x1F6FC),
120    (0x1F7E0, 0x1F7EB),
121    (0x1F7F0, 0x1F7F0),
122    (0x1F90C, 0x1F93A),
123    (0x1F93C, 0x1F945),
124    (0x1F947, 0x1F9AF),
125    (0x1F9B4, 0x1F9FF),
126    (0x1FA70, 0x1FA7C),
127    (0x1FA80, 0x1FA89),
128    (0x1FA8F, 0x1FAC6),
129    (0x1FACE, 0x1FADC),
130    (0x1FADF, 0x1FAE9),
131    (0x1FAF0, 0x1FAF8),];
132
133/// ¿Este carácter se pinta a color **por defecto**, sin que nadie se lo pida
134/// con un selector de variación?
135pub(crate) fn presentacion_emoji(ch: char) -> bool {
136    let cp = ch as u32;
137    PRESENTACION_EMOJI
138        .binary_search_by(|&(ini, fin)| {
139            if cp < ini {
140                core::cmp::Ordering::Greater
141            } else if cp > fin {
142                core::cmp::Ordering::Less
143            } else {
144                core::cmp::Ordering::Equal
145            }
146        })
147        .is_ok()
148}
149
150#[cfg(test)]
151mod tests {
152    use super::presentacion_emoji;
153
154    /// La tabla tiene que estar ordenada y sin solapes, o la bisección miente
155    /// en silencio: devolvería `false` para un codepoint que sí está.
156    #[test]
157    fn la_tabla_esta_ordenada_y_es_disjunta() {
158        let mut anterior = None;
159        for &(ini, fin) in super::PRESENTACION_EMOJI {
160            assert!(ini <= fin, "rango dado vuelta: {ini:04X}..{fin:04X}");
161            if let Some(prev_fin) = anterior {
162                assert!(
163                    ini > prev_fin + 1,
164                    "rango {ini:04X} pegado o solapado con el anterior que termina en {prev_fin:04X} \
165                     (si son contiguos hay que fundirlos, no listarlos aparte)"
166                );
167            }
168            anterior = Some(fin);
169        }
170    }
171
172    /// Los símbolos del chrome de la UI **no** son presentación emoji, y por
173    /// eso este arreglo no los toca. Son justo los que `extended_pictographic`
174    /// sí marca — o sea, el control negativo de por qué esta tabla existe.
175    #[test]
176    fn los_simbolos_del_chrome_no_van_a_la_fuente_de_emoji() {
177        for ch in ['⚠', '❤', '✔', '▶', '©', '‼', '☀', '⤴', '✎', '→', '●', '✓', '★'] {
178            assert!(
179                !presentacion_emoji(ch),
180                "{ch:?} (U+{:04X}) se iría a la fuente de emoji y saldría de colores",
181                ch as u32
182            );
183        }
184    }
185
186    /// Y los emoji de verdad sí, incluidos los del BMP —que es donde una
187    /// heurística por «está arriba de U+1F000» se habría equivocado.
188    #[test]
189    fn los_emoji_de_verdad_son_presentacion_emoji() {
190        for ch in ['😀', '👍', '⭐', '⚽', '✅', '❌', '⌚', '🇦', '🏽', '🎉', '🔥'] {
191            assert!(
192                presentacion_emoji(ch),
193                "{ch:?} (U+{:04X}) saldría monocromo",
194                ch as u32
195            );
196        }
197    }
198}