fix(input): target configured mouse viewport (#145)

This commit is contained in:
Dave Lane authored and GitHub committed 2026-10-08 20:50:30 -04:00
1 parent 5ef6b9efa0
commit 24962b2092
18 files changed
+1116 -124

No files matched your search

+9
View File
@@ -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.
+55
View File
@@ -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
View File
@@ -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)};
}
+72 -25
View File
@@ -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.
*/
+95 -15
View File
@@ -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)};
+40 -37
View File
@@ -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(
+79 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>();
+1 -1
View File
@@ -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);
+28
View File
@@ -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},
+140
View File
@@ -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;
+88
View File
@@ -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();
+11
View File
@@ -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);
}