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