From 1db512adf012ac0469dc36c6674be2f91df2789d Mon Sep 17 00:00:00 2001 From: Ricardo Subtil Date: Tue, 19 Dec 2023 15:31:32 +0000 Subject: [PATCH] Optimize mouse UI picking on BoxContainer, FlowContainer and GridContainer --- scene/gui/box_container.cpp | 33 ++++++++++++++++++++++++++ scene/gui/box_container.h | 4 ++++ scene/gui/container.cpp | 11 +++++++++ scene/gui/container.h | 2 ++ scene/gui/flow_container.cpp | 45 ++++++++++++++++++++++++++++++++++++ scene/gui/flow_container.h | 6 +++++ scene/gui/grid_container.cpp | 41 ++++++++++++++++++++++++++++++++ scene/gui/grid_container.h | 4 ++++ scene/main/viewport.cpp | 20 +++++++++++++++- 9 files changed, 165 insertions(+), 1 deletion(-) diff --git a/scene/gui/box_container.cpp b/scene/gui/box_container.cpp index 97729b1ac544..d591b3655e0c 100644 --- a/scene/gui/box_container.cpp +++ b/scene/gui/box_container.cpp @@ -199,6 +199,9 @@ void BoxContainer::_resort() { delta = -1; } + cached_children_pos.clear(); + cached_children_idx.clear(); + for (int i = start; i != end; i += delta) { Control *c = Object::cast_to(get_child(i)); if (!c || !c->is_visible_in_tree()) { @@ -237,6 +240,8 @@ void BoxContainer::_resort() { } fit_child_in_rect(c, rect); + cached_children_pos.push_back(vertical ? rect.position.y : rect.position.x); + cached_children_idx.push_back(i); ofs = to; idx++; @@ -288,6 +293,34 @@ Size2 BoxContainer::get_minimum_size() const { return minimum; } +Vector BoxContainer::get_children_at_pos(const Point2 &p_pos) const { + if(cached_children_pos.is_empty()) { + return Vector(); + } + + int coord = vertical ? p_pos.y : p_pos.x; + // Custom binary search; we are searching for the closest value to coord, not an exact one. + int bin_start = 0; + int bin_end = cached_children_pos.size(); + while (bin_start < bin_end) { + int mid = (bin_start + bin_end) / 2; + if (coord < cached_children_pos[mid]) { + bin_end = mid; + } else { + bin_start = mid + 1; + } + } + + int idx = cached_children_idx[MAX(bin_start - 1, 0)]; + + Vector children; + CanvasItem *child = Object::cast_to(get_child(idx)); + if (child) { + children.push_back(child); + } + return children; +} + void BoxContainer::_update_theme_item_cache() { Container::_update_theme_item_cache(); diff --git a/scene/gui/box_container.h b/scene/gui/box_container.h index 31fd13c22f0e..d317b5f75cef 100644 --- a/scene/gui/box_container.h +++ b/scene/gui/box_container.h @@ -51,6 +51,9 @@ class BoxContainer : public Container { int separation = 0; } theme_cache; + Vector cached_children_pos; + Vector cached_children_idx; + void _resort(); protected: @@ -72,6 +75,7 @@ class BoxContainer : public Container { bool is_vertical() const; virtual Size2 get_minimum_size() const override; + virtual Vector get_children_at_pos(const Point2 &p_pos) const override; virtual Vector get_allowed_size_flags_horizontal() const override; virtual Vector get_allowed_size_flags_vertical() const override; diff --git a/scene/gui/container.cpp b/scene/gui/container.cpp index 4e23db4caef5..38e8026c50b5 100644 --- a/scene/gui/container.cpp +++ b/scene/gui/container.cpp @@ -81,6 +81,17 @@ void Container::remove_child_notify(Node *p_child) { queue_sort(); } +Vector Container::get_children_at_pos(const Point2 &p_pos) const { + Vector children; + for (int i = get_child_count() - 1; i >= 0; i--) { + CanvasItem *child = Object::cast_to(get_child(i)); + if (child && child->is_visible()) { + children.push_back(child); + } + } + return children; +} + void Container::_sort_children() { if (!is_inside_tree()) { return; diff --git a/scene/gui/container.h b/scene/gui/container.h index 94c3c540d72b..bc60b415ab20 100644 --- a/scene/gui/container.h +++ b/scene/gui/container.h @@ -32,6 +32,7 @@ #define CONTAINER_H #include "scene/gui/control.h" +#include "core/templates/vector.h" class Container : public Control { GDCLASS(Container, Control); @@ -59,6 +60,7 @@ class Container : public Control { }; void fit_child_in_rect(Control *p_child, const Rect2 &p_rect); + virtual Vector get_children_at_pos(const Point2 &p_pos) const; virtual Vector get_allowed_size_flags_horizontal() const; virtual Vector get_allowed_size_flags_vertical() const; diff --git a/scene/gui/flow_container.cpp b/scene/gui/flow_container.cpp index 12ce6e17cb75..1afda07f1388 100644 --- a/scene/gui/flow_container.cpp +++ b/scene/gui/flow_container.cpp @@ -57,6 +57,9 @@ void FlowContainer::_resort() { int current_container_size = vertical ? get_rect().size.y : get_rect().size.x; int children_in_current_line = 0; + cached_children_pos.clear(); + cached_children_idx.clear(); + // First pass for line wrapping and minimum size calculation. for (int i = 0; i < get_child_count(); i++) { Control *child = Object::cast_to(get_child(i)); @@ -128,6 +131,9 @@ void FlowContainer::_resort() { ofs.x = 0; ofs.y = 0; + cached_children_pos.push_back(0); // First child + cached_children_idx.push_back(0); + for (int i = 0; i < get_child_count(); i++) { Control *child = Object::cast_to(get_child(i)); if (!child || !child->is_visible()) { @@ -145,10 +151,13 @@ void FlowContainer::_resort() { if (vertical) { ofs.x += line_data.min_line_height + theme_cache.h_separation; ofs.y = 0; + cached_children_pos.push_back(ofs.x); } else { ofs.x = 0; ofs.y += line_data.min_line_height + theme_cache.v_separation; + cached_children_pos.push_back(ofs.y); } + cached_children_idx.push_back(i); line_data = lines_data[current_line_idx]; } @@ -245,6 +254,42 @@ Size2 FlowContainer::get_minimum_size() const { return minimum; } +Vector FlowContainer::get_children_at_pos(const Point2 &p_pos) const +{ + if (cached_children_pos.is_empty()) { + return Vector(); + } + + int coord = vertical ? p_pos.x : p_pos.y; + // Custom binary search; we are searching for the closest value to coord, not an exact one. + int bin_start = 0; + int bin_end = cached_children_pos.size(); + while(bin_start < bin_end) { + int mid = (bin_start + bin_end) / 2; + if(coord < cached_children_pos[mid]) { + bin_end = mid; + } else { + bin_start = mid + 1; + } + } + + int start_idx = cached_children_idx[MAX(bin_start - 1, 0)]; + int end_idx = cached_children_idx[MIN(bin_end, cached_children_idx.size() - 1)]; + + if(start_idx == end_idx) { + end_idx = get_child_count(); + } + + Vector children; + for(end_idx--; end_idx >= start_idx; end_idx--) { + CanvasItem *child = Object::cast_to(get_child(end_idx)); + if (child && child->is_visible()) { + children.push_back(child); + } + } + return children; +} + Vector FlowContainer::get_allowed_size_flags_horizontal() const { Vector flags; flags.append(SIZE_FILL); diff --git a/scene/gui/flow_container.h b/scene/gui/flow_container.h index 4535601fc3a8..30bb2e065a0f 100644 --- a/scene/gui/flow_container.h +++ b/scene/gui/flow_container.h @@ -32,6 +32,7 @@ #define FLOW_CONTAINER_H #include "scene/gui/container.h" +#include "core/templates/vector.h" class FlowContainer : public Container { GDCLASS(FlowContainer, Container); @@ -57,6 +58,10 @@ class FlowContainer : public Container { void _resort(); + Vector cached_children_pos; + Vector cached_children_idx; + Vector cached_visible_children; + protected: bool is_fixed = false; @@ -76,6 +81,7 @@ class FlowContainer : public Container { bool is_vertical() const; virtual Size2 get_minimum_size() const override; + virtual Vector get_children_at_pos(const Point2& p_pos) const override; virtual Vector get_allowed_size_flags_horizontal() const override; virtual Vector get_allowed_size_flags_vertical() const override; diff --git a/scene/gui/grid_container.cpp b/scene/gui/grid_container.cpp index 28f86369a249..032036e41fcc 100644 --- a/scene/gui/grid_container.cpp +++ b/scene/gui/grid_container.cpp @@ -190,6 +190,11 @@ void GridContainer::_notification(int p_what) { } valid_controls_index = 0; + cached_children_pos.clear(); + cached_children_idx.clear(); + + cached_children_pos.push_back(0); + cached_children_idx.push_back(0); for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to(get_child(i)); if (!c || !c->is_visible_in_tree()) { @@ -212,6 +217,8 @@ void GridContainer::_notification(int p_what) { // Apply the remaining pixel of the previous row. row_ofs++; } + cached_children_pos.push_back(row_ofs); + cached_children_idx.push_back(i); } } @@ -320,4 +327,38 @@ Size2 GridContainer::get_minimum_size() const { return ms; } +Vector GridContainer::get_children_at_pos(const Point2 &p_pos) const { + if (cached_children_pos.is_empty()) { + return Vector(); + } + + // Custom binary search; we are searching for the closest value to coord, not an exact one. + int bin_start = 0; + int bin_end = cached_children_pos.size(); + while (bin_start < bin_end) { + int mid = (bin_start + bin_end) / 2; + if (p_pos.y < cached_children_pos[mid]) { + bin_end = mid; + } else { + bin_start = mid + 1; + } + } + + int start_idx = cached_children_idx[MAX(bin_start - 1, 0)]; + int end_idx = cached_children_idx[MIN(bin_end, cached_children_idx.size() - 1)]; + + if (start_idx == end_idx) { + end_idx = get_child_count(); + } + + Vector children; + for (end_idx--; end_idx >= start_idx; end_idx--) { + CanvasItem *child = Object::cast_to(get_child(end_idx)); + if (child && child->is_visible()) { + children.push_back(child); + } + } + return children; +} + GridContainer::GridContainer() {} diff --git a/scene/gui/grid_container.h b/scene/gui/grid_container.h index 6fe944e9a494..c5cf8e89f08d 100644 --- a/scene/gui/grid_container.h +++ b/scene/gui/grid_container.h @@ -43,6 +43,9 @@ class GridContainer : public Container { int v_separation = 0; } theme_cache; + Vector cached_children_pos; + Vector cached_children_idx; + protected: virtual void _update_theme_item_cache() override; @@ -53,6 +56,7 @@ class GridContainer : public Container { void set_columns(int p_columns); int get_columns() const; virtual Size2 get_minimum_size() const override; + virtual Vector get_children_at_pos(const Point2 &p_pos) const override; GridContainer(); }; diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index a78c520d584f..e5b7ee525274 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -1681,8 +1681,26 @@ Control *Viewport::_gui_find_control_at_pos(CanvasItem *p_node, const Point2 &p_ } Control *c = Object::cast_to(p_node); + Point2 point = matrix.affine_inverse().xform(p_global); + + if (!c || !c->is_clipping_contents() || c->has_point(point)) { + // If node is a Container, it can optimize the search at the given position. + Container *cnt = Object::cast_to(p_node); + if (cnt && cnt->has_point(point)) { + Vector children = cnt->get_children_at_pos(point); + for (CanvasItem *ci : children) { + if (ci->is_set_as_top_level()) { + continue; + } + + Control *ret = _gui_find_control_at_pos(ci, p_global, matrix); + if (ret) { + return ret; + } + } + } - if (!c || !c->is_clipping_contents() || c->has_point(matrix.affine_inverse().xform(p_global))) { + // Fallback; iterate over all children for (int i = p_node->get_child_count() - 1; i >= 0; i--) { CanvasItem *ci = Object::cast_to(p_node->get_child(i)); if (!ci || ci->is_set_as_top_level()) {