//! `llimphi-icons` — set canónico de iconos vectoriales para apps tawasuyu. //! //! Cada icono es una función pura que devuelve un `BezPath` definido en //! un grid lógico de **24×24 unidades**. El renderer escala al rect que //! reciba, así un mismo icono sirve para 12px (en una fila de lista) y //! para 64px (en una hero card) sin pérdida de nitidez — es vector, //! no bitmap. //! //! ## Diseño //! //! - **Stroke-based, no fill**: los iconos son trazos de ancho 2 (en //! unidades del grid) con joins/caps suaves. El stroke se renderiza //! con el color que la app elija (típicamente `theme.fg_text` o //! `theme.accent`). //! - **Geometría minimal, no marca**: glifos genéricos universales, //! no "marca registrada". Cada uno debe ser reconocible al primer //! vistazo aún en 12×12. //! - **Set acotado**: suficientes para cubrir el grueso de acciones y //! tipos que aparecen en cualquier UI tawasuyu. Si una app necesita uno //! más, lo agrega aquí (no en su propio crate) — la consistencia //! visual importa más que el aislamiento. //! //! ## Catálogo //! //! | Categoría | Iconos | //! |--------------|-----------------------------------------------------| //! | Documento | `file`, `folder`, `folder_open`, `save`, `open` | //! | Edición | `plus`, `minus`, `x`, `check`, `edit`, `trash` | //! | Navegación | `chevron_up`, `chevron_down`, `chevron_left`, `chevron_right`, `home`, `search` | //! | Estado | `info`, `warning`, `error`, `bell` | //! | Sistema | `settings`, `more` | //! | Multimedia | `play`, `pause`, `stop`, `skip_*`, `volume*`, `repeat`, `shuffle`, `record`, `equalizer`, `camera`, `gauge` | //! | Archivos | `image`, `music`, `film`, `archive`, `code`, `file_text`, `link`, `font` | //! //! ## Uso //! //! ```ignore //! use llimphi_icons::{Icon, icon_view}; //! //! // Botón con icono "save": //! let btn = View::new(style) //! .fill(palette.bg_button) //! .children(vec![icon_view(Icon::Save, palette.fg_text, 1.6)]); //! ``` //! //! El parámetro `stroke_width` (3er arg de `icon_view`) está en unidades //! del grid (24×24). `1.6` es el default armonioso; `2.0` para énfasis; //! `1.2` para iconos en tipografías pequeñas. #![forbid(unsafe_code)] // Sin `std`: sólo geometría y `alloc` (W4.6). #![no_std] extern crate alloc; use alloc::string::ToString; pub mod app_icons; use llimphi_compositor::llimphi_layout::taffy::{ prelude::{percent, Size, Style}, Position, }; use llimphi_compositor::peniko::kurbo::{Affine, BezPath, Cap, Join, Stroke}; use llimphi_compositor::peniko::Color; use llimphi_compositor::View; /// Catálogo de iconos del set canónico. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Icon { // --- Documento --- File, Folder, FolderOpen, Save, Open, // --- Edición --- Plus, Minus, X, Check, Edit, Trash, // --- Navegación --- ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Home, Search, // --- Estado --- Info, Warning, Error, Bell, // --- Sistema --- Settings, More, // --- Multimedia --- Play, Pause, Stop, SkipBack, SkipForward, Rewind, FastForward, Volume, VolumeMute, Repeat, Shuffle, Record, Equalizer, Camera, Gauge, // --- Archivos (tipos por extensión, para listados de file manager) --- Image, Music, Film, Archive, Code, FileText, Link, Font, // --- Vistas / layout (toolbars de file manager) --- /// Grilla 2×2 (vista iconos / galería). Grid, /// Tres filas horizontales (vista lista). Rows, /// Tabla: marco + fila de encabezado + columnas (vista detalle). Table, /// Dos paneles verticales lado a lado (modo dual). Columns, // --- Dominio: creador de mundos (llimphi-voxel-studio) --- /// Globo terráqueo (Mundos). Globe, /// Cadena montañosa (Biomas). Mountain, /// Hoja con nervadura (Materiales). Leaf, /// Silueta de persona (Seres). User, /// Gota de agua (Leyes / físicas). Droplet, // --- Sistema / settings panels (sweep glifo→vector 2026-06-30) --- /// Reloj con manecillas (hora, contextos, tiempo). Clock, /// Símbolo de encendido/apagado. Power, /// Sobre de correo. Mail, /// Teclado (atajos). Keyboard, /// Paleta de pintor (themes/apariencia). Palette, /// Candado (seguridad/privacidad). Lock, /// Llave. Key, /// Monitor/pantalla (sistema, acerca del equipo). Monitor, /// Destello de cuatro puntas (animaciones, efectos). Sparkle, /// Micrófono (voz, audio in). Mic, /// Ratón. Mouse, /// Nube (red/online). Cloud, /// Luna (modo oscuro, noche). Moon, /// Flecha circular (recargar/refrescar). Refresh, /// Pieza de rompecabezas (plugins/módulos). Puzzle, Copy, // --- Dominio: editor de imágenes por capas (tullpu) --- /// Cruz de cuatro flechas (mover / desplazar). Move, /// Cuentagotas (leer el color de un píxel). Eyedropper, /// Marco punteado (selección rectangular). Marquee, /// Balde volcando (relleno por inundación). Bucket, /// Pincel (trazo a mano alzada). Brush, /// Goma de borrar. Eraser, /// Rampa de tono (degradé). Gradient, /// Varita mágica (seleccionar por color). Wand, /// Lazo a mano alzada. Lasso, /// Polígono cerrado (lazo poligonal). Polygon, /// La "T" del texto. Type, /// Sello / tampón (clonar). Stamp, /// Curita (pincel corrector, sanar). Bandage, /// Punta de pluma (herramienta de curvas). Pen, /// Tres hojas apiladas (capas). Layers, // --- Dominio: los controles de un editor de imágenes (tullpu, 2026-08-29) --- /// Marco cortado con un tilde: seleccionar todo. SelectAll, /// Núcleo con flechas hacia las esquinas: agrandar la selección. Expand, /// Marco con flechas hacia adentro: achicar la selección. Contract, /// Tijera (cortar). Scissors, /// Tabla con pinza (pegar). Paste, /// Dos hojas y un más (duplicar). Duplicate, /// Las dos escuadras del recorte. Crop, /// Cuatro escuadras abrazando el contenido: recortar a lo visible. Trim, /// Cuadro con un disco encima: la máscara. Mask, /// Disco partido con una mitad tramada: invertir. Invert, /// Eje cortado entre dos triángulos: espejar en horizontal. Mirror, /// El mismo espejo, en vertical. MirrorV, /// Rectángulo. Square, /// Elipse. Circle, /// Rectángulo de esquinas redondeadas. RoundedSquare, /// Estrella de cinco puntas. Star, /// Hexágono regular. Hexagon, /// Segmento con un nodo en cada punta. Line, /// Dos flechas contra una barra: aplanar. Flatten, /// Flecha circular horaria. RotateCw, /// Flecha circular antihoraria. RotateCcw, /// Dos cuadros corridos: la sombra paralela. Shadow, /// Sol con rayos: brillo. Brightness, /// Cuadro partido en diagonal y tramado: contraste. Contrast, /// Disco con tres radios: la rueda de tono. ColorWheel, /// Mitad nítida y mitad que se ralea: desenfoque. Blur, /// Barras de distinta altura sobre una base: niveles. Levels, /// Una S dentro de una caja: la curva. Curve, /// Silueta con el corte punteado alrededor: segmentar. Segment, /// Cuadro chico y flecha que lo agranda: escalar. /// Un corazón. **El cuerpo propio** (`qhali`): el pulso, el diario, la /// medicación. Es el dibujo que el catálogo de widgets ya le daba a ese /// diente como glifo (`♥`); acá deja de ser un carácter de fuente y pasa a /// ser un trazo, que es lo que pide el rail. Heart, Upscale, // --- Transporte, segunda tanda --- /// Lista **anterior**: la pila de renglones con la punta hacia la izquierda. /// No es `SkipBack` (ésa es la pista anterior): lo que salta es la playlist /// entera, y la diferencia tiene que verse en el ícono, no en el tooltip. ListPrev, /// Lista **siguiente**, espejo de [`Icon::ListPrev`]. ListNext, // --- Visibilidad --- /// Ojo abierto: mostrar lo que está escondido (los archivos ocultos de un /// explorador). Prendido = se ve; el estado lo dice el `active` del botón. Eye, } impl Icon { /// Identificador estable en lowercase con underscores. Útil para /// debugging, persistir choices del usuario, o mapear desde strings /// en config. pub const fn name(self) -> &'static str { match self { Icon::File => "file", Icon::Folder => "folder", Icon::FolderOpen => "folder_open", Icon::Save => "save", Icon::Open => "open", Icon::Plus => "plus", Icon::Minus => "minus", Icon::X => "x", Icon::Check => "check", Icon::Edit => "edit", Icon::Trash => "trash", Icon::ChevronUp => "chevron_up", Icon::ChevronDown => "chevron_down", Icon::ChevronLeft => "chevron_left", Icon::ChevronRight => "chevron_right", Icon::Home => "home", Icon::Search => "search", Icon::Info => "info", Icon::Warning => "warning", Icon::Error => "error", Icon::Bell => "bell", Icon::Settings => "settings", Icon::More => "more", Icon::Play => "play", Icon::Pause => "pause", Icon::Stop => "stop", Icon::SkipBack => "skip_back", Icon::SkipForward => "skip_forward", Icon::Rewind => "rewind", Icon::FastForward => "fast_forward", Icon::Volume => "volume", Icon::VolumeMute => "volume_mute", Icon::Repeat => "repeat", Icon::Shuffle => "shuffle", Icon::Record => "record", Icon::Equalizer => "equalizer", Icon::Camera => "camera", Icon::Gauge => "gauge", Icon::Image => "image", Icon::Music => "music", Icon::Film => "film", Icon::Archive => "archive", Icon::Code => "code", Icon::FileText => "file_text", Icon::Link => "link", Icon::Font => "font", Icon::Grid => "grid", Icon::Rows => "rows", Icon::Table => "table", Icon::Columns => "columns", Icon::Globe => "globe", Icon::Mountain => "mountain", Icon::Leaf => "leaf", Icon::User => "user", Icon::Droplet => "droplet", Icon::Clock => "clock", Icon::Power => "power", Icon::Mail => "mail", Icon::Keyboard => "keyboard", Icon::Palette => "palette", Icon::Lock => "lock", Icon::Key => "key", Icon::Monitor => "monitor", Icon::Sparkle => "sparkle", Icon::Mic => "mic", Icon::Mouse => "mouse", Icon::Cloud => "cloud", Icon::Moon => "moon", Icon::Refresh => "refresh", Icon::Puzzle => "puzzle", Icon::Copy => "copy", Icon::Move => "move", Icon::Eyedropper => "eyedropper", Icon::Marquee => "marquee", Icon::Bucket => "bucket", Icon::Brush => "brush", Icon::Eraser => "eraser", Icon::Gradient => "gradient", Icon::Wand => "wand", Icon::Lasso => "lasso", Icon::Polygon => "polygon", Icon::Type => "type", Icon::Stamp => "stamp", Icon::Bandage => "bandage", Icon::Pen => "pen", Icon::Layers => "layers", Icon::SelectAll => "select_all", Icon::Expand => "expand", Icon::Contract => "contract", Icon::Scissors => "scissors", Icon::Paste => "paste", Icon::Duplicate => "duplicate", Icon::Crop => "crop", Icon::Trim => "trim", Icon::Mask => "mask", Icon::Invert => "invert", Icon::Mirror => "mirror", Icon::MirrorV => "mirror_v", Icon::Square => "square", Icon::Circle => "circle", Icon::RoundedSquare => "rounded_square", Icon::Star => "star", Icon::Hexagon => "hexagon", Icon::Line => "line", Icon::Flatten => "flatten", Icon::RotateCw => "rotate_cw", Icon::RotateCcw => "rotate_ccw", Icon::Shadow => "shadow", Icon::Brightness => "brightness", Icon::Contrast => "contrast", Icon::ColorWheel => "color_wheel", Icon::Blur => "blur", Icon::Levels => "levels", Icon::Curve => "curve", Icon::Segment => "segment", Icon::Upscale => "upscale", Icon::ListPrev => "list_prev", Icon::ListNext => "list_next", Icon::Eye => "eye", Icon::Heart => "heart", } } /// Devuelve el `BezPath` del icono en coords del grid 24×24. La /// app raramente lo necesita directamente — es lo que consume /// internamente [`icon_view`] / [`paint_icon`]. pub fn path(self) -> BezPath { match self { Icon::File => path_file(), Icon::Folder => path_folder(), Icon::FolderOpen => path_folder_open(), Icon::Save => path_save(), Icon::Open => path_open(), Icon::Plus => path_plus(), Icon::Minus => path_minus(), Icon::X => path_x(), Icon::Check => path_check(), Icon::Edit => path_edit(), Icon::Trash => path_trash(), Icon::ChevronUp => path_chevron(0.0), Icon::ChevronDown => path_chevron(180.0), Icon::ChevronLeft => path_chevron(270.0), Icon::ChevronRight => path_chevron(90.0), Icon::Home => path_home(), Icon::Search => path_search(), Icon::Info => path_info(), Icon::Warning => path_warning(), Icon::Error => path_error(), Icon::Bell => path_bell(), Icon::Settings => path_settings(), Icon::More => path_more(), Icon::Play => path_play(), Icon::Pause => path_pause(), Icon::Stop => path_stop(), Icon::SkipBack => path_skip(true), Icon::SkipForward => path_skip(false), Icon::Rewind => path_seek(true), Icon::FastForward => path_seek(false), Icon::Volume => path_volume(false), Icon::VolumeMute => path_volume(true), Icon::Repeat => path_repeat(), Icon::Shuffle => path_shuffle(), Icon::Record => path_record(), Icon::Equalizer => path_equalizer(), Icon::Camera => path_camera(), Icon::Gauge => path_gauge(), Icon::Image => path_image(), Icon::Music => path_music(), Icon::Film => path_film(), Icon::Archive => path_archive(), Icon::Code => path_code(), Icon::FileText => path_file_text(), Icon::Link => path_link(), Icon::Font => path_font(), Icon::Grid => path_grid(), Icon::Rows => path_rows(), Icon::Table => path_table(), Icon::Columns => path_columns(), Icon::Globe => path_globe(), Icon::Mountain => path_mountain(), Icon::Leaf => path_leaf(), Icon::User => path_user(), Icon::Droplet => path_droplet(), Icon::Clock => path_clock(), Icon::Power => path_power(), Icon::Mail => path_mail(), Icon::Keyboard => path_keyboard(), Icon::Palette => path_palette(), Icon::Lock => path_lock(), Icon::Key => path_key(), Icon::Monitor => path_monitor(), Icon::Sparkle => path_sparkle(), Icon::Mic => path_mic(), Icon::Mouse => path_mouse(), Icon::Cloud => path_cloud(), Icon::Moon => path_moon(), Icon::Refresh => path_refresh(), Icon::Puzzle => path_puzzle(), Icon::Copy => path_copy(), Icon::Move => path_move(), Icon::Eyedropper => path_eyedropper(), Icon::Marquee => path_marquee(), Icon::Bucket => path_bucket(), Icon::Brush => path_brush(), Icon::Eraser => path_eraser(), Icon::Gradient => path_gradient(), Icon::Wand => path_wand(), Icon::Lasso => path_lasso(), Icon::Polygon => path_polygon(), Icon::Type => path_type(), Icon::Stamp => path_stamp(), Icon::Bandage => path_bandage(), Icon::Pen => path_pen(), Icon::Layers => path_layers(), Icon::SelectAll => path_select_all(), Icon::Expand => path_expand(), Icon::Contract => path_contract(), Icon::Scissors => path_scissors(), Icon::Paste => path_paste(), Icon::Duplicate => path_duplicate(), Icon::Crop => path_crop(), Icon::Trim => path_trim(), Icon::Mask => path_mask(), Icon::Invert => path_invert(), Icon::Mirror => path_mirror(false), Icon::MirrorV => path_mirror(true), Icon::Square => path_square(), Icon::Circle => path_circulo(), Icon::RoundedSquare => path_rounded_square(), Icon::Star => path_star(), Icon::Hexagon => path_hexagon(), Icon::Line => path_line(), Icon::Flatten => path_flatten(), Icon::RotateCw => path_rotate(true), Icon::RotateCcw => path_rotate(false), Icon::Shadow => path_shadow(), Icon::Brightness => path_brightness(), Icon::Contrast => path_contrast(), Icon::ColorWheel => path_color_wheel(), Icon::Blur => path_blur(), Icon::Levels => path_levels(), Icon::Curve => path_curve(), Icon::Segment => path_segment(), Icon::Upscale => path_upscale(), Icon::ListPrev => path_list_skip(true), Icon::ListNext => path_list_skip(false), Icon::Eye => path_eye(), Icon::Heart => path_heart(), } } /// Mapea un **glifo unicode** (el que muchas apps usaban como ícono de /// texto) al `Icon` vectorial equivalente. Es el corazón del barrido /// glifo→vector: un sitio de render hace `from_glyph(s)` y, si hay match, /// pinta el vector (determinista en toda máquina) en vez del glifo de fuente /// (que en hardware sin esa fuente sale notdef/tofu). `None` ⇒ el caller cae /// a texto. Acepta el glifo con o sin variation-selector (`\u{FE0F}`). pub fn from_glyph(glifo: &str) -> Option { let g = glifo.trim().trim_end_matches('\u{FE0F}'); let mut chars = g.chars(); let c = chars.next()?; if chars.next().is_some() { // Más de un code-point (sin el VS16): no es un glifo-ícono simple. return None; } Some(match c { // Multimedia / transporte '▶' | '►' | '⏵' => Icon::Play, '⏸' => Icon::Pause, '⏹' | '■' => Icon::Stop, '⏺' | '●' | '◉' | '⚫' => Icon::Record, '⏮' | '⏪' => Icon::SkipBack, '⏭' | '⏩' => Icon::SkipForward, '🔀' => Icon::Shuffle, '🔁' | '🔂' => Icon::Repeat, '🔊' | '🔉' | '🔈' => Icon::Volume, '🔇' => Icon::VolumeMute, '♪' | '♫' | '🎵' | '🎶' => Icon::Music, '🎙' | '🎤' => Icon::Mic, '🎛' | '🎚' => Icon::Equalizer, '📷' | '📸' => Icon::Camera, '🎞' | '🎬' | '📽' => Icon::Film, '🖼' => Icon::Image, // Navegación / chevrons / flechas '▲' | '△' => Icon::ChevronUp, '▼' | '▽' => Icon::ChevronDown, '◀' | '◁' | '‹' => Icon::ChevronLeft, '▷' | '›' | '❯' | '❭' | '⟩' => Icon::ChevronRight, '⟳' | '🔃' | '🔄' | '↻' | '⥁' => Icon::Refresh, // Sistema / settings '⚙' | '🛠' | '🔧' => Icon::Settings, '☰' | '≡' | '≣' | '𝍢' => Icon::Rows, '▦' | '▤' | '⊞' | '𐩕' => Icon::Grid, '⏻' | '⏼' | '⭘' => Icon::Power, '🎨' => Icon::Palette, '⌨' => Icon::Keyboard, '🖥' | '💻' | '🖳' => Icon::Monitor, '🖱' => Icon::Mouse, '🔐' | '🔒' | '🔏' => Icon::Lock, '🔑' | '🗝' => Icon::Key, '✉' | '📧' | '📨' | '📩' => Icon::Mail, '🧩' => Icon::Puzzle, '☁' => Icon::Cloud, '🌐' | '🌍' | '🌎' | '🌏' => Icon::Globe, '🌙' | '🌚' | '☾' | '◐' => Icon::Moon, '✨' | '✦' | '✶' | '✷' | '❇' | '❋' | '★' | '☆' | '✩' => Icon::Sparkle, '◴' | '◷' | '◵' | '◶' | '🕐' | '🕒' | '⏰' | '⌚' => Icon::Clock, // Documento / contenido '📄' | '📃' | '📝' => Icon::FileText, '📁' | '📂' => Icon::Folder, '💾' => Icon::Save, '🔍' | '🔎' => Icon::Search, '🔔' => Icon::Bell, '🗑' => Icon::Trash, '📋' | '⧉' | '⎘' => Icon::Copy, '👤' | '🧑' | '🙍' => Icon::User, 'ℹ' | 'ⓘ' => Icon::Info, '⚠' => Icon::Warning, '➕' | '+' => Icon::Plus, '✕' | '✖' | '×' | '✗' => Icon::X, '✓' | '✔' => Icon::Check, // --- Los glifos de MENÚ que faltaban (2026-09-10) --- // // Un menú contextual de nahual pintaba once glifos y sólo cinco // resolvían a ícono: los otros seis caían al texto, y dos de ellos // (⛁, ⎙) ni siquiera existen en las fuentes de la máquina, así que // salían de **notdef** — la pila de líneas. Un menú con seis // vectores, tres glifos de fuente y dos notdef no se ve mal por un // detalle: se ve como cinco menús distintos apilados. '✎' | '✏' | '🖉' => Icon::Edit, '🔗' | '⛓' => Icon::Link, '⛁' | '🗜' | '📦' => Icon::Archive, '✂' | '✁' | '✃' | '✄' => Icon::Scissors, '✧' | '❈' | '✵' => Icon::Sparkle, '▸' => Icon::ChevronRight, '↺' | '↶' | '⎌' => Icon::Refresh, '▫' | '▭' | '⌗' => Icon::Code, '◇' | '◈' | '◆' => Icon::Layers, '⎙' | '🖨' => Icon::File, '⋯' | '…' => Icon::More, '⎉' => Icon::Duplicate, '⌫' | '⌦' => Icon::Eraser, '↗' | '⇱' | '⛶' => Icon::Expand, _ => return None, }) } } /// Vista de un glifo como **ícono vectorial** si [`Icon::from_glyph`] lo conoce, /// o como texto (su fallback histórico) si no. Es el reemplazo directo de /// `.text_aligned(glifo, …)` en los sitios de render que usaban íconos-glifo: /// el resultado es determinista en toda máquina cuando hay match. pub fn glyph_or_text_view( glifo: &str, size: f32, color: Color, stroke_width: f32, ) -> View { match Icon::from_glyph(glifo) { Some(icon) => icon_view(icon, color, stroke_width), None => View::new(Style { position: Position::Absolute, size: Size { width: percent(1.0_f32), height: percent(1.0_f32) }, ..Default::default() }) .text_aligned(glifo.to_string(), size, color, llimphi_compositor::llimphi_text::Alignment::Center), } } /// Construye un `View` que pinta el icono ocupando todo el rect del /// padre. El icono se escala uniformemente al mínimo lado y se centra. /// /// - `stroke_width` en unidades del grid 24×24 (típico: `1.6`). pub fn icon_view( icon: Icon, 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_icon(scene, rect, icon, color, stroke_width); }) } /// Pintor crudo — útil cuando una app quiere stampear varios iconos /// dentro del mismo `paint_with` (paneles compuestos, toolbars /// generadas dinámicamente). pub fn paint_icon( scene: &mut llimphi_compositor::Lienzo, rect: llimphi_compositor::PaintRect, icon: Icon, 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); } // ===================================================================== // Paths — todos en grid 24×24, origen top-left, eje Y hacia abajo // ===================================================================== // // Cada path es geometría minimalista. Los joins y caps son Round (los // fija el renderer), así que los corners salen suaves sin tener que // definir curvas extra. fn path_file() -> BezPath { // Documento: rectángulo con esquina superior-derecha plegada. let mut p = BezPath::new(); p.move_to((6.0, 3.0)); p.line_to((14.0, 3.0)); p.line_to((19.0, 8.0)); p.line_to((19.0, 21.0)); p.line_to((6.0, 21.0)); p.close_path(); // Pliegue: línea desde la esquina superior-derecha del file hasta // donde "se dobla", luego al borde. p.move_to((14.0, 3.0)); p.line_to((14.0, 8.0)); p.line_to((19.0, 8.0)); p } fn path_folder() -> BezPath { // Folder cerrado: cuerpo + lengüeta arriba-izquierda. let mut p = BezPath::new(); p.move_to((3.0, 8.0)); p.line_to((3.0, 19.0)); p.line_to((21.0, 19.0)); p.line_to((21.0, 8.0)); p.line_to((11.0, 8.0)); p.line_to((9.0, 5.0)); p.line_to((3.0, 5.0)); p.close_path(); p } fn path_folder_open() -> BezPath { // Folder con tapa levantada: el "techo" se inclina hacia la derecha. let mut p = BezPath::new(); // Caja inferior. p.move_to((3.0, 8.0)); p.line_to((3.0, 19.0)); p.line_to((21.0, 19.0)); p.line_to((23.0, 11.0)); p.line_to((7.0, 11.0)); p.line_to((5.0, 19.0)); // Lengüeta de la izquierda (sigue ahí). p.move_to((3.0, 8.0)); p.line_to((9.0, 8.0)); p.line_to((11.0, 11.0)); p.line_to((21.0, 11.0)); p } fn path_save() -> BezPath { // Floppy: cuadrado con muesca top-right y rectángulo de label abajo. let mut p = BezPath::new(); p.move_to((4.0, 4.0)); p.line_to((17.0, 4.0)); p.line_to((20.0, 7.0)); p.line_to((20.0, 20.0)); p.line_to((4.0, 20.0)); p.close_path(); // Slot del shutter arriba. p.move_to((8.0, 4.0)); p.line_to((8.0, 9.0)); p.line_to((15.0, 9.0)); p.line_to((15.0, 4.0)); // Rectángulo de label abajo. p.move_to((7.0, 13.0)); p.line_to((17.0, 13.0)); p.line_to((17.0, 20.0)); p.line_to((7.0, 20.0)); p.close_path(); p } fn path_open() -> BezPath { // Carpeta abriéndose hacia arriba con una flecha que entra. let mut p = BezPath::new(); // Folder base. p.move_to((3.0, 19.0)); p.line_to((3.0, 8.0)); p.line_to((9.0, 8.0)); p.line_to((11.0, 10.0)); p.line_to((21.0, 10.0)); p.line_to((21.0, 19.0)); p.close_path(); // Flecha entrando desde arriba al centro. p.move_to((12.0, 2.0)); p.line_to((12.0, 14.0)); p.move_to((9.0, 11.0)); p.line_to((12.0, 14.0)); p.line_to((15.0, 11.0)); p } fn path_plus() -> BezPath { let mut p = BezPath::new(); p.move_to((12.0, 5.0)); p.line_to((12.0, 19.0)); p.move_to((5.0, 12.0)); p.line_to((19.0, 12.0)); p } fn path_minus() -> BezPath { let mut p = BezPath::new(); p.move_to((5.0, 12.0)); p.line_to((19.0, 12.0)); p } fn path_x() -> BezPath { let mut p = BezPath::new(); p.move_to((6.0, 6.0)); p.line_to((18.0, 18.0)); p.move_to((18.0, 6.0)); p.line_to((6.0, 18.0)); p } fn path_check() -> BezPath { let mut p = BezPath::new(); p.move_to((5.0, 13.0)); p.line_to((10.0, 18.0)); p.line_to((20.0, 6.0)); p } fn path_edit() -> BezPath { // Lápiz: cuerpo diagonal + punta + tag de borrador. let mut p = BezPath::new(); p.move_to((4.0, 20.0)); p.line_to((8.0, 19.0)); p.line_to((20.0, 7.0)); p.line_to((17.0, 4.0)); p.line_to((5.0, 16.0)); p.close_path(); p.move_to((14.0, 7.0)); p.line_to((17.0, 10.0)); p } fn path_trash() -> BezPath { // Tacho: tapa con manijita + cuerpo con tres barras verticales. let mut p = BezPath::new(); // Tapa. p.move_to((4.0, 6.0)); p.line_to((20.0, 6.0)); // Manijita. p.move_to((9.0, 6.0)); p.line_to((9.0, 4.0)); p.line_to((15.0, 4.0)); p.line_to((15.0, 6.0)); // Cuerpo. p.move_to((6.0, 6.0)); p.line_to((7.0, 21.0)); p.line_to((17.0, 21.0)); p.line_to((18.0, 6.0)); // Barras internas. p.move_to((10.0, 10.0)); p.line_to((10.0, 17.0)); p.move_to((14.0, 10.0)); p.line_to((14.0, 17.0)); p } /// Chevron apuntando hacia arriba (default) o rotado por `angle_deg` /// alrededor del centro del grid (12, 12). 90° = derecha, 180° = abajo, /// 270° = izquierda. fn path_chevron(angle_deg: f64) -> BezPath { let mut p = BezPath::new(); // Chevron base: forma de ^ apuntando arriba. p.move_to((6.0, 14.0)); p.line_to((12.0, 8.0)); p.line_to((18.0, 14.0)); let theta = angle_deg.to_radians(); let center = (12.0, 12.0); Affine::translate(center) * Affine::rotate(theta) * Affine::translate((-center.0, -center.1)) * p } fn path_home() -> BezPath { // Casa: triángulo de techo + caja rectangular. let mut p = BezPath::new(); p.move_to((3.0, 12.0)); p.line_to((12.0, 4.0)); p.line_to((21.0, 12.0)); // Cuerpo. p.move_to((5.0, 11.0)); p.line_to((5.0, 20.0)); p.line_to((19.0, 20.0)); p.line_to((19.0, 11.0)); // Puerta. p.move_to((10.0, 20.0)); p.line_to((10.0, 14.0)); p.line_to((14.0, 14.0)); p.line_to((14.0, 20.0)); p } fn path_search() -> BezPath { // Lupa: círculo (poligonal 16 segmentos) + mango diagonal. let mut p = BezPath::new(); let cx = 10.5; let cy = 10.5; let r = 5.5; let segments = 24; for i in 0..=segments { let theta = core::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)); } } // Mango. p.move_to((14.5, 14.5)); p.line_to((20.0, 20.0)); p } fn path_info() -> BezPath { // i: círculo + punto arriba + barra abajo. let mut p = path_circle(12.0, 12.0, 9.0, 32); // Punto. p.move_to((12.0, 7.0)); p.line_to((12.0, 8.5)); // Barra. p.move_to((12.0, 11.0)); p.line_to((12.0, 17.0)); p } fn path_warning() -> BezPath { // Triángulo con ! adentro. let mut p = BezPath::new(); p.move_to((12.0, 3.0)); p.line_to((22.0, 21.0)); p.line_to((2.0, 21.0)); p.close_path(); p.move_to((12.0, 10.0)); p.line_to((12.0, 15.0)); p.move_to((12.0, 17.5)); p.line_to((12.0, 18.5)); p } fn path_error() -> BezPath { // Círculo con X adentro. let mut p = path_circle(12.0, 12.0, 9.0, 32); p.move_to((8.5, 8.5)); p.line_to((15.5, 15.5)); p.move_to((15.5, 8.5)); p.line_to((8.5, 15.5)); p } fn path_bell() -> BezPath { // Campana: domo + base + badajo. let mut p = BezPath::new(); // Cuerpo con curva suave. p.move_to((5.0, 17.0)); p.curve_to((5.0, 8.0), (8.0, 5.0), (12.0, 5.0)); p.curve_to((16.0, 5.0), (19.0, 8.0), (19.0, 17.0)); p.close_path(); // Base. p.move_to((3.5, 17.0)); p.line_to((20.5, 17.0)); // Badajo. p.move_to((10.5, 20.0)); p.line_to((13.5, 20.0)); p } fn path_settings() -> BezPath { // Engranaje: 8 dientes radiales + agujero central. let mut p = BezPath::new(); let cx = 12.0; let cy = 12.0; let inner_r = 6.5; let outer_r = 9.5; let teeth = 8; for i in 0..teeth * 2 { let theta = core::f64::consts::TAU * (i as f64) / (teeth as f64 * 2.0); // Cada paso alterna entre inner y outer para formar los dientes. let r = if i % 2 == 0 { outer_r } else { inner_r }; 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.close_path(); // Agujero central. let inner = path_circle(cx, cy, 3.0, 16); for el in inner.elements() { p.push(*el); } p } fn path_more() -> BezPath { // Tres puntos horizontales (cada "punto" es un círculo pequeño). let mut p = BezPath::new(); for (cx, cy) in &[(6.0, 12.0), (12.0, 12.0), (18.0, 12.0)] { let dot = path_circle(*cx, *cy, 1.5, 12); for el in dot.elements() { p.push(*el); } } p } /// Helper: aproxima un círculo con `segments` lados rectos. Para iconos /// stroke esto se ve liso a partir de ~16 segmentos por la suavidad del /// Cap::Round. Más barato y más predecible que cubic Beziers para los /// glifos chiquitos donde vivimos. fn path_circle(cx: f64, cy: f64, r: f64, segments: usize) -> BezPath { let mut p = BezPath::new(); for i in 0..=segments { let theta = core::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 } // --------------------------------------------------------------------- // Dominio — creador de mundos (llimphi-voxel-studio) // --------------------------------------------------------------------- fn path_globe() -> BezPath { // Esfera + un meridiano (elipse angosta) + dos paralelos. let mut p = path_circle(12.0, 12.0, 9.0, 32); p.move_to((12.0, 3.0)); p.curve_to((6.5, 7.0), (6.5, 17.0), (12.0, 21.0)); p.move_to((12.0, 3.0)); p.curve_to((17.5, 7.0), (17.5, 17.0), (12.0, 21.0)); p.move_to((3.6, 9.3)); p.line_to((20.4, 9.3)); p.move_to((3.6, 14.7)); p.line_to((20.4, 14.7)); p } fn path_mountain() -> BezPath { // Cordillera de dos picos + nieve en el mayor. let mut p = BezPath::new(); p.move_to((2.0, 20.0)); p.line_to((8.5, 8.0)); p.line_to((13.0, 15.0)); p.line_to((16.0, 11.0)); p.line_to((22.0, 20.0)); p.close_path(); // Nieve del pico mayor. p.move_to((6.6, 11.5)); p.line_to((8.5, 8.0)); p.line_to((10.3, 11.0)); p } fn path_leaf() -> BezPath { // Hoja: contorno por dos curvas + nervadura central. let mut p = BezPath::new(); p.move_to((5.0, 19.0)); p.curve_to((5.0, 9.0), (12.0, 4.0), (19.0, 5.0)); p.curve_to((20.0, 12.0), (15.0, 19.0), (5.0, 19.0)); p.close_path(); p.move_to((7.5, 16.5)); p.line_to((16.5, 7.5)); p } fn path_user() -> BezPath { // Persona: cabeza (círculo) + hombros (arco). let mut p = path_circle(12.0, 8.0, 4.0, 24); p.move_to((4.5, 20.0)); p.curve_to((4.5, 14.5), (19.5, 14.5), (19.5, 20.0)); p } fn path_droplet() -> BezPath { // Gota: punta arriba, panza redonda abajo. let mut p = BezPath::new(); p.move_to((12.0, 3.0)); p.curve_to((12.0, 3.0), (18.5, 11.0), (18.5, 15.5)); p.curve_to((18.5, 19.5), (15.5, 21.5), (12.0, 21.5)); p.curve_to((8.5, 21.5), (5.5, 19.5), (5.5, 15.5)); p.curve_to((5.5, 11.0), (12.0, 3.0), (12.0, 3.0)); p.close_path(); p } // --------------------------------------------------------------------- // Multimedia — transporte de reproductor (media-app y demás) // --------------------------------------------------------------------- fn append(dst: &mut BezPath, src: &BezPath) { for el in src.elements() { dst.push(*el); } } fn path_play() -> BezPath { // Triángulo apuntando a la derecha. let mut p = BezPath::new(); p.move_to((8.0, 5.0)); p.line_to((8.0, 19.0)); p.line_to((18.0, 12.0)); p.close_path(); p } fn path_pause() -> BezPath { // Dos barras verticales. let mut p = BezPath::new(); p.move_to((9.0, 6.0)); p.line_to((9.0, 18.0)); p.move_to((15.0, 6.0)); p.line_to((15.0, 18.0)); p } fn path_stop() -> BezPath { let mut p = BezPath::new(); p.move_to((7.0, 7.0)); p.line_to((17.0, 7.0)); p.line_to((17.0, 17.0)); p.line_to((7.0, 17.0)); p.close_path(); p } /// Saltar pista: barra + triángulo (a la izquierda si `back`). fn path_skip(back: bool) -> BezPath { let mut p = BezPath::new(); if back { p.move_to((7.0, 6.0)); p.line_to((7.0, 18.0)); p.move_to((17.0, 6.0)); p.line_to((17.0, 18.0)); p.line_to((8.0, 12.0)); p.close_path(); } else { p.move_to((7.0, 6.0)); p.line_to((7.0, 18.0)); p.line_to((16.0, 12.0)); p.close_path(); p.move_to((17.0, 6.0)); p.line_to((17.0, 18.0)); } p } /// Lista anterior/siguiente: tres renglones (la lista) y una punta que sale /// hacia ese lado. Un solo glifo, en el mismo trazo de 24×24 que el resto del /// transporte — antes eran los textos `||<` y `>||`, que en la barra se leían /// como dos caracteres sueltos al lado de seis íconos vectoriales. fn path_eye() -> BezPath { // Ojo: la almendra (dos cuadráticas de punta a punta) + la pupila. let mut p = BezPath::new(); p.move_to((2.0, 12.0)); p.quad_to((12.0, 2.5), (22.0, 12.0)); p.quad_to((12.0, 21.5), (2.0, 12.0)); p.close_path(); let (cx, cy, r) = (12.0, 12.0, 3.2); let segments = 20; for i in 0..=segments { let theta = core::f64::consts::TAU * (i as f64) / (segments as f64); let (x, y) = (cx + r * theta.cos(), cy + r * theta.sin()); if i == 0 { p.move_to((x, y)); } else { p.line_to((x, y)); } } p } fn path_list_skip(back: bool) -> BezPath { let mut p = BezPath::new(); if back { // Punta a la izquierda contra su barra, y la lista a la derecha. p.move_to((4.0, 6.0)); p.line_to((4.0, 18.0)); p.move_to((11.0, 7.0)); p.line_to((11.0, 17.0)); p.line_to((5.0, 12.0)); p.close_path(); for y in [7.5_f64, 12.0, 16.5] { p.move_to((14.0, y)); p.line_to((20.0, y)); } } else { for y in [7.5_f64, 12.0, 16.5] { p.move_to((4.0, y)); p.line_to((10.0, y)); } p.move_to((13.0, 7.0)); p.line_to((13.0, 17.0)); p.line_to((19.0, 12.0)); p.close_path(); p.move_to((20.0, 6.0)); p.line_to((20.0, 18.0)); } p } /// Avance rápido: dos triángulos (a la izquierda si `rewind`). fn path_seek(rewind: bool) -> BezPath { let mut p = BezPath::new(); if rewind { p.move_to((11.0, 6.0)); p.line_to((11.0, 18.0)); p.line_to((4.0, 12.0)); p.close_path(); p.move_to((20.0, 6.0)); p.line_to((20.0, 18.0)); p.line_to((13.0, 12.0)); p.close_path(); } else { p.move_to((4.0, 6.0)); p.line_to((4.0, 18.0)); p.line_to((11.0, 12.0)); p.close_path(); p.move_to((13.0, 6.0)); p.line_to((13.0, 18.0)); p.line_to((20.0, 12.0)); p.close_path(); } p } /// Altavoz; con ondas (normal) o con una X (mute). fn path_volume(mute: bool) -> BezPath { let mut p = BezPath::new(); p.move_to((3.0, 9.0)); p.line_to((8.0, 9.0)); p.line_to((12.0, 5.0)); p.line_to((12.0, 19.0)); p.line_to((8.0, 15.0)); p.line_to((3.0, 15.0)); p.close_path(); if mute { p.move_to((15.0, 9.0)); p.line_to((21.0, 15.0)); p.move_to((21.0, 9.0)); p.line_to((15.0, 15.0)); } else { p.move_to((15.0, 9.0)); p.quad_to((17.5, 12.0), (15.0, 15.0)); p.move_to((17.5, 7.0)); p.quad_to((21.5, 12.0), (17.5, 17.0)); } p } fn path_repeat() -> BezPath { // Dos flechas horizontales opuestas (loop compacto). let mut p = BezPath::new(); p.move_to((6.0, 9.0)); p.line_to((16.0, 9.0)); p.move_to((14.0, 7.0)); p.line_to((17.0, 9.0)); p.line_to((14.0, 11.0)); p.move_to((18.0, 15.0)); p.line_to((8.0, 15.0)); p.move_to((10.0, 13.0)); p.line_to((7.0, 15.0)); p.line_to((10.0, 17.0)); p } fn path_shuffle() -> BezPath { // Dos flechas que se cruzan. let mut p = BezPath::new(); p.move_to((5.0, 8.0)); p.line_to((19.0, 16.0)); p.move_to((16.0, 15.5)); p.line_to((20.0, 16.5)); p.line_to((17.5, 13.0)); p.move_to((5.0, 16.0)); p.line_to((19.0, 8.0)); p.move_to((17.5, 11.0)); p.line_to((20.0, 7.5)); p.line_to((16.0, 8.5)); p } fn path_record() -> BezPath { path_circle(12.0, 12.0, 5.0, 20) } fn path_equalizer() -> BezPath { let mut p = BezPath::new(); // Tres deslizadores verticales. p.move_to((7.0, 5.0)); p.line_to((7.0, 19.0)); p.move_to((12.0, 5.0)); p.line_to((12.0, 19.0)); p.move_to((17.0, 5.0)); p.line_to((17.0, 19.0)); // Perillas a distinta altura. p.move_to((5.0, 9.0)); p.line_to((9.0, 9.0)); p.move_to((10.0, 14.0)); p.line_to((14.0, 14.0)); p.move_to((15.0, 8.0)); p.line_to((19.0, 8.0)); p } fn path_camera() -> BezPath { let mut p = BezPath::new(); p.move_to((4.0, 8.0)); p.line_to((7.0, 8.0)); p.line_to((9.0, 6.0)); p.line_to((15.0, 6.0)); p.line_to((17.0, 8.0)); p.line_to((20.0, 8.0)); p.line_to((20.0, 18.0)); p.line_to((4.0, 18.0)); p.close_path(); append(&mut p, &path_circle(12.0, 13.0, 3.5, 16)); p } fn path_gauge() -> BezPath { // Esfera + aguja (velocidad). let mut p = path_circle(12.0, 13.0, 6.0, 20); p.move_to((12.0, 13.0)); p.line_to((16.0, 9.0)); p } // --------------------------------------------------------------------- // Archivos — tipos por extensión (listados de file manager / shell) // --------------------------------------------------------------------- fn path_image() -> BezPath { // Marco con una montaña y un sol (el clásico "imagen"). let mut p = BezPath::new(); p.move_to((4.0, 5.0)); p.line_to((20.0, 5.0)); p.line_to((20.0, 19.0)); p.line_to((4.0, 19.0)); p.close_path(); // Sol. append(&mut p, &path_circle(8.5, 9.5, 1.6, 12)); // Montaña (línea quebrada hasta el borde derecho). p.move_to((4.0, 17.0)); p.line_to((10.0, 12.0)); p.line_to((14.0, 15.0)); p.line_to((17.0, 12.0)); p.line_to((20.0, 15.0)); p } fn path_music() -> BezPath { // Nota musical: dos cabezas redondas unidas por una plica con bandera. let mut p = BezPath::new(); // Plicas. p.move_to((9.0, 18.0)); p.line_to((9.0, 6.0)); p.line_to((19.0, 4.0)); p.line_to((19.0, 16.0)); // Cabeza izquierda. append(&mut p, &path_circle(7.0, 18.0, 2.0, 14)); // Cabeza derecha. append(&mut p, &path_circle(17.0, 16.0, 2.0, 14)); p } fn path_film() -> BezPath { // Tira de película: rectángulo con perforaciones a los lados. let mut p = BezPath::new(); p.move_to((4.0, 5.0)); p.line_to((20.0, 5.0)); p.line_to((20.0, 19.0)); p.line_to((4.0, 19.0)); p.close_path(); // Rieles internos (separan perforaciones del cuadro central). p.move_to((8.0, 5.0)); p.line_to((8.0, 19.0)); p.move_to((16.0, 5.0)); p.line_to((16.0, 19.0)); // Perforaciones (cuatro tics por lado). for y in [7.5, 11.0, 14.5] { p.move_to((4.0, y)); p.line_to((8.0, y)); p.move_to((16.0, y)); p.line_to((20.0, y)); } p } fn path_archive() -> BezPath { // Caja/paquete: tapa arriba + cuerpo + tirador del cierre. let mut p = BezPath::new(); // Tapa. p.move_to((3.0, 5.0)); p.line_to((21.0, 5.0)); p.line_to((21.0, 9.0)); p.line_to((3.0, 9.0)); p.close_path(); // Cuerpo. p.move_to((4.5, 9.0)); p.line_to((4.5, 20.0)); p.line_to((19.5, 20.0)); p.line_to((19.5, 9.0)); // Pestaña del cierre. p.move_to((10.0, 12.0)); p.line_to((14.0, 12.0)); p } fn path_code() -> BezPath { // Corchetes angulares — universal para "código". let mut p = BezPath::new(); // Chevron izquierdo. p.move_to((9.0, 7.0)); p.line_to((4.0, 12.0)); p.line_to((9.0, 17.0)); // Chevron derecho. p.move_to((15.0, 7.0)); p.line_to((20.0, 12.0)); p.line_to((15.0, 17.0)); // Barra diagonal central. p.move_to((13.0, 6.0)); p.line_to((11.0, 18.0)); p } fn path_file_text() -> BezPath { // Documento (como `file`) con líneas de texto adentro. let mut p = path_file(); p.move_to((8.5, 12.0)); p.line_to((16.5, 12.0)); p.move_to((8.5, 15.0)); p.line_to((16.5, 15.0)); p.move_to((8.5, 18.0)); p.line_to((13.5, 18.0)); p } fn path_link() -> BezPath { // Symlink: dos eslabones de cadena en diagonal. let mut p = BezPath::new(); // Eslabón superior-izquierdo (cápsula inclinada). p.move_to((10.0, 14.0)); p.line_to((7.0, 11.0)); p.curve_to((5.0, 9.0), (5.0, 7.0), (7.0, 5.0)); p.curve_to((9.0, 3.0), (11.0, 3.0), (13.0, 5.0)); p.line_to((15.0, 7.0)); // Eslabón inferior-derecho. p.move_to((14.0, 10.0)); p.line_to((17.0, 13.0)); p.curve_to((19.0, 15.0), (19.0, 17.0), (17.0, 19.0)); p.curve_to((15.0, 21.0), (13.0, 21.0), (11.0, 19.0)); p.line_to((9.0, 17.0)); p } fn path_font() -> BezPath { // Letra "A" serif — glifo de fuente tipográfica. let mut p = BezPath::new(); // Astas de la A. p.move_to((6.0, 20.0)); p.line_to((12.0, 4.0)); p.line_to((18.0, 20.0)); // Travesaño. p.move_to((8.5, 14.0)); p.line_to((15.5, 14.0)); // Serifas inferiores. p.move_to((4.5, 20.0)); p.line_to((7.5, 20.0)); p.move_to((16.5, 20.0)); p.line_to((19.5, 20.0)); p } fn path_grid() -> BezPath { let mut p = BezPath::new(); // Cuatro celdas 2x2. for (x, y) in [(4.0, 4.0), (13.0, 4.0), (4.0, 13.0), (13.0, 13.0)] { p.move_to((x, y)); p.line_to((x + 7.0, y)); p.line_to((x + 7.0, y + 7.0)); p.line_to((x, y + 7.0)); p.close_path(); } p } fn path_rows() -> BezPath { let mut p = BezPath::new(); // Tres filas (vista lista). for y in [6.0, 12.0, 18.0] { p.move_to((4.0, y)); p.line_to((20.0, y)); } p } fn path_table() -> BezPath { let mut p = BezPath::new(); // Marco. p.move_to((4.0, 5.0)); p.line_to((20.0, 5.0)); p.line_to((20.0, 19.0)); p.line_to((4.0, 19.0)); p.close_path(); // Fila de encabezado. p.move_to((4.0, 9.5)); p.line_to((20.0, 9.5)); // Separador de columnas (debajo del encabezado). p.move_to((11.0, 9.5)); p.line_to((11.0, 19.0)); p } fn path_columns() -> BezPath { let mut p = BezPath::new(); // Dos paneles verticales lado a lado (modo dual). p.move_to((4.0, 4.0)); p.line_to((11.0, 4.0)); p.line_to((11.0, 20.0)); p.line_to((4.0, 20.0)); p.close_path(); p.move_to((13.0, 4.0)); p.line_to((20.0, 4.0)); p.line_to((20.0, 20.0)); p.line_to((13.0, 20.0)); p.close_path(); p } // ---- Sweep glifo→vector: íconos de sistema/settings ----------------------- fn path_clock() -> BezPath { let mut p = path_circle(12.0, 12.0, 8.5, 28); p.move_to((12.0, 12.0)); p.line_to((12.0, 6.5)); // manecilla de hora p.move_to((12.0, 12.0)); p.line_to((16.0, 13.5)); // minutero p } /// **La misma esfera, con las manecillas a una hora dada.** /// /// [`path_clock`] es un glifo: sus dos agujas están **clavadas** (la de hora a /// las 12, el minutero apuntando a las dos y cuarto), porque un ícono de /// catálogo no sabe qué hora es. Esta versión sí: recibe `hour` (0..=23) y /// `minute` (0..=59) y devuelve el mismo dibujo —mismo centro, mismo radio, /// mismo grid de 24×24— con las agujas donde de verdad van. /// /// Existe acá y no del lado de quien la pinta porque el reloj de un rail, el de /// un panel y el de una app tienen que ser **el mismo dibujo**: un segundo /// reloj dibujado a mano en otro crate es cómo dos relojes de la misma suite /// terminan con distinto largo de aguja. /// /// La de hora es más corta y avanza con los minutos (a las 6:30 apunta entre el /// 6 y el 7, no al 6 clavado), que es lo que distingue un reloj de un dibujo de /// reloj. pub fn path_clock_at(hour: u8, minute: u8) -> BezPath { let m = (minute % 60) as f64; let h = (hour % 12) as f64 + m / 60.0; // Ángulos desde las 12 y en sentido horario: 30° por hora, 6° por minuto. let ang_h = core::f64::consts::TAU * h / 12.0; let ang_m = core::f64::consts::TAU * m / 60.0; // En el grid el eje Y va hacia abajo, así que «arriba» es `-cos`. let aguja = |p: &mut BezPath, ang: f64, largo: f64| { p.move_to((12.0, 12.0)); p.line_to((12.0 + largo * ang.sin(), 12.0 - largo * ang.cos())); }; let mut p = path_circle(12.0, 12.0, 8.5, 28); aguja(&mut p, ang_h, 4.4); // manecilla de hora aguja(&mut p, ang_m, 6.2); // minutero p } /// Pinta [`path_clock_at`] en `rect` con el mismo encuadre que [`paint_icon`]: /// cuadrado centrado, grid de 24×24, joins y caps redondos. /// /// Es un hermano de `paint_icon` y no un `Icon` del catálogo porque un `Icon` es /// `Copy` y sin parámetros — la hora no entra en el enum sin volverlo un enum /// con datos, que es exactamente lo que el resto del catálogo no es. pub fn paint_clock_at( scene: &mut llimphi_compositor::Lienzo, rect: llimphi_compositor::PaintRect, color: Color, stroke_width: f32, hour: u8, minute: u8, ) { 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); scene.stroke(&stroke, xform, color, None, &path_clock_at(hour, minute)); } fn path_power() -> BezPath { use core::f64::consts::{FRAC_PI_2, TAU}; let mut p = BezPath::new(); let (cx, cy, r) = (12.0, 13.0, 7.0); let segs = 28; // Anillo con un hueco arriba (clásico símbolo de encendido). let start = -FRAC_PI_2 + 0.55; let end = -FRAC_PI_2 - 0.55 + TAU; for i in 0..=segs { let t = start + (end - start) * (i as f64) / (segs as f64); let (x, y) = (cx + r * t.cos(), cy + r * t.sin()); if i == 0 { p.move_to((x, y)); } else { p.line_to((x, y)); } } p.move_to((12.0, 4.0)); p.line_to((12.0, 12.0)); // barra vertical p } fn path_mail() -> BezPath { let mut p = BezPath::new(); p.move_to((3.5, 6.0)); p.line_to((20.5, 6.0)); p.line_to((20.5, 18.0)); p.line_to((3.5, 18.0)); p.close_path(); p.move_to((3.5, 6.5)); p.line_to((12.0, 13.0)); p.line_to((20.5, 6.5)); // solapa p } fn path_keyboard() -> BezPath { let mut p = BezPath::new(); p.move_to((2.5, 7.0)); p.line_to((21.5, 7.0)); p.line_to((21.5, 17.0)); p.line_to((2.5, 17.0)); p.close_path(); for (x, y) in [(5.5, 10.0), (9.0, 10.0), (12.5, 10.0), (16.0, 10.0)] { p.move_to((x, y)); p.line_to((x + 1.6, y)); } p.move_to((8.0, 13.8)); p.line_to((16.0, 13.8)); // barra espaciadora p } fn path_palette() -> BezPath { let mut p = path_circle(12.0, 12.0, 8.5, 28); let hueco = path_circle(12.0, 16.5, 1.8, 12); // agujero del pulgar for el in hueco.elements() { p.push(*el); } for (cx, cy) in [(8.3, 9.0), (12.0, 7.4), (15.7, 9.4)] { let muestra = path_circle(cx, cy, 1.0, 10); for el in muestra.elements() { p.push(*el); } } p } fn path_lock() -> BezPath { let mut p = BezPath::new(); p.move_to((6.0, 11.0)); p.line_to((18.0, 11.0)); p.line_to((18.0, 20.0)); p.line_to((6.0, 20.0)); p.close_path(); p.move_to((8.5, 11.0)); p.line_to((8.5, 8.0)); p.curve_to((8.5, 5.0), (15.5, 5.0), (15.5, 8.0)); p.line_to((15.5, 11.0)); // arco p } fn path_key() -> BezPath { let mut p = path_circle(8.0, 8.0, 3.5, 18); // ojo p.move_to((10.5, 10.5)); p.line_to((19.0, 19.0)); // caña p.move_to((16.5, 16.5)); p.line_to((19.0, 14.0)); // diente p } fn path_monitor() -> BezPath { let mut p = BezPath::new(); p.move_to((3.0, 5.0)); p.line_to((21.0, 5.0)); p.line_to((21.0, 16.0)); p.line_to((3.0, 16.0)); p.close_path(); p.move_to((9.5, 19.0)); p.line_to((14.5, 19.0)); // base p.move_to((12.0, 16.0)); p.line_to((12.0, 19.0)); // cuello p } fn path_sparkle() -> BezPath { let mut p = BezPath::new(); p.move_to((12.0, 3.0)); p.line_to((13.7, 10.3)); p.line_to((21.0, 12.0)); p.line_to((13.7, 13.7)); p.line_to((12.0, 21.0)); p.line_to((10.3, 13.7)); p.line_to((3.0, 12.0)); p.line_to((10.3, 10.3)); p.close_path(); p } fn path_mic() -> BezPath { let mut p = BezPath::new(); p.move_to((9.0, 6.0)); p.curve_to((9.0, 3.5), (15.0, 3.5), (15.0, 6.0)); p.line_to((15.0, 11.0)); p.curve_to((15.0, 13.5), (9.0, 13.5), (9.0, 11.0)); p.close_path(); p.move_to((6.5, 11.0)); p.curve_to((6.5, 16.0), (17.5, 16.0), (17.5, 11.0)); // soporte p.move_to((12.0, 16.0)); p.line_to((12.0, 20.0)); p.move_to((8.5, 20.0)); p.line_to((15.5, 20.0)); p } fn path_mouse() -> BezPath { let mut p = BezPath::new(); p.move_to((7.0, 8.0)); p.curve_to((7.0, 4.0), (17.0, 4.0), (17.0, 8.0)); p.line_to((17.0, 16.0)); p.curve_to((17.0, 20.0), (7.0, 20.0), (7.0, 16.0)); p.close_path(); p.move_to((12.0, 5.0)); p.line_to((12.0, 9.0)); // rueda p } fn path_cloud() -> BezPath { let mut p = BezPath::new(); p.move_to((7.0, 17.0)); p.curve_to((3.5, 17.0), (3.5, 12.0), (7.0, 12.0)); p.curve_to((7.0, 7.5), (13.5, 7.0), (14.5, 11.0)); p.curve_to((18.5, 10.5), (20.5, 16.0), (17.0, 17.0)); p.close_path(); p } /// Un corazón, en la caja de 24 de todo el juego. /// /// Dos lóbulos y una punta abajo, con las mismas curvas cúbicas que el resto: /// se dibuja **cerrado** para que el relleno lo pinte macizo como los demás /// dientes, y no como un contorno que a 20 px se lee como una mancha. fn path_heart() -> BezPath { let mut p = BezPath::new(); p.move_to((12.0, 20.5)); // El lóbulo izquierdo, subiendo desde la punta. p.curve_to((3.0, 14.5), (2.0, 10.5), (3.5, 7.5)); p.curve_to((5.5, 3.5), (10.5, 4.0), (12.0, 8.0)); // Y el derecho, en espejo. p.curve_to((13.5, 4.0), (18.5, 3.5), (20.5, 7.5)); p.curve_to((22.0, 10.5), (21.0, 14.5), (12.0, 20.5)); p.close_path(); p } fn path_moon() -> BezPath { let mut p = BezPath::new(); p.move_to((15.0, 4.5)); p.curve_to((9.0, 6.0), (9.0, 18.0), (15.0, 19.5)); p.curve_to((10.0, 17.0), (10.0, 7.0), (15.0, 4.5)); p.close_path(); p } fn path_refresh() -> BezPath { let mut p = BezPath::new(); let (cx, cy, r) = (12.0, 12.0, 7.0); let segs = 24; let (start, end) = (-2.2_f64, 2.2_f64); for i in 0..=segs { let t = start + (end - start) * (i as f64) / (segs as f64); let (x, y) = (cx + r * t.cos(), cy + r * t.sin()); if i == 0 { p.move_to((x, y)); } else { p.line_to((x, y)); } } let (ex, ey) = (cx + r * end.cos(), cy + r * end.sin()); p.move_to((ex - 1.6, ey - 1.9)); p.line_to((ex, ey)); p.line_to((ex + 2.2, ey - 0.7)); // cabeza de flecha p } fn path_copy() -> BezPath { // Dos hojas: la de adelante entera, la de atrás asomando arriba-izquierda // como una "L". Es el glifo universal de copiar — se reconoce sin rótulo, // que es justamente para lo que se agregó (los chips «copiar» de shuma). let mut p = BezPath::new(); p.move_to((16.0, 8.0)); p.line_to((16.0, 4.0)); p.line_to((4.0, 4.0)); p.line_to((4.0, 16.0)); p.line_to((8.0, 16.0)); p.move_to((8.0, 8.0)); p.line_to((20.0, 8.0)); p.line_to((20.0, 20.0)); p.line_to((8.0, 20.0)); p.close_path(); p } fn path_puzzle() -> BezPath { let mut p = BezPath::new(); p.move_to((5.0, 9.0)); p.line_to((9.5, 9.0)); p.curve_to((9.5, 6.0), (14.5, 6.0), (14.5, 9.0)); p.line_to((19.0, 9.0)); p.line_to((19.0, 19.0)); p.line_to((5.0, 19.0)); p.close_path(); p } // --------------------------------------------------------------------- // Herramientas de pintura — la caja de tullpu (y de cualquier editor) // // Son los catorce glifos que antes no existían y obligaban a rotular cada // herramienta con texto («○ pincel (p)»): una columna de palabras donde // debería haber una columna de dibujos. Cada uno tiene que leerse a 20 px. // --------------------------------------------------------------------- fn path_move() -> BezPath { // Cruz de cuatro flechas: los dos ejes completos + las puntas. let mut p = BezPath::new(); p.move_to((12.0, 2.5)); p.line_to((12.0, 21.5)); p.move_to((2.5, 12.0)); p.line_to((21.5, 12.0)); // Puntas: arriba, abajo, izquierda, derecha. p.move_to((9.0, 5.5)); p.line_to((12.0, 2.5)); p.line_to((15.0, 5.5)); p.move_to((9.0, 18.5)); p.line_to((12.0, 21.5)); p.line_to((15.0, 18.5)); p.move_to((5.5, 9.0)); p.line_to((2.5, 12.0)); p.line_to((5.5, 15.0)); p.move_to((18.5, 9.0)); p.line_to((21.5, 12.0)); p.line_to((18.5, 15.0)); p } fn path_eyedropper() -> BezPath { // Pipeta en diagonal: punta abajo-izquierda, bulbo arriba-derecha. let mut p = BezPath::new(); p.move_to((3.5, 20.5)); p.line_to((5.5, 15.5)); p.line_to((13.0, 8.0)); p.line_to((16.0, 11.0)); p.line_to((8.5, 18.5)); p.close_path(); p.move_to((14.5, 4.0)); p.line_to((20.0, 9.5)); p.line_to((17.0, 12.5)); p.line_to((11.5, 7.0)); p.close_path(); p } fn path_marquee() -> BezPath { // Marco de línea cortada: esquinas enteras y guiones en los lados. let mut p = BezPath::new(); p.move_to((4.0, 7.5)); p.line_to((4.0, 4.0)); p.line_to((7.5, 4.0)); p.move_to((10.5, 4.0)); p.line_to((13.5, 4.0)); p.move_to((16.5, 4.0)); p.line_to((20.0, 4.0)); p.line_to((20.0, 7.5)); p.move_to((20.0, 10.5)); p.line_to((20.0, 13.5)); p.move_to((20.0, 16.5)); p.line_to((20.0, 20.0)); p.line_to((16.5, 20.0)); p.move_to((13.5, 20.0)); p.line_to((10.5, 20.0)); p.move_to((7.5, 20.0)); p.line_to((4.0, 20.0)); p.line_to((4.0, 16.5)); p.move_to((4.0, 13.5)); p.line_to((4.0, 10.5)); p } fn path_bucket() -> BezPath { // Balde: cuerpo trapezoidal + asa + la gota que cae por el pico. let mut p = BezPath::new(); p.move_to((4.0, 7.5)); p.line_to((16.0, 7.5)); p.line_to((14.0, 20.0)); p.line_to((6.0, 20.0)); p.close_path(); p.move_to((6.5, 7.5)); p.curve_to((6.5, 3.0), (13.5, 3.0), (13.5, 7.5)); p.move_to((19.5, 11.0)); p.curve_to((21.5, 14.0), (21.5, 16.5), (19.5, 16.5)); p.curve_to((17.5, 16.5), (17.5, 14.0), (19.5, 11.0)); p.close_path(); p } fn path_brush() -> BezPath { // Pincel: mango diagonal, la virola que ata las cerdas, y la cabeza que // moja. Sin la virola el dibujo se lee como una espátula. let mut p = BezPath::new(); p.move_to((20.5, 3.5)); p.line_to((13.0, 11.0)); p.move_to((10.5, 8.5)); p.line_to((15.5, 13.5)); p.move_to((9.0, 10.0)); p.line_to((14.0, 15.0)); p.line_to((10.5, 18.5)); p.curve_to((8.5, 20.5), (6.0, 20.0), (3.5, 20.5)); p.curve_to((4.0, 18.0), (3.5, 15.5), (5.5, 13.5)); p.close_path(); p } fn path_eraser() -> BezPath { // Goma inclinada: el bloque, la banda que marca lo gastado, y el piso. let mut p = BezPath::new(); p.move_to((9.5, 3.5)); p.line_to((20.5, 14.5)); p.line_to((15.0, 20.0)); p.line_to((4.0, 9.0)); p.close_path(); p.move_to((15.0, 9.0)); p.line_to((9.5, 14.5)); p.move_to((10.0, 20.0)); p.line_to((21.0, 20.0)); p } fn path_gradient() -> BezPath { // Degradé: el cuadro + una trama diagonal que se ralea hacia el claro. Con // franjas horizontales el dibujo se leía como una hoja de texto. let mut p = BezPath::new(); p.move_to((4.0, 4.0)); p.line_to((20.0, 4.0)); p.line_to((20.0, 20.0)); p.line_to((4.0, 20.0)); p.close_path(); p.move_to((5.0, 19.0)); p.line_to((19.0, 5.0)); p.move_to((9.0, 19.0)); p.line_to((19.0, 9.0)); p.move_to((13.5, 19.0)); p.line_to((19.0, 13.5)); p } fn path_wand() -> BezPath { // Varita: la vara en diagonal + dos destellos de distinto tamaño. let mut p = BezPath::new(); p.move_to((3.5, 20.5)); p.line_to((13.0, 11.0)); p.move_to((17.5, 3.0)); p.line_to((17.5, 9.0)); p.move_to((14.5, 6.0)); p.line_to((20.5, 6.0)); p.move_to((8.0, 3.5)); p.line_to((8.0, 7.5)); p.move_to((6.0, 5.5)); p.line_to((10.0, 5.5)); p } fn path_lasso() -> BezPath { // Lazo: el bucle cerrado + la cuerda que cuelga y se enrosca. let mut p = BezPath::new(); p.move_to((12.0, 4.0)); p.curve_to((17.5, 4.0), (21.0, 7.0), (21.0, 10.5)); p.curve_to((21.0, 14.0), (17.5, 17.0), (12.0, 17.0)); p.curve_to((6.5, 17.0), (3.0, 14.0), (3.0, 10.5)); p.curve_to((3.0, 7.0), (6.5, 4.0), (12.0, 4.0)); p.close_path(); p.move_to((7.0, 15.8)); p.curve_to((6.0, 18.5), (7.5, 20.0), (9.5, 20.5)); p } fn path_polygon() -> BezPath { // Polígono cerrado de vértices marcados: lo que el lazo poligonal deja. let mut p = BezPath::new(); p.move_to((4.0, 9.0)); p.line_to((11.0, 3.5)); p.line_to((20.0, 8.0)); p.line_to((17.0, 19.5)); p.line_to((7.0, 20.5)); p.close_path(); p } fn path_type() -> BezPath { // La "T": travesaño con orejas + asta + pie. let mut p = BezPath::new(); p.move_to((5.0, 6.5)); p.line_to((5.0, 4.0)); p.line_to((19.0, 4.0)); p.line_to((19.0, 6.5)); p.move_to((12.0, 4.0)); p.line_to((12.0, 20.0)); p.move_to((8.5, 20.0)); p.line_to((15.5, 20.0)); p } fn path_stamp() -> BezPath { // Sello: piso, almohadilla, cuerpo cónico y mango. let mut p = BezPath::new(); p.move_to((3.5, 20.5)); p.line_to((20.5, 20.5)); p.move_to((5.5, 18.0)); p.line_to((18.5, 18.0)); p.line_to((18.5, 15.0)); p.line_to((5.5, 15.0)); p.close_path(); p.move_to((8.0, 15.0)); p.line_to((9.5, 9.5)); p.line_to((14.5, 9.5)); p.line_to((16.0, 15.0)); p.move_to((9.5, 9.5)); p.line_to((9.5, 6.0)); p.curve_to((9.5, 3.0), (14.5, 3.0), (14.5, 6.0)); p.line_to((14.5, 9.5)); p } fn path_bandage() -> BezPath { // Curita en diagonal, con la gasa del medio: el glifo de "sanar". let mut p = BezPath::new(); p.move_to((3.9, 15.9)); p.line_to((15.9, 3.9)); p.curve_to((18.0, 1.8), (22.2, 6.0), (20.1, 8.1)); p.line_to((8.1, 20.1)); p.curve_to((6.0, 22.2), (1.8, 18.0), (3.9, 15.9)); p.close_path(); p.move_to((11.3, 8.5)); p.line_to((15.5, 12.7)); p.move_to((8.5, 11.3)); p.line_to((12.7, 15.5)); p } fn path_pen() -> BezPath { // Punta de pluma: el cuerpo en cuña y la hendidura que baja a la punta. let mut p = BezPath::new(); p.move_to((20.5, 3.5)); p.line_to((10.0, 8.0)); p.line_to((4.5, 19.5)); p.line_to((16.0, 14.0)); p.close_path(); p.move_to((4.5, 19.5)); p.line_to((13.0, 11.0)); p } fn path_layers() -> BezPath { // Tres hojas apiladas vistas en perspectiva: la de arriba entera, las de // abajo insinuadas por su canto. Es el glifo del panel de capas. let mut p = BezPath::new(); p.move_to((12.0, 3.0)); p.line_to((21.0, 8.0)); p.line_to((12.0, 13.0)); p.line_to((3.0, 8.0)); p.close_path(); p.move_to((3.0, 12.0)); p.line_to((12.0, 17.0)); p.line_to((21.0, 12.0)); p.move_to((3.0, 16.0)); p.line_to((12.0, 21.0)); p.line_to((21.0, 16.0)); p } // --------------------------------------------------------------------- // Controles de un editor de imágenes — para que un panel de acciones se lea // de un vistazo en vez de leerse renglón por renglón. // // Son stroke puro (el pintor sólo traza, nunca rellena), así que un dibujo que // dependa de un relleno para leerse no se lee: donde hace falta "lleno" va // trama. // --------------------------------------------------------------------- fn path_select_all() -> BezPath { // El marco cortado de la selección, con un tilde adentro: todo. let mut p = path_marquee(); p.move_to((8.0, 12.0)); p.line_to((11.0, 15.0)); p.line_to((16.5, 8.5)); p } fn path_expand() -> BezPath { // El núcleo y cuatro flechas que se van a las esquinas. let mut p = BezPath::new(); p.move_to((9.0, 9.0)); p.line_to((15.0, 9.0)); p.line_to((15.0, 15.0)); p.line_to((9.0, 15.0)); p.close_path(); p.move_to((7.0, 7.0)); p.line_to((3.5, 3.5)); p.move_to((3.5, 7.0)); p.line_to((3.5, 3.5)); p.line_to((7.0, 3.5)); p.move_to((17.0, 7.0)); p.line_to((20.5, 3.5)); p.move_to((17.0, 3.5)); p.line_to((20.5, 3.5)); p.line_to((20.5, 7.0)); p.move_to((7.0, 17.0)); p.line_to((3.5, 20.5)); p.move_to((3.5, 17.0)); p.line_to((3.5, 20.5)); p.line_to((7.0, 20.5)); p.move_to((17.0, 17.0)); p.line_to((20.5, 20.5)); p.move_to((17.0, 20.5)); p.line_to((20.5, 20.5)); p.line_to((20.5, 17.0)); p } fn path_contract() -> BezPath { // El marco y cuatro flechas que se vienen al centro. let mut p = BezPath::new(); p.move_to((3.5, 3.5)); p.line_to((20.5, 3.5)); p.line_to((20.5, 20.5)); p.line_to((3.5, 20.5)); p.close_path(); p.move_to((6.5, 6.5)); p.line_to((10.5, 10.5)); p.move_to((10.5, 7.0)); p.line_to((10.5, 10.5)); p.line_to((7.0, 10.5)); p.move_to((17.5, 6.5)); p.line_to((13.5, 10.5)); p.move_to((13.5, 7.0)); p.line_to((13.5, 10.5)); p.line_to((17.0, 10.5)); p.move_to((6.5, 17.5)); p.line_to((10.5, 13.5)); p.move_to((7.0, 13.5)); p.line_to((10.5, 13.5)); p.line_to((10.5, 17.0)); p.move_to((17.5, 17.5)); p.line_to((13.5, 13.5)); p.move_to((17.0, 13.5)); p.line_to((13.5, 13.5)); p.line_to((13.5, 17.0)); p } fn path_scissors() -> BezPath { // Dos ojos y dos hojas que se cruzan. let mut p = path_circle(7.5, 18.0, 2.6, 20); for el in path_circle(16.5, 18.0, 2.6, 20).elements() { p.push(*el); } p.move_to((5.5, 3.5)); p.line_to((15.5, 15.8)); p.move_to((18.5, 3.5)); p.line_to((8.5, 15.8)); p } fn path_paste() -> BezPath { // Tabla con la pinza arriba: el portapapeles. let mut p = BezPath::new(); p.move_to((9.0, 5.0)); p.line_to((5.5, 5.0)); p.line_to((5.5, 20.5)); p.line_to((18.5, 20.5)); p.line_to((18.5, 5.0)); p.line_to((15.0, 5.0)); p.move_to((9.0, 7.0)); p.line_to((9.0, 3.5)); p.line_to((15.0, 3.5)); p.line_to((15.0, 7.0)); p.close_path(); p } fn path_duplicate() -> BezPath { // Dos hojas y el más que dice que aparece una más. let mut p = BezPath::new(); p.move_to((15.0, 7.5)); p.line_to((15.0, 4.0)); p.line_to((4.0, 4.0)); p.line_to((4.0, 15.0)); p.line_to((7.5, 15.0)); p.move_to((7.5, 7.5)); p.line_to((16.5, 7.5)); p.line_to((16.5, 16.5)); p.line_to((7.5, 16.5)); p.close_path(); p.move_to((19.5, 15.5)); p.line_to((19.5, 21.0)); p.move_to((16.8, 18.25)); p.line_to((22.2, 18.25)); p } fn path_crop() -> BezPath { // Las dos escuadras cruzadas del recorte. let mut p = BezPath::new(); p.move_to((7.0, 2.5)); p.line_to((7.0, 17.0)); p.line_to((21.5, 17.0)); p.move_to((2.5, 7.0)); p.line_to((17.0, 7.0)); p.line_to((17.0, 21.5)); p } fn path_trim() -> BezPath { // El contenido y cuatro escuadras que se le vienen encima. let mut p = BezPath::new(); p.move_to((9.0, 9.0)); p.line_to((15.0, 9.0)); p.line_to((15.0, 15.0)); p.line_to((9.0, 15.0)); p.close_path(); p.move_to((4.0, 7.5)); p.line_to((4.0, 4.0)); p.line_to((7.5, 4.0)); p.move_to((16.5, 4.0)); p.line_to((20.0, 4.0)); p.line_to((20.0, 7.5)); p.move_to((20.0, 16.5)); p.line_to((20.0, 20.0)); p.line_to((16.5, 20.0)); p.move_to((7.5, 20.0)); p.line_to((4.0, 20.0)); p.line_to((4.0, 16.5)); p } fn path_mask() -> BezPath { // El cuadro y el disco que le tapa una esquina. let mut p = BezPath::new(); p.move_to((3.5, 4.0)); p.line_to((14.5, 4.0)); p.line_to((14.5, 15.0)); p.line_to((3.5, 15.0)); p.close_path(); for el in path_circle(15.5, 15.5, 5.0, 28).elements() { p.push(*el); } p } fn path_invert() -> BezPath { // Disco partido al medio, con una mitad tramada. let mut p = path_circle(12.0, 12.0, 8.5, 32); p.move_to((12.0, 3.5)); p.line_to((12.0, 20.5)); p.move_to((14.0, 8.0)); p.line_to((19.0, 8.0)); p.move_to((14.0, 12.0)); p.line_to((20.3, 12.0)); p.move_to((14.0, 16.0)); p.line_to((19.0, 16.0)); p } /// El espejo: el eje cortado y los dos triángulos enfrentados. `vertical` gira /// el dibujo un cuarto de vuelta — son la misma idea en el otro eje, y tenerlos /// en una sola función es lo que impide que se desalineen entre sí. fn path_mirror(vertical: bool) -> BezPath { let mut p = BezPath::new(); if vertical { p.move_to((3.0, 12.0)); p.line_to((7.0, 12.0)); p.move_to((10.0, 12.0)); p.line_to((14.0, 12.0)); p.move_to((17.0, 12.0)); p.line_to((21.0, 12.0)); p.move_to((7.0, 9.5)); p.line_to((12.0, 4.0)); p.line_to((17.0, 9.5)); p.close_path(); p.move_to((7.0, 14.5)); p.line_to((12.0, 20.0)); p.line_to((17.0, 14.5)); p.close_path(); return p; } p.move_to((12.0, 3.0)); p.line_to((12.0, 7.0)); p.move_to((12.0, 10.0)); p.line_to((12.0, 14.0)); p.move_to((12.0, 17.0)); p.line_to((12.0, 21.0)); p.move_to((9.5, 7.0)); p.line_to((4.0, 12.0)); p.line_to((9.5, 17.0)); p.close_path(); p.move_to((14.5, 7.0)); p.line_to((20.0, 12.0)); p.line_to((14.5, 17.0)); p.close_path(); p } fn path_square() -> BezPath { let mut p = BezPath::new(); p.move_to((3.5, 6.0)); p.line_to((20.5, 6.0)); p.line_to((20.5, 18.0)); p.line_to((3.5, 18.0)); p.close_path(); p } fn path_circulo() -> BezPath { // Elipse por cuatro arcos (kappa · radio). let mut p = BezPath::new(); p.move_to((3.5, 12.0)); p.curve_to((3.5, 8.41), (7.31, 5.5), (12.0, 5.5)); p.curve_to((16.69, 5.5), (20.5, 8.41), (20.5, 12.0)); p.curve_to((20.5, 15.59), (16.69, 18.5), (12.0, 18.5)); p.curve_to((7.31, 18.5), (3.5, 15.59), (3.5, 12.0)); p.close_path(); p } fn path_rounded_square() -> BezPath { let mut p = BezPath::new(); p.move_to((8.0, 4.0)); p.line_to((16.0, 4.0)); p.curve_to((18.2, 4.0), (20.0, 5.8), (20.0, 8.0)); p.line_to((20.0, 16.0)); p.curve_to((20.0, 18.2), (18.2, 20.0), (16.0, 20.0)); p.line_to((8.0, 20.0)); p.curve_to((5.8, 20.0), (4.0, 18.2), (4.0, 16.0)); p.line_to((4.0, 8.0)); p.curve_to((4.0, 5.8), (5.8, 4.0), (8.0, 4.0)); p.close_path(); p } fn path_star() -> BezPath { // Cinco puntas, radio 9 y 3.9, la primera mirando arriba. let mut p = BezPath::new(); p.move_to((12.0, 3.0)); p.line_to((14.29, 8.85)); p.line_to((20.56, 9.22)); p.line_to((15.71, 13.21)); p.line_to((17.29, 19.28)); p.line_to((12.0, 15.9)); p.line_to((6.71, 19.28)); p.line_to((8.29, 13.21)); p.line_to((3.44, 9.22)); p.line_to((9.71, 8.85)); p.close_path(); p } fn path_hexagon() -> BezPath { let mut p = BezPath::new(); p.move_to((12.0, 3.5)); p.line_to((19.36, 7.75)); p.line_to((19.36, 16.25)); p.line_to((12.0, 20.5)); p.line_to((4.64, 16.25)); p.line_to((4.64, 7.75)); p.close_path(); p } fn path_line() -> BezPath { // El segmento y sus dos nodos: es una forma con puntas, no una raya suelta. let mut p = BezPath::new(); p.move_to((6.5, 17.5)); p.line_to((17.5, 6.5)); p.move_to((3.5, 17.0)); p.line_to((6.5, 17.0)); p.line_to((6.5, 20.0)); p.line_to((3.5, 20.0)); p.close_path(); p.move_to((17.5, 4.0)); p.line_to((20.5, 4.0)); p.line_to((20.5, 7.0)); p.line_to((17.5, 7.0)); p.close_path(); p } fn path_flatten() -> BezPath { // Dos flechas que se juntan contra una barra. let mut p = BezPath::new(); p.move_to((3.5, 12.0)); p.line_to((20.5, 12.0)); p.move_to((12.0, 3.5)); p.line_to((12.0, 9.0)); p.move_to((9.0, 6.0)); p.line_to((12.0, 9.0)); p.line_to((15.0, 6.0)); p.move_to((12.0, 20.5)); p.line_to((12.0, 15.0)); p.move_to((9.0, 18.0)); p.line_to((12.0, 15.0)); p.line_to((15.0, 18.0)); p } /// La flecha circular. `horario` la manda para un lado o para el otro; la punta /// va arriba en los dos casos, que es donde el ojo la busca. fn path_rotate(horario: bool) -> BezPath { let mut p = BezPath::new(); if horario { p.move_to((19.0, 12.0)); p.curve_to((19.0, 15.87), (15.87, 19.0), (12.0, 19.0)); p.curve_to((8.13, 19.0), (5.0, 15.87), (5.0, 12.0)); p.curve_to((5.0, 8.13), (8.13, 5.0), (12.0, 5.0)); p.move_to((9.2, 2.3)); p.line_to((12.4, 5.0)); p.line_to((9.2, 7.7)); return p; } p.move_to((5.0, 12.0)); p.curve_to((5.0, 15.87), (8.13, 19.0), (12.0, 19.0)); p.curve_to((15.87, 19.0), (19.0, 15.87), (19.0, 12.0)); p.curve_to((19.0, 8.13), (15.87, 5.0), (12.0, 5.0)); p.move_to((14.8, 2.3)); p.line_to((11.6, 5.0)); p.line_to((14.8, 7.7)); p } fn path_shadow() -> BezPath { // El objeto y su sombra, corridos. let mut p = BezPath::new(); p.move_to((8.5, 8.5)); p.line_to((20.5, 8.5)); p.line_to((20.5, 20.5)); p.line_to((8.5, 20.5)); p.close_path(); p.move_to((3.5, 3.5)); p.line_to((15.5, 3.5)); p.line_to((15.5, 15.5)); p.line_to((3.5, 15.5)); p.close_path(); p } fn path_brightness() -> BezPath { // Sol: disco y ocho rayos. let mut p = path_circle(12.0, 12.0, 4.5, 24); p.move_to((12.0, 2.5)); p.line_to((12.0, 5.5)); p.move_to((12.0, 18.5)); p.line_to((12.0, 21.5)); p.move_to((2.5, 12.0)); p.line_to((5.5, 12.0)); p.move_to((18.5, 12.0)); p.line_to((21.5, 12.0)); p.move_to((5.3, 5.3)); p.line_to((7.4, 7.4)); p.move_to((16.6, 16.6)); p.line_to((18.7, 18.7)); p.move_to((18.7, 5.3)); p.line_to((16.6, 7.4)); p.move_to((7.4, 16.6)); p.line_to((5.3, 18.7)); p } fn path_contrast() -> BezPath { // Cuadro partido en diagonal, con un triángulo tramado. let mut p = BezPath::new(); p.move_to((4.0, 4.0)); p.line_to((20.0, 4.0)); p.line_to((20.0, 20.0)); p.line_to((4.0, 20.0)); p.close_path(); p.move_to((20.0, 4.0)); p.line_to((4.0, 20.0)); p.move_to((11.0, 4.0)); p.line_to((4.0, 11.0)); p.move_to((16.0, 4.0)); p.line_to((4.0, 16.0)); p } fn path_color_wheel() -> BezPath { // Disco con tres radios: el círculo de tono. let mut p = path_circle(12.0, 12.0, 8.5, 32); p.move_to((12.0, 12.0)); p.line_to((12.0, 3.5)); p.move_to((12.0, 12.0)); p.line_to((19.36, 16.25)); p.move_to((12.0, 12.0)); p.line_to((4.64, 16.25)); p } fn path_blur() -> BezPath { // Una mitad con canto y otra que se ralea. let mut p = BezPath::new(); p.move_to((12.0, 4.0)); p.line_to((4.0, 4.0)); p.line_to((4.0, 20.0)); p.line_to((12.0, 20.0)); p.move_to((14.5, 5.5)); p.line_to((14.5, 18.5)); p.move_to((17.5, 7.5)); p.line_to((17.5, 16.5)); p.move_to((20.0, 9.5)); p.line_to((20.0, 14.5)); p } fn path_levels() -> BezPath { // El histograma: barras y su piso. let mut p = BezPath::new(); p.move_to((3.5, 20.0)); p.line_to((20.5, 20.0)); p.move_to((6.0, 20.0)); p.line_to((6.0, 15.0)); p.move_to((9.0, 20.0)); p.line_to((9.0, 9.0)); p.move_to((12.0, 20.0)); p.line_to((12.0, 4.5)); p.move_to((15.0, 20.0)); p.line_to((15.0, 11.0)); p.move_to((18.0, 20.0)); p.line_to((18.0, 16.0)); p } fn path_curve() -> BezPath { // La caja del editor de curvas y su S. let mut p = BezPath::new(); p.move_to((4.0, 4.0)); p.line_to((20.0, 4.0)); p.line_to((20.0, 20.0)); p.line_to((4.0, 20.0)); p.close_path(); // Los tiradores van bien afuera de la recta a propósito: con los de la // primera versión (11,19)-(13,5) la curva salía casi diagonal y el dibujo // se leía como un cuadro tachado, no como una curva. p.move_to((4.0, 20.0)); p.curve_to((15.0, 20.5), (9.0, 3.5), (20.0, 4.0)); p } fn path_segment() -> BezPath { // La silueta y el corte punteado que la separa del fondo. let mut p = BezPath::new(); p.move_to((8.0, 18.5)); p.curve_to((5.0, 14.0), (7.0, 6.5), (12.0, 6.5)); p.curve_to((17.0, 6.5), (19.0, 14.0), (16.0, 18.5)); p.close_path(); p.move_to((3.5, 8.0)); p.line_to((3.5, 12.0)); p.move_to((3.5, 15.0)); p.line_to((3.5, 19.0)); p.move_to((20.5, 8.0)); p.line_to((20.5, 12.0)); p.move_to((20.5, 15.0)); p.line_to((20.5, 19.0)); p } fn path_upscale() -> BezPath { // El cuadro chico y la flecha que lo lleva a grande. let mut p = BezPath::new(); p.move_to((4.0, 13.0)); p.line_to((11.0, 13.0)); p.line_to((11.0, 20.0)); p.line_to((4.0, 20.0)); p.close_path(); p.move_to((13.0, 11.0)); p.line_to((20.5, 3.5)); p.move_to((14.5, 3.5)); p.line_to((20.5, 3.5)); p.line_to((20.5, 9.5)); p } #[cfg(test)] mod tests { use super::*; // Sólo los usan los tests: arriba quedaban como imports muertos. Deriva de // la migración a `no_std`, donde todo lo de `alloc` se importa a mano. use alloc::vec::Vec; /// **Las agujas apuntan a la hora que se les pide.** /// /// Se mide la punta de cada aguja en el grid de 24×24, que es lo único que /// el dibujo promete: a las 3 en punto el minutero mira a las 12 (arriba) y /// la de hora a las 3 (derecha); a las 6 en punto las dos caen sobre el eje /// vertical, una arriba y otra abajo. Es un `assert` sobre coordenadas y no /// un pantallazo: un reloj mal orientado se ve bien en una captura de las /// 12:00 y mal el resto del día. #[test] fn las_agujas_del_reloj_marcan_la_hora() { // Punta de la aguja `i` (0 = hora, 1 = minutero) en `(x, y)`. let punta = |h: u8, m: u8, i: usize| -> (f64, f64) { let pts: Vec<(f64, f64)> = path_clock_at(h, m) .elements() .iter() .filter_map(|e| match e { llimphi_compositor::peniko::kurbo::PathEl::LineTo(p) => Some((p.x, p.y)), _ => None, }) .collect(); // Los últimos dos `line_to` son las dos agujas (antes va el círculo). let n = pts.len(); pts[n - 2 + i] }; let cerca = |a: f64, b: f64| (a - b).abs() < 0.05; // 3:00 — minutero a las 12, aguja de hora a las 3. let (hx, hy) = punta(3, 0, 0); assert!(cerca(hy, 12.0) && hx > 12.0, "3:00: la aguja de hora mira a la derecha"); let (mx, my) = punta(3, 0, 1); assert!(cerca(mx, 12.0) && my < 12.0, "3:00: el minutero mira arriba"); // 6:00 — las dos sobre el eje vertical, la de hora abajo. let (hx, hy) = punta(6, 0, 0); assert!(cerca(hx, 12.0) && hy > 12.0, "6:00: la aguja de hora mira abajo"); // 6:30 — la de hora **no** se queda en el 6: avanzó media hora, o sea // que ya pasó del eje. Esto es lo que separa un reloj de un dibujo. let (hx, _) = punta(6, 30, 0); assert!(hx < 11.9, "6:30: la aguja de hora ya dejó atrás el 6"); // Las 24 h entran por el 12: 15:00 se ve igual que 3:00. assert_eq!(punta(15, 0, 0), punta(3, 0, 0), "la tarde se dibuja como la mañana"); // Y el minutero es más largo que la de hora, en toda hora. for h in 0..24u8 { let (hx, hy) = punta(h, 20, 0); let (mx, my) = punta(h, 20, 1); let largo = |x: f64, y: f64| ((x - 12.0).powi(2) + (y - 12.0).powi(2)).sqrt(); assert!(largo(mx, my) > largo(hx, hy), "{h}:20 — el minutero es el largo"); } } #[test] fn all_icons_have_nonempty_path() { let all = [ Icon::File, Icon::Folder, Icon::FolderOpen, Icon::Save, Icon::Open, Icon::Plus, Icon::Minus, Icon::X, Icon::Check, Icon::Edit, Icon::Trash, Icon::ChevronUp, Icon::ChevronDown, Icon::ChevronLeft, Icon::ChevronRight, Icon::Home, Icon::Search, Icon::Info, Icon::Warning, Icon::Error, Icon::Bell, Icon::Settings, Icon::More, Icon::Play, Icon::Pause, Icon::Stop, Icon::SkipBack, Icon::SkipForward, Icon::Rewind, Icon::FastForward, Icon::Volume, Icon::VolumeMute, Icon::Repeat, Icon::Shuffle, Icon::Record, Icon::Equalizer, Icon::Camera, Icon::Gauge, Icon::Image, Icon::Music, Icon::Film, Icon::Archive, Icon::Code, Icon::FileText, Icon::Link, Icon::Font, ]; for icon in all { let p = icon.path(); assert!( p.elements().len() > 0, "icono {} produjo path vacío", icon.name() ); } } #[test] fn icon_names_are_unique() { let all = [ Icon::File, Icon::Folder, Icon::FolderOpen, Icon::Save, Icon::Open, Icon::Plus, Icon::Minus, Icon::X, Icon::Check, Icon::Edit, Icon::Trash, Icon::ChevronUp, Icon::ChevronDown, Icon::ChevronLeft, Icon::ChevronRight, Icon::Home, Icon::Search, Icon::Info, Icon::Warning, Icon::Error, Icon::Bell, Icon::Settings, Icon::More, Icon::Play, Icon::Pause, Icon::Stop, Icon::SkipBack, Icon::SkipForward, Icon::Rewind, Icon::FastForward, Icon::Volume, Icon::VolumeMute, Icon::Repeat, Icon::Shuffle, Icon::Record, Icon::Equalizer, Icon::Camera, Icon::Gauge, Icon::Image, Icon::Music, Icon::Film, Icon::Archive, Icon::Code, Icon::FileText, Icon::Link, Icon::Font, ]; 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 Icon::name()"); } #[test] fn iconos_nuevos_tienen_path() { let nuevos = [ Icon::Clock, Icon::Power, Icon::Mail, Icon::Keyboard, Icon::Palette, Icon::Lock, Icon::Key, Icon::Monitor, Icon::Sparkle, Icon::Mic, Icon::Mouse, Icon::Cloud, Icon::Moon, Icon::Refresh, Icon::Puzzle, Icon::Copy, ]; for icon in nuevos { assert!(!icon.path().elements().is_empty(), "{} sin path", icon.name()); } } #[test] fn iconos_de_pintura_tienen_path() { let herramientas = [ Icon::Move, Icon::Eyedropper, Icon::Marquee, Icon::Bucket, Icon::Brush, Icon::Eraser, Icon::Gradient, Icon::Wand, Icon::Lasso, Icon::Polygon, Icon::Type, Icon::Stamp, Icon::Bandage, Icon::Pen, Icon::Layers, ]; for icon in herramientas { assert!(!icon.path().elements().is_empty(), "{} sin path", icon.name()); } // Y sus nombres no chocan con los del set viejo. let mut nombres: Vec<&str> = herramientas.iter().map(|i| i.name()).collect(); let n = nombres.len(); nombres.sort(); nombres.dedup(); assert_eq!(nombres.len(), n, "nombres duplicados entre las herramientas"); } #[test] fn iconos_de_control_tienen_path() { let controles = [ Icon::SelectAll, Icon::Expand, Icon::Contract, Icon::Scissors, Icon::Paste, Icon::Duplicate, Icon::Crop, Icon::Trim, Icon::Mask, Icon::Invert, Icon::Mirror, Icon::MirrorV, Icon::Square, Icon::Circle, Icon::RoundedSquare, Icon::Star, Icon::Hexagon, Icon::Line, Icon::Flatten, Icon::RotateCw, Icon::RotateCcw, Icon::Shadow, Icon::Brightness, Icon::Contrast, Icon::ColorWheel, Icon::Blur, Icon::Levels, Icon::Curve, Icon::Segment, Icon::Upscale, Icon::Heart, ]; for icon in controles { assert!(!icon.path().elements().is_empty(), "{} sin path", icon.name()); } let mut nombres: Vec<&str> = controles.iter().map(|i| i.name()).collect(); let n = nombres.len(); nombres.sort(); nombres.dedup(); assert_eq!(nombres.len(), n, "nombres duplicados entre los controles"); } #[test] fn ningun_glifo_de_menu_cae_al_texto() { // El barrido del 2026-09-10 sobre el contextual de nahual: de once // glifos, seis caían al fallback de texto y dos de ésos ni existen en // las fuentes de la máquina (salían de **notdef**, la pila de líneas). // Un menú con vectores, glifos de fuente y notdef mezclados no se ve // mal por un detalle: se ve como tres menús apilados. // // Este test es la cobertura: un glifo nuevo en un menú se agrega acá y // el que lo agregue se entera en el acto si no tiene ícono. for g in [ "✂", "✎", "🔗", "⛁", "✧", "▸", "↺", "◇", "◈", "▫", "⎙", "🗑", "⎘", "✓", "✗", "+", "ⓘ", "★", "✦", "⚙", "▦", "☰", "⟳", "📁", "📄", ] { assert!( Icon::from_glyph(g).is_some(), "el glifo {g:?} no tiene ícono vectorial: en un menú sale como texto", ); } } /// Los dos de lista tienen trazo y nombre propio: si uno se agrega al enum /// y se olvida en `path()`, el botón sale vacío y nadie lo nota hasta verlo. #[test] fn los_iconos_de_lista_tienen_path_y_nombre() { assert!(!Icon::Eye.path().elements().is_empty(), "eye sin path"); for icon in [Icon::ListPrev, Icon::ListNext] { assert!(!icon.path().elements().is_empty(), "{} sin path", icon.name()); } assert_ne!(Icon::ListPrev.name(), Icon::ListNext.name()); assert_ne!(Icon::ListPrev.path().to_svg(), Icon::ListNext.path().to_svg()); // Y no son los de pista: el ícono tiene que distinguir lista de pista. assert_ne!(Icon::ListPrev.path().to_svg(), Icon::SkipBack.path().to_svg()); } #[test] fn from_glyph_mapea_los_comunes() { // Casos que cubrían el bug del wawa-panel y de los menús. assert_eq!(Icon::from_glyph("⚙"), Some(Icon::Settings)); assert_eq!(Icon::from_glyph("🎨"), Some(Icon::Palette)); assert_eq!(Icon::from_glyph("⌨"), Some(Icon::Keyboard)); assert_eq!(Icon::from_glyph("⏻"), Some(Icon::Power)); assert_eq!(Icon::from_glyph("✉"), Some(Icon::Mail)); assert_eq!(Icon::from_glyph("🔊"), Some(Icon::Volume)); assert_eq!(Icon::from_glyph("≡"), Some(Icon::Rows)); assert_eq!(Icon::from_glyph("▶"), Some(Icon::Play)); // Variation selector (emoji presentation) se tolera. assert_eq!(Icon::from_glyph("🖥\u{FE0F}"), Some(Icon::Monitor)); // Texto real (no un glifo-ícono) no matchea → caería a texto. assert_eq!(Icon::from_glyph("Archivo"), None); assert_eq!(Icon::from_glyph(""), None); } }