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