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