gioser

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Definiciones

⎇ main llimphi-icons/src/app_icons.rs Rust · 915 líneas · 27.7 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! `app_icons` — iconos de marca, uno por dominio/app de tawasuyu.
2//!
3//! A diferencia del set canónico de [`crate::Icon`] (glifos genéricos de
4//! acción: file, save, search…), aquí vive **un glifo distintivo por app**.
5//! Cada app tiene su símbolo y su **color de marca** propios, pero todos
6//! comparten el mismo lenguaje visual:
7//!
8//! - **Mismo grid lógico 24×24**, origen top-left, eje Y hacia abajo.
9//! - **Stroke-based, sin fill**: trazos con `Join::Round` + `Cap::Round`.
10//! - **Geometría minimal**: reconocible al primer vistazo aún en 16×16.
11//! - **Aire de ~3 unidades** en los bordes para que respire dentro de un chip.
12//!
13//! La idea es que un dock/spotlight/menú pinte `app_icon_view(AppIcon::Pluma)`
14//! y obtenga el glifo de la pluma en su color de tinta, sin que la app tenga
15//! que cargar un PNG ni declarar su propia geometría.
16//!
17//! ```ignore
18//! use llimphi_icons::app_icons::{AppIcon, app_icon_view};
19//!
20//! // Resuelve desde el id del registro de apps:
21//! if let Some(icon) = AppIcon::from_app_id("cosmos") {
22//!     let chip = View::new(style).children(vec![app_icon_view(icon, 1.8)]);
23//! }
24//! ```
25
26use llimphi_compositor::llimphi_layout::taffy::{
27    prelude::{percent, Size, Style},
28    Position,
29};
30use alloc::format;
31use alloc::string::ToString;
32use alloc::string::String;
33use llimphi_compositor::peniko::kurbo::{Affine, BezPath, Cap, Join, Stroke};
34use llimphi_compositor::peniko::Color;
35use llimphi_compositor::View;
36
37/// Una app de tawasuyu con icono de marca. El identificador (`name`) coincide
38/// con el `id` del `AppEntry` en `app-bus`.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum AppIcon {
41    // --- 00_unanchay · PERCIBIR ---
42    Chaka,
43    Khipu,
44    Pineal,
45    Pluma,
46    Puriy,
47    Rimay,
48    // --- 01_yachay · CONOCER ---
49    Cosmos,
50    Dominium,
51    Iniy,
52    Nakui,
53    Tinkuy,
54    // --- 02_ruway · HACER ---
55    Suilad,
56    Cards,
57    Chasqui,
58    Llimphi,
59    Media,
60    Mirada,
61    Nada,
62    Nahual,
63    Shuma,
64    Supay,
65    Takiy,
66    Tullpu,
67    Wawa,
68    Hapiy,
69    Raymi,
70    // --- 03_ukupacha · RAÍZ ---
71    Agora,
72    Arje,
73    Minga,
74    Sandokan,
75    WawaExplorer,
76}
77
78/// Las 29 apps, en orden de cuadrante. Útil para iterar (galerías, tests).
79pub const ALL: [AppIcon; 31] = [
80    AppIcon::Chaka,
81    AppIcon::Khipu,
82    AppIcon::Pineal,
83    AppIcon::Pluma,
84    AppIcon::Puriy,
85    AppIcon::Rimay,
86    AppIcon::Cosmos,
87    AppIcon::Dominium,
88    AppIcon::Iniy,
89    AppIcon::Nakui,
90    AppIcon::Tinkuy,
91    AppIcon::Suilad,
92    AppIcon::Cards,
93    AppIcon::Chasqui,
94    AppIcon::Llimphi,
95    AppIcon::Media,
96    AppIcon::Mirada,
97    AppIcon::Nada,
98    AppIcon::Nahual,
99    AppIcon::Shuma,
100    AppIcon::Supay,
101    AppIcon::Takiy,
102    AppIcon::Tullpu,
103    AppIcon::Wawa,
104    AppIcon::Hapiy,
105    AppIcon::Raymi,
106    AppIcon::Agora,
107    AppIcon::Arje,
108    AppIcon::Minga,
109    AppIcon::Sandokan,
110    AppIcon::WawaExplorer,
111];
112
113impl AppIcon {
114    /// Id estable de la app (coincide con `AppEntry.id` / nombre del dominio).
115    pub const fn name(self) -> &'static str {
116        match self {
117            AppIcon::Chaka => "chaka",
118            AppIcon::Khipu => "khipu",
119            AppIcon::Pineal => "pineal",
120            AppIcon::Pluma => "pluma",
121            AppIcon::Puriy => "puriy",
122            AppIcon::Rimay => "rimay",
123            AppIcon::Cosmos => "cosmos",
124            AppIcon::Dominium => "dominium",
125            AppIcon::Iniy => "iniy",
126            AppIcon::Nakui => "nakui",
127            AppIcon::Tinkuy => "tinkuy",
128            AppIcon::Suilad => "suilad",
129            AppIcon::Cards => "cards",
130            AppIcon::Chasqui => "chasqui",
131            AppIcon::Llimphi => "llimphi",
132            AppIcon::Media => "media",
133            AppIcon::Mirada => "mirada",
134            AppIcon::Nada => "nada",
135            AppIcon::Nahual => "nahual",
136            AppIcon::Shuma => "shuma",
137            AppIcon::Supay => "supay",
138            AppIcon::Takiy => "takiy",
139            AppIcon::Tullpu => "tullpu",
140            AppIcon::Wawa => "wawa",
141            AppIcon::Hapiy => "hapiy",
142            AppIcon::Raymi => "raymi",
143            AppIcon::Agora => "agora",
144            AppIcon::Arje => "arje",
145            AppIcon::Minga => "minga",
146            AppIcon::Sandokan => "sandokan",
147            AppIcon::WawaExplorer => "wawa-explorer",
148        }
149    }
150
151    /// Resuelve una app desde su `id` del registro. Acepta tanto
152    /// `"wawa-explorer"` como `"wawa_explorer"`.
153    pub fn from_app_id(id: &str) -> Option<AppIcon> {
154        let id = id.trim().to_ascii_lowercase();
155        let id = id.replace('_', "-");
156        ALL.into_iter().find(|a| a.name() == id)
157    }
158
159    /// Color de marca de la app — el que el dock/menú debería usar para
160    /// pintar el glifo por default.
161    pub const fn brand(self) -> Color {
162        let (r, g, b) = match self {
163            AppIcon::Chaka => (43, 166, 164),
164            AppIcon::Khipu => (181, 101, 29),
165            AppIcon::Pineal => (108, 79, 216),
166            AppIcon::Pluma => (61, 59, 142),
167            AppIcon::Puriy => (63, 163, 77),
168            AppIcon::Rimay => (232, 131, 58),
169            AppIcon::Cosmos => (230, 184, 0),
170            AppIcon::Dominium => (74, 111, 165),
171            AppIcon::Iniy => (124, 179, 66),
172            AppIcon::Nakui => (194, 84, 157),
173            AppIcon::Tinkuy => (217, 83, 79),
174            AppIcon::Suilad => (42, 168, 196),
175            AppIcon::Cards => (142, 99, 206),
176            AppIcon::Chasqui => (52, 179, 106),
177            AppIcon::Llimphi => (229, 91, 122),
178            AppIcon::Media => (226, 62, 87),
179            AppIcon::Mirada => (45, 125, 210),
180            AppIcon::Nada => (136, 147, 160),
181            AppIcon::Nahual => (124, 77, 191),
182            AppIcon::Shuma => (224, 165, 38),
183            AppIcon::Supay => (155, 63, 181),
184            AppIcon::Takiy => (229, 99, 155),
185            AppIcon::Tullpu => (224, 96, 58),
186            AppIcon::Wawa => (91, 141, 239),
187            AppIcon::Hapiy => (38, 184, 156),
188            AppIcon::Raymi => (216, 88, 120),
189            AppIcon::Agora => (47, 158, 143),
190            AppIcon::Arje => (176, 141, 87),
191            AppIcon::Minga => (224, 123, 57),
192            AppIcon::Sandokan => (192, 57, 43),
193            AppIcon::WawaExplorer => (110, 160, 240),
194        };
195        Color::from_rgb8(r, g, b)
196    }
197
198    /// `BezPath` del glifo en coords del grid 24×24.
199    pub fn path(self) -> BezPath {
200        match self {
201            AppIcon::Chaka => path_chaka(),
202            AppIcon::Khipu => path_khipu(),
203            AppIcon::Pineal => path_pineal(),
204            AppIcon::Pluma => path_pluma(),
205            AppIcon::Puriy => path_puriy(),
206            AppIcon::Rimay => path_rimay(),
207            AppIcon::Cosmos => path_cosmos(),
208            AppIcon::Dominium => path_dominium(),
209            AppIcon::Iniy => path_iniy(),
210            AppIcon::Nakui => path_nakui(),
211            AppIcon::Tinkuy => path_tinkuy(),
212            AppIcon::Suilad => path_suilad(),
213            AppIcon::Cards => path_cards(),
214            AppIcon::Chasqui => path_chasqui(),
215            AppIcon::Llimphi => path_llimphi(),
216            AppIcon::Media => path_media(),
217            AppIcon::Mirada => path_mirada(),
218            AppIcon::Nada => path_nada(),
219            AppIcon::Nahual => path_nahual(),
220            AppIcon::Shuma => path_shuma(),
221            AppIcon::Supay => path_supay(),
222            AppIcon::Takiy => path_takiy(),
223            AppIcon::Tullpu => path_tullpu(),
224            AppIcon::Wawa => path_wawa(),
225            AppIcon::Hapiy => path_hapiy(),
226            AppIcon::Raymi => path_raymi(),
227            AppIcon::Agora => path_agora(),
228            AppIcon::Arje => path_arje(),
229            AppIcon::Minga => path_minga(),
230            AppIcon::Sandokan => path_sandokan(),
231            AppIcon::WawaExplorer => path_wawa_explorer(),
232        }
233    }
234}
235
236/// `View` que pinta el icono de app en su **color de marca**, ocupando todo
237/// el rect del padre, escalado uniforme y centrado.
238///
239/// - `stroke_width` en unidades del grid 24×24 (típico de marca: `1.8`).
240pub fn app_icon_view<Msg: Clone + 'static>(icon: AppIcon, stroke_width: f32) -> View<Msg> {
241    app_icon_view_colored(icon, icon.brand(), stroke_width)
242}
243
244/// Igual que [`app_icon_view`] pero forzando un color (p.ej. monocromo
245/// `theme.fg_text` para un menú denso donde el color distrae).
246pub fn app_icon_view_colored<Msg: Clone + 'static>(
247    icon: AppIcon,
248    color: Color,
249    stroke_width: f32,
250) -> View<Msg> {
251    View::new(Style {
252        position: Position::Absolute,
253        size: Size {
254            width: percent(1.0_f32),
255            height: percent(1.0_f32),
256        },
257        ..Default::default()
258    })
259    .paint_with(move |scene, _ts, rect| {
260        paint_app_icon(scene, rect, icon, color, stroke_width);
261    })
262}
263
264/// Exporta el icono de app como **SVG** (string), en su color de marca.
265/// Mismo dibujo que [`app_icon_view`] pero en un formato de archivo: sirve para
266/// los `.desktop` (freedesktop `scalable/apps/<id>.svg`), la web, o cualquier
267/// consumidor que no renderice con Llimphi. El path es stroke-only con remates
268/// redondos, igual que el pintor vectorial. `viewBox 0 0 24 24` (la grilla).
269// GATEADO A `std`: `BezPath::to_svg` vive en el módulo `svg` de kurbo, que es
270// std-only. El consumidor de este export son los `.desktop` y la web — mundos
271// que en una jaula o en el kernel no existen.
272#[cfg(feature = "std")]
273pub fn app_icon_svg(icon: AppIcon, stroke_width: f32) -> String {
274    let d = icon.path().to_svg();
275    let [r, g, b, _] = icon.brand().components;
276    let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
277    let hex = format!("#{:02x}{:02x}{:02x}", q(r), q(g), q(b));
278    format!(
279        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" \
280fill=\"none\" stroke=\"{hex}\" stroke-width=\"{sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\
281<path d=\"{d}\"/></svg>\n",
282        sw = fmt_num(stroke_width),
283    )
284}
285
286/// Formatea un f32 sin ceros de cola (`1.8` no `1.8000`).
287fn fmt_num(v: f32) -> String {
288    let s = format!("{v:.3}");
289    let s = s.trim_end_matches('0').trim_end_matches('.');
290    s.to_string()
291}
292
293/// Pintor crudo — para stampear varios iconos de app dentro del mismo
294/// `paint_with` (una grilla de launcher, por ejemplo).
295pub fn paint_app_icon(
296    scene: &mut llimphi_compositor::Lienzo,
297    rect: llimphi_compositor::PaintRect,
298    icon: AppIcon,
299    color: Color,
300    stroke_width: f32,
301) {
302    let side = rect.w.min(rect.h) as f64;
303    if side <= 0.0 {
304        return;
305    }
306    let scale = side / 24.0;
307    let tx = rect.x as f64 + (rect.w as f64 - side) * 0.5;
308    let ty = rect.y as f64 + (rect.h as f64 - side) * 0.5;
309    let xform = Affine::translate((tx, ty)) * Affine::scale(scale);
310
311    let stroke = Stroke::new(stroke_width as f64)
312        .with_join(Join::Round)
313        .with_caps(Cap::Round);
314    let path = icon.path();
315    scene.stroke(&stroke, xform, color, None, &path);
316}
317
318// =====================================================================
319// Helpers
320// =====================================================================
321
322/// Círculo aproximado con `segments` lados rectos (liso por el Cap::Round).
323fn circle(cx: f64, cy: f64, r: f64, segments: usize) -> BezPath {
324    let mut p = BezPath::new();
325    for i in 0..=segments {
326        let theta = core::f64::consts::TAU * (i as f64) / (segments as f64);
327        let x = cx + r * theta.cos();
328        let y = cy + r * theta.sin();
329        if i == 0 {
330            p.move_to((x, y));
331        } else {
332            p.line_to((x, y));
333        }
334    }
335    p
336}
337
338/// Empuja todos los elementos de `src` dentro de `dst` (para componer
339/// glifos hechos de varias subformas).
340fn push_all(dst: &mut BezPath, src: BezPath) {
341    for el in src.elements() {
342        dst.push(*el);
343    }
344}
345
346// =====================================================================
347// Glifos — uno por app. Grid 24×24, margen ~3.
348// =====================================================================
349
350// --- 00_unanchay · PERCIBIR ---
351
352fn path_chaka() -> BezPath {
353    // chaka = puente: tablero recto + arco + dos pilotes.
354    let mut p = BezPath::new();
355    // Tablero.
356    p.move_to((3.0, 9.0));
357    p.line_to((21.0, 9.0));
358    // Arco bajo el tablero.
359    p.move_to((5.0, 18.0));
360    p.curve_to((5.0, 11.0), (19.0, 11.0), (19.0, 18.0));
361    // Pilotes que conectan tablero y arco.
362    p.move_to((9.0, 9.0));
363    p.line_to((9.0, 12.5));
364    p.move_to((15.0, 9.0));
365    p.line_to((15.0, 12.5));
366    p
367}
368
369fn path_khipu() -> BezPath {
370    // khipu: cordón principal + tres ramales con nudos (puntos).
371    let mut p = BezPath::new();
372    // Cordón superior.
373    p.move_to((4.0, 6.0));
374    p.line_to((20.0, 6.0));
375    // Ramales.
376    p.move_to((7.0, 6.0));
377    p.line_to((7.0, 19.0));
378    p.move_to((12.0, 6.0));
379    p.line_to((12.0, 20.0));
380    p.move_to((17.0, 6.0));
381    p.line_to((17.0, 18.0));
382    // Nudos.
383    push_all(&mut p, circle(7.0, 12.0, 1.3, 10));
384    push_all(&mut p, circle(12.0, 10.0, 1.3, 10));
385    push_all(&mut p, circle(12.0, 16.0, 1.3, 10));
386    push_all(&mut p, circle(17.0, 11.0, 1.3, 10));
387    p
388}
389
390fn path_pineal() -> BezPath {
391    // pineal = tercer ojo: párpado almendrado + iris + antena/rayo arriba.
392    let mut p = BezPath::new();
393    p.move_to((4.0, 12.0));
394    p.curve_to((8.0, 7.0), (16.0, 7.0), (20.0, 12.0));
395    p.curve_to((16.0, 17.0), (8.0, 17.0), (4.0, 12.0));
396    push_all(&mut p, circle(12.0, 12.0, 2.6, 14));
397    p.move_to((12.0, 3.0));
398    p.line_to((12.0, 5.5));
399    p
400}
401
402fn path_pluma() -> BezPath {
403    // pluma = plumín: rombo apuntando abajo + ranura + ojal.
404    let mut p = BezPath::new();
405    p.move_to((12.0, 3.0));
406    p.line_to((16.0, 9.0));
407    p.line_to((13.5, 20.0));
408    p.line_to((10.5, 20.0));
409    p.line_to((8.0, 9.0));
410    p.close_path();
411    // Ranura.
412    p.move_to((12.0, 11.5));
413    p.line_to((12.0, 19.0));
414    // Ojal.
415    push_all(&mut p, circle(12.0, 9.5, 1.2, 10));
416    p
417}
418
419fn path_puriy() -> BezPath {
420    // puriy = caminar/recorrido: senda curva ascendente con flecha.
421    let mut p = BezPath::new();
422    p.move_to((6.0, 20.0));
423    p.curve_to((6.0, 12.0), (18.0, 12.0), (18.0, 4.0));
424    // Cabeza de flecha.
425    p.move_to((15.0, 6.0));
426    p.line_to((18.0, 4.0));
427    p.line_to((20.5, 6.5));
428    p
429}
430
431fn path_rimay() -> BezPath {
432    // rimay = palabra/habla: globo de diálogo con cola + dos renglones.
433    let mut p = BezPath::new();
434    p.move_to((4.0, 6.0));
435    p.line_to((20.0, 6.0));
436    p.line_to((20.0, 15.0));
437    p.line_to((11.0, 15.0));
438    p.line_to((8.0, 19.0));
439    p.line_to((8.0, 15.0));
440    p.line_to((4.0, 15.0));
441    p.close_path();
442    // Renglones.
443    p.move_to((8.0, 9.5));
444    p.line_to((16.0, 9.5));
445    p.move_to((8.0, 12.0));
446    p.line_to((13.0, 12.0));
447    p
448}
449
450// --- 01_yachay · CONOCER ---
451
452fn path_cosmos() -> BezPath {
453    // cosmos = destello de 4 puntas + dos estrellas pequeñas.
454    let mut p = BezPath::new();
455    p.move_to((12.0, 4.0));
456    p.line_to((13.4, 10.6));
457    p.line_to((20.0, 12.0));
458    p.line_to((13.4, 13.4));
459    p.line_to((12.0, 20.0));
460    p.line_to((10.6, 13.4));
461    p.line_to((4.0, 12.0));
462    p.line_to((10.6, 10.6));
463    p.close_path();
464    // Estrellas chicas.
465    push_all(&mut p, circle(19.0, 6.0, 0.8, 8));
466    push_all(&mut p, circle(5.5, 18.0, 0.8, 8));
467    p
468}
469
470fn path_dominium() -> BezPath {
471    // dominium = ERP/libro mayor: barras de distinta altura sobre una base.
472    let mut p = BezPath::new();
473    // Base.
474    p.move_to((3.0, 20.0));
475    p.line_to((21.0, 20.0));
476    // Columnas.
477    p.move_to((6.0, 14.0));
478    p.line_to((9.0, 14.0));
479    p.line_to((9.0, 20.0));
480    p.line_to((6.0, 20.0));
481    p.close_path();
482    p.move_to((10.5, 8.0));
483    p.line_to((13.5, 8.0));
484    p.line_to((13.5, 20.0));
485    p.line_to((10.5, 20.0));
486    p.close_path();
487    p.move_to((15.0, 11.0));
488    p.line_to((18.0, 11.0));
489    p.line_to((18.0, 20.0));
490    p.line_to((15.0, 20.0));
491    p.close_path();
492    p
493}
494
495fn path_iniy() -> BezPath {
496    // iniy = aliento/creer: brote con tallo y dos hojas.
497    let mut p = BezPath::new();
498    // Tallo.
499    p.move_to((12.0, 20.0));
500    p.line_to((12.0, 10.0));
501    // Hoja izquierda.
502    p.move_to((12.0, 14.0));
503    p.curve_to((8.0, 14.0), (6.0, 11.0), (7.0, 8.0));
504    p.curve_to((10.0, 9.0), (12.0, 11.0), (12.0, 14.0));
505    // Hoja derecha.
506    p.move_to((12.0, 12.0));
507    p.curve_to((15.5, 12.0), (17.0, 9.0), (16.5, 6.0));
508    p.curve_to((14.0, 7.0), (12.0, 9.0), (12.0, 12.0));
509    p
510}
511
512fn path_nakui() -> BezPath {
513    // nakui = grafo de morfismos: tres nodos + aristas.
514    let mut p = BezPath::new();
515    // Aristas (primero, para que queden bajo los nodos).
516    p.move_to((7.5, 9.0));
517    p.line_to((16.5, 9.0));
518    p.move_to((7.5, 9.8));
519    p.line_to((10.8, 16.0));
520    p.move_to((16.5, 9.8));
521    p.line_to((13.2, 16.0));
522    // Nodos.
523    push_all(&mut p, circle(6.0, 8.0, 2.2, 14));
524    push_all(&mut p, circle(18.0, 8.0, 2.2, 14));
525    push_all(&mut p, circle(12.0, 18.0, 2.2, 14));
526    p
527}
528
529fn path_tinkuy() -> BezPath {
530    // tinkuy = encuentro/choque: dos flechas que convergen + chispa.
531    let mut p = BezPath::new();
532    // Flecha izquierda →
533    p.move_to((3.0, 12.0));
534    p.line_to((9.5, 12.0));
535    p.move_to((7.5, 10.0));
536    p.line_to((9.5, 12.0));
537    p.line_to((7.5, 14.0));
538    // Flecha derecha ←
539    p.move_to((21.0, 12.0));
540    p.line_to((14.5, 12.0));
541    p.move_to((16.5, 10.0));
542    p.line_to((14.5, 12.0));
543    p.line_to((16.5, 14.0));
544    // Chispa central.
545    push_all(&mut p, circle(12.0, 12.0, 1.6, 10));
546    p
547}
548
549// --- 02_ruway · HACER ---
550
551fn path_suilad() -> BezPath {
552    // suilad = saludo mutuo (élfico); ciclo de reciprocidad: dos flechas curvas en ciclo.
553    let mut p = BezPath::new();
554    // Arco superior, flecha hacia la derecha-abajo.
555    p.move_to((6.0, 8.0));
556    p.curve_to((9.0, 4.0), (15.0, 4.0), (18.0, 8.5));
557    p.move_to((15.5, 8.0));
558    p.line_to((18.0, 8.5));
559    p.line_to((18.5, 5.8));
560    // Arco inferior, flecha hacia la izquierda-arriba.
561    p.move_to((18.0, 16.0));
562    p.curve_to((15.0, 20.0), (9.0, 20.0), (6.0, 15.5));
563    p.move_to((8.5, 16.0));
564    p.line_to((6.0, 15.5));
565    p.line_to((5.5, 18.2));
566    p
567}
568
569fn path_cards() -> BezPath {
570    // cards = naipes apilados: carta frontal + borde de la de atrás.
571    let mut p = BezPath::new();
572    // Carta de atrás (asoma arriba y a la derecha).
573    p.move_to((8.0, 5.0));
574    p.line_to((19.0, 5.0));
575    p.line_to((19.0, 16.0));
576    // Carta frontal.
577    p.move_to((5.0, 9.0));
578    p.line_to((15.0, 9.0));
579    p.line_to((15.0, 20.0));
580    p.line_to((5.0, 20.0));
581    p.close_path();
582    p
583}
584
585fn path_chasqui() -> BezPath {
586    // chasqui = mensajero: avión de papel.
587    let mut p = BezPath::new();
588    p.move_to((4.0, 11.0));
589    p.line_to((20.0, 4.0));
590    p.line_to((13.0, 20.0));
591    p.line_to((11.0, 13.0));
592    p.close_path();
593    // Pliegue central.
594    p.move_to((11.0, 13.0));
595    p.line_to((20.0, 4.0));
596    p
597}
598
599fn path_llimphi() -> BezPath {
600    // llimphi = pintura/color: paleta con apoyo para el pulgar + 3 gotas.
601    let mut p = BezPath::new();
602    p.move_to((4.0, 12.0));
603    p.curve_to((4.0, 6.0), (11.0, 4.0), (15.0, 5.0));
604    p.curve_to((20.0, 6.5), (21.0, 12.0), (18.0, 15.0));
605    p.curve_to((16.0, 16.5), (16.5, 13.5), (14.0, 14.0));
606    p.curve_to((11.5, 14.5), (12.5, 18.0), (9.0, 18.0));
607    p.curve_to((5.5, 18.0), (4.0, 15.0), (4.0, 12.0));
608    p.close_path();
609    // Gotas de pintura.
610    push_all(&mut p, circle(8.0, 9.0, 1.1, 10));
611    push_all(&mut p, circle(12.0, 8.0, 1.1, 10));
612    push_all(&mut p, circle(15.5, 10.0, 1.1, 10));
613    p
614}
615
616fn path_media() -> BezPath {
617    // media = reproducción: marco + triángulo de play.
618    let mut p = BezPath::new();
619    p.move_to((4.0, 6.0));
620    p.line_to((20.0, 6.0));
621    p.line_to((20.0, 18.0));
622    p.line_to((4.0, 18.0));
623    p.close_path();
624    // Play.
625    p.move_to((10.0, 9.0));
626    p.line_to((10.0, 15.0));
627    p.line_to((16.0, 12.0));
628    p.close_path();
629    p
630}
631
632fn path_mirada() -> BezPath {
633    // mirada = ojo: párpado + iris + pupila.
634    let mut p = BezPath::new();
635    p.move_to((3.0, 12.0));
636    p.curve_to((8.0, 6.0), (16.0, 6.0), (21.0, 12.0));
637    p.curve_to((16.0, 18.0), (8.0, 18.0), (3.0, 12.0));
638    p.close_path();
639    push_all(&mut p, circle(12.0, 12.0, 3.4, 18));
640    push_all(&mut p, circle(12.0, 12.0, 1.0, 8));
641    p
642}
643
644fn path_nada() -> BezPath {
645    // nada = vacío: conjunto vacío ∅ (anillo + diagonal).
646    let mut p = circle(12.0, 12.0, 8.0, 28);
647    p.move_to((6.5, 17.5));
648    p.line_to((17.5, 6.5));
649    p
650}
651
652fn path_nahual() -> BezPath {
653    // nahual = máscara/mutación de forma: antifaz con dos ojos.
654    let mut p = BezPath::new();
655    p.move_to((4.0, 9.0));
656    p.curve_to((4.0, 6.5), (8.0, 6.0), (10.0, 7.5));
657    p.curve_to((11.0, 8.2), (13.0, 8.2), (14.0, 7.5));
658    p.curve_to((16.0, 6.0), (20.0, 6.5), (20.0, 9.0));
659    p.curve_to((20.0, 13.5), (16.0, 16.5), (12.0, 15.5));
660    p.curve_to((8.0, 16.5), (4.0, 13.5), (4.0, 9.0));
661    p.close_path();
662    push_all(&mut p, circle(9.0, 10.0, 1.3, 10));
663    push_all(&mut p, circle(15.0, 10.0, 1.3, 10));
664    p
665}
666
667fn path_shuma() -> BezPath {
668    // shuma = discernir: embudo/filtro.
669    let mut p = BezPath::new();
670    p.move_to((4.0, 6.0));
671    p.line_to((20.0, 6.0));
672    p.line_to((13.0, 14.0));
673    p.line_to((13.0, 19.0));
674    p.line_to((11.0, 20.0));
675    p.line_to((11.0, 14.0));
676    p.close_path();
677    p
678}
679
680fn path_supay() -> BezPath {
681    // supay = espíritu del ukhupacha: llama doble.
682    let mut p = BezPath::new();
683    // Llama exterior.
684    p.move_to((12.0, 3.0));
685    p.curve_to((17.0, 9.0), (16.0, 14.0), (12.0, 21.0));
686    p.curve_to((8.0, 14.0), (7.0, 9.0), (12.0, 3.0));
687    p.close_path();
688    // Llama interior.
689    p.move_to((12.0, 9.0));
690    p.curve_to((14.0, 12.0), (13.0, 16.0), (12.0, 18.0));
691    p.curve_to((11.0, 16.0), (10.0, 12.0), (12.0, 9.0));
692    p.close_path();
693    p
694}
695
696fn path_takiy() -> BezPath {
697    // takiy = cantar: corchea + ondas de sonido.
698    let mut p = BezPath::new();
699    // Cabeza de nota.
700    push_all(&mut p, circle(8.0, 18.0, 2.4, 16));
701    // Plica.
702    p.move_to((10.4, 18.0));
703    p.line_to((10.4, 6.0));
704    // Banderola.
705    p.move_to((10.4, 6.0));
706    p.curve_to((13.5, 7.0), (14.5, 9.0), (13.5, 11.0));
707    // Ondas.
708    p.move_to((16.0, 9.0));
709    p.curve_to((18.0, 11.0), (18.0, 13.0), (16.0, 15.0));
710    p
711}
712
713fn path_tullpu() -> BezPath {
714    // tullpu = tinte/color: tres gotas.
715    let mut p = BezPath::new();
716    // Gota 1.
717    p.move_to((8.0, 5.0));
718    p.curve_to((11.0, 9.0), (11.0, 11.0), (8.0, 12.0));
719    p.curve_to((5.0, 11.0), (5.0, 9.0), (8.0, 5.0));
720    p.close_path();
721    // Gota 2.
722    p.move_to((16.0, 6.0));
723    p.curve_to((19.0, 10.0), (19.0, 12.0), (16.0, 13.0));
724    p.curve_to((13.0, 12.0), (13.0, 10.0), (16.0, 6.0));
725    p.close_path();
726    // Gota 3.
727    p.move_to((12.0, 13.0));
728    p.curve_to((15.0, 17.0), (15.0, 19.0), (12.0, 20.0));
729    p.curve_to((9.0, 19.0), (9.0, 17.0), (12.0, 13.0));
730    p.close_path();
731    p
732}
733
734fn path_wawa() -> BezPath {
735    // wawa = célula/semilla (el SO en gestación): membrana + núcleo.
736    let mut p = circle(12.0, 12.0, 8.0, 28);
737    push_all(&mut p, circle(12.0, 12.0, 3.0, 16));
738    p
739}
740
741// --- 03_ukupacha · RAÍZ ---
742
743fn path_agora() -> BezPath {
744    // agora = firma/confianza: escudo con check.
745    let mut p = BezPath::new();
746    p.move_to((12.0, 3.0));
747    p.line_to((20.0, 6.0));
748    p.line_to((20.0, 12.0));
749    p.curve_to((20.0, 17.0), (16.0, 20.0), (12.0, 21.0));
750    p.curve_to((8.0, 20.0), (4.0, 17.0), (4.0, 12.0));
751    p.line_to((4.0, 6.0));
752    p.close_path();
753    // Check.
754    p.move_to((8.5, 12.0));
755    p.line_to((11.0, 14.5));
756    p.line_to((16.0, 8.5));
757    p
758}
759
760fn path_arje() -> BezPath {
761    // arje = arché/raíz de confianza: ancla.
762    let mut p = BezPath::new();
763    // Anillo.
764    push_all(&mut p, circle(12.0, 5.0, 2.2, 14));
765    // Caña.
766    p.move_to((12.0, 7.2));
767    p.line_to((12.0, 19.0));
768    // Travesaño.
769    p.move_to((8.0, 10.0));
770    p.line_to((16.0, 10.0));
771    // Uñas/brazos.
772    p.move_to((6.0, 14.0));
773    p.curve_to((6.0, 18.5), (9.0, 20.0), (12.0, 20.0));
774    p.move_to((18.0, 14.0));
775    p.curve_to((18.0, 18.5), (15.0, 20.0), (12.0, 20.0));
776    p
777}
778
779fn path_minga() -> BezPath {
780    // minga = trabajo comunal: tres figuras.
781    let mut p = BezPath::new();
782    // Figura central.
783    push_all(&mut p, circle(12.0, 7.0, 2.2, 14));
784    p.move_to((8.0, 18.0));
785    p.curve_to((8.0, 13.0), (16.0, 13.0), (16.0, 18.0));
786    // Figura izquierda.
787    push_all(&mut p, circle(5.5, 10.0, 1.6, 12));
788    p.move_to((2.5, 18.0));
789    p.curve_to((2.5, 14.5), (6.0, 13.5), (7.5, 15.0));
790    // Figura derecha.
791    push_all(&mut p, circle(18.5, 10.0, 1.6, 12));
792    p.move_to((21.5, 18.0));
793    p.curve_to((21.5, 14.5), (18.0, 13.5), (16.5, 15.0));
794    p
795}
796
797fn path_sandokan() -> BezPath {
798    // sandokan = caja/contenedor aislado: cubo isométrico.
799    let mut p = BezPath::new();
800    // Cara frontal.
801    p.move_to((5.0, 8.0));
802    p.line_to((14.0, 8.0));
803    p.line_to((14.0, 18.0));
804    p.line_to((5.0, 18.0));
805    p.close_path();
806    // Tapa.
807    p.move_to((5.0, 8.0));
808    p.line_to((9.0, 4.0));
809    p.line_to((18.0, 4.0));
810    p.line_to((14.0, 8.0));
811    // Cara lateral.
812    p.move_to((14.0, 8.0));
813    p.line_to((18.0, 4.0));
814    p.line_to((18.0, 14.0));
815    p.line_to((14.0, 18.0));
816    p
817}
818
819fn path_hapiy() -> BezPath {
820    // hapiy = asir/capturar: marco de cuatro esquinas (encuadre de captura).
821    let mut p = BezPath::new();
822    p.move_to((4.0, 8.0));
823    p.line_to((4.0, 4.0));
824    p.line_to((8.0, 4.0)); // sup-izq
825    p.move_to((16.0, 4.0));
826    p.line_to((20.0, 4.0));
827    p.line_to((20.0, 8.0)); // sup-der
828    p.move_to((20.0, 16.0));
829    p.line_to((20.0, 20.0));
830    p.line_to((16.0, 20.0)); // inf-der
831    p.move_to((8.0, 20.0));
832    p.line_to((4.0, 20.0));
833    p.line_to((4.0, 16.0)); // inf-izq
834    p
835}
836
837fn path_raymi() -> BezPath {
838    // raymi = fiesta/calendario: hoja con anillas, cabecera y día marcado.
839    let mut p = BezPath::new();
840    p.move_to((4.0, 6.0));
841    p.line_to((20.0, 6.0));
842    p.line_to((20.0, 20.0));
843    p.line_to((4.0, 20.0));
844    p.close_path();
845    p.move_to((4.0, 10.0));
846    p.line_to((20.0, 10.0)); // cabecera
847    p.move_to((8.0, 4.0));
848    p.line_to((8.0, 7.5)); // anilla izq
849    p.move_to((16.0, 4.0));
850    p.line_to((16.0, 7.5)); // anilla der
851    push_all(&mut p, circle(12.0, 15.0, 1.8, 12)); // día marcado
852    p
853}
854
855fn path_wawa_explorer() -> BezPath {
856    // wawa-explorer = launchpad: grilla 2×2.
857    let mut p = BezPath::new();
858    for (x, y) in &[(5.0, 5.0), (13.0, 5.0), (5.0, 13.0), (13.0, 13.0)] {
859        p.move_to((*x, *y));
860        p.line_to((*x + 6.0, *y));
861        p.line_to((*x + 6.0, *y + 6.0));
862        p.line_to((*x, *y + 6.0));
863        p.close_path();
864    }
865    p
866}
867
868#[cfg(test)]
869mod tests {
870    use alloc::vec::Vec;
871
872    use super::*;
873
874    #[test]
875    fn all_app_icons_have_nonempty_path() {
876        for icon in ALL {
877            let p = icon.path();
878            assert!(
879                p.elements().len() > 0,
880                "icono de app {} produjo path vacío",
881                icon.name()
882            );
883        }
884    }
885
886    #[test]
887    fn svg_export_es_valido() {
888        for icon in ALL {
889            let svg = app_icon_svg(icon, 1.8);
890            assert!(svg.starts_with("<svg"), "{} svg sin cabecera", icon.name());
891            assert!(svg.contains("<path d=\"M"), "{} svg sin path", icon.name());
892            assert!(svg.contains("stroke=\"#"), "{} svg sin color de marca", icon.name());
893            assert!(svg.trim_end().ends_with("</svg>"), "{} svg sin cierre", icon.name());
894        }
895    }
896
897    #[test]
898    fn app_names_are_unique() {
899        let mut names: Vec<&str> = ALL.iter().map(|i| i.name()).collect();
900        let n = names.len();
901        names.sort();
902        names.dedup();
903        assert_eq!(names.len(), n, "nombres duplicados en AppIcon::name()");
904    }
905
906    #[test]
907    fn from_app_id_roundtrips() {
908        for icon in ALL {
909            assert_eq!(AppIcon::from_app_id(icon.name()), Some(icon));
910        }
911        // Tolera underscores y mayúsculas.
912        assert_eq!(AppIcon::from_app_id("WAWA_EXPLORER"), Some(AppIcon::WawaExplorer));
913        assert_eq!(AppIcon::from_app_id("desconocida"), None);
914    }
915}