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