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