Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulian Eisel <julian@blender.org>2022-07-19 17:42:17 +0300
committerJulian Eisel <julian@blender.org>2022-07-19 19:04:03 +0300
commitfb9dc810f17c463496381bbc9fba3b96c6437ce0 (patch)
treec41f5ebb5be691e9267286a8fb78617e4725fd21 /source/blender/editors/interface/views
parent801513efa068d2d181534a599d21e425331b275d (diff)
UI Code Quality: Move view related files to own folder
Part of T98518.
Diffstat (limited to 'source/blender/editors/interface/views')
-rw-r--r--source/blender/editors/interface/views/abstract_view.cc109
-rw-r--r--source/blender/editors/interface/views/abstract_view_item.cc373
-rw-r--r--source/blender/editors/interface/views/grid_view.cc464
-rw-r--r--source/blender/editors/interface/views/interface_view.cc190
-rw-r--r--source/blender/editors/interface/views/tree_view.cc554
5 files changed, 1690 insertions, 0 deletions
diff --git a/source/blender/editors/interface/views/abstract_view.cc b/source/blender/editors/interface/views/abstract_view.cc
new file mode 100644
index 00000000000..077c76a08f1
--- /dev/null
+++ b/source/blender/editors/interface/views/abstract_view.cc
@@ -0,0 +1,109 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup edinterface
+ */
+
+#include "interface_intern.h"
+
+#include "UI_abstract_view.hh"
+
+namespace blender::ui {
+
+void AbstractView::register_item(AbstractViewItem &item)
+{
+ /* Actually modifies the item, not the view. But for the public API it "feels" a bit nicer to
+ * have the view base class register the items, rather than setting the view on the item. */
+ item.view_ = this;
+}
+
+/* ---------------------------------------------------------------------- */
+/** \name View Reconstruction
+ * \{ */
+
+bool AbstractView::is_reconstructed() const
+{
+ return is_reconstructed_;
+}
+
+void AbstractView::update_from_old(uiBlock &new_block)
+{
+ uiBlock *old_block = new_block.oldblock;
+ if (!old_block) {
+ is_reconstructed_ = true;
+ return;
+ }
+
+ uiViewHandle *old_view_handle = ui_block_view_find_matching_in_old_block(
+ &new_block, reinterpret_cast<uiViewHandle *>(this));
+ if (old_view_handle == nullptr) {
+ /* Initial construction, nothing to update. */
+ is_reconstructed_ = true;
+ return;
+ }
+
+ AbstractView &old_view = reinterpret_cast<AbstractView &>(*old_view_handle);
+
+ /* Update own persistent data. */
+ /* Keep the rename buffer persistent while renaming! The rename button uses the buffer's
+ * pointer to identify itself over redraws. */
+ rename_buffer_ = std::move(old_view.rename_buffer_);
+ old_view.rename_buffer_ = nullptr;
+
+ update_children_from_old(old_view);
+
+ /* Finished (re-)constructing the tree. */
+ is_reconstructed_ = true;
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+/** \name Default implementations of virtual functions
+ * \{ */
+
+bool AbstractView::listen(const wmNotifier & /*notifier*/) const
+{
+ /* Nothing by default. */
+ return false;
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+/** \name Renaming
+ * \{ */
+
+bool AbstractView::is_renaming() const
+{
+ return rename_buffer_ != nullptr;
+}
+
+bool AbstractView::begin_renaming()
+{
+ if (is_renaming()) {
+ return false;
+ }
+
+ rename_buffer_ = std::make_unique<decltype(rename_buffer_)::element_type>();
+ return true;
+}
+
+void AbstractView::end_renaming()
+{
+ BLI_assert(is_renaming());
+ rename_buffer_ = nullptr;
+}
+
+Span<char> AbstractView::get_rename_buffer() const
+{
+ return *rename_buffer_;
+}
+MutableSpan<char> AbstractView::get_rename_buffer()
+{
+ return *rename_buffer_;
+}
+
+/** \} */
+
+} // namespace blender::ui
diff --git a/source/blender/editors/interface/views/abstract_view_item.cc b/source/blender/editors/interface/views/abstract_view_item.cc
new file mode 100644
index 00000000000..f73183d07e9
--- /dev/null
+++ b/source/blender/editors/interface/views/abstract_view_item.cc
@@ -0,0 +1,373 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup edinterface
+ */
+
+#include "BKE_context.h"
+
+#include "BLI_listbase.h"
+#include "BLI_string.h"
+
+#include "WM_api.h"
+
+#include "UI_interface.h"
+#include "interface_intern.h"
+
+#include "UI_abstract_view.hh"
+
+namespace blender::ui {
+
+/* ---------------------------------------------------------------------- */
+/** \name View Reconstruction
+ * \{ */
+
+void AbstractViewItem::update_from_old(const AbstractViewItem &old)
+{
+ is_active_ = old.is_active_;
+ is_renaming_ = old.is_renaming_;
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+/** \name Renaming
+ * \{ */
+
+bool AbstractViewItem::supports_renaming() const
+{
+ /* No renaming by default. */
+ return false;
+}
+bool AbstractViewItem::rename(StringRefNull /*new_name*/)
+{
+ /* No renaming by default. */
+ return false;
+}
+
+StringRef AbstractViewItem::get_rename_string() const
+{
+ /* No rename string by default. */
+ return {};
+}
+
+bool AbstractViewItem::is_renaming() const
+{
+ return is_renaming_;
+}
+
+void AbstractViewItem::begin_renaming()
+{
+ AbstractView &view = get_view();
+ if (view.is_renaming() || !supports_renaming()) {
+ return;
+ }
+
+ if (view.begin_renaming()) {
+ is_renaming_ = true;
+ }
+
+ StringRef initial_str = get_rename_string();
+ std::copy(std::begin(initial_str), std::end(initial_str), std::begin(view.get_rename_buffer()));
+}
+
+void AbstractViewItem::rename_apply()
+{
+ const AbstractView &view = get_view();
+ rename(view.get_rename_buffer().data());
+ end_renaming();
+}
+
+void AbstractViewItem::end_renaming()
+{
+ if (!is_renaming()) {
+ return;
+ }
+
+ is_renaming_ = false;
+
+ AbstractView &view = get_view();
+ view.end_renaming();
+}
+
+static AbstractViewItem *find_item_from_rename_button(const uiBut &rename_but)
+{
+ /* A minimal sanity check, can't do much more here. */
+ BLI_assert(rename_but.type == UI_BTYPE_TEXT && rename_but.poin);
+
+ LISTBASE_FOREACH (uiBut *, but, &rename_but.block->buttons) {
+ if (but->type != UI_BTYPE_VIEW_ITEM) {
+ continue;
+ }
+
+ uiButViewItem *view_item_but = (uiButViewItem *)but;
+ AbstractViewItem *item = reinterpret_cast<AbstractViewItem *>(view_item_but->view_item);
+ const AbstractView &view = item->get_view();
+
+ if (item->is_renaming() && (view.get_rename_buffer().data() == rename_but.poin)) {
+ return item;
+ }
+ }
+
+ return nullptr;
+}
+
+static void rename_button_fn(bContext *UNUSED(C), void *arg, char *UNUSED(origstr))
+{
+ const uiBut *rename_but = static_cast<uiBut *>(arg);
+ AbstractViewItem *item = find_item_from_rename_button(*rename_but);
+ BLI_assert(item);
+ item->rename_apply();
+}
+
+void AbstractViewItem::add_rename_button(uiBlock &block)
+{
+ AbstractView &view = get_view();
+ uiBut *rename_but = uiDefBut(&block,
+ UI_BTYPE_TEXT,
+ 1,
+ "",
+ 0,
+ 0,
+ UI_UNIT_X * 10,
+ UI_UNIT_Y,
+ view.get_rename_buffer().data(),
+ 1.0f,
+ view.get_rename_buffer().size(),
+ 0,
+ 0,
+ "");
+
+ /* Gotta be careful with what's passed to the `arg1` here. Any view data will be freed once the
+ * callback is executed. */
+ UI_but_func_rename_set(rename_but, rename_button_fn, rename_but);
+ UI_but_flag_disable(rename_but, UI_BUT_UNDO);
+
+ const bContext *evil_C = reinterpret_cast<bContext *>(block.evil_C);
+ ARegion *region = CTX_wm_region(evil_C);
+ /* Returns false if the button was removed. */
+ if (UI_but_active_only(evil_C, region, &block, rename_but) == false) {
+ end_renaming();
+ }
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+/** \name Context Menu
+ * \{ */
+
+void AbstractViewItem::build_context_menu(bContext & /*C*/, uiLayout & /*column*/) const
+{
+ /* No context menu by default. */
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+/** \name Drag 'n Drop
+ * \{ */
+
+std::unique_ptr<AbstractViewItemDragController> AbstractViewItem::create_drag_controller() const
+{
+ /* There's no drag controller (and hence no drag support) by default. */
+ return nullptr;
+}
+
+std::unique_ptr<AbstractViewItemDropController> AbstractViewItem::create_drop_controller() const
+{
+ /* There's no drop controller (and hence no drop support) by default. */
+ return nullptr;
+}
+
+AbstractViewItemDragController::AbstractViewItemDragController(AbstractView &view) : view_(view)
+{
+}
+
+void AbstractViewItemDragController::on_drag_start()
+{
+ /* Do nothing by default. */
+}
+
+AbstractViewItemDropController::AbstractViewItemDropController(AbstractView &view) : view_(view)
+{
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+/** \name General Getters & Setters
+ * \{ */
+
+AbstractView &AbstractViewItem::get_view() const
+{
+ if (UNLIKELY(!view_)) {
+ throw std::runtime_error(
+ "Invalid state, item must be registered through AbstractView::register_item()");
+ }
+ return *view_;
+}
+
+bool AbstractViewItem::is_active() const
+{
+ BLI_assert_msg(get_view().is_reconstructed(),
+ "State can't be queried until reconstruction is completed");
+ return is_active_;
+}
+
+/** \} */
+
+} // namespace blender::ui
+
+/* ---------------------------------------------------------------------- */
+/** \name C-API
+ * \{ */
+
+namespace blender::ui {
+
+/**
+ * Helper class to provide a higher level public (C-)API. Has access to private/protected view item
+ * members and ensures some invariants that way.
+ */
+class ViewItemAPIWrapper {
+ public:
+ static bool matches(const AbstractViewItem &a, const AbstractViewItem &b)
+ {
+ if (typeid(a) != typeid(b)) {
+ return false;
+ }
+ /* TODO should match the view as well. */
+ return a.matches(b);
+ }
+
+ static bool can_rename(const AbstractViewItem &item)
+ {
+ const AbstractView &view = item.get_view();
+ return !view.is_renaming() && item.supports_renaming();
+ }
+
+ static bool drag_start(bContext &C, const AbstractViewItem &item)
+ {
+ const std::unique_ptr<AbstractViewItemDragController> drag_controller =
+ item.create_drag_controller();
+ if (!drag_controller) {
+ return false;
+ }
+
+ WM_event_start_drag(&C,
+ ICON_NONE,
+ drag_controller->get_drag_type(),
+ drag_controller->create_drag_data(),
+ 0,
+ WM_DRAG_FREE_DATA);
+ drag_controller->on_drag_start();
+
+ return true;
+ }
+
+ static bool can_drop(const AbstractViewItem &item,
+ const wmDrag &drag,
+ const char **r_disabled_hint)
+ {
+ const std::unique_ptr<AbstractViewItemDropController> drop_controller =
+ item.create_drop_controller();
+ if (!drop_controller) {
+ return false;
+ }
+
+ return drop_controller->can_drop(drag, r_disabled_hint);
+ }
+
+ static std::string drop_tooltip(const AbstractViewItem &item, const wmDrag &drag)
+ {
+ const std::unique_ptr<AbstractViewItemDropController> drop_controller =
+ item.create_drop_controller();
+ if (!drop_controller) {
+ return {};
+ }
+
+ return drop_controller->drop_tooltip(drag);
+ }
+
+ static bool drop_handle(bContext &C, const AbstractViewItem &item, const ListBase &drags)
+ {
+ std::unique_ptr<AbstractViewItemDropController> drop_controller =
+ item.create_drop_controller();
+
+ const char *disabled_hint_dummy = nullptr;
+ LISTBASE_FOREACH (const wmDrag *, drag, &drags) {
+ if (drop_controller->can_drop(*drag, &disabled_hint_dummy)) {
+ return drop_controller->on_drop(&C, *drag);
+ }
+ }
+
+ return false;
+ }
+};
+
+} // namespace blender::ui
+
+using namespace blender::ui;
+
+bool UI_view_item_is_active(const uiViewItemHandle *item_handle)
+{
+ const AbstractViewItem &item = reinterpret_cast<const AbstractViewItem &>(*item_handle);
+ return item.is_active();
+}
+
+bool UI_view_item_matches(const uiViewItemHandle *a_handle, const uiViewItemHandle *b_handle)
+{
+ const AbstractViewItem &a = reinterpret_cast<const AbstractViewItem &>(*a_handle);
+ const AbstractViewItem &b = reinterpret_cast<const AbstractViewItem &>(*b_handle);
+ return ViewItemAPIWrapper::matches(a, b);
+}
+
+bool UI_view_item_can_rename(const uiViewItemHandle *item_handle)
+{
+ const AbstractViewItem &item = reinterpret_cast<const AbstractViewItem &>(*item_handle);
+ return ViewItemAPIWrapper::can_rename(item);
+}
+
+void UI_view_item_begin_rename(uiViewItemHandle *item_handle)
+{
+ AbstractViewItem &item = reinterpret_cast<AbstractViewItem &>(*item_handle);
+ item.begin_renaming();
+}
+
+void UI_view_item_context_menu_build(bContext *C,
+ const uiViewItemHandle *item_handle,
+ uiLayout *column)
+{
+ const AbstractViewItem &item = reinterpret_cast<const AbstractViewItem &>(*item_handle);
+ item.build_context_menu(*C, *column);
+}
+
+bool UI_view_item_drag_start(bContext *C, const uiViewItemHandle *item_)
+{
+ const AbstractViewItem &item = reinterpret_cast<const AbstractViewItem &>(*item_);
+ return ViewItemAPIWrapper::drag_start(*C, item);
+}
+
+bool UI_view_item_can_drop(const uiViewItemHandle *item_,
+ const wmDrag *drag,
+ const char **r_disabled_hint)
+{
+ const AbstractViewItem &item = reinterpret_cast<const AbstractViewItem &>(*item_);
+ return ViewItemAPIWrapper::can_drop(item, *drag, r_disabled_hint);
+}
+
+char *UI_view_item_drop_tooltip(const uiViewItemHandle *item_, const wmDrag *drag)
+{
+ const AbstractViewItem &item = reinterpret_cast<const AbstractViewItem &>(*item_);
+
+ const std::string tooltip = ViewItemAPIWrapper::drop_tooltip(item, *drag);
+ return tooltip.empty() ? nullptr : BLI_strdup(tooltip.c_str());
+}
+
+bool UI_view_item_drop_handle(bContext *C, const uiViewItemHandle *item_, const ListBase *drags)
+{
+ const AbstractViewItem &item = reinterpret_cast<const AbstractViewItem &>(*item_);
+ return ViewItemAPIWrapper::drop_handle(*C, item, *drags);
+}
+
+/** \} */
diff --git a/source/blender/editors/interface/views/grid_view.cc b/source/blender/editors/interface/views/grid_view.cc
new file mode 100644
index 00000000000..37fbb33f83b
--- /dev/null
+++ b/source/blender/editors/interface/views/grid_view.cc
@@ -0,0 +1,464 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup edinterface
+ */
+
+#include <limits>
+#include <stdexcept>
+
+#include "BLI_index_range.hh"
+
+#include "WM_types.h"
+
+#include "UI_interface.h"
+#include "interface_intern.h"
+
+#include "UI_grid_view.hh"
+
+namespace blender::ui {
+
+/* ---------------------------------------------------------------------- */
+
+AbstractGridView::AbstractGridView() : style_(UI_preview_tile_size_x(), UI_preview_tile_size_y())
+{
+}
+
+AbstractGridViewItem &AbstractGridView::add_item(std::unique_ptr<AbstractGridViewItem> item)
+{
+ items_.append(std::move(item));
+
+ AbstractGridViewItem &added_item = *items_.last();
+ added_item.view_ = this;
+
+ item_map_.add(added_item.identifier_, &added_item);
+
+ return added_item;
+}
+
+void AbstractGridView::foreach_item(ItemIterFn iter_fn) const
+{
+ for (const auto &item_ptr : items_) {
+ iter_fn(*item_ptr);
+ }
+}
+
+AbstractGridViewItem *AbstractGridView::find_matching_item(
+ const AbstractGridViewItem &item_to_match, const AbstractGridView &view_to_search_in) const
+{
+ AbstractGridViewItem *const *match = view_to_search_in.item_map_.lookup_ptr(
+ item_to_match.identifier_);
+ BLI_assert(!match || item_to_match.matches(**match));
+
+ return match ? *match : nullptr;
+}
+
+void AbstractGridView::change_state_delayed()
+{
+ BLI_assert_msg(
+ is_reconstructed(),
+ "These state changes are supposed to be delayed until reconstruction is completed");
+ foreach_item([](AbstractGridViewItem &item) { item.change_state_delayed(); });
+}
+
+void AbstractGridView::update_children_from_old(const AbstractView &old_view)
+{
+ const AbstractGridView &old_grid_view = dynamic_cast<const AbstractGridView &>(old_view);
+
+ foreach_item([this, &old_grid_view](AbstractGridViewItem &new_item) {
+ const AbstractGridViewItem *matching_old_item = find_matching_item(new_item, old_grid_view);
+ if (!matching_old_item) {
+ return;
+ }
+
+ new_item.update_from_old(*matching_old_item);
+ });
+}
+
+const GridViewStyle &AbstractGridView::get_style() const
+{
+ return style_;
+}
+
+int AbstractGridView::get_item_count() const
+{
+ return items_.size();
+}
+
+GridViewStyle::GridViewStyle(int width, int height) : tile_width(width), tile_height(height)
+{
+}
+
+/* ---------------------------------------------------------------------- */
+
+AbstractGridViewItem::AbstractGridViewItem(StringRef identifier) : identifier_(identifier)
+{
+}
+
+bool AbstractGridViewItem::matches(const AbstractViewItem &other) const
+{
+ const AbstractGridViewItem &other_grid_item = dynamic_cast<const AbstractGridViewItem &>(other);
+ return identifier_ == other_grid_item.identifier_;
+}
+
+void AbstractGridViewItem::grid_tile_click_fn(struct bContext * /*C*/,
+ void *but_arg1,
+ void * /*arg2*/)
+{
+ uiButViewItem *view_item_but = (uiButViewItem *)but_arg1;
+ AbstractGridViewItem &grid_item = reinterpret_cast<AbstractGridViewItem &>(
+ *view_item_but->view_item);
+
+ grid_item.activate();
+}
+
+void AbstractGridViewItem::add_grid_tile_button(uiBlock &block)
+{
+ const GridViewStyle &style = get_view().get_style();
+ view_item_but_ = (uiButViewItem *)uiDefBut(&block,
+ UI_BTYPE_VIEW_ITEM,
+ 0,
+ "",
+ 0,
+ 0,
+ style.tile_width,
+ style.tile_height,
+ nullptr,
+ 0,
+ 0,
+ 0,
+ 0,
+ "");
+
+ view_item_but_->view_item = reinterpret_cast<uiViewItemHandle *>(this);
+ UI_but_func_set(&view_item_but_->but, grid_tile_click_fn, view_item_but_, nullptr);
+}
+
+void AbstractGridViewItem::on_activate()
+{
+ /* Do nothing by default. */
+}
+
+std::optional<bool> AbstractGridViewItem::should_be_active() const
+{
+ return std::nullopt;
+}
+
+void AbstractGridViewItem::change_state_delayed()
+{
+ const std::optional<bool> should_be_active = this->should_be_active();
+ if (should_be_active.has_value() && *should_be_active) {
+ activate();
+ }
+}
+
+void AbstractGridViewItem::activate()
+{
+ BLI_assert_msg(get_view().is_reconstructed(),
+ "Item activation can't be done until reconstruction is completed");
+
+ if (is_active()) {
+ return;
+ }
+
+ /* Deactivate other items in the tree. */
+ get_view().foreach_item([](auto &item) { item.deactivate(); });
+
+ on_activate();
+
+ is_active_ = true;
+}
+
+void AbstractGridViewItem::deactivate()
+{
+ is_active_ = false;
+}
+
+const AbstractGridView &AbstractGridViewItem::get_view() const
+{
+ if (UNLIKELY(!view_)) {
+ throw std::runtime_error(
+ "Invalid state, item must be added through AbstractGridView::add_item()");
+ }
+ return *view_;
+}
+
+/* ---------------------------------------------------------------------- */
+
+/**
+ * Helper for only adding layout items for grid items that are actually in view. 3 main functions:
+ * - #is_item_visible(): Query if an item of a given index is visible in the view (others should be
+ * skipped when building the layout).
+ * - #fill_layout_before_visible(): Add empty space to the layout before a visible row is drawn, so
+ * the layout height is the same as if all items were added (important to get the correct scroll
+ * height).
+ * - #fill_layout_after_visible(): Same thing, just adds empty space for after the last visible
+ * row.
+ *
+ * Does two assumptions:
+ * - Top-to-bottom flow (ymax = 0 and ymin < 0). If that's not good enough, View2D should
+ * probably provide queries for the scroll offset.
+ * - Only vertical scrolling. For horizontal scrolling, spacers would have to be added on the
+ * side(s) as well.
+ */
+class BuildOnlyVisibleButtonsHelper {
+ const View2D &v2d_;
+ const AbstractGridView &grid_view_;
+ const GridViewStyle &style_;
+ const int cols_per_row_ = 0;
+ /* Indices of items within the view. Calculated by constructor */
+ IndexRange visible_items_range_{};
+
+ public:
+ BuildOnlyVisibleButtonsHelper(const View2D &v2d,
+ const AbstractGridView &grid_view,
+ int cols_per_row);
+
+ bool is_item_visible(int item_idx) const;
+ void fill_layout_before_visible(uiBlock &block) const;
+ void fill_layout_after_visible(uiBlock &block) const;
+
+ private:
+ IndexRange get_visible_range() const;
+ void add_spacer_button(uiBlock &block, int row_count) const;
+};
+
+BuildOnlyVisibleButtonsHelper::BuildOnlyVisibleButtonsHelper(const View2D &v2d,
+ const AbstractGridView &grid_view,
+ const int cols_per_row)
+ : v2d_(v2d), grid_view_(grid_view), style_(grid_view.get_style()), cols_per_row_(cols_per_row)
+{
+ visible_items_range_ = get_visible_range();
+}
+
+IndexRange BuildOnlyVisibleButtonsHelper::get_visible_range() const
+{
+ int first_idx_in_view = 0;
+ int max_items_in_view = 0;
+
+ const float scroll_ofs_y = abs(v2d_.cur.ymax - v2d_.tot.ymax);
+ if (!IS_EQF(scroll_ofs_y, 0)) {
+ const int scrolled_away_rows = (int)scroll_ofs_y / style_.tile_height;
+
+ first_idx_in_view = scrolled_away_rows * cols_per_row_;
+ }
+
+ const float view_height = BLI_rctf_size_y(&v2d_.cur);
+ const int count_rows_in_view = std::max(round_fl_to_int(view_height / style_.tile_height), 1);
+ max_items_in_view = (count_rows_in_view + 1) * cols_per_row_;
+
+ BLI_assert(max_items_in_view > 0);
+ return IndexRange(first_idx_in_view, max_items_in_view);
+}
+
+bool BuildOnlyVisibleButtonsHelper::is_item_visible(const int item_idx) const
+{
+ return visible_items_range_.contains(item_idx);
+}
+
+void BuildOnlyVisibleButtonsHelper::fill_layout_before_visible(uiBlock &block) const
+{
+ const float scroll_ofs_y = abs(v2d_.cur.ymax - v2d_.tot.ymax);
+
+ if (IS_EQF(scroll_ofs_y, 0)) {
+ return;
+ }
+
+ const int scrolled_away_rows = (int)scroll_ofs_y / style_.tile_height;
+ add_spacer_button(block, scrolled_away_rows);
+}
+
+void BuildOnlyVisibleButtonsHelper::fill_layout_after_visible(uiBlock &block) const
+{
+ const int last_item_idx = grid_view_.get_item_count() - 1;
+ const int last_visible_idx = visible_items_range_.last();
+
+ if (last_item_idx > last_visible_idx) {
+ const int remaining_rows = (cols_per_row_ > 0) ?
+ (last_item_idx - last_visible_idx) / cols_per_row_ :
+ 0;
+ BuildOnlyVisibleButtonsHelper::add_spacer_button(block, remaining_rows);
+ }
+}
+
+void BuildOnlyVisibleButtonsHelper::add_spacer_button(uiBlock &block, const int row_count) const
+{
+ /* UI code only supports button dimensions of `signed short` size, the layout height we want to
+ * fill may be bigger than that. So add multiple labels of the maximum size if necessary. */
+ for (int remaining_rows = row_count; remaining_rows > 0;) {
+ const short row_count_this_iter = std::min(
+ std::numeric_limits<short>::max() / style_.tile_height, remaining_rows);
+
+ uiDefBut(&block,
+ UI_BTYPE_LABEL,
+ 0,
+ "",
+ 0,
+ 0,
+ UI_UNIT_X,
+ row_count_this_iter * style_.tile_height,
+ nullptr,
+ 0,
+ 0,
+ 0,
+ 0,
+ "");
+ remaining_rows -= row_count_this_iter;
+ }
+}
+
+/* ---------------------------------------------------------------------- */
+
+class GridViewLayoutBuilder {
+ uiBlock &block_;
+
+ friend class GridViewBuilder;
+
+ public:
+ GridViewLayoutBuilder(uiBlock &block);
+
+ void build_from_view(const AbstractGridView &grid_view, const View2D &v2d) const;
+
+ private:
+ void build_grid_tile(uiLayout &grid_layout, AbstractGridViewItem &item) const;
+
+ uiLayout *current_layout() const;
+};
+
+GridViewLayoutBuilder::GridViewLayoutBuilder(uiBlock &block) : block_(block)
+{
+}
+
+void GridViewLayoutBuilder::build_grid_tile(uiLayout &grid_layout,
+ AbstractGridViewItem &item) const
+{
+ uiLayout *overlap = uiLayoutOverlap(&grid_layout);
+
+ item.add_grid_tile_button(block_);
+ item.build_grid_tile(*uiLayoutRow(overlap, false));
+}
+
+void GridViewLayoutBuilder::build_from_view(const AbstractGridView &grid_view,
+ const View2D &v2d) const
+{
+ uiLayout *prev_layout = current_layout();
+
+ uiLayout &layout = *uiLayoutColumn(current_layout(), false);
+ const GridViewStyle &style = grid_view.get_style();
+
+ const int cols_per_row = std::max(uiLayoutGetWidth(&layout) / style.tile_width, 1);
+
+ BuildOnlyVisibleButtonsHelper build_visible_helper(v2d, grid_view, cols_per_row);
+
+ build_visible_helper.fill_layout_before_visible(block_);
+
+ /* Use `-cols_per_row` because the grid layout uses a multiple of the passed absolute value for
+ * the number of columns then, rather than distributing the number of items evenly over rows and
+ * stretching the items to fit (see #uiLayoutItemGridFlow.columns_len). */
+ uiLayout *grid_layout = uiLayoutGridFlow(&layout, true, -cols_per_row, true, true, true);
+
+ int item_idx = 0;
+ grid_view.foreach_item([&](AbstractGridViewItem &item) {
+ /* Skip if item isn't visible. */
+ if (!build_visible_helper.is_item_visible(item_idx)) {
+ item_idx++;
+ return;
+ }
+
+ build_grid_tile(*grid_layout, item);
+ item_idx++;
+ });
+
+ /* If there are not enough items to fill the layout, add padding items so the layout doesn't
+ * stretch over the entire width. */
+ if (grid_view.get_item_count() < cols_per_row) {
+ for (int padding_item_idx = 0; padding_item_idx < (cols_per_row - grid_view.get_item_count());
+ padding_item_idx++) {
+ uiItemS(grid_layout);
+ }
+ }
+
+ UI_block_layout_set_current(&block_, prev_layout);
+
+ build_visible_helper.fill_layout_after_visible(block_);
+}
+
+uiLayout *GridViewLayoutBuilder::current_layout() const
+{
+ return block_.curlayout;
+}
+
+/* ---------------------------------------------------------------------- */
+
+GridViewBuilder::GridViewBuilder(uiBlock &block) : block_(block)
+{
+}
+
+void GridViewBuilder::build_grid_view(AbstractGridView &grid_view, const View2D &v2d)
+{
+ grid_view.build_items();
+ grid_view.update_from_old(block_);
+ grid_view.change_state_delayed();
+
+ GridViewLayoutBuilder builder(block_);
+ builder.build_from_view(grid_view, v2d);
+}
+
+/* ---------------------------------------------------------------------- */
+
+PreviewGridItem::PreviewGridItem(StringRef identifier, StringRef label, int preview_icon_id)
+ : AbstractGridViewItem(identifier), label(label), preview_icon_id(preview_icon_id)
+{
+}
+
+void PreviewGridItem::build_grid_tile(uiLayout &layout) const
+{
+ const GridViewStyle &style = get_view().get_style();
+ uiBlock *block = uiLayoutGetBlock(&layout);
+
+ uiBut *but = uiDefBut(block,
+ UI_BTYPE_PREVIEW_TILE,
+ 0,
+ label.c_str(),
+ 0,
+ 0,
+ style.tile_width,
+ style.tile_height,
+ nullptr,
+ 0,
+ 0,
+ 0,
+ 0,
+ "");
+ ui_def_but_icon(but,
+ preview_icon_id,
+ /* NOLINTNEXTLINE: bugprone-suspicious-enum-usage */
+ UI_HAS_ICON | UI_BUT_ICON_PREVIEW);
+}
+
+void PreviewGridItem::set_on_activate_fn(ActivateFn fn)
+{
+ activate_fn_ = fn;
+}
+
+void PreviewGridItem::set_is_active_fn(IsActiveFn fn)
+{
+ is_active_fn_ = fn;
+}
+
+void PreviewGridItem::on_activate()
+{
+ if (activate_fn_) {
+ activate_fn_(*this);
+ }
+}
+
+std::optional<bool> PreviewGridItem::should_be_active() const
+{
+ if (is_active_fn_) {
+ return is_active_fn_();
+ }
+ return std::nullopt;
+}
+
+} // namespace blender::ui
diff --git a/source/blender/editors/interface/views/interface_view.cc b/source/blender/editors/interface/views/interface_view.cc
new file mode 100644
index 00000000000..b35f6d2c969
--- /dev/null
+++ b/source/blender/editors/interface/views/interface_view.cc
@@ -0,0 +1,190 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup edinterface
+ *
+ * This part of the UI-View API is mostly needed to support persistent state of items within the
+ * view. Views are stored in #uiBlock's, and kept alive with it until after the next redraw. So we
+ * can compare the old view items with the new view items and keep state persistent for matching
+ * ones.
+ */
+
+#include <memory>
+#include <type_traits>
+#include <variant>
+
+#include "DNA_screen_types.h"
+
+#include "BKE_screen.h"
+
+#include "BLI_listbase.h"
+
+#include "ED_screen.h"
+
+#include "interface_intern.h"
+
+#include "UI_interface.hh"
+
+#include "UI_abstract_view.hh"
+#include "UI_grid_view.hh"
+#include "UI_tree_view.hh"
+
+using namespace blender;
+using namespace blender::ui;
+
+/**
+ * Wrapper to store views in a #ListBase, addressable via an identifier.
+ */
+struct ViewLink : public Link {
+ std::string idname;
+ std::unique_ptr<AbstractView> view;
+};
+
+template<class T>
+static T *ui_block_add_view_impl(uiBlock &block,
+ StringRef idname,
+ std::unique_ptr<AbstractView> view)
+{
+ ViewLink *view_link = MEM_new<ViewLink>(__func__);
+ BLI_addtail(&block.views, view_link);
+
+ view_link->view = std::move(view);
+ view_link->idname = idname;
+
+ return dynamic_cast<T *>(view_link->view.get());
+}
+
+AbstractGridView *UI_block_add_view(uiBlock &block,
+ StringRef idname,
+ std::unique_ptr<AbstractGridView> grid_view)
+{
+ return ui_block_add_view_impl<AbstractGridView>(block, idname, std::move(grid_view));
+}
+
+AbstractTreeView *UI_block_add_view(uiBlock &block,
+ StringRef idname,
+ std::unique_ptr<AbstractTreeView> tree_view)
+{
+ return ui_block_add_view_impl<AbstractTreeView>(block, idname, std::move(tree_view));
+}
+
+void ui_block_free_views(uiBlock *block)
+{
+ LISTBASE_FOREACH_MUTABLE (ViewLink *, link, &block->views) {
+ MEM_delete(link);
+ }
+}
+
+void UI_block_views_listen(const uiBlock *block, const wmRegionListenerParams *listener_params)
+{
+ ARegion *region = listener_params->region;
+
+ LISTBASE_FOREACH (ViewLink *, view_link, &block->views) {
+ if (view_link->view->listen(*listener_params->notifier)) {
+ ED_region_tag_redraw(region);
+ }
+ }
+}
+
+uiViewItemHandle *UI_block_view_find_item_at(const ARegion *region, const int xy[2])
+{
+ uiButViewItem *item_but = (uiButViewItem *)ui_view_item_find_mouse_over(region, xy);
+ if (!item_but) {
+ return nullptr;
+ }
+
+ return item_but->view_item;
+}
+
+uiViewItemHandle *UI_block_view_find_active_item(const ARegion *region)
+{
+ uiButViewItem *item_but = (uiButViewItem *)ui_view_item_find_active(region);
+ if (!item_but) {
+ return nullptr;
+ }
+
+ return item_but->view_item;
+}
+
+static StringRef ui_block_view_find_idname(const uiBlock &block, const AbstractView &view)
+{
+ /* First get the idname the of the view we're looking for. */
+ LISTBASE_FOREACH (ViewLink *, view_link, &block.views) {
+ if (view_link->view.get() == &view) {
+ return view_link->idname;
+ }
+ }
+
+ return {};
+}
+
+template<class T>
+static T *ui_block_view_find_matching_in_old_block_impl(const uiBlock &new_block,
+ const T &new_view)
+{
+ uiBlock *old_block = new_block.oldblock;
+ if (!old_block) {
+ return nullptr;
+ }
+
+ StringRef idname = ui_block_view_find_idname(new_block, new_view);
+ if (idname.is_empty()) {
+ return nullptr;
+ }
+
+ LISTBASE_FOREACH (ViewLink *, old_view_link, &old_block->views) {
+ if (old_view_link->idname == idname) {
+ return dynamic_cast<T *>(old_view_link->view.get());
+ }
+ }
+
+ return nullptr;
+}
+
+uiViewHandle *ui_block_view_find_matching_in_old_block(const uiBlock *new_block,
+ const uiViewHandle *new_view_handle)
+{
+ BLI_assert(new_block && new_view_handle);
+ const AbstractView &new_view = reinterpret_cast<const AbstractView &>(*new_view_handle);
+
+ AbstractView *old_view = ui_block_view_find_matching_in_old_block_impl(*new_block, new_view);
+ return reinterpret_cast<uiViewHandle *>(old_view);
+}
+
+uiButViewItem *ui_block_view_find_matching_view_item_but_in_old_block(
+ const uiBlock *new_block, const uiViewItemHandle *new_item_handle)
+{
+ uiBlock *old_block = new_block->oldblock;
+ if (!old_block) {
+ return nullptr;
+ }
+
+ const AbstractViewItem &new_item = *reinterpret_cast<const AbstractViewItem *>(new_item_handle);
+ const AbstractView *old_view = ui_block_view_find_matching_in_old_block_impl(
+ *new_block, new_item.get_view());
+ if (!old_view) {
+ return nullptr;
+ }
+
+ LISTBASE_FOREACH (uiBut *, old_but, &old_block->buttons) {
+ if (old_but->type != UI_BTYPE_VIEW_ITEM) {
+ continue;
+ }
+ uiButViewItem *old_item_but = (uiButViewItem *)old_but;
+ if (!old_item_but->view_item) {
+ continue;
+ }
+ AbstractViewItem &old_item = *reinterpret_cast<AbstractViewItem *>(old_item_but->view_item);
+ /* Check if the item is from the expected view. */
+ if (&old_item.get_view() != old_view) {
+ continue;
+ }
+
+ if (UI_view_item_matches(reinterpret_cast<const uiViewItemHandle *>(&new_item),
+ reinterpret_cast<const uiViewItemHandle *>(&old_item))) {
+ return old_item_but;
+ }
+ }
+
+ return nullptr;
+}
diff --git a/source/blender/editors/interface/views/tree_view.cc b/source/blender/editors/interface/views/tree_view.cc
new file mode 100644
index 00000000000..c224226ba17
--- /dev/null
+++ b/source/blender/editors/interface/views/tree_view.cc
@@ -0,0 +1,554 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup edinterface
+ */
+
+#include "DNA_userdef_types.h"
+#include "DNA_windowmanager_types.h"
+
+#include "BKE_context.h"
+
+#include "BLT_translation.h"
+
+#include "interface_intern.h"
+
+#include "UI_interface.h"
+
+#include "WM_api.h"
+#include "WM_types.h"
+
+#include "UI_tree_view.hh"
+
+namespace blender::ui {
+
+/* ---------------------------------------------------------------------- */
+
+/**
+ * Add a tree-item to the container. This is the only place where items should be added, it
+ * handles important invariants!
+ */
+AbstractTreeViewItem &TreeViewItemContainer::add_tree_item(
+ std::unique_ptr<AbstractTreeViewItem> item)
+{
+ children_.append(std::move(item));
+
+ /* The first item that will be added to the root sets this. */
+ if (root_ == nullptr) {
+ root_ = this;
+ }
+ AbstractTreeView &tree_view = static_cast<AbstractTreeView &>(*root_);
+ AbstractTreeViewItem &added_item = *children_.last();
+ added_item.root_ = root_;
+ tree_view.register_item(added_item);
+
+ if (root_ != this) {
+ /* Any item that isn't the root can be assumed to the a #AbstractTreeViewItem. Not entirely
+ * nice to static_cast this, but well... */
+ added_item.parent_ = static_cast<AbstractTreeViewItem *>(this);
+ }
+
+ return added_item;
+}
+
+void TreeViewItemContainer::foreach_item_recursive(ItemIterFn iter_fn, IterOptions options) const
+{
+ for (const auto &child : children_) {
+ iter_fn(*child);
+ if (bool(options & IterOptions::SkipCollapsed) && child->is_collapsed()) {
+ continue;
+ }
+
+ child->foreach_item_recursive(iter_fn, options);
+ }
+}
+
+/* ---------------------------------------------------------------------- */
+
+void AbstractTreeView::foreach_item(ItemIterFn iter_fn, IterOptions options) const
+{
+ foreach_item_recursive(iter_fn, options);
+}
+
+void AbstractTreeView::update_children_from_old(const AbstractView &old_view)
+{
+ const AbstractTreeView &old_tree_view = dynamic_cast<const AbstractTreeView &>(old_view);
+
+ update_children_from_old_recursive(*this, old_tree_view);
+}
+
+void AbstractTreeView::update_children_from_old_recursive(const TreeViewOrItem &new_items,
+ const TreeViewOrItem &old_items)
+{
+ for (const auto &new_item : new_items.children_) {
+ AbstractTreeViewItem *matching_old_item = find_matching_child(*new_item, old_items);
+ if (!matching_old_item) {
+ continue;
+ }
+
+ new_item->update_from_old(*matching_old_item);
+
+ /* Recurse into children of the matched item. */
+ update_children_from_old_recursive(*new_item, *matching_old_item);
+ }
+}
+
+AbstractTreeViewItem *AbstractTreeView::find_matching_child(
+ const AbstractTreeViewItem &lookup_item, const TreeViewOrItem &items)
+{
+ for (const auto &iter_item : items.children_) {
+ if (lookup_item.matches_single(*iter_item)) {
+ /* We have a matching item! */
+ return iter_item.get();
+ }
+ }
+
+ return nullptr;
+}
+
+void AbstractTreeView::change_state_delayed()
+{
+ BLI_assert_msg(
+ is_reconstructed(),
+ "These state changes are supposed to be delayed until reconstruction is completed");
+ foreach_item([](AbstractTreeViewItem &item) { item.change_state_delayed(); });
+}
+
+/* ---------------------------------------------------------------------- */
+
+void AbstractTreeViewItem::tree_row_click_fn(struct bContext * /*C*/,
+ void *but_arg1,
+ void * /*arg2*/)
+{
+ uiButViewItem *item_but = (uiButViewItem *)but_arg1;
+ AbstractTreeViewItem &tree_item = reinterpret_cast<AbstractTreeViewItem &>(*item_but->view_item);
+
+ tree_item.activate();
+ /* Not only activate the item, also show its children. Maybe this should be optional, or
+ * controlled by the specific tree-view. */
+ tree_item.set_collapsed(false);
+}
+
+void AbstractTreeViewItem::add_treerow_button(uiBlock &block)
+{
+ /* For some reason a width > (UI_UNIT_X * 2) make the layout system use all available width. */
+ view_item_but_ = (uiButViewItem *)uiDefBut(
+ &block, UI_BTYPE_VIEW_ITEM, 0, "", 0, 0, UI_UNIT_X * 10, UI_UNIT_Y, nullptr, 0, 0, 0, 0, "");
+
+ view_item_but_->view_item = reinterpret_cast<uiViewItemHandle *>(this);
+ UI_but_func_set(&view_item_but_->but, tree_row_click_fn, view_item_but_, nullptr);
+}
+
+void AbstractTreeViewItem::add_indent(uiLayout &row) const
+{
+ uiBlock *block = uiLayoutGetBlock(&row);
+ uiLayout *subrow = uiLayoutRow(&row, true);
+ uiLayoutSetFixedSize(subrow, true);
+
+ const float indent_size = count_parents() * UI_DPI_ICON_SIZE;
+ uiDefBut(block, UI_BTYPE_SEPR, 0, "", 0, 0, indent_size, 0, nullptr, 0.0, 0.0, 0, 0, "");
+
+ /* Indent items without collapsing icon some more within their parent. Makes it clear that they
+ * are actually nested and not just a row at the same level without a chevron. */
+ if (!is_collapsible() && parent_) {
+ uiDefBut(block, UI_BTYPE_SEPR, 0, "", 0, 0, 0.2f * UI_UNIT_X, 0, nullptr, 0.0, 0.0, 0, 0, "");
+ }
+
+ /* Restore. */
+ UI_block_layout_set_current(block, &row);
+}
+
+void AbstractTreeViewItem::collapse_chevron_click_fn(struct bContext *C,
+ void * /*but_arg1*/,
+ void * /*arg2*/)
+{
+ /* There's no data we could pass to this callback. It must be either the button itself or a
+ * consistent address to match buttons over redraws. So instead of passing it somehow, just
+ * lookup the hovered item via context here. */
+
+ const wmWindow *win = CTX_wm_window(C);
+ const ARegion *region = CTX_wm_region(C);
+ uiViewItemHandle *hovered_item_handle = UI_block_view_find_item_at(region, win->eventstate->xy);
+
+ AbstractTreeViewItem *hovered_item = from_item_handle<AbstractTreeViewItem>(hovered_item_handle);
+ BLI_assert(hovered_item != nullptr);
+
+ hovered_item->toggle_collapsed();
+ /* When collapsing an item with an active child, make this collapsed item active instead so the
+ * active item stays visible. */
+ if (hovered_item->has_active_child()) {
+ hovered_item->activate();
+ }
+}
+
+bool AbstractTreeViewItem::is_collapse_chevron_but(const uiBut *but)
+{
+ return but->type == UI_BTYPE_BUT_TOGGLE && ELEM(but->icon, ICON_TRIA_RIGHT, ICON_TRIA_DOWN) &&
+ (but->func == collapse_chevron_click_fn);
+}
+
+void AbstractTreeViewItem::add_collapse_chevron(uiBlock &block) const
+{
+ if (!is_collapsible()) {
+ return;
+ }
+
+ const BIFIconID icon = is_collapsed() ? ICON_TRIA_RIGHT : ICON_TRIA_DOWN;
+ uiBut *but = uiDefIconBut(
+ &block, UI_BTYPE_BUT_TOGGLE, 0, icon, 0, 0, UI_UNIT_X, UI_UNIT_Y, nullptr, 0, 0, 0, 0, "");
+ /* Note that we're passing the tree-row button here, not the chevron one. */
+ UI_but_func_set(but, collapse_chevron_click_fn, nullptr, nullptr);
+ UI_but_flag_disable(but, UI_BUT_UNDO);
+
+ /* Check if the query for the button matches the created button. */
+ BLI_assert(is_collapse_chevron_but(but));
+}
+
+void AbstractTreeViewItem::add_rename_button(uiLayout &row)
+{
+ uiBlock *block = uiLayoutGetBlock(&row);
+ eUIEmbossType previous_emboss = UI_block_emboss_get(block);
+
+ uiLayoutRow(&row, false);
+ /* Enable emboss for the text button. */
+ UI_block_emboss_set(block, UI_EMBOSS);
+
+ AbstractViewItem::add_rename_button(*block);
+
+ UI_block_emboss_set(block, previous_emboss);
+ UI_block_layout_set_current(block, &row);
+}
+
+bool AbstractTreeViewItem::has_active_child() const
+{
+ bool found = false;
+ foreach_item_recursive([&found](const AbstractTreeViewItem &item) {
+ if (item.is_active()) {
+ found = true;
+ }
+ });
+
+ return found;
+}
+
+void AbstractTreeViewItem::on_activate()
+{
+ /* Do nothing by default. */
+}
+
+std::optional<bool> AbstractTreeViewItem::should_be_active() const
+{
+ return std::nullopt;
+}
+
+bool AbstractTreeViewItem::supports_collapsing() const
+{
+ return true;
+}
+
+StringRef AbstractTreeViewItem::get_rename_string() const
+{
+ return label_;
+}
+
+bool AbstractTreeViewItem::rename(StringRefNull new_name)
+{
+ /* It is important to update the label after renaming, so #AbstractTreeViewItem::matches_single()
+ * recognizes the item. (It only compares labels by default.) */
+ label_ = new_name;
+ return true;
+}
+
+void AbstractTreeViewItem::update_from_old(const AbstractViewItem &old)
+{
+ AbstractViewItem::update_from_old(old);
+
+ const AbstractTreeViewItem &old_tree_item = dynamic_cast<const AbstractTreeViewItem &>(old);
+ is_open_ = old_tree_item.is_open_;
+}
+
+bool AbstractTreeViewItem::matches_single(const AbstractTreeViewItem &other) const
+{
+ return label_ == other.label_;
+}
+
+AbstractTreeView &AbstractTreeViewItem::get_tree_view() const
+{
+ return dynamic_cast<AbstractTreeView &>(get_view());
+}
+
+int AbstractTreeViewItem::count_parents() const
+{
+ int i = 0;
+ for (AbstractTreeViewItem *parent = parent_; parent; parent = parent->parent_) {
+ i++;
+ }
+ return i;
+}
+
+void AbstractTreeViewItem::activate()
+{
+ BLI_assert_msg(get_tree_view().is_reconstructed(),
+ "Item activation can't be done until reconstruction is completed");
+
+ if (is_active()) {
+ return;
+ }
+
+ /* Deactivate other items in the tree. */
+ get_tree_view().foreach_item([](auto &item) { item.deactivate(); });
+
+ on_activate();
+ /* Make sure the active item is always visible. */
+ ensure_parents_uncollapsed();
+
+ is_active_ = true;
+}
+
+void AbstractTreeViewItem::deactivate()
+{
+ is_active_ = false;
+}
+
+bool AbstractTreeViewItem::is_hovered() const
+{
+ BLI_assert_msg(get_tree_view().is_reconstructed(),
+ "State can't be queried until reconstruction is completed");
+ BLI_assert_msg(view_item_but_ != nullptr,
+ "Hovered state can't be queried before the tree row is being built");
+
+ const uiViewItemHandle *this_item_handle = reinterpret_cast<const uiViewItemHandle *>(this);
+ /* The new layout hasn't finished construction yet, so the final state of the button is unknown.
+ * Get the matching button from the previous redraw instead. */
+ uiButViewItem *old_item_but = ui_block_view_find_matching_view_item_but_in_old_block(
+ view_item_but_->but.block, this_item_handle);
+ return old_item_but && (old_item_but->but.flag & UI_ACTIVE);
+}
+
+bool AbstractTreeViewItem::is_collapsed() const
+{
+ BLI_assert_msg(get_tree_view().is_reconstructed(),
+ "State can't be queried until reconstruction is completed");
+ return is_collapsible() && !is_open_;
+}
+
+void AbstractTreeViewItem::toggle_collapsed()
+{
+ is_open_ = !is_open_;
+}
+
+void AbstractTreeViewItem::set_collapsed(bool collapsed)
+{
+ is_open_ = !collapsed;
+}
+
+bool AbstractTreeViewItem::is_collapsible() const
+{
+ if (children_.is_empty()) {
+ return false;
+ }
+ return this->supports_collapsing();
+}
+
+void AbstractTreeViewItem::ensure_parents_uncollapsed()
+{
+ for (AbstractTreeViewItem *parent = parent_; parent; parent = parent->parent_) {
+ parent->set_collapsed(false);
+ }
+}
+
+bool AbstractTreeViewItem::matches(const AbstractViewItem &other) const
+{
+ const AbstractTreeViewItem &other_tree_item = dynamic_cast<const AbstractTreeViewItem &>(other);
+
+ if (!matches_single(other_tree_item)) {
+ return false;
+ }
+ if (count_parents() != other_tree_item.count_parents()) {
+ return false;
+ }
+
+ for (AbstractTreeViewItem *parent = parent_, *other_parent = other_tree_item.parent_;
+ parent && other_parent;
+ parent = parent->parent_, other_parent = other_parent->parent_) {
+ if (!parent->matches_single(*other_parent)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+uiButViewItem *AbstractTreeViewItem::view_item_button()
+{
+ return view_item_but_;
+}
+
+void AbstractTreeViewItem::change_state_delayed()
+{
+ const std::optional<bool> should_be_active = this->should_be_active();
+ if (should_be_active.has_value() && *should_be_active) {
+ activate();
+ }
+}
+
+/* ---------------------------------------------------------------------- */
+
+class TreeViewLayoutBuilder {
+ uiBlock &block_;
+
+ friend TreeViewBuilder;
+
+ public:
+ void build_from_tree(const AbstractTreeView &tree_view);
+ void build_row(AbstractTreeViewItem &item) const;
+
+ uiBlock &block() const;
+ uiLayout *current_layout() const;
+
+ private:
+ /* Created through #TreeViewBuilder. */
+ TreeViewLayoutBuilder(uiBlock &block);
+
+ static void polish_layout(const uiBlock &block);
+};
+
+TreeViewLayoutBuilder::TreeViewLayoutBuilder(uiBlock &block) : block_(block)
+{
+}
+
+void TreeViewLayoutBuilder::build_from_tree(const AbstractTreeView &tree_view)
+{
+ uiLayout *prev_layout = current_layout();
+
+ uiLayout *box = uiLayoutBox(prev_layout);
+ uiLayoutColumn(box, false);
+
+ tree_view.foreach_item([this](AbstractTreeViewItem &item) { build_row(item); },
+ AbstractTreeView::IterOptions::SkipCollapsed);
+
+ UI_block_layout_set_current(&block(), prev_layout);
+}
+
+void TreeViewLayoutBuilder::polish_layout(const uiBlock &block)
+{
+ LISTBASE_FOREACH_BACKWARD (uiBut *, but, &block.buttons) {
+ if (AbstractTreeViewItem::is_collapse_chevron_but(but) && but->next &&
+ /* Embossed buttons with padding-less text padding look weird, so don't touch them. */
+ ELEM(but->next->emboss, UI_EMBOSS_NONE, UI_EMBOSS_NONE_OR_STATUS)) {
+ UI_but_drawflag_enable(static_cast<uiBut *>(but->next), UI_BUT_NO_TEXT_PADDING);
+ }
+
+ if (but->type == UI_BTYPE_VIEW_ITEM) {
+ break;
+ }
+ }
+}
+
+void TreeViewLayoutBuilder::build_row(AbstractTreeViewItem &item) const
+{
+ uiBlock &block_ = block();
+
+ uiLayout *prev_layout = current_layout();
+ eUIEmbossType previous_emboss = UI_block_emboss_get(&block_);
+
+ uiLayout *overlap = uiLayoutOverlap(prev_layout);
+
+ uiLayoutRow(overlap, false);
+ /* Every item gets one! Other buttons can be overlapped on top. */
+ item.add_treerow_button(block_);
+
+ /* After adding tree-row button (would disable hover highlighting). */
+ UI_block_emboss_set(&block_, UI_EMBOSS_NONE);
+
+ uiLayout *row = uiLayoutRow(overlap, true);
+ item.add_indent(*row);
+ item.add_collapse_chevron(block_);
+
+ if (item.is_renaming()) {
+ item.add_rename_button(*row);
+ }
+ else {
+ item.build_row(*row);
+ }
+ polish_layout(block_);
+
+ UI_block_emboss_set(&block_, previous_emboss);
+ UI_block_layout_set_current(&block_, prev_layout);
+}
+
+uiBlock &TreeViewLayoutBuilder::block() const
+{
+ return block_;
+}
+
+uiLayout *TreeViewLayoutBuilder::current_layout() const
+{
+ return block().curlayout;
+}
+
+/* ---------------------------------------------------------------------- */
+
+TreeViewBuilder::TreeViewBuilder(uiBlock &block) : block_(block)
+{
+}
+
+void TreeViewBuilder::build_tree_view(AbstractTreeView &tree_view)
+{
+ tree_view.build_tree();
+ tree_view.update_from_old(block_);
+ tree_view.change_state_delayed();
+
+ TreeViewLayoutBuilder builder(block_);
+ builder.build_from_tree(tree_view);
+}
+
+/* ---------------------------------------------------------------------- */
+
+BasicTreeViewItem::BasicTreeViewItem(StringRef label, BIFIconID icon_) : icon(icon_)
+{
+ label_ = label;
+}
+
+void BasicTreeViewItem::build_row(uiLayout &row)
+{
+ add_label(row);
+}
+
+void BasicTreeViewItem::add_label(uiLayout &layout, StringRefNull label_override)
+{
+ const StringRefNull label = label_override.is_empty() ? StringRefNull(label_) : label_override;
+
+ /* Some padding for labels without collapse chevron and no icon. Looks weird without. */
+ if (icon == ICON_NONE && !is_collapsible()) {
+ uiItemS_ex(&layout, 0.8f);
+ }
+ uiItemL(&layout, IFACE_(label.c_str()), icon);
+}
+
+void BasicTreeViewItem::on_activate()
+{
+ if (activate_fn_) {
+ activate_fn_(*this);
+ }
+}
+
+void BasicTreeViewItem::set_on_activate_fn(ActivateFn fn)
+{
+ activate_fn_ = fn;
+}
+
+void BasicTreeViewItem::set_is_active_fn(IsActiveFn is_active_fn)
+{
+ is_active_fn_ = is_active_fn;
+}
+
+std::optional<bool> BasicTreeViewItem::should_be_active() const
+{
+ if (is_active_fn_) {
+ return is_active_fn_();
+ }
+ return std::nullopt;
+}
+
+} // namespace blender::ui