Show a "disconnected overlay" when project becomes read-only (#3733)

Antonio Scandurra created

<img width="1136" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/9daaf21a-90d6-4074-9c65-360df5661221">


Release Notes:

- N/A

Change summary

crates/editor2/src/element.rs       | 14 +++
crates/gpui2/src/elements/div.rs    | 40 ++++++++++++
crates/gpui2/src/elements/text.rs   |  7 -
crates/gpui2/src/window.rs          | 13 +++
crates/workspace2/src/pane_group.rs | 12 ++-
crates/workspace2/src/workspace2.rs | 97 ++++++++++++++++++++----------
6 files changed, 138 insertions(+), 45 deletions(-)

Detailed changes

crates/editor2/src/element.rs 🔗

@@ -884,7 +884,11 @@ impl EditorElement {
                 bounds: text_bounds,
             }),
             |cx| {
-                if text_bounds.contains(&cx.mouse_position()) {
+                let interactive_text_bounds = InteractiveBounds {
+                    bounds: text_bounds,
+                    stacking_order: cx.stacking_order().clone(),
+                };
+                if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) {
                     if self
                         .editor
                         .read(cx)
@@ -1361,8 +1365,12 @@ impl EditorElement {
             ));
         }
 
+        let interactive_track_bounds = InteractiveBounds {
+            bounds: track_bounds,
+            stacking_order: cx.stacking_order().clone(),
+        };
         let mut mouse_position = cx.mouse_position();
-        if track_bounds.contains(&mouse_position) {
+        if interactive_track_bounds.visibly_contains(&mouse_position, cx) {
             cx.set_cursor_style(CursorStyle::Arrow);
         }
 
@@ -1392,7 +1400,7 @@ impl EditorElement {
                         cx.stop_propagation();
                     } else {
                         editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
-                        if track_bounds.contains(&event.position) {
+                        if interactive_track_bounds.visibly_contains(&event.position, cx) {
                             editor.scroll_manager.show_scrollbar(cx);
                         }
                     }

crates/gpui2/src/elements/div.rs 🔗

@@ -62,6 +62,18 @@ impl Interactivity {
             }));
     }
 
+    pub fn capture_any_mouse_down(
+        &mut self,
+        listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+    ) {
+        self.mouse_down_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture && bounds.visibly_contains(&event.position, cx) {
+                    (listener)(event, cx)
+                }
+            }));
+    }
+
     pub fn on_any_mouse_down(
         &mut self,
         listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
@@ -90,6 +102,18 @@ impl Interactivity {
             }));
     }
 
