#include #include "panels/XCUILayoutLabPanel.h" #include "XCUIBackend/ImGuiXCUIInputSource.h" #include "XCUIBackend/XCUIHostedPreviewPresenter.h" #include "XCUIBackend/XCUIPanelCanvasHost.h" #include #include #include #include #include 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(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(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& 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(); auto canvasHost = std::make_unique(); 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(); auto canvasHost = std::make_unique(); 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(); auto canvasHost = std::make_unique(); 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(); 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()); 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* previewPresenterRaw = previewPresenter.get(); XCUILayoutLabPanel panel(nullptr, std::move(previewPresenter), std::make_unique()); 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