Skip to main content

iced_cube/
icon.rs

1//! Lucide icons rendered through iced's `svg` widget.
2//!
3//! Build a [`Glyph`] with the [`lucide!`](crate::lucide) macro so it is
4//! resolved at compile time and only the icons you use end up in the binary.
5
6use iced::widget::{Svg, svg};
7use iced::{Color, Length, Theme};
8
9pub use lucide_static_svg::Icon;
10
11/// Embedded SVG source for one icon.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct Glyph {
14    name: &'static str,
15    svg: &'static str,
16}
17
18impl Glyph {
19    /// Resolves an icon. Call this in a `const` so unused icons are not linked.
20    pub const fn new(icon: Icon) -> Self {
21        Self {
22            name: icon.name(),
23            svg: icon.svg_str(),
24        }
25    }
26
27    /// Kebab-case Lucide name, such as `chevron-right`.
28    pub const fn name(self) -> &'static str {
29        self.name
30    }
31
32    /// Raw SVG markup.
33    pub const fn svg(self) -> &'static str {
34        self.svg
35    }
36}
37
38/// Resolves a Lucide icon to a [`Glyph`] at compile time.
39///
40/// ```
41/// let save = iced_cube::lucide!(Save);
42/// assert_eq!(save.name(), "save");
43/// ```
44#[macro_export]
45macro_rules! lucide {
46    ($name:ident) => {{
47        const GLYPH: $crate::icon::Glyph = $crate::icon::Glyph::new($crate::icon::Icon::$name);
48        GLYPH
49    }};
50}
51
52/// Opacity of an icon in a disabled component, matching its faded text.
53pub const DISABLED_OPACITY: f32 = 0.5;
54
55/// The opacity for an icon in an enabled or disabled component.
56pub const fn opacity(enabled: bool) -> f32 {
57    if enabled { 1.0 } else { DISABLED_OPACITY }
58}
59
60/// Renders a glyph at `size` pixels, tinted with the theme foreground.
61pub fn icon<'a>(glyph: Glyph, size: f32) -> Svg<'a, Theme> {
62    themed(glyph, size, 1.0, |theme| {
63        theme.extended_palette().background.base.text
64    })
65}
66
67/// Renders a glyph at `size` pixels in a fixed colour, or the theme
68/// foreground when `color` is `None`. The colour's alpha becomes the icon's
69/// opacity.
70pub fn tinted<'a>(glyph: Glyph, size: f32, color: Option<Color>) -> Svg<'a, Theme> {
71    match color {
72        Some(color) => themed(glyph, size, color.a, move |_| color),
73        None => icon(glyph, size),
74    }
75}
76
77/// Renders a glyph at `size` pixels, tinted with `color` resolved from the
78/// theme on every draw, and drawn at `opacity`.
79///
80/// iced ignores the alpha of an svg tint, so the tint is drawn opaque and
81/// any fading must come through `opacity`. Pass [`opacity`]`(enabled)` for a
82/// component that can be disabled rather than fading the colour.
83pub fn themed<'a>(
84    glyph: Glyph,
85    size: f32,
86    opacity: f32,
87    color: impl Fn(&Theme) -> Color + 'a,
88) -> Svg<'a, Theme> {
89    svg(svg::Handle::from_memory(glyph.svg().as_bytes()))
90        .width(Length::Fixed(size))
91        .height(Length::Fixed(size))
92        .opacity(opacity.clamp(0.0, 1.0))
93        .style(move |theme: &Theme, _status| svg::Style {
94            color: Some(opaque(color(theme))),
95        })
96}
97
98/// `color` with full alpha, which is how iced draws an svg tint anyway.
99pub const fn opaque(color: Color) -> Color {
100    Color { a: 1.0, ..color }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn disabled_icons_are_drawn_at_half_opacity() {
109        assert_eq!(opacity(true), 1.0);
110        assert_eq!(opacity(false), DISABLED_OPACITY);
111    }
112
113    #[test]
114    fn opaque_keeps_the_channels_and_drops_the_alpha() {
115        let colour = opaque(Color::from_rgba(0.1, 0.2, 0.3, 0.4));
116        assert_eq!(colour, Color::from_rgb(0.1, 0.2, 0.3));
117    }
118
119    #[test]
120    fn macro_resolves_name_and_markup() {
121        let glyph = crate::lucide!(ChevronRight);
122        assert_eq!(glyph.name(), "chevron-right");
123        assert!(glyph.svg().starts_with("<svg"));
124    }
125
126    #[test]
127    fn lucide_svgs_use_current_colour_so_tinting_works() {
128        assert!(crate::lucide!(Save).svg().contains("currentColor"));
129    }
130}