Browse Source

Add hover state to Tree items display

David Giardi 1 year ago
parent
commit
ebe1a2d7ec

+ 15 - 0
doc/classes/Tree.xml

@@ -509,6 +509,12 @@
 		<theme_item name="font_disabled_color" data_type="color" type="Color" default="Color(0.875, 0.875, 0.875, 0.5)">
 		<theme_item name="font_disabled_color" data_type="color" type="Color" default="Color(0.875, 0.875, 0.875, 0.5)">
 			Text [Color] for a [constant TreeItem.CELL_MODE_CHECK] mode cell when it's non-editable (see [method TreeItem.set_editable]).
 			Text [Color] for a [constant TreeItem.CELL_MODE_CHECK] mode cell when it's non-editable (see [method TreeItem.set_editable]).
 		</theme_item>
 		</theme_item>
+		<theme_item name="font_hovered_color" data_type="color" type="Color" default="Color(0.95, 0.95, 0.95, 1)">
+			Text [Color] used when the item is hovered.
+		</theme_item>
+		<theme_item name="font_hovered_dimmed_color" data_type="color" type="Color" default="Color(0.875, 0.875, 0.875, 1)">
+			Text [Color] used when the item is hovered, while a button of the same item is hovered as the same time.
+		</theme_item>
 		<theme_item name="font_outline_color" data_type="color" type="Color" default="Color(0, 0, 0, 1)">
 		<theme_item name="font_outline_color" data_type="color" type="Color" default="Color(0, 0, 0, 1)">
 			The tint of text outline of the item.
 			The tint of text outline of the item.
 		</theme_item>
 		</theme_item>
@@ -645,6 +651,9 @@
 		<theme_item name="updown" data_type="icon" type="Texture2D">
 		<theme_item name="updown" data_type="icon" type="Texture2D">
 			The updown arrow icon to display for the [constant TreeItem.CELL_MODE_RANGE] mode cell.
 			The updown arrow icon to display for the [constant TreeItem.CELL_MODE_RANGE] mode cell.
 		</theme_item>
 		</theme_item>
+		<theme_item name="button_hover" data_type="style" type="StyleBox">
+			[StyleBox] used when a button in the tree is hovered.
+		</theme_item>
 		<theme_item name="button_pressed" data_type="style" type="StyleBox">
 		<theme_item name="button_pressed" data_type="style" type="StyleBox">
 			[StyleBox] used when a button in the tree is pressed.
 			[StyleBox] used when a button in the tree is pressed.
 		</theme_item>
 		</theme_item>
@@ -666,6 +675,12 @@
 		<theme_item name="focus" data_type="style" type="StyleBox">
 		<theme_item name="focus" data_type="style" type="StyleBox">
 			The focused style for the [Tree], drawn on top of everything.
 			The focused style for the [Tree], drawn on top of everything.
 		</theme_item>
 		</theme_item>
+		<theme_item name="hovered" data_type="style" type="StyleBox">
+			[StyleBox] for the item being hovered.
+		</theme_item>
+		<theme_item name="hovered_dimmed" data_type="style" type="StyleBox">
+			[StyleBox] for the item being hovered, while a button of the same item is hovered as the same time.
+		</theme_item>
 		<theme_item name="panel" data_type="style" type="StyleBox">
 		<theme_item name="panel" data_type="style" type="StyleBox">
 			The background style for the [Tree].
 			The background style for the [Tree].
 		</theme_item>
 		</theme_item>

+ 1 - 1
editor/project_manager/project_list.cpp

@@ -88,7 +88,7 @@ void ProjectListItemControl::_notification(int p_what) {
 				draw_style_box(get_theme_stylebox(SNAME("selected"), SNAME("Tree")), Rect2(Point2(), get_size()));
 				draw_style_box(get_theme_stylebox(SNAME("selected"), SNAME("Tree")), Rect2(Point2(), get_size()));
 			}
 			}
 			if (is_hovering) {
 			if (is_hovering) {
-				draw_style_box(get_theme_stylebox(SNAME("hover"), SNAME("Tree")), Rect2(Point2(), get_size()));
+				draw_style_box(get_theme_stylebox(SNAME("hovered"), SNAME("Tree")), Rect2(Point2(), get_size()));
 			}
 			}
 
 
 			draw_line(Point2(0, get_size().y + 1), Point2(get_size().x, get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));
 			draw_line(Point2(0, get_size().y + 1), Point2(get_size().x, get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));

+ 9 - 1
editor/themes/editor_theme_manager.cpp

