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    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    Hash,
222    HistoryRerun,
223    Indicator,
224    IndicatorX,
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    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
492impl ComponentPreview for Icon {
493    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
494        v_flex()
495            .gap_6()
496            .children(vec![
497                example_group_with_title(
498                    "Sizes",
499                    vec![
500                        single_example("Default", Icon::new(IconName::Star).into_any_element()),
501                        single_example(
502                            "Small",
503                            Icon::new(IconName::Star)
504                                .size(IconSize::Small)
505                                .into_any_element(),
506                        ),
507                        single_example(
508                            "Large",
509                            Icon::new(IconName::Star)
510                                .size(IconSize::XLarge)
511                                .into_any_element(),
512                        ),
513                    ],
514                ),
515                example_group_with_title(
516                    "Colors",
517                    vec![
518                        single_example("Default", Icon::new(IconName::Bell).into_any_element()),
519                        single_example(
520                            "Custom Color",
521                            Icon::new(IconName::Bell)
522                                .color(Color::Error)
523                                .into_any_element(),
524                        ),
525                    ],
526                ),
527            ])
528            .into_any_element()
529    }
530}