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