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