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