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    ZedPredictDisabled,
327    ZedXCopilot,
328}
329
330impl From<IconName> for Icon {
331    fn from(icon: IconName) -> Self {
332        Icon::new(icon)
333    }
334}
335
336/// The source of an icon.
337enum IconSource {
338    /// An SVG embedded in the Zed binary.
339    Svg(SharedString),
340    /// An image file located at the specified path.
341    ///
342    /// Currently our SVG renderer is missing support for the following features:
343    /// 1. Loading SVGs from external files.
344    /// 2. Rendering polychrome SVGs.
345    ///
346    /// In order to support icon themes, we render the icons as images instead.
347    Image(Arc<Path>),
348}
349
350impl IconSource {
351    fn from_path(path: impl Into<SharedString>) -> Self {
352        let path = path.into();
353        if path.starts_with("icons/file_icons") {
354            Self::Svg(path)
355        } else {
356            Self::Image(Arc::from(PathBuf::from(path.as_ref())))
357        }
358    }
359}
360
361#[derive(IntoElement)]
362pub struct Icon {
363    source: IconSource,
364    color: Color,
365    size: Rems,
366    transformation: Transformation,
367}
368
369impl Icon {
370    pub fn new(icon: IconName) -> Self {
371        Self {
372            source: IconSource::Svg(icon.path().into()),
373            color: Color::default(),
374            size: IconSize::default().rems(),
375            transformation: Transformation::default(),
376        }
377    }
378
379    pub fn from_path(path: impl Into<SharedString>) -> Self {
380        Self {
381            source: IconSource::from_path(path),
382            color: Color::default(),
383            size: IconSize::default().rems(),
384            transformation: Transformation::default(),
385        }
386    }
387
388    pub fn color(mut self, color: Color) -> Self {
389        self.color = color;
390        self
391    }
392
393    pub fn size(mut self, size: IconSize) -> Self {
394        self.size = size.rems();
395        self
396    }
397
398    /// Sets a custom size for the icon, in [`Rems`].
399    ///
400    /// Not to be exposed outside of the `ui` crate.
401    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
402        self.size = size;
403        self
404    }
405
406    pub fn transform(mut self, transformation: Transformation) -> Self {
407        self.transformation = transformation;
408        self
409    }
410}
411
412impl RenderOnce for Icon {
413    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
414        match self.source {
415            IconSource::Svg(path) => svg()
416                .with_transformation(self.transformation)
417                .size(self.size)
418                .flex_none()
419                .path(path)
420                .text_color(self.color.color(cx))
421                .into_any_element(),
422            IconSource::Image(path) => img(path)
423                .size(self.size)
424                .flex_none()
425                .text_color(self.color.color(cx))
426                .into_any_element(),
427        }
428    }
429}
430
431#[derive(IntoElement)]
432pub struct IconWithIndicator {
433    icon: Icon,
434    indicator: Option<Indicator>,
435    indicator_border_color: Option<Hsla>,
436}
437
438impl IconWithIndicator {
439    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
440        Self {
441            icon,
442            indicator,
443            indicator_border_color: None,
444        }
445    }
446
447    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
448        self.indicator = indicator;
449        self
450    }
451
452    pub fn indicator_color(mut self, color: Color) -> Self {
453        if let Some(indicator) = self.indicator.as_mut() {
454            indicator.color = color;
455        }
456        self
457    }
458
459    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
460        self.indicator_border_color = color;
461        self
462    }
463}
464
465impl RenderOnce for IconWithIndicator {
466    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
467        let indicator_border_color = self
468            .indicator_border_color
469            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
470
471        div()
472            .relative()
473            .child(self.icon)
474            .when_some(self.indicator, |this, indicator| {
475                this.child(
476                    div()
477                        .absolute()
478                        .size_2p5()
479                        .border_2()
480                        .border_color(indicator_border_color)
481                        .rounded_full()
482                        .bottom_neg_0p5()
483                        .right_neg_0p5()
484                        .child(indicator),
485                )
486            })
487    }
488}
489
490impl ComponentPreview for Icon {
491    fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Icon>> {
492        let arrow_icons = vec![
493            IconName::ArrowDown,
494            IconName::ArrowLeft,
495            IconName::ArrowRight,
496            IconName::ArrowUp,
497            IconName::ArrowCircle,
498        ];
499
500        vec![example_group_with_title(
501            "Arrow Icons",
502            arrow_icons
503                .into_iter()
504                .map(|icon| {
505                    let name = format!("{:?}", icon).to_string();
506                    ComponentExample::new(name, Icon::new(icon))
507                })
508                .collect(),
509        )]
510    }
511}