From 35cd535b4cc4065209805453a62c02d6fa03fd67 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 11:44:16 +0800 Subject: [PATCH] Internalize shell command dispatch handling --- .../XCEditor/Core/UIEditorShellInteraction.h | 12 ++- .../src/Core/UIEditorShellInteraction.cpp | 15 +++- .../shell/editor_shell_interaction/main.cpp | 40 +++++++-- .../unit/test_ui_editor_shell_interaction.cpp | 86 +++++++++++++++++++ 4 files changed, 144 insertions(+), 9 deletions(-) diff --git a/new_editor/include/XCEditor/Core/UIEditorShellInteraction.h b/new_editor/include/XCEditor/Core/UIEditorShellInteraction.h index 73649bab..b3a57615 100644 --- a/new_editor/include/XCEditor/Core/UIEditorShellInteraction.h +++ b/new_editor/include/XCEditor/Core/UIEditorShellInteraction.h @@ -39,6 +39,10 @@ struct UIEditorShellInteractionPalette { Widgets::UIEditorMenuPopupPalette popupPalette = {}; }; +struct UIEditorShellInteractionServices { + const UIEditorCommandDispatcher* commandDispatcher = nullptr; +}; + struct UIEditorShellInteractionMenuButtonRequest { std::string menuId = {}; std::string label = {}; @@ -84,10 +88,13 @@ struct UIEditorShellInteractionRequest { struct UIEditorShellInteractionResult { bool consumed = false; + bool menuModal = false; + bool workspaceInputSuppressed = false; bool requestPointerCapture = false; bool releasePointerCapture = false; bool viewportInteractionChanged = false; bool commandTriggered = false; + bool commandDispatched = false; std::string menuId = {}; std::string popupId = {}; std::string itemId = {}; @@ -95,6 +102,7 @@ struct UIEditorShellInteractionResult { std::string viewportPanelId = {}; UIEditorViewportInputBridgeFrame viewportInputFrame = {}; UIEditorMenuSessionMutationResult menuMutation = {}; + UIEditorCommandDispatchResult commandDispatchResult = {}; UIEditorWorkspaceInteractionResult workspaceResult = {}; }; @@ -121,7 +129,8 @@ UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( const UIEditorWorkspaceController& controller, const UIEditorShellInteractionModel& model, const UIEditorShellInteractionState& state = {}, - const UIEditorShellInteractionMetrics& metrics = {}); + const UIEditorShellInteractionMetrics& metrics = {}, + const UIEditorShellInteractionServices& services = {}); UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( UIEditorShellInteractionState& state, @@ -129,6 +138,7 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( const ::XCEngine::UI::UIRect& bounds, const UIEditorShellInteractionModel& model, const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const UIEditorShellInteractionServices& services = {}, const UIEditorShellInteractionMetrics& metrics = {}); void AppendUIEditorShellInteraction( diff --git a/new_editor/src/Core/UIEditorShellInteraction.cpp b/new_editor/src/Core/UIEditorShellInteraction.cpp index 501dc233..eed82475 100644 --- a/new_editor/src/Core/UIEditorShellInteraction.cpp +++ b/new_editor/src/Core/UIEditorShellInteraction.cpp @@ -220,6 +220,7 @@ bool HasMeaningfulInteractionResult( result.requestPointerCapture || result.releasePointerCapture || result.commandTriggered || + result.commandDispatched || result.menuMutation.changed || result.workspaceResult.consumed || !result.menuId.empty() || @@ -469,7 +470,9 @@ UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( const UIEditorWorkspaceController& controller, const UIEditorShellInteractionModel& model, const UIEditorShellInteractionState& state, - const UIEditorShellInteractionMetrics& metrics) { + const UIEditorShellInteractionMetrics& metrics, + const UIEditorShellInteractionServices& services) { + (void)services; return BuildRequest( bounds, controller, @@ -484,6 +487,7 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( const UIRect& bounds, const UIEditorShellInteractionModel& model, const std::vector& inputEvents, + const UIEditorShellInteractionServices& services, const UIEditorShellInteractionMetrics& metrics) { UIEditorShellInteractionResult interactionResult = {}; bool menuModalDuringFrame = state.menuSession.HasOpenMenu(); @@ -610,6 +614,13 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( } else if (hit.popupItem->enabled) { eventResult.commandTriggered = true; eventResult.commandId = hit.popupItem->commandId; + if (services.commandDispatcher != nullptr) { + eventResult.commandDispatchResult = + services.commandDispatcher->Dispatch( + eventResult.commandId, + controller); + eventResult.commandDispatched = true; + } eventResult.menuMutation = state.menuSession.CloseAll(); } else { eventResult.menuMutation = @@ -700,6 +711,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( finalHit.popupItem != nullptr ? finalHit.popupItem->itemId : std::string(); frame.focused = state.focused || state.menuSession.HasOpenMenu(); interactionResult.workspaceResult = frame.workspaceInteractionFrame.result; + interactionResult.menuModal = state.menuSession.HasOpenMenu(); + interactionResult.workspaceInputSuppressed = menuModalDuringFrame; interactionResult.requestPointerCapture = interactionResult.workspaceResult.requestPointerCapture; interactionResult.releasePointerCapture = diff --git a/tests/UI/Editor/integration/shell/editor_shell_interaction/main.cpp b/tests/UI/Editor/integration/shell/editor_shell_interaction/main.cpp index f7ea1a87..9d9fd4b0 100644 --- a/tests/UI/Editor/integration/shell/editor_shell_interaction/main.cpp +++ b/tests/UI/Editor/integration/shell/editor_shell_interaction/main.cpp @@ -63,6 +63,7 @@ using XCEngine::UI::Editor::UIEditorPanelRegistry; using XCEngine::UI::Editor::UIEditorShellInteractionFrame; using XCEngine::UI::Editor::UIEditorShellInteractionModel; using XCEngine::UI::Editor::UIEditorShellInteractionResult; +using XCEngine::UI::Editor::UIEditorShellInteractionServices; using XCEngine::UI::Editor::UIEditorShellInteractionState; using XCEngine::UI::Editor::UIEditorViewportInputBridgeFrame; using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind; @@ -125,6 +126,7 @@ bool HasMeaningfulInteractionResult(const UIEditorShellInteractionResult& result result.requestPointerCapture || result.releasePointerCapture || result.commandTriggered || + result.commandDispatched || result.menuMutation.changed || result.workspaceResult.consumed || !result.menuId.empty() || @@ -781,10 +783,15 @@ void ScenarioApp::SetInteractionResult(const UIEditorShellInteractionResult& res return; } + if (result.commandDispatched) { + SetDispatchResult(result.commandDispatchResult); + return; + } + if (result.commandTriggered) { - const UIEditorCommandDispatchResult dispatchResult = - m_commandDispatcher.Dispatch(result.commandId, m_controller); - SetDispatchResult(dispatchResult); + m_lastStatus = "Triggered"; + m_lastMessage = "命令已命中,但当前场景没有把 dispatcher 接到 root shell。"; + m_lastColor = kWarning; return; } @@ -866,24 +873,28 @@ void ScenarioApp::SetDispatchResult(const UIEditorCommandDispatchResult& result) void ScenarioApp::RenderFrame() { UpdateLayout(); m_cachedModel = BuildInteractionModel(); + UIEditorShellInteractionServices services = {}; + services.commandDispatcher = &m_commandDispatcher; m_cachedFrame = UpdateUIEditorShellInteraction( m_interactionState, m_controller, m_shellRect, m_cachedModel, - m_pendingInputEvents); + m_pendingInputEvents, + services); m_pendingInputEvents.clear(); ApplyHostCaptureRequests(m_cachedFrame.result); SetInteractionResult(m_cachedFrame.result); - if (m_cachedFrame.result.commandTriggered) { + if (m_cachedFrame.result.commandDispatched) { m_cachedModel = BuildInteractionModel(); m_cachedFrame = UpdateUIEditorShellInteraction( m_interactionState, m_controller, m_shellRect, m_cachedModel, - {}); + {}, + services); } const auto* viewportFrame = @@ -905,7 +916,7 @@ void ScenarioApp::RenderFrame() { drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 92.0f), "2. 验证 hover 子菜单时,child popup 是否直接展开,不需要额外点击。", kTextPrimary, 12.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 114.0f), "3. 验证 outside pointer down / Esc / focus loss 是否能正确收起 popup chain。", kTextPrimary, 12.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 136.0f), "4. 验证菜单打开时会屏蔽 workspace 输入;菜单关闭后,workspace 交互立即恢复。", kTextPrimary, 12.0f); - drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 158.0f), "5. 验证 root shell 会继续透传 viewport / splitter 的 capture 请求,不接旧 editor 业务。", kTextPrimary, 12.0f); + drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 158.0f), "5. 验证菜单命令会在 root shell 内直接 dispatch,宿主不再二次派发。", kTextPrimary, 12.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 182.0f), "建议操作:点击 File,hover `Workspace Tools`,点预览外空白处,再点 `Document` 或拖 splitter。", kTextWeak, 11.0f); DrawCard(drawList, m_controlsRect, "操作", "只保留这个场景必要的控制。"); @@ -926,6 +937,21 @@ void ScenarioApp::RenderFrame() { addStateLine("Selected Presentation", selectedPresentation, kTextPrimary, 11.0f); addStateLine("Active Panel", m_controller.GetWorkspace().activePanelId.empty() ? "(none)" : m_controller.GetWorkspace().activePanelId, kTextPrimary, 11.0f); addStateLine("Focused", FormatBool(m_cachedFrame.focused), m_cachedFrame.focused ? kSuccess : kTextMuted); + addStateLine("Menu Modal", FormatBool(m_cachedFrame.result.menuModal), m_cachedFrame.result.menuModal ? kSuccess : kTextMuted, 11.0f); + addStateLine( + "Workspace Suppressed", + FormatBool(m_cachedFrame.result.workspaceInputSuppressed), + m_cachedFrame.result.workspaceInputSuppressed ? kWarning : kTextMuted, + 11.0f); + addStateLine( + "Command Dispatch", + m_cachedFrame.result.commandDispatched + ? std::string(GetUIEditorCommandDispatchStatusName(m_cachedFrame.result.commandDispatchResult.status)) + : std::string("(none)"), + m_cachedFrame.result.commandDispatched && m_cachedFrame.result.commandDispatchResult.commandExecuted + ? kSuccess + : kTextMuted, + 11.0f); addStateLine("Result", m_lastStatus, m_lastColor); drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY + 4.0f), m_lastMessage, kTextMuted, 11.0f); stateY += 34.0f; diff --git a/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp index 3708a02c..8c146c7d 100644 --- a/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp @@ -19,6 +19,10 @@ using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::ResolveUIEditorShellInteractionRequest; +using XCEngine::UI::Editor::UIEditorCommandDispatchStatus; +using XCEngine::UI::Editor::UIEditorCommandDispatcher; +using XCEngine::UI::Editor::UIEditorCommandPanelSource; +using XCEngine::UI::Editor::UIEditorCommandRegistry; using XCEngine::UI::Editor::UIEditorMenuItemKind; using XCEngine::UI::Editor::UIEditorPanelPresentationKind; using XCEngine::UI::Editor::UIEditorPanelRegistry; @@ -29,8 +33,11 @@ using XCEngine::UI::Editor::UIEditorShellInteractionFrame; using XCEngine::UI::Editor::UIEditorShellInteractionMenuButtonRequest; using XCEngine::UI::Editor::UIEditorShellInteractionModel; using XCEngine::UI::Editor::UIEditorShellInteractionPopupItemRequest; +using XCEngine::UI::Editor::UIEditorShellInteractionServices; using XCEngine::UI::Editor::UIEditorShellInteractionState; using XCEngine::UI::Editor::UIEditorWorkspaceController; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus; using XCEngine::UI::Editor::UIEditorWorkspaceModel; using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel; using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; @@ -66,6 +73,33 @@ UIEditorWorkspaceModel BuildWorkspace() { return workspace; } +UIEditorCommandRegistry BuildCommandRegistry() { + UIEditorCommandRegistry registry = {}; + registry.commands = { + { + "workspace.show_inspector", + "Show Inspector", + { UIEditorWorkspaceCommandKind::ShowPanel, UIEditorCommandPanelSource::FixedPanelId, "inspector" } + }, + { + "workspace.focus_inspector", + "Focus Inspector", + { UIEditorWorkspaceCommandKind::ActivatePanel, UIEditorCommandPanelSource::FixedPanelId, "inspector" } + }, + { + "workspace.reset_layout", + "Reset Layout", + { UIEditorWorkspaceCommandKind::ResetWorkspace, UIEditorCommandPanelSource::None, {} } + }, + { + "workspace.close", + "Close", + { UIEditorWorkspaceCommandKind::HidePanel, UIEditorCommandPanelSource::ActivePanel, {} } + } + }; + return registry; +} + UIEditorResolvedMenuModel BuildResolvedMenuModel() { UIEditorResolvedMenuModel model = {}; @@ -334,11 +368,63 @@ TEST(UIEditorShellInteractionTest, ClickCommandReturnsDispatchHookAndClosesMenu) { MakeLeftPointerDown(RectCenter(commandItem->rect)) }); EXPECT_TRUE(frame.result.commandTriggered); + EXPECT_FALSE(frame.result.commandDispatched); EXPECT_EQ(frame.result.commandId, "workspace.close"); EXPECT_FALSE(state.menuSession.HasOpenMenu()); EXPECT_TRUE(frame.request.popupRequests.empty()); } +TEST(UIEditorShellInteractionTest, ClickCommandDispatchesInsideShellAndUpdatesWorkspaceSameFrame) { + auto controller = BuildController(); + const auto model = BuildInteractionModel(); + const UIEditorCommandDispatcher dispatcher(BuildCommandRegistry()); + UIEditorShellInteractionServices services = {}; + services.commandDispatcher = &dispatcher; + + UIEditorShellInteractionState state = {}; + auto frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + {}, + services); + const auto* windowButton = FindMenuButton(frame, "window"); + ASSERT_NE(windowButton, nullptr); + + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakeLeftPointerDown(RectCenter(windowButton->rect)) }, + services); + const auto* commandItem = FindPopupItem(frame, "window-focus-inspector"); + ASSERT_NE(commandItem, nullptr); + + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakeLeftPointerDown(RectCenter(commandItem->rect)) }, + services); + + EXPECT_TRUE(frame.result.commandTriggered); + EXPECT_TRUE(frame.result.commandDispatched); + EXPECT_EQ(frame.result.commandId, "workspace.focus_inspector"); + EXPECT_EQ( + frame.result.commandDispatchResult.status, + UIEditorCommandDispatchStatus::Dispatched); + EXPECT_TRUE(frame.result.commandDispatchResult.commandExecuted); + EXPECT_EQ( + frame.result.commandDispatchResult.commandResult.status, + UIEditorWorkspaceCommandStatus::Changed); + EXPECT_EQ(controller.GetWorkspace().activePanelId, "inspector"); + EXPECT_FALSE(state.menuSession.HasOpenMenu()); + EXPECT_TRUE(frame.request.popupRequests.empty()); +} + TEST(UIEditorShellInteractionTest, PointerDownOutsideDismissesWholeMenuChain) { auto controller = BuildController(); const auto model = BuildInteractionModel();