Increase border width used to indicate speaking (#4077)

Marshall Bowers created

This PR increases the width of the border that we use to indicate when a
call participant is speaking.

This should make it more apparent in the UI when someone is speaking.

Release Notes:

- Increased the width of the ring used to indicate when someone is
speaking in a call.

Change summary

crates/collab_ui/src/collab_titlebar_item.rs |  8 ++
crates/gpui_macros/src/style_helpers.rs      | 14 ++++
crates/ui/src/components/avatar/avatar.rs    | 15 +++-
crates/ui/src/components/stories/avatar.rs   | 64 +++++++++++++++------
4 files changed, 75 insertions(+), 26 deletions(-)

Detailed changes

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -486,8 +486,12 @@ impl CollabTitlebarItem {
             .child(
                 Avatar::new(user.avatar_uri.clone())
                     .grayscale(!is_present)
-                    .when(is_speaking, |avatar| {
-                        avatar.border_color(cx.theme().status().info_border)
+                    .border_color(if is_speaking {
+                        cx.theme().status().info_border
+                    } else {
+                        // We draw the border in a transparent color rather to avoid
+                        // the layout shift that would come with adding/removing the border.
+                        gpui::transparent_black()
                     })
                     .when(is_muted, |avatar| {
                         avatar.indicator(

crates/gpui_macros/src/style_helpers.rs 🔗

@@ -85,6 +85,18 @@ fn generate_methods() -> Vec<TokenStream2> {
     }
 
     for (prefix, fields, prefix_doc_string) in border_prefixes() {
+        methods.push(generate_custom_value_setter(
+            // The plain method names (e.g., `border`, `border_t`, `border_r`, etc.) are special-cased
+            // versions of the 1px variants. This better matches Tailwind, but breaks our existing
+            // convention of the suffix-less variant of the method being the one that accepts a custom value
+            //
+            // To work around this, we're assigning a `_width` suffix here.
+            &format!("{prefix}_width"),
+            quote! { AbsoluteLength },
+            &fields,
+            prefix_doc_string,
+        ));
+
         for (suffix, width_tokens, suffix_doc_string) in border_suffixes() {
             methods.push(generate_predefined_setter(
                 prefix,
@@ -141,7 +153,7 @@ fn generate_predefined_setter(
 }
 
 fn generate_custom_value_setter(
-    prefix: &'static str,
+    prefix: &str,
     length_type: TokenStream2,
     fields: &[TokenStream2],
     doc_string: &str,

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

@@ -99,20 +99,27 @@ impl RenderOnce for Avatar {
             self = self.shape(AvatarShape::Circle);
         }
 
-        let size = self.size.unwrap_or_else(|| cx.rem_size());
+        let border_width = if self.border_color.is_some() {
+            px(2.)
+        } else {
+            px(0.)
+        };
+
+        let image_size = self.size.unwrap_or_else(|| cx.rem_size());
+        let container_size = image_size + border_width * 2.;
 
         div()
-            .size(size + px(2.))
+            .size(container_size)
             .map(|mut div| {
                 div.style().corner_radii = self.image.style().corner_radii.clone();
                 div
             })
             .when_some(self.border_color, |this, color| {
-                this.border().border_color(color)
+                this.border_width(border_width).border_color(color)
             })
             .child(
                 self.image
-                    .size(size)
+                    .size(image_size)
                     .bg(cx.theme().colors().ghost_element_background),
             )
             .children(

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

@@ -1,5 +1,5 @@
 use gpui::Render;
-use story::Story;
+use story::{StoryContainer, StoryItem, StorySection};
 
 use crate::{prelude::*, AudioStatus, Availability, AvatarAvailabilityIndicator};
 use crate::{Avatar, AvatarAudioStatusIndicator};
@@ -7,31 +7,57 @@ use crate::{Avatar, AvatarAudioStatusIndicator};
 pub struct AvatarStory;
 
 impl Render for AvatarStory {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        Story::container()
-            .child(Story::title_for::<Avatar>())
-            .child(Story::label("Default"))
-            .child(Avatar::new(
-                "https://avatars.githubusercontent.com/u/1714999?v=4",
-            ))
-            .child(Avatar::new(
-                "https://avatars.githubusercontent.com/u/326587?v=4",
-            ))
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        StoryContainer::new("Avatar", "crates/ui/src/components/stories/avatar.rs")
             .child(
-                Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                    .indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
+                StorySection::new()
+                    .child(StoryItem::new(
+                        "Default",
+                        Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4"),
+                    ))
+                    .child(StoryItem::new(
+                        "Default",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4"),
+                    )),
             )
             .child(
-                Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                    .indicator(AvatarAvailabilityIndicator::new(Availability::Busy)),
+                StorySection::new()
+                    .child(StoryItem::new(
+                        "With free availability indicator",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
+                    ))
+                    .child(StoryItem::new(
+                        "With busy availability indicator",
+                        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)),
+                StorySection::new()
+                    .child(StoryItem::new(
+                        "With info border",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .border_color(cx.theme().status().info_border),
+                    ))
+                    .child(StoryItem::new(
+                        "With error border",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .border_color(cx.theme().status().error_border),
+                    )),
             )
             .child(
-                Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                    .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
+                StorySection::new()
+                    .child(StoryItem::new(
+                        "With muted audio indicator",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
+                    ))
+                    .child(StoryItem::new(
+                        "With deafened audio indicator",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
+                    )),
             )
     }
 }