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::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
  9pub use icon_decoration::*;
 10pub use icons::*;
 11
 12use crate::{prelude::*, Indicator};
 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, IntoComponent)]
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
268// View this component preview using `workspace: open component-preview`
269impl ComponentPreview for Icon {
270    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
271        v_flex()
272            .gap_6()
273            .children(vec![
274                example_group_with_title(
275                    "Sizes",
276                    vec![
277                        single_example("Default", Icon::new(IconName::Star).into_any_element()),
278                        single_example(
279                            "Small",
280                            Icon::new(IconName::Star)
281                                .size(IconSize::Small)
282                                .into_any_element(),
283                        ),
284                        single_example(
285                            "Large",
286                            Icon::new(IconName::Star)
287                                .size(IconSize::XLarge)
288                                .into_any_element(),
289                        ),
290                    ],
291                ),
292                example_group_with_title(
293                    "Colors",
294                    vec![
295                        single_example("Default", Icon::new(IconName::Bell).into_any_element()),
296                        single_example(
297                            "Custom Color",
298                            Icon::new(IconName::Bell)
299                                .color(Color::Error)
300                                .into_any_element(),
301                        ),
302                    ],
303                ),
304            ])
305            .into_any_element()
306    }
307}