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