Start work on styling of follower avatars in facepiles

Max Brunsfeld created

* Make follower avatars smaller than top-level avatars
* Make avatars in facepiles overlap
* Render an opaque background behind avatars in facepiles.

Change summary

crates/collab_ui/src/collab_titlebar_item.rs | 35 ++++++++++++++-------
crates/theme/src/theme.rs                    | 14 ++++++-
styles/src/styleTree/workspace.ts            | 24 ++++++++++-----
3 files changed, 50 insertions(+), 23 deletions(-)

Detailed changes

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -17,7 +17,7 @@ use gpui::{
 };
 use settings::Settings;
 use std::{ops::Range, sync::Arc};
-use theme::Theme;
+use theme::{AvatarStyle, Theme};
 use util::ResultExt;
 use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
 
@@ -614,21 +614,22 @@ impl CollabTitlebarItem {
         if let Some(location) = location {
             if let ParticipantLocation::SharedProject { project_id } = location {
                 if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
-                    avatar_style = theme.workspace.titlebar.avatar;
+                    avatar_style = &theme.workspace.titlebar.avatar;
                 } else {
-                    avatar_style = theme.workspace.titlebar.inactive_avatar;
+                    avatar_style = &theme.workspace.titlebar.inactive_avatar;
                 }
             } else {
-                avatar_style = theme.workspace.titlebar.inactive_avatar;
+                avatar_style = &theme.workspace.titlebar.inactive_avatar;
             }
         } else {
-            avatar_style = theme.workspace.titlebar.avatar;
+            avatar_style = &theme.workspace.titlebar.avatar;
         }
 
         let content = Stack::new()
             .with_children(user.avatar.as_ref().map(|avatar| {
                 let flex = Flex::row()
-                    .with_child(Self::render_face(avatar.clone(), avatar_style, theme))
+                    // .with_reversed_paint_order()
+                    .with_child(Self::render_face(avatar.clone(), avatar_style.clone()))
                     .with_children(
                         (|| {
                             let room = room?.read(cx);
@@ -652,14 +653,22 @@ impl CollabTitlebarItem {
                                         }
                                     })?;
 
-                                Some(Self::render_face(avatar.clone(), avatar_style, theme))
+                                Some(
+                                    Container::new(Self::render_face(
+                                        avatar.clone(),
+                                        theme.workspace.titlebar.follower_avatar.clone(),
+                                    ))
+                                    .with_margin_left(
+                                        -1.0 * theme.workspace.titlebar.follower_avatar_overlap,
+                                    )
+                                    .boxed(),
+                                )
                             }))
                         })()
                         .into_iter()
                         .flatten(),
                     );
 
-                let room = ActiveCall::global(cx).read(cx).room();
                 if let (Some(replica_id), Some(room)) = (replica_id, room) {
                     let followed_by_self = is_being_followed
                         && room
@@ -669,7 +678,6 @@ impl CollabTitlebarItem {
                             .any(|&follower| {
                                 Some(follower) == workspace.read(cx).client().peer_id()
                             });
-
                     if followed_by_self {
                         let color = theme.editor.replica_selection_style(replica_id).selection;
                         return flex.contained().with_background_color(color).boxed();
@@ -742,11 +750,14 @@ impl CollabTitlebarItem {
         }
     }
 
-    fn render_face(avatar: Arc<ImageData>, avatar_style: ImageStyle, theme: &Theme) -> ElementBox {
+    fn render_face(avatar: Arc<ImageData>, avatar_style: AvatarStyle) -> ElementBox {
         Image::new(avatar)
-            .with_style(avatar_style)
+            .with_style(avatar_style.image)
             .constrained()
-            .with_width(theme.workspace.titlebar.avatar_width)
+            .with_width(avatar_style.width)
+            .contained()
+            .with_background_color(Color::white())
+            .with_corner_radius(avatar_style.image.corner_radius)
             .aligned()
             .boxed()
     }

crates/theme/src/theme.rs 🔗

@@ -74,12 +74,13 @@ pub struct Titlebar {
     pub container: ContainerStyle,
     pub height: f32,
     pub title: TextStyle,
-    pub avatar_width: f32,
     pub avatar_margin: f32,
     pub avatar_ribbon: AvatarRibbon,
+    pub follower_avatar_overlap: f32,
     pub offline_icon: OfflineIcon,
-    pub avatar: ImageStyle,
-    pub inactive_avatar: ImageStyle,
+    pub avatar: AvatarStyle,
+    pub inactive_avatar: AvatarStyle,
+    pub follower_avatar: AvatarStyle,
     pub sign_in_prompt: Interactive<ContainedText>,
     pub outdated_warning: ContainedText,
     pub share_button: Interactive<ContainedText>,
@@ -88,6 +89,13 @@ pub struct Titlebar {
     pub toggle_contacts_badge: ContainerStyle,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct AvatarStyle {
+    #[serde(flatten)]
+    pub image: ImageStyle,
+    pub width: f32,
+}
+
 #[derive(Deserialize, Default)]
 pub struct ContactsPopover {
     #[serde(flatten)]

styles/src/styleTree/workspace.ts 🔗

@@ -41,6 +41,7 @@ export default function workspace(colorScheme: ColorScheme) {
     },
   };
   const avatarWidth = 18;
+  const followerAvatarWidth = 14;
 
   return {
     background: background(layer),
@@ -80,7 +81,6 @@ export default function workspace(colorScheme: ColorScheme) {
     },
     statusBar: statusBar(colorScheme),
     titlebar: {
-      avatarWidth,
       avatarMargin: 8,
       height: 33, // 32px + 1px for overlaid border
       background: background(layer),
@@ -95,6 +95,7 @@ export default function workspace(colorScheme: ColorScheme) {
 
       // Collaborators
       avatar: {
+        width: avatarWidth,
         cornerRadius: avatarWidth / 2,
         border: {
           color: "#00000088",
@@ -102,6 +103,7 @@ export default function workspace(colorScheme: ColorScheme) {
         },
       },
       inactiveAvatar: {
+        width: avatarWidth,
         cornerRadius: avatarWidth / 2,
         border: {
           color: "#00000088",
@@ -109,6 +111,15 @@ export default function workspace(colorScheme: ColorScheme) {
         },
         grayscale: true,
       },
+      followerAvatar: {
+        width: followerAvatarWidth,
+        cornerRadius: followerAvatarWidth / 2,
+        border: {
+          color: "#00000088",
+          width: 1,
+        },
+      },
+      followerAvatarOverlap: 4,
       avatarRibbon: {
         height: 3,
         width: 12,
@@ -118,7 +129,7 @@ export default function workspace(colorScheme: ColorScheme) {
       // Sign in buttom
       // FlatButton, Variant
       signInPrompt: {
-        ...titlebarButton
+        ...titlebarButton,
       },
 
       // Offline Indicator
@@ -184,8 +195,8 @@ export default function workspace(colorScheme: ColorScheme) {
         background: foreground(layer, "accent"),
       },
       shareButton: {
-        ...titlebarButton
-      }
+        ...titlebarButton,
+      },
     },
 
     toolbar: {
@@ -241,9 +252,6 @@ export default function workspace(colorScheme: ColorScheme) {
         shadow: colorScheme.modalShadow,
       },
     },
-    dropTargetOverlayColor: withOpacity(
-      foreground(layer, "variant"),
-      0.5
-    ),
+    dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5),
   };
 }