Add XCUI expansion state and coverage tests

This commit is contained in:
2026-04-05 07:29:27 +08:00
parent 646e5855ce
commit 511e94fd30
18 changed files with 1213 additions and 53 deletions

View File

@@ -100,6 +100,12 @@ set(NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE
set(NEW_EDITOR_HOSTED_PREVIEW_PRESENTER_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h
)
set(NEW_EDITOR_WINDOW_UI_COMPOSITOR_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/IWindowUICompositor.h
)
set(NEW_EDITOR_IMGUI_WINDOW_UI_COMPOSITOR_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiWindowUICompositor.h
)
set(NEW_EDITOR_NATIVE_BACKDROP_RENDERER_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.h
)
@@ -265,6 +271,37 @@ else()
message(STATUS "Skipping new_editor_xcui_hosted_preview_presenter_tests because presenter header or ImGui sources are missing.")
endif()
if(EXISTS "${NEW_EDITOR_WINDOW_UI_COMPOSITOR_HEADER}" AND
EXISTS "${NEW_EDITOR_IMGUI_WINDOW_UI_COMPOSITOR_HEADER}" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_imgui_window_ui_compositor.cpp")
add_executable(new_editor_imgui_window_ui_compositor_tests
test_imgui_window_ui_compositor.cpp
)
xcengine_configure_new_editor_test_target(new_editor_imgui_window_ui_compositor_tests)
target_link_libraries(new_editor_imgui_window_ui_compositor_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
user32
comdlg32
)
target_include_directories(new_editor_imgui_window_ui_compositor_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_SOURCE_DIR}/editor/src
${CMAKE_BINARY_DIR}/_deps/imgui-src
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
)
xcengine_discover_new_editor_gtests(new_editor_imgui_window_ui_compositor_tests)
else()
message(STATUS "Skipping new_editor_imgui_window_ui_compositor_tests because compositor headers or the test source 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

View File

@@ -0,0 +1,283 @@
#include <gtest/gtest.h>
#include "XCUIBackend/IEditorHostCompositor.h"
#include "XCUIBackend/ImGuiWindowUICompositor.h"
#include "XCUIBackend/UITextureRegistration.h"
#include <array>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
namespace {
using XCEngine::Editor::Platform::D3D12WindowRenderer;
using XCEngine::Editor::XCUIBackend::IEditorHostCompositor;
using XCEngine::Editor::XCUIBackend::ImGuiWindowUICompositor;
using XCEngine::Editor::XCUIBackend::UITextureRegistration;
class RecordingHostCompositor final : public IEditorHostCompositor {
public:
bool initializeResult = true;
bool handleWindowMessageResult = false;
bool createTextureDescriptorResult = false;
bool invokeConfigureFonts = false;
int initializeCount = 0;
int shutdownCount = 0;
int beginFrameCount = 0;
int endFrameCount = 0;
int handleWindowMessageCount = 0;
int createTextureDescriptorCount = 0;
int freeTextureDescriptorCount = 0;
HWND lastHwnd = nullptr;
UINT lastMessage = 0u;
WPARAM lastWParam = 0u;
LPARAM lastLParam = 0u;
D3D12WindowRenderer* initializedRenderer = nullptr;
D3D12WindowRenderer* presentedRenderer = nullptr;
::XCEngine::RHI::RHIDevice* lastDevice = nullptr;
::XCEngine::RHI::RHITexture* lastTexture = nullptr;
bool beforeUiRenderProvided = false;
bool afterUiRenderProvided = false;
std::array<float, 4> lastClearColor = { 0.0f, 0.0f, 0.0f, 0.0f };
UITextureRegistration nextRegistration = {};
UITextureRegistration freedRegistration = {};
std::vector<std::string> callOrder = {};
bool Initialize(
HWND hwnd,
D3D12WindowRenderer& windowRenderer,
const ConfigureFontsCallback& configureFonts) override {
++initializeCount;
lastHwnd = hwnd;
initializedRenderer = &windowRenderer;
callOrder.push_back("initialize");
if (invokeConfigureFonts && configureFonts) {
configureFonts();
}
return initializeResult;
}
void Shutdown() override {
++shutdownCount;
callOrder.push_back("shutdown");
}
bool HandleWindowMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) override {
++handleWindowMessageCount;
lastHwnd = hwnd;
lastMessage = message;
lastWParam = wParam;
lastLParam = lParam;
callOrder.push_back("message");
return handleWindowMessageResult;
}
void BeginFrame() override {
++beginFrameCount;
callOrder.push_back("begin");
}
void EndFrameAndPresent(
D3D12WindowRenderer& windowRenderer,
const float clearColor[4],
const RenderCallback& beforeUiRender,
const RenderCallback& afterUiRender) override {
++endFrameCount;
presentedRenderer = &windowRenderer;
beforeUiRenderProvided = static_cast<bool>(beforeUiRender);
afterUiRenderProvided = static_cast<bool>(afterUiRender);
for (std::size_t index = 0; index < lastClearColor.size(); ++index) {
lastClearColor[index] = clearColor[index];
}
callOrder.push_back("present");
}
bool CreateTextureDescriptor(
::XCEngine::RHI::RHIDevice* device,
::XCEngine::RHI::RHITexture* texture,
UITextureRegistration& outRegistration) override {
++createTextureDescriptorCount;
lastDevice = device;
lastTexture = texture;
if (createTextureDescriptorResult) {
outRegistration = nextRegistration;
}
return createTextureDescriptorResult;
}
void FreeTextureDescriptor(const UITextureRegistration& registration) override {
++freeTextureDescriptorCount;
freedRegistration = registration;
}
};
HWND MakeFakeHwnd() {
return reinterpret_cast<HWND>(static_cast<std::uintptr_t>(0x1234u));
}
UITextureRegistration MakeRegistration() {
UITextureRegistration registration = {};
registration.cpuHandle.ptr = 11u;
registration.gpuHandle.ptr = 29u;
registration.texture.nativeHandle = 43u;
registration.texture.width = 256u;
registration.texture.height = 128u;
registration.texture.kind = ::XCEngine::UI::UITextureHandleKind::ShaderResourceView;
return registration;
}
TEST(ImGuiWindowUICompositorTest, InitializeForwardsToHostAndConfigureFontsCallback) {
auto host = std::make_unique<RecordingHostCompositor>();
RecordingHostCompositor* hostPtr = host.get();
hostPtr->invokeConfigureFonts = true;
ImGuiWindowUICompositor compositor(std::move(host));
D3D12WindowRenderer renderer = {};
bool configureFontsCalled = false;
EXPECT_TRUE(compositor.Initialize(
MakeFakeHwnd(),
renderer,
[&configureFontsCalled]() { configureFontsCalled = true; }));
EXPECT_TRUE(configureFontsCalled);
ASSERT_NE(hostPtr, nullptr);
EXPECT_EQ(hostPtr->initializeCount, 1);
EXPECT_EQ(hostPtr->lastHwnd, MakeFakeHwnd());
EXPECT_EQ(hostPtr->initializedRenderer, &renderer);
}
TEST(ImGuiWindowUICompositorTest, RenderFrameCallsHostBeginUiAndPresentInOrder) {
auto host = std::make_unique<RecordingHostCompositor>();
RecordingHostCompositor* hostPtr = host.get();
ImGuiWindowUICompositor compositor(std::move(host));
D3D12WindowRenderer renderer = {};
ASSERT_TRUE(compositor.Initialize(MakeFakeHwnd(), renderer, {}));
hostPtr->callOrder.clear();
bool uiRendered = false;
constexpr float clearColor[4] = { 0.1f, 0.2f, 0.3f, 0.4f };
compositor.RenderFrame(
clearColor,
[&]() {
uiRendered = true;
hostPtr->callOrder.push_back("ui");
},
[](const ::XCEngine::Rendering::RenderContext&, const ::XCEngine::Rendering::RenderSurface&) {},
[](const ::XCEngine::Rendering::RenderContext&, const ::XCEngine::Rendering::RenderSurface&) {});
EXPECT_TRUE(uiRendered);
EXPECT_EQ(hostPtr->beginFrameCount, 1);
EXPECT_EQ(hostPtr->endFrameCount, 1);
EXPECT_EQ(hostPtr->presentedRenderer, &renderer);
EXPECT_TRUE(hostPtr->beforeUiRenderProvided);
EXPECT_TRUE(hostPtr->afterUiRenderProvided);
EXPECT_EQ(hostPtr->lastClearColor[0], clearColor[0]);
EXPECT_EQ(hostPtr->lastClearColor[1], clearColor[1]);
EXPECT_EQ(hostPtr->lastClearColor[2], clearColor[2]);
EXPECT_EQ(hostPtr->lastClearColor[3], clearColor[3]);
EXPECT_EQ(
hostPtr->callOrder,
(std::vector<std::string>{ "begin", "ui", "present" }));
}
TEST(ImGuiWindowUICompositorTest, HandleWindowMessageAndTextureRegistrationForwardToHost) {
auto host = std::make_unique<RecordingHostCompositor>();
RecordingHostCompositor* hostPtr = host.get();
hostPtr->handleWindowMessageResult = true;
hostPtr->createTextureDescriptorResult = true;
hostPtr->nextRegistration = MakeRegistration();
ImGuiWindowUICompositor compositor(std::move(host));
EXPECT_TRUE(compositor.HandleWindowMessage(MakeFakeHwnd(), WM_SIZE, 7u, 19u));
EXPECT_EQ(hostPtr->handleWindowMessageCount, 1);
EXPECT_EQ(hostPtr->lastMessage, static_cast<UINT>(WM_SIZE));
EXPECT_EQ(hostPtr->lastWParam, static_cast<WPARAM>(7u));
EXPECT_EQ(hostPtr->lastLParam, static_cast<LPARAM>(19u));
UITextureRegistration registration = {};
auto* fakeDevice = reinterpret_cast<::XCEngine::RHI::RHIDevice*>(static_cast<std::uintptr_t>(0x41u));
auto* fakeTexture = reinterpret_cast<::XCEngine::RHI::RHITexture*>(static_cast<std::uintptr_t>(0x59u));
EXPECT_TRUE(compositor.CreateTextureDescriptor(fakeDevice, fakeTexture, registration));
EXPECT_EQ(hostPtr->createTextureDescriptorCount, 1);
EXPECT_EQ(hostPtr->lastDevice, fakeDevice);
EXPECT_EQ(hostPtr->lastTexture, fakeTexture);
EXPECT_EQ(registration.cpuHandle.ptr, hostPtr->nextRegistration.cpuHandle.ptr);
EXPECT_EQ(registration.gpuHandle.ptr, hostPtr->nextRegistration.gpuHandle.ptr);
EXPECT_EQ(registration.texture.nativeHandle, hostPtr->nextRegistration.texture.nativeHandle);
compositor.FreeTextureDescriptor(registration);
EXPECT_EQ(hostPtr->freeTextureDescriptorCount, 1);
EXPECT_EQ(hostPtr->freedRegistration.cpuHandle.ptr, registration.cpuHandle.ptr);
EXPECT_EQ(hostPtr->freedRegistration.gpuHandle.ptr, registration.gpuHandle.ptr);
EXPECT_EQ(hostPtr->freedRegistration.texture.nativeHandle, registration.texture.nativeHandle);
}
TEST(ImGuiWindowUICompositorTest, ShutdownClearsRendererBindingAndPreventsFurtherRender) {
auto host = std::make_unique<RecordingHostCompositor>();
RecordingHostCompositor* hostPtr = host.get();
ImGuiWindowUICompositor compositor(std::move(host));
D3D12WindowRenderer renderer = {};
ASSERT_TRUE(compositor.Initialize(MakeFakeHwnd(), renderer, {}));
bool firstUiRendered = false;
compositor.RenderFrame(
std::array<float, 4>{ 0.0f, 0.0f, 0.0f, 1.0f }.data(),
[&]() { firstUiRendered = true; },
{},
{});
EXPECT_TRUE(firstUiRendered);
EXPECT_EQ(hostPtr->beginFrameCount, 1);
EXPECT_EQ(hostPtr->endFrameCount, 1);
compositor.Shutdown();
EXPECT_EQ(hostPtr->shutdownCount, 1);
bool secondUiRendered = false;
compositor.RenderFrame(
std::array<float, 4>{ 1.0f, 0.0f, 0.0f, 1.0f }.data(),
[&]() { secondUiRendered = true; },
{},
{});
EXPECT_FALSE(secondUiRendered);
EXPECT_EQ(hostPtr->beginFrameCount, 1);
EXPECT_EQ(hostPtr->endFrameCount, 1);
}
TEST(ImGuiWindowUICompositorTest, NullHostCompositorReturnsSafeDefaults) {
ImGuiWindowUICompositor compositor(std::unique_ptr<IEditorHostCompositor>{});
D3D12WindowRenderer renderer = {};
bool configureFontsCalled = false;
EXPECT_FALSE(compositor.Initialize(
MakeFakeHwnd(),
renderer,
[&configureFontsCalled]() { configureFontsCalled = true; }));
EXPECT_FALSE(configureFontsCalled);
EXPECT_FALSE(compositor.HandleWindowMessage(MakeFakeHwnd(), WM_CLOSE, 0u, 0u));
UITextureRegistration registration = {};
EXPECT_FALSE(compositor.CreateTextureDescriptor(nullptr, nullptr, registration));
bool uiRendered = false;
compositor.RenderFrame(
std::array<float, 4>{ 0.0f, 0.0f, 0.0f, 1.0f }.data(),
[&]() { uiRendered = true; },
{},
{});
EXPECT_FALSE(uiRendered);
compositor.Shutdown();
}
} // namespace

View File

@@ -250,3 +250,116 @@ TEST(NewEditorXCUILayoutLabRuntimeTest, ClickSelectionPersistsOnSharedCollection
ASSERT_TRUE(persistedFrame.stats.documentsReady);
EXPECT_EQ(persistedFrame.stats.selectedElementId, selectedElementId);
}
TEST(NewEditorXCUILayoutLabRuntimeTest, ClickingTreeRootTogglesIndentedChildrenVisibility) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baseline = runtime.Update(BuildInputState());
ASSERT_TRUE(baseline.stats.documentsReady);
EXPECT_EQ(baseline.stats.expandedTreeItemCount, 1u);
XCEngine::UI::UIRect treeRootRect = {};
XCEngine::UI::UIRect treeChildRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("treeAssetsRoot", treeRootRect));
ASSERT_TRUE(runtime.TryGetElementRect("treeScenes", treeChildRect));
ASSERT_GT(treeRootRect.width, 0.0f);
ASSERT_GT(treeRootRect.height, 0.0f);
const XCEngine::UI::UIPoint rootClickPoint(
treeRootRect.x + 18.0f,
treeRootRect.y + treeRootRect.height * 0.5f);
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState hoverInput = BuildInputState();
hoverInput.pointerPosition = rootClickPoint;
const auto& hoveredFrame = runtime.Update(hoverInput);
ASSERT_TRUE(hoveredFrame.stats.documentsReady);
ASSERT_EQ(hoveredFrame.stats.hoveredElementId, "treeAssetsRoot")
<< "treeRootRect=("
<< treeRootRect.x << ", "
<< treeRootRect.y << ", "
<< treeRootRect.width << ", "
<< treeRootRect.height << ")";
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState collapseInput = BuildInputState();
collapseInput.pointerPosition = rootClickPoint;
collapseInput.pointerPressed = true;
const auto& collapsedFrame = runtime.Update(collapseInput);
ASSERT_TRUE(collapsedFrame.stats.documentsReady);
EXPECT_EQ(collapsedFrame.stats.selectedElementId, "treeAssetsRoot");
const auto& collapsedPersistedFrame = runtime.Update(BuildInputState());
ASSERT_TRUE(collapsedPersistedFrame.stats.documentsReady);
EXPECT_EQ(collapsedPersistedFrame.stats.selectedElementId, "treeAssetsRoot");
EXPECT_EQ(collapsedPersistedFrame.stats.expandedTreeItemCount, 0u);
EXPECT_FALSE(runtime.TryGetElementRect("treeScenes", treeChildRect));
ASSERT_TRUE(runtime.TryGetElementRect("treeAssetsRoot", treeRootRect));
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState expandInput = BuildInputState();
expandInput.pointerPosition = XCEngine::UI::UIPoint(
treeRootRect.x + 18.0f,
treeRootRect.y + treeRootRect.height * 0.5f);
expandInput.pointerPressed = true;
const auto& expandedClickFrame = runtime.Update(expandInput);
ASSERT_TRUE(expandedClickFrame.stats.documentsReady);
const auto& expandedFrame = runtime.Update(BuildInputState());
ASSERT_TRUE(expandedFrame.stats.documentsReady);
EXPECT_EQ(expandedFrame.stats.expandedTreeItemCount, 1u);
EXPECT_TRUE(runtime.TryGetElementRect("treeScenes", treeChildRect));
EXPECT_GT(treeChildRect.height, 0.0f);
}
TEST(NewEditorXCUILayoutLabRuntimeTest, ClickingPropertySectionHeaderTogglesFieldVisibility) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baseline = runtime.Update(BuildInputState());
ASSERT_TRUE(baseline.stats.documentsReady);
XCEngine::UI::UIRect sectionRect = {};
XCEngine::UI::UIRect fieldRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("inspectorTransform", sectionRect));
ASSERT_TRUE(runtime.TryGetElementRect("fieldPosition", fieldRect));
const float expandedHeight = sectionRect.height;
const XCEngine::UI::UIPoint sectionHeaderPoint(
sectionRect.x + 18.0f,
sectionRect.y + 10.0f);
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState hoverInput = BuildInputState();
hoverInput.pointerPosition = sectionHeaderPoint;
const auto& hoveredFrame = runtime.Update(hoverInput);
ASSERT_TRUE(hoveredFrame.stats.documentsReady);
ASSERT_EQ(hoveredFrame.stats.hoveredElementId, "inspectorTransform")
<< "sectionRect=("
<< sectionRect.x << ", "
<< sectionRect.y << ", "
<< sectionRect.width << ", "
<< sectionRect.height << "), expandedPropertySectionCount="
<< baseline.stats.expandedPropertySectionCount;
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState collapseInput = BuildInputState();
collapseInput.pointerPosition = sectionHeaderPoint;
collapseInput.pointerPressed = true;
const auto& collapsedFrame = runtime.Update(collapseInput);
ASSERT_TRUE(collapsedFrame.stats.documentsReady);
const auto& collapsedPersistedFrame = runtime.Update(BuildInputState());
ASSERT_TRUE(collapsedPersistedFrame.stats.documentsReady);
EXPECT_EQ(collapsedPersistedFrame.stats.selectedElementId, "inspectorTransform");
ASSERT_TRUE(runtime.TryGetElementRect("inspectorTransform", sectionRect));
EXPECT_LT(sectionRect.height, expandedHeight);
EXPECT_FALSE(runtime.TryGetElementRect("fieldPosition", fieldRect));
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState expandInput = BuildInputState();
expandInput.pointerPosition = XCEngine::UI::UIPoint(
sectionRect.x + 18.0f,
sectionRect.y + 10.0f);
expandInput.pointerPressed = true;
const auto& expandedClickFrame = runtime.Update(expandInput);
ASSERT_TRUE(expandedClickFrame.stats.documentsReady);
const auto& expandedFrame = runtime.Update(BuildInputState());
ASSERT_TRUE(expandedFrame.stats.documentsReady);
EXPECT_TRUE(runtime.TryGetElementRect("fieldPosition", fieldRect));
EXPECT_GT(fieldRect.height, 0.0f);
}