icon.rs

  1#![allow(missing_docs)]
  2
  3mod decorated_icon;
  4mod icon_decoration;
  5
  6use std::path::{Path, PathBuf};
  7use std::sync::Arc;
  8
  9pub use decorated_icon::*;
 10use gpui::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
 11pub use icon_decoration::*;
 12use serde::{Deserialize, Serialize};
 13use strum::{EnumIter, EnumString, IntoStaticStr};
 14use ui_macros::DerivePathStr;
 15
 16use crate::{prelude::*, Indicator};
 17
 18#[derive(IntoElement)]
 19pub enum AnyIcon {
 20    Icon(Icon),
 21    AnimatedIcon(AnimationElement<Icon>),
 22}
 23
 24impl AnyIcon {
 25    /// Returns a new [`AnyIcon`] after applying the given mapping function
 26    /// to the contained [`Icon`].
 27    pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
 28        match self {
 29            Self::Icon(icon) => Self::Icon(f(icon)),
 30            Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
 31        }
 32    }
 33}
 34
 35impl From<Icon> for AnyIcon {
 36    fn from(value: Icon) -> Self {
 37        Self::Icon(value)
 38    }
 39}
 40
 41impl From<AnimationElement<Icon>> for AnyIcon {
 42    fn from(value: AnimationElement<Icon>) -> Self {
 43        Self::AnimatedIcon(value)
 44    }
 45}
 46
 47impl RenderOnce for AnyIcon {
 48    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
 49        match self {
 50            Self::Icon(icon) => icon.into_any_element(),
 51            Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
 52        }
 53    }
 54}
 55
 56#[derive(Default, PartialEq, Copy, Clone)]
 57pub enum IconSize {
 58    /// 10px
 59    Indicator,
 60    /// 12px
 61    XSmall,
 62    /// 14px
 63    Small,
 64    #[default]
 65    /// 16px
 66    Medium,
 67    /// 48px
 68    XLarge,
 69    Custom(Rems),
 70}
 71
 72impl IconSize {
 73    pub fn rems(self) -> Rems {
 74        match self {
 75            IconSize::Indicator => rems_from_px(10.),
 76            IconSize::XSmall => rems_from_px(12.),
 77            IconSize::Small => rems_from_px(14.),
 78            IconSize::Medium => rems_from_px(16.),
 79            IconSize::XLarge => rems_from_px(48.),
 80            IconSize::Custom(size) => size,
 81        }
 82    }
 83
 84    /// Returns the individual components of the square that contains this [`IconSize`].
 85    ///
 86    /// The returned tuple contains:
 87    ///   1. The length of one side of the square
 88    ///   2. The padding of one side of the square
 89    pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
 90        let icon_size = self.rems() * window.rem_size();
 91        let padding = match self {
 92            IconSize::Indicator => DynamicSpacing::Base00.px(cx),
 93            IconSize::XSmall => DynamicSpacing::Base02.px(cx),
 94            IconSize::Small => DynamicSpacing::Base02.px(cx),
 95            IconSize::Medium => DynamicSpacing::Base02.px(cx),
 96            IconSize::XLarge => DynamicSpacing::Base02.px(cx),
 97            // TODO: Wire into dynamic spacing
 98            IconSize::Custom(size) => size.to_pixels(window.rem_size()),
 99        };
100
101        (icon_size, padding)
102    }
103
104    /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
105    pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels {
106        let (icon_size, padding) = self.square_components(window, cx);
107
108        icon_size + padding * 2.
109    }
110}
111
112#[derive(
113    Debug,
114    PartialEq,
115    Eq,
116    Copy,
117    Clone,
118    EnumIter,
119    EnumString,
120    IntoStaticStr,
121    Serialize,
122    Deserialize,
123    DerivePathStr,
124)]
125#[strum(serialize_all = "snake_case")]
126#[path_str(prefix = "icons", suffix = ".svg")]
127pub enum IconName {
128    Ai,
129    AiAnthropic,
130    AiAnthropicHosted,
131    AiDeepSeek,
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    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    Mic,
241    MicMute,
242    Microscope,
243    Minimize,
244    Option,
245    PageDown,
246    PageUp,
247    PanelLeft,
248    PanelRight,
249    Pencil,
250    Person,
251    PersonCircle,
252    PhoneIncoming,
253    Pin,
254    Play,
255    Plus,
256    PocketKnife,
257    Public,
258    PullRequest,
259    Quote,
260    RefreshTitle,
261    Regex,
262    ReplNeutral,
263    Replace,
264    ReplaceAll,
265    ReplaceNext,
266    ReplyArrowRight,
267    Rerun,
268    Return,
269    Reveal,
270    RotateCcw,
271    RotateCw,
272    Route,
273    Save,
274    Screen,
275    SearchCode,
276    SearchSelection,
277    SelectAll,
278    Server,
279    Settings,
280    SettingsAlt,
281    Shift,
282    Slash,
283    SlashSquare,
284    Sliders,
285    SlidersVertical,
286    Snip,
287    Space,
288    Sparkle,
289    SparkleAlt,
290    SparkleFilled,
291    Spinner,
292    Split,
293    SquareDot,
294    SquareMinus,
295    SquarePlus,
296    Star,
297    StarFilled,
298    Stop,
299    Strikethrough,
300    Supermaven,
301    SupermavenDisabled,
302    SupermavenError,
303    SupermavenInit,
304    SwatchBook,
305    Tab,
306    Terminal,
307    TextSnippet,
308    ThumbsUp,
309    ThumbsDown,
310    Trash,
311    TrashAlt,
312    Triangle,
313    TriangleRight,
314    Undo,
315    Unpin,
316    Update,
317    UserGroup,
318    Visible,
319    Wand,
320    Warning,
321    WholeWord,
322    X,
323    XCircle,
324    ZedAssistant,
325    ZedAssistant2,
326    ZedAssistantFilled,
327    ZedPredict,
328    ZedPredictUp,
329    ZedPredictDown,
330    ZedPredictDisabled,
331    ZedXCopilot,
332}
333
334impl From<IconName> for Icon {
335    fn from(icon: IconName) -> Self {
336        Icon::new(icon)
337    }
338}
339
340/// The source of an icon.
341enum IconSource {
342    /// An SVG embedded in the Zed binary.
343    Svg(SharedString),
344    /// An image file located at the specified path.
345    ///
346    /// Currently our SVG renderer is missing support for the following features:
347    /// 1. Loading SVGs from external files.
348    /// 2. Rendering polychrome SVGs.
349    ///
350    /// In order to support icon themes, we render the icons as images instead.
351    Image(Arc<Path>),
352}
353
354impl IconSource {
355    fn from_path(path: impl Into<SharedString>) -> Self {
356        let path = path.into();
357        if path.starts_with("icons/file_icons") {
358            Self::Svg(path)
359        } else {
360            Self::Image(Arc::from(PathBuf::from(path.as_ref())))
361        }
362    }
363}
364
365#[derive(IntoElement, IntoComponent)]
366pub struct Icon {
367    source: IconSource,
368    color: Color,
369    size: Rems,
370    transformation: Transformation,
371}
372
373impl Icon {
374    pub fn new(icon: IconName) -> Self {
375        Self {
376            source: IconSource::Svg(icon.path().into()),
377            color: Color::default(),
378            size: IconSize::default().rems(),
379            transformation: Transformation::default(),
380        }
381    }
382
383    pub fn from_path(path: impl Into<SharedString>) -> Self {
384        Self {
385            source: IconSource::from_path(path),
386            color: Color::default(),
387            size: IconSize::default().rems(),
388            transformation: Transformation::default(),
389        }
390    }
391
392    pub fn color(mut self, color: Color) -> Self {
393        self.color = color;
394        self
395    }
396
397    pub fn size(mut self, size: IconSize) -> Self {
398        self.size = size.rems();
399        self
400    }
401
402    /// Sets a custom size for the icon, in [`Rems`].
403    ///
404    /// Not to be exposed outside of the `ui` crate.
405    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
406        self.size = size;
407        self
408    }
409
410    pub fn transform(mut self, transformation: Transformation) -> Self {
411        self.transformation = transformation;
412        self
413    }
414}
415
416impl RenderOnce for Icon {
417    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
418        match self.source {
419            IconSource::Svg(path) => svg()
420                .with_transformation(self.transformation)
421                .size(self.size)
422                .flex_none()
423                .path(path)
424                .text_color(self.color.color(cx))
425                .into_any_element(),
426            IconSource::Image(path) => img(path)
427                .size(self.size)
428                .flex_none()
429                .text_color(self.color.color(cx))
430                .into_any_element(),
431        }
432    }
433}
434
435#[derive(IntoElement)]
436pub struct IconWithIndicator {
437    icon: Icon,
438    indicator: Option<Indicator>,
439    indicator_border_color: Option<Hsla>,
440}
441
442impl IconWithIndicator {
443    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
444        Self {
445            icon,
446            indicator,
447            indicator_border_color: None,
448        }
449    }
450
451    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
452        self.indicator = indicator;
453        self
454    }
455
456    pub fn indicator_color(mut self, color: Color) -> Self {
457        if let Some(indicator) = self.indicator.as_mut() {
458            indicator.color = color;
459        }
460        self
461    }
462
463    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
464        self.indicator_border_color = color;
465        self
466    }
467}
468
469impl RenderOnce for IconWithIndicator {
470    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
471        let indicator_border_color = self
472            .indicator_border_color
473            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
474
475        div()
476            .relative()
477            .child(self.icon)
478            .when_some(self.indicator, |this, indicator| {
479                this.child(
480                    div()
481                        .absolute()
482                        .size_2p5()
483                        .border_2()
484                        .border_color(indicator_border_color)
485                        .rounded_full()
486                        .bottom_neg_0p5()
487                        .right_neg_0p5()
488                        .child(indicator),
489                )
490            })
491    }
492}
493
494// View this component preview using `workspace: open component-preview`
495impl ComponentPreview for Icon {
496    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
497        v_flex()
498            .gap_6()
499            .children(vec![
500                example_group_with_title(
501                    "Sizes",
502                    vec![
503                        single_example("Default", Icon::new(IconName::Star).into_any_element()),
504                        single_example(
505                            "Small",
506                            Icon::new(IconName::Star)
507                                .size(IconSize::Small)
508                                .into_any_element(),
509                        ),
510                        single_example(
511                            "Large",
512                            Icon::new(IconName::Star)
513                                .size(IconSize::XLarge)
514                                .into_any_element(),
515                        ),
516                    ],
517                ),
518                example_group_with_title(
519                    "Colors",
520                    vec![
521                        single_example("Default", Icon::new(IconName::Bell).into_any_element()),
522                        single_example(
523                            "Custom Color",
524                            Icon::new(IconName::Bell)
525                                .color(Color::Error)
526                                .into_any_element(),
527                        ),
528                    ],
529                ),
530            ])
531            .into_any_element()
532    }
533}