Refactor XCUI editor module layout

This commit is contained in:
2026-04-10 00:41:28 +08:00
parent 4b47764f26
commit 02a0e626fe
263 changed files with 12396 additions and 7592 deletions

View File

@@ -0,0 +1,314 @@
#include <XCEditor/Collections/UIEditorTreeView.h>
#include <XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h>
#include <algorithm>
#include <numeric>
namespace XCEngine::UI::Editor::Widgets {
namespace {
float ClampNonNegative(float value) {
return (std::max)(value, 0.0f);
}
std::vector<std::size_t> BuildItemOffsets(std::size_t count) {
std::vector<std::size_t> offsets(count);
std::iota(offsets.begin(), offsets.end(), 0u);
return offsets;
}
float ResolveTreeViewRowHeight(
const UIEditorTreeViewItem& item,
const UIEditorTreeViewMetrics& metrics) {
return item.desiredHeight > 0.0f ? item.desiredHeight : metrics.rowHeight;
}
::XCEngine::UI::UIPoint ResolveDisclosureGlyphPosition(
const ::XCEngine::UI::UIRect& rect,
float insetY) {
return ::XCEngine::UI::UIPoint(rect.x + 2.0f, rect.y + insetY - 1.0f);
}
} // namespace
bool IsUIEditorTreeViewPointInside(
const ::XCEngine::UI::UIRect& rect,
const ::XCEngine::UI::UIPoint& point) {
return point.x >= rect.x &&
point.x <= rect.x + rect.width &&
point.y >= rect.y &&
point.y <= rect.y + rect.height;
}
bool DoesUIEditorTreeViewItemHaveChildren(
const std::vector<UIEditorTreeViewItem>& items,
std::size_t itemIndex) {
if (itemIndex >= items.size() || items[itemIndex].forceLeaf) {
return false;
}
const std::vector<std::size_t> itemOffsets = BuildItemOffsets(items.size());
return ::XCEngine::UI::Widgets::UIFlatHierarchyHasChildren(
itemOffsets,
itemIndex,
[&](std::size_t offset) {
return items[offset].depth;
});
}
std::size_t FindUIEditorTreeViewItemIndex(
const std::vector<UIEditorTreeViewItem>& items,
std::string_view itemId) {
for (std::size_t itemIndex = 0u; itemIndex < items.size(); ++itemIndex) {
if (items[itemIndex].itemId == itemId) {
return itemIndex;
}
}
return UIEditorTreeViewInvalidIndex;
}
std::size_t FindUIEditorTreeViewParentItemIndex(
const std::vector<UIEditorTreeViewItem>& items,
std::size_t itemIndex) {
const std::vector<std::size_t> itemOffsets = BuildItemOffsets(items.size());
const std::size_t parentOffset = ::XCEngine::UI::Widgets::UIFlatHierarchyFindParentOffset(
itemOffsets,
itemIndex,
[&](std::size_t offset) {
return items[offset].depth;
});
return parentOffset != ::XCEngine::UI::Widgets::kInvalidUIFlatHierarchyItemOffset
? parentOffset
: UIEditorTreeViewInvalidIndex;
}
std::vector<std::size_t> CollectUIEditorTreeViewVisibleItemIndices(
const std::vector<UIEditorTreeViewItem>& items,
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel) {
std::vector<std::size_t> visibleItemIndices = {};
if (items.empty()) {
return visibleItemIndices;
}
const std::vector<std::size_t> itemOffsets = BuildItemOffsets(items.size());
for (std::size_t itemOffset = 0u; itemOffset < items.size(); ++itemOffset) {
const bool visible = ::XCEngine::UI::Widgets::UIFlatHierarchyIsVisible(
itemOffsets,
itemOffset,
[&](std::size_t offset) {
return items[offset].depth;
},
[&](std::size_t offset) {
return expansionModel.IsExpanded(items[offset].itemId);
});
if (visible) {
visibleItemIndices.push_back(itemOffset);
}
}
return visibleItemIndices;
}
std::size_t FindUIEditorTreeViewFirstVisibleChildItemIndex(
const std::vector<UIEditorTreeViewItem>& items,
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
std::size_t itemIndex) {
if (itemIndex >= items.size()) {
return UIEditorTreeViewInvalidIndex;
}
const std::vector<std::size_t> itemOffsets = BuildItemOffsets(items.size());
const std::size_t childOffset =
::XCEngine::UI::Widgets::UIFlatHierarchyFindFirstVisibleChildOffset(
itemOffsets,
itemIndex,
[&](std::size_t offset) {
return items[offset].depth;
},
[&](std::size_t offset) {
return ::XCEngine::UI::Widgets::UIFlatHierarchyIsVisible(
itemOffsets,
offset,
[&](std::size_t visibleOffset) {
return items[visibleOffset].depth;
},
[&](std::size_t visibleOffset) {
return expansionModel.IsExpanded(items[visibleOffset].itemId);
});
});
return childOffset != ::XCEngine::UI::Widgets::kInvalidUIFlatHierarchyItemOffset
? childOffset
: UIEditorTreeViewInvalidIndex;
}
UIEditorTreeViewLayout BuildUIEditorTreeViewLayout(
const ::XCEngine::UI::UIRect& bounds,
const std::vector<UIEditorTreeViewItem>& items,
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
const UIEditorTreeViewMetrics& metrics) {
UIEditorTreeViewLayout layout = {};
layout.bounds = ::XCEngine::UI::UIRect(
bounds.x,
bounds.y,
ClampNonNegative(bounds.width),
ClampNonNegative(bounds.height));
layout.visibleItemIndices = CollectUIEditorTreeViewVisibleItemIndices(items, expansionModel);
layout.rowRects.reserve(layout.visibleItemIndices.size());
layout.disclosureRects.reserve(layout.visibleItemIndices.size());
layout.labelRects.reserve(layout.visibleItemIndices.size());
layout.itemHasChildren.reserve(layout.visibleItemIndices.size());
layout.itemExpanded.reserve(layout.visibleItemIndices.size());
float rowY = layout.bounds.y;
for (std::size_t visibleOffset = 0u;
visibleOffset < layout.visibleItemIndices.size();
++visibleOffset) {
const std::size_t itemIndex = layout.visibleItemIndices[visibleOffset];
const UIEditorTreeViewItem& item = items[itemIndex];
const float rowHeight = ResolveTreeViewRowHeight(item, metrics);
const bool hasChildren = DoesUIEditorTreeViewItemHaveChildren(items, itemIndex);
const bool expanded = hasChildren && expansionModel.IsExpanded(item.itemId);
const ::XCEngine::UI::UIRect rowRect(
layout.bounds.x,
rowY,
layout.bounds.width,
rowHeight);
const float contentX =
rowRect.x + metrics.horizontalPadding + static_cast<float>(item.depth) * metrics.indentWidth;
const ::XCEngine::UI::UIRect disclosureRect(
contentX,
rowRect.y + (rowRect.height - metrics.disclosureExtent) * 0.5f,
metrics.disclosureExtent,
metrics.disclosureExtent);
const ::XCEngine::UI::UIRect labelRect(
disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap,
rowRect.y,
(rowRect.x + rowRect.width) -
(disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap) -
metrics.horizontalPadding,
rowRect.height);
layout.rowRects.push_back(rowRect);
layout.disclosureRects.push_back(disclosureRect);
layout.labelRects.push_back(labelRect);
layout.itemHasChildren.push_back(hasChildren);
layout.itemExpanded.push_back(expanded);
rowY += rowHeight + metrics.rowGap;
}
return layout;
}
UIEditorTreeViewHitTarget HitTestUIEditorTreeView(
const UIEditorTreeViewLayout& layout,
const ::XCEngine::UI::UIPoint& point) {
for (std::size_t visibleOffset = 0u; visibleOffset < layout.rowRects.size(); ++visibleOffset) {
if (!IsUIEditorTreeViewPointInside(layout.rowRects[visibleOffset], point)) {
continue;
}
UIEditorTreeViewHitTarget target = {};
target.visibleIndex = visibleOffset;
target.itemIndex = layout.visibleItemIndices[visibleOffset];
target.kind =
layout.itemHasChildren[visibleOffset] &&
IsUIEditorTreeViewPointInside(layout.disclosureRects[visibleOffset], point)
? UIEditorTreeViewHitTargetKind::Disclosure
: UIEditorTreeViewHitTargetKind::Row;
return target;
}
return {};
}
void AppendUIEditorTreeViewBackground(
::XCEngine::UI::UIDrawList& drawList,
const UIEditorTreeViewLayout& layout,
const std::vector<UIEditorTreeViewItem>& items,
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
const UIEditorTreeViewState& state,
const UIEditorTreeViewPalette& palette,
const UIEditorTreeViewMetrics& metrics) {
drawList.AddFilledRect(layout.bounds, palette.surfaceColor, metrics.cornerRounding);
drawList.AddRectOutline(
layout.bounds,
state.focused ? palette.focusedBorderColor : palette.borderColor,
state.focused ? metrics.focusedBorderThickness : metrics.borderThickness,
metrics.cornerRounding);
for (std::size_t visibleOffset = 0u; visibleOffset < layout.rowRects.size(); ++visibleOffset) {
const UIEditorTreeViewItem& item = items[layout.visibleItemIndices[visibleOffset]];
const bool selected = selectionModel.IsSelected(item.itemId);
const bool hovered = state.hoveredItemId == item.itemId;
if (!selected && !hovered) {
continue;
}
const ::XCEngine::UI::UIColor rowColor =
selected
? (state.focused ? palette.rowSelectedFocusedColor : palette.rowSelectedColor)
: palette.rowHoverColor;
drawList.AddFilledRect(layout.rowRects[visibleOffset], rowColor, metrics.cornerRounding);
}
}
void AppendUIEditorTreeViewForeground(
::XCEngine::UI::UIDrawList& drawList,
const UIEditorTreeViewLayout& layout,
const std::vector<UIEditorTreeViewItem>& items,
const UIEditorTreeViewPalette& palette,
const UIEditorTreeViewMetrics& metrics) {
drawList.PushClipRect(layout.bounds);
for (std::size_t visibleOffset = 0u; visibleOffset < layout.rowRects.size(); ++visibleOffset) {
const UIEditorTreeViewItem& item = items[layout.visibleItemIndices[visibleOffset]];
if (layout.itemHasChildren[visibleOffset]) {
drawList.AddText(
ResolveDisclosureGlyphPosition(
layout.disclosureRects[visibleOffset],
metrics.labelInsetY),
layout.itemExpanded[visibleOffset] ? "v" : ">",
palette.disclosureColor,
12.0f);
}
drawList.PushClipRect(layout.labelRects[visibleOffset]);
drawList.AddText(
::XCEngine::UI::UIPoint(
layout.labelRects[visibleOffset].x,
layout.labelRects[visibleOffset].y + metrics.labelInsetY),
item.label,
palette.textColor,
12.0f);
drawList.PopClipRect();
}
drawList.PopClipRect();
}
void AppendUIEditorTreeView(
::XCEngine::UI::UIDrawList& drawList,
const ::XCEngine::UI::UIRect& bounds,
const std::vector<UIEditorTreeViewItem>& items,
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
const UIEditorTreeViewState& state,
const UIEditorTreeViewPalette& palette,
const UIEditorTreeViewMetrics& metrics) {
const UIEditorTreeViewLayout layout =
BuildUIEditorTreeViewLayout(bounds, items, expansionModel, metrics);
AppendUIEditorTreeViewBackground(
drawList,
layout,
items,
selectionModel,
state,
palette,
metrics);
AppendUIEditorTreeViewForeground(drawList, layout, items, palette, metrics);
}
} // namespace XCEngine::UI::Editor::Widgets