icon.rs

  1#![allow(missing_docs)]
  2use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
  3use serde::{Deserialize, Serialize};
  4use strum::{EnumIter, EnumString, IntoStaticStr};
  5use ui_macros::DerivePathStr;
  6
  7use crate::{
  8    prelude::*,
  9    traits::component_preview::{example_group, ComponentExample, ComponentPreview},
 10    Indicator,
 11};
 12
 13#[derive(IntoElement)]
 14pub enum AnyIcon {
 15    Icon(Icon),
 16    AnimatedIcon(AnimationElement<Icon>),
 17}
 18
 19impl AnyIcon {
 20    /// Returns a new [`AnyIcon`] after applying the given mapping function
 21    /// to the contained [`Icon`].
 22    pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
 23        match self {
 24            Self::Icon(icon) => Self::Icon(f(icon)),
 25            Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
 26        }
 27    }
 28}
 29
 30impl From<Icon> for AnyIcon {
 31    fn from(value: Icon) -> Self {
 32        Self::Icon(value)
 33    }
 34}
 35
 36impl From<AnimationElement<Icon>> for AnyIcon {
 37    fn from(value: AnimationElement<Icon>) -> Self {
 38        Self::AnimatedIcon(value)
 39    }
 40}
 41
 42impl RenderOnce for AnyIcon {
 43    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
 44        match self {
 45            Self::Icon(icon) => icon.into_any_element(),
 46            Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
 47        }
 48    }
 49}
 50
 51/// The decoration for an icon.
 52///
 53/// For example, this can show an indicator, an "x",
 54/// or a diagonal strikethrough to indicate something is disabled.
 55#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
 56pub enum IconDecoration {
 57    Strikethrough,
 58    IndicatorDot,
 59    X,
 60}
 61
 62#[derive(Default, PartialEq, Copy, Clone)]
 63pub enum IconSize {
 64    /// 10px
 65    Indicator,
 66    /// 12px
 67    XSmall,
 68    /// 14px
 69    Small,
 70    #[default]
 71    /// 16px
 72    Medium,
 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        }
 83    }
 84
 85    /// Returns the individual components of the square that contains this [`IconSize`].
 86    ///
 87    /// The returned tuple contains:
 88    ///   1. The length of one side of the square
 89    ///   2. The padding of one side of the square
 90    pub fn square_components(&self, cx: &mut WindowContext) -> (Pixels, Pixels) {
 91        let icon_size = self.rems() * cx.rem_size();
 92        let padding = match self {
 93            IconSize::Indicator => Spacing::None.px(cx),
 94            IconSize::XSmall => Spacing::XSmall.px(cx),
 95            IconSize::Small => Spacing::XSmall.px(cx),
 96            IconSize::Medium => Spacing::XSmall.px(cx),
 97        };
 98
 99        (icon_size, padding)
100    }
101
102    /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
103    pub fn square(&self, cx: &mut WindowContext) -> Pixels {
104        let (icon_size, padding) = self.square_components(cx);
105
106        icon_size + padding * 2.
107    }
108}
109
110#[derive(
111    Debug,
112    PartialEq,
113    Eq,
114    Copy,
115    Clone,
116    EnumIter,
117    EnumString,
118    IntoStaticStr,
119    Serialize,
120    Deserialize,
121    DerivePathStr,
122)]
123#[strum(serialize_all = "snake_case")]
124#[path_str(prefix = "icons", suffix = ".svg")]
125pub enum IconName {
126    Ai,
127    AiAnthropic,
128    AiAnthropicHosted,
129    AiGoogle,
130    AiOllama,
131    AiOpenAi,
132    AiZed,
133    ArrowCircle,
134    ArrowDown,
135    ArrowDownFromLine,
136    ArrowLeft,
137    ArrowRight,
138    ArrowUp,
139    ArrowUpFromLine,
140    ArrowUpRight,
141    AtSign,
142    AudioOff,
143    AudioOn,
144    Backspace,
145    Bell,
146    BellDot,
147    BellOff,
148    BellRing,
149    Bolt,
150    Book,
151    BookCopy,
152    BookPlus,
153    CaseSensitive,
154    Check,
155    ChevronDown,
156    ChevronDownSmall, // This chevron indicates a popover menu.
157    ChevronLeft,
158    ChevronRight,
159    ChevronUp,
160    ChevronUpDown,
161    Close,
162    Code,
163    Command,
164    Context,
165    Control,
166    Copilot,
167    CopilotDisabled,
168    CopilotError,
169    CopilotInit,
170    Copy,
171    CountdownTimer,
172    CursorIBeam,
173    TextSnippet,
174    Dash,
175    DatabaseZap,
176    Delete,
177    Diff,
178    Disconnected,
179    Download,
180    Ellipsis,
181    EllipsisVertical,
182    Envelope,
183    Escape,
184    Exit,
185    ExpandVertical,
186    ExternalLink,
187    Eye,
188    File,
189    FileCode,
190    FileDoc,
191    FileGeneric,
192    FileGit,
193    FileLock,
194    FileRust,
195    FileText,
196    FileToml,
197    FileTree,
198    Filter,
199    Folder,
200    FolderOpen,
201    FolderX,
202    Font,
203    FontSize,
204    FontWeight,
205    GenericClose,
206    GenericMaximize,
207    GenericMinimize,
208    GenericRestore,
209    Github,
210    Hash,
211    HistoryRerun,
212    Indicator,
213    IndicatorX,
214    InlayHint,
215    Library,
216    LineHeight,
217    Link,
218    ListTree,
219    ListX,
220    MagnifyingGlass,
221    MailOpen,
222    Maximize,
223    Menu,
224    MessageBubbles,
225    Mic,
226    MicMute,
227    Microscope,
228    Minimize,
229    Option,
230    PageDown,
231    PageUp,
232    Pencil,
233    Person,
234    Pin,
235    Play,
236    Plus,
237    PocketKnife,
238    Public,
239    PullRequest,
240    Quote,
241    RefreshTitle,
242    Regex,
243    ReplNeutral,
244    Replace,
245    ReplaceAll,
246    ReplaceNext,
247    ReplyArrowRight,
248    Rerun,
249    Return,
250    Reveal,
251    RotateCcw,
252    RotateCw,
253    Route,
254    Save,
255    Screen,
256    SearchCode,
257    SearchSelection,
258    SelectAll,
259    Server,
260    Settings,
261    SettingsAlt,
262    Shift,
263    Slash,
264    SlashSquare,
265    Sliders,
266    SlidersVertical,
267    Snip,
268    Space,
269    Sparkle,
270    SparkleAlt,
271    SparkleFilled,
272    Spinner,
273    Split,
274    Star,
275    StarFilled,
276    Stop,
277    Strikethrough,
278    Supermaven,
279    SupermavenDisabled,
280    SupermavenError,
281    SupermavenInit,
282    Tab,
283    Terminal,
284    Trash,
285    TrashAlt,
286    TriangleRight,
287    Undo,
288    Unpin,
289    Update,
290    UserGroup,
291    Visible,
292    Wand,
293    Warning,
294    WholeWord,
295    XCircle,
296    ZedAssistant,
297    ZedAssistantFilled,
298    ZedXCopilot,
299}
300
301impl From<IconName> for Icon {
302    fn from(icon: IconName) -> Self {
303        Icon::new(icon)
304    }
305}
306
307#[derive(IntoElement)]
308pub struct Icon {
309    path: SharedString,
310    color: Color,
311    size: Rems,
312    transformation: Transformation,
313}
314
315impl Icon {
316    pub fn new(icon: IconName) -> Self {
317        Self {
318            path: icon.path().into(),
319            color: Color::default(),
320            size: IconSize::default().rems(),
321            transformation: Transformation::default(),
322        }
323    }
324
325    pub fn from_path(path: impl Into<SharedString>) -> Self {
326        Self {
327            path: path.into(),
328            color: Color::default(),
329            size: IconSize::default().rems(),
330            transformation: Transformation::default(),
331        }
332    }
333
334    pub fn color(mut self, color: Color) -> Self {
335        self.color = color;
336        self
337    }
338
339    pub fn size(mut self, size: IconSize) -> Self {
340        self.size = size.rems();
341        self
342    }
343
344    /// Sets a custom size for the icon, in [`Rems`].
345    ///
346    /// Not to be exposed outside of the `ui` crate.
347    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
348        self.size = size;
349        self
350    }
351
352    pub fn transform(mut self, transformation: Transformation) -> Self {
353        self.transformation = transformation;
354        self
355    }
356}
357
358impl RenderOnce for Icon {
359    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
360        svg()
361            .with_transformation(self.transformation)
362            .size(self.size)
363            .flex_none()
364            .path(self.path)
365            .text_color(self.color.color(cx))
366    }
367}
368
369#[derive(IntoElement)]
370pub struct DecoratedIcon {
371    icon: Icon,
372    decoration: IconDecoration,
373    decoration_color: Color,
374    parent_background: Option<Hsla>,
375}
376
377impl DecoratedIcon {
378    pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
379        Self {
380            icon,
381            decoration,
382            decoration_color: Color::Default,
383            parent_background: None,
384        }
385    }
386
387    pub fn decoration_color(mut self, color: Color) -> Self {
388        self.decoration_color = color;
389        self
390    }
391
392    pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
393        self.parent_background = background;
394        self
395    }
396}
397
398impl RenderOnce for DecoratedIcon {
399    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
400        let background = self
401            .parent_background
402            .unwrap_or(cx.theme().colors().background);
403
404        let size = self.icon.size;
405
406        let decoration_icon = match self.decoration {
407            IconDecoration::Strikethrough => IconName::Strikethrough,
408            IconDecoration::IndicatorDot => IconName::Indicator,
409            IconDecoration::X => IconName::IndicatorX,
410        };
411
412        let decoration_svg = |icon: IconName| {
413            svg()
414                .absolute()
415                .top_0()
416                .left_0()
417                .path(icon.path())
418                .size(size)
419                .flex_none()
420                .text_color(self.decoration_color.color(cx))
421        };
422
423        let decoration_knockout = |icon: IconName| {
424            svg()
425                .absolute()
426                .top(-rems_from_px(2.))
427                .left(-rems_from_px(3.))
428                .path(icon.path())
429                .size(size + rems_from_px(2.))
430                .flex_none()
431                .text_color(background)
432        };
433
434        div()
435            .relative()
436            .size(self.icon.size)
437            .child(self.icon)
438            .child(decoration_knockout(decoration_icon))
439            .child(decoration_svg(decoration_icon))
440    }
441}
442
443#[derive(IntoElement)]
444pub struct IconWithIndicator {
445    icon: Icon,
446    indicator: Option<Indicator>,
447    indicator_border_color: Option<Hsla>,
448}
449
450impl IconWithIndicator {
451    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
452        Self {
453            icon,
454            indicator,
455            indicator_border_color: None,
456        }
457    }
458
459    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
460        self.indicator = indicator;
461        self
462    }
463
464    pub fn indicator_color(mut self, color: Color) -> Self {
465        if let Some(indicator) = self.indicator.as_mut() {
466            indicator.color = color;
467        }
468        self
469    }
470
471    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
472        self.indicator_border_color = color;
473        self
474    }
475}
476
477impl RenderOnce for IconWithIndicator {
478    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
479        let indicator_border_color = self
480            .indicator_border_color
481            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
482
483        div()
484            .relative()
485            .child(self.icon)
486            .when_some(self.indicator, |this, indicator| {
487                this.child(
488                    div()
489                        .absolute()
490                        .size_2p5()
491                        .border_2()
492                        .border_color(indicator_border_color)
493                        .rounded_full()
494                        .bottom_neg_0p5()
495                        .right_neg_0p5()
496                        .child(indicator),
497                )
498            })
499    }
500}
501
502impl ComponentPreview for Icon {
503    fn examples() -> Vec<ComponentExampleGroup<Icon>> {
504        let arrow_icons = vec![
505            IconName::ArrowDown,
506            IconName::ArrowLeft,
507            IconName::ArrowRight,
508            IconName::ArrowUp,
509            IconName::ArrowCircle,
510        ];
511
512        vec![example_group(
513            "Arrow Icons",
514            arrow_icons
515                .into_iter()
516                .map(|icon| {
517                    let name = format!("{:?}", icon).to_string();
518                    ComponentExample::new(name, Icon::new(icon))
519                })
520                .collect(),
521        )]
522    }
523}