mirror of
https://github.com/LizardByte/libvirtualhid.git
synced 2026-10-23 03:27:34 +09:00
fix(input): target configured mouse viewport (#145)
This commit is contained in:
1 parent
5ef6b9efa0
commit
24962b2092
18 files changed
+1116
-124
No files matched your search
@@ -22,3 +22,12 @@ features. `DeviceNode` paths can help diagnose host-side enumeration; client
|
||||
feature support still depends on the physical controller, connection, client,
|
||||
and game. See the [end-user compatibility matrix](end-user-gamepad-guide.md#compatibility-matrix)
|
||||
for observed streaming behavior.
|
||||
|
||||
Route `RuntimeOptions::log_callback` into the host logger for lifecycle,
|
||||
failure, and debug-level input coordinate diagnostics. When streaming one
|
||||
monitor from a multi-monitor desktop, supply the full desktop bounds and the
|
||||
selected viewport in `CreateMouseOptions`. See the
|
||||
[mouse viewport setup](usage.md#absolute-mouse-viewports).
|
||||
Use native screen units for that geometry: screen points on macOS and desktop
|
||||
pixels on Windows, Linux, and FreeBSD. The captured image dimensions supplied
|
||||
to `Mouse::move_absolute()` can differ from the viewport dimensions.
|
||||
@@ -64,6 +64,61 @@ The API also exposes `Keyboard`, `Mouse`, `Touchscreen`, `Trackpad`, and
|
||||
`PenTablet` where supported. `DeviceNode` provides paths for diagnostics or
|
||||
handoff to another local input consumer.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Hosts can route libvirtualhid diagnostics into their own logging system by
|
||||
installing `RuntimeOptions::log_callback` before creating the runtime:
|
||||
|
||||
```cpp
|
||||
lvh::RuntimeOptions runtime_options;
|
||||
runtime_options.backend = lvh::BackendKind::platform_default;
|
||||
runtime_options.log_callback = [](lvh::LogLevel level, const std::string &message) {
|
||||
host_log(level, message);
|
||||
};
|
||||
auto runtime = lvh::Runtime::create(runtime_options);
|
||||
```
|
||||
|
||||
The callback receives runtime and device lifecycle messages, operation failures,
|
||||
and debug-level mouse coordinate diagnostics. It runs synchronously on the
|
||||
calling thread. If a consumer callback throws, libvirtualhid disables it for
|
||||
subsequent messages so it cannot interrupt input delivery.
|
||||
|
||||
## Absolute Mouse Viewports
|
||||
|
||||
Absolute mouse coordinates can target one monitor inside a larger virtual
|
||||
desktop. Supply both the desktop bounds and the selected viewport in native
|
||||
screen coordinates when creating the mouse: screen points on macOS, and desktop
|
||||
pixels on Windows, Linux, and FreeBSD. Offsets and dimensions must use the same
|
||||
units.
|
||||
|
||||
```cpp
|
||||
lvh::CreateMouseOptions mouse_options;
|
||||
mouse_options.profile = lvh::profiles::mouse();
|
||||
mouse_options.desktop = {.offset_x = -1920, .offset_y = 0, .width = 3840, .height = 1080};
|
||||
mouse_options.viewport = {.offset_x = 0, .offset_y = 0, .width = 1920, .height = 1080};
|
||||
auto created = runtime->create_mouse(mouse_options);
|
||||
```
|
||||
|
||||
On macOS, use the global screen-point geometry reported by `CGDisplayBounds`
|
||||
for the desktop and viewport. A Retina display with 3840×2160 backing pixels
|
||||
and 1920×1080 screen points needs a 1920×1080 viewport. Absolute source
|
||||
coordinates may still use the 3840×2160 captured image dimensions; the backend
|
||||
maps that image's center to (960, 540) screen points. See Apple's
|
||||
[coordinate guidance](https://developer.apple.com/library/archive/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/APIs/APIs.html).
|
||||
|
||||
`Mouse::move_absolute()` coordinates are scaled from their supplied source
|
||||
dimensions into the target viewport, then normalized against the virtual
|
||||
desktop where the platform input API requires it. This contract covers
|
||||
CoreGraphics on macOS, `SendInput` on Windows, and the XTest or `uinput` path on
|
||||
Linux and FreeBSD, including virtual desktops whose origin is negative. Leave
|
||||
both viewport dimensions at zero to retain the platform-default pointer area
|
||||
(the main display on macOS and the virtual desktop on other current backends).
|
||||
The macOS default refreshes on every mouse submission that uses a location,
|
||||
including after display resolution or main-display changes. If its bounds are
|
||||
temporarily unavailable, the submission fails and a later submission retries.
|
||||
A configured target viewport must be fully contained by its desktop and remains
|
||||
fixed for the mouse's lifetime; recreate the mouse when that geometry changes.
|
||||
|
||||
## License and diagnostic tool
|
||||
|
||||
On Windows and macOS, `get_license_status`, `activate_license`,
|
||||
|
||||
+162
-7
@@ -5,9 +5,12 @@
|
||||
|
||||
// standard includes
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
// local includes
|
||||
@@ -19,6 +22,51 @@
|
||||
|
||||
namespace lvh::detail {
|
||||
|
||||
/**
|
||||
* @brief Exception-safe dispatcher for a consumer diagnostic callback.
|
||||
*/
|
||||
class Logger {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a dispatcher for an optional callback.
|
||||
*
|
||||
* @param callback Consumer diagnostic callback.
|
||||
*/
|
||||
explicit Logger(LogCallback callback):
|
||||
callback_ {std::move(callback)} {}
|
||||
|
||||
/**
|
||||
* @brief Report whether a callback is installed.
|
||||
*
|
||||
* @return `true` when messages have a destination.
|
||||
*/
|
||||
bool enabled() const noexcept {
|
||||
return static_cast<bool>(callback_) && !callback_failed_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Emit one diagnostic message without allowing consumer exceptions to escape.
|
||||
*
|
||||
* @param level Message severity.
|
||||
* @param message Diagnostic text.
|
||||
*/
|
||||
void emit(LogLevel level, const std::string &message) const noexcept {
|
||||
if (!enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callback_(level, message);
|
||||
} catch (...) {
|
||||
callback_failed_.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
LogCallback callback_;
|
||||
mutable std::atomic_bool callback_failed_ = false; ///< Whether the consumer callback threw an exception.
|
||||
};
|
||||
|
||||
class SynchronizedState {
|
||||
public:
|
||||
template<class Func>
|
||||
@@ -71,14 +119,21 @@ namespace lvh::detail {
|
||||
};
|
||||
|
||||
struct MouseDevice: SynchronizedState {
|
||||
explicit MouseDevice(DeviceId device_id, CreateMouseOptions create_options, std::unique_ptr<BackendMouse> backend_mouse):
|
||||
explicit MouseDevice(
|
||||
DeviceId device_id,
|
||||
CreateMouseOptions create_options,
|
||||
std::unique_ptr<BackendMouse> backend_mouse,
|
||||
std::shared_ptr<Logger> device_logger
|
||||
):
|
||||
id {device_id},
|
||||
options {std::move(create_options)},
|
||||
backend {std::move(backend_mouse)} {}
|
||||
backend {std::move(backend_mouse)},
|
||||
logger {std::move(device_logger)} {}
|
||||
|
||||
DeviceId id;
|
||||
CreateMouseOptions options;
|
||||
std::unique_ptr<BackendMouse> backend;
|
||||
std::shared_ptr<Logger> logger;
|
||||
bool open = true;
|
||||
MouseEvent last_event;
|
||||
std::size_t submitted_events = 0;
|
||||
@@ -141,11 +196,15 @@ namespace lvh::detail {
|
||||
class RuntimeState: public SynchronizedState {
|
||||
public:
|
||||
explicit RuntimeState(RuntimeOptions runtime_options):
|
||||
options {runtime_options},
|
||||
backend {create_backend(runtime_options.backend)},
|
||||
caps {backend->capabilities()} {}
|
||||
options {std::move(runtime_options)},
|
||||
logger {std::make_shared<Logger>(options.log_callback)},
|
||||
backend {create_backend(options.backend)},
|
||||
caps {backend->capabilities()} {
|
||||
logger->emit(LogLevel::info, "initialized " + caps.backend_name + " backend");
|
||||
}
|
||||
|
||||
RuntimeOptions options;
|
||||
std::shared_ptr<Logger> logger;
|
||||
std::unique_ptr<Backend> backend;
|
||||
BackendCapabilities caps;
|
||||
DeviceId next_device_id = 1;
|
||||
@@ -199,10 +258,91 @@ namespace lvh {
|
||||
if (options.profile.name.empty()) {
|
||||
return OperationStatus::failure(ErrorCode::invalid_argument, "device profile name must not be empty");
|
||||
}
|
||||
if (const auto valid_dimensions = [](const PointerViewport &viewport) {
|
||||
return viewport.width >= 0 && viewport.height >= 0 &&
|
||||
((viewport.width == 0 && viewport.height == 0) || (viewport.width > 0 && viewport.height > 0));
|
||||
};
|
||||
!valid_dimensions(options.desktop) || !valid_dimensions(options.viewport)) {
|
||||
return OperationStatus::failure(
|
||||
ErrorCode::invalid_argument,
|
||||
"mouse viewport dimensions must both be positive or both be zero"
|
||||
);
|
||||
}
|
||||
const auto has_desktop = options.desktop.width > 0;
|
||||
if (const auto has_viewport = options.viewport.width > 0; has_desktop != has_viewport) {
|
||||
return OperationStatus::failure(
|
||||
ErrorCode::invalid_argument,
|
||||
"mouse desktop and target viewport must be configured together"
|
||||
);
|
||||
}
|
||||
if (has_desktop) {
|
||||
const auto desktop_right = static_cast<std::int64_t>(options.desktop.offset_x) + options.desktop.width;
|
||||
const auto desktop_bottom = static_cast<std::int64_t>(options.desktop.offset_y) + options.desktop.height;
|
||||
const auto viewport_right = static_cast<std::int64_t>(options.viewport.offset_x) + options.viewport.width;
|
||||
const auto viewport_bottom = static_cast<std::int64_t>(options.viewport.offset_y) + options.viewport.height;
|
||||
if (options.viewport.offset_x < options.desktop.offset_x || options.viewport.offset_y < options.desktop.offset_y || viewport_right > desktop_right || viewport_bottom > desktop_bottom) {
|
||||
return OperationStatus::failure(
|
||||
ErrorCode::invalid_argument,
|
||||
"mouse target viewport must be contained by the virtual desktop"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return OperationStatus::success();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Format a mouse event for the consumer diagnostic callback.
|
||||
*
|
||||
* @param id Runtime mouse identifier.
|
||||
* @param event Mouse event being submitted.
|
||||
* @param desktop Configured virtual desktop bounds.
|
||||
* @param viewport Configured target viewport.
|
||||
* @return Human-readable diagnostic message.
|
||||
*/
|
||||
std::string mouse_event_description(
|
||||
DeviceId id,
|
||||
const MouseEvent &event,
|
||||
const PointerViewport &desktop,
|
||||
const PointerViewport &viewport
|
||||
) {
|
||||
std::ostringstream message;
|
||||
message << "mouse " << id << ' ';
|
||||
switch (event.kind) {
|
||||
using enum MouseEventKind;
|
||||
|
||||
case relative_motion:
|
||||
message << "relative motion x=" << event.x << " y=" << event.y;
|
||||
break;
|
||||
case absolute_motion:
|
||||
message << "absolute motion x=";
|
||||
if (event.has_fractional_absolute_coordinates) {
|
||||
message << event.absolute_x << " y=" << event.absolute_y;
|
||||
} else {
|
||||
message << event.x << " y=" << event.y;
|
||||
}
|
||||
message << " source=" << event.width << 'x' << event.height;
|
||||
if (viewport.width > 0 && viewport.height > 0) {
|
||||
message << " viewport=" << viewport.offset_x << ',' << viewport.offset_y << ' '
|
||||
<< viewport.width << 'x' << viewport.height << " desktop=" << desktop.offset_x << ','
|
||||
<< desktop.offset_y << ' ' << desktop.width << 'x' << desktop.height;
|
||||
} else {
|
||||
message << " viewport=platform-default";
|
||||
}
|
||||
break;
|
||||
case button:
|
||||
message << "button " << static_cast<int>(std::to_underlying(event.button)) << (event.pressed ? " pressed" : " released");
|
||||
break;
|
||||
case vertical_scroll:
|
||||
message << "vertical scroll distance=" << event.high_resolution_scroll;
|
||||
break;
|
||||
case horizontal_scroll:
|
||||
message << "horizontal scroll distance=" << event.high_resolution_scroll;
|
||||
break;
|
||||
}
|
||||
return message.str();
|
||||
}
|
||||
|
||||
OperationStatus validate_touchscreen_options(const CreateTouchscreenOptions &options) {
|
||||
if (options.profile.device_type != DeviceType::touchscreen) {
|
||||
return OperationStatus::failure(ErrorCode::unsupported_profile, "device profile is not a touchscreen");
|
||||
@@ -612,10 +752,18 @@ namespace lvh {
|
||||
|
||||
OperationStatus Mouse::submit(const MouseEvent &event) {
|
||||
if (const auto validation = validate_mouse_event(event); !validation.ok()) {
|
||||
device_->logger->emit(LogLevel::warning, "rejected mouse event: " + validation.message());
|
||||
return validation;
|
||||
}
|
||||
|
||||
return with_device(device_, [&event](auto &device) {
|
||||
if (device_->logger->enabled()) {
|
||||
device_->logger->emit(
|
||||
LogLevel::debug,
|
||||
mouse_event_description(device_->id, event, device_->options.desktop, device_->options.viewport)
|
||||
);
|
||||
}
|
||||
|
||||
const auto status = with_device(device_, [&event](auto &device) {
|
||||
if (!device.open) {
|
||||
return OperationStatus::failure(ErrorCode::device_closed, "mouse is closed");
|
||||
}
|
||||
@@ -630,6 +778,10 @@ namespace lvh {
|
||||
++device.submitted_events;
|
||||
return OperationStatus::success();
|
||||
});
|
||||
if (!status.ok()) {
|
||||
device_->logger->emit(LogLevel::error, "mouse input failed: " + status.message());
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
OperationStatus Mouse::move_relative(std::int32_t delta_x, std::int32_t delta_y) {
|
||||
@@ -1129,6 +1281,7 @@ namespace lvh {
|
||||
|
||||
MouseCreationResult Runtime::create_mouse(const CreateMouseOptions &options) {
|
||||
if (const auto validation = validate_mouse_options(options); !validation.ok()) {
|
||||
state_->logger->emit(LogLevel::warning, "rejected mouse creation: " + validation.message());
|
||||
return {validation, nullptr};
|
||||
}
|
||||
|
||||
@@ -1138,15 +1291,17 @@ namespace lvh {
|
||||
|
||||
auto backend_result = state_->backend->create_mouse(id, options);
|
||||
if (!backend_result) {
|
||||
state_->logger->emit(LogLevel::error, "mouse creation failed: " + backend_result.status.message());
|
||||
return {std::move(backend_result.status), nullptr};
|
||||
}
|
||||
|
||||
auto device = std::make_shared<detail::MouseDevice>(id, options, std::move(backend_result.mouse));
|
||||
auto device = std::make_shared<detail::MouseDevice>(id, options, std::move(backend_result.mouse), state_->logger);
|
||||
state_->with_lock([this, &device]() {
|
||||
state_->mice.emplace_back(device);
|
||||
});
|
||||
|
||||
auto mouse = std::make_unique<Mouse>(detail::RuntimeConstructionToken {}, std::move(device));
|
||||
state_->logger->emit(LogLevel::info, std::format("created mouse {}", id));
|
||||
return {OperationStatus::success(), std::move(mouse)};
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,24 @@ namespace lvh {
|
||||
platform_default, ///< Native backend for the current platform.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Diagnostic message severity.
|
||||
*/
|
||||
enum class LogLevel : std::uint8_t {
|
||||
debug, ///< Detailed diagnostic information.
|
||||
info, ///< Normal runtime lifecycle information.
|
||||
warning, ///< Recoverable problem or fallback.
|
||||
error, ///< Operation failure.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Consumer callback that receives libvirtualhid diagnostic messages.
|
||||
*
|
||||
* The callback is invoked synchronously from the thread performing the
|
||||
* operation. If the callback throws, it is disabled for subsequent messages.
|
||||
*/
|
||||
using LogCallback = std::function<void(LogLevel level, const std::string &message)>;
|
||||
|
||||
/**
|
||||
* @brief Runtime creation options.
|
||||
*/
|
||||
@@ -115,6 +133,11 @@ namespace lvh {
|
||||
* @brief Backend implementation requested by the caller.
|
||||
*/
|
||||
BackendKind backend = BackendKind::fake;
|
||||
|
||||
/**
|
||||
* @brief Optional callback for diagnostic messages.
|
||||
*/
|
||||
LogCallback log_callback;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -442,6 +465,36 @@ namespace lvh {
|
||||
std::string stable_id;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Viewport in the platform's native screen coordinate space.
|
||||
*
|
||||
* Mouse desktop and viewport geometry use screen points on macOS and desktop
|
||||
* pixels on Windows, Linux, and FreeBSD. Use the same units for offsets and
|
||||
* dimensions; source coordinates supplied to Mouse::move_absolute() remain
|
||||
* in the caller's source coordinate space and are scaled into this viewport.
|
||||
*/
|
||||
struct PointerViewport {
|
||||
/**
|
||||
* @brief Horizontal viewport offset in native screen coordinates.
|
||||
*/
|
||||
std::int32_t offset_x = 0;
|
||||
|
||||
/**
|
||||
* @brief Vertical viewport offset in native screen coordinates.
|
||||
*/
|
||||
std::int32_t offset_y = 0;
|
||||
|
||||
/**
|
||||
* @brief Viewport width in native screen units, or `0` to use the platform default.
|
||||
*/
|
||||
std::int32_t width = 0;
|
||||
|
||||
/**
|
||||
* @brief Viewport height in native screen units, or `0` to use the platform default.
|
||||
*/
|
||||
std::int32_t height = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Full mouse creation request.
|
||||
*/
|
||||
@@ -455,6 +508,25 @@ namespace lvh {
|
||||
* @brief Consumer-defined stable identity string.
|
||||
*/
|
||||
std::string stable_id;
|
||||
|
||||
/**
|
||||
* @brief Native virtual-desktop bounds used to normalize the target viewport.
|
||||
*
|
||||
* Set this together with `viewport`; leave both dimensions at zero to use
|
||||
* the platform-default pointer area. Use screen points on macOS and desktop
|
||||
* pixels on Windows, Linux, and FreeBSD.
|
||||
*/
|
||||
PointerViewport desktop;
|
||||
|
||||
/**
|
||||
* @brief Native desktop viewport that receives absolute mouse input.
|
||||
*
|
||||
* Set this together with `desktop`; leave both dimensions at zero to use
|
||||
* the platform-default pointer area. On macOS the default follows the current
|
||||
* main display bounds on each submission; configured geometry remains fixed
|
||||
* for the mouse's lifetime.
|
||||
*/
|
||||
PointerViewport viewport;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -768,31 +840,6 @@ namespace lvh {
|
||||
std::string text;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Pixel viewport used by backends that need screen-local pointer coordinates.
|
||||
*/
|
||||
struct PointerViewport {
|
||||
/**
|
||||
* @brief Horizontal viewport offset in pixels.
|
||||
*/
|
||||
std::int32_t offset_x = 0;
|
||||
|
||||
/**
|
||||
* @brief Vertical viewport offset in pixels.
|
||||
*/
|
||||
std::int32_t offset_y = 0;
|
||||
|
||||
/**
|
||||
* @brief Viewport width in pixels, or `0` to use the platform default.
|
||||
*/
|
||||
std::int32_t width = 0;
|
||||
|
||||
/**
|
||||
* @brief Viewport height in pixels, or `0` to use the platform default.
|
||||
*/
|
||||
std::int32_t height = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Pointer state transition requested for contact-capable devices.
|
||||
*/
|
||||
|
||||
@@ -808,6 +808,37 @@ namespace lvh::detail {
|
||||
return static_cast<int>(numerator / limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Scale a source coordinate through a target viewport into an absolute input axis.
|
||||
*
|
||||
* @param value Source coordinate.
|
||||
* @param source_dimension Source coordinate-space dimension.
|
||||
* @param viewport_offset Native target viewport offset.
|
||||
* @param viewport_dimension Native target viewport dimension.
|
||||
* @param desktop_offset Native virtual-desktop origin.
|
||||
* @param desktop_dimension Native virtual-desktop dimension.
|
||||
* @return Normalized evdev absolute-axis value.
|
||||
*/
|
||||
int scale_absolute_axis_to_viewport(
|
||||
float value,
|
||||
std::int32_t source_dimension,
|
||||
std::int32_t viewport_offset,
|
||||
std::int32_t viewport_dimension,
|
||||
std::int32_t desktop_offset,
|
||||
std::int32_t desktop_dimension
|
||||
) {
|
||||
if (source_dimension <= 0 || desktop_dimension <= 0 || viewport_dimension <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto clamped = std::clamp(value, 0.0F, static_cast<float>(source_dimension));
|
||||
const auto viewport_span = static_cast<float>(std::max(viewport_dimension - 1, 0));
|
||||
const auto target = static_cast<float>(viewport_offset - desktop_offset) +
|
||||
clamped * viewport_span / static_cast<float>(source_dimension);
|
||||
const auto normalized = std::clamp(target / static_cast<float>(desktop_dimension), 0.0F, 1.0F);
|
||||
return static_cast<int>(std::lround(normalized * static_cast<float>(absolute_axis_max)));
|
||||
}
|
||||
|
||||
int scale_normalized_axis(float value, int maximum) {
|
||||
return static_cast<int>(std::lround(std::clamp(value, 0.0F, 1.0F) * static_cast<float>(maximum)));
|
||||
}
|
||||
@@ -1692,15 +1723,24 @@ namespace lvh::detail {
|
||||
*/
|
||||
class UinputMouse final: public BackendMouse {
|
||||
public:
|
||||
UinputMouse(int relative_file_descriptor, int absolute_file_descriptor):
|
||||
UinputMouse(
|
||||
int relative_file_descriptor,
|
||||
int absolute_file_descriptor,
|
||||
PointerViewport desktop = {},
|
||||
PointerViewport viewport = {}
|
||||
):
|
||||
relative_device_ {relative_file_descriptor},
|
||||
absolute_device_ {absolute_file_descriptor} {}
|
||||
absolute_device_ {absolute_file_descriptor},
|
||||
desktop_ {desktop},
|
||||
viewport_ {viewport} {}
|
||||
|
||||
~UinputMouse() override {
|
||||
static_cast<void>(close());
|
||||
}
|
||||
|
||||
OperationStatus create(DeviceId id, const CreateMouseOptions &options) {
|
||||
desktop_ = options.desktop;
|
||||
viewport_ = options.viewport;
|
||||
if (const auto status = relative_device_.create(id, options.profile, UinputMouseDeviceKind::relative); !status.ok()) {
|
||||
return status;
|
||||
}
|
||||
@@ -1749,6 +1789,8 @@ namespace lvh::detail {
|
||||
private:
|
||||
UinputMouseDevice relative_device_;
|
||||
UinputMouseDevice absolute_device_;
|
||||
PointerViewport desktop_; ///< Native virtual-desktop bounds used for absolute input.
|
||||
PointerViewport viewport_; ///< Native target viewport used for absolute input.
|
||||
UinputMouseDeviceKind last_motion_device_ = UinputMouseDeviceKind::relative;
|
||||
std::byte relative_buttons_down_ {};
|
||||
std::byte absolute_buttons_down_ {};
|
||||
@@ -1805,10 +1847,32 @@ namespace lvh::detail {
|
||||
|
||||
OperationStatus submit_absolute_motion(const MouseEvent &event) {
|
||||
auto &absolute = device(UinputMouseDeviceKind::absolute);
|
||||
if (const auto status = absolute.emit(EV_ABS, ABS_X, scale_absolute_axis(event.x, event.width)); !status.ok()) {
|
||||
const auto x = event.has_fractional_absolute_coordinates ? event.absolute_x : static_cast<float>(event.x);
|
||||
const auto y = event.has_fractional_absolute_coordinates ? event.absolute_y : static_cast<float>(event.y);
|
||||
const auto absolute_x = viewport_.width > 0 ?
|
||||
scale_absolute_axis_to_viewport(
|
||||
x,
|
||||
event.width,
|
||||
viewport_.offset_x,
|
||||
viewport_.width,
|
||||
desktop_.offset_x,
|
||||
desktop_.width
|
||||
) :
|
||||
scale_absolute_axis(event.x, event.width);
|
||||
const auto absolute_y = viewport_.height > 0 ?
|
||||
scale_absolute_axis_to_viewport(
|
||||
y,
|
||||
event.height,
|
||||
viewport_.offset_y,
|
||||
viewport_.height,
|
||||
desktop_.offset_y,
|
||||
desktop_.height
|
||||
) :
|
||||
scale_absolute_axis(event.y, event.height);
|
||||
if (const auto status = absolute.emit(EV_ABS, ABS_X, absolute_x); !status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (const auto status = absolute.emit(EV_ABS, ABS_Y, scale_absolute_axis(event.y, event.height)); !status.ok()) {
|
||||
if (const auto status = absolute.emit(EV_ABS, ABS_Y, absolute_y); !status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (const auto status = absolute.synchronize(); !status.ok()) {
|
||||
@@ -2478,7 +2542,9 @@ namespace lvh::detail {
|
||||
static_cast<void>(close());
|
||||
}
|
||||
|
||||
OperationStatus create() {
|
||||
OperationStatus create(const CreateMouseOptions &options = {}) {
|
||||
desktop_ = options.desktop;
|
||||
viewport_ = options.viewport;
|
||||
display_ = XOpenDisplay(nullptr);
|
||||
if (display_ == nullptr) {
|
||||
return OperationStatus::failure(ErrorCode::backend_unavailable, "failed to open X display for XTest mouse fallback");
|
||||
@@ -2530,11 +2596,23 @@ namespace lvh::detail {
|
||||
private:
|
||||
void submit_absolute_motion(const MouseEvent &event) {
|
||||
const auto screen = DefaultScreen(display_);
|
||||
const auto screen_width = DisplayWidth(display_, screen);
|
||||
const auto screen_height = DisplayHeight(display_, screen);
|
||||
const auto x = scale_absolute_axis(event.x, event.width) * std::max(screen_width - 1, 0) / absolute_axis_max;
|
||||
const auto y = scale_absolute_axis(event.y, event.height) * std::max(screen_height - 1, 0) / absolute_axis_max;
|
||||
XTestFakeMotionEvent(display_, screen, x, y, CurrentTime);
|
||||
const auto screen_width = std::max(DisplayWidth(display_, screen) - 1, 0);
|
||||
const auto screen_height = std::max(DisplayHeight(display_, screen) - 1, 0);
|
||||
const auto event_x = event.has_fractional_absolute_coordinates ? event.absolute_x : static_cast<float>(event.x);
|
||||
const auto event_y = event.has_fractional_absolute_coordinates ? event.absolute_y : static_cast<float>(event.y);
|
||||
const auto x = viewport_.width > 0 ?
|
||||
viewport_.offset_x - desktop_.offset_x + static_cast<int>(std::lround(std::clamp(event_x, 0.0F, static_cast<float>(event.width)) * static_cast<float>(std::max(viewport_.width - 1, 0)) / static_cast<float>(event.width))) :
|
||||
scale_absolute_axis(event.x, event.width) * screen_width / absolute_axis_max;
|
||||
const auto y = viewport_.height > 0 ?
|
||||
viewport_.offset_y - desktop_.offset_y + static_cast<int>(std::lround(std::clamp(event_y, 0.0F, static_cast<float>(event.height)) * static_cast<float>(std::max(viewport_.height - 1, 0)) / static_cast<float>(event.height))) :
|
||||
scale_absolute_axis(event.y, event.height) * screen_height / absolute_axis_max;
|
||||
XTestFakeMotionEvent(
|
||||
display_,
|
||||
screen,
|
||||
std::clamp(x, 0, screen_width),
|
||||
std::clamp(y, 0, screen_height),
|
||||
CurrentTime
|
||||
);
|
||||
}
|
||||
|
||||
void submit_scroll(std::int32_t distance, int positive_button, int negative_button) {
|
||||
@@ -2547,6 +2625,8 @@ namespace lvh::detail {
|
||||
}
|
||||
|
||||
Display *display_ = nullptr;
|
||||
PointerViewport desktop_; ///< Native virtual-desktop bounds represented by the X root window.
|
||||
PointerViewport viewport_; ///< Native target viewport used for absolute input.
|
||||
};
|
||||
#else
|
||||
bool can_use_xtest() {
|
||||
@@ -3611,19 +3691,19 @@ namespace lvh::detail {
|
||||
BackendMouseCreationResult create_mouse(DeviceId id, const CreateMouseOptions &options) override {
|
||||
const auto relative_fd = open_uinput(O_RDWR | O_CLOEXEC | O_NONBLOCK);
|
||||
if (relative_fd < 0) {
|
||||
return create_xtest_mouse();
|
||||
return create_xtest_mouse(options);
|
||||
}
|
||||
|
||||
const auto absolute_fd = open_uinput(O_RDWR | O_CLOEXEC | O_NONBLOCK);
|
||||
if (absolute_fd < 0) {
|
||||
static_cast<void>(system_close(relative_fd));
|
||||
return create_xtest_mouse();
|
||||
return create_xtest_mouse(options);
|
||||
}
|
||||
|
||||
auto mouse = std::make_unique<UinputMouse>(relative_fd, absolute_fd);
|
||||
if (const auto status = mouse->create(id, options); !status.ok()) {
|
||||
static_cast<void>(mouse->close());
|
||||
auto fallback = create_xtest_mouse();
|
||||
auto fallback = create_xtest_mouse(options);
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
}
|
||||
@@ -3694,10 +3774,10 @@ namespace lvh::detail {
|
||||
#endif
|
||||
}
|
||||
|
||||
BackendMouseCreationResult create_xtest_mouse() {
|
||||
BackendMouseCreationResult create_xtest_mouse(const CreateMouseOptions &options) {
|
||||
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
||||
auto mouse = std::make_unique<XTestMouse>();
|
||||
if (const auto status = mouse->create(); !status.ok()) {
|
||||
if (const auto status = mouse->create(options); !status.ok()) {
|
||||
return {status, nullptr};
|
||||
}
|
||||
return {OperationStatus::success(), std::move(mouse)};
|
||||
|
||||
@@ -459,14 +459,35 @@ namespace lvh::detail {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Resolve a portable pointer viewport to CoreGraphics desktop bounds.
|
||||
*
|
||||
* @param viewport Consumer-selected viewport in CoreGraphics screen points, or zero dimensions for the main display.
|
||||
* @return Current CoreGraphics bounds used for mouse mapping and confinement.
|
||||
*/
|
||||
inline CGRect mouse_viewport_bounds(const PointerViewport &viewport) {
|
||||
if (viewport.width > 0 && viewport.height > 0) {
|
||||
return CGRect {
|
||||
.origin = CGPoint {
|
||||
static_cast<CGFloat>(viewport.offset_x),
|
||||
static_cast<CGFloat>(viewport.offset_y),
|
||||
},
|
||||
.size = CGSize {
|
||||
static_cast<CGFloat>(viewport.width),
|
||||
static_cast<CGFloat>(viewport.height),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return CGDisplayBounds(CGMainDisplayID());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shared macOS backend state.
|
||||
*/
|
||||
class MacosInputState {
|
||||
public:
|
||||
MacosInputState():
|
||||
display {CGMainDisplayID()},
|
||||
display_scaling {display_scaling_for(display)},
|
||||
source {CGEventSourceCreate(kCGEventSourceStateHIDSystemState)},
|
||||
keyboard_source {CGEventSourceCreate(kCGEventSourceStatePrivate)},
|
||||
mouse_event {source ? CGEventCreate(source) : nullptr},
|
||||
@@ -489,31 +510,6 @@ namespace lvh::detail {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Compute the coordinate scaling factor for a display.
|
||||
*
|
||||
* @param display_id CoreGraphics display identifier.
|
||||
* @return Coordinate scaling factor.
|
||||
*/
|
||||
static CGFloat display_scaling_for(CGDirectDisplayID display_id) {
|
||||
const auto mode = CGDisplayCopyDisplayMode(display_id);
|
||||
if (!mode) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
const auto logical_width = CGDisplayModeGetPixelWidth(mode);
|
||||
if (logical_width == 0) {
|
||||
CFRelease(mode);
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
const auto scaling = static_cast<CGFloat>(CGDisplayPixelsWide(display_id)) / static_cast<CGFloat>(logical_width);
|
||||
CFRelease(mode);
|
||||
return scaling;
|
||||
}
|
||||
|
||||
CGDirectDisplayID display {}; ///< CoreGraphics identifier for the target display.
|
||||
CGFloat display_scaling = 1.0; ///< Scaling factor from logical to physical display pixels.
|
||||
CGEventSourceRef source {}; ///< CoreGraphics event source for mouse and scroll events.
|
||||
CGEventSourceRef keyboard_source {}; ///< CoreGraphics event source for keyboard events.
|
||||
CGEventRef mouse_event {}; ///< Reusable CoreGraphics mouse event.
|
||||
@@ -728,8 +724,9 @@ namespace lvh::detail {
|
||||
*/
|
||||
class MacosMouse final: public BackendMouse {
|
||||
public:
|
||||
explicit MacosMouse(std::shared_ptr<MacosInputState> state):
|
||||
state_ {std::move(state)} {}
|
||||
MacosMouse(std::shared_ptr<MacosInputState> state, const PointerViewport &viewport):
|
||||
state_ {std::move(state)},
|
||||
viewport_ {viewport} {}
|
||||
|
||||
~MacosMouse() override {
|
||||
static_cast<void>(close());
|
||||
@@ -746,6 +743,13 @@ namespace lvh::detail {
|
||||
return OperationStatus::failure(ErrorCode::backend_failure, "macOS mouse event source is unavailable");
|
||||
}
|
||||
|
||||
if (event.kind == relative_motion || event.kind == absolute_motion || event.kind == button) {
|
||||
viewport_bounds_ = mouse_viewport_bounds(viewport_);
|
||||
if (viewport_bounds_.size.width < 1 || viewport_bounds_.size.height < 1) {
|
||||
return OperationStatus::failure(ErrorCode::backend_failure, "macOS mouse viewport is unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.kind) {
|
||||
case relative_motion:
|
||||
return submit_relative_motion(event.x, event.y);
|
||||
@@ -772,8 +776,7 @@ namespace lvh::detail {
|
||||
CGPoint current_location() const {
|
||||
const auto snapshot_event = CGEventCreate(state_->source);
|
||||
if (!snapshot_event) {
|
||||
const auto display_bounds = CGDisplayBounds(state_->display);
|
||||
return display_bounds.origin;
|
||||
return viewport_bounds_.origin;
|
||||
}
|
||||
|
||||
const auto current = CGEventGetLocation(snapshot_event);
|
||||
@@ -788,10 +791,9 @@ namespace lvh::detail {
|
||||
CGPoint previous_location,
|
||||
int click_count
|
||||
) const {
|
||||
const auto display_bounds = CGDisplayBounds(state_->display);
|
||||
const auto location = CGPoint {
|
||||
std::clamp(raw_location.x, display_bounds.origin.x, display_bounds.origin.x + display_bounds.size.width - 1),
|
||||
std::clamp(raw_location.y, display_bounds.origin.y, display_bounds.origin.y + display_bounds.size.height - 1)
|
||||
std::clamp(raw_location.x, viewport_bounds_.origin.x, viewport_bounds_.origin.x + viewport_bounds_.size.width - 1),
|
||||
std::clamp(raw_location.y, viewport_bounds_.origin.y, viewport_bounds_.origin.y + viewport_bounds_.size.height - 1)
|
||||
};
|
||||
|
||||
const auto event = state_->mouse_event;
|
||||
@@ -820,8 +822,7 @@ namespace lvh::detail {
|
||||
}
|
||||
|
||||
OperationStatus submit_absolute_motion(const MouseEvent &event) {
|
||||
const auto display_bounds = CGDisplayBounds(state_->display);
|
||||
const auto location = absolute_mouse_location(event, display_bounds);
|
||||
const auto location = absolute_mouse_location(event, viewport_bounds_);
|
||||
const auto motion = macos_mouse_motion(mouse_down_);
|
||||
return post_mouse(motion.button, motion.event_type, location, current_location(), 0);
|
||||
}
|
||||
@@ -865,6 +866,8 @@ namespace lvh::detail {
|
||||
}
|
||||
|
||||
std::shared_ptr<MacosInputState> state_;
|
||||
PointerViewport viewport_; ///< Requested screen geometry, with zero dimensions selecting the current main display.
|
||||
CGRect viewport_bounds_ {}; ///< CoreGraphics bounds resolved for the current mouse submission.
|
||||
std::array<bool, 3> mouse_down_ {};
|
||||
std::array<std::array<std::chrono::steady_clock::time_point, 2>, 3> last_mouse_event_ {};
|
||||
std::mutex mutex_;
|
||||
@@ -914,7 +917,7 @@ namespace lvh::detail {
|
||||
return {OperationStatus::failure(ErrorCode::backend_failure, "macOS mouse event source is unavailable"), nullptr};
|
||||
}
|
||||
|
||||
return {OperationStatus::success(), std::make_unique<MacosMouse>(std::move(state))};
|
||||
return {OperationStatus::success(), std::make_unique<MacosMouse>(std::move(state), options.viewport)};
|
||||
}
|
||||
|
||||
BackendTouchscreenCreationResult create_touchscreen(
|
||||
|
||||
@@ -432,6 +432,36 @@ namespace lvh::detail {
|
||||
return static_cast<LONG>(std::lround(scaled));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Scale an absolute source coordinate through a target viewport into a virtual desktop axis.
|
||||
*
|
||||
* @param value Absolute source coordinate.
|
||||
* @param source_dimension Source coordinate-space dimension.
|
||||
* @param viewport_offset Target viewport offset in native desktop pixels.
|
||||
* @param viewport_dimension Target viewport dimension in native desktop pixels.
|
||||
* @param desktop_offset Virtual desktop origin in native desktop pixels.
|
||||
* @param desktop_dimension Virtual desktop dimension in native desktop pixels.
|
||||
* @return Win32 normalized absolute coordinate.
|
||||
*/
|
||||
LONG scale_absolute_axis_to_viewport(
|
||||
float value,
|
||||
std::int32_t source_dimension,
|
||||
std::int32_t viewport_offset,
|
||||
std::int32_t viewport_dimension,
|
||||
std::int32_t desktop_offset,
|
||||
std::int32_t desktop_dimension
|
||||
) {
|
||||
if (source_dimension <= 0 || viewport_dimension <= 0 || desktop_dimension <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto clamped = std::clamp(value, 0.0F, static_cast<float>(source_dimension));
|
||||
const auto viewport_span = static_cast<float>(std::max(viewport_dimension - 1, 0));
|
||||
const auto target = static_cast<float>(viewport_offset - desktop_offset) +
|
||||
clamped * viewport_span / static_cast<float>(source_dimension);
|
||||
return scale_absolute_axis(target, desktop_dimension);
|
||||
}
|
||||
|
||||
PointerViewport resolve_pointer_viewport(PointerViewport viewport) {
|
||||
if (viewport.width > 0 && viewport.height > 0) {
|
||||
return viewport;
|
||||
@@ -2146,6 +2176,16 @@ namespace lvh::detail {
|
||||
|
||||
class WindowsMouse final: public BackendMouse {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a Win32 mouse injection backend.
|
||||
*
|
||||
* @param desktop Native virtual-desktop bounds.
|
||||
* @param viewport Native desktop viewport receiving absolute input.
|
||||
*/
|
||||
explicit WindowsMouse(PointerViewport desktop = {}, PointerViewport viewport = {}):
|
||||
desktop_ {desktop},
|
||||
viewport_ {viewport} {}
|
||||
|
||||
OperationStatus submit(const MouseEvent &event) override {
|
||||
using enum ErrorCode;
|
||||
|
||||
@@ -2167,14 +2207,33 @@ namespace lvh::detail {
|
||||
break;
|
||||
case absolute_motion:
|
||||
mouse.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
|
||||
mouse.dx = scale_absolute_axis(
|
||||
event.has_fractional_absolute_coordinates ? event.absolute_x : static_cast<float>(event.x),
|
||||
event.width
|
||||
);
|
||||
mouse.dy = scale_absolute_axis(
|
||||
event.has_fractional_absolute_coordinates ? event.absolute_y : static_cast<float>(event.y),
|
||||
event.height
|
||||
);
|
||||
if (viewport_.width > 0 && viewport_.height > 0) {
|
||||
mouse.dx = scale_absolute_axis_to_viewport(
|
||||
event.has_fractional_absolute_coordinates ? event.absolute_x : static_cast<float>(event.x),
|
||||
event.width,
|
||||
viewport_.offset_x,
|
||||
viewport_.width,
|
||||
desktop_.offset_x,
|
||||
desktop_.width
|
||||
);
|
||||
mouse.dy = scale_absolute_axis_to_viewport(
|
||||
event.has_fractional_absolute_coordinates ? event.absolute_y : static_cast<float>(event.y),
|
||||
event.height,
|
||||
viewport_.offset_y,
|
||||
viewport_.height,
|
||||
desktop_.offset_y,
|
||||
desktop_.height
|
||||
);
|
||||
} else {
|
||||
mouse.dx = scale_absolute_axis(
|
||||
event.has_fractional_absolute_coordinates ? event.absolute_x : static_cast<float>(event.x),
|
||||
event.width
|
||||
);
|
||||
mouse.dy = scale_absolute_axis(
|
||||
event.has_fractional_absolute_coordinates ? event.absolute_y : static_cast<float>(event.y),
|
||||
event.height
|
||||
);
|
||||
}
|
||||
break;
|
||||
case button:
|
||||
mouse.dwFlags = mouse_button_flags(event.button, event.pressed);
|
||||
@@ -2199,6 +2258,8 @@ namespace lvh::detail {
|
||||
}
|
||||
|
||||
private:
|
||||
PointerViewport desktop_; ///< Native virtual-desktop bounds.
|
||||
PointerViewport viewport_; ///< Native desktop viewport receiving absolute input.
|
||||
bool open_ = true;
|
||||
};
|
||||
|
||||
@@ -2278,10 +2339,13 @@ namespace lvh::detail {
|
||||
public:
|
||||
WindowsHidMouse(
|
||||
std::shared_ptr<WindowsBackendContext> context,
|
||||
std::shared_ptr<WindowsVhfDeviceState> state
|
||||
std::shared_ptr<WindowsVhfDeviceState> state,
|
||||
const PointerViewport &desktop,
|
||||
const PointerViewport &viewport
|
||||
):
|
||||
context_ {std::move(context)},
|
||||
state_ {std::move(state)} {}
|
||||
state_ {std::move(state)},
|
||||
fallback_ {desktop, viewport} {}
|
||||
|
||||
OperationStatus submit(const MouseEvent &event) override {
|
||||
using enum ErrorCode;
|
||||
@@ -2439,7 +2503,10 @@ namespace lvh::detail {
|
||||
if (!status.ok()) {
|
||||
return {status, nullptr};
|
||||
}
|
||||
return {std::move(status), std::make_unique<WindowsHidMouse>(shared_from_this(), std::move(state))};
|
||||
return {
|
||||
std::move(status),
|
||||
std::make_unique<WindowsHidMouse>(shared_from_this(), std::move(state), options.desktop, options.viewport),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3079,7 +3146,7 @@ namespace lvh::detail {
|
||||
}
|
||||
}
|
||||
|
||||
return {OperationStatus::success(), std::make_unique<WindowsMouse>()};
|
||||
return {OperationStatus::success(), std::make_unique<WindowsMouse>(options.desktop, options.viewport)};
|
||||
}
|
||||
|
||||
BackendTouchscreenCreationResult create_touchscreen(
|
||||
|
||||
@@ -644,6 +644,26 @@ namespace lvh::detail::test {
|
||||
*/
|
||||
int linux_absolute_axis(std::int32_t value, std::int32_t limit);
|
||||
|
||||
/**
|
||||
* @brief Scale an absolute pointer coordinate through a target viewport.
|
||||
*
|
||||
* @param value Source coordinate.
|
||||
* @param source_dimension Source coordinate-space dimension.
|
||||
* @param viewport_offset Target viewport offset.
|
||||
* @param viewport_dimension Target viewport dimension.
|
||||
* @param desktop_offset Virtual desktop origin.
|
||||
* @param desktop_dimension Virtual desktop dimension.
|
||||
* @return Linux absolute axis value.
|
||||
*/
|
||||
int linux_absolute_axis_to_viewport(
|
||||
float value,
|
||||
std::int32_t source_dimension,
|
||||
std::int32_t viewport_offset,
|
||||
std::int32_t viewport_dimension,
|
||||
std::int32_t desktop_offset,
|
||||
std::int32_t desktop_dimension
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Decode UTF-8 into Unicode code points using the Linux backend decoder.
|
||||
*
|
||||
@@ -927,6 +947,18 @@ namespace lvh::detail::test {
|
||||
*/
|
||||
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe(const MouseEvent &event);
|
||||
|
||||
/**
|
||||
* @brief Submit a mouse event to a pipe-backed uinput mouse with explicit desktop mapping.
|
||||
*
|
||||
* @param event Mouse event.
|
||||
* @param options Mouse creation options containing desktop and target viewports.
|
||||
* @return Submission status and captured input events.
|
||||
*/
|
||||
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe(
|
||||
const MouseEvent &event,
|
||||
const CreateMouseOptions &options
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Submit mouse events to one pipe-backed uinput mouse.
|
||||
*
|
||||
|
||||
@@ -23,6 +23,34 @@ namespace lvh::detail::test {
|
||||
double y {}; ///< Vertical coordinate.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Portable representation of CoreGraphics viewport bounds for tests.
|
||||
*/
|
||||
struct MacosViewportBounds {
|
||||
double origin_x {}; ///< Horizontal viewport origin.
|
||||
double origin_y {}; ///< Vertical viewport origin.
|
||||
double width {}; ///< Viewport width.
|
||||
double height {}; ///< Viewport height.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Display and cursor state supplied before one mouse submission.
|
||||
*/
|
||||
struct MacosMouseSubmission {
|
||||
MacosViewportBounds display_bounds; ///< Current main-display bounds in screen points.
|
||||
MacosPoint cursor_location; ///< Cursor location before submission, in screen points.
|
||||
MouseEvent event; ///< Mouse event submitted to the backend.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Mouse submission status and captured CoreGraphics destinations.
|
||||
*/
|
||||
struct MacosMouseSubmissionResult {
|
||||
OperationStatus status; ///< Backend submission status.
|
||||
std::optional<MacosPoint> posted_location; ///< Location on the posted event, if any.
|
||||
std::optional<MacosPoint> warped_location; ///< Cursor warp destination, if any.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Portable representation of CoreGraphics mouse motion metadata for tests.
|
||||
*/
|
||||
@@ -132,6 +160,28 @@ namespace lvh::detail::test {
|
||||
double height
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Resolve an explicit portable mouse viewport to CoreGraphics bounds.
|
||||
*
|
||||
* @param viewport Portable pointer viewport.
|
||||
* @return CoreGraphics bounds represented with portable scalar fields.
|
||||
*/
|
||||
MacosViewportBounds macos_backend_mouse_viewport_bounds(const PointerViewport &viewport);
|
||||
|
||||
/**
|
||||
* @brief Submit to one backend mouse while simulating changing display geometry without delivering input.
|
||||
*
|
||||
* @param viewport Requested mouse viewport in native screen points, or zero dimensions for the main display.
|
||||
* @param initial_display_bounds Main-display bounds when the mouse is created.
|
||||
* @param submissions Display geometry, cursor location, and event for each submission.
|
||||
* @return Submission statuses and captured event and warp locations.
|
||||
*/
|
||||
std::vector<MacosMouseSubmissionResult> macos_backend_mouse_submissions(
|
||||
const PointerViewport &viewport,
|
||||
const MacosViewportBounds &initial_display_bounds,
|
||||
const std::vector<MacosMouseSubmission> &submissions
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Select CoreGraphics motion metadata for a mouse button state.
|
||||
*
|
||||
|
||||
@@ -278,4 +278,24 @@ namespace lvh::detail::test {
|
||||
WindowsOverlappedIoResult windows_backend_overlapped_device_io();
|
||||
WindowsBackendSendInputResult windows_backend_send_input_devices();
|
||||
|
||||
/**
|
||||
* @brief Scale a source coordinate through a target viewport into a virtual desktop axis.
|
||||
*
|
||||
* @param value Source coordinate.
|
||||
* @param source_dimension Source coordinate-space dimension.
|
||||
* @param viewport_offset Target viewport offset.
|
||||
* @param viewport_dimension Target viewport dimension.
|
||||
* @param desktop_offset Virtual desktop origin.
|
||||
* @param desktop_dimension Virtual desktop dimension.
|
||||
* @return Win32 normalized absolute coordinate.
|
||||
*/
|
||||
std::int32_t windows_backend_scale_absolute_axis_to_viewport(
|
||||
float value,
|
||||
std::int32_t source_dimension,
|
||||
std::int32_t viewport_offset,
|
||||
std::int32_t viewport_dimension,
|
||||
std::int32_t desktop_offset,
|
||||
std::int32_t desktop_dimension
|
||||
);
|
||||
|
||||
} // namespace lvh::detail::test
|
||||
+66
-25
@@ -996,6 +996,24 @@ namespace lvh::detail::test {
|
||||
return scale_absolute_axis(value, limit);
|
||||
}
|
||||
|
||||
int linux_absolute_axis_to_viewport(
|
||||
float value,
|
||||
std::int32_t source_dimension,
|
||||
std::int32_t viewport_offset,
|
||||
std::int32_t viewport_dimension,
|
||||
std::int32_t desktop_offset,
|
||||
std::int32_t desktop_dimension
|
||||
) {
|
||||
return scale_absolute_axis_to_viewport(
|
||||
value,
|
||||
source_dimension,
|
||||
viewport_offset,
|
||||
viewport_dimension,
|
||||
desktop_offset,
|
||||
desktop_dimension
|
||||
);
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> linux_decode_utf8(const std::string &text) {
|
||||
return decode_utf8(text);
|
||||
}
|
||||
@@ -1321,35 +1339,58 @@ namespace lvh::detail::test {
|
||||
return mouse.submit({.kind = MouseEventKind::relative_motion, .x = 1, .y = 1});
|
||||
}
|
||||
|
||||
namespace {
|
||||
/**
|
||||
* @brief Submit mouse events through pipe-backed uinput devices.
|
||||
*
|
||||
* @param events Mouse events to submit.
|
||||
* @param options Mouse creation options containing optional viewport bounds.
|
||||
* @return Submission status and captured input events.
|
||||
*/
|
||||
LinuxInputSubmissionResult submit_uinput_mouse_pipe_sequence(
|
||||
const std::vector<MouseEvent> &events,
|
||||
const CreateMouseOptions &options
|
||||
) {
|
||||
std::array<int, 2> descriptors {-1, -1};
|
||||
if (::pipe(descriptors.data()) != 0) {
|
||||
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
||||
}
|
||||
|
||||
const auto absolute_descriptor = ::dup(descriptors[1]);
|
||||
if (absolute_descriptor < 0) {
|
||||
static_cast<void>(::close(descriptors[0]));
|
||||
static_cast<void>(::close(descriptors[1]));
|
||||
return {system_error_status(ErrorCode::backend_failure, "failed to duplicate pipe", errno), {}};
|
||||
}
|
||||
|
||||
UinputMouse mouse {descriptors[1], absolute_descriptor, options.desktop, options.viewport};
|
||||
auto status = OperationStatus::success();
|
||||
for (const auto &event : events) {
|
||||
status = mouse.submit(event);
|
||||
if (!status.ok()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
static_cast<void>(mouse.close());
|
||||
auto records = read_input_events_until_eof(descriptors[0]);
|
||||
static_cast<void>(::close(descriptors[0]));
|
||||
return {std::move(status), std::move(records)};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe(const MouseEvent &event) {
|
||||
return linux_uinput_mouse_submit_pipe_sequence(std::vector<MouseEvent> {event});
|
||||
return submit_uinput_mouse_pipe_sequence(std::vector<MouseEvent> {event}, {});
|
||||
}
|
||||
|
||||
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe(
|
||||
const MouseEvent &event,
|
||||
const CreateMouseOptions &options
|
||||
) {
|
||||
return submit_uinput_mouse_pipe_sequence(std::vector<MouseEvent> {event}, options);
|
||||
}
|
||||
|
||||
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe_sequence(const std::vector<MouseEvent> &events) {
|
||||
std::array<int, 2> descriptors {-1, -1};
|
||||
if (::pipe(descriptors.data()) != 0) {
|
||||
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
||||
}
|
||||
|
||||
const auto absolute_descriptor = ::dup(descriptors[1]);
|
||||
if (absolute_descriptor < 0) {
|
||||
static_cast<void>(::close(descriptors[0]));
|
||||
static_cast<void>(::close(descriptors[1]));
|
||||
return {system_error_status(ErrorCode::backend_failure, "failed to duplicate pipe", errno), {}};
|
||||
}
|
||||
|
||||
UinputMouse mouse {descriptors[1], absolute_descriptor};
|
||||
auto status = OperationStatus::success();
|
||||
for (const auto &event : events) {
|
||||
status = mouse.submit(event);
|
||||
if (!status.ok()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
static_cast<void>(mouse.close());
|
||||
auto records = read_input_events_until_eof(descriptors[0]);
|
||||
static_cast<void>(::close(descriptors[0]));
|
||||
return {std::move(status), std::move(records)};
|
||||
return submit_uinput_mouse_pipe_sequence(events, {});
|
||||
}
|
||||
|
||||
LinuxMouseInputSubmissionResult linux_uinput_mouse_submit_split_pipe_sequence(const std::vector<MouseEvent> &events) {
|
||||
|
||||
+150
-2
@@ -3,15 +3,126 @@
|
||||
* @brief macOS backend test hook definitions.
|
||||
*/
|
||||
|
||||
// local includes
|
||||
#include "fixtures/macos_backend_test_hooks.hpp"
|
||||
// standard includes
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// local includes
|
||||
#include "core/backend.hpp"
|
||||
#include "fixtures/macos_backend_test_hooks.hpp"
|
||||
#include "platform/macos/macos_broker_client.hpp"
|
||||
|
||||
// platform includes
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#include <Carbon/Carbon.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <IOKit/hidsystem/IOLLEvent.h>
|
||||
|
||||
namespace {
|
||||
|
||||
struct MouseEnvironment;
|
||||
|
||||
/**
|
||||
* @brief Access the active mouse environment for the calling thread.
|
||||
*
|
||||
* @return Reference to the scoped environment pointer, or a null pointer when no environment is active.
|
||||
*/
|
||||
MouseEnvironment *&mouse_environment() {
|
||||
static thread_local MouseEnvironment *environment = nullptr;
|
||||
return environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Scoped display geometry and captured mouse delivery for backend submission tests.
|
||||
*/
|
||||
struct MouseEnvironment {
|
||||
explicit MouseEnvironment(const lvh::detail::test::MacosViewportBounds &bounds):
|
||||
display_bounds {bounds} {
|
||||
mouse_environment() = this;
|
||||
}
|
||||
|
||||
MouseEnvironment(const MouseEnvironment &) = delete;
|
||||
MouseEnvironment &operator=(const MouseEnvironment &) = delete;
|
||||
MouseEnvironment(MouseEnvironment &&) = delete;
|
||||
MouseEnvironment &operator=(MouseEnvironment &&) = delete;
|
||||
|
||||
~MouseEnvironment() {
|
||||
mouse_environment() = nullptr;
|
||||
}
|
||||
|
||||
lvh::detail::test::MacosViewportBounds display_bounds;
|
||||
lvh::detail::test::MacosPoint cursor_location;
|
||||
std::optional<lvh::detail::test::MacosPoint> posted_location;
|
||||
std::optional<lvh::detail::test::MacosPoint> warped_location;
|
||||
};
|
||||
|
||||
CGDirectDisplayID mouse_main_display_id() {
|
||||
return mouse_environment() ? 42 : CGMainDisplayID();
|
||||
}
|
||||
|
||||
CGRect mouse_display_bounds(CGDirectDisplayID display) {
|
||||
if (!mouse_environment()) {
|
||||
return CGDisplayBounds(display);
|
||||
}
|
||||
const auto &bounds = mouse_environment()->display_bounds;
|
||||
return CGRect {{bounds.origin_x, bounds.origin_y}, {bounds.width, bounds.height}};
|
||||
}
|
||||
|
||||
CGPoint mouse_event_location(CGEventRef event) {
|
||||
if (!mouse_environment()) {
|
||||
return CGEventGetLocation(event);
|
||||
}
|
||||
return CGPoint {mouse_environment()->cursor_location.x, mouse_environment()->cursor_location.y};
|
||||
}
|
||||
|
||||
void mouse_event_post(CGEventTapLocation tap, CGEventRef event) {
|
||||
if (!mouse_environment()) {
|
||||
CGEventPost(tap, event);
|
||||
return;
|
||||
}
|
||||
const auto point = CGEventGetLocation(event);
|
||||
mouse_environment()->posted_location = {.x = point.x, .y = point.y};
|
||||
}
|
||||
|
||||
CGError mouse_warp_cursor_position(CGPoint point) {
|
||||
if (!mouse_environment()) {
|
||||
return CGWarpMouseCursorPosition(point);
|
||||
}
|
||||
mouse_environment()->warped_location = {.x = point.x, .y = point.y};
|
||||
return kCGErrorSuccess;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Load the backend's dependencies before these macros, then compile it in a
|
||||
// separate namespace so instrumented inline symbols remain isolated.
|
||||
#define macos macos_backend_test
|
||||
#define CGMainDisplayID mouse_main_display_id
|
||||
#define CGDisplayBounds mouse_display_bounds
|
||||
#define CGEventGetLocation mouse_event_location
|
||||
#define CGEventPost mouse_event_post
|
||||
#define CGWarpMouseCursorPosition mouse_warp_cursor_position
|
||||
#define create_platform_backend create_platform_backend_for_macos_backend_test_hooks
|
||||
#include "../../src/platform/macos/macos_backend.cpp"
|
||||
#undef create_platform_backend
|
||||
#undef CGWarpMouseCursorPosition
|
||||
#undef CGEventPost
|
||||
#undef CGEventGetLocation
|
||||
#undef CGDisplayBounds
|
||||
#undef CGMainDisplayID
|
||||
#undef macos
|
||||
|
||||
namespace lvh::detail::test {
|
||||
|
||||
namespace macos = lvh::detail::macos_backend_test;
|
||||
|
||||
std::optional<std::uint16_t> macos_backend_key_code(KeyboardKeyCode key_code) {
|
||||
const auto mapped = macos::macos_key_code(key_code);
|
||||
if (!mapped) {
|
||||
@@ -93,6 +204,43 @@ namespace lvh::detail::test {
|
||||
return {.x = location.x, .y = location.y};
|
||||
}
|
||||
|
||||
MacosViewportBounds macos_backend_mouse_viewport_bounds(const PointerViewport &viewport) {
|
||||
const auto bounds = macos::mouse_viewport_bounds(viewport);
|
||||
return {
|
||||
.origin_x = bounds.origin.x,
|
||||
.origin_y = bounds.origin.y,
|
||||
.width = bounds.size.width,
|
||||
.height = bounds.size.height,
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<MacosMouseSubmissionResult> macos_backend_mouse_submissions(
|
||||
const PointerViewport &viewport,
|
||||
const MacosViewportBounds &initial_display_bounds,
|
||||
const std::vector<MacosMouseSubmission> &submissions
|
||||
) {
|
||||
MouseEnvironment environment {initial_display_bounds};
|
||||
auto backend = create_platform_backend_for_macos_backend_test_hooks();
|
||||
CreateMouseOptions options;
|
||||
options.profile.device_type = DeviceType::mouse;
|
||||
options.viewport = viewport;
|
||||
auto created = backend->create_mouse(1, options);
|
||||
if (!created) {
|
||||
return {{.status = created.status, .posted_location = std::nullopt, .warped_location = std::nullopt}};
|
||||
}
|
||||
|
||||
std::vector<MacosMouseSubmissionResult> results;
|
||||
for (const auto &submission : submissions) {
|
||||
environment.display_bounds = submission.display_bounds;
|
||||
environment.cursor_location = submission.cursor_location;
|
||||
environment.posted_location.reset();
|
||||
environment.warped_location.reset();
|
||||
const auto status = created.mouse->submit(submission.event);
|
||||
results.emplace_back(status, environment.posted_location, environment.warped_location);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
MacosMouseMotionResult macos_backend_mouse_motion(bool left_down, bool right_down, bool middle_down) {
|
||||
const auto motion = macos::macos_mouse_motion({left_down, right_down, middle_down});
|
||||
return {
|
||||
|
||||
+18
@@ -486,6 +486,24 @@ namespace lvh::detail {
|
||||
|
||||
namespace test {
|
||||
|
||||
std::int32_t windows_backend_scale_absolute_axis_to_viewport(
|
||||
float value,
|
||||
std::int32_t source_dimension,
|
||||
std::int32_t viewport_offset,
|
||||
std::int32_t viewport_dimension,
|
||||
std::int32_t desktop_offset,
|
||||
std::int32_t desktop_dimension
|
||||
) {
|
||||
return scale_absolute_axis_to_viewport(
|
||||
value,
|
||||
source_dimension,
|
||||
viewport_offset,
|
||||
viewport_dimension,
|
||||
desktop_offset,
|
||||
desktop_dimension
|
||||
);
|
||||
}
|
||||
|
||||
WindowsBackendLifecycleResult windows_backend_fake_channel_lifecycle() {
|
||||
WindowsBackendLifecycleResult result;
|
||||
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
|
||||
|
||||
@@ -95,7 +95,7 @@ TEST(GitHubActionsEvaluationTest, RemainingTimeClampsAtWindowBoundaries) {
|
||||
EXPECT_EQ(remaining(started_at, started_at - 1s), 0s);
|
||||
}
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#if !defined(_WIN32) && !defined(__APPLE__)
|
||||
TEST(LicenseApiTest, UnsupportedPlatformReturnsExplicitFailure) {
|
||||
const auto queried = lvh::get_license_status();
|
||||
EXPECT_FALSE(queried);
|
||||
|
||||
@@ -150,6 +150,13 @@ TEST_F(LinuxBackendTest, ScalesAbsoluteAxesAndScrollSteps) {
|
||||
EXPECT_EQ(lvh::detail::test::linux_absolute_axis(101, 100), 65535);
|
||||
EXPECT_EQ(lvh::detail::test::linux_absolute_axis(1, 0), 0);
|
||||
|
||||
EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(0.0F, 1920, 0, 1920, -1920, 3840), 32768);
|
||||
EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(960.0F, 1920, 0, 1920, -1920, 3840), 49143);
|
||||
EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(1920.0F, 1920, 0, 1920, -1920, 3840), 65518);
|
||||
EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(1.0F, 0, 0, 1920, -1920, 3840), 0);
|
||||
EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(1.0F, 1, 0, 0, 0, 1), 0);
|
||||
EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(1.0F, 1, 0, 1, 0, 0), 0);
|
||||
|
||||
EXPECT_EQ(lvh::detail::test::linux_legacy_scroll_steps(0), 0);
|
||||
EXPECT_EQ(lvh::detail::test::linux_legacy_scroll_steps(1), 1);
|
||||
EXPECT_EQ(lvh::detail::test::linux_legacy_scroll_steps(-1), -1);
|
||||
@@ -677,6 +684,27 @@ TEST_F(LinuxBackendTest, PipeBackedUinputMouseEmitsEvents) {
|
||||
EXPECT_EQ(result.events.back().type, EV_SYN);
|
||||
}
|
||||
|
||||
TEST_F(LinuxBackendTest, PipeBackedUinputMouseMapsConfiguredViewport) {
|
||||
lvh::MouseEvent event {
|
||||
.kind = lvh::MouseEventKind::absolute_motion,
|
||||
.x = 960,
|
||||
.y = 540,
|
||||
.width = 1920,
|
||||
.height = 1080,
|
||||
};
|
||||
lvh::CreateMouseOptions options;
|
||||
options.desktop = {.offset_x = -1920, .offset_y = 0, .width = 3840, .height = 1080};
|
||||
options.viewport = {.offset_x = 0, .offset_y = 0, .width = 1920, .height = 1080};
|
||||
|
||||
const auto result = lvh::detail::test::linux_uinput_mouse_submit_pipe(event, options);
|
||||
ASSERT_TRUE(result.status.ok()) << result.status.message();
|
||||
ASSERT_EQ(result.events.size(), 3U);
|
||||
EXPECT_EQ(result.events[0].code, ABS_X);
|
||||
EXPECT_EQ(result.events[0].value, 49143);
|
||||
EXPECT_EQ(result.events[1].code, ABS_Y);
|
||||
EXPECT_EQ(result.events[1].value, 32737);
|
||||
}
|
||||
|
||||
TEST_F(LinuxBackendTest, PipeBackedUinputMouseRoutesMotionAndButtonsAcrossSplitDevices) {
|
||||
const std::vector<lvh::MouseEvent> events {
|
||||
{.kind = lvh::MouseEventKind::absolute_motion, .x = 50, .y = 25, .width = 100, .height = 100},
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
* @brief Unit tests for macOS backend internals.
|
||||
*/
|
||||
|
||||
// standard includes
|
||||
#include <array>
|
||||
|
||||
// lib includes
|
||||
#include <libvirtualhid/libvirtualhid.hpp>
|
||||
|
||||
@@ -189,6 +192,143 @@ TEST_F(MacosBackendTest, ConvertsAbsoluteMouseCoordinates) {
|
||||
EXPECT_DOUBLE_EQ(location.y, 170.0);
|
||||
}
|
||||
|
||||
TEST_F(MacosBackendTest, UsesConfiguredMouseViewport) {
|
||||
const auto bounds = lvh::detail::test::macos_backend_mouse_viewport_bounds({
|
||||
.offset_x = -1920,
|
||||
.offset_y = 120,
|
||||
.width = 1920,
|
||||
.height = 1080,
|
||||
});
|
||||
|
||||
EXPECT_DOUBLE_EQ(bounds.origin_x, -1920.0);
|
||||
EXPECT_DOUBLE_EQ(bounds.origin_y, 120.0);
|
||||
EXPECT_DOUBLE_EQ(bounds.width, 1920.0);
|
||||
EXPECT_DOUBLE_EQ(bounds.height, 1080.0);
|
||||
}
|
||||
|
||||
TEST_F(MacosBackendTest, RefreshesDefaultMouseBoundsBetweenSubmissions) {
|
||||
const lvh::MouseEvent center {
|
||||
.kind = lvh::MouseEventKind::absolute_motion,
|
||||
.x = 960,
|
||||
.y = 540,
|
||||
.width = 1920,
|
||||
.height = 1080,
|
||||
};
|
||||
const std::vector<lvh::detail::test::MacosMouseSubmission> submissions {
|
||||
{.display_bounds = {0, 0, 1920, 1080}, .cursor_location = {}, .event = center},
|
||||
{.display_bounds = {0, 0, 2560, 1440}, .cursor_location = {}, .event = center},
|
||||
{.display_bounds = {0, 0, 2560, 1440}, .cursor_location = {2400, 1300}, .event = {.kind = lvh::MouseEventKind::relative_motion, .x = 400, .y = 300}},
|
||||
{.display_bounds = {-1280, -720, 1280, 720}, .cursor_location = {100, 100}, .event = {.kind = lvh::MouseEventKind::button, .button = lvh::MouseButton::left, .pressed = true}},
|
||||
};
|
||||
const auto results = lvh::detail::test::macos_backend_mouse_submissions({}, {0, 0, 1920, 1080}, submissions);
|
||||
|
||||
ASSERT_EQ(results.size(), 4U);
|
||||
const std::array<lvh::detail::test::MacosPoint, 4> expected {{{960, 540}, {1280, 720}, {2559, 1439}, {-1, -1}}};
|
||||
for (std::size_t index = 0; index < results.size(); ++index) {
|
||||
SCOPED_TRACE(index);
|
||||
ASSERT_TRUE(results[index].status.ok()) << results[index].status.message();
|
||||
ASSERT_TRUE(results[index].posted_location);
|
||||
ASSERT_TRUE(results[index].warped_location);
|
||||
EXPECT_DOUBLE_EQ(results[index].posted_location->x, expected[index].x);
|
||||
EXPECT_DOUBLE_EQ(results[index].posted_location->y, expected[index].y);
|
||||
EXPECT_DOUBLE_EQ(results[index].warped_location->x, expected[index].x);
|
||||
EXPECT_DOUBLE_EQ(results[index].warped_location->y, expected[index].y);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MacosBackendTest, RecoversFromTemporarilyUnavailableDefaultMouseBounds) {
|
||||
const lvh::MouseEvent center {
|
||||
.kind = lvh::MouseEventKind::absolute_motion,
|
||||
.x = 50,
|
||||
.y = 50,
|
||||
.width = 100,
|
||||
.height = 100,
|
||||
};
|
||||
const std::vector<lvh::detail::test::MacosMouseSubmission> submissions {
|
||||
{.display_bounds = {0, 0, 1920, 1080}, .cursor_location = {}, .event = center},
|
||||
{.display_bounds = {0, 0, 0, 1080}, .cursor_location = {}, .event = center},
|
||||
{.display_bounds = {0, 0, 1920, 0}, .cursor_location = {}, .event = center},
|
||||
{.display_bounds = {0, 0, 2560, 1440}, .cursor_location = {}, .event = center},
|
||||
};
|
||||
const auto results = lvh::detail::test::macos_backend_mouse_submissions({}, {0, 0, 0, 0}, submissions);
|
||||
|
||||
ASSERT_EQ(results.size(), 4U);
|
||||
ASSERT_TRUE(results[0].status.ok()) << results[0].status.message();
|
||||
ASSERT_TRUE(results[0].posted_location);
|
||||
EXPECT_DOUBLE_EQ(results[0].posted_location->x, 960);
|
||||
EXPECT_DOUBLE_EQ(results[0].posted_location->y, 540);
|
||||
for (const auto index : {1U, 2U}) {
|
||||
EXPECT_EQ(results[index].status.code(), lvh::ErrorCode::backend_failure);
|
||||
EXPECT_FALSE(results[index].posted_location);
|
||||
EXPECT_FALSE(results[index].warped_location);
|
||||
}
|
||||
ASSERT_TRUE(results[3].status.ok()) << results[3].status.message();
|
||||
ASSERT_TRUE(results[3].posted_location);
|
||||
ASSERT_TRUE(results[3].warped_location);
|
||||
EXPECT_DOUBLE_EQ(results[3].posted_location->x, 1280);
|
||||
EXPECT_DOUBLE_EQ(results[3].posted_location->y, 720);
|
||||
EXPECT_DOUBLE_EQ(results[3].warped_location->x, 1280);
|
||||
EXPECT_DOUBLE_EQ(results[3].warped_location->y, 720);
|
||||
}
|
||||
|
||||
TEST_F(MacosBackendTest, KeepsConfiguredMouseViewportWhenDefaultDisplayChanges) {
|
||||
const lvh::PointerViewport viewport {.offset_x = -1920, .offset_y = 120, .width = 1920, .height = 1080};
|
||||
const lvh::MouseEvent center {
|
||||
.kind = lvh::MouseEventKind::absolute_motion,
|
||||
.x = 50,
|
||||
.y = 50,
|
||||
.width = 100,
|
||||
.height = 100,
|
||||
};
|
||||
const std::vector<lvh::detail::test::MacosMouseSubmission> submissions {
|
||||
{.display_bounds = {0, 0, 2560, 1440}, .cursor_location = {}, .event = center},
|
||||
{.display_bounds = {0, 0, 0, 0}, .cursor_location = {-100, 200}, .event = {.kind = lvh::MouseEventKind::relative_motion, .x = 400, .y = -300}},
|
||||
};
|
||||
const auto results = lvh::detail::test::macos_backend_mouse_submissions(viewport, {0, 0, 1920, 1080}, submissions);
|
||||
|
||||
ASSERT_EQ(results.size(), 2U);
|
||||
const std::array<lvh::detail::test::MacosPoint, 2> expected {{{-960, 660}, {-1, 120}}};
|
||||
for (std::size_t index = 0; index < results.size(); ++index) {
|
||||
ASSERT_TRUE(results[index].status.ok()) << results[index].status.message();
|
||||
ASSERT_TRUE(results[index].posted_location);
|
||||
ASSERT_TRUE(results[index].warped_location);
|
||||
EXPECT_DOUBLE_EQ(results[index].posted_location->x, expected[index].x);
|
||||
EXPECT_DOUBLE_EQ(results[index].posted_location->y, expected[index].y);
|
||||
EXPECT_DOUBLE_EQ(results[index].warped_location->x, expected[index].x);
|
||||
EXPECT_DOUBLE_EQ(results[index].warped_location->y, expected[index].y);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MacosBackendTest, ConfiguredAndDefaultRetinaMouseMappingsAgreeInScreenPoints) {
|
||||
const std::vector<lvh::detail::test::MacosMouseSubmission> submissions {
|
||||
{.display_bounds = {0, 0, 1920, 1080}, .cursor_location = {}, .event = {.kind = lvh::MouseEventKind::absolute_motion, .x = 1920, .y = 1080, .width = 3840, .height = 2160}},
|
||||
{.display_bounds = {0, 0, 1920, 1080}, .cursor_location = {}, .event = {.kind = lvh::MouseEventKind::absolute_motion, .x = 3840, .y = 2160, .width = 3840, .height = 2160}},
|
||||
};
|
||||
const auto defaults = lvh::detail::test::macos_backend_mouse_submissions({}, {0, 0, 1920, 1080}, submissions);
|
||||
const auto configured = lvh::detail::test::macos_backend_mouse_submissions({.width = 1920, .height = 1080}, {0, 0, 1920, 1080}, submissions);
|
||||
|
||||
ASSERT_EQ(defaults.size(), 2U);
|
||||
ASSERT_EQ(configured.size(), defaults.size());
|
||||
const std::array<lvh::detail::test::MacosPoint, 2> expected {{{960, 540}, {1919, 1079}}};
|
||||
for (std::size_t index = 0; index < defaults.size(); ++index) {
|
||||
SCOPED_TRACE(index);
|
||||
ASSERT_TRUE(defaults[index].status.ok()) << defaults[index].status.message();
|
||||
ASSERT_TRUE(configured[index].status.ok()) << configured[index].status.message();
|
||||
ASSERT_TRUE(defaults[index].posted_location);
|
||||
ASSERT_TRUE(configured[index].posted_location);
|
||||
ASSERT_TRUE(defaults[index].warped_location);
|
||||
ASSERT_TRUE(configured[index].warped_location);
|
||||
EXPECT_DOUBLE_EQ(defaults[index].posted_location->x, expected[index].x);
|
||||
EXPECT_DOUBLE_EQ(defaults[index].posted_location->y, expected[index].y);
|
||||
EXPECT_DOUBLE_EQ(configured[index].posted_location->x, expected[index].x);
|
||||
EXPECT_DOUBLE_EQ(configured[index].posted_location->y, expected[index].y);
|
||||
EXPECT_DOUBLE_EQ(defaults[index].warped_location->x, expected[index].x);
|
||||
EXPECT_DOUBLE_EQ(defaults[index].warped_location->y, expected[index].y);
|
||||
EXPECT_DOUBLE_EQ(configured[index].warped_location->x, expected[index].x);
|
||||
EXPECT_DOUBLE_EQ(configured[index].warped_location->y, expected[index].y);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MacosBackendTest, SelectsMouseMotionMetadataForHeldButtons) {
|
||||
using lvh::detail::test::macos_backend_mouse_motion;
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
// lib includes
|
||||
#include <libvirtualhid/libvirtualhid.hpp>
|
||||
|
||||
// standard includes
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// local includes
|
||||
#include "fixtures/fixtures.hpp"
|
||||
#if defined(_WIN32)
|
||||
@@ -32,6 +38,65 @@ TEST(RuntimeTest, FakeBackendReportsCapabilities) {
|
||||
EXPECT_FALSE(runtime->capabilities().requires_installed_driver);
|
||||
}
|
||||
|
||||
TEST(RuntimeTest, RoutesDiagnosticsToConsumerCallback) {
|
||||
struct Diagnostic {
|
||||
lvh::LogLevel level;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
|
||||
lvh::RuntimeOptions runtime_options;
|
||||
runtime_options.log_callback = [&diagnostics](lvh::LogLevel level, const std::string &message) {
|
||||
diagnostics.emplace_back(level, message);
|
||||
};
|
||||
auto runtime = lvh::Runtime::create(runtime_options);
|
||||
|
||||
lvh::CreateMouseOptions mouse_options;
|
||||
mouse_options.profile = lvh::profiles::mouse();
|
||||
mouse_options.desktop = {.offset_x = -1920, .offset_y = 0, .width = 3840, .height = 1080};
|
||||
mouse_options.viewport = {.offset_x = -1920, .offset_y = 0, .width = 1920, .height = 1080};
|
||||
auto created = runtime->create_mouse(mouse_options);
|
||||
ASSERT_TRUE(created) << created.status.message();
|
||||
EXPECT_TRUE(created.mouse->move_relative(1, -1).ok());
|
||||
EXPECT_TRUE(created.mouse->move_absolute(960, 540, 1920, 1080).ok());
|
||||
EXPECT_TRUE(created.mouse->move_absolute(0.5F, 0.25F, 1, 1).ok());
|
||||
EXPECT_TRUE(created.mouse->button(lvh::MouseButton::left, true).ok());
|
||||
EXPECT_TRUE(created.mouse->vertical_scroll(120).ok());
|
||||
EXPECT_TRUE(created.mouse->horizontal_scroll(-120).ok());
|
||||
EXPECT_TRUE(created.mouse->close().ok());
|
||||
EXPECT_EQ(created.mouse->move_relative(1, 1).code(), lvh::ErrorCode::device_closed);
|
||||
|
||||
const auto has_message = [&diagnostics](lvh::LogLevel level, std::string_view text) {
|
||||
return std::ranges::any_of(diagnostics, [level, text](const auto &diagnostic) {
|
||||
return diagnostic.level == level && diagnostic.message.contains(text);
|
||||
});
|
||||
};
|
||||
EXPECT_TRUE(has_message(lvh::LogLevel::info, "initialized fake backend"));
|
||||
EXPECT_TRUE(has_message(lvh::LogLevel::info, "created mouse 1"));
|
||||
EXPECT_TRUE(has_message(lvh::LogLevel::debug, "viewport=-1920,0 1920x1080"));
|
||||
EXPECT_TRUE(has_message(lvh::LogLevel::error, "mouse input failed: mouse is closed"));
|
||||
}
|
||||
|
||||
TEST(RuntimeTest, DisablesConsumerDiagnosticsAfterException) {
|
||||
struct ConsumerLoggerFailure {};
|
||||
|
||||
std::size_t callback_count = 0;
|
||||
lvh::RuntimeOptions options;
|
||||
options.log_callback = [&callback_count](lvh::LogLevel, const std::string &) {
|
||||
++callback_count;
|
||||
throw ConsumerLoggerFailure {};
|
||||
};
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
auto runtime = lvh::Runtime::create(options);
|
||||
auto created = runtime->create_mouse();
|
||||
ASSERT_TRUE(created);
|
||||
EXPECT_TRUE(created.mouse->move_relative(1, 1).ok());
|
||||
});
|
||||
EXPECT_EQ(callback_count, 1U);
|
||||
}
|
||||
|
||||
TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) {
|
||||
lvh::RuntimeOptions options;
|
||||
options.backend = lvh::BackendKind::platform_default;
|
||||
@@ -273,6 +338,29 @@ TEST(RuntimeTest, CreatesSubmitsAndClosesMouse) {
|
||||
EXPECT_EQ(created.mouse->move_relative(1, 1).code(), lvh::ErrorCode::device_closed);
|
||||
}
|
||||
|
||||
TEST(RuntimeTest, RejectsIncompleteMouseViewport) {
|
||||
auto runtime = lvh::Runtime::create();
|
||||
const auto expect_invalid = [&runtime](lvh::PointerViewport desktop, lvh::PointerViewport viewport) {
|
||||
lvh::CreateMouseOptions options;
|
||||
options.profile = lvh::profiles::mouse();
|
||||
options.desktop = desktop;
|
||||
options.viewport = viewport;
|
||||
|
||||
const auto created = runtime->create_mouse(options);
|
||||
EXPECT_FALSE(created);
|
||||
EXPECT_EQ(created.status.code(), lvh::ErrorCode::invalid_argument);
|
||||
};
|
||||
|
||||
expect_invalid({}, {.width = 1920});
|
||||
expect_invalid({}, {.width = 1920, .height = 1080});
|
||||
expect_invalid({.width = 3840, .height = 1080}, {});
|
||||
expect_invalid({.width = -1, .height = 1080}, {});
|
||||
expect_invalid(
|
||||
{.offset_x = -1920, .width = 3840, .height = 1080},
|
||||
{.offset_x = 0, .offset_y = 1080, .width = 1920, .height = 1080}
|
||||
);
|
||||
}
|
||||
|
||||
TEST(RuntimeTest, CreatesSubmitsAndClosesTouchDevices) {
|
||||
auto runtime = lvh::Runtime::create();
|
||||
|
||||
|
||||
@@ -449,3 +449,14 @@ TEST_F(WindowsBackendTest, SendInputDevicesTranslateKeyboardMouseFailuresAndUnsu
|
||||
EXPECT_EQ(result.sent_inputs[17].mouse_x, 1);
|
||||
EXPECT_EQ(result.sent_inputs[17].mouse_y, 1);
|
||||
}
|
||||
|
||||
TEST_F(WindowsBackendTest, MapsAbsoluteMouseInputIntoConfiguredViewport) {
|
||||
using lvh::detail::test::windows_backend_scale_absolute_axis_to_viewport;
|
||||
|
||||
EXPECT_EQ(windows_backend_scale_absolute_axis_to_viewport(0.0F, 1920, 0, 1920, -1920, 3840), 32768);
|
||||
EXPECT_EQ(windows_backend_scale_absolute_axis_to_viewport(960.0F, 1920, 0, 1920, -1920, 3840), 49143);
|
||||
EXPECT_EQ(windows_backend_scale_absolute_axis_to_viewport(1920.0F, 1920, 0, 1920, -1920, 3840), 65518);
|
||||
EXPECT_EQ(windows_backend_scale_absolute_axis_to_viewport(1.0F, 0, 0, 1920, -1920, 3840), 0);
|
||||
EXPECT_EQ(windows_backend_scale_absolute_axis_to_viewport(1.0F, 1, 0, 0, 0, 1), 0);
|
||||
EXPECT_EQ(windows_backend_scale_absolute_axis_to_viewport(1.0F, 1, 0, 1, 0, 0), 0);
|
||||
}
|
||||
Reference in new issue
Block a user