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