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 ZedAssistant2,
307 ZedAssistantFilled,
308 ZedPredict,
309 ZedXCopilot,
310}
311
312impl From<IconName> for Icon {
313 fn from(icon: IconName) -> Self {
314 Icon::new(icon)
315 }
316}
317
318#[derive(IntoElement)]
319pub struct Icon {
320 path: SharedString,
321 color: Color,
322 size: Rems,
323 transformation: Transformation,
324}
325
326impl Icon {
327 pub fn new(icon: IconName) -> Self {
328 Self {
329 path: icon.path().into(),
330 color: Color::default(),
331 size: IconSize::default().rems(),
332 transformation: Transformation::default(),
333 }
334 }
335
336 pub fn from_path(path: impl Into<SharedString>) -> Self {
337 Self {
338 path: path.into(),
339 color: Color::default(),
340 size: IconSize::default().rems(),
341 transformation: Transformation::default(),
342 }
343 }
344
345 pub fn color(mut self, color: Color) -> Self {
346 self.color = color;
347 self
348 }
349
350 pub fn size(mut self, size: IconSize) -> Self {
351 self.size = size.rems();
352 self
353 }
354
355 /// Sets a custom size for the icon, in [`Rems`].
356 ///
357 /// Not to be exposed outside of the `ui` crate.
358 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
359 self.size = size;
360 self
361 }
362
363 pub fn transform(mut self, transformation: Transformation) -> Self {
364 self.transformation = transformation;
365 self
366 }
367}
368
369impl RenderOnce for Icon {
370 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
371 svg()
372 .with_transformation(self.transformation)
373 .size(self.size)
374 .flex_none()
375 .path(self.path)
376 .text_color(self.color.color(cx))
377 }
378}
379
380const ICON_DECORATION_SIZE: f32 = 11.0;
381
382/// An icon silhouette used to knockout the background of an element
383/// for an icon to sit on top of it, emulating a stroke/border.
384#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, DerivePathStr)]
385#[strum(serialize_all = "snake_case")]
386#[path_str(prefix = "icons/knockouts", suffix = ".svg")]
387pub enum KnockoutIconName {
388 // /icons/knockouts/x1.svg
389 XFg,
390 XBg,
391 DotFg,
392 DotBg,
393 TriangleFg,
394 TriangleBg,
395}
396
397#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString)]
398pub enum IconDecorationKind {
399 // Slash,
400 X,
401 Dot,
402 Triangle,
403}
404
405impl IconDecorationKind {
406 fn fg(&self) -> KnockoutIconName {
407 match self {
408 Self::X => KnockoutIconName::XFg,
409 Self::Dot => KnockoutIconName::DotFg,
410 Self::Triangle => KnockoutIconName::TriangleFg,
411 }
412 }
413
414 fn bg(&self) -> KnockoutIconName {
415 match self {
416 Self::X => KnockoutIconName::XBg,
417 Self::Dot => KnockoutIconName::DotBg,
418 Self::Triangle => KnockoutIconName::TriangleBg,
419 }
420 }
421}
422
423/// The decoration for an icon.
424///
425/// For example, this can show an indicator, an "x",
426/// or a diagonal strikethrough to indicate something is disabled.
427#[derive(IntoElement)]
428pub struct IconDecoration {
429 kind: IconDecorationKind,
430 color: Hsla,
431 knockout_color: Hsla,
432 knockout_hover_color: Hsla,
433 position: Point<Pixels>,
434 group_name: Option<SharedString>,
435}
436
437impl IconDecoration {
438 /// Create a new icon decoration
439 pub fn new(kind: IconDecorationKind, knockout_color: Hsla, cx: &WindowContext) -> Self {
440 let color = cx.theme().colors().icon;
441 let position = Point::default();
442
443 Self {
444 kind,
445 color,
446 knockout_color,
447 knockout_hover_color: knockout_color,
448 position,
449 group_name: None,
450 }
451 }
452
453 /// Sets the kind of decoration
454 pub fn kind(mut self, kind: IconDecorationKind) -> Self {
455 self.kind = kind;
456 self
457 }
458
459 /// Sets the color of the decoration
460 pub fn color(mut self, color: Hsla) -> Self {
461 self.color = color;
462 self
463 }
464
465 /// Sets the color of the decoration's knockout
466 ///
467 /// Match this to the background of the element
468 /// the icon will be rendered on
469 pub fn knockout_color(mut self, color: Hsla) -> Self {
470 self.knockout_color = color;
471 self
472 }
473
474 /// Sets the color of the decoration that is used on hover
475 pub fn knockout_hover_color(mut self, color: Hsla) -> Self {
476 self.knockout_hover_color = color;
477 self
478 }
479
480 /// Sets the position of the decoration
481 pub fn position(mut self, position: Point<Pixels>) -> Self {
482 self.position = position;
483 self
484 }
485
486 /// Sets the name of the group the decoration belongs to
487 pub fn group_name(mut self, name: Option<SharedString>) -> Self {
488 self.group_name = name;
489 self
490 }
491}
492
493impl RenderOnce for IconDecoration {
494 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
495 div()
496 .size(px(ICON_DECORATION_SIZE))
497 .flex_none()
498 .absolute()
499 .bottom(self.position.y)
500 .right(self.position.x)
501 .child(
502 // foreground
503 svg()
504 .absolute()
505 .bottom_0()
506 .right_0()
507 .size(px(ICON_DECORATION_SIZE))
508 .path(self.kind.fg().path())
509 .text_color(self.color),
510 )
511 .child(
512 // background
513 svg()
514 .absolute()
515 .bottom_0()
516 .right_0()
517 .size(px(ICON_DECORATION_SIZE))
518 .path(self.kind.bg().path())
519 .text_color(self.knockout_color)
520 .when(self.group_name.is_none(), |this| {
521 this.hover(|style| style.text_color(self.knockout_hover_color))
522 })
523 .when_some(self.group_name.clone(), |this, group_name| {
524 this.group_hover(group_name, |style| {
525 style.text_color(self.knockout_hover_color)
526 })
527 }),
528 )
529 }
530}
531
532impl ComponentPreview for IconDecoration {
533 fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
534 let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
535
536 let examples = all_kinds
537 .iter()
538 .map(|kind| {
539 let name = format!("{:?}", kind).to_string();
540
541 single_example(
542 name,
543 IconDecoration::new(*kind, cx.theme().colors().surface_background, cx),
544 )
545 })
546 .collect();
547
548 vec![example_group(examples)]
549 }
550}
551
552#[derive(IntoElement)]
553pub struct DecoratedIcon {
554 icon: Icon,
555 decoration: Option<IconDecoration>,
556}
557
558impl DecoratedIcon {
559 pub fn new(icon: Icon, decoration: Option<IconDecoration>) -> Self {
560 Self { icon, decoration }
561 }
562}
563
564impl RenderOnce for DecoratedIcon {
565 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
566 div()
567 .relative()
568 .size(self.icon.size)
569 .child(self.icon)
570 .when_some(self.decoration, |this, decoration| this.child(decoration))
571 }
572}
573
574impl ComponentPreview for DecoratedIcon {
575 fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
576 let icon_1 = Icon::new(IconName::FileDoc);
577 let icon_2 = Icon::new(IconName::FileDoc);
578 let icon_3 = Icon::new(IconName::FileDoc);
579 let icon_4 = Icon::new(IconName::FileDoc);
580
581 let decoration_x = IconDecoration::new(
582 IconDecorationKind::X,
583 cx.theme().colors().surface_background,
584 cx,
585 )
586 .color(cx.theme().status().error)
587 .position(Point {
588 x: px(-2.),
589 y: px(-2.),
590 });
591
592 let decoration_triangle = IconDecoration::new(
593 IconDecorationKind::Triangle,
594 cx.theme().colors().surface_background,
595 cx,
596 )
597 .color(cx.theme().status().error)
598 .position(Point {
599 x: px(-2.),
600 y: px(-2.),
601 });
602
603 let decoration_dot = IconDecoration::new(
604 IconDecorationKind::Dot,
605 cx.theme().colors().surface_background,
606 cx,
607 )
608 .color(cx.theme().status().error)
609 .position(Point {
610 x: px(-2.),
611 y: px(-2.),
612 });
613
614 let examples = vec![
615 single_example("no_decoration", DecoratedIcon::new(icon_1, None)),
616 single_example(
617 "with_decoration",
618 DecoratedIcon::new(icon_2, Some(decoration_x)),
619 ),
620 single_example(
621 "with_decoration",
622 DecoratedIcon::new(icon_3, Some(decoration_triangle)),
623 ),
624 single_example(
625 "with_decoration",
626 DecoratedIcon::new(icon_4, Some(decoration_dot)),
627 ),
628 ];
629
630 vec![example_group(examples)]
631 }
632}
633
634#[derive(IntoElement)]
635pub struct IconWithIndicator {
636 icon: Icon,
637 indicator: Option<Indicator>,
638 indicator_border_color: Option<Hsla>,
639}
640
641impl IconWithIndicator {
642 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
643 Self {
644 icon,
645 indicator,
646 indicator_border_color: None,
647 }
648 }
649
650 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
651 self.indicator = indicator;
652 self
653 }
654
655 pub fn indicator_color(mut self, color: Color) -> Self {
656 if let Some(indicator) = self.indicator.as_mut() {
657 indicator.color = color;
658 }
659 self
660 }
661
662 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
663 self.indicator_border_color = color;
664 self
665 }
666}
667
668impl RenderOnce for IconWithIndicator {
669 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
670 let indicator_border_color = self
671 .indicator_border_color
672 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
673
674 div()
675 .relative()
676 .child(self.icon)
677 .when_some(self.indicator, |this, indicator| {
678 this.child(
679 div()
680 .absolute()
681 .size_2p5()
682 .border_2()
683 .border_color(indicator_border_color)
684 .rounded_full()
685 .bottom_neg_0p5()
686 .right_neg_0p5()
687 .child(indicator),
688 )
689 })
690 }
691}
692
693impl ComponentPreview for Icon {
694 fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
695 let arrow_icons = vec![
696 IconName::ArrowDown,
697 IconName::ArrowLeft,
698 IconName::ArrowRight,
699 IconName::ArrowUp,
700 IconName::ArrowCircle,
701 ];
702
703 vec![example_group_with_title(
704 "Arrow Icons",
705 arrow_icons
706 .into_iter()
707 .map(|icon| {
708 let name = format!("{:?}", icon).to_string();
709 ComponentExample::new(name, Icon::new(icon))
710 })
711 .collect(),
712 )]
713 }
714}