1mod decorated_icon;
2mod icon_decoration;
3
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7pub use decorated_icon::*;
8use gpui::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
9pub use icon_decoration::*;
10use serde::{Deserialize, Serialize};
11use strum::{EnumIter, EnumString, IntoStaticStr};
12use ui_macros::DerivePathStr;
13
14use crate::{prelude::*, Indicator};
15
16#[derive(IntoElement)]
17pub enum AnyIcon {
18 Icon(Icon),
19 AnimatedIcon(AnimationElement<Icon>),
20}
21
22impl AnyIcon {
23 /// Returns a new [`AnyIcon`] after applying the given mapping function
24 /// to the contained [`Icon`].
25 pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
26 match self {
27 Self::Icon(icon) => Self::Icon(f(icon)),
28 Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
29 }
30 }
31}
32
33impl From<Icon> for AnyIcon {
34 fn from(value: Icon) -> Self {
35 Self::Icon(value)
36 }
37}
38
39impl From<AnimationElement<Icon>> for AnyIcon {
40 fn from(value: AnimationElement<Icon>) -> Self {
41 Self::AnimatedIcon(value)
42 }
43}
44
45impl RenderOnce for AnyIcon {
46 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
47 match self {
48 Self::Icon(icon) => icon.into_any_element(),
49 Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
50 }
51 }
52}
53
54#[derive(Default, PartialEq, Copy, Clone)]
55pub enum IconSize {
56 /// 10px
57 Indicator,
58 /// 12px
59 XSmall,
60 /// 14px
61 Small,
62 #[default]
63 /// 16px
64 Medium,
65 /// 48px
66 XLarge,
67 Custom(Rems),
68}
69
70impl IconSize {
71 pub fn rems(self) -> Rems {
72 match self {
73 IconSize::Indicator => rems_from_px(10.),
74 IconSize::XSmall => rems_from_px(12.),
75 IconSize::Small => rems_from_px(14.),
76 IconSize::Medium => rems_from_px(16.),
77 IconSize::XLarge => rems_from_px(48.),
78 IconSize::Custom(size) => size,
79 }
80 }
81
82 /// Returns the individual components of the square that contains this [`IconSize`].
83 ///
84 /// The returned tuple contains:
85 /// 1. The length of one side of the square
86 /// 2. The padding of one side of the square
87 pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
88 let icon_size = self.rems() * window.rem_size();
89 let padding = match self {
90 IconSize::Indicator => DynamicSpacing::Base00.px(cx),
91 IconSize::XSmall => DynamicSpacing::Base02.px(cx),
92 IconSize::Small => DynamicSpacing::Base02.px(cx),
93 IconSize::Medium => DynamicSpacing::Base02.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
110#[derive(
111 Debug,
112 PartialEq,
113 Eq,
114 Copy,
115 Clone,
116 EnumIter,
117 EnumString,
118 IntoStaticStr,
119 Serialize,
120 Deserialize,
121 DerivePathStr,
122)]
123#[strum(serialize_all = "snake_case")]
124#[path_str(prefix = "icons", suffix = ".svg")]
125pub enum IconName {
126 Ai,
127 AiAnthropic,
128 AiBedrock,
129 AiAnthropicHosted,
130 AiDeepSeek,
131 AiGoogle,
132 AiLmStudio,
133 AiMistral,
134 AiOllama,
135 AiOpenAi,
136 AiZed,
137 ArrowCircle,
138 ArrowDown,
139 ArrowDownFromLine,
140 ArrowLeft,
141 ArrowRight,
142 ArrowUp,
143 ArrowUpFromLine,
144 ArrowUpRight,
145 AtSign,
146 AudioOff,
147 AudioOn,
148 Backspace,
149 Bell,
150 BellDot,
151 BellOff,
152 BellRing,
153 Blocks,
154 Bolt,
155 Book,
156 BookCopy,
157 BookPlus,
158 CaseSensitive,
159 Check,
160 ChevronDown,
161 /// This chevron indicates a popover menu.
162 ChevronDownSmall,
163 ChevronLeft,
164 ChevronRight,
165 ChevronUp,
166 ChevronUpDown,
167 Circle,
168 Close,
169 Code,
170 Command,
171 Context,
172 Control,
173 Copilot,
174 CopilotDisabled,
175 CopilotError,
176 CopilotInit,
177 Copy,
178 CountdownTimer,
179 CursorIBeam,
180 Dash,
181 DatabaseZap,
182 Delete,
183 Diff,
184 Disconnected,
185 Download,
186 Ellipsis,
187 EllipsisVertical,
188 Envelope,
189 Eraser,
190 Escape,
191 ExpandVertical,
192 Exit,
193 ExternalLink,
194 Eye,
195 File,
196 FileCode,
197 FileDoc,
198 FileDiff,
199 FileGeneric,
200 FileGit,
201 FileLock,
202 FileRust,
203 FileSearch,
204 FileText,
205 FileToml,
206 FileTree,
207 Filter,
208 Folder,
209 FolderOpen,
210 FolderX,
211 Font,
212 FontSize,
213 FontWeight,
214 GenericClose,
215 GenericMaximize,
216 GenericMinimize,
217 GenericRestore,
218 Github,
219 Globe,
220 GitBranch,
221 Hash,
222 HistoryRerun,
223 Indicator,
224 Info,
225 InlayHint,
226 Keyboard,
227 Library,
228 LineHeight,
229 Link,
230 ListTree,
231 ListX,
232 LockOutlined,
233 MagnifyingGlass,
234 MailOpen,
235 Maximize,
236 Menu,
237 MessageBubbles,
238 MessageCircle,
239 Mic,
240 MicMute,
241 Microscope,
242 Minimize,
243 Option,
244 PageDown,
245 PageUp,
246 PanelLeft,
247 PanelRight,
248 Pencil,
249 Person,
250 PersonCircle,
251 PhoneIncoming,
252 Pin,
253 Play,
254 Plus,
255 PocketKnife,
256 Public,
257 PullRequest,
258 Quote,
259 RefreshTitle,
260 Regex,
261 ReplNeutral,
262 Replace,
263 ReplaceAll,
264 ReplaceNext,
265 ReplyArrowRight,
266 Rerun,
267 Return,
268 Reveal,
269 RotateCcw,
270 RotateCw,
271 Route,
272 Save,
273 Screen,
274 SearchCode,
275 SearchSelection,
276 SelectAll,
277 Server,
278 Settings,
279 SettingsAlt,
280 Shift,
281 Slash,
282 SlashSquare,
283 Sliders,
284 SlidersVertical,
285 Snip,
286 Space,
287 Sparkle,
288 SparkleAlt,
289 SparkleFilled,
290 Spinner,
291 Split,
292 SquareDot,
293 SquareMinus,
294 SquarePlus,
295 Star,
296 StarFilled,
297 Stop,
298 Strikethrough,
299 Supermaven,
300 SupermavenDisabled,
301 SupermavenError,
302 SupermavenInit,
303 SwatchBook,
304 Tab,
305 Terminal,
306 TextSnippet,
307 ThumbsUp,
308 ThumbsDown,
309 Trash,
310 TrashAlt,
311 Triangle,
312 TriangleRight,
313 Undo,
314 Unpin,
315 Update,
316 UserGroup,
317 Visible,
318 Wand,
319 Warning,
320 WholeWord,
321 X,
322 XCircle,
323 ZedAssistant,
324 ZedAssistant2,
325 ZedAssistantFilled,
326 ZedPredict,
327 ZedPredictUp,
328 ZedPredictDown,
329 ZedPredictDisabled,
330 ZedXCopilot,
331}
332
333impl From<IconName> for Icon {
334 fn from(icon: IconName) -> Self {
335 Icon::new(icon)
336 }
337}
338
339/// The source of an icon.
340enum IconSource {
341 /// An SVG embedded in the Zed binary.
342 Svg(SharedString),
343 /// An image file located at the specified path.
344 ///
345 /// Currently our SVG renderer is missing support for the following features:
346 /// 1. Loading SVGs from external files.
347 /// 2. Rendering polychrome SVGs.
348 ///
349 /// In order to support icon themes, we render the icons as images instead.
350 Image(Arc<Path>),
351}
352
353impl IconSource {
354 fn from_path(path: impl Into<SharedString>) -> Self {
355 let path = path.into();
356 if path.starts_with("icons/file_icons") {
357 Self::Svg(path)
358 } else {
359 Self::Image(Arc::from(PathBuf::from(path.as_ref())))
360 }
361 }
362}
363
364#[derive(IntoElement, IntoComponent)]
365pub struct Icon {
366 source: IconSource,
367 color: Color,
368 size: Rems,
369 transformation: Transformation,
370}
371
372impl Icon {
373 pub fn new(icon: IconName) -> Self {
374 Self {
375 source: IconSource::Svg(icon.path().into()),
376 color: Color::default(),
377 size: IconSize::default().rems(),
378 transformation: Transformation::default(),
379 }
380 }
381
382 pub fn from_path(path: impl Into<SharedString>) -> Self {
383 Self {
384 source: IconSource::from_path(path),
385 color: Color::default(),
386 size: IconSize::default().rems(),
387 transformation: Transformation::default(),
388 }
389 }
390
391 pub fn color(mut self, color: Color) -> Self {
392 self.color = color;
393 self
394 }
395
396 pub fn size(mut self, size: IconSize) -> Self {
397 self.size = size.rems();
398 self
399 }
400
401 /// Sets a custom size for the icon, in [`Rems`].
402 ///
403 /// Not to be exposed outside of the `ui` crate.
404 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
405 self.size = size;
406 self
407 }
408
409 pub fn transform(mut self, transformation: Transformation) -> Self {
410 self.transformation = transformation;
411 self
412 }
413}
414
415impl RenderOnce for Icon {
416 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
417 match self.source {
418 IconSource::Svg(path) => svg()
419 .with_transformation(self.transformation)
420 .size(self.size)
421 .flex_none()
422 .path(path)
423 .text_color(self.color.color(cx))
424 .into_any_element(),
425 IconSource::Image(path) => img(path)
426 .size(self.size)
427 .flex_none()
428 .text_color(self.color.color(cx))
429 .into_any_element(),
430 }
431 }
432}
433
434#[derive(IntoElement)]
435pub struct IconWithIndicator {
436 icon: Icon,
437 indicator: Option<Indicator>,
438 indicator_border_color: Option<Hsla>,
439}
440
441impl IconWithIndicator {
442 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
443 Self {
444 icon,
445 indicator,
446 indicator_border_color: None,
447 }
448 }
449
450 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
451 self.indicator = indicator;
452 self
453 }
454
455 pub fn indicator_color(mut self, color: Color) -> Self {
456 if let Some(indicator) = self.indicator.as_mut() {
457 indicator.color = color;
458 }
459 self
460 }
461
462 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
463 self.indicator_border_color = color;
464 self
465 }
466}
467
468impl RenderOnce for IconWithIndicator {
469 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
470 let indicator_border_color = self
471 .indicator_border_color
472 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
473
474 div()
475 .relative()
476 .child(self.icon)
477 .when_some(self.indicator, |this, indicator| {
478 this.child(
479 div()
480 .absolute()
481 .size_2p5()
482 .border_2()
483 .border_color(indicator_border_color)
484 .rounded_full()
485 .bottom_neg_0p5()
486 .right_neg_0p5()
487 .child(indicator),
488 )
489 })
490 }
491}
492
493// View this component preview using `workspace: open component-preview`
494impl ComponentPreview for Icon {
495 fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
496 v_flex()
497 .gap_6()
498 .children(vec![
499 example_group_with_title(
500 "Sizes",
501 vec![
502 single_example("Default", Icon::new(IconName::Star).into_any_element()),
503 single_example(
504 "Small",
505 Icon::new(IconName::Star)
506 .size(IconSize::Small)
507 .into_any_element(),
508 ),
509 single_example(
510 "Large",
511 Icon::new(IconName::Star)
512 .size(IconSize::XLarge)
513 .into_any_element(),
514 ),
515 ],
516 ),
517 example_group_with_title(
518 "Colors",
519 vec![
520 single_example("Default", Icon::new(IconName::Bell).into_any_element()),
521 single_example(
522 "Custom Color",
523 Icon::new(IconName::Bell)
524 .color(Color::Error)
525 .into_any_element(),
526 ),
527 ],
528 ),
529 ])
530 .into_any_element()
531 }
532}