From 452783c62b1e577c940bbbb2fb429b0b46a256ee Mon Sep 17 00:00:00 2001 From: dugramen Date: Tue, 21 Apr 2026 15:54:32 -0400 Subject: [PATCH] Add sticky tree item support --- doc/classes/EditorSettings.xml | 3 + doc/classes/Tree.xml | 3 + editor/settings/editor_settings.cpp | 2 +- editor/themes/editor_theme_manager.cpp | 2 + editor/themes/editor_theme_manager.h | 1 + editor/themes/theme_classic.cpp | 1 + editor/themes/theme_modern.cpp | 1 + scene/gui/tree.cpp | 184 ++++++++++++++++++++----- scene/gui/tree.h | 19 ++- 9 files changed, 178 insertions(+), 38 deletions(-) diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 80d4a751ab..e9f0be7424 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -983,6 +983,9 @@ Expanding main editor window content to the title, if supported by [DisplayServer]. See [constant DisplayServer.WINDOW_FLAG_EXTEND_TO_TITLE]. Specific to the macOS platform. + + The maximum number of tree items allowed to stick to the top while its children are in view. A value of [code]0[/code] disables sticking. + The preferred monitor to display the project manager. diff --git a/doc/classes/Tree.xml b/doc/classes/Tree.xml index 9aa5e070f2..7002b4f3ee 100644 --- a/doc/classes/Tree.xml +++ b/doc/classes/Tree.xml @@ -650,6 +650,9 @@ The maximum distance between the mouse cursor and the control's border to trigger border scrolling when dragging. + + The maximum number of tree items allowed to stick to the top while its children are in view. A value of [code]0[/code] disables sticking. + The speed of border scrolling. diff --git a/editor/settings/editor_settings.cpp b/editor/settings/editor_settings.cpp index 9182680ec7..5b30f4dbea 100644 --- a/editor/settings/editor_settings.cpp +++ b/editor/settings/editor_settings.cpp @@ -59,7 +59,6 @@ #include "scene/gui/file_dialog.h" #include "scene/main/node.h" #include "scene/main/scene_tree.h" -#include "scene/main/window.h" #include "scene/resources/animation.h" #include "servers/display/display_server.h" @@ -506,6 +505,7 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/appearance/use_embedded_menu", false, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_EDITOR_BASIC_SETTING) EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/appearance/use_native_file_dialogs", false, "", PROPERTY_USAGE_DEFAULT) EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/appearance/expand_to_title", true, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED | PROPERTY_USAGE_EDITOR_BASIC_SETTING) + EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_RANGE, "interface/editor/appearance/max_sticky_tree_items", 6, "0,16") EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_RANGE, "interface/editor/fonts/main_font_size", 14, "8,48,1") EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_RANGE, "interface/editor/fonts/code_font_size", 14, "8,48,1") diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index ff04e1bdc7..ac30b03859 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -268,6 +268,7 @@ EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config( config.gizmo_handle_scale = EDITOR_GET("interface/touchscreen/scale_gizmo_handles"); config.subresource_hue_tint = EDITOR_GET("docks/property_editor/subresource_hue_tint"); config.dragging_hover_wait_msec = (float)EDITOR_GET("interface/editor/timers/dragging_hover_wait_seconds") * 1000; + config.max_sticky_tree_items = EDITOR_GET("interface/editor/appearance/max_sticky_tree_items"); // Handle theme style. if (config.preset != "Custom") { @@ -718,6 +719,7 @@ bool EditorThemeManager::is_generated_theme_outdated() { // TODO: We can use this information more intelligently to do partial theme updates and speed things up. outdated_cache = EditorSettings::get_singleton()->check_changed_settings_in_group("interface/theme") || EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/fonts") || + EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/appearance/max_sticky_tree_items") || EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen/enable_touch_optimizations") || EditorSettings::get_singleton()->check_changed_settings_in_group("editors/visual_editors") || EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/theme") || diff --git a/editor/themes/editor_theme_manager.h b/editor/themes/editor_theme_manager.h index 26d858f9e2..d2eb4540f1 100644 --- a/editor/themes/editor_theme_manager.h +++ b/editor/themes/editor_theme_manager.h @@ -80,6 +80,7 @@ public: int inspector_property_height = 28; float subresource_hue_tint = 0.0; float dragging_hover_wait_msec = 0; + int max_sticky_tree_items = 5; // Make sure to keep those in sync with the definitions in the editor settings. const float default_icon_saturation = 2.0; diff --git a/editor/themes/theme_classic.cpp b/editor/themes/theme_classic.cpp index 9b745af1d0..5d74efe87d 100644 --- a/editor/themes/theme_classic.cpp +++ b/editor/themes/theme_classic.cpp @@ -624,6 +624,7 @@ void ThemeClassic::populate_standard_styles(const Ref &p_theme, Edi p_theme->set_constant("icon_h_separation", "Tree", (p_config.increased_margin + 2) * EDSCALE); p_theme->set_constant("button_margin", "Tree", p_config.base_margin * EDSCALE); p_theme->set_constant("dragging_unfold_wait_msec", "Tree", p_config.dragging_hover_wait_msec); + p_theme->set_constant("scroll_max_sticky_items", "Tree", p_config.max_sticky_tree_items); p_theme->set_constant("scroll_border", "Tree", 40 * EDSCALE); p_theme->set_constant("scroll_speed", "Tree", 12); p_theme->set_constant("outline_size", "Tree", 0); diff --git a/editor/themes/theme_modern.cpp b/editor/themes/theme_modern.cpp index eefe3f8a69..5e4fae6b0d 100644 --- a/editor/themes/theme_modern.cpp +++ b/editor/themes/theme_modern.cpp @@ -650,6 +650,7 @@ void ThemeModern::populate_standard_styles(const Ref &p_theme, Edit p_theme->set_constant("icon_h_separation", "Tree", p_config.base_margin * 1.5 * EDSCALE); p_theme->set_constant("button_margin", "Tree", p_config.base_margin * EDSCALE); p_theme->set_constant("dragging_unfold_wait_msec", "Tree", p_config.dragging_hover_wait_msec); + p_theme->set_constant("scroll_max_sticky_items", "Tree", p_config.max_sticky_tree_items); p_theme->set_constant("scroll_border", "Tree", 40 * EDSCALE); p_theme->set_constant("scroll_speed", "Tree", 12); p_theme->set_constant("outline_size", "Tree", 0); diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index 238f4681b7..218d2399bd 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -122,6 +122,7 @@ void TreeItem::_change_tree(Tree *p_tree) { } if (tree) { + tree->sticky_list.clear(); if (tree->root == this) { tree->root = nullptr; } @@ -2129,7 +2130,7 @@ Rect2i Tree::convert_rtl_rect(const Rect2i &p_rect) const { return p_rect; } -void Tree::draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color, int p_ol_size, const Color &p_ol_color) const { +void Tree::draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color, int p_ol_size, const Color &p_ol_color, const RID &p_ci) const { ERR_FAIL_COND(theme_cache.font.is_null()); Rect2i rect = p_rect; @@ -2169,7 +2170,7 @@ void Tree::draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, co break; } - RID ci = content_ci; + RID ci = p_ci; if (rtl) { if (ts.width > 0) { @@ -2319,7 +2320,7 @@ void Tree::update_item_cache(TreeItem *p_item) const { } } -int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 &p_draw_size, TreeItem *p_item, int &r_self_height) { +int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 &p_draw_size, TreeItem *p_item, int &r_self_height, const RID &p_ci) { const real_t bottom_margin = theme_cache.panel_style->get_margin(SIDE_BOTTOM); // Extra stylebox space below the content const real_t draw_height = p_draw_size.height + bottom_margin; // Visible height including bottom margin @@ -2332,9 +2333,11 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 return 0; } - RID ci = content_ci; + RID ci = p_ci; + RID sb_ci = ci == content_ci ? stylebox_ci : ci; int htotal = 0; + int htotal_raw = 0; int label_h = 0; bool rtl = cache.rtl; @@ -2444,22 +2447,22 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 if (p_item->cells[0].selected) { if (is_row_hovered) { if (has_focus(true)) { - theme_cache.hovered_selected_focus->draw(stylebox_ci, row_rect); + theme_cache.hovered_selected_focus->draw(sb_ci, row_rect); } else { - theme_cache.hovered_selected->draw(stylebox_ci, row_rect); + theme_cache.hovered_selected->draw(sb_ci, row_rect); } } else { if (has_focus(true)) { - theme_cache.selected_focus->draw(stylebox_ci, row_rect); + theme_cache.selected_focus->draw(sb_ci, row_rect); } else { - theme_cache.selected->draw(stylebox_ci, row_rect); + theme_cache.selected->draw(sb_ci, row_rect); } } } else if (!drop_mode_flags) { if (is_cell_button_hovered) { - theme_cache.hovered_dimmed->draw(stylebox_ci, row_rect); + theme_cache.hovered_dimmed->draw(sb_ci, row_rect); } else { - theme_cache.hovered->draw(stylebox_ci, row_rect); + theme_cache.hovered->draw(sb_ci, row_rect); } } } @@ -2471,9 +2474,9 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 // Cell hover. if (is_cell_hovered && !p_item->cells[i].selected && !drop_mode_flags) { if (is_cell_button_hovered) { - theme_cache.hovered_dimmed->draw(stylebox_ci, r); + theme_cache.hovered_dimmed->draw(sb_ci, r); } else { - theme_cache.hovered->draw(stylebox_ci, r); + theme_cache.hovered->draw(sb_ci, r); } } } @@ -2487,15 +2490,15 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 if (p_item->cells[i].selected) { if (is_cell_hovered) { if (has_focus(true)) { - theme_cache.hovered_selected_focus->draw(stylebox_ci, r); + theme_cache.hovered_selected_focus->draw(sb_ci, r); } else { - theme_cache.hovered_selected->draw(stylebox_ci, r); + theme_cache.hovered_selected->draw(sb_ci, r); } } else { if (has_focus(true)) { - theme_cache.selected_focus->draw(stylebox_ci, r); + theme_cache.selected_focus->draw(sb_ci, r); } else { - theme_cache.selected->draw(stylebox_ci, r); + theme_cache.selected->draw(sb_ci, r); } } } @@ -2561,7 +2564,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 switch (p_item->cells[i].mode) { case TreeItem::CELL_MODE_STRING: { - draw_item_rect(p_item->cells[i], item_rect, cell_color, icon_col, outline_size, font_outline_color); + draw_item_rect(p_item->cells[i], item_rect, cell_color, icon_col, outline_size, font_outline_color, ci); } break; case TreeItem::CELL_MODE_CHECK: { Point2i check_ofs = item_rect.position; @@ -2596,7 +2599,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 cell_color = theme_cache.font_disabled_color; } - draw_item_rect(p_item->cells[i], item_rect, cell_color, icon_col, outline_size, font_outline_color); + draw_item_rect(p_item->cells[i], item_rect, cell_color, icon_col, outline_size, font_outline_color, ci); } break; case TreeItem::CELL_MODE_RANGE: { @@ -2679,7 +2682,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } if (!p_item->cells[i].editable) { - draw_item_rect(p_item->cells[i], item_rect, cell_color, icon_col, outline_size, font_outline_color); + draw_item_rect(p_item->cells[i], item_rect, cell_color, icon_col, outline_size, font_outline_color, ci); break; } @@ -2707,7 +2710,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 ir.position += theme_cache.custom_button->get_offset(); } - draw_item_rect(p_item->cells[i], ir, cell_color, icon_col, outline_size, font_outline_color); + draw_item_rect(p_item->cells[i], ir, cell_color, icon_col, outline_size, font_outline_color, ci); downarrow->draw(ci, arrow_pos); @@ -2808,10 +2811,11 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 if (!skip) { children_pos.x += theme_cache.item_margin; htotal += label_h; - children_pos.y += htotal; + htotal_raw += label_h; + children_pos.y += htotal_raw; } - if (!p_item->collapsed) { // If not collapsed, check the children. + if (!p_item->collapsed && p_ci == content_ci) { // If not collapsed, check the children. TreeItem *c = p_item->first_child; int base_ofs = children_pos.y - theme_cache.offset.y + p_draw_ofs.y; @@ -2823,7 +2827,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 int child_h = -1; int child_self_height = 0; if (htotal >= 0) { - child_h = draw_item(children_pos, p_draw_ofs, p_draw_size, c, child_self_height); + child_h = draw_item(children_pos, p_draw_ofs, p_draw_size, c, child_self_height, ci); child_self_height += theme_cache.v_separation; } @@ -2940,7 +2944,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 break; // Last loop done, stop. } - if (theme_cache.draw_relationship_lines == 0) { + if (theme_cache.draw_relationship_lines == 0 && theme_cache.scroll_max_sticky_items == 0) { return -1; // No need to draw anymore, full stop. } @@ -2948,6 +2952,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 children_pos.y = theme_cache.offset.y + draw_height; } else { htotal += child_h; + htotal_raw += child_h; children_pos.y += child_h; } @@ -2955,7 +2960,14 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } } - return htotal; + if (theme_cache.scroll_max_sticky_items > 0 && p_ci == content_ci && !p_item->collapsed) { + sticky_candidates.push_back(p_item); + p_item->cached_start = p_pos.y; + p_item->cached_total_end = p_pos.y + htotal_raw; + p_item->cached_label_height = label_h; + } + + return htotal_raw; } int Tree::_count_selected_items(TreeItem *p_from) const { @@ -3145,7 +3157,24 @@ void Tree::_range_click_timeout() { } } -int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int x_limit, bool p_double_click, TreeItem *p_item, MouseButton p_button, const Ref &p_mod) { +int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int x_limit, bool p_double_click, TreeItem *p_item, MouseButton p_button, const Ref &p_mod, bool p_first_call, bool p_skip_children) { + if (p_first_call) { + // Handle sticky item input first + for (TreeItem *item : sticky_list) { + Point2 relative_pos = p_pos; + relative_pos.y -= theme_cache.offset.y; + relative_pos.y -= item->sticky_offset.y; + if (relative_pos.y > item->cached_label_height) { + continue; + } + int result = propagate_mouse_event(relative_pos, x_ofs, y_ofs, x_limit, p_double_click, item, p_button, p_mod, false, true); + if (result < 0) { + return result; + } + } + p_first_call = false; + } + if (p_item && !p_item->is_visible_in_tree()) { // Skip any processing of invisible items. return 0; @@ -3257,12 +3286,18 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int if (select_mode == SELECT_MULTI && icount > 1 && p_button != MouseButton::RIGHT) { if (!already_selected) { select_single_item(p_item, root, col); + if (p_skip_children) { + ensure_cursor_is_visible(); + } } single_select_defer = p_item; single_select_defer_column = col; } else { if (p_button != MouseButton::RIGHT || !c.selected) { select_single_item(p_item, root, col); + if (p_skip_children && p_button != MouseButton::RIGHT) { + ensure_cursor_is_visible(); + } } emit_signal(SNAME("item_mouse_selected"), get_local_mouse_position(), p_button); @@ -3409,12 +3444,12 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int new_pos.y -= item_h; } - if (!p_item->collapsed) { // If not collapsed, check the children. + if ((p_item && !p_item->collapsed) && !p_skip_children) { // If not collapsed, check the children. TreeItem *c = p_item->first_child; while (c) { - int child_h = propagate_mouse_event(new_pos, x_ofs, y_ofs, x_limit, p_double_click, c, p_button, p_mod); + int child_h = propagate_mouse_event(new_pos, x_ofs, y_ofs, x_limit, p_double_click, c, p_button, p_mod, false, false); if (child_h < 0) { return -1; // Break, stop propagating, no need to anymore. @@ -5251,12 +5286,16 @@ void Tree::_notification(int p_what) { rendering_server->canvas_item_set_custom_rect(drop_indicator_ci, !is_visibility_clip_disabled(), main_clip_rect); rendering_server->canvas_item_set_clip(drop_indicator_ci, true); + rendering_server->canvas_item_clear(last_sticky_ci); + rendering_server->canvas_item_set_clip(last_sticky_ci, true); + cache.rtl = is_layout_rtl(); content_scale_factor = popup_editor->is_embedded() ? 1.0 : popup_editor->get_parent_visible_window()->get_content_scale_factor(); + sticky_candidates.clear(); if (root && get_size().x > 0 && get_size().y > 0) { int self_height = 0; // Just to pass a reference, we don't need the root's `self_height`. - draw_item(Point2(), draw_ofs, draw_size, root, self_height); + draw_item(Point2(), draw_ofs, draw_size, root, self_height, content_ci); // Draw drop indicator. if (drop_mode_flags && drop_mode_over) { @@ -5511,6 +5550,55 @@ void Tree::_notification(int p_what) { } } + sticky_stack_end = 0; + if (root) { + sticky_list.clear(); + Vector2 stick_ofs; + Vector2 last_ofs = stick_ofs; + real_t height_limit = get_size().y * .4; // Prevent sticky items from ever exceeding 40% of the space + + // Filter sticky candidates and cache their sticky positions + for (int i = sticky_candidates.size() - 1; i >= 0; i--) { + TreeItem *item = sticky_candidates[i]; + bool past_top = item->cached_start - theme_cache.offset.y < stick_ofs.y; + bool stickable = !item->collapsed && item->get_child_count() > 0; + bool hidden_root = hide_root && item == root; + if (past_top && stickable && !hidden_root) { + if (item->cached_total_end - theme_cache.offset.y > stick_ofs.y) { + last_ofs = stick_ofs; + stick_ofs.y = MIN(stick_ofs.y, item->cached_total_end - theme_cache.offset.y - item->cached_label_height); + item->sticky_offset = stick_ofs; + sticky_list.push_back(item); + stick_ofs.x += theme_cache.item_margin; + stick_ofs.y += item->cached_label_height; + } + } + if ((int)sticky_list.size() >= theme_cache.scroll_max_sticky_items || stick_ofs.y >= height_limit) { + break; + } + } + + sticky_stack_end = stick_ofs.y; + for (int i = sticky_list.size() - 1; i >= 0; i--) { + bool is_last = i == (int)sticky_list.size() - 1; + TreeItem *item = sticky_list[i]; + + // Draw sticky items without a scroll offset temporarily + Point2 old_offset = theme_cache.offset; + theme_cache.offset.y = 0; + int self_height = 0; + draw_item(item->sticky_offset, draw_ofs, draw_size, item, self_height, is_last ? last_sticky_ci : header_ci); + theme_cache.offset = old_offset; + } + + if (!sticky_list.is_empty()) { + rendering_server->canvas_item_set_custom_rect(last_sticky_ci, !is_visibility_clip_disabled(), Rect2(0, last_ofs.y + draw_ofs.y, get_size().x, sticky_stack_end - last_ofs.y)); + rendering_server->canvas_item_set_custom_rect(content_ci, !is_visibility_clip_disabled(), main_clip_rect.grow_side(SIDE_TOP, -(sticky_stack_end + content_rect.position.y))); + rendering_server->canvas_item_set_custom_rect(stylebox_ci, !is_visibility_clip_disabled(), main_clip_rect.grow_side(SIDE_TOP, -(sticky_stack_end + content_rect.position.y))); + rendering_server->canvas_item_set_custom_rect(header_ci, !is_visibility_clip_disabled(), header_clip_rect.grow_side(SIDE_BOTTOM, last_ofs.y + content_rect.position.y)); + } + } + if (scroll_hint_mode != SCROLL_HINT_MODE_DISABLED) { Size2 size = get_size(); float v_scroll_value = v_scroll->get_value(); @@ -5518,7 +5606,7 @@ void Tree::_notification(int p_what) { if (v_scroll_value > 1 || v_scroll_below_max) { int hint_height = theme_cache.scroll_hint->get_height(); if ((scroll_hint_mode == SCROLL_HINT_MODE_BOTH || scroll_hint_mode == SCROLL_HINT_MODE_TOP) && v_scroll_value > 1) { - draw_texture_rect(theme_cache.scroll_hint, Rect2(Point2(), Size2(size.width, hint_height)), tile_scroll_hint, theme_cache.scroll_hint_color); + draw_texture_rect(theme_cache.scroll_hint, Rect2(0, sticky_stack_end > 0 ? sticky_stack_end + draw_ofs.y : 0, size.width, hint_height), tile_scroll_hint, theme_cache.scroll_hint_color); } if ((scroll_hint_mode == SCROLL_HINT_MODE_BOTH || scroll_hint_mode == SCROLL_HINT_MODE_BOTTOM) && v_scroll_below_max) { draw_texture_rect(theme_cache.scroll_hint, Rect2(Point2(0, size.height - hint_height), Size2(size.width, -hint_height)), tile_scroll_hint, theme_cache.scroll_hint_color); @@ -5561,6 +5649,7 @@ void Tree::set_self_modulate(const Color &p_self_modulate) { CanvasItem::set_self_modulate(p_self_modulate); RS::get_singleton()->canvas_item_set_self_modulate(header_ci, p_self_modulate); + RS::get_singleton()->canvas_item_set_self_modulate(last_sticky_ci, p_self_modulate); RS::get_singleton()->canvas_item_set_self_modulate(content_ci, p_self_modulate); RS::get_singleton()->canvas_item_set_self_modulate(custom_ci, p_self_modulate); RS::get_singleton()->canvas_item_set_self_modulate(stylebox_ci, p_self_modulate); @@ -6131,8 +6220,21 @@ void Tree::ensure_cursor_is_visible() { const int tbh = _get_title_button_height(); y_offset -= tbh; + bool selected_is_sticky = false; + for (TreeItem *item : sticky_list) { + if (selected_item == item) { + y_offset -= item->sticky_offset.y; + selected_is_sticky = true; + break; + } + } + + if (!selected_is_sticky) { + y_offset -= sticky_stack_end; + } + const int cell_h = compute_item_height(selected_item) + theme_cache.v_separation; - int screen_h = area_size.height - tbh; + int screen_h = area_size.height - tbh - sticky_stack_end; if (cell_h > screen_h) { // Screen size is too small, maybe it was not resized yet. delta_v = y_offset - v_scroll->get_value(); @@ -6555,7 +6657,7 @@ void Tree::_do_incr_search(const String &p_add) { ensure_cursor_is_visible(); } -TreeItem *Tree::_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_column, int &r_height, int &r_section) const { +TreeItem *Tree::_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_column, int &r_height, int &r_section, bool p_sticky_check) const { r_column = -1; r_height = 0; r_section = COLUMN_NOT_FOUND; @@ -6564,6 +6666,16 @@ TreeItem *Tree::_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_ return nullptr; } + if (p_sticky_check) { + Point2 relative_pos = p_pos - theme_cache.offset; + for (TreeItem *item : sticky_list) { + if (item && relative_pos.y >= item->sticky_offset.y && relative_pos.y < item->sticky_offset.y + item->cached_label_height) { + item = _find_item_at_pos(item, relative_pos - Point2(0, item->cached_start), r_column, r_height, r_section, false); + return item; + } + } + } + Point2 pos = p_pos; // Calculate target parent based on horizontal mouse position. @@ -6690,7 +6802,7 @@ TreeItem *Tree::_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_ TreeItem *n = p_item->get_first_child(); while (n) { int ch; - TreeItem *r = _find_item_at_pos(n, pos, r_column, ch, r_section); + TreeItem *r = _find_item_at_pos(n, pos, r_column, ch, r_section, false); pos.y -= ch; r_height += ch; if (r) { @@ -7406,6 +7518,7 @@ void Tree::_bind_methods() { BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Tree, scroll_border); BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Tree, scroll_speed); + BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Tree, scroll_max_sticky_items); BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Tree, scrollbar_margin_top); BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Tree, scrollbar_margin_right); @@ -7455,6 +7568,10 @@ Tree::Tree() { rs->canvas_item_set_parent(drop_indicator_ci, get_canvas_item()); rs->canvas_item_set_use_parent_material(drop_indicator_ci, true); + last_sticky_ci = rs->canvas_item_create(); + rs->canvas_item_set_parent(last_sticky_ci, get_canvas_item()); + rs->canvas_item_set_use_parent_material(last_sticky_ci, true); + popup_editor = memnew(Popup); add_child(popup_editor, false, INTERNAL_MODE_FRONT); @@ -7512,4 +7629,5 @@ Tree::~Tree() { RenderingServer::get_singleton()->free_rid(header_ci); RenderingServer::get_singleton()->free_rid(custom_ci); RenderingServer::get_singleton()->free_rid(stylebox_ci); + RenderingServer::get_singleton()->free_rid(last_sticky_ci); } diff --git a/scene/gui/tree.h b/scene/gui/tree.h index 6bef6adfcb..a27f96f375 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -156,6 +156,11 @@ private: TreeItem *first_child = nullptr; TreeItem *last_child = nullptr; + int cached_start = 0; + int cached_label_height = 0; + int cached_total_end = 0; + Point2 sticky_offset; + LocalVector children_cache; bool is_root = false; // For tree root. Tree *tree = nullptr; // Tree (for reference). @@ -517,6 +522,10 @@ private: bool propagate_mouse_activated = false; float content_scale_factor = 0.0; + int sticky_stack_end = 0; + LocalVector sticky_candidates; + LocalVector sticky_list; + Rect2 custom_popup_rect; int edited_col = -1; int selected_col = -1; @@ -560,6 +569,7 @@ private: RID custom_ci; // Separate canvas item for drawing custom content. RID header_ci; // Separate canvas item for drawing column headers. RID content_ci; // Separate canvas item for drawing tree rows. + RID last_sticky_ci; // Separate canvas item for drawing the last sticky item, which can partially hide underneath other sticky items. RID drop_indicator_ci; VBoxContainer *popup_editor_vb = nullptr; @@ -587,10 +597,10 @@ private: void update_column(int p_col); void update_item_cell(TreeItem *p_item, int p_col) const; void update_item_cache(TreeItem *p_item) const; - void draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color, int p_ol_size, const Color &p_ol_color) const; - int draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 &p_draw_size, TreeItem *p_item, int &r_self_height); + void draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color, int p_ol_size, const Color &p_ol_color, const RID &p_ci) const; + int draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 &p_draw_size, TreeItem *p_item, int &r_self_height, const RID &p_ci); void select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_col, TreeItem *p_prev = nullptr, bool *r_in_range = nullptr, bool p_force_deselect = false); - int propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int x_limit, bool p_double_click, TreeItem *p_item, MouseButton p_button, const Ref &p_mod); + int propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int x_limit, bool p_double_click, TreeItem *p_item, MouseButton p_button, const Ref &p_mod, bool p_first_call = true, bool p_skip_children = false); void _line_editor_submit(String p_text); void _apply_multiline_edit(bool p_hide_focus = false); void _text_editor_popup_modal_close(); @@ -693,6 +703,7 @@ private: int scroll_border = 0; int scroll_speed = 0; + int scroll_max_sticky_items = 5; int scrollbar_margin_top = -1; int scrollbar_margin_right = -1; @@ -748,7 +759,7 @@ private: 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; + TreeItem *_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_column, int &r_height, int &r_section, bool p_sticky_check = true) const; void _find_button_at_pos(const Point2 &p_pos, TreeItem *&r_item, int &r_column, int &r_index, int &r_section) const;