Cargo.lock 🔗
@@ -936,9 +936,11 @@ dependencies = [
"futures",
"fuzzy",
"gpui",
+ "language",
"log",
"picker",
"postage",
+ "project",
"serde",
"settings",
"theme",
Max Brunsfeld created
Allow interacting with the contacts panel using the keyboard
Cargo.lock | 2
assets/keymaps/default.json | 14
assets/themes/cave-dark.json | 145 +--
assets/themes/cave-light.json | 145 +--
assets/themes/dark.json | 145 +--
assets/themes/light.json | 145 +--
assets/themes/solarized-dark.json | 145 +--
assets/themes/solarized-light.json | 145 +--
assets/themes/sulphurpool-dark.json | 145 +--
assets/themes/sulphurpool-light.json | 145 +--
crates/client/src/http.rs | 26
crates/client/src/test.rs | 20
crates/collab/src/rpc.rs | 74 -
crates/collab/src/rpc/store.rs | 79 -
crates/contacts_panel/Cargo.toml | 5
crates/contacts_panel/src/contacts_panel.rs | 862 +++++++++++++++++-----
crates/theme/src/theme.rs | 23
crates/workspace/src/sidebar.rs | 14
crates/workspace/src/workspace.rs | 4
styles/src/styleTree/app.ts | 2
styles/src/styleTree/contactsPanel.ts | 111 ++
21 files changed, 1,405 insertions(+), 991 deletions(-)
@@ -936,9 +936,11 @@ dependencies = [
"futures",
"fuzzy",
"gpui",
+ "language",
"log",
"picker",
"postage",
+ "project",
"serde",
"settings",
"theme",
@@ -324,6 +324,20 @@
"side": "Left",
"item_index": 0
}
+ ],
+ "cmd-9": [
+ "workspace::ToggleSidebarItemFocus",
+ {
+ "side": "Right",
+ "item_index": 0
+ }
+ ],
+ "cmd-shift-(": [
+ "workspace::ToggleSidebarItem",
+ {
+ "side": "Right",
+ "item_index": 0
+ }
]
}
},
@@ -1029,9 +1029,7 @@
"chat_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"channel_name": {
"family": "Zed Sans",
@@ -1248,9 +1246,7 @@
"contacts_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"user_query_editor": {
"background": "#19171c",
@@ -1278,33 +1274,61 @@
"left": 8,
"right": 8,
"top": 4
+ },
+ "margin": {
+ "left": 12,
+ "right": 12
}
},
"user_query_editor_height": 32,
"add_contact_button": {
"margin": {
- "left": 6
+ "left": 6,
+ "right": 12
},
"color": "#e2dfe7",
"button_width": 8,
"icon_width": 8
},
- "row": {
- "padding": {
- "left": 8
- }
- },
"row_height": 28,
- "header": {
+ "section_icon_size": 8,
+ "header_row": {
"family": "Zed Mono",
"color": "#8b8792",
"size": 14,
"margin": {
- "top": 8
+ "top": 14
+ },
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "family": "Zed Mono",
+ "color": "#e2dfe7",
+ "size": 14,
+ "background": "#5852605c"
+ }
+ },
+ "contact_row": {
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "background": "#5852605c"
+ }
+ },
+ "tree_branch": {
+ "color": "#655f6d",
+ "width": 1,
+ "hover": {
+ "color": "#655f6d"
+ },
+ "active": {
+ "color": "#655f6d"
}
},
- "tree_branch_color": "#655f6d",
- "tree_branch_width": 1,
"contact_avatar": {
"corner_radius": 10,
"width": 18
@@ -1313,10 +1337,11 @@
"family": "Zed Mono",
"color": "#e2dfe7",
"size": 14,
- "padding": {
+ "margin": {
"left": 8
}
},
+ "contact_button_spacing": 8,
"contact_button": {
"background": "#26232a",
"color": "#e2dfe7",
@@ -1334,7 +1359,7 @@
"button_width": 16,
"corner_radius": 8
},
- "project": {
+ "shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1343,38 +1368,32 @@
},
"name": {
"family": "Zed Mono",
- "color": "#7e7887",
+ "color": "#8b8792",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- }
- },
- "shared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#8b8792",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
+ "left": 12,
+ "right": 12
},
"background": "#26232a",
- "corner_radius": 6
+ "hover": {
+ "background": "#5852603d"
+ },
+ "active": {
+ "background": "#5852605c"
+ }
},
- "hovered_shared_project": {
+ "unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1386,53 +1405,27 @@
"color": "#8b8792",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- },
- "background": "#5852603d",
- "corner_radius": 6
- },
- "unshared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#7e7887",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
- }
- },
- "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
- }
+ "left": 12,
+ "right": 12
},
- "padding": {
- "left": 8
+ "background": "#26232a",
+ "hover": {
+ "background": "#5852603d"
},
- "corner_radius": 6
+ "active": {
+ "background": "#5852605c"
+ }
}
},
"contact_finder": {
@@ -1029,9 +1029,7 @@
"chat_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"channel_name": {
"family": "Zed Sans",
@@ -1248,9 +1246,7 @@
"contacts_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"user_query_editor": {
"background": "#efecf4",
@@ -1278,33 +1274,61 @@
"left": 8,
"right": 8,
"top": 4
+ },
+ "margin": {
+ "left": 12,
+ "right": 12
}
},
"user_query_editor_height": 32,
"add_contact_button": {
"margin": {
- "left": 6
+ "left": 6,
+ "right": 12
},
"color": "#26232a",
"button_width": 8,
"icon_width": 8
},
- "row": {
- "padding": {
- "left": 8
- }
- },
"row_height": 28,
- "header": {
+ "section_icon_size": 8,
+ "header_row": {
"family": "Zed Mono",
"color": "#585260",
"size": 14,
"margin": {
- "top": 8
+ "top": 14
+ },
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "family": "Zed Mono",
+ "color": "#26232a",
+ "size": 14,
+ "background": "#8b87922e"
+ }
+ },
+ "contact_row": {
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "background": "#8b87922e"
+ }
+ },
+ "tree_branch": {
+ "color": "#7e7887",
+ "width": 1,
+ "hover": {
+ "color": "#7e7887"
+ },
+ "active": {
+ "color": "#7e7887"
}
},
- "tree_branch_color": "#7e7887",
- "tree_branch_width": 1,
"contact_avatar": {
"corner_radius": 10,
"width": 18
@@ -1313,10 +1337,11 @@
"family": "Zed Mono",
"color": "#26232a",
"size": 14,
- "padding": {
+ "margin": {
"left": 8
}
},
+ "contact_button_spacing": 8,
"contact_button": {
"background": "#e2dfe7",
"color": "#26232a",
@@ -1334,7 +1359,7 @@
"button_width": 16,
"corner_radius": 8
},
- "project": {
+ "shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1343,38 +1368,32 @@
},
"name": {
"family": "Zed Mono",
- "color": "#655f6d",
+ "color": "#585260",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- }
- },
- "shared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#585260",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
+ "left": 12,
+ "right": 12
},
"background": "#e2dfe7",
- "corner_radius": 6
+ "hover": {
+ "background": "#8b87921f"
+ },
+ "active": {
+ "background": "#8b87922e"
+ }
},
- "hovered_shared_project": {
+ "unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1386,53 +1405,27 @@
"color": "#585260",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- },
- "background": "#8b87921f",
- "corner_radius": 6
- },
- "unshared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#655f6d",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
- }
- },
- "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
- }
+ "left": 12,
+ "right": 12
},
- "padding": {
- "left": 8
+ "background": "#e2dfe7",
+ "hover": {
+ "background": "#8b87921f"
},
- "corner_radius": 6
+ "active": {
+ "background": "#8b87922e"
+ }
}
},
"contact_finder": {
@@ -1029,9 +1029,7 @@
"chat_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"channel_name": {
"family": "Zed Sans",
@@ -1248,9 +1246,7 @@
"contacts_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"user_query_editor": {
"background": "#000000",
@@ -1278,33 +1274,61 @@
"left": 8,
"right": 8,
"top": 4
+ },
+ "margin": {
+ "left": 12,
+ "right": 12
}
},
"user_query_editor_height": 32,
"add_contact_button": {
"margin": {
- "left": 6
+ "left": 6,
+ "right": 12
},
"color": "#c6c6c6",
"button_width": 8,
"icon_width": 8
},
- "row": {
- "padding": {
- "left": 8
- }
- },
"row_height": 28,
- "header": {
+ "section_icon_size": 8,
+ "header_row": {
"family": "Zed Mono",
"color": "#9c9c9c",
"size": 14,
"margin": {
- "top": 8
+ "top": 14
+ },
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "family": "Zed Mono",
+ "color": "#f1f1f1",
+ "size": 14,
+ "background": "#1c1c1c"
+ }
+ },
+ "contact_row": {
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "background": "#1c1c1c"
+ }
+ },
+ "tree_branch": {
+ "color": "#000000",
+ "width": 1,
+ "hover": {
+ "color": "#000000"
+ },
+ "active": {
+ "color": "#000000"
}
},
- "tree_branch_color": "#404040",
- "tree_branch_width": 1,
"contact_avatar": {
"corner_radius": 10,
"width": 18
@@ -1313,10 +1337,11 @@
"family": "Zed Mono",
"color": "#f1f1f1",
"size": 14,
- "padding": {
+ "margin": {
"left": 8
}
},
+ "contact_button_spacing": 8,
"contact_button": {
"background": "#2b2b2b",
"color": "#c6c6c6",
@@ -1334,7 +1359,7 @@
"button_width": 16,
"corner_radius": 8
},
- "project": {
+ "shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1343,38 +1368,32 @@
},
"name": {
"family": "Zed Mono",
- "color": "#474747",
+ "color": "#9c9c9c",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- }
- },
- "shared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#9c9c9c",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
+ "left": 12,
+ "right": 12
},
"background": "#1c1c1c",
- "corner_radius": 6
+ "hover": {
+ "background": "#232323"
+ },
+ "active": {
+ "background": "#2b2b2b"
+ }
},
- "hovered_shared_project": {
+ "unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1386,53 +1405,27 @@
"color": "#9c9c9c",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- },
- "background": "#232323",
- "corner_radius": 6
- },
- "unshared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#474747",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
- }
- },
- "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
- }
+ "left": 12,
+ "right": 12
},
- "padding": {
- "left": 8
+ "background": "#1c1c1c",
+ "hover": {
+ "background": "#232323"
},
- "corner_radius": 6
+ "active": {
+ "background": "#2b2b2b"
+ }
}
},
"contact_finder": {
@@ -1029,9 +1029,7 @@
"chat_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"channel_name": {
"family": "Zed Sans",
@@ -1248,9 +1246,7 @@
"contacts_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"user_query_editor": {
"background": "#ffffff",
@@ -1278,33 +1274,61 @@
"left": 8,
"right": 8,
"top": 4
+ },
+ "margin": {
+ "left": 12,
+ "right": 12
}
},
"user_query_editor_height": 32,
"add_contact_button": {
"margin": {
- "left": 6
+ "left": 6,
+ "right": 12
},
"color": "#393939",
"button_width": 8,
"icon_width": 8
},
- "row": {
- "padding": {
- "left": 8
- }
- },
"row_height": 28,
- "header": {
+ "section_icon_size": 8,
+ "header_row": {
"family": "Zed Mono",
"color": "#474747",
"size": 14,
"margin": {
- "top": 8
+ "top": 14
+ },
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "family": "Zed Mono",
+ "color": "#2b2b2b",
+ "size": 14,
+ "background": "#d5d5d5"
+ }
+ },
+ "contact_row": {
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "background": "#d5d5d5"
+ }
+ },
+ "tree_branch": {
+ "color": "#b8b8b8",
+ "width": 1,
+ "hover": {
+ "color": "#b8b8b8"
+ },
+ "active": {
+ "color": "#b8b8b8"
}
},
- "tree_branch_color": "#e3e3e3",
- "tree_branch_width": 1,
"contact_avatar": {
"corner_radius": 10,
"width": 18
@@ -1313,10 +1337,11 @@
"family": "Zed Mono",
"color": "#2b2b2b",
"size": 14,
- "padding": {
+ "margin": {
"left": 8
}
},
+ "contact_button_spacing": 8,
"contact_button": {
"background": "#eaeaea",
"color": "#393939",
@@ -1334,7 +1359,7 @@
"button_width": 16,
"corner_radius": 8
},
- "project": {
+ "shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1343,38 +1368,32 @@
},
"name": {
"family": "Zed Mono",
- "color": "#808080",
+ "color": "#474747",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- }
- },
- "shared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#474747",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
+ "left": 12,
+ "right": 12
},
"background": "#f8f8f8",
- "corner_radius": 6
+ "hover": {
+ "background": "#eaeaea"
+ },
+ "active": {
+ "background": "#e3e3e3"
+ }
},
- "hovered_shared_project": {
+ "unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1386,53 +1405,27 @@
"color": "#474747",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- },
- "background": "#eaeaea",
- "corner_radius": 6
- },
- "unshared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#808080",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
- }
- },
- "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
- }
+ "left": 12,
+ "right": 12
},
- "padding": {
- "left": 8
+ "background": "#f8f8f8",
+ "hover": {
+ "background": "#eaeaea"
},
- "corner_radius": 6
+ "active": {
+ "background": "#e3e3e3"
+ }
}
},
"contact_finder": {
@@ -1029,9 +1029,7 @@
"chat_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"channel_name": {
"family": "Zed Sans",
@@ -1248,9 +1246,7 @@
"contacts_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"user_query_editor": {
"background": "#002b36",
@@ -1278,33 +1274,61 @@
"left": 8,
"right": 8,
"top": 4
+ },
+ "margin": {
+ "left": 12,
+ "right": 12
}
},
"user_query_editor_height": 32,
"add_contact_button": {
"margin": {
- "left": 6
+ "left": 6,
+ "right": 12
},
"color": "#eee8d5",
"button_width": 8,
"icon_width": 8
},
- "row": {
- "padding": {
- "left": 8
- }
- },
"row_height": 28,
- "header": {
+ "section_icon_size": 8,
+ "header_row": {
"family": "Zed Mono",
"color": "#93a1a1",
"size": 14,
"margin": {
- "top": 8
+ "top": 14
+ },
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "family": "Zed Mono",
+ "color": "#eee8d5",
+ "size": 14,
+ "background": "#586e755c"
+ }
+ },
+ "contact_row": {
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "background": "#586e755c"
+ }
+ },
+ "tree_branch": {
+ "color": "#657b83",
+ "width": 1,
+ "hover": {
+ "color": "#657b83"
+ },
+ "active": {
+ "color": "#657b83"
}
},
- "tree_branch_color": "#657b83",
- "tree_branch_width": 1,
"contact_avatar": {
"corner_radius": 10,
"width": 18
@@ -1313,10 +1337,11 @@
"family": "Zed Mono",
"color": "#eee8d5",
"size": 14,
- "padding": {
+ "margin": {
"left": 8
}
},
+ "contact_button_spacing": 8,
"contact_button": {
"background": "#073642",
"color": "#eee8d5",
@@ -1334,7 +1359,7 @@
"button_width": 16,
"corner_radius": 8
},
- "project": {
+ "shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1343,38 +1368,32 @@
},
"name": {
"family": "Zed Mono",
- "color": "#839496",
+ "color": "#93a1a1",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- }
- },
- "shared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#93a1a1",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
+ "left": 12,
+ "right": 12
},
"background": "#073642",
- "corner_radius": 6
+ "hover": {
+ "background": "#586e753d"
+ },
+ "active": {
+ "background": "#586e755c"
+ }
},
- "hovered_shared_project": {
+ "unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1386,53 +1405,27 @@
"color": "#93a1a1",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- },
- "background": "#586e753d",
- "corner_radius": 6
- },
- "unshared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#839496",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
- }
- },
- "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
- }
+ "left": 12,
+ "right": 12
},
- "padding": {
- "left": 8
+ "background": "#073642",
+ "hover": {
+ "background": "#586e753d"
},
- "corner_radius": 6
+ "active": {
+ "background": "#586e755c"
+ }
}
},
"contact_finder": {
@@ -1029,9 +1029,7 @@
"chat_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"channel_name": {
"family": "Zed Sans",
@@ -1248,9 +1246,7 @@
"contacts_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"user_query_editor": {
"background": "#fdf6e3",
@@ -1278,33 +1274,61 @@
"left": 8,
"right": 8,
"top": 4
+ },
+ "margin": {
+ "left": 12,
+ "right": 12
}
},
"user_query_editor_height": 32,
"add_contact_button": {
"margin": {
- "left": 6
+ "left": 6,
+ "right": 12
},
"color": "#073642",
"button_width": 8,
"icon_width": 8
},
- "row": {
- "padding": {
- "left": 8
- }
- },
"row_height": 28,
- "header": {
+ "section_icon_size": 8,
+ "header_row": {
"family": "Zed Mono",
"color": "#586e75",
"size": 14,
"margin": {
- "top": 8
+ "top": 14
+ },
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "family": "Zed Mono",
+ "color": "#073642",
+ "size": 14,
+ "background": "#93a1a12e"
+ }
+ },
+ "contact_row": {
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "background": "#93a1a12e"
+ }
+ },
+ "tree_branch": {
+ "color": "#839496",
+ "width": 1,
+ "hover": {
+ "color": "#839496"
+ },
+ "active": {
+ "color": "#839496"
}
},
- "tree_branch_color": "#839496",
- "tree_branch_width": 1,
"contact_avatar": {
"corner_radius": 10,
"width": 18
@@ -1313,10 +1337,11 @@
"family": "Zed Mono",
"color": "#073642",
"size": 14,
- "padding": {
+ "margin": {
"left": 8
}
},
+ "contact_button_spacing": 8,
"contact_button": {
"background": "#eee8d5",
"color": "#073642",
@@ -1334,7 +1359,7 @@
"button_width": 16,
"corner_radius": 8
},
- "project": {
+ "shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1343,38 +1368,32 @@
},
"name": {
"family": "Zed Mono",
- "color": "#657b83",
+ "color": "#586e75",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- }
- },
- "shared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#586e75",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
+ "left": 12,
+ "right": 12
},
"background": "#eee8d5",
- "corner_radius": 6
+ "hover": {
+ "background": "#93a1a11f"
+ },
+ "active": {
+ "background": "#93a1a12e"
+ }
},
- "hovered_shared_project": {
+ "unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1386,53 +1405,27 @@
"color": "#586e75",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- },
- "background": "#93a1a11f",
- "corner_radius": 6
- },
- "unshared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#657b83",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
- }
- },
- "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
- }
+ "left": 12,
+ "right": 12
},
- "padding": {
- "left": 8
+ "background": "#eee8d5",
+ "hover": {
+ "background": "#93a1a11f"
},
- "corner_radius": 6
+ "active": {
+ "background": "#93a1a12e"
+ }
}
},
"contact_finder": {
@@ -1029,9 +1029,7 @@
"chat_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"channel_name": {
"family": "Zed Sans",
@@ -1248,9 +1246,7 @@
"contacts_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"user_query_editor": {
"background": "#202746",
@@ -1278,33 +1274,61 @@
"left": 8,
"right": 8,
"top": 4
+ },
+ "margin": {
+ "left": 12,
+ "right": 12
}
},
"user_query_editor_height": 32,
"add_contact_button": {
"margin": {
- "left": 6
+ "left": 6,
+ "right": 12
},
"color": "#dfe2f1",
"button_width": 8,
"icon_width": 8
},
- "row": {
- "padding": {
- "left": 8
- }
- },
"row_height": 28,
- "header": {
+ "section_icon_size": 8,
+ "header_row": {
"family": "Zed Mono",
"color": "#979db4",
"size": 14,
"margin": {
- "top": 8
+ "top": 14
+ },
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "family": "Zed Mono",
+ "color": "#dfe2f1",
+ "size": 14,
+ "background": "#5e66875c"
+ }
+ },
+ "contact_row": {
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "background": "#5e66875c"
+ }
+ },
+ "tree_branch": {
+ "color": "#6b7394",
+ "width": 1,
+ "hover": {
+ "color": "#6b7394"
+ },
+ "active": {
+ "color": "#6b7394"
}
},
- "tree_branch_color": "#6b7394",
- "tree_branch_width": 1,
"contact_avatar": {
"corner_radius": 10,
"width": 18
@@ -1313,10 +1337,11 @@
"family": "Zed Mono",
"color": "#dfe2f1",
"size": 14,
- "padding": {
+ "margin": {
"left": 8
}
},
+ "contact_button_spacing": 8,
"contact_button": {
"background": "#293256",
"color": "#dfe2f1",
@@ -1334,7 +1359,7 @@
"button_width": 16,
"corner_radius": 8
},
- "project": {
+ "shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1343,38 +1368,32 @@
},
"name": {
"family": "Zed Mono",
- "color": "#898ea4",
+ "color": "#979db4",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- }
- },
- "shared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#979db4",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
+ "left": 12,
+ "right": 12
},
"background": "#293256",
- "corner_radius": 6
+ "hover": {
+ "background": "#5e66873d"
+ },
+ "active": {
+ "background": "#5e66875c"
+ }
},
- "hovered_shared_project": {
+ "unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1386,53 +1405,27 @@
"color": "#979db4",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- },
- "background": "#5e66873d",
- "corner_radius": 6
- },
- "unshared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#898ea4",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
- }
- },
- "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
- }
+ "left": 12,
+ "right": 12
},
- "padding": {
- "left": 8
+ "background": "#293256",
+ "hover": {
+ "background": "#5e66873d"
},
- "corner_radius": 6
+ "active": {
+ "background": "#5e66875c"
+ }
}
},
"contact_finder": {
@@ -1029,9 +1029,7 @@
"chat_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"channel_name": {
"family": "Zed Sans",
@@ -1248,9 +1246,7 @@
"contacts_panel": {
"padding": {
"top": 12,
- "left": 12,
- "bottom": 12,
- "right": 12
+ "bottom": 12
},
"user_query_editor": {
"background": "#f5f7ff",
@@ -1278,33 +1274,61 @@
"left": 8,
"right": 8,
"top": 4
+ },
+ "margin": {
+ "left": 12,
+ "right": 12
}
},
"user_query_editor_height": 32,
"add_contact_button": {
"margin": {
- "left": 6
+ "left": 6,
+ "right": 12
},
"color": "#293256",
"button_width": 8,
"icon_width": 8
},
- "row": {
- "padding": {
- "left": 8
- }
- },
"row_height": 28,
- "header": {
+ "section_icon_size": 8,
+ "header_row": {
"family": "Zed Mono",
"color": "#5e6687",
"size": 14,
"margin": {
- "top": 8
+ "top": 14
+ },
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "family": "Zed Mono",
+ "color": "#293256",
+ "size": 14,
+ "background": "#979db42e"
+ }
+ },
+ "contact_row": {
+ "padding": {
+ "left": 12,
+ "right": 12
+ },
+ "active": {
+ "background": "#979db42e"
+ }
+ },
+ "tree_branch": {
+ "color": "#898ea4",
+ "width": 1,
+ "hover": {
+ "color": "#898ea4"
+ },
+ "active": {
+ "color": "#898ea4"
}
},
- "tree_branch_color": "#898ea4",
- "tree_branch_width": 1,
"contact_avatar": {
"corner_radius": 10,
"width": 18
@@ -1313,10 +1337,11 @@
"family": "Zed Mono",
"color": "#293256",
"size": 14,
- "padding": {
+ "margin": {
"left": 8
}
},
+ "contact_button_spacing": 8,
"contact_button": {
"background": "#dfe2f1",
"color": "#293256",
@@ -1334,7 +1359,7 @@
"button_width": 16,
"corner_radius": 8
},
- "project": {
+ "shared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1343,38 +1368,32 @@
},
"name": {
"family": "Zed Mono",
- "color": "#6b7394",
+ "color": "#5e6687",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- }
- },
- "shared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#5e6687",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
+ "left": 12,
+ "right": 12
},
"background": "#dfe2f1",
- "corner_radius": 6
+ "hover": {
+ "background": "#979db41f"
+ },
+ "active": {
+ "background": "#979db42e"
+ }
},
- "hovered_shared_project": {
+ "unshared_project_row": {
"guest_avatar_spacing": 4,
"height": 24,
"guest_avatar": {
@@ -1386,53 +1405,27 @@
"color": "#5e6687",
"size": 14,
"margin": {
+ "left": 8,
"right": 6
}
},
- "padding": {
- "left": 8
- },
- "background": "#979db41f",
- "corner_radius": 6
- },
- "unshared_project": {
- "guest_avatar_spacing": 4,
- "height": 24,
- "guest_avatar": {
- "corner_radius": 8,
- "width": 14
- },
- "name": {
- "family": "Zed Mono",
- "color": "#6b7394",
- "size": 14,
+ "guests": {
"margin": {
- "right": 6
+ "left": 8,
+ "right": 8
}
},
"padding": {
- "left": 8
- }
- },
- "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
- }
+ "left": 12,
+ "right": 12
},
- "padding": {
- "left": 8
+ "background": "#dfe2f1",
+ "hover": {
+ "background": "#979db41f"
},
- "corner_radius": 6
+ "active": {
+ "background": "#979db42e"
+ }
}
},
"contact_finder": {
@@ -8,6 +8,7 @@ pub use isahc::{
http::{Method, Uri},
Error,
};
+use smol::future::FutureExt;
use std::sync::Arc;
pub use url::Url;
@@ -23,18 +24,19 @@ pub trait HttpClient: Send + Sync {
body: AsyncBody,
follow_redirects: bool,
) -> BoxFuture<'a, Result<Response, Error>> {
- self.send(
- isahc::Request::builder()
- .redirect_policy(if follow_redirects {
- RedirectPolicy::Follow
- } else {
- RedirectPolicy::None
- })
- .method(Method::GET)
- .uri(uri)
- .body(body)
- .unwrap(),
- )
+ let request = isahc::Request::builder()
+ .redirect_policy(if follow_redirects {
+ RedirectPolicy::Follow
+ } else {
+ RedirectPolicy::None
+ })
+ .method(Method::GET)
+ .uri(uri)
+ .body(body);
+ match request {
+ Ok(request) => self.send(request),
+ Err(error) => async move { Err(error.into()) }.boxed(),
+ }
}
}
@@ -41,12 +41,14 @@ impl FakeServer {
Arc::get_mut(client)
.unwrap()
.override_authenticate({
- let state = server.state.clone();
+ let state = Arc::downgrade(&server.state);
move |cx| {
- let mut state = state.lock();
- state.auth_count += 1;
- let access_token = state.access_token.to_string();
+ let state = state.clone();
cx.spawn(move |_| async move {
+ let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
+ let mut state = state.lock();
+ state.auth_count += 1;
+ let access_token = state.access_token.to_string();
Ok(Credentials {
user_id: client_user_id,
access_token,
@@ -55,21 +57,23 @@ impl FakeServer {
}
})
.override_establish_connection({
- let peer = server.peer.clone();
- let state = server.state.clone();
+ let peer = Arc::downgrade(&server.peer).clone();
+ let state = Arc::downgrade(&server.state);
move |credentials, cx| {
let peer = peer.clone();
let state = state.clone();
let credentials = credentials.clone();
cx.spawn(move |cx| async move {
- assert_eq!(credentials.user_id, client_user_id);
-
+ let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
+ let peer = peer.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
if state.lock().forbid_connections {
Err(EstablishConnectionError::Other(anyhow!(
"server is forbidding connections"
)))?
}
+ assert_eq!(credentials.user_id, client_user_id);
+
if credentials.access_token != state.lock().access_token.to_string() {
Err(EstablishConnectionError::Unauthorized)?
}
@@ -5016,13 +5016,11 @@ mod tests {
cx_c: &mut TestAppContext,
) {
cx_a.foreground().forbid_parking();
- let lang_registry = Arc::new(LanguageRegistry::test());
- let fs = FakeFs::new(cx_a.background());
// Connect to a server as 3 clients.
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
- let client_a = server.create_client(cx_a, "user_a").await;
- let client_b = server.create_client(cx_b, "user_b").await;
+ let mut client_a = server.create_client(cx_a, "user_a").await;
+ let mut client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
server
.make_contacts(vec![
@@ -5046,27 +5044,10 @@ mod tests {
});
}
- // Share a worktree as client A.
+ // Share a project as client A.
+ let fs = FakeFs::new(cx_a.background());
fs.create_dir(Path::new("/a")).await.unwrap();
-
- let project_a = cx_a.update(|cx| {
- Project::local(
- client_a.clone(),
- client_a.user_store.clone(),
- lang_registry.clone(),
- fs.clone(),
- cx,
- )
- });
- let (worktree_a, _) = project_a
- .update(cx_a, |p, cx| {
- p.find_or_create_local_worktree("/a", true, cx)
- })
- .await
- .unwrap();
- worktree_a
- .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
- .await;
+ let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
deterministic.run_until_parked();
for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
@@ -5104,16 +5085,7 @@ mod tests {
});
}
- let _project_b = Project::remote(
- project_id,
- client_b.clone(),
- client_b.user_store.clone(),
- lang_registry.clone(),
- fs.clone(),
- &mut cx_b.to_async(),
- )
- .await
- .unwrap();
+ let _project_b = client_b.build_remote_project(project_id, cx_b).await;
deterministic.run_until_parked();
for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
@@ -5129,12 +5101,32 @@ mod tests {
});
}
+ // Add a local project as client B
+ let fs = FakeFs::new(cx_b.background());
+ fs.create_dir(Path::new("/b")).await.unwrap();
+ let (_project_b, _) = client_b.build_local_project(fs, "/b", cx_a).await;
+
+ deterministic.run_until_parked();
+ for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
+ client.user_store.read_with(*cx, |store, _| {
+ assert_eq!(
+ contacts(store),
+ [
+ ("user_a", true, vec![("a", true, vec!["user_b"])]),
+ ("user_b", true, vec![("b", false, vec![])]),
+ ("user_c", true, vec![])
+ ]
+ )
+ });
+ }
+
project_a
.condition(&cx_a, |project, _| {
project.collaborators().contains_key(&client_b.peer_id)
})
.await;
+ client_a.project.take();
cx_a.update(move |_| drop(project_a));
deterministic.run_until_parked();
for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
@@ -5143,7 +5135,7 @@ mod tests {
contacts(store),
[
("user_a", true, vec![]),
- ("user_b", true, vec![]),
+ ("user_b", true, vec![("b", false, vec![])]),
("user_c", true, vec![])
]
)
@@ -5159,7 +5151,7 @@ mod tests {
contacts(store),
[
("user_a", true, vec![]),
- ("user_b", true, vec![]),
+ ("user_b", true, vec![("b", false, vec![])]),
("user_c", false, vec![])
]
)
@@ -5182,7 +5174,7 @@ mod tests {
contacts(store),
[
("user_a", true, vec![]),
- ("user_b", true, vec![]),
+ ("user_b", true, vec![("b", false, vec![])]),
("user_c", true, vec![])
]
)
@@ -5194,7 +5186,7 @@ mod tests {
.contacts()
.iter()
.map(|contact| {
- let worktrees = contact
+ let projects = contact
.projects
.iter()
.map(|p| {
@@ -5205,11 +5197,7 @@ mod tests {
)
})
.collect();
- (
- contact.user.github_login.as_str(),
- contact.online,
- worktrees,
- )
+ (contact.user.github_login.as_str(), contact.online, projects)
})
.collect()
}
@@ -272,73 +272,30 @@ impl Store {
let mut metadata = Vec::new();
for project_id in project_ids {
if let Some(project) = self.projects.get(&project_id) {
- metadata.push(proto::ProjectMetadata {
- id: project_id,
- is_shared: project.share.is_some(),
- worktree_root_names: project
- .worktrees
- .values()
- .map(|worktree| worktree.root_name.clone())
- .collect(),
- guests: project
- .share
- .iter()
- .flat_map(|share| {
- share.guests.values().map(|(_, user_id)| user_id.to_proto())
- })
- .collect(),
- });
+ if project.host_user_id == user_id {
+ metadata.push(proto::ProjectMetadata {
+ id: project_id,
+ is_shared: project.share.is_some(),
+ worktree_root_names: project
+ .worktrees
+ .values()
+ .map(|worktree| worktree.root_name.clone())
+ .collect(),
+ guests: project
+ .share
+ .iter()
+ .flat_map(|share| {
+ share.guests.values().map(|(_, user_id)| user_id.to_proto())
+ })
+ .collect(),
+ });
+ }
}
}
metadata
}
- // pub fn contacts_for_user(&self, user_id: UserId) -> Vec<proto::Contact> {
- // let mut contacts = HashMap::default();
- // for project_id in self
- // .visible_projects_by_user_id
- // .get(&user_id)
- // .unwrap_or(&HashSet::default())
- // {
- // let project = &self.projects[project_id];
-
- // let mut guests = HashSet::default();
- // if let Ok(share) = project.share() {
- // for guest_connection_id in share.guests.keys() {
- // if let Ok(user_id) = self.user_id_for_connection(*guest_connection_id) {
- // guests.insert(user_id.to_proto());
- // }
- // }
- // }
-
- // if let Ok(host_user_id) = self.user_id_for_connection(project.host_connection_id) {
- // let mut worktree_root_names = project
- // .worktrees
- // .values()
- // .filter(|worktree| worktree.visible)
- // .map(|worktree| worktree.root_name.clone())
- // .collect::<Vec<_>>();
- // worktree_root_names.sort_unstable();
- // contacts
- // .entry(host_user_id)
- // .or_insert_with(|| proto::Contact {
- // user_id: host_user_id.to_proto(),
- // projects: Vec::new(),
- // })
- // .projects
- // .push(proto::ProjectMetadata {
- // id: *project_id,
- // worktree_root_names,
- // is_shared: project.share.is_some(),
- // guests: guests.into_iter().collect(),
- // });
- // }
- // }
-
- // contacts.into_values().collect()
- // }
-
pub fn register_project(
&mut self,
host_connection_id: ConnectionId,
@@ -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"] }
@@ -8,7 +8,7 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
- impl_actions,
+ impl_actions, impl_internal_actions,
platform::CursorStyle,
AppContext, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext,
RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
@@ -17,27 +17,47 @@ use serde::Deserialize;
use settings::Settings;
use std::sync::Arc;
use theme::IconButton;
-use workspace::{sidebar::SidebarItem, AppState, JoinProject, Workspace};
+use workspace::{
+ menu::{Confirm, SelectNext, SelectPrev},
+ sidebar::SidebarItem,
+ AppState, JoinProject, Workspace,
+};
impl_actions!(
contacts_panel,
[RequestContact, RemoveContact, RespondToContactRequest]
);
-#[derive(Debug)]
+impl_internal_actions!(contacts_panel, [ToggleExpanded]);
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
+enum Section {
+ Requests,
+ Online,
+ Offline,
+}
+
+#[derive(Clone, Debug)]
enum ContactEntry {
- Header(&'static str),
+ Header(Section),
IncomingRequest(Arc<User>),
OutgoingRequest(Arc<User>),
Contact(Arc<Contact>),
+ ContactProject(Arc<Contact>, usize),
}
+#[derive(Clone)]
+struct ToggleExpanded(Section);
+
pub struct ContactsPanel {
entries: Vec<ContactEntry>,
match_candidates: Vec<StringMatchCandidate>,
list_state: ListState,
user_store: ModelHandle<UserStore>,
filter_editor: ViewHandle<Editor>,
+ collapsed_sections: Vec<Section>,
+ selection: Option<usize>,
+ app_state: Arc<AppState>,
_maintain_contacts: Subscription,
}
@@ -60,6 +80,10 @@ 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);
+ cx.add_action(ContactsPanel::confirm);
+ cx.add_action(ContactsPanel::toggle_expanded);
}
impl ContactsPanel {
@@ -68,7 +92,7 @@ impl ContactsPanel {
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
- let user_query_editor = cx.add_view(|cx| {
+ let filter_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line(
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
cx,
@@ -77,9 +101,19 @@ impl ContactsPanel {
editor
});
- cx.subscribe(&user_query_editor, |this, _, event, cx| {
+ cx.subscribe(&filter_editor, |this, _, event, cx| {
if let editor::Event::BufferEdited = event {
- this.update_entries(cx)
+ let query = this.filter_editor.read(cx).text(cx);
+ if !query.is_empty() {
+ this.selection.take();
+ }
+ this.update_entries(cx);
+ if !query.is_empty() {
+ this.selection = this
+ .entries
+ .iter()
+ .position(|entry| !matches!(entry, ContactEntry::Header(_)));
+ }
}
})
.detach();
@@ -116,24 +150,19 @@ 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())
- .contained()
- .aligned()
- .left()
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(theme.header.container)
- .boxed()
+ ContactEntry::Header(section) => {
+ let is_collapsed = this.collapsed_sections.contains(§ion);
+ Self::render_header(*section, theme, is_selected, is_collapsed, cx)
}
ContactEntry::IncomingRequest(user) => Self::render_contact_request(
user.clone(),
this.user_store.clone(),
theme,
true,
+ is_selected,
cx,
),
ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
@@ -141,200 +170,262 @@ 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,
+ collapsed_sections: Default::default(),
entries: Default::default(),
match_candidates: Default::default(),
- filter_editor: user_query_editor,
+ filter_editor,
_maintain_contacts: cx
.observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)),
user_store: app_state.user_store.clone(),
+ app_state,
};
this.update_entries(cx);
this
}
+ fn render_header(
+ section: Section,
+ theme: &theme::ContactsPanel,
+ is_selected: bool,
+ is_collapsed: bool,
+ cx: &mut LayoutContext,
+ ) -> ElementBox {
+ enum Header {}
+
+ let header_style = theme.header_row.style_for(&Default::default(), is_selected);
+ let text = match section {
+ Section::Requests => "Requests",
+ Section::Online => "Online",
+ Section::Offline => "Offline",
+ };
+ let icon_size = theme.section_icon_size;
+ MouseEventHandler::new::<Header, _, _>(section as usize, cx, |_, _| {
+ Flex::row()
+ .with_child(
+ Svg::new(if is_collapsed {
+ "icons/disclosure-closed.svg"
+ } else {
+ "icons/disclosure-open.svg"
+ })
+ .with_color(header_style.text.color)
+ .constrained()
+ .with_max_width(icon_size)
+ .with_max_height(icon_size)
+ .aligned()
+ .constrained()
+ .with_width(icon_size)
+ .boxed(),
+ )
+ .with_child(
+ Label::new(text.to_string(), header_style.text.clone())
+ .aligned()
+ .left()
+ .contained()
+ .with_margin_left(theme.contact_username.container.margin.left)
+ .flex(1., true)
+ .boxed(),
+ )
+ .constrained()
+ .with_height(theme.row_height)
+ .contained()
+ .with_style(header_style.container)
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(move |_, cx| cx.dispatch_action(ToggleExpanded(section)))
+ .boxed()
+ }
+
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()
+ .flex(1., true)
+ .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 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 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 tree_branch = theme.tree_branch.clone();
+ 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) + (theme.row_height - line_height) / 2.;
- Flex::column()
- .with_child(
- Flex::row()
- .with_children(contact.user.avatar.clone().map(|avatar| {
+ MouseEventHandler::new::<JoinProject, _, _>(project_id as usize, cx, |mouse_state, _| {
+ let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
+ let row = 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 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_child(
+ Label::new(
+ project.worktree_root_names.join(", "),
+ row.name.text.clone(),
+ )
+ .aligned()
+ .left()
+ .contained()
+ .with_style(row.name.container)
+ .flex(1., false)
+ .boxed(),
+ )
+ .with_children(project.guests.iter().filter_map(|participant| {
+ participant.avatar.clone().map(|avatar| {
Image::new(avatar)
- .with_style(theme.contact_avatar)
+ .with_style(row.guest_avatar)
.aligned()
.left()
+ .contained()
+ .with_margin_right(row.guest_avatar_spacing)
.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(),
- )
- .with_children(
- contact
- .non_empty_projects()
- .enumerate()
- .map(|(ix, project)| {
- let project_id = project.id;
- 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)
- .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
- })
- .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(theme.unshared_project.height)
- .boxed()
- }),
- )
- .contained()
- .with_style(theme.row.clone())
- .boxed()
+ })
+ }))
+ .constrained()
+ .with_height(theme.row_height)
+ .contained()
+ .with_style(row.container)
+ .boxed()
+ })
+ .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(),
+ });
+ }
+ })
+ .boxed()
}
fn render_contact_request(
@@ -342,6 +433,7 @@ impl ContactsPanel {
user_store: ModelHandle<UserStore>,
theme: &theme::ContactsPanel,
is_incoming: bool,
+ is_selected: bool,
cx: &mut LayoutContext,
) -> ElementBox {
enum Decline {}
@@ -365,11 +457,13 @@ impl ContactsPanel {
.with_style(theme.contact_username.container)
.aligned()
.left()
+ .flex(1., true)
.boxed(),
);
let user_id = user.id;
let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
+ let button_spacing = theme.contact_button_spacing;
if is_incoming {
row.add_children([
@@ -381,7 +475,7 @@ impl ContactsPanel {
};
render_icon_button(button_style, "icons/decline.svg")
.aligned()
- .flex_float()
+ // .flex_float()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
@@ -391,7 +485,9 @@ impl ContactsPanel {
accept: false,
})
})
- .flex_float()
+ // .flex_float()
+ .contained()
+ .with_margin_right(button_spacing)
.boxed(),
MouseEventHandler::new::<Accept, _, _>(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
@@ -437,7 +533,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()
}
@@ -446,6 +546,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();
@@ -471,13 +572,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();
@@ -502,18 +601,18 @@ 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() {
- self.entries.push(ContactEntry::Header("Requests"));
- self.entries.append(&mut request_entries);
+ self.entries.push(ContactEntry::Header(Section::Requests));
+ if !self.collapsed_sections.contains(&Section::Requests) {
+ self.entries.append(&mut request_entries);
+ }
}
let contacts = user_store.contacts();
@@ -543,22 +642,39 @@ 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, section) in [
+ (online_contacts, Section::Online),
+ (offline_contacts, Section::Offline),
+ ] {
+ if !matches.is_empty() {
+ self.entries.push(ContactEntry::Header(section));
+ if !self.collapsed_sections.contains(§ion) {
+ 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(prev_selected_entry) = prev_selected_entry {
+ self.selection.take();
+ for (ix, entry) in self.entries.iter().enumerate() {
+ if *entry == prev_selected_entry {
+ self.selection = Some(ix);
+ break;
+ }
}
}
@@ -594,6 +710,60 @@ 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 confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+ if let Some(selection) = self.selection {
+ if let Some(entry) = self.entries.get(selection) {
+ match entry {
+ ContactEntry::Header(section) => {
+ let section = *section;
+ self.toggle_expanded(&ToggleExpanded(section), cx);
+ }
+ ContactEntry::ContactProject(contact, project_ix) => {
+ cx.dispatch_global_action(JoinProject {
+ project_id: contact.projects[*project_ix].id,
+ app_state: self.app_state.clone(),
+ })
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+
+ fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
+ let section = action.0;
+ if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
+ self.collapsed_sections.remove(ix);
+ } else {
+ self.collapsed_sections.push(section);
+ }
+ self.update_entries(cx);
+ }
}
impl SidebarItem for ContactsPanel {
@@ -604,6 +774,10 @@ impl SidebarItem for ContactsPanel {
.incoming_contact_requests()
.is_empty()
}
+
+ fn contains_focused_view(&self, cx: &AppContext) -> bool {
+ self.filter_editor.is_focused(cx)
+ }
}
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
@@ -671,4 +845,270 @@ 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(section_1) => {
+ if let ContactEntry::Header(section_2) = other {
+ return section_1 == section_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;
+ use workspace::WorkspaceParams;
+
+ #[gpui::test]
+ async fn test_contact_panel(cx: &mut TestAppContext) {
+ let (app_state, server) = init(cx).await;
+ let workspace_params = cx.update(WorkspaceParams::test);
+ let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
+ let panel = cx.add_view(0, |cx| {
+ ContactsPanel::new(app_state.clone(), workspace.downgrade(), 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,
+ should_notify: false,
+ 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,
+ should_notify: false,
+ 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,
+ should_notify: false,
+ projects: vec![],
+ },
+ ],
+ ..Default::default()
+ });
+
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ render_to_strings(&panel, cx),
+ &[
+ "+",
+ "v Requests",
+ " incoming user_one",
+ " 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),
+ &[
+ "+",
+ "v Online",
+ " user_four <=== selected",
+ " dir2",
+ "v Offline",
+ " user_five",
+ ]
+ );
+
+ panel.update(cx, |panel, cx| {
+ panel.select_next(&Default::default(), cx);
+ });
+ assert_eq!(
+ render_to_strings(&panel, cx),
+ &[
+ "+",
+ "v Online",
+ " user_four",
+ " dir2 <=== selected",
+ "v Offline",
+ " user_five",
+ ]
+ );
+
+ panel.update(cx, |panel, cx| {
+ panel.select_next(&Default::default(), cx);
+ });
+ assert_eq!(
+ render_to_strings(&panel, cx),
+ &[
+ "+",
+ "v Online",
+ " user_four",
+ " dir2",
+ "v Offline <=== selected",
+ " user_five",
+ ]
+ );
+ }
+
+ 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().enumerate().map(|(ix, entry)| {
+ let mut string = match entry {
+ ContactEntry::Header(name) => {
+ let icon = if panel.collapsed_sections.contains(name) {
+ ">"
+ } else {
+ "v"
+ };
+ format!("{} {:?}", icon, 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(", ")
+ )
+ }
+ };
+
+ if panel.selection == Some(ix) {
+ string.push_str(" <=== selected");
+ }
+
+ string
+ }));
+ 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,
+ )
+ }
}
@@ -246,22 +246,27 @@ 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,
pub contact_button: Interactive<IconButton>,
+ pub contact_button_spacing: f32,
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,
+ pub tree_branch: Interactive<TreeBranch>,
+ pub section_icon_size: f32,
+}
+
+#[derive(Deserialize, Default, Clone, Copy)]
+pub struct TreeBranch {
+ pub width: f32,
+ pub color: Color,
}
#[derive(Deserialize, Default)]
@@ -286,8 +291,8 @@ pub struct IconButton {
pub struct ProjectRow {
#[serde(flatten)]
pub container: ContainerStyle,
- pub height: f32,
pub name: ContainedText,
+ pub guests: ContainerStyle,
pub guest_avatar: ImageStyle,
pub guest_avatar_spacing: f32,
}
@@ -10,10 +10,14 @@ use theme::Theme;
pub trait SidebarItem: View {
fn should_show_badge(&self, cx: &AppContext) -> bool;
+ fn contains_focused_view(&self, _: &AppContext) -> bool {
+ false
+ }
}
pub trait SidebarItemHandle {
fn should_show_badge(&self, cx: &AppContext) -> bool;
+ fn is_focused(&self, cx: &AppContext) -> bool;
fn to_any(&self) -> AnyViewHandle;
}
@@ -25,6 +29,10 @@ where
self.read(cx).should_show_badge(cx)
}
+ fn is_focused(&self, cx: &AppContext) -> bool {
+ ViewHandle::is_focused(&self, cx) || self.read(cx).contains_focused_view(cx)
+ }
+
fn to_any(&self) -> AnyViewHandle {
self.into()
}
@@ -114,10 +122,10 @@ impl Sidebar {
cx.notify();
}
- pub fn active_item(&self) -> Option<&dyn SidebarItemHandle> {
+ pub fn active_item(&self) -> Option<&Rc<dyn SidebarItemHandle>> {
self.active_item_ix
.and_then(|ix| self.items.get(ix))
- .map(|item| item.view.as_ref())
+ .map(|item| &item.view)
}
fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
@@ -170,7 +178,7 @@ impl View for Sidebar {
container.add_child(
Hook::new(
- ChildView::new(active_item)
+ ChildView::new(active_item.to_any())
.constrained()
.with_max_width(*self.custom_width.borrow())
.boxed(),
@@ -1123,13 +1123,13 @@ impl Workspace {
};
let active_item = sidebar.update(cx, |sidebar, cx| {
sidebar.activate_item(action.item_index, cx);
- sidebar.active_item().map(|item| item.to_any())
+ sidebar.active_item().cloned()
});
if let Some(active_item) = active_item {
if active_item.is_focused(cx) {
cx.focus_self();
} else {
- cx.focus(active_item);
+ cx.focus(active_item.to_any());
}
}
cx.notify();
@@ -13,7 +13,7 @@ import projectDiagnostics from "./projectDiagnostics";
import contactNotification from "./contactNotification";
export const panel = {
- padding: { top: 12, left: 12, bottom: 12, right: 12 },
+ padding: { top: 12, bottom: 12 },
};
export default function app(theme: Theme): Object {
@@ -3,7 +3,10 @@ import { panel } from "./app";
import { backgroundColor, border, borderColor, iconColor, player, text } from "./components";
export default function contactsPanel(theme: Theme) {
- const project = {
+ const nameMargin = 8;
+ const sidePadding = 12;
+
+ const projectRow = {
guestAvatarSpacing: 4,
height: 24,
guestAvatar: {
@@ -13,21 +16,19 @@ export default function contactsPanel(theme: Theme) {
name: {
...text(theme, "mono", "placeholder", { size: "sm" }),
margin: {
+ left: nameMargin,
right: 6,
},
},
- padding: {
- left: 8,
+ guests: {
+ margin: {
+ left: nameMargin,
+ right: nameMargin,
+ }
},
- };
-
- const sharedProject = {
- ...project,
- background: backgroundColor(theme, 300),
- cornerRadius: 6,
- name: {
- ...project.name,
- ...text(theme, "mono", "secondary", { size: "sm" }),
+ padding: {
+ left: sidePadding,
+ right: sidePadding,
},
};
@@ -54,34 +55,62 @@ export default function contactsPanel(theme: Theme) {
right: 8,
top: 4,
},
+ margin: {
+ left: sidePadding,
+ right: sidePadding,
+ }
},
userQueryEditorHeight: 32,
addContactButton: {
- margin: { left: 6 },
+ margin: { left: 6, right: 12 },
color: iconColor(theme, "primary"),
buttonWidth: 8,
iconWidth: 8,
},
- row: {
- padding: { left: 8 },
- },
rowHeight: 28,
- header: {
+ sectionIconSize: 8,
+ headerRow: {
...text(theme, "mono", "secondary", { size: "sm" }),
- margin: { top: 8 },
+ margin: { top: 14 },
+ padding: {
+ left: sidePadding,
+ right: sidePadding,
+ },
+ active: {
+ ...text(theme, "mono", "primary", { size: "sm" }),
+ background: backgroundColor(theme, 100, "active"),
+ }
+ },
+ contactRow: {
+ padding: {
+ left: sidePadding,
+ right: sidePadding
+ },
+ active: {
+ background: backgroundColor(theme, 100, "active"),
+ }
+ },
+ treeBranch: {
+ color: borderColor(theme, "active"),
+ width: 1,
+ hover: {
+ color: borderColor(theme, "active"),
+ },
+ active: {
+ color: borderColor(theme, "active"),
+ }
},
- treeBranchColor: borderColor(theme, "muted"),
- treeBranchWidth: 1,
contactAvatar: {
cornerRadius: 10,
width: 18,
},
contactUsername: {
...text(theme, "mono", "primary", { size: "sm" }),
- padding: {
- left: 8,
+ margin: {
+ left: nameMargin,
},
},
+ contactButtonSpacing: nameMargin,
contactButton: {
...contactButton,
hover: {
@@ -93,17 +122,33 @@ 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: {
- ...project,
- cornerRadius: 6,
+ sharedProjectRow: {
+ ...projectRow,
+ background: backgroundColor(theme, 300),
+ name: {
+ ...projectRow.name,
+ ...text(theme, "mono", "secondary", { size: "sm" }),
+ },
+ hover: {
+ background: backgroundColor(theme, 300, "hovered"),
+ },
+ active: {
+ background: backgroundColor(theme, 300, "active"),
+ }
},
+ unsharedProjectRow: {
+ ...projectRow,
+ background: backgroundColor(theme, 300),
+ name: {
+ ...projectRow.name,
+ ...text(theme, "mono", "secondary", { size: "sm" }),
+ },
+ hover: {
+ background: backgroundColor(theme, 300, "hovered"),
+ },
+ active: {
+ background: backgroundColor(theme, 300, "active"),
+ }
+ }
}
}