Refine project panel runtime ownership

This commit is contained in:
2026-04-28 15:24:47 +08:00
parent cd166037bf
commit 02eafc2bac
5 changed files with 56 additions and 71 deletions

View File

@@ -29,6 +29,7 @@
- `EditorWorkspacePanelRuntimeSet` 托管产品面板生命周期。新增 panel 时先改 `EditorProductManifest.*`,再按需要调整 `BuildEditorWorkspaceModel()` 的默认布局和测试;不要再手工在 registry / menu / runtime set / viewport host 多处分别补定义。
- `EditorSelectionService` 是 hierarchy/project/inspector/scene viewport 之间的选择同步核心。不要在单个 panel 内维护另一套长期选择真相。
- `EditorProjectRuntime` 包装 `ProjectBrowserModel`,负责 project tree/grid、选择、文件操作、scene asset open request。文件系统改动后要刷新并 revalidate selection。
- `ProjectPanel` 只消费由 `EditorContext` / `EditorPanelServices` 提供的 `EditorProjectRuntime`,不再拥有或初始化自己的 project runtime。测试也应显式创建 `EditorProjectRuntime` 并通过 `SetProjectRuntime()` 注入,不要把 `projectRoot` 传给 panel。
- `EditorSceneRuntime` 负责 startup scene、editor scene camera、hierarchy selection、component list、transform edit history 和 scene tool state。
当前目录地图:
@@ -118,6 +119,7 @@ ctest --test-dir build -C Debug -R "editor|xceditor" --output-on-failure
- 已把 `game` panel 明确标成 placeholder viewport而不是隐式共享 scene renderer 或假装 Game runtime 已完成。
- 已新增 manifest validation 测试,确保产品 manifest 能声明 panel runtime owner 和 viewport renderer owner并覆盖 `game` placeholder 的预期状态。
- 已更新根 `AGENT.md` 和本文件,去掉旧 `--project` 说法,记录当前 XCUI editor、`XCEditorCore` 分层和 product manifest 规则。
- 已移除 `ProjectPanel` 自持 `EditorProjectRuntime` 的 fallback 路径project runtime 事实源现在只来自 `EditorContext`,面板测试改为显式 runtime 注入。
- 本次改动验证过:
- `cmake --build build --config Debug --target XCEditor`
- `cmake --build build --config Debug --target editor_app_core_tests`

View File

