Skip to main content

gpui_component/
icon.rs

1use std::sync::Arc;
2
3use crate::{ActiveTheme, Sizable, Size};
4use gpui::{
5    AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Pixels, Radians, Render,
6    RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
7    prelude::FluentBuilder as _, svg,
8};
9pub use gpui_kit_assets::IconNamed;
10
11// Preserve the original enum (including exhaustive matches and inherent view)
12// while the complete, shared catalog is owned by gpui-kit-assets.
13macro_rules! component_icon_names {
14    ($($name:ident => $path:literal,)*) => {
15        /// Default component icon names, retained for source compatibility.
16        /// For the complete Lucide catalog, use `gpui_kit_assets::IconName`.
17        #[derive(Clone, IntoElement)]
18        pub enum IconName {
19            $($name,)*
20        }
21
22        impl From<IconName> for gpui_kit_assets::IconName {
23            fn from(name: IconName) -> Self {
24                match name {
25                    $(IconName::$name => Self::$name,)*
26                }
27            }
28        }
29
30        impl IconNamed for IconName {
31            fn path(self) -> SharedString {
32                match self { $(Self::$name => $path,)* }.into()
33            }
34        }
35    };
36}
37
38gpui_kit_assets::__component_icon_names!(component_icon_names);
39
40impl IconName {
41    /// Return the icon as an Entity<Icon>.
42    pub fn view(self, cx: &mut App) -> Entity<Icon> {
43        Icon::build(self).view(cx)
44    }
45}
46
47impl From<IconName> for AnyElement {
48    fn from(name: IconName) -> Self {
49        Icon::build(name).into_any_element()
50    }
51}
52
53impl RenderOnce for IconName {
54    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
55        Icon::build(self)
56    }
57}
58
59impl<T: IconNamed> From<T> for Icon {
60    fn from(value: T) -> Self {
61        Icon::build(value)
62    }
63}
64
65/// Component view construction for the shared `gpui_kit_assets::IconName`.
66/// The legacy component `IconName` retains its inherent `view` method.
67pub trait IconNameExt {
68    fn view(self, cx: &mut App) -> Entity<Icon>;
69}
70
71impl IconNameExt for gpui_kit_assets::IconName {
72    fn view(self, cx: &mut App) -> Entity<Icon> {
73        Icon::build(self).view(cx)
74    }
75}
76
77#[derive(Clone)]
78pub(crate) enum IconSource {
79    Path(SharedString),
80    Data(Arc<[u8]>),
81}
82
83#[derive(Clone, IntoElement)]
84pub struct Icon {
85    style: StyleRefinement,
86    source: IconSource,
87    text_color: Option<Hsla>,
88    size: Option<Size>,
89    transformation: Option<Transformation>,
90}
91
92impl Default for Icon {
93    fn default() -> Self {
94        Self {
95            style: StyleRefinement::default(),
96            source: IconSource::Path("".into()),
97            text_color: None,
98            size: None,
99            transformation: None,
100        }
101    }
102}
103
104impl Icon {
105    pub fn new(icon: impl Into<Icon>) -> Self {
106        icon.into()
107    }
108
109    fn build(name: impl IconNamed) -> Self {
110        Self::default().path(name.path())
111    }
112
113    /// Set the icon path of the Assets bundle
114    ///
115    /// For example: `icons/foo.svg`
116    /// Replaces any previously set path or SVG data.
117    pub fn path(mut self, path: impl Into<SharedString>) -> Self {
118        self.source = IconSource::Path(path.into());
119        self
120    }
121
122    /// Set raw SVG bytes without registering an asset path.
123    ///
124    /// Copies the bytes into shared storage; the input need not be static.
125    /// Cloning the icon shares those bytes. Replaces any previously set path or data.
126    /// Parsing and rendering follow GPUI's SVG behavior.
127    ///
128    /// ```
129    /// use gpui_component::Icon;
130    ///
131    /// let bytes = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
132    ///     <path d="M4 12h16" stroke="currentColor"/>
133    /// </svg>"#;
134    /// let icon = Icon::default().data(bytes);
135    /// ```
136    pub fn data(mut self, data: &[u8]) -> Self {
137        self.source = IconSource::Data(Arc::from(data));
138        self
139    }
140
141    #[cfg(any(target_os = "macos", target_os = "windows", test))]
142    pub(crate) fn source_ref(&self) -> &IconSource {
143        &self.source
144    }
145
146    /// Create a new view for the icon
147    pub fn view(self, cx: &mut App) -> Entity<Icon> {
148        cx.new(|_| self)
149    }
150
151    /// Set the SVG transformation, replacing any previous transformation or rotation.
152    pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
153        self.transformation = Some(transformation);
154        self
155    }
156
157    pub fn empty() -> Self {
158        Self::default()
159    }
160
161    /// Rotate the icon by the given angle
162    ///
163    /// Replaces any previous transformation or rotation.
164    pub fn rotate(mut self, radians: impl Into<Radians>) -> Self {
165        self.transformation = Some(Transformation::rotate(radians));
166        self
167    }
168
169    fn into_svg(self, text_size: Pixels, fallback_color: Hsla) -> Svg {
170        let text_color = self.text_color.unwrap_or(fallback_color);
171        let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
172
173        svg()
174            .map(|mut this| {
175                *this.style() = self.style;
176                this
177            })
178            .flex_shrink_0()
179            .text_color(text_color)
180            .when(!has_base_size, |this| this.size(text_size))
181            .when_some(self.size, |this, size| match size {
182                Size::Size(px) => this.size(px),
183                Size::XSmall => this.size_3(),
184                Size::Small => this.size_3p5(),
185                Size::Medium => this.size_4(),
186                Size::Large => this.size_6(),
187            })
188            .map(|this| match self.source {
189                IconSource::Path(path) => this.path(path),
190                IconSource::Data(data) => this.data(&data),
191            })
192            .when_some(self.transformation, |this, transformation| {
193                this.with_transformation(transformation)
194            })
195    }
196}
197
198impl Styled for Icon {
199    fn style(&mut self) -> &mut StyleRefinement {
200        &mut self.style
201    }
202
203    fn text_color(mut self, color: impl Into<Hsla>) -> Self {
204        self.text_color = Some(color.into());
205        self
206    }
207}
208
209impl Sizable for Icon {
210    fn with_size(mut self, size: impl Into<Size>) -> Self {
211        self.size = Some(size.into());
212        self
213    }
214}
215
216impl RenderOnce for Icon {
217    fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
218        let text_size = window.text_style().font_size.to_pixels(window.rem_size());
219        self.into_svg(text_size, window.text_style().color)
220    }
221}
222
223impl From<Icon> for AnyElement {
224    fn from(val: Icon) -> Self {
225        val.into_any_element()
226    }
227}
228
229impl Render for Icon {
230    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
231        let text_size = window.text_style().font_size.to_pixels(window.rem_size());
232        self.clone().into_svg(text_size, cx.theme().foreground)
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use gpui::{px, size};
240
241    const SVG: &[u8] = include_bytes!("../../assets/assets/icons/arrow-up.svg");
242
243    #[test]
244    fn test_icon_builder_preserves_owned_data_and_transform_on_clone() {
245        let transformation = Transformation::scale(size(0.5, 0.5))
246            .with_rotation(gpui::radians(std::f32::consts::FRAC_PI_2));
247        let icon = {
248            let bytes = SVG.to_vec();
249            Icon::default()
250                .data(&bytes)
251                .large()
252                .text_color(gpui::red())
253                .transform(transformation)
254        };
255        let cloned = icon.clone();
256        let (IconSource::Data(original), IconSource::Data(copy)) = (&icon.source, &cloned.source)
257        else {
258            panic!("cloning must preserve the data source");
259        };
260        assert_eq!(copy.as_ref(), SVG);
261        assert!(Arc::ptr_eq(original, copy));
262        assert_eq!(cloned.transformation, Some(transformation));
263        assert_eq!(cloned.size, icon.size);
264        assert_eq!(cloned.text_color, icon.text_color);
265
266        let mut svg = cloned.into_svg(px(12.), gpui::blue());
267        assert_eq!(svg.style().text.color, Some(gpui::red()));
268        assert_eq!(svg.style().size.width, Some(gpui::rems(1.5).into()));
269
270        let rotated = icon.rotate(gpui::radians(std::f32::consts::PI)).clone();
271        assert_eq!(
272            rotated.transformation,
273            Some(Transformation::rotate(gpui::radians(std::f32::consts::PI)))
274        );
275    }
276
277    #[test]
278    fn test_icon_source_builders_replace_previous_source() {
279        let icon = Icon::new(IconName::Search).data(SVG);
280        assert!(matches!(icon.source_ref(), IconSource::Data(bytes) if bytes.as_ref() == SVG));
281
282        let icon = icon.path("icons/replacement.svg");
283        assert!(
284            matches!(icon.source_ref(), IconSource::Path(path) if path == "icons/replacement.svg")
285        );
286
287        let icon = icon.data(SVG).data(b"replacement");
288        assert!(
289            matches!(icon.source_ref(), IconSource::Data(bytes) if bytes.as_ref() == b"replacement")
290        );
291
292        let icon = icon.path("");
293        assert!(matches!(icon.source_ref(), IconSource::Path(path) if path.is_empty()));
294    }
295}