@@ -945,6 +945,8 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the
 
 
 			p_theme->set_color("custom_button_font_highlight", "Tree", p_config.font_hover_color);
 			p_theme->set_color("custom_button_font_highlight", "Tree", p_config.font_hover_color);
 			p_theme->set_color(SceneStringName(font_color), "Tree", p_config.font_color);
 			p_theme->set_color(SceneStringName(font_color), "Tree", p_config.font_color);
+			p_theme->set_color("font_hovered_color", "Tree", p_config.mono_color);
+			p_theme->set_color("font_hovered_dimmed_color", "Tree", p_config.font_color);
 			p_theme->set_color("font_selected_color", "Tree", p_config.mono_color);
 			p_theme->set_color("font_selected_color", "Tree", p_config.mono_color);
 			p_theme->set_color("font_disabled_color", "Tree", p_config.font_disabled_color);
 			p_theme->set_color("font_disabled_color", "Tree", p_config.font_disabled_color);
 			p_theme->set_color("font_outline_color", "Tree", p_config.font_outline_color);
 			p_theme->set_color("font_outline_color", "Tree", p_config.font_outline_color);
@@ -997,7 +999,13 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the
 			Ref<StyleBoxFlat> style_tree_hover = p_config.base_style->duplicate();
 			Ref<StyleBoxFlat> style_tree_hover = p_config.base_style->duplicate();
 			style_tree_hover->set_bg_color(p_config.highlight_color * Color(1, 1, 1, 0.4));
 			style_tree_hover->set_bg_color(p_config.highlight_color * Color(1, 1, 1, 0.4));
 			style_tree_hover->set_border_width_all(0);
 			style_tree_hover->set_border_width_all(0);
-			p_theme->set_stylebox("hover", "Tree", style_tree_hover);
+			p_theme->set_stylebox("hovered", "Tree", style_tree_hover);
+			p_theme->set_stylebox("button_hover", "Tree", style_tree_hover);
+
+			Ref<StyleBoxFlat> style_tree_hover_dimmed = p_config.base_style->duplicate();
+			style_tree_hover_dimmed->set_bg_color(p_config.highlight_color * Color(1, 1, 1, 0.2));
+			style_tree_hover_dimmed->set_border_width_all(0);
+			p_theme->set_stylebox("hovered_dimmed", "Tree", style_tree_hover_dimmed);
 
 
 			p_theme->set_stylebox("selected_focus", "Tree", style_tree_focus);
 			p_theme->set_stylebox("selected_focus", "Tree", style_tree_focus);
 			p_theme->set_stylebox("selected", "Tree", style_tree_selected);
 			p_theme->set_stylebox("selected", "Tree", style_tree_selected);

+ 295 - 100
scene/gui/tree.cpp

@@ -2165,12 +2165,18 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2
 
 
 		int ofs = p_pos.x + ((p_item->disable_folding || hide_folding) ? theme_cache.h_separation : theme_cache.item_margin);
 		int ofs = p_pos.x + ((p_item->disable_folding || hide_folding) ? theme_cache.h_separation : theme_cache.item_margin);
 		int skip2 = 0;
 		int skip2 = 0;
+
+		bool is_row_hovered = (!cache.hover_header_row && cache.hover_item == p_item);
+
 		for (int i = 0; i < columns.size(); i++) {
 		for (int i = 0; i < columns.size(); i++) {
 			if (skip2) {
 			if (skip2) {
 				skip2--;
 				skip2--;
 				continue;
 				continue;
 			}
 			}
 
 
+			bool is_col_hovered = cache.hover_column == i;
+			bool is_cell_hovered = is_row_hovered && is_col_hovered;
+			bool is_cell_button_hovered = is_cell_hovered && cache.hover_button_index_in_column != -1;
 			int item_width = get_column_width(i);
 			int item_width = get_column_width(i);
 
 
 			if (i == 0) {
 			if (i == 0) {
@@ -2203,6 +2209,8 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2
 
 
 				int total_ofs = ofs - theme_cache.offset.x;
 				int total_ofs = ofs - theme_cache.offset.x;
 
 
+				// If part of the column is beyond the right side of the control due to scrolling, clamp the label width
+				// so that all buttons attached to the cell remain within view.
 				if (total_ofs + item_width > p_draw_size.width) {
 				if (total_ofs + item_width > p_draw_size.width) {
 					item_width = MAX(buttons_width, p_draw_size.width - total_ofs);
 					item_width = MAX(buttons_width, p_draw_size.width - total_ofs);
 				}
 				}
@@ -2247,17 +2255,42 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2
 				RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(r.position.x, r.position.y + r.size.height), r.position + r.size, theme_cache.guide_color, 1);
 				RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(r.position.x, r.position.y + r.size.height), r.position + r.size, theme_cache.guide_color, 1);
 			}
 			}
 
 
