Repository: godotengine/godot (language: cpp)

Issue:
Tree button click detection is off
### Tested versions

Godot v4.4.beta2 

### System information

Fedora Linux 40 (KDE Plasma) on Wayland - X11 display driver, Multi-window

### Issue description

Tree button click detection is off. 
Mouse is over one button but you click on another. 
In some cases it draws hover state for one button but clicks on another.

https://github.com/user-attachments/assets/0a4e895d-eb4a-46db-878e-2f057e2df852



### Steps to reproduce


Have multiple buttons in a column.
Try to interact with them.

### Minimal reproduction project (MRP)

[treemouse.zip](https://github.com/user-attachments/files/18604902/treemouse.zip)


Discussion / hints:
Seems like this is a broken feature from the start, introduced in https://github.com/godotengine/godot/pull/88530

Will confirm that it was broken from the first commit once I've compiled it

Edit: Was the same way from the start, so some computation in the PR was incorrect

CC @davthedev 
The other thing that makes the click fell off, is that it applies margin only on the right side of the button. Is this the correct behavior? 

From 4.3

https://github.com/user-attachments/assets/a26dbfb1-6f28-4401-ad02-81a67e326cb7

There is something to refactor in this big widget, in order to bring the hover & click detection to the same logic.

What you see is because there is one legacy, yet complex algorithm to compute clicks that was there before, and I added a simpler algorithm to compute hover position updates in order to limit possible side effects. I have not merged both algorithms yet, which should be done at some point.

Thank you for bringing those corner cases to my attention. The second one is mysterious, would be because of the horizontal scrolling?

I do not have the time to look at it right now, health being in the way at the moment. But I will check it out later.


> Thank you for bringing those corner cases to my attention. The second one is mysterious, would be because of the horizontal scrolling?

Yes that looks to be the case.

I made a PR for get_item_area_rect and scrolling, https://github.com/godotengine/godot/pull/102116. Maybe something can be used from that?

The simple fix is using hovered items/buttons for clicks, as they are supposed to be the same.
Or at least it would be simple if the hovered button wasn't wrong.

https://github.com/user-attachments/assets/490500ad-91ef-4cc8-9ab6-c20da7326170

Fix patch:
diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp
index 81a70378786e..8ecc475805b7 100644
--- a/scene/gui/tree.cpp
+++ b/scene/gui/tree.cpp
@@ -2872,10 +2872,7 @@ void Tree::_range_click_timeout() {
 		Ref<InputEventMouseButton> mb;
 		mb.instantiate();
 
-		int x_limit = get_size().width - theme_cache.panel_style->get_minimum_size().width;
-		if (v_scroll->is_visible()) {
-			x_limit -= v_scroll->get_minimum_size().width;
-		}
+		int x_limit = _get_content_rect().size.x;
 
 		cache.rtl = is_layout_rtl();
 
@@ -2909,6 +2906,10 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int
 		// Skip any processing of invisible items.
 		return 0;
 	}
+	if (p_pos.x > x_limit) {
+		// Inside the scroll area.
+		return -1;
+	}
 
 	int item_h = compute_item_height(p_item) + theme_cache.v_separation;
 
@@ -2930,96 +2931,42 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int
 			return -1;
 		}
 
