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, _cx: &mut WindowContext) -> 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}
 72
 73impl IconSize {
 74    pub fn rems(self) -> Rems {
 75        match self {
 76            IconSize::Indicator => rems_from_px(10.),
 77            IconSize::XSmall => rems_from_px(12.),
 78            IconSize::Small => rems_from_px(14.),
 79            IconSize::Medium => rems_from_px(16.),
 80        }
 81    }
 82
 83    /// Returns the individual components of the square that contains this [`IconSize`].
 84    ///
 85    /// The returned tuple contains:
 86    ///   1. The length of one side of the square
 87    ///   2. The padding of one side of the square
 88    pub fn square_components(&self, cx: &mut WindowContext) -> (Pixels, Pixels) {
 89        let icon_size = self.rems() * cx.rem_size();
 90        let padding = match self {
 91            IconSize::Indicator => DynamicSpacing::Base00.px(cx),
 92            IconSize::XSmall => DynamicSpacing::Base02.px(cx),
 93            IconSize::Small => DynamicSpacing::Base02.px(cx),
 94            IconSize::Medium => DynamicSpacing::Base02.px(cx),
 95        };
 96
 97        (icon_size, padding)
 98    }
 99
100    /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
101    pub fn square(&self, cx: &mut WindowContext) -> Pixels {
102        let (icon_size, padding) = self.square_components(cx);
103
104        icon_size + padding * 2.
105    }
106}
107
108#[derive(
109    Debug,
110    PartialEq,
111    Eq,
112    Copy,
113    Clone,
114    EnumIter,
115    EnumString,
116    IntoStaticStr,
117    Serialize,
118    Deserialize,
119    DerivePathStr,
120)]
121#[strum(serialize_all = "snake_case")]
122#[path_str(prefix = "icons", suffix = ".svg")]
123pub enum IconName {
124    Ai,
125    AiAnthropic,
126    AiAnthropicHosted,
127    AiGoogle,
128    AiLmStudio,
129    AiOllama,
130    AiOpenAi,
131    AiZed,
132    ArrowCircle,
133    ArrowDown,
134    ArrowDownFromLine,
135    ArrowLeft,
136    ArrowRight,
137    ArrowUp,
138    ArrowUpFromLine,
139    ArrowUpRight,
140    AtSign,
141    AudioOff,
142    AudioOn,
143    Backspace,
144    Bell,
145    BellDot,
146    BellOff,
147    BellRing,
148    Blocks,
149    Bolt,
150    Book,
151    BookCopy,
152    BookPlus,
153    CaseSensitive,
154    Check,
155    ChevronDown,
156    /// This chevron indicates a popover menu.
157    ChevronDownSmall,
158    ChevronLeft,
159    ChevronRight,
160    ChevronUp,
161    ChevronUpDown,
162    Close,
163    Code,
164    Command,
165    Context,
166    Control,
167    Copilot,
168    CopilotDisabled,
169    CopilotError,
170    CopilotInit,
171    Copy,
172    CountdownTimer,
173    CursorIBeam,
174    Dash,
175    DatabaseZap,
176    Delete,
177    Diff,
178    Disconnected,
179    Download,
180    Ellipsis,
181    EllipsisVertical,
182    Envelope,
183    Eraser,
184    Escape,
185    ExpandVertical,
186    Exit,
187    ExternalLink,
188    Eye,
189    File,
190    FileCode,
191    FileDoc,
192    FileDiff,
193    FileGeneric,
194    FileGit,
195    FileLock,
196    FileRust,
197    FileSearch,
198    FileText,
199    FileToml,
200    FileTree,
201    Filter,
202    Folder,
203    FolderOpen,
204    FolderX,
205    Font,
206    FontSize,
207    FontWeight,
208    GenericClose,
209    GenericMaximize,
210    GenericMinimize,
211    GenericRestore,
212    Github,
213    Globe,
214    GitBranch,
215    Hash,
216    HistoryRerun,
217    Indicator,
218    IndicatorX,
219    Info,
220    InlayHint,
221    Keyboard,
222    Library,
223    LineHeight,
224    Link,
225    ListTree,
226    ListX,
227    MagnifyingGlass,
228    MailOpen,
229    Maximize,
230    Menu,
231    MessageBubbles,
232    MessageCircle,
233    Mic,
234    MicMute,
235    Microscope,
236    Minimize,
237    Option,
238    PageDown,
239    PageUp,
240    PanelLeft,
241    PanelRight,
242    Pencil,
243    Person,
244    PersonCircle,
245    PhoneIncoming,
246    Pin,
247    Play,
248    Plus,
249    PocketKnife,
250    Public,
251    PullRequest,
252    Quote,
253    RefreshTitle,
254    Regex,
255    ReplNeutral,
256    Replace,
257    ReplaceAll,
258    ReplaceNext,
259    ReplyArrowRight,
260    Rerun,
261    Return,
262    Reveal,
263    RotateCcw,
264    RotateCw,
265    Route,
266    Save,
267    Screen,
268    SearchCode,
269    SearchSelection,
270    SelectAll,
271    Server,
272    Settings,
273    SettingsAlt,
274    Shift,
275    Slash,
276    SlashSquare,
277    Sliders,
278    SlidersVertical,
279    Snip,
280    Space,
281    Sparkle,
282    SparkleAlt,
283    SparkleFilled,
284    Spinner,
285    Split,
286    SquareDot,
287    SquareMinus,
288    SquarePlus,
289    Star,
290    StarFilled,
291    Stop,
292    Strikethrough,
293    Supermaven,
294    SupermavenDisabled,
295    SupermavenError,
296    SupermavenInit,
297    SwatchBook,
298    Tab,
299    Terminal,
300    TextSnippet,
301    ThumbsUp,
302    ThumbsDown,
303    Trash,
304    TrashAlt,
305    Triangle,
306    TriangleRight,
307    Undo,
308    Unpin,
309    Update,
310    UserGroup,
311    Visible,
312    Wand,
313    Warning,
314    WholeWord,
315    X,
316    XCircle,
317    ZedAssistant,
318    ZedAssistant2,
319    ZedAssistantFilled,
320    ZedPredict,
321    ZedXCopilot,
322}
323
324impl From<IconName> for Icon {
325    fn from(icon: IconName) -> Self {
326        Icon::new(icon)
327    }
328}
329
330/// The source of an icon.
331enum IconSource {
332    /// An SVG embedded in the Zed binary.
333    Svg(SharedString),
334    /// An image file located at the specified path.
335    ///
336    /// Currently our SVG renderer is missing support for the following features:
337    /// 1. Loading SVGs from external files.
338    /// 2. Rendering polychrome SVGs.
339    ///
340    /// In order to support icon themes, we render the icons as images instead.
341    Image(Arc<Path>),
342}
343
344impl IconSource {
345    fn from_path(path: impl Into<SharedString>) -> Self {
346        let path = path.into();
347        if path.starts_with("icons/file_icons") {
348            Self::Svg(path)
349        } else {
350            Self::Image(Arc::from(PathBuf::from(path.as_ref())))
351        }
352    }
353}
354
355#[derive(IntoElement)]
356pub struct Icon {
357    source: IconSource,
358    color: Color,
359    size: Rems,
360    transformation: Transformation,
361}
362
363impl Icon {
364    pub fn new(icon: IconName) -> Self {
365        Self {
366            source: IconSource::Svg(icon.path().into()),
367            color: Color::default(),
368            size: IconSize::default().rems(),
369            transformation: Transformation::default(),
370        }
371    }
372
373    pub fn from_path(path: impl Into<SharedString>) -> Self {
374        Self {
375            source: IconSource::from_path(path),
376            color: Color::default(),
377            size: IconSize::default().rems(),
378            transformation: Transformation::default(),
379        }
380    }
381
382    pub fn color(mut self, color: Color) -> Self {
383        self.color = color;
384        self
385    }
386
387    pub fn size(mut self, size: IconSize) -> Self {
388        self.size = size.rems();
389        self
390    }
391
392    /// Sets a custom size for the icon, in [`Rems`].
393    ///
394    /// Not to be exposed outside of the `ui` crate.
395    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
396        self.size = size;
397        self
398    }
399
400    pub fn transform(mut self, transformation: Transformation) -> Self {
401        self.transformation = transformation;
402        self
403    }
404}
405
406impl RenderOnce for Icon {
407    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
408        match self.source {
409            IconSource::Svg(path) => svg()
410                .with_transformation(self.transformation)
411                .size(self.size)
412                .flex_none()
413                .path(path)
414                .text_color(self.color.color(cx))
415                .into_any_element(),
416            IconSource::Image(path) => img(path)
417                .size(self.size)
418                .flex_none()
419                .text_color(self.color.color(cx))
420                .into_any_element(),
421        }
422    }
423}
424
425#[derive(IntoElement)]
426pub struct IconWithIndicator {
427    icon: Icon,
428    indicator: Option<Indicator>,
429    indicator_border_color: Option<Hsla>,
430}
431
432impl IconWithIndicator {
433    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
434        Self {
435            icon,
436            indicator,
437            indicator_border_color: None,
438        }
439    }
440
441    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
442        self.indicator = indicator;
443        self
444    }
445
446    pub fn indicator_color(mut self, color: Color) -> Self {
447        if let Some(indicator) = self.indicator.as_mut() {
448            indicator.color = color;
449        }
450        self
451    }
452
453    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
454        self.indicator_border_color = color;
455        self
456    }
457}
458
459impl RenderOnce for IconWithIndicator {
460    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
461        let indicator_border_color = self
462            .indicator_border_color
463            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
464
465        div()
466            .relative()
467            .child(self.icon)
468            .when_some(self.indicator, |this, indicator| {
469                this.child(
470                    div()
471                        .absolute()
472                        .size_2p5()
473                        .border_2()
474                        .border_color(indicator_border_color)
475                        .rounded_full()
476                        .bottom_neg_0p5()
477                        .right_neg_0p5()
478                        .child(indicator),
479                )
480            })
481    }
482}
483
484impl ComponentPreview for Icon {
485    fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
486        let arrow_icons = vec![
487            IconName::ArrowDown,
488            IconName::ArrowLeft,
489            IconName::ArrowRight,
490            IconName::ArrowUp,
491            IconName::ArrowCircle,
492        ];
493
494        vec![example_group_with_title(
495            "Arrow Icons",
496            arrow_icons
497                .into_iter()
498                .map(|icon| {
499                    let name = format!("{:?}", icon).to_string();
500                    ComponentExample::new(name, Icon::new(icon))
501                })
502                .collect(),
503        )]
504    }
505}