Fix captured mouse input

Refs; #135
This commit is contained in:
komaruworld committed 2026-10-03 14:28:12 +03:00
1 parent 1ba2941e7f
commit bbfd603e17
8 files changed
+131 -20

No files matched your search

@@ -152,6 +152,7 @@ struct MouseMotionEvent {
float delta_x = 0.0f;
float delta_y = 0.0f;
std::uint32_t buttons = 0;
bool relative_mode = false;
};
struct MouseButtonEvent {
@@ -160,6 +161,7 @@ struct MouseButtonEvent {
std::uint8_t clicks = 0;
float x = 0.0f;
float y = 0.0f;
bool relative_mode = false;
};
struct MouseWheelEvent {
@@ -167,6 +169,7 @@ struct MouseWheelEvent {
float delta_y = 0.0f;
float mouse_x = 0.0f;
float mouse_y = 0.0f;
bool relative_mode = false;
};
struct TouchEvent {
@@ -14,7 +14,8 @@ namespace platform {
// Returns false for invalid arguments and SDL events outside the platform
// contract.
bool ConvertSdlEvent(SDL_Window* window, const SDL_Event& source,
PlatformEvent* destination);
PlatformEvent* destination,
bool relative_mouse_mode = false);
} // namespace platform
} // namespace mocktail
+5 -4
View File
@@ -6,7 +6,7 @@ namespace mocktail {
namespace platform {
bool ConvertSdlEvent(SDL_Window* window, const SDL_Event& source,
PlatformEvent* destination) {
PlatformEvent* destination, bool relative_mouse_mode) {
if (window == nullptr || destination == nullptr) {
return false;
}
@@ -60,20 +60,21 @@ bool ConvertSdlEvent(SDL_Window* window, const SDL_Event& source,
case SDL_EVENT_MOUSE_MOTION:
destination->payload = MouseMotionEvent{
source.motion.x, source.motion.y, source.motion.xrel,
source.motion.yrel, static_cast<std::uint32_t>(source.motion.state)};
source.motion.yrel, static_cast<std::uint32_t>(source.motion.state),
relative_mouse_mode};
return true;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
destination->payload = MouseButtonEvent{
source.button.down, source.button.button, source.button.clicks,
source.button.x, source.button.y};
source.button.x, source.button.y, relative_mouse_mode};
return true;
case SDL_EVENT_MOUSE_WHEEL: {
// SDL already applies the host's natural-scrolling preference to x/y.
// Undoing SDL_MOUSEWHEEL_FLIPPED here would ignore the OS setting.
destination->payload = MouseWheelEvent{
source.wheel.x, source.wheel.y, source.wheel.mouse_x,
source.wheel.mouse_y};
source.wheel.mouse_y, relative_mouse_mode};
return true;
}
case SDL_EVENT_FINGER_DOWN:
+2 -1
View File
@@ -119,7 +119,8 @@ class SdlPlatformRuntime final : public PlatformRuntime {
*has_event = false;
SDL_Event sdl_event;
while (SDL_PollEvent(&sdl_event)) {
if (ConvertSdlEvent(window_, sdl_event, event)) {
if (ConvertSdlEvent(window_, sdl_event, event,
SDL_GetWindowRelativeMouseMode(window_))) {
*has_event = true;
return Status::Ok();
}
+15 -12
View File
@@ -545,8 +545,8 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseMotionLocked(
const float delta_x = transform.HostLogicalToGuestX(event.delta_x);
const float delta_y = transform.HostLogicalToGuestY(event.delta_y);
if (event.x == 0.0f && event.y == 0.0f &&
(delta_x != 0.0f || delta_y != 0.0f)) {
if (event.relative_mode) {
// Android's captured-pointer path maintains an unclipped virtual position.
mouse_x_ += delta_x;
mouse_y_ += delta_y;
} else {
@@ -559,7 +559,10 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseMotionLocked(
const float clamped_y = std::clamp(mouse_y_, 0.0f, max_y);
return NativeResultLocked(
sink_.mouse_move(sink_.context, clamped_x, clamped_y, delta_x, delta_y),
sink_.mouse_move(sink_.context,
event.relative_mode ? mouse_x_ : clamped_x,
event.relative_mode ? mouse_y_ : clamped_y,
delta_x, delta_y),
RobloxInputEventKind::kMouseMotion);
}
@@ -576,15 +579,12 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseButtonLocked(
RobloxInputEventKind::kMouseButton,
Unsupported("SDL mouse button has no Roblox mapping"));
}
if (event.x > 0.0f || event.y > 0.0f ||
(mouse_x_ == 0.0f && mouse_y_ == 0.0f)) {
const platform::SurfaceCoordinateTransform transform =
CoordinateTransform(snapshot_.viewport);
const platform::SurfaceCoordinateTransform transform =
CoordinateTransform(snapshot_.viewport);
if (!event.relative_mode) {
mouse_x_ = transform.HostLogicalToGuestX(event.x);
mouse_y_ = transform.HostLogicalToGuestY(event.y);
}
const platform::SurfaceCoordinateTransform transform =
CoordinateTransform(snapshot_.viewport);
const float max_x = std::max(0.0F, transform.guest_width() - 1.0F);
const float max_y = std::max(0.0F, transform.guest_height() - 1.0F);
const float clamped_x = std::clamp(mouse_x_, 0.0f, max_x);
@@ -615,10 +615,13 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseWheelLocked(
}
const platform::SurfaceCoordinateTransform transform =
CoordinateTransform(snapshot_.viewport);
mouse_x_ = transform.HostLogicalToGuestX(std::max(0.0f, event.mouse_x));
mouse_y_ = transform.HostLogicalToGuestY(std::max(0.0f, event.mouse_y));
if (!event.relative_mode) {
mouse_x_ = transform.HostLogicalToGuestX(std::max(0.0f, event.mouse_x));
mouse_y_ = transform.HostLogicalToGuestY(std::max(0.0f, event.mouse_y));
}
return NativeResultLocked(
sink_.mouse_wheel(sink_.context, mouse_x_, mouse_y_, event.delta_y),
sink_.mouse_wheel(sink_.context, std::max(0.0f, mouse_x_),
std::max(0.0f, mouse_y_), event.delta_y),
RobloxInputEventKind::kMouseWheel);
}
+5 -2
View File
@@ -2172,7 +2172,9 @@ bool PumpEvents() {
const bool fullscreen_shortcut = HandleFullscreenShortcut(event);
platform::PlatformEvent platform_event;
const bool converted =
platform::ConvertSdlEvent(g_state.sdl_window, event, &platform_event);
platform::ConvertSdlEvent(
g_state.sdl_window, event, &platform_event,
SDL_GetWindowRelativeMouseMode(g_state.sdl_window));
if (is_window_event && g_window_surface_lifecycle.active()) {
int pixel_width = 0;
int pixel_height = 0;
@@ -2238,7 +2240,8 @@ bool PumpEvents() {
std::get_if<platform::MouseMotionEvent>(&pending_motion.payload);
const auto* nxt =
std::get_if<platform::MouseMotionEvent>(&platform_event.payload);
if (cur != nullptr && nxt != nullptr) {
if (cur != nullptr && nxt != nullptr &&
cur->relative_mode == nxt->relative_mode) {
cur->delta_x += nxt->delta_x;
cur->delta_y += nxt->delta_y;
cur->x = nxt->x;
@@ -167,6 +167,26 @@ TEST(SdlEventConverterTest, PreservesHostWheelDirectionAndPrecision) {
}
}
TEST(SdlEventConverterTest, PreservesRelativeModeForAllMouseEvents) {
auto* window = reinterpret_cast<SDL_Window*>(0x1);
platform::PlatformEvent event;
for (const bool relative_mode : {false, true}) {
SDL_Event source{};
source.type = SDL_EVENT_MOUSE_MOTION;
ASSERT_TRUE(platform::ConvertSdlEvent(window, source, &event, relative_mode));
EXPECT_EQ(std::get<platform::MouseMotionEvent>(event.payload).relative_mode,
relative_mode);
source.type = SDL_EVENT_MOUSE_BUTTON_DOWN;
ASSERT_TRUE(platform::ConvertSdlEvent(window, source, &event, relative_mode));
EXPECT_EQ(std::get<platform::MouseButtonEvent>(event.payload).relative_mode,
relative_mode);
source.type = SDL_EVENT_MOUSE_WHEEL;
ASSERT_TRUE(platform::ConvertSdlEvent(window, source, &event, relative_mode));
EXPECT_EQ(std::get<platform::MouseWheelEvent>(event.payload).relative_mode,
relative_mode);
}
}
TEST(SdlEventConverterTest, RejectsInvalidArgumentsAndUnsupportedEvents) {
SDL_Event source{};
source.type = SDL_EVENT_USER;
+79
View File
@@ -257,6 +257,85 @@ TEST_F(RobloxInputRouterTest, MiddleClickUsesMouseButton3OnPressAndRelease) {
EXPECT_EQ(router_.Snapshot().active_mouse_buttons, 0U);
}
TEST_F(RobloxInputRouterTest, MouseOriginIsAnAbsolutePosition) {
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
100.0F, 80.0F, 0.0F, 0.0F, 0})).dispatched());
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
0.0F, 0.0F, -4.0F, -7.0F, 0})).dispatched());
const auto& move = probe_.mouse_moves.back();
EXPECT_FLOAT_EQ(move.x, 0.0F);
EXPECT_FLOAT_EQ(move.y, 0.0F);
EXPECT_FLOAT_EQ(move.delta_x, -4.0F);
EXPECT_FLOAT_EQ(move.delta_y, -7.0F);
}
TEST_F(RobloxInputRouterTest, ClickAtOriginDoesNotReusePreviousPosition) {
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
100.0F, 80.0F, 0.0F, 0.0F, 0})).dispatched());
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseButtonEvent{
true, SDL_BUTTON_RIGHT, 1, 0.0F, 0.0F})).dispatched());
const auto& button = probe_.mouse_buttons.back();
EXPECT_FLOAT_EQ(button.x, 0.0F);
EXPECT_FLOAT_EQ(button.y, 0.0F);
}
TEST_F(RobloxInputRouterTest, CapturedPointerKeepsMovingBeyondWindowEdges) {
float x = 640.0F;
float y = 360.0F;
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
x, y, 0.0F, 0.0F, 0})).dispatched());
const std::pair<float, float> deltas[] = {
{4000.0F, 0.0F}, {-8000.0F, 0.0F}, {4000.0F, 0.0F},
{0.0F, 4000.0F}, {0.0F, -8000.0F}, {0.0F, 4000.0F}};
for (const auto& delta : deltas) {
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
640.0F, 360.0F, delta.first, delta.second, SDL_BUTTON_RMASK, true}))
.dispatched());
x += delta.first;
y += delta.second;
const auto& move = probe_.mouse_moves.back();
EXPECT_FLOAT_EQ(move.x, x);
EXPECT_FLOAT_EQ(move.y, y);
EXPECT_FLOAT_EQ(move.delta_x, delta.first);
EXPECT_FLOAT_EQ(move.delta_y, delta.second);
}
}
TEST_F(RobloxInputRouterTest, CapturedButtonsAndWheelPreserveVirtualPosition) {
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
100.0F, 80.0F, 0.0F, 0.0F, 0})).dispatched());
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
1279.0F, 0.0F, 1500.0F, -200.0F, 0, true})).dispatched());
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseButtonEvent{
true, SDL_BUTTON_LEFT, 1, 640.0F, 360.0F, true})).dispatched());
EXPECT_FLOAT_EQ(probe_.mouse_buttons.back().x, 1279.0F);
EXPECT_FLOAT_EQ(probe_.mouse_buttons.back().y, 0.0F);
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseWheelEvent{
0.0F, -1.0F, 640.0F, 360.0F, true})).dispatched());
EXPECT_FLOAT_EQ(probe_.mouse_wheels.back().x, 1600.0F);
EXPECT_FLOAT_EQ(probe_.mouse_wheels.back().y, 0.0F);
EXPECT_FLOAT_EQ(probe_.mouse_wheels.back().delta_y, -1.0F);
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
1279.0F, 0.0F, 1.0F, 2.0F, 0, true})).dispatched());
EXPECT_FLOAT_EQ(probe_.mouse_moves.back().x, 1601.0F);
EXPECT_FLOAT_EQ(probe_.mouse_moves.back().y, -118.0F);
}
TEST_F(RobloxInputRouterTest, AbsoluteMotionResetsPositionAfterCapture) {
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
640.0F, 360.0F, 4000.0F, -4000.0F, 0, true})).dispatched());
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
20.0F, 30.0F, 0.0F, 0.0F, 0})).dispatched());
ASSERT_TRUE(router_.HandleEvent(Event(platform::MouseMotionEvent{
640.0F, 360.0F, 1.0F, 2.0F, 0, true})).dispatched());
EXPECT_FLOAT_EQ(probe_.mouse_moves.back().x, 21.0F);
EXPECT_FLOAT_EQ(probe_.mouse_moves.back().y, 32.0F);
}
TEST_F(RobloxInputRouterTest,
ScalesMouseCoordinatesAfterDisplayScaleChange) {
const RobloxInputDispatchResult resize = router_.HandleEvent(