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