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}