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