mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-23 11:01:04 +02:00
Keep both the CallProtocol and AccountProtocol bridges. Keep our pointer hold and catalog validation. Re-apply our JNI local-frame counting, singleton handles, class handle cache, handle-table report and thread detach release on upstream's split jnivm files.
898 lines
35 KiB
C++
898 lines
35 KiB
C++
#include "runtime/roblox_input_router.h"
|
|
|
|
#include <SDL3/SDL_events.h>
|
|
#include <SDL3/SDL_keycode.h>
|
|
#include <SDL3/SDL_mouse.h>
|
|
#include <SDL3/SDL_scancode.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "mocktail/platform/sdl_event_converter.h"
|
|
|
|
namespace mocktail {
|
|
namespace runtime {
|
|
namespace {
|
|
|
|
struct MouseMoveCall {
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
float delta_x = 0.0f;
|
|
float delta_y = 0.0f;
|
|
};
|
|
|
|
struct MouseButtonCall {
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
bool pressed = false;
|
|
int32_t button = -1;
|
|
};
|
|
|
|
struct MouseWheelCall {
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
float delta_y = 0.0f;
|
|
};
|
|
|
|
struct TouchCall {
|
|
int32_t pointer_id = -1;
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
int32_t action = -1;
|
|
int32_t width = 0;
|
|
int32_t height = 0;
|
|
};
|
|
|
|
struct KeyCall {
|
|
bool pressed = false;
|
|
int32_t scan_code = 0;
|
|
int32_t key_code = 0;
|
|
bool repeat = false;
|
|
};
|
|
|
|
struct Probe {
|
|
std::vector<MouseMoveCall> mouse_moves;
|
|
std::vector<MouseButtonCall> mouse_buttons;
|
|
std::vector<MouseWheelCall> mouse_wheels;
|
|
std::vector<TouchCall> touches;
|
|
std::vector<KeyCall> keys;
|
|
std::vector<std::string> text_operations;
|
|
std::vector<std::string> ordered_operations;
|
|
Status next_status = Status::Ok();
|
|
|
|
size_t CallCount() const {
|
|
return mouse_moves.size() + mouse_buttons.size() + mouse_wheels.size() +
|
|
touches.size() + keys.size() + text_operations.size();
|
|
}
|
|
};
|
|
|
|
class ClipboardProbe final : public platform::TextClipboard {
|
|
public:
|
|
Status ReadText(std::string* text) override {
|
|
++read_calls;
|
|
*text = value;
|
|
return Status::Ok();
|
|
}
|
|
|
|
Status WriteText(const std::string& text) override {
|
|
value = text;
|
|
return Status::Ok();
|
|
}
|
|
|
|
std::string value;
|
|
int read_calls = 0;
|
|
};
|
|
|
|
Status ConsumeStatus(Probe* probe) {
|
|
Status status = probe->next_status;
|
|
probe->next_status = Status::Ok();
|
|
return status;
|
|
}
|
|
|
|
Status MouseMove(void* context, float x, float y, float delta_x,
|
|
float delta_y) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->mouse_moves.push_back({x, y, delta_x, delta_y});
|
|
return ConsumeStatus(probe);
|
|
}
|
|
|
|
Status MouseButton(void* context, float x, float y, bool pressed,
|
|
int32_t button) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->mouse_buttons.push_back({x, y, pressed, button});
|
|
return ConsumeStatus(probe);
|
|
}
|
|
|
|
Status MouseWheel(void* context, float x, float y, float delta_y) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->mouse_wheels.push_back({x, y, delta_y});
|
|
return ConsumeStatus(probe);
|
|
}
|
|
|
|
Status Touch(void* context, int32_t pointer_id, float x, float y,
|
|
int32_t action, int32_t width, int32_t height) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->touches.push_back({pointer_id, x, y, action, width, height});
|
|
return ConsumeStatus(probe);
|
|
}
|
|
|
|
Status Key(void* context, bool pressed, int32_t scan_code, int32_t key_code,
|
|
bool repeat) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->keys.push_back({pressed, scan_code, key_code, repeat});
|
|
probe->ordered_operations.push_back(pressed ? "key-down" : "key-up");
|
|
return ConsumeStatus(probe);
|
|
}
|
|
|
|
Status SyncText(void* context, const char*, std::size_t, int32_t) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->text_operations.push_back("sync");
|
|
probe->ordered_operations.push_back("sync");
|
|
return ConsumeStatus(probe);
|
|
}
|
|
|
|
Status PassText(void* context, int64_t, const char*, std::size_t, bool,
|
|
int32_t) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->text_operations.push_back("pass");
|
|
probe->ordered_operations.push_back("pass");
|
|
return ConsumeStatus(probe);
|
|
}
|
|
|
|
Status TextHandle(void* context, int64_t) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->text_operations.push_back("handle");
|
|
probe->ordered_operations.push_back("handle");
|
|
return ConsumeStatus(probe);
|
|
}
|
|
|
|
RobloxInputSink Sink(Probe* probe) {
|
|
RobloxInputSink sink = {probe, MouseMove, MouseButton, MouseWheel,
|
|
Touch, Key, {}};
|
|
sink.text = {probe, SyncText, PassText, TextHandle, TextHandle};
|
|
return sink;
|
|
}
|
|
|
|
platform::PlatformEvent Event(platform::PlatformEventPayload payload) {
|
|
return {123, std::move(payload)};
|
|
}
|
|
|
|
class RobloxInputRouterTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
ASSERT_TRUE(router_.Activate({1280, 720, 2560, 1440, 2.0F}, true).ok());
|
|
}
|
|
|
|
Probe probe_;
|
|
RobloxInputRouter router_{Sink(&probe_)};
|
|
};
|
|
|
|
TEST(RobloxInputMappingTest, MapsMouseButtonsForNativeInput) {
|
|
EXPECT_EQ(MapSdlMouseButtonToRoblox(SDL_BUTTON_LEFT), 0);
|
|
EXPECT_EQ(MapSdlMouseButtonToRoblox(SDL_BUTTON_RIGHT), 1);
|
|
EXPECT_EQ(MapSdlMouseButtonToRoblox(SDL_BUTTON_MIDDLE), 2);
|
|
EXPECT_EQ(MapSdlMouseButtonToRoblox(SDL_BUTTON_X1), 7);
|
|
EXPECT_EQ(MapSdlMouseButtonToRoblox(SDL_BUTTON_X2), 15);
|
|
EXPECT_EQ(MapSdlMouseButtonToRoblox(42), -1);
|
|
}
|
|
|
|
TEST(RobloxInputMappingTest, MapsSdlUsbKeysToLinuxAndAndroidCodes) {
|
|
EXPECT_EQ(MapSdlKeyToAndroid(SDL_SCANCODE_W).scan_code, 17);
|
|
EXPECT_EQ(MapSdlKeyToAndroid(SDL_SCANCODE_W).key_code, 51);
|
|
EXPECT_EQ(MapSdlKeyToAndroid(SDL_SCANCODE_SPACE).scan_code, 57);
|
|
EXPECT_EQ(MapSdlKeyToAndroid(SDL_SCANCODE_SPACE).key_code, 62);
|
|
EXPECT_EQ(MapSdlKeyToAndroid(SDL_SCANCODE_LSHIFT).scan_code, 42);
|
|
EXPECT_EQ(MapSdlKeyToAndroid(SDL_SCANCODE_LSHIFT).key_code, 59);
|
|
EXPECT_FALSE(MapSdlKeyToAndroid(SDL_SCANCODE_UNKNOWN).valid());
|
|
}
|
|
|
|
TEST(SurfaceCoordinateTransformTest,
|
|
KeepsAndroidDensityAndDrawableScaleAsInverseOperations) {
|
|
const platform::SurfaceCoordinateTransform retina = {1280, 720, 2560, 1440,
|
|
2.0F};
|
|
ASSERT_TRUE(retina.valid());
|
|
EXPECT_EQ(retina.rounded_guest_width(), 1280);
|
|
EXPECT_EQ(retina.rounded_guest_height(), 720);
|
|
EXPECT_FLOAT_EQ(retina.HostLogicalToGuestX(321.0F), 321.0F);
|
|
EXPECT_FLOAT_EQ(retina.GuestToHostLogicalY(123.0F), 123.0F);
|
|
|
|
const platform::SurfaceCoordinateTransform x11 = {1624, 811, 1624, 811,
|
|
1.145833F};
|
|
const float host_x = x11.GuestToHostLogicalX(943.0F);
|
|
const float host_y = x11.GuestToHostLogicalY(527.0F);
|
|
EXPECT_NEAR(host_x, 1080.5F, 0.1F);
|
|
EXPECT_NEAR(host_y, 603.85F, 0.1F);
|
|
EXPECT_NEAR(x11.HostLogicalToGuestX(host_x), 943.0F, 0.01F);
|
|
EXPECT_NEAR(x11.HostLogicalToGuestY(host_y), 527.0F, 0.01F);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, RoutesMouseMotionButtonAndVerticalWheel) {
|
|
EXPECT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{
|
|
100.0f, 80.0f, 5.0f, -3.0f, 0}))
|
|
.dispatched());
|
|
EXPECT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseButtonEvent{
|
|
true, SDL_BUTTON_RIGHT, 1, 100.0f, 80.0f}))
|
|
.dispatched());
|
|
EXPECT_TRUE(router_
|
|
.HandleEvent(Event(
|
|
platform::MouseWheelEvent{0.0f, -2.0f, -4.0f, -5.0f}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_moves.size(), 1U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].x, 100.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].y, 80.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].delta_x, 5.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].delta_y, -3.0f);
|
|
ASSERT_EQ(probe_.mouse_buttons.size(), 1U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons[0].x, 100.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons[0].y, 80.0f);
|
|
EXPECT_EQ(probe_.mouse_buttons[0].button, 1);
|
|
ASSERT_EQ(probe_.mouse_wheels.size(), 1U);
|
|
// A wheel event without a usable position keeps the pointer where it was.
|
|
EXPECT_FLOAT_EQ(probe_.mouse_wheels[0].x, 100.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_wheels[0].y, 80.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_wheels[0].delta_y, -2.0f);
|
|
}
|
|
|
|
|
|
TEST_F(RobloxInputRouterTest, HoldsPointerPositionDuringRelativeMotion) {
|
|
// Relative-mode motion reports zero absolute coordinates. The pointer stays
|
|
// where it was captured while the deltas pass through, so a long camera
|
|
// turn neither walks the reported position to the viewport edge nor
|
|
// swallows the reversal. The next absolute event moves the pointer again.
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{640.0f, 360.0f, 0.0f,
|
|
0.0f, 0}))
|
|
.dispatched());
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, 2000.0f,
|
|
0.0f, 0}))
|
|
.dispatched());
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, -100.0f,
|
|
40.0f, 0}))
|
|
.dispatched());
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{200.0f, 100.0f, 3.0f,
|
|
3.0f, 0}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_moves.size(), 4U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].x, 640.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].y, 360.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].delta_x, 2000.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].x, 640.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].y, 360.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].delta_x, -100.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].delta_y, 40.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[3].x, 200.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[3].y, 100.0f);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, HoldsPointerWhenRelativeMotionCarriesAPosition) {
|
|
// Wayland reports a drifting absolute position during relative mode instead
|
|
// of zeroes, and it can leave the window. The capture flag decides, not the
|
|
// coordinates, so the held position survives the drift and the reversal.
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{
|
|
640.0f, 360.0f, 0.0f, 0.0f, 0, false}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{
|
|
1840.0f, 470.0f, 1200.0f, 110.0f, 0, true}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{
|
|
-510.0f, 430.0f, -2350.0f, -40.0f, 0, true}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_moves.size(), 3U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].x, 640.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].y, 360.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].delta_x, 1200.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].x, 640.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].y, 360.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].delta_x, -2350.0f);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, HoldsPointerWhenRelativeButtonCarriesAPosition) {
|
|
// The release that ends a Wayland camera drag carries the drifted position
|
|
// too. It must not move the pointer Roblox draws.
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{
|
|
640.0f, 360.0f, 0.0f, 0.0f, 0, false}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseButtonEvent{
|
|
false, SDL_BUTTON_RIGHT, 1, 1840.0f, 470.0f, true}))
|
|
.dispatched());
|
|
|
|
ASSERT_FALSE(probe_.mouse_buttons.empty());
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons.back().x, 640.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons.back().y, 360.0f);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, HoldsPointerWhenRelativeWheelCarriesAPosition) {
|
|
// Zooming the camera mid-drag scrolls while capture is held. Wayland stamps
|
|
// the drifted position on the wheel event too, so the capture flag decides
|
|
// there as well.
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{
|
|
640.0f, 360.0f, 0.0f, 0.0f, 0, false}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseWheelEvent{
|
|
0.0f, -1.0f, 1840.0f, 470.0f, true}))
|
|
.dispatched());
|
|
|
|
ASSERT_FALSE(probe_.mouse_wheels.empty());
|
|
EXPECT_FLOAT_EQ(probe_.mouse_wheels.back().x, 640.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_wheels.back().y, 360.0f);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest,
|
|
AnchorsUnknownPointerAtViewportCenterForRelativeMotion) {
|
|
// No absolute position has been seen yet, so the held position is the
|
|
// viewport center rather than the top-left corner.
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, 25.0f,
|
|
-10.0f, 0}))
|
|
.dispatched());
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, 300.0f,
|
|
0.0f, 0}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_moves.size(), 2U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].x, 639.5f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].y, 359.5f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].delta_x, 25.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].delta_y, -10.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].x, 639.5f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].y, 359.5f);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, ClampsHeldPointerToViewportAfterResize) {
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{1200.0f, 700.0f, 0.0f,
|
|
0.0f, 0}))
|
|
.dispatched());
|
|
ASSERT_EQ(router_
|
|
.HandleEvent(Event(
|
|
platform::WindowResizedEvent{800, 600, 800, 600, 1.0F}))
|
|
.state,
|
|
RobloxInputDispatchState::kStateUpdated);
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, 50.0f,
|
|
0.0f, 0}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_moves.size(), 2U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].x, 799.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].y, 599.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].delta_x, 50.0f);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest,
|
|
KeepsHeldPointerAcrossZeroCoordinateWheelAndButtonEvents) {
|
|
// While the pointer is captured, wheel and button events also arrive with
|
|
// zero coordinates. None of them may move the held position.
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{640.0f, 360.0f, 0.0f,
|
|
0.0f, 0}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(
|
|
platform::MouseWheelEvent{0.0f, -1.0f, 0.0f, 0.0f}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseButtonEvent{
|
|
true, SDL_BUTTON_LEFT, 1, 0.0f, 0.0f}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseButtonEvent{
|
|
false, SDL_BUTTON_LEFT, 1, 0.0f, 0.0f}))
|
|
.dispatched());
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, -500.0f,
|
|
0.0f, 0}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_wheels.size(), 1U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_wheels[0].x, 640.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_wheels[0].y, 360.0f);
|
|
ASSERT_EQ(probe_.mouse_buttons.size(), 2U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons[0].x, 640.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons[0].y, 360.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons[1].x, 640.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons[1].y, 360.0f);
|
|
ASSERT_EQ(probe_.mouse_moves.size(), 2U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].x, 640.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].y, 360.0f);
|
|
}
|
|
|
|
// A fractionally scaled X11 desktop: the buffer is 1:1 with logical units
|
|
// while the display scale is not, which is the case where the guest-surface
|
|
// scale actually changes a look delta.
|
|
constexpr platform::WindowResizedEvent kFractionalScalingViewport{
|
|
1624, 811, 1624, 811, 1.145833F};
|
|
|
|
TEST_F(RobloxInputRouterTest, ScalesLookDeltasUnlessRawMouseIsEnabled) {
|
|
ASSERT_EQ(router_.HandleEvent(Event(kFractionalScalingViewport)).state,
|
|
RobloxInputDispatchState::kStateUpdated);
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, 10.0f,
|
|
-6.0f, 0}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_moves.size(), 1U);
|
|
EXPECT_NEAR(probe_.mouse_moves[0].delta_x, 8.727f, 0.01f);
|
|
EXPECT_NEAR(probe_.mouse_moves[0].delta_y, -5.236f, 0.01f);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, RawMouseSendsLookDeltasUnscaled) {
|
|
ASSERT_EQ(router_.HandleEvent(Event(kFractionalScalingViewport)).state,
|
|
RobloxInputDispatchState::kStateUpdated);
|
|
router_.SetRawMouseEnabled(true);
|
|
ASSERT_TRUE(
|
|
router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, 10.0f,
|
|
-6.0f, 0}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_moves.size(), 1U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].delta_x, 10.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].delta_y, -6.0f);
|
|
// The reported position stays in guest units whatever the delta carries:
|
|
// with no absolute position seen, it is the guest viewport center.
|
|
const platform::SurfaceCoordinateTransform transform = {1624, 811, 1624, 811,
|
|
1.145833F};
|
|
EXPECT_NEAR(probe_.mouse_moves[0].x, (transform.guest_width() - 1.0F) / 2.0F,
|
|
0.01f);
|
|
EXPECT_NEAR(probe_.mouse_moves[0].y,
|
|
(transform.guest_height() - 1.0F) / 2.0F, 0.01f);
|
|
}
|
|
TEST_F(RobloxInputRouterTest, MiddleClickUsesMouseButton3OnPressAndRelease) {
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseButtonEvent{
|
|
true, SDL_BUTTON_MIDDLE, 1, 100.0f, 80.0f}))
|
|
.dispatched());
|
|
EXPECT_EQ(router_.Snapshot().active_mouse_buttons, 1U);
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseButtonEvent{
|
|
false, SDL_BUTTON_MIDDLE, 1, 100.0f, 80.0f}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_buttons.size(), 2U);
|
|
EXPECT_TRUE(probe_.mouse_buttons[0].pressed);
|
|
EXPECT_EQ(probe_.mouse_buttons[0].button, 2);
|
|
EXPECT_FALSE(probe_.mouse_buttons[1].pressed);
|
|
EXPECT_EQ(probe_.mouse_buttons[1].button, 2);
|
|
EXPECT_EQ(router_.Snapshot().active_mouse_buttons, 0U);
|
|
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest,
|
|
ScalesMouseCoordinatesAfterDisplayScaleChange) {
|
|
const RobloxInputDispatchResult resize = router_.HandleEvent(
|
|
Event(platform::WindowResizedEvent{1000, 800, 1250, 1000, 1.25F}));
|
|
ASSERT_EQ(resize.state, RobloxInputDispatchState::kStateUpdated);
|
|
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{
|
|
400.0f, 240.0f, 8.0f, -4.0f, 0}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseButtonEvent{
|
|
true, SDL_BUTTON_LEFT, 1, 300.0f, 200.0f}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(
|
|
platform::MouseWheelEvent{0.0f, 2.0f, 600.0f, 400.0f}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_moves.size(), 1U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].x, 400.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].y, 240.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].delta_x, 8.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].delta_y, -4.0f);
|
|
|
|
ASSERT_EQ(probe_.mouse_buttons.size(), 1U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons[0].x, 300.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons[0].y, 200.0f);
|
|
|
|
ASSERT_EQ(probe_.mouse_wheels.size(), 1U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_wheels[0].x, 600.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_wheels[0].y, 400.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_wheels[0].delta_y, 2.0f);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest,
|
|
DividesPhysicalPointerCoordinatesByAndroidDensity) {
|
|
const RobloxInputDispatchResult resize = router_.HandleEvent(
|
|
Event(platform::WindowResizedEvent{1624, 811, 1624, 811, 1.145833F}));
|
|
ASSERT_EQ(resize.state, RobloxInputDispatchState::kStateUpdated);
|
|
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseMotionEvent{
|
|
1080.5F, 604.0F, 11.45833F, -5.729165F, 0}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.mouse_moves.size(), 1U);
|
|
EXPECT_NEAR(probe_.mouse_moves[0].x, 943.0F, 0.1F);
|
|
EXPECT_NEAR(probe_.mouse_moves[0].y, 527.14F, 0.1F);
|
|
EXPECT_NEAR(probe_.mouse_moves[0].delta_x, 10.0F, 0.01F);
|
|
EXPECT_NEAR(probe_.mouse_moves[0].delta_y, -5.0F, 0.01F);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, ScalesNormalizedTouchAndKeepsStablePointerIds) {
|
|
EXPECT_TRUE(router_
|
|
.HandleEvent(Event(platform::TouchEvent{
|
|
platform::TouchEvent::Action::kDown, 10, 1001, 0.25f,
|
|
0.5f, 0.0f, 0.0f, 1.0f}))
|
|
.dispatched());
|
|
EXPECT_TRUE(router_
|
|
.HandleEvent(Event(platform::TouchEvent{
|
|
platform::TouchEvent::Action::kDown, 10, 9000000000LL,
|
|
0.75f, 0.25f, 0.0f, 0.0f, 1.0f}))
|
|
.dispatched());
|
|
EXPECT_TRUE(router_
|
|
.HandleEvent(Event(platform::TouchEvent{
|
|
platform::TouchEvent::Action::kMove, 10, 1001, 0.5f, 1.0f,
|
|
0.25f, 0.5f, 1.0f}))
|
|
.dispatched());
|
|
EXPECT_TRUE(router_
|
|
.HandleEvent(Event(platform::TouchEvent{
|
|
platform::TouchEvent::Action::kUp, 10, 1001, 0.5f, 1.0f,
|
|
0.0f, 0.0f, 0.0f}))
|
|
.dispatched());
|
|
EXPECT_TRUE(router_
|
|
.HandleEvent(Event(platform::TouchEvent{
|
|
platform::TouchEvent::Action::kDown, 20, 2002, 0.1f, 0.1f,
|
|
0.0f, 0.0f, 1.0f}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.touches.size(), 5U);
|
|
EXPECT_EQ(probe_.touches[0].pointer_id, 0);
|
|
EXPECT_EQ(probe_.touches[0].action, 0);
|
|
EXPECT_FLOAT_EQ(probe_.touches[0].x, 320.0f);
|
|
EXPECT_FLOAT_EQ(probe_.touches[0].y, 360.0f);
|
|
EXPECT_EQ(probe_.touches[0].width, 1280);
|
|
EXPECT_EQ(probe_.touches[0].height, 720);
|
|
EXPECT_EQ(probe_.touches[1].pointer_id, 1);
|
|
EXPECT_EQ(probe_.touches[2].pointer_id, 0);
|
|
EXPECT_EQ(probe_.touches[2].action, 1);
|
|
EXPECT_EQ(probe_.touches[3].pointer_id, 0);
|
|
EXPECT_EQ(probe_.touches[3].action, 2);
|
|
EXPECT_EQ(probe_.touches[4].pointer_id, 0);
|
|
EXPECT_EQ(probe_.touches[4].action, 0);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, ResizeChangesTouchScaleAndNativeViewport) {
|
|
RobloxInputDispatchResult resize = router_.HandleEvent(
|
|
Event(platform::WindowResizedEvent{640, 360, 1920, 1080, 3.0F}));
|
|
EXPECT_EQ(resize.state, RobloxInputDispatchState::kStateUpdated);
|
|
|
|
EXPECT_TRUE(router_
|
|
.HandleEvent(Event(
|
|
platform::TouchEvent{platform::TouchEvent::Action::kDown,
|
|
1, 2, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f}))
|
|
.dispatched());
|
|
|
|
ASSERT_EQ(probe_.touches.size(), 1U);
|
|
EXPECT_FLOAT_EQ(probe_.touches[0].x, 320.0f);
|
|
EXPECT_FLOAT_EQ(probe_.touches[0].y, 180.0f);
|
|
EXPECT_EQ(probe_.touches[0].width, 640);
|
|
EXPECT_EQ(probe_.touches[0].height, 360);
|
|
EXPECT_EQ(router_.Snapshot().viewport.pixel_width, 1920);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, RoutesMappedHardwareKeyAndRepeat) {
|
|
RobloxInputDispatchResult down = router_.HandleEvent(
|
|
Event(platform::KeyEvent{true, false, SDL_SCANCODE_W, SDLK_W, 0}));
|
|
RobloxInputDispatchResult repeat = router_.HandleEvent(
|
|
Event(platform::KeyEvent{true, true, SDL_SCANCODE_W, SDLK_W, 0}));
|
|
RobloxInputDispatchResult up = router_.HandleEvent(
|
|
Event(platform::KeyEvent{false, false, SDL_SCANCODE_W, SDLK_W, 0}));
|
|
|
|
EXPECT_TRUE(down.dispatched());
|
|
EXPECT_TRUE(repeat.dispatched());
|
|
EXPECT_TRUE(up.dispatched());
|
|
ASSERT_EQ(probe_.keys.size(), 3U);
|
|
EXPECT_EQ(probe_.keys[0].scan_code, 17);
|
|
EXPECT_EQ(probe_.keys[0].key_code, 51);
|
|
EXPECT_FALSE(probe_.keys[0].repeat);
|
|
EXPECT_TRUE(probe_.keys[1].repeat);
|
|
EXPECT_FALSE(probe_.keys[2].pressed);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, FocusLossReleasesAllPressedInputs) {
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::MouseButtonEvent{
|
|
true, SDL_BUTTON_LEFT, 1, 12.0f, 34.0f}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(
|
|
platform::TouchEvent{platform::TouchEvent::Action::kDown,
|
|
7, 8, 0.1f, 0.2f, 0.0f, 0.0f, 1.0f}))
|
|
.dispatched());
|
|
ASSERT_TRUE(router_
|
|
.HandleEvent(Event(platform::KeyEvent{
|
|
true, false, SDL_SCANCODE_SPACE, SDLK_SPACE, 0}))
|
|
.dispatched());
|
|
|
|
RobloxInputDispatchResult focus =
|
|
router_.HandleEvent(Event(platform::WindowFocusEvent{false}));
|
|
EXPECT_EQ(focus.state, RobloxInputDispatchState::kStateUpdated);
|
|
ASSERT_EQ(probe_.mouse_buttons.size(), 2U);
|
|
EXPECT_FALSE(probe_.mouse_buttons.back().pressed);
|
|
ASSERT_EQ(probe_.touches.size(), 2U);
|
|
EXPECT_EQ(probe_.touches.back().action, 2);
|
|
ASSERT_EQ(probe_.keys.size(), 2U);
|
|
EXPECT_FALSE(probe_.keys.back().pressed);
|
|
EXPECT_EQ(router_.Snapshot().active_mouse_buttons, 0U);
|
|
EXPECT_EQ(router_.Snapshot().active_touches, 0U);
|
|
EXPECT_EQ(router_.Snapshot().active_keys, 0U);
|
|
|
|
const auto motion = router_.HandleEvent(
|
|
Event(platform::MouseMotionEvent{1.0f, 2.0f, 1.0f, 1.0f, 0}));
|
|
EXPECT_EQ(motion.state, RobloxInputDispatchState::kDispatched);
|
|
EXPECT_TRUE(router_.Snapshot().focused);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, TextFailsClosedWithoutTextboxHandle) {
|
|
RobloxInputDispatchResult result =
|
|
router_.HandleEvent(Event(platform::TextInputEvent{"hello"}));
|
|
|
|
EXPECT_EQ(result.state, RobloxInputDispatchState::kIgnoredUnsupported);
|
|
EXPECT_EQ(result.kind, RobloxInputEventKind::kText);
|
|
EXPECT_EQ(result.status.code(), StatusCode::kUnsupported);
|
|
EXPECT_EQ(router_.Snapshot().ignored_text_events, 1U);
|
|
EXPECT_EQ(probe_.CallCount(), 0U);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, FocusedTextSuppressesDuplicateHardwareEditKeys) {
|
|
ASSERT_TRUE(router_.BeginTextFocusSession({42, 1, "a", false, false}).ok());
|
|
EXPECT_TRUE(
|
|
router_.HandleEvent(Event(platform::TextInputEvent{"b"})).dispatched());
|
|
EXPECT_TRUE(router_
|
|
.HandleEvent(Event(platform::KeyEvent{
|
|
true, false, SDL_SCANCODE_BACKSPACE, SDLK_BACKSPACE, 0}))
|
|
.dispatched());
|
|
const auto key_up = router_.HandleEvent(Event(platform::KeyEvent{
|
|
false, false, SDL_SCANCODE_BACKSPACE, SDLK_BACKSPACE, 0}));
|
|
EXPECT_EQ(key_up.state, RobloxInputDispatchState::kStateUpdated);
|
|
EXPECT_TRUE(probe_.keys.empty());
|
|
EXPECT_EQ(probe_.text_operations,
|
|
(std::vector<std::string>{"sync", "pass", "sync", "pass"}));
|
|
EXPECT_EQ(router_.Snapshot().text_events, 2u);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, ReportsHostTextFocusCompletion) {
|
|
for (const auto scancode : {SDL_SCANCODE_RETURN, SDL_SCANCODE_ESCAPE}) {
|
|
ASSERT_TRUE(router_.BeginTextFocusSession({42, 3, "", false, false}).ok());
|
|
EXPECT_EQ(router_.Snapshot().text_focus_generation, 3U);
|
|
ASSERT_TRUE(router_.HandleEvent(Event(platform::KeyEvent{
|
|
true, false, static_cast<uint32_t>(scancode), 0, 0})).status.ok());
|
|
EXPECT_EQ(router_.Snapshot().text_focus_generation, 0U);
|
|
}
|
|
ASSERT_TRUE(router_.BeginTextFocusSession({43, 4, "", true, false}).ok());
|
|
ASSERT_TRUE(router_.HandleEvent(Event(platform::KeyEvent{
|
|
true, false, SDL_SCANCODE_RETURN, SDLK_RETURN, 0})).status.ok());
|
|
EXPECT_EQ(router_.Snapshot().text_focus_generation, 4U);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, MouseClicksKeepTextFocusUntilEngineReleasesIt) {
|
|
RobloxTextFocusSession session{42, 1, "draft", false, false};
|
|
session.area_x = 40;
|
|
session.area_y = 300;
|
|
session.area_width = 400;
|
|
session.area_height = 30;
|
|
ASSERT_TRUE(router_.BeginTextFocusSession(session).ok());
|
|
|
|
for (const auto button : {SDL_BUTTON_LEFT, SDL_BUTTON_MIDDLE,
|
|
SDL_BUTTON_RIGHT}) {
|
|
SCOPED_TRACE(button);
|
|
for (const bool pressed : {true, false}) {
|
|
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseButtonEvent{
|
|
pressed, static_cast<uint8_t>(button), 1, 100.0F, 310.0F}))
|
|
.dispatched());
|
|
EXPECT_EQ(router_.Snapshot().text_focus_generation, 1U);
|
|
}
|
|
}
|
|
EXPECT_TRUE(probe_.text_operations.empty());
|
|
EXPECT_EQ(probe_.mouse_buttons.size(), 6U);
|
|
EXPECT_TRUE(router_.HandleEvent(Event(platform::TextInputEvent{" next"}))
|
|
.dispatched());
|
|
EXPECT_EQ(probe_.text_operations,
|
|
(std::vector<std::string>{"sync", "pass"}));
|
|
|
|
ASSERT_TRUE(router_.EndTextFocusSession(42, 1, false).ok());
|
|
EXPECT_EQ(router_.Snapshot().text_focus_generation, 0U);
|
|
EXPECT_FALSE(router_.HandleEvent(Event(platform::TextInputEvent{"late"}))
|
|
.dispatched());
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, MouseClickRespectsManualTextFocusRelease) {
|
|
ASSERT_TRUE(router_.BeginTextFocusSession({42, 1, "draft", true, false}).ok());
|
|
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseButtonEvent{
|
|
true, SDL_BUTTON_LEFT, 1, 900.0F, 600.0F})).dispatched());
|
|
EXPECT_TRUE(probe_.text_operations.empty());
|
|
EXPECT_EQ(router_.Snapshot().text_focus_generation, 1U);
|
|
EXPECT_TRUE(router_.HandleEvent(Event(platform::TextInputEvent{" next"}))
|
|
.dispatched());
|
|
}
|
|
|
|
TEST(RobloxInputRouterClipboardTest,
|
|
CtrlVPastesWithoutForwardingTheLetterKeyToRoblox) {
|
|
Probe probe;
|
|
ClipboardProbe clipboard;
|
|
clipboard.value = u8"clipboard🙂";
|
|
RobloxInputRouter router(Sink(&probe), {}, &clipboard);
|
|
ASSERT_TRUE(router.Activate({1280, 720, 2560, 1440}, true).ok());
|
|
ASSERT_TRUE(router.BeginTextFocusSession({42, 1, "old", false, false}).ok());
|
|
SDL_Event source{};
|
|
source.type = SDL_EVENT_KEY_DOWN;
|
|
source.key.down = true;
|
|
source.key.scancode = SDL_SCANCODE_V;
|
|
source.key.key = SDLK_V;
|
|
source.key.mod = SDL_KMOD_CTRL;
|
|
platform::PlatformEvent converted;
|
|
auto* non_owning_window = reinterpret_cast<SDL_Window*>(0x1);
|
|
ASSERT_TRUE(
|
|
platform::ConvertSdlEvent(non_owning_window, source, &converted));
|
|
const RobloxInputDispatchResult down = router.HandleEvent(converted);
|
|
|
|
source.type = SDL_EVENT_KEY_UP;
|
|
source.key.down = false;
|
|
source.key.mod = SDL_KMOD_NONE;
|
|
ASSERT_TRUE(
|
|
platform::ConvertSdlEvent(non_owning_window, source, &converted));
|
|
const RobloxInputDispatchResult up = router.HandleEvent(converted);
|
|
|
|
EXPECT_TRUE(down.dispatched());
|
|
EXPECT_EQ(down.kind, RobloxInputEventKind::kText);
|
|
EXPECT_EQ(up.state, RobloxInputDispatchState::kStateUpdated);
|
|
EXPECT_EQ(clipboard.read_calls, 1);
|
|
EXPECT_TRUE(probe.keys.empty());
|
|
EXPECT_EQ(probe.text_operations,
|
|
(std::vector<std::string>{"sync", "pass"}));
|
|
EXPECT_EQ(router.Snapshot().text_events, 1u);
|
|
EXPECT_EQ(router.Snapshot().keyboard_events, 0u);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest,
|
|
FocusedTextRoutesAsciiKeyPairAndCommittedTextInSdlOrder) {
|
|
ASSERT_TRUE(router_.BeginTextFocusSession({42, 1, "", false, false}).ok());
|
|
|
|
const RobloxInputDispatchResult down = router_.HandleEvent(
|
|
Event(platform::KeyEvent{true, false, SDL_SCANCODE_A, SDLK_A, 0}));
|
|
const RobloxInputDispatchResult text =
|
|
router_.HandleEvent(Event(platform::TextInputEvent{"a"}));
|
|
const RobloxInputDispatchResult up = router_.HandleEvent(
|
|
Event(platform::KeyEvent{false, false, SDL_SCANCODE_A, SDLK_A, 0}));
|
|
|
|
EXPECT_TRUE(down.dispatched());
|
|
EXPECT_EQ(down.kind, RobloxInputEventKind::kKeyboard);
|
|
EXPECT_TRUE(text.dispatched());
|
|
EXPECT_EQ(text.kind, RobloxInputEventKind::kText);
|
|
EXPECT_TRUE(up.dispatched());
|
|
EXPECT_EQ(up.kind, RobloxInputEventKind::kKeyboard);
|
|
ASSERT_EQ(probe_.keys.size(), 2u);
|
|
EXPECT_TRUE(probe_.keys[0].pressed);
|
|
EXPECT_EQ(probe_.keys[0].scan_code, 30);
|
|
EXPECT_EQ(probe_.keys[0].key_code, 29);
|
|
EXPECT_FALSE(probe_.keys[0].repeat);
|
|
EXPECT_FALSE(probe_.keys[1].pressed);
|
|
EXPECT_EQ(probe_.keys[1].scan_code, 30);
|
|
EXPECT_EQ(probe_.keys[1].key_code, 29);
|
|
EXPECT_FALSE(probe_.keys[1].repeat);
|
|
EXPECT_EQ(probe_.ordered_operations,
|
|
(std::vector<std::string>{"key-down", "sync", "pass", "key-up"}));
|
|
const RobloxInputSnapshot snapshot = router_.Snapshot();
|
|
EXPECT_EQ(snapshot.keyboard_events, 2u);
|
|
EXPECT_EQ(snapshot.text_events, 1u);
|
|
EXPECT_EQ(snapshot.ignored_text_events, 0u);
|
|
EXPECT_EQ(snapshot.active_keys, 0u);
|
|
EXPECT_EQ(snapshot.native_errors, 0u);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, FocusedTextEditingPreeditStreamsToNative) {
|
|
ASSERT_TRUE(router_.BeginTextFocusSession({42, 1, "", false, false}).ok());
|
|
|
|
const RobloxInputDispatchResult result =
|
|
router_.HandleEvent(Event(platform::TextEditingEvent{"preedit", 0, 7}));
|
|
|
|
EXPECT_EQ(result.state, RobloxInputDispatchState::kDispatched);
|
|
EXPECT_EQ(result.kind, RobloxInputEventKind::kText);
|
|
EXPECT_TRUE(result.status.ok());
|
|
EXPECT_TRUE(probe_.keys.empty());
|
|
EXPECT_EQ(probe_.text_operations, (std::vector<std::string>{"sync", "pass"}));
|
|
EXPECT_EQ(probe_.ordered_operations,
|
|
(std::vector<std::string>{"sync", "pass"}));
|
|
const RobloxInputSnapshot snapshot = router_.Snapshot();
|
|
EXPECT_EQ(snapshot.keyboard_events, 0u);
|
|
EXPECT_EQ(snapshot.text_events, 1u);
|
|
EXPECT_EQ(snapshot.ignored_text_events, 0u);
|
|
EXPECT_EQ(snapshot.active_keys, 0u);
|
|
EXPECT_EQ(snapshot.native_errors, 0u);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, WindowFocusLossReleasesFocusedTextbox) {
|
|
ASSERT_TRUE(
|
|
router_.BeginTextFocusSession({42, 1, "secret", false, false}).ok());
|
|
const auto result =
|
|
router_.HandleEvent(Event(platform::WindowFocusEvent{false}));
|
|
EXPECT_EQ(result.state, RobloxInputDispatchState::kStateUpdated);
|
|
EXPECT_EQ(probe_.text_operations, (std::vector<std::string>{"handle"}));
|
|
const auto text = router_.HandleEvent(Event(platform::TextInputEvent{"x"}));
|
|
EXPECT_EQ(text.state, RobloxInputDispatchState::kIgnoredUnsupported);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, DeactivatePreventsAllLaterNativeCalls) {
|
|
ASSERT_TRUE(router_.Deactivate().ok());
|
|
const size_t before = probe_.CallCount();
|
|
|
|
RobloxInputDispatchResult result = router_.HandleEvent(Event(
|
|
platform::MouseButtonEvent{true, SDL_BUTTON_LEFT, 1, 100.0f, 100.0f}));
|
|
|
|
EXPECT_EQ(result.state, RobloxInputDispatchState::kIgnoredInactive);
|
|
EXPECT_EQ(probe_.CallCount(), before);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest,
|
|
ConvertedSdlClickTraversesTheProductionTypedPath) {
|
|
SDL_Event source{};
|
|
source.type = SDL_EVENT_MOUSE_BUTTON_DOWN;
|
|
source.button.down = true;
|
|
source.button.button = SDL_BUTTON_LEFT;
|
|
source.button.clicks = 1;
|
|
source.button.x = 321.0f;
|
|
source.button.y = 123.0f;
|
|
platform::PlatformEvent converted;
|
|
auto* non_owning_window = reinterpret_cast<SDL_Window*>(0x1);
|
|
|
|
ASSERT_TRUE(platform::ConvertSdlEvent(non_owning_window, source, &converted));
|
|
RobloxInputDispatchResult result = router_.HandleEvent(converted);
|
|
|
|
ASSERT_TRUE(result.dispatched());
|
|
ASSERT_EQ(probe_.mouse_buttons.size(), 1U);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons[0].x, 321.0f);
|
|
EXPECT_FLOAT_EQ(probe_.mouse_buttons[0].y, 123.0f);
|
|
EXPECT_TRUE(probe_.mouse_buttons[0].pressed);
|
|
EXPECT_EQ(probe_.mouse_buttons[0].button, 0);
|
|
}
|
|
|
|
TEST_F(RobloxInputRouterTest, NativeFailuresAreObservableAndCounted) {
|
|
probe_.next_status =
|
|
Status::Error(StatusCode::kPlatformError, "injected failure");
|
|
|
|
RobloxInputDispatchResult result = router_.HandleEvent(
|
|
Event(platform::MouseMotionEvent{10.0f, 20.0f, 1.0f, 2.0f, 0}));
|
|
|
|
EXPECT_EQ(result.state, RobloxInputDispatchState::kNativeError);
|
|
EXPECT_EQ(result.status.code(), StatusCode::kPlatformError);
|
|
EXPECT_EQ(router_.Snapshot().native_errors, 1U);
|
|
EXPECT_EQ(router_.Snapshot().mouse_events, 0U);
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace runtime
|
|
} // namespace mocktail
|