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