diff --git a/Assets.meta b/Assets.meta new file mode 100644 index 00000000..8357e494 --- /dev/null +++ b/Assets.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 1 +guid: 60a2f7fbbea9b0ef86cefe4d4ea75578 +folderAsset: true +importer: FolderImporter +importerVersion: 5 diff --git a/Assets/XCUI.meta b/Assets/XCUI.meta new file mode 100644 index 00000000..264e260a --- /dev/null +++ b/Assets/XCUI.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 1 +guid: 6dd3f8fecfde5a3f1d52cb3a9e18ed53 +folderAsset: true +importer: FolderImporter +importerVersion: 5 diff --git a/Assets/XCUI/NewEditor.meta b/Assets/XCUI/NewEditor.meta new file mode 100644 index 00000000..3f5f5f75 --- /dev/null +++ b/Assets/XCUI/NewEditor.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 1 +guid: b6816d88212c73fe81921340c22e0939 +folderAsset: true +importer: FolderImporter +importerVersion: 5 diff --git a/Assets/XCUI/NewEditor/Demo.meta b/Assets/XCUI/NewEditor/Demo.meta new file mode 100644 index 00000000..1aefbdaa --- /dev/null +++ b/Assets/XCUI/NewEditor/Demo.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 1 +guid: de79bdd523120a9286b09d0a06bf150c +folderAsset: true +importer: FolderImporter +importerVersion: 5 diff --git a/Assets/XCUI/NewEditor/Demo/Theme.xctheme b/Assets/XCUI/NewEditor/Demo/Theme.xctheme new file mode 100644 index 00000000..5ccb70bb --- /dev/null +++ b/Assets/XCUI/NewEditor/Demo/Theme.xctheme @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets/XCUI/NewEditor/Demo/Theme.xctheme.meta b/Assets/XCUI/NewEditor/Demo/Theme.xctheme.meta new file mode 100644 index 00000000..4f9ae341 --- /dev/null +++ b/Assets/XCUI/NewEditor/Demo/Theme.xctheme.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 1 +guid: 35810d9b2e3744637d2655cf8f6ed2d3 +folderAsset: false +importer: UIThemeImporter +importerVersion: 5 diff --git a/Assets/XCUI/NewEditor/Demo/View.xcui b/Assets/XCUI/NewEditor/Demo/View.xcui new file mode 100644 index 00000000..df8ceded --- /dev/null +++ b/Assets/XCUI/NewEditor/Demo/View.xcui @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets/XCUI/NewEditor/Demo/View.xcui.meta b/Assets/XCUI/NewEditor/Demo/View.xcui.meta new file mode 100644 index 00000000..d2ddf277 --- /dev/null +++ b/Assets/XCUI/NewEditor/Demo/View.xcui.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 1 +guid: 9586ad2d05baa7ff00e16bb3c6eab1ea +folderAsset: false +importer: UIViewImporter +importerVersion: 5 diff --git a/Assets/XCUI/NewEditor/LayoutLab.meta b/Assets/XCUI/NewEditor/LayoutLab.meta new file mode 100644 index 00000000..3bfaa2f6 --- /dev/null +++ b/Assets/XCUI/NewEditor/LayoutLab.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 1 +guid: 2767aae796d78abc773d2576b6bdce54 +folderAsset: true +importer: FolderImporter +importerVersion: 5 diff --git a/Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme b/Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme new file mode 100644 index 00000000..798a42ba --- /dev/null +++ b/Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme.meta b/Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme.meta new file mode 100644 index 00000000..c1749d05 --- /dev/null +++ b/Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 1 +guid: fca776270d9840e08180c456593e683d +folderAsset: false +importer: UIThemeImporter +importerVersion: 5 diff --git a/Assets/XCUI/NewEditor/LayoutLab/View.xcui b/Assets/XCUI/NewEditor/LayoutLab/View.xcui new file mode 100644 index 00000000..99b88dfd --- /dev/null +++ b/Assets/XCUI/NewEditor/LayoutLab/View.xcui @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets/XCUI/NewEditor/LayoutLab/View.xcui.meta b/Assets/XCUI/NewEditor/LayoutLab/View.xcui.meta new file mode 100644 index 00000000..8bce64ff --- /dev/null +++ b/Assets/XCUI/NewEditor/LayoutLab/View.xcui.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 1 +guid: 9a03d17147d903cad459db2c205a9f30 +folderAsset: false +importer: UIViewImporter +importerVersion: 5 diff --git a/CMakeLists.txt b/CMakeLists.txt index 136aadb1..c800b496 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,13 @@ cmake_minimum_required(VERSION 3.15) + +if(MSVC) + if(POLICY CMP0141) + cmake_policy(SET CMP0141 NEW) + endif() + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT + "$<$:Embedded>") +endif() + project(XCEngine) set(CMAKE_CXX_STANDARD 20) @@ -7,6 +16,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) enable_testing() option(XCENGINE_ENABLE_MONO_SCRIPTING "Build the Mono-based C# scripting runtime" ON) +option(XCENGINE_BUILD_NEW_EDITOR "Build the experimental new_editor skeleton" ON) set( XCENGINE_MONO_ROOT_DIR "${CMAKE_SOURCE_DIR}/参考/Fermion/Fermion/external/mono" @@ -15,6 +25,9 @@ set( add_subdirectory(engine) add_subdirectory(editor) +if(XCENGINE_BUILD_NEW_EDITOR) + add_subdirectory(new_editor) +endif() add_subdirectory(managed) add_subdirectory(mvs/RenderDoc) add_subdirectory(tests) diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md new file mode 100644 index 00000000..c4fc8c3d --- /dev/null +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -0,0 +1,78 @@ +# XCUI Phase Status 2026-04-05 + +## Scope + +Current execution stays inside the XCUI module and `new_editor`. +Old `editor` replacement is explicitly out of scope for this phase. + +## Three-Layer Status + +### 1. Common Core + +- `UI::DrawData`, input event types, focus routing, style/theme resolution are in active use. +- `UIDocumentCompiler` was restored to a stable buildable baseline after a broken parallel schema attempt corrupted the file. +- Build-system hardening for MSVC/PDB output paths has started in root CMake, `engine/CMakeLists.txt`, `new_editor/CMakeLists.txt`, and `tests/NewEditor/CMakeLists.txt`. + +Current gap: + +- Schema/validation is not yet landed in a stable form. +- Shared widget/runtime instantiation is still thin and mostly editor-side. + +### 2. Runtime/Game Layer + +- Runtime-side XCUI is still shallow. +- The main concrete progress here is that the retained-mode demo runtime now supports a real `TextField` input path with UTF-8 text entry and backspace handling. +- This proves that the runtime-facing layer is no longer limited to static cards/buttons. + +Current gap: + +- No real game-facing screen host, menu stack, HUD stack, or shared runtime widget library yet. + +### 3. Editor Layer + +- `new_editor` remains the isolated XCUI sandbox. +- Native hosted preview is working as `RHI offscreen surface -> ImGui shell texture embed`. +- `XCUI Demo` remains the long-lived effect and behavior testbed. +- `LayoutLab` now includes a `ScrollView` prototype and a more editor-like three-column authored layout. +- Panel diagnostics were expanded to clearly separate preview/runtime/input state and native vs legacy paths. +- `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`. + +Current gap: + +- The shell is still ImGui-hosted. +- Editor-specialized widgets are still incomplete: tree, list virtualization, property grid, toolbar/menu, text area, icon atlas widgets. + +## Validated This Phase + +- `new_editor_xcui_demo_runtime_tests`: `6/6` +- `new_editor_xcui_layout_lab_runtime_tests`: `5/5` +- `new_editor_xcui_rhi_command_compiler_tests`: `6/6` +- `new_editor_xcui_rhi_render_backend_tests`: `5/5` +- `XCNewEditor` Debug target builds successfully + +## Landed This Phase + +- Demo runtime `TextField` with UTF-8 text insertion, caret state, and backspace. +- Demo authored resources updated to exercise the input field. +- LayoutLab `ScrollView` prototype with clipping and hover rejection outside clipped content. +- RHI image path improvements: + - clipped image UV adjustment + - mirrored image UV preservation + - external texture binding reuse + - per-batch scissor application +- `new_editor` panel/shell diagnostics improvements for hosted preview state. +- XCUI asset document loading changed to prefer direct source compilation before `ResourceManager` fallback for the sandbox path, fixing the LayoutLab crash. + +## Phase Risks Still Open + +- Schema/validation work must be restarted cleanly after the corrupted parallel attempt. +- `ScrollView` is still authored/static; no wheel-driven scrolling or virtualization yet. +- `Image` widgets still do not have source-rect/atlas-subregion level API in the high-level draw command model. +- Editor shell still depends on ImGui as host chrome. + +## Next Phase + +1. Re-open common-layer schema/validation on a clean branch and land the smallest stable version. +2. Add next editor-facing widgets: `TextArea`, list/tree, property-style sections. +3. Move more diagnostics and shell affordances into XCUI-owned editor-layer surfaces instead of only ImGui HUDs. +4. Continue phased validation, commit, push, and plan refresh after each stable batch. diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 5ec1bd5c..06cc2162 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -1,4 +1,9 @@ cmake_minimum_required(VERSION 3.15) + +if(MSVC AND POLICY CMP0141) + cmake_policy(SET CMP0141 NEW) +endif() + project(XCEngineLib) set(CMAKE_CXX_STANDARD 20) @@ -20,6 +25,79 @@ endif() find_package(Vulkan REQUIRED) +set(XCENGINE_OPTIONAL_UI_RESOURCE_SOURCES) +set(XCENGINE_UIDOCUMENT_COMPILER_SOURCE + "${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/UI/UIDocumentCompiler.cpp") +if(EXISTS "${XCENGINE_UIDOCUMENT_COMPILER_SOURCE}") + list(APPEND XCENGINE_OPTIONAL_UI_RESOURCE_SOURCES + ${XCENGINE_UIDOCUMENT_COMPILER_SOURCE} + ) +else() + set(XCENGINE_UIDOCUMENT_COMPILER_FALLBACK_SOURCE + "${CMAKE_CURRENT_BINARY_DIR}/generated/UIDocumentCompilerFallback.cpp") + file(GENERATE OUTPUT "${XCENGINE_UIDOCUMENT_COMPILER_FALLBACK_SOURCE}" CONTENT [=[ +#include + +namespace XCEngine { +namespace Resources { + +namespace { +constexpr const char* kUnavailableMessage = + "UIDocument compiler implementation is unavailable in this workspace."; +} + +bool CompileUIDocument(const UIDocumentCompileRequest&, UIDocumentCompileResult& outResult) +{ + outResult = {}; + outResult.errorMessage = Containers::String(kUnavailableMessage); + outResult.succeeded = false; + return false; +} + +bool WriteUIDocumentArtifact( + const Containers::String&, + const UIDocumentCompileResult&, + Containers::String* outErrorMessage) +{ + if (outErrorMessage != nullptr) { + *outErrorMessage = Containers::String(kUnavailableMessage); + } + return false; +} + +bool LoadUIDocumentArtifact( + const Containers::String&, + UIDocumentKind, + UIDocumentCompileResult& outResult) +{ + outResult = {}; + outResult.errorMessage = Containers::String(kUnavailableMessage); + outResult.succeeded = false; + return false; +} + +Containers::String GetUIDocumentDefaultRootTag(UIDocumentKind kind) +{ + switch (kind) { + case UIDocumentKind::Theme: + return Containers::String("Theme"); + case UIDocumentKind::Schema: + return Containers::String("Schema"); + case UIDocumentKind::View: + default: + return Containers::String("View"); + } +} + +} // namespace Resources +} // namespace XCEngine +]=]) + list(APPEND XCENGINE_OPTIONAL_UI_RESOURCE_SOURCES + "${XCENGINE_UIDOCUMENT_COMPILER_FALLBACK_SOURCE}" + ) + message(STATUS "UIDocumentCompiler.cpp is missing; using generated fallback implementation for build stability.") +endif() + add_library(XCEngine STATIC # Core (Types, RefCounted, SmartPtr, Event) ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Types.h @@ -309,7 +387,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioClip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/UI/UIDocumentCompiler.cpp + ${XCENGINE_OPTIONAL_UI_RESOURCE_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/UI/UIDocuments.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/UI/UIDocumentLoaders.cpp @@ -504,6 +582,16 @@ target_link_libraries(XCEngine PUBLIC if(MSVC) target_link_libraries(XCEngine PUBLIC delayimp) target_link_options(XCEngine INTERFACE "/DELAYLOAD:assimp-vc143-mt.dll") + set_target_properties(XCEngine PROPERTIES + MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>" + COMPILE_PDB_NAME "XCEngine-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" + VS_GLOBAL_UseMultiToolTask "false" + ) endif() if(XCENGINE_ENABLE_MONO_SCRIPTING) diff --git a/engine/include/XCEngine/UI/Types.h b/engine/include/XCEngine/UI/Types.h index 7024c667..da0c88f4 100644 --- a/engine/include/XCEngine/UI/Types.h +++ b/engine/include/XCEngine/UI/Types.h @@ -5,6 +5,11 @@ namespace XCEngine { namespace UI { +enum class UITextureHandleKind : std::uint8_t { + ImGuiDescriptor = 0, + ShaderResourceView +}; + struct UIPoint { float x = 0.0f; float y = 0.0f; @@ -46,6 +51,7 @@ struct UITextureHandle { std::uintptr_t nativeHandle = 0; std::uint32_t width = 0; std::uint32_t height = 0; + UITextureHandleKind kind = UITextureHandleKind::ImGuiDescriptor; constexpr bool IsValid() const { return nativeHandle != 0 && width > 0 && height > 0; diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt new file mode 100644 index 00000000..fd99b3a5 --- /dev/null +++ b/new_editor/CMakeLists.txt @@ -0,0 +1,120 @@ +cmake_minimum_required(VERSION 3.15) + +if(MSVC AND POLICY CMP0141) + cmake_policy(SET CMP0141 NEW) +endif() + +project(XCNewEditor VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_definitions(-DUNICODE -D_UNICODE) + +set(XCENGINE_ROOT_DIR "") +if(EXISTS "${CMAKE_SOURCE_DIR}/engine/CMakeLists.txt") + set(XCENGINE_ROOT_DIR "${CMAKE_SOURCE_DIR}") +elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../engine/CMakeLists.txt") + set(XCENGINE_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/..") +else() + message(FATAL_ERROR "Unable to locate XCEngine root directory from new_editor.") +endif() + +if(NOT TARGET XCEngine) + add_subdirectory(${XCENGINE_ROOT_DIR}/engine ${CMAKE_CURRENT_BINARY_DIR}/engine_dependency) +endif() + +set(IMGUI_SOURCE_DIR "${CMAKE_BINARY_DIR}/_deps/imgui-src") +if(NOT EXISTS "${IMGUI_SOURCE_DIR}/imgui.cpp") + include(FetchContent) + FetchContent_Declare( + imgui_new_editor + GIT_REPOSITORY https://gitee.com/mirrors/imgui.git + GIT_TAG docking + GIT_SHALLOW TRUE + ) + FetchContent_MakeAvailable(imgui_new_editor) + set(IMGUI_SOURCE_DIR "${imgui_new_editor_SOURCE_DIR}") +endif() + +if(NOT EXISTS "${IMGUI_SOURCE_DIR}/imgui.cpp") + message(FATAL_ERROR "ImGui source was not found at ${IMGUI_SOURCE_DIR}.") +endif() + +set(NEW_EDITOR_SOURCES + src/main.cpp + src/Application.cpp + src/panels/Panel.cpp + src/panels/XCUIDemoPanel.cpp + src/panels/XCUILayoutLabPanel.cpp + src/Rendering/MainWindowBackdropPass.cpp + src/Rendering/MainWindowNativeBackdropRenderer.cpp + src/XCUIBackend/ImGuiXCUIInputAdapter.cpp + src/XCUIBackend/XCUIEditorFontSetup.cpp + src/XCUIBackend/XCUIAssetDocumentSource.cpp + src/XCUIBackend/XCUIInputBridge.cpp + src/XCUIBackend/XCUIRHICommandCompiler.cpp + src/XCUIBackend/XCUIRHIRenderBackend.cpp + src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp + src/XCUIBackend/XCUIDemoRuntime.cpp + src/XCUIBackend/XCUILayoutLabRuntime.cpp + ${IMGUI_SOURCE_DIR}/imgui.cpp + ${IMGUI_SOURCE_DIR}/imgui_demo.cpp + ${IMGUI_SOURCE_DIR}/imgui_draw.cpp + ${IMGUI_SOURCE_DIR}/imgui_tables.cpp + ${IMGUI_SOURCE_DIR}/imgui_widgets.cpp + ${IMGUI_SOURCE_DIR}/backends/imgui_impl_win32.cpp + ${IMGUI_SOURCE_DIR}/backends/imgui_impl_dx12.cpp +) + +add_executable(${PROJECT_NAME} WIN32 ${NEW_EDITOR_SOURCES}) + +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${XCENGINE_ROOT_DIR}/engine/include + ${XCENGINE_ROOT_DIR}/editor/src + ${IMGUI_SOURCE_DIR} + ${IMGUI_SOURCE_DIR}/backends +) + +file(TO_CMAKE_PATH "${XCENGINE_ROOT_DIR}" XCENGINE_ROOT_DIR_CMAKE) + +target_compile_definitions(${PROJECT_NAME} PRIVATE + UNICODE + _UNICODE + XCENGINE_NEW_EDITOR_REPO_ROOT="${XCENGINE_ROOT_DIR_CMAKE}" +) +target_compile_options(${PROJECT_NAME} PRIVATE /utf-8) + +if(MSVC) + target_compile_options(${PROJECT_NAME} PRIVATE /FS) + target_link_options(${PROJECT_NAME} PRIVATE + $<$:/INCREMENTAL:NO>) + set_property(TARGET ${PROJECT_NAME} PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + set_target_properties(${PROJECT_NAME} PROPERTIES + MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>" + COMPILE_PDB_NAME "XCNewEditor-compile" + COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/new_editor/compile-pdb" + COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/new_editor/compile-pdb/Debug" + COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/new_editor/compile-pdb/Release" + COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_BINARY_DIR}/new_editor/compile-pdb/MinSizeRel" + COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/new_editor/compile-pdb/RelWithDebInfo" + VS_GLOBAL_UseMultiToolTask "false" + ) +endif() + +target_link_libraries(${PROJECT_NAME} PRIVATE + XCEngine + d3d12.lib + dxgi.lib + user32 + gdi32 +) + +set_target_properties(${PROJECT_NAME} PROPERTIES + OUTPUT_NAME "XCNewEditor" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/new_editor/bin" + PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/new_editor/bin" + VS_DEBUGGER_WORKING_DIRECTORY "${XCENGINE_ROOT_DIR}" +) diff --git a/new_editor/README.md b/new_editor/README.md new file mode 100644 index 00000000..69bb68f3 --- /dev/null +++ b/new_editor/README.md @@ -0,0 +1,16 @@ +# new_editor XCUI sandbox + +`new_editor` is an **isolated playground** for the XCUI experiment. It is intentionally separated from the main `editor` so you can redesign the UI stack without destabilizing the current ImGui-based editor. + +## Current scope +- Provides a minimal Win32 + ImGui host (scaffolded elsewhere) that links the engine and XCUI code. +- Hosts a single demo panel that loads XCUI documents and reroutes draw commands through `ImGuiTransitionBackend`. +- Bundles themed XCUI documents under `resources/` so the demo can run without touching the primary editor's assets. + +## Resources +`resources/xcui_demo_view.xcui` describes the card layout, placeholder texts, and debug markers the demo renders. `resources/xcui_demo_theme.xctheme` defines colors, spacing, and tokenized styles that the runtime resolves. + +## Next steps +1. Expand the demo UI to exercise more layout primitives (stack, grid, overlays) and token-based styling. +2. Hook the runtime into actual document hot-reload so the panel responds to source edits. +3. Use `new_editor` to prototype a native XCUI shell before deciding whether to migrate the real editor. diff --git a/new_editor/resources/xcui_demo_theme.xctheme b/new_editor/resources/xcui_demo_theme.xctheme new file mode 100644 index 00000000..c2745cb0 --- /dev/null +++ b/new_editor/resources/xcui_demo_theme.xctheme @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/new_editor/resources/xcui_demo_view.xcui b/new_editor/resources/xcui_demo_view.xcui new file mode 100644 index 00000000..aa655414 --- /dev/null +++ b/new_editor/resources/xcui_demo_view.xcui @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/new_editor/resources/xcui_layout_lab_theme.xctheme b/new_editor/resources/xcui_layout_lab_theme.xctheme new file mode 100644 index 00000000..57c2f96e --- /dev/null +++ b/new_editor/resources/xcui_layout_lab_theme.xctheme @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/new_editor/resources/xcui_layout_lab_view.xcui b/new_editor/resources/xcui_layout_lab_view.xcui new file mode 100644 index 00000000..cb4d8410 --- /dev/null +++ b/new_editor/resources/xcui_layout_lab_view.xcui @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/new_editor/src/Application.cpp b/new_editor/src/Application.cpp new file mode 100644 index 00000000..a1641bb9 --- /dev/null +++ b/new_editor/src/Application.cpp @@ -0,0 +1,931 @@ +#include "Application.h" + +#include + +#include + +#include +#include + +namespace XCEngine { +namespace NewEditor { + +namespace { +constexpr wchar_t kWindowClassName[] = L"XCNewEditorWindowClass"; +constexpr wchar_t kWindowTitle[] = L"XCNewEditor"; +constexpr float kClearColor[4] = { 0.08f, 0.09f, 0.11f, 1.0f }; +constexpr float kHostedPreviewClearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + +template +void ShutdownAndDelete(ResourceType*& resource) { + if (resource == nullptr) { + return; + } + + resource->Shutdown(); + delete resource; + resource = nullptr; +} + +void ConfigureFonts() { + ImGuiIO& io = ImGui::GetIO(); + ImFont* uiFont = nullptr; + ::XCEngine::Editor::XCUIBackend::BuildDefaultXCUIEditorFontAtlas(*io.Fonts, uiFont); + io.FontDefault = uiFont; +} + +const char* GetHostedPreviewPathLabel(bool nativeRequested, bool nativePresenterBound) { + if (nativeRequested && nativePresenterBound) { + return "native queued offscreen surface"; + } + if (nativeRequested) { + return "native requested, legacy presenter bound"; + } + if (nativePresenterBound) { + return "legacy requested, native presenter still bound"; + } + return "legacy imgui transition"; +} + +const char* GetHostedPreviewStateLabel( + bool hostedPreviewEnabled, + bool nativePresenterBound, + bool presentedThisFrame, + bool queuedToNativePassThisFrame, + bool surfaceImageAvailable, + bool surfaceAllocated, + bool surfaceReady, + bool descriptorAvailable) { + if (!hostedPreviewEnabled) { + return "disabled"; + } + + if (nativePresenterBound) { + if (surfaceImageAvailable && surfaceReady) { + return "live"; + } + if (queuedToNativePassThisFrame || surfaceAllocated || descriptorAvailable) { + return "warming"; + } + return "awaiting submit"; + } + + if (presentedThisFrame) { + return "live"; + } + return "idle"; +} +} // namespace + +std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> +Application::CreateHostedPreviewPresenter(bool nativePreview) { + if (nativePreview) { + return ::XCEngine::Editor::XCUIBackend::CreateQueuedNativeXCUIHostedPreviewPresenter( + m_hostedPreviewQueue, + m_hostedPreviewSurfaceRegistry); + } + + return ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter(); +} + +void Application::ConfigureHostedPreviewPresenters() { + if (m_demoPanel != nullptr) { + m_demoPanel->SetHostedPreviewEnabled(true); + m_demoPanel->SetHostedPreviewPresenter(CreateHostedPreviewPresenter(m_showNativeDemoPanelPreview)); + } + + if (m_layoutLabPanel != nullptr) { + m_layoutLabPanel->SetHostedPreviewEnabled(true); + m_layoutLabPanel->SetHostedPreviewPresenter(CreateHostedPreviewPresenter(m_showNativeLayoutLabPreview)); + } +} + +Application::HostedPreviewPanelDiagnostics Application::BuildHostedPreviewPanelDiagnostics( + const char* debugName, + const char* fallbackDebugSource, + bool visible, + bool hostedPreviewEnabled, + bool nativeRequested, + bool nativePresenterBound, + const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& previewStats) const { + HostedPreviewPanelDiagnostics diagnostics = {}; + if (debugName != nullptr) { + diagnostics.debugName = debugName; + } + if (fallbackDebugSource != nullptr) { + diagnostics.debugSource = fallbackDebugSource; + } + + diagnostics.visible = visible; + diagnostics.hostedPreviewEnabled = hostedPreviewEnabled; + diagnostics.nativeRequested = nativeRequested; + diagnostics.nativePresenterBound = nativePresenterBound; + diagnostics.presentedThisFrame = previewStats.presented; + diagnostics.queuedToNativePassThisFrame = previewStats.queuedToNativePass; + diagnostics.submittedDrawListCount = previewStats.submittedDrawListCount; + diagnostics.submittedCommandCount = previewStats.submittedCommandCount; + diagnostics.flushedDrawListCount = previewStats.flushedDrawListCount; + diagnostics.flushedCommandCount = previewStats.flushedCommandCount; + + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor descriptor = {}; + if (!diagnostics.debugName.empty() && + m_hostedPreviewSurfaceRegistry.TryGetSurfaceDescriptor(diagnostics.debugName.c_str(), descriptor)) { + diagnostics.descriptorAvailable = true; + diagnostics.surfaceImageAvailable = descriptor.image.IsValid(); + diagnostics.surfaceWidth = descriptor.image.surfaceWidth; + diagnostics.surfaceHeight = descriptor.image.surfaceHeight; + diagnostics.logicalWidth = descriptor.logicalSize.width; + diagnostics.logicalHeight = descriptor.logicalSize.height; + diagnostics.queuedFrameIndex = descriptor.queuedFrameIndex; + if (!descriptor.debugSource.empty()) { + diagnostics.debugSource = descriptor.debugSource; + } + } + + if (!diagnostics.debugName.empty()) { + const HostedPreviewOffscreenSurface* previewSurface = FindHostedPreviewSurface(diagnostics.debugName); + if (previewSurface != nullptr) { + diagnostics.surfaceAllocated = + previewSurface->colorTexture != nullptr || + previewSurface->colorView != nullptr || + previewSurface->width > 0u || + previewSurface->height > 0u; + diagnostics.surfaceReady = previewSurface->IsReady(); + if (diagnostics.surfaceWidth == 0u) { + diagnostics.surfaceWidth = previewSurface->width; + } + if (diagnostics.surfaceHeight == 0u) { + diagnostics.surfaceHeight = previewSurface->height; + } + } + } + + return diagnostics; +} + +int Application::Run(HINSTANCE instance, int nCmdShow) { + if (!CreateMainWindow(instance, nCmdShow)) { + return -1; + } + + m_xcuiInputSource.Reset(); + + auto& resourceManager = ::XCEngine::Resources::ResourceManager::Get(); + resourceManager.Initialize(); + resourceManager.SetResourceRoot(XCENGINE_NEW_EDITOR_REPO_ROOT); + + if (!InitializeRenderer()) { + resourceManager.Shutdown(); + return -1; + } + + InitializeImGui(); + m_demoPanel = std::make_unique( + &m_xcuiInputSource, + CreateHostedPreviewPresenter(m_showNativeDemoPanelPreview)); + m_layoutLabPanel = std::make_unique( + &m_xcuiInputSource, + CreateHostedPreviewPresenter(m_showNativeLayoutLabPreview)); + m_running = true; + m_renderReady = true; + + MSG message = {}; + while (m_running) { + while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) { + if (message.message == WM_QUIT) { + m_running = false; + break; + } + + TranslateMessage(&message); + DispatchMessageW(&message); + } + + if (!m_running) { + break; + } + + Frame(); + } + + m_demoPanel.reset(); + m_layoutLabPanel.reset(); + ShutdownImGui(); + ShutdownRenderer(); + resourceManager.Shutdown(); + return static_cast(message.wParam); +} + +LRESULT CALLBACK Application::StaticWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + Application* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + if (message == WM_NCCREATE) { + const CREATESTRUCTW* createStruct = reinterpret_cast(lParam); + app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + } + + return app != nullptr + ? app->WndProc(hwnd, message, wParam, lParam) + : DefWindowProcW(hwnd, message, wParam, lParam); +} + +LRESULT Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + m_xcuiInputSource.HandleWindowMessage(hwnd, message, wParam, lParam); + + if (::XCEngine::Editor::UI::ImGuiBackendBridge::HandleWindowMessage(hwnd, message, wParam, lParam)) { + return true; + } + + switch (message) { + case WM_SIZE: + if (wParam != SIZE_MINIMIZED && m_renderReady) { + m_windowRenderer.Resize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + case WM_CLOSE: + DestroyWindow(hwnd); + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + case WM_PAINT: { + PAINTSTRUCT paintStruct = {}; + BeginPaint(hwnd, &paintStruct); + if (m_renderReady) { + Frame(); + } + EndPaint(hwnd, &paintStruct); + return 0; + } + default: + return DefWindowProcW(hwnd, message, wParam, lParam); + } +} + +bool Application::CreateMainWindow(HINSTANCE instance, int nCmdShow) { + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(windowClass); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = &Application::StaticWndProc; + windowClass.hInstance = instance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.lpszClassName = kWindowClassName; + if (!RegisterClassExW(&windowClass)) { + return false; + } + + m_hwnd = CreateWindowExW( + 0, + kWindowClassName, + kWindowTitle, + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + 1360, + 840, + nullptr, + nullptr, + instance, + this); + if (m_hwnd == nullptr) { + return false; + } + + ShowWindow(m_hwnd, nCmdShow); + UpdateWindow(m_hwnd); + return true; +} + +bool Application::InitializeRenderer() { + RECT clientRect = {}; + if (!GetClientRect(m_hwnd, &clientRect)) { + return false; + } + + const int width = clientRect.right - clientRect.left; + const int height = clientRect.bottom - clientRect.top; + const bool initialized = width > 0 && + height > 0 && + m_windowRenderer.Initialize(m_hwnd, width, height); + if (initialized) { + m_startTime = std::chrono::steady_clock::now(); + } + return initialized; +} + +void Application::InitializeImGui() { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + ConfigureFonts(); + ImGui::StyleColorsDark(); + m_imguiBackend.Initialize( + m_hwnd, + m_windowRenderer.GetDevice(), + m_windowRenderer.GetCommandQueue(), + m_windowRenderer.GetSrvHeap(), + m_windowRenderer.GetSrvDescriptorSize(), + m_windowRenderer.GetSrvDescriptorCount()); +} + +void Application::ShutdownImGui() { + DestroyHostedPreviewSurfaces(); + m_imguiBackend.Shutdown(); + ImGui::DestroyContext(); +} + +void Application::ShutdownRenderer() { + m_renderReady = false; + m_hostedPreviewRenderBackend.Shutdown(); + m_nativeBackdropRenderer.Shutdown(); + m_windowRenderer.Shutdown(); +} + +void Application::DestroyHostedPreviewSurfaces() { + for (HostedPreviewOffscreenSurface& previewSurface : m_hostedPreviewSurfaces) { + if (previewSurface.imguiCpuHandle.ptr != 0) { + m_imguiBackend.FreeTextureDescriptor(previewSurface.imguiCpuHandle, previewSurface.imguiGpuHandle); + } + ShutdownAndDelete(previewSurface.colorView); + ShutdownAndDelete(previewSurface.colorTexture); + previewSurface = {}; + } + m_hostedPreviewSurfaces.clear(); +} + +void Application::SyncHostedPreviewSurfaces() { + const auto isNativePreviewEnabled = [this](const std::string& debugName) { + return + (debugName == "XCUI Demo" && m_showNativeDemoPanelPreview) || + (debugName == "XCUI Layout Lab" && m_showNativeLayoutLabPreview); + }; + + const auto syncSurfaceForNameAndSize = + [this, &isNativePreviewEnabled]( + const std::string& debugName, + const ::XCEngine::UI::UISize& logicalSize) { + if (!isNativePreviewEnabled(debugName)) { + return; + } + + const std::uint32_t width = logicalSize.width > 1.0f + ? static_cast(std::ceil(logicalSize.width)) + : 0u; + const std::uint32_t height = logicalSize.height > 1.0f + ? static_cast(std::ceil(logicalSize.height)) + : 0u; + if (width == 0u || height == 0u) { + return; + } + + HostedPreviewOffscreenSurface& previewSurface = FindOrAddHostedPreviewSurface(debugName); + EnsureHostedPreviewSurface(previewSurface, width, height); + }; + + for (const auto& descriptor : m_hostedPreviewSurfaceRegistry.GetDescriptors()) { + syncSurfaceForNameAndSize(descriptor.debugName, descriptor.logicalSize); + } + + for (const auto& queuedFrame : m_hostedPreviewQueue.GetQueuedFrames()) { + syncSurfaceForNameAndSize(queuedFrame.debugName, queuedFrame.logicalSize); + } +} + +Application::HostedPreviewOffscreenSurface* Application::FindHostedPreviewSurface(const std::string& debugName) { + for (HostedPreviewOffscreenSurface& previewSurface : m_hostedPreviewSurfaces) { + if (previewSurface.debugName == debugName) { + return &previewSurface; + } + } + + return nullptr; +} + +const Application::HostedPreviewOffscreenSurface* Application::FindHostedPreviewSurface(const std::string& debugName) const { + for (const HostedPreviewOffscreenSurface& previewSurface : m_hostedPreviewSurfaces) { + if (previewSurface.debugName == debugName) { + return &previewSurface; + } + } + + return nullptr; +} + +Application::HostedPreviewOffscreenSurface& Application::FindOrAddHostedPreviewSurface(const std::string& debugName) { + HostedPreviewOffscreenSurface* existingSurface = FindHostedPreviewSurface(debugName); + if (existingSurface != nullptr) { + return *existingSurface; + } + + HostedPreviewOffscreenSurface previewSurface = {}; + previewSurface.debugName = debugName; + m_hostedPreviewSurfaces.push_back(std::move(previewSurface)); + return m_hostedPreviewSurfaces.back(); +} + +bool Application::EnsureHostedPreviewSurface( + HostedPreviewOffscreenSurface& previewSurface, + std::uint32_t width, + std::uint32_t height) { + if (previewSurface.IsReady() && previewSurface.width == width && previewSurface.height == height) { + return true; + } + + if (previewSurface.imguiCpuHandle.ptr != 0) { + m_imguiBackend.FreeTextureDescriptor(previewSurface.imguiCpuHandle, previewSurface.imguiGpuHandle); + } + ShutdownAndDelete(previewSurface.colorView); + ShutdownAndDelete(previewSurface.colorTexture); + previewSurface.imguiCpuHandle = {}; + previewSurface.imguiGpuHandle = {}; + previewSurface.imguiTextureId = {}; + previewSurface.width = width; + previewSurface.height = height; + previewSurface.colorState = ::XCEngine::RHI::ResourceStates::Common; + + if (width == 0u || height == 0u) { + return false; + } + + ::XCEngine::RHI::TextureDesc colorDesc = {}; + colorDesc.width = width; + colorDesc.height = height; + colorDesc.depth = 1; + colorDesc.mipLevels = 1; + colorDesc.arraySize = 1; + colorDesc.format = static_cast(::XCEngine::RHI::Format::R8G8B8A8_UNorm); + colorDesc.textureType = static_cast(::XCEngine::RHI::TextureType::Texture2D); + colorDesc.sampleCount = 1; + previewSurface.colorTexture = m_windowRenderer.GetRHIDevice()->CreateTexture(colorDesc); + if (previewSurface.colorTexture == nullptr) { + return false; + } + + ::XCEngine::RHI::ResourceViewDesc colorViewDesc = {}; + colorViewDesc.format = static_cast(::XCEngine::RHI::Format::R8G8B8A8_UNorm); + colorViewDesc.dimension = ::XCEngine::RHI::ResourceViewDimension::Texture2D; + previewSurface.colorView = + m_windowRenderer.GetRHIDevice()->CreateRenderTargetView(previewSurface.colorTexture, colorViewDesc); + if (previewSurface.colorView == nullptr) { + ShutdownAndDelete(previewSurface.colorTexture); + return false; + } + + if (!m_imguiBackend.CreateTextureDescriptor( + m_windowRenderer.GetRHIDevice(), + previewSurface.colorTexture, + &previewSurface.imguiCpuHandle, + &previewSurface.imguiGpuHandle, + &previewSurface.imguiTextureId)) { + ShutdownAndDelete(previewSurface.colorView); + ShutdownAndDelete(previewSurface.colorTexture); + previewSurface.imguiCpuHandle = {}; + previewSurface.imguiGpuHandle = {}; + previewSurface.imguiTextureId = {}; + return false; + } + + return true; +} + +bool Application::RenderHostedPreviewOffscreenSurface( + HostedPreviewOffscreenSurface& previewSurface, + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::UI::UIDrawData& drawData) { + if (!previewSurface.IsReady() || renderContext.commandList == nullptr) { + return false; + } + + ::XCEngine::Rendering::RenderSurface renderSurface(previewSurface.width, previewSurface.height); + renderSurface.SetColorAttachment(previewSurface.colorView); + renderSurface.SetAutoTransitionEnabled(false); + renderSurface.SetColorStateBefore(::XCEngine::RHI::ResourceStates::RenderTarget); + renderSurface.SetColorStateAfter(::XCEngine::RHI::ResourceStates::RenderTarget); + + renderContext.commandList->TransitionBarrier( + previewSurface.colorView, + previewSurface.colorState, + ::XCEngine::RHI::ResourceStates::RenderTarget); + renderContext.commandList->SetRenderTargets(1, &previewSurface.colorView, nullptr); + renderContext.commandList->ClearRenderTarget(previewSurface.colorView, kHostedPreviewClearColor); + + if (!m_hostedPreviewRenderBackend.Render(renderContext, renderSurface, drawData)) { + previewSurface.colorState = ::XCEngine::RHI::ResourceStates::RenderTarget; + return false; + } + + renderContext.commandList->TransitionBarrier( + previewSurface.colorView, + ::XCEngine::RHI::ResourceStates::RenderTarget, + ::XCEngine::RHI::ResourceStates::PixelShaderResource); + previewSurface.colorState = ::XCEngine::RHI::ResourceStates::PixelShaderResource; + return true; +} + +void Application::RenderShellChrome() { + ImGuiViewport* viewport = ImGui::GetMainViewport(); + if (viewport == nullptr) { + return; + } + + ImGuiWindowFlags windowFlags = + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNavFocus | + ImGuiWindowFlags_MenuBar; + + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + ImGui::SetNextWindowViewport(viewport->ID); + ImGui::SetNextWindowBgAlpha(0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + const bool opened = ImGui::Begin("XCNewEditorShell", nullptr, windowFlags); + ImGui::PopStyleVar(3); + + if (opened) { + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("View")) { + const bool demoVisible = m_demoPanel != nullptr ? m_demoPanel->IsVisible() : false; + bool demoToggle = demoVisible; + if (ImGui::MenuItem("XCUI Demo", nullptr, &demoToggle) && m_demoPanel != nullptr) { + m_demoPanel->SetVisible(demoToggle); + } + + const bool layoutLabVisible = + m_layoutLabPanel != nullptr ? m_layoutLabPanel->IsVisible() : false; + bool layoutLabToggle = layoutLabVisible; + if (ImGui::MenuItem("XCUI Layout Lab", nullptr, &layoutLabToggle) && + m_layoutLabPanel != nullptr) { + m_layoutLabPanel->SetVisible(layoutLabToggle); + } + + ImGui::MenuItem("ImGui Demo", nullptr, &m_showImGuiDemoWindow); + ImGui::Separator(); + ImGui::MenuItem("Native Backdrop", nullptr, &m_showNativeBackdrop); + ImGui::MenuItem("Pulse Accent", nullptr, &m_pulseNativeBackdropAccent); + ImGui::MenuItem("Native XCUI Overlay", nullptr, &m_showNativeXCUIOverlay); + ImGui::MenuItem("Hosted Preview HUD", nullptr, &m_showHostedPreviewHud); + bool nativeDemoPanelPreview = m_showNativeDemoPanelPreview; + if (ImGui::MenuItem("Native Demo Panel Preview", nullptr, &nativeDemoPanelPreview) && + nativeDemoPanelPreview != m_showNativeDemoPanelPreview) { + m_showNativeDemoPanelPreview = nativeDemoPanelPreview; + ConfigureHostedPreviewPresenters(); + } + bool nativeLayoutLabPreview = m_showNativeLayoutLabPreview; + if (ImGui::MenuItem("Native Layout Lab Preview", nullptr, &nativeLayoutLabPreview) && + nativeLayoutLabPreview != m_showNativeLayoutLabPreview) { + m_showNativeLayoutLabPreview = nativeLayoutLabPreview; + ConfigureHostedPreviewPresenters(); + } + ImGui::EndMenu(); + } + + ImGui::SeparatorText("XCUI Sandbox"); + const MainWindowNativeBackdropRenderer::OverlayStats& nativeOverlayStats = + m_nativeBackdropRenderer.GetLastOverlayStats(); + const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameStats& overlayFrameStats = + m_nativeOverlayRuntime.GetFrameResult().stats; + const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewDrainStats& hostedPreviewStats = + m_hostedPreviewQueue.GetLastDrainStats(); + if (m_showNativeXCUIOverlay) { + ImGui::TextDisabled( + "Native XCUI overlay: %s | runtime %zu cmds (%zu fill, %zu outline, %zu text, %zu image, clips %zu/%zu)", + overlayFrameStats.nativeOverlayReady ? "preflight OK" : "preflight issues", + overlayFrameStats.commandCount, + overlayFrameStats.filledRectCommandCount, + overlayFrameStats.rectOutlineCommandCount, + overlayFrameStats.textCommandCount, + overlayFrameStats.imageCommandCount, + overlayFrameStats.clipPushCommandCount, + overlayFrameStats.clipPopCommandCount); + ImGui::TextDisabled( + "%s | supported %zu | unsupported %zu | prev native pass %zu cmds, %zu rendered, %zu skipped", + overlayFrameStats.nativeOverlayStatusMessage.empty() + ? "Overlay diagnostics unavailable" + : overlayFrameStats.nativeOverlayStatusMessage.c_str(), + overlayFrameStats.nativeSupportedCommandCount, + overlayFrameStats.nativeUnsupportedCommandCount, + nativeOverlayStats.commandCount, + nativeOverlayStats.renderedCommandCount, + nativeOverlayStats.skippedCommandCount); + } else { + ImGui::TextDisabled( + m_showNativeBackdrop + ? "Transition backend + runtime diagnostics + native backbuffer pass" + : "Transition backend + runtime diagnostics"); + } + ImGui::TextDisabled( + "Hosted preview queue: %zu frames | queued %zu cmds | rendered %zu cmds | skipped %zu cmds", + hostedPreviewStats.queuedFrameCount, + hostedPreviewStats.queuedCommandCount, + hostedPreviewStats.renderedCommandCount, + hostedPreviewStats.skippedCommandCount); + std::size_t allocatedSurfaceCount = 0u; + std::size_t readySurfaceCount = 0u; + for (const HostedPreviewOffscreenSurface& previewSurface : m_hostedPreviewSurfaces) { + if (previewSurface.colorTexture != nullptr || previewSurface.colorView != nullptr) { + ++allocatedSurfaceCount; + } + if (previewSurface.IsReady()) { + ++readySurfaceCount; + } + } + ImGui::TextDisabled( + "Hosted surfaces: %zu registry entries | %zu allocated | %zu ready", + m_hostedPreviewSurfaceRegistry.GetDescriptors().size(), + allocatedSurfaceCount, + readySurfaceCount); + if (m_demoPanel != nullptr) { + ImGui::TextDisabled( + "XCUI Demo preview: %s", + m_showNativeDemoPanelPreview ? "native offscreen preview surface" : "ImGui hosted preview"); + } + if (m_layoutLabPanel != nullptr) { + ImGui::TextDisabled( + "Layout Lab preview: %s", + m_showNativeLayoutLabPreview ? "native offscreen preview surface" : "ImGui hosted preview"); + } + ImGui::EndMenuBar(); + } + + ImGui::DockSpace( + ImGui::GetID("XCNewEditorDockSpace"), + ImVec2(0.0f, 0.0f), + ImGuiDockNodeFlags_PassthruCentralNode); + } + + ImGui::End(); + + if (m_showHostedPreviewHud) { + RenderHostedPreviewHud(); + } +} + +void Application::RenderHostedPreviewHud() { + ImGuiViewport* viewport = ImGui::GetMainViewport(); + if (viewport == nullptr) { + return; + } + + const HostedPreviewPanelDiagnostics demoDiagnostics = BuildHostedPreviewPanelDiagnostics( + "XCUI Demo", + "new_editor.panels.xcui_demo", + m_demoPanel != nullptr && m_demoPanel->IsVisible(), + m_demoPanel != nullptr && m_demoPanel->IsHostedPreviewEnabled(), + m_showNativeDemoPanelPreview, + m_demoPanel != nullptr && m_demoPanel->IsUsingNativeHostedPreview(), + m_demoPanel != nullptr + ? m_demoPanel->GetLastPreviewStats() + : ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats{}); + const HostedPreviewPanelDiagnostics layoutLabDiagnostics = BuildHostedPreviewPanelDiagnostics( + "XCUI Layout Lab", + "new_editor.panels.xcui_layout_lab", + m_layoutLabPanel != nullptr && m_layoutLabPanel->IsVisible(), + m_layoutLabPanel != nullptr && m_layoutLabPanel->IsHostedPreviewEnabled(), + m_showNativeLayoutLabPreview, + m_layoutLabPanel != nullptr && m_layoutLabPanel->IsUsingNativeHostedPreview(), + m_layoutLabPanel != nullptr + ? m_layoutLabPanel->GetLastPreviewStats() + : ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats{}); + + std::size_t allocatedSurfaceCount = 0u; + std::size_t readySurfaceCount = 0u; + for (const HostedPreviewOffscreenSurface& previewSurface : m_hostedPreviewSurfaces) { + if (previewSurface.colorTexture != nullptr || previewSurface.colorView != nullptr) { + ++allocatedSurfaceCount; + } + if (previewSurface.IsReady()) { + ++readySurfaceCount; + } + } + + const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewDrainStats& drainStats = + m_hostedPreviewQueue.GetLastDrainStats(); + + ImGuiWindowFlags windowFlags = + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav; + + ImGui::SetNextWindowViewport(viewport->ID); + ImGui::SetNextWindowPos( + ImVec2(viewport->WorkPos.x + viewport->WorkSize.x - 18.0f, viewport->WorkPos.y + 42.0f), + ImGuiCond_Always, + ImVec2(1.0f, 0.0f)); + ImGui::SetNextWindowBgAlpha(0.9f); + if (!ImGui::Begin("XCUI Hosted Preview HUD", nullptr, windowFlags)) { + ImGui::End(); + return; + } + + ImGui::TextUnformatted("XCUI Hosted Preview"); + ImGui::Text( + "Registry %zu | surfaces %zu/%zu ready | last native drain %zu rendered, %zu skipped", + m_hostedPreviewSurfaceRegistry.GetDescriptors().size(), + readySurfaceCount, + allocatedSurfaceCount, + drainStats.renderedFrameCount, + drainStats.skippedFrameCount); + ImGui::Separator(); + + const auto drawPanelRow = [](const HostedPreviewPanelDiagnostics& diagnostics) { + const char* const pathLabel = + GetHostedPreviewPathLabel(diagnostics.nativeRequested, diagnostics.nativePresenterBound); + const char* const stateLabel = GetHostedPreviewStateLabel( + diagnostics.hostedPreviewEnabled, + diagnostics.nativePresenterBound, + diagnostics.presentedThisFrame, + diagnostics.queuedToNativePassThisFrame, + diagnostics.surfaceImageAvailable, + diagnostics.surfaceAllocated, + diagnostics.surfaceReady, + diagnostics.descriptorAvailable); + + ImGui::Text( + "%s [%s] %s", + diagnostics.debugName.c_str(), + diagnostics.visible ? "visible" : "hidden", + stateLabel); + ImGui::TextDisabled("%s", pathLabel); + ImGui::Text( + "source %s | submit %zu lists / %zu cmds | flush %zu lists / %zu cmds", + diagnostics.debugSource.empty() ? "n/a" : diagnostics.debugSource.c_str(), + diagnostics.submittedDrawListCount, + diagnostics.submittedCommandCount, + diagnostics.flushedDrawListCount, + diagnostics.flushedCommandCount); + if (diagnostics.nativePresenterBound) { + ImGui::Text( + "surface %ux%u | logical %.0f x %.0f | descriptor %s | image %s | submit->native %s", + diagnostics.surfaceWidth, + diagnostics.surfaceHeight, + diagnostics.logicalWidth, + diagnostics.logicalHeight, + diagnostics.descriptorAvailable ? "yes" : "no", + diagnostics.surfaceImageAvailable ? "yes" : "no", + diagnostics.queuedToNativePassThisFrame ? "yes" : "no"); + } else { + ImGui::Text( + "legacy present %s | cached native surface %s", + diagnostics.presentedThisFrame ? "yes" : "no", + (diagnostics.surfaceAllocated || diagnostics.surfaceImageAvailable) ? "retained" : "none"); + } + }; + + drawPanelRow(demoDiagnostics); + ImGui::Separator(); + drawPanelRow(layoutLabDiagnostics); + + ImGui::End(); +} + +void Application::RenderQueuedHostedPreviews( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface) { + (void)surface; + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewDrainStats drainStats = {}; + const auto& queuedFrames = m_hostedPreviewQueue.GetQueuedFrames(); + drainStats.queuedFrameCount = queuedFrames.size(); + for (const auto& queuedFrame : queuedFrames) { + drainStats.queuedDrawListCount += queuedFrame.drawData.GetDrawListCount(); + drainStats.queuedCommandCount += queuedFrame.drawData.GetTotalCommandCount(); + } + + if (queuedFrames.empty()) { + m_hostedPreviewQueue.SetLastDrainStats(drainStats); + return; + } + + m_hostedPreviewRenderBackend.SetTextAtlasProvider(&m_hostedPreviewTextAtlasProvider); + std::size_t queuedFrameIndex = 0u; + for (const auto& queuedFrame : queuedFrames) { + m_hostedPreviewSurfaceRegistry.RecordQueuedFrame(queuedFrame, queuedFrameIndex); + if (queuedFrame.debugName.empty()) { + ++drainStats.skippedFrameCount; + ++queuedFrameIndex; + continue; + } + + const std::uint32_t expectedWidth = queuedFrame.logicalSize.width > 1.0f + ? static_cast(std::ceil(queuedFrame.logicalSize.width)) + : 0u; + const std::uint32_t expectedHeight = queuedFrame.logicalSize.height > 1.0f + ? static_cast(std::ceil(queuedFrame.logicalSize.height)) + : 0u; + if (expectedWidth == 0u || expectedHeight == 0u) { + ++drainStats.skippedFrameCount; + ++queuedFrameIndex; + continue; + } + + HostedPreviewOffscreenSurface& previewSurface = FindOrAddHostedPreviewSurface(queuedFrame.debugName); + if (!EnsureHostedPreviewSurface(previewSurface, expectedWidth, expectedHeight) || + !RenderHostedPreviewOffscreenSurface(previewSurface, renderContext, queuedFrame.drawData)) { + ++drainStats.skippedFrameCount; + ++queuedFrameIndex; + continue; + } + + ++drainStats.renderedFrameCount; + const auto& overlayStats = m_hostedPreviewRenderBackend.GetLastOverlayStats(); + drainStats.renderedDrawListCount += overlayStats.drawListCount; + drainStats.renderedCommandCount += overlayStats.renderedCommandCount; + drainStats.skippedCommandCount += overlayStats.skippedCommandCount; + m_hostedPreviewSurfaceRegistry.UpdateSurface( + queuedFrame.debugName, + previewSurface.imguiTextureId, + previewSurface.width, + previewSurface.height, + ::XCEngine::UI::UIRect( + 0.0f, + 0.0f, + static_cast(previewSurface.width), + static_cast(previewSurface.height))); + ++queuedFrameIndex; + } + + m_hostedPreviewQueue.SetLastDrainStats(drainStats); +} + +void Application::Frame() { + if (!m_renderReady || !m_windowRenderer.BeginFrame()) { + m_xcuiInputSource.ClearFrameTransients(); + return; + } + + m_hostedPreviewQueue.BeginFrame(); + m_hostedPreviewSurfaceRegistry.BeginFrame(); + SyncHostedPreviewSurfaces(); + m_imguiBackend.BeginFrame(); + RenderShellChrome(); + if (m_demoPanel) { + m_demoPanel->RenderIfVisible(); + } + if (m_layoutLabPanel) { + m_layoutLabPanel->RenderIfVisible(); + } + if (m_showImGuiDemoWindow) { + ImGui::ShowDemoWindow(&m_showImGuiDemoWindow); + } + + SyncHostedPreviewSurfaces(); + + ImGui::Render(); + m_windowRenderer.Render( + m_imguiBackend, + kClearColor, + [this]( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface) { + RenderQueuedHostedPreviews(renderContext, surface); + + if (!m_showNativeBackdrop && !m_showNativeXCUIOverlay) { + return; + } + + MainWindowNativeBackdropRenderer::FrameState frameState = {}; + frameState.elapsedSeconds = static_cast( + std::chrono::duration(std::chrono::steady_clock::now() - m_startTime).count()); + frameState.pulseAccent = m_pulseNativeBackdropAccent; + frameState.drawBackdrop = m_showNativeBackdrop; + if (m_showNativeXCUIOverlay) { + const float width = static_cast(surface.GetWidth()); + const float height = static_cast(surface.GetHeight()); + const float horizontalMargin = (std::min)(width * 0.14f, 128.0f); + const float topMargin = (std::min)(height * 0.15f, 132.0f); + const float bottomMargin = (std::min)(height * 0.12f, 96.0f); + + ::XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState overlayInput = {}; + overlayInput.canvasRect = ::XCEngine::UI::UIRect( + horizontalMargin, + topMargin, + (std::max)(0.0f, width - horizontalMargin * 2.0f), + (std::max)(0.0f, height - topMargin - bottomMargin)); + overlayInput.pointerPosition = m_xcuiInputSource.GetPointerPosition(); + overlayInput.pointerInside = + overlayInput.pointerPosition.x >= overlayInput.canvasRect.x && + overlayInput.pointerPosition.y >= overlayInput.canvasRect.y && + overlayInput.pointerPosition.x <= overlayInput.canvasRect.x + overlayInput.canvasRect.width && + overlayInput.pointerPosition.y <= overlayInput.canvasRect.y + overlayInput.canvasRect.height; + const auto& overlayFrame = m_nativeOverlayRuntime.Update(overlayInput); + frameState.overlayDrawData = &overlayFrame.drawData; + } + m_nativeBackdropRenderer.Render(renderContext, surface, frameState); + }); + + m_xcuiInputSource.ClearFrameTransients(); +} + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/Application.h b/new_editor/src/Application.h new file mode 100644 index 00000000..86b603d2 --- /dev/null +++ b/new_editor/src/Application.h @@ -0,0 +1,140 @@ +#pragma once + +#include "panels/XCUIDemoPanel.h" +#include "panels/XCUILayoutLabPanel.h" + +#include "Platform/D3D12WindowRenderer.h" +#include "Rendering/MainWindowNativeBackdropRenderer.h" +#include "UI/ImGuiBackendBridge.h" +#include "XCUIBackend/XCUIHostedPreviewPresenter.h" +#include "XCUIBackend/XCUIInputBridge.h" +#include "XCUIBackend/XCUILayoutLabRuntime.h" +#include "XCUIBackend/XCUIRHIRenderBackend.h" +#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h" + +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace NewEditor { + +class Application { +public: + int Run(HINSTANCE instance, int nCmdShow); + +private: + struct HostedPreviewPanelDiagnostics { + std::string debugName = {}; + std::string debugSource = {}; + bool visible = false; + bool hostedPreviewEnabled = false; + bool nativeRequested = false; + bool nativePresenterBound = false; + bool descriptorAvailable = false; + bool surfaceImageAvailable = false; + bool surfaceAllocated = false; + bool surfaceReady = false; + bool presentedThisFrame = false; + bool queuedToNativePassThisFrame = false; + std::uint32_t surfaceWidth = 0; + std::uint32_t surfaceHeight = 0; + float logicalWidth = 0.0f; + float logicalHeight = 0.0f; + std::size_t queuedFrameIndex = 0; + std::size_t submittedDrawListCount = 0; + std::size_t submittedCommandCount = 0; + std::size_t flushedDrawListCount = 0; + std::size_t flushedCommandCount = 0; + }; + + struct HostedPreviewOffscreenSurface { + std::string debugName = {}; + std::uint32_t width = 0; + std::uint32_t height = 0; + ::XCEngine::RHI::RHITexture* colorTexture = nullptr; + ::XCEngine::RHI::RHIResourceView* colorView = nullptr; + D3D12_CPU_DESCRIPTOR_HANDLE imguiCpuHandle = {}; + D3D12_GPU_DESCRIPTOR_HANDLE imguiGpuHandle = {}; + ImTextureID imguiTextureId = {}; + ::XCEngine::RHI::ResourceStates colorState = ::XCEngine::RHI::ResourceStates::Common; + + bool IsReady() const { + return !debugName.empty() && + colorTexture != nullptr && + colorView != nullptr && + imguiTextureId != ImTextureID{} && + width > 0u && + height > 0u; + } + }; + + static LRESULT CALLBACK StaticWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); + LRESULT WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); + + bool CreateMainWindow(HINSTANCE instance, int nCmdShow); + bool InitializeRenderer(); + void InitializeImGui(); + void ShutdownImGui(); + void ShutdownRenderer(); + void DestroyHostedPreviewSurfaces(); + void SyncHostedPreviewSurfaces(); + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> CreateHostedPreviewPresenter( + bool nativePreview); + void ConfigureHostedPreviewPresenters(); + HostedPreviewPanelDiagnostics BuildHostedPreviewPanelDiagnostics( + const char* debugName, + const char* fallbackDebugSource, + bool visible, + bool hostedPreviewEnabled, + bool nativeRequested, + bool nativePresenterBound, + const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& previewStats) const; + HostedPreviewOffscreenSurface* FindHostedPreviewSurface(const std::string& debugName); + const HostedPreviewOffscreenSurface* FindHostedPreviewSurface(const std::string& debugName) const; + HostedPreviewOffscreenSurface& FindOrAddHostedPreviewSurface(const std::string& debugName); + bool EnsureHostedPreviewSurface( + HostedPreviewOffscreenSurface& previewSurface, + std::uint32_t width, + std::uint32_t height); + bool RenderHostedPreviewOffscreenSurface( + HostedPreviewOffscreenSurface& previewSurface, + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::UI::UIDrawData& drawData); + void RenderShellChrome(); + void RenderHostedPreviewHud(); + void RenderQueuedHostedPreviews( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface); + void Frame(); + + HWND m_hwnd = nullptr; + ::XCEngine::Editor::Platform::D3D12WindowRenderer m_windowRenderer; + ::XCEngine::Editor::UI::ImGuiBackendBridge m_imguiBackend; + std::unique_ptr m_demoPanel; + std::unique_ptr m_layoutLabPanel; + ::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource m_xcuiInputSource; + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewQueue m_hostedPreviewQueue; + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceRegistry m_hostedPreviewSurfaceRegistry; + ::XCEngine::Editor::XCUIBackend::XCUIStandaloneTextAtlasProvider m_hostedPreviewTextAtlasProvider; + ::XCEngine::Editor::XCUIBackend::XCUIRHIRenderBackend m_hostedPreviewRenderBackend; + std::vector m_hostedPreviewSurfaces = {}; + ::XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime m_nativeOverlayRuntime; + MainWindowNativeBackdropRenderer m_nativeBackdropRenderer; + bool m_showImGuiDemoWindow = false; + bool m_showNativeBackdrop = true; + bool m_pulseNativeBackdropAccent = true; + bool m_showNativeXCUIOverlay = true; + bool m_showHostedPreviewHud = true; + bool m_showNativeDemoPanelPreview = true; + bool m_showNativeLayoutLabPreview = false; + bool m_running = false; + bool m_renderReady = false; + std::chrono::steady_clock::time_point m_startTime = {}; +}; + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/Rendering/MainWindowBackdropPass.cpp b/new_editor/src/Rendering/MainWindowBackdropPass.cpp new file mode 100644 index 00000000..5bf0afc0 --- /dev/null +++ b/new_editor/src/Rendering/MainWindowBackdropPass.cpp @@ -0,0 +1,275 @@ +#include "Rendering/MainWindowBackdropPass.h" + +#include +#include +#include +#include + +namespace XCEngine { +namespace NewEditor { + +namespace { + +struct BackdropConstants { + Math::Vector4 viewportAndTime = Math::Vector4::Zero(); + Math::Vector4 primaryAccent = Math::Vector4::Zero(); + Math::Vector4 secondaryAccent = Math::Vector4::Zero(); +}; + +constexpr char kMainWindowBackdropShader[] = R"( +cbuffer BackdropConstants : register(b0) { + float4 gViewportAndTime; + float4 gPrimaryAccent; + float4 gSecondaryAccent; +}; + +struct VSOutput { + float4 position : SV_POSITION; + float2 uv : TEXCOORD0; +}; + +VSOutput MainVS(uint vertexId : SV_VertexID) { + VSOutput output; + float2 uv = float2((vertexId << 1) & 2, vertexId & 2); + output.position = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); + output.uv = uv; + return output; +} + +float Hash21(float2 p) { + p = frac(p * float2(123.34, 456.21)); + p += dot(p, p + 45.32); + return frac(p.x * p.y); +} + +float4 MainPS(VSOutput input) : SV_TARGET0 { + const float2 uv = input.uv; + const float time = gViewportAndTime.z; + const float pulseEnabled = gViewportAndTime.w; + + float3 baseLow = float3(0.055, 0.062, 0.078); + float3 baseHigh = float3(0.105, 0.118, 0.142); + float3 color = lerp(baseLow, baseHigh, saturate(uv.y * 0.9 + 0.08)); + + const float vignetteX = uv.x * (1.0 - uv.x); + const float vignetteY = uv.y * (1.0 - uv.y); + const float vignette = saturate(pow(vignetteX * vignetteY * 5.5, 0.42)); + color *= lerp(0.72, 1.0, vignette); + + const float pulse = pulseEnabled > 0.5 ? (0.5 + 0.5 * sin(time * 1.15)) : 0.55; + const float diagonalBand = saturate( + 1.0 - abs(frac(uv.x * 0.95 - uv.y * 0.82 + time * 0.018) - 0.5) * 7.0); + color += gPrimaryAccent.rgb * diagonalBand * (0.05 + pulse * 0.04); + + const float stripePhase = uv.y * 26.0 + time * 0.6; + const float stripe = pow(saturate(sin(stripePhase) * 0.5 + 0.5), 6.0); + color += gSecondaryAccent.rgb * stripe * 0.018; + + const float headerMask = 1.0 - smoothstep(0.085, 0.16, uv.y); + color = lerp(color, color + gPrimaryAccent.rgb * 0.12, headerMask); + + const float leftRailMask = 1.0 - smoothstep(0.0, 0.18, uv.x); + const float railNoise = Hash21(float2(floor(uv.y * 90.0), floor(time * 8.0))); + color += gSecondaryAccent.rgb * leftRailMask * (0.03 + railNoise * 0.015); + + return float4(saturate(color), 1.0); +} +)"; + +} // namespace + +void MainWindowBackdropPass::Shutdown() { + DestroyResources(); +} + +bool MainWindowBackdropPass::Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const RenderParams& params) { + if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) { + return false; + } + + const auto& colorAttachments = surface.GetColorAttachments(); + if (colorAttachments.empty() || colorAttachments[0] == nullptr) { + return false; + } + + if (!EnsureInitialized(renderContext)) { + return false; + } + + RHI::RHICommandList* commandList = renderContext.commandList; + if (commandList == nullptr) { + return false; + } + + RHI::RHIResourceView* renderTarget = colorAttachments[0]; + + BackdropConstants constants = {}; + constants.viewportAndTime = Math::Vector4( + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + params.elapsedSeconds, + params.pulseAccent ? 1.0f : 0.0f); + constants.primaryAccent = Math::Vector4(0.87f, 0.41f, 0.16f, 1.0f); + constants.secondaryAccent = Math::Vector4(0.24f, 0.72f, 0.84f, 1.0f); + m_constantSet->WriteConstant(0, &constants, sizeof(constants)); + + commandList->SetRenderTargets(1, &renderTarget, nullptr); + + const RHI::Viewport viewport = { + 0.0f, + 0.0f, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + 0.0f, + 1.0f + }; + const RHI::Rect scissorRect = { + 0, + 0, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()) + }; + + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + commandList->SetPipelineState(m_pipelineState); + + RHI::RHIDescriptorSet* descriptorSets[] = { m_constantSet }; + commandList->SetGraphicsDescriptorSets(0, 1, descriptorSets, m_pipelineLayout); + commandList->Draw(3, 1, 0, 0); + return true; +} + +bool MainWindowBackdropPass::EnsureInitialized( + const ::XCEngine::Rendering::RenderContext& renderContext) { + if (m_pipelineState != nullptr && + m_pipelineLayout != nullptr && + m_constantPool != nullptr && + m_constantSet != nullptr && + m_device == renderContext.device && + m_backendType == renderContext.backendType) { + return true; + } + + DestroyResources(); + return CreateResources(renderContext); +} + +bool MainWindowBackdropPass::CreateResources( + const ::XCEngine::Rendering::RenderContext& renderContext) { + if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) { + return false; + } + + m_device = renderContext.device; + m_backendType = renderContext.backendType; + + RHI::DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0; + constantBinding.type = static_cast(RHI::DescriptorType::CBV); + constantBinding.count = 1; + + RHI::DescriptorSetLayoutDesc constantLayout = {}; + constantLayout.bindings = &constantBinding; + constantLayout.bindingCount = 1; + + RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = &constantLayout; + pipelineLayoutDesc.setLayoutCount = 1; + m_pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_pipelineLayout == nullptr) { + DestroyResources(); + return false; + } + + RHI::DescriptorPoolDesc constantPoolDesc = {}; + constantPoolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + constantPoolDesc.descriptorCount = 1; + constantPoolDesc.shaderVisible = false; + m_constantPool = m_device->CreateDescriptorPool(constantPoolDesc); + if (m_constantPool == nullptr) { + DestroyResources(); + return false; + } + + m_constantSet = m_constantPool->AllocateSet(constantLayout); + if (m_constantSet == nullptr) { + DestroyResources(); + return false; + } + + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = m_pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + pipelineDesc.renderTargetCount = 1; + pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + pipelineDesc.depthStencilFormat = static_cast(RHI::Format::Unknown); + pipelineDesc.sampleCount = 1; + + pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + + pipelineDesc.blendState.blendEnable = false; + pipelineDesc.blendState.colorWriteMask = 0xF; + + pipelineDesc.depthStencilState.depthTestEnable = false; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::Always); + + pipelineDesc.vertexShader.source.assign( + kMainWindowBackdropShader, + kMainWindowBackdropShader + sizeof(kMainWindowBackdropShader) - 1); + pipelineDesc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kMainWindowBackdropShader, + kMainWindowBackdropShader + sizeof(kMainWindowBackdropShader) - 1); + pipelineDesc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + + m_pipelineState = m_device->CreatePipelineState(pipelineDesc); + if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { + DestroyResources(); + return false; + } + + return true; +} + +void MainWindowBackdropPass::DestroyResources() { + if (m_pipelineState != nullptr) { + m_pipelineState->Shutdown(); + delete m_pipelineState; + m_pipelineState = nullptr; + } + if (m_constantSet != nullptr) { + m_constantSet->Shutdown(); + delete m_constantSet; + m_constantSet = nullptr; + } + if (m_constantPool != nullptr) { + m_constantPool->Shutdown(); + delete m_constantPool; + m_constantPool = nullptr; + } + if (m_pipelineLayout != nullptr) { + m_pipelineLayout->Shutdown(); + delete m_pipelineLayout; + m_pipelineLayout = nullptr; + } + + m_device = nullptr; + m_backendType = RHI::RHIType::D3D12; +} + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/Rendering/MainWindowBackdropPass.h b/new_editor/src/Rendering/MainWindowBackdropPass.h new file mode 100644 index 00000000..d702fc6e --- /dev/null +++ b/new_editor/src/Rendering/MainWindowBackdropPass.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace NewEditor { + +class MainWindowBackdropPass { +public: + struct RenderParams { + float elapsedSeconds = 0.0f; + bool pulseAccent = true; + }; + + void Shutdown(); + bool Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const RenderParams& params); + +private: + bool EnsureInitialized(const ::XCEngine::Rendering::RenderContext& renderContext); + bool CreateResources(const ::XCEngine::Rendering::RenderContext& renderContext); + void DestroyResources(); + + ::XCEngine::RHI::RHIDevice* m_device = nullptr; + ::XCEngine::RHI::RHIType m_backendType = ::XCEngine::RHI::RHIType::D3D12; + ::XCEngine::RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; + ::XCEngine::RHI::RHIDescriptorPool* m_constantPool = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* m_constantSet = nullptr; + ::XCEngine::RHI::RHIPipelineState* m_pipelineState = nullptr; +}; + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.cpp b/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.cpp new file mode 100644 index 00000000..9567d106 --- /dev/null +++ b/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.cpp @@ -0,0 +1,44 @@ +#include "Rendering/MainWindowNativeBackdropRenderer.h" + +namespace XCEngine { +namespace NewEditor { + +void MainWindowNativeBackdropRenderer::Shutdown() { + m_overlayBackend.Shutdown(); + m_backdropPass.Shutdown(); +} + +bool MainWindowNativeBackdropRenderer::Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const FrameState& frameState) { + m_overlayBackend.ResetStats(); + + if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) { + return false; + } + + if (frameState.drawBackdrop) { + MainWindowBackdropPass::RenderParams backdropParams = {}; + backdropParams.elapsedSeconds = frameState.elapsedSeconds; + backdropParams.pulseAccent = frameState.pulseAccent; + if (!m_backdropPass.Render(renderContext, surface, backdropParams)) { + return false; + } + } + + if (frameState.overlayDrawData == nullptr) { + return true; + } + + const auto& colorAttachments = surface.GetColorAttachments(); + if (colorAttachments.empty() || colorAttachments[0] == nullptr) { + return false; + } + + m_overlayBackend.SetTextAtlasProvider(&m_textAtlasProvider); + return m_overlayBackend.Render(renderContext, surface, *frameState.overlayDrawData); +} + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.h b/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.h new file mode 100644 index 00000000..c639d24b --- /dev/null +++ b/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.h @@ -0,0 +1,39 @@ +#pragma once + +#include "Rendering/MainWindowBackdropPass.h" +#include "XCUIBackend/XCUIRHIRenderBackend.h" +#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h" + +#include +#include +#include + +namespace XCEngine { +namespace NewEditor { + +class MainWindowNativeBackdropRenderer { +public: + using OverlayStats = ::XCEngine::Editor::XCUIBackend::XCUIRHIRenderBackend::OverlayStats; + + struct FrameState { + float elapsedSeconds = 0.0f; + bool pulseAccent = true; + bool drawBackdrop = true; + const ::XCEngine::UI::UIDrawData* overlayDrawData = nullptr; + }; + + void Shutdown(); + bool Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const FrameState& frameState); + const OverlayStats& GetLastOverlayStats() const { return m_overlayBackend.GetLastOverlayStats(); } + +private: + MainWindowBackdropPass m_backdropPass = {}; + ::XCEngine::Editor::XCUIBackend::XCUIStandaloneTextAtlasProvider m_textAtlasProvider = {}; + ::XCEngine::Editor::XCUIBackend::XCUIRHIRenderBackend m_overlayBackend = {}; +}; + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/UI/ImGuiBackendBridge.h b/new_editor/src/UI/ImGuiBackendBridge.h new file mode 100644 index 00000000..2e646f1e --- /dev/null +++ b/new_editor/src/UI/ImGuiBackendBridge.h @@ -0,0 +1,3 @@ +#pragma once + +#include "../../../editor/src/UI/ImGuiBackendBridge.h" diff --git a/new_editor/src/XCUIBackend/IXCUITextAtlasProvider.h b/new_editor/src/XCUIBackend/IXCUITextAtlasProvider.h new file mode 100644 index 00000000..09a3f624 --- /dev/null +++ b/new_editor/src/XCUIBackend/IXCUITextAtlasProvider.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +class IXCUITextAtlasProvider { +public: + enum class PixelFormat : std::uint8_t { + Unknown = 0, + Alpha8, + RGBA32 + }; + + struct AtlasTextureView { + const unsigned char* pixels = nullptr; + int width = 0; + int height = 0; + int stride = 0; + int bytesPerPixel = 0; + PixelFormat format = PixelFormat::Unknown; + // Stable cache key for the logical atlas storage owned by the provider. + std::uintptr_t atlasStorageKey = 0; + // Cache key for the currently exposed pixel buffer. This may change independently + // from atlasStorageKey when the provider re-packs or re-uploads atlas pixels. + std::uintptr_t pixelDataKey = 0; + + bool IsValid() const { + return pixels != nullptr && width > 0 && height > 0 && bytesPerPixel > 0 && stride > 0; + } + }; + + // Opaque font handle. Callers must treat this as provider-scoped data and must not + // assume it remains valid after the provider swaps to a different atlas generation. + struct FontHandle { + std::uintptr_t value = 0; + + bool IsValid() const { + return value != 0; + } + }; + + struct FontInfo { + FontHandle handle = {}; + float nominalSize = 0.0f; + }; + + struct BakedFontInfo { + float lineHeight = 0.0f; + float ascent = 0.0f; + float descent = 0.0f; + float rasterizerDensity = 0.0f; + }; + + struct GlyphInfo { + std::uint32_t requestedCodepoint = 0; + std::uint32_t resolvedCodepoint = 0; + bool visible = false; + bool colored = false; + float advanceX = 0.0f; + float x0 = 0.0f; + float y0 = 0.0f; + float x1 = 0.0f; + float y1 = 0.0f; + float u0 = 0.0f; + float v0 = 0.0f; + float u1 = 0.0f; + float v1 = 0.0f; + }; + + virtual ~IXCUITextAtlasProvider() = default; + + // Returns false until atlas pixels and corresponding font/glyph metadata are ready. + // Callers may request a preferred pixel format, but providers may return a different + // format if only one backing representation is available. + virtual bool GetAtlasTextureView(PixelFormat preferredFormat, AtlasTextureView& outView) const = 0; + virtual std::size_t GetFontCount() const = 0; + virtual FontHandle GetFont(std::size_t index) const = 0; + virtual FontHandle GetDefaultFont() const = 0; + virtual bool GetFontInfo(FontHandle font, FontInfo& outInfo) const = 0; + virtual bool GetBakedFontInfo(FontHandle font, float fontSize, BakedFontInfo& outInfo) const = 0; + virtual bool FindGlyph(FontHandle font, float fontSize, std::uint32_t codepoint, GlyphInfo& outInfo) const = 0; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.cpp b/new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.cpp new file mode 100644 index 00000000..c754d1dc --- /dev/null +++ b/new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.cpp @@ -0,0 +1,223 @@ +#include "XCUIBackend/ImGuiTextAtlasProvider.h" + +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace { + +IXCUITextAtlasProvider::FontHandle MakeFontHandle(const ImFont* font) { + IXCUITextAtlasProvider::FontHandle handle = {}; + handle.value = reinterpret_cast(font); + return handle; +} + +ImFont* ResolveFontHandle(IXCUITextAtlasProvider::FontHandle handle) { + return reinterpret_cast(handle.value); +} + +ImFontAtlas* ResolveAtlas(::ImGuiContext* context) { + if (context == nullptr) { + return nullptr; + } + return context->IO.Fonts; +} + +ImFont* ResolveDefaultFont(::ImGuiContext* context) { + ImFontAtlas* atlas = ResolveAtlas(context); + if (atlas == nullptr) { + return nullptr; + } + + ImFont* font = context->IO.FontDefault; + if (font == nullptr && atlas->Fonts.Size > 0) { + font = atlas->Fonts[0]; + } + return font; +} + +float ResolveNominalFontSize(const ImFont& font) { + return font.LegacySize > 0.0f ? font.LegacySize : 16.0f; +} + +float ResolveRequestedFontSize(const ImFont& font, float requestedFontSize) { + return requestedFontSize > 0.0f ? requestedFontSize : ResolveNominalFontSize(font); +} + +bool IsFontOwnedByAtlas(const ImFont* font, const ImFontAtlas* atlas) { + return font != nullptr && atlas != nullptr && font->OwnerAtlas == atlas; +} + +ImFontBaked* ResolveBakedFont( + IXCUITextAtlasProvider::FontHandle fontHandle, + ImFontAtlas* atlas, + float requestedFontSize) { + ImFont* font = ResolveFontHandle(fontHandle); + if (!IsFontOwnedByAtlas(font, atlas)) { + return nullptr; + } + + const float resolvedFontSize = ResolveRequestedFontSize(*font, requestedFontSize); + if (resolvedFontSize <= 0.0f) { + return nullptr; + } + + return font->GetFontBaked(resolvedFontSize); +} + +} // namespace + +ImGuiTextAtlasProvider::ImGuiTextAtlasProvider(::ImGuiContext* context) + : m_context(context) { +} + +void ImGuiTextAtlasProvider::SetContext(::ImGuiContext* context) { + m_context = context; +} + +::ImGuiContext* ImGuiTextAtlasProvider::GetContext() const { + return m_context; +} + +bool ImGuiTextAtlasProvider::GetAtlasTextureView( + PixelFormat preferredFormat, + AtlasTextureView& outView) const { + outView = {}; + + ImFontAtlas* atlas = ResolveAtlas(ResolveContext()); + if (atlas == nullptr) { + return false; + } + + unsigned char* pixels = nullptr; + int width = 0; + int height = 0; + int bytesPerPixel = 0; + PixelFormat resolvedFormat = preferredFormat; + + switch (preferredFormat) { + case PixelFormat::Alpha8: + atlas->GetTexDataAsAlpha8(&pixels, &width, &height, &bytesPerPixel); + break; + case PixelFormat::RGBA32: + case PixelFormat::Unknown: + default: + atlas->GetTexDataAsRGBA32(&pixels, &width, &height, &bytesPerPixel); + resolvedFormat = PixelFormat::RGBA32; + break; + } + + if (pixels == nullptr || width <= 0 || height <= 0 || bytesPerPixel <= 0) { + return false; + } + + outView.pixels = pixels; + outView.width = width; + outView.height = height; + outView.stride = width * bytesPerPixel; + outView.bytesPerPixel = bytesPerPixel; + outView.format = resolvedFormat; + outView.atlasStorageKey = reinterpret_cast(atlas); + outView.pixelDataKey = reinterpret_cast(pixels); + return true; +} + +std::size_t ImGuiTextAtlasProvider::GetFontCount() const { + const ImFontAtlas* atlas = ResolveAtlas(ResolveContext()); + return atlas != nullptr ? static_cast(atlas->Fonts.Size) : 0u; +} + +IXCUITextAtlasProvider::FontHandle ImGuiTextAtlasProvider::GetFont(std::size_t index) const { + const ImFontAtlas* atlas = ResolveAtlas(ResolveContext()); + if (atlas == nullptr || index >= static_cast(atlas->Fonts.Size)) { + return {}; + } + + return MakeFontHandle(atlas->Fonts[static_cast(index)]); +} + +IXCUITextAtlasProvider::FontHandle ImGuiTextAtlasProvider::GetDefaultFont() const { + return MakeFontHandle(ResolveDefaultFont(ResolveContext())); +} + +bool ImGuiTextAtlasProvider::GetFontInfo(FontHandle fontHandle, FontInfo& outInfo) const { + outInfo = {}; + + ImFontAtlas* atlas = ResolveAtlas(ResolveContext()); + ImFont* font = ResolveFontHandle(fontHandle); + if (!IsFontOwnedByAtlas(font, atlas)) { + return false; + } + + outInfo.handle = fontHandle; + outInfo.nominalSize = ResolveNominalFontSize(*font); + return true; +} + +bool ImGuiTextAtlasProvider::GetBakedFontInfo( + FontHandle fontHandle, + float fontSize, + BakedFontInfo& outInfo) const { + outInfo = {}; + + ImFontAtlas* atlas = ResolveAtlas(ResolveContext()); + ImFontBaked* bakedFont = ResolveBakedFont(fontHandle, atlas, fontSize); + if (bakedFont == nullptr) { + return false; + } + + outInfo.lineHeight = bakedFont->Size; + outInfo.ascent = bakedFont->Ascent; + outInfo.descent = bakedFont->Descent; + outInfo.rasterizerDensity = bakedFont->RasterizerDensity; + return true; +} + +bool ImGuiTextAtlasProvider::FindGlyph( + FontHandle fontHandle, + float fontSize, + std::uint32_t codepoint, + GlyphInfo& outInfo) const { + outInfo = {}; + + if (codepoint > IM_UNICODE_CODEPOINT_MAX) { + return false; + } + + ImFontAtlas* atlas = ResolveAtlas(ResolveContext()); + ImFontBaked* bakedFont = ResolveBakedFont(fontHandle, atlas, fontSize); + if (bakedFont == nullptr) { + return false; + } + + ImFontGlyph* glyph = bakedFont->FindGlyph(static_cast(codepoint)); + if (glyph == nullptr) { + return false; + } + + outInfo.requestedCodepoint = codepoint; + outInfo.resolvedCodepoint = glyph->Codepoint; + outInfo.visible = glyph->Visible != 0; + outInfo.colored = glyph->Colored != 0; + outInfo.advanceX = glyph->AdvanceX; + outInfo.x0 = glyph->X0; + outInfo.y0 = glyph->Y0; + outInfo.x1 = glyph->X1; + outInfo.y1 = glyph->Y1; + outInfo.u0 = glyph->U0; + outInfo.v0 = glyph->V0; + outInfo.u1 = glyph->U1; + outInfo.v1 = glyph->V1; + return true; +} + +::ImGuiContext* ImGuiTextAtlasProvider::ResolveContext() const { + return m_context != nullptr ? m_context : ImGui::GetCurrentContext(); +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.h b/new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.h new file mode 100644 index 00000000..935c9c55 --- /dev/null +++ b/new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.h @@ -0,0 +1,35 @@ +#pragma once + +#include "IXCUITextAtlasProvider.h" + +struct ImGuiContext; + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +class ImGuiTextAtlasProvider final : public IXCUITextAtlasProvider { +public: + ImGuiTextAtlasProvider() = default; + explicit ImGuiTextAtlasProvider(::ImGuiContext* context); + + void SetContext(::ImGuiContext* context); + ::ImGuiContext* GetContext() const; + + bool GetAtlasTextureView(PixelFormat preferredFormat, AtlasTextureView& outView) const override; + std::size_t GetFontCount() const override; + FontHandle GetFont(std::size_t index) const override; + FontHandle GetDefaultFont() const override; + bool GetFontInfo(FontHandle font, FontInfo& outInfo) const override; + bool GetBakedFontInfo(FontHandle font, float fontSize, BakedFontInfo& outInfo) const override; + bool FindGlyph(FontHandle font, float fontSize, std::uint32_t codepoint, GlyphInfo& outInfo) const override; + +private: + ::ImGuiContext* ResolveContext() const; + + ::ImGuiContext* m_context = nullptr; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/ImGuiTransitionBackend.h b/new_editor/src/XCUIBackend/ImGuiTransitionBackend.h new file mode 100644 index 00000000..f67c6dee --- /dev/null +++ b/new_editor/src/XCUIBackend/ImGuiTransitionBackend.h @@ -0,0 +1,193 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +class ImGuiTransitionBackend { +public: + void BeginFrame() { + m_lastFlushedDrawListCount = 0; + m_lastFlushedCommandCount = 0; + m_pendingCommandCount = 0; + m_pendingDrawLists.clear(); + } + + void Submit(const ::XCEngine::UI::UIDrawList& drawList) { + m_pendingCommandCount += drawList.GetCommandCount(); + m_pendingDrawLists.push_back(drawList); + } + + void Submit(::XCEngine::UI::UIDrawList&& drawList) { + m_pendingCommandCount += drawList.GetCommandCount(); + m_pendingDrawLists.push_back(std::move(drawList)); + } + + void Submit(const ::XCEngine::UI::UIDrawData& drawData) { + for (const ::XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { + Submit(drawList); + } + } + + bool HasPendingDrawData() const { + return !m_pendingDrawLists.empty(); + } + + std::size_t GetPendingDrawListCount() const { + return m_pendingDrawLists.size(); + } + + std::size_t GetPendingCommandCount() const { + return m_pendingCommandCount; + } + + std::size_t GetLastFlushedDrawListCount() const { + return m_lastFlushedDrawListCount; + } + + std::size_t GetLastFlushedCommandCount() const { + return m_lastFlushedCommandCount; + } + + bool EndFrame(ImDrawList* targetDrawList = nullptr) { + ImDrawList* drawList = targetDrawList != nullptr ? targetDrawList : ImGui::GetWindowDrawList(); + if (drawList == nullptr) { + ClearPendingState(); + return false; + } + + std::size_t clipDepth = 0; + for (const ::XCEngine::UI::UIDrawList& pendingDrawList : m_pendingDrawLists) { + for (const ::XCEngine::UI::UIDrawCommand& command : pendingDrawList.GetCommands()) { + RenderCommand(*drawList, command, clipDepth); + } + } + + while (clipDepth > 0) { + drawList->PopClipRect(); + --clipDepth; + } + + m_lastFlushedDrawListCount = m_pendingDrawLists.size(); + m_lastFlushedCommandCount = m_pendingCommandCount; + ClearPendingState(); + return true; + } + +private: + static ImVec2 ToImVec2(const ::XCEngine::UI::UIPoint& point) { + return ImVec2(point.x, point.y); + } + + static ImVec2 ToImVec2Min(const ::XCEngine::UI::UIRect& rect) { + return ImVec2(rect.x, rect.y); + } + + static ImVec2 ToImVec2Max(const ::XCEngine::UI::UIRect& rect) { + return ImVec2(rect.x + rect.width, rect.y + rect.height); + } + + static ImTextureID ToImTextureId(const ::XCEngine::UI::UITextureHandle& texture) { + if (texture.kind != ::XCEngine::UI::UITextureHandleKind::ImGuiDescriptor) { + return static_cast(0); + } + return static_cast(texture.nativeHandle); + } + + static ImU32 ToImU32(const ::XCEngine::UI::UIColor& color) { + const float r = (std::max)(0.0f, (std::min)(1.0f, color.r)); + const float g = (std::max)(0.0f, (std::min)(1.0f, color.g)); + const float b = (std::max)(0.0f, (std::min)(1.0f, color.b)); + const float a = (std::max)(0.0f, (std::min)(1.0f, color.a)); + return IM_COL32( + static_cast(r * 255.0f), + static_cast(g * 255.0f), + static_cast(b * 255.0f), + static_cast(a * 255.0f)); + } + + static void RenderCommand( + ImDrawList& drawList, + const ::XCEngine::UI::UIDrawCommand& command, + std::size_t& clipDepth) { + switch (command.type) { + case ::XCEngine::UI::UIDrawCommandType::FilledRect: + drawList.AddRectFilled( + ToImVec2Min(command.rect), + ToImVec2Max(command.rect), + ToImU32(command.color), + command.rounding); + break; + case ::XCEngine::UI::UIDrawCommandType::RectOutline: + drawList.AddRect( + ToImVec2Min(command.rect), + ToImVec2Max(command.rect), + ToImU32(command.color), + command.rounding, + 0, + command.thickness > 0.0f ? command.thickness : 1.0f); + break; + case ::XCEngine::UI::UIDrawCommandType::Text: + if (!command.text.empty()) { + ImFont* font = ImGui::GetFont(); + drawList.AddText( + font, + command.fontSize > 0.0f ? command.fontSize : ImGui::GetFontSize(), + ToImVec2(command.position), + ToImU32(command.color), + command.text.c_str()); + } + break; + case ::XCEngine::UI::UIDrawCommandType::Image: + if (command.texture.IsValid() && + command.texture.kind == ::XCEngine::UI::UITextureHandleKind::ImGuiDescriptor) { + drawList.AddImage( + ToImTextureId(command.texture), + ToImVec2Min(command.rect), + ToImVec2Max(command.rect), + ImVec2(0.0f, 0.0f), + ImVec2(1.0f, 1.0f), + ToImU32(command.color)); + } + break; + case ::XCEngine::UI::UIDrawCommandType::PushClipRect: + drawList.PushClipRect( + ToImVec2Min(command.rect), + ToImVec2Max(command.rect), + command.intersectWithCurrentClip); + ++clipDepth; + break; + case ::XCEngine::UI::UIDrawCommandType::PopClipRect: + if (clipDepth > 0) { + drawList.PopClipRect(); + --clipDepth; + } + break; + default: + break; + } + } + + void ClearPendingState() { + m_pendingCommandCount = 0; + m_pendingDrawLists.clear(); + } + + std::vector<::XCEngine::UI::UIDrawList> m_pendingDrawLists; + std::size_t m_pendingCommandCount = 0; + std::size_t m_lastFlushedDrawListCount = 0; + std::size_t m_lastFlushedCommandCount = 0; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.cpp b/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.cpp new file mode 100644 index 00000000..8398701e --- /dev/null +++ b/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.cpp @@ -0,0 +1,226 @@ +#include "XCUIBackend/ImGuiXCUIInputAdapter.h" + +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace { + +using XCEngine::Input::KeyCode; + +struct ImGuiKeyMappingEntry { + ImGuiKey imguiKey = ImGuiKey_None; + KeyCode keyCode = KeyCode::None; +}; + +constexpr ImGuiKeyMappingEntry kImGuiKeyMappings[] = { + { ImGuiKey_A, KeyCode::A }, + { ImGuiKey_B, KeyCode::B }, + { ImGuiKey_C, KeyCode::C }, + { ImGuiKey_D, KeyCode::D }, + { ImGuiKey_E, KeyCode::E }, + { ImGuiKey_F, KeyCode::F }, + { ImGuiKey_G, KeyCode::G }, + { ImGuiKey_H, KeyCode::H }, + { ImGuiKey_I, KeyCode::I }, + { ImGuiKey_J, KeyCode::J }, + { ImGuiKey_K, KeyCode::K }, + { ImGuiKey_L, KeyCode::L }, + { ImGuiKey_M, KeyCode::M }, + { ImGuiKey_N, KeyCode::N }, + { ImGuiKey_O, KeyCode::O }, + { ImGuiKey_P, KeyCode::P }, + { ImGuiKey_Q, KeyCode::Q }, + { ImGuiKey_R, KeyCode::R }, + { ImGuiKey_S, KeyCode::S }, + { ImGuiKey_T, KeyCode::T }, + { ImGuiKey_U, KeyCode::U }, + { ImGuiKey_V, KeyCode::V }, + { ImGuiKey_W, KeyCode::W }, + { ImGuiKey_X, KeyCode::X }, + { ImGuiKey_Y, KeyCode::Y }, + { ImGuiKey_Z, KeyCode::Z }, + { ImGuiKey_0, KeyCode::Zero }, + { ImGuiKey_1, KeyCode::One }, + { ImGuiKey_2, KeyCode::Two }, + { ImGuiKey_3, KeyCode::Three }, + { ImGuiKey_4, KeyCode::Four }, + { ImGuiKey_5, KeyCode::Five }, + { ImGuiKey_6, KeyCode::Six }, + { ImGuiKey_7, KeyCode::Seven }, + { ImGuiKey_8, KeyCode::Eight }, + { ImGuiKey_9, KeyCode::Nine }, + { ImGuiKey_Space, KeyCode::Space }, + { ImGuiKey_Tab, KeyCode::Tab }, + { ImGuiKey_Enter, KeyCode::Enter }, + { ImGuiKey_KeypadEnter, KeyCode::Enter }, + { ImGuiKey_Escape, KeyCode::Escape }, + { ImGuiKey_LeftShift, KeyCode::LeftShift }, + { ImGuiKey_RightShift, KeyCode::RightShift }, + { ImGuiKey_LeftCtrl, KeyCode::LeftCtrl }, + { ImGuiKey_RightCtrl, KeyCode::RightCtrl }, + { ImGuiKey_LeftAlt, KeyCode::LeftAlt }, + { ImGuiKey_RightAlt, KeyCode::RightAlt }, + { ImGuiKey_UpArrow, KeyCode::Up }, + { ImGuiKey_DownArrow, KeyCode::Down }, + { ImGuiKey_LeftArrow, KeyCode::Left }, + { ImGuiKey_RightArrow, KeyCode::Right }, + { ImGuiKey_Home, KeyCode::Home }, + { ImGuiKey_End, KeyCode::End }, + { ImGuiKey_PageUp, KeyCode::PageUp }, + { ImGuiKey_PageDown, KeyCode::PageDown }, + { ImGuiKey_Delete, KeyCode::Delete }, + { ImGuiKey_Backspace, KeyCode::Backspace }, + { ImGuiKey_F1, KeyCode::F1 }, + { ImGuiKey_F2, KeyCode::F2 }, + { ImGuiKey_F3, KeyCode::F3 }, + { ImGuiKey_F4, KeyCode::F4 }, + { ImGuiKey_F5, KeyCode::F5 }, + { ImGuiKey_F6, KeyCode::F6 }, + { ImGuiKey_F7, KeyCode::F7 }, + { ImGuiKey_F8, KeyCode::F8 }, + { ImGuiKey_F9, KeyCode::F9 }, + { ImGuiKey_F10, KeyCode::F10 }, + { ImGuiKey_F11, KeyCode::F11 }, + { ImGuiKey_F12, KeyCode::F12 }, + { ImGuiKey_Minus, KeyCode::Minus }, + { ImGuiKey_Equal, KeyCode::Equals }, + { ImGuiKey_LeftBracket, KeyCode::BracketLeft }, + { ImGuiKey_RightBracket, KeyCode::BracketRight }, + { ImGuiKey_Semicolon, KeyCode::Semicolon }, + { ImGuiKey_Apostrophe, KeyCode::Quote }, + { ImGuiKey_Comma, KeyCode::Comma }, + { ImGuiKey_Period, KeyCode::Period }, + { ImGuiKey_Slash, KeyCode::Slash }, + { ImGuiKey_Backslash, KeyCode::Backslash }, + { ImGuiKey_GraveAccent, KeyCode::Backtick }, +}; + +bool IsPointerPositionValid(const ImVec2& position) { + return std::isfinite(position.x) && + std::isfinite(position.y) && + position.x > -std::numeric_limits::max() * 0.5f && + position.y > -std::numeric_limits::max() * 0.5f; +} + +bool SnapshotHasKeyRepeat(const ImGuiIO& io, ImGuiKey key) { + if (key < ImGuiKey_NamedKey_BEGIN || key >= ImGuiKey_NamedKey_END) { + return false; + } + + const ImGuiKeyData& data = io.KeysData[key - ImGuiKey_NamedKey_BEGIN]; + return data.Down && + data.DownDurationPrev >= 0.0f && + ImGui::IsKeyPressed(key, true); +} + +void UpsertKeyState( + std::vector& states, + std::int32_t keyCode, + bool down, + bool repeat) { + if (keyCode == 0) { + return; + } + + for (XCUIInputBridgeKeyState& state : states) { + if (state.keyCode != keyCode) { + continue; + } + + state.down = state.down || down; + state.repeat = state.repeat || repeat; + return; + } + + XCUIInputBridgeKeyState state = {}; + state.keyCode = keyCode; + state.down = down; + state.repeat = repeat; + states.push_back(state); +} + +void SortSnapshotKeys(XCUIInputBridgeFrameSnapshot& snapshot) { + std::sort( + snapshot.keys.begin(), + snapshot.keys.end(), + [](const XCUIInputBridgeKeyState& lhs, const XCUIInputBridgeKeyState& rhs) { + return lhs.keyCode < rhs.keyCode; + }); +} + +} // namespace + +XCUIInputBridgeFrameSnapshot ImGuiXCUIInputAdapter::CaptureSnapshot( + const ImGuiIO& io, + const XCUIInputBridgeCaptureOptions& options) { + XCUIInputBridgeFrameSnapshot snapshot = {}; + snapshot.pointerPosition = UI::UIPoint( + io.MousePos.x - options.pointerOffset.x, + io.MousePos.y - options.pointerOffset.y); + snapshot.pointerInside = options.hasPointerInsideOverride + ? options.pointerInsideOverride + : IsPointerPositionValid(io.MousePos); + snapshot.wheelDelta = UI::UIPoint(io.MouseWheelH, io.MouseWheel); + snapshot.modifiers.shift = io.KeyShift; + snapshot.modifiers.control = io.KeyCtrl; + snapshot.modifiers.alt = io.KeyAlt; + snapshot.modifiers.super = io.KeySuper; + snapshot.windowFocused = options.windowFocused; + snapshot.wantCaptureMouse = io.WantCaptureMouse; + snapshot.wantCaptureKeyboard = io.WantCaptureKeyboard; + snapshot.wantTextInput = io.WantTextInput; + snapshot.timestampNanoseconds = options.timestampNanoseconds; + + for (std::size_t index = 0; index < XCUIInputBridgeFrameSnapshot::PointerButtonCount; ++index) { + snapshot.pointerButtonsDown[index] = io.MouseDown[index]; + } + + snapshot.characters.reserve(static_cast(io.InputQueueCharacters.Size)); + for (int index = 0; index < io.InputQueueCharacters.Size; ++index) { + const ImWchar character = io.InputQueueCharacters[index]; + if (character != 0) { + snapshot.characters.push_back(static_cast(character)); + } + } + + snapshot.keys.reserve(sizeof(kImGuiKeyMappings) / sizeof(kImGuiKeyMappings[0])); + for (const ImGuiKeyMappingEntry& mapping : kImGuiKeyMappings) { + if (mapping.keyCode == KeyCode::None) { + continue; + } + + const bool isDown = ImGui::IsKeyDown(mapping.imguiKey); + const bool isRepeat = SnapshotHasKeyRepeat(io, mapping.imguiKey); + if (!isDown && !isRepeat) { + continue; + } + + UpsertKeyState( + snapshot.keys, + static_cast(mapping.keyCode), + isDown, + isRepeat); + } + + SortSnapshotKeys(snapshot); + return snapshot; +} + +std::int32_t ImGuiXCUIInputAdapter::MapKeyCode(ImGuiKey key) { + for (const ImGuiKeyMappingEntry& mapping : kImGuiKeyMappings) { + if (mapping.imguiKey == key) { + return static_cast(mapping.keyCode); + } + } + + return static_cast(KeyCode::None); +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.h b/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.h new file mode 100644 index 00000000..baed09d7 --- /dev/null +++ b/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.h @@ -0,0 +1,22 @@ +#pragma once + +#include "XCUIBackend/XCUIInputBridge.h" + +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +class ImGuiXCUIInputAdapter { +public: + static XCUIInputBridgeFrameSnapshot CaptureSnapshot( + const ImGuiIO& io, + const XCUIInputBridgeCaptureOptions& options = XCUIInputBridgeCaptureOptions()); + + static std::int32_t MapKeyCode(ImGuiKey key); +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.cpp b/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.cpp new file mode 100644 index 00000000..098289fb --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.cpp @@ -0,0 +1,750 @@ +#include "XCUIAssetDocumentSource.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace fs = std::filesystem; + +namespace { + +using XCEngine::Containers::String; +using XCEngine::Resources::CompileUIDocument; +using XCEngine::Resources::ResourceManager; +using XCEngine::Resources::UIDocumentCompileRequest; +using XCEngine::Resources::UIDocumentCompileResult; +using XCEngine::Resources::UIDocumentKind; +using XCEngine::Resources::UIDocumentResource; +using XCEngine::Resources::UITheme; +using XCEngine::Resources::UIView; + +String ToContainersString(const std::string& value) { + return String(value.c_str()); +} + +std::string ToStdString(const String& value) { + return std::string(value.CStr()); +} + +std::string ToGenericString(const fs::path& path) { + return path.lexically_normal().generic_string(); +} + +bool PathExists(const fs::path& path) { + std::error_code ec; + return !path.empty() && fs::exists(path, ec) && !fs::is_directory(path, ec); +} + +bool TryGetWriteTime( + const fs::path& path, + fs::file_time_type& outWriteTime) { + std::error_code ec; + if (path.empty() || !fs::exists(path, ec) || fs::is_directory(path, ec)) { + return false; + } + + outWriteTime = fs::last_write_time(path, ec); + return !ec; +} + +std::string SanitizeSetName(const std::string& setName) { + std::string sanitized = {}; + sanitized.reserve(setName.size()); + for (unsigned char ch : setName) { + if (std::isalnum(ch) != 0 || ch == '_' || ch == '-') { + sanitized.push_back(static_cast(ch)); + } + } + + return sanitized.empty() ? std::string("Default") : sanitized; +} + +std::string ToSnakeCase(const std::string& value) { + std::string snake = {}; + snake.reserve(value.size() + 8u); + + char previous = '\0'; + for (unsigned char rawCh : value) { + if (!(std::isalnum(rawCh) != 0)) { + if (!snake.empty() && snake.back() != '_') { + snake.push_back('_'); + } + previous = '_'; + continue; + } + + const char ch = static_cast(rawCh); + const bool isUpper = std::isupper(rawCh) != 0; + const bool previousIsLowerOrDigit = + std::islower(static_cast(previous)) != 0 || + std::isdigit(static_cast(previous)) != 0; + if (isUpper && !snake.empty() && previousIsLowerOrDigit && snake.back() != '_') { + snake.push_back('_'); + } + + snake.push_back(static_cast(std::tolower(rawCh))); + previous = ch; + } + + while (!snake.empty() && snake.back() == '_') { + snake.pop_back(); + } + + return snake.empty() ? std::string("default") : snake; +} + +std::optional GetCurrentPath() { + std::error_code ec; + const fs::path currentPath = fs::current_path(ec); + if (ec) { + return std::nullopt; + } + return currentPath; +} + +fs::path GetConfiguredResourceRoot() { + const String resourceRoot = ResourceManager::Get().GetResourceRoot(); + if (resourceRoot.Empty()) { + return fs::path(); + } + + return fs::path(resourceRoot.CStr()).lexically_normal(); +} + +std::optional FindKnownDocumentUnderRoot( + const fs::path& root, + const XCUIAssetDocumentSource::PathSet& paths) { + if (PathExists(root / paths.view.primaryRelativePath)) { + return paths.view.primaryRelativePath; + } + if (PathExists(root / paths.theme.primaryRelativePath)) { + return paths.theme.primaryRelativePath; + } + if (PathExists(root / paths.view.legacyRelativePath)) { + return paths.view.legacyRelativePath; + } + if (PathExists(root / paths.theme.legacyRelativePath)) { + return paths.theme.legacyRelativePath; + } + return std::nullopt; +} + +void AppendUniqueSearchRoot( + const fs::path& searchRoot, + std::vector& outRoots, + std::unordered_set& seenRoots) { + if (searchRoot.empty()) { + return; + } + + const fs::path normalized = searchRoot.lexically_normal(); + const std::string key = ToGenericString(normalized); + if (!seenRoots.insert(key).second) { + return; + } + + outRoots.push_back(normalized); +} + +std::vector BuildRepositoryRootSearchRoots( + const std::vector& explicitSearchRoots, + bool includeDefaultSearchRoots) { + std::vector searchRoots = {}; + std::unordered_set seenRoots = {}; + + for (const fs::path& explicitSearchRoot : explicitSearchRoots) { + AppendUniqueSearchRoot(explicitSearchRoot, searchRoots, seenRoots); + } + + if (!includeDefaultSearchRoots) { + return searchRoots; + } + +#ifdef XCENGINE_NEW_EDITOR_REPO_ROOT + AppendUniqueSearchRoot( + fs::path(XCENGINE_NEW_EDITOR_REPO_ROOT), + searchRoots, + seenRoots); +#endif + + const fs::path resourceRoot = GetConfiguredResourceRoot(); + if (!resourceRoot.empty()) { + AppendUniqueSearchRoot(resourceRoot, searchRoots, seenRoots); + } + + const std::optional currentPath = GetCurrentPath(); + if (currentPath.has_value()) { + AppendUniqueSearchRoot(*currentPath, searchRoots, seenRoots); + } + + return searchRoots; +} + +void AddCandidate( + std::vector& candidates, + std::unordered_set& seenPaths, + const std::string& requestPath, + const fs::path& resolvedPath, + XCUIAssetDocumentSource::PathOrigin origin) { + const std::string key = ToGenericString(resolvedPath); + if (requestPath.empty() || resolvedPath.empty() || !seenPaths.insert(key).second) { + return; + } + + XCUIAssetDocumentSource::ResolutionCandidate candidate = {}; + candidate.requestPath = requestPath; + candidate.resolvedPath = resolvedPath.lexically_normal(); + candidate.origin = origin; + candidates.push_back(std::move(candidate)); +} + +void AddRelativeCandidateIfReachable( + const std::string& relativePath, + const fs::path& repositoryRoot, + const fs::path& resourceRoot, + XCUIAssetDocumentSource::PathOrigin origin, + std::vector& candidates, + std::unordered_set& seenPaths) { + if (relativePath.empty()) { + return; + } + + const fs::path relative(relativePath); + if (PathExists(relative)) { + AddCandidate(candidates, seenPaths, relativePath, relative, origin); + return; + } + + if (!resourceRoot.empty() && PathExists(resourceRoot / relative)) { + AddCandidate(candidates, seenPaths, relativePath, resourceRoot / relative, origin); + return; + } + + if (!repositoryRoot.empty() && PathExists(repositoryRoot / relative)) { + AddCandidate( + candidates, + seenPaths, + (repositoryRoot / relative).generic_string(), + repositoryRoot / relative, + origin); + } +} + +template +bool TryLoadDocumentFromResourceManager( + const std::string& requestPath, + UIDocumentCompileResult& outResult) { + static_assert( + std::is_base_of_v, + "TDocumentResource must derive from UIDocumentResource"); + + auto resource = ResourceManager::Get().Load(ToContainersString(requestPath)); + if (!resource || resource->GetDocument().valid == false) { + return false; + } + + outResult = UIDocumentCompileResult(); + outResult.document = resource->GetDocument(); + outResult.succeeded = outResult.document.valid; + return outResult.succeeded; +} + +bool TryLoadDocumentFromResourceManager( + UIDocumentKind kind, + const std::string& requestPath, + UIDocumentCompileResult& outResult) { + switch (kind) { + case UIDocumentKind::View: + return TryLoadDocumentFromResourceManager(requestPath, outResult); + case UIDocumentKind::Theme: + return TryLoadDocumentFromResourceManager(requestPath, outResult); + default: + return false; + } +} + +bool TryCompileDocumentDirect( + const XCUIAssetDocumentSource::DocumentPathSpec& spec, + const std::string& requestPath, + UIDocumentCompileResult& outResult) { + UIDocumentCompileRequest request = {}; + request.kind = spec.kind; + request.path = ToContainersString(requestPath); + request.expectedRootTag = ToContainersString(spec.expectedRootTag); + return CompileUIDocument(request, outResult) && outResult.succeeded; +} + +void CollectTrackedSourcePaths( + const XCUIAssetDocumentSource::DocumentLoadState& documentState, + std::vector& outPaths, + std::unordered_set& seenPaths) { + if (!documentState.succeeded) { + return; + } + + auto pushPath = [&](const String& path) { + if (path.Empty()) { + return; + } + + const std::string text = ToStdString(path); + if (seenPaths.insert(text).second) { + outPaths.push_back(text); + } + }; + + if (!documentState.sourcePath.empty() && + seenPaths.insert(documentState.sourcePath).second) { + outPaths.push_back(documentState.sourcePath); + } + + if (!documentState.compileResult.document.valid) { + return; + } + + pushPath(documentState.compileResult.document.sourcePath); + for (const String& dependency : documentState.compileResult.document.dependencies) { + pushPath(dependency); + } +} + +void UpdateTrackedWriteTimes( + const XCUIAssetDocumentSource::LoadState& state, + std::vector& outTrackedWriteTimes, + std::vector& outMissingTrackedSourcePaths, + bool& outChangeTrackingReady, + std::string& outTrackingStatusMessage) { + outTrackedWriteTimes.clear(); + outMissingTrackedSourcePaths.clear(); + outTrackedWriteTimes.reserve(state.trackedSourcePaths.size()); + outMissingTrackedSourcePaths.reserve(state.trackedSourcePaths.size()); + + for (const std::string& pathText : state.trackedSourcePaths) { + XCUIAssetDocumentSource::TrackedWriteTime tracked = {}; + tracked.path = fs::path(pathText).lexically_normal(); + if (!TryGetWriteTime(tracked.path, tracked.writeTime)) { + outMissingTrackedSourcePaths.push_back(ToGenericString(tracked.path)); + continue; + } + + outTrackedWriteTimes.push_back(std::move(tracked)); + } + + outChangeTrackingReady = + !state.trackedSourcePaths.empty() && + outTrackedWriteTimes.size() == state.trackedSourcePaths.size(); + + if (state.trackedSourcePaths.empty()) { + outTrackingStatusMessage = "No XCUI source files were recorded for hot reload."; + return; + } + + if (outMissingTrackedSourcePaths.empty()) { + outTrackingStatusMessage = + "Tracking " + + std::to_string(static_cast(outTrackedWriteTimes.size())) + + " XCUI source file(s) for hot reload."; + return; + } + + outTrackingStatusMessage = + "Tracking " + + std::to_string(static_cast(outTrackedWriteTimes.size())) + + " of " + + std::to_string(static_cast(state.trackedSourcePaths.size())) + + " XCUI source file(s); unable to stat " + + std::to_string(static_cast(outMissingTrackedSourcePaths.size())) + + " path(s)."; +} + +XCUIAssetDocumentSource::DocumentLoadState LoadDocument( + const XCUIAssetDocumentSource::DocumentPathSpec& spec, + const fs::path& repositoryRoot, + bool preferCompilerFallback) { + XCUIAssetDocumentSource::DocumentLoadState state = {}; + state.kind = spec.kind; + state.expectedRootTag = spec.expectedRootTag; + state.primaryRelativePath = spec.primaryRelativePath; + state.legacyRelativePath = spec.legacyRelativePath; + + state.candidatePaths = XCUIAssetDocumentSource::CollectCandidatePaths( + spec, + repositoryRoot, + GetConfiguredResourceRoot()); + if (state.candidatePaths.empty()) { + state.errorMessage = + "Unable to locate XCUI document source. Expected " + + spec.primaryRelativePath + + " or legacy mirror " + + spec.legacyRelativePath + "."; + return state; + } + + state.attemptMessages.reserve(state.candidatePaths.size()); + + for (const XCUIAssetDocumentSource::ResolutionCandidate& candidate : state.candidatePaths) { + state.requestedPath = candidate.requestPath; + state.resolvedPath = candidate.resolvedPath; + state.pathOrigin = candidate.origin; + state.usedLegacyFallback = + candidate.origin == XCUIAssetDocumentSource::PathOrigin::LegacyMirror; + + UIDocumentCompileResult compileResult = {}; + if (TryCompileDocumentDirect(spec, candidate.requestPath, compileResult)) { + state.backend = XCUIAssetDocumentSource::LoadBackend::CompilerFallback; + state.compileResult = std::move(compileResult); + state.sourcePath = ToStdString(state.compileResult.document.sourcePath); + if (state.sourcePath.empty()) { + state.sourcePath = ToGenericString(candidate.resolvedPath); + } + state.statusMessage = + (preferCompilerFallback + ? std::string("Tracked source changed; direct compile refresh succeeded from ") + : std::string("Direct compile load succeeded from ")) + + ToGenericString(candidate.resolvedPath) + + "."; + state.succeeded = true; + return state; + } + + UIDocumentCompileResult resourceResult = {}; + if (TryLoadDocumentFromResourceManager(spec.kind, candidate.requestPath, resourceResult)) { + state.backend = XCUIAssetDocumentSource::LoadBackend::ResourceManager; + state.compileResult = std::move(resourceResult); + state.sourcePath = ToStdString(state.compileResult.document.sourcePath); + if (state.sourcePath.empty()) { + state.sourcePath = ToGenericString(candidate.resolvedPath); + } + state.statusMessage = + std::string("Loaded via ResourceManager from ") + + ToString(candidate.origin) + + " path: " + + ToGenericString(candidate.resolvedPath) + + "."; + state.succeeded = true; + return state; + } + + const std::string compileError = compileResult.errorMessage.Empty() + ? std::string("ResourceManager load failed and compile fallback returned no diagnostic.") + : ToStdString(compileResult.errorMessage); + state.attemptMessages.push_back( + std::string(ToString(candidate.origin)) + + " candidate " + + ToGenericString(candidate.resolvedPath) + + " -> " + + compileError); + } + + state.requestedPath.clear(); + state.resolvedPath.clear(); + state.sourcePath.clear(); + state.backend = XCUIAssetDocumentSource::LoadBackend::None; + state.pathOrigin = XCUIAssetDocumentSource::PathOrigin::None; + state.usedLegacyFallback = false; + + state.errorMessage = state.attemptMessages.empty() + ? std::string("Failed to load XCUI document.") + : state.attemptMessages.front(); + if (state.attemptMessages.size() > 1u) { + state.errorMessage += " ("; + state.errorMessage += std::to_string( + static_cast(state.attemptMessages.size())); + state.errorMessage += " candidates tried)"; + } + return state; +} + +} // namespace + +const char* ToString(XCUIAssetDocumentSource::PathOrigin origin) { + switch (origin) { + case XCUIAssetDocumentSource::PathOrigin::ProjectAssets: + return "project-assets"; + case XCUIAssetDocumentSource::PathOrigin::LegacyMirror: + return "legacy-mirror"; + default: + return "none"; + } +} + +const char* ToString(XCUIAssetDocumentSource::LoadBackend backend) { + switch (backend) { + case XCUIAssetDocumentSource::LoadBackend::ResourceManager: + return "resource-manager"; + case XCUIAssetDocumentSource::LoadBackend::CompilerFallback: + return "compiler-fallback"; + default: + return "none"; + } +} + +XCUIAssetDocumentSource::XCUIAssetDocumentSource() = default; + +XCUIAssetDocumentSource::XCUIAssetDocumentSource(PathSet paths) + : m_paths(std::move(paths)) { +} + +void XCUIAssetDocumentSource::SetPathSet(PathSet paths) { + m_paths = std::move(paths); +} + +const XCUIAssetDocumentSource::PathSet& XCUIAssetDocumentSource::GetPathSet() const { + return m_paths; +} + +bool XCUIAssetDocumentSource::Reload() { + const bool preferCompilerFallback = m_state.succeeded && HasTrackedChanges(); + m_state = LoadState(); + m_state.paths = m_paths; + + m_state.repositoryDiscovery = DiagnoseRepositoryRoot(m_paths); + m_state.repositoryRoot = m_state.repositoryDiscovery.repositoryRoot; + + m_state.view = LoadDocument(m_paths.view, m_state.repositoryRoot, preferCompilerFallback); + if (!m_state.view.succeeded) { + m_state.errorMessage = m_state.view.errorMessage; + if (!m_state.repositoryDiscovery.statusMessage.empty()) { + m_state.errorMessage += " " + m_state.repositoryDiscovery.statusMessage; + } + m_state.statusMessage = + "XCUI view document load failed. " + + m_state.repositoryDiscovery.statusMessage; + m_trackedWriteTimes.clear(); + return false; + } + + m_state.theme = LoadDocument(m_paths.theme, m_state.repositoryRoot, preferCompilerFallback); + if (!m_state.theme.succeeded) { + m_state.errorMessage = m_state.theme.errorMessage; + if (!m_state.repositoryDiscovery.statusMessage.empty()) { + m_state.errorMessage += " " + m_state.repositoryDiscovery.statusMessage; + } + m_state.statusMessage = + "XCUI theme document load failed. " + + m_state.repositoryDiscovery.statusMessage; + m_trackedWriteTimes.clear(); + return false; + } + + std::unordered_set seenPaths = {}; + CollectTrackedSourcePaths(m_state.view, m_state.trackedSourcePaths, seenPaths); + CollectTrackedSourcePaths(m_state.theme, m_state.trackedSourcePaths, seenPaths); + UpdateTrackedWriteTimes( + m_state, + m_trackedWriteTimes, + m_state.missingTrackedSourcePaths, + m_state.changeTrackingReady, + m_state.trackingStatusMessage); + + m_state.usedLegacyFallback = + m_state.view.usedLegacyFallback || m_state.theme.usedLegacyFallback; + m_state.succeeded = true; + m_state.statusMessage = + (m_state.usedLegacyFallback + ? std::string("XCUI documents loaded with legacy mirror fallback. ") + : std::string("XCUI documents loaded from Assets/XCUI/NewEditor. ")) + + m_state.trackingStatusMessage; + return true; +} + +bool XCUIAssetDocumentSource::ReloadIfChanged() { + if (!m_state.succeeded) { + return Reload(); + } + + return HasTrackedChanges() ? Reload() : true; +} + +bool XCUIAssetDocumentSource::HasTrackedChanges() const { + if (!m_state.succeeded) { + return true; + } + + if (m_state.trackedSourcePaths.empty()) { + return false; + } + + if (m_trackedWriteTimes.size() != m_state.trackedSourcePaths.size()) { + return true; + } + + for (const TrackedWriteTime& tracked : m_trackedWriteTimes) { + fs::file_time_type currentWriteTime = {}; + if (!TryGetWriteTime(tracked.path, currentWriteTime) || + currentWriteTime != tracked.writeTime) { + return true; + } + } + + return false; +} + +bool XCUIAssetDocumentSource::IsLoaded() const { + return m_state.succeeded; +} + +const XCUIAssetDocumentSource::LoadState& XCUIAssetDocumentSource::GetState() const { + return m_state; +} + +XCUIAssetDocumentSource::PathSet XCUIAssetDocumentSource::MakePathSet( + const std::string& setName) { + PathSet paths = {}; + paths.setName = SanitizeSetName(setName); + + paths.view.kind = UIDocumentKind::View; + paths.view.expectedRootTag = "View"; + paths.view.primaryRelativePath = BuildProjectAssetViewPath(paths.setName); + paths.view.legacyRelativePath = BuildLegacyViewPath(paths.setName); + + paths.theme.kind = UIDocumentKind::Theme; + paths.theme.expectedRootTag = "Theme"; + paths.theme.primaryRelativePath = BuildProjectAssetThemePath(paths.setName); + paths.theme.legacyRelativePath = BuildLegacyThemePath(paths.setName); + return paths; +} + +XCUIAssetDocumentSource::PathSet XCUIAssetDocumentSource::MakeDemoPathSet() { + return MakePathSet("Demo"); +} + +XCUIAssetDocumentSource::PathSet XCUIAssetDocumentSource::MakeLayoutLabPathSet() { + return MakePathSet("LayoutLab"); +} + +std::vector +XCUIAssetDocumentSource::CollectCandidatePaths( + const DocumentPathSpec& spec, + const fs::path& repositoryRoot) { + return CollectCandidatePaths(spec, repositoryRoot, GetConfiguredResourceRoot()); +} + +std::vector +XCUIAssetDocumentSource::CollectCandidatePaths( + const DocumentPathSpec& spec, + const fs::path& repositoryRoot, + const fs::path& resourceRoot) { + std::vector candidates = {}; + std::unordered_set seenPaths = {}; + + AddRelativeCandidateIfReachable( + spec.primaryRelativePath, + repositoryRoot, + resourceRoot, + PathOrigin::ProjectAssets, + candidates, + seenPaths); + AddRelativeCandidateIfReachable( + spec.legacyRelativePath, + repositoryRoot, + resourceRoot, + PathOrigin::LegacyMirror, + candidates, + seenPaths); + return candidates; +} + +XCUIAssetDocumentSource::RepositoryDiscovery +XCUIAssetDocumentSource::DiagnoseRepositoryRoot(const PathSet& paths) { + return DiagnoseRepositoryRoot(paths, {}, true); +} + +XCUIAssetDocumentSource::RepositoryDiscovery +XCUIAssetDocumentSource::DiagnoseRepositoryRoot( + const PathSet& paths, + const std::vector& searchRoots, + bool includeDefaultSearchRoots) { + RepositoryDiscovery discovery = {}; + discovery.probes.reserve(searchRoots.size() + 3u); + + const std::vector effectiveSearchRoots = BuildRepositoryRootSearchRoots( + searchRoots, + includeDefaultSearchRoots); + discovery.probes.reserve(effectiveSearchRoots.size()); + + for (const fs::path& searchRoot : effectiveSearchRoots) { + RepositoryProbe probe = {}; + probe.searchRoot = searchRoot; + + fs::path current = searchRoot; + while (!current.empty()) { + const std::optional matchedRelativePath = + FindKnownDocumentUnderRoot(current, paths); + if (matchedRelativePath.has_value()) { + probe.matched = true; + probe.matchedRoot = current.lexically_normal(); + probe.matchedRelativePath = *matchedRelativePath; + discovery.repositoryRoot = probe.matchedRoot; + discovery.probes.push_back(std::move(probe)); + discovery.statusMessage = + "Repository root resolved to " + + ToGenericString(discovery.repositoryRoot) + + " via " + + discovery.probes.back().matchedRelativePath + + "."; + return discovery; + } + + const fs::path parent = current.parent_path(); + if (parent == current) { + break; + } + current = parent; + } + + discovery.probes.push_back(std::move(probe)); + } + + discovery.statusMessage = + "Repository root not found for XCUI set '" + + paths.setName + + "'. Probed " + + std::to_string(static_cast(discovery.probes.size())) + + " search root(s)."; + return discovery; +} + +std::string XCUIAssetDocumentSource::BuildProjectAssetViewPath( + const std::string& setName) { + const std::string folderName = SanitizeSetName(setName); + return std::string(kProjectAssetRoot) + "/" + folderName + "/View.xcui"; +} + +std::string XCUIAssetDocumentSource::BuildProjectAssetThemePath( + const std::string& setName) { + const std::string folderName = SanitizeSetName(setName); + return std::string(kProjectAssetRoot) + "/" + folderName + "/Theme.xctheme"; +} + +std::string XCUIAssetDocumentSource::BuildLegacyViewPath( + const std::string& setName) { + return std::string(kLegacyResourceRoot) + + "/xcui_" + + ToSnakeCase(SanitizeSetName(setName)) + + "_view.xcui"; +} + +std::string XCUIAssetDocumentSource::BuildLegacyThemePath( + const std::string& setName) { + return std::string(kLegacyResourceRoot) + + "/xcui_" + + ToSnakeCase(SanitizeSetName(setName)) + + "_theme.xctheme"; +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.h b/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.h new file mode 100644 index 00000000..6a9a7a57 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.h @@ -0,0 +1,148 @@ +#pragma once + +#include + +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +class XCUIAssetDocumentSource { +public: + static constexpr const char* kProjectAssetRoot = "Assets/XCUI/NewEditor"; + static constexpr const char* kLegacyResourceRoot = "new_editor/resources"; + + enum class PathOrigin { + None, + ProjectAssets, + LegacyMirror + }; + + enum class LoadBackend { + None, + ResourceManager, + CompilerFallback + }; + + struct ResolutionCandidate { + std::string requestPath = {}; + std::filesystem::path resolvedPath = {}; + PathOrigin origin = PathOrigin::None; + }; + + struct RepositoryProbe { + std::filesystem::path searchRoot = {}; + std::filesystem::path matchedRoot = {}; + std::string matchedRelativePath = {}; + bool matched = false; + }; + + struct RepositoryDiscovery { + std::filesystem::path repositoryRoot = {}; + std::vector probes = {}; + std::string statusMessage = {}; + }; + + struct DocumentPathSpec { + Resources::UIDocumentKind kind = Resources::UIDocumentKind::View; + std::string expectedRootTag = {}; + std::string primaryRelativePath = {}; + std::string legacyRelativePath = {}; + }; + + struct PathSet { + std::string setName = {}; + DocumentPathSpec view = {}; + DocumentPathSpec theme = {}; + }; + + struct DocumentLoadState { + Resources::UIDocumentKind kind = Resources::UIDocumentKind::View; + std::string expectedRootTag = {}; + std::string primaryRelativePath = {}; + std::string legacyRelativePath = {}; + std::string requestedPath = {}; + std::filesystem::path resolvedPath = {}; + std::string sourcePath = {}; + std::string statusMessage = {}; + std::string errorMessage = {}; + std::vector candidatePaths = {}; + std::vector attemptMessages = {}; + LoadBackend backend = LoadBackend::None; + PathOrigin pathOrigin = PathOrigin::None; + Resources::UIDocumentCompileResult compileResult = {}; + bool succeeded = false; + bool usedLegacyFallback = false; + }; + + struct LoadState { + PathSet paths = {}; + DocumentLoadState view = {}; + DocumentLoadState theme = {}; + std::vector trackedSourcePaths = {}; + std::vector missingTrackedSourcePaths = {}; + std::string statusMessage = {}; + std::string errorMessage = {}; + std::string trackingStatusMessage = {}; + std::filesystem::path repositoryRoot = {}; + RepositoryDiscovery repositoryDiscovery = {}; + bool succeeded = false; + bool changeTrackingReady = false; + bool usedLegacyFallback = false; + }; + + struct TrackedWriteTime { + std::filesystem::path path = {}; + std::filesystem::file_time_type writeTime = {}; + }; + + XCUIAssetDocumentSource(); + explicit XCUIAssetDocumentSource(PathSet paths); + + void SetPathSet(PathSet paths); + const PathSet& GetPathSet() const; + + bool Reload(); + bool ReloadIfChanged(); + bool HasTrackedChanges() const; + bool IsLoaded() const; + + const LoadState& GetState() const; + + static PathSet MakePathSet(const std::string& setName); + static PathSet MakeDemoPathSet(); + static PathSet MakeLayoutLabPathSet(); + + static std::vector CollectCandidatePaths( + const DocumentPathSpec& spec, + const std::filesystem::path& repositoryRoot); + static std::vector CollectCandidatePaths( + const DocumentPathSpec& spec, + const std::filesystem::path& repositoryRoot, + const std::filesystem::path& resourceRoot); + static RepositoryDiscovery DiagnoseRepositoryRoot(const PathSet& paths); + static RepositoryDiscovery DiagnoseRepositoryRoot( + const PathSet& paths, + const std::vector& searchRoots, + bool includeDefaultSearchRoots); + + static std::string BuildProjectAssetViewPath(const std::string& setName); + static std::string BuildProjectAssetThemePath(const std::string& setName); + static std::string BuildLegacyViewPath(const std::string& setName); + static std::string BuildLegacyThemePath(const std::string& setName); + +private: + PathSet m_paths = {}; + LoadState m_state = {}; + std::vector m_trackedWriteTimes = {}; +}; + +const char* ToString(XCUIAssetDocumentSource::PathOrigin origin); +const char* ToString(XCUIAssetDocumentSource::LoadBackend backend); + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp b/new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp new file mode 100644 index 00000000..6b451728 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp @@ -0,0 +1,1986 @@ +#include "XCUIDemoRuntime.h" +#include "XCUIAssetDocumentSource.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace fs = std::filesystem; + +namespace { + +using XCEngine::Containers::String; +using XCEngine::Input::KeyCode; +using XCEngine::Math::Color; +using XCEngine::Resources::CompileUIDocument; +using XCEngine::Resources::UIDocumentAttribute; +using XCEngine::Resources::UIDocumentCompileRequest; +using XCEngine::Resources::UIDocumentCompileResult; +using XCEngine::Resources::UIDocumentKind; +using XCEngine::Resources::UIDocumentModel; +using XCEngine::Resources::UIDocumentNode; +using XCEngine::Resources::ResourceManager; +using XCEngine::UI::UIColor; +using XCEngine::UI::UIContext; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIElementId; +using XCEngine::UI::UIInputDispatcher; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIInputPath; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; +using XCEngine::UI::UISize; +namespace Layout = XCEngine::UI::Layout; +namespace Style = XCEngine::UI::Style; + +constexpr std::size_t kInvalidIndex = static_cast(-1); +constexpr char kViewRelativePath[] = "new_editor/resources/xcui_demo_view.xcui"; +constexpr char kThemeRelativePath[] = "new_editor/resources/xcui_demo_theme.xctheme"; +constexpr char kToggleAccentCommandId[] = "demo.toggleAccent"; +constexpr float kApproximateTextWidthFactor = 0.58f; + +struct DemoNode { + UIElementId elementId = 0; + std::string elementKey = {}; + std::string tagName = {}; + std::string styleName = {}; + std::string actionId = {}; + std::string staticText = {}; + std::string resolvedText = {}; + std::string gapAttr = {}; + std::string paddingAttr = {}; + std::string widthAttr = {}; + std::unordered_map attributes = {}; + std::size_t parentIndex = kInvalidIndex; + std::vector children = {}; + UIInputPath path = {}; + UIRect rect = {}; + UISize desiredSize = {}; + bool interactive = false; +}; + +struct RuntimeBuildContext { + RuntimeBuildContext() + : inputDispatcher(XCEngine::UI::UIInputDispatcherOptions()) { + } + + UIDocumentCompileResult viewDocument = {}; + UIDocumentCompileResult themeDocument = {}; + Style::UITheme baseTheme = {}; + Style::UITheme activeTheme = {}; + Style::UIStyleSheet styleSheet = {}; + UIContext uiContext = {}; + UIInputDispatcher inputDispatcher; + XCUIDemoFrameResult frameResult = {}; + std::vector nodes = {}; + std::unordered_map nodeIndexByKey = {}; + std::unordered_map nodeIndexById = {}; + std::unordered_map toggleStates = {}; + std::unordered_map textFieldValues = {}; + std::unordered_map textFieldCarets = {}; + UIElementId toggleButtonId = 0; + UIElementId registeredShortcutOwnerId = 0; + UIElementId armedElementId = 0; + bool documentsReady = false; + bool accentEnabled = false; + std::string resourceError = {}; + std::string lastCommandId = {}; + XCUIAssetDocumentSource documentSource = XCUIAssetDocumentSource( + XCUIAssetDocumentSource::MakeDemoPathSet()); + fs::path repoRoot = {}; + fs::path viewPath = {}; + fs::path themePath = {}; + fs::file_time_type viewWriteTime = {}; + fs::file_time_type themeWriteTime = {}; +}; + +Color AdjustBrightness(const Color& color, float multiplier) { + return Color( + (std::min)(1.0f, (std::max)(0.0f, color.r * multiplier)), + (std::min)(1.0f, (std::max)(0.0f, color.g * multiplier)), + (std::min)(1.0f, (std::max)(0.0f, color.b * multiplier)), + color.a); +} + +UIColor ToUIColor(const Color& color) { + return UIColor(color.r, color.g, color.b, color.a); +} + +std::string ToStdString(const String& value) { + return std::string(value.CStr()); +} + +String ToContainersString(const std::string& value) { + return String(value.c_str()); +} + +float Clamp01(float value) { + return (std::min)(1.0f, (std::max)(0.0f, value)); +} + +bool IsUtf8ContinuationByte(unsigned char value) { + return (value & 0xC0u) == 0x80u; +} + +std::size_t CountUtf8Codepoints(const std::string& text) { + std::size_t count = 0u; + for (unsigned char ch : text) { + if (!IsUtf8ContinuationByte(ch)) { + ++count; + } + } + return count; +} + +float MeasureGlyphRunWidth(const std::string& text, float fontSize) { + return fontSize * kApproximateTextWidthFactor * static_cast(CountUtf8Codepoints(text)); +} + +std::size_t AdvanceUtf8Offset(const std::string& text, std::size_t offset) { + if (offset >= text.size()) { + return text.size(); + } + + ++offset; + while (offset < text.size() && IsUtf8ContinuationByte(static_cast(text[offset]))) { + ++offset; + } + return offset; +} + +std::size_t RetreatUtf8Offset(const std::string& text, std::size_t offset) { + if (offset == 0u || text.empty()) { + return 0u; + } + + offset = (std::min)(offset, text.size()); + do { + --offset; + } while (offset > 0u && IsUtf8ContinuationByte(static_cast(text[offset]))); + return offset; +} + +void AppendUtf8Codepoint(std::string& text, std::uint32_t codepoint) { + if (codepoint <= 0x7Fu) { + text.push_back(static_cast(codepoint)); + return; + } + + if (codepoint <= 0x7FFu) { + text.push_back(static_cast(0xC0u | ((codepoint >> 6u) & 0x1Fu))); + text.push_back(static_cast(0x80u | (codepoint & 0x3Fu))); + return; + } + + if (codepoint >= 0xD800u && codepoint <= 0xDFFFu) { + return; + } + + if (codepoint <= 0xFFFFu) { + text.push_back(static_cast(0xE0u | ((codepoint >> 12u) & 0x0Fu))); + text.push_back(static_cast(0x80u | ((codepoint >> 6u) & 0x3Fu))); + text.push_back(static_cast(0x80u | (codepoint & 0x3Fu))); + return; + } + + if (codepoint <= 0x10FFFFu) { + text.push_back(static_cast(0xF0u | ((codepoint >> 18u) & 0x07u))); + text.push_back(static_cast(0x80u | ((codepoint >> 12u) & 0x3Fu))); + text.push_back(static_cast(0x80u | ((codepoint >> 6u) & 0x3Fu))); + text.push_back(static_cast(0x80u | (codepoint & 0x3Fu))); + } +} + +bool ContainsPoint(const UIRect& rect, const UIPoint& point) { + return point.x >= rect.x && + point.y >= rect.y && + point.x <= rect.x + rect.width && + point.y <= rect.y + rect.height; +} + +const UIDocumentAttribute* FindAttribute(const UIDocumentNode& node, const char* name) { + for (const UIDocumentAttribute& attribute : node.attributes) { + if (std::string(attribute.name.CStr()) == name) { + return &attribute; + } + } + + return nullptr; +} + +std::string GetAttributeValue( + const UIDocumentNode& node, + const char* name, + const std::string& fallback = {}) { + const UIDocumentAttribute* attribute = FindAttribute(node, name); + return attribute != nullptr + ? std::string(attribute->value.CStr()) + : fallback; +} + +std::string GetNodeAttribute( + const DemoNode& node, + const char* name, + const std::string& fallback = {}) { + const auto it = node.attributes.find(name); + return it != node.attributes.end() ? it->second : fallback; +} + +bool TryParseFloat(const std::string& text, float& outValue) { + if (text.empty()) { + return false; + } + + char* end = nullptr; + const float parsed = std::strtof(text.c_str(), &end); + if (end == text.c_str() || (end != nullptr && *end != '\0')) { + return false; + } + + outValue = parsed; + return true; +} + +bool TryParseBool(const std::string& text, bool& outValue) { + if (text == "1" || text == "true" || text == "on" || text == "yes") { + outValue = true; + return true; + } + + if (text == "0" || text == "false" || text == "off" || text == "no") { + outValue = false; + return true; + } + + return false; +} + +std::vector ParseFloatList(const std::string& text) { + std::vector values = {}; + std::stringstream stream(text); + std::string token; + while (std::getline(stream, token, ',')) { + float value = 0.0f; + if (TryParseFloat(token, value)) { + values.push_back(value); + } + } + return values; +} + +bool TryParseHexColor(const std::string& text, Color& outColor) { + if ((text.size() != 7u && text.size() != 9u) || text[0] != '#') { + return false; + } + + auto parseChannel = [&](std::size_t offset) -> int { + return std::stoi(text.substr(offset, 2), nullptr, 16); + }; + + try { + const int r = parseChannel(1); + const int g = parseChannel(3); + const int b = parseChannel(5); + const int a = text.size() == 9u ? parseChannel(7) : 255; + outColor = Color( + static_cast(r) / 255.0f, + static_cast(g) / 255.0f, + static_cast(b) / 255.0f, + static_cast(a) / 255.0f); + return true; + } catch (...) { + return false; + } +} + +Style::UIThickness ParseStyleThickness(const std::string& text, float fallback = 0.0f) { + const std::vector values = ParseFloatList(text); + if (values.empty()) { + float uniform = fallback; + TryParseFloat(text, uniform); + return Style::UIThickness::Uniform((std::max)(0.0f, uniform)); + } + + if (values.size() == 1u) { + return Style::UIThickness::Uniform((std::max)(0.0f, values[0])); + } + + if (values.size() == 2u) { + return Style::UIThickness{ + (std::max)(0.0f, values[0]), + (std::max)(0.0f, values[1]), + (std::max)(0.0f, values[0]), + (std::max)(0.0f, values[1]) }; + } + + return Style::UIThickness{ + (std::max)(0.0f, values[0]), + (std::max)(0.0f, values[1]), + (std::max)(0.0f, values.size() > 2u ? values[2] : values[0]), + (std::max)(0.0f, values.size() > 3u ? values[3] : values[1]) }; +} + +Style::UICornerRadius ParseCornerRadius(const std::string& text, float fallback = 0.0f) { + const std::vector values = ParseFloatList(text); + if (values.empty()) { + float uniform = fallback; + TryParseFloat(text, uniform); + return Style::UICornerRadius::Uniform((std::max)(0.0f, uniform)); + } + + if (values.size() == 1u) { + return Style::UICornerRadius::Uniform((std::max)(0.0f, values[0])); + } + + Style::UICornerRadius radius = {}; + radius.topLeft = (std::max)(0.0f, values[0]); + radius.topRight = (std::max)(0.0f, values.size() > 1u ? values[1] : values[0]); + radius.bottomRight = (std::max)(0.0f, values.size() > 2u ? values[2] : radius.topRight); + radius.bottomLeft = (std::max)(0.0f, values.size() > 3u ? values[3] : radius.topLeft); + return radius; +} + +Layout::UILayoutThickness ToLayoutThickness(const Style::UIThickness& thickness) { + return Layout::UILayoutThickness( + thickness.left, + thickness.top, + thickness.right, + thickness.bottom); +} + +float ResolveFloatToken( + const Style::UITheme& theme, + const std::string& tokenName, + float fallbackValue) { + const Style::UITokenResolveResult result = + theme.ResolveToken(tokenName, Style::UIStyleValueType::Float); + if (result.status != Style::UITokenResolveStatus::Resolved) { + return fallbackValue; + } + + const float* value = result.value.TryGetFloat(); + return value != nullptr ? *value : fallbackValue; +} + +Color ResolveColorToken( + const Style::UITheme& theme, + const std::string& tokenName, + const Color& fallbackValue) { + const Style::UITokenResolveResult result = + theme.ResolveToken(tokenName, Style::UIStyleValueType::Color); + if (result.status != Style::UITokenResolveStatus::Resolved) { + return fallbackValue; + } + + const Color* value = result.value.TryGetColor(); + return value != nullptr ? *value : fallbackValue; +} + +Color ResolveColorSpec( + const std::string& rawValue, + const Style::UITheme& theme, + const Color& fallbackValue) { + if (rawValue.empty()) { + return fallbackValue; + } + + Color parsedColor = {}; + if (TryParseHexColor(rawValue, parsedColor)) { + return parsedColor; + } + + std::string tokenName = rawValue; + constexpr char kTokenPrefix[] = "token:"; + if (tokenName.rfind(kTokenPrefix, 0u) == 0u) { + tokenName = tokenName.substr(sizeof(kTokenPrefix) - 1u); + } + + return ResolveColorToken(theme, tokenName, fallbackValue); +} + +float MeasureTextWidth(const std::string& text, float fontSize) { + return (std::max)(32.0f, MeasureGlyphRunWidth(text, fontSize)); +} + +float MeasureTextHeight(float fontSize) { + return fontSize + 8.0f; +} + +UIRect InsetRect(const UIRect& rect, const Layout::UILayoutThickness& padding) { + return UIRect( + rect.x + padding.left, + rect.y + padding.top, + (std::max)(0.0f, rect.width - padding.Horizontal()), + (std::max)(0.0f, rect.height - padding.Vertical())); +} + +bool IsToggleNode(const DemoNode& node) { + return node.tagName == "Toggle"; +} + +bool IsProgressBarNode(const DemoNode& node) { + return node.tagName == "ProgressBar"; +} + +bool IsSwatchNode(const DemoNode& node) { + return node.tagName == "Swatch"; +} + +bool IsTextFieldNode(const DemoNode& node) { + return node.tagName == "TextField"; +} + +Style::UIStyleValue ParseThemeTokenValue( + const std::string& typeName, + const std::string& rawValue, + bool& outSucceeded) { + outSucceeded = true; + + if (typeName == "float") { + float value = 0.0f; + outSucceeded = TryParseFloat(rawValue, value); + return Style::UIStyleValue(value); + } + + if (typeName == "color") { + Color color = {}; + outSucceeded = TryParseHexColor(rawValue, color); + return Style::UIStyleValue(color); + } + + if (typeName == "thickness") { + return Style::UIStyleValue(ParseStyleThickness(rawValue)); + } + + if (typeName == "corner-radius") { + return Style::UIStyleValue(ParseCornerRadius(rawValue)); + } + + outSucceeded = false; + return Style::UIStyleValue(); +} + +Style::UITheme BuildThemeFromDocument(const UIDocumentModel& document, std::string& outError) { + Style::UIThemeDefinition definition = {}; + definition.name = document.displayName.Empty() + ? "XCUI Demo Theme" + : ToStdString(document.displayName); + + for (const UIDocumentNode& child : document.rootNode.children) { + if (ToStdString(child.tagName) != "Token") { + continue; + } + + const std::string tokenName = GetAttributeValue(child, "name"); + const std::string typeName = GetAttributeValue(child, "type"); + const std::string valueText = GetAttributeValue(child, "value"); + if (tokenName.empty() || typeName.empty() || valueText.empty()) { + continue; + } + + bool succeeded = false; + const Style::UIStyleValue parsedValue = + ParseThemeTokenValue(typeName, valueText, succeeded); + if (!succeeded) { + outError = "Failed to parse theme token: " + tokenName; + return Style::BuildBuiltinTheme(Style::UIBuiltinThemeKind::NeutralDark); + } + + definition.SetToken(tokenName, parsedValue); + } + + return Style::BuildTheme(definition); +} + +void ConfigureDemoStyleSheet(Style::UIStyleSheet& styleSheet) { + styleSheet.DefaultStyle().SetProperty( + Style::UIStylePropertyId::ForegroundColor, + Style::UIStyleValue::Token("color.text.primary")); + styleSheet.DefaultStyle().SetProperty( + Style::UIStylePropertyId::BorderColor, + Style::UIStyleValue::Token("color.outline")); + styleSheet.DefaultStyle().SetProperty( + Style::UIStylePropertyId::BorderWidth, + Style::UIStyleValue::Token("line.default")); + styleSheet.DefaultStyle().SetProperty( + Style::UIStylePropertyId::CornerRadius, + Style::UIStyleValue::Token("radius.card")); + styleSheet.DefaultStyle().SetProperty( + Style::UIStylePropertyId::FontSize, + Style::UIStyleValue::Token("font.body")); + styleSheet.DefaultStyle().SetProperty( + Style::UIStylePropertyId::Gap, + Style::UIStyleValue::Token("space.regular")); + + styleSheet.GetOrCreateTypeStyle("View").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.surface")); + styleSheet.GetOrCreateTypeStyle("View").SetProperty( + Style::UIStylePropertyId::Padding, + Style::UIStyleValue::Token("padding.panel")); + + styleSheet.GetOrCreateTypeStyle("Column").SetProperty( + Style::UIStylePropertyId::Gap, + Style::UIStyleValue::Token("space.regular")); + styleSheet.GetOrCreateTypeStyle("Row").SetProperty( + Style::UIStylePropertyId::Gap, + Style::UIStyleValue::Token("space.regular")); + + styleSheet.GetOrCreateTypeStyle("Card").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.surface.elevated")); + styleSheet.GetOrCreateTypeStyle("Card").SetProperty( + Style::UIStylePropertyId::Padding, + Style::UIStyleValue::Token("padding.card")); + + styleSheet.GetOrCreateTypeStyle("Button").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.accent")); + styleSheet.GetOrCreateTypeStyle("Button").SetProperty( + Style::UIStylePropertyId::Padding, + Style::UIStyleValue::Token("padding.card")); + styleSheet.GetOrCreateTypeStyle("Button").SetProperty( + Style::UIStylePropertyId::ForegroundColor, + Style::UIStyleValue(Color(0.06f, 0.08f, 0.10f, 1.0f))); + styleSheet.GetOrCreateTypeStyle("Toggle").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.surface.card")); + styleSheet.GetOrCreateTypeStyle("Toggle").SetProperty( + Style::UIStylePropertyId::Padding, + Style::UIStyleValue(ParseStyleThickness("10,8"))); + styleSheet.GetOrCreateTypeStyle("Toggle").SetProperty( + Style::UIStylePropertyId::CornerRadius, + Style::UIStyleValue::Token("radius.pill")); + styleSheet.GetOrCreateTypeStyle("ProgressBar").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.surface.track")); + styleSheet.GetOrCreateTypeStyle("ProgressBar").SetProperty( + Style::UIStylePropertyId::Padding, + Style::UIStyleValue(ParseStyleThickness("4"))); + styleSheet.GetOrCreateTypeStyle("ProgressBar").SetProperty( + Style::UIStylePropertyId::CornerRadius, + Style::UIStyleValue::Token("radius.button")); + styleSheet.GetOrCreateTypeStyle("Swatch").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.surface.card")); + styleSheet.GetOrCreateTypeStyle("Swatch").SetProperty( + Style::UIStylePropertyId::Padding, + Style::UIStyleValue(ParseStyleThickness("6"))); + styleSheet.GetOrCreateTypeStyle("Swatch").SetProperty( + Style::UIStylePropertyId::CornerRadius, + Style::UIStyleValue::Token("radius.button")); + styleSheet.GetOrCreateTypeStyle("TextField").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.surface.input")); + styleSheet.GetOrCreateTypeStyle("TextField").SetProperty( + Style::UIStylePropertyId::Padding, + Style::UIStyleValue(ParseStyleThickness("12,10"))); + styleSheet.GetOrCreateTypeStyle("TextField").SetProperty( + Style::UIStylePropertyId::CornerRadius, + Style::UIStyleValue::Token("radius.button")); + + styleSheet.GetOrCreateNamedStyle("HeroCard").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.surface.elevated")); + styleSheet.GetOrCreateNamedStyle("MetricCard").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.surface.card")); + styleSheet.GetOrCreateNamedStyle("DebugCard").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.surface.card")); + styleSheet.GetOrCreateNamedStyle("AccentButton").SetProperty( + Style::UIStylePropertyId::BackgroundColor, + Style::UIStyleValue::Token("color.accent")); + styleSheet.GetOrCreateNamedStyle("AccentButton").SetProperty( + Style::UIStylePropertyId::ForegroundColor, + Style::UIStyleValue(Color(0.06f, 0.08f, 0.10f, 1.0f))); + + styleSheet.GetOrCreateNamedStyle("HeroTitle").SetProperty( + Style::UIStylePropertyId::FontSize, + Style::UIStyleValue::Token("font.title")); + styleSheet.GetOrCreateNamedStyle("HeroSubtitle").SetProperty( + Style::UIStylePropertyId::ForegroundColor, + Style::UIStyleValue::Token("color.text.secondary")); + styleSheet.GetOrCreateNamedStyle("MetricLabel").SetProperty( + Style::UIStylePropertyId::ForegroundColor, + Style::UIStyleValue::Token("color.text.secondary")); + styleSheet.GetOrCreateNamedStyle("MetricValue").SetProperty( + Style::UIStylePropertyId::FontSize, + Style::UIStyleValue::Token("font.title")); + styleSheet.GetOrCreateNamedStyle("ButtonLabel").SetProperty( + Style::UIStylePropertyId::ForegroundColor, + Style::UIStyleValue(Color(0.06f, 0.08f, 0.10f, 1.0f))); + styleSheet.GetOrCreateNamedStyle("CommandField").SetProperty( + Style::UIStylePropertyId::BorderColor, + Style::UIStyleValue::Token("color.accent.alt")); + styleSheet.GetOrCreateNamedStyle("Meta").SetProperty( + Style::UIStylePropertyId::ForegroundColor, + Style::UIStyleValue::Token("color.text.secondary")); +} + +UIElementId HashElementKey(const std::string& key) { + constexpr std::uint64_t kOffsetBasis = 1469598103934665603ull; + constexpr std::uint64_t kPrime = 1099511628211ull; + std::uint64_t hash = kOffsetBasis; + for (unsigned char ch : key) { + hash ^= ch; + hash *= kPrime; + } + + return hash == 0u ? 1u : hash; +} + +std::optional FindRepositoryRoot() { + std::vector candidates = {}; + const String resourceRoot = ResourceManager::Get().GetResourceRoot(); + if (!resourceRoot.Empty()) { + candidates.push_back(fs::path(resourceRoot.CStr())); + } + + std::error_code ec; + const fs::path currentPath = fs::current_path(ec); + if (!ec) { + candidates.push_back(currentPath); + } + + for (const fs::path& candidate : candidates) { + fs::path probe = candidate; + while (!probe.empty()) { + if (fs::exists(probe / kViewRelativePath) && + fs::exists(probe / kThemeRelativePath)) { + return probe; + } + + const fs::path parent = probe.parent_path(); + if (parent == probe) { + break; + } + probe = parent; + } + } + + return std::nullopt; +} + +bool TryGetWriteTime(const fs::path& path, fs::file_time_type& outWriteTime) { + try { + if (!path.empty() && fs::exists(path)) { + outWriteTime = fs::last_write_time(path); + return true; + } + } catch (...) { + } + + return false; +} + +bool UpdateTrackedWriteTimes(RuntimeBuildContext& state) { + fs::file_time_type viewWriteTime = {}; + fs::file_time_type themeWriteTime = {}; + if (!TryGetWriteTime(state.viewPath, viewWriteTime) || + !TryGetWriteTime(state.themePath, themeWriteTime)) { + return false; + } + + state.viewWriteTime = viewWriteTime; + state.themeWriteTime = themeWriteTime; + return true; +} + +bool DocumentsChangedOnDisk(const RuntimeBuildContext& state) { + fs::file_time_type viewWriteTime = {}; + fs::file_time_type themeWriteTime = {}; + if (!TryGetWriteTime(state.viewPath, viewWriteTime) || + !TryGetWriteTime(state.themePath, themeWriteTime)) { + return false; + } + + return viewWriteTime != state.viewWriteTime || + themeWriteTime != state.themeWriteTime; +} + +std::string BuildElementKey( + const UIDocumentNode& node, + const std::string& parentKey, + std::size_t siblingIndex) { + const std::string explicitId = GetAttributeValue(node, "id"); + if (!explicitId.empty()) { + return explicitId; + } + + const std::string generated = + ToStdString(node.tagName) + std::to_string(static_cast(siblingIndex)); + return parentKey.empty() + ? generated + : parentKey + "/" + generated; +} + +std::string BuildNodeDisplayText( + const RuntimeBuildContext& state, + const DemoNode& node, + const XCUIDemoFrameStats& stats) { + if (node.tagName != "Text") { + return node.staticText; + } + + if (node.elementKey == "statusFocus") { + return "Focus: " + (stats.focusedElementId.empty() ? std::string("none") : stats.focusedElementId); + } + if (node.elementKey == "statusLayout") { + return "Layout: gen " + std::to_string(static_cast(stats.treeGeneration)) + + " / dirty roots " + std::to_string(stats.dirtyRootCount); + } + if (node.elementKey == "statusCommands") { + return "Commands: " + std::to_string(stats.commandCount) + + " / last " + (stats.lastCommandId.empty() ? std::string("none") : stats.lastCommandId); + } + if (node.elementKey == "statusWidgets") { + const auto densityIt = state.toggleStates.find("toggleDensity"); + const auto diagnosticsIt = state.toggleStates.find("toggleDiagnostics"); + const bool densityEnabled = densityIt != state.toggleStates.end() && densityIt->second; + const bool diagnosticsEnabled = diagnosticsIt != state.toggleStates.end() && diagnosticsIt->second; + return "Widgets: density " + + std::string(densityEnabled ? "on" : "off") + + " / debug " + + std::string(diagnosticsEnabled ? "on" : "off"); + } + if (node.elementKey == "subtitle" && stats.accentEnabled) { + return "Markup -> Layout -> Style -> DrawData (accent alt active)"; + } + + return node.staticText; +} + +Style::UIStylePropertyResolution ResolveProperty( + const DemoNode& node, + const Style::UITheme& theme, + const Style::UIStyleSheet& styleSheet, + Style::UIStylePropertyId propertyId) { + Style::UIStyleResolveContext context = {}; + context.theme = &theme; + context.styleSheet = &styleSheet; + context.selector.typeName = node.tagName; + context.selector.styleName = node.styleName; + return Style::ResolveStyleProperty(propertyId, context); +} + +Color GetColorProperty( + const DemoNode& node, + const Style::UITheme& theme, + const Style::UIStyleSheet& styleSheet, + Style::UIStylePropertyId propertyId, + const Color& fallback) { + const Style::UIStylePropertyResolution resolution = + ResolveProperty(node, theme, styleSheet, propertyId); + const Color* value = resolution.resolved ? resolution.value.TryGetColor() : nullptr; + return value != nullptr ? *value : fallback; +} + +float GetFloatProperty( + const DemoNode& node, + const Style::UITheme& theme, + const Style::UIStyleSheet& styleSheet, + Style::UIStylePropertyId propertyId, + float fallback) { + const Style::UIStylePropertyResolution resolution = + ResolveProperty(node, theme, styleSheet, propertyId); + const float* value = resolution.resolved ? resolution.value.TryGetFloat() : nullptr; + return value != nullptr ? *value : fallback; +} + +Style::UIThickness GetPaddingProperty( + const DemoNode& node, + const Style::UITheme& theme, + const Style::UIStyleSheet& styleSheet) { + const Style::UIStylePropertyResolution resolution = + ResolveProperty(node, theme, styleSheet, Style::UIStylePropertyId::Padding); + const Style::UIThickness* value = resolution.resolved + ? resolution.value.TryGetThickness() + : nullptr; + return value != nullptr ? *value : Style::UIThickness::Uniform(0.0f); +} + +Style::UICornerRadius GetCornerRadiusProperty( + const DemoNode& node, + const Style::UITheme& theme, + const Style::UIStyleSheet& styleSheet) { + const Style::UIStylePropertyResolution resolution = + ResolveProperty(node, theme, styleSheet, Style::UIStylePropertyId::CornerRadius); + const Style::UICornerRadius* value = resolution.resolved + ? resolution.value.TryGetCornerRadius() + : nullptr; + return value != nullptr ? *value : Style::UICornerRadius::Uniform(0.0f); +} + +float ResolveGap(const DemoNode& node, const Style::UITheme& theme, const Style::UIStyleSheet& styleSheet) { + if (!node.gapAttr.empty()) { + float gap = 0.0f; + if (TryParseFloat(node.gapAttr, gap)) { + return (std::max)(0.0f, gap); + } + } + + return GetFloatProperty(node, theme, styleSheet, Style::UIStylePropertyId::Gap, 0.0f); +} + +Layout::UILayoutThickness ResolvePadding( + const DemoNode& node, + const Style::UITheme& theme, + const Style::UIStyleSheet& styleSheet) { + if (!node.paddingAttr.empty()) { + return ToLayoutThickness(ParseStyleThickness(node.paddingAttr)); + } + + return ToLayoutThickness(GetPaddingProperty(node, theme, styleSheet)); +} + +Layout::UILayoutLength ResolveWidth(const DemoNode& node) { + if (node.widthAttr == "stretch") { + return Layout::UILayoutLength::Stretch(); + } + + float width = 0.0f; + if (TryParseFloat(node.widthAttr, width)) { + return Layout::UILayoutLength::Pixels(width); + } + + return Layout::UILayoutLength::Auto(); +} + +std::string ResolveToggleStateKey(const DemoNode& node) { + const std::string stateKey = GetNodeAttribute(node, "state-key"); + return !stateKey.empty() ? stateKey : node.elementKey; +} + +bool ResolveToggleState(const RuntimeBuildContext& state, const DemoNode& node) { + const std::string stateKey = ResolveToggleStateKey(node); + const auto it = state.toggleStates.find(stateKey); + if (it != state.toggleStates.end()) { + return it->second; + } + + bool defaultValue = false; + return TryParseBool(GetNodeAttribute(node, "default"), defaultValue) ? defaultValue : false; +} + +float ResolveProgressValue(const RuntimeBuildContext& state, const DemoNode& node) { + const std::string stateKey = GetNodeAttribute(node, "state-key"); + if (!stateKey.empty()) { + DemoNode stateProbe = node; + stateProbe.elementKey = stateKey; + const bool enabled = ResolveToggleState(state, stateProbe); + const std::string valueWhenEnabled = GetNodeAttribute(node, "value-on"); + const std::string valueWhenDisabled = GetNodeAttribute(node, "value-off"); + float parsedValue = 0.0f; + if (enabled && TryParseFloat(valueWhenEnabled, parsedValue)) { + return Clamp01(parsedValue); + } + if (!enabled && TryParseFloat(valueWhenDisabled, parsedValue)) { + return Clamp01(parsedValue); + } + } + + const std::string valueText = GetNodeAttribute(node, "value"); + if (valueText == "accent") { + return state.accentEnabled ? 1.0f : 0.0f; + } + + float parsedValue = 0.0f; + return TryParseFloat(valueText, parsedValue) ? Clamp01(parsedValue) : 0.0f; +} + +std::string ResolveTextFieldStateKey(const DemoNode& node) { + const std::string stateKey = GetNodeAttribute(node, "state-key"); + return !stateKey.empty() ? stateKey : node.elementKey; +} + +void EnsureTextFieldStateInitialized(RuntimeBuildContext& state, const DemoNode& node) { + if (!IsTextFieldNode(node)) { + return; + } + + const std::string stateKey = ResolveTextFieldStateKey(node); + auto valueIt = state.textFieldValues.find(stateKey); + if (valueIt == state.textFieldValues.end()) { + valueIt = state.textFieldValues + .emplace(stateKey, GetNodeAttribute(node, "value")) + .first; + } + + auto caretIt = state.textFieldCarets.find(stateKey); + if (caretIt == state.textFieldCarets.end()) { + caretIt = state.textFieldCarets.emplace(stateKey, valueIt->second.size()).first; + } + + caretIt->second = (std::min)(caretIt->second, valueIt->second.size()); +} + +std::string ResolveTextFieldValue(RuntimeBuildContext& state, const DemoNode& node) { + EnsureTextFieldStateInitialized(state, node); + return state.textFieldValues[ResolveTextFieldStateKey(node)]; +} + +std::size_t ResolveTextFieldCaret(RuntimeBuildContext& state, const DemoNode& node) { + EnsureTextFieldStateInitialized(state, node); + const std::string stateKey = ResolveTextFieldStateKey(node); + std::size_t& caret = state.textFieldCarets[stateKey]; + caret = (std::min)(caret, state.textFieldValues[stateKey].size()); + return caret; +} + +DemoNode* TryGetNodeByElementId(RuntimeBuildContext& state, UIElementId elementId) { + const auto it = state.nodeIndexById.find(elementId); + return it != state.nodeIndexById.end() ? &state.nodes[it->second] : nullptr; +} + +bool HandleTextFieldCharacterInput( + RuntimeBuildContext& state, + UIElementId elementId, + std::uint32_t character) { + if (character < 32u || character == 127u) { + return false; + } + + DemoNode* node = TryGetNodeByElementId(state, elementId); + if (node == nullptr || !IsTextFieldNode(*node)) { + return false; + } + + EnsureTextFieldStateInitialized(state, *node); + const std::string stateKey = ResolveTextFieldStateKey(*node); + std::string& value = state.textFieldValues[stateKey]; + std::size_t& caret = state.textFieldCarets[stateKey]; + caret = (std::min)(caret, value.size()); + + std::string encoded = {}; + AppendUtf8Codepoint(encoded, character); + if (encoded.empty()) { + return false; + } + + value.insert(caret, encoded); + caret += encoded.size(); + state.lastCommandId = "demo.text.edit." + stateKey; + return true; +} + +bool HandleTextFieldKeyDown( + RuntimeBuildContext& state, + UIElementId elementId, + std::int32_t keyCode) { + DemoNode* node = TryGetNodeByElementId(state, elementId); + if (node == nullptr || !IsTextFieldNode(*node)) { + return false; + } + + EnsureTextFieldStateInitialized(state, *node); + const std::string stateKey = ResolveTextFieldStateKey(*node); + std::string& value = state.textFieldValues[stateKey]; + std::size_t& caret = state.textFieldCarets[stateKey]; + caret = (std::min)(caret, value.size()); + + if (keyCode == static_cast(KeyCode::Backspace) || + keyCode == static_cast(KeyCode::Delete)) { + if (caret > 0u) { + const std::size_t previousCaret = RetreatUtf8Offset(value, caret); + value.erase(previousCaret, caret - previousCaret); + caret = previousCaret; + state.lastCommandId = "demo.text.edit." + stateKey; + } + return true; + } + + if (keyCode == static_cast(KeyCode::Left)) { + caret = RetreatUtf8Offset(value, caret); + return true; + } + + if (keyCode == static_cast(KeyCode::Right)) { + caret = AdvanceUtf8Offset(value, caret); + return true; + } + + if (keyCode == static_cast(KeyCode::Home)) { + caret = 0u; + return true; + } + + if (keyCode == static_cast(KeyCode::End)) { + caret = value.size(); + return true; + } + + if (keyCode == static_cast(KeyCode::Enter)) { + state.lastCommandId = "demo.text.submit." + stateKey; + return true; + } + + return false; +} + +std::string BuildActivationCommandId(const DemoNode& node) { + if (!node.actionId.empty()) { + return node.actionId; + } + + if (IsToggleNode(node)) { + return "demo.toggle." + ResolveToggleStateKey(node); + } + + return "demo.activate." + node.elementKey; +} + +bool IsInteractiveElementId(const RuntimeBuildContext& state, UIElementId elementId) { + const auto it = state.nodeIndexById.find(elementId); + return it != state.nodeIndexById.end() && state.nodes[it->second].interactive; +} + +void ActivateNode(RuntimeBuildContext& state, UIElementId elementId) { + const auto it = state.nodeIndexById.find(elementId); + if (it == state.nodeIndexById.end()) { + return; + } + + const DemoNode& node = state.nodes[it->second]; + if (IsToggleNode(node)) { + const std::string stateKey = ResolveToggleStateKey(node); + state.toggleStates[stateKey] = !ResolveToggleState(state, node); + } + + if (node.actionId == kToggleAccentCommandId || node.elementKey == "toggleAccent") { + state.accentEnabled = !state.accentEnabled; + } + + state.lastCommandId = BuildActivationCommandId(node); +} + +void BuildDemoNodesRecursive( + RuntimeBuildContext& state, + const UIDocumentNode& sourceNode, + std::size_t parentIndex, + const std::string& parentKey, + std::size_t siblingIndex) { + DemoNode node = {}; + node.tagName = ToStdString(sourceNode.tagName); + for (const UIDocumentAttribute& attribute : sourceNode.attributes) { + node.attributes[ToStdString(attribute.name)] = ToStdString(attribute.value); + } + node.styleName = GetAttributeValue(sourceNode, "style"); + node.staticText = GetAttributeValue(sourceNode, "text"); + node.actionId = GetAttributeValue(sourceNode, "action"); + node.gapAttr = GetAttributeValue(sourceNode, "gap"); + node.paddingAttr = GetAttributeValue(sourceNode, "padding"); + node.widthAttr = GetAttributeValue(sourceNode, "width"); + node.parentIndex = parentIndex; + node.elementKey = BuildElementKey(sourceNode, parentKey, siblingIndex); + node.elementId = HashElementKey(node.elementKey); + node.interactive = + node.tagName == "Button" || + node.tagName == "Toggle" || + node.tagName == "TextField"; + + const std::size_t nodeIndex = state.nodes.size(); + state.nodes.push_back(node); + state.nodeIndexByKey[state.nodes[nodeIndex].elementKey] = nodeIndex; + state.nodeIndexById[state.nodes[nodeIndex].elementId] = nodeIndex; + if (parentIndex != kInvalidIndex) { + state.nodes[parentIndex].children.push_back(nodeIndex); + } + + if (state.nodes[nodeIndex].actionId == kToggleAccentCommandId || + state.nodes[nodeIndex].elementKey == "toggleAccent") { + state.toggleButtonId = state.nodes[nodeIndex].elementId; + state.nodes[nodeIndex].interactive = true; + } + + if (IsTextFieldNode(state.nodes[nodeIndex])) { + EnsureTextFieldStateInitialized(state, state.nodes[nodeIndex]); + } + + for (std::size_t childIndex = 0; childIndex < sourceNode.children.Size(); ++childIndex) { + BuildDemoNodesRecursive( + state, + sourceNode.children[childIndex], + nodeIndex, + state.nodes[nodeIndex].elementKey, + childIndex); + } +} + +void RebuildInputPaths(RuntimeBuildContext& state, std::size_t index) { + DemoNode& node = state.nodes[index]; + if (node.parentIndex == kInvalidIndex) { + node.path.elements = { node.elementId }; + } else { + node.path = state.nodes[node.parentIndex].path; + node.path.elements.push_back(node.elementId); + } + + for (std::size_t childIndex : node.children) { + RebuildInputPaths(state, childIndex); + } +} + +UISize MeasureNode(RuntimeBuildContext& state, std::size_t index) { + DemoNode& node = state.nodes[index]; + if (node.tagName == "Text") { + const float fontSize = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::FontSize, + 14.0f); + node.desiredSize = UISize( + MeasureTextWidth(node.resolvedText, fontSize), + MeasureTextHeight(fontSize)); + return node.desiredSize; + } + + if (IsToggleNode(node)) { + const float fontSize = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::FontSize, + 14.0f); + const Layout::UILayoutThickness padding = + ResolvePadding(node, state.activeTheme, state.styleSheet); + const float gap = ResolveGap(node, state.activeTheme, state.styleSheet); + const float contentWidth = 34.0f + gap + MeasureTextWidth(node.staticText, fontSize); + const float contentHeight = (std::max)(18.0f, MeasureTextHeight(fontSize)); + node.desiredSize = UISize( + contentWidth + padding.Horizontal(), + contentHeight + padding.Vertical()); + return node.desiredSize; + } + + if (IsProgressBarNode(node)) { + const float fontSize = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::FontSize, + 14.0f); + const Layout::UILayoutThickness padding = + ResolvePadding(node, state.activeTheme, state.styleSheet); + const float contentWidth = (std::max)(180.0f, MeasureTextWidth(node.staticText, fontSize) + 84.0f); + const float contentHeight = ResolveFloatToken(state.activeTheme, "font.body", 14.0f) + 10.0f; + node.desiredSize = UISize( + contentWidth + padding.Horizontal(), + contentHeight + padding.Vertical()); + return node.desiredSize; + } + + if (IsSwatchNode(node)) { + const float fontSize = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::FontSize, + 14.0f); + const Layout::UILayoutThickness padding = + ResolvePadding(node, state.activeTheme, state.styleSheet); + float contentHeight = 54.0f; + TryParseFloat(GetNodeAttribute(node, "height"), contentHeight); + if (!node.staticText.empty()) { + contentHeight += MeasureTextHeight(fontSize) + 4.0f; + } + node.desiredSize = UISize(92.0f + padding.Horizontal(), contentHeight + padding.Vertical()); + return node.desiredSize; + } + + if (IsTextFieldNode(node)) { + const float fontSize = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::FontSize, + 14.0f); + const Layout::UILayoutThickness padding = + ResolvePadding(node, state.activeTheme, state.styleSheet); + const std::string value = ResolveTextFieldValue(state, node); + const std::string placeholder = GetNodeAttribute(node, "placeholder"); + float minWidth = 220.0f; + TryParseFloat(GetNodeAttribute(node, "min-width"), minWidth); + const std::string& probeText = value.empty() ? placeholder : value; + node.desiredSize = UISize( + (std::max)( + minWidth, + MeasureTextWidth(probeText.empty() ? std::string(" ") : probeText, fontSize) + + padding.Horizontal() + + 18.0f), + MeasureTextHeight(fontSize) + padding.Vertical() + 8.0f); + return node.desiredSize; + } + + std::vector items = {}; + items.reserve(node.children.size()); + for (std::size_t childIndex : node.children) { + DemoNode& child = state.nodes[childIndex]; + const UISize childSize = MeasureNode(state, childIndex); + + Layout::UILayoutItem item = {}; + item.desiredContentSize = childSize; + item.width = ResolveWidth(child); + item.horizontalAlignment = item.width.IsStretch() + ? Layout::UILayoutAlignment::Stretch + : Layout::UILayoutAlignment::Start; + items.push_back(item); + } + + if (node.tagName == "Row" || node.tagName == "Column") { + Layout::UIStackLayoutOptions options = {}; + options.axis = node.tagName == "Row" + ? Layout::UILayoutAxis::Horizontal + : Layout::UILayoutAxis::Vertical; + options.spacing = ResolveGap(node, state.activeTheme, state.styleSheet); + options.padding = ResolvePadding(node, state.activeTheme, state.styleSheet); + node.desiredSize = Layout::MeasureStackLayout(options, items).desiredSize; + } else { + Layout::UIOverlayLayoutOptions options = {}; + options.padding = ResolvePadding(node, state.activeTheme, state.styleSheet); + node.desiredSize = Layout::MeasureOverlayLayout(options, items).desiredSize; + } + + if (node.desiredSize.width <= 0.0f) { + node.desiredSize.width = 32.0f; + } + if (node.desiredSize.height <= 0.0f) { + node.desiredSize.height = 24.0f; + } + return node.desiredSize; +} + +void ArrangeNode(RuntimeBuildContext& state, std::size_t index, const UIRect& bounds) { + DemoNode& node = state.nodes[index]; + node.rect = bounds; + if (node.children.empty()) { + return; + } + + std::vector items = {}; + items.reserve(node.children.size()); + for (std::size_t childIndex : node.children) { + DemoNode& child = state.nodes[childIndex]; + Layout::UILayoutItem item = {}; + item.desiredContentSize = child.desiredSize; + item.width = ResolveWidth(child); + item.horizontalAlignment = item.width.IsStretch() + ? Layout::UILayoutAlignment::Stretch + : Layout::UILayoutAlignment::Start; + items.push_back(item); + } + + std::vector childResults = {}; + if (node.tagName == "Row" || node.tagName == "Column") { + Layout::UIStackLayoutOptions options = {}; + options.axis = node.tagName == "Row" + ? Layout::UILayoutAxis::Horizontal + : Layout::UILayoutAxis::Vertical; + options.spacing = ResolveGap(node, state.activeTheme, state.styleSheet); + options.padding = ResolvePadding(node, state.activeTheme, state.styleSheet); + childResults = Layout::ArrangeStackLayout(options, items, bounds).children; + } else { + Layout::UIOverlayLayoutOptions options = {}; + options.padding = ResolvePadding(node, state.activeTheme, state.styleSheet); + childResults = Layout::ArrangeOverlayLayout(options, items, bounds).children; + } + + for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) { + ArrangeNode(state, node.children[childOffset], childResults[childOffset].arrangedRect); + } +} + +std::size_t HitTest(const RuntimeBuildContext& state, const UIPoint& point) { + std::size_t bestIndex = kInvalidIndex; + std::size_t bestDepth = 0u; + for (std::size_t index = 0; index < state.nodes.size(); ++index) { + const DemoNode& node = state.nodes[index]; + if (!ContainsPoint(node.rect, point)) { + continue; + } + + const std::size_t depth = node.path.Size(); + if (bestIndex == kInvalidIndex || depth >= bestDepth) { + bestIndex = index; + bestDepth = depth; + } + } + + return bestIndex; +} + +std::size_t ResolveInteractiveTarget(const RuntimeBuildContext& state, std::size_t index) { + std::size_t current = index; + while (current != kInvalidIndex) { + if (state.nodes[current].interactive) { + return current; + } + current = state.nodes[current].parentIndex; + } + + return index; +} + +void RegisterShortcutIfNeeded(RuntimeBuildContext& state) { + if (state.toggleButtonId == 0u || state.registeredShortcutOwnerId == state.toggleButtonId) { + return; + } + + state.inputDispatcher.GetShortcutRegistry().Clear(); + + UI::UIShortcutBinding binding = {}; + binding.scope = UI::UIShortcutScope::Global; + binding.ownerId = state.toggleButtonId; + binding.commandId = kToggleAccentCommandId; + binding.chord.keyCode = static_cast(KeyCode::P); + binding.chord.modifiers.control = true; + state.inputDispatcher.GetShortcutRegistry().RegisterBinding(binding); + state.registeredShortcutOwnerId = state.toggleButtonId; +} + +std::string FindElementKeyById(const RuntimeBuildContext& state, UIElementId elementId) { + const auto it = state.nodeIndexById.find(elementId); + return it != state.nodeIndexById.end() + ? state.nodes[it->second].elementKey + : std::string(); +} + +void DrawProgressBarNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawList& drawList) { + const Layout::UILayoutThickness padding = + ResolvePadding(node, state.activeTheme, state.styleSheet); + const UIRect contentRect = InsetRect(node.rect, padding); + const float rounding = + GetCornerRadiusProperty(node, state.activeTheme, state.styleSheet).topLeft; + const Color trackColor = ResolveColorSpec( + GetNodeAttribute(node, "track"), + state.activeTheme, + GetColorProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::BackgroundColor, + Color(0.16f, 0.21f, 0.28f, 1.0f))); + const Color fillColor = ResolveColorSpec( + GetNodeAttribute(node, "fill"), + state.activeTheme, + ResolveColorToken(state.activeTheme, "color.accent", Color(0.37f, 0.89f, 1.0f, 1.0f))); + const Color textColor = GetColorProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::ForegroundColor, + Color(0.92f, 0.94f, 0.97f, 1.0f)); + const float fontSize = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::FontSize, + 14.0f); + const float value = ResolveProgressValue(state, node); + const float fillWidth = contentRect.width * value; + const UIRect fillRect(contentRect.x, contentRect.y, fillWidth, contentRect.height); + const int percent = static_cast(value * 100.0f + 0.5f); + const std::string percentText = std::to_string(percent) + "%"; + const float percentWidth = MeasureTextWidth(percentText, fontSize); + + drawList.AddFilledRect(contentRect, ToUIColor(trackColor), rounding); + if (fillRect.width > 0.0f) { + drawList.AddFilledRect(fillRect, ToUIColor(fillColor), rounding); + } + + if (!node.staticText.empty()) { + drawList.AddText( + UIPoint(contentRect.x + 8.0f, contentRect.y + (contentRect.height - fontSize) * 0.5f - 2.0f), + node.staticText, + ToUIColor(textColor), + fontSize); + } + + drawList.AddText( + UIPoint( + contentRect.x + (std::max)(8.0f, contentRect.width - percentWidth - 8.0f), + contentRect.y + (contentRect.height - fontSize) * 0.5f - 2.0f), + percentText, + ToUIColor(textColor), + fontSize); +} + +void DrawSwatchNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawList& drawList) { + const Layout::UILayoutThickness padding = + ResolvePadding(node, state.activeTheme, state.styleSheet); + const UIRect contentRect = InsetRect(node.rect, padding); + const float rounding = + GetCornerRadiusProperty(node, state.activeTheme, state.styleSheet).topLeft; + const Color borderColor = GetColorProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::BorderColor, + Color::Clear()); + const Color labelColor = GetColorProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::ForegroundColor, + Color(0.92f, 0.94f, 0.97f, 1.0f)); + const float fontSize = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::FontSize, + 14.0f); + const Color swatchColor = ResolveColorSpec( + GetNodeAttribute(node, "color", GetNodeAttribute(node, "token")), + state.activeTheme, + ResolveColorToken(state.activeTheme, "color.surface.card", Color(0.11f, 0.15f, 0.20f, 1.0f))); + + UIRect swatchRect = contentRect; + if (!node.staticText.empty()) { + const float labelHeight = MeasureTextHeight(fontSize); + swatchRect.height = (std::max)(0.0f, swatchRect.height - labelHeight - 4.0f); + } + + drawList.AddFilledRect(swatchRect, ToUIColor(swatchColor), rounding); + if (borderColor.a > 0.0f) { + drawList.AddRectOutline(swatchRect, ToUIColor(borderColor), 1.0f, rounding); + } + + if (!node.staticText.empty()) { + drawList.AddText( + UIPoint(contentRect.x, swatchRect.y + swatchRect.height + 4.0f), + node.staticText, + ToUIColor(labelColor), + fontSize); + } +} + +void DrawTextFieldNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawList& drawList) { + const Layout::UILayoutThickness padding = + ResolvePadding(node, state.activeTheme, state.styleSheet); + const UIRect contentRect = InsetRect(node.rect, padding); + const float fontSize = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::FontSize, + 14.0f); + const std::string value = ResolveTextFieldValue(state, node); + const std::string placeholder = GetNodeAttribute(node, "placeholder"); + const bool showingPlaceholder = value.empty() && !placeholder.empty(); + const std::string& displayText = showingPlaceholder ? placeholder : value; + const Color textColor = showingPlaceholder + ? ResolveColorToken(state.activeTheme, "color.text.placeholder", Color(0.49f, 0.56f, 0.64f, 1.0f)) + : GetColorProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::ForegroundColor, + Color(0.92f, 0.94f, 0.97f, 1.0f)); + + const float textY = contentRect.y + (contentRect.height - fontSize) * 0.5f - 2.0f; + drawList.AddText( + UIPoint(contentRect.x + 2.0f, textY), + displayText, + ToUIColor(textColor), + fontSize); + + const bool focused = + state.frameResult.stats.focusedElementId == node.elementKey && + !node.elementKey.empty(); + if (!focused) { + return; + } + + const std::size_t caret = ResolveTextFieldCaret(state, node); + const float caretX = + contentRect.x + + 2.0f + + MeasureGlyphRunWidth(value.substr(0u, caret), fontSize); + const Color caretColor = ResolveColorToken( + state.activeTheme, + "color.accent", + Color(0.37f, 0.89f, 1.0f, 1.0f)); + const UIRect caretRect( + caretX, + contentRect.y + 3.0f, + 2.0f, + (std::max)(12.0f, contentRect.height - 6.0f)); + drawList.AddFilledRect(caretRect, ToUIColor(caretColor), 1.0f); +} + +void DrawToggleNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawList& drawList) { + const Layout::UILayoutThickness padding = + ResolvePadding(node, state.activeTheme, state.styleSheet); + const UIRect contentRect = InsetRect(node.rect, padding); + const float fontSize = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::FontSize, + 14.0f); + const float gap = ResolveGap(node, state.activeTheme, state.styleSheet); + const bool enabled = ResolveToggleState(state, node); + const Color labelColor = GetColorProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::ForegroundColor, + Color(0.92f, 0.94f, 0.97f, 1.0f)); + const Color offColor = ResolveColorSpec( + GetNodeAttribute(node, "off-fill"), + state.activeTheme, + ResolveColorToken(state.activeTheme, "color.surface.track", Color(0.16f, 0.21f, 0.28f, 1.0f))); + const Color onColor = ResolveColorSpec( + GetNodeAttribute(node, "fill"), + state.activeTheme, + ResolveColorToken(state.activeTheme, "color.accent", Color(0.37f, 0.89f, 1.0f, 1.0f))); + const Color knobColor = enabled + ? Color(0.06f, 0.08f, 0.10f, 1.0f) + : ResolveColorToken(state.activeTheme, "color.text.primary", Color(0.97f, 0.98f, 1.0f, 1.0f)); + const float trackHeight = 18.0f; + const float trackWidth = 34.0f; + const UIRect trackRect( + contentRect.x, + contentRect.y + (contentRect.height - trackHeight) * 0.5f, + trackWidth, + trackHeight); + const float knobInset = 2.0f; + const float knobSize = trackHeight - knobInset * 2.0f; + const UIRect knobRect( + enabled + ? trackRect.x + trackRect.width - knobSize - knobInset + : trackRect.x + knobInset, + trackRect.y + knobInset, + knobSize, + knobSize); + + drawList.AddFilledRect(trackRect, ToUIColor(enabled ? onColor : offColor), trackHeight * 0.5f); + drawList.AddFilledRect(knobRect, ToUIColor(knobColor), knobSize * 0.5f); + drawList.AddText( + UIPoint(trackRect.x + trackRect.width + gap, contentRect.y + (contentRect.height - fontSize) * 0.5f - 2.0f), + node.staticText, + ToUIColor(labelColor), + fontSize); +} + +void DrawNode(RuntimeBuildContext& state, std::size_t index, UIDrawList& drawList) { + DemoNode& node = state.nodes[index]; + + const bool hovered = + state.frameResult.stats.hoveredElementId == node.elementKey && + !node.elementKey.empty(); + const bool focused = + state.frameResult.stats.focusedElementId == node.elementKey && + !node.elementKey.empty(); + + Color background = GetColorProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::BackgroundColor, + Color::Clear()); + if (hovered && node.interactive) { + background = AdjustBrightness(background, 1.1f); + } else if (focused && node.interactive) { + background = AdjustBrightness(background, 1.18f); + } + + const Color foreground = GetColorProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::ForegroundColor, + Color(0.92f, 0.94f, 0.97f, 1.0f)); + Color borderColor = GetColorProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::BorderColor, + Color::Clear()); + if (focused && IsTextFieldNode(node)) { + borderColor = ResolveColorToken( + state.activeTheme, + "color.accent", + Color(0.37f, 0.89f, 1.0f, 1.0f)); + } + const float borderWidth = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::BorderWidth, + 0.0f); + const float fontSize = GetFloatProperty( + node, + state.activeTheme, + state.styleSheet, + Style::UIStylePropertyId::FontSize, + 14.0f); + const float rounding = + GetCornerRadiusProperty(node, state.activeTheme, state.styleSheet).topLeft; + + if (background.a > 0.0f && node.tagName != "Text") { + drawList.AddFilledRect(node.rect, ToUIColor(background), rounding); + } + + if (borderColor.a > 0.0f && borderWidth > 0.0f && node.tagName != "Text") { + drawList.AddRectOutline(node.rect, ToUIColor(borderColor), borderWidth, rounding); + } + + if (node.tagName == "Text") { + drawList.AddText( + UIPoint(node.rect.x, node.rect.y), + node.resolvedText, + ToUIColor(foreground), + fontSize); + } else if (IsTextFieldNode(node)) { + DrawTextFieldNode(state, node, drawList); + } else if (IsProgressBarNode(node)) { + DrawProgressBarNode(state, node, drawList); + } else if (IsSwatchNode(node)) { + DrawSwatchNode(state, node, drawList); + } else if (IsToggleNode(node)) { + DrawToggleNode(state, node, drawList); + } + + for (std::size_t childIndex : node.children) { + DrawNode(state, childIndex, drawList); + } +} + +} // namespace + +struct XCUIDemoRuntime::RuntimeState { + RuntimeBuildContext data = {}; +}; + +XCUIDemoRuntime::XCUIDemoRuntime() + : m_state(std::make_unique()) { +} + +XCUIDemoRuntime::~XCUIDemoRuntime() = default; + +XCUIDemoRuntime::XCUIDemoRuntime(XCUIDemoRuntime&& other) noexcept = default; + +XCUIDemoRuntime& XCUIDemoRuntime::operator=(XCUIDemoRuntime&& other) noexcept = default; + +bool XCUIDemoRuntime::ReloadDocuments() { + RuntimeBuildContext& state = m_state->data; + state.nodes.clear(); + state.nodeIndexByKey.clear(); + state.nodeIndexById.clear(); + state.toggleButtonId = 0u; + state.registeredShortcutOwnerId = 0u; + state.armedElementId = 0u; + state.documentsReady = false; + state.resourceError.clear(); + state.frameResult = XCUIDemoFrameResult(); + state.documentSource.SetPathSet(XCUIAssetDocumentSource::MakeDemoPathSet()); + if (!state.documentSource.Reload()) { + const XCUIAssetDocumentSource::LoadState& loadState = state.documentSource.GetState(); + state.resourceError = !loadState.errorMessage.empty() + ? loadState.errorMessage + : loadState.statusMessage; + state.frameResult.stats.statusMessage = state.resourceError; + return false; + } + + const XCUIAssetDocumentSource::LoadState& loadState = state.documentSource.GetState(); + state.repoRoot = loadState.repositoryRoot; + state.viewPath = loadState.view.sourcePath; + state.themePath = loadState.theme.sourcePath; + state.viewDocument = loadState.view.compileResult; + state.themeDocument = loadState.theme.compileResult; + + std::string themeError = {}; + state.baseTheme = BuildThemeFromDocument(state.themeDocument.document, themeError); + if (!themeError.empty()) { + state.resourceError = themeError; + state.frameResult.stats.statusMessage = themeError; + return false; + } + + state.styleSheet = Style::UIStyleSheet(); + ConfigureDemoStyleSheet(state.styleSheet); + state.documentsReady = true; + state.frameResult.stats.statusMessage = loadState.usedLegacyFallback + ? "Documents loaded (legacy fallback)" + : "Documents loaded"; + return true; +} + +const XCUIDemoFrameResult& XCUIDemoRuntime::Update(const XCUIDemoInputState& input) { + RuntimeBuildContext& state = m_state->data; + state.frameResult.drawData.Clear(); + state.frameResult.stats = XCUIDemoFrameStats(); + state.frameResult.stats.accentEnabled = state.accentEnabled; + state.frameResult.stats.lastCommandId = state.lastCommandId; + + if (state.documentsReady && state.documentSource.HasTrackedChanges()) { + ReloadDocuments(); + } + + if (!state.documentsReady && !ReloadDocuments()) { + state.frameResult.stats.documentsReady = false; + state.frameResult.stats.statusMessage = state.resourceError.empty() + ? std::string("XCUI demo documents are unavailable.") + : state.resourceError; + return state.frameResult; + } + + state.activeTheme = state.baseTheme; + if (state.accentEnabled) { + const Style::UITokenResolveResult accentAlt = + state.baseTheme.ResolveToken("color.accent.alt", Style::UIStyleValueType::Color); + if (accentAlt.status == Style::UITokenResolveStatus::Resolved) { + state.activeTheme.SetToken("color.accent", accentAlt.value); + } + } + + state.nodes.clear(); + state.nodeIndexByKey.clear(); + state.nodeIndexById.clear(); + state.toggleButtonId = 0u; + BuildDemoNodesRecursive(state, state.viewDocument.document.rootNode, kInvalidIndex, std::string(), 0u); + if (!state.nodes.empty()) { + RebuildInputPaths(state, 0u); + } + + RegisterShortcutIfNeeded(state); + + const UI::UIElementTreeRebuildResult rebuildResult = state.uiContext.Rebuild( + [&state](UI::UIBuildContext& buildContext) { + std::function emitNode = [&](std::size_t index) { + const DemoNode& node = state.nodes[index]; + UI::UIBuildElementDesc desc = {}; + desc.id = node.elementId; + desc.typeName = node.tagName; + desc.structuralRevision = static_cast(node.children.size()); + desc.localStateRevision = + (node.elementId == state.toggleButtonId && state.accentEnabled) || + (IsToggleNode(node) && ResolveToggleState(state, node)) + ? 1u + : 0u; + if (node.children.empty()) { + buildContext.AddLeaf(desc); + return; + } + + auto scope = buildContext.PushElement(desc); + for (std::size_t childIndex : node.children) { + emitNode(childIndex); + } + }; + + if (!state.nodes.empty()) { + emitNode(0u); + } + }); + + if (!rebuildResult.succeeded) { + state.documentsReady = false; + state.resourceError = rebuildResult.errorMessage; + state.frameResult.stats.statusMessage = rebuildResult.errorMessage; + return state.frameResult; + } + + for (DemoNode& node : state.nodes) { + node.resolvedText = BuildNodeDisplayText(state, node, state.frameResult.stats); + node.desiredSize = UISize(0.0f, 0.0f); + node.rect = UIRect(0.0f, 0.0f, 0.0f, 0.0f); + } + + if (!state.nodes.empty()) { + MeasureNode(state, 0u); + ArrangeNode(state, 0u, input.canvasRect); + } + + std::size_t hoveredIndex = kInvalidIndex; + if (input.pointerInside) { + hoveredIndex = ResolveInteractiveTarget(state, HitTest(state, input.pointerPosition)); + } + + UIInputPath hoveredPath = {}; + if (hoveredIndex != kInvalidIndex && hoveredIndex < state.nodes.size()) { + hoveredPath = state.nodes[hoveredIndex].path; + state.frameResult.stats.hoveredElementId = state.nodes[hoveredIndex].elementKey; + } + + auto dispatchHandler = [&](const UI::UIInputDispatchRequest& request) { + UI::UIInputDispatchDecision decision = {}; + if (request.isTargetElement && IsInteractiveElementId(state, request.elementId)) { + decision.handled = true; + } + return decision; + }; + + auto dispatchEvent = [&](const UIInputEvent& event) { + const UI::UIInputDispatchSummary summary = + state.inputDispatcher.Dispatch(event, hoveredPath, dispatchHandler); + + if (event.type == UIInputEventType::PointerButtonDown && + event.pointerButton == UI::UIPointerButton::Left) { + state.armedElementId = + hoveredPath.Target() != 0u && + IsInteractiveElementId(state, hoveredPath.Target()) + ? hoveredPath.Target() + : 0u; + return; + } + + if (event.type == UIInputEventType::PointerButtonUp && + event.pointerButton == UI::UIPointerButton::Left) { + if (state.armedElementId != 0u && + hoveredPath.Target() == state.armedElementId) { + ActivateNode(state, hoveredPath.Target()); + } + state.armedElementId = 0u; + return; + } + + if (event.type == UIInputEventType::FocusLost) { + state.armedElementId = 0u; + return; + } + + if (event.type == UIInputEventType::KeyDown && + !event.repeat && + summary.shortcutHandled && + summary.commandId == kToggleAccentCommandId) { + ActivateNode(state, state.toggleButtonId); + return; + } + + const UIElementId focusedElementId = + state.inputDispatcher.GetFocusController().GetFocusedPath().Target(); + + if (event.type == UIInputEventType::Character && + !event.modifiers.control && + !event.modifiers.alt && + !event.modifiers.super) { + if (HandleTextFieldCharacterInput(state, focusedElementId, event.character)) { + return; + } + } + + if (event.type == UIInputEventType::KeyDown) { + if (HandleTextFieldKeyDown(state, focusedElementId, event.keyCode)) { + return; + } + } + }; + + if (!input.events.empty()) { + for (const UIInputEvent& event : input.events) { + dispatchEvent(event); + } + if (!input.pointerDown) { + state.armedElementId = 0u; + } + } else { + if (input.pointerPressed) { + UIInputEvent event = {}; + event.type = UIInputEventType::PointerButtonDown; + event.pointerButton = UI::UIPointerButton::Left; + event.position = input.pointerPosition; + dispatchEvent(event); + } + + if (input.pointerReleased) { + UIInputEvent event = {}; + event.type = UIInputEventType::PointerButtonUp; + event.pointerButton = UI::UIPointerButton::Left; + event.position = input.pointerPosition; + dispatchEvent(event); + } else if (!input.pointerDown) { + state.armedElementId = 0u; + } + + if (input.shortcutPressed) { + UIInputEvent event = {}; + event.type = UIInputEventType::KeyDown; + event.keyCode = static_cast(KeyCode::P); + event.modifiers.control = true; + dispatchEvent(event); + } + } + + state.frameResult.stats.focusedElementId = FindElementKeyById( + state, + state.inputDispatcher.GetFocusController().GetFocusedPath().Target()); + state.frameResult.stats.accentEnabled = state.accentEnabled; + state.frameResult.stats.lastCommandId = state.lastCommandId; + + for (DemoNode& node : state.nodes) { + node.resolvedText = BuildNodeDisplayText(state, node, state.frameResult.stats); + } + + UIDrawList drawList("XCUI Demo Runtime"); + drawList.PushClipRect(input.canvasRect); + if (!state.nodes.empty()) { + DrawNode(state, 0u, drawList); + } + drawList.PopClipRect(); + state.frameResult.drawData.AddDrawList(std::move(drawList)); + + state.frameResult.stats.documentsReady = true; + state.frameResult.stats.statusMessage = "Runtime frame ready"; + state.frameResult.stats.dependencyCount = + static_cast(state.viewDocument.document.dependencies.Size()) + + static_cast(state.themeDocument.document.dependencies.Size()); + state.frameResult.stats.elementCount = state.nodes.size(); + state.frameResult.stats.dirtyRootCount = rebuildResult.dirtyRootIds.size(); + state.frameResult.stats.treeGeneration = rebuildResult.generation; + state.frameResult.stats.accentEnabled = state.accentEnabled; + state.frameResult.stats.lastCommandId = state.lastCommandId; + state.frameResult.stats.drawListCount = state.frameResult.drawData.GetDrawListCount(); + state.frameResult.stats.commandCount = state.frameResult.drawData.GetTotalCommandCount(); + + state.uiContext.GetElementTree().ClearAllDirtyFlags(); + return state.frameResult; +} + +const XCUIDemoFrameResult& XCUIDemoRuntime::GetFrameResult() const { + return m_state->data.frameResult; +} + +bool XCUIDemoRuntime::TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const { + const RuntimeBuildContext& state = m_state->data; + const auto it = state.nodeIndexByKey.find(elementId); + if (it == state.nodeIndexByKey.end()) { + return false; + } + + outRect = state.nodes[it->second].rect; + return true; +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIDemoRuntime.h b/new_editor/src/XCUIBackend/XCUIDemoRuntime.h new file mode 100644 index 00000000..2d923a6d --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIDemoRuntime.h @@ -0,0 +1,78 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace XCEngine { +namespace UI { +struct UIRect; +struct UIPoint; +} // namespace UI +namespace Editor { +namespace XCUIBackend { + +struct XCUIDemoInputState { + UI::UIRect canvasRect = {}; + UI::UIPoint pointerPosition = {}; + bool pointerInside = false; + bool pointerPressed = false; + bool pointerReleased = false; + bool pointerDown = false; + bool windowFocused = false; + bool shortcutPressed = false; + bool wantCaptureMouse = false; + bool wantCaptureKeyboard = false; + bool wantTextInput = false; + std::vector events = {}; +}; + +struct XCUIDemoFrameStats { + bool documentsReady = false; + std::string statusMessage = {}; + std::size_t dependencyCount = 0; + std::size_t elementCount = 0; + std::size_t dirtyRootCount = 0; + std::size_t drawListCount = 0; + std::size_t commandCount = 0; + std::uint64_t treeGeneration = 0; + bool accentEnabled = false; + std::string lastCommandId = {}; + std::string focusedElementId = {}; + std::string hoveredElementId = {}; +}; + +struct XCUIDemoFrameResult { + UI::UIDrawData drawData = {}; + XCUIDemoFrameStats stats = {}; +}; + +class XCUIDemoRuntime { +public: + XCUIDemoRuntime(); + ~XCUIDemoRuntime(); + + XCUIDemoRuntime(XCUIDemoRuntime&& other) noexcept; + XCUIDemoRuntime& operator=(XCUIDemoRuntime&& other) noexcept; + + XCUIDemoRuntime(const XCUIDemoRuntime&) = delete; + XCUIDemoRuntime& operator=(const XCUIDemoRuntime&) = delete; + + bool ReloadDocuments(); + + const XCUIDemoFrameResult& Update(const XCUIDemoInputState& input); + const XCUIDemoFrameResult& GetFrameResult() const; + + bool TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const; + +private: + struct RuntimeState; + std::unique_ptr m_state; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIEditorFontSetup.cpp b/new_editor/src/XCUIBackend/XCUIEditorFontSetup.cpp new file mode 100644 index 00000000..088f82d0 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIEditorFontSetup.cpp @@ -0,0 +1,60 @@ +#include "XCUIBackend/XCUIEditorFontSetup.h" + +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace { + +constexpr float kUiFontSize = 18.0f; +constexpr const char* kPrimaryUiFontPath = "C:/Windows/Fonts/segoeui.ttf"; +constexpr const char* kChineseFallbackFontPath = "C:/Windows/Fonts/msyh.ttc"; + +} // namespace + +bool BuildDefaultXCUIEditorFontAtlas(::ImFontAtlas& atlas, ::ImFont*& outDefaultFont) { + outDefaultFont = nullptr; + atlas.Clear(); + + ImFontConfig baseConfig = {}; + baseConfig.OversampleH = 2; + baseConfig.OversampleV = 1; + baseConfig.PixelSnapH = true; + + outDefaultFont = atlas.AddFontFromFileTTF( + kPrimaryUiFontPath, + kUiFontSize, + &baseConfig, + atlas.GetGlyphRangesDefault()); + + if (outDefaultFont != nullptr) { + ImFontConfig mergeConfig = baseConfig; + mergeConfig.MergeMode = true; + mergeConfig.PixelSnapH = true; + atlas.AddFontFromFileTTF( + kChineseFallbackFontPath, + kUiFontSize, + &mergeConfig, + atlas.GetGlyphRangesChineseSimplifiedCommon()); + } else { + outDefaultFont = atlas.AddFontFromFileTTF( + kChineseFallbackFontPath, + kUiFontSize, + &baseConfig, + atlas.GetGlyphRangesChineseSimplifiedCommon()); + } + + if (outDefaultFont == nullptr) { + ImFontConfig fallbackConfig = baseConfig; + fallbackConfig.SizePixels = kUiFontSize; + outDefaultFont = atlas.AddFontDefault(&fallbackConfig); + } + + return outDefaultFont != nullptr; +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIEditorFontSetup.h b/new_editor/src/XCUIBackend/XCUIEditorFontSetup.h new file mode 100644 index 00000000..33725a16 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIEditorFontSetup.h @@ -0,0 +1,14 @@ +#pragma once + +struct ImFont; +struct ImFontAtlas; + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +bool BuildDefaultXCUIEditorFontAtlas(::ImFontAtlas& atlas, ::ImFont*& outDefaultFont); + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h b/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h new file mode 100644 index 00000000..8abe6eae --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h @@ -0,0 +1,349 @@ +#pragma once + +#include "XCUIBackend/ImGuiTransitionBackend.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +struct ImDrawList; + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +struct XCUIHostedPreviewFrame { + const ::XCEngine::UI::UIDrawData* drawData = nullptr; + ImDrawList* targetDrawList = nullptr; + ::XCEngine::UI::UIRect canvasRect = {}; + ::XCEngine::UI::UISize logicalSize = {}; + const char* debugName = nullptr; + const char* debugSource = nullptr; +}; + +struct XCUIHostedPreviewStats { + bool presented = false; + bool queuedToNativePass = false; + std::size_t submittedDrawListCount = 0; + std::size_t submittedCommandCount = 0; + std::size_t flushedDrawListCount = 0; + std::size_t flushedCommandCount = 0; +}; + +struct XCUIHostedPreviewQueuedFrame { + std::string debugName = {}; + std::string debugSource = {}; + ::XCEngine::UI::UIRect canvasRect = {}; + ::XCEngine::UI::UISize logicalSize = {}; + ::XCEngine::UI::UIDrawData drawData = {}; +}; + +struct XCUIHostedPreviewSurfaceImage { + ImTextureID textureId = {}; + ImVec2 uvMin = ImVec2(0.0f, 0.0f); + ImVec2 uvMax = ImVec2(1.0f, 1.0f); + ::XCEngine::UI::UIRect renderedCanvasRect = {}; + std::uint32_t surfaceWidth = 0; + std::uint32_t surfaceHeight = 0; + + bool IsValid() const { + return textureId != ImTextureID{} && surfaceWidth > 0u && surfaceHeight > 0u; + } +}; + +struct XCUIHostedPreviewDrainStats { + std::size_t queuedFrameCount = 0; + std::size_t queuedDrawListCount = 0; + std::size_t queuedCommandCount = 0; + std::size_t renderedFrameCount = 0; + std::size_t renderedDrawListCount = 0; + std::size_t renderedCommandCount = 0; + std::size_t skippedFrameCount = 0; + std::size_t skippedCommandCount = 0; +}; + +struct XCUIHostedPreviewSurfaceDescriptor { + std::string debugName = {}; + std::string debugSource = {}; + ::XCEngine::UI::UIRect canvasRect = {}; + ::XCEngine::UI::UISize logicalSize = {}; + std::size_t queuedFrameIndex = 0; + std::size_t submittedDrawListCount = 0; + std::size_t submittedCommandCount = 0; + bool queuedThisFrame = false; + XCUIHostedPreviewSurfaceImage image = {}; +}; + +class XCUIHostedPreviewSurfaceRegistry { +public: + void BeginFrame() { + for (XCUIHostedPreviewSurfaceDescriptor& descriptor : m_descriptors) { + descriptor.queuedThisFrame = false; + } + } + + void RecordQueuedFrame( + const XCUIHostedPreviewQueuedFrame& queuedFrame, + std::size_t queuedFrameIndex = 0u) { + if (queuedFrame.debugName.empty()) { + return; + } + + XCUIHostedPreviewSurfaceDescriptor* descriptor = FindMutableDescriptor(queuedFrame.debugName); + if (descriptor == nullptr) { + XCUIHostedPreviewSurfaceDescriptor newDescriptor = {}; + newDescriptor.debugName = queuedFrame.debugName; + m_descriptors.push_back(std::move(newDescriptor)); + descriptor = &m_descriptors.back(); + } + + descriptor->debugSource = queuedFrame.debugSource; + descriptor->canvasRect = queuedFrame.canvasRect; + descriptor->logicalSize = queuedFrame.logicalSize; + descriptor->queuedFrameIndex = queuedFrameIndex; + descriptor->submittedDrawListCount = queuedFrame.drawData.GetDrawListCount(); + descriptor->submittedCommandCount = queuedFrame.drawData.GetTotalCommandCount(); + descriptor->queuedThisFrame = true; + } + + void UpdateSurface( + const std::string& debugName, + ImTextureID textureId, + std::uint32_t surfaceWidth, + std::uint32_t surfaceHeight, + const ::XCEngine::UI::UIRect& renderedCanvasRect) { + if (debugName.empty() || textureId == ImTextureID{} || surfaceWidth == 0u || surfaceHeight == 0u) { + return; + } + + XCUIHostedPreviewSurfaceDescriptor* descriptor = FindMutableDescriptor(debugName); + if (descriptor == nullptr) { + XCUIHostedPreviewSurfaceDescriptor newDescriptor = {}; + newDescriptor.debugName = debugName; + m_descriptors.push_back(std::move(newDescriptor)); + descriptor = &m_descriptors.back(); + } + + descriptor->image.textureId = textureId; + descriptor->image.surfaceWidth = surfaceWidth; + descriptor->image.surfaceHeight = surfaceHeight; + descriptor->image.renderedCanvasRect = renderedCanvasRect; + descriptor->image.uvMin = ImVec2( + renderedCanvasRect.x / static_cast(surfaceWidth), + renderedCanvasRect.y / static_cast(surfaceHeight)); + descriptor->image.uvMax = ImVec2( + (renderedCanvasRect.x + renderedCanvasRect.width) / static_cast(surfaceWidth), + (renderedCanvasRect.y + renderedCanvasRect.height) / static_cast(surfaceHeight)); + } + + bool TryGetSurfaceDescriptor( + const char* debugName, + XCUIHostedPreviewSurfaceDescriptor& outDescriptor) const { + outDescriptor = {}; + if (debugName == nullptr || debugName[0] == '\0') { + return false; + } + + for (const XCUIHostedPreviewSurfaceDescriptor& descriptor : m_descriptors) { + if (descriptor.debugName == debugName) { + outDescriptor = descriptor; + return true; + } + } + + return false; + } + + bool TryGetSurfaceImage(const char* debugName, XCUIHostedPreviewSurfaceImage& outImage) const { + outImage = {}; + XCUIHostedPreviewSurfaceDescriptor descriptor = {}; + if (!TryGetSurfaceDescriptor(debugName, descriptor)) { + return false; + } + + outImage = descriptor.image; + return outImage.IsValid(); + } + + const std::vector& GetDescriptors() const { + return m_descriptors; + } + +private: + XCUIHostedPreviewSurfaceDescriptor* FindMutableDescriptor(const std::string& debugName) { + for (XCUIHostedPreviewSurfaceDescriptor& descriptor : m_descriptors) { + if (descriptor.debugName == debugName) { + return &descriptor; + } + } + + return nullptr; + } + + std::vector m_descriptors = {}; +}; + +class XCUIHostedPreviewQueue { +public: + void BeginFrame() { + m_queuedFrames.clear(); + } + + bool Submit(const XCUIHostedPreviewFrame& frame, XCUIHostedPreviewStats* outStats = nullptr) { + XCUIHostedPreviewStats stats = {}; + if (frame.drawData == nullptr) { + if (outStats != nullptr) { + *outStats = stats; + } + return false; + } + + XCUIHostedPreviewQueuedFrame queuedFrame = {}; + if (frame.debugName != nullptr) { + queuedFrame.debugName = frame.debugName; + } + if (frame.debugSource != nullptr) { + queuedFrame.debugSource = frame.debugSource; + } + queuedFrame.canvasRect = frame.canvasRect; + queuedFrame.logicalSize = frame.logicalSize.width > 0.0f && frame.logicalSize.height > 0.0f + ? frame.logicalSize + : ::XCEngine::UI::UISize(frame.canvasRect.width, frame.canvasRect.height); + queuedFrame.drawData = *frame.drawData; + + stats.presented = true; + stats.queuedToNativePass = true; + stats.submittedDrawListCount = queuedFrame.drawData.GetDrawListCount(); + stats.submittedCommandCount = queuedFrame.drawData.GetTotalCommandCount(); + m_queuedFrames.push_back(std::move(queuedFrame)); + + if (outStats != nullptr) { + *outStats = stats; + } + return true; + } + + const std::vector& GetQueuedFrames() const { + return m_queuedFrames; + } + + void SetLastDrainStats(const XCUIHostedPreviewDrainStats& stats) { + m_lastDrainStats = stats; + } + + const XCUIHostedPreviewDrainStats& GetLastDrainStats() const { + return m_lastDrainStats; + } + +private: + std::vector m_queuedFrames = {}; + XCUIHostedPreviewDrainStats m_lastDrainStats = {}; +}; + +class IXCUIHostedPreviewPresenter { +public: + virtual ~IXCUIHostedPreviewPresenter() = default; + + virtual bool Present(const XCUIHostedPreviewFrame& frame) = 0; + virtual const XCUIHostedPreviewStats& GetLastStats() const = 0; + virtual bool IsNativeQueued() const { return false; } + virtual bool TryGetSurfaceImage( + const char* debugName, + XCUIHostedPreviewSurfaceImage& outImage) const { + outImage = {}; + return false; + } + virtual bool TryGetSurfaceDescriptor( + const char* debugName, + XCUIHostedPreviewSurfaceDescriptor& outDescriptor) const { + outDescriptor = {}; + return false; + } +}; + +class ImGuiXCUIHostedPreviewPresenter final : public IXCUIHostedPreviewPresenter { +public: + bool Present(const XCUIHostedPreviewFrame& frame) override { + m_lastStats = {}; + if (frame.drawData == nullptr) { + return false; + } + + m_backend.BeginFrame(); + m_backend.Submit(*frame.drawData); + m_lastStats.submittedDrawListCount = m_backend.GetPendingDrawListCount(); + m_lastStats.submittedCommandCount = m_backend.GetPendingCommandCount(); + m_lastStats.presented = m_backend.EndFrame(frame.targetDrawList); + m_lastStats.flushedDrawListCount = m_backend.GetLastFlushedDrawListCount(); + m_lastStats.flushedCommandCount = m_backend.GetLastFlushedCommandCount(); + return m_lastStats.presented; + } + + const XCUIHostedPreviewStats& GetLastStats() const override { + return m_lastStats; + } + +private: + ImGuiTransitionBackend m_backend = {}; + XCUIHostedPreviewStats m_lastStats = {}; +}; + +class QueuedNativeXCUIHostedPreviewPresenter final : public IXCUIHostedPreviewPresenter { +public: + QueuedNativeXCUIHostedPreviewPresenter( + XCUIHostedPreviewQueue& queue, + XCUIHostedPreviewSurfaceRegistry& surfaceRegistry) + : m_queue(queue) + , m_surfaceRegistry(surfaceRegistry) { + } + + bool Present(const XCUIHostedPreviewFrame& frame) override { + m_lastStats = {}; + return m_queue.Submit(frame, &m_lastStats); + } + + const XCUIHostedPreviewStats& GetLastStats() const override { + return m_lastStats; + } + + bool IsNativeQueued() const override { + return true; + } + + bool TryGetSurfaceImage( + const char* debugName, + XCUIHostedPreviewSurfaceImage& outImage) const override { + return m_surfaceRegistry.TryGetSurfaceImage(debugName, outImage); + } + + bool TryGetSurfaceDescriptor( + const char* debugName, + XCUIHostedPreviewSurfaceDescriptor& outDescriptor) const override { + return m_surfaceRegistry.TryGetSurfaceDescriptor(debugName, outDescriptor); + } + +private: + XCUIHostedPreviewQueue& m_queue; + XCUIHostedPreviewSurfaceRegistry& m_surfaceRegistry; + XCUIHostedPreviewStats m_lastStats = {}; +}; + +inline std::unique_ptr CreateImGuiXCUIHostedPreviewPresenter() { + return std::make_unique(); +} + +inline std::unique_ptr CreateQueuedNativeXCUIHostedPreviewPresenter( + XCUIHostedPreviewQueue& queue, + XCUIHostedPreviewSurfaceRegistry& surfaceRegistry) { + return std::make_unique(queue, surfaceRegistry); +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIInputBridge.cpp b/new_editor/src/XCUIBackend/XCUIInputBridge.cpp new file mode 100644 index 00000000..9168b78e --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIInputBridge.cpp @@ -0,0 +1,596 @@ +#include "XCUIBackend/XCUIInputBridge.h" + +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIPoint; + +UIPoint Subtract(const UIPoint& lhs, const UIPoint& rhs) { + return UIPoint(lhs.x - rhs.x, lhs.y - rhs.y); +} + +UIPointerButton ToPointerButton(std::size_t index) { + switch (index) { + case 0u: + return UIPointerButton::Left; + case 1u: + return UIPointerButton::Right; + case 2u: + return UIPointerButton::Middle; + case 3u: + return UIPointerButton::X1; + case 4u: + return UIPointerButton::X2; + default: + return UIPointerButton::None; + } +} + +void AppendEvent(std::vector& events, const UIInputEvent& event) { + events.push_back(event); +} + +UIInputEvent MakeBaseEvent( + UIInputEventType type, + const XCUIInputBridgeFrameSnapshot& snapshot) { + UIInputEvent event = {}; + event.type = type; + event.position = snapshot.pointerPosition; + event.timestampNanoseconds = snapshot.timestampNanoseconds; + event.modifiers = snapshot.modifiers; + return event; +} + +void AppendUniqueKeyCode(std::vector& keyCodes, std::int32_t keyCode) { + if (keyCode == 0) { + return; + } + + const auto it = std::find(keyCodes.begin(), keyCodes.end(), keyCode); + if (it == keyCodes.end()) { + keyCodes.push_back(keyCode); + } +} + +bool IsPointerPositionValid(const ImVec2& position) { + return std::isfinite(position.x) && + std::isfinite(position.y) && + position.x > -std::numeric_limits::max() * 0.5f && + position.y > -std::numeric_limits::max() * 0.5f; +} + +std::uint64_t GetTimestampNanoseconds() { + return static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); +} + +UIPoint MakeClientPoint(LPARAM lParam) { + return UIPoint( + static_cast(static_cast(LOWORD(static_cast(lParam)))), + static_cast(static_cast(HIWORD(static_cast(lParam))))); +} + +std::int32_t TranslateVirtualKeyToXCUIKeyCode(WPARAM wParam, LPARAM lParam) { + switch (static_cast(wParam)) { + case 'A': return static_cast(KeyCode::A); + case 'B': return static_cast(KeyCode::B); + case 'C': return static_cast(KeyCode::C); + case 'D': return static_cast(KeyCode::D); + case 'E': return static_cast(KeyCode::E); + case 'F': return static_cast(KeyCode::F); + case 'G': return static_cast(KeyCode::G); + case 'H': return static_cast(KeyCode::H); + case 'I': return static_cast(KeyCode::I); + case 'J': return static_cast(KeyCode::J); + case 'K': return static_cast(KeyCode::K); + case 'L': return static_cast(KeyCode::L); + case 'M': return static_cast(KeyCode::M); + case 'N': return static_cast(KeyCode::N); + case 'O': return static_cast(KeyCode::O); + case 'P': return static_cast(KeyCode::P); + case 'Q': return static_cast(KeyCode::Q); + case 'R': return static_cast(KeyCode::R); + case 'S': return static_cast(KeyCode::S); + case 'T': return static_cast(KeyCode::T); + case 'U': return static_cast(KeyCode::U); + case 'V': return static_cast(KeyCode::V); + case 'W': return static_cast(KeyCode::W); + case 'X': return static_cast(KeyCode::X); + case 'Y': return static_cast(KeyCode::Y); + case 'Z': return static_cast(KeyCode::Z); + case '0': return static_cast(KeyCode::Zero); + case '1': return static_cast(KeyCode::One); + case '2': return static_cast(KeyCode::Two); + case '3': return static_cast(KeyCode::Three); + case '4': return static_cast(KeyCode::Four); + case '5': return static_cast(KeyCode::Five); + case '6': return static_cast(KeyCode::Six); + case '7': return static_cast(KeyCode::Seven); + case '8': return static_cast(KeyCode::Eight); + case '9': return static_cast(KeyCode::Nine); + case VK_SPACE: return static_cast(KeyCode::Space); + case VK_TAB: return static_cast(KeyCode::Tab); + case VK_RETURN: return static_cast(KeyCode::Enter); + case VK_ESCAPE: return static_cast(KeyCode::Escape); + case VK_SHIFT: { + const UINT scanCode = (static_cast(lParam) >> 16) & 0xFFu; + const UINT leftShiftScanCode = MapVirtualKeyW(VK_LSHIFT, MAPVK_VK_TO_VSC); + return static_cast( + scanCode == leftShiftScanCode ? KeyCode::LeftShift : KeyCode::RightShift); + } + case VK_CONTROL: + return static_cast( + (static_cast(lParam) & 0x01000000u) != 0u ? KeyCode::RightCtrl : KeyCode::LeftCtrl); + case VK_MENU: + return static_cast( + (static_cast(lParam) & 0x01000000u) != 0u ? KeyCode::RightAlt : KeyCode::LeftAlt); + case VK_UP: return static_cast(KeyCode::Up); + case VK_DOWN: return static_cast(KeyCode::Down); + case VK_LEFT: return static_cast(KeyCode::Left); + case VK_RIGHT: return static_cast(KeyCode::Right); + case VK_HOME: return static_cast(KeyCode::Home); + case VK_END: return static_cast(KeyCode::End); + case VK_PRIOR: return static_cast(KeyCode::PageUp); + case VK_NEXT: return static_cast(KeyCode::PageDown); + case VK_DELETE: return static_cast(KeyCode::Delete); + case VK_BACK: return static_cast(KeyCode::Backspace); + case VK_F1: return static_cast(KeyCode::F1); + case VK_F2: return static_cast(KeyCode::F2); + case VK_F3: return static_cast(KeyCode::F3); + case VK_F4: return static_cast(KeyCode::F4); + case VK_F5: return static_cast(KeyCode::F5); + case VK_F6: return static_cast(KeyCode::F6); + case VK_F7: return static_cast(KeyCode::F7); + case VK_F8: return static_cast(KeyCode::F8); + case VK_F9: return static_cast(KeyCode::F9); + case VK_F10: return static_cast(KeyCode::F10); + case VK_F11: return static_cast(KeyCode::F11); + case VK_F12: return static_cast(KeyCode::F12); + case VK_OEM_MINUS: return static_cast(KeyCode::Minus); + case VK_OEM_PLUS: return static_cast(KeyCode::Equals); + case VK_OEM_4: return static_cast(KeyCode::BracketLeft); + case VK_OEM_6: return static_cast(KeyCode::BracketRight); + case VK_OEM_1: return static_cast(KeyCode::Semicolon); + case VK_OEM_7: return static_cast(KeyCode::Quote); + case VK_OEM_COMMA: return static_cast(KeyCode::Comma); + case VK_OEM_PERIOD: return static_cast(KeyCode::Period); + case VK_OEM_2: return static_cast(KeyCode::Slash); + case VK_OEM_5: return static_cast(KeyCode::Backslash); + case VK_OEM_3: return static_cast(KeyCode::Backtick); + default: + return static_cast(KeyCode::None); + } +} + +} // namespace + +const XCUIInputBridgeKeyState* XCUIInputBridgeFrameSnapshot::FindKeyState(std::int32_t keyCode) const { + for (const XCUIInputBridgeKeyState& keyState : keys) { + if (keyState.keyCode == keyCode) { + return &keyState; + } + } + + return nullptr; +} + +bool XCUIInputBridgeFrameSnapshot::IsKeyDown(std::int32_t keyCode) const { + const XCUIInputBridgeKeyState* keyState = FindKeyState(keyCode); + return keyState != nullptr && keyState->down; +} + +bool XCUIInputBridgeFrameDelta::HasEvents() const { + return !events.empty(); +} + +bool XCUIInputBridgeFrameDelta::HasPointerActivity() const { + return pointer.moved || + pointer.entered || + pointer.left || + pointer.wheelDelta.x != 0.0f || + pointer.wheelDelta.y != 0.0f || + std::any_of(pointer.pressed.begin(), pointer.pressed.end(), [](bool value) { return value; }) || + std::any_of(pointer.released.begin(), pointer.released.end(), [](bool value) { return value; }); +} + +bool XCUIInputBridgeFrameDelta::HasKeyboardActivity() const { + return !keyboard.pressedKeys.empty() || + !keyboard.releasedKeys.empty() || + !keyboard.repeatedKeys.empty() || + !keyboard.characters.empty(); +} + +bool XCUIInputBridgeFrameDelta::HasEventType(UI::UIInputEventType type) const { + return std::any_of( + events.begin(), + events.end(), + [type](const UIInputEvent& event) { + return event.type == type; + }); +} + +void XCUIInputBridge::Reset() { + m_hasBaseline = false; + m_baseline = {}; +} + +void XCUIInputBridge::Prime(const XCUIInputBridgeFrameSnapshot& snapshot) { + m_baseline = snapshot; + m_hasBaseline = true; +} + +XCUIInputBridgeFrameDelta XCUIInputBridge::Translate(const XCUIInputBridgeFrameSnapshot& current) { + const XCUIInputBridgeFrameSnapshot previous = m_hasBaseline ? m_baseline : XCUIInputBridgeFrameSnapshot(); + XCUIInputBridgeFrameDelta delta = Translate(previous, current); + m_baseline = current; + m_hasBaseline = true; + return delta; +} + +XCUIInputBridgeFrameDelta XCUIInputBridge::Translate( + const XCUIInputBridgeFrameSnapshot& previous, + const XCUIInputBridgeFrameSnapshot& current) { + XCUIInputBridgeFrameDelta delta = {}; + delta.state = current; + delta.pointer.wheelDelta = current.wheelDelta; + delta.keyboard.characters = current.characters; + + delta.focusGained = current.windowFocused && !previous.windowFocused; + delta.focusLost = previous.windowFocused && !current.windowFocused; + + if (delta.focusGained) { + AppendEvent(delta.events, MakeBaseEvent(UIInputEventType::FocusGained, current)); + } + if (delta.focusLost) { + AppendEvent(delta.events, MakeBaseEvent(UIInputEventType::FocusLost, current)); + } + + delta.pointer.entered = current.pointerInside && !previous.pointerInside; + delta.pointer.left = previous.pointerInside && !current.pointerInside; + if (previous.pointerInside && current.pointerInside) { + delta.pointer.delta = Subtract(current.pointerPosition, previous.pointerPosition); + } + + const bool pointerMovedInside = + current.pointerInside && + (delta.pointer.entered || + previous.pointerPosition.x != current.pointerPosition.x || + previous.pointerPosition.y != current.pointerPosition.y); + delta.pointer.moved = pointerMovedInside; + + if (delta.pointer.entered) { + AppendEvent(delta.events, MakeBaseEvent(UIInputEventType::PointerEnter, current)); + } + if (delta.pointer.moved) { + UIInputEvent event = MakeBaseEvent(UIInputEventType::PointerMove, current); + event.delta = delta.pointer.delta; + AppendEvent(delta.events, event); + } + + for (std::size_t index = 0; index < XCUIInputBridgeFrameSnapshot::PointerButtonCount; ++index) { + const bool wasDown = previous.pointerButtonsDown[index]; + const bool isDown = current.pointerButtonsDown[index]; + if (isDown == wasDown) { + continue; + } + + UIInputEvent event = MakeBaseEvent( + isDown ? UIInputEventType::PointerButtonDown : UIInputEventType::PointerButtonUp, + current); + event.pointerButton = ToPointerButton(index); + + if (isDown) { + delta.pointer.pressed[index] = true; + } else { + delta.pointer.released[index] = true; + } + + AppendEvent(delta.events, event); + } + + if (current.wheelDelta.x != 0.0f || current.wheelDelta.y != 0.0f) { + UIInputEvent event = MakeBaseEvent(UIInputEventType::PointerWheel, current); + event.delta = current.wheelDelta; + event.wheelDelta = current.wheelDelta.y != 0.0f ? current.wheelDelta.y : current.wheelDelta.x; + AppendEvent(delta.events, event); + } + + if (delta.pointer.left) { + UIInputEvent event = MakeBaseEvent(UIInputEventType::PointerLeave, current); + if (previous.pointerInside) { + event.delta = Subtract(current.pointerPosition, previous.pointerPosition); + } + AppendEvent(delta.events, event); + } + + std::vector keyCodes = {}; + keyCodes.reserve(previous.keys.size() + current.keys.size()); + for (const XCUIInputBridgeKeyState& keyState : previous.keys) { + AppendUniqueKeyCode(keyCodes, keyState.keyCode); + } + for (const XCUIInputBridgeKeyState& keyState : current.keys) { + AppendUniqueKeyCode(keyCodes, keyState.keyCode); + } + std::sort(keyCodes.begin(), keyCodes.end()); + + for (std::int32_t keyCode : keyCodes) { + const XCUIInputBridgeKeyState* previousKeyState = previous.FindKeyState(keyCode); + const XCUIInputBridgeKeyState* currentKeyState = current.FindKeyState(keyCode); + const bool wasDown = previousKeyState != nullptr && previousKeyState->down; + const bool isDown = currentKeyState != nullptr && currentKeyState->down; + + if (isDown && !wasDown) { + UIInputEvent event = MakeBaseEvent(UIInputEventType::KeyDown, current); + event.keyCode = keyCode; + delta.keyboard.pressedKeys.push_back(keyCode); + AppendEvent(delta.events, event); + continue; + } + + if (!isDown && wasDown) { + UIInputEvent event = MakeBaseEvent(UIInputEventType::KeyUp, current); + event.keyCode = keyCode; + delta.keyboard.releasedKeys.push_back(keyCode); + AppendEvent(delta.events, event); + continue; + } + + if (isDown && wasDown && currentKeyState != nullptr && currentKeyState->repeat) { + UIInputEvent event = MakeBaseEvent(UIInputEventType::KeyDown, current); + event.keyCode = keyCode; + event.repeat = true; + delta.keyboard.repeatedKeys.push_back(keyCode); + AppendEvent(delta.events, event); + } + } + + for (std::uint32_t character : current.characters) { + if (character == 0u) { + continue; + } + + UIInputEvent event = MakeBaseEvent(UIInputEventType::Character, current); + event.character = character; + AppendEvent(delta.events, event); + } + + return delta; +} + +void XCUIWin32InputSource::Reset() { + m_pointerPosition = {}; + m_pointerInside = false; + m_windowFocused = false; + m_trackingMouseLeave = false; + m_pointerButtonsDown.fill(false); + m_wheelDelta = {}; + m_modifiers = {}; + m_keyStates.clear(); + m_characters.clear(); +} + +void XCUIWin32InputSource::HandleWindowMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + switch (message) { + case WM_SETFOCUS: + m_windowFocused = true; + UpdateModifierState(); + return; + case WM_KILLFOCUS: + m_windowFocused = false; + m_pointerInside = false; + m_trackingMouseLeave = false; + m_pointerButtonsDown.fill(false); + m_keyStates.clear(); + UpdateModifierState(); + return; + case WM_MOUSEMOVE: { + m_pointerPosition = MakeClientPoint(lParam); + UpdatePointerInside(hwnd, m_pointerPosition.x, m_pointerPosition.y, true); + UpdateModifierState(); + if (hwnd != nullptr && !m_trackingMouseLeave) { + TRACKMOUSEEVENT trackMouseEvent = {}; + trackMouseEvent.cbSize = sizeof(trackMouseEvent); + trackMouseEvent.dwFlags = TME_LEAVE; + trackMouseEvent.hwndTrack = hwnd; + m_trackingMouseLeave = TrackMouseEvent(&trackMouseEvent) == TRUE; + } + return; + } + case WM_MOUSELEAVE: + m_trackingMouseLeave = false; + m_pointerInside = false; + return; + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: { + m_pointerPosition = MakeClientPoint(lParam); + UpdatePointerInside(hwnd, m_pointerPosition.x, m_pointerPosition.y, true); + UpdateModifierState(); + switch (message) { + case WM_LBUTTONDOWN: SetPointerButtonDown(0u, true); return; + case WM_LBUTTONUP: SetPointerButtonDown(0u, false); return; + case WM_RBUTTONDOWN: SetPointerButtonDown(1u, true); return; + case WM_RBUTTONUP: SetPointerButtonDown(1u, false); return; + case WM_MBUTTONDOWN: SetPointerButtonDown(2u, true); return; + case WM_MBUTTONUP: SetPointerButtonDown(2u, false); return; + case WM_XBUTTONDOWN: + SetPointerButtonDown(HIWORD(wParam) == XBUTTON1 ? 3u : 4u, true); + return; + case WM_XBUTTONUP: + SetPointerButtonDown(HIWORD(wParam) == XBUTTON1 ? 3u : 4u, false); + return; + default: + return; + } + } + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: { + POINT screenPoint = { + static_cast(static_cast(LOWORD(static_cast(lParam)))), + static_cast(static_cast(HIWORD(static_cast(lParam)))) + }; + if (hwnd != nullptr && ScreenToClient(hwnd, &screenPoint)) { + m_pointerPosition = UIPoint(static_cast(screenPoint.x), static_cast(screenPoint.y)); + UpdatePointerInside(hwnd, m_pointerPosition.x, m_pointerPosition.y, false); + } + const float wheelStep = static_cast(GET_WHEEL_DELTA_WPARAM(wParam)) / static_cast(WHEEL_DELTA); + if (message == WM_MOUSEHWHEEL) { + m_wheelDelta.x += wheelStep; + } else { + m_wheelDelta.y += wheelStep; + } + UpdateModifierState(); + return; + } + case WM_KEYDOWN: + case WM_SYSKEYDOWN: { + UpdateModifierState(); + const std::int32_t keyCode = TranslateVirtualKeyToXCUIKeyCode(wParam, lParam); + const bool repeat = (static_cast(lParam) & 0x40000000u) != 0u; + SetKeyDown(keyCode, true, repeat); + UpdateModifierState(); + return; + } + case WM_KEYUP: + case WM_SYSKEYUP: + SetKeyDown(TranslateVirtualKeyToXCUIKeyCode(wParam, lParam), false, false); + UpdateModifierState(); + return; + case WM_CHAR: + case WM_SYSCHAR: + if (wParam != 0u) { + m_characters.push_back(static_cast(wParam)); + } + return; + default: + return; + } +} + +void XCUIWin32InputSource::ClearFrameTransients() { + m_wheelDelta = {}; + m_characters.clear(); + for (XCUIInputBridgeKeyState& keyState : m_keyStates) { + keyState.repeat = false; + } +} + +XCUIInputBridgeFrameSnapshot XCUIWin32InputSource::CaptureSnapshot( + const XCUIInputBridgeCaptureOptions& options) const { + XCUIInputBridgeFrameSnapshot snapshot = {}; + snapshot.pointerPosition = UIPoint( + m_pointerPosition.x - options.pointerOffset.x, + m_pointerPosition.y - options.pointerOffset.y); + snapshot.pointerInside = options.hasPointerInsideOverride ? options.pointerInsideOverride : m_pointerInside; + snapshot.pointerButtonsDown = m_pointerButtonsDown; + snapshot.wheelDelta = m_wheelDelta; + snapshot.modifiers = m_modifiers; + snapshot.windowFocused = options.windowFocused && m_windowFocused; + snapshot.wantCaptureMouse = false; + snapshot.wantCaptureKeyboard = false; + snapshot.wantTextInput = false; + snapshot.timestampNanoseconds = + options.timestampNanoseconds != 0u ? options.timestampNanoseconds : GetTimestampNanoseconds(); + snapshot.keys = m_keyStates; + snapshot.characters = m_characters; + return snapshot; +} + +void XCUIWin32InputSource::UpdateModifierState() { + m_modifiers.shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0; + m_modifiers.control = (GetKeyState(VK_CONTROL) & 0x8000) != 0; + m_modifiers.alt = (GetKeyState(VK_MENU) & 0x8000) != 0; + m_modifiers.super = + (GetKeyState(VK_LWIN) & 0x8000) != 0 || + (GetKeyState(VK_RWIN) & 0x8000) != 0; +} + +void XCUIWin32InputSource::UpdatePointerInside( + HWND hwnd, + float x, + float y, + bool assumeInsideIfUnknown) { + if (hwnd == nullptr) { + m_pointerInside = assumeInsideIfUnknown; + return; + } + + RECT clientRect = {}; + if (!GetClientRect(hwnd, &clientRect)) { + m_pointerInside = assumeInsideIfUnknown; + return; + } + + m_pointerInside = + x >= static_cast(clientRect.left) && + y >= static_cast(clientRect.top) && + x < static_cast(clientRect.right) && + y < static_cast(clientRect.bottom); +} + +void XCUIWin32InputSource::SetPointerButtonDown(std::size_t index, bool down) { + if (index < m_pointerButtonsDown.size()) { + m_pointerButtonsDown[index] = down; + } +} + +void XCUIWin32InputSource::SetKeyDown(std::int32_t keyCode, bool down, bool repeat) { + if (keyCode == 0) { + return; + } + + const auto it = std::find_if( + m_keyStates.begin(), + m_keyStates.end(), + [keyCode](const XCUIInputBridgeKeyState& state) { + return state.keyCode == keyCode; + }); + + if (!down) { + if (it != m_keyStates.end()) { + m_keyStates.erase(it); + } + return; + } + + if (it != m_keyStates.end()) { + it->down = true; + it->repeat = it->repeat || repeat; + return; + } + + XCUIInputBridgeKeyState state = {}; + state.keyCode = keyCode; + state.down = true; + state.repeat = repeat; + m_keyStates.push_back(state); + std::sort( + m_keyStates.begin(), + m_keyStates.end(), + [](const XCUIInputBridgeKeyState& lhs, const XCUIInputBridgeKeyState& rhs) { + return lhs.keyCode < rhs.keyCode; + }); +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIInputBridge.h b/new_editor/src/XCUIBackend/XCUIInputBridge.h new file mode 100644 index 00000000..21fb300c --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIInputBridge.h @@ -0,0 +1,152 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +struct XCUIInputBridgeKeyState { + std::int32_t keyCode = 0; + bool down = false; + bool repeat = false; +}; + +struct XCUIInputBridgeFrameSnapshot { + static constexpr std::size_t PointerButtonCount = 5u; + + UI::UIPoint pointerPosition = {}; + bool pointerInside = false; + std::array pointerButtonsDown = {}; + UI::UIPoint wheelDelta = {}; + UI::UIInputModifiers modifiers = {}; + bool windowFocused = false; + bool wantCaptureMouse = false; + bool wantCaptureKeyboard = false; + bool wantTextInput = false; + std::uint64_t timestampNanoseconds = 0; + std::vector keys = {}; + std::vector characters = {}; + + const XCUIInputBridgeKeyState* FindKeyState(std::int32_t keyCode) const; + bool IsKeyDown(std::int32_t keyCode) const; +}; + +struct XCUIInputBridgeCaptureOptions { + UI::UIPoint pointerOffset = {}; + bool hasPointerInsideOverride = false; + bool pointerInsideOverride = false; + bool windowFocused = true; + std::uint64_t timestampNanoseconds = 0; +}; + +struct XCUIInputBridgePointerDelta { + UI::UIPoint delta = {}; + UI::UIPoint wheelDelta = {}; + std::array pressed = {}; + std::array released = {}; + bool moved = false; + bool entered = false; + bool left = false; +}; + +struct XCUIInputBridgeKeyboardDelta { + std::vector pressedKeys = {}; + std::vector releasedKeys = {}; + std::vector repeatedKeys = {}; + std::vector characters = {}; +}; + +struct XCUIInputBridgeFrameDelta { + XCUIInputBridgeFrameSnapshot state = {}; + XCUIInputBridgePointerDelta pointer = {}; + XCUIInputBridgeKeyboardDelta keyboard = {}; + std::vector events = {}; + bool focusGained = false; + bool focusLost = false; + + bool HasEvents() const; + bool HasPointerActivity() const; + bool HasKeyboardActivity() const; + bool HasEventType(UI::UIInputEventType type) const; +}; + +class XCUIInputBridge { +public: + void Reset(); + void Prime(const XCUIInputBridgeFrameSnapshot& snapshot); + + bool HasBaseline() const { + return m_hasBaseline; + } + + const XCUIInputBridgeFrameSnapshot& GetBaseline() const { + return m_baseline; + } + + XCUIInputBridgeFrameDelta Translate(const XCUIInputBridgeFrameSnapshot& current); + + static XCUIInputBridgeFrameDelta Translate( + const XCUIInputBridgeFrameSnapshot& previous, + const XCUIInputBridgeFrameSnapshot& current); + +private: + bool m_hasBaseline = false; + XCUIInputBridgeFrameSnapshot m_baseline = {}; +}; + +class XCUIWin32InputSource { +public: + void Reset(); + void HandleWindowMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); + void ClearFrameTransients(); + + XCUIInputBridgeFrameSnapshot CaptureSnapshot( + const XCUIInputBridgeCaptureOptions& options = XCUIInputBridgeCaptureOptions()) const; + + const UI::UIPoint& GetPointerPosition() const { + return m_pointerPosition; + } + + bool IsPointerInsideWindow() const { + return m_pointerInside; + } + + bool IsWindowFocused() const { + return m_windowFocused; + } + +private: + void UpdateModifierState(); + void UpdatePointerInside(HWND hwnd, float x, float y, bool assumeInsideIfUnknown); + void SetPointerButtonDown(std::size_t index, bool down); + void SetKeyDown(std::int32_t keyCode, bool down, bool repeat); + + UI::UIPoint m_pointerPosition = {}; + bool m_pointerInside = false; + bool m_windowFocused = false; + bool m_trackingMouseLeave = false; + std::array m_pointerButtonsDown = {}; + UI::UIPoint m_wheelDelta = {}; + UI::UIInputModifiers m_modifiers = {}; + std::vector m_keyStates = {}; + std::vector m_characters = {}; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp new file mode 100644 index 00000000..555827f8 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp @@ -0,0 +1,916 @@ +#include "XCUILayoutLabRuntime.h" +#include "XCUIAssetDocumentSource.h" +#include "XCUIRHICommandSupport.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace fs = std::filesystem; + +namespace { + +using XCEngine::Containers::String; +using XCEngine::Math::Color; +using XCEngine::Resources::CompileUIDocument; +using XCEngine::Resources::UIDocumentAttribute; +using XCEngine::Resources::UIDocumentCompileRequest; +using XCEngine::Resources::UIDocumentCompileResult; +using XCEngine::Resources::UIDocumentKind; +using XCEngine::Resources::UIDocumentModel; +using XCEngine::Resources::UIDocumentNode; +using XCEngine::Resources::ResourceManager; +using XCEngine::UI::UIColor; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; +namespace Style = XCEngine::UI::Style; + +constexpr std::size_t kInvalidIndex = static_cast(-1); +constexpr char kViewRelativePath[] = "new_editor/resources/xcui_layout_lab_view.xcui"; +constexpr char kThemeRelativePath[] = "new_editor/resources/xcui_layout_lab_theme.xctheme"; + +struct LayoutNode { + std::string id = {}; + std::string tagName = {}; + std::string title = {}; + std::string subtitle = {}; + std::string tone = {}; + std::string widthAttr = {}; + std::string heightAttr = {}; + std::string xAttr = {}; + std::string yAttr = {}; + std::string gapAttr = {}; + std::string paddingAttr = {}; + std::string scrollYAttr = {}; + std::size_t parentIndex = kInvalidIndex; + std::vector children = {}; + UIRect rect = {}; + int depth = 0; +}; + +struct RuntimeBuildContext { + UIDocumentCompileResult viewDocument = {}; + UIDocumentCompileResult themeDocument = {}; + Style::UITheme theme = {}; + XCUILayoutLabFrameResult frameResult = {}; + std::vector nodes = {}; + std::unordered_map nodeIndexById = {}; + std::unordered_map rectsById = {}; + bool documentsReady = false; + std::string statusMessage = {}; + XCUIAssetDocumentSource documentSource = XCUIAssetDocumentSource( + XCUIAssetDocumentSource::MakeLayoutLabPathSet()); + fs::path repoRoot = {}; + fs::path viewPath = {}; + fs::path themePath = {}; + fs::file_time_type viewWriteTime = {}; + fs::file_time_type themeWriteTime = {}; +}; + +String ToContainersString(const std::string& value) { + return String(value.c_str()); +} + +std::string ToStdString(const String& value) { + return std::string(value.CStr()); +} + +const UIDocumentAttribute* FindAttribute(const UIDocumentNode& node, const char* name) { + for (const UIDocumentAttribute& attribute : node.attributes) { + if (std::string(attribute.name.CStr()) == name) { + return &attribute; + } + } + + return nullptr; +} + +std::string GetAttributeValue( + const UIDocumentNode& node, + const char* name, + const std::string& fallback = {}) { + const UIDocumentAttribute* attribute = FindAttribute(node, name); + return attribute != nullptr + ? std::string(attribute->value.CStr()) + : fallback; +} + +bool TryParseFloat(const std::string& text, float& outValue) { + if (text.empty()) { + return false; + } + + char* end = nullptr; + const float parsed = std::strtof(text.c_str(), &end); + if (end == text.c_str() || (end != nullptr && *end != '\0')) { + return false; + } + + outValue = parsed; + return true; +} + +bool TryParseHexColor(const std::string& text, Color& outColor) { + if ((text.size() != 7u && text.size() != 9u) || text[0] != '#') { + return false; + } + + auto parseChannel = [&](std::size_t offset) -> int { + return std::stoi(text.substr(offset, 2), nullptr, 16); + }; + + try { + const int r = parseChannel(1); + const int g = parseChannel(3); + const int b = parseChannel(5); + const int a = text.size() == 9u ? parseChannel(7) : 255; + outColor = Color( + static_cast(r) / 255.0f, + static_cast(g) / 255.0f, + static_cast(b) / 255.0f, + static_cast(a) / 255.0f); + return true; + } catch (...) { + return false; + } +} + +Style::UIStyleValue ParseThemeTokenValue( + const std::string& typeName, + const std::string& rawValue, + bool& outSucceeded) { + outSucceeded = true; + + if (typeName == "float") { + float value = 0.0f; + outSucceeded = TryParseFloat(rawValue, value); + return Style::UIStyleValue(value); + } + + if (typeName == "color") { + Color color = {}; + outSucceeded = TryParseHexColor(rawValue, color); + return Style::UIStyleValue(color); + } + + outSucceeded = false; + return Style::UIStyleValue(); +} + +Style::UITheme BuildThemeFromDocument(const UIDocumentModel& document, std::string& outError) { + Style::UIThemeDefinition definition = {}; + definition.name = document.displayName.Empty() + ? "Layout Lab Theme" + : ToStdString(document.displayName); + + for (const UIDocumentNode& child : document.rootNode.children) { + if (ToStdString(child.tagName) != "Token") { + continue; + } + + const std::string tokenName = GetAttributeValue(child, "name"); + const std::string typeName = GetAttributeValue(child, "type"); + const std::string valueText = GetAttributeValue(child, "value"); + if (tokenName.empty() || typeName.empty() || valueText.empty()) { + continue; + } + + bool succeeded = false; + const Style::UIStyleValue parsedValue = + ParseThemeTokenValue(typeName, valueText, succeeded); + if (!succeeded) { + outError = "Failed to parse layout lab theme token: " + tokenName; + return Style::BuildBuiltinTheme(Style::UIBuiltinThemeKind::NeutralDark); + } + + definition.SetToken(tokenName, parsedValue); + } + + return Style::BuildTheme(definition); +} + +std::optional FindRepositoryRoot() { + std::vector candidates = { + fs::path(XCENGINE_NEW_EDITOR_REPO_ROOT) + }; + + const String resourceRoot = ResourceManager::Get().GetResourceRoot(); + if (!resourceRoot.Empty()) { + candidates.push_back(fs::path(resourceRoot.CStr())); + } + + std::error_code ec; + const fs::path currentPath = fs::current_path(ec); + if (!ec) { + candidates.push_back(currentPath); + } + + for (const fs::path& candidate : candidates) { + fs::path probe = candidate; + while (!probe.empty()) { + if (fs::exists(probe / kViewRelativePath) && + fs::exists(probe / kThemeRelativePath)) { + return probe; + } + + const fs::path parent = probe.parent_path(); + if (parent == probe) { + break; + } + probe = parent; + } + } + + return std::nullopt; +} + +bool TryGetWriteTime(const fs::path& path, fs::file_time_type& outWriteTime) { + try { + if (!path.empty() && fs::exists(path)) { + outWriteTime = fs::last_write_time(path); + return true; + } + } catch (...) { + } + + return false; +} + +bool UpdateTrackedWriteTimes(RuntimeBuildContext& state) { + fs::file_time_type viewWriteTime = {}; + fs::file_time_type themeWriteTime = {}; + if (!TryGetWriteTime(state.viewPath, viewWriteTime) || + !TryGetWriteTime(state.themePath, themeWriteTime)) { + return false; + } + + state.viewWriteTime = viewWriteTime; + state.themeWriteTime = themeWriteTime; + return true; +} + +bool DocumentsChangedOnDisk(const RuntimeBuildContext& state) { + fs::file_time_type viewWriteTime = {}; + fs::file_time_type themeWriteTime = {}; + if (!TryGetWriteTime(state.viewPath, viewWriteTime) || + !TryGetWriteTime(state.themePath, themeWriteTime)) { + return false; + } + + return viewWriteTime != state.viewWriteTime || + themeWriteTime != state.themeWriteTime; +} + +bool IsStretch(const std::string& value) { + return value.empty() || value == "stretch"; +} + +bool IsScrollViewTag(const std::string& tagName) { + return tagName == "ScrollView"; +} + +float ResolveScalar( + const std::string& text, + float referenceValue, + float fallbackValue) { + float parsedValue = 0.0f; + if (!TryParseFloat(text, parsedValue)) { + return fallbackValue; + } + + if (parsedValue > 0.0f && parsedValue <= 1.0f) { + return referenceValue * parsedValue; + } + + return parsedValue; +} + +float ResolveFloatToken( + const Style::UITheme& theme, + const std::string& tokenName, + float fallbackValue) { + const Style::UITokenResolveResult result = + theme.ResolveToken(tokenName, Style::UIStyleValueType::Float); + if (result.status != Style::UITokenResolveStatus::Resolved) { + return fallbackValue; + } + + const float* value = result.value.TryGetFloat(); + return value != nullptr ? *value : fallbackValue; +} + +Color ResolveColorToken( + const Style::UITheme& theme, + const std::string& tokenName, + const Color& fallbackValue) { + const Style::UITokenResolveResult result = + theme.ResolveToken(tokenName, Style::UIStyleValueType::Color); + if (result.status != Style::UITokenResolveStatus::Resolved) { + return fallbackValue; + } + + const Color* value = result.value.TryGetColor(); + return value != nullptr ? *value : fallbackValue; +} + +UIColor ToUIColor(const Color& color) { + return UIColor(color.r, color.g, color.b, color.a); +} + +UIRect InsetRect(const UIRect& rect, float padding) { + const float width = (std::max)(0.0f, rect.width - padding * 2.0f); + const float height = (std::max)(0.0f, rect.height - padding * 2.0f); + return UIRect(rect.x + padding, rect.y + padding, width, height); +} + +UIRect IntersectRects(const UIRect& a, const UIRect& b) { + const float left = (std::max)(a.x, b.x); + const float top = (std::max)(a.y, b.y); + const float right = (std::min)(a.x + a.width, b.x + b.width); + const float bottom = (std::min)(a.y + a.height, b.y + b.height); + return UIRect( + left, + top, + (std::max)(0.0f, right - left), + (std::max)(0.0f, bottom - top)); +} + +bool ContainsPoint(const UIRect& rect, const UIPoint& point) { + return point.x >= rect.x && + point.y >= rect.y && + point.x <= rect.x + rect.width && + point.y <= rect.y + rect.height; +} + +void AnalyzeNativeOverlayCompatibility( + const XCEngine::UI::UIDrawData& drawData, + XCUILayoutLabFrameStats& stats) { + const XCUIRHICommandSupportSummary summary = SummarizeXCUIRHICommandSupport(drawData); + stats.filledRectCommandCount = summary.stats.filledRectCommandCount; + stats.rectOutlineCommandCount = summary.stats.rectOutlineCommandCount; + stats.textCommandCount = summary.stats.textCommandCount; + stats.imageCommandCount = summary.stats.imageCommandCount; + stats.clipPushCommandCount = summary.stats.clipPushCommandCount; + stats.clipPopCommandCount = summary.stats.clipPopCommandCount; + stats.nativeSupportedCommandCount = summary.stats.supportedCommandCount; + stats.nativeUnsupportedCommandCount = summary.stats.unsupportedCommandCount; + stats.nativeUnsupportedImageCommandCount = summary.stats.unsupportedImageCommandCount; + stats.nativeUnsupportedUnknownCommandCount = summary.stats.unsupportedUnknownCommandCount; + + stats.nativeOverlayReady = stats.documentsReady && + stats.nativeUnsupportedCommandCount == 0u; + stats.nativeOverlayStatusMessage = summary.diagnostic; +} + +std::string BuildNodeId( + const UIDocumentNode& node, + const std::string& parentId, + std::size_t siblingIndex) { + const std::string explicitId = GetAttributeValue(node, "id"); + if (!explicitId.empty()) { + return explicitId; + } + + const std::string generated = + ToStdString(node.tagName) + std::to_string(static_cast(siblingIndex)); + return parentId.empty() + ? generated + : parentId + "/" + generated; +} + +void BuildNodesRecursive( + RuntimeBuildContext& state, + const UIDocumentNode& node, + std::size_t parentIndex, + const std::string& parentId, + std::size_t siblingIndex, + int depth) { + const std::size_t nodeIndex = state.nodes.size(); + LayoutNode layoutNode = {}; + layoutNode.id = BuildNodeId(node, parentId, siblingIndex); + layoutNode.tagName = ToStdString(node.tagName); + layoutNode.title = GetAttributeValue(node, "title"); + layoutNode.subtitle = GetAttributeValue(node, "subtitle"); + layoutNode.tone = GetAttributeValue(node, "tone"); + layoutNode.widthAttr = GetAttributeValue(node, "width"); + layoutNode.heightAttr = GetAttributeValue(node, "height"); + layoutNode.xAttr = GetAttributeValue(node, "x"); + layoutNode.yAttr = GetAttributeValue(node, "y"); + layoutNode.gapAttr = GetAttributeValue(node, "gap"); + layoutNode.paddingAttr = GetAttributeValue(node, "padding"); + layoutNode.scrollYAttr = GetAttributeValue(node, "scrollY"); + layoutNode.parentIndex = parentIndex; + layoutNode.depth = depth; + + state.nodes.push_back(layoutNode); + state.nodeIndexById[state.nodes.back().id] = nodeIndex; + if (parentIndex != kInvalidIndex) { + state.nodes[parentIndex].children.push_back(nodeIndex); + } + + for (std::size_t childIndex = 0; childIndex < node.children.Size(); ++childIndex) { + BuildNodesRecursive( + state, + node.children[childIndex], + nodeIndex, + state.nodes[nodeIndex].id, + childIndex, + depth + 1); + } +} + +float ResolveGap(const LayoutNode& node, const Style::UITheme& theme) { + if (!node.gapAttr.empty()) { + return ResolveScalar( + node.gapAttr, + ResolveFloatToken(theme, "space.gap", 14.0f), + ResolveFloatToken(theme, "space.gap", 14.0f)); + } + + return node.tagName == "Column" + ? ResolveFloatToken(theme, "space.stack", 12.0f) + : ResolveFloatToken(theme, "space.gap", 14.0f); +} + +float ResolvePadding(const LayoutNode& node, const Style::UITheme& theme) { + if (!node.paddingAttr.empty()) { + return ResolveScalar( + node.paddingAttr, + ResolveFloatToken(theme, "space.outer", 18.0f), + ResolveFloatToken(theme, "space.outer", 18.0f)); + } + + return node.tagName == "View" + ? ResolveFloatToken(theme, "space.outer", 18.0f) + : 0.0f; +} + +float ResolveScrollOffset(const LayoutNode& node, const Style::UITheme& theme) { + if (node.scrollYAttr.empty()) { + return 0.0f; + } + + return (std::max)( + 0.0f, + ResolveScalar( + node.scrollYAttr, + ResolveFloatToken(theme, "size.scrollStep", 64.0f), + 0.0f)); +} + +float ResolveListItemHeight(const Style::UITheme& theme) { + return ResolveFloatToken(theme, "size.listItemHeight", 60.0f); +} + +UIRect GetContentRect(const LayoutNode& node, const Style::UITheme& theme) { + return InsetRect(node.rect, ResolvePadding(node, theme)); +} + +void LayoutNodeTree(RuntimeBuildContext& state, std::size_t nodeIndex); + +void LayoutColumnChildren(RuntimeBuildContext& state, std::size_t nodeIndex) { + LayoutNode& node = state.nodes[nodeIndex]; + const float padding = ResolvePadding(node, state.theme); + const float gap = ResolveGap(node, state.theme); + const UIRect contentRect = InsetRect(node.rect, padding); + if (node.children.empty()) { + return; + } + + float fixedHeight = 0.0f; + std::size_t stretchCount = 0; + std::vector resolvedHeights(node.children.size(), 0.0f); + for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) { + const LayoutNode& child = state.nodes[node.children[childOffset]]; + if (IsStretch(child.heightAttr)) { + ++stretchCount; + continue; + } + + resolvedHeights[childOffset] = ResolveScalar(child.heightAttr, contentRect.height, 0.0f); + fixedHeight += resolvedHeights[childOffset]; + } + + const float totalGap = gap * static_cast((std::max)(1u, node.children.size()) - 1u); + const float remainingHeight = (std::max)(0.0f, contentRect.height - fixedHeight - totalGap); + const float stretchHeight = + stretchCount > 0u ? remainingHeight / static_cast(stretchCount) : 0.0f; + + float cursorY = contentRect.y; + for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) { + LayoutNode& child = state.nodes[node.children[childOffset]]; + const float childHeight = + !IsStretch(child.heightAttr) ? resolvedHeights[childOffset] : stretchHeight; + const float childWidth = + !IsStretch(child.widthAttr) + ? ResolveScalar(child.widthAttr, contentRect.width, contentRect.width) + : contentRect.width; + child.rect = UIRect(contentRect.x, cursorY, childWidth, childHeight); + cursorY += childHeight + gap; + LayoutNodeTree(state, node.children[childOffset]); + } +} + +void LayoutRowChildren(RuntimeBuildContext& state, std::size_t nodeIndex) { + LayoutNode& node = state.nodes[nodeIndex]; + const float padding = ResolvePadding(node, state.theme); + const float gap = ResolveGap(node, state.theme); + const UIRect contentRect = InsetRect(node.rect, padding); + if (node.children.empty()) { + return; + } + + float fixedWidth = 0.0f; + std::size_t stretchCount = 0; + std::vector resolvedWidths(node.children.size(), 0.0f); + for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) { + const LayoutNode& child = state.nodes[node.children[childOffset]]; + if (IsStretch(child.widthAttr)) { + ++stretchCount; + continue; + } + + resolvedWidths[childOffset] = ResolveScalar(child.widthAttr, contentRect.width, 0.0f); + fixedWidth += resolvedWidths[childOffset]; + } + + const float totalGap = gap * static_cast((std::max)(1u, node.children.size()) - 1u); + const float remainingWidth = (std::max)(0.0f, contentRect.width - fixedWidth - totalGap); + const float stretchWidth = + stretchCount > 0u ? remainingWidth / static_cast(stretchCount) : 0.0f; + + float cursorX = contentRect.x; + for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) { + LayoutNode& child = state.nodes[node.children[childOffset]]; + const float childWidth = + !IsStretch(child.widthAttr) ? resolvedWidths[childOffset] : stretchWidth; + const float childHeight = + !IsStretch(child.heightAttr) + ? ResolveScalar(child.heightAttr, contentRect.height, contentRect.height) + : contentRect.height; + child.rect = UIRect(cursorX, contentRect.y, childWidth, childHeight); + cursorX += childWidth + gap; + LayoutNodeTree(state, node.children[childOffset]); + } +} + +void LayoutOverlayChildren(RuntimeBuildContext& state, std::size_t nodeIndex) { + LayoutNode& node = state.nodes[nodeIndex]; + const UIRect contentRect = GetContentRect(node, state.theme); + + for (std::size_t childIndex : node.children) { + LayoutNode& child = state.nodes[childIndex]; + const float offsetX = ResolveScalar(child.xAttr, contentRect.width, 0.0f); + const float offsetY = ResolveScalar(child.yAttr, contentRect.height, 0.0f); + const float childWidth = + child.widthAttr.empty() + ? (std::max)(0.0f, contentRect.width - offsetX) + : ResolveScalar(child.widthAttr, contentRect.width, contentRect.width - offsetX); + const float childHeight = + child.heightAttr.empty() + ? (std::max)(0.0f, contentRect.height - offsetY) + : ResolveScalar(child.heightAttr, contentRect.height, contentRect.height - offsetY); + + child.rect = UIRect( + contentRect.x + offsetX, + contentRect.y + offsetY, + (std::min)(childWidth, (std::max)(0.0f, contentRect.width - offsetX)), + (std::min)(childHeight, (std::max)(0.0f, contentRect.height - offsetY))); + LayoutNodeTree(state, childIndex); + } +} + +void LayoutScrollViewChildren(RuntimeBuildContext& state, std::size_t nodeIndex) { + LayoutNode& node = state.nodes[nodeIndex]; + const float gap = ResolveGap(node, state.theme); + const float scrollOffset = ResolveScrollOffset(node, state.theme); + const float defaultItemHeight = ResolveListItemHeight(state.theme); + const UIRect contentRect = GetContentRect(node, state.theme); + + float cursorY = contentRect.y - scrollOffset; + for (std::size_t childIndex : node.children) { + LayoutNode& child = state.nodes[childIndex]; + const float childHeight = + !IsStretch(child.heightAttr) + ? ResolveScalar(child.heightAttr, contentRect.height, defaultItemHeight) + : defaultItemHeight; + const float childWidth = + !IsStretch(child.widthAttr) + ? (std::min)( + ResolveScalar(child.widthAttr, contentRect.width, contentRect.width), + contentRect.width) + : contentRect.width; + child.rect = UIRect(contentRect.x, cursorY, childWidth, childHeight); + cursorY += childHeight + gap; + LayoutNodeTree(state, childIndex); + } +} + +void LayoutNodeTree(RuntimeBuildContext& state, std::size_t nodeIndex) { + const LayoutNode& node = state.nodes[nodeIndex]; + state.rectsById[node.id] = node.rect; + + if (node.tagName == "View" || node.tagName == "Column") { + LayoutColumnChildren(state, nodeIndex); + } else if (node.tagName == "Row") { + LayoutRowChildren(state, nodeIndex); + } else if (node.tagName == "Overlay") { + LayoutOverlayChildren(state, nodeIndex); + } else if (IsScrollViewTag(node.tagName)) { + LayoutScrollViewChildren(state, nodeIndex); + } +} + +void DrawNode( + RuntimeBuildContext& state, + std::size_t nodeIndex, + UIDrawList& drawList, + const std::string& hoveredId) { + const LayoutNode& node = state.nodes[nodeIndex]; + + if (node.tagName == "View") { + const Color panelColor = ResolveColorToken( + state.theme, + "color.panel", + Color(0.07f, 0.10f, 0.14f, 1.0f)); + drawList.AddFilledRect(node.rect, ToUIColor(panelColor), 0.0f); + } else if (IsScrollViewTag(node.tagName)) { + const Color surfaceColor = ResolveColorToken( + state.theme, + "color.scroll.surface", + Color(0.09f, 0.12f, 0.16f, 1.0f)); + const Color borderColor = ResolveColorToken( + state.theme, + "color.border", + Color(0.24f, 0.34f, 0.43f, 1.0f)); + const float rounding = ResolveFloatToken(state.theme, "radius.card", 10.0f); + drawList.AddFilledRect(node.rect, ToUIColor(surfaceColor), rounding); + drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding); + } else if (node.tagName == "Card") { + const Color cardColor = ResolveColorToken( + state.theme, + node.tone == "accent" + ? "color.accent" + : (node.tone == "accent-alt" ? "color.card.alt" : "color.card"), + node.tone == "accent" + ? Color(0.19f, 0.33f, 0.44f, 1.0f) + : Color(0.12f, 0.17f, 0.23f, 1.0f)); + const Color borderColor = ResolveColorToken( + state.theme, + "color.border", + Color(0.24f, 0.34f, 0.43f, 1.0f)); + const Color textColor = ResolveColorToken( + state.theme, + "color.text", + Color(0.95f, 0.97f, 1.0f, 1.0f)); + const Color mutedColor = ResolveColorToken( + state.theme, + "color.text.muted", + Color(0.72f, 0.79f, 0.86f, 1.0f)); + const float rounding = ResolveFloatToken(state.theme, "radius.card", 10.0f); + const float inset = ResolveFloatToken(state.theme, "space.cardInset", 12.0f); + const float titleFont = ResolveFloatToken(state.theme, "font.title", 16.0f); + const float bodyFont = ResolveFloatToken(state.theme, "font.body", 13.0f); + + drawList.AddFilledRect(node.rect, ToUIColor(cardColor), rounding); + drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding); + if (!node.title.empty()) { + drawList.AddText( + UIPoint(node.rect.x + inset, node.rect.y + inset), + node.title, + ToUIColor(textColor), + titleFont); + } + if (!node.subtitle.empty()) { + drawList.AddText( + UIPoint(node.rect.x + inset, node.rect.y + inset + titleFont + 8.0f), + node.subtitle, + ToUIColor(mutedColor), + bodyFont); + } + if (node.id == hoveredId) { + drawList.AddRectOutline( + node.rect, + UIColor(1.0f, 0.82f, 0.45f, 1.0f), + 2.0f, + rounding); + } + } + + const bool clipsChildren = IsScrollViewTag(node.tagName); + if (clipsChildren) { + drawList.PushClipRect(GetContentRect(node, state.theme)); + } + for (std::size_t childIndex : node.children) { + DrawNode(state, childIndex, drawList, hoveredId); + } + if (clipsChildren) { + drawList.PopClipRect(); + } +} + +bool IsPointInsideNodeClipping( + const RuntimeBuildContext& state, + std::size_t nodeIndex, + const UIPoint& point) { + std::size_t currentIndex = nodeIndex; + while (currentIndex != kInvalidIndex) { + const LayoutNode& currentNode = state.nodes[currentIndex]; + if (IsScrollViewTag(currentNode.tagName) && + !ContainsPoint(GetContentRect(currentNode, state.theme), point)) { + return false; + } + currentIndex = currentNode.parentIndex; + } + + return true; +} + +std::size_t HitTest( + const RuntimeBuildContext& state, + const UIPoint& point) { + std::size_t bestIndex = kInvalidIndex; + int bestDepth = -1; + for (std::size_t index = 0; index < state.nodes.size(); ++index) { + const LayoutNode& node = state.nodes[index]; + if (node.tagName != "Card" || + !ContainsPoint(node.rect, point) || + !IsPointInsideNodeClipping(state, index, point)) { + continue; + } + + if (node.depth >= bestDepth) { + bestIndex = index; + bestDepth = node.depth; + } + } + + return bestIndex; +} + +} // namespace + +struct XCUILayoutLabRuntime::RuntimeState { + RuntimeBuildContext data = {}; +}; + +XCUILayoutLabRuntime::XCUILayoutLabRuntime() + : m_state(std::make_unique()) { +} + +XCUILayoutLabRuntime::~XCUILayoutLabRuntime() = default; + +XCUILayoutLabRuntime::XCUILayoutLabRuntime(XCUILayoutLabRuntime&& other) noexcept = default; + +XCUILayoutLabRuntime& XCUILayoutLabRuntime::operator=(XCUILayoutLabRuntime&& other) noexcept = default; + +bool XCUILayoutLabRuntime::ReloadDocuments() { + RuntimeBuildContext& state = m_state->data; + state.documentsReady = false; + state.statusMessage.clear(); + state.viewDocument = {}; + state.themeDocument = {}; + state.nodes.clear(); + state.nodeIndexById.clear(); + state.rectsById.clear(); + + state.documentSource.SetPathSet(XCUIAssetDocumentSource::MakeLayoutLabPathSet()); + if (!state.documentSource.Reload()) { + const XCUIAssetDocumentSource::LoadState& loadState = state.documentSource.GetState(); + state.statusMessage = !loadState.errorMessage.empty() + ? loadState.errorMessage + : loadState.statusMessage; + state.frameResult.stats.statusMessage = state.statusMessage; + return false; + } + + const XCUIAssetDocumentSource::LoadState& loadState = state.documentSource.GetState(); + state.repoRoot = loadState.repositoryRoot; + state.viewPath = loadState.view.sourcePath; + state.themePath = loadState.theme.sourcePath; + state.viewDocument = loadState.view.compileResult; + state.themeDocument = loadState.theme.compileResult; + + std::string themeError = {}; + state.theme = BuildThemeFromDocument(state.themeDocument.document, themeError); + if (!themeError.empty()) { + state.statusMessage = themeError; + state.frameResult.stats.statusMessage = state.statusMessage; + return false; + } + + BuildNodesRecursive( + state, + state.viewDocument.document.rootNode, + kInvalidIndex, + std::string(), + 0u, + 0); + state.documentsReady = !state.nodes.empty(); + state.statusMessage = state.documentsReady + ? (loadState.usedLegacyFallback + ? "Layout lab documents loaded (legacy fallback)" + : "Layout lab documents loaded") + : "Layout lab view is empty"; + state.frameResult.stats.statusMessage = state.statusMessage; + return state.documentsReady; +} + +const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLabInputState& input) { + RuntimeBuildContext& state = m_state->data; + state.frameResult = XCUILayoutLabFrameResult(); + state.rectsById.clear(); + + if (state.documentsReady && state.documentSource.HasTrackedChanges()) { + ReloadDocuments(); + } + + if (!state.documentsReady && !ReloadDocuments()) { + state.frameResult.stats.documentsReady = false; + state.frameResult.stats.statusMessage = state.statusMessage; + state.frameResult.stats.nativeOverlayStatusMessage = state.statusMessage; + return state.frameResult; + } + + if (state.nodes.empty()) { + state.frameResult.stats.documentsReady = false; + state.frameResult.stats.statusMessage = "Layout lab has no nodes."; + state.frameResult.stats.nativeOverlayStatusMessage = state.frameResult.stats.statusMessage; + return state.frameResult; + } + + state.nodes[0].rect = input.canvasRect; + LayoutNodeTree(state, 0u); + if (!state.nodes.empty()) { + state.rectsById[state.nodes[0].id] = state.nodes[0].rect; + } + + if (input.pointerInside) { + const std::size_t hoveredIndex = HitTest(state, input.pointerPosition); + if (hoveredIndex != kInvalidIndex) { + state.frameResult.stats.hoveredElementId = state.nodes[hoveredIndex].id; + } + } + + UIDrawList drawList("XCUI Layout Lab"); + drawList.PushClipRect(input.canvasRect); + DrawNode(state, 0u, drawList, state.frameResult.stats.hoveredElementId); + drawList.PopClipRect(); + + state.frameResult.drawData.AddDrawList(std::move(drawList)); + state.frameResult.stats.documentsReady = true; + state.frameResult.stats.statusMessage = state.statusMessage; + state.frameResult.stats.drawListCount = state.frameResult.drawData.GetDrawListCount(); + state.frameResult.stats.commandCount = state.frameResult.drawData.GetTotalCommandCount(); + AnalyzeNativeOverlayCompatibility(state.frameResult.drawData, state.frameResult.stats); + for (const LayoutNode& node : state.nodes) { + if (node.tagName == "Row") { + ++state.frameResult.stats.rowCount; + } else if (node.tagName == "Column") { + ++state.frameResult.stats.columnCount; + } else if (node.tagName == "Overlay") { + ++state.frameResult.stats.overlayCount; + } else if (IsScrollViewTag(node.tagName)) { + ++state.frameResult.stats.scrollViewCount; + } + } + + return state.frameResult; +} + +const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::GetFrameResult() const { + return m_state->data.frameResult; +} + +bool XCUILayoutLabRuntime::TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const { + const RuntimeBuildContext& state = m_state->data; + const auto it = state.rectsById.find(elementId); + if (it == state.rectsById.end()) { + return false; + } + + outRect = it->second; + return true; +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h new file mode 100644 index 00000000..08f64a47 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +#include +#include + +namespace XCEngine { +namespace UI { +struct UIRect; +struct UIPoint; +} // namespace UI +namespace Editor { +namespace XCUIBackend { + +struct XCUILayoutLabInputState { + UI::UIRect canvasRect = {}; + UI::UIPoint pointerPosition = {}; + bool pointerInside = false; +}; + +struct XCUILayoutLabFrameStats { + bool documentsReady = false; + std::string statusMessage = {}; + std::size_t drawListCount = 0; + std::size_t commandCount = 0; + std::size_t filledRectCommandCount = 0; + std::size_t rectOutlineCommandCount = 0; + std::size_t textCommandCount = 0; + std::size_t imageCommandCount = 0; + std::size_t clipPushCommandCount = 0; + std::size_t clipPopCommandCount = 0; + std::size_t nativeSupportedCommandCount = 0; + std::size_t nativeUnsupportedCommandCount = 0; + std::size_t nativeUnsupportedImageCommandCount = 0; + std::size_t nativeUnsupportedUnknownCommandCount = 0; + bool nativeOverlayReady = false; + std::string nativeOverlayStatusMessage = {}; + std::size_t rowCount = 0; + std::size_t columnCount = 0; + std::size_t overlayCount = 0; + std::size_t scrollViewCount = 0; + std::string hoveredElementId = {}; +}; + +struct XCUILayoutLabFrameResult { + UI::UIDrawData drawData = {}; + XCUILayoutLabFrameStats stats = {}; +}; + +class XCUILayoutLabRuntime { +public: + XCUILayoutLabRuntime(); + ~XCUILayoutLabRuntime(); + + XCUILayoutLabRuntime(XCUILayoutLabRuntime&& other) noexcept; + XCUILayoutLabRuntime& operator=(XCUILayoutLabRuntime&& other) noexcept; + + XCUILayoutLabRuntime(const XCUILayoutLabRuntime&) = delete; + XCUILayoutLabRuntime& operator=(const XCUILayoutLabRuntime&) = delete; + + bool ReloadDocuments(); + const XCUILayoutLabFrameResult& Update(const XCUILayoutLabInputState& input); + const XCUILayoutLabFrameResult& GetFrameResult() const; + bool TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const; + +private: + struct RuntimeState; + std::unique_ptr m_state; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIRHICommandCompiler.cpp b/new_editor/src/XCUIBackend/XCUIRHICommandCompiler.cpp new file mode 100644 index 00000000..0ab71d1d --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIRHICommandCompiler.cpp @@ -0,0 +1,590 @@ +#include "XCUIBackend/XCUIRHICommandCompiler.h" + +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace { + +using Compiler = XCUIRHICommandCompiler; + +UI::UIRect NormalizeRect(const UI::UIRect& rect) { + UI::UIRect normalized = rect; + if (normalized.width < 0.0f) { + normalized.x += normalized.width; + normalized.width = -normalized.width; + } + if (normalized.height < 0.0f) { + normalized.y += normalized.height; + normalized.height = -normalized.height; + } + return normalized; +} + +bool IsRectEmpty(const UI::UIRect& rect) { + return rect.width <= 0.0f || rect.height <= 0.0f; +} + +bool IntersectRects( + const UI::UIRect& lhs, + const UI::UIRect& rhs, + UI::UIRect& outIntersection) { + const UI::UIRect left = NormalizeRect(lhs); + const UI::UIRect right = NormalizeRect(rhs); + const float minX = (std::max)(left.x, right.x); + const float minY = (std::max)(left.y, right.y); + const float maxX = (std::min)(left.x + left.width, right.x + right.width); + const float maxY = (std::min)(left.y + left.height, right.y + right.height); + if (maxX <= minX || maxY <= minY) { + outIntersection = {}; + return false; + } + + outIntersection = UI::UIRect(minX, minY, maxX - minX, maxY - minY); + return true; +} + +bool RectEquals(const UI::UIRect& lhs, const UI::UIRect& rhs) { + return lhs.x == rhs.x && + lhs.y == rhs.y && + lhs.width == rhs.width && + lhs.height == rhs.height; +} + +bool TextureEquals(const UI::UITextureHandle& lhs, const UI::UITextureHandle& rhs) { + return lhs.nativeHandle == rhs.nativeHandle && + lhs.width == rhs.width && + lhs.height == rhs.height && + lhs.kind == rhs.kind; +} + +void AppendFilledRectVertices( + std::vector& outVertices, + const UI::UIRect& rect, + const UI::UIColor& color) { + const Compiler::ColorVertex vertices[6] = { + { { rect.x, rect.y }, { color.r, color.g, color.b, color.a } }, + { { rect.x + rect.width, rect.y }, { color.r, color.g, color.b, color.a } }, + { { rect.x + rect.width, rect.y + rect.height }, { color.r, color.g, color.b, color.a } }, + { { rect.x, rect.y }, { color.r, color.g, color.b, color.a } }, + { { rect.x + rect.width, rect.y + rect.height }, { color.r, color.g, color.b, color.a } }, + { { rect.x, rect.y + rect.height }, { color.r, color.g, color.b, color.a } } + }; + outVertices.insert(outVertices.end(), std::begin(vertices), std::end(vertices)); +} + +bool AppendClippedFilledRect( + std::vector& outVertices, + const UI::UIRect& rect, + const UI::UIColor& color, + const UI::UIRect& clipRect) { + UI::UIRect clipped = {}; + if (!IntersectRects(rect, clipRect, clipped)) { + return false; + } + + AppendFilledRectVertices(outVertices, clipped, color); + return true; +} + +bool AppendClippedRectOutline( + std::vector& outVertices, + const UI::UIRect& rect, + const UI::UIColor& color, + float thickness, + const UI::UIRect& clipRect) { + const UI::UIRect normalized = NormalizeRect(rect); + if (IsRectEmpty(normalized)) { + return false; + } + + const float resolvedThickness = (std::max)(1.0f, thickness); + const float halfWidth = normalized.width * 0.5f; + const float halfHeight = normalized.height * 0.5f; + const float edgeThickness = (std::min)(resolvedThickness, (std::min)(halfWidth, halfHeight)); + if (edgeThickness <= 0.0f) { + return false; + } + + bool rendered = false; + rendered |= AppendClippedFilledRect( + outVertices, + UI::UIRect(normalized.x, normalized.y, normalized.width, edgeThickness), + color, + clipRect); + rendered |= AppendClippedFilledRect( + outVertices, + UI::UIRect( + normalized.x, + normalized.y + normalized.height - edgeThickness, + normalized.width, + edgeThickness), + color, + clipRect); + rendered |= AppendClippedFilledRect( + outVertices, + UI::UIRect( + normalized.x, + normalized.y + edgeThickness, + edgeThickness, + normalized.height - edgeThickness * 2.0f), + color, + clipRect); + rendered |= AppendClippedFilledRect( + outVertices, + UI::UIRect( + normalized.x + normalized.width - edgeThickness, + normalized.y + edgeThickness, + edgeThickness, + normalized.height - edgeThickness * 2.0f), + color, + clipRect); + return rendered; +} + +void AppendTexturedRectVertices( + std::vector& outVertices, + const UI::UIRect& rect, + float u0, + float v0, + float u1, + float v1, + const UI::UIColor& color) { + const Compiler::TexturedVertex vertices[6] = { + { { rect.x, rect.y }, { u0, v0 }, { color.r, color.g, color.b, color.a } }, + { { rect.x + rect.width, rect.y }, { u1, v0 }, { color.r, color.g, color.b, color.a } }, + { { rect.x + rect.width, rect.y + rect.height }, { u1, v1 }, { color.r, color.g, color.b, color.a } }, + { { rect.x, rect.y }, { u0, v0 }, { color.r, color.g, color.b, color.a } }, + { { rect.x + rect.width, rect.y + rect.height }, { u1, v1 }, { color.r, color.g, color.b, color.a } }, + { { rect.x, rect.y + rect.height }, { u0, v1 }, { color.r, color.g, color.b, color.a } } + }; + outVertices.insert(outVertices.end(), std::begin(vertices), std::end(vertices)); +} + +bool AppendClippedTexturedRect( + std::vector& outVertices, + const UI::UIRect& rect, + float u0, + float v0, + float u1, + float v1, + const UI::UIColor& color, + const UI::UIRect& clipRect) { + const bool flipU = rect.width < 0.0f; + const bool flipV = rect.height < 0.0f; + const UI::UIRect normalized = NormalizeRect(rect); + if (IsRectEmpty(normalized)) { + return false; + } + + UI::UIRect clipped = {}; + if (!IntersectRects(normalized, clipRect, clipped)) { + return false; + } + + const float leftT = (clipped.x - normalized.x) / normalized.width; + const float rightT = (clipped.x + clipped.width - normalized.x) / normalized.width; + const float topT = (clipped.y - normalized.y) / normalized.height; + const float bottomT = (clipped.y + clipped.height - normalized.y) / normalized.height; + const float resolvedU0 = flipU ? u1 : u0; + const float resolvedU1 = flipU ? u0 : u1; + const float resolvedV0 = flipV ? v1 : v0; + const float resolvedV1 = flipV ? v0 : v1; + const float clippedU0 = resolvedU0 + (resolvedU1 - resolvedU0) * leftT; + const float clippedU1 = resolvedU0 + (resolvedU1 - resolvedU0) * rightT; + const float clippedV0 = resolvedV0 + (resolvedV1 - resolvedV0) * topT; + const float clippedV1 = resolvedV0 + (resolvedV1 - resolvedV0) * bottomT; + AppendTexturedRectVertices( + outVertices, + clipped, + clippedU0, + clippedV0, + clippedU1, + clippedV1, + color); + return true; +} + +bool DecodeNextUtf8(const char*& cursor, const char* end, std::uint32_t& outCodepoint) { + if (cursor >= end) { + return false; + } + + const unsigned char lead = static_cast(*cursor++); + if (lead < 0x80u) { + outCodepoint = static_cast(lead); + return true; + } + + const int continuationCount = + (lead & 0xE0u) == 0xC0u ? 1 : + (lead & 0xF0u) == 0xE0u ? 2 : + (lead & 0xF8u) == 0xF0u ? 3 : + -1; + if (continuationCount < 0 || cursor + continuationCount > end) { + outCodepoint = 0xFFFDu; + cursor = end; + return true; + } + + std::uint32_t codepoint = + continuationCount == 1 ? static_cast(lead & 0x1Fu) : + continuationCount == 2 ? static_cast(lead & 0x0Fu) : + static_cast(lead & 0x07u); + for (int index = 0; index < continuationCount; ++index) { + const unsigned char continuation = static_cast(*cursor); + if ((continuation & 0xC0u) != 0x80u) { + outCodepoint = 0xFFFDu; + return true; + } + ++cursor; + codepoint = (codepoint << 6u) | static_cast(continuation & 0x3Fu); + } + + outCodepoint = codepoint; + return true; +} + +void AppendOrMergeBatch( + std::vector& outBatches, + Compiler::BatchKind kind, + std::size_t drawListIndex, + std::size_t commandIndex, + std::size_t firstVertex, + std::size_t vertexCount, + const UI::UIRect& clipRect, + const UI::UITextureHandle& texture, + bool allowMerge) { + if (vertexCount == 0u) { + return; + } + + if (allowMerge && !outBatches.empty()) { + Compiler::Batch& lastBatch = outBatches.back(); + if (lastBatch.kind == kind && + lastBatch.drawListIndex == drawListIndex && + lastBatch.firstCommandIndex + lastBatch.commandCount == commandIndex && + RectEquals(lastBatch.clipRect, clipRect) && + TextureEquals(lastBatch.texture, texture) && + static_cast(lastBatch.firstVertex + lastBatch.vertexCount) == firstVertex) { + lastBatch.vertexCount += static_cast(vertexCount); + lastBatch.commandCount += 1u; + return; + } + } + + Compiler::Batch batch = {}; + batch.kind = kind; + batch.drawListIndex = static_cast(drawListIndex); + batch.firstCommandIndex = static_cast(commandIndex); + batch.commandCount = 1u; + batch.firstVertex = static_cast(firstVertex); + batch.vertexCount = static_cast(vertexCount); + batch.clipRect = clipRect; + batch.texture = texture; + outBatches.push_back(batch); +} + +bool CompileTextCommand( + const UI::UIDrawCommand& command, + const UI::UIRect& clipRect, + const Compiler::TextGlyphProvider& glyphProvider, + std::vector& outVertices, + Compiler::TextRunContext& outContext, + std::size_t& outAddedVertexCount) { + outContext = {}; + if (!glyphProvider.BeginText(command.fontSize, outContext)) { + outAddedVertexCount = 0u; + return false; + } + + if (!outContext.texture.IsValid() || outContext.lineHeight <= 0.0f) { + outAddedVertexCount = 0u; + return false; + } + + const std::size_t startVertex = outVertices.size(); + const float startX = command.position.x; + float cursorX = startX; + float cursorY = command.position.y; + + const char* cursor = command.text.c_str(); + const char* const end = cursor + command.text.size(); + while (cursor < end) { + std::uint32_t codepoint = 0u; + if (!DecodeNextUtf8(cursor, end, codepoint)) { + break; + } + + if (codepoint == '\r') { + continue; + } + if (codepoint == '\n') { + cursorX = startX; + cursorY += outContext.lineHeight; + continue; + } + + Compiler::TextGlyph glyph = {}; + if (!glyphProvider.ResolveGlyph(outContext, codepoint, glyph)) { + continue; + } + + if (glyph.visible) { + const UI::UIRect glyphRect( + cursorX + glyph.x0, + cursorY + glyph.y0, + glyph.x1 - glyph.x0, + glyph.y1 - glyph.y0); + AppendClippedTexturedRect( + outVertices, + glyphRect, + glyph.u0, + glyph.v0, + glyph.u1, + glyph.v1, + command.color, + clipRect); + } + + cursorX += glyph.advanceX; + } + + outAddedVertexCount = outVertices.size() - startVertex; + return true; +} + +} // namespace + +void XCUIRHICommandCompiler::CompiledDrawData::Clear() { + colorVertices.clear(); + texturedVertices.clear(); + batches.clear(); + stats = {}; +} + +bool XCUIRHICommandCompiler::CompiledDrawData::Empty() const { + return colorVertices.empty() && texturedVertices.empty() && batches.empty(); +} + +void XCUIRHICommandCompiler::Compile( + const UI::UIDrawData& drawData, + const CompileConfig& config, + CompiledDrawData& outCompiledDrawData) const { + outCompiledDrawData.Clear(); + outCompiledDrawData.stats.drawListCount = drawData.GetDrawListCount(); + outCompiledDrawData.stats.commandCount = drawData.GetTotalCommandCount(); + if (drawData.Empty()) { + return; + } + + UI::UIRect surfaceClipRect = NormalizeRect(config.surfaceClipRect); + if (IsRectEmpty(surfaceClipRect)) { + surfaceClipRect = {}; + } + + outCompiledDrawData.colorVertices.reserve(drawData.GetTotalCommandCount() * 24u); + outCompiledDrawData.texturedVertices.reserve(drawData.GetTotalCommandCount() * 36u); + outCompiledDrawData.batches.reserve(drawData.GetTotalCommandCount()); + + std::vector clipStack = {}; + UI::UIRect currentClipRect = surfaceClipRect; + + std::size_t drawListIndex = 0u; + for (const UI::UIDrawList& drawList : drawData.GetDrawLists()) { + std::size_t commandIndex = 0u; + for (const UI::UIDrawCommand& command : drawList.GetCommands()) { + bool compiled = false; + bool culled = false; + bool unsupported = false; + + switch (command.type) { + case UI::UIDrawCommandType::FilledRect: { + ++outCompiledDrawData.stats.filledRectCommandCount; + const std::size_t firstVertex = outCompiledDrawData.colorVertices.size(); + compiled = AppendClippedFilledRect( + outCompiledDrawData.colorVertices, + command.rect, + command.color, + currentClipRect); + if (compiled) { + AppendOrMergeBatch( + outCompiledDrawData.batches, + BatchKind::Color, + drawListIndex, + commandIndex, + firstVertex, + outCompiledDrawData.colorVertices.size() - firstVertex, + currentClipRect, + {}, + config.mergeBatchesWithinDrawList); + } else { + culled = true; + } + break; + } + case UI::UIDrawCommandType::RectOutline: { + ++outCompiledDrawData.stats.rectOutlineCommandCount; + const std::size_t firstVertex = outCompiledDrawData.colorVertices.size(); + compiled = AppendClippedRectOutline( + outCompiledDrawData.colorVertices, + command.rect, + command.color, + command.thickness, + currentClipRect); + if (compiled) { + AppendOrMergeBatch( + outCompiledDrawData.batches, + BatchKind::Color, + drawListIndex, + commandIndex, + firstVertex, + outCompiledDrawData.colorVertices.size() - firstVertex, + currentClipRect, + {}, + config.mergeBatchesWithinDrawList); + } else { + culled = true; + } + break; + } + case UI::UIDrawCommandType::Text: { + ++outCompiledDrawData.stats.textCommandCount; + if (command.text.empty()) { + culled = true; + break; + } + if (config.textGlyphProvider == nullptr) { + unsupported = true; + break; + } + + const std::size_t firstVertex = outCompiledDrawData.texturedVertices.size(); + std::size_t addedVertexCount = 0u; + TextRunContext textRunContext = {}; + compiled = CompileTextCommand( + command, + currentClipRect, + *config.textGlyphProvider, + outCompiledDrawData.texturedVertices, + textRunContext, + addedVertexCount); + if (compiled && addedVertexCount > 0u) { + AppendOrMergeBatch( + outCompiledDrawData.batches, + BatchKind::Textured, + drawListIndex, + commandIndex, + firstVertex, + addedVertexCount, + currentClipRect, + textRunContext.texture, + config.mergeBatchesWithinDrawList); + } else if (!compiled) { + unsupported = true; + } else { + compiled = false; + culled = true; + } + break; + } + case UI::UIDrawCommandType::Image: { + ++outCompiledDrawData.stats.imageCommandCount; + if (!command.texture.IsValid()) { + unsupported = true; + break; + } + + const std::size_t firstVertex = outCompiledDrawData.texturedVertices.size(); + compiled = AppendClippedTexturedRect( + outCompiledDrawData.texturedVertices, + command.rect, + 0.0f, + 0.0f, + 1.0f, + 1.0f, + command.color, + currentClipRect); + if (compiled) { + AppendOrMergeBatch( + outCompiledDrawData.batches, + BatchKind::Textured, + drawListIndex, + commandIndex, + firstVertex, + outCompiledDrawData.texturedVertices.size() - firstVertex, + currentClipRect, + command.texture, + config.mergeBatchesWithinDrawList); + } else { + culled = true; + } + break; + } + case UI::UIDrawCommandType::PushClipRect: { + ++outCompiledDrawData.stats.clipPushCommandCount; + UI::UIRect nextClipRect = NormalizeRect(command.rect); + UI::UIRect intersection = {}; + if (command.intersectWithCurrentClip) { + if (!IntersectRects(currentClipRect, nextClipRect, intersection)) { + intersection = {}; + } + } else if (!IntersectRects(surfaceClipRect, nextClipRect, intersection)) { + intersection = {}; + } + clipStack.push_back(currentClipRect); + currentClipRect = intersection; + compiled = true; + outCompiledDrawData.stats.maxClipDepth = + (std::max)(outCompiledDrawData.stats.maxClipDepth, clipStack.size()); + break; + } + case UI::UIDrawCommandType::PopClipRect: + ++outCompiledDrawData.stats.clipPopCommandCount; + if (!clipStack.empty()) { + currentClipRect = clipStack.back(); + clipStack.pop_back(); + } else { + currentClipRect = surfaceClipRect; + ++outCompiledDrawData.stats.clipStackUnderflowCount; + } + compiled = true; + break; + default: + unsupported = true; + break; + } + + if (compiled) { + ++outCompiledDrawData.stats.compiledCommandCount; + } else { + ++outCompiledDrawData.stats.skippedCommandCount; + if (culled) { + ++outCompiledDrawData.stats.culledCommandCount; + } + if (unsupported) { + ++outCompiledDrawData.stats.unsupportedCommandCount; + } + } + + ++commandIndex; + } + ++drawListIndex; + } + + outCompiledDrawData.stats.colorVertexCount = outCompiledDrawData.colorVertices.size(); + outCompiledDrawData.stats.texturedVertexCount = outCompiledDrawData.texturedVertices.size(); + outCompiledDrawData.stats.triangleCount = + (outCompiledDrawData.stats.colorVertexCount + + outCompiledDrawData.stats.texturedVertexCount) / + 3u; + outCompiledDrawData.stats.batchCount = outCompiledDrawData.batches.size(); + outCompiledDrawData.stats.danglingClipDepth = clipStack.size(); +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIRHICommandCompiler.h b/new_editor/src/XCUIBackend/XCUIRHICommandCompiler.h new file mode 100644 index 00000000..12434a94 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIRHICommandCompiler.h @@ -0,0 +1,119 @@ +#pragma once + +#include + +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +class XCUIRHICommandCompiler { +public: + struct ColorVertex { + float position[2] = { 0.0f, 0.0f }; + float color[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; + }; + + struct TexturedVertex { + float position[2] = { 0.0f, 0.0f }; + float uv[2] = { 0.0f, 0.0f }; + float color[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; + }; + + enum class BatchKind : std::uint8_t { + Color = 0, + Textured + }; + + struct Batch { + BatchKind kind = BatchKind::Color; + std::uint32_t drawListIndex = 0; + std::uint32_t firstCommandIndex = 0; + std::uint32_t commandCount = 0; + std::uint32_t firstVertex = 0; + std::uint32_t vertexCount = 0; + UI::UIRect clipRect = {}; + UI::UITextureHandle texture = {}; + }; + + struct Stats { + std::size_t drawListCount = 0; + std::size_t commandCount = 0; + std::size_t compiledCommandCount = 0; + std::size_t skippedCommandCount = 0; + std::size_t culledCommandCount = 0; + std::size_t unsupportedCommandCount = 0; + std::size_t filledRectCommandCount = 0; + std::size_t rectOutlineCommandCount = 0; + std::size_t textCommandCount = 0; + std::size_t imageCommandCount = 0; + std::size_t clipPushCommandCount = 0; + std::size_t clipPopCommandCount = 0; + std::size_t colorVertexCount = 0; + std::size_t texturedVertexCount = 0; + std::size_t triangleCount = 0; + std::size_t batchCount = 0; + std::size_t maxClipDepth = 0; + std::size_t clipStackUnderflowCount = 0; + std::size_t danglingClipDepth = 0; + }; + + struct CompiledDrawData { + std::vector colorVertices = {}; + std::vector texturedVertices = {}; + std::vector batches = {}; + Stats stats = {}; + + void Clear(); + bool Empty() const; + }; + + struct TextRunContext { + float requestedFontSize = 0.0f; + float resolvedFontSize = 0.0f; + float lineHeight = 0.0f; + UI::UITextureHandle texture = {}; + }; + + struct TextGlyph { + float x0 = 0.0f; + float y0 = 0.0f; + float x1 = 0.0f; + float y1 = 0.0f; + float u0 = 0.0f; + float v0 = 0.0f; + float u1 = 0.0f; + float v1 = 0.0f; + float advanceX = 0.0f; + bool visible = true; + }; + + class TextGlyphProvider { + public: + virtual ~TextGlyphProvider() = default; + + virtual bool BeginText(float requestedFontSize, TextRunContext& outContext) const = 0; + virtual bool ResolveGlyph( + const TextRunContext& context, + std::uint32_t codepoint, + TextGlyph& outGlyph) const = 0; + }; + + struct CompileConfig { + UI::UIRect surfaceClipRect = {}; + const TextGlyphProvider* textGlyphProvider = nullptr; + bool mergeBatchesWithinDrawList = true; + }; + + void Compile( + const UI::UIDrawData& drawData, + const CompileConfig& config, + CompiledDrawData& outCompiledDrawData) const; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIRHICommandSupport.h b/new_editor/src/XCUIBackend/XCUIRHICommandSupport.h new file mode 100644 index 00000000..22dc3410 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIRHICommandSupport.h @@ -0,0 +1,267 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +enum class XCUIRHICommandCategory : std::uint8_t { + FilledRect = 0, + RectOutline, + Text, + Image, + PushClipRect, + PopClipRect, + Unknown +}; + +enum class XCUIRHICommandSupportReason : std::uint8_t { + Supported = 0, + UnsupportedImageTexture, + UnsupportedUnknownCommand +}; + +struct XCUIRHICommandClassification { + XCUIRHICommandCategory category = XCUIRHICommandCategory::Unknown; + XCUIRHICommandSupportReason supportReason = + XCUIRHICommandSupportReason::UnsupportedUnknownCommand; + + constexpr bool IsSupported() const { + return supportReason == XCUIRHICommandSupportReason::Supported; + } +}; + +struct XCUIRHICommandSupportStats { + std::size_t drawListCount = 0; + std::size_t commandCount = 0; + std::size_t filledRectCommandCount = 0; + std::size_t rectOutlineCommandCount = 0; + std::size_t textCommandCount = 0; + std::size_t imageCommandCount = 0; + std::size_t clipPushCommandCount = 0; + std::size_t clipPopCommandCount = 0; + std::size_t supportedCommandCount = 0; + std::size_t unsupportedCommandCount = 0; + std::size_t unsupportedImageCommandCount = 0; + std::size_t unsupportedUnknownCommandCount = 0; + + constexpr bool HasCommands() const { + return commandCount != 0u; + } + + constexpr bool SupportsAllCommands() const { + return unsupportedCommandCount == 0u; + } +}; + +struct XCUIRHICommandDiagnosticOptions { + const char* noCommandsMessage = "Overlay runtime produced no commands."; + const char* allSupportedMessage = "All commands preflight for native overlay."; + const char* unsupportedPrefix = "command(s) will be skipped by native overlay: "; + const char* unsupportedImageDescription = + "image command(s) missing valid ShaderResourceView textures"; + const char* unsupportedUnknownDescription = "unknown command type(s)"; +}; + +struct XCUIRHICommandSupportSummary { + XCUIRHICommandSupportStats stats = {}; + std::string diagnostic = {}; +}; + +inline XCUIRHICommandClassification ClassifyXCUIRHICommandSupport( + const UI::UIDrawCommand& command) { + XCUIRHICommandClassification classification = {}; + + switch (command.type) { + case UI::UIDrawCommandType::FilledRect: + classification.category = XCUIRHICommandCategory::FilledRect; + classification.supportReason = XCUIRHICommandSupportReason::Supported; + break; + case UI::UIDrawCommandType::RectOutline: + classification.category = XCUIRHICommandCategory::RectOutline; + classification.supportReason = XCUIRHICommandSupportReason::Supported; + break; + case UI::UIDrawCommandType::Text: + classification.category = XCUIRHICommandCategory::Text; + classification.supportReason = XCUIRHICommandSupportReason::Supported; + break; + case UI::UIDrawCommandType::Image: + classification.category = XCUIRHICommandCategory::Image; + classification.supportReason = + (command.texture.IsValid() && + command.texture.kind == UI::UITextureHandleKind::ShaderResourceView) + ? XCUIRHICommandSupportReason::Supported + : XCUIRHICommandSupportReason::UnsupportedImageTexture; + break; + case UI::UIDrawCommandType::PushClipRect: + classification.category = XCUIRHICommandCategory::PushClipRect; + classification.supportReason = XCUIRHICommandSupportReason::Supported; + break; + case UI::UIDrawCommandType::PopClipRect: + classification.category = XCUIRHICommandCategory::PopClipRect; + classification.supportReason = XCUIRHICommandSupportReason::Supported; + break; + default: + classification.category = XCUIRHICommandCategory::Unknown; + classification.supportReason = XCUIRHICommandSupportReason::UnsupportedUnknownCommand; + break; + } + + return classification; +} + +inline void AccumulateXCUIRHICommandSupport( + const XCUIRHICommandClassification& classification, + XCUIRHICommandSupportStats& stats) { + switch (classification.category) { + case XCUIRHICommandCategory::FilledRect: + ++stats.filledRectCommandCount; + break; + case XCUIRHICommandCategory::RectOutline: + ++stats.rectOutlineCommandCount; + break; + case XCUIRHICommandCategory::Text: + ++stats.textCommandCount; + break; + case XCUIRHICommandCategory::Image: + ++stats.imageCommandCount; + break; + case XCUIRHICommandCategory::PushClipRect: + ++stats.clipPushCommandCount; + break; + case XCUIRHICommandCategory::PopClipRect: + ++stats.clipPopCommandCount; + break; + case XCUIRHICommandCategory::Unknown: + break; + } + + ++stats.commandCount; + + if (classification.IsSupported()) { + ++stats.supportedCommandCount; + return; + } + + ++stats.unsupportedCommandCount; + + switch (classification.supportReason) { + case XCUIRHICommandSupportReason::Supported: + break; + case XCUIRHICommandSupportReason::UnsupportedImageTexture: + ++stats.unsupportedImageCommandCount; + break; + case XCUIRHICommandSupportReason::UnsupportedUnknownCommand: + ++stats.unsupportedUnknownCommandCount; + break; + } +} + +inline void AccumulateXCUIRHICommandSupport( + const UI::UIDrawCommand& command, + XCUIRHICommandSupportStats& stats) { + AccumulateXCUIRHICommandSupport(ClassifyXCUIRHICommandSupport(command), stats); +} + +inline void AccumulateXCUIRHICommandSupport( + const UI::UIDrawList& drawList, + XCUIRHICommandSupportStats& stats) { + ++stats.drawListCount; + for (const UI::UIDrawCommand& command : drawList.GetCommands()) { + AccumulateXCUIRHICommandSupport(command, stats); + } +} + +inline XCUIRHICommandSupportStats AnalyzeXCUIRHICommandSupport( + const UI::UIDrawList& drawList) { + XCUIRHICommandSupportStats stats = {}; + AccumulateXCUIRHICommandSupport(drawList, stats); + return stats; +} + +inline XCUIRHICommandSupportStats AnalyzeXCUIRHICommandSupport( + const UI::UIDrawData& drawData) { + XCUIRHICommandSupportStats stats = {}; + for (const UI::UIDrawList& drawList : drawData.GetDrawLists()) { + AccumulateXCUIRHICommandSupport(drawList, stats); + } + return stats; +} + +inline void AppendXCUIRHICommandDiagnosticIssue( + std::ostringstream& builder, + bool& hasPreviousIssue, + std::size_t count, + const char* description) { + if (count == 0u) { + return; + } + + if (hasPreviousIssue) { + builder << ", "; + } + + builder << count << ' ' << description; + hasPreviousIssue = true; +} + +inline std::string BuildXCUIRHICommandSupportDiagnostic( + const XCUIRHICommandSupportStats& stats, + const XCUIRHICommandDiagnosticOptions& options = {}) { + if (!stats.HasCommands()) { + return std::string(options.noCommandsMessage); + } + + if (stats.SupportsAllCommands()) { + return std::string(options.allSupportedMessage); + } + + std::ostringstream builder = {}; + builder << stats.unsupportedCommandCount << ' ' << options.unsupportedPrefix; + + bool hasPreviousIssue = false; + AppendXCUIRHICommandDiagnosticIssue( + builder, + hasPreviousIssue, + stats.unsupportedImageCommandCount, + options.unsupportedImageDescription); + AppendXCUIRHICommandDiagnosticIssue( + builder, + hasPreviousIssue, + stats.unsupportedUnknownCommandCount, + options.unsupportedUnknownDescription); + + if (!hasPreviousIssue) { + builder << "unsupported command(s)"; + } + + return builder.str(); +} + +inline XCUIRHICommandSupportSummary SummarizeXCUIRHICommandSupport( + const UI::UIDrawData& drawData, + const XCUIRHICommandDiagnosticOptions& options = {}) { + XCUIRHICommandSupportSummary summary = {}; + summary.stats = AnalyzeXCUIRHICommandSupport(drawData); + summary.diagnostic = BuildXCUIRHICommandSupportDiagnostic(summary.stats, options); + return summary; +} + +inline XCUIRHICommandSupportSummary SummarizeXCUIRHICommandSupport( + const UI::UIDrawList& drawList, + const XCUIRHICommandDiagnosticOptions& options = {}) { + XCUIRHICommandSupportSummary summary = {}; + summary.stats = AnalyzeXCUIRHICommandSupport(drawList); + summary.diagnostic = BuildXCUIRHICommandSupportDiagnostic(summary.stats, options); + return summary; +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIRHIRenderBackend.cpp b/new_editor/src/XCUIBackend/XCUIRHIRenderBackend.cpp new file mode 100644 index 00000000..88827808 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIRHIRenderBackend.cpp @@ -0,0 +1,582 @@ +#include "XCUIBackend/XCUIRHIRenderBackend.h" + +#include "XCUIBackend/XCUIRHICommandCompiler.h" + +#include +#include +#include + +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace { + +using CommandCompiler = XCUIRHICommandCompiler; + +struct OverlayConstants { Math::Vector4 viewportSize = Math::Vector4::Zero(); }; + +class TextGlyphProvider final : public CommandCompiler::TextGlyphProvider { +public: + TextGlyphProvider( + const IXCUITextAtlasProvider& atlasProvider, + IXCUITextAtlasProvider::FontHandle fontHandle, + const UI::UITextureHandle& textureHandle) + : m_atlasProvider(atlasProvider), m_fontHandle(fontHandle), m_textureHandle(textureHandle) {} + + bool BeginText(float requestedFontSize, CommandCompiler::TextRunContext& outContext) const override { + outContext = {}; + if (!m_fontHandle.IsValid() || !m_textureHandle.IsValid()) return false; + IXCUITextAtlasProvider::FontInfo fontInfo = {}; + if (!m_atlasProvider.GetFontInfo(m_fontHandle, fontInfo)) return false; + const float resolvedFontSize = requestedFontSize > 0.0f + ? requestedFontSize + : (fontInfo.nominalSize > 0.0f ? fontInfo.nominalSize : 16.0f); + IXCUITextAtlasProvider::BakedFontInfo baked = {}; + if (!m_atlasProvider.GetBakedFontInfo(m_fontHandle, resolvedFontSize, baked)) return false; + outContext.requestedFontSize = requestedFontSize; + outContext.resolvedFontSize = resolvedFontSize; + outContext.lineHeight = baked.lineHeight; + outContext.texture = m_textureHandle; + return true; + } + + bool ResolveGlyph( + const CommandCompiler::TextRunContext& context, + std::uint32_t codepoint, + CommandCompiler::TextGlyph& outGlyph) const override { + outGlyph = {}; + IXCUITextAtlasProvider::GlyphInfo glyph = {}; + if (!m_atlasProvider.FindGlyph(m_fontHandle, context.resolvedFontSize, codepoint, glyph)) return false; + outGlyph.x0 = glyph.x0; outGlyph.y0 = glyph.y0; outGlyph.x1 = glyph.x1; outGlyph.y1 = glyph.y1; + outGlyph.u0 = glyph.u0; outGlyph.v0 = glyph.v0; outGlyph.u1 = glyph.u1; outGlyph.v1 = glyph.v1; + outGlyph.advanceX = glyph.advanceX; outGlyph.visible = glyph.visible; + return true; + } + +private: + const IXCUITextAtlasProvider& m_atlasProvider; + IXCUITextAtlasProvider::FontHandle m_fontHandle = {}; + UI::UITextureHandle m_textureHandle = {}; +}; + +constexpr char kColorShader[] = + "cbuffer OverlayConstants : register(b0) { float4 gViewportSize; };" + "struct VSInput { float2 position : POSITION; float4 color : COLOR0; };" + "struct VSOutput { float4 position : SV_POSITION; float4 color : COLOR0; };" + "VSOutput MainVS(VSInput input) { VSOutput output;" + " float2 viewport = max(gViewportSize.xy, float2(1.0, 1.0));" + " float2 ndc = float2(input.position.x / viewport.x * 2.0 - 1.0, 1.0 - input.position.y / viewport.y * 2.0);" + " output.position = float4(ndc, 0.0, 1.0); output.color = input.color; return output; }" + "float4 MainPS(VSOutput input) : SV_TARGET0 { return input.color; }"; + +constexpr char kTexturedShader[] = + "cbuffer OverlayConstants : register(b0) { float4 gViewportSize; };" + "Texture2D gOverlayTexture : register(t0); SamplerState gOverlaySampler : register(s0);" + "struct VSInput { float2 position : POSITION; float2 uv : TEXCOORD0; float4 color : COLOR0; };" + "struct VSOutput { float4 position : SV_POSITION; float2 uv : TEXCOORD0; float4 color : COLOR0; };" + "VSOutput MainVS(VSInput input) { VSOutput output;" + " float2 viewport = max(gViewportSize.xy, float2(1.0, 1.0));" + " float2 ndc = float2(input.position.x / viewport.x * 2.0 - 1.0, 1.0 - input.position.y / viewport.y * 2.0);" + " output.position = float4(ndc, 0.0, 1.0); output.uv = input.uv; output.color = input.color; return output; }" + "float4 MainPS(VSOutput input) : SV_TARGET0 {" + " const float4 sampled = gOverlayTexture.Sample(gOverlaySampler, input.uv);" + " float4 color = sampled * input.color; if (color.a <= 0.001f) { discard; } return color; }"; + +RHI::Rect MakeSurfaceScissorRect(const ::XCEngine::Rendering::RenderSurface& surface) { + return RHI::Rect{ + 0, + 0, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()) + }; +} + +RHI::Rect ClampBatchClipRect( + const ::XCEngine::Rendering::RenderSurface& surface, + const UI::UIRect& clipRect) { + const float surfaceWidth = static_cast(surface.GetWidth()); + const float surfaceHeight = static_cast(surface.GetHeight()); + const float minX = (std::max)(0.0f, (std::min)(clipRect.x, surfaceWidth)); + const float minY = (std::max)(0.0f, (std::min)(clipRect.y, surfaceHeight)); + const float maxX = (std::max)(minX, (std::min)(clipRect.x + clipRect.width, surfaceWidth)); + const float maxY = (std::max)(minY, (std::min)(clipRect.y + clipRect.height, surfaceHeight)); + return RHI::Rect{ + static_cast(std::floor(minX)), + static_cast(std::floor(minY)), + static_cast(std::ceil(maxX)), + static_cast(std::ceil(maxY)) + }; +} + +bool RectEquals(const RHI::Rect& lhs, const RHI::Rect& rhs) { + return lhs.left == rhs.left && + lhs.top == rhs.top && + lhs.right == rhs.right && + lhs.bottom == rhs.bottom; +} + +} // namespace + +void XCUIRHIRenderBackend::SetTextAtlasProvider(const IXCUITextAtlasProvider* provider) { + if (m_textAtlasProvider == provider) return; + m_textAtlasProvider = provider; + ResetFontAtlasResources(); +} + +void XCUIRHIRenderBackend::Shutdown() { DestroyResources(); } +void XCUIRHIRenderBackend::ResetStats() { m_lastOverlayStats = {}; } + +bool XCUIRHIRenderBackend::Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const ::XCEngine::UI::UIDrawData& drawData) { + ResetStats(); + if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) return false; + const auto& colorAttachments = surface.GetColorAttachments(); + if (colorAttachments.empty() || colorAttachments[0] == nullptr || renderContext.commandList == nullptr) return false; + if (!EnsureInitialized(renderContext)) return false; + + bool fontAtlasReady = false; + const IXCUITextAtlasProvider* atlasProvider = ResolveActiveTextAtlasProvider(fontAtlasReady); + return RenderCompiledDrawData( + *renderContext.commandList, + colorAttachments[0], + surface, + drawData, + atlasProvider, + fontAtlasReady); +} + +const IXCUITextAtlasProvider* XCUIRHIRenderBackend::ResolveActiveTextAtlasProvider( + bool& outFontAtlasReady) { + outFontAtlasReady = false; + if (m_textAtlasProvider == nullptr) { + return nullptr; + } + + outFontAtlasReady = EnsureFontAtlasResources(*m_textAtlasProvider); + return m_textAtlasProvider; +} + +bool XCUIRHIRenderBackend::EnsureInitialized(const ::XCEngine::Rendering::RenderContext& renderContext) { + if (m_overlayPipelineState != nullptr && + m_overlayPipelineLayout != nullptr && + m_overlayConstantPool != nullptr && + m_overlayConstantSet != nullptr && + m_texturedOverlayPipelineState != nullptr && + m_texturedOverlayPipelineLayout != nullptr && + m_overlayTexturePool != nullptr && + m_overlaySamplerPool != nullptr && + m_overlaySamplerSet != nullptr && + m_overlaySampler != nullptr && + m_device == renderContext.device && + m_backendType == renderContext.backendType) { + return true; + } + DestroyResources(); + return CreateResources(renderContext); +} + +bool XCUIRHIRenderBackend::CreateResources(const ::XCEngine::Rendering::RenderContext& renderContext) { + if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) return false; + m_device = renderContext.device; + m_backendType = renderContext.backendType; + return CreateOverlayResources() && CreateTexturedOverlayResources(); +} + +bool XCUIRHIRenderBackend::CreateOverlayResources() { + RHI::DescriptorSetLayoutBinding binding = { 0, static_cast(RHI::DescriptorType::CBV), 1 }; + RHI::DescriptorSetLayoutDesc layout = { &binding, 1 }; + RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = &layout; + pipelineLayoutDesc.setLayoutCount = 1; + m_overlayPipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_overlayPipelineLayout == nullptr) { DestroyResources(); return false; } + + RHI::DescriptorPoolDesc poolDesc = {}; + poolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + poolDesc.descriptorCount = 1; + poolDesc.shaderVisible = false; + m_overlayConstantPool = m_device->CreateDescriptorPool(poolDesc); + if (m_overlayConstantPool == nullptr) { DestroyResources(); return false; } + m_overlayConstantSet = m_overlayConstantPool->AllocateSet(layout); + if (m_overlayConstantSet == nullptr) { DestroyResources(); return false; } + + RHI::GraphicsPipelineDesc desc = {}; + desc.pipelineLayout = m_overlayPipelineLayout; + desc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + desc.renderTargetCount = 1; + desc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + desc.depthStencilFormat = static_cast(RHI::Format::Unknown); + desc.sampleCount = 1; + desc.inputLayout.elements = { + { "POSITION", 0, static_cast(RHI::Format::R32G32_Float), 0, 0, 0, 0 }, + { "COLOR", 0, static_cast(RHI::Format::R32G32B32A32_Float), 0, 8, 0, 0 } + }; + desc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + desc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + desc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + desc.rasterizerState.depthClipEnable = true; + desc.blendState.blendEnable = true; + desc.blendState.srcBlend = static_cast(RHI::BlendFactor::SrcAlpha); + desc.blendState.dstBlend = static_cast(RHI::BlendFactor::InvSrcAlpha); + desc.blendState.srcBlendAlpha = static_cast(RHI::BlendFactor::One); + desc.blendState.dstBlendAlpha = static_cast(RHI::BlendFactor::InvSrcAlpha); + desc.blendState.blendOp = static_cast(RHI::BlendOp::Add); + desc.blendState.blendOpAlpha = static_cast(RHI::BlendOp::Add); + desc.blendState.colorWriteMask = 0xF; + desc.depthStencilState.depthTestEnable = false; + desc.depthStencilState.depthWriteEnable = false; + desc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::Always); + desc.vertexShader.source.assign(kColorShader, kColorShader + sizeof(kColorShader) - 1); + desc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + desc.vertexShader.entryPoint = L"MainVS"; + desc.vertexShader.profile = L"vs_5_0"; + desc.fragmentShader.source.assign(kColorShader, kColorShader + sizeof(kColorShader) - 1); + desc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + desc.fragmentShader.entryPoint = L"MainPS"; + desc.fragmentShader.profile = L"ps_5_0"; + m_overlayPipelineState = m_device->CreatePipelineState(desc); + if (m_overlayPipelineState == nullptr || !m_overlayPipelineState->IsValid()) { DestroyResources(); return false; } + return true; +} + +bool XCUIRHIRenderBackend::CreateTexturedOverlayResources() { + RHI::DescriptorSetLayoutBinding constantBinding = { 0, static_cast(RHI::DescriptorType::CBV), 1 }; + RHI::DescriptorSetLayoutBinding textureBinding = { 0, static_cast(RHI::DescriptorType::SRV), 1 }; + RHI::DescriptorSetLayoutBinding samplerBinding = { 0, static_cast(RHI::DescriptorType::Sampler), 1 }; + RHI::DescriptorSetLayoutDesc setLayouts[3] = { { &constantBinding, 1 }, { &textureBinding, 1 }, { &samplerBinding, 1 } }; + RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = setLayouts; + pipelineLayoutDesc.setLayoutCount = 3; + m_texturedOverlayPipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_texturedOverlayPipelineLayout == nullptr) { DestroyResources(); return false; } + + RHI::DescriptorPoolDesc texturePoolDesc = {}; + texturePoolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + texturePoolDesc.descriptorCount = 64; + texturePoolDesc.shaderVisible = true; + m_overlayTexturePool = m_device->CreateDescriptorPool(texturePoolDesc); + if (m_overlayTexturePool == nullptr) { DestroyResources(); return false; } + + RHI::DescriptorPoolDesc samplerPoolDesc = {}; + samplerPoolDesc.type = RHI::DescriptorHeapType::Sampler; + samplerPoolDesc.descriptorCount = 1; + samplerPoolDesc.shaderVisible = true; + m_overlaySamplerPool = m_device->CreateDescriptorPool(samplerPoolDesc); + if (m_overlaySamplerPool == nullptr) { DestroyResources(); return false; } + m_overlaySamplerSet = m_overlaySamplerPool->AllocateSet(setLayouts[2]); + if (m_overlaySamplerSet == nullptr) { DestroyResources(); return false; } + + RHI::SamplerDesc samplerDesc = {}; + samplerDesc.filter = static_cast(RHI::FilterMode::Linear); + samplerDesc.addressU = static_cast(RHI::TextureAddressMode::Clamp); + samplerDesc.addressV = static_cast(RHI::TextureAddressMode::Clamp); + samplerDesc.addressW = static_cast(RHI::TextureAddressMode::Clamp); + samplerDesc.comparisonFunc = static_cast(RHI::ComparisonFunc::Always); + samplerDesc.maxLod = 1000.0f; + m_overlaySampler = m_device->CreateSampler(samplerDesc); + if (m_overlaySampler == nullptr) { DestroyResources(); return false; } + m_overlaySamplerSet->UpdateSampler(0, m_overlaySampler); + + RHI::GraphicsPipelineDesc desc = {}; + desc.pipelineLayout = m_texturedOverlayPipelineLayout; + desc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + desc.renderTargetCount = 1; + desc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + desc.depthStencilFormat = static_cast(RHI::Format::Unknown); + desc.sampleCount = 1; + desc.inputLayout.elements = { + { "POSITION", 0, static_cast(RHI::Format::R32G32_Float), 0, 0, 0, 0 }, + { "TEXCOORD", 0, static_cast(RHI::Format::R32G32_Float), 0, 8, 0, 0 }, + { "COLOR", 0, static_cast(RHI::Format::R32G32B32A32_Float), 0, 16, 0, 0 } + }; + desc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + desc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + desc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + desc.rasterizerState.depthClipEnable = true; + desc.blendState.blendEnable = true; + desc.blendState.srcBlend = static_cast(RHI::BlendFactor::SrcAlpha); + desc.blendState.dstBlend = static_cast(RHI::BlendFactor::InvSrcAlpha); + desc.blendState.srcBlendAlpha = static_cast(RHI::BlendFactor::One); + desc.blendState.dstBlendAlpha = static_cast(RHI::BlendFactor::InvSrcAlpha); + desc.blendState.blendOp = static_cast(RHI::BlendOp::Add); + desc.blendState.blendOpAlpha = static_cast(RHI::BlendOp::Add); + desc.blendState.colorWriteMask = 0xF; + desc.depthStencilState.depthTestEnable = false; + desc.depthStencilState.depthWriteEnable = false; + desc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::Always); + desc.vertexShader.source.assign(kTexturedShader, kTexturedShader + sizeof(kTexturedShader) - 1); + desc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + desc.vertexShader.entryPoint = L"MainVS"; + desc.vertexShader.profile = L"vs_5_0"; + desc.fragmentShader.source.assign(kTexturedShader, kTexturedShader + sizeof(kTexturedShader) - 1); + desc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + desc.fragmentShader.entryPoint = L"MainPS"; + desc.fragmentShader.profile = L"ps_5_0"; + m_texturedOverlayPipelineState = m_device->CreatePipelineState(desc); + if (m_texturedOverlayPipelineState == nullptr || !m_texturedOverlayPipelineState->IsValid()) { DestroyResources(); return false; } + return true; +} + +bool XCUIRHIRenderBackend::EnsureOverlayVertexBufferCapacity(std::size_t requiredVertexCount) { + const std::uint64_t requiredBytes = static_cast(requiredVertexCount * sizeof(CommandCompiler::ColorVertex)); + if (requiredBytes == 0u) return true; + if (m_overlayVertexBuffer != nullptr && m_overlayVertexBufferCapacity >= requiredBytes) return true; + if (m_overlayVertexBufferView != nullptr) { m_overlayVertexBufferView->Shutdown(); delete m_overlayVertexBufferView; m_overlayVertexBufferView = nullptr; } + if (m_overlayVertexBuffer != nullptr) { m_overlayVertexBuffer->Shutdown(); delete m_overlayVertexBuffer; m_overlayVertexBuffer = nullptr; } + m_overlayVertexBufferCapacity = (std::max)(requiredBytes, 4096u); + RHI::BufferDesc bufferDesc = {}; + bufferDesc.size = m_overlayVertexBufferCapacity; + bufferDesc.stride = static_cast(sizeof(CommandCompiler::ColorVertex)); + bufferDesc.bufferType = static_cast(RHI::BufferType::Vertex); + m_overlayVertexBuffer = m_device->CreateBuffer(bufferDesc); + if (m_overlayVertexBuffer == nullptr) { m_overlayVertexBufferCapacity = 0u; return false; } + m_overlayVertexBuffer->SetStride(bufferDesc.stride); + m_overlayVertexBuffer->SetBufferType(RHI::BufferType::Vertex); + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.dimension = RHI::ResourceViewDimension::Buffer; + viewDesc.structureByteStride = bufferDesc.stride; + m_overlayVertexBufferView = m_device->CreateVertexBufferView(m_overlayVertexBuffer, viewDesc); + return m_overlayVertexBufferView != nullptr; +} + +bool XCUIRHIRenderBackend::EnsureTexturedOverlayVertexBufferCapacity(std::size_t requiredVertexCount) { + const std::uint64_t requiredBytes = static_cast(requiredVertexCount * sizeof(CommandCompiler::TexturedVertex)); + if (requiredBytes == 0u) return true; + if (m_texturedOverlayVertexBuffer != nullptr && m_texturedOverlayVertexBufferCapacity >= requiredBytes) return true; + if (m_texturedOverlayVertexBufferView != nullptr) { m_texturedOverlayVertexBufferView->Shutdown(); delete m_texturedOverlayVertexBufferView; m_texturedOverlayVertexBufferView = nullptr; } + if (m_texturedOverlayVertexBuffer != nullptr) { m_texturedOverlayVertexBuffer->Shutdown(); delete m_texturedOverlayVertexBuffer; m_texturedOverlayVertexBuffer = nullptr; } + m_texturedOverlayVertexBufferCapacity = (std::max)(requiredBytes, 4096u); + RHI::BufferDesc bufferDesc = {}; + bufferDesc.size = m_texturedOverlayVertexBufferCapacity; + bufferDesc.stride = static_cast(sizeof(CommandCompiler::TexturedVertex)); + bufferDesc.bufferType = static_cast(RHI::BufferType::Vertex); + m_texturedOverlayVertexBuffer = m_device->CreateBuffer(bufferDesc); + if (m_texturedOverlayVertexBuffer == nullptr) { m_texturedOverlayVertexBufferCapacity = 0u; return false; } + m_texturedOverlayVertexBuffer->SetStride(bufferDesc.stride); + m_texturedOverlayVertexBuffer->SetBufferType(RHI::BufferType::Vertex); + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.dimension = RHI::ResourceViewDimension::Buffer; + viewDesc.structureByteStride = bufferDesc.stride; + m_texturedOverlayVertexBufferView = m_device->CreateVertexBufferView(m_texturedOverlayVertexBuffer, viewDesc); + return m_texturedOverlayVertexBufferView != nullptr; +} + +bool XCUIRHIRenderBackend::EnsureFontAtlasResources(const IXCUITextAtlasProvider& atlasProvider) { + if (m_overlayTexturePool == nullptr) return false; + IXCUITextAtlasProvider::AtlasTextureView atlasView = {}; + if (!atlasProvider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView) || + !atlasView.IsValid() || atlasView.bytesPerPixel != 4) { + return false; + } + const TextFontHandle font = atlasProvider.GetDefaultFont(); + if (!font.IsValid()) return false; + if (m_overlayFontTexture != nullptr && m_overlayFontTextureView != nullptr && m_overlayFontTextureSet != nullptr && + m_overlayFont.value == font.value && + m_fontAtlasStorageKey == atlasView.atlasStorageKey && + m_fontAtlasPixelDataKey == atlasView.pixelDataKey) { + return true; + } + ResetFontAtlasResources(); + RHI::TextureDesc textureDesc = {}; + textureDesc.width = static_cast(atlasView.width); + textureDesc.height = static_cast(atlasView.height); + textureDesc.depth = 1; textureDesc.mipLevels = 1; textureDesc.arraySize = 1; + textureDesc.format = static_cast(RHI::Format::R8G8B8A8_UNorm); + textureDesc.textureType = static_cast(RHI::TextureType::Texture2D); + textureDesc.sampleCount = 1; + m_overlayFontTexture = m_device->CreateTexture( + textureDesc, + atlasView.pixels, + static_cast(atlasView.width) * static_cast(atlasView.height) * static_cast(atlasView.bytesPerPixel), + static_cast(atlasView.stride)); + if (m_overlayFontTexture == nullptr) { ResetFontAtlasResources(); return false; } + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.format = static_cast(RHI::Format::R8G8B8A8_UNorm); + viewDesc.dimension = RHI::ResourceViewDimension::Texture2D; + viewDesc.mipLevel = 0; + m_overlayFontTextureView = m_device->CreateShaderResourceView(m_overlayFontTexture, viewDesc); + if (m_overlayFontTextureView == nullptr) { ResetFontAtlasResources(); return false; } + RHI::DescriptorSetLayoutBinding textureBinding = { 0, static_cast(RHI::DescriptorType::SRV), 1 }; + RHI::DescriptorSetLayoutDesc textureLayout = { &textureBinding, 1 }; + m_overlayFontTextureSet = m_overlayTexturePool->AllocateSet(textureLayout); + if (m_overlayFontTextureSet == nullptr) { ResetFontAtlasResources(); return false; } + m_overlayFontTextureSet->Update(0, m_overlayFontTextureView); + m_overlayFont = font; + m_overlayFontTextureHandle.nativeHandle = reinterpret_cast(m_overlayFontTextureView); + m_overlayFontTextureHandle.width = static_cast(atlasView.width); + m_overlayFontTextureHandle.height = static_cast(atlasView.height); + m_overlayFontTextureHandle.kind = UI::UITextureHandleKind::ShaderResourceView; + m_fontAtlasStorageKey = atlasView.atlasStorageKey; + m_fontAtlasPixelDataKey = atlasView.pixelDataKey; + return true; +} + +::XCEngine::RHI::RHIDescriptorSet* XCUIRHIRenderBackend::ResolveTextureSet( + const ::XCEngine::UI::UITextureHandle& texture, + bool* outCacheHit) { + if (outCacheHit != nullptr) { + *outCacheHit = false; + } + if (!texture.IsValid() || texture.kind != UI::UITextureHandleKind::ShaderResourceView || m_overlayTexturePool == nullptr) return nullptr; + if (m_overlayFontTextureSet != nullptr && m_overlayFontTextureHandle.IsValid() && texture.nativeHandle == m_overlayFontTextureHandle.nativeHandle) { + if (outCacheHit != nullptr) { + *outCacheHit = true; + } + return m_overlayFontTextureSet; + } + for (const ExternalTextureBinding& binding : m_externalTextureBindings) { + if (binding.handleKey == texture.nativeHandle && binding.textureSet != nullptr && binding.shaderView != nullptr) { + if (outCacheHit != nullptr) { + *outCacheHit = true; + } + return binding.textureSet; + } + } + RHI::RHIResourceView* shaderView = reinterpret_cast(texture.nativeHandle); + if (shaderView == nullptr || !shaderView->IsValid() || shaderView->GetViewType() != RHI::ResourceViewType::ShaderResource) return nullptr; + RHI::DescriptorSetLayoutBinding textureBinding = { 0, static_cast(RHI::DescriptorType::SRV), 1 }; + RHI::DescriptorSetLayoutDesc textureLayout = { &textureBinding, 1 }; + RHI::RHIDescriptorSet* textureSet = m_overlayTexturePool->AllocateSet(textureLayout); + if (textureSet == nullptr) return nullptr; + textureSet->Update(0, shaderView); + m_externalTextureBindings.push_back({ texture.nativeHandle, shaderView, textureSet }); + return textureSet; +} + +bool XCUIRHIRenderBackend::RenderCompiledDrawData( + ::XCEngine::RHI::RHICommandList& commandList, + ::XCEngine::RHI::RHIResourceView* renderTarget, + const ::XCEngine::Rendering::RenderSurface& surface, + const ::XCEngine::UI::UIDrawData& drawData, + const IXCUITextAtlasProvider* atlasProvider, + bool fontAtlasReady) { + CommandCompiler compiler = {}; + CommandCompiler::CompileConfig config = {}; + config.surfaceClipRect = UI::UIRect(0.0f, 0.0f, static_cast(surface.GetWidth()), static_cast(surface.GetHeight())); + std::optional glyphProvider = std::nullopt; + if (fontAtlasReady && atlasProvider != nullptr) { + glyphProvider.emplace(*atlasProvider, m_overlayFont, m_overlayFontTextureHandle); + config.textGlyphProvider = &(*glyphProvider); + } + CommandCompiler::CompiledDrawData compiled = {}; + compiler.Compile(drawData, config, compiled); + m_lastOverlayStats.drawListCount = compiled.stats.drawListCount; + m_lastOverlayStats.commandCount = compiled.stats.commandCount; + m_lastOverlayStats.batchCount = compiled.stats.batchCount; + m_lastOverlayStats.skippedCommandCount = compiled.stats.skippedCommandCount; + m_lastOverlayStats.vertexCount = compiled.stats.colorVertexCount + compiled.stats.texturedVertexCount; + m_lastOverlayStats.triangleCount = compiled.stats.triangleCount; + if (compiled.Empty()) return true; + if (!compiled.colorVertices.empty() && !EnsureOverlayVertexBufferCapacity(compiled.colorVertices.size())) return false; + if (!compiled.texturedVertices.empty() && !EnsureTexturedOverlayVertexBufferCapacity(compiled.texturedVertices.size())) return false; + if (!compiled.colorVertices.empty()) m_overlayVertexBuffer->SetData(compiled.colorVertices.data(), compiled.colorVertices.size() * sizeof(CommandCompiler::ColorVertex)); + if (!compiled.texturedVertices.empty()) m_texturedOverlayVertexBuffer->SetData(compiled.texturedVertices.data(), compiled.texturedVertices.size() * sizeof(CommandCompiler::TexturedVertex)); + OverlayConstants constants = {}; + constants.viewportSize = Math::Vector4(static_cast(surface.GetWidth()), static_cast(surface.GetHeight()), 0.0f, 0.0f); + m_overlayConstantSet->WriteConstant(0, &constants, sizeof(constants)); + commandList.SetRenderTargets(1, &renderTarget, nullptr); + const RHI::Viewport viewport = { 0.0f, 0.0f, static_cast(surface.GetWidth()), static_cast(surface.GetHeight()), 0.0f, 1.0f }; + const RHI::Rect fullSurfaceScissorRect = MakeSurfaceScissorRect(surface); + commandList.SetViewport(viewport); + commandList.SetScissorRect(fullSurfaceScissorRect); + commandList.SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + RHI::Rect currentScissorRect = fullSurfaceScissorRect; + for (const CommandCompiler::Batch& batch : compiled.batches) { + if (batch.vertexCount == 0u) continue; + const RHI::Rect batchScissorRect = ClampBatchClipRect(surface, batch.clipRect); + if (!RectEquals(currentScissorRect, batchScissorRect)) { + commandList.SetScissorRect(batchScissorRect); + currentScissorRect = batchScissorRect; + } + if (!RectEquals(batchScissorRect, fullSurfaceScissorRect)) { + ++m_lastOverlayStats.scissoredBatchCount; + } + if (batch.kind == CommandCompiler::BatchKind::Color) { + ++m_lastOverlayStats.colorBatchCount; + m_lastOverlayStats.renderedCommandCount += batch.commandCount; + commandList.SetPipelineState(m_overlayPipelineState); + RHI::RHIResourceView* vertexBuffers[] = { m_overlayVertexBufferView }; + const std::uint64_t offsets[] = { 0u }; + const std::uint32_t strides[] = { static_cast(sizeof(CommandCompiler::ColorVertex)) }; + commandList.SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + RHI::RHIDescriptorSet* descriptorSets[] = { m_overlayConstantSet }; + commandList.SetGraphicsDescriptorSets(0, 1, descriptorSets, m_overlayPipelineLayout); + commandList.Draw(batch.vertexCount, 1, batch.firstVertex, 0); + continue; + } + ++m_lastOverlayStats.texturedBatchCount; + ++m_lastOverlayStats.textureResolveCount; + bool textureCacheHit = false; + RHI::RHIDescriptorSet* textureSet = ResolveTextureSet(batch.texture, &textureCacheHit); + if (textureCacheHit) { + ++m_lastOverlayStats.textureCacheHitCount; + } + if (textureSet == nullptr || m_overlaySamplerSet == nullptr) { + m_lastOverlayStats.skippedCommandCount += batch.commandCount; + continue; + } + m_lastOverlayStats.renderedCommandCount += batch.commandCount; + commandList.SetPipelineState(m_texturedOverlayPipelineState); + RHI::RHIResourceView* vertexBuffers[] = { m_texturedOverlayVertexBufferView }; + const std::uint64_t offsets[] = { 0u }; + const std::uint32_t strides[] = { static_cast(sizeof(CommandCompiler::TexturedVertex)) }; + commandList.SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + RHI::RHIDescriptorSet* descriptorSets[] = { m_overlayConstantSet, textureSet, m_overlaySamplerSet }; + commandList.SetGraphicsDescriptorSets(0, 3, descriptorSets, m_texturedOverlayPipelineLayout); + commandList.Draw(batch.vertexCount, 1, batch.firstVertex, 0); + } + if (!RectEquals(currentScissorRect, fullSurfaceScissorRect)) { + commandList.SetScissorRect(fullSurfaceScissorRect); + } + return true; +} + +void XCUIRHIRenderBackend::ResetFontAtlasResources() { + if (m_overlayFontTextureSet != nullptr) { m_overlayFontTextureSet->Shutdown(); delete m_overlayFontTextureSet; m_overlayFontTextureSet = nullptr; } + if (m_overlayFontTextureView != nullptr) { m_overlayFontTextureView->Shutdown(); delete m_overlayFontTextureView; m_overlayFontTextureView = nullptr; } + if (m_overlayFontTexture != nullptr) { m_overlayFontTexture->Shutdown(); delete m_overlayFontTexture; m_overlayFontTexture = nullptr; } + m_overlayFont = {}; + m_overlayFontTextureHandle = {}; + m_fontAtlasStorageKey = 0; + m_fontAtlasPixelDataKey = 0; +} + +void XCUIRHIRenderBackend::DestroyResources() { + for (ExternalTextureBinding& binding : m_externalTextureBindings) { + if (binding.textureSet != nullptr) { binding.textureSet->Shutdown(); delete binding.textureSet; binding.textureSet = nullptr; } + binding.shaderView = nullptr; + binding.handleKey = 0; + } + m_externalTextureBindings.clear(); + if (m_texturedOverlayVertexBufferView != nullptr) { m_texturedOverlayVertexBufferView->Shutdown(); delete m_texturedOverlayVertexBufferView; m_texturedOverlayVertexBufferView = nullptr; } + if (m_texturedOverlayVertexBuffer != nullptr) { m_texturedOverlayVertexBuffer->Shutdown(); delete m_texturedOverlayVertexBuffer; m_texturedOverlayVertexBuffer = nullptr; } + if (m_overlayVertexBufferView != nullptr) { m_overlayVertexBufferView->Shutdown(); delete m_overlayVertexBufferView; m_overlayVertexBufferView = nullptr; } + if (m_overlayVertexBuffer != nullptr) { m_overlayVertexBuffer->Shutdown(); delete m_overlayVertexBuffer; m_overlayVertexBuffer = nullptr; } + ResetFontAtlasResources(); + if (m_texturedOverlayPipelineState != nullptr) { m_texturedOverlayPipelineState->Shutdown(); delete m_texturedOverlayPipelineState; m_texturedOverlayPipelineState = nullptr; } + if (m_overlaySamplerSet != nullptr) { m_overlaySamplerSet->Shutdown(); delete m_overlaySamplerSet; m_overlaySamplerSet = nullptr; } + if (m_overlaySampler != nullptr) { m_overlaySampler->Shutdown(); delete m_overlaySampler; m_overlaySampler = nullptr; } + if (m_overlaySamplerPool != nullptr) { m_overlaySamplerPool->Shutdown(); delete m_overlaySamplerPool; m_overlaySamplerPool = nullptr; } + if (m_overlayTexturePool != nullptr) { m_overlayTexturePool->Shutdown(); delete m_overlayTexturePool; m_overlayTexturePool = nullptr; } + if (m_texturedOverlayPipelineLayout != nullptr) { m_texturedOverlayPipelineLayout->Shutdown(); delete m_texturedOverlayPipelineLayout; m_texturedOverlayPipelineLayout = nullptr; } + if (m_overlayPipelineState != nullptr) { m_overlayPipelineState->Shutdown(); delete m_overlayPipelineState; m_overlayPipelineState = nullptr; } + if (m_overlayConstantSet != nullptr) { m_overlayConstantSet->Shutdown(); delete m_overlayConstantSet; m_overlayConstantSet = nullptr; } + if (m_overlayConstantPool != nullptr) { m_overlayConstantPool->Shutdown(); delete m_overlayConstantPool; m_overlayConstantPool = nullptr; } + if (m_overlayPipelineLayout != nullptr) { m_overlayPipelineLayout->Shutdown(); delete m_overlayPipelineLayout; m_overlayPipelineLayout = nullptr; } + m_overlayVertexBufferCapacity = 0u; + m_texturedOverlayVertexBufferCapacity = 0u; + m_lastOverlayStats = {}; + m_device = nullptr; + m_backendType = RHI::RHIType::D3D12; +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIRHIRenderBackend.h b/new_editor/src/XCUIBackend/XCUIRHIRenderBackend.h new file mode 100644 index 00000000..8b1c50db --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIRHIRenderBackend.h @@ -0,0 +1,120 @@ +#pragma once + +#include "IXCUITextAtlasProvider.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +class XCUIRHIRenderBackend { +public: + struct OverlayStats { + std::size_t drawListCount = 0; + std::size_t commandCount = 0; + std::size_t batchCount = 0; + std::size_t colorBatchCount = 0; + std::size_t texturedBatchCount = 0; + std::size_t scissoredBatchCount = 0; + std::size_t renderedCommandCount = 0; + std::size_t skippedCommandCount = 0; + std::size_t textureResolveCount = 0; + std::size_t textureCacheHitCount = 0; + std::size_t vertexCount = 0; + std::size_t triangleCount = 0; + }; + + void SetTextAtlasProvider(const IXCUITextAtlasProvider* provider); + const IXCUITextAtlasProvider* GetTextAtlasProvider() const { return m_textAtlasProvider; } + void Shutdown(); + void ResetStats(); + bool Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const ::XCEngine::UI::UIDrawData& drawData); + const OverlayStats& GetLastStats() const { return m_lastOverlayStats; } + const OverlayStats& GetLastOverlayStats() const { return m_lastOverlayStats; } + +private: + using TextFontHandle = IXCUITextAtlasProvider::FontHandle; + + struct ExternalTextureBinding { + std::uintptr_t handleKey = 0; + ::XCEngine::RHI::RHIResourceView* shaderView = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* textureSet = nullptr; + }; + + bool EnsureInitialized(const ::XCEngine::Rendering::RenderContext& renderContext); + bool CreateResources(const ::XCEngine::Rendering::RenderContext& renderContext); + bool CreateOverlayResources(); + bool CreateTexturedOverlayResources(); + const IXCUITextAtlasProvider* ResolveActiveTextAtlasProvider(bool& outFontAtlasReady); + bool EnsureOverlayVertexBufferCapacity(std::size_t requiredVertexCount); + bool EnsureTexturedOverlayVertexBufferCapacity(std::size_t requiredVertexCount); + bool EnsureFontAtlasResources(const IXCUITextAtlasProvider& atlasProvider); + void ResetFontAtlasResources(); + ::XCEngine::RHI::RHIDescriptorSet* ResolveTextureSet( + const ::XCEngine::UI::UITextureHandle& texture, + bool* outCacheHit = nullptr); + bool RenderCompiledDrawData( + ::XCEngine::RHI::RHICommandList& commandList, + ::XCEngine::RHI::RHIResourceView* renderTarget, + const ::XCEngine::Rendering::RenderSurface& surface, + const ::XCEngine::UI::UIDrawData& drawData, + const IXCUITextAtlasProvider* atlasProvider, + bool fontAtlasReady); + void DestroyResources(); + + const IXCUITextAtlasProvider* m_textAtlasProvider = nullptr; + ::XCEngine::RHI::RHIDevice* m_device = nullptr; + ::XCEngine::RHI::RHIType m_backendType = ::XCEngine::RHI::RHIType::D3D12; + ::XCEngine::RHI::RHIPipelineLayout* m_overlayPipelineLayout = nullptr; + ::XCEngine::RHI::RHIDescriptorPool* m_overlayConstantPool = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* m_overlayConstantSet = nullptr; + ::XCEngine::RHI::RHIPipelineState* m_overlayPipelineState = nullptr; + ::XCEngine::RHI::RHIPipelineLayout* m_texturedOverlayPipelineLayout = nullptr; + ::XCEngine::RHI::RHIDescriptorPool* m_overlayTexturePool = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* m_overlayFontTextureSet = nullptr; + ::XCEngine::RHI::RHIDescriptorPool* m_overlaySamplerPool = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* m_overlaySamplerSet = nullptr; + ::XCEngine::RHI::RHIPipelineState* m_texturedOverlayPipelineState = nullptr; + ::XCEngine::RHI::RHISampler* m_overlaySampler = nullptr; + ::XCEngine::RHI::RHITexture* m_overlayFontTexture = nullptr; + ::XCEngine::RHI::RHIResourceView* m_overlayFontTextureView = nullptr; + ::XCEngine::UI::UITextureHandle m_overlayFontTextureHandle = {}; + ::XCEngine::RHI::RHIBuffer* m_overlayVertexBuffer = nullptr; + ::XCEngine::RHI::RHIResourceView* m_overlayVertexBufferView = nullptr; + ::XCEngine::RHI::RHIBuffer* m_texturedOverlayVertexBuffer = nullptr; + ::XCEngine::RHI::RHIResourceView* m_texturedOverlayVertexBufferView = nullptr; + std::uint64_t m_overlayVertexBufferCapacity = 0; + std::uint64_t m_texturedOverlayVertexBufferCapacity = 0; + TextFontHandle m_overlayFont = {}; + std::uintptr_t m_fontAtlasStorageKey = 0; + std::uintptr_t m_fontAtlasPixelDataKey = 0; + int m_fontAtlasWidth = 0; + int m_fontAtlasHeight = 0; + int m_fontAtlasStride = 0; + std::vector m_externalTextureBindings = {}; + OverlayStats m_lastOverlayStats = {}; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp b/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp new file mode 100644 index 00000000..2394c298 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp @@ -0,0 +1,222 @@ +#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h" + +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace { + +IXCUITextAtlasProvider::FontHandle MakeFontHandle(const ImFont* font) { + IXCUITextAtlasProvider::FontHandle handle = {}; + handle.value = reinterpret_cast(font); + return handle; +} + +ImFont* ResolveFontHandle(IXCUITextAtlasProvider::FontHandle handle) { + return reinterpret_cast(handle.value); +} + +float ResolveNominalFontSize(const ImFont& font) { + return font.LegacySize > 0.0f ? font.LegacySize : 16.0f; +} + +float ResolveRequestedFontSize(const ImFont& font, float requestedFontSize) { + return requestedFontSize > 0.0f ? requestedFontSize : ResolveNominalFontSize(font); +} + +bool IsFontOwnedByAtlas(const ImFont* font, const ImFontAtlas* atlas) { + return font != nullptr && atlas != nullptr && font->OwnerAtlas == atlas; +} + +ImFontBaked* ResolveBakedFont( + IXCUITextAtlasProvider::FontHandle fontHandle, + ImFontAtlas* atlas, + float requestedFontSize) { + ImFont* font = ResolveFontHandle(fontHandle); + if (!IsFontOwnedByAtlas(font, atlas)) { + return nullptr; + } + + const float resolvedFontSize = ResolveRequestedFontSize(*font, requestedFontSize); + if (resolvedFontSize <= 0.0f) { + return nullptr; + } + + return font->GetFontBaked(resolvedFontSize); +} + +} // namespace + +XCUIStandaloneTextAtlasProvider::XCUIStandaloneTextAtlasProvider() { + RebuildDefaultEditorAtlas(); +} + +void XCUIStandaloneTextAtlasProvider::Reset() { + m_atlas.Clear(); + m_defaultFont = nullptr; + m_ready = false; +} + +bool XCUIStandaloneTextAtlasProvider::RebuildDefaultEditorAtlas() { + Reset(); + m_ready = BuildDefaultXCUIEditorFontAtlas(m_atlas, m_defaultFont); + return m_ready; +} + +bool XCUIStandaloneTextAtlasProvider::IsReady() const { + return m_ready && ResolveDefaultFont() != nullptr; +} + +bool XCUIStandaloneTextAtlasProvider::GetAtlasTextureView( + PixelFormat preferredFormat, + AtlasTextureView& outView) const { + outView = {}; + + ImFontAtlas* atlas = ResolveAtlas(); + if (atlas == nullptr) { + return false; + } + + unsigned char* pixels = nullptr; + int width = 0; + int height = 0; + int bytesPerPixel = 0; + PixelFormat resolvedFormat = preferredFormat; + + switch (preferredFormat) { + case PixelFormat::Alpha8: + atlas->GetTexDataAsAlpha8(&pixels, &width, &height, &bytesPerPixel); + resolvedFormat = PixelFormat::Alpha8; + break; + case PixelFormat::RGBA32: + case PixelFormat::Unknown: + default: + atlas->GetTexDataAsRGBA32(&pixels, &width, &height, &bytesPerPixel); + resolvedFormat = PixelFormat::RGBA32; + break; + } + + if (pixels == nullptr || width <= 0 || height <= 0 || bytesPerPixel <= 0) { + return false; + } + + outView.pixels = pixels; + outView.width = width; + outView.height = height; + outView.stride = width * bytesPerPixel; + outView.bytesPerPixel = bytesPerPixel; + outView.format = resolvedFormat; + outView.atlasStorageKey = reinterpret_cast(atlas); + outView.pixelDataKey = reinterpret_cast(pixels); + return true; +} + +std::size_t XCUIStandaloneTextAtlasProvider::GetFontCount() const { + const ImFontAtlas* atlas = ResolveAtlas(); + return atlas != nullptr ? static_cast(atlas->Fonts.Size) : 0u; +} + +IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetFont(std::size_t index) const { + const ImFontAtlas* atlas = ResolveAtlas(); + if (atlas == nullptr || index >= static_cast(atlas->Fonts.Size)) { + return {}; + } + + return MakeFontHandle(atlas->Fonts[static_cast(index)]); +} + +IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetDefaultFont() const { + return MakeFontHandle(ResolveDefaultFont()); +} + +bool XCUIStandaloneTextAtlasProvider::GetFontInfo(FontHandle fontHandle, FontInfo& outInfo) const { + outInfo = {}; + + ImFontAtlas* atlas = ResolveAtlas(); + ImFont* font = ResolveFontHandle(fontHandle); + if (!IsFontOwnedByAtlas(font, atlas)) { + return false; + } + + outInfo.handle = fontHandle; + outInfo.nominalSize = ResolveNominalFontSize(*font); + return true; +} + +bool XCUIStandaloneTextAtlasProvider::GetBakedFontInfo( + FontHandle fontHandle, + float fontSize, + BakedFontInfo& outInfo) const { + outInfo = {}; + + ImFontBaked* bakedFont = ResolveBakedFont(fontHandle, ResolveAtlas(), fontSize); + if (bakedFont == nullptr) { + return false; + } + + outInfo.lineHeight = bakedFont->Size; + outInfo.ascent = bakedFont->Ascent; + outInfo.descent = bakedFont->Descent; + outInfo.rasterizerDensity = bakedFont->RasterizerDensity; + return true; +} + +bool XCUIStandaloneTextAtlasProvider::FindGlyph( + FontHandle fontHandle, + float fontSize, + std::uint32_t codepoint, + GlyphInfo& outInfo) const { + outInfo = {}; + + if (codepoint > IM_UNICODE_CODEPOINT_MAX) { + return false; + } + + ImFontBaked* bakedFont = ResolveBakedFont(fontHandle, ResolveAtlas(), fontSize); + if (bakedFont == nullptr) { + return false; + } + + ImFontGlyph* glyph = bakedFont->FindGlyph(static_cast(codepoint)); + if (glyph == nullptr) { + return false; + } + + outInfo.requestedCodepoint = codepoint; + outInfo.resolvedCodepoint = glyph->Codepoint; + outInfo.visible = glyph->Visible != 0; + outInfo.colored = glyph->Colored != 0; + outInfo.advanceX = glyph->AdvanceX; + outInfo.x0 = glyph->X0; + outInfo.y0 = glyph->Y0; + outInfo.x1 = glyph->X1; + outInfo.y1 = glyph->Y1; + outInfo.u0 = glyph->U0; + outInfo.v0 = glyph->V0; + outInfo.u1 = glyph->U1; + outInfo.v1 = glyph->V1; + return true; +} + +::ImFontAtlas* XCUIStandaloneTextAtlasProvider::ResolveAtlas() const { + return m_ready ? &m_atlas : nullptr; +} + +::ImFont* XCUIStandaloneTextAtlasProvider::ResolveDefaultFont() const { + ImFontAtlas* atlas = ResolveAtlas(); + if (atlas == nullptr) { + return nullptr; + } + + if (IsFontOwnedByAtlas(m_defaultFont, atlas)) { + return m_defaultFont; + } + + return atlas->Fonts.empty() ? nullptr : atlas->Fonts[0]; +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h b/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h new file mode 100644 index 00000000..2a6baff2 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h @@ -0,0 +1,42 @@ +#pragma once + +#include "IXCUITextAtlasProvider.h" +#include "XCUIEditorFontSetup.h" + +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +class XCUIStandaloneTextAtlasProvider final : public IXCUITextAtlasProvider { +public: + XCUIStandaloneTextAtlasProvider(); + + XCUIStandaloneTextAtlasProvider(const XCUIStandaloneTextAtlasProvider&) = delete; + XCUIStandaloneTextAtlasProvider& operator=(const XCUIStandaloneTextAtlasProvider&) = delete; + + void Reset(); + bool RebuildDefaultEditorAtlas(); + bool IsReady() const; + + bool GetAtlasTextureView(PixelFormat preferredFormat, AtlasTextureView& outView) const override; + std::size_t GetFontCount() const override; + FontHandle GetFont(std::size_t index) const override; + FontHandle GetDefaultFont() const override; + bool GetFontInfo(FontHandle font, FontInfo& outInfo) const override; + bool GetBakedFontInfo(FontHandle font, float fontSize, BakedFontInfo& outInfo) const override; + bool FindGlyph(FontHandle font, float fontSize, std::uint32_t codepoint, GlyphInfo& outInfo) const override; + +private: + ::ImFontAtlas* ResolveAtlas() const; + ::ImFont* ResolveDefaultFont() const; + + mutable ::ImFontAtlas m_atlas = {}; + ::ImFont* m_defaultFont = nullptr; + bool m_ready = false; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/main.cpp b/new_editor/src/main.cpp new file mode 100644 index 00000000..52a3ef0a --- /dev/null +++ b/new_editor/src/main.cpp @@ -0,0 +1,6 @@ +#include "Application.h" + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { + XCEngine::NewEditor::Application app; + return app.Run(hInstance, nCmdShow); +} diff --git a/new_editor/src/panels/Panel.cpp b/new_editor/src/panels/Panel.cpp new file mode 100644 index 00000000..8d28032e --- /dev/null +++ b/new_editor/src/panels/Panel.cpp @@ -0,0 +1,44 @@ +#include "Panel.h" + +#include + +namespace XCEngine { +namespace NewEditor { + +Panel::Panel(std::string name, bool visible) + : m_name(std::move(name)) + , m_visible(visible) { +} + +Panel::~Panel() = default; + +const std::string& Panel::GetName() const { + return m_name; +} + +void Panel::SetName(const std::string& name) { + m_name = name; +} + +bool Panel::IsVisible() const { + return m_visible; +} + +void Panel::SetVisible(bool visible) { + m_visible = visible; +} + +void Panel::ToggleVisible() { + m_visible = !m_visible; +} + +void Panel::RenderIfVisible() { + if (!m_visible) { + return; + } + + Render(); +} + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/panels/Panel.h b/new_editor/src/panels/Panel.h new file mode 100644 index 00000000..136fa85f --- /dev/null +++ b/new_editor/src/panels/Panel.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace XCEngine { +namespace NewEditor { + +class Panel { +public: + explicit Panel(std::string name, bool visible = true); + virtual ~Panel(); + + const std::string& GetName() const; + void SetName(const std::string& name); + + bool IsVisible() const; + void SetVisible(bool visible); + void ToggleVisible(); + + void RenderIfVisible(); + virtual void Render() = 0; + +private: + std::string m_name; + bool m_visible = true; +}; + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/panels/XCUIDemoPanel.cpp b/new_editor/src/panels/XCUIDemoPanel.cpp new file mode 100644 index 00000000..7498724f --- /dev/null +++ b/new_editor/src/panels/XCUIDemoPanel.cpp @@ -0,0 +1,438 @@ +#include "XCUIDemoPanel.h" + +#include "XCUIBackend/ImGuiXCUIInputAdapter.h" + +#include + +#include + +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace NewEditor { + +namespace { + +constexpr float kCanvasHudHeight = 82.0f; +constexpr float kCanvasHudPadding = 10.0f; +constexpr ImU32 kPreviewFrameColor = IM_COL32(54, 72, 94, 255); +constexpr ImU32 kPreviewPlaceholderFill = IM_COL32(18, 24, 32, 255); +constexpr ImU32 kPreviewPlaceholderText = IM_COL32(191, 205, 224, 255); +constexpr ImU32 kPreviewPlaceholderSubtleText = IM_COL32(132, 147, 170, 255); +constexpr char kPreviewDebugName[] = "XCUI Demo"; +constexpr char kPreviewDebugSource[] = "new_editor.panels.xcui_demo"; + +UI::UIRect ToUIRect(const ImVec2& minPoint, const ImVec2& size) { + return UI::UIRect(minPoint.x, minPoint.y, size.x, size.y); +} + +bool ContainsPoint(const UI::UIRect& rect, const UI::UIPoint& point) { + return point.x >= rect.x && + point.y >= rect.y && + point.x <= rect.x + rect.width && + point.y <= rect.y + rect.height; +} + +void DrawHostedPreviewFrame( + ImDrawList* drawList, + const ImVec2& minPoint, + const ImVec2& size) { + if (drawList == nullptr || size.x <= 1.0f || size.y <= 1.0f) { + return; + } + + const ImVec2 maxPoint(minPoint.x + size.x, minPoint.y + size.y); + drawList->AddRect(minPoint, maxPoint, kPreviewFrameColor, 8.0f, 0, 1.0f); +} + +void DrawHostedPreviewPlaceholder( + ImDrawList* drawList, + const ImVec2& minPoint, + const ImVec2& size, + const char* title, + const char* subtitle) { + if (drawList == nullptr || size.x <= 1.0f || size.y <= 1.0f) { + return; + } + + const ImVec2 maxPoint(minPoint.x + size.x, minPoint.y + size.y); + drawList->AddRectFilled(minPoint, maxPoint, kPreviewPlaceholderFill, 8.0f); + drawList->AddRect(minPoint, maxPoint, kPreviewFrameColor, 8.0f, 0, 1.0f); + drawList->AddText(ImVec2(minPoint.x + 14.0f, minPoint.y + 14.0f), kPreviewPlaceholderText, title); + if (subtitle != nullptr && subtitle[0] != '\0') { + drawList->AddText( + ImVec2(minPoint.x + 14.0f, minPoint.y + 36.0f), + kPreviewPlaceholderSubtleText, + subtitle); + } +} + +void DrawRectOverlay( + ImDrawList* drawList, + ::XCEngine::Editor::XCUIBackend::XCUIDemoRuntime& runtime, + const std::string& elementId, + ImU32 color, + const char* label) { + if (drawList == nullptr || elementId.empty()) { + return; + } + + UI::UIRect rect = {}; + if (!runtime.TryGetElementRect(elementId, rect)) { + return; + } + + const ImVec2 minPoint(rect.x, rect.y); + const ImVec2 maxPoint(rect.x + rect.width, rect.y + rect.height); + drawList->AddRect(minPoint, maxPoint, color, 6.0f, 0, 2.0f); + if (label != nullptr && label[0] != '\0') { + drawList->AddText(ImVec2(minPoint.x + 4.0f, minPoint.y + 4.0f), color, label); + } +} + +const char* GetPreviewPathLabel(bool nativeHostedPreview) { + return nativeHostedPreview ? "native queued offscreen surface" : "legacy imgui transition"; +} + +const char* GetPreviewStateLabel( + bool nativeHostedPreview, + const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& previewStats, + bool hasHostedSurfaceDescriptor, + bool showHostedSurfaceImage) { + if (nativeHostedPreview) { + if (showHostedSurfaceImage) { + return "live"; + } + if (previewStats.queuedToNativePass || hasHostedSurfaceDescriptor) { + return "warming"; + } + return "awaiting submit"; + } + + return previewStats.presented ? "live" : "idle"; +} + +} // namespace + +XCUIDemoPanel::XCUIDemoPanel(::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource) + : XCUIDemoPanel(inputSource, ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter()) {} + +XCUIDemoPanel::XCUIDemoPanel( + ::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource, + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter) + : Panel("XCUI Demo") + , m_inputSource(inputSource) + , m_previewPresenter(std::move(previewPresenter)) { + if (m_previewPresenter == nullptr) { + m_previewPresenter = ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter(); + } + m_lastReloadSucceeded = m_runtime.ReloadDocuments(); +} + +void XCUIDemoPanel::SetHostedPreviewEnabled(bool enabled) { + m_hostedPreviewEnabled = enabled; + if (!m_hostedPreviewEnabled) { + m_lastPreviewStats = {}; + } +} + +const ::XCEngine::Editor::XCUIBackend::XCUIDemoFrameResult& XCUIDemoPanel::GetFrameResult() const { + return m_runtime.GetFrameResult(); +} + +const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& XCUIDemoPanel::GetLastPreviewStats() const { + return m_lastPreviewStats; +} + +void XCUIDemoPanel::SetHostedPreviewPresenter( + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter) { + m_previewPresenter = std::move(previewPresenter); + if (m_previewPresenter == nullptr) { + m_previewPresenter = ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter(); + } + m_lastPreviewStats = {}; +} + +bool XCUIDemoPanel::IsUsingNativeHostedPreview() const { + return m_previewPresenter != nullptr && m_previewPresenter->IsNativeQueued(); +} + +void XCUIDemoPanel::Render() { + ImGui::SetNextWindowSize(ImVec2(1040.0f, 720.0f), ImGuiCond_Appearing); + ImGui::SetNextWindowDockID(ImGui::GetID("XCNewEditorDockSpace"), ImGuiCond_Appearing); + + bool open = true; + if (!ImGui::Begin(GetName().c_str(), &open)) { + ImGui::End(); + if (!open) { + SetVisible(false); + } + return; + } + + if (ImGui::Button("Reload Documents")) { + m_lastReloadSucceeded = m_runtime.ReloadDocuments(); + } + ImGui::SameLine(); + ImGui::Checkbox("Canvas HUD", &m_showCanvasHud); + ImGui::SameLine(); + ImGui::Checkbox("Debug Rects", &m_showDebugRects); + ImGui::SameLine(); + ImGui::TextUnformatted(m_lastReloadSucceeded ? "Reload: OK" : "Reload: Failed"); + ImGui::Separator(); + + const float diagnosticsHeight = 232.0f; + const ImVec2 hostRegion = ImGui::GetContentRegionAvail(); + const float canvasHeight = (std::max)(140.0f, hostRegion.y - diagnosticsHeight); + + ImGui::BeginChild("XCUIDemoCanvasHost", ImVec2(0.0f, canvasHeight), true, ImGuiWindowFlags_NoScrollWithMouse); + const ImVec2 canvasHostMin = ImGui::GetCursorScreenPos(); + const ImVec2 availableSize = ImGui::GetContentRegionAvail(); + const float topInset = m_showCanvasHud ? (kCanvasHudHeight + kCanvasHudPadding) : 0.0f; + const ImVec2 canvasMin(canvasHostMin.x, canvasHostMin.y + topInset); + const ImVec2 canvasSize( + availableSize.x, + (std::max)(0.0f, availableSize.y - topInset)); + const bool validCanvas = canvasSize.x > 1.0f && canvasSize.y > 1.0f; + const bool nativeHostedPreview = IsUsingNativeHostedPreview(); + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor hostedSurfaceDescriptor = {}; + const bool hasHostedSurfaceDescriptor = + nativeHostedPreview && + m_previewPresenter != nullptr && + m_previewPresenter->TryGetSurfaceDescriptor(kPreviewDebugName, hostedSurfaceDescriptor); + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage hostedSurfaceImage = {}; + const bool showHostedSurfaceImage = + validCanvas && + nativeHostedPreview && + m_previewPresenter != nullptr && + m_previewPresenter->TryGetSurfaceImage(kPreviewDebugName, hostedSurfaceImage); + const char* const previewPathLabel = GetPreviewPathLabel(nativeHostedPreview); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + if (validCanvas) { + ImGui::SetCursorScreenPos(canvasMin); + if (showHostedSurfaceImage) { + ImGui::Image(hostedSurfaceImage.textureId, canvasSize, hostedSurfaceImage.uvMin, hostedSurfaceImage.uvMax); + DrawHostedPreviewFrame(drawList, canvasMin, canvasSize); + } else { + ImGui::InvisibleButton("##XCUIDemoCanvas", canvasSize); + const char* placeholderSubtitle = + nativeHostedPreview + ? "Waiting for native queued render output to publish back into the sandbox panel." + : "Legacy ImGui transition path stays active until native offscreen preview is enabled."; + DrawHostedPreviewPlaceholder( + drawList, + canvasMin, + canvasSize, + nativeHostedPreview ? "Native XCUI preview pending" : "Legacy XCUI canvas host", + placeholderSubtitle); + } + } else { + ImGui::Dummy(ImVec2(0.0f, 0.0f)); + } + + ::XCEngine::Editor::XCUIBackend::XCUIDemoInputState input = {}; + ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeCaptureOptions bridgeOptions = {}; + bridgeOptions.timestampNanoseconds = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); + const UI::UIRect canvasRect = ToUIRect(canvasMin, canvasSize); + + ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot snapshot = {}; + if (m_inputSource != nullptr) { + bridgeOptions.hasPointerInsideOverride = true; + bridgeOptions.windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); + const UI::UIPoint pointerPosition = m_inputSource->GetPointerPosition(); + bridgeOptions.pointerInsideOverride = validCanvas && ContainsPoint(canvasRect, pointerPosition); + snapshot = m_inputSource->CaptureSnapshot(bridgeOptions); + } else { + bridgeOptions.hasPointerInsideOverride = true; + bridgeOptions.pointerInsideOverride = validCanvas && ImGui::IsItemHovered(); + bridgeOptions.windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); + snapshot = ::XCEngine::Editor::XCUIBackend::ImGuiXCUIInputAdapter::CaptureSnapshot( + ImGui::GetIO(), + bridgeOptions); + } + + if (!m_inputBridge.HasBaseline()) { + m_inputBridge.Prime(snapshot); + } + const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta frameDelta = + m_inputBridge.Translate(snapshot); + + input.canvasRect = canvasRect; + input.pointerPosition = snapshot.pointerPosition; + input.pointerInside = snapshot.pointerInside; + input.pointerPressed = frameDelta.pointer.pressed[0]; + input.pointerReleased = frameDelta.pointer.released[0]; + input.pointerDown = snapshot.pointerButtonsDown[0]; + input.windowFocused = snapshot.windowFocused; + input.shortcutPressed = false; + input.wantCaptureMouse = snapshot.wantCaptureMouse; + input.wantCaptureKeyboard = snapshot.wantCaptureKeyboard; + input.wantTextInput = snapshot.wantTextInput; + input.events = frameDelta.events; + + const ::XCEngine::Editor::XCUIBackend::XCUIDemoFrameResult& frame = m_runtime.Update(input); + + if (m_hostedPreviewEnabled && m_previewPresenter != nullptr) { + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame previewFrame = {}; + previewFrame.drawData = &frame.drawData; + previewFrame.targetDrawList = drawList; + previewFrame.canvasRect = canvasRect; + previewFrame.logicalSize = UI::UISize(canvasRect.width, canvasRect.height); + previewFrame.debugName = kPreviewDebugName; + previewFrame.debugSource = kPreviewDebugSource; + m_previewPresenter->Present(previewFrame); + m_lastPreviewStats = m_previewPresenter->GetLastStats(); + } else { + m_lastPreviewStats = {}; + } + + const ::XCEngine::Editor::XCUIBackend::XCUIDemoFrameStats& stats = frame.stats; + const char* const previewStateLabel = GetPreviewStateLabel( + nativeHostedPreview, + m_lastPreviewStats, + hasHostedSurfaceDescriptor, + showHostedSurfaceImage); + if (m_showCanvasHud) { + const ImVec2 hudMin(canvasHostMin.x + 8.0f, canvasHostMin.y + 8.0f); + const ImVec2 hudMax( + canvasHostMin.x + (std::min)(availableSize.x - 8.0f, 430.0f), + canvasHostMin.y + kCanvasHudHeight); + drawList->AddRectFilled( + hudMin, + hudMax, + IM_COL32(16, 22, 30, 220), + 8.0f); + drawList->AddRect( + hudMin, + hudMax, + IM_COL32(52, 72, 96, 255), + 8.0f, + 0, + 1.0f); + + ImGui::SetCursorScreenPos(ImVec2(hudMin.x + 10.0f, hudMin.y + 8.0f)); + ImGui::BeginGroup(); + ImGui::TextUnformatted("XCUI Runtime"); + ImGui::Text("%s | %s", previewPathLabel, previewStateLabel); + ImGui::TextUnformatted(stats.statusMessage.c_str()); + ImGui::Text( + "Tree %llu | Elements %zu | Commands %zu", + static_cast(stats.treeGeneration), + stats.elementCount, + stats.commandCount); + ImGui::Text( + "Submit %zu/%zu | Flush %zu/%zu", + m_lastPreviewStats.submittedDrawListCount, + m_lastPreviewStats.submittedCommandCount, + m_lastPreviewStats.flushedDrawListCount, + m_lastPreviewStats.flushedCommandCount); + ImGui::EndGroup(); + } + + if (m_showDebugRects && validCanvas && (!nativeHostedPreview || showHostedSurfaceImage)) { + DrawRectOverlay(drawList, m_runtime, stats.hoveredElementId, IM_COL32(255, 195, 64, 255), "hover"); + DrawRectOverlay(drawList, m_runtime, stats.focusedElementId, IM_COL32(64, 214, 255, 255), "focus"); + DrawRectOverlay(drawList, m_runtime, "toggleAccent", IM_COL32(150, 255, 150, 160), "toggle"); + } + ImGui::EndChild(); + + ImGui::Separator(); + ImGui::BeginChild("XCUIDemoDiagnostics", ImVec2(0.0f, 0.0f), false, ImGuiWindowFlags_NoScrollbar); + ImGui::SeparatorText("Preview"); + ImGui::Text("Path: %s | state: %s", previewPathLabel, previewStateLabel); + ImGui::Text( + "Presenter: presented %s | submit->native %s", + m_lastPreviewStats.presented ? "yes" : "no", + m_lastPreviewStats.queuedToNativePass ? "yes" : "no"); + ImGui::Text( + "Submitted: %zu lists / %zu cmds | Flushed: %zu lists / %zu cmds", + m_lastPreviewStats.submittedDrawListCount, + m_lastPreviewStats.submittedCommandCount, + m_lastPreviewStats.flushedDrawListCount, + m_lastPreviewStats.flushedCommandCount); + ImGui::TextWrapped( + "Source: %s", + hasHostedSurfaceDescriptor && !hostedSurfaceDescriptor.debugSource.empty() + ? hostedSurfaceDescriptor.debugSource.c_str() + : kPreviewDebugSource); + if (nativeHostedPreview) { + ImGui::Text( + "Surface descriptor: %s | image published: %s | queued frame index: %zu", + hasHostedSurfaceDescriptor ? "yes" : "no", + showHostedSurfaceImage ? "yes" : "no", + hasHostedSurfaceDescriptor ? hostedSurfaceDescriptor.queuedFrameIndex : 0u); + if (hasHostedSurfaceDescriptor) { + ImGui::Text( + "Surface: %ux%u | logical: %.0f x %.0f | rendered rect: %.0f, %.0f %.0f x %.0f", + hostedSurfaceDescriptor.image.surfaceWidth, + hostedSurfaceDescriptor.image.surfaceHeight, + hostedSurfaceDescriptor.logicalSize.width, + hostedSurfaceDescriptor.logicalSize.height, + hostedSurfaceDescriptor.image.renderedCanvasRect.x, + hostedSurfaceDescriptor.image.renderedCanvasRect.y, + hostedSurfaceDescriptor.image.renderedCanvasRect.width, + hostedSurfaceDescriptor.image.renderedCanvasRect.height); + } else { + ImGui::TextDisabled("No native surface descriptor has been published back yet."); + } + } else { + ImGui::TextDisabled("Legacy path renders directly into the panel draw list. No native surface descriptor exists."); + } + + ImGui::SeparatorText("Runtime"); + ImGui::Text("Status: %s", stats.statusMessage.c_str()); + ImGui::Text( + "Tree gen: %llu | elements: %zu | dirty roots: %zu", + static_cast(stats.treeGeneration), + stats.elementCount, + stats.dirtyRootCount); + ImGui::Text( + "Draw lists: %zu | draw cmds: %zu | dependencies: %zu", + stats.drawListCount, + stats.commandCount, + stats.dependencyCount); + ImGui::Text( + "Hovered: %s | Focused: %s", + stats.hoveredElementId.empty() ? "none" : stats.hoveredElementId.c_str(), + stats.focusedElementId.empty() ? "none" : stats.focusedElementId.c_str()); + ImGui::Text( + "Last command: %s | Accent: %s", + stats.lastCommandId.empty() ? "none" : stats.lastCommandId.c_str(), + stats.accentEnabled ? "on" : "off"); + ImGui::Text("Canvas: %.0f x %.0f", input.canvasRect.width, input.canvasRect.height); + + ImGui::SeparatorText("Input"); + ImGui::Text( + "Pointer: %.0f, %.0f | inside %s | down %s | pressed %s | released %s", + input.pointerPosition.x, + input.pointerPosition.y, + input.pointerInside ? "yes" : "no", + input.pointerDown ? "yes" : "no", + input.pointerPressed ? "yes" : "no", + input.pointerReleased ? "yes" : "no"); + ImGui::Text( + "Focus %s | capture mouse %s | capture keyboard %s | text input %s | events %zu", + input.windowFocused ? "yes" : "no", + input.wantCaptureMouse ? "yes" : "no", + input.wantCaptureKeyboard ? "yes" : "no", + input.wantTextInput ? "yes" : "no", + input.events.size()); + ImGui::EndChild(); + + ImGui::End(); + + if (!open) { + SetVisible(false); + } +} + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/panels/XCUIDemoPanel.h b/new_editor/src/panels/XCUIDemoPanel.h new file mode 100644 index 00000000..1f983688 --- /dev/null +++ b/new_editor/src/panels/XCUIDemoPanel.h @@ -0,0 +1,45 @@ +#pragma once + +#include "Panel.h" + +#include "XCUIBackend/XCUIHostedPreviewPresenter.h" +#include "XCUIBackend/XCUIInputBridge.h" +#include "XCUIBackend/XCUIDemoRuntime.h" + +#include + +namespace XCEngine { +namespace NewEditor { + +class XCUIDemoPanel : public Panel { +public: + explicit XCUIDemoPanel( + ::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource = nullptr); + XCUIDemoPanel( + ::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource, + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter); + ~XCUIDemoPanel() override = default; + + void Render() override; + void SetHostedPreviewEnabled(bool enabled); + void SetHostedPreviewPresenter( + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter); + bool IsHostedPreviewEnabled() const { return m_hostedPreviewEnabled; } + bool IsUsingNativeHostedPreview() const; + const ::XCEngine::Editor::XCUIBackend::XCUIDemoFrameResult& GetFrameResult() const; + const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& GetLastPreviewStats() const; + +private: + bool m_lastReloadSucceeded = false; + bool m_hostedPreviewEnabled = true; + bool m_showCanvasHud = true; + bool m_showDebugRects = true; + ::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* m_inputSource = nullptr; + ::XCEngine::Editor::XCUIBackend::XCUIInputBridge m_inputBridge; + ::XCEngine::Editor::XCUIBackend::XCUIDemoRuntime m_runtime; + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> m_previewPresenter; + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats m_lastPreviewStats = {}; +}; + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/panels/XCUILayoutLabPanel.cpp b/new_editor/src/panels/XCUILayoutLabPanel.cpp new file mode 100644 index 00000000..fa7283cd --- /dev/null +++ b/new_editor/src/panels/XCUILayoutLabPanel.cpp @@ -0,0 +1,347 @@ +#include "XCUILayoutLabPanel.h" + +#include + +#include + +#include +#include + +namespace XCEngine { +namespace NewEditor { + +namespace { + +constexpr ImU32 kPreviewFrameColor = IM_COL32(54, 72, 94, 255); +constexpr ImU32 kPreviewPlaceholderFill = IM_COL32(18, 24, 32, 255); +constexpr ImU32 kPreviewPlaceholderText = IM_COL32(191, 205, 224, 255); +constexpr ImU32 kPreviewPlaceholderSubtleText = IM_COL32(132, 147, 170, 255); +constexpr char kPreviewDebugName[] = "XCUI Layout Lab"; +constexpr char kPreviewDebugSource[] = "new_editor.panels.xcui_layout_lab"; + +UI::UIRect ToUIRect(const ImVec2& minPoint, const ImVec2& size) { + return UI::UIRect(minPoint.x, minPoint.y, size.x, size.y); +} + +bool ContainsPoint(const UI::UIRect& rect, const UI::UIPoint& point) { + return point.x >= rect.x && + point.y >= rect.y && + point.x <= rect.x + rect.width && + point.y <= rect.y + rect.height; +} + +void DrawHostedPreviewFrame( + ImDrawList* drawList, + const ImVec2& minPoint, + const ImVec2& size) { + if (drawList == nullptr || size.x <= 1.0f || size.y <= 1.0f) { + return; + } + + const ImVec2 maxPoint(minPoint.x + size.x, minPoint.y + size.y); + drawList->AddRect(minPoint, maxPoint, kPreviewFrameColor, 8.0f, 0, 1.0f); +} + +void DrawHostedPreviewPlaceholder( + ImDrawList* drawList, + const ImVec2& minPoint, + const ImVec2& size, + const char* title, + const char* subtitle) { + if (drawList == nullptr || size.x <= 1.0f || size.y <= 1.0f) { + return; + } + + const ImVec2 maxPoint(minPoint.x + size.x, minPoint.y + size.y); + drawList->AddRectFilled(minPoint, maxPoint, kPreviewPlaceholderFill, 8.0f); + drawList->AddRect(minPoint, maxPoint, kPreviewFrameColor, 8.0f, 0, 1.0f); + drawList->AddText(ImVec2(minPoint.x + 14.0f, minPoint.y + 14.0f), kPreviewPlaceholderText, title); + if (subtitle != nullptr && subtitle[0] != '\0') { + drawList->AddText( + ImVec2(minPoint.x + 14.0f, minPoint.y + 36.0f), + kPreviewPlaceholderSubtleText, + subtitle); + } +} + +void DrawCanvasBadge( + ImDrawList* drawList, + const ImVec2& minPoint, + const char* title, + const char* subtitle) { + if (drawList == nullptr) { + return; + } + + const ImVec2 badgeMin(minPoint.x + 10.0f, minPoint.y + 10.0f); + const ImVec2 badgeMax(minPoint.x + 300.0f, minPoint.y + 52.0f); + drawList->AddRectFilled(badgeMin, badgeMax, IM_COL32(16, 22, 30, 216), 8.0f); + drawList->AddRect(badgeMin, badgeMax, IM_COL32(52, 72, 96, 255), 8.0f, 0, 1.0f); + drawList->AddText(ImVec2(badgeMin.x + 10.0f, badgeMin.y + 8.0f), kPreviewPlaceholderText, title); + drawList->AddText( + ImVec2(badgeMin.x + 10.0f, badgeMin.y + 28.0f), + kPreviewPlaceholderSubtleText, + subtitle); +} + +const char* GetPreviewPathLabel(bool nativeHostedPreview) { + return nativeHostedPreview ? "native queued offscreen surface" : "legacy imgui transition"; +} + +const char* GetPreviewStateLabel( + bool nativeHostedPreview, + const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& previewStats, + bool hasHostedSurfaceDescriptor, + bool showHostedSurfaceImage) { + if (nativeHostedPreview) { + if (showHostedSurfaceImage) { + return "live"; + } + if (previewStats.queuedToNativePass || hasHostedSurfaceDescriptor) { + return "warming"; + } + return "awaiting submit"; + } + + return previewStats.presented ? "live" : "idle"; +} + +} // namespace + +XCUILayoutLabPanel::XCUILayoutLabPanel(::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource) + : XCUILayoutLabPanel(inputSource, ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter()) {} + +XCUILayoutLabPanel::XCUILayoutLabPanel( + ::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource, + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter) + : Panel("XCUI Layout Lab") + , m_inputSource(inputSource) + , m_previewPresenter(std::move(previewPresenter)) { + if (m_previewPresenter == nullptr) { + m_previewPresenter = ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter(); + } + m_lastReloadSucceeded = m_runtime.ReloadDocuments(); +} + +void XCUILayoutLabPanel::SetHostedPreviewEnabled(bool enabled) { + m_hostedPreviewEnabled = enabled; + if (!m_hostedPreviewEnabled) { + m_lastPreviewStats = {}; + } +} + +const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& XCUILayoutLabPanel::GetFrameResult() const { + return m_runtime.GetFrameResult(); +} + +const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& XCUILayoutLabPanel::GetLastPreviewStats() const { + return m_lastPreviewStats; +} + +void XCUILayoutLabPanel::SetHostedPreviewPresenter( + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter) { + m_previewPresenter = std::move(previewPresenter); + if (m_previewPresenter == nullptr) { + m_previewPresenter = ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter(); + } + m_lastPreviewStats = {}; +} + +bool XCUILayoutLabPanel::IsUsingNativeHostedPreview() const { + return m_previewPresenter != nullptr && m_previewPresenter->IsNativeQueued(); +} + +void XCUILayoutLabPanel::Render() { + ImGui::SetNextWindowSize(ImVec2(960.0f, 720.0f), ImGuiCond_Appearing); + ImGui::SetNextWindowDockID(ImGui::GetID("XCNewEditorDockSpace"), ImGuiCond_Appearing); + + bool open = true; + if (!ImGui::Begin(GetName().c_str(), &open)) { + ImGui::End(); + if (!open) { + SetVisible(false); + } + return; + } + + if (ImGui::Button("Reload Layout Lab")) { + m_lastReloadSucceeded = m_runtime.ReloadDocuments(); + } + ImGui::SameLine(); + ImGui::TextUnformatted(m_lastReloadSucceeded ? "Reload: OK" : "Reload: Failed"); + ImGui::Separator(); + + const float diagnosticsHeight = 240.0f; + const ImVec2 hostRegion = ImGui::GetContentRegionAvail(); + const float canvasHeight = (std::max)(140.0f, hostRegion.y - diagnosticsHeight); + + ImGui::BeginChild("XCUILayoutLabCanvasHost", ImVec2(0.0f, canvasHeight), true, ImGuiWindowFlags_NoScrollWithMouse); + const ImVec2 canvasMin = ImGui::GetCursorScreenPos(); + const ImVec2 canvasSize = ImGui::GetContentRegionAvail(); + const bool validCanvas = canvasSize.x > 1.0f && canvasSize.y > 1.0f; + const bool nativeHostedPreview = IsUsingNativeHostedPreview(); + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor hostedSurfaceDescriptor = {}; + const bool hasHostedSurfaceDescriptor = + nativeHostedPreview && + m_previewPresenter != nullptr && + m_previewPresenter->TryGetSurfaceDescriptor(kPreviewDebugName, hostedSurfaceDescriptor); + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage hostedSurfaceImage = {}; + const bool showHostedSurfaceImage = + validCanvas && + nativeHostedPreview && + m_previewPresenter != nullptr && + m_previewPresenter->TryGetSurfaceImage(kPreviewDebugName, hostedSurfaceImage); + const char* const previewPathLabel = GetPreviewPathLabel(nativeHostedPreview); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + if (validCanvas) { + ImGui::SetCursorScreenPos(canvasMin); + if (showHostedSurfaceImage) { + ImGui::Image(hostedSurfaceImage.textureId, canvasSize, hostedSurfaceImage.uvMin, hostedSurfaceImage.uvMax); + DrawHostedPreviewFrame(drawList, canvasMin, canvasSize); + } else { + ImGui::InvisibleButton("##XCUILayoutLabCanvas", canvasSize); + const char* placeholderSubtitle = + nativeHostedPreview + ? "Waiting for native queued render output to publish back into the layout sandbox." + : "Legacy ImGui transition path remains active until native offscreen preview is enabled."; + DrawHostedPreviewPlaceholder( + drawList, + canvasMin, + canvasSize, + nativeHostedPreview ? "Native layout preview pending" : "Legacy layout canvas host", + placeholderSubtitle); + } + DrawCanvasBadge(drawList, canvasMin, "Layout Lab", previewPathLabel); + } else { + ImGui::Dummy(ImVec2(0.0f, 0.0f)); + } + + ::XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = {}; + input.canvasRect = ToUIRect(canvasMin, canvasSize); + if (m_inputSource != nullptr) { + input.pointerPosition = m_inputSource->GetPointerPosition(); + input.pointerInside = validCanvas && ContainsPoint(input.canvasRect, input.pointerPosition); + } else { + input.pointerPosition = UI::UIPoint(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y); + input.pointerInside = validCanvas && ImGui::IsItemHovered(); + } + + const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& frame = m_runtime.Update(input); + + if (m_hostedPreviewEnabled && m_previewPresenter != nullptr) { + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame previewFrame = {}; + previewFrame.drawData = &frame.drawData; + previewFrame.targetDrawList = ImGui::GetWindowDrawList(); + previewFrame.canvasRect = input.canvasRect; + previewFrame.logicalSize = UI::UISize(input.canvasRect.width, input.canvasRect.height); + previewFrame.debugName = kPreviewDebugName; + previewFrame.debugSource = kPreviewDebugSource; + m_previewPresenter->Present(previewFrame); + m_lastPreviewStats = m_previewPresenter->GetLastStats(); + } else { + m_lastPreviewStats = {}; + } + + const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameStats& stats = frame.stats; + const char* const previewStateLabel = GetPreviewStateLabel( + nativeHostedPreview, + m_lastPreviewStats, + hasHostedSurfaceDescriptor, + showHostedSurfaceImage); + ImGui::EndChild(); + + ImGui::Separator(); + ImGui::BeginChild("XCUILayoutLabDiagnostics", ImVec2(0.0f, 0.0f), false, ImGuiWindowFlags_NoScrollbar); + ImGui::SeparatorText("Preview"); + ImGui::Text("Path: %s | state: %s", previewPathLabel, previewStateLabel); + ImGui::Text( + "Presenter: presented %s | submit->native %s", + m_lastPreviewStats.presented ? "yes" : "no", + m_lastPreviewStats.queuedToNativePass ? "yes" : "no"); + ImGui::Text( + "Submitted: %zu lists / %zu cmds | Flushed: %zu lists / %zu cmds", + m_lastPreviewStats.submittedDrawListCount, + m_lastPreviewStats.submittedCommandCount, + m_lastPreviewStats.flushedDrawListCount, + m_lastPreviewStats.flushedCommandCount); + ImGui::TextWrapped( + "Source: %s", + hasHostedSurfaceDescriptor && !hostedSurfaceDescriptor.debugSource.empty() + ? hostedSurfaceDescriptor.debugSource.c_str() + : kPreviewDebugSource); + if (nativeHostedPreview) { + ImGui::Text( + "Surface descriptor: %s | image published: %s | queued frame index: %zu", + hasHostedSurfaceDescriptor ? "yes" : "no", + showHostedSurfaceImage ? "yes" : "no", + hasHostedSurfaceDescriptor ? hostedSurfaceDescriptor.queuedFrameIndex : 0u); + if (hasHostedSurfaceDescriptor) { + ImGui::Text( + "Surface: %ux%u | logical: %.0f x %.0f | rendered rect: %.0f, %.0f %.0f x %.0f", + hostedSurfaceDescriptor.image.surfaceWidth, + hostedSurfaceDescriptor.image.surfaceHeight, + hostedSurfaceDescriptor.logicalSize.width, + hostedSurfaceDescriptor.logicalSize.height, + hostedSurfaceDescriptor.image.renderedCanvasRect.x, + hostedSurfaceDescriptor.image.renderedCanvasRect.y, + hostedSurfaceDescriptor.image.renderedCanvasRect.width, + hostedSurfaceDescriptor.image.renderedCanvasRect.height); + } else { + ImGui::TextDisabled("No native surface descriptor has been published back yet."); + } + } else { + ImGui::TextDisabled("Legacy path renders directly into the panel draw list. No native surface descriptor exists."); + } + + ImGui::SeparatorText("Runtime"); + ImGui::Text("Status: %s", stats.statusMessage.c_str()); + ImGui::Text( + "Rows: %zu | Columns: %zu | Overlays: %zu | Scroll views: %zu", + stats.rowCount, + stats.columnCount, + stats.overlayCount, + stats.scrollViewCount); + ImGui::Text( + "Draw lists: %zu | Draw commands: %zu", + stats.drawListCount, + stats.commandCount); + ImGui::Text( + "Command types: fill %zu | outline %zu | text %zu | image %zu | clip %zu/%zu", + stats.filledRectCommandCount, + stats.rectOutlineCommandCount, + stats.textCommandCount, + stats.imageCommandCount, + stats.clipPushCommandCount, + stats.clipPopCommandCount); + ImGui::Text( + "Native overlay: %s | supported %zu | unsupported %zu", + stats.nativeOverlayReady ? "preflight OK" : "preflight issues", + stats.nativeSupportedCommandCount, + stats.nativeUnsupportedCommandCount); + ImGui::TextWrapped( + "Native note: %s", + stats.nativeOverlayStatusMessage.empty() ? "none" : stats.nativeOverlayStatusMessage.c_str()); + ImGui::Text( + "Hovered: %s | canvas: %.0f x %.0f", + stats.hoveredElementId.empty() ? "none" : stats.hoveredElementId.c_str(), + input.canvasRect.width, + input.canvasRect.height); + + ImGui::SeparatorText("Input"); + ImGui::Text( + "Pointer: %.0f, %.0f | inside %s", + input.pointerPosition.x, + input.pointerPosition.y, + input.pointerInside ? "yes" : "no"); + ImGui::EndChild(); + + ImGui::End(); + + if (!open) { + SetVisible(false); + } +} + +} // namespace NewEditor +} // namespace XCEngine diff --git a/new_editor/src/panels/XCUILayoutLabPanel.h b/new_editor/src/panels/XCUILayoutLabPanel.h new file mode 100644 index 00000000..3e1fc975 --- /dev/null +++ b/new_editor/src/panels/XCUILayoutLabPanel.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Panel.h" + +#include "XCUIBackend/XCUIHostedPreviewPresenter.h" +#include "XCUIBackend/XCUIInputBridge.h" +#include "XCUIBackend/XCUILayoutLabRuntime.h" + +#include + +namespace XCEngine { +namespace NewEditor { + +class XCUILayoutLabPanel : public Panel { +public: + explicit XCUILayoutLabPanel( + ::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource = nullptr); + XCUILayoutLabPanel( + ::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource, + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter); + ~XCUILayoutLabPanel() override = default; + + void Render() override; + void SetHostedPreviewEnabled(bool enabled); + void SetHostedPreviewPresenter( + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter); + bool IsHostedPreviewEnabled() const { return m_hostedPreviewEnabled; } + bool IsUsingNativeHostedPreview() const; + const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& GetFrameResult() const; + const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& GetLastPreviewStats() const; + +private: + bool m_lastReloadSucceeded = false; + bool m_hostedPreviewEnabled = true; + ::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* m_inputSource = nullptr; + ::XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime m_runtime; + std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> m_previewPresenter; + ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats m_lastPreviewStats = {}; +}; + +} // namespace NewEditor +} // namespace XCEngine diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 18ee4088..843f74fc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.15) project(XCEngineTests) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) # ============================================================ @@ -43,6 +43,7 @@ add_subdirectory(RHI) add_subdirectory(Resources) add_subdirectory(Input) add_subdirectory(Editor) +add_subdirectory(NewEditor) if(WIN32) find_program(XCENGINE_POWERSHELL_EXECUTABLE NAMES powershell pwsh REQUIRED) @@ -78,4 +79,4 @@ add_custom_target(print_tests COMMAND ${CMAKE_COMMAND} -E echo "===== XCEngine Test Suite =====" COMMAND ${CMAKE_CTEST_COMMAND} -N COMMENT "Available tests:" -) +) diff --git a/tests/NewEditor/CMakeLists.txt b/tests/NewEditor/CMakeLists.txt new file mode 100644 index 00000000..e82397ab --- /dev/null +++ b/tests/NewEditor/CMakeLists.txt @@ -0,0 +1,508 @@ +cmake_minimum_required(VERSION 3.15) + +project(XCEngine_NewEditorTests) + +file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCENGINE_TEST_REPO_ROOT_CMAKE) + +function(xcengine_configure_new_editor_test_target target_name) + if(MSVC) + set_target_properties(${target_name} PROPERTIES + LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" + COMPILE_PDB_NAME "${target_name}-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" + VS_GLOBAL_UseMultiToolTask "false" + ) + target_compile_options(${target_name} PRIVATE /FS /utf-8) + target_link_options(${target_name} PRIVATE + $<$:/INCREMENTAL:NO>) + endif() +endfunction() + +function(xcengine_discover_new_editor_gtests target_name) + include(GoogleTest) + gtest_discover_tests(${target_name} + DISCOVERY_MODE PRE_TEST) +endfunction() + +set(NEW_EDITOR_RUNTIME_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIDemoRuntime.h +) +set(NEW_EDITOR_RUNTIME_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp +) +set(NEW_EDITOR_LAYOUT_LAB_RUNTIME_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h +) +set(NEW_EDITOR_LAYOUT_LAB_RUNTIME_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp +) +set(NEW_EDITOR_ASSET_DOCUMENT_SOURCE_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.h +) +set(NEW_EDITOR_ASSET_DOCUMENT_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.cpp +) +set(NEW_EDITOR_UI_DOCUMENT_COMPILER_SOURCE + ${CMAKE_SOURCE_DIR}/engine/src/Resources/UI/UIDocumentCompiler.cpp +) +set(NEW_EDITOR_BACKEND_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiTransitionBackend.h +) +set(NEW_EDITOR_INPUT_BRIDGE_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIInputBridge.h +) +set(NEW_EDITOR_INPUT_BRIDGE_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIInputBridge.cpp +) +set(NEW_EDITOR_IMGUI_INPUT_ADAPTER_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.h +) +set(NEW_EDITOR_IMGUI_INPUT_ADAPTER_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.cpp +) +set(NEW_EDITOR_RHI_COMMAND_SUPPORT_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIRHICommandSupport.h +) +set(NEW_EDITOR_RHI_COMMAND_COMPILER_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIRHICommandCompiler.h +) +set(NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIRHICommandCompiler.cpp +) +set(NEW_EDITOR_RHI_RENDER_BACKEND_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIRHIRenderBackend.h +) +set(NEW_EDITOR_RHI_RENDER_BACKEND_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIRHIRenderBackend.cpp +) +set(NEW_EDITOR_IMGUI_TEXT_ATLAS_PROVIDER_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.h +) +set(NEW_EDITOR_IMGUI_TEXT_ATLAS_PROVIDER_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.cpp +) +set(NEW_EDITOR_FONT_SETUP_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIEditorFontSetup.h +) +set(NEW_EDITOR_FONT_SETUP_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIEditorFontSetup.cpp +) +set(NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h +) +set(NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp +) +set(NEW_EDITOR_HOSTED_PREVIEW_PRESENTER_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h +) +set(NEW_EDITOR_NATIVE_BACKDROP_RENDERER_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.h +) + +if(EXISTS "${NEW_EDITOR_RUNTIME_HEADER}" AND + EXISTS "${NEW_EDITOR_RUNTIME_SOURCE}" AND + EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE_HEADER}" AND + EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}") + add_executable(new_editor_xcui_demo_runtime_tests + test_xcui_demo_runtime.cpp + ${NEW_EDITOR_RUNTIME_SOURCE} + ${NEW_EDITOR_ASSET_DOCUMENT_SOURCE} + ) + + xcengine_configure_new_editor_test_target(new_editor_xcui_demo_runtime_tests) + + target_link_libraries(new_editor_xcui_demo_runtime_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + ) + + target_include_directories(new_editor_xcui_demo_runtime_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ) + target_compile_definitions(new_editor_xcui_demo_runtime_tests PRIVATE + XCENGINE_NEW_EDITOR_REPO_ROOT="${XCENGINE_TEST_REPO_ROOT_CMAKE}" + ) + + xcengine_discover_new_editor_gtests(new_editor_xcui_demo_runtime_tests) +else() + message(STATUS "Skipping new_editor_xcui_demo_runtime_tests because XCUIDemoRuntime files are missing.") +endif() + +if(EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE_HEADER}" AND + EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}" AND + EXISTS "${NEW_EDITOR_UI_DOCUMENT_COMPILER_SOURCE}" AND + EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_xcui_asset_document_source.cpp") + add_executable(new_editor_xcui_asset_document_source_tests + test_xcui_asset_document_source.cpp + ${NEW_EDITOR_ASSET_DOCUMENT_SOURCE} + ${NEW_EDITOR_UI_DOCUMENT_COMPILER_SOURCE} + ) + + xcengine_configure_new_editor_test_target(new_editor_xcui_asset_document_source_tests) + + target_link_libraries(new_editor_xcui_asset_document_source_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + ) + + target_include_directories(new_editor_xcui_asset_document_source_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ) + target_compile_definitions(new_editor_xcui_asset_document_source_tests PRIVATE + XCENGINE_NEW_EDITOR_REPO_ROOT="${XCENGINE_TEST_REPO_ROOT_CMAKE}" + ) + + include(GoogleTest) + gtest_discover_tests(new_editor_xcui_asset_document_source_tests) +else() + message(STATUS "Skipping new_editor_xcui_asset_document_source_tests because asset document source files or the test source are missing.") +endif() + +if(EXISTS "${NEW_EDITOR_LAYOUT_LAB_RUNTIME_HEADER}" AND + EXISTS "${NEW_EDITOR_LAYOUT_LAB_RUNTIME_SOURCE}" AND + EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE_HEADER}" AND + EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}") + add_executable(new_editor_xcui_layout_lab_runtime_tests + test_xcui_layout_lab_runtime.cpp + ${NEW_EDITOR_LAYOUT_LAB_RUNTIME_SOURCE} + ${NEW_EDITOR_ASSET_DOCUMENT_SOURCE} + ) + + xcengine_configure_new_editor_test_target(new_editor_xcui_layout_lab_runtime_tests) + + target_link_libraries(new_editor_xcui_layout_lab_runtime_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + ) + + target_include_directories(new_editor_xcui_layout_lab_runtime_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ) + target_compile_definitions(new_editor_xcui_layout_lab_runtime_tests PRIVATE + XCENGINE_NEW_EDITOR_REPO_ROOT="${XCENGINE_TEST_REPO_ROOT_CMAKE}" + ) + + xcengine_discover_new_editor_gtests(new_editor_xcui_layout_lab_runtime_tests) +else() + message(STATUS "Skipping new_editor_xcui_layout_lab_runtime_tests because XCUILayoutLabRuntime files are missing.") +endif() + +if(EXISTS "${NEW_EDITOR_BACKEND_HEADER}" AND EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp") + add_executable(new_editor_imgui_transition_backend_tests + test_new_editor_imgui_transition_backend.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp + ) + + xcengine_configure_new_editor_test_target(new_editor_imgui_transition_backend_tests) + + target_link_libraries(new_editor_imgui_transition_backend_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + user32 + comdlg32 + ) + + target_include_directories(new_editor_imgui_transition_backend_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ${CMAKE_BINARY_DIR}/_deps/imgui-src + ${CMAKE_BINARY_DIR}/_deps/imgui-src/backends + ) + + xcengine_discover_new_editor_gtests(new_editor_imgui_transition_backend_tests) +else() + message(STATUS "Skipping new_editor_imgui_transition_backend_tests because backend or ImGui sources are missing.") +endif() + +if(EXISTS "${NEW_EDITOR_HOSTED_PREVIEW_PRESENTER_HEADER}" AND EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp") + add_executable(new_editor_xcui_hosted_preview_presenter_tests + test_xcui_hosted_preview_presenter.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp + ) + + xcengine_configure_new_editor_test_target(new_editor_xcui_hosted_preview_presenter_tests) + + target_link_libraries(new_editor_xcui_hosted_preview_presenter_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + user32 + comdlg32 + ) + + target_include_directories(new_editor_xcui_hosted_preview_presenter_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ${CMAKE_BINARY_DIR}/_deps/imgui-src + ${CMAKE_BINARY_DIR}/_deps/imgui-src/backends + ) + + xcengine_discover_new_editor_gtests(new_editor_xcui_hosted_preview_presenter_tests) +else() + message(STATUS "Skipping new_editor_xcui_hosted_preview_presenter_tests because presenter header or ImGui sources are missing.") +endif() + +if(EXISTS "${NEW_EDITOR_NATIVE_BACKDROP_RENDERER_HEADER}") + add_executable(new_editor_native_backdrop_renderer_api_tests + test_main_window_native_backdrop_renderer_api.cpp + ) + + xcengine_configure_new_editor_test_target(new_editor_native_backdrop_renderer_api_tests) + + target_link_libraries(new_editor_native_backdrop_renderer_api_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + ) + + target_include_directories(new_editor_native_backdrop_renderer_api_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ${CMAKE_BINARY_DIR}/_deps/imgui-src + ) + + xcengine_discover_new_editor_gtests(new_editor_native_backdrop_renderer_api_tests) +else() + message(STATUS "Skipping new_editor_native_backdrop_renderer_api_tests because renderer header is missing.") +endif() + +if(EXISTS "${NEW_EDITOR_INPUT_BRIDGE_HEADER}" AND + EXISTS "${NEW_EDITOR_INPUT_BRIDGE_SOURCE}") + add_executable(new_editor_xcui_input_bridge_tests + test_xcui_input_bridge.cpp + ${NEW_EDITOR_INPUT_BRIDGE_SOURCE} + ) + + xcengine_configure_new_editor_test_target(new_editor_xcui_input_bridge_tests) + + target_link_libraries(new_editor_xcui_input_bridge_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + ) + + target_include_directories(new_editor_xcui_input_bridge_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ${CMAKE_BINARY_DIR}/_deps/imgui-src + ${CMAKE_BINARY_DIR}/_deps/imgui-src/backends + ) + + xcengine_discover_new_editor_gtests(new_editor_xcui_input_bridge_tests) +else() + message(STATUS "Skipping new_editor_xcui_input_bridge_tests because input bridge files are missing.") +endif() + +if(EXISTS "${NEW_EDITOR_IMGUI_INPUT_ADAPTER_HEADER}" AND + EXISTS "${NEW_EDITOR_IMGUI_INPUT_ADAPTER_SOURCE}" AND + EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_imgui_xcui_input_adapter.cpp" AND + EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp") + add_executable(new_editor_imgui_xcui_input_adapter_tests + test_imgui_xcui_input_adapter.cpp + ${NEW_EDITOR_IMGUI_INPUT_ADAPTER_SOURCE} + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp + ) + + xcengine_configure_new_editor_test_target(new_editor_imgui_xcui_input_adapter_tests) + + target_link_libraries(new_editor_imgui_xcui_input_adapter_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + user32 + comdlg32 + ) + + target_include_directories(new_editor_imgui_xcui_input_adapter_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ${CMAKE_BINARY_DIR}/_deps/imgui-src + ${CMAKE_BINARY_DIR}/_deps/imgui-src/backends + ) + + xcengine_discover_new_editor_gtests(new_editor_imgui_xcui_input_adapter_tests) +else() + message(STATUS "Skipping new_editor_imgui_xcui_input_adapter_tests because ImGui adapter files, test source, or ImGui sources are missing.") +endif() + +if(EXISTS "${NEW_EDITOR_RHI_COMMAND_SUPPORT_HEADER}") + add_executable(new_editor_xcui_rhi_command_support_tests + test_xcui_rhi_command_support.cpp + ) + + xcengine_configure_new_editor_test_target(new_editor_xcui_rhi_command_support_tests) + + target_link_libraries(new_editor_xcui_rhi_command_support_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + ) + + target_include_directories(new_editor_xcui_rhi_command_support_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ) + + xcengine_discover_new_editor_gtests(new_editor_xcui_rhi_command_support_tests) +else() + message(STATUS "Skipping new_editor_xcui_rhi_command_support_tests because support helper header is missing.") +endif() + +if(EXISTS "${NEW_EDITOR_RHI_RENDER_BACKEND_HEADER}" AND + EXISTS "${NEW_EDITOR_RHI_RENDER_BACKEND_SOURCE}" AND + EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_HEADER}" AND + EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE}" AND + EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_xcui_rhi_render_backend.cpp") + add_executable(new_editor_xcui_rhi_render_backend_tests + test_xcui_rhi_render_backend.cpp + ${NEW_EDITOR_RHI_RENDER_BACKEND_SOURCE} + ${NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE} + ) + + xcengine_configure_new_editor_test_target(new_editor_xcui_rhi_render_backend_tests) + + target_link_libraries(new_editor_xcui_rhi_render_backend_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + ) + + target_include_directories(new_editor_xcui_rhi_render_backend_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ) + + xcengine_discover_new_editor_gtests(new_editor_xcui_rhi_render_backend_tests) +else() + message(STATUS "Skipping new_editor_xcui_rhi_render_backend_tests because backend/compiler sources or the test source are missing.") +endif() + +if(EXISTS "${NEW_EDITOR_IMGUI_TEXT_ATLAS_PROVIDER_HEADER}" AND + EXISTS "${NEW_EDITOR_IMGUI_TEXT_ATLAS_PROVIDER_SOURCE}" AND + EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp") + add_executable(new_editor_imgui_text_atlas_provider_tests + test_imgui_text_atlas_provider.cpp + ${NEW_EDITOR_IMGUI_TEXT_ATLAS_PROVIDER_SOURCE} + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp + ) + + xcengine_configure_new_editor_test_target(new_editor_imgui_text_atlas_provider_tests) + + target_link_libraries(new_editor_imgui_text_atlas_provider_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + user32 + comdlg32 + ) + + target_include_directories(new_editor_imgui_text_atlas_provider_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ${CMAKE_BINARY_DIR}/_deps/imgui-src + ${CMAKE_BINARY_DIR}/_deps/imgui-src/backends + ) + + xcengine_discover_new_editor_gtests(new_editor_imgui_text_atlas_provider_tests) +else() + message(STATUS "Skipping new_editor_imgui_text_atlas_provider_tests because atlas provider or ImGui sources are missing.") +endif() + +if(EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_HEADER}" AND + EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE}" AND + EXISTS "${NEW_EDITOR_FONT_SETUP_HEADER}" AND + EXISTS "${NEW_EDITOR_FONT_SETUP_SOURCE}" AND + EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp") + add_executable(new_editor_xcui_standalone_text_atlas_provider_tests + test_xcui_standalone_text_atlas_provider.cpp + ${NEW_EDITOR_FONT_SETUP_SOURCE} + ${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE} + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp + ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp + ) + + xcengine_configure_new_editor_test_target(new_editor_xcui_standalone_text_atlas_provider_tests) + + target_link_libraries(new_editor_xcui_standalone_text_atlas_provider_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + user32 + comdlg32 + ) + + target_include_directories(new_editor_xcui_standalone_text_atlas_provider_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ${CMAKE_BINARY_DIR}/_deps/imgui-src + ${CMAKE_BINARY_DIR}/_deps/imgui-src/backends + ) + + xcengine_discover_new_editor_gtests(new_editor_xcui_standalone_text_atlas_provider_tests) +else() + message(STATUS "Skipping new_editor_xcui_standalone_text_atlas_provider_tests because standalone atlas provider, font setup, or ImGui sources are missing.") +endif() + +if(EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_HEADER}" AND + EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE}") + add_executable(new_editor_xcui_rhi_command_compiler_tests + test_xcui_rhi_command_compiler.cpp + ${NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE} + ) + + xcengine_configure_new_editor_test_target(new_editor_xcui_rhi_command_compiler_tests) + + target_link_libraries(new_editor_xcui_rhi_command_compiler_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + ) + + target_include_directories(new_editor_xcui_rhi_command_compiler_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ) + + xcengine_discover_new_editor_gtests(new_editor_xcui_rhi_command_compiler_tests) +else() + message(STATUS "Skipping new_editor_xcui_rhi_command_compiler_tests because compiler files are missing.") +endif() diff --git a/tests/NewEditor/test_imgui_text_atlas_provider.cpp b/tests/NewEditor/test_imgui_text_atlas_provider.cpp new file mode 100644 index 00000000..e8e75337 --- /dev/null +++ b/tests/NewEditor/test_imgui_text_atlas_provider.cpp @@ -0,0 +1,90 @@ +#include + +#include "XCUIBackend/ImGuiTextAtlasProvider.h" + +#include + +#include + +namespace { + +using XCEngine::Editor::XCUIBackend::IXCUITextAtlasProvider; +using XCEngine::Editor::XCUIBackend::ImGuiTextAtlasProvider; + +class ImGuiContextScope { +public: + ImGuiContextScope() { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + } + + ~ImGuiContextScope() { + ImGui::DestroyContext(); + } +}; + +void BuildDefaultFontAtlas() { + ImGuiIO& io = ImGui::GetIO(); + if (io.Fonts->Fonts.empty()) { + io.Fonts->AddFontDefault(); + } + + unsigned char* pixels = nullptr; + int width = 0; + int height = 0; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); +} + +TEST(ImGuiTextAtlasProviderTest, ReturnsEmptyResultsWhenNoContextIsAvailable) { + ImGuiTextAtlasProvider provider = {}; + IXCUITextAtlasProvider::AtlasTextureView atlasView = {}; + IXCUITextAtlasProvider::FontInfo fontInfo = {}; + IXCUITextAtlasProvider::BakedFontInfo bakedFontInfo = {}; + IXCUITextAtlasProvider::GlyphInfo glyphInfo = {}; + + EXPECT_EQ(provider.GetContext(), nullptr); + EXPECT_FALSE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView)); + EXPECT_EQ(provider.GetFontCount(), 0u); + EXPECT_FALSE(provider.GetDefaultFont().IsValid()); + EXPECT_FALSE(provider.GetFontInfo({}, fontInfo)); + EXPECT_FALSE(provider.GetBakedFontInfo({}, 14.0f, bakedFontInfo)); + EXPECT_FALSE(provider.FindGlyph({}, 14.0f, 'A', glyphInfo)); +} + +TEST(ImGuiTextAtlasProviderTest, ExposesAtlasAndGlyphDataFromExplicitContext) { + ImGuiContextScope contextScope; + BuildDefaultFontAtlas(); + + ImGuiTextAtlasProvider provider(ImGui::GetCurrentContext()); + + IXCUITextAtlasProvider::AtlasTextureView atlasView = {}; + ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView)); + EXPECT_TRUE(atlasView.IsValid()); + EXPECT_EQ(atlasView.format, IXCUITextAtlasProvider::PixelFormat::RGBA32); + EXPECT_GT(atlasView.atlasStorageKey, 0u); + EXPECT_GT(atlasView.pixelDataKey, 0u); + + const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont(); + ASSERT_TRUE(defaultFont.IsValid()); + ASSERT_GE(provider.GetFontCount(), 1u); + + IXCUITextAtlasProvider::FontInfo fontInfo = {}; + ASSERT_TRUE(provider.GetFontInfo(defaultFont, fontInfo)); + EXPECT_TRUE(fontInfo.handle.IsValid()); + EXPECT_GT(fontInfo.nominalSize, 0.0f); + + IXCUITextAtlasProvider::BakedFontInfo bakedFontInfo = {}; + ASSERT_TRUE(provider.GetBakedFontInfo(defaultFont, 0.0f, bakedFontInfo)); + EXPECT_GT(bakedFontInfo.lineHeight, 0.0f); + EXPECT_GT(bakedFontInfo.rasterizerDensity, 0.0f); + + IXCUITextAtlasProvider::GlyphInfo glyphInfo = {}; + ASSERT_TRUE(provider.FindGlyph(defaultFont, 0.0f, 'A', glyphInfo)); + EXPECT_EQ(glyphInfo.requestedCodepoint, static_cast('A')); + EXPECT_GT(glyphInfo.advanceX, 0.0f); + EXPECT_GE(glyphInfo.u1, glyphInfo.u0); + EXPECT_GE(glyphInfo.v1, glyphInfo.v0); +} + +} // namespace diff --git a/tests/NewEditor/test_imgui_xcui_input_adapter.cpp b/tests/NewEditor/test_imgui_xcui_input_adapter.cpp new file mode 100644 index 00000000..9b15634c --- /dev/null +++ b/tests/NewEditor/test_imgui_xcui_input_adapter.cpp @@ -0,0 +1,88 @@ +#include + +#include "XCUIBackend/ImGuiXCUIInputAdapter.h" + +#include + +namespace { + +using XCEngine::Editor::XCUIBackend::ImGuiXCUIInputAdapter; +using XCEngine::Editor::XCUIBackend::XCUIInputBridgeCaptureOptions; +using XCEngine::Input::KeyCode; + +class ImGuiContextScope { +public: + ImGuiContextScope() { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + } + + ~ImGuiContextScope() { + ImGui::DestroyContext(); + } +}; + +void PrepareImGui(float width = 1024.0f, float height = 768.0f) { + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(width, height); + io.DeltaTime = 1.0f / 60.0f; +} + +} // namespace + +TEST(ImGuiXCUIInputAdapterTest, CaptureSnapshotMapsImGuiStateIntoXCUIFrameSnapshot) { + ImGuiContextScope contextScope; + PrepareImGui(800.0f, 600.0f); + + ImGuiIO& io = ImGui::GetIO(); + io.WantCaptureMouse = true; + io.WantCaptureKeyboard = true; + io.WantTextInput = true; + io.AddMousePosEvent(120.0f, 72.0f); + io.AddMouseButtonEvent(0, true); + io.AddMouseWheelEvent(0.0f, 1.0f); + io.AddKeyEvent(ImGuiKey_LeftCtrl, true); + io.AddKeyEvent(ImGuiKey_P, true); + io.AddInputCharacter('p'); + + ImGui::NewFrame(); + + XCUIInputBridgeCaptureOptions options = {}; + options.pointerOffset = XCEngine::UI::UIPoint(20.0f, 12.0f); + options.windowFocused = false; + options.timestampNanoseconds = 99u; + + const auto snapshot = ImGuiXCUIInputAdapter::CaptureSnapshot(io, options); + + ImGui::EndFrame(); + + EXPECT_FLOAT_EQ(snapshot.pointerPosition.x, 100.0f); + EXPECT_FLOAT_EQ(snapshot.pointerPosition.y, 60.0f); + EXPECT_TRUE(snapshot.pointerInside); + EXPECT_TRUE(snapshot.pointerButtonsDown[0]); + EXPECT_FLOAT_EQ(snapshot.wheelDelta.x, 0.0f); + EXPECT_FLOAT_EQ(snapshot.wheelDelta.y, 1.0f); + EXPECT_TRUE(snapshot.modifiers.control); + EXPECT_FALSE(snapshot.windowFocused); + EXPECT_TRUE(snapshot.wantCaptureMouse); + EXPECT_TRUE(snapshot.wantCaptureKeyboard); + EXPECT_TRUE(snapshot.wantTextInput); + EXPECT_EQ(snapshot.timestampNanoseconds, 99u); + EXPECT_TRUE(snapshot.IsKeyDown(static_cast(KeyCode::LeftCtrl))); + EXPECT_TRUE(snapshot.IsKeyDown(static_cast(KeyCode::P))); + ASSERT_EQ(snapshot.characters.size(), 1u); + EXPECT_EQ(snapshot.characters[0], static_cast('p')); +} + +TEST(ImGuiXCUIInputAdapterTest, MapKeyCodeReturnsXCUIKeyCodesForNamedKeys) { + EXPECT_EQ( + ImGuiXCUIInputAdapter::MapKeyCode(ImGuiKey_A), + static_cast(KeyCode::A)); + EXPECT_EQ( + ImGuiXCUIInputAdapter::MapKeyCode(ImGuiKey_F12), + static_cast(KeyCode::F12)); + EXPECT_EQ( + ImGuiXCUIInputAdapter::MapKeyCode(ImGuiKey_None), + static_cast(KeyCode::None)); +} diff --git a/tests/NewEditor/test_main_window_native_backdrop_renderer_api.cpp b/tests/NewEditor/test_main_window_native_backdrop_renderer_api.cpp new file mode 100644 index 00000000..300692da --- /dev/null +++ b/tests/NewEditor/test_main_window_native_backdrop_renderer_api.cpp @@ -0,0 +1,32 @@ +#include + +#include "Rendering/MainWindowNativeBackdropRenderer.h" + +#include + +namespace { + +using XCEngine::NewEditor::MainWindowNativeBackdropRenderer; +using XCEngine::Rendering::RenderContext; +using XCEngine::Rendering::RenderSurface; + +TEST(NewEditorMainWindowNativeBackdropRendererApiTest, ExposesFrameStateAndSurfaceRenderEntry) { + using FrameState = MainWindowNativeBackdropRenderer::FrameState; + + static_assert(std::is_same_v< + decltype(std::declval().elapsedSeconds), + float>); + static_assert(std::is_same_v< + decltype(std::declval().pulseAccent), + bool>); + static_assert(std::is_same_v< + decltype(std::declval().Render( + std::declval(), + std::declval(), + std::declval())), + bool>); + + SUCCEED(); +} + +} // namespace diff --git a/tests/NewEditor/test_new_editor_imgui_transition_backend.cpp b/tests/NewEditor/test_new_editor_imgui_transition_backend.cpp new file mode 100644 index 00000000..272fa26c --- /dev/null +++ b/tests/NewEditor/test_new_editor_imgui_transition_backend.cpp @@ -0,0 +1,306 @@ +#include + +#include "XCUIBackend/ImGuiTransitionBackend.h" + +#include + +#include + +namespace { + +class ImGuiContextScope { +public: + ImGuiContextScope() { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + } + + ~ImGuiContextScope() { + ImGui::DestroyContext(); + } +}; + +void PrepareImGui(float width = 1024.0f, float height = 768.0f) { + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(width, height); + io.DeltaTime = 1.0f / 60.0f; + unsigned char* fontPixels = nullptr; + int fontWidth = 0; + int fontHeight = 0; + io.Fonts->GetTexDataAsRGBA32(&fontPixels, &fontWidth, &fontHeight); + io.Fonts->SetTexID(static_cast(1)); +} + +} // namespace + +TEST(NewEditorImGuiTransitionBackendTest, BeginFrameClearsPendingQueue) { + ImGuiContextScope contextScope; + + XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {}; + XCEngine::UI::UIDrawList drawList("Pending"); + drawList.AddFilledRect( + XCEngine::UI::UIRect(0.0f, 0.0f, 20.0f, 20.0f), + XCEngine::UI::UIColor(1.0f, 0.0f, 0.0f, 1.0f)); + + backend.BeginFrame(); + backend.Submit(drawList); + ASSERT_EQ(backend.GetPendingDrawListCount(), 1u); + ASSERT_EQ(backend.GetPendingCommandCount(), 1u); + + backend.BeginFrame(); + EXPECT_EQ(backend.GetPendingDrawListCount(), 0u); + EXPECT_EQ(backend.GetPendingCommandCount(), 0u); +} + +TEST(NewEditorImGuiTransitionBackendTest, EndFrameFlushesSubmittedCommands) { + ImGuiContextScope contextScope; + PrepareImGui(); + + XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {}; + XCEngine::UI::UIDrawList drawList("Flush"); + drawList.PushClipRect(XCEngine::UI::UIRect(0.0f, 0.0f, 240.0f, 160.0f)); + drawList.AddFilledRect( + XCEngine::UI::UIRect(10.0f, 10.0f, 70.0f, 45.0f), + XCEngine::UI::UIColor(0.2f, 0.4f, 0.6f, 1.0f), + 6.0f); + drawList.AddRectOutline( + XCEngine::UI::UIRect(10.0f, 10.0f, 70.0f, 45.0f), + XCEngine::UI::UIColor(0.9f, 0.9f, 0.9f, 1.0f), + 2.0f, + 6.0f); + drawList.PopClipRect(); + + backend.BeginFrame(); + backend.Submit(drawList); + + ImGui::NewFrame(); + ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendTest")); + ImDrawList* targetDrawList = ImGui::GetWindowDrawList(); + ASSERT_NE(targetDrawList, nullptr); + + const bool flushed = backend.EndFrame(targetDrawList); + + ImGui::End(); + ImGui::EndFrame(); + + EXPECT_TRUE(flushed); + EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 1u); + EXPECT_EQ(backend.GetLastFlushedCommandCount(), 4u); + EXPECT_EQ(backend.GetPendingCommandCount(), 0u); + EXPECT_GT(targetDrawList->VtxBuffer.Size, 0); + EXPECT_GT(targetDrawList->CmdBuffer.Size, 0); +} + +TEST(NewEditorImGuiTransitionBackendTest, EndFrameFlushesMultipleDrawLists) { + ImGuiContextScope contextScope; + PrepareImGui(800.0f, 600.0f); + + XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {}; + + XCEngine::UI::UIDrawList firstDrawList("First"); + firstDrawList.AddFilledRect( + XCEngine::UI::UIRect(8.0f, 8.0f, 40.0f, 30.0f), + XCEngine::UI::UIColor(0.7f, 0.1f, 0.1f, 1.0f)); + + XCEngine::UI::UIDrawList secondDrawList("Second"); + secondDrawList.PushClipRect(XCEngine::UI::UIRect(0.0f, 0.0f, 300.0f, 200.0f)); + secondDrawList.AddRectOutline( + XCEngine::UI::UIRect(24.0f, 26.0f, 52.0f, 28.0f), + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 1.5f, + 5.0f); + secondDrawList.PopClipRect(); + + backend.BeginFrame(); + backend.Submit(firstDrawList); + backend.Submit(std::move(secondDrawList)); + ASSERT_EQ(backend.GetPendingDrawListCount(), 2u); + ASSERT_EQ(backend.GetPendingCommandCount(), 4u); + + ImGui::NewFrame(); + ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendMultiDrawList")); + ImDrawList* targetDrawList = ImGui::GetWindowDrawList(); + ASSERT_NE(targetDrawList, nullptr); + + const bool flushed = backend.EndFrame(targetDrawList); + + ImGui::End(); + ImGui::EndFrame(); + + EXPECT_TRUE(flushed); + EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 2u); + EXPECT_EQ(backend.GetLastFlushedCommandCount(), 4u); + EXPECT_EQ(backend.GetPendingDrawListCount(), 0u); + EXPECT_EQ(backend.GetPendingCommandCount(), 0u); + EXPECT_GT(targetDrawList->CmdBuffer.Size, 0); +} + +TEST(NewEditorImGuiTransitionBackendTest, EndFrameWithNoPendingDataLeavesFlushCountsAtZero) { + ImGuiContextScope contextScope; + PrepareImGui(640.0f, 480.0f); + + XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {}; + backend.BeginFrame(); + + ImGui::NewFrame(); + ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendEmptyFrame")); + ImDrawList* targetDrawList = ImGui::GetWindowDrawList(); + ASSERT_NE(targetDrawList, nullptr); + + const bool flushed = backend.EndFrame(targetDrawList); + + ImGui::End(); + ImGui::EndFrame(); + + EXPECT_TRUE(flushed); + EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 0u); + EXPECT_EQ(backend.GetLastFlushedCommandCount(), 0u); + EXPECT_EQ(backend.GetPendingDrawListCount(), 0u); + EXPECT_EQ(backend.GetPendingCommandCount(), 0u); +} + +TEST(NewEditorImGuiTransitionBackendTest, SubmitDrawDataAggregatesMultipleListsBeforeFlush) { + ImGuiContextScope contextScope; + PrepareImGui(720.0f, 512.0f); + + XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {}; + + XCEngine::UI::UIDrawData drawData = {}; + XCEngine::UI::UIDrawList& firstDrawList = drawData.EmplaceDrawList("First"); + firstDrawList.AddFilledRect( + XCEngine::UI::UIRect(12.0f, 14.0f, 48.0f, 24.0f), + XCEngine::UI::UIColor(0.4f, 0.5f, 0.9f, 1.0f)); + + XCEngine::UI::UIDrawList& secondDrawList = drawData.EmplaceDrawList("Second"); + secondDrawList.AddRectOutline( + XCEngine::UI::UIRect(36.0f, 42.0f, 64.0f, 28.0f), + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 2.0f, + 3.0f); + secondDrawList.AddText( + XCEngine::UI::UIPoint(40.0f, 48.0f), + "xcui", + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 14.0f); + + backend.BeginFrame(); + backend.Submit(drawData); + ASSERT_EQ(backend.GetPendingDrawListCount(), 2u); + ASSERT_EQ(backend.GetPendingCommandCount(), 3u); + + ImGui::NewFrame(); + ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendDrawData")); + ImDrawList* targetDrawList = ImGui::GetWindowDrawList(); + ASSERT_NE(targetDrawList, nullptr); + + const bool flushed = backend.EndFrame(targetDrawList); + + ImGui::End(); + ImGui::EndFrame(); + + EXPECT_TRUE(flushed); + EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 2u); + EXPECT_EQ(backend.GetLastFlushedCommandCount(), 3u); + EXPECT_EQ(backend.GetPendingDrawListCount(), 0u); + EXPECT_EQ(backend.GetPendingCommandCount(), 0u); +} + +TEST(NewEditorImGuiTransitionBackendTest, EndFrameAcceptsTextCommandsThatUseDefaultFontSize) { + ImGuiContextScope contextScope; + PrepareImGui(720.0f, 512.0f); + + XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {}; + XCEngine::UI::UIDrawList drawList("DefaultFontText"); + drawList.AddText( + XCEngine::UI::UIPoint(24.0f, 30.0f), + "fallback font text", + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 0.0f); + + backend.BeginFrame(); + backend.Submit(drawList); + + ImGui::NewFrame(); + ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendDefaultFontText")); + ImDrawList* targetDrawList = ImGui::GetWindowDrawList(); + ASSERT_NE(targetDrawList, nullptr); + + const bool flushed = backend.EndFrame(targetDrawList); + + ImGui::End(); + ImGui::EndFrame(); + + EXPECT_TRUE(flushed); + EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 1u); + EXPECT_EQ(backend.GetLastFlushedCommandCount(), 1u); + EXPECT_EQ(backend.GetPendingDrawListCount(), 0u); + EXPECT_EQ(backend.GetPendingCommandCount(), 0u); +} + +TEST(NewEditorImGuiTransitionBackendTest, EndFrameSkipsEmptyTextGeometryButClearsPendingState) { + ImGuiContextScope contextScope; + PrepareImGui(720.0f, 512.0f); + + XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {}; + XCEngine::UI::UIDrawList drawList("EmptyText"); + drawList.AddText( + XCEngine::UI::UIPoint(24.0f, 30.0f), + "", + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 14.0f); + + backend.BeginFrame(); + backend.Submit(drawList); + + ImGui::NewFrame(); + ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendEmptyText")); + ImGui::TextUnformatted("anchor"); + ImDrawList* targetDrawList = ImGui::GetWindowDrawList(); + ASSERT_NE(targetDrawList, nullptr); + const int initialVertexCount = targetDrawList->VtxBuffer.Size; + + const bool flushed = backend.EndFrame(targetDrawList); + + ImGui::End(); + ImGui::EndFrame(); + + EXPECT_TRUE(flushed); + EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 1u); + EXPECT_EQ(backend.GetLastFlushedCommandCount(), 1u); + EXPECT_EQ(targetDrawList->VtxBuffer.Size, initialVertexCount); + EXPECT_EQ(backend.GetPendingDrawListCount(), 0u); + EXPECT_EQ(backend.GetPendingCommandCount(), 0u); +} + +TEST(NewEditorImGuiTransitionBackendTest, EndFrameRestoresClipRectStackAfterUnbalancedPush) { + ImGuiContextScope contextScope; + PrepareImGui(800.0f, 600.0f); + + XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {}; + XCEngine::UI::UIDrawList drawList("ClipRecovery"); + drawList.PushClipRect(XCEngine::UI::UIRect(0.0f, 0.0f, 180.0f, 120.0f)); + drawList.AddFilledRect( + XCEngine::UI::UIRect(16.0f, 18.0f, 52.0f, 30.0f), + XCEngine::UI::UIColor(0.9f, 0.3f, 0.2f, 1.0f)); + + backend.BeginFrame(); + backend.Submit(drawList); + + ImGui::NewFrame(); + ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendClipRecovery")); + ImDrawList* targetDrawList = ImGui::GetWindowDrawList(); + ASSERT_NE(targetDrawList, nullptr); + const int initialClipDepth = targetDrawList->_ClipRectStack.Size; + + const bool flushed = backend.EndFrame(targetDrawList); + + EXPECT_TRUE(flushed); + EXPECT_EQ(targetDrawList->_ClipRectStack.Size, initialClipDepth); + EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 1u); + EXPECT_EQ(backend.GetLastFlushedCommandCount(), 2u); + EXPECT_EQ(backend.GetPendingCommandCount(), 0u); + + ImGui::End(); + ImGui::EndFrame(); +} diff --git a/tests/NewEditor/test_xcui_asset_document_source.cpp b/tests/NewEditor/test_xcui_asset_document_source.cpp new file mode 100644 index 00000000..c08814bb --- /dev/null +++ b/tests/NewEditor/test_xcui_asset_document_source.cpp @@ -0,0 +1,257 @@ +#include + +#include "XCUIBackend/XCUIAssetDocumentSource.h" + +#include +#include + +#include +#include +#include +#include + +namespace { + +namespace fs = std::filesystem; + +using XCEngine::Containers::String; +using XCEngine::Editor::XCUIBackend::XCUIAssetDocumentSource; +using XCEngine::Resources::ResourceManager; + +String ToContainersString(const std::string& value) { + return String(value.c_str()); +} + +void WriteTextFile(const fs::path& path, const std::string& contents) { + fs::create_directories(path.parent_path()); + std::ofstream output(path, std::ios::binary | std::ios::trunc); + output << contents; +} + +std::string BuildMinimalViewDocument(const std::string& themeReference) { + return + "\n" + " \n" + " \n" + " \n" + "\n"; +} + +std::string BuildMinimalThemeDocument() { + return + "\n" + " \n" + "\n"; +} + +class ScopedCurrentPath { +public: + explicit ScopedCurrentPath(const fs::path& newPath) { + m_originalPath = fs::current_path(); + fs::create_directories(newPath); + fs::current_path(newPath); + } + + ~ScopedCurrentPath() { + if (!m_originalPath.empty()) { + fs::current_path(m_originalPath); + } + } + +private: + fs::path m_originalPath = {}; +}; + +class XCUIAssetDocumentSourceTest : public ::testing::Test { +protected: + void SetUp() override { + m_originalResourceRoot = ResourceManager::Get().GetResourceRoot(); + ResourceManager::Get().SetResourceRoot(String()); + + m_tempRoot = fs::temp_directory_path() / + fs::path("xcui_asset_document_source_tests"); + m_tempRoot /= fs::path(::testing::UnitTest::GetInstance()->current_test_info()->name()); + fs::remove_all(m_tempRoot); + fs::create_directories(m_tempRoot); + } + + void TearDown() override { + ResourceManager::Get().UnloadAll(); + ResourceManager::Get().SetResourceRoot(m_originalResourceRoot); + + std::error_code ec; + fs::remove_all(m_tempRoot, ec); + } + + fs::path CreateRepositorySubdir(const std::string& relativePath) const { + const fs::path path = m_tempRoot / fs::path(relativePath); + fs::create_directories(path); + return path; + } + + void WriteProjectDocuments(const XCUIAssetDocumentSource::PathSet& paths) const { + WriteTextFile( + m_tempRoot / fs::path(paths.view.primaryRelativePath), + BuildMinimalViewDocument("Theme.xctheme")); + WriteTextFile( + m_tempRoot / fs::path(paths.theme.primaryRelativePath), + BuildMinimalThemeDocument()); + } + + void WriteLegacyDocuments(const XCUIAssetDocumentSource::PathSet& paths) const { + WriteTextFile( + m_tempRoot / fs::path(paths.view.legacyRelativePath), + BuildMinimalViewDocument( + fs::path(paths.theme.legacyRelativePath).filename().generic_string())); + WriteTextFile( + m_tempRoot / fs::path(paths.theme.legacyRelativePath), + BuildMinimalThemeDocument()); + } + + fs::path m_tempRoot = {}; + +private: + String m_originalResourceRoot = {}; +}; + +TEST_F(XCUIAssetDocumentSourceTest, DemoAndLayoutLabPathSetsUseExpectedPaths) { + const XCUIAssetDocumentSource::PathSet demoPaths = + XCUIAssetDocumentSource::MakeDemoPathSet(); + EXPECT_EQ(demoPaths.setName, "Demo"); + EXPECT_EQ(demoPaths.view.primaryRelativePath, "Assets/XCUI/NewEditor/Demo/View.xcui"); + EXPECT_EQ(demoPaths.theme.primaryRelativePath, "Assets/XCUI/NewEditor/Demo/Theme.xctheme"); + EXPECT_EQ(demoPaths.view.legacyRelativePath, "new_editor/resources/xcui_demo_view.xcui"); + EXPECT_EQ(demoPaths.theme.legacyRelativePath, "new_editor/resources/xcui_demo_theme.xctheme"); + + const XCUIAssetDocumentSource::PathSet layoutLabPaths = + XCUIAssetDocumentSource::MakeLayoutLabPathSet(); + EXPECT_EQ(layoutLabPaths.setName, "LayoutLab"); + EXPECT_EQ(layoutLabPaths.view.primaryRelativePath, "Assets/XCUI/NewEditor/LayoutLab/View.xcui"); + EXPECT_EQ(layoutLabPaths.theme.primaryRelativePath, "Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme"); + EXPECT_EQ(layoutLabPaths.view.legacyRelativePath, "new_editor/resources/xcui_layout_lab_view.xcui"); + EXPECT_EQ(layoutLabPaths.theme.legacyRelativePath, "new_editor/resources/xcui_layout_lab_theme.xctheme"); +} + +TEST_F(XCUIAssetDocumentSourceTest, MakePathSetSanitizesNamesAndBuildsLegacySnakeCase) { + const XCUIAssetDocumentSource::PathSet paths = + XCUIAssetDocumentSource::MakePathSet(" Layout Lab! Debug-42 "); + + EXPECT_EQ(paths.setName, "LayoutLabDebug-42"); + EXPECT_EQ( + paths.view.primaryRelativePath, + "Assets/XCUI/NewEditor/LayoutLabDebug-42/View.xcui"); + EXPECT_EQ( + paths.theme.legacyRelativePath, + "new_editor/resources/xcui_layout_lab_debug_42_theme.xctheme"); +} + +TEST_F(XCUIAssetDocumentSourceTest, CollectCandidatePathsPrefersProjectAssetsBeforeLegacyMirror) { + const XCUIAssetDocumentSource::PathSet paths = + XCUIAssetDocumentSource::MakePathSet("Worker1CandidateOrder"); + WriteProjectDocuments(paths); + WriteLegacyDocuments(paths); + + const std::vector candidates = + XCUIAssetDocumentSource::CollectCandidatePaths(paths.view, m_tempRoot, fs::path()); + + ASSERT_EQ(candidates.size(), 2u); + EXPECT_EQ(candidates[0].origin, XCUIAssetDocumentSource::PathOrigin::ProjectAssets); + EXPECT_EQ(candidates[0].resolvedPath, fs::path(m_tempRoot / paths.view.primaryRelativePath).lexically_normal()); + EXPECT_EQ(candidates[1].origin, XCUIAssetDocumentSource::PathOrigin::LegacyMirror); + EXPECT_EQ(candidates[1].resolvedPath, fs::path(m_tempRoot / paths.view.legacyRelativePath).lexically_normal()); +} + +TEST_F(XCUIAssetDocumentSourceTest, DiagnoseRepositoryRootReportsProjectAssetAncestor) { + const XCUIAssetDocumentSource::PathSet paths = + XCUIAssetDocumentSource::MakeDemoPathSet(); + WriteProjectDocuments(paths); + + const fs::path searchRoot = CreateRepositorySubdir("tools/worker1/deep"); + const XCUIAssetDocumentSource::RepositoryDiscovery discovery = + XCUIAssetDocumentSource::DiagnoseRepositoryRoot(paths, { searchRoot }, false); + + EXPECT_EQ(discovery.repositoryRoot, m_tempRoot.lexically_normal()); + ASSERT_EQ(discovery.probes.size(), 1u); + EXPECT_TRUE(discovery.probes[0].matched); + EXPECT_EQ(discovery.probes[0].searchRoot, searchRoot.lexically_normal()); + EXPECT_EQ(discovery.probes[0].matchedRelativePath, paths.view.primaryRelativePath); + EXPECT_NE(discovery.statusMessage.find(paths.view.primaryRelativePath), std::string::npos); +} + +TEST_F(XCUIAssetDocumentSourceTest, DiagnoseRepositoryRootReportsLegacyMirrorAncestor) { + const XCUIAssetDocumentSource::PathSet paths = + XCUIAssetDocumentSource::MakeLayoutLabPathSet(); + WriteLegacyDocuments(paths); + + const fs::path searchRoot = CreateRepositorySubdir("sandbox/runtime/session"); + const XCUIAssetDocumentSource::RepositoryDiscovery discovery = + XCUIAssetDocumentSource::DiagnoseRepositoryRoot(paths, { searchRoot }, false); + + EXPECT_EQ(discovery.repositoryRoot, m_tempRoot.lexically_normal()); + ASSERT_EQ(discovery.probes.size(), 1u); + EXPECT_TRUE(discovery.probes[0].matched); + EXPECT_EQ(discovery.probes[0].matchedRelativePath, paths.view.legacyRelativePath); + EXPECT_NE(discovery.statusMessage.find(paths.view.legacyRelativePath), std::string::npos); +} + +TEST_F(XCUIAssetDocumentSourceTest, ReloadUsesLegacyFallbackAndTracksSourceChanges) { + const XCUIAssetDocumentSource::PathSet paths = + XCUIAssetDocumentSource::MakePathSet("Worker1HotReloadRegression"); + WriteLegacyDocuments(paths); + + const fs::path sandboxPath = CreateRepositorySubdir("runtime/worker1"); + ScopedCurrentPath scopedCurrentPath(sandboxPath); + + XCUIAssetDocumentSource source(paths); + ASSERT_TRUE(source.Reload()); + + const XCUIAssetDocumentSource::LoadState& initialState = source.GetState(); + EXPECT_TRUE(initialState.succeeded); + EXPECT_TRUE(initialState.usedLegacyFallback); + EXPECT_TRUE(initialState.changeTrackingReady); + EXPECT_TRUE(initialState.missingTrackedSourcePaths.empty()); + EXPECT_EQ(initialState.repositoryRoot, m_tempRoot.lexically_normal()); + EXPECT_EQ(initialState.view.pathOrigin, XCUIAssetDocumentSource::PathOrigin::LegacyMirror); + EXPECT_EQ(initialState.theme.pathOrigin, XCUIAssetDocumentSource::PathOrigin::LegacyMirror); + EXPECT_FALSE(initialState.trackedSourcePaths.empty()); + EXPECT_NE(initialState.trackingStatusMessage.find("Tracking "), std::string::npos); + EXPECT_FALSE(source.HasTrackedChanges()); + + const fs::path themePath = m_tempRoot / fs::path(paths.theme.legacyRelativePath); + fs::last_write_time( + themePath, + fs::last_write_time(themePath) + std::chrono::seconds(2)); + + EXPECT_TRUE(source.HasTrackedChanges()); + ASSERT_TRUE(source.ReloadIfChanged()); + + const XCUIAssetDocumentSource::LoadState& reloadedState = source.GetState(); + EXPECT_EQ( + reloadedState.view.backend, + XCUIAssetDocumentSource::LoadBackend::CompilerFallback); + EXPECT_EQ( + reloadedState.theme.backend, + XCUIAssetDocumentSource::LoadBackend::CompilerFallback); + EXPECT_TRUE(reloadedState.changeTrackingReady); +} + +TEST_F(XCUIAssetDocumentSourceTest, ReloadFailureIncludesRepositoryDiscoveryDiagnostic) { + const XCUIAssetDocumentSource::PathSet paths = + XCUIAssetDocumentSource::MakePathSet("Worker1MissingDocuments"); + const fs::path sandboxPath = CreateRepositorySubdir("runtime/missing"); + ScopedCurrentPath scopedCurrentPath(sandboxPath); + + XCUIAssetDocumentSource source(paths); + EXPECT_FALSE(source.Reload()); + + const XCUIAssetDocumentSource::LoadState& state = source.GetState(); + EXPECT_FALSE(state.succeeded); + EXPECT_TRUE(state.repositoryRoot.empty()); + EXPECT_NE(state.repositoryDiscovery.statusMessage.find("Repository root not found"), std::string::npos); + EXPECT_NE(state.errorMessage.find(paths.view.primaryRelativePath), std::string::npos); + EXPECT_NE(state.errorMessage.find("Repository root not found"), std::string::npos); + EXPECT_TRUE(state.view.candidatePaths.empty()); + EXPECT_TRUE(state.view.attemptMessages.empty()); +} + +} // namespace diff --git a/tests/NewEditor/test_xcui_demo_runtime.cpp b/tests/NewEditor/test_xcui_demo_runtime.cpp new file mode 100644 index 00000000..aef61411 --- /dev/null +++ b/tests/NewEditor/test_xcui_demo_runtime.cpp @@ -0,0 +1,336 @@ +#include + +#include "XCUIBackend/XCUIDemoRuntime.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +namespace fs = std::filesystem; + +using XCEngine::UI::UIDrawCommand; +using XCEngine::UI::UIDrawCommandType; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; + +XCEngine::Editor::XCUIBackend::XCUIDemoInputState BuildInputState( + float width = 720.0f, + float height = 420.0f) { + XCEngine::Editor::XCUIBackend::XCUIDemoInputState input = {}; + input.canvasRect = XCEngine::UI::UIRect(0.0f, 0.0f, width, height); + input.pointerPosition = XCEngine::UI::UIPoint(width * 0.5f, height * 0.5f); + input.pointerInside = true; + return input; +} + +UIInputEvent MakeCharacterEvent(std::uint32_t character) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = character; + return event; +} + +UIInputEvent MakeKeyDownEvent(XCEngine::Input::KeyCode keyCode, bool repeat = false) { + UIInputEvent event = {}; + event.type = UIInputEventType::KeyDown; + event.keyCode = static_cast(keyCode); + event.repeat = repeat; + return event; +} + +fs::path FindDemoResourcePath() { + fs::path probe = fs::current_path(); + for (int i = 0; i < 8; ++i) { + const fs::path canonicalCandidate = probe / "Assets/XCUI/NewEditor/Demo/View.xcui"; + if (fs::exists(canonicalCandidate)) { + return canonicalCandidate; + } + + const fs::path legacyCandidate = probe / "new_editor/resources/xcui_demo_view.xcui"; + if (fs::exists(legacyCandidate)) { + return legacyCandidate; + } + + if (!probe.has_parent_path()) { + break; + } + probe = probe.parent_path(); + } + + return {}; +} + +std::vector CollectTextCommands(const XCEngine::UI::UIDrawData& drawData) { + std::vector textCommands = {}; + for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { + for (const UIDrawCommand& command : drawList.GetCommands()) { + if (command.type == UIDrawCommandType::Text) { + textCommands.push_back(&command); + } + } + } + return textCommands; +} + +const UIDrawCommand* FindTextCommand( + const XCEngine::UI::UIDrawData& drawData, + const std::string& text) { + for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { + for (const UIDrawCommand& command : drawList.GetCommands()) { + if (command.type == UIDrawCommandType::Text && command.text == text) { + return &command; + } + } + } + return nullptr; +} + +class FileTimestampRestoreScope { +public: + explicit FileTimestampRestoreScope(fs::path path) + : m_path(std::move(path)) { + if (!m_path.empty() && fs::exists(m_path)) { + m_originalWriteTime = fs::last_write_time(m_path); + std::ifstream input(m_path, std::ios::binary); + std::ostringstream stream; + stream << input.rdbuf(); + m_originalContents = stream.str(); + m_valid = true; + } + } + + ~FileTimestampRestoreScope() { + if (m_valid) { + std::ofstream output(m_path, std::ios::binary | std::ios::trunc); + output << m_originalContents; + output.close(); + fs::last_write_time(m_path, m_originalWriteTime); + } + } + +private: + fs::path m_path; + fs::file_time_type m_originalWriteTime = {}; + std::string m_originalContents = {}; + bool m_valid = false; +}; + +} // namespace + +TEST(NewEditorXCUIDemoRuntimeTest, UpdateProvidesDeterministicFrameContainer) { + XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; + const bool reloadSucceeded = runtime.ReloadDocuments(); + + const auto& firstFrame = runtime.Update(BuildInputState()); + EXPECT_EQ(firstFrame.stats.documentsReady, reloadSucceeded); + EXPECT_EQ(firstFrame.stats.drawListCount, firstFrame.drawData.GetDrawListCount()); + EXPECT_EQ(firstFrame.stats.commandCount, firstFrame.drawData.GetTotalCommandCount()); + + const auto& secondFrame = runtime.Update(BuildInputState()); + EXPECT_GE(secondFrame.stats.treeGeneration, firstFrame.stats.treeGeneration); + EXPECT_EQ(secondFrame.stats.drawListCount, secondFrame.drawData.GetDrawListCount()); + EXPECT_EQ(secondFrame.stats.commandCount, secondFrame.drawData.GetTotalCommandCount()); + + if (secondFrame.stats.documentsReady) { + EXPECT_GT(secondFrame.stats.elementCount, 0u); + EXPECT_GT(secondFrame.stats.drawListCount, 0u); + EXPECT_GT(secondFrame.stats.commandCount, 0u); + } else { + EXPECT_FALSE(secondFrame.stats.statusMessage.empty()); + } +} + +TEST(NewEditorXCUIDemoRuntimeTest, RuntimeFrameEmitsTextCommandsWithResolvedFontSizes) { + XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; + ASSERT_TRUE(runtime.ReloadDocuments()); + + const auto& frame = runtime.Update(BuildInputState()); + ASSERT_TRUE(frame.stats.documentsReady); + + const std::vector textCommands = CollectTextCommands(frame.drawData); + ASSERT_FALSE(textCommands.empty()); + + for (const UIDrawCommand* command : textCommands) { + ASSERT_NE(command, nullptr); + EXPECT_FALSE(command->text.empty()); + EXPECT_GT(command->fontSize, 0.0f); + } + + const UIDrawCommand* titleCommand = FindTextCommand(frame.drawData, "New XCUI Shell"); + ASSERT_NE(titleCommand, nullptr); + EXPECT_FLOAT_EQ(titleCommand->fontSize, 18.0f); + + const UIDrawCommand* metricValueCommand = FindTextCommand(frame.drawData, "Driven by runtime"); + ASSERT_NE(metricValueCommand, nullptr); + EXPECT_FLOAT_EQ(metricValueCommand->fontSize, 18.0f); + + const UIDrawCommand* buttonLabelCommand = FindTextCommand(frame.drawData, "Toggle Accent"); + ASSERT_NE(buttonLabelCommand, nullptr); + EXPECT_FLOAT_EQ(buttonLabelCommand->fontSize, 14.0f); +} + +TEST(NewEditorXCUIDemoRuntimeTest, InputStateTransitionsAreAcceptedAndFrameStillBuilds) { + XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; + runtime.ReloadDocuments(); + + XCEngine::Editor::XCUIBackend::XCUIDemoInputState frameInput = BuildInputState(); + frameInput.pointerPressed = true; + frameInput.pointerDown = true; + const auto& pressedFrame = runtime.Update(frameInput); + + frameInput.pointerPressed = false; + frameInput.pointerReleased = true; + frameInput.pointerDown = false; + frameInput.shortcutPressed = true; + const auto& releasedFrame = runtime.Update(frameInput); + + EXPECT_GE(releasedFrame.stats.treeGeneration, pressedFrame.stats.treeGeneration); + EXPECT_EQ(releasedFrame.stats.drawListCount, releasedFrame.drawData.GetDrawListCount()); + EXPECT_EQ(releasedFrame.stats.commandCount, releasedFrame.drawData.GetTotalCommandCount()); + + if (releasedFrame.stats.documentsReady) { + EXPECT_GT(releasedFrame.stats.elementCount, 0u); + EXPECT_GE(releasedFrame.stats.dirtyRootCount, 0u); + } +} + +TEST(NewEditorXCUIDemoRuntimeTest, PointerToggleUpdatesFocusStatusTextAndAccentState) { + XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; + ASSERT_TRUE(runtime.ReloadDocuments()); + + const auto& baselineFrame = runtime.Update(BuildInputState()); + ASSERT_TRUE(baselineFrame.stats.documentsReady); + EXPECT_FALSE(baselineFrame.stats.accentEnabled); + EXPECT_NE( + FindTextCommand(baselineFrame.drawData, "Markup -> Layout -> Style -> DrawData"), + nullptr); + + XCEngine::UI::UIRect buttonRect = {}; + ASSERT_TRUE(runtime.TryGetElementRect("toggleAccent", buttonRect)); + + const XCEngine::UI::UIPoint buttonCenter( + buttonRect.x + buttonRect.width * 0.5f, + buttonRect.y + buttonRect.height * 0.5f); + + XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState(); + pressedInput.pointerPosition = buttonCenter; + pressedInput.pointerPressed = true; + pressedInput.pointerDown = true; + const auto& pressedFrame = runtime.Update(pressedInput); + ASSERT_TRUE(pressedFrame.stats.documentsReady); + + XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState(); + releasedInput.pointerPosition = buttonCenter; + releasedInput.pointerReleased = true; + const auto& toggledFrame = runtime.Update(releasedInput); + + ASSERT_TRUE(toggledFrame.stats.documentsReady); + EXPECT_TRUE(toggledFrame.stats.accentEnabled); + EXPECT_EQ(toggledFrame.stats.lastCommandId, "demo.toggleAccent"); + EXPECT_EQ(toggledFrame.stats.focusedElementId, "toggleAccent"); + + const UIDrawCommand* focusStatusCommand = FindTextCommand( + toggledFrame.drawData, + "Focus: toggleAccent"); + ASSERT_NE(focusStatusCommand, nullptr); + EXPECT_FLOAT_EQ(focusStatusCommand->fontSize, 14.0f); +} + +TEST(NewEditorXCUIDemoRuntimeTest, UpdateAutoReloadsWhenSourceTimestampChanges) { + const fs::path viewPath = FindDemoResourcePath(); + ASSERT_FALSE(viewPath.empty()); + ASSERT_TRUE(fs::exists(viewPath)); + + FileTimestampRestoreScope restoreScope(viewPath); + + XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; + ASSERT_TRUE(runtime.ReloadDocuments()); + + const auto& baselineFrame = runtime.Update(BuildInputState()); + ASSERT_TRUE(baselineFrame.stats.documentsReady); + XCEngine::UI::UIRect probeRect = {}; + EXPECT_FALSE(runtime.TryGetElementRect("autoReloadProbe", probeRect)); + + std::ifstream input(viewPath, std::ios::binary); + std::ostringstream stream; + stream << input.rdbuf(); + const std::string originalContents = stream.str(); + input.close(); + + const std::string marker = "\n"; + const std::size_t insertPosition = originalContents.rfind(marker); + ASSERT_NE(insertPosition, std::string::npos); + + const std::string injectedNode = + " \n"; + std::string modifiedContents = originalContents; + modifiedContents.insert(insertPosition, injectedNode); + + std::ofstream output(viewPath, std::ios::binary | std::ios::trunc); + output << modifiedContents; + output.close(); + + const fs::file_time_type originalWriteTime = fs::last_write_time(viewPath); + fs::last_write_time(viewPath, originalWriteTime + std::chrono::seconds(2)); + + const auto& reloadedFrame = runtime.Update(BuildInputState()); + EXPECT_TRUE(reloadedFrame.stats.documentsReady); + EXPECT_GT(reloadedFrame.stats.elementCount, 0u); + EXPECT_GT(reloadedFrame.stats.commandCount, 0u); + EXPECT_TRUE(runtime.TryGetElementRect("autoReloadProbe", probeRect)); +} + +TEST(NewEditorXCUIDemoRuntimeTest, TextFieldAcceptsUtf8CharactersAndBackspace) { + XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; + ASSERT_TRUE(runtime.ReloadDocuments()); + + const auto& baselineFrame = runtime.Update(BuildInputState()); + ASSERT_TRUE(baselineFrame.stats.documentsReady); + + XCEngine::UI::UIRect promptRect = {}; + ASSERT_TRUE(runtime.TryGetElementRect("agentPrompt", promptRect)); + + const XCEngine::UI::UIPoint promptCenter( + promptRect.x + promptRect.width * 0.5f, + promptRect.y + promptRect.height * 0.5f); + + XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState(); + pressedInput.pointerPosition = promptCenter; + pressedInput.pointerPressed = true; + pressedInput.pointerDown = true; + runtime.Update(pressedInput); + + XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState(); + releasedInput.pointerPosition = promptCenter; + releasedInput.pointerReleased = true; + const auto& focusedFrame = runtime.Update(releasedInput); + ASSERT_TRUE(focusedFrame.stats.documentsReady); + EXPECT_EQ(focusedFrame.stats.focusedElementId, "agentPrompt"); + + XCEngine::Editor::XCUIBackend::XCUIDemoInputState textInput = BuildInputState(); + textInput.events.push_back(MakeCharacterEvent('A')); + textInput.events.push_back(MakeCharacterEvent('I')); + textInput.events.push_back(MakeCharacterEvent(0x4F60u)); + const auto& typedFrame = runtime.Update(textInput); + + ASSERT_TRUE(typedFrame.stats.documentsReady); + EXPECT_EQ(typedFrame.stats.focusedElementId, "agentPrompt"); + EXPECT_NE(FindTextCommand(typedFrame.drawData, "AI你"), nullptr); + EXPECT_EQ(typedFrame.stats.lastCommandId, "demo.text.edit.agentPrompt"); + + XCEngine::Editor::XCUIBackend::XCUIDemoInputState backspaceInput = BuildInputState(); + backspaceInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::Backspace)); + const auto& backspacedFrame = runtime.Update(backspaceInput); + + ASSERT_TRUE(backspacedFrame.stats.documentsReady); + EXPECT_NE(FindTextCommand(backspacedFrame.drawData, "AI"), nullptr); + EXPECT_EQ(backspacedFrame.stats.focusedElementId, "agentPrompt"); +} diff --git a/tests/NewEditor/test_xcui_hosted_preview_presenter.cpp b/tests/NewEditor/test_xcui_hosted_preview_presenter.cpp new file mode 100644 index 00000000..ec085886 --- /dev/null +++ b/tests/NewEditor/test_xcui_hosted_preview_presenter.cpp @@ -0,0 +1,523 @@ +#include + +#include "XCUIBackend/XCUIHostedPreviewPresenter.h" + +#include + +#include + +#include + +namespace { + +using XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter; +using XCEngine::Editor::XCUIBackend::CreateQueuedNativeXCUIHostedPreviewPresenter; +using XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewDrainStats; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewQueue; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewQueuedFrame; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceRegistry; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats; + +class ImGuiContextScope { +public: + ImGuiContextScope() { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + } + + ~ImGuiContextScope() { + ImGui::DestroyContext(); + } +}; + +void PrepareImGui(float width = 1024.0f, float height = 768.0f) { + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(width, height); + io.DeltaTime = 1.0f / 60.0f; + unsigned char* fontPixels = nullptr; + int fontWidth = 0; + int fontHeight = 0; + io.Fonts->GetTexDataAsRGBA32(&fontPixels, &fontWidth, &fontHeight); + io.Fonts->SetTexID(static_cast(1)); +} + +TEST(XCUIHostedPreviewPresenterTest, PresentReturnsFalseAndClearsStatsWhenFrameHasNoDrawData) { + ImGuiContextScope contextScope; + PrepareImGui(); + + std::unique_ptr presenter = CreateImGuiXCUIHostedPreviewPresenter(); + ASSERT_NE(presenter, nullptr); + + XCUIHostedPreviewFrame frame = {}; + const bool presented = presenter->Present(frame); + const XCUIHostedPreviewStats& stats = presenter->GetLastStats(); + + EXPECT_FALSE(presented); + EXPECT_FALSE(stats.presented); + EXPECT_EQ(stats.submittedDrawListCount, 0u); + EXPECT_EQ(stats.submittedCommandCount, 0u); + EXPECT_EQ(stats.flushedDrawListCount, 0u); + EXPECT_EQ(stats.flushedCommandCount, 0u); +} + +TEST(XCUIHostedPreviewPresenterTest, PresentFlushesDrawDataIntoProvidedImGuiDrawList) { + ImGuiContextScope contextScope; + PrepareImGui(800.0f, 600.0f); + + std::unique_ptr presenter = CreateImGuiXCUIHostedPreviewPresenter(); + ASSERT_NE(presenter, nullptr); + + XCEngine::UI::UIDrawData drawData = {}; + XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("HostedPreview"); + drawList.AddFilledRect( + XCEngine::UI::UIRect(12.0f, 14.0f, 48.0f, 30.0f), + XCEngine::UI::UIColor(0.25f, 0.5f, 0.8f, 1.0f)); + drawList.AddText( + XCEngine::UI::UIPoint(18.0f, 24.0f), + "xcui", + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 14.0f); + + ImGui::NewFrame(); + ASSERT_TRUE(ImGui::Begin("HostedPreviewPresenterWindow")); + ImDrawList* targetDrawList = ImGui::GetWindowDrawList(); + ASSERT_NE(targetDrawList, nullptr); + + XCUIHostedPreviewFrame frame = {}; + frame.drawData = &drawData; + frame.targetDrawList = targetDrawList; + + const bool presented = presenter->Present(frame); + const XCUIHostedPreviewStats& stats = presenter->GetLastStats(); + + ImGui::End(); + ImGui::EndFrame(); + + EXPECT_TRUE(presented); + EXPECT_TRUE(stats.presented); + EXPECT_EQ(stats.submittedDrawListCount, 1u); + EXPECT_EQ(stats.submittedCommandCount, 2u); + EXPECT_EQ(stats.flushedDrawListCount, 1u); + EXPECT_EQ(stats.flushedCommandCount, 2u); + EXPECT_GT(targetDrawList->VtxBuffer.Size, 0); + EXPECT_GT(targetDrawList->CmdBuffer.Size, 0); +} + +TEST(XCUIHostedPreviewPresenterTest, QueuedNativePresenterCopiesFrameIntoQueue) { + XCUIHostedPreviewQueue queue = {}; + XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {}; + queue.BeginFrame(); + + std::unique_ptr presenter = + CreateQueuedNativeXCUIHostedPreviewPresenter(queue, surfaceRegistry); + ASSERT_NE(presenter, nullptr); + EXPECT_TRUE(presenter->IsNativeQueued()); + + XCEngine::UI::UIDrawData drawData = {}; + XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("HostedPreviewNative"); + drawList.AddFilledRect( + XCEngine::UI::UIRect(20.0f, 24.0f, 56.0f, 32.0f), + XCEngine::UI::UIColor(0.2f, 0.35f, 0.9f, 1.0f)); + drawList.AddText( + XCEngine::UI::UIPoint(28.0f, 36.0f), + "native", + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 16.0f); + + XCUIHostedPreviewFrame frame = {}; + frame.drawData = &drawData; + frame.debugName = "Hosted Preview Native Queue"; + frame.debugSource = "tests.hosted_preview.native_queue"; + frame.canvasRect = XCEngine::UI::UIRect(32.0f, 48.0f, 320.0f, 180.0f); + frame.logicalSize = XCEngine::UI::UISize(640.0f, 360.0f); + + EXPECT_TRUE(presenter->Present(frame)); + + const XCUIHostedPreviewStats& stats = presenter->GetLastStats(); + EXPECT_TRUE(stats.presented); + EXPECT_TRUE(stats.queuedToNativePass); + EXPECT_EQ(stats.submittedDrawListCount, 1u); + EXPECT_EQ(stats.submittedCommandCount, 2u); + EXPECT_EQ(stats.flushedDrawListCount, 0u); + EXPECT_EQ(stats.flushedCommandCount, 0u); + + drawData.Clear(); + + const auto& queuedFrames = queue.GetQueuedFrames(); + ASSERT_EQ(queuedFrames.size(), 1u); + EXPECT_EQ(queuedFrames[0].debugName, "Hosted Preview Native Queue"); + EXPECT_EQ(queuedFrames[0].debugSource, "tests.hosted_preview.native_queue"); + EXPECT_FLOAT_EQ(queuedFrames[0].canvasRect.x, 32.0f); + EXPECT_FLOAT_EQ(queuedFrames[0].canvasRect.y, 48.0f); + EXPECT_FLOAT_EQ(queuedFrames[0].canvasRect.width, 320.0f); + EXPECT_FLOAT_EQ(queuedFrames[0].canvasRect.height, 180.0f); + EXPECT_FLOAT_EQ(queuedFrames[0].logicalSize.width, 640.0f); + EXPECT_FLOAT_EQ(queuedFrames[0].logicalSize.height, 360.0f); + EXPECT_EQ(queuedFrames[0].drawData.GetDrawListCount(), 1u); + EXPECT_EQ(queuedFrames[0].drawData.GetTotalCommandCount(), 2u); +} + +TEST(XCUIHostedPreviewPresenterTest, QueuedNativePresenterFallsBackLogicalSizeToCanvasRectAndDelegatesSurfaceQueries) { + XCUIHostedPreviewQueue queue = {}; + XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {}; + queue.BeginFrame(); + surfaceRegistry.BeginFrame(); + + std::unique_ptr presenter = + CreateQueuedNativeXCUIHostedPreviewPresenter(queue, surfaceRegistry); + ASSERT_NE(presenter, nullptr); + + XCEngine::UI::UIDrawData drawData = {}; + XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("HostedPreviewFallback"); + drawList.AddFilledRect( + XCEngine::UI::UIRect(6.0f, 10.0f, 40.0f, 24.0f), + XCEngine::UI::UIColor(0.85f, 0.35f, 0.2f, 1.0f)); + + XCUIHostedPreviewFrame frame = {}; + frame.drawData = &drawData; + frame.debugName = "XCUI Demo"; + frame.debugSource = "tests.hosted_preview.logical_size_fallback"; + frame.canvasRect = XCEngine::UI::UIRect(18.0f, 22.0f, 320.0f, 180.0f); + + ASSERT_TRUE(presenter->Present(frame)); + ASSERT_EQ(queue.GetQueuedFrames().size(), 1u); + + const XCUIHostedPreviewQueuedFrame& queuedFrame = queue.GetQueuedFrames().front(); + EXPECT_FLOAT_EQ(queuedFrame.logicalSize.width, 320.0f); + EXPECT_FLOAT_EQ(queuedFrame.logicalSize.height, 180.0f); + + XCUIHostedPreviewSurfaceDescriptor descriptor = {}; + EXPECT_FALSE(presenter->TryGetSurfaceDescriptor("XCUI Demo", descriptor)); + EXPECT_TRUE(descriptor.debugName.empty()); + + surfaceRegistry.RecordQueuedFrame(queuedFrame, 0u); + + ASSERT_TRUE(presenter->TryGetSurfaceDescriptor("XCUI Demo", descriptor)); + EXPECT_EQ(descriptor.debugName, "XCUI Demo"); + EXPECT_EQ(descriptor.debugSource, "tests.hosted_preview.logical_size_fallback"); + EXPECT_FLOAT_EQ(descriptor.canvasRect.x, 18.0f); + EXPECT_FLOAT_EQ(descriptor.canvasRect.y, 22.0f); + EXPECT_FLOAT_EQ(descriptor.canvasRect.width, 320.0f); + EXPECT_FLOAT_EQ(descriptor.canvasRect.height, 180.0f); + EXPECT_FLOAT_EQ(descriptor.logicalSize.width, 320.0f); + EXPECT_FLOAT_EQ(descriptor.logicalSize.height, 180.0f); + EXPECT_EQ(descriptor.submittedDrawListCount, 1u); + EXPECT_EQ(descriptor.submittedCommandCount, 1u); + EXPECT_TRUE(descriptor.queuedThisFrame); + + XCUIHostedPreviewSurfaceImage image = {}; + EXPECT_FALSE(presenter->TryGetSurfaceImage("XCUI Demo", image)); + EXPECT_FALSE(image.IsValid()); + + surfaceRegistry.UpdateSurface( + "XCUI Demo", + static_cast(static_cast(11)), + 640u, + 360u, + XCEngine::UI::UIRect(0.0f, 0.0f, 320.0f, 180.0f)); + + ASSERT_TRUE(presenter->TryGetSurfaceImage("XCUI Demo", image)); + EXPECT_TRUE(image.IsValid()); + EXPECT_EQ(image.textureId, static_cast(static_cast(11))); + EXPECT_FLOAT_EQ(image.uvMin.x, 0.0f); + EXPECT_FLOAT_EQ(image.uvMin.y, 0.0f); + EXPECT_FLOAT_EQ(image.uvMax.x, 0.5f); + EXPECT_FLOAT_EQ(image.uvMax.y, 0.5f); +} + +TEST(XCUIHostedPreviewPresenterTest, QueuedNativePresenterRejectsMissingDrawDataAndLeavesQueueUntouched) { + XCUIHostedPreviewQueue queue = {}; + XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {}; + queue.BeginFrame(); + + std::unique_ptr presenter = + CreateQueuedNativeXCUIHostedPreviewPresenter(queue, surfaceRegistry); + ASSERT_NE(presenter, nullptr); + + XCUIHostedPreviewFrame frame = {}; + frame.debugName = "Missing DrawData"; + frame.targetDrawList = reinterpret_cast(1); + + EXPECT_FALSE(presenter->Present(frame)); + + const XCUIHostedPreviewStats& stats = presenter->GetLastStats(); + EXPECT_FALSE(stats.presented); + EXPECT_FALSE(stats.queuedToNativePass); + EXPECT_EQ(stats.submittedDrawListCount, 0u); + EXPECT_EQ(stats.submittedCommandCount, 0u); + EXPECT_EQ(stats.flushedDrawListCount, 0u); + EXPECT_EQ(stats.flushedCommandCount, 0u); + EXPECT_TRUE(queue.GetQueuedFrames().empty()); +} + +TEST(XCUIHostedPreviewPresenterTest, HostedPreviewQueuePreservesSubmissionOrderAndPayloadMetadata) { + XCUIHostedPreviewQueue queue = {}; + queue.BeginFrame(); + + XCEngine::UI::UIDrawData firstDrawData = {}; + XCEngine::UI::UIDrawList& firstDrawList = firstDrawData.EmplaceDrawList("FirstPreview"); + firstDrawList.AddFilledRect( + XCEngine::UI::UIRect(0.0f, 0.0f, 20.0f, 10.0f), + XCEngine::UI::UIColor(1.0f, 0.2f, 0.2f, 1.0f)); + + XCEngine::UI::UIDrawData secondDrawData = {}; + XCEngine::UI::UIDrawList& secondDrawList = secondDrawData.EmplaceDrawList("SecondPreview"); + secondDrawList.AddFilledRect( + XCEngine::UI::UIRect(10.0f, 8.0f, 24.0f, 14.0f), + XCEngine::UI::UIColor(0.2f, 0.7f, 1.0f, 1.0f)); + secondDrawList.AddText( + XCEngine::UI::UIPoint(12.0f, 12.0f), + "queued", + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 13.0f); + + XCUIHostedPreviewStats firstStats = {}; + XCUIHostedPreviewFrame firstFrame = {}; + firstFrame.drawData = &firstDrawData; + firstFrame.debugName = "First Native Preview"; + ASSERT_TRUE(queue.Submit(firstFrame, &firstStats)); + + XCUIHostedPreviewStats secondStats = {}; + XCUIHostedPreviewFrame secondFrame = {}; + secondFrame.drawData = &secondDrawData; + ASSERT_TRUE(queue.Submit(secondFrame, &secondStats)); + + firstDrawData.Clear(); + secondDrawData.Clear(); + + const auto& queuedFrames = queue.GetQueuedFrames(); + ASSERT_EQ(queuedFrames.size(), 2u); + + EXPECT_EQ(firstStats.submittedDrawListCount, 1u); + EXPECT_EQ(firstStats.submittedCommandCount, 1u); + EXPECT_TRUE(firstStats.queuedToNativePass); + EXPECT_EQ(secondStats.submittedDrawListCount, 1u); + EXPECT_EQ(secondStats.submittedCommandCount, 2u); + EXPECT_TRUE(secondStats.queuedToNativePass); + + EXPECT_EQ(queuedFrames[0].debugName, "First Native Preview"); + EXPECT_EQ(queuedFrames[0].drawData.GetDrawListCount(), 1u); + EXPECT_EQ(queuedFrames[0].drawData.GetTotalCommandCount(), 1u); + EXPECT_EQ(queuedFrames[1].debugName, ""); + EXPECT_EQ(queuedFrames[1].drawData.GetDrawListCount(), 1u); + EXPECT_EQ(queuedFrames[1].drawData.GetTotalCommandCount(), 2u); +} + +TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryIgnoresUnnamedQueuedFramesAndKeepsDescriptorListStable) { + XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {}; + + XCUIHostedPreviewQueuedFrame unnamedFrame = {}; + unnamedFrame.debugSource = "tests.hosted_preview.unnamed"; + unnamedFrame.canvasRect = XCEngine::UI::UIRect(10.0f, 12.0f, 120.0f, 80.0f); + unnamedFrame.logicalSize = XCEngine::UI::UISize(120.0f, 80.0f); + unnamedFrame.drawData.EmplaceDrawList("Unnamed").AddFilledRect( + XCEngine::UI::UIRect(0.0f, 0.0f, 16.0f, 16.0f), + XCEngine::UI::UIColor(0.4f, 0.7f, 0.9f, 1.0f)); + + surfaceRegistry.RecordQueuedFrame(unnamedFrame, 5u); + EXPECT_TRUE(surfaceRegistry.GetDescriptors().empty()); + + surfaceRegistry.UpdateSurface( + "XCUI Demo", + static_cast(static_cast(13)), + 800u, + 600u, + XCEngine::UI::UIRect(0.0f, 0.0f, 400.0f, 300.0f)); + ASSERT_EQ(surfaceRegistry.GetDescriptors().size(), 1u); + + surfaceRegistry.RecordQueuedFrame(unnamedFrame, 9u); + ASSERT_EQ(surfaceRegistry.GetDescriptors().size(), 1u); + EXPECT_EQ(surfaceRegistry.GetDescriptors().front().debugName, "XCUI Demo"); + EXPECT_TRUE(surfaceRegistry.GetDescriptors().front().image.IsValid()); +} + +TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryExposesImageUvForRenderedCanvasRect) { + XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {}; + XCUIHostedPreviewSurfaceImage image = {}; + + surfaceRegistry.UpdateSurface( + "XCUI Demo", + static_cast(static_cast(7)), + 1024u, + 768u, + XCEngine::UI::UIRect(128.0f, 96.0f, 512.0f, 384.0f)); + + ASSERT_TRUE(surfaceRegistry.TryGetSurfaceImage("XCUI Demo", image)); + EXPECT_TRUE(image.IsValid()); + EXPECT_EQ(image.textureId, static_cast(static_cast(7))); + EXPECT_EQ(image.surfaceWidth, 1024u); + EXPECT_EQ(image.surfaceHeight, 768u); + EXPECT_FLOAT_EQ(image.uvMin.x, 0.125f); + EXPECT_FLOAT_EQ(image.uvMin.y, 0.125f); + EXPECT_FLOAT_EQ(image.uvMax.x, 0.625f); + EXPECT_FLOAT_EQ(image.uvMax.y, 0.625f); +} + +TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryTracksQueuedFrameMetadataAlongsideLatestSurfaceImage) { + XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {}; + surfaceRegistry.BeginFrame(); + + XCEngine::UI::UIDrawData drawData = {}; + XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("RegistryPreview"); + drawList.AddFilledRect( + XCEngine::UI::UIRect(8.0f, 6.0f, 30.0f, 18.0f), + XCEngine::UI::UIColor(0.3f, 0.7f, 0.9f, 1.0f)); + drawList.AddText( + XCEngine::UI::UIPoint(16.0f, 12.0f), + "meta", + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 12.0f); + + XCUIHostedPreviewQueuedFrame queuedFrame = {}; + queuedFrame.debugName = "XCUI Demo"; + queuedFrame.debugSource = "tests.hosted_preview.registry"; + queuedFrame.canvasRect = XCEngine::UI::UIRect(24.0f, 32.0f, 300.0f, 160.0f); + queuedFrame.logicalSize = XCEngine::UI::UISize(600.0f, 320.0f); + queuedFrame.drawData = drawData; + + surfaceRegistry.RecordQueuedFrame(queuedFrame, 2u); + + XCUIHostedPreviewSurfaceDescriptor descriptor = {}; + ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor)); + EXPECT_EQ(descriptor.debugName, "XCUI Demo"); + EXPECT_EQ(descriptor.debugSource, "tests.hosted_preview.registry"); + EXPECT_FLOAT_EQ(descriptor.canvasRect.x, 24.0f); + EXPECT_FLOAT_EQ(descriptor.canvasRect.y, 32.0f); + EXPECT_FLOAT_EQ(descriptor.canvasRect.width, 300.0f); + EXPECT_FLOAT_EQ(descriptor.canvasRect.height, 160.0f); + EXPECT_FLOAT_EQ(descriptor.logicalSize.width, 600.0f); + EXPECT_FLOAT_EQ(descriptor.logicalSize.height, 320.0f); + EXPECT_EQ(descriptor.queuedFrameIndex, 2u); + EXPECT_EQ(descriptor.submittedDrawListCount, 1u); + EXPECT_EQ(descriptor.submittedCommandCount, 2u); + EXPECT_TRUE(descriptor.queuedThisFrame); + EXPECT_FALSE(descriptor.image.IsValid()); + + surfaceRegistry.UpdateSurface( + "XCUI Demo", + static_cast(static_cast(9)), + 1024u, + 512u, + XCEngine::UI::UIRect(128.0f, 64.0f, 320.0f, 160.0f)); + + ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor)); + EXPECT_TRUE(descriptor.queuedThisFrame); + EXPECT_TRUE(descriptor.image.IsValid()); + EXPECT_EQ(descriptor.image.textureId, static_cast(static_cast(9))); + EXPECT_FLOAT_EQ(descriptor.image.uvMin.x, 0.125f); + EXPECT_FLOAT_EQ(descriptor.image.uvMin.y, 0.125f); + EXPECT_FLOAT_EQ(descriptor.image.uvMax.x, 0.4375f); + EXPECT_FLOAT_EQ(descriptor.image.uvMax.y, 0.4375f); + + surfaceRegistry.BeginFrame(); + + ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor)); + EXPECT_FALSE(descriptor.queuedThisFrame); + EXPECT_TRUE(descriptor.image.IsValid()); +} + +TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryRejectsInvalidSurfaceUpdatesWithoutClobberingExistingImage) { + XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {}; + XCUIHostedPreviewSurfaceDescriptor descriptor = {}; + + surfaceRegistry.UpdateSurface( + "XCUI Demo", + static_cast(static_cast(17)), + 512u, + 256u, + XCEngine::UI::UIRect(64.0f, 32.0f, 256.0f, 128.0f)); + + ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor)); + ASSERT_TRUE(descriptor.image.IsValid()); + const XCUIHostedPreviewSurfaceImage originalImage = descriptor.image; + + surfaceRegistry.UpdateSurface( + "XCUI Demo", + ImTextureID{}, + 512u, + 256u, + XCEngine::UI::UIRect(0.0f, 0.0f, 128.0f, 64.0f)); + surfaceRegistry.UpdateSurface( + "", + static_cast(static_cast(19)), + 512u, + 256u, + XCEngine::UI::UIRect(0.0f, 0.0f, 128.0f, 64.0f)); + surfaceRegistry.UpdateSurface( + "XCUI Demo", + static_cast(static_cast(21)), + 0u, + 256u, + XCEngine::UI::UIRect(0.0f, 0.0f, 128.0f, 64.0f)); + + ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor)); + EXPECT_EQ(descriptor.image.textureId, originalImage.textureId); + EXPECT_EQ(descriptor.image.surfaceWidth, originalImage.surfaceWidth); + EXPECT_EQ(descriptor.image.surfaceHeight, originalImage.surfaceHeight); + EXPECT_FLOAT_EQ(descriptor.image.uvMin.x, originalImage.uvMin.x); + EXPECT_FLOAT_EQ(descriptor.image.uvMin.y, originalImage.uvMin.y); + EXPECT_FLOAT_EQ(descriptor.image.uvMax.x, originalImage.uvMax.x); + EXPECT_FLOAT_EQ(descriptor.image.uvMax.y, originalImage.uvMax.y); +} + +TEST(XCUIHostedPreviewPresenterTest, BeginFrameClearsQueuedFramesButKeepsLastDrainStatsUntilReplaced) { + XCUIHostedPreviewQueue queue = {}; + + XCEngine::UI::UIDrawData drawData = {}; + XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("Preview"); + drawList.AddFilledRect( + XCEngine::UI::UIRect(4.0f, 4.0f, 18.0f, 12.0f), + XCEngine::UI::UIColor(0.4f, 0.6f, 0.9f, 1.0f)); + + XCUIHostedPreviewFrame frame = {}; + frame.drawData = &drawData; + ASSERT_TRUE(queue.Submit(frame)); + ASSERT_EQ(queue.GetQueuedFrames().size(), 1u); + + XCUIHostedPreviewDrainStats initialDrainStats = {}; + initialDrainStats.queuedFrameCount = 3u; + initialDrainStats.queuedCommandCount = 7u; + initialDrainStats.renderedFrameCount = 2u; + initialDrainStats.skippedFrameCount = 1u; + queue.SetLastDrainStats(initialDrainStats); + + queue.BeginFrame(); + + EXPECT_TRUE(queue.GetQueuedFrames().empty()); + const XCUIHostedPreviewDrainStats& drainStats = queue.GetLastDrainStats(); + EXPECT_EQ(drainStats.queuedFrameCount, 3u); + EXPECT_EQ(drainStats.queuedCommandCount, 7u); + EXPECT_EQ(drainStats.renderedFrameCount, 2u); + EXPECT_EQ(drainStats.skippedFrameCount, 1u); +} + +TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryQueriesClearOutputForMissingOrInvalidNames) { + XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {}; + XCUIHostedPreviewSurfaceDescriptor descriptor = {}; + descriptor.debugName = "stale"; + descriptor.debugSource = "stale"; + descriptor.queuedThisFrame = true; + + XCUIHostedPreviewSurfaceImage image = {}; + image.textureId = static_cast(static_cast(23)); + image.surfaceWidth = 64u; + image.surfaceHeight = 64u; + + EXPECT_FALSE(surfaceRegistry.TryGetSurfaceDescriptor(nullptr, descriptor)); + EXPECT_TRUE(descriptor.debugName.empty()); + EXPECT_TRUE(descriptor.debugSource.empty()); + EXPECT_FALSE(descriptor.queuedThisFrame); + EXPECT_FALSE(descriptor.image.IsValid()); + + EXPECT_FALSE(surfaceRegistry.TryGetSurfaceImage("", image)); + EXPECT_FALSE(image.IsValid()); + EXPECT_EQ(image.surfaceWidth, 0u); + EXPECT_EQ(image.surfaceHeight, 0u); +} + +} // namespace diff --git a/tests/NewEditor/test_xcui_input_bridge.cpp b/tests/NewEditor/test_xcui_input_bridge.cpp new file mode 100644 index 00000000..9401ee1f --- /dev/null +++ b/tests/NewEditor/test_xcui_input_bridge.cpp @@ -0,0 +1,146 @@ +#include + +#include "XCUIBackend/XCUIInputBridge.h" + +#include + +namespace { + +using XCEngine::Editor::XCUIBackend::XCUIInputBridge; +using XCEngine::Editor::XCUIBackend::XCUIInputBridgeCaptureOptions; +using XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta; +using XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot; +using XCEngine::Editor::XCUIBackend::XCUIInputBridgeKeyState; +using XCEngine::Editor::XCUIBackend::XCUIWin32InputSource; +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; + +XCUIInputBridgeKeyState MakeKeyState(KeyCode keyCode, bool down, bool repeat = false) { + XCUIInputBridgeKeyState state = {}; + state.keyCode = static_cast(keyCode); + state.down = down; + state.repeat = repeat; + return state; +} + +} // namespace + +TEST(XCUIInputBridgeTest, TranslateBuildsPointerKeyboardAndCharacterEventsFromSnapshots) { + XCUIInputBridgeFrameSnapshot previous = {}; + + XCUIInputBridgeFrameSnapshot current = {}; + current.pointerInside = true; + current.pointerPosition = UIPoint(32.0f, 48.0f); + current.pointerButtonsDown[0] = true; + current.windowFocused = true; + current.keys.push_back(MakeKeyState(KeyCode::P, true)); + current.characters.push_back(static_cast('p')); + + const XCUIInputBridgeFrameDelta delta = XCUIInputBridge::Translate(previous, current); + + EXPECT_TRUE(delta.focusGained); + EXPECT_TRUE(delta.pointer.entered); + EXPECT_TRUE(delta.pointer.moved); + EXPECT_TRUE(delta.pointer.pressed[0]); + ASSERT_EQ(delta.keyboard.pressedKeys.size(), 1u); + EXPECT_EQ(delta.keyboard.pressedKeys[0], static_cast(KeyCode::P)); + ASSERT_EQ(delta.keyboard.characters.size(), 1u); + EXPECT_EQ(delta.keyboard.characters[0], static_cast('p')); + + EXPECT_TRUE(delta.HasEventType(UIInputEventType::FocusGained)); + EXPECT_TRUE(delta.HasEventType(UIInputEventType::PointerEnter)); + EXPECT_TRUE(delta.HasEventType(UIInputEventType::PointerMove)); + EXPECT_TRUE(delta.HasEventType(UIInputEventType::PointerButtonDown)); + EXPECT_TRUE(delta.HasEventType(UIInputEventType::KeyDown)); + EXPECT_TRUE(delta.HasEventType(UIInputEventType::Character)); +} + +TEST(XCUIInputBridgeTest, PrimeSuppressesSyntheticFirstFrameTransitions) { + XCUIInputBridge bridge = {}; + + XCUIInputBridgeFrameSnapshot baseline = {}; + baseline.pointerInside = true; + baseline.pointerPosition = UIPoint(24.0f, 12.0f); + baseline.pointerButtonsDown[0] = true; + baseline.windowFocused = true; + baseline.keys.push_back(MakeKeyState(KeyCode::P, true)); + + bridge.Prime(baseline); + + const XCUIInputBridgeFrameDelta firstDelta = bridge.Translate(baseline); + EXPECT_FALSE(firstDelta.HasEvents()); + + XCUIInputBridgeFrameSnapshot released = baseline; + released.pointerButtonsDown[0] = false; + released.keys.clear(); + + const XCUIInputBridgeFrameDelta secondDelta = bridge.Translate(released); + EXPECT_TRUE(secondDelta.pointer.released[0]); + ASSERT_EQ(secondDelta.keyboard.releasedKeys.size(), 1u); + EXPECT_EQ(secondDelta.keyboard.releasedKeys[0], static_cast(KeyCode::P)); + EXPECT_TRUE(secondDelta.HasEventType(UIInputEventType::PointerButtonUp)); + EXPECT_TRUE(secondDelta.HasEventType(UIInputEventType::KeyUp)); +} + +TEST(XCUIInputBridgeTest, Win32InputSourceCapturesPointerWheelKeyRepeatAndCharacters) { + XCUIWin32InputSource inputSource = {}; + + inputSource.HandleWindowMessage(nullptr, WM_SETFOCUS, 0, 0); + inputSource.HandleWindowMessage(nullptr, WM_MOUSEMOVE, 0, MAKELPARAM(64, 96)); + inputSource.HandleWindowMessage(nullptr, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(64, 96)); + inputSource.HandleWindowMessage(nullptr, WM_MOUSEWHEEL, MAKEWPARAM(0, WHEEL_DELTA), 0); + inputSource.HandleWindowMessage(nullptr, WM_KEYDOWN, 'P', 0); + inputSource.HandleWindowMessage(nullptr, WM_KEYDOWN, 'P', 1u << 30u); + inputSource.HandleWindowMessage(nullptr, WM_CHAR, 'p', 0); + + XCUIInputBridgeCaptureOptions options = {}; + options.windowFocused = true; + const XCUIInputBridgeFrameSnapshot snapshot = inputSource.CaptureSnapshot(options); + + EXPECT_TRUE(snapshot.windowFocused); + EXPECT_TRUE(snapshot.pointerInside); + EXPECT_EQ(snapshot.pointerPosition.x, 64.0f); + EXPECT_EQ(snapshot.pointerPosition.y, 96.0f); + EXPECT_TRUE(snapshot.pointerButtonsDown[0]); + EXPECT_FLOAT_EQ(snapshot.wheelDelta.y, 1.0f); + EXPECT_TRUE(snapshot.IsKeyDown(static_cast(KeyCode::P))); + ASSERT_EQ(snapshot.characters.size(), 1u); + EXPECT_EQ(snapshot.characters[0], static_cast('p')); + + const XCUIInputBridgeFrameDelta delta = XCUIInputBridge::Translate(XCUIInputBridgeFrameSnapshot(), snapshot); + EXPECT_TRUE(delta.pointer.pressed[0]); + ASSERT_EQ(delta.keyboard.pressedKeys.size(), 1u); + EXPECT_EQ(delta.keyboard.pressedKeys[0], static_cast(KeyCode::P)); + EXPECT_TRUE(delta.HasEventType(UIInputEventType::Character)); + + inputSource.ClearFrameTransients(); + const XCUIInputBridgeFrameSnapshot afterClear = inputSource.CaptureSnapshot(options); + EXPECT_FLOAT_EQ(afterClear.wheelDelta.x, 0.0f); + EXPECT_FLOAT_EQ(afterClear.wheelDelta.y, 0.0f); + EXPECT_TRUE(afterClear.characters.empty()); + const XCUIInputBridgeKeyState* repeatedKey = + afterClear.FindKeyState(static_cast(KeyCode::P)); + ASSERT_NE(repeatedKey, nullptr); + EXPECT_FALSE(repeatedKey->repeat); +} + +TEST(XCUIInputBridgeTest, Win32InputSourceClearsPressedStateOnFocusLoss) { + XCUIWin32InputSource inputSource = {}; + + inputSource.HandleWindowMessage(nullptr, WM_SETFOCUS, 0, 0); + inputSource.HandleWindowMessage(nullptr, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(20, 24)); + inputSource.HandleWindowMessage(nullptr, WM_KEYDOWN, 'P', 0); + + XCUIInputBridgeCaptureOptions options = {}; + options.windowFocused = true; + XCUIInputBridgeFrameSnapshot focused = inputSource.CaptureSnapshot(options); + EXPECT_TRUE(focused.pointerButtonsDown[0]); + EXPECT_TRUE(focused.IsKeyDown(static_cast(KeyCode::P))); + + inputSource.HandleWindowMessage(nullptr, WM_KILLFOCUS, 0, 0); + XCUIInputBridgeFrameSnapshot blurred = inputSource.CaptureSnapshot(options); + EXPECT_FALSE(blurred.windowFocused); + EXPECT_FALSE(blurred.pointerButtonsDown[0]); + EXPECT_FALSE(blurred.IsKeyDown(static_cast(KeyCode::P))); +} diff --git a/tests/NewEditor/test_xcui_layout_lab_runtime.cpp b/tests/NewEditor/test_xcui_layout_lab_runtime.cpp new file mode 100644 index 00000000..cd9bbd7f --- /dev/null +++ b/tests/NewEditor/test_xcui_layout_lab_runtime.cpp @@ -0,0 +1,192 @@ +#include + +#include "XCUIBackend/XCUILayoutLabRuntime.h" + +#include + +#include +#include + +namespace { + +using XCEngine::UI::UIDrawCommand; +using XCEngine::UI::UIDrawCommandType; + +XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState BuildInputState( + float width = 960.0f, + float height = 640.0f) { + XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = {}; + input.canvasRect = XCEngine::UI::UIRect(0.0f, 0.0f, width, height); + input.pointerPosition = XCEngine::UI::UIPoint(width * 0.5f, height * 0.5f); + input.pointerInside = true; + return input; +} + +std::vector CollectTextCommands(const XCEngine::UI::UIDrawData& drawData) { + std::vector textCommands = {}; + for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { + for (const UIDrawCommand& command : drawList.GetCommands()) { + if (command.type == UIDrawCommandType::Text) { + textCommands.push_back(&command); + } + } + } + return textCommands; +} + +const UIDrawCommand* FindTextCommand( + const XCEngine::UI::UIDrawData& drawData, + const std::string& text) { + for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { + for (const UIDrawCommand& command : drawList.GetCommands()) { + if (command.type == UIDrawCommandType::Text && command.text == text) { + return &command; + } + } + } + return nullptr; +} + +std::size_t CountCommandsOfType( + const XCEngine::UI::UIDrawData& drawData, + UIDrawCommandType type) { + std::size_t count = 0; + for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { + for (const UIDrawCommand& command : drawList.GetCommands()) { + if (command.type == type) { + ++count; + } + } + } + return count; +} + +} // namespace + +TEST(NewEditorXCUILayoutLabRuntimeTest, UpdateBuildsLayoutSmokeFrame) { + XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime; + const bool reloadSucceeded = runtime.ReloadDocuments(); + + const auto& frame = runtime.Update(BuildInputState()); + EXPECT_EQ(frame.stats.documentsReady, reloadSucceeded); + EXPECT_EQ(frame.stats.drawListCount, frame.drawData.GetDrawListCount()); + EXPECT_EQ(frame.stats.commandCount, frame.drawData.GetTotalCommandCount()); + + if (frame.stats.documentsReady) { + EXPECT_GT(frame.stats.drawListCount, 0u); + EXPECT_GT(frame.stats.commandCount, 0u); + EXPECT_GE(frame.stats.rowCount, 1u); + EXPECT_GE(frame.stats.columnCount, 1u); + EXPECT_GE(frame.stats.overlayCount, 1u); + EXPECT_GE(frame.stats.scrollViewCount, 2u); + + XCEngine::UI::UIRect heroRect = {}; + EXPECT_TRUE(runtime.TryGetElementRect("heroCard", heroRect)); + EXPECT_GT(heroRect.width, 0.0f); + EXPECT_GT(heroRect.height, 0.0f); + } else { + EXPECT_FALSE(frame.stats.statusMessage.empty()); + } +} + +TEST(NewEditorXCUILayoutLabRuntimeTest, FrameIncludesTextCommandsWithThemeFontSizes) { + XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime; + ASSERT_TRUE(runtime.ReloadDocuments()); + + const auto& frame = runtime.Update(BuildInputState()); + ASSERT_TRUE(frame.stats.documentsReady); + + const std::vector textCommands = CollectTextCommands(frame.drawData); + ASSERT_FALSE(textCommands.empty()); + + for (const UIDrawCommand* command : textCommands) { + ASSERT_NE(command, nullptr); + EXPECT_FALSE(command->text.empty()); + EXPECT_GT(command->fontSize, 0.0f); + } + + const UIDrawCommand* titleCommand = FindTextCommand(frame.drawData, "XCUI Layout Lab"); + ASSERT_NE(titleCommand, nullptr); + EXPECT_FLOAT_EQ(titleCommand->fontSize, 16.0f); + + const UIDrawCommand* subtitleCommand = FindTextCommand( + frame.drawData, + "Editor-style panels with overlay and scroll semantics."); + ASSERT_NE(subtitleCommand, nullptr); + EXPECT_FLOAT_EQ(subtitleCommand->fontSize, 13.0f); +} + +TEST(NewEditorXCUILayoutLabRuntimeTest, HoverProbeResolvesTrackedElementRect) { + XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime; + ASSERT_TRUE(runtime.ReloadDocuments()); + + const auto& baseline = runtime.Update(BuildInputState()); + ASSERT_TRUE(baseline.stats.documentsReady); + + XCEngine::UI::UIRect probeRect = {}; + ASSERT_TRUE(runtime.TryGetElementRect("assetLighting", probeRect)); + ASSERT_GT(probeRect.width, 0.0f); + ASSERT_GT(probeRect.height, 0.0f); + + XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = BuildInputState(); + input.pointerPosition = XCEngine::UI::UIPoint( + probeRect.x + probeRect.width * 0.5f, + probeRect.y + probeRect.height * 0.5f); + const auto& frame = runtime.Update(input); + + ASSERT_TRUE(frame.stats.documentsReady); + EXPECT_FALSE(frame.stats.hoveredElementId.empty()); + + XCEngine::UI::UIRect hoveredRect = {}; + EXPECT_TRUE(runtime.TryGetElementRect(frame.stats.hoveredElementId, hoveredRect)); + EXPECT_GT(hoveredRect.width, 0.0f); + EXPECT_GT(hoveredRect.height, 0.0f); +} + +TEST(NewEditorXCUILayoutLabRuntimeTest, ScrollViewOffsetsContentAndAddsNestedClips) { + XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime; + ASSERT_TRUE(runtime.ReloadDocuments()); + + const auto& frame = runtime.Update(BuildInputState()); + ASSERT_TRUE(frame.stats.documentsReady); + + XCEngine::UI::UIRect assetListRect = {}; + XCEngine::UI::UIRect headerRect = {}; + XCEngine::UI::UIRect visibleItemRect = {}; + ASSERT_TRUE(runtime.TryGetElementRect("assetList", assetListRect)); + ASSERT_TRUE(runtime.TryGetElementRect("assetListHeader", headerRect)); + ASSERT_TRUE(runtime.TryGetElementRect("assetLighting", visibleItemRect)); + + EXPECT_LT(headerRect.y, assetListRect.y); + EXPECT_GT(visibleItemRect.y, assetListRect.y); + EXPECT_LT(visibleItemRect.y, assetListRect.y + assetListRect.height); + + EXPECT_EQ( + CountCommandsOfType(frame.drawData, UIDrawCommandType::PushClipRect), + frame.stats.clipPushCommandCount); + EXPECT_EQ( + CountCommandsOfType(frame.drawData, UIDrawCommandType::PopClipRect), + frame.stats.clipPopCommandCount); + EXPECT_GE(frame.stats.clipPushCommandCount, 3u); + EXPECT_GE(frame.stats.clipPopCommandCount, 3u); +} + +TEST(NewEditorXCUILayoutLabRuntimeTest, HoverIgnoresClippedScrollViewContent) { + XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime; + ASSERT_TRUE(runtime.ReloadDocuments()); + + const auto& baseline = runtime.Update(BuildInputState()); + ASSERT_TRUE(baseline.stats.documentsReady); + + XCEngine::UI::UIRect assetListRect = {}; + ASSERT_TRUE(runtime.TryGetElementRect("assetList", assetListRect)); + + XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = BuildInputState(); + input.pointerPosition = XCEngine::UI::UIPoint( + assetListRect.x + assetListRect.width * 0.5f, + assetListRect.y + assetListRect.height + 6.0f); + const auto& frame = runtime.Update(input); + + ASSERT_TRUE(frame.stats.documentsReady); + EXPECT_TRUE(frame.stats.hoveredElementId.empty()); +} diff --git a/tests/NewEditor/test_xcui_rhi_command_compiler.cpp b/tests/NewEditor/test_xcui_rhi_command_compiler.cpp new file mode 100644 index 00000000..2fcdf29d --- /dev/null +++ b/tests/NewEditor/test_xcui_rhi_command_compiler.cpp @@ -0,0 +1,242 @@ +#include + +#include "XCUIBackend/XCUIRHICommandCompiler.h" + +namespace { + +using XCEngine::Editor::XCUIBackend::XCUIRHICommandCompiler; +using XCEngine::UI::UIColor; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; +using XCEngine::UI::UITextureHandle; +using XCEngine::UI::UITextureHandleKind; + +class StubTextGlyphProvider final : public XCUIRHICommandCompiler::TextGlyphProvider { +public: + bool BeginText( + float requestedFontSize, + XCUIRHICommandCompiler::TextRunContext& outContext) const override { + outContext.requestedFontSize = requestedFontSize; + outContext.resolvedFontSize = requestedFontSize > 0.0f ? requestedFontSize : 14.0f; + outContext.lineHeight = 12.0f; + outContext.texture = UITextureHandle{ 99u, 256u, 256u, UITextureHandleKind::ShaderResourceView }; + return true; + } + + bool ResolveGlyph( + const XCUIRHICommandCompiler::TextRunContext&, + std::uint32_t codepoint, + XCUIRHICommandCompiler::TextGlyph& outGlyph) const override { + switch (codepoint) { + case 'A': + outGlyph.x0 = 0.0f; + outGlyph.y0 = 0.0f; + outGlyph.x1 = 8.0f; + outGlyph.y1 = 10.0f; + outGlyph.u0 = 0.0f; + outGlyph.v0 = 0.0f; + outGlyph.u1 = 0.25f; + outGlyph.v1 = 0.5f; + outGlyph.advanceX = 8.0f; + outGlyph.visible = true; + return true; + case 'B': + outGlyph.x0 = 0.0f; + outGlyph.y0 = 0.0f; + outGlyph.x1 = 7.0f; + outGlyph.y1 = 10.0f; + outGlyph.u0 = 0.25f; + outGlyph.v0 = 0.0f; + outGlyph.u1 = 0.5f; + outGlyph.v1 = 0.5f; + outGlyph.advanceX = 7.0f; + outGlyph.visible = true; + return true; + default: + return false; + } + } +}; + +TEST(XCUIRHICommandCompilerTest, CompileMergesAdjacentColorAndTexturedCommandsPerDrawList) { + XCUIRHICommandCompiler compiler = {}; + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("Batches"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, 10.0f, 10.0f), UIColor(1.0f, 0.0f, 0.0f, 1.0f)); + drawList.AddFilledRect(UIRect(10.0f, 0.0f, 10.0f, 10.0f), UIColor(0.0f, 1.0f, 0.0f, 1.0f)); + + const UITextureHandle texture{ 7u, 64u, 64u, UITextureHandleKind::ShaderResourceView }; + drawList.AddImage(UIRect(0.0f, 20.0f, 10.0f, 10.0f), texture, UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + drawList.AddImage(UIRect(10.0f, 20.0f, 10.0f, 10.0f), texture, UIColor(0.5f, 0.5f, 0.5f, 1.0f)); + + XCUIRHICommandCompiler::CompileConfig config = {}; + config.surfaceClipRect = UIRect(0.0f, 0.0f, 100.0f, 100.0f); + + XCUIRHICommandCompiler::CompiledDrawData compiled = {}; + compiler.Compile(drawData, config, compiled); + + ASSERT_EQ(compiled.batches.size(), 2u); + EXPECT_EQ(compiled.batches[0].kind, XCUIRHICommandCompiler::BatchKind::Color); + EXPECT_EQ(compiled.batches[0].commandCount, 2u); + EXPECT_EQ(compiled.batches[0].vertexCount, 12u); + EXPECT_EQ(compiled.batches[1].kind, XCUIRHICommandCompiler::BatchKind::Textured); + EXPECT_EQ(compiled.batches[1].commandCount, 2u); + EXPECT_EQ(compiled.batches[1].vertexCount, 12u); + EXPECT_EQ(compiled.batches[1].texture.nativeHandle, texture.nativeHandle); + EXPECT_EQ(compiled.stats.compiledCommandCount, 4u); + EXPECT_EQ(compiled.stats.batchCount, 2u); + EXPECT_EQ(compiled.stats.colorVertexCount, 12u); + EXPECT_EQ(compiled.stats.texturedVertexCount, 12u); +} + +TEST(XCUIRHICommandCompilerTest, CompileTracksClipStackTransitionsAndUnderflow) { + XCUIRHICommandCompiler compiler = {}; + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("ClipStack"); + drawList.PushClipRect(UIRect(0.0f, 0.0f, 50.0f, 50.0f)); + drawList.PushClipRect(UIRect(10.0f, 10.0f, 40.0f, 40.0f)); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, 30.0f, 30.0f), UIColor(1.0f, 0.0f, 0.0f, 1.0f)); + drawList.PopClipRect(); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, 20.0f, 20.0f), UIColor(0.0f, 1.0f, 0.0f, 1.0f)); + drawList.PopClipRect(); + drawList.PopClipRect(); + + XCUIRHICommandCompiler::CompileConfig config = {}; + config.surfaceClipRect = UIRect(0.0f, 0.0f, 100.0f, 100.0f); + + XCUIRHICommandCompiler::CompiledDrawData compiled = {}; + compiler.Compile(drawData, config, compiled); + + ASSERT_EQ(compiled.batches.size(), 2u); + EXPECT_EQ(compiled.stats.compiledCommandCount, 7u); + EXPECT_EQ(compiled.stats.clipPushCommandCount, 2u); + EXPECT_EQ(compiled.stats.clipPopCommandCount, 3u); + EXPECT_EQ(compiled.stats.clipStackUnderflowCount, 1u); + EXPECT_EQ(compiled.stats.maxClipDepth, 2u); + EXPECT_EQ(compiled.stats.danglingClipDepth, 0u); + EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.x, 10.0f); + EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.y, 10.0f); + EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.width, 40.0f); + EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.height, 40.0f); + ASSERT_GE(compiled.colorVertices.size(), 12u); + EXPECT_FLOAT_EQ(compiled.colorVertices[0].position[0], 10.0f); + EXPECT_FLOAT_EQ(compiled.colorVertices[0].position[1], 10.0f); + EXPECT_FLOAT_EQ(compiled.colorVertices[6].position[0], 0.0f); + EXPECT_FLOAT_EQ(compiled.colorVertices[6].position[1], 0.0f); +} + +TEST(XCUIRHICommandCompilerTest, CompileUsesTextGlyphProviderOutputForTextBatches) { + XCUIRHICommandCompiler compiler = {}; + StubTextGlyphProvider glyphProvider = {}; + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("Text"); + drawList.AddText(UIPoint(4.0f, 6.0f), "A\nB", UIColor(1.0f, 1.0f, 1.0f, 1.0f), 16.0f); + + XCUIRHICommandCompiler::CompileConfig config = {}; + config.surfaceClipRect = UIRect(0.0f, 0.0f, 100.0f, 100.0f); + config.textGlyphProvider = &glyphProvider; + + XCUIRHICommandCompiler::CompiledDrawData compiled = {}; + compiler.Compile(drawData, config, compiled); + + ASSERT_EQ(compiled.batches.size(), 1u); + EXPECT_EQ(compiled.batches[0].kind, XCUIRHICommandCompiler::BatchKind::Textured); + EXPECT_EQ(compiled.batches[0].vertexCount, 12u); + EXPECT_EQ(compiled.batches[0].texture.nativeHandle, 99u); + EXPECT_EQ(compiled.stats.textCommandCount, 1u); + EXPECT_EQ(compiled.stats.compiledCommandCount, 1u); + ASSERT_EQ(compiled.texturedVertices.size(), 12u); + EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[0], 4.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[1], 6.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[6].position[0], 4.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[6].position[1], 18.0f); +} + +TEST(XCUIRHICommandCompilerTest, CompileReportsUnsupportedTextWhenNoGlyphProviderIsAvailable) { + XCUIRHICommandCompiler compiler = {}; + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("UnsupportedText"); + drawList.AddText(UIPoint(0.0f, 0.0f), "xcui", UIColor(1.0f, 1.0f, 1.0f, 1.0f), 12.0f); + + XCUIRHICommandCompiler::CompileConfig config = {}; + config.surfaceClipRect = UIRect(0.0f, 0.0f, 32.0f, 32.0f); + + XCUIRHICommandCompiler::CompiledDrawData compiled = {}; + compiler.Compile(drawData, config, compiled); + + EXPECT_TRUE(compiled.Empty()); + EXPECT_EQ(compiled.stats.textCommandCount, 1u); + EXPECT_EQ(compiled.stats.compiledCommandCount, 0u); + EXPECT_EQ(compiled.stats.skippedCommandCount, 1u); + EXPECT_EQ(compiled.stats.unsupportedCommandCount, 1u); +} + +TEST(XCUIRHICommandCompilerTest, CompileClipsImageCommandsAndAdjustsUvCoordinates) { + XCUIRHICommandCompiler compiler = {}; + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("ClippedImage"); + drawList.PushClipRect(UIRect(10.0f, 8.0f, 12.0f, 10.0f)); + drawList.AddImage( + UIRect(4.0f, 4.0f, 20.0f, 20.0f), + UITextureHandle{ 77u, 32u, 32u, UITextureHandleKind::ShaderResourceView }, + UIColor(0.7f, 0.8f, 0.9f, 1.0f)); + + XCUIRHICommandCompiler::CompileConfig config = {}; + config.surfaceClipRect = UIRect(0.0f, 0.0f, 64.0f, 64.0f); + + XCUIRHICommandCompiler::CompiledDrawData compiled = {}; + compiler.Compile(drawData, config, compiled); + + ASSERT_EQ(compiled.batches.size(), 1u); + ASSERT_EQ(compiled.texturedVertices.size(), 6u); + EXPECT_EQ(compiled.batches[0].kind, XCUIRHICommandCompiler::BatchKind::Textured); + EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.x, 10.0f); + EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.y, 8.0f); + EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.width, 12.0f); + EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.height, 10.0f); + + EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[0], 10.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[1], 8.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[0].uv[0], 0.3f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[0].uv[1], 0.2f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[1].position[0], 22.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[1].uv[0], 0.9f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[2].position[1], 18.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[2].uv[1], 0.7f); + EXPECT_EQ(compiled.stats.imageCommandCount, 1u); + EXPECT_EQ(compiled.stats.compiledCommandCount, 2u); +} + +TEST(XCUIRHICommandCompilerTest, CompilePreservesMirroredImageUvForNegativeRectExtents) { + XCUIRHICommandCompiler compiler = {}; + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("MirroredImage"); + drawList.AddImage( + UIRect(24.0f, 18.0f, -16.0f, -12.0f), + UITextureHandle{ 91u, 64u, 64u, UITextureHandleKind::ShaderResourceView }, + UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + + XCUIRHICommandCompiler::CompileConfig config = {}; + config.surfaceClipRect = UIRect(0.0f, 0.0f, 64.0f, 64.0f); + + XCUIRHICommandCompiler::CompiledDrawData compiled = {}; + compiler.Compile(drawData, config, compiled); + + ASSERT_EQ(compiled.batches.size(), 1u); + ASSERT_EQ(compiled.texturedVertices.size(), 6u); + EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[0], 8.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[1], 6.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[0].uv[0], 1.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[0].uv[1], 1.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[1].position[0], 24.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[1].uv[0], 0.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[2].position[1], 18.0f); + EXPECT_FLOAT_EQ(compiled.texturedVertices[2].uv[1], 0.0f); + EXPECT_EQ(compiled.stats.imageCommandCount, 1u); + EXPECT_EQ(compiled.stats.compiledCommandCount, 1u); +} + +} // namespace diff --git a/tests/NewEditor/test_xcui_rhi_command_support.cpp b/tests/NewEditor/test_xcui_rhi_command_support.cpp new file mode 100644 index 00000000..21e8b2c2 --- /dev/null +++ b/tests/NewEditor/test_xcui_rhi_command_support.cpp @@ -0,0 +1,202 @@ +#include + +#include "XCUIBackend/XCUIRHICommandSupport.h" + +#include + +namespace { + +using XCEngine::Editor::XCUIBackend::AccumulateXCUIRHICommandSupport; +using XCEngine::Editor::XCUIBackend::AnalyzeXCUIRHICommandSupport; +using XCEngine::Editor::XCUIBackend::BuildXCUIRHICommandSupportDiagnostic; +using XCEngine::Editor::XCUIBackend::ClassifyXCUIRHICommandSupport; +using XCEngine::Editor::XCUIBackend::SummarizeXCUIRHICommandSupport; +using XCEngine::Editor::XCUIBackend::XCUIRHICommandCategory; +using XCEngine::Editor::XCUIBackend::XCUIRHICommandDiagnosticOptions; +using XCEngine::Editor::XCUIBackend::XCUIRHICommandSupportReason; +using XCEngine::Editor::XCUIBackend::XCUIRHICommandSupportStats; +using XCEngine::UI::UIColor; +using XCEngine::UI::UIDrawCommand; +using XCEngine::UI::UIDrawCommandType; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; +using XCEngine::UI::UITextureHandle; +using XCEngine::UI::UITextureHandleKind; + +UITextureHandle MakeShaderResourceTexture() { + UITextureHandle texture = {}; + texture.nativeHandle = 42u; + texture.width = 64u; + texture.height = 64u; + texture.kind = UITextureHandleKind::ShaderResourceView; + return texture; +} + +UIDrawCommand MakeUnknownCommand() { + UIDrawCommand command = {}; + command.type = static_cast(255); + return command; +} + +} // namespace + +TEST(XCUIRHICommandSupportTest, ClassifySupportedCommandTypes) { + UIDrawList drawList("Supported"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, 12.0f, 10.0f), UIColor(1.0f, 0.0f, 0.0f, 1.0f)); + drawList.AddRectOutline( + UIRect(1.0f, 2.0f, 8.0f, 5.0f), + UIColor(0.0f, 1.0f, 0.0f, 1.0f), + 2.0f, + 1.0f); + drawList.AddText(UIPoint(4.0f, 5.0f), "label", UIColor(1.0f, 1.0f, 1.0f, 1.0f), 14.0f); + drawList.AddImage( + UIRect(2.0f, 3.0f, 18.0f, 14.0f), + MakeShaderResourceTexture(), + UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + drawList.PushClipRect(UIRect(0.0f, 0.0f, 24.0f, 20.0f)); + drawList.PopClipRect(); + + const auto& commands = drawList.GetCommands(); + + EXPECT_EQ( + ClassifyXCUIRHICommandSupport(commands[0]).category, + XCUIRHICommandCategory::FilledRect); + EXPECT_EQ( + ClassifyXCUIRHICommandSupport(commands[1]).category, + XCUIRHICommandCategory::RectOutline); + EXPECT_EQ( + ClassifyXCUIRHICommandSupport(commands[2]).category, + XCUIRHICommandCategory::Text); + EXPECT_EQ( + ClassifyXCUIRHICommandSupport(commands[3]).category, + XCUIRHICommandCategory::Image); + EXPECT_EQ( + ClassifyXCUIRHICommandSupport(commands[4]).category, + XCUIRHICommandCategory::PushClipRect); + EXPECT_EQ( + ClassifyXCUIRHICommandSupport(commands[5]).category, + XCUIRHICommandCategory::PopClipRect); + + for (const UIDrawCommand& command : commands) { + EXPECT_TRUE(ClassifyXCUIRHICommandSupport(command).IsSupported()); + } +} + +TEST(XCUIRHICommandSupportTest, InvalidImageCommandIsClassifiedAsUnsupportedTexture) { + UIDrawCommand command = {}; + command.type = UIDrawCommandType::Image; + command.rect = UIRect(0.0f, 0.0f, 12.0f, 12.0f); + command.texture.nativeHandle = 7u; + command.texture.width = 32u; + command.texture.height = 32u; + command.texture.kind = UITextureHandleKind::ImGuiDescriptor; + + const auto classification = ClassifyXCUIRHICommandSupport(command); + + EXPECT_EQ(classification.category, XCUIRHICommandCategory::Image); + EXPECT_EQ( + classification.supportReason, + XCUIRHICommandSupportReason::UnsupportedImageTexture); + EXPECT_FALSE(classification.IsSupported()); +} + +TEST(XCUIRHICommandSupportTest, UnknownCommandTypeIsClassifiedSeparately) { + const auto classification = ClassifyXCUIRHICommandSupport(MakeUnknownCommand()); + + EXPECT_EQ(classification.category, XCUIRHICommandCategory::Unknown); + EXPECT_EQ( + classification.supportReason, + XCUIRHICommandSupportReason::UnsupportedUnknownCommand); + EXPECT_FALSE(classification.IsSupported()); +} + +TEST(XCUIRHICommandSupportTest, AnalyzeDrawDataAggregatesSupportedAndUnsupportedCounts) { + UIDrawData drawData = {}; + + UIDrawList& firstDrawList = drawData.EmplaceDrawList("First"); + firstDrawList.AddFilledRect( + UIRect(0.0f, 0.0f, 8.0f, 8.0f), + UIColor(1.0f, 0.0f, 0.0f, 1.0f)); + firstDrawList.AddImage( + UIRect(2.0f, 2.0f, 12.0f, 10.0f), + MakeShaderResourceTexture(), + UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + + UIDrawList& secondDrawList = drawData.EmplaceDrawList("Second"); + secondDrawList.PushClipRect(UIRect(0.0f, 0.0f, 100.0f, 60.0f)); + secondDrawList.AddText( + UIPoint(5.0f, 6.0f), + "status", + UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 12.0f); + + UITextureHandle invalidTexture = {}; + invalidTexture.nativeHandle = 9u; + invalidTexture.width = 32u; + invalidTexture.height = 32u; + invalidTexture.kind = UITextureHandleKind::ImGuiDescriptor; + secondDrawList.AddImage( + UIRect(1.0f, 1.0f, 6.0f, 6.0f), + invalidTexture, + UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + + const auto stats = AnalyzeXCUIRHICommandSupport(drawData); + + EXPECT_EQ(stats.drawListCount, 2u); + EXPECT_EQ(stats.commandCount, 5u); + EXPECT_EQ(stats.filledRectCommandCount, 1u); + EXPECT_EQ(stats.textCommandCount, 1u); + EXPECT_EQ(stats.imageCommandCount, 2u); + EXPECT_EQ(stats.clipPushCommandCount, 1u); + EXPECT_EQ(stats.clipPopCommandCount, 0u); + EXPECT_EQ(stats.supportedCommandCount, 4u); + EXPECT_EQ(stats.unsupportedCommandCount, 1u); + EXPECT_EQ(stats.unsupportedImageCommandCount, 1u); + EXPECT_EQ(stats.unsupportedUnknownCommandCount, 0u); + EXPECT_FALSE(stats.SupportsAllCommands()); +} + +TEST(XCUIRHICommandSupportTest, DiagnosticIncludesUnsupportedReasonsAndCanBeCustomized) { + XCUIRHICommandSupportStats stats = {}; + AccumulateXCUIRHICommandSupport(MakeUnknownCommand(), stats); + + UIDrawCommand invalidImage = {}; + invalidImage.type = UIDrawCommandType::Image; + invalidImage.texture.nativeHandle = 11u; + invalidImage.texture.width = 16u; + invalidImage.texture.height = 16u; + invalidImage.texture.kind = UITextureHandleKind::ImGuiDescriptor; + AccumulateXCUIRHICommandSupport(invalidImage, stats); + + const std::string defaultDiagnostic = BuildXCUIRHICommandSupportDiagnostic(stats); + EXPECT_NE(defaultDiagnostic.find("2 command(s) will be skipped by native overlay: "), std::string::npos); + EXPECT_NE(defaultDiagnostic.find("1 image command(s) missing valid ShaderResourceView textures"), std::string::npos); + EXPECT_NE(defaultDiagnostic.find("1 unknown command type(s)"), std::string::npos); + + XCUIRHICommandDiagnosticOptions options = {}; + options.noCommandsMessage = "No overlay commands."; + options.allSupportedMessage = "Everything supported."; + options.unsupportedPrefix = "command(s) rejected:"; + + const std::string customDiagnostic = BuildXCUIRHICommandSupportDiagnostic(stats, options); + EXPECT_NE(customDiagnostic.find("2 command(s) rejected:"), std::string::npos); +} + +TEST(XCUIRHICommandSupportTest, SummaryReturnsNoCommandAndAllSupportedMessages) { + const auto emptySummary = SummarizeXCUIRHICommandSupport(UIDrawData()); + EXPECT_EQ(emptySummary.stats.commandCount, 0u); + EXPECT_EQ(emptySummary.diagnostic, "Overlay runtime produced no commands."); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("Supported"); + drawList.AddFilledRect( + UIRect(0.0f, 0.0f, 4.0f, 4.0f), + UIColor(0.2f, 0.4f, 0.8f, 1.0f)); + + const auto supportedSummary = SummarizeXCUIRHICommandSupport(drawData); + EXPECT_EQ(supportedSummary.stats.supportedCommandCount, 1u); + EXPECT_TRUE(supportedSummary.stats.SupportsAllCommands()); + EXPECT_EQ(supportedSummary.diagnostic, "All commands preflight for native overlay."); +} diff --git a/tests/NewEditor/test_xcui_rhi_render_backend.cpp b/tests/NewEditor/test_xcui_rhi_render_backend.cpp new file mode 100644 index 00000000..da5d53a2 --- /dev/null +++ b/tests/NewEditor/test_xcui_rhi_render_backend.cpp @@ -0,0 +1,710 @@ +#include + +#include "XCUIBackend/XCUIRHIRenderBackend.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +using XCEngine::Editor::XCUIBackend::IXCUITextAtlasProvider; +using XCEngine::Editor::XCUIBackend::XCUIRHIRenderBackend; +using XCEngine::Rendering::RenderContext; +using XCEngine::Rendering::RenderSurface; +using XCEngine::RHI::BlendDesc; +using XCEngine::RHI::BufferDesc; +using XCEngine::RHI::BufferType; +using XCEngine::RHI::CommandQueueType; +using XCEngine::RHI::ComparisonFunc; +using XCEngine::RHI::DepthStencilStateDesc; +using XCEngine::RHI::DescriptorHeapType; +using XCEngine::RHI::DescriptorPoolDesc; +using XCEngine::RHI::DescriptorSetLayoutBinding; +using XCEngine::RHI::DescriptorSetLayoutDesc; +using XCEngine::RHI::Format; +using XCEngine::RHI::GraphicsPipelineDesc; +using XCEngine::RHI::InputLayoutDesc; +using XCEngine::RHI::PipelineStateHash; +using XCEngine::RHI::PipelineType; +using XCEngine::RHI::PrimitiveTopology; +using XCEngine::RHI::RasterizerDesc; +using XCEngine::RHI::Rect; +using XCEngine::RHI::ResourceStates; +using XCEngine::RHI::ResourceViewDesc; +using XCEngine::RHI::ResourceViewDimension; +using XCEngine::RHI::ResourceViewType; +using XCEngine::RHI::RHICommandList; +using XCEngine::RHI::RHICommandQueue; +using XCEngine::RHI::RHIDescriptorPool; +using XCEngine::RHI::RHIDescriptorSet; +using XCEngine::RHI::RHIDevice; +using XCEngine::RHI::RHIPipelineLayout; +using XCEngine::RHI::RHIPipelineState; +using XCEngine::RHI::RHIResourceView; +using XCEngine::RHI::RHISampler; +using XCEngine::RHI::RHIBuffer; +using XCEngine::RHI::RHIShader; +using XCEngine::RHI::RHIType; +using XCEngine::RHI::SamplerDesc; +using XCEngine::RHI::Viewport; +using XCEngine::UI::UIColor; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIRect; +using XCEngine::UI::UITextureHandle; +using XCEngine::UI::UITextureHandleKind; + +class FakeResourceViewBase { +public: + explicit FakeResourceViewBase( + ResourceViewType viewType, + ResourceViewDimension dimension = ResourceViewDimension::Texture2D, + Format format = Format::R8G8B8A8_UNorm) + : m_viewType(viewType) + , m_dimension(dimension) + , m_format(format) { + } + + void ShutdownBase() { + m_valid = false; + } + + void* GetNativeHandleBase() { + return this; + } + + bool IsValidBase() const { + return m_valid; + } + + ResourceViewType GetViewTypeBase() const { + return m_viewType; + } + + ResourceViewDimension GetDimensionBase() const { + return m_dimension; + } + + Format GetFormatBase() const { + return m_format; + } + + void SetValid(bool valid) { + m_valid = valid; + } + +private: + ResourceViewType m_viewType = ResourceViewType::ShaderResource; + ResourceViewDimension m_dimension = ResourceViewDimension::Texture2D; + Format m_format = Format::R8G8B8A8_UNorm; + bool m_valid = true; +}; + +class FakeShaderResourceView final : public XCEngine::RHI::RHIShaderResourceView { +public: + explicit FakeShaderResourceView(bool valid = true) + : m_base(ResourceViewType::ShaderResource) { + m_base.SetValid(valid); + } + + void Shutdown() override { m_base.ShutdownBase(); } + void* GetNativeHandle() override { return m_base.GetNativeHandleBase(); } + bool IsValid() const override { return m_base.IsValidBase(); } + ResourceViewType GetViewType() const override { return m_base.GetViewTypeBase(); } + ResourceViewDimension GetDimension() const override { return m_base.GetDimensionBase(); } + Format GetFormat() const override { return m_base.GetFormatBase(); } + +private: + FakeResourceViewBase m_base; +}; + +class FakeRenderTargetView final : public XCEngine::RHI::RHIRenderTargetView { +public: + FakeRenderTargetView() + : m_base(ResourceViewType::RenderTarget) { + } + + void Shutdown() override { m_base.ShutdownBase(); } + void* GetNativeHandle() override { return m_base.GetNativeHandleBase(); } + bool IsValid() const override { return m_base.IsValidBase(); } + ResourceViewType GetViewType() const override { return m_base.GetViewTypeBase(); } + ResourceViewDimension GetDimension() const override { return m_base.GetDimensionBase(); } + Format GetFormat() const override { return m_base.GetFormatBase(); } + +private: + FakeResourceViewBase m_base; +}; + +class FakeVertexBufferView final : public XCEngine::RHI::RHIVertexBufferView { +public: + explicit FakeVertexBufferView(std::uint32_t size, std::uint32_t stride) + : m_size(size) + , m_stride(stride) + , m_base(ResourceViewType::VertexBuffer, ResourceViewDimension::Buffer, Format::Unknown) { + } + + void Shutdown() override { m_base.ShutdownBase(); } + void* GetNativeHandle() override { return m_base.GetNativeHandleBase(); } + bool IsValid() const override { return m_base.IsValidBase(); } + ResourceViewType GetViewType() const override { return m_base.GetViewTypeBase(); } + ResourceViewDimension GetDimension() const override { return m_base.GetDimensionBase(); } + Format GetFormat() const override { return m_base.GetFormatBase(); } + std::uint64_t GetBufferAddress() const override { return 0u; } + std::uint32_t GetSize() const override { return m_size; } + std::uint32_t GetStride() const override { return m_stride; } + +private: + std::uint32_t m_size = 0; + std::uint32_t m_stride = 0; + FakeResourceViewBase m_base; +}; + +class FakeBuffer final : public RHIBuffer { +public: + explicit FakeBuffer(const BufferDesc& desc) + : m_size(desc.size) + , m_stride(desc.stride) + , m_type(static_cast(desc.bufferType)) + , m_storage(static_cast(desc.size), 0u) { + } + + void* Map() override { return m_storage.data(); } + void Unmap() override {} + + void SetData(const void* data, size_t size, size_t offset = 0) override { + if (data == nullptr || offset + size > m_storage.size()) { + return; + } + std::memcpy(m_storage.data() + offset, data, size); + } + + std::uint64_t GetSize() const override { return m_size; } + BufferType GetBufferType() const override { return m_type; } + void SetBufferType(BufferType type) override { m_type = type; } + std::uint32_t GetStride() const override { return m_stride; } + void SetStride(std::uint32_t stride) override { m_stride = stride; } + void* GetNativeHandle() override { return this; } + ResourceStates GetState() const override { return m_state; } + void SetState(ResourceStates state) override { m_state = state; } + const std::string& GetName() const override { return m_name; } + void SetName(const std::string& name) override { m_name = name; } + void Shutdown() override { m_storage.clear(); } + +private: + std::uint64_t m_size = 0; + std::uint32_t m_stride = 0; + BufferType m_type = BufferType::Vertex; + ResourceStates m_state = ResourceStates::Common; + std::string m_name = {}; + std::vector m_storage = {}; +}; + +class FakeDescriptorSet final : public RHIDescriptorSet { +public: + explicit FakeDescriptorSet(const DescriptorSetLayoutDesc& layout) { + if (layout.bindings != nullptr && layout.bindingCount > 0u) { + m_bindings.assign(layout.bindings, layout.bindings + layout.bindingCount); + m_views.resize(layout.bindingCount, nullptr); + m_samplers.resize(layout.bindingCount, nullptr); + } + } + + void Shutdown() override { + m_views.clear(); + m_samplers.clear(); + m_constantBuffer.clear(); + m_constantDirty = false; + } + + void Bind() override {} + void Unbind() override {} + + void Update(std::uint32_t offset, RHIResourceView* view) override { + if (offset >= m_views.size()) { + m_views.resize(offset + 1u, nullptr); + } + m_views[offset] = view; + } + + void UpdateSampler(std::uint32_t offset, RHISampler* sampler) override { + if (offset >= m_samplers.size()) { + m_samplers.resize(offset + 1u, nullptr); + } + m_samplers[offset] = sampler; + } + + void WriteConstant(std::uint32_t, const void* data, size_t size, size_t offset = 0) override { + if (data == nullptr) { + return; + } + if (m_constantBuffer.size() < offset + size) { + m_constantBuffer.resize(offset + size, 0u); + } + std::memcpy(m_constantBuffer.data() + offset, data, size); + m_constantDirty = true; + } + + std::uint32_t GetBindingCount() const override { + return static_cast(m_bindings.size()); + } + + const DescriptorSetLayoutBinding* GetBindings() const override { + return m_bindings.empty() ? nullptr : m_bindings.data(); + } + + void* GetConstantBufferData() override { + return m_constantBuffer.empty() ? nullptr : m_constantBuffer.data(); + } + + size_t GetConstantBufferSize() const override { + return m_constantBuffer.size(); + } + + bool IsConstantDirty() const override { + return m_constantDirty; + } + + void MarkConstantClean() override { + m_constantDirty = false; + } + +private: + std::vector m_bindings = {}; + std::vector m_views = {}; + std::vector m_samplers = {}; + std::vector m_constantBuffer = {}; + bool m_constantDirty = false; +}; + +class FakeDescriptorPool final : public RHIDescriptorPool { +public: + explicit FakeDescriptorPool(const DescriptorPoolDesc& desc) + : m_desc(desc) { + } + + bool Initialize(const DescriptorPoolDesc& desc) override { + m_desc = desc; + return true; + } + + void Shutdown() override {} + void* GetNativeHandle() override { return this; } + std::uint32_t GetDescriptorCount() const override { return m_desc.descriptorCount; } + DescriptorHeapType GetType() const override { return m_desc.type; } + + RHIDescriptorSet* AllocateSet(const DescriptorSetLayoutDesc& layout) override { + ++m_allocationCount; + return new FakeDescriptorSet(layout); + } + + void FreeSet(RHIDescriptorSet* set) override { + delete set; + } + + std::uint32_t GetAllocationCount() const { + return m_allocationCount; + } + +private: + DescriptorPoolDesc m_desc = {}; + std::uint32_t m_allocationCount = 0; +}; + +class FakePipelineLayout final : public RHIPipelineLayout { +public: + bool Initialize(const XCEngine::RHI::RHIPipelineLayoutDesc& desc) override { + m_desc = desc; + return true; + } + + void Shutdown() override {} + void* GetNativeHandle() override { return this; } + +private: + XCEngine::RHI::RHIPipelineLayoutDesc m_desc = {}; +}; + +class FakePipelineState final : public RHIPipelineState { +public: + explicit FakePipelineState(const GraphicsPipelineDesc& desc) + : m_inputLayout(desc.inputLayout) + , m_rasterizer(desc.rasterizerState) + , m_blend(desc.blendState) + , m_depthStencil(desc.depthStencilState) { + } + + void SetInputLayout(const InputLayoutDesc& layout) override { m_inputLayout = layout; } + void SetRasterizerState(const RasterizerDesc& state) override { m_rasterizer = state; } + void SetBlendState(const BlendDesc& state) override { m_blend = state; } + void SetDepthStencilState(const DepthStencilStateDesc& state) override { m_depthStencil = state; } + void SetTopology(std::uint32_t topologyType) override { m_topologyType = topologyType; } + void SetRenderTargetFormats(std::uint32_t, const std::uint32_t*, std::uint32_t) override {} + void SetSampleCount(std::uint32_t count) override { m_sampleCount = count; } + void SetComputeShader(RHIShader* shader) override { m_computeShader = shader; } + const RasterizerDesc& GetRasterizerState() const override { return m_rasterizer; } + const BlendDesc& GetBlendState() const override { return m_blend; } + const DepthStencilStateDesc& GetDepthStencilState() const override { return m_depthStencil; } + const InputLayoutDesc& GetInputLayout() const override { return m_inputLayout; } + PipelineStateHash GetHash() const override { return {}; } + RHIShader* GetComputeShader() const override { return m_computeShader; } + bool HasComputeShader() const override { return m_computeShader != nullptr; } + bool IsValid() const override { return true; } + void EnsureValid() override {} + void Shutdown() override {} + void Bind() override {} + void Unbind() override {} + void* GetNativeHandle() override { return this; } + PipelineType GetType() const override { return PipelineType::Graphics; } + +private: + InputLayoutDesc m_inputLayout = {}; + RasterizerDesc m_rasterizer = {}; + BlendDesc m_blend = {}; + DepthStencilStateDesc m_depthStencil = {}; + std::uint32_t m_topologyType = 0u; + std::uint32_t m_sampleCount = 1u; + RHIShader* m_computeShader = nullptr; +}; + +class FakeSampler final : public RHISampler { +public: + void Shutdown() override {} + void Bind(unsigned int) override {} + void Unbind(unsigned int) override {} + void* GetNativeHandle() override { return this; } + unsigned int GetID() override { return 1u; } +}; + +class FakeCommandQueue final : public RHICommandQueue { +public: + void Shutdown() override {} + void ExecuteCommandLists(std::uint32_t, void**) override {} + void Signal(XCEngine::RHI::RHIFence*, std::uint64_t) override {} + void Wait(XCEngine::RHI::RHIFence*, std::uint64_t) override {} + std::uint64_t GetCompletedValue() override { return 0u; } + void WaitForIdle() override {} + CommandQueueType GetType() const override { return CommandQueueType::Direct; } + std::uint64_t GetTimestampFrequency() const override { return 0u; } + void* GetNativeHandle() override { return this; } + void WaitForPreviousFrame() override {} + std::uint64_t GetCurrentFrame() const override { return 0u; } +}; + +class FakeCommandList final : public RHICommandList { +public: + void Shutdown() override {} + void Reset() override {} + void Close() override {} + void TransitionBarrier(RHIResourceView*, ResourceStates, ResourceStates) override {} + void BeginRenderPass(XCEngine::RHI::RHIRenderPass*, XCEngine::RHI::RHIFramebuffer*, const Rect&, std::uint32_t, const XCEngine::RHI::ClearValue*) override {} + void EndRenderPass() override {} + void SetShader(RHIShader*) override {} + + void SetPipelineState(RHIPipelineState* pso) override { + pipelineStateHistory.push_back(pso); + } + + void SetGraphicsDescriptorSets( + std::uint32_t, + std::uint32_t count, + RHIDescriptorSet** descriptorSets, + RHIPipelineLayout*) override { + if (count >= 2u && descriptorSets != nullptr) { + textureDescriptorSetHistory.push_back(descriptorSets[1]); + } + } + + void SetComputeDescriptorSets(std::uint32_t, std::uint32_t, RHIDescriptorSet**, RHIPipelineLayout*) override {} + void SetPrimitiveTopology(PrimitiveTopology topology) override { primitiveTopologyHistory.push_back(topology); } + void SetViewport(const Viewport& viewport) override { viewportHistory.push_back(viewport); } + void SetViewports(std::uint32_t count, const Viewport* viewports) override { + for (std::uint32_t index = 0; index < count; ++index) { + viewportHistory.push_back(viewports[index]); + } + } + void SetScissorRect(const Rect& rect) override { scissorHistory.push_back(rect); } + void SetScissorRects(std::uint32_t count, const Rect* rects) override { + for (std::uint32_t index = 0; index < count; ++index) { + scissorHistory.push_back(rects[index]); + } + } + void SetRenderTargets(std::uint32_t count, RHIResourceView**, RHIResourceView* = nullptr) override { + renderTargetBindHistory.push_back(count); + } + void SetStencilRef(std::uint8_t) override {} + void SetBlendFactor(const float[4]) override {} + void SetVertexBuffers(std::uint32_t, std::uint32_t, RHIResourceView**, const std::uint64_t*, const std::uint32_t*) override {} + void SetIndexBuffer(RHIResourceView*, std::uint64_t) override {} + + void Draw(std::uint32_t vertexCount, std::uint32_t, std::uint32_t startVertex, std::uint32_t) override { + drawVertexCounts.push_back(vertexCount); + drawStartVertices.push_back(startVertex); + } + + void DrawIndexed(std::uint32_t, std::uint32_t, std::uint32_t, std::int32_t, std::uint32_t) override {} + void Clear(float, float, float, float, std::uint32_t) override {} + void ClearRenderTarget(RHIResourceView*, const float[4], std::uint32_t = 0, const Rect* = nullptr) override {} + void ClearDepthStencil(RHIResourceView*, float, std::uint8_t, std::uint32_t = 0, const Rect* = nullptr) override {} + void CopyResource(RHIResourceView*, RHIResourceView*) override {} + void Dispatch(std::uint32_t, std::uint32_t, std::uint32_t) override {} + void* GetNativeHandle() override { return this; } + + std::vector pipelineStateHistory = {}; + std::vector primitiveTopologyHistory = {}; + std::vector viewportHistory = {}; + std::vector scissorHistory = {}; + std::vector renderTargetBindHistory = {}; + std::vector textureDescriptorSetHistory = {}; + std::vector drawVertexCounts = {}; + std::vector drawStartVertices = {}; +}; + +class FakeDevice final : public RHIDevice { +public: + bool Initialize(const XCEngine::RHI::RHIDeviceDesc&) override { return true; } + void Shutdown() override {} + RHIBuffer* CreateBuffer(const BufferDesc& desc) override { return new FakeBuffer(desc); } + XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc&) override { return nullptr; } + XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc&, const void*, size_t, std::uint32_t = 0) override { return nullptr; } + XCEngine::RHI::RHISwapChain* CreateSwapChain(const XCEngine::RHI::SwapChainDesc&, RHICommandQueue*) override { return nullptr; } + RHICommandList* CreateCommandList(const XCEngine::RHI::CommandListDesc&) override { return nullptr; } + RHICommandQueue* CreateCommandQueue(const XCEngine::RHI::CommandQueueDesc&) override { return nullptr; } + RHIShader* CreateShader(const XCEngine::RHI::ShaderCompileDesc&) override { return nullptr; } + RHIPipelineState* CreatePipelineState(const GraphicsPipelineDesc& desc) override { return new FakePipelineState(desc); } + RHIPipelineLayout* CreatePipelineLayout(const XCEngine::RHI::RHIPipelineLayoutDesc&) override { return new FakePipelineLayout(); } + XCEngine::RHI::RHIFence* CreateFence(const XCEngine::RHI::FenceDesc&) override { return nullptr; } + RHISampler* CreateSampler(const SamplerDesc&) override { return new FakeSampler(); } + XCEngine::RHI::RHIRenderPass* CreateRenderPass(std::uint32_t, const XCEngine::RHI::AttachmentDesc*, const XCEngine::RHI::AttachmentDesc*) override { return nullptr; } + XCEngine::RHI::RHIFramebuffer* CreateFramebuffer(XCEngine::RHI::RHIRenderPass*, std::uint32_t, std::uint32_t, std::uint32_t, RHIResourceView**, RHIResourceView*) override { return nullptr; } + RHIDescriptorPool* CreateDescriptorPool(const DescriptorPoolDesc& desc) override { return new FakeDescriptorPool(desc); } + RHIDescriptorSet* CreateDescriptorSet(RHIDescriptorPool*, const DescriptorSetLayoutDesc&) override { return nullptr; } + RHIResourceView* CreateVertexBufferView(RHIBuffer* buffer, const ResourceViewDesc& desc) override { + return new FakeVertexBufferView(static_cast(buffer != nullptr ? buffer->GetSize() : 0u), desc.structureByteStride); + } + RHIResourceView* CreateIndexBufferView(RHIBuffer*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateRenderTargetView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateDepthStencilView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateShaderResourceView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateUnorderedAccessView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; } + const XCEngine::RHI::RHICapabilities& GetCapabilities() const override { return m_capabilities; } + const XCEngine::RHI::RHIDeviceInfo& GetDeviceInfo() const override { return m_deviceInfo; } + void* GetNativeDevice() override { return this; } + +private: + XCEngine::RHI::RHICapabilities m_capabilities = {}; + XCEngine::RHI::RHIDeviceInfo m_deviceInfo = {}; +}; + +UITextureHandle MakeExternalTextureHandle(FakeShaderResourceView& shaderView, std::uint32_t width = 32u, std::uint32_t height = 32u) { + UITextureHandle texture = {}; + texture.nativeHandle = reinterpret_cast(&shaderView); + texture.width = width; + texture.height = height; + texture.kind = UITextureHandleKind::ShaderResourceView; + return texture; +} + +} // namespace + +TEST(XCUIRHIRenderBackendTest, ExposesNativeOverlayEntryAndAtlasInjection) { + static_assert(std::is_same_v< + decltype(std::declval().Render( + std::declval(), + std::declval(), + std::declval())), + bool>); + static_assert(std::is_same_v< + decltype(std::declval().SetTextAtlasProvider( + std::declval())) , + void>); + static_assert(std::is_same_v< + decltype(std::declval().GetLastOverlayStats().drawListCount), + std::size_t>); + + SUCCEED(); +} + +TEST(XCUIRHIRenderBackendTest, RenderRejectsInvalidContextAndLeavesStatsClear) { + XCUIRHIRenderBackend backend = {}; + RenderSurface surface(128, 72); + UIDrawData drawData = {}; + + EXPECT_FALSE(backend.Render(RenderContext(), surface, drawData)); + EXPECT_EQ(backend.GetLastOverlayStats().commandCount, 0u); + EXPECT_EQ(backend.GetLastOverlayStats().renderedCommandCount, 0u); +} + +TEST(XCUIRHIRenderBackendTest, SetTextAtlasProviderStoresPointerAndResetStatsClearsCounters) { + XCUIRHIRenderBackend backend = {}; + + class StubAtlasProvider final : public IXCUITextAtlasProvider { + public: + bool GetAtlasTextureView(PixelFormat, AtlasTextureView& outView) const override { + outView = {}; + return false; + } + std::size_t GetFontCount() const override { return 0u; } + FontHandle GetFont(std::size_t) const override { return {}; } + FontHandle GetDefaultFont() const override { return {}; } + bool GetFontInfo(FontHandle, FontInfo& outInfo) const override { + outInfo = {}; + return false; + } + bool GetBakedFontInfo(FontHandle, float, BakedFontInfo& outInfo) const override { + outInfo = {}; + return false; + } + bool FindGlyph(FontHandle, float, std::uint32_t, GlyphInfo& outInfo) const override { + outInfo = {}; + return false; + } + } atlasProvider; + + backend.SetTextAtlasProvider(&atlasProvider); + EXPECT_EQ(backend.GetTextAtlasProvider(), &atlasProvider); + + backend.SetTextAtlasProvider(nullptr); + EXPECT_EQ(backend.GetTextAtlasProvider(), nullptr); + + backend.ResetStats(); + EXPECT_EQ(backend.GetLastStats().drawListCount, 0u); + EXPECT_EQ(backend.GetLastStats().commandCount, 0u); + EXPECT_EQ(backend.GetLastStats().batchCount, 0u); + EXPECT_EQ(backend.GetLastStats().renderedCommandCount, 0u); + EXPECT_EQ(backend.GetLastStats().skippedCommandCount, 0u); + EXPECT_EQ(backend.GetLastStats().textureResolveCount, 0u); + EXPECT_EQ(backend.GetLastStats().textureCacheHitCount, 0u); +} + +TEST(XCUIRHIRenderBackendTest, RenderReusesExternalTextureBindingsAcrossTexturedBatches) { + FakeDevice device = {}; + FakeCommandList commandList = {}; + FakeCommandQueue commandQueue = {}; + FakeRenderTargetView renderTarget = {}; + FakeShaderResourceView externalTextureView(true); + + RenderContext context = {}; + context.device = &device; + context.commandList = &commandList; + context.commandQueue = &commandQueue; + context.backendType = RHIType::D3D12; + + RenderSurface surface(64, 48); + surface.SetColorAttachment(&renderTarget); + + UIDrawData drawData = {}; + UIDrawList& firstDrawList = drawData.EmplaceDrawList("FirstTexturedBatch"); + firstDrawList.PushClipRect(UIRect(4.0f, 6.0f, 12.0f, 10.0f)); + firstDrawList.AddImage( + UIRect(0.0f, 0.0f, 18.0f, 18.0f), + MakeExternalTextureHandle(externalTextureView), + UIColor(1.0f, 0.8f, 0.6f, 1.0f)); + firstDrawList.PopClipRect(); + + UIDrawList& secondDrawList = drawData.EmplaceDrawList("SecondTexturedBatch"); + secondDrawList.PushClipRect(UIRect(20.0f, 8.0f, 10.0f, 12.0f)); + secondDrawList.AddImage( + UIRect(18.0f, 4.0f, 16.0f, 16.0f), + MakeExternalTextureHandle(externalTextureView), + UIColor(0.7f, 0.9f, 1.0f, 1.0f)); + secondDrawList.PopClipRect(); + + XCUIRHIRenderBackend backend = {}; + ASSERT_TRUE(backend.Render(context, surface, drawData)); + + const XCUIRHIRenderBackend::OverlayStats& stats = backend.GetLastOverlayStats(); + EXPECT_EQ(stats.drawListCount, 2u); + EXPECT_EQ(stats.commandCount, 6u); + EXPECT_EQ(stats.batchCount, 2u); + EXPECT_EQ(stats.colorBatchCount, 0u); + EXPECT_EQ(stats.texturedBatchCount, 2u); + EXPECT_EQ(stats.scissoredBatchCount, 2u); + EXPECT_EQ(stats.renderedCommandCount, 2u); + EXPECT_EQ(stats.skippedCommandCount, 0u); + EXPECT_EQ(stats.textureResolveCount, 2u); + EXPECT_EQ(stats.textureCacheHitCount, 1u); + EXPECT_EQ(stats.vertexCount, 12u); + EXPECT_EQ(stats.triangleCount, 4u); + + ASSERT_EQ(commandList.renderTargetBindHistory.size(), 1u); + ASSERT_EQ(commandList.viewportHistory.size(), 1u); + ASSERT_EQ(commandList.drawVertexCounts.size(), 2u); + EXPECT_EQ(commandList.drawVertexCounts[0], 6u); + EXPECT_EQ(commandList.drawVertexCounts[1], 6u); + ASSERT_EQ(commandList.textureDescriptorSetHistory.size(), 2u); + EXPECT_EQ(commandList.textureDescriptorSetHistory[0], commandList.textureDescriptorSetHistory[1]); + + ASSERT_EQ(commandList.scissorHistory.size(), 4u); + EXPECT_EQ(commandList.scissorHistory[0].left, 0); + EXPECT_EQ(commandList.scissorHistory[0].top, 0); + EXPECT_EQ(commandList.scissorHistory[0].right, 64); + EXPECT_EQ(commandList.scissorHistory[0].bottom, 48); + EXPECT_EQ(commandList.scissorHistory[1].left, 4); + EXPECT_EQ(commandList.scissorHistory[1].top, 6); + EXPECT_EQ(commandList.scissorHistory[1].right, 16); + EXPECT_EQ(commandList.scissorHistory[1].bottom, 16); + EXPECT_EQ(commandList.scissorHistory[2].left, 20); + EXPECT_EQ(commandList.scissorHistory[2].top, 8); + EXPECT_EQ(commandList.scissorHistory[2].right, 30); + EXPECT_EQ(commandList.scissorHistory[2].bottom, 20); + EXPECT_EQ(commandList.scissorHistory[3].left, 0); + EXPECT_EQ(commandList.scissorHistory[3].top, 0); + EXPECT_EQ(commandList.scissorHistory[3].right, 64); + EXPECT_EQ(commandList.scissorHistory[3].bottom, 48); +} + +TEST(XCUIRHIRenderBackendTest, RenderSkipsTexturedBatchWhenExternalTextureViewIsInvalid) { + FakeDevice device = {}; + FakeCommandList commandList = {}; + FakeCommandQueue commandQueue = {}; + FakeRenderTargetView renderTarget = {}; + FakeShaderResourceView invalidTextureView(false); + + RenderContext context = {}; + context.device = &device; + context.commandList = &commandList; + context.commandQueue = &commandQueue; + context.backendType = RHIType::D3D12; + + RenderSurface surface(32, 32); + surface.SetColorAttachment(&renderTarget); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("InvalidExternalTexture"); + drawList.AddImage( + UIRect(2.0f, 4.0f, 12.0f, 8.0f), + MakeExternalTextureHandle(invalidTextureView), + UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + + XCUIRHIRenderBackend backend = {}; + ASSERT_TRUE(backend.Render(context, surface, drawData)); + + const XCUIRHIRenderBackend::OverlayStats& stats = backend.GetLastOverlayStats(); + EXPECT_EQ(stats.commandCount, 1u); + EXPECT_EQ(stats.batchCount, 1u); + EXPECT_EQ(stats.texturedBatchCount, 1u); + EXPECT_EQ(stats.renderedCommandCount, 0u); + EXPECT_EQ(stats.skippedCommandCount, 1u); + EXPECT_EQ(stats.textureResolveCount, 1u); + EXPECT_EQ(stats.textureCacheHitCount, 0u); + EXPECT_EQ(commandList.drawVertexCounts.size(), 0u); + EXPECT_EQ(commandList.textureDescriptorSetHistory.size(), 0u); +} diff --git a/tests/NewEditor/test_xcui_standalone_text_atlas_provider.cpp b/tests/NewEditor/test_xcui_standalone_text_atlas_provider.cpp new file mode 100644 index 00000000..ff7c7fc6 --- /dev/null +++ b/tests/NewEditor/test_xcui_standalone_text_atlas_provider.cpp @@ -0,0 +1,45 @@ +#include + +#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h" + +#include + +namespace { + +using XCEngine::Editor::XCUIBackend::IXCUITextAtlasProvider; +using XCEngine::Editor::XCUIBackend::XCUIStandaloneTextAtlasProvider; + +TEST(XCUIStandaloneTextAtlasProviderTest, BuildsDefaultEditorAtlasWithoutImGuiContext) { + XCUIStandaloneTextAtlasProvider provider = {}; + + IXCUITextAtlasProvider::AtlasTextureView atlasView = {}; + ASSERT_TRUE(provider.IsReady()); + ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView)); + EXPECT_TRUE(atlasView.IsValid()); + EXPECT_EQ(atlasView.format, IXCUITextAtlasProvider::PixelFormat::RGBA32); + EXPECT_GT(provider.GetFontCount(), 0u); +} + +TEST(XCUIStandaloneTextAtlasProviderTest, ExposesDefaultFontMetricsAndGlyphs) { + XCUIStandaloneTextAtlasProvider provider = {}; + const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont(); + ASSERT_TRUE(defaultFont.IsValid()); + + IXCUITextAtlasProvider::FontInfo fontInfo = {}; + ASSERT_TRUE(provider.GetFontInfo(defaultFont, fontInfo)); + EXPECT_GT(fontInfo.nominalSize, 0.0f); + + IXCUITextAtlasProvider::BakedFontInfo bakedFontInfo = {}; + ASSERT_TRUE(provider.GetBakedFontInfo(defaultFont, 0.0f, bakedFontInfo)); + EXPECT_GT(bakedFontInfo.lineHeight, 0.0f); + EXPECT_GT(bakedFontInfo.rasterizerDensity, 0.0f); + + IXCUITextAtlasProvider::GlyphInfo glyphInfo = {}; + ASSERT_TRUE(provider.FindGlyph(defaultFont, 0.0f, static_cast('A'), glyphInfo)); + EXPECT_EQ(glyphInfo.requestedCodepoint, static_cast('A')); + EXPECT_GT(glyphInfo.advanceX, 0.0f); + EXPECT_GE(glyphInfo.u1, glyphInfo.u0); + EXPECT_GE(glyphInfo.v1, glyphInfo.v0); +} + +} // namespace