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