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