icon.rs

  1mod decorated_icon;
  2mod icon_decoration;
  3
  4use std::path::{Path, PathBuf};
  5use std::sync::Arc;
  6
  7pub use decorated_icon::*;
  8use gpui::{AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation, img, svg};
  9pub use icon_decoration::*;
 10pub use icons::*;
 11
 12use crate::traits::transformable::Transformable;
 13use crate::{Indicator, prelude::*};
 14
 15#[derive(IntoElement)]
 16pub enum AnyIcon {
 17    Icon(Icon),
 18    AnimatedIcon(AnimationElement<Icon>),
 19}
 20
 21impl AnyIcon {
 22    /// Returns a new [`AnyIcon`] after applying the given mapping function
 23    /// to the contained [`Icon`].
 24    pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
 25        match self {
 26            Self::Icon(icon) => Self::Icon(f(icon)),
 27            Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
 28        }
 29    }
 30}
 31
 32impl From<Icon> for AnyIcon {
 33    fn from(value: Icon) -> Self {
 34        Self::Icon(value)
 35    }
 36}
 37
 38impl From<AnimationElement<Icon>> for AnyIcon {
 39    fn from(value: AnimationElement<Icon>) -> Self {
 40        Self::AnimatedIcon(value)
 41    }
 42}
 43
 44impl RenderOnce for AnyIcon {
 45    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
 46        match self {
 47            Self::Icon(icon) => icon.into_any_element(),
 48            Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
 49        }
 50    }
 51}
 52
 53#[derive(Default, PartialEq, Copy, Clone)]
 54pub enum IconSize {
 55    /// 10px
 56    Indicator,
 57    /// 12px
 58    XSmall,
 59    /// 14px
 60    Small,
 61    #[default]
 62    /// 16px
 63    Medium,
 64    /// 48px
 65    XLarge,
 66    Custom(Rems),
 67}
 68
 69impl IconSize {
 70    pub fn rems(self) -> Rems {
 71        match self {
 72            IconSize::Indicator => rems_from_px(10.),
 73            IconSize::XSmall => rems_from_px(12.),
 74            IconSize::Small => rems_from_px(14.),
 75            IconSize::Medium => rems_from_px(16.),
 76            IconSize::XLarge => rems_from_px(48.),
 77            IconSize::Custom(size) => size,
 78        }
 79    }
 80
 81    /// Returns the individual components of the square that contains this [`IconSize`].
 82    ///
 83    /// The returned tuple contains:
 84    ///   1. The length of one side of the square
 85    ///   2. The padding of one side of the square
 86    pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
 87        let icon_size = self.rems() * window.rem_size();
 88        let padding = match self {
 89            IconSize::Indicator => DynamicSpacing::Base00.px(cx),
 90            IconSize::XSmall => DynamicSpacing::Base02.px(cx),
 91            IconSize::Small => DynamicSpacing::Base02.px(cx),
 92            IconSize::Medium => DynamicSpacing::Base02.px(cx),
 93            IconSize::XLarge => DynamicSpacing::Base02.px(cx),
 94            // TODO: Wire into dynamic spacing
 95            IconSize::Custom(size) => size.to_pixels(window.rem_size()),
 96        };
 97
 98        (icon_size, padding)
 99    }
