Add XCUI new editor sandbox phase 1

This commit is contained in:
2026-04-05 04:55:25 +08:00
parent e23f469e5a
commit 67a28bdd4a
76 changed files with 14671 additions and 3 deletions

5
Assets.meta Normal file
View File

@@ -0,0 +1,5 @@
fileFormatVersion: 1
guid: 60a2f7fbbea9b0ef86cefe4d4ea75578
folderAsset: true
importer: FolderImporter
importerVersion: 5

5
Assets/XCUI.meta Normal file
View File

@@ -0,0 +1,5 @@
fileFormatVersion: 1
guid: 6dd3f8fecfde5a3f1d52cb3a9e18ed53
folderAsset: true
importer: FolderImporter
importerVersion: 5

View File

@@ -0,0 +1,5 @@
fileFormatVersion: 1
guid: b6816d88212c73fe81921340c22e0939
folderAsset: true
importer: FolderImporter
importerVersion: 5

View File

@@ -0,0 +1,5 @@
fileFormatVersion: 1
guid: de79bdd523120a9286b09d0a06bf150c
folderAsset: true
importer: FolderImporter
importerVersion: 5

View File

@@ -0,0 +1,24 @@
<Theme name="XCUI New Editor Theme">
<Token name="color.surface" type="color" value="#0D1218" />
<Token name="color.surface.elevated" type="color" value="#141C26" />
<Token name="color.surface.card" type="color" value="#1D2733" />
<Token name="color.surface.input" type="color" value="#101923" />
<Token name="color.text.primary" type="color" value="#F7F9FC" />
<Token name="color.text.secondary" type="color" value="#BCC8DB" />
<Token name="color.text.placeholder" type="color" value="#7B8A9B" />
<Token name="color.accent" type="color" value="#5EE3FF" />
<Token name="color.accent.alt" type="color" value="#FFB65F" />
<Token name="color.outline" type="color" value="#3A4A5A" />
<Token name="color.surface.track" type="color" value="#243241" />
<Token name="space.compact" type="float" value="8" />
<Token name="space.regular" type="float" value="14" />
<Token name="space.loose" type="float" value="20" />
<Token name="padding.panel" type="thickness" value="18" />
<Token name="padding.card" type="thickness" value="12" />
<Token name="radius.card" type="corner-radius" value="10" />
<Token name="radius.button" type="corner-radius" value="6" />
<Token name="radius.pill" type="corner-radius" value="18" />
<Token name="font.body" type="float" value="14" />
<Token name="font.title" type="float" value="18" />
<Token name="line.default" type="float" value="1" />
</Theme>

View File

@@ -0,0 +1,5 @@
fileFormatVersion: 1
guid: 35810d9b2e3744637d2655cf8f6ed2d3
folderAsset: false
importer: UIThemeImporter
importerVersion: 5

View File

@@ -0,0 +1,67 @@
<View name="NewEditor Demo" theme="Theme.xctheme" style="Root">
<Column id="rootColumn" gap="10" padding="16">
<Card id="hero" style="HeroCard">
<Column gap="4">
<Text id="title" text="New XCUI Shell" style="HeroTitle" />
<Text id="subtitle" text="Markup -> Layout -> Style -> DrawData" style="HeroSubtitle" />
</Column>
</Card>
<Row id="metricRow" gap="10">
<Card id="metric1" style="MetricCard" width="stretch">
<Column gap="6">
<Text text="Tree status" style="MetricLabel" />
<Text text="Driven by runtime" style="MetricValue" />
<ProgressBar
id="densityMeter"
text="Density stress"
state-key="toggleDensity"
value-off="0.44"
value-on="0.86"
fill="token:color.accent"
width="stretch" />
</Column>
</Card>
<Card id="metric2" style="MetricCard" width="stretch">
<Column gap="6">
<Text text="Input" style="MetricLabel" />
<Text text="Hover + shortcuts" style="MetricValue" />
<Row gap="6">
<Swatch id="swatchAccent" text="Accent" token="color.accent" width="stretch" height="24" />
<Swatch id="swatchAlt" text="Alt" token="color.accent.alt" width="stretch" height="24" />
</Row>
</Column>
</Card>
</Row>
<Row id="toggleRow" gap="8">
<Toggle id="toggleDensity" text="Density stress" width="stretch" />
<Toggle
id="toggleDiagnostics"
text="Debug path"
width="stretch"
fill="token:color.accent.alt" />
</Row>
<Card id="commandCard" style="MetricCard">
<Column gap="6">
<Text text="Agent command" style="MetricLabel" />
<TextField
id="agentPrompt"
style="CommandField"
width="stretch"
min-width="240"
placeholder="Type a command or note for XCUI..."
value="" />
</Column>
</Card>
<Button id="toggleAccent" action="demo.toggleAccent" style="AccentButton">
<Text text="Toggle Accent" style="ButtonLabel" />
</Button>
<Card id="debug" style="DebugCard">
<Column gap="3">
<Text id="statusFocus" text="Focus: waiting" style="Meta" />
<Text id="statusLayout" text="Layout: idle" style="Meta" />
<Text id="statusCommands" text="Commands: ready" style="Meta" />
<Text id="statusWidgets" text="Widgets: ready" style="Meta" />
</Column>
</Card>
</Column>
</View>

View File

@@ -0,0 +1,5 @@
fileFormatVersion: 1
guid: 9586ad2d05baa7ff00e16bb3c6eab1ea
folderAsset: false
importer: UIViewImporter
importerVersion: 5

View File

@@ -0,0 +1,5 @@
fileFormatVersion: 1
guid: 2767aae796d78abc773d2576b6bdce54
folderAsset: true
importer: FolderImporter
importerVersion: 5

View File

@@ -0,0 +1,19 @@
<Theme name="Layout Lab Theme">
<Token name="color.panel" type="color" value="#10161D" />
<Token name="color.card" type="color" value="#1C2632" />
<Token name="color.card.alt" type="color" value="#243243" />
<Token name="color.accent" type="color" value="#2E5A73" />
<Token name="color.border" type="color" value="#3B4E62" />
<Token name="color.scroll.surface" type="color" value="#121B24" />
<Token name="color.text" type="color" value="#F1F6FB" />
<Token name="color.text.muted" type="color" value="#B8C6D6" />
<Token name="space.outer" type="float" value="18" />
<Token name="space.gap" type="float" value="14" />
<Token name="space.stack" type="float" value="12" />
<Token name="space.cardInset" type="float" value="12" />
<Token name="radius.card" type="float" value="10" />
<Token name="size.listItemHeight" type="float" value="60" />
<Token name="size.scrollStep" type="float" value="64" />
<Token name="font.title" type="float" value="16" />
<Token name="font.body" type="float" value="13" />
</Theme>

View File

@@ -0,0 +1,5 @@
fileFormatVersion: 1
guid: fca776270d9840e08180c456593e683d
folderAsset: false
importer: UIThemeImporter
importerVersion: 5

View File

@@ -0,0 +1,105 @@
<View name="Layout Lab" theme="Theme.xctheme">
<Column id="root" gap="14" padding="18">
<Card
id="heroCard"
height="92"
tone="accent"
title="XCUI Layout Lab"
subtitle="Editor-style panels with overlay and scroll semantics." />
<Row id="mainRow" height="stretch" gap="14">
<Column id="leftRail" width="272" gap="10">
<Card
id="toolShelf"
height="62"
tone="accent-alt"
title="Tool Shelf"
subtitle="Scene, asset, and play-mode actions." />
<ScrollView id="assetList" height="stretch" padding="10" gap="8" scrollY="88">
<Card
id="assetListHeader"
height="54"
tone="accent-alt"
title="Project Browser"
subtitle="Pinned filters and import shortcuts." />
<Card id="assetMaterials" title="Materials_Master" subtitle="14 assets selected" />
<Card id="assetTerrain" title="Terrain_Cliffs_04" subtitle="Texture2D -4096 x 4096" />
<Card id="assetLighting" title="Lighting_GlobalRig" subtitle="Prefab -Directional setup" />
<Card id="assetCharacter" title="Hero_Character_Controller" subtitle="Prefab -Animation graph bound" />
<Card id="assetUiAtlas" title="UI_RuntimeAtlas" subtitle="SpriteAtlas -83 packed sprites" />
<Card id="assetAudio" title="Audio_Ambience_Forest" subtitle="AudioBank -loop region authored" />
<Card id="assetShader" title="Shader_StylizedTerrain" subtitle="ShaderGraph -6 exposed params" />
<Card id="assetQuest" title="Quest_DebugFlow" subtitle="ScriptableObject -branching state" />
</ScrollView>
</Column>
<Column id="centerColumn" width="stretch" gap="10">
<Card
id="viewportToolbar"
height="62"
title="Viewport Toolbar"
subtitle="Gizmos, snap presets, camera bookmarks." />
<Overlay id="viewportOverlay" height="stretch">
<Card
id="viewportBase"
title="Scene Viewport"
subtitle="Primary preview surface with composition overlays." />
<Card
id="viewportBadge"
x="18"
y="18"
width="224"
height="68"
tone="accent-alt"
title="Selection Overlay"
subtitle="Bounds, pivots, nav markers." />
<Card
id="viewportInspectorBubble"
x="0.58"
y="0.54"
width="0.32"
height="88"
tone="accent-alt"
title="Context Bubble"
subtitle="Inline quick edit affordance." />
</Overlay>
</Column>
<Column id="inspectorColumn" width="320" gap="10">
<Card
id="inspectorSummary"
height="88"
title="Inspector Summary"
subtitle="Transform, renderer, and prefab overrides." />
<ScrollView id="inspectorSections" height="stretch" padding="10" gap="8" scrollY="52">
<Card id="inspectorTransform" title="Transform" subtitle="Position / rotation / scale" />
<Card id="inspectorMesh" title="Mesh Renderer" subtitle="Materials, shadow flags, probes" />
<Card id="inspectorPhysics" title="Physics Body" subtitle="Mass, drag, collision matrix" />
<Card id="inspectorAnimation" title="Animation Graph" subtitle="Parameters and blend tree state" />
<Card id="inspectorAudio" title="Audio Sources" subtitle="Spatial mix and snapshot sends" />
<Card id="inspectorMetadata" title="Metadata" subtitle="Tags, labels, import provenance" />
</ScrollView>
</Column>
</Row>
<Overlay id="footerOverlay" height="132">
<Card
id="footerBase"
title="Timeline Strip"
subtitle="Bottom utility lane for profiler, logs, and timeline cues." />
<Card
id="timelineMarkers"
x="18"
y="42"
width="220"
height="58"
tone="accent-alt"
title="Markers" />
<Card
id="timelinePlayback"
x="0.68"
y="42"
width="220"
height="58"
tone="accent-alt"
title="Playback Controls" />
</Overlay>
</Column>
</View>

View File

@@ -0,0 +1,5 @@
fileFormatVersion: 1
guid: 9a03d17147d903cad459db2c205a9f30
folderAsset: false
importer: UIViewImporter
importerVersion: 5

View File

@@ -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
"$<$<CONFIG:Debug,RelWithDebInfo>: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)

View File

@@ -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.

View File

@@ -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 <XCEngine/Resources/UI/UIDocumentCompiler.h>
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 "$<$<CONFIG:Debug,RelWithDebInfo>: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)

View File

@@ -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;

120
new_editor/CMakeLists.txt Normal file
View File

@@ -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
$<$<CONFIG:Debug,RelWithDebInfo>:/INCREMENTAL:NO>)
set_property(TARGET ${PROJECT_NAME} PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
set_target_properties(${PROJECT_NAME} PROPERTIES
MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>: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}"
)

16
new_editor/README.md Normal file
View File

@@ -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.

View File

@@ -0,0 +1,19 @@
<Theme name="XCUI New Editor Theme">
<Token name="color.surface" type="color" value="#0D1218" />
<Token name="color.surface.elevated" type="color" value="#141C26" />
<Token name="color.surface.card" type="color" value="#1D2733" />
<Token name="color.text.primary" type="color" value="#F7F9FC" />
<Token name="color.text.secondary" type="color" value="#BCC8DB" />
<Token name="color.accent" type="color" value="#5EE3FF" />
<Token name="color.outline" type="color" value="#3A4A5A" />
<Token name="space.compact" type="float" value="8" />
<Token name="space.regular" type="float" value="14" />
<Token name="space.loose" type="float" value="20" />
<Token name="padding.panel" type="thickness" value="18" />
<Token name="padding.card" type="thickness" value="12" />
<Token name="radius.card" type="corner-radius" value="10" />
<Token name="radius.button" type="corner-radius" value="6" />
<Token name="font.body" type="float" value="14" />
<Token name="font.title" type="float" value="18" />
<Token name="line.default" type="float" value="1" />
</Theme>

View File

@@ -0,0 +1,30 @@
<View name="NewEditor Demo" theme="xcui_demo_theme.xctheme" style="Root">
<Column id="rootColumn" gap="12" padding="18">
<Card id="hero" style="HeroCard">
<Column gap="6">
<Text id="title" text="New XCUI Shell" style="HeroTitle" />
<Text id="subtitle" text="Markup -> Layout -> Style -> DrawData" style="HeroSubtitle" />
</Column>
</Card>
<Row id="metricRow" gap="10">
<Card id="metric1" style="MetricCard" width="stretch">
<Text text="Tree status" style="MetricLabel" />
<Text text="Driven by runtime" style="MetricValue" />
</Card>
<Card id="metric2" style="MetricCard" width="stretch">
<Text text="Input" style="MetricLabel" />
<Text text="Hover + shortcuts" style="MetricValue" />
</Card>
</Row>
<Button id="toggleAccent" style="AccentButton">
<Text text="Toggle Accent" style="ButtonLabel" />
</Button>
<Card id="debug" style="DebugCard">
<Column gap="4">
<Text id="statusFocus" text="Focus: waiting" style="Meta" />
<Text id="statusLayout" text="Layout: idle" style="Meta" />
<Text id="statusCommands" text="Commands: ready" style="Meta" />
</Column>
</Card>
</Column>
</View>

View File

@@ -0,0 +1,16 @@
<Theme name="Layout Lab Theme">
<Token name="color.panel" type="color" value="#10161D" />
<Token name="color.card" type="color" value="#1C2632" />
<Token name="color.card.alt" type="color" value="#243243" />
<Token name="color.accent" type="color" value="#2E5A73" />
<Token name="color.border" type="color" value="#3B4E62" />
<Token name="color.text" type="color" value="#F1F6FB" />
<Token name="color.text.muted" type="color" value="#B8C6D6" />
<Token name="space.outer" type="float" value="18" />
<Token name="space.gap" type="float" value="14" />
<Token name="space.stack" type="float" value="12" />
<Token name="space.cardInset" type="float" value="12" />
<Token name="radius.card" type="float" value="10" />
<Token name="font.title" type="float" value="16" />
<Token name="font.body" type="float" value="13" />
</Theme>

View File

@@ -0,0 +1,66 @@
<View name="Layout Lab" theme="xcui_layout_lab_theme.xctheme">
<Column id="root" gap="14" padding="18">
<Card
id="heroCard"
height="92"
tone="accent"
title="XCUI Layout Lab"
subtitle="Resource-driven row / column / overlay stress." />
<Row id="mainRow" height="stretch" gap="14">
<Column id="leftColumn" width="0.28" gap="12">
<Card id="leftTop" height="stretch" title="Left Column" subtitle="Stack item 1" />
<Card id="leftBottom" height="stretch" title="Left Column" subtitle="Stack item 2" />
</Column>
<Overlay id="centerOverlay" width="0.42">
<Card
id="overlayBase"
title="Center Overlay"
subtitle="Base layer filling the entire region." />
<Card
id="overlayNorth"
x="18"
y="18"
width="0.42"
height="72"
tone="accent-alt"
title="Overlay A"
subtitle="Floating note" />
<Card
id="overlayCenter"
x="0.28"
y="0.45"
width="0.44"
height="86"
tone="accent-alt"
title="Overlay B"
subtitle="Centered overlay layer" />
</Overlay>
<Column id="rightColumn" width="stretch" gap="12">
<Card id="rightTop" height="stretch" title="Right Column" subtitle="Another stacked column" />
<Card id="rightBottom" height="stretch" title="Right Column" subtitle="Pairs with the overlay stage" />
</Column>
</Row>
<Overlay id="footerOverlay" height="146">
<Card
id="footerBase"
title="Footer Overlay"
subtitle="Second overlay region for quick smoke testing." />
<Card
id="footerLeft"
x="18"
y="48"
width="180"
height="58"
tone="accent-alt"
title="Footer A" />
<Card
id="footerRight"
x="0.74"
y="48"
width="180"
height="58"
tone="accent-alt"
title="Footer B" />
</Overlay>
</Column>
</View>

View File

@@ -0,0 +1,931 @@
#include "Application.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <imgui.h>
#include <algorithm>
#include <cmath>
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 <typename ResourceType>
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<XCUIDemoPanel>(
&m_xcuiInputSource,
CreateHostedPreviewPresenter(m_showNativeDemoPanelPreview));
m_layoutLabPanel = std::make_unique<XCUILayoutLabPanel>(
&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<int>(message.wParam);
}
LRESULT CALLBACK Application::StaticWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
Application* app = reinterpret_cast<Application*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
if (message == WM_NCCREATE) {
const CREATESTRUCTW* createStruct = reinterpret_cast<const CREATESTRUCTW*>(lParam);
app = reinterpret_cast<Application*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(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<int>(LOWORD(lParam)), static_cast<int>(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::uint32_t>(std::ceil(logicalSize.width))
: 0u;
const std::uint32_t height = logicalSize.height > 1.0f
? static_cast<std::uint32_t>(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<std::uint32_t>(::XCEngine::RHI::Format::R8G8B8A8_UNorm);
colorDesc.textureType = static_cast<std::uint32_t>(::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<std::uint32_t>(::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::uint32_t>(std::ceil(queuedFrame.logicalSize.width))
: 0u;
const std::uint32_t expectedHeight = queuedFrame.logicalSize.height > 1.0f
? static_cast<std::uint32_t>(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<float>(previewSurface.width),
static_cast<float>(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<float>(
std::chrono::duration<double>(std::chrono::steady_clock::now() - m_startTime).count());
frameState.pulseAccent = m_pulseNativeBackdropAccent;
frameState.drawBackdrop = m_showNativeBackdrop;
if (m_showNativeXCUIOverlay) {
const float width = static_cast<float>(surface.GetWidth());
const float height = static_cast<float>(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

View File

@@ -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 <chrono>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <windows.h>
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<XCUIDemoPanel> m_demoPanel;
std::unique_ptr<XCUILayoutLabPanel> 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<HostedPreviewOffscreenSurface> 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

View File

@@ -0,0 +1,275 @@
#include "Rendering/MainWindowBackdropPass.h"
#include <XCEngine/Core/Math/Vector4.h>
#include <XCEngine/RHI/RHICommandList.h>
#include <XCEngine/RHI/RHIDevice.h>
#include <XCEngine/RHI/RHITypes.h>
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<float>(surface.GetWidth()),
static_cast<float>(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<float>(surface.GetWidth()),
static_cast<float>(surface.GetHeight()),
0.0f,
1.0f
};
const RHI::Rect scissorRect = {
0,
0,
static_cast<int32_t>(surface.GetWidth()),
static_cast<int32_t>(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<uint32_t>(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<uint32_t>(RHI::PrimitiveTopologyType::Triangle);
pipelineDesc.renderTargetCount = 1;
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::Unknown);
pipelineDesc.sampleCount = 1;
pipelineDesc.rasterizerState.fillMode = static_cast<uint32_t>(RHI::FillMode::Solid);
pipelineDesc.rasterizerState.cullMode = static_cast<uint32_t>(RHI::CullMode::None);
pipelineDesc.rasterizerState.frontFace = static_cast<uint32_t>(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<uint32_t>(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

View File

@@ -0,0 +1,41 @@
#pragma once
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/RHI/RHIDescriptorPool.h>
#include <XCEngine/RHI/RHIDescriptorSet.h>
#include <XCEngine/RHI/RHIEnums.h>
#include <XCEngine/RHI/RHIPipelineLayout.h>
#include <XCEngine/RHI/RHIPipelineState.h>
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

View File

@@ -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

View File

@@ -0,0 +1,39 @@
#pragma once
#include "Rendering/MainWindowBackdropPass.h"
#include "XCUIBackend/XCUIRHIRenderBackend.h"
#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h"
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/UI/DrawData.h>
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

View File

@@ -0,0 +1,3 @@
#pragma once
#include "../../../editor/src/UI/ImGuiBackendBridge.h"

View File

@@ -0,0 +1,90 @@
#pragma once
#include <cstddef>
#include <cstdint>
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

View File

@@ -0,0 +1,223 @@
#include "XCUIBackend/ImGuiTextAtlasProvider.h"
#include <imgui.h>
#include <imgui_internal.h>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
namespace {
IXCUITextAtlasProvider::FontHandle MakeFontHandle(const ImFont* font) {
IXCUITextAtlasProvider::FontHandle handle = {};
handle.value = reinterpret_cast<std::uintptr_t>(font);
return handle;
}
ImFont* ResolveFontHandle(IXCUITextAtlasProvider::FontHandle handle) {
return reinterpret_cast<ImFont*>(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<std::uintptr_t>(atlas);
outView.pixelDataKey = reinterpret_cast<std::uintptr_t>(pixels);
return true;
}
std::size_t ImGuiTextAtlasProvider::GetFontCount() const {
const ImFontAtlas* atlas = ResolveAtlas(ResolveContext());
return atlas != nullptr ? static_cast<std::size_t>(atlas->Fonts.Size) : 0u;
}
IXCUITextAtlasProvider::FontHandle ImGuiTextAtlasProvider::GetFont(std::size_t index) const {
const ImFontAtlas* atlas = ResolveAtlas(ResolveContext());
if (atlas == nullptr || index >= static_cast<std::size_t>(atlas->Fonts.Size)) {
return {};
}
return MakeFontHandle(atlas->Fonts[static_cast<int>(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<ImWchar>(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

View File

@@ -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

View File

@@ -0,0 +1,193 @@
#pragma once
#include <XCEngine/UI/DrawData.h>
#include <imgui.h>
#include <algorithm>
#include <cstddef>
#include <utility>
#include <vector>
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<ImTextureID>(0);
}
return static_cast<ImTextureID>(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<int>(r * 255.0f),
static_cast<int>(g * 255.0f),
static_cast<int>(b * 255.0f),
static_cast<int>(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

View File

@@ -0,0 +1,226 @@
#include "XCUIBackend/ImGuiXCUIInputAdapter.h"
#include <algorithm>
#include <cmath>
#include <limits>
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<float>::max() * 0.5f &&
position.y > -std::numeric_limits<float>::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<XCUIInputBridgeKeyState>& 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<std::size_t>(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<std::uint32_t>(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<std::int32_t>(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<std::int32_t>(mapping.keyCode);
}
}
return static_cast<std::int32_t>(KeyCode::None);
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,22 @@
#pragma once
#include "XCUIBackend/XCUIInputBridge.h"
#include <imgui.h>
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

View File

@@ -0,0 +1,750 @@
#include "XCUIAssetDocumentSource.h"
#include <XCEngine/Core/Asset/ResourceHandle.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Resources/UI/UIDocuments.h>
#include <algorithm>
#include <cctype>
#include <optional>
#include <type_traits>
#include <unordered_set>
#include <utility>
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<char>(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<char>(rawCh);
const bool isUpper = std::isupper(rawCh) != 0;
const bool previousIsLowerOrDigit =
std::islower(static_cast<unsigned char>(previous)) != 0 ||
std::isdigit(static_cast<unsigned char>(previous)) != 0;
if (isUpper && !snake.empty() && previousIsLowerOrDigit && snake.back() != '_') {
snake.push_back('_');
}
snake.push_back(static_cast<char>(std::tolower(rawCh)));
previous = ch;
}
while (!snake.empty() && snake.back() == '_') {
snake.pop_back();
}
return snake.empty() ? std::string("default") : snake;
}
std::optional<fs::path> 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<std::string> 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<fs::path>& outRoots,
std::unordered_set<std::string>& 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<fs::path> BuildRepositoryRootSearchRoots(
const std::vector<fs::path>& explicitSearchRoots,
bool includeDefaultSearchRoots) {
std::vector<fs::path> searchRoots = {};
std::unordered_set<std::string> 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<fs::path> currentPath = GetCurrentPath();
if (currentPath.has_value()) {
AppendUniqueSearchRoot(*currentPath, searchRoots, seenRoots);
}
return searchRoots;
}
void AddCandidate(
std::vector<XCUIAssetDocumentSource::ResolutionCandidate>& candidates,
std::unordered_set<std::string>& 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<XCUIAssetDocumentSource::ResolutionCandidate>& candidates,
std::unordered_set<std::string>& 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 <typename TDocumentResource>
bool TryLoadDocumentFromResourceManager(
const std::string& requestPath,
UIDocumentCompileResult& outResult) {
static_assert(
std::is_base_of_v<UIDocumentResource, TDocumentResource>,
"TDocumentResource must derive from UIDocumentResource");
auto resource = ResourceManager::Get().Load<TDocumentResource>(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<UIView>(requestPath, outResult);
case UIDocumentKind::Theme:
return TryLoadDocumentFromResourceManager<UITheme>(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<std::string>& outPaths,
std::unordered_set<std::string>& 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<XCUIAssetDocumentSource::TrackedWriteTime>& outTrackedWriteTimes,
std::vector<std::string>& 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<unsigned long long>(outTrackedWriteTimes.size())) +
" XCUI source file(s) for hot reload.";
return;
}
outTrackingStatusMessage =
"Tracking " +
std::to_string(static_cast<unsigned long long>(outTrackedWriteTimes.size())) +
" of " +
std::to_string(static_cast<unsigned long long>(state.trackedSourcePaths.size())) +
" XCUI source file(s); unable to stat " +
std::to_string(static_cast<unsigned long long>(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<unsigned long long>(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<std::string> 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::ResolutionCandidate>
XCUIAssetDocumentSource::CollectCandidatePaths(
const DocumentPathSpec& spec,
const fs::path& repositoryRoot) {
return CollectCandidatePaths(spec, repositoryRoot, GetConfiguredResourceRoot());
}
std::vector<XCUIAssetDocumentSource::ResolutionCandidate>
XCUIAssetDocumentSource::CollectCandidatePaths(
const DocumentPathSpec& spec,
const fs::path& repositoryRoot,
const fs::path& resourceRoot) {
std::vector<ResolutionCandidate> candidates = {};
std::unordered_set<std::string> 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<fs::path>& searchRoots,
bool includeDefaultSearchRoots) {
RepositoryDiscovery discovery = {};
discovery.probes.reserve(searchRoots.size() + 3u);
const std::vector<fs::path> 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<std::string> 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<unsigned long long>(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

View File

@@ -0,0 +1,148 @@
#pragma once
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
#include <filesystem>
#include <string>
#include <vector>
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<RepositoryProbe> 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<ResolutionCandidate> candidatePaths = {};
std::vector<std::string> 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<std::string> trackedSourcePaths = {};
std::vector<std::string> 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<ResolutionCandidate> CollectCandidatePaths(
const DocumentPathSpec& spec,
const std::filesystem::path& repositoryRoot);
static std::vector<ResolutionCandidate> 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<std::filesystem::path>& 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<TrackedWriteTime> m_trackedWriteTimes = {};
};
const char* ToString(XCUIAssetDocumentSource::PathOrigin origin);
const char* ToString(XCUIAssetDocumentSource::LoadBackend backend);
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
#pragma once
#include <XCEngine/UI/DrawData.h>
#include <memory>
#include <filesystem>
#include <string>
#include <vector>
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<UI::UIInputEvent> 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<RuntimeState> m_state;
};
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,60 @@
#include "XCUIBackend/XCUIEditorFontSetup.h"
#include <imgui.h>
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

View File

@@ -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

View File

@@ -0,0 +1,349 @@
#pragma once
#include "XCUIBackend/ImGuiTransitionBackend.h"
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Types.h>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct ImDrawList;
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
struct XCUIHostedPreviewFrame {
const ::XCEngine::UI::UIDrawData* drawData = nullptr;
ImDrawList* targetDrawList = nullptr;
::XCEngine::UI::UIRect canvasRect = {};
::XCEngine::UI::UISize logicalSize = {};
const char* debugName = nullptr;
const char* debugSource = nullptr;
};
struct XCUIHostedPreviewStats {
bool presented = false;
bool queuedToNativePass = false;
std::size_t submittedDrawListCount = 0;
std::size_t submittedCommandCount = 0;
std::size_t flushedDrawListCount = 0;
std::size_t flushedCommandCount = 0;
};
struct XCUIHostedPreviewQueuedFrame {
std::string debugName = {};
std::string debugSource = {};
::XCEngine::UI::UIRect canvasRect = {};
::XCEngine::UI::UISize logicalSize = {};
::XCEngine::UI::UIDrawData drawData = {};
};
struct XCUIHostedPreviewSurfaceImage {
ImTextureID textureId = {};
ImVec2 uvMin = ImVec2(0.0f, 0.0f);
ImVec2 uvMax = ImVec2(1.0f, 1.0f);
::XCEngine::UI::UIRect renderedCanvasRect = {};
std::uint32_t surfaceWidth = 0;
std::uint32_t surfaceHeight = 0;
bool IsValid() const {
return textureId != ImTextureID{} && surfaceWidth > 0u && surfaceHeight > 0u;
}
};
struct XCUIHostedPreviewDrainStats {
std::size_t queuedFrameCount = 0;
std::size_t queuedDrawListCount = 0;
std::size_t queuedCommandCount = 0;
std::size_t renderedFrameCount = 0;
std::size_t renderedDrawListCount = 0;
std::size_t renderedCommandCount = 0;
std::size_t skippedFrameCount = 0;
std::size_t skippedCommandCount = 0;
};
struct XCUIHostedPreviewSurfaceDescriptor {
std::string debugName = {};
std::string debugSource = {};
::XCEngine::UI::UIRect canvasRect = {};
::XCEngine::UI::UISize logicalSize = {};
std::size_t queuedFrameIndex = 0;
std::size_t submittedDrawListCount = 0;
std::size_t submittedCommandCount = 0;
bool queuedThisFrame = false;
XCUIHostedPreviewSurfaceImage image = {};
};
class XCUIHostedPreviewSurfaceRegistry {
public:
void BeginFrame() {
for (XCUIHostedPreviewSurfaceDescriptor& descriptor : m_descriptors) {
descriptor.queuedThisFrame = false;
}
}
void RecordQueuedFrame(
const XCUIHostedPreviewQueuedFrame& queuedFrame,
std::size_t queuedFrameIndex = 0u) {
if (queuedFrame.debugName.empty()) {
return;
}
XCUIHostedPreviewSurfaceDescriptor* descriptor = FindMutableDescriptor(queuedFrame.debugName);
if (descriptor == nullptr) {
XCUIHostedPreviewSurfaceDescriptor newDescriptor = {};
newDescriptor.debugName = queuedFrame.debugName;
m_descriptors.push_back(std::move(newDescriptor));
descriptor = &m_descriptors.back();
}
descriptor->debugSource = queuedFrame.debugSource;
descriptor->canvasRect = queuedFrame.canvasRect;
descriptor->logicalSize = queuedFrame.logicalSize;
descriptor->queuedFrameIndex = queuedFrameIndex;
descriptor->submittedDrawListCount = queuedFrame.drawData.GetDrawListCount();
descriptor->submittedCommandCount = queuedFrame.drawData.GetTotalCommandCount();
descriptor->queuedThisFrame = true;
}
void UpdateSurface(
const std::string& debugName,
ImTextureID textureId,
std::uint32_t surfaceWidth,
std::uint32_t surfaceHeight,
const ::XCEngine::UI::UIRect& renderedCanvasRect) {
if (debugName.empty() || textureId == ImTextureID{} || surfaceWidth == 0u || surfaceHeight == 0u) {
return;
}
XCUIHostedPreviewSurfaceDescriptor* descriptor = FindMutableDescriptor(debugName);
if (descriptor == nullptr) {
XCUIHostedPreviewSurfaceDescriptor newDescriptor = {};
newDescriptor.debugName = debugName;
m_descriptors.push_back(std::move(newDescriptor));
descriptor = &m_descriptors.back();
}
descriptor->image.textureId = textureId;
descriptor->image.surfaceWidth = surfaceWidth;
descriptor->image.surfaceHeight = surfaceHeight;
descriptor->image.renderedCanvasRect = renderedCanvasRect;
descriptor->image.uvMin = ImVec2(
renderedCanvasRect.x / static_cast<float>(surfaceWidth),
renderedCanvasRect.y / static_cast<float>(surfaceHeight));
descriptor->image.uvMax = ImVec2(
(renderedCanvasRect.x + renderedCanvasRect.width) / static_cast<float>(surfaceWidth),
(renderedCanvasRect.y + renderedCanvasRect.height) / static_cast<float>(surfaceHeight));
}
bool TryGetSurfaceDescriptor(
const char* debugName,
XCUIHostedPreviewSurfaceDescriptor& outDescriptor) const {
outDescriptor = {};
if (debugName == nullptr || debugName[0] == '\0') {
return false;
}
for (const XCUIHostedPreviewSurfaceDescriptor& descriptor : m_descriptors) {
if (descriptor.debugName == debugName) {
outDescriptor = descriptor;
return true;
}
}
return false;
}
bool TryGetSurfaceImage(const char* debugName, XCUIHostedPreviewSurfaceImage& outImage) const {
outImage = {};
XCUIHostedPreviewSurfaceDescriptor descriptor = {};
if (!TryGetSurfaceDescriptor(debugName, descriptor)) {
return false;
}
outImage = descriptor.image;
return outImage.IsValid();
}
const std::vector<XCUIHostedPreviewSurfaceDescriptor>& GetDescriptors() const {
return m_descriptors;
}
private:
XCUIHostedPreviewSurfaceDescriptor* FindMutableDescriptor(const std::string& debugName) {
for (XCUIHostedPreviewSurfaceDescriptor& descriptor : m_descriptors) {
if (descriptor.debugName == debugName) {
return &descriptor;
}
}
return nullptr;
}
std::vector<XCUIHostedPreviewSurfaceDescriptor> m_descriptors = {};
};
class XCUIHostedPreviewQueue {
public:
void BeginFrame() {
m_queuedFrames.clear();
}
bool Submit(const XCUIHostedPreviewFrame& frame, XCUIHostedPreviewStats* outStats = nullptr) {
XCUIHostedPreviewStats stats = {};
if (frame.drawData == nullptr) {
if (outStats != nullptr) {
*outStats = stats;
}
return false;
}
XCUIHostedPreviewQueuedFrame queuedFrame = {};
if (frame.debugName != nullptr) {
queuedFrame.debugName = frame.debugName;
}
if (frame.debugSource != nullptr) {
queuedFrame.debugSource = frame.debugSource;
}
queuedFrame.canvasRect = frame.canvasRect;
queuedFrame.logicalSize = frame.logicalSize.width > 0.0f && frame.logicalSize.height > 0.0f
? frame.logicalSize
: ::XCEngine::UI::UISize(frame.canvasRect.width, frame.canvasRect.height);
queuedFrame.drawData = *frame.drawData;
stats.presented = true;
stats.queuedToNativePass = true;
stats.submittedDrawListCount = queuedFrame.drawData.GetDrawListCount();
stats.submittedCommandCount = queuedFrame.drawData.GetTotalCommandCount();
m_queuedFrames.push_back(std::move(queuedFrame));
if (outStats != nullptr) {
*outStats = stats;
}
return true;
}
const std::vector<XCUIHostedPreviewQueuedFrame>& GetQueuedFrames() const {
return m_queuedFrames;
}
void SetLastDrainStats(const XCUIHostedPreviewDrainStats& stats) {
m_lastDrainStats = stats;
}
const XCUIHostedPreviewDrainStats& GetLastDrainStats() const {
return m_lastDrainStats;
}
private:
std::vector<XCUIHostedPreviewQueuedFrame> m_queuedFrames = {};
XCUIHostedPreviewDrainStats m_lastDrainStats = {};
};
class IXCUIHostedPreviewPresenter {
public:
virtual ~IXCUIHostedPreviewPresenter() = default;
virtual bool Present(const XCUIHostedPreviewFrame& frame) = 0;
virtual const XCUIHostedPreviewStats& GetLastStats() const = 0;
virtual bool IsNativeQueued() const { return false; }
virtual bool TryGetSurfaceImage(
const char* debugName,
XCUIHostedPreviewSurfaceImage& outImage) const {
outImage = {};
return false;
}
virtual bool TryGetSurfaceDescriptor(
const char* debugName,
XCUIHostedPreviewSurfaceDescriptor& outDescriptor) const {
outDescriptor = {};
return false;
}
};
class ImGuiXCUIHostedPreviewPresenter final : public IXCUIHostedPreviewPresenter {
public:
bool Present(const XCUIHostedPreviewFrame& frame) override {
m_lastStats = {};
if (frame.drawData == nullptr) {
return false;
}
m_backend.BeginFrame();
m_backend.Submit(*frame.drawData);
m_lastStats.submittedDrawListCount = m_backend.GetPendingDrawListCount();
m_lastStats.submittedCommandCount = m_backend.GetPendingCommandCount();
m_lastStats.presented = m_backend.EndFrame(frame.targetDrawList);
m_lastStats.flushedDrawListCount = m_backend.GetLastFlushedDrawListCount();
m_lastStats.flushedCommandCount = m_backend.GetLastFlushedCommandCount();
return m_lastStats.presented;
}
const XCUIHostedPreviewStats& GetLastStats() const override {
return m_lastStats;
}
private:
ImGuiTransitionBackend m_backend = {};
XCUIHostedPreviewStats m_lastStats = {};
};
class QueuedNativeXCUIHostedPreviewPresenter final : public IXCUIHostedPreviewPresenter {
public:
QueuedNativeXCUIHostedPreviewPresenter(
XCUIHostedPreviewQueue& queue,
XCUIHostedPreviewSurfaceRegistry& surfaceRegistry)
: m_queue(queue)
, m_surfaceRegistry(surfaceRegistry) {
}
bool Present(const XCUIHostedPreviewFrame& frame) override {
m_lastStats = {};
return m_queue.Submit(frame, &m_lastStats);
}
const XCUIHostedPreviewStats& GetLastStats() const override {
return m_lastStats;
}
bool IsNativeQueued() const override {
return true;
}
bool TryGetSurfaceImage(
const char* debugName,
XCUIHostedPreviewSurfaceImage& outImage) const override {
return m_surfaceRegistry.TryGetSurfaceImage(debugName, outImage);
}
bool TryGetSurfaceDescriptor(
const char* debugName,
XCUIHostedPreviewSurfaceDescriptor& outDescriptor) const override {
return m_surfaceRegistry.TryGetSurfaceDescriptor(debugName, outDescriptor);
}
private:
XCUIHostedPreviewQueue& m_queue;
XCUIHostedPreviewSurfaceRegistry& m_surfaceRegistry;
XCUIHostedPreviewStats m_lastStats = {};
};
inline std::unique_ptr<IXCUIHostedPreviewPresenter> CreateImGuiXCUIHostedPreviewPresenter() {
return std::make_unique<ImGuiXCUIHostedPreviewPresenter>();
}
inline std::unique_ptr<IXCUIHostedPreviewPresenter> CreateQueuedNativeXCUIHostedPreviewPresenter(
XCUIHostedPreviewQueue& queue,
XCUIHostedPreviewSurfaceRegistry& surfaceRegistry) {
return std::make_unique<QueuedNativeXCUIHostedPreviewPresenter>(queue, surfaceRegistry);
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,596 @@
#include "XCUIBackend/XCUIInputBridge.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <limits>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIPoint;
UIPoint Subtract(const UIPoint& lhs, const UIPoint& rhs) {
return UIPoint(lhs.x - rhs.x, lhs.y - rhs.y);
}
UIPointerButton ToPointerButton(std::size_t index) {
switch (index) {
case 0u:
return UIPointerButton::Left;
case 1u:
return UIPointerButton::Right;
case 2u:
return UIPointerButton::Middle;
case 3u:
return UIPointerButton::X1;
case 4u:
return UIPointerButton::X2;
default:
return UIPointerButton::None;
}
}
void AppendEvent(std::vector<UIInputEvent>& events, const UIInputEvent& event) {
events.push_back(event);
}
UIInputEvent MakeBaseEvent(
UIInputEventType type,
const XCUIInputBridgeFrameSnapshot& snapshot) {
UIInputEvent event = {};
event.type = type;
event.position = snapshot.pointerPosition;
event.timestampNanoseconds = snapshot.timestampNanoseconds;
event.modifiers = snapshot.modifiers;
return event;
}
void AppendUniqueKeyCode(std::vector<std::int32_t>& keyCodes, std::int32_t keyCode) {
if (keyCode == 0) {
return;
}
const auto it = std::find(keyCodes.begin(), keyCodes.end(), keyCode);
if (it == keyCodes.end()) {
keyCodes.push_back(keyCode);
}
}
bool IsPointerPositionValid(const ImVec2& position) {
return std::isfinite(position.x) &&
std::isfinite(position.y) &&
position.x > -std::numeric_limits<float>::max() * 0.5f &&
position.y > -std::numeric_limits<float>::max() * 0.5f;
}
std::uint64_t GetTimestampNanoseconds() {
return static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count());
}
UIPoint MakeClientPoint(LPARAM lParam) {
return UIPoint(
static_cast<float>(static_cast<short>(LOWORD(static_cast<DWORD_PTR>(lParam)))),
static_cast<float>(static_cast<short>(HIWORD(static_cast<DWORD_PTR>(lParam)))));
}
std::int32_t TranslateVirtualKeyToXCUIKeyCode(WPARAM wParam, LPARAM lParam) {
switch (static_cast<UINT>(wParam)) {
case 'A': return static_cast<std::int32_t>(KeyCode::A);
case 'B': return static_cast<std::int32_t>(KeyCode::B);
case 'C': return static_cast<std::int32_t>(KeyCode::C);
case 'D': return static_cast<std::int32_t>(KeyCode::D);
case 'E': return static_cast<std::int32_t>(KeyCode::E);
case 'F': return static_cast<std::int32_t>(KeyCode::F);
case 'G': return static_cast<std::int32_t>(KeyCode::G);
case 'H': return static_cast<std::int32_t>(KeyCode::H);
case 'I': return static_cast<std::int32_t>(KeyCode::I);
case 'J': return static_cast<std::int32_t>(KeyCode::J);
case 'K': return static_cast<std::int32_t>(KeyCode::K);
case 'L': return static_cast<std::int32_t>(KeyCode::L);
case 'M': return static_cast<std::int32_t>(KeyCode::M);
case 'N': return static_cast<std::int32_t>(KeyCode::N);
case 'O': return static_cast<std::int32_t>(KeyCode::O);
case 'P': return static_cast<std::int32_t>(KeyCode::P);
case 'Q': return static_cast<std::int32_t>(KeyCode::Q);
case 'R': return static_cast<std::int32_t>(KeyCode::R);
case 'S': return static_cast<std::int32_t>(KeyCode::S);
case 'T': return static_cast<std::int32_t>(KeyCode::T);
case 'U': return static_cast<std::int32_t>(KeyCode::U);
case 'V': return static_cast<std::int32_t>(KeyCode::V);
case 'W': return static_cast<std::int32_t>(KeyCode::W);
case 'X': return static_cast<std::int32_t>(KeyCode::X);
case 'Y': return static_cast<std::int32_t>(KeyCode::Y);
case 'Z': return static_cast<std::int32_t>(KeyCode::Z);
case '0': return static_cast<std::int32_t>(KeyCode::Zero);
case '1': return static_cast<std::int32_t>(KeyCode::One);
case '2': return static_cast<std::int32_t>(KeyCode::Two);
case '3': return static_cast<std::int32_t>(KeyCode::Three);
case '4': return static_cast<std::int32_t>(KeyCode::Four);
case '5': return static_cast<std::int32_t>(KeyCode::Five);
case '6': return static_cast<std::int32_t>(KeyCode::Six);
case '7': return static_cast<std::int32_t>(KeyCode::Seven);
case '8': return static_cast<std::int32_t>(KeyCode::Eight);
case '9': return static_cast<std::int32_t>(KeyCode::Nine);
case VK_SPACE: return static_cast<std::int32_t>(KeyCode::Space);
case VK_TAB: return static_cast<std::int32_t>(KeyCode::Tab);
case VK_RETURN: return static_cast<std::int32_t>(KeyCode::Enter);
case VK_ESCAPE: return static_cast<std::int32_t>(KeyCode::Escape);
case VK_SHIFT: {
const UINT scanCode = (static_cast<UINT>(lParam) >> 16) & 0xFFu;
const UINT leftShiftScanCode = MapVirtualKeyW(VK_LSHIFT, MAPVK_VK_TO_VSC);
return static_cast<std::int32_t>(
scanCode == leftShiftScanCode ? KeyCode::LeftShift : KeyCode::RightShift);
}
case VK_CONTROL:
return static_cast<std::int32_t>(
(static_cast<UINT>(lParam) & 0x01000000u) != 0u ? KeyCode::RightCtrl : KeyCode::LeftCtrl);
case VK_MENU:
return static_cast<std::int32_t>(
(static_cast<UINT>(lParam) & 0x01000000u) != 0u ? KeyCode::RightAlt : KeyCode::LeftAlt);
case VK_UP: return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN: return static_cast<std::int32_t>(KeyCode::Down);
case VK_LEFT: return static_cast<std::int32_t>(KeyCode::Left);
case VK_RIGHT: return static_cast<std::int32_t>(KeyCode::Right);
case VK_HOME: return static_cast<std::int32_t>(KeyCode::Home);
case VK_END: return static_cast<std::int32_t>(KeyCode::End);
case VK_PRIOR: return static_cast<std::int32_t>(KeyCode::PageUp);
case VK_NEXT: return static_cast<std::int32_t>(KeyCode::PageDown);
case VK_DELETE: return static_cast<std::int32_t>(KeyCode::Delete);
case VK_BACK: return static_cast<std::int32_t>(KeyCode::Backspace);
case VK_F1: return static_cast<std::int32_t>(KeyCode::F1);
case VK_F2: return static_cast<std::int32_t>(KeyCode::F2);
case VK_F3: return static_cast<std::int32_t>(KeyCode::F3);
case VK_F4: return static_cast<std::int32_t>(KeyCode::F4);
case VK_F5: return static_cast<std::int32_t>(KeyCode::F5);
case VK_F6: return static_cast<std::int32_t>(KeyCode::F6);
case VK_F7: return static_cast<std::int32_t>(KeyCode::F7);
case VK_F8: return static_cast<std::int32_t>(KeyCode::F8);
case VK_F9: return static_cast<std::int32_t>(KeyCode::F9);
case VK_F10: return static_cast<std::int32_t>(KeyCode::F10);
case VK_F11: return static_cast<std::int32_t>(KeyCode::F11);
case VK_F12: return static_cast<std::int32_t>(KeyCode::F12);
case VK_OEM_MINUS: return static_cast<std::int32_t>(KeyCode::Minus);
case VK_OEM_PLUS: return static_cast<std::int32_t>(KeyCode::Equals);
case VK_OEM_4: return static_cast<std::int32_t>(KeyCode::BracketLeft);
case VK_OEM_6: return static_cast<std::int32_t>(KeyCode::BracketRight);
case VK_OEM_1: return static_cast<std::int32_t>(KeyCode::Semicolon);
case VK_OEM_7: return static_cast<std::int32_t>(KeyCode::Quote);
case VK_OEM_COMMA: return static_cast<std::int32_t>(KeyCode::Comma);
case VK_OEM_PERIOD: return static_cast<std::int32_t>(KeyCode::Period);
case VK_OEM_2: return static_cast<std::int32_t>(KeyCode::Slash);
case VK_OEM_5: return static_cast<std::int32_t>(KeyCode::Backslash);
case VK_OEM_3: return static_cast<std::int32_t>(KeyCode::Backtick);
default:
return static_cast<std::int32_t>(KeyCode::None);
}
}
} // namespace
const XCUIInputBridgeKeyState* XCUIInputBridgeFrameSnapshot::FindKeyState(std::int32_t keyCode) const {
for (const XCUIInputBridgeKeyState& keyState : keys) {
if (keyState.keyCode == keyCode) {
return &keyState;
}
}
return nullptr;
}
bool XCUIInputBridgeFrameSnapshot::IsKeyDown(std::int32_t keyCode) const {
const XCUIInputBridgeKeyState* keyState = FindKeyState(keyCode);
return keyState != nullptr && keyState->down;
}
bool XCUIInputBridgeFrameDelta::HasEvents() const {
return !events.empty();
}
bool XCUIInputBridgeFrameDelta::HasPointerActivity() const {
return pointer.moved ||
pointer.entered ||
pointer.left ||
pointer.wheelDelta.x != 0.0f ||
pointer.wheelDelta.y != 0.0f ||
std::any_of(pointer.pressed.begin(), pointer.pressed.end(), [](bool value) { return value; }) ||
std::any_of(pointer.released.begin(), pointer.released.end(), [](bool value) { return value; });
}
bool XCUIInputBridgeFrameDelta::HasKeyboardActivity() const {
return !keyboard.pressedKeys.empty() ||
!keyboard.releasedKeys.empty() ||
!keyboard.repeatedKeys.empty() ||
!keyboard.characters.empty();
}
bool XCUIInputBridgeFrameDelta::HasEventType(UI::UIInputEventType type) const {
return std::any_of(
events.begin(),
events.end(),
[type](const UIInputEvent& event) {
return event.type == type;
});
}
void XCUIInputBridge::Reset() {
m_hasBaseline = false;
m_baseline = {};
}
void XCUIInputBridge::Prime(const XCUIInputBridgeFrameSnapshot& snapshot) {
m_baseline = snapshot;
m_hasBaseline = true;
}
XCUIInputBridgeFrameDelta XCUIInputBridge::Translate(const XCUIInputBridgeFrameSnapshot& current) {
const XCUIInputBridgeFrameSnapshot previous = m_hasBaseline ? m_baseline : XCUIInputBridgeFrameSnapshot();
XCUIInputBridgeFrameDelta delta = Translate(previous, current);
m_baseline = current;
m_hasBaseline = true;
return delta;
}
XCUIInputBridgeFrameDelta XCUIInputBridge::Translate(
const XCUIInputBridgeFrameSnapshot& previous,
const XCUIInputBridgeFrameSnapshot& current) {
XCUIInputBridgeFrameDelta delta = {};
delta.state = current;
delta.pointer.wheelDelta = current.wheelDelta;
delta.keyboard.characters = current.characters;
delta.focusGained = current.windowFocused && !previous.windowFocused;
delta.focusLost = previous.windowFocused && !current.windowFocused;
if (delta.focusGained) {
AppendEvent(delta.events, MakeBaseEvent(UIInputEventType::FocusGained, current));
}
if (delta.focusLost) {
AppendEvent(delta.events, MakeBaseEvent(UIInputEventType::FocusLost, current));
}
delta.pointer.entered = current.pointerInside && !previous.pointerInside;
delta.pointer.left = previous.pointerInside && !current.pointerInside;
if (previous.pointerInside && current.pointerInside) {
delta.pointer.delta = Subtract(current.pointerPosition, previous.pointerPosition);
}
const bool pointerMovedInside =
current.pointerInside &&
(delta.pointer.entered ||
previous.pointerPosition.x != current.pointerPosition.x ||
previous.pointerPosition.y != current.pointerPosition.y);
delta.pointer.moved = pointerMovedInside;
if (delta.pointer.entered) {
AppendEvent(delta.events, MakeBaseEvent(UIInputEventType::PointerEnter, current));
}
if (delta.pointer.moved) {
UIInputEvent event = MakeBaseEvent(UIInputEventType::PointerMove, current);
event.delta = delta.pointer.delta;
AppendEvent(delta.events, event);
}
for (std::size_t index = 0; index < XCUIInputBridgeFrameSnapshot::PointerButtonCount; ++index) {
const bool wasDown = previous.pointerButtonsDown[index];
const bool isDown = current.pointerButtonsDown[index];
if (isDown == wasDown) {
continue;
}
UIInputEvent event = MakeBaseEvent(
isDown ? UIInputEventType::PointerButtonDown : UIInputEventType::PointerButtonUp,
current);
event.pointerButton = ToPointerButton(index);
if (isDown) {
delta.pointer.pressed[index] = true;
} else {
delta.pointer.released[index] = true;
}
AppendEvent(delta.events, event);
}
if (current.wheelDelta.x != 0.0f || current.wheelDelta.y != 0.0f) {
UIInputEvent event = MakeBaseEvent(UIInputEventType::PointerWheel, current);
event.delta = current.wheelDelta;
event.wheelDelta = current.wheelDelta.y != 0.0f ? current.wheelDelta.y : current.wheelDelta.x;
AppendEvent(delta.events, event);
}
if (delta.pointer.left) {
UIInputEvent event = MakeBaseEvent(UIInputEventType::PointerLeave, current);
if (previous.pointerInside) {
event.delta = Subtract(current.pointerPosition, previous.pointerPosition);
}
AppendEvent(delta.events, event);
}
std::vector<std::int32_t> keyCodes = {};
keyCodes.reserve(previous.keys.size() + current.keys.size());
for (const XCUIInputBridgeKeyState& keyState : previous.keys) {
AppendUniqueKeyCode(keyCodes, keyState.keyCode);
}
for (const XCUIInputBridgeKeyState& keyState : current.keys) {
AppendUniqueKeyCode(keyCodes, keyState.keyCode);
}
std::sort(keyCodes.begin(), keyCodes.end());
for (std::int32_t keyCode : keyCodes) {
const XCUIInputBridgeKeyState* previousKeyState = previous.FindKeyState(keyCode);
const XCUIInputBridgeKeyState* currentKeyState = current.FindKeyState(keyCode);
const bool wasDown = previousKeyState != nullptr && previousKeyState->down;
const bool isDown = currentKeyState != nullptr && currentKeyState->down;
if (isDown && !wasDown) {
UIInputEvent event = MakeBaseEvent(UIInputEventType::KeyDown, current);
event.keyCode = keyCode;
delta.keyboard.pressedKeys.push_back(keyCode);
AppendEvent(delta.events, event);
continue;
}
if (!isDown && wasDown) {
UIInputEvent event = MakeBaseEvent(UIInputEventType::KeyUp, current);
event.keyCode = keyCode;
delta.keyboard.releasedKeys.push_back(keyCode);
AppendEvent(delta.events, event);
continue;
}
if (isDown && wasDown && currentKeyState != nullptr && currentKeyState->repeat) {
UIInputEvent event = MakeBaseEvent(UIInputEventType::KeyDown, current);
event.keyCode = keyCode;
event.repeat = true;
delta.keyboard.repeatedKeys.push_back(keyCode);
AppendEvent(delta.events, event);
}
}
for (std::uint32_t character : current.characters) {
if (character == 0u) {
continue;
}
UIInputEvent event = MakeBaseEvent(UIInputEventType::Character, current);
event.character = character;
AppendEvent(delta.events, event);
}
return delta;
}
void XCUIWin32InputSource::Reset() {
m_pointerPosition = {};
m_pointerInside = false;
m_windowFocused = false;
m_trackingMouseLeave = false;
m_pointerButtonsDown.fill(false);
m_wheelDelta = {};
m_modifiers = {};
m_keyStates.clear();
m_characters.clear();
}
void XCUIWin32InputSource::HandleWindowMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_SETFOCUS:
m_windowFocused = true;
UpdateModifierState();
return;
case WM_KILLFOCUS:
m_windowFocused = false;
m_pointerInside = false;
m_trackingMouseLeave = false;
m_pointerButtonsDown.fill(false);
m_keyStates.clear();
UpdateModifierState();
return;
case WM_MOUSEMOVE: {
m_pointerPosition = MakeClientPoint(lParam);
UpdatePointerInside(hwnd, m_pointerPosition.x, m_pointerPosition.y, true);
UpdateModifierState();
if (hwnd != nullptr && !m_trackingMouseLeave) {
TRACKMOUSEEVENT trackMouseEvent = {};
trackMouseEvent.cbSize = sizeof(trackMouseEvent);
trackMouseEvent.dwFlags = TME_LEAVE;
trackMouseEvent.hwndTrack = hwnd;
m_trackingMouseLeave = TrackMouseEvent(&trackMouseEvent) == TRUE;
}
return;
}
case WM_MOUSELEAVE:
m_trackingMouseLeave = false;
m_pointerInside = false;
return;
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
case WM_XBUTTONDOWN:
case WM_XBUTTONUP: {
m_pointerPosition = MakeClientPoint(lParam);
UpdatePointerInside(hwnd, m_pointerPosition.x, m_pointerPosition.y, true);
UpdateModifierState();
switch (message) {
case WM_LBUTTONDOWN: SetPointerButtonDown(0u, true); return;
case WM_LBUTTONUP: SetPointerButtonDown(0u, false); return;
case WM_RBUTTONDOWN: SetPointerButtonDown(1u, true); return;
case WM_RBUTTONUP: SetPointerButtonDown(1u, false); return;
case WM_MBUTTONDOWN: SetPointerButtonDown(2u, true); return;
case WM_MBUTTONUP: SetPointerButtonDown(2u, false); return;
case WM_XBUTTONDOWN:
SetPointerButtonDown(HIWORD(wParam) == XBUTTON1 ? 3u : 4u, true);
return;
case WM_XBUTTONUP:
SetPointerButtonDown(HIWORD(wParam) == XBUTTON1 ? 3u : 4u, false);
return;
default:
return;
}
}
case WM_MOUSEWHEEL:
case WM_MOUSEHWHEEL: {
POINT screenPoint = {
static_cast<LONG>(static_cast<short>(LOWORD(static_cast<DWORD_PTR>(lParam)))),
static_cast<LONG>(static_cast<short>(HIWORD(static_cast<DWORD_PTR>(lParam))))
};
if (hwnd != nullptr && ScreenToClient(hwnd, &screenPoint)) {
m_pointerPosition = UIPoint(static_cast<float>(screenPoint.x), static_cast<float>(screenPoint.y));
UpdatePointerInside(hwnd, m_pointerPosition.x, m_pointerPosition.y, false);
}
const float wheelStep = static_cast<float>(GET_WHEEL_DELTA_WPARAM(wParam)) / static_cast<float>(WHEEL_DELTA);
if (message == WM_MOUSEHWHEEL) {
m_wheelDelta.x += wheelStep;
} else {
m_wheelDelta.y += wheelStep;
}
UpdateModifierState();
return;
}
case WM_KEYDOWN:
case WM_SYSKEYDOWN: {
UpdateModifierState();
const std::int32_t keyCode = TranslateVirtualKeyToXCUIKeyCode(wParam, lParam);
const bool repeat = (static_cast<UINT>(lParam) & 0x40000000u) != 0u;
SetKeyDown(keyCode, true, repeat);
UpdateModifierState();
return;
}
case WM_KEYUP:
case WM_SYSKEYUP:
SetKeyDown(TranslateVirtualKeyToXCUIKeyCode(wParam, lParam), false, false);
UpdateModifierState();
return;
case WM_CHAR:
case WM_SYSCHAR:
if (wParam != 0u) {
m_characters.push_back(static_cast<std::uint32_t>(wParam));
}
return;
default:
return;
}
}
void XCUIWin32InputSource::ClearFrameTransients() {
m_wheelDelta = {};
m_characters.clear();
for (XCUIInputBridgeKeyState& keyState : m_keyStates) {
keyState.repeat = false;
}
}
XCUIInputBridgeFrameSnapshot XCUIWin32InputSource::CaptureSnapshot(
const XCUIInputBridgeCaptureOptions& options) const {
XCUIInputBridgeFrameSnapshot snapshot = {};
snapshot.pointerPosition = UIPoint(
m_pointerPosition.x - options.pointerOffset.x,
m_pointerPosition.y - options.pointerOffset.y);
snapshot.pointerInside = options.hasPointerInsideOverride ? options.pointerInsideOverride : m_pointerInside;
snapshot.pointerButtonsDown = m_pointerButtonsDown;
snapshot.wheelDelta = m_wheelDelta;
snapshot.modifiers = m_modifiers;
snapshot.windowFocused = options.windowFocused && m_windowFocused;
snapshot.wantCaptureMouse = false;
snapshot.wantCaptureKeyboard = false;
snapshot.wantTextInput = false;
snapshot.timestampNanoseconds =
options.timestampNanoseconds != 0u ? options.timestampNanoseconds : GetTimestampNanoseconds();
snapshot.keys = m_keyStates;
snapshot.characters = m_characters;
return snapshot;
}
void XCUIWin32InputSource::UpdateModifierState() {
m_modifiers.shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
m_modifiers.control = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
m_modifiers.alt = (GetKeyState(VK_MENU) & 0x8000) != 0;
m_modifiers.super =
(GetKeyState(VK_LWIN) & 0x8000) != 0 ||
(GetKeyState(VK_RWIN) & 0x8000) != 0;
}
void XCUIWin32InputSource::UpdatePointerInside(
HWND hwnd,
float x,
float y,
bool assumeInsideIfUnknown) {
if (hwnd == nullptr) {
m_pointerInside = assumeInsideIfUnknown;
return;
}
RECT clientRect = {};
if (!GetClientRect(hwnd, &clientRect)) {
m_pointerInside = assumeInsideIfUnknown;
return;
}
m_pointerInside =
x >= static_cast<float>(clientRect.left) &&
y >= static_cast<float>(clientRect.top) &&
x < static_cast<float>(clientRect.right) &&
y < static_cast<float>(clientRect.bottom);
}
void XCUIWin32InputSource::SetPointerButtonDown(std::size_t index, bool down) {
if (index < m_pointerButtonsDown.size()) {
m_pointerButtonsDown[index] = down;
}
}
void XCUIWin32InputSource::SetKeyDown(std::int32_t keyCode, bool down, bool repeat) {
if (keyCode == 0) {
return;
}
const auto it = std::find_if(
m_keyStates.begin(),
m_keyStates.end(),
[keyCode](const XCUIInputBridgeKeyState& state) {
return state.keyCode == keyCode;
});
if (!down) {
if (it != m_keyStates.end()) {
m_keyStates.erase(it);
}
return;
}
if (it != m_keyStates.end()) {
it->down = true;
it->repeat = it->repeat || repeat;
return;
}
XCUIInputBridgeKeyState state = {};
state.keyCode = keyCode;
state.down = true;
state.repeat = repeat;
m_keyStates.push_back(state);
std::sort(
m_keyStates.begin(),
m_keyStates.end(),
[](const XCUIInputBridgeKeyState& lhs, const XCUIInputBridgeKeyState& rhs) {
return lhs.keyCode < rhs.keyCode;
});
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,152 @@
#pragma once
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/Types.h>
#include <imgui.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <vector>
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
struct XCUIInputBridgeKeyState {
std::int32_t keyCode = 0;
bool down = false;
bool repeat = false;
};
struct XCUIInputBridgeFrameSnapshot {
static constexpr std::size_t PointerButtonCount = 5u;
UI::UIPoint pointerPosition = {};
bool pointerInside = false;
std::array<bool, PointerButtonCount> pointerButtonsDown = {};
UI::UIPoint wheelDelta = {};
UI::UIInputModifiers modifiers = {};
bool windowFocused = false;
bool wantCaptureMouse = false;
bool wantCaptureKeyboard = false;
bool wantTextInput = false;
std::uint64_t timestampNanoseconds = 0;
std::vector<XCUIInputBridgeKeyState> keys = {};
std::vector<std::uint32_t> characters = {};
const XCUIInputBridgeKeyState* FindKeyState(std::int32_t keyCode) const;
bool IsKeyDown(std::int32_t keyCode) const;
};
struct XCUIInputBridgeCaptureOptions {
UI::UIPoint pointerOffset = {};
bool hasPointerInsideOverride = false;
bool pointerInsideOverride = false;
bool windowFocused = true;
std::uint64_t timestampNanoseconds = 0;
};
struct XCUIInputBridgePointerDelta {
UI::UIPoint delta = {};
UI::UIPoint wheelDelta = {};
std::array<bool, XCUIInputBridgeFrameSnapshot::PointerButtonCount> pressed = {};
std::array<bool, XCUIInputBridgeFrameSnapshot::PointerButtonCount> released = {};
bool moved = false;
bool entered = false;
bool left = false;
};
struct XCUIInputBridgeKeyboardDelta {
std::vector<std::int32_t> pressedKeys = {};
std::vector<std::int32_t> releasedKeys = {};
std::vector<std::int32_t> repeatedKeys = {};
std::vector<std::uint32_t> characters = {};
};
struct XCUIInputBridgeFrameDelta {
XCUIInputBridgeFrameSnapshot state = {};
XCUIInputBridgePointerDelta pointer = {};
XCUIInputBridgeKeyboardDelta keyboard = {};
std::vector<UI::UIInputEvent> events = {};
bool focusGained = false;
bool focusLost = false;
bool HasEvents() const;
bool HasPointerActivity() const;
bool HasKeyboardActivity() const;
bool HasEventType(UI::UIInputEventType type) const;
};
class XCUIInputBridge {
public:
void Reset();
void Prime(const XCUIInputBridgeFrameSnapshot& snapshot);
bool HasBaseline() const {
return m_hasBaseline;
}
const XCUIInputBridgeFrameSnapshot& GetBaseline() const {
return m_baseline;
}
XCUIInputBridgeFrameDelta Translate(const XCUIInputBridgeFrameSnapshot& current);
static XCUIInputBridgeFrameDelta Translate(
const XCUIInputBridgeFrameSnapshot& previous,
const XCUIInputBridgeFrameSnapshot& current);
private:
bool m_hasBaseline = false;
XCUIInputBridgeFrameSnapshot m_baseline = {};
};
class XCUIWin32InputSource {
public:
void Reset();
void HandleWindowMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
void ClearFrameTransients();
XCUIInputBridgeFrameSnapshot CaptureSnapshot(
const XCUIInputBridgeCaptureOptions& options = XCUIInputBridgeCaptureOptions()) const;
const UI::UIPoint& GetPointerPosition() const {
return m_pointerPosition;
}
bool IsPointerInsideWindow() const {
return m_pointerInside;
}
bool IsWindowFocused() const {
return m_windowFocused;
}
private:
void UpdateModifierState();
void UpdatePointerInside(HWND hwnd, float x, float y, bool assumeInsideIfUnknown);
void SetPointerButtonDown(std::size_t index, bool down);
void SetKeyDown(std::int32_t keyCode, bool down, bool repeat);
UI::UIPoint m_pointerPosition = {};
bool m_pointerInside = false;
bool m_windowFocused = false;
bool m_trackingMouseLeave = false;
std::array<bool, XCUIInputBridgeFrameSnapshot::PointerButtonCount> m_pointerButtonsDown = {};
UI::UIPoint m_wheelDelta = {};
UI::UIInputModifiers m_modifiers = {};
std::vector<XCUIInputBridgeKeyState> m_keyStates = {};
std::vector<std::uint32_t> m_characters = {};
};
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,916 @@
#include "XCUILayoutLabRuntime.h"
#include "XCUIAssetDocumentSource.h"
#include "XCUIRHICommandSupport.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
#include <XCEngine/UI/Style/Theme.h>
#include <XCEngine/UI/Types.h>
#include <algorithm>
#include <cstdlib>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
namespace fs = std::filesystem;
namespace {
using XCEngine::Containers::String;
using XCEngine::Math::Color;
using XCEngine::Resources::CompileUIDocument;
using XCEngine::Resources::UIDocumentAttribute;
using XCEngine::Resources::UIDocumentCompileRequest;
using XCEngine::Resources::UIDocumentCompileResult;
using XCEngine::Resources::UIDocumentKind;
using XCEngine::Resources::UIDocumentModel;
using XCEngine::Resources::UIDocumentNode;
using XCEngine::Resources::ResourceManager;
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
namespace Style = XCEngine::UI::Style;
constexpr std::size_t kInvalidIndex = static_cast<std::size_t>(-1);
constexpr char kViewRelativePath[] = "new_editor/resources/xcui_layout_lab_view.xcui";
constexpr char kThemeRelativePath[] = "new_editor/resources/xcui_layout_lab_theme.xctheme";
struct LayoutNode {
std::string id = {};
std::string tagName = {};
std::string title = {};
std::string subtitle = {};
std::string tone = {};
std::string widthAttr = {};
std::string heightAttr = {};
std::string xAttr = {};
std::string yAttr = {};
std::string gapAttr = {};
std::string paddingAttr = {};
std::string scrollYAttr = {};
std::size_t parentIndex = kInvalidIndex;
std::vector<std::size_t> children = {};
UIRect rect = {};
int depth = 0;
};
struct RuntimeBuildContext {
UIDocumentCompileResult viewDocument = {};
UIDocumentCompileResult themeDocument = {};
Style::UITheme theme = {};
XCUILayoutLabFrameResult frameResult = {};
std::vector<LayoutNode> nodes = {};
std::unordered_map<std::string, std::size_t> nodeIndexById = {};
std::unordered_map<std::string, UIRect> rectsById = {};
bool documentsReady = false;
std::string statusMessage = {};
XCUIAssetDocumentSource documentSource = XCUIAssetDocumentSource(
XCUIAssetDocumentSource::MakeLayoutLabPathSet());
fs::path repoRoot = {};
fs::path viewPath = {};
fs::path themePath = {};
fs::file_time_type viewWriteTime = {};
fs::file_time_type themeWriteTime = {};
};
String ToContainersString(const std::string& value) {
return String(value.c_str());
}
std::string ToStdString(const String& value) {
return std::string(value.CStr());
}
const UIDocumentAttribute* FindAttribute(const UIDocumentNode& node, const char* name) {
for (const UIDocumentAttribute& attribute : node.attributes) {
if (std::string(attribute.name.CStr()) == name) {
return &attribute;
}
}
return nullptr;
}
std::string GetAttributeValue(
const UIDocumentNode& node,
const char* name,
const std::string& fallback = {}) {
const UIDocumentAttribute* attribute = FindAttribute(node, name);
return attribute != nullptr
? std::string(attribute->value.CStr())
: fallback;
}
bool TryParseFloat(const std::string& text, float& outValue) {
if (text.empty()) {
return false;
}
char* end = nullptr;
const float parsed = std::strtof(text.c_str(), &end);
if (end == text.c_str() || (end != nullptr && *end != '\0')) {
return false;
}
outValue = parsed;
return true;
}
bool TryParseHexColor(const std::string& text, Color& outColor) {
if ((text.size() != 7u && text.size() != 9u) || text[0] != '#') {
return false;
}
auto parseChannel = [&](std::size_t offset) -> int {
return std::stoi(text.substr(offset, 2), nullptr, 16);
};
try {
const int r = parseChannel(1);
const int g = parseChannel(3);
const int b = parseChannel(5);
const int a = text.size() == 9u ? parseChannel(7) : 255;
outColor = Color(
static_cast<float>(r) / 255.0f,
static_cast<float>(g) / 255.0f,
static_cast<float>(b) / 255.0f,
static_cast<float>(a) / 255.0f);
return true;
} catch (...) {
return false;
}
}
Style::UIStyleValue ParseThemeTokenValue(
const std::string& typeName,
const std::string& rawValue,
bool& outSucceeded) {
outSucceeded = true;
if (typeName == "float") {
float value = 0.0f;
outSucceeded = TryParseFloat(rawValue, value);
return Style::UIStyleValue(value);
}
if (typeName == "color") {
Color color = {};
outSucceeded = TryParseHexColor(rawValue, color);
return Style::UIStyleValue(color);
}
outSucceeded = false;
return Style::UIStyleValue();
}
Style::UITheme BuildThemeFromDocument(const UIDocumentModel& document, std::string& outError) {
Style::UIThemeDefinition definition = {};
definition.name = document.displayName.Empty()
? "Layout Lab Theme"
: ToStdString(document.displayName);
for (const UIDocumentNode& child : document.rootNode.children) {
if (ToStdString(child.tagName) != "Token") {
continue;
}
const std::string tokenName = GetAttributeValue(child, "name");
const std::string typeName = GetAttributeValue(child, "type");
const std::string valueText = GetAttributeValue(child, "value");
if (tokenName.empty() || typeName.empty() || valueText.empty()) {
continue;
}
bool succeeded = false;
const Style::UIStyleValue parsedValue =
ParseThemeTokenValue(typeName, valueText, succeeded);
if (!succeeded) {
outError = "Failed to parse layout lab theme token: " + tokenName;
return Style::BuildBuiltinTheme(Style::UIBuiltinThemeKind::NeutralDark);
}
definition.SetToken(tokenName, parsedValue);
}
return Style::BuildTheme(definition);
}
std::optional<fs::path> FindRepositoryRoot() {
std::vector<fs::path> candidates = {
fs::path(XCENGINE_NEW_EDITOR_REPO_ROOT)
};
const String resourceRoot = ResourceManager::Get().GetResourceRoot();
if (!resourceRoot.Empty()) {
candidates.push_back(fs::path(resourceRoot.CStr()));
}
std::error_code ec;
const fs::path currentPath = fs::current_path(ec);
if (!ec) {
candidates.push_back(currentPath);
}
for (const fs::path& candidate : candidates) {
fs::path probe = candidate;
while (!probe.empty()) {
if (fs::exists(probe / kViewRelativePath) &&
fs::exists(probe / kThemeRelativePath)) {
return probe;
}
const fs::path parent = probe.parent_path();
if (parent == probe) {
break;
}
probe = parent;
}
}
return std::nullopt;
}
bool TryGetWriteTime(const fs::path& path, fs::file_time_type& outWriteTime) {
try {
if (!path.empty() && fs::exists(path)) {
outWriteTime = fs::last_write_time(path);
return true;
}
} catch (...) {
}
return false;
}
bool UpdateTrackedWriteTimes(RuntimeBuildContext& state) {
fs::file_time_type viewWriteTime = {};
fs::file_time_type themeWriteTime = {};
if (!TryGetWriteTime(state.viewPath, viewWriteTime) ||
!TryGetWriteTime(state.themePath, themeWriteTime)) {
return false;
}
state.viewWriteTime = viewWriteTime;
state.themeWriteTime = themeWriteTime;
return true;
}
bool DocumentsChangedOnDisk(const RuntimeBuildContext& state) {
fs::file_time_type viewWriteTime = {};
fs::file_time_type themeWriteTime = {};
if (!TryGetWriteTime(state.viewPath, viewWriteTime) ||
!TryGetWriteTime(state.themePath, themeWriteTime)) {
return false;
}
return viewWriteTime != state.viewWriteTime ||
themeWriteTime != state.themeWriteTime;
}
bool IsStretch(const std::string& value) {
return value.empty() || value == "stretch";
}
bool IsScrollViewTag(const std::string& tagName) {
return tagName == "ScrollView";
}
float ResolveScalar(
const std::string& text,
float referenceValue,
float fallbackValue) {
float parsedValue = 0.0f;
if (!TryParseFloat(text, parsedValue)) {
return fallbackValue;
}
if (parsedValue > 0.0f && parsedValue <= 1.0f) {
return referenceValue * parsedValue;
}
return parsedValue;
}
float ResolveFloatToken(
const Style::UITheme& theme,
const std::string& tokenName,
float fallbackValue) {
const Style::UITokenResolveResult result =
theme.ResolveToken(tokenName, Style::UIStyleValueType::Float);
if (result.status != Style::UITokenResolveStatus::Resolved) {
return fallbackValue;
}
const float* value = result.value.TryGetFloat();
return value != nullptr ? *value : fallbackValue;
}
Color ResolveColorToken(
const Style::UITheme& theme,
const std::string& tokenName,
const Color& fallbackValue) {
const Style::UITokenResolveResult result =
theme.ResolveToken(tokenName, Style::UIStyleValueType::Color);
if (result.status != Style::UITokenResolveStatus::Resolved) {
return fallbackValue;
}
const Color* value = result.value.TryGetColor();
return value != nullptr ? *value : fallbackValue;
}
UIColor ToUIColor(const Color& color) {
return UIColor(color.r, color.g, color.b, color.a);
}
UIRect InsetRect(const UIRect& rect, float padding) {
const float width = (std::max)(0.0f, rect.width - padding * 2.0f);
const float height = (std::max)(0.0f, rect.height - padding * 2.0f);
return UIRect(rect.x + padding, rect.y + padding, width, height);
}
UIRect IntersectRects(const UIRect& a, const UIRect& b) {
const float left = (std::max)(a.x, b.x);
const float top = (std::max)(a.y, b.y);
const float right = (std::min)(a.x + a.width, b.x + b.width);
const float bottom = (std::min)(a.y + a.height, b.y + b.height);
return UIRect(
left,
top,
(std::max)(0.0f, right - left),
(std::max)(0.0f, bottom - top));
}
bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
return point.x >= rect.x &&
point.y >= rect.y &&
point.x <= rect.x + rect.width &&
point.y <= rect.y + rect.height;
}
void AnalyzeNativeOverlayCompatibility(
const XCEngine::UI::UIDrawData& drawData,
XCUILayoutLabFrameStats& stats) {
const XCUIRHICommandSupportSummary summary = SummarizeXCUIRHICommandSupport(drawData);
stats.filledRectCommandCount = summary.stats.filledRectCommandCount;
stats.rectOutlineCommandCount = summary.stats.rectOutlineCommandCount;
stats.textCommandCount = summary.stats.textCommandCount;
stats.imageCommandCount = summary.stats.imageCommandCount;
stats.clipPushCommandCount = summary.stats.clipPushCommandCount;
stats.clipPopCommandCount = summary.stats.clipPopCommandCount;
stats.nativeSupportedCommandCount = summary.stats.supportedCommandCount;
stats.nativeUnsupportedCommandCount = summary.stats.unsupportedCommandCount;
stats.nativeUnsupportedImageCommandCount = summary.stats.unsupportedImageCommandCount;
stats.nativeUnsupportedUnknownCommandCount = summary.stats.unsupportedUnknownCommandCount;
stats.nativeOverlayReady = stats.documentsReady &&
stats.nativeUnsupportedCommandCount == 0u;
stats.nativeOverlayStatusMessage = summary.diagnostic;
}
std::string BuildNodeId(
const UIDocumentNode& node,
const std::string& parentId,
std::size_t siblingIndex) {
const std::string explicitId = GetAttributeValue(node, "id");
if (!explicitId.empty()) {
return explicitId;
}
const std::string generated =
ToStdString(node.tagName) + std::to_string(static_cast<unsigned long long>(siblingIndex));
return parentId.empty()
? generated
: parentId + "/" + generated;
}
void BuildNodesRecursive(
RuntimeBuildContext& state,
const UIDocumentNode& node,
std::size_t parentIndex,
const std::string& parentId,
std::size_t siblingIndex,
int depth) {
const std::size_t nodeIndex = state.nodes.size();
LayoutNode layoutNode = {};
layoutNode.id = BuildNodeId(node, parentId, siblingIndex);
layoutNode.tagName = ToStdString(node.tagName);
layoutNode.title = GetAttributeValue(node, "title");
layoutNode.subtitle = GetAttributeValue(node, "subtitle");
layoutNode.tone = GetAttributeValue(node, "tone");
layoutNode.widthAttr = GetAttributeValue(node, "width");
layoutNode.heightAttr = GetAttributeValue(node, "height");
layoutNode.xAttr = GetAttributeValue(node, "x");
layoutNode.yAttr = GetAttributeValue(node, "y");
layoutNode.gapAttr = GetAttributeValue(node, "gap");
layoutNode.paddingAttr = GetAttributeValue(node, "padding");
layoutNode.scrollYAttr = GetAttributeValue(node, "scrollY");
layoutNode.parentIndex = parentIndex;
layoutNode.depth = depth;
state.nodes.push_back(layoutNode);
state.nodeIndexById[state.nodes.back().id] = nodeIndex;
if (parentIndex != kInvalidIndex) {
state.nodes[parentIndex].children.push_back(nodeIndex);
}
for (std::size_t childIndex = 0; childIndex < node.children.Size(); ++childIndex) {
BuildNodesRecursive(
state,
node.children[childIndex],
nodeIndex,
state.nodes[nodeIndex].id,
childIndex,
depth + 1);
}
}
float ResolveGap(const LayoutNode& node, const Style::UITheme& theme) {
if (!node.gapAttr.empty()) {
return ResolveScalar(
node.gapAttr,
ResolveFloatToken(theme, "space.gap", 14.0f),
ResolveFloatToken(theme, "space.gap", 14.0f));
}
return node.tagName == "Column"
? ResolveFloatToken(theme, "space.stack", 12.0f)
: ResolveFloatToken(theme, "space.gap", 14.0f);
}
float ResolvePadding(const LayoutNode& node, const Style::UITheme& theme) {
if (!node.paddingAttr.empty()) {
return ResolveScalar(
node.paddingAttr,
ResolveFloatToken(theme, "space.outer", 18.0f),
ResolveFloatToken(theme, "space.outer", 18.0f));
}
return node.tagName == "View"
? ResolveFloatToken(theme, "space.outer", 18.0f)
: 0.0f;
}
float ResolveScrollOffset(const LayoutNode& node, const Style::UITheme& theme) {
if (node.scrollYAttr.empty()) {
return 0.0f;
}
return (std::max)(
0.0f,
ResolveScalar(
node.scrollYAttr,
ResolveFloatToken(theme, "size.scrollStep", 64.0f),
0.0f));
}
float ResolveListItemHeight(const Style::UITheme& theme) {
return ResolveFloatToken(theme, "size.listItemHeight", 60.0f);
}
UIRect GetContentRect(const LayoutNode& node, const Style::UITheme& theme) {
return InsetRect(node.rect, ResolvePadding(node, theme));
}
void LayoutNodeTree(RuntimeBuildContext& state, std::size_t nodeIndex);
void LayoutColumnChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
LayoutNode& node = state.nodes[nodeIndex];
const float padding = ResolvePadding(node, state.theme);
const float gap = ResolveGap(node, state.theme);
const UIRect contentRect = InsetRect(node.rect, padding);
if (node.children.empty()) {
return;
}
float fixedHeight = 0.0f;
std::size_t stretchCount = 0;
std::vector<float> resolvedHeights(node.children.size(), 0.0f);
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
const LayoutNode& child = state.nodes[node.children[childOffset]];
if (IsStretch(child.heightAttr)) {
++stretchCount;
continue;
}
resolvedHeights[childOffset] = ResolveScalar(child.heightAttr, contentRect.height, 0.0f);
fixedHeight += resolvedHeights[childOffset];
}
const float totalGap = gap * static_cast<float>((std::max<std::size_t>)(1u, node.children.size()) - 1u);
const float remainingHeight = (std::max)(0.0f, contentRect.height - fixedHeight - totalGap);
const float stretchHeight =
stretchCount > 0u ? remainingHeight / static_cast<float>(stretchCount) : 0.0f;
float cursorY = contentRect.y;
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
LayoutNode& child = state.nodes[node.children[childOffset]];
const float childHeight =
!IsStretch(child.heightAttr) ? resolvedHeights[childOffset] : stretchHeight;
const float childWidth =
!IsStretch(child.widthAttr)
? ResolveScalar(child.widthAttr, contentRect.width, contentRect.width)
: contentRect.width;
child.rect = UIRect(contentRect.x, cursorY, childWidth, childHeight);
cursorY += childHeight + gap;
LayoutNodeTree(state, node.children[childOffset]);
}
}
void LayoutRowChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
LayoutNode& node = state.nodes[nodeIndex];
const float padding = ResolvePadding(node, state.theme);
const float gap = ResolveGap(node, state.theme);
const UIRect contentRect = InsetRect(node.rect, padding);
if (node.children.empty()) {
return;
}
float fixedWidth = 0.0f;
std::size_t stretchCount = 0;
std::vector<float> resolvedWidths(node.children.size(), 0.0f);
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
const LayoutNode& child = state.nodes[node.children[childOffset]];
if (IsStretch(child.widthAttr)) {
++stretchCount;
continue;
}
resolvedWidths[childOffset] = ResolveScalar(child.widthAttr, contentRect.width, 0.0f);
fixedWidth += resolvedWidths[childOffset];
}
const float totalGap = gap * static_cast<float>((std::max<std::size_t>)(1u, node.children.size()) - 1u);
const float remainingWidth = (std::max)(0.0f, contentRect.width - fixedWidth - totalGap);
const float stretchWidth =
stretchCount > 0u ? remainingWidth / static_cast<float>(stretchCount) : 0.0f;
float cursorX = contentRect.x;
for (std::size_t childOffset = 0; childOffset < node.children.size(); ++childOffset) {
LayoutNode& child = state.nodes[node.children[childOffset]];
const float childWidth =
!IsStretch(child.widthAttr) ? resolvedWidths[childOffset] : stretchWidth;
const float childHeight =
!IsStretch(child.heightAttr)
? ResolveScalar(child.heightAttr, contentRect.height, contentRect.height)
: contentRect.height;
child.rect = UIRect(cursorX, contentRect.y, childWidth, childHeight);
cursorX += childWidth + gap;
LayoutNodeTree(state, node.children[childOffset]);
}
}
void LayoutOverlayChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
LayoutNode& node = state.nodes[nodeIndex];
const UIRect contentRect = GetContentRect(node, state.theme);
for (std::size_t childIndex : node.children) {
LayoutNode& child = state.nodes[childIndex];
const float offsetX = ResolveScalar(child.xAttr, contentRect.width, 0.0f);
const float offsetY = ResolveScalar(child.yAttr, contentRect.height, 0.0f);
const float childWidth =
child.widthAttr.empty()
? (std::max)(0.0f, contentRect.width - offsetX)
: ResolveScalar(child.widthAttr, contentRect.width, contentRect.width - offsetX);
const float childHeight =
child.heightAttr.empty()
? (std::max)(0.0f, contentRect.height - offsetY)
: ResolveScalar(child.heightAttr, contentRect.height, contentRect.height - offsetY);
child.rect = UIRect(
contentRect.x + offsetX,
contentRect.y + offsetY,
(std::min)(childWidth, (std::max)(0.0f, contentRect.width - offsetX)),
(std::min)(childHeight, (std::max)(0.0f, contentRect.height - offsetY)));
LayoutNodeTree(state, childIndex);
}
}
void LayoutScrollViewChildren(RuntimeBuildContext& state, std::size_t nodeIndex) {
LayoutNode& node = state.nodes[nodeIndex];
const float gap = ResolveGap(node, state.theme);
const float scrollOffset = ResolveScrollOffset(node, state.theme);
const float defaultItemHeight = ResolveListItemHeight(state.theme);
const UIRect contentRect = GetContentRect(node, state.theme);
float cursorY = contentRect.y - scrollOffset;
for (std::size_t childIndex : node.children) {
LayoutNode& child = state.nodes[childIndex];
const float childHeight =
!IsStretch(child.heightAttr)
? ResolveScalar(child.heightAttr, contentRect.height, defaultItemHeight)
: defaultItemHeight;
const float childWidth =
!IsStretch(child.widthAttr)
? (std::min)(
ResolveScalar(child.widthAttr, contentRect.width, contentRect.width),
contentRect.width)
: contentRect.width;
child.rect = UIRect(contentRect.x, cursorY, childWidth, childHeight);
cursorY += childHeight + gap;
LayoutNodeTree(state, childIndex);
}
}
void LayoutNodeTree(RuntimeBuildContext& state, std::size_t nodeIndex) {
const LayoutNode& node = state.nodes[nodeIndex];
state.rectsById[node.id] = node.rect;
if (node.tagName == "View" || node.tagName == "Column") {
LayoutColumnChildren(state, nodeIndex);
} else if (node.tagName == "Row") {
LayoutRowChildren(state, nodeIndex);
} else if (node.tagName == "Overlay") {
LayoutOverlayChildren(state, nodeIndex);
} else if (IsScrollViewTag(node.tagName)) {
LayoutScrollViewChildren(state, nodeIndex);
}
}
void DrawNode(
RuntimeBuildContext& state,
std::size_t nodeIndex,
UIDrawList& drawList,
const std::string& hoveredId) {
const LayoutNode& node = state.nodes[nodeIndex];
if (node.tagName == "View") {
const Color panelColor = ResolveColorToken(
state.theme,
"color.panel",
Color(0.07f, 0.10f, 0.14f, 1.0f));
drawList.AddFilledRect(node.rect, ToUIColor(panelColor), 0.0f);
} else if (IsScrollViewTag(node.tagName)) {
const Color surfaceColor = ResolveColorToken(
state.theme,
"color.scroll.surface",
Color(0.09f, 0.12f, 0.16f, 1.0f));
const Color borderColor = ResolveColorToken(
state.theme,
"color.border",
Color(0.24f, 0.34f, 0.43f, 1.0f));
const float rounding = ResolveFloatToken(state.theme, "radius.card", 10.0f);
drawList.AddFilledRect(node.rect, ToUIColor(surfaceColor), rounding);
drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding);
} else if (node.tagName == "Card") {
const Color cardColor = ResolveColorToken(
state.theme,
node.tone == "accent"
? "color.accent"
: (node.tone == "accent-alt" ? "color.card.alt" : "color.card"),
node.tone == "accent"
? Color(0.19f, 0.33f, 0.44f, 1.0f)
: Color(0.12f, 0.17f, 0.23f, 1.0f));
const Color borderColor = ResolveColorToken(
state.theme,
"color.border",
Color(0.24f, 0.34f, 0.43f, 1.0f));
const Color textColor = ResolveColorToken(
state.theme,
"color.text",
Color(0.95f, 0.97f, 1.0f, 1.0f));
const Color mutedColor = ResolveColorToken(
state.theme,
"color.text.muted",
Color(0.72f, 0.79f, 0.86f, 1.0f));
const float rounding = ResolveFloatToken(state.theme, "radius.card", 10.0f);
const float inset = ResolveFloatToken(state.theme, "space.cardInset", 12.0f);
const float titleFont = ResolveFloatToken(state.theme, "font.title", 16.0f);
const float bodyFont = ResolveFloatToken(state.theme, "font.body", 13.0f);
drawList.AddFilledRect(node.rect, ToUIColor(cardColor), rounding);
drawList.AddRectOutline(node.rect, ToUIColor(borderColor), 1.0f, rounding);
if (!node.title.empty()) {
drawList.AddText(
UIPoint(node.rect.x + inset, node.rect.y + inset),
node.title,
ToUIColor(textColor),
titleFont);
}
if (!node.subtitle.empty()) {
drawList.AddText(
UIPoint(node.rect.x + inset, node.rect.y + inset + titleFont + 8.0f),
node.subtitle,
ToUIColor(mutedColor),
bodyFont);
}
if (node.id == hoveredId) {
drawList.AddRectOutline(
node.rect,
UIColor(1.0f, 0.82f, 0.45f, 1.0f),
2.0f,
rounding);
}
}
const bool clipsChildren = IsScrollViewTag(node.tagName);
if (clipsChildren) {
drawList.PushClipRect(GetContentRect(node, state.theme));
}
for (std::size_t childIndex : node.children) {
DrawNode(state, childIndex, drawList, hoveredId);
}
if (clipsChildren) {
drawList.PopClipRect();
}
}
bool IsPointInsideNodeClipping(
const RuntimeBuildContext& state,
std::size_t nodeIndex,
const UIPoint& point) {
std::size_t currentIndex = nodeIndex;
while (currentIndex != kInvalidIndex) {
const LayoutNode& currentNode = state.nodes[currentIndex];
if (IsScrollViewTag(currentNode.tagName) &&
!ContainsPoint(GetContentRect(currentNode, state.theme), point)) {
return false;
}
currentIndex = currentNode.parentIndex;
}
return true;
}
std::size_t HitTest(
const RuntimeBuildContext& state,
const UIPoint& point) {
std::size_t bestIndex = kInvalidIndex;
int bestDepth = -1;
for (std::size_t index = 0; index < state.nodes.size(); ++index) {
const LayoutNode& node = state.nodes[index];
if (node.tagName != "Card" ||
!ContainsPoint(node.rect, point) ||
!IsPointInsideNodeClipping(state, index, point)) {
continue;
}
if (node.depth >= bestDepth) {
bestIndex = index;
bestDepth = node.depth;
}
}
return bestIndex;
}
} // namespace
struct XCUILayoutLabRuntime::RuntimeState {
RuntimeBuildContext data = {};
};
XCUILayoutLabRuntime::XCUILayoutLabRuntime()
: m_state(std::make_unique<RuntimeState>()) {
}
XCUILayoutLabRuntime::~XCUILayoutLabRuntime() = default;
XCUILayoutLabRuntime::XCUILayoutLabRuntime(XCUILayoutLabRuntime&& other) noexcept = default;
XCUILayoutLabRuntime& XCUILayoutLabRuntime::operator=(XCUILayoutLabRuntime&& other) noexcept = default;
bool XCUILayoutLabRuntime::ReloadDocuments() {
RuntimeBuildContext& state = m_state->data;
state.documentsReady = false;
state.statusMessage.clear();
state.viewDocument = {};
state.themeDocument = {};
state.nodes.clear();
state.nodeIndexById.clear();
state.rectsById.clear();
state.documentSource.SetPathSet(XCUIAssetDocumentSource::MakeLayoutLabPathSet());
if (!state.documentSource.Reload()) {
const XCUIAssetDocumentSource::LoadState& loadState = state.documentSource.GetState();
state.statusMessage = !loadState.errorMessage.empty()
? loadState.errorMessage
: loadState.statusMessage;
state.frameResult.stats.statusMessage = state.statusMessage;
return false;
}
const XCUIAssetDocumentSource::LoadState& loadState = state.documentSource.GetState();
state.repoRoot = loadState.repositoryRoot;
state.viewPath = loadState.view.sourcePath;
state.themePath = loadState.theme.sourcePath;
state.viewDocument = loadState.view.compileResult;
state.themeDocument = loadState.theme.compileResult;
std::string themeError = {};
state.theme = BuildThemeFromDocument(state.themeDocument.document, themeError);
if (!themeError.empty()) {
state.statusMessage = themeError;
state.frameResult.stats.statusMessage = state.statusMessage;
return false;
}
BuildNodesRecursive(
state,
state.viewDocument.document.rootNode,
kInvalidIndex,
std::string(),
0u,
0);
state.documentsReady = !state.nodes.empty();
state.statusMessage = state.documentsReady
? (loadState.usedLegacyFallback
? "Layout lab documents loaded (legacy fallback)"
: "Layout lab documents loaded")
: "Layout lab view is empty";
state.frameResult.stats.statusMessage = state.statusMessage;
return state.documentsReady;
}
const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLabInputState& input) {
RuntimeBuildContext& state = m_state->data;
state.frameResult = XCUILayoutLabFrameResult();
state.rectsById.clear();
if (state.documentsReady && state.documentSource.HasTrackedChanges()) {
ReloadDocuments();
}
if (!state.documentsReady && !ReloadDocuments()) {
state.frameResult.stats.documentsReady = false;
state.frameResult.stats.statusMessage = state.statusMessage;
state.frameResult.stats.nativeOverlayStatusMessage = state.statusMessage;
return state.frameResult;
}
if (state.nodes.empty()) {
state.frameResult.stats.documentsReady = false;
state.frameResult.stats.statusMessage = "Layout lab has no nodes.";
state.frameResult.stats.nativeOverlayStatusMessage = state.frameResult.stats.statusMessage;
return state.frameResult;
}
state.nodes[0].rect = input.canvasRect;
LayoutNodeTree(state, 0u);
if (!state.nodes.empty()) {
state.rectsById[state.nodes[0].id] = state.nodes[0].rect;
}
if (input.pointerInside) {
const std::size_t hoveredIndex = HitTest(state, input.pointerPosition);
if (hoveredIndex != kInvalidIndex) {
state.frameResult.stats.hoveredElementId = state.nodes[hoveredIndex].id;
}
}
UIDrawList drawList("XCUI Layout Lab");
drawList.PushClipRect(input.canvasRect);
DrawNode(state, 0u, drawList, state.frameResult.stats.hoveredElementId);
drawList.PopClipRect();
state.frameResult.drawData.AddDrawList(std::move(drawList));
state.frameResult.stats.documentsReady = true;
state.frameResult.stats.statusMessage = state.statusMessage;
state.frameResult.stats.drawListCount = state.frameResult.drawData.GetDrawListCount();
state.frameResult.stats.commandCount = state.frameResult.drawData.GetTotalCommandCount();
AnalyzeNativeOverlayCompatibility(state.frameResult.drawData, state.frameResult.stats);
for (const LayoutNode& node : state.nodes) {
if (node.tagName == "Row") {
++state.frameResult.stats.rowCount;
} else if (node.tagName == "Column") {
++state.frameResult.stats.columnCount;
} else if (node.tagName == "Overlay") {
++state.frameResult.stats.overlayCount;
} else if (IsScrollViewTag(node.tagName)) {
++state.frameResult.stats.scrollViewCount;
}
}
return state.frameResult;
}
const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::GetFrameResult() const {
return m_state->data.frameResult;
}
bool XCUILayoutLabRuntime::TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const {
const RuntimeBuildContext& state = m_state->data;
const auto it = state.rectsById.find(elementId);
if (it == state.rectsById.end()) {
return false;
}
outRect = it->second;
return true;
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,74 @@
#pragma once
#include <XCEngine/UI/DrawData.h>
#include <memory>
#include <string>
namespace XCEngine {
namespace UI {
struct UIRect;
struct UIPoint;
} // namespace UI
namespace Editor {
namespace XCUIBackend {
struct XCUILayoutLabInputState {
UI::UIRect canvasRect = {};
UI::UIPoint pointerPosition = {};
bool pointerInside = false;
};
struct XCUILayoutLabFrameStats {
bool documentsReady = false;
std::string statusMessage = {};
std::size_t drawListCount = 0;
std::size_t commandCount = 0;
std::size_t filledRectCommandCount = 0;
std::size_t rectOutlineCommandCount = 0;
std::size_t textCommandCount = 0;
std::size_t imageCommandCount = 0;
std::size_t clipPushCommandCount = 0;
std::size_t clipPopCommandCount = 0;
std::size_t nativeSupportedCommandCount = 0;
std::size_t nativeUnsupportedCommandCount = 0;
std::size_t nativeUnsupportedImageCommandCount = 0;
std::size_t nativeUnsupportedUnknownCommandCount = 0;
bool nativeOverlayReady = false;
std::string nativeOverlayStatusMessage = {};
std::size_t rowCount = 0;
std::size_t columnCount = 0;
std::size_t overlayCount = 0;
std::size_t scrollViewCount = 0;
std::string hoveredElementId = {};
};
struct XCUILayoutLabFrameResult {
UI::UIDrawData drawData = {};
XCUILayoutLabFrameStats stats = {};
};
class XCUILayoutLabRuntime {
public:
XCUILayoutLabRuntime();
~XCUILayoutLabRuntime();
XCUILayoutLabRuntime(XCUILayoutLabRuntime&& other) noexcept;
XCUILayoutLabRuntime& operator=(XCUILayoutLabRuntime&& other) noexcept;
XCUILayoutLabRuntime(const XCUILayoutLabRuntime&) = delete;
XCUILayoutLabRuntime& operator=(const XCUILayoutLabRuntime&) = delete;
bool ReloadDocuments();
const XCUILayoutLabFrameResult& Update(const XCUILayoutLabInputState& input);
const XCUILayoutLabFrameResult& GetFrameResult() const;
bool TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const;
private:
struct RuntimeState;
std::unique_ptr<RuntimeState> m_state;
};
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,590 @@
#include "XCUIBackend/XCUIRHICommandCompiler.h"
#include <algorithm>
#include <iterator>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
namespace {
using Compiler = XCUIRHICommandCompiler;
UI::UIRect NormalizeRect(const UI::UIRect& rect) {
UI::UIRect normalized = rect;
if (normalized.width < 0.0f) {
normalized.x += normalized.width;
normalized.width = -normalized.width;
}
if (normalized.height < 0.0f) {
normalized.y += normalized.height;
normalized.height = -normalized.height;
}
return normalized;
}
bool IsRectEmpty(const UI::UIRect& rect) {
return rect.width <= 0.0f || rect.height <= 0.0f;
}
bool IntersectRects(
const UI::UIRect& lhs,
const UI::UIRect& rhs,
UI::UIRect& outIntersection) {
const UI::UIRect left = NormalizeRect(lhs);
const UI::UIRect right = NormalizeRect(rhs);
const float minX = (std::max)(left.x, right.x);
const float minY = (std::max)(left.y, right.y);
const float maxX = (std::min)(left.x + left.width, right.x + right.width);
const float maxY = (std::min)(left.y + left.height, right.y + right.height);
if (maxX <= minX || maxY <= minY) {
outIntersection = {};
return false;
}
outIntersection = UI::UIRect(minX, minY, maxX - minX, maxY - minY);
return true;
}
bool RectEquals(const UI::UIRect& lhs, const UI::UIRect& rhs) {
return lhs.x == rhs.x &&
lhs.y == rhs.y &&
lhs.width == rhs.width &&
lhs.height == rhs.height;
}
bool TextureEquals(const UI::UITextureHandle& lhs, const UI::UITextureHandle& rhs) {
return lhs.nativeHandle == rhs.nativeHandle &&
lhs.width == rhs.width &&
lhs.height == rhs.height &&
lhs.kind == rhs.kind;
}
void AppendFilledRectVertices(
std::vector<Compiler::ColorVertex>& outVertices,
const UI::UIRect& rect,
const UI::UIColor& color) {
const Compiler::ColorVertex vertices[6] = {
{ { rect.x, rect.y }, { color.r, color.g, color.b, color.a } },
{ { rect.x + rect.width, rect.y }, { color.r, color.g, color.b, color.a } },
{ { rect.x + rect.width, rect.y + rect.height }, { color.r, color.g, color.b, color.a } },
{ { rect.x, rect.y }, { color.r, color.g, color.b, color.a } },
{ { rect.x + rect.width, rect.y + rect.height }, { color.r, color.g, color.b, color.a } },
{ { rect.x, rect.y + rect.height }, { color.r, color.g, color.b, color.a } }
};
outVertices.insert(outVertices.end(), std::begin(vertices), std::end(vertices));
}
bool AppendClippedFilledRect(
std::vector<Compiler::ColorVertex>& outVertices,
const UI::UIRect& rect,
const UI::UIColor& color,
const UI::UIRect& clipRect) {
UI::UIRect clipped = {};
if (!IntersectRects(rect, clipRect, clipped)) {
return false;
}
AppendFilledRectVertices(outVertices, clipped, color);
return true;
}
bool AppendClippedRectOutline(
std::vector<Compiler::ColorVertex>& outVertices,
const UI::UIRect& rect,
const UI::UIColor& color,
float thickness,
const UI::UIRect& clipRect) {
const UI::UIRect normalized = NormalizeRect(rect);
if (IsRectEmpty(normalized)) {
return false;
}
const float resolvedThickness = (std::max)(1.0f, thickness);
const float halfWidth = normalized.width * 0.5f;
const float halfHeight = normalized.height * 0.5f;
const float edgeThickness = (std::min)(resolvedThickness, (std::min)(halfWidth, halfHeight));
if (edgeThickness <= 0.0f) {
return false;
}
bool rendered = false;
rendered |= AppendClippedFilledRect(
outVertices,
UI::UIRect(normalized.x, normalized.y, normalized.width, edgeThickness),
color,
clipRect);
rendered |= AppendClippedFilledRect(
outVertices,
UI::UIRect(
normalized.x,
normalized.y + normalized.height - edgeThickness,
normalized.width,
edgeThickness),
color,
clipRect);
rendered |= AppendClippedFilledRect(
outVertices,
UI::UIRect(
normalized.x,
normalized.y + edgeThickness,
edgeThickness,
normalized.height - edgeThickness * 2.0f),
color,
clipRect);
rendered |= AppendClippedFilledRect(
outVertices,
UI::UIRect(
normalized.x + normalized.width - edgeThickness,
normalized.y + edgeThickness,
edgeThickness,
normalized.height - edgeThickness * 2.0f),
color,
clipRect);
return rendered;
}
void AppendTexturedRectVertices(
std::vector<Compiler::TexturedVertex>& outVertices,
const UI::UIRect& rect,
float u0,
float v0,
float u1,
float v1,
const UI::UIColor& color) {
const Compiler::TexturedVertex vertices[6] = {
{ { rect.x, rect.y }, { u0, v0 }, { color.r, color.g, color.b, color.a } },
{ { rect.x + rect.width, rect.y }, { u1, v0 }, { color.r, color.g, color.b, color.a } },
{ { rect.x + rect.width, rect.y + rect.height }, { u1, v1 }, { color.r, color.g, color.b, color.a } },
{ { rect.x, rect.y }, { u0, v0 }, { color.r, color.g, color.b, color.a } },
{ { rect.x + rect.width, rect.y + rect.height }, { u1, v1 }, { color.r, color.g, color.b, color.a } },
{ { rect.x, rect.y + rect.height }, { u0, v1 }, { color.r, color.g, color.b, color.a } }
};
outVertices.insert(outVertices.end(), std::begin(vertices), std::end(vertices));
}
bool AppendClippedTexturedRect(
std::vector<Compiler::TexturedVertex>& outVertices,
const UI::UIRect& rect,
float u0,
float v0,
float u1,
float v1,
const UI::UIColor& color,
const UI::UIRect& clipRect) {
const bool flipU = rect.width < 0.0f;
const bool flipV = rect.height < 0.0f;
const UI::UIRect normalized = NormalizeRect(rect);
if (IsRectEmpty(normalized)) {
return false;
}
UI::UIRect clipped = {};
if (!IntersectRects(normalized, clipRect, clipped)) {
return false;
}
const float leftT = (clipped.x - normalized.x) / normalized.width;
const float rightT = (clipped.x + clipped.width - normalized.x) / normalized.width;
const float topT = (clipped.y - normalized.y) / normalized.height;
const float bottomT = (clipped.y + clipped.height - normalized.y) / normalized.height;
const float resolvedU0 = flipU ? u1 : u0;
const float resolvedU1 = flipU ? u0 : u1;
const float resolvedV0 = flipV ? v1 : v0;
const float resolvedV1 = flipV ? v0 : v1;
const float clippedU0 = resolvedU0 + (resolvedU1 - resolvedU0) * leftT;
const float clippedU1 = resolvedU0 + (resolvedU1 - resolvedU0) * rightT;
const float clippedV0 = resolvedV0 + (resolvedV1 - resolvedV0) * topT;
const float clippedV1 = resolvedV0 + (resolvedV1 - resolvedV0) * bottomT;
AppendTexturedRectVertices(
outVertices,
clipped,
clippedU0,
clippedV0,
clippedU1,
clippedV1,
color);
return true;
}
bool DecodeNextUtf8(const char*& cursor, const char* end, std::uint32_t& outCodepoint) {
if (cursor >= end) {
return false;
}
const unsigned char lead = static_cast<unsigned char>(*cursor++);
if (lead < 0x80u) {
outCodepoint = static_cast<std::uint32_t>(lead);
return true;
}
const int continuationCount =
(lead & 0xE0u) == 0xC0u ? 1 :
(lead & 0xF0u) == 0xE0u ? 2 :
(lead & 0xF8u) == 0xF0u ? 3 :
-1;
if (continuationCount < 0 || cursor + continuationCount > end) {
outCodepoint = 0xFFFDu;
cursor = end;
return true;
}
std::uint32_t codepoint =
continuationCount == 1 ? static_cast<std::uint32_t>(lead & 0x1Fu) :
continuationCount == 2 ? static_cast<std::uint32_t>(lead & 0x0Fu) :
static_cast<std::uint32_t>(lead & 0x07u);
for (int index = 0; index < continuationCount; ++index) {
const unsigned char continuation = static_cast<unsigned char>(*cursor);
if ((continuation & 0xC0u) != 0x80u) {
outCodepoint = 0xFFFDu;
return true;
}
++cursor;
codepoint = (codepoint << 6u) | static_cast<std::uint32_t>(continuation & 0x3Fu);
}
outCodepoint = codepoint;
return true;
}
void AppendOrMergeBatch(
std::vector<Compiler::Batch>& outBatches,
Compiler::BatchKind kind,
std::size_t drawListIndex,
std::size_t commandIndex,
std::size_t firstVertex,
std::size_t vertexCount,
const UI::UIRect& clipRect,
const UI::UITextureHandle& texture,
bool allowMerge) {
if (vertexCount == 0u) {
return;
}
if (allowMerge && !outBatches.empty()) {
Compiler::Batch& lastBatch = outBatches.back();
if (lastBatch.kind == kind &&
lastBatch.drawListIndex == drawListIndex &&
lastBatch.firstCommandIndex + lastBatch.commandCount == commandIndex &&
RectEquals(lastBatch.clipRect, clipRect) &&
TextureEquals(lastBatch.texture, texture) &&
static_cast<std::size_t>(lastBatch.firstVertex + lastBatch.vertexCount) == firstVertex) {
lastBatch.vertexCount += static_cast<std::uint32_t>(vertexCount);
lastBatch.commandCount += 1u;
return;
}
}
Compiler::Batch batch = {};
batch.kind = kind;
batch.drawListIndex = static_cast<std::uint32_t>(drawListIndex);
batch.firstCommandIndex = static_cast<std::uint32_t>(commandIndex);
batch.commandCount = 1u;
batch.firstVertex = static_cast<std::uint32_t>(firstVertex);
batch.vertexCount = static_cast<std::uint32_t>(vertexCount);
batch.clipRect = clipRect;
batch.texture = texture;
outBatches.push_back(batch);
}
bool CompileTextCommand(
const UI::UIDrawCommand& command,
const UI::UIRect& clipRect,
const Compiler::TextGlyphProvider& glyphProvider,
std::vector<Compiler::TexturedVertex>& outVertices,
Compiler::TextRunContext& outContext,
std::size_t& outAddedVertexCount) {
outContext = {};
if (!glyphProvider.BeginText(command.fontSize, outContext)) {
outAddedVertexCount = 0u;
return false;
}
if (!outContext.texture.IsValid() || outContext.lineHeight <= 0.0f) {
outAddedVertexCount = 0u;
return false;
}
const std::size_t startVertex = outVertices.size();
const float startX = command.position.x;
float cursorX = startX;
float cursorY = command.position.y;
const char* cursor = command.text.c_str();
const char* const end = cursor + command.text.size();
while (cursor < end) {
std::uint32_t codepoint = 0u;
if (!DecodeNextUtf8(cursor, end, codepoint)) {
break;
}
if (codepoint == '\r') {
continue;
}
if (codepoint == '\n') {
cursorX = startX;
cursorY += outContext.lineHeight;
continue;
}
Compiler::TextGlyph glyph = {};
if (!glyphProvider.ResolveGlyph(outContext, codepoint, glyph)) {
continue;
}
if (glyph.visible) {
const UI::UIRect glyphRect(
cursorX + glyph.x0,
cursorY + glyph.y0,
glyph.x1 - glyph.x0,
glyph.y1 - glyph.y0);
AppendClippedTexturedRect(
outVertices,
glyphRect,
glyph.u0,
glyph.v0,
glyph.u1,
glyph.v1,
command.color,
clipRect);
}
cursorX += glyph.advanceX;
}
outAddedVertexCount = outVertices.size() - startVertex;
return true;
}
} // namespace
void XCUIRHICommandCompiler::CompiledDrawData::Clear() {
colorVertices.clear();
texturedVertices.clear();
batches.clear();
stats = {};
}
bool XCUIRHICommandCompiler::CompiledDrawData::Empty() const {
return colorVertices.empty() && texturedVertices.empty() && batches.empty();
}
void XCUIRHICommandCompiler::Compile(
const UI::UIDrawData& drawData,
const CompileConfig& config,
CompiledDrawData& outCompiledDrawData) const {
outCompiledDrawData.Clear();
outCompiledDrawData.stats.drawListCount = drawData.GetDrawListCount();
outCompiledDrawData.stats.commandCount = drawData.GetTotalCommandCount();
if (drawData.Empty()) {
return;
}
UI::UIRect surfaceClipRect = NormalizeRect(config.surfaceClipRect);
if (IsRectEmpty(surfaceClipRect)) {
surfaceClipRect = {};
}
outCompiledDrawData.colorVertices.reserve(drawData.GetTotalCommandCount() * 24u);
outCompiledDrawData.texturedVertices.reserve(drawData.GetTotalCommandCount() * 36u);
outCompiledDrawData.batches.reserve(drawData.GetTotalCommandCount());
std::vector<UI::UIRect> clipStack = {};
UI::UIRect currentClipRect = surfaceClipRect;
std::size_t drawListIndex = 0u;
for (const UI::UIDrawList& drawList : drawData.GetDrawLists()) {
std::size_t commandIndex = 0u;
for (const UI::UIDrawCommand& command : drawList.GetCommands()) {
bool compiled = false;
bool culled = false;
bool unsupported = false;
switch (command.type) {
case UI::UIDrawCommandType::FilledRect: {
++outCompiledDrawData.stats.filledRectCommandCount;
const std::size_t firstVertex = outCompiledDrawData.colorVertices.size();
compiled = AppendClippedFilledRect(
outCompiledDrawData.colorVertices,
command.rect,
command.color,
currentClipRect);
if (compiled) {
AppendOrMergeBatch(
outCompiledDrawData.batches,
BatchKind::Color,
drawListIndex,
commandIndex,
firstVertex,
outCompiledDrawData.colorVertices.size() - firstVertex,
currentClipRect,
{},
config.mergeBatchesWithinDrawList);
} else {
culled = true;
}
break;
}
case UI::UIDrawCommandType::RectOutline: {
++outCompiledDrawData.stats.rectOutlineCommandCount;
const std::size_t firstVertex = outCompiledDrawData.colorVertices.size();
compiled = AppendClippedRectOutline(
outCompiledDrawData.colorVertices,
command.rect,
command.color,
command.thickness,
currentClipRect);
if (compiled) {
AppendOrMergeBatch(
outCompiledDrawData.batches,
BatchKind::Color,
drawListIndex,
commandIndex,
firstVertex,
outCompiledDrawData.colorVertices.size() - firstVertex,
currentClipRect,
{},
config.mergeBatchesWithinDrawList);
} else {
culled = true;
}
break;
}
case UI::UIDrawCommandType::Text: {
++outCompiledDrawData.stats.textCommandCount;
if (command.text.empty()) {
culled = true;
break;
}
if (config.textGlyphProvider == nullptr) {
unsupported = true;
break;
}
const std::size_t firstVertex = outCompiledDrawData.texturedVertices.size();
std::size_t addedVertexCount = 0u;
TextRunContext textRunContext = {};
compiled = CompileTextCommand(
command,
currentClipRect,
*config.textGlyphProvider,
outCompiledDrawData.texturedVertices,
textRunContext,
addedVertexCount);
if (compiled && addedVertexCount > 0u) {
AppendOrMergeBatch(
outCompiledDrawData.batches,
BatchKind::Textured,
drawListIndex,
commandIndex,
firstVertex,
addedVertexCount,
currentClipRect,
textRunContext.texture,
config.mergeBatchesWithinDrawList);
} else if (!compiled) {
unsupported = true;
} else {
compiled = false;
culled = true;
}
break;
}
case UI::UIDrawCommandType::Image: {
++outCompiledDrawData.stats.imageCommandCount;
if (!command.texture.IsValid()) {
unsupported = true;
break;
}
const std::size_t firstVertex = outCompiledDrawData.texturedVertices.size();
compiled = AppendClippedTexturedRect(
outCompiledDrawData.texturedVertices,
command.rect,
0.0f,
0.0f,
1.0f,
1.0f,
command.color,
currentClipRect);
if (compiled) {
AppendOrMergeBatch(
outCompiledDrawData.batches,
BatchKind::Textured,
drawListIndex,
commandIndex,
firstVertex,
outCompiledDrawData.texturedVertices.size() - firstVertex,
currentClipRect,
command.texture,
config.mergeBatchesWithinDrawList);
} else {
culled = true;
}
break;
}
case UI::UIDrawCommandType::PushClipRect: {
++outCompiledDrawData.stats.clipPushCommandCount;
UI::UIRect nextClipRect = NormalizeRect(command.rect);
UI::UIRect intersection = {};
if (command.intersectWithCurrentClip) {
if (!IntersectRects(currentClipRect, nextClipRect, intersection)) {
intersection = {};
}
} else if (!IntersectRects(surfaceClipRect, nextClipRect, intersection)) {
intersection = {};
}
clipStack.push_back(currentClipRect);
currentClipRect = intersection;
compiled = true;
outCompiledDrawData.stats.maxClipDepth =
(std::max)(outCompiledDrawData.stats.maxClipDepth, clipStack.size());
break;
}
case UI::UIDrawCommandType::PopClipRect:
++outCompiledDrawData.stats.clipPopCommandCount;
if (!clipStack.empty()) {
currentClipRect = clipStack.back();
clipStack.pop_back();
} else {
currentClipRect = surfaceClipRect;
++outCompiledDrawData.stats.clipStackUnderflowCount;
}
compiled = true;
break;
default:
unsupported = true;
break;
}
if (compiled) {
++outCompiledDrawData.stats.compiledCommandCount;
} else {
++outCompiledDrawData.stats.skippedCommandCount;
if (culled) {
++outCompiledDrawData.stats.culledCommandCount;
}
if (unsupported) {
++outCompiledDrawData.stats.unsupportedCommandCount;
}
}
++commandIndex;
}
++drawListIndex;
}
outCompiledDrawData.stats.colorVertexCount = outCompiledDrawData.colorVertices.size();
outCompiledDrawData.stats.texturedVertexCount = outCompiledDrawData.texturedVertices.size();
outCompiledDrawData.stats.triangleCount =
(outCompiledDrawData.stats.colorVertexCount +
outCompiledDrawData.stats.texturedVertexCount) /
3u;
outCompiledDrawData.stats.batchCount = outCompiledDrawData.batches.size();
outCompiledDrawData.stats.danglingClipDepth = clipStack.size();
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,119 @@
#pragma once
#include <XCEngine/UI/DrawData.h>
#include <cstddef>
#include <cstdint>
#include <vector>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
class XCUIRHICommandCompiler {
public:
struct ColorVertex {
float position[2] = { 0.0f, 0.0f };
float color[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
};
struct TexturedVertex {
float position[2] = { 0.0f, 0.0f };
float uv[2] = { 0.0f, 0.0f };
float color[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
};
enum class BatchKind : std::uint8_t {
Color = 0,
Textured
};
struct Batch {
BatchKind kind = BatchKind::Color;
std::uint32_t drawListIndex = 0;
std::uint32_t firstCommandIndex = 0;
std::uint32_t commandCount = 0;
std::uint32_t firstVertex = 0;
std::uint32_t vertexCount = 0;
UI::UIRect clipRect = {};
UI::UITextureHandle texture = {};
};
struct Stats {
std::size_t drawListCount = 0;
std::size_t commandCount = 0;
std::size_t compiledCommandCount = 0;
std::size_t skippedCommandCount = 0;
std::size_t culledCommandCount = 0;
std::size_t unsupportedCommandCount = 0;
std::size_t filledRectCommandCount = 0;
std::size_t rectOutlineCommandCount = 0;
std::size_t textCommandCount = 0;
std::size_t imageCommandCount = 0;
std::size_t clipPushCommandCount = 0;
std::size_t clipPopCommandCount = 0;
std::size_t colorVertexCount = 0;
std::size_t texturedVertexCount = 0;
std::size_t triangleCount = 0;
std::size_t batchCount = 0;
std::size_t maxClipDepth = 0;
std::size_t clipStackUnderflowCount = 0;
std::size_t danglingClipDepth = 0;
};
struct CompiledDrawData {
std::vector<ColorVertex> colorVertices = {};
std::vector<TexturedVertex> texturedVertices = {};
std::vector<Batch> batches = {};
Stats stats = {};
void Clear();
bool Empty() const;
};
struct TextRunContext {
float requestedFontSize = 0.0f;
float resolvedFontSize = 0.0f;
float lineHeight = 0.0f;
UI::UITextureHandle texture = {};
};
struct TextGlyph {
float x0 = 0.0f;
float y0 = 0.0f;
float x1 = 0.0f;
float y1 = 0.0f;
float u0 = 0.0f;
float v0 = 0.0f;
float u1 = 0.0f;
float v1 = 0.0f;
float advanceX = 0.0f;
bool visible = true;
};
class TextGlyphProvider {
public:
virtual ~TextGlyphProvider() = default;
virtual bool BeginText(float requestedFontSize, TextRunContext& outContext) const = 0;
virtual bool ResolveGlyph(
const TextRunContext& context,
std::uint32_t codepoint,
TextGlyph& outGlyph) const = 0;
};
struct CompileConfig {
UI::UIRect surfaceClipRect = {};
const TextGlyphProvider* textGlyphProvider = nullptr;
bool mergeBatchesWithinDrawList = true;
};
void Compile(
const UI::UIDrawData& drawData,
const CompileConfig& config,
CompiledDrawData& outCompiledDrawData) const;
};
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,267 @@
#pragma once
#include <XCEngine/UI/DrawData.h>
#include <cstddef>
#include <cstdint>
#include <sstream>
#include <string>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
enum class XCUIRHICommandCategory : std::uint8_t {
FilledRect = 0,
RectOutline,
Text,
Image,
PushClipRect,
PopClipRect,
Unknown
};
enum class XCUIRHICommandSupportReason : std::uint8_t {
Supported = 0,
UnsupportedImageTexture,
UnsupportedUnknownCommand
};
struct XCUIRHICommandClassification {
XCUIRHICommandCategory category = XCUIRHICommandCategory::Unknown;
XCUIRHICommandSupportReason supportReason =
XCUIRHICommandSupportReason::UnsupportedUnknownCommand;
constexpr bool IsSupported() const {
return supportReason == XCUIRHICommandSupportReason::Supported;
}
};
struct XCUIRHICommandSupportStats {
std::size_t drawListCount = 0;
std::size_t commandCount = 0;
std::size_t filledRectCommandCount = 0;
std::size_t rectOutlineCommandCount = 0;
std::size_t textCommandCount = 0;
std::size_t imageCommandCount = 0;
std::size_t clipPushCommandCount = 0;
std::size_t clipPopCommandCount = 0;
std::size_t supportedCommandCount = 0;
std::size_t unsupportedCommandCount = 0;
std::size_t unsupportedImageCommandCount = 0;
std::size_t unsupportedUnknownCommandCount = 0;
constexpr bool HasCommands() const {
return commandCount != 0u;
}
constexpr bool SupportsAllCommands() const {
return unsupportedCommandCount == 0u;
}
};
struct XCUIRHICommandDiagnosticOptions {
const char* noCommandsMessage = "Overlay runtime produced no commands.";
const char* allSupportedMessage = "All commands preflight for native overlay.";
const char* unsupportedPrefix = "command(s) will be skipped by native overlay: ";
const char* unsupportedImageDescription =
"image command(s) missing valid ShaderResourceView textures";
const char* unsupportedUnknownDescription = "unknown command type(s)";
};
struct XCUIRHICommandSupportSummary {
XCUIRHICommandSupportStats stats = {};
std::string diagnostic = {};
};
inline XCUIRHICommandClassification ClassifyXCUIRHICommandSupport(
const UI::UIDrawCommand& command) {
XCUIRHICommandClassification classification = {};
switch (command.type) {
case UI::UIDrawCommandType::FilledRect:
classification.category = XCUIRHICommandCategory::FilledRect;
classification.supportReason = XCUIRHICommandSupportReason::Supported;
break;
case UI::UIDrawCommandType::RectOutline:
classification.category = XCUIRHICommandCategory::RectOutline;
classification.supportReason = XCUIRHICommandSupportReason::Supported;
break;
case UI::UIDrawCommandType::Text:
classification.category = XCUIRHICommandCategory::Text;
classification.supportReason = XCUIRHICommandSupportReason::Supported;
break;
case UI::UIDrawCommandType::Image:
classification.category = XCUIRHICommandCategory::Image;
classification.supportReason =
(command.texture.IsValid() &&
command.texture.kind == UI::UITextureHandleKind::ShaderResourceView)
? XCUIRHICommandSupportReason::Supported
: XCUIRHICommandSupportReason::UnsupportedImageTexture;
break;
case UI::UIDrawCommandType::PushClipRect:
classification.category = XCUIRHICommandCategory::PushClipRect;
classification.supportReason = XCUIRHICommandSupportReason::Supported;
break;
case UI::UIDrawCommandType::PopClipRect:
classification.category = XCUIRHICommandCategory::PopClipRect;
classification.supportReason = XCUIRHICommandSupportReason::Supported;
break;
default:
classification.category = XCUIRHICommandCategory::Unknown;
classification.supportReason = XCUIRHICommandSupportReason::UnsupportedUnknownCommand;
break;
}
return classification;
}
inline void AccumulateXCUIRHICommandSupport(
const XCUIRHICommandClassification& classification,
XCUIRHICommandSupportStats& stats) {
switch (classification.category) {
case XCUIRHICommandCategory::FilledRect:
++stats.filledRectCommandCount;
break;
case XCUIRHICommandCategory::RectOutline:
++stats.rectOutlineCommandCount;
break;
case XCUIRHICommandCategory::Text:
++stats.textCommandCount;
break;
case XCUIRHICommandCategory::Image:
++stats.imageCommandCount;
break;
case XCUIRHICommandCategory::PushClipRect:
++stats.clipPushCommandCount;
break;
case XCUIRHICommandCategory::PopClipRect:
++stats.clipPopCommandCount;
break;
case XCUIRHICommandCategory::Unknown:
break;
}
++stats.commandCount;
if (classification.IsSupported()) {
++stats.supportedCommandCount;
return;
}
++stats.unsupportedCommandCount;
switch (classification.supportReason) {
case XCUIRHICommandSupportReason::Supported:
break;
case XCUIRHICommandSupportReason::UnsupportedImageTexture:
++stats.unsupportedImageCommandCount;
break;
case XCUIRHICommandSupportReason::UnsupportedUnknownCommand:
++stats.unsupportedUnknownCommandCount;
break;
}
}
inline void AccumulateXCUIRHICommandSupport(
const UI::UIDrawCommand& command,
XCUIRHICommandSupportStats& stats) {
AccumulateXCUIRHICommandSupport(ClassifyXCUIRHICommandSupport(command), stats);
}
inline void AccumulateXCUIRHICommandSupport(
const UI::UIDrawList& drawList,
XCUIRHICommandSupportStats& stats) {
++stats.drawListCount;
for (const UI::UIDrawCommand& command : drawList.GetCommands()) {
AccumulateXCUIRHICommandSupport(command, stats);
}
}
inline XCUIRHICommandSupportStats AnalyzeXCUIRHICommandSupport(
const UI::UIDrawList& drawList) {
XCUIRHICommandSupportStats stats = {};
AccumulateXCUIRHICommandSupport(drawList, stats);
return stats;
}
inline XCUIRHICommandSupportStats AnalyzeXCUIRHICommandSupport(
const UI::UIDrawData& drawData) {
XCUIRHICommandSupportStats stats = {};
for (const UI::UIDrawList& drawList : drawData.GetDrawLists()) {
AccumulateXCUIRHICommandSupport(drawList, stats);
}
return stats;
}
inline void AppendXCUIRHICommandDiagnosticIssue(
std::ostringstream& builder,
bool& hasPreviousIssue,
std::size_t count,
const char* description) {
if (count == 0u) {
return;
}
if (hasPreviousIssue) {
builder << ", ";
}
builder << count << ' ' << description;
hasPreviousIssue = true;
}
inline std::string BuildXCUIRHICommandSupportDiagnostic(
const XCUIRHICommandSupportStats& stats,
const XCUIRHICommandDiagnosticOptions& options = {}) {
if (!stats.HasCommands()) {
return std::string(options.noCommandsMessage);
}
if (stats.SupportsAllCommands()) {
return std::string(options.allSupportedMessage);
}
std::ostringstream builder = {};
builder << stats.unsupportedCommandCount << ' ' << options.unsupportedPrefix;
bool hasPreviousIssue = false;
AppendXCUIRHICommandDiagnosticIssue(
builder,
hasPreviousIssue,
stats.unsupportedImageCommandCount,
options.unsupportedImageDescription);
AppendXCUIRHICommandDiagnosticIssue(
builder,
hasPreviousIssue,
stats.unsupportedUnknownCommandCount,
options.unsupportedUnknownDescription);
if (!hasPreviousIssue) {
builder << "unsupported command(s)";
}
return builder.str();
}
inline XCUIRHICommandSupportSummary SummarizeXCUIRHICommandSupport(
const UI::UIDrawData& drawData,
const XCUIRHICommandDiagnosticOptions& options = {}) {
XCUIRHICommandSupportSummary summary = {};
summary.stats = AnalyzeXCUIRHICommandSupport(drawData);
summary.diagnostic = BuildXCUIRHICommandSupportDiagnostic(summary.stats, options);
return summary;
}
inline XCUIRHICommandSupportSummary SummarizeXCUIRHICommandSupport(
const UI::UIDrawList& drawList,
const XCUIRHICommandDiagnosticOptions& options = {}) {
XCUIRHICommandSupportSummary summary = {};
summary.stats = AnalyzeXCUIRHICommandSupport(drawList);
summary.diagnostic = BuildXCUIRHICommandSupportDiagnostic(summary.stats, options);
return summary;
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,582 @@
#include "XCUIBackend/XCUIRHIRenderBackend.h"
#include "XCUIBackend/XCUIRHICommandCompiler.h"
#include <XCEngine/Core/Math/Vector4.h>
#include <XCEngine/RHI/RHICommandList.h>
#include <XCEngine/RHI/RHIDevice.h>
#include <algorithm>
#include <cmath>
#include <optional>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
namespace {
using CommandCompiler = XCUIRHICommandCompiler;
struct OverlayConstants { Math::Vector4 viewportSize = Math::Vector4::Zero(); };
class TextGlyphProvider final : public CommandCompiler::TextGlyphProvider {
public:
TextGlyphProvider(
const IXCUITextAtlasProvider& atlasProvider,
IXCUITextAtlasProvider::FontHandle fontHandle,
const UI::UITextureHandle& textureHandle)
: m_atlasProvider(atlasProvider), m_fontHandle(fontHandle), m_textureHandle(textureHandle) {}
bool BeginText(float requestedFontSize, CommandCompiler::TextRunContext& outContext) const override {
outContext = {};
if (!m_fontHandle.IsValid() || !m_textureHandle.IsValid()) return false;
IXCUITextAtlasProvider::FontInfo fontInfo = {};
if (!m_atlasProvider.GetFontInfo(m_fontHandle, fontInfo)) return false;
const float resolvedFontSize = requestedFontSize > 0.0f
? requestedFontSize
: (fontInfo.nominalSize > 0.0f ? fontInfo.nominalSize : 16.0f);
IXCUITextAtlasProvider::BakedFontInfo baked = {};
if (!m_atlasProvider.GetBakedFontInfo(m_fontHandle, resolvedFontSize, baked)) return false;
outContext.requestedFontSize = requestedFontSize;
outContext.resolvedFontSize = resolvedFontSize;
outContext.lineHeight = baked.lineHeight;
outContext.texture = m_textureHandle;
return true;
}
bool ResolveGlyph(
const CommandCompiler::TextRunContext& context,
std::uint32_t codepoint,
CommandCompiler::TextGlyph& outGlyph) const override {
outGlyph = {};
IXCUITextAtlasProvider::GlyphInfo glyph = {};
if (!m_atlasProvider.FindGlyph(m_fontHandle, context.resolvedFontSize, codepoint, glyph)) return false;
outGlyph.x0 = glyph.x0; outGlyph.y0 = glyph.y0; outGlyph.x1 = glyph.x1; outGlyph.y1 = glyph.y1;
outGlyph.u0 = glyph.u0; outGlyph.v0 = glyph.v0; outGlyph.u1 = glyph.u1; outGlyph.v1 = glyph.v1;
outGlyph.advanceX = glyph.advanceX; outGlyph.visible = glyph.visible;
return true;
}
private:
const IXCUITextAtlasProvider& m_atlasProvider;
IXCUITextAtlasProvider::FontHandle m_fontHandle = {};
UI::UITextureHandle m_textureHandle = {};
};
constexpr char kColorShader[] =
"cbuffer OverlayConstants : register(b0) { float4 gViewportSize; };"
"struct VSInput { float2 position : POSITION; float4 color : COLOR0; };"
"struct VSOutput { float4 position : SV_POSITION; float4 color : COLOR0; };"
"VSOutput MainVS(VSInput input) { VSOutput output;"
" float2 viewport = max(gViewportSize.xy, float2(1.0, 1.0));"
" float2 ndc = float2(input.position.x / viewport.x * 2.0 - 1.0, 1.0 - input.position.y / viewport.y * 2.0);"
" output.position = float4(ndc, 0.0, 1.0); output.color = input.color; return output; }"
"float4 MainPS(VSOutput input) : SV_TARGET0 { return input.color; }";
constexpr char kTexturedShader[] =
"cbuffer OverlayConstants : register(b0) { float4 gViewportSize; };"
"Texture2D gOverlayTexture : register(t0); SamplerState gOverlaySampler : register(s0);"
"struct VSInput { float2 position : POSITION; float2 uv : TEXCOORD0; float4 color : COLOR0; };"
"struct VSOutput { float4 position : SV_POSITION; float2 uv : TEXCOORD0; float4 color : COLOR0; };"
"VSOutput MainVS(VSInput input) { VSOutput output;"
" float2 viewport = max(gViewportSize.xy, float2(1.0, 1.0));"
" float2 ndc = float2(input.position.x / viewport.x * 2.0 - 1.0, 1.0 - input.position.y / viewport.y * 2.0);"
" output.position = float4(ndc, 0.0, 1.0); output.uv = input.uv; output.color = input.color; return output; }"
"float4 MainPS(VSOutput input) : SV_TARGET0 {"
" const float4 sampled = gOverlayTexture.Sample(gOverlaySampler, input.uv);"
" float4 color = sampled * input.color; if (color.a <= 0.001f) { discard; } return color; }";
RHI::Rect MakeSurfaceScissorRect(const ::XCEngine::Rendering::RenderSurface& surface) {
return RHI::Rect{
0,
0,
static_cast<int32_t>(surface.GetWidth()),
static_cast<int32_t>(surface.GetHeight())
};
}
RHI::Rect ClampBatchClipRect(
const ::XCEngine::Rendering::RenderSurface& surface,
const UI::UIRect& clipRect) {
const float surfaceWidth = static_cast<float>(surface.GetWidth());
const float surfaceHeight = static_cast<float>(surface.GetHeight());
const float minX = (std::max)(0.0f, (std::min)(clipRect.x, surfaceWidth));
const float minY = (std::max)(0.0f, (std::min)(clipRect.y, surfaceHeight));
const float maxX = (std::max)(minX, (std::min)(clipRect.x + clipRect.width, surfaceWidth));
const float maxY = (std::max)(minY, (std::min)(clipRect.y + clipRect.height, surfaceHeight));
return RHI::Rect{
static_cast<int32_t>(std::floor(minX)),
static_cast<int32_t>(std::floor(minY)),
static_cast<int32_t>(std::ceil(maxX)),
static_cast<int32_t>(std::ceil(maxY))
};
}
bool RectEquals(const RHI::Rect& lhs, const RHI::Rect& rhs) {
return lhs.left == rhs.left &&
lhs.top == rhs.top &&
lhs.right == rhs.right &&
lhs.bottom == rhs.bottom;
}
} // namespace
void XCUIRHIRenderBackend::SetTextAtlasProvider(const IXCUITextAtlasProvider* provider) {
if (m_textAtlasProvider == provider) return;
m_textAtlasProvider = provider;
ResetFontAtlasResources();
}
void XCUIRHIRenderBackend::Shutdown() { DestroyResources(); }
void XCUIRHIRenderBackend::ResetStats() { m_lastOverlayStats = {}; }
bool XCUIRHIRenderBackend::Render(
const ::XCEngine::Rendering::RenderContext& renderContext,
const ::XCEngine::Rendering::RenderSurface& surface,
const ::XCEngine::UI::UIDrawData& drawData) {
ResetStats();
if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) return false;
const auto& colorAttachments = surface.GetColorAttachments();
if (colorAttachments.empty() || colorAttachments[0] == nullptr || renderContext.commandList == nullptr) return false;
if (!EnsureInitialized(renderContext)) return false;
bool fontAtlasReady = false;
const IXCUITextAtlasProvider* atlasProvider = ResolveActiveTextAtlasProvider(fontAtlasReady);
return RenderCompiledDrawData(
*renderContext.commandList,
colorAttachments[0],
surface,
drawData,
atlasProvider,
fontAtlasReady);
}
const IXCUITextAtlasProvider* XCUIRHIRenderBackend::ResolveActiveTextAtlasProvider(
bool& outFontAtlasReady) {
outFontAtlasReady = false;
if (m_textAtlasProvider == nullptr) {
return nullptr;
}
outFontAtlasReady = EnsureFontAtlasResources(*m_textAtlasProvider);
return m_textAtlasProvider;
}
bool XCUIRHIRenderBackend::EnsureInitialized(const ::XCEngine::Rendering::RenderContext& renderContext) {
if (m_overlayPipelineState != nullptr &&
m_overlayPipelineLayout != nullptr &&
m_overlayConstantPool != nullptr &&
m_overlayConstantSet != nullptr &&
m_texturedOverlayPipelineState != nullptr &&
m_texturedOverlayPipelineLayout != nullptr &&
m_overlayTexturePool != nullptr &&
m_overlaySamplerPool != nullptr &&
m_overlaySamplerSet != nullptr &&
m_overlaySampler != nullptr &&
m_device == renderContext.device &&
m_backendType == renderContext.backendType) {
return true;
}
DestroyResources();
return CreateResources(renderContext);
}
bool XCUIRHIRenderBackend::CreateResources(const ::XCEngine::Rendering::RenderContext& renderContext) {
if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) return false;
m_device = renderContext.device;
m_backendType = renderContext.backendType;
return CreateOverlayResources() && CreateTexturedOverlayResources();
}
bool XCUIRHIRenderBackend::CreateOverlayResources() {
RHI::DescriptorSetLayoutBinding binding = { 0, static_cast<uint32_t>(RHI::DescriptorType::CBV), 1 };
RHI::DescriptorSetLayoutDesc layout = { &binding, 1 };
RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {};
pipelineLayoutDesc.setLayouts = &layout;
pipelineLayoutDesc.setLayoutCount = 1;
m_overlayPipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc);
if (m_overlayPipelineLayout == nullptr) { DestroyResources(); return false; }
RHI::DescriptorPoolDesc poolDesc = {};
poolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV;
poolDesc.descriptorCount = 1;
poolDesc.shaderVisible = false;
m_overlayConstantPool = m_device->CreateDescriptorPool(poolDesc);
if (m_overlayConstantPool == nullptr) { DestroyResources(); return false; }
m_overlayConstantSet = m_overlayConstantPool->AllocateSet(layout);
if (m_overlayConstantSet == nullptr) { DestroyResources(); return false; }
RHI::GraphicsPipelineDesc desc = {};
desc.pipelineLayout = m_overlayPipelineLayout;
desc.topologyType = static_cast<uint32_t>(RHI::PrimitiveTopologyType::Triangle);
desc.renderTargetCount = 1;
desc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
desc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::Unknown);
desc.sampleCount = 1;
desc.inputLayout.elements = {
{ "POSITION", 0, static_cast<uint32_t>(RHI::Format::R32G32_Float), 0, 0, 0, 0 },
{ "COLOR", 0, static_cast<uint32_t>(RHI::Format::R32G32B32A32_Float), 0, 8, 0, 0 }
};
desc.rasterizerState.fillMode = static_cast<uint32_t>(RHI::FillMode::Solid);
desc.rasterizerState.cullMode = static_cast<uint32_t>(RHI::CullMode::None);
desc.rasterizerState.frontFace = static_cast<uint32_t>(RHI::FrontFace::CounterClockwise);
desc.rasterizerState.depthClipEnable = true;
desc.blendState.blendEnable = true;
desc.blendState.srcBlend = static_cast<uint32_t>(RHI::BlendFactor::SrcAlpha);
desc.blendState.dstBlend = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
desc.blendState.srcBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::One);
desc.blendState.dstBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
desc.blendState.blendOp = static_cast<uint32_t>(RHI::BlendOp::Add);
desc.blendState.blendOpAlpha = static_cast<uint32_t>(RHI::BlendOp::Add);
desc.blendState.colorWriteMask = 0xF;
desc.depthStencilState.depthTestEnable = false;
desc.depthStencilState.depthWriteEnable = false;
desc.depthStencilState.depthFunc = static_cast<uint32_t>(RHI::ComparisonFunc::Always);
desc.vertexShader.source.assign(kColorShader, kColorShader + sizeof(kColorShader) - 1);
desc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL;
desc.vertexShader.entryPoint = L"MainVS";
desc.vertexShader.profile = L"vs_5_0";
desc.fragmentShader.source.assign(kColorShader, kColorShader + sizeof(kColorShader) - 1);
desc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL;
desc.fragmentShader.entryPoint = L"MainPS";
desc.fragmentShader.profile = L"ps_5_0";
m_overlayPipelineState = m_device->CreatePipelineState(desc);
if (m_overlayPipelineState == nullptr || !m_overlayPipelineState->IsValid()) { DestroyResources(); return false; }
return true;
}
bool XCUIRHIRenderBackend::CreateTexturedOverlayResources() {
RHI::DescriptorSetLayoutBinding constantBinding = { 0, static_cast<uint32_t>(RHI::DescriptorType::CBV), 1 };
RHI::DescriptorSetLayoutBinding textureBinding = { 0, static_cast<uint32_t>(RHI::DescriptorType::SRV), 1 };
RHI::DescriptorSetLayoutBinding samplerBinding = { 0, static_cast<uint32_t>(RHI::DescriptorType::Sampler), 1 };
RHI::DescriptorSetLayoutDesc setLayouts[3] = { { &constantBinding, 1 }, { &textureBinding, 1 }, { &samplerBinding, 1 } };
RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {};
pipelineLayoutDesc.setLayouts = setLayouts;
pipelineLayoutDesc.setLayoutCount = 3;
m_texturedOverlayPipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc);
if (m_texturedOverlayPipelineLayout == nullptr) { DestroyResources(); return false; }
RHI::DescriptorPoolDesc texturePoolDesc = {};
texturePoolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV;
texturePoolDesc.descriptorCount = 64;
texturePoolDesc.shaderVisible = true;
m_overlayTexturePool = m_device->CreateDescriptorPool(texturePoolDesc);
if (m_overlayTexturePool == nullptr) { DestroyResources(); return false; }
RHI::DescriptorPoolDesc samplerPoolDesc = {};
samplerPoolDesc.type = RHI::DescriptorHeapType::Sampler;
samplerPoolDesc.descriptorCount = 1;
samplerPoolDesc.shaderVisible = true;
m_overlaySamplerPool = m_device->CreateDescriptorPool(samplerPoolDesc);
if (m_overlaySamplerPool == nullptr) { DestroyResources(); return false; }
m_overlaySamplerSet = m_overlaySamplerPool->AllocateSet(setLayouts[2]);
if (m_overlaySamplerSet == nullptr) { DestroyResources(); return false; }
RHI::SamplerDesc samplerDesc = {};
samplerDesc.filter = static_cast<uint32_t>(RHI::FilterMode::Linear);
samplerDesc.addressU = static_cast<uint32_t>(RHI::TextureAddressMode::Clamp);
samplerDesc.addressV = static_cast<uint32_t>(RHI::TextureAddressMode::Clamp);
samplerDesc.addressW = static_cast<uint32_t>(RHI::TextureAddressMode::Clamp);
samplerDesc.comparisonFunc = static_cast<uint32_t>(RHI::ComparisonFunc::Always);
samplerDesc.maxLod = 1000.0f;
m_overlaySampler = m_device->CreateSampler(samplerDesc);
if (m_overlaySampler == nullptr) { DestroyResources(); return false; }
m_overlaySamplerSet->UpdateSampler(0, m_overlaySampler);
RHI::GraphicsPipelineDesc desc = {};
desc.pipelineLayout = m_texturedOverlayPipelineLayout;
desc.topologyType = static_cast<uint32_t>(RHI::PrimitiveTopologyType::Triangle);
desc.renderTargetCount = 1;
desc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
desc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::Unknown);
desc.sampleCount = 1;
desc.inputLayout.elements = {
{ "POSITION", 0, static_cast<uint32_t>(RHI::Format::R32G32_Float), 0, 0, 0, 0 },
{ "TEXCOORD", 0, static_cast<uint32_t>(RHI::Format::R32G32_Float), 0, 8, 0, 0 },
{ "COLOR", 0, static_cast<uint32_t>(RHI::Format::R32G32B32A32_Float), 0, 16, 0, 0 }
};
desc.rasterizerState.fillMode = static_cast<uint32_t>(RHI::FillMode::Solid);
desc.rasterizerState.cullMode = static_cast<uint32_t>(RHI::CullMode::None);
desc.rasterizerState.frontFace = static_cast<uint32_t>(RHI::FrontFace::CounterClockwise);
desc.rasterizerState.depthClipEnable = true;
desc.blendState.blendEnable = true;
desc.blendState.srcBlend = static_cast<uint32_t>(RHI::BlendFactor::SrcAlpha);
desc.blendState.dstBlend = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
desc.blendState.srcBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::One);
desc.blendState.dstBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
desc.blendState.blendOp = static_cast<uint32_t>(RHI::BlendOp::Add);
desc.blendState.blendOpAlpha = static_cast<uint32_t>(RHI::BlendOp::Add);
desc.blendState.colorWriteMask = 0xF;
desc.depthStencilState.depthTestEnable = false;
desc.depthStencilState.depthWriteEnable = false;
desc.depthStencilState.depthFunc = static_cast<uint32_t>(RHI::ComparisonFunc::Always);
desc.vertexShader.source.assign(kTexturedShader, kTexturedShader + sizeof(kTexturedShader) - 1);
desc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL;
desc.vertexShader.entryPoint = L"MainVS";
desc.vertexShader.profile = L"vs_5_0";
desc.fragmentShader.source.assign(kTexturedShader, kTexturedShader + sizeof(kTexturedShader) - 1);
desc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL;
desc.fragmentShader.entryPoint = L"MainPS";
desc.fragmentShader.profile = L"ps_5_0";
m_texturedOverlayPipelineState = m_device->CreatePipelineState(desc);
if (m_texturedOverlayPipelineState == nullptr || !m_texturedOverlayPipelineState->IsValid()) { DestroyResources(); return false; }
return true;
}
bool XCUIRHIRenderBackend::EnsureOverlayVertexBufferCapacity(std::size_t requiredVertexCount) {
const std::uint64_t requiredBytes = static_cast<std::uint64_t>(requiredVertexCount * sizeof(CommandCompiler::ColorVertex));
if (requiredBytes == 0u) return true;
if (m_overlayVertexBuffer != nullptr && m_overlayVertexBufferCapacity >= requiredBytes) return true;
if (m_overlayVertexBufferView != nullptr) { m_overlayVertexBufferView->Shutdown(); delete m_overlayVertexBufferView; m_overlayVertexBufferView = nullptr; }
if (m_overlayVertexBuffer != nullptr) { m_overlayVertexBuffer->Shutdown(); delete m_overlayVertexBuffer; m_overlayVertexBuffer = nullptr; }
m_overlayVertexBufferCapacity = (std::max<std::uint64_t>)(requiredBytes, 4096u);
RHI::BufferDesc bufferDesc = {};
bufferDesc.size = m_overlayVertexBufferCapacity;
bufferDesc.stride = static_cast<std::uint32_t>(sizeof(CommandCompiler::ColorVertex));
bufferDesc.bufferType = static_cast<uint32_t>(RHI::BufferType::Vertex);
m_overlayVertexBuffer = m_device->CreateBuffer(bufferDesc);
if (m_overlayVertexBuffer == nullptr) { m_overlayVertexBufferCapacity = 0u; return false; }
m_overlayVertexBuffer->SetStride(bufferDesc.stride);
m_overlayVertexBuffer->SetBufferType(RHI::BufferType::Vertex);
RHI::ResourceViewDesc viewDesc = {};
viewDesc.dimension = RHI::ResourceViewDimension::Buffer;
viewDesc.structureByteStride = bufferDesc.stride;
m_overlayVertexBufferView = m_device->CreateVertexBufferView(m_overlayVertexBuffer, viewDesc);
return m_overlayVertexBufferView != nullptr;
}
bool XCUIRHIRenderBackend::EnsureTexturedOverlayVertexBufferCapacity(std::size_t requiredVertexCount) {
const std::uint64_t requiredBytes = static_cast<std::uint64_t>(requiredVertexCount * sizeof(CommandCompiler::TexturedVertex));
if (requiredBytes == 0u) return true;
if (m_texturedOverlayVertexBuffer != nullptr && m_texturedOverlayVertexBufferCapacity >= requiredBytes) return true;
if (m_texturedOverlayVertexBufferView != nullptr) { m_texturedOverlayVertexBufferView->Shutdown(); delete m_texturedOverlayVertexBufferView; m_texturedOverlayVertexBufferView = nullptr; }
if (m_texturedOverlayVertexBuffer != nullptr) { m_texturedOverlayVertexBuffer->Shutdown(); delete m_texturedOverlayVertexBuffer; m_texturedOverlayVertexBuffer = nullptr; }
m_texturedOverlayVertexBufferCapacity = (std::max<std::uint64_t>)(requiredBytes, 4096u);
RHI::BufferDesc bufferDesc = {};
bufferDesc.size = m_texturedOverlayVertexBufferCapacity;
bufferDesc.stride = static_cast<std::uint32_t>(sizeof(CommandCompiler::TexturedVertex));
bufferDesc.bufferType = static_cast<uint32_t>(RHI::BufferType::Vertex);
m_texturedOverlayVertexBuffer = m_device->CreateBuffer(bufferDesc);
if (m_texturedOverlayVertexBuffer == nullptr) { m_texturedOverlayVertexBufferCapacity = 0u; return false; }
m_texturedOverlayVertexBuffer->SetStride(bufferDesc.stride);
m_texturedOverlayVertexBuffer->SetBufferType(RHI::BufferType::Vertex);
RHI::ResourceViewDesc viewDesc = {};
viewDesc.dimension = RHI::ResourceViewDimension::Buffer;
viewDesc.structureByteStride = bufferDesc.stride;
m_texturedOverlayVertexBufferView = m_device->CreateVertexBufferView(m_texturedOverlayVertexBuffer, viewDesc);
return m_texturedOverlayVertexBufferView != nullptr;
}
bool XCUIRHIRenderBackend::EnsureFontAtlasResources(const IXCUITextAtlasProvider& atlasProvider) {
if (m_overlayTexturePool == nullptr) return false;
IXCUITextAtlasProvider::AtlasTextureView atlasView = {};
if (!atlasProvider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView) ||
!atlasView.IsValid() || atlasView.bytesPerPixel != 4) {
return false;
}
const TextFontHandle font = atlasProvider.GetDefaultFont();
if (!font.IsValid()) return false;
if (m_overlayFontTexture != nullptr && m_overlayFontTextureView != nullptr && m_overlayFontTextureSet != nullptr &&
m_overlayFont.value == font.value &&
m_fontAtlasStorageKey == atlasView.atlasStorageKey &&
m_fontAtlasPixelDataKey == atlasView.pixelDataKey) {
return true;
}
ResetFontAtlasResources();
RHI::TextureDesc textureDesc = {};
textureDesc.width = static_cast<std::uint32_t>(atlasView.width);
textureDesc.height = static_cast<std::uint32_t>(atlasView.height);
textureDesc.depth = 1; textureDesc.mipLevels = 1; textureDesc.arraySize = 1;
textureDesc.format = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
textureDesc.textureType = static_cast<uint32_t>(RHI::TextureType::Texture2D);
textureDesc.sampleCount = 1;
m_overlayFontTexture = m_device->CreateTexture(
textureDesc,
atlasView.pixels,
static_cast<std::size_t>(atlasView.width) * static_cast<std::size_t>(atlasView.height) * static_cast<std::size_t>(atlasView.bytesPerPixel),
static_cast<std::uint32_t>(atlasView.stride));
if (m_overlayFontTexture == nullptr) { ResetFontAtlasResources(); return false; }
RHI::ResourceViewDesc viewDesc = {};
viewDesc.format = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
viewDesc.dimension = RHI::ResourceViewDimension::Texture2D;
viewDesc.mipLevel = 0;
m_overlayFontTextureView = m_device->CreateShaderResourceView(m_overlayFontTexture, viewDesc);
if (m_overlayFontTextureView == nullptr) { ResetFontAtlasResources(); return false; }
RHI::DescriptorSetLayoutBinding textureBinding = { 0, static_cast<uint32_t>(RHI::DescriptorType::SRV), 1 };
RHI::DescriptorSetLayoutDesc textureLayout = { &textureBinding, 1 };
m_overlayFontTextureSet = m_overlayTexturePool->AllocateSet(textureLayout);
if (m_overlayFontTextureSet == nullptr) { ResetFontAtlasResources(); return false; }
m_overlayFontTextureSet->Update(0, m_overlayFontTextureView);
m_overlayFont = font;
m_overlayFontTextureHandle.nativeHandle = reinterpret_cast<std::uintptr_t>(m_overlayFontTextureView);
m_overlayFontTextureHandle.width = static_cast<std::uint32_t>(atlasView.width);
m_overlayFontTextureHandle.height = static_cast<std::uint32_t>(atlasView.height);
m_overlayFontTextureHandle.kind = UI::UITextureHandleKind::ShaderResourceView;
m_fontAtlasStorageKey = atlasView.atlasStorageKey;
m_fontAtlasPixelDataKey = atlasView.pixelDataKey;
return true;
}
::XCEngine::RHI::RHIDescriptorSet* XCUIRHIRenderBackend::ResolveTextureSet(
const ::XCEngine::UI::UITextureHandle& texture,
bool* outCacheHit) {
if (outCacheHit != nullptr) {
*outCacheHit = false;
}
if (!texture.IsValid() || texture.kind != UI::UITextureHandleKind::ShaderResourceView || m_overlayTexturePool == nullptr) return nullptr;
if (m_overlayFontTextureSet != nullptr && m_overlayFontTextureHandle.IsValid() && texture.nativeHandle == m_overlayFontTextureHandle.nativeHandle) {
if (outCacheHit != nullptr) {
*outCacheHit = true;
}
return m_overlayFontTextureSet;
}
for (const ExternalTextureBinding& binding : m_externalTextureBindings) {
if (binding.handleKey == texture.nativeHandle && binding.textureSet != nullptr && binding.shaderView != nullptr) {
if (outCacheHit != nullptr) {
*outCacheHit = true;
}
return binding.textureSet;
}
}
RHI::RHIResourceView* shaderView = reinterpret_cast<RHI::RHIResourceView*>(texture.nativeHandle);
if (shaderView == nullptr || !shaderView->IsValid() || shaderView->GetViewType() != RHI::ResourceViewType::ShaderResource) return nullptr;
RHI::DescriptorSetLayoutBinding textureBinding = { 0, static_cast<uint32_t>(RHI::DescriptorType::SRV), 1 };
RHI::DescriptorSetLayoutDesc textureLayout = { &textureBinding, 1 };
RHI::RHIDescriptorSet* textureSet = m_overlayTexturePool->AllocateSet(textureLayout);
if (textureSet == nullptr) return nullptr;
textureSet->Update(0, shaderView);
m_externalTextureBindings.push_back({ texture.nativeHandle, shaderView, textureSet });
return textureSet;
}
bool XCUIRHIRenderBackend::RenderCompiledDrawData(
::XCEngine::RHI::RHICommandList& commandList,
::XCEngine::RHI::RHIResourceView* renderTarget,
const ::XCEngine::Rendering::RenderSurface& surface,
const ::XCEngine::UI::UIDrawData& drawData,
const IXCUITextAtlasProvider* atlasProvider,
bool fontAtlasReady) {
CommandCompiler compiler = {};
CommandCompiler::CompileConfig config = {};
config.surfaceClipRect = UI::UIRect(0.0f, 0.0f, static_cast<float>(surface.GetWidth()), static_cast<float>(surface.GetHeight()));
std::optional<TextGlyphProvider> glyphProvider = std::nullopt;
if (fontAtlasReady && atlasProvider != nullptr) {
glyphProvider.emplace(*atlasProvider, m_overlayFont, m_overlayFontTextureHandle);
config.textGlyphProvider = &(*glyphProvider);
}
CommandCompiler::CompiledDrawData compiled = {};
compiler.Compile(drawData, config, compiled);
m_lastOverlayStats.drawListCount = compiled.stats.drawListCount;
m_lastOverlayStats.commandCount = compiled.stats.commandCount;
m_lastOverlayStats.batchCount = compiled.stats.batchCount;
m_lastOverlayStats.skippedCommandCount = compiled.stats.skippedCommandCount;
m_lastOverlayStats.vertexCount = compiled.stats.colorVertexCount + compiled.stats.texturedVertexCount;
m_lastOverlayStats.triangleCount = compiled.stats.triangleCount;
if (compiled.Empty()) return true;
if (!compiled.colorVertices.empty() && !EnsureOverlayVertexBufferCapacity(compiled.colorVertices.size())) return false;
if (!compiled.texturedVertices.empty() && !EnsureTexturedOverlayVertexBufferCapacity(compiled.texturedVertices.size())) return false;
if (!compiled.colorVertices.empty()) m_overlayVertexBuffer->SetData(compiled.colorVertices.data(), compiled.colorVertices.size() * sizeof(CommandCompiler::ColorVertex));
if (!compiled.texturedVertices.empty()) m_texturedOverlayVertexBuffer->SetData(compiled.texturedVertices.data(), compiled.texturedVertices.size() * sizeof(CommandCompiler::TexturedVertex));
OverlayConstants constants = {};
constants.viewportSize = Math::Vector4(static_cast<float>(surface.GetWidth()), static_cast<float>(surface.GetHeight()), 0.0f, 0.0f);
m_overlayConstantSet->WriteConstant(0, &constants, sizeof(constants));
commandList.SetRenderTargets(1, &renderTarget, nullptr);
const RHI::Viewport viewport = { 0.0f, 0.0f, static_cast<float>(surface.GetWidth()), static_cast<float>(surface.GetHeight()), 0.0f, 1.0f };
const RHI::Rect fullSurfaceScissorRect = MakeSurfaceScissorRect(surface);
commandList.SetViewport(viewport);
commandList.SetScissorRect(fullSurfaceScissorRect);
commandList.SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList);
RHI::Rect currentScissorRect = fullSurfaceScissorRect;
for (const CommandCompiler::Batch& batch : compiled.batches) {
if (batch.vertexCount == 0u) continue;
const RHI::Rect batchScissorRect = ClampBatchClipRect(surface, batch.clipRect);
if (!RectEquals(currentScissorRect, batchScissorRect)) {
commandList.SetScissorRect(batchScissorRect);
currentScissorRect = batchScissorRect;
}
if (!RectEquals(batchScissorRect, fullSurfaceScissorRect)) {
++m_lastOverlayStats.scissoredBatchCount;
}
if (batch.kind == CommandCompiler::BatchKind::Color) {
++m_lastOverlayStats.colorBatchCount;
m_lastOverlayStats.renderedCommandCount += batch.commandCount;
commandList.SetPipelineState(m_overlayPipelineState);
RHI::RHIResourceView* vertexBuffers[] = { m_overlayVertexBufferView };
const std::uint64_t offsets[] = { 0u };
const std::uint32_t strides[] = { static_cast<std::uint32_t>(sizeof(CommandCompiler::ColorVertex)) };
commandList.SetVertexBuffers(0, 1, vertexBuffers, offsets, strides);
RHI::RHIDescriptorSet* descriptorSets[] = { m_overlayConstantSet };
commandList.SetGraphicsDescriptorSets(0, 1, descriptorSets, m_overlayPipelineLayout);
commandList.Draw(batch.vertexCount, 1, batch.firstVertex, 0);
continue;
}
++m_lastOverlayStats.texturedBatchCount;
++m_lastOverlayStats.textureResolveCount;
bool textureCacheHit = false;
RHI::RHIDescriptorSet* textureSet = ResolveTextureSet(batch.texture, &textureCacheHit);
if (textureCacheHit) {
++m_lastOverlayStats.textureCacheHitCount;
}
if (textureSet == nullptr || m_overlaySamplerSet == nullptr) {
m_lastOverlayStats.skippedCommandCount += batch.commandCount;
continue;
}
m_lastOverlayStats.renderedCommandCount += batch.commandCount;
commandList.SetPipelineState(m_texturedOverlayPipelineState);
RHI::RHIResourceView* vertexBuffers[] = { m_texturedOverlayVertexBufferView };
const std::uint64_t offsets[] = { 0u };
const std::uint32_t strides[] = { static_cast<std::uint32_t>(sizeof(CommandCompiler::TexturedVertex)) };
commandList.SetVertexBuffers(0, 1, vertexBuffers, offsets, strides);
RHI::RHIDescriptorSet* descriptorSets[] = { m_overlayConstantSet, textureSet, m_overlaySamplerSet };
commandList.SetGraphicsDescriptorSets(0, 3, descriptorSets, m_texturedOverlayPipelineLayout);
commandList.Draw(batch.vertexCount, 1, batch.firstVertex, 0);
}
if (!RectEquals(currentScissorRect, fullSurfaceScissorRect)) {
commandList.SetScissorRect(fullSurfaceScissorRect);
}
return true;
}
void XCUIRHIRenderBackend::ResetFontAtlasResources() {
if (m_overlayFontTextureSet != nullptr) { m_overlayFontTextureSet->Shutdown(); delete m_overlayFontTextureSet; m_overlayFontTextureSet = nullptr; }
if (m_overlayFontTextureView != nullptr) { m_overlayFontTextureView->Shutdown(); delete m_overlayFontTextureView; m_overlayFontTextureView = nullptr; }
if (m_overlayFontTexture != nullptr) { m_overlayFontTexture->Shutdown(); delete m_overlayFontTexture; m_overlayFontTexture = nullptr; }
m_overlayFont = {};
m_overlayFontTextureHandle = {};
m_fontAtlasStorageKey = 0;
m_fontAtlasPixelDataKey = 0;
}
void XCUIRHIRenderBackend::DestroyResources() {
for (ExternalTextureBinding& binding : m_externalTextureBindings) {
if (binding.textureSet != nullptr) { binding.textureSet->Shutdown(); delete binding.textureSet; binding.textureSet = nullptr; }
binding.shaderView = nullptr;
binding.handleKey = 0;
}
m_externalTextureBindings.clear();
if (m_texturedOverlayVertexBufferView != nullptr) { m_texturedOverlayVertexBufferView->Shutdown(); delete m_texturedOverlayVertexBufferView; m_texturedOverlayVertexBufferView = nullptr; }
if (m_texturedOverlayVertexBuffer != nullptr) { m_texturedOverlayVertexBuffer->Shutdown(); delete m_texturedOverlayVertexBuffer; m_texturedOverlayVertexBuffer = nullptr; }
if (m_overlayVertexBufferView != nullptr) { m_overlayVertexBufferView->Shutdown(); delete m_overlayVertexBufferView; m_overlayVertexBufferView = nullptr; }
if (m_overlayVertexBuffer != nullptr) { m_overlayVertexBuffer->Shutdown(); delete m_overlayVertexBuffer; m_overlayVertexBuffer = nullptr; }
ResetFontAtlasResources();
if (m_texturedOverlayPipelineState != nullptr) { m_texturedOverlayPipelineState->Shutdown(); delete m_texturedOverlayPipelineState; m_texturedOverlayPipelineState = nullptr; }
if (m_overlaySamplerSet != nullptr) { m_overlaySamplerSet->Shutdown(); delete m_overlaySamplerSet; m_overlaySamplerSet = nullptr; }
if (m_overlaySampler != nullptr) { m_overlaySampler->Shutdown(); delete m_overlaySampler; m_overlaySampler = nullptr; }
if (m_overlaySamplerPool != nullptr) { m_overlaySamplerPool->Shutdown(); delete m_overlaySamplerPool; m_overlaySamplerPool = nullptr; }
if (m_overlayTexturePool != nullptr) { m_overlayTexturePool->Shutdown(); delete m_overlayTexturePool; m_overlayTexturePool = nullptr; }
if (m_texturedOverlayPipelineLayout != nullptr) { m_texturedOverlayPipelineLayout->Shutdown(); delete m_texturedOverlayPipelineLayout; m_texturedOverlayPipelineLayout = nullptr; }
if (m_overlayPipelineState != nullptr) { m_overlayPipelineState->Shutdown(); delete m_overlayPipelineState; m_overlayPipelineState = nullptr; }
if (m_overlayConstantSet != nullptr) { m_overlayConstantSet->Shutdown(); delete m_overlayConstantSet; m_overlayConstantSet = nullptr; }
if (m_overlayConstantPool != nullptr) { m_overlayConstantPool->Shutdown(); delete m_overlayConstantPool; m_overlayConstantPool = nullptr; }
if (m_overlayPipelineLayout != nullptr) { m_overlayPipelineLayout->Shutdown(); delete m_overlayPipelineLayout; m_overlayPipelineLayout = nullptr; }
m_overlayVertexBufferCapacity = 0u;
m_texturedOverlayVertexBufferCapacity = 0u;
m_lastOverlayStats = {};
m_device = nullptr;
m_backendType = RHI::RHIType::D3D12;
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,120 @@
#pragma once
#include "IXCUITextAtlasProvider.h"
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/RHI/RHIBuffer.h>
#include <XCEngine/RHI/RHICommandList.h>
#include <XCEngine/RHI/RHIDescriptorPool.h>
#include <XCEngine/RHI/RHIDescriptorSet.h>
#include <XCEngine/RHI/RHIEnums.h>
#include <XCEngine/RHI/RHIPipelineLayout.h>
#include <XCEngine/RHI/RHIPipelineState.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/RHI/RHISampler.h>
#include <XCEngine/RHI/RHITexture.h>
#include <XCEngine/UI/DrawData.h>
#include <cstddef>
#include <cstdint>
#include <vector>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
class XCUIRHIRenderBackend {
public:
struct OverlayStats {
std::size_t drawListCount = 0;
std::size_t commandCount = 0;
std::size_t batchCount = 0;
std::size_t colorBatchCount = 0;
std::size_t texturedBatchCount = 0;
std::size_t scissoredBatchCount = 0;
std::size_t renderedCommandCount = 0;
std::size_t skippedCommandCount = 0;
std::size_t textureResolveCount = 0;
std::size_t textureCacheHitCount = 0;
std::size_t vertexCount = 0;
std::size_t triangleCount = 0;
};
void SetTextAtlasProvider(const IXCUITextAtlasProvider* provider);
const IXCUITextAtlasProvider* GetTextAtlasProvider() const { return m_textAtlasProvider; }
void Shutdown();
void ResetStats();
bool Render(
const ::XCEngine::Rendering::RenderContext& renderContext,
const ::XCEngine::Rendering::RenderSurface& surface,
const ::XCEngine::UI::UIDrawData& drawData);
const OverlayStats& GetLastStats() const { return m_lastOverlayStats; }
const OverlayStats& GetLastOverlayStats() const { return m_lastOverlayStats; }
private:
using TextFontHandle = IXCUITextAtlasProvider::FontHandle;
struct ExternalTextureBinding {
std::uintptr_t handleKey = 0;
::XCEngine::RHI::RHIResourceView* shaderView = nullptr;
::XCEngine::RHI::RHIDescriptorSet* textureSet = nullptr;
};
bool EnsureInitialized(const ::XCEngine::Rendering::RenderContext& renderContext);
bool CreateResources(const ::XCEngine::Rendering::RenderContext& renderContext);
bool CreateOverlayResources();
bool CreateTexturedOverlayResources();
const IXCUITextAtlasProvider* ResolveActiveTextAtlasProvider(bool& outFontAtlasReady);
bool EnsureOverlayVertexBufferCapacity(std::size_t requiredVertexCount);
bool EnsureTexturedOverlayVertexBufferCapacity(std::size_t requiredVertexCount);
bool EnsureFontAtlasResources(const IXCUITextAtlasProvider& atlasProvider);
void ResetFontAtlasResources();
::XCEngine::RHI::RHIDescriptorSet* ResolveTextureSet(
const ::XCEngine::UI::UITextureHandle& texture,
bool* outCacheHit = nullptr);
bool RenderCompiledDrawData(
::XCEngine::RHI::RHICommandList& commandList,
::XCEngine::RHI::RHIResourceView* renderTarget,
const ::XCEngine::Rendering::RenderSurface& surface,
const ::XCEngine::UI::UIDrawData& drawData,
const IXCUITextAtlasProvider* atlasProvider,
bool fontAtlasReady);
void DestroyResources();
const IXCUITextAtlasProvider* m_textAtlasProvider = nullptr;
::XCEngine::RHI::RHIDevice* m_device = nullptr;
::XCEngine::RHI::RHIType m_backendType = ::XCEngine::RHI::RHIType::D3D12;
::XCEngine::RHI::RHIPipelineLayout* m_overlayPipelineLayout = nullptr;
::XCEngine::RHI::RHIDescriptorPool* m_overlayConstantPool = nullptr;
::XCEngine::RHI::RHIDescriptorSet* m_overlayConstantSet = nullptr;
::XCEngine::RHI::RHIPipelineState* m_overlayPipelineState = nullptr;
::XCEngine::RHI::RHIPipelineLayout* m_texturedOverlayPipelineLayout = nullptr;
::XCEngine::RHI::RHIDescriptorPool* m_overlayTexturePool = nullptr;
::XCEngine::RHI::RHIDescriptorSet* m_overlayFontTextureSet = nullptr;
::XCEngine::RHI::RHIDescriptorPool* m_overlaySamplerPool = nullptr;
::XCEngine::RHI::RHIDescriptorSet* m_overlaySamplerSet = nullptr;
::XCEngine::RHI::RHIPipelineState* m_texturedOverlayPipelineState = nullptr;
::XCEngine::RHI::RHISampler* m_overlaySampler = nullptr;
::XCEngine::RHI::RHITexture* m_overlayFontTexture = nullptr;
::XCEngine::RHI::RHIResourceView* m_overlayFontTextureView = nullptr;
::XCEngine::UI::UITextureHandle m_overlayFontTextureHandle = {};
::XCEngine::RHI::RHIBuffer* m_overlayVertexBuffer = nullptr;
::XCEngine::RHI::RHIResourceView* m_overlayVertexBufferView = nullptr;
::XCEngine::RHI::RHIBuffer* m_texturedOverlayVertexBuffer = nullptr;
::XCEngine::RHI::RHIResourceView* m_texturedOverlayVertexBufferView = nullptr;
std::uint64_t m_overlayVertexBufferCapacity = 0;
std::uint64_t m_texturedOverlayVertexBufferCapacity = 0;
TextFontHandle m_overlayFont = {};
std::uintptr_t m_fontAtlasStorageKey = 0;
std::uintptr_t m_fontAtlasPixelDataKey = 0;
int m_fontAtlasWidth = 0;
int m_fontAtlasHeight = 0;
int m_fontAtlasStride = 0;
std::vector<ExternalTextureBinding> m_externalTextureBindings = {};
OverlayStats m_lastOverlayStats = {};
};
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,222 @@
#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h"
#include <imgui_internal.h>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
namespace {
IXCUITextAtlasProvider::FontHandle MakeFontHandle(const ImFont* font) {
IXCUITextAtlasProvider::FontHandle handle = {};
handle.value = reinterpret_cast<std::uintptr_t>(font);
return handle;
}
ImFont* ResolveFontHandle(IXCUITextAtlasProvider::FontHandle handle) {
return reinterpret_cast<ImFont*>(handle.value);
}
float ResolveNominalFontSize(const ImFont& font) {
return font.LegacySize > 0.0f ? font.LegacySize : 16.0f;
}
float ResolveRequestedFontSize(const ImFont& font, float requestedFontSize) {
return requestedFontSize > 0.0f ? requestedFontSize : ResolveNominalFontSize(font);
}
bool IsFontOwnedByAtlas(const ImFont* font, const ImFontAtlas* atlas) {
return font != nullptr && atlas != nullptr && font->OwnerAtlas == atlas;
}
ImFontBaked* ResolveBakedFont(
IXCUITextAtlasProvider::FontHandle fontHandle,
ImFontAtlas* atlas,
float requestedFontSize) {
ImFont* font = ResolveFontHandle(fontHandle);
if (!IsFontOwnedByAtlas(font, atlas)) {
return nullptr;
}
const float resolvedFontSize = ResolveRequestedFontSize(*font, requestedFontSize);
if (resolvedFontSize <= 0.0f) {
return nullptr;
}
return font->GetFontBaked(resolvedFontSize);
}
} // namespace
XCUIStandaloneTextAtlasProvider::XCUIStandaloneTextAtlasProvider() {
RebuildDefaultEditorAtlas();
}
void XCUIStandaloneTextAtlasProvider::Reset() {
m_atlas.Clear();
m_defaultFont = nullptr;
m_ready = false;
}
bool XCUIStandaloneTextAtlasProvider::RebuildDefaultEditorAtlas() {
Reset();
m_ready = BuildDefaultXCUIEditorFontAtlas(m_atlas, m_defaultFont);
return m_ready;
}
bool XCUIStandaloneTextAtlasProvider::IsReady() const {
return m_ready && ResolveDefaultFont() != nullptr;
}
bool XCUIStandaloneTextAtlasProvider::GetAtlasTextureView(
PixelFormat preferredFormat,
AtlasTextureView& outView) const {
outView = {};
ImFontAtlas* atlas = ResolveAtlas();
if (atlas == nullptr) {
return false;
}
unsigned char* pixels = nullptr;
int width = 0;
int height = 0;
int bytesPerPixel = 0;
PixelFormat resolvedFormat = preferredFormat;
switch (preferredFormat) {
case PixelFormat::Alpha8:
atlas->GetTexDataAsAlpha8(&pixels, &width, &height, &bytesPerPixel);
resolvedFormat = PixelFormat::Alpha8;
break;
case PixelFormat::RGBA32:
case PixelFormat::Unknown:
default:
atlas->GetTexDataAsRGBA32(&pixels, &width, &height, &bytesPerPixel);
resolvedFormat = PixelFormat::RGBA32;
break;
}
if (pixels == nullptr || width <= 0 || height <= 0 || bytesPerPixel <= 0) {
return false;
}
outView.pixels = pixels;
outView.width = width;
outView.height = height;
outView.stride = width * bytesPerPixel;
outView.bytesPerPixel = bytesPerPixel;
outView.format = resolvedFormat;
outView.atlasStorageKey = reinterpret_cast<std::uintptr_t>(atlas);
outView.pixelDataKey = reinterpret_cast<std::uintptr_t>(pixels);
return true;
}
std::size_t XCUIStandaloneTextAtlasProvider::GetFontCount() const {
const ImFontAtlas* atlas = ResolveAtlas();
return atlas != nullptr ? static_cast<std::size_t>(atlas->Fonts.Size) : 0u;
}
IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetFont(std::size_t index) const {
const ImFontAtlas* atlas = ResolveAtlas();
if (atlas == nullptr || index >= static_cast<std::size_t>(atlas->Fonts.Size)) {
return {};
}
return MakeFontHandle(atlas->Fonts[static_cast<int>(index)]);
}
IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetDefaultFont() const {
return MakeFontHandle(ResolveDefaultFont());
}
bool XCUIStandaloneTextAtlasProvider::GetFontInfo(FontHandle fontHandle, FontInfo& outInfo) const {
outInfo = {};
ImFontAtlas* atlas = ResolveAtlas();
ImFont* font = ResolveFontHandle(fontHandle);
if (!IsFontOwnedByAtlas(font, atlas)) {
return false;
}
outInfo.handle = fontHandle;
outInfo.nominalSize = ResolveNominalFontSize(*font);
return true;
}
bool XCUIStandaloneTextAtlasProvider::GetBakedFontInfo(
FontHandle fontHandle,
float fontSize,
BakedFontInfo& outInfo) const {
outInfo = {};
ImFontBaked* bakedFont = ResolveBakedFont(fontHandle, ResolveAtlas(), fontSize);
if (bakedFont == nullptr) {
return false;
}
outInfo.lineHeight = bakedFont->Size;
outInfo.ascent = bakedFont->Ascent;
outInfo.descent = bakedFont->Descent;
outInfo.rasterizerDensity = bakedFont->RasterizerDensity;
return true;
}
bool XCUIStandaloneTextAtlasProvider::FindGlyph(
FontHandle fontHandle,
float fontSize,
std::uint32_t codepoint,
GlyphInfo& outInfo) const {
outInfo = {};
if (codepoint > IM_UNICODE_CODEPOINT_MAX) {
return false;
}
ImFontBaked* bakedFont = ResolveBakedFont(fontHandle, ResolveAtlas(), fontSize);
if (bakedFont == nullptr) {
return false;
}
ImFontGlyph* glyph = bakedFont->FindGlyph(static_cast<ImWchar>(codepoint));
if (glyph == nullptr) {
return false;
}
outInfo.requestedCodepoint = codepoint;
outInfo.resolvedCodepoint = glyph->Codepoint;
outInfo.visible = glyph->Visible != 0;
outInfo.colored = glyph->Colored != 0;
outInfo.advanceX = glyph->AdvanceX;
outInfo.x0 = glyph->X0;
outInfo.y0 = glyph->Y0;
outInfo.x1 = glyph->X1;
outInfo.y1 = glyph->Y1;
outInfo.u0 = glyph->U0;
outInfo.v0 = glyph->V0;
outInfo.u1 = glyph->U1;
outInfo.v1 = glyph->V1;
return true;
}
::ImFontAtlas* XCUIStandaloneTextAtlasProvider::ResolveAtlas() const {
return m_ready ? &m_atlas : nullptr;
}
::ImFont* XCUIStandaloneTextAtlasProvider::ResolveDefaultFont() const {
ImFontAtlas* atlas = ResolveAtlas();
if (atlas == nullptr) {
return nullptr;
}
if (IsFontOwnedByAtlas(m_defaultFont, atlas)) {
return m_defaultFont;
}
return atlas->Fonts.empty() ? nullptr : atlas->Fonts[0];
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,42 @@
#pragma once
#include "IXCUITextAtlasProvider.h"
#include "XCUIEditorFontSetup.h"
#include <imgui.h>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
class XCUIStandaloneTextAtlasProvider final : public IXCUITextAtlasProvider {
public:
XCUIStandaloneTextAtlasProvider();
XCUIStandaloneTextAtlasProvider(const XCUIStandaloneTextAtlasProvider&) = delete;
XCUIStandaloneTextAtlasProvider& operator=(const XCUIStandaloneTextAtlasProvider&) = delete;
void Reset();
bool RebuildDefaultEditorAtlas();
bool IsReady() const;
bool GetAtlasTextureView(PixelFormat preferredFormat, AtlasTextureView& outView) const override;
std::size_t GetFontCount() const override;
FontHandle GetFont(std::size_t index) const override;
FontHandle GetDefaultFont() const override;
bool GetFontInfo(FontHandle font, FontInfo& outInfo) const override;
bool GetBakedFontInfo(FontHandle font, float fontSize, BakedFontInfo& outInfo) const override;
bool FindGlyph(FontHandle font, float fontSize, std::uint32_t codepoint, GlyphInfo& outInfo) const override;
private:
::ImFontAtlas* ResolveAtlas() const;
::ImFont* ResolveDefaultFont() const;
mutable ::ImFontAtlas m_atlas = {};
::ImFont* m_defaultFont = nullptr;
bool m_ready = false;
};
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

6
new_editor/src/main.cpp Normal file
View File

@@ -0,0 +1,6 @@
#include "Application.h"
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
XCEngine::NewEditor::Application app;
return app.Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,44 @@
#include "Panel.h"
#include <utility>
namespace XCEngine {
namespace NewEditor {
Panel::Panel(std::string name, bool visible)
: m_name(std::move(name))
, m_visible(visible) {
}
Panel::~Panel() = default;
const std::string& Panel::GetName() const {
return m_name;
}
void Panel::SetName(const std::string& name) {
m_name = name;
}
bool Panel::IsVisible() const {
return m_visible;
}
void Panel::SetVisible(bool visible) {
m_visible = visible;
}
void Panel::ToggleVisible() {
m_visible = !m_visible;
}
void Panel::RenderIfVisible() {
if (!m_visible) {
return;
}
Render();
}
} // namespace NewEditor
} // namespace XCEngine

View File

@@ -0,0 +1,29 @@
#pragma once
#include <string>
namespace XCEngine {
namespace NewEditor {
class Panel {
public:
explicit Panel(std::string name, bool visible = true);
virtual ~Panel();
const std::string& GetName() const;
void SetName(const std::string& name);
bool IsVisible() const;
void SetVisible(bool visible);
void ToggleVisible();
void RenderIfVisible();
virtual void Render() = 0;
private:
std::string m_name;
bool m_visible = true;
};
} // namespace NewEditor
} // namespace XCEngine

View File

@@ -0,0 +1,438 @@
#include "XCUIDemoPanel.h"
#include "XCUIBackend/ImGuiXCUIInputAdapter.h"
#include <XCEngine/UI/Types.h>
#include <imgui.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <string>
#include <utility>
namespace XCEngine {
namespace NewEditor {
namespace {
constexpr float kCanvasHudHeight = 82.0f;
constexpr float kCanvasHudPadding = 10.0f;
constexpr ImU32 kPreviewFrameColor = IM_COL32(54, 72, 94, 255);
constexpr ImU32 kPreviewPlaceholderFill = IM_COL32(18, 24, 32, 255);
constexpr ImU32 kPreviewPlaceholderText = IM_COL32(191, 205, 224, 255);
constexpr ImU32 kPreviewPlaceholderSubtleText = IM_COL32(132, 147, 170, 255);
constexpr char kPreviewDebugName[] = "XCUI Demo";
constexpr char kPreviewDebugSource[] = "new_editor.panels.xcui_demo";
UI::UIRect ToUIRect(const ImVec2& minPoint, const ImVec2& size) {
return UI::UIRect(minPoint.x, minPoint.y, size.x, size.y);
}
bool ContainsPoint(const UI::UIRect& rect, const UI::UIPoint& point) {
return point.x >= rect.x &&
point.y >= rect.y &&
point.x <= rect.x + rect.width &&
point.y <= rect.y + rect.height;
}
void DrawHostedPreviewFrame(
ImDrawList* drawList,
const ImVec2& minPoint,
const ImVec2& size) {
if (drawList == nullptr || size.x <= 1.0f || size.y <= 1.0f) {
return;
}
const ImVec2 maxPoint(minPoint.x + size.x, minPoint.y + size.y);
drawList->AddRect(minPoint, maxPoint, kPreviewFrameColor, 8.0f, 0, 1.0f);
}
void DrawHostedPreviewPlaceholder(
ImDrawList* drawList,
const ImVec2& minPoint,
const ImVec2& size,
const char* title,
const char* subtitle) {
if (drawList == nullptr || size.x <= 1.0f || size.y <= 1.0f) {
return;
}
const ImVec2 maxPoint(minPoint.x + size.x, minPoint.y + size.y);
drawList->AddRectFilled(minPoint, maxPoint, kPreviewPlaceholderFill, 8.0f);
drawList->AddRect(minPoint, maxPoint, kPreviewFrameColor, 8.0f, 0, 1.0f);
drawList->AddText(ImVec2(minPoint.x + 14.0f, minPoint.y + 14.0f), kPreviewPlaceholderText, title);
if (subtitle != nullptr && subtitle[0] != '\0') {
drawList->AddText(
ImVec2(minPoint.x + 14.0f, minPoint.y + 36.0f),
kPreviewPlaceholderSubtleText,
subtitle);
}
}
void DrawRectOverlay(
ImDrawList* drawList,
::XCEngine::Editor::XCUIBackend::XCUIDemoRuntime& runtime,
const std::string& elementId,
ImU32 color,
const char* label) {
if (drawList == nullptr || elementId.empty()) {
return;
}
UI::UIRect rect = {};
if (!runtime.TryGetElementRect(elementId, rect)) {
return;
}
const ImVec2 minPoint(rect.x, rect.y);
const ImVec2 maxPoint(rect.x + rect.width, rect.y + rect.height);
drawList->AddRect(minPoint, maxPoint, color, 6.0f, 0, 2.0f);
if (label != nullptr && label[0] != '\0') {
drawList->AddText(ImVec2(minPoint.x + 4.0f, minPoint.y + 4.0f), color, label);
}
}
const char* GetPreviewPathLabel(bool nativeHostedPreview) {
return nativeHostedPreview ? "native queued offscreen surface" : "legacy imgui transition";
}
const char* GetPreviewStateLabel(
bool nativeHostedPreview,
const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& previewStats,
bool hasHostedSurfaceDescriptor,
bool showHostedSurfaceImage) {
if (nativeHostedPreview) {
if (showHostedSurfaceImage) {
return "live";
}
if (previewStats.queuedToNativePass || hasHostedSurfaceDescriptor) {
return "warming";
}
return "awaiting submit";
}
return previewStats.presented ? "live" : "idle";
}
} // namespace
XCUIDemoPanel::XCUIDemoPanel(::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource)
: XCUIDemoPanel(inputSource, ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter()) {}
XCUIDemoPanel::XCUIDemoPanel(
::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource,
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter)
: Panel("XCUI Demo")
, m_inputSource(inputSource)
, m_previewPresenter(std::move(previewPresenter)) {
if (m_previewPresenter == nullptr) {
m_previewPresenter = ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter();
}
m_lastReloadSucceeded = m_runtime.ReloadDocuments();
}
void XCUIDemoPanel::SetHostedPreviewEnabled(bool enabled) {
m_hostedPreviewEnabled = enabled;
if (!m_hostedPreviewEnabled) {
m_lastPreviewStats = {};
}
}
const ::XCEngine::Editor::XCUIBackend::XCUIDemoFrameResult& XCUIDemoPanel::GetFrameResult() const {
return m_runtime.GetFrameResult();
}
const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& XCUIDemoPanel::GetLastPreviewStats() const {
return m_lastPreviewStats;
}
void XCUIDemoPanel::SetHostedPreviewPresenter(
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter) {
m_previewPresenter = std::move(previewPresenter);
if (m_previewPresenter == nullptr) {
m_previewPresenter = ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter();
}
m_lastPreviewStats = {};
}
bool XCUIDemoPanel::IsUsingNativeHostedPreview() const {
return m_previewPresenter != nullptr && m_previewPresenter->IsNativeQueued();
}
void XCUIDemoPanel::Render() {
ImGui::SetNextWindowSize(ImVec2(1040.0f, 720.0f), ImGuiCond_Appearing);
ImGui::SetNextWindowDockID(ImGui::GetID("XCNewEditorDockSpace"), ImGuiCond_Appearing);
bool open = true;
if (!ImGui::Begin(GetName().c_str(), &open)) {
ImGui::End();
if (!open) {
SetVisible(false);
}
return;
}
if (ImGui::Button("Reload Documents")) {
m_lastReloadSucceeded = m_runtime.ReloadDocuments();
}
ImGui::SameLine();
ImGui::Checkbox("Canvas HUD", &m_showCanvasHud);
ImGui::SameLine();
ImGui::Checkbox("Debug Rects", &m_showDebugRects);
ImGui::SameLine();
ImGui::TextUnformatted(m_lastReloadSucceeded ? "Reload: OK" : "Reload: Failed");
ImGui::Separator();
const float diagnosticsHeight = 232.0f;
const ImVec2 hostRegion = ImGui::GetContentRegionAvail();
const float canvasHeight = (std::max)(140.0f, hostRegion.y - diagnosticsHeight);
ImGui::BeginChild("XCUIDemoCanvasHost", ImVec2(0.0f, canvasHeight), true, ImGuiWindowFlags_NoScrollWithMouse);
const ImVec2 canvasHostMin = ImGui::GetCursorScreenPos();
const ImVec2 availableSize = ImGui::GetContentRegionAvail();
const float topInset = m_showCanvasHud ? (kCanvasHudHeight + kCanvasHudPadding) : 0.0f;
const ImVec2 canvasMin(canvasHostMin.x, canvasHostMin.y + topInset);
const ImVec2 canvasSize(
availableSize.x,
(std::max)(0.0f, availableSize.y - topInset));
const bool validCanvas = canvasSize.x > 1.0f && canvasSize.y > 1.0f;
const bool nativeHostedPreview = IsUsingNativeHostedPreview();
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor hostedSurfaceDescriptor = {};
const bool hasHostedSurfaceDescriptor =
nativeHostedPreview &&
m_previewPresenter != nullptr &&
m_previewPresenter->TryGetSurfaceDescriptor(kPreviewDebugName, hostedSurfaceDescriptor);
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage hostedSurfaceImage = {};
const bool showHostedSurfaceImage =
validCanvas &&
nativeHostedPreview &&
m_previewPresenter != nullptr &&
m_previewPresenter->TryGetSurfaceImage(kPreviewDebugName, hostedSurfaceImage);
const char* const previewPathLabel = GetPreviewPathLabel(nativeHostedPreview);
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (validCanvas) {
ImGui::SetCursorScreenPos(canvasMin);
if (showHostedSurfaceImage) {
ImGui::Image(hostedSurfaceImage.textureId, canvasSize, hostedSurfaceImage.uvMin, hostedSurfaceImage.uvMax);
DrawHostedPreviewFrame(drawList, canvasMin, canvasSize);
} else {
ImGui::InvisibleButton("##XCUIDemoCanvas", canvasSize);
const char* placeholderSubtitle =
nativeHostedPreview
? "Waiting for native queued render output to publish back into the sandbox panel."
: "Legacy ImGui transition path stays active until native offscreen preview is enabled.";
DrawHostedPreviewPlaceholder(
drawList,
canvasMin,
canvasSize,
nativeHostedPreview ? "Native XCUI preview pending" : "Legacy XCUI canvas host",
placeholderSubtitle);
}
} else {
ImGui::Dummy(ImVec2(0.0f, 0.0f));
}
::XCEngine::Editor::XCUIBackend::XCUIDemoInputState input = {};
::XCEngine::Editor::XCUIBackend::XCUIInputBridgeCaptureOptions bridgeOptions = {};
bridgeOptions.timestampNanoseconds = static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count());
const UI::UIRect canvasRect = ToUIRect(canvasMin, canvasSize);
::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot snapshot = {};
if (m_inputSource != nullptr) {
bridgeOptions.hasPointerInsideOverride = true;
bridgeOptions.windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
const UI::UIPoint pointerPosition = m_inputSource->GetPointerPosition();
bridgeOptions.pointerInsideOverride = validCanvas && ContainsPoint(canvasRect, pointerPosition);
snapshot = m_inputSource->CaptureSnapshot(bridgeOptions);
} else {
bridgeOptions.hasPointerInsideOverride = true;
bridgeOptions.pointerInsideOverride = validCanvas && ImGui::IsItemHovered();
bridgeOptions.windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
snapshot = ::XCEngine::Editor::XCUIBackend::ImGuiXCUIInputAdapter::CaptureSnapshot(
ImGui::GetIO(),
bridgeOptions);
}
if (!m_inputBridge.HasBaseline()) {
m_inputBridge.Prime(snapshot);
}
const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta frameDelta =
m_inputBridge.Translate(snapshot);
input.canvasRect = canvasRect;
input.pointerPosition = snapshot.pointerPosition;
input.pointerInside = snapshot.pointerInside;
input.pointerPressed = frameDelta.pointer.pressed[0];
input.pointerReleased = frameDelta.pointer.released[0];
input.pointerDown = snapshot.pointerButtonsDown[0];
input.windowFocused = snapshot.windowFocused;
input.shortcutPressed = false;
input.wantCaptureMouse = snapshot.wantCaptureMouse;
input.wantCaptureKeyboard = snapshot.wantCaptureKeyboard;
input.wantTextInput = snapshot.wantTextInput;
input.events = frameDelta.events;
const ::XCEngine::Editor::XCUIBackend::XCUIDemoFrameResult& frame = m_runtime.Update(input);
if (m_hostedPreviewEnabled && m_previewPresenter != nullptr) {
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame previewFrame = {};
previewFrame.drawData = &frame.drawData;
previewFrame.targetDrawList = drawList;
previewFrame.canvasRect = canvasRect;
previewFrame.logicalSize = UI::UISize(canvasRect.width, canvasRect.height);
previewFrame.debugName = kPreviewDebugName;
previewFrame.debugSource = kPreviewDebugSource;
m_previewPresenter->Present(previewFrame);
m_lastPreviewStats = m_previewPresenter->GetLastStats();
} else {
m_lastPreviewStats = {};
}
const ::XCEngine::Editor::XCUIBackend::XCUIDemoFrameStats& stats = frame.stats;
const char* const previewStateLabel = GetPreviewStateLabel(
nativeHostedPreview,
m_lastPreviewStats,
hasHostedSurfaceDescriptor,
showHostedSurfaceImage);
if (m_showCanvasHud) {
const ImVec2 hudMin(canvasHostMin.x + 8.0f, canvasHostMin.y + 8.0f);
const ImVec2 hudMax(
canvasHostMin.x + (std::min)(availableSize.x - 8.0f, 430.0f),
canvasHostMin.y + kCanvasHudHeight);
drawList->AddRectFilled(
hudMin,
hudMax,
IM_COL32(16, 22, 30, 220),
8.0f);
drawList->AddRect(
hudMin,
hudMax,
IM_COL32(52, 72, 96, 255),
8.0f,
0,
1.0f);
ImGui::SetCursorScreenPos(ImVec2(hudMin.x + 10.0f, hudMin.y + 8.0f));
ImGui::BeginGroup();
ImGui::TextUnformatted("XCUI Runtime");
ImGui::Text("%s | %s", previewPathLabel, previewStateLabel);
ImGui::TextUnformatted(stats.statusMessage.c_str());
ImGui::Text(
"Tree %llu | Elements %zu | Commands %zu",
static_cast<unsigned long long>(stats.treeGeneration),
stats.elementCount,
stats.commandCount);
ImGui::Text(
"Submit %zu/%zu | Flush %zu/%zu",
m_lastPreviewStats.submittedDrawListCount,
m_lastPreviewStats.submittedCommandCount,
m_lastPreviewStats.flushedDrawListCount,
m_lastPreviewStats.flushedCommandCount);
ImGui::EndGroup();
}
if (m_showDebugRects && validCanvas && (!nativeHostedPreview || showHostedSurfaceImage)) {
DrawRectOverlay(drawList, m_runtime, stats.hoveredElementId, IM_COL32(255, 195, 64, 255), "hover");
DrawRectOverlay(drawList, m_runtime, stats.focusedElementId, IM_COL32(64, 214, 255, 255), "focus");
DrawRectOverlay(drawList, m_runtime, "toggleAccent", IM_COL32(150, 255, 150, 160), "toggle");
}
ImGui::EndChild();
ImGui::Separator();
ImGui::BeginChild("XCUIDemoDiagnostics", ImVec2(0.0f, 0.0f), false, ImGuiWindowFlags_NoScrollbar);
ImGui::SeparatorText("Preview");
ImGui::Text("Path: %s | state: %s", previewPathLabel, previewStateLabel);
ImGui::Text(
"Presenter: presented %s | submit->native %s",
m_lastPreviewStats.presented ? "yes" : "no",
m_lastPreviewStats.queuedToNativePass ? "yes" : "no");
ImGui::Text(
"Submitted: %zu lists / %zu cmds | Flushed: %zu lists / %zu cmds",
m_lastPreviewStats.submittedDrawListCount,
m_lastPreviewStats.submittedCommandCount,
m_lastPreviewStats.flushedDrawListCount,
m_lastPreviewStats.flushedCommandCount);
ImGui::TextWrapped(
"Source: %s",
hasHostedSurfaceDescriptor && !hostedSurfaceDescriptor.debugSource.empty()
? hostedSurfaceDescriptor.debugSource.c_str()
: kPreviewDebugSource);
if (nativeHostedPreview) {
ImGui::Text(
"Surface descriptor: %s | image published: %s | queued frame index: %zu",
hasHostedSurfaceDescriptor ? "yes" : "no",
showHostedSurfaceImage ? "yes" : "no",
hasHostedSurfaceDescriptor ? hostedSurfaceDescriptor.queuedFrameIndex : 0u);
if (hasHostedSurfaceDescriptor) {
ImGui::Text(
"Surface: %ux%u | logical: %.0f x %.0f | rendered rect: %.0f, %.0f %.0f x %.0f",
hostedSurfaceDescriptor.image.surfaceWidth,
hostedSurfaceDescriptor.image.surfaceHeight,
hostedSurfaceDescriptor.logicalSize.width,
hostedSurfaceDescriptor.logicalSize.height,
hostedSurfaceDescriptor.image.renderedCanvasRect.x,
hostedSurfaceDescriptor.image.renderedCanvasRect.y,
hostedSurfaceDescriptor.image.renderedCanvasRect.width,
hostedSurfaceDescriptor.image.renderedCanvasRect.height);
} else {
ImGui::TextDisabled("No native surface descriptor has been published back yet.");
}
} else {
ImGui::TextDisabled("Legacy path renders directly into the panel draw list. No native surface descriptor exists.");
}
ImGui::SeparatorText("Runtime");
ImGui::Text("Status: %s", stats.statusMessage.c_str());
ImGui::Text(
"Tree gen: %llu | elements: %zu | dirty roots: %zu",
static_cast<unsigned long long>(stats.treeGeneration),
stats.elementCount,
stats.dirtyRootCount);
ImGui::Text(
"Draw lists: %zu | draw cmds: %zu | dependencies: %zu",
stats.drawListCount,
stats.commandCount,
stats.dependencyCount);
ImGui::Text(
"Hovered: %s | Focused: %s",
stats.hoveredElementId.empty() ? "none" : stats.hoveredElementId.c_str(),
stats.focusedElementId.empty() ? "none" : stats.focusedElementId.c_str());
ImGui::Text(
"Last command: %s | Accent: %s",
stats.lastCommandId.empty() ? "none" : stats.lastCommandId.c_str(),
stats.accentEnabled ? "on" : "off");
ImGui::Text("Canvas: %.0f x %.0f", input.canvasRect.width, input.canvasRect.height);
ImGui::SeparatorText("Input");
ImGui::Text(
"Pointer: %.0f, %.0f | inside %s | down %s | pressed %s | released %s",
input.pointerPosition.x,
input.pointerPosition.y,
input.pointerInside ? "yes" : "no",
input.pointerDown ? "yes" : "no",
input.pointerPressed ? "yes" : "no",
input.pointerReleased ? "yes" : "no");
ImGui::Text(
"Focus %s | capture mouse %s | capture keyboard %s | text input %s | events %zu",
input.windowFocused ? "yes" : "no",
input.wantCaptureMouse ? "yes" : "no",
input.wantCaptureKeyboard ? "yes" : "no",
input.wantTextInput ? "yes" : "no",
input.events.size());
ImGui::EndChild();
ImGui::End();
if (!open) {
SetVisible(false);
}
}
} // namespace NewEditor
} // namespace XCEngine

View File

@@ -0,0 +1,45 @@
#pragma once
#include "Panel.h"
#include "XCUIBackend/XCUIHostedPreviewPresenter.h"
#include "XCUIBackend/XCUIInputBridge.h"
#include "XCUIBackend/XCUIDemoRuntime.h"
#include <memory>
namespace XCEngine {
namespace NewEditor {
class XCUIDemoPanel : public Panel {
public:
explicit XCUIDemoPanel(
::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource = nullptr);
XCUIDemoPanel(
::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource,
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter);
~XCUIDemoPanel() override = default;
void Render() override;
void SetHostedPreviewEnabled(bool enabled);
void SetHostedPreviewPresenter(
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter);
bool IsHostedPreviewEnabled() const { return m_hostedPreviewEnabled; }
bool IsUsingNativeHostedPreview() const;
const ::XCEngine::Editor::XCUIBackend::XCUIDemoFrameResult& GetFrameResult() const;
const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& GetLastPreviewStats() const;
private:
bool m_lastReloadSucceeded = false;
bool m_hostedPreviewEnabled = true;
bool m_showCanvasHud = true;
bool m_showDebugRects = true;
::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* m_inputSource = nullptr;
::XCEngine::Editor::XCUIBackend::XCUIInputBridge m_inputBridge;
::XCEngine::Editor::XCUIBackend::XCUIDemoRuntime m_runtime;
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> m_previewPresenter;
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats m_lastPreviewStats = {};
};
} // namespace NewEditor
} // namespace XCEngine

View File

@@ -0,0 +1,347 @@
#include "XCUILayoutLabPanel.h"
#include <XCEngine/UI/Types.h>
#include <imgui.h>
#include <algorithm>
#include <utility>
namespace XCEngine {
namespace NewEditor {
namespace {
constexpr ImU32 kPreviewFrameColor = IM_COL32(54, 72, 94, 255);
constexpr ImU32 kPreviewPlaceholderFill = IM_COL32(18, 24, 32, 255);
constexpr ImU32 kPreviewPlaceholderText = IM_COL32(191, 205, 224, 255);
constexpr ImU32 kPreviewPlaceholderSubtleText = IM_COL32(132, 147, 170, 255);
constexpr char kPreviewDebugName[] = "XCUI Layout Lab";
constexpr char kPreviewDebugSource[] = "new_editor.panels.xcui_layout_lab";
UI::UIRect ToUIRect(const ImVec2& minPoint, const ImVec2& size) {
return UI::UIRect(minPoint.x, minPoint.y, size.x, size.y);
}
bool ContainsPoint(const UI::UIRect& rect, const UI::UIPoint& point) {
return point.x >= rect.x &&
point.y >= rect.y &&
point.x <= rect.x + rect.width &&
point.y <= rect.y + rect.height;
}
void DrawHostedPreviewFrame(
ImDrawList* drawList,
const ImVec2& minPoint,
const ImVec2& size) {
if (drawList == nullptr || size.x <= 1.0f || size.y <= 1.0f) {
return;
}
const ImVec2 maxPoint(minPoint.x + size.x, minPoint.y + size.y);
drawList->AddRect(minPoint, maxPoint, kPreviewFrameColor, 8.0f, 0, 1.0f);
}
void DrawHostedPreviewPlaceholder(
ImDrawList* drawList,
const ImVec2& minPoint,
const ImVec2& size,
const char* title,
const char* subtitle) {
if (drawList == nullptr || size.x <= 1.0f || size.y <= 1.0f) {
return;
}
const ImVec2 maxPoint(minPoint.x + size.x, minPoint.y + size.y);
drawList->AddRectFilled(minPoint, maxPoint, kPreviewPlaceholderFill, 8.0f);
drawList->AddRect(minPoint, maxPoint, kPreviewFrameColor, 8.0f, 0, 1.0f);
drawList->AddText(ImVec2(minPoint.x + 14.0f, minPoint.y + 14.0f), kPreviewPlaceholderText, title);
if (subtitle != nullptr && subtitle[0] != '\0') {
drawList->AddText(
ImVec2(minPoint.x + 14.0f, minPoint.y + 36.0f),
kPreviewPlaceholderSubtleText,
subtitle);
}
}
void DrawCanvasBadge(
ImDrawList* drawList,
const ImVec2& minPoint,
const char* title,
const char* subtitle) {
if (drawList == nullptr) {
return;
}
const ImVec2 badgeMin(minPoint.x + 10.0f, minPoint.y + 10.0f);
const ImVec2 badgeMax(minPoint.x + 300.0f, minPoint.y + 52.0f);
drawList->AddRectFilled(badgeMin, badgeMax, IM_COL32(16, 22, 30, 216), 8.0f);
drawList->AddRect(badgeMin, badgeMax, IM_COL32(52, 72, 96, 255), 8.0f, 0, 1.0f);
drawList->AddText(ImVec2(badgeMin.x + 10.0f, badgeMin.y + 8.0f), kPreviewPlaceholderText, title);
drawList->AddText(
ImVec2(badgeMin.x + 10.0f, badgeMin.y + 28.0f),
kPreviewPlaceholderSubtleText,
subtitle);
}
const char* GetPreviewPathLabel(bool nativeHostedPreview) {
return nativeHostedPreview ? "native queued offscreen surface" : "legacy imgui transition";
}
const char* GetPreviewStateLabel(
bool nativeHostedPreview,
const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& previewStats,
bool hasHostedSurfaceDescriptor,
bool showHostedSurfaceImage) {
if (nativeHostedPreview) {
if (showHostedSurfaceImage) {
return "live";
}
if (previewStats.queuedToNativePass || hasHostedSurfaceDescriptor) {
return "warming";
}
return "awaiting submit";
}
return previewStats.presented ? "live" : "idle";
}
} // namespace
XCUILayoutLabPanel::XCUILayoutLabPanel(::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource)
: XCUILayoutLabPanel(inputSource, ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter()) {}
XCUILayoutLabPanel::XCUILayoutLabPanel(
::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource,
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter)
: Panel("XCUI Layout Lab")
, m_inputSource(inputSource)
, m_previewPresenter(std::move(previewPresenter)) {
if (m_previewPresenter == nullptr) {
m_previewPresenter = ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter();
}
m_lastReloadSucceeded = m_runtime.ReloadDocuments();
}
void XCUILayoutLabPanel::SetHostedPreviewEnabled(bool enabled) {
m_hostedPreviewEnabled = enabled;
if (!m_hostedPreviewEnabled) {
m_lastPreviewStats = {};
}
}
const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& XCUILayoutLabPanel::GetFrameResult() const {
return m_runtime.GetFrameResult();
}
const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& XCUILayoutLabPanel::GetLastPreviewStats() const {
return m_lastPreviewStats;
}
void XCUILayoutLabPanel::SetHostedPreviewPresenter(
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter) {
m_previewPresenter = std::move(previewPresenter);
if (m_previewPresenter == nullptr) {
m_previewPresenter = ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter();
}
m_lastPreviewStats = {};
}
bool XCUILayoutLabPanel::IsUsingNativeHostedPreview() const {
return m_previewPresenter != nullptr && m_previewPresenter->IsNativeQueued();
}
void XCUILayoutLabPanel::Render() {
ImGui::SetNextWindowSize(ImVec2(960.0f, 720.0f), ImGuiCond_Appearing);
ImGui::SetNextWindowDockID(ImGui::GetID("XCNewEditorDockSpace"), ImGuiCond_Appearing);
bool open = true;
if (!ImGui::Begin(GetName().c_str(), &open)) {
ImGui::End();
if (!open) {
SetVisible(false);
}
return;
}
if (ImGui::Button("Reload Layout Lab")) {
m_lastReloadSucceeded = m_runtime.ReloadDocuments();
}
ImGui::SameLine();
ImGui::TextUnformatted(m_lastReloadSucceeded ? "Reload: OK" : "Reload: Failed");
ImGui::Separator();
const float diagnosticsHeight = 240.0f;
const ImVec2 hostRegion = ImGui::GetContentRegionAvail();
const float canvasHeight = (std::max)(140.0f, hostRegion.y - diagnosticsHeight);
ImGui::BeginChild("XCUILayoutLabCanvasHost", ImVec2(0.0f, canvasHeight), true, ImGuiWindowFlags_NoScrollWithMouse);
const ImVec2 canvasMin = ImGui::GetCursorScreenPos();
const ImVec2 canvasSize = ImGui::GetContentRegionAvail();
const bool validCanvas = canvasSize.x > 1.0f && canvasSize.y > 1.0f;
const bool nativeHostedPreview = IsUsingNativeHostedPreview();
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor hostedSurfaceDescriptor = {};
const bool hasHostedSurfaceDescriptor =
nativeHostedPreview &&
m_previewPresenter != nullptr &&
m_previewPresenter->TryGetSurfaceDescriptor(kPreviewDebugName, hostedSurfaceDescriptor);
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage hostedSurfaceImage = {};
const bool showHostedSurfaceImage =
validCanvas &&
nativeHostedPreview &&
m_previewPresenter != nullptr &&
m_previewPresenter->TryGetSurfaceImage(kPreviewDebugName, hostedSurfaceImage);
const char* const previewPathLabel = GetPreviewPathLabel(nativeHostedPreview);
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (validCanvas) {
ImGui::SetCursorScreenPos(canvasMin);
if (showHostedSurfaceImage) {
ImGui::Image(hostedSurfaceImage.textureId, canvasSize, hostedSurfaceImage.uvMin, hostedSurfaceImage.uvMax);
DrawHostedPreviewFrame(drawList, canvasMin, canvasSize);
} else {
ImGui::InvisibleButton("##XCUILayoutLabCanvas", canvasSize);
const char* placeholderSubtitle =
nativeHostedPreview
? "Waiting for native queued render output to publish back into the layout sandbox."
: "Legacy ImGui transition path remains active until native offscreen preview is enabled.";
DrawHostedPreviewPlaceholder(
drawList,
canvasMin,
canvasSize,
nativeHostedPreview ? "Native layout preview pending" : "Legacy layout canvas host",
placeholderSubtitle);
}
DrawCanvasBadge(drawList, canvasMin, "Layout Lab", previewPathLabel);
} else {
ImGui::Dummy(ImVec2(0.0f, 0.0f));
}
::XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = {};
input.canvasRect = ToUIRect(canvasMin, canvasSize);
if (m_inputSource != nullptr) {
input.pointerPosition = m_inputSource->GetPointerPosition();
input.pointerInside = validCanvas && ContainsPoint(input.canvasRect, input.pointerPosition);
} else {
input.pointerPosition = UI::UIPoint(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y);
input.pointerInside = validCanvas && ImGui::IsItemHovered();
}
const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& frame = m_runtime.Update(input);
if (m_hostedPreviewEnabled && m_previewPresenter != nullptr) {
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame previewFrame = {};
previewFrame.drawData = &frame.drawData;
previewFrame.targetDrawList = ImGui::GetWindowDrawList();
previewFrame.canvasRect = input.canvasRect;
previewFrame.logicalSize = UI::UISize(input.canvasRect.width, input.canvasRect.height);
previewFrame.debugName = kPreviewDebugName;
previewFrame.debugSource = kPreviewDebugSource;
m_previewPresenter->Present(previewFrame);
m_lastPreviewStats = m_previewPresenter->GetLastStats();
} else {
m_lastPreviewStats = {};
}
const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameStats& stats = frame.stats;
const char* const previewStateLabel = GetPreviewStateLabel(
nativeHostedPreview,
m_lastPreviewStats,
hasHostedSurfaceDescriptor,
showHostedSurfaceImage);
ImGui::EndChild();
ImGui::Separator();
ImGui::BeginChild("XCUILayoutLabDiagnostics", ImVec2(0.0f, 0.0f), false, ImGuiWindowFlags_NoScrollbar);
ImGui::SeparatorText("Preview");
ImGui::Text("Path: %s | state: %s", previewPathLabel, previewStateLabel);
ImGui::Text(
"Presenter: presented %s | submit->native %s",
m_lastPreviewStats.presented ? "yes" : "no",
m_lastPreviewStats.queuedToNativePass ? "yes" : "no");
ImGui::Text(
"Submitted: %zu lists / %zu cmds | Flushed: %zu lists / %zu cmds",
m_lastPreviewStats.submittedDrawListCount,
m_lastPreviewStats.submittedCommandCount,
m_lastPreviewStats.flushedDrawListCount,
m_lastPreviewStats.flushedCommandCount);
ImGui::TextWrapped(
"Source: %s",
hasHostedSurfaceDescriptor && !hostedSurfaceDescriptor.debugSource.empty()
? hostedSurfaceDescriptor.debugSource.c_str()
: kPreviewDebugSource);
if (nativeHostedPreview) {
ImGui::Text(
"Surface descriptor: %s | image published: %s | queued frame index: %zu",
hasHostedSurfaceDescriptor ? "yes" : "no",
showHostedSurfaceImage ? "yes" : "no",
hasHostedSurfaceDescriptor ? hostedSurfaceDescriptor.queuedFrameIndex : 0u);
if (hasHostedSurfaceDescriptor) {
ImGui::Text(
"Surface: %ux%u | logical: %.0f x %.0f | rendered rect: %.0f, %.0f %.0f x %.0f",
hostedSurfaceDescriptor.image.surfaceWidth,
hostedSurfaceDescriptor.image.surfaceHeight,
hostedSurfaceDescriptor.logicalSize.width,
hostedSurfaceDescriptor.logicalSize.height,
hostedSurfaceDescriptor.image.renderedCanvasRect.x,
hostedSurfaceDescriptor.image.renderedCanvasRect.y,
hostedSurfaceDescriptor.image.renderedCanvasRect.width,
hostedSurfaceDescriptor.image.renderedCanvasRect.height);
} else {
ImGui::TextDisabled("No native surface descriptor has been published back yet.");
}
} else {
ImGui::TextDisabled("Legacy path renders directly into the panel draw list. No native surface descriptor exists.");
}
ImGui::SeparatorText("Runtime");
ImGui::Text("Status: %s", stats.statusMessage.c_str());
ImGui::Text(
"Rows: %zu | Columns: %zu | Overlays: %zu | Scroll views: %zu",
stats.rowCount,
stats.columnCount,
stats.overlayCount,
stats.scrollViewCount);
ImGui::Text(
"Draw lists: %zu | Draw commands: %zu",
stats.drawListCount,
stats.commandCount);
ImGui::Text(
"Command types: fill %zu | outline %zu | text %zu | image %zu | clip %zu/%zu",
stats.filledRectCommandCount,
stats.rectOutlineCommandCount,
stats.textCommandCount,
stats.imageCommandCount,
stats.clipPushCommandCount,
stats.clipPopCommandCount);
ImGui::Text(
"Native overlay: %s | supported %zu | unsupported %zu",
stats.nativeOverlayReady ? "preflight OK" : "preflight issues",
stats.nativeSupportedCommandCount,
stats.nativeUnsupportedCommandCount);
ImGui::TextWrapped(
"Native note: %s",
stats.nativeOverlayStatusMessage.empty() ? "none" : stats.nativeOverlayStatusMessage.c_str());
ImGui::Text(
"Hovered: %s | canvas: %.0f x %.0f",
stats.hoveredElementId.empty() ? "none" : stats.hoveredElementId.c_str(),
input.canvasRect.width,
input.canvasRect.height);
ImGui::SeparatorText("Input");
ImGui::Text(
"Pointer: %.0f, %.0f | inside %s",
input.pointerPosition.x,
input.pointerPosition.y,
input.pointerInside ? "yes" : "no");
ImGui::EndChild();
ImGui::End();
if (!open) {
SetVisible(false);
}
}
} // namespace NewEditor
} // namespace XCEngine

View File

@@ -0,0 +1,42 @@
#pragma once
#include "Panel.h"
#include "XCUIBackend/XCUIHostedPreviewPresenter.h"
#include "XCUIBackend/XCUIInputBridge.h"
#include "XCUIBackend/XCUILayoutLabRuntime.h"
#include <memory>
namespace XCEngine {
namespace NewEditor {
class XCUILayoutLabPanel : public Panel {
public:
explicit XCUILayoutLabPanel(
::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource = nullptr);
XCUILayoutLabPanel(
::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource,
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter);
~XCUILayoutLabPanel() override = default;
void Render() override;
void SetHostedPreviewEnabled(bool enabled);
void SetHostedPreviewPresenter(
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter);
bool IsHostedPreviewEnabled() const { return m_hostedPreviewEnabled; }
bool IsUsingNativeHostedPreview() const;
const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& GetFrameResult() const;
const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& GetLastPreviewStats() const;
private:
bool m_lastReloadSucceeded = false;
bool m_hostedPreviewEnabled = true;
::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* m_inputSource = nullptr;
::XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime m_runtime;
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> m_previewPresenter;
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats m_lastPreviewStats = {};
};
} // namespace NewEditor
} // namespace XCEngine

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.15)
project(XCEngineTests)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ============================================================
@@ -43,6 +43,7 @@ add_subdirectory(RHI)
add_subdirectory(Resources)
add_subdirectory(Input)
add_subdirectory(Editor)
add_subdirectory(NewEditor)
if(WIN32)
find_program(XCENGINE_POWERSHELL_EXECUTABLE NAMES powershell pwsh REQUIRED)
@@ -78,4 +79,4 @@ add_custom_target(print_tests
COMMAND ${CMAKE_COMMAND} -E echo "===== XCEngine Test Suite ====="
COMMAND ${CMAKE_CTEST_COMMAND} -N
COMMENT "Available tests:"
)
)

View File

@@ -0,0 +1,508 @@
cmake_minimum_required(VERSION 3.15)
project(XCEngine_NewEditorTests)
file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCENGINE_TEST_REPO_ROOT_CMAKE)
function(xcengine_configure_new_editor_test_target target_name)
if(MSVC)
set_target_properties(${target_name} PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
COMPILE_PDB_NAME "${target_name}-compile"
COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb"
COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Debug"
COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Release"
COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/MinSizeRel"
COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/RelWithDebInfo"
VS_GLOBAL_UseMultiToolTask "false"
)
target_compile_options(${target_name} PRIVATE /FS /utf-8)
target_link_options(${target_name} PRIVATE
$<$<CONFIG:Debug,RelWithDebInfo>:/INCREMENTAL:NO>)
endif()
endfunction()
function(xcengine_discover_new_editor_gtests target_name)
include(GoogleTest)
gtest_discover_tests(${target_name}
DISCOVERY_MODE PRE_TEST)
endfunction()
set(NEW_EDITOR_RUNTIME_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIDemoRuntime.h
)
set(NEW_EDITOR_RUNTIME_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp
)
set(NEW_EDITOR_LAYOUT_LAB_RUNTIME_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h
)
set(NEW_EDITOR_LAYOUT_LAB_RUNTIME_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp
)
set(NEW_EDITOR_ASSET_DOCUMENT_SOURCE_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.h
)
set(NEW_EDITOR_ASSET_DOCUMENT_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.cpp
)
set(NEW_EDITOR_UI_DOCUMENT_COMPILER_SOURCE
${CMAKE_SOURCE_DIR}/engine/src/Resources/UI/UIDocumentCompiler.cpp
)
set(NEW_EDITOR_BACKEND_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiTransitionBackend.h
)
set(NEW_EDITOR_INPUT_BRIDGE_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIInputBridge.h
)
set(NEW_EDITOR_INPUT_BRIDGE_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIInputBridge.cpp
)
set(NEW_EDITOR_IMGUI_INPUT_ADAPTER_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.h
)
set(NEW_EDITOR_IMGUI_INPUT_ADAPTER_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.cpp
)
set(NEW_EDITOR_RHI_COMMAND_SUPPORT_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIRHICommandSupport.h
)
set(NEW_EDITOR_RHI_COMMAND_COMPILER_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIRHICommandCompiler.h
)
set(NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIRHICommandCompiler.cpp
)
set(NEW_EDITOR_RHI_RENDER_BACKEND_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIRHIRenderBackend.h
)
set(NEW_EDITOR_RHI_RENDER_BACKEND_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIRHIRenderBackend.cpp
)
set(NEW_EDITOR_IMGUI_TEXT_ATLAS_PROVIDER_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.h
)
set(NEW_EDITOR_IMGUI_TEXT_ATLAS_PROVIDER_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.cpp
)
set(NEW_EDITOR_FONT_SETUP_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIEditorFontSetup.h
)
set(NEW_EDITOR_FONT_SETUP_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIEditorFontSetup.cpp
)
set(NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h
)
set(NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp
)
set(NEW_EDITOR_HOSTED_PREVIEW_PRESENTER_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h
)
set(NEW_EDITOR_NATIVE_BACKDROP_RENDERER_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.h
)
if(EXISTS "${NEW_EDITOR_RUNTIME_HEADER}" AND
EXISTS "${NEW_EDITOR_RUNTIME_SOURCE}" AND
EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE_HEADER}" AND
EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}")
add_executable(new_editor_xcui_demo_runtime_tests
test_xcui_demo_runtime.cpp
${NEW_EDITOR_RUNTIME_SOURCE}
${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}
)
xcengine_configure_new_editor_test_target(new_editor_xcui_demo_runtime_tests)
target_link_libraries(new_editor_xcui_demo_runtime_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_xcui_demo_runtime_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
)
target_compile_definitions(new_editor_xcui_demo_runtime_tests PRIVATE
XCENGINE_NEW_EDITOR_REPO_ROOT="${XCENGINE_TEST_REPO_ROOT_CMAKE}"
)
xcengine_discover_new_editor_gtests(new_editor_xcui_demo_runtime_tests)
else()
message(STATUS "Skipping new_editor_xcui_demo_runtime_tests because XCUIDemoRuntime files are missing.")
endif()
if(EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE_HEADER}" AND
EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}" AND
EXISTS "${NEW_EDITOR_UI_DOCUMENT_COMPILER_SOURCE}" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_xcui_asset_document_source.cpp")
add_executable(new_editor_xcui_asset_document_source_tests
test_xcui_asset_document_source.cpp
${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}
${NEW_EDITOR_UI_DOCUMENT_COMPILER_SOURCE}
)
xcengine_configure_new_editor_test_target(new_editor_xcui_asset_document_source_tests)
target_link_libraries(new_editor_xcui_asset_document_source_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_xcui_asset_document_source_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
)
target_compile_definitions(new_editor_xcui_asset_document_source_tests PRIVATE
XCENGINE_NEW_EDITOR_REPO_ROOT="${XCENGINE_TEST_REPO_ROOT_CMAKE}"
)
include(GoogleTest)
gtest_discover_tests(new_editor_xcui_asset_document_source_tests)
else()
message(STATUS "Skipping new_editor_xcui_asset_document_source_tests because asset document source files or the test source are missing.")
endif()
if(EXISTS "${NEW_EDITOR_LAYOUT_LAB_RUNTIME_HEADER}" AND
EXISTS "${NEW_EDITOR_LAYOUT_LAB_RUNTIME_SOURCE}" AND
EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE_HEADER}" AND
EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}")
add_executable(new_editor_xcui_layout_lab_runtime_tests
test_xcui_layout_lab_runtime.cpp
${NEW_EDITOR_LAYOUT_LAB_RUNTIME_SOURCE}
${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}
)
xcengine_configure_new_editor_test_target(new_editor_xcui_layout_lab_runtime_tests)
target_link_libraries(new_editor_xcui_layout_lab_runtime_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_xcui_layout_lab_runtime_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
)
target_compile_definitions(new_editor_xcui_layout_lab_runtime_tests PRIVATE
XCENGINE_NEW_EDITOR_REPO_ROOT="${XCENGINE_TEST_REPO_ROOT_CMAKE}"
)
xcengine_discover_new_editor_gtests(new_editor_xcui_layout_lab_runtime_tests)
else()
message(STATUS "Skipping new_editor_xcui_layout_lab_runtime_tests because XCUILayoutLabRuntime files are missing.")
endif()
if(EXISTS "${NEW_EDITOR_BACKEND_HEADER}" AND EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp")
add_executable(new_editor_imgui_transition_backend_tests
test_new_editor_imgui_transition_backend.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp
)
xcengine_configure_new_editor_test_target(new_editor_imgui_transition_backend_tests)
target_link_libraries(new_editor_imgui_transition_backend_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
user32
comdlg32
)
target_include_directories(new_editor_imgui_transition_backend_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_BINARY_DIR}/_deps/imgui-src
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
)
xcengine_discover_new_editor_gtests(new_editor_imgui_transition_backend_tests)
else()
message(STATUS "Skipping new_editor_imgui_transition_backend_tests because backend or ImGui sources are missing.")
endif()
if(EXISTS "${NEW_EDITOR_HOSTED_PREVIEW_PRESENTER_HEADER}" AND EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp")
add_executable(new_editor_xcui_hosted_preview_presenter_tests
test_xcui_hosted_preview_presenter.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp
)
xcengine_configure_new_editor_test_target(new_editor_xcui_hosted_preview_presenter_tests)
target_link_libraries(new_editor_xcui_hosted_preview_presenter_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
user32
comdlg32
)
target_include_directories(new_editor_xcui_hosted_preview_presenter_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_BINARY_DIR}/_deps/imgui-src
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
)
xcengine_discover_new_editor_gtests(new_editor_xcui_hosted_preview_presenter_tests)
else()
message(STATUS "Skipping new_editor_xcui_hosted_preview_presenter_tests because presenter header or ImGui sources are missing.")
endif()
if(EXISTS "${NEW_EDITOR_NATIVE_BACKDROP_RENDERER_HEADER}")
add_executable(new_editor_native_backdrop_renderer_api_tests
test_main_window_native_backdrop_renderer_api.cpp
)
xcengine_configure_new_editor_test_target(new_editor_native_backdrop_renderer_api_tests)
target_link_libraries(new_editor_native_backdrop_renderer_api_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_native_backdrop_renderer_api_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_BINARY_DIR}/_deps/imgui-src
)
xcengine_discover_new_editor_gtests(new_editor_native_backdrop_renderer_api_tests)
else()
message(STATUS "Skipping new_editor_native_backdrop_renderer_api_tests because renderer header is missing.")
endif()
if(EXISTS "${NEW_EDITOR_INPUT_BRIDGE_HEADER}" AND
EXISTS "${NEW_EDITOR_INPUT_BRIDGE_SOURCE}")
add_executable(new_editor_xcui_input_bridge_tests
test_xcui_input_bridge.cpp
${NEW_EDITOR_INPUT_BRIDGE_SOURCE}
)
xcengine_configure_new_editor_test_target(new_editor_xcui_input_bridge_tests)
target_link_libraries(new_editor_xcui_input_bridge_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_xcui_input_bridge_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_BINARY_DIR}/_deps/imgui-src
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
)
xcengine_discover_new_editor_gtests(new_editor_xcui_input_bridge_tests)
else()
message(STATUS "Skipping new_editor_xcui_input_bridge_tests because input bridge files are missing.")
endif()
if(EXISTS "${NEW_EDITOR_IMGUI_INPUT_ADAPTER_HEADER}" AND
EXISTS "${NEW_EDITOR_IMGUI_INPUT_ADAPTER_SOURCE}" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_imgui_xcui_input_adapter.cpp" AND
EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp")
add_executable(new_editor_imgui_xcui_input_adapter_tests
test_imgui_xcui_input_adapter.cpp
${NEW_EDITOR_IMGUI_INPUT_ADAPTER_SOURCE}
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp
)
xcengine_configure_new_editor_test_target(new_editor_imgui_xcui_input_adapter_tests)
target_link_libraries(new_editor_imgui_xcui_input_adapter_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
user32
comdlg32
)
target_include_directories(new_editor_imgui_xcui_input_adapter_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_BINARY_DIR}/_deps/imgui-src
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
)
xcengine_discover_new_editor_gtests(new_editor_imgui_xcui_input_adapter_tests)
else()
message(STATUS "Skipping new_editor_imgui_xcui_input_adapter_tests because ImGui adapter files, test source, or ImGui sources are missing.")
endif()
if(EXISTS "${NEW_EDITOR_RHI_COMMAND_SUPPORT_HEADER}")
add_executable(new_editor_xcui_rhi_command_support_tests
test_xcui_rhi_command_support.cpp
)
xcengine_configure_new_editor_test_target(new_editor_xcui_rhi_command_support_tests)
target_link_libraries(new_editor_xcui_rhi_command_support_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_xcui_rhi_command_support_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
)
xcengine_discover_new_editor_gtests(new_editor_xcui_rhi_command_support_tests)
else()
message(STATUS "Skipping new_editor_xcui_rhi_command_support_tests because support helper header is missing.")
endif()
if(EXISTS "${NEW_EDITOR_RHI_RENDER_BACKEND_HEADER}" AND
EXISTS "${NEW_EDITOR_RHI_RENDER_BACKEND_SOURCE}" AND
EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_HEADER}" AND
EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE}" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_xcui_rhi_render_backend.cpp")
add_executable(new_editor_xcui_rhi_render_backend_tests
test_xcui_rhi_render_backend.cpp
${NEW_EDITOR_RHI_RENDER_BACKEND_SOURCE}
${NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE}
)
xcengine_configure_new_editor_test_target(new_editor_xcui_rhi_render_backend_tests)
target_link_libraries(new_editor_xcui_rhi_render_backend_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_xcui_rhi_render_backend_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
)
xcengine_discover_new_editor_gtests(new_editor_xcui_rhi_render_backend_tests)
else()
message(STATUS "Skipping new_editor_xcui_rhi_render_backend_tests because backend/compiler sources or the test source are missing.")
endif()
if(EXISTS "${NEW_EDITOR_IMGUI_TEXT_ATLAS_PROVIDER_HEADER}" AND
EXISTS "${NEW_EDITOR_IMGUI_TEXT_ATLAS_PROVIDER_SOURCE}" AND
EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp")
add_executable(new_editor_imgui_text_atlas_provider_tests
test_imgui_text_atlas_provider.cpp
${NEW_EDITOR_IMGUI_TEXT_ATLAS_PROVIDER_SOURCE}
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp
)
xcengine_configure_new_editor_test_target(new_editor_imgui_text_atlas_provider_tests)
target_link_libraries(new_editor_imgui_text_atlas_provider_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
user32
comdlg32
)
target_include_directories(new_editor_imgui_text_atlas_provider_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_BINARY_DIR}/_deps/imgui-src
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
)
xcengine_discover_new_editor_gtests(new_editor_imgui_text_atlas_provider_tests)
else()
message(STATUS "Skipping new_editor_imgui_text_atlas_provider_tests because atlas provider or ImGui sources are missing.")
endif()
if(EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_HEADER}" AND
EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE}" AND
EXISTS "${NEW_EDITOR_FONT_SETUP_HEADER}" AND
EXISTS "${NEW_EDITOR_FONT_SETUP_SOURCE}" AND
EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp")
add_executable(new_editor_xcui_standalone_text_atlas_provider_tests
test_xcui_standalone_text_atlas_provider.cpp
${NEW_EDITOR_FONT_SETUP_SOURCE}
${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE}
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp
)
xcengine_configure_new_editor_test_target(new_editor_xcui_standalone_text_atlas_provider_tests)
target_link_libraries(new_editor_xcui_standalone_text_atlas_provider_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
user32
comdlg32
)
target_include_directories(new_editor_xcui_standalone_text_atlas_provider_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_BINARY_DIR}/_deps/imgui-src
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
)
xcengine_discover_new_editor_gtests(new_editor_xcui_standalone_text_atlas_provider_tests)
else()
message(STATUS "Skipping new_editor_xcui_standalone_text_atlas_provider_tests because standalone atlas provider, font setup, or ImGui sources are missing.")
endif()
if(EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_HEADER}" AND
EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE}")
add_executable(new_editor_xcui_rhi_command_compiler_tests
test_xcui_rhi_command_compiler.cpp
${NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE}
)
xcengine_configure_new_editor_test_target(new_editor_xcui_rhi_command_compiler_tests)
target_link_libraries(new_editor_xcui_rhi_command_compiler_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_xcui_rhi_command_compiler_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
)
xcengine_discover_new_editor_gtests(new_editor_xcui_rhi_command_compiler_tests)
else()
message(STATUS "Skipping new_editor_xcui_rhi_command_compiler_tests because compiler files are missing.")
endif()

View File

@@ -0,0 +1,90 @@
#include <gtest/gtest.h>
#include "XCUIBackend/ImGuiTextAtlasProvider.h"
#include <imgui.h>
#include <cstdint>
namespace {
using XCEngine::Editor::XCUIBackend::IXCUITextAtlasProvider;
using XCEngine::Editor::XCUIBackend::ImGuiTextAtlasProvider;
class ImGuiContextScope {
public:
ImGuiContextScope() {
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
}
~ImGuiContextScope() {
ImGui::DestroyContext();
}
};
void BuildDefaultFontAtlas() {
ImGuiIO& io = ImGui::GetIO();
if (io.Fonts->Fonts.empty()) {
io.Fonts->AddFontDefault();
}
unsigned char* pixels = nullptr;
int width = 0;
int height = 0;
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
}
TEST(ImGuiTextAtlasProviderTest, ReturnsEmptyResultsWhenNoContextIsAvailable) {
ImGuiTextAtlasProvider provider = {};
IXCUITextAtlasProvider::AtlasTextureView atlasView = {};
IXCUITextAtlasProvider::FontInfo fontInfo = {};
IXCUITextAtlasProvider::BakedFontInfo bakedFontInfo = {};
IXCUITextAtlasProvider::GlyphInfo glyphInfo = {};
EXPECT_EQ(provider.GetContext(), nullptr);
EXPECT_FALSE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView));
EXPECT_EQ(provider.GetFontCount(), 0u);
EXPECT_FALSE(provider.GetDefaultFont().IsValid());
EXPECT_FALSE(provider.GetFontInfo({}, fontInfo));
EXPECT_FALSE(provider.GetBakedFontInfo({}, 14.0f, bakedFontInfo));
EXPECT_FALSE(provider.FindGlyph({}, 14.0f, 'A', glyphInfo));
}
TEST(ImGuiTextAtlasProviderTest, ExposesAtlasAndGlyphDataFromExplicitContext) {
ImGuiContextScope contextScope;
BuildDefaultFontAtlas();
ImGuiTextAtlasProvider provider(ImGui::GetCurrentContext());
IXCUITextAtlasProvider::AtlasTextureView atlasView = {};
ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView));
EXPECT_TRUE(atlasView.IsValid());
EXPECT_EQ(atlasView.format, IXCUITextAtlasProvider::PixelFormat::RGBA32);
EXPECT_GT(atlasView.atlasStorageKey, 0u);
EXPECT_GT(atlasView.pixelDataKey, 0u);
const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont();
ASSERT_TRUE(defaultFont.IsValid());
ASSERT_GE(provider.GetFontCount(), 1u);
IXCUITextAtlasProvider::FontInfo fontInfo = {};
ASSERT_TRUE(provider.GetFontInfo(defaultFont, fontInfo));
EXPECT_TRUE(fontInfo.handle.IsValid());
EXPECT_GT(fontInfo.nominalSize, 0.0f);
IXCUITextAtlasProvider::BakedFontInfo bakedFontInfo = {};
ASSERT_TRUE(provider.GetBakedFontInfo(defaultFont, 0.0f, bakedFontInfo));
EXPECT_GT(bakedFontInfo.lineHeight, 0.0f);
EXPECT_GT(bakedFontInfo.rasterizerDensity, 0.0f);
IXCUITextAtlasProvider::GlyphInfo glyphInfo = {};
ASSERT_TRUE(provider.FindGlyph(defaultFont, 0.0f, 'A', glyphInfo));
EXPECT_EQ(glyphInfo.requestedCodepoint, static_cast<std::uint32_t>('A'));
EXPECT_GT(glyphInfo.advanceX, 0.0f);
EXPECT_GE(glyphInfo.u1, glyphInfo.u0);
EXPECT_GE(glyphInfo.v1, glyphInfo.v0);
}
} // namespace

View File

@@ -0,0 +1,88 @@
#include <gtest/gtest.h>
#include "XCUIBackend/ImGuiXCUIInputAdapter.h"
#include <XCEngine/Input/InputTypes.h>
namespace {
using XCEngine::Editor::XCUIBackend::ImGuiXCUIInputAdapter;
using XCEngine::Editor::XCUIBackend::XCUIInputBridgeCaptureOptions;
using XCEngine::Input::KeyCode;
class ImGuiContextScope {
public:
ImGuiContextScope() {
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
}
~ImGuiContextScope() {
ImGui::DestroyContext();
}
};
void PrepareImGui(float width = 1024.0f, float height = 768.0f) {
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize = ImVec2(width, height);
io.DeltaTime = 1.0f / 60.0f;
}
} // namespace
TEST(ImGuiXCUIInputAdapterTest, CaptureSnapshotMapsImGuiStateIntoXCUIFrameSnapshot) {
ImGuiContextScope contextScope;
PrepareImGui(800.0f, 600.0f);
ImGuiIO& io = ImGui::GetIO();
io.WantCaptureMouse = true;
io.WantCaptureKeyboard = true;
io.WantTextInput = true;
io.AddMousePosEvent(120.0f, 72.0f);
io.AddMouseButtonEvent(0, true);
io.AddMouseWheelEvent(0.0f, 1.0f);
io.AddKeyEvent(ImGuiKey_LeftCtrl, true);
io.AddKeyEvent(ImGuiKey_P, true);
io.AddInputCharacter('p');
ImGui::NewFrame();
XCUIInputBridgeCaptureOptions options = {};
options.pointerOffset = XCEngine::UI::UIPoint(20.0f, 12.0f);
options.windowFocused = false;
options.timestampNanoseconds = 99u;
const auto snapshot = ImGuiXCUIInputAdapter::CaptureSnapshot(io, options);
ImGui::EndFrame();
EXPECT_FLOAT_EQ(snapshot.pointerPosition.x, 100.0f);
EXPECT_FLOAT_EQ(snapshot.pointerPosition.y, 60.0f);
EXPECT_TRUE(snapshot.pointerInside);
EXPECT_TRUE(snapshot.pointerButtonsDown[0]);
EXPECT_FLOAT_EQ(snapshot.wheelDelta.x, 0.0f);
EXPECT_FLOAT_EQ(snapshot.wheelDelta.y, 1.0f);
EXPECT_TRUE(snapshot.modifiers.control);
EXPECT_FALSE(snapshot.windowFocused);
EXPECT_TRUE(snapshot.wantCaptureMouse);
EXPECT_TRUE(snapshot.wantCaptureKeyboard);
EXPECT_TRUE(snapshot.wantTextInput);
EXPECT_EQ(snapshot.timestampNanoseconds, 99u);
EXPECT_TRUE(snapshot.IsKeyDown(static_cast<std::int32_t>(KeyCode::LeftCtrl)));
EXPECT_TRUE(snapshot.IsKeyDown(static_cast<std::int32_t>(KeyCode::P)));
ASSERT_EQ(snapshot.characters.size(), 1u);
EXPECT_EQ(snapshot.characters[0], static_cast<std::uint32_t>('p'));
}
TEST(ImGuiXCUIInputAdapterTest, MapKeyCodeReturnsXCUIKeyCodesForNamedKeys) {
EXPECT_EQ(
ImGuiXCUIInputAdapter::MapKeyCode(ImGuiKey_A),
static_cast<std::int32_t>(KeyCode::A));
EXPECT_EQ(
ImGuiXCUIInputAdapter::MapKeyCode(ImGuiKey_F12),
static_cast<std::int32_t>(KeyCode::F12));
EXPECT_EQ(
ImGuiXCUIInputAdapter::MapKeyCode(ImGuiKey_None),
static_cast<std::int32_t>(KeyCode::None));
}

View File

@@ -0,0 +1,32 @@
#include <gtest/gtest.h>
#include "Rendering/MainWindowNativeBackdropRenderer.h"
#include <type_traits>
namespace {
using XCEngine::NewEditor::MainWindowNativeBackdropRenderer;
using XCEngine::Rendering::RenderContext;
using XCEngine::Rendering::RenderSurface;
TEST(NewEditorMainWindowNativeBackdropRendererApiTest, ExposesFrameStateAndSurfaceRenderEntry) {
using FrameState = MainWindowNativeBackdropRenderer::FrameState;
static_assert(std::is_same_v<
decltype(std::declval<FrameState>().elapsedSeconds),
float>);
static_assert(std::is_same_v<
decltype(std::declval<FrameState>().pulseAccent),
bool>);
static_assert(std::is_same_v<
decltype(std::declval<MainWindowNativeBackdropRenderer&>().Render(
std::declval<const RenderContext&>(),
std::declval<const RenderSurface&>(),
std::declval<const FrameState&>())),
bool>);
SUCCEED();
}
} // namespace

View File

@@ -0,0 +1,306 @@
#include <gtest/gtest.h>
#include "XCUIBackend/ImGuiTransitionBackend.h"
#include <XCEngine/UI/DrawData.h>
#include <imgui.h>
namespace {
class ImGuiContextScope {
public:
ImGuiContextScope() {
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
}
~ImGuiContextScope() {
ImGui::DestroyContext();
}
};
void PrepareImGui(float width = 1024.0f, float height = 768.0f) {
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize = ImVec2(width, height);
io.DeltaTime = 1.0f / 60.0f;
unsigned char* fontPixels = nullptr;
int fontWidth = 0;
int fontHeight = 0;
io.Fonts->GetTexDataAsRGBA32(&fontPixels, &fontWidth, &fontHeight);
io.Fonts->SetTexID(static_cast<ImTextureID>(1));
}
} // namespace
TEST(NewEditorImGuiTransitionBackendTest, BeginFrameClearsPendingQueue) {
ImGuiContextScope contextScope;
XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {};
XCEngine::UI::UIDrawList drawList("Pending");
drawList.AddFilledRect(
XCEngine::UI::UIRect(0.0f, 0.0f, 20.0f, 20.0f),
XCEngine::UI::UIColor(1.0f, 0.0f, 0.0f, 1.0f));
backend.BeginFrame();
backend.Submit(drawList);
ASSERT_EQ(backend.GetPendingDrawListCount(), 1u);
ASSERT_EQ(backend.GetPendingCommandCount(), 1u);
backend.BeginFrame();
EXPECT_EQ(backend.GetPendingDrawListCount(), 0u);
EXPECT_EQ(backend.GetPendingCommandCount(), 0u);
}
TEST(NewEditorImGuiTransitionBackendTest, EndFrameFlushesSubmittedCommands) {
ImGuiContextScope contextScope;
PrepareImGui();
XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {};
XCEngine::UI::UIDrawList drawList("Flush");
drawList.PushClipRect(XCEngine::UI::UIRect(0.0f, 0.0f, 240.0f, 160.0f));
drawList.AddFilledRect(
XCEngine::UI::UIRect(10.0f, 10.0f, 70.0f, 45.0f),
XCEngine::UI::UIColor(0.2f, 0.4f, 0.6f, 1.0f),
6.0f);
drawList.AddRectOutline(
XCEngine::UI::UIRect(10.0f, 10.0f, 70.0f, 45.0f),
XCEngine::UI::UIColor(0.9f, 0.9f, 0.9f, 1.0f),
2.0f,
6.0f);
drawList.PopClipRect();
backend.BeginFrame();
backend.Submit(drawList);
ImGui::NewFrame();
ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendTest"));
ImDrawList* targetDrawList = ImGui::GetWindowDrawList();
ASSERT_NE(targetDrawList, nullptr);
const bool flushed = backend.EndFrame(targetDrawList);
ImGui::End();
ImGui::EndFrame();
EXPECT_TRUE(flushed);
EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 1u);
EXPECT_EQ(backend.GetLastFlushedCommandCount(), 4u);
EXPECT_EQ(backend.GetPendingCommandCount(), 0u);
EXPECT_GT(targetDrawList->VtxBuffer.Size, 0);
EXPECT_GT(targetDrawList->CmdBuffer.Size, 0);
}
TEST(NewEditorImGuiTransitionBackendTest, EndFrameFlushesMultipleDrawLists) {
ImGuiContextScope contextScope;
PrepareImGui(800.0f, 600.0f);
XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {};
XCEngine::UI::UIDrawList firstDrawList("First");
firstDrawList.AddFilledRect(
XCEngine::UI::UIRect(8.0f, 8.0f, 40.0f, 30.0f),
XCEngine::UI::UIColor(0.7f, 0.1f, 0.1f, 1.0f));
XCEngine::UI::UIDrawList secondDrawList("Second");
secondDrawList.PushClipRect(XCEngine::UI::UIRect(0.0f, 0.0f, 300.0f, 200.0f));
secondDrawList.AddRectOutline(
XCEngine::UI::UIRect(24.0f, 26.0f, 52.0f, 28.0f),
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
1.5f,
5.0f);
secondDrawList.PopClipRect();
backend.BeginFrame();
backend.Submit(firstDrawList);
backend.Submit(std::move(secondDrawList));
ASSERT_EQ(backend.GetPendingDrawListCount(), 2u);
ASSERT_EQ(backend.GetPendingCommandCount(), 4u);
ImGui::NewFrame();
ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendMultiDrawList"));
ImDrawList* targetDrawList = ImGui::GetWindowDrawList();
ASSERT_NE(targetDrawList, nullptr);
const bool flushed = backend.EndFrame(targetDrawList);
ImGui::End();
ImGui::EndFrame();
EXPECT_TRUE(flushed);
EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 2u);
EXPECT_EQ(backend.GetLastFlushedCommandCount(), 4u);
EXPECT_EQ(backend.GetPendingDrawListCount(), 0u);
EXPECT_EQ(backend.GetPendingCommandCount(), 0u);
EXPECT_GT(targetDrawList->CmdBuffer.Size, 0);
}
TEST(NewEditorImGuiTransitionBackendTest, EndFrameWithNoPendingDataLeavesFlushCountsAtZero) {
ImGuiContextScope contextScope;
PrepareImGui(640.0f, 480.0f);
XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {};
backend.BeginFrame();
ImGui::NewFrame();
ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendEmptyFrame"));
ImDrawList* targetDrawList = ImGui::GetWindowDrawList();
ASSERT_NE(targetDrawList, nullptr);
const bool flushed = backend.EndFrame(targetDrawList);
ImGui::End();
ImGui::EndFrame();
EXPECT_TRUE(flushed);
EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 0u);
EXPECT_EQ(backend.GetLastFlushedCommandCount(), 0u);
EXPECT_EQ(backend.GetPendingDrawListCount(), 0u);
EXPECT_EQ(backend.GetPendingCommandCount(), 0u);
}
TEST(NewEditorImGuiTransitionBackendTest, SubmitDrawDataAggregatesMultipleListsBeforeFlush) {
ImGuiContextScope contextScope;
PrepareImGui(720.0f, 512.0f);
XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {};
XCEngine::UI::UIDrawData drawData = {};
XCEngine::UI::UIDrawList& firstDrawList = drawData.EmplaceDrawList("First");
firstDrawList.AddFilledRect(
XCEngine::UI::UIRect(12.0f, 14.0f, 48.0f, 24.0f),
XCEngine::UI::UIColor(0.4f, 0.5f, 0.9f, 1.0f));
XCEngine::UI::UIDrawList& secondDrawList = drawData.EmplaceDrawList("Second");
secondDrawList.AddRectOutline(
XCEngine::UI::UIRect(36.0f, 42.0f, 64.0f, 28.0f),
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
2.0f,
3.0f);
secondDrawList.AddText(
XCEngine::UI::UIPoint(40.0f, 48.0f),
"xcui",
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
14.0f);
backend.BeginFrame();
backend.Submit(drawData);
ASSERT_EQ(backend.GetPendingDrawListCount(), 2u);
ASSERT_EQ(backend.GetPendingCommandCount(), 3u);
ImGui::NewFrame();
ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendDrawData"));
ImDrawList* targetDrawList = ImGui::GetWindowDrawList();
ASSERT_NE(targetDrawList, nullptr);
const bool flushed = backend.EndFrame(targetDrawList);
ImGui::End();
ImGui::EndFrame();
EXPECT_TRUE(flushed);
EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 2u);
EXPECT_EQ(backend.GetLastFlushedCommandCount(), 3u);
EXPECT_EQ(backend.GetPendingDrawListCount(), 0u);
EXPECT_EQ(backend.GetPendingCommandCount(), 0u);
}
TEST(NewEditorImGuiTransitionBackendTest, EndFrameAcceptsTextCommandsThatUseDefaultFontSize) {
ImGuiContextScope contextScope;
PrepareImGui(720.0f, 512.0f);
XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {};
XCEngine::UI::UIDrawList drawList("DefaultFontText");
drawList.AddText(
XCEngine::UI::UIPoint(24.0f, 30.0f),
"fallback font text",
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
0.0f);
backend.BeginFrame();
backend.Submit(drawList);
ImGui::NewFrame();
ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendDefaultFontText"));
ImDrawList* targetDrawList = ImGui::GetWindowDrawList();
ASSERT_NE(targetDrawList, nullptr);
const bool flushed = backend.EndFrame(targetDrawList);
ImGui::End();
ImGui::EndFrame();
EXPECT_TRUE(flushed);
EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 1u);
EXPECT_EQ(backend.GetLastFlushedCommandCount(), 1u);
EXPECT_EQ(backend.GetPendingDrawListCount(), 0u);
EXPECT_EQ(backend.GetPendingCommandCount(), 0u);
}
TEST(NewEditorImGuiTransitionBackendTest, EndFrameSkipsEmptyTextGeometryButClearsPendingState) {
ImGuiContextScope contextScope;
PrepareImGui(720.0f, 512.0f);
XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {};
XCEngine::UI::UIDrawList drawList("EmptyText");
drawList.AddText(
XCEngine::UI::UIPoint(24.0f, 30.0f),
"",
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
14.0f);
backend.BeginFrame();
backend.Submit(drawList);
ImGui::NewFrame();
ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendEmptyText"));
ImGui::TextUnformatted("anchor");
ImDrawList* targetDrawList = ImGui::GetWindowDrawList();
ASSERT_NE(targetDrawList, nullptr);
const int initialVertexCount = targetDrawList->VtxBuffer.Size;
const bool flushed = backend.EndFrame(targetDrawList);
ImGui::End();
ImGui::EndFrame();
EXPECT_TRUE(flushed);
EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 1u);
EXPECT_EQ(backend.GetLastFlushedCommandCount(), 1u);
EXPECT_EQ(targetDrawList->VtxBuffer.Size, initialVertexCount);
EXPECT_EQ(backend.GetPendingDrawListCount(), 0u);
EXPECT_EQ(backend.GetPendingCommandCount(), 0u);
}
TEST(NewEditorImGuiTransitionBackendTest, EndFrameRestoresClipRectStackAfterUnbalancedPush) {
ImGuiContextScope contextScope;
PrepareImGui(800.0f, 600.0f);
XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {};
XCEngine::UI::UIDrawList drawList("ClipRecovery");
drawList.PushClipRect(XCEngine::UI::UIRect(0.0f, 0.0f, 180.0f, 120.0f));
drawList.AddFilledRect(
XCEngine::UI::UIRect(16.0f, 18.0f, 52.0f, 30.0f),
XCEngine::UI::UIColor(0.9f, 0.3f, 0.2f, 1.0f));
backend.BeginFrame();
backend.Submit(drawList);
ImGui::NewFrame();
ASSERT_TRUE(ImGui::Begin("NewEditorXCUIBackendClipRecovery"));
ImDrawList* targetDrawList = ImGui::GetWindowDrawList();
ASSERT_NE(targetDrawList, nullptr);
const int initialClipDepth = targetDrawList->_ClipRectStack.Size;
const bool flushed = backend.EndFrame(targetDrawList);
EXPECT_TRUE(flushed);
EXPECT_EQ(targetDrawList->_ClipRectStack.Size, initialClipDepth);
EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 1u);
EXPECT_EQ(backend.GetLastFlushedCommandCount(), 2u);
EXPECT_EQ(backend.GetPendingCommandCount(), 0u);
ImGui::End();
ImGui::EndFrame();
}

View File

@@ -0,0 +1,257 @@
#include <gtest/gtest.h>
#include "XCUIBackend/XCUIAssetDocumentSource.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Containers/String.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
namespace {
namespace fs = std::filesystem;
using XCEngine::Containers::String;
using XCEngine::Editor::XCUIBackend::XCUIAssetDocumentSource;
using XCEngine::Resources::ResourceManager;
String ToContainersString(const std::string& value) {
return String(value.c_str());
}
void WriteTextFile(const fs::path& path, const std::string& contents) {
fs::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary | std::ios::trunc);
output << contents;
}
std::string BuildMinimalViewDocument(const std::string& themeReference) {
return
"<View name=\"Test\" theme=\"" + themeReference + "\">\n"
" <Column id=\"root\">\n"
" <Text text=\"hello\" />\n"
" </Column>\n"
"</View>\n";
}
std::string BuildMinimalThemeDocument() {
return
"<Theme name=\"TestTheme\">\n"
" <Token name=\"color.text.primary\" type=\"color\" value=\"#FFFFFF\" />\n"
"</Theme>\n";
}
class ScopedCurrentPath {
public:
explicit ScopedCurrentPath(const fs::path& newPath) {
m_originalPath = fs::current_path();
fs::create_directories(newPath);
fs::current_path(newPath);
}
~ScopedCurrentPath() {
if (!m_originalPath.empty()) {
fs::current_path(m_originalPath);
}
}
private:
fs::path m_originalPath = {};
};
class XCUIAssetDocumentSourceTest : public ::testing::Test {
protected:
void SetUp() override {
m_originalResourceRoot = ResourceManager::Get().GetResourceRoot();
ResourceManager::Get().SetResourceRoot(String());
m_tempRoot = fs::temp_directory_path() /
fs::path("xcui_asset_document_source_tests");
m_tempRoot /= fs::path(::testing::UnitTest::GetInstance()->current_test_info()->name());
fs::remove_all(m_tempRoot);
fs::create_directories(m_tempRoot);
}
void TearDown() override {
ResourceManager::Get().UnloadAll();
ResourceManager::Get().SetResourceRoot(m_originalResourceRoot);
std::error_code ec;
fs::remove_all(m_tempRoot, ec);
}
fs::path CreateRepositorySubdir(const std::string& relativePath) const {
const fs::path path = m_tempRoot / fs::path(relativePath);
fs::create_directories(path);
return path;
}
void WriteProjectDocuments(const XCUIAssetDocumentSource::PathSet& paths) const {
WriteTextFile(
m_tempRoot / fs::path(paths.view.primaryRelativePath),
BuildMinimalViewDocument("Theme.xctheme"));
WriteTextFile(
m_tempRoot / fs::path(paths.theme.primaryRelativePath),
BuildMinimalThemeDocument());
}
void WriteLegacyDocuments(const XCUIAssetDocumentSource::PathSet& paths) const {
WriteTextFile(
m_tempRoot / fs::path(paths.view.legacyRelativePath),
BuildMinimalViewDocument(
fs::path(paths.theme.legacyRelativePath).filename().generic_string()));
WriteTextFile(
m_tempRoot / fs::path(paths.theme.legacyRelativePath),
BuildMinimalThemeDocument());
}
fs::path m_tempRoot = {};
private:
String m_originalResourceRoot = {};
};
TEST_F(XCUIAssetDocumentSourceTest, DemoAndLayoutLabPathSetsUseExpectedPaths) {
const XCUIAssetDocumentSource::PathSet demoPaths =
XCUIAssetDocumentSource::MakeDemoPathSet();
EXPECT_EQ(demoPaths.setName, "Demo");
EXPECT_EQ(demoPaths.view.primaryRelativePath, "Assets/XCUI/NewEditor/Demo/View.xcui");
EXPECT_EQ(demoPaths.theme.primaryRelativePath, "Assets/XCUI/NewEditor/Demo/Theme.xctheme");
EXPECT_EQ(demoPaths.view.legacyRelativePath, "new_editor/resources/xcui_demo_view.xcui");
EXPECT_EQ(demoPaths.theme.legacyRelativePath, "new_editor/resources/xcui_demo_theme.xctheme");
const XCUIAssetDocumentSource::PathSet layoutLabPaths =
XCUIAssetDocumentSource::MakeLayoutLabPathSet();
EXPECT_EQ(layoutLabPaths.setName, "LayoutLab");
EXPECT_EQ(layoutLabPaths.view.primaryRelativePath, "Assets/XCUI/NewEditor/LayoutLab/View.xcui");
EXPECT_EQ(layoutLabPaths.theme.primaryRelativePath, "Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme");
EXPECT_EQ(layoutLabPaths.view.legacyRelativePath, "new_editor/resources/xcui_layout_lab_view.xcui");
EXPECT_EQ(layoutLabPaths.theme.legacyRelativePath, "new_editor/resources/xcui_layout_lab_theme.xctheme");
}
TEST_F(XCUIAssetDocumentSourceTest, MakePathSetSanitizesNamesAndBuildsLegacySnakeCase) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakePathSet(" Layout Lab! Debug-42 ");
EXPECT_EQ(paths.setName, "LayoutLabDebug-42");
EXPECT_EQ(
paths.view.primaryRelativePath,
"Assets/XCUI/NewEditor/LayoutLabDebug-42/View.xcui");
EXPECT_EQ(
paths.theme.legacyRelativePath,
"new_editor/resources/xcui_layout_lab_debug_42_theme.xctheme");
}
TEST_F(XCUIAssetDocumentSourceTest, CollectCandidatePathsPrefersProjectAssetsBeforeLegacyMirror) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakePathSet("Worker1CandidateOrder");
WriteProjectDocuments(paths);
WriteLegacyDocuments(paths);
const std::vector<XCUIAssetDocumentSource::ResolutionCandidate> candidates =
XCUIAssetDocumentSource::CollectCandidatePaths(paths.view, m_tempRoot, fs::path());
ASSERT_EQ(candidates.size(), 2u);
EXPECT_EQ(candidates[0].origin, XCUIAssetDocumentSource::PathOrigin::ProjectAssets);
EXPECT_EQ(candidates[0].resolvedPath, fs::path(m_tempRoot / paths.view.primaryRelativePath).lexically_normal());
EXPECT_EQ(candidates[1].origin, XCUIAssetDocumentSource::PathOrigin::LegacyMirror);
EXPECT_EQ(candidates[1].resolvedPath, fs::path(m_tempRoot / paths.view.legacyRelativePath).lexically_normal());
}
TEST_F(XCUIAssetDocumentSourceTest, DiagnoseRepositoryRootReportsProjectAssetAncestor) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakeDemoPathSet();
WriteProjectDocuments(paths);
const fs::path searchRoot = CreateRepositorySubdir("tools/worker1/deep");
const XCUIAssetDocumentSource::RepositoryDiscovery discovery =
XCUIAssetDocumentSource::DiagnoseRepositoryRoot(paths, { searchRoot }, false);
EXPECT_EQ(discovery.repositoryRoot, m_tempRoot.lexically_normal());
ASSERT_EQ(discovery.probes.size(), 1u);
EXPECT_TRUE(discovery.probes[0].matched);
EXPECT_EQ(discovery.probes[0].searchRoot, searchRoot.lexically_normal());
EXPECT_EQ(discovery.probes[0].matchedRelativePath, paths.view.primaryRelativePath);
EXPECT_NE(discovery.statusMessage.find(paths.view.primaryRelativePath), std::string::npos);
}
TEST_F(XCUIAssetDocumentSourceTest, DiagnoseRepositoryRootReportsLegacyMirrorAncestor) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakeLayoutLabPathSet();
WriteLegacyDocuments(paths);
const fs::path searchRoot = CreateRepositorySubdir("sandbox/runtime/session");
const XCUIAssetDocumentSource::RepositoryDiscovery discovery =
XCUIAssetDocumentSource::DiagnoseRepositoryRoot(paths, { searchRoot }, false);
EXPECT_EQ(discovery.repositoryRoot, m_tempRoot.lexically_normal());
ASSERT_EQ(discovery.probes.size(), 1u);
EXPECT_TRUE(discovery.probes[0].matched);
EXPECT_EQ(discovery.probes[0].matchedRelativePath, paths.view.legacyRelativePath);
EXPECT_NE(discovery.statusMessage.find(paths.view.legacyRelativePath), std::string::npos);
}
TEST_F(XCUIAssetDocumentSourceTest, ReloadUsesLegacyFallbackAndTracksSourceChanges) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakePathSet("Worker1HotReloadRegression");
WriteLegacyDocuments(paths);
const fs::path sandboxPath = CreateRepositorySubdir("runtime/worker1");
ScopedCurrentPath scopedCurrentPath(sandboxPath);
XCUIAssetDocumentSource source(paths);
ASSERT_TRUE(source.Reload());
const XCUIAssetDocumentSource::LoadState& initialState = source.GetState();
EXPECT_TRUE(initialState.succeeded);
EXPECT_TRUE(initialState.usedLegacyFallback);
EXPECT_TRUE(initialState.changeTrackingReady);
EXPECT_TRUE(initialState.missingTrackedSourcePaths.empty());
EXPECT_EQ(initialState.repositoryRoot, m_tempRoot.lexically_normal());
EXPECT_EQ(initialState.view.pathOrigin, XCUIAssetDocumentSource::PathOrigin::LegacyMirror);
EXPECT_EQ(initialState.theme.pathOrigin, XCUIAssetDocumentSource::PathOrigin::LegacyMirror);
EXPECT_FALSE(initialState.trackedSourcePaths.empty());
EXPECT_NE(initialState.trackingStatusMessage.find("Tracking "), std::string::npos);
EXPECT_FALSE(source.HasTrackedChanges());
const fs::path themePath = m_tempRoot / fs::path(paths.theme.legacyRelativePath);
fs::last_write_time(
themePath,
fs::last_write_time(themePath) + std::chrono::seconds(2));
EXPECT_TRUE(source.HasTrackedChanges());
ASSERT_TRUE(source.ReloadIfChanged());
const XCUIAssetDocumentSource::LoadState& reloadedState = source.GetState();
EXPECT_EQ(
reloadedState.view.backend,
XCUIAssetDocumentSource::LoadBackend::CompilerFallback);
EXPECT_EQ(
reloadedState.theme.backend,
XCUIAssetDocumentSource::LoadBackend::CompilerFallback);
EXPECT_TRUE(reloadedState.changeTrackingReady);
}
TEST_F(XCUIAssetDocumentSourceTest, ReloadFailureIncludesRepositoryDiscoveryDiagnostic) {
const XCUIAssetDocumentSource::PathSet paths =
XCUIAssetDocumentSource::MakePathSet("Worker1MissingDocuments");
const fs::path sandboxPath = CreateRepositorySubdir("runtime/missing");
ScopedCurrentPath scopedCurrentPath(sandboxPath);
XCUIAssetDocumentSource source(paths);
EXPECT_FALSE(source.Reload());
const XCUIAssetDocumentSource::LoadState& state = source.GetState();
EXPECT_FALSE(state.succeeded);
EXPECT_TRUE(state.repositoryRoot.empty());
EXPECT_NE(state.repositoryDiscovery.statusMessage.find("Repository root not found"), std::string::npos);
EXPECT_NE(state.errorMessage.find(paths.view.primaryRelativePath), std::string::npos);
EXPECT_NE(state.errorMessage.find("Repository root not found"), std::string::npos);
EXPECT_TRUE(state.view.candidatePaths.empty());
EXPECT_TRUE(state.view.attemptMessages.empty());
}
} // namespace

View File

@@ -0,0 +1,336 @@
#include <gtest/gtest.h>
#include "XCUIBackend/XCUIDemoRuntime.h"
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/Types.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
namespace {
namespace fs = std::filesystem;
using XCEngine::UI::UIDrawCommand;
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
XCEngine::Editor::XCUIBackend::XCUIDemoInputState BuildInputState(
float width = 720.0f,
float height = 420.0f) {
XCEngine::Editor::XCUIBackend::XCUIDemoInputState input = {};
input.canvasRect = XCEngine::UI::UIRect(0.0f, 0.0f, width, height);
input.pointerPosition = XCEngine::UI::UIPoint(width * 0.5f, height * 0.5f);
input.pointerInside = true;
return input;
}
UIInputEvent MakeCharacterEvent(std::uint32_t character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = character;
return event;
}
UIInputEvent MakeKeyDownEvent(XCEngine::Input::KeyCode keyCode, bool repeat = false) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(keyCode);
event.repeat = repeat;
return event;
}
fs::path FindDemoResourcePath() {
fs::path probe = fs::current_path();
for (int i = 0; i < 8; ++i) {
const fs::path canonicalCandidate = probe / "Assets/XCUI/NewEditor/Demo/View.xcui";
if (fs::exists(canonicalCandidate)) {
return canonicalCandidate;
}
const fs::path legacyCandidate = probe / "new_editor/resources/xcui_demo_view.xcui";
if (fs::exists(legacyCandidate)) {
return legacyCandidate;
}
if (!probe.has_parent_path()) {
break;
}
probe = probe.parent_path();
}
return {};
}
std::vector<const UIDrawCommand*> CollectTextCommands(const XCEngine::UI::UIDrawData& drawData) {
std::vector<const UIDrawCommand*> textCommands = {};
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == UIDrawCommandType::Text) {
textCommands.push_back(&command);
}
}
}
return textCommands;
}
const UIDrawCommand* FindTextCommand(
const XCEngine::UI::UIDrawData& drawData,
const std::string& text) {
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == UIDrawCommandType::Text && command.text == text) {
return &command;
}
}
}
return nullptr;
}
class FileTimestampRestoreScope {
public:
explicit FileTimestampRestoreScope(fs::path path)
: m_path(std::move(path)) {
if (!m_path.empty() && fs::exists(m_path)) {
m_originalWriteTime = fs::last_write_time(m_path);
std::ifstream input(m_path, std::ios::binary);
std::ostringstream stream;
stream << input.rdbuf();
m_originalContents = stream.str();
m_valid = true;
}
}
~FileTimestampRestoreScope() {
if (m_valid) {
std::ofstream output(m_path, std::ios::binary | std::ios::trunc);
output << m_originalContents;
output.close();
fs::last_write_time(m_path, m_originalWriteTime);
}
}
private:
fs::path m_path;
fs::file_time_type m_originalWriteTime = {};
std::string m_originalContents = {};
bool m_valid = false;
};
} // namespace
TEST(NewEditorXCUIDemoRuntimeTest, UpdateProvidesDeterministicFrameContainer) {
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
const bool reloadSucceeded = runtime.ReloadDocuments();
const auto& firstFrame = runtime.Update(BuildInputState());
EXPECT_EQ(firstFrame.stats.documentsReady, reloadSucceeded);
EXPECT_EQ(firstFrame.stats.drawListCount, firstFrame.drawData.GetDrawListCount());
EXPECT_EQ(firstFrame.stats.commandCount, firstFrame.drawData.GetTotalCommandCount());
const auto& secondFrame = runtime.Update(BuildInputState());
EXPECT_GE(secondFrame.stats.treeGeneration, firstFrame.stats.treeGeneration);
EXPECT_EQ(secondFrame.stats.drawListCount, secondFrame.drawData.GetDrawListCount());
EXPECT_EQ(secondFrame.stats.commandCount, secondFrame.drawData.GetTotalCommandCount());
if (secondFrame.stats.documentsReady) {
EXPECT_GT(secondFrame.stats.elementCount, 0u);
EXPECT_GT(secondFrame.stats.drawListCount, 0u);
EXPECT_GT(secondFrame.stats.commandCount, 0u);
} else {
EXPECT_FALSE(secondFrame.stats.statusMessage.empty());
}
}
TEST(NewEditorXCUIDemoRuntimeTest, RuntimeFrameEmitsTextCommandsWithResolvedFontSizes) {
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& frame = runtime.Update(BuildInputState());
ASSERT_TRUE(frame.stats.documentsReady);
const std::vector<const UIDrawCommand*> textCommands = CollectTextCommands(frame.drawData);
ASSERT_FALSE(textCommands.empty());
for (const UIDrawCommand* command : textCommands) {
ASSERT_NE(command, nullptr);
EXPECT_FALSE(command->text.empty());
EXPECT_GT(command->fontSize, 0.0f);
}
const UIDrawCommand* titleCommand = FindTextCommand(frame.drawData, "New XCUI Shell");
ASSERT_NE(titleCommand, nullptr);
EXPECT_FLOAT_EQ(titleCommand->fontSize, 18.0f);
const UIDrawCommand* metricValueCommand = FindTextCommand(frame.drawData, "Driven by runtime");
ASSERT_NE(metricValueCommand, nullptr);
EXPECT_FLOAT_EQ(metricValueCommand->fontSize, 18.0f);
const UIDrawCommand* buttonLabelCommand = FindTextCommand(frame.drawData, "Toggle Accent");
ASSERT_NE(buttonLabelCommand, nullptr);
EXPECT_FLOAT_EQ(buttonLabelCommand->fontSize, 14.0f);
}
TEST(NewEditorXCUIDemoRuntimeTest, InputStateTransitionsAreAcceptedAndFrameStillBuilds) {
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
runtime.ReloadDocuments();
XCEngine::Editor::XCUIBackend::XCUIDemoInputState frameInput = BuildInputState();
frameInput.pointerPressed = true;
frameInput.pointerDown = true;
const auto& pressedFrame = runtime.Update(frameInput);
frameInput.pointerPressed = false;
frameInput.pointerReleased = true;
frameInput.pointerDown = false;
frameInput.shortcutPressed = true;
const auto& releasedFrame = runtime.Update(frameInput);
EXPECT_GE(releasedFrame.stats.treeGeneration, pressedFrame.stats.treeGeneration);
EXPECT_EQ(releasedFrame.stats.drawListCount, releasedFrame.drawData.GetDrawListCount());
EXPECT_EQ(releasedFrame.stats.commandCount, releasedFrame.drawData.GetTotalCommandCount());
if (releasedFrame.stats.documentsReady) {
EXPECT_GT(releasedFrame.stats.elementCount, 0u);
EXPECT_GE(releasedFrame.stats.dirtyRootCount, 0u);
}
}
TEST(NewEditorXCUIDemoRuntimeTest, PointerToggleUpdatesFocusStatusTextAndAccentState) {
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baselineFrame = runtime.Update(BuildInputState());
ASSERT_TRUE(baselineFrame.stats.documentsReady);
EXPECT_FALSE(baselineFrame.stats.accentEnabled);
EXPECT_NE(
FindTextCommand(baselineFrame.drawData, "Markup -> Layout -> Style -> DrawData"),
nullptr);
XCEngine::UI::UIRect buttonRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("toggleAccent", buttonRect));
const XCEngine::UI::UIPoint buttonCenter(
buttonRect.x + buttonRect.width * 0.5f,
buttonRect.y + buttonRect.height * 0.5f);
XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState();
pressedInput.pointerPosition = buttonCenter;
pressedInput.pointerPressed = true;
pressedInput.pointerDown = true;
const auto& pressedFrame = runtime.Update(pressedInput);
ASSERT_TRUE(pressedFrame.stats.documentsReady);
XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState();
releasedInput.pointerPosition = buttonCenter;
releasedInput.pointerReleased = true;
const auto& toggledFrame = runtime.Update(releasedInput);
ASSERT_TRUE(toggledFrame.stats.documentsReady);
EXPECT_TRUE(toggledFrame.stats.accentEnabled);
EXPECT_EQ(toggledFrame.stats.lastCommandId, "demo.toggleAccent");
EXPECT_EQ(toggledFrame.stats.focusedElementId, "toggleAccent");
const UIDrawCommand* focusStatusCommand = FindTextCommand(
toggledFrame.drawData,
"Focus: toggleAccent");
ASSERT_NE(focusStatusCommand, nullptr);
EXPECT_FLOAT_EQ(focusStatusCommand->fontSize, 14.0f);
}
TEST(NewEditorXCUIDemoRuntimeTest, UpdateAutoReloadsWhenSourceTimestampChanges) {
const fs::path viewPath = FindDemoResourcePath();
ASSERT_FALSE(viewPath.empty());
ASSERT_TRUE(fs::exists(viewPath));
FileTimestampRestoreScope restoreScope(viewPath);
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baselineFrame = runtime.Update(BuildInputState());
ASSERT_TRUE(baselineFrame.stats.documentsReady);
XCEngine::UI::UIRect probeRect = {};
EXPECT_FALSE(runtime.TryGetElementRect("autoReloadProbe", probeRect));
std::ifstream input(viewPath, std::ios::binary);
std::ostringstream stream;
stream << input.rdbuf();
const std::string originalContents = stream.str();
input.close();
const std::string marker = "</Column>\n</View>";
const std::size_t insertPosition = originalContents.rfind(marker);
ASSERT_NE(insertPosition, std::string::npos);
const std::string injectedNode =
" <Text id=\"autoReloadProbe\" text=\"Auto Reload Probe\" style=\"Meta\" />\n";
std::string modifiedContents = originalContents;
modifiedContents.insert(insertPosition, injectedNode);
std::ofstream output(viewPath, std::ios::binary | std::ios::trunc);
output << modifiedContents;
output.close();
const fs::file_time_type originalWriteTime = fs::last_write_time(viewPath);
fs::last_write_time(viewPath, originalWriteTime + std::chrono::seconds(2));
const auto& reloadedFrame = runtime.Update(BuildInputState());
EXPECT_TRUE(reloadedFrame.stats.documentsReady);
EXPECT_GT(reloadedFrame.stats.elementCount, 0u);
EXPECT_GT(reloadedFrame.stats.commandCount, 0u);
EXPECT_TRUE(runtime.TryGetElementRect("autoReloadProbe", probeRect));
}
TEST(NewEditorXCUIDemoRuntimeTest, TextFieldAcceptsUtf8CharactersAndBackspace) {
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baselineFrame = runtime.Update(BuildInputState());
ASSERT_TRUE(baselineFrame.stats.documentsReady);
XCEngine::UI::UIRect promptRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("agentPrompt", promptRect));
const XCEngine::UI::UIPoint promptCenter(
promptRect.x + promptRect.width * 0.5f,
promptRect.y + promptRect.height * 0.5f);
XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState();
pressedInput.pointerPosition = promptCenter;
pressedInput.pointerPressed = true;
pressedInput.pointerDown = true;
runtime.Update(pressedInput);
XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState();
releasedInput.pointerPosition = promptCenter;
releasedInput.pointerReleased = true;
const auto& focusedFrame = runtime.Update(releasedInput);
ASSERT_TRUE(focusedFrame.stats.documentsReady);
EXPECT_EQ(focusedFrame.stats.focusedElementId, "agentPrompt");
XCEngine::Editor::XCUIBackend::XCUIDemoInputState textInput = BuildInputState();
textInput.events.push_back(MakeCharacterEvent('A'));
textInput.events.push_back(MakeCharacterEvent('I'));
textInput.events.push_back(MakeCharacterEvent(0x4F60u));
const auto& typedFrame = runtime.Update(textInput);
ASSERT_TRUE(typedFrame.stats.documentsReady);
EXPECT_EQ(typedFrame.stats.focusedElementId, "agentPrompt");
EXPECT_NE(FindTextCommand(typedFrame.drawData, "AI你"), nullptr);
EXPECT_EQ(typedFrame.stats.lastCommandId, "demo.text.edit.agentPrompt");
XCEngine::Editor::XCUIBackend::XCUIDemoInputState backspaceInput = BuildInputState();
backspaceInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::Backspace));
const auto& backspacedFrame = runtime.Update(backspaceInput);
ASSERT_TRUE(backspacedFrame.stats.documentsReady);
EXPECT_NE(FindTextCommand(backspacedFrame.drawData, "AI"), nullptr);
EXPECT_EQ(backspacedFrame.stats.focusedElementId, "agentPrompt");
}

View File

@@ -0,0 +1,523 @@
#include <gtest/gtest.h>
#include "XCUIBackend/XCUIHostedPreviewPresenter.h"
#include <XCEngine/UI/DrawData.h>
#include <imgui.h>
#include <cstdint>
namespace {
using XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter;
using XCEngine::Editor::XCUIBackend::CreateQueuedNativeXCUIHostedPreviewPresenter;
using XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewDrainStats;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewQueue;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewQueuedFrame;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceRegistry;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats;
class ImGuiContextScope {
public:
ImGuiContextScope() {
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
}
~ImGuiContextScope() {
ImGui::DestroyContext();
}
};
void PrepareImGui(float width = 1024.0f, float height = 768.0f) {
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize = ImVec2(width, height);
io.DeltaTime = 1.0f / 60.0f;
unsigned char* fontPixels = nullptr;
int fontWidth = 0;
int fontHeight = 0;
io.Fonts->GetTexDataAsRGBA32(&fontPixels, &fontWidth, &fontHeight);
io.Fonts->SetTexID(static_cast<ImTextureID>(1));
}
TEST(XCUIHostedPreviewPresenterTest, PresentReturnsFalseAndClearsStatsWhenFrameHasNoDrawData) {
ImGuiContextScope contextScope;
PrepareImGui();
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter = CreateImGuiXCUIHostedPreviewPresenter();
ASSERT_NE(presenter, nullptr);
XCUIHostedPreviewFrame frame = {};
const bool presented = presenter->Present(frame);
const XCUIHostedPreviewStats& stats = presenter->GetLastStats();
EXPECT_FALSE(presented);
EXPECT_FALSE(stats.presented);
EXPECT_EQ(stats.submittedDrawListCount, 0u);
EXPECT_EQ(stats.submittedCommandCount, 0u);
EXPECT_EQ(stats.flushedDrawListCount, 0u);
EXPECT_EQ(stats.flushedCommandCount, 0u);
}
TEST(XCUIHostedPreviewPresenterTest, PresentFlushesDrawDataIntoProvidedImGuiDrawList) {
ImGuiContextScope contextScope;
PrepareImGui(800.0f, 600.0f);
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter = CreateImGuiXCUIHostedPreviewPresenter();
ASSERT_NE(presenter, nullptr);
XCEngine::UI::UIDrawData drawData = {};
XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("HostedPreview");
drawList.AddFilledRect(
XCEngine::UI::UIRect(12.0f, 14.0f, 48.0f, 30.0f),
XCEngine::UI::UIColor(0.25f, 0.5f, 0.8f, 1.0f));
drawList.AddText(
XCEngine::UI::UIPoint(18.0f, 24.0f),
"xcui",
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
14.0f);
ImGui::NewFrame();
ASSERT_TRUE(ImGui::Begin("HostedPreviewPresenterWindow"));
ImDrawList* targetDrawList = ImGui::GetWindowDrawList();
ASSERT_NE(targetDrawList, nullptr);
XCUIHostedPreviewFrame frame = {};
frame.drawData = &drawData;
frame.targetDrawList = targetDrawList;
const bool presented = presenter->Present(frame);
const XCUIHostedPreviewStats& stats = presenter->GetLastStats();
ImGui::End();
ImGui::EndFrame();
EXPECT_TRUE(presented);
EXPECT_TRUE(stats.presented);
EXPECT_EQ(stats.submittedDrawListCount, 1u);
EXPECT_EQ(stats.submittedCommandCount, 2u);
EXPECT_EQ(stats.flushedDrawListCount, 1u);
EXPECT_EQ(stats.flushedCommandCount, 2u);
EXPECT_GT(targetDrawList->VtxBuffer.Size, 0);
EXPECT_GT(targetDrawList->CmdBuffer.Size, 0);
}
TEST(XCUIHostedPreviewPresenterTest, QueuedNativePresenterCopiesFrameIntoQueue) {
XCUIHostedPreviewQueue queue = {};
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
queue.BeginFrame();
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter =
CreateQueuedNativeXCUIHostedPreviewPresenter(queue, surfaceRegistry);
ASSERT_NE(presenter, nullptr);
EXPECT_TRUE(presenter->IsNativeQueued());
XCEngine::UI::UIDrawData drawData = {};
XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("HostedPreviewNative");
drawList.AddFilledRect(
XCEngine::UI::UIRect(20.0f, 24.0f, 56.0f, 32.0f),
XCEngine::UI::UIColor(0.2f, 0.35f, 0.9f, 1.0f));
drawList.AddText(
XCEngine::UI::UIPoint(28.0f, 36.0f),
"native",
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
16.0f);
XCUIHostedPreviewFrame frame = {};
frame.drawData = &drawData;
frame.debugName = "Hosted Preview Native Queue";
frame.debugSource = "tests.hosted_preview.native_queue";
frame.canvasRect = XCEngine::UI::UIRect(32.0f, 48.0f, 320.0f, 180.0f);
frame.logicalSize = XCEngine::UI::UISize(640.0f, 360.0f);
EXPECT_TRUE(presenter->Present(frame));
const XCUIHostedPreviewStats& stats = presenter->GetLastStats();
EXPECT_TRUE(stats.presented);
EXPECT_TRUE(stats.queuedToNativePass);
EXPECT_EQ(stats.submittedDrawListCount, 1u);
EXPECT_EQ(stats.submittedCommandCount, 2u);
EXPECT_EQ(stats.flushedDrawListCount, 0u);
EXPECT_EQ(stats.flushedCommandCount, 0u);
drawData.Clear();
const auto& queuedFrames = queue.GetQueuedFrames();
ASSERT_EQ(queuedFrames.size(), 1u);
EXPECT_EQ(queuedFrames[0].debugName, "Hosted Preview Native Queue");
EXPECT_EQ(queuedFrames[0].debugSource, "tests.hosted_preview.native_queue");
EXPECT_FLOAT_EQ(queuedFrames[0].canvasRect.x, 32.0f);
EXPECT_FLOAT_EQ(queuedFrames[0].canvasRect.y, 48.0f);
EXPECT_FLOAT_EQ(queuedFrames[0].canvasRect.width, 320.0f);
EXPECT_FLOAT_EQ(queuedFrames[0].canvasRect.height, 180.0f);
EXPECT_FLOAT_EQ(queuedFrames[0].logicalSize.width, 640.0f);
EXPECT_FLOAT_EQ(queuedFrames[0].logicalSize.height, 360.0f);
EXPECT_EQ(queuedFrames[0].drawData.GetDrawListCount(), 1u);
EXPECT_EQ(queuedFrames[0].drawData.GetTotalCommandCount(), 2u);
}
TEST(XCUIHostedPreviewPresenterTest, QueuedNativePresenterFallsBackLogicalSizeToCanvasRectAndDelegatesSurfaceQueries) {
XCUIHostedPreviewQueue queue = {};
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
queue.BeginFrame();
surfaceRegistry.BeginFrame();
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter =
CreateQueuedNativeXCUIHostedPreviewPresenter(queue, surfaceRegistry);
ASSERT_NE(presenter, nullptr);
XCEngine::UI::UIDrawData drawData = {};
XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("HostedPreviewFallback");
drawList.AddFilledRect(
XCEngine::UI::UIRect(6.0f, 10.0f, 40.0f, 24.0f),
XCEngine::UI::UIColor(0.85f, 0.35f, 0.2f, 1.0f));
XCUIHostedPreviewFrame frame = {};
frame.drawData = &drawData;
frame.debugName = "XCUI Demo";
frame.debugSource = "tests.hosted_preview.logical_size_fallback";
frame.canvasRect = XCEngine::UI::UIRect(18.0f, 22.0f, 320.0f, 180.0f);
ASSERT_TRUE(presenter->Present(frame));
ASSERT_EQ(queue.GetQueuedFrames().size(), 1u);
const XCUIHostedPreviewQueuedFrame& queuedFrame = queue.GetQueuedFrames().front();
EXPECT_FLOAT_EQ(queuedFrame.logicalSize.width, 320.0f);
EXPECT_FLOAT_EQ(queuedFrame.logicalSize.height, 180.0f);
XCUIHostedPreviewSurfaceDescriptor descriptor = {};
EXPECT_FALSE(presenter->TryGetSurfaceDescriptor("XCUI Demo", descriptor));
EXPECT_TRUE(descriptor.debugName.empty());
surfaceRegistry.RecordQueuedFrame(queuedFrame, 0u);
ASSERT_TRUE(presenter->TryGetSurfaceDescriptor("XCUI Demo", descriptor));
EXPECT_EQ(descriptor.debugName, "XCUI Demo");
EXPECT_EQ(descriptor.debugSource, "tests.hosted_preview.logical_size_fallback");
EXPECT_FLOAT_EQ(descriptor.canvasRect.x, 18.0f);
EXPECT_FLOAT_EQ(descriptor.canvasRect.y, 22.0f);
EXPECT_FLOAT_EQ(descriptor.canvasRect.width, 320.0f);
EXPECT_FLOAT_EQ(descriptor.canvasRect.height, 180.0f);
EXPECT_FLOAT_EQ(descriptor.logicalSize.width, 320.0f);
EXPECT_FLOAT_EQ(descriptor.logicalSize.height, 180.0f);
EXPECT_EQ(descriptor.submittedDrawListCount, 1u);
EXPECT_EQ(descriptor.submittedCommandCount, 1u);
EXPECT_TRUE(descriptor.queuedThisFrame);
XCUIHostedPreviewSurfaceImage image = {};
EXPECT_FALSE(presenter->TryGetSurfaceImage("XCUI Demo", image));
EXPECT_FALSE(image.IsValid());
surfaceRegistry.UpdateSurface(
"XCUI Demo",
static_cast<ImTextureID>(static_cast<intptr_t>(11)),
640u,
360u,
XCEngine::UI::UIRect(0.0f, 0.0f, 320.0f, 180.0f));
ASSERT_TRUE(presenter->TryGetSurfaceImage("XCUI Demo", image));
EXPECT_TRUE(image.IsValid());
EXPECT_EQ(image.textureId, static_cast<ImTextureID>(static_cast<intptr_t>(11)));
EXPECT_FLOAT_EQ(image.uvMin.x, 0.0f);
EXPECT_FLOAT_EQ(image.uvMin.y, 0.0f);
EXPECT_FLOAT_EQ(image.uvMax.x, 0.5f);
EXPECT_FLOAT_EQ(image.uvMax.y, 0.5f);
}
TEST(XCUIHostedPreviewPresenterTest, QueuedNativePresenterRejectsMissingDrawDataAndLeavesQueueUntouched) {
XCUIHostedPreviewQueue queue = {};
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
queue.BeginFrame();
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter =
CreateQueuedNativeXCUIHostedPreviewPresenter(queue, surfaceRegistry);
ASSERT_NE(presenter, nullptr);
XCUIHostedPreviewFrame frame = {};
frame.debugName = "Missing DrawData";
frame.targetDrawList = reinterpret_cast<ImDrawList*>(1);
EXPECT_FALSE(presenter->Present(frame));
const XCUIHostedPreviewStats& stats = presenter->GetLastStats();
EXPECT_FALSE(stats.presented);
EXPECT_FALSE(stats.queuedToNativePass);
EXPECT_EQ(stats.submittedDrawListCount, 0u);
EXPECT_EQ(stats.submittedCommandCount, 0u);
EXPECT_EQ(stats.flushedDrawListCount, 0u);
EXPECT_EQ(stats.flushedCommandCount, 0u);
EXPECT_TRUE(queue.GetQueuedFrames().empty());
}
TEST(XCUIHostedPreviewPresenterTest, HostedPreviewQueuePreservesSubmissionOrderAndPayloadMetadata) {
XCUIHostedPreviewQueue queue = {};
queue.BeginFrame();
XCEngine::UI::UIDrawData firstDrawData = {};
XCEngine::UI::UIDrawList& firstDrawList = firstDrawData.EmplaceDrawList("FirstPreview");
firstDrawList.AddFilledRect(
XCEngine::UI::UIRect(0.0f, 0.0f, 20.0f, 10.0f),
XCEngine::UI::UIColor(1.0f, 0.2f, 0.2f, 1.0f));
XCEngine::UI::UIDrawData secondDrawData = {};
XCEngine::UI::UIDrawList& secondDrawList = secondDrawData.EmplaceDrawList("SecondPreview");
secondDrawList.AddFilledRect(
XCEngine::UI::UIRect(10.0f, 8.0f, 24.0f, 14.0f),
XCEngine::UI::UIColor(0.2f, 0.7f, 1.0f, 1.0f));
secondDrawList.AddText(
XCEngine::UI::UIPoint(12.0f, 12.0f),
"queued",
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
13.0f);
XCUIHostedPreviewStats firstStats = {};
XCUIHostedPreviewFrame firstFrame = {};
firstFrame.drawData = &firstDrawData;
firstFrame.debugName = "First Native Preview";
ASSERT_TRUE(queue.Submit(firstFrame, &firstStats));
XCUIHostedPreviewStats secondStats = {};
XCUIHostedPreviewFrame secondFrame = {};
secondFrame.drawData = &secondDrawData;
ASSERT_TRUE(queue.Submit(secondFrame, &secondStats));
firstDrawData.Clear();
secondDrawData.Clear();
const auto& queuedFrames = queue.GetQueuedFrames();
ASSERT_EQ(queuedFrames.size(), 2u);
EXPECT_EQ(firstStats.submittedDrawListCount, 1u);
EXPECT_EQ(firstStats.submittedCommandCount, 1u);
EXPECT_TRUE(firstStats.queuedToNativePass);
EXPECT_EQ(secondStats.submittedDrawListCount, 1u);
EXPECT_EQ(secondStats.submittedCommandCount, 2u);
EXPECT_TRUE(secondStats.queuedToNativePass);
EXPECT_EQ(queuedFrames[0].debugName, "First Native Preview");
EXPECT_EQ(queuedFrames[0].drawData.GetDrawListCount(), 1u);
EXPECT_EQ(queuedFrames[0].drawData.GetTotalCommandCount(), 1u);
EXPECT_EQ(queuedFrames[1].debugName, "");
EXPECT_EQ(queuedFrames[1].drawData.GetDrawListCount(), 1u);
EXPECT_EQ(queuedFrames[1].drawData.GetTotalCommandCount(), 2u);
}
TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryIgnoresUnnamedQueuedFramesAndKeepsDescriptorListStable) {
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
XCUIHostedPreviewQueuedFrame unnamedFrame = {};
unnamedFrame.debugSource = "tests.hosted_preview.unnamed";
unnamedFrame.canvasRect = XCEngine::UI::UIRect(10.0f, 12.0f, 120.0f, 80.0f);
unnamedFrame.logicalSize = XCEngine::UI::UISize(120.0f, 80.0f);
unnamedFrame.drawData.EmplaceDrawList("Unnamed").AddFilledRect(
XCEngine::UI::UIRect(0.0f, 0.0f, 16.0f, 16.0f),
XCEngine::UI::UIColor(0.4f, 0.7f, 0.9f, 1.0f));
surfaceRegistry.RecordQueuedFrame(unnamedFrame, 5u);
EXPECT_TRUE(surfaceRegistry.GetDescriptors().empty());
surfaceRegistry.UpdateSurface(
"XCUI Demo",
static_cast<ImTextureID>(static_cast<intptr_t>(13)),
800u,
600u,
XCEngine::UI::UIRect(0.0f, 0.0f, 400.0f, 300.0f));
ASSERT_EQ(surfaceRegistry.GetDescriptors().size(), 1u);
surfaceRegistry.RecordQueuedFrame(unnamedFrame, 9u);
ASSERT_EQ(surfaceRegistry.GetDescriptors().size(), 1u);
EXPECT_EQ(surfaceRegistry.GetDescriptors().front().debugName, "XCUI Demo");
EXPECT_TRUE(surfaceRegistry.GetDescriptors().front().image.IsValid());
}
TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryExposesImageUvForRenderedCanvasRect) {
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
XCUIHostedPreviewSurfaceImage image = {};
surfaceRegistry.UpdateSurface(
"XCUI Demo",
static_cast<ImTextureID>(static_cast<intptr_t>(7)),
1024u,
768u,
XCEngine::UI::UIRect(128.0f, 96.0f, 512.0f, 384.0f));
ASSERT_TRUE(surfaceRegistry.TryGetSurfaceImage("XCUI Demo", image));
EXPECT_TRUE(image.IsValid());
EXPECT_EQ(image.textureId, static_cast<ImTextureID>(static_cast<intptr_t>(7)));
EXPECT_EQ(image.surfaceWidth, 1024u);
EXPECT_EQ(image.surfaceHeight, 768u);
EXPECT_FLOAT_EQ(image.uvMin.x, 0.125f);
EXPECT_FLOAT_EQ(image.uvMin.y, 0.125f);
EXPECT_FLOAT_EQ(image.uvMax.x, 0.625f);
EXPECT_FLOAT_EQ(image.uvMax.y, 0.625f);
}
TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryTracksQueuedFrameMetadataAlongsideLatestSurfaceImage) {
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
surfaceRegistry.BeginFrame();
XCEngine::UI::UIDrawData drawData = {};
XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("RegistryPreview");
drawList.AddFilledRect(
XCEngine::UI::UIRect(8.0f, 6.0f, 30.0f, 18.0f),
XCEngine::UI::UIColor(0.3f, 0.7f, 0.9f, 1.0f));
drawList.AddText(
XCEngine::UI::UIPoint(16.0f, 12.0f),
"meta",
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
12.0f);
XCUIHostedPreviewQueuedFrame queuedFrame = {};
queuedFrame.debugName = "XCUI Demo";
queuedFrame.debugSource = "tests.hosted_preview.registry";
queuedFrame.canvasRect = XCEngine::UI::UIRect(24.0f, 32.0f, 300.0f, 160.0f);
queuedFrame.logicalSize = XCEngine::UI::UISize(600.0f, 320.0f);
queuedFrame.drawData = drawData;
surfaceRegistry.RecordQueuedFrame(queuedFrame, 2u);
XCUIHostedPreviewSurfaceDescriptor descriptor = {};
ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor));
EXPECT_EQ(descriptor.debugName, "XCUI Demo");
EXPECT_EQ(descriptor.debugSource, "tests.hosted_preview.registry");
EXPECT_FLOAT_EQ(descriptor.canvasRect.x, 24.0f);
EXPECT_FLOAT_EQ(descriptor.canvasRect.y, 32.0f);
EXPECT_FLOAT_EQ(descriptor.canvasRect.width, 300.0f);
EXPECT_FLOAT_EQ(descriptor.canvasRect.height, 160.0f);
EXPECT_FLOAT_EQ(descriptor.logicalSize.width, 600.0f);
EXPECT_FLOAT_EQ(descriptor.logicalSize.height, 320.0f);
EXPECT_EQ(descriptor.queuedFrameIndex, 2u);
EXPECT_EQ(descriptor.submittedDrawListCount, 1u);
EXPECT_EQ(descriptor.submittedCommandCount, 2u);
EXPECT_TRUE(descriptor.queuedThisFrame);
EXPECT_FALSE(descriptor.image.IsValid());
surfaceRegistry.UpdateSurface(
"XCUI Demo",
static_cast<ImTextureID>(static_cast<intptr_t>(9)),
1024u,
512u,
XCEngine::UI::UIRect(128.0f, 64.0f, 320.0f, 160.0f));
ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor));
EXPECT_TRUE(descriptor.queuedThisFrame);
EXPECT_TRUE(descriptor.image.IsValid());
EXPECT_EQ(descriptor.image.textureId, static_cast<ImTextureID>(static_cast<intptr_t>(9)));
EXPECT_FLOAT_EQ(descriptor.image.uvMin.x, 0.125f);
EXPECT_FLOAT_EQ(descriptor.image.uvMin.y, 0.125f);
EXPECT_FLOAT_EQ(descriptor.image.uvMax.x, 0.4375f);
EXPECT_FLOAT_EQ(descriptor.image.uvMax.y, 0.4375f);
surfaceRegistry.BeginFrame();
ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor));
EXPECT_FALSE(descriptor.queuedThisFrame);
EXPECT_TRUE(descriptor.image.IsValid());
}
TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryRejectsInvalidSurfaceUpdatesWithoutClobberingExistingImage) {
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
XCUIHostedPreviewSurfaceDescriptor descriptor = {};
surfaceRegistry.UpdateSurface(
"XCUI Demo",
static_cast<ImTextureID>(static_cast<intptr_t>(17)),
512u,
256u,
XCEngine::UI::UIRect(64.0f, 32.0f, 256.0f, 128.0f));
ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor));
ASSERT_TRUE(descriptor.image.IsValid());
const XCUIHostedPreviewSurfaceImage originalImage = descriptor.image;
surfaceRegistry.UpdateSurface(
"XCUI Demo",
ImTextureID{},
512u,
256u,
XCEngine::UI::UIRect(0.0f, 0.0f, 128.0f, 64.0f));
surfaceRegistry.UpdateSurface(
"",
static_cast<ImTextureID>(static_cast<intptr_t>(19)),
512u,
256u,
XCEngine::UI::UIRect(0.0f, 0.0f, 128.0f, 64.0f));
surfaceRegistry.UpdateSurface(
"XCUI Demo",
static_cast<ImTextureID>(static_cast<intptr_t>(21)),
0u,
256u,
XCEngine::UI::UIRect(0.0f, 0.0f, 128.0f, 64.0f));
ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor));
EXPECT_EQ(descriptor.image.textureId, originalImage.textureId);
EXPECT_EQ(descriptor.image.surfaceWidth, originalImage.surfaceWidth);
EXPECT_EQ(descriptor.image.surfaceHeight, originalImage.surfaceHeight);
EXPECT_FLOAT_EQ(descriptor.image.uvMin.x, originalImage.uvMin.x);
EXPECT_FLOAT_EQ(descriptor.image.uvMin.y, originalImage.uvMin.y);
EXPECT_FLOAT_EQ(descriptor.image.uvMax.x, originalImage.uvMax.x);
EXPECT_FLOAT_EQ(descriptor.image.uvMax.y, originalImage.uvMax.y);
}
TEST(XCUIHostedPreviewPresenterTest, BeginFrameClearsQueuedFramesButKeepsLastDrainStatsUntilReplaced) {
XCUIHostedPreviewQueue queue = {};
XCEngine::UI::UIDrawData drawData = {};
XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("Preview");
drawList.AddFilledRect(
XCEngine::UI::UIRect(4.0f, 4.0f, 18.0f, 12.0f),
XCEngine::UI::UIColor(0.4f, 0.6f, 0.9f, 1.0f));
XCUIHostedPreviewFrame frame = {};
frame.drawData = &drawData;
ASSERT_TRUE(queue.Submit(frame));
ASSERT_EQ(queue.GetQueuedFrames().size(), 1u);
XCUIHostedPreviewDrainStats initialDrainStats = {};
initialDrainStats.queuedFrameCount = 3u;
initialDrainStats.queuedCommandCount = 7u;
initialDrainStats.renderedFrameCount = 2u;
initialDrainStats.skippedFrameCount = 1u;
queue.SetLastDrainStats(initialDrainStats);
queue.BeginFrame();
EXPECT_TRUE(queue.GetQueuedFrames().empty());
const XCUIHostedPreviewDrainStats& drainStats = queue.GetLastDrainStats();
EXPECT_EQ(drainStats.queuedFrameCount, 3u);
EXPECT_EQ(drainStats.queuedCommandCount, 7u);
EXPECT_EQ(drainStats.renderedFrameCount, 2u);
EXPECT_EQ(drainStats.skippedFrameCount, 1u);
}
TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryQueriesClearOutputForMissingOrInvalidNames) {
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
XCUIHostedPreviewSurfaceDescriptor descriptor = {};
descriptor.debugName = "stale";
descriptor.debugSource = "stale";
descriptor.queuedThisFrame = true;
XCUIHostedPreviewSurfaceImage image = {};
image.textureId = static_cast<ImTextureID>(static_cast<intptr_t>(23));
image.surfaceWidth = 64u;
image.surfaceHeight = 64u;
EXPECT_FALSE(surfaceRegistry.TryGetSurfaceDescriptor(nullptr, descriptor));
EXPECT_TRUE(descriptor.debugName.empty());
EXPECT_TRUE(descriptor.debugSource.empty());
EXPECT_FALSE(descriptor.queuedThisFrame);
EXPECT_FALSE(descriptor.image.IsValid());
EXPECT_FALSE(surfaceRegistry.TryGetSurfaceImage("", image));
EXPECT_FALSE(image.IsValid());
EXPECT_EQ(image.surfaceWidth, 0u);
EXPECT_EQ(image.surfaceHeight, 0u);
}
} // namespace

View File

@@ -0,0 +1,146 @@
#include <gtest/gtest.h>
#include "XCUIBackend/XCUIInputBridge.h"
#include <XCEngine/Input/InputTypes.h>
namespace {
using XCEngine::Editor::XCUIBackend::XCUIInputBridge;
using XCEngine::Editor::XCUIBackend::XCUIInputBridgeCaptureOptions;
using XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta;
using XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot;
using XCEngine::Editor::XCUIBackend::XCUIInputBridgeKeyState;
using XCEngine::Editor::XCUIBackend::XCUIWin32InputSource;
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
XCUIInputBridgeKeyState MakeKeyState(KeyCode keyCode, bool down, bool repeat = false) {
XCUIInputBridgeKeyState state = {};
state.keyCode = static_cast<std::int32_t>(keyCode);
state.down = down;
state.repeat = repeat;
return state;
}
} // namespace
TEST(XCUIInputBridgeTest, TranslateBuildsPointerKeyboardAndCharacterEventsFromSnapshots) {
XCUIInputBridgeFrameSnapshot previous = {};
XCUIInputBridgeFrameSnapshot current = {};
current.pointerInside = true;
current.pointerPosition = UIPoint(32.0f, 48.0f);
current.pointerButtonsDown[0] = true;
current.windowFocused = true;
current.keys.push_back(MakeKeyState(KeyCode::P, true));
current.characters.push_back(static_cast<std::uint32_t>('p'));
const XCUIInputBridgeFrameDelta delta = XCUIInputBridge::Translate(previous, current);
EXPECT_TRUE(delta.focusGained);
EXPECT_TRUE(delta.pointer.entered);
EXPECT_TRUE(delta.pointer.moved);
EXPECT_TRUE(delta.pointer.pressed[0]);
ASSERT_EQ(delta.keyboard.pressedKeys.size(), 1u);
EXPECT_EQ(delta.keyboard.pressedKeys[0], static_cast<std::int32_t>(KeyCode::P));
ASSERT_EQ(delta.keyboard.characters.size(), 1u);
EXPECT_EQ(delta.keyboard.characters[0], static_cast<std::uint32_t>('p'));
EXPECT_TRUE(delta.HasEventType(UIInputEventType::FocusGained));
EXPECT_TRUE(delta.HasEventType(UIInputEventType::PointerEnter));
EXPECT_TRUE(delta.HasEventType(UIInputEventType::PointerMove));
EXPECT_TRUE(delta.HasEventType(UIInputEventType::PointerButtonDown));
EXPECT_TRUE(delta.HasEventType(UIInputEventType::KeyDown));
EXPECT_TRUE(delta.HasEventType(UIInputEventType::Character));
}
TEST(XCUIInputBridgeTest, PrimeSuppressesSyntheticFirstFrameTransitions) {
XCUIInputBridge bridge = {};
XCUIInputBridgeFrameSnapshot baseline = {};
baseline.pointerInside = true;
baseline.pointerPosition = UIPoint(24.0f, 12.0f);
baseline.pointerButtonsDown[0] = true;
baseline.windowFocused = true;
baseline.keys.push_back(MakeKeyState(KeyCode::P, true));
bridge.Prime(baseline);
const XCUIInputBridgeFrameDelta firstDelta = bridge.Translate(baseline);
EXPECT_FALSE(firstDelta.HasEvents());
XCUIInputBridgeFrameSnapshot released = baseline;
released.pointerButtonsDown[0] = false;
released.keys.clear();
const XCUIInputBridgeFrameDelta secondDelta = bridge.Translate(released);
EXPECT_TRUE(secondDelta.pointer.released[0]);
ASSERT_EQ(secondDelta.keyboard.releasedKeys.size(), 1u);
EXPECT_EQ(secondDelta.keyboard.releasedKeys[0], static_cast<std::int32_t>(KeyCode::P));
EXPECT_TRUE(secondDelta.HasEventType(UIInputEventType::PointerButtonUp));
EXPECT_TRUE(secondDelta.HasEventType(UIInputEventType::KeyUp));
}
TEST(XCUIInputBridgeTest, Win32InputSourceCapturesPointerWheelKeyRepeatAndCharacters) {
XCUIWin32InputSource inputSource = {};
inputSource.HandleWindowMessage(nullptr, WM_SETFOCUS, 0, 0);
inputSource.HandleWindowMessage(nullptr, WM_MOUSEMOVE, 0, MAKELPARAM(64, 96));
inputSource.HandleWindowMessage(nullptr, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(64, 96));
inputSource.HandleWindowMessage(nullptr, WM_MOUSEWHEEL, MAKEWPARAM(0, WHEEL_DELTA), 0);
inputSource.HandleWindowMessage(nullptr, WM_KEYDOWN, 'P', 0);
inputSource.HandleWindowMessage(nullptr, WM_KEYDOWN, 'P', 1u << 30u);
inputSource.HandleWindowMessage(nullptr, WM_CHAR, 'p', 0);
XCUIInputBridgeCaptureOptions options = {};
options.windowFocused = true;
const XCUIInputBridgeFrameSnapshot snapshot = inputSource.CaptureSnapshot(options);
EXPECT_TRUE(snapshot.windowFocused);
EXPECT_TRUE(snapshot.pointerInside);
EXPECT_EQ(snapshot.pointerPosition.x, 64.0f);
EXPECT_EQ(snapshot.pointerPosition.y, 96.0f);
EXPECT_TRUE(snapshot.pointerButtonsDown[0]);
EXPECT_FLOAT_EQ(snapshot.wheelDelta.y, 1.0f);
EXPECT_TRUE(snapshot.IsKeyDown(static_cast<std::int32_t>(KeyCode::P)));
ASSERT_EQ(snapshot.characters.size(), 1u);
EXPECT_EQ(snapshot.characters[0], static_cast<std::uint32_t>('p'));
const XCUIInputBridgeFrameDelta delta = XCUIInputBridge::Translate(XCUIInputBridgeFrameSnapshot(), snapshot);
EXPECT_TRUE(delta.pointer.pressed[0]);
ASSERT_EQ(delta.keyboard.pressedKeys.size(), 1u);
EXPECT_EQ(delta.keyboard.pressedKeys[0], static_cast<std::int32_t>(KeyCode::P));
EXPECT_TRUE(delta.HasEventType(UIInputEventType::Character));
inputSource.ClearFrameTransients();
const XCUIInputBridgeFrameSnapshot afterClear = inputSource.CaptureSnapshot(options);
EXPECT_FLOAT_EQ(afterClear.wheelDelta.x, 0.0f);
EXPECT_FLOAT_EQ(afterClear.wheelDelta.y, 0.0f);
EXPECT_TRUE(afterClear.characters.empty());
const XCUIInputBridgeKeyState* repeatedKey =
afterClear.FindKeyState(static_cast<std::int32_t>(KeyCode::P));
ASSERT_NE(repeatedKey, nullptr);
EXPECT_FALSE(repeatedKey->repeat);
}
TEST(XCUIInputBridgeTest, Win32InputSourceClearsPressedStateOnFocusLoss) {
XCUIWin32InputSource inputSource = {};
inputSource.HandleWindowMessage(nullptr, WM_SETFOCUS, 0, 0);
inputSource.HandleWindowMessage(nullptr, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(20, 24));
inputSource.HandleWindowMessage(nullptr, WM_KEYDOWN, 'P', 0);
XCUIInputBridgeCaptureOptions options = {};
options.windowFocused = true;
XCUIInputBridgeFrameSnapshot focused = inputSource.CaptureSnapshot(options);
EXPECT_TRUE(focused.pointerButtonsDown[0]);
EXPECT_TRUE(focused.IsKeyDown(static_cast<std::int32_t>(KeyCode::P)));
inputSource.HandleWindowMessage(nullptr, WM_KILLFOCUS, 0, 0);
XCUIInputBridgeFrameSnapshot blurred = inputSource.CaptureSnapshot(options);
EXPECT_FALSE(blurred.windowFocused);
EXPECT_FALSE(blurred.pointerButtonsDown[0]);
EXPECT_FALSE(blurred.IsKeyDown(static_cast<std::int32_t>(KeyCode::P)));
}

View File

@@ -0,0 +1,192 @@
#include <gtest/gtest.h>
#include "XCUIBackend/XCUILayoutLabRuntime.h"
#include <XCEngine/UI/Types.h>
#include <string>
#include <vector>
namespace {
using XCEngine::UI::UIDrawCommand;
using XCEngine::UI::UIDrawCommandType;
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState BuildInputState(
float width = 960.0f,
float height = 640.0f) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = {};
input.canvasRect = XCEngine::UI::UIRect(0.0f, 0.0f, width, height);
input.pointerPosition = XCEngine::UI::UIPoint(width * 0.5f, height * 0.5f);
input.pointerInside = true;
return input;
}
std::vector<const UIDrawCommand*> CollectTextCommands(const XCEngine::UI::UIDrawData& drawData) {
std::vector<const UIDrawCommand*> textCommands = {};
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == UIDrawCommandType::Text) {
textCommands.push_back(&command);
}
}
}
return textCommands;
}
const UIDrawCommand* FindTextCommand(
const XCEngine::UI::UIDrawData& drawData,
const std::string& text) {
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == UIDrawCommandType::Text && command.text == text) {
return &command;
}
}
}
return nullptr;
}
std::size_t CountCommandsOfType(
const XCEngine::UI::UIDrawData& drawData,
UIDrawCommandType type) {
std::size_t count = 0;
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == type) {
++count;
}
}
}
return count;
}
} // namespace
TEST(NewEditorXCUILayoutLabRuntimeTest, UpdateBuildsLayoutSmokeFrame) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
const bool reloadSucceeded = runtime.ReloadDocuments();
const auto& frame = runtime.Update(BuildInputState());
EXPECT_EQ(frame.stats.documentsReady, reloadSucceeded);
EXPECT_EQ(frame.stats.drawListCount, frame.drawData.GetDrawListCount());
EXPECT_EQ(frame.stats.commandCount, frame.drawData.GetTotalCommandCount());
if (frame.stats.documentsReady) {
EXPECT_GT(frame.stats.drawListCount, 0u);
EXPECT_GT(frame.stats.commandCount, 0u);
EXPECT_GE(frame.stats.rowCount, 1u);
EXPECT_GE(frame.stats.columnCount, 1u);
EXPECT_GE(frame.stats.overlayCount, 1u);
EXPECT_GE(frame.stats.scrollViewCount, 2u);
XCEngine::UI::UIRect heroRect = {};
EXPECT_TRUE(runtime.TryGetElementRect("heroCard", heroRect));
EXPECT_GT(heroRect.width, 0.0f);
EXPECT_GT(heroRect.height, 0.0f);
} else {
EXPECT_FALSE(frame.stats.statusMessage.empty());
}
}
TEST(NewEditorXCUILayoutLabRuntimeTest, FrameIncludesTextCommandsWithThemeFontSizes) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& frame = runtime.Update(BuildInputState());
ASSERT_TRUE(frame.stats.documentsReady);
const std::vector<const UIDrawCommand*> textCommands = CollectTextCommands(frame.drawData);
ASSERT_FALSE(textCommands.empty());
for (const UIDrawCommand* command : textCommands) {
ASSERT_NE(command, nullptr);
EXPECT_FALSE(command->text.empty());
EXPECT_GT(command->fontSize, 0.0f);
}
const UIDrawCommand* titleCommand = FindTextCommand(frame.drawData, "XCUI Layout Lab");
ASSERT_NE(titleCommand, nullptr);
EXPECT_FLOAT_EQ(titleCommand->fontSize, 16.0f);
const UIDrawCommand* subtitleCommand = FindTextCommand(
frame.drawData,
"Editor-style panels with overlay and scroll semantics.");
ASSERT_NE(subtitleCommand, nullptr);
EXPECT_FLOAT_EQ(subtitleCommand->fontSize, 13.0f);
}
TEST(NewEditorXCUILayoutLabRuntimeTest, HoverProbeResolvesTrackedElementRect) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baseline = runtime.Update(BuildInputState());
ASSERT_TRUE(baseline.stats.documentsReady);
XCEngine::UI::UIRect probeRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("assetLighting", probeRect));
ASSERT_GT(probeRect.width, 0.0f);
ASSERT_GT(probeRect.height, 0.0f);
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = BuildInputState();
input.pointerPosition = XCEngine::UI::UIPoint(
probeRect.x + probeRect.width * 0.5f,
probeRect.y + probeRect.height * 0.5f);
const auto& frame = runtime.Update(input);
ASSERT_TRUE(frame.stats.documentsReady);
EXPECT_FALSE(frame.stats.hoveredElementId.empty());
XCEngine::UI::UIRect hoveredRect = {};
EXPECT_TRUE(runtime.TryGetElementRect(frame.stats.hoveredElementId, hoveredRect));
EXPECT_GT(hoveredRect.width, 0.0f);
EXPECT_GT(hoveredRect.height, 0.0f);
}
TEST(NewEditorXCUILayoutLabRuntimeTest, ScrollViewOffsetsContentAndAddsNestedClips) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& frame = runtime.Update(BuildInputState());
ASSERT_TRUE(frame.stats.documentsReady);
XCEngine::UI::UIRect assetListRect = {};
XCEngine::UI::UIRect headerRect = {};
XCEngine::UI::UIRect visibleItemRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("assetList", assetListRect));
ASSERT_TRUE(runtime.TryGetElementRect("assetListHeader", headerRect));
ASSERT_TRUE(runtime.TryGetElementRect("assetLighting", visibleItemRect));
EXPECT_LT(headerRect.y, assetListRect.y);
EXPECT_GT(visibleItemRect.y, assetListRect.y);
EXPECT_LT(visibleItemRect.y, assetListRect.y + assetListRect.height);
EXPECT_EQ(
CountCommandsOfType(frame.drawData, UIDrawCommandType::PushClipRect),
frame.stats.clipPushCommandCount);
EXPECT_EQ(
CountCommandsOfType(frame.drawData, UIDrawCommandType::PopClipRect),
frame.stats.clipPopCommandCount);
EXPECT_GE(frame.stats.clipPushCommandCount, 3u);
EXPECT_GE(frame.stats.clipPopCommandCount, 3u);
}
TEST(NewEditorXCUILayoutLabRuntimeTest, HoverIgnoresClippedScrollViewContent) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baseline = runtime.Update(BuildInputState());
ASSERT_TRUE(baseline.stats.documentsReady);
XCEngine::UI::UIRect assetListRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("assetList", assetListRect));
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = BuildInputState();
input.pointerPosition = XCEngine::UI::UIPoint(
assetListRect.x + assetListRect.width * 0.5f,
assetListRect.y + assetListRect.height + 6.0f);
const auto& frame = runtime.Update(input);
ASSERT_TRUE(frame.stats.documentsReady);
EXPECT_TRUE(frame.stats.hoveredElementId.empty());
}

View File

@@ -0,0 +1,242 @@
#include <gtest/gtest.h>
#include "XCUIBackend/XCUIRHICommandCompiler.h"
namespace {
using XCEngine::Editor::XCUIBackend::XCUIRHICommandCompiler;
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::UITextureHandle;
using XCEngine::UI::UITextureHandleKind;
class StubTextGlyphProvider final : public XCUIRHICommandCompiler::TextGlyphProvider {
public:
bool BeginText(
float requestedFontSize,
XCUIRHICommandCompiler::TextRunContext& outContext) const override {
outContext.requestedFontSize = requestedFontSize;
outContext.resolvedFontSize = requestedFontSize > 0.0f ? requestedFontSize : 14.0f;
outContext.lineHeight = 12.0f;
outContext.texture = UITextureHandle{ 99u, 256u, 256u, UITextureHandleKind::ShaderResourceView };
return true;
}
bool ResolveGlyph(
const XCUIRHICommandCompiler::TextRunContext&,
std::uint32_t codepoint,
XCUIRHICommandCompiler::TextGlyph& outGlyph) const override {
switch (codepoint) {
case 'A':
outGlyph.x0 = 0.0f;
outGlyph.y0 = 0.0f;
outGlyph.x1 = 8.0f;
outGlyph.y1 = 10.0f;
outGlyph.u0 = 0.0f;
outGlyph.v0 = 0.0f;
outGlyph.u1 = 0.25f;
outGlyph.v1 = 0.5f;
outGlyph.advanceX = 8.0f;
outGlyph.visible = true;
return true;
case 'B':
outGlyph.x0 = 0.0f;
outGlyph.y0 = 0.0f;
outGlyph.x1 = 7.0f;
outGlyph.y1 = 10.0f;
outGlyph.u0 = 0.25f;
outGlyph.v0 = 0.0f;
outGlyph.u1 = 0.5f;
outGlyph.v1 = 0.5f;
outGlyph.advanceX = 7.0f;
outGlyph.visible = true;
return true;
default:
return false;
}
}
};
TEST(XCUIRHICommandCompilerTest, CompileMergesAdjacentColorAndTexturedCommandsPerDrawList) {
XCUIRHICommandCompiler compiler = {};
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("Batches");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, 10.0f, 10.0f), UIColor(1.0f, 0.0f, 0.0f, 1.0f));
drawList.AddFilledRect(UIRect(10.0f, 0.0f, 10.0f, 10.0f), UIColor(0.0f, 1.0f, 0.0f, 1.0f));
const UITextureHandle texture{ 7u, 64u, 64u, UITextureHandleKind::ShaderResourceView };
drawList.AddImage(UIRect(0.0f, 20.0f, 10.0f, 10.0f), texture, UIColor(1.0f, 1.0f, 1.0f, 1.0f));
drawList.AddImage(UIRect(10.0f, 20.0f, 10.0f, 10.0f), texture, UIColor(0.5f, 0.5f, 0.5f, 1.0f));
XCUIRHICommandCompiler::CompileConfig config = {};
config.surfaceClipRect = UIRect(0.0f, 0.0f, 100.0f, 100.0f);
XCUIRHICommandCompiler::CompiledDrawData compiled = {};
compiler.Compile(drawData, config, compiled);
ASSERT_EQ(compiled.batches.size(), 2u);
EXPECT_EQ(compiled.batches[0].kind, XCUIRHICommandCompiler::BatchKind::Color);
EXPECT_EQ(compiled.batches[0].commandCount, 2u);
EXPECT_EQ(compiled.batches[0].vertexCount, 12u);
EXPECT_EQ(compiled.batches[1].kind, XCUIRHICommandCompiler::BatchKind::Textured);
EXPECT_EQ(compiled.batches[1].commandCount, 2u);
EXPECT_EQ(compiled.batches[1].vertexCount, 12u);
EXPECT_EQ(compiled.batches[1].texture.nativeHandle, texture.nativeHandle);
EXPECT_EQ(compiled.stats.compiledCommandCount, 4u);
EXPECT_EQ(compiled.stats.batchCount, 2u);
EXPECT_EQ(compiled.stats.colorVertexCount, 12u);
EXPECT_EQ(compiled.stats.texturedVertexCount, 12u);
}
TEST(XCUIRHICommandCompilerTest, CompileTracksClipStackTransitionsAndUnderflow) {
XCUIRHICommandCompiler compiler = {};
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("ClipStack");
drawList.PushClipRect(UIRect(0.0f, 0.0f, 50.0f, 50.0f));
drawList.PushClipRect(UIRect(10.0f, 10.0f, 40.0f, 40.0f));
drawList.AddFilledRect(UIRect(0.0f, 0.0f, 30.0f, 30.0f), UIColor(1.0f, 0.0f, 0.0f, 1.0f));
drawList.PopClipRect();
drawList.AddFilledRect(UIRect(0.0f, 0.0f, 20.0f, 20.0f), UIColor(0.0f, 1.0f, 0.0f, 1.0f));
drawList.PopClipRect();
drawList.PopClipRect();
XCUIRHICommandCompiler::CompileConfig config = {};
config.surfaceClipRect = UIRect(0.0f, 0.0f, 100.0f, 100.0f);
XCUIRHICommandCompiler::CompiledDrawData compiled = {};
compiler.Compile(drawData, config, compiled);
ASSERT_EQ(compiled.batches.size(), 2u);
EXPECT_EQ(compiled.stats.compiledCommandCount, 7u);
EXPECT_EQ(compiled.stats.clipPushCommandCount, 2u);
EXPECT_EQ(compiled.stats.clipPopCommandCount, 3u);
EXPECT_EQ(compiled.stats.clipStackUnderflowCount, 1u);
EXPECT_EQ(compiled.stats.maxClipDepth, 2u);
EXPECT_EQ(compiled.stats.danglingClipDepth, 0u);
EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.x, 10.0f);
EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.y, 10.0f);
EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.width, 40.0f);
EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.height, 40.0f);
ASSERT_GE(compiled.colorVertices.size(), 12u);
EXPECT_FLOAT_EQ(compiled.colorVertices[0].position[0], 10.0f);
EXPECT_FLOAT_EQ(compiled.colorVertices[0].position[1], 10.0f);
EXPECT_FLOAT_EQ(compiled.colorVertices[6].position[0], 0.0f);
EXPECT_FLOAT_EQ(compiled.colorVertices[6].position[1], 0.0f);
}
TEST(XCUIRHICommandCompilerTest, CompileUsesTextGlyphProviderOutputForTextBatches) {
XCUIRHICommandCompiler compiler = {};
StubTextGlyphProvider glyphProvider = {};
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("Text");
drawList.AddText(UIPoint(4.0f, 6.0f), "A\nB", UIColor(1.0f, 1.0f, 1.0f, 1.0f), 16.0f);
XCUIRHICommandCompiler::CompileConfig config = {};
config.surfaceClipRect = UIRect(0.0f, 0.0f, 100.0f, 100.0f);
config.textGlyphProvider = &glyphProvider;
XCUIRHICommandCompiler::CompiledDrawData compiled = {};
compiler.Compile(drawData, config, compiled);
ASSERT_EQ(compiled.batches.size(), 1u);
EXPECT_EQ(compiled.batches[0].kind, XCUIRHICommandCompiler::BatchKind::Textured);
EXPECT_EQ(compiled.batches[0].vertexCount, 12u);
EXPECT_EQ(compiled.batches[0].texture.nativeHandle, 99u);
EXPECT_EQ(compiled.stats.textCommandCount, 1u);
EXPECT_EQ(compiled.stats.compiledCommandCount, 1u);
ASSERT_EQ(compiled.texturedVertices.size(), 12u);
EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[0], 4.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[1], 6.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[6].position[0], 4.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[6].position[1], 18.0f);
}
TEST(XCUIRHICommandCompilerTest, CompileReportsUnsupportedTextWhenNoGlyphProviderIsAvailable) {
XCUIRHICommandCompiler compiler = {};
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("UnsupportedText");
drawList.AddText(UIPoint(0.0f, 0.0f), "xcui", UIColor(1.0f, 1.0f, 1.0f, 1.0f), 12.0f);
XCUIRHICommandCompiler::CompileConfig config = {};
config.surfaceClipRect = UIRect(0.0f, 0.0f, 32.0f, 32.0f);
XCUIRHICommandCompiler::CompiledDrawData compiled = {};
compiler.Compile(drawData, config, compiled);
EXPECT_TRUE(compiled.Empty());
EXPECT_EQ(compiled.stats.textCommandCount, 1u);
EXPECT_EQ(compiled.stats.compiledCommandCount, 0u);
EXPECT_EQ(compiled.stats.skippedCommandCount, 1u);
EXPECT_EQ(compiled.stats.unsupportedCommandCount, 1u);
}
TEST(XCUIRHICommandCompilerTest, CompileClipsImageCommandsAndAdjustsUvCoordinates) {
XCUIRHICommandCompiler compiler = {};
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("ClippedImage");
drawList.PushClipRect(UIRect(10.0f, 8.0f, 12.0f, 10.0f));
drawList.AddImage(
UIRect(4.0f, 4.0f, 20.0f, 20.0f),
UITextureHandle{ 77u, 32u, 32u, UITextureHandleKind::ShaderResourceView },
UIColor(0.7f, 0.8f, 0.9f, 1.0f));
XCUIRHICommandCompiler::CompileConfig config = {};
config.surfaceClipRect = UIRect(0.0f, 0.0f, 64.0f, 64.0f);
XCUIRHICommandCompiler::CompiledDrawData compiled = {};
compiler.Compile(drawData, config, compiled);
ASSERT_EQ(compiled.batches.size(), 1u);
ASSERT_EQ(compiled.texturedVertices.size(), 6u);
EXPECT_EQ(compiled.batches[0].kind, XCUIRHICommandCompiler::BatchKind::Textured);
EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.x, 10.0f);
EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.y, 8.0f);
EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.width, 12.0f);
EXPECT_FLOAT_EQ(compiled.batches[0].clipRect.height, 10.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[0], 10.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[1], 8.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[0].uv[0], 0.3f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[0].uv[1], 0.2f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[1].position[0], 22.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[1].uv[0], 0.9f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[2].position[1], 18.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[2].uv[1], 0.7f);
EXPECT_EQ(compiled.stats.imageCommandCount, 1u);
EXPECT_EQ(compiled.stats.compiledCommandCount, 2u);
}
TEST(XCUIRHICommandCompilerTest, CompilePreservesMirroredImageUvForNegativeRectExtents) {
XCUIRHICommandCompiler compiler = {};
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("MirroredImage");
drawList.AddImage(
UIRect(24.0f, 18.0f, -16.0f, -12.0f),
UITextureHandle{ 91u, 64u, 64u, UITextureHandleKind::ShaderResourceView },
UIColor(1.0f, 1.0f, 1.0f, 1.0f));
XCUIRHICommandCompiler::CompileConfig config = {};
config.surfaceClipRect = UIRect(0.0f, 0.0f, 64.0f, 64.0f);
XCUIRHICommandCompiler::CompiledDrawData compiled = {};
compiler.Compile(drawData, config, compiled);
ASSERT_EQ(compiled.batches.size(), 1u);
ASSERT_EQ(compiled.texturedVertices.size(), 6u);
EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[0], 8.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[0].position[1], 6.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[0].uv[0], 1.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[0].uv[1], 1.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[1].position[0], 24.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[1].uv[0], 0.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[2].position[1], 18.0f);
EXPECT_FLOAT_EQ(compiled.texturedVertices[2].uv[1], 0.0f);
EXPECT_EQ(compiled.stats.imageCommandCount, 1u);
EXPECT_EQ(compiled.stats.compiledCommandCount, 1u);
}
} // namespace

View File

@@ -0,0 +1,202 @@
#include <gtest/gtest.h>
#include "XCUIBackend/XCUIRHICommandSupport.h"
#include <string>
namespace {
using XCEngine::Editor::XCUIBackend::AccumulateXCUIRHICommandSupport;
using XCEngine::Editor::XCUIBackend::AnalyzeXCUIRHICommandSupport;
using XCEngine::Editor::XCUIBackend::BuildXCUIRHICommandSupportDiagnostic;
using XCEngine::Editor::XCUIBackend::ClassifyXCUIRHICommandSupport;
using XCEngine::Editor::XCUIBackend::SummarizeXCUIRHICommandSupport;
using XCEngine::Editor::XCUIBackend::XCUIRHICommandCategory;
using XCEngine::Editor::XCUIBackend::XCUIRHICommandDiagnosticOptions;
using XCEngine::Editor::XCUIBackend::XCUIRHICommandSupportReason;
using XCEngine::Editor::XCUIBackend::XCUIRHICommandSupportStats;
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawCommand;
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::UITextureHandle;
using XCEngine::UI::UITextureHandleKind;
UITextureHandle MakeShaderResourceTexture() {
UITextureHandle texture = {};
texture.nativeHandle = 42u;
texture.width = 64u;
texture.height = 64u;
texture.kind = UITextureHandleKind::ShaderResourceView;
return texture;
}
UIDrawCommand MakeUnknownCommand() {
UIDrawCommand command = {};
command.type = static_cast<UIDrawCommandType>(255);
return command;
}
} // namespace
TEST(XCUIRHICommandSupportTest, ClassifySupportedCommandTypes) {
UIDrawList drawList("Supported");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, 12.0f, 10.0f), UIColor(1.0f, 0.0f, 0.0f, 1.0f));
drawList.AddRectOutline(
UIRect(1.0f, 2.0f, 8.0f, 5.0f),
UIColor(0.0f, 1.0f, 0.0f, 1.0f),
2.0f,
1.0f);
drawList.AddText(UIPoint(4.0f, 5.0f), "label", UIColor(1.0f, 1.0f, 1.0f, 1.0f), 14.0f);
drawList.AddImage(
UIRect(2.0f, 3.0f, 18.0f, 14.0f),
MakeShaderResourceTexture(),
UIColor(1.0f, 1.0f, 1.0f, 1.0f));
drawList.PushClipRect(UIRect(0.0f, 0.0f, 24.0f, 20.0f));
drawList.PopClipRect();
const auto& commands = drawList.GetCommands();
EXPECT_EQ(
ClassifyXCUIRHICommandSupport(commands[0]).category,
XCUIRHICommandCategory::FilledRect);
EXPECT_EQ(
ClassifyXCUIRHICommandSupport(commands[1]).category,
XCUIRHICommandCategory::RectOutline);
EXPECT_EQ(
ClassifyXCUIRHICommandSupport(commands[2]).category,
XCUIRHICommandCategory::Text);
EXPECT_EQ(
ClassifyXCUIRHICommandSupport(commands[3]).category,
XCUIRHICommandCategory::Image);
EXPECT_EQ(
ClassifyXCUIRHICommandSupport(commands[4]).category,
XCUIRHICommandCategory::PushClipRect);
EXPECT_EQ(
ClassifyXCUIRHICommandSupport(commands[5]).category,
XCUIRHICommandCategory::PopClipRect);
for (const UIDrawCommand& command : commands) {
EXPECT_TRUE(ClassifyXCUIRHICommandSupport(command).IsSupported());
}
}
TEST(XCUIRHICommandSupportTest, InvalidImageCommandIsClassifiedAsUnsupportedTexture) {
UIDrawCommand command = {};
command.type = UIDrawCommandType::Image;
command.rect = UIRect(0.0f, 0.0f, 12.0f, 12.0f);
command.texture.nativeHandle = 7u;
command.texture.width = 32u;
command.texture.height = 32u;
command.texture.kind = UITextureHandleKind::ImGuiDescriptor;
const auto classification = ClassifyXCUIRHICommandSupport(command);
EXPECT_EQ(classification.category, XCUIRHICommandCategory::Image);
EXPECT_EQ(
classification.supportReason,
XCUIRHICommandSupportReason::UnsupportedImageTexture);
EXPECT_FALSE(classification.IsSupported());
}
TEST(XCUIRHICommandSupportTest, UnknownCommandTypeIsClassifiedSeparately) {
const auto classification = ClassifyXCUIRHICommandSupport(MakeUnknownCommand());
EXPECT_EQ(classification.category, XCUIRHICommandCategory::Unknown);
EXPECT_EQ(
classification.supportReason,
XCUIRHICommandSupportReason::UnsupportedUnknownCommand);
EXPECT_FALSE(classification.IsSupported());
}
TEST(XCUIRHICommandSupportTest, AnalyzeDrawDataAggregatesSupportedAndUnsupportedCounts) {
UIDrawData drawData = {};
UIDrawList& firstDrawList = drawData.EmplaceDrawList("First");
firstDrawList.AddFilledRect(
UIRect(0.0f, 0.0f, 8.0f, 8.0f),
UIColor(1.0f, 0.0f, 0.0f, 1.0f));
firstDrawList.AddImage(
UIRect(2.0f, 2.0f, 12.0f, 10.0f),
MakeShaderResourceTexture(),
UIColor(1.0f, 1.0f, 1.0f, 1.0f));
UIDrawList& secondDrawList = drawData.EmplaceDrawList("Second");
secondDrawList.PushClipRect(UIRect(0.0f, 0.0f, 100.0f, 60.0f));
secondDrawList.AddText(
UIPoint(5.0f, 6.0f),
"status",
UIColor(1.0f, 1.0f, 1.0f, 1.0f),
12.0f);
UITextureHandle invalidTexture = {};
invalidTexture.nativeHandle = 9u;
invalidTexture.width = 32u;
invalidTexture.height = 32u;
invalidTexture.kind = UITextureHandleKind::ImGuiDescriptor;
secondDrawList.AddImage(
UIRect(1.0f, 1.0f, 6.0f, 6.0f),
invalidTexture,
UIColor(1.0f, 1.0f, 1.0f, 1.0f));
const auto stats = AnalyzeXCUIRHICommandSupport(drawData);
EXPECT_EQ(stats.drawListCount, 2u);
EXPECT_EQ(stats.commandCount, 5u);
EXPECT_EQ(stats.filledRectCommandCount, 1u);
EXPECT_EQ(stats.textCommandCount, 1u);
EXPECT_EQ(stats.imageCommandCount, 2u);
EXPECT_EQ(stats.clipPushCommandCount, 1u);
EXPECT_EQ(stats.clipPopCommandCount, 0u);
EXPECT_EQ(stats.supportedCommandCount, 4u);
EXPECT_EQ(stats.unsupportedCommandCount, 1u);
EXPECT_EQ(stats.unsupportedImageCommandCount, 1u);
EXPECT_EQ(stats.unsupportedUnknownCommandCount, 0u);
EXPECT_FALSE(stats.SupportsAllCommands());
}
TEST(XCUIRHICommandSupportTest, DiagnosticIncludesUnsupportedReasonsAndCanBeCustomized) {
XCUIRHICommandSupportStats stats = {};
AccumulateXCUIRHICommandSupport(MakeUnknownCommand(), stats);
UIDrawCommand invalidImage = {};
invalidImage.type = UIDrawCommandType::Image;
invalidImage.texture.nativeHandle = 11u;
invalidImage.texture.width = 16u;
invalidImage.texture.height = 16u;
invalidImage.texture.kind = UITextureHandleKind::ImGuiDescriptor;
AccumulateXCUIRHICommandSupport(invalidImage, stats);
const std::string defaultDiagnostic = BuildXCUIRHICommandSupportDiagnostic(stats);
EXPECT_NE(defaultDiagnostic.find("2 command(s) will be skipped by native overlay: "), std::string::npos);
EXPECT_NE(defaultDiagnostic.find("1 image command(s) missing valid ShaderResourceView textures"), std::string::npos);
EXPECT_NE(defaultDiagnostic.find("1 unknown command type(s)"), std::string::npos);
XCUIRHICommandDiagnosticOptions options = {};
options.noCommandsMessage = "No overlay commands.";
options.allSupportedMessage = "Everything supported.";
options.unsupportedPrefix = "command(s) rejected:";
const std::string customDiagnostic = BuildXCUIRHICommandSupportDiagnostic(stats, options);
EXPECT_NE(customDiagnostic.find("2 command(s) rejected:"), std::string::npos);
}
TEST(XCUIRHICommandSupportTest, SummaryReturnsNoCommandAndAllSupportedMessages) {
const auto emptySummary = SummarizeXCUIRHICommandSupport(UIDrawData());
EXPECT_EQ(emptySummary.stats.commandCount, 0u);
EXPECT_EQ(emptySummary.diagnostic, "Overlay runtime produced no commands.");
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("Supported");
drawList.AddFilledRect(
UIRect(0.0f, 0.0f, 4.0f, 4.0f),
UIColor(0.2f, 0.4f, 0.8f, 1.0f));
const auto supportedSummary = SummarizeXCUIRHICommandSupport(drawData);
EXPECT_EQ(supportedSummary.stats.supportedCommandCount, 1u);
EXPECT_TRUE(supportedSummary.stats.SupportsAllCommands());
EXPECT_EQ(supportedSummary.diagnostic, "All commands preflight for native overlay.");
}

View File

@@ -0,0 +1,710 @@
#include <gtest/gtest.h>
#include "XCUIBackend/XCUIRHIRenderBackend.h"
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/RHI/RHIBuffer.h>
#include <XCEngine/RHI/RHICapabilities.h>
#include <XCEngine/RHI/RHICommandList.h>
#include <XCEngine/RHI/RHICommandQueue.h>
#include <XCEngine/RHI/RHIDevice.h>
#include <XCEngine/RHI/RHIDescriptorPool.h>
#include <XCEngine/RHI/RHIDescriptorSet.h>
#include <XCEngine/RHI/RHIPipelineLayout.h>
#include <XCEngine/RHI/RHIPipelineState.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/RHI/RHISampler.h>
#include <XCEngine/UI/DrawData.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <type_traits>
#include <vector>
namespace {
using XCEngine::Editor::XCUIBackend::IXCUITextAtlasProvider;
using XCEngine::Editor::XCUIBackend::XCUIRHIRenderBackend;
using XCEngine::Rendering::RenderContext;
using XCEngine::Rendering::RenderSurface;
using XCEngine::RHI::BlendDesc;
using XCEngine::RHI::BufferDesc;
using XCEngine::RHI::BufferType;
using XCEngine::RHI::CommandQueueType;
using XCEngine::RHI::ComparisonFunc;
using XCEngine::RHI::DepthStencilStateDesc;
using XCEngine::RHI::DescriptorHeapType;
using XCEngine::RHI::DescriptorPoolDesc;
using XCEngine::RHI::DescriptorSetLayoutBinding;
using XCEngine::RHI::DescriptorSetLayoutDesc;
using XCEngine::RHI::Format;
using XCEngine::RHI::GraphicsPipelineDesc;
using XCEngine::RHI::InputLayoutDesc;
using XCEngine::RHI::PipelineStateHash;
using XCEngine::RHI::PipelineType;
using XCEngine::RHI::PrimitiveTopology;
using XCEngine::RHI::RasterizerDesc;
using XCEngine::RHI::Rect;
using XCEngine::RHI::ResourceStates;
using XCEngine::RHI::ResourceViewDesc;
using XCEngine::RHI::ResourceViewDimension;
using XCEngine::RHI::ResourceViewType;
using XCEngine::RHI::RHICommandList;
using XCEngine::RHI::RHICommandQueue;
using XCEngine::RHI::RHIDescriptorPool;
using XCEngine::RHI::RHIDescriptorSet;
using XCEngine::RHI::RHIDevice;
using XCEngine::RHI::RHIPipelineLayout;
using XCEngine::RHI::RHIPipelineState;
using XCEngine::RHI::RHIResourceView;
using XCEngine::RHI::RHISampler;
using XCEngine::RHI::RHIBuffer;
using XCEngine::RHI::RHIShader;
using XCEngine::RHI::RHIType;
using XCEngine::RHI::SamplerDesc;
using XCEngine::RHI::Viewport;
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIRect;
using XCEngine::UI::UITextureHandle;
using XCEngine::UI::UITextureHandleKind;
class FakeResourceViewBase {
public:
explicit FakeResourceViewBase(
ResourceViewType viewType,
ResourceViewDimension dimension = ResourceViewDimension::Texture2D,
Format format = Format::R8G8B8A8_UNorm)
: m_viewType(viewType)
, m_dimension(dimension)
, m_format(format) {
}
void ShutdownBase() {
m_valid = false;
}
void* GetNativeHandleBase() {
return this;
}
bool IsValidBase() const {
return m_valid;
}
ResourceViewType GetViewTypeBase() const {
return m_viewType;
}
ResourceViewDimension GetDimensionBase() const {
return m_dimension;
}
Format GetFormatBase() const {
return m_format;
}
void SetValid(bool valid) {
m_valid = valid;
}
private:
ResourceViewType m_viewType = ResourceViewType::ShaderResource;
ResourceViewDimension m_dimension = ResourceViewDimension::Texture2D;
Format m_format = Format::R8G8B8A8_UNorm;
bool m_valid = true;
};
class FakeShaderResourceView final : public XCEngine::RHI::RHIShaderResourceView {
public:
explicit FakeShaderResourceView(bool valid = true)
: m_base(ResourceViewType::ShaderResource) {
m_base.SetValid(valid);
}
void Shutdown() override { m_base.ShutdownBase(); }
void* GetNativeHandle() override { return m_base.GetNativeHandleBase(); }
bool IsValid() const override { return m_base.IsValidBase(); }
ResourceViewType GetViewType() const override { return m_base.GetViewTypeBase(); }
ResourceViewDimension GetDimension() const override { return m_base.GetDimensionBase(); }
Format GetFormat() const override { return m_base.GetFormatBase(); }
private:
FakeResourceViewBase m_base;
};
class FakeRenderTargetView final : public XCEngine::RHI::RHIRenderTargetView {
public:
FakeRenderTargetView()
: m_base(ResourceViewType::RenderTarget) {
}
void Shutdown() override { m_base.ShutdownBase(); }
void* GetNativeHandle() override { return m_base.GetNativeHandleBase(); }
bool IsValid() const override { return m_base.IsValidBase(); }
ResourceViewType GetViewType() const override { return m_base.GetViewTypeBase(); }
ResourceViewDimension GetDimension() const override { return m_base.GetDimensionBase(); }
Format GetFormat() const override { return m_base.GetFormatBase(); }
private:
FakeResourceViewBase m_base;
};
class FakeVertexBufferView final : public XCEngine::RHI::RHIVertexBufferView {
public:
explicit FakeVertexBufferView(std::uint32_t size, std::uint32_t stride)
: m_size(size)
, m_stride(stride)
, m_base(ResourceViewType::VertexBuffer, ResourceViewDimension::Buffer, Format::Unknown) {
}
void Shutdown() override { m_base.ShutdownBase(); }
void* GetNativeHandle() override { return m_base.GetNativeHandleBase(); }
bool IsValid() const override { return m_base.IsValidBase(); }
ResourceViewType GetViewType() const override { return m_base.GetViewTypeBase(); }
ResourceViewDimension GetDimension() const override { return m_base.GetDimensionBase(); }
Format GetFormat() const override { return m_base.GetFormatBase(); }
std::uint64_t GetBufferAddress() const override { return 0u; }
std::uint32_t GetSize() const override { return m_size; }
std::uint32_t GetStride() const override { return m_stride; }
private:
std::uint32_t m_size = 0;
std::uint32_t m_stride = 0;
FakeResourceViewBase m_base;
};
class FakeBuffer final : public RHIBuffer {
public:
explicit FakeBuffer(const BufferDesc& desc)
: m_size(desc.size)
, m_stride(desc.stride)
, m_type(static_cast<BufferType>(desc.bufferType))
, m_storage(static_cast<std::size_t>(desc.size), 0u) {
}
void* Map() override { return m_storage.data(); }
void Unmap() override {}
void SetData(const void* data, size_t size, size_t offset = 0) override {
if (data == nullptr || offset + size > m_storage.size()) {
return;
}
std::memcpy(m_storage.data() + offset, data, size);
}
std::uint64_t GetSize() const override { return m_size; }
BufferType GetBufferType() const override { return m_type; }
void SetBufferType(BufferType type) override { m_type = type; }
std::uint32_t GetStride() const override { return m_stride; }
void SetStride(std::uint32_t stride) override { m_stride = stride; }
void* GetNativeHandle() override { return this; }
ResourceStates GetState() const override { return m_state; }
void SetState(ResourceStates state) override { m_state = state; }
const std::string& GetName() const override { return m_name; }
void SetName(const std::string& name) override { m_name = name; }
void Shutdown() override { m_storage.clear(); }
private:
std::uint64_t m_size = 0;
std::uint32_t m_stride = 0;
BufferType m_type = BufferType::Vertex;
ResourceStates m_state = ResourceStates::Common;
std::string m_name = {};
std::vector<std::uint8_t> m_storage = {};
};
class FakeDescriptorSet final : public RHIDescriptorSet {
public:
explicit FakeDescriptorSet(const DescriptorSetLayoutDesc& layout) {
if (layout.bindings != nullptr && layout.bindingCount > 0u) {
m_bindings.assign(layout.bindings, layout.bindings + layout.bindingCount);
m_views.resize(layout.bindingCount, nullptr);
m_samplers.resize(layout.bindingCount, nullptr);
}
}
void Shutdown() override {
m_views.clear();
m_samplers.clear();
m_constantBuffer.clear();
m_constantDirty = false;
}
void Bind() override {}
void Unbind() override {}
void Update(std::uint32_t offset, RHIResourceView* view) override {
if (offset >= m_views.size()) {
m_views.resize(offset + 1u, nullptr);
}
m_views[offset] = view;
}
void UpdateSampler(std::uint32_t offset, RHISampler* sampler) override {
if (offset >= m_samplers.size()) {
m_samplers.resize(offset + 1u, nullptr);
}
m_samplers[offset] = sampler;
}
void WriteConstant(std::uint32_t, const void* data, size_t size, size_t offset = 0) override {
if (data == nullptr) {
return;
}
if (m_constantBuffer.size() < offset + size) {
m_constantBuffer.resize(offset + size, 0u);
}
std::memcpy(m_constantBuffer.data() + offset, data, size);
m_constantDirty = true;
}
std::uint32_t GetBindingCount() const override {
return static_cast<std::uint32_t>(m_bindings.size());
}
const DescriptorSetLayoutBinding* GetBindings() const override {
return m_bindings.empty() ? nullptr : m_bindings.data();
}
void* GetConstantBufferData() override {
return m_constantBuffer.empty() ? nullptr : m_constantBuffer.data();
}
size_t GetConstantBufferSize() const override {
return m_constantBuffer.size();
}
bool IsConstantDirty() const override {
return m_constantDirty;
}
void MarkConstantClean() override {
m_constantDirty = false;
}
private:
std::vector<DescriptorSetLayoutBinding> m_bindings = {};
std::vector<RHIResourceView*> m_views = {};
std::vector<RHISampler*> m_samplers = {};
std::vector<std::uint8_t> m_constantBuffer = {};
bool m_constantDirty = false;
};
class FakeDescriptorPool final : public RHIDescriptorPool {
public:
explicit FakeDescriptorPool(const DescriptorPoolDesc& desc)
: m_desc(desc) {
}
bool Initialize(const DescriptorPoolDesc& desc) override {
m_desc = desc;
return true;
}
void Shutdown() override {}
void* GetNativeHandle() override { return this; }
std::uint32_t GetDescriptorCount() const override { return m_desc.descriptorCount; }
DescriptorHeapType GetType() const override { return m_desc.type; }
RHIDescriptorSet* AllocateSet(const DescriptorSetLayoutDesc& layout) override {
++m_allocationCount;
return new FakeDescriptorSet(layout);
}
void FreeSet(RHIDescriptorSet* set) override {
delete set;
}
std::uint32_t GetAllocationCount() const {
return m_allocationCount;
}
private:
DescriptorPoolDesc m_desc = {};
std::uint32_t m_allocationCount = 0;
};
class FakePipelineLayout final : public RHIPipelineLayout {
public:
bool Initialize(const XCEngine::RHI::RHIPipelineLayoutDesc& desc) override {
m_desc = desc;
return true;
}
void Shutdown() override {}
void* GetNativeHandle() override { return this; }
private:
XCEngine::RHI::RHIPipelineLayoutDesc m_desc = {};
};
class FakePipelineState final : public RHIPipelineState {
public:
explicit FakePipelineState(const GraphicsPipelineDesc& desc)
: m_inputLayout(desc.inputLayout)
, m_rasterizer(desc.rasterizerState)
, m_blend(desc.blendState)
, m_depthStencil(desc.depthStencilState) {
}
void SetInputLayout(const InputLayoutDesc& layout) override { m_inputLayout = layout; }
void SetRasterizerState(const RasterizerDesc& state) override { m_rasterizer = state; }
void SetBlendState(const BlendDesc& state) override { m_blend = state; }
void SetDepthStencilState(const DepthStencilStateDesc& state) override { m_depthStencil = state; }
void SetTopology(std::uint32_t topologyType) override { m_topologyType = topologyType; }
void SetRenderTargetFormats(std::uint32_t, const std::uint32_t*, std::uint32_t) override {}
void SetSampleCount(std::uint32_t count) override { m_sampleCount = count; }
void SetComputeShader(RHIShader* shader) override { m_computeShader = shader; }
const RasterizerDesc& GetRasterizerState() const override { return m_rasterizer; }
const BlendDesc& GetBlendState() const override { return m_blend; }
const DepthStencilStateDesc& GetDepthStencilState() const override { return m_depthStencil; }
const InputLayoutDesc& GetInputLayout() const override { return m_inputLayout; }
PipelineStateHash GetHash() const override { return {}; }
RHIShader* GetComputeShader() const override { return m_computeShader; }
bool HasComputeShader() const override { return m_computeShader != nullptr; }
bool IsValid() const override { return true; }
void EnsureValid() override {}
void Shutdown() override {}
void Bind() override {}
void Unbind() override {}
void* GetNativeHandle() override { return this; }
PipelineType GetType() const override { return PipelineType::Graphics; }
private:
InputLayoutDesc m_inputLayout = {};
RasterizerDesc m_rasterizer = {};
BlendDesc m_blend = {};
DepthStencilStateDesc m_depthStencil = {};
std::uint32_t m_topologyType = 0u;
std::uint32_t m_sampleCount = 1u;
RHIShader* m_computeShader = nullptr;
};
class FakeSampler final : public RHISampler {
public:
void Shutdown() override {}
void Bind(unsigned int) override {}
void Unbind(unsigned int) override {}
void* GetNativeHandle() override { return this; }
unsigned int GetID() override { return 1u; }
};
class FakeCommandQueue final : public RHICommandQueue {
public:
void Shutdown() override {}
void ExecuteCommandLists(std::uint32_t, void**) override {}
void Signal(XCEngine::RHI::RHIFence*, std::uint64_t) override {}
void Wait(XCEngine::RHI::RHIFence*, std::uint64_t) override {}
std::uint64_t GetCompletedValue() override { return 0u; }
void WaitForIdle() override {}
CommandQueueType GetType() const override { return CommandQueueType::Direct; }
std::uint64_t GetTimestampFrequency() const override { return 0u; }
void* GetNativeHandle() override { return this; }
void WaitForPreviousFrame() override {}
std::uint64_t GetCurrentFrame() const override { return 0u; }
};
class FakeCommandList final : public RHICommandList {
public:
void Shutdown() override {}
void Reset() override {}
void Close() override {}
void TransitionBarrier(RHIResourceView*, ResourceStates, ResourceStates) override {}
void BeginRenderPass(XCEngine::RHI::RHIRenderPass*, XCEngine::RHI::RHIFramebuffer*, const Rect&, std::uint32_t, const XCEngine::RHI::ClearValue*) override {}
void EndRenderPass() override {}
void SetShader(RHIShader*) override {}
void SetPipelineState(RHIPipelineState* pso) override {
pipelineStateHistory.push_back(pso);
}
void SetGraphicsDescriptorSets(
std::uint32_t,
std::uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout*) override {
if (count >= 2u && descriptorSets != nullptr) {
textureDescriptorSetHistory.push_back(descriptorSets[1]);
}
}
void SetComputeDescriptorSets(std::uint32_t, std::uint32_t, RHIDescriptorSet**, RHIPipelineLayout*) override {}
void SetPrimitiveTopology(PrimitiveTopology topology) override { primitiveTopologyHistory.push_back(topology); }
void SetViewport(const Viewport& viewport) override { viewportHistory.push_back(viewport); }
void SetViewports(std::uint32_t count, const Viewport* viewports) override {
for (std::uint32_t index = 0; index < count; ++index) {
viewportHistory.push_back(viewports[index]);
}
}
void SetScissorRect(const Rect& rect) override { scissorHistory.push_back(rect); }
void SetScissorRects(std::uint32_t count, const Rect* rects) override {
for (std::uint32_t index = 0; index < count; ++index) {
scissorHistory.push_back(rects[index]);
}
}
void SetRenderTargets(std::uint32_t count, RHIResourceView**, RHIResourceView* = nullptr) override {
renderTargetBindHistory.push_back(count);
}
void SetStencilRef(std::uint8_t) override {}
void SetBlendFactor(const float[4]) override {}
void SetVertexBuffers(std::uint32_t, std::uint32_t, RHIResourceView**, const std::uint64_t*, const std::uint32_t*) override {}
void SetIndexBuffer(RHIResourceView*, std::uint64_t) override {}
void Draw(std::uint32_t vertexCount, std::uint32_t, std::uint32_t startVertex, std::uint32_t) override {
drawVertexCounts.push_back(vertexCount);
drawStartVertices.push_back(startVertex);
}
void DrawIndexed(std::uint32_t, std::uint32_t, std::uint32_t, std::int32_t, std::uint32_t) override {}
void Clear(float, float, float, float, std::uint32_t) override {}
void ClearRenderTarget(RHIResourceView*, const float[4], std::uint32_t = 0, const Rect* = nullptr) override {}
void ClearDepthStencil(RHIResourceView*, float, std::uint8_t, std::uint32_t = 0, const Rect* = nullptr) override {}
void CopyResource(RHIResourceView*, RHIResourceView*) override {}
void Dispatch(std::uint32_t, std::uint32_t, std::uint32_t) override {}
void* GetNativeHandle() override { return this; }
std::vector<RHIPipelineState*> pipelineStateHistory = {};
std::vector<PrimitiveTopology> primitiveTopologyHistory = {};
std::vector<Viewport> viewportHistory = {};
std::vector<Rect> scissorHistory = {};
std::vector<std::uint32_t> renderTargetBindHistory = {};
std::vector<RHIDescriptorSet*> textureDescriptorSetHistory = {};
std::vector<std::uint32_t> drawVertexCounts = {};
std::vector<std::uint32_t> drawStartVertices = {};
};
class FakeDevice final : public RHIDevice {
public:
bool Initialize(const XCEngine::RHI::RHIDeviceDesc&) override { return true; }
void Shutdown() override {}
RHIBuffer* CreateBuffer(const BufferDesc& desc) override { return new FakeBuffer(desc); }
XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc&) override { return nullptr; }
XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc&, const void*, size_t, std::uint32_t = 0) override { return nullptr; }
XCEngine::RHI::RHISwapChain* CreateSwapChain(const XCEngine::RHI::SwapChainDesc&, RHICommandQueue*) override { return nullptr; }
RHICommandList* CreateCommandList(const XCEngine::RHI::CommandListDesc&) override { return nullptr; }
RHICommandQueue* CreateCommandQueue(const XCEngine::RHI::CommandQueueDesc&) override { return nullptr; }
RHIShader* CreateShader(const XCEngine::RHI::ShaderCompileDesc&) override { return nullptr; }
RHIPipelineState* CreatePipelineState(const GraphicsPipelineDesc& desc) override { return new FakePipelineState(desc); }
RHIPipelineLayout* CreatePipelineLayout(const XCEngine::RHI::RHIPipelineLayoutDesc&) override { return new FakePipelineLayout(); }
XCEngine::RHI::RHIFence* CreateFence(const XCEngine::RHI::FenceDesc&) override { return nullptr; }
RHISampler* CreateSampler(const SamplerDesc&) override { return new FakeSampler(); }
XCEngine::RHI::RHIRenderPass* CreateRenderPass(std::uint32_t, const XCEngine::RHI::AttachmentDesc*, const XCEngine::RHI::AttachmentDesc*) override { return nullptr; }
XCEngine::RHI::RHIFramebuffer* CreateFramebuffer(XCEngine::RHI::RHIRenderPass*, std::uint32_t, std::uint32_t, std::uint32_t, RHIResourceView**, RHIResourceView*) override { return nullptr; }
RHIDescriptorPool* CreateDescriptorPool(const DescriptorPoolDesc& desc) override { return new FakeDescriptorPool(desc); }
RHIDescriptorSet* CreateDescriptorSet(RHIDescriptorPool*, const DescriptorSetLayoutDesc&) override { return nullptr; }
RHIResourceView* CreateVertexBufferView(RHIBuffer* buffer, const ResourceViewDesc& desc) override {
return new FakeVertexBufferView(static_cast<std::uint32_t>(buffer != nullptr ? buffer->GetSize() : 0u), desc.structureByteStride);
}
RHIResourceView* CreateIndexBufferView(RHIBuffer*, const ResourceViewDesc&) override { return nullptr; }
RHIResourceView* CreateRenderTargetView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; }
RHIResourceView* CreateDepthStencilView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; }
RHIResourceView* CreateShaderResourceView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; }
RHIResourceView* CreateUnorderedAccessView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; }
const XCEngine::RHI::RHICapabilities& GetCapabilities() const override { return m_capabilities; }
const XCEngine::RHI::RHIDeviceInfo& GetDeviceInfo() const override { return m_deviceInfo; }
void* GetNativeDevice() override { return this; }
private:
XCEngine::RHI::RHICapabilities m_capabilities = {};
XCEngine::RHI::RHIDeviceInfo m_deviceInfo = {};
};
UITextureHandle MakeExternalTextureHandle(FakeShaderResourceView& shaderView, std::uint32_t width = 32u, std::uint32_t height = 32u) {
UITextureHandle texture = {};
texture.nativeHandle = reinterpret_cast<std::uintptr_t>(&shaderView);
texture.width = width;
texture.height = height;
texture.kind = UITextureHandleKind::ShaderResourceView;
return texture;
}
} // namespace
TEST(XCUIRHIRenderBackendTest, ExposesNativeOverlayEntryAndAtlasInjection) {
static_assert(std::is_same_v<
decltype(std::declval<XCUIRHIRenderBackend&>().Render(
std::declval<const RenderContext&>(),
std::declval<const RenderSurface&>(),
std::declval<const UIDrawData&>())),
bool>);
static_assert(std::is_same_v<
decltype(std::declval<XCUIRHIRenderBackend&>().SetTextAtlasProvider(
std::declval<const IXCUITextAtlasProvider*>())) ,
void>);
static_assert(std::is_same_v<
decltype(std::declval<const XCUIRHIRenderBackend&>().GetLastOverlayStats().drawListCount),
std::size_t>);
SUCCEED();
}
TEST(XCUIRHIRenderBackendTest, RenderRejectsInvalidContextAndLeavesStatsClear) {
XCUIRHIRenderBackend backend = {};
RenderSurface surface(128, 72);
UIDrawData drawData = {};
EXPECT_FALSE(backend.Render(RenderContext(), surface, drawData));
EXPECT_EQ(backend.GetLastOverlayStats().commandCount, 0u);
EXPECT_EQ(backend.GetLastOverlayStats().renderedCommandCount, 0u);
}
TEST(XCUIRHIRenderBackendTest, SetTextAtlasProviderStoresPointerAndResetStatsClearsCounters) {
XCUIRHIRenderBackend backend = {};
class StubAtlasProvider final : public IXCUITextAtlasProvider {
public:
bool GetAtlasTextureView(PixelFormat, AtlasTextureView& outView) const override {
outView = {};
return false;
}
std::size_t GetFontCount() const override { return 0u; }
FontHandle GetFont(std::size_t) const override { return {}; }
FontHandle GetDefaultFont() const override { return {}; }
bool GetFontInfo(FontHandle, FontInfo& outInfo) const override {
outInfo = {};
return false;
}
bool GetBakedFontInfo(FontHandle, float, BakedFontInfo& outInfo) const override {
outInfo = {};
return false;
}
bool FindGlyph(FontHandle, float, std::uint32_t, GlyphInfo& outInfo) const override {
outInfo = {};
return false;
}
} atlasProvider;
backend.SetTextAtlasProvider(&atlasProvider);
EXPECT_EQ(backend.GetTextAtlasProvider(), &atlasProvider);
backend.SetTextAtlasProvider(nullptr);
EXPECT_EQ(backend.GetTextAtlasProvider(), nullptr);
backend.ResetStats();
EXPECT_EQ(backend.GetLastStats().drawListCount, 0u);
EXPECT_EQ(backend.GetLastStats().commandCount, 0u);
EXPECT_EQ(backend.GetLastStats().batchCount, 0u);
EXPECT_EQ(backend.GetLastStats().renderedCommandCount, 0u);
EXPECT_EQ(backend.GetLastStats().skippedCommandCount, 0u);
EXPECT_EQ(backend.GetLastStats().textureResolveCount, 0u);
EXPECT_EQ(backend.GetLastStats().textureCacheHitCount, 0u);
}
TEST(XCUIRHIRenderBackendTest, RenderReusesExternalTextureBindingsAcrossTexturedBatches) {
FakeDevice device = {};
FakeCommandList commandList = {};
FakeCommandQueue commandQueue = {};
FakeRenderTargetView renderTarget = {};
FakeShaderResourceView externalTextureView(true);
RenderContext context = {};
context.device = &device;
context.commandList = &commandList;
context.commandQueue = &commandQueue;
context.backendType = RHIType::D3D12;
RenderSurface surface(64, 48);
surface.SetColorAttachment(&renderTarget);
UIDrawData drawData = {};
UIDrawList& firstDrawList = drawData.EmplaceDrawList("FirstTexturedBatch");
firstDrawList.PushClipRect(UIRect(4.0f, 6.0f, 12.0f, 10.0f));
firstDrawList.AddImage(
UIRect(0.0f, 0.0f, 18.0f, 18.0f),
MakeExternalTextureHandle(externalTextureView),
UIColor(1.0f, 0.8f, 0.6f, 1.0f));
firstDrawList.PopClipRect();
UIDrawList& secondDrawList = drawData.EmplaceDrawList("SecondTexturedBatch");
secondDrawList.PushClipRect(UIRect(20.0f, 8.0f, 10.0f, 12.0f));
secondDrawList.AddImage(
UIRect(18.0f, 4.0f, 16.0f, 16.0f),
MakeExternalTextureHandle(externalTextureView),
UIColor(0.7f, 0.9f, 1.0f, 1.0f));
secondDrawList.PopClipRect();
XCUIRHIRenderBackend backend = {};
ASSERT_TRUE(backend.Render(context, surface, drawData));
const XCUIRHIRenderBackend::OverlayStats& stats = backend.GetLastOverlayStats();
EXPECT_EQ(stats.drawListCount, 2u);
EXPECT_EQ(stats.commandCount, 6u);
EXPECT_EQ(stats.batchCount, 2u);
EXPECT_EQ(stats.colorBatchCount, 0u);
EXPECT_EQ(stats.texturedBatchCount, 2u);
EXPECT_EQ(stats.scissoredBatchCount, 2u);
EXPECT_EQ(stats.renderedCommandCount, 2u);
EXPECT_EQ(stats.skippedCommandCount, 0u);
EXPECT_EQ(stats.textureResolveCount, 2u);
EXPECT_EQ(stats.textureCacheHitCount, 1u);
EXPECT_EQ(stats.vertexCount, 12u);
EXPECT_EQ(stats.triangleCount, 4u);
ASSERT_EQ(commandList.renderTargetBindHistory.size(), 1u);
ASSERT_EQ(commandList.viewportHistory.size(), 1u);
ASSERT_EQ(commandList.drawVertexCounts.size(), 2u);
EXPECT_EQ(commandList.drawVertexCounts[0], 6u);
EXPECT_EQ(commandList.drawVertexCounts[1], 6u);
ASSERT_EQ(commandList.textureDescriptorSetHistory.size(), 2u);
EXPECT_EQ(commandList.textureDescriptorSetHistory[0], commandList.textureDescriptorSetHistory[1]);
ASSERT_EQ(commandList.scissorHistory.size(), 4u);
EXPECT_EQ(commandList.scissorHistory[0].left, 0);
EXPECT_EQ(commandList.scissorHistory[0].top, 0);
EXPECT_EQ(commandList.scissorHistory[0].right, 64);
EXPECT_EQ(commandList.scissorHistory[0].bottom, 48);
EXPECT_EQ(commandList.scissorHistory[1].left, 4);
EXPECT_EQ(commandList.scissorHistory[1].top, 6);
EXPECT_EQ(commandList.scissorHistory[1].right, 16);
EXPECT_EQ(commandList.scissorHistory[1].bottom, 16);
EXPECT_EQ(commandList.scissorHistory[2].left, 20);
EXPECT_EQ(commandList.scissorHistory[2].top, 8);
EXPECT_EQ(commandList.scissorHistory[2].right, 30);
EXPECT_EQ(commandList.scissorHistory[2].bottom, 20);
EXPECT_EQ(commandList.scissorHistory[3].left, 0);
EXPECT_EQ(commandList.scissorHistory[3].top, 0);
EXPECT_EQ(commandList.scissorHistory[3].right, 64);
EXPECT_EQ(commandList.scissorHistory[3].bottom, 48);
}
TEST(XCUIRHIRenderBackendTest, RenderSkipsTexturedBatchWhenExternalTextureViewIsInvalid) {
FakeDevice device = {};
FakeCommandList commandList = {};
FakeCommandQueue commandQueue = {};
FakeRenderTargetView renderTarget = {};
FakeShaderResourceView invalidTextureView(false);
RenderContext context = {};
context.device = &device;
context.commandList = &commandList;
context.commandQueue = &commandQueue;
context.backendType = RHIType::D3D12;
RenderSurface surface(32, 32);
surface.SetColorAttachment(&renderTarget);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("InvalidExternalTexture");
drawList.AddImage(
UIRect(2.0f, 4.0f, 12.0f, 8.0f),
MakeExternalTextureHandle(invalidTextureView),
UIColor(1.0f, 1.0f, 1.0f, 1.0f));
XCUIRHIRenderBackend backend = {};
ASSERT_TRUE(backend.Render(context, surface, drawData));
const XCUIRHIRenderBackend::OverlayStats& stats = backend.GetLastOverlayStats();
EXPECT_EQ(stats.commandCount, 1u);
EXPECT_EQ(stats.batchCount, 1u);
EXPECT_EQ(stats.texturedBatchCount, 1u);
EXPECT_EQ(stats.renderedCommandCount, 0u);
EXPECT_EQ(stats.skippedCommandCount, 1u);
EXPECT_EQ(stats.textureResolveCount, 1u);
EXPECT_EQ(stats.textureCacheHitCount, 0u);
EXPECT_EQ(commandList.drawVertexCounts.size(), 0u);
EXPECT_EQ(commandList.textureDescriptorSetHistory.size(), 0u);
}

View File

@@ -0,0 +1,45 @@
#include <gtest/gtest.h>
#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h"
#include <cstdint>
namespace {
using XCEngine::Editor::XCUIBackend::IXCUITextAtlasProvider;
using XCEngine::Editor::XCUIBackend::XCUIStandaloneTextAtlasProvider;
TEST(XCUIStandaloneTextAtlasProviderTest, BuildsDefaultEditorAtlasWithoutImGuiContext) {
XCUIStandaloneTextAtlasProvider provider = {};
IXCUITextAtlasProvider::AtlasTextureView atlasView = {};
ASSERT_TRUE(provider.IsReady());
ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView));
EXPECT_TRUE(atlasView.IsValid());
EXPECT_EQ(atlasView.format, IXCUITextAtlasProvider::PixelFormat::RGBA32);
EXPECT_GT(provider.GetFontCount(), 0u);
}
TEST(XCUIStandaloneTextAtlasProviderTest, ExposesDefaultFontMetricsAndGlyphs) {
XCUIStandaloneTextAtlasProvider provider = {};
const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont();
ASSERT_TRUE(defaultFont.IsValid());
IXCUITextAtlasProvider::FontInfo fontInfo = {};
ASSERT_TRUE(provider.GetFontInfo(defaultFont, fontInfo));
EXPECT_GT(fontInfo.nominalSize, 0.0f);
IXCUITextAtlasProvider::BakedFontInfo bakedFontInfo = {};
ASSERT_TRUE(provider.GetBakedFontInfo(defaultFont, 0.0f, bakedFontInfo));
EXPECT_GT(bakedFontInfo.lineHeight, 0.0f);
EXPECT_GT(bakedFontInfo.rasterizerDensity, 0.0f);
IXCUITextAtlasProvider::GlyphInfo glyphInfo = {};
ASSERT_TRUE(provider.FindGlyph(defaultFont, 0.0f, static_cast<std::uint32_t>('A'), glyphInfo));
EXPECT_EQ(glyphInfo.requestedCodepoint, static_cast<std::uint32_t>('A'));
EXPECT_GT(glyphInfo.advanceX, 0.0f);
EXPECT_GE(glyphInfo.u1, glyphInfo.u0);
EXPECT_GE(glyphInfo.v1, glyphInfo.v0);
}
} // namespace