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