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