Add the ability to render icons as indicators (#11273)

Marshall Bowers and Nate Butler created

This PR adds the ability to render `Icon`s as an `Indicator`.

Release Notes:

- N/A

Co-authored-by: Nate Butler <nate@zed.dev>

Change summary

crates/gpui/src/elements/animation.rs |  9 +++
crates/ui/src/components/icon.rs      | 56 +++++++++++++++++++++--
crates/ui/src/components/indicator.rs | 70 ++++++++++++++++++++++------
3 files changed, 114 insertions(+), 21 deletions(-)

Detailed changes

crates/gpui/src/elements/animation.rs 🔗

@@ -72,6 +72,15 @@ pub struct AnimationElement<E> {
     animator: Box<dyn Fn(E, f32) -> E + 'static>,
 }
 
+impl<E> AnimationElement<E> {
+    /// Returns a new [`AnimationElement<E>`] after applying the given function
+    /// to the element being animated.
+    pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
+        self.element = self.element.map(f);
+        self
+    }
+}
+
 impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
     type Element = AnimationElement<E>;
 

crates/ui/src/components/icon.rs 🔗

@@ -1,8 +1,46 @@
-use gpui::{svg, Hsla, IntoElement, Rems, Transformation};
+use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
 use strum::EnumIter;
 
 use crate::{prelude::*, Indicator};
 
+#[derive(IntoElement)]
+pub enum AnyIcon {
+    Icon(Icon),
+    AnimatedIcon(AnimationElement<Icon>),
+}
+
+impl AnyIcon {
+    /// Returns a new [`AnyIcon`] after applying the given mapping function
+    /// to the contained [`Icon`].
+    pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
+        match self {
+            Self::Icon(icon) => Self::Icon(f(icon)),
+            Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
+        }
+    }
+}
+
+impl From<Icon> for AnyIcon {
+    fn from(value: Icon) -> Self {
+        Self::Icon(value)
+    }
+}
+
+impl From<AnimationElement<Icon>> for AnyIcon {
+    fn from(value: AnimationElement<Icon>) -> Self {
+        Self::AnimatedIcon(value)
+    }
+}
+
+impl RenderOnce for AnyIcon {
+    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+        match self {
+            Self::Icon(icon) => icon.into_any_element(),
+            Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
+        }
+    }
+}
+
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconSize {
     Indicator,
@@ -236,7 +274,7 @@ impl IconName {
 pub struct Icon {
     path: SharedString,
     color: Color,
-    size: IconSize,
+    size: Rems,
     transformation: Transformation,
 }
 
@@ -245,7 +283,7 @@ impl Icon {
         Self {
             path: icon.path().into(),
             color: Color::default(),
-            size: IconSize::default(),
+            size: IconSize::default().rems(),
             transformation: Transformation::default(),
         }
     }
@@ -254,7 +292,7 @@ impl Icon {
         Self {
             path: path.into(),
             color: Color::default(),
-            size: IconSize::default(),
+            size: IconSize::default().rems(),
             transformation: Transformation::default(),
         }
     }
@@ -265,6 +303,14 @@ impl Icon {
     }
 
     pub fn size(mut self, size: IconSize) -> Self {
+        self.size = size.rems();
+        self
+    }
+
+    /// Sets a custom size for the icon, in [`Rems`].
+    ///
+    /// Not to be exposed outside of the `ui` crate.
+    pub(crate) fn custom_size(mut self, size: Rems) -> Self {
         self.size = size;
         self
     }
@@ -279,7 +325,7 @@ impl RenderOnce for Icon {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
         svg()
             .with_transformation(self.transformation)
-            .size(self.size.rems())
+            .size(self.size)
             .flex_none()
             .path(self.path)
             .text_color(self.color.color(cx))

crates/ui/src/components/indicator.rs 🔗

@@ -1,17 +1,17 @@
-use gpui::Position;
+use gpui::Transformation;
 
-use crate::prelude::*;
+use crate::{prelude::*, AnyIcon};
 
 #[derive(Default)]
 pub enum IndicatorStyle {
     #[default]
     Dot,
     Bar,
+    Icon(AnyIcon),
 }
 
 #[derive(IntoElement)]
 pub struct Indicator {
-    position: Position,
     style: IndicatorStyle,
     pub color: Color,
 }
@@ -19,7 +19,6 @@ pub struct Indicator {
 impl Indicator {
     pub fn dot() -> Self {
         Self {
-            position: Position::Relative,
             style: IndicatorStyle::Dot,
             color: Color::Default,
         }
@@ -27,32 +26,71 @@ impl Indicator {
 
     pub fn bar() -> Self {
         Self {
-            position: Position::Relative,
             style: IndicatorStyle::Dot,
             color: Color::Default,
         }
     }
 
+    pub fn icon(icon: impl Into<AnyIcon>) -> Self {
+        Self {
+            style: IndicatorStyle::Icon(icon.into()),
+            color: Color::Default,
+        }
+    }
+
     pub fn color(mut self, color: Color) -> Self {
         self.color = color;
         self
     }
+}
+
+impl RenderOnce for Indicator {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let container = div().flex_none();
+
+        match self.style {
+            IndicatorStyle::Icon(icon) => container
+                .child(icon.map(|icon| icon.custom_size(rems_from_px(8.)).color(self.color))),
+            IndicatorStyle::Dot => container
+                .w_1p5()
+                .h_1p5()
+                .rounded_full()
+                .bg(self.color.color(cx)),
+            IndicatorStyle::Bar => container
+                .w_full()
+                .h_1p5()
+                .rounded_t_md()
+                .bg(self.color.color(cx)),
+        }
+    }
+}
+
+#[derive(IntoElement)]
+pub struct IndicatorIcon {
+    icon: Icon,
+    transformation: Option<Transformation>,
+}
 
-    pub fn absolute(mut self) -> Self {
-        self.position = Position::Absolute;
+impl IndicatorIcon {
+    pub fn new(icon: Icon) -> Self {
+        Self {
+            icon,
+            transformation: None,
+        }
+    }
+
+    pub fn transformation(mut self, transformation: Transformation) -> Self {
+        self.transformation = Some(transformation);
         self
     }
 }
 
-impl RenderOnce for Indicator {
-    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        div()
-            .flex_none()
-            .map(|this| match self.style {
-                IndicatorStyle::Dot => this.w_1p5().h_1p5().rounded_full(),
-                IndicatorStyle::Bar => this.w_full().h_1p5().rounded_t_md(),
+impl RenderOnce for IndicatorIcon {
+    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+        self.icon
+            .custom_size(rems_from_px(8.))
+            .when_some(self.transformation, |this, transformation| {
+                this.transform(transformation)
             })
-            .when(self.position == Position::Absolute, |this| this.absolute())
-            .bg(self.color.color(cx))
     }
 }