关键节点

This commit is contained in:
2026-04-25 16:46:01 +08:00
parent 6002d86a7e
commit ef41c44464
516 changed files with 6175 additions and 12401 deletions

View File

@@ -0,0 +1,248 @@
#include <XCEditor/Menu/UIEditorMenuBar.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;
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 IsVisibleColor(const UIColor& color) {
return color.a > 0.0f;
}
float ResolveLabelTop(const UIRect& rect, const UIEditorMenuBarMetrics& metrics) {
return rect.y +
(std::max)(0.0f, (rect.height - ClampNonNegative(metrics.labelFontSize)) * 0.5f) +
metrics.labelInsetY;
}
bool IsButtonFocused(
const UIEditorMenuBarState& state,
std::size_t index) {
if (!state.focused) {
return false;
}
return state.openIndex == index || state.hoveredIndex == index;
}
UIColor ResolveButtonFillColor(
bool open,
bool hovered,
const UIEditorMenuBarPalette& palette) {
if (open) {
return palette.buttonOpenColor;
}
if (hovered) {
return palette.buttonHoveredColor;
}
return palette.buttonColor;
}
UIColor ResolveButtonBorderColor(
bool open,
bool focused,
const UIEditorMenuBarPalette& palette) {
if (focused) {
return palette.focusedBorderColor;
}
if (open) {
return palette.openBorderColor;
}
return palette.borderColor;
}
float ResolveButtonBorderThickness(
bool open,
bool focused,
const UIEditorMenuBarMetrics& metrics) {
if (focused) {
return metrics.focusedBorderThickness;
}
if (open) {
return metrics.openBorderThickness;
}
return metrics.baseBorderThickness;
}
} // namespace
float ResolveUIEditorMenuBarMeasuredLabelWidth(
const UIEditorMenuBarItem& item,
const UIEditorMenuBarMetrics& metrics,
const UIEditorTextMeasurer* textMeasurer) {
return ResolveUIEditorMeasuredTextWidth(
item.label,
item.measuredLabelWidth,
metrics.labelFontSize,
metrics.estimatedGlyphWidth,
textMeasurer);
}
float ResolveUIEditorMenuBarDesiredButtonWidth(
const UIEditorMenuBarItem& item,
const UIEditorMenuBarMetrics& metrics) {
return ResolveUIEditorMenuBarMeasuredLabelWidth(item, metrics) +
ClampNonNegative(metrics.buttonPaddingX) * 2.0f;
}
UIEditorMenuBarLayout BuildUIEditorMenuBarLayout(
const UIRect& bounds,
const std::vector<UIEditorMenuBarItem>& items,
const UIEditorMenuBarMetrics& metrics) {
UIEditorMenuBarLayout layout = {};
layout.bounds = UIRect(
bounds.x,
bounds.y,
ClampNonNegative(bounds.width),
ClampNonNegative(bounds.height));
layout.contentRect = UIRect(
layout.bounds.x + ClampNonNegative(metrics.horizontalInset),
layout.bounds.y + ClampNonNegative(metrics.verticalInset),
(std::max)(
layout.bounds.width - ClampNonNegative(metrics.horizontalInset) * 2.0f,
0.0f),
(std::max)(
layout.bounds.height - ClampNonNegative(metrics.verticalInset) * 2.0f,
0.0f));
layout.buttonRects.reserve(items.size());
float cursorX = layout.contentRect.x;
for (const UIEditorMenuBarItem& item : items) {
const float width = ResolveUIEditorMenuBarDesiredButtonWidth(item, metrics);
layout.buttonRects.emplace_back(
cursorX,
layout.contentRect.y,
width,
layout.contentRect.height);
cursorX += width + ClampNonNegative(metrics.buttonGap);
}
return layout;
}
UIEditorMenuBarHitTarget HitTestUIEditorMenuBar(
const UIEditorMenuBarLayout& layout,
const UIPoint& point) {
UIEditorMenuBarHitTarget target = {};
if (!IsPointInsideRect(layout.bounds, point)) {
return target;
}
for (std::size_t index = 0; index < layout.buttonRects.size(); ++index) {
if (IsPointInsideRect(layout.buttonRects[index], point)) {
target.kind = UIEditorMenuBarHitTargetKind::Button;
target.index = index;
return target;
}
}
target.kind = UIEditorMenuBarHitTargetKind::BarBackground;
return target;
}
void AppendUIEditorMenuBarBackground(
UIDrawList& drawList,
const UIEditorMenuBarLayout& layout,
const std::vector<UIEditorMenuBarItem>& items,
const UIEditorMenuBarState& state,
const UIEditorMenuBarPalette& palette,
const UIEditorMenuBarMetrics& metrics) {
drawList.AddFilledRect(layout.bounds, palette.barColor, metrics.barCornerRounding);
const UIColor barBorderColor =
state.focused ? palette.focusedBorderColor : palette.borderColor;
const float barBorderThickness =
state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness;
if (IsVisibleColor(barBorderColor) && barBorderThickness > 0.0f) {
drawList.AddRectOutline(
layout.bounds,
barBorderColor,
barBorderThickness,
metrics.barCornerRounding);
}
for (std::size_t index = 0; index < layout.buttonRects.size() && index < items.size(); ++index) {
const bool open = state.openIndex == index;
const bool hovered = state.hoveredIndex == index;
const bool focused = IsButtonFocused(state, index);
const UIColor buttonFillColor = ResolveButtonFillColor(open, hovered, palette);
const UIColor buttonBorderColor = ResolveButtonBorderColor(open, focused, palette);
const float buttonBorderThickness = ResolveButtonBorderThickness(open, focused, metrics);
if (IsVisibleColor(buttonFillColor)) {
drawList.AddFilledRect(
layout.buttonRects[index],
buttonFillColor,
metrics.buttonCornerRounding);
}
if (IsVisibleColor(buttonBorderColor) && buttonBorderThickness > 0.0f) {
drawList.AddRectOutline(
layout.buttonRects[index],
buttonBorderColor,
buttonBorderThickness,
metrics.buttonCornerRounding);
}
}
}
void AppendUIEditorMenuBarForeground(
UIDrawList& drawList,
const UIEditorMenuBarLayout& layout,
const std::vector<UIEditorMenuBarItem>& items,
const UIEditorMenuBarState&,
const UIEditorMenuBarPalette& palette,
const UIEditorMenuBarMetrics& metrics) {
for (std::size_t index = 0; index < layout.buttonRects.size() && index < items.size(); ++index) {
const UIRect& rect = layout.buttonRects[index];
const float textLeft = rect.x + ClampNonNegative(metrics.buttonPaddingX);
const float textRight = rect.x + rect.width - ClampNonNegative(metrics.buttonPaddingX);
if (textRight <= textLeft) {
continue;
}
drawList.PushClipRect(UIRect(textLeft, rect.y, textRight - textLeft, rect.height), true);
drawList.AddText(
UIPoint(textLeft, ResolveLabelTop(rect, metrics)),
items[index].label,
items[index].enabled ? palette.textPrimary : palette.textDisabled,
ClampNonNegative(metrics.labelFontSize));
drawList.PopClipRect();
}
}
void AppendUIEditorMenuBar(
UIDrawList& drawList,
const UIRect& bounds,
const std::vector<UIEditorMenuBarItem>& items,
const UIEditorMenuBarState& state,
const UIEditorMenuBarPalette& palette,
const UIEditorMenuBarMetrics& metrics) {
const UIEditorMenuBarLayout layout = BuildUIEditorMenuBarLayout(bounds, items, metrics);
AppendUIEditorMenuBarBackground(drawList, layout, items, state, palette, metrics);
AppendUIEditorMenuBarForeground(drawList, layout, items, state, palette, metrics);
}
} // namespace XCEngine::UI::Editor::Widgets

