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