Created
June 30, 2025 00:20
-
-
Save marler8997/97d7b075f8f6096882512e14fca459af to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
#include <cstdint> | |
#include <optional> | |
enum class ReleaseMouseButtons { No, Yes }; | |
struct SendMousePos { int32_t x; int32_t y; }; | |
void send_mouse_move(std::optional<SendMousePos> pos, ReleaseMouseButtons); | |
void send_mouse_button(uint32_t button_flags); | |
void send_mouse_scrolls(bool precise, float delta_x, float delta_y); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "SendInput.h" | |
#include <mutex> | |
#include <vector> | |
#include <windows.h> | |
#include <tn/Log.h> | |
#include "ErrorState.h" | |
#include "OsError.h" | |
#include "ScrollWheel.h" | |
#include "SendInputExtraMagic.h" | |
namespace | |
{ | |
struct { | |
std::mutex mutex; | |
bool new_inputs_to_2 = false; | |
std::vector<INPUT> inputs1; | |
std::vector<INPUT> inputs2; | |
HANDLE event = NULL; | |
} global; | |
DWORD WINAPI thread_proc(LPVOID) | |
{ | |
TN_INFO("SendInput thread started"); | |
{ | |
HRESULT hr = SetThreadDescription(GetCurrentThread(), L"SendInput"); | |
if (FAILED(hr)) { | |
TN_ERROR("SetThreadDescription for SendInput failed, hr=0x{:x}", (DWORD)hr); | |
// just continue on | |
} | |
} | |
std::vector<INPUT> local_inputs; | |
while (true) { | |
{ | |
DWORD result = WaitForSingleObject(global.event, INFINITE); | |
if (result != WAIT_OBJECT_0) { | |
TN_ERROR("WaitForSingleObject returned {}, error={}", result, GetLastError()); | |
std::terminate(); | |
} | |
} | |
std::vector<INPUT>* inputs_ref; | |
{ | |
std::lock_guard<std::mutex> lock(global.mutex); | |
inputs_ref = (global.new_inputs_to_2) | |
? &global.inputs2 | |
: &global.inputs1; | |
global.new_inputs_to_2 = !global.new_inputs_to_2; | |
} | |
if (!inputs_ref->empty()) { | |
DWORD send_input_error = 0; | |
UINT sent = SendInput(inputs_ref->size(), inputs_ref->data(), sizeof(INPUT)); | |
if (sent != inputs_ref->size()) { | |
DWORD error = GetLastError(); | |
TN_ERROR("SendInput for {} inputs failed, returned {}, error={}", inputs_ref->size(), sent, error); | |
send_input_error = ensure_non_zero(error); | |
} | |
report_error_state(ErrorState::SendInputMouse, send_input_error); | |
inputs_ref->clear(); | |
} | |
} | |
} | |
void inside_mutex_notify() | |
{ | |
if (!global.event) { | |
global.event = CreateEvent(NULL, FALSE, FALSE, NULL); | |
if (global.event == NULL) { | |
TN_ERROR("CreateEvent failed, error={}", GetLastError()); | |
std::terminate(); | |
} | |
HANDLE thread = CreateThread(NULL, 0, thread_proc, NULL, 0, NULL); | |
if (thread == NULL) { | |
TN_ERROR("CreateThread failed, error={}", GetLastError()); | |
std::terminate(); | |
} | |
if (!CloseHandle(thread)) std::terminate(); | |
} | |
if (!SetEvent(global.event)) { | |
TN_ERROR("SetEvent failed, error={}", GetLastError()); | |
std::terminate(); | |
} | |
} | |
} | |
void send_mouse_move(std::optional<SendMousePos> pos, ReleaseMouseButtons release) | |
{ | |
std::optional<SendMousePos> normalized; | |
if (pos.has_value()) { | |
int virtualScreenLeft = GetSystemMetrics(SM_XVIRTUALSCREEN); | |
int virtualScreenTop = GetSystemMetrics(SM_YVIRTUALSCREEN); | |
int virtualScreenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN); | |
int virtualScreenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN); | |
normalized.emplace( | |
(pos->x - virtualScreenLeft) * 65535 / (virtualScreenWidth - 1), | |
(pos->y - virtualScreenTop) * 65535 / (virtualScreenHeight - 1) | |
); | |
} | |
{ | |
std::lock_guard lock(global.mutex); | |
std::vector<INPUT>& inputs = global.new_inputs_to_2 ? global.inputs2 : global.inputs1; | |
auto inputs_size_before = inputs.size(); | |
if (release == ReleaseMouseButtons::Yes) { | |
static const struct { | |
const char* name; | |
int vkey; | |
DWORD release_flag; | |
DWORD mouse_data; | |
} release_keys[] = { | |
{ "left", VK_LBUTTON, MOUSEEVENTF_LEFTUP, 0 }, | |
{ "right", VK_RBUTTON, MOUSEEVENTF_RIGHTUP, 0 }, | |
{ "middle", VK_MBUTTON, MOUSEEVENTF_MIDDLEUP, 0 }, | |
{ "x1", VK_XBUTTON1, MOUSEEVENTF_XUP, XBUTTON1 }, | |
{ "x2", VK_XBUTTON2, MOUSEEVENTF_XUP, XBUTTON2 }, | |
}; | |
for (auto release_key : release_keys) { | |
if (GetAsyncKeyState(release_key.vkey) & 0x8000) { | |
TN_INFO("auto release mouse button: {}", release_key.name); | |
inputs.push_back({ | |
.type = INPUT_MOUSE, | |
.mi = { | |
.dx = 0, | |
.dy = 0, | |
.mouseData = release_key.mouse_data, | |
.dwFlags = release_key.release_flag, | |
.time = 0, | |
.dwExtraInfo = EXTRA_INFO_REMOTE_INPUT_MAGIC | |
} | |
}); | |
} | |
} | |
} | |
if (normalized.has_value()) { | |
inputs.push_back({ | |
.type = INPUT_MOUSE, | |
.mi = { | |
.dx = normalized->x, | |
.dy = normalized->y, | |
.mouseData = 0, | |
.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK, | |
.time = 0, | |
.dwExtraInfo = EXTRA_INFO_REMOTE_INPUT_MAGIC | |
} | |
}); | |
} | |
if (inputs_size_before != inputs.size()) { | |
inside_mutex_notify(); | |
} | |
} | |
} | |
void send_mouse_button(uint32_t button_flags) | |
{ | |
std::lock_guard lock(global.mutex); | |
std::vector<INPUT>& inputs = global.new_inputs_to_2 ? global.inputs2 : global.inputs1; | |
inputs.push_back({ | |
.type = INPUT_MOUSE, | |
.mi = { | |
.dx = 0, | |
.dy = 0, | |
.mouseData = 0, | |
.dwFlags = button_flags, | |
.time = 0, | |
.dwExtraInfo = EXTRA_INFO_REMOTE_INPUT_MAGIC | |
} | |
}); | |
inside_mutex_notify(); | |
} | |
void send_mouse_scrolls(bool precise, float delta_x, float delta_y) | |
{ | |
float scale = precise | |
? SCROLL_WHEEL_MACOS_PRECISE_TO_WIN32 | |
: SCROLL_WHEEL_MACOS_COARSE_TO_WIN32; | |
int delta_x_int = -delta_x * scale; | |
int delta_y_int = delta_y * scale; | |
const char *precise_str = precise ? "precise" : "coarse"; | |
if (delta_x_int == 0 && delta_y_int == 0) { | |
if (delta_x != 0 || delta_y != 0) { | |
TN_WARN("scroll delta {}x{} ({}) is too small!", delta_x, delta_y, precise_str); | |
} | |
return; | |
} | |
std::lock_guard lock(global.mutex); | |
std::vector<INPUT>& inputs = global.new_inputs_to_2 ? global.inputs2 : global.inputs1; | |
for (int is_y = 0; is_y <= 1; is_y++) { | |
int delta_int = is_y ? delta_y_int : delta_x_int; | |
if (delta_int != 0) { | |
inputs.push_back({ | |
.type = INPUT_MOUSE, | |
.mi = { | |
.dx = 0, | |
.dy = 0, | |
.mouseData = (DWORD)delta_int, | |
.dwFlags = is_y ? (DWORD)MOUSEEVENTF_WHEEL : (DWORD)MOUSEEVENTF_HWHEEL, | |
.time = 0, | |
.dwExtraInfo = EXTRA_INFO_REMOTE_INPUT_MAGIC | |
} | |
}); | |
} | |
} | |
inside_mutex_notify(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment