tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
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
52pub mod app_icons;
53
54use llimphi_ui::llimphi_layout::taffy::{
55 prelude::{percent, Size, Style},
56 Position,
57};
58use llimphi_ui::llimphi_raster::kurbo::{Affine, BezPath, Cap, Join, Stroke};
59use llimphi_ui::llimphi_raster::peniko::Color;
60use llimphi_ui::View;
61
62/// Catálogo de iconos del set canónico.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum Icon {
65 // --- Documento ---
66 File,
67 Folder,
68 FolderOpen,
69 Save,
70 Open,
71 // --- Edición ---
72 Plus,
73 Minus,
74 X,
75 Check,
76 Edit,
77 Trash,
78 // --- Navegación ---
79 ChevronUp,
80 ChevronDown,
81 ChevronLeft,
82 ChevronRight,
83 Home,
84 Search,
85 // --- Estado ---
86 Info,
87 Warning,
88 Error,
89 Bell,
90 // --- Sistema ---
91 Settings,
92 More,
93 // --- Multimedia ---
94 Play,
95 Pause,
96 Stop,
97 SkipBack,
98 SkipForward,
99 Rewind,
100 FastForward,
101 Volume,
102 VolumeMute,
103 Repeat,
104 Shuffle,
105 Record,
106 Equalizer,
107 Camera,
108 Gauge,
109 // --- Archivos (tipos por extensión, para listados de file manager) ---
110 Image,
111 Music,
112 Film,
113 Archive,
114 Code,
115 FileText,
116 Link,
117 Font,
118 // --- Vistas / layout (toolbars de file manager) ---
119 /// Grilla 2×2 (vista iconos / galería).
120 Grid,
121 /// Tres filas horizontales (vista lista).
122 Rows,
123 /// Tabla: marco + fila de encabezado + columnas (vista detalle).
124 Table,
125 /// Dos paneles verticales lado a lado (modo dual).
126 Columns,
127}
128
129impl Icon {
130 /// Identificador estable en lowercase con underscores. Útil para
131 /// debugging, persistir choices del usuario, o mapear desde strings
132 /// en config.
133 pub const fn name(self) -> &'static str {
134 match self {
135 Icon::File => "file",
136 Icon::Folder => "folder",
137 Icon::FolderOpen => "folder_open",
138 Icon::Save => "save",
139 Icon::Open => "open",
140 Icon::Plus => "plus",
141 Icon::Minus => "minus",
142 Icon::X => "x",
143 Icon::Check => "check",
144 Icon::Edit => "edit",
145 Icon::Trash => "trash",
146 Icon::ChevronUp => "chevron_up",
147 Icon::ChevronDown => "chevron_down",
148 Icon::ChevronLeft => "chevron_left",
149 Icon::ChevronRight => "chevron_right",
150 Icon::Home => "home",
151 Icon::Search => "search",
152 Icon::Info => "info",
153 Icon::Warning => "warning",
154 Icon::Error => "error",
155 Icon::Bell => "bell",
156 Icon::Settings => "settings",
157 Icon::More => "more",
158 Icon::Play => "play",
159 Icon::Pause => "pause",
160 Icon::Stop => "stop",
161 Icon::SkipBack => "skip_back",
162 Icon::SkipForward => "skip_forward",
163 Icon::Rewind => "rewind",
164 Icon::FastForward => "fast_forward",
165 Icon::Volume => "volume",
166 Icon::VolumeMute => "volume_mute",
167 Icon::Repeat => "repeat",
168 Icon::Shuffle => "shuffle",
169 Icon::Record => "record",
170 Icon::Equalizer => "equalizer",
171 Icon::Camera => "camera",
172 Icon::Gauge => "gauge",
173 Icon::Image => "image",
174 Icon::Music => "music",
175 Icon::Film => "film",
176 Icon::Archive => "archive",
177 Icon::Code => "code",
178 Icon::FileText => "file_text",
179 Icon::Link => "link",
180 Icon::Font => "font",
181 Icon::Grid => "grid",
182 Icon::Rows => "rows",
183 Icon::Table => "table",
184 Icon::Columns => "columns",
185 }
186 }
187
188 /// Devuelve el `BezPath` del icono en coords del grid 24×24. La
189 /// app raramente lo necesita directamente — es lo que consume
190 /// internamente [`icon_view`] / [`paint_icon`].
191 pub fn path(self) -> BezPath {
192 match self {
193 Icon::File => path_file(),
194 Icon::Folder => path_folder(),
195 Icon::FolderOpen => path_folder_open(),
196 Icon::Save => path_save(),
197 Icon::Open => path_open(),
198 Icon::Plus => path_plus(),
199 Icon::Minus => path_minus(),
200 Icon::X => path_x(),
201 Icon::Check => path_check(),
202 Icon::Edit => path_edit(),
203 Icon::Trash => path_trash(),
204 Icon::ChevronUp => path_chevron(0.0),
205 Icon::ChevronDown => path_chevron(180.0),
206 Icon::ChevronLeft => path_chevron(270.0),
207 Icon::ChevronRight => path_chevron(90.0),
208 Icon::Home => path_home(),
209 Icon::Search => path_search(),
210 Icon::Info => path_info(),
211 Icon::Warning => path_warning(),
212 Icon::Error => path_error(),
213 Icon::Bell => path_bell(),
214 Icon::Settings => path_settings(),
215 Icon::More => path_more(),
216 Icon::Play => path_play(),
217 Icon::Pause => path_pause(),
218 Icon::Stop => path_stop(),
219 Icon::SkipBack => path_skip(true),
220 Icon::SkipForward => path_skip(false),
221 Icon::Rewind => path_seek(true),
222 Icon::FastForward => path_seek(false),
223 Icon::Volume => path_volume(false),
224 Icon::VolumeMute => path_volume(true),
225 Icon::Repeat => path_repeat(),
226 Icon::Shuffle => path_shuffle(),
227 Icon::Record => path_record(),
228 Icon::Equalizer => path_equalizer(),
229 Icon::Camera => path_camera(),
230 Icon::Gauge => path_gauge(),
231 Icon::Image => path_image(),
232 Icon::Music => path_music(),
233 Icon::Film => path_film(),
234 Icon::Archive => path_archive(),
235 Icon::Code => path_code(),
236 Icon::FileText => path_file_text(),
237 Icon::Link => path_link(),
238 Icon::Font => path_font(),
239 Icon::Grid => path_grid(),
240 Icon::Rows => path_rows(),
241 Icon::Table => path_table(),
242 Icon::Columns => path_columns(),
243 }
244 }
245}
246
247/// Construye un `View` que pinta el icono ocupando todo el rect del
248/// padre. El icono se escala uniformemente al mínimo lado y se centra.
249///
250/// - `stroke_width` en unidades del grid 24×24 (típico: `1.6`).
251pub fn icon_view<Msg: Clone + 'static>(
252 icon: Icon,
253 color: Color,
254 stroke_width: f32,
255) -> View<Msg> {
256 View::new(Style {
257 position: Position::Absolute,
258 size: Size {
259 width: percent(1.0_f32),
260 height: percent(1.0_f32),
261 },
262 ..Default::default()
263 })
264 .paint_with(move |scene, _ts, rect| {
265 paint_icon(scene, rect, icon, color, stroke_width);
266 })
267}
268
269/// Pintor crudo — útil cuando una app quiere stampear varios iconos
270/// dentro del mismo `paint_with` (paneles compuestos, toolbars
271/// generadas dinámicamente).
272pub fn paint_icon(
273 scene: &mut llimphi_ui::llimphi_raster::vello::Scene,
274 rect: llimphi_ui::PaintRect,
275 icon: Icon,
276 color: Color,
277 stroke_width: f32,
278) {
279 let side = rect.w.min(rect.h) as f64;
280 if side <= 0.0 {
281 return;
282 }
283 let scale = side / 24.0;
284 let tx = rect.x as f64 + (rect.w as f64 - side) * 0.5;
285 let ty = rect.y as f64 + (rect.h as f64 - side) * 0.5;
286 let xform = Affine::translate((tx, ty)) * Affine::scale(scale);
287
288 let stroke = Stroke::new(stroke_width as f64)
289 .with_join(Join::Round)
290 .with_caps(Cap::Round);
291 let path = icon.path();
292 scene.stroke(&stroke, xform, color, None, &path);
293}
294
295// =====================================================================
296// Paths — todos en grid 24×24, origen top-left, eje Y hacia abajo
297// =====================================================================
298//
299// Cada path es geometría minimalista. Los joins y caps son Round (los
300// fija el renderer), así que los corners salen suaves sin tener que
301// definir curvas extra.
302
303fn path_file() -> BezPath {
304 // Documento: rectángulo con esquina superior-derecha plegada.
305 let mut p = BezPath::new();
306 p.move_to((6.0, 3.0));
307 p.line_to((14.0, 3.0));
308 p.line_to((19.0, 8.0));
309 p.line_to((19.0, 21.0));
310 p.line_to((6.0, 21.0));
311 p.close_path();
312 // Pliegue: línea desde la esquina superior-derecha del file hasta
313 // donde "se dobla", luego al borde.
314 p.move_to((14.0, 3.0));
315 p.line_to((14.0, 8.0));
316 p.line_to((19.0, 8.0));
317 p
318}
319
320fn path_folder() -> BezPath {
321 // Folder cerrado: cuerpo + lengüeta arriba-izquierda.
322 let mut p = BezPath::new();
323 p.move_to((3.0, 8.0));
324 p.line_to((3.0, 19.0));
325 p.line_to((21.0, 19.0));
326 p.line_to((21.0, 8.0));
327 p.line_to((11.0, 8.0));
328 p.line_to((9.0, 5.0));
329 p.line_to((3.0, 5.0));
330 p.close_path();
331 p
332}
333
334fn path_folder_open() -> BezPath {
335 // Folder con tapa levantada: el "techo" se inclina hacia la derecha.
336 let mut p = BezPath::new();
337 // Caja inferior.
338 p.move_to((3.0, 8.0));
339 p.line_to((3.0, 19.0));
340 p.line_to((21.0, 19.0));
341 p.line_to((23.0, 11.0));
342 p.line_to((7.0, 11.0));
343 p.line_to((5.0, 19.0));
344 // Lengüeta de la izquierda (sigue ahí).
345 p.move_to((3.0, 8.0));
346 p.line_to((9.0, 8.0));
347 p.line_to((11.0, 11.0));
348 p.line_to((21.0, 11.0));
349 p
350}
351
352fn path_save() -> BezPath {
353 // Floppy: cuadrado con muesca top-right y rectángulo de label abajo.
354 let mut p = BezPath::new();
355 p.move_to((4.0, 4.0));
356 p.line_to((17.0, 4.0));
357 p.line_to((20.0, 7.0));
358 p.line_to((20.0, 20.0));
359 p.line_to((4.0, 20.0));
360 p.close_path();
361 // Slot del shutter arriba.
362 p.move_to((8.0, 4.0));
363 p.line_to((8.0, 9.0));
364 p.line_to((15.0, 9.0));
365 p.line_to((15.0, 4.0));
366 // Rectángulo de label abajo.
367 p.move_to((7.0, 13.0));
368 p.line_to((17.0, 13.0));
369 p.line_to((17.0, 20.0));
370 p.line_to((7.0, 20.0));
371 p.close_path();
372 p
373}
374
375fn path_open() -> BezPath {
376 // Carpeta abriéndose hacia arriba con una flecha que entra.
377 let mut p = BezPath::new();
378 // Folder base.
379 p.move_to((3.0, 19.0));
380 p.line_to((3.0, 8.0));
381 p.line_to((9.0, 8.0));
382 p.line_to((11.0, 10.0));
383 p.line_to((21.0, 10.0));
384 p.line_to((21.0, 19.0));
385 p.close_path();
386 // Flecha entrando desde arriba al centro.
387 p.move_to((12.0, 2.0));
388 p.line_to((12.0, 14.0));
389 p.move_to((9.0, 11.0));
390 p.line_to((12.0, 14.0));
391 p.line_to((15.0, 11.0));
392 p
393}
394
395fn path_plus() -> BezPath {
396 let mut p = BezPath::new();
397 p.move_to((12.0, 5.0));
398 p.line_to((12.0, 19.0));
399 p.move_to((5.0, 12.0));
400 p.line_to((19.0, 12.0));
401 p
402}
403
404fn path_minus() -> BezPath {
405 let mut p = BezPath::new();
406 p.move_to((5.0, 12.0));
407 p.line_to((19.0, 12.0));
408 p
409}
410
411fn path_x() -> BezPath {
412 let mut p = BezPath::new();
413 p.move_to((6.0, 6.0));
414 p.line_to((18.0, 18.0));
415 p.move_to((18.0, 6.0));
416 p.line_to((6.0, 18.0));
417 p
418}
419
420fn path_check() -> BezPath {
421 let mut p = BezPath::new();
422 p.move_to((5.0, 13.0));
423 p.line_to((10.0, 18.0));
424 p.line_to((20.0, 6.0));
425 p
426}
427
428fn path_edit() -> BezPath {
429 // Lápiz: cuerpo diagonal + punta + tag de borrador.
430 let mut p = BezPath::new();
431 p.move_to((4.0, 20.0));
432 p.line_to((8.0, 19.0));
433 p.line_to((20.0, 7.0));
434 p.line_to((17.0, 4.0));
435 p.line_to((5.0, 16.0));
436 p.close_path();
437 p.move_to((14.0, 7.0));
438 p.line_to((17.0, 10.0));
439 p
440}
441
442fn path_trash() -> BezPath {
443 // Tacho: tapa con manijita + cuerpo con tres barras verticales.
444 let mut p = BezPath::new();
445 // Tapa.
446 p.move_to((4.0, 6.0));
447 p.line_to((20.0, 6.0));
448 // Manijita.
449 p.move_to((9.0, 6.0));
450 p.line_to((9.0, 4.0));
451 p.line_to((15.0, 4.0));
452 p.line_to((15.0, 6.0));
453 // Cuerpo.
454 p.move_to((6.0, 6.0));
455 p.line_to((7.0, 21.0));
456 p.line_to((17.0, 21.0));
457 p.line_to((18.0, 6.0));
458 // Barras internas.
459 p.move_to((10.0, 10.0));
460 p.line_to((10.0, 17.0));
461 p.move_to((14.0, 10.0));
462 p.line_to((14.0, 17.0));
463 p
464}
465
466/// Chevron apuntando hacia arriba (default) o rotado por `angle_deg`
467/// alrededor del centro del grid (12, 12). 90° = derecha, 180° = abajo,
468/// 270° = izquierda.
469fn path_chevron(angle_deg: f64) -> BezPath {
470 let mut p = BezPath::new();
471 // Chevron base: forma de ^ apuntando arriba.
472 p.move_to((6.0, 14.0));
473 p.line_to((12.0, 8.0));
474 p.line_to((18.0, 14.0));
475 let theta = angle_deg.to_radians();
476 let center = (12.0, 12.0);
477 Affine::translate(center)
478 * Affine::rotate(theta)
479 * Affine::translate((-center.0, -center.1))
480 * p
481}
482
483fn path_home() -> BezPath {
484 // Casa: triángulo de techo + caja rectangular.
485 let mut p = BezPath::new();
486 p.move_to((3.0, 12.0));
487 p.line_to((12.0, 4.0));
488 p.line_to((21.0, 12.0));
489 // Cuerpo.
490 p.move_to((5.0, 11.0));
491 p.line_to((5.0, 20.0));
492 p.line_to((19.0, 20.0));
493 p.line_to((19.0, 11.0));
494 // Puerta.
495 p.move_to((10.0, 20.0));
496 p.line_to((10.0, 14.0));
497 p.line_to((14.0, 14.0));
498 p.line_to((14.0, 20.0));
499 p
500}
501
502fn path_search() -> BezPath {
503 // Lupa: círculo (poligonal 16 segmentos) + mango diagonal.
504 let mut p = BezPath::new();
505 let cx = 10.5;
506 let cy = 10.5;
507 let r = 5.5;
508 let segments = 24;
509 for i in 0..=segments {
510 let theta = std::f64::consts::TAU * (i as f64) / (segments as f64);
511 let x = cx + r * theta.cos();
512 let y = cy + r * theta.sin();
513 if i == 0 {
514 p.move_to((x, y));
515 } else {
516 p.line_to((x, y));
517 }
518 }
519 // Mango.
520 p.move_to((14.5, 14.5));
521 p.line_to((20.0, 20.0));
522 p
523}
524
525fn path_info() -> BezPath {
526 // i: círculo + punto arriba + barra abajo.
527 let mut p = path_circle(12.0, 12.0, 9.0, 32);
528 // Punto.
529 p.move_to((12.0, 7.0));
530 p.line_to((12.0, 8.5));
531 // Barra.
532 p.move_to((12.0, 11.0));
533 p.line_to((12.0, 17.0));
534 p
535}
536
537fn path_warning() -> BezPath {
538 // Triángulo con ! adentro.
539 let mut p = BezPath::new();
540 p.move_to((12.0, 3.0));
541 p.line_to((22.0, 21.0));
542 p.line_to((2.0, 21.0));
543 p.close_path();
544 p.move_to((12.0, 10.0));
545 p.line_to((12.0, 15.0));
546 p.move_to((12.0, 17.5));
547 p.line_to((12.0, 18.5));
548 p
549}
550
551fn path_error() -> BezPath {
552 // Círculo con X adentro.
553 let mut p = path_circle(12.0, 12.0, 9.0, 32);
554 p.move_to((8.5, 8.5));
555 p.line_to((15.5, 15.5));
556 p.move_to((15.5, 8.5));
557 p.line_to((8.5, 15.5));
558 p
559}
560
561fn path_bell() -> BezPath {
562 // Campana: domo + base + badajo.
563 let mut p = BezPath::new();
564 // Cuerpo con curva suave.
565 p.move_to((5.0, 17.0));
566 p.curve_to((5.0, 8.0), (8.0, 5.0), (12.0, 5.0));
567 p.curve_to((16.0, 5.0), (19.0, 8.0), (19.0, 17.0));
568 p.close_path();
569 // Base.
570 p.move_to((3.5, 17.0));
571 p.line_to((20.5, 17.0));
572 // Badajo.
573 p.move_to((10.5, 20.0));
574 p.line_to((13.5, 20.0));
575 p
576}
577
578fn path_settings() -> BezPath {
579 // Engranaje: 8 dientes radiales + agujero central.
580 let mut p = BezPath::new();
581 let cx = 12.0;
582 let cy = 12.0;
583 let inner_r = 6.5;
584 let outer_r = 9.5;
585 let teeth = 8;
586 for i in 0..teeth * 2 {
587 let theta = std::f64::consts::TAU * (i as f64) / (teeth as f64 * 2.0);
588 // Cada paso alterna entre inner y outer para formar los dientes.
589 let r = if i % 2 == 0 { outer_r } else { inner_r };
590 let x = cx + r * theta.cos();
591 let y = cy + r * theta.sin();
592 if i == 0 {
593 p.move_to((x, y));
594 } else {
595 p.line_to((x, y));
596 }
597 }
598 p.close_path();
599 // Agujero central.
600 let inner = path_circle(cx, cy, 3.0, 16);
601 for el in inner.elements() {
602 p.push(*el);
603 }
604 p
605}
606
607fn path_more() -> BezPath {
608 // Tres puntos horizontales (cada "punto" es un círculo pequeño).
609 let mut p = BezPath::new();
610 for (cx, cy) in &[(6.0, 12.0), (12.0, 12.0), (18.0, 12.0)] {
611 let dot = path_circle(*cx, *cy, 1.5, 12);
612 for el in dot.elements() {
613 p.push(*el);
614 }
615 }
616 p
617}
618
619/// Helper: aproxima un círculo con `segments` lados rectos. Para iconos
620/// stroke esto se ve liso a partir de ~16 segmentos por la suavidad del
621/// Cap::Round. Más barato y más predecible que cubic Beziers para los
622/// glifos chiquitos donde vivimos.
623fn path_circle(cx: f64, cy: f64, r: f64, segments: usize) -> BezPath {
624 let mut p = BezPath::new();
625 for i in 0..=segments {
626 let theta = std::f64::consts::TAU * (i as f64) / (segments as f64);
627 let x = cx + r * theta.cos();
628 let y = cy + r * theta.sin();
629 if i == 0 {
630 p.move_to((x, y));
631 } else {
632 p.line_to((x, y));
633 }
634 }
635 p
636}
637
638// ---------------------------------------------------------------------
639// Multimedia — transporte de reproductor (media-app y demás)
640// ---------------------------------------------------------------------
641
642fn append(dst: &mut BezPath, src: &BezPath) {
643 for el in src.elements() {
644 dst.push(*el);
645 }
646}
647
648fn path_play() -> BezPath {
649 // Triángulo apuntando a la derecha.
650 let mut p = BezPath::new();
651 p.move_to((8.0, 5.0));
652 p.line_to((8.0, 19.0));
653 p.line_to((18.0, 12.0));
654 p.close_path();
655 p
656}
657
658fn path_pause() -> BezPath {
659 // Dos barras verticales.
660 let mut p = BezPath::new();
661 p.move_to((9.0, 6.0));
662 p.line_to((9.0, 18.0));
663 p.move_to((15.0, 6.0));
664 p.line_to((15.0, 18.0));
665 p
666}
667
668fn path_stop() -> BezPath {
669 let mut p = BezPath::new();
670 p.move_to((7.0, 7.0));
671 p.line_to((17.0, 7.0));
672 p.line_to((17.0, 17.0));
673 p.line_to((7.0, 17.0));
674 p.close_path();
675 p
676}
677
678/// Saltar pista: barra + triángulo (a la izquierda si `back`).
679fn path_skip(back: bool) -> BezPath {
680 let mut p = BezPath::new();
681 if back {
682 p.move_to((7.0, 6.0));
683 p.line_to((7.0, 18.0));
684 p.move_to((17.0, 6.0));
685 p.line_to((17.0, 18.0));
686 p.line_to((8.0, 12.0));
687 p.close_path();
688 } else {
689 p.move_to((7.0, 6.0));
690 p.line_to((7.0, 18.0));
691 p.line_to((16.0, 12.0));
692 p.close_path();
693 p.move_to((17.0, 6.0));
694 p.line_to((17.0, 18.0));
695 }
696 p
697}
698
699/// Avance rápido: dos triángulos (a la izquierda si `rewind`).
700fn path_seek(rewind: bool) -> BezPath {
701 let mut p = BezPath::new();
702 if rewind {
703 p.move_to((11.0, 6.0));
704 p.line_to((11.0, 18.0));
705 p.line_to((4.0, 12.0));
706 p.close_path();
707 p.move_to((20.0, 6.0));
708 p.line_to((20.0, 18.0));
709 p.line_to((13.0, 12.0));
710 p.close_path();
711 } else {
712 p.move_to((4.0, 6.0));
713 p.line_to((4.0, 18.0));
714 p.line_to((11.0, 12.0));
715 p.close_path();
716 p.move_to((13.0, 6.0));
717 p.line_to((13.0, 18.0));
718 p.line_to((20.0, 12.0));
719 p.close_path();
720 }
721 p
722}
723
724/// Altavoz; con ondas (normal) o con una X (mute).
725fn path_volume(mute: bool) -> BezPath {
726 let mut p = BezPath::new();
727 p.move_to((3.0, 9.0));
728 p.line_to((8.0, 9.0));
729 p.line_to((12.0, 5.0));
730 p.line_to((12.0, 19.0));
731 p.line_to((8.0, 15.0));
732 p.line_to((3.0, 15.0));
733 p.close_path();
734 if mute {
735 p.move_to((15.0, 9.0));
736 p.line_to((21.0, 15.0));
737 p.move_to((21.0, 9.0));
738 p.line_to((15.0, 15.0));
739 } else {
740 p.move_to((15.0, 9.0));
741 p.quad_to((17.5, 12.0), (15.0, 15.0));
742 p.move_to((17.5, 7.0));
743 p.quad_to((21.5, 12.0), (17.5, 17.0));
744 }
745 p
746}
747
748fn path_repeat() -> BezPath {
749 // Dos flechas horizontales opuestas (loop compacto).
750 let mut p = BezPath::new();
751 p.move_to((6.0, 9.0));
752 p.line_to((16.0, 9.0));
753 p.move_to((14.0, 7.0));
754 p.line_to((17.0, 9.0));
755 p.line_to((14.0, 11.0));
756 p.move_to((18.0, 15.0));
757 p.line_to((8.0, 15.0));
758 p.move_to((10.0, 13.0));
759 p.line_to((7.0, 15.0));
760 p.line_to((10.0, 17.0));
761 p
762}
763
764fn path_shuffle() -> BezPath {
765 // Dos flechas que se cruzan.
766 let mut p = BezPath::new();
767 p.move_to((5.0, 8.0));
768 p.line_to((19.0, 16.0));
769 p.move_to((16.0, 15.5));
770 p.line_to((20.0, 16.5));
771 p.line_to((17.5, 13.0));
772 p.move_to((5.0, 16.0));
773 p.line_to((19.0, 8.0));
774 p.move_to((17.5, 11.0));
775 p.line_to((20.0, 7.5));
776 p.line_to((16.0, 8.5));
777 p
778}
779
780fn path_record() -> BezPath {
781 path_circle(12.0, 12.0, 5.0, 20)
782}
783
784fn path_equalizer() -> BezPath {
785 let mut p = BezPath::new();
786 // Tres deslizadores verticales.
787 p.move_to((7.0, 5.0));
788 p.line_to((7.0, 19.0));
789 p.move_to((12.0, 5.0));
790 p.line_to((12.0, 19.0));
791 p.move_to((17.0, 5.0));
792 p.line_to((17.0, 19.0));
793 // Perillas a distinta altura.
794 p.move_to((5.0, 9.0));
795 p.line_to((9.0, 9.0));
796 p.move_to((10.0, 14.0));
797 p.line_to((14.0, 14.0));
798 p.move_to((15.0, 8.0));
799 p.line_to((19.0, 8.0));
800 p
801}
802
803fn path_camera() -> BezPath {
804 let mut p = BezPath::new();
805 p.move_to((4.0, 8.0));
806 p.line_to((7.0, 8.0));
807 p.line_to((9.0, 6.0));
808 p.line_to((15.0, 6.0));
809 p.line_to((17.0, 8.0));
810 p.line_to((20.0, 8.0));
811 p.line_to((20.0, 18.0));
812 p.line_to((4.0, 18.0));
813 p.close_path();
814 append(&mut p, &path_circle(12.0, 13.0, 3.5, 16));
815 p
816}
817
818fn path_gauge() -> BezPath {
819 // Esfera + aguja (velocidad).
820 let mut p = path_circle(12.0, 13.0, 6.0, 20);
821 p.move_to((12.0, 13.0));
822 p.line_to((16.0, 9.0));
823 p
824}
825
826// ---------------------------------------------------------------------
827// Archivos — tipos por extensión (listados de file manager / shell)
828// ---------------------------------------------------------------------
829
830fn path_image() -> BezPath {
831 // Marco con una montaña y un sol (el clásico "imagen").
832 let mut p = BezPath::new();
833 p.move_to((4.0, 5.0));
834 p.line_to((20.0, 5.0));
835 p.line_to((20.0, 19.0));
836 p.line_to((4.0, 19.0));
837 p.close_path();
838 // Sol.
839 append(&mut p, &path_circle(8.5, 9.5, 1.6, 12));
840 // Montaña (línea quebrada hasta el borde derecho).
841 p.move_to((4.0, 17.0));
842 p.line_to((10.0, 12.0));
843 p.line_to((14.0, 15.0));
844 p.line_to((17.0, 12.0));
845 p.line_to((20.0, 15.0));
846 p
847}
848
849fn path_music() -> BezPath {
850 // Nota musical: dos cabezas redondas unidas por una plica con bandera.
851 let mut p = BezPath::new();
852 // Plicas.
853 p.move_to((9.0, 18.0));
854 p.line_to((9.0, 6.0));
855 p.line_to((19.0, 4.0));
856 p.line_to((19.0, 16.0));
857 // Cabeza izquierda.
858 append(&mut p, &path_circle(7.0, 18.0, 2.0, 14));
859 // Cabeza derecha.
860 append(&mut p, &path_circle(17.0, 16.0, 2.0, 14));
861 p
862}
863
864fn path_film() -> BezPath {
865 // Tira de película: rectángulo con perforaciones a los lados.
866 let mut p = BezPath::new();
867 p.move_to((4.0, 5.0));
868 p.line_to((20.0, 5.0));
869 p.line_to((20.0, 19.0));
870 p.line_to((4.0, 19.0));
871 p.close_path();
872 // Rieles internos (separan perforaciones del cuadro central).
873 p.move_to((8.0, 5.0));
874 p.line_to((8.0, 19.0));
875 p.move_to((16.0, 5.0));
876 p.line_to((16.0, 19.0));
877 // Perforaciones (cuatro tics por lado).
878 for y in [7.5, 11.0, 14.5] {
879 p.move_to((4.0, y));
880 p.line_to((8.0, y));
881 p.move_to((16.0, y));
882 p.line_to((20.0, y));
883 }
884 p
885}
886
887fn path_archive() -> BezPath {
888 // Caja/paquete: tapa arriba + cuerpo + tirador del cierre.
889 let mut p = BezPath::new();
890 // Tapa.
891 p.move_to((3.0, 5.0));
892 p.line_to((21.0, 5.0));
893 p.line_to((21.0, 9.0));
894 p.line_to((3.0, 9.0));
895 p.close_path();
896 // Cuerpo.
897 p.move_to((4.5, 9.0));
898 p.line_to((4.5, 20.0));
899 p.line_to((19.5, 20.0));
900 p.line_to((19.5, 9.0));
901 // Pestaña del cierre.
902 p.move_to((10.0, 12.0));
903 p.line_to((14.0, 12.0));
904 p
905}
906
907fn path_code() -> BezPath {
908 // Corchetes angulares </> — universal para "código".
909 let mut p = BezPath::new();
910 // Chevron izquierdo.
911 p.move_to((9.0, 7.0));
912 p.line_to((4.0, 12.0));
913 p.line_to((9.0, 17.0));
914 // Chevron derecho.
915 p.move_to((15.0, 7.0));
916 p.line_to((20.0, 12.0));
917 p.line_to((15.0, 17.0));
918 // Barra diagonal central.
919 p.move_to((13.0, 6.0));
920 p.line_to((11.0, 18.0));
921 p
922}
923
924fn path_file_text() -> BezPath {
925 // Documento (como `file`) con líneas de texto adentro.
926 let mut p = path_file();
927 p.move_to((8.5, 12.0));
928 p.line_to((16.5, 12.0));
929 p.move_to((8.5, 15.0));
930 p.line_to((16.5, 15.0));
931 p.move_to((8.5, 18.0));
932 p.line_to((13.5, 18.0));
933 p
934}
935
936fn path_link() -> BezPath {
937 // Symlink: dos eslabones de cadena en diagonal.
938 let mut p = BezPath::new();
939 // Eslabón superior-izquierdo (cápsula inclinada).
940 p.move_to((10.0, 14.0));
941 p.line_to((7.0, 11.0));
942 p.curve_to((5.0, 9.0), (5.0, 7.0), (7.0, 5.0));
943 p.curve_to((9.0, 3.0), (11.0, 3.0), (13.0, 5.0));
944 p.line_to((15.0, 7.0));
945 // Eslabón inferior-derecho.
946 p.move_to((14.0, 10.0));
947 p.line_to((17.0, 13.0));
948 p.curve_to((19.0, 15.0), (19.0, 17.0), (17.0, 19.0));
949 p.curve_to((15.0, 21.0), (13.0, 21.0), (11.0, 19.0));
950 p.line_to((9.0, 17.0));
951 p
952}
953
954fn path_font() -> BezPath {
955 // Letra "A" serif — glifo de fuente tipográfica.
956 let mut p = BezPath::new();
957 // Astas de la A.
958 p.move_to((6.0, 20.0));
959 p.line_to((12.0, 4.0));
960 p.line_to((18.0, 20.0));
961 // Travesaño.
962 p.move_to((8.5, 14.0));
963 p.line_to((15.5, 14.0));
964 // Serifas inferiores.
965 p.move_to((4.5, 20.0));
966 p.line_to((7.5, 20.0));
967 p.move_to((16.5, 20.0));
968 p.line_to((19.5, 20.0));
969 p
970}
971
972fn path_grid() -> BezPath {
973 let mut p = BezPath::new();
974 // Cuatro celdas 2x2.
975 for (x, y) in [(4.0, 4.0), (13.0, 4.0), (4.0, 13.0), (13.0, 13.0)] {
976 p.move_to((x, y));
977 p.line_to((x + 7.0, y));
978 p.line_to((x + 7.0, y + 7.0));
979 p.line_to((x, y + 7.0));
980 p.close_path();
981 }
982 p
983}
984
985fn path_rows() -> BezPath {
986 let mut p = BezPath::new();
987 // Tres filas (vista lista).
988 for y in [6.0, 12.0, 18.0] {
989 p.move_to((4.0, y));
990 p.line_to((20.0, y));
991 }
992 p
993}
994
995fn path_table() -> BezPath {
996 let mut p = BezPath::new();
997 // Marco.
998 p.move_to((4.0, 5.0));
999 p.line_to((20.0, 5.0));
1000 p.line_to((20.0, 19.0));
1001 p.line_to((4.0, 19.0));
1002 p.close_path();
1003 // Fila de encabezado.
1004 p.move_to((4.0, 9.5));
1005 p.line_to((20.0, 9.5));
1006 // Separador de columnas (debajo del encabezado).
1007 p.move_to((11.0, 9.5));
1008 p.line_to((11.0, 19.0));
1009 p
1010}
1011
1012fn path_columns() -> BezPath {
1013 let mut p = BezPath::new();
1014 // Dos paneles verticales lado a lado (modo dual).
1015 p.move_to((4.0, 4.0));
1016 p.line_to((11.0, 4.0));
1017 p.line_to((11.0, 20.0));
1018 p.line_to((4.0, 20.0));
1019 p.close_path();
1020 p.move_to((13.0, 4.0));
1021 p.line_to((20.0, 4.0));
1022 p.line_to((20.0, 20.0));
1023 p.line_to((13.0, 20.0));
1024 p.close_path();
1025 p
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030 use super::*;
1031
1032 #[test]
1033 fn all_icons_have_nonempty_path() {
1034 let all = [
1035 Icon::File, Icon::Folder, Icon::FolderOpen, Icon::Save, Icon::Open,
1036 Icon::Plus, Icon::Minus, Icon::X, Icon::Check, Icon::Edit, Icon::Trash,
1037 Icon::ChevronUp, Icon::ChevronDown, Icon::ChevronLeft, Icon::ChevronRight,
1038 Icon::Home, Icon::Search, Icon::Info, Icon::Warning, Icon::Error,
1039 Icon::Bell, Icon::Settings, Icon::More,
1040 Icon::Play, Icon::Pause, Icon::Stop, Icon::SkipBack, Icon::SkipForward,
1041 Icon::Rewind, Icon::FastForward, Icon::Volume, Icon::VolumeMute,
1042 Icon::Repeat, Icon::Shuffle, Icon::Record, Icon::Equalizer,
1043 Icon::Camera, Icon::Gauge,
1044 Icon::Image, Icon::Music, Icon::Film, Icon::Archive,
1045 Icon::Code, Icon::FileText, Icon::Link, Icon::Font,
1046 ];
1047 for icon in all {
1048 let p = icon.path();
1049 assert!(
1050 p.elements().len() > 0,
1051 "icono {} produjo path vacío",
1052 icon.name()
1053 );
1054 }
1055 }
1056
1057 #[test]
1058 fn icon_names_are_unique() {
1059 let all = [
1060 Icon::File, Icon::Folder, Icon::FolderOpen, Icon::Save, Icon::Open,
1061 Icon::Plus, Icon::Minus, Icon::X, Icon::Check, Icon::Edit, Icon::Trash,
1062 Icon::ChevronUp, Icon::ChevronDown, Icon::ChevronLeft, Icon::ChevronRight,
1063 Icon::Home, Icon::Search, Icon::Info, Icon::Warning, Icon::Error,
1064 Icon::Bell, Icon::Settings, Icon::More,
1065 Icon::Play, Icon::Pause, Icon::Stop, Icon::SkipBack, Icon::SkipForward,
1066 Icon::Rewind, Icon::FastForward, Icon::Volume, Icon::VolumeMute,
1067 Icon::Repeat, Icon::Shuffle, Icon::Record, Icon::Equalizer,
1068 Icon::Camera, Icon::Gauge,
1069 Icon::Image, Icon::Music, Icon::Film, Icon::Archive,
1070 Icon::Code, Icon::FileText, Icon::Link, Icon::Font,
1071 ];
1072 let mut names: Vec<&str> = all.iter().map(|i| i.name()).collect();
1073 let n = names.len();
1074 names.sort();
1075 names.dedup();
1076 assert_eq!(names.len(), n, "nombres duplicados en Icon::name()");
1077 }
1078}