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