Skip to main content

guise/icon/
mod.rs

1//! `Icon` — a themed glyph drawn from the embedded [Lucide](https://lucide.dev)
2//! icon font. Lucide is the default icon set for guise: every icon in the set
3//! is an [`IconName`] variant, and any component that accepts a [`Glyph`] takes
4//! an `IconName` directly.
5//!
6//! The font ships inside the crate and registers itself with gpui's text
7//! system on first render — no asset pipeline or app setup required. Icons
8//! inherit the surrounding text color by default (pass [`Icon::color`] to
9//! tint).
10
11mod lucide;
12
13pub use lucide::{IconName, LUCIDE_VERSION};
14
15use std::borrow::Cow;
16use std::sync::atomic::{AtomicBool, Ordering};
17
18use gpui::prelude::*;
19use gpui::{div, px, App, IntoElement, SharedString, Window};
20
21use crate::devtools::Probed;
22use crate::theme::{theme, ColorName, Size};
23
24/// The family name baked into the embedded Lucide font.
25pub(crate) const FONT_FAMILY: &str = "lucide";
26
27static FONT_BYTES: &[u8] = include_bytes!("../../assets/lucide/lucide.ttf");
28static FONT_REGISTERED: AtomicBool = AtomicBool::new(false);
29
30/// Register the embedded Lucide font with gpui's text system. Idempotent and
31/// cheap after the first call; every glyph-drawing render path goes through it.
32pub(crate) fn ensure_font(cx: &App) {
33    if !FONT_REGISTERED.swap(true, Ordering::Relaxed) {
34        cx.text_system()
35            .add_fonts(vec![Cow::Borrowed(FONT_BYTES)])
36            .expect("failed to register the embedded Lucide icon font");
37    }
38}
39
40/// Icon content for components with an icon slot: a Lucide icon or a short
41/// piece of text (an emoji, "+", "</>", …). Both render inline and inherit
42/// the surrounding text size and color.
43#[derive(Debug, Clone, IntoElement)]
44pub enum Glyph {
45    Lucide(IconName),
46    Text(SharedString),
47}
48
49impl From<IconName> for Glyph {
50    fn from(name: IconName) -> Self {
51        Glyph::Lucide(name)
52    }
53}
54
55impl From<&'static str> for Glyph {
56    fn from(text: &'static str) -> Self {
57        Glyph::Text(SharedString::new_static(text))
58    }
59}
60
61impl From<String> for Glyph {
62    fn from(text: String) -> Self {
63        Glyph::Text(text.into())
64    }
65}
66
67impl From<SharedString> for Glyph {
68    fn from(text: SharedString) -> Self {
69        Glyph::Text(text)
70    }
71}
72
73impl RenderOnce for Glyph {
74    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
75        let element = match self {
76            Glyph::Lucide(name) => {
77                ensure_font(cx);
78                div()
79                    .font_family(FONT_FAMILY)
80                    .child(SharedString::new_static(name.glyph()))
81            }
82            Glyph::Text(text) => div().child(text),
83        };
84
85        element.probe("Glyph")
86    }
87}
88
89/// A themed icon glyph. Inherits the parent's text color unless [`color`] is
90/// set.
91///
92/// [`color`]: Icon::color
93#[derive(IntoElement)]
94pub struct Icon {
95    name: IconName,
96    size: Size,
97    color: Option<ColorName>,
98}
99
100impl Icon {
101    pub fn new(name: IconName) -> Self {
102        Icon {
103            name,
104            size: Size::Md,
105            color: None,
106        }
107    }
108
109    pub fn size(mut self, size: Size) -> Self {
110        self.size = size;
111        self
112    }
113
114    /// Tint the glyph with a palette color (defaults to inheriting text color).
115    pub fn color(mut self, color: ColorName) -> Self {
116        self.color = Some(color);
117        self
118    }
119
120    fn glyph_px(&self) -> f32 {
121        match self.size {
122            Size::Xs => 14.0,
123            Size::Sm => 16.0,
124            Size::Md => 20.0,
125            Size::Lg => 26.0,
126            Size::Xl => 32.0,
127        }
128    }
129}
130
131impl RenderOnce for Icon {
132    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
133        ensure_font(cx);
134        let t = theme(cx);
135        let tint = self.color.map(|c| t.color(c, t.primary_shade()).hsla());
136        let size = px(self.glyph_px());
137        let mut el = div()
138            .flex()
139            .items_center()
140            .justify_center()
141            .font_family(FONT_FAMILY)
142            .text_size(size)
143            .line_height(size)
144            .child(SharedString::new_static(self.name.glyph()));
145        if let Some(tint) = tint {
146            el = el.text_color(tint);
147        }
148        el.probe("Icon")
149            .attr("name", self.name.name())
150            .attr("size", self.size.label())
151    }
152}