Files
XCEngine/new_editor/src/Collections/UIEditorTreeView.cpp

360 lines
14 KiB
C++

#include <XCEditor/Collections/UIEditorTreeView.h>
#include <XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h>
#include <algorithm>
#include <cmath>
#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;
}
constexpr float kTreeFontSize = 12.0f;
float ResolveTreeViewRowHeight(
const UIEditorTreeViewItem& item,
const UIEditorTreeViewMetrics& metrics) {
return item.desiredHeight > 0.0f ? item.desiredHeight : metrics.rowHeight;
}
float ResolveTextTop(
const ::XCEngine::UI::UIRect& rect,
float fontSize,
float insetY) {
const float lineHeight = fontSize * 1.6f;
return rect.y + std::floor((rect.height - lineHeight) * 0.5f) + insetY;
}
void AppendDisclosureArrow(
::XCEngine::UI::UIDrawList& drawList,
const ::XCEngine::UI::UIRect& rect,
bool expanded,
const ::XCEngine::UI::UIColor& color) {
constexpr float kOpticalCenterYOffset = -1.0f;
const float centerX = std::floor(rect.x + rect.width * 0.5f) + 0.5f;
const float centerY =
std::floor(rect.y + rect.height * 0.5f + kOpticalCenterYOffset) + 0.5f;
const float halfExtent = (std::max)(3.0f, std::floor((std::min)(rect.width, rect.height) * 0.24f));
const float triangleHeight = halfExtent * 1.45f;
::XCEngine::UI::UIPoint points[3] = {};
if (expanded) {
points[0] = ::XCEngine::UI::UIPoint(centerX - halfExtent, centerY - triangleHeight * 0.5f);
points[1] = ::XCEngine::UI::UIPoint(centerX + halfExtent, centerY - triangleHeight * 0.5f);
points[2] = ::XCEngine::UI::UIPoint(centerX, centerY + triangleHeight * 0.5f);
} else {
points[0] = ::XCEngine::UI::UIPoint(centerX - triangleHeight * 0.5f, centerY - halfExtent);
points[1] = ::XCEngine::UI::UIPoint(centerX - triangleHeight * 0.5f, centerY + halfExtent);
points[2] = ::XCEngine::UI::UIPoint(centerX + triangleHeight * 0.5f, centerY);
}
drawList.AddFilledTriangle(points[0], points[1], points[2], color);
}
} // 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.iconRects.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 bool hasLeadingIcon = item.leadingIcon.IsValid();
const float iconExtent = ClampNonNegative(metrics.iconExtent);
const float contentStartX = disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap;
const ::XCEngine::UI::UIRect iconRect(
hasLeadingIcon ? contentStartX : 0.0f,
rowRect.y + (rowRect.height - iconExtent) * 0.5f + metrics.iconInsetY,
hasLeadingIcon ? iconExtent : 0.0f,
hasLeadingIcon ? iconExtent : 0.0f);
const float labelStartX =
hasLeadingIcon
? iconRect.x + iconRect.width + metrics.iconLabelGap
: contentStartX;
const ::XCEngine::UI::UIRect labelRect(
labelStartX,
rowRect.y,
(rowRect.x + rowRect.width) -
labelStartX -
metrics.horizontalPadding,
rowRect.height);
layout.rowRects.push_back(rowRect);
layout.disclosureRects.push_back(disclosureRect);
layout.iconRects.push_back(iconRect);
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]) {
AppendDisclosureArrow(
drawList,
layout.disclosureRects[visibleOffset],
layout.itemExpanded[visibleOffset],
palette.disclosureColor);
}
if (item.leadingIcon.IsValid()) {
drawList.AddImage(layout.iconRects[visibleOffset], item.leadingIcon);
}
drawList.PushClipRect(layout.labelRects[visibleOffset]);
drawList.AddText(
::XCEngine::UI::UIPoint(
layout.labelRects[visibleOffset].x,
ResolveTextTop(layout.labelRects[visibleOffset], kTreeFontSize, metrics.labelInsetY)),
item.label,
palette.textColor,
kTreeFontSize);
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