Files
llimphi/llimphi-icons/src/app_icons.rs
T
sergio e65e9cc623 feat: llimphi standalone — framework UI soberano extraído del monorepo
Motor gráfico Llimphi como workspace independiente: bucle Elm
(input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley.
Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets
+ módulos, sin dependencias al resto del monorepo. cargo check --workspace
pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 04:23:42 +00:00

825 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `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<AppIcon> {
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<Msg: Clone + 'static>(icon: AppIcon, stroke_width: f32) -> View<Msg> {
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<Msg: Clone + 'static>(
icon: AppIcon,
color: Color,
stroke_width: f32,
) -> View<Msg> {
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);
}
}