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    knockout_hover_color: Hsla,
431    position: Point<Pixels>,
432    group_name: Option<SharedString>,
433}
434
435impl IconDecoration {
436    /// Create a new icon decoration
437    pub fn new(kind: IconDecorationKind, knockout_color: Hsla, cx: &WindowContext) -> Self {
438        let color = cx.theme().colors().icon;
439        let position = Point::default();
440
441        Self {
442            kind,
443            color,
444            knockout_color,
445            knockout_hover_color: knockout_color,
446            position,
447            group_name: None,
448        }
449    }
450
451    /// Sets the kind of decoration
452    pub fn kind(mut self, kind: IconDecorationKind) -> Self {
453        self.kind = kind;
454        self
455    }
456
457    /// Sets the color of the decoration
458    pub fn color(mut self, color: Hsla) -> Self {
459        self.color = color;
460        self
461    }
462
463    /// Sets the color of the decoration's knockout
464    ///
465    /// Match this to the background of the element
466    /// the icon will be rendered on
467    pub fn knockout_color(mut self, color: Hsla) -> Self {
468        self.knockout_color = color;
469        self
470    }
471
472    /// Sets the color of the decoration that is used on hover
473    pub fn knockout_hover_color(mut self, color: Hsla) -> Self {
474        self.knockout_hover_color = color;
475        self
476    }
477
478    /// Sets the position of the decoration
479    pub fn position(mut self, position: Point<Pixels>) -> Self {
480        self.position = position;
481        self
482    }
483
484    /// Sets the name of the group the decoration belongs to
485    pub fn group_name(mut self, name: Option<SharedString>) -> Self {
486        self.group_name = name;
487        self
488    }
489}
490
491impl RenderOnce for IconDecoration {
492    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
493        div()
494            .size(px(ICON_DECORATION_SIZE))
495            .flex_none()
496            .absolute()
497            .bottom(self.position.y)
498            .right(self.position.x)
499            .child(
500                // foreground
501                svg()
502                    .absolute()
503                    .bottom_0()
504                    .right_0()
505                    .size(px(ICON_DECORATION_SIZE))
506                    .path(self.kind.fg().path())
507                    .text_color(self.color),
508            )
509            .child(
510                // background
511                svg()
512                    .absolute()
513                    .bottom_0()
514                    .right_0()
515                    .size(px(ICON_DECORATION_SIZE))
516                    .path(self.kind.bg().path())
517                    .text_color(self.knockout_color)
518                    .when(self.group_name.is_none(), |this| {
519                        this.hover(|style| style.text_color(self.knockout_hover_color))
520                    })
521                    .when_some(self.group_name.clone(), |this, group_name| {
522                        this.group_hover(group_name, |style| {
523                            style.text_color(self.knockout_hover_color)
524                        })
525                    }),
526            )
527    }
528}
529
530impl ComponentPreview for IconDecoration {
531    fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
532        let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
533
534        let examples = all_kinds
535            .iter()
536            .map(|kind| {
537                let name = format!("{:?}", kind).to_string();
538
539                single_example(
540                    name,
541                    IconDecoration::new(*kind, cx.theme().colors().surface_background, cx),
542                )
543            })
544            .collect();
545
546        vec![example_group(examples)]
547    }
548}
549
550#[derive(IntoElement)]
551pub struct DecoratedIcon {
552    icon: Icon,
553    decoration: Option<IconDecoration>,
554}
555
556impl DecoratedIcon {
557    pub fn new(icon: Icon, decoration: Option<IconDecoration>) -> Self {
558        Self { icon, decoration }
559    }
560}
561
562impl RenderOnce for DecoratedIcon {
563    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
564        div()
565            .relative()
566            .size(self.icon.size)
567            .child(self.icon)
568            .when_some(self.decoration, |this, decoration| this.child(decoration))
569    }
570}
571
572impl ComponentPreview for DecoratedIcon {
573    fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
574        let icon_1 = Icon::new(IconName::FileDoc);
575        let icon_2 = Icon::new(IconName::FileDoc);
576        let icon_3 = Icon::new(IconName::FileDoc);
577        let icon_4 = Icon::new(IconName::FileDoc);
578
579        let decoration_x = IconDecoration::new(
580            IconDecorationKind::X,
581            cx.theme().colors().surface_background,
582            cx,
583        )
584        .color(cx.theme().status().error)
585        .position(Point {
586            x: px(-2.),
587            y: px(-2.),
588        });
589
590        let decoration_triangle = IconDecoration::new(
591            IconDecorationKind::Triangle,
592            cx.theme().colors().surface_background,
593            cx,
594        )
595        .color(cx.theme().status().error)
596        .position(Point {
597            x: px(-2.),
598            y: px(-2.),
599        });
600
601        let decoration_dot = IconDecoration::new(
602            IconDecorationKind::Dot,
603            cx.theme().colors().surface_background,
604            cx,
605        )
606        .color(cx.theme().status().error)
607        .position(Point {
608            x: px(-2.),
609            y: px(-2.),
610        });
611
612        let examples = vec![
613            single_example("no_decoration", DecoratedIcon::new(icon_1, None)),
614            single_example(
615                "with_decoration",
616                DecoratedIcon::new(icon_2, Some(decoration_x)),
617            ),
618            single_example(
619                "with_decoration",
620                DecoratedIcon::new(icon_3, Some(decoration_triangle)),
621            ),
622            single_example(
623                "with_decoration",
624                DecoratedIcon::new(icon_4, Some(decoration_dot)),
625            ),
626        ];
627
628        vec![example_group(examples)]
629    }
630}
631
632#[derive(IntoElement)]
633pub struct IconWithIndicator {
634    icon: Icon,
635    indicator: Option<Indicator>,
636    indicator_border_color: Option<Hsla>,
637}
638
639impl IconWithIndicator {
640    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
641        Self {
642            icon,
643            indicator,
644            indicator_border_color: None,
645        }
646    }
647
648    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
649        self.indicator = indicator;
650        self
651    }
652
653    pub fn indicator_color(mut self, color: Color) -> Self {
654        if let Some(indicator) = self.indicator.as_mut() {
655            indicator.color = color;
656        }
657        self
658    }
659
660    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
661        self.indicator_border_color = color;
662        self
663    }
664}
665
666impl RenderOnce for IconWithIndicator {
667    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
668        let indicator_border_color = self
669            .indicator_border_color
670            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
671
672        div()
673            .relative()
674            .child(self.icon)
675            .when_some(self.indicator, |this, indicator| {
676                this.child(
677                    div()
678                        .absolute()
679                        .size_2p5()
680                        .border_2()
681                        .border_color(indicator_border_color)
682                        .rounded_full()
683                        .bottom_neg_0p5()
684                        .right_neg_0p5()
685                        .child(indicator),
686                )
687            })
688    }
689}
690
691impl ComponentPreview for Icon {
692    fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
693        let arrow_icons = vec![
694            IconName::ArrowDown,
695            IconName::ArrowLeft,
696            IconName::ArrowRight,
697            IconName::ArrowUp,
698            IconName::ArrowCircle,
699        ];
700
701        vec![example_group_with_title(
702            "Arrow Icons",
703            arrow_icons
704                .into_iter()
705                .map(|icon| {
706                    let name = format!("{:?}", icon).to_string();
707                    ComponentExample::new(name, Icon::new(icon))
708                })
709                .collect(),
710        )]
711    }
712}