Skip to content

Instantly share code, notes, and snippets.

@marler8997
Created June 30, 2025 00:20
Show Gist options
  • Save marler8997/97d7b075f8f6096882512e14fca459af to your computer and use it in GitHub Desktop.
Save marler8997/97d7b075f8f6096882512e14fca459af to your computer and use it in GitHub Desktop.
#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);
#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