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    ArrowUp,
144    ArrowUpFromLine,
145    ArrowUpRight,
146    ArrowUpRightAlt,
147    AtSign,
148    AudioOff,
149    AudioOn,
150    Backspace,
151    Bell,
152    BellDot,
153    BellOff,
154    BellRing,
155    Blocks,
156    Bolt,
157    Book,
158    BookCopy,
159    BookPlus,
160    Brain,
161    CaseSensitive,
162    Check,
163    ChevronDown,
164    /// This chevron indicates a popover menu.
165    ChevronDownSmall,
166    ChevronLeft,
167    ChevronRight,
168    ChevronUp,
169    ChevronUpDown,
170    Circle,
171    Close,
172    Code,
173    Command,
174    Context,
175    Control,
176    Copilot,
177    CopilotDisabled,
178    CopilotError,
179    CopilotInit,
180    Copy,
181    CountdownTimer,
182    CursorIBeam,
183    Dash,
184    DebugBreakpoint,
185    DebugIgnoreBreakpoints,
186    DebugPause,
187    DebugContinue,
188    DebugStepOver,
189    DebugStepInto,
190    DebugStepOut,
191    DebugStepBack,
192    DebugRestart,
193    Debug,
194    DebugStop,
195    DebugDisconnect,
196    DebugLogBreakpoint,
197    DatabaseZap,
198    Delete,
199    Diff,
200    Disconnected,
201    Download,
202    Ellipsis,
203    EllipsisVertical,
204    Envelope,
205    Eraser,
206    Escape,
207    ExpandVertical,
208    Exit,
209    ExternalLink,
210    ExpandUp,
211    ExpandDown,
212    Eye,
213    File,
214    FileCode,
215    FileDoc,
216    FileDiff,
217    FileGeneric,
218    FileGit,
219    FileLock,
220    FileRust,
221    FileSearch,
222    FileText,
223    FileToml,
224    FileTree,
225    Filter,
226    Folder,
227    FolderOpen,
228    FolderX,
229    Font,
230    FontSize,
231    FontWeight,
232    GenericClose,
233    GenericMaximize,
234    GenericMinimize,
235    GenericRestore,
236    Github,
237    Globe,
238    GitBranch,
239    GitBranchSmall,
240    Hash,
241    HistoryRerun,
242    Indicator,
243    Info,
244    InlayHint,
245    Keyboard,
246    Library,
247    LineHeight,
248    Link,
249    ListTree,
250    ListX,
251    LockOutlined,
252    MagnifyingGlass,
253    MailOpen,
254    Maximize,
255    Menu,
256    MessageBubbles,
257    MessageCircle,
258    Cloud,
259    Mic,
260    MicMute,
261    Microscope,
262    Minimize,
263    Option,
264    PageDown,
265    PageUp,
266    PanelLeft,
267    PanelRight,
268    Pencil,
269    Person,
270    PersonCircle,
271    PhoneIncoming,
272    Pin,
273    Play,
274    Plus,
275    PocketKnife,
276    Public,
277    PullRequest,
278    Quote,
279    RefreshTitle,
280    Regex,
281    ReplNeutral,
282    Replace,
283    ReplaceAll,
284    ReplaceNext,
285    ReplyArrowRight,
286    Rerun,
287    Return,
288    Reveal,
289    RotateCcw,
290    RotateCw,
291    Route,
292    Save,
293    Screen,
294    SearchCode,
295    SearchSelection,
296    SelectAll,
297    Server,
298    Settings,
299    SettingsAlt,
300    Shift,
301    Slash,
302    SlashSquare,
303    Sliders,
304    SlidersVertical,
305    Snip,
306    Space,
307    Sparkle,
308    SparkleAlt,
309    SparkleFilled,
310    Spinner,
311    Split,
312    SquareDot,
313    SquareMinus,
314    SquarePlus,
315    Star,
316    StarFilled,
317    Stop,
318    Strikethrough,
319    Supermaven,
320    SupermavenDisabled,
321    SupermavenError,
322    SupermavenInit,
323    SwatchBook,
324    Tab,
325    Terminal,
326    TextSnippet,
327    ThumbsUp,
328    ThumbsDown,
329    Trash,
330    TrashAlt,
331    Triangle,
332    TriangleRight,
333    Undo,
334    Unpin,
335    Update,
336    UserGroup,
337    Visible,
338    Wand,
339    Warning,
340    WholeWord,
341    X,
342    XCircle,
343    ZedAssistant,
344    ZedAssistant2,
345    ZedAssistantFilled,
346    ZedPredict,
347    ZedPredictUp,
348    ZedPredictDown,
349    ZedPredictDisabled,
350    ZedPredictError,
351    ZedXCopilot,
352}
353
354impl From<IconName> for Icon {
355    fn from(icon: IconName) -> Self {
356        Icon::new(icon)
357    }
358}
359
360/// The source of an icon.
361enum IconSource {
362    /// An SVG embedded in the Zed binary.
363    Svg(SharedString),
364    /// An image file located at the specified path.
365    ///
366    /// Currently our SVG renderer is missing support for the following features:
367    /// 1. Loading SVGs from external files.
368    /// 2. Rendering polychrome SVGs.
369    ///
370    /// In order to support icon themes, we render the icons as images instead.
371    Image(Arc<Path>),
372}
373
374impl IconSource {
375    fn from_path(path: impl Into<SharedString>) -> Self {
376        let path = path.into();
377        if path.starts_with("icons/file_icons") {
378            Self::Svg(path)
379        } else {
380            Self::Image(Arc::from(PathBuf::from(path.as_ref())))
381        }
382    }
383}
384
385#[derive(IntoElement, IntoComponent)]
386pub struct Icon {
387    source: IconSource,
388    color: Color,
389    size: Rems,
390    transformation: Transformation,
391}
392
393impl Icon {
394    pub fn new(icon: IconName) -> Self {
395        Self {
396            source: IconSource::Svg(icon.path().into()),
397            color: Color::default(),
398            size: IconSize::default().rems(),
399            transformation: Transformation::default(),
400        }
401    }
402
403    pub fn from_path(path: impl Into<SharedString>) -> Self {
404        Self {
405            source: IconSource::from_path(path),
406            color: Color::default(),
407            size: IconSize::default().rems(),
408            transformation: Transformation::default(),
409        }
410    }
411
412    pub fn color(mut self, color: Color) -> Self {
413        self.color = color;
414        self
415    }
416
417    pub fn size(mut self, size: IconSize) -> Self {
418        self.size = size.rems();
419        self
420    }
421
422    /// Sets a custom size for the icon, in [`Rems`].
423    ///
424    /// Not to be exposed outside of the `ui` crate.
425    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
426        self.size = size;
427        self
428    }
429
430    pub fn transform(mut self, transformation: Transformation) -> Self {
431        self.transformation = transformation;
432        self
433    }
434}
435
436impl RenderOnce for Icon {
437    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
438        match self.source {
439            IconSource::Svg(path) => svg()
440                .with_transformation(self.transformation)
441                .size(self.size)
442                .flex_none()
443                .path(path)
444                .text_color(self.color.color(cx))
445                .into_any_element(),
446            IconSource::Image(path) => img(path)
447                .size(self.size)
448                .flex_none()
449                .text_color(self.color.color(cx))
450                .into_any_element(),
451        }
452    }
453}
454
455#[derive(IntoElement)]
456pub struct IconWithIndicator {
457    icon: Icon,
458    indicator: Option<Indicator>,
459    indicator_border_color: Option<Hsla>,
460}
461
462impl IconWithIndicator {
463    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
464        Self {
465            icon,
466            indicator,
467            indicator_border_color: None,
468        }
469    }
470
471    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
472        self.indicator = indicator;
473        self
474    }
475
476    pub fn indicator_color(mut self, color: Color) -> Self {
477        if let Some(indicator) = self.indicator.as_mut() {
478            indicator.color = color;
479        }
480        self
481    }
482
483    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
484        self.indicator_border_color = color;
485        self
486    }
487}
488
489impl RenderOnce for IconWithIndicator {
490    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
491        let indicator_border_color = self
492            .indicator_border_color
493            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
494
495        div()
496            .relative()
497            .child(self.icon)
498            .when_some(self.indicator, |this, indicator| {
499                this.child(
500                    div()
501                        .absolute()
502                        .size_2p5()
503                        .border_2()
504                        .border_color(indicator_border_color)
505                        .rounded_full()
506                        .bottom_neg_0p5()
507                        .right_neg_0p5()
508                        .child(indicator),
509                )
510            })
511    }
512}
513
514// View this component preview using `workspace: open component-preview`
515impl ComponentPreview for Icon {
516    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
517        v_flex()
518            .gap_6()
519            .children(vec![
520                example_group_with_title(
521                    "Sizes",
522                    vec![
523                        single_example("Default", Icon::new(IconName::Star).into_any_element()),
524                        single_example(
525                            "Small",
526                            Icon::new(IconName::Star)
527                                .size(IconSize::Small)
528                                .into_any_element(),
529                        ),
530                        single_example(
531                            "Large",
532                            Icon::new(IconName::Star)
533                                .size(IconSize::XLarge)
534                                .into_any_element(),
535                        ),
536                    ],
537                ),
538                example_group_with_title(
539                    "Colors",
540                    vec![
541                        single_example("Default", Icon::new(IconName::Bell).into_any_element()),
542                        single_example(
543                            "Custom Color",
544                            Icon::new(IconName::Bell)
545                                .color(Color::Error)
546                                .into_any_element(),
547                        ),
548                    ],
549                ),
550            ])
551            .into_any_element()
552    }
553}