Refine editor shell asset contract

This commit is contained in:
2026-04-07 13:01:48 +08:00
parent e7669dc8c3
commit 6bf61ad8e2
4 changed files with 130 additions and 54 deletions

View File

@@ -199,6 +199,9 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) {
bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
m_hInstance = hInstance;
m_shellAssetDefinition = BuildDefaultEditorShellAsset(ResolveRepoRootPath());
m_structuredShell = BuildStructuredEditorShellBinding(m_shellAssetDefinition);
m_shellServices = BuildStructuredEditorShellServices(m_structuredShell);
m_screenAsset = m_structuredShell.screenAsset;
WNDCLASSEXW windowClass = {};
windowClass.cbSize = sizeof(windowClass);
@@ -397,14 +400,11 @@ void Application::QueueWindowFocusEvent(UIInputEventType type) {
bool Application::LoadStructuredScreen(const char* triggerReason) {
(void)triggerReason;
m_screenAsset = {};
m_screenAsset.screenId = m_shellAssetDefinition.screenId;
m_screenAsset.documentPath = m_shellAssetDefinition.documentPath.string();
m_screenAsset.themePath = m_shellAssetDefinition.themePath.string();
m_screenAsset = m_structuredShell.screenAsset;
const bool loaded = m_screenPlayer.Load(m_screenAsset);
const EditorShellAssetValidationResult shellAssetValidation =
ValidateEditorShellAsset(m_shellAssetDefinition);
const EditorShellAssetValidationResult& shellAssetValidation =
m_structuredShell.assetValidation;
const auto shortcutValidation = m_structuredShell.shortcutManager.ValidateConfiguration();
m_useStructuredScreen = loaded;
m_runtimeStatus = loaded ? "XCUI Editor Shell" : "Editor Shell | Load Error";
m_runtimeError.clear();
@@ -416,6 +416,11 @@ bool Application::LoadStructuredScreen(const char* triggerReason) {
m_runtimeError,
"Editor shell asset invalid: " + shellAssetValidation.message);
}
if (!shortcutValidation.IsValid()) {
AppendErrorMessage(
m_runtimeError,
"Structured shell shortcut manager invalid: " + shortcutValidation.message);
}
RebuildTrackedFileStates();
return loaded;
}

View File

@@ -19,6 +19,19 @@ Widgets::UIEditorStatusBarSegment BuildDefaultShellModeSegment() {
return segment;
}
Widgets::UIEditorStatusBarSegment BuildDefaultActivePanelSegment(
const UIEditorWorkspaceModel& workspace) {
Widgets::UIEditorStatusBarSegment segment = {};
segment.segmentId = "active-panel";
segment.label = workspace.activePanelId.empty() ? std::string("(none)") : workspace.activePanelId;
segment.slot = Widgets::UIEditorStatusBarSlot::Trailing;
segment.tone = Widgets::UIEditorStatusBarTextTone::Muted;
segment.interactive = false;
segment.showSeparator = false;
segment.desiredWidth = 144.0f;
return segment;
}
EditorShellAssetValidationResult MakeValidationError(
EditorShellAssetValidationCode code,
std::string message) {
@@ -124,9 +137,13 @@ UIEditorWorkspacePanelPresentationModel BuildShellPresentation(
}
UIEditorShellInteractionDefinition BuildDefaultShellDefinition(
const UIEditorPanelRegistry& panelRegistry) {
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace) {
UIEditorShellInteractionDefinition definition = {};
definition.statusSegments = { BuildDefaultShellModeSegment() };
definition.statusSegments = {
BuildDefaultShellModeSegment(),
BuildDefaultActivePanelSegment(workspace)
};
definition.workspacePresentations.reserve(panelRegistry.panels.size());
for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) {
definition.workspacePresentations.push_back(BuildShellPresentation(descriptor));
@@ -144,7 +161,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);
asset.shellDefinition = BuildDefaultShellDefinition(asset.panelRegistry, asset.workspace);
return asset;
}

View File

@@ -2,6 +2,8 @@
#define NOMINMAX
#endif
#include "Core/EditorShellAsset.h"
#include <XCEditor/Core/UIEditorCommandDispatcher.h>
#include <XCEditor/Core/UIEditorMenuModel.h>
#include <XCEditor/Core/UIEditorShellInteraction.h>
@@ -40,11 +42,16 @@ using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::AppendUIEditorShellInteraction;
using XCEngine::UI::Editor::BuildDefaultEditorShellAsset;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
using XCEngine::UI::Editor::BuildEditorShellShortcutManager;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
using XCEngine::UI::Editor::EditorShellAsset;
using XCEngine::UI::Editor::EditorShellAssetValidationResult;
using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationFrame;
using XCEngine::UI::Editor::GetUIEditorCommandDispatchStatusName;
using XCEngine::UI::Editor::UpdateUIEditorShellInteraction;
@@ -64,6 +71,7 @@ using XCEngine::UI::Editor::UIEditorShellInteractionFrame;
using XCEngine::UI::Editor::UIEditorShellInteractionResult;
using XCEngine::UI::Editor::UIEditorShellInteractionServices;
using XCEngine::UI::Editor::UIEditorShellInteractionState;
using XCEngine::UI::Editor::UIEditorShortcutManager;
using XCEngine::UI::Editor::UIEditorViewportInputBridgeFrame;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
using XCEngine::UI::Editor::UIEditorWorkspaceController;
@@ -71,6 +79,7 @@ using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
using XCEngine::UI::Editor::ValidateEditorShellAsset;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
using XCEngine::UI::Widgets::UIPopupDismissReason;
@@ -380,6 +389,59 @@ UIEditorMenuModel BuildMenuModel() {
return model;
}
EditorShellAsset BuildScenarioShellAsset() {
EditorShellAsset asset = BuildDefaultEditorShellAsset(ResolveRepoRootPath());
asset.panelRegistry = BuildPanelRegistry();
asset.workspace = BuildWorkspace();
asset.workspaceSession =
BuildDefaultUIEditorWorkspaceSession(asset.panelRegistry, asset.workspace);
asset.shellDefinition = {};
asset.shellDefinition.menuModel = BuildMenuModel();
asset.shellDefinition.statusSegments = {
UIEditorStatusBarSegment{
"mode",
"Shell Contract",
UIEditorStatusBarSlot::Leading,
{},
true,
true,
122.0f
},
UIEditorStatusBarSegment{
"active",
asset.workspace.activePanelId.empty()
? std::string("(none)")
: asset.workspace.activePanelId,
UIEditorStatusBarSlot::Trailing,
{},
true,
true,
120.0f
}
};
asset.shellDefinition.workspacePresentations.reserve(asset.panelRegistry.panels.size());
for (const auto& descriptor : asset.panelRegistry.panels) {
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 Interaction";
presentation.viewportShellModel.spec.chrome.showTopBar = true;
presentation.viewportShellModel.spec.chrome.showBottomBar = true;
presentation.viewportShellModel.frame.hasTexture = false;
presentation.viewportShellModel.frame.statusText =
"这里只验证 Editor 根壳交互,不接旧 editor 业务面板。";
}
asset.shellDefinition.workspacePresentations.push_back(std::move(presentation));
}
asset.shortcutAsset.commandRegistry = BuildCommandRegistry();
return asset;
}
class ScenarioApp {
public:
int Run(HINSTANCE hInstance, int nCmdShow);
@@ -406,9 +468,11 @@ private:
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
EditorShellAsset m_shellAsset = {};
EditorShellAssetValidationResult m_assetValidation = {};
UIEditorWorkspaceController m_controller = {};
UIEditorCommandDispatcher m_commandDispatcher = {};
UIEditorMenuModel m_menuModel = {};
UIEditorShortcutManager m_shortcutManager = {};
UIEditorShellInteractionServices m_shellServices = {};
UIEditorShellInteractionState m_interactionState = {};
UIEditorShellInteractionFrame m_cachedFrame = {};
std::vector<UIInputEvent> m_pendingInputEvents = {};
@@ -623,12 +687,26 @@ void ScenarioApp::ResetScenario() {
if (GetCapture() == m_hwnd) {
ReleaseCapture();
}
m_controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
m_commandDispatcher = UIEditorCommandDispatcher(BuildCommandRegistry());
m_menuModel = BuildMenuModel();
m_shellAsset = BuildScenarioShellAsset();
m_shellAsset.captureRootPath = m_captureRoot;
m_assetValidation = ValidateEditorShellAsset(m_shellAsset);
m_controller = UIEditorWorkspaceController(
m_shellAsset.panelRegistry,
m_shellAsset.workspace,
m_shellAsset.workspaceSession);
m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset);
m_shellServices = {};
m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher();
m_shellServices.shortcutManager = &m_shortcutManager;
m_interactionState = {};
m_cachedFrame = {};
m_pendingInputEvents.clear();
if (!m_assetValidation.IsValid()) {
m_lastStatus = "Invalid";
m_lastMessage = "场景 asset contract 非法: " + m_assetValidation.message;
m_lastColor = kDanger;
return;
}
m_lastStatus = "Ready";
m_lastMessage = "等待交互。这里只验证 Editor 根壳统一交互 contract不接旧 editor 业务。";
m_lastColor = kWarning;
@@ -732,42 +810,14 @@ void ScenarioApp::ApplyHostCaptureRequests(const UIEditorShellInteractionResult&
}
UIEditorShellInteractionDefinition ScenarioApp::BuildInteractionDefinition() const {
UIEditorShellInteractionDefinition definition = {};
definition.menuModel = m_menuModel;
definition.statusSegments = {
UIEditorStatusBarSegment{
"mode",
"Shell Contract",
UIEditorStatusBarSlot::Leading,
{},
true,
true,
122.0f
},
UIEditorStatusBarSegment{
"active",
m_controller.GetWorkspace().activePanelId.empty()
UIEditorShellInteractionDefinition definition = m_shellAsset.shellDefinition;
for (UIEditorStatusBarSegment& segment : definition.statusSegments) {
if (segment.segmentId == "active") {
segment.label = m_controller.GetWorkspace().activePanelId.empty()
? std::string("(none)")
: m_controller.GetWorkspace().activePanelId,
UIEditorStatusBarSlot::Trailing,
{},
true,
true,
120.0f
: m_controller.GetWorkspace().activePanelId;
}
};
UIEditorWorkspacePanelPresentationModel presentation = {};
presentation.panelId = "scene";
presentation.kind = UIEditorPanelPresentationKind::ViewportShell;
presentation.viewportShellModel.spec.chrome.title = "Scene";
presentation.viewportShellModel.spec.chrome.subtitle = "Editor Shell Interaction";
presentation.viewportShellModel.spec.chrome.showTopBar = true;
presentation.viewportShellModel.spec.chrome.showBottomBar = true;
presentation.viewportShellModel.frame.hasTexture = false;
presentation.viewportShellModel.frame.statusText =
"这里只验证 Editor 根壳交互,不接旧 editor 业务面板。";
definition.workspacePresentations = { presentation };
}
return definition;
}
@@ -783,7 +833,7 @@ void ScenarioApp::SetInteractionResult(const UIEditorShellInteractionResult& res
if (result.commandTriggered) {
m_lastStatus = "Triggered";
m_lastMessage = "命令已命中,但当前场景没有把 dispatcher 接到 root shell";
m_lastMessage = "命令已命中,但本帧没有完成 dispatch。请检查 shell services / asset contract";
m_lastColor = kWarning;
return;
}
@@ -866,15 +916,13 @@ void ScenarioApp::SetDispatchResult(const UIEditorCommandDispatchResult& result)
void ScenarioApp::RenderFrame() {
UpdateLayout();
const UIEditorShellInteractionDefinition definition = BuildInteractionDefinition();
UIEditorShellInteractionServices services = {};
services.commandDispatcher = &m_commandDispatcher;
m_cachedFrame = UpdateUIEditorShellInteraction(
m_interactionState,
m_controller,
m_shellRect,
definition,
m_pendingInputEvents,
services);
m_shellServices);
m_pendingInputEvents.clear();
ApplyHostCaptureRequests(m_cachedFrame.result);
SetInteractionResult(m_cachedFrame.result);
@@ -916,6 +964,11 @@ 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(
"Asset Validation",
m_assetValidation.IsValid() ? "OK" : m_assetValidation.message,
m_assetValidation.IsValid() ? kSuccess : kDanger,
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);

View File

@@ -38,8 +38,9 @@ TEST(EditorShellAssetValidationTest, DefaultShellAssetPassesValidation) {
ASSERT_EQ(
shellAsset.shellDefinition.workspacePresentations.size(),
shellAsset.panelRegistry.panels.size());
ASSERT_EQ(shellAsset.shellDefinition.statusSegments.size(), 1u);
ASSERT_EQ(shellAsset.shellDefinition.statusSegments.size(), 2u);
EXPECT_EQ(shellAsset.shellDefinition.statusSegments.front().label, "Editor Shell");
EXPECT_EQ(shellAsset.shellDefinition.statusSegments.back().label, "editor-foundation-root");
EXPECT_EQ(
shellAsset.shellDefinition.workspacePresentations.front().panelId,
"editor-foundation-root");