Merge pull request #2053 from zed-industries/on-move-out

Julia created

Hide hovers when mouse leaves area & window focus is lost

Change summary

crates/editor/src/editor.rs                     | 10 ++--
crates/editor/src/element.rs                    |  9 ++++
crates/editor/src/hover_popover.rs              | 16 ++++++--
crates/editor/src/scroll.rs                     |  8 ++--
crates/gpui/src/elements/mouse_event_handler.rs | 10 +++++
crates/gpui/src/platform/mac/window.rs          | 33 +++++++-----------
crates/gpui/src/presenter.rs                    | 17 ++++++++-
crates/gpui/src/scene/mouse_event.rs            | 13 +++++++
crates/gpui/src/scene/mouse_region.rs           | 27 ++++++++++++++
9 files changed, 105 insertions(+), 38 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -44,7 +44,7 @@ use gpui::{
     ViewContext, ViewHandle, WeakViewHandle,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
-use hover_popover::{hide_hover, HoverState};
+use hover_popover::{hide_hover, HideHover, HoverState};
 pub use items::MAX_TAB_TITLE_LEN;
 use itertools::Itertools;
 pub use language::{char_kind, CharKind};
@@ -1321,7 +1321,7 @@ impl Editor {
                 }
             }
 
-            hide_hover(self, cx);
+            hide_hover(self, &HideHover, cx);
 
             if old_cursor_position.to_display_point(&display_map).row()
                 != new_cursor_position.to_display_point(&display_map).row()
@@ -1696,7 +1696,7 @@ impl Editor {
             return;
         }
 
