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