Files
spice2x-r3d/src/spice2x/touch/touch.cpp
T
bicarusandGitHub 7ccd938f9b overlay: mouse-as-touch needs to check if subscreen window is active (#842)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #840 

## Description of change
Currently, for subscreen games, mouse events are still delivered even
when the subscreen overlay window is not visible.

Change this so that the subscreen overlay must be active and under the
mouse cursor for the mouse-to-touch transformation to occur. Applies to
both native and wintouchemu.

## Testing
Needs to test everything again..

iidx:

- [ ]  full screen with overlay
- [ ]  single window with overlay
- [ ]  two window

Test: mouse, poke, api, real touch screen

sdvx:

- [ ] full screen with overlay
- [ ] windowed

popn

- [x] full screen with overlay
- [x] window with overlay
- [x] dedicated window

gitadora

- [x] single window with overlay
- [x] dedicated sub window

nostalgia

- [x] fullscreen
- [x] windowed

test: poke

wintouchemu
- [ ] Do all of the above again with wintouchemu
2026-07-28 01:14:47 -07:00

1009 lines
31 KiB
C++

// enable touch functions - set version to windows 7
// mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601
#include <thread>
#include <mutex>
#include <windowsx.h>
#include "touch.h"
#include "avs/game.h"
#include "external/imgui/imgui.h"
#include "games/jb/jb.h"
#include "hooks/graphics/graphics.h"
#include "misc/eamuse.h"
#include "overlay/overlay.h"
#include "rawinput/touch.h"
#include "util/circular_buffer.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
#include "gdi_overlay.h"
#include "handler.h"
#include "touch_gestures.h"
#include "win7.h"
#include "win8.h"
// constants
const char *SPICETOUCH_CLASS_NAME = "spiceTouchClass";
const char *SPICETOUCH_FONT_NAME = "Courier New";
const char *INSERT_CARD_TEXT = "Insert Card";
// settings
static const int TOUCH_EVENT_BUFFER_SIZE = 1024 * 4;
static const int TOUCH_EVENT_BUFFER_THRESHOLD1 = 1024 * 2;
static const int TOUCH_EVENT_BUFFER_THRESHOLD2 = 1024 * 3;
// timer id for the overlay repaint tick
static const UINT_PTR SPICETOUCH_OVERLAY_TIMER_ID = 1;
// overlay repaint interval; the WinXP-compat build stays at 30 FPS
#if !SPICE_XP
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 60;
#else
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 30;
#endif // !SPICE_XP
// in mainline spicetools, this was false (show by default)
// in spice2x, this is true (hide by default)
bool SPICETOUCH_CARD_DISABLE = true;
HWND SPICETOUCH_TOUCH_HWND = nullptr;
int SPICETOUCH_TOUCH_X = 0;
int SPICETOUCH_TOUCH_Y = 0;
int SPICETOUCH_TOUCH_WIDTH = 0;
int SPICETOUCH_TOUCH_HEIGHT = 0;
// touch states
std::vector<TouchPoint> TOUCH_POINTS;
std::mutex TOUCH_POINTS_M;
static circular_buffer<TouchEvent> TOUCH_EVENTS(TOUCH_EVENT_BUFFER_SIZE);
std::mutex TOUCH_EVENTS_M;
// general states
static bool SPICETOUCH_INITIALIZED = false;
static bool SPICETOUCH_ATTACHED = false;
static bool SPICETOUCH_ATTACHED_DXHOOK = false;
static HWND SPICETOUCH_HWND = nullptr;
static WNDPROC SPICETOUCH_OLD_PROC = nullptr;
static bool SPICETOUCH_ENABLE_MOUSE = true;
static bool SPICETOUCH_REGISTERED_TOUCH = false;
static bool SPICETOUCH_CALL_OLD_PROC = false;
static std::thread *SPICETOUCH_TOUCH_THREAD = nullptr;
static HFONT SPICETOUCH_FONT;
static RECT SPICETOUCH_CARD_RECT;
static bool SPICETOUCH_CARD_ENABLED = false;
static const char *LOG_MODULE_NAME = "touch";
static TouchHandler *TOUCH_HANDLER = nullptr;
TouchHandler::TouchHandler(std::string name) {
log_info("touch", "Using touch handler: {}", name);
}
class RawInputTouchHandler : public TouchHandler {
public:
RawInputTouchHandler() : TouchHandler("rawinput") {
}
virtual bool window_register(HWND hWnd) override {
return true;
}
virtual bool window_unregister(HWND hWnd) override {
return true;
}
virtual void handle_message(msg_handler_result&, HWND, UINT, WPARAM, LPARAM) override {
}
};
/*
* Add touch event but take care of buffer size
* Be careful, this doesn't lock the mutex on it's own
*/
void add_touch_event(TouchEvent *te) {
// check if first threshold is passed
if (TOUCH_EVENTS.size() > TOUCH_EVENT_BUFFER_THRESHOLD1) {
switch (te->type) {
case TOUCH_DOWN:
// ignore touch down events after first threshold
return;
case TOUCH_MOVE:
// add move event if we're not over the second threshold
if (TOUCH_EVENTS.size() <= TOUCH_EVENT_BUFFER_THRESHOLD2) {
TOUCH_EVENTS.put(*te);
}
return;
case TOUCH_UP:
// check if buffer is full
if (TOUCH_EVENTS.full()) {
// ignore event
return;
}
// when the buffer isn't full yet, add the touch up event
TOUCH_EVENTS.put(*te);
return;
default:
return;
}
}
// add the touch up event
TOUCH_EVENTS.put(*te);
}
static void touch_initialize() {
// check if already initialized
if (SPICETOUCH_INITIALIZED) {
return;
}
SPICETOUCH_INITIALIZED = true;
if (!RI_MGR) {
log_fatal(
LOG_MODULE_NAME,
"touch_initialize() - RI Manager not available, called too early! "
"This is a BUG in spice, please file a bug report with log.txt.");
}
// initialize handler
if (rawinput::touch::is_enabled(RI_MGR.get())) {
TOUCH_HANDLER = new RawInputTouchHandler();
} else if (Win8Handler::is_available()) {
TOUCH_HANDLER = new Win8Handler();
} else if (Win7Handler::is_available()) {
TOUCH_HANDLER = new Win7Handler();
} else {
log_warning(LOG_MODULE_NAME, "no touch handler available");
}
}
static inline void touch_register_window(HWND hWnd) {
// register touch handling for window
if (TOUCH_HANDLER != nullptr) {
TOUCH_HANDLER->window_register(hWnd);
}
}
static inline void touch_unregister_window(HWND hWnd) {
// unregister touch handling for window
if (TOUCH_HANDLER != nullptr) {
TOUCH_HANDLER->window_unregister(hWnd);
}
}
bool is_touch_available(LPCSTR caller) {
static bool called_once = false;
if (!RI_MGR) {
log_fatal(
LOG_MODULE_NAME,
"is_touch_available({}) - RI Manager not available, called too early! "
"This is a BUG in spice, please file a bug report with log.txt.",
caller);
}
// initialize touch handler
touch_initialize();
if (!called_once) {
log_misc("touch", "is_touch_available called by: {}, returning {}",
caller, (TOUCH_HANDLER != nullptr) ? "TRUE" : "false");
called_once = true;
}
return TOUCH_HANDLER != nullptr;
}
void update_card_button() {
// check if enabled
if (!SPICETOUCH_CARD_ENABLED) {
return;
}
// check touch points
for (TouchPoint touchPoint : TOUCH_POINTS) {
POINT pt {};
pt.x = touchPoint.x;
pt.y = touchPoint.y;
if (PtInRect(&SPICETOUCH_CARD_RECT, pt) != 0) {
eamuse_card_insert(0);
}
}
}
static void release_all_mouse_touch_points() {
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
for (size_t x = 0; x < TOUCH_POINTS.size();) {
TouchPoint *tp = &TOUCH_POINTS[x];
if (tp->id == 0u) {
TouchEvent te {
.id = tp->id,
.x = tp->x,
.y = tp->y,
.type = TOUCH_UP,
.mouse = tp->mouse,
};
add_touch_event(&te);
TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
} else {
x++;
}
}
}
static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
// check if touch was registered
if (!SPICETOUCH_REGISTERED_TOUCH) {
SPICETOUCH_REGISTERED_TOUCH = true;
// register the handler when a touch screen is present
if (is_touch_available("SpiceTouchWndProc")) {
TOUCH_HANDLER->window_register(hWnd);
}
// enable the card button whenever the option is set, even without a touch
// screen (mouse clicks are handled as touch input and can trigger it)
if (!SPICETOUCH_CARD_DISABLE) {
SPICETOUCH_CARD_ENABLED = true;
}
}
// update rawinput touch display resolution
if (msg == WM_DISPLAYCHANGE) {
log_info("touch", "detected display mode change");
rawinput::touch::display_update();
}
const auto is_windowed_sub =
(GRAPHICS_IIDX_WSUB && hWnd == TDJ_SUBSCREEN_WINDOW) ||
(hWnd == SDVX_SUBSCREEN_WINDOW) ||
(hWnd == POPN_SUBSCREEN_WINDOW) ||
(hWnd == GFDM_SUBSCREEN_WINDOW);
if (msg == WM_CLOSE) {
if (is_windowed_sub) {
log_misc("touch", "ignore WM_CLOSE for subscreen window");
return false;
}
}
// check messages for dedicated window
if (SPICETOUCH_TOUCH_THREAD != nullptr) {
// check if overlay is enabled
auto overlay_enabled = overlay::OVERLAY
&& overlay::OVERLAY->renderer == overlay::OverlayRenderer::SOFTWARE;
// we don't want mouse events to interfere
SPICETOUCH_ENABLE_MOUSE = !overlay_enabled || !overlay::OVERLAY->get_active();
switch (msg) {
case WM_MOUSEACTIVATE: {
// Set the main window as the active window to reactivate the imgui cursor
HWND parent = GetParent(hWnd);
if (GetActiveWindow() != parent) {
SetActiveWindow(parent);
}
return MA_NOACTIVATE;
}
case WM_SETCURSOR: {
// set cursor back to the overlay one
if (overlay_enabled && LOWORD(lParam) == HTCLIENT && overlay::OVERLAY->update_cursor()) {
//return true; TODO: can make cursor invisible?
}
break;
}
case WM_TIMER: {
// request a repaint; the frame is composed into an offscreen buffer, so no
// background erase is needed (bErase = FALSE avoids a transparent flash)
InvalidateRect(hWnd, NULL, FALSE);
break;
}
case WM_PAINT: {
// get window rect
HWND parent = GetParent(hWnd);
RECT windowRect {}, clientRect {};
GetWindowRect(parent, &windowRect);
GetClientRect(parent, &clientRect);
// adjust to client area
POINT p1 {.x = clientRect.left, .y = clientRect.top};
POINT p2 {.x = clientRect.right, .y = clientRect.bottom};
ClientToScreen(parent, &p1);
ClientToScreen(parent, &p2);
windowRect.left = p1.x;
windowRect.top = p1.y;
windowRect.right = p2.x;
windowRect.bottom = p2.y;
// check if rect needs to update
RECT windowRectOld {};
GetWindowRect(hWnd, &windowRectOld);
if (memcmp(&windowRectOld, &windowRect, sizeof(RECT)) != 0) {
SetWindowPos(hWnd, HWND_TOP,
windowRect.left, windowRect.top,
windowRect.right - windowRect.left,
windowRect.bottom - windowRect.top,
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
}
// render the software overlay before BeginPaint so only GDI composition
// and the final blit happen while the window is being painted
int overlay_width = 0, overlay_height = 0;
uint32_t *overlay_pixels = nullptr;
bool overlay_pixels_dirty = false;
if (overlay_enabled) {
overlay::OVERLAY->update();
overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
overlay_pixels = overlay::OVERLAY.get()->sw_get_pixel_data(
&overlay_width, &overlay_height);
overlay_pixels_dirty = overlay::OVERLAY->sw_pixels_dirty;
}
bool overlay_active = overlay_enabled && overlay::OVERLAY->get_active();
// compose the whole frame into an offscreen back buffer and present it with a
// single blit; the window never shows a half-erased (transparent) surface
// mid-paint, which is what caused the occasional flicker at higher frame rates
PAINTSTRUCT paint {};
HDC hdc = BeginPaint(hWnd, &paint);
RECT bufferRect {};
GetClientRect(hWnd, &bufferRect);
int buffer_width = bufferRect.right - bufferRect.left;
int buffer_height = bufferRect.bottom - bufferRect.top;
HBRUSH color_key_brush =
(HBRUSH) GetClassLongPtr(hWnd, GCLP_HBRBACKGROUND);
HDC draw_dc = touch_gdi_overlay_begin_frame(
hdc,
color_key_brush,
buffer_width,
buffer_height,
overlay_pixels,
overlay_pixels_dirty,
overlay_width,
overlay_height);
if (draw_dc == nullptr) {
EndPaint(hWnd, &paint);
return 0;
}
// draw the insert-card button below the jubeat debug overlay; it is
// hidden while the overlay is active
if (SPICETOUCH_CARD_ENABLED && SPICETOUCH_FONT != nullptr && !overlay_active) {
// create brushes
HBRUSH brushBorder = CreateSolidBrush(RGB(0, 196, 0));
HBRUSH brushFill = CreateSolidBrush(RGB(255, 192, 203));
// get window rect
RECT windowRect {};
GetWindowRect(hWnd, &windowRect);
bool should_rotate = avs::game::is_model({ "J44", "K44" });
// create box rect
RECT boxRect {};
if (should_rotate) {
boxRect.left = windowRect.right - 75;
boxRect.top = 20;
boxRect.right = windowRect.right - 44;
boxRect.bottom = 151;
} else {
boxRect.left = 20;
boxRect.top = 44;
boxRect.right = 141;
boxRect.bottom = 75;
}
// save box rect for touch input
SPICETOUCH_CARD_RECT = boxRect;
// draw borders
FillRect(draw_dc, &boxRect, brushBorder);
// modify box rect
boxRect.left += 1;
boxRect.top += 1;
boxRect.right -= 1;
boxRect.bottom -= 1;
// fill box
FillRect(draw_dc, &boxRect, brushFill);
// modify box rect
if (should_rotate) {
boxRect.left += 5 + 20;
boxRect.top += 5;
} else {
boxRect.left += 5;
boxRect.top += 5;
}
// draw text
SelectObject(draw_dc, SPICETOUCH_FONT);
SetTextColor(draw_dc, RGB(0, 196, 0));
DrawText(draw_dc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);
// delete objects
DeleteObject(brushFill);
DeleteObject(brushBorder);
}
#if !SPICE_XP
// draw the jubeat debug overlay on top (hidden while the overlay is active)
if (overlay_enabled && !overlay_active && games::jb::touch_debug_overlay_enabled()) {
games::jb::touch_draw_debug_overlay(draw_dc);
}
#endif // !SPICE_XP
// present the composed frame in a single blit
touch_gdi_overlay_present(hdc);
EndPaint(hWnd, &paint);
return 0;
}
case WM_CREATE: {
// set to layered window
SetWindowLong(hWnd, GWL_EXSTYLE, GetWindowLong(hWnd, GWL_EXSTYLE) | WS_EX_LAYERED);
// set alpha value
SetLayeredWindowAttributes(hWnd, 0, 0xFFu, LWA_ALPHA);
SetLayeredWindowAttributes(hWnd, RGB(255, 192, 203), 0, LWA_COLORKEY);
// get window rect
RECT windowRect {};
GetWindowRect(hWnd, &windowRect);
bool should_rotate = avs::game::is_model({ "J44", "K44" });
auto rotation = should_rotate ? 2700 : 0;
// load font
SPICETOUCH_FONT = CreateFont(20, 0, rotation, rotation, FW_NORMAL, FALSE, FALSE, FALSE,
ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
NONANTIALIASED_QUALITY, DEFAULT_PITCH,
SPICETOUCH_FONT_NAME);
if (SPICETOUCH_FONT == nullptr) {
log_warning(LOG_MODULE_NAME, "font '{}' could not be loaded", SPICETOUCH_FONT_NAME);
}
return 0;
}
case WM_DESTROY: {
touch_gdi_overlay_release();
if (SPICETOUCH_FONT != nullptr) {
DeleteObject(SPICETOUCH_FONT);
SPICETOUCH_FONT = nullptr;
}
PostQuitMessage(0);
return 0;
}
default:
break;
}
}
msg_handler_result result {
.action = ACTION_PASS,
.return_value = 0,
};
// check if imgui is handling this mouse event
if (is_windowed_sub) {
// do nothing, don't let imgui hijack clicks on the sub window
result.action = ACTION_PASS;
} else if (overlay::OVERLAY != nullptr && overlay::OVERLAY->get_active() && ImGui::GetIO().WantCaptureMouse) {
result.action = ACTION_RETURN_DEFAULT;
} else if (TOUCH_HANDLER != nullptr) {
// call touch handler
TOUCH_HANDLER->handle_message(result, hWnd, msg, wParam, lParam);
}
if (result.action == ACTION_RETURN_STORED) {
// return the value from the touch handler
return result.return_value;
}
if (result.action == ACTION_PASS) {
// parse mouse messages
switch (msg) {
case WM_LBUTTONDOWN: {
// check if mouse is enabled
if (SPICETOUCH_ENABLE_MOUSE) {
if (is_mouse_message_from_touchscreen()) {
return 0;
}
// subscribe to mouse messages even when the cursor leaves the window
SetCapture(hWnd);
// release all old events before inserting a new one
release_all_mouse_touch_points();
// lock touch points
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
// create touch point
TouchPoint tp {
.id = 0,
.x = GET_X_LPARAM(lParam),
.y = GET_Y_LPARAM(lParam),
.mouse = true,
.down_ms = get_performance_milliseconds(),
};
TOUCH_POINTS.push_back(tp);
// add touch down event
TouchEvent te {
.id = tp.id,
.x = tp.x,
.y = tp.y,
.type = TOUCH_DOWN,
.mouse = tp.mouse,
};
add_touch_event(&te);
// card button
update_card_button();
}
break;
}
case WM_MOUSEMOVE: {
// check if mouse is enabled
if (SPICETOUCH_ENABLE_MOUSE) {
if (is_mouse_message_from_touchscreen()) {
return 0;
}
// lock touch points
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
// update point
for (auto &x : TOUCH_POINTS) {
TouchPoint *tp = &x;
// find ID 0
if (tp->id == 0u) {
// update touch point position
tp->x = GET_X_LPARAM(lParam);
tp->y = GET_Y_LPARAM(lParam);
// add touch move event
TouchEvent te {
.id = tp->id,
.x = tp->x,
.y = tp->y,
.type = TOUCH_MOVE,
.mouse = tp->mouse,
};
add_touch_event(&te);
break;
}
}
}
break;
}
case WM_LBUTTONUP: {
// check if mouse is enabled
if (SPICETOUCH_ENABLE_MOUSE) {
if (is_mouse_message_from_touchscreen()) {
return 0;
}
release_all_mouse_touch_points();
if (GetCapture() == hWnd) {
ReleaseCapture();
}
}
break;
}
case WM_CAPTURECHANGED:
case WM_CANCELMODE: {
// to deal with window losing the capture after SetCapture
release_all_mouse_touch_points();
if (msg == WM_CANCELMODE && GetCapture() == hWnd) {
ReleaseCapture();
}
break;
}
default:
// call original function
if (SPICETOUCH_CALL_OLD_PROC && SPICETOUCH_OLD_PROC != nullptr) {
return SPICETOUCH_OLD_PROC(hWnd, msg, wParam, lParam);
}
}
}
// clean up
if (SPICETOUCH_ATTACHED_DXHOOK) {
return 0;
} else {
return DefWindowProc(hWnd, msg, wParam, lParam);
}
}
void touch_attach_wnd(HWND hWnd) {
// check if already attached
if (SPICETOUCH_ATTACHED) {
return;
}
SPICETOUCH_ATTACHED = true;
// initialize touch handler
touch_initialize();
touch_register_window(hWnd);
// hook window process
SPICETOUCH_HWND = hWnd;
SPICETOUCH_OLD_PROC = (WNDPROC) SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR) SpiceTouchWndProc);
// update rawinput
rawinput::touch::display_update();
}
void touch_attach_dx_hook() {
// check if already attached
if (SPICETOUCH_ATTACHED) {
return;
}
SPICETOUCH_ATTACHED = true;
SPICETOUCH_ATTACHED_DXHOOK = true;
// initialize touch handler
touch_initialize();
log_info("touch", "touch_attach_dx_hook: attaching SpiceTouchWndProc...");
// add dx hook
graphics_add_wnd_proc(SpiceTouchWndProc);
// update rawinput
rawinput::touch::display_update();
}
void touch_create_wnd(HWND hWnd, bool overlay) {
// check if already attached
if (SPICETOUCH_ATTACHED) {
return;
}
SPICETOUCH_ATTACHED = true;
// initialize touch handler
touch_initialize();
log_info("touch", "touch_create_wnd: creating SPICETOUCH_TOUCH_THREAD...");
// create thread
SPICETOUCH_TOUCH_THREAD = new std::thread([hWnd, overlay]() {
// create class
WNDCLASSEX wndClass {};
wndClass.cbSize = sizeof(WNDCLASSEX);
wndClass.style = 3;
wndClass.lpfnWndProc = SpiceTouchWndProc;
wndClass.cbClsExtra = 0;
wndClass.cbWndExtra = 0;
wndClass.hInstance = GetModuleHandle(nullptr);
wndClass.hIcon = nullptr;
wndClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
wndClass.hbrBackground = CreateSolidBrush(RGB(255, 192, 203));
wndClass.lpszMenuName = nullptr;
wndClass.lpszClassName = SPICETOUCH_CLASS_NAME;
wndClass.hIconSm = nullptr;
// register class
if (!RegisterClassExA(&wndClass)) {
log_warning(LOG_MODULE_NAME, "failed to register SpiceTouch class: {}", GetLastError());
return;
}
// calculate touch window dimensions
update_spicetouch_window_dimensions(hWnd);
// create window
HWND touch_window = CreateWindowExA(
0,
SPICETOUCH_CLASS_NAME,
"SpiceTools Touch",
(DWORD) CW_USEDEFAULT,
SPICETOUCH_TOUCH_X,
SPICETOUCH_TOUCH_Y,
SPICETOUCH_TOUCH_WIDTH,
SPICETOUCH_TOUCH_HEIGHT,
hWnd,
nullptr,
GetModuleHandle(nullptr),
nullptr
);
log_misc(
LOG_MODULE_NAME, "create SpiceTouch window ({}x{} @ {}, {})",
SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT, SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y);
// check window
if (touch_window == nullptr) {
log_warning(LOG_MODULE_NAME, "failed to create SpiceTouch window: {}", GetLastError());
return;
}
// save reference
SPICETOUCH_TOUCH_HWND = touch_window;
// window settings
ShowWindow(touch_window, SW_SHOWNOACTIVATE);
UpdateWindow(touch_window);
// disable the OS touch contact visualization for our own touch window
disable_touch_gestures(touch_window);
// register
touch_register_window(touch_window);
// overlay
if (overlay && overlay::ENABLED) {
if (overlay::OVERLAY) {
log_warning(LOG_MODULE_NAME, "requested overlay, but already existing");
} else {
// create instance
overlay::OVERLAY.reset(new overlay::SpiceOverlay(touch_window));
// draw overlay repaint timer (30 FPS on WinXP, 60 FPS otherwise)
SetTimer(touch_window, SPICETOUCH_OVERLAY_TIMER_ID, SPICETOUCH_OVERLAY_TIMER_MS, NULL);
}
}
// window loop
MSG msg {};
while (GetMessage(&msg, nullptr, 0, 0) && SPICETOUCH_TOUCH_THREAD != nullptr) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// kill overlay
if (overlay) {
overlay::OVERLAY.reset();
}
// unregister
touch_unregister_window(touch_window);
log_misc(LOG_MODULE_NAME, "window closed");
});
// update rawinput
rawinput::touch::display_update();
}
void touch_detach() {
// remove window process hook
if (SPICETOUCH_HWND != nullptr) {
touch_unregister_window(SPICETOUCH_HWND);
SetWindowLongPtr(SPICETOUCH_HWND, GWLP_WNDPROC, (LONG_PTR) SPICETOUCH_OLD_PROC);
SPICETOUCH_HWND = nullptr;
SPICETOUCH_OLD_PROC = nullptr;
}
// remove dx hook
graphics_remove_wnd_proc(SpiceTouchWndProc);
SPICETOUCH_TOUCH_THREAD = nullptr;
SPICETOUCH_ATTACHED_DXHOOK = false;
SPICETOUCH_ATTACHED = false;
}
void touch_write_points(std::vector<TouchPoint> *touch_points) {
// check size first
if (touch_points->empty()) {
return;
}
// lock
std::lock_guard<std::mutex> points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> events(TOUCH_EVENTS_M);
// iterate through all the provided touch points
for (auto &tp : *touch_points) {
// find touch point to update
bool found = false;
for (auto &arr_tp : TOUCH_POINTS) {
if (arr_tp.id == tp.id) {
// mark as found
found = true;
// update position
arr_tp.x = tp.x;
arr_tp.y = tp.y;
// add touch move event
TouchEvent te {
.id = tp.id,
.x = tp.x,
.y = tp.y,
.type = TOUCH_MOVE,
.mouse = tp.mouse,
};
add_touch_event(&te);
}
}
// create new touch point when not found
if (!found) {
// stamp the landing time so debounce can measure the contact's age
tp.down_ms = get_performance_milliseconds();
// add touch point
TOUCH_POINTS.push_back(tp);
// add touch down event
TouchEvent te {
.id = tp.id,
.x = tp.x,
.y = tp.y,
.type = TOUCH_DOWN,
.mouse = tp.mouse,
};
add_touch_event(&te);
}
}
}
void touch_remove_points(std::vector<DWORD> *touch_point_ids) {
// check size first
if (touch_point_ids->empty()) {
return;
}
// lock
std::lock_guard<std::mutex> points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> events(TOUCH_EVENTS_M);
// find the touch points to remove
for (auto id : *touch_point_ids) {
for (size_t x = 0; x < TOUCH_POINTS.size(); x++) {
TouchPoint *tp = &TOUCH_POINTS[x];
// check if the IDs match
if (tp->id == id) {
// add touch up event
TouchEvent te {
.id = id,
.x = tp->x,
.y = tp->y,
.type = TOUCH_UP,
.mouse = tp->mouse,
};
add_touch_event(&te);
// delete touch point
TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
break;
}
}
}
}
void touch_get_points(std::vector<TouchPoint> &touch_points, bool overlay) {
// update timeouts
if (RI_MGR) {
rawinput::touch::update_timeouts(RI_MGR.get());
}
// overlay override
if (!overlay &&
overlay::OVERLAY &&
overlay::OVERLAY->get_active() &&
!overlay::OVERLAY->has_subscreen_touch_transform() &&
ImGui::GetIO().WantCaptureMouse) {
return;
}
// lock
std::lock_guard<std::mutex> lock(TOUCH_POINTS_M);
// append touch points
touch_points.insert(touch_points.end(), TOUCH_POINTS.begin(), TOUCH_POINTS.end());
}
void touch_get_events(std::vector<TouchEvent> &touch_events, bool overlay) {
// update timeouts
if (RI_MGR) {
rawinput::touch::update_timeouts(RI_MGR.get());
}
// lock
std::lock_guard<std::mutex> lock(TOUCH_EVENTS_M);
// overlay override
if (!overlay &&
overlay::OVERLAY &&
overlay::OVERLAY->get_active() &&
!overlay::OVERLAY->has_subscreen_touch_transform() &&
ImGui::GetIO().WantCaptureMouse) {
TOUCH_EVENTS.reset();
return;
}
// append touch points
while (!TOUCH_EVENTS.empty()) {
touch_events.push_back(TOUCH_EVENTS.get());
}
}
void update_spicetouch_window_dimensions(HWND hWnd) {
RECT clientRect {};
GetClientRect(hWnd, &clientRect);
// adjust to client area
POINT topleft {.x = 0, .y = 0};
ClientToScreen(hWnd, &topleft);
POINT bottomright {.x = clientRect.right, .y = clientRect.bottom};
ClientToScreen(hWnd, &bottomright);
SPICETOUCH_TOUCH_X = topleft.x;
SPICETOUCH_TOUCH_Y = topleft.y;
SPICETOUCH_TOUCH_WIDTH = bottomright.x - topleft.x;
SPICETOUCH_TOUCH_HEIGHT = bottomright.y - topleft.y;
}
// https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
bool is_mouse_message_from_touchscreen() {
constexpr ULONG_PTR MI_WP_SIGNATURE = 0xFF515700;
constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00;
return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE;
}