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