From 1c3c17c608df7f05ba27a95f4a5ceb5e67f935d7 Mon Sep 17 00:00:00 2001 From: Markus Sauermann <6299227+Sauermann@users.noreply.github.com> Date: Fri, 6 Jan 2023 21:48:20 +0100 Subject: [PATCH] Refactor mouse_entered and mouse_exited notifications The previous implementation for signals mouse_entered and mouse_exited had shortcomings that relate to focused windows and pressed mouse buttons. For example a Control can be hovered by mouse, even if it is occluded by an embedded window. This patch changes the behavior, so that Control and Viewport send their mouse-enter/exit-notifications based solely on mouse position, visible area, and input restrictions and not on which window has focus or which mouse buttons are pressed. This implicitly also changes when the mouse_entered and mouse_exited signals are sent. This functionality can not be implemented as a part of Viewport::_gui_input_event, because of its interplay with Windows and because Viewport::_gui_input_event is based on input and not on visibility. --- doc/classes/Control.xml | 16 ++- doc/classes/Node.xml | 4 +- doc/classes/Window.xml | 4 +- scene/gui/subviewport_container.cpp | 8 -- scene/main/viewport.cpp | 201 +++++++++++++++++++++++----- scene/main/viewport.h | 13 +- scene/main/window.cpp | 22 ++- scene/main/window.h | 1 + 8 files changed, 213 insertions(+), 56 deletions(-) diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml index 54c38f2db9f9..ee790b696860 100644 --- a/doc/classes/Control.xml +++ b/doc/classes/Control.xml @@ -1094,15 +1094,15 @@ - Emitted when the mouse enters the control's [code]Rect[/code] area, provided its [member mouse_filter] lets the event reach it. - [b]Note:[/b] [signal mouse_entered] will not be emitted if the mouse enters a child [Control] node before entering the parent's [code]Rect[/code] area, at least until the mouse is moved to reach the parent's [code]Rect[/code] area. + Emitted when the mouse cursor enters the control's visible area, that is not occluded behind other Controls or Windows, provided its [member mouse_filter] lets the event reach it and regardless if it's currently focused or not. + [b]Note:[/b] [member CanvasItem.z_index] doesn't affect, which Control receives the signal. - Emitted when the mouse leaves the control's [code]Rect[/code] area, provided its [member mouse_filter] lets the event reach it. - [b]Note:[/b] [signal mouse_exited] will be emitted if the mouse enters a child [Control] node, even if the mouse cursor is still inside the parent's [code]Rect[/code] area. - If you want to check whether the mouse truly left the area, ignoring any top nodes, you can use code like this: + Emitted when the mouse cursor leaves the control's visible area, that is not occluded behind other Controls or Windows, provided its [member mouse_filter] lets the event reach it and regardless if it's currently focused or not. + [b]Note:[/b] [member CanvasItem.z_index] doesn't affect, which Control receives the signal. + [b]Note:[/b] If you want to check whether the mouse truly left the area, ignoring any top nodes, you can use code like this: [codeblock] func _on_mouse_exited(): if not Rect2(Vector2(), size).has_point(get_local_mouse_position()): @@ -1140,10 +1140,12 @@ Sent when the node changes size. Use [member size] to get the new size. - Sent when the mouse pointer enters the node. + Sent when the mouse cursor enters the control's visible area, that is not occluded behind other Controls or Windows, provided its [member mouse_filter] lets the event reach it and regardless if it's currently focused or not. + [b]Note:[/b] [member CanvasItem.z_index] doesn't affect, which Control receives the notification. - Sent when the mouse pointer exits the node. + Sent when the mouse cursor leaves the control's visible area, that is not occluded behind other Controls or Windows, provided its [member mouse_filter] lets the event reach it and regardless if it's currently focused or not. + [b]Note:[/b] [member CanvasItem.z_index] doesn't affect, which Control receives the notification. Sent when the node grabs focus. diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml index ce02c3e51aae..49ab3918bb09 100644 --- a/doc/classes/Node.xml +++ b/doc/classes/Node.xml @@ -1052,10 +1052,10 @@ Notification received from the OS when the screen's DPI has been changed. Only implemented on macOS. - Notification received when the mouse enters the viewport. + Notification received when the mouse cursor enters the [Viewport]'s visible area, that is not occluded behind other [Control]s or [Window]s, provided its [member Viewport.gui_disable_input] is [code]false[/code] and regardless if it's currently focused or not. - Notification received when the mouse leaves the viewport. + Notification received when the mouse cursor leaves the [Viewport]'s visible area, that is not occluded behind other [Control]s or [Window]s, provided its [member Viewport.gui_disable_input] is [code]false[/code] and regardless if it's currently focused or not. Notification received from the OS when the application is exceeding its allocated memory. diff --git a/doc/classes/Window.xml b/doc/classes/Window.xml index 4114a83584bf..33a64be50c52 100644 --- a/doc/classes/Window.xml +++ b/doc/classes/Window.xml @@ -719,12 +719,12 @@ - Emitted when the mouse cursor enters the [Window]'s area, regardless if it's currently focused or not. + Emitted when the mouse cursor enters the [Window]'s visible area, that is not occluded behind other [Control]s or windows, provided its [member Viewport.gui_disable_input] is [code]false[/code] and regardless if it's currently focused or not. - Emitted when the mouse cursor exits the [Window]'s area (including when it's hovered over another window on top of this one). + Emitted when the mouse cursor leaves the [Window]'s visible area, that is not occluded behind other [Control]s or windows, provided its [member Viewport.gui_disable_input] is [code]false[/code] and regardless if it's currently focused or not. diff --git a/scene/gui/subviewport_container.cpp b/scene/gui/subviewport_container.cpp index 105c35383b32..851a94b32f2f 100644 --- a/scene/gui/subviewport_container.cpp +++ b/scene/gui/subviewport_container.cpp @@ -147,14 +147,6 @@ void SubViewportContainer::_notification(int p_what) { } } break; - case NOTIFICATION_MOUSE_ENTER: { - _notify_viewports(NOTIFICATION_VP_MOUSE_ENTER); - } break; - - case NOTIFICATION_MOUSE_EXIT: { - _notify_viewports(NOTIFICATION_VP_MOUSE_EXIT); - } break; - case NOTIFICATION_FOCUS_ENTER: { // If focused, send InputEvent to the SubViewport before the Gui-Input stage. set_process_input(true); diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index d9a1cfe96561..94e5c0755226 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -421,7 +421,12 @@ void Viewport::_sub_window_remove(Window *p_window) { ERR_FAIL_NULL(RenderingServer::get_singleton()); - RS::get_singleton()->free(gui.sub_windows[index].canvas_item); + SubWindow sw = gui.sub_windows[index]; + if (gui.subwindow_over == sw.window) { + sw.window->_mouse_leave_viewport(); + gui.subwindow_over = nullptr; + } + RS::get_singleton()->free(sw.canvas_item); gui.sub_windows.remove_at(index); if (gui.sub_windows.size() == 0) { @@ -633,10 +638,8 @@ void Viewport::_notification(int p_what) { case NOTIFICATION_VP_MOUSE_EXIT: { gui.mouse_in_viewport = false; _drop_physics_mouseover(); - _drop_mouse_over(); - _gui_cancel_tooltip(); - // When the mouse exits the viewport, we want to end mouse_over, but - // not mouse_focus, because, for example, we want to continue + // When the mouse exits the viewport, we don't want to end + // mouse_focus, because, for example, we want to continue // dragging a scrollbar even if the mouse has left the viewport. } break; @@ -1885,25 +1888,10 @@ void Viewport::_gui_input_event(Ref p_event) { } Control *over = nullptr; - if (gui.mouse_in_viewport) { - over = gui_find_control(mpos); - } - - if (over != gui.mouse_over) { - if (!gui.mouse_over) { - _drop_physics_mouseover(); - } - _drop_mouse_over(); - _gui_cancel_tooltip(); - - if (over) { - _gui_call_notification(over, Control::NOTIFICATION_MOUSE_ENTER); - gui.mouse_over = over; - } - } - if (gui.mouse_focus) { over = gui.mouse_focus; + } else if (gui.mouse_in_viewport) { + over = gui_find_control(mpos); } DisplayServer::CursorShape ds_cursor_shape = (DisplayServer::CursorShape)Input::get_singleton()->get_default_cursor_shape(); @@ -2382,7 +2370,7 @@ void Viewport::_gui_hide_control(Control *p_control) { gui_release_focus(); } if (gui.mouse_over == p_control) { - gui.mouse_over = nullptr; + _drop_mouse_over(); } if (gui.drag_mouse_over == p_control) { gui.drag_mouse_over = nullptr; @@ -2405,7 +2393,7 @@ void Viewport::_gui_remove_control(Control *p_control) { gui.key_focus = nullptr; } if (gui.mouse_over == p_control) { - gui.mouse_over = nullptr; + _drop_mouse_over(); } if (gui.drag_mouse_over == p_control) { gui.drag_mouse_over = nullptr; @@ -2459,13 +2447,6 @@ void Viewport::_gui_accept_event() { } } -void Viewport::_drop_mouse_over() { - if (gui.mouse_over) { - _gui_call_notification(gui.mouse_over, Control::NOTIFICATION_MOUSE_EXIT); - gui.mouse_over = nullptr; - } -} - void Viewport::_drop_mouse_focus() { Control *c = gui.mouse_focus; BitField mask = gui.mouse_focus_mask; @@ -2949,6 +2930,156 @@ bool Viewport::_sub_windows_forward_input(const Ref &p_event) { return true; } +void Viewport::_update_mouse_over() { + // Update gui.mouse_over and gui.subwindow_over in all Viewports. + // Send necessary mouse_enter/mouse_exit signals and the NOTIFICATION_VP_MOUSE_ENTER/NOTIFICATION_VP_MOUSE_EXIT notifications for every Viewport in the SceneTree. + + if (is_attached_in_viewport()) { + // Execute this function only, when it is processed by a native Window or a SubViewport, that has no SubViewportContainer as parent. + return; + } + + if (get_tree()->get_root()->is_embedding_subwindows() || is_sub_viewport()) { + // Use embedder logic for calculating mouse position. + _update_mouse_over(gui.last_mouse_pos); + } else { + // Native Window: Use DisplayServer logic for calculating mouse position. + Window *receiving_window = get_tree()->get_root()->gui.windowmanager_window_over; + if (!receiving_window) { + return; + } + + Vector2 pos = DisplayServer::get_singleton()->mouse_get_position() - receiving_window->get_position(); + pos = receiving_window->get_final_transform().affine_inverse().xform(pos); + + receiving_window->_update_mouse_over(pos); + } +} + +void Viewport::_update_mouse_over(Vector2 p_pos) { + // Look for embedded windows at mouse position. + if (is_embedding_subwindows()) { + for (int i = gui.sub_windows.size() - 1; i >= 0; i--) { + Window *sw = gui.sub_windows[i].window; + Rect2 swrect = Rect2(sw->get_position(), sw->get_size()); + Rect2 swrect_border = swrect; + + if (!sw->get_flag(Window::FLAG_BORDERLESS)) { + int title_height = sw->get_theme_constant(SNAME("title_height")); + int margin = sw->get_theme_constant(SNAME("resize_margin")); + swrect_border.position.y -= title_height + margin; + swrect_border.size.y += title_height + margin * 2; + swrect_border.position.x -= margin; + swrect_border.size.x += margin * 2; + } + + if (swrect_border.has_point(p_pos)) { + if (gui.mouse_over) { + _drop_mouse_over(); + } else if (!gui.subwindow_over) { + _drop_physics_mouseover(); + } + if (swrect.has_point(p_pos)) { + if (sw != gui.subwindow_over) { + if (gui.subwindow_over) { + gui.subwindow_over->_mouse_leave_viewport(); + } + gui.subwindow_over = sw; + if (!sw->is_input_disabled()) { + sw->notification(NOTIFICATION_VP_MOUSE_ENTER); + } + } + if (!sw->is_input_disabled()) { + sw->_update_mouse_over(sw->get_final_transform().affine_inverse().xform(p_pos - sw->get_position())); + } + } else { + if (gui.subwindow_over) { + gui.subwindow_over->_mouse_leave_viewport(); + gui.subwindow_over = nullptr; + } + } + return; + } + } + + if (gui.subwindow_over) { + // Take care of moving mouse out of any embedded Window. + gui.subwindow_over->_mouse_leave_viewport(); + gui.subwindow_over = nullptr; + } + } + + // Look for Controls at mouse position. + Control *over = gui_find_control(p_pos); + bool notify_embedded_viewports = false; + if (over != gui.mouse_over) { + if (gui.mouse_over) { + _drop_mouse_over(); + } else { + _drop_physics_mouseover(); + } + + gui.mouse_over = over; + if (over) { + over->notification(Control::NOTIFICATION_MOUSE_ENTER); + notify_embedded_viewports = true; + } + } + + if (over) { + SubViewportContainer *c = Object::cast_to(over); + if (!c) { + return; + } + Vector2 pos = c->get_global_transform_with_canvas().affine_inverse().xform(p_pos); + if (c->is_stretch_enabled()) { + pos /= c->get_stretch_shrink(); + } + + for (int i = 0; i < c->get_child_count(); i++) { + SubViewport *v = Object::cast_to(c->get_child(i)); + if (!v || v->is_input_disabled()) { + continue; + } + if (notify_embedded_viewports) { + v->notification(NOTIFICATION_VP_MOUSE_ENTER); + } + v->_update_mouse_over(v->get_final_transform().affine_inverse().xform(pos)); + } + } +} + +void Viewport::_mouse_leave_viewport() { + if (!is_inside_tree() || is_input_disabled()) { + return; + } + if (gui.subwindow_over) { + gui.subwindow_over->_mouse_leave_viewport(); + gui.subwindow_over = nullptr; + } else if (gui.mouse_over) { + _drop_mouse_over(); + } + notification(NOTIFICATION_VP_MOUSE_EXIT); +} + +void Viewport::_drop_mouse_over() { + _gui_cancel_tooltip(); + SubViewportContainer *c = Object::cast_to(gui.mouse_over); + if (c) { + for (int i = 0; i < c->get_child_count(); i++) { + SubViewport *v = Object::cast_to(c->get_child(i)); + if (!v) { + continue; + } + v->_mouse_leave_viewport(); + } + } + if (gui.mouse_over->is_inside_tree()) { + gui.mouse_over->notification(Control::NOTIFICATION_MOUSE_EXIT); + } + gui.mouse_over = nullptr; +} + void Viewport::push_input(const Ref &p_event, bool p_local_coords) { ERR_MAIN_THREAD_GUARD; ERR_FAIL_COND(!is_inside_tree()); @@ -2974,6 +3105,8 @@ void Viewport::push_input(const Ref &p_event, bool p_local_coords) { Ref me = ev; if (me.is_valid()) { gui.last_mouse_pos = me->get_position(); + + _update_mouse_over(); } if (is_embedding_subwindows() && _sub_windows_forward_input(ev)) { @@ -3111,7 +3244,7 @@ void Viewport::set_disable_input(bool p_disable) { } if (p_disable) { _drop_mouse_focus(); - _drop_mouse_over(); + _mouse_leave_viewport(); _gui_cancel_tooltip(); } disable_input = p_disable; @@ -4616,6 +4749,10 @@ bool SubViewport::is_directly_attached_to_screen() const { return Object::cast_to(get_parent()) && get_parent()->get_viewport() && get_parent()->get_viewport()->is_directly_attached_to_screen(); } +bool SubViewport::is_attached_in_viewport() const { + return Object::cast_to(get_parent()); +} + void SubViewport::_notification(int p_what) { ERR_MAIN_THREAD_GUARD; switch (p_what) { diff --git a/scene/main/viewport.h b/scene/main/viewport.h index e56d69a8684c..7cfad421194d 100644 --- a/scene/main/viewport.h +++ b/scene/main/viewport.h @@ -346,8 +346,6 @@ class Viewport : public Node { Ref vrs_texture; struct GUI { - // info used when this is a window - bool forced_mouse_focus = false; //used for menu buttons bool mouse_in_viewport = true; bool key_event_accepted = false; @@ -358,6 +356,8 @@ class Viewport : public Node { BitField mouse_focus_mask; Control *key_focus = nullptr; Control *mouse_over = nullptr; + Window *subwindow_over = nullptr; // mouse_over and subwindow_over are mutually exclusive. At all times at least one of them is nullptr. + Window *windowmanager_window_over = nullptr; // Only used in root Viewport. Control *drag_mouse_over = nullptr; Vector2 drag_mouse_over_pos; Control *tooltip_control = nullptr; @@ -466,6 +466,9 @@ class Viewport : public Node { bool _sub_windows_forward_input(const Ref &p_event); SubWindowResize _sub_window_get_resize_margin(Window *p_subwindow, const Point2 &p_point); + void _update_mouse_over(); + void _update_mouse_over(Vector2 p_pos); + virtual bool _can_consume_input_events() const { return true; } uint64_t event_count = 0; @@ -478,6 +481,8 @@ class Viewport : public Node { Size2i _get_size_2d_override() const; bool _is_size_allocated() const; + void _mouse_leave_viewport(); + void _notification(int p_what); void _process_picking(); static void _bind_methods(); @@ -665,6 +670,8 @@ class Viewport : public Node { virtual Transform2D get_screen_transform_internal(bool p_absolute_position = false) const; virtual Transform2D get_popup_base_transform() const { return Transform2D(); } virtual bool is_directly_attached_to_screen() const { return false; }; + virtual bool is_attached_in_viewport() const { return false; }; + virtual bool is_sub_viewport() const { return false; }; #ifndef _3D_DISABLED bool use_xr = false; @@ -797,6 +804,8 @@ class SubViewport : public Viewport { virtual Transform2D get_screen_transform_internal(bool p_absolute_position = false) const override; virtual Transform2D get_popup_base_transform() const override; virtual bool is_directly_attached_to_screen() const override; + virtual bool is_attached_in_viewport() const override; + virtual bool is_sub_viewport() const override { return true; }; void _validate_property(PropertyInfo &p_property) const; SubViewport(); diff --git a/scene/main/window.cpp b/scene/main/window.cpp index 3ea53da14131..6182530431af 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -677,16 +677,20 @@ void Window::_event_callback(DisplayServer::WindowEvent p_event) { switch (p_event) { case DisplayServer::WINDOW_EVENT_MOUSE_ENTER: { _propagate_window_notification(this, NOTIFICATION_WM_MOUSE_ENTER); - emit_signal(SNAME("mouse_entered")); + Window *root = get_tree()->get_root(); + DEV_ASSERT(!root->gui.windowmanager_window_over); // Entering a window while a window is hovered should never happen. + root->gui.windowmanager_window_over = this; notification(NOTIFICATION_VP_MOUSE_ENTER); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CURSOR_SHAPE)) { DisplayServer::get_singleton()->cursor_set_shape(DisplayServer::CURSOR_ARROW); //restore cursor shape } } break; case DisplayServer::WINDOW_EVENT_MOUSE_EXIT: { - notification(NOTIFICATION_VP_MOUSE_EXIT); + Window *root = get_tree()->get_root(); + DEV_ASSERT(root->gui.windowmanager_window_over); // Exiting a window, while no window is hovered should never happen. + root->gui.windowmanager_window_over->_mouse_leave_viewport(); + root->gui.windowmanager_window_over = nullptr; _propagate_window_notification(this, NOTIFICATION_WM_MOUSE_EXIT); - emit_signal(SNAME("mouse_exited")); } break; case DisplayServer::WINDOW_EVENT_FOCUS_IN: { focused = true; @@ -1283,6 +1287,14 @@ void Window::_notification(int p_what) { RS::get_singleton()->viewport_set_active(get_viewport_rid(), false); } break; + + case NOTIFICATION_VP_MOUSE_ENTER: { + emit_signal(SceneStringNames::get_singleton()->mouse_entered); + } break; + + case NOTIFICATION_VP_MOUSE_EXIT: { + emit_signal(SceneStringNames::get_singleton()->mouse_exited); + } break; } } @@ -2495,6 +2507,10 @@ bool Window::is_directly_attached_to_screen() const { return is_inside_tree(); } +bool Window::is_attached_in_viewport() const { + return get_embedder(); +} + void Window::_bind_methods() { ClassDB::bind_method(D_METHOD("set_title", "title"), &Window::set_title); ClassDB::bind_method(D_METHOD("get_title"), &Window::get_title); diff --git a/scene/main/window.h b/scene/main/window.h index 7a10499d9b7f..24142b8a9116 100644 --- a/scene/main/window.h +++ b/scene/main/window.h @@ -404,6 +404,7 @@ class Window : public Viewport { virtual Transform2D get_screen_transform_internal(bool p_absolute_position = false) const override; virtual Transform2D get_popup_base_transform() const override; virtual bool is_directly_attached_to_screen() const override; + virtual bool is_attached_in_viewport() const override; Rect2i get_parent_rect() const; virtual DisplayServer::WindowID get_window_id() const override;