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