View File

@@ -0,0 +1,265 @@
#include <XCEditor/Menu/UIEditorMenuModel.h>
#include <XCEditor/Foundation/UIEditorShortcutManager.h>
#include <utility>
#include <unordered_set>
namespace XCEngine::UI::Editor {
namespace {
UIEditorMenuModelValidationResult MakeValidationError(
UIEditorMenuModelValidationCode code,
std::string message) {
UIEditorMenuModelValidationResult result = {};
result.code = code;
result.message = std::move(message);
return result;
}
bool ResolveCheckedState(
const UIEditorMenuCheckedStateBinding& binding,
const UIEditorWorkspaceController& controller) {
if (binding.source == UIEditorMenuCheckedStateSource::None) {
return false;
}
const UIEditorPanelSessionState* panelState =
FindUIEditorPanelSessionState(controller.GetSession(), binding.panelId);
if (panelState == nullptr) {
return false;
}
switch (binding.source) {
case UIEditorMenuCheckedStateSource::PanelOpen:
return panelState->open;
case UIEditorMenuCheckedStateSource::PanelVisible:
return panelState->visible;
case UIEditorMenuCheckedStateSource::PanelActive:
return controller.GetWorkspace().activePanelId == binding.panelId;
case UIEditorMenuCheckedStateSource::None:
break;
}
return false;
}
UIEditorMenuModelValidationResult ValidateMenuItems(
const std::vector<UIEditorMenuItemDescriptor>& items,
const UIEditorCommandRegistry& commandRegistry,
std::string_view parentPath) {
for (std::size_t index = 0; index < items.size(); ++index) {
const UIEditorMenuItemDescriptor& item = items[index];
const std::string itemPath =
std::string(parentPath) + "[" + std::to_string(index) + "]";
switch (item.kind) {
case UIEditorMenuItemKind::Command:
if (item.commandId.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::EmptyCommandId,
"Menu item '" + itemPath + "' must define a commandId.");
}
if (FindUIEditorCommandDescriptor(commandRegistry, item.commandId) == nullptr) {
return MakeValidationError(
UIEditorMenuModelValidationCode::UnknownCommandId,
"Menu item '" + itemPath + "' references unknown command '" +
item.commandId + "'.");
}
if (item.label.empty()) {
const UIEditorCommandDescriptor* descriptor =
FindUIEditorCommandDescriptor(commandRegistry, item.commandId);
if (descriptor == nullptr || descriptor->displayName.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::MissingItemLabel,
"Menu item '" + itemPath + "' must define a label or use a command with displayName.");
}
}
if (!item.children.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::CommandItemHasChildren,
"Command menu item '" + itemPath + "' must not define children.");
}
if (item.checkedState.source != UIEditorMenuCheckedStateSource::None &&
item.checkedState.panelId.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::MissingCheckedStatePanelId,
"Command menu item '" + itemPath + "' checked state requires a panelId.");
}
break;
case UIEditorMenuItemKind::Separator:
if (!item.commandId.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::SeparatorHasCommandId,
"Separator menu item '" + itemPath + "' must not define a commandId.");
}
if (!item.children.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::SeparatorHasChildren,
"Separator menu item '" + itemPath + "' must not define children.");
}
if (item.checkedState.source != UIEditorMenuCheckedStateSource::None) {
return MakeValidationError(
UIEditorMenuModelValidationCode::UnexpectedCheckedState,
"Separator menu item '" + itemPath + "' must not define checked state.");
}
break;
case UIEditorMenuItemKind::Submenu:
if (item.label.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::SubmenuMissingLabel,
"Submenu item '" + itemPath + "' must define a label.");
}
if (item.children.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::SubmenuEmptyChildren,
"Submenu item '" + itemPath + "' must contain at least one child.");
}
if (!item.commandId.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::SubmenuHasCommandId,
"Submenu item '" + itemPath + "' must not define a commandId.");
}
if (item.checkedState.source != UIEditorMenuCheckedStateSource::None) {
return MakeValidationError(
UIEditorMenuModelValidationCode::UnexpectedCheckedState,
"Submenu item '" + itemPath + "' must not define checked state.");
}
{
const auto childValidation =
ValidateMenuItems(item.children, commandRegistry, itemPath);
if (!childValidation.IsValid()) {
return childValidation;
}
}
break;
}
}
return {};
}
UIEditorResolvedMenuItem ResolveMenuItem(
const UIEditorMenuItemDescriptor& item,
const UIEditorCommandDispatcher& commandDispatcher,
const UIEditorWorkspaceController& controller,
const UIEditorShortcutManager* shortcutManager) {
UIEditorResolvedMenuItem resolved = {};
resolved.kind = item.kind;
resolved.itemId = item.itemId;
resolved.label = item.label;
resolved.commandId = item.commandId;
switch (item.kind) {
case UIEditorMenuItemKind::Separator:
resolved.enabled = false;
return resolved;
case UIEditorMenuItemKind::Submenu:
for (const UIEditorMenuItemDescriptor& child : item.children) {
resolved.children.push_back(
ResolveMenuItem(child, commandDispatcher, controller, shortcutManager));
}
resolved.enabled = !resolved.children.empty();
return resolved;
case UIEditorMenuItemKind::Command:
break;
}
const UIEditorCommandEvaluationResult evaluation =
commandDispatcher.Evaluate(item.commandId, controller);
resolved.commandDisplayName = evaluation.displayName;
if (resolved.label.empty()) {
resolved.label = evaluation.displayName;
}
resolved.shortcutText =
shortcutManager != nullptr
? shortcutManager->GetPreferredShortcutText(item.commandId)
: std::string();
resolved.enabled = evaluation.IsExecutable();
resolved.previewStatus = evaluation.previewResult.status;
resolved.message = evaluation.message;
resolved.checked = ResolveCheckedState(item.checkedState, controller);
return resolved;
}
} // namespace
std::string_view GetUIEditorMenuItemKindName(UIEditorMenuItemKind kind) {
switch (kind) {
case UIEditorMenuItemKind::Command:
return "Command";
case UIEditorMenuItemKind::Separator:
return "Separator";
case UIEditorMenuItemKind::Submenu:
return "Submenu";
}
return "Unknown";
}
UIEditorMenuModelValidationResult ValidateUIEditorMenuModel(
const UIEditorMenuModel& model,
const UIEditorCommandRegistry& commandRegistry) {
const auto commandValidation = ValidateUIEditorCommandRegistry(commandRegistry);
if (!commandValidation.IsValid()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::InvalidCommandRegistry,
commandValidation.message);
}
std::unordered_set<std::string> seenMenuIds = {};
for (std::size_t index = 0; index < model.menus.size(); ++index) {
const UIEditorMenuDescriptor& menu = model.menus[index];
const std::string menuPath = "menus[" + std::to_string(index) + "]";
if (menu.menuId.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::EmptyMenuId,
menuPath + " must define menuId.");
}
if (menu.label.empty()) {
return MakeValidationError(
UIEditorMenuModelValidationCode::EmptyMenuLabel,
menuPath + " must define label.");
}
if (!seenMenuIds.insert(menu.menuId).second) {
return MakeValidationError(
UIEditorMenuModelValidationCode::DuplicateMenuId,
"Duplicate menuId '" + menu.menuId + "'.");
}
const auto itemValidation =
ValidateMenuItems(menu.items, commandRegistry, menuPath + ".items");
if (!itemValidation.IsValid()) {
return itemValidation;
}
}
return {};
}
UIEditorResolvedMenuModel BuildUIEditorResolvedMenuModel(
const UIEditorMenuModel& model,
const UIEditorCommandDispatcher& commandDispatcher,
const UIEditorWorkspaceController& controller,
const UIEditorShortcutManager* shortcutManager) {
UIEditorResolvedMenuModel resolved = {};
for (const UIEditorMenuDescriptor& menu : model.menus) {
UIEditorResolvedMenuDescriptor resolvedMenu = {};
resolvedMenu.menuId = menu.menuId;
resolvedMenu.label = menu.label;
for (const UIEditorMenuItemDescriptor& item : menu.items) {
resolvedMenu.items.push_back(
ResolveMenuItem(item, commandDispatcher, controller, shortcutManager));
}
resolved.menus.push_back(std::move(resolvedMenu));
}
return resolved;
}
} // namespace XCEngine::UI::Editor

