tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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}