-			if (i == 0) {
-				if (p_item->cells[0].selected && select_mode == SELECT_ROW) {
+			if (i == 0 && select_mode == SELECT_ROW) {
+				if (p_item->cells[0].selected || is_row_hovered) {
 					const Rect2 content_rect = _get_content_rect();
 					const Rect2 content_rect = _get_content_rect();
 					Rect2i row_rect = Rect2i(Point2i(content_rect.position.x, item_rect.position.y), Size2i(content_rect.size.x, item_rect.size.y));
 					Rect2i row_rect = Rect2i(Point2i(content_rect.position.x, item_rect.position.y), Size2i(content_rect.size.x, item_rect.size.y));
 					if (rtl) {
 					if (rtl) {
 						row_rect.position.x = get_size().width - row_rect.position.x - row_rect.size.x;
 						row_rect.position.x = get_size().width - row_rect.position.x - row_rect.size.x;
 					}
 					}
-					if (has_focus()) {
-						theme_cache.selected_focus->draw(ci, row_rect);
+
+					if (p_item->cells[0].selected) {
+						if (has_focus()) {
+							theme_cache.selected_focus->draw(ci, row_rect);
+						} else {
+							theme_cache.selected->draw(ci, row_rect);
+						}
+					} else if (!drop_mode_flags) {
+						if (is_cell_button_hovered) {
+							theme_cache.hovered_dimmed->draw(ci, row_rect);
+						} else {
+							theme_cache.hovered->draw(ci, row_rect);
+						}
+					}
+				}
+			}
+
+			if (select_mode != SELECT_ROW) {
+				Rect2i r = cell_rect;
+				if (rtl) {
+					r.position.x = get_size().width - r.position.x - r.size.x;
+				}
+
+				// Cell hover.
+				if (is_cell_hovered && !p_item->cells[i].selected && !drop_mode_flags) {
+					if (is_cell_button_hovered) {
+						theme_cache.hovered_dimmed->draw(ci, r);
 					} else {
 					} else {
-						theme_cache.selected->draw(ci, row_rect);
+						theme_cache.hovered->draw(ci, r);
 					}
 					}
 				}
 				}
 			}
 			}
@@ -2335,7 +2368,9 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2
 			if (p_item->cells[i].custom_color) {
 			if (p_item->cells[i].custom_color) {
 				cell_color = p_item->cells[i].color;
 				cell_color = p_item->cells[i].color;
 			} else {
 			} else {
-				cell_color = p_item->cells[i].selected ? theme_cache.font_selected_color : theme_cache.font_color;
+				bool draw_as_hover = !drop_mode_flags && (select_mode == SELECT_ROW ? is_row_hovered : is_cell_hovered);
+				bool draw_as_hover_dim = draw_as_hover && is_cell_button_hovered;
+				cell_color = p_item->cells[i].selected ? theme_cache.font_selected_color : (draw_as_hover_dim ? theme_cache.font_hovered_dimmed_color : (draw_as_hover ? theme_cache.font_hovered_color : theme_cache.font_color));
 			}
 			}
 
 
 			Color font_outline_color = theme_cache.font_outline_color;
 			Color font_outline_color = theme_cache.font_outline_color;
@@ -2487,7 +2522,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2
 					ir.size.width -= downarrow->get_width();
 					ir.size.width -= downarrow->get_width();
 
 
 					if (p_item->cells[i].custom_button) {
 					if (p_item->cells[i].custom_button) {
-						if (cache.hover_item == p_item && cache.hover_cell == i) {
+						if (cache.hover_item == p_item && cache.hover_column == i) {
 							if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT)) {
 							if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT)) {
 								draw_style_box(theme_cache.custom_button_pressed, ir);
 								draw_style_box(theme_cache.custom_button_pressed, ir);
 							} else {
 							} else {
@@ -2508,19 +2543,26 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2
 				} break;
 				} break;
 			}
 			}
 
 
