Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make mouse enter/exit notifications match mouse events #84547

Merged
merged 1 commit into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Make mouse-enter/exit notifications match mouse event propagation
`NOTIFICATION_MOUSE_ENTER` and `NOTIFICATION_MOUSE_EXIT` now includes
the areas of children control nodes if the mouse filters allow it.

In order to check if a Control node itself was entered/exited, the newly
introduced `NOTIFICATION_MOUSE_ENTER_SELF` and
`NOTIFICATION_MOUSE_EXIT_SELF` can be used.

Co-authored-by: Markus Sauermann <6299227+Sauermann@users.noreply.github.com>
  • Loading branch information
kitbdev and Sauermann committed Nov 9, 2023
commit d24d73ba3140b540a017bb230e57d9cde0c3d806
23 changes: 18 additions & 5 deletions doc/classes/Control.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1104,13 +1104,13 @@
</signal>
<signal name="mouse_entered">
<description>
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.
Emitted when the mouse cursor enters the control's (or any child 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.
</description>
</signal>
<signal name="mouse_exited">
<description>
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.
Emitted when the mouse cursor leaves the control's (and all child 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]
Expand Down Expand Up @@ -1150,12 +1150,24 @@
Sent when the node changes size. Use [member size] to get the new size.
</constant>
<constant name="NOTIFICATION_MOUSE_ENTER" value="41">
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 cursor enters the control's (or any child 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.
See also [constant NOTIFICATION_MOUSE_ENTER_SELF].
</constant>
<constant name="NOTIFICATION_MOUSE_EXIT" value="42">
Sent when the mouse cursor leaves the control's (and all child 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.
See also [constant NOTIFICATION_MOUSE_EXIT_SELF].
</constant>
<constant name="NOTIFICATION_MOUSE_ENTER_SELF" value="60" is_experimental="true">
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.
See also [constant NOTIFICATION_MOUSE_ENTER].
</constant>
<constant name="NOTIFICATION_MOUSE_EXIT_SELF" value="61" is_experimental="true">
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.
[b]Note:[/b] [member CanvasItem.z_index] doesn't affect which Control receives the notification.
See also [constant NOTIFICATION_MOUSE_EXIT].
</constant>
<constant name="NOTIFICATION_FOCUS_ENTER" value="43">
Sent when the node grabs focus.
Expand Down Expand Up @@ -1320,6 +1332,7 @@
</constant>
<constant name="MOUSE_FILTER_IGNORE" value="2" enum="MouseFilter">
The control will not receive mouse movement input events and mouse button input events if clicked on through [method _gui_input]. The control will also not receive the [signal mouse_entered] nor [signal mouse_exited] signals. This will not block other controls from receiving these events or firing the signals. Ignored events will not be handled automatically.
[b]Note:[/b] If the control has received [signal mouse_entered] but not [signal mouse_exited], changing the [member mouse_filter] to [constant MOUSE_FILTER_IGNORE] will cause [signal mouse_exited] to be emitted.
</constant>
<constant name="GROW_DIRECTION_BEGIN" value="0" enum="GrowDirection">
The control will grow to the left or top to make up if its minimum size is changed to be greater than its current size on the respective axis.
Expand Down
11 changes: 11 additions & 0 deletions scene/gui/control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1831,9 +1831,18 @@ bool Control::has_point(const Point2 &p_point) const {
void Control::set_mouse_filter(MouseFilter p_filter) {
ERR_MAIN_THREAD_GUARD;
ERR_FAIL_INDEX(p_filter, 3);

if (data.mouse_filter == p_filter) {
return;
}

data.mouse_filter = p_filter;
notify_property_list_changed();
update_configuration_warnings();

if (get_viewport()) {
Sauermann marked this conversation as resolved.
Show resolved Hide resolved
get_viewport()->_gui_update_mouse_over();
}
}

Control::MouseFilter Control::get_mouse_filter() const {
Expand Down Expand Up @@ -3568,6 +3577,8 @@ void Control::_bind_methods() {
BIND_CONSTANT(NOTIFICATION_RESIZED);
BIND_CONSTANT(NOTIFICATION_MOUSE_ENTER);
BIND_CONSTANT(NOTIFICATION_MOUSE_EXIT);
BIND_CONSTANT(NOTIFICATION_MOUSE_ENTER_SELF);
BIND_CONSTANT(NOTIFICATION_MOUSE_EXIT_SELF);
BIND_CONSTANT(NOTIFICATION_FOCUS_ENTER);
BIND_CONSTANT(NOTIFICATION_FOCUS_EXIT);
BIND_CONSTANT(NOTIFICATION_THEME_CHANGED);
Expand Down
2 changes: 2 additions & 0 deletions scene/gui/control.h
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ class Control : public CanvasItem {
NOTIFICATION_SCROLL_BEGIN = 47,
NOTIFICATION_SCROLL_END = 48,
NOTIFICATION_LAYOUT_DIRECTION_CHANGED = 49,
NOTIFICATION_MOUSE_ENTER_SELF = 60,
NOTIFICATION_MOUSE_EXIT_SELF = 61,
};

// Editor plugin interoperability.
Expand Down
4 changes: 4 additions & 0 deletions scene/main/canvas_item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,10 @@ void CanvasItem::set_as_top_level(bool p_top_level) {
_enter_canvas();

_notify_transform();

if (get_viewport()) {
get_viewport()->canvas_item_top_level_changed();
}
}

void CanvasItem::_top_level_changed() {
Expand Down
163 changes: 151 additions & 12 deletions scene/main/viewport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2408,8 +2408,8 @@ void Viewport::_gui_hide_control(Control *p_control) {
if (gui.key_focus == p_control) {
gui_release_focus();
}
if (gui.mouse_over == p_control) {
_drop_mouse_over();
if (gui.mouse_over == p_control || gui.mouse_over_hierarchy.find(p_control) >= 0) {
_drop_mouse_over(p_control->get_parent_control());
}
if (gui.drag_mouse_over == p_control) {
gui.drag_mouse_over = nullptr;
Expand All @@ -2431,8 +2431,8 @@ void Viewport::_gui_remove_control(Control *p_control) {
if (gui.key_focus == p_control) {
gui.key_focus = nullptr;
}
if (gui.mouse_over == p_control) {
_drop_mouse_over();
if (gui.mouse_over == p_control || gui.mouse_over_hierarchy.find(p_control) >= 0) {
_drop_mouse_over(p_control->get_parent_control());
}
if (gui.drag_mouse_over == p_control) {
gui.drag_mouse_over = nullptr;
Expand All @@ -2442,6 +2442,94 @@ void Viewport::_gui_remove_control(Control *p_control) {
}
}

void Viewport::canvas_item_top_level_changed() {
_gui_update_mouse_over();
}

void Viewport::_gui_update_mouse_over() {
if (gui.mouse_over == nullptr || gui.mouse_over_hierarchy.is_empty()) {
return;
}

// Rebuild the mouse over hierarchy.
LocalVector<Control *> new_mouse_over_hierarchy;
LocalVector<Control *> needs_enter;
LocalVector<int> needs_exit;
Comment on lines +2455 to +2457
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to avoid allocations in such a hot function. @Sauermann has an idea on maybe using just references to nodes at the ends of the chain. Maybe the other vector needs something else. This is just an idea about optimization, not meant as a blocker for this PR in any case.

Copy link
Contributor Author

@kitbdev kitbdev Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this function (_gui_mouse_over_hierarchy) is hot. It is only called when set_top_level() or set_mouse_filter() is called, and only executed if the mouse is over something.
If we want I can add a guard before the allocations that is similar to the loop below it but smaller.

Copy link
Contributor Author

@kitbdev kitbdev Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the mouse_over_hierarchy contains a list of all nodes that have gotten mouse_entered but still need a mouse_exited. If we change this to only store the ends, we wouldn't be able to send the correct signals when the mouse filter ignore is changed, or it would be more complicated.


CanvasItem *ancestor = gui.mouse_over;
bool removing = false;
bool reached_top = false;
while (ancestor) {
Control *ancestor_control = Object::cast_to<Control>(ancestor);
if (ancestor_control) {
int found = gui.mouse_over_hierarchy.find(ancestor_control);
if (found >= 0) {
// Remove the node if the propagation chain has been broken or it is now MOUSE_FILTER_IGNORE.
if (removing || ancestor_control->get_mouse_filter() == Control::MOUSE_FILTER_IGNORE) {
needs_exit.push_back(found);
}
}
if (found == 0) {
if (removing) {
// Stop if the chain has been broken and the top of the hierarchy has been reached.
break;
}
reached_top = true;
}
if (!removing && ancestor_control->get_mouse_filter() != Control::MOUSE_FILTER_IGNORE) {
new_mouse_over_hierarchy.push_back(ancestor_control);
// Add the node if it was not found and it is now not MOUSE_FILTER_IGNORE.
if (found < 0) {
needs_enter.push_back(ancestor_control);
}
}
if (ancestor_control->get_mouse_filter() == Control::MOUSE_FILTER_STOP) {
// MOUSE_FILTER_STOP breaks the propagation chain.
if (reached_top) {
break;
}
removing = true;
}
}
if (ancestor->is_set_as_top_level()) {
// Top level breaks the propagation chain.
if (reached_top) {
break;
} else {
removing = true;
ancestor = Object::cast_to<CanvasItem>(ancestor->get_parent());
continue;
}
}
ancestor = ancestor->get_parent_item();
}
if (needs_exit.is_empty() && needs_enter.is_empty()) {
return;
}

// Send Mouse Exit Self notification.
if (gui.mouse_over && !needs_exit.is_empty() && needs_exit[0] == (int)gui.mouse_over_hierarchy.size() - 1) {
gui.mouse_over->notification(Control::NOTIFICATION_MOUSE_EXIT_SELF);
gui.mouse_over = nullptr;
}

// Send Mouse Exit notifications.
for (int exit_control_index : needs_exit) {
gui.mouse_over_hierarchy[exit_control_index]->notification(Control::NOTIFICATION_MOUSE_EXIT);
}

// Update the mouse over hierarchy.
gui.mouse_over_hierarchy.resize(new_mouse_over_hierarchy.size());
for (int i = 0; i < (int)new_mouse_over_hierarchy.size(); i++) {
gui.mouse_over_hierarchy[i] = new_mouse_over_hierarchy[new_mouse_over_hierarchy.size() - 1 - i];
}

// Send Mouse Enter notifications.
for (int i = needs_enter.size() - 1; i >= 0; i--) {
needs_enter[i]->notification(Control::NOTIFICATION_MOUSE_ENTER);
}
}

Window *Viewport::get_base_window() const {
ERR_READ_THREAD_GUARD_V(nullptr);
ERR_FAIL_COND_V(!is_inside_tree(), nullptr);
Expand Down Expand Up @@ -3069,16 +3157,58 @@ void Viewport::_update_mouse_over(Vector2 p_pos) {
// 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();
if (over != gui.mouse_over || (!over && !gui.mouse_over_hierarchy.is_empty())) {
// Find the common ancestor of `gui.mouse_over` and `over`.
Control *common_ancestor = nullptr;
LocalVector<Control *> over_ancestors;

if (over) {
// Get all ancestors that the mouse is currently over and need an enter signal.
CanvasItem *ancestor = over;
while (ancestor) {
Control *ancestor_control = Object::cast_to<Control>(ancestor);
if (ancestor_control) {
if (ancestor_control->get_mouse_filter() != Control::MOUSE_FILTER_IGNORE) {
int found = gui.mouse_over_hierarchy.find(ancestor_control);
if (found >= 0) {
common_ancestor = gui.mouse_over_hierarchy[found];
break;
}
over_ancestors.push_back(ancestor_control);
}
if (ancestor_control->get_mouse_filter() == Control::MOUSE_FILTER_STOP) {
// MOUSE_FILTER_STOP breaks the propagation chain.
break;
}
}
if (ancestor->is_set_as_top_level()) {
// Top level breaks the propagation chain.
break;
}
ancestor = ancestor->get_parent_item();
}
}

if (gui.mouse_over || !gui.mouse_over_hierarchy.is_empty()) {
// Send Mouse Exit Self and Mouse Exit notifications.
_drop_mouse_over(common_ancestor);
} else {
_drop_physics_mouseover();
}

gui.mouse_over = over;
if (over) {
over->notification(Control::NOTIFICATION_MOUSE_ENTER);
gui.mouse_over = over;
gui.mouse_over_hierarchy.reserve(gui.mouse_over_hierarchy.size() + over_ancestors.size());

// Send Mouse Enter notifications to parents first.
for (int i = over_ancestors.size() - 1; i >= 0; i--) {
over_ancestors[i]->notification(Control::NOTIFICATION_MOUSE_ENTER);
gui.mouse_over_hierarchy.push_back(over_ancestors[i]);
}

// Send Mouse Enter Self notification.
gui.mouse_over->notification(Control::NOTIFICATION_MOUSE_ENTER_SELF);

notify_embedded_viewports = true;
}
}
Expand Down Expand Up @@ -3119,7 +3249,7 @@ void Viewport::_mouse_leave_viewport() {
notification(NOTIFICATION_VP_MOUSE_EXIT);
}

void Viewport::_drop_mouse_over() {
void Viewport::_drop_mouse_over(Control *p_until_control) {
_gui_cancel_tooltip();
SubViewportContainer *c = Object::cast_to<SubViewportContainer>(gui.mouse_over);
if (c) {
Expand All @@ -3131,10 +3261,19 @@ void Viewport::_drop_mouse_over() {
v->_mouse_leave_viewport();
}
}
if (gui.mouse_over->is_inside_tree()) {
gui.mouse_over->notification(Control::NOTIFICATION_MOUSE_EXIT);
if (gui.mouse_over && gui.mouse_over->is_inside_tree()) {
gui.mouse_over->notification(Control::NOTIFICATION_MOUSE_EXIT_SELF);
}
gui.mouse_over = nullptr;

// Send Mouse Exit notifications to children first. Don't send to p_until_control or above.
int notification_until = p_until_control ? gui.mouse_over_hierarchy.find(p_until_control) + 1 : 0;
for (int i = gui.mouse_over_hierarchy.size() - 1; i >= notification_until; i--) {
if (gui.mouse_over_hierarchy[i]->is_inside_tree()) {
gui.mouse_over_hierarchy[i]->notification(Control::NOTIFICATION_MOUSE_EXIT);
}
}
gui.mouse_over_hierarchy.resize(notification_until);
}

void Viewport::push_input(const Ref<InputEvent> &p_event, bool p_local_coords) {
Expand Down
5 changes: 4 additions & 1 deletion scene/main/viewport.h
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ class Viewport : public Node {
BitField<MouseButtonMask> mouse_focus_mask;
Control *key_focus = nullptr;
Control *mouse_over = nullptr;
LocalVector<Control *> mouse_over_hierarchy;
Sauermann marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down Expand Up @@ -429,6 +430,7 @@ class Viewport : public Node {

void _gui_remove_control(Control *p_control);
void _gui_hide_control(Control *p_control);
void _gui_update_mouse_over();

void _gui_force_drag(Control *p_base, const Variant &p_data, Control *p_control);
void _gui_set_drag_preview(Control *p_base, Control *p_control);
Expand All @@ -455,7 +457,7 @@ class Viewport : public Node {
void _canvas_layer_add(CanvasLayer *p_canvas_layer);
void _canvas_layer_remove(CanvasLayer *p_canvas_layer);

void _drop_mouse_over();
void _drop_mouse_over(Control *p_until_control = nullptr);
void _drop_mouse_focus();
void _drop_physics_mouseover(bool p_paused_only = false);

Expand Down Expand Up @@ -494,6 +496,7 @@ class Viewport : public Node {

public:
void canvas_parent_mark_dirty(Node *p_node);
void canvas_item_top_level_changed();

uint64_t get_processed_events_count() const { return event_count; }

Expand Down
Loading
Loading