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