Browse Source

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 <[email protected]>
kit 1 year ago
parent
commit
d24d73ba31

+ 18 - 5
doc/classes/Control.xml

@@ -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]
@@ -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.
@@ -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.

+ 11 - 0
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);

+ 2 - 0
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.

+ 4 - 0
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() {

+ 151 - 12
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<Control *> new_mouse_over_hierarchy;
+	LocalVector<Control *> needs_enter;
+	LocalVector<int> needs_exit;
+
+	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);
@@ -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;
 		}
 	}
@@ -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) {
@@ -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) {

+ 4 - 1
scene/main/viewport.h

@@ -361,6 +361,7 @@ private:
 		BitField<MouseButtonMask> mouse_focus_mask;
 		Control *key_focus = nullptr;
 		Control *mouse_over = nullptr;
+		LocalVector<Control *> 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; }
 

+ 639 - 5
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);