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/gpui/src/app.rs b/crates/gpui/src/app.rs index 59be4e7a6d..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, @@ -1376,7 +1368,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 +1536,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 +1544,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 +1747,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 +1759,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 +1772,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 +1952,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 +2046,7 @@ impl ReadView for AppContext { struct Window { root_view: AnyViewHandle, - focused_view_id: usize, + focused_view_id: Option, invalidation: Option, } @@ -2080,7 +2074,7 @@ pub enum Effect { }, Focus { window_id: usize, - view_id: usize, + view_id: Option, }, ResizeWindow { window_id: usize, @@ -2514,14 +2508,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/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/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/project/src/project.rs b/crates/project/src/project.rs index 9c7ffcf199..6dda03c216 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, @@ -2672,20 +2700,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..6633242aed 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(); @@ -1297,6 +1303,28 @@ 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( + 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(), + ) + .capture(|_, _, _| true) + .boxed(), + ) + } else { + None + } + } } impl Entity for Workspace { @@ -1311,39 +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())) - .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") } 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"