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.
116#[derive(Clone)]
117enum IconSource {
118    /// An SVG embedded in the Zed binary.
119    Embedded(SharedString),
120    /// An image file located at the specified path.
121    ///
122    /// Currently our SVG renderer is missing support for rendering polychrome SVGs.
123    ///
124    /// In order to support icon themes, we render the icons as images instead.
125    External(Arc<Path>),
126    /// An SVG not embedded in the Zed binary.
127    ExternalSvg(SharedString),
128}
129
130#[derive(Clone, IntoElement, RegisterComponent)]
131pub struct Icon {
132    source: IconSource,
133    color: Color,
134    size: Rems,
135    transformation: Transformation,
136}
137
138impl Icon {
139    pub fn new(icon: IconName) -> Self {
140        Self {
141            source: IconSource::Embedded(icon.path().into()),
142            color: Color::default(),
143            size: IconSize::default().rems(),
144            transformation: Transformation::default(),
145        }
146    }
147
148    /// Create an icon from a path. Uses a heuristic to determine if it's embedded or external:
149    /// - Paths starting with "icons/" are treated as embedded SVGs
150    /// - Other paths are treated as external raster images (from icon themes)
151    pub fn from_path(path: impl Into<SharedString>) -> Self {
152        let path = path.into();
153        let source = if path.starts_with("icons/") {
154            IconSource::Embedded(path)
155        } else {
156            IconSource::External(Arc::from(PathBuf::from(path.as_ref())))
157        };
158        Self {
159            source,
160            color: Color::default(),
161            size: IconSize::default().rems(),
162            transformation: Transformation::default(),
163        }
164    }
165
166    pub fn from_external_svg(svg: SharedString) -> Self {
167        Self {
168            source: IconSource::ExternalSvg(svg),
169            color: Color::default(),
170            size: IconSize::default().rems(),
171            transformation: Transformation::default(),
172        }
173    }
174
175    pub fn color(mut self, color: Color) -> Self {
176        self.color = color;
177        self
178    }
179
180    pub fn size(mut self, size: IconSize) -> Self {
181        self.size = size.rems();
182        self
183    }
184
185    /// Sets a custom size for the icon, in [`Rems`].
186    ///
187    /// Not to be exposed outside of the `ui` crate.
188    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
189        self.size = size;
190        self
191    }
192}
193
194impl Transformable for Icon {
195    fn transform(mut self, transformation: Transformation) -> Self {
196        self.transformation = transformation;
197        self
198    }
199}
200
201impl RenderOnce for Icon {
202    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
203        match self.source {
204            IconSource::Embedded(path) => svg()
205                .with_transformation(self.transformation)
206                .size(self.size)
207                .flex_none()
208                .path(path)
209                .text_color(self.color.color(cx))
210                .into_any_element(),
211            IconSource::ExternalSvg(path) => svg()
212                .external_path(path)
213                .with_transformation(self.transformation)
214                .size(self.size)
215                .flex_none()
216                .text_color(self.color.color(cx))
217                .into_any_element(),
218            IconSource::External(path) => img(path)
219                .size(self.size)
220                .flex_none()
221                .text_color(self.color.color(cx))
222                .into_any_element(),
223        }
224    }
225}
226
227#[derive(IntoElement)]
228pub struct IconWithIndicator {
229    icon: Icon,
230    indicator: Option<Indicator>,
231    indicator_border_color: Option<Hsla>,
232}
233
234impl IconWithIndicator {
235    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
236        Self {
237            icon,
238            indicator,
239            indicator_border_color: None,
240        }
241    }
242
243    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
244        self.indicator = indicator;
245        self
246    }
247
248    pub fn indicator_color(mut self, color: Color) -> Self {
249        if let Some(indicator) = self.indicator.as_mut() {
250            indicator.color = color;
251        }
252        self
253    }
254
255    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
256        self.indicator_border_color = color;
257        self
258    }
259}
260
261impl RenderOnce for IconWithIndicator {
262    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
263        let indicator_border_color = self
264            .indicator_border_color
265            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
266
267        div()
268            .relative()
269            .child(self.icon)
270            .when_some(self.indicator, |this, indicator| {
271                this.child(
272                    div()
273                        .absolute()
274                        .size_2p5()
275                        .border_2()
276                        .border_color(indicator_border_color)
277                        .rounded_full()
278                        .bottom_neg_0p5()
279                        .right_neg_0p5()
280                        .child(indicator),
281                )
282            })
283    }
284}
285
286impl Component for Icon {
287    fn scope() -> ComponentScope {
288        ComponentScope::Images
289    }
290
291    fn description() -> Option<&'static str> {
292        Some(
293            "A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.",
294        )
295    }
296
297    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
298        Some(
299            v_flex()
300                .gap_6()
301                .children(vec![
302                    example_group_with_title(
303                        "Sizes",
304                        vec![single_example(
305                            "XSmall, Small, Default, Large",
306                            h_flex()
307                                .gap_1()
308                                .child(Icon::new(IconName::Star).size(IconSize::XSmall))
309                                .child(Icon::new(IconName::Star).size(IconSize::Small))
310                                .child(Icon::new(IconName::Star))
311                                .child(Icon::new(IconName::Star).size(IconSize::XLarge))
312                                .into_any_element(),
313                        )],
314                    ),
315                    example_group(vec![single_example(
316                        "All Icons",
317                        h_flex()
318                            .image_cache(gpui::retain_all("all icons"))
319                            .flex_wrap()
320                            .gap_2()
321                            .children(<IconName as strum::IntoEnumIterator>::iter().map(
322                                |icon_name: IconName| {
323                                    let name: SharedString = format!("{icon_name:?}").into();
324                                    v_flex()
325                                        .min_w_0()
326                                        .w_24()
327                                        .p_1p5()
328                                        .gap_2()
329                                        .border_1()
330                                        .border_color(cx.theme().colors().border_variant)
331                                        .bg(cx.theme().colors().element_disabled)
332                                        .rounded_sm()
333                                        .items_center()
334                                        .child(Icon::new(icon_name))
335                                        .child(Label::new(name).size(LabelSize::XSmall).truncate())
336                                },
337                            ))
338                            .into_any_element(),
339                    )]),
340                ])
341                .into_any_element(),
342        )
343    }
344}