View File

@@ -0,0 +1,305 @@
#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) {
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 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(
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 =
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,
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,
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<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

View File

@@ -0,0 +1,222 @@
#include <XCEditor/Menu/UIEditorMenuSession.h>
#include <algorithm>
#include <utility>
namespace XCEngine::UI::Editor {
namespace {
bool AreEquivalentPopupEntries(
const ::XCEngine::UI::Widgets::UIPopupOverlayEntry& lhs,
const ::XCEngine::UI::Widgets::UIPopupOverlayEntry& rhs) {
return lhs.popupId == rhs.popupId &&
lhs.parentPopupId == rhs.parentPopupId &&
lhs.anchorRect.x == rhs.anchorRect.x &&
lhs.anchorRect.y == rhs.anchorRect.y &&
lhs.anchorRect.width == rhs.anchorRect.width &&
lhs.anchorRect.height == rhs.anchorRect.height &&
lhs.anchorPath == rhs.anchorPath &&
lhs.surfacePath == rhs.surfacePath &&
lhs.placement == rhs.placement &&
lhs.dismissOnPointerOutside == rhs.dismissOnPointerOutside &&
lhs.dismissOnEscape == rhs.dismissOnEscape &&
lhs.dismissOnFocusLoss == rhs.dismissOnFocusLoss;
}
} // namespace
bool UIEditorMenuSession::IsPopupOpen(std::string_view popupId) const {
return m_popupOverlayModel.FindPopup(popupId) != nullptr;
}
const UIEditorMenuPopupState* UIEditorMenuSession::FindPopupState(
std::string_view popupId) const {
for (const UIEditorMenuPopupState& state : m_popupStates) {
if (state.popupId == popupId) {
return &state;
}
}
return nullptr;
}
void UIEditorMenuSession::Reset() {
m_openRootMenuId.clear();
m_popupOverlayModel = {};
m_popupStates.clear();
m_openSubmenuItemIds.clear();
}
UIEditorMenuSessionMutationResult UIEditorMenuSession::OpenMenuBarRoot(
std::string_view menuId,
::XCEngine::UI::Widgets::UIPopupOverlayEntry entry) {
return OpenRootMenu(menuId, std::move(entry));
}
UIEditorMenuSessionMutationResult UIEditorMenuSession::OpenRootMenu(
std::string_view menuId,
::XCEngine::UI::Widgets::UIPopupOverlayEntry entry) {
if (menuId.empty() || entry.popupId.empty()) {
return BuildResult({});
}
const ::XCEngine::UI::Widgets::UIPopupOverlayEntry* rootPopup =
m_popupOverlayModel.GetRootPopup();
if (rootPopup != nullptr &&
m_openRootMenuId == menuId &&
AreEquivalentPopupEntries(*rootPopup, entry)) {
return BuildResult({});
}
entry.parentPopupId.clear();
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
m_popupOverlayModel.OpenPopup(std::move(entry));
if (mutation.changed) {
m_popupStates.clear();
UIEditorMenuPopupState rootState = {};
rootState.popupId = mutation.openedPopupId;
rootState.menuId = std::string(menuId);
m_popupStates.push_back(std::move(rootState));
RebuildDerivedState();
}
return BuildResult(mutation);
}
UIEditorMenuSessionMutationResult UIEditorMenuSession::HoverMenuBarRoot(
std::string_view menuId,
::XCEngine::UI::Widgets::UIPopupOverlayEntry entry) {
if (!HasOpenMenu() || IsMenuOpen(menuId)) {
return BuildResult({});
}
return OpenRootMenu(menuId, std::move(entry));
}
UIEditorMenuSessionMutationResult UIEditorMenuSession::HoverSubmenu(
std::string_view itemId,
::XCEngine::UI::Widgets::UIPopupOverlayEntry entry) {
if (!HasOpenMenu() ||
itemId.empty() ||
entry.popupId.empty() ||
entry.parentPopupId.empty() ||
m_popupOverlayModel.FindPopup(entry.parentPopupId) == nullptr) {
return BuildResult({});
}
const ::XCEngine::UI::Widgets::UIPopupOverlayEntry* existingPopup =
m_popupOverlayModel.FindPopup(entry.popupId);
if (existingPopup != nullptr &&
existingPopup->parentPopupId == entry.parentPopupId) {
return BuildResult({});
}
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
m_popupOverlayModel.OpenPopup(std::move(entry));
if (mutation.changed) {
RemoveClosedPopupStates(mutation.closedPopupIds);
UIEditorMenuPopupState popupState = {};
popupState.popupId = mutation.openedPopupId;
popupState.menuId = m_openRootMenuId;
popupState.itemId = std::string(itemId);
m_popupStates.push_back(std::move(popupState));
RebuildDerivedState();
}
return BuildResult(mutation);
}
UIEditorMenuSessionMutationResult UIEditorMenuSession::CloseAll(
::XCEngine::UI::Widgets::UIPopupDismissReason dismissReason) {
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
m_popupOverlayModel.CloseAll(dismissReason);
if (mutation.changed) {
RemoveClosedPopupStates(mutation.closedPopupIds);
RebuildDerivedState();
}
return BuildResult(mutation);
}
UIEditorMenuSessionMutationResult UIEditorMenuSession::DismissFromEscape() {
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
m_popupOverlayModel.DismissFromEscape();
if (mutation.changed) {
RemoveClosedPopupStates(mutation.closedPopupIds);
RebuildDerivedState();
}
return BuildResult(mutation);
}
UIEditorMenuSessionMutationResult UIEditorMenuSession::DismissFromPointerDown(
const UIInputPath& hitPath) {
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
m_popupOverlayModel.DismissFromPointerDown(hitPath);
if (mutation.changed) {
RemoveClosedPopupStates(mutation.closedPopupIds);
RebuildDerivedState();
}
return BuildResult(mutation);
}
UIEditorMenuSessionMutationResult UIEditorMenuSession::DismissFromFocusLoss(
const UIInputPath& focusedPath) {
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult mutation =
m_popupOverlayModel.DismissFromFocusLoss(focusedPath);
if (mutation.changed) {
RemoveClosedPopupStates(mutation.closedPopupIds);
RebuildDerivedState();
}
return BuildResult(mutation);
}
UIEditorMenuSessionMutationResult UIEditorMenuSession::BuildResult(
const ::XCEngine::UI::Widgets::UIPopupOverlayMutationResult& mutation) const {
UIEditorMenuSessionMutationResult result = {};
result.changed = mutation.changed;
result.openRootMenuId = m_openRootMenuId;
result.openedPopupId = mutation.openedPopupId;
result.closedPopupIds = mutation.closedPopupIds;
result.dismissReason = mutation.dismissReason;
return result;
}
void UIEditorMenuSession::RemoveClosedPopupStates(
const std::vector<std::string>& closedPopupIds) {
if (closedPopupIds.empty()) {
return;
}
std::erase_if(
m_popupStates,
[&closedPopupIds](const UIEditorMenuPopupState& state) {
return std::find(
closedPopupIds.begin(),
closedPopupIds.end(),
state.popupId) != closedPopupIds.end();
});
}
void UIEditorMenuSession::RebuildDerivedState() {
m_openSubmenuItemIds.clear();
if (m_popupStates.empty() || !m_popupOverlayModel.HasOpenPopups()) {
m_openRootMenuId.clear();
return;
}
m_openRootMenuId = m_popupStates.front().menuId;
for (std::size_t index = 1u; index < m_popupStates.size(); ++index) {
if (!m_popupStates[index].itemId.empty()) {
m_openSubmenuItemIds.push_back(m_popupStates[index].itemId);
}
}
}
} // namespace XCEngine::UI::Editor