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