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