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 position: Point<Pixels>,
431}
432
433impl IconDecoration {
434 /// Create a new icon decoration
435 pub fn new(kind: IconDecorationKind, knockout_color: Hsla, cx: &WindowContext) -> Self {
436 let color = cx.theme().colors().icon;
437 let position = Point::default();
438
439 Self {
440 kind,
441 color,
442 knockout_color,
443 position,
444 }
445 }
446
447 /// Sets the kind of decoration
448 pub fn kind(mut self, kind: IconDecorationKind) -> Self {
449 self.kind = kind;
450 self
451 }
452
453 /// Sets the color of the decoration
454 pub fn color(mut self, color: Hsla) -> Self {
455 self.color = color;
456 self
457 }
458
459 /// Sets the color of the decoration's knockout
460 ///
461 /// Match this to the background of the element
462 /// the icon will be rendered on
463 pub fn knockout_color(mut self, color: Hsla) -> Self {
464 self.knockout_color = color;
465 self
466 }
467
468 /// Sets the position of the decoration
469 pub fn position(mut self, position: Point<Pixels>) -> Self {
470 self.position = position;
471 self
472 }
473}
474
475impl RenderOnce for IconDecoration {
476 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
477 div()
478 .size(px(ICON_DECORATION_SIZE))
479 .flex_none()
480 .absolute()
481 .bottom(self.position.y)
482 .right(self.position.x)
483 .child(
484 // foreground
485 svg()
486 .absolute()
487 .bottom_0()
488 .right_0()
489 .size(px(ICON_DECORATION_SIZE))
490 .path(self.kind.fg().path())
491 .text_color(self.color),
492 )
493 .child(
494 // background
495 svg()
496 .absolute()
497 .bottom_0()
498 .right_0()
499 .size(px(ICON_DECORATION_SIZE))
500 .path(self.kind.bg().path())
501 .text_color(self.knockout_color),
502 )
503 }
504}
505
506impl ComponentPreview for IconDecoration {
507 fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
508 let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
509
510 let examples = all_kinds
511 .iter()
512 .map(|kind| {
513 let name = format!("{:?}", kind).to_string();
514
515 single_example(
516 name,
517 IconDecoration::new(*kind, cx.theme().colors().surface_background, cx),
518 )
519 })
520 .collect();
521
522 vec![example_group(examples)]
523 }
524}
525
526#[derive(IntoElement)]
527pub struct DecoratedIcon {
528 icon: Icon,
529 decoration: Option<IconDecoration>,
530}
531
532impl DecoratedIcon {
533 pub fn new(icon: Icon, decoration: Option<IconDecoration>) -> Self {
534 Self { icon, decoration }
535 }
536}
537
538impl RenderOnce for DecoratedIcon {
539 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
540 div()
541 .relative()
542 .size(self.icon.size)
543 .child(self.icon)
544 .when_some(self.decoration, |this, decoration| this.child(decoration))
545 }
546}
547
548impl ComponentPreview for DecoratedIcon {
549 fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
550 let icon_1 = Icon::new(IconName::FileDoc);
551 let icon_2 = Icon::new(IconName::FileDoc);
552 let icon_3 = Icon::new(IconName::FileDoc);
553 let icon_4 = Icon::new(IconName::FileDoc);
554
555 let decoration_x = IconDecoration::new(
556 IconDecorationKind::X,
557 cx.theme().colors().surface_background,
558 cx,
559 )
560 .color(cx.theme().status().error)
561 .position(Point {
562 x: px(-2.),
563 y: px(-2.),
564 });
565
566 let decoration_triangle = IconDecoration::new(
567 IconDecorationKind::Triangle,
568 cx.theme().colors().surface_background,
569 cx,
570 )
571 .color(cx.theme().status().error)
572 .position(Point {
573 x: px(-2.),
574 y: px(-2.),
575 });
576
577 let decoration_dot = IconDecoration::new(
578 IconDecorationKind::Dot,
579 cx.theme().colors().surface_background,
580 cx,
581 )
582 .color(cx.theme().status().error)
583 .position(Point {
584 x: px(-2.),
585 y: px(-2.),
586 });
587
588 let examples = vec![
589 single_example("no_decoration", DecoratedIcon::new(icon_1, None)),
590 single_example(
591 "with_decoration",
592 DecoratedIcon::new(icon_2, Some(decoration_x)),
593 ),
594 single_example(
595 "with_decoration",
596 DecoratedIcon::new(icon_3, Some(decoration_triangle)),
597 ),
598 single_example(
599 "with_decoration",
600 DecoratedIcon::new(icon_4, Some(decoration_dot)),
601 ),
602 ];
603
604 vec![example_group(examples)]
605 }
606}
607
608#[derive(IntoElement)]
609pub struct IconWithIndicator {
610 icon: Icon,
611 indicator: Option<Indicator>,
612 indicator_border_color: Option<Hsla>,
613}
614
615impl IconWithIndicator {
616 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
617 Self {
618 icon,
619 indicator,
620 indicator_border_color: None,
621 }
622 }
623
624 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
625 self.indicator = indicator;
626 self
627 }
628
629 pub fn indicator_color(mut self, color: Color) -> Self {
630 if let Some(indicator) = self.indicator.as_mut() {
631 indicator.color = color;
632 }
633 self
634 }
635
636 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
637 self.indicator_border_color = color;
638 self
639 }
640}
641
642impl RenderOnce for IconWithIndicator {
643 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
644 let indicator_border_color = self
645 .indicator_border_color
646 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
647
648 div()
649 .relative()
650 .child(self.icon)
651 .when_some(self.indicator, |this, indicator| {
652 this.child(
653 div()
654 .absolute()
655 .size_2p5()
656 .border_2()
657 .border_color(indicator_border_color)
658 .rounded_full()
659 .bottom_neg_0p5()
660 .right_neg_0p5()
661 .child(indicator),
662 )
663 })
664 }
665}
666
667impl ComponentPreview for Icon {
668 fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
669 let arrow_icons = vec![
670 IconName::ArrowDown,
671 IconName::ArrowLeft,
672 IconName::ArrowRight,
673 IconName::ArrowUp,
674 IconName::ArrowCircle,
675 ];
676
677 vec![example_group_with_title(
678 "Arrow Icons",
679 arrow_icons
680 .into_iter()
681 .map(|icon| {
682 let name = format!("{:?}", icon).to_string();
683 ComponentExample::new(name, Icon::new(icon))
684 })
685 .collect(),
686 )]
687 }
688}