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 Svg(SharedString),
119 /// An image file located at the specified path.
120 ///
121 /// Currently our SVG renderer is missing support for the following features:
122 /// 1. Loading SVGs from external files.
123 /// 2. Rendering polychrome SVGs.
124 ///
125 /// In order to support icon themes, we render the icons as images instead.
126 Image(Arc<Path>),
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::Svg(path)
134 } else {
135 Self::Image(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::Svg(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 color(mut self, color: Color) -> Self {
168 self.color = color;
169 self
170 }
171
172 pub fn size(mut self, size: IconSize) -> Self {
173 self.size = size.rems();
174 self
175 }
176
177 /// Sets a custom size for the icon, in [`Rems`].
178 ///
179 /// Not to be exposed outside of the `ui` crate.
180 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
181 self.size = size;
182 self
183 }
184}
185
186impl Transformable for Icon {
187 fn transform(mut self, transformation: Transformation) -> Self {
188 self.transformation = transformation;
189 self
190 }
191}
192
193impl RenderOnce for Icon {
194 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
195 match self.source {
196 IconSource::Svg(path) => svg()
197 .with_transformation(self.transformation)
198 .size(self.size)
199 .flex_none()
200 .path(path)
201 .text_color(self.color.color(cx))
202 .into_any_element(),
203 IconSource::Image(path) => img(path)
204 .size(self.size)
205 .flex_none()
206 .text_color(self.color.color(cx))
207 .into_any_element(),
208 }
209 }
210}
211
212#[derive(IntoElement)]
213pub struct IconWithIndicator {
214 icon: Icon,
215 indicator: Option<Indicator>,
216 indicator_border_color: Option<Hsla>,
217}
218
219impl IconWithIndicator {
220 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
221 Self {
222 icon,
223 indicator,
224 indicator_border_color: None,
225 }
226 }
227
228 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
229 self.indicator = indicator;
230 self
231 }
232
233 pub fn indicator_color(mut self, color: Color) -> Self {
234 if let Some(indicator) = self.indicator.as_mut() {
235 indicator.color = color;
236 }
237 self
238 }
239
240 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
241 self.indicator_border_color = color;
242 self
243 }
244}
245
246impl RenderOnce for IconWithIndicator {
247 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
248 let indicator_border_color = self
249 .indicator_border_color
250 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
251
252 div()
253 .relative()
254 .child(self.icon)
255 .when_some(self.indicator, |this, indicator| {
256 this.child(
257 div()
258 .absolute()
259 .size_2p5()
260 .border_2()
261 .border_color(indicator_border_color)
262 .rounded_full()
263 .bottom_neg_0p5()
264 .right_neg_0p5()
265 .child(indicator),
266 )
267 })
268 }
269}
270
271impl Component for Icon {
272 fn scope() -> ComponentScope {
273 ComponentScope::Images
274 }
275
276 fn description() -> Option<&'static str> {
277 Some(
278 "A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.",
279 )
280 }
281
282 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
283 Some(
284 v_flex()
285 .gap_6()
286 .children(vec![
287 example_group_with_title(
288 "Sizes",
289 vec![
290 single_example("Default", Icon::new(IconName::Star).into_any_element()),
291 single_example(
292 "Small",
293 Icon::new(IconName::Star)
294 .size(IconSize::Small)
295 .into_any_element(),
296 ),
297 single_example(
298 "Large",
299 Icon::new(IconName::Star)
300 .size(IconSize::XLarge)
301 .into_any_element(),
302 ),
303 ],
304 ),
305 example_group_with_title(
306 "Colors",
307 vec![
308 single_example("Default", Icon::new(IconName::Bell).into_any_element()),
309 single_example(
310 "Custom Color",
311 Icon::new(IconName::Bell)
312 .color(Color::Error)
313 .into_any_element(),
314 ),
315 ],
316 ),
317 ])
318 .into_any_element(),
319 )
320 }
321}