From 78e330923dc77cf77252b5970ed8cde8d62501d3 Mon Sep 17 00:00:00 2001 From: Julian Eisel Date: Wed, 2 Oct 2024 11:19:49 +0200 Subject: [PATCH] UI: Tree-view scrolling and resizing support Implements scrolling support for tree-views, as well as changing the size of the scroll-able tree-view. This is important to be able to place them in many places in the UI, where a compact layout is preferred over an every expanding one (causing following contents to be scrolled out of view). UI-lists would use scrolling and resizing to ensure this, now tree-views are on par. Enables scrolling and resizing for: - Bone collection UI (`UILayout.template_bone_collection_tree()`) - Grease Pencil layer UI (`UILayout.template_grease_pencil_layer_tree()`) - Light link collection UI (`UILayout.template_light_linking_collection()`) - UI to define a node tree interface (`UILayout.template_node_tree_interface()`) These are all cases where compact UIs make more sense than expanding ones. Internally this is enabled by calling the `set_default_rows()` method of the tree-view, although the API might change still. It shouldn't be quite simple to implement this for grid-views too if necessary, or other potential view types. Although I'd like to do some smaller code quality improvements still, this feature is important for some other modules (e.g. grease pencil module for the layers UI in grease pencil v3), so I decided to prioritize merging this. Pull Request: https://projects.blender.org/blender/blender/pulls/119668 --- .../keyconfig/keymap_data/blender_default.py | 2 + .../editors/include/UI_abstract_view.hh | 10 +- .../blender/editors/include/UI_tree_view.hh | 31 +- .../editors/interface/interface_ops.cc | 80 ++++- ...interface_template_bone_collection_tree.cc | 2 +- ...rface_template_grease_pencil_layer_tree.cc | 2 +- .../interface_template_light_linking.cc | 2 +- .../interface_template_node_tree_interface.cc | 2 +- .../editors/interface/views/abstract_view.cc | 12 +- .../editors/interface/views/interface_view.cc | 2 +- .../editors/interface/views/tree_view.cc | 275 ++++++++++++++---- 11 files changed, 341 insertions(+), 79 deletions(-) diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index f5070386cde..a8760218ff3 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -1012,6 +1012,8 @@ def km_user_interface(_params): ("ui.list_start_filter", {"type": 'F', "value": 'PRESS', "ctrl": True}, None), # UI views (polls check if there's a UI view under the cursor). ("ui.view_start_filter", {"type": 'F', "value": 'PRESS', "ctrl": True}, None), + ("ui.view_scroll", {"type": 'WHEELUPMOUSE', "value": 'ANY'}, None), + ("ui.view_scroll", {"type": 'WHEELDOWNMOUSE', "value": 'ANY'}, None), ]) return keymap diff --git a/source/blender/editors/include/UI_abstract_view.hh b/source/blender/editors/include/UI_abstract_view.hh index f20e584ba83..4b667e75166 100644 --- a/source/blender/editors/include/UI_abstract_view.hh +++ b/source/blender/editors/include/UI_abstract_view.hh @@ -46,6 +46,11 @@ namespace blender::ui { class AbstractViewItem; class AbstractViewItemDragController; +enum class ViewScrollDirection { + UP, + DOWN, +}; + class AbstractView { friend class AbstractViewItem; friend struct ::ViewLink; @@ -92,10 +97,13 @@ class AbstractView { */ virtual bool begin_filtering(const bContext &C) const; - virtual void draw_overlays(const ARegion ®ion) const; + virtual void draw_overlays(const ARegion ®ion, const uiBlock &block) const; virtual void foreach_view_item(FunctionRef iter_fn) const = 0; + virtual bool supports_scrolling() const; + virtual void scroll(ViewScrollDirection direction); + /** * Makes \a item valid for display in this view. Behavior is undefined for items not registered * with this. diff --git a/source/blender/editors/include/UI_tree_view.hh b/source/blender/editors/include/UI_tree_view.hh index 3ecf36d5d41..cf8d0b891bd 100644 --- a/source/blender/editors/include/UI_tree_view.hh +++ b/source/blender/editors/include/UI_tree_view.hh @@ -112,29 +112,36 @@ using TreeViewOrItem = TreeViewItemContainer; * \{ */ class AbstractTreeView : public AbstractView, public TreeViewItemContainer { - int min_rows_ = 0; + /* Shared pointer so the pointer can be kept persistent over redraws. The grip button gets a + * pointer to modify the value on resizing, and it uses it to identify the button over redraws.*/ + /* TODO support region zoom. */ + std::shared_ptr custom_height_ = nullptr; + std::shared_ptr scroll_value_ = nullptr; friend class AbstractTreeViewItem; friend class TreeViewBuilder; + friend class TreeViewLayoutBuilder; friend class TreeViewItemDropTarget; public: /* virtual */ ~AbstractTreeView() override = default; - void draw_overlays(const ARegion ®ion) const override; + void draw_overlays(const ARegion ®ion, const uiBlock &block) const override; void foreach_item(ItemIterFn iter_fn, IterOptions options = IterOptions::None) const; + void scroll(ViewScrollDirection direction) override; + /** * \param xy: The mouse coordinates in window space. */ AbstractTreeViewItem *find_hovered(const ARegion ®ion, const int2 &xy); - /** Visual feature: Define a number of item rows the view will always show at minimum. If there + /** Visual feature: Define a number of item rows the view will show by default. If there * are fewer items, empty dummy items will be added. These contribute to the view bounds, so the * drop target of the view includes them, but they are not interactive (e.g. no mouse-hover * highlight). */ - void set_min_rows(int min_rows); + void set_default_rows(int min_rows); protected: virtual void build_tree() = 0; @@ -146,14 +153,18 @@ class AbstractTreeView : public AbstractView, public TreeViewItemContainer { const TreeViewOrItem &old_items); static AbstractTreeViewItem *find_matching_child( const AbstractTreeViewItem &lookup_item, const Span possible_items); + std::optional tot_visible_row_count() const; - void draw_hierarchy_lines(const ARegion ®ion) const; - void draw_hierarchy_lines_recursive(const ARegion ®ion, - const TreeViewOrItem &parent, - const uint pos, - const float aspect) const; + bool supports_scrolling() const override; - AbstractTreeViewItem *find_last_visible_descendant(const AbstractTreeViewItem &parent) const; + void draw_hierarchy_lines(const ARegion ®ion, const uiBlock &block) const; + void get_hierarchy_lines(const ARegion ®ion, + const TreeViewOrItem &parent, + const float aspect, + Vector> &lines, + int &visible_item_index) const; + + int count_visible_descendants(const AbstractTreeViewItem &parent) const; }; /** \} */ diff --git a/source/blender/editors/interface/interface_ops.cc b/source/blender/editors/interface/interface_ops.cc index 0d986033330..431a83d6cd8 100644 --- a/source/blender/editors/interface/interface_ops.cc +++ b/source/blender/editors/interface/interface_ops.cc @@ -2521,18 +2521,23 @@ static void UI_OT_list_start_filter(wmOperatorType *ot) /** \name UI View Start Filter Operator * \{ */ -static bool ui_view_focused_poll(bContext *C) +static AbstractView *get_view_focused(bContext *C) { const wmWindow *win = CTX_wm_window(C); if (!(win && win->eventstate)) { - return false; + return nullptr; } const ARegion *region = CTX_wm_region(C); if (!region) { - return false; + return nullptr; } - const blender::ui::AbstractView *view = UI_region_view_find_at(region, win->eventstate->xy, 0); + return UI_region_view_find_at(region, win->eventstate->xy, 0); +} + +static bool ui_view_focused_poll(bContext *C) +{ + const AbstractView *view = get_view_focused(C); return view != nullptr; } @@ -2613,6 +2618,72 @@ static void UI_OT_view_drop(wmOperatorType *ot) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name UI View Drop Operator + * \{ */ + +static bool ui_view_scroll_poll(bContext *C) +{ + const AbstractView *view = get_view_focused(C); + if (!view) { + return false; + } + + return view->supports_scrolling(); +} + +static int ui_view_scroll_invoke(bContext *C, wmOperator * /*op*/, const wmEvent *event) +{ + ARegion *region = CTX_wm_region(C); + int type = event->type; + bool invert_direction = false; + + if (type == MOUSEPAN) { + int dummy_val; + ui_pan_to_scroll(event, &type, &dummy_val); + + /* 'ui_pan_to_scroll' gives the absolute direction. */ + if (event->flag & WM_EVENT_SCROLL_INVERT) { + invert_direction = true; + } + } + + AbstractView *view = get_view_focused(C); + std::optional direction = + [type, invert_direction]() -> std::optional { + switch (type) { + case WHEELUPMOUSE: + return invert_direction ? ViewScrollDirection::DOWN : ViewScrollDirection::UP; + case WHEELDOWNMOUSE: + return invert_direction ? ViewScrollDirection::UP : ViewScrollDirection::DOWN; + default: + return std::nullopt; + } + }(); + if (!direction) { + return OPERATOR_CANCELLED; + } + + BLI_assert(view->supports_scrolling()); + view->scroll(*direction); + + ED_region_tag_redraw(region); + return OPERATOR_FINISHED; +} + +static void UI_OT_view_scroll(wmOperatorType *ot) +{ + ot->name = "View Scroll"; + ot->idname = "UI_OT_view_scroll"; + + ot->invoke = ui_view_scroll_invoke; + ot->poll = ui_view_scroll_poll; + + ot->flag = OPTYPE_INTERNAL; +} + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name UI View Item Rename Operator * @@ -2757,6 +2828,7 @@ void ED_operatortypes_ui() WM_operatortype_append(UI_OT_view_start_filter); WM_operatortype_append(UI_OT_view_drop); + WM_operatortype_append(UI_OT_view_scroll); WM_operatortype_append(UI_OT_view_item_rename); WM_operatortype_append(UI_OT_override_type_set_button); diff --git a/source/blender/editors/interface/templates/interface_template_bone_collection_tree.cc b/source/blender/editors/interface/templates/interface_template_bone_collection_tree.cc index add71c37d7b..b617db70fe7 100644 --- a/source/blender/editors/interface/templates/interface_template_bone_collection_tree.cc +++ b/source/blender/editors/interface/templates/interface_template_bone_collection_tree.cc @@ -474,7 +474,7 @@ void uiTemplateBoneCollectionTree(uiLayout *layout, bContext *C) "Bone Collection Tree View", std::make_unique(*armature)); tree_view->set_context_menu_title("Bone Collection"); - tree_view->set_min_rows(3); + tree_view->set_default_rows(3); ui::TreeViewBuilder::build_tree_view(*tree_view, *layout); } diff --git a/source/blender/editors/interface/templates/interface_template_grease_pencil_layer_tree.cc b/source/blender/editors/interface/templates/interface_template_grease_pencil_layer_tree.cc index 6c6cb519e65..9618bec87a7 100644 --- a/source/blender/editors/interface/templates/interface_template_grease_pencil_layer_tree.cc +++ b/source/blender/editors/interface/templates/interface_template_grease_pencil_layer_tree.cc @@ -473,7 +473,7 @@ void uiTemplateGreasePencilLayerTree(uiLayout *layout, bContext *C) "Grease Pencil Layer Tree View", std::make_unique(grease_pencil)); tree_view->set_context_menu_title("Grease Pencil Layer"); - tree_view->set_min_rows(6); + tree_view->set_default_rows(6); ui::TreeViewBuilder::build_tree_view(*tree_view, *layout); } diff --git a/source/blender/editors/interface/templates/interface_template_light_linking.cc b/source/blender/editors/interface/templates/interface_template_light_linking.cc index 69b4a8dffdc..c88e49d54d3 100644 --- a/source/blender/editors/interface/templates/interface_template_light_linking.cc +++ b/source/blender/editors/interface/templates/interface_template_light_linking.cc @@ -400,7 +400,7 @@ void uiTemplateLightLinkingCollection(uiLayout *layout, "Light Linking Collection Tree View", std::make_unique(*context_layout, *collection)); tree_view->set_context_menu_title("Light Linking"); - tree_view->set_min_rows(3); + tree_view->set_default_rows(3); blender::ui::TreeViewBuilder::build_tree_view(*tree_view, *layout); } diff --git a/source/blender/editors/interface/templates/interface_template_node_tree_interface.cc b/source/blender/editors/interface/templates/interface_template_node_tree_interface.cc index 7fef596989c..474641fb0f5 100644 --- a/source/blender/editors/interface/templates/interface_template_node_tree_interface.cc +++ b/source/blender/editors/interface/templates/interface_template_node_tree_interface.cc @@ -519,7 +519,7 @@ void uiTemplateNodeTreeInterface(uiLayout *layout, PointerRNA *ptr) "Node Tree Declaration Tree View", std::make_unique(nodetree, interface)); tree_view->set_context_menu_title("Node Tree Interface"); - tree_view->set_min_rows(3); + tree_view->set_default_rows(3); blender::ui::TreeViewBuilder::build_tree_view(*tree_view, *layout); } diff --git a/source/blender/editors/interface/views/abstract_view.cc b/source/blender/editors/interface/views/abstract_view.cc index 1c37e4e336e..c75b47e00fb 100644 --- a/source/blender/editors/interface/views/abstract_view.cc +++ b/source/blender/editors/interface/views/abstract_view.cc @@ -121,11 +121,21 @@ bool AbstractView::begin_filtering(const bContext & /*C*/) const return false; } -void AbstractView::draw_overlays(const ARegion & /*region*/) const +void AbstractView::draw_overlays(const ARegion & /*region*/, const uiBlock & /*block*/) const { /* Nothing by default. */ } +bool AbstractView::supports_scrolling() const +{ + return false; +} + +void AbstractView::scroll(ViewScrollDirection /*direction*/) +{ + BLI_assert_msg(false, "Unsupported for this view type"); +} + /** \} */ /* ---------------------------------------------------------------------- */ diff --git a/source/blender/editors/interface/views/interface_view.cc b/source/blender/editors/interface/views/interface_view.cc index 23c06c5aabe..097a2198388 100644 --- a/source/blender/editors/interface/views/interface_view.cc +++ b/source/blender/editors/interface/views/interface_view.cc @@ -145,7 +145,7 @@ void ui_block_views_listen(const uiBlock *block, const wmRegionListenerParams *l void ui_block_views_draw_overlays(const ARegion *region, const uiBlock *block) { LISTBASE_FOREACH (ViewLink *, view_link, &block->views) { - view_link->view->draw_overlays(*region); + view_link->view->draw_overlays(*region, *block); } } diff --git a/source/blender/editors/interface/views/tree_view.cc b/source/blender/editors/interface/views/tree_view.cc index 8091faabfa0..c2690ec1b71 100644 --- a/source/blender/editors/interface/views/tree_view.cc +++ b/source/blender/editors/interface/views/tree_view.cc @@ -18,6 +18,7 @@ #include "interface_intern.hh" #include "UI_interface.hh" +#include "UI_view2d.hh" #include "WM_api.hh" #include "WM_types.hh" @@ -124,75 +125,117 @@ AbstractTreeViewItem *AbstractTreeView::find_hovered(const ARegion ®ion, cons return hovered_item; } -void AbstractTreeView::set_min_rows(int min_rows) +void AbstractTreeView::set_default_rows(int default_rows) { - min_rows_ = min_rows; + custom_height_ = std::make_unique(default_rows * padded_item_height()); } -AbstractTreeViewItem *AbstractTreeView::find_last_visible_descendant( - const AbstractTreeViewItem &parent) const +int AbstractTreeView::count_visible_descendants(const AbstractTreeViewItem &parent) const { if (parent.is_collapsed()) { - return nullptr; + return 0; + } + int count = 0; + for (const auto &item : parent.children_) { + if (!item->is_filtered_visible()) { + continue; + } + count++; + count += count_visible_descendants(*item); } - AbstractTreeViewItem *last_descendant = parent.children_.last().get(); - while (!last_descendant->children_.is_empty() && !last_descendant->is_collapsed()) { - last_descendant = last_descendant->children_.last().get(); - } - - return last_descendant; + return count; } -void AbstractTreeView::draw_hierarchy_lines_recursive(const ARegion ®ion, - const TreeViewOrItem &parent, - const uint pos, - const float aspect) const +void AbstractTreeView::get_hierarchy_lines(const ARegion ®ion, + const TreeViewOrItem &parent, + const float aspect, + Vector> &lines, + int &visible_item_index) const { + const int scroll_ofs = scroll_value_ ? *scroll_value_ : 0; + const int max_visible_row_count = tot_visible_row_count().value_or( + std::numeric_limits::max()); + for (const auto &item : parent.children_) { + if (!item->is_filtered_visible()) { + continue; + } + + const int item_index = visible_item_index; + visible_item_index++; + if (!item->is_collapsible() || item->is_collapsed()) { continue; } - this->draw_hierarchy_lines_recursive(region, *item, pos, aspect); + /* Draw a hierarchy line for the descendants of this item. */ const AbstractTreeViewItem *first_descendant = item->children_.first().get(); - const AbstractTreeViewItem *last_descendant = find_last_visible_descendant(*item); - if (!first_descendant->view_item_but_ || !last_descendant || !last_descendant->view_item_but_) + const int descendant_count = count_visible_descendants(*item); + + const int first_descendant_index = item_index + 1; + const int last_descendant_index = first_descendant_index + descendant_count; + { - return; + const bool line_ends_above_visible = last_descendant_index < scroll_ofs; + if (line_ends_above_visible) { + continue; + } + + const bool line_starts_below_visible = first_descendant_index > + (scroll_ofs + long(max_visible_row_count)); + /* Can return here even, following items won't be in view anymore. */ + if (line_starts_below_visible) { + return; + } } - const uiButViewItem &first_child_but = *first_descendant->view_item_button(); - const uiButViewItem &last_child_but = *last_descendant->view_item_button(); - BLI_assert(first_child_but.block == last_child_but.block); - const uiBlock *block = first_child_but.block; + const int x = ((first_descendant->indent_width() + uiLayoutListItemPaddingWidth() - + (0.5f * UI_ICON_SIZE) + U.pixelsize + UI_SCALE_FAC) / + aspect); + const int ymax = std::max(0, first_descendant_index - scroll_ofs) * padded_item_height(); + const int ymin = std::min(max_visible_row_count, last_descendant_index - scroll_ofs) * + padded_item_height(); + lines.append(std::make_pair(int2(x, ymax), int2(x, ymin))); - rcti first_child_rect; - ui_but_to_pixelrect(&first_child_rect, ®ion, block, &first_child_but); - rcti last_child_rect; - ui_but_to_pixelrect(&last_child_rect, ®ion, block, &last_child_but); - - const float x = first_child_rect.xmin + - ((first_descendant->indent_width() + uiLayoutListItemPaddingWidth() - - (0.5f * UI_ICON_SIZE) + U.pixelsize) / - aspect); - const int first_child_top = first_child_rect.ymax - (2.0f * UI_SCALE_FAC / aspect); - const int last_child_bottom = last_child_rect.ymin + (4.0f * UI_SCALE_FAC / aspect); - immBegin(GPU_PRIM_LINES, 2); - immVertex2f(pos, x, first_child_top); - immVertex2f(pos, x, last_child_bottom); - immEnd(); + this->get_hierarchy_lines(region, *item, aspect, lines, visible_item_index); } } -void AbstractTreeView::draw_hierarchy_lines(const ARegion ®ion) const +static uiButViewItem *find_first_view_item_but(const uiBlock &block, const AbstractTreeView &view) +{ + LISTBASE_FOREACH (uiBut *, but, &block.buttons) { + if (but->type != UI_BTYPE_VIEW_ITEM) { + continue; + } + uiButViewItem *view_item_but = static_cast(but); + if (&view_item_but->view_item->get_view() == &view) { + return view_item_but; + } + } + return nullptr; +} + +void AbstractTreeView::draw_hierarchy_lines(const ARegion ®ion, const uiBlock &block) const { const float aspect = (region.v2d.flag & V2D_IS_INIT) ? BLI_rctf_size_y(®ion.v2d.cur) / (BLI_rcti_size_y(®ion.v2d.mask) + 1) : 1.0f; + uiButViewItem *first_item_but = find_first_view_item_but(block, *this); + if (!first_item_but) { + return; + } + + Vector> lines; + int index = 0; + get_hierarchy_lines(region, *this, aspect, lines, index); + if (lines.is_empty()) { + return; + } + GPUVertFormat *format = immVertexFormat(); uint pos = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR); @@ -200,21 +243,33 @@ void AbstractTreeView::draw_hierarchy_lines(const ARegion ®ion) const GPU_line_width(1.0f / aspect); GPU_blend(GPU_BLEND_ALPHA); - draw_hierarchy_lines_recursive(region, *this, pos, aspect); + + rcti first_item_but_pixel_rect; + ui_but_to_pixelrect(&first_item_but_pixel_rect, ®ion, &block, first_item_but); + int2 top_left{first_item_but_pixel_rect.xmin, first_item_but_pixel_rect.ymax}; + + for (const auto &line : lines) { + immBegin(GPU_PRIM_LINES, 2); + immVertex2f(pos, top_left.x + line.first.x, top_left.y - line.first.y); + immVertex2f(pos, top_left.x + line.second.x, top_left.y - line.second.y); + immEnd(); + } GPU_blend(GPU_BLEND_NONE); immUnbindProgram(); } -void AbstractTreeView::draw_overlays(const ARegion ®ion) const +void AbstractTreeView::draw_overlays(const ARegion ®ion, const uiBlock &block) const { - this->draw_hierarchy_lines(region); + this->draw_hierarchy_lines(region, block); } void AbstractTreeView::update_children_from_old(const AbstractView &old_view) { const AbstractTreeView &old_tree_view = dynamic_cast(old_view); + custom_height_ = old_tree_view.custom_height_; + scroll_value_ = old_tree_view.scroll_value_; this->update_children_from_old_recursive(*this, old_tree_view); } @@ -257,6 +312,31 @@ AbstractTreeViewItem *AbstractTreeView::find_matching_child( return nullptr; } +std::optional AbstractTreeView::tot_visible_row_count() const +{ + if (!custom_height_) { + return {}; + } + if (*custom_height_ < UI_UNIT_Y) { + return 1; + } + return round_fl_to_int(float(*custom_height_) / padded_item_height()); +} + +bool AbstractTreeView::supports_scrolling() const +{ + return custom_height_ && scroll_value_; +} + +void AbstractTreeView::scroll(ViewScrollDirection direction) +{ + if (!supports_scrolling()) { + return; + } + /* Scroll value will be sanitized/clamped when drawing. */ + *scroll_value_ += ((direction == ViewScrollDirection::UP) ? -1 : 1); +} + /* ---------------------------------------------------------------------- */ TreeViewItemDropTarget::TreeViewItemDropTarget(AbstractTreeViewItem &view_item, @@ -645,7 +725,7 @@ class TreeViewLayoutBuilder { friend TreeViewBuilder; public: - void build_from_tree(const AbstractTreeView &tree_view); + void build_from_tree(AbstractTreeView &tree_view); void build_row(AbstractTreeViewItem &item) const; uiBlock &block() const; @@ -660,23 +740,97 @@ TreeViewLayoutBuilder::TreeViewLayoutBuilder(uiLayout &layout) : block_(*uiLayou { } -void TreeViewLayoutBuilder::build_from_tree(const AbstractTreeView &tree_view) +static int count_visible_items(AbstractTreeView &tree_view) { - uiLayout &parent_layout = this->current_layout(); - - if (add_box_) { - uiLayout *box = uiLayoutBox(&parent_layout); - uiLayoutColumn(box, true); - } - else { - uiLayoutColumn(&parent_layout, true); - } - - tree_view.foreach_item([this](AbstractTreeViewItem &item) { build_row(item); }, + int item_count = 0; + tree_view.foreach_item([&](AbstractTreeViewItem &) { item_count++; }, AbstractTreeView::IterOptions::SkipCollapsed | AbstractTreeView::IterOptions::SkipFiltered); + return item_count; +} - UI_block_layout_set_current(&block(), &parent_layout); +void TreeViewLayoutBuilder::build_from_tree(AbstractTreeView &tree_view) +{ + uiLayout &parent_layout = this->current_layout(); + uiBlock *block = uiLayoutGetBlock(&parent_layout); + + uiLayout *col = nullptr; + if (add_box_) { + uiLayout *box = uiLayoutBox(&parent_layout); + col = uiLayoutColumn(box, true); + } + else { + col = uiLayoutColumn(&parent_layout, true); + } + /* Row for the tree-view and the scroll bar. */ + uiLayout *row = uiLayoutRow(col, false); + + const std::optional visible_row_count = tree_view.tot_visible_row_count(); + const int tot_items = count_visible_items(tree_view); + + /* Column for the tree view. */ + uiLayoutColumn(row, true); + + /* Clamp scroll-value to valid range. */ + if (tree_view.scroll_value_ && visible_row_count) { + *tree_view.scroll_value_ = std::clamp( + *tree_view.scroll_value_, 0, tot_items - *visible_row_count); + } + + const int first_visible_index = tree_view.scroll_value_ ? *tree_view.scroll_value_ : 0; + const int max_visible_index = visible_row_count ? first_visible_index + *visible_row_count - 1 : + std::numeric_limits::max(); + int index = 0; + tree_view.foreach_item( + [&, this](AbstractTreeViewItem &item) { + if ((index >= first_visible_index) && (index <= max_visible_index)) { + this->build_row(item); + } + index++; + }, + AbstractTreeView::IterOptions::SkipCollapsed | AbstractTreeView::IterOptions::SkipFiltered); + + if (tree_view.custom_height_) { + uiLayoutColumn(row, false); + + *tree_view.custom_height_ = visible_row_count.value_or(1) * padded_item_height(); + if (!tree_view.scroll_value_) { + tree_view.scroll_value_ = std::make_unique(0); + } + + if (visible_row_count && (tot_items > *visible_row_count)) { + uiBut *but = uiDefButI(block, + UI_BTYPE_SCROLL, + 0, + "", + 0, + 0, + V2D_SCROLL_WIDTH, + *tree_view.custom_height_, + tree_view.scroll_value_.get(), + 0, + tot_items - *visible_row_count, + ""); + uiButScrollBar *but_scroll = reinterpret_cast(but); + but_scroll->visual_height = *visible_row_count; + } + + UI_block_layout_set_current(block, col); + uiDefIconButI(block, + UI_BTYPE_GRIP, + 0, + ICON_GRIP, + 0, + 0, + UI_UNIT_X * 10, + UI_UNIT_Y * 0.5f, + tree_view.custom_height_.get(), + 0, + 0, + ""); + } + + UI_block_layout_set_current(block, &parent_layout); } void TreeViewLayoutBuilder::build_row(AbstractTreeViewItem &item) const @@ -740,16 +894,21 @@ uiLayout &TreeViewLayoutBuilder::current_layout() const void TreeViewBuilder::ensure_min_rows_items(AbstractTreeView &tree_view) { + const std::optional visible_rows = tree_view.tot_visible_row_count(); + if (!visible_rows) { + return; + } + int tot_visible_items = 0; tree_view.foreach_item( [&tot_visible_items](AbstractTreeViewItem & /*item*/) { tot_visible_items++; }, AbstractTreeView::IterOptions::SkipCollapsed | AbstractTreeView::IterOptions::SkipFiltered); - if (tot_visible_items >= tree_view.min_rows_) { + if (tot_visible_items >= *visible_rows) { return; } - for (int i = 0; i < (tree_view.min_rows_ - tot_visible_items); i++) { + for (int i = 0; i < (*visible_rows - tot_visible_items); i++) { BasicTreeViewItem &new_item = tree_view.add_tree_item(""); new_item.disable_interaction(); }