Mainline `Icon` and `IconButton` changes (#3022)

Marshall Bowers and Nate Butler created

This PR mainlines the `Icon` and `IconButton` changes from the
`gpui2-ui` branch.

Release Notes:

- N/A

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

Change summary

crates/ui/src/components/toolbar.rs   |  8 +-
crates/ui/src/elements/icon.rs        | 89 +++++++++++++++++++++++++++-
crates/ui/src/elements/icon_button.rs | 46 ++++++++++----
crates/ui/src/templates/chat_panel.rs |  9 +-
crates/ui/src/templates/status_bar.rs | 21 +++---
crates/ui/src/templates/tab_bar.rs    | 16 ++--
crates/ui/src/templates/title_bar.rs  | 19 ++++-
7 files changed, 159 insertions(+), 49 deletions(-)

Detailed changes

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

@@ -2,7 +2,7 @@ use gpui2::elements::div;
 use gpui2::style::StyleHelpers;
 use gpui2::{Element, IntoElement, ParentElement, ViewContext};
 
-use crate::{breadcrumb, icon_button, theme};
+use crate::{breadcrumb, theme, IconAsset, IconButton};
 
 pub struct ToolbarItem {}
 
@@ -27,9 +27,9 @@ impl Toolbar {
             .child(
                 div()
                     .flex()
-                    .child(icon_button("icons/inlay_hint.svg"))
-                    .child(icon_button("icons/magnifying_glass.svg"))
-                    .child(icon_button("icons/magic-wand.svg")),
+                    .child(IconButton::new(IconAsset::InlayHint))
+                    .child(IconButton::new(IconAsset::MagnifyingGlass))
+                    .child(IconButton::new(IconAsset::MagicWand)),
             )
     }
 }

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

@@ -1,8 +1,41 @@
+use std::sync::Arc;
+
 use crate::theme::theme;
+use crate::Theme;
 use gpui2::elements::svg;
 use gpui2::style::StyleHelpers;
-use gpui2::IntoElement;
 use gpui2::{Element, ViewContext};
