400 lines
18 KiB
C++
400 lines
18 KiB
C++
#include "Application.h"
|
|
|
|
#include <XCEngine/Input/InputTypes.h>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
namespace {
|
|
|
|
using XCEngine::Editor::XCUIBackend::XCUIEditorCommandRouter;
|
|
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame;
|
|
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewQueue;
|
|
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceRegistry;
|
|
using XCEngine::Editor::XCUIBackend::UITextureRegistration;
|
|
using XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta;
|
|
using XCEngine::Input::KeyCode;
|
|
using XCEngine::NewEditor::Application;
|
|
using XCEngine::UI::UIColor;
|
|
using XCEngine::UI::UIDrawData;
|
|
using XCEngine::UI::UIDrawList;
|
|
using XCEngine::UI::UIRect;
|
|
using XCEngine::UI::UISize;
|
|
using XCEngine::UI::UITextureHandle;
|
|
using XCEngine::UI::UITextureHandleKind;
|
|
|
|
constexpr std::size_t ToPanelIndex(Application::ShellPanelId panelId) {
|
|
return static_cast<std::size_t>(panelId);
|
|
}
|
|
|
|
struct ShellCommandHarness {
|
|
Application::ShellViewToggleState viewToggles = {};
|
|
std::array<Application::ShellPanelChromeState, static_cast<std::size_t>(Application::ShellPanelId::Count)>
|
|
panels = {};
|
|
int hostedPreviewReconfigureCount = 0;
|
|
|
|
ShellCommandHarness() {
|
|
panels[ToPanelIndex(Application::ShellPanelId::XCUIDemo)] = {
|
|
Application::ShellPanelId::XCUIDemo,
|
|
"XCUI Demo",
|
|
"XCUI Demo",
|
|
"new_editor.panels.xcui_demo",
|
|
true,
|
|
true,
|
|
Application::ShellHostedPreviewMode::NativeOffscreen
|
|
};
|
|
panels[ToPanelIndex(Application::ShellPanelId::XCUILayoutLab)] = {
|
|
Application::ShellPanelId::XCUILayoutLab,
|
|
"XCUI Layout Lab",
|
|
"XCUI Layout Lab",
|
|
"new_editor.panels.xcui_layout_lab",
|
|
true,
|
|
true,
|
|
Application::ShellHostedPreviewMode::HostedPresenter
|
|
};
|
|
}
|
|
|
|
const Application::ShellPanelChromeState& Panel(Application::ShellPanelId panelId) const {
|
|
return panels[ToPanelIndex(panelId)];
|
|
}
|
|
|
|
Application::ShellCommandBindings BuildBindings() {
|
|
Application::ShellCommandBindings bindings = {};
|
|
bindings.getXCUIDemoPanelVisible = [this]() {
|
|
return Panel(Application::ShellPanelId::XCUIDemo).visible;
|
|
};
|
|
bindings.setXCUIDemoPanelVisible = [this](bool visible) {
|
|
panels[ToPanelIndex(Application::ShellPanelId::XCUIDemo)].visible = visible;
|
|
};
|
|
bindings.getXCUILayoutLabPanelVisible = [this]() {
|
|
return Panel(Application::ShellPanelId::XCUILayoutLab).visible;
|
|
};
|
|
bindings.setXCUILayoutLabPanelVisible = [this](bool visible) {
|
|
panels[ToPanelIndex(Application::ShellPanelId::XCUILayoutLab)].visible = visible;
|
|
};
|
|
bindings.getNativeBackdropVisible = [this]() {
|
|
return viewToggles.nativeBackdropVisible;
|
|
};
|
|
bindings.setNativeBackdropVisible = [this](bool visible) {
|
|
viewToggles.nativeBackdropVisible = visible;
|
|
};
|
|
bindings.getPulseAccentEnabled = [this]() {
|
|
return viewToggles.pulseAccentEnabled;
|
|
};
|
|
bindings.setPulseAccentEnabled = [this](bool enabled) {
|
|
viewToggles.pulseAccentEnabled = enabled;
|
|
};
|
|
bindings.getNativeXCUIOverlayVisible = [this]() {
|
|
return viewToggles.nativeXCUIOverlayVisible;
|
|
};
|
|
bindings.setNativeXCUIOverlayVisible = [this](bool visible) {
|
|
viewToggles.nativeXCUIOverlayVisible = visible;
|
|
};
|
|
bindings.getHostedPreviewHudVisible = [this]() {
|
|
return viewToggles.hostedPreviewHudVisible;
|
|
};
|
|
bindings.setHostedPreviewHudVisible = [this](bool visible) {
|
|
viewToggles.hostedPreviewHudVisible = visible;
|
|
};
|
|
bindings.getNativeDemoPanelPreviewEnabled = [this]() {
|
|
return Panel(Application::ShellPanelId::XCUIDemo).previewMode ==
|
|
Application::ShellHostedPreviewMode::NativeOffscreen;
|
|
};
|
|
bindings.setNativeDemoPanelPreviewEnabled = [this](bool enabled) {
|
|
panels[ToPanelIndex(Application::ShellPanelId::XCUIDemo)].previewMode =
|
|
enabled
|
|
? Application::ShellHostedPreviewMode::NativeOffscreen
|
|
: Application::ShellHostedPreviewMode::HostedPresenter;
|
|
};
|
|
bindings.getNativeLayoutLabPreviewEnabled = [this]() {
|
|
return Panel(Application::ShellPanelId::XCUILayoutLab).previewMode ==
|
|
Application::ShellHostedPreviewMode::NativeOffscreen;
|
|
};
|
|
bindings.setNativeLayoutLabPreviewEnabled = [this](bool enabled) {
|
|
panels[ToPanelIndex(Application::ShellPanelId::XCUILayoutLab)].previewMode =
|
|
enabled
|
|
? Application::ShellHostedPreviewMode::NativeOffscreen
|
|
: Application::ShellHostedPreviewMode::HostedPresenter;
|
|
};
|
|
bindings.onHostedPreviewModeChanged = [this]() { ++hostedPreviewReconfigureCount; };
|
|
return bindings;
|
|
}
|
|
};
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, RegisterShellViewCommandsInvokesBoundToggleHandlers) {
|
|
ShellCommandHarness harness = {};
|
|
XCUIEditorCommandRouter router = {};
|
|
|
|
Application::RegisterShellViewCommands(router, harness.BuildBindings());
|
|
|
|
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleXCUIDemoPanel));
|
|
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleXCUILayoutLabPanel));
|
|
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleNativeBackdrop));
|
|
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::TogglePulseAccent));
|
|
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleNativeXCUIOverlay));
|
|
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleHostedPreviewHud));
|
|
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleNativeDemoPanelPreview));
|
|
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleNativeLayoutLabPreview));
|
|
|
|
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleXCUIDemoPanel));
|
|
EXPECT_FALSE(harness.Panel(Application::ShellPanelId::XCUIDemo).visible);
|
|
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleNativeBackdrop));
|
|
EXPECT_FALSE(harness.viewToggles.nativeBackdropVisible);
|
|
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleNativeXCUIOverlay));
|
|
EXPECT_FALSE(harness.viewToggles.nativeXCUIOverlayVisible);
|
|
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleHostedPreviewHud));
|
|
EXPECT_FALSE(harness.viewToggles.hostedPreviewHudVisible);
|
|
}
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, PreviewModeCommandsTriggerHostedPreviewReconfigureCallback) {
|
|
ShellCommandHarness harness = {};
|
|
XCUIEditorCommandRouter router = {};
|
|
|
|
Application::RegisterShellViewCommands(router, harness.BuildBindings());
|
|
|
|
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleNativeDemoPanelPreview));
|
|
EXPECT_EQ(
|
|
harness.Panel(Application::ShellPanelId::XCUIDemo).previewMode,
|
|
Application::ShellHostedPreviewMode::HostedPresenter);
|
|
EXPECT_EQ(harness.hostedPreviewReconfigureCount, 1);
|
|
|
|
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleNativeLayoutLabPreview));
|
|
EXPECT_EQ(
|
|
harness.Panel(Application::ShellPanelId::XCUILayoutLab).previewMode,
|
|
Application::ShellHostedPreviewMode::NativeOffscreen);
|
|
EXPECT_EQ(harness.hostedPreviewReconfigureCount, 2);
|
|
}
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, BuildShellShortcutSnapshotCarriesBridgeStateAndKeyboardEdges) {
|
|
XCUIInputBridgeFrameDelta frameDelta = {};
|
|
frameDelta.state.windowFocused = true;
|
|
frameDelta.state.wantCaptureKeyboard = true;
|
|
frameDelta.state.wantTextInput = true;
|
|
frameDelta.state.modifiers.control = true;
|
|
frameDelta.state.modifiers.shift = true;
|
|
frameDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
|
|
frameDelta.keyboard.repeatedKeys.push_back(static_cast<std::int32_t>(KeyCode::Two));
|
|
|
|
const auto snapshot = Application::BuildShellShortcutSnapshot(frameDelta);
|
|
|
|
EXPECT_TRUE(snapshot.windowFocused);
|
|
EXPECT_TRUE(snapshot.wantCaptureKeyboard);
|
|
EXPECT_TRUE(snapshot.wantTextInput);
|
|
EXPECT_TRUE(snapshot.modifiers.control);
|
|
EXPECT_TRUE(snapshot.modifiers.shift);
|
|
EXPECT_TRUE(snapshot.IsKeyDown(static_cast<std::int32_t>(KeyCode::One)));
|
|
EXPECT_TRUE(snapshot.IsKeyDown(static_cast<std::int32_t>(KeyCode::Two)));
|
|
|
|
const auto* repeatedKey = snapshot.FindKeyState(static_cast<std::int32_t>(KeyCode::Two));
|
|
ASSERT_NE(repeatedKey, nullptr);
|
|
EXPECT_TRUE(repeatedKey->repeat);
|
|
}
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, RegisteredShellShortcutsMatchExpectedViewCommands) {
|
|
ShellCommandHarness harness = {};
|
|
XCUIEditorCommandRouter router = {};
|
|
Application::RegisterShellViewCommands(router, harness.BuildBindings());
|
|
|
|
XCUIInputBridgeFrameDelta frameDelta = {};
|
|
frameDelta.state.windowFocused = true;
|
|
frameDelta.state.modifiers.control = true;
|
|
frameDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
|
|
|
|
const auto demoSnapshot = Application::BuildShellShortcutSnapshot(frameDelta);
|
|
const auto demoMatch = router.MatchShortcut({ &demoSnapshot });
|
|
ASSERT_TRUE(demoMatch.matched);
|
|
EXPECT_EQ(demoMatch.commandId, Application::ShellCommandIds::ToggleXCUIDemoPanel);
|
|
|
|
XCUIInputBridgeFrameDelta previewDelta = {};
|
|
previewDelta.state.windowFocused = true;
|
|
previewDelta.state.modifiers.control = true;
|
|
previewDelta.state.modifiers.alt = true;
|
|
previewDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::Two));
|
|
|
|
const auto previewSnapshot = Application::BuildShellShortcutSnapshot(previewDelta);
|
|
const auto previewMatch = router.MatchShortcut({ &previewSnapshot });
|
|
ASSERT_TRUE(previewMatch.matched);
|
|
EXPECT_EQ(previewMatch.commandId, Application::ShellCommandIds::ToggleNativeLayoutLabPreview);
|
|
}
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, RegisteredShellShortcutsRespectCaptureAndRepeatGuards) {
|
|
ShellCommandHarness harness = {};
|
|
XCUIEditorCommandRouter router = {};
|
|
Application::RegisterShellViewCommands(router, harness.BuildBindings());
|
|
|
|
XCUIInputBridgeFrameDelta capturedDelta = {};
|
|
capturedDelta.state.windowFocused = true;
|
|
capturedDelta.state.wantCaptureKeyboard = true;
|
|
capturedDelta.state.modifiers.control = true;
|
|
capturedDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
|
|
|
|
const auto capturedSnapshot = Application::BuildShellShortcutSnapshot(capturedDelta);
|
|
EXPECT_FALSE(router.MatchShortcut({ &capturedSnapshot }).matched);
|
|
|
|
XCUIInputBridgeFrameDelta textInputDelta = {};
|
|
textInputDelta.state.windowFocused = true;
|
|
textInputDelta.state.wantTextInput = true;
|
|
textInputDelta.state.modifiers.control = true;
|
|
textInputDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
|
|
|
|
const auto textInputSnapshot = Application::BuildShellShortcutSnapshot(textInputDelta);
|
|
EXPECT_FALSE(router.MatchShortcut({ &textInputSnapshot }).matched);
|
|
|
|
XCUIInputBridgeFrameDelta repeatedDelta = {};
|
|
repeatedDelta.state.windowFocused = true;
|
|
repeatedDelta.state.modifiers.control = true;
|
|
repeatedDelta.keyboard.repeatedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
|
|
|
|
const auto repeatedSnapshot = Application::BuildShellShortcutSnapshot(repeatedDelta);
|
|
EXPECT_FALSE(router.MatchShortcut({ &repeatedSnapshot }).matched);
|
|
}
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, PreviewShortcutInvokesCommandHandlerAndReconfigureCallback) {
|
|
ShellCommandHarness harness = {};
|
|
XCUIEditorCommandRouter router = {};
|
|
Application::RegisterShellViewCommands(router, harness.BuildBindings());
|
|
|
|
XCUIInputBridgeFrameDelta previewDelta = {};
|
|
previewDelta.state.windowFocused = true;
|
|
previewDelta.state.modifiers.control = true;
|
|
previewDelta.state.modifiers.alt = true;
|
|
previewDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
|
|
|
|
const auto previewSnapshot = Application::BuildShellShortcutSnapshot(previewDelta);
|
|
EXPECT_TRUE(router.InvokeMatchingShortcut({ &previewSnapshot }));
|
|
EXPECT_EQ(
|
|
harness.Panel(Application::ShellPanelId::XCUIDemo).previewMode,
|
|
Application::ShellHostedPreviewMode::HostedPresenter);
|
|
EXPECT_EQ(harness.hostedPreviewReconfigureCount, 1);
|
|
}
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, BeginHostedPreviewFrameLifecycleClearsQueueAndResetsRegistryFlags) {
|
|
XCUIHostedPreviewQueue queue = {};
|
|
XCUIHostedPreviewSurfaceRegistry registry = {};
|
|
|
|
UIDrawData drawData = {};
|
|
UIDrawList& drawList = drawData.EmplaceDrawList("HostedPreview");
|
|
drawList.AddFilledRect(UIRect(4.0f, 6.0f, 24.0f, 16.0f), UIColor(1.0f, 1.0f, 1.0f, 1.0f));
|
|
|
|
XCUIHostedPreviewFrame frame = {};
|
|
frame.drawData = &drawData;
|
|
frame.canvasRect = UIRect(0.0f, 0.0f, 160.0f, 90.0f);
|
|
frame.logicalSize = UISize(160.0f, 90.0f);
|
|
frame.debugName = "HostedPreview";
|
|
frame.debugSource = "tests.application";
|
|
ASSERT_TRUE(queue.Submit(frame));
|
|
ASSERT_EQ(queue.GetQueuedFrames().size(), 1u);
|
|
|
|
registry.RecordQueuedFrame(queue.GetQueuedFrames().front(), 0u);
|
|
ASSERT_EQ(registry.GetDescriptors().size(), 1u);
|
|
EXPECT_TRUE(registry.GetDescriptors().front().queuedThisFrame);
|
|
|
|
Application::BeginHostedPreviewFrameLifecycle(queue, registry);
|
|
|
|
EXPECT_TRUE(queue.GetQueuedFrames().empty());
|
|
ASSERT_EQ(registry.GetDescriptors().size(), 1u);
|
|
EXPECT_FALSE(registry.GetDescriptors().front().queuedThisFrame);
|
|
}
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, HostedPreviewRegistrationGuardsAllowTextureOnlyNativePublication) {
|
|
UITextureRegistration registration = {};
|
|
registration.texture = UITextureHandle{ 99u, 256u, 128u, UITextureHandleKind::ShaderResourceView };
|
|
|
|
EXPECT_TRUE(Application::HasHostedPreviewTextureRegistration(registration));
|
|
EXPECT_TRUE(Application::HasHostedPreviewPublishedTexture(registration));
|
|
|
|
registration = {};
|
|
registration.cpuHandle.ptr = 11u;
|
|
registration.gpuHandle.ptr = 29u;
|
|
|
|
EXPECT_TRUE(Application::HasHostedPreviewTextureRegistration(registration));
|
|
EXPECT_FALSE(Application::HasHostedPreviewPublishedTexture(registration));
|
|
}
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, NativeHostedPreviewConsumptionLeavesHostedPathInDirectAppendMode) {
|
|
const auto consumption = Application::ResolveNativeHostedPreviewConsumption(
|
|
false,
|
|
false,
|
|
false,
|
|
"Pending",
|
|
"Waiting");
|
|
|
|
EXPECT_EQ(
|
|
consumption.surfaceState,
|
|
Application::NativeHostedPreviewSurfaceState::Disabled);
|
|
EXPECT_FALSE(consumption.queueRuntimeFrame);
|
|
EXPECT_TRUE(consumption.appendRuntimeDrawDataToShell);
|
|
EXPECT_FALSE(consumption.showSurfaceImage);
|
|
EXPECT_TRUE(consumption.drawRuntimeDebugRects);
|
|
EXPECT_TRUE(consumption.placeholderTitle.empty());
|
|
EXPECT_TRUE(consumption.placeholderSubtitle.empty());
|
|
EXPECT_EQ(
|
|
Application::ComposeNativeHostedPreviewStatusLine(consumption, "Runtime status"),
|
|
"Runtime status");
|
|
}
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, NativeHostedPreviewConsumptionQueuesFrameWhileAwaitingFirstSurfacePublish) {
|
|
const auto consumption = Application::ResolveNativeHostedPreviewConsumption(
|
|
true,
|
|
false,
|
|
false,
|
|
"Native XCUI preview pending",
|
|
"Waiting for queued native preview output to publish into the shell card.");
|
|
|
|
EXPECT_EQ(
|
|
consumption.surfaceState,
|
|
Application::NativeHostedPreviewSurfaceState::AwaitingSubmit);
|
|
EXPECT_TRUE(consumption.queueRuntimeFrame);
|
|
EXPECT_FALSE(consumption.appendRuntimeDrawDataToShell);
|
|
EXPECT_FALSE(consumption.showSurfaceImage);
|
|
EXPECT_FALSE(consumption.drawRuntimeDebugRects);
|
|
EXPECT_EQ(consumption.placeholderTitle, "Native XCUI preview pending");
|
|
EXPECT_EQ(
|
|
consumption.placeholderSubtitle,
|
|
"Waiting for queued native preview output to publish into the shell card.");
|
|
EXPECT_EQ(
|
|
Application::ComposeNativeHostedPreviewStatusLine(consumption, "Runtime status"),
|
|
"Native surface awaiting submit | Runtime status");
|
|
}
|
|
|
|
TEST(ApplicationShellCommandBindingsTest, NativeHostedPreviewConsumptionDistinguishesWarmingFromLiveSurfaceStates) {
|
|
const auto warming = Application::ResolveNativeHostedPreviewConsumption(
|
|
true,
|
|
true,
|
|
false,
|
|
"Native layout preview pending",
|
|
"Waiting for queued native preview output to publish into the layout card.");
|
|
const auto live = Application::ResolveNativeHostedPreviewConsumption(
|
|
true,
|
|
true,
|
|
true,
|
|
"Native layout preview pending",
|
|
"Waiting for queued native preview output to publish into the layout card.");
|
|
|
|
EXPECT_EQ(
|
|
warming.surfaceState,
|
|
Application::NativeHostedPreviewSurfaceState::Warming);
|
|
EXPECT_TRUE(warming.queueRuntimeFrame);
|
|
EXPECT_FALSE(warming.appendRuntimeDrawDataToShell);
|
|
EXPECT_FALSE(warming.showSurfaceImage);
|
|
EXPECT_FALSE(warming.drawRuntimeDebugRects);
|
|
EXPECT_EQ(warming.placeholderTitle, "Native layout preview pending");
|
|
EXPECT_EQ(
|
|
Application::ComposeNativeHostedPreviewStatusLine(warming, "Layout status"),
|
|
"Native surface warming | Layout status");
|
|
|
|
EXPECT_EQ(
|
|
live.surfaceState,
|
|
Application::NativeHostedPreviewSurfaceState::Live);
|
|
EXPECT_TRUE(live.queueRuntimeFrame);
|
|
EXPECT_FALSE(live.appendRuntimeDrawDataToShell);
|
|
EXPECT_TRUE(live.showSurfaceImage);
|
|
EXPECT_TRUE(live.drawRuntimeDebugRects);
|
|
EXPECT_TRUE(live.placeholderTitle.empty());
|
|
EXPECT_TRUE(live.placeholderSubtitle.empty());
|
|
EXPECT_EQ(
|
|
Application::ComposeNativeHostedPreviewStatusLine(live, "Layout status"),
|
|
"Native surface live | Layout status");
|
|
}
|
|
|
|
} // namespace
|