Reuse panel frame composition in native XCUI shell

This commit is contained in:
2026-04-05 17:41:31 +08:00
parent 63b5f12b93
commit 3db09ea5d0
10 changed files with 651 additions and 954 deletions

View File

@@ -8,6 +8,7 @@
#include <imgui.h>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
@@ -18,9 +19,16 @@ 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 {
@@ -50,6 +58,7 @@ void PrepareImGui(float width = 1280.0f, float height = 900.0f) {
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;
@@ -59,10 +68,79 @@ public:
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 {
@@ -119,6 +197,91 @@ private:
};
};
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&,
@@ -181,68 +344,92 @@ void PressKeyOnce(
}
TEST(NewEditorXCUILayoutLabPanelTest, MapsPreviousNextHomeAndEndIntoRuntimeNavigation) {
ImGuiContextScope contextScope;
auto previewPresenter = std::make_unique<StubHostedPreviewPresenter>();
auto canvasHost = std::make_unique<StubCanvasHost>();
StubCanvasHost* canvasHostPtr = canvasHost.get();
ImGuiXCUIInputSnapshotSource inputSource(nullptr);
XCUILayoutLabPanel panel(&inputSource, std::move(previewPresenter), std::move(canvasHost));
XCUILayoutLabPanel panel(nullptr, std::move(previewPresenter), std::move(canvasHost));
panel.SetHostedPreviewEnabled(false);
RenderPanelFrame(panel, contextScope);
ClickElement(panel, *canvasHostPtr, "assetLighting", contextScope);
XCUIPanelCanvasSession session = MakeCanvasSession();
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, true, false, true));
ClickElementWithComposition(panel, session, "assetLighting");
ASSERT_EQ(panel.GetFrameResult().stats.selectedElementId, "assetLighting");
canvasHostPtr->SetHovered(false);
canvasHostPtr->SetWindowFocused(true);
session.hovered = false;
session.windowFocused = true;
PressKeyOnce(panel, contextScope, ImGuiKey_DownArrow);
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");
PressKeyOnce(panel, contextScope, ImGuiKey_UpArrow);
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");
PressKeyOnce(panel, contextScope, ImGuiKey_End);
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");
PressKeyOnce(panel, contextScope, ImGuiKey_Home);
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) {
ImGuiContextScope contextScope;
auto previewPresenter = std::make_unique<StubHostedPreviewPresenter>();
auto canvasHost = std::make_unique<StubCanvasHost>();
StubCanvasHost* canvasHostPtr = canvasHost.get();
ImGuiXCUIInputSnapshotSource inputSource(nullptr);
XCUILayoutLabPanel panel(&inputSource, std::move(previewPresenter), std::move(canvasHost));
XCUILayoutLabPanel panel(nullptr, std::move(previewPresenter), std::move(canvasHost));
panel.SetHostedPreviewEnabled(false);
RenderPanelFrame(panel, contextScope);
ClickElement(panel, *canvasHostPtr, "treeScenes", contextScope);
XCUIPanelCanvasSession session = MakeCanvasSession();
ComposePanelFrame(panel, session, MakePointerSnapshot(session.pointerPosition, true, false, true));
ClickElementWithComposition(panel, session, "treeScenes");
ASSERT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeScenes");
canvasHostPtr->SetHovered(false);
canvasHostPtr->SetWindowFocused(true);
session.hovered = false;
session.windowFocused = true;
PressKeyOnce(panel, contextScope, ImGuiKey_LeftArrow);
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");
PressKeyOnce(panel, contextScope, ImGuiKey_LeftArrow);
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);
PressKeyOnce(panel, contextScope, ImGuiKey_RightArrow);
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);
PressKeyOnce(panel, contextScope, ImGuiKey_RightArrow);
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");
}
@@ -259,4 +446,81 @@ TEST(NewEditorXCUILayoutLabPanelTest, DefaultFallbackDoesNotCreateImplicitHosted
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