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