From d0052ccfb53b891859a37e10f8e09193ef0d5b17 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 21:44:33 -0700 Subject: [PATCH 1/9] Avoid panic when trying to fetch an invalid URL --- crates/client/src/http.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) 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(), + } } } From 297fa1af55d79d017c474a82ce57c6c7e51938b7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 21:45:12 -0700 Subject: [PATCH 2/9] Avoid possible memory leak of FakeServer in tests --- crates/client/src/test.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) 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)? } From 08a7543913fd2987c0c7b283e4b5cdcd1d4eb107 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 21:45:49 -0700 Subject: [PATCH 3/9] WIP - start work on keyboard navigation in contacts panel --- Cargo.lock | 2 + assets/themes/cave-dark.json | 99 +-- assets/themes/cave-light.json | 99 +-- assets/themes/dark.json | 99 +-- assets/themes/light.json | 99 +-- assets/themes/solarized-dark.json | 99 +-- assets/themes/solarized-light.json | 99 +-- assets/themes/sulphurpool-dark.json | 99 +-- assets/themes/sulphurpool-light.json | 99 +-- crates/contacts_panel/Cargo.toml | 5 + crates/contacts_panel/src/contacts_panel.rs | 680 ++++++++++++++------ crates/theme/src/theme.rs | 10 +- styles/src/styleTree/contactsPanel.ts | 61 +- 13 files changed, 791 insertions(+), 759 deletions(-) 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/themes/cave-dark.json b/assets/themes/cave-dark.json index cb1208a1db764bbb540dc73540eb723bfded2d91..826206477d74fbac497cb28b662a9449c3a5dcca 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1249,20 +1249,29 @@ "button_width": 8, "icon_width": 8 }, - "row": { - "padding": { - "left": 8 - } - }, - "row_height": 28, - "header": { + "header_row": { "family": "Zed Mono", "color": "#8b8792", "size": 14, "margin": { "top": 8 + }, + "active": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14, + "background": "#5852605c" } }, + "contact_row": { + "padding": { + "left": 8 + }, + "active": { + "background": "#5852605c" + } + }, + "row_height": 28, "tree_branch_color": "#655f6d", "tree_branch_width": 1, "contact_avatar": { @@ -1294,26 +1303,7 @@ "button_width": 16, "corner_radius": 8 }, - "project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#7e7887", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - } - }, - "shared_project": { + "shared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1332,9 +1322,15 @@ "left": 8 }, "background": "#26232a", - "corner_radius": 6 + "corner_radius": 6, + "hover": { + "background": "#5852603d" + }, + "active": { + "background": "#5852605c" + } }, - "hovered_shared_project": { + "unshared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1352,47 +1348,14 @@ "padding": { "left": 8 }, - "background": "#5852603d", - "corner_radius": 6 - }, - "unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#7e7887", - "size": 14, - "margin": { - "right": 6 - } + "background": "#26232a", + "corner_radius": 6, + "hover": { + "background": "#5852603d" }, - "padding": { - "left": 8 + "active": { + "background": "#5852605c" } - }, - "hovered_unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#7e7887", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - }, - "corner_radius": 6 } }, "contact_finder": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index f0b3f5bd438a53178d9f56f88e8fbf1dbd41472d..19158e53b17a3f1080e08dbf74c2e16657e4e1f1 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1249,20 +1249,29 @@ "button_width": 8, "icon_width": 8 }, - "row": { - "padding": { - "left": 8 - } - }, - "row_height": 28, - "header": { + "header_row": { "family": "Zed Mono", "color": "#585260", "size": 14, "margin": { "top": 8 + }, + "active": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14, + "background": "#8b87922e" } }, + "contact_row": { + "padding": { + "left": 8 + }, + "active": { + "background": "#8b87922e" + } + }, + "row_height": 28, "tree_branch_color": "#7e7887", "tree_branch_width": 1, "contact_avatar": { @@ -1294,26 +1303,7 @@ "button_width": 16, "corner_radius": 8 }, - "project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#655f6d", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - } - }, - "shared_project": { + "shared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1332,9 +1322,15 @@ "left": 8 }, "background": "#e2dfe7", - "corner_radius": 6 + "corner_radius": 6, + "hover": { + "background": "#8b87921f" + }, + "active": { + "background": "#8b87922e" + } }, - "hovered_shared_project": { + "unshared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1352,47 +1348,14 @@ "padding": { "left": 8 }, - "background": "#8b87921f", - "corner_radius": 6 - }, - "unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#655f6d", - "size": 14, - "margin": { - "right": 6 - } + "background": "#e2dfe7", + "corner_radius": 6, + "hover": { + "background": "#8b87921f" }, - "padding": { - "left": 8 + "active": { + "background": "#8b87922e" } - }, - "hovered_unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#655f6d", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - }, - "corner_radius": 6 } }, "contact_finder": { diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 9cc3badc8104dbee88d2862ad7feeedba22383e8..3adc9ec29e2c10331a771c6a6d93ba1235e89625 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1249,20 +1249,29 @@ "button_width": 8, "icon_width": 8 }, - "row": { - "padding": { - "left": 8 - } - }, - "row_height": 28, - "header": { + "header_row": { "family": "Zed Mono", "color": "#9c9c9c", "size": 14, "margin": { "top": 8 + }, + "active": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14, + "background": "#1c1c1c" } }, + "contact_row": { + "padding": { + "left": 8 + }, + "active": { + "background": "#1c1c1c" + } + }, + "row_height": 28, "tree_branch_color": "#404040", "tree_branch_width": 1, "contact_avatar": { @@ -1294,26 +1303,7 @@ "button_width": 16, "corner_radius": 8 }, - "project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#474747", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - } - }, - "shared_project": { + "shared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1332,9 +1322,15 @@ "left": 8 }, "background": "#1c1c1c", - "corner_radius": 6 + "corner_radius": 6, + "hover": { + "background": "#232323" + }, + "active": { + "background": "#2b2b2b" + } }, - "hovered_shared_project": { + "unshared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1352,47 +1348,14 @@ "padding": { "left": 8 }, - "background": "#232323", - "corner_radius": 6 - }, - "unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#474747", - "size": 14, - "margin": { - "right": 6 - } + "background": "#1c1c1c", + "corner_radius": 6, + "hover": { + "background": "#232323" }, - "padding": { - "left": 8 + "active": { + "background": "#2b2b2b" } - }, - "hovered_unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#474747", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - }, - "corner_radius": 6 } }, "contact_finder": { diff --git a/assets/themes/light.json b/assets/themes/light.json index e2563fadad64d74d0b7add301cbfb2fc0969be6d..69bca672f74a622158078132009914064df75f7b 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1249,20 +1249,29 @@ "button_width": 8, "icon_width": 8 }, - "row": { - "padding": { - "left": 8 - } - }, - "row_height": 28, - "header": { + "header_row": { "family": "Zed Mono", "color": "#474747", "size": 14, "margin": { "top": 8 + }, + "active": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14, + "background": "#d5d5d5" } }, + "contact_row": { + "padding": { + "left": 8 + }, + "active": { + "background": "#d5d5d5" + } + }, + "row_height": 28, "tree_branch_color": "#e3e3e3", "tree_branch_width": 1, "contact_avatar": { @@ -1294,26 +1303,7 @@ "button_width": 16, "corner_radius": 8 }, - "project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#808080", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - } - }, - "shared_project": { + "shared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1332,9 +1322,15 @@ "left": 8 }, "background": "#f8f8f8", - "corner_radius": 6 + "corner_radius": 6, + "hover": { + "background": "#eaeaea" + }, + "active": { + "background": "#e3e3e3" + } }, - "hovered_shared_project": { + "unshared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1352,47 +1348,14 @@ "padding": { "left": 8 }, - "background": "#eaeaea", - "corner_radius": 6 - }, - "unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#808080", - "size": 14, - "margin": { - "right": 6 - } + "background": "#f8f8f8", + "corner_radius": 6, + "hover": { + "background": "#eaeaea" }, - "padding": { - "left": 8 + "active": { + "background": "#e3e3e3" } - }, - "hovered_unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#808080", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - }, - "corner_radius": 6 } }, "contact_finder": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 6e8c405b6c212bf77fc99c99ea3c2a6dcf5a2f07..75e0eb9b7b460fecc6f02aa3969badfa978d0983 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1249,20 +1249,29 @@ "button_width": 8, "icon_width": 8 }, - "row": { - "padding": { - "left": 8 - } - }, - "row_height": 28, - "header": { + "header_row": { "family": "Zed Mono", "color": "#93a1a1", "size": 14, "margin": { "top": 8 + }, + "active": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14, + "background": "#586e755c" } }, + "contact_row": { + "padding": { + "left": 8 + }, + "active": { + "background": "#586e755c" + } + }, + "row_height": 28, "tree_branch_color": "#657b83", "tree_branch_width": 1, "contact_avatar": { @@ -1294,26 +1303,7 @@ "button_width": 16, "corner_radius": 8 }, - "project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#839496", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - } - }, - "shared_project": { + "shared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1332,9 +1322,15 @@ "left": 8 }, "background": "#073642", - "corner_radius": 6 + "corner_radius": 6, + "hover": { + "background": "#586e753d" + }, + "active": { + "background": "#586e755c" + } }, - "hovered_shared_project": { + "unshared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1352,47 +1348,14 @@ "padding": { "left": 8 }, - "background": "#586e753d", - "corner_radius": 6 - }, - "unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#839496", - "size": 14, - "margin": { - "right": 6 - } + "background": "#073642", + "corner_radius": 6, + "hover": { + "background": "#586e753d" }, - "padding": { - "left": 8 + "active": { + "background": "#586e755c" } - }, - "hovered_unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#839496", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - }, - "corner_radius": 6 } }, "contact_finder": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 3f5b26ee56a24a883fc21c45a9de60bec20b9a5d..e88872bdfb02b91a3cde810a4f8afe6a267bc7ad 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1249,20 +1249,29 @@ "button_width": 8, "icon_width": 8 }, - "row": { - "padding": { - "left": 8 - } - }, - "row_height": 28, - "header": { + "header_row": { "family": "Zed Mono", "color": "#586e75", "size": 14, "margin": { "top": 8 + }, + "active": { + "family": "Zed Mono", + "color": "#073642", + "size": 14, + "background": "#93a1a12e" } }, + "contact_row": { + "padding": { + "left": 8 + }, + "active": { + "background": "#93a1a12e" + } + }, + "row_height": 28, "tree_branch_color": "#839496", "tree_branch_width": 1, "contact_avatar": { @@ -1294,26 +1303,7 @@ "button_width": 16, "corner_radius": 8 }, - "project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#657b83", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - } - }, - "shared_project": { + "shared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1332,9 +1322,15 @@ "left": 8 }, "background": "#eee8d5", - "corner_radius": 6 + "corner_radius": 6, + "hover": { + "background": "#93a1a11f" + }, + "active": { + "background": "#93a1a12e" + } }, - "hovered_shared_project": { + "unshared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1352,47 +1348,14 @@ "padding": { "left": 8 }, - "background": "#93a1a11f", - "corner_radius": 6 - }, - "unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#657b83", - "size": 14, - "margin": { - "right": 6 - } + "background": "#eee8d5", + "corner_radius": 6, + "hover": { + "background": "#93a1a11f" }, - "padding": { - "left": 8 + "active": { + "background": "#93a1a12e" } - }, - "hovered_unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#657b83", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - }, - "corner_radius": 6 } }, "contact_finder": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 0f2a868f24d028be4c960db27b39b36b540a3e5d..ef136a0e4404af94996912fa75150c94c07c59d8 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1249,20 +1249,29 @@ "button_width": 8, "icon_width": 8 }, - "row": { - "padding": { - "left": 8 - } - }, - "row_height": 28, - "header": { + "header_row": { "family": "Zed Mono", "color": "#979db4", "size": 14, "margin": { "top": 8 + }, + "active": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14, + "background": "#5e66875c" } }, + "contact_row": { + "padding": { + "left": 8 + }, + "active": { + "background": "#5e66875c" + } + }, + "row_height": 28, "tree_branch_color": "#6b7394", "tree_branch_width": 1, "contact_avatar": { @@ -1294,26 +1303,7 @@ "button_width": 16, "corner_radius": 8 }, - "project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#898ea4", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - } - }, - "shared_project": { + "shared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1332,9 +1322,15 @@ "left": 8 }, "background": "#293256", - "corner_radius": 6 + "corner_radius": 6, + "hover": { + "background": "#5e66873d" + }, + "active": { + "background": "#5e66875c" + } }, - "hovered_shared_project": { + "unshared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1352,47 +1348,14 @@ "padding": { "left": 8 }, - "background": "#5e66873d", - "corner_radius": 6 - }, - "unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#898ea4", - "size": 14, - "margin": { - "right": 6 - } + "background": "#293256", + "corner_radius": 6, + "hover": { + "background": "#5e66873d" }, - "padding": { - "left": 8 + "active": { + "background": "#5e66875c" } - }, - "hovered_unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#898ea4", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - }, - "corner_radius": 6 } }, "contact_finder": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index b9106c62f3d273a537616f0cbd38090b8c854411..998a540f1b4eb9b57ee602bd744fca8f8b41560e 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1249,20 +1249,29 @@ "button_width": 8, "icon_width": 8 }, - "row": { - "padding": { - "left": 8 - } - }, - "row_height": 28, - "header": { + "header_row": { "family": "Zed Mono", "color": "#5e6687", "size": 14, "margin": { "top": 8 + }, + "active": { + "family": "Zed Mono", + "color": "#293256", + "size": 14, + "background": "#979db42e" } }, + "contact_row": { + "padding": { + "left": 8 + }, + "active": { + "background": "#979db42e" + } + }, + "row_height": 28, "tree_branch_color": "#898ea4", "tree_branch_width": 1, "contact_avatar": { @@ -1294,26 +1303,7 @@ "button_width": 16, "corner_radius": 8 }, - "project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#6b7394", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - } - }, - "shared_project": { + "shared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1332,9 +1322,15 @@ "left": 8 }, "background": "#dfe2f1", - "corner_radius": 6 + "corner_radius": 6, + "hover": { + "background": "#979db41f" + }, + "active": { + "background": "#979db42e" + } }, - "hovered_shared_project": { + "unshared_project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { @@ -1352,47 +1348,14 @@ "padding": { "left": 8 }, - "background": "#979db41f", - "corner_radius": 6 - }, - "unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#6b7394", - "size": 14, - "margin": { - "right": 6 - } + "background": "#dfe2f1", + "corner_radius": 6, + "hover": { + "background": "#979db41f" }, - "padding": { - "left": 8 + "active": { + "background": "#979db42e" } - }, - "hovered_unshared_project": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#6b7394", - "size": 14, - "margin": { - "right": 6 - } - }, - "padding": { - "left": 8 - }, - "corner_radius": 6 } }, "contact_finder": { 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 5d96a1b0c20f351c659e83256eb30610cde9ba10..4cc8091a77ebee79016cc8b68ac76a9b6de36a10 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -15,6 +15,7 @@ use serde::Deserialize; use settings::Settings; use std::sync::Arc; use theme::IconButton; +use workspace::menu::{SelectNext, SelectPrev}; use workspace::{AppState, JoinProject}; impl_actions!( @@ -22,12 +23,13 @@ impl_actions!( [RequestContact, RemoveContact, RespondToContactRequest] ); -#[derive(Debug)] +#[derive(Clone, Debug)] enum ContactEntry { Header(&'static str), IncomingRequest(Arc), OutgoingRequest(Arc), Contact(Arc), + ContactProject(Arc, usize), } pub struct ContactsPanel { @@ -36,6 +38,7 @@ pub struct ContactsPanel { list_state: ListState, user_store: ModelHandle, filter_editor: ViewHandle, + selection: Option, _maintain_contacts: Subscription, } @@ -57,6 +60,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::respond_to_contact_request); cx.add_action(ContactsPanel::clear_filter); + cx.add_action(ContactsPanel::select_next); + cx.add_action(ContactsPanel::select_prev); } impl ContactsPanel { @@ -72,6 +77,7 @@ impl ContactsPanel { cx.subscribe(&user_query_editor, |this, _, event, cx| { if let editor::Event::BufferEdited = event { + this.selection.take(); this.update_entries(cx) } }) @@ -88,17 +94,20 @@ impl ContactsPanel { let theme = &theme.contacts_panel; let current_user_id = this.user_store.read(cx).current_user().map(|user| user.id); + let is_selected = this.selection == Some(ix); match &this.entries[ix] { ContactEntry::Header(text) => { - Label::new(text.to_string(), theme.header.text.clone()) + let header_style = + theme.header_row.style_for(&Default::default(), is_selected); + Label::new(text.to_string(), header_style.text.clone()) .contained() .aligned() .left() .constrained() .with_height(theme.row_height) .contained() - .with_style(theme.header.container) + .with_style(header_style.container) .boxed() } ContactEntry::IncomingRequest(user) => Self::render_contact_request( @@ -106,6 +115,7 @@ impl ContactsPanel { this.user_store.clone(), theme, true, + is_selected, cx, ), ContactEntry::OutgoingRequest(user) => Self::render_contact_request( @@ -113,18 +123,36 @@ impl ContactsPanel { this.user_store.clone(), theme, false, + is_selected, cx, ), - ContactEntry::Contact(contact) => Self::render_contact( - contact.clone(), - current_user_id, - app_state.clone(), - theme, - cx, - ), + ContactEntry::Contact(contact) => { + Self::render_contact(contact.clone(), theme, is_selected) + } + ContactEntry::ContactProject(contact, project_ix) => { + let is_last_project_for_contact = + this.entries.get(ix + 1).map_or(true, |next| { + if let ContactEntry::ContactProject(next_contact, _) = next { + next_contact.user.id != contact.user.id + } else { + true + } + }); + Self::render_contact_project( + contact.clone(), + current_user_id, + *project_ix, + app_state.clone(), + theme, + is_last_project_for_contact, + is_selected, + cx, + ) + } } } }), + selection: None, entries: Default::default(), match_candidates: Default::default(), filter_editor: user_query_editor, @@ -137,175 +165,173 @@ impl ContactsPanel { } fn render_contact( + contact: Arc, + theme: &theme::ContactsPanel, + is_selected: bool, + ) -> ElementBox { + Flex::row() + .with_children(contact.user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + contact.user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .boxed(), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style( + *theme + .contact_row + .style_for(&Default::default(), is_selected), + ) + .boxed() + } + + fn render_contact_project( contact: Arc, 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 font_cache = cx.font_cache(); - let line_height = theme.unshared_project.name.text.line_height(font_cache); - let cap_height = theme.unshared_project.name.text.cap_height(font_cache); - let baseline_offset = theme.unshared_project.name.text.baseline_offset(font_cache) - + (theme.unshared_project.height - line_height) / 2.; - let tree_branch_width = theme.tree_branch_width; - let tree_branch_color = theme.tree_branch_color; let host_avatar_height = theme .contact_avatar .width .or(theme.contact_avatar.height) .unwrap_or(0.); + let row = &theme.unshared_project_row.default; + let line_height = row.name.text.line_height(font_cache); + let cap_height = row.name.text.cap_height(font_cache); + let baseline_offset = + row.name.text.baseline_offset(font_cache) + (row.height - line_height) / 2.; + let tree_branch_width = theme.tree_branch_width; + let tree_branch_color = theme.tree_branch_color; - Flex::column() + Flex::row() .with_child( - Flex::row() - .with_children(contact.user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - contact.user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .boxed(), + Canvas::new(move |bounds, _, cx| { + let start_x = bounds.min_x() + (bounds.width() / 2.) - (tree_branch_width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch_width, + if is_last_project { + end_y + } else { + bounds.max_y() + }, + ), + ), + background: Some(tree_branch_color), + border: gpui::Border::default(), + corner_radius: 0., + }); + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + tree_branch_width), + ), + background: Some(tree_branch_color), + border: gpui::Border::default(), + corner_radius: 0., + }); + }) + .constrained() + .with_width(host_avatar_height) + .boxed(), ) - .with_children( - contact - .non_empty_projects() - .enumerate() - .map(|(ix, project)| { - let project_id = project.id; + .with_child({ + let is_host = Some(contact.user.id) == current_user_id; + let is_guest = !is_host + && project + .guests + .iter() + .any(|guest| Some(guest.id) == current_user_id); + let is_shared = project.is_shared; + let app_state = app_state.clone(); + + MouseEventHandler::new::( + project_id as usize, + cx, + |mouse_state, _| { + let style = if project.is_shared { + &theme.shared_project_row + } else { + &theme.unshared_project_row + } + .style_for(mouse_state, is_selected); Flex::row() .with_child( - Canvas::new(move |bounds, _, cx| { - let start_x = bounds.min_x() + (bounds.width() / 2.) - - (tree_branch_width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = - bounds.min_y() + baseline_offset - (cap_height / 2.); - - cx.scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch_width, - if ix + 1 == project_count { - end_y - } else { - bounds.max_y() - }, - ), - ), - background: Some(tree_branch_color), - border: gpui::Border::default(), - corner_radius: 0., - }); - cx.scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch_width), - ), - background: Some(tree_branch_color), - border: gpui::Border::default(), - corner_radius: 0., - }); - }) - .constrained() - .with_width(host_avatar_height) + Label::new( + project.worktree_root_names.join(", "), + style.name.text.clone(), + ) + .aligned() + .left() + .contained() + .with_style(style.name.container) .boxed(), ) - .with_child({ - let is_host = Some(contact.user.id) == current_user_id; - let is_guest = !is_host - && project - .guests - .iter() - .any(|guest| Some(guest.id) == current_user_id); - let is_shared = project.is_shared; - let app_state = app_state.clone(); - - MouseEventHandler::new::( - 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(), - }); - } + .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() }) - .flex(1., true) - .boxed() - }) + })) + .contained() + .with_style(style.container) .constrained() - .with_height(theme.unshared_project.height) + .with_height(style.height) .boxed() - }), - ) - .contained() - .with_style(theme.row.clone()) + }, + ) + .with_cursor_style(if !is_host && is_shared { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + .on_click(move |_, cx| { + if !is_host && !is_guest { + cx.dispatch_global_action(JoinProject { + project_id, + app_state: app_state.clone(), + }); + } + }) + .flex(1., true) + .boxed() + }) + .constrained() + .with_height(row.height) .boxed() } @@ -314,6 +340,7 @@ impl ContactsPanel { user_store: ModelHandle, theme: &theme::ContactsPanel, is_incoming: bool, + is_selected: bool, cx: &mut LayoutContext, ) -> ElementBox { enum Reject {} @@ -409,7 +436,11 @@ impl ContactsPanel { row.constrained() .with_height(theme.row_height) .contained() - .with_style(theme.row) + .with_style( + *theme + .contact_row + .style_for(&Default::default(), is_selected), + ) .boxed() } @@ -418,6 +449,7 @@ impl ContactsPanel { let query = self.filter_editor.read(cx).text(cx); let executor = cx.background().clone(); + let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); self.entries.clear(); let mut request_entries = Vec::new(); @@ -443,13 +475,11 @@ impl ContactsPanel { &Default::default(), executor.clone(), )); - if !matches.is_empty() { - request_entries.extend( - matches.iter().map(|mat| { - ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone()) - }), - ); - } + request_entries.extend( + matches + .iter() + .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), + ); } let outgoing = user_store.outgoing_contact_requests(); @@ -474,13 +504,11 @@ impl ContactsPanel { &Default::default(), executor.clone(), )); - if !matches.is_empty() { - request_entries.extend( - matches.iter().map(|mat| { - ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone()) - }), - ); - } + request_entries.extend( + matches + .iter() + .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + ); } if !request_entries.is_empty() { @@ -515,22 +543,33 @@ impl ContactsPanel { .iter() .partition::, _>(|mat| contacts[mat.candidate_id].online); - if !online_contacts.is_empty() { - self.entries.push(ContactEntry::Header("Online")); - self.entries.extend( - online_contacts - .into_iter() - .map(|mat| ContactEntry::Contact(contacts[mat.candidate_id].clone())), - ); + for (matches, name) in [(online_contacts, "Online"), (offline_contacts, "Offline")] { + if !matches.is_empty() { + self.entries.push(ContactEntry::Header(name)); + for mat in matches { + let contact = &contacts[mat.candidate_id]; + self.entries.push(ContactEntry::Contact(contact.clone())); + self.entries + .extend(contact.projects.iter().enumerate().filter_map( + |(ix, project)| { + if project.worktree_root_names.is_empty() { + None + } else { + Some(ContactEntry::ContactProject(contact.clone(), ix)) + } + }, + )); + } + } } + } - if !offline_contacts.is_empty() { - self.entries.push(ContactEntry::Header("Offline")); - self.entries.extend( - offline_contacts - .into_iter() - .map(|mat| ContactEntry::Contact(contacts[mat.candidate_id].clone())), - ); + if let Some(selection) = &mut self.selection { + for (ix, entry) in self.entries.iter().enumerate() { + if Some(entry) == prev_selected_entry.as_ref() { + *selection = ix; + break; + } } } @@ -566,6 +605,30 @@ impl ContactsPanel { self.filter_editor .update(cx, |editor, cx| editor.set_text("", cx)); } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + 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 render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { @@ -633,4 +696,249 @@ impl View for ContactsPanel { .with_style(theme.container) .boxed() } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.filter_editor); + } + + fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { + let mut cx = Self::default_keymap_context(); + cx.set.insert("menu".into()); + cx + } +} + +impl PartialEq for ContactEntry { + fn eq(&self, other: &Self) -> bool { + match self { + ContactEntry::Header(name_1) => { + if let ContactEntry::Header(name_2) = other { + return name_1 == name_2; + } + } + ContactEntry::IncomingRequest(user_1) => { + if let ContactEntry::IncomingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ContactEntry::OutgoingRequest(user_1) => { + if let ContactEntry::OutgoingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ContactEntry::Contact(contact_1) => { + if let ContactEntry::Contact(contact_2) = other { + return contact_1.user.id == contact_2.user.id; + } + } + ContactEntry::ContactProject(contact_1, ix_1) => { + if let ContactEntry::ContactProject(contact_2, ix_2) = other { + return contact_1.user.id == contact_2.user.id && ix_1 == ix_2; + } + } + } + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use client::{proto, test::FakeServer, ChannelList, Client}; + use gpui::TestAppContext; + use language::LanguageRegistry; + use theme::ThemeRegistry; + + #[gpui::test] + async fn test_contact_panel(cx: &mut TestAppContext) { + let (app_state, server) = init(cx).await; + let panel = cx.add_view(0, |cx| ContactsPanel::new(app_state.clone(), cx)); + + let get_users_request = server.receive::().await.unwrap(); + server + .respond( + get_users_request.receipt(), + proto::UsersResponse { + users: [ + "user_zero", + "user_one", + "user_two", + "user_three", + "user_four", + "user_five", + ] + .into_iter() + .enumerate() + .map(|(id, name)| proto::User { + id: id as u64, + github_login: name.to_string(), + ..Default::default() + }) + .collect(), + }, + ) + .await; + + server.send(proto::UpdateContacts { + incoming_requests: vec![proto::IncomingContactRequest { + requester_id: 1, + should_notify: false, + }], + outgoing_requests: vec![2], + contacts: vec![ + proto::Contact { + user_id: 3, + online: true, + projects: vec![proto::ProjectMetadata { + id: 101, + worktree_root_names: vec!["dir1".to_string()], + is_shared: true, + guests: vec![2], + }], + }, + proto::Contact { + user_id: 4, + online: true, + projects: vec![proto::ProjectMetadata { + id: 102, + worktree_root_names: vec!["dir2".to_string()], + is_shared: true, + guests: vec![2], + }], + }, + proto::Contact { + user_id: 5, + online: false, + projects: vec![], + }, + ], + ..Default::default() + }); + + cx.foreground().run_until_parked(); + assert_eq!( + render_to_strings(&panel, cx), + &[ + "+", + "v Requests", + " incoming user_one <=== selected", + " outgoing user_two", + "v Online", + " user_four", + " dir2", + " user_three", + " dir1", + "v Offline", + " user_five", + ] + ); + + panel.update(cx, |panel, cx| { + panel + .filter_editor + .update(cx, |editor, cx| editor.set_text("f", cx)) + }); + cx.foreground().run_until_parked(); + assert_eq!( + render_to_strings(&panel, cx), + &[ + "+", + "Online", + " user_four <=== selected", + " dir2", + "Offline", + " user_five", + ] + ); + + panel.update(cx, |panel, cx| { + panel.select_next(&Default::default(), cx); + }); + assert_eq!( + render_to_strings(&panel, cx), + &[ + "+", + "Online", + " user_four", + " dir2 <=== selected", + "Offline", + " user_five", + ] + ); + + panel.update(cx, |panel, cx| { + panel.select_next(&Default::default(), cx); + }); + assert_eq!( + render_to_strings(&panel, cx), + &[ + "+", + "Online", + " user_four", + " dir2", + "Offline", + " user_five <=== selected", + ] + ); + } + + fn render_to_strings(panel: &ViewHandle, cx: &TestAppContext) -> Vec { + panel.read_with(cx, |panel, _| { + let mut entries = Vec::new(); + entries.push("+".to_string()); + entries.extend(panel.entries.iter().map(|entry| match entry { + ContactEntry::Header(name) => { + format!("{}", name) + } + ContactEntry::IncomingRequest(user) => { + format!(" incoming {}", user.github_login) + } + ContactEntry::OutgoingRequest(user) => { + format!(" outgoing {}", user.github_login) + } + ContactEntry::Contact(contact) => { + format!(" {}", contact.user.github_login) + } + ContactEntry::ContactProject(contact, project_ix) => { + format!( + " {}", + contact.projects[*project_ix].worktree_root_names.join(", ") + ) + } + })); + entries + }) + } + + async fn init(cx: &mut TestAppContext) -> (Arc, 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 72db11c4931436ad6bf90650561d3baa5fe1eef1..c55fe0ae5f220e51975dab4e96cb0af8203e131d 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -235,11 +235,13 @@ pub struct CommandPalette { pub struct ContactsPanel { #[serde(flatten)] pub container: ContainerStyle, - pub header: ContainedText, pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, pub add_contact_button: IconButton, - pub row: ContainerStyle, + pub header_row: Interactive, + 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, @@ -247,10 +249,6 @@ pub struct ContactsPanel { pub disabled_contact_button: IconButton, pub tree_branch_width: f32, pub tree_branch_color: Color, - pub shared_project: ProjectRow, - pub hovered_shared_project: ProjectRow, - pub unshared_project: ProjectRow, - pub hovered_unshared_project: ProjectRow, } #[derive(Deserialize, Default)] diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 3cc0f35c3eb176911699741591042d4f34177293..69c62b5c024355a30e1483156d76f3fbe3a5dec8 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -21,16 +21,6 @@ export default function contactsPanel(theme: Theme) { }, }; - const sharedProject = { - ...project, - background: backgroundColor(theme, 300), - cornerRadius: 6, - name: { - ...project.name, - ...text(theme, "mono", "secondary", { size: "sm" }), - }, - }; - const contactButton = { background: backgroundColor(theme, 100), color: iconColor(theme, "primary"), @@ -62,14 +52,21 @@ export default function contactsPanel(theme: Theme) { buttonWidth: 8, iconWidth: 8, }, - row: { - padding: { left: 8 }, - }, - rowHeight: 28, - header: { + headerRow: { ...text(theme, "mono", "secondary", { size: "sm" }), margin: { top: 8 }, + active: { + ...text(theme, "mono", "primary", { size: "sm" }), + background: backgroundColor(theme, 100, "active"), + } + }, + contactRow: { + padding: { left: 8 }, + active: { + background: backgroundColor(theme, 100, "active"), + } }, + rowHeight: 28, treeBranchColor: borderColor(theme, "muted"), treeBranchWidth: 1, contactAvatar: { @@ -93,17 +90,35 @@ export default function contactsPanel(theme: Theme) { background: backgroundColor(theme, 100), color: iconColor(theme, "muted"), }, - project, - sharedProject, - hoveredSharedProject: { - ...sharedProject, - background: backgroundColor(theme, 300, "hovered"), + sharedProjectRow: { + ...project, + background: backgroundColor(theme, 300), cornerRadius: 6, + name: { + ...project.name, + ...text(theme, "mono", "secondary", { size: "sm" }), + }, + hover: { + background: backgroundColor(theme, 300, "hovered"), + }, + active: { + background: backgroundColor(theme, 300, "active"), + } }, - unsharedProject: project, - hoveredUnsharedProject: { + unsharedProjectRow: { ...project, + background: backgroundColor(theme, 300), cornerRadius: 6, - }, + name: { + ...project.name, + ...text(theme, "mono", "secondary", { size: "sm" }), + }, + hover: { + background: backgroundColor(theme, 300, "hovered"), + }, + active: { + background: backgroundColor(theme, 300, "active"), + } + } } } From 4739c683af45d10cddbfcc9e10515aa8967a9446 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 May 2022 16:49:56 -0700 Subject: [PATCH 4/9] Fix bug where Contacts included projects for which the use was a guest --- crates/collab/src/rpc.rs | 74 +++++++++++++------------------ crates/collab/src/rpc/store.rs | 79 ++++++++-------------------------- 2 files changed, 49 insertions(+), 104 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 33d1d526775f841e98e24e154d1ae29dcf54292e..c48ac3b83a8dd20da953d9210d225218981fbb99 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -4995,13 +4995,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![ @@ -5025,27 +5023,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)] { @@ -5083,16 +5064,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)] { @@ -5108,12 +5080,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)] { @@ -5122,7 +5114,7 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![]), + ("user_b", true, vec![("b", false, vec![])]), ("user_c", true, vec![]) ] ) @@ -5138,7 +5130,7 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![]), + ("user_b", true, vec![("b", false, vec![])]), ("user_c", false, vec![]) ] ) @@ -5161,7 +5153,7 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![]), + ("user_b", true, vec![("b", false, vec![])]), ("user_c", true, vec![]) ] ) @@ -5173,7 +5165,7 @@ mod tests { .contacts() .iter() .map(|contact| { - let worktrees = contact + let projects = contact .projects .iter() .map(|p| { @@ -5184,11 +5176,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 8ca270622832b4fc23151508d3df4a4fa9f013c8..52cf2b262810174f7aae8fa876ef797dd59bb77c 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -259,73 +259,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, From 615319b2ab14da98c93ad7526da8490233e2d963 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 May 2022 16:50:51 -0700 Subject: [PATCH 5/9] Rework the contact panel's styling to allow keyboard navigation Co-authored-by: Nathan Sobo --- assets/themes/cave-dark.json | 63 ++++-- assets/themes/cave-light.json | 63 ++++-- assets/themes/dark.json | 63 ++++-- assets/themes/light.json | 63 ++++-- assets/themes/solarized-dark.json | 63 ++++-- assets/themes/solarized-light.json | 63 ++++-- assets/themes/sulphurpool-dark.json | 63 ++++-- assets/themes/sulphurpool-light.json | 63 ++++-- crates/contacts_panel/src/contacts_panel.rs | 211 ++++++++++---------- crates/theme/src/theme.rs | 12 +- styles/src/styleTree/app.ts | 2 +- styles/src/styleTree/contactsPanel.ts | 61 ++++-- 12 files changed, 526 insertions(+), 264 deletions(-) diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 826206477d74fbac497cb28b662a9449c3a5dcca..a944d171fa607303228f857983e7f7bba3c412be 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -989,9 +989,7 @@ "chat_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "channel_name": { "family": "Zed Sans", @@ -1208,9 +1206,7 @@ "contacts_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "user_query_editor": { "background": "#19171c", @@ -1238,23 +1234,33 @@ "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_height": 28, "header_row": { "family": "Zed Mono", "color": "#8b8792", "size": 14, "margin": { - "top": 8 + "top": 14 + }, + "padding": { + "left": 12, + "right": 12 }, "active": { "family": "Zed Mono", @@ -1265,15 +1271,23 @@ }, "contact_row": { "padding": { - "left": 8 + "left": 12, + "right": 12 }, "active": { "background": "#5852605c" } }, - "row_height": 28, - "tree_branch_color": "#655f6d", - "tree_branch_width": 1, + "tree_branch": { + "color": "#655f6d", + "width": 1, + "hover": { + "color": "#655f6d" + }, + "active": { + "color": "#655f6d" + } + }, "contact_avatar": { "corner_radius": 10, "width": 18 @@ -1282,10 +1296,11 @@ "family": "Zed Mono", "color": "#e2dfe7", "size": 14, - "padding": { + "margin": { "left": 8 } }, + "contact_button_spacing": 8, "contact_button": { "background": "#26232a", "color": "#e2dfe7", @@ -1315,14 +1330,21 @@ "color": "#8b8792", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#26232a", - "corner_radius": 6, "hover": { "background": "#5852603d" }, @@ -1342,14 +1364,21 @@ "color": "#8b8792", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#26232a", - "corner_radius": 6, "hover": { "background": "#5852603d" }, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 19158e53b17a3f1080e08dbf74c2e16657e4e1f1..47f397a017e853071c475bac4bb443322762ba12 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -989,9 +989,7 @@ "chat_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "channel_name": { "family": "Zed Sans", @@ -1208,9 +1206,7 @@ "contacts_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "user_query_editor": { "background": "#efecf4", @@ -1238,23 +1234,33 @@ "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_height": 28, "header_row": { "family": "Zed Mono", "color": "#585260", "size": 14, "margin": { - "top": 8 + "top": 14 + }, + "padding": { + "left": 12, + "right": 12 }, "active": { "family": "Zed Mono", @@ -1265,15 +1271,23 @@ }, "contact_row": { "padding": { - "left": 8 + "left": 12, + "right": 12 }, "active": { "background": "#8b87922e" } }, - "row_height": 28, - "tree_branch_color": "#7e7887", - "tree_branch_width": 1, + "tree_branch": { + "color": "#7e7887", + "width": 1, + "hover": { + "color": "#7e7887" + }, + "active": { + "color": "#7e7887" + } + }, "contact_avatar": { "corner_radius": 10, "width": 18 @@ -1282,10 +1296,11 @@ "family": "Zed Mono", "color": "#26232a", "size": 14, - "padding": { + "margin": { "left": 8 } }, + "contact_button_spacing": 8, "contact_button": { "background": "#e2dfe7", "color": "#26232a", @@ -1315,14 +1330,21 @@ "color": "#585260", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#e2dfe7", - "corner_radius": 6, "hover": { "background": "#8b87921f" }, @@ -1342,14 +1364,21 @@ "color": "#585260", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#e2dfe7", - "corner_radius": 6, "hover": { "background": "#8b87921f" }, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 3adc9ec29e2c10331a771c6a6d93ba1235e89625..f9aeb3e3b17b69c55f3c01846495ad0ab7b2c01d 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -989,9 +989,7 @@ "chat_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "channel_name": { "family": "Zed Sans", @@ -1208,9 +1206,7 @@ "contacts_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "user_query_editor": { "background": "#000000", @@ -1238,23 +1234,33 @@ "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_height": 28, "header_row": { "family": "Zed Mono", "color": "#9c9c9c", "size": 14, "margin": { - "top": 8 + "top": 14 + }, + "padding": { + "left": 12, + "right": 12 }, "active": { "family": "Zed Mono", @@ -1265,15 +1271,23 @@ }, "contact_row": { "padding": { - "left": 8 + "left": 12, + "right": 12 }, "active": { "background": "#1c1c1c" } }, - "row_height": 28, - "tree_branch_color": "#404040", - "tree_branch_width": 1, + "tree_branch": { + "color": "#000000", + "width": 1, + "hover": { + "color": "#000000" + }, + "active": { + "color": "#000000" + } + }, "contact_avatar": { "corner_radius": 10, "width": 18 @@ -1282,10 +1296,11 @@ "family": "Zed Mono", "color": "#f1f1f1", "size": 14, - "padding": { + "margin": { "left": 8 } }, + "contact_button_spacing": 8, "contact_button": { "background": "#2b2b2b", "color": "#c6c6c6", @@ -1315,14 +1330,21 @@ "color": "#9c9c9c", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#1c1c1c", - "corner_radius": 6, "hover": { "background": "#232323" }, @@ -1342,14 +1364,21 @@ "color": "#9c9c9c", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#1c1c1c", - "corner_radius": 6, "hover": { "background": "#232323" }, diff --git a/assets/themes/light.json b/assets/themes/light.json index 69bca672f74a622158078132009914064df75f7b..6a9624b5874afca5c06065f1ae5bf27e3a770223 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -989,9 +989,7 @@ "chat_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "channel_name": { "family": "Zed Sans", @@ -1208,9 +1206,7 @@ "contacts_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "user_query_editor": { "background": "#ffffff", @@ -1238,23 +1234,33 @@ "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_height": 28, "header_row": { "family": "Zed Mono", "color": "#474747", "size": 14, "margin": { - "top": 8 + "top": 14 + }, + "padding": { + "left": 12, + "right": 12 }, "active": { "family": "Zed Mono", @@ -1265,15 +1271,23 @@ }, "contact_row": { "padding": { - "left": 8 + "left": 12, + "right": 12 }, "active": { "background": "#d5d5d5" } }, - "row_height": 28, - "tree_branch_color": "#e3e3e3", - "tree_branch_width": 1, + "tree_branch": { + "color": "#b8b8b8", + "width": 1, + "hover": { + "color": "#b8b8b8" + }, + "active": { + "color": "#b8b8b8" + } + }, "contact_avatar": { "corner_radius": 10, "width": 18 @@ -1282,10 +1296,11 @@ "family": "Zed Mono", "color": "#2b2b2b", "size": 14, - "padding": { + "margin": { "left": 8 } }, + "contact_button_spacing": 8, "contact_button": { "background": "#eaeaea", "color": "#393939", @@ -1315,14 +1330,21 @@ "color": "#474747", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#f8f8f8", - "corner_radius": 6, "hover": { "background": "#eaeaea" }, @@ -1342,14 +1364,21 @@ "color": "#474747", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#f8f8f8", - "corner_radius": 6, "hover": { "background": "#eaeaea" }, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 75e0eb9b7b460fecc6f02aa3969badfa978d0983..d360fea75c3b4ca519d1ec4293b417c2d960f78a 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -989,9 +989,7 @@ "chat_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "channel_name": { "family": "Zed Sans", @@ -1208,9 +1206,7 @@ "contacts_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "user_query_editor": { "background": "#002b36", @@ -1238,23 +1234,33 @@ "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_height": 28, "header_row": { "family": "Zed Mono", "color": "#93a1a1", "size": 14, "margin": { - "top": 8 + "top": 14 + }, + "padding": { + "left": 12, + "right": 12 }, "active": { "family": "Zed Mono", @@ -1265,15 +1271,23 @@ }, "contact_row": { "padding": { - "left": 8 + "left": 12, + "right": 12 }, "active": { "background": "#586e755c" } }, - "row_height": 28, - "tree_branch_color": "#657b83", - "tree_branch_width": 1, + "tree_branch": { + "color": "#657b83", + "width": 1, + "hover": { + "color": "#657b83" + }, + "active": { + "color": "#657b83" + } + }, "contact_avatar": { "corner_radius": 10, "width": 18 @@ -1282,10 +1296,11 @@ "family": "Zed Mono", "color": "#eee8d5", "size": 14, - "padding": { + "margin": { "left": 8 } }, + "contact_button_spacing": 8, "contact_button": { "background": "#073642", "color": "#eee8d5", @@ -1315,14 +1330,21 @@ "color": "#93a1a1", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#073642", - "corner_radius": 6, "hover": { "background": "#586e753d" }, @@ -1342,14 +1364,21 @@ "color": "#93a1a1", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#073642", - "corner_radius": 6, "hover": { "background": "#586e753d" }, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index e88872bdfb02b91a3cde810a4f8afe6a267bc7ad..0f93014911bc3e3c55fe6070215fbff158155ae1 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -989,9 +989,7 @@ "chat_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "channel_name": { "family": "Zed Sans", @@ -1208,9 +1206,7 @@ "contacts_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "user_query_editor": { "background": "#fdf6e3", @@ -1238,23 +1234,33 @@ "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_height": 28, "header_row": { "family": "Zed Mono", "color": "#586e75", "size": 14, "margin": { - "top": 8 + "top": 14 + }, + "padding": { + "left": 12, + "right": 12 }, "active": { "family": "Zed Mono", @@ -1265,15 +1271,23 @@ }, "contact_row": { "padding": { - "left": 8 + "left": 12, + "right": 12 }, "active": { "background": "#93a1a12e" } }, - "row_height": 28, - "tree_branch_color": "#839496", - "tree_branch_width": 1, + "tree_branch": { + "color": "#839496", + "width": 1, + "hover": { + "color": "#839496" + }, + "active": { + "color": "#839496" + } + }, "contact_avatar": { "corner_radius": 10, "width": 18 @@ -1282,10 +1296,11 @@ "family": "Zed Mono", "color": "#073642", "size": 14, - "padding": { + "margin": { "left": 8 } }, + "contact_button_spacing": 8, "contact_button": { "background": "#eee8d5", "color": "#073642", @@ -1315,14 +1330,21 @@ "color": "#586e75", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#eee8d5", - "corner_radius": 6, "hover": { "background": "#93a1a11f" }, @@ -1342,14 +1364,21 @@ "color": "#586e75", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#eee8d5", - "corner_radius": 6, "hover": { "background": "#93a1a11f" }, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index ef136a0e4404af94996912fa75150c94c07c59d8..ab316085b20aeedf5dd02cdd5bdce9f640d7d6df 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -989,9 +989,7 @@ "chat_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "channel_name": { "family": "Zed Sans", @@ -1208,9 +1206,7 @@ "contacts_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "user_query_editor": { "background": "#202746", @@ -1238,23 +1234,33 @@ "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_height": 28, "header_row": { "family": "Zed Mono", "color": "#979db4", "size": 14, "margin": { - "top": 8 + "top": 14 + }, + "padding": { + "left": 12, + "right": 12 }, "active": { "family": "Zed Mono", @@ -1265,15 +1271,23 @@ }, "contact_row": { "padding": { - "left": 8 + "left": 12, + "right": 12 }, "active": { "background": "#5e66875c" } }, - "row_height": 28, - "tree_branch_color": "#6b7394", - "tree_branch_width": 1, + "tree_branch": { + "color": "#6b7394", + "width": 1, + "hover": { + "color": "#6b7394" + }, + "active": { + "color": "#6b7394" + } + }, "contact_avatar": { "corner_radius": 10, "width": 18 @@ -1282,10 +1296,11 @@ "family": "Zed Mono", "color": "#dfe2f1", "size": 14, - "padding": { + "margin": { "left": 8 } }, + "contact_button_spacing": 8, "contact_button": { "background": "#293256", "color": "#dfe2f1", @@ -1315,14 +1330,21 @@ "color": "#979db4", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#293256", - "corner_radius": 6, "hover": { "background": "#5e66873d" }, @@ -1342,14 +1364,21 @@ "color": "#979db4", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#293256", - "corner_radius": 6, "hover": { "background": "#5e66873d" }, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 998a540f1b4eb9b57ee602bd744fca8f8b41560e..56a2468b775a26ee9f41b258f8c9765466c3dad6 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -989,9 +989,7 @@ "chat_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "channel_name": { "family": "Zed Sans", @@ -1208,9 +1206,7 @@ "contacts_panel": { "padding": { "top": 12, - "left": 12, - "bottom": 12, - "right": 12 + "bottom": 12 }, "user_query_editor": { "background": "#f5f7ff", @@ -1238,23 +1234,33 @@ "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_height": 28, "header_row": { "family": "Zed Mono", "color": "#5e6687", "size": 14, "margin": { - "top": 8 + "top": 14 + }, + "padding": { + "left": 12, + "right": 12 }, "active": { "family": "Zed Mono", @@ -1265,15 +1271,23 @@ }, "contact_row": { "padding": { - "left": 8 + "left": 12, + "right": 12 }, "active": { "background": "#979db42e" } }, - "row_height": 28, - "tree_branch_color": "#898ea4", - "tree_branch_width": 1, + "tree_branch": { + "color": "#898ea4", + "width": 1, + "hover": { + "color": "#898ea4" + }, + "active": { + "color": "#898ea4" + } + }, "contact_avatar": { "corner_radius": 10, "width": 18 @@ -1282,10 +1296,11 @@ "family": "Zed Mono", "color": "#293256", "size": 14, - "padding": { + "margin": { "left": 8 } }, + "contact_button_spacing": 8, "contact_button": { "background": "#dfe2f1", "color": "#293256", @@ -1315,14 +1330,21 @@ "color": "#5e6687", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#dfe2f1", - "corner_radius": 6, "hover": { "background": "#979db41f" }, @@ -1342,14 +1364,21 @@ "color": "#5e6687", "size": 14, "margin": { + "left": 8, "right": 6 } }, + "guests": { + "margin": { + "left": 8, + "right": 8 + } + }, "padding": { - "left": 8 + "left": 12, + "right": 12 }, "background": "#dfe2f1", - "corner_radius": 6, "hover": { "background": "#979db41f" }, diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 4cc8091a77ebee79016cc8b68ac76a9b6de36a10..9c8e4b1d12fafdcf4aad8be45ed6ed2202ac7b63 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -186,6 +186,7 @@ impl ContactsPanel { .with_style(theme.contact_username.container) .aligned() .left() + .flex(1., true) .boxed(), ) .constrained() @@ -211,6 +212,13 @@ impl ContactsPanel { ) -> ElementBox { 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 host_avatar_height = theme @@ -219,120 +227,103 @@ impl ContactsPanel { .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) + (row.height - line_height) / 2.; - let tree_branch_width = theme.tree_branch_width; - let tree_branch_color = theme.tree_branch_color; + row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - 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({ - 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 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); - MouseEventHandler::new::( - project_id as usize, - cx, - |mouse_state, _| { - let style = if project.is_shared { - &theme.shared_project_row - } else { - &theme.unshared_project_row - } - .style_for(mouse_state, is_selected); - Flex::row() - .with_child( - 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() - }) - })) + 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(row.guest_avatar) + .aligned() + .left() .contained() - .with_style(style.container) - .constrained() - .with_height(style.height) + .with_margin_right(row.guest_avatar_spacing) .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) + }) + })) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(row.container) .boxed() - }) - .constrained() - .with_height(row.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(), + }); + } + }) + .boxed() } fn render_contact_request( @@ -364,11 +355,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([ @@ -380,7 +373,7 @@ impl ContactsPanel { }; render_icon_button(button_style, "icons/reject.svg") .aligned() - .flex_float() + // .flex_float() .boxed() }) .with_cursor_style(CursorStyle::PointingHand) @@ -390,7 +383,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 { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c55fe0ae5f220e51975dab4e96cb0af8203e131d..92a8b2dba78fd1bc4dd8fae8e30d25cde423c4d1 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -246,9 +246,15 @@ pub struct ContactsPanel { 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 tree_branch: Interactive, +} + +#[derive(Deserialize, Default, Clone, Copy)] +pub struct TreeBranch { + pub width: f32, + pub color: Color, } #[derive(Deserialize, Default)] @@ -273,8 +279,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/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 0da6ada222d77ba1ec2cdbe7b1446ef76535cf05..8e5a12d9b8615b73dacf0999344c9877691e1b09 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -12,7 +12,7 @@ import workspace from "./workspace"; import projectDiagnostics from "./projectDiagnostics"; 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 69c62b5c024355a30e1483156d76f3fbe3a5dec8..5135e8ba45f04cb534f7747cff287b8c94fd1a73 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,11 +16,19 @@ export default function contactsPanel(theme: Theme) { name: { ...text(theme, "mono", "placeholder", { size: "sm" }), margin: { + left: nameMargin, right: 6, }, }, + guests: { + margin: { + left: nameMargin, + right: nameMargin, + } + }, padding: { - left: 8, + left: sidePadding, + right: sidePadding, }, }; @@ -44,41 +55,61 @@ 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, }, + rowHeight: 28, 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: 8 }, + padding: { + left: sidePadding, + right: sidePadding + }, active: { background: backgroundColor(theme, 100, "active"), } }, - rowHeight: 28, - treeBranchColor: borderColor(theme, "muted"), - treeBranchWidth: 1, + treeBranch: { + color: borderColor(theme, "active"), + width: 1, + hover: { + color: borderColor(theme, "active"), + }, + active: { + color: borderColor(theme, "active"), + } + }, contactAvatar: { cornerRadius: 10, width: 18, }, contactUsername: { ...text(theme, "mono", "primary", { size: "sm" }), - padding: { - left: 8, + margin: { + left: nameMargin, }, }, + contactButtonSpacing: nameMargin, contactButton: { ...contactButton, hover: { @@ -91,11 +122,10 @@ export default function contactsPanel(theme: Theme) { color: iconColor(theme, "muted"), }, sharedProjectRow: { - ...project, + ...projectRow, background: backgroundColor(theme, 300), - cornerRadius: 6, name: { - ...project.name, + ...projectRow.name, ...text(theme, "mono", "secondary", { size: "sm" }), }, hover: { @@ -106,11 +136,10 @@ export default function contactsPanel(theme: Theme) { } }, unsharedProjectRow: { - ...project, + ...projectRow, background: backgroundColor(theme, 300), - cornerRadius: 6, name: { - ...project.name, + ...projectRow.name, ...text(theme, "mono", "secondary", { size: "sm" }), }, hover: { From 72e7079005a9583ec5d921427aeb4e383adb9d0a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 May 2022 17:28:35 -0700 Subject: [PATCH 6/9] Add the ability to expand and collapse sections of the contacts panel Also, allow joining projects using the keyboard. --- assets/themes/cave-dark.json | 1 + assets/themes/cave-light.json | 1 + assets/themes/dark.json | 1 + assets/themes/light.json | 1 + assets/themes/solarized-dark.json | 1 + assets/themes/solarized-light.json | 1 + assets/themes/sulphurpool-dark.json | 1 + assets/themes/sulphurpool-light.json | 1 + crates/contacts_panel/src/contacts_panel.rs | 220 ++++++++++++++------ crates/theme/src/theme.rs | 1 + styles/src/styleTree/contactsPanel.ts | 1 + 11 files changed, 167 insertions(+), 63 deletions(-) diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index a944d171fa607303228f857983e7f7bba3c412be..fa837acb6a19599da5340b3fe2a65eca3c5d9f75 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1251,6 +1251,7 @@ "icon_width": 8 }, "row_height": 28, + "section_icon_size": 8, "header_row": { "family": "Zed Mono", "color": "#8b8792", diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 47f397a017e853071c475bac4bb443322762ba12..6090e856b66ca47a8d50cc01d701fd4757979fa7 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1251,6 +1251,7 @@ "icon_width": 8 }, "row_height": 28, + "section_icon_size": 8, "header_row": { "family": "Zed Mono", "color": "#585260", diff --git a/assets/themes/dark.json b/assets/themes/dark.json index f9aeb3e3b17b69c55f3c01846495ad0ab7b2c01d..70d58fc640672884362d47e35efb1ddac3dd8e4c 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1251,6 +1251,7 @@ "icon_width": 8 }, "row_height": 28, + "section_icon_size": 8, "header_row": { "family": "Zed Mono", "color": "#9c9c9c", diff --git a/assets/themes/light.json b/assets/themes/light.json index 6a9624b5874afca5c06065f1ae5bf27e3a770223..3a3e5e2628b034f76b5d8d823ef79b3b29c88ca2 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1251,6 +1251,7 @@ "icon_width": 8 }, "row_height": 28, + "section_icon_size": 8, "header_row": { "family": "Zed Mono", "color": "#474747", diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index d360fea75c3b4ca519d1ec4293b417c2d960f78a..008b07ed962f60be703934e7b7816136dc74e951 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1251,6 +1251,7 @@ "icon_width": 8 }, "row_height": 28, + "section_icon_size": 8, "header_row": { "family": "Zed Mono", "color": "#93a1a1", diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 0f93014911bc3e3c55fe6070215fbff158155ae1..60ac66e5ed465b5c11c79f645baedbdeeb24ec35 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1251,6 +1251,7 @@ "icon_width": 8 }, "row_height": 28, + "section_icon_size": 8, "header_row": { "family": "Zed Mono", "color": "#586e75", diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index ab316085b20aeedf5dd02cdd5bdce9f640d7d6df..7add13add7dca34b2cb08056147c4692f7d2f242 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1251,6 +1251,7 @@ "icon_width": 8 }, "row_height": 28, + "section_icon_size": 8, "header_row": { "family": "Zed Mono", "color": "#979db4", diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 56a2468b775a26ee9f41b258f8c9765466c3dad6..169d4a5bfa104e2320b6d3bcc4ddadb485a0f166 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1251,6 +1251,7 @@ "icon_width": 8 }, "row_height": 28, + "section_icon_size": 8, "header_row": { "family": "Zed Mono", "color": "#5e6687", diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 9c8e4b1d12fafdcf4aad8be45ed6ed2202ac7b63..caa2b3414314d64de39dafa41bc85edb50f3b288 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -15,7 +15,7 @@ use serde::Deserialize; use settings::Settings; use std::sync::Arc; use theme::IconButton; -use workspace::menu::{SelectNext, SelectPrev}; +use workspace::menu::{Confirm, SelectNext, SelectPrev}; use workspace::{AppState, JoinProject}; impl_actions!( @@ -23,9 +23,16 @@ impl_actions!( [RequestContact, RemoveContact, RespondToContactRequest] ); +#[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), @@ -38,7 +45,9 @@ pub struct ContactsPanel { list_state: ListState, user_store: ModelHandle, filter_editor: ViewHandle, + collapsed_sections: Vec
, selection: Option, + app_state: Arc, _maintain_contacts: Subscription, } @@ -62,6 +71,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContactsPanel::clear_filter); cx.add_action(ContactsPanel::select_next); cx.add_action(ContactsPanel::select_prev); + cx.add_action(ContactsPanel::confirm); } impl ContactsPanel { @@ -97,18 +107,9 @@ impl ContactsPanel { let is_selected = this.selection == Some(ix); match &this.entries[ix] { - ContactEntry::Header(text) => { - let header_style = - theme.header_row.style_for(&Default::default(), is_selected); - Label::new(text.to_string(), header_style.text.clone()) - .contained() - .aligned() - .left() - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(header_style.container) - .boxed() + ContactEntry::Header(section) => { + let is_collapsed = this.collapsed_sections.contains(§ion); + Self::render_header(*section, theme, is_selected, is_collapsed) } ContactEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), @@ -153,17 +154,64 @@ impl ContactsPanel { } }), selection: None, + collapsed_sections: Default::default(), entries: Default::default(), match_candidates: Default::default(), filter_editor: user_query_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, + ) -> ElementBox { + 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; + 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() + } + fn render_contact( contact: Arc, theme: &theme::ContactsPanel, @@ -507,8 +555,10 @@ impl ContactsPanel { } 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(); @@ -538,22 +588,27 @@ impl ContactsPanel { .iter() .partition::, _>(|mat| contacts[mat.candidate_id].online); - for (matches, name) in [(online_contacts, "Online"), (offline_contacts, "Offline")] { + for (matches, section) in [ + (online_contacts, Section::Online), + (offline_contacts, Section::Offline), + ] { if !matches.is_empty() { - self.entries.push(ContactEntry::Header(name)); - for mat in matches { - let contact = &contacts[mat.candidate_id]; - self.entries.push(ContactEntry::Contact(contact.clone())); - self.entries - .extend(contact.projects.iter().enumerate().filter_map( - |(ix, project)| { - if project.worktree_root_names.is_empty() { - None - } else { - Some(ContactEntry::ContactProject(contact.clone(), ix)) - } - }, - )); + 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)) + } + }, + )); + } } } } @@ -624,6 +679,32 @@ impl ContactsPanel { 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) => { + 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); + } + ContactEntry::ContactProject(contact, project_ix) => { + cx.dispatch_global_action(JoinProject { + project_id: contact.projects[*project_ix].id, + app_state: self.app_state.clone(), + }) + } + _ => {} + } + } + } else { + } + } } fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { @@ -706,9 +787,9 @@ impl View for ContactsPanel { impl PartialEq for ContactEntry { fn eq(&self, other: &Self) -> bool { match self { - ContactEntry::Header(name_1) => { - if let ContactEntry::Header(name_2) = other { - return name_1 == name_2; + ContactEntry::Header(section_1) => { + if let ContactEntry::Header(section_2) = other { + return section_1 == section_2; } } ContactEntry::IncomingRequest(user_1) => { @@ -816,7 +897,7 @@ mod tests { &[ "+", "v Requests", - " incoming user_one <=== selected", + " incoming user_one", " outgoing user_two", "v Online", " user_four", @@ -838,10 +919,10 @@ mod tests { render_to_strings(&panel, cx), &[ "+", - "Online", - " user_four <=== selected", + "v Online", + " user_four", " dir2", - "Offline", + "v Offline", " user_five", ] ); @@ -853,10 +934,10 @@ mod tests { render_to_strings(&panel, cx), &[ "+", - "Online", + "v Online <=== selected", " user_four", - " dir2 <=== selected", - "Offline", + " dir2", + "v Offline", " user_five", ] ); @@ -868,11 +949,11 @@ mod tests { render_to_strings(&panel, cx), &[ "+", - "Online", - " user_four", + "v Online", + " user_four <=== selected", " dir2", - "Offline", - " user_five <=== selected", + "v Offline", + " user_five", ] ); } @@ -881,25 +962,38 @@ mod tests { panel.read_with(cx, |panel, _| { let mut entries = Vec::new(); entries.push("+".to_string()); - entries.extend(panel.entries.iter().map(|entry| match entry { - ContactEntry::Header(name) => { - format!("{}", name) - } - ContactEntry::IncomingRequest(user) => { - format!(" incoming {}", user.github_login) - } - ContactEntry::OutgoingRequest(user) => { - format!(" outgoing {}", user.github_login) - } - ContactEntry::Contact(contact) => { - format!(" {}", contact.user.github_login) - } - ContactEntry::ContactProject(contact, project_ix) => { - format!( - " {}", - contact.projects[*project_ix].worktree_root_names.join(", ") - ) + entries.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 }) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 92a8b2dba78fd1bc4dd8fae8e30d25cde423c4d1..5ec5c259148b618ec958fdb6e77f49eb34bce06e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -249,6 +249,7 @@ pub struct ContactsPanel { pub contact_button_spacing: f32, pub disabled_contact_button: IconButton, pub tree_branch: Interactive, + pub section_icon_size: f32, } #[derive(Deserialize, Default, Clone, Copy)] diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 5135e8ba45f04cb534f7747cff287b8c94fd1a73..a2caafadec633aeda5ecd7fc3334cecb784a2b6a 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -68,6 +68,7 @@ export default function contactsPanel(theme: Theme) { iconWidth: 8, }, rowHeight: 28, + sectionIconSize: 8, headerRow: { ...text(theme, "mono", "secondary", { size: "sm" }), margin: { top: 14 }, From 77b524c83ecc6a6f4d7341a75c97e42fbd9297bd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 12 May 2022 11:48:11 +0200 Subject: [PATCH 7/9] Allow toggling sections in contacts panel by clicking on them --- crates/contacts_panel/src/contacts_panel.rs | 92 ++++++++++++--------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 6fc8cbb7ce66954dfa0124f4c964ef36a2138901..ba2ca12546e376090488ee10c1f73a693a3d30a7 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, @@ -28,6 +28,8 @@ impl_actions!( [RequestContact, RemoveContact, RespondToContactRequest] ); +impl_internal_actions!(contacts_panel, [ToggleExpanded]); + #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] enum Section { Requests, @@ -44,6 +46,9 @@ enum ContactEntry { ContactProject(Arc, usize), } +#[derive(Clone)] +struct ToggleExpanded(Section); + pub struct ContactsPanel { entries: Vec, match_candidates: Vec, @@ -78,6 +83,7 @@ pub fn init(cx: &mut MutableAppContext) { 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 { @@ -140,7 +146,7 @@ impl ContactsPanel { match &this.entries[ix] { ContactEntry::Header(section) => { let is_collapsed = this.collapsed_sections.contains(§ion); - Self::render_header(*section, theme, is_selected, is_collapsed) + Self::render_header(*section, theme, is_selected, is_collapsed, cx) } ContactEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), @@ -203,7 +209,10 @@ impl ContactsPanel { 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", @@ -211,36 +220,41 @@ impl ContactsPanel { Section::Offline => "Offline", }; let icon_size = theme.section_icon_size; - 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()) + 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() - .left() - .contained() - .with_margin_left(theme.contact_username.container.margin.left) - .flex(1., true) + .constrained() + .with_width(icon_size) .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(header_style.container) - .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( @@ -716,13 +730,8 @@ impl ContactsPanel { if let Some(entry) = self.entries.get(selection) { match entry { ContactEntry::Header(section) => { - 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); + let section = *section; + self.toggle_expanded(&ToggleExpanded(section), cx); } ContactEntry::ContactProject(contact, project_ix) => { cx.dispatch_global_action(JoinProject { @@ -733,8 +742,17 @@ impl ContactsPanel { _ => {} } } + } + } + + 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); } } From c7802af88b5cec1a07d57cbe5d53e37228cf6fae Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 12 May 2022 12:11:27 +0200 Subject: [PATCH 8/9] Use `cmd-9` and `cmd-shift-9` to toggle contacts panel focus/visibility --- assets/keymaps/default.json | 14 ++++++++++++++ crates/contacts_panel/src/contacts_panel.rs | 4 ++++ crates/workspace/src/sidebar.rs | 14 +++++++++++--- crates/workspace/src/workspace.rs | 4 ++-- 4 files changed, 31 insertions(+), 5 deletions(-) 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/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index ba2ca12546e376090488ee10c1f73a693a3d30a7..6af6da6f23f3a2a70a4f664772af8fa3b8a0596f 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -764,6 +764,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 { 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(); From 47ed9c76ed4467c035e3d188fb00ae32878de32b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 May 2022 09:56:32 -0700 Subject: [PATCH 9/9] Select the first contact when changing the filter in the contacts panel Co-authored-by: Antonio Scandurra --- crates/contacts_panel/src/contacts_panel.rs | 36 +++++++++++++-------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 6af6da6f23f3a2a70a4f664772af8fa3b8a0596f..03968d5aaaf740552423b4331026a38150d233f1 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -92,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, @@ -101,10 +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.selection.take(); - 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(); @@ -194,7 +203,7 @@ impl ContactsPanel { 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(), @@ -659,10 +668,11 @@ impl ContactsPanel { } } - if let Some(selection) = &mut self.selection { + if let Some(prev_selected_entry) = prev_selected_entry { + self.selection.take(); for (ix, entry) in self.entries.iter().enumerate() { - if Some(entry) == prev_selected_entry.as_ref() { - *selection = ix; + if *entry == prev_selected_entry { + self.selection = Some(ix); break; } } @@ -991,7 +1001,7 @@ mod tests { &[ "+", "v Online", - " user_four", + " user_four <=== selected", " dir2", "v Offline", " user_five", @@ -1005,9 +1015,9 @@ mod tests { render_to_strings(&panel, cx), &[ "+", - "v Online <=== selected", + "v Online", " user_four", - " dir2", + " dir2 <=== selected", "v Offline", " user_five", ] @@ -1021,9 +1031,9 @@ mod tests { &[ "+", "v Online", - " user_four <=== selected", + " user_four", " dir2", - "v Offline", + "v Offline <=== selected", " user_five", ] );