Style the buttons in the contact panel and contact finder

Max Brunsfeld , Nathan Sobo , and Antonio Scandurra created

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

assets/themes/cave-dark.json                | 33 ++++---
assets/themes/cave-light.json               | 33 ++++---
assets/themes/dark.json                     | 33 ++++---
assets/themes/light.json                    | 33 ++++---
assets/themes/solarized-dark.json           | 33 ++++---
assets/themes/solarized-light.json          | 33 ++++---
assets/themes/sulphurpool-dark.json         | 33 ++++---
assets/themes/sulphurpool-light.json        | 33 ++++---
crates/contacts_panel/src/contact_finder.rs | 13 --
crates/contacts_panel/src/contacts_panel.rs | 99 ++++++++++++----------
crates/theme/src/theme.rs                   |  2 
styles/src/styleTree/contactFinder.ts       |  7 +
styles/src/styleTree/contactsPanel.ts       | 10 +
13 files changed, 230 insertions(+), 165 deletions(-)

Detailed changes

assets/themes/cave-dark.json 🔗

@@ -1245,7 +1245,8 @@
         "left": 6
       },
       "color": "#e2dfe7",
-      "width": 8
+      "button_width": 8,
+      "icon_width": 8
     },
     "row_height": 28,
     "tree_branch_color": "#655f6d",
@@ -1264,17 +1265,20 @@
     },
     "contact_button": {
       "background": "#26232a",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#e2dfe7",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#5852603d"
+      }
     },
     "disabled_contact_button": {
       "background": "#26232a",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#8b8792",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     },
     "header": {
       "family": "Zed Mono",
@@ -1482,17 +1486,20 @@
     },
     "contact_button": {
       "background": "#26232a",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#e2dfe7",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#5852603d"
+      }
     },
     "disabled_contact_button": {
       "background": "#26232a",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#8b8792",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     }
   },
   "search": {

assets/themes/cave-light.json 🔗

@@ -1245,7 +1245,8 @@
         "left": 6
       },
       "color": "#26232a",
-      "width": 8
+      "button_width": 8,
+      "icon_width": 8
     },
     "row_height": 28,
     "tree_branch_color": "#7e7887",
@@ -1264,17 +1265,20 @@
     },
     "contact_button": {
       "background": "#e2dfe7",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#26232a",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#8b87921f"
+      }
     },
     "disabled_contact_button": {
       "background": "#e2dfe7",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#585260",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     },
     "header": {
       "family": "Zed Mono",
@@ -1482,17 +1486,20 @@
     },
     "contact_button": {
       "background": "#e2dfe7",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#26232a",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#8b87921f"
+      }
     },
     "disabled_contact_button": {
       "background": "#e2dfe7",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#585260",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     }
   },
   "search": {

assets/themes/dark.json 🔗

@@ -1245,7 +1245,8 @@
         "left": 6
       },
       "color": "#c6c6c6",
-      "width": 8
+      "button_width": 8,
+      "icon_width": 8
     },
     "row_height": 28,
     "tree_branch_color": "#404040",
@@ -1264,17 +1265,20 @@
     },
     "contact_button": {
       "background": "#2b2b2b",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#c6c6c6",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#323232"
+      }
     },
     "disabled_contact_button": {
       "background": "#2b2b2b",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#555555",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     },
     "header": {
       "family": "Zed Mono",
@@ -1482,17 +1486,20 @@
     },
     "contact_button": {
       "background": "#2b2b2b",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#c6c6c6",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#323232"
+      }
     },
     "disabled_contact_button": {
       "background": "#2b2b2b",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#555555",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     }
   },
   "search": {

assets/themes/light.json 🔗

@@ -1245,7 +1245,8 @@
         "left": 6
       },
       "color": "#393939",
-      "width": 8
+      "button_width": 8,
+      "icon_width": 8
     },
     "row_height": 28,
     "tree_branch_color": "#e3e3e3",
