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    TriangleRight,
279    Undo,
280    Unpin,
281    Update,
282    UserGroup,
283    Visible,
284    Warning,
285    WholeWord,
286    XCircle,
287    ZedAssistant,
288    ZedAssistantFilled,
289    ZedXCopilot,
290}
291
292#[derive(IntoElement)]
293pub struct Icon {
294    path: SharedString,
295    color: Color,
296    size: Rems,
297    transformation: Transformation,
298}
299
300impl Icon {
301    pub fn new(icon: IconName) -> Self {
302        Self {
303            path: icon.path().into(),
304            color: Color::default(),
305            size: IconSize::default().rems(),
306            transformation: Transformation::default(),
307        }
308    }
309
310    pub fn from_path(path: impl Into<SharedString>) -> Self {
311        Self {
312            path: path.into(),
313            color: Color::default(),
314            size: IconSize::default().rems(),
315            transformation: Transformation::default(),
316        }
317    }
318
319    pub fn color(mut self, color: Color) -> Self {
320        self.color = color;
321        self
322    }
323
324    pub fn size(mut self, size: IconSize) -> Self {
325        self.size = size.rems();
326        self
327    }
328
329    /// Sets a custom size for the icon, in [`Rems`].
330    ///
331    /// Not to be exposed outside of the `ui` crate.
332    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
333        self.size = size;
334        self
335    }
336
337    pub fn transform(mut self, transformation: Transformation) -> Self {
338        self.transformation = transformation;
339        self
340    }
341}
342
343impl RenderOnce for Icon {
344    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
345        svg()
346            .with_transformation(self.transformation)
347            .size(self.size)
348            .flex_none()
349            .path(self.path)
350            .text_color(self.color.color(cx))
351    }
352}
353
354#[derive(IntoElement)]
355pub struct DecoratedIcon {
356    icon: Icon,
357    decoration: IconDecoration,
358    decoration_color: Color,
359    parent_background: Option<Hsla>,
360}
361
362impl DecoratedIcon {
363    pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
364        Self {
365            icon,
366            decoration,
367            decoration_color: Color::Default,
368            parent_background: None,
369        }
370    }
371
372    pub fn decoration_color(mut self, color: Color) -> Self {
373        self.decoration_color = color;
374        self
375    }
376
377    pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
378        self.parent_background = background;
379        self
380    }
381}
382
383impl RenderOnce for DecoratedIcon {
384    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
385        let background = self
386            .parent_background
387            .unwrap_or(cx.theme().colors().background);
388
389        let size = self.icon.size;
390
391        let decoration_icon = match self.decoration {
392            IconDecoration::Strikethrough => IconName::Strikethrough,
393            IconDecoration::IndicatorDot => IconName::Indicator,
394            IconDecoration::X => IconName::IndicatorX,
395        };
396
397        let decoration_svg = |icon: IconName| {
398            svg()
399                .absolute()
400                .top_0()
401                .left_0()
402                .path(icon.path())
403                .size(size)
404                .flex_none()
405                .text_color(self.decoration_color.color(cx))
406        };
407
408        let decoration_knockout = |icon: IconName| {
409            svg()
410                .absolute()
411                .top(-rems_from_px(2.))
412                .left(-rems_from_px(3.))
413                .path(icon.path())
414                .size(size + rems_from_px(2.))
415                .flex_none()
416                .text_color(background)
417        };
418
419        div()
420            .relative()
421            .size(self.icon.size)
422            .child(self.icon)
423            .child(decoration_knockout(decoration_icon))
424            .child(decoration_svg(decoration_icon))
425    }
426}
427
428#[derive(IntoElement)]
429pub struct IconWithIndicator {
430    icon: Icon,
431    indicator: Option<Indicator>,
432    indicator_border_color: Option<Hsla>,
433}
434
435impl IconWithIndicator {
436    pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
437        Self {
438            icon,
439            indicator,
440            indicator_border_color: None,
441        }
442    }
443
444    pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
445        self.indicator = indicator;
446        self
447    }
448
449    pub fn indicator_color(mut self, color: Color) -> Self {
450        if let Some(indicator) = self.indicator.as_mut() {
451            indicator.color = color;
452        }
453        self
454    }
455
456    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
457        self.indicator_border_color = color;
458        self
459    }
460}
461
462impl RenderOnce for IconWithIndicator {
463    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
464        let indicator_border_color = self
465            .indicator_border_color
466            .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
467
468        div()
469            .relative()
470            .child(self.icon)
471            .when_some(self.indicator, |this, indicator| {
472                this.child(
473                    div()
474                        .absolute()
475                        .w_2()
476                        .h_2()
477                        .border_1()
478                        .border_color(indicator_border_color)
479                        .rounded_full()
480                        .bottom_neg_0p5()
481                        .right_neg_1()
482                        .child(indicator),
483                )
484            })
485    }
486}