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::{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    FileSearch,
196    FileText,
197    FileToml,
198    FileTree,
199    Filter,
200    Folder,
201    FolderOpen,
202    FolderX,
203    Font,
204    FontSize,
205    FontWeight,
206    GenericClose,
207    GenericMaximize,
208    GenericMinimize,
209    GenericRestore,
210    Github,
211    Hash,
212    HistoryRerun,
213    Indicator,
214    IndicatorX,
215    InlayHint,
216    Library,
217    LineHeight,
218    Link,
219    ListTree,
220    ListX,
221    MagnifyingGlass,
222    MailOpen,
223    Maximize,
224    Menu,
225    MessageBubbles,
226    Mic,
227    MicMute,
228    Microscope,
229    Minimize,
230    Option,
231    PageDown,
232    PageUp,
233    Pencil,
234    Person,
235    Pin,
236    Play,
237    Plus,
238    PocketKnife,
239    Public,
240    PullRequest,
241    Quote,
242    RefreshTitle,
243    Regex,
244    ReplNeutral,
245    Replace,
246    ReplaceAll,
247    ReplaceNext,
248    ReplyArrowRight,
249    Rerun,
250    Return,
251    Reveal,
252    RotateCcw,
253    RotateCw,
254    Route,
255    Save,
256    Screen,
257    SearchCode,
258    SearchSelection,
259    SelectAll,
260    Server,
261    Settings,
262    SettingsAlt,
263    Shift,
264    Slash,
265    SlashSquare,
266    Sliders,
267    SlidersVertical,
268    Snip,
269    Space,
270    Sparkle,
271    SparkleAlt,
272    SparkleFilled,
273    Spinner,
274    Split,
275    Star,
276    StarFilled,
277    Stop,
278    Strikethrough,
279    Supermaven,
280    SupermavenDisabled,
281    SupermavenError,
282    SupermavenInit,
283    Tab,
284    Terminal,
285    Trash,
286    TrashAlt,
287    TriangleRight,
288    Undo,
289    Unpin,
290    Update,
291    UserGroup,
292    Visible,
293    Wand,
294    Warning,
295    WholeWord,
296    XCircle,
297    ZedAssistant,
298    ZedAssistantFilled,
299    ZedXCopilot,
300}
301
302impl From<IconName> for Icon {
303    fn from(icon: IconName) -> Self {
304        Icon::new(icon)
305    }
306}
307
308#[derive(IntoElement)]
309pub struct Icon {
310    path: SharedString,
311    color: Color,
312    size: Rems,
313    transformation: Transformation,
314}
315
316impl Icon {
317    pub fn new(icon: IconName) -> Self {
318        Self {
319            path: icon.path().into(),
320            color: Color::default(),
321            size: IconSize::default().rems(),
322            transformation: Transformation::default(),
323        }
324    }
325
326    pub fn from_path(path: impl Into<SharedString>) -> Self {
327        Self {
328            path: path.into(),
329            color: Color::default(),
330            size: IconSize::default().rems(),
331            transformation: Transformation::default(),
332        }
333    }
334
335    pub fn color(mut self, color: Color) -> Self {
336        self.color = color;
337        self
338    }
339
340    pub fn size(mut self, size: IconSize) -> Self {
341        self.size = size.rems();
342        self
343    }
344
345    /// Sets a custom size for the icon, in [`Rems`].
346    ///
347    /// Not to be exposed outside of the `ui` crate.
348    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
349        self.size = size;
350        self
351    }
352
353    pub fn transform(mut self, transformation: Transformation) -> Self {
354        self.transformation = transformation;
355        self
356    }
357}
358
359impl RenderOnce for Icon {
360    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
361        svg()
362            .with_transformation(self.transformation)
363            .size(self.size)
364            .flex_none()
365            .path(self.path)
366            .text_color(self.color.color(cx))
367    }
368}
369
370#[derive(IntoElement)]
371pub struct DecoratedIcon {
372    icon: Icon,
373    decoration: IconDecoration,
374    decoration_color: Color,
375    parent_background: Option<Hsla>,
376}
377
378impl DecoratedIcon {
379    pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
380        Self {
381            icon,
382            decoration,
383            decoration_color: Color::Default,
384            parent_background: None,
385        }
386    }
387
388    pub fn decoration_color(mut self, color: Color) -> Self {
389        self.decoration_color = color;
390        self
391    }
392
393    pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
394        self.parent_background = background;
395        self
396    }
397}
398
399impl RenderOnce for DecoratedIcon {
400    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
401        let background = self
402            .parent_background
403            .unwrap_or(cx.theme().colors().background);
404
405        let size = self.icon.size;
406
407        let decoration_icon = match self.decoration {
408            IconDecoration::Strikethrough => IconName::Strikethrough,
409            IconDecoration::IndicatorDot => IconName::Indicator,
410            IconDecoration::X => IconName::IndicatorX,
411        };
412
413        let decoration_svg = |icon: IconName| {
414            svg()
415                .absolute()
416                .top_0()
417                .left_0()
418                .path(icon.path())
419                .size(size)
420                .flex_none()
421                .text_color(self.decoration_color.color(cx))
422        };
423
424        let decoration_knockout = |icon: IconName| {
425            svg()
426                .absolute()
427                .top(-rems_from_px(2.))
428                .left(-rems_from_px(3.))
429                .path(icon.path())
430                .size(size + rems_from_px(2.))
431                .flex_none()
432                .text_color(background)
433        };
434
435        div()
436            .relative()
437            .size(self.icon.size)
438            .child(self.icon)
439            .child(decoration_knockout(decoration_icon))
440            .child(decoration_svg(decoration_icon))
441    }
442}
443
444#[derive(IntoElement)]
445pub struct IconWithIndicator {
446    icon: Icon,
447    indicator: Option<Indicator>,
448    indicator_border_color: Option<Hsla>,
449}
450
451impl IconWithIndicator {
452    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
453        Self {
454            icon,
455            indicator,
456            indicator_border_color: None,
457        }
458    }
459
460    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
461        self.indicator = indicator;
462        self
463    }
464
465    pub fn indicator_color(mut self, color: Color) -> Self {
466        if let Some(indicator) = self.indicator.as_mut() {
467            indicator.color = color;
468        }
469        self
470    }
471
472    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
473        self.indicator_border_color = color;
474        self
475    }
476}
477
478impl RenderOnce for IconWithIndicator {
479    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
480        let indicator_border_color = self
481            .indicator_border_color
482            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
483
484        div()
485            .relative()
486            .child(self.icon)
487            .when_some(self.indicator, |this, indicator| {
488                this.child(
489                    div()
490                        .absolute()
491                        .size_2p5()
492                        .border_2()
493                        .border_color(indicator_border_color)
494                        .rounded_full()
495                        .bottom_neg_0p5()
496                        .right_neg_0p5()
497                        .child(indicator),
498                )
499            })
500    }
501}
502
503impl ComponentPreview for Icon {
504    fn examples() -> Vec<ComponentExampleGroup<Icon>> {
505        let arrow_icons = vec![
506            IconName::ArrowDown,
507            IconName::ArrowLeft,
508            IconName::ArrowRight,
509            IconName::ArrowUp,
510            IconName::ArrowCircle,
511        ];
512
513        vec![example_group_with_title(
514            "Arrow Icons",
515            arrow_icons
516                .into_iter()
517                .map(|icon| {
518                    let name = format!("{:?}", icon).to_string();
519                    ComponentExample::new(name, Icon::new(icon))
520                })
521                .collect(),
522        )]
523    }
524}