+			// Draw the buttons inside the cell.
 			for (int j = p_item->cells[i].buttons.size() - 1; j >= 0; j--) {
 			for (int j = p_item->cells[i].buttons.size() - 1; j >= 0; j--) {
 				Ref<Texture2D> button_texture = p_item->cells[i].buttons[j].texture;
 				Ref<Texture2D> button_texture = p_item->cells[i].buttons[j].texture;
 				Size2 button_size = button_texture->get_size() + theme_cache.button_pressed->get_minimum_size();
 				Size2 button_size = button_texture->get_size() + theme_cache.button_pressed->get_minimum_size();
 
 
 				Point2i button_ofs = Point2i(ofs + item_width_with_buttons - button_size.width, p_pos.y) - theme_cache.offset + p_draw_ofs;
 				Point2i button_ofs = Point2i(ofs + item_width_with_buttons - button_size.width, p_pos.y) - theme_cache.offset + p_draw_ofs;
 
 
-				if (cache.click_type == Cache::CLICK_BUTTON && cache.click_item == p_item && cache.click_column == i && cache.click_index == j && !p_item->cells[i].buttons[j].disabled) {
-					// Being pressed.
+				bool should_draw_pressed = cache.click_type == Cache::CLICK_BUTTON && cache.click_item == p_item && cache.click_column == i && cache.click_index == j && !p_item->cells[i].buttons[j].disabled;
+				bool should_draw_hovered = !should_draw_pressed && !drop_mode_flags && cache.hover_item == p_item && cache.hover_column == i && cache.hover_button_index_in_column == j && !p_item->cells[i].buttons[j].disabled;
+
+				if (should_draw_pressed || should_draw_hovered) {
 					Point2 od = button_ofs;
 					Point2 od = button_ofs;
 					if (rtl) {
 					if (rtl) {
 						od.x = get_size().width - od.x - button_size.x;
 						od.x = get_size().width - od.x - button_size.x;
 					}
 					}
-					theme_cache.button_pressed->draw(get_canvas_item(), Rect2(od.x, od.y, button_size.width, MAX(button_size.height, label_h)));
+					if (should_draw_pressed) {
+						theme_cache.button_pressed->draw(get_canvas_item(), Rect2(od.x, od.y, button_size.width, MAX(button_size.height, label_h)));
+					} else {
+						theme_cache.button_hover->draw(get_canvas_item(), Rect2(od.x, od.y, button_size.width, MAX(button_size.height, label_h)));
+					}
 				}
 				}
 
 
 				button_ofs.y += (label_h - button_size.height) / 2;
 				button_ofs.y += (label_h - button_size.height) / 2;
@@ -2551,6 +2593,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2
 			}
 			}
 		}
 		}
 
 
+		// Draw the folding arrow.
 		if (!p_item->disable_folding && !hide_folding && p_item->first_child && p_item->get_visible_child_count() != 0) { //has visible children, draw the guide box
 		if (!p_item->disable_folding && !hide_folding && p_item->first_child && p_item->get_visible_child_count() != 0) { //has visible children, draw the guide box
 
 
 			Ref<Texture2D> arrow;
 			Ref<Texture2D> arrow;
@@ -2966,6 +3009,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int
 			col_width = MAX(button_w, MIN(limit_w, col_width));
 			col_width = MAX(button_w, MIN(limit_w, col_width));
 		}
 		}
 
 