+    pub fn capture_any_mouse_up(
+        &mut self,
+        listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
+    ) {
+        self.mouse_up_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture && bounds.visibly_contains(&event.position, cx) {
+                    (listener)(event, cx)
+                }
+            }));
+    }
+
     pub fn on_any_mouse_up(
         &mut self,
         listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
@@ -384,6 +408,14 @@ pub trait InteractiveElement: Sized {
         self
     }
 
+    fn capture_any_mouse_down(
+        mut self,
+        listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.interactivity().capture_any_mouse_down(listener);
+        self
+    }
+
     fn on_any_mouse_down(
         mut self,
         listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
@@ -401,6 +433,14 @@ pub trait InteractiveElement: Sized {
         self
     }
 
+    fn capture_any_mouse_up(
+        mut self,
+        listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.interactivity().capture_any_mouse_up(listener);
+        self
+    }
+
     fn on_mouse_down_out(
         mut self,
         listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,

crates/gpui2/src/elements/text.rs 🔗

@@ -358,14 +358,13 @@ impl Element for InteractiveText {
 
     fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
         if let Some(click_listener) = self.click_listener.take() {
-            if let Some(ix) = state
-                .text_state
-                .index_for_position(bounds, cx.mouse_position())
-            {
+            let mouse_position = cx.mouse_position();
+            if let Some(ix) = state.text_state.index_for_position(bounds, mouse_position) {
                 if self
                     .clickable_ranges
                     .iter()
                     .any(|range| range.contains(&ix))
+                    && cx.was_top_layer(&mouse_position, cx.stacking_order())
                 {
                     cx.set_cursor_style(crate::CursorStyle::PointingHand)
                 }

crates/gpui2/src/window.rs 🔗

@@ -273,6 +273,7 @@ pub struct Window {
     pub(crate) drawing: bool,
     activation_observers: SubscriberSet<(), AnyObserver>,
     pub(crate) focus: Option<FocusId>,
+    focus_enabled: bool,
 
     #[cfg(any(test, feature = "test-support"))]
     pub(crate) focus_invalidated: bool,
@@ -420,6 +421,7 @@ impl Window {
             drawing: false,
             activation_observers: SubscriberSet::new(),
             focus: None,
+            focus_enabled: true,
 
             #[cfg(any(test, feature = "test-support"))]
             focus_invalidated: false,
@@ -496,7 +498,7 @@ impl<'a> WindowContext<'a> {
 
     /// Move focus to the element associated with the given `FocusHandle`.
     pub fn focus(&mut self, handle: &FocusHandle) {
-        if self.window.focus == Some(handle.id) {
+        if !self.window.focus_enabled || self.window.focus == Some(handle.id) {
             return;
         }
 
@@ -516,10 +518,19 @@ impl<'a> WindowContext<'a> {
 
     /// Remove focus from all elements within this context's window.
     pub fn blur(&mut self) {
+        if !self.window.focus_enabled {
+            return;
+        }
+
         self.window.focus = None;
         self.notify();
     }
 
+    pub fn disable_focus(&mut self) {
+        self.blur();
+        self.window.focus_enabled = false;
+    }
+
     pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
         let focus_handle = self.focused();
 

crates/workspace2/src/pane_group.rs 🔗

@@ -564,9 +564,9 @@ mod element {
     use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
 
     use gpui::{
-        px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, IntoElement,
-        MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Size, Style,
-        WindowContext,
+        px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, InteractiveBounds,
+        IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
+        Size, Style, WindowContext,
     };
     use parking_lot::Mutex;
     use smallvec::SmallVec;
@@ -717,7 +717,11 @@ mod element {
             };
 
             cx.with_z_index(3, |cx| {
-                if handle_bounds.contains(&cx.mouse_position()) {
+                let interactive_handle_bounds = InteractiveBounds {
+                    bounds: handle_bounds,
+                    stacking_order: cx.stacking_order().clone(),
+                };
+                if interactive_handle_bounds.visibly_contains(&cx.mouse_position(), cx) {
                     cx.set_cursor_style(match axis {
                         Axis::Vertical => CursorStyle::ResizeUpDown,
                         Axis::Horizontal => CursorStyle::ResizeLeftRight,

crates/workspace2/src/workspace2.rs 🔗

@@ -25,12 +25,13 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
-    AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
-    DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels,
-    InteractiveElement, KeyContext, ManagedView, Model, ModelContext, ParentElement,
-    PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View,
-    ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView,
+    AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow,
+    Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle,
+    FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
+    ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
+    Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -64,6 +65,7 @@ use std::{
 use theme::{ActiveTheme, ThemeSettings};
 pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
+use ui::Label;
 use util::ResultExt;
 use uuid::Uuid;
 pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
@@ -502,7 +504,7 @@ impl Workspace {
 
                 project::Event::DisconnectedFromHost => {
                     this.update_window_edited(cx);
-                    cx.blur();
+                    cx.disable_focus();
                 }
 
                 project::Event::Closed => {
@@ -2519,32 +2521,6 @@ impl Workspace {
         }
     }
 
-    //     fn render_disconnected_overlay(
-    //         &self,
-    //         cx: &mut ViewContext<Workspace>,
-    //     ) -> Option<AnyElement<Workspace>> {
-    //         if self.project.read(cx).is_read_only() {
-    //             enum DisconnectedOverlay {}
-    //             Some(
-    //                 MouseEventHandler::new::<DisconnectedOverlay, _>(0, cx, |_, cx| {
-    //                     let theme = &theme::current(cx);
-    //                     Label::new(
-    //                         "Your connection to the remote project has been lost.",
-    //                         theme.workspace.disconnected_overlay.text.clone(),
-    //                     )
-    //                     .aligned()
-    //                     .contained()
-    //                     .with_style(theme.workspace.disconnected_overlay.container)
-    //                 })
-    //                 .with_cursor_style(CursorStyle::Arrow)
-    //                 .capture_all()
-    //                 .into_any_named("disconnected overlay"),
-    //             )
-    //         } else {
-    //             None
-    //         }
-    //     }
-
     fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
         if self.notifications.is_empty() {
             None
@@ -3661,6 +3637,11 @@ impl Render for Workspace {
                     })),
             )
             .child(self.status_bar.clone())
+            .children(if self.project.read(cx).is_read_only() {
+                Some(DisconnectedOverlay)
+            } else {
+                None
+            })
     }
 }
 
@@ -4284,6 +4265,56 @@ fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
     Some(size((width as f64).into(), (height as f64).into()))
 }
 
+struct DisconnectedOverlay;
+
+impl Element for DisconnectedOverlay {
+    type State = AnyElement;
+
+    fn layout(
+        &mut self,
+        _: Option<Self::State>,
+        cx: &mut WindowContext,
+    ) -> (LayoutId, Self::State) {
+        let mut background = cx.theme().colors().elevated_surface_background;
+        background.fade_out(0.2);
+        let mut overlay = div()
+            .bg(background)
+            .absolute()
+            .left_0()
+            .top_0()
+            .size_full()
+            .flex()
+            .items_center()
+            .justify_center()
+            .capture_any_mouse_down(|_, cx| cx.stop_propagation())
+            .capture_any_mouse_up(|_, cx| cx.stop_propagation())
+            .child(Label::new(
+                "Your connection to the remote project has been lost.",
+            ))
+            .into_any();
+        (overlay.layout(cx), overlay)
+    }
+
+    fn paint(&mut self, bounds: Bounds<Pixels>, overlay: &mut Self::State, cx: &mut WindowContext) {
+        cx.with_z_index(u8::MAX, |cx| {
+            cx.add_opaque_layer(bounds);
+            overlay.paint(cx);
+        })
+    }
+}
+
+impl IntoElement for DisconnectedOverlay {
+    type Element = Self;
+
+    fn element_id(&self) -> Option<ui::prelude::ElementId> {
+        None
+    }
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::{cell::RefCell, rc::Rc};