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    Svg(SharedString),
119    /// An image file located at the specified path.
120    ///
121    /// Currently our SVG renderer is missing support for the following features:
122    /// 1. Loading SVGs from external files.
123    /// 2. Rendering polychrome SVGs.
124    ///
125    /// In order to support icon themes, we render the icons as images instead.
126    Image(Arc<Path>),
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::Svg(path)
134        } else {
135            Self::Image(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::Svg(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 const fn color(mut self, color: Color) -> Self {
168        self.color = color;
169        self
170    }
171
172    pub fn size(mut self, size: IconSize) -> Self {
173        self.size = size.rems();
174        self
175    }
176
177    /// Sets a custom size for the icon, in [`Rems`].
178    ///
179    /// Not to be exposed outside of the `ui` crate.
180    pub(crate) const fn custom_size(mut self, size: Rems) -> Self {
181        self.size = size;
182        self
183    }
184}
185
186impl Transformable for Icon {
187    fn transform(mut self, transformation: Transformation) -> Self {
188        self.transformation = transformation;
189        self
190    }
191}
192
193impl RenderOnce for Icon {
194    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
195        match self.source {
196            IconSource::Svg(path) => svg()
197                .with_transformation(self.transformation)
198                .size(self.size)
199                .flex_none()
200                .path(path)
201                .text_color(self.color.color(cx))
202                .into_any_element(),
203            IconSource::Image(path) => img(path)
204                .size(self.size)
205                .flex_none()
206                .text_color(self.color.color(cx))
207                .into_any_element(),
208        }
209    }
210}
211
212#[derive(IntoElement)]
213pub struct IconWithIndicator {
214    icon: Icon,
215    indicator: Option<Indicator>,
216    indicator_border_color: Option<Hsla>,
217}
218
219impl IconWithIndicator {
220    pub const fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
221        Self {
222            icon,
223            indicator,
224            indicator_border_color: None,
225        }
226    }
227
228    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
229        self.indicator = indicator;
230        self
231    }
232
233    pub const fn indicator_color(mut self, color: Color) -> Self {
234        if let Some(indicator) = self.indicator.as_mut() {
235            indicator.color = color;
236        }
237        self
238    }
239
240    pub const fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
241        self.indicator_border_color = color;
242        self
243    }
244}
245
246impl RenderOnce for IconWithIndicator {
247    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
248        let indicator_border_color = self
249            .indicator_border_color
250            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
251
252        div()
253            .relative()
254            .child(self.icon)
255            .when_some(self.indicator, |this, indicator| {
256                this.child(
257                    div()
258                        .absolute()
259                        .size_2p5()
260                        .border_2()
261                        .border_color(indicator_border_color)
262                        .rounded_full()
263                        .bottom_neg_0p5()
264                        .right_neg_0p5()
265                        .child(indicator),
266                )
267            })
268    }
269}
270
271impl Component for Icon {
272    fn scope() -> ComponentScope {
273        ComponentScope::Images
274    }
275
276    fn description() -> Option<&'static str> {
277        Some(
278            "A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.",
279        )
280    }
281
282    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
283        Some(
284            v_flex()
285                .gap_6()
286                .children(vec![
287                    example_group_with_title(
288                        "Sizes",
289                        vec![
290                            single_example("Default", Icon::new(IconName::Star).into_any_element()),
291                            single_example(
292                                "Small",
293                                Icon::new(IconName::Star)
294                                    .size(IconSize::Small)
295                                    .into_any_element(),
296                            ),
297                            single_example(
298                                "Large",
299                                Icon::new(IconName::Star)
300                                    .size(IconSize::XLarge)
301                                    .into_any_element(),
302                            ),
303                        ],
304                    ),
305                    example_group_with_title(
306                        "Colors",
307                        vec![
308                            single_example("Default", Icon::new(IconName::Bell).into_any_element()),
309                            single_example(
310                                "Custom Color",
311                                Icon::new(IconName::Bell)
312                                    .color(Color::Error)
313                                    .into_any_element(),
314                            ),
315                        ],
316                    ),
317                    example_group_with_title(
318                        "All Icons",
319                        vec![single_example(
320                            "All Icons",
321                            h_flex()
322                                .image_cache(gpui::retain_all("all icons"))
323                                .flex_wrap()
324                                .gap_2()
325                                .children(<IconName as strum::IntoEnumIterator>::iter().map(
326                                    |icon_name| {
327                                        h_flex()
328                                            .gap_1()
329                                            .border_1()
330                                            .rounded_md()
331                                            .px_2()
332                                            .py_1()
333                                            .border_color(Color::Muted.color(cx))
334                                            .child(SharedString::new_static(icon_name.into()))
335                                            .child(Icon::new(icon_name).into_any_element())
336                                    },
337                                ))
338                                .into_any_element(),
339                        )],
340                    ),
341                ])
342                .into_any_element(),
343        )
344    }
345}