icon.rs

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