360 lines
14 KiB
C++
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
|