关键节点
This commit is contained in:
248
editor/src/Menu/UIEditorMenuBar.cpp
Normal file
248
editor/src/Menu/UIEditorMenuBar.cpp
Normal 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
|
||||
265
editor/src/Menu/UIEditorMenuModel.cpp
Normal file
265
editor/src/Menu/UIEditorMenuModel.cpp
Normal 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
|
||||
305
editor/src/Menu/UIEditorMenuPopup.cpp
Normal file
305
editor/src/Menu/UIEditorMenuPopup.cpp
Normal 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
|
||||
222
editor/src/Menu/UIEditorMenuSession.cpp
Normal file
222
editor/src/Menu/UIEditorMenuSession.cpp
Normal 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
|
||||
Reference in New Issue
Block a user