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