+use gpui2::{Hsla, IntoElement};
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum IconColor {
+    #[default]
+    Default,
+    Muted,
+    Disabled,
+    Placeholder,
+    Accent,
+    Error,
+    Warning,
+    Success,
+    Info,
+}
+
+impl IconColor {
+    pub fn color(self, theme: Arc<Theme>) -> Hsla {
+        match self {
+            IconColor::Default => theme.lowest.base.default.foreground,
+            IconColor::Muted => theme.lowest.variant.default.foreground,
+            IconColor::Disabled => theme.lowest.base.disabled.foreground,
+            IconColor::Placeholder => theme.lowest.base.disabled.foreground,
+            IconColor::Accent => theme.lowest.accent.default.foreground,
+            IconColor::Error => theme.lowest.negative.default.foreground,
+            IconColor::Warning => theme.lowest.warning.default.foreground,
+            IconColor::Success => theme.lowest.positive.default.foreground,
+            IconColor::Info => theme.lowest.accent.default.foreground,
+        }
+    }
+}
 
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconAsset {
@@ -10,21 +43,40 @@ pub enum IconAsset {
     ArrowLeft,
     ArrowRight,
     ArrowUpRight,
+    AudioOff,
+    AudioOn,
     Bolt,
     ChevronDown,
     ChevronLeft,
     ChevronRight,
     ChevronUp,
-    #[default]
+    Close,
+    ExclamationTriangle,
     File,
     FileDoc,
     FileGit,
     FileLock,
     FileRust,
     FileToml,
+    FileTree,
     Folder,
     FolderOpen,
+    FolderX,
+    #[default]
     Hash,
+    InlayHint,
+    MagicWand,
+    MagnifyingGlass,
+    MessageBubbles,
+    Mic,
+    MicMute,
+    Plus,
+    Screen,
+    Split,
+    Terminal,
+    XCircle,
+    Copilot,
+    Envelope,
 }
 
 impl IconAsset {
@@ -34,20 +86,39 @@ impl IconAsset {
             IconAsset::ArrowLeft => "icons/arrow_left.svg",
             IconAsset::ArrowRight => "icons/arrow_right.svg",
             IconAsset::ArrowUpRight => "icons/arrow_up_right.svg",
+            IconAsset::AudioOff => "icons/speaker-off.svg",
+            IconAsset::AudioOn => "icons/speaker-loud.svg",
             IconAsset::Bolt => "icons/bolt.svg",
             IconAsset::ChevronDown => "icons/chevron_down.svg",
             IconAsset::ChevronLeft => "icons/chevron_left.svg",
             IconAsset::ChevronRight => "icons/chevron_right.svg",
             IconAsset::ChevronUp => "icons/chevron_up.svg",
+            IconAsset::Close => "icons/x.svg",
+            IconAsset::ExclamationTriangle => "icons/warning.svg",
             IconAsset::File => "icons/file_icons/file.svg",
             IconAsset::FileDoc => "icons/file_icons/book.svg",
             IconAsset::FileGit => "icons/file_icons/git.svg",
             IconAsset::FileLock => "icons/file_icons/lock.svg",
             IconAsset::FileRust => "icons/file_icons/rust.svg",
             IconAsset::FileToml => "icons/file_icons/toml.svg",
+            IconAsset::FileTree => "icons/project.svg",
             IconAsset::Folder => "icons/file_icons/folder.svg",
             IconAsset::FolderOpen => "icons/file_icons/folder_open.svg",
+            IconAsset::FolderX => "icons/stop_sharing.svg",
             IconAsset::Hash => "icons/hash.svg",
+            IconAsset::InlayHint => "icons/inlay_hint.svg",
+            IconAsset::MagicWand => "icons/magic-wand.svg",
+            IconAsset::MagnifyingGlass => "icons/magnifying_glass.svg",
+            IconAsset::MessageBubbles => "icons/conversations.svg",
+            IconAsset::Mic => "icons/mic.svg",
+            IconAsset::MicMute => "icons/mic-mute.svg",
+            IconAsset::Plus => "icons/plus.svg",
+            IconAsset::Screen => "icons/desktop.svg",
+            IconAsset::Split => "icons/split.svg",
+            IconAsset::Terminal => "icons/terminal.svg",
+            IconAsset::XCircle => "icons/error.svg",
+            IconAsset::Copilot => "icons/copilot.svg",
+            IconAsset::Envelope => "icons/feedback.svg",
         }
     }
 }
