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