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