Skip to main content

snora_core/
icon.rs

1//! Icon vocabulary.
2//!
3//! snora supports icons from multiple sources. Each source is gated behind
4//! a Cargo feature so that unused icon backends are eliminated at compile
5//! time (DCE) — no bundled asset blob you aren't using.
6//!
7//! | Source | Feature flag | Variant |
8//! |--------|--------------|---------|
9//! | Plain text (glyph / emoji) | always available | [`Icon::Text`] |
10//! | Lucide icon set | `lucide-icons` | [`Icon::Lucide`] |
11//! | Custom SVG file | `svg-icons` | [`Icon::Svg`] |
12//!
13//! When a feature is disabled, its variant does not exist in the enum at
14//! all — the compiler will refuse code that references it, so there are no
15//! runtime "unimplemented!" paths.
16//!
17//! # Fallback ergonomics
18//!
19//! `Icon` accepts `&str` / `String` conversions directly so that icon fields
20//! in user code can degrade gracefully even with all features disabled:
21//!
22//! ```rust
23//! # use snora_core::Icon;
24//! let icon: Icon = "★".into();        // always works
25//! let icon: Icon = String::from("★").into();
26//! ```
27
28/// An icon, with feature-gated source variants.
29///
30/// See the crate-level documentation and the
31/// [Icons guide](https://github.com/nabbisen/snora/blob/main/docs/guides/icons.md)
32/// for the full discussion of when to use each variant.
33///
34/// Implements [`PartialEq`] when all active features support it.
35/// `Icon::Text` and `Icon::Svg` are always comparable; `Icon::Lucide`
36/// requires `lucide-icons` to expose `PartialEq` on the inner type.
37/// If `lucide-icons` is enabled, a manual impl is used that compares the
38/// enum discriminant only for the `Lucide` variant.
39#[derive(Debug, Clone)]
40#[cfg_attr(not(feature = "lucide-icons"), derive(PartialEq))]
41pub enum Icon {
42    /// Renders the given string as text. The engine may choose its font
43    /// and size; a single-glyph string acts as a tiny glyph icon.
44    Text(String),
45
46    /// A built-in Lucide icon. Requires the `lucide-icons` feature.
47    #[cfg(feature = "lucide-icons")]
48    Lucide(lucide_icons::Icon),
49
50    /// A custom SVG file loaded from disk. Requires the `svg-icons`
51    /// feature. The engine is responsible for reading and rasterizing
52    /// the file.
53    #[cfg(feature = "svg-icons")]
54    Svg(std::path::PathBuf),
55}
56
57/// Manual `PartialEq` when `lucide-icons` is active, since
58/// `lucide_icons::Icon` does not derive `PartialEq` itself.
59/// Two `Lucide` variants are considered equal only when they hold the
60/// same discriminant value (compared via `as usize`).
61#[cfg(feature = "lucide-icons")]
62impl PartialEq for Icon {
63    fn eq(&self, other: &Self) -> bool {
64        match (self, other) {
65            (Icon::Text(a), Icon::Text(b)) => a == b,
66            (Icon::Lucide(a), Icon::Lucide(b)) => (*a as usize) == (*b as usize),
67            #[cfg(feature = "svg-icons")]
68            (Icon::Svg(a), Icon::Svg(b)) => a == b,
69            _ => false,
70        }
71    }
72}
73
74impl From<&str> for Icon {
75    fn from(s: &str) -> Self {
76        Icon::Text(s.to_string())
77    }
78}
79
80impl From<String> for Icon {
81    fn from(s: String) -> Self {
82        Icon::Text(s)
83    }
84}
85
86#[cfg(feature = "lucide-icons")]
87impl From<lucide_icons::Icon> for Icon {
88    fn from(i: lucide_icons::Icon) -> Self {
89        Icon::Lucide(i)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn text_icons_equal_when_same_string() {
99        let a: Icon = "★".into();
100        let b: Icon = "★".into();
101        assert_eq!(a, b);
102    }
103
104    #[test]
105    fn text_icons_not_equal_when_different_string() {
106        let a: Icon = "★".into();
107        let b: Icon = "☆".into();
108        assert_ne!(a, b);
109    }
110
111    #[test]
112    fn text_icon_from_string_vs_str() {
113        let a: Icon = "hello".into();
114        let b: Icon = String::from("hello").into();
115        assert_eq!(a, b);
116    }
117
118    #[cfg(feature = "svg-icons")]
119    #[test]
120    fn svg_icons_equal_when_same_path() {
121        let a = Icon::Svg(std::path::PathBuf::from("icons/foo.svg"));
122        let b = Icon::Svg(std::path::PathBuf::from("icons/foo.svg"));
123        assert_eq!(a, b);
124    }
125
126    #[cfg(feature = "svg-icons")]
127    #[test]
128    fn svg_icons_not_equal_when_different_path() {
129        let a = Icon::Svg(std::path::PathBuf::from("icons/foo.svg"));
130        let b = Icon::Svg(std::path::PathBuf::from("icons/bar.svg"));
131        assert_ne!(a, b);
132    }
133}