fix line endings
This commit is contained in:
+213
-213
@@ -1,213 +1,213 @@
|
||||
#include "gdi_overlay.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// the back buffer matches the window DC; the software buffer has a fixed 32-bit layout
|
||||
enum class BufferType {
|
||||
TargetCompatible,
|
||||
Bgra32,
|
||||
};
|
||||
|
||||
struct GdiBuffer {
|
||||
HDC dc = nullptr;
|
||||
HBITMAP bitmap = nullptr;
|
||||
// bitmap originally selected into the memory DC, restored before cleanup
|
||||
HGDIOBJ old_bitmap = nullptr;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
// back buffer holds the complete frame; overlay buffer holds ImGui software pixels
|
||||
GdiBuffer BACK_BUFFER;
|
||||
GdiBuffer OVERLAY_BUFFER;
|
||||
|
||||
void release_buffer(GdiBuffer &buffer) {
|
||||
// destroy the DC before the bitmap so cleanup is safe even if restoration fails
|
||||
if (buffer.dc != nullptr) {
|
||||
if (buffer.old_bitmap != nullptr && buffer.old_bitmap != HGDI_ERROR) {
|
||||
SelectObject(buffer.dc, buffer.old_bitmap);
|
||||
}
|
||||
DeleteDC(buffer.dc);
|
||||
}
|
||||
if (buffer.bitmap != nullptr) {
|
||||
DeleteObject(buffer.bitmap);
|
||||
}
|
||||
|
||||
buffer = {};
|
||||
}
|
||||
|
||||
// ensures the buffer has a memory DC with a bitmap of the requested size and type
|
||||
// selected into it. a matching allocation is reused; otherwise the old resources are
|
||||
// released and recreated. returns false if the dimensions or any GDI operation fail.
|
||||
bool ensure_buffer(
|
||||
GdiBuffer &buffer,
|
||||
HDC target_dc,
|
||||
int width,
|
||||
int height,
|
||||
BufferType type,
|
||||
const char *name) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (buffer.dc != nullptr && buffer.bitmap != nullptr &&
|
||||
buffer.width == width && buffer.height == height) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// keep allocations across frames and recreate only after a size change
|
||||
release_buffer(buffer);
|
||||
buffer.dc = CreateCompatibleDC(target_dc);
|
||||
if (buffer.dc == nullptr) {
|
||||
log_warning("touch", "failed to create {} DC: {}", name, GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// compatible bitmaps are fast presentation targets; BGRA bitmaps accept raw pixels
|
||||
if (type == BufferType::TargetCompatible) {
|
||||
buffer.bitmap = CreateCompatibleBitmap(target_dc, width, height);
|
||||
} else {
|
||||
buffer.bitmap = CreateBitmap(width, height, 1, sizeof(uint32_t) * 8, nullptr);
|
||||
}
|
||||
if (buffer.bitmap == nullptr) {
|
||||
log_warning("touch", "failed to create {} bitmap: {}", name, GetLastError());
|
||||
release_buffer(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer.old_bitmap = SelectObject(buffer.dc, buffer.bitmap);
|
||||
if (buffer.old_bitmap == nullptr || buffer.old_bitmap == HGDI_ERROR) {
|
||||
log_warning("touch", "failed to select {} bitmap: {}", name, GetLastError());
|
||||
release_buffer(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer.width = width;
|
||||
buffer.height = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool update_overlay_buffer(
|
||||
HDC target_dc,
|
||||
const uint32_t *pixels,
|
||||
bool pixels_dirty,
|
||||
int width,
|
||||
int height) {
|
||||
if (pixels == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool needs_update = pixels_dirty || OVERLAY_BUFFER.bitmap == nullptr ||
|
||||
OVERLAY_BUFFER.width != width || OVERLAY_BUFFER.height != height;
|
||||
if (!ensure_buffer(
|
||||
OVERLAY_BUFFER,
|
||||
target_dc,
|
||||
width,
|
||||
height,
|
||||
BufferType::Bgra32,
|
||||
"software overlay")) {
|
||||
return false;
|
||||
}
|
||||
if (!needs_update) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// SetDIBits requires the destination bitmap not to be selected into a DC
|
||||
HGDIOBJ overlay_bitmap =
|
||||
SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.old_bitmap);
|
||||
if (overlay_bitmap == nullptr || overlay_bitmap == HGDI_ERROR) {
|
||||
log_warning("touch", "failed to deselect software overlay bitmap: {}", GetLastError());
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
return false;
|
||||
}
|
||||
|
||||
BITMAPINFO bitmap_info {};
|
||||
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bitmap_info.bmiHeader.biWidth = width;
|
||||
bitmap_info.bmiHeader.biHeight = -height;
|
||||
bitmap_info.bmiHeader.biPlanes = 1;
|
||||
bitmap_info.bmiHeader.biBitCount = sizeof(uint32_t) * 8;
|
||||
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
int copied_lines = SetDIBits(
|
||||
target_dc,
|
||||
OVERLAY_BUFFER.bitmap,
|
||||
0,
|
||||
height,
|
||||
pixels,
|
||||
&bitmap_info,
|
||||
DIB_RGB_COLORS);
|
||||
|
||||
HGDIOBJ old_bitmap = SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.bitmap);
|
||||
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
|
||||
log_warning("touch", "failed to reselect software overlay bitmap: {}", GetLastError());
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
return false;
|
||||
}
|
||||
OVERLAY_BUFFER.old_bitmap = old_bitmap;
|
||||
|
||||
if (copied_lines != height) {
|
||||
log_warning("touch", "failed to update software overlay bitmap: {} of {} lines copied",
|
||||
copied_lines, height);
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
HDC touch_gdi_overlay_begin_frame(
|
||||
HDC target_dc,
|
||||
HBRUSH background_brush,
|
||||
int width,
|
||||
int height,
|
||||
const uint32_t *overlay_pixels,
|
||||
bool overlay_pixels_dirty,
|
||||
int overlay_width,
|
||||
int overlay_height) {
|
||||
if (!ensure_buffer(
|
||||
BACK_BUFFER,
|
||||
target_dc,
|
||||
width,
|
||||
height,
|
||||
BufferType::TargetCompatible,
|
||||
"overlay back buffer")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HDC draw_dc = BACK_BUFFER.dc;
|
||||
SetBkMode(draw_dc, TRANSPARENT);
|
||||
|
||||
// start each frame from the transparent color-key background
|
||||
RECT buffer_rect {0, 0, width, height};
|
||||
FillRect(draw_dc, &buffer_rect, background_brush);
|
||||
|
||||
if (update_overlay_buffer(
|
||||
target_dc,
|
||||
overlay_pixels,
|
||||
overlay_pixels_dirty,
|
||||
overlay_width,
|
||||
overlay_height) &&
|
||||
!BitBlt(draw_dc, 0, 0, overlay_width, overlay_height,
|
||||
OVERLAY_BUFFER.dc, 0, 0, SRCCOPY)) {
|
||||
log_warning("touch", "failed to draw software overlay bitmap: {}", GetLastError());
|
||||
}
|
||||
|
||||
return draw_dc;
|
||||
}
|
||||
|
||||
void touch_gdi_overlay_present(HDC target_dc) {
|
||||
// one full-window blit exposes the completed frame without an intermediate erase
|
||||
if (!BitBlt(target_dc, 0, 0, BACK_BUFFER.width, BACK_BUFFER.height,
|
||||
BACK_BUFFER.dc, 0, 0, SRCCOPY)) {
|
||||
log_warning("touch", "failed to present overlay back buffer: {}", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
void touch_gdi_overlay_release() {
|
||||
release_buffer(BACK_BUFFER);
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
}
|
||||
#include "gdi_overlay.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// the back buffer matches the window DC; the software buffer has a fixed 32-bit layout
|
||||
enum class BufferType {
|
||||
TargetCompatible,
|
||||
Bgra32,
|
||||
};
|
||||
|
||||
struct GdiBuffer {
|
||||
HDC dc = nullptr;
|
||||
HBITMAP bitmap = nullptr;
|
||||
// bitmap originally selected into the memory DC, restored before cleanup
|
||||
HGDIOBJ old_bitmap = nullptr;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
// back buffer holds the complete frame; overlay buffer holds ImGui software pixels
|
||||
GdiBuffer BACK_BUFFER;
|
||||
GdiBuffer OVERLAY_BUFFER;
|
||||
|
||||
void release_buffer(GdiBuffer &buffer) {
|
||||
// destroy the DC before the bitmap so cleanup is safe even if restoration fails
|
||||
if (buffer.dc != nullptr) {
|
||||
if (buffer.old_bitmap != nullptr && buffer.old_bitmap != HGDI_ERROR) {
|
||||
SelectObject(buffer.dc, buffer.old_bitmap);
|
||||
}
|
||||
DeleteDC(buffer.dc);
|
||||
}
|
||||
if (buffer.bitmap != nullptr) {
|
||||
DeleteObject(buffer.bitmap);
|
||||
}
|
||||
|
||||
buffer = {};
|
||||
}
|
||||
|
||||
// ensures the buffer has a memory DC with a bitmap of the requested size and type
|
||||
// selected into it. a matching allocation is reused; otherwise the old resources are
|
||||
// released and recreated. returns false if the dimensions or any GDI operation fail.
|
||||
bool ensure_buffer(
|
||||
GdiBuffer &buffer,
|
||||
HDC target_dc,
|
||||
int width,
|
||||
int height,
|
||||
BufferType type,
|
||||
const char *name) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (buffer.dc != nullptr && buffer.bitmap != nullptr &&
|
||||
buffer.width == width && buffer.height == height) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// keep allocations across frames and recreate only after a size change
|
||||
release_buffer(buffer);
|
||||
buffer.dc = CreateCompatibleDC(target_dc);
|
||||
if (buffer.dc == nullptr) {
|
||||
log_warning("touch", "failed to create {} DC: {}", name, GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// compatible bitmaps are fast presentation targets; BGRA bitmaps accept raw pixels
|
||||
if (type == BufferType::TargetCompatible) {
|
||||
buffer.bitmap = CreateCompatibleBitmap(target_dc, width, height);
|
||||
} else {
|
||||
buffer.bitmap = CreateBitmap(width, height, 1, sizeof(uint32_t) * 8, nullptr);
|
||||
}
|
||||
if (buffer.bitmap == nullptr) {
|
||||
log_warning("touch", "failed to create {} bitmap: {}", name, GetLastError());
|
||||
release_buffer(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer.old_bitmap = SelectObject(buffer.dc, buffer.bitmap);
|
||||
if (buffer.old_bitmap == nullptr || buffer.old_bitmap == HGDI_ERROR) {
|
||||
log_warning("touch", "failed to select {} bitmap: {}", name, GetLastError());
|
||||
release_buffer(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer.width = width;
|
||||
buffer.height = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool update_overlay_buffer(
|
||||
HDC target_dc,
|
||||
const uint32_t *pixels,
|
||||
bool pixels_dirty,
|
||||
int width,
|
||||
int height) {
|
||||
if (pixels == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool needs_update = pixels_dirty || OVERLAY_BUFFER.bitmap == nullptr ||
|
||||
OVERLAY_BUFFER.width != width || OVERLAY_BUFFER.height != height;
|
||||
if (!ensure_buffer(
|
||||
OVERLAY_BUFFER,
|
||||
target_dc,
|
||||
width,
|
||||
height,
|
||||
BufferType::Bgra32,
|
||||
"software overlay")) {
|
||||
return false;
|
||||
}
|
||||
if (!needs_update) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// SetDIBits requires the destination bitmap not to be selected into a DC
|
||||
HGDIOBJ overlay_bitmap =
|
||||
SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.old_bitmap);
|
||||
if (overlay_bitmap == nullptr || overlay_bitmap == HGDI_ERROR) {
|
||||
log_warning("touch", "failed to deselect software overlay bitmap: {}", GetLastError());
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
return false;
|
||||
}
|
||||
|
||||
BITMAPINFO bitmap_info {};
|
||||
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bitmap_info.bmiHeader.biWidth = width;
|
||||
bitmap_info.bmiHeader.biHeight = -height;
|
||||
bitmap_info.bmiHeader.biPlanes = 1;
|
||||
bitmap_info.bmiHeader.biBitCount = sizeof(uint32_t) * 8;
|
||||
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
int copied_lines = SetDIBits(
|
||||
target_dc,
|
||||
OVERLAY_BUFFER.bitmap,
|
||||
0,
|
||||
height,
|
||||
pixels,
|
||||
&bitmap_info,
|
||||
DIB_RGB_COLORS);
|
||||
|
||||
HGDIOBJ old_bitmap = SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.bitmap);
|
||||
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
|
||||
log_warning("touch", "failed to reselect software overlay bitmap: {}", GetLastError());
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
return false;
|
||||
}
|
||||
OVERLAY_BUFFER.old_bitmap = old_bitmap;
|
||||
|
||||
if (copied_lines != height) {
|
||||
log_warning("touch", "failed to update software overlay bitmap: {} of {} lines copied",
|
||||
copied_lines, height);
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
HDC touch_gdi_overlay_begin_frame(
|
||||
HDC target_dc,
|
||||
HBRUSH background_brush,
|
||||
int width,
|
||||
int height,
|
||||
const uint32_t *overlay_pixels,
|
||||
bool overlay_pixels_dirty,
|
||||
int overlay_width,
|
||||
int overlay_height) {
|
||||
if (!ensure_buffer(
|
||||
BACK_BUFFER,
|
||||
target_dc,
|
||||
width,
|
||||
height,
|
||||
BufferType::TargetCompatible,
|
||||
"overlay back buffer")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HDC draw_dc = BACK_BUFFER.dc;
|
||||
SetBkMode(draw_dc, TRANSPARENT);
|
||||
|
||||
// start each frame from the transparent color-key background
|
||||
RECT buffer_rect {0, 0, width, height};
|
||||
FillRect(draw_dc, &buffer_rect, background_brush);
|
||||
|
||||
if (update_overlay_buffer(
|
||||
target_dc,
|
||||
overlay_pixels,
|
||||
overlay_pixels_dirty,
|
||||
overlay_width,
|
||||
overlay_height) &&
|
||||
!BitBlt(draw_dc, 0, 0, overlay_width, overlay_height,
|
||||
OVERLAY_BUFFER.dc, 0, 0, SRCCOPY)) {
|
||||
log_warning("touch", "failed to draw software overlay bitmap: {}", GetLastError());
|
||||
}
|
||||
|
||||
return draw_dc;
|
||||
}
|
||||
|
||||
void touch_gdi_overlay_present(HDC target_dc) {
|
||||
// one full-window blit exposes the completed frame without an intermediate erase
|
||||
if (!BitBlt(target_dc, 0, 0, BACK_BUFFER.width, BACK_BUFFER.height,
|
||||
BACK_BUFFER.dc, 0, 0, SRCCOPY)) {
|
||||
log_warning("touch", "failed to present overlay back buffer: {}", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
void touch_gdi_overlay_release() {
|
||||
release_buffer(BACK_BUFFER);
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
// prepares a complete offscreen frame and returns its drawing DC; returns null on failure
|
||||
HDC touch_gdi_overlay_begin_frame(
|
||||
HDC target_dc,
|
||||
HBRUSH background_brush,
|
||||
int width,
|
||||
int height,
|
||||
const uint32_t *overlay_pixels,
|
||||
bool overlay_pixels_dirty,
|
||||
int overlay_width,
|
||||
int overlay_height);
|
||||
|
||||
// presents the frame prepared by the most recent successful begin call
|
||||
void touch_gdi_overlay_present(HDC target_dc);
|
||||
|
||||
// releases all cached GDI resources
|
||||
void touch_gdi_overlay_release();
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
// prepares a complete offscreen frame and returns its drawing DC; returns null on failure
|
||||
HDC touch_gdi_overlay_begin_frame(
|
||||
HDC target_dc,
|
||||
HBRUSH background_brush,
|
||||
int width,
|
||||
int height,
|
||||
const uint32_t *overlay_pixels,
|
||||
bool overlay_pixels_dirty,
|
||||
int overlay_width,
|
||||
int overlay_height);
|
||||
|
||||
// presents the frame prepared by the most recent successful begin call
|
||||
void touch_gdi_overlay_present(HDC target_dc);
|
||||
|
||||
// releases all cached GDI resources
|
||||
void touch_gdi_overlay_release();
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace nativetouch::inject {
|
||||
|
||||
enum class ContactOwner {
|
||||
None,
|
||||
Mouse,
|
||||
Synthetic,
|
||||
};
|
||||
|
||||
bool initialize_touch_injection();
|
||||
void initialize_synthetic_touch();
|
||||
void refresh_contact_lifetime();
|
||||
|
||||
bool contact_is_active();
|
||||
bool contact_is_owned_by(ContactOwner owner, HWND window);
|
||||
bool begin_contact(
|
||||
ContactOwner owner,
|
||||
HWND window,
|
||||
POINT position,
|
||||
bool transform_returned_coordinates);
|
||||
bool update_contact(ContactOwner owner, HWND window, POINT position);
|
||||
void set_contact_timer(ContactOwner owner, HWND window, UINT_PTR timer_id);
|
||||
bool release_active_contact();
|
||||
|
||||
HWND get_injection_window();
|
||||
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param);
|
||||
bool handle_synthetic_message(HWND window, UINT message, WPARAM w_param, LPARAM l_param);
|
||||
}
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace nativetouch::inject {
|
||||
|
||||
enum class ContactOwner {
|
||||
None,
|
||||
Mouse,
|
||||
Synthetic,
|
||||
};
|
||||
|
||||
bool initialize_touch_injection();
|
||||
void initialize_synthetic_touch();
|
||||
void refresh_contact_lifetime();
|
||||
|
||||
bool contact_is_active();
|
||||
bool contact_is_owned_by(ContactOwner owner, HWND window);
|
||||
bool begin_contact(
|
||||
ContactOwner owner,
|
||||
HWND window,
|
||||
POINT position,
|
||||
bool transform_returned_coordinates);
|
||||
bool update_contact(ContactOwner owner, HWND window, POINT position);
|
||||
void set_contact_timer(ContactOwner owner, HWND window, UINT_PTR timer_id);
|
||||
bool release_active_contact();
|
||||
|
||||
HWND get_injection_window();
|
||||
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param);
|
||||
bool handle_synthetic_message(HWND window, UINT message, WPARAM w_param, LPARAM l_param);
|
||||
}
|
||||
|
||||
@@ -1,146 +1,146 @@
|
||||
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
||||
#define _WIN32_WINNT 0x0602
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "inject_internal.h"
|
||||
#include "settings.h"
|
||||
#include "transform.h"
|
||||
|
||||
#include "touch/touch.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace nativetouch::inject {
|
||||
|
||||
constexpr UINT CONTACT_TIMER_INTERVAL_MS = 16;
|
||||
|
||||
static int mouse_contact_timer_token;
|
||||
|
||||
struct PrimaryMouseButton {
|
||||
UINT down_message;
|
||||
UINT double_click_message;
|
||||
UINT up_message;
|
||||
WPARAM state_mask;
|
||||
};
|
||||
|
||||
// honor the user's swapped-button setting when choosing the primary button
|
||||
static PrimaryMouseButton get_primary_mouse_button() {
|
||||
if (GetSystemMetrics(SM_SWAPBUTTON)) {
|
||||
return { WM_RBUTTONDOWN, WM_RBUTTONDBLCLK, WM_RBUTTONUP, MK_RBUTTON };
|
||||
}
|
||||
return { WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, WM_LBUTTONUP, MK_LBUTTON };
|
||||
}
|
||||
|
||||
// use the current physical cursor but reject points outside the subscreen
|
||||
static bool get_mouse_injection_position(HWND window, POINT *position) {
|
||||
|
||||
// queued WM_MOUSEMOVE coordinates can lag behind the cursor; injecting them makes
|
||||
// Windows move its primary pointer back to stale positions during a drag.
|
||||
if (!GetCursorPos(position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
POINT transformed = *position;
|
||||
return transform::mouse_to_game(window, &transformed);
|
||||
}
|
||||
|
||||
// release the active injected contact and its window capture
|
||||
static void end_mouse_contact(HWND window) {
|
||||
if (contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||
release_active_contact();
|
||||
}
|
||||
}
|
||||
|
||||
// begin a contact at the physical cursor position and capture future mouse input
|
||||
static void begin_mouse_contact(HWND window) {
|
||||
if (contact_is_active()) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINT position;
|
||||
if (!get_mouse_injection_position(window, &position) ||
|
||||
!begin_contact(ContactOwner::Mouse, window, position, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// keep receiving drag messages after the cursor leaves the client area
|
||||
SetCapture(window);
|
||||
if (!settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
|
||||
const auto timer_id = reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token);
|
||||
if (SetTimer(window, timer_id, CONTACT_TIMER_INTERVAL_MS, nullptr)) {
|
||||
set_contact_timer(ContactOwner::Mouse, window, timer_id);
|
||||
} else {
|
||||
log_warning("touch::native", "failed to start mouse touch injection timer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update the contact while the primary button remains held
|
||||
static void move_mouse_contact(
|
||||
HWND window, WPARAM w_param, WPARAM primary_button_state) {
|
||||
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINT position;
|
||||
if (!get_mouse_injection_position(window, &position)) {
|
||||
end_mouse_contact(window);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((w_param & primary_button_state) == 0) {
|
||||
end_mouse_contact(window);
|
||||
return;
|
||||
}
|
||||
|
||||
update_contact(ContactOwner::Mouse, window, position);
|
||||
}
|
||||
|
||||
// emit stationary update frames so Windows keeps the contact alive
|
||||
static void refresh_mouse_contact(HWND window) {
|
||||
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINT position {};
|
||||
if (!GetCursorPos(&position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINT transformed = position;
|
||||
if (!transform::mouse_to_game(window, &transformed)) {
|
||||
end_mouse_contact(window);
|
||||
return;
|
||||
}
|
||||
|
||||
update_contact(ContactOwner::Mouse, window, position);
|
||||
}
|
||||
|
||||
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param) {
|
||||
if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST &&
|
||||
is_mouse_message_from_touchscreen()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message == WM_TIMER &&
|
||||
w_param == reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token)) {
|
||||
refresh_mouse_contact(window);
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto primary_button = get_primary_mouse_button();
|
||||
if (message == primary_button.down_message ||
|
||||
message == primary_button.double_click_message) {
|
||||
begin_mouse_contact(window);
|
||||
} else if (message == WM_MOUSEMOVE) {
|
||||
move_mouse_contact(window, w_param, primary_button.state_mask);
|
||||
} else if (message == primary_button.up_message) {
|
||||
end_mouse_contact(window);
|
||||
} else if (message == WM_CANCELMODE || message == WM_KILLFOCUS ||
|
||||
message == WM_CAPTURECHANGED) {
|
||||
end_mouse_contact(window);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
||||
#define _WIN32_WINNT 0x0602
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "inject_internal.h"
|
||||
#include "settings.h"
|
||||
#include "transform.h"
|
||||
|
||||
#include "touch/touch.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace nativetouch::inject {
|
||||
|
||||
constexpr UINT CONTACT_TIMER_INTERVAL_MS = 16;
|
||||
|
||||
static int mouse_contact_timer_token;
|
||||
|
||||
struct PrimaryMouseButton {
|
||||
UINT down_message;
|
||||
UINT double_click_message;
|
||||
UINT up_message;
|
||||
WPARAM state_mask;
|
||||
};
|
||||
|
||||
// honor the user's swapped-button setting when choosing the primary button
|
||||
static PrimaryMouseButton get_primary_mouse_button() {
|
||||
if (GetSystemMetrics(SM_SWAPBUTTON)) {
|
||||
return { WM_RBUTTONDOWN, WM_RBUTTONDBLCLK, WM_RBUTTONUP, MK_RBUTTON };
|
||||
}
|
||||
return { WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, WM_LBUTTONUP, MK_LBUTTON };
|
||||
}
|
||||
|
||||
// use the current physical cursor but reject points outside the subscreen
|
||||
static bool get_mouse_injection_position(HWND window, POINT *position) {
|
||||
|
||||
// queued WM_MOUSEMOVE coordinates can lag behind the cursor; injecting them makes
|
||||
// Windows move its primary pointer back to stale positions during a drag.
|
||||
if (!GetCursorPos(position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
POINT transformed = *position;
|
||||
return transform::mouse_to_game(window, &transformed);
|
||||
}
|
||||
|
||||
// release the active injected contact and its window capture
|
||||
static void end_mouse_contact(HWND window) {
|
||||
if (contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||
release_active_contact();
|
||||
}
|
||||
}
|
||||
|
||||
// begin a contact at the physical cursor position and capture future mouse input
|
||||
static void begin_mouse_contact(HWND window) {
|
||||
if (contact_is_active()) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINT position;
|
||||
if (!get_mouse_injection_position(window, &position) ||
|
||||
!begin_contact(ContactOwner::Mouse, window, position, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// keep receiving drag messages after the cursor leaves the client area
|
||||
SetCapture(window);
|
||||
if (!settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
|
||||
const auto timer_id = reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token);
|
||||
if (SetTimer(window, timer_id, CONTACT_TIMER_INTERVAL_MS, nullptr)) {
|
||||
set_contact_timer(ContactOwner::Mouse, window, timer_id);
|
||||
} else {
|
||||
log_warning("touch::native", "failed to start mouse touch injection timer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update the contact while the primary button remains held
|
||||
static void move_mouse_contact(
|
||||
HWND window, WPARAM w_param, WPARAM primary_button_state) {
|
||||
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINT position;
|
||||
if (!get_mouse_injection_position(window, &position)) {
|
||||
end_mouse_contact(window);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((w_param & primary_button_state) == 0) {
|
||||
end_mouse_contact(window);
|
||||
return;
|
||||
}
|
||||
|
||||
update_contact(ContactOwner::Mouse, window, position);
|
||||
}
|
||||
|
||||
// emit stationary update frames so Windows keeps the contact alive
|
||||
static void refresh_mouse_contact(HWND window) {
|
||||
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINT position {};
|
||||
if (!GetCursorPos(&position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINT transformed = position;
|
||||
if (!transform::mouse_to_game(window, &transformed)) {
|
||||
end_mouse_contact(window);
|
||||
return;
|
||||
}
|
||||
|
||||
update_contact(ContactOwner::Mouse, window, position);
|
||||
}
|
||||
|
||||
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param) {
|
||||
if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST &&
|
||||
is_mouse_message_from_touchscreen()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message == WM_TIMER &&
|
||||
w_param == reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token)) {
|
||||
refresh_mouse_contact(window);
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto primary_button = get_primary_mouse_button();
|
||||
if (message == primary_button.down_message ||
|
||||
message == primary_button.double_click_message) {
|
||||
begin_mouse_contact(window);
|
||||
} else if (message == WM_MOUSEMOVE) {
|
||||
move_mouse_contact(window, w_param, primary_button.state_mask);
|
||||
} else if (message == primary_button.up_message) {
|
||||
end_mouse_contact(window);
|
||||
} else if (message == WM_CANCELMODE || message == WM_KILLFOCUS ||
|
||||
message == WM_CAPTURECHANGED) {
|
||||
end_mouse_contact(window);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,175 +1,175 @@
|
||||
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
||||
#define _WIN32_WINNT 0x0602
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include "inject.h"
|
||||
#include "inject_internal.h"
|
||||
#include "settings.h"
|
||||
#include "transform.h"
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace nativetouch::inject {
|
||||
|
||||
constexpr UINT SYNTHETIC_CONTACT_TIMEOUT_MS = 100;
|
||||
|
||||
enum class SyntheticTouchMessage : WPARAM {
|
||||
Up, // used by callers releasing a contact
|
||||
DownGameSpace, // coordinates are relative to the game's logical touch surface
|
||||
DownScreenSpace, // coordinates are absolute pixels in Windows desktop coordinates
|
||||
};
|
||||
|
||||
static std::once_flag synthetic_initialization_once;
|
||||
static int synthetic_contact_timer_token;
|
||||
static UINT synthetic_touch_message;
|
||||
|
||||
void initialize_synthetic_touch() {
|
||||
std::call_once(synthetic_initialization_once, [] {
|
||||
synthetic_touch_message = RegisterWindowMessageW(L"spice2x.native_touch.inject");
|
||||
if (synthetic_touch_message == 0) {
|
||||
log_warning(
|
||||
"touch::native", "failed to register synthetic touch message: {}", GetLastError());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// synthetic touches preempt the mouse and keep it disabled until release or timeout
|
||||
static void begin_synthetic_contact(HWND window, POINT position, bool screen_space) {
|
||||
// remember when Windows-returned coordinates must map back into game space
|
||||
const auto transform_returned_coordinates =
|
||||
transform::is_tdj_dedicated_subscreen(window) ||
|
||||
settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
|
||||
if (!screen_space && !transform::game_to_screen(window, &position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto timer_id = reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token);
|
||||
|
||||
// when this producer already owns the contact, move it instead of releasing and
|
||||
// re-pressing so continuous input (such as the API surface) drags smoothly
|
||||
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
|
||||
if (update_contact(ContactOwner::Synthetic, window, position)) {
|
||||
// refresh the safety timeout while updates keep arriving
|
||||
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
||||
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!release_active_contact()) {
|
||||
return;
|
||||
}
|
||||
if (!begin_contact(
|
||||
ContactOwner::Synthetic,
|
||||
window,
|
||||
position,
|
||||
transform_returned_coordinates)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
||||
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
||||
} else {
|
||||
log_warning("touch::native", "failed to start synthetic touch timeout timer");
|
||||
}
|
||||
}
|
||||
|
||||
static void end_synthetic_contact(HWND window) {
|
||||
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
|
||||
release_active_contact();
|
||||
}
|
||||
}
|
||||
|
||||
bool handle_synthetic_message(
|
||||
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
|
||||
if (synthetic_touch_message != 0 && message == synthetic_touch_message) {
|
||||
POINT position { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) };
|
||||
switch (static_cast<SyntheticTouchMessage>(w_param)) {
|
||||
case SyntheticTouchMessage::DownGameSpace:
|
||||
begin_synthetic_contact(window, position, false);
|
||||
break;
|
||||
case SyntheticTouchMessage::DownScreenSpace:
|
||||
begin_synthetic_contact(window, position, true);
|
||||
break;
|
||||
default:
|
||||
end_synthetic_contact(window);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message == WM_TIMER &&
|
||||
w_param == reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token)) {
|
||||
end_synthetic_contact(window);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static HWND prepare_synthetic_touch() {
|
||||
if (!initialize_touch_injection()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto window = get_injection_window();
|
||||
if (window == nullptr || synthetic_touch_message == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
static bool post_synthetic_touch(
|
||||
HWND window, POINT position, SyntheticTouchMessage message) {
|
||||
return PostMessageW(
|
||||
window,
|
||||
synthetic_touch_message,
|
||||
static_cast<WPARAM>(message),
|
||||
MAKELPARAM(position.x, position.y)) != FALSE;
|
||||
}
|
||||
|
||||
// inject a point expressed in the game's synthetic touch coordinate space
|
||||
bool inject_synthetic_touch(POINT position, bool down) {
|
||||
const auto window = prepare_synthetic_touch();
|
||||
if (window == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto message = down
|
||||
? SyntheticTouchMessage::DownGameSpace
|
||||
: SyntheticTouchMessage::Up;
|
||||
return post_synthetic_touch(window, position, message);
|
||||
}
|
||||
|
||||
// map a logical canvas point onto the live injection window before injecting it
|
||||
bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down) {
|
||||
const auto window = prepare_synthetic_touch();
|
||||
if (window == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!down) {
|
||||
return post_synthetic_touch(window, position, SyntheticTouchMessage::Up);
|
||||
}
|
||||
|
||||
RECT client_rect {};
|
||||
if (canvas.cx <= 0 || canvas.cy <= 0 ||
|
||||
!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
position.x = MulDiv(position.x, client_rect.right, canvas.cx);
|
||||
position.y = MulDiv(position.y, client_rect.bottom, canvas.cy);
|
||||
if (!ClientToScreen(window, &position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return post_synthetic_touch(window, position, SyntheticTouchMessage::DownScreenSpace);
|
||||
}
|
||||
}
|
||||
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
||||
#define _WIN32_WINNT 0x0602
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include "inject.h"
|
||||
#include "inject_internal.h"
|
||||
#include "settings.h"
|
||||
#include "transform.h"
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace nativetouch::inject {
|
||||
|
||||
constexpr UINT SYNTHETIC_CONTACT_TIMEOUT_MS = 100;
|
||||
|
||||
enum class SyntheticTouchMessage : WPARAM {
|
||||
Up, // used by callers releasing a contact
|
||||
DownGameSpace, // coordinates are relative to the game's logical touch surface
|
||||
DownScreenSpace, // coordinates are absolute pixels in Windows desktop coordinates
|
||||
};
|
||||
|
||||
static std::once_flag synthetic_initialization_once;
|
||||
static int synthetic_contact_timer_token;
|
||||
static UINT synthetic_touch_message;
|
||||
|
||||
void initialize_synthetic_touch() {
|
||||
std::call_once(synthetic_initialization_once, [] {
|
||||
synthetic_touch_message = RegisterWindowMessageW(L"spice2x.native_touch.inject");
|
||||
if (synthetic_touch_message == 0) {
|
||||
log_warning(
|
||||
"touch::native", "failed to register synthetic touch message: {}", GetLastError());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// synthetic touches preempt the mouse and keep it disabled until release or timeout
|
||||
static void begin_synthetic_contact(HWND window, POINT position, bool screen_space) {
|
||||
// remember when Windows-returned coordinates must map back into game space
|
||||
const auto transform_returned_coordinates =
|
||||
transform::is_tdj_dedicated_subscreen(window) ||
|
||||
settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
|
||||
if (!screen_space && !transform::game_to_screen(window, &position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto timer_id = reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token);
|
||||
|
||||
// when this producer already owns the contact, move it instead of releasing and
|
||||
// re-pressing so continuous input (such as the API surface) drags smoothly
|
||||
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
|
||||
if (update_contact(ContactOwner::Synthetic, window, position)) {
|
||||
// refresh the safety timeout while updates keep arriving
|
||||
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
||||
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!release_active_contact()) {
|
||||
return;
|
||||
}
|
||||
if (!begin_contact(
|
||||
ContactOwner::Synthetic,
|
||||
window,
|
||||
position,
|
||||
transform_returned_coordinates)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
||||
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
||||
} else {
|
||||
log_warning("touch::native", "failed to start synthetic touch timeout timer");
|
||||
}
|
||||
}
|
||||
|
||||
static void end_synthetic_contact(HWND window) {
|
||||
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
|
||||
release_active_contact();
|
||||
}
|
||||
}
|
||||
|
||||
bool handle_synthetic_message(
|
||||
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
|
||||
if (synthetic_touch_message != 0 && message == synthetic_touch_message) {
|
||||
POINT position { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) };
|
||||
switch (static_cast<SyntheticTouchMessage>(w_param)) {
|
||||
case SyntheticTouchMessage::DownGameSpace:
|
||||
begin_synthetic_contact(window, position, false);
|
||||
break;
|
||||
case SyntheticTouchMessage::DownScreenSpace:
|
||||
begin_synthetic_contact(window, position, true);
|
||||
break;
|
||||
default:
|
||||
end_synthetic_contact(window);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message == WM_TIMER &&
|
||||
w_param == reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token)) {
|
||||
end_synthetic_contact(window);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static HWND prepare_synthetic_touch() {
|
||||
if (!initialize_touch_injection()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto window = get_injection_window();
|
||||
if (window == nullptr || synthetic_touch_message == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
static bool post_synthetic_touch(
|
||||
HWND window, POINT position, SyntheticTouchMessage message) {
|
||||
return PostMessageW(
|
||||
window,
|
||||
synthetic_touch_message,
|
||||
static_cast<WPARAM>(message),
|
||||
MAKELPARAM(position.x, position.y)) != FALSE;
|
||||
}
|
||||
|
||||
// inject a point expressed in the game's synthetic touch coordinate space
|
||||
bool inject_synthetic_touch(POINT position, bool down) {
|
||||
const auto window = prepare_synthetic_touch();
|
||||
if (window == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto message = down
|
||||
? SyntheticTouchMessage::DownGameSpace
|
||||
: SyntheticTouchMessage::Up;
|
||||
return post_synthetic_touch(window, position, message);
|
||||
}
|
||||
|
||||
// map a logical canvas point onto the live injection window before injecting it
|
||||
bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down) {
|
||||
const auto window = prepare_synthetic_touch();
|
||||
if (window == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!down) {
|
||||
return post_synthetic_touch(window, position, SyntheticTouchMessage::Up);
|
||||
}
|
||||
|
||||
RECT client_rect {};
|
||||
if (canvas.cx <= 0 || canvas.cy <= 0 ||
|
||||
!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
position.x = MulDiv(position.x, client_rect.right, canvas.cx);
|
||||
position.y = MulDiv(position.y, client_rect.bottom, canvas.cy);
|
||||
if (!ClientToScreen(window, &position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return post_synthetic_touch(window, position, SyntheticTouchMessage::DownScreenSpace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace nativetouch::settings {
|
||||
extern bool EMULATE_DIGITIZER;
|
||||
extern bool REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP;
|
||||
extern bool SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
|
||||
#pragma once
|
||||
|
||||
namespace nativetouch::settings {
|
||||
extern bool EMULATE_DIGITIZER;
|
||||
extern bool REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP;
|
||||
extern bool SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
|
||||
}
|
||||
@@ -1,215 +1,215 @@
|
||||
#include "transform.h"
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "settings.h"
|
||||
#include "touch/touch.h"
|
||||
|
||||
namespace nativetouch::transform {
|
||||
|
||||
static bool game_client_to_screen(HWND window, POINT *position) {
|
||||
RECT client_rect {};
|
||||
if (window == nullptr ||
|
||||
!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||
!PtInRect(&client_rect, *position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ClientToScreen(window, position) != FALSE;
|
||||
}
|
||||
|
||||
static bool screen_to_game_client(HWND window, POINT *position) {
|
||||
RECT client_rect {};
|
||||
if (window == nullptr ||
|
||||
!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||
!ScreenToClient(window, position) ||
|
||||
!PtInRect(&client_rect, *position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_tdj_dedicated_subscreen(HWND window) {
|
||||
return window != nullptr && GRAPHICS_WINDOWED && GRAPHICS_IIDX_WSUB &&
|
||||
window == TDJ_SUBSCREEN_WINDOW;
|
||||
}
|
||||
|
||||
// mouse-as-touch only applies while the cursor is over the target window
|
||||
static bool is_cursor_over_window(HWND window, POINT position) {
|
||||
return screen_to_game_client(window, &position);
|
||||
}
|
||||
|
||||
// convert game touch coordinates to Windows desktop coordinates
|
||||
bool game_to_screen(HWND window, POINT *position) {
|
||||
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
||||
return game_client_to_screen(window, position);
|
||||
}
|
||||
|
||||
if (!is_tdj_dedicated_subscreen(window)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
RECT client_rect {};
|
||||
if (!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
position->x = MulDiv(
|
||||
position->x - SPICETOUCH_TOUCH_X,
|
||||
client_rect.right,
|
||||
SPICETOUCH_TOUCH_WIDTH);
|
||||
position->y = MulDiv(
|
||||
position->y - SPICETOUCH_TOUCH_Y,
|
||||
client_rect.bottom,
|
||||
SPICETOUCH_TOUCH_HEIGHT);
|
||||
return ClientToScreen(window, position) != FALSE;
|
||||
}
|
||||
|
||||
static bool overlay_owns_touch_input() {
|
||||
// the arena SMALL window is the touch surface whenever it exists, so the
|
||||
// subscreen overlay must not claim touch input in those window modes
|
||||
if (graphics_gitadora_has_dedicated_subscreen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return overlay::OVERLAY != nullptr &&
|
||||
overlay::OVERLAY->get_active() &&
|
||||
overlay::OVERLAY->has_subscreen_touch_transform();
|
||||
}
|
||||
|
||||
static bool transform_overlay_touch_position(POINT *position) {
|
||||
// convert physical screen coordinates to the window-relative coordinates the overlay expects
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
position->x -= SPICETOUCH_TOUCH_X;
|
||||
position->y -= SPICETOUCH_TOUCH_Y;
|
||||
}
|
||||
|
||||
// ask the overlay to do the game-specific translation
|
||||
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
|
||||
}
|
||||
|
||||
// SDVX still expects portrait coordinates when its image is rendered in landscape:
|
||||
// (x, y) -> (width * (1 - y / height), height * x / width).
|
||||
bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto input_x = position->x;
|
||||
position->x = width - MulDiv(position->y, width, height);
|
||||
position->y = MulDiv(input_x, height, width);
|
||||
return true;
|
||||
}
|
||||
|
||||
// the digitizer is mapped to the zero-based primary display, so the contact is already
|
||||
// in the effective landscape resolution the rotation is based on
|
||||
static bool transform_sdvx_landscape_touch_position(POINT *position) {
|
||||
const auto landscape_width = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT);
|
||||
const auto landscape_height = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH);
|
||||
|
||||
return sdvx_landscape_rotate(position, landscape_width, landscape_height);
|
||||
}
|
||||
|
||||
// convert physical screen coordinates to game touch coordinates for a known target
|
||||
bool screen_to_game(HWND window, POINT *position) {
|
||||
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
||||
return screen_to_game_client(window, position);
|
||||
}
|
||||
|
||||
// scale the resized IIDX subscreen client area into the game's touch-display coordinates
|
||||
if (is_tdj_dedicated_subscreen(window)) {
|
||||
RECT client_rect {};
|
||||
if (!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ScreenToClient(window, position)) {
|
||||
return false;
|
||||
}
|
||||
if (!PtInRect(&client_rect, *position)) {
|
||||
return false;
|
||||
}
|
||||
position->x = SPICETOUCH_TOUCH_X +
|
||||
MulDiv(position->x, SPICETOUCH_TOUCH_WIDTH, client_rect.right);
|
||||
position->y = SPICETOUCH_TOUCH_Y +
|
||||
MulDiv(position->y, SPICETOUCH_TOUCH_HEIGHT, client_rect.bottom);
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if subscreen overlay is active and can transform the touch point;
|
||||
// if not, the touch point is valid as-is
|
||||
if (!overlay_owns_touch_input()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ask the overlay to transform the touch point into game coordinates
|
||||
return transform_overlay_touch_position(position);
|
||||
}
|
||||
|
||||
bool mouse_to_game(HWND window, POINT *position) {
|
||||
|
||||
// exception: iidx tdj dedicated subscreen window is allowed
|
||||
if (is_tdj_dedicated_subscreen(window)) {
|
||||
return screen_to_game(window, position);
|
||||
}
|
||||
|
||||
// exception: sdvx windowed subscreen does not use the subscreen overlay transform
|
||||
if (GRAPHICS_WINDOWED && window == SDVX_SUBSCREEN_WINDOW) {
|
||||
return is_cursor_over_window(window, *position);
|
||||
}
|
||||
|
||||
// exception: the arena SMALL window is the touch panel, so accept the mouse there
|
||||
// (and only there) with the coordinates a real contact on it would produce
|
||||
if (graphics_gitadora_has_dedicated_subscreen()) {
|
||||
return window == GFDM_SUBSCREEN_WINDOW &&
|
||||
is_cursor_over_window(window, *position);
|
||||
}
|
||||
|
||||
// if this game has a subscreen overlay that can transform touch input
|
||||
// but the window is hidden or not under the cursor, reject mouse-as-touch
|
||||
// (e.g., iidx/sdvx are rejected here, but nostalgia is allowed)
|
||||
if (overlay::OVERLAY != nullptr &&
|
||||
overlay::OVERLAY->has_subscreen_touch_transform() &&
|
||||
!overlay::OVERLAY->accepts_subscreen_mouse_input()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return screen_to_game(window, position);
|
||||
}
|
||||
|
||||
// route hardware screen coordinates through dedicated or overlay mapping and report the result
|
||||
Result hardware_to_game(POINT *position) {
|
||||
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
|
||||
const auto active_overlay = overlay_owns_touch_input();
|
||||
|
||||
// special case for SDVX landscape mode
|
||||
if (!dedicated_subscreen && !active_overlay &&
|
||||
GRAPHICS_FS_ORIENTATION_SWAP && avs::game::is_model("KFC")) {
|
||||
return transform_sdvx_landscape_touch_position(position) ?
|
||||
Result::Transformed : Result::Rejected;
|
||||
}
|
||||
|
||||
// no dedicated subscreen or active overlay mapping; pass the point through unchanged
|
||||
if (!dedicated_subscreen && !active_overlay) {
|
||||
return Result::Unchanged;
|
||||
}
|
||||
|
||||
// route through the dedicated subscreen when active, otherwise through the overlay
|
||||
const auto valid = screen_to_game(
|
||||
dedicated_subscreen ? TDJ_SUBSCREEN_WINDOW : nullptr,
|
||||
position);
|
||||
|
||||
// reject out-of-bounds points and any coordinate conversion failure
|
||||
return valid ? Result::Transformed : Result::Rejected;
|
||||
}
|
||||
}
|
||||
#include "transform.h"
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "settings.h"
|
||||
#include "touch/touch.h"
|
||||
|
||||
namespace nativetouch::transform {
|
||||
|
||||
static bool game_client_to_screen(HWND window, POINT *position) {
|
||||
RECT client_rect {};
|
||||
if (window == nullptr ||
|
||||
!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||
!PtInRect(&client_rect, *position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ClientToScreen(window, position) != FALSE;
|
||||
}
|
||||
|
||||
static bool screen_to_game_client(HWND window, POINT *position) {
|
||||
RECT client_rect {};
|
||||
if (window == nullptr ||
|
||||
!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||
!ScreenToClient(window, position) ||
|
||||
!PtInRect(&client_rect, *position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_tdj_dedicated_subscreen(HWND window) {
|
||||
return window != nullptr && GRAPHICS_WINDOWED && GRAPHICS_IIDX_WSUB &&
|
||||
window == TDJ_SUBSCREEN_WINDOW;
|
||||
}
|
||||
|
||||
// mouse-as-touch only applies while the cursor is over the target window
|
||||
static bool is_cursor_over_window(HWND window, POINT position) {
|
||||
return screen_to_game_client(window, &position);
|
||||
}
|
||||
|
||||
// convert game touch coordinates to Windows desktop coordinates
|
||||
bool game_to_screen(HWND window, POINT *position) {
|
||||
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
||||
return game_client_to_screen(window, position);
|
||||
}
|
||||
|
||||
if (!is_tdj_dedicated_subscreen(window)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
RECT client_rect {};
|
||||
if (!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
position->x = MulDiv(
|
||||
position->x - SPICETOUCH_TOUCH_X,
|
||||
client_rect.right,
|
||||
SPICETOUCH_TOUCH_WIDTH);
|
||||
position->y = MulDiv(
|
||||
position->y - SPICETOUCH_TOUCH_Y,
|
||||
client_rect.bottom,
|
||||
SPICETOUCH_TOUCH_HEIGHT);
|
||||
return ClientToScreen(window, position) != FALSE;
|
||||
}
|
||||
|
||||
static bool overlay_owns_touch_input() {
|
||||
// the arena SMALL window is the touch surface whenever it exists, so the
|
||||
// subscreen overlay must not claim touch input in those window modes
|
||||
if (graphics_gitadora_has_dedicated_subscreen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return overlay::OVERLAY != nullptr &&
|
||||
overlay::OVERLAY->get_active() &&
|
||||
overlay::OVERLAY->has_subscreen_touch_transform();
|
||||
}
|
||||
|
||||
static bool transform_overlay_touch_position(POINT *position) {
|
||||
// convert physical screen coordinates to the window-relative coordinates the overlay expects
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
position->x -= SPICETOUCH_TOUCH_X;
|
||||
position->y -= SPICETOUCH_TOUCH_Y;
|
||||
}
|
||||
|
||||
// ask the overlay to do the game-specific translation
|
||||
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
|
||||
}
|
||||
|
||||
// SDVX still expects portrait coordinates when its image is rendered in landscape:
|
||||
// (x, y) -> (width * (1 - y / height), height * x / width).
|
||||
bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto input_x = position->x;
|
||||
position->x = width - MulDiv(position->y, width, height);
|
||||
position->y = MulDiv(input_x, height, width);
|
||||
return true;
|
||||
}
|
||||
|
||||
// the digitizer is mapped to the zero-based primary display, so the contact is already
|
||||
// in the effective landscape resolution the rotation is based on
|
||||
static bool transform_sdvx_landscape_touch_position(POINT *position) {
|
||||
const auto landscape_width = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT);
|
||||
const auto landscape_height = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH);
|
||||
|
||||
return sdvx_landscape_rotate(position, landscape_width, landscape_height);
|
||||
}
|
||||
|
||||
// convert physical screen coordinates to game touch coordinates for a known target
|
||||
bool screen_to_game(HWND window, POINT *position) {
|
||||
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
||||
return screen_to_game_client(window, position);
|
||||
}
|
||||
|
||||
// scale the resized IIDX subscreen client area into the game's touch-display coordinates
|
||||
if (is_tdj_dedicated_subscreen(window)) {
|
||||
RECT client_rect {};
|
||||
if (!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ScreenToClient(window, position)) {
|
||||
return false;
|
||||
}
|
||||
if (!PtInRect(&client_rect, *position)) {
|
||||
return false;
|
||||
}
|
||||
position->x = SPICETOUCH_TOUCH_X +
|
||||
MulDiv(position->x, SPICETOUCH_TOUCH_WIDTH, client_rect.right);
|
||||
position->y = SPICETOUCH_TOUCH_Y +
|
||||
MulDiv(position->y, SPICETOUCH_TOUCH_HEIGHT, client_rect.bottom);
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if subscreen overlay is active and can transform the touch point;
|
||||
// if not, the touch point is valid as-is
|
||||
if (!overlay_owns_touch_input()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ask the overlay to transform the touch point into game coordinates
|
||||
return transform_overlay_touch_position(position);
|
||||
}
|
||||
|
||||
bool mouse_to_game(HWND window, POINT *position) {
|
||||
|
||||
// exception: iidx tdj dedicated subscreen window is allowed
|
||||
if (is_tdj_dedicated_subscreen(window)) {
|
||||
return screen_to_game(window, position);
|
||||
}
|
||||
|
||||
// exception: sdvx windowed subscreen does not use the subscreen overlay transform
|
||||
if (GRAPHICS_WINDOWED && window == SDVX_SUBSCREEN_WINDOW) {
|
||||
return is_cursor_over_window(window, *position);
|
||||
}
|
||||
|
||||
// exception: the arena SMALL window is the touch panel, so accept the mouse there
|
||||
// (and only there) with the coordinates a real contact on it would produce
|
||||
if (graphics_gitadora_has_dedicated_subscreen()) {
|
||||
return window == GFDM_SUBSCREEN_WINDOW &&
|
||||
is_cursor_over_window(window, *position);
|
||||
}
|
||||
|
||||
// if this game has a subscreen overlay that can transform touch input
|
||||
// but the window is hidden or not under the cursor, reject mouse-as-touch
|
||||
// (e.g., iidx/sdvx are rejected here, but nostalgia is allowed)
|
||||
if (overlay::OVERLAY != nullptr &&
|
||||
overlay::OVERLAY->has_subscreen_touch_transform() &&
|
||||
!overlay::OVERLAY->accepts_subscreen_mouse_input()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return screen_to_game(window, position);
|
||||
}
|
||||
|
||||
// route hardware screen coordinates through dedicated or overlay mapping and report the result
|
||||
Result hardware_to_game(POINT *position) {
|
||||
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
|
||||
const auto active_overlay = overlay_owns_touch_input();
|
||||
|
||||
// special case for SDVX landscape mode
|
||||
if (!dedicated_subscreen && !active_overlay &&
|
||||
GRAPHICS_FS_ORIENTATION_SWAP && avs::game::is_model("KFC")) {
|
||||
return transform_sdvx_landscape_touch_position(position) ?
|
||||
Result::Transformed : Result::Rejected;
|
||||
}
|
||||
|
||||
// no dedicated subscreen or active overlay mapping; pass the point through unchanged
|
||||
if (!dedicated_subscreen && !active_overlay) {
|
||||
return Result::Unchanged;
|
||||
}
|
||||
|
||||
// route through the dedicated subscreen when active, otherwise through the overlay
|
||||
const auto valid = screen_to_game(
|
||||
dedicated_subscreen ? TDJ_SUBSCREEN_WINDOW : nullptr,
|
||||
position);
|
||||
|
||||
// reject out-of-bounds points and any coordinate conversion failure
|
||||
return valid ? Result::Transformed : Result::Rejected;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace nativetouch::transform {
|
||||
enum class Result {
|
||||
Unchanged,
|
||||
Transformed,
|
||||
Rejected,
|
||||
};
|
||||
|
||||
bool is_tdj_dedicated_subscreen(HWND window);
|
||||
bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height);
|
||||
bool game_to_screen(HWND window, POINT *position);
|
||||
bool screen_to_game(HWND window, POINT *position);
|
||||
bool mouse_to_game(HWND window, POINT *position);
|
||||
Result hardware_to_game(POINT *position);
|
||||
}
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace nativetouch::transform {
|
||||
enum class Result {
|
||||
Unchanged,
|
||||
Transformed,
|
||||
Rejected,
|
||||
};
|
||||
|
||||
bool is_tdj_dedicated_subscreen(HWND window);
|
||||
bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height);
|
||||
bool game_to_screen(HWND window, POINT *position);
|
||||
bool screen_to_game(HWND window, POINT *position);
|
||||
bool mouse_to_game(HWND window, POINT *position);
|
||||
Result hardware_to_game(POINT *position);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user