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}