Merge pull request #537 from zed-industries/disconnected-status

Nathan Sobo created

Render overlay after remote project becomes read-only

Change summary

crates/client/src/client.rs               |   6 +
crates/gpui/src/app.rs                    |  43 +++++-----
crates/gpui/src/elements/event_handler.rs |  16 ++++
crates/gpui/src/presenter.rs              |  14 ++-
crates/project/src/project.rs             |  49 ++++++++----
crates/theme/src/theme.rs                 |   1 
crates/workspace/src/workspace.rs         | 100 +++++++++++++++++-------
crates/zed/assets/themes/black.toml       |   5 +
crates/zed/assets/themes/dark.toml        |   5 +
crates/zed/assets/themes/light.toml       |   5 +
10 files changed, 170 insertions(+), 74 deletions(-)

Detailed changes

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<Credentials>,
     status: (watch::Sender<Status>, watch::Receiver<Status>),

crates/gpui/src/app.rs 🔗

@@ -979,14 +979,6 @@ impl MutableAppContext {
             .and_then(|window| window.root_view.clone().downcast::<T>())
     }
 
-    pub fn root_view_id(&self, window_id: usize) -> Option<usize> {
-        self.cx.root_view_id(window_id)
-    }
-
-    pub fn focused_view_id(&self, window_id: usize) -> Option<usize> {
-        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<usize>) {
         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<usize> {
         self.windows
             .get(&window_id)
-            .map(|window| window.focused_view_id)
+            .and_then(|window| window.focused_view_id)
     }
 
     pub fn background(&self) -> &Arc<executor::Background> {
@@ -2052,7 +2046,7 @@ impl ReadView for AppContext {
 
 struct Window {
     root_view: AnyViewHandle,
-    focused_view_id: usize,
+    focused_view_id: Option<usize>,
     invalidation: Option<WindowInvalidation>,
 }
 
@@ -2080,7 +2074,7 @@ pub enum Effect {
     },
     Focus {
         window_id: usize,
-        view_id: usize,
+        view_id: Option<usize>,
     },
     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,
         });
     }
 

crates/gpui/src/elements/event_handler.rs 🔗

@@ -8,6 +8,7 @@ use crate::{
 
 pub struct EventHandler {
     child: ElementBox,
+    capture: Option<Box<dyn FnMut(&Event, RectF, &mut EventContext) -> bool>>,
     mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> 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<F>(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 {

crates/gpui/src/presenter.rs 🔗

@@ -51,13 +51,15 @@ impl Presenter {
     }
 
     pub fn dispatch_path(&self, app: &AppContext) -> Vec<usize> {
-        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
     }
 

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<Option<()>>,
     },
 }
 
@@ -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<Self>| {
             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<Self>) {
+        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<Client>,
         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(())
     }
 

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)]

crates/workspace/src/workspace.rs 🔗

@@ -576,7 +576,13 @@ pub struct Workspace {
 
 impl Workspace {
     pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
-        cx.observe(&params.project, |_, _, cx| cx.notify()).detach();
+        cx.observe(&params.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<ElementBox> {
+        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<Self>) -> 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")
     }
 

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"

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"

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"