tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-icons/src/lib.rs Rust · 2854 lines · 88.2 KB History Who touched what raw

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

1//! `llimphi-icons` — set canónico de iconos vectoriales para apps tawasuyu.
2//!
3//! Cada icono es una función pura que devuelve un `BezPath` definido en
4//! un grid lógico de **24×24 unidades**. El renderer escala al rect que
5//! reciba, así un mismo icono sirve para 12px (en una fila de lista) y
6//! para 64px (en una hero card) sin pérdida de nitidez — es vector,
7//! no bitmap.
8//!
9//! ## Diseño
10//!
11//! - **Stroke-based, no fill**: los iconos son trazos de ancho 2 (en
12//!   unidades del grid) con joins/caps suaves. El stroke se renderiza
13//!   con el color que la app elija (típicamente `theme.fg_text` o
14//!   `theme.accent`).
15//! - **Geometría minimal, no marca**: glifos genéricos universales,
16//!   no "marca registrada". Cada uno debe ser reconocible al primer
17//!   vistazo aún en 12×12.
18//! - **Set acotado**: suficientes para cubrir el grueso de acciones y
19//!   tipos que aparecen en cualquier UI tawasuyu. Si una app necesita uno
20//!   más, lo agrega aquí (no en su propio crate) — la consistencia
21//!   visual importa más que el aislamiento.
22//!
23//! ## Catálogo
24//!
25//! | Categoría    | Iconos                                              |
26//! |--------------|-----------------------------------------------------|
27//! | Documento    | `file`, `folder`, `folder_open`, `save`, `open`     |
28//! | Edición      | `plus`, `minus`, `x`, `check`, `edit`, `trash`      |
29//! | Navegación   | `chevron_up`, `chevron_down`, `chevron_left`, `chevron_right`, `home`, `search` |
30//! | Estado       | `info`, `warning`, `error`, `bell`                  |
31//! | Sistema      | `settings`, `more`                                  |
32//! | Multimedia   | `play`, `pause`, `stop`, `skip_*`, `volume*`, `repeat`, `shuffle`, `record`, `equalizer`, `camera`, `gauge` |
33//! | Archivos     | `image`, `music`, `film`, `archive`, `code`, `file_text`, `link`, `font` |
34//!
35//! ## Uso
36//!
37//! ```ignore
38//! use llimphi_icons::{Icon, icon_view};
39//!
40//! // Botón con icono "save":
41//! let btn = View::new(style)
42//!     .fill(palette.bg_button)
43//!     .children(vec![icon_view(Icon::Save, palette.fg_text, 1.6)]);
44//! ```
45//!
46//! El parámetro `stroke_width` (3er arg de `icon_view`) está en unidades
47//! del grid (24×24). `1.6` es el default armonioso; `2.0` para énfasis;
48//! `1.2` para iconos en tipografías pequeñas.
49
50#![forbid(unsafe_code)]
51
52
53// Sin `std`: sólo geometría y `alloc` (W4.6).
54#![no_std]
55
56extern crate alloc;
57
58use alloc::string::ToString;
59
60pub mod app_icons;
61
62use llimphi_compositor::llimphi_layout::taffy::{
63    prelude::{percent, Size, Style},
64    Position,
65};
66use llimphi_compositor::peniko::kurbo::{Affine, BezPath, Cap, Join, Stroke};
67use llimphi_compositor::peniko::Color;
68use llimphi_compositor::View;
69
70/// Catálogo de iconos del set canónico.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Icon {
73    // --- Documento ---
74    File,
75    Folder,
76    FolderOpen,
77    Save,
78    Open,
79    // --- Edición ---
80    Plus,
81    Minus,
82    X,
83    Check,
84    Edit,
85    Trash,
86    // --- Navegación ---
87    ChevronUp,
88    ChevronDown,
89    ChevronLeft,
90    ChevronRight,
91    Home,
92    Search,
93    // --- Estado ---
94    Info,
95    Warning,
96    Error,
97    Bell,
98    // --- Sistema ---
99    Settings,
100    More,
101    // --- Multimedia ---
102    Play,
103    Pause,
104    Stop,
105    SkipBack,
106    SkipForward,
107    Rewind,
108    FastForward,
109    Volume,
110    VolumeMute,
111    Repeat,
112    Shuffle,
113    Record,
114    Equalizer,
115    Camera,
116    Gauge,
117    // --- Archivos (tipos por extensión, para listados de file manager) ---
118    Image,
119    Music,
120    Film,
121    Archive,
122    Code,
123    FileText,
124    Link,
125    Font,
126    // --- Vistas / layout (toolbars de file manager) ---
127    /// Grilla 2×2 (vista iconos / galería).
128    Grid,
129    /// Tres filas horizontales (vista lista).
130    Rows,
131    /// Tabla: marco + fila de encabezado + columnas (vista detalle).
132    Table,
133    /// Dos paneles verticales lado a lado (modo dual).
134    Columns,
135    // --- Dominio: creador de mundos (llimphi-voxel-studio) ---
136    /// Globo terráqueo (Mundos).
137    Globe,
138    /// Cadena montañosa (Biomas).
139    Mountain,
140    /// Hoja con nervadura (Materiales).
141    Leaf,
142    /// Silueta de persona (Seres).
143    User,
144    /// Gota de agua (Leyes / físicas).
145    Droplet,
146    // --- Sistema / settings panels (sweep glifo→vector 2026-06-30) ---
147    /// Reloj con manecillas (hora, contextos, tiempo).
148    Clock,
149    /// Símbolo de encendido/apagado.
150    Power,
151    /// Sobre de correo.
152    Mail,
153    /// Teclado (atajos).
154    Keyboard,
155    /// Paleta de pintor (themes/apariencia).
156    Palette,
157    /// Candado (seguridad/privacidad).
158    Lock,
159    /// Llave.
160    Key,
161    /// Monitor/pantalla (sistema, acerca del equipo).
162    Monitor,
163    /// Destello de cuatro puntas (animaciones, efectos).
164    Sparkle,
165    /// Micrófono (voz, audio in).
166    Mic,
167    /// Ratón.
168    Mouse,
169    /// Nube (red/online).
170    Cloud,
171    /// Luna (modo oscuro, noche).
172    Moon,
173    /// Flecha circular (recargar/refrescar).
174    Refresh,
175    /// Pieza de rompecabezas (plugins/módulos).
176    Puzzle,
177    Copy,
178    // --- Dominio: editor de imágenes por capas (tullpu) ---
179    /// Cruz de cuatro flechas (mover / desplazar).
180    Move,
181    /// Cuentagotas (leer el color de un píxel).
182    Eyedropper,
183    /// Marco punteado (selección rectangular).
184    Marquee,
185    /// Balde volcando (relleno por inundación).
186    Bucket,
187    /// Pincel (trazo a mano alzada).
188    Brush,
189    /// Goma de borrar.
190    Eraser,
191    /// Rampa de tono (degradé).
192    Gradient,
193    /// Varita mágica (seleccionar por color).
194    Wand,
195    /// Lazo a mano alzada.
196    Lasso,
197    /// Polígono cerrado (lazo poligonal).
198    Polygon,
199    /// La "T" del texto.
200    Type,
201    /// Sello / tampón (clonar).
202    Stamp,
203    /// Curita (pincel corrector, sanar).
204    Bandage,
205    /// Punta de pluma (herramienta de curvas).
206    Pen,
207    /// Tres hojas apiladas (capas).
208    Layers,
209    // --- Dominio: los controles de un editor de imágenes (tullpu, 2026-08-29) ---
210    /// Marco cortado con un tilde: seleccionar todo.
211    SelectAll,
212    /// Núcleo con flechas hacia las esquinas: agrandar la selección.
213    Expand,
214    /// Marco con flechas hacia adentro: achicar la selección.
215    Contract,
216    /// Tijera (cortar).
217    Scissors,
218    /// Tabla con pinza (pegar).
219    Paste,
220    /// Dos hojas y un más (duplicar).
221    Duplicate,
222    /// Las dos escuadras del recorte.
223    Crop,
224    /// Cuatro escuadras abrazando el contenido: recortar a lo visible.
225    Trim,
226    /// Cuadro con un disco encima: la máscara.
227    Mask,
228    /// Disco partido con una mitad tramada: invertir.
229    Invert,
230    /// Eje cortado entre dos triángulos: espejar en horizontal.
231    Mirror,
232    /// El mismo espejo, en vertical.
233    MirrorV,
234    /// Rectángulo.
235    Square,
236    /// Elipse.
237    Circle,
238    /// Rectángulo de esquinas redondeadas.
239    RoundedSquare,
240    /// Estrella de cinco puntas.
241    Star,
242    /// Hexágono regular.
243    Hexagon,
244    /// Segmento con un nodo en cada punta.
245    Line,
246    /// Dos flechas contra una barra: aplanar.
247    Flatten,
248    /// Flecha circular horaria.
249    RotateCw,
250    /// Flecha circular antihoraria.
251    RotateCcw,
252    /// Dos cuadros corridos: la sombra paralela.
253    Shadow,
254    /// Sol con rayos: brillo.
255    Brightness,
256    /// Cuadro partido en diagonal y tramado: contraste.
257    Contrast,
258    /// Disco con tres radios: la rueda de tono.
259    ColorWheel,
260    /// Mitad nítida y mitad que se ralea: desenfoque.
261    Blur,
262    /// Barras de distinta altura sobre una base: niveles.
263    Levels,
264    /// Una S dentro de una caja: la curva.
265    Curve,
266    /// Silueta con el corte punteado alrededor: segmentar.
267    Segment,
268    /// Cuadro chico y flecha que lo agranda: escalar.
269    /// Un corazón. **El cuerpo propio** (`qhali`): el pulso, el diario, la
270    /// medicación. Es el dibujo que el catálogo de widgets ya le daba a ese
271    /// diente como glifo (`♥`); acá deja de ser un carácter de fuente y pasa a
272    /// ser un trazo, que es lo que pide el rail.
273    Heart,
274    Upscale,
275    // --- Transporte, segunda tanda ---
276    /// Lista **anterior**: la pila de renglones con la punta hacia la izquierda.
277    /// No es `SkipBack` (ésa es la pista anterior): lo que salta es la playlist
278    /// entera, y la diferencia tiene que verse en el ícono, no en el tooltip.
279    ListPrev,
280    /// Lista **siguiente**, espejo de [`Icon::ListPrev`].
281    ListNext,
282    // --- Visibilidad ---
283    /// Ojo abierto: mostrar lo que está escondido (los archivos ocultos de un
284    /// explorador). Prendido = se ve; el estado lo dice el `active` del botón.
285    Eye,
286}
287
288impl Icon {
289    /// Identificador estable en lowercase con underscores. Útil para
290    /// debugging, persistir choices del usuario, o mapear desde strings
291    /// en config.
292    pub const fn name(self) -> &'static str {
293        match self {
294            Icon::File => "file",
295            Icon::Folder => "folder",
296            Icon::FolderOpen => "folder_open",
297            Icon::Save => "save",
298            Icon::Open => "open",
299            Icon::Plus => "plus",
300            Icon::Minus => "minus",
301            Icon::X => "x",
302            Icon::Check => "check",
303            Icon::Edit => "edit",
304            Icon::Trash => "trash",
305            Icon::ChevronUp => "chevron_up",
306            Icon::ChevronDown => "chevron_down",
307            Icon::ChevronLeft => "chevron_left",
308            Icon::ChevronRight => "chevron_right",
309            Icon::Home => "home",
310            Icon::Search => "search",
311            Icon::Info => "info",
312            Icon::Warning => "warning",
313            Icon::Error => "error",
314            Icon::Bell => "bell",
315            Icon::Settings => "settings",
316            Icon::More => "more",
317            Icon::Play => "play",
318            Icon::Pause => "pause",
319            Icon::Stop => "stop",
320            Icon::SkipBack => "skip_back",
321            Icon::SkipForward => "skip_forward",
322            Icon::Rewind => "rewind",
323            Icon::FastForward => "fast_forward",
324            Icon::Volume => "volume",
325            Icon::VolumeMute => "volume_mute",
326            Icon::Repeat => "repeat",
327            Icon::Shuffle => "shuffle",
328            Icon::Record => "record",
329            Icon::Equalizer => "equalizer",
330            Icon::Camera => "camera",
331            Icon::Gauge => "gauge",
332            Icon::Image => "image",
333            Icon::Music => "music",
334            Icon::Film => "film",
335            Icon::Archive => "archive",
336            Icon::Code => "code",
337            Icon::FileText => "file_text",
338            Icon::Link => "link",
339            Icon::Font => "font",
340            Icon::Grid => "grid",
341            Icon::Rows => "rows",
342            Icon::Table => "table",
343            Icon::Columns => "columns",
344            Icon::Globe => "globe",
345            Icon::Mountain => "mountain",
346            Icon::Leaf => "leaf",
347            Icon::User => "user",
348            Icon::Droplet => "droplet",
349            Icon::Clock => "clock",
350            Icon::Power => "power",
351            Icon::Mail => "mail",
352            Icon::Keyboard => "keyboard",
353            Icon::Palette => "palette",
354            Icon::Lock => "lock",
355            Icon::Key => "key",
356            Icon::Monitor => "monitor",
357            Icon::Sparkle => "sparkle",
358            Icon::Mic => "mic",
359            Icon::Mouse => "mouse",
360            Icon::Cloud => "cloud",
361            Icon::Moon => "moon",
362            Icon::Refresh => "refresh",
363            Icon::Puzzle => "puzzle",
364            Icon::Copy => "copy",
365            Icon::Move => "move",
366            Icon::Eyedropper => "eyedropper",
367            Icon::Marquee => "marquee",
368            Icon::Bucket => "bucket",
369            Icon::Brush => "brush",
370            Icon::Eraser => "eraser",
371            Icon::Gradient => "gradient",
372            Icon::Wand => "wand",
373            Icon::Lasso => "lasso",
374            Icon::Polygon => "polygon",
375            Icon::Type => "type",
376            Icon::Stamp => "stamp",
377            Icon::Bandage => "bandage",
378            Icon::Pen => "pen",
379            Icon::Layers => "layers",
380            Icon::SelectAll => "select_all",
381            Icon::Expand => "expand",
382            Icon::Contract => "contract",
383            Icon::Scissors => "scissors",
384            Icon::Paste => "paste",
385            Icon::Duplicate => "duplicate",
386            Icon::Crop => "crop",
387            Icon::Trim => "trim",
388            Icon::Mask => "mask",
389            Icon::Invert => "invert",
390            Icon::Mirror => "mirror",
391            Icon::MirrorV => "mirror_v",
392            Icon::Square => "square",
393            Icon::Circle => "circle",
394            Icon::RoundedSquare => "rounded_square",
395            Icon::Star => "star",
396            Icon::Hexagon => "hexagon",
397            Icon::Line => "line",
398            Icon::Flatten => "flatten",
399            Icon::RotateCw => "rotate_cw",
400            Icon::RotateCcw => "rotate_ccw",
401            Icon::Shadow => "shadow",
402            Icon::Brightness => "brightness",
403            Icon::Contrast => "contrast",
404            Icon::ColorWheel => "color_wheel",
405            Icon::Blur => "blur",
406            Icon::Levels => "levels",
407            Icon::Curve => "curve",
408            Icon::Segment => "segment",
409            Icon::Upscale => "upscale",
410            Icon::ListPrev => "list_prev",
411            Icon::ListNext => "list_next",
412            Icon::Eye => "eye",
413            Icon::Heart => "heart",
414        }
415    }
416
417    /// Devuelve el `BezPath` del icono en coords del grid 24×24. La
418    /// app raramente lo necesita directamente — es lo que consume
419    /// internamente [`icon_view`] / [`paint_icon`].
420    pub fn path(self) -> BezPath {
421        match self {
422            Icon::File => path_file(),
423            Icon::Folder => path_folder(),
424            Icon::FolderOpen => path_folder_open(),
425            Icon::Save => path_save(),
426            Icon::Open => path_open(),
427            Icon::Plus => path_plus(),
428            Icon::Minus => path_minus(),
429            Icon::X => path_x(),
430            Icon::Check => path_check(),
431            Icon::Edit => path_edit(),
432            Icon::Trash => path_trash(),
433            Icon::ChevronUp => path_chevron(0.0),
434            Icon::ChevronDown => path_chevron(180.0),
435            Icon::ChevronLeft => path_chevron(270.0),
436            Icon::ChevronRight => path_chevron(90.0),
437            Icon::Home => path_home(),
438            Icon::Search => path_search(),
439            Icon::Info => path_info(),
440            Icon::Warning => path_warning(),
441            Icon::Error => path_error(),
442            Icon::Bell => path_bell(),
443            Icon::Settings => path_settings(),
444            Icon::More => path_more(),
445            Icon::Play => path_play(),
446            Icon::Pause => path_pause(),
447            Icon::Stop => path_stop(),
448            Icon::SkipBack => path_skip(true),
449            Icon::SkipForward => path_skip(false),
450            Icon::Rewind => path_seek(true),
451            Icon::FastForward => path_seek(false),
452            Icon::Volume => path_volume(false),
453            Icon::VolumeMute => path_volume(true),
454            Icon::Repeat => path_repeat(),
455            Icon::Shuffle => path_shuffle(),
456            Icon::Record => path_record(),
457            Icon::Equalizer => path_equalizer(),
458            Icon::Camera => path_camera(),
459            Icon::Gauge => path_gauge(),
460            Icon::Image => path_image(),
461            Icon::Music => path_music(),
462            Icon::Film => path_film(),
463            Icon::Archive => path_archive(),
464            Icon::Code => path_code(),
465            Icon::FileText => path_file_text(),
466            Icon::Link => path_link(),
467            Icon::Font => path_font(),
468            Icon::Grid => path_grid(),
469            Icon::Rows => path_rows(),
470            Icon::Table => path_table(),
471            Icon::Columns => path_columns(),
472            Icon::Globe => path_globe(),
473            Icon::Mountain => path_mountain(),
474            Icon::Leaf => path_leaf(),
475            Icon::User => path_user(),
476            Icon::Droplet => path_droplet(),
477            Icon::Clock => path_clock(),
478            Icon::Power => path_power(),
479            Icon::Mail => path_mail(),
480            Icon::Keyboard => path_keyboard(),
481            Icon::Palette => path_palette(),
482            Icon::Lock => path_lock(),
483            Icon::Key => path_key(),
484            Icon::Monitor => path_monitor(),
485            Icon::Sparkle => path_sparkle(),
486            Icon::Mic => path_mic(),
487            Icon::Mouse => path_mouse(),
488            Icon::Cloud => path_cloud(),
489            Icon::Moon => path_moon(),
490            Icon::Refresh => path_refresh(),
491            Icon::Puzzle => path_puzzle(),
492            Icon::Copy => path_copy(),
493            Icon::Move => path_move(),
494            Icon::Eyedropper => path_eyedropper(),
495            Icon::Marquee => path_marquee(),
496            Icon::Bucket => path_bucket(),
497            Icon::Brush => path_brush(),
498            Icon::Eraser => path_eraser(),
499            Icon::Gradient => path_gradient(),
500            Icon::Wand => path_wand(),
501            Icon::Lasso => path_lasso(),
502            Icon::Polygon => path_polygon(),
503            Icon::Type => path_type(),
504            Icon::Stamp => path_stamp(),
505            Icon::Bandage => path_bandage(),
506            Icon::Pen => path_pen(),
507            Icon::Layers => path_layers(),
508            Icon::SelectAll => path_select_all(),
509            Icon::Expand => path_expand(),
510            Icon::Contract => path_contract(),
511            Icon::Scissors => path_scissors(),
512            Icon::Paste => path_paste(),
513            Icon::Duplicate => path_duplicate(),
514            Icon::Crop => path_crop(),
515            Icon::Trim => path_trim(),
516            Icon::Mask => path_mask(),
517            Icon::Invert => path_invert(),
518            Icon::Mirror => path_mirror(false),
519            Icon::MirrorV => path_mirror(true),
520            Icon::Square => path_square(),
521            Icon::Circle => path_circulo(),
522            Icon::RoundedSquare => path_rounded_square(),
523            Icon::Star => path_star(),
524            Icon::Hexagon => path_hexagon(),
525            Icon::Line => path_line(),
526            Icon::Flatten => path_flatten(),
527            Icon::RotateCw => path_rotate(true),
528            Icon::RotateCcw => path_rotate(false),
529            Icon::Shadow => path_shadow(),
530            Icon::Brightness => path_brightness(),
531            Icon::Contrast => path_contrast(),
532            Icon::ColorWheel => path_color_wheel(),
533            Icon::Blur => path_blur(),
534            Icon::Levels => path_levels(),
535            Icon::Curve => path_curve(),
536            Icon::Segment => path_segment(),
537            Icon::Upscale => path_upscale(),
538            Icon::ListPrev => path_list_skip(true),
539            Icon::ListNext => path_list_skip(false),
540            Icon::Eye => path_eye(),
541            Icon::Heart => path_heart(),
542        }
543    }
544
545    /// Mapea un **glifo unicode** (el que muchas apps usaban como ícono de
546    /// texto) al `Icon` vectorial equivalente. Es el corazón del barrido
547    /// glifo→vector: un sitio de render hace `from_glyph(s)` y, si hay match,
548    /// pinta el vector (determinista en toda máquina) en vez del glifo de fuente
549    /// (que en hardware sin esa fuente sale notdef/tofu). `None` ⇒ el caller cae
550    /// a texto. Acepta el glifo con o sin variation-selector (`\u{FE0F}`).
551    pub fn from_glyph(glifo: &str) -> Option<Icon> {
552        let g = glifo.trim().trim_end_matches('\u{FE0F}');
553        let mut chars = g.chars();
554        let c = chars.next()?;
555        if chars.next().is_some() {
556            // Más de un code-point (sin el VS16): no es un glifo-ícono simple.
557            return None;
558        }
559        Some(match c {
560            // Multimedia / transporte
561            '▶' | '►' | '⏵' => Icon::Play,
562            '⏸' => Icon::Pause,
563            '⏹' | '■' => Icon::Stop,
564            '⏺' | '●' | '◉' | '⚫' => Icon::Record,
565            '⏮' | '⏪' => Icon::SkipBack,
566            '⏭' | '⏩' => Icon::SkipForward,
567            '🔀' => Icon::Shuffle,
568            '🔁' | '🔂' => Icon::Repeat,
569            '🔊' | '🔉' | '🔈' => Icon::Volume,
570            '🔇' => Icon::VolumeMute,
571            '♪' | '♫' | '🎵' | '🎶' => Icon::Music,
572            '🎙' | '🎤' => Icon::Mic,
573            '🎛' | '🎚' => Icon::Equalizer,
574            '📷' | '📸' => Icon::Camera,
575            '🎞' | '🎬' | '📽' => Icon::Film,
576            '🖼' => Icon::Image,
577            // Navegación / chevrons / flechas
578            '▲' | '△' => Icon::ChevronUp,
579            '▼' | '▽' => Icon::ChevronDown,
580            '◀' | '◁' | '‹' => Icon::ChevronLeft,
581            '▷' | '›' | '❯' | '❭' | '⟩' => Icon::ChevronRight,
582            '⟳' | '🔃' | '🔄' | '↻' | '⥁' => Icon::Refresh,
583            // Sistema / settings
584            '⚙' | '🛠' | '🔧' => Icon::Settings,
585            '☰' | '≡' | '≣' | '𝍢' => Icon::Rows,
586            '▦' | '▤' | '⊞' | '𐩕' => Icon::Grid,
587            '⏻' | '⏼' | '⭘' => Icon::Power,
588            '🎨' => Icon::Palette,
589            '⌨' => Icon::Keyboard,
590            '🖥' | '💻' | '🖳' => Icon::Monitor,
591            '🖱' => Icon::Mouse,
592            '🔐' | '🔒' | '🔏' => Icon::Lock,
593            '🔑' | '🗝' => Icon::Key,
594            '✉' | '📧' | '📨' | '📩' => Icon::Mail,
595            '🧩' => Icon::Puzzle,
596            '☁' => Icon::Cloud,
597            '🌐' | '🌍' | '🌎' | '🌏' => Icon::Globe,
598            '🌙' | '🌚' | '☾' | '◐' => Icon::Moon,
599            '✨' | '✦' | '✶' | '✷' | '❇' | '❋' | '★' | '☆' | '✩' => Icon::Sparkle,
600            '◴' | '◷' | '◵' | '◶' | '🕐' | '🕒' | '⏰' | '⌚' => Icon::Clock,
601            // Documento / contenido
602            '📄' | '📃' | '📝' => Icon::FileText,
603            '📁' | '📂' => Icon::Folder,
604            '💾' => Icon::Save,
605            '🔍' | '🔎' => Icon::Search,
606            '🔔' => Icon::Bell,
607            '🗑' => Icon::Trash,
608            '📋' | '⧉' | '⎘' => Icon::Copy,
609            '👤' | '🧑' | '🙍' => Icon::User,
610            'ℹ' | 'ⓘ' => Icon::Info,
611            '⚠' => Icon::Warning,
612            '➕' | '+' => Icon::Plus,
613            '✕' | '✖' | '×' | '✗' => Icon::X,
614            '✓' | '✔' => Icon::Check,
615            // --- Los glifos de MENÚ que faltaban (2026-09-10) ---
616            //
617            // Un menú contextual de nahual pintaba once glifos y sólo cinco
618            // resolvían a ícono: los otros seis caían al texto, y dos de ellos
619            // (⛁, ⎙) ni siquiera existen en las fuentes de la máquina, así que
620            // salían de **notdef** — la pila de líneas. Un menú con seis
621            // vectores, tres glifos de fuente y dos notdef no se ve mal por un
622            // detalle: se ve como cinco menús distintos apilados.
623            '✎' | '✏' | '🖉' => Icon::Edit,
624            '🔗' | '⛓' => Icon::Link,
625            '⛁' | '🗜' | '📦' => Icon::Archive,
626            '✂' | '✁' | '✃' | '✄' => Icon::Scissors,
627            '✧' | '❈' | '✵' => Icon::Sparkle,
628            '▸' => Icon::ChevronRight,
629            '↺' | '↶' | '⎌' => Icon::Refresh,
630            '▫' | '▭' | '⌗' => Icon::Code,
631            '◇' | '◈' | '◆' => Icon::Layers,
632            '⎙' | '🖨' => Icon::File,
633            '⋯' | '…' => Icon::More,
634            '⎉' => Icon::Duplicate,
635            '⌫' | '⌦' => Icon::Eraser,
636            '↗' | '⇱' | '⛶' => Icon::Expand,
637            _ => return None,
638        })
639    }
640}
641
642/// Vista de un glifo como **ícono vectorial** si [`Icon::from_glyph`] lo conoce,
643/// o como texto (su fallback histórico) si no. Es el reemplazo directo de
644/// `.text_aligned(glifo, …)` en los sitios de render que usaban íconos-glifo:
645/// el resultado es determinista en toda máquina cuando hay match.
646pub fn glyph_or_text_view<Msg: Clone + 'static>(
647    glifo: &str,
648    size: f32,
649    color: Color,
650    stroke_width: f32,
651) -> View<Msg> {
652    match Icon::from_glyph(glifo) {
653        Some(icon) => icon_view(icon, color, stroke_width),
654        None => View::new(Style {
655            position: Position::Absolute,
656            size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
657            ..Default::default()
658        })
659        .text_aligned(glifo.to_string(), size, color, llimphi_compositor::llimphi_text::Alignment::Center),
660    }
661}
662
663/// Construye un `View` que pinta el icono ocupando todo el rect del
664/// padre. El icono se escala uniformemente al mínimo lado y se centra.
665///
666/// - `stroke_width` en unidades del grid 24×24 (típico: `1.6`).
667pub fn icon_view<Msg: Clone + 'static>(
668    icon: Icon,
669    color: Color,
670    stroke_width: f32,
671) -> View<Msg> {
672    View::new(Style {
673        position: Position::Absolute,
674        size: Size {
675            width: percent(1.0_f32),
676            height: percent(1.0_f32),
677        },
678        ..Default::default()
679    })
680    .paint_with(move |scene, _ts, rect| {
681        paint_icon(scene, rect, icon, color, stroke_width);
682    })
683}
684
685/// Pintor crudo — útil cuando una app quiere stampear varios iconos
686/// dentro del mismo `paint_with` (paneles compuestos, toolbars
687/// generadas dinámicamente).
688pub fn paint_icon(
689    scene: &mut llimphi_compositor::Lienzo,
690    rect: llimphi_compositor::PaintRect,
691    icon: Icon,
692    color: Color,
693    stroke_width: f32,
694) {
695    let side = rect.w.min(rect.h) as f64;
696    if side <= 0.0 {
697        return;
698    }
699    let scale = side / 24.0;
700    let tx = rect.x as f64 + (rect.w as f64 - side) * 0.5;
701    let ty = rect.y as f64 + (rect.h as f64 - side) * 0.5;
702    let xform = Affine::translate((tx, ty)) * Affine::scale(scale);
703
704    let stroke = Stroke::new(stroke_width as f64)
705        .with_join(Join::Round)
706        .with_caps(Cap::Round);
707    let path = icon.path();
708    scene.stroke(&stroke, xform, color, None, &path);
709}
710
711// =====================================================================
712// Paths — todos en grid 24×24, origen top-left, eje Y hacia abajo
713// =====================================================================
714//
715// Cada path es geometría minimalista. Los joins y caps son Round (los
716// fija el renderer), así que los corners salen suaves sin tener que
717// definir curvas extra.
718
719fn path_file() -> BezPath {
720    // Documento: rectángulo con esquina superior-derecha plegada.
721    let mut p = BezPath::new();
722    p.move_to((6.0, 3.0));
723    p.line_to((14.0, 3.0));
724    p.line_to((19.0, 8.0));
725    p.line_to((19.0, 21.0));
726    p.line_to((6.0, 21.0));
727    p.close_path();
728    // Pliegue: línea desde la esquina superior-derecha del file hasta
729    // donde "se dobla", luego al borde.
730    p.move_to((14.0, 3.0));
731    p.line_to((14.0, 8.0));
732    p.line_to((19.0, 8.0));
733    p
734}
735
736fn path_folder() -> BezPath {
737    // Folder cerrado: cuerpo + lengüeta arriba-izquierda.
738    let mut p = BezPath::new();
739    p.move_to((3.0, 8.0));
740    p.line_to((3.0, 19.0));
741    p.line_to((21.0, 19.0));
742    p.line_to((21.0, 8.0));
743    p.line_to((11.0, 8.0));
744    p.line_to((9.0, 5.0));
745    p.line_to((3.0, 5.0));
746    p.close_path();
747    p
748}
749
750fn path_folder_open() -> BezPath {
751    // Folder con tapa levantada: el "techo" se inclina hacia la derecha.
752    let mut p = BezPath::new();
753    // Caja inferior.
754    p.move_to((3.0, 8.0));
755    p.line_to((3.0, 19.0));
756    p.line_to((21.0, 19.0));
757    p.line_to((23.0, 11.0));
758    p.line_to((7.0, 11.0));
759    p.line_to((5.0, 19.0));
760    // Lengüeta de la izquierda (sigue ahí).
761    p.move_to((3.0, 8.0));
762    p.line_to((9.0, 8.0));
763    p.line_to((11.0, 11.0));
764    p.line_to((21.0, 11.0));
765    p
766}
767
768fn path_save() -> BezPath {
769    // Floppy: cuadrado con muesca top-right y rectángulo de label abajo.
770    let mut p = BezPath::new();
771    p.move_to((4.0, 4.0));
772    p.line_to((17.0, 4.0));
773    p.line_to((20.0, 7.0));
774    p.line_to((20.0, 20.0));
775    p.line_to((4.0, 20.0));
776    p.close_path();
777    // Slot del shutter arriba.
778    p.move_to((8.0, 4.0));
779    p.line_to((8.0, 9.0));
780    p.line_to((15.0, 9.0));
781    p.line_to((15.0, 4.0));
782    // Rectángulo de label abajo.
783    p.move_to((7.0, 13.0));
784    p.line_to((17.0, 13.0));
785    p.line_to((17.0, 20.0));
786    p.line_to((7.0, 20.0));
787    p.close_path();
788    p
789}
790
791fn path_open() -> BezPath {
792    // Carpeta abriéndose hacia arriba con una flecha que entra.
793    let mut p = BezPath::new();
794    // Folder base.
795    p.move_to((3.0, 19.0));
796    p.line_to((3.0, 8.0));
797    p.line_to((9.0, 8.0));
798    p.line_to((11.0, 10.0));
799    p.line_to((21.0, 10.0));
800    p.line_to((21.0, 19.0));
801    p.close_path();
802    // Flecha entrando desde arriba al centro.
803    p.move_to((12.0, 2.0));
804    p.line_to((12.0, 14.0));
805    p.move_to((9.0, 11.0));
806    p.line_to((12.0, 14.0));
807    p.line_to((15.0, 11.0));
808    p
809}
810
811fn path_plus() -> BezPath {
812    let mut p = BezPath::new();
813    p.move_to((12.0, 5.0));
814    p.line_to((12.0, 19.0));
815    p.move_to((5.0, 12.0));
816    p.line_to((19.0, 12.0));
817    p
818}
819
820fn path_minus() -> BezPath {
821    let mut p = BezPath::new();
822    p.move_to((5.0, 12.0));
823    p.line_to((19.0, 12.0));
824    p
825}
826
827fn path_x() -> BezPath {
828    let mut p = BezPath::new();
829    p.move_to((6.0, 6.0));
830    p.line_to((18.0, 18.0));
831    p.move_to((18.0, 6.0));
832    p.line_to((6.0, 18.0));
833    p
834}
835
836fn path_check() -> BezPath {
837    let mut p = BezPath::new();
838    p.move_to((5.0, 13.0));
839    p.line_to((10.0, 18.0));
840    p.line_to((20.0, 6.0));
841    p
842}
843
844fn path_edit() -> BezPath {
845    // Lápiz: cuerpo diagonal + punta + tag de borrador.
846    let mut p = BezPath::new();
847    p.move_to((4.0, 20.0));
848    p.line_to((8.0, 19.0));
849    p.line_to((20.0, 7.0));
850    p.line_to((17.0, 4.0));
851    p.line_to((5.0, 16.0));
852    p.close_path();
853    p.move_to((14.0, 7.0));
854    p.line_to((17.0, 10.0));
855    p
856}
857
858fn path_trash() -> BezPath {
859    // Tacho: tapa con manijita + cuerpo con tres barras verticales.
860    let mut p = BezPath::new();
861    // Tapa.
862    p.move_to((4.0, 6.0));
863    p.line_to((20.0, 6.0));
864    // Manijita.
865    p.move_to((9.0, 6.0));
866    p.line_to((9.0, 4.0));
867    p.line_to((15.0, 4.0));
868    p.line_to((15.0, 6.0));
869    // Cuerpo.
870    p.move_to((6.0, 6.0));
871    p.line_to((7.0, 21.0));
872    p.line_to((17.0, 21.0));
873    p.line_to((18.0, 6.0));
874    // Barras internas.
875    p.move_to((10.0, 10.0));
876    p.line_to((10.0, 17.0));
877    p.move_to((14.0, 10.0));
878    p.line_to((14.0, 17.0));
879    p
880}
881
882/// Chevron apuntando hacia arriba (default) o rotado por `angle_deg`
883/// alrededor del centro del grid (12, 12). 90° = derecha, 180° = abajo,
884/// 270° = izquierda.
885fn path_chevron(angle_deg: f64) -> BezPath {
886    let mut p = BezPath::new();
887    // Chevron base: forma de ^ apuntando arriba.
888    p.move_to((6.0, 14.0));
889    p.line_to((12.0, 8.0));
890    p.line_to((18.0, 14.0));
891    let theta = angle_deg.to_radians();
892    let center = (12.0, 12.0);
893    Affine::translate(center)
894        * Affine::rotate(theta)
895        * Affine::translate((-center.0, -center.1))
896        * p
897}
898
899fn path_home() -> BezPath {
900    // Casa: triángulo de techo + caja rectangular.
901    let mut p = BezPath::new();
902    p.move_to((3.0, 12.0));
903    p.line_to((12.0, 4.0));
904    p.line_to((21.0, 12.0));
905    // Cuerpo.
906    p.move_to((5.0, 11.0));
907    p.line_to((5.0, 20.0));
908    p.line_to((19.0, 20.0));
909    p.line_to((19.0, 11.0));
910    // Puerta.
911    p.move_to((10.0, 20.0));
912    p.line_to((10.0, 14.0));
913    p.line_to((14.0, 14.0));
914    p.line_to((14.0, 20.0));
915    p
916}
917
918fn path_search() -> BezPath {
919    // Lupa: círculo (poligonal 16 segmentos) + mango diagonal.
920    let mut p = BezPath::new();
921    let cx = 10.5;
922    let cy = 10.5;
923    let r = 5.5;
924    let segments = 24;
925    for i in 0..=segments {
926        let theta = core::f64::consts::TAU * (i as f64) / (segments as f64);
927        let x = cx + r * theta.cos();
928        let y = cy + r * theta.sin();
929        if i == 0 {
930            p.move_to((x, y));
931        } else {
932            p.line_to((x, y));
933        }
934    }
935    // Mango.
936    p.move_to((14.5, 14.5));
937    p.line_to((20.0, 20.0));
938    p
939}
940
941fn path_info() -> BezPath {
942    // i: círculo + punto arriba + barra abajo.
943    let mut p = path_circle(12.0, 12.0, 9.0, 32);
944    // Punto.
945    p.move_to((12.0, 7.0));
946    p.line_to((12.0, 8.5));
947    // Barra.
948    p.move_to((12.0, 11.0));
949    p.line_to((12.0, 17.0));
950    p
951}
952
953fn path_warning() -> BezPath {
954    // Triángulo con ! adentro.
955    let mut p = BezPath::new();
956    p.move_to((12.0, 3.0));
957    p.line_to((22.0, 21.0));
958    p.line_to((2.0, 21.0));
959    p.close_path();
960    p.move_to((12.0, 10.0));
961    p.line_to((12.0, 15.0));
962    p.move_to((12.0, 17.5));
963    p.line_to((12.0, 18.5));
964    p
965}
966
967fn path_error() -> BezPath {
968    // Círculo con X adentro.
969    let mut p = path_circle(12.0, 12.0, 9.0, 32);
970    p.move_to((8.5, 8.5));
971    p.line_to((15.5, 15.5));
972    p.move_to((15.5, 8.5));
973    p.line_to((8.5, 15.5));
974    p
975}
976
977fn path_bell() -> BezPath {
978    // Campana: domo + base + badajo.
979    let mut p = BezPath::new();
980    // Cuerpo con curva suave.
981    p.move_to((5.0, 17.0));
982    p.curve_to((5.0, 8.0), (8.0, 5.0), (12.0, 5.0));
983    p.curve_to((16.0, 5.0), (19.0, 8.0), (19.0, 17.0));
984    p.close_path();
985    // Base.
986    p.move_to((3.5, 17.0));
987    p.line_to((20.5, 17.0));
988    // Badajo.
989    p.move_to((10.5, 20.0));
990    p.line_to((13.5, 20.0));
991    p
992}
993
994fn path_settings() -> BezPath {
995    // Engranaje: 8 dientes radiales + agujero central.
996    let mut p = BezPath::new();
997    let cx = 12.0;
998    let cy = 12.0;
999    let inner_r = 6.5;
1000    let outer_r = 9.5;
1001    let teeth = 8;
1002    for i in 0..teeth * 2 {
1003        let theta = core::f64::consts::TAU * (i as f64) / (teeth as f64 * 2.0);
1004        // Cada paso alterna entre inner y outer para formar los dientes.
1005        let r = if i % 2 == 0 { outer_r } else { inner_r };
1006        let x = cx + r * theta.cos();
1007        let y = cy + r * theta.sin();
1008        if i == 0 {
1009            p.move_to((x, y));
1010        } else {
1011            p.line_to((x, y));
1012        }
1013    }
1014    p.close_path();
1015    // Agujero central.
1016    let inner = path_circle(cx, cy, 3.0, 16);
1017    for el in inner.elements() {
1018        p.push(*el);
1019    }
1020    p
1021}
1022
1023fn path_more() -> BezPath {
1024    // Tres puntos horizontales (cada "punto" es un círculo pequeño).
1025    let mut p = BezPath::new();
1026    for (cx, cy) in &[(6.0, 12.0), (12.0, 12.0), (18.0, 12.0)] {
1027        let dot = path_circle(*cx, *cy, 1.5, 12);
1028        for el in dot.elements() {
1029            p.push(*el);
1030        }
1031    }
1032    p
1033}
1034
1035/// Helper: aproxima un círculo con `segments` lados rectos. Para iconos
1036/// stroke esto se ve liso a partir de ~16 segmentos por la suavidad del
1037/// Cap::Round. Más barato y más predecible que cubic Beziers para los
1038/// glifos chiquitos donde vivimos.
1039fn path_circle(cx: f64, cy: f64, r: f64, segments: usize) -> BezPath {
1040    let mut p = BezPath::new();
1041    for i in 0..=segments {
1042        let theta = core::f64::consts::TAU * (i as f64) / (segments as f64);
1043        let x = cx + r * theta.cos();
1044        let y = cy + r * theta.sin();
1045        if i == 0 {
1046            p.move_to((x, y));
1047        } else {
1048            p.line_to((x, y));
1049        }
1050    }
1051    p
1052}
1053
1054// ---------------------------------------------------------------------
1055// Dominio — creador de mundos (llimphi-voxel-studio)
1056// ---------------------------------------------------------------------
1057
1058fn path_globe() -> BezPath {
1059    // Esfera + un meridiano (elipse angosta) + dos paralelos.
1060    let mut p = path_circle(12.0, 12.0, 9.0, 32);
1061    p.move_to((12.0, 3.0));
1062    p.curve_to((6.5, 7.0), (6.5, 17.0), (12.0, 21.0));
1063    p.move_to((12.0, 3.0));
1064    p.curve_to((17.5, 7.0), (17.5, 17.0), (12.0, 21.0));
1065    p.move_to((3.6, 9.3));
1066    p.line_to((20.4, 9.3));
1067    p.move_to((3.6, 14.7));
1068    p.line_to((20.4, 14.7));
1069    p
1070}
1071
1072fn path_mountain() -> BezPath {
1073    // Cordillera de dos picos + nieve en el mayor.
1074    let mut p = BezPath::new();
1075    p.move_to((2.0, 20.0));
1076    p.line_to((8.5, 8.0));
1077    p.line_to((13.0, 15.0));
1078    p.line_to((16.0, 11.0));
1079    p.line_to((22.0, 20.0));
1080    p.close_path();
1081    // Nieve del pico mayor.
1082    p.move_to((6.6, 11.5));
1083    p.line_to((8.5, 8.0));
1084    p.line_to((10.3, 11.0));
1085    p
1086}
1087
1088fn path_leaf() -> BezPath {
1089    // Hoja: contorno por dos curvas + nervadura central.
1090    let mut p = BezPath::new();
1091    p.move_to((5.0, 19.0));
1092    p.curve_to((5.0, 9.0), (12.0, 4.0), (19.0, 5.0));
1093    p.curve_to((20.0, 12.0), (15.0, 19.0), (5.0, 19.0));
1094    p.close_path();
1095    p.move_to((7.5, 16.5));
1096    p.line_to((16.5, 7.5));
1097    p
1098}
1099
1100fn path_user() -> BezPath {
1101    // Persona: cabeza (círculo) + hombros (arco).
1102    let mut p = path_circle(12.0, 8.0, 4.0, 24);
1103    p.move_to((4.5, 20.0));
1104    p.curve_to((4.5, 14.5), (19.5, 14.5), (19.5, 20.0));
1105    p
1106}
1107
1108fn path_droplet() -> BezPath {
1109    // Gota: punta arriba, panza redonda abajo.
1110    let mut p = BezPath::new();
1111    p.move_to((12.0, 3.0));
1112    p.curve_to((12.0, 3.0), (18.5, 11.0), (18.5, 15.5));
1113    p.curve_to((18.5, 19.5), (15.5, 21.5), (12.0, 21.5));
1114    p.curve_to((8.5, 21.5), (5.5, 19.5), (5.5, 15.5));
1115    p.curve_to((5.5, 11.0), (12.0, 3.0), (12.0, 3.0));
1116    p.close_path();
1117    p
1118}
1119
1120// ---------------------------------------------------------------------
1121// Multimedia — transporte de reproductor (media-app y demás)
1122// ---------------------------------------------------------------------
1123
1124fn append(dst: &mut BezPath, src: &BezPath) {
1125    for el in src.elements() {
1126        dst.push(*el);
1127    }
1128}
1129
1130fn path_play() -> BezPath {
1131    // Triángulo apuntando a la derecha.
1132    let mut p = BezPath::new();
1133    p.move_to((8.0, 5.0));
1134    p.line_to((8.0, 19.0));
1135    p.line_to((18.0, 12.0));
1136    p.close_path();
1137    p
1138}
1139
1140fn path_pause() -> BezPath {
1141    // Dos barras verticales.
1142    let mut p = BezPath::new();
1143    p.move_to((9.0, 6.0));
1144    p.line_to((9.0, 18.0));
1145    p.move_to((15.0, 6.0));
1146    p.line_to((15.0, 18.0));
1147    p
1148}
1149
1150fn path_stop() -> BezPath {
1151    let mut p = BezPath::new();
1152    p.move_to((7.0, 7.0));
1153    p.line_to((17.0, 7.0));
1154    p.line_to((17.0, 17.0));
1155    p.line_to((7.0, 17.0));
1156    p.close_path();
1157    p
1158}
1159
1160/// Saltar pista: barra + triángulo (a la izquierda si `back`).
1161fn path_skip(back: bool) -> BezPath {
1162    let mut p = BezPath::new();
1163    if back {
1164        p.move_to((7.0, 6.0));
1165        p.line_to((7.0, 18.0));
1166        p.move_to((17.0, 6.0));
1167        p.line_to((17.0, 18.0));
1168        p.line_to((8.0, 12.0));
1169        p.close_path();
1170    } else {
1171        p.move_to((7.0, 6.0));
1172        p.line_to((7.0, 18.0));
1173        p.line_to((16.0, 12.0));
1174        p.close_path();
1175        p.move_to((17.0, 6.0));
1176        p.line_to((17.0, 18.0));
1177    }
1178    p
1179}
1180
1181/// Lista anterior/siguiente: tres renglones (la lista) y una punta que sale
1182/// hacia ese lado. Un solo glifo, en el mismo trazo de 24×24 que el resto del
1183/// transporte — antes eran los textos `||<` y `>||`, que en la barra se leían
1184/// como dos caracteres sueltos al lado de seis íconos vectoriales.
1185fn path_eye() -> BezPath {
1186    // Ojo: la almendra (dos cuadráticas de punta a punta) + la pupila.
1187    let mut p = BezPath::new();
1188    p.move_to((2.0, 12.0));
1189    p.quad_to((12.0, 2.5), (22.0, 12.0));
1190    p.quad_to((12.0, 21.5), (2.0, 12.0));
1191    p.close_path();
1192    let (cx, cy, r) = (12.0, 12.0, 3.2);
1193    let segments = 20;
1194    for i in 0..=segments {
1195        let theta = core::f64::consts::TAU * (i as f64) / (segments as f64);
1196        let (x, y) = (cx + r * theta.cos(), cy + r * theta.sin());
1197        if i == 0 {
1198            p.move_to((x, y));
1199        } else {
1200            p.line_to((x, y));
1201        }
1202    }
1203    p
1204}
1205
1206fn path_list_skip(back: bool) -> BezPath {
1207    let mut p = BezPath::new();
1208    if back {
1209        // Punta a la izquierda contra su barra, y la lista a la derecha.
1210        p.move_to((4.0, 6.0));
1211        p.line_to((4.0, 18.0));
1212        p.move_to((11.0, 7.0));
1213        p.line_to((11.0, 17.0));
1214        p.line_to((5.0, 12.0));
1215        p.close_path();
1216        for y in [7.5_f64, 12.0, 16.5] {
1217            p.move_to((14.0, y));
1218            p.line_to((20.0, y));
1219        }
1220    } else {
1221        for y in [7.5_f64, 12.0, 16.5] {
1222            p.move_to((4.0, y));
1223            p.line_to((10.0, y));
1224        }
1225        p.move_to((13.0, 7.0));
1226        p.line_to((13.0, 17.0));
1227        p.line_to((19.0, 12.0));
1228        p.close_path();
1229        p.move_to((20.0, 6.0));
1230        p.line_to((20.0, 18.0));
1231    }
1232    p
1233}
1234
1235/// Avance rápido: dos triángulos (a la izquierda si `rewind`).
1236fn path_seek(rewind: bool) -> BezPath {
1237    let mut p = BezPath::new();
1238    if rewind {
1239        p.move_to((11.0, 6.0));
1240        p.line_to((11.0, 18.0));
1241        p.line_to((4.0, 12.0));
1242        p.close_path();
1243        p.move_to((20.0, 6.0));
1244        p.line_to((20.0, 18.0));
1245        p.line_to((13.0, 12.0));
1246        p.close_path();
1247    } else {
1248        p.move_to((4.0, 6.0));
1249        p.line_to((4.0, 18.0));
1250        p.line_to((11.0, 12.0));
1251        p.close_path();
1252        p.move_to((13.0, 6.0));
1253        p.line_to((13.0, 18.0));
1254        p.line_to((20.0, 12.0));
1255        p.close_path();
1256    }
1257    p
1258}
1259
1260/// Altavoz; con ondas (normal) o con una X (mute).
1261fn path_volume(mute: bool) -> BezPath {
1262    let mut p = BezPath::new();
1263    p.move_to((3.0, 9.0));
1264    p.line_to((8.0, 9.0));
1265    p.line_to((12.0, 5.0));
1266    p.line_to((12.0, 19.0));
1267    p.line_to((8.0, 15.0));
1268    p.line_to((3.0, 15.0));
1269    p.close_path();
1270    if mute {
1271        p.move_to((15.0, 9.0));
1272        p.line_to((21.0, 15.0));
1273        p.move_to((21.0, 9.0));
1274        p.line_to((15.0, 15.0));
1275    } else {
1276        p.move_to((15.0, 9.0));
1277        p.quad_to((17.5, 12.0), (15.0, 15.0));
1278        p.move_to((17.5, 7.0));
1279        p.quad_to((21.5, 12.0), (17.5, 17.0));
1280    }
1281    p
1282}
1283
1284fn path_repeat() -> BezPath {
1285    // Dos flechas horizontales opuestas (loop compacto).
1286    let mut p = BezPath::new();
1287    p.move_to((6.0, 9.0));
1288    p.line_to((16.0, 9.0));
1289    p.move_to((14.0, 7.0));
1290    p.line_to((17.0, 9.0));
1291    p.line_to((14.0, 11.0));
1292    p.move_to((18.0, 15.0));
1293    p.line_to((8.0, 15.0));
1294    p.move_to((10.0, 13.0));
1295    p.line_to((7.0, 15.0));
1296    p.line_to((10.0, 17.0));
1297    p
1298}
1299
1300fn path_shuffle() -> BezPath {
1301    // Dos flechas que se cruzan.
1302    let mut p = BezPath::new();
1303    p.move_to((5.0, 8.0));
1304    p.line_to((19.0, 16.0));
1305    p.move_to((16.0, 15.5));
1306    p.line_to((20.0, 16.5));
1307    p.line_to((17.5, 13.0));
1308    p.move_to((5.0, 16.0));
1309    p.line_to((19.0, 8.0));
1310    p.move_to((17.5, 11.0));
1311    p.line_to((20.0, 7.5));
1312    p.line_to((16.0, 8.5));
1313    p
1314}
1315
1316fn path_record() -> BezPath {
1317    path_circle(12.0, 12.0, 5.0, 20)
1318}
1319
1320fn path_equalizer() -> BezPath {
1321    let mut p = BezPath::new();
1322    // Tres deslizadores verticales.
1323    p.move_to((7.0, 5.0));
1324    p.line_to((7.0, 19.0));
1325    p.move_to((12.0, 5.0));
1326    p.line_to((12.0, 19.0));
1327    p.move_to((17.0, 5.0));
1328    p.line_to((17.0, 19.0));
1329    // Perillas a distinta altura.
1330    p.move_to((5.0, 9.0));
1331    p.line_to((9.0, 9.0));
1332    p.move_to((10.0, 14.0));
1333    p.line_to((14.0, 14.0));
1334    p.move_to((15.0, 8.0));
1335    p.line_to((19.0, 8.0));
1336    p
1337}
1338
1339fn path_camera() -> BezPath {
1340    let mut p = BezPath::new();
1341    p.move_to((4.0, 8.0));
1342    p.line_to((7.0, 8.0));
1343    p.line_to((9.0, 6.0));
1344    p.line_to((15.0, 6.0));
1345    p.line_to((17.0, 8.0));
1346    p.line_to((20.0, 8.0));
1347    p.line_to((20.0, 18.0));
1348    p.line_to((4.0, 18.0));
1349    p.close_path();
1350    append(&mut p, &path_circle(12.0, 13.0, 3.5, 16));
1351    p
1352}
1353
1354fn path_gauge() -> BezPath {
1355    // Esfera + aguja (velocidad).
1356    let mut p = path_circle(12.0, 13.0, 6.0, 20);
1357    p.move_to((12.0, 13.0));
1358    p.line_to((16.0, 9.0));
1359    p
1360}
1361
1362// ---------------------------------------------------------------------
1363// Archivos — tipos por extensión (listados de file manager / shell)
1364// ---------------------------------------------------------------------
1365
1366fn path_image() -> BezPath {
1367    // Marco con una montaña y un sol (el clásico "imagen").
1368    let mut p = BezPath::new();
1369    p.move_to((4.0, 5.0));
1370    p.line_to((20.0, 5.0));
1371    p.line_to((20.0, 19.0));
1372    p.line_to((4.0, 19.0));
1373    p.close_path();
1374    // Sol.
1375    append(&mut p, &path_circle(8.5, 9.5, 1.6, 12));
1376    // Montaña (línea quebrada hasta el borde derecho).
1377    p.move_to((4.0, 17.0));
1378    p.line_to((10.0, 12.0));
1379    p.line_to((14.0, 15.0));
1380    p.line_to((17.0, 12.0));
1381    p.line_to((20.0, 15.0));
1382    p
1383}
1384
1385fn path_music() -> BezPath {
1386    // Nota musical: dos cabezas redondas unidas por una plica con bandera.
1387    let mut p = BezPath::new();
1388    // Plicas.
1389    p.move_to((9.0, 18.0));
1390    p.line_to((9.0, 6.0));
1391    p.line_to((19.0, 4.0));
1392    p.line_to((19.0, 16.0));
1393    // Cabeza izquierda.
1394    append(&mut p, &path_circle(7.0, 18.0, 2.0, 14));
1395    // Cabeza derecha.
1396    append(&mut p, &path_circle(17.0, 16.0, 2.0, 14));
1397    p
1398}
1399
1400fn path_film() -> BezPath {
1401    // Tira de película: rectángulo con perforaciones a los lados.
1402    let mut p = BezPath::new();
1403    p.move_to((4.0, 5.0));
1404    p.line_to((20.0, 5.0));
1405    p.line_to((20.0, 19.0));
1406    p.line_to((4.0, 19.0));
1407    p.close_path();
1408    // Rieles internos (separan perforaciones del cuadro central).
1409    p.move_to((8.0, 5.0));
1410    p.line_to((8.0, 19.0));
1411    p.move_to((16.0, 5.0));
1412    p.line_to((16.0, 19.0));
1413    // Perforaciones (cuatro tics por lado).
1414    for y in [7.5, 11.0, 14.5] {
1415        p.move_to((4.0, y));
1416        p.line_to((8.0, y));
1417        p.move_to((16.0, y));
1418        p.line_to((20.0, y));
1419    }
1420    p
1421}
1422
1423fn path_archive() -> BezPath {
1424    // Caja/paquete: tapa arriba + cuerpo + tirador del cierre.
1425    let mut p = BezPath::new();
1426    // Tapa.
1427    p.move_to((3.0, 5.0));
1428    p.line_to((21.0, 5.0));
1429    p.line_to((21.0, 9.0));
1430    p.line_to((3.0, 9.0));
1431    p.close_path();
1432    // Cuerpo.
1433    p.move_to((4.5, 9.0));
1434    p.line_to((4.5, 20.0));
1435    p.line_to((19.5, 20.0));
1436    p.line_to((19.5, 9.0));
1437    // Pestaña del cierre.
1438    p.move_to((10.0, 12.0));
1439    p.line_to((14.0, 12.0));
1440    p
1441}
1442
1443fn path_code() -> BezPath {
1444    // Corchetes angulares </> — universal para "código".
1445    let mut p = BezPath::new();
1446    // Chevron izquierdo.
1447    p.move_to((9.0, 7.0));
1448    p.line_to((4.0, 12.0));
1449    p.line_to((9.0, 17.0));
1450    // Chevron derecho.
1451    p.move_to((15.0, 7.0));
1452    p.line_to((20.0, 12.0));
1453    p.line_to((15.0, 17.0));
1454    // Barra diagonal central.
1455    p.move_to((13.0, 6.0));
1456    p.line_to((11.0, 18.0));
1457    p
1458}
1459
1460fn path_file_text() -> BezPath {
1461    // Documento (como `file`) con líneas de texto adentro.
1462    let mut p = path_file();
1463    p.move_to((8.5, 12.0));
1464    p.line_to((16.5, 12.0));
1465    p.move_to((8.5, 15.0));
1466    p.line_to((16.5, 15.0));
1467    p.move_to((8.5, 18.0));
1468    p.line_to((13.5, 18.0));
1469    p
1470}
1471
1472fn path_link() -> BezPath {
1473    // Symlink: dos eslabones de cadena en diagonal.
1474    let mut p = BezPath::new();
1475    // Eslabón superior-izquierdo (cápsula inclinada).
1476    p.move_to((10.0, 14.0));
1477    p.line_to((7.0, 11.0));
1478    p.curve_to((5.0, 9.0), (5.0, 7.0), (7.0, 5.0));
1479    p.curve_to((9.0, 3.0), (11.0, 3.0), (13.0, 5.0));
1480    p.line_to((15.0, 7.0));
1481    // Eslabón inferior-derecho.
1482    p.move_to((14.0, 10.0));
1483    p.line_to((17.0, 13.0));
1484    p.curve_to((19.0, 15.0), (19.0, 17.0), (17.0, 19.0));
1485    p.curve_to((15.0, 21.0), (13.0, 21.0), (11.0, 19.0));
1486    p.line_to((9.0, 17.0));
1487    p
1488}
1489
1490fn path_font() -> BezPath {
1491    // Letra "A" serif — glifo de fuente tipográfica.
1492    let mut p = BezPath::new();
1493    // Astas de la A.
1494    p.move_to((6.0, 20.0));
1495    p.line_to((12.0, 4.0));
1496    p.line_to((18.0, 20.0));
1497    // Travesaño.
1498    p.move_to((8.5, 14.0));
1499    p.line_to((15.5, 14.0));
1500    // Serifas inferiores.
1501    p.move_to((4.5, 20.0));
1502    p.line_to((7.5, 20.0));
1503    p.move_to((16.5, 20.0));
1504    p.line_to((19.5, 20.0));
1505    p
1506}
1507
1508fn path_grid() -> BezPath {
1509    let mut p = BezPath::new();
1510    // Cuatro celdas 2x2.
1511    for (x, y) in [(4.0, 4.0), (13.0, 4.0), (4.0, 13.0), (13.0, 13.0)] {
1512        p.move_to((x, y));
1513        p.line_to((x + 7.0, y));
1514        p.line_to((x + 7.0, y + 7.0));
1515        p.line_to((x, y + 7.0));
1516        p.close_path();
1517    }
1518    p
1519}
1520
1521fn path_rows() -> BezPath {
1522    let mut p = BezPath::new();
1523    // Tres filas (vista lista).
1524    for y in [6.0, 12.0, 18.0] {
1525        p.move_to((4.0, y));
1526        p.line_to((20.0, y));
1527    }
1528    p
1529}
1530
1531fn path_table() -> BezPath {
1532    let mut p = BezPath::new();
1533    // Marco.
1534    p.move_to((4.0, 5.0));
1535    p.line_to((20.0, 5.0));
1536    p.line_to((20.0, 19.0));
1537    p.line_to((4.0, 19.0));
1538    p.close_path();
1539    // Fila de encabezado.
1540    p.move_to((4.0, 9.5));
1541    p.line_to((20.0, 9.5));
1542    // Separador de columnas (debajo del encabezado).
1543    p.move_to((11.0, 9.5));
1544    p.line_to((11.0, 19.0));
1545    p
1546}
1547
1548fn path_columns() -> BezPath {
1549    let mut p = BezPath::new();
1550    // Dos paneles verticales lado a lado (modo dual).
1551    p.move_to((4.0, 4.0));
1552    p.line_to((11.0, 4.0));
1553    p.line_to((11.0, 20.0));
1554    p.line_to((4.0, 20.0));
1555    p.close_path();
1556    p.move_to((13.0, 4.0));
1557    p.line_to((20.0, 4.0));
1558    p.line_to((20.0, 20.0));
1559    p.line_to((13.0, 20.0));
1560    p.close_path();
1561    p
1562}
1563
1564// ---- Sweep glifo→vector: íconos de sistema/settings -----------------------
1565
1566fn path_clock() -> BezPath {
1567    let mut p = path_circle(12.0, 12.0, 8.5, 28);
1568    p.move_to((12.0, 12.0));
1569    p.line_to((12.0, 6.5)); // manecilla de hora
1570    p.move_to((12.0, 12.0));
1571    p.line_to((16.0, 13.5)); // minutero
1572    p
1573}
1574
1575/// **La misma esfera, con las manecillas a una hora dada.**
1576///
1577/// [`path_clock`] es un glifo: sus dos agujas están **clavadas** (la de hora a
1578/// las 12, el minutero apuntando a las dos y cuarto), porque un ícono de
1579/// catálogo no sabe qué hora es. Esta versión sí: recibe `hour` (0..=23) y
1580/// `minute` (0..=59) y devuelve el mismo dibujo —mismo centro, mismo radio,
1581/// mismo grid de 24×24— con las agujas donde de verdad van.
1582///
1583/// Existe acá y no del lado de quien la pinta porque el reloj de un rail, el de
1584/// un panel y el de una app tienen que ser **el mismo dibujo**: un segundo
1585/// reloj dibujado a mano en otro crate es cómo dos relojes de la misma suite
1586/// terminan con distinto largo de aguja.
1587///
1588/// La de hora es más corta y avanza con los minutos (a las 6:30 apunta entre el
1589/// 6 y el 7, no al 6 clavado), que es lo que distingue un reloj de un dibujo de
1590/// reloj.
1591pub fn path_clock_at(hour: u8, minute: u8) -> BezPath {
1592    let m = (minute % 60) as f64;
1593    let h = (hour % 12) as f64 + m / 60.0;
1594    // Ángulos desde las 12 y en sentido horario: 30° por hora, 6° por minuto.
1595    let ang_h = core::f64::consts::TAU * h / 12.0;
1596    let ang_m = core::f64::consts::TAU * m / 60.0;
1597    // En el grid el eje Y va hacia abajo, así que «arriba» es `-cos`.
1598    let aguja = |p: &mut BezPath, ang: f64, largo: f64| {
1599        p.move_to((12.0, 12.0));
1600        p.line_to((12.0 + largo * ang.sin(), 12.0 - largo * ang.cos()));
1601    };
1602    let mut p = path_circle(12.0, 12.0, 8.5, 28);
1603    aguja(&mut p, ang_h, 4.4); // manecilla de hora
1604    aguja(&mut p, ang_m, 6.2); // minutero
1605    p
1606}
1607
1608/// Pinta [`path_clock_at`] en `rect` con el mismo encuadre que [`paint_icon`]:
1609/// cuadrado centrado, grid de 24×24, joins y caps redondos.
1610///
1611/// Es un hermano de `paint_icon` y no un `Icon` del catálogo porque un `Icon` es
1612/// `Copy` y sin parámetros — la hora no entra en el enum sin volverlo un enum
1613/// con datos, que es exactamente lo que el resto del catálogo no es.
1614pub fn paint_clock_at(
1615    scene: &mut llimphi_compositor::Lienzo,
1616    rect: llimphi_compositor::PaintRect,
1617    color: Color,
1618    stroke_width: f32,
1619    hour: u8,
1620    minute: u8,
1621) {
1622    let side = rect.w.min(rect.h) as f64;
1623    if side <= 0.0 {
1624        return;
1625    }
1626    let scale = side / 24.0;
1627    let tx = rect.x as f64 + (rect.w as f64 - side) * 0.5;
1628    let ty = rect.y as f64 + (rect.h as f64 - side) * 0.5;
1629    let xform = Affine::translate((tx, ty)) * Affine::scale(scale);
1630    let stroke = Stroke::new(stroke_width as f64)
1631        .with_join(Join::Round)
1632        .with_caps(Cap::Round);
1633    scene.stroke(&stroke, xform, color, None, &path_clock_at(hour, minute));
1634}
1635
1636fn path_power() -> BezPath {
1637    use core::f64::consts::{FRAC_PI_2, TAU};
1638    let mut p = BezPath::new();
1639    let (cx, cy, r) = (12.0, 13.0, 7.0);
1640    let segs = 28;
1641    // Anillo con un hueco arriba (clásico símbolo de encendido).
1642    let start = -FRAC_PI_2 + 0.55;
1643    let end = -FRAC_PI_2 - 0.55 + TAU;
1644    for i in 0..=segs {
1645        let t = start + (end - start) * (i as f64) / (segs as f64);
1646        let (x, y) = (cx + r * t.cos(), cy + r * t.sin());
1647        if i == 0 {
1648            p.move_to((x, y));
1649        } else {
1650            p.line_to((x, y));
1651        }
1652    }
1653    p.move_to((12.0, 4.0));
1654    p.line_to((12.0, 12.0)); // barra vertical
1655    p
1656}
1657
1658fn path_mail() -> BezPath {
1659    let mut p = BezPath::new();
1660    p.move_to((3.5, 6.0));
1661    p.line_to((20.5, 6.0));
1662    p.line_to((20.5, 18.0));
1663    p.line_to((3.5, 18.0));
1664    p.close_path();
1665    p.move_to((3.5, 6.5));
1666    p.line_to((12.0, 13.0));
1667    p.line_to((20.5, 6.5)); // solapa
1668    p
1669}
1670
1671fn path_keyboard() -> BezPath {
1672    let mut p = BezPath::new();
1673    p.move_to((2.5, 7.0));
1674    p.line_to((21.5, 7.0));
1675    p.line_to((21.5, 17.0));
1676    p.line_to((2.5, 17.0));
1677    p.close_path();
1678    for (x, y) in [(5.5, 10.0), (9.0, 10.0), (12.5, 10.0), (16.0, 10.0)] {
1679        p.move_to((x, y));
1680        p.line_to((x + 1.6, y));
1681    }
1682    p.move_to((8.0, 13.8));
1683    p.line_to((16.0, 13.8)); // barra espaciadora
1684    p
1685}
1686
1687fn path_palette() -> BezPath {
1688    let mut p = path_circle(12.0, 12.0, 8.5, 28);
1689    let hueco = path_circle(12.0, 16.5, 1.8, 12); // agujero del pulgar
1690    for el in hueco.elements() {
1691        p.push(*el);
1692    }
1693    for (cx, cy) in [(8.3, 9.0), (12.0, 7.4), (15.7, 9.4)] {
1694        let muestra = path_circle(cx, cy, 1.0, 10);
1695        for el in muestra.elements() {
1696            p.push(*el);
1697        }
1698    }
1699    p
1700}
1701
1702fn path_lock() -> BezPath {
1703    let mut p = BezPath::new();
1704    p.move_to((6.0, 11.0));
1705    p.line_to((18.0, 11.0));
1706    p.line_to((18.0, 20.0));
1707    p.line_to((6.0, 20.0));
1708    p.close_path();
1709    p.move_to((8.5, 11.0));
1710    p.line_to((8.5, 8.0));
1711    p.curve_to((8.5, 5.0), (15.5, 5.0), (15.5, 8.0));
1712    p.line_to((15.5, 11.0)); // arco
1713    p
1714}
1715
1716fn path_key() -> BezPath {
1717    let mut p = path_circle(8.0, 8.0, 3.5, 18); // ojo
1718    p.move_to((10.5, 10.5));
1719    p.line_to((19.0, 19.0)); // caña
1720    p.move_to((16.5, 16.5));
1721    p.line_to((19.0, 14.0)); // diente
1722    p
1723}
1724
1725fn path_monitor() -> BezPath {
1726    let mut p = BezPath::new();
1727    p.move_to((3.0, 5.0));
1728    p.line_to((21.0, 5.0));
1729    p.line_to((21.0, 16.0));
1730    p.line_to((3.0, 16.0));
1731    p.close_path();
1732    p.move_to((9.5, 19.0));
1733    p.line_to((14.5, 19.0)); // base
1734    p.move_to((12.0, 16.0));
1735    p.line_to((12.0, 19.0)); // cuello
1736    p
1737}
1738
1739fn path_sparkle() -> BezPath {
1740    let mut p = BezPath::new();
1741    p.move_to((12.0, 3.0));
1742    p.line_to((13.7, 10.3));
1743    p.line_to((21.0, 12.0));
1744    p.line_to((13.7, 13.7));
1745    p.line_to((12.0, 21.0));
1746    p.line_to((10.3, 13.7));
1747    p.line_to((3.0, 12.0));
1748    p.line_to((10.3, 10.3));
1749    p.close_path();
1750    p
1751}
1752
1753fn path_mic() -> BezPath {
1754    let mut p = BezPath::new();
1755    p.move_to((9.0, 6.0));
1756    p.curve_to((9.0, 3.5), (15.0, 3.5), (15.0, 6.0));
1757    p.line_to((15.0, 11.0));
1758    p.curve_to((15.0, 13.5), (9.0, 13.5), (9.0, 11.0));
1759    p.close_path();
1760    p.move_to((6.5, 11.0));
1761    p.curve_to((6.5, 16.0), (17.5, 16.0), (17.5, 11.0)); // soporte
1762    p.move_to((12.0, 16.0));
1763    p.line_to((12.0, 20.0));
1764    p.move_to((8.5, 20.0));
1765    p.line_to((15.5, 20.0));
1766    p
1767}
1768
1769fn path_mouse() -> BezPath {
1770    let mut p = BezPath::new();
1771    p.move_to((7.0, 8.0));
1772    p.curve_to((7.0, 4.0), (17.0, 4.0), (17.0, 8.0));
1773    p.line_to((17.0, 16.0));
1774    p.curve_to((17.0, 20.0), (7.0, 20.0), (7.0, 16.0));
1775    p.close_path();
1776    p.move_to((12.0, 5.0));
1777    p.line_to((12.0, 9.0)); // rueda
1778    p
1779}
1780
1781fn path_cloud() -> BezPath {
1782    let mut p = BezPath::new();
1783    p.move_to((7.0, 17.0));
1784    p.curve_to((3.5, 17.0), (3.5, 12.0), (7.0, 12.0));
1785    p.curve_to((7.0, 7.5), (13.5, 7.0), (14.5, 11.0));
1786    p.curve_to((18.5, 10.5), (20.5, 16.0), (17.0, 17.0));
1787    p.close_path();
1788    p
1789}
1790
1791/// Un corazón, en la caja de 24 de todo el juego.
1792///
1793/// Dos lóbulos y una punta abajo, con las mismas curvas cúbicas que el resto:
1794/// se dibuja **cerrado** para que el relleno lo pinte macizo como los demás
1795/// dientes, y no como un contorno que a 20 px se lee como una mancha.
1796fn path_heart() -> BezPath {
1797    let mut p = BezPath::new();
1798    p.move_to((12.0, 20.5));
1799    // El lóbulo izquierdo, subiendo desde la punta.
1800    p.curve_to((3.0, 14.5), (2.0, 10.5), (3.5, 7.5));
1801    p.curve_to((5.5, 3.5), (10.5, 4.0), (12.0, 8.0));
1802    // Y el derecho, en espejo.
1803    p.curve_to((13.5, 4.0), (18.5, 3.5), (20.5, 7.5));
1804    p.curve_to((22.0, 10.5), (21.0, 14.5), (12.0, 20.5));
1805    p.close_path();
1806    p
1807}
1808
1809fn path_moon() -> BezPath {
1810    let mut p = BezPath::new();
1811    p.move_to((15.0, 4.5));
1812    p.curve_to((9.0, 6.0), (9.0, 18.0), (15.0, 19.5));
1813    p.curve_to((10.0, 17.0), (10.0, 7.0), (15.0, 4.5));
1814    p.close_path();
1815    p
1816}
1817
1818fn path_refresh() -> BezPath {
1819    let mut p = BezPath::new();
1820    let (cx, cy, r) = (12.0, 12.0, 7.0);
1821    let segs = 24;
1822    let (start, end) = (-2.2_f64, 2.2_f64);
1823    for i in 0..=segs {
1824        let t = start + (end - start) * (i as f64) / (segs as f64);
1825        let (x, y) = (cx + r * t.cos(), cy + r * t.sin());
1826        if i == 0 {
1827            p.move_to((x, y));
1828        } else {
1829            p.line_to((x, y));
1830        }
1831    }
1832    let (ex, ey) = (cx + r * end.cos(), cy + r * end.sin());
1833    p.move_to((ex - 1.6, ey - 1.9));
1834    p.line_to((ex, ey));
1835    p.line_to((ex + 2.2, ey - 0.7)); // cabeza de flecha
1836    p
1837}
1838
1839fn path_copy() -> BezPath {
1840    // Dos hojas: la de adelante entera, la de atrás asomando arriba-izquierda
1841    // como una "L". Es el glifo universal de copiar — se reconoce sin rótulo,
1842    // que es justamente para lo que se agregó (los chips «copiar» de shuma).
1843    let mut p = BezPath::new();
1844    p.move_to((16.0, 8.0));
1845    p.line_to((16.0, 4.0));
1846    p.line_to((4.0, 4.0));
1847    p.line_to((4.0, 16.0));
1848    p.line_to((8.0, 16.0));
1849    p.move_to((8.0, 8.0));
1850    p.line_to((20.0, 8.0));
1851    p.line_to((20.0, 20.0));
1852    p.line_to((8.0, 20.0));
1853    p.close_path();
1854    p
1855}
1856
1857fn path_puzzle() -> BezPath {
1858    let mut p = BezPath::new();
1859    p.move_to((5.0, 9.0));
1860    p.line_to((9.5, 9.0));
1861    p.curve_to((9.5, 6.0), (14.5, 6.0), (14.5, 9.0));
1862    p.line_to((19.0, 9.0));
1863    p.line_to((19.0, 19.0));
1864    p.line_to((5.0, 19.0));
1865    p.close_path();
1866    p
1867}
1868
1869// ---------------------------------------------------------------------
1870// Herramientas de pintura — la caja de tullpu (y de cualquier editor)
1871//
1872// Son los catorce glifos que antes no existían y obligaban a rotular cada
1873// herramienta con texto («○ pincel (p)»): una columna de palabras donde
1874// debería haber una columna de dibujos. Cada uno tiene que leerse a 20 px.
1875// ---------------------------------------------------------------------
1876
1877fn path_move() -> BezPath {
1878    // Cruz de cuatro flechas: los dos ejes completos + las puntas.
1879    let mut p = BezPath::new();
1880    p.move_to((12.0, 2.5));
1881    p.line_to((12.0, 21.5));
1882    p.move_to((2.5, 12.0));
1883    p.line_to((21.5, 12.0));
1884    // Puntas: arriba, abajo, izquierda, derecha.
1885    p.move_to((9.0, 5.5));
1886    p.line_to((12.0, 2.5));
1887    p.line_to((15.0, 5.5));
1888    p.move_to((9.0, 18.5));
1889    p.line_to((12.0, 21.5));
1890    p.line_to((15.0, 18.5));
1891    p.move_to((5.5, 9.0));
1892    p.line_to((2.5, 12.0));
1893    p.line_to((5.5, 15.0));
1894    p.move_to((18.5, 9.0));
1895    p.line_to((21.5, 12.0));
1896    p.line_to((18.5, 15.0));
1897    p
1898}
1899
1900fn path_eyedropper() -> BezPath {
1901    // Pipeta en diagonal: punta abajo-izquierda, bulbo arriba-derecha.
1902    let mut p = BezPath::new();
1903    p.move_to((3.5, 20.5));
1904    p.line_to((5.5, 15.5));
1905    p.line_to((13.0, 8.0));
1906    p.line_to((16.0, 11.0));
1907    p.line_to((8.5, 18.5));
1908    p.close_path();
1909    p.move_to((14.5, 4.0));
1910    p.line_to((20.0, 9.5));
1911    p.line_to((17.0, 12.5));
1912    p.line_to((11.5, 7.0));
1913    p.close_path();
1914    p
1915}
1916
1917fn path_marquee() -> BezPath {
1918    // Marco de línea cortada: esquinas enteras y guiones en los lados.
1919    let mut p = BezPath::new();
1920    p.move_to((4.0, 7.5));
1921    p.line_to((4.0, 4.0));
1922    p.line_to((7.5, 4.0));
1923    p.move_to((10.5, 4.0));
1924    p.line_to((13.5, 4.0));
1925    p.move_to((16.5, 4.0));
1926    p.line_to((20.0, 4.0));
1927    p.line_to((20.0, 7.5));
1928    p.move_to((20.0, 10.5));
1929    p.line_to((20.0, 13.5));
1930    p.move_to((20.0, 16.5));
1931    p.line_to((20.0, 20.0));
1932    p.line_to((16.5, 20.0));
1933    p.move_to((13.5, 20.0));
1934    p.line_to((10.5, 20.0));
1935    p.move_to((7.5, 20.0));
1936    p.line_to((4.0, 20.0));
1937    p.line_to((4.0, 16.5));
1938    p.move_to((4.0, 13.5));
1939    p.line_to((4.0, 10.5));
1940    p
1941}
1942
1943fn path_bucket() -> BezPath {
1944    // Balde: cuerpo trapezoidal + asa + la gota que cae por el pico.
1945    let mut p = BezPath::new();
1946    p.move_to((4.0, 7.5));
1947    p.line_to((16.0, 7.5));
1948    p.line_to((14.0, 20.0));
1949    p.line_to((6.0, 20.0));
1950    p.close_path();
1951    p.move_to((6.5, 7.5));
1952    p.curve_to((6.5, 3.0), (13.5, 3.0), (13.5, 7.5));
1953    p.move_to((19.5, 11.0));
1954    p.curve_to((21.5, 14.0), (21.5, 16.5), (19.5, 16.5));
1955    p.curve_to((17.5, 16.5), (17.5, 14.0), (19.5, 11.0));
1956    p.close_path();
1957    p
1958}
1959
1960fn path_brush() -> BezPath {
1961    // Pincel: mango diagonal, la virola que ata las cerdas, y la cabeza que
1962    // moja. Sin la virola el dibujo se lee como una espátula.
1963    let mut p = BezPath::new();
1964    p.move_to((20.5, 3.5));
1965    p.line_to((13.0, 11.0));
1966    p.move_to((10.5, 8.5));
1967    p.line_to((15.5, 13.5));
1968    p.move_to((9.0, 10.0));
1969    p.line_to((14.0, 15.0));
1970    p.line_to((10.5, 18.5));
1971    p.curve_to((8.5, 20.5), (6.0, 20.0), (3.5, 20.5));
1972    p.curve_to((4.0, 18.0), (3.5, 15.5), (5.5, 13.5));
1973    p.close_path();
1974    p
1975}
1976
1977fn path_eraser() -> BezPath {
1978    // Goma inclinada: el bloque, la banda que marca lo gastado, y el piso.
1979    let mut p = BezPath::new();
1980    p.move_to((9.5, 3.5));
1981    p.line_to((20.5, 14.5));
1982    p.line_to((15.0, 20.0));
1983    p.line_to((4.0, 9.0));
1984    p.close_path();
1985    p.move_to((15.0, 9.0));
1986    p.line_to((9.5, 14.5));
1987    p.move_to((10.0, 20.0));
1988    p.line_to((21.0, 20.0));
1989    p
1990}
1991
1992fn path_gradient() -> BezPath {
1993    // Degradé: el cuadro + una trama diagonal que se ralea hacia el claro. Con
1994    // franjas horizontales el dibujo se leía como una hoja de texto.
1995    let mut p = BezPath::new();
1996    p.move_to((4.0, 4.0));
1997    p.line_to((20.0, 4.0));
1998    p.line_to((20.0, 20.0));
1999    p.line_to((4.0, 20.0));
2000    p.close_path();
2001    p.move_to((5.0, 19.0));
2002    p.line_to((19.0, 5.0));
2003    p.move_to((9.0, 19.0));
2004    p.line_to((19.0, 9.0));
2005    p.move_to((13.5, 19.0));
2006    p.line_to((19.0, 13.5));
2007    p
2008}
2009
2010fn path_wand() -> BezPath {
2011    // Varita: la vara en diagonal + dos destellos de distinto tamaño.
2012    let mut p = BezPath::new();
2013    p.move_to((3.5, 20.5));
2014    p.line_to((13.0, 11.0));
2015    p.move_to((17.5, 3.0));
2016    p.line_to((17.5, 9.0));
2017    p.move_to((14.5, 6.0));
2018    p.line_to((20.5, 6.0));
2019    p.move_to((8.0, 3.5));
2020    p.line_to((8.0, 7.5));
2021    p.move_to((6.0, 5.5));
2022    p.line_to((10.0, 5.5));
2023    p
2024}
2025
2026fn path_lasso() -> BezPath {
2027    // Lazo: el bucle cerrado + la cuerda que cuelga y se enrosca.
2028    let mut p = BezPath::new();
2029    p.move_to((12.0, 4.0));
2030    p.curve_to((17.5, 4.0), (21.0, 7.0), (21.0, 10.5));
2031    p.curve_to((21.0, 14.0), (17.5, 17.0), (12.0, 17.0));
2032    p.curve_to((6.5, 17.0), (3.0, 14.0), (3.0, 10.5));
2033    p.curve_to((3.0, 7.0), (6.5, 4.0), (12.0, 4.0));
2034    p.close_path();
2035    p.move_to((7.0, 15.8));
2036    p.curve_to((6.0, 18.5), (7.5, 20.0), (9.5, 20.5));
2037    p
2038}
2039
2040fn path_polygon() -> BezPath {
2041    // Polígono cerrado de vértices marcados: lo que el lazo poligonal deja.
2042    let mut p = BezPath::new();
2043    p.move_to((4.0, 9.0));
2044    p.line_to((11.0, 3.5));
2045    p.line_to((20.0, 8.0));
2046    p.line_to((17.0, 19.5));
2047    p.line_to((7.0, 20.5));
2048    p.close_path();
2049    p
2050}
2051
2052fn path_type() -> BezPath {
2053    // La "T": travesaño con orejas + asta + pie.
2054    let mut p = BezPath::new();
2055    p.move_to((5.0, 6.5));
2056    p.line_to((5.0, 4.0));
2057    p.line_to((19.0, 4.0));
2058    p.line_to((19.0, 6.5));
2059    p.move_to((12.0, 4.0));
2060    p.line_to((12.0, 20.0));
2061    p.move_to((8.5, 20.0));
2062    p.line_to((15.5, 20.0));
2063    p
2064}
2065
2066fn path_stamp() -> BezPath {
2067    // Sello: piso, almohadilla, cuerpo cónico y mango.
2068    let mut p = BezPath::new();
2069    p.move_to((3.5, 20.5));
2070    p.line_to((20.5, 20.5));
2071    p.move_to((5.5, 18.0));
2072    p.line_to((18.5, 18.0));
2073    p.line_to((18.5, 15.0));
2074    p.line_to((5.5, 15.0));
2075    p.close_path();
2076    p.move_to((8.0, 15.0));
2077    p.line_to((9.5, 9.5));
2078    p.line_to((14.5, 9.5));
2079    p.line_to((16.0, 15.0));
2080    p.move_to((9.5, 9.5));
2081    p.line_to((9.5, 6.0));
2082    p.curve_to((9.5, 3.0), (14.5, 3.0), (14.5, 6.0));
2083    p.line_to((14.5, 9.5));
2084    p
2085}
2086
2087fn path_bandage() -> BezPath {
2088    // Curita en diagonal, con la gasa del medio: el glifo de "sanar".
2089    let mut p = BezPath::new();
2090    p.move_to((3.9, 15.9));
2091    p.line_to((15.9, 3.9));
2092    p.curve_to((18.0, 1.8), (22.2, 6.0), (20.1, 8.1));
2093    p.line_to((8.1, 20.1));
2094    p.curve_to((6.0, 22.2), (1.8, 18.0), (3.9, 15.9));
2095    p.close_path();
2096    p.move_to((11.3, 8.5));
2097    p.line_to((15.5, 12.7));
2098    p.move_to((8.5, 11.3));
2099    p.line_to((12.7, 15.5));
2100    p
2101}
2102
2103fn path_pen() -> BezPath {
2104    // Punta de pluma: el cuerpo en cuña y la hendidura que baja a la punta.
2105    let mut p = BezPath::new();
2106    p.move_to((20.5, 3.5));
2107    p.line_to((10.0, 8.0));
2108    p.line_to((4.5, 19.5));
2109    p.line_to((16.0, 14.0));
2110    p.close_path();
2111    p.move_to((4.5, 19.5));
2112    p.line_to((13.0, 11.0));
2113    p
2114}
2115
2116fn path_layers() -> BezPath {
2117    // Tres hojas apiladas vistas en perspectiva: la de arriba entera, las de
2118    // abajo insinuadas por su canto. Es el glifo del panel de capas.
2119    let mut p = BezPath::new();
2120    p.move_to((12.0, 3.0));
2121    p.line_to((21.0, 8.0));
2122    p.line_to((12.0, 13.0));
2123    p.line_to((3.0, 8.0));
2124    p.close_path();
2125    p.move_to((3.0, 12.0));
2126    p.line_to((12.0, 17.0));
2127    p.line_to((21.0, 12.0));
2128    p.move_to((3.0, 16.0));
2129    p.line_to((12.0, 21.0));
2130    p.line_to((21.0, 16.0));
2131    p
2132}
2133
2134// ---------------------------------------------------------------------
2135// Controles de un editor de imágenes — para que un panel de acciones se lea
2136// de un vistazo en vez de leerse renglón por renglón.
2137//
2138// Son stroke puro (el pintor sólo traza, nunca rellena), así que un dibujo que
2139// dependa de un relleno para leerse no se lee: donde hace falta "lleno" va
2140// trama.
2141// ---------------------------------------------------------------------
2142
2143fn path_select_all() -> BezPath {
2144    // El marco cortado de la selección, con un tilde adentro: todo.
2145    let mut p = path_marquee();
2146    p.move_to((8.0, 12.0));
2147    p.line_to((11.0, 15.0));
2148    p.line_to((16.5, 8.5));
2149    p
2150}
2151
2152fn path_expand() -> BezPath {
2153    // El núcleo y cuatro flechas que se van a las esquinas.
2154    let mut p = BezPath::new();
2155    p.move_to((9.0, 9.0));
2156    p.line_to((15.0, 9.0));
2157    p.line_to((15.0, 15.0));
2158    p.line_to((9.0, 15.0));
2159    p.close_path();
2160    p.move_to((7.0, 7.0));
2161    p.line_to((3.5, 3.5));
2162    p.move_to((3.5, 7.0));
2163    p.line_to((3.5, 3.5));
2164    p.line_to((7.0, 3.5));
2165    p.move_to((17.0, 7.0));
2166    p.line_to((20.5, 3.5));
2167    p.move_to((17.0, 3.5));
2168    p.line_to((20.5, 3.5));
2169    p.line_to((20.5, 7.0));
2170    p.move_to((7.0, 17.0));
2171    p.line_to((3.5, 20.5));
2172    p.move_to((3.5, 17.0));
2173    p.line_to((3.5, 20.5));
2174    p.line_to((7.0, 20.5));
2175    p.move_to((17.0, 17.0));
2176    p.line_to((20.5, 20.5));
2177    p.move_to((17.0, 20.5));
2178    p.line_to((20.5, 20.5));
2179    p.line_to((20.5, 17.0));
2180    p
2181}
2182
2183fn path_contract() -> BezPath {
2184    // El marco y cuatro flechas que se vienen al centro.
2185    let mut p = BezPath::new();
2186    p.move_to((3.5, 3.5));
2187    p.line_to((20.5, 3.5));
2188    p.line_to((20.5, 20.5));
2189    p.line_to((3.5, 20.5));
2190    p.close_path();
2191    p.move_to((6.5, 6.5));
2192    p.line_to((10.5, 10.5));
2193    p.move_to((10.5, 7.0));
2194    p.line_to((10.5, 10.5));
2195    p.line_to((7.0, 10.5));
2196    p.move_to((17.5, 6.5));
2197    p.line_to((13.5, 10.5));
2198    p.move_to((13.5, 7.0));
2199    p.line_to((13.5, 10.5));
2200    p.line_to((17.0, 10.5));
2201    p.move_to((6.5, 17.5));
2202    p.line_to((10.5, 13.5));
2203    p.move_to((7.0, 13.5));
2204    p.line_to((10.5, 13.5));
2205    p.line_to((10.5, 17.0));
2206    p.move_to((17.5, 17.5));
2207    p.line_to((13.5, 13.5));
2208    p.move_to((17.0, 13.5));
2209    p.line_to((13.5, 13.5));
2210    p.line_to((13.5, 17.0));
2211    p
2212}
2213
2214fn path_scissors() -> BezPath {
2215    // Dos ojos y dos hojas que se cruzan.
2216    let mut p = path_circle(7.5, 18.0, 2.6, 20);
2217    for el in path_circle(16.5, 18.0, 2.6, 20).elements() {
2218        p.push(*el);
2219    }
2220    p.move_to((5.5, 3.5));
2221    p.line_to((15.5, 15.8));
2222    p.move_to((18.5, 3.5));
2223    p.line_to((8.5, 15.8));
2224    p
2225}
2226
2227fn path_paste() -> BezPath {
2228    // Tabla con la pinza arriba: el portapapeles.
2229    let mut p = BezPath::new();
2230    p.move_to((9.0, 5.0));
2231    p.line_to((5.5, 5.0));
2232    p.line_to((5.5, 20.5));
2233    p.line_to((18.5, 20.5));
2234    p.line_to((18.5, 5.0));
2235    p.line_to((15.0, 5.0));
2236    p.move_to((9.0, 7.0));
2237    p.line_to((9.0, 3.5));
2238    p.line_to((15.0, 3.5));
2239    p.line_to((15.0, 7.0));
2240    p.close_path();
2241    p
2242}
2243
2244fn path_duplicate() -> BezPath {
2245    // Dos hojas y el más que dice que aparece una más.
2246    let mut p = BezPath::new();
2247    p.move_to((15.0, 7.5));
2248    p.line_to((15.0, 4.0));
2249    p.line_to((4.0, 4.0));
2250    p.line_to((4.0, 15.0));
2251    p.line_to((7.5, 15.0));
2252    p.move_to((7.5, 7.5));
2253    p.line_to((16.5, 7.5));
2254    p.line_to((16.5, 16.5));
2255    p.line_to((7.5, 16.5));
2256    p.close_path();
2257    p.move_to((19.5, 15.5));
2258    p.line_to((19.5, 21.0));
2259    p.move_to((16.8, 18.25));
2260    p.line_to((22.2, 18.25));
2261    p
2262}
2263
2264fn path_crop() -> BezPath {
2265    // Las dos escuadras cruzadas del recorte.
2266    let mut p = BezPath::new();
2267    p.move_to((7.0, 2.5));
2268    p.line_to((7.0, 17.0));
2269    p.line_to((21.5, 17.0));
2270    p.move_to((2.5, 7.0));
2271    p.line_to((17.0, 7.0));
2272    p.line_to((17.0, 21.5));
2273    p
2274}
2275
2276fn path_trim() -> BezPath {
2277    // El contenido y cuatro escuadras que se le vienen encima.
2278    let mut p = BezPath::new();
2279    p.move_to((9.0, 9.0));
2280    p.line_to((15.0, 9.0));
2281    p.line_to((15.0, 15.0));
2282    p.line_to((9.0, 15.0));
2283    p.close_path();
2284    p.move_to((4.0, 7.5));
2285    p.line_to((4.0, 4.0));
2286    p.line_to((7.5, 4.0));
2287    p.move_to((16.5, 4.0));
2288    p.line_to((20.0, 4.0));
2289    p.line_to((20.0, 7.5));
2290    p.move_to((20.0, 16.5));
2291    p.line_to((20.0, 20.0));
2292    p.line_to((16.5, 20.0));
2293    p.move_to((7.5, 20.0));
2294    p.line_to((4.0, 20.0));
2295    p.line_to((4.0, 16.5));
2296    p
2297}
2298
2299fn path_mask() -> BezPath {
2300    // El cuadro y el disco que le tapa una esquina.
2301    let mut p = BezPath::new();
2302    p.move_to((3.5, 4.0));
2303    p.line_to((14.5, 4.0));
2304    p.line_to((14.5, 15.0));
2305    p.line_to((3.5, 15.0));
2306    p.close_path();
2307    for el in path_circle(15.5, 15.5, 5.0, 28).elements() {
2308        p.push(*el);
2309    }
2310    p
2311}
2312
2313fn path_invert() -> BezPath {
2314    // Disco partido al medio, con una mitad tramada.
2315    let mut p = path_circle(12.0, 12.0, 8.5, 32);
2316    p.move_to((12.0, 3.5));
2317    p.line_to((12.0, 20.5));
2318    p.move_to((14.0, 8.0));
2319    p.line_to((19.0, 8.0));
2320    p.move_to((14.0, 12.0));
2321    p.line_to((20.3, 12.0));
2322    p.move_to((14.0, 16.0));
2323    p.line_to((19.0, 16.0));
2324    p
2325}
2326
2327/// El espejo: el eje cortado y los dos triángulos enfrentados. `vertical` gira
2328/// el dibujo un cuarto de vuelta — son la misma idea en el otro eje, y tenerlos
2329/// en una sola función es lo que impide que se desalineen entre sí.
2330fn path_mirror(vertical: bool) -> BezPath {
2331    let mut p = BezPath::new();
2332    if vertical {
2333        p.move_to((3.0, 12.0));
2334        p.line_to((7.0, 12.0));
2335        p.move_to((10.0, 12.0));
2336        p.line_to((14.0, 12.0));
2337        p.move_to((17.0, 12.0));
2338        p.line_to((21.0, 12.0));
2339        p.move_to((7.0, 9.5));
2340        p.line_to((12.0, 4.0));
2341        p.line_to((17.0, 9.5));
2342        p.close_path();
2343        p.move_to((7.0, 14.5));
2344        p.line_to((12.0, 20.0));
2345        p.line_to((17.0, 14.5));
2346        p.close_path();
2347        return p;
2348    }
2349    p.move_to((12.0, 3.0));
2350    p.line_to((12.0, 7.0));
2351    p.move_to((12.0, 10.0));
2352    p.line_to((12.0, 14.0));
2353    p.move_to((12.0, 17.0));
2354    p.line_to((12.0, 21.0));
2355    p.move_to((9.5, 7.0));
2356    p.line_to((4.0, 12.0));
2357    p.line_to((9.5, 17.0));
2358    p.close_path();
2359    p.move_to((14.5, 7.0));
2360    p.line_to((20.0, 12.0));
2361    p.line_to((14.5, 17.0));
2362    p.close_path();
2363    p
2364}
2365
2366fn path_square() -> BezPath {
2367    let mut p = BezPath::new();
2368    p.move_to((3.5, 6.0));
2369    p.line_to((20.5, 6.0));
2370    p.line_to((20.5, 18.0));
2371    p.line_to((3.5, 18.0));
2372    p.close_path();
2373    p
2374}
2375
2376fn path_circulo() -> BezPath {
2377    // Elipse por cuatro arcos (kappa · radio).
2378    let mut p = BezPath::new();
2379    p.move_to((3.5, 12.0));
2380    p.curve_to((3.5, 8.41), (7.31, 5.5), (12.0, 5.5));
2381    p.curve_to((16.69, 5.5), (20.5, 8.41), (20.5, 12.0));
2382    p.curve_to((20.5, 15.59), (16.69, 18.5), (12.0, 18.5));
2383    p.curve_to((7.31, 18.5), (3.5, 15.59), (3.5, 12.0));
2384    p.close_path();
2385    p
2386}
2387
2388fn path_rounded_square() -> BezPath {
2389    let mut p = BezPath::new();
2390    p.move_to((8.0, 4.0));
2391    p.line_to((16.0, 4.0));
2392    p.curve_to((18.2, 4.0), (20.0, 5.8), (20.0, 8.0));
2393    p.line_to((20.0, 16.0));
2394    p.curve_to((20.0, 18.2), (18.2, 20.0), (16.0, 20.0));
2395    p.line_to((8.0, 20.0));
2396    p.curve_to((5.8, 20.0), (4.0, 18.2), (4.0, 16.0));
2397    p.line_to((4.0, 8.0));
2398    p.curve_to((4.0, 5.8), (5.8, 4.0), (8.0, 4.0));
2399    p.close_path();
2400    p
2401}
2402
2403fn path_star() -> BezPath {
2404    // Cinco puntas, radio 9 y 3.9, la primera mirando arriba.
2405    let mut p = BezPath::new();
2406    p.move_to((12.0, 3.0));
2407    p.line_to((14.29, 8.85));
2408    p.line_to((20.56, 9.22));
2409    p.line_to((15.71, 13.21));
2410    p.line_to((17.29, 19.28));
2411    p.line_to((12.0, 15.9));
2412    p.line_to((6.71, 19.28));
2413    p.line_to((8.29, 13.21));
2414    p.line_to((3.44, 9.22));
2415    p.line_to((9.71, 8.85));
2416    p.close_path();
2417    p
2418}
2419
2420fn path_hexagon() -> BezPath {
2421    let mut p = BezPath::new();
2422    p.move_to((12.0, 3.5));
2423    p.line_to((19.36, 7.75));
2424    p.line_to((19.36, 16.25));
2425    p.line_to((12.0, 20.5));
2426    p.line_to((4.64, 16.25));
2427    p.line_to((4.64, 7.75));
2428    p.close_path();
2429    p
2430}
2431
2432fn path_line() -> BezPath {
2433    // El segmento y sus dos nodos: es una forma con puntas, no una raya suelta.
2434    let mut p = BezPath::new();
2435    p.move_to((6.5, 17.5));
2436    p.line_to((17.5, 6.5));
2437    p.move_to((3.5, 17.0));
2438    p.line_to((6.5, 17.0));
2439    p.line_to((6.5, 20.0));
2440    p.line_to((3.5, 20.0));
2441    p.close_path();
2442    p.move_to((17.5, 4.0));
2443    p.line_to((20.5, 4.0));
2444    p.line_to((20.5, 7.0));
2445    p.line_to((17.5, 7.0));
2446    p.close_path();
2447    p
2448}
2449
2450fn path_flatten() -> BezPath {
2451    // Dos flechas que se juntan contra una barra.
2452    let mut p = BezPath::new();
2453    p.move_to((3.5, 12.0));
2454    p.line_to((20.5, 12.0));
2455    p.move_to((12.0, 3.5));
2456    p.line_to((12.0, 9.0));
2457    p.move_to((9.0, 6.0));
2458    p.line_to((12.0, 9.0));
2459    p.line_to((15.0, 6.0));
2460    p.move_to((12.0, 20.5));
2461    p.line_to((12.0, 15.0));
2462    p.move_to((9.0, 18.0));
2463    p.line_to((12.0, 15.0));
2464    p.line_to((15.0, 18.0));
2465    p
2466}
2467
2468/// La flecha circular. `horario` la manda para un lado o para el otro; la punta
2469/// va arriba en los dos casos, que es donde el ojo la busca.
2470fn path_rotate(horario: bool) -> BezPath {
2471    let mut p = BezPath::new();
2472    if horario {
2473        p.move_to((19.0, 12.0));
2474        p.curve_to((19.0, 15.87), (15.87, 19.0), (12.0, 19.0));
2475        p.curve_to((8.13, 19.0), (5.0, 15.87), (5.0, 12.0));
2476        p.curve_to((5.0, 8.13), (8.13, 5.0), (12.0, 5.0));
2477        p.move_to((9.2, 2.3));
2478        p.line_to((12.4, 5.0));
2479        p.line_to((9.2, 7.7));
2480        return p;
2481    }
2482    p.move_to((5.0, 12.0));
2483    p.curve_to((5.0, 15.87), (8.13, 19.0), (12.0, 19.0));
2484    p.curve_to((15.87, 19.0), (19.0, 15.87), (19.0, 12.0));
2485    p.curve_to((19.0, 8.13), (15.87, 5.0), (12.0, 5.0));
2486    p.move_to((14.8, 2.3));
2487    p.line_to((11.6, 5.0));
2488    p.line_to((14.8, 7.7));
2489    p
2490}
2491
2492fn path_shadow() -> BezPath {
2493    // El objeto y su sombra, corridos.
2494    let mut p = BezPath::new();
2495    p.move_to((8.5, 8.5));
2496    p.line_to((20.5, 8.5));
2497    p.line_to((20.5, 20.5));
2498    p.line_to((8.5, 20.5));
2499    p.close_path();
2500    p.move_to((3.5, 3.5));
2501    p.line_to((15.5, 3.5));
2502    p.line_to((15.5, 15.5));
2503    p.line_to((3.5, 15.5));
2504    p.close_path();
2505    p
2506}
2507
2508fn path_brightness() -> BezPath {
2509    // Sol: disco y ocho rayos.
2510    let mut p = path_circle(12.0, 12.0, 4.5, 24);
2511    p.move_to((12.0, 2.5));
2512    p.line_to((12.0, 5.5));
2513    p.move_to((12.0, 18.5));
2514    p.line_to((12.0, 21.5));
2515    p.move_to((2.5, 12.0));
2516    p.line_to((5.5, 12.0));
2517    p.move_to((18.5, 12.0));
2518    p.line_to((21.5, 12.0));
2519    p.move_to((5.3, 5.3));
2520    p.line_to((7.4, 7.4));
2521    p.move_to((16.6, 16.6));
2522    p.line_to((18.7, 18.7));
2523    p.move_to((18.7, 5.3));
2524    p.line_to((16.6, 7.4));
2525    p.move_to((7.4, 16.6));
2526    p.line_to((5.3, 18.7));
2527    p
2528}
2529
2530fn path_contrast() -> BezPath {
2531    // Cuadro partido en diagonal, con un triángulo tramado.
2532    let mut p = BezPath::new();
2533    p.move_to((4.0, 4.0));
2534    p.line_to((20.0, 4.0));
2535    p.line_to((20.0, 20.0));
2536    p.line_to((4.0, 20.0));
2537    p.close_path();
2538    p.move_to((20.0, 4.0));
2539    p.line_to((4.0, 20.0));
2540    p.move_to((11.0, 4.0));
2541    p.line_to((4.0, 11.0));
2542    p.move_to((16.0, 4.0));
2543    p.line_to((4.0, 16.0));
2544    p
2545}
2546
2547fn path_color_wheel() -> BezPath {
2548    // Disco con tres radios: el círculo de tono.
2549    let mut p = path_circle(12.0, 12.0, 8.5, 32);
2550    p.move_to((12.0, 12.0));
2551    p.line_to((12.0, 3.5));
2552    p.move_to((12.0, 12.0));
2553    p.line_to((19.36, 16.25));
2554    p.move_to((12.0, 12.0));
2555    p.line_to((4.64, 16.25));
2556    p
2557}
2558
2559fn path_blur() -> BezPath {
2560    // Una mitad con canto y otra que se ralea.
2561    let mut p = BezPath::new();
2562    p.move_to((12.0, 4.0));
2563    p.line_to((4.0, 4.0));
2564    p.line_to((4.0, 20.0));
2565    p.line_to((12.0, 20.0));
2566    p.move_to((14.5, 5.5));
2567    p.line_to((14.5, 18.5));
2568    p.move_to((17.5, 7.5));
2569    p.line_to((17.5, 16.5));
2570    p.move_to((20.0, 9.5));
2571    p.line_to((20.0, 14.5));
2572    p
2573}
2574
2575fn path_levels() -> BezPath {
2576    // El histograma: barras y su piso.
2577    let mut p = BezPath::new();
2578    p.move_to((3.5, 20.0));
2579    p.line_to((20.5, 20.0));
2580    p.move_to((6.0, 20.0));
2581    p.line_to((6.0, 15.0));
2582    p.move_to((9.0, 20.0));
2583    p.line_to((9.0, 9.0));
2584    p.move_to((12.0, 20.0));
2585    p.line_to((12.0, 4.5));
2586    p.move_to((15.0, 20.0));
2587    p.line_to((15.0, 11.0));
2588    p.move_to((18.0, 20.0));
2589    p.line_to((18.0, 16.0));
2590    p
2591}
2592
2593fn path_curve() -> BezPath {
2594    // La caja del editor de curvas y su S.
2595    let mut p = BezPath::new();
2596    p.move_to((4.0, 4.0));
2597    p.line_to((20.0, 4.0));
2598    p.line_to((20.0, 20.0));
2599    p.line_to((4.0, 20.0));
2600    p.close_path();
2601    // Los tiradores van bien afuera de la recta a propósito: con los de la
2602    // primera versión (11,19)-(13,5) la curva salía casi diagonal y el dibujo
2603    // se leía como un cuadro tachado, no como una curva.
2604    p.move_to((4.0, 20.0));
2605    p.curve_to((15.0, 20.5), (9.0, 3.5), (20.0, 4.0));
2606    p
2607}
2608
2609fn path_segment() -> BezPath {
2610    // La silueta y el corte punteado que la separa del fondo.
2611    let mut p = BezPath::new();
2612    p.move_to((8.0, 18.5));
2613    p.curve_to((5.0, 14.0), (7.0, 6.5), (12.0, 6.5));
2614    p.curve_to((17.0, 6.5), (19.0, 14.0), (16.0, 18.5));
2615    p.close_path();
2616    p.move_to((3.5, 8.0));
2617    p.line_to((3.5, 12.0));
2618    p.move_to((3.5, 15.0));
2619    p.line_to((3.5, 19.0));
2620    p.move_to((20.5, 8.0));
2621    p.line_to((20.5, 12.0));
2622    p.move_to((20.5, 15.0));
2623    p.line_to((20.5, 19.0));
2624    p
2625}
2626
2627fn path_upscale() -> BezPath {
2628    // El cuadro chico y la flecha que lo lleva a grande.
2629    let mut p = BezPath::new();
2630    p.move_to((4.0, 13.0));
2631    p.line_to((11.0, 13.0));
2632    p.line_to((11.0, 20.0));
2633    p.line_to((4.0, 20.0));
2634    p.close_path();
2635    p.move_to((13.0, 11.0));
2636    p.line_to((20.5, 3.5));
2637    p.move_to((14.5, 3.5));
2638    p.line_to((20.5, 3.5));
2639    p.line_to((20.5, 9.5));
2640    p
2641}
2642
2643#[cfg(test)]
2644mod tests {
2645    use super::*;
2646    // Sólo los usan los tests: arriba quedaban como imports muertos. Deriva de
2647    // la migración a `no_std`, donde todo lo de `alloc` se importa a mano.
2648    use alloc::vec::Vec;
2649
2650    /// **Las agujas apuntan a la hora que se les pide.**
2651    ///
2652    /// Se mide la punta de cada aguja en el grid de 24×24, que es lo único que
2653    /// el dibujo promete: a las 3 en punto el minutero mira a las 12 (arriba) y
2654    /// la de hora a las 3 (derecha); a las 6 en punto las dos caen sobre el eje
2655    /// vertical, una arriba y otra abajo. Es un `assert` sobre coordenadas y no
2656    /// un pantallazo: un reloj mal orientado se ve bien en una captura de las
2657    /// 12:00 y mal el resto del día.
2658    #[test]
2659    fn las_agujas_del_reloj_marcan_la_hora() {
2660        // Punta de la aguja `i` (0 = hora, 1 = minutero) en `(x, y)`.
2661        let punta = |h: u8, m: u8, i: usize| -> (f64, f64) {
2662            let pts: Vec<(f64, f64)> = path_clock_at(h, m)
2663                .elements()
2664                .iter()
2665                .filter_map(|e| match e {
2666                    llimphi_compositor::peniko::kurbo::PathEl::LineTo(p) => Some((p.x, p.y)),
2667                    _ => None,
2668                })
2669                .collect();
2670            // Los últimos dos `line_to` son las dos agujas (antes va el círculo).
2671            let n = pts.len();
2672            pts[n - 2 + i]
2673        };
2674        let cerca = |a: f64, b: f64| (a - b).abs() < 0.05;
2675
2676        // 3:00 — minutero a las 12, aguja de hora a las 3.
2677        let (hx, hy) = punta(3, 0, 0);
2678        assert!(cerca(hy, 12.0) && hx > 12.0, "3:00: la aguja de hora mira a la derecha");
2679        let (mx, my) = punta(3, 0, 1);
2680        assert!(cerca(mx, 12.0) && my < 12.0, "3:00: el minutero mira arriba");
2681
2682        // 6:00 — las dos sobre el eje vertical, la de hora abajo.
2683        let (hx, hy) = punta(6, 0, 0);
2684        assert!(cerca(hx, 12.0) && hy > 12.0, "6:00: la aguja de hora mira abajo");
2685
2686        // 6:30 — la de hora **no** se queda en el 6: avanzó media hora, o sea
2687        // que ya pasó del eje. Esto es lo que separa un reloj de un dibujo.
2688        let (hx, _) = punta(6, 30, 0);
2689        assert!(hx < 11.9, "6:30: la aguja de hora ya dejó atrás el 6");
2690
2691        // Las 24 h entran por el 12: 15:00 se ve igual que 3:00.
2692        assert_eq!(punta(15, 0, 0), punta(3, 0, 0), "la tarde se dibuja como la mañana");
2693
2694        // Y el minutero es más largo que la de hora, en toda hora.
2695        for h in 0..24u8 {
2696            let (hx, hy) = punta(h, 20, 0);
2697            let (mx, my) = punta(h, 20, 1);
2698            let largo = |x: f64, y: f64| ((x - 12.0).powi(2) + (y - 12.0).powi(2)).sqrt();
2699            assert!(largo(mx, my) > largo(hx, hy), "{h}:20 — el minutero es el largo");
2700        }
2701    }
2702
2703    #[test]
2704    fn all_icons_have_nonempty_path() {
2705        let all = [
2706            Icon::File, Icon::Folder, Icon::FolderOpen, Icon::Save, Icon::Open,
2707            Icon::Plus, Icon::Minus, Icon::X, Icon::Check, Icon::Edit, Icon::Trash,
2708            Icon::ChevronUp, Icon::ChevronDown, Icon::ChevronLeft, Icon::ChevronRight,
2709            Icon::Home, Icon::Search, Icon::Info, Icon::Warning, Icon::Error,
2710            Icon::Bell, Icon::Settings, Icon::More,
2711            Icon::Play, Icon::Pause, Icon::Stop, Icon::SkipBack, Icon::SkipForward,
2712            Icon::Rewind, Icon::FastForward, Icon::Volume, Icon::VolumeMute,
2713            Icon::Repeat, Icon::Shuffle, Icon::Record, Icon::Equalizer,
2714            Icon::Camera, Icon::Gauge,
2715            Icon::Image, Icon::Music, Icon::Film, Icon::Archive,
2716            Icon::Code, Icon::FileText, Icon::Link, Icon::Font,
2717        ];
2718        for icon in all {
2719            let p = icon.path();
2720            assert!(
2721                p.elements().len() > 0,
2722                "icono {} produjo path vacío",
2723                icon.name()
2724            );
2725        }
2726    }
2727
2728    #[test]
2729    fn icon_names_are_unique() {
2730        let all = [
2731            Icon::File, Icon::Folder, Icon::FolderOpen, Icon::Save, Icon::Open,
2732            Icon::Plus, Icon::Minus, Icon::X, Icon::Check, Icon::Edit, Icon::Trash,
2733            Icon::ChevronUp, Icon::ChevronDown, Icon::ChevronLeft, Icon::ChevronRight,
2734            Icon::Home, Icon::Search, Icon::Info, Icon::Warning, Icon::Error,
2735            Icon::Bell, Icon::Settings, Icon::More,
2736            Icon::Play, Icon::Pause, Icon::Stop, Icon::SkipBack, Icon::SkipForward,
2737            Icon::Rewind, Icon::FastForward, Icon::Volume, Icon::VolumeMute,
2738            Icon::Repeat, Icon::Shuffle, Icon::Record, Icon::Equalizer,
2739            Icon::Camera, Icon::Gauge,
2740            Icon::Image, Icon::Music, Icon::Film, Icon::Archive,
2741            Icon::Code, Icon::FileText, Icon::Link, Icon::Font,
2742        ];
2743        let mut names: Vec<&str> = all.iter().map(|i| i.name()).collect();
2744        let n = names.len();
2745        names.sort();
2746        names.dedup();
2747        assert_eq!(names.len(), n, "nombres duplicados en Icon::name()");
2748    }
2749
2750    #[test]
2751    fn iconos_nuevos_tienen_path() {
2752        let nuevos = [
2753            Icon::Clock, Icon::Power, Icon::Mail, Icon::Keyboard, Icon::Palette,
2754            Icon::Lock, Icon::Key, Icon::Monitor, Icon::Sparkle, Icon::Mic,
2755            Icon::Mouse, Icon::Cloud, Icon::Moon, Icon::Refresh, Icon::Puzzle,
2756            Icon::Copy,
2757        ];
2758        for icon in nuevos {
2759            assert!(!icon.path().elements().is_empty(), "{} sin path", icon.name());
2760        }
2761    }
2762
2763    #[test]
2764    fn iconos_de_pintura_tienen_path() {
2765        let herramientas = [
2766            Icon::Move, Icon::Eyedropper, Icon::Marquee, Icon::Bucket, Icon::Brush,
2767            Icon::Eraser, Icon::Gradient, Icon::Wand, Icon::Lasso, Icon::Polygon,
2768            Icon::Type, Icon::Stamp, Icon::Bandage, Icon::Pen, Icon::Layers,
2769        ];
2770        for icon in herramientas {
2771            assert!(!icon.path().elements().is_empty(), "{} sin path", icon.name());
2772        }
2773        // Y sus nombres no chocan con los del set viejo.
2774        let mut nombres: Vec<&str> = herramientas.iter().map(|i| i.name()).collect();
2775        let n = nombres.len();
2776        nombres.sort();
2777        nombres.dedup();
2778        assert_eq!(nombres.len(), n, "nombres duplicados entre las herramientas");
2779    }
2780
2781    #[test]
2782    fn iconos_de_control_tienen_path() {
2783        let controles = [
2784            Icon::SelectAll, Icon::Expand, Icon::Contract, Icon::Scissors, Icon::Paste,
2785            Icon::Duplicate, Icon::Crop, Icon::Trim, Icon::Mask, Icon::Invert,
2786            Icon::Mirror, Icon::MirrorV, Icon::Square, Icon::Circle, Icon::RoundedSquare,
2787            Icon::Star, Icon::Hexagon, Icon::Line, Icon::Flatten, Icon::RotateCw,
2788            Icon::RotateCcw, Icon::Shadow, Icon::Brightness, Icon::Contrast,
2789            Icon::ColorWheel, Icon::Blur, Icon::Levels, Icon::Curve, Icon::Segment,
2790            Icon::Upscale, Icon::Heart,
2791        ];
2792        for icon in controles {
2793            assert!(!icon.path().elements().is_empty(), "{} sin path", icon.name());
2794        }
2795        let mut nombres: Vec<&str> = controles.iter().map(|i| i.name()).collect();
2796        let n = nombres.len();
2797        nombres.sort();
2798        nombres.dedup();
2799        assert_eq!(nombres.len(), n, "nombres duplicados entre los controles");
2800    }
2801
2802    #[test]
2803    fn ningun_glifo_de_menu_cae_al_texto() {
2804        // El barrido del 2026-09-10 sobre el contextual de nahual: de once
2805        // glifos, seis caían al fallback de texto y dos de ésos ni existen en
2806        // las fuentes de la máquina (salían de **notdef**, la pila de líneas).
2807        // Un menú con vectores, glifos de fuente y notdef mezclados no se ve
2808        // mal por un detalle: se ve como tres menús apilados.
2809        //
2810        // Este test es la cobertura: un glifo nuevo en un menú se agrega acá y
2811        // el que lo agregue se entera en el acto si no tiene ícono.
2812        for g in [
2813            "✂", "✎", "🔗", "⛁", "✧", "▸", "↺", "◇", "◈", "▫", "⎙", "🗑", "⎘",
2814            "✓", "✗", "+", "ⓘ", "★", "✦", "⚙", "▦", "☰", "⟳", "📁", "📄",
2815        ] {
2816            assert!(
2817                Icon::from_glyph(g).is_some(),
2818                "el glifo {g:?} no tiene ícono vectorial: en un menú sale como texto",
2819            );
2820        }
2821    }
2822
2823    /// Los dos de lista tienen trazo y nombre propio: si uno se agrega al enum
2824    /// y se olvida en `path()`, el botón sale vacío y nadie lo nota hasta verlo.
2825    #[test]
2826    fn los_iconos_de_lista_tienen_path_y_nombre() {
2827        assert!(!Icon::Eye.path().elements().is_empty(), "eye sin path");
2828        for icon in [Icon::ListPrev, Icon::ListNext] {
2829            assert!(!icon.path().elements().is_empty(), "{} sin path", icon.name());
2830        }
2831        assert_ne!(Icon::ListPrev.name(), Icon::ListNext.name());
2832        assert_ne!(Icon::ListPrev.path().to_svg(), Icon::ListNext.path().to_svg());
2833        // Y no son los de pista: el ícono tiene que distinguir lista de pista.
2834        assert_ne!(Icon::ListPrev.path().to_svg(), Icon::SkipBack.path().to_svg());
2835    }
2836
2837    #[test]
2838    fn from_glyph_mapea_los_comunes() {
2839        // Casos que cubrían el bug del wawa-panel y de los menús.
2840        assert_eq!(Icon::from_glyph("⚙"), Some(Icon::Settings));
2841        assert_eq!(Icon::from_glyph("🎨"), Some(Icon::Palette));
2842        assert_eq!(Icon::from_glyph("⌨"), Some(Icon::Keyboard));
2843        assert_eq!(Icon::from_glyph("⏻"), Some(Icon::Power));
2844        assert_eq!(Icon::from_glyph("✉"), Some(Icon::Mail));
2845        assert_eq!(Icon::from_glyph("🔊"), Some(Icon::Volume));
2846        assert_eq!(Icon::from_glyph("≡"), Some(Icon::Rows));
2847        assert_eq!(Icon::from_glyph("▶"), Some(Icon::Play));
2848        // Variation selector (emoji presentation) se tolera.
2849        assert_eq!(Icon::from_glyph("🖥\u{FE0F}"), Some(Icon::Monitor));
2850        // Texto real (no un glifo-ícono) no matchea → caería a texto.
2851        assert_eq!(Icon::from_glyph("Archivo"), None);
2852        assert_eq!(Icon::from_glyph(""), None);
2853    }
2854}