Files
XCEngine/editor/src/Menu/UIEditorMenuPopup.cpp

306 lines
11 KiB
C++
Raw Normal View History

#include <XCEditor/Menu/UIEditorMenuPopup.h>
#include <XCEditor/Foundation/UIEditorTextLayout.h>
#include <algorithm>
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;
}
bool IsInteractiveItem(const UIEditorMenuPopupItem& item) {
return item.kind != UIEditorMenuItemKind::Separator;
}
float ResolveRowTextTop(const UIRect& rect, const UIEditorMenuPopupMetrics& metrics) {
2026-04-08 02:52:28 +08:00
return rect.y + (std::max)(0.0f, (rect.height - metrics.labelFontSize) * 0.5f) + metrics.labelInsetY;
}
2026-04-08 02:52:28 +08:00
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 ResolveUIEditorMenuPopupMeasuredLabelWidth(
const UIEditorMenuPopupItem& item,
const UIEditorMenuPopupMetrics& metrics,
const UIEditorTextMeasurer* textMeasurer) {
return ResolveUIEditorMeasuredTextWidth(
item.label,
item.measuredLabelWidth,
metrics.labelFontSize,
metrics.estimatedGlyphWidth,
textMeasurer);
}
float ResolveUIEditorMenuPopupMeasuredShortcutWidth(
const UIEditorMenuPopupItem& item,
const UIEditorMenuPopupMetrics& metrics,
const UIEditorTextMeasurer* textMeasurer) {
return ResolveUIEditorMeasuredTextWidth(
item.shortcutText,
item.measuredShortcutWidth,
metrics.labelFontSize,
metrics.estimatedGlyphWidth,
textMeasurer);
}
float ResolveUIEditorMenuPopupDesiredWidth(
const std::vector<UIEditorMenuPopupItem>& items,
const UIEditorMenuPopupMetrics& metrics) {
float widestRow = 0.0f;
for (const UIEditorMenuPopupItem& item : items) {
if (item.kind == UIEditorMenuItemKind::Separator) {
continue;
}
const float labelWidth =
ResolveUIEditorMenuPopupMeasuredLabelWidth(item, metrics);
const float shortcutWidth =
item.shortcutText.empty()
? 0.0f
: ResolveUIEditorMenuPopupMeasuredShortcutWidth(item, metrics);
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<UIEditorMenuPopupItem>& 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<UIEditorMenuPopupItem>& 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<UIEditorMenuPopupItem>& 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<UIEditorMenuPopupItem>& 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<UIEditorMenuPopupItem>& 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(
2026-04-08 02:52:28 +08:00
UIPoint(checkLeft, ResolveGlyphTop(rect, metrics)),
"*",
palette.glyphColor,
2026-04-08 02:52:28 +08:00
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 =
ResolveUIEditorMenuPopupMeasuredShortcutWidth(item, metrics);
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,
2026-04-08 02:52:28 +08:00
metrics.labelFontSize);
drawList.PopClipRect();
if (!item.shortcutText.empty()) {
const float shortcutWidth =
ResolveUIEditorMenuPopupMeasuredShortcutWidth(item, metrics);
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,
2026-04-08 02:52:28 +08:00
metrics.labelFontSize);
}
if (item.hasSubmenu) {
const float submenuLeft =
rect.x + rect.width -
ClampNonNegative(metrics.shortcutInsetRight) -
ClampNonNegative(metrics.submenuIndicatorWidth);
drawList.AddText(
UIPoint(
submenuLeft,
2026-04-08 02:52:28 +08:00
ResolveGlyphTop(rect, metrics)),
">",
palette.glyphColor,
2026-04-08 02:52:28 +08:00
metrics.glyphFontSize);
}
}
}
void AppendUIEditorMenuPopup(
UIDrawList& drawList,
const UIRect& popupRect,
const std::vector<UIEditorMenuPopupItem>& 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