100
101    /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
102    pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels {
103        let (icon_size, padding) = self.square_components(window, cx);
104
105        icon_size + padding * 2.
106    }
107}
108
109impl From<IconName> for Icon {
110    fn from(icon: IconName) -> Self {
111        Icon::new(icon)
112    }
113}
114
115/// The source of an icon.
116enum IconSource {
117    /// An SVG embedded in the Zed binary.
118    Embedded(SharedString),
119    /// An image file located at the specified path.
120    ///
121    /// Currently our SVG renderer is missing support for rendering polychrome SVGs.
122    ///
123    /// In order to support icon themes, we render the icons as images instead.
124    External(Arc<Path>),
125    /// An SVG not embedded in the Zed binary.
126    ExternalSvg(SharedString),
127}
128
129#[derive(IntoElement, RegisterComponent)]
130pub struct Icon {
131    source: IconSource,
132    color: Color,
133    size: Rems,
134    transformation: Transformation,
135}
136
137impl Icon {
138    pub fn new(icon: IconName) -> Self {
139        Self {
140            source: IconSource::Embedded(icon.path().into()),
141            color: Color::default(),
142            size: IconSize::default().rems(),
143            transformation: Transformation::default(),
144        }
145    }
146
147    /// Create an icon from a path. Uses a heuristic to determine if it's embedded or external:
148    /// - Paths starting with "icons/" are treated as embedded SVGs
149    /// - Other paths are treated as external raster images (from icon themes)
150    pub fn from_path(path: impl Into<SharedString>) -> Self {
151        let path = path.into();
152        let source = if path.starts_with("icons/") {
153            IconSource::Embedded(path)
154        } else {
155            IconSource::External(Arc::from(PathBuf::from(path.as_ref())))
156        };
157        Self {
158            source,
159            color: Color::default(),
160            size: IconSize::default().rems(),
161            transformation: Transformation::default(),
162        }
163    }
164
165    pub fn from_external_svg(svg: SharedString) -> Self {
166        Self {
167            source: IconSource::ExternalSvg(svg),
168            color: Color::default(),
169            size: IconSize::default().rems(),
170            transformation: Transformation::default(),
171        }
172    }
173
174    pub fn color(mut self, color: Color) -> Self {
175        self.color = color;
176        self
177    }
178
179    pub fn size(mut self, size: IconSize) -> Self {
180        self.size = size.rems();
181        self
182    }
183
184    /// Sets a custom size for the icon, in [`Rems`].
185    ///
186    /// Not to be exposed outside of the `ui` crate.
187    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
188        self.size = size;
189        self
190    }
191}
192
193impl Transformable for Icon {
194    fn transform(mut self, transformation: Transformation) -> Self {
195        self.transformation = transformation;
196        self
197    }
198}
199
200impl RenderOnce for Icon {
201    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
202        match self.source {
203            IconSource::Embedded(path) => svg()
204                .with_transformation(self.transformation)
205                .size(self.size)
206                .flex_none()
207                .path(path)
208                .text_color(self.color.color(cx))
209                .into_any_element(),
210            IconSource::ExternalSvg(path) => svg()
211                .external_path(path)
212                .with_transformation(self.transformation)
213                .size(self.size)
214                .flex_none()
215                .text_color(self.color.color(cx))
216                .into_any_element(),
217            IconSource::External(path) => img(path)
218                .size(self.size)
219                .flex_none()
220                .text_color(self.color.color(cx))
221                .into_any_element(),
222        }
223    }
224}
225
226#[derive(IntoElement)]
227pub struct IconWithIndicator {
228    icon: Icon,
229    indicator: Option<Indicator>,
230    indicator_border_color: Option<Hsla>,
231}
232
233impl IconWithIndicator {
234    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
235        Self {
236            icon,
237            indicator,
238            indicator_border_color: None,
239        }
240    }
241
242    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
243        self.indicator = indicator;
244        self
245    }
246
247    pub fn indicator_color(mut self, color: Color) -> Self {
248        if let Some(indicator) = self.indicator.as_mut() {
249            indicator.color = color;
250        }
251        self
252    }
253
254    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
255        self.indicator_border_color = color;
256        self
257    }
258}
259
260impl RenderOnce for IconWithIndicator {
261    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
262        let indicator_border_color = self
263            .indicator_border_color
264            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
265
266        div()
267            .relative()
268            .child(self.icon)
269            .when_some(self.indicator, |this, indicator| {
270                this.child(
271                    div()
272                        .absolute()
273                        .size_2p5()
274                        .border_2()
275                        .border_color(indicator_border_color)
276                        .rounded_full()
277                        .bottom_neg_0p5()
278                        .right_neg_0p5()
279                        .child(indicator),
280                )
281            })
282    }
283}
284
285impl Component for Icon {
286    fn scope() -> ComponentScope {
287        ComponentScope::Images
288    }
289
290    fn description() -> Option<&'static str> {
291        Some(
292            "A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.",
293        )
294    }
295
296    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
297        Some(
298            v_flex()
299                .gap_6()
300                .children(vec![
301                    example_group_with_title(
302                        "Sizes",
303                        vec![single_example(
304                            "XSmall, Small, Default, Large",
305                            h_flex()
306                                .gap_1()
307                                .child(
308                                    Icon::new(IconName::Star)
309                                        .size(IconSize::XSmall)
310                                        .into_any_element(),
311                                )
312                                .child(
313                                    Icon::new(IconName::Star)
314                                        .size(IconSize::Small)
315                                        .into_any_element(),
316                                )
317                                .child(Icon::new(IconName::Star).into_any_element())
318                                .child(
319                                    Icon::new(IconName::Star)
320                                        .size(IconSize::XLarge)
321                                        .into_any_element(),
322                                )
323                                .into_any_element(),
324                        )],
325                    ),
326                    example_group_with_title(
327                        "Colors",
328                        vec![single_example(
329                            "Default & Custom",
330                            h_flex()
331                                .gap_1()
332                                .child(Icon::new(IconName::Star).into_any_element())
333                                .child(
334                                    Icon::new(IconName::Star)
335                                        .color(Color::Error)
336                                        .into_any_element(),
337                                )
338                                .into_any_element(),
339                        )],
340                    ),
341                    example_group_with_title(
342                        "All Icons",
343                        vec![single_example(
344                            "All Icons",
345                            h_flex()
346                                .image_cache(gpui::retain_all("all icons"))
347                                .flex_wrap()
348                                .gap_2()
349                                .children(<IconName as strum::IntoEnumIterator>::iter().map(
350                                    |icon_name| {
351                                        h_flex()
352                                            .p_1()
353                                            .gap_1()
354                                            .border_1()
355                                            .border_color(cx.theme().colors().border_variant)
356                                            .bg(cx.theme().colors().element_disabled)
357                                            .rounded_sm()
358                                            .child(Icon::new(icon_name).into_any_element())
359                                            .child(SharedString::new_static(icon_name.into()))
360                                    },
361                                ))
362                                .into_any_element(),
363                        )],
364                    ),
365                ])
366                .into_any_element(),
367        )
368    }
369}