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