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