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    SparkleFilled,
232    Spinner,
233    Split,
234    Star,
235    StarFilled,
236    Stop,
237    Strikethrough,
238    Supermaven,
239    SupermavenDisabled,
240    SupermavenError,
241    SupermavenInit,
242    Tab,
243    Terminal,
244    TextCursor,
245    Trash,
246    TriangleRight,
247    Update,
248    WholeWord,
249    XCircle,
250    ZedAssistant,
251    ZedAssistantFilled,
252    ZedXCopilot,
253    Visible,
254}
255
256impl IconName {
257    pub fn path(self) -> &'static str {
258        match self {
259            IconName::Ai => "icons/ai.svg",
260            IconName::ArrowCircle => "icons/arrow_circle.svg",
261            IconName::ArrowDown => "icons/arrow_down.svg",
262            IconName::ArrowDownFromLine => "icons/arrow_down_from_line.svg",
263            IconName::ArrowLeft => "icons/arrow_left.svg",
264            IconName::ArrowRight => "icons/arrow_right.svg",
265            IconName::ArrowUp => "icons/arrow_up.svg",
266            IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
267            IconName::ArrowUpRight => "icons/arrow_up_right.svg",
268            IconName::AtSign => "icons/at_sign.svg",
269            IconName::AudioOff => "icons/speaker_off.svg",
270            IconName::AudioOn => "icons/speaker_loud.svg",
271            IconName::Backspace => "icons/backspace.svg",
272            IconName::Bell => "icons/bell.svg",
273            IconName::BellDot => "icons/bell_dot.svg",
274            IconName::BellOff => "icons/bell_off.svg",
275            IconName::BellRing => "icons/bell_ring.svg",
276            IconName::Bolt => "icons/bolt.svg",
277            IconName::Book => "icons/book.svg",
278            IconName::BookCopy => "icons/book_copy.svg",
279            IconName::BookPlus => "icons/book_plus.svg",
280            IconName::CaseSensitive => "icons/case_insensitive.svg",
281            IconName::Check => "icons/check.svg",
282            IconName::ChevronDown => "icons/chevron_down.svg",
283            IconName::ChevronDownSmall => "icons/chevron_down_small.svg",
284            IconName::ChevronLeft => "icons/chevron_left.svg",
285            IconName::ChevronRight => "icons/chevron_right.svg",
286            IconName::ChevronUp => "icons/chevron_up.svg",
287            IconName::ChevronUpDown => "icons/chevron_up_down.svg",
288            IconName::Close => "icons/x.svg",
289            IconName::Code => "icons/code.svg",
290            IconName::Collab => "icons/user_group_16.svg",
291            IconName::Command => "icons/command.svg",
292            IconName::Context => "icons/context.svg",
293            IconName::Control => "icons/control.svg",
294            IconName::Copilot => "icons/copilot.svg",
295            IconName::CopilotDisabled => "icons/copilot_disabled.svg",
296            IconName::CopilotError => "icons/copilot_error.svg",
297            IconName::CopilotInit => "icons/copilot_init.svg",
298            IconName::Copy => "icons/copy.svg",
299            IconName::CountdownTimer => "icons/countdown_timer.svg",
300            IconName::Dash => "icons/dash.svg",
301            IconName::Delete => "icons/delete.svg",
302            IconName::Disconnected => "icons/disconnected.svg",
303            IconName::Download => "icons/download.svg",
304            IconName::Ellipsis => "icons/ellipsis.svg",
305            IconName::Envelope => "icons/feedback.svg",
306            IconName::Escape => "icons/escape.svg",
307            IconName::ExclamationTriangle => "icons/warning.svg",
308            IconName::Exit => "icons/exit.svg",
309            IconName::ExpandVertical => "icons/expand_vertical.svg",
310            IconName::ExternalLink => "icons/external_link.svg",
311            IconName::File => "icons/file.svg",
312            IconName::FileDoc => "icons/file_icons/book.svg",
313            IconName::FileGeneric => "icons/file_icons/file.svg",
314            IconName::FileGit => "icons/file_icons/git.svg",
315            IconName::FileLock => "icons/file_icons/lock.svg",
316            IconName::FileRust => "icons/file_icons/rust.svg",
317            IconName::FileToml => "icons/file_icons/toml.svg",
318            IconName::FileTree => "icons/project.svg",
319            IconName::Filter => "icons/filter.svg",
320            IconName::Folder => "icons/file_icons/folder.svg",
321            IconName::FolderOpen => "icons/file_icons/folder_open.svg",
322            IconName::FolderX => "icons/stop_sharing.svg",
323            IconName::Font => "icons/font.svg",
324            IconName::FontSize => "icons/font_size.svg",
325            IconName::FontWeight => "icons/font_weight.svg",
326            IconName::Github => "icons/github.svg",
327            IconName::GenericMinimize => "icons/generic_minimize.svg",
328            IconName::GenericMaximize => "icons/generic_maximize.svg",
329            IconName::GenericClose => "icons/generic_close.svg",
330            IconName::GenericRestore => "icons/generic_restore.svg",
331            IconName::Hash => "icons/hash.svg",
332            IconName::HistoryRerun => "icons/history_rerun.svg",
333            IconName::Indicator => "icons/indicator.svg",
334            IconName::IndicatorX => "icons/indicator_x.svg",
335            IconName::InlayHint => "icons/inlay_hint.svg",
336            IconName::Library => "icons/library.svg",
337            IconName::LineHeight => "icons/line_height.svg",
338            IconName::Link => "icons/link.svg",
339            IconName::ListTree => "icons/list_tree.svg",
340            IconName::MagicWand => "icons/magic_wand.svg",
341            IconName::MagnifyingGlass => "icons/magnifying_glass.svg",
342            IconName::MailOpen => "icons/mail_open.svg",
343            IconName::Maximize => "icons/maximize.svg",
344            IconName::Menu => "icons/menu.svg",
345            IconName::MessageBubbles => "icons/conversations.svg",
346            IconName::Mic => "icons/mic.svg",
347            IconName::MicMute => "icons/mic_mute.svg",
348            IconName::Minimize => "icons/minimize.svg",
349            IconName::Option => "icons/option.svg",
350            IconName::PageDown => "icons/page_down.svg",
351            IconName::PageUp => "icons/page_up.svg",
352            IconName::Pencil => "icons/pencil.svg",
353            IconName::Person => "icons/person.svg",
354            IconName::Play => "icons/play.svg",
355            IconName::Plus => "icons/plus.svg",
356            IconName::Public => "icons/public.svg",
357            IconName::PullRequest => "icons/pull_request.svg",
358            IconName::Quote => "icons/quote.svg",
359            IconName::Regex => "icons/regex.svg",
360            IconName::ReplNeutral => "icons/repl_neutral.svg",
361            IconName::Replace => "icons/replace.svg",
362            IconName::ReplaceAll => "icons/replace_all.svg",
363            IconName::ReplaceNext => "icons/replace_next.svg",
364            IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
365            IconName::Rerun => "icons/rerun.svg",
366            IconName::Return => "icons/return.svg",
367            IconName::Reveal => "icons/reveal.svg",
368            IconName::RotateCcw => "icons/rotate_ccw.svg",
369            IconName::RotateCw => "icons/rotate_cw.svg",
370            IconName::Save => "icons/save.svg",
371            IconName::Screen => "icons/desktop.svg",
372            IconName::SearchSelection => "icons/search_selection.svg",
373            IconName::SelectAll => "icons/select_all.svg",
374            IconName::Server => "icons/server.svg",
375            IconName::Settings => "icons/file_icons/settings.svg",
376            IconName::Shift => "icons/shift.svg",
377            IconName::Sliders => "icons/sliders.svg",
378            IconName::Snip => "icons/snip.svg",
379            IconName::Space => "icons/space.svg",
380            IconName::Sparkle => "icons/sparkle.svg",
381            IconName::SparkleFilled => "icons/sparkle_filled.svg",
382            IconName::Spinner => "icons/spinner.svg",
383            IconName::Split => "icons/split.svg",
384            IconName::Star => "icons/star.svg",
385            IconName::StarFilled => "icons/star_filled.svg",
386            IconName::Stop => "icons/stop.svg",
387            IconName::Strikethrough => "icons/strikethrough.svg",
388            IconName::Supermaven => "icons/supermaven.svg",
389            IconName::SupermavenDisabled => "icons/supermaven_disabled.svg",
390            IconName::SupermavenError => "icons/supermaven_error.svg",
391            IconName::SupermavenInit => "icons/supermaven_init.svg",
392            IconName::Tab => "icons/tab.svg",
393            IconName::Terminal => "icons/terminal.svg",
394            IconName::TextCursor => "icons/text-cursor.svg",
395            IconName::Trash => "icons/trash.svg",
396            IconName::TriangleRight => "icons/triangle_right.svg",
397            IconName::Update => "icons/update.svg",
398            IconName::WholeWord => "icons/word_search.svg",
399            IconName::XCircle => "icons/error.svg",
400            IconName::ZedAssistant => "icons/zed_assistant.svg",
401            IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
402            IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
403            IconName::Visible => "icons/visible.svg",
404        }
405    }
406}
407
408#[derive(IntoElement)]
409pub struct Icon {
410    path: SharedString,
411    color: Color,
412    size: Rems,
413    transformation: Transformation,
414}
415
416impl Icon {
417    pub fn new(icon: IconName) -> Self {
418        Self {
419            path: icon.path().into(),
420            color: Color::default(),
421            size: IconSize::default().rems(),
422            transformation: Transformation::default(),
423        }
424    }
425
426    pub fn from_path(path: impl Into<SharedString>) -> Self {
427        Self {
428            path: path.into(),
429            color: Color::default(),
430            size: IconSize::default().rems(),
431            transformation: Transformation::default(),
432        }
433    }
434
435    pub fn color(mut self, color: Color) -> Self {
436        self.color = color;
437        self
438    }
439
440    pub fn size(mut self, size: IconSize) -> Self {
441        self.size = size.rems();
442        self
443    }
444
445    /// Sets a custom size for the icon, in [`Rems`].
446    ///
447    /// Not to be exposed outside of the `ui` crate.
448    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
449        self.size = size;
450        self
451    }
452
453    pub fn transform(mut self, transformation: Transformation) -> Self {
454        self.transformation = transformation;
455        self
456    }
457}
458
459impl RenderOnce for Icon {
460    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
461        svg()
462            .with_transformation(self.transformation)
463            .size(self.size)
464            .flex_none()
465            .path(self.path)
466            .text_color(self.color.color(cx))
467    }
468}
469
470#[derive(IntoElement)]
471pub struct DecoratedIcon {
472    icon: Icon,
473    decoration: IconDecoration,
474    decoration_color: Color,
475    parent_background: Option<Hsla>,
476}
477
478impl DecoratedIcon {
479    pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
480        Self {
481            icon,
482            decoration,
483            decoration_color: Color::Default,
484            parent_background: None,
485        }
486    }
487
488    pub fn decoration_color(mut self, color: Color) -> Self {
489        self.decoration_color = color;
490        self
491    }
492
493    pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
494        self.parent_background = background;
495        self
496    }
497}
498
499impl RenderOnce for DecoratedIcon {
500    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
501        let background = self
502            .parent_background
503            .unwrap_or(cx.theme().colors().background);
504
505        let size = self.icon.size;
506
507        let decoration_icon = match self.decoration {
508            IconDecoration::Strikethrough => IconName::Strikethrough,
509            IconDecoration::IndicatorDot => IconName::Indicator,
510            IconDecoration::X => IconName::IndicatorX,
511        };
512
513        let decoration_svg = |icon: IconName| {
514            svg()
515                .absolute()
516                .top_0()
517                .left_0()
518                .path(icon.path())
519                .size(size)
520                .flex_none()
521                .text_color(self.decoration_color.color(cx))
522        };
523
524        let decoration_knockout = |icon: IconName| {
525            svg()
526                .absolute()
527                .top(-rems_from_px(2.))
528                .left(-rems_from_px(3.))
529                .path(icon.path())
530                .size(size + rems_from_px(2.))
531                .flex_none()
532                .text_color(background)
533        };
534
535        div()
536            .relative()
537            .size(self.icon.size)
538            .child(self.icon)
539            .child(decoration_knockout(decoration_icon))
540            .child(decoration_svg(decoration_icon))
541    }
542}
543
544#[derive(IntoElement)]
545pub struct IconWithIndicator {
546    icon: Icon,
547    indicator: Option<Indicator>,
548    indicator_border_color: Option<Hsla>,
549}
550
551impl IconWithIndicator {
552    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
553        Self {
554            icon,
555            indicator,
556            indicator_border_color: None,
557        }
558    }
559
560    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
561        self.indicator = indicator;
562        self
563    }
564
565    pub fn indicator_color(mut self, color: Color) -> Self {
566        if let Some(indicator) = self.indicator.as_mut() {
567            indicator.color = color;
568        }
569        self
570    }
571
572    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
573        self.indicator_border_color = color;
574        self
575    }
576}
577
578impl RenderOnce for IconWithIndicator {
579    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
580        let indicator_border_color = self
581            .indicator_border_color
582            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
583
584        div()
585            .relative()
586            .child(self.icon)
587            .when_some(self.indicator, |this, indicator| {
588                this.child(
589                    div()
590                        .absolute()
591                        .w_2()
592                        .h_2()
593                        .border_1()
594                        .border_color(indicator_border_color)
595                        .rounded_full()
596                        .bottom_neg_0p5()
597                        .right_neg_1()
598                        .child(indicator),
599                )
600            })
601    }
602}