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