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 an embedded SVG path (e.g., "icons/ai.svg").
148    /// These are SVGs bundled in the Zed binary.
149    pub fn from_embedded(path: impl Into<SharedString>) -> Self {
150        Self {
151            source: IconSource::Embedded(path.into()),
152            color: Color::default(),
153            size: IconSize::default().rems(),
154            transformation: Transformation::default(),
155        }
156    }
157
158    /// Create an icon from an external file path (e.g., from an extension).
159    /// This renders the file as a raster image.
160    pub fn from_path(path: impl Into<SharedString>) -> Self {
161        let path = path.into();
162        Self {
163            source: IconSource::External(Arc::from(PathBuf::from(path.as_ref()))),
164            color: Color::default(),
165            size: IconSize::default().rems(),
166            transformation: Transformation::default(),
167        }
168    }
169
170    pub fn from_external_svg(svg: SharedString) -> Self {
171        Self {
172            source: IconSource::ExternalSvg(svg),
173            color: Color::default(),
174            size: IconSize::default().rems(),
175            transformation: Transformation::default(),
176        }
177    }
178
179    pub fn color(mut self, color: Color) -> Self {
180        self.color = color;
181        self
182    }
183
184    pub fn size(mut self, size: IconSize) -> Self {
185        self.size = size.rems();
186        self
187    }
188
189    /// Sets a custom size for the icon, in [`Rems`].
190    ///
191    /// Not to be exposed outside of the `ui` crate.
192    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
193        self.size = size;
194        self
195    }
196}
197
198impl Transformable for Icon {
199    fn transform(mut self, transformation: Transformation) -> Self {
200        self.transformation = transformation;
201        self
202    }
203}
204
205impl RenderOnce for Icon {
206    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
207        match self.source {
208            IconSource::Embedded(path) => svg()
209                .with_transformation(self.transformation)
210                .size(self.size)
211                .flex_none()
212                .path(path)
213                .text_color(self.color.color(cx))
214                .into_any_element(),
215            IconSource::ExternalSvg(path) => svg()
216                .external_path(path)
217                .with_transformation(self.transformation)
218                .size(self.size)
219                .flex_none()
220                .text_color(self.color.color(cx))
221                .into_any_element(),
222            IconSource::External(path) => img(path)
223                .size(self.size)
224                .flex_none()
225                .text_color(self.color.color(cx))
226                .into_any_element(),
227        }
228    }
229}
230
231#[derive(IntoElement)]
232pub struct IconWithIndicator {
233    icon: Icon,
234    indicator: Option<Indicator>,
235    indicator_border_color: Option<Hsla>,
236}
237
238impl IconWithIndicator {
239    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
240        Self {
241            icon,
242            indicator,
243            indicator_border_color: None,
244        }
245    }
246
247    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
248        self.indicator = indicator;
249        self
250    }
251
252    pub fn indicator_color(mut self, color: Color) -> Self {
253        if let Some(indicator) = self.indicator.as_mut() {
254            indicator.color = color;
255        }
256        self
257    }
258
259    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
260        self.indicator_border_color = color;
261        self
262    }
263}
264
265impl RenderOnce for IconWithIndicator {
266    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
267        let indicator_border_color = self
268            .indicator_border_color
269            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
270
271        div()
272            .relative()
273            .child(self.icon)
274            .when_some(self.indicator, |this, indicator| {
275                this.child(
276                    div()
277                        .absolute()
278                        .size_2p5()
279                        .border_2()
280                        .border_color(indicator_border_color)
281                        .rounded_full()
282                        .bottom_neg_0p5()
283                        .right_neg_0p5()
284                        .child(indicator),
285                )
286            })
287    }
288}
289
290impl Component for Icon {
291    fn scope() -> ComponentScope {
292        ComponentScope::Images
293    }
294
295    fn description() -> Option<&'static str> {
296        Some(
297            "A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.",
298        )
299    }
300
301    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
302        Some(
303            v_flex()
304                .gap_6()
305                .children(vec![
306                    example_group_with_title(
307                        "Sizes",
308                        vec![single_example(
309                            "XSmall, Small, Default, Large",
310                            h_flex()
311                                .gap_1()
312                                .child(
313                                    Icon::new(IconName::Star)
314                                        .size(IconSize::XSmall)
315                                        .into_any_element(),
316                                )
317                                .child(
318                                    Icon::new(IconName::Star)
319                                        .size(IconSize::Small)
320                                        .into_any_element(),
321                                )
322                                .child(Icon::new(IconName::Star).into_any_element())
323                                .child(
324                                    Icon::new(IconName::Star)
325                                        .size(IconSize::XLarge)
326                                        .into_any_element(),
327                                )
328                                .into_any_element(),
329                        )],
330                    ),
331                    example_group_with_title(
332                        "Colors",
333                        vec![single_example(
334                            "Default & Custom",
335                            h_flex()
336                                .gap_1()
337                                .child(Icon::new(IconName::Star).into_any_element())
338                                .child(
339                                    Icon::new(IconName::Star)
340                                        .color(Color::Error)
341                                        .into_any_element(),
342                                )
343                                .into_any_element(),
344                        )],
345                    ),
346                    example_group_with_title(
347                        "All Icons",
348                        vec![single_example(
349                            "All Icons",
350                            h_flex()
351                                .image_cache(gpui::retain_all("all icons"))
352                                .flex_wrap()
353                                .gap_2()
354                                .children(<IconName as strum::IntoEnumIterator>::iter().map(
355                                    |icon_name| {
356                                        h_flex()
357                                            .p_1()
358                                            .gap_1()
359                                            .border_1()
360                                            .border_color(cx.theme().colors().border_variant)
361                                            .bg(cx.theme().colors().element_disabled)
362                                            .rounded_sm()
363                                            .child(Icon::new(icon_name).into_any_element())
364                                            .child(SharedString::new_static(icon_name.into()))
365                                    },
366                                ))
367                                .into_any_element(),
368                        )],
369                    ),
370                ])
371                .into_any_element(),
372        )
373    }
374}