diff options
| -rw-r--r-- | core/object/script_language.cpp | 34 | ||||
| -rw-r--r-- | core/object/script_language.h | 7 | ||||
| -rw-r--r-- | core/object/worker_thread_pool.cpp | 9 | ||||
| -rw-r--r-- | core/object/worker_thread_pool.h | 1 | ||||
| -rw-r--r-- | doc/classes/CanvasItem.xml | 6 | ||||
| -rw-r--r-- | doc/classes/Control.xml | 23 | ||||
| -rw-r--r-- | doc/classes/EditorSpinSlider.xml | 3 | ||||
| -rw-r--r-- | doc/classes/InputEventKey.xml | 20 | ||||
| -rw-r--r-- | doc/classes/ProjectSettings.xml | 2 | ||||
| -rw-r--r-- | doc/classes/ResourceSaver.xml | 1 | ||||
| -rw-r--r-- | editor/editor_autoload_settings.cpp | 6 | ||||
| -rw-r--r-- | editor/editor_properties.cpp | 6 | ||||
| -rw-r--r-- | modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs | 8 | ||||
| -rw-r--r-- | modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs | 8 | ||||
| -rw-r--r-- | modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs | 8 | ||||
| -rw-r--r-- | scene/gui/control.cpp | 12 | ||||
| -rw-r--r-- | scene/gui/control.h | 2 | ||||
| -rw-r--r-- | scene/gui/label.cpp | 15 | ||||
| -rw-r--r-- | scene/main/canvas_item.cpp | 4 | ||||
| -rw-r--r-- | scene/main/viewport.cpp | 163 | ||||
| -rw-r--r-- | scene/main/viewport.h | 5 | ||||
| -rw-r--r-- | tests/scene/test_viewport.h | 644 |
22 files changed, 929 insertions, 58 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 < 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=""""> 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/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index f63eca94e2..2517b6a47f 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -349,7 +349,7 @@ If [member display/window/vsync/vsync_mode] is [code]Enabled[/code], on monitors with variable refresh rate enabled (G-Sync/FreeSync), using a FPS limit a few frames lower than the monitor's refresh rate will [url=https://blurbusters.com/howto-low-lag-vsync-on/]reduce input lag while avoiding tearing[/url]. If [member display/window/vsync/vsync_mode] is [code]Disabled[/code], limiting the FPS to a high value that can be consistently reached on the system can reduce input lag compared to an uncapped framerate. Since this works by ensuring the GPU load is lower than 100%, this latency reduction is only effective in GPU-bottlenecked scenarios, not CPU-bottlenecked scenarios. See also [member physics/common/physics_ticks_per_second]. - This setting can be overridden using the [code]--max-fps <fps;>[/code] command line argument (including with a value of [code]0[/code] for unlimited framerate). + This setting can be overridden using the [code]--max-fps <fps>[/code] command line argument (including with a value of [code]0[/code] for unlimited framerate). [b]Note:[/b] This property is only read when the project starts. To change the rendering FPS cap at runtime, set [member Engine.max_fps] instead. </member> <member name="audio/buses/channel_disable_threshold_db" type="float" setter="" getter="" default="-60.0"> 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); |
