diff --git a/Cargo.lock b/Cargo.lock index 57dcade5b9a1a63c3d85c9535e0671c2fbd70d57..60b35ef3c624651fd7cc8e6f52d7e9d645492c19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -936,9 +936,11 @@ dependencies = [ "futures", "fuzzy", "gpui", + "language", "log", "picker", "postage", + "project", "serde", "settings", "theme", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index dd83b91ed06f95361349d340e2c708f8145acc46..fdd5a4fcbb0e9da9f8727119253e59d8e82cacfb 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -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 + } ] } }, diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index bdc611b830065dfee72f0e0104eb08373579ee80..13eb8f247367069dfa3c1ec04e19dd11e3690c8e 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -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": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index b97b7f13fd9a45238c0bee8a9408c775ec6dfd1b..097d2755aa256b6d5942e0737408b3a25cfc28c7 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -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": { diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 37cb0a80bbde9b35a5e725c674380adad86a02d7..c4e33765ce10a32e4f60794b06396f94a15b46dc 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -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": { diff --git a/assets/themes/light.json b/assets/themes/light.json index 7491b039b91eb91b780f4a815a018d2eafde5549..1cff4df3ad5148a4ce60d883c2b1863013484575 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -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": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index b1fc074ff73131023d55aeb5d78678164d488db4..8606c78ca06fccbcc214505d664540314bb8a8fb 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -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": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 72e12bcb5b5b00653056c79ca2d33caaebdf4bef..fb8e781ea0d9c8361c8a76f118f6ffa19c35feaf 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -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": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 26568e91b2be123ca2005cc5c165736fab776f36..607cecf990c3853624f682c82091ff92ed8bc226 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -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": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 031d6652b9f9acc8aa1c123d993b0dd7db231a77..d66382331ae8c43419772ddd017b635b94a230ed 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -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": { diff --git a/crates/client/src/http.rs b/crates/client/src/http.rs index 4dbc9b629a213aed0b1533e51e18cf93f559ee9b..7f4cafa17d68f78b2abb61243bee87a0e7f66a31 100644 --- a/crates/client/src/http.rs +++ b/crates/client/src/http.rs @@ -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> { - 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(), + } } } diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index ad7cb81132b3f9b74b6e7870325b7a96b6f37287..face7db16e2713c9208b1d702a3f7918d84bb164 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -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)? } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index ecd384794551e46f0d42f34072b79f2b32962d2b..1e7384c2c3dd91c2591ee46725f0ab73c7c99aa3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -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() } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 4ab6df0adc9fd4bbdc62a2bd187abf5f029a12eb..07103204e5553d7f6a19f96209261c706fafb18c 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -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 { - // 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::>(); - // 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, diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index 619bcad3385255a232103efad30b954170e3f0da..de49f070b94338cd64e5cf29056fd7c1bb2088ec 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -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"] } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index e26e64f9f6ee18353dd00163fe1008d52b384b92..03968d5aaaf740552423b4331026a38150d233f1 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -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), OutgoingRequest(Arc), Contact(Arc), + ContactProject(Arc, usize), } +#[derive(Clone)] +struct ToggleExpanded(Section); + pub struct ContactsPanel { entries: Vec, match_candidates: Vec, list_state: ListState, user_store: ModelHandle, filter_editor: ViewHandle, + collapsed_sections: Vec
, + selection: Option, + app_state: Arc, _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, cx: &mut ViewContext, ) -> 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::(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, + 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, current_user_id: Option, + project_ix: usize, app_state: Arc, 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::(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::( - 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, 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::(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::, _>(|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) { + 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) { + 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) { + 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) { + 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) { + 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::().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, cx: &TestAppContext) -> Vec { + 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, 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::().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, + ) + } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9875f974983706204b0287913cc2eb15402123ab..c6ad68ace6f78fcbf3288161eef2fb36284f8f7f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -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, + pub contact_row: Interactive, + pub shared_project_row: Interactive, + pub unshared_project_row: Interactive, pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainedText, pub contact_button: Interactive, + 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, + 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, } diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 366c74e43f7a3bcd31dd44cf6ee0465b07f66558..685782a2d230c26c5946a22e92f0e2a959b2f1f5 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -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> { 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) -> 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(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 21d5581640d8654f7192bb3d7b07d771fcf8ff2f..fada690bb52a3fc80fddf0c32b83dee9cd433cea 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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(); diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 84835970279e89de515547d6232bcf3dd647ffbf..9822f6766e3e2a99731dfca0b34494892ec68ea9 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -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 { diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 3cc0f35c3eb176911699741591042d4f34177293..a2caafadec633aeda5ecd7fc3334cecb784a2b6a 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -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"), + } + } } }