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