tawasuyu

tawasuyu/llimphi

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

Rust 238TOML 102Markdown 83Shell 24178 Definiciones

⎇ dba855e446ca2bd4e7d146bdc168fb232cad15a4 llimphi-icons/src/lib.rs Rust · 1078 líneas · 30.7 KB Historia Quién tocó qué crudo

refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel ccab39f14 · Sergio ·

1//! `llimphi-icons` — set canónico de iconos vectoriales para apps tawasuyu.
2//!
3//! Cada icono es una función pura que devuelve un `BezPath` definido en
4//! un grid lógico de **24×24 unidades**. El renderer escala al rect que
5//! reciba, así un mismo icono sirve para 12px (en una fila de lista) y
6//! para 64px (en una hero card) sin pérdida de nitidez — es vector,
7//! no bitmap.
8//!
9//! ## Diseño
10//!
11//! - **Stroke-based, no fill**: los iconos son trazos de ancho 2 (en
12//!   unidades del grid) con joins/caps suaves. El stroke se renderiza
13//!   con el color que la app elija (típicamente `theme.fg_text` o
14//!   `theme.accent`).
15//! - **Geometría minimal, no marca**: glifos genéricos universales,
16//!   no "marca registrada". Cada uno debe ser reconocible al primer
17//!   vistazo aún en 12×12.
18//! - **Set acotado**: suficientes para cubrir el grueso de acciones y
19//!   tipos que aparecen en cualquier UI tawasuyu. Si una app necesita uno
20//!   más, lo agrega aquí (no en su propio crate) — la consistencia
21//!   visual importa más que el aislamiento.
22//!
23//! ## Catálogo
24//!
25//! | Categoría    | Iconos                                              |
26//! |--------------|-----------------------------------------------------|
27//! | Documento    | `file`, `folder`, `folder_open`, `save`, `open`     |
28//! | Edición      | `plus`, `minus`, `x`, `check`, `edit`, `trash`      |
29//! | Navegación   | `chevron_up`, `chevron_down`, `chevron_left`, `chevron_right`, `home`, `search` |
30//! | Estado       | `info`, `warning`, `error`, `bell`                  |
31//! | Sistema      | `settings`, `more`                                  |
32//! | Multimedia   | `play`, `pause`, `stop`, `skip_*`, `volume*`, `repeat`, `shuffle`, `record`, `equalizer`, `camera`, `gauge` |
33//! | Archivos     | `image`, `music`, `film`, `archive`, `code`, `file_text`, `link`, `font` |
34//!
35//! ## Uso
36//!
37//! ```ignore
38//! use llimphi_icons::{Icon, icon_view};
39//!
40//! // Botón con icono "save":
41//! let btn = View::new(style)
42//!     .fill(palette.bg_button)
43//!     .children(vec![icon_view(Icon::Save, palette.fg_text, 1.6)]);
44//! ```
45//!
46//! El parámetro `stroke_width` (3er arg de `icon_view`) está en unidades
47//! del grid (24×24). `1.6` es el default armonioso; `2.0` para énfasis;
48//! `1.2` para iconos en tipografías pequeñas.
49
50#![forbid(unsafe_code)]
51
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}