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