Add editor shell interaction contract

This commit is contained in:
2026-04-07 10:16:55 +08:00
parent 558b6438cf
commit ec06340f58
11 changed files with 2312 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ add_library(XCUIEditorLib STATIC
src/Core/UIEditorMenuSession.cpp
src/Core/UIEditorPanelRegistry.cpp
src/Core/UIEditorShellCompose.cpp
src/Core/UIEditorShellInteraction.cpp
src/Core/UIEditorShortcutManager.cpp
src/Core/UIEditorViewportInputBridge.cpp
src/Core/UIEditorViewportShell.cpp

View File

@@ -0,0 +1,139 @@
#pragma once
#include <XCEditor/Core/UIEditorMenuModel.h>
#include <XCEditor/Core/UIEditorMenuSession.h>
#include <XCEditor/Core/UIEditorShellCompose.h>
#include <XCEditor/Core/UIEditorWorkspaceCompose.h>
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
#include <XCEngine/UI/DrawData.h>
#include <string>
#include <vector>
namespace XCEngine::UI::Editor {
struct UIEditorShellInteractionModel {
UIEditorResolvedMenuModel resolvedMenuModel = {};
std::vector<Widgets::UIEditorStatusBarSegment> statusSegments = {};
std::vector<UIEditorWorkspacePanelPresentationModel> workspacePresentations = {};
};
struct UIEditorShellInteractionState {
UIEditorShellComposeState composeState = {};
UIEditorMenuSession menuSession = {};
::XCEngine::UI::UIPoint pointerPosition = {};
bool focused = false;
bool hasPointerPosition = false;
};
struct UIEditorShellInteractionMetrics {
UIEditorShellComposeMetrics shellMetrics = {};
Widgets::UIEditorMenuPopupMetrics popupMetrics = {};
};
struct UIEditorShellInteractionPalette {
UIEditorShellComposePalette shellPalette = {};
Widgets::UIEditorMenuPopupPalette popupPalette = {};
};
struct UIEditorShellInteractionMenuButtonRequest {
std::string menuId = {};
std::string label = {};
std::string popupId = {};
::XCEngine::UI::UIRect rect = {};
::XCEngine::UI::UIInputPath path = {};
bool enabled = true;
};
struct UIEditorShellInteractionPopupItemRequest {
std::string popupId = {};
std::string menuId = {};
std::string itemId = {};
std::string label = {};
std::string commandId = {};
std::string childPopupId = {};
::XCEngine::UI::UIRect rect = {};
::XCEngine::UI::UIInputPath path = {};
UIEditorMenuItemKind kind = UIEditorMenuItemKind::Command;
bool enabled = false;
bool checked = false;
bool hasSubmenu = false;
};
struct UIEditorShellInteractionPopupRequest {
std::string popupId = {};
std::string menuId = {};
std::string sourceItemId = {};
::XCEngine::UI::Widgets::UIPopupOverlayEntry overlayEntry = {};
::XCEngine::UI::Widgets::UIPopupPlacementResult placement = {};
Widgets::UIEditorMenuPopupLayout layout = {};
std::vector<UIEditorResolvedMenuItem> resolvedItems = {};
std::vector<Widgets::UIEditorMenuPopupItem> widgetItems = {};
std::vector<UIEditorShellInteractionPopupItemRequest> itemRequests = {};
};
struct UIEditorShellInteractionRequest {
UIEditorShellComposeRequest shellRequest = {};
std::vector<Widgets::UIEditorMenuBarItem> menuBarItems = {};
std::vector<UIEditorShellInteractionMenuButtonRequest> menuButtons = {};
std::vector<UIEditorShellInteractionPopupRequest> popupRequests = {};
};
struct UIEditorShellInteractionResult {
bool consumed = false;
bool commandTriggered = false;
std::string menuId = {};
std::string popupId = {};
std::string itemId = {};
std::string commandId = {};
UIEditorMenuSessionMutationResult menuMutation = {};
};
struct UIEditorShellInteractionPopupFrame {
std::string popupId = {};
Widgets::UIEditorMenuPopupState popupState = {};
};
struct UIEditorShellInteractionFrame {
UIEditorShellInteractionRequest request = {};
UIEditorShellComposeFrame shellFrame = {};
std::vector<UIEditorShellInteractionPopupFrame> popupFrames = {};
UIEditorShellInteractionResult result = {};
std::string openRootMenuId = {};
std::string hoveredMenuId = {};
std::string hoveredPopupId = {};
std::string hoveredItemId = {};
bool focused = false;
};
UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest(
const ::XCEngine::UI::UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
const UIEditorShellInteractionModel& model,
const Widgets::UIEditorDockHostState& dockHostState = {},
const UIEditorShellInteractionState& state = {},
const UIEditorShellInteractionMetrics& metrics = {});
UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
UIEditorShellInteractionState& state,
const ::XCEngine::UI::UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
const UIEditorShellInteractionModel& model,
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
const Widgets::UIEditorDockHostState& dockHostState = {},
const UIEditorShellInteractionMetrics& metrics = {});
void AppendUIEditorShellInteraction(
::XCEngine::UI::UIDrawList& drawList,
const UIEditorShellInteractionFrame& frame,
const UIEditorShellInteractionModel& model,
const UIEditorShellInteractionState& state,
const UIEditorShellInteractionPalette& palette = {},
const UIEditorShellInteractionMetrics& metrics = {});
} // namespace XCEngine::UI::Editor

