Repository: godotengine/godot (language: cpp)

Issue:
Incorrect mouseover hit check in PopupMenu::_get_mouse_over
### Tested versions

v4.4.stable.official [4c311cbee]

### System information

Godot v4.4.stable - Debian GNU/Linux 12 (bookworm) 12 on X11 - OpenGL 3 (Compatibility)

### Issue description

[`PopupMenu::_get_mouse_over`](https://github.com/godotengine/godot/blob/594d64ec244b6b99321e2096987d4d69de4c8845/scene/gui/popup_menu.cpp#L318) appears to have accumulated several mistakes. The results are not very noticeable in the default theme, but things like larger scroll bars can make the problems more obvious.

Here in Right-to-Left layout, the highlight is not present with the cursor over Item 3:
![Image](https://github.com/user-attachments/assets/c8338f3d-39df-47bf-8692-9b55b42b3afd)

Here in Left-to-Right layout, the highlight remains with the cursor over the scroll bar:
![Image](https://github.com/user-attachments/assets/e6007ef6-9b4a-486f-8585-2df1d2785342)

The scrollbar width is deducted from the target area twice in RTL layout:
https://github.com/godotengine/godot/blob/594d64ec244b6b99321e2096987d4d69de4c8845/scene/gui/popup_menu.cpp#L323-L327

The left margin modifies the position, but neither it nor the right margin are deducted from the width.
https://github.com/godotengine/godot/blob/594d64ec244b6b99321e2096987d4d69de4c8845/scene/gui/popup_menu.cpp#L330-L333

(PR of simplified version incoming)

### Steps to reproduce

Most prominent symptom is with Right-to-Left layout and wide scroll bars:
1. Create an OptionButton.
2. Set its Control Layout Direction to Right-to-Left.
3. Add many items, to require a scroll bar.
4. Set a theme with wide vertical scroll bars.
5. Run.
6. Click menu.
7. Observe the mouseover highlight is removed from an item when cursor is towards the right hand edge.

(Or run provided reproduction project. The RTL menu is the lower of the two.)

### Minimal reproduction project (MRP)

[popup-test.zip](https://github.com/user-attachments/files/19458226/popup-test.zip)


Discussion / hints:

Fix patch:
diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp
index dadb7e8ceb31..2119454c73ba 100644
--- a/scene/gui/popup_menu.cpp
+++ b/scene/gui/popup_menu.cpp
@@ -316,32 +316,26 @@ int PopupMenu::_get_items_total_height() const {
 }
 
 int PopupMenu::_get_mouse_over(const Point2 &p_over) const {
-	// Make the item area exclude shadows and the vertical margins and scrollbar.
-	Rect2 item_clickable_area = panel->get_global_rect();
-	if (scroll_container->get_v_scroll_bar()->is_visible_in_tree()) {
-		const int scroll_width = scroll_container->get_v_scroll_bar()->get_size().width;
-		if (is_layout_rtl()) {
-			item_clickable_area.position.x += scroll_width;
-			item_clickable_area.size.width -= scroll_width;
-		}
-		item_clickable_area.size.width -= scroll_width;
-	}
-	float win_scale = get_content_scale_factor();
-	item_clickable_area.position.x += theme_cache.panel_style->get_margin(SIDE_LEFT);
-	item_clickable_area.position.y += theme_cache.panel_style->get_margin(SIDE_TOP);
-	item_clickable_area.position *= win_scale;
-	item_clickable_area.size.y -= theme_cache.panel_style->get_margin(SIDE_TOP) + theme_cache.panel_style->get_margin(SIDE_BOTTOM);
-	item_clickable_area.size *= win_scale;
-
-	if (!item_clickable_area.has_point(p_over)) {
+	// Transform to scroll_container local coordinates.
+	const Point2 scaled_pos = p_over / get_content_scale_factor();
+	const Point2 over_scroll_container =
+			scroll_container->get_global_transform_with_canvas().xform_inv(scaled_pos);
+
+	// Check if point is inside the item control as clipped by scroll_container.
+	const Rect2 scroll_container_rect = Rect2(Point2(), scroll_container->get_size());
+	const Rect2 bounding_rect = scroll_container_rect.intersection(control->get_rect());
+	if (!bounding_rect.has_point(over_scroll_container)) {
 		return -1;
 	}
 
-	float ofs = item_clickable_area.position.y + (float)theme_cache.v_separation * win_scale * 0.5;
+	// Perform item hit check in control node local space,
+	// so we don't need to worry about any of the container theming.
+	const float over_control_y = control->get_transform().xform_inv(over_scroll_container).y;
+	float bottom_edge = 0;
 	for (int i = 0; i < items.size(); i++) {
-		ofs += i > 0 ? (float)theme_cache.v_separation * win_scale : (float)theme_cache.v_separation * win_scale * 0.5;
-		ofs += _get_item_height(i) * win_scale;
-		if (p_over.y - control->get_position().y * win_scale < ofs) {
+		bottom_edge += theme_cache.v_separation;
+		bottom_edge += _get_item_height(i);
+		if (bottom_edge > over_control_y) {
 			return i;
 		}
 	}


Test patch: