From 864438c508d193b831daddafb1d1443abf232ff6 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 11:31:13 +0800 Subject: [PATCH] resources: formalize internal shader ir --- engine/CMakeLists.txt | 1 + .../Shader/ShaderAuthoringParser.cpp | 32 +-- .../Resources/Shader/ShaderAuthoringParser.h | 55 +---- engine/src/Resources/Shader/ShaderIR.h | 58 +++++ engine/src/Resources/Shader/ShaderLoader.cpp | 54 ++--- .../XCEditor/Core/UIEditorShellInteraction.h | 21 +- .../src/Core/UIEditorShellInteraction.cpp | 102 ++++----- tests/UI/Editor/integration/README.md | 4 +- .../captures/.gitkeep | 1 - .../shell/editor_shell_interaction/main.cpp | 155 ++++++++++++- .../unit/test_ui_editor_shell_interaction.cpp | 207 ++++++++++-------- 11 files changed, 431 insertions(+), 259 deletions(-) create mode 100644 engine/src/Resources/Shader/ShaderIR.h diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index b68d62a8..654ed036 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -387,6 +387,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Material/MaterialLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/Shader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderLoader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderIR.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderSourceUtils.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderSourceUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderAuthoringParser.h diff --git a/engine/src/Resources/Shader/ShaderAuthoringParser.cpp b/engine/src/Resources/Shader/ShaderAuthoringParser.cpp index 7a372053..cde56734 100644 --- a/engine/src/Resources/Shader/ShaderAuthoringParser.cpp +++ b/engine/src/Resources/Shader/ShaderAuthoringParser.cpp @@ -433,10 +433,10 @@ bool TryParseUnityStyleBlendDirective( } void SetOrReplaceAuthoringTag( - std::vector& tags, + std::vector& tags, const Containers::String& name, const Containers::String& value) { - for (AuthoringTagEntry& tag : tags) { + for (ShaderTagIR& tag : tags) { if (tag.name == name) { tag.value = value; return; @@ -926,7 +926,7 @@ bool TryTokenizeQuotedArguments(const std::string& line, std::vector& outTags) { + std::vector& outTags) { const size_t openBrace = line.find('{'); const size_t closeBrace = line.rfind('}'); if (openBrace == std::string::npos || @@ -1117,7 +1117,7 @@ bool TryParseAuthoringResourceLine( bool ParseLegacyBackendSplitShaderAuthoring( const Containers::String& path, const std::string& sourceText, - AuthoringShaderDesc& outDesc, + ShaderIR& outDesc, Containers::String* outError) { (void)path; outDesc = {}; @@ -1147,8 +1147,8 @@ bool ParseLegacyBackendSplitShaderAuthoring( std::vector blockStack; BlockKind pendingBlock = BlockKind::None; - AuthoringSubShaderEntry* currentSubShader = nullptr; - AuthoringPassEntry* currentPass = nullptr; + ShaderSubShaderIR* currentSubShader = nullptr; + ShaderPassIR* currentPass = nullptr; bool inProgram = false; auto currentBlock = [&blockStack]() -> BlockKind { @@ -1183,7 +1183,7 @@ bool ParseLegacyBackendSplitShaderAuthoring( continue; } if (pragmaTokens.size() >= 6u && pragmaTokens[1] == "backend") { - AuthoringBackendVariantEntry backendVariant = {}; + ShaderBackendVariantIR backendVariant = {}; if (!TryParseShaderBackend(pragmaTokens[2].c_str(), backendVariant.backend)) { return fail("invalid backend pragma backend name", humanLine); } @@ -1310,7 +1310,7 @@ bool ParseLegacyBackendSplitShaderAuthoring( } if (StartsWithKeyword(line, "Tags")) { - std::vector parsedTags; + std::vector parsedTags; if (!TryParseInlineTagAssignments(line, parsedTags)) { return fail("Tags block must use inline key/value pairs", humanLine); } @@ -1375,12 +1375,12 @@ bool ParseLegacyBackendSplitShaderAuthoring( return fail("shader does not declare any SubShader blocks", 0); } - for (const AuthoringSubShaderEntry& subShader : outDesc.subShaders) { + for (const ShaderSubShaderIR& subShader : outDesc.subShaders) { if (subShader.passes.empty()) { continue; } - for (const AuthoringPassEntry& pass : subShader.passes) { + for (const ShaderPassIR& pass : subShader.passes) { if (pass.name.Empty()) { return fail("a Pass is missing a Name directive", 0); } @@ -1396,7 +1396,7 @@ bool ParseLegacyBackendSplitShaderAuthoring( bool ParseUnityStyleSingleSourceShaderAuthoring( const Containers::String& path, const std::string& sourceText, - AuthoringShaderDesc& outDesc, + ShaderIR& outDesc, Containers::String* outError) { (void)path; outDesc = {}; @@ -1435,8 +1435,8 @@ bool ParseUnityStyleSingleSourceShaderAuthoring( size_t nextExtractedBlock = 0; std::vector blockStack; BlockKind pendingBlock = BlockKind::None; - AuthoringSubShaderEntry* currentSubShader = nullptr; - AuthoringPassEntry* currentPass = nullptr; + ShaderSubShaderIR* currentSubShader = nullptr; + ShaderPassIR* currentPass = nullptr; bool inProgramBlock = false; bool inSharedIncludeBlock = false; @@ -1611,7 +1611,7 @@ bool ParseUnityStyleSingleSourceShaderAuthoring( } if (StartsWithKeyword(line, "Tags")) { - std::vector parsedTags; + std::vector parsedTags; if (!TryParseInlineTagAssignments(line, parsedTags)) { return fail("Tags block must use inline key/value pairs", humanLine); } @@ -1886,12 +1886,12 @@ bool ParseUnityStyleSingleSourceShaderAuthoring( return fail("shader does not declare any SubShader blocks", 0); } - for (AuthoringSubShaderEntry& subShader : outDesc.subShaders) { + for (ShaderSubShaderIR& subShader : outDesc.subShaders) { if (subShader.passes.empty()) { continue; } - for (AuthoringPassEntry& pass : subShader.passes) { + for (ShaderPassIR& pass : subShader.passes) { if (pass.name.Empty()) { return fail("a Pass is missing a Name directive", 0); } diff --git a/engine/src/Resources/Shader/ShaderAuthoringParser.h b/engine/src/Resources/Shader/ShaderAuthoringParser.h index 7b36c3b9..6933bdbd 100644 --- a/engine/src/Resources/Shader/ShaderAuthoringParser.h +++ b/engine/src/Resources/Shader/ShaderAuthoringParser.h @@ -1,8 +1,6 @@ #pragma once -#include -#include -#include +#include "ShaderIR.h" #include #include @@ -17,51 +15,6 @@ enum class ShaderAuthoringStyle { UnityStyleSingleSource }; -struct AuthoringTagEntry { - Containers::String name; - Containers::String value; -}; - -struct AuthoringBackendVariantEntry { - ShaderBackend backend = ShaderBackend::Generic; - ShaderLanguage language = ShaderLanguage::GLSL; - Containers::String vertexSourcePath; - Containers::String fragmentSourcePath; - Containers::String vertexProfile; - Containers::String fragmentProfile; -}; - -struct AuthoringPassEntry { - Containers::String name; - bool hasFixedFunctionState = false; - MaterialRenderState fixedFunctionState = {}; - std::vector tags; - Containers::Array resources; - Containers::Array keywordDeclarations; - Containers::String vertexEntryPoint; - Containers::String fragmentEntryPoint; - Containers::String sharedProgramSource; - Containers::String programSource; - Containers::String targetProfile; - std::vector backendVariants; -}; - -struct AuthoringSubShaderEntry { - bool hasFixedFunctionState = false; - MaterialRenderState fixedFunctionState = {}; - std::vector tags; - Containers::String sharedProgramSource; - std::vector passes; -}; - -struct AuthoringShaderDesc { - Containers::String name; - Containers::String fallback; - Containers::String sharedProgramSource; - Containers::Array properties; - std::vector subShaders; -}; - void AppendAuthoringSourceBlock( Containers::String& target, const Containers::String& sourceBlock); @@ -86,14 +39,14 @@ ShaderAuthoringStyle DetectShaderAuthoringStyle(const std::string& sourceText); bool ParseLegacyBackendSplitShaderAuthoring( const Containers::String& path, const std::string& sourceText, - AuthoringShaderDesc& outDesc, + ShaderIR& outDesc, Containers::String* outError); bool ParseUnityStyleSingleSourceShaderAuthoring( const Containers::String& path, const std::string& sourceText, - AuthoringShaderDesc& outDesc, + ShaderIR& outDesc, Containers::String* outError); } // namespace Resources -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/engine/src/Resources/Shader/ShaderIR.h b/engine/src/Resources/Shader/ShaderIR.h new file mode 100644 index 00000000..d563753b --- /dev/null +++ b/engine/src/Resources/Shader/ShaderIR.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#include + +namespace XCEngine { +namespace Resources { + +struct ShaderTagIR { + Containers::String name; + Containers::String value; +}; + +struct ShaderBackendVariantIR { + ShaderBackend backend = ShaderBackend::Generic; + ShaderLanguage language = ShaderLanguage::GLSL; + Containers::String vertexSourcePath; + Containers::String fragmentSourcePath; + Containers::String vertexProfile; + Containers::String fragmentProfile; +}; + +struct ShaderPassIR { + Containers::String name; + bool hasFixedFunctionState = false; + MaterialRenderState fixedFunctionState = {}; + std::vector tags; + Containers::Array resources; + Containers::Array keywordDeclarations; + Containers::String vertexEntryPoint; + Containers::String fragmentEntryPoint; + Containers::String sharedProgramSource; + Containers::String programSource; + Containers::String targetProfile; + std::vector backendVariants; +}; + +struct ShaderSubShaderIR { + bool hasFixedFunctionState = false; + MaterialRenderState fixedFunctionState = {}; + std::vector tags; + Containers::String sharedProgramSource; + std::vector passes; +}; + +struct ShaderIR { + Containers::String name; + Containers::String fallback; + Containers::String sharedProgramSource; + Containers::Array properties; + std::vector subShaders; +}; + +} // namespace Resources +} // namespace XCEngine diff --git a/engine/src/Resources/Shader/ShaderLoader.cpp b/engine/src/Resources/Shader/ShaderLoader.cpp index 194601ef..cd72a0b6 100644 --- a/engine/src/Resources/Shader/ShaderLoader.cpp +++ b/engine/src/Resources/Shader/ShaderLoader.cpp @@ -395,33 +395,33 @@ bool ReadTextFile(const Containers::String& path, Containers::String& outText) { size_t CalculateShaderMemorySize(const Shader& shader); -LoadResult BuildShaderFromAuthoringDesc( +LoadResult BuildShaderFromIR( const Containers::String& path, - const AuthoringShaderDesc& authoringDesc) { + const ShaderIR& shaderIR) { auto shader = std::make_unique(); IResource::ConstructParams params; params.path = path; params.guid = ResourceGUID::Generate(path); - params.name = authoringDesc.name; + params.name = shaderIR.name; shader->Initialize(params); - shader->SetFallback(authoringDesc.fallback); + shader->SetFallback(shaderIR.fallback); - for (const ShaderPropertyDesc& property : authoringDesc.properties) { + for (const ShaderPropertyDesc& property : shaderIR.properties) { shader->AddProperty(property); } - for (const AuthoringSubShaderEntry& subShader : authoringDesc.subShaders) { - for (const AuthoringPassEntry& pass : subShader.passes) { + for (const ShaderSubShaderIR& subShader : shaderIR.subShaders) { + for (const ShaderPassIR& pass : subShader.passes) { ShaderPass shaderPass = {}; shaderPass.name = pass.name; shaderPass.hasFixedFunctionState = pass.hasFixedFunctionState; shaderPass.fixedFunctionState = pass.fixedFunctionState; shader->AddPass(shaderPass); - for (const AuthoringTagEntry& subShaderTag : subShader.tags) { + for (const ShaderTagIR& subShaderTag : subShader.tags) { shader->SetPassTag(pass.name, subShaderTag.name, subShaderTag.value); } - for (const AuthoringTagEntry& passTag : pass.tags) { + for (const ShaderTagIR& passTag : pass.tags) { shader->SetPassTag(pass.name, passTag.name, passTag.value); } @@ -435,7 +435,7 @@ LoadResult BuildShaderFromAuthoringDesc( if (!pass.backendVariants.empty()) { const std::vector keywordSets = BuildShaderKeywordVariantSets(pass.keywordDeclarations); - for (const AuthoringBackendVariantEntry& backendVariant : pass.backendVariants) { + for (const ShaderBackendVariantIR& backendVariant : pass.backendVariants) { Containers::String vertexSourceCode; ShaderStageVariant vertexVariant = {}; vertexVariant.stage = ShaderType::Vertex; @@ -486,7 +486,7 @@ LoadResult BuildShaderFromAuthoringDesc( } } else if (!pass.programSource.Empty()) { Containers::String combinedSource; - AppendAuthoringSourceBlock(combinedSource, authoringDesc.sharedProgramSource); + AppendAuthoringSourceBlock(combinedSource, shaderIR.sharedProgramSource); AppendAuthoringSourceBlock(combinedSource, subShader.sharedProgramSource); AppendAuthoringSourceBlock(combinedSource, pass.sharedProgramSource); AppendAuthoringSourceBlock(combinedSource, pass.programSource); @@ -549,16 +549,16 @@ bool CollectLegacyBackendSplitShaderDependencyPaths( Containers::Array& outDependencies) { outDependencies.Clear(); - AuthoringShaderDesc authoringDesc = {}; + ShaderIR shaderIR = {}; Containers::String parseError; - if (!ParseLegacyBackendSplitShaderAuthoring(path, sourceText, authoringDesc, &parseError)) { + if (!ParseLegacyBackendSplitShaderAuthoring(path, sourceText, shaderIR, &parseError)) { return false; } std::unordered_set seenPaths; - for (const AuthoringSubShaderEntry& subShader : authoringDesc.subShaders) { - for (const AuthoringPassEntry& pass : subShader.passes) { - for (const AuthoringBackendVariantEntry& backendVariant : pass.backendVariants) { + for (const ShaderSubShaderIR& subShader : shaderIR.subShaders) { + for (const ShaderPassIR& pass : subShader.passes) { + for (const ShaderBackendVariantIR& backendVariant : pass.backendVariants) { const Containers::String resolvedVertexPath = ResolveShaderDependencyPath(backendVariant.vertexSourcePath, path); const std::string vertexKey = ToStdString(resolvedVertexPath); @@ -585,17 +585,17 @@ bool CollectUnityStyleSingleSourceShaderDependencyPaths( Containers::Array& outDependencies) { outDependencies.Clear(); - AuthoringShaderDesc authoringDesc = {}; + ShaderIR shaderIR = {}; Containers::String parseError; - if (!ParseUnityStyleSingleSourceShaderAuthoring(path, sourceText, authoringDesc, &parseError)) { + if (!ParseUnityStyleSingleSourceShaderAuthoring(path, sourceText, shaderIR, &parseError)) { return false; } std::unordered_set seenPaths; - CollectQuotedIncludeDependencyPaths(path, authoringDesc.sharedProgramSource, seenPaths, outDependencies); - for (const AuthoringSubShaderEntry& subShader : authoringDesc.subShaders) { + CollectQuotedIncludeDependencyPaths(path, shaderIR.sharedProgramSource, seenPaths, outDependencies); + for (const ShaderSubShaderIR& subShader : shaderIR.subShaders) { CollectQuotedIncludeDependencyPaths(path, subShader.sharedProgramSource, seenPaths, outDependencies); - for (const AuthoringPassEntry& pass : subShader.passes) { + for (const ShaderPassIR& pass : subShader.passes) { CollectQuotedIncludeDependencyPaths(path, pass.sharedProgramSource, seenPaths, outDependencies); CollectQuotedIncludeDependencyPaths(path, pass.programSource, seenPaths, outDependencies); } @@ -607,25 +607,25 @@ bool CollectUnityStyleSingleSourceShaderDependencyPaths( LoadResult LoadLegacyBackendSplitShaderAuthoring( const Containers::String& path, const std::string& sourceText) { - AuthoringShaderDesc authoringDesc = {}; + ShaderIR shaderIR = {}; Containers::String parseError; - if (!ParseLegacyBackendSplitShaderAuthoring(path, sourceText, authoringDesc, &parseError)) { + if (!ParseLegacyBackendSplitShaderAuthoring(path, sourceText, shaderIR, &parseError)) { return LoadResult(parseError); } - return BuildShaderFromAuthoringDesc(path, authoringDesc); + return BuildShaderFromIR(path, shaderIR); } LoadResult LoadUnityStyleSingleSourceShaderAuthoring( const Containers::String& path, const std::string& sourceText) { - AuthoringShaderDesc authoringDesc = {}; + ShaderIR shaderIR = {}; Containers::String parseError; - if (!ParseUnityStyleSingleSourceShaderAuthoring(path, sourceText, authoringDesc, &parseError)) { + if (!ParseUnityStyleSingleSourceShaderAuthoring(path, sourceText, shaderIR, &parseError)) { return LoadResult(parseError); } - return BuildShaderFromAuthoringDesc(path, authoringDesc); + return BuildShaderFromIR(path, shaderIR); } template diff --git a/new_editor/include/XCEditor/Core/UIEditorShellInteraction.h b/new_editor/include/XCEditor/Core/UIEditorShellInteraction.h index 27366ac2..73649bab 100644 --- a/new_editor/include/XCEditor/Core/UIEditorShellInteraction.h +++ b/new_editor/include/XCEditor/Core/UIEditorShellInteraction.h @@ -3,7 +3,8 @@ #include #include #include -#include +#include +#include #include #include @@ -22,6 +23,7 @@ struct UIEditorShellInteractionModel { struct UIEditorShellInteractionState { UIEditorShellComposeState composeState = {}; UIEditorMenuSession menuSession = {}; + UIEditorWorkspaceInteractionState workspaceInteractionState = {}; ::XCEngine::UI::UIPoint pointerPosition = {}; bool focused = false; bool hasPointerPosition = false; @@ -82,12 +84,18 @@ struct UIEditorShellInteractionRequest { struct UIEditorShellInteractionResult { bool consumed = false; + bool requestPointerCapture = false; + bool releasePointerCapture = false; + bool viewportInteractionChanged = false; bool commandTriggered = false; std::string menuId = {}; std::string popupId = {}; std::string itemId = {}; std::string commandId = {}; + std::string viewportPanelId = {}; + UIEditorViewportInputBridgeFrame viewportInputFrame = {}; UIEditorMenuSessionMutationResult menuMutation = {}; + UIEditorWorkspaceInteractionResult workspaceResult = {}; }; struct UIEditorShellInteractionPopupFrame { @@ -98,6 +106,7 @@ struct UIEditorShellInteractionPopupFrame { struct UIEditorShellInteractionFrame { UIEditorShellInteractionRequest request = {}; UIEditorShellComposeFrame shellFrame = {}; + UIEditorWorkspaceInteractionFrame workspaceInteractionFrame = {}; std::vector popupFrames = {}; UIEditorShellInteractionResult result = {}; std::string openRootMenuId = {}; @@ -109,23 +118,17 @@ struct UIEditorShellInteractionFrame { UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( const ::XCEngine::UI::UIRect& bounds, - const UIEditorPanelRegistry& panelRegistry, - const UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, + const UIEditorWorkspaceController& controller, const UIEditorShellInteractionModel& model, - const Widgets::UIEditorDockHostState& dockHostState = {}, const UIEditorShellInteractionState& state = {}, const UIEditorShellInteractionMetrics& metrics = {}); UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( UIEditorShellInteractionState& state, + UIEditorWorkspaceController& controller, 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( diff --git a/new_editor/src/Core/UIEditorShellInteraction.cpp b/new_editor/src/Core/UIEditorShellInteraction.cpp index cf2324ba..501dc233 100644 --- a/new_editor/src/Core/UIEditorShellInteraction.cpp +++ b/new_editor/src/Core/UIEditorShellInteraction.cpp @@ -217,8 +217,11 @@ std::size_t FindPopupItemIndex( bool HasMeaningfulInteractionResult( const UIEditorShellInteractionResult& result) { return result.consumed || + result.requestPointerCapture || + result.releasePointerCapture || result.commandTriggered || result.menuMutation.changed || + result.workspaceResult.consumed || !result.menuId.empty() || !result.popupId.empty() || !result.itemId.empty() || @@ -227,11 +230,8 @@ bool HasMeaningfulInteractionResult( BuildRequestOutput BuildRequest( const UIRect& bounds, - const UIEditorPanelRegistry& panelRegistry, - const UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, + const UIEditorWorkspaceController& controller, const UIEditorShellInteractionModel& model, - const Widgets::UIEditorDockHostState& dockHostState, const UIEditorShellInteractionState& state, const UIEditorShellInteractionMetrics& metrics) { BuildRequestOutput output = {}; @@ -242,11 +242,11 @@ BuildRequestOutput BuildRequest( BuildShellComposeModel(model, request.menuBarItems); request.shellRequest = ResolveUIEditorShellComposeRequest( bounds, - panelRegistry, - workspace, - session, + controller.GetPanelRegistry(), + controller.GetWorkspace(), + controller.GetSession(), shellModel, - dockHostState, + state.workspaceInteractionState.dockHostInteractionState.dockHostState, state.composeState, metrics.shellMetrics); @@ -444,7 +444,7 @@ bool ShouldUsePointerPosition(const UIInputEvent& event) { } } -std::vector FilterComposeInputEvents( +std::vector FilterWorkspaceInputEvents( const std::vector& inputEvents, bool menuModalDuringFrame) { if (!menuModalDuringFrame) { @@ -466,44 +466,32 @@ std::vector FilterComposeInputEvents( UIEditorShellInteractionRequest ResolveUIEditorShellInteractionRequest( const UIRect& bounds, - const UIEditorPanelRegistry& panelRegistry, - const UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, + const UIEditorWorkspaceController& controller, const UIEditorShellInteractionModel& model, - const Widgets::UIEditorDockHostState& dockHostState, const UIEditorShellInteractionState& state, const UIEditorShellInteractionMetrics& metrics) { return BuildRequest( bounds, - panelRegistry, - workspace, - session, + controller, model, - dockHostState, state, metrics).request; } UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( UIEditorShellInteractionState& state, + UIEditorWorkspaceController& controller, const UIRect& bounds, - const UIEditorPanelRegistry& panelRegistry, - const UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, const UIEditorShellInteractionModel& model, const std::vector& inputEvents, - const Widgets::UIEditorDockHostState& dockHostState, const UIEditorShellInteractionMetrics& metrics) { UIEditorShellInteractionResult interactionResult = {}; bool menuModalDuringFrame = state.menuSession.HasOpenMenu(); BuildRequestOutput requestBuild = BuildRequest( bounds, - panelRegistry, - workspace, - session, + controller, model, - dockHostState, state, metrics); UIEditorShellInteractionRequest request = std::move(requestBuild.request); @@ -516,11 +504,8 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( requestBuild = BuildRequest( bounds, - panelRegistry, - workspace, - session, + controller, model, - dockHostState, state, metrics); request = std::move(requestBuild.request); @@ -664,43 +649,48 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( menuModalDuringFrame = true; request = BuildRequest( bounds, - panelRegistry, - workspace, - session, + controller, model, - dockHostState, state, metrics).request; } } + const std::vector workspaceInputEvents = + FilterWorkspaceInputEvents(inputEvents, menuModalDuringFrame); + UIEditorWorkspaceInteractionModel workspaceModel = {}; + workspaceModel.workspacePresentations = model.workspacePresentations; + UIEditorWorkspaceInteractionFrame workspaceInteractionFrame = + UpdateUIEditorWorkspaceInteraction( + state.workspaceInteractionState, + controller, + request.shellRequest.layout.workspaceRect, + workspaceModel, + workspaceInputEvents, + metrics.shellMetrics.dockHostMetrics); + state.composeState.workspaceState = state.workspaceInteractionState.composeState; + + request = BuildRequest( + bounds, + controller, + model, + 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 composeInputEvents = - FilterComposeInputEvents(inputEvents, menuModalDuringFrame); - UIEditorShellInteractionFrame frame = {}; frame.request = request; - frame.shellFrame = UpdateUIEditorShellCompose( - state.composeState, - bounds, - panelRegistry, - workspace, - session, - shellModel, - composeInputEvents, - dockHostState, - metrics.shellMetrics); + frame.shellFrame.layout = request.shellRequest.layout; + frame.shellFrame.workspaceFrame = workspaceInteractionFrame.composeFrame; + frame.workspaceInteractionFrame = std::move(workspaceInteractionFrame); 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(); @@ -709,6 +699,20 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( frame.hoveredItemId = finalHit.popupItem != nullptr ? finalHit.popupItem->itemId : std::string(); frame.focused = state.focused || state.menuSession.HasOpenMenu(); + interactionResult.workspaceResult = frame.workspaceInteractionFrame.result; + interactionResult.requestPointerCapture = + interactionResult.workspaceResult.requestPointerCapture; + interactionResult.releasePointerCapture = + interactionResult.workspaceResult.releasePointerCapture; + interactionResult.viewportInteractionChanged = + interactionResult.workspaceResult.viewportInteractionChanged; + interactionResult.viewportPanelId = + interactionResult.workspaceResult.viewportPanelId; + interactionResult.viewportInputFrame = + interactionResult.workspaceResult.viewportInputFrame; + interactionResult.consumed = + interactionResult.consumed || interactionResult.workspaceResult.consumed; + frame.result = std::move(interactionResult); return frame; } diff --git a/tests/UI/Editor/integration/README.md b/tests/UI/Editor/integration/README.md index 6d32d91e..5b7571fa 100644 --- a/tests/UI/Editor/integration/README.md +++ b/tests/UI/Editor/integration/README.md @@ -47,7 +47,7 @@ Scenarios: - `editor.shell.editor_shell_interaction` Build target: `editor_ui_editor_shell_interaction_validation` Executable: `XCUIEditorShellInteractionValidation.exe` - Scope: root shell interaction only; menu bar root switching, submenu hover chain, outside/Esc dismiss, command hook, and workspace input shielding + Scope: root shell unified interaction only; menu bar root switching, submenu hover chain, outside/Esc dismiss, command hook, menu-modal workspace shielding, and post-dismiss workspace interaction restore - `editor.shell.dock_host_basic` Build target: `editor_ui_dock_host_basic_validation` @@ -139,7 +139,7 @@ Selected controls: Click `切到 Scene / 切到 Document`, toggle `TopBar / BottomBar / Texture`, inspect `MenuBar Rect / Workspace Rect / StatusBar Rect / Selected Presentation / Request Size`, press `Reset`, press `截图` or `F12`. - `shell/editor_shell_interaction/` - Click `File / Window`, hover `Workspace Tools`, click outside the menu or press `Esc`, trigger a menu command, inspect `Open Root / Popup Chain / Submenu Path / Result / Active Panel / Visible Panels`, press `Reset`, `Capture`, or `F12`. + Click `File / Window`, hover `Workspace Tools`, click outside the menu or press `Esc`, then click `Document` or drag a splitter, inspect `Open Root / Popup Chain / Submenu Path / Selected Presentation / Active Panel / Host Capture / Result`, press `Reset`, `Capture`, or `F12`. - `shell/dock_host_basic/` Drag `root-split`, click `Document A`, close `Document B`, click `Details`, close `Console`, inspect `Hover / Result / Active Panel / Visible Panels / Capture / split ratio`, press `Reset`, `Capture`, or `F12`. diff --git a/tests/UI/Editor/integration/shell/editor_shell_interaction/captures/.gitkeep b/tests/UI/Editor/integration/shell/editor_shell_interaction/captures/.gitkeep index 8b137891..e69de29b 100644 --- a/tests/UI/Editor/integration/shell/editor_shell_interaction/captures/.gitkeep +++ b/tests/UI/Editor/integration/shell/editor_shell_interaction/captures/.gitkeep @@ -1 +0,0 @@ - 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 bc8cf248..f7ea1a87 100644 --- a/tests/UI/Editor/integration/shell/editor_shell_interaction/main.cpp +++ b/tests/UI/Editor/integration/shell/editor_shell_interaction/main.cpp @@ -46,6 +46,7 @@ using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels; +using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationFrame; using XCEngine::UI::Editor::GetUIEditorCommandDispatchStatusName; using XCEngine::UI::Editor::UpdateUIEditorShellInteraction; using XCEngine::UI::Editor::UIEditorCommandDispatchResult; @@ -63,6 +64,7 @@ using XCEngine::UI::Editor::UIEditorShellInteractionFrame; using XCEngine::UI::Editor::UIEditorShellInteractionModel; using XCEngine::UI::Editor::UIEditorShellInteractionResult; using XCEngine::UI::Editor::UIEditorShellInteractionState; +using XCEngine::UI::Editor::UIEditorViewportInputBridgeFrame; using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind; using XCEngine::UI::Editor::UIEditorWorkspaceController; using XCEngine::UI::Editor::UIEditorWorkspaceModel; @@ -120,11 +122,15 @@ bool ContainsPoint(const UIRect& rect, float x, float y) { bool HasMeaningfulInteractionResult(const UIEditorShellInteractionResult& result) { return result.consumed || + result.requestPointerCapture || + result.releasePointerCapture || result.commandTriggered || result.menuMutation.changed || + result.workspaceResult.consumed || !result.menuId.empty() || !result.popupId.empty() || !result.itemId.empty() || + !result.viewportPanelId.empty() || !result.commandId.empty(); } @@ -149,6 +155,34 @@ std::string FormatDismissReason(UIPopupDismissReason reason) { return "Unknown"; } +std::string DescribeViewportEvent(const UIEditorViewportInputBridgeFrame& frame) { + if (frame.captureStarted) { + return "Viewport CaptureStarted"; + } + if (frame.captureEnded) { + return "Viewport CaptureEnded"; + } + if (frame.focusGained) { + return "Viewport FocusGained"; + } + if (frame.focusLost) { + return "Viewport FocusLost"; + } + if (frame.pointerPressedInside) { + return "Viewport PointerDownInside"; + } + if (frame.pointerReleasedInside) { + return "Viewport PointerUpInside"; + } + if (frame.pointerMoved) { + return "Viewport PointerMove"; + } + if (frame.wheelDelta != 0.0f) { + return "Viewport Wheel"; + } + return "Viewport Input"; +} + std::string JoinVisiblePanelIds( const UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceSession& session) { @@ -357,8 +391,11 @@ private: void UpdateLayout(); void HandleMouseMove(float x, float y); void HandleLeftButtonDown(float x, float y); + void HandleLeftButtonUp(float x, float y); void ExecuteAction(ActionId action); UIEditorShellInteractionModel BuildInteractionModel() const; + bool HasInteractiveCaptureState() const; + void ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result); void SetInteractionResult(const UIEditorShellInteractionResult& result); void SetDispatchResult(const UIEditorCommandDispatchResult& result); void RenderFrame(); @@ -464,6 +501,12 @@ LRESULT CALLBACK ScenarioApp::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP return 0; } break; + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandleLeftButtonUp(static_cast(GET_X_LPARAM(lParam)), static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; case WM_SETFOCUS: if (app != nullptr) { UIInputEvent event = {}; @@ -480,6 +523,16 @@ LRESULT CALLBACK ScenarioApp::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP return 0; } break; + case WM_CAPTURECHANGED: + if (app != nullptr && + reinterpret_cast(lParam) != hwnd && + app->HasInteractiveCaptureState()) { + UIInputEvent event = {}; + event.type = UIInputEventType::FocusLost; + app->m_pendingInputEvents.push_back(event); + return 0; + } + break; case WM_KEYDOWN: case WM_SYSKEYDOWN: if (app != nullptr) { @@ -553,6 +606,9 @@ bool ScenarioApp::Initialize(HINSTANCE hInstance, int nCmdShow) { } void ScenarioApp::Shutdown() { + if (GetCapture() == m_hwnd) { + ReleaseCapture(); + } m_autoScreenshot.Shutdown(); m_renderer.Shutdown(); if (m_hwnd != nullptr && IsWindow(m_hwnd)) { @@ -564,6 +620,9 @@ void ScenarioApp::Shutdown() { } void ScenarioApp::ResetScenario() { + if (GetCapture() == m_hwnd) { + ReleaseCapture(); + } m_controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); m_commandDispatcher = UIEditorCommandDispatcher(BuildCommandRegistry()); m_menuModel = BuildMenuModel(); @@ -627,6 +686,14 @@ void ScenarioApp::HandleLeftButtonDown(float x, float y) { m_pendingInputEvents.push_back(event); } +void ScenarioApp::HandleLeftButtonUp(float x, float y) { + UIInputEvent event = {}; + event.type = UIInputEventType::PointerButtonUp; + event.position = UIPoint(x, y); + event.pointerButton = UIPointerButton::Left; + m_pendingInputEvents.push_back(event); +} + void ScenarioApp::ExecuteAction(ActionId action) { if (action == ActionId::Reset) { ResetScenario(); @@ -642,6 +709,29 @@ void ScenarioApp::ExecuteAction(ActionId action) { m_lastColor = kWarning; } +bool ScenarioApp::HasInteractiveCaptureState() const { + if (m_interactionState.workspaceInteractionState.dockHostInteractionState.splitterDragState.active) { + return true; + } + + for (const auto& panelState : m_interactionState.workspaceInteractionState.composeState.panelStates) { + if (panelState.viewportShellState.inputBridgeState.captured) { + return true; + } + } + + return false; +} + +void ScenarioApp::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result) { + if (result.requestPointerCapture && GetCapture() != m_hwnd) { + SetCapture(m_hwnd); + } + if (result.releasePointerCapture && GetCapture() == m_hwnd) { + ReleaseCapture(); + } +} + UIEditorShellInteractionModel ScenarioApp::BuildInteractionModel() const { UIEditorShellInteractionModel model = {}; model.resolvedMenuModel = BuildUIEditorResolvedMenuModel( @@ -720,6 +810,46 @@ void ScenarioApp::SetInteractionResult(const UIEditorShellInteractionResult& res return; } + if (result.workspaceResult.dockHostResult.layoutResult.status != + XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationStatus::Rejected) { + m_lastStatus = "WorkspaceLayout"; + m_lastMessage = result.workspaceResult.dockHostResult.layoutResult.message; + m_lastColor = kSuccess; + return; + } + + if (result.workspaceResult.dockHostResult.commandResult.status != + XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus::Rejected) { + m_lastStatus = "WorkspaceCommand"; + m_lastMessage = result.workspaceResult.dockHostResult.commandResult.message; + m_lastColor = kSuccess; + return; + } + + if (!result.viewportPanelId.empty()) { + m_lastStatus = result.viewportPanelId; + m_lastMessage = DescribeViewportEvent(result.viewportInputFrame); + m_lastColor = + result.viewportInputFrame.captureStarted || result.viewportInputFrame.focusGained + ? kSuccess + : kWarning; + return; + } + + if (result.requestPointerCapture) { + m_lastStatus = "Capture"; + m_lastMessage = "宿主已收到 root shell 返回的 pointer capture 请求。"; + m_lastColor = kSuccess; + return; + } + + if (result.releasePointerCapture) { + m_lastStatus = "Release"; + m_lastMessage = "宿主已执行 root shell 返回的 pointer release。"; + m_lastColor = kWarning; + return; + } + if (result.consumed) { m_lastStatus = "NoOp"; m_lastMessage = "这次输入被根壳交互层消费,但没有触发额外状态变化。"; @@ -738,27 +868,29 @@ void ScenarioApp::RenderFrame() { m_cachedModel = BuildInteractionModel(); m_cachedFrame = UpdateUIEditorShellInteraction( m_interactionState, + m_controller, m_shellRect, - m_controller.GetPanelRegistry(), - m_controller.GetWorkspace(), - m_controller.GetSession(), m_cachedModel, m_pendingInputEvents); m_pendingInputEvents.clear(); + ApplyHostCaptureRequests(m_cachedFrame.result); SetInteractionResult(m_cachedFrame.result); if (m_cachedFrame.result.commandTriggered) { m_cachedModel = BuildInteractionModel(); m_cachedFrame = UpdateUIEditorShellInteraction( m_interactionState, + m_controller, m_shellRect, - m_controller.GetPanelRegistry(), - m_controller.GetWorkspace(), - m_controller.GetSession(), m_cachedModel, {}); } + const auto* viewportFrame = + FindUIEditorWorkspaceViewportPresentationFrame(m_cachedFrame.workspaceInteractionFrame.composeFrame, "scene"); + const std::string selectedPresentation = + viewportFrame != nullptr ? "ViewportShell" : "DockHost Placeholder"; + RECT clientRect = {}; GetClientRect(m_hwnd, &clientRect); const float width = static_cast((std::max)(clientRect.right - clientRect.left, 1L)); @@ -772,9 +904,9 @@ void ScenarioApp::RenderFrame() { drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 70.0f), "1. 验证 MenuBar 的 root open / root switch 行为是否统一稳定。", kTextPrimary, 12.0f); 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. 验证预览区是真实 root shell:MenuBar + Workspace + StatusBar + popup overlay。", kTextPrimary, 12.0f); - drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 158.0f), "5. 验证 command 只通过最小 dispatch hook 回传,不接旧 editor 业务。", kTextPrimary, 12.0f); - drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 182.0f), "建议操作:点击 File,hover `Workspace Tools`,再按 Esc 或点预览区外空白处。", kTextWeak, 11.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 + 182.0f), "建议操作:点击 File,hover `Workspace Tools`,点预览外空白处,再点 `Document` 或拖 splitter。", kTextWeak, 11.0f); DrawCard(drawList, m_controlsRect, "操作", "只保留这个场景必要的控制。"); for (const ButtonState& button : m_buttons) { @@ -791,13 +923,16 @@ void ScenarioApp::RenderFrame() { addStateLine("Open Root", m_cachedFrame.openRootMenuId.empty() ? "(none)" : m_cachedFrame.openRootMenuId, kTextPrimary); addStateLine("Popup Chain", JoinPopupChainIds(m_interactionState), kTextPrimary, 11.0f); addStateLine("Submenu Path", JoinSubmenuPathIds(m_interactionState), kTextPrimary, 11.0f); + 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("Result", m_lastStatus, m_lastColor); drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY + 4.0f), m_lastMessage, kTextMuted, 11.0f); stateY += 34.0f; addStateLine("Visible Panels", JoinVisiblePanelIds(m_controller.GetWorkspace(), m_controller.GetSession()), kTextWeak, 11.0f); + addStateLine("Host Capture", FormatBool(GetCapture() == m_hwnd), GetCapture() == m_hwnd ? kSuccess : kTextMuted, 11.0f); addStateLine( - "Capture", + "Screenshot", m_autoScreenshot.HasPendingCapture() ? "截图排队中..." : (m_autoScreenshot.GetLastCaptureSummary().empty() 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 f1061184..3708a02c 100644 --- a/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp @@ -12,9 +12,9 @@ using XCEngine::Input::KeyCode; using XCEngine::UI::UIInputEvent; using XCEngine::UI::UIInputEventType; using XCEngine::UI::UIPoint; -using XCEngine::UI::UIRect; using XCEngine::UI::UIPointerButton; -using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; @@ -30,6 +30,7 @@ using XCEngine::UI::Editor::UIEditorShellInteractionMenuButtonRequest; using XCEngine::UI::Editor::UIEditorShellInteractionModel; using XCEngine::UI::Editor::UIEditorShellInteractionPopupItemRequest; using XCEngine::UI::Editor::UIEditorShellInteractionState; +using XCEngine::UI::Editor::UIEditorWorkspaceController; using XCEngine::UI::Editor::UIEditorWorkspaceModel; using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel; using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; @@ -149,6 +150,10 @@ UIEditorShellInteractionModel BuildInteractionModel() { return model; } +UIEditorWorkspaceController BuildController() { + return BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); +} + const UIEditorShellInteractionMenuButtonRequest* FindMenuButton( const UIEditorShellInteractionFrame& frame, std::string_view menuId) { @@ -210,28 +215,21 @@ UIInputEvent MakeFocusLost() { } // namespace TEST(UIEditorShellInteractionTest, ClickMenuBarRootOpensSingleRootPopup) { - const auto registry = BuildPanelRegistry(); - const auto workspace = BuildWorkspace(); - const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + auto controller = BuildController(); const auto model = BuildInteractionModel(); UIEditorShellInteractionState state = {}; const auto request = ResolveUIEditorShellInteractionRequest( UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, + controller, model, - {}, state); ASSERT_EQ(request.menuButtons.size(), 2u); const auto frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(RectCenter(request.menuButtons.front().rect)) }); @@ -243,18 +241,14 @@ TEST(UIEditorShellInteractionTest, ClickMenuBarRootOpensSingleRootPopup) { } TEST(UIEditorShellInteractionTest, HoverOtherRootSwitchesRootPopup) { - const auto registry = BuildPanelRegistry(); - const auto workspace = BuildWorkspace(); - const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + auto controller = BuildController(); const auto model = BuildInteractionModel(); UIEditorShellInteractionState state = {}; auto frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); const auto* windowButton = FindMenuButton(frame, "window"); @@ -262,10 +256,8 @@ TEST(UIEditorShellInteractionTest, HoverOtherRootSwitchesRootPopup) { frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakePointerMove(RectCenter(windowButton->rect)) }); @@ -276,18 +268,14 @@ TEST(UIEditorShellInteractionTest, HoverOtherRootSwitchesRootPopup) { } TEST(UIEditorShellInteractionTest, HoverSubmenuOpensChildPopupAndEscapeCollapsesChain) { - const auto registry = BuildPanelRegistry(); - const auto workspace = BuildWorkspace(); - const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + auto controller = BuildController(); const auto model = BuildInteractionModel(); UIEditorShellInteractionState state = {}; auto frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); const auto* submenuItem = FindPopupItem(frame, "file-workspace-tools"); @@ -296,10 +284,8 @@ TEST(UIEditorShellInteractionTest, HoverSubmenuOpensChildPopupAndEscapeCollapses frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakePointerMove(RectCenter(submenuItem->rect)) }); @@ -309,10 +295,8 @@ TEST(UIEditorShellInteractionTest, HoverSubmenuOpensChildPopupAndEscapeCollapses frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeKeyDown(KeyCode::Escape) }); EXPECT_EQ(frame.openRootMenuId, "file"); @@ -320,10 +304,8 @@ TEST(UIEditorShellInteractionTest, HoverSubmenuOpensChildPopupAndEscapeCollapses frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeKeyDown(KeyCode::Escape) }); EXPECT_FALSE(state.menuSession.HasOpenMenu()); @@ -331,18 +313,14 @@ TEST(UIEditorShellInteractionTest, HoverSubmenuOpensChildPopupAndEscapeCollapses } TEST(UIEditorShellInteractionTest, ClickCommandReturnsDispatchHookAndClosesMenu) { - const auto registry = BuildPanelRegistry(); - const auto workspace = BuildWorkspace(); - const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + auto controller = BuildController(); const auto model = BuildInteractionModel(); UIEditorShellInteractionState state = {}; auto frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); const auto* commandItem = FindPopupItem(frame, "file-close"); @@ -350,10 +328,8 @@ TEST(UIEditorShellInteractionTest, ClickCommandReturnsDispatchHookAndClosesMenu) frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(RectCenter(commandItem->rect)) }); @@ -364,28 +340,22 @@ TEST(UIEditorShellInteractionTest, ClickCommandReturnsDispatchHookAndClosesMenu) } TEST(UIEditorShellInteractionTest, PointerDownOutsideDismissesWholeMenuChain) { - const auto registry = BuildPanelRegistry(); - const auto workspace = BuildWorkspace(); - const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + auto controller = BuildController(); const auto model = BuildInteractionModel(); UIEditorShellInteractionState state = {}; auto frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); ASSERT_TRUE(state.menuSession.HasOpenMenu()); frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(UIPoint(900.0f, 500.0f)) }); @@ -395,19 +365,15 @@ TEST(UIEditorShellInteractionTest, PointerDownOutsideDismissesWholeMenuChain) { } TEST(UIEditorShellInteractionTest, DisabledSubmenuDoesNotOpenChildPopup) { - const auto registry = BuildPanelRegistry(); - const auto workspace = BuildWorkspace(); - const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + auto controller = BuildController(); auto model = BuildInteractionModel(); model.resolvedMenuModel.menus[0].items[0].enabled = false; UIEditorShellInteractionState state = {}; auto frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); const auto* submenuItem = FindPopupItem(frame, "file-workspace-tools"); @@ -417,10 +383,8 @@ TEST(UIEditorShellInteractionTest, DisabledSubmenuDoesNotOpenChildPopup) { frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakePointerMove(RectCenter(submenuItem->rect)) }); @@ -430,18 +394,14 @@ TEST(UIEditorShellInteractionTest, DisabledSubmenuDoesNotOpenChildPopup) { } TEST(UIEditorShellInteractionTest, FocusLostDismissesWholeMenuChain) { - const auto registry = BuildPanelRegistry(); - const auto workspace = BuildWorkspace(); - const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + auto controller = BuildController(); const auto model = BuildInteractionModel(); UIEditorShellInteractionState state = {}; auto frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); const auto* submenuItem = FindPopupItem(frame, "file-workspace-tools"); @@ -449,20 +409,16 @@ TEST(UIEditorShellInteractionTest, FocusLostDismissesWholeMenuChain) { frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakePointerMove(RectCenter(submenuItem->rect)) }); ASSERT_EQ(frame.request.popupRequests.size(), 2u); frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeFocusLost() }); @@ -474,18 +430,14 @@ TEST(UIEditorShellInteractionTest, FocusLostDismissesWholeMenuChain) { } TEST(UIEditorShellInteractionTest, OpenMenuConsumesWorkspacePointerDownForThatFrame) { - const auto registry = BuildPanelRegistry(); - const auto workspace = BuildWorkspace(); - const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + auto controller = BuildController(); const auto model = BuildInteractionModel(); UIEditorShellInteractionState state = {}; auto frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); ASSERT_TRUE(state.menuSession.HasOpenMenu()); @@ -495,31 +447,100 @@ TEST(UIEditorShellInteractionTest, OpenMenuConsumesWorkspacePointerDownForThatFr frame.shellFrame.workspaceFrame.viewportFrames.front().viewportShellFrame.slotLayout.inputRect; frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(RectCenter(inputRect)) }); + EXPECT_FALSE(frame.result.workspaceResult.consumed); EXPECT_FALSE(frame.shellFrame.workspaceFrame.viewportFrames.front() .viewportShellFrame.inputFrame.pointerPressedInside); EXPECT_FALSE(state.menuSession.HasOpenMenu()); } -TEST(UIEditorShellInteractionTest, InvalidResolvedMenuStateClosesInvisibleModalChain) { - const auto registry = BuildPanelRegistry(); - const auto workspace = BuildWorkspace(); - const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); +TEST(UIEditorShellInteractionTest, MenuClosedBubblesViewportCaptureFromWorkspaceInteraction) { + auto controller = BuildController(); const auto model = BuildInteractionModel(); UIEditorShellInteractionState state = {}; auto frame = UpdateUIEditorShellInteraction( state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + {}); + ASSERT_FALSE(frame.shellFrame.workspaceFrame.viewportFrames.empty()); + + const UIRect inputRect = + frame.shellFrame.workspaceFrame.viewportFrames.front().viewportShellFrame.slotLayout.inputRect; + const UIPoint center = RectCenter(inputRect); + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { + MakePointerMove(center), + MakeLeftPointerDown(center) + }); + + EXPECT_TRUE(frame.result.consumed); + EXPECT_TRUE(frame.result.requestPointerCapture); + EXPECT_TRUE(frame.result.workspaceResult.requestPointerCapture); + EXPECT_EQ(frame.result.viewportPanelId, "scene"); + EXPECT_TRUE(frame.result.viewportInputFrame.captureStarted); +} + +TEST(UIEditorShellInteractionTest, ClosingMenuRestoresWorkspaceInteractionOnNextFrame) { + auto controller = BuildController(); + const auto model = BuildInteractionModel(); + + UIEditorShellInteractionState state = {}; + auto frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); + ASSERT_TRUE(state.menuSession.HasOpenMenu()); + ASSERT_FALSE(frame.shellFrame.workspaceFrame.viewportFrames.empty()); + + const UIRect inputRect = + frame.shellFrame.workspaceFrame.viewportFrames.front().viewportShellFrame.slotLayout.inputRect; + const UIPoint center = RectCenter(inputRect); + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakeLeftPointerDown(UIPoint(900.0f, 500.0f)) }); + EXPECT_FALSE(state.menuSession.HasOpenMenu()); + EXPECT_FALSE(frame.result.workspaceResult.consumed); + + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { + MakePointerMove(center), + MakeLeftPointerDown(center) + }); + + EXPECT_TRUE(frame.result.workspaceResult.consumed); + EXPECT_TRUE(frame.result.requestPointerCapture); + EXPECT_EQ(frame.result.viewportPanelId, "scene"); +} + +TEST(UIEditorShellInteractionTest, InvalidResolvedMenuStateClosesInvisibleModalChain) { + auto controller = BuildController(); + const auto model = BuildInteractionModel(); + + UIEditorShellInteractionState state = {}; + auto frame = UpdateUIEditorShellInteraction( + state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, model, { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); ASSERT_TRUE(state.menuSession.HasOpenMenu()); @@ -530,10 +551,8 @@ TEST(UIEditorShellInteractionTest, InvalidResolvedMenuStateClosesInvisibleModalC frame = UpdateUIEditorShellInteraction( state, + controller, UIRect(0.0f, 0.0f, 1280.0f, 720.0f), - registry, - workspace, - session, updatedModel, {});