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
89 let padding = match self {
90 IconSize::Indicator => DynamicSpacing::Base00.px(cx),
91 IconSize::XSmall => DynamicSpacing::Base02.px(cx),
92 IconSize::Small => DynamicSpacing::Base04.px(cx),
93 IconSize::Medium => DynamicSpacing::Base06.px(cx),
94 IconSize::XLarge => DynamicSpacing::Base02.px(cx),
95 // TODO: Wire into dynamic spacing
96 IconSize::Custom(size) => size.to_pixels(window.rem_size()),
97 };
98
99 (icon_size, padding)
100 }
101
102 /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
103 pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels {
104 let (icon_size, padding) = self.square_components(window, cx);
105
106 icon_size + padding * 2.
107 }
108}
109
110impl From<IconName> for Icon {
111 fn from(icon: IconName) -> Self {
112 Icon::new(icon)
113 }
114}
115
116/// The source of an icon.
117enum IconSource {
118 /// An SVG embedded in the Zed binary.
119 Svg(SharedString),
120 /// An image file located at the specified path.
121 ///
122 /// Currently our SVG renderer is missing support for the following features:
123 /// 1. Loading SVGs from external files.
124 /// 2. Rendering polychrome SVGs.
125 ///
126 /// In order to support icon themes, we render the icons as images instead.
127 Image(Arc<Path>),
128}
129
130impl IconSource {
131 fn from_path(path: impl Into<SharedString>) -> Self {
132 let path = path.into();
133 if path.starts_with("icons/") {
134 Self::Svg(path)
135 } else {
136 Self::Image(Arc::from(PathBuf::from(path.as_ref())))
137 }
138 }
139}
140
141#[derive(IntoElement, RegisterComponent)]
142pub struct Icon {
143 source: IconSource,
144 color: Color,
145 size: Rems,
146 transformation: Transformation,
147}
148
149impl Icon {
150 pub fn new(icon: IconName) -> Self {
151 Self {
152 source: IconSource::Svg(icon.path().into()),
153 color: Color::default(),
154 size: IconSize::default().rems(),
155 transformation: Transformation::default(),
156 }
157 }
158
159 pub fn from_path(path: impl Into<SharedString>) -> Self {
160 Self {
161 source: IconSource::from_path(path),
162 color: Color::default(),
163 size: IconSize::default().rems(),
164 transformation: Transformation::default(),
165 }
166 }
167
168 pub fn color(mut self, color: Color) -> Self {
169 self.color = color;
170 self
171 }
172
173 pub fn size(mut self, size: IconSize) -> Self {
174 self.size = size.rems();
175 self
176 }
177
178 /// Sets a custom size for the icon, in [`Rems`].
179 ///
180 /// Not to be exposed outside of the `ui` crate.
181 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
182 self.size = size;
183 self
184 }
185}
186
187impl Transformable for Icon {
188 fn transform(mut self, transformation: Transformation) -> Self {
189 self.transformation = transformation;
190 self
191 }
192}
193
194impl RenderOnce for Icon {
195 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
196 match self.source {
197 IconSource::Svg(path) => svg()
198 .with_transformation(self.transformation)
199 .size(self.size)
200 .flex_none()
201 .path(path)
202 .text_color(self.color.color(cx))
203 .into_any_element(),
204 IconSource::Image(path) => img(path)
205 .size(self.size)
206 .flex_none()
207 .text_color(self.color.color(cx))
208 .into_any_element(),
209 }
210 }
211}
212
213#[derive(IntoElement)]
214pub struct IconWithIndicator {
215 icon: Icon,
216 indicator: Option<Indicator>,
217 indicator_border_color: Option<Hsla>,
218}
219
220impl IconWithIndicator {
221 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
222 Self {
223 icon,
224 indicator,
225 indicator_border_color: None,
226 }
227 }
228
229 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
230 self.indicator = indicator;
231 self
232 }
233
234 pub fn indicator_color(mut self, color: Color) -> Self {
235 if let Some(indicator) = self.indicator.as_mut() {
236 indicator.color = color;
237 }
238 self
239 }
240
241 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
242 self.indicator_border_color = color;
243 self
244 }
245}
246
247impl RenderOnce for IconWithIndicator {
248 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
249 let indicator_border_color = self
250 .indicator_border_color
251 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
252
253 div()
254 .relative()
255 .child(self.icon)
256 .when_some(self.indicator, |this, indicator| {
257 this.child(
258 div()
259 .absolute()
260 .size_2p5()
261 .border_2()
262 .border_color(indicator_border_color)
263 .rounded_full()
264 .bottom_neg_0p5()
265 .right_neg_0p5()
266 .child(indicator),
267 )
268 })
269 }
270}
271
272impl Component for Icon {
273 fn scope() -> ComponentScope {
274 ComponentScope::Images
275 }
276
277 fn description() -> Option<&'static str> {
278 Some(
279 "A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.",
280 )
281 }
282
283 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
284 Some(
285 v_flex()
286 .gap_6()
287 .children(vec![
288 example_group_with_title(
289 "Sizes",
290 vec![
291 single_example("Default", Icon::new(IconName::Star).into_any_element()),
292 single_example(
293 "Small",
294 Icon::new(IconName::Star)
295 .size(IconSize::Small)
296 .into_any_element(),
297 ),
298 single_example(
299 "Large",
300 Icon::new(IconName::Star)
301 .size(IconSize::XLarge)
302 .into_any_element(),
303 ),
304 ],
305 ),
306 example_group_with_title(
307 "Colors",
308 vec![
309 single_example("Default", Icon::new(IconName::Bell).into_any_element()),
310 single_example(
311 "Custom Color",
312 Icon::new(IconName::Bell)
313 .color(Color::Error)
314 .into_any_element(),
315 ),
316 ],
317 ),
318 ])
319 .into_any_element(),
320 )
321 }
322}