@@ -1264,17 +1265,20 @@
     },
     "contact_button": {
       "background": "#eaeaea",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#393939",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#e3e3e3"
+      }
     },
     "disabled_contact_button": {
       "background": "#eaeaea",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#9c9c9c",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     },
     "header": {
       "family": "Zed Mono",
@@ -1482,17 +1486,20 @@
     },
     "contact_button": {
       "background": "#eaeaea",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#393939",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#e3e3e3"
+      }
     },
     "disabled_contact_button": {
       "background": "#eaeaea",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#9c9c9c",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     }
   },
   "search": {

assets/themes/solarized-dark.json 🔗

@@ -1245,7 +1245,8 @@
         "left": 6
       },
       "color": "#eee8d5",
-      "width": 8
+      "button_width": 8,
+      "icon_width": 8
     },
     "row_height": 28,
     "tree_branch_color": "#657b83",
@@ -1264,17 +1265,20 @@
     },
     "contact_button": {
       "background": "#073642",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#eee8d5",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#586e753d"
+      }
     },
     "disabled_contact_button": {
       "background": "#073642",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#93a1a1",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     },
     "header": {
       "family": "Zed Mono",
@@ -1482,17 +1486,20 @@
     },
     "contact_button": {
       "background": "#073642",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#eee8d5",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#586e753d"
+      }
     },
     "disabled_contact_button": {
       "background": "#073642",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#93a1a1",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     }
   },
   "search": {

assets/themes/solarized-light.json 🔗

@@ -1245,7 +1245,8 @@
         "left": 6
       },
       "color": "#073642",
-      "width": 8
+      "button_width": 8,
+      "icon_width": 8
     },
     "row_height": 28,
     "tree_branch_color": "#839496",
@@ -1264,17 +1265,20 @@
     },
     "contact_button": {
       "background": "#eee8d5",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#073642",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#93a1a11f"
+      }
     },
     "disabled_contact_button": {
       "background": "#eee8d5",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#586e75",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     },
     "header": {
       "family": "Zed Mono",
@@ -1482,17 +1486,20 @@
     },
     "contact_button": {
       "background": "#eee8d5",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#073642",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#93a1a11f"
+      }
     },
     "disabled_contact_button": {
       "background": "#eee8d5",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#586e75",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     }
   },
   "search": {

assets/themes/sulphurpool-dark.json 🔗

@@ -1245,7 +1245,8 @@
         "left": 6
       },
       "color": "#dfe2f1",
-      "width": 8
+      "button_width": 8,
+      "icon_width": 8
     },
     "row_height": 28,
     "tree_branch_color": "#6b7394",
@@ -1264,17 +1265,20 @@
     },
     "contact_button": {
       "background": "#293256",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#dfe2f1",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#5e66873d"
+      }
     },
     "disabled_contact_button": {
       "background": "#293256",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#979db4",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     },
     "header": {
       "family": "Zed Mono",
@@ -1482,17 +1486,20 @@
     },
     "contact_button": {
       "background": "#293256",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#dfe2f1",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#5e66873d"
+      }
     },
     "disabled_contact_button": {
       "background": "#293256",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#979db4",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     }
   },
   "search": {

assets/themes/sulphurpool-light.json 🔗

@@ -1245,7 +1245,8 @@
         "left": 6
       },
       "color": "#293256",
-      "width": 8
+      "button_width": 8,
+      "icon_width": 8
     },
     "row_height": 28,
     "tree_branch_color": "#898ea4",
