diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml
index b5333a045b2..a498bbeed38 100644
--- a/doc/classes/Control.xml
+++ b/doc/classes/Control.xml
@@ -1104,13 +1104,13 @@
- 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.
- 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]
@@ -1150,12 +1150,24 @@
Sent when the node changes size. Use [member size] to get the new size.
- 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].
+ 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].
+
+
+ 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].
+
+
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].
Sent when the node grabs focus.
@@ -1320,6 +1332,7 @@
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.
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.
diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp
index c7ff5980cb6..87539ef8f27 100644
--- a/scene/gui/control.cpp
+++ b/scene/gui/control.cpp
@@ -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()) {
+ get_viewport()->_gui_update_mouse_over();
+ }
}
Control::MouseFilter Control::get_mouse_filter() const {
@@ -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);
diff --git a/scene/gui/control.h b/scene/gui/control.h
index abbdc42fa4a..db1bd3a346a 100644
--- a/scene/gui/control.h
+++ b/scene/gui/control.h
@@ -368,6 +368,8 @@ public:
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.
diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp
index a350b97bc8e..4ee81e5cb09 100644
--- a/scene/main/canvas_item.cpp
+++ b/scene/main/canvas_item.cpp
@@ -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() {
diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp
index 2b28f21f576..43bdb1395b6 100644
--- a/scene/main/viewport.cpp
+++ b/scene/main/viewport.cpp
@@ -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;
@@ -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;
@@ -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 new_mouse_over_hierarchy;
+ LocalVector needs_enter;
+ LocalVector needs_exit;
+
+ CanvasItem *ancestor = gui.mouse_over;
+ bool removing = false;
+ bool reached_top = false;
+ while (ancestor) {
+ Control *ancestor_control = Object::cast_to(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(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);
@@ -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 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(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;
}
}
@@ -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(gui.mouse_over);
if (c) {
@@ -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 &p_event, bool p_local_coords) {
diff --git a/scene/main/viewport.h b/scene/main/viewport.h
index 65777c973f5..82a9bfc438d 100644
--- a/scene/main/viewport.h
+++ b/scene/main/viewport.h
@@ -361,6 +361,7 @@ private:
BitField mouse_focus_mask;
Control *key_focus = nullptr;
Control *mouse_over = nullptr;
+ LocalVector mouse_over_hierarchy;
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;
@@ -429,6 +430,7 @@ private:
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);
@@ -455,7 +457,7 @@ private:
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);
@@ -494,6 +496,7 @@ protected:
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; }
diff --git a/tests/scene/test_viewport.h b/tests/scene/test_viewport.h
index 0c53668c6d2..1afae66ee02 100644
--- a/tests/scene/test_viewport.h
+++ b/tests/scene/test_viewport.h
@@ -50,17 +50,39 @@ protected:
void _notification(int p_what) {
switch (p_what) {
case NOTIFICATION_MOUSE_ENTER: {
+ if (mouse_over) {
+ invalid_order = true;
+ }
mouse_over = true;
} break;
case NOTIFICATION_MOUSE_EXIT: {
+ if (!mouse_over) {
+ invalid_order = true;
+ }
mouse_over = false;
} break;
+
+ case NOTIFICATION_MOUSE_ENTER_SELF: {
+ if (mouse_over_self) {
+ invalid_order = true;
+ }
+ mouse_over_self = true;
+ } break;
+
+ case NOTIFICATION_MOUSE_EXIT_SELF: {
+ if (!mouse_over_self) {
+ invalid_order = true;
+ }
+ mouse_over_self = false;
+ } break;
}
}
public:
bool mouse_over = false;
+ bool mouse_over_self = false;
+ bool invalid_order = false;
};
// `NotificationControlViewport`-derived class that additionally
@@ -119,12 +141,15 @@ public:
TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
DragStart *node_a = memnew(DragStart);
- Control *node_b = memnew(Control);
+ NotificationControlViewport *node_b = memnew(NotificationControlViewport);
Node2D *node_c = memnew(Node2D);
DragTarget *node_d = memnew(DragTarget);
- Control *node_e = memnew(Control);
+ NotificationControlViewport *node_e = memnew(NotificationControlViewport);
Node *node_f = memnew(Node);
- Control *node_g = memnew(Control);
+ NotificationControlViewport *node_g = memnew(NotificationControlViewport);
+ NotificationControlViewport *node_h = memnew(NotificationControlViewport);
+ NotificationControlViewport *node_i = memnew(NotificationControlViewport);
+ NotificationControlViewport *node_j = memnew(NotificationControlViewport);
node_a->set_name(SNAME("NodeA"));
node_b->set_name(SNAME("NodeB"));
@@ -133,6 +158,9 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
node_e->set_name(SNAME("NodeE"));
node_f->set_name(SNAME("NodeF"));
node_g->set_name(SNAME("NodeG"));
+ node_h->set_name(SNAME("NodeH"));
+ node_i->set_name(SNAME("NodeI"));
+ node_j->set_name(SNAME("NodeJ"));
node_a->set_position(Point2i(0, 0));
node_b->set_position(Point2i(10, 10));
@@ -140,16 +168,25 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
node_d->set_position(Point2i(10, 10));
node_e->set_position(Point2i(10, 100));
node_g->set_position(Point2i(10, 100));
+ node_h->set_position(Point2i(10, 120));
+ node_i->set_position(Point2i(2, 0));
+ node_j->set_position(Point2i(2, 0));
node_a->set_size(Point2i(30, 30));
node_b->set_size(Point2i(30, 30));
node_d->set_size(Point2i(30, 30));
node_e->set_size(Point2i(10, 10));
node_g->set_size(Point2i(10, 10));
+ node_h->set_size(Point2i(10, 10));
+ node_i->set_size(Point2i(10, 10));
+ node_j->set_size(Point2i(10, 10));
node_a->set_focus_mode(Control::FOCUS_CLICK);
node_b->set_focus_mode(Control::FOCUS_CLICK);
node_d->set_focus_mode(Control::FOCUS_CLICK);
node_e->set_focus_mode(Control::FOCUS_CLICK);
node_g->set_focus_mode(Control::FOCUS_CLICK);
+ node_h->set_focus_mode(Control::FOCUS_CLICK);
+ node_i->set_focus_mode(Control::FOCUS_CLICK);
+ node_j->set_focus_mode(Control::FOCUS_CLICK);
Window *root = SceneTree::get_singleton()->get_root();
DisplayServerMock *DS = (DisplayServerMock *)(DisplayServer::get_singleton());
@@ -162,6 +199,9 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
// - e (Control)
// - f (Node)
// - g (Control)
+ // - h (Control)
+ // - i (Control)
+ // - j (Control)
root->add_child(node_a);
root->add_child(node_b);
node_b->add_child(node_c);
@@ -169,12 +209,17 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
root->add_child(node_e);
node_e->add_child(node_f);
node_f->add_child(node_g);
+ root->add_child(node_h);
+ node_h->add_child(node_i);
+ node_i->add_child(node_j);
Point2i on_a = Point2i(5, 5);
Point2i on_b = Point2i(15, 15);
Point2i on_d = Point2i(25, 25);
Point2i on_e = Point2i(15, 105);
Point2i on_g = Point2i(15, 105);
+ Point2i on_i = Point2i(13, 125);
+ Point2i on_j = Point2i(15, 125);
Point2i on_background = Point2i(500, 500);
Point2i on_outside = Point2i(-1, -1);
@@ -419,26 +464,612 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
SUBCASE("[Viewport][GuiInputEvent] Mouse Motion") {
// FIXME: Tooltips are not yet tested. They likely require an internal clock.
- SUBCASE("[Viewport][GuiInputEvent] Mouse Motion changes the Control, that it is over.") {
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Motion changes the Control that it is over.") {
SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
CHECK_FALSE(node_a->mouse_over);
+ CHECK_FALSE(node_a->mouse_over_self);
// Move over Control.
SEND_GUI_MOUSE_MOTION_EVENT(on_a, MouseButtonMask::NONE, Key::NONE);
CHECK(node_a->mouse_over);
+ CHECK(node_a->mouse_over_self);
// No change.
SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(1, 1), MouseButtonMask::NONE, Key::NONE);
CHECK(node_a->mouse_over);
+ CHECK(node_a->mouse_over_self);
// Move over other Control.
SEND_GUI_MOUSE_MOTION_EVENT(on_d, MouseButtonMask::NONE, Key::NONE);
CHECK_FALSE(node_a->mouse_over);
+ CHECK_FALSE(node_a->mouse_over_self);
CHECK(node_d->mouse_over);
+ CHECK(node_d->mouse_over_self);
- // Move to background
+ // Move to background.
SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
CHECK_FALSE(node_d->mouse_over);
+ CHECK_FALSE(node_d->mouse_over_self);
+
+ CHECK_FALSE(node_a->invalid_order);
+ CHECK_FALSE(node_d->invalid_order);
+ }
+
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Enter/Exit notification propagation.") {
+ node_d->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ node_g->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_b->mouse_over);
+ CHECK_FALSE(node_b->mouse_over_self);
+ CHECK_FALSE(node_d->mouse_over);
+ CHECK_FALSE(node_d->mouse_over_self);
+
+ // Move to Control node_d. node_b receives mouse over since it is only separated by a CanvasItem.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_d, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_b->mouse_over);
+ CHECK_FALSE(node_b->mouse_over_self);
+ CHECK(node_d->mouse_over);
+ CHECK(node_d->mouse_over_self);
+
+ // Move to background.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_b->mouse_over);
+ CHECK_FALSE(node_b->mouse_over_self);
+ CHECK_FALSE(node_d->mouse_over);
+ CHECK_FALSE(node_d->mouse_over_self);
+
+ CHECK_FALSE(node_e->mouse_over);
+ CHECK_FALSE(node_e->mouse_over_self);
+ CHECK_FALSE(node_g->mouse_over);
+ CHECK_FALSE(node_g->mouse_over_self);
+
+ // Move to Control node_g. node_g receives mouse over but node_e does not since it is separated by a non-CanvasItem.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_g, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_e->mouse_over);
+ CHECK_FALSE(node_e->mouse_over_self);
+ CHECK(node_g->mouse_over);
+ CHECK(node_g->mouse_over_self);
+
+ // Move to background.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_e->mouse_over);
+ CHECK_FALSE(node_e->mouse_over_self);
+ CHECK_FALSE(node_g->mouse_over);
+ CHECK_FALSE(node_g->mouse_over_self);
+
+ CHECK_FALSE(node_b->invalid_order);
+ CHECK_FALSE(node_d->invalid_order);
+ CHECK_FALSE(node_e->invalid_order);
+ CHECK_FALSE(node_g->invalid_order);
+
+ node_d->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_g->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ }
+
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Enter/Exit notification propagation when moving into child.") {
+ SIGNAL_WATCH(node_i, SNAME("mouse_entered"));
+ SIGNAL_WATCH(node_i, SNAME("mouse_exited"));
+ Array signal_args;
+ signal_args.push_back(Array());
+
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+ // Move to background.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+
+ // Move to Control node_i.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_i, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_i->mouse_over);
+ CHECK(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+ SIGNAL_CHECK(SNAME("mouse_entered"), signal_args);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Move to child Control node_j. node_i should not receive any new Mouse Enter signals.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Move to parent Control node_i. node_i should not receive any new Mouse Enter signals.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_i, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_i->mouse_over);
+ CHECK(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Move to background.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK(SNAME("mouse_exited"), signal_args);
+
+ CHECK_FALSE(node_i->invalid_order);
+ CHECK_FALSE(node_j->invalid_order);
+
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+
+ SIGNAL_UNWATCH(node_i, SNAME("mouse_entered"));
+ SIGNAL_UNWATCH(node_i, SNAME("mouse_exited"));
+ }
+
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Enter/Exit notification propagation with top level.") {
+ node_c->set_as_top_level(true);
+ node_i->set_as_top_level(true);
+ node_c->set_position(node_b->get_global_position());
+ node_i->set_position(node_h->get_global_position());
+ node_d->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_b->mouse_over);
+ CHECK_FALSE(node_b->mouse_over_self);
+ CHECK_FALSE(node_d->mouse_over);
+ CHECK_FALSE(node_d->mouse_over_self);
+
+ // Move to Control node_d. node_b does not receive mouse over since node_c is top level.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_d, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_b->mouse_over);
+ CHECK_FALSE(node_b->mouse_over_self);
+ CHECK(node_d->mouse_over);
+ CHECK(node_d->mouse_over_self);
+
+ // Move to background.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_b->mouse_over);
+ CHECK_FALSE(node_b->mouse_over_self);
+ CHECK_FALSE(node_d->mouse_over);
+ CHECK_FALSE(node_d->mouse_over_self);
+
+ CHECK_FALSE(node_g->mouse_over);
+ CHECK_FALSE(node_g->mouse_over_self);
+ CHECK_FALSE(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+
+ // Move to Control node_j. node_h does not receive mouse over since node_i is top level.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+
+ // Move to background.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+
+ CHECK_FALSE(node_b->invalid_order);
+ CHECK_FALSE(node_d->invalid_order);
+ CHECK_FALSE(node_e->invalid_order);
+ CHECK_FALSE(node_h->invalid_order);
+ CHECK_FALSE(node_i->invalid_order);
+ CHECK_FALSE(node_j->invalid_order);
+
+ node_c->set_as_top_level(false);
+ node_i->set_as_top_level(false);
+ node_c->set_position(Point2i(0, 0));
+ node_i->set_position(Point2i(0, 0));
+ node_d->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ }
+
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Enter/Exit notification propagation with mouse filter stop.") {
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+ // Move to background.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+
+ // Move to Control node_j. node_h does not receive mouse over since node_i is MOUSE_FILTER_STOP.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+
+ // Move to background.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+
+ CHECK_FALSE(node_h->invalid_order);
+ CHECK_FALSE(node_i->invalid_order);
+ CHECK_FALSE(node_j->invalid_order);
+
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ }
+
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Enter/Exit notification propagation with mouse filter ignore.") {
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_IGNORE);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+ // Move to background.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+
+ // Move to Control node_j. node_i does not receive mouse over since node_i is MOUSE_FILTER_IGNORE.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+
+ // Move to background.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+ CHECK_FALSE(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+
+ CHECK_FALSE(node_h->invalid_order);
+ CHECK_FALSE(node_i->invalid_order);
+ CHECK_FALSE(node_j->invalid_order);
+
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ }
+
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Enter/Exit notification when changing top level.") {
+ SIGNAL_WATCH(node_i, SNAME("mouse_entered"));
+ SIGNAL_WATCH(node_i, SNAME("mouse_exited"));
+ Array signal_args;
+ signal_args.push_back(Array());
+
+ node_d->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+ // Move to Control node_d.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_d, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_b->mouse_over);
+ CHECK_FALSE(node_b->mouse_over_self);
+ CHECK(node_d->mouse_over);
+ CHECK(node_d->mouse_over_self);
+
+ // Change node_c to be top level. node_b should receive Mouse Exit.
+ node_c->set_as_top_level(true);
+ CHECK_FALSE(node_b->mouse_over);
+ CHECK_FALSE(node_b->mouse_over_self);
+ CHECK(node_d->mouse_over);
+ CHECK(node_d->mouse_over_self);
+
+ // Change node_c to be not top level. node_b should receive Mouse Enter.
+ node_c->set_as_top_level(false);
+ CHECK(node_b->mouse_over);
+ CHECK_FALSE(node_b->mouse_over_self);
+ CHECK(node_d->mouse_over);
+ CHECK(node_d->mouse_over_self);
+
+ // Move to Control node_j.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK(SNAME("mouse_entered"), signal_args);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Change node_i to top level. node_h should receive Mouse Exit. node_i should not receive any new signals.
+ node_i->set_as_top_level(true);
+ CHECK_FALSE(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Change node_i to not top level. node_h should receive Mouse Enter. node_i should not receive any new signals.
+ node_i->set_as_top_level(false);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ CHECK_FALSE(node_b->invalid_order);
+ CHECK_FALSE(node_d->invalid_order);
+ CHECK_FALSE(node_e->invalid_order);
+ CHECK_FALSE(node_h->invalid_order);
+ CHECK_FALSE(node_i->invalid_order);
+ CHECK_FALSE(node_j->invalid_order);
+
+ node_d->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+
+ SIGNAL_UNWATCH(node_i, SNAME("mouse_entered"));
+ SIGNAL_UNWATCH(node_i, SNAME("mouse_exited"));
+ }
+
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Enter/Exit notification when changing the mouse filter to stop.") {
+ SIGNAL_WATCH(node_i, SNAME("mouse_entered"));
+ SIGNAL_WATCH(node_i, SNAME("mouse_exited"));
+ Array signal_args;
+ signal_args.push_back(Array());
+
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+ // Move to Control node_j.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK(SNAME("mouse_entered"), signal_args);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Change node_i to MOUSE_FILTER_STOP. node_h should receive Mouse Exit. node_i should not receive any new signals.
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ CHECK_FALSE(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Change node_i to MOUSE_FILTER_PASS. node_h should receive Mouse Enter. node_i should not receive any new signals.
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ CHECK_FALSE(node_h->invalid_order);
+ CHECK_FALSE(node_i->invalid_order);
+ CHECK_FALSE(node_j->invalid_order);
+
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+
+ SIGNAL_UNWATCH(node_i, SNAME("mouse_entered"));
+ SIGNAL_UNWATCH(node_i, SNAME("mouse_exited"));
+ }
+
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Enter/Exit notification when changing the mouse filter to ignore.") {
+ SIGNAL_WATCH(node_i, SNAME("mouse_entered"));
+ SIGNAL_WATCH(node_i, SNAME("mouse_exited"));
+ Array signal_args;
+ signal_args.push_back(Array());
+
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+ // Move to Control node_j.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK(SNAME("mouse_entered"), signal_args);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Change node_i to MOUSE_FILTER_IGNORE. node_i should receive Mouse Exit.
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_IGNORE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK(SNAME("mouse_exited"), signal_args);
+
+ // Change node_i to MOUSE_FILTER_PASS. node_i should receive Mouse Enter.
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK(SNAME("mouse_entered"), signal_args);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Change node_j to MOUSE_FILTER_IGNORE. After updating the mouse motion, node_i should now have mouse_over_self.
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_IGNORE);
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Change node_j to MOUSE_FILTER_PASS. After updating the mouse motion, node_j should now have mouse_over_self.
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ CHECK_FALSE(node_h->invalid_order);
+ CHECK_FALSE(node_i->invalid_order);
+ CHECK_FALSE(node_j->invalid_order);
+
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+
+ SIGNAL_UNWATCH(node_i, SNAME("mouse_entered"));
+ SIGNAL_UNWATCH(node_i, SNAME("mouse_exited"));
+ }
+
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Enter/Exit notification when removing the hovered Control.") {
+ SIGNAL_WATCH(node_h, SNAME("mouse_entered"));
+ SIGNAL_WATCH(node_h, SNAME("mouse_exited"));
+ Array signal_args;
+ signal_args.push_back(Array());
+
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+ // Move to Control node_j.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK(SNAME("mouse_entered"), signal_args);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Remove node_i from the tree. node_i and node_j should receive Mouse Exit. node_h should not receive any new signals.
+ node_h->remove_child(node_i);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Add node_i to the tree and update the mouse. node_i and node_j should receive Mouse Enter. node_h should not receive any new signals.
+ node_h->add_child(node_i);
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ CHECK_FALSE(node_h->invalid_order);
+ CHECK_FALSE(node_i->invalid_order);
+ CHECK_FALSE(node_j->invalid_order);
+
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+
+ SIGNAL_UNWATCH(node_h, SNAME("mouse_entered"));
+ SIGNAL_UNWATCH(node_h, SNAME("mouse_exited"));
+ }
+
+ SUBCASE("[Viewport][GuiInputEvent] Mouse Enter/Exit notification when hiding the hovered Control.") {
+ SIGNAL_WATCH(node_h, SNAME("mouse_entered"));
+ SIGNAL_WATCH(node_h, SNAME("mouse_exited"));
+ Array signal_args;
+ signal_args.push_back(Array());
+
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+ // Move to Control node_j.
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK(SNAME("mouse_entered"), signal_args);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Hide node_i. node_i and node_j should receive Mouse Exit. node_h should not receive any new signals.
+ node_i->hide();
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK_FALSE(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK_FALSE(node_j->mouse_over);
+ CHECK_FALSE(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ // Show node_i and update the mouse. node_i and node_j should receive Mouse Enter. node_h should not receive any new signals.
+ node_i->show();
+ SEND_GUI_MOUSE_MOTION_EVENT(on_j, MouseButtonMask::NONE, Key::NONE);
+ CHECK(node_h->mouse_over);
+ CHECK_FALSE(node_h->mouse_over_self);
+ CHECK(node_i->mouse_over);
+ CHECK_FALSE(node_i->mouse_over_self);
+ CHECK(node_j->mouse_over);
+ CHECK(node_j->mouse_over_self);
+ SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+ SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+ CHECK_FALSE(node_h->invalid_order);
+ CHECK_FALSE(node_i->invalid_order);
+ CHECK_FALSE(node_j->invalid_order);
+
+ node_i->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+ node_j->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+
+ SIGNAL_UNWATCH(node_h, SNAME("mouse_entered"));
+ SIGNAL_UNWATCH(node_h, SNAME("mouse_exited"));
}
SUBCASE("[Viewport][GuiInputEvent] Window Mouse Enter/Exit signals.") {
@@ -710,6 +1341,9 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
}
}
+ memdelete(node_j);
+ memdelete(node_i);
+ memdelete(node_h);
memdelete(node_g);
memdelete(node_f);
memdelete(node_e);