@@ -55,20 +126,30 @@ impl IconAsset {
 #[derive(Element, Clone)]
 pub struct Icon {
     asset: IconAsset,
+    color: IconColor,
 }
 
 pub fn icon(asset: IconAsset) -> Icon {
-    Icon { asset }
+    Icon {
+        asset,
+        color: IconColor::default(),
+    }
 }
 
 impl Icon {
+    pub fn color(mut self, color: IconColor) -> Self {
+        self.color = color;
+        self
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
+        let fill = self.color.color(theme);
 
         svg()
             .flex_none()
             .path(self.asset.path())
             .size_4()
-            .fill(theme.lowest.variant.default.foreground)
+            .fill(fill)
     }
 }

crates/ui/src/elements/icon_button.rs 🔗

@@ -1,26 +1,47 @@
-use gpui2::elements::{div, svg};
+use gpui2::elements::div;
 use gpui2::style::{StyleHelpers, Styleable};
 use gpui2::{Element, IntoElement, ParentElement, ViewContext};
 
-use crate::prelude::*;
-use crate::theme;
+use crate::{icon, theme, IconColor};
+use crate::{prelude::*, IconAsset};
 
 #[derive(Element)]
 pub struct IconButton {
-    path: &'static str,
+    icon: IconAsset,
+    color: IconColor,
     variant: ButtonVariant,
     state: InteractionState,
 }
 
-pub fn icon_button(path: &'static str) -> IconButton {
+pub fn icon_button() -> IconButton {
     IconButton {
-        path,
+        icon: IconAsset::default(),
+        color: IconColor::default(),
         variant: ButtonVariant::default(),
         state: InteractionState::default(),
     }
 }
 
 impl IconButton {
+    pub fn new(icon: IconAsset) -> Self {
+        Self {
+            icon,
+            color: IconColor::default(),
+            variant: ButtonVariant::default(),
+            state: InteractionState::default(),
+        }
+    }
+
+    pub fn icon(mut self, icon: IconAsset) -> Self {
+        self.icon = icon;
+        self
+    }
+
+    pub fn color(mut self, color: IconColor) -> Self {
+        self.color = color;
+        self
+    }
+
     pub fn variant(mut self, variant: ButtonVariant) -> Self {
         self.variant = variant;
         self
@@ -34,13 +55,10 @@ impl IconButton {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
-        let icon_color;
-
-        if self.state == InteractionState::Disabled {
-            icon_color = theme.highest.base.disabled.foreground;
-        } else {
-            icon_color = theme.highest.base.default.foreground;
-        }
+        let icon_color = match (self.state, self.color) {
+            (InteractionState::Disabled, _) => IconColor::Disabled,
+            _ => self.color,
+        };
 
         let mut div = div();
         if self.variant == ButtonVariant::Filled {
@@ -57,6 +75,6 @@ impl IconButton {
             .fill(theme.highest.base.hovered.background)
             .active()
             .fill(theme.highest.base.pressed.background)
-            .child(svg().path(self.path).w_4().h_4().fill(icon_color))
+            .child(icon(self.icon).color(icon_color))
     }
 }

crates/ui/src/templates/chat_panel.rs 🔗

@@ -1,12 +1,13 @@
 use std::marker::PhantomData;
 
-use crate::icon_button;
-use crate::theme::theme;
 use gpui2::elements::div::ScrollState;
 use gpui2::style::StyleHelpers;
 use gpui2::{elements::div, IntoElement};
 use gpui2::{Element, ParentElement, ViewContext};
 
+use crate::theme::theme;
+use crate::{icon_button, IconAsset};
+
 #[derive(Element)]
 pub struct ChatPanel<V: 'static> {
     view_type: PhantomData<V>,
@@ -57,8 +58,8 @@ impl<V: 'static> ChatPanel<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button("icons/plus.svg"))
-                            .child(icon_button("icons/split.svg")),
+                            .child(icon_button().icon(IconAsset::Plus))
+                            .child(icon_button().icon(IconAsset::Split)),
                     ),
             )
     }

crates/ui/src/templates/status_bar.rs 🔗

@@ -1,11 +1,12 @@
 use std::marker::PhantomData;
 
-use crate::theme::{theme, Theme};
-use crate::{icon_button, text_button, tool_divider};
 use gpui2::style::StyleHelpers;
 use gpui2::{elements::div, IntoElement};
 use gpui2::{Element, ParentElement, ViewContext};
 
+use crate::theme::{theme, Theme};
+use crate::{icon_button, text_button, tool_divider, IconAsset};
+
 #[derive(Default, PartialEq)]
 pub enum Tool {
     #[default]
@@ -105,10 +106,10 @@ impl<V: 'static> StatusBar<V> {
             .flex()
             .items_center()
             .gap_1()
-            .child(icon_button("icons/project.svg"))
-            .child(icon_button("icons/hash.svg"))
+            .child(icon_button().icon(IconAsset::FileTree))
+            .child(icon_button().icon(IconAsset::Hash))
             .child(tool_divider())
-            .child(icon_button("icons/error.svg"))
+            .child(icon_button().icon(IconAsset::XCircle))
     }
     fn right_tools(&self, theme: &Theme) -> impl Element<V> {
         div()
@@ -129,8 +130,8 @@ impl<V: 'static> StatusBar<V> {
                     .flex()
                     .items_center()
                     .gap_1()
-                    .child(icon_button("icons/copilot.svg"))
-                    .child(icon_button("icons/feedback.svg")),
+                    .child(icon_button().icon(IconAsset::Copilot))
+                    .child(icon_button().icon(IconAsset::Envelope)),
             )
             .child(tool_divider())
             .child(
@@ -138,9 +139,9 @@ impl<V: 'static> StatusBar<V> {
                     .flex()
                     .items_center()
                     .gap_1()
-                    .child(icon_button("icons/terminal.svg"))
-                    .child(icon_button("icons/conversations.svg"))
-                    .child(icon_button("icons/ai.svg")),
+                    .child(icon_button().icon(IconAsset::Terminal))
+                    .child(icon_button().icon(IconAsset::MessageBubbles))
+                    .child(icon_button().icon(IconAsset::Ai)),
             )
     }
 }