@@ -1264,17 +1265,20 @@
     },
     "contact_button": {
       "background": "#dfe2f1",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#293256",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#979db41f"
+      }
     },
     "disabled_contact_button": {
       "background": "#dfe2f1",
-      "corner_radius": 8,
-      "padding": 4,
       "color": "#5e6687",
-      "width": 8
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     },
     "header": {
       "family": "Zed Mono",
@@ -1482,17 +1486,20 @@
     },
     "contact_button": {
       "background": "#dfe2f1",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#293256",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8,
+      "hover": {
+        "background": "#979db41f"
+      }
     },
     "disabled_contact_button": {
       "background": "#dfe2f1",
-      "corner_radius": 9,
-      "padding": 4,
       "color": "#5e6687",
-      "width": 10
+      "icon_width": 8,
+      "button_width": 16,
+      "corner_radius": 8
     }
   },
   "search": {

crates/contacts_panel/src/contact_finder.rs 🔗

@@ -9,6 +9,8 @@ use std::sync::Arc;
 use util::TryFutureExt;
 use workspace::Workspace;
 
+use crate::render_icon_button;
+
 actions!(contact_finder, [Toggle]);
 
 pub fn init(cx: &mut MutableAppContext) {
@@ -142,16 +144,7 @@ impl PickerDelegate for ContactFinder {
                     .boxed(),
             )
             .with_child(
-                Svg::new(icon_path)
-                    .with_color(button_style.color)
-                    .constrained()
-                    .with_width(button_style.icon_width)
-                    .aligned()
-                    .constrained()
-                    .with_width(button_style.button_width)
-                    .with_height(button_style.button_width)
-                    .contained()
-                    .with_style(button_style.container)
+                render_icon_button(button_style, icon_path)
                     .aligned()
                     .flex_float()
                     .boxed(),

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -14,6 +14,7 @@ use gpui::{
 use serde::Deserialize;
 use settings::Settings;
 use std::sync::Arc;
+use theme::IconButton;
 use workspace::{AppState, JoinProject};
 
 impl_actions!(
@@ -335,49 +336,38 @@ impl ContactsPanel {
                 .boxed(),
             );
 
-        let button_style = if user_store.read(cx).is_contact_request_pending(&user) {
-            &theme.disabled_contact_button
-        } else {
-            &theme.contact_button
-        };
+        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
 
         row.add_children([
-            MouseEventHandler::new::<Reject, _, _>(user.id as usize, cx, |_, _| {
-                Svg::new("icons/reject.svg")
-                    .with_color(button_style.color)
-                    .constrained()
-                    .with_width(button_style.icon_width)
-                    .aligned()
-                    .constrained()
-                    .with_width(button_style.button_width)
-                    .with_height(button_style.button_width)
-                    .contained()
-                    .with_style(button_style.container)
+            MouseEventHandler::new::<Reject, _, _>(user.id as usize, cx, |mouse_state, _| {
+                let button_style = if is_contact_request_pending {
+                    &theme.disabled_contact_button
+                } else {
+                    &theme.contact_button.style_for(mouse_state, false)
+                };
+                render_icon_button(button_style, "icons/reject.svg")
                     .aligned()
+                    .flex_float()
                     .boxed()
             })
+            .with_cursor_style(CursorStyle::PointingHand)
             .on_click(move |_, cx| {
                 cx.dispatch_action(RespondToContactRequest {
                     user_id,
                     accept: false,
                 })
             })
-            .with_cursor_style(CursorStyle::PointingHand)
             .flex_float()
             .boxed(),
-            MouseEventHandler::new::<Accept, _, _>(user.id as usize, cx, |_, _| {
-                Svg::new("icons/accept.svg")
-                    .with_color(button_style.color)
-                    .constrained()
-                    .with_width(button_style.icon_width)
-                    .with_height(button_style.icon_width)
-                    .aligned()
-                    .constrained()
-                    .with_width(button_style.button_width)
-                    .with_height(button_style.button_width)
-                    .contained()
-                    .with_style(button_style.container)
+            MouseEventHandler::new::<Accept, _, _>(user.id as usize, cx, |mouse_state, _| {
+                let button_style = if is_contact_request_pending {
+                    &theme.disabled_contact_button
+                } else {
+                    &theme.contact_button.style_for(mouse_state, false)
+                };
+                render_icon_button(button_style, "icons/accept.svg")
                     .aligned()
+                    .flex_float()
                     .boxed()
             })
             .on_click(move |_, cx| {
@@ -402,11 +392,7 @@ impl ContactsPanel {
         enum Cancel {}
 
         let user_id = user.id;
-        let button_style = if user_store.read(cx).is_contact_request_pending(&user) {
-            &theme.disabled_contact_button
-        } else {
-            &theme.contact_button
-        };
+        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
 
         let mut row = Flex::row()
             .with_children(user.avatar.clone().map(|avatar| {
@@ -429,19 +415,21 @@ impl ContactsPanel {
             );
 
         row.add_child(
-            MouseEventHandler::new::<Cancel, _, _>(user.id as usize, cx, |_, _| {
-                Svg::new("icons/reject.svg")
-                    .with_color(button_style.color)
-                    .constrained()
-                    .with_width(button_style.icon_width)
-                    .with_height(button_style.icon_width)
-                    .contained()
-                    .with_style(button_style.container)
+            MouseEventHandler::new::<Cancel, _, _>(user.id as usize, cx, |mouse_state, _| {
+                let button_style = if is_contact_request_pending {
+                    &theme.disabled_contact_button
+                } else {
+                    &theme.contact_button.style_for(mouse_state, false)
+                };
+
+                render_icon_button(button_style, "icons/reject.svg")
                     .aligned()
+                    .flex_float()
                     .boxed()
             })
-            .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id)))
+            .with_padding(Padding::uniform(2.))
             .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id)))
             .flex_float()
             .boxed(),
         );
@@ -546,17 +534,21 @@ impl ContactsPanel {
                 &Default::default(),
                 executor.clone(),
             ));
-            if !matches.is_empty() {
-                let (online_contacts, offline_contacts) = matches
-                    .iter()
-                    .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
 
+            let (online_contacts, offline_contacts) = matches
+                .iter()
+                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
+
+            if !online_contacts.is_empty() {
                 self.entries.push(ContactEntry::Header("Online"));
                 self.entries.extend(
                     online_contacts
                         .into_iter()
                         .map(|mat| ContactEntry::Contact(contacts[mat.candidate_id].clone())),
                 );
+            }
+
+            if !offline_contacts.is_empty() {
                 self.entries.push(ContactEntry::Header("Offline"));
                 self.entries.extend(
                     offline_contacts
@@ -595,6 +587,19 @@ impl ContactsPanel {
     }
 }
 
+fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
+    Svg::new(svg_path)
+        .with_color(style.color)
+        .constrained()
+        .with_width(style.icon_width)
+        .aligned()
+        .contained()
+        .with_style(style.container)
+        .constrained()
+        .with_width(style.button_width)
+        .with_height(style.button_width)
+}
+
 pub enum Event {}
 
 impl Entity for ContactsPanel {

crates/theme/src/theme.rs 🔗

@@ -241,7 +241,7 @@ pub struct ContactsPanel {
     pub row_height: f32,
     pub contact_avatar: ImageStyle,
     pub contact_username: ContainedText,
-    pub contact_button: IconButton,
+    pub contact_button: Interactive<IconButton>,
     pub disabled_contact_button: IconButton,
     pub tree_branch_width: f32,
     pub tree_branch_color: Color,

styles/src/styleTree/contactFinder.ts 🔗

@@ -23,7 +23,12 @@ export default function contactFinder(theme: Theme) {
         left: 8,
       },
     },
-    contactButton,
+    contactButton: {
+      ...contactButton,
+      hover: {
+        background: backgroundColor(theme, 100, "hovered")
+      }
+    },
     disabledContactButton: {
       ...contactButton,
       background: backgroundColor(theme, 100),

styles/src/styleTree/contactsPanel.ts 🔗

@@ -58,7 +58,8 @@ export default function contactsPanel(theme: Theme) {
     addContactButton: {
       margin: { left: 6 },
       color: iconColor(theme, "primary"),
-      width: 8,
+      buttonWidth: 8,
+      iconWidth: 8,
     },
     rowHeight: 28,
     treeBranchColor: borderColor(theme, "muted"),
@@ -73,7 +74,12 @@ export default function contactsPanel(theme: Theme) {
         left: 8,
       },
     },
-    contactButton,
+    contactButton: {
+      ...contactButton,
+      hover: {
+        background: backgroundColor(theme, 100, "hovered"),
+      },
+    },
     disabledContactButton: {
       ...contactButton,
       background: backgroundColor(theme, 100),