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, Hsla, IntoElement, Rems, Transformation};
 11pub use icon_decoration::*;
 12use serde::{Deserialize, Serialize};
 13use strum::{EnumIter, EnumString, IntoStaticStr};
 14use ui_macros::DerivePathStr;
 15
 16use crate::{
 17    prelude::*,
 18    traits::component_preview::{ComponentExample, ComponentPreview},
 19    Indicator,
 20};
 21
 22#[derive(IntoElement)]
 23pub enum AnyIcon {
 24    Icon(Icon),
 25    AnimatedIcon(AnimationElement<Icon>),
 26}
 27
 28impl AnyIcon {
 29    /// Returns a new [`AnyIcon`] after applying the given mapping function
 30    /// to the contained [`Icon`].
 31    pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
 32        match self {
 33            Self::Icon(icon) => Self::Icon(f(icon)),
 34            Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
 35        }
 36    }
 37}
 38
 39impl From<Icon> for AnyIcon {
 40    fn from(value: Icon) -> Self {
 41        Self::Icon(value)
 42    }
 43}
 44
 45impl From<AnimationElement<Icon>> for AnyIcon {
 46    fn from(value: AnimationElement<Icon>) -> Self {
 47        Self::AnimatedIcon(value)
 48    }
 49}
 50
 51impl RenderOnce for AnyIcon {
 52    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
 53        match self {
 54            Self::Icon(icon) => icon.into_any_element(),
 55            Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
 56        }
 57    }
 58}
 59
 60#[derive(Default, PartialEq, Copy, Clone)]
 61pub enum IconSize {
 62    /// 10px
 63    Indicator,
 64    /// 12px
 65    XSmall,
 66    /// 14px
 67    Small,
 68    #[default]
 69    /// 16px
 70    Medium,
 71    /// 48px
 72    XLarge,
 73}
 74
 75impl IconSize {
 76    pub fn rems(self) -> Rems {
 77        match self {
 78            IconSize::Indicator => rems_from_px(10.),
 79            IconSize::XSmall => rems_from_px(12.),
 80            IconSize::Small => rems_from_px(14.),
 81            IconSize::Medium => rems_from_px(16.),
 82            IconSize::XLarge => rems_from_px(48.),
 83        }
 84    }
 85
 86    /// Returns the individual components of the square that contains this [`IconSize`].
 87    ///
 88    /// The returned tuple contains:
 89    ///   1. The length of one side of the square
 90    ///   2. The padding of one side of the square
 91    pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
 92        let icon_size = self.rems() * window.rem_size();
 93        let padding = match self {
 94            IconSize::Indicator => DynamicSpacing::Base00.px(cx),
 95            IconSize::XSmall => DynamicSpacing::Base02.px(cx),
 96            IconSize::Small => DynamicSpacing::Base02.px(cx),
 97            IconSize::Medium => DynamicSpacing::Base02.px(cx),
 98            IconSize::XLarge => DynamicSpacing::Base02.px(cx),
 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    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    IndicatorX,
224    Info,
225    InlayHint,
226    Keyboard,
227    Library,
228    LineHeight,
229    Link,
230    ListTree,
231    ListX,
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    ZedXCopilot,
327}
328
329impl From<IconName> for Icon {
330    fn from(icon: IconName) -> Self {
331        Icon::new(icon)
332    }
333}
334
335/// The source of an icon.
336enum IconSource {
337    /// An SVG embedded in the Zed binary.
338    Svg(SharedString),
339    /// An image file located at the specified path.
340    ///
341    /// Currently our SVG renderer is missing support for the following features:
342    /// 1. Loading SVGs from external files.
343    /// 2. Rendering polychrome SVGs.
344    ///
345    /// In order to support icon themes, we render the icons as images instead.
346    Image(Arc<Path>),
347}
348
349impl IconSource {
350    fn from_path(path: impl Into<SharedString>) -> Self {
351        let path = path.into();
352        if path.starts_with("icons/file_icons") {
353            Self::Svg(path)
354        } else {
355            Self::Image(Arc::from(PathBuf::from(path.as_ref())))
356        }
357    }
358}
359
360#[derive(IntoElement)]
361pub struct Icon {
362    source: IconSource,
363    color: Color,
364    size: Rems,
365    transformation: Transformation,
366}
367
368impl Icon {
369    pub fn new(icon: IconName) -> Self {
370        Self {
371            source: IconSource::Svg(icon.path().into()),
372            color: Color::default(),
373            size: IconSize::default().rems(),
374            transformation: Transformation::default(),
375        }
376    }
377
378    pub fn from_path(path: impl Into<SharedString>) -> Self {
379        Self {
380            source: IconSource::from_path(path),
381            color: Color::default(),
382            size: IconSize::default().rems(),
383            transformation: Transformation::default(),
384        }
385    }
386
387    pub fn color(mut self, color: Color) -> Self {
388        self.color = color;
389        self
390    }
391
392    pub fn size(mut self, size: IconSize) -> Self {
393        self.size = size.rems();
394        self
395    }
396
397    /// Sets a custom size for the icon, in [`Rems`].
398    ///
399    /// Not to be exposed outside of the `ui` crate.
400    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
401        self.size = size;
402        self
403    }
404
405    pub fn transform(mut self, transformation: Transformation) -> Self {
406        self.transformation = transformation;
407        self
408    }
409}
410
411impl RenderOnce for Icon {
412    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
413        match self.source {
414            IconSource::Svg(path) => svg()
415                .with_transformation(self.transformation)
416                .size(self.size)
417                .flex_none()
418                .path(path)
419                .text_color(self.color.color(cx))
420                .into_any_element(),
421            IconSource::Image(path) => img(path)
422                .size(self.size)
423                .flex_none()
424                .text_color(self.color.color(cx))
425                .into_any_element(),
426        }
427    }
428}
429
430#[derive(IntoElement)]
431pub struct IconWithIndicator {
432    icon: Icon,
433    indicator: Option<Indicator>,
434    indicator_border_color: Option<Hsla>,
435}
436
437impl IconWithIndicator {
438    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
439        Self {
440            icon,
441            indicator,
442            indicator_border_color: None,
443        }
444    }
445
446    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
447        self.indicator = indicator;
448        self
449    }
450
451    pub fn indicator_color(mut self, color: Color) -> Self {
452        if let Some(indicator) = self.indicator.as_mut() {
453            indicator.color = color;
454        }
455        self
456    }
457
458    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
459        self.indicator_border_color = color;
460        self
461    }
462}
463
464impl RenderOnce for IconWithIndicator {
465    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
466        let indicator_border_color = self
467            .indicator_border_color
468            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
469
470        div()
471            .relative()
472            .child(self.icon)
473            .when_some(self.indicator, |this, indicator| {
474                this.child(
475                    div()
476                        .absolute()
477                        .size_2p5()
478                        .border_2()
479                        .border_color(indicator_border_color)
480                        .rounded_full()
481                        .bottom_neg_0p5()
482                        .right_neg_0p5()
483                        .child(indicator),
484                )
485            })
486    }
487}
488
489impl ComponentPreview for Icon {
490    fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Icon>> {
491        let arrow_icons = vec![
492            IconName::ArrowDown,
493            IconName::ArrowLeft,
494            IconName::ArrowRight,
495            IconName::ArrowUp,
496            IconName::ArrowCircle,
497        ];
498
499        vec![example_group_with_title(
500            "Arrow Icons",
501            arrow_icons
502                .into_iter()
503                .map(|icon| {
504                    let name = format!("{:?}", icon).to_string();
505                    ComponentExample::new(name, Icon::new(icon))
506                })
507                .collect(),
508        )]
509    }
510}