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