diff --git a/docs/plan/editor-feature-colocation-framework-refactor-plan.md b/docs/plan/editor-feature-colocation-framework-refactor-plan.md deleted file mode 100644 index 4aa38d57..00000000 --- a/docs/plan/editor-feature-colocation-framework-refactor-plan.md +++ /dev/null @@ -1,181 +0,0 @@ -# XCEditor Feature Colocation And Framework Boundary Refactor Plan - -## Status - -Completed on 2026-04-30. - -This document is now the final closure record for the product-layer refactor, not a -pending migration checklist. - -## Goal - -Eliminate the old editor product split where: - -- feature UI lived under `Product/Content` -- feature registration lived under `Product/Modules` -- reusable hosted-content contracts lived under `Product/Hosting` - -That structure fragmented ownership, hid real runtime boundaries, and kept the -registry layer fatter than its name suggested. - -## Final Architecture - -```text -editor/src/Product/ - Commands/ - Core/ - Effects/ - Features/ - Utility/ - AddComponent/ - AddComponentPanel.* - AddComponentUtilityFeature.* - ColorPicker/ - ColorPickerPanel.* - ColorPickerUtilityFeature.* - Workspace/ - Console/ - ConsolePanel.* - ConsoleWorkspaceFeature.* - Game/ - GameViewportController.* - GameViewportFeature.* - GameWorkspaceFeature.* - Hierarchy/ - HierarchyModel.* - HierarchyPanel.* - HierarchyWorkspaceFeature.* - Inspector/ - Components/* - InspectorPanel.* - InspectorPresentationModel.* - InspectorSubject.* - InspectorWorkspaceFeature.* - Project/ - ProjectPanel.* - ProjectWorkspaceFeature.* - Scene/ - SceneEditCommandRoute.* - SceneViewportController.* - SceneViewportFeature.* - SceneViewportSceneOverlay.* - SceneViewportSession.* - SceneViewportToolOverlay.* - SceneViewportTransformGizmo.* - SceneViewportTransformGizmoSupport.* - SceneWorkspaceFeature.* - Framework/ - UtilityWindow/ - EditorUtilityWindowContent.h - Workspace/ - EditorWorkspaceHostedContent.h - EditorWorkspaceHostedContentSet.h/.cpp - Registry/ - EditorProductRegistry.h/.cpp - Rendering/ - Runtime/ - Diagnostics/ - Features/ - EditorFeatureComposition.h/.cpp - Shell/ - Store/ - Windowing/ - Services/ - State/ - Support/ -``` - -## Final Rules - -- `Features/*` owns feature identity, feature-local registration metadata, and feature - implementation together. -- `Framework/*` owns only stable host-facing contracts and generic hosted-content - containers. -- `Registry/*` owns only descriptor types, lookup, and validation. -- `Runtime/Features/*` owns runtime feature composition and content instantiation. -- no central `EditorWorkspacePanelIds.h` pool exists; workspace feature identity now - lives in the owning feature headers. -- obsolete `Product/Content`, `Product/Modules`, and `Product/Hosting` trees do not - exist anymore. - -## What Was Closed - -### 1. Framework Boundary - -- `Product/Hosting` was replaced by `Product/Framework`. -- hosted-content contracts were split into: - - `EditorWorkspaceHostedContent.h` - - `EditorWorkspaceHostedContentSet.h/.cpp` - - `EditorUtilityWindowContent.h` - -### 2. Feature Colocation - -- all workspace feature registration files now sit beside their owning feature UI. -- all utility feature registration files now sit beside their owning utility content. -- feature IDs were pulled back into feature-owned headers instead of a centralized - workspace panel id table. - -### 3. Thin Registry - -- `EditorProductModuleRegistry` was replaced by `EditorProductRegistry`. -- public registration API terminology now uses `Feature`, not `Module`. -- runtime-specific composition was removed from `Registry` and moved into: - - `Product/Runtime/Features/EditorFeatureComposition.*` - -### 4. Physical Cleanup - -- `Product/Content` was removed. -- `Product/Modules` was removed. -- `Product/Hosting` was removed. -- old include paths and old product registration entry points were removed from the - build. - -## Validation - -Validated during the refactor on 2026-04-30 with repeated build and smoke cycles. - -Latest validation commands: - -- `cmake --build build --config Debug --target XCEditor` -- smoke test with: - - `XCUIEDITOR_SMOKE_TEST=1` - - `XCUIEDITOR_SMOKE_TEST_DURATION_SECONDS=12` - -Latest observed smoke result: - -- `EXIT_CODE=0` -- `DURATION_SECONDS=14.25` - -## Marker Gate - -Final marker audit on 2026-04-30 found a single pending marker file: - -- `20260430-154622-394.dog` - -That file was deleted only after: - -1. obsolete product trees were physically removed -2. registry responsibilities were reduced to descriptor/lookup/validation only -3. feature identity ownership was moved back into feature-owned headers -4. the latest build succeeded -5. the latest 12-second smoke test exited cleanly - -Current marker status: - -- no `.dog` files remain in the workspace - -## Completion Criteria - -The refactor is closed because all of the following are true: - -1. `Product/Framework` replaced `Product/Hosting`. -2. `Product/Features` replaced `Product/Content` and absorbed feature-local - registration. -3. `Product/Registry` replaced `EditorProductModuleRegistry`. -4. feature behavior and feature registration are physically co-located. -5. feature identity is no longer stored in a global workspace panel id pool. -6. the registry is thin and feature-agnostic. -7. runtime feature assembly lives under `Runtime/Features`. -8. the latest `XCEditor` build passed. -9. the latest 12-second smoke test passed. -10. the `.dog` marker gate was processed after items 1-9 were satisfied. diff --git a/docs/plan/editor-windowing-runtime-boundary-refactor-plan.md b/docs/plan/editor-windowing-runtime-boundary-refactor-plan.md deleted file mode 100644 index 515f5c82..00000000 --- a/docs/plan/editor-windowing-runtime-boundary-refactor-plan.md +++ /dev/null @@ -1,171 +0,0 @@ -# XCEditor Windowing Runtime Boundary Refactor Plan - -## Goal - -Eliminate the last invalid architectural seam inside the editor windowing stack: -`Product/Core/Windowing/Contracts` and `Product/Core/Windowing/EditorWorkspaceShellRuntime` -were pretending to be reusable core abstractions, but they actually described concrete -editor runtime services and a single concrete shell runtime flow. - -The target state is: - -- `Product/Core/Windowing` contains only stable value types and transfer models. -- `Product/Runtime/Shell` owns shell-definition and workspace-shell runtime services. -- `Product/Runtime/Diagnostics` owns frame-status and workspace trace services. -- windowing runtime code binds concrete runtime services directly instead of routing - them through fake cross-layer contracts. - -## Root Problem - -The previous structure had three coupled problems: - -1. `EditorShellDefinitionProvider`, `EditorFrameStatusService`, and - `EditorFrameValidation` were not true abstraction boundaries. - They each had one editor-owned runtime implementation. -2. `EditorWorkspaceShellRuntime` was stored under `Core`, but its API was tied to - editor runtime services, shell composition, viewport runtime wiring, and per-frame - status synchronization. -3. These pseudo-contracts were threaded through constructor signatures and per-frame - update contexts, so runtime-specific behavior leaked into supposedly reusable core - types. - -This made `Core` look reusable while actually encoding editor runtime policy. - -## Target Architecture - -```text -editor/src/Product/ - Core/ - Windowing/ - EditorShellVariant.h - EditorWindowGeometry.h - EditorWindowMetrics.h - EditorWindowTransferRequests.h - EditorWindowTypes.h - Runtime/ - Diagnostics/ - EditorFrameStatusController.* - WorkspaceTraceEntry.h - Shell/ - EditorShellDefinitionService.* - EditorShellRuntime.* - EditorWorkspaceShellRuntime.h - Windowing/ - Content/ - Frame/ - Runtime/ - Workspace/ -``` - -Rules: - -- `Core/Windowing` may only host stable primitive data, geometry, metrics, enums, and - transfer request types. -- runtime services must live in `Runtime/*`, even when they are used widely. -- per-frame content contexts may carry frame data, but not service locators or fake - contracts for globally owned runtime services. -- windowing runtime objects may depend directly on concrete editor runtime services - when there is only one valid implementation. - -## Execution Stages - -### Stage 1 - Remove Fake Contracts - -- Delete `Product/Core/Windowing/Contracts/*`. -- Delete `Product/Core/Windowing/EditorFrameContracts.h`. -- remove inheritance from runtime services that were only satisfying those fake - contracts. - -Exit condition: - -- no editor runtime service inherits from `Core/Windowing/Contracts` - -Status: - -- completed - -### Stage 2 - Move Runtime Shell Ownership - -- Move `EditorWorkspaceShellRuntime` out of `Product/Core/Windowing` into - `Product/Runtime/Shell`. -- update all include paths to the runtime-owned shell boundary. - -Exit condition: - -- workspace shell runtime is owned only by `Runtime/Shell` - -Status: - -- completed - -### Stage 3 - Rewire Windowing Runtime To Concrete Services - -- bind `EditorShellDefinitionService` and `EditorFrameStatusController` directly in - `EditorShellRuntime` -- bind the same concrete services directly in `EditorWindowRuntimeController` and - `EditorWindowManager` -- remove `shellDefinitionProvider` and `frameStatusService` from - `EditorWindowContentFrameContext` -- stop passing shell-definition and frame-status pseudo-services through per-frame - window update calls - -Exit condition: - -- the frame loop no longer depends on fake contracts or contract-style service passing - -Status: - -- completed - -### Stage 4 - Physical Cleanup And Audit - -- remove the empty `Core/Windowing/Contracts` directory -- search for all old type names and include paths -- rebuild `XCEditor` -- run the required 12-second smoke test - -Exit condition: - -- no code references the removed contract layer -- `XCEditor` builds -- smoke test exits cleanly - -Status: - -- completed - -## Validation - -Validated on 2026-04-30: - -- `cmake --build build --config Debug --target XCEditor` -- smoke test: - `XCUIEDITOR_SMOKE_TEST=1` - `XCUIEDITOR_SMOKE_TEST_DURATION_SECONDS=12` - -Observed result: - -- build succeeded -- smoke process exited with `EXIT_CODE=0` -- measured smoke duration was `14.22` seconds - -## Final State - -The root architectural issue is closed when all of the following are true: - -1. `Product/Core/Windowing` contains only stable value-layer windowing types. -2. no `Contracts` directory exists under `Product/Core/Windowing`. -3. `EditorWorkspaceShellRuntime` is runtime-owned, not core-owned. -4. frame status and shell definition services are concrete runtime services, not - pseudo-interfaces. -5. windowing frame code no longer forwards fake contract services per frame. -6. build and smoke validation both pass. - -Current status: - -- all six conditions are satisfied - -## Marker Audit - -No `.dog` files are present in the workspace as of the final audit on 2026-04-30, so -there is no remaining marker file to remove for this refactor. diff --git a/docs/plan/target-architecture.md b/docs/plan/target-architecture.md new file mode 100644 index 00000000..c8dcd2b9 --- /dev/null +++ b/docs/plan/target-architecture.md @@ -0,0 +1,454 @@ +# XCEngine Target Architecture Plan + +## 1. Goal + +This document defines the target repository architecture for XCEngine. + +The primary goals are: + +- Separate runtime and pipeline responsibilities at the source and build level. +- Stop pipeline-only code from leaking into runtime targets. +- Establish a stable shared contract layer for content identifiers and cooked payload formats. +- Separate project source assets, import cache, and final cooked output. +- Reshape the repository so future refactors are driven by dependency boundaries instead of directory names. + +## 2. Core Design Rules + +### 2.1 Stage Separation + +The codebase is split by execution stage: + +- `engine/runtime`: game runtime code +- `pipeline`: asset import, compilation, cook, package, catalog generation +- `editor`: current editor application tree, kept as-is in this phase +- `programs`: executable entry points + +### 2.2 Dependency Separation + +The codebase is split by dependency direction: + +- `foundation` is the lowest layer +- `shared` contains stable cross-stage data contracts +- `runtime` depends on `foundation` and `shared` +- `pipeline` depends on `foundation` and `shared`, but must not depend on `runtime` +- current top-level `editor` remains a consumer of runtime and pipeline, but its internal restructuring is out of scope for this phase + +### 2.3 Artifact Separation + +Project data is split by lifecycle: + +- `project/Assets`: source assets +- `project/Library`: import cache and intermediate artifacts +- `project/Content`: final cooked output + +This separation is mandatory. Runtime code must not treat `Library` artifacts as its main content source. + +## 3. Target Repository Layout + +```text +XCEngine/ +├─ CMakeLists.txt +├─ cmake/ +│ ├─ presets/ +│ ├─ toolchains/ +│ └─ targets/ +├─ engine/ +│ ├─ foundation/ +│ │ ├─ base/ +│ │ ├─ containers/ +│ │ ├─ memory/ +│ │ ├─ threading/ +│ │ ├─ diagnostics/ +│ │ ├─ filesystem/ +│ │ ├─ math/ +│ │ └─ serialization/ +│ ├─ shared/ +│ │ ├─ content/ +│ │ │ ├─ AssetRef/ +│ │ │ ├─ LocalID/ +│ │ │ ├─ ResourceType/ +│ │ │ ├─ CatalogFormat/ +│ │ │ └─ PackageFormat/ +│ │ └─ cooked/ +│ │ ├─ CompiledTexture/ +│ │ ├─ CompiledMaterial/ +│ │ ├─ CompiledShader/ +│ │ ├─ CompiledModel/ +│ │ ├─ CompiledUIDocument/ +│ │ ├─ CompiledVolume/ +│ │ └─ CompiledGaussianSplat/ +│ ├─ runtime/ +│ │ ├─ platform/ +│ │ ├─ content/ +│ │ │ ├─ AssetManager/ +│ │ │ ├─ ResourceStore/ +│ │ │ ├─ AsyncLoad/ +│ │ │ ├─ Catalog/ +│ │ │ ├─ Package/ +│ │ │ └─ CookedReaders/ +│ │ ├─ resources/ +│ │ │ ├─ Texture/ +│ │ │ ├─ Mesh/ +│ │ │ ├─ Material/ +│ │ │ ├─ Shader/ +│ │ │ ├─ Model/ +│ │ │ ├─ UI/ +│ │ │ ├─ Volume/ +│ │ │ └─ GaussianSplat/ +│ │ ├─ scene/ +│ │ ├─ rendering/ +│ │ │ ├─ core/ +│ │ │ ├─ passes/ +│ │ │ ├─ pipelines/ +│ │ │ ├─ frame_graph/ +│ │ │ └─ gpu_cache/ +│ │ ├─ audio/ +│ │ ├─ physics/ +│ │ └─ scripting/ +│ └─ builtin_content/ +│ ├─ source/ +│ └─ cooked/ +├─ pipeline/ +│ ├─ core/ +│ │ ├─ SourceAssetDB/ +│ │ ├─ ArtifactDB/ +│ │ ├─ SourceIndex/ +│ │ ├─ DependencyGraph/ +│ │ ├─ BuildCache/ +│ │ ├─ BuildGraph/ +│ │ └─ ImportService/ +│ ├─ importers/ +│ │ ├─ TextureImporter/ +│ │ ├─ MaterialImporter/ +│ │ ├─ ShaderImporter/ +│ │ ├─ ModelImporter/ +│ │ ├─ UIDocumentImporter/ +│ │ ├─ VolumeImporter/ +│ │ └─ GaussianSplatImporter/ +│ ├─ compilers/ +│ │ ├─ ShaderCompiler/ +│ │ └─ UIDocumentCompiler/ +│ ├─ writers/ +│ │ ├─ ArtifactWriter/ +│ │ ├─ CatalogWriter/ +│ │ └─ PackageWriter/ +│ ├─ cook/ +│ └─ tools/ +├─ editor/ # existing editor tree retained in current phase +├─ programs/ +│ ├─ xcgame/ +│ ├─ xcassetbuild/ +│ ├─ xccook/ +│ └─ xcshadercook/ +├─ managed/ +│ ├─ runtime/ +│ └─ editor/ +├─ project/ +│ ├─ Assets/ +│ ├─ Settings/ +│ ├─ Library/ +│ │ ├─ SourceDB/ +│ │ ├─ ArtifactDB/ +│ │ ├─ Artifacts/ +│ │ ├─ ImportLogs/ +│ │ └─ ScriptAssemblies/ +│ └─ Content/ +│ ├─ Catalogs/ +│ └─ Packages/ +├─ tests/ +│ ├─ foundation/ +│ ├─ shared/ +│ ├─ runtime/ +│ ├─ pipeline/ +│ ├─ editor/ +│ └─ e2e/ +├─ docs/ +├─ tools/ +└─ third_party/ +``` + +## 4. Directory Responsibilities + +### 4.1 `engine/foundation` + +Lowest-level common code used by the whole repository. + +Typical contents: + +- primitive types +- logging and diagnostics +- string and container utilities +- math +- threading +- memory allocation +- binary/text serialization helpers +- low-level filesystem wrappers + +This layer must not know about assets, scene, rendering, editor, or pipeline stages. + +### 4.2 `engine/shared` + +Stable cross-stage contracts shared by runtime, pipeline, and editor. + +`shared/content` contains content addressing and package-level protocols: + +- `AssetRef` +- `LocalID` +- `ResourceType` +- catalog entry structures +- package entry structures +- versioning constants + +`shared/cooked` contains cooked payload definitions for each resource type: + +- compiled texture payload +- compiled shader payload +- compiled material payload +- compiled model payload +- compiled UI payload +- compiled volume payload +- compiled gaussian splat payload + +This layer must contain data contracts only. It must not contain managers, importers, loaders, caches, or build orchestration code. + +### 4.3 `engine/runtime` + +Game runtime code only. + +Key rules: + +- runtime reads cooked content +- runtime must not own reimport or build-cache logic +- runtime must not call importers or artifact builders +- runtime must not depend on `pipeline` + +Runtime content subsystem split: + +- `AssetManager`: runtime-facing load, unload, resolve, and async load entry point +- `ResourceStore`: CPU-side runtime object cache +- `AsyncLoad`: background load scheduling +- `Catalog`: cooked catalog resolution +- `Package`: cooked package access +- `CookedReaders`: parse cooked payloads into runtime objects + +Rendering split: + +- `core`: renderer base types and context +- `passes`: render passes +- `pipelines`: scene/game/render pipelines +- `frame_graph`: frame execution graph and scheduling +- `gpu_cache`: GPU-side residency and upload cache + +### 4.4 `pipeline` + +Authoring pipeline and cook system. + +`pipeline/core` responsibilities: + +- source asset database +- artifact database +- path-to-asset index +- dependency graph +- build cache +- build graph +- high-level import service + +`pipeline/importers` responsibilities: + +- source file ingestion +- asset-specific import logic +- import settings interpretation + +`pipeline/compilers` responsibilities: + +- shader compilation +- UI document compilation + +`pipeline/writers` responsibilities: + +- write Library artifacts +- write cooked catalogs +- write cooked packages + +This layer is the only place allowed to rebuild `Library` artifacts. + +### 4.5 top-level `editor` + +The editor codebase remains in its current structure for this phase. + +This plan does not prescribe a refactor of: + +- `editor/app` +- `editor/include/XCEditor` +- `editor/src/UI` +- `editor/src/Product` +- `editor/resources` +- editor-specific build target layout + +The editor remains a consumer of runtime and pipeline outputs, but its internal restructuring is not +part of the current architecture plan. + +### 4.6 `programs` + +Executable entry points. + +Examples: + +- runtime game executable +- editor executable +- asset build tool +- cook tool +- shader cook tool + +Tool `main()` functions belong here, not inside runtime or pipeline libraries. + +### 4.7 `project` + +Project-side content and generated outputs. + +- `Assets`: source assets under authoring +- `Settings`: project configuration +- `Library`: imported cache and intermediate state +- `Content`: final cooked output + +The repository root must not contain a global `generated/` directory for project outputs. + +### 4.8 `tests` + +Tests mirror architecture boundaries. + +- `foundation`: low-level base library tests +- `shared`: payload and protocol tests +- `runtime`: runtime subsystem tests +- `pipeline`: importer/build/cook tests +- `editor`: editor application and editor-engine bridge tests +- `e2e`: full flow tests across project, pipeline, and runtime + +## 5. Dependency Rules + +The dependency graph must converge to: + +```text +foundation + ^ + | +shared + ^ ^ + | | +runtime pipeline + ^ ^ + | | + +--------editor (existing structure, unchanged in this phase) + +programs -> runtime / pipeline +``` + +Mandatory constraints: + +- `pipeline` must not depend on `runtime` +- `runtime` must not depend on `pipeline` +- top-level `editor` can orchestrate both runtime and pipeline +- shared code must remain data-contract-oriented + +## 6. Build Target Plan + +Suggested target families: + +```text +xc_foundation +xc_shared_content +xc_shared_cooked + +xc_runtime_platform +xc_runtime_content +xc_runtime_resources +xc_runtime_scene +xc_runtime_rendering +xc_runtime_audio +xc_runtime_physics +xc_runtime_scripting +xc_runtime + +xc_pipeline_core +xc_pipeline_importers +xc_pipeline_compilers +xc_pipeline_writers +xc_pipeline_cook +xc_pipeline + +xcgame +xcassetbuild +xccook +xcshadercook +``` + +Build constraints: + +- shipping game links `xc_runtime` +- current editor targets remain unchanged in this phase +- asset tools link `xc_pipeline` + +## 7. Current-to-Target Mapping + +### 7.1 Content System + +Current: + +- `ResourceManager` +- `ResourceCache` +- `AsyncLoader` +- `ProjectAssetIndex` +- `AssetImportService` +- `AssetDatabase` + +Target: + +- `ResourceManager` -> `engine/runtime/content/AssetManager` +- `ResourceCache` -> `engine/runtime/content/ResourceStore` +- `AsyncLoader` -> `engine/runtime/content/AsyncLoad` +- `ProjectAssetIndex` -> `pipeline/core/SourceIndex` +- `AssetImportService` -> `pipeline/core/ImportService` +- `AssetDatabase` -> split into `pipeline/core/SourceAssetDB` and `pipeline/core/ArtifactDB` + +Critical rule: + +- runtime asset load path must stop rebuilding source artifacts + +### 7.2 Rendering Cache + +Current: + +- `RenderResourceCache` + +Target: + +- `engine/runtime/rendering/gpu_cache` + +This remains runtime code, but is explicitly recognized as GPU-side cache rather than authoring cache. + +### 7.3 Editor + +Editor restructuring is explicitly out of scope for this phase. + +The current editor source tree and editor build targets remain unchanged. + +## 8. Hard Architectural Rules + +The following rules are non-negotiable: + +1. `AssetManager` must not expose reimport, clear-library, or build-cache APIs. +2. Runtime load paths must consume cooked outputs, not `Library` internal artifacts as a long-term contract. +3. `Library` is a pipeline-owned cache, not runtime-owned content. +4. Cross-stage shared code must contain stable data contracts only. +5. New runtime and pipeline features must enter through the correct layer instead of extending the nearest existing folder. + +## 9. Migration Direction + +Refactoring should proceed in this order: + +1. Split build targets before moving many files. +2. Separate runtime content loading from pipeline import and rebuild logic. +3. Move project-generated outputs under `project/Library` and `project/Content`. +4. Align tests with new boundaries. + +This sequence matters. File moves without target and dependency separation will only recreate the current coupling under new folder names. diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index b43e8fce..9e91cfed 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -346,10 +346,11 @@ if(XCENGINE_BUILD_XCUI_EDITOR_CORE) target_include_directories(XCEditorCore PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/engine/include ${CMAKE_SOURCE_DIR}/engine/third_party/stb + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src ) xcui_editor_apply_common_target_settings(XCEditorCore PUBLIC) @@ -381,10 +382,11 @@ if(XCENGINE_BUILD_XCUI_EDITOR_CORE) target_include_directories(XCEditorRendering PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/engine/include ${CMAKE_SOURCE_DIR}/engine/third_party/stb + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src ) xcui_editor_apply_common_target_settings(XCEditorRendering PUBLIC) @@ -431,11 +433,11 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) target_include_directories(XCEditorHost PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/engine/include ${CMAKE_SOURCE_DIR}/engine/third_party/stb PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/app/Bootstrap ${CMAKE_CURRENT_SOURCE_DIR}/app/Host/D3D12 ${CMAKE_CURRENT_SOURCE_DIR}/app/Host/Win32 diff --git a/editor/src/Product/Commands/EditorCommand.cpp b/editor/src/Product/Commands/EditorCommand.cpp index ed5f45f7..0e2422e5 100644 --- a/editor/src/Product/Commands/EditorCommand.cpp +++ b/editor/src/Product/Commands/EditorCommand.cpp @@ -14,6 +14,16 @@ std::string_view GetEditorCommandKindName(EditorCommandKind kind) { return "AppendConsoleEntry"; case EditorCommandKind::SetSelection: return "SetSelection"; + case EditorCommandKind::SetCurrentProjectFolder: + return "SetCurrentProjectFolder"; + case EditorCommandKind::SetRuntimeMode: + return "SetRuntimeMode"; + case EditorCommandKind::SetSceneDocumentState: + return "SetSceneDocumentState"; + case EditorCommandKind::ReplaceWindowWorkspace: + return "ReplaceWindowWorkspace"; + case EditorCommandKind::SetWindowWorkspaceState: + return "SetWindowWorkspaceState"; case EditorCommandKind::RequestOpenUtilityWindow: return "RequestOpenUtilityWindow"; case EditorCommandKind::ConsumePendingUtilityWindowRequest: @@ -61,6 +71,53 @@ EditorCommand EditorCommand::SetSelection(App::EditorSelectionState selection) { return command; } +EditorCommand EditorCommand::SetCurrentProjectFolder( + std::string currentProjectFolderId) { + EditorCommand command = {}; + command.kind = EditorCommandKind::SetCurrentProjectFolder; + command.currentProjectFolderId = std::move(currentProjectFolderId); + return command; +} + +EditorCommand EditorCommand::SetRuntimeMode(App::EditorRuntimeMode runtimeMode) { + EditorCommand command = {}; + command.kind = EditorCommandKind::SetRuntimeMode; + command.runtimeMode = runtimeMode; + return command; +} + +EditorCommand EditorCommand::SetSceneDocumentState( + std::filesystem::path scenePath, + std::string sceneName, + bool dirty) { + EditorCommand command = {}; + command.kind = EditorCommandKind::SetSceneDocumentState; + command.sceneDocumentPath = std::move(scenePath); + command.sceneDocumentName = std::move(sceneName); + command.sceneDocumentDirty = dirty; + return command; +} + +EditorCommand EditorCommand::ReplaceWindowWorkspace( + UIEditorWindowWorkspaceSet windowWorkspace) { + EditorCommand command = {}; + command.kind = EditorCommandKind::ReplaceWindowWorkspace; + command.windowWorkspace = std::move(windowWorkspace); + return command; +} + +EditorCommand EditorCommand::SetWindowWorkspaceState( + std::string windowId, + UIEditorWorkspaceModel workspace, + UIEditorWorkspaceSession session) { + EditorCommand command = {}; + command.kind = EditorCommandKind::SetWindowWorkspaceState; + command.targetWindowId = std::move(windowId); + command.targetWindowWorkspace = std::move(workspace); + command.targetWindowSession = std::move(session); + return command; +} + EditorCommand EditorCommand::RequestOpenUtilityWindow( App::EditorUtilityWindowKind kind) { EditorCommand command = {}; diff --git a/editor/src/Product/Commands/EditorCommand.h b/editor/src/Product/Commands/EditorCommand.h index 8bf3b477..457f8394 100644 --- a/editor/src/Product/Commands/EditorCommand.h +++ b/editor/src/Product/Commands/EditorCommand.h @@ -5,6 +5,7 @@ #include #include +#include #include namespace XCEngine::UI::Editor::Product { @@ -15,6 +16,11 @@ enum class EditorCommandKind : std::uint8_t { SetStatus, AppendConsoleEntry, SetSelection, + SetCurrentProjectFolder, + SetRuntimeMode, + SetSceneDocumentState, + ReplaceWindowWorkspace, + SetWindowWorkspaceState, RequestOpenUtilityWindow, ConsumePendingUtilityWindowRequest, }; @@ -26,6 +32,15 @@ struct EditorCommand { EditorStatusState status = {}; App::EditorConsoleEntry consoleEntry = {}; App::EditorSelectionState selection = {}; + std::string currentProjectFolderId = {}; + App::EditorRuntimeMode runtimeMode = App::EditorRuntimeMode::Edit; + std::filesystem::path sceneDocumentPath = {}; + std::string sceneDocumentName = {}; + bool sceneDocumentDirty = false; + UIEditorWindowWorkspaceSet windowWorkspace = {}; + std::string targetWindowId = {}; + UIEditorWorkspaceModel targetWindowWorkspace = {}; + UIEditorWorkspaceSession targetWindowSession = {}; App::EditorUtilityWindowKind utilityWindowKind = App::EditorUtilityWindowKind::None; @@ -34,6 +49,17 @@ struct EditorCommand { static EditorCommand SetReadyStatus(); static EditorCommand AppendConsoleEntry(std::string channel, std::string message); static EditorCommand SetSelection(App::EditorSelectionState selection); + static EditorCommand SetCurrentProjectFolder(std::string currentProjectFolderId); + static EditorCommand SetRuntimeMode(App::EditorRuntimeMode runtimeMode); + static EditorCommand SetSceneDocumentState( + std::filesystem::path scenePath, + std::string sceneName, + bool dirty); + static EditorCommand ReplaceWindowWorkspace(UIEditorWindowWorkspaceSet windowWorkspace); + static EditorCommand SetWindowWorkspaceState( + std::string windowId, + UIEditorWorkspaceModel workspace, + UIEditorWorkspaceSession session); static EditorCommand RequestOpenUtilityWindow(App::EditorUtilityWindowKind kind); static EditorCommand ConsumePendingUtilityWindowRequest(); }; diff --git a/editor/src/Product/Registry/EditorProductRegistry.cpp b/editor/src/Product/Registry/EditorProductRegistry.cpp index 7ef2b5fc..0e5f886b 100644 --- a/editor/src/Product/Registry/EditorProductRegistry.cpp +++ b/editor/src/Product/Registry/EditorProductRegistry.cpp @@ -9,7 +9,13 @@ #include "Product/Features/Workspace/Project/ProjectWorkspaceFeature.h" #include "Product/Features/Workspace/Scene/SceneWorkspaceFeature.h" +#include +#include +#include + #include +#include +#include #include #include @@ -17,6 +23,11 @@ namespace XCEngine::UI::Editor::App { namespace { +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIShortcutBinding; +using XCEngine::UI::UIShortcutScope; + EditorProductRegistryValidationResult BuildValidationError( EditorProductRegistryValidationCode code, std::string message) { @@ -26,6 +37,222 @@ EditorProductRegistryValidationResult BuildValidationError( return result; } +UIEditorCommandDescriptor BuildWorkspaceCommand( + std::string commandId, + std::string displayName, + UIEditorWorkspaceCommandKind kind, + std::string panelId = {}) { + UIEditorCommandDescriptor command = {}; + command.commandId = std::move(commandId); + command.displayName = std::move(displayName); + command.kind = UIEditorCommandKind::Workspace; + command.workspaceCommand.kind = kind; + if (kind == UIEditorWorkspaceCommandKind::ResetWorkspace) { + command.workspaceCommand.panelSource = UIEditorCommandPanelSource::None; + } else { + command.workspaceCommand.panelSource = UIEditorCommandPanelSource::FixedPanelId; + command.workspaceCommand.panelId = std::move(panelId); + } + return command; +} + +UIEditorCommandDescriptor BuildHostCommand( + std::string commandId, + std::string displayName) { + UIEditorCommandDescriptor command = {}; + command.commandId = std::move(commandId); + command.displayName = std::move(displayName); + command.kind = UIEditorCommandKind::Host; + command.workspaceCommand.panelSource = UIEditorCommandPanelSource::None; + command.workspaceCommand.panelId.clear(); + return command; +} + +std::string BuildWorkspaceActivationCommandId(std::string_view panelId) { + return std::string("view.activate_") + std::string(panelId); +} + +UIEditorMenuItemDescriptor BuildCommandItem( + std::string itemId, + std::string label, + std::string commandId, + UIEditorMenuCheckedStateBinding checkedState = {}) { + UIEditorMenuItemDescriptor item = {}; + item.kind = UIEditorMenuItemKind::Command; + item.itemId = std::move(itemId); + item.label = std::move(label); + item.commandId = std::move(commandId); + item.checkedState = std::move(checkedState); + return item; +} + +UIEditorMenuItemDescriptor BuildSeparatorItem(std::string itemId) { + UIEditorMenuItemDescriptor item = {}; + item.kind = UIEditorMenuItemKind::Separator; + item.itemId = std::move(itemId); + return item; +} + +UIEditorMenuItemDescriptor BuildSubmenuItem( + std::string itemId, + std::string label, + std::vector children) { + UIEditorMenuItemDescriptor item = {}; + item.kind = UIEditorMenuItemKind::Submenu; + item.itemId = std::move(itemId); + item.label = std::move(label); + item.children = std::move(children); + return item; +} + +UIShortcutBinding BuildBinding( + std::string commandId, + std::int32_t keyCode, + bool control = false, + bool shift = false, + bool alt = false) { + UIShortcutBinding binding = {}; + binding.scope = UIShortcutScope::Global; + binding.triggerEventType = UIInputEventType::KeyDown; + binding.commandId = std::move(commandId); + binding.chord.keyCode = keyCode; + binding.chord.modifiers.control = control; + binding.chord.modifiers.shift = shift; + binding.chord.modifiers.alt = alt; + return binding; +} + +UIEditorPanelDescriptor BuildWorkspaceFeaturePanelDescriptor( + const EditorWorkspaceFeatureDescriptor& feature) { + UIEditorPanelDescriptor descriptor = {}; + descriptor.panelId = std::string(feature.panelId); + descriptor.defaultTitle = std::string(feature.defaultTitle); + descriptor.presentationKind = feature.presentationKind; + descriptor.placeholder = feature.placeholder; + descriptor.canHide = feature.canHide; + descriptor.canClose = feature.canClose; + if (descriptor.presentationKind == UIEditorPanelPresentationKind::ViewportShell) { + descriptor.viewportShellSpec.chrome.title = descriptor.defaultTitle; + descriptor.viewportShellSpec.chrome.showTopBar = + feature.showViewportTopBar; + descriptor.viewportShellSpec.chrome.showBottomBar = + feature.showViewportBottomBar; + } + return descriptor; +} + +bool TryResolveWorkspaceFeature( + std::string_view panelId, + const EditorWorkspaceFeatureDescriptor*& outFeature, + std::string* errorMessage = nullptr) { + outFeature = FindEditorWorkspaceFeature(panelId); + if (outFeature != nullptr) { + return true; + } + + if (errorMessage != nullptr) { + std::ostringstream message = {}; + message << "Default workspace layout references unknown panel id '" + << panelId << "'."; + *errorMessage = message.str(); + } + return false; +} + +bool TryBuildDefaultEditorWorkspaceModel( + UIEditorWorkspaceModel& outWorkspace, + std::string* errorMessage = nullptr) { + const EditorWorkspaceFeatureDescriptor* hierarchy = nullptr; + const EditorWorkspaceFeatureDescriptor* scene = nullptr; + const EditorWorkspaceFeatureDescriptor* game = nullptr; + const EditorWorkspaceFeatureDescriptor* inspector = nullptr; + const EditorWorkspaceFeatureDescriptor* console = nullptr; + const EditorWorkspaceFeatureDescriptor* project = nullptr; + if (!TryResolveWorkspaceFeature( + kHierarchyWorkspaceFeaturePanelId, + hierarchy, + errorMessage) || + !TryResolveWorkspaceFeature( + kSceneWorkspaceFeaturePanelId, + scene, + errorMessage) || + !TryResolveWorkspaceFeature( + kGameWorkspaceFeaturePanelId, + game, + errorMessage) || + !TryResolveWorkspaceFeature( + kInspectorWorkspaceFeaturePanelId, + inspector, + errorMessage) || + !TryResolveWorkspaceFeature( + kConsoleWorkspaceFeaturePanelId, + console, + errorMessage) || + !TryResolveWorkspaceFeature( + kProjectWorkspaceFeaturePanelId, + project, + errorMessage)) { + return false; + } + + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "workspace-root", + UIEditorWorkspaceSplitAxis::Vertical, + 0.75f, + BuildUIEditorWorkspaceSplit( + "workspace-top", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.7875f, + BuildUIEditorWorkspaceSplit( + "workspace-main", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.19047619f, + BuildUIEditorWorkspaceSingleTabStack( + "hierarchy-panel", + std::string(hierarchy->panelId), + std::string(hierarchy->defaultTitle), + hierarchy->placeholder), + BuildUIEditorWorkspaceTabStack( + "center-tabs", + { + BuildUIEditorWorkspacePanel( + "scene-panel", + std::string(scene->panelId), + std::string(scene->defaultTitle), + scene->placeholder), + BuildUIEditorWorkspacePanel( + "game-panel", + std::string(game->panelId), + std::string(game->defaultTitle), + game->placeholder) + }, + 0u)), + BuildUIEditorWorkspaceSingleTabStack( + "inspector-panel", + std::string(inspector->panelId), + std::string(inspector->defaultTitle), + inspector->placeholder)), + BuildUIEditorWorkspaceTabStack( + "bottom-tabs", + { + BuildUIEditorWorkspacePanel( + "console-panel", + std::string(console->panelId), + std::string(console->defaultTitle), + console->placeholder), + BuildUIEditorWorkspacePanel( + "project-panel", + std::string(project->panelId), + std::string(project->defaultTitle), + project->placeholder) + }, + 1u)); + workspace.activePanelId = std::string(scene->panelId); + outWorkspace = std::move(workspace); + return true; +} + } // namespace std::span GetEditorWorkspaceFeatures() { @@ -51,6 +278,226 @@ const EditorWorkspaceFeatureDescriptor* FindEditorWorkspaceFeature( return nullptr; } +UIEditorPanelRegistry BuildEditorWorkspacePanelRegistry() { + UIEditorPanelRegistry registry = {}; + registry.panels.reserve(GetEditorWorkspaceFeatures().size()); + for (const EditorWorkspaceFeatureDescriptor& feature : GetEditorWorkspaceFeatures()) { + registry.panels.push_back(BuildWorkspaceFeaturePanelDescriptor(feature)); + } + return registry; +} + +UIEditorWorkspaceModel BuildDefaultEditorWorkspaceModel() { + UIEditorWorkspaceModel workspace = {}; + std::string errorMessage = {}; + if (TryBuildDefaultEditorWorkspaceModel(workspace, &errorMessage)) { + return workspace; + } + + assert(false && "Default editor workspace layout is out of sync with the workspace registry."); + std::terminate(); +} + +std::vector BuildEditorWorkspaceShellCommands() { + std::vector commands = {}; + commands.reserve(GetEditorWorkspaceFeatures().size() + 1u); + commands.push_back( + BuildWorkspaceCommand( + std::string(kEditorResetLayoutCommandId), + "Reset Layout", + UIEditorWorkspaceCommandKind::ResetWorkspace)); + for (const EditorWorkspaceFeatureDescriptor& feature : GetEditorWorkspaceFeatures()) { + commands.push_back( + BuildWorkspaceCommand( + BuildWorkspaceActivationCommandId(feature.panelId), + std::string(feature.defaultTitle), + UIEditorWorkspaceCommandKind::ActivatePanel, + std::string(feature.panelId))); + } + return commands; +} + +UIEditorMenuDescriptor BuildEditorWorkspaceViewMenu() { + std::vector panelMenuItems = {}; + panelMenuItems.reserve(GetEditorWorkspaceFeatures().size()); + for (const EditorWorkspaceFeatureDescriptor& feature : GetEditorWorkspaceFeatures()) { + UIEditorMenuCheckedStateBinding activeBinding = { + UIEditorMenuCheckedStateSource::PanelActive, + std::string(feature.panelId) + }; + panelMenuItems.push_back( + BuildCommandItem( + std::string("view-panel-") + std::string(feature.panelId), + std::string(feature.defaultTitle), + BuildWorkspaceActivationCommandId(feature.panelId), + std::move(activeBinding))); + } + + UIEditorMenuDescriptor viewMenu = {}; + viewMenu.menuId = "view"; + viewMenu.label = "View"; + viewMenu.items = { + BuildCommandItem( + "view-reset-layout", + "Reset Layout", + std::string(kEditorResetLayoutCommandId)), + BuildSeparatorItem("view-separator-panels"), + BuildSubmenuItem( + "view-panels", + "Panels", + std::move(panelMenuItems)) + }; + return viewMenu; +} + +EditorShellCommandPreset BuildEditorShellCommandPreset() { + EditorShellCommandPreset preset = {}; + preset.commandRegistry.commands = { + BuildHostCommand("file.new_project", "New Project..."), + BuildHostCommand("file.open_project", "Open Project..."), + BuildHostCommand("file.save_project", "Save Project"), + BuildHostCommand("file.new_scene", "New Scene"), + BuildHostCommand("file.open_scene", "Open Scene"), + BuildHostCommand("file.save_scene", "Save Scene"), + BuildHostCommand("file.save_scene_as", "Save Scene As..."), + BuildHostCommand("file.exit", "Exit"), + BuildHostCommand("edit.undo", "Undo"), + BuildHostCommand("edit.redo", "Redo"), + BuildHostCommand("edit.cut", "Cut"), + BuildHostCommand("edit.copy", "Copy"), + BuildHostCommand("edit.paste", "Paste"), + BuildHostCommand("edit.duplicate", "Duplicate"), + BuildHostCommand("edit.delete", "Delete"), + BuildHostCommand("edit.rename", "Rename"), + BuildHostCommand("assets.create_folder", "Create Folder"), + BuildHostCommand("assets.create_material", "Create Material"), + BuildHostCommand("assets.copy_path", "Copy Path"), + BuildHostCommand("assets.show_in_explorer", "Show in Explorer"), + BuildHostCommand("assets.reimport_selected", "Reimport Selected Asset"), + BuildHostCommand("assets.reimport_all", "Reimport All Assets"), + BuildHostCommand("assets.clear_library", "Clear Library"), + BuildHostCommand("run.play", "Play"), + BuildHostCommand("run.pause", "Pause"), + BuildHostCommand("run.step", "Step"), + BuildHostCommand("scripts.rebuild", "Rebuild Script Assemblies"), + BuildHostCommand("help.about", "About"), + }; + + const std::vector workspaceCommands = + BuildEditorWorkspaceShellCommands(); + preset.commandRegistry.commands.insert( + preset.commandRegistry.commands.end(), + workspaceCommands.begin(), + workspaceCommands.end()); + + UIEditorMenuDescriptor fileMenu = {}; + fileMenu.menuId = "file"; + fileMenu.label = "File"; + fileMenu.items = { + BuildCommandItem("file-new-project", {}, "file.new_project"), + BuildCommandItem("file-open-project", {}, "file.open_project"), + BuildCommandItem("file-save-project", {}, "file.save_project"), + BuildSeparatorItem("file-separator-project"), + BuildCommandItem("file-new-scene", {}, "file.new_scene"), + BuildCommandItem("file-open-scene", {}, "file.open_scene"), + BuildCommandItem("file-save-scene", {}, "file.save_scene"), + BuildCommandItem("file-save-scene-as", {}, "file.save_scene_as"), + BuildSeparatorItem("file-separator-exit"), + BuildCommandItem("file-exit", {}, "file.exit") + }; + + UIEditorMenuDescriptor editMenu = {}; + editMenu.menuId = "edit"; + editMenu.label = "Edit"; + editMenu.items = { + BuildCommandItem("edit-undo", {}, "edit.undo"), + BuildCommandItem("edit-redo", {}, "edit.redo"), + BuildSeparatorItem("edit-separator-history"), + BuildCommandItem("edit-cut", {}, "edit.cut"), + BuildCommandItem("edit-copy", {}, "edit.copy"), + BuildCommandItem("edit-paste", {}, "edit.paste"), + BuildCommandItem("edit-duplicate", {}, "edit.duplicate"), + BuildCommandItem("edit-delete", {}, "edit.delete"), + BuildCommandItem("edit-rename", {}, "edit.rename") + }; + + UIEditorMenuDescriptor assetsMenu = {}; + assetsMenu.menuId = "assets"; + assetsMenu.label = "Assets"; + assetsMenu.items = { + BuildSubmenuItem( + "assets-create", + "Create", + { + BuildCommandItem("assets-create-folder", "Folder", "assets.create_folder"), + BuildCommandItem("assets-create-material", "Material", "assets.create_material") + }), + BuildSeparatorItem("assets-separator-create"), + BuildCommandItem("assets-show-in-explorer", {}, "assets.show_in_explorer"), + BuildCommandItem("assets-copy-path", {}, "assets.copy_path"), + BuildSeparatorItem("assets-separator-utility"), + BuildCommandItem("assets-reimport-selected", {}, "assets.reimport_selected"), + BuildCommandItem("assets-reimport-all", {}, "assets.reimport_all"), + BuildSeparatorItem("assets-separator-clear"), + BuildCommandItem("assets-clear-library", {}, "assets.clear_library") + }; + + UIEditorMenuDescriptor runMenu = {}; + runMenu.menuId = "run"; + runMenu.label = "Run"; + runMenu.items = { + BuildCommandItem("run-play", {}, "run.play"), + BuildCommandItem("run-pause", {}, "run.pause"), + BuildCommandItem("run-step", {}, "run.step") + }; + + UIEditorMenuDescriptor scriptsMenu = {}; + scriptsMenu.menuId = "scripts"; + scriptsMenu.label = "Scripts"; + scriptsMenu.items = { + BuildCommandItem("scripts-rebuild", {}, "scripts.rebuild") + }; + + UIEditorMenuDescriptor helpMenu = {}; + helpMenu.menuId = "help"; + helpMenu.label = "Help"; + helpMenu.items = { + BuildCommandItem("help-about", {}, "help.about") + }; + + preset.menuModel.menus = { + std::move(fileMenu), + std::move(editMenu), + std::move(assetsMenu), + std::move(runMenu), + std::move(scriptsMenu), + BuildEditorWorkspaceViewMenu(), + std::move(helpMenu) + }; + + preset.shortcutBindings = { + BuildBinding("file.new_scene", static_cast(KeyCode::N), true), + BuildBinding("file.open_scene", static_cast(KeyCode::O), true), + BuildBinding("file.save_scene", static_cast(KeyCode::S), true), + BuildBinding("file.save_scene_as", static_cast(KeyCode::S), true, true), + BuildBinding("edit.undo", static_cast(KeyCode::Z), true), + BuildBinding("edit.redo", static_cast(KeyCode::Y), true), + BuildBinding("edit.cut", static_cast(KeyCode::X), true), + BuildBinding("edit.copy", static_cast(KeyCode::C), true), + BuildBinding("edit.paste", static_cast(KeyCode::V), true), + BuildBinding("edit.duplicate", static_cast(KeyCode::D), true), + BuildBinding("edit.delete", static_cast(KeyCode::Delete)), + BuildBinding("edit.rename", static_cast(KeyCode::F2)), + BuildBinding("assets.create_folder", static_cast(KeyCode::N), true, true), + BuildBinding("run.play", static_cast(KeyCode::F5)), + BuildBinding("run.pause", static_cast(KeyCode::F6)), + BuildBinding("run.step", static_cast(KeyCode::F7)), + BuildBinding("file.exit", static_cast(KeyCode::F4), false, false, true) + }; + + return preset; +} + std::span GetEditorUtilityFeatures() { static const std::array kUtilityFeatures = { GetColorPickerUtilityFeature(), @@ -153,6 +600,85 @@ EditorProductRegistryValidationResult ValidateEditorProductRegistry() { } } + const UIEditorPanelRegistry workspacePanelRegistry = + BuildEditorWorkspacePanelRegistry(); + if (const UIEditorPanelRegistryValidationResult panelRegistryValidation = + ValidateUIEditorPanelRegistry(workspacePanelRegistry); + !panelRegistryValidation.IsValid()) { + return BuildValidationError( + EditorProductRegistryValidationCode::InvalidDefaultWorkspaceLayout, + "Workspace panel registry is invalid: " + panelRegistryValidation.message); + } + + UIEditorWorkspaceModel defaultWorkspace = {}; + std::string layoutError = {}; + if (!TryBuildDefaultEditorWorkspaceModel(defaultWorkspace, &layoutError)) { + return BuildValidationError( + EditorProductRegistryValidationCode::InvalidDefaultWorkspaceLayout, + std::move(layoutError)); + } + if (const UIEditorWorkspaceValidationResult workspaceValidation = + ValidateUIEditorWorkspace(defaultWorkspace); + !workspaceValidation.IsValid()) { + return BuildValidationError( + EditorProductRegistryValidationCode::InvalidDefaultWorkspaceLayout, + "Default workspace layout is invalid: " + workspaceValidation.message); + } + + const UIEditorCommandRegistry workspaceCommandRegistry = { + BuildEditorWorkspaceShellCommands() + }; + if (const UIEditorCommandRegistryValidationResult commandValidation = + ValidateUIEditorCommandRegistry(workspaceCommandRegistry); + !commandValidation.IsValid()) { + return BuildValidationError( + EditorProductRegistryValidationCode::InvalidDefaultWorkspaceLayout, + "Workspace command registry is invalid: " + commandValidation.message); + } + + const UIEditorMenuModel workspaceMenuModel = { + { BuildEditorWorkspaceViewMenu() } + }; + if (const UIEditorMenuModelValidationResult menuValidation = + ValidateUIEditorMenuModel( + workspaceMenuModel, + workspaceCommandRegistry); + !menuValidation.IsValid()) { + return BuildValidationError( + EditorProductRegistryValidationCode::InvalidDefaultWorkspaceLayout, + "Workspace view menu is invalid: " + menuValidation.message); + } + + const EditorShellCommandPreset shellPreset = BuildEditorShellCommandPreset(); + if (const UIEditorCommandRegistryValidationResult shellCommandValidation = + ValidateUIEditorCommandRegistry(shellPreset.commandRegistry); + !shellCommandValidation.IsValid()) { + return BuildValidationError( + EditorProductRegistryValidationCode::InvalidShellCommandPreset, + "Shell command preset is invalid: " + shellCommandValidation.message); + } + if (const UIEditorMenuModelValidationResult shellMenuValidation = + ValidateUIEditorMenuModel( + shellPreset.menuModel, + shellPreset.commandRegistry); + !shellMenuValidation.IsValid()) { + return BuildValidationError( + EditorProductRegistryValidationCode::InvalidShellCommandPreset, + "Shell command preset menu is invalid: " + shellMenuValidation.message); + } + + UIEditorShortcutManager shellShortcutManager(shellPreset.commandRegistry); + for (const UIShortcutBinding& binding : shellPreset.shortcutBindings) { + shellShortcutManager.RegisterBinding(binding); + } + if (const UIEditorShortcutManagerValidationResult shortcutValidation = + shellShortcutManager.ValidateConfiguration(); + !shortcutValidation.IsValid()) { + return BuildValidationError( + EditorProductRegistryValidationCode::InvalidShellShortcutConfiguration, + "Shell shortcut preset is invalid: " + shortcutValidation.message); + } + const std::span utilityFeatures = GetEditorUtilityFeatures(); for (std::size_t index = 0u; index < utilityFeatures.size(); ++index) { diff --git a/editor/src/Product/Registry/EditorProductRegistry.h b/editor/src/Product/Registry/EditorProductRegistry.h index c1e5e7f2..e208763b 100644 --- a/editor/src/Product/Registry/EditorProductRegistry.h +++ b/editor/src/Product/Registry/EditorProductRegistry.h @@ -3,14 +3,18 @@ #include "Product/Framework/UtilityWindow/EditorUtilityWindowContent.h" #include "Product/Framework/Workspace/EditorWorkspaceHostedContent.h" +#include +#include #include #include +#include #include #include #include #include #include +#include namespace XCEngine::UI::Editor::System { @@ -76,12 +80,15 @@ enum class EditorProductRegistryValidationCode : std::uint8_t { WorkspaceNonViewportHasViewportKind, WorkspacePlaceholderViewportMissingStatus, MissingWorkspaceContentFactory, + InvalidDefaultWorkspaceLayout, InvalidUtilityWindowKind, EmptyUtilityWindowId, EmptyUtilityWindowTitle, DuplicateUtilityWindowKind, DuplicateUtilityWindowId, MissingUtilityContentFactory, + InvalidShellCommandPreset, + InvalidShellShortcutConfiguration, }; struct EditorProductRegistryValidationResult { @@ -94,9 +101,22 @@ struct EditorProductRegistryValidationResult { } }; +struct EditorShellCommandPreset { + UIEditorCommandRegistry commandRegistry = {}; + UIEditorMenuModel menuModel = {}; + std::vector<::XCEngine::UI::UIShortcutBinding> shortcutBindings = {}; +}; + +inline constexpr std::string_view kEditorResetLayoutCommandId = "view.reset_layout"; + std::span GetEditorWorkspaceFeatures(); const EditorWorkspaceFeatureDescriptor* FindEditorWorkspaceFeature( std::string_view panelId); +UIEditorPanelRegistry BuildEditorWorkspacePanelRegistry(); +UIEditorWorkspaceModel BuildDefaultEditorWorkspaceModel(); +std::vector BuildEditorWorkspaceShellCommands(); +UIEditorMenuDescriptor BuildEditorWorkspaceViewMenu(); +EditorShellCommandPreset BuildEditorShellCommandPreset(); std::span GetEditorUtilityFeatures(); const EditorUtilityFeatureDescriptor* FindEditorUtilityFeature( diff --git a/editor/src/Product/Runtime/EditorProductRuntime.cpp b/editor/src/Product/Runtime/EditorProductRuntime.cpp index 8cda7d0a..e02c144c 100644 --- a/editor/src/Product/Runtime/EditorProductRuntime.cpp +++ b/editor/src/Product/Runtime/EditorProductRuntime.cpp @@ -50,7 +50,9 @@ bool EditorProductRuntime::Initialize( BuildInitialWindowWorkspaceSet(m_shellDefinitionService.GetShellAsset(), "main"); m_store.Reset(std::move(initialState)); m_selectionService = {}; + m_selectionService.BindStore(&m_store); m_projectRuntime.Reset(); + m_projectRuntime.BindStore(&m_store); AppendUIEditorRuntimeTrace("startup", "EditorProjectRuntime::Initialize begin"); m_projectRuntime.Initialize(runtimePaths.projectRoot); AppendUIEditorRuntimeTrace("startup", "EditorProjectRuntime::Initialize end"); @@ -67,17 +69,14 @@ bool EditorProductRuntime::Initialize( AppendUIEditorRuntimeTrace("startup", "EditorSceneRuntime::Initialize end"); m_sceneRuntime.BindSelectionService(&m_selectionService); m_runtimeCommandService.Initialize( - m_store.GetMutableSession(), + m_store, m_sceneRuntime, runtimePaths, startupScene); ResetEditorColorPickerToolState(m_colorPickerToolState); - SyncSessionFromSelectionService(); m_frameStatusController.Bind( m_store, - [this]() { - SyncSessionFromSelectionService(); - }); + {}); m_frameStatusController.Reset(); m_frameStatusController.SetReadyStatus(); m_valid = true; @@ -145,12 +144,13 @@ bool EditorProductRuntime::IsPrimaryWorkspaceWindow(std::string_view windowId) c void EditorProductRuntime::CommitWindowWorkspaceSet( UIEditorWindowWorkspaceSet windowWorkspace) { - m_store.ReplaceWindowWorkspace(std::move(windowWorkspace)); + m_store.Dispatch(Product::EditorCommand::ReplaceWindowWorkspace( + std::move(windowWorkspace))); } -bool EditorProductRuntime::MutateWindowWorkspaceState( +bool EditorProductRuntime::RunWindowWorkspaceFrame( std::string_view windowId, - UIEditorWorkspaceController& workspaceController, + UIEditorWorkspaceController& workspaceControllerDraft, const std::function& mutator, std::string& outError) { if (windowId.empty()) { @@ -163,21 +163,42 @@ bool EditorProductRuntime::MutateWindowWorkspaceState( return false; } - const bool mutated = m_store.MutateWindowWorkspaceState( - windowId, - [&workspaceController, &mutator](UIEditorWindowWorkspaceState& windowState) { - workspaceController.Rebind( - windowState.workspace, - windowState.session); - mutator(workspaceController); - }); - if (!mutated) { + const UIEditorWindowWorkspaceState* const windowState = + m_store.FindWindowWorkspaceState(windowId); + if (windowState == nullptr) { outError = "workspace mutation references unknown authoritative window '" + std::string(windowId) + "'"; return false; } + UIEditorWorkspaceController frameController = workspaceControllerDraft; + const UIEditorWorkspaceLayoutOperationResult restoreResult = + frameController.RestoreLayoutSnapshot(UIEditorWorkspaceLayoutSnapshot{ + .workspace = windowState->workspace, + .session = windowState->session, + }); + if (restoreResult.status == UIEditorWorkspaceLayoutOperationStatus::Rejected) { + outError = + "workspace mutation failed to restore authoritative state for window '" + + std::string(windowId) + "': " + restoreResult.message; + return false; + } + + mutator(frameController); + const Product::EditorStore::DispatchResult dispatchResult = + m_store.Dispatch(Product::EditorCommand::SetWindowWorkspaceState( + std::string(windowId), + frameController.GetWorkspace(), + frameController.GetSession())); + if (!dispatchResult.handled) { + outError = + "workspace mutation failed to commit authoritative state for window '" + + std::string(windowId) + "'"; + return false; + } + workspaceControllerDraft = std::move(frameController); + outError.clear(); return true; } @@ -206,11 +227,6 @@ std::optional EditorProductRuntime::ConsumeOpenUtilityW return m_store.ConsumePendingUtilityWindowRequest(); } -void EditorProductRuntime::SyncSessionFromSelectionService() { - m_store.Dispatch( - Product::EditorCommand::SetSelection(m_selectionService.GetSelection())); -} - bool EditorProductRuntime::RequestOpenSceneAsset(const std::filesystem::path& scenePath) { const bool opened = m_runtimeCommandService.RequestOpenSceneAsset(scenePath); m_frameStatusController.SetStatus( @@ -220,7 +236,6 @@ bool EditorProductRuntime::RequestOpenSceneAsset(const std::filesystem::path& sc : m_runtimeCommandService.GetLastMessage().empty() ? std::string("Failed to open scene asset.") : m_runtimeCommandService.GetLastMessage()); - SyncSessionFromSelectionService(); return opened; } diff --git a/editor/src/Product/Runtime/EditorProductRuntime.h b/editor/src/Product/Runtime/EditorProductRuntime.h index 5704ef83..02c029ba 100644 --- a/editor/src/Product/Runtime/EditorProductRuntime.h +++ b/editor/src/Product/Runtime/EditorProductRuntime.h @@ -46,9 +46,9 @@ public: std::string_view windowId) const; bool IsPrimaryWorkspaceWindow(std::string_view windowId) const; void CommitWindowWorkspaceSet(UIEditorWindowWorkspaceSet windowWorkspace); - bool MutateWindowWorkspaceState( + bool RunWindowWorkspaceFrame( std::string_view windowId, - UIEditorWorkspaceController& workspaceController, + UIEditorWorkspaceController& workspaceControllerDraft, const std::function& mutator, std::string& outError); EditorColorPickerToolState& GetColorPickerToolState(); @@ -67,8 +67,6 @@ public: void TickEditorRuntime(); private: - void SyncSessionFromSelectionService(); - EditorShellDefinitionService m_shellDefinitionService = {}; EditorFrameStatusController m_frameStatusController = {}; Product::EditorStore m_store = {}; diff --git a/editor/src/Product/Runtime/Shell/EditorShellAssetBuilder.cpp b/editor/src/Product/Runtime/Shell/EditorShellAssetBuilder.cpp index 9053573c..48faded3 100644 --- a/editor/src/Product/Runtime/Shell/EditorShellAssetBuilder.cpp +++ b/editor/src/Product/Runtime/Shell/EditorShellAssetBuilder.cpp @@ -1,29 +1,20 @@ #include "Product/Runtime/Shell/EditorShellAssetBuilder.h" -#include "Product/Features/Workspace/Console/ConsoleWorkspaceFeature.h" -#include "Product/Features/Workspace/Game/GameWorkspaceFeature.h" -#include "Product/Features/Workspace/Hierarchy/HierarchyWorkspaceFeature.h" -#include "Product/Features/Workspace/Inspector/InspectorWorkspaceFeature.h" -#include "Product/Features/Workspace/Project/ProjectWorkspaceFeature.h" -#include "Product/Features/Workspace/Scene/SceneWorkspaceFeature.h" + +#include "Product/Core/Assets/EditorIconService.h" #include "Product/Registry/EditorProductRegistry.h" -#include + +#include +#include #include #include -#include #include -#include -#include "Product/Core/Assets/EditorIconService.h" +#include namespace XCEngine::UI::Editor::App { -UIEditorPanelRegistry BuildEditorPanelRegistry(); -UIEditorWorkspaceModel BuildEditorWorkspaceModel( - const UIEditorPanelRegistry& panelRegistry); -UIEditorCommandRegistry BuildEditorCommandRegistry(); -UIEditorMenuModel BuildEditorMenuModel(); -std::vector<::XCEngine::UI::UIShortcutBinding> BuildEditorShortcutBindings(); UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition( - const UIEditorPanelRegistry& panelRegistry); + const UIEditorPanelRegistry& panelRegistry, + const UIEditorMenuModel& menuModel); std::string ResolveEditorPanelTitle( const UIEditorPanelRegistry& registry, std::string_view panelId); @@ -71,9 +62,10 @@ UIEditorWorkspacePanelPresentationModel BuildPanelPresentation( } // namespace UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition( - const UIEditorPanelRegistry& panelRegistry) { + const UIEditorPanelRegistry& panelRegistry, + const UIEditorMenuModel& menuModel) { UIEditorShellInteractionDefinition definition = {}; - definition.menuModel = BuildEditorMenuModel(); + definition.menuModel = menuModel; definition.toolbarButtons = { BuildToolbarButton("run.play", BuiltInIconKind::PlayButton), BuildToolbarButton("run.pause", BuiltInIconKind::PauseButton), @@ -103,429 +95,26 @@ std::string ResolveEditorPanelTitle( namespace XCEngine::UI::Editor::App { -namespace { - -using XCEngine::Input::KeyCode; -using XCEngine::UI::UIInputEventType; -using XCEngine::UI::UIShortcutBinding; -using XCEngine::UI::UIShortcutScope; - -UIEditorCommandDescriptor BuildHostCommand( - std::string commandId, - std::string displayName) { - UIEditorCommandDescriptor command = {}; - command.commandId = std::move(commandId); - command.displayName = std::move(displayName); - command.kind = UIEditorCommandKind::Host; - command.workspaceCommand.panelSource = UIEditorCommandPanelSource::None; - command.workspaceCommand.panelId.clear(); - return command; -} - -UIEditorCommandDescriptor BuildWorkspaceCommand( - std::string commandId, - std::string displayName, - UIEditorWorkspaceCommandKind kind, - std::string panelId = {}) { - UIEditorCommandDescriptor command = {}; - command.commandId = std::move(commandId); - command.displayName = std::move(displayName); - command.workspaceCommand.kind = kind; - if (kind == UIEditorWorkspaceCommandKind::ResetWorkspace) { - command.workspaceCommand.panelSource = UIEditorCommandPanelSource::None; - } else { - command.workspaceCommand.panelSource = UIEditorCommandPanelSource::FixedPanelId; - command.workspaceCommand.panelId = std::move(panelId); - } - return command; -} - -std::string BuildPanelActivationCommandId(std::string_view panelId) { - return std::string("view.activate_") + std::string(panelId); -} - -UIShortcutBinding BuildBinding( - std::string commandId, - std::int32_t keyCode, - bool control = false, - bool shift = false, - bool alt = false) { - UIShortcutBinding binding = {}; - binding.scope = UIShortcutScope::Global; - binding.triggerEventType = UIInputEventType::KeyDown; - binding.commandId = std::move(commandId); - binding.chord.keyCode = keyCode; - binding.chord.modifiers.control = control; - binding.chord.modifiers.shift = shift; - binding.chord.modifiers.alt = alt; - return binding; -} - -} // namespace - -UIEditorCommandRegistry BuildEditorCommandRegistry() { - UIEditorCommandRegistry registry = {}; - registry.commands = { - BuildHostCommand("file.new_project", "New Project..."), - BuildHostCommand("file.open_project", "Open Project..."), - BuildHostCommand("file.save_project", "Save Project"), - BuildHostCommand("file.new_scene", "New Scene"), - BuildHostCommand("file.open_scene", "Open Scene"), - BuildHostCommand("file.save_scene", "Save Scene"), - BuildHostCommand("file.save_scene_as", "Save Scene As..."), - BuildHostCommand("file.exit", "Exit"), - BuildHostCommand("edit.undo", "Undo"), - BuildHostCommand("edit.redo", "Redo"), - BuildHostCommand("edit.cut", "Cut"), - BuildHostCommand("edit.copy", "Copy"), - BuildHostCommand("edit.paste", "Paste"), - BuildHostCommand("edit.duplicate", "Duplicate"), - BuildHostCommand("edit.delete", "Delete"), - BuildHostCommand("edit.rename", "Rename"), - BuildHostCommand("assets.create_folder", "Create Folder"), - BuildHostCommand("assets.create_material", "Create Material"), - BuildHostCommand("assets.copy_path", "Copy Path"), - BuildHostCommand("assets.show_in_explorer", "Show in Explorer"), - BuildHostCommand("assets.reimport_selected", "Reimport Selected Asset"), - BuildHostCommand("assets.reimport_all", "Reimport All Assets"), - BuildHostCommand("assets.clear_library", "Clear Library"), - BuildHostCommand("run.play", "Play"), - BuildHostCommand("run.pause", "Pause"), - BuildHostCommand("run.step", "Step"), - BuildHostCommand("scripts.rebuild", "Rebuild Script Assemblies"), - BuildHostCommand("help.about", "About"), - BuildWorkspaceCommand( - "view.reset_layout", - "Reset Layout", - UIEditorWorkspaceCommandKind::ResetWorkspace) - }; - for (const EditorWorkspaceFeatureDescriptor& feature : GetEditorWorkspaceFeatures()) { - registry.commands.push_back( - BuildWorkspaceCommand( - BuildPanelActivationCommandId(feature.panelId), - std::string(feature.defaultTitle), - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(feature.panelId))); - } - return registry; -} - -std::vector BuildEditorShortcutBindings() { - return { - BuildBinding("file.new_scene", static_cast(KeyCode::N), true), - BuildBinding("file.open_scene", static_cast(KeyCode::O), true), - BuildBinding("file.save_scene", static_cast(KeyCode::S), true), - BuildBinding("file.save_scene_as", static_cast(KeyCode::S), true, true), - BuildBinding("edit.undo", static_cast(KeyCode::Z), true), - BuildBinding("edit.redo", static_cast(KeyCode::Y), true), - BuildBinding("edit.cut", static_cast(KeyCode::X), true), - BuildBinding("edit.copy", static_cast(KeyCode::C), true), - BuildBinding("edit.paste", static_cast(KeyCode::V), true), - BuildBinding("edit.duplicate", static_cast(KeyCode::D), true), - BuildBinding("edit.delete", static_cast(KeyCode::Delete)), - BuildBinding("edit.rename", static_cast(KeyCode::F2)), - BuildBinding("assets.create_folder", static_cast(KeyCode::N), true, true), - BuildBinding("run.play", static_cast(KeyCode::F5)), - BuildBinding("run.pause", static_cast(KeyCode::F6)), - BuildBinding("run.step", static_cast(KeyCode::F7)), - BuildBinding("file.exit", static_cast(KeyCode::F4), false, false, true) - }; -} - -} // namespace XCEngine::UI::Editor::App - -namespace XCEngine::UI::Editor::App { - -namespace { - -UIEditorPanelDescriptor BuildWorkspaceFeaturePanelDescriptor( - const EditorWorkspaceFeatureDescriptor& feature) { - UIEditorPanelDescriptor descriptor = {}; - descriptor.panelId = std::string(feature.panelId); - descriptor.defaultTitle = std::string(feature.defaultTitle); - descriptor.presentationKind = feature.presentationKind; - descriptor.placeholder = feature.placeholder; - descriptor.canHide = feature.canHide; - descriptor.canClose = feature.canClose; - if (descriptor.presentationKind == UIEditorPanelPresentationKind::ViewportShell) { - descriptor.viewportShellSpec.chrome.title = descriptor.defaultTitle; - descriptor.viewportShellSpec.chrome.showTopBar = - feature.showViewportTopBar; - descriptor.viewportShellSpec.chrome.showBottomBar = - feature.showViewportBottomBar; - } - return descriptor; -} - -const UIEditorPanelDescriptor& RequirePanelDescriptor( - const UIEditorPanelRegistry& registry, - std::string_view panelId) { - if (const UIEditorPanelDescriptor* descriptor = - FindUIEditorPanelDescriptor(registry, panelId); - descriptor != nullptr) { - return *descriptor; - } - - static const UIEditorPanelDescriptor fallback = {}; - return fallback; -} - -} // namespace - -UIEditorPanelRegistry BuildEditorPanelRegistry() { - UIEditorPanelRegistry registry = {}; - for (const EditorWorkspaceFeatureDescriptor& feature : GetEditorWorkspaceFeatures()) { - registry.panels.push_back(BuildWorkspaceFeaturePanelDescriptor(feature)); - } - return registry; -} - -UIEditorWorkspaceModel BuildEditorWorkspaceModel( - const UIEditorPanelRegistry& panelRegistry) { - const UIEditorPanelDescriptor& hierarchy = - RequirePanelDescriptor(panelRegistry, kHierarchyWorkspaceFeaturePanelId); - const UIEditorPanelDescriptor& scene = - RequirePanelDescriptor(panelRegistry, kSceneWorkspaceFeaturePanelId); - const UIEditorPanelDescriptor& game = - RequirePanelDescriptor(panelRegistry, kGameWorkspaceFeaturePanelId); - const UIEditorPanelDescriptor& inspector = - RequirePanelDescriptor(panelRegistry, kInspectorWorkspaceFeaturePanelId); - const UIEditorPanelDescriptor& console = - RequirePanelDescriptor(panelRegistry, kConsoleWorkspaceFeaturePanelId); - const UIEditorPanelDescriptor& project = - RequirePanelDescriptor(panelRegistry, kProjectWorkspaceFeaturePanelId); - - UIEditorWorkspaceModel workspace = {}; - workspace.root = BuildUIEditorWorkspaceSplit( - "workspace-root", - UIEditorWorkspaceSplitAxis::Vertical, - 0.75f, - BuildUIEditorWorkspaceSplit( - "workspace-top", - UIEditorWorkspaceSplitAxis::Horizontal, - 0.7875f, - BuildUIEditorWorkspaceSplit( - "workspace-main", - UIEditorWorkspaceSplitAxis::Horizontal, - 0.19047619f, - BuildUIEditorWorkspaceSingleTabStack( - "hierarchy-panel", - std::string(kHierarchyWorkspaceFeaturePanelId), - hierarchy.defaultTitle, - hierarchy.placeholder), - BuildUIEditorWorkspaceTabStack( - "center-tabs", - { - BuildUIEditorWorkspacePanel( - "scene-panel", - std::string(kSceneWorkspaceFeaturePanelId), - scene.defaultTitle, - scene.placeholder), - BuildUIEditorWorkspacePanel( - "game-panel", - std::string(kGameWorkspaceFeaturePanelId), - game.defaultTitle, - game.placeholder) - }, - 0u)), - BuildUIEditorWorkspaceSingleTabStack( - "inspector-panel", - std::string(kInspectorWorkspaceFeaturePanelId), - inspector.defaultTitle, - inspector.placeholder)), - BuildUIEditorWorkspaceTabStack( - "bottom-tabs", - { - BuildUIEditorWorkspacePanel( - "console-panel", - std::string(kConsoleWorkspaceFeaturePanelId), - console.defaultTitle, - console.placeholder), - BuildUIEditorWorkspacePanel( - "project-panel", - std::string(kProjectWorkspaceFeaturePanelId), - project.defaultTitle, - project.placeholder) - }, - 1u)); - workspace.activePanelId = std::string(kSceneWorkspaceFeaturePanelId); - return workspace; -} - -} // namespace XCEngine::UI::Editor::App - -namespace XCEngine::UI::Editor::App { - -namespace { - -UIEditorMenuItemDescriptor BuildCommandItem( - std::string itemId, - std::string label, - std::string commandId, - UIEditorMenuCheckedStateBinding checkedState = {}) { - UIEditorMenuItemDescriptor item = {}; - item.kind = UIEditorMenuItemKind::Command; - item.itemId = std::move(itemId); - item.label = std::move(label); - item.commandId = std::move(commandId); - item.checkedState = std::move(checkedState); - return item; -} - -UIEditorMenuItemDescriptor BuildSeparatorItem(std::string itemId) { - UIEditorMenuItemDescriptor item = {}; - item.kind = UIEditorMenuItemKind::Separator; - item.itemId = std::move(itemId); - return item; -} - -UIEditorMenuItemDescriptor BuildSubmenuItem( - std::string itemId, - std::string label, - std::vector children) { - UIEditorMenuItemDescriptor item = {}; - item.kind = UIEditorMenuItemKind::Submenu; - item.itemId = std::move(itemId); - item.label = std::move(label); - item.children = std::move(children); - return item; -} - -} // namespace - -UIEditorMenuModel BuildEditorMenuModel() { - UIEditorMenuModel model = {}; - - UIEditorMenuDescriptor fileMenu = {}; - fileMenu.menuId = "file"; - fileMenu.label = "File"; - fileMenu.items = { - BuildCommandItem("file-new-project", "New Project...", "file.new_project"), - BuildCommandItem("file-open-project", "Open Project...", "file.open_project"), - BuildCommandItem("file-save-project", "Save Project", "file.save_project"), - BuildSeparatorItem("file-separator-project"), - BuildCommandItem("file-new-scene", "New Scene", "file.new_scene"), - BuildCommandItem("file-open-scene", "Open Scene", "file.open_scene"), - BuildCommandItem("file-save-scene", "Save Scene", "file.save_scene"), - BuildCommandItem("file-save-scene-as", "Save Scene As...", "file.save_scene_as"), - BuildSeparatorItem("file-separator-exit"), - BuildCommandItem("file-exit", "Exit", "file.exit") - }; - - UIEditorMenuDescriptor editMenu = {}; - editMenu.menuId = "edit"; - editMenu.label = "Edit"; - editMenu.items = { - BuildCommandItem("edit-undo", "Undo", "edit.undo"), - BuildCommandItem("edit-redo", "Redo", "edit.redo"), - BuildSeparatorItem("edit-separator-history"), - BuildCommandItem("edit-cut", "Cut", "edit.cut"), - BuildCommandItem("edit-copy", "Copy", "edit.copy"), - BuildCommandItem("edit-paste", "Paste", "edit.paste"), - BuildCommandItem("edit-duplicate", "Duplicate", "edit.duplicate"), - BuildCommandItem("edit-delete", "Delete", "edit.delete"), - BuildCommandItem("edit-rename", "Rename", "edit.rename") - }; - - UIEditorMenuDescriptor assetsMenu = {}; - assetsMenu.menuId = "assets"; - assetsMenu.label = "Assets"; - assetsMenu.items = { - BuildSubmenuItem( - "assets-create", - "Create", - { - BuildCommandItem("assets-create-folder", "Folder", "assets.create_folder"), - BuildCommandItem("assets-create-material", "Material", "assets.create_material") - }), - BuildSeparatorItem("assets-separator-create"), - BuildCommandItem("assets-show-in-explorer", "Show in Explorer", "assets.show_in_explorer"), - BuildCommandItem("assets-copy-path", "Copy Path", "assets.copy_path"), - BuildSeparatorItem("assets-separator-utility"), - BuildCommandItem("assets-reimport-selected", "Reimport Selected Asset", "assets.reimport_selected"), - BuildCommandItem("assets-reimport-all", "Reimport All Assets", "assets.reimport_all"), - BuildSeparatorItem("assets-separator-clear"), - BuildCommandItem("assets-clear-library", "Clear Library", "assets.clear_library") - }; - - UIEditorMenuDescriptor runMenu = {}; - runMenu.menuId = "run"; - runMenu.label = "Run"; - runMenu.items = { - BuildCommandItem("run-play", "Play", "run.play"), - BuildCommandItem("run-pause", "Pause", "run.pause"), - BuildCommandItem("run-step", "Step", "run.step") - }; - - UIEditorMenuDescriptor scriptsMenu = {}; - scriptsMenu.menuId = "scripts"; - scriptsMenu.label = "Scripts"; - scriptsMenu.items = { - BuildCommandItem("scripts-rebuild", "Rebuild Script Assemblies", "scripts.rebuild") - }; - - std::vector panelMenuItems = {}; - panelMenuItems.reserve(GetEditorWorkspaceFeatures().size()); - for (const EditorWorkspaceFeatureDescriptor& panel : GetEditorWorkspaceFeatures()) { - UIEditorMenuCheckedStateBinding activeBinding = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(panel.panelId) - }; - panelMenuItems.push_back( - BuildCommandItem( - std::string("view-panel-") + std::string(panel.panelId), - std::string(panel.defaultTitle), - BuildPanelActivationCommandId(panel.panelId), - std::move(activeBinding))); - } - - UIEditorMenuDescriptor viewMenu = {}; - viewMenu.menuId = "view"; - viewMenu.label = "View"; - viewMenu.items = { - BuildCommandItem("view-reset-layout", "Reset Layout", "view.reset_layout"), - BuildSeparatorItem("view-separator-panels"), - BuildSubmenuItem( - "view-panels", - "Panels", - std::move(panelMenuItems)) - }; - - UIEditorMenuDescriptor helpMenu = {}; - helpMenu.menuId = "help"; - helpMenu.label = "Help"; - helpMenu.items = { - BuildCommandItem("help-about", "About", "help.about") - }; - - model.menus = { - std::move(fileMenu), - std::move(editMenu), - std::move(assetsMenu), - std::move(runMenu), - std::move(scriptsMenu), - std::move(viewMenu), - std::move(helpMenu) - }; - return model; -} - -} // namespace XCEngine::UI::Editor::App - -namespace XCEngine::UI::Editor::App { - - EditorShellAsset BuildEditorApplicationShellAsset(const EditorRuntimePaths& runtimePaths) { + const EditorProductRegistryValidationResult registryValidation = + ValidateEditorProductRegistry(); + if (!registryValidation.IsValid()) { + assert(false && "Editor product registry is invalid."); + std::terminate(); + } + + EditorShellCommandPreset shellPreset = BuildEditorShellCommandPreset(); EditorShellAsset asset = {}; asset.screenId = "editor.shell"; asset.captureRootPath = runtimePaths.captureRoot.lexically_normal(); - asset.panelRegistry = BuildEditorPanelRegistry(); - asset.workspace = BuildEditorWorkspaceModel(asset.panelRegistry); + asset.panelRegistry = BuildEditorWorkspacePanelRegistry(); + asset.workspace = BuildDefaultEditorWorkspaceModel(); asset.workspaceSession = BuildDefaultUIEditorWorkspaceSession(asset.panelRegistry, asset.workspace); - asset.shellDefinition = BuildBaseEditorShellDefinition(asset.panelRegistry); - asset.shortcutAsset.commandRegistry = BuildEditorCommandRegistry(); - asset.shortcutAsset.bindings = BuildEditorShortcutBindings(); + asset.shellDefinition = + BuildBaseEditorShellDefinition(asset.panelRegistry, shellPreset.menuModel); + asset.shortcutAsset.commandRegistry = std::move(shellPreset.commandRegistry); + asset.shortcutAsset.bindings = std::move(shellPreset.shortcutBindings); return asset; } @@ -562,6 +151,3 @@ UIEditorShellInteractionDefinition BuildEditorApplicationShellInteractionDefinit } } // namespace XCEngine::UI::Editor::App - - - diff --git a/editor/src/Product/Runtime/Store/EditorStore.cpp b/editor/src/Product/Runtime/Store/EditorStore.cpp index 674bf075..0b64963f 100644 --- a/editor/src/Product/Runtime/Store/EditorStore.cpp +++ b/editor/src/Product/Runtime/Store/EditorStore.cpp @@ -12,12 +12,7 @@ constexpr std::size_t kMaxConsoleEntries = 256u; bool AreSelectionStatesEqual( const App::EditorSelectionState& left, const App::EditorSelectionState& right) { - return left.kind == right.kind && - left.itemId == right.itemId && - left.displayName == right.displayName && - left.absolutePath == right.absolutePath && - left.directory == right.directory && - left.stamp == right.stamp; + return left == right; } bool AreWindowWorkspaceStatesEqual( @@ -46,6 +41,16 @@ bool AreWindowWorkspaceSetsEqual( return true; } +void CanonicalizeWindowWorkspaceState(UIEditorWindowWorkspaceState& state) { + state.workspace = CanonicalizeUIEditorWorkspaceModel(std::move(state.workspace)); +} + +void CanonicalizeWindowWorkspaceSet(UIEditorWindowWorkspaceSet& windowWorkspace) { + for (UIEditorWindowWorkspaceState& windowState : windowWorkspace.windows) { + CanonicalizeWindowWorkspaceState(windowState); + } +} + void AppendConsoleEntry( EditorState& state, App::EditorConsoleEntry entry) { @@ -70,45 +75,8 @@ const UIEditorWindowWorkspaceState* EditorStore::FindWindowWorkspaceState( return FindUIEditorWindowWorkspaceState(m_state.windowWorkspace, windowId); } -bool EditorStore::ReplaceWindowWorkspace(UIEditorWindowWorkspaceSet windowWorkspace) { - if (AreWindowWorkspaceSetsEqual(m_state.windowWorkspace, windowWorkspace)) { - return false; - } - - m_state.windowWorkspace = std::move(windowWorkspace); - SyncSessionProjections(m_state); - NotifyObservers(); - return true; -} - -bool EditorStore::MutateWindowWorkspaceState( - std::string_view windowId, - const std::function& mutator) { - if (!mutator) { - return false; - } - - UIEditorWindowWorkspaceState* const windowState = - FindMutableUIEditorWindowWorkspaceState(m_state.windowWorkspace, windowId); - if (windowState == nullptr) { - return false; - } - - const UIEditorWindowWorkspaceState previousState = *windowState; - mutator(*windowState); - if (!AreWindowWorkspaceStatesEqual(previousState, *windowState)) { - SyncSessionProjections(m_state); - NotifyObservers(); - } - - return true; -} - -App::EditorSession& EditorStore::GetMutableSession() { - return m_state.session; -} - void EditorStore::Reset(EditorState initialState) { + CanonicalizeWindowWorkspaceSet(initialState.windowWorkspace); SyncSessionProjections(initialState); m_state = std::move(initialState); NotifyObservers(); @@ -152,6 +120,63 @@ EditorStore::DispatchResult EditorStore::Dispatch(const EditorCommand& command) } break; + case EditorCommandKind::SetCurrentProjectFolder: + if (m_state.session.currentProjectFolderId != command.currentProjectFolderId) { + m_state.session.currentProjectFolderId = command.currentProjectFolderId; + result.stateChanged = true; + } + break; + + case EditorCommandKind::SetRuntimeMode: + if (m_state.session.runtimeMode != command.runtimeMode) { + m_state.session.runtimeMode = command.runtimeMode; + result.stateChanged = true; + } + break; + + case EditorCommandKind::SetSceneDocumentState: + if (m_state.session.currentScenePath != command.sceneDocumentPath || + m_state.session.currentSceneName != command.sceneDocumentName || + m_state.session.sceneDocumentDirty != command.sceneDocumentDirty) { + m_state.session.currentScenePath = command.sceneDocumentPath; + m_state.session.currentSceneName = command.sceneDocumentName; + m_state.session.sceneDocumentDirty = command.sceneDocumentDirty; + result.stateChanged = true; + } + break; + + case EditorCommandKind::ReplaceWindowWorkspace: + { + UIEditorWindowWorkspaceSet nextWindowWorkspace = command.windowWorkspace; + CanonicalizeWindowWorkspaceSet(nextWindowWorkspace); + if (!AreWindowWorkspaceSetsEqual( + m_state.windowWorkspace, + nextWindowWorkspace)) { + m_state.windowWorkspace = std::move(nextWindowWorkspace); + result.stateChanged = true; + } + } + break; + + case EditorCommandKind::SetWindowWorkspaceState: + if (UIEditorWindowWorkspaceState* const windowState = + FindMutableUIEditorWindowWorkspaceState( + m_state.windowWorkspace, + command.targetWindowId); + windowState != nullptr) { + UIEditorWindowWorkspaceState nextState = *windowState; + nextState.workspace = std::move(command.targetWindowWorkspace); + nextState.session = command.targetWindowSession; + CanonicalizeWindowWorkspaceState(nextState); + if (!AreWindowWorkspaceStatesEqual(*windowState, nextState)) { + *windowState = std::move(nextState); + result.stateChanged = true; + } + } else { + result.handled = false; + } + break; + case EditorCommandKind::RequestOpenUtilityWindow: if (command.utilityWindowKind == App::EditorUtilityWindowKind::None) { break; @@ -230,7 +255,9 @@ bool EditorStore::Unsubscribe(SubscriptionToken token) { } void EditorStore::SyncSessionProjections(EditorState& state) { - state.session.consoleEntries = state.consoleEntries; + if (state.session.currentProjectFolderId.empty()) { + state.session.currentProjectFolderId = "Assets"; + } } void EditorStore::NotifyObservers() const { diff --git a/editor/src/Product/Runtime/Store/EditorStore.h b/editor/src/Product/Runtime/Store/EditorStore.h index c91b874d..45740fc6 100644 --- a/editor/src/Product/Runtime/Store/EditorStore.h +++ b/editor/src/Product/Runtime/Store/EditorStore.h @@ -29,11 +29,6 @@ public: const UIEditorWindowWorkspaceSet& GetWindowWorkspace() const; const UIEditorWindowWorkspaceState* FindWindowWorkspaceState( std::string_view windowId) const; - bool ReplaceWindowWorkspace(UIEditorWindowWorkspaceSet windowWorkspace); - bool MutateWindowWorkspaceState( - std::string_view windowId, - const std::function& mutator); - App::EditorSession& GetMutableSession(); void Reset(EditorState initialState = {}); DispatchResult Dispatch(const EditorCommand& command); diff --git a/editor/src/Product/Runtime/Windowing/Content/EditorWorkspaceWindowContentController.cpp b/editor/src/Product/Runtime/Windowing/Content/EditorWorkspaceWindowContentController.cpp index eefcf306..9bf6bb67 100644 --- a/editor/src/Product/Runtime/Windowing/Content/EditorWorkspaceWindowContentController.cpp +++ b/editor/src/Product/Runtime/Windowing/Content/EditorWorkspaceWindowContentController.cpp @@ -150,7 +150,7 @@ EditorWindowFrameTransferRequests EditorWorkspaceWindowContentController::Update EditorWindowFrameTransferRequests transferRequests = {}; std::string mutationError = {}; - if (!m_productRuntime.MutateWindowWorkspaceState( + if (!m_productRuntime.RunWindowWorkspaceFrame( m_windowId, m_workspaceController, [&](UIEditorWorkspaceController& workspaceController) { diff --git a/editor/src/Product/Services/Project/EditorProjectRuntime.cpp b/editor/src/Product/Services/Project/EditorProjectRuntime.cpp index 4e9053c1..845675ad 100644 --- a/editor/src/Product/Services/Project/EditorProjectRuntime.cpp +++ b/editor/src/Product/Services/Project/EditorProjectRuntime.cpp @@ -1,5 +1,8 @@ #include "Product/Services/Project/EditorProjectRuntime.h" +#include "Product/Commands/EditorCommand.h" +#include "Product/Runtime/Store/EditorStore.h" + namespace XCEngine::UI::Editor::App { namespace { @@ -86,9 +89,17 @@ void EditorProjectRuntime::Reset() { bool EditorProjectRuntime::Initialize(const std::filesystem::path& projectRoot) { Reset(); m_browserModel.Initialize(projectRoot); + ProjectAuthoritativeCurrentFolderToBrowser(); + CommitProjectedCurrentFolderToStore(); return true; } +void EditorProjectRuntime::BindStore(Product::EditorStore* store) { + m_store = store; + ProjectAuthoritativeCurrentFolderToBrowser(); + CommitProjectedCurrentFolderToStore(); +} + void EditorProjectRuntime::BindSelectionService( EditorSelectionService* selectionService) { m_selectionService = @@ -96,15 +107,18 @@ void EditorProjectRuntime::BindSelectionService( } void EditorProjectRuntime::Refresh() { - m_browserModel.Refresh(); + m_browserModel.Refresh(AuthoritativeCurrentFolderId()); + CommitProjectedCurrentFolderToStore(); + ProjectAuthoritativeCurrentFolderToBrowser(); RevalidateSelection(); } const ProjectBrowserModel& EditorProjectRuntime::GetBrowserModel() const { - return m_browserModel; + return const_cast(this)->GetBrowserModel(); } ProjectBrowserModel& EditorProjectRuntime::GetBrowserModel() { + ProjectAuthoritativeCurrentFolderToBrowser(); return m_browserModel; } @@ -141,10 +155,16 @@ void EditorProjectRuntime::ClearSelection() { } bool EditorProjectRuntime::NavigateToFolder(std::string_view itemId) { - if (!m_browserModel.NavigateToFolder(itemId)) { + if (itemId.empty() || m_browserModel.FindFolderEntry(itemId) == nullptr) { return false; } + if (itemId == AuthoritativeCurrentFolderId()) { + return false; + } + + SetAuthoritativeCurrentFolderId(std::string(itemId)); + ProjectAuthoritativeCurrentFolderToBrowser(); ClearSelection(); return true; } @@ -173,12 +193,12 @@ bool EditorProjectRuntime::OpenItem(std::string_view itemId) { const ProjectBrowserModel::FolderEntry* EditorProjectRuntime::FindFolderEntry( std::string_view itemId) const { - return m_browserModel.FindFolderEntry(itemId); + return GetBrowserModel().FindFolderEntry(itemId); } const ProjectBrowserModel::AssetEntry* EditorProjectRuntime::FindAssetEntry( std::string_view itemId) const { - return m_browserModel.FindAssetEntry(itemId); + return GetBrowserModel().FindAssetEntry(itemId); } EditorProjectRuntime::AssetCommandTarget @@ -186,7 +206,7 @@ EditorProjectRuntime::ResolveAssetCommandTarget( std::string_view explicitItemId, bool forceCurrentFolder) const { AssetCommandTarget target = {}; - target.currentFolder = FindFolderEntry(m_browserModel.GetCurrentFolderId()); + target.currentFolder = FindFolderEntry(AuthoritativeCurrentFolderId()); const ProjectBrowserModel::AssetEntry* explicitAsset = explicitItemId.empty() ? nullptr : FindAssetEntry(explicitItemId); @@ -281,7 +301,7 @@ EditorProjectRuntime::ResolveEditCommandTarget( } if (const ProjectBrowserModel::FolderEntry* currentFolder = - FindFolderEntry(m_browserModel.GetCurrentFolderId()); + FindFolderEntry(AuthoritativeCurrentFolderId()); currentFolder != nullptr) { return BuildEditCommandTarget(m_browserModel, *currentFolder); } @@ -295,6 +315,8 @@ bool EditorProjectRuntime::CreateFolder( std::string* createdFolderId) { const bool created = m_browserModel.CreateFolder(parentFolderId, requestedName, createdFolderId); + CommitProjectedCurrentFolderToStore(); + ProjectAuthoritativeCurrentFolderToBrowser(); RevalidateSelection(); return created; } @@ -305,6 +327,8 @@ bool EditorProjectRuntime::CreateMaterial( std::string* createdItemId) { const bool created = m_browserModel.CreateMaterial(parentFolderId, requestedName, createdItemId); + CommitProjectedCurrentFolderToStore(); + ProjectAuthoritativeCurrentFolderToBrowser(); RevalidateSelection(); return created; } @@ -320,6 +344,8 @@ bool EditorProjectRuntime::RenameItem( return false; } + CommitProjectedCurrentFolderToStore(); + ProjectAuthoritativeCurrentFolderToBrowser(); if (SelectionTargetsItem(itemId)) { if (!renameOutput->empty() && !SetSelection(*renameOutput)) { ClearSelection(); @@ -336,6 +362,8 @@ bool EditorProjectRuntime::DeleteItem(std::string_view itemId) { return false; } + CommitProjectedCurrentFolderToStore(); + ProjectAuthoritativeCurrentFolderToBrowser(); if (selectionAffected) { ClearSelection(); } else { @@ -359,6 +387,8 @@ bool EditorProjectRuntime::MoveItemToFolder( return false; } + CommitProjectedCurrentFolderToStore(); + ProjectAuthoritativeCurrentFolderToBrowser(); if (selectionAffected) { ClearSelection(); } else { @@ -387,6 +417,8 @@ bool EditorProjectRuntime::ReparentFolder( return false; } + CommitProjectedCurrentFolderToStore(); + ProjectAuthoritativeCurrentFolderToBrowser(); if (selectionAffected) { ClearSelection(); } else { @@ -403,6 +435,8 @@ bool EditorProjectRuntime::MoveFolderToRoot( return false; } + CommitProjectedCurrentFolderToStore(); + ProjectAuthoritativeCurrentFolderToBrowser(); if (selectionAffected) { ClearSelection(); } else { @@ -419,6 +453,27 @@ const EditorSelectionService& EditorProjectRuntime::SelectionService() const { return *m_selectionService; } +bool EditorProjectRuntime::BrowserModelInitialized() const { + return !m_browserModel.GetAssetsRootPath().empty(); +} + +std::string_view EditorProjectRuntime::AuthoritativeCurrentFolderId() const { + if (m_store != nullptr) { + return m_store->GetState().session.currentProjectFolderId; + } + + return m_browserModel.GetCurrentFolderId(); +} + +void EditorProjectRuntime::SetAuthoritativeCurrentFolderId(std::string folderId) { + if (m_store != nullptr) { + m_store->Dispatch(Product::EditorCommand::SetCurrentProjectFolder(std::move(folderId))); + return; + } + + m_browserModel.SetCurrentFolderProjection(folderId); +} + void EditorProjectRuntime::ApplySelection( const ProjectBrowserModel::AssetEntry& asset) { SelectionService().SetProjectSelection( @@ -448,4 +503,27 @@ bool EditorProjectRuntime::SelectionTargetsItem(std::string_view itemId) const { IsSameOrDescendantItemId(GetSelection().itemId, itemId); } +void EditorProjectRuntime::ProjectAuthoritativeCurrentFolderToBrowser() { + if (!BrowserModelInitialized()) { + return; + } + + const std::string_view currentFolderId = AuthoritativeCurrentFolderId(); + if (!currentFolderId.empty()) { + m_browserModel.SetCurrentFolderProjection(currentFolderId); + } +} + +void EditorProjectRuntime::CommitProjectedCurrentFolderToStore() { + if (m_store == nullptr || !BrowserModelInitialized()) { + return; + } + + const std::string& projectedCurrentFolderId = m_browserModel.GetCurrentFolderId(); + if (!projectedCurrentFolderId.empty() && + projectedCurrentFolderId != m_store->GetState().session.currentProjectFolderId) { + m_store->Dispatch(Product::EditorCommand::SetCurrentProjectFolder(projectedCurrentFolderId)); + } +} + } // namespace XCEngine::UI::Editor::App diff --git a/editor/src/Product/Services/Project/EditorProjectRuntime.h b/editor/src/Product/Services/Project/EditorProjectRuntime.h index 32bc9594..d006646d 100644 --- a/editor/src/Product/Services/Project/EditorProjectRuntime.h +++ b/editor/src/Product/Services/Project/EditorProjectRuntime.h @@ -12,6 +12,12 @@ #include #include +namespace XCEngine::UI::Editor::Product { + +class EditorStore; + +} // namespace XCEngine::UI::Editor::Product + namespace XCEngine::UI::Editor::App { class EditorProjectRuntime { @@ -46,6 +52,7 @@ public: void Reset(); bool Initialize(const std::filesystem::path& projectRoot); + void BindStore(Product::EditorStore* store); void BindSelectionService(EditorSelectionService* selectionService); void Refresh(); @@ -107,11 +114,17 @@ public: private: EditorSelectionService& SelectionService(); const EditorSelectionService& SelectionService() const; + bool BrowserModelInitialized() const; + std::string_view AuthoritativeCurrentFolderId() const; + void SetAuthoritativeCurrentFolderId(std::string folderId); + void ProjectAuthoritativeCurrentFolderToBrowser(); + void CommitProjectedCurrentFolderToStore(); void ApplySelection(const ProjectBrowserModel::AssetEntry& asset); void RevalidateSelection(); bool SelectionTargetsItem(std::string_view itemId) const; ProjectBrowserModel m_browserModel = {}; + Product::EditorStore* m_store = nullptr; EditorSelectionService m_ownedSelectionService = {}; EditorSelectionService* m_selectionService = &m_ownedSelectionService; }; diff --git a/editor/src/Product/Services/Project/ProjectBrowserModel.cpp b/editor/src/Product/Services/Project/ProjectBrowserModel.cpp index 9ec49035..11c71aa0 100644 --- a/editor/src/Product/Services/Project/ProjectBrowserModel.cpp +++ b/editor/src/Product/Services/Project/ProjectBrowserModel.cpp @@ -564,9 +564,9 @@ void ProjectBrowserModel::Initialize(const std::filesystem::path& projectRoot) { std::to_string(m_assetEntries.size())); } -void ProjectBrowserModel::Refresh() { +void ProjectBrowserModel::Refresh(std::string_view preferredCurrentFolderId) { TraceProjectBrowser("ProjectBrowserModel::Refresh begin"); - RefreshBrowserState(); + RefreshBrowserState(preferredCurrentFolderId); TraceProjectBrowser( "ProjectBrowserModel::Refresh end currentFolder=" + m_currentFolderId + @@ -576,8 +576,22 @@ void ProjectBrowserModel::Refresh() { std::to_string(m_assetEntries.size())); } -void ProjectBrowserModel::RefreshBrowserState() { +bool ProjectBrowserModel::SetCurrentFolderProjection(std::string_view itemId) { + if (itemId == m_currentFolderId) { + return false; + } + + m_currentFolderId = std::string(itemId); + EnsureValidCurrentFolder(); + RefreshAssetList(); + return true; +} + +void ProjectBrowserModel::RefreshBrowserState(std::string_view preferredCurrentFolderId) { RefreshFolderTree(); + if (!preferredCurrentFolderId.empty()) { + m_currentFolderId = std::string(preferredCurrentFolderId); + } EnsureValidCurrentFolder(); RefreshAssetList(); } diff --git a/editor/src/Product/Services/Project/ProjectBrowserModel.h b/editor/src/Product/Services/Project/ProjectBrowserModel.h index 3f4699bd..7d8f142c 100644 --- a/editor/src/Product/Services/Project/ProjectBrowserModel.h +++ b/editor/src/Product/Services/Project/ProjectBrowserModel.h @@ -55,7 +55,8 @@ public: void Reset(); void Initialize(const std::filesystem::path& projectRoot); - void Refresh(); + void Refresh(std::string_view preferredCurrentFolderId = {}); + bool SetCurrentFolderProjection(std::string_view itemId); bool Empty() const; std::filesystem::path GetProjectRootPath() const; @@ -105,7 +106,7 @@ public: std::vector BuildAncestorFolderIds(std::string_view itemId) const; private: - void RefreshBrowserState(); + void RefreshBrowserState(std::string_view preferredCurrentFolderId = {}); void RefreshFolderTree(); void RefreshAssetList(); void EnsureValidCurrentFolder(); diff --git a/editor/src/Product/Services/Runtime/EditorPlayModeRuntime.cpp b/editor/src/Product/Services/Runtime/EditorPlayModeRuntime.cpp index 10e24233..3bf14be1 100644 --- a/editor/src/Product/Services/Runtime/EditorPlayModeRuntime.cpp +++ b/editor/src/Product/Services/Runtime/EditorPlayModeRuntime.cpp @@ -46,10 +46,10 @@ UIEditorHostCommandDispatchResult BuildExecutedDispatch( } // namespace void EditorPlayModeRuntime::Initialize( - EditorSession& session, + Product::EditorStore& store, EditorSceneRuntime& sceneRuntime) { Shutdown(); - m_session = &session; + m_store = &store; m_sceneRuntime = &sceneRuntime; SetRuntimeMode(EditorRuntimeMode::Edit); } @@ -57,10 +57,10 @@ void EditorPlayModeRuntime::Initialize( void EditorPlayModeRuntime::Shutdown() { m_runtimeLoop.Stop(); m_playSession.reset(); - if (m_session != nullptr) { + if (m_store != nullptr) { SetRuntimeMode(EditorRuntimeMode::Edit); } - m_session = nullptr; + m_store = nullptr; m_sceneRuntime = nullptr; m_lastFrameTickTime = {}; m_lastMessage.clear(); @@ -80,11 +80,11 @@ void EditorPlayModeRuntime::TickFrame() { } bool EditorPlayModeRuntime::IsReady() const { - return m_session != nullptr && m_sceneRuntime != nullptr; + return m_store != nullptr && m_sceneRuntime != nullptr; } bool EditorPlayModeRuntime::IsPlayModeActive() const { - return IsReady() && m_session->runtimeMode != EditorRuntimeMode::Edit; + return IsReady() && GetRuntimeMode() != EditorRuntimeMode::Edit; } bool EditorPlayModeRuntime::StopPlayMode() { @@ -122,7 +122,7 @@ UIEditorHostCommandEvaluationResult EditorPlayModeRuntime::EvaluateRunCommand( if (!IsPlayModeActive()) { return BuildDisabledResult("Play mode is not running."); } - return m_session->runtimeMode == EditorRuntimeMode::Paused + return GetRuntimeMode() == EditorRuntimeMode::Paused ? BuildExecutableResult("Resume play mode.") : BuildExecutableResult("Pause play mode."); } @@ -156,7 +156,7 @@ UIEditorHostCommandDispatchResult EditorPlayModeRuntime::DispatchRunCommand( if (commandId == "run.play") { const bool executed = IsPlayModeActive() ? StopPlayMode() : StartPlayMode(); SetLastMessage(executed - ? (m_session->runtimeMode == EditorRuntimeMode::Edit + ? (GetRuntimeMode() == EditorRuntimeMode::Edit ? std::string("Play mode stopped.") : std::string("Play mode started.")) : std::string("Failed to toggle play mode.")); @@ -166,11 +166,11 @@ UIEditorHostCommandDispatchResult EditorPlayModeRuntime::DispatchRunCommand( } if (commandId == "run.pause") { - const bool executed = m_session->runtimeMode == EditorRuntimeMode::Paused + const bool executed = GetRuntimeMode() == EditorRuntimeMode::Paused ? ResumePlayMode() : PausePlayMode(); SetLastMessage(executed - ? (m_session->runtimeMode == EditorRuntimeMode::Paused + ? (GetRuntimeMode() == EditorRuntimeMode::Paused ? std::string("Play mode paused.") : std::string("Play mode resumed.")) : std::string("Failed to toggle play mode pause.")); @@ -213,7 +213,7 @@ void EditorPlayModeRuntime::Tick(float deltaSeconds) { } if (!m_runtimeLoop.IsRunning()) { - if (m_session->runtimeMode != EditorRuntimeMode::Edit) { + if (GetRuntimeMode() != EditorRuntimeMode::Edit) { SetRuntimeMode(EditorRuntimeMode::Edit); } return; @@ -303,9 +303,17 @@ bool EditorPlayModeRuntime::StepPlayMode() { return true; } +EditorRuntimeMode EditorPlayModeRuntime::GetRuntimeMode() const { + if (m_store == nullptr) { + return EditorRuntimeMode::Edit; + } + + return m_store->GetState().session.runtimeMode; +} + void EditorPlayModeRuntime::SetRuntimeMode(EditorRuntimeMode mode) { - if (m_session != nullptr) { - m_session->runtimeMode = mode; + if (m_store != nullptr) { + m_store->Dispatch(Product::EditorCommand::SetRuntimeMode(mode)); } } diff --git a/editor/src/Product/Services/Runtime/EditorPlayModeRuntime.h b/editor/src/Product/Services/Runtime/EditorPlayModeRuntime.h index e8eeaeab..a1d320a3 100644 --- a/editor/src/Product/Services/Runtime/EditorPlayModeRuntime.h +++ b/editor/src/Product/Services/Runtime/EditorPlayModeRuntime.h @@ -1,6 +1,7 @@ #pragma once #include "Product/Commands/EditorHostCommandBridge.h" +#include "Product/Runtime/Store/EditorStore.h" #include "Product/State/EditorSession.h" #include @@ -14,12 +15,11 @@ namespace XCEngine::UI::Editor::App { class EditorSceneRuntime; class EditorScenePlaySession; -struct EditorSession; class EditorPlayModeRuntime final { public: void Initialize( - EditorSession& session, + Product::EditorStore& store, EditorSceneRuntime& sceneRuntime); void Shutdown(); @@ -43,10 +43,11 @@ private: bool PausePlayMode(); bool ResumePlayMode(); bool StepPlayMode(); + EditorRuntimeMode GetRuntimeMode() const; void SetRuntimeMode(EditorRuntimeMode mode); void SetLastMessage(std::string message); - EditorSession* m_session = nullptr; + Product::EditorStore* m_store = nullptr; EditorSceneRuntime* m_sceneRuntime = nullptr; ::XCEngine::Components::RuntimeLoop m_runtimeLoop{ ::XCEngine::Components::RuntimeLoop::Settings{} }; diff --git a/editor/src/Product/Services/Runtime/EditorRuntimeCommandService.cpp b/editor/src/Product/Services/Runtime/EditorRuntimeCommandService.cpp index cb9d6582..d88ed3f6 100644 --- a/editor/src/Product/Services/Runtime/EditorRuntimeCommandService.cpp +++ b/editor/src/Product/Services/Runtime/EditorRuntimeCommandService.cpp @@ -47,14 +47,19 @@ EditorRuntimeCommandService::~EditorRuntimeCommandService() { } void EditorRuntimeCommandService::Initialize( - EditorSession& session, + Product::EditorStore& store, EditorSceneRuntime& sceneRuntime, const EditorRuntimePaths& runtimePaths, const EditorStartupSceneResult& startupScene) { Shutdown(); m_sceneRuntime = &sceneRuntime; - m_sceneDocumentRuntime.Initialize(session, sceneRuntime, startupScene); - m_playModeRuntime.Initialize(session, sceneRuntime); + m_sceneDocumentRuntime.Initialize( + store, + sceneRuntime, + startupScene); + m_playModeRuntime.Initialize( + store, + sceneRuntime); m_scriptingRuntimeService.Initialize(runtimePaths.projectRoot); } diff --git a/editor/src/Product/Services/Runtime/EditorRuntimeCommandService.h b/editor/src/Product/Services/Runtime/EditorRuntimeCommandService.h index c7d7b202..4bce2915 100644 --- a/editor/src/Product/Services/Runtime/EditorRuntimeCommandService.h +++ b/editor/src/Product/Services/Runtime/EditorRuntimeCommandService.h @@ -2,6 +2,7 @@ #include "Product/Commands/EditorHostCommandBridge.h" #include "Product/Core/Environment/EditorRuntimePaths.h" +#include "Product/Runtime/Store/EditorStore.h" #include "Product/Services/Runtime/EditorPlayModeRuntime.h" #include "Product/Services/Runtime/EditorSceneDocumentRuntime.h" #include "Product/Services/Runtime/EditorScriptingRuntimeService.h" @@ -14,7 +15,6 @@ namespace XCEngine::UI::Editor::App { class EditorSceneRuntime; struct EditorStartupSceneResult; -struct EditorSession; class EditorRuntimeCommandService final : public EditorHostCommandBridge::RuntimeCommandOwner { @@ -25,7 +25,7 @@ public: EditorRuntimeCommandService& operator=(const EditorRuntimeCommandService&) = delete; void Initialize( - EditorSession& session, + Product::EditorStore& store, EditorSceneRuntime& sceneRuntime, const EditorRuntimePaths& runtimePaths, const EditorStartupSceneResult& startupScene); diff --git a/editor/src/Product/Services/Runtime/EditorSceneDocumentRuntime.cpp b/editor/src/Product/Services/Runtime/EditorSceneDocumentRuntime.cpp index e5a6d8d7..9c2ac3f5 100644 --- a/editor/src/Product/Services/Runtime/EditorSceneDocumentRuntime.cpp +++ b/editor/src/Product/Services/Runtime/EditorSceneDocumentRuntime.cpp @@ -58,22 +58,22 @@ std::string ResolveDocumentSceneName( } // namespace void EditorSceneDocumentRuntime::Initialize( - EditorSession& session, + Product::EditorStore& store, EditorSceneRuntime& sceneRuntime, const EditorStartupSceneResult& startupScene) { Shutdown(); - m_session = &session; + m_store = &store; m_sceneRuntime = &sceneRuntime; ApplyStartupSceneDocument(startupScene); CaptureCleanSceneRevision(); } void EditorSceneDocumentRuntime::Shutdown() { - if (m_session != nullptr) { + if (m_store != nullptr) { m_sceneDocument = {}; - SyncSessionDocumentProjection(); + WriteProjectedSceneDocument({}, {}, false); } - m_session = nullptr; + m_store = nullptr; m_sceneRuntime = nullptr; m_sceneDocument = {}; m_lastCleanSceneContentRevision = 0u; @@ -86,7 +86,7 @@ void EditorSceneDocumentRuntime::Tick() { } bool EditorSceneDocumentRuntime::IsReady() const { - return m_session != nullptr && m_sceneRuntime != nullptr; + return m_store != nullptr && m_sceneRuntime != nullptr; } bool EditorSceneDocumentRuntime::HasActiveScene() const { @@ -116,11 +116,11 @@ bool EditorSceneDocumentRuntime::RequestOpenSceneAsset( m_sceneDocument.ready = true; m_sceneDocument.loadedFromDisk = true; - m_sceneDocument.currentPath = scenePath; - m_sceneDocument.currentName = ResolveActiveSceneDisplayName(scenePath); - m_sceneDocument.dirty = false; + WriteProjectedSceneDocument( + scenePath, + ResolveActiveSceneDisplayName(scenePath), + false); CaptureCleanSceneRevision(); - SyncSessionDocumentProjection(); SetLastMessage("Opened scene: " + scenePath.string()); return true; } @@ -152,7 +152,8 @@ UIEditorHostCommandEvaluationResult EditorSceneDocumentRuntime::EvaluateFileComm if (!HasActiveScene()) { return BuildDisabledResult("No active scene to save."); } - if (m_sceneDocument.currentPath.empty()) { + const EditorSession* const session = TryGetProjectedSession(); + if (session == nullptr || session->currentScenePath.empty()) { return BuildDisabledResult( "Save Scene As is required before saving this scene."); } @@ -190,17 +191,19 @@ UIEditorHostCommandDispatchResult EditorSceneDocumentRuntime::DispatchFileComman m_sceneDocument.ready = true; m_sceneDocument.loadedFromDisk = false; - m_sceneDocument.currentPath.clear(); - m_sceneDocument.currentName = ResolveActiveSceneDisplayName(); - m_sceneDocument.dirty = true; + WriteProjectedSceneDocument( + {}, + ResolveActiveSceneDisplayName(), + true); CaptureCleanSceneRevision(); - SyncSessionDocumentProjection(); SetLastMessage("New scene created."); return BuildExecutedDispatch(m_lastMessage); } if (commandId == "file.save_scene") { - const std::filesystem::path scenePath = m_sceneDocument.currentPath; + const EditorSession* const session = TryGetProjectedSession(); + const std::filesystem::path scenePath = + session != nullptr ? session->currentScenePath : std::filesystem::path{}; if (!m_sceneRuntime->SaveScene(scenePath)) { SetLastMessage("Failed to save scene: " + scenePath.string()); return BuildRejectedDispatch(m_lastMessage); @@ -208,10 +211,11 @@ UIEditorHostCommandDispatchResult EditorSceneDocumentRuntime::DispatchFileComman m_sceneDocument.ready = true; m_sceneDocument.loadedFromDisk = !scenePath.empty(); - m_sceneDocument.currentName = ResolveActiveSceneDisplayName(scenePath); - m_sceneDocument.dirty = false; + WriteProjectedSceneDocument( + scenePath, + ResolveActiveSceneDisplayName(scenePath), + false); CaptureCleanSceneRevision(); - SyncSessionDocumentProjection(); SetLastMessage("Scene saved: " + scenePath.string()); return BuildExecutedDispatch(m_lastMessage); } @@ -220,34 +224,47 @@ UIEditorHostCommandDispatchResult EditorSceneDocumentRuntime::DispatchFileComman return BuildRejectedDispatch(evaluation.message); } +const EditorSession* EditorSceneDocumentRuntime::TryGetProjectedSession() const { + return m_store != nullptr + ? &m_store->GetState().session + : nullptr; +} + void EditorSceneDocumentRuntime::ApplyStartupSceneDocument( const EditorStartupSceneResult& startupScene) { m_sceneDocument.ready = startupScene.ready; m_sceneDocument.loadedFromDisk = startupScene.loadedFromDisk; - m_sceneDocument.currentPath = startupScene.scenePath; - m_sceneDocument.currentName = - ResolveDocumentSceneName(startupScene.sceneName, startupScene.scenePath); - m_sceneDocument.dirty = false; - SyncSessionDocumentProjection(); + WriteProjectedSceneDocument( + startupScene.scenePath, + ResolveDocumentSceneName(startupScene.sceneName, startupScene.scenePath), + false); } std::string EditorSceneDocumentRuntime::ResolveActiveSceneDisplayName( const std::filesystem::path& fallbackPath) const { const std::filesystem::path& scenePath = - fallbackPath.empty() ? m_sceneDocument.currentPath : fallbackPath; + !fallbackPath.empty() + ? fallbackPath + : (TryGetProjectedSession() != nullptr + ? TryGetProjectedSession()->currentScenePath + : std::filesystem::path{}); const std::string sceneName = IsReady() ? m_sceneRuntime->GetActiveSceneName() : std::string(); return ResolveDocumentSceneName(sceneName, scenePath); } -void EditorSceneDocumentRuntime::SyncSessionDocumentProjection() { - if (m_session == nullptr) { +void EditorSceneDocumentRuntime::WriteProjectedSceneDocument( + std::filesystem::path scenePath, + std::string sceneName, + bool dirty) { + if (m_store == nullptr) { return; } - m_session->currentScenePath = m_sceneDocument.currentPath; - m_session->currentSceneName = m_sceneDocument.currentName; - m_session->sceneDocumentDirty = m_sceneDocument.dirty; + m_store->Dispatch(Product::EditorCommand::SetSceneDocumentState( + std::move(scenePath), + std::move(sceneName), + dirty)); } void EditorSceneDocumentRuntime::SyncSceneDocumentDirtyFromRevision() { @@ -259,9 +276,14 @@ void EditorSceneDocumentRuntime::SyncSceneDocumentDirtyFromRevision() { if (revision != m_lastObservedSceneContentRevision) { m_lastObservedSceneContentRevision = revision; } - if (revision != m_lastCleanSceneContentRevision) { - m_sceneDocument.dirty = true; - SyncSessionDocumentProjection(); + const EditorSession* const session = TryGetProjectedSession(); + if (revision != m_lastCleanSceneContentRevision && + session != nullptr && + !session->sceneDocumentDirty) { + WriteProjectedSceneDocument( + session->currentScenePath, + session->currentSceneName, + true); } } diff --git a/editor/src/Product/Services/Runtime/EditorSceneDocumentRuntime.h b/editor/src/Product/Services/Runtime/EditorSceneDocumentRuntime.h index 990d8386..97ed35ab 100644 --- a/editor/src/Product/Services/Runtime/EditorSceneDocumentRuntime.h +++ b/editor/src/Product/Services/Runtime/EditorSceneDocumentRuntime.h @@ -1,6 +1,8 @@ #pragma once #include "Product/Commands/EditorHostCommandBridge.h" +#include "Product/Runtime/Store/EditorStore.h" +#include "Product/State/EditorSession.h" #include #include @@ -11,13 +13,12 @@ namespace XCEngine::UI::Editor::App { class EditorSceneRuntime; -struct EditorSession; struct EditorStartupSceneResult; class EditorSceneDocumentRuntime final { public: void Initialize( - EditorSession& session, + Product::EditorStore& store, EditorSceneRuntime& sceneRuntime, const EditorStartupSceneResult& startupScene); void Shutdown(); @@ -43,20 +44,21 @@ private: struct SceneDocumentState { bool ready = false; bool loadedFromDisk = false; - std::filesystem::path currentPath = {}; - std::string currentName = {}; - bool dirty = false; }; + const EditorSession* TryGetProjectedSession() const; void ApplyStartupSceneDocument(const EditorStartupSceneResult& startupScene); std::string ResolveActiveSceneDisplayName( const std::filesystem::path& fallbackPath = {}) const; - void SyncSessionDocumentProjection(); + void WriteProjectedSceneDocument( + std::filesystem::path scenePath, + std::string sceneName, + bool dirty); void SyncSceneDocumentDirtyFromRevision(); void CaptureCleanSceneRevision(); void SetLastMessage(std::string message); - EditorSession* m_session = nullptr; + Product::EditorStore* m_store = nullptr; EditorSceneRuntime* m_sceneRuntime = nullptr; SceneDocumentState m_sceneDocument = {}; std::uint64_t m_lastCleanSceneContentRevision = 0u; diff --git a/editor/src/Product/State/EditorSelectionService.h b/editor/src/Product/State/EditorSelectionService.h index b751b5e0..28ee4211 100644 --- a/editor/src/Product/State/EditorSelectionService.h +++ b/editor/src/Product/State/EditorSelectionService.h @@ -1,7 +1,7 @@ #pragma once +#include "Product/Runtime/Store/EditorStore.h" #include "Product/State/EditorSelectionStamp.h" -#include "Product/State/EditorSession.h" #include #include @@ -12,21 +12,26 @@ namespace XCEngine::UI::Editor::App { class EditorSelectionService { public: + void BindStore(Product::EditorStore* store) { + m_store = store; + } + const EditorSelectionState& GetSelection() const { - return m_selection; + return CurrentSelection(); } bool HasSelection() const { - return m_selection.kind != EditorSelectionKind::None && - !m_selection.itemId.empty(); + const EditorSelectionState& selection = CurrentSelection(); + return selection.kind != EditorSelectionKind::None && + !selection.itemId.empty(); } bool HasSelectionKind(EditorSelectionKind kind) const { - return m_selection.kind == kind && HasSelection(); + return CurrentSelection().kind == kind && HasSelection(); } std::uint64_t GetStamp() const { - return m_selection.stamp; + return CurrentSelection().stamp; } bool SetProjectSelection( @@ -40,11 +45,12 @@ public: selection.displayName = std::string(displayName); selection.absolutePath = absolutePath; selection.directory = directory; + const EditorSelectionState& currentSelection = CurrentSelection(); const bool changed = - m_selection.kind != selection.kind || - m_selection.itemId != selection.itemId || - m_selection.absolutePath != selection.absolutePath || - m_selection.directory != selection.directory; + currentSelection.kind != selection.kind || + currentSelection.itemId != selection.itemId || + currentSelection.absolutePath != selection.absolutePath || + currentSelection.directory != selection.directory; ApplySelection(std::move(selection), changed); return changed; } @@ -56,28 +62,30 @@ public: selection.kind = EditorSelectionKind::HierarchyNode; selection.itemId = std::string(itemId); selection.displayName = std::string(displayName); + const EditorSelectionState& currentSelection = CurrentSelection(); const bool changed = - m_selection.kind != selection.kind || - m_selection.itemId != selection.itemId; + currentSelection.kind != selection.kind || + currentSelection.itemId != selection.itemId; ApplySelection(std::move(selection), changed); return changed; } void SetSelection(EditorSelectionState selection) { + const EditorSelectionState& currentSelection = CurrentSelection(); bool changed = true; switch (selection.kind) { case EditorSelectionKind::ProjectItem: changed = - m_selection.kind != selection.kind || - m_selection.itemId != selection.itemId || - m_selection.absolutePath != selection.absolutePath || - m_selection.directory != selection.directory; + currentSelection.kind != selection.kind || + currentSelection.itemId != selection.itemId || + currentSelection.absolutePath != selection.absolutePath || + currentSelection.directory != selection.directory; break; case EditorSelectionKind::HierarchyNode: changed = - m_selection.kind != selection.kind || - m_selection.itemId != selection.itemId; + currentSelection.kind != selection.kind || + currentSelection.itemId != selection.itemId; break; case EditorSelectionKind::None: @@ -90,21 +98,38 @@ public: } void ClearSelection() { - m_selection = {}; - m_selection.stamp = GenerateEditorSelectionStamp(); + EditorSelectionState selection = {}; + selection.stamp = GenerateEditorSelectionStamp(); + CommitSelection(std::move(selection)); } private: - void ApplySelection( - EditorSelectionState selection, - bool changed) { - const std::uint64_t previousStamp = m_selection.stamp; - selection.stamp = changed - ? GenerateEditorSelectionStamp() - : (previousStamp != 0u ? previousStamp : GenerateEditorSelectionStamp()); + const EditorSelectionState& CurrentSelection() const { + return m_store != nullptr + ? m_store->GetState().session.selection + : m_selection; + } + + void CommitSelection(EditorSelectionState selection) { + if (m_store != nullptr) { + m_store->Dispatch(Product::EditorCommand::SetSelection(std::move(selection))); + return; + } + m_selection = std::move(selection); } + void ApplySelection( + EditorSelectionState selection, + bool changed) { + const std::uint64_t previousStamp = CurrentSelection().stamp; + selection.stamp = changed + ? GenerateEditorSelectionStamp() + : (previousStamp != 0u ? previousStamp : GenerateEditorSelectionStamp()); + CommitSelection(std::move(selection)); + } + + Product::EditorStore* m_store = nullptr; EditorSelectionState m_selection = {}; }; diff --git a/editor/src/Product/State/EditorSession.h b/editor/src/Product/State/EditorSession.h index f308224c..f24bd611 100644 --- a/editor/src/Product/State/EditorSession.h +++ b/editor/src/Product/State/EditorSession.h @@ -4,7 +4,6 @@ #include #include #include -#include namespace XCEngine::UI::Editor { @@ -43,22 +42,28 @@ struct EditorSelectionState { std::filesystem::path absolutePath = {}; bool directory = false; std::uint64_t stamp = 0u; + + bool operator==(const EditorSelectionState& other) const = default; }; struct EditorConsoleEntry { std::string channel = {}; std::string message = {}; + + bool operator==(const EditorConsoleEntry& other) const = default; }; struct EditorSession { std::filesystem::path workspaceRoot = {}; std::filesystem::path projectRoot = {}; + std::string currentProjectFolderId = {}; std::filesystem::path currentScenePath = {}; std::string currentSceneName = {}; EditorRuntimeMode runtimeMode = EditorRuntimeMode::Edit; EditorSelectionState selection = {}; - std::vector consoleEntries = {}; bool sceneDocumentDirty = false; + + bool operator==(const EditorSession& other) const = default; }; std::string_view GetEditorRuntimeModeName(EditorRuntimeMode mode); diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index b7e20a02..0e1a8091 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -67,7 +67,6 @@ set(EDITOR_UI_UNIT_TEST_SOURCES set(EDITOR_APP_CORE_TEST_SOURCES test_editor_host_command_bridge.cpp test_editor_project_runtime.cpp - test_editor_runtime_coordinator.cpp test_editor_scene_runtime_backend.cpp test_editor_shell_asset_validation.cpp test_editor_window_frame_orchestrator.cpp @@ -132,6 +131,7 @@ if(TARGET XCEditorCore) target_include_directories(editor_app_core_tests PRIVATE + ${CMAKE_SOURCE_DIR}/editor/src ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Composition ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Core ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Features @@ -179,6 +179,7 @@ if(TARGET XCEditorCore) target_include_directories(editor_app_feature_tests PRIVATE + ${CMAKE_SOURCE_DIR}/editor/src ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Composition ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Core ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Features @@ -249,3 +250,74 @@ endif() gtest_discover_tests(editor_windowing_phase1_tests DISCOVERY_MODE POST_BUILD ) + +add_executable(editor_runtime_state_tests + test_editor_runtime_command_service.cpp + test_editor_store.cpp +) + +target_link_libraries(editor_runtime_state_tests + PRIVATE + XCEditorCore + GTest::gtest_main +) + +target_include_directories(editor_runtime_state_tests + PRIVATE + ${CMAKE_SOURCE_DIR}/editor/src + ${CMAKE_SOURCE_DIR}/engine/include +) + +if(MSVC) + target_compile_options(editor_runtime_state_tests PRIVATE /utf-8 /FS) + set_target_properties(editor_runtime_state_tests PROPERTIES + MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>" + COMPILE_PDB_NAME "editor_runtime_state_tests-compile" + COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb" + COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Debug" + COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Release" + COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/MinSizeRel" + COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/RelWithDebInfo" + ) + set_property(TARGET editor_runtime_state_tests PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +gtest_discover_tests(editor_runtime_state_tests + DISCOVERY_MODE POST_BUILD +) + +add_executable(editor_product_registry_tests + test_editor_product_registry.cpp +) + +target_link_libraries(editor_product_registry_tests + PRIVATE + XCEditorCore + GTest::gtest_main +) + +target_include_directories(editor_product_registry_tests + PRIVATE + ${CMAKE_SOURCE_DIR}/editor/src + ${CMAKE_SOURCE_DIR}/engine/include +) + +if(MSVC) + target_compile_options(editor_product_registry_tests PRIVATE /utf-8 /FS) + set_target_properties(editor_product_registry_tests PROPERTIES + MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>" + COMPILE_PDB_NAME "editor_product_registry_tests-compile" + COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb" + COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Debug" + COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Release" + COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/MinSizeRel" + COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/RelWithDebInfo" + ) + set_property(TARGET editor_product_registry_tests PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +gtest_discover_tests(editor_product_registry_tests + DISCOVERY_MODE POST_BUILD +) diff --git a/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp b/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp index da392741..c413f2b1 100644 --- a/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp +++ b/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp @@ -1,9 +1,10 @@ #include -#include "Panels/EditorPanelIds.h" -#include "Commands/EditorEditCommandRoute.h" -#include "Commands/EditorHostCommandBridge.h" -#include "State/EditorCommandFocusService.h" +#include "Product/Commands/EditorHostCommandBridge.h" +#include "Product/Core/Commands/EditorEditCommandRoute.h" +#include "Product/Features/Workspace/Hierarchy/HierarchyWorkspaceFeature.h" +#include "Product/Features/Workspace/Project/ProjectWorkspaceFeature.h" +#include "Product/State/EditorCommandFocusService.h" #include #include @@ -293,7 +294,8 @@ TEST(EditorHostCommandBridgeTest, InspectorEditCommandsDelegateToBoundInspectorR TEST(EditorHostCommandBridgeTest, ActivePanelRouteIsUsedAsFallbackWhenNoExplicitCommandFocusExists) { UIEditorWorkspaceController controller = - MakeWorkspaceController(XCEngine::UI::Editor::App::kHierarchyPanelId); + MakeWorkspaceController( + XCEngine::UI::Editor::App::kHierarchyWorkspaceFeaturePanelId); StubEditCommandRoute hierarchyRoute = {}; hierarchyRoute.evaluationResult.executable = true; @@ -310,7 +312,8 @@ TEST(EditorHostCommandBridgeTest, ActivePanelRouteIsUsedAsFallbackWhenNoExplicit TEST(EditorHostCommandBridgeTest, ExplicitCommandFocusOverridesActivePanelFallback) { UIEditorWorkspaceController controller = - MakeWorkspaceController(XCEngine::UI::Editor::App::kProjectPanelId); + MakeWorkspaceController( + XCEngine::UI::Editor::App::kProjectWorkspaceFeaturePanelId); EditorCommandFocusService commandFocus = {}; commandFocus.ClaimFocus(EditorActionRoute::Scene); diff --git a/tests/UI/Editor/unit/test_editor_product_registry.cpp b/tests/UI/Editor/unit/test_editor_product_registry.cpp new file mode 100644 index 00000000..d6479913 --- /dev/null +++ b/tests/UI/Editor/unit/test_editor_product_registry.cpp @@ -0,0 +1,195 @@ +#include + +#include "Product/Registry/EditorProductRegistry.h" +#include "Product/Runtime/Shell/EditorShellAssetBuilder.h" + +#include +#include +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { +namespace { + +EditorRuntimePaths TestRuntimePaths() { + return EditorRuntimePaths{ + .workspaceRoot = ".", + .executableRoot = ".", + .resourceRoot = "editor/resources", + .projectRoot = "project", + .captureRoot = "captures", + }; +} + +const UIEditorMenuDescriptor* FindMenu( + const UIEditorMenuModel& model, + std::string_view menuId) { + for (const UIEditorMenuDescriptor& menu : model.menus) { + if (menu.menuId == menuId) { + return &menu; + } + } + + return nullptr; +} + +const UIEditorMenuItemDescriptor* FindMenuItem( + const std::vector& items, + std::string_view itemId) { + for (const UIEditorMenuItemDescriptor& item : items) { + if (item.itemId == itemId) { + return &item; + } + } + + return nullptr; +} + +const UIEditorCommandDescriptor* FindWorkspacePanelCommand( + const UIEditorCommandRegistry& registry, + std::string_view panelId) { + for (const UIEditorCommandDescriptor& command : registry.commands) { + if (command.kind == UIEditorCommandKind::Workspace && + command.workspaceCommand.panelId == panelId) { + return &command; + } + } + + return nullptr; +} + +TEST(EditorProductRegistryTests, DefaultWorkspaceMetadataValidates) { + const EditorProductRegistryValidationResult validation = + ValidateEditorProductRegistry(); + EXPECT_TRUE(validation.IsValid()) << validation.message; + + const UIEditorPanelRegistry panelRegistry = BuildEditorWorkspacePanelRegistry(); + ASSERT_EQ(panelRegistry.panels.size(), GetEditorWorkspaceFeatures().size()); + + const UIEditorWorkspaceModel workspace = BuildDefaultEditorWorkspaceModel(); + const UIEditorWorkspaceValidationResult workspaceValidation = + ValidateUIEditorWorkspace(workspace); + EXPECT_TRUE(workspaceValidation.IsValid()) << workspaceValidation.message; + EXPECT_EQ(workspace.activePanelId, "scene"); +} + +TEST(EditorProductRegistryTests, WorkspaceCommandsAndViewMenuStayInSync) { + UIEditorCommandRegistry commandRegistry = {}; + commandRegistry.commands = BuildEditorWorkspaceShellCommands(); + const UIEditorCommandRegistryValidationResult commandValidation = + ValidateUIEditorCommandRegistry(commandRegistry); + EXPECT_TRUE(commandValidation.IsValid()) << commandValidation.message; + + const UIEditorMenuDescriptor viewMenu = BuildEditorWorkspaceViewMenu(); + const UIEditorMenuModel menuModel = { { viewMenu } }; + const UIEditorMenuModelValidationResult menuValidation = + ValidateUIEditorMenuModel(menuModel, commandRegistry); + EXPECT_TRUE(menuValidation.IsValid()) << menuValidation.message; + + const UIEditorCommandDescriptor* resetLayoutCommand = + FindUIEditorCommandDescriptor(commandRegistry, kEditorResetLayoutCommandId); + ASSERT_NE(resetLayoutCommand, nullptr); + EXPECT_EQ( + resetLayoutCommand->workspaceCommand.kind, + UIEditorWorkspaceCommandKind::ResetWorkspace); + EXPECT_EQ( + resetLayoutCommand->workspaceCommand.panelSource, + UIEditorCommandPanelSource::None); + + const UIEditorMenuItemDescriptor* panelsSubmenu = + FindMenuItem(viewMenu.items, "view-panels"); + ASSERT_NE(panelsSubmenu, nullptr); + ASSERT_EQ(panelsSubmenu->kind, UIEditorMenuItemKind::Submenu); + ASSERT_EQ(panelsSubmenu->children.size(), GetEditorWorkspaceFeatures().size()); + + for (const EditorWorkspaceFeatureDescriptor& feature : GetEditorWorkspaceFeatures()) { + const UIEditorCommandDescriptor* command = + FindWorkspacePanelCommand(commandRegistry, feature.panelId); + ASSERT_NE(command, nullptr); + EXPECT_EQ(command->displayName, feature.defaultTitle); + EXPECT_EQ( + command->workspaceCommand.kind, + UIEditorWorkspaceCommandKind::ActivatePanel); + EXPECT_EQ( + command->workspaceCommand.panelSource, + UIEditorCommandPanelSource::FixedPanelId); + + const UIEditorMenuItemDescriptor* menuItem = + FindMenuItem( + panelsSubmenu->children, + std::string("view-panel-") + std::string(feature.panelId)); + ASSERT_NE(menuItem, nullptr); + EXPECT_EQ(menuItem->label, feature.defaultTitle); + EXPECT_EQ(menuItem->commandId, command->commandId); + EXPECT_EQ( + menuItem->checkedState.source, + UIEditorMenuCheckedStateSource::PanelActive); + EXPECT_EQ(menuItem->checkedState.panelId, feature.panelId); + } +} + +TEST(EditorProductRegistryTests, ShellAssetUsesRegistryWorkspacePreset) { + const EditorShellCommandPreset shellPreset = BuildEditorShellCommandPreset(); + const UIEditorCommandRegistryValidationResult shellCommandValidation = + ValidateUIEditorCommandRegistry(shellPreset.commandRegistry); + EXPECT_TRUE(shellCommandValidation.IsValid()) << shellCommandValidation.message; + const UIEditorMenuModelValidationResult shellMenuValidation = + ValidateUIEditorMenuModel( + shellPreset.menuModel, + shellPreset.commandRegistry); + EXPECT_TRUE(shellMenuValidation.IsValid()) << shellMenuValidation.message; + UIEditorShortcutManager shortcutManager(shellPreset.commandRegistry); + for (const ::XCEngine::UI::UIShortcutBinding& binding : shellPreset.shortcutBindings) { + shortcutManager.RegisterBinding(binding); + } + const UIEditorShortcutManagerValidationResult shortcutValidation = + shortcutManager.ValidateConfiguration(); + EXPECT_TRUE(shortcutValidation.IsValid()) << shortcutValidation.message; + + const EditorShellAsset shell = BuildEditorApplicationShellAsset(TestRuntimePaths()); + const EditorShellAssetValidationResult assetValidation = + ValidateEditorShellAsset(shell); + EXPECT_TRUE(assetValidation.IsValid()) << assetValidation.message; + + const UIEditorPanelRegistry registryPanels = BuildEditorWorkspacePanelRegistry(); + EXPECT_EQ(shell.panelRegistry.panels.size(), registryPanels.panels.size()); + EXPECT_TRUE( + AreUIEditorWorkspaceModelsEquivalent( + shell.workspace, + BuildDefaultEditorWorkspaceModel())); + + const UIEditorMenuDescriptor* assetViewMenu = + FindMenu(shell.shellDefinition.menuModel, "view"); + ASSERT_NE(assetViewMenu, nullptr); + const UIEditorMenuDescriptor registryViewMenu = BuildEditorWorkspaceViewMenu(); + ASSERT_EQ(assetViewMenu->items.size(), registryViewMenu.items.size()); + ASSERT_NE(FindMenuItem(assetViewMenu->items, "view-panels"), nullptr); + + for (const UIEditorCommandDescriptor& expected : shellPreset.commandRegistry.commands) { + const UIEditorCommandDescriptor* actual = + FindUIEditorCommandDescriptor( + shell.shortcutAsset.commandRegistry, + expected.commandId); + ASSERT_NE(actual, nullptr); + EXPECT_EQ(actual->displayName, expected.displayName); + EXPECT_EQ(actual->kind, expected.kind); + EXPECT_EQ(actual->workspaceCommand.kind, expected.workspaceCommand.kind); + EXPECT_EQ( + actual->workspaceCommand.panelSource, + expected.workspaceCommand.panelSource); + EXPECT_EQ(actual->workspaceCommand.panelId, expected.workspaceCommand.panelId); + } + + EXPECT_EQ( + shell.shellDefinition.menuModel.menus.size(), + shellPreset.menuModel.menus.size()); + EXPECT_EQ( + shell.shortcutAsset.bindings.size(), + shellPreset.shortcutBindings.size()); +} + +} // namespace +} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_editor_project_runtime.cpp b/tests/UI/Editor/unit/test_editor_project_runtime.cpp index 7f6c8524..4420031e 100644 --- a/tests/UI/Editor/unit/test_editor_project_runtime.cpp +++ b/tests/UI/Editor/unit/test_editor_project_runtime.cpp @@ -1,5 +1,6 @@ -#include "Project/EditorProjectRuntime.h" -#include "State/EditorSelectionService.h" +#include "Product/Services/Project/EditorProjectRuntime.h" +#include "Product/Runtime/Store/EditorStore.h" +#include "Product/State/EditorSelectionService.h" #include @@ -7,6 +8,7 @@ #include #include #include +#include namespace XCEngine::UI::Editor::App { namespace { @@ -185,5 +187,63 @@ TEST(EditorProjectRuntimeTests, BoundSelectionServiceBecomesTheSingleProjectSele EXPECT_FALSE(runtime.HasSelection()); } +TEST(EditorProjectRuntimeTests, BoundStorePreservesPersistedCurrentFolderDuringInitialize) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts")); + + Product::EditorStore store = {}; + Product::EditorState initialState = {}; + initialState.session.currentProjectFolderId = "Assets/Scenes"; + store.Reset(std::move(initialState)); + + EditorProjectRuntime runtime = {}; + runtime.BindStore(&store); + ASSERT_TRUE(runtime.Initialize(repo.Root() / "project")); + + EXPECT_EQ(store.GetState().session.currentProjectFolderId, "Assets/Scenes"); + EXPECT_EQ(runtime.GetBrowserModel().GetCurrentFolderId(), "Assets/Scenes"); +} + +TEST(EditorProjectRuntimeTests, BoundStoreCanonicalizesInvalidPersistedCurrentFolderDuringInitialize) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); + + Product::EditorStore store = {}; + Product::EditorState initialState = {}; + initialState.session.currentProjectFolderId = "Assets/Missing"; + store.Reset(std::move(initialState)); + + EditorProjectRuntime runtime = {}; + runtime.BindStore(&store); + ASSERT_TRUE(runtime.Initialize(repo.Root() / "project")); + + EXPECT_EQ(store.GetState().session.currentProjectFolderId, "Assets"); + EXPECT_EQ(runtime.GetBrowserModel().GetCurrentFolderId(), "Assets"); +} + +TEST(EditorProjectRuntimeTests, BoundStoreRemainsTheAuthoritativeCurrentFolderSourceAtRuntime) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts")); + + Product::EditorStore store = {}; + store.Reset({}); + + EditorProjectRuntime runtime = {}; + runtime.BindStore(&store); + ASSERT_TRUE(runtime.Initialize(repo.Root() / "project")); + + ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes")); + EXPECT_EQ(store.GetState().session.currentProjectFolderId, "Assets/Scenes"); + EXPECT_EQ(runtime.GetBrowserModel().GetCurrentFolderId(), "Assets/Scenes"); + + const Product::EditorStore::DispatchResult dispatchResult = + store.Dispatch(Product::EditorCommand::SetCurrentProjectFolder("Assets/Scripts")); + ASSERT_TRUE(dispatchResult.handled); + ASSERT_TRUE(dispatchResult.stateChanged); + EXPECT_EQ(runtime.GetBrowserModel().GetCurrentFolderId(), "Assets/Scripts"); +} + } // namespace } // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_editor_runtime_command_service.cpp b/tests/UI/Editor/unit/test_editor_runtime_command_service.cpp new file mode 100644 index 00000000..adaf6382 --- /dev/null +++ b/tests/UI/Editor/unit/test_editor_runtime_command_service.cpp @@ -0,0 +1,217 @@ +#include + +#include "Product/Core/Scene/EditorSceneBackend.h" +#include "Product/Runtime/Store/EditorStore.h" +#include "Product/Services/Runtime/EditorRuntimeCommandService.h" +#include "Product/Services/Scene/EditorSceneRuntime.h" + +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { +namespace { + +using ::XCEngine::Components::Scene; + +class FakeEditorSceneBackend; + +class FakeEditorScenePlaySession final : public EditorScenePlaySession { +public: + explicit FakeEditorScenePlaySession(FakeEditorSceneBackend& backend) + : m_backend(backend) {} + + ~FakeEditorScenePlaySession() override; + + Scene* GetRuntimeScene() const override; + +private: + FakeEditorSceneBackend& m_backend; +}; + +class FakeEditorSceneBackend final : public EditorSceneBackend { +public: + FakeEditorSceneBackend() { + editScene = std::make_unique("Edit"); + activeScene = editScene.get(); + } + + EditorStartupSceneResult EnsureStartupScene( + const std::filesystem::path&) override { + return EditorStartupSceneResult{ + .ready = true, + .loadedFromDisk = true, + .scenePath = std::filesystem::path("D:/Project/Assets/Scenes/Main.xc"), + .sceneName = "Edit" + }; + } + + EditorSceneHierarchySnapshot BuildHierarchySnapshot() const override { + return {}; + } + + bool NewScene(std::string_view sceneName) override { + editScene = std::make_unique( + sceneName.empty() ? std::string("Untitled") : std::string(sceneName)); + activeScene = editScene.get(); + return true; + } + + bool OpenSceneAsset(const std::filesystem::path& scenePath) override { + lastOpenedScenePath = scenePath; + if (!openSceneResult) { + return false; + } + + editScene = std::make_unique(openedSceneName); + activeScene = editScene.get(); + return true; + } + + bool SaveActiveScene(const std::filesystem::path&) override { + return true; + } + + Scene* GetActiveScene() const override { + return activeScene; + } + + std::unique_ptr BeginPlaySession() override { + ++beginPlaySessionCallCount; + runtimeScene = std::make_unique("Runtime"); + activeScene = runtimeScene.get(); + return std::make_unique(*this); + } + + std::optional GetObjectSnapshot( + std::string_view) const override { + return std::nullopt; + } + + bool AddComponent(std::string_view, std::string_view) override { + return false; + } + + bool RenameGameObject(std::string_view, std::string_view) override { + return false; + } + + bool DeleteGameObject(std::string_view) override { + return false; + } + + std::string DuplicateGameObject(std::string_view) override { + return {}; + } + + bool ReparentGameObject(std::string_view, std::string_view) override { + return false; + } + + bool MoveGameObjectBefore(std::string_view, std::string_view) override { + return false; + } + + bool MoveGameObjectAfter(std::string_view, std::string_view) override { + return false; + } + + bool MoveGameObjectToRoot(std::string_view) override { + return false; + } + + void EndPlaySession() { + ++endPlaySessionCallCount; + runtimeScene.reset(); + activeScene = editScene.get(); + } + + std::unique_ptr editScene = {}; + std::unique_ptr runtimeScene = {}; + Scene* activeScene = nullptr; + std::filesystem::path lastOpenedScenePath = {}; + int beginPlaySessionCallCount = 0; + int endPlaySessionCallCount = 0; + bool openSceneResult = true; + std::string openedSceneName = "Opened"; +}; + +FakeEditorScenePlaySession::~FakeEditorScenePlaySession() { + m_backend.EndPlaySession(); +} + +Scene* FakeEditorScenePlaySession::GetRuntimeScene() const { + return m_backend.runtimeScene.get(); +} + +struct RuntimeCommandServiceHarness { + RuntimeCommandServiceHarness() { + auto backend = std::make_unique(); + backendPtr = backend.get(); + sceneRuntime.SetBackend(std::move(backend)); + + Product::EditorState initialState = {}; + initialState.session.projectRoot = "D:/Project"; + store.Reset(std::move(initialState)); + + const EditorStartupSceneResult startupScene = + sceneRuntime.Initialize("D:/Project"); + EXPECT_TRUE(startupScene.ready); + + EditorRuntimePaths runtimePaths = {}; + runtimePaths.projectRoot = "D:/Project"; + service.Initialize(store, sceneRuntime, runtimePaths, startupScene); + } + + Product::EditorStore store = {}; + EditorSceneRuntime sceneRuntime = {}; + FakeEditorSceneBackend* backendPtr = nullptr; + EditorRuntimeCommandService service = {}; +}; + +TEST(EditorRuntimeCommandServiceTests, InitializationProjectsStartupSceneIntoStoreSession) { + RuntimeCommandServiceHarness harness = {}; + + EXPECT_EQ( + harness.store.GetState().session.currentScenePath, + std::filesystem::path("D:/Project/Assets/Scenes/Main.xc")); + EXPECT_EQ(harness.store.GetState().session.currentSceneName, "Edit"); + EXPECT_FALSE(harness.store.GetState().session.sceneDocumentDirty); + EXPECT_EQ(harness.store.GetState().session.runtimeMode, EditorRuntimeMode::Edit); +} + +TEST(EditorRuntimeCommandServiceTests, FileAndRunCommandsMutateStoreSessionState) { + RuntimeCommandServiceHarness harness = {}; + ASSERT_NE(harness.backendPtr, nullptr); + + int notificationCount = 0; + harness.store.Subscribe( + [&](const Product::EditorState&) { + ++notificationCount; + }); + + const UIEditorHostCommandDispatchResult newSceneResult = + harness.service.DispatchFileCommand("file.new_scene"); + ASSERT_TRUE(newSceneResult.commandExecuted); + EXPECT_TRUE(harness.store.GetState().session.currentScenePath.empty()); + EXPECT_EQ(harness.store.GetState().session.currentSceneName, "Untitled"); + EXPECT_TRUE(harness.store.GetState().session.sceneDocumentDirty); + + const UIEditorHostCommandDispatchResult playResult = + harness.service.DispatchRunCommand("run.play"); + ASSERT_TRUE(playResult.commandExecuted); + EXPECT_EQ(harness.store.GetState().session.runtimeMode, EditorRuntimeMode::Play); + + const UIEditorHostCommandDispatchResult stopResult = + harness.service.DispatchRunCommand("run.stop"); + ASSERT_TRUE(stopResult.commandExecuted); + EXPECT_EQ(harness.store.GetState().session.runtimeMode, EditorRuntimeMode::Edit); + EXPECT_GE(notificationCount, 3); +} + +} // namespace +} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_editor_runtime_coordinator.cpp b/tests/UI/Editor/unit/test_editor_runtime_coordinator.cpp deleted file mode 100644 index 8d5a6444..00000000 --- a/tests/UI/Editor/unit/test_editor_runtime_coordinator.cpp +++ /dev/null @@ -1,309 +0,0 @@ -#include - -#include "Project/EditorProjectRuntime.h" -#include "Runtime/EditorRuntimeCoordinator.h" -#include "Scene/EditorSceneBackend.h" -#include "Scene/EditorSceneRuntime.h" -#include "State/EditorSession.h" - -#include - -#include -#include -#include -#include -#include - -namespace XCEngine::UI::Editor::App { -namespace { - -using ::XCEngine::Components::Scene; - -class FakeEditorSceneBackend; - -class FakeEditorScenePlaySession final : public EditorScenePlaySession { -public: - explicit FakeEditorScenePlaySession(FakeEditorSceneBackend& backend) - : m_backend(backend) {} - - ~FakeEditorScenePlaySession() override; - - Scene* GetRuntimeScene() const override; - -private: - FakeEditorSceneBackend& m_backend; -}; - -class FakeEditorSceneBackend final : public EditorSceneBackend { -public: - FakeEditorSceneBackend() { - editScene = std::make_unique("Edit"); - activeScene = editScene.get(); - } - - EditorStartupSceneResult EnsureStartupScene( - const std::filesystem::path&) override { - return EditorStartupSceneResult{ - .ready = true, - .loadedFromDisk = true, - .scenePath = std::filesystem::path("D:/Project/Assets/Scenes/Main.xc"), - .sceneName = "Edit" - }; - } - - EditorSceneHierarchySnapshot BuildHierarchySnapshot() const override { - return {}; - } - - bool NewScene(std::string_view sceneName) override { - editScene = std::make_unique( - sceneName.empty() ? std::string("Untitled") : std::string(sceneName)); - activeScene = editScene.get(); - return true; - } - - bool OpenSceneAsset(const std::filesystem::path& scenePath) override { - lastOpenedScenePath = scenePath; - if (!openSceneResult) { - return false; - } - - editScene = std::make_unique(openedSceneName); - activeScene = editScene.get(); - return true; - } - - bool SaveActiveScene(const std::filesystem::path&) override { - ++saveActiveSceneCallCount; - savedActiveSceneWasEditScene = activeScene == editScene.get(); - return true; - } - - Scene* GetActiveScene() const override { - return activeScene; - } - - std::unique_ptr BeginPlaySession() override { - ++beginPlaySessionCallCount; - if (failBeginPlaySession) { - return nullptr; - } - - runtimeScene = std::make_unique("Runtime"); - activeScene = runtimeScene.get(); - return std::make_unique(*this); - } - - std::optional GetObjectSnapshot( - std::string_view) const override { - return std::nullopt; - } - - bool AddComponent(std::string_view, std::string_view) override { - return false; - } - - bool RenameGameObject(std::string_view, std::string_view) override { - return false; - } - - bool DeleteGameObject(std::string_view) override { - return false; - } - - std::string DuplicateGameObject(std::string_view) override { - return {}; - } - - bool ReparentGameObject(std::string_view, std::string_view) override { - return false; - } - - bool MoveGameObjectBefore(std::string_view, std::string_view) override { - return false; - } - - bool MoveGameObjectAfter(std::string_view, std::string_view) override { - return false; - } - - bool MoveGameObjectToRoot(std::string_view) override { - return false; - } - - void EndPlaySession() { - ++endPlaySessionCallCount; - runtimeScene.reset(); - activeScene = editScene.get(); - } - - Scene* EditScene() const { - return editScene.get(); - } - - Scene* RuntimeScene() const { - return runtimeScene.get(); - } - - std::unique_ptr editScene = {}; - std::unique_ptr runtimeScene = {}; - Scene* activeScene = nullptr; - std::filesystem::path lastOpenedScenePath = {}; - int beginPlaySessionCallCount = 0; - int endPlaySessionCallCount = 0; - int saveActiveSceneCallCount = 0; - bool savedActiveSceneWasEditScene = false; - bool failBeginPlaySession = false; - bool openSceneResult = true; - std::string openedSceneName = "Opened"; -}; - -FakeEditorScenePlaySession::~FakeEditorScenePlaySession() { - m_backend.EndPlaySession(); -} - -Scene* FakeEditorScenePlaySession::GetRuntimeScene() const { - return m_backend.RuntimeScene(); -} - -struct RuntimeCoordinatorHarness { - RuntimeCoordinatorHarness( - EditorRuntimePaths runtimePaths = {}) { - auto backend = std::make_unique(); - backendPtr = backend.get(); - sceneRuntime.SetBackend(std::move(backend)); - const EditorStartupSceneResult startupScene = - sceneRuntime.Initialize("D:/Project"); - EXPECT_TRUE(startupScene.ready); - coordinator.Initialize( - session, - sceneRuntime, - projectRuntime, - runtimePaths, - startupScene); - } - - EditorSession session = {}; - EditorProjectRuntime projectRuntime = {}; - EditorSceneRuntime sceneRuntime = {}; - FakeEditorSceneBackend* backendPtr = nullptr; - EditorRuntimeCoordinator coordinator = {}; -}; - -TEST(EditorRuntimeCoordinatorTests, InitializeProjectsStartupSceneDocumentStateToSession) { - RuntimeCoordinatorHarness harness = {}; - - EXPECT_EQ( - harness.session.currentScenePath, - std::filesystem::path("D:/Project/Assets/Scenes/Main.xc")); - EXPECT_EQ(harness.session.currentSceneName, "Edit"); - EXPECT_FALSE(harness.session.sceneDocumentDirty); - EXPECT_EQ(harness.session.runtimeMode, EditorRuntimeMode::Edit); -} - -TEST(EditorRuntimeCoordinatorTests, ScriptRebuildEvaluationIsNoLongerHardcodedStub) { - EditorRuntimePaths runtimePaths = {}; - runtimePaths.projectRoot = "D:/Project"; - RuntimeCoordinatorHarness harness(runtimePaths); - - const UIEditorHostCommandEvaluationResult evaluation = - harness.coordinator.EvaluateScriptCommand("scripts.rebuild"); - EXPECT_NE( - evaluation.message, - "Script rebuild is owned by the runtime coordinator, but no in-process script assembly builder is bound."); -} - -TEST(EditorRuntimeCoordinatorTests, NewSceneProjectsUnsavedDirtyDocumentStateToSession) { - RuntimeCoordinatorHarness harness = {}; - - const UIEditorHostCommandDispatchResult newSceneResult = - harness.coordinator.DispatchFileCommand("file.new_scene"); - EXPECT_TRUE(newSceneResult.commandExecuted); - EXPECT_TRUE(harness.session.currentScenePath.empty()); - EXPECT_EQ(harness.session.currentSceneName, "Untitled"); - EXPECT_TRUE(harness.session.sceneDocumentDirty); -} - -TEST(EditorRuntimeCoordinatorTests, OpenSceneProjectsCoordinatorOwnedDocumentStateToSession) { - RuntimeCoordinatorHarness harness = {}; - ASSERT_NE(harness.backendPtr, nullptr); - harness.backendPtr->openedSceneName = "Opened Scene"; - - const std::filesystem::path scenePath = - "D:/Project/Assets/Scenes/Secondary.xc"; - ASSERT_TRUE(harness.coordinator.RequestOpenSceneAsset(scenePath)); - EXPECT_EQ(harness.backendPtr->lastOpenedScenePath, scenePath); - EXPECT_EQ(harness.session.currentScenePath, scenePath); - EXPECT_EQ(harness.session.currentSceneName, "Opened Scene"); - EXPECT_FALSE(harness.session.sceneDocumentDirty); -} - -TEST(EditorRuntimeCoordinatorTests, PlayModeRunsRuntimeSceneAndRestoresEditSceneOnStop) { - RuntimeCoordinatorHarness harness = {}; - ASSERT_NE(harness.backendPtr, nullptr); - ASSERT_EQ(harness.sceneRuntime.GetActiveScene(), harness.backendPtr->EditScene()); - - const UIEditorHostCommandDispatchResult playResult = - harness.coordinator.DispatchRunCommand("run.play"); - EXPECT_TRUE(playResult.commandExecuted); - EXPECT_EQ(harness.session.runtimeMode, EditorRuntimeMode::Play); - EXPECT_EQ(harness.backendPtr->beginPlaySessionCallCount, 1); - EXPECT_EQ(harness.sceneRuntime.GetActiveScene(), harness.backendPtr->RuntimeScene()); - EXPECT_NE(harness.sceneRuntime.GetActiveScene(), harness.backendPtr->EditScene()); - - const UIEditorHostCommandDispatchResult stopResult = - harness.coordinator.DispatchRunCommand("run.stop"); - EXPECT_TRUE(stopResult.commandExecuted); - EXPECT_EQ(harness.session.runtimeMode, EditorRuntimeMode::Edit); - EXPECT_EQ(harness.backendPtr->endPlaySessionCallCount, 1); - EXPECT_EQ(harness.sceneRuntime.GetActiveScene(), harness.backendPtr->EditScene()); -} - -TEST(EditorRuntimeCoordinatorTests, StepFromEditCreatesPausedPlaySession) { - RuntimeCoordinatorHarness harness = {}; - ASSERT_NE(harness.backendPtr, nullptr); - - const UIEditorHostCommandDispatchResult stepResult = - harness.coordinator.DispatchRunCommand("run.step"); - EXPECT_TRUE(stepResult.commandExecuted); - EXPECT_EQ(harness.session.runtimeMode, EditorRuntimeMode::Paused); - EXPECT_EQ(harness.backendPtr->beginPlaySessionCallCount, 1); - EXPECT_EQ(harness.sceneRuntime.GetActiveScene(), harness.backendPtr->RuntimeScene()); - - const UIEditorHostCommandDispatchResult stopResult = - harness.coordinator.DispatchRunCommand("run.stop"); - EXPECT_TRUE(stopResult.commandExecuted); - EXPECT_EQ(harness.backendPtr->endPlaySessionCallCount, 1); - EXPECT_EQ(harness.sceneRuntime.GetActiveScene(), harness.backendPtr->EditScene()); -} - -TEST(EditorRuntimeCoordinatorTests, SaveAfterStoppingPlayTargetsRestoredEditScene) { - RuntimeCoordinatorHarness harness = {}; - ASSERT_NE(harness.backendPtr, nullptr); - - ASSERT_TRUE(harness.coordinator.DispatchRunCommand("run.play").commandExecuted); - ASSERT_TRUE(harness.coordinator.DispatchRunCommand("run.stop").commandExecuted); - - const UIEditorHostCommandDispatchResult saveResult = - harness.coordinator.DispatchFileCommand("file.save_scene"); - EXPECT_TRUE(saveResult.commandExecuted); - EXPECT_EQ(harness.backendPtr->saveActiveSceneCallCount, 1); - EXPECT_TRUE(harness.backendPtr->savedActiveSceneWasEditScene); -} - -TEST(EditorRuntimeCoordinatorTests, FailedPlaySessionLeavesEditorInEditMode) { - RuntimeCoordinatorHarness harness = {}; - ASSERT_NE(harness.backendPtr, nullptr); - harness.backendPtr->failBeginPlaySession = true; - - const UIEditorHostCommandDispatchResult playResult = - harness.coordinator.DispatchRunCommand("run.play"); - EXPECT_FALSE(playResult.commandExecuted); - EXPECT_EQ(harness.session.runtimeMode, EditorRuntimeMode::Edit); - EXPECT_EQ(harness.backendPtr->beginPlaySessionCallCount, 1); - EXPECT_EQ(harness.backendPtr->endPlaySessionCallCount, 0); - EXPECT_EQ(harness.sceneRuntime.GetActiveScene(), harness.backendPtr->EditScene()); -} - -} // namespace -} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp b/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp index 48f3f75f..f19eacce 100644 --- a/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp +++ b/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp @@ -1,5 +1,5 @@ -#include "Scene/EditorSceneBackend.h" -#include "Scene/EditorSceneRuntime.h" +#include "Product/Core/Scene/EditorSceneBackend.h" +#include "Product/Services/Scene/EditorSceneRuntime.h" #include 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 6901661f..d1f6bbc9 100644 --- a/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp +++ b/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp @@ -1,9 +1,8 @@ #include -#include "EditorWorkspacePanelRegistry.h" -#include "EditorShellAssetBuilder.h" -#include "Panels/EditorPanelIds.h" -#include "Product/EditorProductManifest.h" +#include "Product/Features/Workspace/Game/GameWorkspaceFeature.h" +#include "Product/Registry/EditorProductRegistry.h" +#include "Product/Runtime/Shell/EditorShellAssetBuilder.h" #include @@ -13,15 +12,12 @@ namespace { using XCEngine::Input::KeyCode; using XCEngine::UI::Editor::App::BuildEditorApplicationShellAsset; -using XCEngine::UI::Editor::App::EditorProductManifestValidationCode; using XCEngine::UI::Editor::App::EditorProductViewportKind; using XCEngine::UI::Editor::App::EditorRuntimePaths; -using XCEngine::UI::Editor::App::EditorWorkspacePanelCompositionValidationCode; -using XCEngine::UI::Editor::App::FindEditorProductPanel; -using XCEngine::UI::Editor::App::GetEditorProductPanels; -using XCEngine::UI::Editor::App::kGamePanelId; -using XCEngine::UI::Editor::App::ValidateEditorProductManifest; -using XCEngine::UI::Editor::App::ValidateEditorWorkspacePanelComposition; +using XCEngine::UI::Editor::App::FindEditorWorkspaceFeature; +using XCEngine::UI::Editor::App::GetEditorWorkspaceFeatures; +using XCEngine::UI::Editor::App::kGameWorkspaceFeaturePanelId; +using XCEngine::UI::Editor::App::ValidateEditorProductRegistry; using XCEngine::UI::Editor::EditorShellAssetValidationCode; using XCEngine::UI::Editor::FindUIEditorPanelDescriptor; using XCEngine::UI::Editor::UIEditorCommandPanelSource; @@ -86,23 +82,14 @@ TEST(EditorShellAssetValidationTest, DefaultShellAssetPassesValidation) { shellAsset.panelRegistry.panels.front().presentationKind); } -TEST(EditorShellAssetValidationTest, ProductManifestIsPureDataAndCompositionIsComplete) { - const auto manifestValidation = ValidateEditorProductManifest(); - EXPECT_EQ(manifestValidation.code, EditorProductManifestValidationCode::None) - << manifestValidation.message; - ASSERT_TRUE(manifestValidation.IsValid()); - - const auto compositionValidation = ValidateEditorWorkspacePanelComposition(); - EXPECT_EQ( - compositionValidation.code, - EditorWorkspacePanelCompositionValidationCode::None) - << compositionValidation.message; - ASSERT_TRUE(compositionValidation.IsValid()); +TEST(EditorShellAssetValidationTest, ProductRegistryAndCompositionAreComplete) { + const auto registryValidation = ValidateEditorProductRegistry(); + ASSERT_TRUE(registryValidation.IsValid()) << registryValidation.message; const auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths()); - ASSERT_EQ(shellAsset.panelRegistry.panels.size(), GetEditorProductPanels().size()); + ASSERT_EQ(shellAsset.panelRegistry.panels.size(), GetEditorWorkspaceFeatures().size()); - const auto* gamePanel = FindEditorProductPanel(kGamePanelId); + const auto* gamePanel = FindEditorWorkspaceFeature(kGameWorkspaceFeaturePanelId); ASSERT_NE(gamePanel, nullptr); EXPECT_EQ(gamePanel->viewportKind, EditorProductViewportKind::Game); EXPECT_TRUE(gamePanel->viewportPlaceholderStatus.empty()); diff --git a/tests/UI/Editor/unit/test_editor_store.cpp b/tests/UI/Editor/unit/test_editor_store.cpp new file mode 100644 index 00000000..8d1f7f93 --- /dev/null +++ b/tests/UI/Editor/unit/test_editor_store.cpp @@ -0,0 +1,100 @@ +#include + +#include "Product/Runtime/Store/EditorStore.h" + +#include + +#include +#include + +namespace XCEngine::UI::Editor::Product { +namespace { + +TEST(EditorStoreTests, SetSceneDocumentStateNotifiesObserversWhenSessionChanges) { + EditorStore store = {}; + EditorState initialState = {}; + initialState.session.projectRoot = "D:/Project"; + store.Reset(std::move(initialState)); + + int notificationCount = 0; + EditorState observedState = {}; + store.Subscribe( + [&](const EditorState& state) { + ++notificationCount; + observedState = state; + }); + + const EditorStore::DispatchResult result = store.Dispatch( + EditorCommand::SetSceneDocumentState( + "D:/Project/Assets/Scenes/Main.xc", + "Main", + true)); + + EXPECT_TRUE(result.handled); + EXPECT_TRUE(result.stateChanged); + EXPECT_EQ(notificationCount, 1); + EXPECT_EQ( + observedState.session.currentScenePath, + std::filesystem::path("D:/Project/Assets/Scenes/Main.xc")); + EXPECT_EQ(observedState.session.currentSceneName, "Main"); + EXPECT_TRUE(observedState.session.sceneDocumentDirty); +} + +TEST(EditorStoreTests, SetCurrentProjectFolderSkipsNotificationsWhenSessionDoesNotChange) { + EditorStore store = {}; + store.Reset({}); + + int notificationCount = 0; + store.Subscribe( + [&](const EditorState&) { + ++notificationCount; + }); + + const EditorStore::DispatchResult result = store.Dispatch( + EditorCommand::SetCurrentProjectFolder("Assets")); + + EXPECT_TRUE(result.handled); + EXPECT_FALSE(result.stateChanged); + EXPECT_EQ(notificationCount, 0); +} + +TEST(EditorStoreTests, SetWindowWorkspaceStateCommitsAuthoritativeWindowDraft) { + EditorStore store = {}; + EditorState initialState = {}; + initialState.windowWorkspace.primaryWindowId = "main"; + initialState.windowWorkspace.activeWindowId = "main"; + + UIEditorWindowWorkspaceState mainWindow = {}; + mainWindow.windowId = "main"; + initialState.windowWorkspace.windows.push_back(mainWindow); + store.Reset(std::move(initialState)); + + UIEditorWorkspaceModel nextWorkspace = {}; + nextWorkspace.root = BuildUIEditorWorkspaceSingleTabStack( + "root-tabs", + "scene", + "Scene", + true); + nextWorkspace.activePanelId = "scene"; + UIEditorWorkspaceSession nextSession = {}; + + int notificationCount = 0; + store.Subscribe( + [&](const EditorState&) { + ++notificationCount; + }); + + const EditorStore::DispatchResult result = store.Dispatch( + EditorCommand::SetWindowWorkspaceState("main", nextWorkspace, nextSession)); + + ASSERT_TRUE(result.handled); + ASSERT_TRUE(result.stateChanged); + EXPECT_EQ(notificationCount, 1); + const UIEditorWindowWorkspaceState* const storedWindow = + store.FindWindowWorkspaceState("main"); + ASSERT_NE(storedWindow, nullptr); + EXPECT_EQ(storedWindow->workspace.activePanelId, "scene"); +} + +} // namespace +} // namespace XCEngine::UI::Editor::Product diff --git a/tests/UI/Editor/unit/test_editor_window_frame_orchestrator.cpp b/tests/UI/Editor/unit/test_editor_window_frame_orchestrator.cpp index fc9f8f56..3dbdbcc7 100644 --- a/tests/UI/Editor/unit/test_editor_window_frame_orchestrator.cpp +++ b/tests/UI/Editor/unit/test_editor_window_frame_orchestrator.cpp @@ -1,46 +1,18 @@ #include -#include "Frame/EditorWindowFrameOrchestrator.h" +#include "Product/Runtime/Diagnostics/EditorFrameStatusController.h" +#include "Product/Runtime/Windowing/Frame/EditorWindowFrameOrchestrator.h" #include #include #include #include +#include namespace XCEngine::UI::Editor::App { namespace { -class FakeShellDefinitionProvider final : public EditorShellDefinitionProvider { -public: - UIEditorShellInteractionDefinition BuildShellDefinition( - const UIEditorWorkspaceController&, - std::string_view, - std::string_view, - EditorShellVariant) const override { - return {}; - } -}; - -class FakeFrameStatusService final : public EditorFrameStatusService { -public: - void SetStatus(std::string, std::string) override {} - void SetReadyStatus() override {} - std::string ComposeStatusText() const override { return {}; } - void UpdateStatusFromShellResult( - const UIEditorWorkspaceController&, - const UIEditorShellInteractionResult&) override {} - std::string DescribeWorkspaceState( - const UIEditorWorkspaceController&, - const UIEditorShellInteractionState&) const override { - return {}; - } - std::vector SyncWorkspacePanelFrameEvents( - const std::vector&) override { - return {}; - } -}; - class FakeWorkspaceShellRuntime final : public EditorWorkspaceShellRuntime { public: void Initialize( @@ -60,8 +32,6 @@ public: void SetViewportSurfacePresentationEnabled(bool) override {} void Update( - const EditorShellDefinitionProvider&, - EditorFrameStatusService&, UIEditorWorkspaceController&, const ::XCEngine::UI::UIRect&, const std::vector<::XCEngine::UI::UIInputEvent>&, @@ -135,8 +105,7 @@ public: TEST(EditorWindowFrameOrchestratorTests, UtilityWindowRequestsFlowThroughFrameTransferRequests) { EditorWindowFrameOrchestrator orchestrator = {}; - FakeShellDefinitionProvider shellDefinitionProvider = {}; - FakeFrameStatusService frameStatusService = {}; + EditorFrameStatusController frameStatusController = {}; FakeWorkspaceShellRuntime shellRuntime = {}; UIEditorWorkspaceController workspaceController = {}; ::XCEngine::UI::UIDrawData drawData = {}; @@ -152,8 +121,7 @@ TEST(EditorWindowFrameOrchestratorTests, UtilityWindowRequestsFlowThroughFrameTr const EditorWindowFrameTransferRequests transferRequests = orchestrator.UpdateAndAppend( - shellDefinitionProvider, - frameStatusService, + frameStatusController, &consumeUtilityWindowRequest, workspaceController, shellRuntime, diff --git a/tests/UI/Editor/unit/test_editor_window_input_routing.cpp b/tests/UI/Editor/unit/test_editor_window_input_routing.cpp index 70204593..0497464c 100644 --- a/tests/UI/Editor/unit/test_editor_window_input_routing.cpp +++ b/tests/UI/Editor/unit/test_editor_window_input_routing.cpp @@ -2,7 +2,7 @@ #include #include -#include "EditorWindowPointerCapture.h" +#include "Product/Framework/Windowing/EditorWindowPointerCapture.h" #include diff --git a/tests/UI/Editor/unit/test_game_viewport_runtime.cpp b/tests/UI/Editor/unit/test_game_viewport_runtime.cpp index 8eb20003..831fc37e 100644 --- a/tests/UI/Editor/unit/test_game_viewport_runtime.cpp +++ b/tests/UI/Editor/unit/test_game_viewport_runtime.cpp @@ -1,9 +1,9 @@ #include -#include "Game/GameViewportFeature.h" -#include "Panels/EditorPanelIds.h" -#include "State/EditorCommandFocusService.h" -#include "Viewport/GameViewportRenderService.h" +#include "Product/Features/Workspace/Game/GameViewportFeature.h" +#include "Product/Features/Workspace/Game/GameWorkspaceFeature.h" +#include "Product/Rendering/Viewport/GameViewportRenderService.h" +#include "Product/State/EditorCommandFocusService.h" #include @@ -39,7 +39,7 @@ UIEditorWorkspaceComposeState BuildGameComposeState( const UIEditorViewportInputBridgeState& inputBridgeState) { UIEditorWorkspaceComposeState composeState = {}; UIEditorWorkspacePanelPresentationState panelState = {}; - panelState.panelId = std::string(kGamePanelId); + panelState.panelId = std::string(kGameWorkspaceFeaturePanelId); panelState.viewportShellState.inputBridgeState = inputBridgeState; composeState.panelStates.push_back(std::move(panelState)); return composeState; @@ -51,7 +51,7 @@ UIEditorWorkspaceComposeFrame BuildGameComposeFrame( const UISize& requestedViewportSize) { UIEditorWorkspaceComposeFrame composeFrame = {}; UIEditorWorkspaceViewportComposeFrame viewportFrame = {}; - viewportFrame.panelId = std::string(kGamePanelId); + viewportFrame.panelId = std::string(kGameWorkspaceFeaturePanelId); viewportFrame.viewportShellFrame.inputFrame = inputFrame; viewportFrame.viewportShellFrame.requestedViewportSize = requestedViewportSize; viewportFrame.viewportShellFrame.slotLayout.inputRect = inputRect; diff --git a/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp b/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp index 254472a4..be5f9400 100644 --- a/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp +++ b/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp @@ -1,5 +1,5 @@ -#include "Hierarchy/HierarchyModel.h" -#include "Scene/EngineEditorSceneBackend.h" +#include "Product/Features/Workspace/Hierarchy/HierarchyModel.h" +#include "Product/Services/Scene/EngineEditorSceneBackend.h" #include #include diff --git a/tests/UI/Editor/unit/test_inspector_presentation.cpp b/tests/UI/Editor/unit/test_inspector_presentation.cpp index a87088aa..d0b04857 100644 --- a/tests/UI/Editor/unit/test_inspector_presentation.cpp +++ b/tests/UI/Editor/unit/test_inspector_presentation.cpp @@ -1,11 +1,11 @@ -#include "Inspector/Components/IInspectorComponentEditor.h" -#include "Inspector/Components/InspectorComponentEditorRegistry.h" -#include "Inspector/InspectorFieldValueApplier.h" -#include "Inspector/InspectorPresentationModel.h" -#include "Inspector/InspectorSubject.h" -#include "Project/EditorProjectRuntime.h" -#include "Scene/EditorSceneRuntime.h" -#include "Scene/EngineEditorSceneBackend.h" +#include "Product/Features/Workspace/Inspector/Components/IInspectorComponentEditor.h" +#include "Product/Features/Workspace/Inspector/Components/InspectorComponentEditorRegistry.h" +#include "Product/Features/Workspace/Inspector/InspectorFieldValueApplier.h" +#include "Product/Features/Workspace/Inspector/InspectorPresentationModel.h" +#include "Product/Features/Workspace/Inspector/InspectorSubject.h" +#include "Product/Services/Project/EditorProjectRuntime.h" +#include "Product/Services/Scene/EditorSceneRuntime.h" +#include "Product/Services/Scene/EngineEditorSceneBackend.h" #include #include diff --git a/tests/UI/Editor/unit/test_project_browser_model.cpp b/tests/UI/Editor/unit/test_project_browser_model.cpp index 12e137b5..e99cee0a 100644 --- a/tests/UI/Editor/unit/test_project_browser_model.cpp +++ b/tests/UI/Editor/unit/test_project_browser_model.cpp @@ -1,4 +1,4 @@ -#include "Project/ProjectBrowserModel.h" +#include "Product/Services/Project/ProjectBrowserModel.h" #include diff --git a/tests/UI/Editor/unit/test_project_panel.cpp b/tests/UI/Editor/unit/test_project_panel.cpp index 2585670b..b5b8e10c 100644 --- a/tests/UI/Editor/unit/test_project_panel.cpp +++ b/tests/UI/Editor/unit/test_project_panel.cpp @@ -1,8 +1,7 @@ -#include "Project/ProjectPanel.h" -#include "Assets/EditorIconService.h" -#include "SystemInteractionService.h" - -#include "Panels/EditorPanelIds.h" +#include "Product/Core/Assets/EditorIconService.h" +#include "Product/Features/Workspace/Project/ProjectPanel.h" +#include "Product/Features/Workspace/Project/ProjectWorkspaceFeature.h" +#include "Product/Framework/System/SystemInteractionService.h" #include @@ -117,7 +116,7 @@ private: UIEditorHostedPanelDispatchEntry MakeProjectDispatchEntry() { UIEditorHostedPanelDispatchEntry entry = {}; - entry.panelId = std::string(kProjectPanelId); + entry.panelId = std::string(kProjectWorkspaceFeaturePanelId); entry.presentationKind = UIEditorPanelPresentationKind::HostedContent; entry.mounted = true; entry.bounds = ::XCEngine::UI::UIRect(0.0f, 0.0f, 640.0f, 360.0f); diff --git a/tests/UI/Editor/unit/test_scene_viewport_render_plan.cpp b/tests/UI/Editor/unit/test_scene_viewport_render_plan.cpp index 1bdd635f..acf98fc0 100644 --- a/tests/UI/Editor/unit/test_scene_viewport_render_plan.cpp +++ b/tests/UI/Editor/unit/test_scene_viewport_render_plan.cpp @@ -1,4 +1,4 @@ -#include "Viewport/SceneViewportRenderPlan.h" +#include "Product/Rendering/Viewport/SceneViewportRenderPlan.h" #include diff --git a/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp b/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp index 52380fcd..e5aab362 100644 --- a/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp +++ b/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp @@ -1,16 +1,15 @@ -#include "Scene/EditorSceneRuntime.h" -#include "Scene/EngineEditorSceneBackend.h" -#include "Scene/SceneViewportController.h" -#include "Scene/SceneViewportSession.h" -#include "Inspector/InspectorSubject.h" -#include "Project/EditorProjectRuntime.h" -#include "Viewport/SceneViewportRenderService.h" -#include "Viewport/ViewportHostService.h" -#include "Viewport/ViewportRenderTargets.h" -#include "Viewport/ViewportRenderTargetUtils.h" -#include "State/EditorSelectionService.h" - -#include "Panels/EditorPanelIds.h" +#include "Product/Features/Workspace/Inspector/InspectorSubject.h" +#include "Product/Features/Workspace/Scene/SceneViewportController.h" +#include "Product/Features/Workspace/Scene/SceneViewportSession.h" +#include "Product/Features/Workspace/Scene/SceneWorkspaceFeature.h" +#include "Product/Rendering/Viewport/SceneViewportRenderService.h" +#include "Product/Rendering/Viewport/ViewportHostService.h" +#include "Product/Rendering/Viewport/ViewportRenderTargets.h" +#include "Product/Rendering/Viewport/ViewportRenderTargetUtils.h" +#include "Product/Services/Project/EditorProjectRuntime.h" +#include "Product/Services/Scene/EditorSceneRuntime.h" +#include "Product/Services/Scene/EngineEditorSceneBackend.h" +#include "Product/State/EditorSelectionService.h" #include #include @@ -185,7 +184,8 @@ UIEditorWorkspaceComposeState BuildSceneComposeState( const UIEditorViewportInputBridgeState& inputBridgeState) { UIEditorWorkspaceComposeState composeState = {}; UIEditorWorkspacePanelPresentationState panelState = {}; - panelState.panelId = std::string(::XCEngine::UI::Editor::App::kScenePanelId); + panelState.panelId = std::string( + ::XCEngine::UI::Editor::App::kSceneWorkspaceFeaturePanelId); panelState.viewportShellState.inputBridgeState = inputBridgeState; composeState.panelStates.push_back(std::move(panelState)); return composeState; @@ -197,7 +197,8 @@ UIEditorWorkspaceComposeFrame BuildSceneComposeFrame( const UISize& requestedViewportSize) { UIEditorWorkspaceComposeFrame composeFrame = {}; UIEditorWorkspaceViewportComposeFrame viewportFrame = {}; - viewportFrame.panelId = std::string(::XCEngine::UI::Editor::App::kScenePanelId); + viewportFrame.panelId = std::string( + ::XCEngine::UI::Editor::App::kSceneWorkspaceFeaturePanelId); viewportFrame.viewportShellFrame.inputFrame = inputFrame; viewportFrame.viewportShellFrame.requestedViewportSize = requestedViewportSize; viewportFrame.viewportShellFrame.slotLayout.inputRect = inputRect; diff --git a/tests/UI/Editor/unit/test_viewport_object_id_picker.cpp b/tests/UI/Editor/unit/test_viewport_object_id_picker.cpp index d6539e13..966c3025 100644 --- a/tests/UI/Editor/unit/test_viewport_object_id_picker.cpp +++ b/tests/UI/Editor/unit/test_viewport_object_id_picker.cpp @@ -1,4 +1,4 @@ -#include "Viewport/ViewportObjectIdPicker.h" +#include "Product/Rendering/Viewport/ViewportObjectIdPicker.h" #include