#include #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; } 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) { const float lineHeight = fontSize * 1.6f; return rect.y + std::floor((rect.height - lineHeight) * 0.5f); } 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& 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.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(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, 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& 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]) { 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)), item.label, palette.textColor, kTreeFontSize); 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