+		// Cell button detection code.
 		for (int j = c.buttons.size() - 1; j >= 0; j--) {
 		for (int j = c.buttons.size() - 1; j >= 0; j--) {
 			Ref<Texture2D> b = c.buttons[j].texture;
 			Ref<Texture2D> b = c.buttons[j].texture;
 			int w = b->get_size().width + theme_cache.button_pressed->get_minimum_size().width;
 			int w = b->get_size().width + theme_cache.button_pressed->get_minimum_size().width;
@@ -3485,7 +3529,11 @@ bool Tree::_scroll(bool p_horizontal, float p_pages) {
 	double prev_value = scroll->get_value();
 	double prev_value = scroll->get_value();
 	scroll->set_value(scroll->get_value() + scroll->get_page() * p_pages);
 	scroll->set_value(scroll->get_value() + scroll->get_page() * p_pages);
 
 
-	return scroll->get_value() != prev_value;
+	bool scroll_happened = scroll->get_value() != prev_value;
+	if (scroll_happened) {
+		_determine_hovered_item();
+	}
+	return scroll_happened;
 }
 }
 
 
 Rect2 Tree::_get_scrollbar_layout_rect() const {
 Rect2 Tree::_get_scrollbar_layout_rect() const {
@@ -3710,92 +3758,10 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) {
 
 
 	Ref<InputEventMouseMotion> mm = p_event;
 	Ref<InputEventMouseMotion> mm = p_event;
 	if (mm.is_valid()) {
 	if (mm.is_valid()) {
-		Ref<StyleBox> bg = theme_cache.panel_style;
-		bool rtl = is_layout_rtl();
-
-		Point2 pos = mm->get_position();
-		if (rtl) {
-			pos.x = get_size().width - pos.x;
-		}
-		pos -= theme_cache.panel_style->get_offset();
-
-		Cache::ClickType old_hover = cache.hover_type;
-		int old_index = cache.hover_index;
-
-		cache.hover_type = Cache::CLICK_NONE;
-		cache.hover_index = 0;
-		if (show_column_titles) {
-			pos.y -= _get_title_button_height();
-			if (pos.y < 0) {
-				pos.x += theme_cache.offset.x;
-				int len = 0;
-				for (int i = 0; i < columns.size(); i++) {
-					len += get_column_width(i);
-					if (pos.x < len) {
-						cache.hover_type = Cache::CLICK_TITLE;
-						cache.hover_index = i;
-						break;
-					}
-				}
-			}
-		}
-
-		if (root) {
-			Point2 mpos = mm->get_position();
-			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();
-				}
-
-				TreeItem *old_it = cache.hover_item;
-				int old_col = cache.hover_cell;
-
-				int col, h, section;
-				TreeItem *it = _find_item_at_pos(root, mpos, col, h, section);
-
-				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();
-					}
-				}
-
-				cache.hover_item = it;
-				cache.hover_cell = col;
-
-				if (it != old_it || col != old_col) {
-					if (old_it && old_col >= old_it->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_it && old_it->cells[old_col].custom_button;
-						bool is_over_button = it && it->cells[col].custom_button;
-						if (was_over_button || is_over_button) {
-							queue_redraw();
-						}
-					}
-				}
-			}
-		}
-
-		// Update if mouse enters/exits columns
-		if (cache.hover_type != old_hover || cache.hover_index != old_index) {
-			queue_redraw();
-		}
+		hovered_pos = mm->get_position();
+		_determine_hovered_item();
 
 
+		bool rtl = is_layout_rtl();
 		if (pressing_for_editor && popup_pressing_edited_item && (popup_pressing_edited_item->get_cell_mode(popup_pressing_edited_item_column) == TreeItem::CELL_MODE_RANGE)) {
 		if (pressing_for_editor && popup_pressing_edited_item && (popup_pressing_edited_item->get_cell_mode(popup_pressing_edited_item_column) == TreeItem::CELL_MODE_RANGE)) {
 			/* This needs to happen now, because the popup can be closed when pressing another item, and must remain the popup edited item until it actually closes */
 			/* This needs to happen now, because the popup can be closed when pressing another item, and must remain the popup edited item until it actually closes */
 			popup_edited_item = popup_pressing_edited_item;
 			popup_edited_item = popup_pressing_edited_item;
@@ -4080,6 +4046,174 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) {
 	}
 	}
 }
 }
 
 
+void Tree::_determine_hovered_item() {
+	Ref<StyleBox> bg = theme_cache.panel_style;
+	bool rtl = is_layout_rtl();
+
+	Point2 pos = hovered_pos;
+	if (rtl) {
+		pos.x = get_size().width - pos.x;
+	}
+	pos -= theme_cache.panel_style->get_offset();
+
+	bool old_header_row = cache.hover_header_row;
+	int old_header_column = cache.hover_header_column;
+	TreeItem *old_item = cache.hover_item;
+	int old_column = cache.hover_column;
+	int old_button_index_in_column = cache.hover_button_index_in_column;
+
+	// Determine hover on column headers.
+	cache.hover_header_row = false;
+	cache.hover_header_column = 0;
+	if (show_column_titles && is_mouse_hovering) {
+		pos.y -= _get_title_button_height();
+		if (pos.y < 0) {
+			pos.x += theme_cache.offset.x;
+			int len = 0;
+			for (int i = 0; i < columns.size(); i++) {
+				len += get_column_width(i);
+				if (pos.x < len) {
+					cache.hover_header_row = true;
+					cache.hover_header_column = i;
+					cache.hover_button_index_in_column = -1;
+					break;
+				}
+			}
+		}
+	}
+
+	// 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 (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();
+				}
+			}
+
+			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().
+					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();
+					}
+				}
+			}
+		}
+	}
+
+	// Reduce useless redraw calls.
+
+	bool hovered_cell_button_changed = (cache.hover_button_index_in_column != old_button_index_in_column);
+	bool hovered_column_changed = (cache.hover_column != old_column);
+
+	// Mouse has moved from row to row, or from cell to cell within same row unless selection mode is full row which saves a useless redraw.
+	bool item_hover_needs_redraw = !cache.hover_header_row && (cache.hover_item != old_item || hovered_cell_button_changed || (select_mode != SELECT_ROW && hovered_column_changed));
+	// Mouse has moved between two different column header sections.
+	bool header_hover_needs_redraw = cache.hover_header_row && cache.hover_header_column != old_header_column;
+	// Mouse has moved between header and "main" areas.
+	bool whole_needs_redraw = cache.hover_header_row != old_header_row;
+
+	if (whole_needs_redraw || header_hover_needs_redraw || item_hover_needs_redraw) {
+		queue_redraw();
+	}
+}
+
 bool Tree::edit_selected(bool p_force_edit) {
 bool Tree::edit_selected(bool p_force_edit) {
 	TreeItem *s = get_selected();
 	TreeItem *s = get_selected();
 	ERR_FAIL_NULL_V_MSG(s, false, "No item selected.");
 	ERR_FAIL_NULL_V_MSG(s, false, "No item selected.");
@@ -4304,9 +4438,20 @@ void Tree::_notification(int p_what) {
 			}
 			}
 		} break;
 		} break;
 
 
