From a53f47e561ded7456e110e644daf9daecdb0007b Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 04:53:24 +0800 Subject: [PATCH] Build XCEditor viewport input bridge foundation --- new_editor/CMakeLists.txt | 1 + .../Core/UIEditorViewportInputBridge.h | 67 ++ .../src/Core/UIEditorViewportInputBridge.cpp | 221 +++++ tests/UI/Editor/integration/CMakeLists.txt | 5 + tests/UI/Editor/integration/README.md | 9 + .../Editor/integration/state/CMakeLists.txt | 3 + .../CMakeLists.txt | 30 + .../captures/.gitkeep | 1 + .../viewport_input_bridge_basic/main.cpp | 866 ++++++++++++++++++ tests/UI/Editor/unit/CMakeLists.txt | 1 + .../test_ui_editor_viewport_input_bridge.cpp | 196 ++++ 11 files changed, 1400 insertions(+) create mode 100644 new_editor/include/XCEditor/Core/UIEditorViewportInputBridge.h create mode 100644 new_editor/src/Core/UIEditorViewportInputBridge.cpp create mode 100644 tests/UI/Editor/integration/state/viewport_input_bridge_basic/CMakeLists.txt create mode 100644 tests/UI/Editor/integration/state/viewport_input_bridge_basic/captures/.gitkeep create mode 100644 tests/UI/Editor/integration/state/viewport_input_bridge_basic/main.cpp create mode 100644 tests/UI/Editor/unit/test_ui_editor_viewport_input_bridge.cpp diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index d4e13fed..a385381c 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -19,6 +19,7 @@ add_library(XCUIEditorLib STATIC src/Core/UIEditorMenuSession.cpp src/Core/UIEditorPanelRegistry.cpp src/Core/UIEditorShortcutManager.cpp + src/Core/UIEditorViewportInputBridge.cpp src/Core/UIEditorWorkspaceLayoutPersistence.cpp src/Core/UIEditorWorkspaceController.cpp src/Core/UIEditorWorkspaceModel.cpp diff --git a/new_editor/include/XCEditor/Core/UIEditorViewportInputBridge.h b/new_editor/include/XCEditor/Core/UIEditorViewportInputBridge.h new file mode 100644 index 00000000..30b1d782 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorViewportInputBridge.h @@ -0,0 +1,67 @@ +#pragma once + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor { + +struct UIEditorViewportInputBridgeConfig { + bool focusOnPointerDownInside = true; + bool clearFocusOnPointerDownOutside = true; + bool capturePointerOnPointerDownInside = true; +}; + +struct UIEditorViewportInputBridgeState { + bool hovered = false; + bool focused = false; + bool captured = false; + ::XCEngine::UI::UIPointerButton captureButton = ::XCEngine::UI::UIPointerButton::None; + ::XCEngine::UI::UIPoint lastScreenPointerPosition = {}; + ::XCEngine::UI::UIPoint lastLocalPointerPosition = {}; + bool hasPointerPosition = false; + ::XCEngine::UI::UIInputModifiers modifiers = {}; + std::unordered_set pressedKeys = {}; + std::uint8_t pointerButtonsDownMask = 0; +}; + +struct UIEditorViewportInputBridgeFrame { + bool hovered = false; + bool focused = false; + bool captured = false; + bool pointerInside = false; + bool pointerMoved = false; + bool pointerPressedInside = false; + bool pointerReleasedInside = false; + bool focusGained = false; + bool focusLost = false; + bool captureStarted = false; + bool captureEnded = false; + ::XCEngine::UI::UIPointerButton changedPointerButton = ::XCEngine::UI::UIPointerButton::None; + ::XCEngine::UI::UIPoint screenPointerPosition = {}; + ::XCEngine::UI::UIPoint localPointerPosition = {}; + ::XCEngine::UI::UIPoint pointerDelta = {}; + float wheelDelta = 0.0f; + ::XCEngine::UI::UIInputModifiers modifiers = {}; + std::vector pressedKeyCodes = {}; + std::vector releasedKeyCodes = {}; + std::vector characters = {}; +}; + +bool IsUIEditorViewportInputBridgeKeyDown( + const UIEditorViewportInputBridgeState& state, + std::int32_t keyCode); + +bool IsUIEditorViewportInputBridgePointerButtonDown( + const UIEditorViewportInputBridgeState& state, + ::XCEngine::UI::UIPointerButton button); + +UIEditorViewportInputBridgeFrame UpdateUIEditorViewportInputBridge( + UIEditorViewportInputBridgeState& state, + const ::XCEngine::UI::UIRect& inputRect, + const std::vector<::XCEngine::UI::UIInputEvent>& events, + const UIEditorViewportInputBridgeConfig& config = {}); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Core/UIEditorViewportInputBridge.cpp b/new_editor/src/Core/UIEditorViewportInputBridge.cpp new file mode 100644 index 00000000..04907f3a --- /dev/null +++ b/new_editor/src/Core/UIEditorViewportInputBridge.cpp @@ -0,0 +1,221 @@ +#include + +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIPointerButton; +using ::XCEngine::UI::UIRect; + +bool ContainsPoint(const UIRect& rect, const UIPoint& point) { + return point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height; +} + +UIPoint ToLocalPoint(const UIRect& rect, const UIPoint& point) { + return UIPoint(point.x - rect.x, point.y - rect.y); +} + +std::uint8_t ButtonMask(UIPointerButton button) { + switch (button) { + case UIPointerButton::Left: return 1u << 0u; + case UIPointerButton::Right: return 1u << 1u; + case UIPointerButton::Middle: return 1u << 2u; + case UIPointerButton::X1: return 1u << 3u; + case UIPointerButton::X2: return 1u << 4u; + case UIPointerButton::None: + default: + return 0u; + } +} + +bool AnyPointerButtonsDown(const UIEditorViewportInputBridgeState& state) { + return state.pointerButtonsDownMask != 0u; +} + +void ClearCapture(UIEditorViewportInputBridgeState& state) { + state.captured = false; + state.captureButton = UIPointerButton::None; +} + +void ClearInputState(UIEditorViewportInputBridgeState& state) { + state.pressedKeys.clear(); + state.pointerButtonsDownMask = 0u; + ClearCapture(state); + state.hovered = false; +} + +void UpdatePointerPosition( + UIEditorViewportInputBridgeState& state, + UIEditorViewportInputBridgeFrame& frame, + const UIRect& inputRect, + const UIPoint& screenPosition, + const ::XCEngine::UI::UIInputModifiers& modifiers) { + if (state.hasPointerPosition) { + frame.pointerDelta.x += screenPosition.x - state.lastScreenPointerPosition.x; + frame.pointerDelta.y += screenPosition.y - state.lastScreenPointerPosition.y; + frame.pointerMoved = + frame.pointerMoved || + screenPosition.x != state.lastScreenPointerPosition.x || + screenPosition.y != state.lastScreenPointerPosition.y; + } + + state.lastScreenPointerPosition = screenPosition; + state.lastLocalPointerPosition = ToLocalPoint(inputRect, screenPosition); + state.hasPointerPosition = true; + state.modifiers = modifiers; + + frame.screenPointerPosition = state.lastScreenPointerPosition; + frame.localPointerPosition = state.lastLocalPointerPosition; + frame.modifiers = state.modifiers; +} + +} // namespace + +bool IsUIEditorViewportInputBridgeKeyDown( + const UIEditorViewportInputBridgeState& state, + std::int32_t keyCode) { + return state.pressedKeys.contains(keyCode); +} + +bool IsUIEditorViewportInputBridgePointerButtonDown( + const UIEditorViewportInputBridgeState& state, + UIPointerButton button) { + const std::uint8_t mask = ButtonMask(button); + return mask != 0u && (state.pointerButtonsDownMask & mask) != 0u; +} + +UIEditorViewportInputBridgeFrame UpdateUIEditorViewportInputBridge( + UIEditorViewportInputBridgeState& state, + const UIRect& inputRect, + const std::vector& events, + const UIEditorViewportInputBridgeConfig& config) { + UIEditorViewportInputBridgeFrame frame = {}; + frame.screenPointerPosition = state.lastScreenPointerPosition; + frame.localPointerPosition = state.lastLocalPointerPosition; + frame.modifiers = state.modifiers; + + for (const UIInputEvent& event : events) { + const bool inside = ContainsPoint(inputRect, event.position); + switch (event.type) { + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerMove: + UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers); + state.hovered = inside; + break; + case UIInputEventType::PointerLeave: + state.hovered = false; + state.lastScreenPointerPosition = event.position; + state.lastLocalPointerPosition = ToLocalPoint(inputRect, event.position); + frame.screenPointerPosition = state.lastScreenPointerPosition; + frame.localPointerPosition = state.lastLocalPointerPosition; + frame.modifiers = state.modifiers; + break; + case UIInputEventType::PointerButtonDown: + UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers); + state.hovered = inside; + state.pointerButtonsDownMask |= ButtonMask(event.pointerButton); + frame.changedPointerButton = event.pointerButton; + if (inside) { + frame.pointerPressedInside = true; + if (config.focusOnPointerDownInside && !state.focused) { + state.focused = true; + frame.focusGained = true; + } + if (config.capturePointerOnPointerDownInside && !state.captured) { + state.captured = true; + state.captureButton = event.pointerButton; + frame.captureStarted = true; + } + } else if (config.clearFocusOnPointerDownOutside) { + if (state.focused) { + state.focused = false; + frame.focusLost = true; + } + if (state.captured) { + ClearCapture(state); + frame.captureEnded = true; + } + } + break; + case UIInputEventType::PointerButtonUp: + UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers); + state.hovered = inside; + state.pointerButtonsDownMask &= static_cast(~ButtonMask(event.pointerButton)); + frame.changedPointerButton = event.pointerButton; + if (inside) { + frame.pointerReleasedInside = true; + } + if (state.captured && + state.captureButton == event.pointerButton && + !AnyPointerButtonsDown(state)) { + ClearCapture(state); + frame.captureEnded = true; + } + break; + case UIInputEventType::PointerWheel: + UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers); + state.hovered = inside; + if (inside || state.captured) { + frame.wheelDelta += event.wheelDelta; + } + break; + case UIInputEventType::KeyDown: + state.modifiers = event.modifiers; + frame.modifiers = state.modifiers; + if (state.focused) { + state.pressedKeys.insert(event.keyCode); + frame.pressedKeyCodes.push_back(event.keyCode); + } + break; + case UIInputEventType::KeyUp: + state.modifiers = event.modifiers; + frame.modifiers = state.modifiers; + if (state.focused) { + state.pressedKeys.erase(event.keyCode); + frame.releasedKeyCodes.push_back(event.keyCode); + } + break; + case UIInputEventType::Character: + if (state.focused) { + frame.characters.push_back(event.character); + } + break; + case UIInputEventType::FocusLost: + if (state.focused) { + frame.focusLost = true; + } + if (state.captured) { + frame.captureEnded = true; + } + state.focused = false; + ClearInputState(state); + break; + case UIInputEventType::FocusGained: + state.modifiers = event.modifiers; + frame.modifiers = state.modifiers; + break; + case UIInputEventType::None: + default: + break; + } + } + + frame.pointerInside = ContainsPoint(inputRect, state.lastScreenPointerPosition); + frame.hovered = state.hovered; + frame.focused = state.focused; + frame.captured = state.captured; + frame.screenPointerPosition = state.lastScreenPointerPosition; + frame.localPointerPosition = state.lastLocalPointerPosition; + frame.modifiers = state.modifiers; + return frame; +} + +} // namespace XCEngine::UI::Editor diff --git a/tests/UI/Editor/integration/CMakeLists.txt b/tests/UI/Editor/integration/CMakeLists.txt index 2ed4c9a1..76ce5161 100644 --- a/tests/UI/Editor/integration/CMakeLists.txt +++ b/tests/UI/Editor/integration/CMakeLists.txt @@ -29,6 +29,11 @@ if(TARGET editor_ui_viewport_slot_basic_validation) editor_ui_viewport_slot_basic_validation) endif() +if(TARGET editor_ui_viewport_input_bridge_basic_validation) + list(APPEND EDITOR_UI_INTEGRATION_TARGETS + editor_ui_viewport_input_bridge_basic_validation) +endif() + add_custom_target(editor_ui_integration_tests DEPENDS ${EDITOR_UI_INTEGRATION_TARGETS} diff --git a/tests/UI/Editor/integration/README.md b/tests/UI/Editor/integration/README.md index 8aa1f835..c4bf5c7d 100644 --- a/tests/UI/Editor/integration/README.md +++ b/tests/UI/Editor/integration/README.md @@ -22,6 +22,7 @@ Layout: - `state/panel_session_flow/`: panel session state flow only - `state/layout_persistence/`: layout save/load/reset only - `state/shortcut_dispatch/`: shortcut match/suppression/dispatch only +- `state/viewport_input_bridge_basic/`: viewport hover/focus/capture, local pointer coordinates, wheel/key/character bridge only Scenarios: @@ -75,6 +76,11 @@ Scenarios: Executable: `XCUIEditorShortcutDispatchValidation.exe` Scope: shortcut match + scope + suppression + command dispatch +- `editor.state.viewport_input_bridge_basic` + Build target: `editor_ui_viewport_input_bridge_basic_validation` + Executable: `XCUIEditorViewportInputBridgeBasicValidation.exe` + Scope: viewport hover/focus/capture, local pointer coordinates, wheel/key/character bridge only + Run: ```bash @@ -117,3 +123,6 @@ Selected controls: - `state/shortcut_dispatch/` Press `Ctrl+P / Ctrl+H / Ctrl+W / Ctrl+O / Ctrl+R`, toggle `Text Input`, press `F12`. + +- `state/viewport_input_bridge_basic/` + Hover surface, click and drag outside, roll the wheel, press `A / W / F / Space`, type characters, click outside to clear focus, press `F12`. diff --git a/tests/UI/Editor/integration/state/CMakeLists.txt b/tests/UI/Editor/integration/state/CMakeLists.txt index 50f96325..fee709e0 100644 --- a/tests/UI/Editor/integration/state/CMakeLists.txt +++ b/tests/UI/Editor/integration/state/CMakeLists.txt @@ -1,3 +1,6 @@ add_subdirectory(panel_session_flow) add_subdirectory(layout_persistence) add_subdirectory(shortcut_dispatch) +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/viewport_input_bridge_basic/CMakeLists.txt") + add_subdirectory(viewport_input_bridge_basic) +endif() diff --git a/tests/UI/Editor/integration/state/viewport_input_bridge_basic/CMakeLists.txt b/tests/UI/Editor/integration/state/viewport_input_bridge_basic/CMakeLists.txt new file mode 100644 index 00000000..d7271a9a --- /dev/null +++ b/tests/UI/Editor/integration/state/viewport_input_bridge_basic/CMakeLists.txt @@ -0,0 +1,30 @@ +add_executable(editor_ui_viewport_input_bridge_basic_validation WIN32 + main.cpp +) + +target_include_directories(editor_ui_viewport_input_bridge_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/new_editor/include + ${CMAKE_SOURCE_DIR}/new_editor/app + ${CMAKE_SOURCE_DIR}/engine/include +) + +target_compile_definitions(editor_ui_viewport_input_bridge_basic_validation PRIVATE + UNICODE + _UNICODE + XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}" +) + +if(MSVC) + target_compile_options(editor_ui_viewport_input_bridge_basic_validation PRIVATE /utf-8 /FS) + set_property(TARGET editor_ui_viewport_input_bridge_basic_validation PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +target_link_libraries(editor_ui_viewport_input_bridge_basic_validation PRIVATE + XCUIEditorLib + XCUIEditorHost +) + +set_target_properties(editor_ui_viewport_input_bridge_basic_validation PROPERTIES + OUTPUT_NAME "XCUIEditorViewportInputBridgeBasicValidation" +) diff --git a/tests/UI/Editor/integration/state/viewport_input_bridge_basic/captures/.gitkeep b/tests/UI/Editor/integration/state/viewport_input_bridge_basic/captures/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/UI/Editor/integration/state/viewport_input_bridge_basic/captures/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/UI/Editor/integration/state/viewport_input_bridge_basic/main.cpp b/tests/UI/Editor/integration/state/viewport_input_bridge_basic/main.cpp new file mode 100644 index 00000000..2672dbfe --- /dev/null +++ b/tests/UI/Editor/integration/state/viewport_input_bridge_basic/main.cpp @@ -0,0 +1,866 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include "Host/AutoScreenshot.h" +#include "Host/InputModifierTracker.h" +#include "Host/NativeRenderer.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT +#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "." +#endif + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIColor; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Host::AutoScreenshotController; +using XCEngine::UI::Editor::Host::InputModifierTracker; +using XCEngine::UI::Editor::Host::NativeRenderer; +using XCEngine::UI::Editor::IsUIEditorViewportInputBridgeKeyDown; +using XCEngine::UI::Editor::IsUIEditorViewportInputBridgePointerButtonDown; +using XCEngine::UI::Editor::UIEditorViewportInputBridgeFrame; +using XCEngine::UI::Editor::UIEditorViewportInputBridgeState; +using XCEngine::UI::Editor::UpdateUIEditorViewportInputBridge; +using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotBackground; +using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotForeground; +using XCEngine::UI::Editor::Widgets::BuildUIEditorViewportSlotLayout; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotChrome; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotFrame; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotLayout; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotState; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot; + +constexpr const wchar_t* kWindowClassName = L"XCUIEditorViewportInputBridgeBasicValidation"; +constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Viewport Input Bridge"; + +constexpr UIColor kWindowBg(0.12f, 0.12f, 0.12f, 1.0f); +constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f); +constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f); +constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f); +constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f); +constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f); +constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f); +constexpr UIColor kButtonBorder(0.47f, 0.47f, 0.47f, 1.0f); +constexpr UIColor kPreviewBg(0.15f, 0.15f, 0.15f, 1.0f); + +enum class ActionId : unsigned char { + Reset = 0, + Capture +}; + +struct ButtonState { + ActionId action = ActionId::Reset; + std::string label = {}; + UIRect rect = {}; +}; + +std::filesystem::path ResolveRepoRootPath() { + std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT; + if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { + root = root.substr(1u, root.size() - 2u); + } + + return std::filesystem::path(root).lexically_normal(); +} + +bool ContainsPoint(const UIRect& rect, float x, float y) { + return x >= rect.x && + x <= rect.x + rect.width && + y >= rect.y && + y <= rect.y + rect.height; +} + +std::string BoolText(bool value) { + return value ? "On" : "Off"; +} + +std::string FormatFloat(float value) { + std::ostringstream stream = {}; + stream.setf(std::ios::fixed, std::ios::floatfield); + stream.precision(1); + stream << value; + return stream.str(); +} + +std::string FormatPoint(const UIPoint& point) { + return "(" + FormatFloat(point.x) + ", " + FormatFloat(point.y) + ")"; +} + +std::string DescribeKeyCode(std::int32_t keyCode) { + switch (static_cast(keyCode)) { + case KeyCode::A: return "A"; + case KeyCode::D: return "D"; + case KeyCode::F: return "F"; + case KeyCode::Q: return "Q"; + case KeyCode::S: return "S"; + case KeyCode::W: return "W"; + case KeyCode::Space: return "Space"; + case KeyCode::LeftShift: return "LeftShift"; + case KeyCode::LeftCtrl: return "LeftCtrl"; + case KeyCode::LeftAlt: return "LeftAlt"; + case KeyCode::Escape: return "Escape"; + case KeyCode::Enter: return "Enter"; + case KeyCode::Left: return "Left"; + case KeyCode::Right: return "Right"; + case KeyCode::Up: return "Up"; + case KeyCode::Down: return "Down"; + case KeyCode::F12: return "F12"; + case KeyCode::None: + default: + return std::to_string(keyCode); + } +} + +std::string FormatKeyCodes(const std::vector& keyCodes) { + if (keyCodes.empty()) { + return "(none)"; + } + + std::string result = {}; + for (std::size_t index = 0u; index < keyCodes.size(); ++index) { + if (index > 0u) { + result += ", "; + } + + result += DescribeKeyCode(keyCodes[index]); + } + + return result; +} + +std::string DescribeButtonStates(const UIEditorViewportInputBridgeState& state) { + return std::string("L=") + + BoolText(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Left)) + + " R=" + + BoolText(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Right)) + + " M=" + + BoolText(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Middle)); +} + +std::string DescribeKeyStates(const UIEditorViewportInputBridgeState& state) { + return std::string("W=") + + BoolText(IsUIEditorViewportInputBridgeKeyDown(state, static_cast(KeyCode::W))) + + " A=" + + BoolText(IsUIEditorViewportInputBridgeKeyDown(state, static_cast(KeyCode::A))) + + " F=" + + BoolText(IsUIEditorViewportInputBridgeKeyDown(state, static_cast(KeyCode::F))) + + " Space=" + + BoolText(IsUIEditorViewportInputBridgeKeyDown(state, static_cast(KeyCode::Space))); +} + +std::string DescribeCharacters(const UIEditorViewportInputBridgeFrame& frame) { + if (frame.characters.empty()) { + return "(none)"; + } + + std::string result = {}; + for (std::size_t index = 0u; index < frame.characters.size(); ++index) { + if (index > 0u) { + result += ", "; + } + + const std::uint32_t character = frame.characters[index]; + if (character >= 32u && character < 127u) { + result.push_back(static_cast(character)); + } else { + result += "U+" + std::to_string(character); + } + } + + return result; +} + +std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) { + switch (wParam) { + case 'A': return static_cast(KeyCode::A); + case 'D': return static_cast(KeyCode::D); + case 'F': return static_cast(KeyCode::F); + case 'Q': return static_cast(KeyCode::Q); + case 'S': return static_cast(KeyCode::S); + case 'W': return static_cast(KeyCode::W); + case VK_SPACE: return static_cast(KeyCode::Space); + case VK_SHIFT: return static_cast(KeyCode::LeftShift); + case VK_CONTROL: return static_cast(KeyCode::LeftCtrl); + case VK_MENU: return static_cast(KeyCode::LeftAlt); + case VK_ESCAPE: return static_cast(KeyCode::Escape); + case VK_RETURN: return static_cast(KeyCode::Enter); + case VK_UP: return static_cast(KeyCode::Up); + case VK_DOWN: return static_cast(KeyCode::Down); + case VK_LEFT: return static_cast(KeyCode::Left); + case VK_RIGHT: return static_cast(KeyCode::Right); + case VK_F12: return static_cast(KeyCode::F12); + default: return static_cast(KeyCode::None); + } +} + +void DrawCard( + UIDrawList& drawList, + const UIRect& rect, + std::string_view title, + std::string_view subtitle = {}) { + drawList.AddFilledRect(rect, kCardBg, 10.0f); + drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f); + drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f); + if (!subtitle.empty()) { + drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 38.0f), std::string(subtitle), kTextMuted, 12.0f); + } +} + +void DrawButton(UIDrawList& drawList, const ButtonState& button) { + drawList.AddFilledRect(button.rect, kButtonBg, 8.0f); + drawList.AddRectOutline(button.rect, kButtonBorder, 1.0f, 8.0f); + drawList.AddText( + UIPoint(button.rect.x + 12.0f, button.rect.y + 10.0f), + button.label, + kTextPrimary, + 12.0f); +} + +class ScenarioApp { +public: + int Run(HINSTANCE hInstance, int nCmdShow) { + if (!Initialize(hInstance, nCmdShow)) { + Shutdown(); + return 1; + } + + MSG message = {}; + while (message.message != WM_QUIT) { + if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + TranslateMessage(&message); + DispatchMessageW(&message); + continue; + } + + RenderFrame(); + Sleep(8); + } + + Shutdown(); + return static_cast(message.wParam); + } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->m_renderer.Resize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + case WM_MOUSEMOVE: + if (app != nullptr) { + app->QueuePointerEvent(UIInputEventType::PointerMove, UIPointerButton::None, wParam, lParam); + TRACKMOUSEEVENT event = {}; + event.cbSize = sizeof(event); + event.dwFlags = TME_LEAVE; + event.hwndTrack = hwnd; + TrackMouseEvent(&event); + return 0; + } + break; + case WM_MOUSELEAVE: + if (app != nullptr) { + app->QueuePointerLeaveEvent(); + return 0; + } + break; + case WM_LBUTTONDOWN: + if (app != nullptr) { + SetFocus(hwnd); + app->HandlePointerDown(UIPointerButton::Left, wParam, lParam); + return 0; + } + break; + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandlePointerUp(UIPointerButton::Left, wParam, lParam); + return 0; + } + break; + case WM_RBUTTONDOWN: + if (app != nullptr) { + SetFocus(hwnd); + app->HandlePointerDown(UIPointerButton::Right, wParam, lParam); + return 0; + } + break; + case WM_RBUTTONUP: + if (app != nullptr) { + app->HandlePointerUp(UIPointerButton::Right, wParam, lParam); + return 0; + } + break; + case WM_MBUTTONDOWN: + if (app != nullptr) { + SetFocus(hwnd); + app->HandlePointerDown(UIPointerButton::Middle, wParam, lParam); + return 0; + } + break; + case WM_MBUTTONUP: + if (app != nullptr) { + app->HandlePointerUp(UIPointerButton::Middle, wParam, lParam); + return 0; + } + break; + case WM_MOUSEWHEEL: + if (app != nullptr) { + app->QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam); + return 0; + } + break; + case WM_SETFOCUS: + if (app != nullptr) { + app->m_inputModifierTracker.SyncFromSystemState(); + app->QueueFocusEvent(UIInputEventType::FocusGained); + return 0; + } + break; + case WM_KILLFOCUS: + if (app != nullptr) { + if (GetCapture() == hwnd) { + ReleaseCapture(); + } + app->m_inputModifierTracker.Reset(); + app->QueueFocusEvent(UIInputEventType::FocusLost); + return 0; + } + break; + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr) { + if (wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + InvalidateRect(hwnd, nullptr, FALSE); + UpdateWindow(hwnd); + return 0; + } + app->QueueKeyEvent(UIInputEventType::KeyDown, wParam, lParam); + return 0; + } + break; + case WM_KEYUP: + case WM_SYSKEYUP: + if (app != nullptr) { + app->QueueKeyEvent(UIInputEventType::KeyUp, wParam, lParam); + return 0; + } + break; + case WM_CHAR: + if (app != nullptr) { + app->QueueCharacterEvent(wParam); + return 0; + } + break; + case WM_PAINT: + if (app != nullptr) { + PAINTSTRUCT paintStruct = {}; + BeginPaint(hwnd, &paintStruct); + app->RenderFrame(); + EndPaint(hwnd, &paintStruct); + return 0; + } + break; + case WM_ERASEBKGND: + return 1; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + default: + break; + } + + return DefWindowProcW(hwnd, message, wParam, lParam); + } + + bool Initialize(HINSTANCE hInstance, int nCmdShow) { + m_captureRoot = + ResolveRepoRootPath() / "tests/UI/Editor/integration/state/viewport_input_bridge_basic/captures"; + m_autoScreenshot.Initialize(m_captureRoot); + + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(windowClass); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = &ScenarioApp::WndProc; + windowClass.hInstance = hInstance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.lpszClassName = kWindowClassName; + + m_windowClassAtom = RegisterClassExW(&windowClass); + if (m_windowClassAtom == 0) { + return false; + } + + m_hwnd = CreateWindowExW( + 0, + kWindowClassName, + kWindowTitle, + WS_OVERLAPPEDWINDOW | WS_VISIBLE, + CW_USEDEFAULT, + CW_USEDEFAULT, + 1520, + 940, + nullptr, + nullptr, + hInstance, + this); + if (m_hwnd == nullptr) { + return false; + } + + if (!m_renderer.Initialize(m_hwnd)) { + return false; + } + + ShowWindow(m_hwnd, nCmdShow); + ResetScenario(); + return true; + } + + void Shutdown() { + m_autoScreenshot.Shutdown(); + m_renderer.Shutdown(); + + if (m_hwnd != nullptr && IsWindow(m_hwnd)) { + DestroyWindow(m_hwnd); + } + m_hwnd = nullptr; + + if (m_windowClassAtom != 0) { + UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr)); + m_windowClassAtom = 0; + } + } + + void ResetScenario() { + m_inputModifierTracker.Reset(); + m_bridgeState = {}; + m_bridgeFrame = {}; + m_pendingEvents.clear(); + m_lastResult = "Ready"; + } + + void QueuePointerEvent(UIInputEventType type, UIPointerButton button, WPARAM wParam, LPARAM lParam) { + UIInputEvent event = {}; + event.type = type; + event.pointerButton = button; + event.position = UIPoint( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast(wParam)); + m_pendingEvents.push_back(event); + } + + void QueuePointerLeaveEvent() { + UIInputEvent event = {}; + event.type = UIInputEventType::PointerLeave; + if (m_hwnd != nullptr) { + POINT clientPoint = {}; + GetCursorPos(&clientPoint); + ScreenToClient(m_hwnd, &clientPoint); + event.position = UIPoint(static_cast(clientPoint.x), static_cast(clientPoint.y)); + } + m_pendingEvents.push_back(event); + } + + void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) { + if (m_hwnd == nullptr) { + return; + } + + POINT screenPoint = { + GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam) + }; + ScreenToClient(m_hwnd, &screenPoint); + + UIInputEvent event = {}; + event.type = UIInputEventType::PointerWheel; + event.position = UIPoint(static_cast(screenPoint.x), static_cast(screenPoint.y)); + event.wheelDelta = static_cast(wheelDelta); + event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast(wParam)); + m_pendingEvents.push_back(event); + } + + void QueueKeyEvent(UIInputEventType type, WPARAM wParam, LPARAM lParam) { + UIInputEvent event = {}; + event.type = type; + event.keyCode = MapVirtualKeyToUIKeyCode(wParam); + event.modifiers = m_inputModifierTracker.ApplyKeyMessage(type, wParam, lParam); + event.repeat = (static_cast(lParam) & 0x40000000u) != 0u; + m_pendingEvents.push_back(event); + } + + void QueueCharacterEvent(WPARAM wParam) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(wParam); + event.modifiers = m_inputModifierTracker.GetCurrentModifiers(); + m_pendingEvents.push_back(event); + } + + void QueueFocusEvent(UIInputEventType type) { + UIInputEvent event = {}; + event.type = type; + event.modifiers = m_inputModifierTracker.GetCurrentModifiers(); + m_pendingEvents.push_back(event); + } + + void HandlePointerDown(UIPointerButton button, WPARAM wParam, LPARAM lParam) { + QueuePointerEvent(UIInputEventType::PointerButtonDown, button, wParam, lParam); + const UIPoint point( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + if (ContainsPoint(m_layout.inputRect, point.x, point.y)) { + SetCapture(m_hwnd); + } + } + + void HandlePointerUp(UIPointerButton button, WPARAM wParam, LPARAM lParam) { + QueuePointerEvent(UIInputEventType::PointerButtonUp, button, wParam, lParam); + + const UIPoint point( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + for (const ButtonState& buttonState : m_buttons) { + if (!ContainsPoint(buttonState.rect, point.x, point.y)) { + continue; + } + + if (buttonState.action == ActionId::Reset) { + ResetScenario(); + m_lastResult = "状态已重置"; + } else { + m_autoScreenshot.RequestCapture("manual_button"); + InvalidateRect(m_hwnd, nullptr, FALSE); + UpdateWindow(m_hwnd); + m_lastResult = "截图已排队"; + } + + break; + } + + if (GetCapture() == m_hwnd) { + ReleaseCapture(); + } + } + + UIEditorViewportSlotChrome BuildChrome() const { + UIEditorViewportSlotChrome chrome = {}; + chrome.title = "Scene View"; + chrome.subtitle = "ViewportInputBridge 验证面板"; + chrome.showTopBar = true; + chrome.showBottomBar = true; + chrome.topBarHeight = 40.0f; + chrome.bottomBarHeight = 28.0f; + return chrome; + } + + UIEditorViewportSlotFrame BuildFrame() const { + UIEditorViewportSlotFrame frame = {}; + frame.hasTexture = true; + frame.texture = { 1u, 1280u, 720u }; + frame.presentedSize = { 1280.0f, 720.0f }; + frame.statusText = "Fake viewport frame"; + return frame; + } + + std::vector BuildToolItems() const { + return { + { "mode", "Perspective", UIEditorViewportSlotToolSlot::Leading, true, true, 98.0f }, + { "input", "Input", UIEditorViewportSlotToolSlot::Trailing, true, true, 58.0f } + }; + } + + std::vector BuildStatusSegments() const { + return { + { "hover", std::string("Hover ") + BoolText(m_bridgeFrame.hovered), UIEditorStatusBarSlot::Leading, {}, true, true, 96.0f }, + { "focus", std::string("Focus ") + BoolText(m_bridgeFrame.focused), UIEditorStatusBarSlot::Leading, {}, true, false, 92.0f }, + { "capture", std::string("Capture ") + BoolText(m_bridgeFrame.captured), UIEditorStatusBarSlot::Trailing, {}, true, true, 110.0f }, + { "wheel", "Wheel " + FormatFloat(m_bridgeFrame.wheelDelta), UIEditorStatusBarSlot::Trailing, {}, true, false, 86.0f } + }; + } + + void UpdateLayoutForCurrentWindow() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + + const float leftColumnWidth = 440.0f; + const float outerPadding = 20.0f; + m_introRect = UIRect(outerPadding, outerPadding, leftColumnWidth, 246.0f); + m_controlsRect = UIRect(outerPadding, 286.0f, leftColumnWidth, 144.0f); + m_stateRect = UIRect(outerPadding, 450.0f, leftColumnWidth, height - 470.0f); + m_previewRect = UIRect( + leftColumnWidth + outerPadding * 2.0f, + outerPadding, + width - leftColumnWidth - outerPadding * 3.0f, + height - outerPadding * 2.0f); + m_slotRect = UIRect( + m_previewRect.x + 18.0f, + m_previewRect.y + 18.0f, + m_previewRect.width - 36.0f, + m_previewRect.height - 36.0f); + + const float buttonHeight = 34.0f; + m_buttons = { + { ActionId::Reset, "Reset", UIRect(m_controlsRect.x + 16.0f, m_controlsRect.y + 54.0f, m_controlsRect.width - 32.0f, buttonHeight) }, + { ActionId::Capture, "截图", UIRect(m_controlsRect.x + 16.0f, m_controlsRect.y + 98.0f, m_controlsRect.width - 32.0f, buttonHeight) } + }; + + m_layout = BuildUIEditorViewportSlotLayout( + m_slotRect, + BuildChrome(), + BuildFrame(), + BuildToolItems(), + BuildStatusSegments()); + } + + void RenderFrame() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + + UpdateLayoutForCurrentWindow(); + + std::vector frameEvents = std::move(m_pendingEvents); + m_pendingEvents.clear(); + m_bridgeFrame = UpdateUIEditorViewportInputBridge( + m_bridgeState, + m_layout.inputRect, + frameEvents); + if (m_bridgeFrame.focusLost) { + m_lastResult = "FocusLost"; + } else if (m_bridgeFrame.focusGained) { + m_lastResult = "FocusGained"; + } else if (m_bridgeFrame.captureStarted) { + m_lastResult = "CaptureStarted"; + } else if (m_bridgeFrame.captureEnded) { + m_lastResult = "CaptureEnded"; + } else if (!m_bridgeFrame.characters.empty()) { + m_lastResult = "Char " + DescribeCharacters(m_bridgeFrame); + } else if (!m_bridgeFrame.pressedKeyCodes.empty()) { + m_lastResult = "KeyDown " + FormatKeyCodes(m_bridgeFrame.pressedKeyCodes); + } else if (!m_bridgeFrame.releasedKeyCodes.empty()) { + m_lastResult = "KeyUp " + FormatKeyCodes(m_bridgeFrame.releasedKeyCodes); + } else if (m_bridgeFrame.wheelDelta != 0.0f) { + m_lastResult = "Wheel " + std::to_string(static_cast(m_bridgeFrame.wheelDelta)); + } else if (m_bridgeFrame.pointerPressedInside) { + m_lastResult = "PointerDownInside"; + } else if (m_bridgeFrame.pointerReleasedInside) { + m_lastResult = "PointerUpInside"; + } + + UIEditorViewportSlotState slotState = {}; + slotState.focused = m_bridgeFrame.focused; + slotState.surfaceHovered = m_bridgeFrame.hovered; + slotState.surfaceActive = m_bridgeFrame.focused || m_bridgeFrame.captured; + slotState.inputCaptured = m_bridgeFrame.captured; + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("ViewportInputBridgeBasic"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); + + DrawCard( + drawList, + m_introRect, + "测试功能:ViewportInputBridge", + "只验证 Editor viewport 输入桥,不混入 Scene/Game 业务。"); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 66.0f), + "检查1:hover / focus / capture / local 坐标。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 86.0f), + "检查2:surface 内左键按下后,Focus + Capture 进入。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 106.0f), + "检查3:drag 出 surface 后,Capture 保留,Local Pos 继续更新。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 126.0f), + "检查4:release 后,Capture 必须释放。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 146.0f), + "检查5:滚轮 / 按键 / 字符只在 Focus 下进入 frame。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 166.0f), + "操作:hover,click,drag,wheel,A/W/F/Space,输入字符。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 186.0f), + "操作:点击 surface 外部,验证 clear focus。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 210.0f), + "结果:左侧状态、底部条、surface 边框必须同步。", + kTextWeak, + 11.0f); + + DrawCard(drawList, m_controlsRect, "操作", "只保留 Reset / 截图两个辅助操作。"); + for (const ButtonState& button : m_buttons) { + DrawButton(drawList, button); + } + + DrawCard(drawList, m_stateRect, "状态", "重点看桥接 frame、按键边沿、字符与 modifiers。"); + float stateY = m_stateRect.y + 66.0f; + auto addStateLine = [&](std::string text, const UIColor& color = kTextPrimary) { + drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY), std::move(text), color, 12.0f); + stateY += 22.0f; + }; + addStateLine("Hover: " + BoolText(m_bridgeFrame.hovered)); + addStateLine("Focus: " + BoolText(m_bridgeFrame.focused)); + addStateLine("Capture: " + BoolText(m_bridgeFrame.captured)); + addStateLine("Pointer Inside: " + BoolText(m_bridgeFrame.pointerInside)); + addStateLine("Screen Pos: " + FormatPoint(m_bridgeFrame.screenPointerPosition)); + addStateLine("Local Pos: " + FormatPoint(m_bridgeFrame.localPointerPosition)); + addStateLine("Delta: " + FormatPoint(m_bridgeFrame.pointerDelta)); + addStateLine("Wheel: " + FormatFloat(m_bridgeFrame.wheelDelta)); + addStateLine("Buttons Down: " + DescribeButtonStates(m_bridgeState)); + addStateLine("Keys Down: " + DescribeKeyStates(m_bridgeState)); + addStateLine( + "Edges: pressIn " + BoolText(m_bridgeFrame.pointerPressedInside) + + " | releaseIn " + BoolText(m_bridgeFrame.pointerReleasedInside)); + addStateLine( + "Focus/Capture Edges: gain " + BoolText(m_bridgeFrame.focusGained) + + " | lost " + BoolText(m_bridgeFrame.focusLost) + + " | cap+ " + BoolText(m_bridgeFrame.captureStarted) + + " | cap- " + BoolText(m_bridgeFrame.captureEnded)); + addStateLine( + std::string("Modifiers: Ctrl ") + BoolText(m_bridgeFrame.modifiers.control) + + " Shift " + BoolText(m_bridgeFrame.modifiers.shift) + + " Alt " + BoolText(m_bridgeFrame.modifiers.alt)); + addStateLine("Characters: " + DescribeCharacters(m_bridgeFrame), kTextMuted); + const std::string captureSummary = + m_autoScreenshot.HasPendingCapture() + ? "截图排队中..." + : (m_autoScreenshot.GetLastCaptureSummary().empty() + ? std::string("截图:F12 或按钮 -> viewport_input_bridge_basic/captures/") + : m_autoScreenshot.GetLastCaptureSummary()); + addStateLine("Result: " + m_lastResult, kTextMuted); + addStateLine(captureSummary, kTextWeak); + + DrawCard(drawList, m_previewRect, "Preview", "这里只放一个 ViewportSlot,用它承载输入边界。"); + drawList.AddFilledRect( + UIRect( + m_previewRect.x + 12.0f, + m_previewRect.y + 44.0f, + m_previewRect.width - 24.0f, + m_previewRect.height - 56.0f), + kPreviewBg, + 10.0f); + + const auto chrome = BuildChrome(); + const auto frame = BuildFrame(); + const auto toolItems = BuildToolItems(); + const auto statusSegments = BuildStatusSegments(); + m_layout = BuildUIEditorViewportSlotLayout( + m_slotRect, + chrome, + frame, + toolItems, + statusSegments); + AppendUIEditorViewportSlotBackground( + drawList, + m_layout, + toolItems, + statusSegments, + slotState); + AppendUIEditorViewportSlotForeground( + drawList, + m_layout, + chrome, + frame, + toolItems, + statusSegments, + slotState); + + const bool framePresented = m_renderer.Render(drawData); + m_autoScreenshot.CaptureIfRequested( + m_renderer, + drawData, + static_cast(width), + static_cast(height), + framePresented); + } + + HWND m_hwnd = nullptr; + ATOM m_windowClassAtom = 0; + NativeRenderer m_renderer = {}; + AutoScreenshotController m_autoScreenshot = {}; + InputModifierTracker m_inputModifierTracker = {}; + std::filesystem::path m_captureRoot = {}; + std::vector m_pendingEvents = {}; + std::vector m_buttons = {}; + UIEditorViewportInputBridgeState m_bridgeState = {}; + UIEditorViewportInputBridgeFrame m_bridgeFrame = {}; + UIRect m_introRect = {}; + UIRect m_controlsRect = {}; + UIRect m_stateRect = {}; + UIRect m_previewRect = {}; + UIRect m_slotRect = {}; + UIEditorViewportSlotLayout m_layout = {}; + std::string m_lastResult = {}; +}; + +} // namespace + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { + return ScenarioApp().Run(hInstance, nCmdShow); +} diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index 44970007..0e10eac0 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -15,6 +15,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES test_ui_editor_panel_frame.cpp test_ui_editor_status_bar.cpp test_ui_editor_tab_strip.cpp + test_ui_editor_viewport_input_bridge.cpp test_ui_editor_viewport_slot.cpp test_ui_editor_shortcut_manager.cpp test_ui_editor_workspace_controller.cpp diff --git a/tests/UI/Editor/unit/test_ui_editor_viewport_input_bridge.cpp b/tests/UI/Editor/unit/test_ui_editor_viewport_input_bridge.cpp new file mode 100644 index 00000000..0120e67b --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_viewport_input_bridge.cpp @@ -0,0 +1,196 @@ +#include + +#include + +namespace { + +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::IsUIEditorViewportInputBridgeKeyDown; +using XCEngine::UI::Editor::IsUIEditorViewportInputBridgePointerButtonDown; +using XCEngine::UI::Editor::UIEditorViewportInputBridgeState; +using XCEngine::UI::Editor::UpdateUIEditorViewportInputBridge; + +UIInputEvent MakePointerEvent( + UIInputEventType type, + float x, + float y, + UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = UIPoint(x, y); + event.pointerButton = button; + return event; +} + +UIInputEvent MakeKeyEvent( + UIInputEventType type, + std::int32_t keyCode) { + UIInputEvent event = {}; + event.type = type; + event.keyCode = keyCode; + return event; +} + +TEST(UIEditorViewportInputBridgeTest, PointerDownInsideStartsFocusAndCaptureAndTracksLocalPosition) { + UIEditorViewportInputBridgeState state = {}; + + const auto frame = UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 280.0f), + MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.hovered); + EXPECT_TRUE(frame.focused); + EXPECT_TRUE(frame.captured); + EXPECT_TRUE(frame.focusGained); + EXPECT_TRUE(frame.captureStarted); + EXPECT_TRUE(frame.pointerPressedInside); + EXPECT_FLOAT_EQ(frame.localPointerPosition.x, 120.0f); + EXPECT_FLOAT_EQ(frame.localPointerPosition.y, 80.0f); + EXPECT_TRUE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Left)); +} + +TEST(UIEditorViewportInputBridgeTest, FirstPointerMoveDoesNotCreateSyntheticDeltaFromOrigin) { + UIEditorViewportInputBridgeState state = {}; + + const auto frame = UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 280.0f) + }); + + EXPECT_TRUE(frame.hovered); + EXPECT_FALSE(frame.pointerMoved); + EXPECT_FLOAT_EQ(frame.pointerDelta.x, 0.0f); + EXPECT_FLOAT_EQ(frame.pointerDelta.y, 0.0f); + EXPECT_FLOAT_EQ(frame.localPointerPosition.x, 120.0f); + EXPECT_FLOAT_EQ(frame.localPointerPosition.y, 80.0f); +} + +TEST(UIEditorViewportInputBridgeTest, PointerUpEndsCaptureAndOutsidePointerDownClearsFocus) { + UIEditorViewportInputBridgeState state = {}; + UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left) + }); + + auto frame = UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + MakePointerEvent(UIInputEventType::PointerButtonUp, 230.0f, 290.0f, UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.pointerReleasedInside); + EXPECT_TRUE(frame.captureEnded); + EXPECT_FALSE(frame.captured); + + frame = UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + MakePointerEvent(UIInputEventType::PointerButtonDown, 40.0f, 60.0f, UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.focusLost); + EXPECT_FALSE(frame.focused); +} + +TEST(UIEditorViewportInputBridgeTest, PointerMoveWhileCapturedKeepsDeltaEvenOutsideSurface) { + UIEditorViewportInputBridgeState state = {}; + UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left) + }); + + const auto frame = UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + MakePointerEvent(UIInputEventType::PointerMove, 60.0f, 120.0f) + }); + + EXPECT_TRUE(frame.pointerMoved); + EXPECT_FLOAT_EQ(frame.pointerDelta.x, -160.0f); + EXPECT_FLOAT_EQ(frame.pointerDelta.y, -160.0f); + EXPECT_FALSE(frame.hovered); + EXPECT_TRUE(frame.captured); + EXPECT_FLOAT_EQ(frame.localPointerPosition.x, -40.0f); + EXPECT_FLOAT_EQ(frame.localPointerPosition.y, -80.0f); +} + +TEST(UIEditorViewportInputBridgeTest, WheelAndKeyboardAreAcceptedOnlyWhileFocused) { + UIEditorViewportInputBridgeState state = {}; + + auto frame = UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + [] { + UIInputEvent wheel = {}; + wheel.type = UIInputEventType::PointerWheel; + wheel.position = UIPoint(220.0f, 280.0f); + wheel.wheelDelta = 120.0f; + return wheel; + }(), + MakeKeyEvent(UIInputEventType::KeyDown, 87) + }); + + EXPECT_FLOAT_EQ(frame.wheelDelta, 120.0f); + EXPECT_TRUE(frame.pressedKeyCodes.empty()); + + frame = UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left), + MakeKeyEvent(UIInputEventType::KeyDown, 87), + MakeKeyEvent(UIInputEventType::KeyUp, 87) + }); + + ASSERT_EQ(frame.pressedKeyCodes.size(), 1u); + ASSERT_EQ(frame.releasedKeyCodes.size(), 1u); + EXPECT_EQ(frame.pressedKeyCodes[0], 87); + EXPECT_EQ(frame.releasedKeyCodes[0], 87); + EXPECT_FALSE(IsUIEditorViewportInputBridgeKeyDown(state, 87)); +} + +TEST(UIEditorViewportInputBridgeTest, FocusLostClearsCapturedStateAndHeldKeys) { + UIEditorViewportInputBridgeState state = {}; + UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + MakePointerEvent(UIInputEventType::PointerButtonDown, 220.0f, 280.0f, UIPointerButton::Left), + MakeKeyEvent(UIInputEventType::KeyDown, 70) + }); + + UIInputEvent focusLost = {}; + focusLost.type = UIInputEventType::FocusLost; + const auto frame = + UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { focusLost }); + + EXPECT_TRUE(frame.focusLost); + EXPECT_TRUE(frame.captureEnded); + EXPECT_FALSE(state.focused); + EXPECT_FALSE(state.captured); + EXPECT_FALSE(IsUIEditorViewportInputBridgeKeyDown(state, 70)); + EXPECT_FALSE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Left)); +} + +} // namespace