crates/ui/src/templates/tab_bar.rs 🔗

@@ -1,13 +1,14 @@
 use std::marker::PhantomData;
 
-use crate::prelude::InteractionState;
-use crate::theme::theme;
-use crate::{icon_button, tab};
 use gpui2::elements::div::ScrollState;
 use gpui2::style::StyleHelpers;
 use gpui2::{elements::div, IntoElement};
 use gpui2::{Element, ParentElement, ViewContext};
 
+use crate::prelude::InteractionState;
+use crate::theme::theme;
+use crate::{icon_button, tab, IconAsset};
+
 #[derive(Element)]
 pub struct TabBar<V: 'static> {
     view_type: PhantomData<V>,
@@ -43,11 +44,12 @@ impl<V: 'static> TabBar<V> {
                             .items_center()
                             .gap_px()
                             .child(
-                                icon_button("icons/arrow_left.svg")
+                                icon_button()
+                                    .icon(IconAsset::ArrowLeft)
                                     .state(InteractionState::Enabled.if_enabled(can_navigate_back)),
                             )
                             .child(
-                                icon_button("icons/arrow_right.svg").state(
+                                icon_button().icon(IconAsset::ArrowRight).state(
                                     InteractionState::Enabled.if_enabled(can_navigate_forward),
                                 ),
                             ),
@@ -83,8 +85,8 @@ impl<V: 'static> TabBar<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button("icons/plus.svg"))
-                            .child(icon_button("icons/split.svg")),
+                            .child(icon_button().icon(IconAsset::Plus))
+                            .child(icon_button().icon(IconAsset::Split)),
                     ),
             )
     }

crates/ui/src/templates/title_bar.rs 🔗

@@ -5,7 +5,10 @@ use gpui2::style::StyleHelpers;
 use gpui2::{Element, IntoElement, ParentElement, ViewContext};
 
 use crate::prelude::Shape;
-use crate::{avatar, follow_group, icon_button, text_button, theme, tool_divider, traffic_lights};
+use crate::{
+    avatar, follow_group, icon_button, text_button, theme, tool_divider, traffic_lights, IconAsset,
+    IconColor,
+};
 
 #[derive(Element)]
 pub struct TitleBar<V: 'static> {
@@ -65,8 +68,8 @@ impl<V: 'static> TitleBar<V> {
                             .flex()
                             .items_center()
                             .gap_1()
-                            .child(icon_button("icons/stop_sharing.svg"))
-                            .child(icon_button("icons/exit.svg")),
+                            .child(icon_button().icon(IconAsset::FolderX))
+                            .child(icon_button().icon(IconAsset::Close)),
                     )
                     .child(tool_divider())
                     .child(
@@ -75,9 +78,13 @@ impl<V: 'static> TitleBar<V> {
                             .flex()
                             .items_center()
                             .gap_1()
-                            .child(icon_button("icons/mic.svg"))
-                            .child(icon_button("icons/speaker-loud.svg"))
-                            .child(icon_button("icons/desktop.svg")),
+                            .child(icon_button().icon(IconAsset::Mic))
+                            .child(icon_button().icon(IconAsset::AudioOn))
+                            .child(
+                                icon_button()
+                                    .icon(IconAsset::Screen)
+                                    .color(IconColor::Accent),
+                            ),
                     )
                     .child(
                         div().px_2().flex().items_center().child(