+		case NOTIFICATION_MOUSE_ENTER: {
+			is_mouse_hovering = true;
+			_determine_hovered_item();
+		} break;
+
 		case NOTIFICATION_MOUSE_EXIT: {
 		case NOTIFICATION_MOUSE_EXIT: {
-			if (cache.hover_type != Cache::CLICK_NONE) {
-				cache.hover_type = Cache::CLICK_NONE;
+			is_mouse_hovering = false;
+			// Clear hovered item cache.
+			if (cache.hover_header_row || cache.hover_item != nullptr) {
+				cache.hover_header_row = false;
+				cache.hover_header_column = -1;
+				cache.hover_item = nullptr;
+				cache.hover_column = -1;
+				cache.hover_button_index_in_column = -1;
 				queue_redraw();
 				queue_redraw();
 			}
 			}
 		} break;
 		} break;
@@ -4420,7 +4565,7 @@ void Tree::_notification(int p_what) {
 				//title buttons
 				//title buttons
 				int ofs2 = theme_cache.panel_style->get_margin(SIDE_LEFT);
 				int ofs2 = theme_cache.panel_style->get_margin(SIDE_LEFT);
 				for (int i = 0; i < columns.size(); i++) {
 				for (int i = 0; i < columns.size(); i++) {
-					Ref<StyleBox> sb = (cache.click_type == Cache::CLICK_TITLE && cache.click_index == i) ? theme_cache.title_button_pressed : ((cache.hover_type == Cache::CLICK_TITLE && cache.hover_index == i) ? theme_cache.title_button_hover : theme_cache.title_button);
+					Ref<StyleBox> sb = (cache.click_type == Cache::CLICK_TITLE && cache.click_index == i) ? theme_cache.title_button_pressed : ((cache.hover_header_row && cache.hover_header_column == i) ? theme_cache.title_button_hover : theme_cache.title_button);
 					Rect2 tbrect = Rect2(ofs2 - theme_cache.offset.x, bg->get_margin(SIDE_TOP), get_column_width(i), tbh);
 					Rect2 tbrect = Rect2(ofs2 - theme_cache.offset.x, bg->get_margin(SIDE_TOP), get_column_width(i), tbh);
 					if (cache.rtl) {
 					if (cache.rtl) {
 						tbrect.position.x = get_size().width - tbrect.size.x - tbrect.position.x;
 						tbrect.position.x = get_size().width - tbrect.size.x - tbrect.position.x;
@@ -4536,6 +4681,8 @@ TreeItem *Tree::create_item(TreeItem *p_parent, int p_index) {
 		}
 		}
 	}
 	}
 
 
+	_determine_hovered_item();
+
 	return ti;
 	return ti;
 }
 }
 
 
@@ -4679,6 +4826,8 @@ void Tree::clear() {
 	popup_edited_item = nullptr;
 	popup_edited_item = nullptr;
 	popup_pressing_edited_item = nullptr;
 	popup_pressing_edited_item = nullptr;
 
 
+	_determine_hovered_item();
+
 	queue_redraw();
 	queue_redraw();
 };
 };
 
 
