Files
XCEngine/tests/NewEditor/test_xcui_layout_lab_panel.cpp

527 lines
20 KiB
C++

#include <gtest/gtest.h>
#include "panels/XCUILayoutLabPanel.h"
#include "XCUIBackend/ImGuiXCUIInputSource.h"
#include "XCUIBackend/XCUIHostedPreviewPresenter.h"
#include "XCUIBackend/XCUIPanelCanvasHost.h"
#include <imgui.h>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
namespace {
using XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter;
using XCEngine::Editor::XCUIBackend::IXCUIPanelCanvasHost;
using XCEngine::Editor::XCUIBackend::ImGuiXCUIInputSnapshotSource;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats;
using XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot;
using XCEngine::Editor::XCUIBackend::XCUIInputBridgeKeyState;
using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasRequest;
using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession;
using XCEngine::Input::KeyCode;
using XCEngine::NewEditor::XCUILayoutLabFrameComposition;
using XCEngine::NewEditor::XCUILayoutLabFrameCompositionRequest;
using XCEngine::NewEditor::XCUILayoutLabPanel;
class ImGuiContextScope {
public:
ImGuiContextScope() {
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
}
~ImGuiContextScope() {
ImGui::DestroyContext();
}
};
void PrepareImGui(float width = 1280.0f, float height = 900.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));
}
class StubHostedPreviewPresenter final : public IXCUIHostedPreviewPresenter {
public:
bool Present(const XCUIHostedPreviewFrame& frame) override {
++m_presentCallCount;
m_lastStats = {};
m_lastStats.presented = frame.drawData != nullptr;
return m_lastStats.presented;
}
const XCUIHostedPreviewStats& GetLastStats() const override {
return m_lastStats;
}
std::size_t GetPresentCallCount() const {
return m_presentCallCount;
}
private:
std::size_t m_presentCallCount = 0u;
XCUIHostedPreviewStats m_lastStats = {};
};
class StubNativeHostedPreviewPresenter final : public IXCUIHostedPreviewPresenter {
public:
bool Present(const XCUIHostedPreviewFrame& frame) override {
++m_presentCallCount;
m_lastFrame = frame;
m_lastStats = {};
m_lastStats.presented = frame.drawData != nullptr;
m_lastStats.queuedToNativePass = frame.drawData != nullptr;
m_lastStats.submittedDrawListCount = frame.drawData != nullptr ? frame.drawData->GetDrawListCount() : 0u;
m_lastStats.submittedCommandCount = frame.drawData != nullptr ? frame.drawData->GetTotalCommandCount() : 0u;
return m_lastStats.presented;
}
const XCUIHostedPreviewStats& GetLastStats() const override {
return m_lastStats;
}
bool IsNativeQueued() const override {
return true;
}
bool TryGetSurfaceImage(
const char* debugName,
XCUIHostedPreviewSurfaceImage& outImage) const override {
outImage = {};
if (debugName == nullptr || m_descriptor.debugName != debugName) {
return false;
}
outImage = m_descriptor.image;
return outImage.IsValid();
}
bool TryGetSurfaceDescriptor(
const char* debugName,
XCUIHostedPreviewSurfaceDescriptor& outDescriptor) const override {
outDescriptor = {};
if (debugName == nullptr || m_descriptor.debugName != debugName) {
return false;
}
outDescriptor = m_descriptor;
return true;
}
void SetDescriptor(XCUIHostedPreviewSurfaceDescriptor descriptor) {
m_descriptor = std::move(descriptor);
}
std::size_t GetPresentCallCount() const {
return m_presentCallCount;
}
const XCUIHostedPreviewFrame& GetLastFrame() const {
return m_lastFrame;
}
private:
std::size_t m_presentCallCount = 0u;
XCUIHostedPreviewFrame m_lastFrame = {};
XCUIHostedPreviewStats m_lastStats = {};
XCUIHostedPreviewSurfaceDescriptor m_descriptor = {};
};
class StubCanvasHost final : public IXCUIPanelCanvasHost {
public:
const char* GetDebugName() const override {
return "StubCanvasHost";
}
XCUIPanelCanvasSession BeginCanvas(const XCUIPanelCanvasRequest&) override {
return m_session;
}
void DrawFilledRect(
const XCEngine::UI::UIRect&,
const XCEngine::UI::UIColor&,
float) override {
}
void DrawOutlineRect(
const XCEngine::UI::UIRect&,
const XCEngine::UI::UIColor&,
float,
float) override {
}
void DrawText(
const XCEngine::UI::UIPoint&,
std::string_view,
const XCEngine::UI::UIColor&,
float) override {
}
void EndCanvas() override {
}
void SetPointerPosition(const XCEngine::UI::UIPoint& position) {
m_session.pointerPosition = position;
}
void SetHovered(bool hovered) {
m_session.hovered = hovered;
}
void SetWindowFocused(bool focused) {
m_session.windowFocused = focused;
}
private:
XCUIPanelCanvasSession m_session = {
XCEngine::UI::UIRect(0.0f, 0.0f, 960.0f, 640.0f),
XCEngine::UI::UIRect(0.0f, 0.0f, 960.0f, 640.0f),
XCEngine::UI::UIPoint(120.0f, 120.0f),
true,
true,
true
};
};
std::uint64_t NextTimestampNanoseconds() {
static std::uint64_t timestampNanoseconds = 1'000'000u;
timestampNanoseconds += 16'666'667u;
return timestampNanoseconds;
}
XCUIPanelCanvasSession MakeCanvasSession() {
XCUIPanelCanvasSession session = {};
session.hostRect = XCEngine::UI::UIRect(0.0f, 0.0f, 960.0f, 640.0f);
session.canvasRect = XCEngine::UI::UIRect(0.0f, 0.0f, 960.0f, 640.0f);
session.pointerPosition = XCEngine::UI::UIPoint(120.0f, 120.0f);
session.validCanvas = true;
session.hovered = true;
session.windowFocused = true;
return session;
}
XCEngine::UI::UITextureHandle MakeSurfaceTextureHandle(
std::uintptr_t nativeHandle,
std::uint32_t width,
std::uint32_t height) {
XCEngine::UI::UITextureHandle texture = {};
texture.nativeHandle = nativeHandle;
texture.width = width;
texture.height = height;
texture.kind = XCEngine::UI::UITextureHandleKind::ShaderResourceView;
return texture;
}
XCUIInputBridgeFrameSnapshot MakePointerSnapshot(
const XCEngine::UI::UIPoint& pointerPosition,
bool pointerInside,
bool pointerDown,
bool windowFocused) {
XCUIInputBridgeFrameSnapshot snapshot = {};
snapshot.pointerPosition = pointerPosition;
snapshot.pointerInside = pointerInside;
snapshot.pointerButtonsDown[0] = pointerDown;
snapshot.windowFocused = windowFocused;
snapshot.timestampNanoseconds = NextTimestampNanoseconds();
return snapshot;
}
XCUIInputBridgeFrameSnapshot MakeKeyboardSnapshot(
const XCEngine::UI::UIPoint& pointerPosition,
bool pointerInside,
bool windowFocused,
KeyCode keyCode) {
XCUIInputBridgeFrameSnapshot snapshot =
MakePointerSnapshot(pointerPosition, pointerInside, false, windowFocused);
snapshot.keys.push_back(XCUIInputBridgeKeyState {
static_cast<std::int32_t>(keyCode),
true,
false
});
return snapshot;
}
const XCUILayoutLabFrameComposition& ComposePanelFrame(
XCUILayoutLabPanel& panel,
const XCUIPanelCanvasSession& canvasSession,
const XCUIInputBridgeFrameSnapshot& snapshot) {
XCUILayoutLabFrameCompositionRequest request = {};
request.canvasSession = canvasSession;
request.inputSnapshot = snapshot;
return panel.ComposeFrame(request);
}
void ClickElementWithComposition(
XCUILayoutLabPanel& panel,
const XCUIPanelCanvasSession& baseSession,
const std::string& elementId) {
XCEngine::UI::UIRect elementRect = {};
ASSERT_TRUE(panel.TryGetElementRect(elementId, elementRect));
const XCEngine::UI::UIPoint clickPoint(
elementRect.x + elementRect.width * 0.5f,
elementRect.y + elementRect.height * 0.5f);
XCUIPanelCanvasSession clickSession = baseSession;
clickSession.pointerPosition = clickPoint;
clickSession.hovered = true;
ComposePanelFrame(panel, clickSession, MakePointerSnapshot(clickPoint, true, true, true));
ComposePanelFrame(panel, clickSession, MakePointerSnapshot(clickPoint, true, false, true));
}
void RenderPanelFrame(
XCUILayoutLabPanel& panel,
ImGuiContextScope&,
const std::function<void(ImGuiIO&)>& configureIo = nullptr) {
PrepareImGui();
if (configureIo) {
configureIo(ImGui::GetIO());
}
ImGui::NewFrame();
panel.Render();
ImGui::Render();
}
void ClickElement(
XCUILayoutLabPanel& panel,
StubCanvasHost& canvasHost,
const std::string& elementId,
ImGuiContextScope& contextScope) {
XCEngine::UI::UIRect elementRect = {};
ASSERT_TRUE(panel.TryGetElementRect(elementId, elementRect));
const XCEngine::UI::UIPoint clickPoint(
elementRect.x + elementRect.width * 0.5f,
elementRect.y + elementRect.height * 0.5f);
canvasHost.SetPointerPosition(clickPoint);
canvasHost.SetHovered(true);
RenderPanelFrame(
panel,
contextScope,
[clickPoint](ImGuiIO& io) {
io.AddMousePosEvent(clickPoint.x, clickPoint.y);
io.AddMouseButtonEvent(ImGuiMouseButton_Left, true);
});
RenderPanelFrame(
panel,
contextScope,
[clickPoint](ImGuiIO& io) {
io.AddMousePosEvent(clickPoint.x, clickPoint.y);
io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
});
}
void PressKeyOnce(
XCUILayoutLabPanel& panel,
ImGuiContextScope& contextScope,
ImGuiKey key) {
RenderPanelFrame(
panel,
contextScope,
[key](ImGuiIO& io) {
io.AddKeyEvent(key, true);
});
RenderPanelFrame(
panel,
contextScope,
[key](ImGuiIO& io) {
io.AddKeyEvent(key, false);
});
}
TEST(NewEditorXCUILayoutLabPanelTest, MapsPreviousNextHomeAndEndIntoRuntimeNavigation) {
auto previewPresenter = std::make_unique<StubHostedPreviewPresenter>();
auto canvasHost = std::make_unique<StubCanvasHost>();
XCUILayoutLabPanel panel(nullptr, std::move(previewPresenter), std::move(canvasHost));
panel.SetHostedPreviewEnabled(false);
XCUIPanelCanvasSession session = MakeCanvasSession();
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, true, false, true));
ClickElementWithComposition(panel, session, "assetLighting");
ASSERT_EQ(panel.GetFrameResult().stats.selectedElementId, "assetLighting");
session.hovered = false;
session.windowFocused = true;
const XCUILayoutLabFrameComposition& downComposition = ComposePanelFrame(
panel,
session,
MakeKeyboardSnapshot(session.pointerPosition, false, true, KeyCode::Down));
EXPECT_TRUE(downComposition.inputState.navigateNext);
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, false, false, true));
EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "assetMaterials");
const XCUILayoutLabFrameComposition& upComposition = ComposePanelFrame(
panel,
session,
MakeKeyboardSnapshot(session.pointerPosition, false, true, KeyCode::Up));
EXPECT_TRUE(upComposition.inputState.navigatePrevious);
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, false, false, true));
EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "assetLighting");
const XCUILayoutLabFrameComposition& endComposition = ComposePanelFrame(
panel,
session,
MakeKeyboardSnapshot(session.pointerPosition, false, true, KeyCode::End));
EXPECT_TRUE(endComposition.inputState.navigateEnd);
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, false, false, true));
const std::string endSelection = panel.GetFrameResult().stats.selectedElementId;
ASSERT_FALSE(endSelection.empty());
EXPECT_NE(endSelection, "assetLighting");
const XCUILayoutLabFrameComposition& homeComposition = ComposePanelFrame(
panel,
session,
MakeKeyboardSnapshot(session.pointerPosition, false, true, KeyCode::Home));
EXPECT_TRUE(homeComposition.inputState.navigateHome);
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, false, false, true));
EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "assetLighting");
}
TEST(NewEditorXCUILayoutLabPanelTest, MapsCollapseAndExpandIntoRuntimeNavigation) {
auto previewPresenter = std::make_unique<StubHostedPreviewPresenter>();
auto canvasHost = std::make_unique<StubCanvasHost>();
XCUILayoutLabPanel panel(nullptr, std::move(previewPresenter), std::move(canvasHost));
panel.SetHostedPreviewEnabled(false);
XCUIPanelCanvasSession session = MakeCanvasSession();
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, true, false, true));
ClickElementWithComposition(panel, session, "treeScenes");
ASSERT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeScenes");
session.hovered = false;
session.windowFocused = true;
const XCUILayoutLabFrameComposition& collapseSelection = ComposePanelFrame(
panel,
session,
MakeKeyboardSnapshot(session.pointerPosition, false, true, KeyCode::Left));
EXPECT_TRUE(collapseSelection.inputState.navigateCollapse);
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, false, false, true));
EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeAssetsRoot");
ComposePanelFrame(panel, session, MakeKeyboardSnapshot(session.pointerPosition, false, true, KeyCode::Left));
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, false, false, true));
EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeAssetsRoot");
EXPECT_EQ(panel.GetFrameResult().stats.expandedTreeItemCount, 0u);
const XCUILayoutLabFrameComposition& expandRoot = ComposePanelFrame(
panel,
session,
MakeKeyboardSnapshot(session.pointerPosition, false, true, KeyCode::Right));
EXPECT_TRUE(expandRoot.inputState.navigateExpand);
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, false, false, true));
EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeAssetsRoot");
EXPECT_EQ(panel.GetFrameResult().stats.expandedTreeItemCount, 1u);
ComposePanelFrame(panel, session, MakeKeyboardSnapshot(session.pointerPosition, false, true, KeyCode::Right));
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, false, false, true));
EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeScenes");
}
TEST(NewEditorXCUILayoutLabPanelTest, DefaultFallbackDoesNotCreateImplicitHostedPreviewPresenter) {
ImGuiContextScope contextScope;
XCUILayoutLabPanel panel(nullptr);
RenderPanelFrame(panel, contextScope);
EXPECT_FALSE(panel.IsUsingNativeHostedPreview());
EXPECT_FALSE(panel.GetLastPreviewStats().presented);
EXPECT_EQ(panel.GetLastPreviewStats().submittedDrawListCount, 0u);
EXPECT_EQ(panel.GetLastPreviewStats().submittedCommandCount, 0u);
}
TEST(NewEditorXCUILayoutLabPanelTest, ComposeFramePublishesShellAgnosticLastCompositionState) {
auto previewPresenter = std::make_unique<StubHostedPreviewPresenter>();
auto canvasHost = std::make_unique<StubCanvasHost>();
XCUILayoutLabPanel panel(nullptr, std::move(previewPresenter), std::move(canvasHost));
XCUIPanelCanvasSession session = MakeCanvasSession();
const XCUILayoutLabFrameComposition& composition =
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, true, false, true));
EXPECT_EQ(&composition, &panel.GetLastFrameComposition());
ASSERT_NE(composition.frameResult, nullptr);
EXPECT_TRUE(composition.previewStats.presented);
EXPECT_EQ(composition.previewPathLabel, "hosted presenter");
EXPECT_EQ(composition.previewStateLabel, "live");
EXPECT_EQ(composition.previewSourceLabel, "new_editor.panels.xcui_layout_lab");
EXPECT_TRUE(composition.hostedPreviewEnabled);
EXPECT_EQ(composition.inputState.canvasRect.width, session.canvasRect.width);
EXPECT_EQ(composition.inputState.canvasRect.height, session.canvasRect.height);
EXPECT_TRUE(composition.inputState.pointerInside);
}
TEST(NewEditorXCUILayoutLabPanelTest, ComposeFrameCarriesNativeQueuedPreviewMetadataIntoComposition) {
auto previewPresenter = std::make_unique<StubNativeHostedPreviewPresenter>();
XCUIHostedPreviewSurfaceDescriptor descriptor = {};
descriptor.debugName = "XCUI Layout Lab";
descriptor.debugSource = "tests.native_layout_preview";
descriptor.logicalSize = XCEngine::UI::UISize(512.0f, 320.0f);
descriptor.queuedFrameIndex = 7u;
descriptor.image.texture = MakeSurfaceTextureHandle(42u, 512u, 320u);
descriptor.image.surfaceWidth = 512u;
descriptor.image.surfaceHeight = 320u;
descriptor.image.renderedCanvasRect = XCEngine::UI::UIRect(0.0f, 0.0f, 512.0f, 320.0f);
previewPresenter->SetDescriptor(std::move(descriptor));
StubNativeHostedPreviewPresenter* previewPresenterRaw = previewPresenter.get();
XCUILayoutLabPanel panel(nullptr, std::move(previewPresenter), std::make_unique<StubCanvasHost>());
XCUIPanelCanvasSession session = MakeCanvasSession();
const XCUILayoutLabFrameComposition& composition =
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, true, false, true));
EXPECT_TRUE(composition.hostedPreviewEnabled);
EXPECT_TRUE(composition.nativeHostedPreview);
EXPECT_TRUE(composition.hasHostedSurfaceDescriptor);
EXPECT_TRUE(composition.showHostedSurfaceImage);
EXPECT_EQ(composition.previewPathLabel, "native queued offscreen surface");
EXPECT_EQ(composition.previewStateLabel, "live");
EXPECT_EQ(composition.previewSourceLabel, "tests.native_layout_preview");
EXPECT_EQ(composition.hostedSurfaceDescriptor.queuedFrameIndex, 7u);
EXPECT_EQ(composition.hostedSurfaceImage.texture.nativeHandle, 42u);
EXPECT_TRUE(composition.previewStats.presented);
EXPECT_TRUE(composition.previewStats.queuedToNativePass);
EXPECT_EQ(previewPresenterRaw->GetPresentCallCount(), 1u);
EXPECT_EQ(previewPresenterRaw->GetLastFrame().debugName, std::string("XCUI Layout Lab"));
EXPECT_EQ(previewPresenterRaw->GetLastFrame().debugSource, std::string("new_editor.panels.xcui_layout_lab"));
EXPECT_FLOAT_EQ(previewPresenterRaw->GetLastFrame().logicalSize.width, session.canvasRect.width);
EXPECT_FLOAT_EQ(previewPresenterRaw->GetLastFrame().logicalSize.height, session.canvasRect.height);
}
TEST(NewEditorXCUILayoutLabPanelTest, ComposeFrameMarksPreviewDisabledWhenPresenterIsInjectedButDisabled) {
auto previewPresenter = std::make_unique<StubHostedPreviewPresenter>();
StubHostedPreviewPresenter* previewPresenterRaw = previewPresenter.get();
XCUILayoutLabPanel panel(nullptr, std::move(previewPresenter), std::make_unique<StubCanvasHost>());
panel.SetHostedPreviewEnabled(false);
XCUIPanelCanvasSession session = MakeCanvasSession();
const XCUILayoutLabFrameComposition& composition =
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, true, false, true));
EXPECT_FALSE(composition.hostedPreviewEnabled);
EXPECT_EQ(composition.previewStateLabel, "disabled");
EXPECT_FALSE(composition.previewStats.presented);
EXPECT_EQ(previewPresenterRaw->GetPresentCallCount(), 0u);
EXPECT_FALSE(panel.GetLastPreviewStats().presented);
}
} // namespace