#include #include #include #include namespace XCEngine::UI::Editor::Widgets { namespace { float ClampNonNegative(float value) { return (std::max)(value, 0.0f); } std::vector BuildItemOffsets(std::size_t count) { std::vector 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& items, std::size_t itemIndex) { if (itemIndex >= items.size() || items[itemIndex].forceLeaf) { return false; } const std::vector 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& 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& items, std::size_t itemIndex) { const std::vector 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 CollectUIEditorTreeViewVisibleItemIndices( const std::vector& items, const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel) { std::vector visibleItemIndices = {}; if (items.empty()) { return visibleItemIndices; } const std::vector 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& items, const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel, std::size_t itemIndex) { if (itemIndex >= items.size()) { return UIEditorTreeViewInvalidIndex; } const std::vector 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& 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(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& 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& 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& 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