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