Переглянути джерело

Merge pull request #72862 from Sauermann/fix-viewport-event-unittests

Add Unit Tests for Viewport InputEvent handling
Rémi Verschelde 2 роки тому
батько
коміт
cd7c65ab67

+ 19 - 1
tests/display_server_mock.h

@@ -42,6 +42,7 @@ private:
 	friend class DisplayServer;
 
 	Point2i mouse_position = Point2i(-1, -1); // Outside of Window.
+	CursorShape cursor_shape = CursorShape::CURSOR_ARROW;
 	bool window_over = false;
 	Callable event_callback;
 	Callable input_event_callback;
@@ -103,6 +104,7 @@ public:
 	bool has_feature(Feature p_feature) const override {
 		switch (p_feature) {
 			case FEATURE_MOUSE:
+			case FEATURE_CURSOR_SHAPE:
 				return true;
 			default: {
 			}
@@ -115,12 +117,24 @@ public:
 	// You can simulate DisplayServer-events by calling this function.
 	// The events will be deliverd to Godot's Input-system.
 	// Mouse-events (Button & Motion) will additionally update the DisplayServer's mouse position.
+	// For Mouse motion events, the `relative`-property is set based on the distance to the previous mouse position.
 	void simulate_event(Ref<InputEvent> p_event) {
+		Ref<InputEvent> event = p_event;
 		Ref<InputEventMouse> me = p_event;
 		if (me.is_valid()) {
+			Ref<InputEventMouseMotion> mm = p_event;
+			if (mm.is_valid()) {
+				mm->set_relative(mm->get_position() - mouse_position);
+				event = mm;
+			}
 			_set_mouse_position(me->get_position());
 		}
-		Input::get_singleton()->parse_input_event(p_event);
+		Input::get_singleton()->parse_input_event(event);
+	}
+
+	// Returns the current cursor shape.
+	CursorShape get_cursor_shape() {
+		return cursor_shape;
 	}
 
 	virtual Point2i mouse_get_position() const override { return mouse_position; }
@@ -129,6 +143,10 @@ public:
 		return Size2i(1920, 1080);
 	}
 
+	virtual void cursor_set_shape(CursorShape p_shape) override {
+		cursor_shape = p_shape;
+	}
+
 	virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override {
 		event_callback = p_callable;
 	}

+ 0 - 3
tests/scene/test_text_edit.h

@@ -1134,7 +1134,6 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
 		SUBCASE("[TextEdit] text drag") {
 			TextEdit *target_text_edit = memnew(TextEdit);
 			SceneTree::get_singleton()->get_root()->add_child(target_text_edit);
-			text_edit->get_viewport()->set_embedding_subwindows(true); // Bypass display server for drop handling.
 
 			target_text_edit->set_size(Size2(200, 200));
 			target_text_edit->set_position(Point2(400, 0));
@@ -3083,8 +3082,6 @@ TEST_CASE("[SceneTree][TextEdit] context menu") {
 	TextEdit *text_edit = memnew(TextEdit);
 	SceneTree::get_singleton()->get_root()->add_child(text_edit);
 
-	text_edit->get_viewport()->set_embedding_subwindows(true); // Bypass display server for drop handling.
-
 	text_edit->set_size(Size2(800, 200));
 	text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vasius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque.");
 	MessageQueue::get_singleton()->flush();

+ 718 - 0
tests/scene/test_viewport.h

@@ -0,0 +1,718 @@
+/**************************************************************************/
+/*  test_viewport.h                                                       */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#ifndef TEST_VIEWPORT_H
+#define TEST_VIEWPORT_H
+
+#include "scene/2d/node_2d.h"
+#include "scene/gui/control.h"
+#include "scene/main/window.h"
+
+#include "tests/test_macros.h"
+
+namespace TestViewport {
+
+class NotificationControl : public Control {
+	GDCLASS(NotificationControl, Control);
+
+protected:
+	void _notification(int p_what) {
+		switch (p_what) {
+			case NOTIFICATION_MOUSE_ENTER: {
+				mouse_over = true;
+			} break;
+
+			case NOTIFICATION_MOUSE_EXIT: {
+				mouse_over = false;
+			} break;
+		}
+	}
+
+public:
+	bool mouse_over = false;
+};
+
+// `NotificationControl`-derived class that additionally
+// - allows start Dragging
+// - stores mouse information of last event
+class DragStart : public NotificationControl {
+	GDCLASS(DragStart, NotificationControl);
+
+public:
+	MouseButton last_mouse_button;
+	Point2i last_mouse_move_position;
+	StringName drag_data_name = SNAME("Drag Data");
+
+	virtual Variant get_drag_data(const Point2 &p_point) override {
+		return drag_data_name;
+	}
+
+	virtual void gui_input(const Ref<InputEvent> &p_event) override {
+		Ref<InputEventMouseButton> mb = p_event;
+		if (mb.is_valid()) {
+			last_mouse_button = mb->get_button_index();
+			return;
+		}
+
+		Ref<InputEventMouseMotion> mm = p_event;
+		if (mm.is_valid()) {
+			last_mouse_move_position = mm->get_position();
+			return;
+		}
+	}
+};
+
+// `NotificationControl`-derived class that acts as a Drag and Drop target.
+class DragTarget : public NotificationControl {
+	GDCLASS(DragTarget, NotificationControl);
+
+public:
+	Variant drag_data;
+	virtual bool can_drop_data(const Point2 &p_point, const Variant &p_data) const override {
+		StringName string_data = p_data;
+		// Verify drag data is compatible.
+		if (string_data != SNAME("Drag Data")) {
+			return false;
+		}
+		// Only the left half is droppable area.
+		if (p_point.x * 2 > get_size().x) {
+			return false;
+		}
+		return true;
+	}
+
+	virtual void drop_data(const Point2 &p_point, const Variant &p_data) override {
+		drag_data = p_data;
+	}
+};
+
+TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
+	DragStart *node_a = memnew(DragStart);
+	Control *node_b = memnew(Control);
+	Node2D *node_c = memnew(Node2D);
+	DragTarget *node_d = memnew(DragTarget);
+	Control *node_e = memnew(Control);
+	Node *node_f = memnew(Node);
+	Control *node_g = memnew(Control);
+
+	node_a->set_name(SNAME("NodeA"));
+	node_b->set_name(SNAME("NodeB"));
+	node_c->set_name(SNAME("NodeC"));
+	node_d->set_name(SNAME("NodeD"));
+	node_e->set_name(SNAME("NodeE"));
+	node_f->set_name(SNAME("NodeF"));
+	node_g->set_name(SNAME("NodeG"));
+
+	node_a->set_position(Point2i(0, 0));
+	node_b->set_position(Point2i(10, 10));
+	node_c->set_position(Point2i(0, 0));
+	node_d->set_position(Point2i(10, 10));
+	node_e->set_position(Point2i(10, 100));
+	node_g->set_position(Point2i(10, 100));
+	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_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);
+	Window *root = SceneTree::get_singleton()->get_root();
+	DisplayServerMock *DS = (DisplayServerMock *)(DisplayServer::get_singleton());
+
+	// Scene tree:
+	// - root
+	//   - a (Control)
+	//   - b (Control)
+	//     - c (Node2D)
+	//       - d (Control)
+	//   - e (Control)
+	//     - f (Node)
+	//       - g (Control)
+	root->add_child(node_a);
+	root->add_child(node_b);
+	node_b->add_child(node_c);
+	node_c->add_child(node_d);
+	root->add_child(node_e);
+	node_e->add_child(node_f);
+	node_f->add_child(node_g);
+
+	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_background = Point2i(500, 500);
+	Point2i on_outside = Point2i(-1, -1);
+
+	// Unit tests for Viewport::gui_find_control and Viewport::_gui_find_control_at_pos
+	SUBCASE("[VIEWPORT][GuiFindControl] Finding Controls at a Viewport-position") {
+		// FIXME: It is extremely difficult to create a situation where the Control has a zero determinant.
+		// Leaving that if-branch untested.
+
+		SUBCASE("[VIEWPORT][GuiFindControl] Basic position tests") {
+			CHECK(root->gui_find_control(on_a) == node_a);
+			CHECK(root->gui_find_control(on_b) == node_b);
+			CHECK(root->gui_find_control(on_d) == node_d);
+			CHECK(root->gui_find_control(on_e) == node_g); // Node F makes G a Root Control at the same position as E
+			CHECK(root->gui_find_control(on_g) == node_g);
+			CHECK_FALSE(root->gui_find_control(on_background));
+		}
+
+		SUBCASE("[VIEWPORT][GuiFindControl] Invisible nodes are not considered as results.") {
+			// Non-Root Control
+			node_d->hide();
+			CHECK(root->gui_find_control(on_d) == node_b);
+			// Root Control
+			node_b->hide();
+			CHECK(root->gui_find_control(on_b) == node_a);
+		}
+
+		SUBCASE("[VIEWPORT][GuiFindControl] Root Control with CanvasItem as parent is affected by parent's transform.") {
+			node_b->remove_child(node_c);
+			node_c->set_position(Point2i(50, 50));
+			root->add_child(node_c);
+			CHECK(root->gui_find_control(Point2i(65, 65)) == node_d);
+		}
+
+		SUBCASE("[VIEWPORT][GuiFindControl] Control Contents Clipping clips accessible position of children.") {
+			CHECK_FALSE(node_b->is_clipping_contents());
+			CHECK(root->gui_find_control(on_d + Point2i(20, 20)) == node_d);
+			node_b->set_clip_contents(true);
+			CHECK(root->gui_find_control(on_d) == node_d);
+			CHECK_FALSE(root->gui_find_control(on_d + Point2i(20, 20)));
+		}
+
+		SUBCASE("[VIEWPORT][GuiFindControl] Top Level Control as descendant of CanvasItem isn't affected by parent's transform.") {
+			CHECK(root->gui_find_control(on_d + Point2i(20, 20)) == node_d);
+			node_d->set_as_top_level(true);
+			CHECK_FALSE(root->gui_find_control(on_d + Point2i(20, 20)));
+			CHECK(root->gui_find_control(on_b) == node_d);
+		}
+	}
+
+	SUBCASE("[Viewport][GuiInputEvent] nullptr as argument doesn't lead to a crash.") {
+		CHECK_NOTHROW(root->push_input(nullptr));
+	}
+
+	// Unit tests for Viewport::_gui_input_event (Mouse Buttons)
+	SUBCASE("[Viewport][GuiInputEvent] Mouse Button Down/Up.") {
+		SUBCASE("[Viewport][GuiInputEvent] Mouse Button Control Focus Change.") {
+			SUBCASE("[Viewport][GuiInputEvent] Grab Focus while no Control has focus.") {
+				CHECK_FALSE(root->gui_get_focus_owner());
+
+				// Click on A
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK(node_a->has_focus());
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Grab Focus from other Control.") {
+				node_a->grab_focus();
+				CHECK(node_a->has_focus());
+
+				// Click on D
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK(node_d->has_focus());
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Non-CanvasItem breaks Transform hierarchy.") {
+				CHECK_FALSE(root->gui_get_focus_owner());
+
+				// Click on G absolute coordinates
+				SEND_GUI_MOUSE_BUTTON_EVENT(Point2i(15, 105), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK(node_g->has_focus());
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(Point2i(15, 105), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] No Focus change when clicking in background.") {
+				CHECK_FALSE(root->gui_get_focus_owner());
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_background, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK_FALSE(root->gui_get_focus_owner());
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_background, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+
+				node_a->grab_focus();
+				CHECK(node_a->has_focus());
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_background, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_background, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+				CHECK(node_a->has_focus());
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Mouse Button No Focus Steal while other Mouse Button is pressed.") {
+				CHECK_FALSE(root->gui_get_focus_owner());
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK(node_a->has_focus());
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_b, MouseButton::RIGHT, (int)MouseButtonMask::LEFT | (int)MouseButtonMask::RIGHT, Key::NONE);
+				CHECK(node_a->has_focus());
+
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_b, MouseButton::RIGHT, MouseButtonMask::LEFT, Key::NONE);
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_b, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+				CHECK(node_a->has_focus());
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Allow Focus Steal with LMB while other Mouse Button is held down and was initially pressed without being over a Control.") {
+				// TODO: Not sure, if this is intended behavior, but this is an edge case.
+				CHECK_FALSE(root->gui_get_focus_owner());
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_background, MouseButton::RIGHT, MouseButtonMask::RIGHT, Key::NONE);
+				CHECK_FALSE(root->gui_get_focus_owner());
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, (int)MouseButtonMask::LEFT | (int)MouseButtonMask::RIGHT, Key::NONE);
+				CHECK(node_a->has_focus());
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::RIGHT, Key::NONE);
+				CHECK(node_a->has_focus());
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_b, MouseButton::LEFT, (int)MouseButtonMask::LEFT | (int)MouseButtonMask::RIGHT, Key::NONE);
+				CHECK(node_b->has_focus());
+
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::RIGHT, Key::NONE);
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d, MouseButton::RIGHT, MouseButtonMask::NONE, Key::NONE);
+				CHECK(node_b->has_focus());
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Ignore Focus from Mouse Buttons when mouse-filter is set to ignore.") {
+				node_d->grab_focus();
+				node_d->set_mouse_filter(Control::MOUSE_FILTER_IGNORE);
+				CHECK(node_d->has_focus());
+
+				// Click on overlapping area B&D.
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK(node_b->has_focus());
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] RMB doesn't grab focus.") {
+				node_a->grab_focus();
+				CHECK(node_a->has_focus());
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_d, MouseButton::RIGHT, MouseButtonMask::RIGHT, Key::NONE);
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d, MouseButton::RIGHT, MouseButtonMask::NONE, Key::NONE);
+				CHECK(node_a->has_focus());
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] LMB on unfocusable Control doesn't grab focus.") {
+				CHECK_FALSE(node_g->has_focus());
+				node_g->set_focus_mode(Control::FOCUS_NONE);
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_g, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_g, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+				CHECK_FALSE(node_g->has_focus());
+
+				// Now verify the opposite with FOCUS_CLICK
+				node_g->set_focus_mode(Control::FOCUS_CLICK);
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_g, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_g, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+				CHECK(node_g->has_focus());
+				node_g->set_focus_mode(Control::FOCUS_CLICK);
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Signal 'gui_focus_changed' is only emitted if a previously unfocused Control grabs focus.") {
+				SIGNAL_WATCH(root, SNAME("gui_focus_changed"));
+				Array node_array;
+				node_array.push_back(node_a);
+				Array signal_args;
+				signal_args.push_back(node_array);
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+				SIGNAL_CHECK(SNAME("gui_focus_changed"), signal_args);
+
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+				CHECK(node_a->has_focus());
+				SIGNAL_CHECK_FALSE(SNAME("gui_focus_changed"));
+
+				SIGNAL_UNWATCH(root, SNAME("gui_focus_changed"));
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Focus Propagation to parent items.") {
+				SUBCASE("[Viewport][GuiInputEvent] Unfocusable Control with MOUSE_FILTER_PASS propagates focus to parent CanvasItem.") {
+					node_d->set_focus_mode(Control::FOCUS_NONE);
+					node_d->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+					SEND_GUI_MOUSE_BUTTON_EVENT(on_d + Point2i(20, 20), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+					CHECK(node_b->has_focus());
+					SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d + Point2i(20, 20), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+
+					// Verify break condition for Root Control.
+					node_a->set_focus_mode(Control::FOCUS_NONE);
+					node_a->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+					SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+					SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+					CHECK(node_b->has_focus());
+				}
+
+				SUBCASE("[Viewport][GuiInputEvent] Top Level CanvasItem stops focus propagation.") {
+					node_d->set_focus_mode(Control::FOCUS_NONE);
+					node_d->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+					node_c->set_as_top_level(true);
+
+					SEND_GUI_MOUSE_BUTTON_EVENT(on_b, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+					SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_b, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+					CHECK_FALSE(root->gui_get_focus_owner());
+
+					node_d->set_focus_mode(Control::FOCUS_CLICK);
+					SEND_GUI_MOUSE_BUTTON_EVENT(on_b, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+					SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_b, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+					CHECK(node_d->has_focus());
+				}
+			}
+		}
+
+		SUBCASE("[Viewport][GuiInputEvent] Process-Mode affects, if GUI Mouse Button Events are processed.") {
+			node_a->last_mouse_button = MouseButton::NONE;
+			node_a->set_process_mode(Node::PROCESS_MODE_DISABLED);
+			SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+			SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+			CHECK(node_a->last_mouse_button == MouseButton::NONE);
+
+			// Now verify that with allowed processing the event is processed.
+			node_a->set_process_mode(Node::PROCESS_MODE_ALWAYS);
+			SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+			SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+			CHECK(node_a->last_mouse_button == MouseButton::LEFT);
+		}
+	}
+
+	// Unit tests for Viewport::_gui_input_event (Mouse Motion)
+	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.") {
+			SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+			CHECK_FALSE(node_a->mouse_over);
+
+			// Move over Control.
+			SEND_GUI_MOUSE_MOTION_EVENT(on_a, MouseButtonMask::NONE, Key::NONE);
+			CHECK(node_a->mouse_over);
+
+			// No change.
+			SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(1, 1), MouseButtonMask::NONE, Key::NONE);
+			CHECK(node_a->mouse_over);
+
+			// Move over other Control.
+			SEND_GUI_MOUSE_MOTION_EVENT(on_d, MouseButtonMask::NONE, Key::NONE);
+			CHECK_FALSE(node_a->mouse_over);
+			CHECK(node_d->mouse_over);
+
+			// Move to background
+			SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+			CHECK_FALSE(node_d->mouse_over);
+		}
+
+		SUBCASE("[Viewport][GuiInputEvent] Window Mouse Enter/Exit signals.") {
+			SIGNAL_WATCH(root, SNAME("mouse_entered"));
+			SIGNAL_WATCH(root, SNAME("mouse_exited"));
+			Array signal_args;
+			signal_args.push_back(Array());
+
+			SEND_GUI_MOUSE_MOTION_EVENT(on_outside, MouseButtonMask::NONE, Key::NONE);
+			SIGNAL_CHECK_FALSE(SNAME("mouse_entered"));
+			SIGNAL_CHECK(SNAME("mouse_exited"), signal_args);
+
+			SEND_GUI_MOUSE_MOTION_EVENT(on_a, MouseButtonMask::NONE, Key::NONE);
+			SIGNAL_CHECK(SNAME("mouse_entered"), signal_args);
+			SIGNAL_CHECK_FALSE(SNAME("mouse_exited"));
+
+			SIGNAL_UNWATCH(root, SNAME("mouse_entered"));
+			SIGNAL_UNWATCH(root, SNAME("mouse_exited"));
+		}
+
+		SUBCASE("[Viewport][GuiInputEvent] Process-Mode affects, if GUI Mouse Motion Events are processed.") {
+			node_a->last_mouse_move_position = on_outside;
+			node_a->set_process_mode(Node::PROCESS_MODE_DISABLED);
+			SEND_GUI_MOUSE_MOTION_EVENT(on_a, MouseButtonMask::NONE, Key::NONE);
+			CHECK(node_a->last_mouse_move_position == on_outside);
+
+			// Now verify that with allowed processing the event is processed.
+			node_a->set_process_mode(Node::PROCESS_MODE_ALWAYS);
+			SEND_GUI_MOUSE_MOTION_EVENT(on_a, MouseButtonMask::NONE, Key::NONE);
+			CHECK(node_a->last_mouse_move_position == on_a);
+		}
+	}
+
+	// Unit tests for Viewport::_gui_input_event (Drag and Drop)
+	SUBCASE("[Viewport][GuiInputEvent] Drag and Drop") {
+		// FIXME: Drag-Preview will likely change. Tests for this part would have to be rewritten anyway.
+		// See https://github.com/godotengine/godot/pull/67531#issuecomment-1385353430 for details.
+		// FIXME: Testing Drag and Drop with non-embedded windows would require DisplayServerMock additions
+		// FIXME: Drag and Drop currently doesn't work with embedded Windows and SubViewports - not testing.
+		// See https://github.com/godotengine/godot/issues/28522 for example.
+		int min_grab_movement = 11;
+		SUBCASE("[Viewport][GuiInputEvent] Drag from one Control to another in the same viewport.") {
+			SUBCASE("[Viewport][GuiInputEvent] Perform successful Drag and Drop on a different Control.") {
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+
+				SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(min_grab_movement, 0), MouseButtonMask::LEFT, Key::NONE);
+				CHECK(root->gui_is_dragging());
+
+				// Move above a Control, that is a Drop target and allows dropping at this point.
+				SEND_GUI_MOUSE_MOTION_EVENT(on_d, MouseButtonMask::LEFT, Key::NONE);
+				CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_CAN_DROP);
+
+				CHECK(root->gui_is_dragging());
+				CHECK_FALSE(root->gui_is_drag_successful());
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+				CHECK(root->gui_is_drag_successful());
+				CHECK((StringName)node_d->drag_data == SNAME("Drag Data"));
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Perform unsuccessful drop on Control.") {
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+
+				// Move, but don't trigger DnD yet.
+				SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(0, min_grab_movement - 1), MouseButtonMask::LEFT, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+
+				// Move and trigger DnD.
+				SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(0, min_grab_movement), MouseButtonMask::LEFT, Key::NONE);
+				CHECK(root->gui_is_dragging());
+
+				// Move above a Control, that is not a Drop target.
+				SEND_GUI_MOUSE_MOTION_EVENT(on_a, MouseButtonMask::LEFT, Key::NONE);
+				CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_FORBIDDEN);
+
+				// Move above a Control, that is a Drop target, but has disallowed this point.
+				SEND_GUI_MOUSE_MOTION_EVENT(on_d + Point2i(20, 0), MouseButtonMask::LEFT, Key::NONE);
+				CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_FORBIDDEN);
+				CHECK(root->gui_is_dragging());
+
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d + Point2i(20, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+				CHECK_FALSE(root->gui_is_drag_successful());
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Perform unsuccessful drop on No-Control.") {
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+
+				// Move, but don't trigger DnD yet.
+				SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(min_grab_movement - 1, 0), MouseButtonMask::LEFT, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+
+				// Move and trigger DnD.
+				SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(min_grab_movement, 0), MouseButtonMask::LEFT, Key::NONE);
+				CHECK(root->gui_is_dragging());
+
+				// Move away from Controls.
+				SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::LEFT, Key::NONE);
+				CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_ARROW); // This could also be CURSOR_FORBIDDEN.
+
+				CHECK(root->gui_is_dragging());
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_background, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+				CHECK_FALSE(root->gui_is_drag_successful());
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Perform unsuccessful drop outside of window.") {
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+
+				// Move and trigger DnD.
+				SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(min_grab_movement, 0), MouseButtonMask::LEFT, Key::NONE);
+				CHECK(root->gui_is_dragging());
+
+				SEND_GUI_MOUSE_MOTION_EVENT(on_d, MouseButtonMask::LEFT, Key::NONE);
+				CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_CAN_DROP);
+
+				// Move outside of window.
+				SEND_GUI_MOUSE_MOTION_EVENT(on_outside, MouseButtonMask::LEFT, Key::NONE);
+				CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_ARROW);
+				CHECK(root->gui_is_dragging());
+
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_outside, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+				CHECK_FALSE(root->gui_is_drag_successful());
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Drag and Drop doesn't work with other Mouse Buttons than LMB.") {
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::MIDDLE, MouseButtonMask::MIDDLE, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+
+				SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(min_grab_movement, 0), MouseButtonMask::MIDDLE, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_a, MouseButton::MIDDLE, MouseButtonMask::NONE, Key::NONE);
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Drag and Drop parent propagation.") {
+				Node2D *node_aa = memnew(Node2D);
+				Control *node_aaa = memnew(Control);
+				Node2D *node_dd = memnew(Node2D);
+				Control *node_ddd = memnew(Control);
+				node_aaa->set_size(Size2i(10, 10));
+				node_aaa->set_position(Point2i(0, 5));
+				node_ddd->set_size(Size2i(10, 10));
+				node_ddd->set_position(Point2i(0, 5));
+				node_a->add_child(node_aa);
+				node_aa->add_child(node_aaa);
+				node_d->add_child(node_dd);
+				node_dd->add_child(node_ddd);
+				Point2i on_aaa = on_a + Point2i(-2, 2);
+				Point2i on_ddd = on_d + Point2i(-2, 2);
+
+				SUBCASE("[Viewport][GuiInputEvent] Drag and Drop propagation to parent Controls.") {
+					node_aaa->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+					node_ddd->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+					SEND_GUI_MOUSE_BUTTON_EVENT(on_aaa, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+					CHECK_FALSE(root->gui_is_dragging());
+
+					SEND_GUI_MOUSE_MOTION_EVENT(on_aaa + Point2i(0, min_grab_movement), MouseButtonMask::LEFT, Key::NONE);
+					CHECK(root->gui_is_dragging());
+
+					SEND_GUI_MOUSE_MOTION_EVENT(on_ddd, MouseButtonMask::LEFT, Key::NONE);
+
+					CHECK(root->gui_is_dragging());
+					CHECK_FALSE(root->gui_is_drag_successful());
+					SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_ddd, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+					CHECK_FALSE(root->gui_is_dragging());
+					CHECK(root->gui_is_drag_successful());
+
+					node_aaa->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+					node_ddd->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+				}
+
+				SUBCASE("[Viewport][GuiInputEvent] Drag and Drop grab-propagation stopped by Top Level.") {
+					node_aaa->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+					node_aaa->set_as_top_level(true);
+
+					SEND_GUI_MOUSE_BUTTON_EVENT(on_aaa, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+					CHECK_FALSE(root->gui_is_dragging());
+
+					SEND_GUI_MOUSE_MOTION_EVENT(on_aaa + Point2i(0, min_grab_movement), MouseButtonMask::LEFT, Key::NONE);
+					CHECK_FALSE(root->gui_is_dragging());
+
+					SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_background, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+					node_aaa->set_as_top_level(false);
+					node_aaa->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+				}
+
+				SUBCASE("[Viewport][GuiInputEvent] Drag and Drop target-propagation stopped by Top Level.") {
+					node_aaa->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+					node_ddd->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+					node_ddd->set_as_top_level(true);
+					node_ddd->set_position(Point2i(30, 100));
+
+					SEND_GUI_MOUSE_BUTTON_EVENT(on_aaa, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+					CHECK_FALSE(root->gui_is_dragging());
+
+					SEND_GUI_MOUSE_MOTION_EVENT(on_aaa + Point2i(0, min_grab_movement), MouseButtonMask::LEFT, Key::NONE);
+					CHECK(root->gui_is_dragging());
+
+					SEND_GUI_MOUSE_MOTION_EVENT(Point2i(35, 105), MouseButtonMask::LEFT, Key::NONE);
+
+					CHECK(root->gui_is_dragging());
+					SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(Point2i(35, 105), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+					CHECK_FALSE(root->gui_is_dragging());
+					CHECK_FALSE(root->gui_is_drag_successful());
+
+					node_ddd->set_position(Point2i(0, 5));
+					node_ddd->set_as_top_level(false);
+					node_aaa->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+					node_ddd->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+				}
+
+				SUBCASE("[Viewport][GuiInputEvent] Drag and Drop grab-propagation stopped by non-CanvasItem.") {
+					node_g->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+					SEND_GUI_MOUSE_BUTTON_EVENT(on_g, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+					SEND_GUI_MOUSE_MOTION_EVENT(on_g + Point2i(0, min_grab_movement), MouseButtonMask::LEFT, Key::NONE);
+					CHECK_FALSE(root->gui_is_dragging());
+
+					SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_background, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+					node_g->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+				}
+
+				SUBCASE("[Viewport][GuiInputEvent] Drag and Drop target-propagation stopped by non-CanvasItem.") {
+					node_g->set_mouse_filter(Control::MOUSE_FILTER_PASS);
+
+					SEND_GUI_MOUSE_BUTTON_EVENT(on_a - Point2i(1, 1), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); // Offset for node_aaa.
+					SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(0, min_grab_movement), MouseButtonMask::LEFT, Key::NONE);
+					CHECK(root->gui_is_dragging());
+
+					SEND_GUI_MOUSE_MOTION_EVENT(on_g, MouseButtonMask::LEFT, Key::NONE);
+					SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_g, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+					CHECK_FALSE(root->gui_is_dragging());
+
+					node_g->set_mouse_filter(Control::MOUSE_FILTER_STOP);
+				}
+
+				memdelete(node_ddd);
+				memdelete(node_dd);
+				memdelete(node_aaa);
+				memdelete(node_aa);
+			}
+
+			SUBCASE("[Viewport][GuiInputEvent] Force Drag and Drop.") {
+				SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+				node_a->force_drag(SNAME("Drag Data"), nullptr);
+				CHECK(root->gui_is_dragging());
+
+				SEND_GUI_MOUSE_MOTION_EVENT(on_d, MouseButtonMask::NONE, Key::NONE);
+
+				// Force Drop doesn't get triggered by mouse Buttons other than LMB.
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_d, MouseButton::RIGHT, MouseButtonMask::RIGHT, Key::NONE);
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_a, MouseButton::RIGHT, MouseButtonMask::NONE, Key::NONE);
+				CHECK(root->gui_is_dragging());
+
+				// Force Drop with LMB-Down.
+				SEND_GUI_MOUSE_BUTTON_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
+				CHECK_FALSE(root->gui_is_dragging());
+				CHECK(root->gui_is_drag_successful());
+
+				SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
+			}
+		}
+	}
+
+	memdelete(node_g);
+	memdelete(node_f);
+	memdelete(node_e);
+	memdelete(node_d);
+	memdelete(node_c);
+	memdelete(node_b);
+	memdelete(node_a);
+}
+
+} // namespace TestViewport
+
+#endif // TEST_VIEWPORT_H

