Skip to content

Commit 1b3ee8d

Browse files
committed
feat: add relative mouse movement
1 parent 1106633 commit 1b3ee8d

13 files changed

Lines changed: 112 additions & 1 deletion

File tree

‎include/robot/Mouse.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class Mouse {
4646
LogicalPoint point, const MouseMoveOptions& options = {}
4747
);
4848

49+
// Relative motion for software that captures the cursor, such as game cameras
50+
// and mouse steering. Units are logical on macOS and raw device counts
51+
// elsewhere, where the OS pointer speed may also apply.
52+
[[nodiscard]] std::expected<void, Error> moveBy(double deltaX, double deltaY);
53+
4954
[[nodiscard]] std::expected<LogicalPoint, Error> position();
5055

5156
[[nodiscard]] std::expected<void, Error> press(MouseButton button);

‎include/robot/backend/IMouseBackend.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ class IMouseBackend {
3030
LogicalPoint point
3131
) = 0;
3232

33+
// Relative motion, reported to the OS as a raw delta. This is what software
34+
// that captures the cursor (game cameras, mouse steering) reads; an absolute
35+
// warp does not produce usable deltas there.
36+
[[nodiscard]] virtual std::expected<void, Error> moveRelative(
37+
double deltaX, double deltaY
38+
) = 0;
39+
3340
// Current pointer position. ErrorCode::Unsupported where it cannot be read.
3441
[[nodiscard]] virtual std::expected<LogicalPoint, Error> cursorPosition() = 0;
3542

‎src/common/Mouse.cpp‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ std::expected<void, Error> Mouse::move(const LogicalPoint point) {
2828
return backend_->warpCursor(point);
2929
}
3030

31+
std::expected<void, Error> Mouse::moveBy(
32+
const double deltaX, const double deltaY
33+
) {
34+
return backend_->moveRelative(deltaX, deltaY);
35+
}
36+
3137
std::expected<void, Error> Mouse::moveSmooth(
3238
const LogicalPoint point, const MouseMoveOptions& options
3339
) {

‎src/platform/linux/UinputBackend.cpp‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ std::expected<void, Error> UinputBackend::warpCursor(LogicalPoint /*point*/) {
123123
));
124124
}
125125

