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