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