From b68b72de113f4638fdacff6eb517a0bc9b996a60 Mon Sep 17 00:00:00 2001
From: James Liu <17558260+JamesLewisLiu@users.noreply.github.com>
Date: Sun, 2 Aug 2026 10:47:12 +0800
Subject: [PATCH 1/3] feat(windows): map touch input to primary display
---
cmake/compile_definitions/windows.cmake | 1 +
docs/configuration.md | 29 ++
src/config.cpp | 3 +
src/config.h | 1 +
src/input.cpp | 52 ++-
src/input.h | 17 +
src/platform/windows/input.cpp | 313 ++++++++++++++++--
src/platform/windows/input_utils.h | 99 ++++++
src_assets/common/assets/web/config.html | 1 +
.../common/assets/web/configs/tabs/Inputs.vue | 9 +
.../assets/web/public/assets/locale/en.json | 2 +
.../platform/windows/test_touch_target.cpp | 302 +++++++++++++++++
tests/unit/test_config_defaults.cpp | 19 ++
tests/unit/test_input.cpp | 136 ++++++++
14 files changed, 934 insertions(+), 50 deletions(-)
create mode 100644 src/platform/windows/input_utils.h
create mode 100644 tests/unit/platform/windows/test_touch_target.cpp
create mode 100644 tests/unit/test_config_defaults.cpp
create mode 100644 tests/unit/test_input.cpp
diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake
index f255d38ab67..1c5b53f4edd 100644
--- a/cmake/compile_definitions/windows.cmake
+++ b/cmake/compile_definitions/windows.cmake
@@ -71,6 +71,7 @@ set(PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/windows/publish.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/misc.h"
"${CMAKE_SOURCE_DIR}/src/platform/windows/misc.cpp"
+ "${CMAKE_SOURCE_DIR}/src/platform/windows/input_utils.h"
"${CMAKE_SOURCE_DIR}/src/platform/windows/input.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/display.h"
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_base.cpp"
diff --git a/docs/configuration.md b/docs/configuration.md
index ed503973a4b..a71ec0bbb5b 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -688,6 +688,35 @@ editing the `conf` file in a text editor. Use the examples as reference.
+### touch_send_to_primary_display
+
+
+
+ Description
+
+ When enabled on Windows, Sunshine maps native touch input sent by the streaming client to the current
+ primary display instead of the display shown in the stream. Relative touch positions are preserved, and
+ physical display bounds are used so the mapping covers the full primary display even when display
+ resolutions or Windows scaling settings differ.
+
+ This setting does not affect pen, absolute mouse, or relative mouse input. If Sunshine cannot determine
+ valid primary display dimensions, touch input falls back to the streamed display.
+
+
+
+ Default
+ @code{}
+ disabled
+ @endcode
+
+
+ Example
+ @code{}
+ touch_send_to_primary_display = enabled
+ @endcode
+
+
+
### keybindings
diff --git a/src/config.cpp b/src/config.cpp
index 5392bca2ee0..4be7a0ea2ab 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -853,11 +853,13 @@ namespace config {
true, // ds5_inputtino_randomize_mac
true, // keyboard enabled
+ false, // map right Alt to the Windows key
true, // mouse enabled
true, // controller enabled
true, // always send scancodes
true, // high resolution scrolling
true, // native pen/touch support
+ false, // send touch input to the primary display
};
/**
@@ -1802,6 +1804,7 @@ namespace config {
bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling);
bool_f(vars, "native_pen_touch", input.native_pen_touch);
+ bool_f(vars, "touch_send_to_primary_display", input.touch_send_to_primary_display);
bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases);
bool_f(vars, "system_tray", sunshine.system_tray);
diff --git a/src/config.h b/src/config.h
index 2795cf17958..0acb1a6647d 100644
--- a/src/config.h
+++ b/src/config.h
@@ -289,6 +289,7 @@ namespace config {
bool high_resolution_scrolling; ///< Enable high-resolution mouse-wheel events.
bool native_pen_touch; ///< Enable native pen and touch injection.
+ bool touch_send_to_primary_display {false}; ///< Map streaming-client touch input to the Windows primary display.
};
namespace flag {
diff --git a/src/input.cpp b/src/input.cpp
index 270409a3be2..1a6afb2036f 100644
--- a/src/input.cpp
+++ b/src/input.cpp
@@ -1086,28 +1086,43 @@ namespace input {
input->gamepads[packet->controllerNumber].id = id;
}
- /**
- * @brief Normalizes coordinates to monitor-local logical touch dimensions.
- * @param touch_port The current touch port metadata.
- * @param coords The in/out coordinate pair to normalize.
- * @return The monitor-local touch port, or std::nullopt if dimensions are invalid.
- */
- std::optional monitor_touch_port(const input::touch_port_t &touch_port, std::pair &coords) {
- const float monitor_logical_w = (touch_port.width * touch_port.scalar_inv) / touch_port.scalar_tpcoords;
- const float monitor_logical_h = (touch_port.height * touch_port.scalar_inv) / touch_port.scalar_tpcoords;
- if (monitor_logical_w <= 0.0f || monitor_logical_h <= 0.0f) {
+ std::optional monitor_touch_port(
+ const input::touch_port_t &touch_port,
+ std::pair &coords,
+ bool use_effective_content
+ ) {
+ // client_to_touchport() has already subtracted the leading client padding from the coordinates. Remove padding
+ // from both sides of the encoded extent here so effective-content coordinates still normalize to 0.0-1.0.
+ const float frame_logical_w = (touch_port.width * touch_port.scalar_inv) / touch_port.scalar_tpcoords;
+ const float frame_logical_h = (touch_port.height * touch_port.scalar_inv) / touch_port.scalar_tpcoords;
+ const float content_width = touch_port.width - (2.0f * touch_port.client_offsetX);
+ const float content_height = touch_port.height - (2.0f * touch_port.client_offsetY);
+ const float monitor_logical_w = (content_width * touch_port.scalar_inv) / touch_port.scalar_tpcoords;
+ const float monitor_logical_h = (content_height * touch_port.scalar_inv) / touch_port.scalar_tpcoords;
+ const float normalization_width = use_effective_content ? monitor_logical_w : frame_logical_w;
+ const float normalization_height = use_effective_content ? monitor_logical_h : frame_logical_h;
+ if (!std::isfinite(normalization_width) ||
+ !std::isfinite(normalization_height) ||
+ normalization_width <= 0.0f ||
+ normalization_height <= 0.0f) {
BOOST_LOG(warning) << "Ignoring touch/pen input due to invalid logical touch dimensions"sv;
return std::nullopt;
}
- coords.first = (coords.first - touch_port.offset_x) / monitor_logical_w;
- coords.second = (coords.second - touch_port.offset_y) / monitor_logical_h;
+ coords.first = (coords.first - touch_port.offset_x) / normalization_width;
+ coords.second = (coords.second - touch_port.offset_y) / normalization_height;
+ if (use_effective_content) {
+ // client_to_touchport() maps encoded padding to the content edges. Keep those edge coordinates inside the
+ // normalized target so native touch cannot escape onto an adjacent display.
+ coords.first = std::clamp(coords.first, 0.0f, 1.0f);
+ coords.second = std::clamp(coords.second, 0.0f, 1.0f);
+ }
return platf::touch_port_t {
touch_port.offset_x,
touch_port.offset_y,
- static_cast(monitor_logical_w),
- static_cast(monitor_logical_h)
+ static_cast(normalization_width),
+ static_cast(normalization_height)
};
}
@@ -1129,7 +1144,12 @@ namespace input {
auto &touch_port = input->touch_port;
- auto abs_port = monitor_touch_port(touch_port, *coords);
+#ifdef _WIN32
+ const bool use_effective_touch_content = config::input.touch_send_to_primary_display;
+#else
+ constexpr bool use_effective_touch_content = false;
+#endif
+ auto abs_port = monitor_touch_port(touch_port, *coords, use_effective_touch_content);
if (!abs_port) {
return;
}
@@ -1180,7 +1200,7 @@ namespace input {
auto &touch_port = input->touch_port;
- auto abs_port = monitor_touch_port(touch_port, *coords);
+ auto abs_port = monitor_touch_port(touch_port, *coords, false);
if (!abs_port) {
return;
}
diff --git a/src/input.h b/src/input.h
index 42e2e680784..db8c306d33d 100644
--- a/src/input.h
+++ b/src/input.h
@@ -6,6 +6,7 @@
// standard includes
#include
+#include
// local includes
#include "platform/common.h"
@@ -79,6 +80,22 @@ namespace input {
}
};
+ /**
+ * @brief Normalize host coordinates against the encoded frame or its effective display content.
+ * @details When effective content is selected, symmetric letterbox or pillarbox padding is removed from the
+ * encoded extent and coordinates in that padding are clamped to the nearest content edge.
+ *
+ * @param touch_port Touch-port metadata for the active stream and captured display.
+ * @param coords Host-relative coordinates to normalize in place.
+ * @param use_effective_content Whether to exclude encoded letterbox or pillarbox regions from normalization.
+ * @return The selected logical touch port, or `std::nullopt` when dimensions are invalid.
+ */
+ std::optional monitor_touch_port(
+ const touch_port_t &touch_port,
+ std::pair &coords,
+ bool use_effective_content
+ );
+
/**
* @brief Scale the ellipse axes according to the provided size.
* @param val The major and minor axis pair.
diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp
index c979feb84fb..fcf8792f605 100644
--- a/src/platform/windows/input.cpp
+++ b/src/platform/windows/input.cpp
@@ -17,7 +17,10 @@
#include
// standard includes
+#include
#include
+#include
+#include
#include
#include
@@ -25,6 +28,7 @@
#include
// local includes
+#include "input_utils.h"
#include "keylayout.h"
#include "misc.h"
#include "src/config.h"
@@ -47,6 +51,188 @@ namespace platf {
65535
};
+ /**
+ * @brief Windows pointer flags that are valid only for the frame in which they are injected.
+ */
+ constexpr auto EDGE_TRIGGERED_POINTER_FLAGS =
+ POINTER_FLAG_DOWN |
+ POINTER_FLAG_UP |
+ POINTER_FLAG_CANCELED |
+ POINTER_FLAG_UPDATE;
+
+ std::optional win_input::make_primary_display_touch_port(std::span displays) {
+ if (displays.empty()) {
+ return std::nullopt;
+ }
+
+ const display_bounds_t *primary_display = nullptr;
+ auto virtual_origin_x = std::numeric_limits::max();
+ auto virtual_origin_y = std::numeric_limits::max();
+
+ for (const auto &display : displays) {
+ if (display.width <= 0 || display.height <= 0) {
+ return std::nullopt;
+ }
+
+ virtual_origin_x = std::min(virtual_origin_x, display.offset_x);
+ virtual_origin_y = std::min(virtual_origin_y, display.offset_y);
+
+ if (display.is_primary) {
+ if (primary_display) {
+ return std::nullopt;
+ }
+ primary_display = &display;
+ }
+ }
+
+ if (!primary_display) {
+ return std::nullopt;
+ }
+
+ // Windows positions displays relative to the primary display and may report negative coordinates. Sunshine's
+ // touch ports are relative to the top-left of the virtual desktop, so translate to that nonnegative space.
+ const auto primary_offset_x = static_cast(primary_display->offset_x) - virtual_origin_x;
+ const auto primary_offset_y = static_cast(primary_display->offset_y) - virtual_origin_y;
+ if (primary_offset_x > std::numeric_limits::max() || primary_offset_y > std::numeric_limits::max()) {
+ return std::nullopt;
+ }
+
+ return touch_port_t {
+ static_cast(primary_offset_x),
+ static_cast(primary_offset_y),
+ primary_display->width,
+ primary_display->height,
+ 0,
+ 0
+ };
+ }
+
+ touch_port_t win_input::select_touch_port(
+ const touch_port_t &streamed_touch_port,
+ bool send_to_primary_display,
+ const std::optional &primary_touch_port
+ ) {
+ if (!send_to_primary_display || !primary_touch_port) {
+ return streamed_touch_port;
+ }
+
+ return *primary_touch_port;
+ }
+
+ std::pair win_input::map_normalized_touch_position(
+ const touch_port_t &touch_port,
+ float normalized_x,
+ float normalized_y
+ ) {
+ const auto map_axis = [](float normalized_coordinate, int offset, int extent) {
+ if (extent <= 0) {
+ return offset;
+ }
+
+ if (!std::isfinite(normalized_coordinate)) {
+ normalized_coordinate = 0.0f;
+ }
+
+ normalized_coordinate = std::clamp(normalized_coordinate, 0.0f, 1.0f);
+ // A normalized coordinate of 1.0 is the far edge of this display. Select its final pixel rather than the first
+ // pixel of a neighboring display.
+ const auto pixel_offset = std::min(static_cast(normalized_coordinate * extent), extent - 1);
+ return offset + pixel_offset;
+ };
+
+ return std::pair {
+ map_axis(normalized_x, touch_port.offset_x, touch_port.width),
+ map_axis(normalized_y, touch_port.offset_y, touch_port.height)
+ };
+ }
+
+ std::uint32_t win_input::apply_touch_pointer_event_flags(
+ std::uint32_t pointer_flags,
+ std::uint8_t event_type,
+ bool designate_primary
+ ) {
+ switch (event_type) {
+ case LI_TOUCH_EVENT_DOWN:
+ case LI_TOUCH_EVENT_MOVE:
+ pointer_flags |= POINTER_FLAG_FIRSTBUTTON;
+ break;
+ case LI_TOUCH_EVENT_HOVER:
+ case LI_TOUCH_EVENT_UP:
+ case LI_TOUCH_EVENT_CANCEL:
+ case LI_TOUCH_EVENT_CANCEL_ALL:
+ case LI_TOUCH_EVENT_HOVER_LEAVE:
+ pointer_flags &= ~POINTER_FLAG_FIRSTBUTTON;
+ break;
+ default:
+ break;
+ }
+
+ if (event_type == LI_TOUCH_EVENT_DOWN && designate_primary) {
+ pointer_flags |= POINTER_FLAG_PRIMARY;
+ }
+
+ return pointer_flags;
+ }
+
+ std::uint32_t win_input::finish_touch_pointer_frame(std::uint32_t pointer_flags) {
+ pointer_flags &= ~EDGE_TRIGGERED_POINTER_FLAGS;
+ if (!(pointer_flags & POINTER_FLAG_INCONTACT)) {
+ pointer_flags &= ~(POINTER_FLAG_FIRSTBUTTON | POINTER_FLAG_PRIMARY);
+ }
+ return pointer_flags;
+ }
+
+ bool win_input::touch_pointer_blocks_primary_designation(std::uint32_t pointer_flags) {
+ return pointer_flags & (POINTER_FLAG_INCONTACT | POINTER_FLAG_PRIMARY);
+ }
+
+ namespace {
+ /**
+ * @brief Query physical bounds for attached Windows displays and build the primary-display touch port.
+ *
+ * @return Primary-display touch port, or `std::nullopt` when the current topology cannot be queried.
+ */
+ std::optional query_primary_display_touch_port() {
+ std::vector displays;
+
+ for (DWORD display_index = 0;; ++display_index) {
+ DISPLAY_DEVICEW display_device {};
+ display_device.cb = sizeof(display_device);
+ if (!EnumDisplayDevicesW(nullptr, display_index, &display_device, 0)) {
+ break;
+ }
+
+ if (!(display_device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) ||
+ (display_device.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER)) {
+ continue;
+ }
+
+ DEVMODEW display_mode {};
+ display_mode.dmSize = sizeof(display_mode);
+ if (!EnumDisplaySettingsExW(display_device.DeviceName, ENUM_CURRENT_SETTINGS, &display_mode, 0)) {
+ return std::nullopt;
+ }
+
+ constexpr DWORD required_fields = DM_POSITION | DM_PELSWIDTH | DM_PELSHEIGHT;
+ if ((display_mode.dmFields & required_fields) != required_fields ||
+ display_mode.dmPelsWidth > static_cast(std::numeric_limits::max()) ||
+ display_mode.dmPelsHeight > static_cast(std::numeric_limits::max())) {
+ return std::nullopt;
+ }
+
+ displays.push_back({
+ static_cast(display_mode.dmPosition.x),
+ static_cast(display_mode.dmPosition.y),
+ static_cast(display_mode.dmPelsWidth),
+ static_cast(display_mode.dmPelsHeight),
+ (display_device.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) != 0
+ });
+ }
+
+ return win_input::make_primary_display_touch_port(displays);
+ }
+ } // namespace
+
/**
* @brief ViGEm client pointer released with `vigem_free`.
*/
@@ -766,6 +952,7 @@ namespace platf {
POINTER_TYPE_INFO touchInfo[10] {}; ///< Touch info.
UINT32 activeTouchSlots {}; ///< Active touch slots.
thread_pool_util::ThreadPool::task_id_t touchRepeatTask {}; ///< Touch repeat task.
+ bool primaryTouchPortWarningLogged {}; ///< Whether invalid primary-display metrics have already been logged.
};
/**
@@ -845,36 +1032,29 @@ namespace platf {
}
/**
- * @brief Populate common `POINTER_INFO` members shared between pen and touch events.
- * @param pointerInfo The pointer info to populate.
- * @param touchPort The current viewport for translating to screen coordinates.
- * @param eventType The type of touch/pen event.
- * @param x The normalized 0.0-1.0 X coordinate.
- * @param y The normalized 0.0-1.0 Y coordinate.
+ * @brief Apply common pointer flags for a pen or touch event.
+ *
+ * @param pointerInfo Pointer state to update.
+ * @param eventType Moonlight touch or pen event type.
+ * @return `true` when the event also requires an updated pixel location.
*/
- void populate_common_pointer_info(POINTER_INFO &pointerInfo, const touch_port_t &touchPort, uint8_t eventType, float x, float y) {
+ [[nodiscard]] bool apply_common_pointer_event(POINTER_INFO &pointerInfo, uint8_t eventType) {
switch (eventType) {
case LI_TOUCH_EVENT_HOVER:
pointerInfo.pointerFlags &= ~POINTER_FLAG_INCONTACT;
pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE;
- pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;
- pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;
- break;
+ return true;
case LI_TOUCH_EVENT_DOWN:
pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN;
- pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;
- pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;
- break;
+ return true;
case LI_TOUCH_EVENT_UP:
// We expect to get another LI_TOUCH_EVENT_HOVER if the pointer remains in range
pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);
pointerInfo.pointerFlags |= POINTER_FLAG_UP;
- break;
+ return false;
case LI_TOUCH_EVENT_MOVE:
pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_UPDATE;
- pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;
- pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;
- break;
+ return true;
case LI_TOUCH_EVENT_CANCEL:
case LI_TOUCH_EVENT_CANCEL_ALL:
// If we were in contact with the touch surface at the time of the cancellation,
@@ -886,20 +1066,20 @@ namespace platf {
}
pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);
pointerInfo.pointerFlags |= POINTER_FLAG_CANCELED;
- break;
+ return false;
case LI_TOUCH_EVENT_HOVER_LEAVE:
pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);
pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;
- break;
+ return false;
case LI_TOUCH_EVENT_BUTTON_ONLY:
// On Windows, we can only pass buttons if we have an active pointer
if (pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;
}
- break;
+ return false;
default:
BOOST_LOG(warning) << "Unknown touch event: "sv << (uint32_t) eventType;
- break;
+ return false;
}
}
@@ -951,7 +1131,15 @@ namespace platf {
// If we have active slots, cancel them all
if (raw->activeTouchSlots > 0) {
for (UINT32 i = 0; i < raw->activeTouchSlots; i++) {
- populate_common_pointer_info(raw->touchInfo[i].touchInfo.pointerInfo, {}, LI_TOUCH_EVENT_CANCEL_ALL, 0.0f, 0.0f);
+ static_cast(apply_common_pointer_event(
+ raw->touchInfo[i].touchInfo.pointerInfo,
+ LI_TOUCH_EVENT_CANCEL_ALL
+ ));
+ raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags = win_input::apply_touch_pointer_event_flags(
+ raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags,
+ LI_TOUCH_EVENT_CANCEL_ALL,
+ false
+ );
raw->touchInfo[i].touchInfo.touchMask = TOUCH_MASK_NONE;
}
if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) {
@@ -965,9 +1153,6 @@ namespace platf {
raw->activeTouchSlots = 0;
}
- // These are edge-triggered pointer state flags that should always be cleared next frame
- constexpr auto EDGE_TRIGGERED_POINTER_FLAGS = POINTER_FLAG_DOWN | POINTER_FLAG_UP | POINTER_FLAG_CANCELED | POINTER_FLAG_UPDATE; ///< Protocol or platform constant for edge triggered pointer flags.
-
/**
* @brief Sends a touch event to the OS.
* @param input The client-specific input context.
@@ -1011,6 +1196,42 @@ namespace platf {
return;
}
+ const bool send_to_primary_display = config::input.touch_send_to_primary_display;
+ std::optional primary_touch_port;
+ if (send_to_primary_display) {
+ primary_touch_port = query_primary_display_touch_port();
+
+ if (!primary_touch_port && !raw->primaryTouchPortWarningLogged) {
+ BOOST_LOG(warning)
+ << "Unable to target the primary display for touch input due to an invalid physical display topology; "sv
+ << "using the streamed display instead"sv;
+ raw->primaryTouchPortWarningLogged = true;
+ } else if (primary_touch_port) {
+ raw->primaryTouchPortWarningLogged = false;
+ }
+ } else {
+ raw->primaryTouchPortWarningLogged = false;
+ }
+
+ // Keep this selection local to native touch injection so pen and mouse input
+ // continue using the streamed display's touch port.
+ const auto selected_touch_port = win_input::select_touch_port(
+ touch_port,
+ send_to_primary_display,
+ primary_touch_port
+ );
+
+ bool designate_primary_touch = touch.eventType == LI_TOUCH_EVENT_DOWN;
+ if (designate_primary_touch) {
+ // Windows permits only one primary pointer during an active touch interaction.
+ for (const auto &active_pointer : raw->touchInfo) {
+ if (win_input::touch_pointer_blocks_primary_designation(active_pointer.touchInfo.pointerInfo.pointerFlags)) {
+ designate_primary_touch = false;
+ break;
+ }
+ }
+ }
+
// Find or allocate an entry for this touch pointer ID
auto pointer = pointer_by_id(raw, touch.pointerId, touch.eventType);
if (!pointer) {
@@ -1024,8 +1245,17 @@ namespace platf {
auto &touchInfo = pointer->touchInfo;
touchInfo.pointerInfo.pointerType = PT_TOUCH;
- // Populate shared pointer info fields
- populate_common_pointer_info(touchInfo.pointerInfo, touch_port, touch.eventType, touch.x, touch.y);
+ // Apply shared pointer state and constrain native touch coordinates to the selected display.
+ if (apply_common_pointer_event(touchInfo.pointerInfo, touch.eventType)) {
+ const auto [pixel_x, pixel_y] = win_input::map_normalized_touch_position(selected_touch_port, touch.x, touch.y);
+ touchInfo.pointerInfo.ptPixelLocation.x = pixel_x;
+ touchInfo.pointerInfo.ptPixelLocation.y = pixel_y;
+ }
+ touchInfo.pointerInfo.pointerFlags = win_input::apply_touch_pointer_event_flags(
+ touchInfo.pointerInfo.pointerFlags,
+ touch.eventType,
+ designate_primary_touch
+ );
touchInfo.touchMask = TOUCH_MASK_NONE;
@@ -1058,10 +1288,22 @@ namespace platf {
float contactHeight = (std::sin(majorAxisAngle) * touch.contactAreaMajor) + (std::sin(minorAxisAngle) * touch.contactAreaMinor);
// Convert into screen coordinates centered at the touch location and constrained by screen dimensions
- touchInfo.rcContact.left = std::max(touch_port.offset_x, touchInfo.pointerInfo.ptPixelLocation.x - std::floor(contactWidth / 2));
- touchInfo.rcContact.right = std::min(touch_port.offset_x + touch_port.width, touchInfo.pointerInfo.ptPixelLocation.x + std::ceil(contactWidth / 2));
- touchInfo.rcContact.top = std::max(touch_port.offset_y, touchInfo.pointerInfo.ptPixelLocation.y - std::floor(contactHeight / 2));
- touchInfo.rcContact.bottom = std::min(touch_port.offset_y + touch_port.height, touchInfo.pointerInfo.ptPixelLocation.y + std::ceil(contactHeight / 2));
+ touchInfo.rcContact.left = std::max(
+ selected_touch_port.offset_x,
+ touchInfo.pointerInfo.ptPixelLocation.x - std::floor(contactWidth / 2)
+ );
+ touchInfo.rcContact.right = std::min(
+ selected_touch_port.offset_x + selected_touch_port.width,
+ touchInfo.pointerInfo.ptPixelLocation.x + std::ceil(contactWidth / 2)
+ );
+ touchInfo.rcContact.top = std::max(
+ selected_touch_port.offset_y,
+ touchInfo.pointerInfo.ptPixelLocation.y - std::floor(contactHeight / 2)
+ );
+ touchInfo.rcContact.bottom = std::min(
+ selected_touch_port.offset_y + selected_touch_port.height,
+ touchInfo.pointerInfo.ptPixelLocation.y + std::ceil(contactHeight / 2)
+ );
touchInfo.touchMask |= TOUCH_MASK_CONTACTAREA;
}
@@ -1084,7 +1326,7 @@ namespace platf {
}
// Clear pointer flags that should only remain set for one frame
- touchInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS;
+ touchInfo.pointerInfo.pointerFlags = win_input::finish_touch_pointer_frame(touchInfo.pointerInfo.pointerFlags);
// If we still have an active touch, refresh the touch state periodically
if (raw->activeTouchSlots > 1 || touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {
@@ -1135,8 +1377,11 @@ namespace platf {
penInfo.pointerInfo.pointerType = PT_PEN;
penInfo.pointerInfo.pointerId = 0;
- // Populate shared pointer info fields
- populate_common_pointer_info(penInfo.pointerInfo, touch_port, pen.eventType, pen.x, pen.y);
+ // Apply shared pointer state while preserving the existing pen coordinate path.
+ if (apply_common_pointer_event(penInfo.pointerInfo, pen.eventType)) {
+ penInfo.pointerInfo.ptPixelLocation.x = pen.x * touch_port.width + touch_port.offset_x;
+ penInfo.pointerInfo.ptPixelLocation.y = pen.y * touch_port.height + touch_port.offset_y;
+ }
// Windows only supports a single pen button, so send all buttons as the barrel button
if (pen.penButtons) {
diff --git a/src/platform/windows/input_utils.h b/src/platform/windows/input_utils.h
new file mode 100644
index 00000000000..7fae54e9b5d
--- /dev/null
+++ b/src/platform/windows/input_utils.h
@@ -0,0 +1,99 @@
+/**
+ * @file src/platform/windows/input_utils.h
+ * @brief Helpers for selecting Windows touch input targets.
+ */
+#pragma once
+
+// standard includes
+#include
+#include
+#include
+#include
+
+// local includes
+#include "src/platform/common.h"
+
+namespace platf::win_input {
+ /**
+ * @brief Physical bounds of an attached Windows display.
+ */
+ struct display_bounds_t {
+ int offset_x; ///< Horizontal display offset in physical virtual-desktop coordinates.
+ int offset_y; ///< Vertical display offset in physical virtual-desktop coordinates.
+ int width; ///< Display width in physical pixels.
+ int height; ///< Display height in physical pixels.
+ bool is_primary; ///< Whether Windows marks this display as the primary display.
+ };
+
+ /**
+ * @brief Build a touch port targeting the Windows primary display.
+ * @details The returned offset is relative to the virtual desktop's top-left corner, matching Sunshine's
+ * nonnegative touch-port coordinate system even when Windows reports displays at negative coordinates.
+ *
+ * @param displays Physical bounds of the currently attached displays.
+ * @return Primary-display touch port, or `std::nullopt` when the display topology is invalid.
+ */
+ std::optional make_primary_display_touch_port(std::span displays);
+
+ /**
+ * @brief Select the touch port used for native Windows touch injection.
+ *
+ * @param streamed_touch_port Touch port associated with the streamed display.
+ * @param send_to_primary_display Whether native touch should target the primary display.
+ * @param primary_touch_port Available primary-display touch port, if valid.
+ * @return The selected touch port.
+ */
+ touch_port_t select_touch_port(
+ const touch_port_t &streamed_touch_port,
+ bool send_to_primary_display,
+ const std::optional &primary_touch_port
+ );
+
+ /**
+ * @brief Map normalized native-touch coordinates to a pixel inside a Windows display.
+ * @details Coordinates outside the normalized content, including client-side black bars, are clamped to the
+ * nearest pixel of the selected display.
+ *
+ * @param touch_port Physical bounds of the selected touch target.
+ * @param normalized_x Horizontal coordinate in normalized video coordinates.
+ * @param normalized_y Vertical coordinate in normalized video coordinates.
+ * @return Pixel coordinates clamped to the selected display's inclusive bounds.
+ */
+ std::pair map_normalized_touch_position(
+ const touch_port_t &touch_port,
+ float normalized_x,
+ float normalized_y
+ );
+
+ /**
+ * @brief Add Windows compatibility flags for a native touch event.
+ * @details Active contacts receive the mouse-compatible first-button flag. The first contact in an interaction
+ * also receives the Windows primary-pointer flag.
+ *
+ * @param pointer_flags Existing Windows pointer flags after applying the event state transition.
+ * @param event_type Moonlight touch event type.
+ * @param designate_primary Whether this contact starts a new primary touch interaction.
+ * @return Pointer flags to inject for the event.
+ */
+ std::uint32_t apply_touch_pointer_event_flags(
+ std::uint32_t pointer_flags,
+ std::uint8_t event_type,
+ bool designate_primary
+ );
+
+ /**
+ * @brief Remove transient Windows touch flags after a frame is injected.
+ *
+ * @param pointer_flags Pointer flags used for the injected frame.
+ * @return Persistent flags to retain for subsequent touch frames.
+ */
+ std::uint32_t finish_touch_pointer_frame(std::uint32_t pointer_flags);
+
+ /**
+ * @brief Determine whether a touch pointer prevents a new primary contact from being designated.
+ *
+ * @param pointer_flags Current Windows pointer flags for a touch pointer.
+ * @return `true` when the pointer belongs to the current contact interaction.
+ */
+ bool touch_pointer_blocks_primary_designation(std::uint32_t pointer_flags);
+} // namespace platf::win_input
diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html
index e62c61ed838..1c16e5c9450 100644
--- a/src_assets/common/assets/web/config.html
+++ b/src_assets/common/assets/web/config.html
@@ -237,6 +237,7 @@ {{ $t('config.configuration') }}
"mouse": "enabled",
"high_resolution_scrolling": "enabled",
"native_pen_touch": "enabled",
+ "touch_send_to_primary_display": "disabled",
"keybindings": "[0x10,0xA0,0x11,0xA2,0x12,0xA4]", // todo: add this to UI
},
},
diff --git a/src_assets/common/assets/web/configs/tabs/Inputs.vue b/src_assets/common/assets/web/configs/tabs/Inputs.vue
index 7fa76a20721..be0fb30c39b 100644
--- a/src_assets/common/assets/web/configs/tabs/Inputs.vue
+++ b/src_assets/common/assets/web/configs/tabs/Inputs.vue
@@ -181,6 +181,15 @@ const config = ref(props.config)
v-model="config.native_pen_touch"
default="true"
>
+
+
+
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json
index 1c0b949c6cc..84fc604c13f 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -394,6 +394,8 @@
"sw_tune_zerolatency": "zerolatency -- good for fast encoding and low-latency streaming (default)",
"system_tray": "Enable system tray",
"system_tray_desc": "Show icon in system tray and display desktop notifications",
+ "touch_send_to_primary_display": "Send Touch Input to the Primary Display",
+ "touch_send_to_primary_display_desc": "When enabled on Windows, Sunshine maps native touch input sent by the streaming client to the current primary display instead of the display shown in the stream. Relative touch positions are preserved, and pen and mouse input are unaffected.",
"touchpad_as_ds4": "Emulate a DS4 gamepad if the client gamepad reports a touchpad is present",
"touchpad_as_ds4_desc": "If disabled, touchpad presence will not be taken into account during gamepad type selection.",
"upnp": "UPnP",
diff --git a/tests/unit/platform/windows/test_touch_target.cpp b/tests/unit/platform/windows/test_touch_target.cpp
new file mode 100644
index 00000000000..708771fc26a
--- /dev/null
+++ b/tests/unit/platform/windows/test_touch_target.cpp
@@ -0,0 +1,302 @@
+/**
+ * @file tests/unit/platform/windows/test_touch_target.cpp
+ * @brief Test Windows touch target selection.
+ */
+#include "../../../tests_common.h"
+
+#ifdef _WIN32
+
+ // platform includes
+ #include
+
+ // standard includes
+ #include
+ #include
+
+ // local includes
+ #include "src/platform/windows/input_utils.h"
+
+namespace {
+ /**
+ * @brief Verify that two touch ports contain identical bounds.
+ *
+ * @param actual Actual touch port.
+ * @param expected Expected touch port.
+ */
+ void expect_touch_ports_equal(const platf::touch_port_t &actual, const platf::touch_port_t &expected) {
+ EXPECT_EQ(actual.offset_x, expected.offset_x);
+ EXPECT_EQ(actual.offset_y, expected.offset_y);
+ EXPECT_EQ(actual.width, expected.width);
+ EXPECT_EQ(actual.height, expected.height);
+ EXPECT_EQ(actual.logical_width, expected.logical_width);
+ EXPECT_EQ(actual.logical_height, expected.logical_height);
+ }
+} // namespace
+
+TEST(WindowsTouchTargetTest, DisabledUsesStreamedTouchPort) {
+ const platf::touch_port_t streamed_touch_port {640, 360, 2560, 1440, 1280, 720};
+ const std::array displays {
+ platf::win_input::display_bounds_t {0, 0, 1920, 1080, true}
+ };
+ const auto primary_touch_port = platf::win_input::make_primary_display_touch_port(displays);
+
+ const auto selected_touch_port = platf::win_input::select_touch_port(streamed_touch_port, false, primary_touch_port);
+
+ expect_touch_ports_equal(selected_touch_port, streamed_touch_port);
+}
+
+TEST(WindowsTouchTargetTest, SingleDisplayUsesPrimaryDisplayBounds) {
+ const platf::touch_port_t streamed_touch_port {0, 0, 2560, 1440, 0, 0};
+ const std::array displays {
+ platf::win_input::display_bounds_t {0, 0, 1920, 1080, true}
+ };
+ const auto primary_touch_port = platf::win_input::make_primary_display_touch_port(displays);
+ ASSERT_TRUE(primary_touch_port);
+
+ const auto selected_touch_port = platf::win_input::select_touch_port(streamed_touch_port, true, primary_touch_port);
+
+ expect_touch_ports_equal(selected_touch_port, {0, 0, 1920, 1080, 0, 0});
+}
+
+TEST(WindowsTouchTargetTest, VirtualDesktopOriginOffsetsPrimaryDisplay) {
+ const std::array displays {
+ platf::win_input::display_bounds_t {-2560, -1440, 2560, 1440, false},
+ platf::win_input::display_bounds_t {0, 0, 1920, 1080, true}
+ };
+ const auto primary_touch_port = platf::win_input::make_primary_display_touch_port(displays);
+ ASSERT_TRUE(primary_touch_port);
+
+ expect_touch_ports_equal(*primary_touch_port, {2560, 1440, 1920, 1080, 0, 0});
+}
+
+TEST(WindowsTouchTargetTest, DifferentResolutionPreservesRelativePosition) {
+ constexpr float normalized_x = 0.5f;
+ constexpr float normalized_y = 0.5f;
+ const platf::touch_port_t streamed_touch_port {3840, 0, 1280, 768, 0, 0};
+ const std::array displays {
+ platf::win_input::display_bounds_t {0, 0, 3840, 2160, true},
+ platf::win_input::display_bounds_t {3840, 0, 1280, 768, false}
+ };
+ const auto primary_touch_port = platf::win_input::make_primary_display_touch_port(displays);
+
+ const auto selected_touch_port = platf::win_input::select_touch_port(streamed_touch_port, true, primary_touch_port);
+ const auto [pixel_x, pixel_y] = platf::win_input::map_normalized_touch_position(
+ selected_touch_port,
+ normalized_x,
+ normalized_y
+ );
+
+ EXPECT_EQ(pixel_x, 1920);
+ EXPECT_EQ(pixel_y, 1080);
+}
+
+TEST(WindowsTouchTargetTest, BlackBarTouchStaysInsideSelectedDisplay) {
+ constexpr platf::touch_port_t selected_touch_port {2560, 1440, 1920, 1080, 0, 0};
+
+ const auto [left, top] = platf::win_input::map_normalized_touch_position(selected_touch_port, -0.25f, -0.25f);
+ const auto [right, bottom] = platf::win_input::map_normalized_touch_position(selected_touch_port, 1.25f, 1.25f);
+
+ EXPECT_EQ(left, 2560);
+ EXPECT_EQ(top, 1440);
+ EXPECT_EQ(right, 4479);
+ EXPECT_EQ(bottom, 2519);
+}
+
+TEST(WindowsTouchTargetTest, ExactVideoEdgeDoesNotReachAdjacentDesktop) {
+ constexpr platf::touch_port_t selected_touch_port {0, 0, 3840, 2160, 0, 0};
+
+ const auto [right, bottom] = platf::win_input::map_normalized_touch_position(selected_touch_port, 1.0f, 1.0f);
+
+ EXPECT_EQ(right, 3839);
+ EXPECT_EQ(bottom, 2159);
+}
+
+TEST(WindowsTouchTargetTest, InvalidExtentAndNonFiniteCoordinatesStayAtDisplayOrigin) {
+ constexpr platf::touch_port_t invalid_touch_port {640, 360, 0, -1, 0, 0};
+ constexpr platf::touch_port_t valid_touch_port {640, 360, 1280, 768, 0, 0};
+
+ const auto [invalid_x, invalid_y] = platf::win_input::map_normalized_touch_position(invalid_touch_port, 0.5f, 0.5f);
+ const auto [non_finite_x, non_finite_y] = platf::win_input::map_normalized_touch_position(
+ valid_touch_port,
+ std::numeric_limits::quiet_NaN(),
+ std::numeric_limits::infinity()
+ );
+
+ EXPECT_EQ(invalid_x, 640);
+ EXPECT_EQ(invalid_y, 360);
+ EXPECT_EQ(non_finite_x, 640);
+ EXPECT_EQ(non_finite_y, 360);
+}
+
+TEST(WindowsTouchTargetTest, PrimarySelectionDoesNotMutateStreamedPortUsedByPenAndMouse) {
+ platf::touch_port_t streamed_touch_port {640, 360, 2560, 1440, 1280, 720};
+ const platf::touch_port_t original_streamed_touch_port = streamed_touch_port;
+ const std::array displays {
+ platf::win_input::display_bounds_t {-640, -360, 640, 360, false},
+ platf::win_input::display_bounds_t {0, 0, 1920, 1080, true}
+ };
+ const auto primary_touch_port = platf::win_input::make_primary_display_touch_port(displays);
+
+ static_cast(platf::win_input::select_touch_port(streamed_touch_port, true, primary_touch_port));
+
+ expect_touch_ports_equal(streamed_touch_port, original_streamed_touch_port);
+}
+
+TEST(WindowsTouchTargetTest, InvalidPrimaryDisplayWidthFallsBackToStreamedDisplay) {
+ const platf::touch_port_t streamed_touch_port {640, 0, 2560, 1440, 0, 0};
+ const std::array displays {
+ platf::win_input::display_bounds_t {0, 0, 0, 1080, true}
+ };
+ const auto primary_touch_port = platf::win_input::make_primary_display_touch_port(displays);
+ ASSERT_FALSE(primary_touch_port);
+
+ const auto selected_touch_port = platf::win_input::select_touch_port(streamed_touch_port, true, primary_touch_port);
+
+ expect_touch_ports_equal(selected_touch_port, streamed_touch_port);
+}
+
+TEST(WindowsTouchTargetTest, InvalidPrimaryDisplayHeightFallsBackToStreamedDisplay) {
+ const platf::touch_port_t streamed_touch_port {0, 720, 1920, 1080, 0, 0};
+ const std::array displays {
+ platf::win_input::display_bounds_t {0, 0, 1920, -1, true}
+ };
+ const auto primary_touch_port = platf::win_input::make_primary_display_touch_port(displays);
+ ASSERT_FALSE(primary_touch_port);
+
+ const auto selected_touch_port = platf::win_input::select_touch_port(streamed_touch_port, true, primary_touch_port);
+
+ expect_touch_ports_equal(selected_touch_port, streamed_touch_port);
+}
+
+TEST(WindowsTouchTargetTest, MissingPrimaryDisplayFallsBackToStreamedDisplay) {
+ const platf::touch_port_t streamed_touch_port {0, 0, 1280, 768, 0, 0};
+ const std::array displays {
+ platf::win_input::display_bounds_t {0, 0, 1280, 768, false}
+ };
+ const auto primary_touch_port = platf::win_input::make_primary_display_touch_port(displays);
+ ASSERT_FALSE(primary_touch_port);
+
+ const auto selected_touch_port = platf::win_input::select_touch_port(streamed_touch_port, true, primary_touch_port);
+
+ expect_touch_ports_equal(selected_touch_port, streamed_touch_port);
+}
+
+TEST(WindowsTouchTargetTest, EmptyDisplayLayoutIsInvalid) {
+ const std::array displays {};
+
+ EXPECT_FALSE(platf::win_input::make_primary_display_touch_port(displays));
+}
+
+TEST(WindowsTouchTargetTest, MultiplePrimaryDisplaysAreInvalid) {
+ const std::array displays {
+ platf::win_input::display_bounds_t {0, 0, 3840, 2160, true},
+ platf::win_input::display_bounds_t {3840, 0, 1280, 768, true}
+ };
+
+ EXPECT_FALSE(platf::win_input::make_primary_display_touch_port(displays));
+}
+
+TEST(WindowsTouchTargetTest, UnrepresentablePrimaryDisplayOffsetIsInvalid) {
+ const std::array displays {
+ platf::win_input::display_bounds_t {std::numeric_limits::min(), 0, 1280, 768, false},
+ platf::win_input::display_bounds_t {std::numeric_limits::max(), 0, 3840, 2160, true}
+ };
+
+ EXPECT_FALSE(platf::win_input::make_primary_display_touch_port(displays));
+}
+
+TEST(WindowsTouchPointerFlagsTest, FirstContactIsPrimaryAndMouseCompatible) {
+ constexpr std::uint32_t event_flags = POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN;
+
+ const auto injected_flags = platf::win_input::apply_touch_pointer_event_flags(event_flags, LI_TOUCH_EVENT_DOWN, true);
+
+ EXPECT_TRUE(injected_flags & POINTER_FLAG_PRIMARY);
+ EXPECT_TRUE(injected_flags & POINTER_FLAG_FIRSTBUTTON);
+ EXPECT_TRUE(platf::win_input::touch_pointer_blocks_primary_designation(injected_flags));
+
+ const auto persistent_flags = platf::win_input::finish_touch_pointer_frame(injected_flags);
+ EXPECT_FALSE(persistent_flags & POINTER_FLAG_DOWN);
+ EXPECT_TRUE(persistent_flags & POINTER_FLAG_PRIMARY);
+ EXPECT_TRUE(persistent_flags & POINTER_FLAG_FIRSTBUTTON);
+}
+
+TEST(WindowsTouchPointerFlagsTest, AdditionalContactIsNotPrimary) {
+ constexpr std::uint32_t event_flags = POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN;
+
+ const auto injected_flags = platf::win_input::apply_touch_pointer_event_flags(
+ event_flags,
+ LI_TOUCH_EVENT_DOWN,
+ false
+ );
+
+ EXPECT_FALSE(injected_flags & POINTER_FLAG_PRIMARY);
+ EXPECT_TRUE(injected_flags & POINTER_FLAG_FIRSTBUTTON);
+}
+
+TEST(WindowsTouchPointerFlagsTest, ContactMoveRetainsMouseCompatibility) {
+ constexpr std::uint32_t event_flags =
+ POINTER_FLAG_INRANGE |
+ POINTER_FLAG_INCONTACT |
+ POINTER_FLAG_UPDATE |
+ POINTER_FLAG_PRIMARY;
+
+ const auto injected_flags = platf::win_input::apply_touch_pointer_event_flags(
+ event_flags,
+ LI_TOUCH_EVENT_MOVE,
+ false
+ );
+ const auto persistent_flags = platf::win_input::finish_touch_pointer_frame(injected_flags);
+
+ EXPECT_FALSE(persistent_flags & POINTER_FLAG_UPDATE);
+ EXPECT_TRUE(persistent_flags & POINTER_FLAG_PRIMARY);
+ EXPECT_TRUE(persistent_flags & POINTER_FLAG_FIRSTBUTTON);
+}
+
+TEST(WindowsTouchPointerFlagsTest, PrimaryReleaseIsInjectedBeforeDesignationIsCleared) {
+ constexpr std::uint32_t event_flags = POINTER_FLAG_UP | POINTER_FLAG_FIRSTBUTTON | POINTER_FLAG_PRIMARY;
+
+ const auto injected_flags = platf::win_input::apply_touch_pointer_event_flags(event_flags, LI_TOUCH_EVENT_UP, false);
+
+ EXPECT_TRUE(injected_flags & POINTER_FLAG_UP);
+ EXPECT_TRUE(injected_flags & POINTER_FLAG_PRIMARY);
+ EXPECT_FALSE(injected_flags & POINTER_FLAG_FIRSTBUTTON);
+ EXPECT_TRUE(platf::win_input::touch_pointer_blocks_primary_designation(injected_flags));
+
+ const auto persistent_flags = platf::win_input::finish_touch_pointer_frame(injected_flags);
+ EXPECT_EQ(persistent_flags, POINTER_FLAG_NONE);
+ EXPECT_FALSE(platf::win_input::touch_pointer_blocks_primary_designation(persistent_flags));
+}
+
+TEST(WindowsTouchPointerFlagsTest, NonContactEventsClearMouseButtonCompatibility) {
+ constexpr std::array event_types {
+ LI_TOUCH_EVENT_HOVER,
+ LI_TOUCH_EVENT_CANCEL,
+ LI_TOUCH_EVENT_CANCEL_ALL,
+ LI_TOUCH_EVENT_HOVER_LEAVE
+ };
+
+ for (const auto event_type : event_types) {
+ constexpr std::uint32_t event_flags = POINTER_FLAG_UPDATE | POINTER_FLAG_FIRSTBUTTON;
+ const auto injected_flags = platf::win_input::apply_touch_pointer_event_flags(event_flags, event_type, false);
+ EXPECT_FALSE(injected_flags & POINTER_FLAG_FIRSTBUTTON);
+ }
+}
+
+TEST(WindowsTouchPointerFlagsTest, UnrelatedEventDoesNotChangeCompatibilityFlags) {
+ constexpr std::uint32_t event_flags = POINTER_FLAG_INRANGE;
+
+ EXPECT_EQ(
+ platf::win_input::apply_touch_pointer_event_flags(event_flags, LI_TOUCH_EVENT_BUTTON_ONLY, true),
+ event_flags
+ );
+ EXPECT_EQ(
+ platf::win_input::apply_touch_pointer_event_flags(
+ event_flags,
+ std::numeric_limits::max(),
+ true
+ ),
+ event_flags
+ );
+}
+
+#endif // _WIN32
diff --git a/tests/unit/test_config_defaults.cpp b/tests/unit/test_config_defaults.cpp
new file mode 100644
index 00000000000..6ba3b579377
--- /dev/null
+++ b/tests/unit/test_config_defaults.cpp
@@ -0,0 +1,19 @@
+/**
+ * @file tests/unit/test_config_defaults.cpp
+ * @brief Test default configuration values.
+ */
+#include "../tests_common.h"
+
+// local includes
+#include "src/config.h"
+
+TEST(ConfigDefaultsTest, InputDefaultsKeepNativeTouchEnabled) {
+ EXPECT_TRUE(config::input.keyboard);
+ EXPECT_FALSE(config::input.key_rightalt_to_key_win);
+ EXPECT_TRUE(config::input.mouse);
+ EXPECT_TRUE(config::input.controller);
+ EXPECT_TRUE(config::input.always_send_scancodes);
+ EXPECT_TRUE(config::input.high_resolution_scrolling);
+ EXPECT_TRUE(config::input.native_pen_touch);
+ EXPECT_FALSE(config::input.touch_send_to_primary_display);
+}
diff --git a/tests/unit/test_input.cpp b/tests/unit/test_input.cpp
new file mode 100644
index 00000000000..3caafe43a86
--- /dev/null
+++ b/tests/unit/test_input.cpp
@@ -0,0 +1,136 @@
+/**
+ * @file tests/unit/test_input.cpp
+ * @brief Test input coordinate normalization helpers.
+ */
+
+// test includes
+#include "../tests_common.h"
+
+// local includes
+#include "src/input.h"
+
+TEST(InputTouchPortTest, EncodedPillarboxUsesStreamedDisplayResolution) {
+ const input::touch_port_t touch_port {
+ {3840, 0, 1920, 1080},
+ 5120,
+ 2160,
+ 60.0f,
+ 0.0f,
+ 32.0f / 45.0f,
+ 1.0f,
+ 0,
+ 0
+ };
+ std::pair coords {5120.0f, 768.0f};
+
+ const auto normalized_port = input::monitor_touch_port(touch_port, coords, true);
+
+ ASSERT_TRUE(normalized_port);
+ EXPECT_EQ(normalized_port->width, 1280);
+ EXPECT_EQ(normalized_port->height, 768);
+ EXPECT_FLOAT_EQ(coords.first, 1.0f);
+ EXPECT_FLOAT_EQ(coords.second, 1.0f);
+}
+
+TEST(InputTouchPortTest, EncodedLetterboxCoversBottomOfPrimaryDisplay) {
+ const input::touch_port_t touch_port {
+ {1280, 0, 1280, 1024},
+ 2560,
+ 1024,
+ 0.0f,
+ 128.0f,
+ 1.0f,
+ 1.0f,
+ 0,
+ 0
+ };
+ std::pair coords {640.0f + touch_port.offset_x, 768.0f + touch_port.offset_y};
+
+ const auto normalized_port = input::monitor_touch_port(touch_port, coords, true);
+
+ ASSERT_TRUE(normalized_port);
+ EXPECT_EQ(normalized_port->width, 1280);
+ EXPECT_EQ(normalized_port->height, 768);
+ EXPECT_FLOAT_EQ(coords.first, 0.5f);
+ EXPECT_FLOAT_EQ(coords.second, 1.0f);
+
+ constexpr platf::touch_port_t primary_display {0, 0, 3840, 2160};
+ EXPECT_FLOAT_EQ(coords.first * primary_display.width, 1920.0f);
+ EXPECT_FLOAT_EQ(coords.second * primary_display.height, 2160.0f);
+}
+
+TEST(InputTouchPortTest, NormalizedCoordinatesAreClampedToDisplayBounds) {
+ const input::touch_port_t touch_port {
+ {0, 0, 1280, 768},
+ 1280,
+ 768,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ 1.0f,
+ 0,
+ 0
+ };
+ std::pair coords {-1.0f, 769.0f};
+
+ ASSERT_TRUE(input::monitor_touch_port(touch_port, coords, true));
+ EXPECT_FLOAT_EQ(coords.first, 0.0f);
+ EXPECT_FLOAT_EQ(coords.second, 1.0f);
+}
+
+TEST(InputTouchPortTest, InvalidEffectiveContentDimensionsAreRejected) {
+ const input::touch_port_t touch_port {
+ {0, 0, 1280, 768},
+ 1280,
+ 768,
+ 640.0f,
+ 0.0f,
+ 1.0f,
+ 1.0f,
+ 0,
+ 0
+ };
+ std::pair coords {0.0f, 0.0f};
+
+ EXPECT_FALSE(input::monitor_touch_port(touch_port, coords, true));
+}
+
+TEST(InputTouchPortTest, InvalidCoordinateScaleIsRejected) {
+ const input::touch_port_t touch_port {
+ {0, 0, 1280, 768},
+ 1280,
+ 768,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ 0.0f,
+ 0,
+ 0
+ };
+ std::pair coords {0.0f, 0.0f};
+
+ EXPECT_FALSE(input::monitor_touch_port(touch_port, coords, false));
+}
+
+TEST(InputTouchPortTest, EncodedFrameNormalizationIsPreservedWhenPrimaryMappingIsDisabled) {
+ const input::touch_port_t touch_port {
+ {1280, 0, 1280, 1024},
+ 2560,
+ 1024,
+ 0.0f,
+ 128.0f,
+ 1.0f,
+ 1.0f,
+ 0,
+ 0
+ };
+ std::pair coords {640.0f + touch_port.offset_x, 768.0f + touch_port.offset_y};
+
+ const auto normalized_port = input::monitor_touch_port(touch_port, coords, false);
+
+ ASSERT_TRUE(normalized_port);
+ EXPECT_EQ(normalized_port->width, 1280);
+ EXPECT_EQ(normalized_port->height, 1024);
+ EXPECT_FLOAT_EQ(coords.first, 0.5f);
+ EXPECT_FLOAT_EQ(coords.second, 0.75f);
+}
From a3b86c045781921c65d7a41ce5520aa3a0a06f9c Mon Sep 17 00:00:00 2001
From: James Liu <17558260+JamesLewisLiu@users.noreply.github.com>
Date: Sun, 2 Aug 2026 21:17:02 +0800
Subject: [PATCH 2/3] feat(windows): add primary touch map rotation
---
docs/configuration.md | 30 +++++++
src/config.cpp | 7 ++
src/config.h | 1 +
src/platform/windows/input.cpp | 51 +++++++++++-
src/platform/windows/input_utils.h | 42 ++++++++++
src_assets/common/assets/web/config.html | 1 +
.../common/assets/web/configs/tabs/Inputs.vue | 13 +++
.../assets/web/public/assets/locale/en.json | 6 ++
.../platform/windows/test_touch_target.cpp | 79 +++++++++++++++++++
tests/unit/test_config_defaults.cpp | 1 +
10 files changed, 230 insertions(+), 1 deletion(-)
diff --git a/docs/configuration.md b/docs/configuration.md
index a71ec0bbb5b..b0acf5f50df 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -717,6 +717,36 @@ editing the `conf` file in a text editor. Use the examples as reference.
+### touch_primary_display_rotation
+
+
+
+ Description
+
+ On Windows, rotates native touch coordinates clockwise before mapping them to the primary display.
+ This option only applies when @code{touch_send_to_primary_display} is enabled and does not affect pen
+ or mouse input. Select the rotation that compensates for applications that interpret touchscreen
+ coordinates in a different display orientation.
+
+
+
+ Allowed values
+ @code{0}, @code{90}, @code{180}, or @code{270}
+
+
+ Default
+ @code{}
+ 0
+ @endcode
+
+
+ Example
+ @code{}
+ touch_primary_display_rotation = 90
+ @endcode
+
+
+
### keybindings
diff --git a/src/config.cpp b/src/config.cpp
index 4be7a0ea2ab..4966f9f66f9 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -860,6 +860,7 @@ namespace config {
true, // high resolution scrolling
true, // native pen/touch support
false, // send touch input to the primary display
+ "0", // primary display touch map rotation
};
/**
@@ -1805,6 +1806,12 @@ namespace config {
bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling);
bool_f(vars, "native_pen_touch", input.native_pen_touch);
bool_f(vars, "touch_send_to_primary_display", input.touch_send_to_primary_display);
+ string_restricted_f(
+ vars,
+ "touch_primary_display_rotation",
+ input.touch_primary_display_rotation,
+ {"0"sv, "90"sv, "180"sv, "270"sv}
+ );
bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases);
bool_f(vars, "system_tray", sunshine.system_tray);
diff --git a/src/config.h b/src/config.h
index 0acb1a6647d..6baf6cd8844 100644
--- a/src/config.h
+++ b/src/config.h
@@ -290,6 +290,7 @@ namespace config {
bool high_resolution_scrolling; ///< Enable high-resolution mouse-wheel events.
bool native_pen_touch; ///< Enable native pen and touch injection.
bool touch_send_to_primary_display {false}; ///< Map streaming-client touch input to the Windows primary display.
+ std::string touch_primary_display_rotation {"0"}; ///< Rotate primary-display touch coordinates clockwise.
};
namespace flag {
diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp
index fcf8792f605..8dda3344017 100644
--- a/src/platform/windows/input.cpp
+++ b/src/platform/windows/input.cpp
@@ -119,6 +119,48 @@ namespace platf {
return *primary_touch_port;
}
+ win_input::touch_rotation_e win_input::parse_touch_rotation(std::string_view rotation) {
+ if (rotation == "90"sv) {
+ return touch_rotation_e::clockwise_90;
+ }
+ if (rotation == "180"sv) {
+ return touch_rotation_e::clockwise_180;
+ }
+ if (rotation == "270"sv) {
+ return touch_rotation_e::clockwise_270;
+ }
+ return touch_rotation_e::none;
+ }
+
+ win_input::touch_rotation_e win_input::select_touch_rotation(
+ bool primary_display_selected,
+ std::string_view configured_rotation
+ ) {
+ if (!primary_display_selected) {
+ return touch_rotation_e::none;
+ }
+
+ return parse_touch_rotation(configured_rotation);
+ }
+
+ std::pair win_input::rotate_normalized_touch_position(
+ float normalized_x,
+ float normalized_y,
+ win_input::touch_rotation_e rotation
+ ) {
+ switch (rotation) {
+ case touch_rotation_e::clockwise_90:
+ return {1.0f - normalized_y, normalized_x};
+ case touch_rotation_e::clockwise_180:
+ return {1.0f - normalized_x, 1.0f - normalized_y};
+ case touch_rotation_e::clockwise_270:
+ return {normalized_y, 1.0f - normalized_x};
+ case touch_rotation_e::none:
+ default:
+ return {normalized_x, normalized_y};
+ }
+ }
+
std::pair win_input::map_normalized_touch_position(
const touch_port_t &touch_port,
float normalized_x,
@@ -1220,6 +1262,10 @@ namespace platf {
send_to_primary_display,
primary_touch_port
);
+ const auto touch_rotation = win_input::select_touch_rotation(
+ send_to_primary_display && primary_touch_port.has_value(),
+ config::input.touch_primary_display_rotation
+ );
bool designate_primary_touch = touch.eventType == LI_TOUCH_EVENT_DOWN;
if (designate_primary_touch) {
@@ -1247,7 +1293,10 @@ namespace platf {
// Apply shared pointer state and constrain native touch coordinates to the selected display.
if (apply_common_pointer_event(touchInfo.pointerInfo, touch.eventType)) {
- const auto [pixel_x, pixel_y] = win_input::map_normalized_touch_position(selected_touch_port, touch.x, touch.y);
+ const auto [normalized_x, normalized_y] =
+ win_input::rotate_normalized_touch_position(touch.x, touch.y, touch_rotation);
+ const auto [pixel_x, pixel_y] =
+ win_input::map_normalized_touch_position(selected_touch_port, normalized_x, normalized_y);
touchInfo.pointerInfo.ptPixelLocation.x = pixel_x;
touchInfo.pointerInfo.ptPixelLocation.y = pixel_y;
}
diff --git a/src/platform/windows/input_utils.h b/src/platform/windows/input_utils.h
index 7fae54e9b5d..54765e93352 100644
--- a/src/platform/windows/input_utils.h
+++ b/src/platform/windows/input_utils.h
@@ -8,12 +8,23 @@
#include
#include
#include
+#include
#include
// local includes
#include "src/platform/common.h"
namespace platf::win_input {
+ /**
+ * @brief Clockwise rotation applied to normalized native-touch coordinates.
+ */
+ enum class touch_rotation_e : std::uint16_t {
+ none = 0, ///< Preserve client touch coordinates.
+ clockwise_90 = 90, ///< Rotate coordinates 90 degrees clockwise.
+ clockwise_180 = 180, ///< Rotate coordinates 180 degrees clockwise.
+ clockwise_270 = 270 ///< Rotate coordinates 270 degrees clockwise.
+ };
+
/**
* @brief Physical bounds of an attached Windows display.
*/
@@ -49,6 +60,37 @@ namespace platf::win_input {
const std::optional &primary_touch_port
);
+ /**
+ * @brief Parse a configured clockwise native-touch rotation.
+ *
+ * @param rotation Rotation expressed as `0`, `90`, `180`, or `270` degrees.
+ * @return Parsed rotation, or `touch_rotation_e::none` for an unsupported value.
+ */
+ touch_rotation_e parse_touch_rotation(std::string_view rotation);
+
+ /**
+ * @brief Select the native-touch rotation for the active target.
+ *
+ * @param primary_display_selected Whether a valid primary-display touch port was selected.
+ * @param configured_rotation Configured clockwise rotation in degrees.
+ * @return Configured rotation for a primary-display target, otherwise `touch_rotation_e::none`.
+ */
+ touch_rotation_e select_touch_rotation(bool primary_display_selected, std::string_view configured_rotation);
+
+ /**
+ * @brief Rotate normalized native-touch coordinates around the center of their target display.
+ *
+ * @param normalized_x Horizontal coordinate in normalized video coordinates.
+ * @param normalized_y Vertical coordinate in normalized video coordinates.
+ * @param rotation Clockwise rotation to apply before mapping to display pixels.
+ * @return Rotated normalized coordinates.
+ */
+ std::pair rotate_normalized_touch_position(
+ float normalized_x,
+ float normalized_y,
+ touch_rotation_e rotation
+ );
+
/**
* @brief Map normalized native-touch coordinates to a pixel inside a Windows display.
* @details Coordinates outside the normalized content, including client-side black bars, are clamped to the
diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html
index 1c16e5c9450..899f3f0e096 100644
--- a/src_assets/common/assets/web/config.html
+++ b/src_assets/common/assets/web/config.html
@@ -238,6 +238,7 @@ {{ $t('config.configuration') }}
"high_resolution_scrolling": "enabled",
"native_pen_touch": "enabled",
"touch_send_to_primary_display": "disabled",
+ "touch_primary_display_rotation": "0",
"keybindings": "[0x10,0xA0,0x11,0xA2,0x12,0xA4]", // todo: add this to UI
},
},
diff --git a/src_assets/common/assets/web/configs/tabs/Inputs.vue b/src_assets/common/assets/web/configs/tabs/Inputs.vue
index be0fb30c39b..555425269da 100644
--- a/src_assets/common/assets/web/configs/tabs/Inputs.vue
+++ b/src_assets/common/assets/web/configs/tabs/Inputs.vue
@@ -190,6 +190,19 @@ const config = ref(props.config)
v-model="config.touch_send_to_primary_display"
default="false"
>
+
+
+
+
{{ $t('config.touch_primary_display_rotation') }}
+
+ {{ $t('config.touch_primary_display_rotation_0') }}
+ {{ $t('config.touch_primary_display_rotation_90') }}
+ {{ $t('config.touch_primary_display_rotation_180') }}
+ {{ $t('config.touch_primary_display_rotation_270') }}
+
+
{{ $t('config.touch_primary_display_rotation_desc') }}
+
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json
index 84fc604c13f..f43a2afa968 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -396,6 +396,12 @@
"system_tray_desc": "Show icon in system tray and display desktop notifications",
"touch_send_to_primary_display": "Send Touch Input to the Primary Display",
"touch_send_to_primary_display_desc": "When enabled on Windows, Sunshine maps native touch input sent by the streaming client to the current primary display instead of the display shown in the stream. Relative touch positions are preserved, and pen and mouse input are unaffected.",
+ "touch_primary_display_rotation": "Primary Display Touch Map Rotation",
+ "touch_primary_display_rotation_0": "No rotation",
+ "touch_primary_display_rotation_90": "90° clockwise",
+ "touch_primary_display_rotation_180": "180° clockwise",
+ "touch_primary_display_rotation_270": "270° clockwise",
+ "touch_primary_display_rotation_desc": "Rotates native touch coordinates clockwise before mapping them to the primary display. Select the rotation that matches the display orientation expected by the application. Pen and mouse input are unaffected.",
"touchpad_as_ds4": "Emulate a DS4 gamepad if the client gamepad reports a touchpad is present",
"touchpad_as_ds4_desc": "If disabled, touchpad presence will not be taken into account during gamepad type selection.",
"upnp": "UPnP",
diff --git a/tests/unit/platform/windows/test_touch_target.cpp b/tests/unit/platform/windows/test_touch_target.cpp
index 708771fc26a..a7fd45064b7 100644
--- a/tests/unit/platform/windows/test_touch_target.cpp
+++ b/tests/unit/platform/windows/test_touch_target.cpp
@@ -111,6 +111,85 @@ TEST(WindowsTouchTargetTest, ExactVideoEdgeDoesNotReachAdjacentDesktop) {
EXPECT_EQ(bottom, 2159);
}
+TEST(WindowsTouchRotationTest, ParsesSupportedClockwiseRotations) {
+ EXPECT_EQ(platf::win_input::parse_touch_rotation("0"), platf::win_input::touch_rotation_e::none);
+ EXPECT_EQ(platf::win_input::parse_touch_rotation("90"), platf::win_input::touch_rotation_e::clockwise_90);
+ EXPECT_EQ(platf::win_input::parse_touch_rotation("180"), platf::win_input::touch_rotation_e::clockwise_180);
+ EXPECT_EQ(platf::win_input::parse_touch_rotation("270"), platf::win_input::touch_rotation_e::clockwise_270);
+ EXPECT_EQ(platf::win_input::parse_touch_rotation("invalid"), platf::win_input::touch_rotation_e::none);
+}
+
+TEST(WindowsTouchRotationTest, RotationRequiresASelectedPrimaryDisplay) {
+ EXPECT_EQ(
+ platf::win_input::select_touch_rotation(false, "90"),
+ platf::win_input::touch_rotation_e::none
+ );
+ EXPECT_EQ(
+ platf::win_input::select_touch_rotation(true, "90"),
+ platf::win_input::touch_rotation_e::clockwise_90
+ );
+}
+
+TEST(WindowsTouchRotationTest, Clockwise90MapsAllDisplayCorners) {
+ constexpr platf::touch_port_t primary_touch_port {0, 0, 1920, 1080, 0, 0};
+ constexpr auto rotation = platf::win_input::touch_rotation_e::clockwise_90;
+
+ const auto map_rotated_corner = [&](float x, float y) {
+ const auto [rotated_x, rotated_y] = platf::win_input::rotate_normalized_touch_position(x, y, rotation);
+ return platf::win_input::map_normalized_touch_position(primary_touch_port, rotated_x, rotated_y);
+ };
+
+ EXPECT_EQ(map_rotated_corner(0.0f, 0.0f), std::pair(1919, 0));
+ EXPECT_EQ(map_rotated_corner(0.0f, 1.0f), std::pair(0, 0));
+ EXPECT_EQ(map_rotated_corner(1.0f, 0.0f), std::pair(1919, 1079));
+ EXPECT_EQ(map_rotated_corner(1.0f, 1.0f), std::pair(0, 1079));
+}
+
+TEST(WindowsTouchRotationTest, Clockwise90CompensatesForCounterclockwiseApplicationTransform) {
+ constexpr float client_x = 0.2f;
+ constexpr float client_y = 0.7f;
+
+ const auto [injected_x, injected_y] = platf::win_input::rotate_normalized_touch_position(
+ client_x,
+ client_y,
+ platf::win_input::touch_rotation_e::clockwise_90
+ );
+
+ // An application applying (y, 1-x) receives the original point after Sunshine applies the inverse rotation.
+ const auto application_x = injected_y;
+ const auto application_y = 1.0f - injected_x;
+ EXPECT_FLOAT_EQ(application_x, client_x);
+ EXPECT_FLOAT_EQ(application_y, client_y);
+}
+
+TEST(WindowsTouchRotationTest, OtherRotationsTransformNormalizedCoordinates) {
+ const auto unchanged = platf::win_input::rotate_normalized_touch_position(
+ 0.25f,
+ 0.75f,
+ platf::win_input::touch_rotation_e::none
+ );
+ const auto clockwise_180 = platf::win_input::rotate_normalized_touch_position(
+ 0.25f,
+ 0.75f,
+ platf::win_input::touch_rotation_e::clockwise_180
+ );
+ const auto clockwise_270 = platf::win_input::rotate_normalized_touch_position(
+ 0.25f,
+ 0.75f,
+ platf::win_input::touch_rotation_e::clockwise_270
+ );
+ const auto unsupported = platf::win_input::rotate_normalized_touch_position(
+ 0.25f,
+ 0.75f,
+ static_cast(45)
+ );
+
+ EXPECT_EQ(unchanged, std::pair(0.25f, 0.75f));
+ EXPECT_EQ(clockwise_180, std::pair(0.75f, 0.25f));
+ EXPECT_EQ(clockwise_270, std::pair(0.75f, 0.75f));
+ EXPECT_EQ(unsupported, unchanged);
+}
+
TEST(WindowsTouchTargetTest, InvalidExtentAndNonFiniteCoordinatesStayAtDisplayOrigin) {
constexpr platf::touch_port_t invalid_touch_port {640, 360, 0, -1, 0, 0};
constexpr platf::touch_port_t valid_touch_port {640, 360, 1280, 768, 0, 0};
diff --git a/tests/unit/test_config_defaults.cpp b/tests/unit/test_config_defaults.cpp
index 6ba3b579377..338901cb563 100644
--- a/tests/unit/test_config_defaults.cpp
+++ b/tests/unit/test_config_defaults.cpp
@@ -16,4 +16,5 @@ TEST(ConfigDefaultsTest, InputDefaultsKeepNativeTouchEnabled) {
EXPECT_TRUE(config::input.high_resolution_scrolling);
EXPECT_TRUE(config::input.native_pen_touch);
EXPECT_FALSE(config::input.touch_send_to_primary_display);
+ EXPECT_EQ(config::input.touch_primary_display_rotation, "0");
}
From c571cc3f0b2f9b7f6f41ecafaf67350f14c9f156 Mon Sep 17 00:00:00 2001
From: James Liu <17558260+JamesLewisLiu@users.noreply.github.com>
Date: Sun, 2 Aug 2026 21:43:36 +0800
Subject: [PATCH 3/3] fix(windows): simplify touch rotation enum use
---
src/platform/windows/input.cpp | 20 ++++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp
index 8dda3344017..f93ca3cb957 100644
--- a/src/platform/windows/input.cpp
+++ b/src/platform/windows/input.cpp
@@ -120,16 +120,18 @@ namespace platf {
}
win_input::touch_rotation_e win_input::parse_touch_rotation(std::string_view rotation) {
+ using enum touch_rotation_e;
+
if (rotation == "90"sv) {
- return touch_rotation_e::clockwise_90;
+ return clockwise_90;
}
if (rotation == "180"sv) {
- return touch_rotation_e::clockwise_180;
+ return clockwise_180;
}
if (rotation == "270"sv) {
- return touch_rotation_e::clockwise_270;
+ return clockwise_270;
}
- return touch_rotation_e::none;
+ return none;
}
win_input::touch_rotation_e win_input::select_touch_rotation(
@@ -148,14 +150,16 @@ namespace platf {
float normalized_y,
win_input::touch_rotation_e rotation
) {
+ using enum touch_rotation_e;
+
switch (rotation) {
- case touch_rotation_e::clockwise_90:
+ case clockwise_90:
return {1.0f - normalized_y, normalized_x};
- case touch_rotation_e::clockwise_180:
+ case clockwise_180:
return {1.0f - normalized_x, 1.0f - normalized_y};
- case touch_rotation_e::clockwise_270:
+ case clockwise_270:
return {normalized_y, 1.0f - normalized_x};
- case touch_rotation_e::none:
+ case none:
default:
return {normalized_x, normalized_y};
}