-        if hide_hover(self, cx) {
+        if hide_hover(self, &HideHover, cx) {
             return;
         }
 
@@ -6176,7 +6176,7 @@ impl View for Editor {
             cx.defer(move |cx| {
                 if let Some(editor) = handle.upgrade(cx) {
                     editor.update(cx, |editor, cx| {
-                        hide_hover(editor, cx);
+                        hide_hover(editor, &HideHover, cx);
                         hide_link_definition(editor, cx);
                     })
                 }
@@ -6225,7 +6225,7 @@ impl View for Editor {
         self.buffer
             .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
         self.hide_context_menu(cx);
-        hide_hover(self, cx);
+        hide_hover(self, &HideHover, cx);
         cx.emit(Event::Blurred);
         cx.notify();
     }

crates/editor/src/element.rs 🔗

@@ -7,7 +7,7 @@ use crate::{
     display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
     git::{diff_hunk_to_display, DisplayDiffHunk},
     hover_popover::{
-        HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
+        HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
     },
     link_go_to_definition::{
         GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
@@ -114,6 +114,7 @@ impl EditorElement {
     fn attach_mouse_handlers(
         view: &WeakViewHandle<Editor>,
         position_map: &Arc<PositionMap>,
+        has_popovers: bool,
         visible_bounds: RectF,
         text_bounds: RectF,
         gutter_bounds: RectF,
@@ -190,6 +191,11 @@ impl EditorElement {
                         }
                     }
                 })
+                .on_move_out(move |_, cx| {
+                    if has_popovers {
+                        cx.dispatch_action(HideHover);
+                    }
+                })
                 .on_scroll({
                     let position_map = position_map.clone();
                     move |e, cx| {
@@ -1870,6 +1876,7 @@ impl Element for EditorElement {
         Self::attach_mouse_handlers(
             &self.view,
             &layout.position_map,
+            layout.hover_popovers.is_some(),
             visible_bounds,
             text_bounds,
             gutter_bounds,

crates/editor/src/hover_popover.rs 🔗

@@ -29,12 +29,16 @@ pub struct HoverAt {
     pub point: Option<DisplayPoint>,
 }
 
+#[derive(Copy, Clone, PartialEq)]
+pub struct HideHover;
+
 actions!(editor, [Hover]);
-impl_internal_actions!(editor, [HoverAt]);
+impl_internal_actions!(editor, [HoverAt, HideHover]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(hover);
     cx.add_action(hover_at);
+    cx.add_action(hide_hover);
 }
 
 /// Bindable action which uses the most recent selection head to trigger a hover
@@ -50,7 +54,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
         if let Some(point) = action.point {
             show_hover(editor, point, false, cx);
         } else {
-            hide_hover(editor, cx);
+            hide_hover(editor, &HideHover, cx);
         }
     }
 }
@@ -58,7 +62,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
 /// Hides the type information popup.
 /// Triggered by the `Hover` action when the cursor is not over a symbol or when the
 /// selections changed.
-pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
+pub fn hide_hover(editor: &mut Editor, _: &HideHover, cx: &mut ViewContext<Editor>) -> bool {
     let did_hide = editor.hover_state.info_popover.take().is_some()
         | editor.hover_state.diagnostic_popover.take().is_some();
 
@@ -67,6 +71,10 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
 
     editor.clear_background_highlights::<HoverState>(cx);
 
+    if did_hide {
+        cx.notify();
+    }
+
     did_hide
 }
 
@@ -121,7 +129,7 @@ fn show_hover(
                 // Hover triggered from same location as last time. Don't show again.
                 return;
             } else {
-                hide_hover(editor, cx);
+                hide_hover(editor, &HideHover, cx);
             }
         }
     }

crates/editor/src/scroll.rs 🔗

@@ -17,7 +17,7 @@ use workspace::WorkspaceId;
 
 use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
-    hover_popover::hide_hover,
+    hover_popover::{hide_hover, HideHover},
     persistence::DB,
     Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
 };
@@ -307,7 +307,7 @@ impl Editor {
     ) {
         let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
 
-        hide_hover(self, cx);
+        hide_hover(self, &HideHover, cx);
         self.scroll_manager.set_scroll_position(
             scroll_position,
             &map,
@@ -323,7 +323,7 @@ impl Editor {
     }
 
     pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
-        hide_hover(self, cx);
+        hide_hover(self, &HideHover, cx);
         let top_row = scroll_anchor
             .top_anchor
             .to_point(&self.buffer().read(cx).snapshot(cx))
@@ -337,7 +337,7 @@ impl Editor {
         scroll_anchor: ScrollAnchor,
         cx: &mut ViewContext<Self>,
     ) {
-        hide_hover(self, cx);
+        hide_hover(self, &HideHover, cx);
         let top_row = scroll_anchor
             .top_anchor
             .to_point(&self.buffer().read(cx).snapshot(cx))

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

@@ -7,7 +7,7 @@ use crate::{
     platform::CursorStyle,
     scene::{
         CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
-        MouseMove, MouseScrollWheel, MouseUp, MouseUpOut,
+        MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
     },
     DebugContext, Element, ElementBox, EventContext, LayoutContext, MeasurementContext,
     MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
@@ -82,6 +82,14 @@ impl<Tag> MouseEventHandler<Tag> {
         self
     }
 
+    pub fn on_move_out(
+        mut self,
+        handler: impl Fn(MouseMoveOut, &mut EventContext) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_move_out(handler);
+        self
+    }
+
     pub fn on_down(
         mut self,
         button: MouseButton,

crates/gpui/src/platform/mac/window.rs 🔗

@@ -66,12 +66,6 @@ const NSNormalWindowLevel: NSInteger = 0;
 #[allow(non_upper_case_globals)]
 const NSPopUpWindowLevel: NSInteger = 101;
 #[allow(non_upper_case_globals)]
-const NSTrackingMouseMoved: NSUInteger = 0x02;
-#[allow(non_upper_case_globals)]
-const NSTrackingActiveAlways: NSUInteger = 0x80;
-#[allow(non_upper_case_globals)]
-const NSTrackingInVisibleRect: NSUInteger = 0x200;
-#[allow(non_upper_case_globals)]
 const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4;
 
 #[repr(C)]
@@ -469,15 +463,7 @@ impl Window {
                 native_window.setTitlebarAppearsTransparent_(YES);
             }
 
-            let tracking_area: id = msg_send![class!(NSTrackingArea), alloc];
-            let _: () = msg_send![
-                tracking_area,
-                initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
-                options: NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect
-                owner: native_view
-                userInfo: nil
-            ];
-            let _: () = msg_send![native_view, addTrackingArea: tracking_area.autorelease()];
+            native_window.setAcceptsMouseMovedEvents_(YES);
 
             native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
             native_view.setWantsBestResolutionOpenGLSurface_(YES);
@@ -873,11 +859,10 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
 
 extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
     let window_state = unsafe { get_window_state(this) };
-
     let mut window_state_borrow = window_state.as_ref().borrow_mut();
 
-    let event =
-        unsafe { Event::from_native(native_event, Some(window_state_borrow.content_size().y())) };
+    let window_height = window_state_borrow.content_size().y();
+    let event = unsafe { Event::from_native(native_event, Some(window_height)) };
 
     if let Some(event) = event {
         if key_equivalent {
@@ -902,6 +887,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
                 function_is_held = event.keystroke.function;
                 Some((event, None))
             }
+
             _ => return NO,
         };
 
@@ -968,9 +954,11 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     let window_state = unsafe { get_window_state(this) };
     let weak_window_state = Rc::downgrade(&window_state);
     let mut window_state_borrow = window_state.as_ref().borrow_mut();
+    let is_active = unsafe { window_state_borrow.native_window.isKeyWindow() == YES };
+
+    let window_height = window_state_borrow.content_size().y();
+    let event = unsafe { Event::from_native(native_event, Some(window_height)) };
 
-    let event =
-        unsafe { Event::from_native(native_event, Some(window_state_borrow.content_size().y())) };
     if let Some(event) = event {
         match &event {
             Event::MouseMoved(
@@ -989,12 +977,16 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     ))
                     .detach();
             }
+
+            Event::MouseMoved(_) if !is_active => return,
+
             Event::MouseUp(MouseButtonEvent {
                 button: MouseButton::Left,
                 ..
             }) => {
                 window_state_borrow.synthetic_drag_counter += 1;
             }
+
             Event::ModifiersChanged(ModifiersChangedEvent { modifiers }) => {
                 // Only raise modifiers changed event when they have actually changed
                 if let Some(Event::ModifiersChanged(ModifiersChangedEvent {
@@ -1008,6 +1000,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
 
                 window_state_borrow.previous_modifiers_changed_event = Some(event.clone());
             }
+
             _ => {}
         }
 

crates/gpui/src/presenter.rs 🔗

@@ -8,7 +8,7 @@ use crate::{
     platform::{CursorStyle, Event},
     scene::{
         CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
-        MouseMove, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
+        MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
     },
     text_layout::TextLayoutCache,
     Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, Appearance,
@@ -245,8 +245,11 @@ impl Presenter {
         //  -> Also updates mouse-related state
         match &event {
             Event::KeyDown(e) => return cx.dispatch_key_down(self.window_id, e),
+
             Event::KeyUp(e) => return cx.dispatch_key_up(self.window_id, e),
+
             Event::ModifiersChanged(e) => return cx.dispatch_modifiers_changed(self.window_id, e),
+
             Event::MouseDown(e) => {
                 // Click events are weird because they can be fired after a drag event.
                 // MDN says that browsers handle this by starting from 'the most
@@ -279,6 +282,7 @@ impl Presenter {
                     platform_event: e.clone(),
                 }));
             }
+
             Event::MouseUp(e) => {
                 // NOTE: The order of event pushes is important! MouseUp events MUST be fired
                 // before click events, and so the MouseUp events need to be pushed before
@@ -296,6 +300,7 @@ impl Presenter {
                     platform_event: e.clone(),
                 }));
             }
+
             Event::MouseMoved(
                 e @ MouseMovedEvent {
                     position,
@@ -347,9 +352,13 @@ impl Presenter {
                     platform_event: e.clone(),
                     started: false,
                 }));
+                mouse_events.push(MouseEvent::MoveOut(MouseMoveOut {
+                    region: Default::default(),
+                }));
 
                 self.last_mouse_moved_event = Some(event.clone());
             }
+
             Event::ScrollWheel(e) => mouse_events.push(MouseEvent::ScrollWheel(MouseScrollWheel {
                 region: Default::default(),
                 platform_event: e.clone(),
@@ -407,6 +416,7 @@ impl Presenter {
                         }
                     }
                 }
+
                 MouseEvent::Down(_) | MouseEvent::Up(_) => {
                     for (region, _) in self.mouse_regions.iter().rev() {
                         if region.bounds.contains_point(self.mouse_position) {
@@ -417,6 +427,7 @@ impl Presenter {
                         }
                     }
                 }
+
                 MouseEvent::Click(e) => {
                     // Only raise click events if the released button is the same as the one stored
                     if self
@@ -439,6 +450,7 @@ impl Presenter {
                         }
                     }
                 }
+
                 MouseEvent::Drag(_) => {
                     for (mouse_region, _) in self.mouse_regions.iter().rev() {
                         if self.clicked_region_ids.contains(&mouse_region.id()) {
@@ -447,7 +459,7 @@ impl Presenter {
                     }
                 }
 
-                MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => {
+                MouseEvent::MoveOut(_) | MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => {
                     for (mouse_region, _) in self.mouse_regions.iter().rev() {
                         // NOT contains
                         if !mouse_region.bounds.contains_point(self.mouse_position) {
@@ -455,6 +467,7 @@ impl Presenter {
                         }
                     }
                 }
+
                 _ => {
                     for (mouse_region, _) in self.mouse_regions.iter().rev() {
                         // Contains

crates/gpui/src/scene/mouse_event.rs 🔗

@@ -21,6 +21,11 @@ impl Deref for MouseMove {
     }
 }
 
+#[derive(Debug, Default, Clone)]
+pub struct MouseMoveOut {
+    pub region: RectF,
+}
+
 #[derive(Debug, Default, Clone)]
 pub struct MouseDrag {
     pub region: RectF,
@@ -138,6 +143,7 @@ impl Deref for MouseScrollWheel {
 #[derive(Debug, Clone)]
 pub enum MouseEvent {
     Move(MouseMove),
+    MoveOut(MouseMoveOut),
     Drag(MouseDrag),
     Hover(MouseHover),
     Down(MouseDown),
@@ -152,6 +158,7 @@ impl MouseEvent {
     pub fn set_region(&mut self, region: RectF) {
         match self {
             MouseEvent::Move(r) => r.region = region,
+            MouseEvent::MoveOut(r) => r.region = region,
             MouseEvent::Drag(r) => r.region = region,
             MouseEvent::Hover(r) => r.region = region,
             MouseEvent::Down(r) => r.region = region,
@@ -168,6 +175,7 @@ impl MouseEvent {
     pub fn is_capturable(&self) -> bool {
         match self {
             MouseEvent::Move(_) => true,
+            MouseEvent::MoveOut(_) => false,
             MouseEvent::Drag(_) => true,
             MouseEvent::Hover(_) => false,
             MouseEvent::Down(_) => true,
@@ -185,6 +193,10 @@ impl MouseEvent {
         discriminant(&MouseEvent::Move(Default::default()))
     }
 
+    pub fn move_out_disc() -> Discriminant<MouseEvent> {
+        discriminant(&MouseEvent::MoveOut(Default::default()))
+    }
+
     pub fn drag_disc() -> Discriminant<MouseEvent> {
         discriminant(&MouseEvent::Drag(Default::default()))
     }
@@ -220,6 +232,7 @@ impl MouseEvent {
     pub fn handler_key(&self) -> HandlerKey {
         match self {
             MouseEvent::Move(_) => HandlerKey::new(Self::move_disc(), None),
+            MouseEvent::MoveOut(_) => HandlerKey::new(Self::move_out_disc(), None),
             MouseEvent::Drag(e) => HandlerKey::new(Self::drag_disc(), e.pressed_button),
             MouseEvent::Hover(_) => HandlerKey::new(Self::hover_disc(), None),
             MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),

crates/gpui/src/scene/mouse_region.rs 🔗

@@ -12,7 +12,7 @@ use super::{
         MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp,
         MouseUpOut,
     },
-    MouseScrollWheel,
+    MouseMoveOut, MouseScrollWheel,
 };
 
 #[derive(Clone)]
@@ -124,6 +124,14 @@ impl MouseRegion {
         self
     }
 
+    pub fn on_move_out(
+        mut self,
+        handler: impl Fn(MouseMoveOut, &mut EventContext) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_move_out(handler);
+        self
+    }
+
     pub fn on_scroll(
         mut self,
         handler: impl Fn(MouseScrollWheel, &mut EventContext) + 'static,
@@ -289,6 +297,23 @@ impl HandlerSet {
         self
     }
 
+    pub fn on_move_out(
+        mut self,
+        handler: impl Fn(MouseMoveOut, &mut EventContext) + 'static,
+    ) -> Self {
+        self.insert(MouseEvent::move_out_disc(), None,
+            Rc::new(move |region_event, cx| {
+                if let MouseEvent::MoveOut(e) = region_event {
+                    handler(e, cx);
+                } else {
+                    panic!(
+                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::MoveOut, found {:?}", 
+                        region_event);
+                }
+            }));
+        self
+    }
+
     pub fn on_down(
         mut self,
         button: MouseButton,