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