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