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