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