@@ -15,7 +15,6 @@
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
#include <functional>
#include <memory>
#include <utility>
namespace XCEngine::UI::Editor::App {
@@ -342,28 +341,16 @@ Widgets::UIEditorMenuPopupState PreserveContextMenuWidgetState(
} // namespace
EditorProjectRuntime* ProjectPanel::ResolveProjectRuntime() {
return m_projectRuntime != nullptr
? m_projectRuntime
: m_ownedProjectRuntime.get();
}
const EditorProjectRuntime* ProjectPanel::ResolveProjectRuntime() const {
return m_projectRuntime != nullptr
? m_projectRuntime
: m_ownedProjectRuntime.get();
}
bool ProjectPanel::HasProjectRuntime() const {
return ResolveProjectRuntime() != nullptr;
return m_projectRuntime != nullptr;
}
ProjectPanel::BrowserModel& ProjectPanel::GetBrowserModel() {
return ResolveProjectRuntime()->GetBrowserModel();
return m_projectRuntime->GetBrowserModel();
}
const ProjectPanel::BrowserModel& ProjectPanel::GetBrowserModel() const {
return ResolveProjectRuntime()->GetBrowserModel();
return m_projectRuntime->GetBrowserModel();
}
void ProjectPanel::RebuildWindowTreeItems() {
@@ -400,12 +387,6 @@ ProjectPanel::GetPresentedWindowTreeExpansionModel() const {
return ResolveUIEditorFilterableTreeHostExpansionModel(m_treeFilterHostFrame);
}
void ProjectPanel::Initialize(const std::filesystem::path& projectRoot) {
m_ownedProjectRuntime = std::make_unique<EditorProjectRuntime>();
m_ownedProjectRuntime->Initialize(projectRoot);
SyncSelectionsFromRuntime();
}
void ProjectPanel::SetProjectRuntime(EditorProjectRuntime* projectRuntime) {
if (m_projectRuntime == projectRuntime) {
return;
@@ -478,16 +459,14 @@ const std::vector<ProjectPanel::Event>& ProjectPanel::GetFrameEvents() const {
}
const ProjectPanel::FolderEntry* ProjectPanel::FindFolderEntry(std::string_view itemId) const {
const EditorProjectRuntime* runtime = ResolveProjectRuntime();
return runtime != nullptr
? runtime->FindFolderEntry(itemId)
return m_projectRuntime != nullptr
? m_projectRuntime->FindFolderEntry(itemId)
: nullptr;
}
const ProjectPanel::AssetEntry* ProjectPanel::FindAssetEntry(std::string_view itemId) const {
const EditorProjectRuntime* runtime = ResolveProjectRuntime();
return runtime != nullptr
? runtime->FindAssetEntry(itemId)
return m_projectRuntime != nullptr
? m_projectRuntime->FindAssetEntry(itemId)
: nullptr;
}
@@ -498,15 +477,14 @@ ProjectPanel::AssetCommandTarget ProjectPanel::ResolveAssetCommandTarget(
return {};
}
return ResolveProjectRuntime()->ResolveAssetCommandTarget(
return m_projectRuntime->ResolveAssetCommandTarget(
explicitItemId,
forceCurrentFolder);
}
const ProjectPanel::AssetEntry* ProjectPanel::GetSelectedAssetEntry() const {
const EditorProjectRuntime* runtime = ResolveProjectRuntime();
return runtime != nullptr && runtime->HasSelection()
? runtime->FindAssetEntry(runtime->GetSelection().itemId)
return m_projectRuntime != nullptr && m_projectRuntime->HasSelection()
? m_projectRuntime->FindAssetEntry(m_projectRuntime->GetSelection().itemId)
: nullptr;
}
@@ -666,7 +644,7 @@ void ProjectPanel::UpdateRenameSession(
std::string renamedItemId = {};
if (m_renameFrame.result.valueChanged &&
!ResolveProjectRuntime()->RenameItem(
!m_projectRuntime->RenameItem(
m_renameFrame.result.itemId,
m_renameFrame.result.valueAfter,
&renamedItemId)) {
@@ -704,7 +682,7 @@ std::optional<ProjectPanel::EditCommandTarget> ProjectPanel::ResolveEditCommandT
return std::nullopt;
}
return ResolveProjectRuntime()->ResolveEditCommandTarget(
return m_projectRuntime->ResolveEditCommandTarget(
explicitItemId,
forceCurrentFolder);
}
@@ -737,14 +715,13 @@ void ProjectPanel::SyncSelectionsFromRuntime() {
}
void ProjectPanel::SyncAssetSelectionFromRuntime() {
const EditorProjectRuntime* runtime = ResolveProjectRuntime();
if (runtime == nullptr || !runtime->HasSelection()) {
if (m_projectRuntime == nullptr || !m_projectRuntime->HasSelection()) {
m_assetSelection.ClearSelection();
return;
}
if (FindAssetEntry(runtime->GetSelection().itemId) != nullptr) {
m_assetSelection.SetSelection(runtime->GetSelection().itemId);
if (FindAssetEntry(m_projectRuntime->GetSelection().itemId) != nullptr) {
m_assetSelection.SetSelection(m_projectRuntime->GetSelection().itemId);
return;
}
@@ -862,7 +839,7 @@ void ProjectPanel::RebuildBrowserScrollLayout() {
}
bool ProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) {
if (!ResolveProjectRuntime()->NavigateToFolder(itemId)) {
if (!m_projectRuntime->NavigateToFolder(itemId)) {
return false;
}
@@ -883,7 +860,7 @@ bool ProjectPanel::OpenProjectItem(std::string_view itemId, EventSource source)
}
if (asset->directory) {
const bool navigated = ResolveProjectRuntime()->OpenItem(asset->itemId);
const bool navigated = m_projectRuntime->OpenItem(asset->itemId);
if (navigated && HasValidBounds(m_layout.bounds)) {
SyncSelectionsFromRuntime();
RebuildPanelLayout(m_layout.bounds);
@@ -901,7 +878,7 @@ bool ProjectPanel::OpenProjectItem(std::string_view itemId, EventSource source)
return navigated;
}
if (!ResolveProjectRuntime()->OpenItem(asset->itemId)) {
if (!m_projectRuntime->OpenItem(asset->itemId)) {
return false;
}
@@ -1365,7 +1342,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand(
m_hoveredAssetItemId.clear();
m_lastPrimaryClickedAssetId = std::string(createdItemId);
m_lastPrimaryClickTime = {};
ResolveProjectRuntime()->SetSelection(createdItemId);
m_projectRuntime->SetSelection(createdItemId);
SyncSelectionsFromRuntime();
const AssetEntry* createdAsset = FindAssetEntry(createdItemId);
@@ -1388,7 +1365,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand(
}
std::string createdFolderId = {};
if (!ResolveProjectRuntime()->CreateFolder(
if (!m_projectRuntime->CreateFolder(
target.containerFolder->itemId,
"New Folder",
&createdFolderId)) {
@@ -1425,7 +1402,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand(
}
std::string createdItemId = {};
if (!ResolveProjectRuntime()->CreateMaterial(
if (!m_projectRuntime->CreateMaterial(
target.containerFolder->itemId,
"New Material",
&createdItemId)) {
@@ -1588,8 +1565,8 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchEditCommand(
if (commandId == "edit.delete") {
const std::string previousCurrentFolderId = GetBrowserModel().GetCurrentFolderId();
const bool hadAssetSelection = ResolveProjectRuntime()->HasSelection();
if (!ResolveProjectRuntime()->DeleteItem(target->itemId)) {
const bool hadAssetSelection = m_projectRuntime->HasSelection();
if (!m_projectRuntime->DeleteItem(target->itemId)) {
return BuildDispatchResult(false, "Failed to delete the selected project item.");
}
@@ -1606,7 +1583,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchEditCommand(
m_browserScrollFrame.layout.contentRect,
m_browserVerticalOffset);
}
if (hadAssetSelection && !ResolveProjectRuntime()->HasSelection()) {
if (hadAssetSelection && !m_projectRuntime->HasSelection()) {
EmitSelectionClearedEvent(EventSource::GridPrimary);
}
if (previousCurrentFolderId != GetBrowserModel().GetCurrentFolderId()) {
@@ -1669,7 +1646,7 @@ void ProjectPanel::Update(
}
if (GetBrowserModel().GetFolderEntries().empty()) {
ResolveProjectRuntime()->Refresh();
m_projectRuntime->Refresh();
SyncSelectionsFromRuntime();
}
@@ -1837,7 +1814,7 @@ void ProjectPanel::Update(
}
return true;
}
} treeDragCallbacks{ m_folderSelection, m_folderExpansion, *ResolveProjectRuntime() };
} treeDragCallbacks{ m_folderSelection, m_folderExpansion, *m_projectRuntime };
const TreeDrag::ProcessResult treeDragResult =
TreeDrag::ProcessInputEvents(
m_treeDragState,
@@ -1849,13 +1826,13 @@ void ProjectPanel::Update(
TreeDrag::kDefaultDragThreshold,
m_splitterDragging || m_assetDragState.dragging);
if (treeDragResult.dropCommitted) {
const bool hadAssetSelection = ResolveProjectRuntime()->HasSelection();
const bool hadAssetSelection = m_projectRuntime->HasSelection();
CloseContextMenu();
ResolveProjectRuntime()->ClearSelection();
m_projectRuntime->ClearSelection();
SyncSelectionsFromRuntime();
m_hoveredAssetItemId.clear();
m_lastPrimaryClickedAssetId.clear();
if (hadAssetSelection && !ResolveProjectRuntime()->HasSelection()) {
if (hadAssetSelection && !m_projectRuntime->HasSelection()) {
EmitSelectionClearedEvent(EventSource::Tree);
}
RebuildPanelLayout(dispatchEntry.bounds);
@@ -1928,7 +1905,7 @@ void ProjectPanel::Update(
} assetDragCallbacks{
m_assetSelection,
m_folderExpansion,
*ResolveProjectRuntime(),
*m_projectRuntime,
m_layout,
GetBrowserModel().GetAssetEntries(),
[this](const UIPoint& point, DropTargetSurface* surface) {
@@ -1954,7 +1931,7 @@ void ProjectPanel::Update(
}
}
if (assetDragResult.dropCommitted) {
const bool hadAssetSelection = ResolveProjectRuntime()->HasSelection();
const bool hadAssetSelection = m_projectRuntime->HasSelection();
CloseContextMenu();
ClearRenameState();
m_hoveredAssetItemId.clear();
@@ -1966,13 +1943,13 @@ void ProjectPanel::Update(
: assetDragCallbacks.movedItemId;
if (const AssetEntry* movedAsset = FindAssetEntry(movedItemId);
movedAsset != nullptr) {
ResolveProjectRuntime()->SetSelection(movedItemId);
m_projectRuntime->SetSelection(movedItemId);
SyncSelectionsFromRuntime();
EmitEvent(EventKind::AssetSelected, EventSource::GridDrag, movedAsset);
} else {
ResolveProjectRuntime()->ClearSelection();
m_projectRuntime->ClearSelection();
SyncSelectionsFromRuntime();
if (hadAssetSelection && !ResolveProjectRuntime()->HasSelection()) {
if (hadAssetSelection && !m_projectRuntime->HasSelection()) {
EmitSelectionClearedEvent(EventSource::GridDrag);
}
}
@@ -2069,8 +2046,8 @@ void ProjectPanel::Update(
const auto& assetEntries = GetBrowserModel().GetAssetEntries();
const std::size_t hitIndex = HitTestAssetTile(event.position);
if (hitIndex >= assetEntries.size()) {
if (ResolveProjectRuntime()->HasSelection()) {
ResolveProjectRuntime()->ClearSelection();
if (m_projectRuntime->HasSelection()) {
m_projectRuntime->ClearSelection();
SyncAssetSelectionFromRuntime();
EmitSelectionClearedEvent(EventSource::Background);
}
@@ -2079,7 +2056,7 @@ void ProjectPanel::Update(
const AssetEntry& assetEntry = assetEntries[hitIndex];
const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId);
const bool selectionChanged = ResolveProjectRuntime()->SetSelection(assetEntry.itemId);
const bool selectionChanged = m_projectRuntime->SetSelection(assetEntry.itemId);
SyncAssetSelectionFromRuntime();
if (selectionChanged) {
EmitEvent(EventKind::AssetSelected, EventSource::GridPrimary, &assetEntry);
@@ -2117,7 +2094,7 @@ void ProjectPanel::Update(
const AssetEntry& assetEntry = assetEntries[hitIndex];
if (!m_assetSelection.IsSelected(assetEntry.itemId)) {
ResolveProjectRuntime()->SetSelection(assetEntry.itemId);
m_projectRuntime->SetSelection(assetEntry.itemId);
SyncAssetSelectionFromRuntime();
EmitEvent(EventKind::AssetSelected, EventSource::GridSecondary, &assetEntry);
}

View File

@@ -21,7 +21,6 @@
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
@@ -82,7 +81,6 @@ public:
bool directory = false;
};
void Initialize(const std::filesystem::path& projectRoot);
void SetProjectRuntime(EditorProjectRuntime* projectRuntime);
void SetCommandFocusService(EditorCommandFocusService* commandFocusService);
void SetSystemInteractionHost(System::SystemInteractionService* systemInteractionHost);
@@ -163,8 +161,6 @@ private:
std::vector<AssetTileLayout> assetTiles = {};
};
EditorProjectRuntime* ResolveProjectRuntime();
const EditorProjectRuntime* ResolveProjectRuntime() const;
bool HasProjectRuntime() const;
BrowserModel& GetBrowserModel();
const BrowserModel& GetBrowserModel() const;
@@ -250,7 +246,6 @@ private:
std::string_view explicitItemId,
bool forceCurrentFolder);
std::unique_ptr<EditorProjectRuntime> m_ownedProjectRuntime = {};
EditorProjectRuntime* m_projectRuntime = nullptr;
EditorCommandFocusService* m_commandFocusService = nullptr;
System::SystemInteractionService* m_systemInteractionHost = nullptr;

View File

@@ -3,6 +3,7 @@
#include <XCEngine/Rendering/Execution/DrawSettings.h>
#include <XCEngine/Rendering/Execution/FrameExecutionContext.h>
#include <XCEngine/Rendering/Builtin/BuiltinPassTypes.h>
#include <XCEngine/Rendering/FrameData/RenderSceneData.h>
#include <XCEngine/Rendering/Materials/RenderMaterialResolve.h>
#include <XCEngine/Rendering/Materials/RenderMaterialStateUtils.h>
#include <XCEngine/Rendering/RenderPass.h>

View File

@@ -143,8 +143,10 @@ UIEditorHostedPanelDispatchEntry MakeProjectDispatchEntry() {
TEST(ProjectPanelTests, CreateFolderCommandCreatesDirectoryAndQueuesRename) {
TemporaryRepo repo = {};
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ProjectPanel panel = {};
panel.Initialize(repo.Root() / "project");
panel.SetProjectRuntime(&runtime);
const UIEditorHostCommandEvaluationResult evaluation =
panel.EvaluateAssetCommand("assets.create_folder");
@@ -168,8 +170,10 @@ TEST(ProjectPanelTests, CreateFolderCommandCreatesDirectoryAndQueuesRename) {
TEST(ProjectPanelTests, CreateMaterialCommandCreatesFileAndQueuesRename) {
TemporaryRepo repo = {};
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ProjectPanel panel = {};
panel.Initialize(repo.Root() / "project");
panel.SetProjectRuntime(&runtime);
const UIEditorHostCommandEvaluationResult evaluation =
panel.EvaluateAssetCommand("assets.create_material");
@@ -191,8 +195,10 @@ TEST(ProjectPanelTests, CreateMaterialCommandCreatesFileAndQueuesRename) {
TEST(ProjectPanelTests, BackgroundContextMenuCreateFolderUsesCurrentFolder) {
TemporaryRepo repo = {};
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ProjectPanel panel = {};
panel.Initialize(repo.Root() / "project");
panel.SetProjectRuntime(&runtime);
const UIEditorHostedPanelDispatchEntry dispatchEntry = MakeProjectDispatchEntry();
panel.Update(
@@ -218,8 +224,10 @@ TEST(ProjectPanelTests, FolderContextMenuCreateFolderUsesFolderTarget) {
TemporaryRepo repo = {};
ASSERT_TRUE(std::filesystem::create_directories(repo.Root() / "project/Assets/FolderA"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ProjectPanel panel = {};
panel.Initialize(repo.Root() / "project");
panel.SetProjectRuntime(&runtime);
const UIEditorHostedPanelDispatchEntry dispatchEntry = MakeProjectDispatchEntry();
panel.Update(
@@ -283,7 +291,9 @@ TEST(ProjectPanelTests, IconServiceCanBeConfiguredBeforeRuntimeInitialization) {
FakeIconService icons = {};
panel.SetIconService(&icons);
panel.Initialize(repo.Root() / "project");
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
panel.SetProjectRuntime(&runtime);
const UIEditorHostCommandEvaluationResult evaluation =
panel.EvaluateAssetCommand("assets.create_folder");