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