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    Hash,
222    HistoryRerun,
223    Indicator,
224    Info,
225    InlayHint,
226    Keyboard,
227    Library,
228    LineHeight,
229    Link,
230    ListTree,
231    ListX,
232    LockOutlined,
233    MagnifyingGlass,
234    MailOpen,
235    Maximize,
236    Menu,
237    MessageBubbles,
238    MessageCircle,
239    Mic,
240    MicMute,
241    Microscope,
242    Minimize,
243    Option,
244    PageDown,
245    PageUp,
246    PanelLeft,
247    PanelRight,
248    Pencil,
249    Person,
250    PersonCircle,
251    PhoneIncoming,
252    Pin,
253    Play,
254    Plus,
255    PocketKnife,
256    Public,
257    PullRequest,
258    Quote,
259    RefreshTitle,
260    Regex,
261    ReplNeutral,
262    Replace,
263    ReplaceAll,
264    ReplaceNext,
265    ReplyArrowRight,
266    Rerun,
267    Return,
268    Reveal,
269    RotateCcw,
270    RotateCw,
271    Route,
272    Save,
273    Screen,
274    SearchCode,
275    SearchSelection,
276    SelectAll,
277    Server,
278    Settings,
279    SettingsAlt,
280    Shift,
281    Slash,
282    SlashSquare,
283    Sliders,
284    SlidersVertical,
285    Snip,
286    Space,
287    Sparkle,
288    SparkleAlt,
289    SparkleFilled,
290    Spinner,
291    Split,
292    SquareDot,
293    SquareMinus,
294    SquarePlus,
295    Star,
296    StarFilled,
297    Stop,
298    Strikethrough,
299    Supermaven,
300    SupermavenDisabled,
301    SupermavenError,
302    SupermavenInit,
303    SwatchBook,
304    Tab,
305    Terminal,
306    TextSnippet,
307    ThumbsUp,
308    ThumbsDown,
309    Trash,
310    TrashAlt,
311    Triangle,
312    TriangleRight,
313    Undo,
314    Unpin,
315    Update,
316    UserGroup,
317    Visible,
318    Wand,
319    Warning,
320    WholeWord,
321    X,
322    XCircle,
323    ZedAssistant,
324    ZedAssistant2,
325    ZedAssistantFilled,
326    ZedPredict,
327    ZedPredictUp,
328    ZedPredictDown,
329    ZedPredictDisabled,
330    ZedXCopilot,
331}
332
333impl From<IconName> for Icon {
334    fn from(icon: IconName) -> Self {
335        Icon::new(icon)
336    }
337}
338
339/// The source of an icon.
340enum IconSource {
341    /// An SVG embedded in the Zed binary.
342    Svg(SharedString),
343    /// An image file located at the specified path.
344    ///
345    /// Currently our SVG renderer is missing support for the following features:
346    /// 1. Loading SVGs from external files.
347    /// 2. Rendering polychrome SVGs.
348    ///
349    /// In order to support icon themes, we render the icons as images instead.
350    Image(Arc<Path>),
351}
352
353impl IconSource {
354    fn from_path(path: impl Into<SharedString>) -> Self {
355        let path = path.into();
356        if path.starts_with("icons/file_icons") {
357            Self::Svg(path)
358        } else {
359            Self::Image(Arc::from(PathBuf::from(path.as_ref())))
360        }
361    }
362}
363
364#[derive(IntoElement, IntoComponent)]
365pub struct Icon {
366    source: IconSource,
367    color: Color,
368    size: Rems,
369    transformation: Transformation,
370}
371
372impl Icon {
373    pub fn new(icon: IconName) -> Self {
374        Self {
375            source: IconSource::Svg(icon.path().into()),
376            color: Color::default(),
377            size: IconSize::default().rems(),
378            transformation: Transformation::default(),
379        }
380    }
381
382    pub fn from_path(path: impl Into<SharedString>) -> Self {
383        Self {
384            source: IconSource::from_path(path),
385            color: Color::default(),
386            size: IconSize::default().rems(),
387            transformation: Transformation::default(),
388        }
389    }
390
391    pub fn color(mut self, color: Color) -> Self {
392        self.color = color;
393        self
394    }
395
396    pub fn size(mut self, size: IconSize) -> Self {
397        self.size = size.rems();
398        self
399    }
400
401    /// Sets a custom size for the icon, in [`Rems`].
402    ///
403    /// Not to be exposed outside of the `ui` crate.
404    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
405        self.size = size;
406        self
407    }
408
409    pub fn transform(mut self, transformation: Transformation) -> Self {
410        self.transformation = transformation;
411        self
412    }
413}
414
415impl RenderOnce for Icon {
416    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
417        match self.source {
418            IconSource::Svg(path) => svg()
419                .with_transformation(self.transformation)
420                .size(self.size)
421                .flex_none()
422                .path(path)
423                .text_color(self.color.color(cx))
424                .into_any_element(),
425            IconSource::Image(path) => img(path)
426                .size(self.size)
427                .flex_none()
428                .text_color(self.color.color(cx))
429                .into_any_element(),
430        }
431    }
432}
433
434#[derive(IntoElement)]
435pub struct IconWithIndicator {
436    icon: Icon,
437    indicator: Option<Indicator>,
438    indicator_border_color: Option<Hsla>,
439}
440
441impl IconWithIndicator {
442    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
443        Self {
444            icon,
445            indicator,
446            indicator_border_color: None,
447        }
448    }
449
450    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
451        self.indicator = indicator;
452        self
453    }
454
455    pub fn indicator_color(mut self, color: Color) -> Self {
456        if let Some(indicator) = self.indicator.as_mut() {
457            indicator.color = color;
458        }
459        self
460    }
461
462    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
463        self.indicator_border_color = color;
464        self
465    }
466}
467
468impl RenderOnce for IconWithIndicator {
469    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
470        let indicator_border_color = self
471            .indicator_border_color
472            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
473
474        div()
475            .relative()
476            .child(self.icon)
477            .when_some(self.indicator, |this, indicator| {
478                this.child(
479                    div()
480                        .absolute()
481                        .size_2p5()
482                        .border_2()
483                        .border_color(indicator_border_color)
484                        .rounded_full()
485                        .bottom_neg_0p5()
486                        .right_neg_0p5()
487                        .child(indicator),
488                )
489            })
490    }
491}
492
493// View this component preview using `workspace: open component-preview`
494impl ComponentPreview for Icon {
495    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
496        v_flex()
497            .gap_6()
498            .children(vec![
499                example_group_with_title(
500                    "Sizes",
501                    vec![
502                        single_example("Default", Icon::new(IconName::Star).into_any_element()),
503                        single_example(
504                            "Small",
505                            Icon::new(IconName::Star)
506                                .size(IconSize::Small)
507                                .into_any_element(),
508                        ),
509                        single_example(
510                            "Large",
511                            Icon::new(IconName::Star)
512                                .size(IconSize::XLarge)
513                                .into_any_element(),
514                        ),
515                    ],
516                ),
517                example_group_with_title(
518                    "Colors",
519                    vec![
520                        single_example("Default", Icon::new(IconName::Bell).into_any_element()),
521                        single_example(
522                            "Custom Color",
523                            Icon::new(IconName::Bell)
524                                .color(Color::Error)
525                                .into_any_element(),
526                        ),
527                    ],
528                ),
529            ])
530            .into_any_element()
531    }
532}