resources: formalize internal shader ir

This commit is contained in:
2026-04-07 11:31:13 +08:00
parent 1c87650fb3
commit 864438c508
11 changed files with 431 additions and 259 deletions

View File

@@ -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

View File

@@ -433,10 +433,10 @@ bool TryParseUnityStyleBlendDirective(
}
void SetOrReplaceAuthoringTag(
std::vector<AuthoringTagEntry>& tags,
std::vector<ShaderTagIR>& 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<std::string
bool TryParseInlineTagAssignments(
const std::string& line,
std::vector<AuthoringTagEntry>& outTags) {
std::vector<ShaderTagIR>& 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<BlockKind> 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<AuthoringTagEntry> parsedTags;
std::vector<ShaderTagIR> 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<BlockKind> 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<AuthoringTagEntry> parsedTags;
std::vector<ShaderTagIR> 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);
}

View File

@@ -1,8 +1,6 @@
#pragma once
#include <XCEngine/Core/Containers/Array.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include "ShaderIR.h"
#include <string>
#include <unordered_set>
@@ -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<AuthoringTagEntry> tags;
Containers::Array<ShaderResourceBindingDesc> resources;
Containers::Array<ShaderKeywordDeclaration> keywordDeclarations;
Containers::String vertexEntryPoint;
Containers::String fragmentEntryPoint;
Containers::String sharedProgramSource;
Containers::String programSource;
Containers::String targetProfile;
std::vector<AuthoringBackendVariantEntry> backendVariants;
};
struct AuthoringSubShaderEntry {
bool hasFixedFunctionState = false;
MaterialRenderState fixedFunctionState = {};
std::vector<AuthoringTagEntry> tags;
Containers::String sharedProgramSource;
std::vector<AuthoringPassEntry> passes;
};
struct AuthoringShaderDesc {
Containers::String name;
Containers::String fallback;
Containers::String sharedProgramSource;
Containers::Array<ShaderPropertyDesc> properties;
std::vector<AuthoringSubShaderEntry> 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
} // namespace XCEngine

View File

@@ -0,0 +1,58 @@
#pragma once
#include <XCEngine/Core/Containers/Array.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include <vector>
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<ShaderTagIR> tags;
Containers::Array<ShaderResourceBindingDesc> resources;
Containers::Array<ShaderKeywordDeclaration> keywordDeclarations;
Containers::String vertexEntryPoint;
Containers::String fragmentEntryPoint;
Containers::String sharedProgramSource;
Containers::String programSource;
Containers::String targetProfile;
std::vector<ShaderBackendVariantIR> backendVariants;
};
struct ShaderSubShaderIR {
bool hasFixedFunctionState = false;
MaterialRenderState fixedFunctionState = {};
std::vector<ShaderTagIR> tags;
Containers::String sharedProgramSource;
std::vector<ShaderPassIR> passes;
};
struct ShaderIR {
Containers::String name;
Containers::String fallback;
Containers::String sharedProgramSource;
Containers::Array<ShaderPropertyDesc> properties;
std::vector<ShaderSubShaderIR> subShaders;
};
} // namespace Resources
} // namespace XCEngine

View File

@@ -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<Shader>();
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<ShaderKeywordSet> 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<Containers::String>& 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<std::string> 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<Containers::String>& 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<std::string> 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<typename T>

View File

@@ -3,7 +3,8 @@
#include <XCEditor/Core/UIEditorMenuModel.h>
#include <XCEditor/Core/UIEditorMenuSession.h>
#include <XCEditor/Core/UIEditorShellCompose.h>
#include <XCEditor/Core/UIEditorWorkspaceCompose.h>
#include <XCEditor/Core/UIEditorWorkspaceController.h>
#include <XCEditor/Core/UIEditorWorkspaceInteraction.h>
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
#include <XCEngine/UI/DrawData.h>
@@ -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<UIEditorShellInteractionPopupFrame> 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(

View File

@@ -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<UIInputEvent> FilterComposeInputEvents(
std::vector<UIInputEvent> FilterWorkspaceInputEvents(
const std::vector<UIInputEvent>& inputEvents,
bool menuModalDuringFrame) {
if (!menuModalDuringFrame) {
@@ -466,44 +466,32 @@ std::vector<UIInputEvent> 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<UIInputEvent>& 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<UIInputEvent> 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<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.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;
}

View File

@@ -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`.

View File

@@ -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<float>(GET_X_LPARAM(lParam)), static_cast<float>(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<HWND>(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<float>((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 shellMenuBar + 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), "建议操作:点击 Filehover `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), "建议操作:点击 Filehover `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()

View File

@@ -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,
{});