Add dedicated indicator for showing a muted call participant (#4076)

Marshall Bowers created

This PR improves the muted indicators to make it clearer when a call
participant is muted.

Previously we used a red border color to denote when a participant was
muted.

Now we render an indicator with an icon to more clearly indicate the
participant's muted status:

<img width="303" alt="Screenshot 2024-01-16 at 4 05 15 PM"
src="https://github.com/zed-industries/zed/assets/1486634/d30fcd84-48e7-4959-b3c4-1054162c6bd6">

Hovering over the indicator will display a tooltip for further
explanation:

<img width="456" alt="Screenshot 2024-01-16 at 4 05 25 PM"
src="https://github.com/zed-industries/zed/assets/1486634/6345846f-196c-47d9-8d65-c8d86e63f823">

This change also paves the way for denoting the deafened status for call
participants.

Release Notes:

- Improved the mute indicator for call participants.

Change summary

crates/collab_ui/src/collab_titlebar_item.rs                     | 20 
crates/ui/src/components/avatar.rs                               |  2 
crates/ui/src/components/avatar/avatar.rs                        |  1 
crates/ui/src/components/avatar/avatar_audio_status_indicator.rs | 65 ++
crates/ui/src/components/icon.rs                                 |  2 
crates/ui/src/components/stories/avatar.rs                       | 12 
6 files changed, 92 insertions(+), 10 deletions(-)

Detailed changes

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -14,8 +14,8 @@ use rpc::proto;
 use std::sync::Arc;
 use theme::ActiveTheme;
 use ui::{
-    h_flex, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
-    IconButton, IconName, TintColor, Tooltip,
+    h_flex, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike,
+    ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, Tooltip,
 };
 use util::ResultExt;
 use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
@@ -486,12 +486,16 @@ impl CollabTitlebarItem {
             .child(
                 Avatar::new(user.avatar_uri.clone())
                     .grayscale(!is_present)
-                    .border_color(if is_speaking {
-                        cx.theme().status().info_border
-                    } else if is_muted {
-                        cx.theme().status().error_border
-                    } else {
-                        Hsla::default()
+                    .when(is_speaking, |avatar| {
+                        avatar.border_color(cx.theme().status().info_border)
+                    })
+                    .when(is_muted, |avatar| {
+                        avatar.indicator(
+                            AvatarAudioStatusIndicator::new(ui::AudioStatus::Muted).tooltip({
+                                let github_login = user.github_login.clone();
+                                move |cx| Tooltip::text(format!("{} is muted", github_login), cx)
+                            }),
+                        )
                     }),
             )
             .children(followers.iter().filter_map(|follower_peer_id| {

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

@@ -1,5 +1,7 @@
 mod avatar;
+mod avatar_audio_status_indicator;
 mod avatar_availability_indicator;
 
 pub use avatar::*;
+pub use avatar_audio_status_indicator::*;
 pub use avatar_availability_indicator::*;

crates/ui/src/components/avatar/avatar_audio_status_indicator.rs 🔗

@@ -0,0 +1,65 @@
+use gpui::AnyView;
+
+use crate::prelude::*;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum AudioStatus {
+    Muted,
+    Deafened,
+}
+
+#[derive(IntoElement)]
+pub struct AvatarAudioStatusIndicator {
+    audio_status: AudioStatus,
+    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+}
+
+impl AvatarAudioStatusIndicator {
+    pub fn new(audio_status: AudioStatus) -> Self {
+        Self {
+            audio_status,
+            tooltip: None,
+        }
+    }
+
+    pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
+}
+
+impl RenderOnce for AvatarAudioStatusIndicator {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let icon_size = IconSize::Indicator;
+
+        let width_in_px = icon_size.rems() * cx.rem_size();
+        let padding_x = px(4.);
+
+        div()
+            .absolute()
+            .bottom(rems(-1. / 16.))
+            .right(rems(-4. / 16.))
+            .w(width_in_px + padding_x)
+            .h(icon_size.rems())
+            .child(
+                h_flex()
+                    .id("muted-indicator")
+                    .justify_center()
+                    .px(padding_x)
+                    .py(px(2.))
+                    .bg(cx.theme().status().error_background)
+                    .rounded_md()
+                    .child(
+                        Icon::new(match self.audio_status {
+                            AudioStatus::Muted => IconName::MicMute,
+                            AudioStatus::Deafened => IconName::AudioOff,
+                        })
+                        .size(icon_size)
+                        .color(Color::Error),
+                    )
+                    .when_some(self.tooltip, |this, tooltip| {
+                        this.tooltip(move |cx| tooltip(cx))
+                    }),
+            )
+    }
+}

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

@@ -5,6 +5,7 @@ use crate::prelude::*;
 
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconSize {
+    Indicator,
     XSmall,
     Small,
     #[default]
@@ -14,6 +15,7 @@ pub enum IconSize {
 impl IconSize {
     pub fn rems(self) -> Rems {
         match self {
+            IconSize::Indicator => rems(10. / 16.),
             IconSize::XSmall => rems(12. / 16.),
             IconSize::Small => rems(14. / 16.),
             IconSize::Medium => rems(16. / 16.),

crates/ui/src/components/stories/avatar.rs 🔗

@@ -1,8 +1,8 @@
 use gpui::Render;
 use story::Story;
 
-use crate::Avatar;
-use crate::{prelude::*, Availability, AvatarAvailabilityIndicator};
+use crate::{prelude::*, AudioStatus, Availability, AvatarAvailabilityIndicator};
+use crate::{Avatar, AvatarAudioStatusIndicator};
 
 pub struct AvatarStory;
 
@@ -25,5 +25,13 @@ impl Render for AvatarStory {
                 Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
                     .indicator(AvatarAvailabilityIndicator::new(Availability::Busy)),
             )
+            .child(
+                Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                    .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
+            )
+            .child(
+                Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                    .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
+            )
     }
 }