Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider children Control nodes for mouse-enter/exit notifications #83276

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions doc/classes/Control.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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 a 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 (or a 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]
Expand Down Expand Up @@ -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.
Sent when the mouse cursor enters the control's (or a 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 (or a 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">
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">
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.
See also [constant NOTIFICATION_MOUSE_EXIT].
</constant>
<constant name="NOTIFICATION_FOCUS_ENTER" value="43">
Sent when the node grabs focus.
Expand Down
2 changes: 2 additions & 0 deletions scene/gui/control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3568,6 +3568,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);
Expand Down
2 changes: 2 additions & 0 deletions scene/gui/control.h
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ class Control : public CanvasItem {
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.
Expand Down
213 changes: 204 additions & 9 deletions scene/main/viewport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2408,8 +2408,9 @@ 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();
bool reevaluate = _send_mouse_exit_notifications(p_control);
if (reevaluate) {
_reevaluate_mouse_over();
}
if (gui.drag_mouse_over == p_control) {
gui.drag_mouse_over = nullptr;
Expand All @@ -2431,8 +2432,9 @@ 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();
bool reevaluate = _send_mouse_exit_notifications(p_control);
if (reevaluate) {
callable_mp(this, &Viewport::_reevaluate_mouse_over).call_deferred();
}
if (gui.drag_mouse_over == p_control) {
gui.drag_mouse_over = nullptr;
Expand All @@ -2442,6 +2444,60 @@ void Viewport::_gui_remove_control(Control *p_control) {
}
}

bool Viewport::_send_mouse_exit_notifications(Control *p_control) {
// Send exit notifications for p_control and child controls.
Control *target = p_control;
if (!target) {
// Send notification to all hovered Control nodes.
if (gui.mouse_over_interval_top) {
// Notifdication for all hovered nodes.
target = gui.mouse_over_interval_top;
} else if (gui.mouse_over) {
// Only single Control is hovered.
target = gui.mouse_over;
} else {
// Nothing to do.
return false;
}
}

if (target == gui.mouse_over_interval_top ||
target == gui.mouse_over ||
target == gui.mouse_over_interval_bottom ||
(gui.mouse_over_interval_top && gui.mouse_over_interval_top->is_ancestor_of(target) &&
((gui.mouse_over && target->is_ancestor_of(gui.mouse_over)) || (gui.mouse_over_interval_bottom && target->is_ancestor_of(gui.mouse_over_interval_bottom))))) {
bool reevaluate = false;
// Target Control is hovered.
Control *control = gui.mouse_over;
if (gui.mouse_over) {
_drop_mouse_over();
reevaluate = true;
} else {
control = gui.mouse_over_interval_bottom;
}

while (control && control != target) {
if (control->is_inside_tree()) {
control->notification(Control::NOTIFICATION_MOUSE_EXIT);
}
control = control->get_parent_control();
reevaluate = true;
}
if (control) {
if (control->is_inside_tree()) {
control->notification(Control::NOTIFICATION_MOUSE_EXIT);
}
reevaluate = true;
}
gui.mouse_over_interval_bottom = target->get_parent_control();
if (target == gui.mouse_over_interval_top) {
gui.mouse_over_interval_top = nullptr;
}
return reevaluate;
}
return false;
}

Window *Viewport::get_base_window() const {
ERR_READ_THREAD_GUARD_V(nullptr);
ERR_FAIL_COND_V(!is_inside_tree(), nullptr);
Expand Down Expand Up @@ -2987,6 +3043,14 @@ bool Viewport::_sub_windows_forward_input(const Ref<InputEvent> &p_event) {
return true;
}

void Viewport::_reevaluate_mouse_over() {
if (!gui.mouse_in_viewport) {
return;
}
// Send mouse entered exited notifications for current viewport.
_update_mouse_over(gui.last_mouse_pos);
}

void Viewport::_update_mouse_over() {
// Update gui.mouse_over and gui.subwindow_over in all Viewports.
// Send necessary mouse_enter/mouse_exit signals and the MOUSE_ENTER/MOUSE_EXIT notifications for every Viewport in the SceneTree.
Expand Down Expand Up @@ -3032,7 +3096,7 @@ void Viewport::_update_mouse_over(Vector2 p_pos) {

if (swrect_border.has_point(p_pos)) {
if (gui.mouse_over) {
_drop_mouse_over();
_send_mouse_exit_notifications();
} else if (!gui.subwindow_over) {
_drop_physics_mouseover();
}
Expand Down Expand Up @@ -3070,15 +3134,142 @@ void Viewport::_update_mouse_over(Vector2 p_pos) {
Control *over = gui_find_control(p_pos);
bool notify_embedded_viewports = false;
if (over != gui.mouse_over) {
Control *common_ancestor = nullptr;
if (gui.mouse_over) {
// Find the common ancestor of `gui.mouse_over` and `over`.
if (over) {
Control *ancestor = gui.mouse_over;
while (ancestor && ancestor != over && !ancestor->is_ancestor_of(over)) {
if (ancestor->is_set_as_top_level()) {
// Top level Control nodes break the propagation chain.
ancestor = nullptr;
break;
}
ancestor = ancestor->get_parent_control();
}
if (ancestor) {
common_ancestor = ancestor;
}
// Now `common_ancestor` contains an ancestor of gui.mouse_over and over.

// It remains to check, that `common_ancestor` is connected in a chain of Control nodes to `over`.
ancestor = over;
while (ancestor) {
if (ancestor == common_ancestor) {
break;
}
if (ancestor->is_set_as_top_level()) {
// Top level Control nodes break the propagation chain.
ancestor = nullptr;
break;
}
ancestor = ancestor->get_parent_control();
}
if (!ancestor) {
// There is no common ancestor.
common_ancestor = nullptr;
}
}

// Beginning from the previously hovered Control, send mouse exit notifications to Control nodes and their parents,
// until the common ancestor.
Control *control = gui.mouse_over;
_drop_mouse_over();
while (control) {
if (over && control == common_ancestor) {
// Common ancestor of over and gui.mouse_over reached. There is no need to go up the scene tree.
gui.mouse_over_interval_bottom = control;
break;
}

if (control->is_inside_tree()) {
control->notification(Control::NOTIFICATION_MOUSE_EXIT);
}

if (control->is_set_as_top_level()) {
// Top level Control nodes break the propagation chain.
gui.mouse_over_interval_bottom = nullptr;
gui.mouse_over_interval_top = nullptr;
break;
}

// Consider only direct Control parents, but not parents, that are separated by non-Control CanvasItems. (Could be changed to include them.)
control = control->get_parent_control();

if (!control) {
break;
}

if (control->get_mouse_filter() == Control::MOUSE_FILTER_IGNORE) {
// Control nodes with MOUSE_FILTER_IGNORE break the propagation chain.
break;
}

gui.mouse_over_interval_bottom = control;
}
if (!control && gui.mouse_over_interval_top) {
gui.mouse_over_interval_top = nullptr;
}
// Now gui.mouse_over_interval_top and gui.mouse_over_interval_bottom are up to date.
} else {
_drop_physics_mouseover();
}

gui.mouse_over = over;
if (over) {
over->notification(Control::NOTIFICATION_MOUSE_ENTER);
LocalVector<Control *> control_list; // List of Control nodes that should receive mouse enter notifications
control_list.push_back(over);

Control *control = over;
if (control != common_ancestor) {
while (control) {
if (control->is_set_as_top_level()) {
// Top level Control nodes break the propagation chain.
break;
}

// Consider only direct Control parents, but not parents, that are separated by non-Control CanvasItems. (Could be changed to include them.)
control = control->get_parent_control();

if (!control) {
break;
}

if (control->get_mouse_filter() == Control::MOUSE_FILTER_IGNORE) {
// Controls that ignore mouse filter break the propagation chain.
break;
}

if (control == common_ancestor) {
// Common ancestor found. No need to go further.
break;
}

control_list.push_back(control);
}
}

gui.mouse_over = over;

// Send Mouse Enter signals to parents first.
for (int i = control_list.size() - 1; i >= 0; i--) {
if (!control_list[i]->is_inside_tree()) {
break;
}
if (control_list[i] != common_ancestor) {
control_list[i]->notification(Control::NOTIFICATION_MOUSE_ENTER);
}
}
if (control_list[0]->is_inside_tree()) {
control_list[0]->notification(Control::NOTIFICATION_MOUSE_ENTER_SELF);
}
if (control_list.size() > 1) {
// Update ancestors necessary.
gui.mouse_over_interval_bottom = control_list[1];
if (!gui.mouse_over_interval_top) {
// Far ancestor is currently not set.
gui.mouse_over_interval_top = control_list[control_list.size() - 1];
}
}
notify_embedded_viewports = true;
}
}
Expand Down Expand Up @@ -3114,7 +3305,7 @@ void Viewport::_mouse_leave_viewport() {
gui.subwindow_over->_mouse_leave_viewport();
gui.subwindow_over = nullptr;
} else if (gui.mouse_over) {
_drop_mouse_over();
_send_mouse_exit_notifications();
}
notification(NOTIFICATION_VP_MOUSE_EXIT);
}
Expand All @@ -3132,7 +3323,11 @@ void Viewport::_drop_mouse_over() {
}
}
if (gui.mouse_over->is_inside_tree()) {
gui.mouse_over->notification(Control::NOTIFICATION_MOUSE_EXIT);
gui.mouse_over->notification(Control::NOTIFICATION_MOUSE_EXIT_SELF);
}
gui.mouse_over_interval_bottom = gui.mouse_over;
if (!gui.mouse_over_interval_top) {
gui.mouse_over_interval_top = gui.mouse_over;
}
gui.mouse_over = nullptr;
}
Expand Down
6 changes: 5 additions & 1 deletion scene/main/viewport.h
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,9 @@ class Viewport : public Node {
Control *mouse_click_grabber = nullptr;
BitField<MouseButtonMask> mouse_focus_mask;
Control *key_focus = nullptr;
Control *mouse_over = nullptr;
Control *mouse_over = nullptr; // Contains the Control node, that the mouse is currently over.
Control *mouse_over_interval_bottom = nullptr; // Contains the deepest node, that has received a NOTIFICATION_MOUSE_ENTER without a NOTIFICATION_MOUSE_ENTER_SELF.
Control *mouse_over_interval_top = nullptr; // Contains the topmost node, that has received a NOTIFICATION_MOUSE_ENTER without a NOTIFICATION_MOUSE_ENTER_SELF.
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;
Expand Down Expand Up @@ -429,6 +431,7 @@ class Viewport : public Node {

void _gui_remove_control(Control *p_control);
void _gui_hide_control(Control *p_control);
bool _send_mouse_exit_notifications(Control *p_control = nullptr);

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);
Expand Down Expand Up @@ -475,6 +478,7 @@ class Viewport : public Node {
void _update_mouse_over();
virtual void _update_mouse_over(Vector2 p_pos);
virtual void _mouse_leave_viewport();
void _reevaluate_mouse_over();

virtual bool _can_consume_input_events() const { return true; }
uint64_t event_count = 0;
Expand Down
Loading
Loading