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