icon.rs

  1mod decorated_icon;
  2mod icon_decoration;
  3
  4use std::path::{Path, PathBuf};
  5use std::sync::Arc;
  6
  7pub use decorated_icon::*;
  8use gpui::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
  9pub use icon_decoration::*;
 10use serde::{Deserialize, Serialize};
 11use strum::{EnumIter, EnumString, IntoStaticStr};
 12use ui_macros::DerivePathStr;
 13
 14use crate::{prelude::*, Indicator};
 15
 16#[derive(IntoElement)]
 17pub enum AnyIcon {
 18    Icon(Icon),
 19    AnimatedIcon(AnimationElement<Icon>),
 20}
 21
 22impl AnyIcon {
 23    /// Returns a new [`AnyIcon`] after applying the given mapping function
 24    /// to the contained [`Icon`].
 25    pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
 26        match self {
 27            Self::Icon(icon) => Self::Icon(f(icon)),
 28            Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
 29        }
 30    }
 31}
 32
 33impl From<Icon> for AnyIcon {
 34    fn from(value: Icon) -> Self {
 35        Self::Icon(value)
 36    }
 37}
 38
 39impl From<AnimationElement<Icon>> for AnyIcon {
 40    fn from(value: AnimationElement<Icon>) -> Self {
 41        Self::AnimatedIcon(value)
 42    }
 43}
 44
 45impl RenderOnce for AnyIcon {
 46    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
 47        match self {
 48            Self::Icon(icon) => icon.into_any_element(),
 49            Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
 50        }
 51    }
 52}
 53
 54#[derive(Default, PartialEq, Copy, Clone)]
 55pub enum IconSize {
 56    /// 10px
 57    Indicator,
 58    /// 12px
 59    XSmall,
 60    /// 14px
 61    Small,
 62    #[default]
 63    /// 16px
 64    Medium,
 65    /// 48px
 66    XLarge,
 67    Custom(Rems),
 68}
 69
 70impl IconSize {
 71    pub fn rems(self) -> Rems {
 72        match self {
 73            IconSize::Indicator => rems_from_px(10.),
 74            IconSize::XSmall => rems_from_px(12.),
 75            IconSize::Small => rems_from_px(14.),
 76            IconSize::Medium => rems_from_px(16.),
 77            IconSize::XLarge => rems_from_px(48.),
 78            IconSize::Custom(size) => size,
 79        }
 80    }
 81
 82    /// Returns the individual components of the square that contains this [`IconSize`].
 83    ///
 84    /// The returned tuple contains:
 85    ///   1. The length of one side of the square
 86    ///   2. The padding of one side of the square
 87    pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
 88        let icon_size = self.rems() * window.rem_size();
 89        let padding = match self {
 90            IconSize::Indicator => DynamicSpacing::Base00.px(cx),
 91            IconSize::XSmall => DynamicSpacing::Base02.px(cx),
 92            IconSize::Small => DynamicSpacing::Base02.px(cx),
 93            IconSize::Medium => DynamicSpacing::Base02.px(cx),
 94            IconSize::XLarge => DynamicSpacing::Base02.px(cx),
 95            // TODO: Wire into dynamic spacing
 96            IconSize::Custom(size) => size.to_pixels(window.rem_size()),
 97        };
 98
 99        (icon_size, padding)
100    }
101
102    /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
103    pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels {
104        let (icon_size, padding) = self.square_components(window, cx);
105
106        icon_size + padding * 2.
107    }
108}
109
110#[derive(
111    Debug,
112    PartialEq,
113    Eq,
114    Copy,
115    Clone,
116    EnumIter,
117    EnumString,
118    IntoStaticStr,
119    Serialize,
120    Deserialize,
121    DerivePathStr,
122)]
123#[strum(serialize_all = "snake_case")]
124#[path_str(prefix = "icons", suffix = ".svg")]
125pub enum IconName {
126    Ai,
127    AiAnthropic,
128    AiBedrock,
129    AiAnthropicHosted,
130    AiDeepSeek,
131    AiEdit,
132    AiGoogle,
133    AiLmStudio,
134    AiMistral,
135    AiOllama,
136    AiOpenAi,
137    AiZed,
138    ArrowCircle,
139    ArrowDown,
140    ArrowDownFromLine,
141    ArrowLeft,
142    ArrowRight,
143    ArrowRightLeft,
144    ArrowUp,
145    ArrowUpFromLine,
146    ArrowUpRight,
147    ArrowUpRightAlt,
148    AtSign,
149    AudioOff,
150    AudioOn,
151    Backspace,
152    Bell,
153    BellDot,
154    BellOff,
155    BellRing,
156    Blocks,
157    Bolt,
158    Book,
159    BookCopy,
160    BookPlus,
161    Brain,
162    CaseSensitive,
163    Check,
164    ChevronDown,
165    /// This chevron indicates a popover menu.
166    ChevronDownSmall,
167    ChevronLeft,
168    ChevronRight,
169    ChevronUp,
170    ChevronUpDown,
171    Circle,
172    Clipboard,
173    Close,
174    Code,
175    Cog,
176    Command,
177    Context,
178    Control,
179    Copilot,
180    CopilotDisabled,
181    CopilotError,
182    CopilotInit,
183    Copy,
184    CountdownTimer,
185    CursorIBeam,
186    Dash,
187    DebugBreakpoint,
188    DebugIgnoreBreakpoints,
189    DebugPause,
190    DebugContinue,
191    DebugStepOver,
192    DebugStepInto,
193    DebugStepOut,
194    DebugStepBack,
195    DebugRestart,
196    Debug,
197    DebugStop,
198    DebugDisconnect,
199    DebugLogBreakpoint,
200    DatabaseZap,
201    Delete,
202    Diff,
203    Disconnected,
204    Download,
205    Ellipsis,
206    EllipsisVertical,
207    Envelope,
208    Eraser,
209    Escape,
210    ExpandVertical,
211    Exit,
212    ExternalLink,
213    ExpandUp,
214    ExpandDown,
215    Eye,
216    File,
217    FileCode,
218    FileDoc,
219    FileDiff,
220    FileGeneric,
221    FileGit,
222    FileLock,
223    FileRust,
224    FileSearch,
225    FileText,
226    FileToml,
227    FileTree,
228    Filter,
229    Folder,
230    FolderOpen,
231    FolderX,
232    Font,
233    FontSize,
234    FontWeight,
235    GenericClose,
236    GenericMaximize,
237    GenericMinimize,
238    GenericRestore,
239    Github,
240    Globe,
241    GitBranch,
242    GitBranchSmall,
243    Hash,
244    HistoryRerun,
245    Indicator,
246    Info,
247    InlayHint,
248    Keyboard,
249    Library,
250    LineHeight,
251    Link,
252    ListTree,
253    ListX,
254    LockOutlined,
255    MagnifyingGlass,
256    MailOpen,
257    Maximize,
258    Menu,
259    MessageBubbles,
260    MessageCircle,
261    Cloud,
262    Mic,
263    MicMute,
264    Microscope,
265    Minimize,
266    Option,
267    PageDown,
268    PageUp,
269    PanelLeft,
270    PanelRight,
271    Pencil,
272    Person,
273    PersonCircle,
274    PhoneIncoming,
275    Pin,
276    Play,
277    Plus,
278    PocketKnife,
279    Public,
280    PullRequest,
281    Quote,
282    RefreshTitle,
283    Regex,
284    ReplNeutral,
285    Replace,
286    ReplaceAll,
287    ReplaceNext,
288    ReplyArrowRight,
289    Rerun,
290    Return,
291    Reveal,
292    RotateCcw,
293    RotateCw,
294    Route,
295    Save,
296    Screen,
297    SearchCode,
298    SearchSelection,
299    SelectAll,
300    Server,
301    Settings,
302    SettingsAlt,
303    Shift,
304    Slash,
305    SlashSquare,
306    Sliders,
307    SlidersVertical,
308    Snip,
309    Space,
310    Sparkle,
311    SparkleAlt,
312    SparkleFilled,
313    Spinner,
314    Split,
315    SquareDot,
316    SquareMinus,
317    SquarePlus,
318    Star,
319    StarFilled,
320    Stop,
321    Strikethrough,
322    Supermaven,
323    SupermavenDisabled,
324    SupermavenError,
325    SupermavenInit,
326    SwatchBook,
327    Tab,
328    Terminal,
329    TextSnippet,
330    ThumbsUp,
331    ThumbsDown,
332    Trash,
333    TrashAlt,
334    Triangle,
335    TriangleRight,
336    Undo,
337    Unpin,
338    Update,
339    UserGroup,
340    Visible,
341    Wand,
342    Warning,
343    WholeWord,
344    X,
345    XCircle,
346    ZedAssistant,
347    ZedAssistant2,
348    ZedAssistantFilled,
349    ZedPredict,
350    ZedPredictUp,
351    ZedPredictDown,
352    ZedPredictDisabled,
353    ZedPredictError,
354    ZedXCopilot,
355}
356
357impl From<IconName> for Icon {
358    fn from(icon: IconName) -> Self {
359        Icon::new(icon)
360    }
361}
362
363/// The source of an icon.
364enum IconSource {
365    /// An SVG embedded in the Zed binary.
366    Svg(SharedString),
367    /// An image file located at the specified path.
368    ///
369    /// Currently our SVG renderer is missing support for the following features:
370    /// 1. Loading SVGs from external files.
371    /// 2. Rendering polychrome SVGs.
372    ///
373    /// In order to support icon themes, we render the icons as images instead.
374    Image(Arc<Path>),
375}
376
377impl IconSource {
378    fn from_path(path: impl Into<SharedString>) -> Self {
379        let path = path.into();
380        if path.starts_with("icons/") {
381            Self::Svg(path)
382        } else {
383            Self::Image(Arc::from(PathBuf::from(path.as_ref())))
384        }
385    }
386}
387
388#[derive(IntoElement, IntoComponent)]
389pub struct Icon {
390    source: IconSource,
391    color: Color,
392    size: Rems,
393    transformation: Transformation,
394}
395
396impl Icon {
397    pub fn new(icon: IconName) -> Self {
398        Self {
399            source: IconSource::Svg(icon.path().into()),
400            color: Color::default(),
401            size: IconSize::default().rems(),
402            transformation: Transformation::default(),
403        }
404    }
405
406    pub fn from_path(path: impl Into<SharedString>) -> Self {
407        Self {
408            source: IconSource::from_path(path),
409            color: Color::default(),
410            size: IconSize::default().rems(),
411            transformation: Transformation::default(),
412        }
413    }
414
415    pub fn color(mut self, color: Color) -> Self {
416        self.color = color;
417        self
418    }
419
420    pub fn size(mut self, size: IconSize) -> Self {
421        self.size = size.rems();
422        self
423    }
424
425    /// Sets a custom size for the icon, in [`Rems`].
426    ///
427    /// Not to be exposed outside of the `ui` crate.
428    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
429        self.size = size;
430        self
431    }
432
433    pub fn transform(mut self, transformation: Transformation) -> Self {
434        self.transformation = transformation;
435        self
436    }
437}
438
439impl RenderOnce for Icon {
440    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
441        match self.source {
442            IconSource::Svg(path) => svg()
443                .with_transformation(self.transformation)
444                .size(self.size)
445                .flex_none()
446                .path(path)
447                .text_color(self.color.color(cx))
448                .into_any_element(),
449            IconSource::Image(path) => img(path)
450                .size(self.size)
451                .flex_none()
452                .text_color(self.color.color(cx))
453                .into_any_element(),
454        }
455    }
456}
457
458#[derive(IntoElement)]
459pub struct IconWithIndicator {
460    icon: Icon,
461    indicator: Option<Indicator>,
462    indicator_border_color: Option<Hsla>,
463}
464
465impl IconWithIndicator {
466    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
467        Self {
468            icon,
469            indicator,
470            indicator_border_color: None,
471        }
472    }
473
474    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
475        self.indicator = indicator;
476        self
477    }
478
479    pub fn indicator_color(mut self, color: Color) -> Self {
480        if let Some(indicator) = self.indicator.as_mut() {
481            indicator.color = color;
482        }
483        self
484    }
485
486    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
487        self.indicator_border_color = color;
488        self
489    }
490}
491
492impl RenderOnce for IconWithIndicator {
493    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
494        let indicator_border_color = self
495            .indicator_border_color
496            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
497
498        div()
499            .relative()
500            .child(self.icon)
501            .when_some(self.indicator, |this, indicator| {
502                this.child(
503                    div()
504                        .absolute()
505                        .size_2p5()
506                        .border_2()
507                        .border_color(indicator_border_color)
508                        .rounded_full()
509                        .bottom_neg_0p5()
510                        .right_neg_0p5()
511                        .child(indicator),
512                )
513            })
514    }
515}
516
517// View this component preview using `workspace: open component-preview`
518impl ComponentPreview for Icon {
519    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
520        v_flex()
521            .gap_6()
522            .children(vec![
523                example_group_with_title(
524                    "Sizes",
525                    vec![
526                        single_example("Default", Icon::new(IconName::Star).into_any_element()),
527                        single_example(
528                            "Small",
529                            Icon::new(IconName::Star)
530                                .size(IconSize::Small)
531                                .into_any_element(),
532                        ),
533                        single_example(
534                            "Large",
535                            Icon::new(IconName::Star)
536                                .size(IconSize::XLarge)
537                                .into_any_element(),
538                        ),
539                    ],
540                ),
541                example_group_with_title(
542                    "Colors",
543                    vec![
544                        single_example("Default", Icon::new(IconName::Bell).into_any_element()),
545                        single_example(
546                            "Custom Color",
547                            Icon::new(IconName::Bell)
548                                .color(Color::Error)
549                                .into_any_element(),
550                        ),
551                    ],
552                ),
553            ])
554            .into_any_element()
555    }
556}