mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-18 01:46:47 +08:00
[fix] restore remote cursors on Windows without a physical mouse
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-16
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOWS_CURSOR_STATE_H_
|
||||
#define _WINDOWS_CURSOR_STATE_H_
|
||||
|
||||
#include <remote_action.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
#include "windows_cursor_shape.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Used by both ordinary and secure-desktop capture. Device absence is only a
|
||||
// compatibility hint: a hidden cursor with a null handle, or touch/pen
|
||||
// suppression, must retain the application's normal hiding behavior.
|
||||
class WindowsCursorState {
|
||||
public:
|
||||
void Sample(const CURSORINFO& info, bool embedded, CursorState* state) {
|
||||
const bool showing = (info.flags & CURSOR_SHOWING) != 0;
|
||||
state->render_mode = showing ? (embedded ? CursorRenderMode::embedded
|
||||
: CursorRenderMode::separate)
|
||||
: CursorRenderMode::hidden;
|
||||
state->visible = state->render_mode == CursorRenderMode::separate;
|
||||
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
|
||||
: RemoteCursorShape::none;
|
||||
state->hidden_reason = CursorHiddenReason::unspecified;
|
||||
if (!showing) {
|
||||
state->hidden_reason = (info.flags & CURSOR_SUPPRESSED)
|
||||
? CursorHiddenReason::system_suppressed
|
||||
: info.hCursor && NoPointingDevice()
|
||||
? CursorHiddenReason::no_pointing_device
|
||||
: CursorHiddenReason::system_hidden;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Query only for potentially recoverable hidden cursors, at most once per
|
||||
// second. Require two successful absent observations. Query failures, RDP
|
||||
// (whose devices may be absent from Raw Input), and present mice all disallow
|
||||
// fallback and break the sequence of absent observations.
|
||||
bool NoPointingDevice() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now < next_device_check_) return absence_confirmed_;
|
||||
next_device_check_ = now + std::chrono::seconds(1);
|
||||
const bool absent = DetectDeviceAbsence();
|
||||
absence_confirmed_ = absent && absent_before_;
|
||||
absent_before_ = absent;
|
||||
return absence_confirmed_;
|
||||
}
|
||||
|
||||
static bool DetectDeviceAbsence() {
|
||||
if (GetSystemMetrics(SM_MOUSEPRESENT) ||
|
||||
GetSystemMetrics(SM_REMOTESESSION)) {
|
||||
return false;
|
||||
}
|
||||
UINT count = 0;
|
||||
if (GetRawInputDeviceList(nullptr, &count, sizeof(RAWINPUTDEVICELIST)) ==
|
||||
static_cast<UINT>(-1) ||
|
||||
count > 4096)
|
||||
return false;
|
||||
if (count == 0) return true;
|
||||
std::vector<RAWINPUTDEVICELIST> devices(count);
|
||||
const UINT read = GetRawInputDeviceList(devices.data(), &count,
|
||||
sizeof(RAWINPUTDEVICELIST));
|
||||
if (read == static_cast<UINT>(-1) || read > devices.size()) return false;
|
||||
for (UINT i = 0; i < read; ++i) {
|
||||
if (devices[i].dwType == RIM_TYPEMOUSE) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool absent_before_ = false;
|
||||
bool absence_confirmed_ = false;
|
||||
std::chrono::steady_clock::time_point next_device_check_{};
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
|
||||
#define CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
@@ -14,8 +15,10 @@ struct SecureDesktopCursorSnapshot {
|
||||
uint32_t shape = 0;
|
||||
int32_t x = 0;
|
||||
int32_t y = 0;
|
||||
uint32_t render_mode = 0;
|
||||
uint32_t hidden_reason = 0;
|
||||
};
|
||||
static_assert(sizeof(SecureDesktopCursorSnapshot) == 20);
|
||||
static_assert(sizeof(SecureDesktopCursorSnapshot) == 28);
|
||||
|
||||
// The capture thread publishes helper samples for the GUI thread. While secure
|
||||
// capture is active, even an unavailable sample must not fall back to sampling
|
||||
@@ -30,13 +33,20 @@ class SecureDesktopCursorState {
|
||||
|
||||
void Update(const SecureDesktopCursorSnapshot& snapshot) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (active_ && snapshot.valid) snapshot_ = snapshot;
|
||||
if (active_) {
|
||||
snapshot_ = snapshot;
|
||||
updated_at_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
bool Get(SecureDesktopCursorSnapshot* snapshot) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!active_ || !snapshot) return false;
|
||||
*snapshot = snapshot_;
|
||||
if (std::chrono::steady_clock::now() - updated_at_ >=
|
||||
std::chrono::milliseconds(1500)) {
|
||||
snapshot->valid = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -44,6 +54,7 @@ class SecureDesktopCursorState {
|
||||
mutable std::mutex mutex_;
|
||||
bool active_ = false;
|
||||
SecureDesktopCursorSnapshot snapshot_{};
|
||||
std::chrono::steady_clock::time_point updated_at_{};
|
||||
};
|
||||
|
||||
inline SecureDesktopCursorState& SharedSecureDesktopCursorState() {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "windows_cursor_shape.h"
|
||||
#include "windows_cursor_state.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace {
|
||||
@@ -1191,10 +1191,14 @@ struct SecureDesktopGdiResources {
|
||||
CURSORINFO cursor{};
|
||||
cursor.cbSize = sizeof(cursor);
|
||||
if (GetCursorInfo(&cursor)) {
|
||||
static thread_local crossdesk::WindowsCursorState cursor_state;
|
||||
crossdesk::CursorState state{};
|
||||
cursor_state.Sample(cursor, false, &state);
|
||||
cursor_snapshot.valid = 1;
|
||||
cursor_snapshot.visible = (cursor.flags & CURSOR_SHOWING) != 0;
|
||||
cursor_snapshot.shape = static_cast<uint32_t>(
|
||||
crossdesk::ShapeFromWindowsCursor(cursor.hCursor));
|
||||
cursor_snapshot.visible = state.visible;
|
||||
cursor_snapshot.shape = static_cast<uint32_t>(state.shape);
|
||||
cursor_snapshot.render_mode = static_cast<uint32_t>(state.render_mode);
|
||||
cursor_snapshot.hidden_reason = static_cast<uint32_t>(state.hidden_reason);
|
||||
cursor_snapshot.x = cursor.ptScreenPos.x;
|
||||
cursor_snapshot.y = cursor.ptScreenPos.y;
|
||||
if (request.show_cursor && cursor_snapshot.visible &&
|
||||
@@ -1208,6 +1212,8 @@ struct SecureDesktopGdiResources {
|
||||
// A frame with an embedded cursor must not get a second cursor
|
||||
// overlay from a native controller sharing the web capture path.
|
||||
cursor_snapshot.visible = 0;
|
||||
cursor_snapshot.render_mode =
|
||||
static_cast<uint32_t>(crossdesk::CursorRenderMode::embedded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,9 @@ inline constexpr wchar_t kCrossDeskSecureDesktopFrameMappingPrefix[] =
|
||||
inline constexpr wchar_t kCrossDeskSecureDesktopFrameReadyEventPrefix[] =
|
||||
L"Global\\CrossDeskSecureDesktopFrameReady-";
|
||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
|
||||
// Version 2 adds cursor metadata to both headers. The GUI and local session
|
||||
// helper must be deployed together; the remote-control wire format is unchanged.
|
||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 2;
|
||||
// Version 3 distinguishes embedded, hidden and unavailable cursor states.
|
||||
// The GUI and local session helper must be deployed together.
|
||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 3;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct CrossDeskSecureDesktopFrameHeader {
|
||||
|
||||
Reference in New Issue
Block a user