Skip to main content

gpui_component/
icon.rs

1use crate::{ActiveTheme, Sizable, Size};
2use gpui::{
3    AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
4    SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
5    prelude::FluentBuilder as _, svg,
6};
7use gpui_component_macros::icon_named;
8
9/// Types implementing this trait can automatically be converted to [`Icon`].
10///
11/// This allows you to implement a custom version of [`IconName`] that functions as a drop-in
12/// replacement for other UI components.
13pub trait IconNamed {
14    /// Returns the embedded path of the icon.
15    fn path(self) -> SharedString;
16}
17
18impl<T: IconNamed> From<T> for Icon {
19    fn from(value: T) -> Self {
20        Icon::build(value)
21    }
22}
23
24// Generate `IconName` from the icons that `gpui-kit-assets` ships.
25// The `$VAR` form resolves to the absolute path published by the assets
26// crate's `build.rs` (via cargo's `links` mechanism) and re-exported by
27// our own `build.rs`. See `gpui_component_macros::icon_named!`'s doc
28// comment for the full mechanism.
29icon_named!(IconName, "$GPUI_KIT_DEFAULT_ICONS_DIR");
30
31impl IconName {
32    /// Return the icon as a Entity<Icon>
33    pub fn view(self, cx: &mut App) -> Entity<Icon> {
34        Icon::build(self).view(cx)
35    }
36}
37
38impl From<IconName> for AnyElement {
39    fn from(val: IconName) -> Self {
40        Icon::build(val).into_any_element()
41    }
42}
43
44impl RenderOnce for IconName {
45    fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement {
46        Icon::build(self)
47    }
48}
49
50#[derive(IntoElement)]
51pub struct Icon {
52    base: Svg,
53    style: StyleRefinement,
54    path: SharedString,
55    text_color: Option<Hsla>,
56    size: Option<Size>,
57    rotation: Option<Radians>,
58}
59
60impl Default for Icon {
61    fn default() -> Self {
62        Self {
63            base: svg().flex_none().size_4(),
64            style: StyleRefinement::default(),
65            path: "".into(),
66            text_color: None,
67            size: None,
68            rotation: None,
69        }
70    }
71}
72
73impl Clone for Icon {
74    fn clone(&self) -> Self {
75        let mut this = Self::default().path(self.path.clone());
76        this.style = self.style.clone();
77        this.rotation = self.rotation;
78        this.size = self.size;
79        this.text_color = self.text_color;
80        this
81    }
82}
83
84impl Icon {
85    pub fn new(icon: impl Into<Icon>) -> Self {
86        icon.into()
87    }
88
89    fn build(name: impl IconNamed) -> Self {
90        Self::default().path(name.path())
91    }
92
93    /// Set the icon path of the Assets bundle
94    ///
95    /// For example: `icons/foo.svg`
96    pub fn path(mut self, path: impl Into<SharedString>) -> Self {
97        self.path = path.into();
98        self
99    }
100
101    #[cfg(any(target_os = "macos", target_os = "windows", test))]
102    pub(crate) fn path_ref(&self) -> &SharedString {
103        &self.path
104    }
105
106    /// Create a new view for the icon
107    pub fn view(self, cx: &mut App) -> Entity<Icon> {
108        cx.new(|_| self)
109    }
110
111    pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
112        self.base = self.base.with_transformation(transformation);
113        self
114    }
115
116    pub fn empty() -> Self {
117        Self::default()
118    }
119
120    /// Rotate the icon by the given angle
121    pub fn rotate(mut self, radians: impl Into<Radians>) -> Self {
122        self.base = self
123            .base
124            .with_transformation(Transformation::rotate(radians));
125        self
126    }
127}
128
129impl Styled for Icon {
130    fn style(&mut self) -> &mut StyleRefinement {
131        &mut self.style
132    }
133
134    fn text_color(mut self, color: impl Into<Hsla>) -> Self {
135        self.text_color = Some(color.into());
136        self
137    }
138}
139
140impl Sizable for Icon {
141    fn with_size(mut self, size: impl Into<Size>) -> Self {
142        self.size = Some(size.into());
143        self
144    }
145}
146
147impl RenderOnce for Icon {
148    fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
149        let text_color = self.text_color.unwrap_or_else(|| window.text_style().color);
150        let text_size = window.text_style().font_size.to_pixels(window.rem_size());
151        let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
152
153        let mut base = self.base;
154        *base.style() = self.style;
155
156        base.flex_shrink_0()
157            .text_color(text_color)
158            .when(!has_base_size, |this| this.size(text_size))
159            .when_some(self.size, |this, size| match size {
160                Size::Size(px) => this.size(px),
161                Size::XSmall => this.size_3(),
162                Size::Small => this.size_3p5(),
163                Size::Medium => this.size_4(),
164                Size::Large => this.size_6(),
165            })
166            .path(self.path)
167    }
168}
169
170impl From<Icon> for AnyElement {
171    fn from(val: Icon) -> Self {
172        val.into_any_element()
173    }
174}
175
176impl Render for Icon {
177    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
178        let text_color = self.text_color.unwrap_or_else(|| cx.theme().foreground);
179        let text_size = window.text_style().font_size.to_pixels(window.rem_size());
180        let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
181
182        let mut base = svg().flex_none();
183        *base.style() = self.style.clone();
184
185        base.flex_shrink_0()
186            .text_color(text_color)
187            .when(!has_base_size, |this| this.size(text_size))
188            .when_some(self.size, |this, size| match size {
189                Size::Size(px) => this.size(px),
190                Size::XSmall => this.size_3(),
191                Size::Small => this.size_3p5(),
192                Size::Medium => this.size_4(),
193                Size::Large => this.size_6(),
194            })
195            .path(self.path.clone())
196            .when_some(self.rotation, |this, rotation| {
197                this.with_transformation(Transformation::rotate(rotation))
198            })
199    }
200}