2026-04-15 08:24:06 +08:00
|
|
|
#include <XCEditor/Menu/UIEditorMenuPopup.h>
|
2026-04-07 03:51:26 +08:00
|
|
|
|
2026-04-22 01:50:00 +08:00
|
|
|
#include <XCEditor/Foundation/UIEditorTextLayout.h>
|
|
|
|
|
|
2026-04-07 03:51:26 +08:00
|
|
|
#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-07 03:51:26 +08:00
|
|
|
}
|
|
|
|
|
|
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;
|
2026-04-07 03:51:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsHighlighted(const UIEditorMenuPopupState& state, std::size_t index) {
|
|
|
|
|
return state.hoveredIndex == index || state.submenuOpenIndex == index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-04-22 01:50:00 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 03:51:26 +08:00
|
|
|
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 =
|
2026-04-22 01:50:00 +08:00
|
|
|
ResolveUIEditorMenuPopupMeasuredLabelWidth(item, metrics);
|
2026-04-07 03:51:26 +08:00
|
|
|
const float shortcutWidth =
|
|
|
|
|
item.shortcutText.empty()
|
|
|
|
|
? 0.0f
|
2026-04-22 01:50:00 +08:00
|
|
|
: ResolveUIEditorMenuPopupMeasuredShortcutWidth(item, metrics);
|
2026-04-07 03:51:26 +08:00
|
|
|
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;
|
2026-04-11 17:07:37 +08:00
|
|
|
const float separatorInset =
|
|
|
|
|
ClampNonNegative(metrics.contentPaddingX) + 3.0f;
|
2026-04-07 03:51:26 +08:00
|
|
|
drawList.AddFilledRect(
|
2026-04-11 17:07:37 +08:00
|
|
|
UIRect(
|
|
|
|
|
rect.x + separatorInset,
|
|
|
|
|
lineY,
|
|
|
|
|
(std::max)(rect.width - separatorInset * 2.0f, 0.0f),
|
|
|
|
|
metrics.separatorThickness),
|
2026-04-07 03:51:26 +08:00
|
|
|
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)),
|
2026-04-07 03:51:26 +08:00
|
|
|
"*",
|
|
|
|
|
palette.glyphColor,
|
2026-04-08 02:52:28 +08:00
|
|
|
metrics.glyphFontSize);
|
2026-04-07 03:51:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 =
|
2026-04-22 01:50:00 +08:00
|
|
|
ResolveUIEditorMenuPopupMeasuredShortcutWidth(item, metrics);
|
2026-04-07 03:51:26 +08:00
|
|
|
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);
|
2026-04-07 03:51:26 +08:00
|
|
|
drawList.PopClipRect();
|
|
|
|
|
|
|
|
|
|
if (!item.shortcutText.empty()) {
|
|
|
|
|
const float shortcutWidth =
|
2026-04-22 01:50:00 +08:00
|
|
|
ResolveUIEditorMenuPopupMeasuredShortcutWidth(item, metrics);
|
2026-04-07 03:51:26 +08:00
|
|
|
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);
|
2026-04-07 03:51:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (item.hasSubmenu) {
|
2026-04-11 22:14:02 +08:00
|
|
|
const float submenuLeft =
|
|
|
|
|
rect.x + rect.width -
|
|
|
|
|
ClampNonNegative(metrics.shortcutInsetRight) -
|
|
|
|
|
ClampNonNegative(metrics.submenuIndicatorWidth);
|
2026-04-07 03:51:26 +08:00
|
|
|
drawList.AddText(
|
|
|
|
|
UIPoint(
|
2026-04-11 22:14:02 +08:00
|
|
|
submenuLeft,
|
2026-04-08 02:52:28 +08:00
|
|
|
ResolveGlyphTop(rect, metrics)),
|
2026-04-07 03:51:26 +08:00
|
|
|
">",
|
|
|
|
|
palette.glyphColor,
|
2026-04-08 02:52:28 +08:00
|
|
|
metrics.glyphFontSize);
|
2026-04-07 03:51:26 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|