tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
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}