Build XCEditor viewport input bridge foundation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/Types.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
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<std::int32_t> 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<std::int32_t> pressedKeyCodes = {};
|
||||
std::vector<std::int32_t> releasedKeyCodes = {};
|
||||
std::vector<std::uint32_t> 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
|
||||
221
new_editor/src/Core/UIEditorViewportInputBridge.cpp
Normal file
221
new_editor/src/Core/UIEditorViewportInputBridge.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
#include <XCEditor/Core/UIEditorViewportInputBridge.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<UIInputEvent>& 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<std::uint8_t>(~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
|
||||
@@ -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}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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$<$<CONFIG:Debug>: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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,866 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Core/UIEditorViewportInputBridge.h>
|
||||
#include <XCEditor/Widgets/UIEditorViewportSlot.h>
|
||||
#include "Host/AutoScreenshot.h"
|
||||
#include "Host/InputModifierTracker.h"
|
||||
#include "Host/NativeRenderer.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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>(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<std::int32_t>& 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<std::int32_t>(KeyCode::W))) +
|
||||
" A=" +
|
||||
BoolText(IsUIEditorViewportInputBridgeKeyDown(state, static_cast<std::int32_t>(KeyCode::A))) +
|
||||
" F=" +
|
||||
BoolText(IsUIEditorViewportInputBridgeKeyDown(state, static_cast<std::int32_t>(KeyCode::F))) +
|
||||
" Space=" +
|
||||
BoolText(IsUIEditorViewportInputBridgeKeyDown(state, static_cast<std::int32_t>(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<char>(character));
|
||||
} else {
|
||||
result += "U+" + std::to_string(character);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) {
|
||||
switch (wParam) {
|
||||
case 'A': return static_cast<std::int32_t>(KeyCode::A);
|
||||
case 'D': return static_cast<std::int32_t>(KeyCode::D);
|
||||
case 'F': return static_cast<std::int32_t>(KeyCode::F);
|
||||
case 'Q': return static_cast<std::int32_t>(KeyCode::Q);
|
||||
case 'S': return static_cast<std::int32_t>(KeyCode::S);
|
||||
case 'W': return static_cast<std::int32_t>(KeyCode::W);
|
||||
case VK_SPACE: return static_cast<std::int32_t>(KeyCode::Space);
|
||||
case VK_SHIFT: return static_cast<std::int32_t>(KeyCode::LeftShift);
|
||||
case VK_CONTROL: return static_cast<std::int32_t>(KeyCode::LeftCtrl);
|
||||
case VK_MENU: return static_cast<std::int32_t>(KeyCode::LeftAlt);
|
||||
case VK_ESCAPE: return static_cast<std::int32_t>(KeyCode::Escape);
|
||||
case VK_RETURN: return static_cast<std::int32_t>(KeyCode::Enter);
|
||||
case VK_UP: return static_cast<std::int32_t>(KeyCode::Up);
|
||||
case VK_DOWN: return static_cast<std::int32_t>(KeyCode::Down);
|
||||
case VK_LEFT: return static_cast<std::int32_t>(KeyCode::Left);
|
||||
case VK_RIGHT: return static_cast<std::int32_t>(KeyCode::Right);
|
||||
case VK_F12: return static_cast<std::int32_t>(KeyCode::F12);
|
||||
default: return static_cast<std::int32_t>(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<int>(message.wParam);
|
||||
}
|
||||
|
||||
private:
|
||||
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
if (message == WM_NCCREATE) {
|
||||
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
switch (message) {
|
||||
case WM_SIZE:
|
||||
if (app != nullptr && wParam != SIZE_MINIMIZED) {
|
||||
app->m_renderer.Resize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(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<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(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<float>(clientPoint.x), static_cast<float>(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<float>(screenPoint.x), static_cast<float>(screenPoint.y));
|
||||
event.wheelDelta = static_cast<float>(wheelDelta);
|
||||
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(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<std::uint32_t>(lParam) & 0x40000000u) != 0u;
|
||||
m_pendingEvents.push_back(event);
|
||||
}
|
||||
|
||||
void QueueCharacterEvent(WPARAM wParam) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(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<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(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<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(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<UIEditorViewportSlotToolItem> BuildToolItems() const {
|
||||
return {
|
||||
{ "mode", "Perspective", UIEditorViewportSlotToolSlot::Leading, true, true, 98.0f },
|
||||
{ "input", "Input", UIEditorViewportSlotToolSlot::Trailing, true, true, 58.0f }
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<UIEditorStatusBarSegment> 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<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((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<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
|
||||
UpdateLayoutForCurrentWindow();
|
||||
|
||||
std::vector<UIInputEvent> 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<int>(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<unsigned int>(width),
|
||||
static_cast<unsigned int>(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<UIInputEvent> m_pendingEvents = {};
|
||||
std::vector<ButtonState> 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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
196
tests/UI/Editor/unit/test_ui_editor_viewport_input_bridge.cpp
Normal file
196
tests/UI/Editor/unit/test_ui_editor_viewport_input_bridge.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Core/UIEditorViewportInputBridge.h>
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user