summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/object/script_language.cpp34
-rw-r--r--core/object/script_language.h7
-rw-r--r--core/object/worker_thread_pool.cpp9
-rw-r--r--core/object/worker_thread_pool.h1
-rw-r--r--doc/classes/CanvasItem.xml6
-rw-r--r--doc/classes/Control.xml23
-rw-r--r--doc/classes/EditorSpinSlider.xml3
-rw-r--r--doc/classes/InputEventKey.xml20
-rw-r--r--doc/classes/ResourceSaver.xml1
-rw-r--r--editor/editor_autoload_settings.cpp6
-rw-r--r--editor/editor_properties.cpp6
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs8
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs8
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs8
-rw-r--r--scene/gui/control.cpp12
-rw-r--r--scene/gui/control.h2
-rw-r--r--scene/gui/label.cpp15
-rw-r--r--scene/main/canvas_item.cpp4
-rw-r--r--scene/main/viewport.cpp163
-rw-r--r--scene/main/viewport.h5
-rw-r--r--tests/scene/test_viewport.h644
21 files changed, 928 insertions, 57 deletions
diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp
index 011f4203ea..2bdbfb5ad1 100644
--- a/core/object/script_language.cpp
+++ b/core/object/script_language.cpp
@@ -39,10 +39,11 @@
ScriptLanguage *ScriptServer::_languages[MAX_LANGUAGES];
int ScriptServer::_language_count = 0;
+bool ScriptServer::languages_ready = false;
+Mutex ScriptServer::languages_mutex;
bool ScriptServer::scripting_enabled = true;
bool ScriptServer::reload_scripts_on_save = false;
-SafeFlag ScriptServer::languages_finished; // Used until GH-76581 is fixed properly.
ScriptEditRequestFunction ScriptServer::edit_request_func = nullptr;
void Script::_notification(int p_what) {
@@ -160,12 +161,13 @@ bool ScriptServer::is_scripting_enabled() {
}
ScriptLanguage *ScriptServer::get_language(int p_idx) {
+ MutexLock lock(languages_mutex);
ERR_FAIL_INDEX_V(p_idx, _language_count, nullptr);
-
return _languages[p_idx];
}
Error ScriptServer::register_language(ScriptLanguage *p_language) {
+ MutexLock lock(languages_mutex);
ERR_FAIL_NULL_V(p_language, ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V_MSG(_language_count >= MAX_LANGUAGES, ERR_UNAVAILABLE, "Script languages limit has been reach, cannot register more.");
for (int i = 0; i < _language_count; i++) {
@@ -179,6 +181,8 @@ Error ScriptServer::register_language(ScriptLanguage *p_language) {
}
Error ScriptServer::unregister_language(const ScriptLanguage *p_language) {
+ MutexLock lock(languages_mutex);
+
for (int i = 0; i < _language_count; i++) {
if (_languages[i] == p_language) {
_language_count--;
@@ -219,17 +223,31 @@ void ScriptServer::init_languages() {
}
}
- for (int i = 0; i < _language_count; i++) {
- _languages[i]->init();
+ {
+ MutexLock lock(languages_mutex);
+
+ for (int i = 0; i < _language_count; i++) {
+ _languages[i]->init();
+ }
+
+ languages_ready = true;
}
}
void ScriptServer::finish_languages() {
+ MutexLock lock(languages_mutex);
+
for (int i = 0; i < _language_count; i++) {
_languages[i]->finish();
}
global_classes_clear();
- languages_finished.set();
+
+ languages_ready = false;
+}
+
+bool ScriptServer::are_languages_initialized() {
+ MutexLock lock(languages_mutex);
+ return languages_ready;
}
void ScriptServer::set_reload_scripts_on_save(bool p_enable) {
@@ -241,7 +259,8 @@ bool ScriptServer::is_reload_scripts_on_save_enabled() {
}
void ScriptServer::thread_enter() {
- if (!languages_finished.is_set()) {
+ MutexLock lock(languages_mutex);
+ if (!languages_ready) {
return;
}
for (int i = 0; i < _language_count; i++) {
@@ -250,7 +269,8 @@ void ScriptServer::thread_enter() {
}
void ScriptServer::thread_exit() {
- if (!languages_finished.is_set()) {
+ MutexLock lock(languages_mutex);
+ if (!languages_ready) {
return;
}
for (int i = 0; i < _language_count; i++) {
diff --git a/core/object/script_language.h b/core/object/script_language.h
index 3e4041d173..85e64c8d62 100644
--- a/core/object/script_language.h
+++ b/core/object/script_language.h
@@ -52,9 +52,11 @@ class ScriptServer {
static ScriptLanguage *_languages[MAX_LANGUAGES];
static int _language_count;
+ static bool languages_ready;
+ static Mutex languages_mutex;
+
static bool scripting_enabled;
static bool reload_scripts_on_save;
- static SafeFlag languages_finished; // Used until GH-76581 is fixed properly.
struct GlobalScriptClass {
StringName language;
@@ -98,8 +100,7 @@ public:
static void init_languages();
static void finish_languages();
-
- static bool are_languages_finished() { return languages_finished.is_set(); }
+ static bool are_languages_initialized();
};
class PlaceHolderScriptInstance;
diff --git a/core/object/worker_thread_pool.cpp b/core/object/worker_thread_pool.cpp
index 2fcd0867e6..784acadab4 100644
--- a/core/object/worker_thread_pool.cpp
+++ b/core/object/worker_thread_pool.cpp
@@ -30,6 +30,7 @@
#include "worker_thread_pool.h"
+#include "core/object/script_language.h"
#include "core/os/os.h"
#include "core/os/thread_safe.h"
@@ -60,6 +61,14 @@ void WorkerThreadPool::_process_task(Task *p_task) {
set_current_thread_safe_for_nodes(false);
pool_thread_index = thread_ids[Thread::get_caller_id()];
ThreadData &curr_thread = threads[pool_thread_index];
+ // Since the WorkerThreadPool is started before the script server,
+ // its pre-created threads can't have ScriptServer::thread_enter() called on them early.
+ // Therefore, we do it late at the first opportunity, so in case the task
+ // about to be run uses scripting, guarantees are held.
+ if (!curr_thread.ready_for_scripting && ScriptServer::are_languages_initialized()) {
+ ScriptServer::thread_enter();
+ curr_thread.ready_for_scripting = true;
+ }
task_mutex.lock();
p_task->pool_thread_index = pool_thread_index;
if (low_priority) {
diff --git a/core/object/worker_thread_pool.h b/core/object/worker_thread_pool.h
index d4d9387765..f323a979f7 100644
--- a/core/object/worker_thread_pool.h
+++ b/core/object/worker_thread_pool.h
@@ -106,6 +106,7 @@ private:
uint32_t index;
Thread thread;
Task *current_low_prio_task = nullptr;
+ bool ready_for_scripting = false;
};
TightLocalVector<ThreadData> threads;
diff --git a/doc/classes/CanvasItem.xml b/doc/classes/CanvasItem.xml
index 84efd11b43..5a2df0e8a4 100644
--- a/doc/classes/CanvasItem.xml
+++ b/doc/classes/CanvasItem.xml
@@ -44,7 +44,7 @@
<param index="7" name="antialiased" type="bool" default="false" />
<description>
Draws an unfilled arc between the given angles with a uniform [param color] and [param width] and optional antialiasing (supported only for positive [param width]). The larger the value of [param point_count], the smoother the curve. See also [method draw_circle].
- If [param width] is negative, then the arc is drawn using [constant RenderingServer.PRIMITIVE_LINE_STRIP]. This means that when the CanvasItem is scaled, the arc will remain thin. If this behavior is not desired, then pass a positive [param width] like [code]1.0[/code].
+ If [param width] is negative, it will be ignored and the arc will be drawn using [constant RenderingServer.PRIMITIVE_LINE_STRIP]. This means that when the CanvasItem is scaled, the arc will remain thin. If this behavior is not desired, then pass a positive [param width] like [code]1.0[/code].
The arc is drawn from [param start_angle] towards the value of [param end_angle] so in clockwise direction if [code]start_angle &lt; end_angle[/code] and counter-clockwise otherwise. Passing the same angles but in reversed order will produce the same arc. If absolute difference of [param start_angle] and [param end_angle] is greater than [constant @GDScript.TAU] radians, then a full circle arc is drawn (i.e. arc will not overlap itself).
</description>
</method>
@@ -246,7 +246,7 @@
<param index="3" name="antialiased" type="bool" default="false" />
<description>
Draws interconnected line segments with a uniform [param color] and [param width] and optional antialiasing (supported only for positive [param width]). When drawing large amounts of lines, this is faster than using individual [method draw_line] calls. To draw disconnected lines, use [method draw_multiline] instead. See also [method draw_polygon].
- If [param width] is negative, the polyline is drawn using [constant RenderingServer.PRIMITIVE_LINE_STRIP]. This means that when the CanvasItem is scaled, the polyline will remain thin. If this behavior is not desired, then pass a positive [param width] like [code]1.0[/code].
+ If [param width] is negative, it will be ignored and the polyline will be drawn using [constant RenderingServer.PRIMITIVE_LINE_STRIP]. This means that when the CanvasItem is scaled, the polyline will remain thin. If this behavior is not desired, then pass a positive [param width] like [code]1.0[/code].
</description>
</method>
<method name="draw_polyline_colors">
@@ -257,7 +257,7 @@
<param index="3" name="antialiased" type="bool" default="false" />
<description>
Draws interconnected line segments with a uniform [param width], point-by-point coloring, and optional antialiasing (supported only for positive [param width]). Colors assigned to line points match by index between [param points] and [param colors], i.e. each line segment is filled with a gradient between the colors of the endpoints. When drawing large amounts of lines, this is faster than using individual [method draw_line] calls. To draw disconnected lines, use [method draw_multiline_colors] instead. See also [method draw_polygon].
- If [param width] is negative, then the polyline is drawn using [constant RenderingServer.PRIMITIVE_LINE_STRIP]. This means that when the CanvasItem is scaled, the polyline will remain thin. If this behavior is not desired, then pass a positive [param width] like [code]1.0[/code].
+ If [param width] is negative, it will be ignored and the polyline will be drawn using [constant RenderingServer.PRIMITIVE_LINE_STRIP]. This means that when the CanvasItem is scaled, the polyline will remain thin. If this behavior is not desired, then pass a positive [param width] like [code]1.0[/code].
</description>
</method>
<method name="draw_primitive">
diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml
index b5333a045b..a498bbeed3 100644
--- a/doc/classes/Control.xml
+++ b/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.
diff --git a/doc/classes/EditorSpinSlider.xml b/doc/classes/EditorSpinSlider.xml
index 6e5ccb4dd0..0d687ba7f5 100644
--- a/doc/classes/EditorSpinSlider.xml
+++ b/doc/classes/EditorSpinSlider.xml
@@ -5,6 +5,7 @@
</brief_description>
<description>
This [Control] node is used in the editor's Inspector dock to allow editing of numeric values. Can be used with [EditorInspectorPlugin] to recreate the same behavior.
+ If [member step] is [code]1[/code], the [EditorSpinSlider] will display up/down arrows, similar to [SpinBox]. If the [member step] is not [code]1[/code], a slider will be displayed instead.
</description>
<tutorials>
</tutorials>
@@ -14,7 +15,7 @@
</member>
<member name="focus_mode" type="int" setter="set_focus_mode" getter="get_focus_mode" overrides="Control" enum="Control.FocusMode" default="2" />
<member name="hide_slider" type="bool" setter="set_hide_slider" getter="is_hiding_slider" default="false">
- If [code]true[/code], the slider is hidden.
+ If [code]true[/code], the slider and up/down arrows are hidden.
</member>
<member name="label" type="String" setter="set_label" getter="get_label" default="&quot;&quot;">
The text that displays to the left of the value.
diff --git a/doc/classes/InputEventKey.xml b/doc/classes/InputEventKey.xml
index 5c4dc8e65d..48a6804290 100644
--- a/doc/classes/InputEventKey.xml
+++ b/doc/classes/InputEventKey.xml
@@ -79,7 +79,25 @@
</member>
<member name="physical_keycode" type="int" setter="set_physical_keycode" getter="get_physical_keycode" enum="Key" default="0">
Represents the physical location of a key on the 101/102-key US QWERTY keyboard, which corresponds to one of the [enum Key] constants.
- To get a human-readable representation of the [InputEventKey], use [code]OS.get_keycode_string(event.keycode)[/code] where [code]event[/code] is the [InputEventKey].
+ To get a human-readable representation of the [InputEventKey], use [method OS.get_keycode_string] in combination with [method DisplayServer.keyboard_get_keycode_from_physical]:
+ [codeblocks]
+ [gdscript]
+ func _input(event):
+ if event is InputEventKey:
+ var keycode = DisplayServer.keyboard_get_keycode_from_physical(event.physical_keycode)
+ print(OS.get_keycode_string(keycode))
+ [/gdscript]
+ [csharp]
+ public override void _Input(InputEvent @event)
+ {
+ if (@event is InputEventKey inputEventKey)
+ {
+ var keycode = DisplayServer.KeyboardGetKeycodeFromPhysical(inputEventKey.PhysicalKeycode);
+ GD.Print(OS.GetKeycodeString(keycode));
+ }
+ }
+ [/csharp]
+ [/codeblocks]
</member>
<member name="pressed" type="bool" setter="set_pressed" getter="is_pressed" default="false">
If [code]true[/code], the key's state is pressed. If [code]false[/code], the key's state is released.
diff --git a/doc/classes/ResourceSaver.xml b/doc/classes/ResourceSaver.xml
index 7b90781fc5..42c9bd7a3c 100644
--- a/doc/classes/ResourceSaver.xml
+++ b/doc/classes/ResourceSaver.xml
@@ -42,6 +42,7 @@
Saves a resource to disk to the given path, using a [ResourceFormatSaver] that recognizes the resource object. If [param path] is empty, [ResourceSaver] will try to use [member Resource.resource_path].
The [param flags] bitmask can be specified to customize the save behavior using [enum SaverFlags] flags.
Returns [constant OK] on success.
+ [b]Note:[/b] When the project is running, any generated UID associated with the resource will not be saved as the required code is only executed in editor mode.
</description>
</method>
</methods>
diff --git a/editor/editor_autoload_settings.cpp b/editor/editor_autoload_settings.cpp
index 6658669d66..be05bfea68 100644
--- a/editor/editor_autoload_settings.cpp
+++ b/editor/editor_autoload_settings.cpp
@@ -339,8 +339,7 @@ void EditorAutoloadSettings::_autoload_button_pressed(Object *p_item, int p_colu
undo_redo->add_do_property(ProjectSettings::get_singleton(), name, Variant());
undo_redo->add_undo_property(ProjectSettings::get_singleton(), name, GLOBAL_GET(name));
- undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_persisting", name, true);
- undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", order);
+ undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", name, order);
undo_redo->add_do_method(this, "update_autoload");
undo_redo->add_undo_method(this, "update_autoload");
@@ -796,8 +795,7 @@ void EditorAutoloadSettings::autoload_remove(const String &p_name) {
undo_redo->add_do_property(ProjectSettings::get_singleton(), name, Variant());
undo_redo->add_undo_property(ProjectSettings::get_singleton(), name, GLOBAL_GET(name));
- undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_persisting", name, true);
- undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", order);
+ undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", name, order);
undo_redo->add_do_method(this, "update_autoload");
undo_redo->add_undo_method(this, "update_autoload");
diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp
index 7884005ab7..b37710724e 100644
--- a/editor/editor_properties.cpp
+++ b/editor/editor_properties.cpp
@@ -3750,7 +3750,7 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_
case Variant::VECTOR2I: {
EditorPropertyVector2i *editor = memnew(EditorPropertyVector2i(p_wide));
EditorPropertyRangeHint hint = _parse_range_hint(p_hint, p_hint_text, 1, true);
- editor->setup(hint.min, hint.max, 1, true, p_hint == PROPERTY_HINT_LINK, hint.suffix);
+ editor->setup(hint.min, hint.max, 1, false, p_hint == PROPERTY_HINT_LINK, hint.suffix);
return editor;
} break;
@@ -3777,7 +3777,7 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_
case Variant::VECTOR3I: {
EditorPropertyVector3i *editor = memnew(EditorPropertyVector3i(p_wide));
EditorPropertyRangeHint hint = _parse_range_hint(p_hint, p_hint_text, 1, true);
- editor->setup(hint.min, hint.max, 1, true, p_hint == PROPERTY_HINT_LINK, hint.suffix);
+ editor->setup(hint.min, hint.max, 1, false, p_hint == PROPERTY_HINT_LINK, hint.suffix);
return editor;
} break;
@@ -3791,7 +3791,7 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_
case Variant::VECTOR4I: {
EditorPropertyVector4i *editor = memnew(EditorPropertyVector4i);
EditorPropertyRangeHint hint = _parse_range_hint(p_hint, p_hint_text, 1, true);
- editor->setup(hint.min, hint.max, 1, true, p_hint == PROPERTY_HINT_LINK, hint.suffix);
+ editor->setup(hint.min, hint.max, 1, false, p_hint == PROPERTY_HINT_LINK, hint.suffix);
return editor;
} break;
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs
index 4ee452455e..215bb4df8c 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs
@@ -182,8 +182,8 @@ namespace Godot
}
// Constants
- private static readonly Vector2I _min = new Vector2I(int.MinValue, int.MinValue);
- private static readonly Vector2I _max = new Vector2I(int.MaxValue, int.MaxValue);
+ private static readonly Vector2I _minValue = new Vector2I(int.MinValue, int.MinValue);
+ private static readonly Vector2I _maxValue = new Vector2I(int.MaxValue, int.MaxValue);
private static readonly Vector2I _zero = new Vector2I(0, 0);
private static readonly Vector2I _one = new Vector2I(1, 1);
@@ -197,12 +197,12 @@ namespace Godot
/// Min vector, a vector with all components equal to <see cref="int.MinValue"/>. Can be used as a negative integer equivalent of <see cref="Vector2.Inf"/>.
/// </summary>
/// <value>Equivalent to <c>new Vector2I(int.MinValue, int.MinValue)</c>.</value>
- public static Vector2I Min { get { return _min; } }
+ public static Vector2I MinValue { get { return _minValue; } }
/// <summary>
/// Max vector, a vector with all components equal to <see cref="int.MaxValue"/>. Can be used as an integer equivalent of <see cref="Vector2.Inf"/>.
/// </summary>
/// <value>Equivalent to <c>new Vector2I(int.MaxValue, int.MaxValue)</c>.</value>
- public static Vector2I Max { get { return _max; } }
+ public static Vector2I MaxValue { get { return _maxValue; } }
/// <summary>
/// Zero vector, a vector with all components set to <c>0</c>.
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs
index db8ceb30e9..fe74ec8884 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs
@@ -193,8 +193,8 @@ namespace Godot
}
// Constants
- private static readonly Vector3I _min = new Vector3I(int.MinValue, int.MinValue, int.MinValue);
- private static readonly Vector3I _max = new Vector3I(int.MaxValue, int.MaxValue, int.MaxValue);
+ private static readonly Vector3I _minValue = new Vector3I(int.MinValue, int.MinValue, int.MinValue);
+ private static readonly Vector3I _maxValue = new Vector3I(int.MaxValue, int.MaxValue, int.MaxValue);
private static readonly Vector3I _zero = new Vector3I(0, 0, 0);
private static readonly Vector3I _one = new Vector3I(1, 1, 1);
@@ -210,12 +210,12 @@ namespace Godot
/// Min vector, a vector with all components equal to <see cref="int.MinValue"/>. Can be used as a negative integer equivalent of <see cref="Vector3.Inf"/>.
/// </summary>
/// <value>Equivalent to <c>new Vector3I(int.MinValue, int.MinValue, int.MinValue)</c>.</value>
- public static Vector3I Min { get { return _min; } }
+ public static Vector3I MinValue { get { return _minValue; } }
/// <summary>
/// Max vector, a vector with all components equal to <see cref="int.MaxValue"/>. Can be used as an integer equivalent of <see cref="Vector3.Inf"/>.
/// </summary>
/// <value>Equivalent to <c>new Vector3I(int.MaxValue, int.MaxValue, int.MaxValue)</c>.</value>
- public static Vector3I Max { get { return _max; } }
+ public static Vector3I MaxValue { get { return _maxValue; } }
/// <summary>
/// Zero vector, a vector with all components set to <c>0</c>.
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs
index e75e996b04..a0a4393523 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs
@@ -228,8 +228,8 @@ namespace Godot
}
// Constants
- private static readonly Vector4I _min = new Vector4I(int.MinValue, int.MinValue, int.MinValue, int.MinValue);
- private static readonly Vector4I _max = new Vector4I(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue);
+ private static readonly Vector4I _minValue = new Vector4I(int.MinValue, int.MinValue, int.MinValue, int.MinValue);
+ private static readonly Vector4I _maxValue = new Vector4I(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue);
private static readonly Vector4I _zero = new Vector4I(0, 0, 0, 0);
private static readonly Vector4I _one = new Vector4I(1, 1, 1, 1);
@@ -238,12 +238,12 @@ namespace Godot
/// Min vector, a vector with all components equal to <see cref="int.MinValue"/>. Can be used as a negative integer equivalent of <see cref="Vector4.Inf"/>.
/// </summary>
/// <value>Equivalent to <c>new Vector4I(int.MinValue, int.MinValue, int.MinValue, int.MinValue)</c>.</value>
- public static Vector4I Min { get { return _min; } }
+ public static Vector4I MinValue { get { return _minValue; } }
/// <summary>
/// Max vector, a vector with all components equal to <see cref="int.MaxValue"/>. Can be used as an integer equivalent of <see cref="Vector4.Inf"/>.
/// </summary>
/// <value>Equivalent to <c>new Vector4I(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue)</c>.</value>
- public static Vector4I Max { get { return _max; } }
+ public static Vector4I MaxValue { get { return _maxValue; } }
/// <summary>
/// Zero vector, a vector with all components set to <c>0</c>.
diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp
index c7ff5980cb..ed54bd000c 100644
--- a/scene/gui/control.cpp
+++ b/scene/gui/control.cpp
@@ -1653,6 +1653,7 @@ void Control::set_custom_minimum_size(const Size2 &p_custom) {
data.custom_minimum_size = p_custom;
update_minimum_size();
+ update_configuration_warnings();
}
Size2 Control::get_custom_minimum_size() const {
@@ -1831,9 +1832,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 +3578,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 abbdc42fa4..db1bd3a346 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/gui/label.cpp b/scene/gui/label.cpp
index 0d48cb1549..2fbd29b048 100644
--- a/scene/gui/label.cpp
+++ b/scene/gui/label.cpp
@@ -33,6 +33,7 @@
#include "core/config/project_settings.h"
#include "core/string/print_string.h"
#include "core/string/translation.h"
+#include "scene/gui/container.h"
#include "scene/theme/theme_db.h"
#include "servers/text_server.h"
@@ -44,6 +45,7 @@ void Label::set_autowrap_mode(TextServer::AutowrapMode p_mode) {
autowrap_mode = p_mode;
lines_dirty = true;
queue_redraw();
+ update_configuration_warnings();
if (clip || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING) {
update_minimum_size();
@@ -327,6 +329,19 @@ inline void draw_glyph_outline(const Glyph &p_gl, const RID &p_canvas, const Col
PackedStringArray Label::get_configuration_warnings() const {
PackedStringArray warnings = Control::get_configuration_warnings();
+ // FIXME: This is not ideal and the sizing model should be fixed,
+ // but for now we have to warn about this impossible to resolve combination.
+ // See GH-83546.
+ if (is_inside_tree() && get_tree()->get_edited_scene_root() != this) {
+ // If the Label happens to be the root node of the edited scene, we don't need
+ // to check what its parent is. It's going to be some node from the editor tree
+ // and it can be a container, but that makes no difference to the user.
+ Container *parent_container = Object::cast_to<Container>(get_parent_control());
+ if (parent_container && autowrap_mode != TextServer::AUTOWRAP_OFF && get_custom_minimum_size() == Size2()) {
+ warnings.push_back(RTR("Labels with autowrapping enabled must have a custom minimum size configured to work correctly inside a container."));
+ }
+ }
+
// Ensure that the font can render all of the required glyphs.
Ref<Font> font;
if (settings.is_valid()) {
diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp
index a350b97bc8..4ee81e5cb0 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 2b28f21f57..43bdb1395b 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<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) {
diff --git a/scene/main/viewport.h b/scene/main/viewport.h
index 65777c973f..82a9bfc438 100644
--- a/scene/main/viewport.h
+++ b/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; }
diff --git a/tests/scene/test_viewport.h b/tests/scene/test_viewport.h
index 0c53668c6d..1afae66ee0 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);