#include #include namespace XCEngine::UI::Editor::Widgets { namespace { using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; using ::XCEngine::UI::Editor::UIEditorMenuItemKind; float ClampNonNegative(float value) { return (std::max)(value, 0.0f); } bool IsPointInsideRect(const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; } float ResolveEstimatedWidth(float explicitWidth, std::string_view text, float glyphWidth) { if (explicitWidth > 0.0f) { return explicitWidth; } return static_cast(text.size()) * glyphWidth; } bool IsInteractiveItem(const UIEditorMenuPopupItem& item) { return item.kind != UIEditorMenuItemKind::Separator; } float ResolveRowTextTop(const UIRect& rect, const UIEditorMenuPopupMetrics& metrics) { return rect.y + (std::max)(0.0f, (rect.height - metrics.labelFontSize) * 0.5f) + metrics.labelInsetY; } float ResolveGlyphTop(const UIRect& rect, const UIEditorMenuPopupMetrics& metrics) { return rect.y + (std::max)(0.0f, (rect.height - metrics.glyphFontSize) * 0.5f) - 0.5f; } bool IsHighlighted(const UIEditorMenuPopupState& state, std::size_t index) { return state.hoveredIndex == index || state.submenuOpenIndex == index; } } // namespace float ResolveUIEditorMenuPopupDesiredWidth( const std::vector& items, const UIEditorMenuPopupMetrics& metrics) { float widestRow = 0.0f; for (const UIEditorMenuPopupItem& item : items) { if (item.kind == UIEditorMenuItemKind::Separator) { continue; } const float labelWidth = ResolveEstimatedWidth(item.desiredLabelWidth, item.label, metrics.estimatedGlyphWidth); const float shortcutWidth = item.shortcutText.empty() ? 0.0f : ResolveEstimatedWidth( item.desiredShortcutWidth, item.shortcutText, metrics.estimatedGlyphWidth); const float submenuWidth = item.hasSubmenu ? ClampNonNegative(metrics.submenuIndicatorWidth) : 0.0f; const float rowWidth = ClampNonNegative(metrics.labelInsetX) + ClampNonNegative(metrics.checkColumnWidth) + labelWidth + (shortcutWidth > 0.0f ? ClampNonNegative(metrics.shortcutGap) + shortcutWidth : 0.0f) + submenuWidth + ClampNonNegative(metrics.shortcutInsetRight); widestRow = (std::max)(widestRow, rowWidth); } return widestRow + ClampNonNegative(metrics.contentPaddingX) * 2.0f; } float MeasureUIEditorMenuPopupHeight( const std::vector& items, const UIEditorMenuPopupMetrics& metrics) { float height = ClampNonNegative(metrics.contentPaddingY) * 2.0f; for (const UIEditorMenuPopupItem& item : items) { height += item.kind == UIEditorMenuItemKind::Separator ? ClampNonNegative(metrics.separatorHeight) : ClampNonNegative(metrics.itemHeight); } return height; } UIEditorMenuPopupLayout BuildUIEditorMenuPopupLayout( const UIRect& popupRect, const std::vector& items, const UIEditorMenuPopupMetrics& metrics) { UIEditorMenuPopupLayout layout = {}; layout.popupRect = UIRect( popupRect.x, popupRect.y, ClampNonNegative(popupRect.width), ClampNonNegative(popupRect.height)); layout.contentRect = UIRect( layout.popupRect.x + ClampNonNegative(metrics.contentPaddingX), layout.popupRect.y + ClampNonNegative(metrics.contentPaddingY), (std::max)( layout.popupRect.width - ClampNonNegative(metrics.contentPaddingX) * 2.0f, 0.0f), (std::max)( layout.popupRect.height - ClampNonNegative(metrics.contentPaddingY) * 2.0f, 0.0f)); float cursorY = layout.contentRect.y; layout.itemRects.reserve(items.size()); for (const UIEditorMenuPopupItem& item : items) { const float itemHeight = item.kind == UIEditorMenuItemKind::Separator ? ClampNonNegative(metrics.separatorHeight) : ClampNonNegative(metrics.itemHeight); layout.itemRects.emplace_back( layout.contentRect.x, cursorY, layout.contentRect.width, itemHeight); cursorY += itemHeight; } return layout; } UIEditorMenuPopupHitTarget HitTestUIEditorMenuPopup( const UIEditorMenuPopupLayout& layout, const std::vector& items, const UIPoint& point) { UIEditorMenuPopupHitTarget target = {}; if (!IsPointInsideRect(layout.popupRect, point)) { return target; } for (std::size_t index = 0; index < layout.itemRects.size() && index < items.size(); ++index) { if (IsInteractiveItem(items[index]) && IsPointInsideRect(layout.itemRects[index], point)) { target.kind = UIEditorMenuPopupHitTargetKind::Item; target.index = index; return target; } } target.kind = UIEditorMenuPopupHitTargetKind::PopupSurface; return target; } void AppendUIEditorMenuPopupBackground( UIDrawList& drawList, const UIEditorMenuPopupLayout& layout, const std::vector& items, const UIEditorMenuPopupState& state, const UIEditorMenuPopupPalette& palette, const UIEditorMenuPopupMetrics& metrics) { drawList.AddFilledRect(layout.popupRect, palette.popupColor, metrics.popupCornerRounding); drawList.AddRectOutline( layout.popupRect, palette.borderColor, metrics.borderThickness, metrics.popupCornerRounding); for (std::size_t index = 0; index < layout.itemRects.size() && index < items.size(); ++index) { const UIEditorMenuPopupItem& item = items[index]; const UIRect& rect = layout.itemRects[index]; if (item.kind == UIEditorMenuItemKind::Separator) { const float lineY = rect.y + rect.height * 0.5f; const float separatorInset = ClampNonNegative(metrics.contentPaddingX) + 3.0f; drawList.AddFilledRect( UIRect( rect.x + separatorInset, lineY, (std::max)(rect.width - separatorInset * 2.0f, 0.0f), metrics.separatorThickness), palette.separatorColor); continue; } if (IsHighlighted(state, index)) { drawList.AddFilledRect( rect, state.submenuOpenIndex == index ? palette.itemOpenColor : palette.itemHoverColor, metrics.rowCornerRounding); } } } void AppendUIEditorMenuPopupForeground( UIDrawList& drawList, const UIEditorMenuPopupLayout& layout, const std::vector& items, const UIEditorMenuPopupState&, const UIEditorMenuPopupPalette& palette, const UIEditorMenuPopupMetrics& metrics) { for (std::size_t index = 0; index < layout.itemRects.size() && index < items.size(); ++index) { const UIEditorMenuPopupItem& item = items[index]; if (item.kind == UIEditorMenuItemKind::Separator) { continue; } const UIRect& rect = layout.itemRects[index]; const UIColor mainColor = item.enabled ? palette.textPrimary : palette.textDisabled; const UIColor secondaryColor = item.enabled ? palette.textMuted : palette.textDisabled; const float checkLeft = rect.x + 6.0f; if (item.checked) { drawList.AddText( UIPoint(checkLeft, ResolveGlyphTop(rect, metrics)), "*", palette.glyphColor, metrics.glyphFontSize); } const float labelLeft = rect.x + ClampNonNegative(metrics.labelInsetX) + ClampNonNegative(metrics.checkColumnWidth); const float labelRight = rect.x + rect.width - ClampNonNegative(metrics.shortcutInsetRight); float labelClipWidth = (std::max)(labelRight - labelLeft, 0.0f); if (!item.shortcutText.empty()) { const float shortcutWidth = ResolveEstimatedWidth( item.desiredShortcutWidth, item.shortcutText, metrics.estimatedGlyphWidth); labelClipWidth = (std::max)( labelClipWidth - shortcutWidth - ClampNonNegative(metrics.shortcutGap), 0.0f); } if (item.hasSubmenu) { labelClipWidth = (std::max)( labelClipWidth - ClampNonNegative(metrics.submenuIndicatorWidth), 0.0f); } drawList.PushClipRect(UIRect(labelLeft, rect.y, labelClipWidth, rect.height), true); drawList.AddText( UIPoint(labelLeft, ResolveRowTextTop(rect, metrics)), item.label, mainColor, metrics.labelFontSize); drawList.PopClipRect(); if (!item.shortcutText.empty()) { const float shortcutWidth = ResolveEstimatedWidth( item.desiredShortcutWidth, item.shortcutText, metrics.estimatedGlyphWidth); const float shortcutLeft = rect.x + rect.width - ClampNonNegative(metrics.shortcutInsetRight) - shortcutWidth - (item.hasSubmenu ? ClampNonNegative(metrics.submenuIndicatorWidth) : 0.0f); drawList.AddText( UIPoint(shortcutLeft, ResolveRowTextTop(rect, metrics)), item.shortcutText, secondaryColor, metrics.labelFontSize); } if (item.hasSubmenu) { const float submenuLeft = rect.x + rect.width - ClampNonNegative(metrics.shortcutInsetRight) - ClampNonNegative(metrics.submenuIndicatorWidth); drawList.AddText( UIPoint( submenuLeft, ResolveGlyphTop(rect, metrics)), ">", palette.glyphColor, metrics.glyphFontSize); } } } void AppendUIEditorMenuPopup( UIDrawList& drawList, const UIRect& popupRect, const std::vector& items, const UIEditorMenuPopupState& state, const UIEditorMenuPopupPalette& palette, const UIEditorMenuPopupMetrics& metrics) { const UIEditorMenuPopupLayout layout = BuildUIEditorMenuPopupLayout(popupRect, items, metrics); AppendUIEditorMenuPopupBackground(drawList, layout, items, state, palette, metrics); AppendUIEditorMenuPopupForeground(drawList, layout, items, state, palette, metrics); } } // namespace XCEngine::UI::Editor::Widgets