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    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    GitBranchSmall,
223    Hash,
224    HistoryRerun,
225    Indicator,
226    Info,
227    InlayHint,
228    Keyboard,
229    Library,
230    LineHeight,
231    Link,
232    ListTree,
233    ListX,
234    LockOutlined,
235    MagnifyingGlass,
236    MailOpen,
237    Maximize,
238    Menu,
239    MessageBubbles,
240    MessageCircle,
241    Cloud,
242    Mic,
243    MicMute,
244    Microscope,
245    Minimize,
246    Option,
247    PageDown,
248    PageUp,
249    PanelLeft,
250    PanelRight,
251    Pencil,
252    Person,
253    PersonCircle,
254    PhoneIncoming,
255    Pin,
256    Play,
257    Plus,
258    PocketKnife,
259    Public,
260    PullRequest,
261    Quote,
262    RefreshTitle,
263    Regex,
264    ReplNeutral,
265    Replace,
266    ReplaceAll,
267    ReplaceNext,
268    ReplyArrowRight,
269    Rerun,
270    Return,
271    Reveal,
272    RotateCcw,
273    RotateCw,
274    Route,
275    Save,
276    Screen,
277    SearchCode,
278    SearchSelection,
279    SelectAll,
280    Server,
281    Settings,
282    SettingsAlt,
283    Shift,
284    Slash,
285    SlashSquare,
286    Sliders,
287    SlidersVertical,
288    Snip,
289    Space,
290    Sparkle,
291    SparkleAlt,
292    SparkleFilled,
293    Spinner,
294    Split,
295    SquareDot,
296    SquareMinus,
297    SquarePlus,
298    Star,
299    StarFilled,
300    Stop,
301    Strikethrough,
302    Supermaven,
303    SupermavenDisabled,
304    SupermavenError,
305    SupermavenInit,
306    SwatchBook,
307    Tab,
308    Terminal,
309    TextSnippet,
310    ThumbsUp,
311    ThumbsDown,
312    Trash,
313    TrashAlt,
314    Triangle,
315    TriangleRight,
316    Undo,
317    Unpin,
318    Update,
319    UserGroup,
320    Visible,
321    Wand,
322    Warning,
323    WholeWord,
324    X,
325    XCircle,
326    ZedAssistant,
327    ZedAssistant2,
328    ZedAssistantFilled,
329    ZedPredict,
330    ZedPredictUp,
331    ZedPredictDown,
332    ZedPredictDisabled,
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}