+ 0 - 1
tests/test_macros.h

@@ -209,7 +209,6 @@ int register_test_command(String p_command, TestFunc p_function);
 		event.instantiate();                                          \
 		event->set_position(m_screen_pos);                            \
 		event->set_button_mask(m_mask);                               \
-		event->set_relative(Vector2(10, 10));                         \
 		_UPDATE_EVENT_MODIFERS(event, m_modifers);                    \
 		_SEND_DISPLAYSERVER_EVENT(event);                             \
 		MessageQueue::get_singleton()->flush();                       \

+ 4 - 0
tests/test_main.cpp

@@ -101,6 +101,7 @@
 #include "tests/scene/test_sprite_frames.h"
 #include "tests/scene/test_text_edit.h"
 #include "tests/scene/test_theme.h"
+#include "tests/scene/test_viewport.h"
 #include "tests/scene/test_visual_shader.h"
 #include "tests/servers/test_text_server.h"
 #include "tests/test_validate_testing.h"
@@ -233,6 +234,9 @@ struct GodotTestCaseListener : public doctest::IReporter {
 
 			memnew(SceneTree);
 			SceneTree::get_singleton()->initialize();
+			if (!DisplayServer::get_singleton()->has_feature(DisplayServer::Feature::FEATURE_SUBWINDOWS)) {
+				SceneTree::get_singleton()->get_root()->set_embedding_subwindows(true);
+			}
 			return;
 		}