@@ -4931,6 +5080,7 @@ int Tree::get_columns() const {
 }
 }
 
 
 void Tree::_scroll_moved(float) {
 void Tree::_scroll_moved(float) {
+	_determine_hovered_item();
 	queue_redraw();
 	queue_redraw();
 }
 }
 
 
@@ -4972,7 +5122,47 @@ int Tree::get_item_offset(TreeItem *p_item) const {
 		}
 		}
 	}
 	}
 
 
-	return -1; //not found
+	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() {
 void Tree::ensure_cursor_is_visible() {
@@ -5803,10 +5993,13 @@ void Tree::_bind_methods() {
 	BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_FONT, Tree, tb_font, "title_button_font");
 	BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_FONT, Tree, tb_font, "title_button_font");
 	BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_FONT_SIZE, Tree, tb_font_size, "title_button_font_size");
 	BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_FONT_SIZE, Tree, tb_font_size, "title_button_font_size");
 
 
+	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, hovered);
+	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, hovered_dimmed);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, selected);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, selected);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, selected_focus);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, selected_focus);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, cursor);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, cursor);
 	BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, Tree, cursor_unfocus, "cursor_unfocused");
 	BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, Tree, cursor_unfocus, "cursor_unfocused");
+	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, button_hover);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, button_pressed);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, button_pressed);
 
 
 	BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Tree, checked);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Tree, checked);
@@ -5827,6 +6020,8 @@ void Tree::_bind_methods() {
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, custom_button_font_highlight);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, custom_button_font_highlight);
 
 
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, font_color);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, font_color);
+	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, font_hovered_color);
+	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, font_hovered_dimmed_color);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, font_selected_color);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, font_selected_color);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, font_disabled_color);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, font_disabled_color);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, drop_position_color);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Tree, drop_position_color);

+ 15 - 3
scene/gui/tree.h

@@ -450,6 +450,9 @@ private:
 	Vector2 pressing_pos;
 	Vector2 pressing_pos;
 	Rect2 pressing_item_rect;
 	Rect2 pressing_item_rect;
 
 
+	Vector2 hovered_pos;
+	bool is_mouse_hovering = false;
+
 	float range_drag_base = 0.0;
 	float range_drag_base = 0.0;
 	bool range_drag_enabled = false;
 	bool range_drag_enabled = false;
 	Vector2 range_drag_capture_pos;
 	Vector2 range_drag_capture_pos;
@@ -545,10 +548,13 @@ private:
 		int font_size = 0;
 		int font_size = 0;
 		int tb_font_size = 0;
 		int tb_font_size = 0;
 
 
+		Ref<StyleBox> hovered;
+		Ref<StyleBox> hovered_dimmed;
 		Ref<StyleBox> selected;
 		Ref<StyleBox> selected;
 		Ref<StyleBox> selected_focus;
 		Ref<StyleBox> selected_focus;
 		Ref<StyleBox> cursor;
 		Ref<StyleBox> cursor;
 		Ref<StyleBox> cursor_unfocus;
 		Ref<StyleBox> cursor_unfocus;
+		Ref<StyleBox> button_hover;
 		Ref<StyleBox> button_pressed;
 		Ref<StyleBox> button_pressed;
 		Ref<StyleBox> title_button;
 		Ref<StyleBox> title_button;
 		Ref<StyleBox> title_button_hover;
 		Ref<StyleBox> title_button_hover;
@@ -572,6 +578,8 @@ private:
 		Ref<Texture2D> updown;
 		Ref<Texture2D> updown;
 
 
 		Color font_color;
 		Color font_color;
+		Color font_hovered_color;
+		Color font_hovered_dimmed_color;
 		Color font_selected_color;
 		Color font_selected_color;
 		Color font_disabled_color;
 		Color font_disabled_color;
 		Color guide_color;
 		Color guide_color;
@@ -623,16 +631,17 @@ private:
 		};
 		};
 
 
 		ClickType click_type = Cache::CLICK_NONE;
 		ClickType click_type = Cache::CLICK_NONE;
-		ClickType hover_type = Cache::CLICK_NONE;
 		int click_index = -1;
 		int click_index = -1;
 		int click_id = -1;
 		int click_id = -1;
 		TreeItem *click_item = nullptr;
 		TreeItem *click_item = nullptr;
 		int click_column = 0;
 		int click_column = 0;
-		int hover_index = -1;
+		int hover_header_column = -1;
+		bool hover_header_row = false;
 		Point2 click_pos;
 		Point2 click_pos;
 
 
 		TreeItem *hover_item = nullptr;
 		TreeItem *hover_item = nullptr;
