From 49e9b63a2da54b467ce0625e460e4765204e367c Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 12:38:23 +0800 Subject: [PATCH] Internalize editor shell asset definition contract --- new_editor/app/Application.h | 46 ++++++ new_editor/src/Core/EditorShellAsset.cpp | 88 +++++++++++ new_editor/src/Core/EditorShellAsset.h | 8 +- .../test_editor_shell_asset_validation.cpp | 63 ++++++++ .../unit/test_structured_editor_shell.cpp | 142 ++++++++++++++++-- 5 files changed, 337 insertions(+), 10 deletions(-) diff --git a/new_editor/app/Application.h b/new_editor/app/Application.h index 0b0542d5..9a4008eb 100644 --- a/new_editor/app/Application.h +++ b/new_editor/app/Application.h @@ -10,6 +10,10 @@ #include "Core/EditorShellAsset.h" +#include +#include +#include + #include #include @@ -24,6 +28,46 @@ namespace XCEngine::UI::Editor { +struct StructuredEditorShellBinding { + ::XCEngine::UI::Runtime::UIScreenAsset screenAsset = {}; + UIEditorWorkspaceController workspaceController = {}; + UIEditorShellInteractionDefinition shellDefinition = {}; + UIEditorShortcutManager shortcutManager = {}; + EditorShellAssetValidationResult assetValidation = {}; + + [[nodiscard]] bool IsValid() const { + return assetValidation.IsValid(); + } +}; + +inline UIEditorShortcutManager BuildStructuredEditorShortcutManager( + const EditorShellAsset& asset) { + (void)asset; + return UIEditorShortcutManager(UIEditorCommandRegistry{}); +} + +inline StructuredEditorShellBinding BuildStructuredEditorShellBinding( + const EditorShellAsset& asset) { + StructuredEditorShellBinding binding = {}; + binding.screenAsset.screenId = asset.screenId; + binding.screenAsset.documentPath = asset.documentPath.string(); + binding.screenAsset.themePath = asset.themePath.string(); + binding.workspaceController = + UIEditorWorkspaceController(asset.panelRegistry, asset.workspace, asset.workspaceSession); + binding.shellDefinition = asset.shellDefinition; + binding.shortcutManager = BuildStructuredEditorShortcutManager(asset); + binding.assetValidation = ValidateEditorShellAsset(asset); + return binding; +} + +inline UIEditorShellInteractionServices BuildStructuredEditorShellServices( + const StructuredEditorShellBinding& binding) { + UIEditorShellInteractionServices services = {}; + services.commandDispatcher = &binding.shortcutManager.GetCommandDispatcher(); + services.shortcutManager = &binding.shortcutManager; + return services; +} + class Application { public: Application(); @@ -65,6 +109,8 @@ private: ::XCEngine::UI::Runtime::UIScreenPlayer m_screenPlayer; ::XCEngine::UI::Runtime::UIScreenAsset m_screenAsset = {}; EditorShellAsset m_shellAssetDefinition = {}; + StructuredEditorShellBinding m_structuredShell = {}; + UIEditorShellInteractionServices m_shellServices = {}; std::vector m_trackedFiles = {}; std::chrono::steady_clock::time_point m_startTime = {}; std::chrono::steady_clock::time_point m_lastFrameTime = {}; diff --git a/new_editor/src/Core/EditorShellAsset.cpp b/new_editor/src/Core/EditorShellAsset.cpp index 9346901a..d7012acc 100644 --- a/new_editor/src/Core/EditorShellAsset.cpp +++ b/new_editor/src/Core/EditorShellAsset.cpp @@ -1,11 +1,24 @@ #include "EditorShellAsset.h" +#include #include namespace XCEngine::UI::Editor { namespace { +Widgets::UIEditorStatusBarSegment BuildDefaultShellModeSegment() { + Widgets::UIEditorStatusBarSegment segment = {}; + segment.segmentId = "mode"; + segment.label = "Editor Shell"; + segment.slot = Widgets::UIEditorStatusBarSlot::Leading; + segment.tone = Widgets::UIEditorStatusBarTextTone::Primary; + segment.interactive = false; + segment.showSeparator = true; + segment.desiredWidth = 112.0f; + return segment; +} + EditorShellAssetValidationResult MakeValidationError( EditorShellAssetValidationCode code, std::string message) { @@ -53,6 +66,74 @@ EditorShellAssetValidationResult ValidateWorkspacePanelsAgainstRegistry( return {}; } +EditorShellAssetValidationResult ValidateShellDefinitionAgainstRegistry( + const UIEditorShellInteractionDefinition& definition, + const UIEditorPanelRegistry& panelRegistry) { + std::unordered_set panelIds = {}; + for (const UIEditorWorkspacePanelPresentationModel& presentation : + definition.workspacePresentations) { + if (!panelIds.insert(presentation.panelId).second) { + return MakeValidationError( + EditorShellAssetValidationCode::DuplicateShellPresentationPanelId, + "Shell definition presentation panel '" + presentation.panelId + + "' is duplicated."); + } + + const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(panelRegistry, presentation.panelId); + if (descriptor == nullptr) { + return MakeValidationError( + EditorShellAssetValidationCode::MissingShellPresentationPanelDescriptor, + "Shell definition presentation panel '" + presentation.panelId + + "' is missing from the panel registry."); + } + + if (presentation.kind != descriptor->presentationKind) { + return MakeValidationError( + EditorShellAssetValidationCode::ShellPresentationKindMismatch, + "Shell definition presentation panel '" + presentation.panelId + + "' kind does not match the panel registry."); + } + } + + for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) { + if (panelIds.find(descriptor.panelId) == panelIds.end()) { + return MakeValidationError( + EditorShellAssetValidationCode::MissingRequiredShellPresentation, + "Shell definition is missing presentation panel '" + descriptor.panelId + + "' required by the panel registry."); + } + } + + return {}; +} + +UIEditorWorkspacePanelPresentationModel BuildShellPresentation( + const UIEditorPanelDescriptor& descriptor) { + UIEditorWorkspacePanelPresentationModel presentation = {}; + presentation.panelId = descriptor.panelId; + presentation.kind = descriptor.presentationKind; + if (descriptor.presentationKind == UIEditorPanelPresentationKind::ViewportShell) { + presentation.viewportShellModel.spec.chrome.title = descriptor.defaultTitle; + presentation.viewportShellModel.spec.chrome.subtitle = "Editor Shell"; + presentation.viewportShellModel.spec.chrome.showTopBar = true; + presentation.viewportShellModel.spec.chrome.showBottomBar = true; + presentation.viewportShellModel.frame.statusText = descriptor.defaultTitle; + } + return presentation; +} + +UIEditorShellInteractionDefinition BuildDefaultShellDefinition( + const UIEditorPanelRegistry& panelRegistry) { + UIEditorShellInteractionDefinition definition = {}; + definition.statusSegments = { BuildDefaultShellModeSegment() }; + definition.workspacePresentations.reserve(panelRegistry.panels.size()); + for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) { + definition.workspacePresentations.push_back(BuildShellPresentation(descriptor)); + } + return definition; +} + } // namespace EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot) { @@ -63,6 +144,7 @@ EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoR asset.panelRegistry = BuildDefaultEditorShellPanelRegistry(); asset.workspace = BuildDefaultEditorShellWorkspaceModel(); asset.workspaceSession = BuildDefaultUIEditorWorkspaceSession(asset.panelRegistry, asset.workspace); + asset.shellDefinition = BuildDefaultShellDefinition(asset.panelRegistry); return asset; } @@ -100,6 +182,12 @@ EditorShellAssetValidationResult ValidateEditorShellAsset(const EditorShellAsset workspaceSessionValidation.message); } + const EditorShellAssetValidationResult shellDefinitionValidation = + ValidateShellDefinitionAgainstRegistry(asset.shellDefinition, asset.panelRegistry); + if (!shellDefinitionValidation.IsValid()) { + return shellDefinitionValidation; + } + return {}; } diff --git a/new_editor/src/Core/EditorShellAsset.h b/new_editor/src/Core/EditorShellAsset.h index f70f41fd..2e519e09 100644 --- a/new_editor/src/Core/EditorShellAsset.h +++ b/new_editor/src/Core/EditorShellAsset.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -18,6 +19,7 @@ struct EditorShellAsset { UIEditorPanelRegistry panelRegistry = {}; UIEditorWorkspaceModel workspace = {}; UIEditorWorkspaceSession workspaceSession = {}; + UIEditorShellInteractionDefinition shellDefinition = {}; }; enum class EditorShellAssetValidationCode : std::uint8_t { @@ -27,7 +29,11 @@ enum class EditorShellAssetValidationCode : std::uint8_t { InvalidWorkspaceSession, MissingPanelDescriptor, PanelTitleMismatch, - PanelPlaceholderMismatch + PanelPlaceholderMismatch, + DuplicateShellPresentationPanelId, + MissingShellPresentationPanelDescriptor, + MissingRequiredShellPresentation, + ShellPresentationKindMismatch }; struct EditorShellAssetValidationResult { diff --git a/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp b/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp index cf8c80e6..b588e7e4 100644 --- a/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp +++ b/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp @@ -16,6 +16,17 @@ TEST(EditorShellAssetValidationTest, DefaultShellAssetPassesValidation) { const auto validation = ValidateEditorShellAsset(shellAsset); EXPECT_TRUE(validation.IsValid()) << validation.message; + ASSERT_EQ( + shellAsset.shellDefinition.workspacePresentations.size(), + shellAsset.panelRegistry.panels.size()); + ASSERT_EQ(shellAsset.shellDefinition.statusSegments.size(), 1u); + EXPECT_EQ(shellAsset.shellDefinition.statusSegments.front().label, "Editor Shell"); + EXPECT_EQ( + shellAsset.shellDefinition.workspacePresentations.front().panelId, + "editor-foundation-root"); + EXPECT_EQ( + shellAsset.shellDefinition.workspacePresentations.front().kind, + shellAsset.panelRegistry.panels.front().presentationKind); } TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspacePanelMissingFromRegistry) { @@ -50,4 +61,56 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidWorkspaceSessionSta EXPECT_EQ(validation.code, EditorShellAssetValidationCode::InvalidWorkspaceSession); } +TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationMissingFromRegistry) { + auto shellAsset = BuildDefaultEditorShellAsset("."); + ASSERT_EQ( + shellAsset.shellDefinition.workspacePresentations.size(), + shellAsset.panelRegistry.panels.size()); + shellAsset.shellDefinition.workspacePresentations.front().panelId = + "editor-foundation-root-renamed"; + + const auto validation = ValidateEditorShellAsset(shellAsset); + EXPECT_EQ( + validation.code, + EditorShellAssetValidationCode::MissingShellPresentationPanelDescriptor); +} + +TEST(EditorShellAssetValidationTest, ValidationRejectsDuplicateShellPresentationPanelId) { + auto shellAsset = BuildDefaultEditorShellAsset("."); + ASSERT_EQ( + shellAsset.shellDefinition.workspacePresentations.size(), + shellAsset.panelRegistry.panels.size()); + shellAsset.shellDefinition.workspacePresentations.push_back( + shellAsset.shellDefinition.workspacePresentations.front()); + + const auto validation = ValidateEditorShellAsset(shellAsset); + EXPECT_EQ( + validation.code, + EditorShellAssetValidationCode::DuplicateShellPresentationPanelId); +} + +TEST(EditorShellAssetValidationTest, ValidationRejectsMissingRequiredShellPresentation) { + auto shellAsset = BuildDefaultEditorShellAsset("."); + shellAsset.shellDefinition.workspacePresentations.clear(); + + const auto validation = ValidateEditorShellAsset(shellAsset); + EXPECT_EQ( + validation.code, + EditorShellAssetValidationCode::MissingRequiredShellPresentation); +} + +TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationKindMismatch) { + auto shellAsset = BuildDefaultEditorShellAsset("."); + ASSERT_EQ( + shellAsset.shellDefinition.workspacePresentations.size(), + shellAsset.panelRegistry.panels.size()); + shellAsset.shellDefinition.workspacePresentations.front().kind = + XCEngine::UI::Editor::UIEditorPanelPresentationKind::ViewportShell; + + const auto validation = ValidateEditorShellAsset(shellAsset); + EXPECT_EQ( + validation.code, + EditorShellAssetValidationCode::ShellPresentationKindMismatch); +} + } // namespace diff --git a/tests/UI/Editor/unit/test_structured_editor_shell.cpp b/tests/UI/Editor/unit/test_structured_editor_shell.cpp index cef2a648..92056661 100644 --- a/tests/UI/Editor/unit/test_structured_editor_shell.cpp +++ b/tests/UI/Editor/unit/test_structured_editor_shell.cpp @@ -1,7 +1,10 @@ #include -#include "Core/EditorShellAsset.h" +#include "Application.h" +#include +#include +#include #include #include @@ -16,6 +19,11 @@ namespace { using XCEngine::UI::Editor::BuildDefaultEditorShellAsset; +using XCEngine::UI::Editor::BuildStructuredEditorShellBinding; +using XCEngine::UI::Editor::BuildStructuredEditorShellServices; +using XCEngine::UI::Editor::ResolveUIEditorShellInteractionModel; +using XCEngine::UI::Editor::AreUIEditorWorkspaceModelsEquivalent; +using XCEngine::UI::Editor::AreUIEditorWorkspaceSessionsEquivalent; using XCEngine::UI::UIDrawCommand; using XCEngine::UI::UIDrawCommandType; using XCEngine::UI::UIDrawData; @@ -56,23 +64,93 @@ bool ContainsPathWithFilename( return false; } +bool PanelRegistriesMatch( + const XCEngine::UI::Editor::UIEditorPanelRegistry& lhs, + const XCEngine::UI::Editor::UIEditorPanelRegistry& rhs) { + if (lhs.panels.size() != rhs.panels.size()) { + return false; + } + + for (std::size_t index = 0; index < lhs.panels.size(); ++index) { + const auto& left = lhs.panels[index]; + const auto& right = rhs.panels[index]; + if (left.panelId != right.panelId || + left.defaultTitle != right.defaultTitle || + left.presentationKind != right.presentationKind || + left.placeholder != right.placeholder || + left.canHide != right.canHide || + left.canClose != right.canClose) { + return false; + } + } + + return true; +} + +bool ShellDefinitionsMatch( + const XCEngine::UI::Editor::UIEditorShellInteractionDefinition& lhs, + const XCEngine::UI::Editor::UIEditorShellInteractionDefinition& rhs) { + if (lhs.menuModel.menus.size() != rhs.menuModel.menus.size() || + lhs.statusSegments.size() != rhs.statusSegments.size() || + lhs.workspacePresentations.size() != rhs.workspacePresentations.size()) { + return false; + } + + for (std::size_t index = 0; index < lhs.menuModel.menus.size(); ++index) { + const auto& left = lhs.menuModel.menus[index]; + const auto& right = rhs.menuModel.menus[index]; + if (left.menuId != right.menuId || + left.label != right.label || + left.items.size() != right.items.size()) { + return false; + } + } + + for (std::size_t index = 0; index < lhs.statusSegments.size(); ++index) { + const auto& left = lhs.statusSegments[index]; + const auto& right = rhs.statusSegments[index]; + if (left.segmentId != right.segmentId || + left.label != right.label || + left.slot != right.slot || + left.tone != right.tone || + left.interactive != right.interactive || + left.showSeparator != right.showSeparator || + left.desiredWidth != right.desiredWidth) { + return false; + } + } + + for (std::size_t index = 0; index < lhs.workspacePresentations.size(); ++index) { + const auto& left = lhs.workspacePresentations[index]; + const auto& right = rhs.workspacePresentations[index]; + if (left.panelId != right.panelId || + left.kind != right.kind || + left.viewportShellModel.spec.chrome.title != right.viewportShellModel.spec.chrome.title || + left.viewportShellModel.spec.chrome.subtitle != right.viewportShellModel.spec.chrome.subtitle || + left.viewportShellModel.spec.chrome.showTopBar != right.viewportShellModel.spec.chrome.showTopBar || + left.viewportShellModel.spec.chrome.showBottomBar != right.viewportShellModel.spec.chrome.showBottomBar || + left.viewportShellModel.frame.statusText != right.viewportShellModel.frame.statusText) { + return false; + } + } + + return true; +} + } // namespace TEST(EditorUIStructuredShellTest, AuthoredEditorShellLoadsFromRepositoryResources) { const auto shell = BuildDefaultEditorShellAsset(RepoRootPath()); + const auto binding = BuildStructuredEditorShellBinding(shell); - ASSERT_TRUE(std::filesystem::exists(shell.documentPath)); - ASSERT_TRUE(std::filesystem::exists(shell.themePath)); - - UIScreenAsset asset = {}; - asset.screenId = shell.screenId; - asset.documentPath = shell.documentPath.string(); - asset.themePath = shell.themePath.string(); + ASSERT_TRUE(binding.IsValid()) << binding.assetValidation.message; + ASSERT_TRUE(std::filesystem::exists(std::filesystem::path(binding.screenAsset.documentPath))); + ASSERT_TRUE(std::filesystem::exists(std::filesystem::path(binding.screenAsset.themePath))); UIDocumentScreenHost host = {}; UIScreenPlayer player(host); - ASSERT_TRUE(player.Load(asset)) << player.GetLastError(); + ASSERT_TRUE(player.Load(binding.screenAsset)) << player.GetLastError(); ASSERT_NE(player.GetDocument(), nullptr); EXPECT_TRUE(player.GetDocument()->hasThemeDocument); EXPECT_TRUE(ContainsPathWithFilename(player.GetDocument()->dependencies, "editor_shell.xctheme")); @@ -91,3 +169,49 @@ TEST(EditorUIStructuredShellTest, AuthoredEditorShellLoadsFromRepositoryResource EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Right Pane Host")); EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Bottom Pane Host")); } + +TEST(EditorUIStructuredShellTest, StructuredShellBindingUsesEditorShellAssetAsSingleSource) { + auto shell = BuildDefaultEditorShellAsset(RepoRootPath()); + shell.shellDefinition.statusSegments.front().label = "Asset Contract"; + XCEngine::UI::Editor::UIEditorMenuDescriptor assetMenu = {}; + assetMenu.menuId = "asset"; + assetMenu.label = "Asset"; + shell.shellDefinition.menuModel.menus = { assetMenu }; + const auto binding = BuildStructuredEditorShellBinding(shell); + + ASSERT_TRUE(binding.IsValid()) << binding.assetValidation.message; + EXPECT_EQ(binding.screenAsset.screenId, shell.screenId); + EXPECT_EQ(std::filesystem::path(binding.screenAsset.documentPath), shell.documentPath); + EXPECT_EQ(std::filesystem::path(binding.screenAsset.themePath), shell.themePath); + + EXPECT_TRUE(PanelRegistriesMatch(binding.workspaceController.GetPanelRegistry(), shell.panelRegistry)); + EXPECT_TRUE(AreUIEditorWorkspaceModelsEquivalent(binding.workspaceController.GetWorkspace(), shell.workspace)); + EXPECT_TRUE(AreUIEditorWorkspaceSessionsEquivalent(binding.workspaceController.GetSession(), shell.workspaceSession)); + EXPECT_TRUE(ShellDefinitionsMatch(binding.shellDefinition, shell.shellDefinition)); + EXPECT_TRUE(binding.workspaceController.ValidateState().IsValid()); + EXPECT_TRUE(binding.shortcutManager.ValidateConfiguration().IsValid()); + + const auto services = BuildStructuredEditorShellServices(binding); + ASSERT_NE(services.commandDispatcher, nullptr); + EXPECT_EQ(services.shortcutManager, &binding.shortcutManager); + + const auto model = ResolveUIEditorShellInteractionModel( + binding.workspaceController, + binding.shellDefinition, + services); + + ASSERT_EQ(model.resolvedMenuModel.menus.size(), 1u); + EXPECT_EQ(model.resolvedMenuModel.menus.front().menuId, "asset"); + EXPECT_EQ(model.resolvedMenuModel.menus.front().label, "Asset"); + ASSERT_EQ(model.statusSegments.size(), shell.shellDefinition.statusSegments.size()); + EXPECT_EQ(model.statusSegments.front().label, shell.shellDefinition.statusSegments.front().label); + ASSERT_EQ( + model.workspacePresentations.size(), + shell.shellDefinition.workspacePresentations.size()); + EXPECT_EQ( + model.workspacePresentations.front().panelId, + shell.shellDefinition.workspacePresentations.front().panelId); + EXPECT_EQ( + model.workspacePresentations.front().kind, + shell.shellDefinition.workspacePresentations.front().kind); +}