WIP - start work on keyboard navigation in contacts panel

This commit is contained in:
Max Brunsfeld 2022-05-10 21:45:49 -07:00
parent 297fa1af55
commit 08a7543913
13 changed files with 792 additions and 760 deletions

2
Cargo.lock generated
View File

@ -936,9 +936,11 @@ dependencies = [
"futures",
"fuzzy",
"gpui",
"language",
"log",
"picker",
"postage",
"project",
"serde",
"settings",
"theme",

View File

@ -1249,20 +1249,29 @@
"button_width": 8,
"icon_width": 8
},
"row": {
"padding": {
"left": 8
}
},
"row_height": 28,
"header": {
"header_row": {
"family": "Zed Mono",
"color": "#8b8792",
"size": 14,
"margin": {
"top": 8
},
"active": {
"family": "Zed Mono",
"color": "#e2dfe7",
"size": 14,
"background": "#5852605c"
}
},
"contact_row": {
"padding": {
"left": 8
},
"active": {
"background": "#5852605c"
}
},
"row_height": 28,
"tree_branch_color": "#655f6d",
"tree_branch_width": 1,
"contact_avatar": {
@ -1294,26 +1303,7 @@
"button_width": 16,
"corner_radius": 8
},
"project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#7e7887",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
}
},
"shared_project": {
"shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1332,9 +1322,15 @@
"left": 8
},
"background": "#26232a",
"corner_radius": 6
"corner_radius": 6,
"hover": {
"background": "#5852603d"
},
"active": {
"background": "#5852605c"
}
},
"hovered_shared_project": {
"unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1352,47 +1348,14 @@
"padding": {
"left": 8
},
"background": "#5852603d",
"corner_radius": 6
},
"unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
"background": "#26232a",
"corner_radius": 6,
"hover": {
"background": "#5852603d"
},
"name": {
"family": "Zed Mono",
"color": "#7e7887",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
"active": {
"background": "#5852605c"
}
},
"hovered_unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#7e7887",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
},
"corner_radius": 6
}
},
"contact_finder": {

View File

@ -1249,20 +1249,29 @@
"button_width": 8,
"icon_width": 8
},
"row": {
"padding": {
"left": 8
}
},
"row_height": 28,
"header": {
"header_row": {
"family": "Zed Mono",
"color": "#585260",
"size": 14,
"margin": {
"top": 8
},
"active": {
"family": "Zed Mono",
"color": "#26232a",
"size": 14,
"background": "#8b87922e"
}
},
"contact_row": {
"padding": {
"left": 8
},
"active": {
"background": "#8b87922e"
}
},
"row_height": 28,
"tree_branch_color": "#7e7887",
"tree_branch_width": 1,
"contact_avatar": {
@ -1294,26 +1303,7 @@
"button_width": 16,
"corner_radius": 8
},
"project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#655f6d",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
}
},
"shared_project": {
"shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1332,9 +1322,15 @@
"left": 8
},
"background": "#e2dfe7",
"corner_radius": 6
"corner_radius": 6,
"hover": {
"background": "#8b87921f"
},
"active": {
"background": "#8b87922e"
}
},
"hovered_shared_project": {
"unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1352,47 +1348,14 @@
"padding": {
"left": 8
},
"background": "#8b87921f",
"corner_radius": 6
},
"unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
"background": "#e2dfe7",
"corner_radius": 6,
"hover": {
"background": "#8b87921f"
},
"name": {
"family": "Zed Mono",
"color": "#655f6d",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
"active": {
"background": "#8b87922e"
}
},
"hovered_unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#655f6d",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
},
"corner_radius": 6
}
},
"contact_finder": {

View File

@ -1249,20 +1249,29 @@
"button_width": 8,
"icon_width": 8
},
"row": {
"padding": {
"left": 8
}
},
"row_height": 28,
"header": {
"header_row": {
"family": "Zed Mono",
"color": "#9c9c9c",
"size": 14,
"margin": {
"top": 8
},
"active": {
"family": "Zed Mono",
"color": "#f1f1f1",
"size": 14,
"background": "#1c1c1c"
}
},
"contact_row": {
"padding": {
"left": 8
},
"active": {
"background": "#1c1c1c"
}
},
"row_height": 28,
"tree_branch_color": "#404040",
"tree_branch_width": 1,
"contact_avatar": {
@ -1294,26 +1303,7 @@
"button_width": 16,
"corner_radius": 8
},
"project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#474747",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
}
},
"shared_project": {
"shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1332,9 +1322,15 @@
"left": 8
},
"background": "#1c1c1c",
"corner_radius": 6
"corner_radius": 6,
"hover": {
"background": "#232323"
},
"active": {
"background": "#2b2b2b"
}
},
"hovered_shared_project": {
"unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1352,47 +1348,14 @@
"padding": {
"left": 8
},
"background": "#232323",
"corner_radius": 6
},
"unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
"background": "#1c1c1c",
"corner_radius": 6,
"hover": {
"background": "#232323"
},
"name": {
"family": "Zed Mono",
"color": "#474747",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
"active": {
"background": "#2b2b2b"
}
},
"hovered_unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#474747",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
},
"corner_radius": 6
}
},
"contact_finder": {

View File

@ -1249,20 +1249,29 @@
"button_width": 8,
"icon_width": 8
},
"row": {
"padding": {
"left": 8
}
},
"row_height": 28,
"header": {
"header_row": {
"family": "Zed Mono",
"color": "#474747",
"size": 14,
"margin": {
"top": 8
},
"active": {
"family": "Zed Mono",
"color": "#2b2b2b",
"size": 14,
"background": "#d5d5d5"
}
},
"contact_row": {
"padding": {
"left": 8
},
"active": {
"background": "#d5d5d5"
}
},
"row_height": 28,
"tree_branch_color": "#e3e3e3",
"tree_branch_width": 1,
"contact_avatar": {
@ -1294,26 +1303,7 @@
"button_width": 16,
"corner_radius": 8
},
"project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#808080",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
}
},
"shared_project": {
"shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1332,9 +1322,15 @@
"left": 8
},
"background": "#f8f8f8",
"corner_radius": 6
"corner_radius": 6,
"hover": {
"background": "#eaeaea"
},
"active": {
"background": "#e3e3e3"
}
},
"hovered_shared_project": {
"unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1352,47 +1348,14 @@
"padding": {
"left": 8
},
"background": "#eaeaea",
"corner_radius": 6
},
"unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
"background": "#f8f8f8",
"corner_radius": 6,
"hover": {
"background": "#eaeaea"
},
"name": {
"family": "Zed Mono",
"color": "#808080",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
"active": {
"background": "#e3e3e3"
}
},
"hovered_unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#808080",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
},
"corner_radius": 6
}
},
"contact_finder": {

View File

@ -1249,20 +1249,29 @@
"button_width": 8,
"icon_width": 8
},
"row": {
"padding": {
"left": 8
}
},
"row_height": 28,
"header": {
"header_row": {
"family": "Zed Mono",
"color": "#93a1a1",
"size": 14,
"margin": {
"top": 8
},
"active": {
"family": "Zed Mono",
"color": "#eee8d5",
"size": 14,
"background": "#586e755c"
}
},
"contact_row": {
"padding": {
"left": 8
},
"active": {
"background": "#586e755c"
}
},
"row_height": 28,
"tree_branch_color": "#657b83",
"tree_branch_width": 1,
"contact_avatar": {
@ -1294,26 +1303,7 @@
"button_width": 16,
"corner_radius": 8
},
"project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#839496",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
}
},
"shared_project": {
"shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1332,9 +1322,15 @@
"left": 8
},
"background": "#073642",
"corner_radius": 6
"corner_radius": 6,
"hover": {
"background": "#586e753d"
},
"active": {
"background": "#586e755c"
}
},
"hovered_shared_project": {
"unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1352,47 +1348,14 @@
"padding": {
"left": 8
},
"background": "#586e753d",
"corner_radius": 6
},
"unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
"background": "#073642",
"corner_radius": 6,
"hover": {
"background": "#586e753d"
},
"name": {
"family": "Zed Mono",
"color": "#839496",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
"active": {
"background": "#586e755c"
}
},
"hovered_unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#839496",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
},
"corner_radius": 6
}
},
"contact_finder": {

View File

@ -1249,20 +1249,29 @@
"button_width": 8,
"icon_width": 8
},
"row": {
"padding": {
"left": 8
}
},
"row_height": 28,
"header": {
"header_row": {
"family": "Zed Mono",
"color": "#586e75",
"size": 14,
"margin": {
"top": 8
},
"active": {
"family": "Zed Mono",
"color": "#073642",
"size": 14,
"background": "#93a1a12e"
}
},
"contact_row": {
"padding": {
"left": 8
},
"active": {
"background": "#93a1a12e"
}
},
"row_height": 28,
"tree_branch_color": "#839496",
"tree_branch_width": 1,
"contact_avatar": {
@ -1294,26 +1303,7 @@
"button_width": 16,
"corner_radius": 8
},
"project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#657b83",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
}
},
"shared_project": {
"shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1332,9 +1322,15 @@
"left": 8
},
"background": "#eee8d5",
"corner_radius": 6
"corner_radius": 6,
"hover": {
"background": "#93a1a11f"
},
"active": {
"background": "#93a1a12e"
}
},
"hovered_shared_project": {
"unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1352,47 +1348,14 @@
"padding": {
"left": 8
},
"background": "#93a1a11f",
"corner_radius": 6
},
"unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
"background": "#eee8d5",
"corner_radius": 6,
"hover": {
"background": "#93a1a11f"
},
"name": {
"family": "Zed Mono",
"color": "#657b83",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
"active": {
"background": "#93a1a12e"
}
},
"hovered_unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#657b83",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
},
"corner_radius": 6
}
},
"contact_finder": {

View File

@ -1249,20 +1249,29 @@
"button_width": 8,
"icon_width": 8
},
"row": {
"padding": {
"left": 8
}
},
"row_height": 28,
"header": {
"header_row": {
"family": "Zed Mono",
"color": "#979db4",
"size": 14,
"margin": {
"top": 8
},
"active": {
"family": "Zed Mono",
"color": "#dfe2f1",
"size": 14,
"background": "#5e66875c"
}
},
"contact_row": {
"padding": {
"left": 8
},
"active": {
"background": "#5e66875c"
}
},
"row_height": 28,
"tree_branch_color": "#6b7394",
"tree_branch_width": 1,
"contact_avatar": {
@ -1294,26 +1303,7 @@
"button_width": 16,
"corner_radius": 8
},
"project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#898ea4",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
}
},
"shared_project": {
"shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1332,9 +1322,15 @@
"left": 8
},
"background": "#293256",
"corner_radius": 6
"corner_radius": 6,
"hover": {
"background": "#5e66873d"
},
"active": {
"background": "#5e66875c"
}
},
"hovered_shared_project": {
"unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1352,47 +1348,14 @@
"padding": {
"left": 8
},
"background": "#5e66873d",
"corner_radius": 6
},
"unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
"background": "#293256",
"corner_radius": 6,
"hover": {
"background": "#5e66873d"
},
"name": {
"family": "Zed Mono",
"color": "#898ea4",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
"active": {
"background": "#5e66875c"
}
},
"hovered_unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#898ea4",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
},
"corner_radius": 6
}
},
"contact_finder": {

View File

@ -1249,20 +1249,29 @@
"button_width": 8,
"icon_width": 8
},
"row": {
"padding": {
"left": 8
}
},
"row_height": 28,
"header": {
"header_row": {
"family": "Zed Mono",
"color": "#5e6687",
"size": 14,
"margin": {
"top": 8
},
"active": {
"family": "Zed Mono",
"color": "#293256",
"size": 14,
"background": "#979db42e"
}
},
"contact_row": {
"padding": {
"left": 8
},
"active": {
"background": "#979db42e"
}
},
"row_height": 28,
"tree_branch_color": "#898ea4",
"tree_branch_width": 1,
"contact_avatar": {
@ -1294,26 +1303,7 @@
"button_width": 16,
"corner_radius": 8
},
"project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#6b7394",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
}
},
"shared_project": {
"shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1332,9 +1322,15 @@
"left": 8
},
"background": "#dfe2f1",
"corner_radius": 6
"corner_radius": 6,
"hover": {
"background": "#979db41f"
},
"active": {
"background": "#979db42e"
}
},
"hovered_shared_project": {
"unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@ -1352,47 +1348,14 @@
"padding": {
"left": 8
},
"background": "#979db41f",
"corner_radius": 6
},
"unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
"background": "#dfe2f1",
"corner_radius": 6,
"hover": {
"background": "#979db41f"
},
"name": {
"family": "Zed Mono",
"color": "#6b7394",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
"active": {
"background": "#979db42e"
}
},
"hovered_unshared_project": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
"corner_radius": 8,
"width": 14
},
"name": {
"family": "Zed Mono",
"color": "#6b7394",
"size": 14,
"margin": {
"right": 6
}
},
"padding": {
"left": 8
},
"corner_radius": 6
}
},
"contact_finder": {

View File

@ -21,3 +21,8 @@ futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@ -15,6 +15,7 @@ use serde::Deserialize;
use settings::Settings;
use std::sync::Arc;
use theme::IconButton;
use workspace::menu::{SelectNext, SelectPrev};
use workspace::{AppState, JoinProject};
impl_actions!(
@ -22,12 +23,13 @@ impl_actions!(
[RequestContact, RemoveContact, RespondToContactRequest]
);
#[derive(Debug)]
#[derive(Clone, Debug)]
enum ContactEntry {
Header(&'static str),
IncomingRequest(Arc<User>),
OutgoingRequest(Arc<User>),
Contact(Arc<Contact>),
ContactProject(Arc<Contact>, usize),
}
pub struct ContactsPanel {
@ -36,6 +38,7 @@ pub struct ContactsPanel {
list_state: ListState,
user_store: ModelHandle<UserStore>,
filter_editor: ViewHandle<Editor>,
selection: Option<usize>,
_maintain_contacts: Subscription,
}
@ -57,6 +60,8 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPanel::remove_contact);
cx.add_action(ContactsPanel::respond_to_contact_request);
cx.add_action(ContactsPanel::clear_filter);
cx.add_action(ContactsPanel::select_next);
cx.add_action(ContactsPanel::select_prev);
}
impl ContactsPanel {
@ -72,6 +77,7 @@ impl ContactsPanel {
cx.subscribe(&user_query_editor, |this, _, event, cx| {
if let editor::Event::BufferEdited = event {
this.selection.take();
this.update_entries(cx)
}
})
@ -88,17 +94,20 @@ impl ContactsPanel {
let theme = &theme.contacts_panel;
let current_user_id =
this.user_store.read(cx).current_user().map(|user| user.id);
let is_selected = this.selection == Some(ix);
match &this.entries[ix] {
ContactEntry::Header(text) => {
Label::new(text.to_string(), theme.header.text.clone())
let header_style =
theme.header_row.style_for(&Default::default(), is_selected);
Label::new(text.to_string(), header_style.text.clone())
.contained()
.aligned()
.left()
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(theme.header.container)
.with_style(header_style.container)
.boxed()
}
ContactEntry::IncomingRequest(user) => Self::render_contact_request(
@ -106,6 +115,7 @@ impl ContactsPanel {
this.user_store.clone(),
theme,
true,
is_selected,
cx,
),
ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
@ -113,18 +123,36 @@ impl ContactsPanel {
this.user_store.clone(),
theme,
false,
is_selected,
cx,
),
ContactEntry::Contact(contact) => Self::render_contact(
contact.clone(),
current_user_id,
app_state.clone(),
theme,
cx,
),
ContactEntry::Contact(contact) => {
Self::render_contact(contact.clone(), theme, is_selected)
}
ContactEntry::ContactProject(contact, project_ix) => {
let is_last_project_for_contact =
this.entries.get(ix + 1).map_or(true, |next| {
if let ContactEntry::ContactProject(next_contact, _) = next {
next_contact.user.id != contact.user.id
} else {
true
}
});
Self::render_contact_project(
contact.clone(),
current_user_id,
*project_ix,
app_state.clone(),
theme,
is_last_project_for_contact,
is_selected,
cx,
)
}
}
}
}),
selection: None,
entries: Default::default(),
match_candidates: Default::default(),
filter_editor: user_query_editor,
@ -137,175 +165,173 @@ impl ContactsPanel {
}
fn render_contact(
contact: Arc<Contact>,
theme: &theme::ContactsPanel,
is_selected: bool,
) -> ElementBox {
Flex::row()
.with_children(contact.user.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
.boxed()
}))
.with_child(
Label::new(
contact.user.github_login.clone(),
theme.contact_username.text.clone(),
)
.contained()
.with_style(theme.contact_username.container)
.aligned()
.left()
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(
*theme
.contact_row
.style_for(&Default::default(), is_selected),
)
.boxed()
}
fn render_contact_project(
contact: Arc<Contact>,
current_user_id: Option<u64>,
project_ix: usize,
app_state: Arc<AppState>,
theme: &theme::ContactsPanel,
is_last_project: bool,
is_selected: bool,
cx: &mut LayoutContext,
) -> ElementBox {
let project_count = contact.non_empty_projects().count();
let project = &contact.projects[project_ix];
let project_id = project.id;
let font_cache = cx.font_cache();
let line_height = theme.unshared_project.name.text.line_height(font_cache);
let cap_height = theme.unshared_project.name.text.cap_height(font_cache);
let baseline_offset = theme.unshared_project.name.text.baseline_offset(font_cache)
+ (theme.unshared_project.height - line_height) / 2.;
let tree_branch_width = theme.tree_branch_width;
let tree_branch_color = theme.tree_branch_color;
let host_avatar_height = theme
.contact_avatar
.width
.or(theme.contact_avatar.height)
.unwrap_or(0.);
let row = &theme.unshared_project_row.default;
let line_height = row.name.text.line_height(font_cache);
let cap_height = row.name.text.cap_height(font_cache);
let baseline_offset =
row.name.text.baseline_offset(font_cache) + (row.height - line_height) / 2.;
let tree_branch_width = theme.tree_branch_width;
let tree_branch_color = theme.tree_branch_color;
Flex::column()
Flex::row()
.with_child(
Flex::row()
.with_children(contact.user.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
.boxed()
}))
.with_child(
Label::new(
contact.user.github_login.clone(),
theme.contact_username.text.clone(),
)
.contained()
.with_style(theme.contact_username.container)
.aligned()
.left()
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.boxed(),
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.) - (tree_branch_width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch_width,
if is_last_project {
end_y
} else {
bounds.max_y()
},
),
),
background: Some(tree_branch_color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch_width),
),
background: Some(tree_branch_color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
.constrained()
.with_width(host_avatar_height)
.boxed(),
)
.with_children(
contact
.non_empty_projects()
.enumerate()
.map(|(ix, project)| {
let project_id = project.id;
.with_child({
let is_host = Some(contact.user.id) == current_user_id;
let is_guest = !is_host
&& project
.guests
.iter()
.any(|guest| Some(guest.id) == current_user_id);
let is_shared = project.is_shared;
let app_state = app_state.clone();
MouseEventHandler::new::<JoinProject, _, _>(
project_id as usize,
cx,
|mouse_state, _| {
let style = if project.is_shared {
&theme.shared_project_row
} else {
&theme.unshared_project_row
}
.style_for(mouse_state, is_selected);
Flex::row()
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch_width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y =
bounds.min_y() + baseline_offset - (cap_height / 2.);
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch_width,
if ix + 1 == project_count {
end_y
} else {
bounds.max_y()
},
),
),
background: Some(tree_branch_color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch_width),
),
background: Some(tree_branch_color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
.constrained()
.with_width(host_avatar_height)
Label::new(
project.worktree_root_names.join(", "),
style.name.text.clone(),
)
.aligned()
.left()
.contained()
.with_style(style.name.container)
.boxed(),
)
.with_child({
let is_host = Some(contact.user.id) == current_user_id;
let is_guest = !is_host
&& project
.guests
.iter()
.any(|guest| Some(guest.id) == current_user_id);
let is_shared = project.is_shared;
let app_state = app_state.clone();
MouseEventHandler::new::<ContactsPanel, _, _>(
project_id as usize,
cx,
|mouse_state, _| {
let style = match (project.is_shared, mouse_state.hovered) {
(false, false) => &theme.unshared_project,
(false, true) => &theme.hovered_unshared_project,
(true, false) => &theme.shared_project,
(true, true) => &theme.hovered_shared_project,
};
Flex::row()
.with_child(
Label::new(
project.worktree_root_names.join(", "),
style.name.text.clone(),
)
.aligned()
.left()
.contained()
.with_style(style.name.container)
.boxed(),
)
.with_children(project.guests.iter().filter_map(
|participant| {
participant.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(style.guest_avatar)
.aligned()
.left()
.contained()
.with_margin_right(
style.guest_avatar_spacing,
)
.boxed()
})
},
))
.contained()
.with_style(style.container)
.constrained()
.with_height(style.height)
.boxed()
},
)
.with_cursor_style(if !is_host && is_shared {
CursorStyle::PointingHand
} else {
CursorStyle::Arrow
.with_children(project.guests.iter().filter_map(|participant| {
participant.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(style.guest_avatar)
.aligned()
.left()
.contained()
.with_margin_right(style.guest_avatar_spacing)
.boxed()
})
.on_click(move |_, cx| {
if !is_host && !is_guest {
cx.dispatch_global_action(JoinProject {
project_id,
app_state: app_state.clone(),
});
}
})
.flex(1., true)
.boxed()
})
}))
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.unshared_project.height)
.with_height(style.height)
.boxed()
}),
)
.contained()
.with_style(theme.row.clone())
},
)
.with_cursor_style(if !is_host && is_shared {
CursorStyle::PointingHand
} else {
CursorStyle::Arrow
})
.on_click(move |_, cx| {
if !is_host && !is_guest {
cx.dispatch_global_action(JoinProject {
project_id,
app_state: app_state.clone(),
});
}
})
.flex(1., true)
.boxed()
})
.constrained()
.with_height(row.height)
.boxed()
}
@ -314,6 +340,7 @@ impl ContactsPanel {
user_store: ModelHandle<UserStore>,
theme: &theme::ContactsPanel,
is_incoming: bool,
is_selected: bool,
cx: &mut LayoutContext,
) -> ElementBox {
enum Reject {}
@ -409,7 +436,11 @@ impl ContactsPanel {
row.constrained()
.with_height(theme.row_height)
.contained()
.with_style(theme.row)
.with_style(
*theme
.contact_row
.style_for(&Default::default(), is_selected),
)
.boxed()
}
@ -418,6 +449,7 @@ impl ContactsPanel {
let query = self.filter_editor.read(cx).text(cx);
let executor = cx.background().clone();
let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
self.entries.clear();
let mut request_entries = Vec::new();
@ -443,13 +475,11 @@ impl ContactsPanel {
&Default::default(),
executor.clone(),
));
if !matches.is_empty() {
request_entries.extend(
matches.iter().map(|mat| {
ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())
}),
);
}
request_entries.extend(
matches
.iter()
.map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
);
}
let outgoing = user_store.outgoing_contact_requests();
@ -474,13 +504,11 @@ impl ContactsPanel {
&Default::default(),
executor.clone(),
));
if !matches.is_empty() {
request_entries.extend(
matches.iter().map(|mat| {
ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())
}),
);
}
request_entries.extend(
matches
.iter()
.map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
);
}
if !request_entries.is_empty() {
@ -515,22 +543,33 @@ impl ContactsPanel {
.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())),
);
for (matches, name) in [(online_contacts, "Online"), (offline_contacts, "Offline")] {
if !matches.is_empty() {
self.entries.push(ContactEntry::Header(name));
for mat in matches {
let contact = &contacts[mat.candidate_id];
self.entries.push(ContactEntry::Contact(contact.clone()));
self.entries
.extend(contact.projects.iter().enumerate().filter_map(
|(ix, project)| {
if project.worktree_root_names.is_empty() {
None
} else {
Some(ContactEntry::ContactProject(contact.clone(), ix))
}
},
));
}
}
}
}
if !offline_contacts.is_empty() {
self.entries.push(ContactEntry::Header("Offline"));
self.entries.extend(
offline_contacts
.into_iter()
.map(|mat| ContactEntry::Contact(contacts[mat.candidate_id].clone())),
);
if let Some(selection) = &mut self.selection {
for (ix, entry) in self.entries.iter().enumerate() {
if Some(entry) == prev_selected_entry.as_ref() {
*selection = ix;
break;
}
}
}
@ -566,6 +605,30 @@ impl ContactsPanel {
self.filter_editor
.update(cx, |editor, cx| editor.set_text("", cx));
}
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selection {
if self.entries.len() > ix + 1 {
self.selection = Some(ix + 1);
}
} else if !self.entries.is_empty() {
self.selection = Some(0);
}
cx.notify();
self.list_state.reset(self.entries.len());
}
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selection {
if ix > 0 {
self.selection = Some(ix - 1);
} else {
self.selection = None;
}
}
cx.notify();
self.list_state.reset(self.entries.len());
}
}
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
@ -633,4 +696,249 @@ impl View for ContactsPanel {
.with_style(theme.container)
.boxed()
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.focus(&self.filter_editor);
}
fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx
}
}
impl PartialEq for ContactEntry {
fn eq(&self, other: &Self) -> bool {
match self {
ContactEntry::Header(name_1) => {
if let ContactEntry::Header(name_2) = other {
return name_1 == name_2;
}
}
ContactEntry::IncomingRequest(user_1) => {
if let ContactEntry::IncomingRequest(user_2) = other {
return user_1.id == user_2.id;
}
}
ContactEntry::OutgoingRequest(user_1) => {
if let ContactEntry::OutgoingRequest(user_2) = other {
return user_1.id == user_2.id;
}
}
ContactEntry::Contact(contact_1) => {
if let ContactEntry::Contact(contact_2) = other {
return contact_1.user.id == contact_2.user.id;
}
}
ContactEntry::ContactProject(contact_1, ix_1) => {
if let ContactEntry::ContactProject(contact_2, ix_2) = other {
return contact_1.user.id == contact_2.user.id && ix_1 == ix_2;
}
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use client::{proto, test::FakeServer, ChannelList, Client};
use gpui::TestAppContext;
use language::LanguageRegistry;
use theme::ThemeRegistry;
#[gpui::test]
async fn test_contact_panel(cx: &mut TestAppContext) {
let (app_state, server) = init(cx).await;
let panel = cx.add_view(0, |cx| ContactsPanel::new(app_state.clone(), cx));
let get_users_request = server.receive::<proto::GetUsers>().await.unwrap();
server
.respond(
get_users_request.receipt(),
proto::UsersResponse {
users: [
"user_zero",
"user_one",
"user_two",
"user_three",
"user_four",
"user_five",
]
.into_iter()
.enumerate()
.map(|(id, name)| proto::User {
id: id as u64,
github_login: name.to_string(),
..Default::default()
})
.collect(),
},
)
.await;
server.send(proto::UpdateContacts {
incoming_requests: vec![proto::IncomingContactRequest {
requester_id: 1,
should_notify: false,
}],
outgoing_requests: vec![2],
contacts: vec![
proto::Contact {
user_id: 3,
online: true,
projects: vec![proto::ProjectMetadata {
id: 101,
worktree_root_names: vec!["dir1".to_string()],
is_shared: true,
guests: vec![2],
}],
},
proto::Contact {
user_id: 4,
online: true,
projects: vec![proto::ProjectMetadata {
id: 102,
worktree_root_names: vec!["dir2".to_string()],
is_shared: true,
guests: vec![2],
}],
},
proto::Contact {
user_id: 5,
online: false,
projects: vec![],
},
],
..Default::default()
});
cx.foreground().run_until_parked();
assert_eq!(
render_to_strings(&panel, cx),
&[
"+",
"v Requests",
" incoming user_one <=== selected",
" outgoing user_two",
"v Online",
" user_four",
" dir2",
" user_three",
" dir1",
"v Offline",
" user_five",
]
);
panel.update(cx, |panel, cx| {
panel
.filter_editor
.update(cx, |editor, cx| editor.set_text("f", cx))
});
cx.foreground().run_until_parked();
assert_eq!(
render_to_strings(&panel, cx),
&[
"+",
"Online",
" user_four <=== selected",
" dir2",
"Offline",
" user_five",
]
);
panel.update(cx, |panel, cx| {
panel.select_next(&Default::default(), cx);
});
assert_eq!(
render_to_strings(&panel, cx),
&[
"+",
"Online",
" user_four",
" dir2 <=== selected",
"Offline",
" user_five",
]
);
panel.update(cx, |panel, cx| {
panel.select_next(&Default::default(), cx);
});
assert_eq!(
render_to_strings(&panel, cx),
&[
"+",
"Online",
" user_four",
" dir2",
"Offline",
" user_five <=== selected",
]
);
}
fn render_to_strings(panel: &ViewHandle<ContactsPanel>, cx: &TestAppContext) -> Vec<String> {
panel.read_with(cx, |panel, _| {
let mut entries = Vec::new();
entries.push("+".to_string());
entries.extend(panel.entries.iter().map(|entry| match entry {
ContactEntry::Header(name) => {
format!("{}", name)
}
ContactEntry::IncomingRequest(user) => {
format!(" incoming {}", user.github_login)
}
ContactEntry::OutgoingRequest(user) => {
format!(" outgoing {}", user.github_login)
}
ContactEntry::Contact(contact) => {
format!(" {}", contact.user.github_login)
}
ContactEntry::ContactProject(contact, project_ix) => {
format!(
" {}",
contact.projects[*project_ix].worktree_root_names.join(", ")
)
}
}));
entries
})
}
async fn init(cx: &mut TestAppContext) -> (Arc<AppState>, FakeServer) {
cx.update(|cx| cx.set_global(Settings::test(cx)));
let themes = ThemeRegistry::new((), cx.font_cache());
let fs = project::FakeFs::new(cx.background().clone());
let languages = Arc::new(LanguageRegistry::test());
let http_client = client::test::FakeHttpClient::with_404_response();
let mut client = Client::new(http_client.clone());
let server = FakeServer::for_client(100, &mut client, &cx).await;
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let channel_list =
cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx));
let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
server
.respond(get_channels.receipt(), Default::default())
.await;
(
Arc::new(AppState {
languages,
themes,
client,
user_store: user_store.clone(),
fs,
channel_list,
build_window_options: || unimplemented!(),
build_workspace: |_, _, _| unimplemented!(),
}),
server,
)
}
}

View File

@ -235,11 +235,13 @@ pub struct CommandPalette {
pub struct ContactsPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub header: ContainedText,
pub user_query_editor: FieldEditor,
pub user_query_editor_height: f32,
pub add_contact_button: IconButton,
pub row: ContainerStyle,
pub header_row: Interactive<ContainedText>,
pub contact_row: Interactive<ContainerStyle>,
pub shared_project_row: Interactive<ProjectRow>,
pub unshared_project_row: Interactive<ProjectRow>,
pub row_height: f32,
pub contact_avatar: ImageStyle,
pub contact_username: ContainedText,
@ -247,10 +249,6 @@ pub struct ContactsPanel {
pub disabled_contact_button: IconButton,
pub tree_branch_width: f32,
pub tree_branch_color: Color,
pub shared_project: ProjectRow,
pub hovered_shared_project: ProjectRow,
pub unshared_project: ProjectRow,
pub hovered_unshared_project: ProjectRow,
}
#[derive(Deserialize, Default)]

View File

@ -21,16 +21,6 @@ export default function contactsPanel(theme: Theme) {
},
};
const sharedProject = {
...project,
background: backgroundColor(theme, 300),
cornerRadius: 6,
name: {
...project.name,
...text(theme, "mono", "secondary", { size: "sm" }),
},
};
const contactButton = {
background: backgroundColor(theme, 100),
color: iconColor(theme, "primary"),
@ -62,14 +52,21 @@ export default function contactsPanel(theme: Theme) {
buttonWidth: 8,
iconWidth: 8,
},
row: {
padding: { left: 8 },
},
rowHeight: 28,
header: {
headerRow: {
...text(theme, "mono", "secondary", { size: "sm" }),
margin: { top: 8 },
active: {
...text(theme, "mono", "primary", { size: "sm" }),
background: backgroundColor(theme, 100, "active"),
}
},
contactRow: {
padding: { left: 8 },
active: {
background: backgroundColor(theme, 100, "active"),
}
},
rowHeight: 28,
treeBranchColor: borderColor(theme, "muted"),
treeBranchWidth: 1,
contactAvatar: {
@ -93,17 +90,35 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 100),
color: iconColor(theme, "muted"),
},
project,
sharedProject,
hoveredSharedProject: {
...sharedProject,
background: backgroundColor(theme, 300, "hovered"),
cornerRadius: 6,
},
unsharedProject: project,
hoveredUnsharedProject: {
sharedProjectRow: {
...project,
background: backgroundColor(theme, 300),
cornerRadius: 6,
name: {
...project.name,
...text(theme, "mono", "secondary", { size: "sm" }),
},
hover: {
background: backgroundColor(theme, 300, "hovered"),
},
active: {
background: backgroundColor(theme, 300, "active"),
}
},
unsharedProjectRow: {
...project,
background: backgroundColor(theme, 300),
cornerRadius: 6,
name: {
...project.name,
...text(theme, "mono", "secondary", { size: "sm" }),
},
hover: {
background: backgroundColor(theme, 300, "hovered"),
},
active: {
background: backgroundColor(theme, 300, "active"),
}
}
}
}