Skip to main content

oxiui_theme/
icons.rs

1//! Icon theme trait and built-in icon set with hand-authored SVG path data.
2//!
3//! No SVG parsing dependency is used.  All path-data strings are `'static`
4//! byte string constants embedded at compile time.  Renderers receive the raw
5//! SVG `d`-attribute string and are responsible for stroking / filling it
6//! according to the requested variant.
7
8// ── Public types ───────────────────────────────────────────────────────────────
9
10/// Drawing variant for an icon.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum IconVariant {
13    /// Outline (stroke) style.
14    Outline,
15    /// Filled (filled-path) style.
16    Filled,
17    /// Rounded stroke style (may alias `Outline` in the built-in set).
18    Rounded,
19}
20
21/// Logical icon name.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum IconName {
24    /// Close / dismiss (X mark).
25    Close,
26    /// Hamburger menu (three horizontal lines).
27    Menu,
28    /// Arrow pointing right.
29    ArrowRight,
30    /// Arrow pointing left.
31    ArrowLeft,
32    /// Arrow pointing up.
33    ArrowUp,
34    /// Arrow pointing down.
35    ArrowDown,
36    /// Check / tick mark.
37    Check,
38    /// Magnifying-glass / search.
39    Search,
40}
41
42/// A source of SVG path-data strings for named icons.
43///
44/// Implementations return the SVG `d` attribute value for the given icon,
45/// variant, and size.  If a combination is unsupported, `None` is returned and
46/// callers should fall back to a default or skip rendering.
47pub trait IconSet: Send + Sync {
48    /// Return the SVG path-data string (the `d` attribute) for the icon.
49    ///
50    /// Returns `None` when the implementation does not support the requested
51    /// combination of icon, variant, and size.
52    fn path_data(&self, icon: IconName, variant: IconVariant, size: u32) -> Option<&'static str>;
53}
54
55// ── Built-in icon set ──────────────────────────────────────────────────────────
56
57/// Built-in icon set with hand-authored SVG path strings.
58///
59/// Supports sizes 16, 20, 24, 32.  `Rounded` aliases `Outline` for all icons.
60pub struct BuiltinIcons;
61
62impl BuiltinIcons {
63    /// Create a new instance of the built-in icon set.
64    pub fn new() -> Self {
65        Self
66    }
67}
68
69impl Default for BuiltinIcons {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl IconSet for BuiltinIcons {
76    fn path_data(&self, icon: IconName, variant: IconVariant, size: u32) -> Option<&'static str> {
77        match (icon, variant, size) {
78            // ── Close (X) ─────────────────────────────────────────────────────
79            (IconName::Close, IconVariant::Outline | IconVariant::Rounded, 16) => {
80                Some("M3 3L13 13M13 3L3 13")
81            }
82            (IconName::Close, IconVariant::Outline | IconVariant::Rounded, 20) => {
83                Some("M4 4L16 16M16 4L4 16")
84            }
85            (IconName::Close, IconVariant::Outline | IconVariant::Rounded, 24) => {
86                Some("M5 5L19 19M19 5L5 19")
87            }
88            (IconName::Close, IconVariant::Outline | IconVariant::Rounded, 32) => {
89                Some("M6 6L26 26M26 6L6 26")
90            }
91            (IconName::Close, IconVariant::Filled, 16) => Some("M3 3L13 13M13 3L3 13"),
92            (IconName::Close, IconVariant::Filled, 20) => Some("M4 4L16 16M16 4L4 16"),
93            (IconName::Close, IconVariant::Filled, 24) => Some("M5 5L19 19M19 5L5 19"),
94            (IconName::Close, IconVariant::Filled, 32) => Some("M6 6L26 26M26 6L6 26"),
95
96            // ── Menu (hamburger) ──────────────────────────────────────────────
97            (IconName::Menu, _, 16) => Some("M2 5H14M2 8H14M2 11H14"),
98            (IconName::Menu, _, 20) => Some("M3 6H17M3 10H17M3 14H17"),
99            (IconName::Menu, _, 24) => Some("M4 7H20M4 12H20M4 17H20"),
100            (IconName::Menu, _, 32) => Some("M5 9H27M5 16H27M5 23H27"),
101
102            // ── Arrow Right ───────────────────────────────────────────────────
103            (IconName::ArrowRight, IconVariant::Outline | IconVariant::Rounded, 16) => {
104                Some("M4 8H12M9 5L12 8L9 11")
105            }
106            (IconName::ArrowRight, _, 20) => Some("M5 10H15M11 6L15 10L11 14"),
107            (IconName::ArrowRight, _, 24) => Some("M6 12H18M13 7L18 12L13 17"),
108            (IconName::ArrowRight, _, 32) => Some("M8 16H24M17 9L24 16L17 23"),
109            (IconName::ArrowRight, IconVariant::Filled, 16) => Some("M4 8H12M9 5L12 8L9 11"),
110
111            // ── Arrow Left ────────────────────────────────────────────────────
112            (IconName::ArrowLeft, IconVariant::Outline | IconVariant::Rounded, 16) => {
113                Some("M12 8H4M7 5L4 8L7 11")
114            }
115            (IconName::ArrowLeft, _, 20) => Some("M15 10H5M9 6L5 10L9 14"),
116            (IconName::ArrowLeft, _, 24) => Some("M18 12H6M11 7L6 12L11 17"),
117            (IconName::ArrowLeft, _, 32) => Some("M24 16H8M15 9L8 16L15 23"),
118            (IconName::ArrowLeft, IconVariant::Filled, 16) => Some("M12 8H4M7 5L4 8L7 11"),
119
120            // ── Arrow Up ──────────────────────────────────────────────────────
121            (IconName::ArrowUp, _, 16) => Some("M8 12V4M5 7L8 4L11 7"),
122            (IconName::ArrowUp, _, 20) => Some("M10 15V5M6 9L10 5L14 9"),
123            (IconName::ArrowUp, _, 24) => Some("M12 19V5M7 10L12 5L17 10"),
124            (IconName::ArrowUp, _, 32) => Some("M16 26V6M9 13L16 6L23 13"),
125
126            // ── Arrow Down ────────────────────────────────────────────────────
127            (IconName::ArrowDown, _, 16) => Some("M8 4V12M5 9L8 12L11 9"),
128            (IconName::ArrowDown, _, 20) => Some("M10 5V15M6 11L10 15L14 11"),
129            (IconName::ArrowDown, _, 24) => Some("M12 5V19M7 14L12 19L17 14"),
130            (IconName::ArrowDown, _, 32) => Some("M16 6V26M9 19L16 26L23 19"),
131
132            // ── Check ─────────────────────────────────────────────────────────
133            (IconName::Check, _, 16) => Some("M2 8L6 12L14 4"),
134            (IconName::Check, _, 20) => Some("M3 10L8 15L17 5"),
135            (IconName::Check, _, 24) => Some("M4 12L9 18L20 6"),
136            (IconName::Check, _, 32) => Some("M5 16L12 24L27 8"),
137
138            // ── Search (magnifying glass) ─────────────────────────────────────
139            (IconName::Search, _, 16) => Some("M7 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0-8 0M11 11L14 14"),
140            (IconName::Search, _, 20) => {
141                Some("M9 9m-5 0a5 5 0 1 0 10 0a5 5 0 1 0-10 0M14 14L18 18")
142            }
143            (IconName::Search, _, 24) => {
144                Some("M11 11m-6 0a6 6 0 1 0 12 0a6 6 0 1 0-12 0M17 17L21 21")
145            }
146            (IconName::Search, _, 32) => {
147                Some("M14 14m-8 0a8 8 0 1 0 16 0a8 8 0 1 0-16 0M22 22L28 28")
148            }
149
150            _ => None,
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn close_all_sizes_outline() {
161        let icons = BuiltinIcons::new();
162        for size in [16u32, 20, 24, 32] {
163            assert!(
164                icons
165                    .path_data(IconName::Close, IconVariant::Outline, size)
166                    .is_some(),
167                "Close outline missing for size {size}"
168            );
169        }
170    }
171
172    #[test]
173    fn close_all_sizes_filled() {
174        let icons = BuiltinIcons::new();
175        for size in [16u32, 20, 24, 32] {
176            assert!(
177                icons
178                    .path_data(IconName::Close, IconVariant::Filled, size)
179                    .is_some(),
180                "Close filled missing for size {size}"
181            );
182        }
183    }
184
185    #[test]
186    fn all_icons_have_24px_outline() {
187        let icons = BuiltinIcons::new();
188        let all = [
189            IconName::Close,
190            IconName::Menu,
191            IconName::ArrowRight,
192            IconName::ArrowLeft,
193            IconName::ArrowUp,
194            IconName::ArrowDown,
195            IconName::Check,
196            IconName::Search,
197        ];
198        for icon in all {
199            assert!(
200                icons.path_data(icon, IconVariant::Outline, 24).is_some(),
201                "{icon:?} missing 24px outline"
202            );
203        }
204    }
205
206    #[test]
207    fn rounded_aliases_outline_for_close() {
208        let icons = BuiltinIcons::new();
209        let outline = icons.path_data(IconName::Close, IconVariant::Outline, 24);
210        let rounded = icons.path_data(IconName::Close, IconVariant::Rounded, 24);
211        assert_eq!(outline, rounded);
212    }
213
214    #[test]
215    fn unknown_size_returns_none() {
216        let icons = BuiltinIcons::new();
217        assert!(icons
218            .path_data(IconName::Close, IconVariant::Outline, 48)
219            .is_none());
220    }
221}