Update call controls & Add tinted buttons (#3886)

Nate Butler created

This PR introduces Tinted button styles and the `selected_style` field
on buttons to allow replicating the previous design of titlebar call
controls. It also updates the styles of the titlebar controls.

### Creating a tinted button:

```
Button::new("accept-cta", "Accept")
    .style(ButtonStyle::Tinted(TintColor::Accent))
    .on_click(...)
```

Ths button will always be tinted blue.

### Creating a button that becomes tinted when selected::

```rust
IconButton::new("screen-share", Icon::Screen)
    .style(ButtonStyle::Subtle)
    .selected(is_screen_sharing)
    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
    .on_click(...),
```

This button will be flat/subtle by default, but be tinted blue when it
is `selected`.

Note: There appears to be some issue where `is_deafened` isn't
activating correctly, making the speaker icon not toggle when selected.

Release Notes:

- Restore call control styles to a similar look to Zed 1.

Change summary

crates/collab_ui/src/collab_titlebar_item.rs     |   7 
crates/ui/src/components/button/button.rs        |   7 +
crates/ui/src/components/button/button_icon.rs   |  11 +
crates/ui/src/components/button/button_like.rs   | 116 ++++++++++++-----
crates/ui/src/components/button/icon_button.rs   |  11 +
crates/ui/src/components/button/toggle_button.rs |   7 +
crates/ui/src/prelude.rs                         |   2 
7 files changed, 122 insertions(+), 39 deletions(-)

Detailed changes

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -14,7 +14,7 @@ use std::sync::Arc;
 use theme::{ActiveTheme, PlayerColors};
 use ui::{
     h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
-    IconButton, IconElement, Tooltip,
+    IconButton, IconElement, TintColor, Tooltip,
 };
 use util::ResultExt;
 use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
@@ -183,6 +183,8 @@ impl Render for CollabTitlebarItem {
                                     if is_shared { "Unshare" } else { "Share" },
                                 )
                                 .style(ButtonStyle::Subtle)
+                                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                                .selected(is_shared)
                                 .label_size(LabelSize::Small)
                                 .on_click(cx.listener(
                                     move |this, _, cx| {
@@ -215,6 +217,7 @@ impl Render for CollabTitlebarItem {
                                 },
                             )
                             .style(ButtonStyle::Subtle)
+                            .selected_style(ButtonStyle::Tinted(TintColor::Negative))
                             .icon_size(IconSize::Small)
                             .selected(is_muted)
                             .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
@@ -229,6 +232,7 @@ impl Render for CollabTitlebarItem {
                                 },
                             )
                             .style(ButtonStyle::Subtle)
+                            .selected_style(ButtonStyle::Tinted(TintColor::Negative))
                             .icon_size(IconSize::Small)
                             .selected(is_deafened)
                             .tooltip(move |cx| {
@@ -239,6 +243,7 @@ impl Render for CollabTitlebarItem {
                         .child(
                             IconButton::new("screen-share", ui::Icon::Screen)
                                 .style(ButtonStyle::Subtle)
+                                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
                                 .icon_size(IconSize::Small)
                                 .selected(is_screen_sharing)
                                 .on_click(move |_, cx| {

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

@@ -92,6 +92,13 @@ impl Selectable for Button {
     }
 }
 
+impl SelectableButton for Button {
+    fn selected_style(mut self, style: ButtonStyle) -> Self {
+        self.base = self.base.selected_style(style);
+        self
+    }
+}
+
 impl Disableable for Button {
     fn disabled(mut self, disabled: bool) -> Self {
         self.base = self.base.disabled(disabled);

crates/ui/src/components/button/button_icon.rs 🔗

@@ -12,6 +12,7 @@ pub(super) struct ButtonIcon {
     disabled: bool,
     selected: bool,
     selected_icon: Option<Icon>,
+    selected_style: Option<ButtonStyle>,
 }
 
 impl ButtonIcon {
@@ -23,6 +24,7 @@ impl ButtonIcon {
             disabled: false,
             selected: false,
             selected_icon: None,
+            selected_style: None,
         }
     }
 
@@ -62,6 +64,13 @@ impl Selectable for ButtonIcon {
     }
 }
 
+impl SelectableButton for ButtonIcon {
+    fn selected_style(mut self, style: ButtonStyle) -> Self {
+        self.selected_style = Some(style);
+        self
+    }
+}
+
 impl RenderOnce for ButtonIcon {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
         let icon = self
@@ -71,6 +80,8 @@ impl RenderOnce for ButtonIcon {
 
         let icon_color = if self.disabled {
             Color::Disabled
+        } else if self.selected_style.is_some() && self.selected {
+            self.selected_style.unwrap().into()
         } else if self.selected {
             Color::Selected
         } else {

crates/ui/src/components/button/button_like.rs 🔗

@@ -4,6 +4,10 @@ use smallvec::SmallVec;
 
 use crate::prelude::*;
 
+pub trait SelectableButton: Selectable {
+    fn selected_style(self, style: ButtonStyle) -> Self;
+}
+
 pub trait ButtonCommon: Clickable + Disableable {
     /// A unique element ID to identify the button.
     fn id(&self) -> &ElementId;
@@ -36,17 +40,68 @@ pub enum IconPosition {
     End,
 }
 
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
+pub enum TintColor {
+    #[default]
+    Accent,
+    Negative,
+    Warning,
+}
+
+impl TintColor {
+    fn button_like_style(self, cx: &mut WindowContext) -> ButtonLikeStyles {
+        match self {
+            TintColor::Accent => ButtonLikeStyles {
+                background: cx.theme().status().info_background,
+                border_color: cx.theme().status().info_border,
+                label_color: cx.theme().colors().text,
+                icon_color: cx.theme().colors().text,
+            },
+            TintColor::Negative => ButtonLikeStyles {
+                background: cx.theme().status().error_background,
+                border_color: cx.theme().status().error_border,
+                label_color: cx.theme().colors().text,
+                icon_color: cx.theme().colors().text,
+            },
+            TintColor::Warning => ButtonLikeStyles {
+                background: cx.theme().status().warning_background,
+                border_color: cx.theme().status().warning_border,
+                label_color: cx.theme().colors().text,
+                icon_color: cx.theme().colors().text,
+            },
+        }
+    }
+}
+
+impl From<TintColor> for Color {
+    fn from(tint: TintColor) -> Self {
+        match tint {
+            TintColor::Accent => Color::Accent,
+            TintColor::Negative => Color::Error,
+            TintColor::Warning => Color::Warning,
+        }
+    }
+}
+
+// Used to go from ButtonStyle -> Color through tint colors.
+impl From<ButtonStyle> for Color {
+    fn from(style: ButtonStyle) -> Self {
+        match style {
+            ButtonStyle::Tinted(tint) => tint.into(),
+            _ => Color::Default,
+        }
+    }
+}
+
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 pub enum ButtonStyle {
     /// A filled button with a solid background color. Provides emphasis versus
     /// the more common subtle button.
     Filled,
 
-    /// 🚧 Under construction 🚧
-    ///
     /// Used to emphasize a button in some way, like a selected state, or a semantic
     /// coloring like an error or success button.
-    Tinted,
+    Tinted(TintColor),
 
     /// The default button style, used for most buttons. Has a transparent background,
     /// but has a background color to indicate states like hover and active.
@@ -86,12 +141,7 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle::Tinted => ButtonLikeStyles {
-                background: gpui::red(),
-                border_color: gpui::red(),
-                label_color: gpui::red(),
-                icon_color: gpui::red(),
-            },
+            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_background,
                 border_color: transparent_black(),
@@ -115,12 +165,7 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle::Tinted => ButtonLikeStyles {
-                background: gpui::red(),
-                border_color: gpui::red(),
-                label_color: gpui::red(),
-                icon_color: gpui::red(),
-            },
+            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_hover,
                 border_color: transparent_black(),
@@ -146,12 +191,7 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle::Tinted => ButtonLikeStyles {
-                background: gpui::red(),
-                border_color: gpui::red(),
-                label_color: gpui::red(),
-                icon_color: gpui::red(),
-            },
+            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_active,
                 border_color: transparent_black(),
@@ -178,12 +218,7 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle::Tinted => ButtonLikeStyles {
-                background: gpui::red(),
-                border_color: gpui::red(),
-                label_color: gpui::red(),
-                icon_color: gpui::red(),
-            },
+            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_background,
                 border_color: cx.theme().colors().border_focused,
@@ -208,12 +243,7 @@ impl ButtonStyle {
                 label_color: Color::Disabled.color(cx),
                 icon_color: Color::Disabled.color(cx),
             },
-            ButtonStyle::Tinted => ButtonLikeStyles {
-                background: gpui::red(),
-                border_color: gpui::red(),
-                label_color: gpui::red(),
-                icon_color: gpui::red(),
-            },
+            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_disabled,
                 border_color: cx.theme().colors().border_disabled,
@@ -264,6 +294,7 @@ pub struct ButtonLike {
     pub(super) style: ButtonStyle,
     pub(super) disabled: bool,
     pub(super) selected: bool,
+    pub(super) selected_style: Option<ButtonStyle>,
     pub(super) width: Option<DefiniteLength>,
     size: ButtonSize,
     rounding: Option<ButtonLikeRounding>,
@@ -280,6 +311,7 @@ impl ButtonLike {
             style: ButtonStyle::default(),
             disabled: false,
             selected: false,
+            selected_style: None,
             width: None,
             size: ButtonSize::Default,
             rounding: Some(ButtonLikeRounding::All),
@@ -309,6 +341,13 @@ impl Selectable for ButtonLike {
     }
 }
 
+impl SelectableButton for ButtonLike {
+    fn selected_style(mut self, style: ButtonStyle) -> Self {
+        self.selected_style = Some(style);
+        self
+    }
+}
+
 impl Clickable for ButtonLike {
     fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
         self.on_click = Some(Box::new(handler));
@@ -364,6 +403,11 @@ impl ParentElement for ButtonLike {
 
 impl RenderOnce for ButtonLike {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let style = self
+            .selected_style
+            .filter(|_| self.selected)
+            .unwrap_or(self.style);
+
         self.base
             .h_flex()
             .id(self.id.clone())
@@ -382,12 +426,12 @@ impl RenderOnce for ButtonLike {
                 ButtonSize::Default | ButtonSize::Compact => this.px_1(),
                 ButtonSize::None => this,
             })
-            .bg(self.style.enabled(cx).background)
+            .bg(style.enabled(cx).background)
             .when(self.disabled, |this| this.cursor_not_allowed())
             .when(!self.disabled, |this| {
                 this.cursor_pointer()
-                    .hover(|hover| hover.bg(self.style.hovered(cx).background))
-                    .active(|active| active.bg(self.style.active(cx).background))
+                    .hover(|hover| hover.bg(style.hovered(cx).background))
+                    .active(|active| active.bg(style.active(cx).background))
             })
             .when_some(
                 self.on_click.filter(|_| !self.disabled),

crates/ui/src/components/button/icon_button.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{AnyView, DefiniteLength};
 
-use crate::prelude::*;
+use crate::{prelude::*, SelectableButton};
 use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize};
 
 use super::button_icon::ButtonIcon;
@@ -55,6 +55,13 @@ impl Selectable for IconButton {
     }
 }
 
+impl SelectableButton for IconButton {
+    fn selected_style(mut self, style: ButtonStyle) -> Self {
+        self.base = self.base.selected_style(style);
+        self
+    }
+}
+
 impl Clickable for IconButton {
     fn on_click(
         mut self,
@@ -109,12 +116,14 @@ impl RenderOnce for IconButton {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
         let is_disabled = self.base.disabled;
         let is_selected = self.base.selected;
+        let selected_style = self.base.selected_style;
 
         self.base.child(
             ButtonIcon::new(self.icon)
                 .disabled(is_disabled)
                 .selected(is_selected)
                 .selected_icon(self.selected_icon)
+                .when_some(selected_style, |this, style| this.selected_style(style))
                 .size(self.icon_size)
                 .color(self.icon_color),
         )

crates/ui/src/components/button/toggle_button.rs 🔗

@@ -63,6 +63,13 @@ impl Selectable for ToggleButton {
     }
 }
 
+impl SelectableButton for ToggleButton {
+    fn selected_style(mut self, style: ButtonStyle) -> Self {
+        self.base.selected_style = Some(style);
+        self
+    }
+}
+
 impl Disableable for ToggleButton {
     fn disabled(mut self, disabled: bool) -> Self {
         self.base = self.base.disabled(disabled);

crates/ui/src/prelude.rs 🔗

@@ -12,7 +12,7 @@ pub use crate::selectable::*;
 pub use crate::styles::{vh, vw};
 pub use crate::visible_on_hover::*;
 pub use crate::{h_stack, v_stack};
-pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
+pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton};
 pub use crate::{ButtonCommon, Color, StyledExt};
 pub use crate::{Icon, IconElement, IconPosition, IconSize};
 pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle};