//! `app_icons` — iconos de marca, uno por dominio/app de gioser. //! //! A diferencia del set canónico de [`crate::Icon`] (glifos genéricos de //! acción: file, save, search…), acá vive **un glifo distintivo por app**. //! Cada app tiene su símbolo y su **color de marca** propios, pero todos //! comparten el mismo lenguaje visual: //! //! - **Mismo grid lógico 24×24**, origen top-left, eje Y hacia abajo. //! - **Stroke-based, sin fill**: trazos con `Join::Round` + `Cap::Round`. //! - **Geometría minimal**: reconocible al primer vistazo aún en 16×16. //! - **Aire de ~3 unidades** en los bordes para que respire dentro de un chip. //! //! La idea es que un dock/spotlight/menú pinte `app_icon_view(AppIcon::Pluma)` //! y obtenga el glifo de la pluma en su color de tinta, sin que la app tenga //! que cargar un PNG ni declarar su propia geometría. //! //! ```ignore //! use llimphi_icons::app_icons::{AppIcon, app_icon_view}; //! //! // Resuelve desde el id del registro de apps: //! if let Some(icon) = AppIcon::from_app_id("cosmos") { //! let chip = View::new(style).children(vec![app_icon_view(icon, 1.8)]); //! } //! ``` use llimphi_ui::llimphi_layout::taffy::{ prelude::{percent, Size, Style}, Position, }; use llimphi_ui::llimphi_raster::kurbo::{Affine, BezPath, Cap, Join, Stroke}; use llimphi_ui::llimphi_raster::peniko::Color; use llimphi_ui::View; /// Una app de gioser con icono de marca. El identificador (`name`) coincide /// con el `id` del `AppEntry` en `app-bus`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppIcon { // --- 00_unanchay · PERCIBIR --- Chaka, Khipu, Pineal, Pluma, Puriy, Rimay, // --- 01_yachay · CONOCER --- Cosmos, Dominium, Iniy, Nakui, Tinkuy, // --- 02_ruway · HACER --- Ayni, Cards, Chasqui, Llimphi, Media, Mirada, Nada, Nahual, Shuma, Supay, Takiy, Tullpu, Wawa, // --- 03_ukupacha · RAÍZ --- Agora, Arje, Minga, Sandokan, WawaExplorer, } /// Las 29 apps, en orden de cuadrante. Útil para iterar (galerías, tests). pub const ALL: [AppIcon; 29] = [ AppIcon::Chaka, AppIcon::Khipu, AppIcon::Pineal, AppIcon::Pluma, AppIcon::Puriy, AppIcon::Rimay, AppIcon::Cosmos, AppIcon::Dominium, AppIcon::Iniy, AppIcon::Nakui, AppIcon::Tinkuy, AppIcon::Ayni, AppIcon::Cards, AppIcon::Chasqui, AppIcon::Llimphi, AppIcon::Media, AppIcon::Mirada, AppIcon::Nada, AppIcon::Nahual, AppIcon::Shuma, AppIcon::Supay, AppIcon::Takiy, AppIcon::Tullpu, AppIcon::Wawa, AppIcon::Agora, AppIcon::Arje, AppIcon::Minga, AppIcon::Sandokan, AppIcon::WawaExplorer, ]; impl AppIcon { /// Id estable de la app (coincide con `AppEntry.id` / nombre del dominio). pub const fn name(self) -> &'static str { match self { AppIcon::Chaka => "chaka", AppIcon::Khipu => "khipu", AppIcon::Pineal => "pineal", AppIcon::Pluma => "pluma", AppIcon::Puriy => "puriy", AppIcon::Rimay => "rimay", AppIcon::Cosmos => "cosmos", AppIcon::Dominium => "dominium", AppIcon::Iniy => "iniy", AppIcon::Nakui => "nakui", AppIcon::Tinkuy => "tinkuy", AppIcon::Ayni => "ayni", AppIcon::Cards => "cards", AppIcon::Chasqui => "chasqui", AppIcon::Llimphi => "llimphi", AppIcon::Media => "media", AppIcon::Mirada => "mirada", AppIcon::Nada => "nada", AppIcon::Nahual => "nahual", AppIcon::Shuma => "shuma", AppIcon::Supay => "supay", AppIcon::Takiy => "takiy", AppIcon::Tullpu => "tullpu", AppIcon::Wawa => "wawa", AppIcon::Agora => "agora", AppIcon::Arje => "arje", AppIcon::Minga => "minga", AppIcon::Sandokan => "sandokan", AppIcon::WawaExplorer => "wawa-explorer", } } /// Resuelve una app desde su `id` del registro. Acepta tanto /// `"wawa-explorer"` como `"wawa_explorer"`. pub fn from_app_id(id: &str) -> Option { let id = id.trim().to_ascii_lowercase(); let id = id.replace('_', "-"); ALL.into_iter().find(|a| a.name() == id) } /// Color de marca de la app — el que el dock/menú debería usar para /// pintar el glifo por default. pub const fn brand(self) -> Color { let (r, g, b) = match self { AppIcon::Chaka => (43, 166, 164), AppIcon::Khipu => (181, 101, 29), AppIcon::Pineal => (108, 79, 216), AppIcon::Pluma => (61, 59, 142), AppIcon::Puriy => (63, 163, 77), AppIcon::Rimay => (232, 131, 58), AppIcon::Cosmos => (230, 184, 0), AppIcon::Dominium => (74, 111, 165), AppIcon::Iniy => (124, 179, 66), AppIcon::Nakui => (194, 84, 157), AppIcon::Tinkuy => (217, 83, 79), AppIcon::Ayni => (42, 168, 196), AppIcon::Cards => (142, 99, 206), AppIcon::Chasqui => (52, 179, 106), AppIcon::Llimphi => (229, 91, 122), AppIcon::Media => (226, 62, 87), AppIcon::Mirada => (45, 125, 210), AppIcon::Nada => (136, 147, 160), AppIcon::Nahual => (124, 77, 191), AppIcon::Shuma => (224, 165, 38), AppIcon::Supay => (155, 63, 181), AppIcon::Takiy => (229, 99, 155), AppIcon::Tullpu => (224, 96, 58), AppIcon::Wawa => (91, 141, 239), AppIcon::Agora => (47, 158, 143), AppIcon::Arje => (176, 141, 87), AppIcon::Minga => (224, 123, 57), AppIcon::Sandokan => (192, 57, 43), AppIcon::WawaExplorer => (110, 160, 240), }; Color::from_rgb8(r, g, b) } /// `BezPath` del glifo en coords del grid 24×24. pub fn path(self) -> BezPath { match self { AppIcon::Chaka => path_chaka(), AppIcon::Khipu => path_khipu(), AppIcon::Pineal => path_pineal(), AppIcon::Pluma => path_pluma(), AppIcon::Puriy => path_puriy(), AppIcon::Rimay => path_rimay(), AppIcon::Cosmos => path_cosmos(), AppIcon::Dominium => path_dominium(), AppIcon::Iniy => path_iniy(), AppIcon::Nakui => path_nakui(), AppIcon::Tinkuy => path_tinkuy(), AppIcon::Ayni => path_ayni(), AppIcon::Cards => path_cards(), AppIcon::Chasqui => path_chasqui(), AppIcon::Llimphi => path_llimphi(), AppIcon::Media => path_media(), AppIcon::Mirada => path_mirada(), AppIcon::Nada => path_nada(), AppIcon::Nahual => path_nahual(), AppIcon::Shuma => path_shuma(), AppIcon::Supay => path_supay(), AppIcon::Takiy => path_takiy(), AppIcon::Tullpu => path_tullpu(), AppIcon::Wawa => path_wawa(), AppIcon::Agora => path_agora(), AppIcon::Arje => path_arje(), AppIcon::Minga => path_minga(), AppIcon::Sandokan => path_sandokan(), AppIcon::WawaExplorer => path_wawa_explorer(), } } } /// `View` que pinta el icono de app en su **color de marca**, ocupando todo /// el rect del padre, escalado uniforme y centrado. /// /// - `stroke_width` en unidades del grid 24×24 (típico de marca: `1.8`). pub fn app_icon_view(icon: AppIcon, stroke_width: f32) -> View { app_icon_view_colored(icon, icon.brand(), stroke_width) } /// Igual que [`app_icon_view`] pero forzando un color (p.ej. monocromo /// `theme.fg_text` para un menú denso donde el color distrae). pub fn app_icon_view_colored( icon: AppIcon, color: Color, stroke_width: f32, ) -> View { View::new(Style { position: Position::Absolute, size: Size { width: percent(1.0_f32), height: percent(1.0_f32), }, ..Default::default() }) .paint_with(move |scene, _ts, rect| { paint_app_icon(scene, rect, icon, color, stroke_width); }) } /// Pintor crudo — para stampear varios iconos de app dentro del mismo /// `paint_with` (una grilla de launcher, por ejemplo). pub fn paint_app_icon( scene: &mut llimphi_ui::llimphi_raster::vello::Scene, rect: llimphi_ui::PaintRect, icon: AppIcon, color: Color, stroke_width: f32, ) { let side = rect.w.min(rect.h) as f64; if side <= 0.0 { return; } let scale = side / 24.0; let tx = rect.x as f64 + (rect.w as f64 - side) * 0.5; let ty = rect.y as f64 + (rect.h as f64 - side) * 0.5; let xform = Affine::translate((tx, ty)) * Affine::scale(scale); let stroke = Stroke::new(stroke_width as f64) .with_join(Join::Round) .with_caps(Cap::Round); let path = icon.path(); scene.stroke(&stroke, xform, color, None, &path); } // ===================================================================== // Helpers // ===================================================================== /// Círculo aproximado con `segments` lados rectos (liso por el Cap::Round). fn circle(cx: f64, cy: f64, r: f64, segments: usize) -> BezPath { let mut p = BezPath::new(); for i in 0..=segments { let theta = std::f64::consts::TAU * (i as f64) / (segments as f64); let x = cx + r * theta.cos(); let y = cy + r * theta.sin(); if i == 0 { p.move_to((x, y)); } else { p.line_to((x, y)); } } p } /// Empuja todos los elementos de `src` dentro de `dst` (para componer /// glifos hechos de varias subformas). fn push_all(dst: &mut BezPath, src: BezPath) { for el in src.elements() { dst.push(*el); } } // ===================================================================== // Glifos — uno por app. Grid 24×24, margen ~3. // ===================================================================== // --- 00_unanchay · PERCIBIR --- fn path_chaka() -> BezPath { // chaka = puente: tablero recto + arco + dos pilotes. let mut p = BezPath::new(); // Tablero. p.move_to((3.0, 9.0)); p.line_to((21.0, 9.0)); // Arco bajo el tablero. p.move_to((5.0, 18.0)); p.curve_to((5.0, 11.0), (19.0, 11.0), (19.0, 18.0)); // Pilotes que conectan tablero y arco. p.move_to((9.0, 9.0)); p.line_to((9.0, 12.5)); p.move_to((15.0, 9.0)); p.line_to((15.0, 12.5)); p } fn path_khipu() -> BezPath { // khipu: cordón principal + tres ramales con nudos (puntos). let mut p = BezPath::new(); // Cordón superior. p.move_to((4.0, 6.0)); p.line_to((20.0, 6.0)); // Ramales. p.move_to((7.0, 6.0)); p.line_to((7.0, 19.0)); p.move_to((12.0, 6.0)); p.line_to((12.0, 20.0)); p.move_to((17.0, 6.0)); p.line_to((17.0, 18.0)); // Nudos. push_all(&mut p, circle(7.0, 12.0, 1.3, 10)); push_all(&mut p, circle(12.0, 10.0, 1.3, 10)); push_all(&mut p, circle(12.0, 16.0, 1.3, 10)); push_all(&mut p, circle(17.0, 11.0, 1.3, 10)); p } fn path_pineal() -> BezPath { // pineal = tercer ojo: párpado almendrado + iris + antena/rayo arriba. let mut p = BezPath::new(); p.move_to((4.0, 12.0)); p.curve_to((8.0, 7.0), (16.0, 7.0), (20.0, 12.0)); p.curve_to((16.0, 17.0), (8.0, 17.0), (4.0, 12.0)); push_all(&mut p, circle(12.0, 12.0, 2.6, 14)); p.move_to((12.0, 3.0)); p.line_to((12.0, 5.5)); p } fn path_pluma() -> BezPath { // pluma = plumín: rombo apuntando abajo + ranura + ojal. let mut p = BezPath::new(); p.move_to((12.0, 3.0)); p.line_to((16.0, 9.0)); p.line_to((13.5, 20.0)); p.line_to((10.5, 20.0)); p.line_to((8.0, 9.0)); p.close_path(); // Ranura. p.move_to((12.0, 11.5)); p.line_to((12.0, 19.0)); // Ojal. push_all(&mut p, circle(12.0, 9.5, 1.2, 10)); p } fn path_puriy() -> BezPath { // puriy = caminar/recorrido: senda curva ascendente con flecha. let mut p = BezPath::new(); p.move_to((6.0, 20.0)); p.curve_to((6.0, 12.0), (18.0, 12.0), (18.0, 4.0)); // Cabeza de flecha. p.move_to((15.0, 6.0)); p.line_to((18.0, 4.0)); p.line_to((20.5, 6.5)); p } fn path_rimay() -> BezPath { // rimay = palabra/habla: globo de diálogo con cola + dos renglones. let mut p = BezPath::new(); p.move_to((4.0, 6.0)); p.line_to((20.0, 6.0)); p.line_to((20.0, 15.0)); p.line_to((11.0, 15.0)); p.line_to((8.0, 19.0)); p.line_to((8.0, 15.0)); p.line_to((4.0, 15.0)); p.close_path(); // Renglones. p.move_to((8.0, 9.5)); p.line_to((16.0, 9.5)); p.move_to((8.0, 12.0)); p.line_to((13.0, 12.0)); p } // --- 01_yachay · CONOCER --- fn path_cosmos() -> BezPath { // cosmos = destello de 4 puntas + dos estrellas pequeñas. let mut p = BezPath::new(); p.move_to((12.0, 4.0)); p.line_to((13.4, 10.6)); p.line_to((20.0, 12.0)); p.line_to((13.4, 13.4)); p.line_to((12.0, 20.0)); p.line_to((10.6, 13.4)); p.line_to((4.0, 12.0)); p.line_to((10.6, 10.6)); p.close_path(); // Estrellas chicas. push_all(&mut p, circle(19.0, 6.0, 0.8, 8)); push_all(&mut p, circle(5.5, 18.0, 0.8, 8)); p } fn path_dominium() -> BezPath { // dominium = ERP/libro mayor: barras de distinta altura sobre una base. let mut p = BezPath::new(); // Base. p.move_to((3.0, 20.0)); p.line_to((21.0, 20.0)); // Columnas. p.move_to((6.0, 14.0)); p.line_to((9.0, 14.0)); p.line_to((9.0, 20.0)); p.line_to((6.0, 20.0)); p.close_path(); p.move_to((10.5, 8.0)); p.line_to((13.5, 8.0)); p.line_to((13.5, 20.0)); p.line_to((10.5, 20.0)); p.close_path(); p.move_to((15.0, 11.0)); p.line_to((18.0, 11.0)); p.line_to((18.0, 20.0)); p.line_to((15.0, 20.0)); p.close_path(); p } fn path_iniy() -> BezPath { // iniy = aliento/creer: brote con tallo y dos hojas. let mut p = BezPath::new(); // Tallo. p.move_to((12.0, 20.0)); p.line_to((12.0, 10.0)); // Hoja izquierda. p.move_to((12.0, 14.0)); p.curve_to((8.0, 14.0), (6.0, 11.0), (7.0, 8.0)); p.curve_to((10.0, 9.0), (12.0, 11.0), (12.0, 14.0)); // Hoja derecha. p.move_to((12.0, 12.0)); p.curve_to((15.5, 12.0), (17.0, 9.0), (16.5, 6.0)); p.curve_to((14.0, 7.0), (12.0, 9.0), (12.0, 12.0)); p } fn path_nakui() -> BezPath { // nakui = grafo de morfismos: tres nodos + aristas. let mut p = BezPath::new(); // Aristas (primero, para que queden bajo los nodos). p.move_to((7.5, 9.0)); p.line_to((16.5, 9.0)); p.move_to((7.5, 9.8)); p.line_to((10.8, 16.0)); p.move_to((16.5, 9.8)); p.line_to((13.2, 16.0)); // Nodos. push_all(&mut p, circle(6.0, 8.0, 2.2, 14)); push_all(&mut p, circle(18.0, 8.0, 2.2, 14)); push_all(&mut p, circle(12.0, 18.0, 2.2, 14)); p } fn path_tinkuy() -> BezPath { // tinkuy = encuentro/choque: dos flechas que convergen + chispa. let mut p = BezPath::new(); // Flecha izquierda → p.move_to((3.0, 12.0)); p.line_to((9.5, 12.0)); p.move_to((7.5, 10.0)); p.line_to((9.5, 12.0)); p.line_to((7.5, 14.0)); // Flecha derecha ← p.move_to((21.0, 12.0)); p.line_to((14.5, 12.0)); p.move_to((16.5, 10.0)); p.line_to((14.5, 12.0)); p.line_to((16.5, 14.0)); // Chispa central. push_all(&mut p, circle(12.0, 12.0, 1.6, 10)); p } // --- 02_ruway · HACER --- fn path_ayni() -> BezPath { // ayni = reciprocidad: dos flechas curvas en ciclo. let mut p = BezPath::new(); // Arco superior, flecha hacia la derecha-abajo. p.move_to((6.0, 8.0)); p.curve_to((9.0, 4.0), (15.0, 4.0), (18.0, 8.5)); p.move_to((15.5, 8.0)); p.line_to((18.0, 8.5)); p.line_to((18.5, 5.8)); // Arco inferior, flecha hacia la izquierda-arriba. p.move_to((18.0, 16.0)); p.curve_to((15.0, 20.0), (9.0, 20.0), (6.0, 15.5)); p.move_to((8.5, 16.0)); p.line_to((6.0, 15.5)); p.line_to((5.5, 18.2)); p } fn path_cards() -> BezPath { // cards = naipes apilados: carta frontal + borde de la de atrás. let mut p = BezPath::new(); // Carta de atrás (asoma arriba y a la derecha). p.move_to((8.0, 5.0)); p.line_to((19.0, 5.0)); p.line_to((19.0, 16.0)); // Carta frontal. p.move_to((5.0, 9.0)); p.line_to((15.0, 9.0)); p.line_to((15.0, 20.0)); p.line_to((5.0, 20.0)); p.close_path(); p } fn path_chasqui() -> BezPath { // chasqui = mensajero: avión de papel. let mut p = BezPath::new(); p.move_to((4.0, 11.0)); p.line_to((20.0, 4.0)); p.line_to((13.0, 20.0)); p.line_to((11.0, 13.0)); p.close_path(); // Pliegue central. p.move_to((11.0, 13.0)); p.line_to((20.0, 4.0)); p } fn path_llimphi() -> BezPath { // llimphi = pintura/color: paleta con apoyo para el pulgar + 3 gotas. let mut p = BezPath::new(); p.move_to((4.0, 12.0)); p.curve_to((4.0, 6.0), (11.0, 4.0), (15.0, 5.0)); p.curve_to((20.0, 6.5), (21.0, 12.0), (18.0, 15.0)); p.curve_to((16.0, 16.5), (16.5, 13.5), (14.0, 14.0)); p.curve_to((11.5, 14.5), (12.5, 18.0), (9.0, 18.0)); p.curve_to((5.5, 18.0), (4.0, 15.0), (4.0, 12.0)); p.close_path(); // Gotas de pintura. push_all(&mut p, circle(8.0, 9.0, 1.1, 10)); push_all(&mut p, circle(12.0, 8.0, 1.1, 10)); push_all(&mut p, circle(15.5, 10.0, 1.1, 10)); p } fn path_media() -> BezPath { // media = reproducción: marco + triángulo de play. let mut p = BezPath::new(); p.move_to((4.0, 6.0)); p.line_to((20.0, 6.0)); p.line_to((20.0, 18.0)); p.line_to((4.0, 18.0)); p.close_path(); // Play. p.move_to((10.0, 9.0)); p.line_to((10.0, 15.0)); p.line_to((16.0, 12.0)); p.close_path(); p } fn path_mirada() -> BezPath { // mirada = ojo: párpado + iris + pupila. let mut p = BezPath::new(); p.move_to((3.0, 12.0)); p.curve_to((8.0, 6.0), (16.0, 6.0), (21.0, 12.0)); p.curve_to((16.0, 18.0), (8.0, 18.0), (3.0, 12.0)); p.close_path(); push_all(&mut p, circle(12.0, 12.0, 3.4, 18)); push_all(&mut p, circle(12.0, 12.0, 1.0, 8)); p } fn path_nada() -> BezPath { // nada = vacío: conjunto vacío ∅ (anillo + diagonal). let mut p = circle(12.0, 12.0, 8.0, 28); p.move_to((6.5, 17.5)); p.line_to((17.5, 6.5)); p } fn path_nahual() -> BezPath { // nahual = máscara/mutación de forma: antifaz con dos ojos. let mut p = BezPath::new(); p.move_to((4.0, 9.0)); p.curve_to((4.0, 6.5), (8.0, 6.0), (10.0, 7.5)); p.curve_to((11.0, 8.2), (13.0, 8.2), (14.0, 7.5)); p.curve_to((16.0, 6.0), (20.0, 6.5), (20.0, 9.0)); p.curve_to((20.0, 13.5), (16.0, 16.5), (12.0, 15.5)); p.curve_to((8.0, 16.5), (4.0, 13.5), (4.0, 9.0)); p.close_path(); push_all(&mut p, circle(9.0, 10.0, 1.3, 10)); push_all(&mut p, circle(15.0, 10.0, 1.3, 10)); p } fn path_shuma() -> BezPath { // shuma = discernir: embudo/filtro. let mut p = BezPath::new(); p.move_to((4.0, 6.0)); p.line_to((20.0, 6.0)); p.line_to((13.0, 14.0)); p.line_to((13.0, 19.0)); p.line_to((11.0, 20.0)); p.line_to((11.0, 14.0)); p.close_path(); p } fn path_supay() -> BezPath { // supay = espíritu del ukhupacha: llama doble. let mut p = BezPath::new(); // Llama exterior. p.move_to((12.0, 3.0)); p.curve_to((17.0, 9.0), (16.0, 14.0), (12.0, 21.0)); p.curve_to((8.0, 14.0), (7.0, 9.0), (12.0, 3.0)); p.close_path(); // Llama interior. p.move_to((12.0, 9.0)); p.curve_to((14.0, 12.0), (13.0, 16.0), (12.0, 18.0)); p.curve_to((11.0, 16.0), (10.0, 12.0), (12.0, 9.0)); p.close_path(); p } fn path_takiy() -> BezPath { // takiy = cantar: corchea + ondas de sonido. let mut p = BezPath::new(); // Cabeza de nota. push_all(&mut p, circle(8.0, 18.0, 2.4, 16)); // Plica. p.move_to((10.4, 18.0)); p.line_to((10.4, 6.0)); // Banderola. p.move_to((10.4, 6.0)); p.curve_to((13.5, 7.0), (14.5, 9.0), (13.5, 11.0)); // Ondas. p.move_to((16.0, 9.0)); p.curve_to((18.0, 11.0), (18.0, 13.0), (16.0, 15.0)); p } fn path_tullpu() -> BezPath { // tullpu = tinte/color: tres gotas. let mut p = BezPath::new(); // Gota 1. p.move_to((8.0, 5.0)); p.curve_to((11.0, 9.0), (11.0, 11.0), (8.0, 12.0)); p.curve_to((5.0, 11.0), (5.0, 9.0), (8.0, 5.0)); p.close_path(); // Gota 2. p.move_to((16.0, 6.0)); p.curve_to((19.0, 10.0), (19.0, 12.0), (16.0, 13.0)); p.curve_to((13.0, 12.0), (13.0, 10.0), (16.0, 6.0)); p.close_path(); // Gota 3. p.move_to((12.0, 13.0)); p.curve_to((15.0, 17.0), (15.0, 19.0), (12.0, 20.0)); p.curve_to((9.0, 19.0), (9.0, 17.0), (12.0, 13.0)); p.close_path(); p } fn path_wawa() -> BezPath { // wawa = célula/semilla (el SO en gestación): membrana + núcleo. let mut p = circle(12.0, 12.0, 8.0, 28); push_all(&mut p, circle(12.0, 12.0, 3.0, 16)); p } // --- 03_ukupacha · RAÍZ --- fn path_agora() -> BezPath { // agora = firma/confianza: escudo con check. let mut p = BezPath::new(); p.move_to((12.0, 3.0)); p.line_to((20.0, 6.0)); p.line_to((20.0, 12.0)); p.curve_to((20.0, 17.0), (16.0, 20.0), (12.0, 21.0)); p.curve_to((8.0, 20.0), (4.0, 17.0), (4.0, 12.0)); p.line_to((4.0, 6.0)); p.close_path(); // Check. p.move_to((8.5, 12.0)); p.line_to((11.0, 14.5)); p.line_to((16.0, 8.5)); p } fn path_arje() -> BezPath { // arje = arché/raíz de confianza: ancla. let mut p = BezPath::new(); // Anillo. push_all(&mut p, circle(12.0, 5.0, 2.2, 14)); // Caña. p.move_to((12.0, 7.2)); p.line_to((12.0, 19.0)); // Travesaño. p.move_to((8.0, 10.0)); p.line_to((16.0, 10.0)); // Uñas/brazos. p.move_to((6.0, 14.0)); p.curve_to((6.0, 18.5), (9.0, 20.0), (12.0, 20.0)); p.move_to((18.0, 14.0)); p.curve_to((18.0, 18.5), (15.0, 20.0), (12.0, 20.0)); p } fn path_minga() -> BezPath { // minga = trabajo comunal: tres figuras. let mut p = BezPath::new(); // Figura central. push_all(&mut p, circle(12.0, 7.0, 2.2, 14)); p.move_to((8.0, 18.0)); p.curve_to((8.0, 13.0), (16.0, 13.0), (16.0, 18.0)); // Figura izquierda. push_all(&mut p, circle(5.5, 10.0, 1.6, 12)); p.move_to((2.5, 18.0)); p.curve_to((2.5, 14.5), (6.0, 13.5), (7.5, 15.0)); // Figura derecha. push_all(&mut p, circle(18.5, 10.0, 1.6, 12)); p.move_to((21.5, 18.0)); p.curve_to((21.5, 14.5), (18.0, 13.5), (16.5, 15.0)); p } fn path_sandokan() -> BezPath { // sandokan = caja/contenedor aislado: cubo isométrico. let mut p = BezPath::new(); // Cara frontal. p.move_to((5.0, 8.0)); p.line_to((14.0, 8.0)); p.line_to((14.0, 18.0)); p.line_to((5.0, 18.0)); p.close_path(); // Tapa. p.move_to((5.0, 8.0)); p.line_to((9.0, 4.0)); p.line_to((18.0, 4.0)); p.line_to((14.0, 8.0)); // Cara lateral. p.move_to((14.0, 8.0)); p.line_to((18.0, 4.0)); p.line_to((18.0, 14.0)); p.line_to((14.0, 18.0)); p } fn path_wawa_explorer() -> BezPath { // wawa-explorer = launchpad: grilla 2×2. let mut p = BezPath::new(); for (x, y) in &[(5.0, 5.0), (13.0, 5.0), (5.0, 13.0), (13.0, 13.0)] { p.move_to((*x, *y)); p.line_to((*x + 6.0, *y)); p.line_to((*x + 6.0, *y + 6.0)); p.line_to((*x, *y + 6.0)); p.close_path(); } p } #[cfg(test)] mod tests { use super::*; #[test] fn all_app_icons_have_nonempty_path() { for icon in ALL { let p = icon.path(); assert!( p.elements().len() > 0, "icono de app {} produjo path vacío", icon.name() ); } } #[test] fn app_names_are_unique() { let mut names: Vec<&str> = ALL.iter().map(|i| i.name()).collect(); let n = names.len(); names.sort(); names.dedup(); assert_eq!(names.len(), n, "nombres duplicados en AppIcon::name()"); } #[test] fn from_app_id_roundtrips() { for icon in ALL { assert_eq!(AppIcon::from_app_id(icon.name()), Some(icon)); } // Tolera underscores y mayúsculas. assert_eq!(AppIcon::from_app_id("WAWA_EXPLORER"), Some(AppIcon::WawaExplorer)); assert_eq!(AppIcon::from_app_id("desconocida"), None); } }