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