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