1#![allow(missing_docs)]
2use gpui::{svg, AnimationElement, Hsla, IntoElement, Point, Rems, Transformation};
3use serde::{Deserialize, Serialize};
4use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
5use ui_macros::DerivePathStr;
6
7use crate::{
8 prelude::*,
9 traits::component_preview::{ComponentExample, ComponentPreview},
10 Indicator,
11};
12
13#[derive(IntoElement)]
14pub enum AnyIcon {
15 Icon(Icon),
16 AnimatedIcon(AnimationElement<Icon>),
17}
18
19impl AnyIcon {
20 /// Returns a new [`AnyIcon`] after applying the given mapping function
21 /// to the contained [`Icon`].
22 pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
23 match self {
24 Self::Icon(icon) => Self::Icon(f(icon)),
25 Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
26 }
27 }
28}
29
30impl From<Icon> for AnyIcon {
31 fn from(value: Icon) -> Self {
32 Self::Icon(value)
33 }
34}
35
36impl From<AnimationElement<Icon>> for AnyIcon {
37 fn from(value: AnimationElement<Icon>) -> Self {
38 Self::AnimatedIcon(value)
39 }
40}
41
42impl RenderOnce for AnyIcon {
43 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
44 match self {
45 Self::Icon(icon) => icon.into_any_element(),
46 Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
47 }
48 }
49}
50
51#[derive(Default, PartialEq, Copy, Clone)]
52pub enum IconSize {
53 /// 10px
54 Indicator,
55 /// 12px
56 XSmall,
57 /// 14px
58 Small,
59 #[default]
60 /// 16px
61 Medium,
62}
63
64impl IconSize {
65 pub fn rems(self) -> Rems {
66 match self {
67 IconSize::Indicator => rems_from_px(10.),
68 IconSize::XSmall => rems_from_px(12.),
69 IconSize::Small => rems_from_px(14.),
70 IconSize::Medium => rems_from_px(16.),
71 }
72 }
73
74 /// Returns the individual components of the square that contains this [`IconSize`].
75 ///
76 /// The returned tuple contains:
77 /// 1. The length of one side of the square
78 /// 2. The padding of one side of the square
79 pub fn square_components(&self, cx: &mut WindowContext) -> (Pixels, Pixels) {
80 let icon_size = self.rems() * cx.rem_size();
81 let padding = match self {
82 IconSize::Indicator => DynamicSpacing::Base00.px(cx),
83 IconSize::XSmall => DynamicSpacing::Base02.px(cx),
84 IconSize::Small => DynamicSpacing::Base02.px(cx),
85 IconSize::Medium => DynamicSpacing::Base02.px(cx),
86 };
87
88 (icon_size, padding)
89 }
90
91 /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
92 pub fn square(&self, cx: &mut WindowContext) -> Pixels {
93 let (icon_size, padding) = self.square_components(cx);
94
95 icon_size + padding * 2.
96 }
97}
98
99#[derive(
100 Debug,
101 PartialEq,
102 Eq,
103 Copy,
104 Clone,
105 EnumIter,
106 EnumString,
107 IntoStaticStr,
108 Serialize,
109 Deserialize,
110 DerivePathStr,
111)]
112#[strum(serialize_all = "snake_case")]
113#[path_str(prefix = "icons", suffix = ".svg")]
114pub enum IconName {
115 Ai,
116 AiAnthropic,
117 AiAnthropicHosted,
118 AiGoogle,
119 AiLmStudio,
120 AiOllama,
121 AiOpenAi,
122 AiZed,
123 ArrowCircle,
124 ArrowDown,
125 ArrowDownFromLine,
126 ArrowLeft,
127 ArrowRight,
128 ArrowUp,
129 ArrowUpFromLine,
130 ArrowUpRight,
131 AtSign,
132 AudioOff,
133 AudioOn,
134 Backspace,
135 Bell,
136 BellDot,
137 BellOff,
138 BellRing,
139 Blocks,
140 Bolt,
141 Book,
142 BookCopy,
143 BookPlus,
144 CaseSensitive,
145 Check,
146 ChevronDown,
147 ChevronDownSmall, // This chevron indicates a popover menu.
148 ChevronLeft,
149 ChevronRight,
150 ChevronUp,
151 ChevronUpDown,
152 Close,
153 Code,
154 Command,
155 Context,
156 Control,
157 Copilot,
158 CopilotDisabled,
159 CopilotError,
160 CopilotInit,
161 Copy,
162 CountdownTimer,
163 CursorIBeam,
164 Dash,
165 DatabaseZap,
166 Delete,
167 Diff,
168 Disconnected,
169 Download,
170 Ellipsis,
171 EllipsisVertical,
172 Envelope,
173 Eraser,
174 Escape,
175 ExpandVertical,
176 Exit,
177 ExternalLink,
178 Eye,
179 File,
180 FileCode,
181 FileDoc,
182 FileDiff,
183 FileGeneric,
184 FileGit,
185 FileLock,
186 FileRust,
187 FileSearch,
188 FileText,
189 FileToml,
190 FileTree,
191 Filter,
192 Folder,
193 FolderOpen,
194 FolderX,
195 Font,
196 FontSize,
197 FontWeight,
198 GenericClose,
199 GenericMaximize,
200 GenericMinimize,
201 GenericRestore,
202 Github,
203 Globe,
204 GitBranch,
205 Hash,
206 HistoryRerun,
207 Indicator,
208 IndicatorX,
209 Info,
210 InlayHint,
211 Keyboard,
212 Library,
213 LineHeight,
214 Link,
215 ListTree,
216 ListX,
217 MagnifyingGlass,
218 MailOpen,
219 Maximize,
220 Menu,
221 MessageBubbles,
222 MessageCircle,
223 Mic,
224 MicMute,
225 Microscope,
226 Minimize,
227 Option,
228 PageDown,
229 PageUp,
230 PanelLeft,
231 PanelRight,
232 Pencil,
233 Person,
234 PhoneIncoming,
235 Pin,
236 Play,
237 Plus,
238 PocketKnife,
239 Public,
240 PullRequest,
241 Quote,
242 RefreshTitle,
243 Regex,
244 ReplNeutral,
245 Replace,
246 ReplaceAll,
247 ReplaceNext,
248 ReplyArrowRight,
249 Rerun,
250 Return,
251 Reveal,
252 RotateCcw,
253 RotateCw,
254 Route,
255 Save,
256 Screen,
257 SearchCode,
258 SearchSelection,
259 SelectAll,
260 Server,
261 Settings,
262 SettingsAlt,
263 Shift,
264 Slash,
265 SlashSquare,
266 Sliders,
267 SlidersVertical,
268 Snip,
269 Space,
270 Sparkle,
271 SparkleAlt,
272 SparkleFilled,
273 Spinner,
274 Split,
275 SquareDot,
276 SquareMinus,
277 SquarePlus,
278 Star,
279 StarFilled,
280 Stop,
281 Strikethrough,
282 Supermaven,
283 SupermavenDisabled,
284 SupermavenError,
285 SupermavenInit,
286 SwatchBook,
287 Tab,
288 Terminal,
289 TextSnippet,
290 ThumbsUp,
291 ThumbsDown,
292 Trash,
293 TrashAlt,
294 Triangle,
295 TriangleRight,
296 Undo,
297 Unpin,
298 Update,
299 UserGroup,
300 Visible,
301 Wand,
302 Warning,
303 WholeWord,
304 X,
305 XCircle,
306 ZedAssistant,
307 ZedAssistant2,
308 ZedAssistantFilled,
309 ZedPredict,
310 ZedXCopilot,
311}
312
313impl From<IconName> for Icon {
314 fn from(icon: IconName) -> Self {
315 Icon::new(icon)
316 }
317}
318
319#[derive(IntoElement)]
320pub struct Icon {
321 path: SharedString,
322 color: Color,
323 size: Rems,
324 transformation: Transformation,
325}
326
327impl Icon {
328 pub fn new(icon: IconName) -> Self {
329 Self {
330 path: icon.path().into(),
331 color: Color::default(),
332 size: IconSize::default().rems(),
333 transformation: Transformation::default(),
334 }
335 }
336
337 pub fn from_path(path: impl Into<SharedString>) -> Self {
338 Self {
339 path: path.into(),
340 color: Color::default(),
341 size: IconSize::default().rems(),
342 transformation: Transformation::default(),
343 }
344 }
345
346 pub fn color(mut self, color: Color) -> Self {
347 self.color = color;
348 self
349 }
350
351 pub fn size(mut self, size: IconSize) -> Self {
352 self.size = size.rems();
353 self
354 }
355
356 /// Sets a custom size for the icon, in [`Rems`].
357 ///
358 /// Not to be exposed outside of the `ui` crate.
359 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
360 self.size = size;
361 self
362 }
363
364 pub fn transform(mut self, transformation: Transformation) -> Self {
365 self.transformation = transformation;
366 self
367 }
368}
369
370impl RenderOnce for Icon {
371 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
372 svg()
373 .with_transformation(self.transformation)
374 .size(self.size)
375 .flex_none()
376 .path(self.path)
377 .text_color(self.color.color(cx))
378 }
379}
380
381const ICON_DECORATION_SIZE: f32 = 11.0;
382
383/// An icon silhouette used to knockout the background of an element
384/// for an icon to sit on top of it, emulating a stroke/border.
385#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, DerivePathStr)]
386#[strum(serialize_all = "snake_case")]
387#[path_str(prefix = "icons/knockouts", suffix = ".svg")]
388pub enum KnockoutIconName {
389 // /icons/knockouts/x1.svg
390 XFg,
391 XBg,
392 DotFg,
393 DotBg,
394 TriangleFg,
395 TriangleBg,
396}
397
398#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString)]
399pub enum IconDecorationKind {
400 // Slash,
401 X,
402 Dot,
403 Triangle,
404}
405
406impl IconDecorationKind {
407 fn fg(&self) -> KnockoutIconName {
408 match self {
409 Self::X => KnockoutIconName::XFg,
410 Self::Dot => KnockoutIconName::DotFg,
411 Self::Triangle => KnockoutIconName::TriangleFg,
412 }
413 }
414
415 fn bg(&self) -> KnockoutIconName {
416 match self {
417 Self::X => KnockoutIconName::XBg,
418 Self::Dot => KnockoutIconName::DotBg,
419 Self::Triangle => KnockoutIconName::TriangleBg,
420 }
421 }
422}
423
424/// The decoration for an icon.
425///
426/// For example, this can show an indicator, an "x",
427/// or a diagonal strikethrough to indicate something is disabled.
428#[derive(IntoElement)]
429pub struct IconDecoration {
430 kind: IconDecorationKind,
431 color: Hsla,
432 knockout_color: Hsla,
433 knockout_hover_color: Hsla,
434 position: Point<Pixels>,
435 group_name: Option<SharedString>,
436}
437
438impl IconDecoration {
439 /// Create a new icon decoration
440 pub fn new(kind: IconDecorationKind, knockout_color: Hsla, cx: &WindowContext) -> Self {
441 let color = cx.theme().colors().icon;
442 let position = Point::default();
443
444 Self {
445 kind,
446 color,
447 knockout_color,
448 knockout_hover_color: knockout_color,
449 position,
450 group_name: None,
451 }
452 }
453
454 /// Sets the kind of decoration
455 pub fn kind(mut self, kind: IconDecorationKind) -> Self {
456 self.kind = kind;
457 self
458 }
459
460 /// Sets the color of the decoration
461 pub fn color(mut self, color: Hsla) -> Self {
462 self.color = color;
463 self
464 }
465
466 /// Sets the color of the decoration's knockout
467 ///
468 /// Match this to the background of the element
469 /// the icon will be rendered on
470 pub fn knockout_color(mut self, color: Hsla) -> Self {
471 self.knockout_color = color;
472 self
473 }
474
475 /// Sets the color of the decoration that is used on hover
476 pub fn knockout_hover_color(mut self, color: Hsla) -> Self {
477 self.knockout_hover_color = color;
478 self
479 }
480
481 /// Sets the position of the decoration
482 pub fn position(mut self, position: Point<Pixels>) -> Self {
483 self.position = position;
484 self
485 }
486
487 /// Sets the name of the group the decoration belongs to
488 pub fn group_name(mut self, name: Option<SharedString>) -> Self {
489 self.group_name = name;
490 self
491 }
492}
493
494impl RenderOnce for IconDecoration {
495 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
496 div()
497 .size(px(ICON_DECORATION_SIZE))
498 .flex_none()
499 .absolute()
500 .bottom(self.position.y)
501 .right(self.position.x)
502 .child(
503 // foreground
504 svg()
505 .absolute()
506 .bottom_0()
507 .right_0()
508 .size(px(ICON_DECORATION_SIZE))
509 .path(self.kind.fg().path())
510 .text_color(self.color),
511 )
512 .child(
513 // background
514 svg()
515 .absolute()
516 .bottom_0()
517 .right_0()
518 .size(px(ICON_DECORATION_SIZE))
519 .path(self.kind.bg().path())
520 .text_color(self.knockout_color)
521 .when(self.group_name.is_none(), |this| {
522 this.hover(|style| style.text_color(self.knockout_hover_color))
523 })
524 .when_some(self.group_name.clone(), |this, group_name| {
525 this.group_hover(group_name, |style| {
526 style.text_color(self.knockout_hover_color)
527 })
528 }),
529 )
530 }
531}
532
533impl ComponentPreview for IconDecoration {
534 fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
535 let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
536
537 let examples = all_kinds
538 .iter()
539 .map(|kind| {
540 let name = format!("{:?}", kind).to_string();
541
542 single_example(
543 name,
544 IconDecoration::new(*kind, cx.theme().colors().surface_background, cx),
545 )
546 })
547 .collect();
548
549 vec![example_group(examples)]
550 }
551}
552
553#[derive(IntoElement)]
554pub struct DecoratedIcon {
555 icon: Icon,
556 decoration: Option<IconDecoration>,
557}
558
559impl DecoratedIcon {
560 pub fn new(icon: Icon, decoration: Option<IconDecoration>) -> Self {
561 Self { icon, decoration }
562 }
563}
564
565impl RenderOnce for DecoratedIcon {
566 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
567 div()
568 .relative()
569 .size(self.icon.size)
570 .child(self.icon)
571 .when_some(self.decoration, |this, decoration| this.child(decoration))
572 }
573}
574
575impl ComponentPreview for DecoratedIcon {
576 fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
577 let icon_1 = Icon::new(IconName::FileDoc);
578 let icon_2 = Icon::new(IconName::FileDoc);
579 let icon_3 = Icon::new(IconName::FileDoc);
580 let icon_4 = Icon::new(IconName::FileDoc);
581
582 let decoration_x = IconDecoration::new(
583 IconDecorationKind::X,
584 cx.theme().colors().surface_background,
585 cx,
586 )
587 .color(cx.theme().status().error)
588 .position(Point {
589 x: px(-2.),
590 y: px(-2.),
591 });
592
593 let decoration_triangle = IconDecoration::new(
594 IconDecorationKind::Triangle,
595 cx.theme().colors().surface_background,
596 cx,
597 )
598 .color(cx.theme().status().error)
599 .position(Point {
600 x: px(-2.),
601 y: px(-2.),
602 });
603
604 let decoration_dot = IconDecoration::new(
605 IconDecorationKind::Dot,
606 cx.theme().colors().surface_background,
607 cx,
608 )
609 .color(cx.theme().status().error)
610 .position(Point {
611 x: px(-2.),
612 y: px(-2.),
613 });
614
615 let examples = vec![
616 single_example("no_decoration", DecoratedIcon::new(icon_1, None)),
617 single_example(
618 "with_decoration",
619 DecoratedIcon::new(icon_2, Some(decoration_x)),
620 ),
621 single_example(
622 "with_decoration",
623 DecoratedIcon::new(icon_3, Some(decoration_triangle)),
624 ),
625 single_example(
626 "with_decoration",
627 DecoratedIcon::new(icon_4, Some(decoration_dot)),
628 ),
629 ];
630
631 vec![example_group(examples)]
632 }
633}
634
635#[derive(IntoElement)]
636pub struct IconWithIndicator {
637 icon: Icon,
638 indicator: Option<Indicator>,
639 indicator_border_color: Option<Hsla>,
640}
641
642impl IconWithIndicator {
643 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
644 Self {
645 icon,
646 indicator,
647 indicator_border_color: None,
648 }
649 }
650
651 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
652 self.indicator = indicator;
653 self
654 }
655
656 pub fn indicator_color(mut self, color: Color) -> Self {
657 if let Some(indicator) = self.indicator.as_mut() {
658 indicator.color = color;
659 }
660 self
661 }
662
663 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
664 self.indicator_border_color = color;
665 self
666 }
667}
668
669impl RenderOnce for IconWithIndicator {
670 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
671 let indicator_border_color = self
672 .indicator_border_color
673 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
674
675 div()
676 .relative()
677 .child(self.icon)
678 .when_some(self.indicator, |this, indicator| {
679 this.child(
680 div()
681 .absolute()
682 .size_2p5()
683 .border_2()
684 .border_color(indicator_border_color)
685 .rounded_full()
686 .bottom_neg_0p5()
687 .right_neg_0p5()
688 .child(indicator),
689 )
690 })
691 }
692}
693
694impl ComponentPreview for Icon {
695 fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
696 let arrow_icons = vec![
697 IconName::ArrowDown,
698 IconName::ArrowLeft,
699 IconName::ArrowRight,
700 IconName::ArrowUp,
701 IconName::ArrowCircle,
702 ];
703
704 vec![example_group_with_title(
705 "Arrow Icons",
706 arrow_icons
707 .into_iter()
708 .map(|icon| {
709 let name = format!("{:?}", icon).to_string();
710 ComponentExample::new(name, Icon::new(icon))
711 })
712 .collect(),
713 )]
714 }
715}