icon.rs

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