-		int hover_cell = -1;
+		int hover_column = -1;
+		int hover_button_index_in_column = -1;
 
 
 		bool rtl = false;
 		bool rtl = false;
 	} cache;
 	} cache;
@@ -660,6 +669,7 @@ private:
 	TreeItem *_search_item_text(TreeItem *p_at, const String &p_find, int *r_col, bool p_selectable, bool p_backwards = false);
 	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 &h, int &section) const;
 	TreeItem *_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_column, int &h, int &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) const;
 
 
@@ -689,6 +699,8 @@ private:
 
 
 	bool enable_recursive_folding = true;
 	bool enable_recursive_folding = true;
 
 
+	void _determine_hovered_item();
+
 	int _count_selected_items(TreeItem *p_from) const;
 	int _count_selected_items(TreeItem *p_from) const;
 	bool _is_branch_selected(TreeItem *p_from) const;
 	bool _is_branch_selected(TreeItem *p_from) const;
 	bool _is_sibling_branch_selected(TreeItem *p_from) const;
 	bool _is_sibling_branch_selected(TreeItem *p_from) const;

+ 5 - 0
scene/theme/default_theme.cpp

@@ -832,10 +832,13 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 
 
 	theme->set_stylebox(SceneStringName(panel), "Tree", make_flat_stylebox(style_normal_color, 4, 4, 4, 5));
 	theme->set_stylebox(SceneStringName(panel), "Tree", make_flat_stylebox(style_normal_color, 4, 4, 4, 5));
 	theme->set_stylebox("focus", "Tree", focus);
 	theme->set_stylebox("focus", "Tree", focus);
+	theme->set_stylebox("hovered", "Tree", make_flat_stylebox(Color(1, 1, 1, 0.07)));
+	theme->set_stylebox("hovered_dimmed", "Tree", make_flat_stylebox(Color(1, 1, 1, 0.03)));
 	theme->set_stylebox("selected", "Tree", make_flat_stylebox(style_selected_color));
 	theme->set_stylebox("selected", "Tree", make_flat_stylebox(style_selected_color));
 	theme->set_stylebox("selected_focus", "Tree", make_flat_stylebox(style_selected_color));
 	theme->set_stylebox("selected_focus", "Tree", make_flat_stylebox(style_selected_color));
 	theme->set_stylebox("cursor", "Tree", focus);
 	theme->set_stylebox("cursor", "Tree", focus);
 	theme->set_stylebox("cursor_unfocused", "Tree", focus);
 	theme->set_stylebox("cursor_unfocused", "Tree", focus);
+	theme->set_stylebox("button_hover", "Tree", make_flat_stylebox(Color(1, 1, 1, 0.07)));
 	theme->set_stylebox("button_pressed", "Tree", button_pressed);
 	theme->set_stylebox("button_pressed", "Tree", button_pressed);
 	theme->set_stylebox("title_button_normal", "Tree", make_flat_stylebox(style_pressed_color, 4, 4, 4, 4));
 	theme->set_stylebox("title_button_normal", "Tree", make_flat_stylebox(style_pressed_color, 4, 4, 4, 4));
 	theme->set_stylebox("title_button_pressed", "Tree", make_flat_stylebox(style_hover_color, 4, 4, 4, 4));
 	theme->set_stylebox("title_button_pressed", "Tree", make_flat_stylebox(style_hover_color, 4, 4, 4, 4));
@@ -863,6 +866,8 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 
 
 	theme->set_color("title_button_color", "Tree", control_font_color);
 	theme->set_color("title_button_color", "Tree", control_font_color);
 	theme->set_color(SceneStringName(font_color), "Tree", control_font_low_color);
 	theme->set_color(SceneStringName(font_color), "Tree", control_font_low_color);
+	theme->set_color("font_hovered_color", "Tree", control_font_hover_color);
+	theme->set_color("font_hovered_dimmed_color", "Tree", control_font_color);
 	theme->set_color("font_selected_color", "Tree", control_font_pressed_color);
 	theme->set_color("font_selected_color", "Tree", control_font_pressed_color);
 	theme->set_color("font_disabled_color", "Tree", control_font_disabled_color);
 	theme->set_color("font_disabled_color", "Tree", control_font_disabled_color);
 	theme->set_color("font_outline_color", "Tree", Color(0, 0, 0));
 	theme->set_color("font_outline_color", "Tree", Color(0, 0, 0));