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