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>
This commit is contained in:
2026-06-04 04:23:42 +00:00
commit e65e9cc623
286 changed files with 46136 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "llimphi-icons"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "llimphi-icons — set mínimo de iconos vectoriales (BezPath en grid 24×24) renderizables vía paint_with. Stroke-based, escalables. Cubre las acciones canónicas de cualquier UI gioser."
[dependencies]
llimphi-ui = { workspace = true }
+136
View File
@@ -0,0 +1,136 @@
//! Galería de los iconos de marca de todas las apps de gioser.
//!
//! Pinta los 29 [`AppIcon`] en una grilla, cada uno en su color de marca
//! con su nombre debajo. Sirve para eyeballear de un vistazo que el set
//! es coherente (mismo peso de trazo, mismo aire) y que cada glifo es
//! reconocible.
//!
//! `cargo run -p llimphi-icons --example app_icons_gallery --release`
use llimphi_icons::app_icons::{app_icon_view, AppIcon, ALL};
use llimphi_ui::llimphi_layout::taffy::prelude::{
auto, length, percent, AlignItems, FlexDirection, JustifyContent, Size, Style,
};
use llimphi_ui::llimphi_layout::taffy::Rect;
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::llimphi_text::Alignment;
use llimphi_ui::{App, Handle, View};
const COLS: usize = 6;
const BG: Color = Color::from_rgb8(18, 20, 24);
const CELL: Color = Color::from_rgb8(28, 31, 38);
const LABEL: Color = Color::from_rgb8(196, 202, 212);
struct Model;
#[derive(Clone)]
enum Msg {}
fn cell(icon: AppIcon) -> View<Msg> {
// Recuadro del glifo (cuadrado, el icono se escala al lado menor).
let icon_box = View::new(Style {
size: Size {
width: length(52.0_f32),
height: length(52.0_f32),
},
flex_shrink: 0.0,
..Default::default()
})
.children(vec![app_icon_view(icon, 2.0)]);
let label = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(16.0_f32),
},
..Default::default()
})
.text_aligned(icon.name().to_string(), 11.0, LABEL, Alignment::Center);
View::new(Style {
size: Size {
width: length(118.0_f32),
height: length(96.0_f32),
},
flex_direction: FlexDirection::Column,
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
gap: Size {
width: length(0.0_f32),
height: length(8.0_f32),
},
..Default::default()
})
.fill(CELL)
.radius(12.0)
.children(vec![icon_box, label])
}
fn row(icons: &[AppIcon]) -> View<Msg> {
View::new(Style {
size: Size {
width: auto(),
height: auto(),
},
flex_direction: FlexDirection::Row,
gap: Size {
width: length(14.0_f32),
height: length(0.0_f32),
},
..Default::default()
})
.children(icons.iter().copied().map(cell).collect())
}
struct Gallery;
impl App for Gallery {
type Model = Model;
type Msg = Msg;
fn title() -> &'static str {
"llimphi-icons · galería de apps"
}
fn initial_size() -> (u32, u32) {
(820, 620)
}
fn init(_: &Handle<Msg>) -> Model {
Model
}
fn update(_model: Model, msg: Msg, _: &Handle<Msg>) -> Model {
match msg {}
}
fn view(_: &Model) -> View<Msg> {
let rows: Vec<View<Msg>> = ALL.chunks(COLS).map(row).collect();
View::new(Style {
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
flex_direction: FlexDirection::Column,
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
gap: Size {
width: length(0.0_f32),
height: length(14.0_f32),
},
padding: Rect {
left: length(20.0_f32),
right: length(20.0_f32),
top: length(20.0_f32),
bottom: length(20.0_f32),
},
..Default::default()
})
.fill(BG)
.children(rows)
}
}
fn main() {
llimphi_ui::run::<Gallery>();
}
+824
View File
@@ -0,0 +1,824 @@
//! `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);
}
}
File diff suppressed because it is too large Load Diff