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    Custom(Pixels),
 74}
 75
 76impl IconSize {
 77    pub fn rems(self) -> Rems {
 78        match self {
 79            IconSize::Indicator => rems_from_px(10.),
 80            IconSize::XSmall => rems_from_px(12.),
 81            IconSize::Small => rems_from_px(14.),
 82            IconSize::Medium => rems_from_px(16.),
 83            IconSize::XLarge => rems_from_px(48.),
 84            IconSize::Custom(size) => rems_from_px(size.into()),
 85        }
 86    }
 87
 88    /// Returns the individual components of the square that contains this [`IconSize`].
 89    ///
 90    /// The returned tuple contains:
 91    ///   1. The length of one side of the square
 92    ///   2. The padding of one side of the square
 93    pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
 94        let icon_size = self.rems() * window.rem_size();
 95        let padding = match self {
 96            IconSize::Indicator => DynamicSpacing::Base00.px(cx),
 97            IconSize::XSmall => DynamicSpacing::Base02.px(cx),
 98            IconSize::Small => DynamicSpacing::Base02.px(cx),
 99            IconSize::Medium => DynamicSpacing::Base02.px(cx),
100            IconSize::XLarge => DynamicSpacing::Base02.px(cx),
101            // TODO: Wire into dynamic spacing
102            IconSize::Custom(size) => px(size.into()),
103        };
104
105        (icon_size, padding)
106    }
107
108    /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
109    pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels {
110        let (icon_size, padding) = self.square_components(window, cx);
111
112        icon_size + padding * 2.
113    }
114}
115
116#[derive(
117    Debug,
118    PartialEq,
119    Eq,
120    Copy,
121    Clone,
122    EnumIter,
123    EnumString,
124    IntoStaticStr,
125    Serialize,
126    Deserialize,
127    DerivePathStr,
128)]
129#[strum(serialize_all = "snake_case")]
130#[path_str(prefix = "icons", suffix = ".svg")]
131pub enum IconName {
132    Ai,
133    AiAnthropic,
134    AiAnthropicHosted,
135    AiDeepSeek,
136    AiGoogle,
137    AiLmStudio,
138    AiOllama,
139    AiOpenAi,
140    AiZed,
141    ArrowCircle,
142    ArrowDown,
143    ArrowDownFromLine,
144    ArrowLeft,
145    ArrowRight,
146    ArrowUp,
147    ArrowUpFromLine,
148    ArrowUpRight,
149    AtSign,
150    AudioOff,
151    AudioOn,
152    Backspace,
153    Bell,
154    BellDot,
155    BellOff,
156    BellRing,
157    Blocks,
158    Bolt,
159    Book,
160    BookCopy,
161    BookPlus,
162    CaseSensitive,
163    Check,
164    ChevronDown,
165    /// This chevron indicates a popover menu.
166    ChevronDownSmall,
167    ChevronLeft,
168    ChevronRight,
169    ChevronUp,
170    ChevronUpDown,
171    Circle,
172    Close,
173    Code,
174    Command,
175    Context,
176    Control,
177    Copilot,
178    CopilotDisabled,
179    CopilotError,
180    CopilotInit,
181    Copy,
182    CountdownTimer,
183    CursorIBeam,
184    Dash,
185    DatabaseZap,
186    Delete,
187    Diff,
188    Disconnected,
189    Download,
190    Ellipsis,
191    EllipsisVertical,
192    Envelope,
193    Eraser,
194    Escape,
195    ExpandVertical,
196    Exit,
197    ExternalLink,
198    Eye,
199    File,
200    FileCode,
201    FileDoc,
202    FileDiff,
203    FileGeneric,
204    FileGit,
205    FileLock,
206    FileRust,
207    FileSearch,
208    FileText,
209    FileToml,
210    FileTree,
211    Filter,
212    Folder,
213    FolderOpen,
214    FolderX,
215    Font,
216    FontSize,
217    FontWeight,
218    GenericClose,
219    GenericMaximize,
220    GenericMinimize,
221    GenericRestore,
222    Github,
223    Globe,
224    GitBranch,
225    Hash,
226    HistoryRerun,
227    Indicator,
228    IndicatorX,
229    Info,
230    InlayHint,
231    Keyboard,
232    Library,
233    LineHeight,
234    Link,
235    ListTree,
236    ListX,
237    LockOutlined,
238    MagnifyingGlass,
239    MailOpen,
240    Maximize,
241    Menu,
242    MessageBubbles,
243    MessageCircle,
244    Mic,
245    MicMute,
246    Microscope,
247    Minimize,
248    Option,
249    PageDown,
250    PageUp,
251    PanelLeft,
252    PanelRight,
253    Pencil,
254    Person,
255    PersonCircle,
256    PhoneIncoming,
257    Pin,
258    Play,
259    Plus,
260    PocketKnife,
261    Public,
262    PullRequest,
263    Quote,
264    RefreshTitle,
265    Regex,
266    ReplNeutral,
267    Replace,
268    ReplaceAll,
269    ReplaceNext,
270    ReplyArrowRight,
271    Rerun,
272    Return,
273    Reveal,
274    RotateCcw,
275    RotateCw,
276    Route,
277    Save,
278    Screen,
279    SearchCode,
280    SearchSelection,
281    SelectAll,
282    Server,
283    Settings,
284    SettingsAlt,
285    Shift,
286    Slash,
287    SlashSquare,
288    Sliders,
289    SlidersVertical,
290    Snip,
291    Space,
292    Sparkle,
293    SparkleAlt,
294    SparkleFilled,
295    Spinner,
296    Split,
297    SquareDot,
298    SquareMinus,
299    SquarePlus,
300    Star,
301    StarFilled,
302    Stop,
303    Strikethrough,
304    Supermaven,
305    SupermavenDisabled,
306    SupermavenError,
307    SupermavenInit,
308    SwatchBook,
309    Tab,
310    Terminal,
311    TextSnippet,
312    ThumbsUp,
313    ThumbsDown,
314    Trash,
315    TrashAlt,
316    Triangle,
317    TriangleRight,
318    Undo,
319    Unpin,
320    Update,
321    UserGroup,
322    Visible,
323    Wand,
324    Warning,
325    WholeWord,
326    X,
327    XCircle,
328    ZedAssistant,
329    ZedAssistant2,
330    ZedAssistantFilled,
331    ZedPredict,
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)]
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
496impl ComponentPreview for Icon {
497    fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Icon>> {
498        let arrow_icons = vec![
499            IconName::ArrowDown,
500            IconName::ArrowLeft,
501            IconName::ArrowRight,
502            IconName::ArrowUp,
503            IconName::ArrowCircle,
504        ];
505
506        vec![example_group_with_title(
507            "Arrow Icons",
508            arrow_icons
509                .into_iter()
510                .map(|icon| {
511                    let name = format!("{:?}", icon).to_string();
512                    ComponentExample::new(name, Icon::new(icon))
513                })
514                .collect(),
515        )]
516    }
517}