diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index c8318e1d9db1..3078d3eeed77 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -32,9 +32,9 @@ #include "container.h" #include "core/config/project_settings.h" -#include "core/math/geometry_2d.h" #include "core/os/os.h" #include "core/string/translation_server.h" +#include "scene/gui/scroll_container.h" #include "scene/main/canvas_layer.h" #include "scene/main/window.h" #include "scene/theme/theme_db.h" @@ -2281,18 +2281,9 @@ Control *Control::_get_focus_neighbor(Side p_side, int p_count) { return c; } - real_t dist = 1e7; + real_t dist = 1e14; Control *result = nullptr; - Point2 points[4]; - - Transform2D xform = get_global_transform(); - - points[0] = xform.xform(Point2()); - points[1] = xform.xform(Point2(get_size().x, 0)); - points[2] = xform.xform(get_size()); - points[3] = xform.xform(Point2(0, get_size().y)); - const Vector2 dir[4] = { Vector2(-1, 0), Vector2(0, -1), @@ -2302,18 +2293,73 @@ Control *Control::_get_focus_neighbor(Side p_side, int p_count) { Vector2 vdir = dir[p_side]; - real_t maxd = -1e7; + Rect2 r = get_global_rect(); + real_t begin_d = vdir.dot(r.get_position()); + real_t end_d = vdir.dot(r.get_end()); + real_t maxd = MAX(begin_d, end_d); - for (int i = 0; i < 4; i++) { - real_t d = vdir.dot(points[i]); - if (d > maxd) { - maxd = d; - } - } + Rect2 clamp = Rect2(Point2(-1e7, -1e7), Size2(2e7, 2e7)); + Rect2 result_rect; Node *base = this; while (base) { + ScrollContainer *sc = Object::cast_to(base); + + if (sc) { + Rect2 sc_r = sc->get_global_rect(); + bool follow_focus = sc->is_following_focus(); + + if (result && !follow_focus && !sc_r.intersects(result_rect)) { + result = nullptr; // Skip invisible control. + } + + if (result == nullptr) { + real_t sc_begin_d = vdir.dot(sc_r.get_position()); + real_t sc_end_d = vdir.dot(sc_r.get_end()); + real_t sc_maxd = sc_begin_d; + real_t sc_mind = sc_end_d; + if (sc_begin_d < sc_end_d) { + sc_maxd = sc_end_d; + sc_mind = sc_begin_d; + } + + if (!follow_focus && maxd < sc_mind) { + // Reposition to find visible control. + maxd = sc_mind; + r.set_position(r.get_position() + (sc_mind - maxd) * vdir); + } + + if (follow_focus || sc_maxd > maxd) { + _window_find_focus_neighbor(vdir, base, r, clamp, maxd, dist, &result); + } + + if (result == nullptr) { + // Reposition to search upwards. + maxd = sc_maxd; + r.set_position(r.get_position() + (sc_maxd - maxd) * vdir); + } else { + result_rect = result->get_global_rect(); + if (follow_focus) { + real_t r_begin_d = vdir.dot(result_rect.get_position()); + real_t r_end_d = vdir.dot(result_rect.get_end()); + real_t r_maxd = r_begin_d; + real_t r_mind = r_end_d; + if (r_begin_d < r_end_d) { + r_maxd = r_end_d; + r_mind = r_begin_d; + } + + if (r_maxd > sc_maxd) { + result_rect.set_position(result_rect.get_position() + (sc_maxd - r_maxd) * vdir); + } else if (r_mind < sc_mind) { + result_rect.set_position(result_rect.get_position() + (sc_mind - r_mind) * vdir); + } + } + } + } + } + Control *c = Object::cast_to(base); if (c) { if (c->data.RI) { @@ -2323,11 +2369,15 @@ Control *Control::_get_focus_neighbor(Side p_side, int p_count) { base = base->get_parent(); } + if (result) { + return result; + } + if (!base) { return nullptr; } - _window_find_focus_neighbor(vdir, base, points, maxd, dist, &result); + _window_find_focus_neighbor(vdir, base, r, clamp, maxd, dist, &result); return result; } @@ -2336,72 +2386,76 @@ Control *Control::find_valid_focus_neighbor(Side p_side) const { return const_cast(this)->_get_focus_neighbor(p_side); } -void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, const Point2 *p_points, real_t p_min, real_t &r_closest_dist, Control **r_closest) { +void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, const Rect2 &p_rect, const Rect2 &p_clamp, real_t p_min, real_t &r_closest_dist, Control **r_closest) { if (Object::cast_to(p_at)) { - return; //bye + return; // Bye. } Control *c = Object::cast_to(p_at); - if (c && c != this && c->get_focus_mode() == FOCUS_ALL && c->is_visible_in_tree()) { - Point2 points[4]; - - Transform2D xform = c->get_global_transform(); - - points[0] = xform.xform(Point2()); - points[1] = xform.xform(Point2(c->get_size().x, 0)); - points[2] = xform.xform(c->get_size()); - points[3] = xform.xform(Point2(0, c->get_size().y)); - - // Tie-breaking aims to address situations where a potential focus neighbor's bounding rect - // is right next to the currently focused control (e.g. in BoxContainer with - // separation overridden to 0). This needs specific handling so that the correct - // focus neighbor is selected. - - // Calculate centers of the potential neighbor, currently focused, and closest controls. - Point2 center = xform.xform(0.5 * c->get_size()); - // We only have the points, not an actual reference. - Point2 p_center = 0.25 * (p_points[0] + p_points[1] + p_points[2] + p_points[3]); - Point2 closest_center; - bool should_tiebreak = false; - if (*r_closest != nullptr) { - should_tiebreak = true; - Control *closest = *r_closest; - Transform2D closest_xform = closest->get_global_transform(); - closest_center = closest_xform.xform(0.5 * closest->get_size()); - } - - real_t min = 1e7; + if (c && c != this && c->get_focus_mode() == FOCUS_ALL && p_clamp.intersects(c->get_global_rect())) { + Rect2 r_c = c->get_global_rect(); + r_c = r_c.intersection(p_clamp); + real_t begin_d = p_dir.dot(r_c.get_position()); + real_t end_d = p_dir.dot(r_c.get_end()); + real_t max = MAX(begin_d, end_d); + + // Use max to allow navigation to overlapping controls (for ScrollContainer case). + if (max > (p_min + CMP_EPSILON)) { + // Calculate the shortest distance. (No shear transform) + // Convert so that C falls in the first quadrant of c (as origin) for easy calculation. + // | ------------- + // | | | | + // | |-----C-----| + // ----|---a | | | + // | | | b------------ + // -|---c---|-----------------------> + // | | | + // ----|---- + // cC = ca + ab + bC + // The shortest distance is the vector ab's length or its positive projection length. + + Vector2 cC_origin = r_c.get_center() - p_rect.get_center(); + Vector2 cC = cC_origin.abs(); // Converted to fall in the first quadrant of c. + + Vector2 ab = cC - 0.5 * r_c.get_size() - 0.5 * p_rect.get_size(); + + real_t min_d = 0.0; + if (ab.x > 0.0) { + min_d += ab.x * ab.x; + } + if (ab.y > 0.0) { + min_d += ab.y * ab.y; + } - for (int i = 0; i < 4; i++) { - real_t d = p_dir.dot(points[i]); - if (d < min) { - min = d; + if (min_d < r_closest_dist || *r_closest == nullptr) { + r_closest_dist = min_d; + *r_closest = c; + } else if (min_d == r_closest_dist) { + // Tie-breaking aims to address situations where a potential focus neighbor's bounding rect + // is right next to the currently focused control (e.g. in BoxContainer with + // separation overridden to 0). This needs specific handling so that the correct + // focus neighbor is selected. + + Point2 p_center = p_rect.get_center(); + Control *closest = *r_closest; + Point2 closest_center = closest->get_global_rect().get_center(); + + // Tie-break in favor of the control most aligned with p_dir. + if (ABS(p_dir.cross(cC_origin)) < ABS(p_dir.cross(closest_center - p_center))) { + *r_closest = c; + } } } + } - if (min > (p_min - CMP_EPSILON)) { - for (int i = 0; i < 4; i++) { - Vector2 la = p_points[i]; - Vector2 lb = p_points[(i + 1) % 4]; - - for (int j = 0; j < 4; j++) { - Vector2 fa = points[j]; - Vector2 fb = points[(j + 1) % 4]; - - Vector2 pa, pb; - real_t d = Geometry2D::get_closest_points_between_segments(la, lb, fa, fb, pa, pb); - if (d < r_closest_dist) { - r_closest_dist = d; - *r_closest = c; - } else if (should_tiebreak && d == r_closest_dist) { - // Tie-break in favor of the control most aligned with p_dir. - if (p_dir.dot((center - p_center).normalized()) > p_dir.dot((closest_center - p_center).normalized())) { - r_closest_dist = d; - *r_closest = c; - } - } - } + ScrollContainer *sc = Object::cast_to(c); + Rect2 intersection = p_clamp; + if (sc) { + if (!sc->is_following_focus() || !sc->is_ancestor_of(this)) { + intersection = p_clamp.intersection(sc->get_global_rect()); + if (!intersection.has_area()) { + return; } } } @@ -2409,10 +2463,18 @@ void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, cons for (int i = 0; i < p_at->get_child_count(); i++) { Node *child = p_at->get_child(i); Control *childc = Object::cast_to(child); - if (childc && childc->data.RI) { - continue; //subwindow, ignore + if (childc) { + if (childc->data.RI) { + continue; // Subwindow, ignore. + } + if (!childc->is_visible_in_tree()) { + continue; // Skip invisible node trees. + } + if (Object::cast_to(childc) && childc->is_ancestor_of(this)) { + continue; // Already searched in it, skip it. + } } - _window_find_focus_neighbor(p_dir, p_at->get_child(i), p_points, p_min, r_closest_dist, r_closest); + _window_find_focus_neighbor(p_dir, child, p_rect, intersection, p_min, r_closest_dist, r_closest); } } diff --git a/scene/gui/control.h b/scene/gui/control.h index 23190381f3f4..acc349034f62 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -314,7 +314,7 @@ class Control : public CanvasItem { // Focus. - void _window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, const Point2 *p_points, real_t p_min, real_t &r_closest_dist, Control **r_closest); + void _window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, const Rect2 &p_rect, const Rect2 &p_clamp, real_t p_min, real_t &r_closest_dist, Control **r_closest); Control *_get_focus_neighbor(Side p_side, int p_count = 0); // Theming.