From b21d91db22cc859bb1803a4d1eb05a12bfce7f01 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 4 Mar 2022 17:28:18 +0100 Subject: [PATCH 1/4] Render overlay after remote project becomes read-only Co-Authored-By: Nathan Sobo --- crates/client/src/client.rs | 6 ++++ crates/project/src/project.rs | 49 +++++++++++++++++++---------- crates/theme/src/theme.rs | 1 + crates/workspace/src/workspace.rs | 19 +++++++++++ crates/zed/assets/themes/black.toml | 5 +++ crates/zed/assets/themes/dark.toml | 5 +++ crates/zed/assets/themes/light.toml | 5 +++ 7 files changed, 73 insertions(+), 17 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index e338b72e74..c40b78987c 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -127,6 +127,12 @@ pub enum Status { ReconnectionError { next_reconnection: Instant }, } +impl Status { + pub fn is_connected(&self) -> bool { + matches!(self, Self::Connected { .. }) + } +} + struct ClientState { credentials: Option, status: (watch::Sender, watch::Receiver), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a89317db90..2ca1a7ce88 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -92,6 +92,7 @@ enum ProjectClientState { sharing_has_stopped: bool, remote_id: u64, replica_id: ReplicaId, + _detect_unshare_task: Task>, }, } @@ -244,7 +245,7 @@ impl Project { let mut status = rpc.status(); while let Some(status) = status.next().await { if let Some(this) = this.upgrade(&cx) { - let remote_id = if let client::Status::Connected { .. } = status { + let remote_id = if status.is_connected() { let response = rpc.request(proto::RegisterProject {}).await?; Some(response.project_id) } else { @@ -333,7 +334,7 @@ impl Project { } let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); - let this = cx.add_model(|cx| { + let this = cx.add_model(|cx: &mut ModelContext| { let mut this = Self { worktrees: Vec::new(), loading_buffers: Default::default(), @@ -346,11 +347,26 @@ impl Project { user_store: user_store.clone(), fs, subscriptions: vec![client.add_model_for_remote_entity(remote_id, cx)], - client, + client: client.clone(), client_state: ProjectClientState::Remote { sharing_has_stopped: false, remote_id, replica_id, + _detect_unshare_task: cx.spawn_weak(move |this, mut cx| { + async move { + let mut status = client.status(); + let is_connected = + status.next().await.map_or(false, |s| s.is_connected()); + // Even if we're initially connected, any future change of the status means we momentarily disconnected. + if !is_connected || status.next().await.is_some() { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.project_unshared(cx)) + } + } + Ok(()) + } + .log_err() + }), }, language_servers_with_diagnostics_running: 0, language_servers: Default::default(), @@ -666,6 +682,18 @@ impl Project { }) } + fn project_unshared(&mut self, cx: &mut ModelContext) { + if let ProjectClientState::Remote { + sharing_has_stopped, + .. + } = &mut self.client_state + { + *sharing_has_stopped = true; + self.collaborators.clear(); + cx.notify(); + } + } + pub fn is_read_only(&self) -> bool { match &self.client_state { ProjectClientState::Local { .. } => false, @@ -2628,20 +2656,7 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| { - if let ProjectClientState::Remote { - sharing_has_stopped, - .. - } = &mut this.client_state - { - *sharing_has_stopped = true; - this.collaborators.clear(); - cx.notify(); - } else { - unreachable!() - } - }); - + this.update(&mut cx, |this, cx| this.project_unshared(cx)); Ok(()) } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1ca2e3a604..c96db5aab2 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -39,6 +39,7 @@ pub struct Workspace { pub right_sidebar: Sidebar, pub status_bar: StatusBar, pub toolbar: Toolbar, + pub disconnected_overlay: ContainedText, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2691516e2f..2188d0b88d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1297,6 +1297,24 @@ impl Workspace { None } } + + fn render_disconnected_overlay(&self, cx: &AppContext) -> Option { + if self.project.read(cx).is_read_only() { + let theme = &self.settings.borrow().theme; + Some( + Label::new( + "Your connection to the remote project has been lost.".to_string(), + theme.workspace.disconnected_overlay.text.clone(), + ) + .aligned() + .contained() + .with_style(theme.workspace.disconnected_overlay.container) + .boxed(), + ) + } else { + None + } + } } impl Entity for Workspace { @@ -1339,6 +1357,7 @@ impl View for Workspace { content.boxed() }) .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed())) + .with_children(self.render_disconnected_overlay(cx)) .flexible(1.0, true) .boxed(), ) diff --git a/crates/zed/assets/themes/black.toml b/crates/zed/assets/themes/black.toml index 769076645f..34de16627e 100644 --- a/crates/zed/assets/themes/black.toml +++ b/crates/zed/assets/themes/black.toml @@ -60,3 +60,8 @@ emphasis = "#4ec9b0" link_uri = { color = "#6a9955", underline = true } link_text = { color = "#cb8f77", italic = true } list_marker = "#4e94ce" + +[workspace.disconnected_overlay] +extends = "$text.base" +color = "#ffffff" +background = "#000000aa" diff --git a/crates/zed/assets/themes/dark.toml b/crates/zed/assets/themes/dark.toml index ed6deed040..fa673ac446 100644 --- a/crates/zed/assets/themes/dark.toml +++ b/crates/zed/assets/themes/dark.toml @@ -60,3 +60,8 @@ emphasis = "#4ec9b0" link_uri = { color = "#6a9955", underline = true } link_text = { color = "#cb8f77", italic = true } list_marker = "#4e94ce" + +[workspace.disconnected_overlay] +extends = "$text.base" +color = "#ffffff" +background = "#000000aa" diff --git a/crates/zed/assets/themes/light.toml b/crates/zed/assets/themes/light.toml index f51b3f4656..2884515e09 100644 --- a/crates/zed/assets/themes/light.toml +++ b/crates/zed/assets/themes/light.toml @@ -60,3 +60,8 @@ emphasis = "#267f29" link_uri = { color = "#6a9955", underline = true } link_text = { color = "#a82121", italic = true } list_marker = "#4e94ce" + +[workspace.disconnected_overlay] +extends = "$text.base" +color = "#ffffff" +background = "#000000cc" From 77e913b5a43e70ba44bc8302801645001e5ca49b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 4 Mar 2022 17:28:53 +0100 Subject: [PATCH 2/4] Blur focused view when project becomes read-only Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 35 +++++++++++++++++++------------ crates/gpui/src/presenter.rs | 14 +++++++------ crates/workspace/src/workspace.rs | 8 ++++++- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 59be4e7a6d..adb3a2153a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1376,7 +1376,7 @@ impl MutableAppContext { window_id, Window { root_view: root_view.clone().into(), - focused_view_id: root_view.id(), + focused_view_id: Some(root_view.id()), invalidation: None, }, ); @@ -1544,7 +1544,7 @@ impl MutableAppContext { .get_or_insert_with(Default::default) .removed .push(view_id); - if window.focused_view_id == view_id { + if window.focused_view_id == Some(view_id) { Some(window.root_view.id()) } else { None @@ -1552,7 +1552,7 @@ impl MutableAppContext { }); if let Some(view_id) = change_focus_to { - self.focus(window_id, view_id); + self.focus(window_id, Some(view_id)); } self.pending_effects @@ -1755,7 +1755,7 @@ impl MutableAppContext { } } - fn focus(&mut self, window_id: usize, focused_id: usize) { + fn focus(&mut self, window_id: usize, focused_id: Option) { if self .cx .windows @@ -1767,7 +1767,7 @@ impl MutableAppContext { } self.update(|this| { - let blurred_id = this.cx.windows.get_mut(&window_id).map(|window| { + let blurred_id = this.cx.windows.get_mut(&window_id).and_then(|window| { let blurred_id = window.focused_view_id; window.focused_view_id = focused_id; blurred_id @@ -1780,9 +1780,11 @@ impl MutableAppContext { } } - if let Some(mut focused_view) = this.cx.views.remove(&(window_id, focused_id)) { - focused_view.on_focus(this, window_id, focused_id); - this.cx.views.insert((window_id, focused_id), focused_view); + if let Some(focused_id) = focused_id { + if let Some(mut focused_view) = this.cx.views.remove(&(window_id, focused_id)) { + focused_view.on_focus(this, window_id, focused_id); + this.cx.views.insert((window_id, focused_id), focused_view); + } } }) } @@ -1958,7 +1960,7 @@ impl AppContext { pub fn focused_view_id(&self, window_id: usize) -> Option { self.windows .get(&window_id) - .map(|window| window.focused_view_id) + .and_then(|window| window.focused_view_id) } pub fn background(&self) -> &Arc { @@ -2052,7 +2054,7 @@ impl ReadView for AppContext { struct Window { root_view: AnyViewHandle, - focused_view_id: usize, + focused_view_id: Option, invalidation: Option, } @@ -2080,7 +2082,7 @@ pub enum Effect { }, Focus { window_id: usize, - view_id: usize, + view_id: Option, }, ResizeWindow { window_id: usize, @@ -2514,14 +2516,21 @@ impl<'a, T: View> ViewContext<'a, T> { let handle = handle.into(); self.app.pending_effects.push_back(Effect::Focus { window_id: handle.window_id, - view_id: handle.view_id, + view_id: Some(handle.view_id), }); } pub fn focus_self(&mut self) { self.app.pending_effects.push_back(Effect::Focus { window_id: self.window_id, - view_id: self.view_id, + view_id: Some(self.view_id), + }); + } + + pub fn blur(&mut self) { + self.app.pending_effects.push_back(Effect::Focus { + window_id: self.window_id, + view_id: None, }); } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 8a41a76e71..a3899ee44e 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -51,13 +51,15 @@ impl Presenter { } pub fn dispatch_path(&self, app: &AppContext) -> Vec { - let mut view_id = app.focused_view_id(self.window_id).unwrap(); - let mut path = vec![view_id]; - while let Some(parent_id) = self.parents.get(&view_id).copied() { - path.push(parent_id); - view_id = parent_id; + let mut path = Vec::new(); + if let Some(mut view_id) = app.focused_view_id(self.window_id) { + path.push(view_id); + while let Some(parent_id) = self.parents.get(&view_id).copied() { + path.push(parent_id); + view_id = parent_id; + } + path.reverse(); } - path.reverse(); path } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2188d0b88d..b54cd94b92 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -576,7 +576,13 @@ pub struct Workspace { impl Workspace { pub fn new(params: &WorkspaceParams, cx: &mut ViewContext) -> Self { - cx.observe(¶ms.project, |_, _, cx| cx.notify()).detach(); + cx.observe(¶ms.project, |_, project, cx| { + if project.read(cx).is_read_only() { + cx.blur(); + } + cx.notify() + }) + .detach(); let pane = cx.add_view(|_| Pane::new(params.settings.clone())); let pane_id = pane.id(); From 38313abc482c221e4afe8d2a014240fa99326bc3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 4 Mar 2022 17:37:37 +0100 Subject: [PATCH 3/4] Disable events when project becomes read-only Co-Authored-By: Nathan Sobo --- crates/gpui/src/elements/event_handler.rs | 16 +++++ crates/workspace/src/workspace.rs | 87 +++++++++++++---------- 2 files changed, 67 insertions(+), 36 deletions(-) diff --git a/crates/gpui/src/elements/event_handler.rs b/crates/gpui/src/elements/event_handler.rs index f880b0bb6c..0eea82fa02 100644 --- a/crates/gpui/src/elements/event_handler.rs +++ b/crates/gpui/src/elements/event_handler.rs @@ -8,6 +8,7 @@ use crate::{ pub struct EventHandler { child: ElementBox, + capture: Option bool>>, mouse_down: Option bool>>, } @@ -15,6 +16,7 @@ impl EventHandler { pub fn new(child: ElementBox) -> Self { Self { child, + capture: None, mouse_down: None, } } @@ -26,6 +28,14 @@ impl EventHandler { self.mouse_down = Some(Box::new(callback)); self } + + pub fn capture(mut self, callback: F) -> Self + where + F: 'static + FnMut(&Event, RectF, &mut EventContext) -> bool, + { + self.capture = Some(Box::new(callback)); + self + } } impl Element for EventHandler { @@ -59,6 +69,12 @@ impl Element for EventHandler { _: &mut Self::PaintState, cx: &mut EventContext, ) -> bool { + if let Some(capture) = self.capture.as_mut() { + if capture(event, bounds, cx) { + return true; + } + } + if self.child.dispatch_event(event, cx) { true } else { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b54cd94b92..6633242aed 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1308,13 +1308,17 @@ impl Workspace { if self.project.read(cx).is_read_only() { let theme = &self.settings.borrow().theme; Some( - Label::new( - "Your connection to the remote project has been lost.".to_string(), - theme.workspace.disconnected_overlay.text.clone(), + EventHandler::new( + Label::new( + "Your connection to the remote project has been lost.".to_string(), + theme.workspace.disconnected_overlay.text.clone(), + ) + .aligned() + .contained() + .with_style(theme.workspace.disconnected_overlay.container) + .boxed(), ) - .aligned() - .contained() - .with_style(theme.workspace.disconnected_overlay.container) + .capture(|_, _, _| true) .boxed(), ) } else { @@ -1335,40 +1339,51 @@ impl View for Workspace { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); let theme = &settings.theme; - Flex::column() - .with_child(self.render_titlebar(&theme, cx)) + Stack::new() .with_child( - Stack::new() - .with_child({ - let mut content = Flex::row(); - content.add_child(self.left_sidebar.render(&settings, cx)); - if let Some(element) = self.left_sidebar.render_active_item(&settings, cx) { - content.add_child(Flexible::new(0.8, false, element).boxed()); - } - content.add_child( - Flex::column() - .with_child( - Flexible::new(1., true, self.center.render(&settings.theme)) + Flex::column() + .with_child(self.render_titlebar(&theme, cx)) + .with_child( + Stack::new() + .with_child({ + let mut content = Flex::row(); + content.add_child(self.left_sidebar.render(&settings, cx)); + if let Some(element) = + self.left_sidebar.render_active_item(&settings, cx) + { + content.add_child(Flexible::new(0.8, false, element).boxed()); + } + content.add_child( + Flex::column() + .with_child( + Flexible::new( + 1., + true, + self.center.render(&settings.theme), + ) + .boxed(), + ) + .with_child(ChildView::new(&self.status_bar).boxed()) + .flexible(1., true) .boxed(), - ) - .with_child(ChildView::new(&self.status_bar).boxed()) - .flexible(1., true) - .boxed(), - ); - if let Some(element) = self.right_sidebar.render_active_item(&settings, cx) - { - content.add_child(Flexible::new(0.8, false, element).boxed()); - } - content.add_child(self.right_sidebar.render(&settings, cx)); - content.boxed() - }) - .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed())) - .with_children(self.render_disconnected_overlay(cx)) - .flexible(1.0, true) + ); + if let Some(element) = + self.right_sidebar.render_active_item(&settings, cx) + { + content.add_child(Flexible::new(0.8, false, element).boxed()); + } + content.add_child(self.right_sidebar.render(&settings, cx)); + content.boxed() + }) + .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed())) + .flexible(1.0, true) + .boxed(), + ) + .contained() + .with_background_color(settings.theme.workspace.background) .boxed(), ) - .contained() - .with_background_color(settings.theme.workspace.background) + .with_children(self.render_disconnected_overlay(cx)) .named("workspace") } From c38de3243d951b4a03b98aeb417733adfb07490c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 4 Mar 2022 17:54:48 +0100 Subject: [PATCH 4/4] :art: Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index adb3a2153a..a0f2a3a4fd 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -979,14 +979,6 @@ impl MutableAppContext { .and_then(|window| window.root_view.clone().downcast::()) } - pub fn root_view_id(&self, window_id: usize) -> Option { - self.cx.root_view_id(window_id) - } - - pub fn focused_view_id(&self, window_id: usize) -> Option { - self.cx.focused_view_id(window_id) - } - pub fn render_view( &mut self, window_id: usize,