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