icon.rs

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