icon.rs

  1use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
  2use serde::{Deserialize, Serialize};
  3use strum::{EnumIter, EnumString, IntoStaticStr};
  4
  5use crate::{prelude::*, Indicator};
  6
  7#[derive(IntoElement)]
  8pub enum AnyIcon {
  9    Icon(Icon),
 10    AnimatedIcon(AnimationElement<Icon>),
 11}
 12
 13impl AnyIcon {
 14    /// Returns a new [`AnyIcon`] after applying the given mapping function
 15    /// to the contained [`Icon`].
 16    pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
 17        match self {
 18            Self::Icon(icon) => Self::Icon(f(icon)),
 19            Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
 20        }
 21    }
 22}
 23
 24impl From<Icon> for AnyIcon {
 25    fn from(value: Icon) -> Self {
 26        Self::Icon(value)
 27    }
 28}
 29
 30impl From<AnimationElement<Icon>> for AnyIcon {
 31    fn from(value: AnimationElement<Icon>) -> Self {
 32        Self::AnimatedIcon(value)
 33    }
 34}
 35
 36impl RenderOnce for AnyIcon {
 37    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
 38        match self {
 39            Self::Icon(icon) => icon.into_any_element(),
 40            Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
 41        }
 42    }
 43}
 44
 45/// The decoration for an icon.
 46///
 47/// For example, this can show an indicator, an "x",
 48/// or a diagonal strkethrough to indicate something is disabled.
 49#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
 50pub enum IconDecoration {
 51    Strikethrough,
 52    IndicatorDot,
 53    X,
 54}
 55
 56#[derive(Default, PartialEq, Copy, Clone)]
 57pub enum IconSize {
 58    /// 10px
 59    Indicator,
 60    /// 12px
 61    XSmall,
 62    /// 14px
 63    Small,
 64    #[default]
 65    /// 16px
 66    Medium,
 67}
 68
 69impl IconSize {
 70    pub fn rems(self) -> Rems {
 71        match self {
 72            IconSize::Indicator => rems_from_px(10.),
 73            IconSize::XSmall => rems_from_px(12.),
 74            IconSize::Small => rems_from_px(14.),
 75            IconSize::Medium => rems_from_px(16.),
 76        }
 77    }
 78
 79    /// Returns the individual components of the square that contains this [`IconSize`].
 80    ///
 81    /// The returned tuple contains:
 82    ///   1. The length of one side of the square
 83    ///   2. The padding of one side of the square
 84    pub fn square_components(&self, cx: &mut WindowContext) -> (Pixels, Pixels) {
 85        let icon_size = self.rems() * cx.rem_size();
 86        let padding = match self {
 87            IconSize::Indicator => Spacing::None.px(cx),
 88            IconSize::XSmall => Spacing::XSmall.px(cx),
 89            IconSize::Small => Spacing::XSmall.px(cx),
 90            IconSize::Medium => Spacing::XSmall.px(cx),
 91        };
 92
 93        (icon_size, padding)
 94    }
 95
 96    /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
 97    pub fn square(&self, cx: &mut WindowContext) -> Pixels {
 98        let (icon_size, padding) = self.square_components(cx);
 99
100        icon_size + padding * 2.
101    }
102}
103
104#[derive(
105    Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, Serialize, Deserialize,
106)]
107pub enum IconName {
108    Ai,
109    ArrowCircle,
110    ArrowDown,
111    ArrowDownFromLine,
112    ArrowLeft,
113    ArrowRight,
114    ArrowUp,
115    ArrowUpFromLine,
116    ArrowUpRight,
117    AtSign,
118    AudioOff,
119    AudioOn,
120    Backspace,
121    Bell,
122    BellDot,
123    BellOff,
124    BellRing,
125    Bolt,
126    Book,
127    BookCopy,
128    BookPlus,
129    CaseSensitive,
130    Check,
131    ChevronDown,
132    /// This chevron indicates a popover menu.
133    ChevronDownSmall,
134    ChevronLeft,
135    ChevronRight,
136    ChevronUp,
137    ChevronUpDown,
138    Close,
139    Code,
140    Collab,
141    Command,
142    Context,
143    Control,
144    Copilot,
145    CopilotDisabled,
146    CopilotError,
147    CopilotInit,
148    Copy,
149    CountdownTimer,
150    Dash,
151    Delete,
152    Disconnected,
153    Download,
154    Ellipsis,
155    Envelope,
156    Escape,
157    ExclamationTriangle,
158    Exit,
159    ExpandVertical,
160    ExternalLink,
161    File,
162    FileDoc,
163    FileGeneric,
164    FileGit,
165    FileLock,
166    FileRust,
167    FileToml,
168    FileTree,
169    Filter,
170    Folder,
171    FolderOpen,
172    FolderX,
173    Font,
174    FontSize,
175    FontWeight,
176    Github,
177    GenericMinimize,
178    GenericMaximize,
179    GenericClose,
180    GenericRestore,
181    Hash,
182    HistoryRerun,
183    Indicator,
184    IndicatorX,
185    InlayHint,
186    Library,
187    LineHeight,
188    Link,
189    ListTree,
190    MagicWand,
191    MagnifyingGlass,
192    MailOpen,
193    Maximize,
194    Menu,
195    MessageBubbles,
196    Mic,
197    MicMute,
198    Minimize,
199    Option,
200    PageDown,
201    PageUp,
202    Pencil,
203    Person,
204    Play,
205    Plus,
206    Public,
207    PullRequest,
208    Quote,
209    Regex,
210    ReplNeutral,
211    Replace,
212    ReplaceAll,
213    ReplaceNext,
214    ReplyArrowRight,
215    Rerun,
216    Return,
217    Reveal,
218    RotateCcw,
219    RotateCw,
220    Save,
221    Screen,
222    SearchSelection,
223    SelectAll,
224    Server,
225    Settings,
226    Shift,
227    Sliders,
228    Snip,
229    Space,
230    Sparkle,
231    SparkleAlt,
232    SparkleFilled,
233    Spinner,
234    Split,
235    Star,
236    StarFilled,
237    Stop,
238    Strikethrough,
239    Supermaven,
240    SupermavenDisabled,
241    SupermavenError,
242    SupermavenInit,
243    Tab,
244    Terminal,
245    TextCursor,
246    Trash,
247    TriangleRight,
248    Update,
249    WholeWord,
250    XCircle,
251    ZedAssistant,
252    ZedAssistantFilled,
253    ZedXCopilot,
254    Visible,
255}
256
257impl IconName {
258    pub fn path(self) -> &'static str {
259        match self {
260            IconName::Ai => "icons/ai.svg",
261            IconName::ArrowCircle => "icons/arrow_circle.svg",
262            IconName::ArrowDown => "icons/arrow_down.svg",
263            IconName::ArrowDownFromLine => "icons/arrow_down_from_line.svg",
264            IconName::ArrowLeft => "icons/arrow_left.svg",
265            IconName::ArrowRight => "icons/arrow_right.svg",
266            IconName::ArrowUp => "icons/arrow_up.svg",
267            IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
268            IconName::ArrowUpRight => "icons/arrow_up_right.svg",
269            IconName::AtSign => "icons/at_sign.svg",
270            IconName::AudioOff => "icons/speaker_off.svg",
271            IconName::AudioOn => "icons/speaker_loud.svg",
272            IconName::Backspace => "icons/backspace.svg",
273            IconName::Bell => "icons/bell.svg",
274            IconName::BellDot => "icons/bell_dot.svg",
275            IconName::BellOff => "icons/bell_off.svg",
276            IconName::BellRing => "icons/bell_ring.svg",
277            IconName::Bolt => "icons/bolt.svg",
278            IconName::Book => "icons/book.svg",
279            IconName::BookCopy => "icons/book_copy.svg",
280            IconName::BookPlus => "icons/book_plus.svg",
281            IconName::CaseSensitive => "icons/case_insensitive.svg",
282            IconName::Check => "icons/check.svg",
283            IconName::ChevronDown => "icons/chevron_down.svg",
284            IconName::ChevronDownSmall => "icons/chevron_down_small.svg",
285            IconName::ChevronLeft => "icons/chevron_left.svg",
286            IconName::ChevronRight => "icons/chevron_right.svg",
287            IconName::ChevronUp => "icons/chevron_up.svg",
288            IconName::ChevronUpDown => "icons/chevron_up_down.svg",
289            IconName::Close => "icons/x.svg",
290            IconName::Code => "icons/code.svg",
291            IconName::Collab => "icons/user_group_16.svg",
292            IconName::Command => "icons/command.svg",
293            IconName::Context => "icons/context.svg",
294            IconName::Control => "icons/control.svg",
295            IconName::Copilot => "icons/copilot.svg",
296            IconName::CopilotDisabled => "icons/copilot_disabled.svg",
297            IconName::CopilotError => "icons/copilot_error.svg",
298            IconName::CopilotInit => "icons/copilot_init.svg",
299            IconName::Copy => "icons/copy.svg",
300            IconName::CountdownTimer => "icons/countdown_timer.svg",
301            IconName::Dash => "icons/dash.svg",
302            IconName::Delete => "icons/delete.svg",
303            IconName::Disconnected => "icons/disconnected.svg",
304            IconName::Download => "icons/download.svg",
305            IconName::Ellipsis => "icons/ellipsis.svg",
306            IconName::Envelope => "icons/feedback.svg",
307            IconName::Escape => "icons/escape.svg",
308            IconName::ExclamationTriangle => "icons/warning.svg",
309            IconName::Exit => "icons/exit.svg",
310            IconName::ExpandVertical => "icons/expand_vertical.svg",
311            IconName::ExternalLink => "icons/external_link.svg",
312            IconName::File => "icons/file.svg",
313            IconName::FileDoc => "icons/file_icons/book.svg",
314            IconName::FileGeneric => "icons/file_icons/file.svg",
315            IconName::FileGit => "icons/file_icons/git.svg",
316            IconName::FileLock => "icons/file_icons/lock.svg",
317            IconName::FileRust => "icons/file_icons/rust.svg",
318            IconName::FileToml => "icons/file_icons/toml.svg",
319            IconName::FileTree => "icons/project.svg",
320            IconName::Filter => "icons/filter.svg",
321            IconName::Folder => "icons/file_icons/folder.svg",
322            IconName::FolderOpen => "icons/file_icons/folder_open.svg",
323            IconName::FolderX => "icons/stop_sharing.svg",
324            IconName::Font => "icons/font.svg",
325            IconName::FontSize => "icons/font_size.svg",
326            IconName::FontWeight => "icons/font_weight.svg",
327            IconName::Github => "icons/github.svg",
328            IconName::GenericMinimize => "icons/generic_minimize.svg",
329            IconName::GenericMaximize => "icons/generic_maximize.svg",
330            IconName::GenericClose => "icons/generic_close.svg",
331            IconName::GenericRestore => "icons/generic_restore.svg",
332            IconName::Hash => "icons/hash.svg",
333            IconName::HistoryRerun => "icons/history_rerun.svg",
334            IconName::Indicator => "icons/indicator.svg",
335            IconName::IndicatorX => "icons/indicator_x.svg",
336            IconName::InlayHint => "icons/inlay_hint.svg",
337            IconName::Library => "icons/library.svg",
338            IconName::LineHeight => "icons/line_height.svg",
339            IconName::Link => "icons/link.svg",
340            IconName::ListTree => "icons/list_tree.svg",
341            IconName::MagicWand => "icons/magic_wand.svg",
342            IconName::MagnifyingGlass => "icons/magnifying_glass.svg",
343            IconName::MailOpen => "icons/mail_open.svg",
344            IconName::Maximize => "icons/maximize.svg",
345            IconName::Menu => "icons/menu.svg",
346            IconName::MessageBubbles => "icons/conversations.svg",
347            IconName::Mic => "icons/mic.svg",
348            IconName::MicMute => "icons/mic_mute.svg",
349            IconName::Minimize => "icons/minimize.svg",
350            IconName::Option => "icons/option.svg",
351            IconName::PageDown => "icons/page_down.svg",
352            IconName::PageUp => "icons/page_up.svg",
353            IconName::Pencil => "icons/pencil.svg",
354            IconName::Person => "icons/person.svg",
355            IconName::Play => "icons/play.svg",
356            IconName::Plus => "icons/plus.svg",
357            IconName::Public => "icons/public.svg",
358            IconName::PullRequest => "icons/pull_request.svg",
359            IconName::Quote => "icons/quote.svg",
360            IconName::Regex => "icons/regex.svg",
361            IconName::ReplNeutral => "icons/repl_neutral.svg",
362            IconName::Replace => "icons/replace.svg",
363            IconName::ReplaceAll => "icons/replace_all.svg",
364            IconName::ReplaceNext => "icons/replace_next.svg",
365            IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
366            IconName::Rerun => "icons/rerun.svg",
367            IconName::Return => "icons/return.svg",
368            IconName::Reveal => "icons/reveal.svg",
369            IconName::RotateCcw => "icons/rotate_ccw.svg",
370            IconName::RotateCw => "icons/rotate_cw.svg",
371            IconName::Save => "icons/save.svg",
372            IconName::Screen => "icons/desktop.svg",
373            IconName::SearchSelection => "icons/search_selection.svg",
374            IconName::SelectAll => "icons/select_all.svg",
375            IconName::Server => "icons/server.svg",
376            IconName::Settings => "icons/file_icons/settings.svg",
377            IconName::Shift => "icons/shift.svg",
378            IconName::Sliders => "icons/sliders.svg",
379            IconName::Snip => "icons/snip.svg",
380            IconName::Space => "icons/space.svg",
381            IconName::Sparkle => "icons/sparkle.svg",
382            IconName::SparkleAlt => "icons/sparkle_alt.svg",
383            IconName::SparkleFilled => "icons/sparkle_filled.svg",
384            IconName::Spinner => "icons/spinner.svg",
385            IconName::Split => "icons/split.svg",
386            IconName::Star => "icons/star.svg",
387            IconName::StarFilled => "icons/star_filled.svg",
388            IconName::Stop => "icons/stop.svg",
389            IconName::Strikethrough => "icons/strikethrough.svg",
390            IconName::Supermaven => "icons/supermaven.svg",
391            IconName::SupermavenDisabled => "icons/supermaven_disabled.svg",
392            IconName::SupermavenError => "icons/supermaven_error.svg",
393            IconName::SupermavenInit => "icons/supermaven_init.svg",
394            IconName::Tab => "icons/tab.svg",
395            IconName::Terminal => "icons/terminal.svg",
396            IconName::TextCursor => "icons/text-cursor.svg",
397            IconName::Trash => "icons/trash.svg",
398            IconName::TriangleRight => "icons/triangle_right.svg",
399            IconName::Update => "icons/update.svg",
400            IconName::WholeWord => "icons/word_search.svg",
401            IconName::XCircle => "icons/error.svg",
402            IconName::ZedAssistant => "icons/zed_assistant.svg",
403            IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
404            IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
405            IconName::Visible => "icons/visible.svg",
406        }
407    }
408}
409
410#[derive(IntoElement)]
411pub struct Icon {
412    path: SharedString,
413    color: Color,
414    size: Rems,
415    transformation: Transformation,
416}
417
418impl Icon {
419    pub fn new(icon: IconName) -> Self {
420        Self {
421            path: icon.path().into(),
422            color: Color::default(),
423            size: IconSize::default().rems(),
424            transformation: Transformation::default(),
425        }
426    }
427
428    pub fn from_path(path: impl Into<SharedString>) -> Self {
429        Self {
430            path: path.into(),
431            color: Color::default(),
432            size: IconSize::default().rems(),
433            transformation: Transformation::default(),
434        }
435    }
436
437    pub fn color(mut self, color: Color) -> Self {
438        self.color = color;
439        self
440    }
441
442    pub fn size(mut self, size: IconSize) -> Self {
443        self.size = size.rems();
444        self
445    }
446
447    /// Sets a custom size for the icon, in [`Rems`].
448    ///
449    /// Not to be exposed outside of the `ui` crate.
450    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
451        self.size = size;
452        self
453    }
454
455    pub fn transform(mut self, transformation: Transformation) -> Self {
456        self.transformation = transformation;
457        self
458    }
459}
460
461impl RenderOnce for Icon {
462    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
463        svg()
464            .with_transformation(self.transformation)
465            .size(self.size)
466            .flex_none()
467            .path(self.path)
468            .text_color(self.color.color(cx))
469    }
470}
471
472#[derive(IntoElement)]
473pub struct DecoratedIcon {
474    icon: Icon,
475    decoration: IconDecoration,
476    decoration_color: Color,
477    parent_background: Option<Hsla>,
478}
479
480impl DecoratedIcon {
481    pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
482        Self {
483            icon,
484            decoration,
485            decoration_color: Color::Default,
486            parent_background: None,
487        }
488    }
489
490    pub fn decoration_color(mut self, color: Color) -> Self {
491        self.decoration_color = color;
492        self
493    }
494
495    pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
496        self.parent_background = background;
497        self
498    }
499}
500
501impl RenderOnce for DecoratedIcon {
502    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
503        let background = self
504            .parent_background
505            .unwrap_or(cx.theme().colors().background);
506
507        let size = self.icon.size;
508
509        let decoration_icon = match self.decoration {
510            IconDecoration::Strikethrough => IconName::Strikethrough,
511            IconDecoration::IndicatorDot => IconName::Indicator,
512            IconDecoration::X => IconName::IndicatorX,
513        };
514
515        let decoration_svg = |icon: IconName| {
516            svg()
517                .absolute()
518                .top_0()
519                .left_0()
520                .path(icon.path())
521                .size(size)
522                .flex_none()
523                .text_color(self.decoration_color.color(cx))
524        };
525
526        let decoration_knockout = |icon: IconName| {
527            svg()
528                .absolute()
529                .top(-rems_from_px(2.))
530                .left(-rems_from_px(3.))
531                .path(icon.path())
532                .size(size + rems_from_px(2.))
533                .flex_none()
534                .text_color(background)
535        };
536
537        div()
538            .relative()
539            .size(self.icon.size)
540            .child(self.icon)
541            .child(decoration_knockout(decoration_icon))
542            .child(decoration_svg(decoration_icon))
543    }
544}
545
546#[derive(IntoElement)]
547pub struct IconWithIndicator {
548    icon: Icon,
549    indicator: Option<Indicator>,
550    indicator_border_color: Option<Hsla>,
551}
552
553impl IconWithIndicator {
554    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
555        Self {
556            icon,
557            indicator,
558            indicator_border_color: None,
559        }
560    }
561
562    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
563        self.indicator = indicator;
564        self
565    }
566
567    pub fn indicator_color(mut self, color: Color) -> Self {
568        if let Some(indicator) = self.indicator.as_mut() {
569            indicator.color = color;
570        }
571        self
572    }
573
574    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
575        self.indicator_border_color = color;
576        self
577    }
578}
579
580impl RenderOnce for IconWithIndicator {
581    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
582        let indicator_border_color = self
583            .indicator_border_color
584            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
585
586        div()
587            .relative()
588            .child(self.icon)
589            .when_some(self.indicator, |this, indicator| {
590                this.child(
591                    div()
592                        .absolute()
593                        .w_2()
594                        .h_2()
595                        .border_1()
596                        .border_color(indicator_border_color)
597                        .rounded_full()
598                        .bottom_neg_0p5()
599                        .right_neg_1()
600                        .child(indicator),
601                )
602            })
603    }
604}