View File

@@ -0,0 +1,756 @@
#include <XCEditor/Core/UIEditorShellInteraction.h>
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
#include <algorithm>
#include <cstdint>
#include <string_view>
#include <utility>
namespace XCEngine::UI::Editor {
namespace {
using XCEngine::Input::KeyCode;
using ::XCEngine::UI::UIElementId;
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIInputPath;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIRect;
using ::XCEngine::UI::UISize;
using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect;
using ::XCEngine::UI::Widgets::UIPopupOverlayEntry;
using ::XCEngine::UI::Widgets::UIPopupPlacement;
using Widgets::AppendUIEditorMenuPopupBackground;
using Widgets::AppendUIEditorMenuPopupForeground;
using Widgets::BuildUIEditorMenuBarLayout;
using Widgets::BuildUIEditorMenuPopupLayout;
using Widgets::HitTestUIEditorMenuBar;
using Widgets::HitTestUIEditorMenuPopup;
using Widgets::MeasureUIEditorMenuPopupHeight;
using Widgets::ResolveUIEditorMenuPopupDesiredWidth;
using Widgets::UIEditorMenuBarHitTargetKind;
using Widgets::UIEditorMenuBarInvalidIndex;
using Widgets::UIEditorMenuPopupHitTargetKind;
using Widgets::UIEditorMenuPopupInvalidIndex;
constexpr UIElementId kShellPathRoot = 0x1E1001ull;
constexpr UIElementId kMenuBarPathRoot = 0x1E1002ull;
constexpr UIElementId kPopupPathRoot = 0x1E1003ull;
constexpr UIElementId kMenuItemPathRoot = 0x1E1004ull;
constexpr UIElementId kOutsidePointerPath = 0x1E10FFull;
struct RequestHit {
const UIEditorShellInteractionMenuButtonRequest* menuButton = nullptr;
const UIEditorShellInteractionPopupRequest* popupRequest = nullptr;
const UIEditorShellInteractionPopupItemRequest* popupItem = nullptr;
};
struct BuildRequestOutput {
UIEditorShellInteractionRequest request = {};
bool hadInvalidPopupState = false;
};
UIElementId HashText(std::string_view text) {
std::uint64_t hash = 1469598103934665603ull;
for (const unsigned char value : text) {
hash ^= value;
hash *= 1099511628211ull;
}
hash &= 0x7FFFFFFFFFFFFFFFull;
return hash == 0u ? 1u : hash;
}
std::string BuildRootPopupId(std::string_view menuId) {
return "editor.menu.root." + std::string(menuId);
}
std::string BuildSubmenuPopupId(std::string_view popupId, std::string_view itemId) {
return std::string(popupId) + ".child." + std::string(itemId);
}
UIInputPath BuildMenuButtonPath(std::string_view menuId) {
return UIInputPath { kShellPathRoot, kMenuBarPathRoot, HashText(menuId) };
}
UIInputPath BuildPopupSurfacePath(std::string_view popupId) {
return UIInputPath { kShellPathRoot, kPopupPathRoot, HashText(popupId) };
}
UIInputPath BuildMenuItemPath(std::string_view popupId, std::string_view itemId) {
return UIInputPath {
kShellPathRoot,
kPopupPathRoot,
HashText(popupId),
kMenuItemPathRoot,
HashText(itemId)
};
}
const UIEditorResolvedMenuDescriptor* FindResolvedMenu(
const UIEditorResolvedMenuModel& model,
std::string_view menuId) {
for (const UIEditorResolvedMenuDescriptor& menu : model.menus) {
if (menu.menuId == menuId) {
return &menu;
}
}
return nullptr;
}
const UIEditorResolvedMenuItem* FindResolvedMenuItemRecursive(
const std::vector<UIEditorResolvedMenuItem>& items,
std::string_view itemId) {
for (const UIEditorResolvedMenuItem& item : items) {
if (item.itemId == itemId) {
return &item;
}
if (!item.children.empty()) {
if (const UIEditorResolvedMenuItem* child =
FindResolvedMenuItemRecursive(item.children, itemId);
child != nullptr) {
return child;
}
}
}
return nullptr;
}
const std::vector<UIEditorResolvedMenuItem>* ResolvePopupItems(
const UIEditorResolvedMenuModel& model,
const UIEditorMenuPopupState& popupState) {
const UIEditorResolvedMenuDescriptor* menu =
FindResolvedMenu(model, popupState.menuId);
if (menu == nullptr) {
return nullptr;
}
if (popupState.itemId.empty()) {
return &menu->items;
}
const UIEditorResolvedMenuItem* item =
FindResolvedMenuItemRecursive(menu->items, popupState.itemId);
if (item == nullptr || item->kind != UIEditorMenuItemKind::Submenu) {
return nullptr;
}
return &item->children;
}
std::vector<Widgets::UIEditorMenuBarItem> BuildMenuBarItems(
const UIEditorResolvedMenuModel& model) {
std::vector<Widgets::UIEditorMenuBarItem> items = {};
items.reserve(model.menus.size());
for (const UIEditorResolvedMenuDescriptor& menu : model.menus) {
Widgets::UIEditorMenuBarItem item = {};
item.menuId = menu.menuId;
item.label = menu.label;
item.enabled = !menu.items.empty();
items.push_back(std::move(item));
}
return items;
}
UIEditorShellComposeModel BuildShellComposeModel(
const UIEditorShellInteractionModel& model,
const std::vector<Widgets::UIEditorMenuBarItem>& menuBarItems) {
UIEditorShellComposeModel shellModel = {};
shellModel.menuBarItems = menuBarItems;
shellModel.statusSegments = model.statusSegments;
shellModel.workspacePresentations = model.workspacePresentations;
return shellModel;
}
std::vector<Widgets::UIEditorMenuPopupItem> BuildPopupWidgetItems(
const std::vector<UIEditorResolvedMenuItem>& items) {
std::vector<Widgets::UIEditorMenuPopupItem> widgetItems = {};
widgetItems.reserve(items.size());
for (const UIEditorResolvedMenuItem& item : items) {
Widgets::UIEditorMenuPopupItem widgetItem = {};
widgetItem.itemId = item.itemId;
widgetItem.kind = item.kind;
widgetItem.label = item.label;
widgetItem.shortcutText = item.shortcutText;
widgetItem.enabled = item.enabled;
widgetItem.checked = item.checked;
widgetItem.hasSubmenu = item.kind == UIEditorMenuItemKind::Submenu && !item.children.empty();
widgetItems.push_back(std::move(widgetItem));
}
return widgetItems;
}
std::size_t FindMenuBarIndex(
const std::vector<Widgets::UIEditorMenuBarItem>& items,
std::string_view menuId) {
for (std::size_t index = 0; index < items.size(); ++index) {
if (items[index].menuId == menuId) {
return index;
}
}
return UIEditorMenuBarInvalidIndex;
}
std::size_t FindPopupItemIndex(
const std::vector<UIEditorShellInteractionPopupItemRequest>& items,
std::string_view itemId) {
for (std::size_t index = 0; index < items.size(); ++index) {
if (items[index].itemId == itemId) {
return index;
}
}
return UIEditorMenuPopupInvalidIndex;
}
bool HasMeaningfulInteractionResult(
const UIEditorShellInteractionResult& result) {
return result.consumed ||
result.commandTriggered ||
result.menuMutation.changed ||
!result.menuId.empty() ||
!result.popupId.empty() ||
!result.itemId.empty() ||
!result.commandId.empty();
}
BuildRequestOutput BuildRequest(
const UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
const UIEditorShellInteractionModel& model,
const Widgets::UIEditorDockHostState& dockHostState,
const UIEditorShellInteractionState& state,
const UIEditorShellInteractionMetrics& metrics) {
BuildRequestOutput output = {};
UIEditorShellInteractionRequest& request = output.request;
request.menuBarItems = BuildMenuBarItems(model.resolvedMenuModel);
const UIEditorShellComposeModel shellModel =
BuildShellComposeModel(model, request.menuBarItems);
request.shellRequest = ResolveUIEditorShellComposeRequest(
bounds,
panelRegistry,
workspace,
session,
shellModel,
dockHostState,
state.composeState,
metrics.shellMetrics);
request.menuButtons.reserve(request.menuBarItems.size());
for (std::size_t index = 0; index < request.menuBarItems.size(); ++index) {
UIEditorShellInteractionMenuButtonRequest button = {};
button.menuId = request.menuBarItems[index].menuId;
button.label = request.menuBarItems[index].label;
button.popupId = BuildRootPopupId(button.menuId);
button.enabled = request.menuBarItems[index].enabled;
if (index < request.shellRequest.layout.menuBarLayout.buttonRects.size()) {
button.rect = request.shellRequest.layout.menuBarLayout.buttonRects[index];
}
button.path = BuildMenuButtonPath(button.menuId);
request.menuButtons.push_back(std::move(button));
}
const auto& popupStates = state.menuSession.GetPopupStates();
request.popupRequests.reserve(popupStates.size());
for (const UIEditorMenuPopupState& popupState : popupStates) {
const UIPopupOverlayEntry* overlayEntry =
state.menuSession.GetPopupOverlayModel().FindPopup(popupState.popupId);
const std::vector<UIEditorResolvedMenuItem>* resolvedItems =
ResolvePopupItems(model.resolvedMenuModel, popupState);
if (overlayEntry == nullptr || resolvedItems == nullptr) {
output.hadInvalidPopupState = true;
continue;
}
UIEditorShellInteractionPopupRequest popupRequest = {};
popupRequest.popupId = popupState.popupId;
popupRequest.menuId = popupState.menuId;
popupRequest.sourceItemId = popupState.itemId;
popupRequest.overlayEntry = *overlayEntry;
popupRequest.resolvedItems = *resolvedItems;
popupRequest.widgetItems = BuildPopupWidgetItems(popupRequest.resolvedItems);
const float popupWidth =
ResolveUIEditorMenuPopupDesiredWidth(popupRequest.widgetItems, metrics.popupMetrics);
const float popupHeight =
MeasureUIEditorMenuPopupHeight(popupRequest.widgetItems, metrics.popupMetrics);
popupRequest.placement = ResolvePopupPlacementRect(
overlayEntry->anchorRect,
UISize(popupWidth, popupHeight),
request.shellRequest.layout.bounds,
overlayEntry->placement);
popupRequest.layout =
BuildUIEditorMenuPopupLayout(popupRequest.placement.rect, popupRequest.widgetItems, metrics.popupMetrics);
popupRequest.itemRequests.reserve(popupRequest.resolvedItems.size());
for (std::size_t index = 0; index < popupRequest.resolvedItems.size(); ++index) {
const UIEditorResolvedMenuItem& resolvedItem = popupRequest.resolvedItems[index];
UIEditorShellInteractionPopupItemRequest itemRequest = {};
itemRequest.popupId = popupRequest.popupId;
itemRequest.menuId = popupRequest.menuId;
itemRequest.itemId = resolvedItem.itemId;
itemRequest.label = resolvedItem.label;
itemRequest.commandId = resolvedItem.commandId;
itemRequest.kind = resolvedItem.kind;
itemRequest.enabled = resolvedItem.enabled;
itemRequest.checked = resolvedItem.checked;
itemRequest.hasSubmenu =
resolvedItem.kind == UIEditorMenuItemKind::Submenu &&
!resolvedItem.children.empty();
if (itemRequest.hasSubmenu) {
itemRequest.childPopupId =
BuildSubmenuPopupId(popupRequest.popupId, itemRequest.itemId);
}
if (index < popupRequest.layout.itemRects.size()) {
itemRequest.rect = popupRequest.layout.itemRects[index];
}
itemRequest.path = BuildMenuItemPath(popupRequest.popupId, itemRequest.itemId);
popupRequest.itemRequests.push_back(std::move(itemRequest));
}
request.popupRequests.push_back(std::move(popupRequest));
}
return output;
}
RequestHit HitTestRequest(
const UIEditorShellInteractionRequest& request,
const UIPoint& point,
bool hasPointerPosition) {
RequestHit hit = {};
if (!hasPointerPosition) {
return hit;
}
for (std::size_t index = request.popupRequests.size(); index > 0u; --index) {
const UIEditorShellInteractionPopupRequest& popupRequest =
request.popupRequests[index - 1u];
const auto popupHit =
HitTestUIEditorMenuPopup(popupRequest.layout, popupRequest.widgetItems, point);
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item &&
popupHit.index < popupRequest.itemRequests.size()) {
hit.popupRequest = &popupRequest;
hit.popupItem = &popupRequest.itemRequests[popupHit.index];
return hit;
}
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::PopupSurface) {
hit.popupRequest = &popupRequest;
return hit;
}
}
const auto menuHit =
HitTestUIEditorMenuBar(request.shellRequest.layout.menuBarLayout, point);
if (menuHit.kind == UIEditorMenuBarHitTargetKind::Button &&
menuHit.index < request.menuButtons.size()) {
hit.menuButton = &request.menuButtons[menuHit.index];
}
return hit;
}
UIPopupOverlayEntry BuildRootPopupEntry(
const UIEditorShellInteractionMenuButtonRequest& button) {
UIPopupOverlayEntry entry = {};
entry.popupId = button.popupId;
entry.anchorRect = button.rect;
entry.anchorPath = button.path;
entry.surfacePath = BuildPopupSurfacePath(button.popupId);
entry.placement = UIPopupPlacement::BottomStart;
return entry;
}
UIPopupOverlayEntry BuildSubmenuPopupEntry(
const UIEditorShellInteractionPopupItemRequest& item) {
UIPopupOverlayEntry entry = {};
entry.popupId = item.childPopupId;
entry.parentPopupId = item.popupId;
entry.anchorRect = item.rect;
entry.anchorPath = item.path;
entry.surfacePath = BuildPopupSurfacePath(item.childPopupId);
entry.placement = UIPopupPlacement::RightStart;
return entry;
}
void UpdateMenuBarVisualState(
UIEditorShellInteractionState& state,
const UIEditorShellInteractionRequest& request,
const RequestHit& hit) {
state.composeState.menuBarState.openIndex = FindMenuBarIndex(
request.menuBarItems,
state.menuSession.GetOpenRootMenuId());
state.composeState.menuBarState.hoveredIndex =
hit.menuButton != nullptr
? FindMenuBarIndex(request.menuBarItems, hit.menuButton->menuId)
: UIEditorMenuBarInvalidIndex;
state.composeState.menuBarState.focused =
state.focused || state.menuSession.HasOpenMenu();
}
std::vector<UIEditorShellInteractionPopupFrame> BuildPopupFrames(
const UIEditorShellInteractionRequest& request,
const UIEditorShellInteractionState& state,
std::string_view hoveredPopupId,
std::string_view hoveredItemId) {
std::vector<UIEditorShellInteractionPopupFrame> popupFrames = {};
popupFrames.reserve(request.popupRequests.size());
for (std::size_t index = 0; index < request.popupRequests.size(); ++index) {
const UIEditorShellInteractionPopupRequest& popupRequest =
request.popupRequests[index];
UIEditorShellInteractionPopupFrame popupFrame = {};
popupFrame.popupId = popupRequest.popupId;
popupFrame.popupState.focused = state.focused || state.menuSession.HasOpenMenu();
popupFrame.popupState.hoveredIndex =
popupRequest.popupId == hoveredPopupId
? FindPopupItemIndex(popupRequest.itemRequests, hoveredItemId)
: UIEditorMenuPopupInvalidIndex;
if (index + 1u < request.popupRequests.size()) {
popupFrame.popupState.submenuOpenIndex = FindPopupItemIndex(
popupRequest.itemRequests,
request.popupRequests[index + 1u].sourceItemId);
}
popupFrames.push_back(std::move(popupFrame));
}
return popupFrames;
}
bool ShouldUsePointerPosition(const UIInputEvent& event) {
switch (event.type) {
case UIInputEventType::PointerMove:
case UIInputEventType::PointerEnter:
case UIInputEventType::PointerButtonDown:
case UIInputEventType::PointerButtonUp:
case UIInputEventType::PointerWheel:
return true;
default:
return false;
}
}
std::vector<UIInputEvent> FilterComposeInputEvents(
const std::vector<UIInputEvent>& inputEvents,
bool menuModalDuringFrame) {
if (!menuModalDuringFrame) {
return inputEvents;
}
std::vector<UIInputEvent> filtered = {};
for (const UIInputEvent& event : inputEvents) {
if (event.type == UIInputEventType::FocusGained ||
event.type == UIInputEventType::FocusLost) {
filtered.push_back(event);
}
}
return filtered;
}
} // namespace
UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest(
const UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
const UIEditorShellInteractionModel& model,
const Widgets::UIEditorDockHostState& dockHostState,
const UIEditorShellInteractionState& state,
const UIEditorShellInteractionMetrics& metrics) {
return BuildRequest(
bounds,
panelRegistry,
workspace,
session,
model,
dockHostState,
state,
metrics).request;
}
UIEditorShellInteractionFrame UpdateUIEditorShellInteraction(
UIEditorShellInteractionState& state,
const UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
const UIEditorShellInteractionModel& model,
const std::vector<UIInputEvent>& inputEvents,
const Widgets::UIEditorDockHostState& dockHostState,
const UIEditorShellInteractionMetrics& metrics) {
UIEditorShellInteractionResult interactionResult = {};
bool menuModalDuringFrame = state.menuSession.HasOpenMenu();
BuildRequestOutput requestBuild = BuildRequest(
bounds,
panelRegistry,
workspace,
session,
model,
dockHostState,
state,
metrics);
UIEditorShellInteractionRequest request = std::move(requestBuild.request);
if (requestBuild.hadInvalidPopupState && state.menuSession.HasOpenMenu()) {
interactionResult.menuMutation = state.menuSession.CloseAll();
interactionResult.consumed = interactionResult.menuMutation.changed;
menuModalDuringFrame =
interactionResult.menuMutation.changed || state.menuSession.HasOpenMenu();
requestBuild = BuildRequest(
bounds,
panelRegistry,
workspace,
session,
model,
dockHostState,
state,
metrics);
request = std::move(requestBuild.request);
}
for (const UIInputEvent& event : inputEvents) {
UIEditorShellInteractionResult eventResult = {};
if (ShouldUsePointerPosition(event)) {
state.pointerPosition = event.position;
state.hasPointerPosition = true;
} else if (event.type == UIInputEventType::PointerLeave) {
state.hasPointerPosition = false;
}
const RequestHit hit =
HitTestRequest(request, state.pointerPosition, state.hasPointerPosition);
switch (event.type) {
case UIInputEventType::FocusGained:
state.focused = true;
break;
case UIInputEventType::FocusLost:
state.focused = false;
if (state.menuSession.HasOpenMenu()) {
eventResult.menuMutation =
state.menuSession.DismissFromFocusLoss({});
eventResult.consumed = eventResult.menuMutation.changed;
}
break;
case UIInputEventType::PointerMove:
case UIInputEventType::PointerEnter:
if (state.menuSession.HasOpenMenu()) {
if (hit.menuButton != nullptr && hit.menuButton->enabled) {
eventResult.menuId = hit.menuButton->menuId;
if (!state.menuSession.IsMenuOpen(hit.menuButton->menuId)) {
eventResult.menuMutation =
state.menuSession.HoverMenuBarRoot(
hit.menuButton->menuId,
BuildRootPopupEntry(*hit.menuButton));
} else {
eventResult.menuMutation =
state.menuSession.DismissFromFocusLoss(hit.menuButton->path);
}
} else if (hit.popupItem != nullptr) {
eventResult.menuId = hit.popupItem->menuId;
eventResult.popupId = hit.popupItem->popupId;
eventResult.itemId = hit.popupItem->itemId;
if (hit.popupItem->hasSubmenu && hit.popupItem->enabled) {
eventResult.menuMutation =
state.menuSession.HoverSubmenu(
hit.popupItem->itemId,
BuildSubmenuPopupEntry(*hit.popupItem));
} else {
eventResult.menuMutation =
state.menuSession.DismissFromFocusLoss(hit.popupItem->path);
}
} else if (hit.popupRequest != nullptr) {
eventResult.menuId = hit.popupRequest->menuId;
eventResult.popupId = hit.popupRequest->popupId;
eventResult.menuMutation =
state.menuSession.DismissFromFocusLoss(
hit.popupRequest->overlayEntry.surfacePath);
}
if (eventResult.menuMutation.changed) {
eventResult.consumed = true;
}
}
break;
case UIInputEventType::PointerButtonDown:
if (event.pointerButton != ::XCEngine::UI::UIPointerButton::Left) {
break;
}
if (hit.menuButton != nullptr && hit.menuButton->enabled) {
state.focused = true;
eventResult.consumed = true;
eventResult.menuId = hit.menuButton->menuId;
if (state.menuSession.IsMenuOpen(hit.menuButton->menuId)) {
eventResult.menuMutation = state.menuSession.CloseAll();
} else {
eventResult.menuMutation =
state.menuSession.OpenMenuBarRoot(
hit.menuButton->menuId,
BuildRootPopupEntry(*hit.menuButton));
}
} else if (hit.popupItem != nullptr) {
state.focused = true;
eventResult.consumed = true;
eventResult.menuId = hit.popupItem->menuId;
eventResult.popupId = hit.popupItem->popupId;
eventResult.itemId = hit.popupItem->itemId;
if (hit.popupItem->hasSubmenu && hit.popupItem->enabled) {
eventResult.menuMutation =
state.menuSession.HoverSubmenu(
hit.popupItem->itemId,
BuildSubmenuPopupEntry(*hit.popupItem));
} else if (hit.popupItem->enabled) {
eventResult.commandTriggered = true;
eventResult.commandId = hit.popupItem->commandId;
eventResult.menuMutation = state.menuSession.CloseAll();
} else {
eventResult.menuMutation =
state.menuSession.DismissFromPointerDown(hit.popupItem->path);
}
} else if (hit.popupRequest != nullptr) {
eventResult.consumed = true;
eventResult.menuId = hit.popupRequest->menuId;
eventResult.popupId = hit.popupRequest->popupId;
eventResult.menuMutation =
state.menuSession.DismissFromPointerDown(
hit.popupRequest->overlayEntry.surfacePath);
} else if (state.menuSession.HasOpenMenu()) {
eventResult.consumed = true;
eventResult.menuMutation =
state.menuSession.DismissFromPointerDown(UIInputPath { kOutsidePointerPath });
}
break;
case UIInputEventType::KeyDown:
if (event.keyCode == static_cast<std::int32_t>(KeyCode::Escape) &&
state.menuSession.HasOpenMenu()) {
eventResult.consumed = true;
eventResult.menuMutation = state.menuSession.DismissFromEscape();
}
break;
default:
break;
}
if (HasMeaningfulInteractionResult(eventResult)) {
interactionResult = std::move(eventResult);
}
if (interactionResult.menuMutation.changed || state.menuSession.HasOpenMenu()) {
menuModalDuringFrame = true;
request = BuildRequest(
bounds,
panelRegistry,
workspace,
session,
model,
dockHostState,
state,
metrics).request;
}
}
const RequestHit finalHit =
HitTestRequest(request, state.pointerPosition, state.hasPointerPosition);
UpdateMenuBarVisualState(state, request, finalHit);
const UIEditorShellComposeModel shellModel =
BuildShellComposeModel(model, request.menuBarItems);
const std::vector<UIInputEvent> composeInputEvents =
FilterComposeInputEvents(inputEvents, menuModalDuringFrame);
UIEditorShellInteractionFrame frame = {};
frame.request = request;
frame.shellFrame = UpdateUIEditorShellCompose(
state.composeState,
bounds,
panelRegistry,
workspace,
session,
shellModel,
composeInputEvents,
dockHostState,
metrics.shellMetrics);
frame.popupFrames = BuildPopupFrames(
frame.request,
state,
finalHit.popupRequest != nullptr ? finalHit.popupRequest->popupId : std::string_view(),
finalHit.popupItem != nullptr ? finalHit.popupItem->itemId : std::string_view());
frame.result = interactionResult;
frame.openRootMenuId = std::string(state.menuSession.GetOpenRootMenuId());
frame.hoveredMenuId =
finalHit.menuButton != nullptr ? finalHit.menuButton->menuId : std::string();
frame.hoveredPopupId =
finalHit.popupRequest != nullptr ? finalHit.popupRequest->popupId : std::string();
frame.hoveredItemId =
finalHit.popupItem != nullptr ? finalHit.popupItem->itemId : std::string();
frame.focused = state.focused || state.menuSession.HasOpenMenu();
return frame;
}
void AppendUIEditorShellInteraction(
::XCEngine::UI::UIDrawList& drawList,
const UIEditorShellInteractionFrame& frame,
const UIEditorShellInteractionModel& model,
const UIEditorShellInteractionState& state,
const UIEditorShellInteractionPalette& palette,
const UIEditorShellInteractionMetrics& metrics) {
const UIEditorShellComposeModel shellModel =
BuildShellComposeModel(model, frame.request.menuBarItems);
AppendUIEditorShellCompose(
drawList,
frame.shellFrame,
shellModel,
state.composeState,
palette.shellPalette,
metrics.shellMetrics);
const std::size_t popupCount =
(std::min)(frame.request.popupRequests.size(), frame.popupFrames.size());
for (std::size_t index = 0; index < popupCount; ++index) {
const UIEditorShellInteractionPopupRequest& popupRequest =
frame.request.popupRequests[index];
const UIEditorShellInteractionPopupFrame& popupFrame =
frame.popupFrames[index];
AppendUIEditorMenuPopupBackground(
drawList,
popupRequest.layout,
popupRequest.widgetItems,
popupFrame.popupState,
palette.popupPalette,
metrics.popupMetrics);
AppendUIEditorMenuPopupForeground(
drawList,
popupRequest.layout,
popupRequest.widgetItems,
popupFrame.popupState,
palette.popupPalette,
metrics.popupMetrics);
}
}
} // namespace XCEngine::UI::Editor