126+
std::expected<void, Error> UinputBackend::moveRelative(
127+
const double deltaX, const double deltaY
128+
) {
129+
const auto dx = static_cast<std::int32_t>(std::lround(deltaX));
130+
const auto dy = static_cast<std::int32_t>(std::lround(deltaY));
131+
132+
if (auto r = writeEvent(fd_, EV_REL, REL_X, dx); !r) return r;
133+
if (auto r = writeEvent(fd_, EV_REL, REL_Y, dy); !r) return r;
134+
return writeEvent(fd_, EV_SYN, SYN_REPORT, 0);
135+
}
136+
126137
std::expected<LogicalPoint, Error> UinputBackend::cursorPosition() {
127138
return std::unexpected(Error::unsupported(
128139
"reading the global pointer position is unavailable through uinput"

‎src/platform/linux/UinputBackend.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ class UinputBackend final : public backend::IKeyboardBackend,
4242

4343
// Mouse.
4444
std::expected<void, Error> warpCursor(LogicalPoint point) override;
45+
std::expected<void, Error> moveRelative(
46+
double deltaX, double deltaY
47+
) override;
4548
std::expected<LogicalPoint, Error> cursorPosition() override;
4649
std::expected<void, Error> button(
4750
MouseButton button, ButtonAction action, int clickCount

‎src/platform/linux/X11MouseBackend.cpp‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ std::expected<void, Error> X11MouseBackend::warpCursor(const LogicalPoint point)
3333
return {};
3434
}
3535

36+
std::expected<void, Error> X11MouseBackend::moveRelative(
37+
const double deltaX, const double deltaY
38+
) {
39+
XTestFakeRelativeMotionEvent(
40+
connection_->display(), static_cast<int>(std::lround(deltaX)),
41+
static_cast<int>(std::lround(deltaY)), CurrentTime
42+
);
43+
XFlush(connection_->display());
44+
return {};
45+
}
46+
3647
std::expected<LogicalPoint, Error> X11MouseBackend::cursorPosition() {
3748
Window rootReturn = 0;
3849
Window childReturn = 0;

‎src/platform/linux/X11MouseBackend.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ class X11MouseBackend final : public backend::IMouseBackend {
1616
: connection_(&connection) {}
1717

1818
std::expected<void, Error> warpCursor(LogicalPoint point) override;
19+
std::expected<void, Error> moveRelative(
20+
double deltaX, double deltaY
21+
) override;
1922
std::expected<LogicalPoint, Error> cursorPosition() override;
2023
std::expected<void, Error> button(
2124
MouseButton button, ButtonAction action, int clickCount

‎src/platform/macos/MacMouseBackend.cpp‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,35 @@ std::expected<void, Error> MacMouseBackend::warpCursor(const LogicalPoint point)
7474
return {};
7575
}
7676

77+
std::expected<void, Error> MacMouseBackend::moveRelative(
78+
const double deltaX, const double deltaY
79+
) {
80+
auto here = cursorPosition();
81+
if (!here) return std::unexpected(here.error());
82+
83+
const CGPoint target = CGPointMake(here->x + deltaX, here->y + deltaY);
84+
const CGEventType type =
85+
pressedButton_ ? dragType(*pressedButton_) : kCGEventMouseMoved;
86+
const CGMouseButton btn =
87+
pressedButton_ ? cgButton(*pressedButton_) : kCGMouseButtonLeft;
88+
89+
CGEventRef event = CGEventCreateMouseEvent(source_, type, target, btn);
90+
if (event == nullptr) {
91+
return std::unexpected(Error::platformError("CGEventCreateMouseEvent"));
92+
}
93+
94+
// Applications that captured the cursor read these fields, not the location.
95+
CGEventSetIntegerValueField(
96+
event, kCGMouseEventDeltaX, static_cast<int64_t>(std::lround(deltaX))
97+
);
98+
CGEventSetIntegerValueField(
99+
event, kCGMouseEventDeltaY, static_cast<int64_t>(std::lround(deltaY))
100+
);
101+
CGEventPost(kCGHIDEventTap, event);
102+
CFRelease(event);
103+
return {};
104+
}
105+
77106
std::expected<LogicalPoint, Error> MacMouseBackend::cursorPosition() {
78107
CGEventRef event = CGEventCreate(nullptr);
79108
if (event == nullptr) {

‎src/platform/macos/MacMouseBackend.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class MacMouseBackend final : public backend::IMouseBackend {
2121
MacMouseBackend& operator=(const MacMouseBackend&) = delete;
2222

2323
std::expected<void, Error> warpCursor(LogicalPoint point) override;
24+
std::expected<void, Error> moveRelative(
25+
double deltaX, double deltaY
26+
) override;
2427
std::expected<LogicalPoint, Error> cursorPosition() override;
2528
std::expected<void, Error> button(
2629
MouseButton button, ButtonAction action, int clickCount

‎src/platform/windows/WinMouseBackend.cpp‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ std::expected<void, Error> WinMouseBackend::warpCursor(const LogicalPoint point)
138138
return send(input);
139139
}
140140

141+
std::expected<void, Error> WinMouseBackend::moveRelative(
142+
const double deltaX, const double deltaY
143+
) {
144+
INPUT input{};
145+
input.type = INPUT_MOUSE;
146+
input.mi.dx = static_cast<LONG>(std::lround(deltaX));
147+
input.mi.dy = static_cast<LONG>(std::lround(deltaY));
148+
input.mi.dwFlags = MOUSEEVENTF_MOVE;
149+
return send(input);
150+
}
151+
141152
std::expected<LogicalPoint, Error> WinMouseBackend::cursorPosition() {
142153
POINT p{};
143154
if (GetCursorPos(&p) == 0) {

0 commit comments

Comments
 (0)