Add XCUI new editor sandbox phase 1
This commit is contained in:
5
Assets.meta
Normal file
5
Assets.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
fileFormatVersion: 1
|
||||
guid: 60a2f7fbbea9b0ef86cefe4d4ea75578
|
||||
folderAsset: true
|
||||
importer: FolderImporter
|
||||
importerVersion: 5
|
||||
5
Assets/XCUI.meta
Normal file
5
Assets/XCUI.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
fileFormatVersion: 1
|
||||
guid: 6dd3f8fecfde5a3f1d52cb3a9e18ed53
|
||||
folderAsset: true
|
||||
importer: FolderImporter
|
||||
importerVersion: 5
|
||||
5
Assets/XCUI/NewEditor.meta
Normal file
5
Assets/XCUI/NewEditor.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
fileFormatVersion: 1
|
||||
guid: b6816d88212c73fe81921340c22e0939
|
||||
folderAsset: true
|
||||
importer: FolderImporter
|
||||
importerVersion: 5
|
||||
5
Assets/XCUI/NewEditor/Demo.meta
Normal file
5
Assets/XCUI/NewEditor/Demo.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
fileFormatVersion: 1
|
||||
guid: de79bdd523120a9286b09d0a06bf150c
|
||||
folderAsset: true
|
||||
importer: FolderImporter
|
||||
importerVersion: 5
|
||||
24
Assets/XCUI/NewEditor/Demo/Theme.xctheme
Normal file
24
Assets/XCUI/NewEditor/Demo/Theme.xctheme
Normal 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>
|
||||
5
Assets/XCUI/NewEditor/Demo/Theme.xctheme.meta
Normal file
5
Assets/XCUI/NewEditor/Demo/Theme.xctheme.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
fileFormatVersion: 1
|
||||
guid: 35810d9b2e3744637d2655cf8f6ed2d3
|
||||
folderAsset: false
|
||||
importer: UIThemeImporter
|
||||
importerVersion: 5
|
||||
67
Assets/XCUI/NewEditor/Demo/View.xcui
Normal file
67
Assets/XCUI/NewEditor/Demo/View.xcui
Normal 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>
|
||||
5
Assets/XCUI/NewEditor/Demo/View.xcui.meta
Normal file
5
Assets/XCUI/NewEditor/Demo/View.xcui.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
fileFormatVersion: 1
|
||||
guid: 9586ad2d05baa7ff00e16bb3c6eab1ea
|
||||
folderAsset: false
|
||||
importer: UIViewImporter
|
||||
importerVersion: 5
|
||||
5
Assets/XCUI/NewEditor/LayoutLab.meta
Normal file
5
Assets/XCUI/NewEditor/LayoutLab.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
fileFormatVersion: 1
|
||||
guid: 2767aae796d78abc773d2576b6bdce54
|
||||
folderAsset: true
|
||||
importer: FolderImporter
|
||||
importerVersion: 5
|
||||
19
Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme
Normal file
19
Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme
Normal 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>
|
||||
5
Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme.meta
Normal file
5
Assets/XCUI/NewEditor/LayoutLab/Theme.xctheme.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
fileFormatVersion: 1
|
||||
guid: fca776270d9840e08180c456593e683d
|
||||
folderAsset: false
|
||||
importer: UIThemeImporter
|
||||
importerVersion: 5
|
||||
105
Assets/XCUI/NewEditor/LayoutLab/View.xcui
Normal file
105
Assets/XCUI/NewEditor/LayoutLab/View.xcui
Normal 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>
|
||||
|
||||
5
Assets/XCUI/NewEditor/LayoutLab/View.xcui.meta
Normal file
5
Assets/XCUI/NewEditor/LayoutLab/View.xcui.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
fileFormatVersion: 1
|
||||
guid: 9a03d17147d903cad459db2c205a9f30
|
||||
folderAsset: false
|
||||
importer: UIViewImporter
|
||||
importerVersion: 5
|
||||
@@ -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)
|
||||
|
||||
78
docs/plan/XCUI_Phase_Status_2026-04-05.md
Normal file
78
docs/plan/XCUI_Phase_Status_2026-04-05.md
Normal 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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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
120
new_editor/CMakeLists.txt
Normal 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
16
new_editor/README.md
Normal 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.
|
||||
19
new_editor/resources/xcui_demo_theme.xctheme
Normal file
19
new_editor/resources/xcui_demo_theme.xctheme
Normal 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>
|
||||
30
new_editor/resources/xcui_demo_view.xcui
Normal file
30
new_editor/resources/xcui_demo_view.xcui
Normal 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>
|
||||
16
new_editor/resources/xcui_layout_lab_theme.xctheme
Normal file
16
new_editor/resources/xcui_layout_lab_theme.xctheme
Normal 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>
|
||||
66
new_editor/resources/xcui_layout_lab_view.xcui
Normal file
66
new_editor/resources/xcui_layout_lab_view.xcui
Normal 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>
|
||||
931
new_editor/src/Application.cpp
Normal file
931
new_editor/src/Application.cpp
Normal 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
|
||||
140
new_editor/src/Application.h
Normal file
140
new_editor/src/Application.h
Normal 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
|
||||
275
new_editor/src/Rendering/MainWindowBackdropPass.cpp
Normal file
275
new_editor/src/Rendering/MainWindowBackdropPass.cpp
Normal 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
|
||||
41
new_editor/src/Rendering/MainWindowBackdropPass.h
Normal file
41
new_editor/src/Rendering/MainWindowBackdropPass.h
Normal 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
|
||||
@@ -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
|
||||
39
new_editor/src/Rendering/MainWindowNativeBackdropRenderer.h
Normal file
39
new_editor/src/Rendering/MainWindowNativeBackdropRenderer.h
Normal 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
|
||||
3
new_editor/src/UI/ImGuiBackendBridge.h
Normal file
3
new_editor/src/UI/ImGuiBackendBridge.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../../editor/src/UI/ImGuiBackendBridge.h"
|
||||
90
new_editor/src/XCUIBackend/IXCUITextAtlasProvider.h
Normal file
90
new_editor/src/XCUIBackend/IXCUITextAtlasProvider.h
Normal 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
|
||||
223
new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.cpp
Normal file
223
new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.cpp
Normal 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
|
||||
35
new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.h
Normal file
35
new_editor/src/XCUIBackend/ImGuiTextAtlasProvider.h
Normal 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
|
||||
193
new_editor/src/XCUIBackend/ImGuiTransitionBackend.h
Normal file
193
new_editor/src/XCUIBackend/ImGuiTransitionBackend.h
Normal 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
|
||||
226
new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.cpp
Normal file
226
new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.cpp
Normal 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
|
||||
22
new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.h
Normal file
22
new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.h
Normal 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
|
||||
750
new_editor/src/XCUIBackend/XCUIAssetDocumentSource.cpp
Normal file
750
new_editor/src/XCUIBackend/XCUIAssetDocumentSource.cpp
Normal 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
|
||||
148
new_editor/src/XCUIBackend/XCUIAssetDocumentSource.h
Normal file
148
new_editor/src/XCUIBackend/XCUIAssetDocumentSource.h
Normal 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
|
||||
1986
new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp
Normal file
1986
new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp
Normal file
File diff suppressed because it is too large
Load Diff
78
new_editor/src/XCUIBackend/XCUIDemoRuntime.h
Normal file
78
new_editor/src/XCUIBackend/XCUIDemoRuntime.h
Normal 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
|
||||
60
new_editor/src/XCUIBackend/XCUIEditorFontSetup.cpp
Normal file
60
new_editor/src/XCUIBackend/XCUIEditorFontSetup.cpp
Normal 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
|
||||
14
new_editor/src/XCUIBackend/XCUIEditorFontSetup.h
Normal file
14
new_editor/src/XCUIBackend/XCUIEditorFontSetup.h
Normal 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
|
||||
349
new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h
Normal file
349
new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h
Normal 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
|
||||
596
new_editor/src/XCUIBackend/XCUIInputBridge.cpp
Normal file
596
new_editor/src/XCUIBackend/XCUIInputBridge.cpp
Normal 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
|
||||
152
new_editor/src/XCUIBackend/XCUIInputBridge.h
Normal file
152
new_editor/src/XCUIBackend/XCUIInputBridge.h
Normal 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
|
||||
916
new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp
Normal file
916
new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp
Normal 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
|
||||
74
new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h
Normal file
74
new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h
Normal 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
|
||||
590
new_editor/src/XCUIBackend/XCUIRHICommandCompiler.cpp
Normal file
590
new_editor/src/XCUIBackend/XCUIRHICommandCompiler.cpp
Normal 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
|
||||
119
new_editor/src/XCUIBackend/XCUIRHICommandCompiler.h
Normal file
119
new_editor/src/XCUIBackend/XCUIRHICommandCompiler.h
Normal 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
|
||||
267
new_editor/src/XCUIBackend/XCUIRHICommandSupport.h
Normal file
267
new_editor/src/XCUIBackend/XCUIRHICommandSupport.h
Normal 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
|
||||
582
new_editor/src/XCUIBackend/XCUIRHIRenderBackend.cpp
Normal file
582
new_editor/src/XCUIBackend/XCUIRHIRenderBackend.cpp
Normal 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
|
||||
120
new_editor/src/XCUIBackend/XCUIRHIRenderBackend.h
Normal file
120
new_editor/src/XCUIBackend/XCUIRHIRenderBackend.h
Normal 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
|
||||
222
new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp
Normal file
222
new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp
Normal 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
|
||||
42
new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h
Normal file
42
new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h
Normal 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
6
new_editor/src/main.cpp
Normal 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);
|
||||
}
|
||||
44
new_editor/src/panels/Panel.cpp
Normal file
44
new_editor/src/panels/Panel.cpp
Normal 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
|
||||
29
new_editor/src/panels/Panel.h
Normal file
29
new_editor/src/panels/Panel.h
Normal 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
|
||||
438
new_editor/src/panels/XCUIDemoPanel.cpp
Normal file
438
new_editor/src/panels/XCUIDemoPanel.cpp
Normal 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
|
||||
45
new_editor/src/panels/XCUIDemoPanel.h
Normal file
45
new_editor/src/panels/XCUIDemoPanel.h
Normal 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
|
||||
347
new_editor/src/panels/XCUILayoutLabPanel.cpp
Normal file
347
new_editor/src/panels/XCUILayoutLabPanel.cpp
Normal 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
|
||||
42
new_editor/src/panels/XCUILayoutLabPanel.h
Normal file
42
new_editor/src/panels/XCUILayoutLabPanel.h
Normal 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
|
||||
@@ -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:"
|
||||
)
|
||||
)
|
||||
|
||||
508
tests/NewEditor/CMakeLists.txt
Normal file
508
tests/NewEditor/CMakeLists.txt
Normal 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()
|
||||
90
tests/NewEditor/test_imgui_text_atlas_provider.cpp
Normal file
90
tests/NewEditor/test_imgui_text_atlas_provider.cpp
Normal 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
|
||||
88
tests/NewEditor/test_imgui_xcui_input_adapter.cpp
Normal file
88
tests/NewEditor/test_imgui_xcui_input_adapter.cpp
Normal 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));
|
||||
}
|
||||
@@ -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
|
||||
306
tests/NewEditor/test_new_editor_imgui_transition_backend.cpp
Normal file
306
tests/NewEditor/test_new_editor_imgui_transition_backend.cpp
Normal 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();
|
||||
}
|
||||
257
tests/NewEditor/test_xcui_asset_document_source.cpp
Normal file
257
tests/NewEditor/test_xcui_asset_document_source.cpp
Normal 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
|
||||
336
tests/NewEditor/test_xcui_demo_runtime.cpp
Normal file
336
tests/NewEditor/test_xcui_demo_runtime.cpp
Normal 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");
|
||||
}
|
||||
523
tests/NewEditor/test_xcui_hosted_preview_presenter.cpp
Normal file
523
tests/NewEditor/test_xcui_hosted_preview_presenter.cpp
Normal 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
|
||||
146
tests/NewEditor/test_xcui_input_bridge.cpp
Normal file
146
tests/NewEditor/test_xcui_input_bridge.cpp
Normal 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)));
|
||||
}
|
||||
192
tests/NewEditor/test_xcui_layout_lab_runtime.cpp
Normal file
192
tests/NewEditor/test_xcui_layout_lab_runtime.cpp
Normal 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());
|
||||
}
|
||||
242
tests/NewEditor/test_xcui_rhi_command_compiler.cpp
Normal file
242
tests/NewEditor/test_xcui_rhi_command_compiler.cpp
Normal 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
|
||||
202
tests/NewEditor/test_xcui_rhi_command_support.cpp
Normal file
202
tests/NewEditor/test_xcui_rhi_command_support.cpp
Normal 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.");
|
||||
}
|
||||
710
tests/NewEditor/test_xcui_rhi_render_backend.cpp
Normal file
710
tests/NewEditor/test_xcui_rhi_render_backend.cpp
Normal 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);
|
||||
}
|
||||
45
tests/NewEditor/test_xcui_standalone_text_atlas_provider.cpp
Normal file
45
tests/NewEditor/test_xcui_standalone_text_atlas_provider.cpp
Normal 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
|
||||
Reference in New Issue
Block a user