-		int x = p_pos.x;
-		/* find clicked column */
-		int col = -1;
-		int col_ofs = 0;
-		int col_width = 0;
-
-		int limit_w = x_limit;
-
-		for (int i = 0; i < columns.size(); i++) {
-			col_width = get_column_width(i);
+		FindColumnButtonResult find_result = _find_column_and_button_at_pos(p_pos.x, p_item, x_ofs, x_limit);
 
-			if (p_item->cells[i].expand_right) {
-				int plus = 1;
-				while (i + plus < columns.size() && !p_item->cells[i + plus].editable && p_item->cells[i + plus].mode == TreeItem::CELL_MODE_STRING && p_item->cells[i + plus].text.is_empty() && p_item->cells[i + plus].icon.is_null()) {
-					col_width += theme_cache.h_separation;
-					col_width += get_column_width(i + plus);
-					plus++;
-				}
-			}
-
-			if (x > col_width) {
-				col_ofs += col_width;
-				x -= col_width;
-				limit_w -= col_width;
-				continue;
-			}
-
-			col = i;
-			break;
-		}
-
-		if (col == -1) {
+		if (find_result.column_index == -1) {
 			return -1;
-		} else if (col == 0) {
-			int margin = x_ofs + theme_cache.item_margin; //-theme_cache.h_separation;
-			//int lm = theme_cache.panel_style->get_margin(SIDE_LEFT);
-			col_width -= margin;
-			limit_w -= margin;
-			col_ofs += margin;
-			x -= margin;
-		} else {
-			col_width -= theme_cache.h_separation;
-			limit_w -= theme_cache.h_separation;
-			x -= theme_cache.h_separation;
 		}
 
+		int col = find_result.column_index;
+		int col_width = find_result.column_width;
+		int col_ofs = find_result.column_offset;
+		int x = find_result.pos_x;
 		const TreeItem::Cell &c = p_item->cells[col];
 
-		if (!cache.rtl && !p_item->cells[col].buttons.is_empty()) {
-			int button_w = 0;
-			for (int j = p_item->cells[col].buttons.size() - 1; j >= 0; j--) {
-				Ref<Texture2D> b = p_item->cells[col].buttons[j].texture;
-				button_w += b->get_size().width + theme_cache.button_pressed->get_minimum_size().width + theme_cache.button_margin;
+		if (find_result.button_index >= 0) {
+			if (c.buttons[find_result.button_index].disabled) {
+				pressed_button = -1;
+				cache.click_type = Cache::CLICK_NONE;
+				return -1;
 			}
 
-			col_width = MAX(button_w, MIN(limit_w, col_width));
-		}
-
-		// Cell button detection code.
-		for (int j = c.buttons.size() - 1; j >= 0; j--) {
-			Ref<Texture2D> b = c.buttons[j].texture;
-			int w = b->get_size().width + theme_cache.button_pressed->get_minimum_size().width;
-
-			if (x > col_width - w) {
-				if (c.buttons[j].disabled) {
-					pressed_button = -1;
-					cache.click_type = Cache::CLICK_NONE;
-					return -1;
-				}
-
-				// Make sure the click is correct.
-				const Point2 click_pos = get_local_mouse_position();
-				if (!get_item_at_position(click_pos)) {
-					pressed_button = -1;
-					cache.click_type = Cache::CLICK_NONE;
-					return -1;
-				}
-
-				pressed_button = j;
-				cache.click_type = Cache::CLICK_BUTTON;
-				cache.click_index = j;
-				cache.click_id = c.buttons[j].id;
-				cache.click_item = p_item;
-				cache.click_column = col;
-				cache.click_pos = click_pos;
-				queue_redraw();
+			// Make sure the click is correct.
+			const Point2 click_pos = get_local_mouse_position();
+			if (!get_item_at_position(click_pos)) {
+				pressed_button = -1;
+				cache.click_type = Cache::CLICK_NONE;
 				return -1;
 			}
 
-			col_width -= w + theme_cache.button_margin;
+			pressed_button = find_result.button_index;
+			cache.click_type = Cache::CLICK_BUTTON;
+			cache.click_index = find_result.button_index;
+			cache.click_id = c.buttons[find_result.button_index].id;
+			cache.click_item = p_item;
+			cache.click_column = col;
+			cache.click_pos = click_pos;
+			queue_redraw();
+			return -1;
 		}
 
 		if (!p_item->disable_folding && !hide_folding && !p_item->cells[col].editable && !p_item->cells[col].selectable && p_item->get_first_child()) {
@@ -3790,7 +3737,7 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) {
 					mb->get_button_index() == MouseButton::RIGHT) {
 				Point2 pos = mb->get_position();
 				if (rtl) {
-					pos.x = get_size().width - pos.x;
+					pos.x = get_size().width - pos.x - 1;
 				}
 				pos -= theme_cache.panel_style->get_offset();
 				if (show_column_titles) {
@@ -3890,7 +3837,7 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) {
 
 				Point2 pos = mb->get_position();
 				if (rtl) {
-					pos.x = get_size().width - pos.x;
+					pos.x = get_size().width - pos.x - 1;
 				}
 				pos -= bg->get_offset();
 				cache.click_type = Cache::CLICK_NONE;
@@ -3922,10 +3869,7 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) {
 				pressing_for_editor = false;
 				propagate_mouse_activated = false;
 
-				int x_limit = get_size().width - theme_cache.panel_style->get_minimum_size().width;
-				if (v_scroll->is_visible()) {
-					x_limit -= v_scroll->get_minimum_size().width;
-				}
+				int x_limit = _get_content_rect().size.x;
 
 				cache.rtl = is_layout_rtl();
 				blocked++;
@@ -4028,7 +3972,7 @@ void Tree::_determine_hovered_item() {
 
 	Point2 pos = hovered_pos;
 	if (rtl) {
-		pos.x = get_size().width - pos.x;
+		pos.x = get_size().width - pos.x - 1;
 	}
 	pos -= theme_cache.panel_style->get_offset();
 
@@ -4059,115 +4003,36 @@ void Tree::_determine_hovered_item() {
 	}
 
 	// Determine hover on rows and items.
-	if (root && is_mouse_hovering) {
-		Point2 mpos = hovered_pos;
-		if (rtl) {
-			mpos.x = get_size().width - mpos.x;
-		}
-		mpos -= theme_cache.panel_style->get_offset();
-		mpos.y -= _get_title_button_height();
-		if (mpos.y >= 0) {
-			if (h_scroll->is_visible_in_tree()) {
-				mpos.x += h_scroll->get_value();
-			}
-			if (v_scroll->is_visible_in_tree()) {
-				mpos.y += v_scroll->get_value();
-			}
-
-			int col, h, section;
-			TreeItem *it = _find_item_at_pos(root, mpos, col, h, section);
-
-			// Find possible hovered button in cell.
-			int col_button_index = -1;
-
-			Point2 cpos = mpos;
-
-			if (it) {
-				const TreeItem::Cell &c = it->cells[col];
-				int col_width = get_column_width(col);
-
-				// In the first column, tree nesting indent impacts the leftmost possible buttons position
-				// and the clickable area of the folding arrow.
-				int col_indent = 0;
-				if (col == 0) {
-					col_indent = _get_item_h_offset(it);
-				}
-
-				// Compute total width of buttons block including spacings.
-				int buttons_width = 0;
-				for (int j = c.buttons.size() - 1; j >= 0; j--) {
-					Ref<Texture2D> b = c.buttons[j].texture;
-					Size2 size = b->get_size() + theme_cache.button_pressed->get_minimum_size();
-					buttons_width += size.width + theme_cache.button_margin;
-				}
-
-				// Adjust when buttons are shifted left into view so that they remain visible even
-				// if part of the cell is beyond the right border due to horizontal scrolling and
-				// a long string in one of the items. This matches the drawing & click handling algorithms
-				// that are based on recursion.
-				int clamped_column_offset = 0;
-				int col_left = 0;
-
-				for (int i = 0; i < col; i++) {
-					int i_col_w = get_column_width(i);
-					cpos.x -= i_col_w;
-					col_left += i_col_w;
-				}
-				col_left -= theme_cache.offset.x;
-
-				// Compute buttons offset that makes them visible, in comparison to what would be their
-				// natural position that would cut them off.
-				if (!rtl) {
-					const Rect2 content_rect = _get_content_rect();
-					int cw = content_rect.size.width;
-					int col_right = col_left + col_width;
-					if (col_right > cw) {
-						clamped_column_offset = col_right - cw - theme_cache.scrollbar_h_separation;
-						int max_clamp_offset = col_width - col_indent - buttons_width;
-						if (clamped_column_offset > max_clamp_offset) {
-							clamped_column_offset = max_clamp_offset;
-						}
-					}
-				}
-				col_width -= clamped_column_offset;
-
-				// Find the actual button under coordinates.
-				for (int j = c.buttons.size() - 1; j >= 0; j--) {
-					Ref<Texture2D> b = c.buttons[j].texture;
-					Size2 size = b->get_size() + theme_cache.button_pressed->get_minimum_size();
-					if (cpos.x > col_width - size.width && col_button_index == -1) {
-						col_button_index = j;
-					}
-					col_width -= size.width + theme_cache.button_margin;
-				}
+	if (root && is_mouse_hovering && hovered_pos.y >= 0) {
+		TreeItem *it;
+		int col, section, col_button_index;
+		_find_button_at_pos(hovered_pos, it, col, col_button_index, section);
+
+		if (drop_mode_flags) {
+			if (it != drop_mode_over) {
+				drop_mode_over = it;
+				queue_redraw();
 			}
-
-			if (drop_mode_flags) {
-				if (it != drop_mode_over) {
-					drop_mode_over = it;
-					queue_redraw();
-				}
-				if (it && section != drop_mode_section) {
-					drop_mode_section = section;
-					queue_redraw();
-				}
+			if (it && section != drop_mode_section) {
+				drop_mode_section = section;
+				queue_redraw();
 			}
+		}
 
-			cache.hover_item = it;
-			cache.hover_column = col;
-			cache.hover_button_index_in_column = col_button_index;
+		cache.hover_item = it;
+		cache.hover_column = col;
+		cache.hover_button_index_in_column = col_button_index;
 
-			if (it != old_item || col != old_column) {
-				if (old_item && old_column >= old_item->cells.size()) {
-					// Columns may have changed since last redraw().
+		if (it != old_item || col != old_column) {
+			if (old_item && old_column >= old_item->cells.size()) {
+				// Columns may have changed since last redraw().
+				queue_redraw();
+			} else {
+				// Only need to update if mouse enters/exits a button.
+				bool was_over_button = old_item && old_item->cells[old_column].custom_button;
+				bool is_over_button = it && it->cells[col].custom_button;
+				if (was_over_button || is_over_button) {
 					queue_redraw();
-				} else {
-					// Only need to update if mouse enters/exits a button.
-					bool was_over_button = old_item && old_item->cells[old_column].custom_button;
-					bool is_over_button = it && it->cells[col].custom_button;
-					if (was_over_button || is_over_button) {
-						queue_redraw();
-					}
 				}
 			}
 		}
@@ -5107,46 +4972,6 @@ int Tree::get_item_offset(TreeItem *p_item) const {
 	return -1; // Not found.
 }
 
-int Tree::_get_item_h_offset(TreeItem *p_item) const {
-	TreeItem *it = root;
-	int nesting_level = 0;
-	if (!it) {
-		return 0;
-	}
-
-	while (true) {
-		if (it == p_item) {
-			if (!hide_root) {
-				nesting_level += 1;
-			}
-			if (hide_folding) {
-				nesting_level -= 1;
-			}
-			return nesting_level * theme_cache.item_margin;
-		}
-
-		if (it->first_child && !it->collapsed) {
-			it = it->first_child;
-			nesting_level += 1;
-
-		} else if (it->next) {
-			it = it->next;
-		} else {
-			while (!it->next) {
-				it = it->parent;
-				nesting_level -= 1;
-				if (it == nullptr) {
-					return 0;
-				}
-			}
-
-			it = it->next;
-		}
-	}
-
-	return -1; // Not found.
-}
-
 void Tree::ensure_cursor_is_visible() {
 	if (!is_inside_tree()) {
 		return;
@@ -5591,25 +5416,33 @@ TreeItem *Tree::_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_
 // When on a button, r_index is valid.
 // When on an item, both r_item and r_column are valid.
 // Otherwise, all output arguments are invalid.
-void Tree::_find_button_at_pos(const Point2 &p_pos, TreeItem *&r_item, int &r_column, int &r_index) const {
+void Tree::_find_button_at_pos(const Point2 &p_pos, TreeItem *&r_item, int &r_column, int &r_index, int &r_section) const {
 	r_item = nullptr;
 	r_column = -1;
 	r_index = -1;
+	r_section = -1;
 
 	if (!root) {
 		return;
 	}
 
-	Point2 pos = p_pos - theme_cache.panel_style->get_offset();
+	Point2 pos = p_pos;
+	if (cache.rtl) {
+		pos.x = get_size().width - pos.x - 1;
+	}
+	pos -= theme_cache.panel_style->get_offset();
 	pos.y -= _get_title_button_height();
 	if (pos.y < 0) {
 		return;
 	}
+	pos += theme_cache.offset; // Scrolling.
 
-	if (cache.rtl) {
-		pos.x = get_size().width - pos.x;
+	int x_limit = _get_content_rect().size.x + theme_cache.offset.width;
+
+	if (pos.x > x_limit) {
+		// Inside the scroll area.
+		return;
 	}
-	pos += theme_cache.offset; // Scrolling.
 
 	int col, h, section;
 	TreeItem *it = _find_item_at_pos(root, pos, col, h, section);
@@ -5619,53 +5452,99 @@ void Tree::_find_button_at_pos(const Point2 &p_pos, TreeItem *&r_item, int &r_co
 
 	r_item = it;
 	r_column = col;
+	r_section = section;
 
 	const TreeItem::Cell &c = it->cells[col];
 	if (c.buttons.is_empty()) {
 		return;
 	}
 
-	int x_limit = get_size().width - theme_cache.panel_style->get_minimum_size().width + theme_cache.offset.x;
-	if (v_scroll->is_visible_in_tree()) {
-		x_limit -= v_scroll->get_minimum_size().width;
-	}
+	// Search the button index.
+	FindColumnButtonResult result = _find_column_and_button_at_pos(pos.x, it, 0, x_limit);
+	r_index = result.button_index;
+}
 
-	for (int i = 0; i < col; i++) {
-		const int col_w = get_column_width(i);
-		pos.x -= col_w;
-		x_limit -= col_w;
-	}
+Tree::FindColumnButtonResult Tree::_find_column_and_button_at_pos(int p_x, const TreeItem *p_item, int p_x_ofs, int p_x_limit) const {
+	int x = p_x;
 
-	int x_check;
-	if (cache.rtl) {
-		x_check = get_column_width(col);
-	} else {
-		// Right edge of the buttons area, relative to the start of the column.
-		int buttons_area_min = 0;
-		if (col == 0) {
-			// Content of column 0 should take indentation into account.
-			for (TreeItem *current = it; current && (current != root || !hide_root); current = current->parent) {
-				buttons_area_min += theme_cache.item_margin;
+	int col = -1;
+	int col_ofs = 0;
+	int col_width = 0;
+
+	int limit_w = p_x_limit;
+
+	FindColumnButtonResult result;
+
+	for (int i = 0; i < columns.size(); i++) {
+		col_width = get_column_width(i);
+
+		if (p_item->cells[i].expand_right) {
+			int plus = 1;
+			while (i + plus < columns.size() && !p_item->cells[i + plus].editable && p_item->cells[i + plus].mode == TreeItem::CELL_MODE_STRING && p_item->cells[i + plus].text.is_empty() && p_item->cells[i + plus].icon.is_null()) {
+				col_width += theme_cache.h_separation;
+				col_width += get_column_width(i + plus);
+				plus++;
 			}
 		}
-		for (int i = c.buttons.size() - 1; i >= 0; i--) {
-			Ref<Texture2D> b = c.buttons[i].texture;
-			buttons_area_min += b->get_size().width + theme_cache.button_pressed->get_minimum_size().width + theme_cache.button_margin;
+
+		if (x > col_width) {
+			col_ofs += col_width;
+			x -= col_width;
+			limit_w -= col_width;
+			continue;
 		}
 
-		x_check = MAX(buttons_area_min, MIN(get_column_width(col), x_limit));
+		col = i;
+		break;
 	}
 
-	for (int i = c.buttons.size() - 1; i >= 0; i--) {
-		Ref<Texture2D> b = c.buttons[i].texture;
-		Size2 size = b->get_size() + theme_cache.button_pressed->get_minimum_size();
-		if (pos.x > x_check - size.width) {
-			x_limit -= theme_cache.item_margin;
-			r_index = i;
-			return;
+	if (col >= 0) {
+		if (col == 0) {
+			int margin = p_x_ofs + theme_cache.item_margin;
+			col_width -= margin;
+			limit_w -= margin;
+			col_ofs += margin;
+			x -= margin;
+		} else {
+			col_width -= theme_cache.h_separation;
+			limit_w -= theme_cache.h_separation;
+			x -= theme_cache.h_separation;
+		}
+
+		if (!cache.rtl && !p_item->cells[col].buttons.is_empty()) {
+			int button_w = 0;
+			for (int j = p_item->cells[col].buttons.size() - 1; j >= 0; j--) {
+				const Ref<Texture2D> &b = p_item->cells[col].buttons[j].texture;
+				button_w += b->get_size().width + theme_cache.button_pressed->get_minimum_size().width + theme_cache.button_margin;
+			}
+
+			col_width = MAX(button_w, MIN(limit_w, col_width));
+		}
+
+		// Cell button detection code.
+		// The first half of the button margin will be applied to the left button,
+		// and the other half to the right button.
+		const int offset_button_width = theme_cache.button_margin / 2;
+		const TreeItem::Cell &c = p_item->cells[col];
+		for (int j = c.buttons.size() - 1; j >= 0; j--) {
+			const Ref<Texture2D> &b = c.buttons[j].texture;
+			int w = b->get_size().width + theme_cache.button_pressed->get_minimum_size().width;
+
+			if (x >= col_width - w - offset_button_width - 1) {
+				result.button_index = j;
+				break;
+			}
+
+			col_width -= w + theme_cache.button_margin;
 		}
-		x_check -= size.width + theme_cache.button_margin;
 	}
+
+	result.column_index = col;
+	result.column_offset = col_ofs;
+	result.column_width = col_width;
+	result.pos_x = x;
+
+	return result;
 }
 
 int Tree::get_column_at_position(const Point2 &p_pos) const {
@@ -5675,7 +5554,7 @@ int Tree::get_column_at_position(const Point2 &p_pos) const {
 
 	Point2 pos = p_pos;
 	if (is_layout_rtl()) {
-		pos.x = get_size().width - pos.x;
+		pos.x = get_size().width - pos.x - 1;
 	}
 	pos -= theme_cache.panel_style->get_offset();
 	pos.y -= _get_title_button_height();
@@ -5706,7 +5585,7 @@ int Tree::get_drop_section_at_position(const Point2 &p_pos) const {
 
 	Point2 pos = p_pos;
 	if (is_layout_rtl()) {
-		pos.x = get_size().width - pos.x;
+		pos.x = get_size().width - pos.x - 1;
 	}
 	pos -= theme_cache.panel_style->get_offset();
 	pos.y -= _get_title_button_height();
@@ -5755,7 +5634,7 @@ TreeItem *Tree::get_item_at_position(const Point2 &p_pos) const {
 
 	Point2 pos = p_pos;
 	if (is_layout_rtl()) {
-		pos.x = get_size().width - pos.x;
+		pos.x = get_size().width - pos.x - 1;
 	}
 	pos -= theme_cache.panel_style->get_offset();
 	pos.y -= _get_title_button_height();
@@ -5785,8 +5664,8 @@ int Tree::get_button_id_at_position(const Point2 &p_pos) const {
 	}
 
 	TreeItem *it;
-	int col, index;
-	_find_button_at_pos(p_pos, it, col, index);
+	int col, index, section;
+	_find_button_at_pos(p_pos, it, col, index, section);
 
 	if (index == -1) {
 		return -1;
@@ -5802,8 +5681,8 @@ String Tree::get_tooltip(const Point2 &p_pos) const {
 	}
 
 	TreeItem *it;
-	int col, index;
-	_find_button_at_pos(p_pos, it, col, index);
+	int col, index, section;
+	_find_button_at_pos(p_pos, it, col, index, section);
 
 	if (index != -1) {
 		return it->cells[col].buttons[index].tooltip;
diff --git a/scene/gui/tree.h b/scene/gui/tree.h
index 2a5d763e704e..9d3324bd3db9 100644
--- a/scene/gui/tree.h
+++ b/scene/gui/tree.h
@@ -670,9 +670,18 @@ class Tree : public Control {
 	TreeItem *_search_item_text(TreeItem *p_at, const String &p_find, int *r_col, bool p_selectable, bool p_backwards = false);
 
 	TreeItem *_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_column, int &r_height, int &r_section) const;
-	int _get_item_h_offset(TreeItem *p_item) const;
 
-	void _find_button_at_pos(const Point2 &p_pos, TreeItem *&r_item, int &r_column, int &r_index) const;
+	void _find_button_at_pos(const Point2 &p_pos, TreeItem *&r_item, int &r_column, int &r_index, int &r_section) const;
+
+	struct FindColumnButtonResult {
+		int column_index = -1;
+		int button_index = -1;
+		int column_width = -1;
+		int column_offset = -1;
+		int pos_x = -1;
+	};
+
+	FindColumnButtonResult _find_column_and_button_at_pos(int p_x, const TreeItem *p_item, int p_x_ofs, int p_x_limit) const;
 
 	/*	float drag_speed;
 	float drag_accum;


Test patch: