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    MagnifyingGlass,
238    MailOpen,
239    Maximize,
240    Menu,
241    MessageBubbles,
242    MessageCircle,
243    Mic,
244    MicMute,
245    Microscope,
246    Minimize,
247    Option,
248    PageDown,
249    PageUp,
250    PanelLeft,
251    PanelRight,
252    Pencil,
253    Person,
254    PersonCircle,
255    PhoneIncoming,
256    Pin,
257    Play,
258    Plus,
259    PocketKnife,
260    Public,
261    PullRequest,
262    Quote,
263    RefreshTitle,
264    Regex,
265    ReplNeutral,
266    Replace,
267    ReplaceAll,
268    ReplaceNext,
269    ReplyArrowRight,
270    Rerun,
271    Return,
272    Reveal,
273    RotateCcw,
274    RotateCw,
275    Route,
276    Save,
277    Screen,
278    SearchCode,
279    SearchSelection,
280    SelectAll,
281    Server,
282    Settings,
283    SettingsAlt,
284    Shift,
285    Slash,
286    SlashSquare,
287    Sliders,
288    SlidersVertical,
289    Snip,
290    Space,
291    Sparkle,
292    SparkleAlt,
293    SparkleFilled,
294    Spinner,
295    Split,
296    SquareDot,
297    SquareMinus,
298    SquarePlus,
299    Star,
300    StarFilled,
301    Stop,
302    Strikethrough,
303    Supermaven,
304    SupermavenDisabled,
305    SupermavenError,
306    SupermavenInit,
307    SwatchBook,
308    Tab,
309    Terminal,
310    TextSnippet,
311    ThumbsUp,
312    ThumbsDown,
313    Trash,
314    TrashAlt,
315    Triangle,
316    TriangleRight,
317    Undo,
318    Unpin,
319    Update,
320    UserGroup,
321    Visible,
322    Wand,
323    Warning,
324    WholeWord,
325    X,
326    XCircle,
327    ZedAssistant,
328    ZedAssistant2,
329    ZedAssistantFilled,
330    ZedPredict,
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)]
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
495impl ComponentPreview for Icon {
496    fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Icon>> {
497        let arrow_icons = vec![
498            IconName::ArrowDown,
499            IconName::ArrowLeft,
500            IconName::ArrowRight,
501            IconName::ArrowUp,
502            IconName::ArrowCircle,
503        ];
504
505        vec![example_group_with_title(
506            "Arrow Icons",
507            arrow_icons
508                .into_iter()
509                .map(|icon| {
510                    let name = format!("{:?}", icon).to_string();
511                    ComponentExample::new(name, Icon::new(icon))
512                })
513                .collect(),
514        )]
515    }
516}