mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-28 16:54:11 +08:00
[fix] restore remote cursors on Windows without a physical mouse
This commit is contained in:
@@ -1584,6 +1584,10 @@ void GuiApplication::BindStreamCallbacks() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
props->selected_display_ = index;
|
props->selected_display_ = index;
|
||||||
|
{
|
||||||
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||||
|
props->cursor_presentation_ = {};
|
||||||
|
}
|
||||||
RemoteAction action{};
|
RemoteAction action{};
|
||||||
action.type = ControlType::display_id;
|
action.type = ControlType::display_id;
|
||||||
action.d = index;
|
action.d = index;
|
||||||
@@ -1883,9 +1887,20 @@ void GuiApplication::ShareLocalCursorState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CursorState sampled{};
|
CursorState sampled{};
|
||||||
if (!cursor_state_provider_.Sample(devices_.display_info_list(),
|
const bool sample_ok = cursor_state_provider_.Sample(
|
||||||
selected_display_, &sampled)) {
|
devices_.display_info_list(), selected_display_, &sampled);
|
||||||
return;
|
if (!sample_ok) {
|
||||||
|
sampled.render_mode = CursorRenderMode::unknown;
|
||||||
|
if (sampled.hidden_reason == CursorHiddenReason::unspecified) {
|
||||||
|
sampled.hidden_reason = CursorHiddenReason::sampling_failed;
|
||||||
|
}
|
||||||
|
} else if (sampled.render_mode == CursorRenderMode::legacy) {
|
||||||
|
// Providers without platform-specific presentation metadata only need to
|
||||||
|
// sample visible/shape; normalize that common case once for every peer.
|
||||||
|
sampled.render_mode = sampled.visible ? CursorRenderMode::separate
|
||||||
|
: CursorRenderMode::hidden;
|
||||||
|
sampled.hidden_reason = sampled.visible ? CursorHiddenReason::unspecified
|
||||||
|
: CursorHiddenReason::system_hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
@@ -1924,21 +1939,23 @@ void GuiApplication::ShareLocalCursorState() {
|
|||||||
|
|
||||||
const CursorState& previous = delivery.last_sent;
|
const CursorState& previous = delivery.last_sent;
|
||||||
const bool shape_changed =
|
const bool shape_changed =
|
||||||
!delivery.has_sent || sampled.visible != previous.visible ||
|
!delivery.has_sent || sampled.render_mode != previous.render_mode ||
|
||||||
sampled.shape != previous.shape ||
|
sampled.hidden_reason != previous.hidden_reason ||
|
||||||
std::abs(sampled.visual_offset_x - previous.visual_offset_x) >
|
(sample_ok &&
|
||||||
kCursorPositionEpsilon ||
|
(sampled.visible != previous.visible || sampled.shape != previous.shape ||
|
||||||
std::abs(sampled.visual_offset_y - previous.visual_offset_y) >
|
std::abs(sampled.visual_offset_x - previous.visual_offset_x) >
|
||||||
kCursorPositionEpsilon;
|
kCursorPositionEpsilon ||
|
||||||
|
std::abs(sampled.visual_offset_y - previous.visual_offset_y) >
|
||||||
|
kCursorPositionEpsilon));
|
||||||
// Preserve sub-point cursor motion. At 10x client zoom, the old roughly
|
// Preserve sub-point cursor motion. At 10x client zoom, the old roughly
|
||||||
// one-logical-pixel threshold became several visible phone points.
|
// one-logical-pixel threshold became several visible phone points.
|
||||||
const bool position_changed =
|
const bool position_changed =
|
||||||
!delivery.has_sent ||
|
sample_ok &&
|
||||||
sampled.position_valid != previous.position_valid ||
|
(!delivery.has_sent || sampled.position_valid != previous.position_valid ||
|
||||||
sampled.display_id != previous.display_id ||
|
sampled.display_id != previous.display_id ||
|
||||||
(sampled.position_valid && previous.position_valid &&
|
(sampled.position_valid && previous.position_valid &&
|
||||||
(std::abs(sampled.x - previous.x) > kCursorPositionEpsilon ||
|
(std::abs(sampled.x - previous.x) > kCursorPositionEpsilon ||
|
||||||
std::abs(sampled.y - previous.y) > kCursorPositionEpsilon));
|
std::abs(sampled.y - previous.y) > kCursorPositionEpsilon)));
|
||||||
const bool position_update_due =
|
const bool position_update_due =
|
||||||
position_changed &&
|
position_changed &&
|
||||||
(delivery.last_sent_time.time_since_epoch().count() == 0 ||
|
(delivery.last_sent_time.time_since_epoch().count() == 0 ||
|
||||||
@@ -1946,16 +1963,12 @@ void GuiApplication::ShareLocalCursorState() {
|
|||||||
const bool heartbeat_due =
|
const bool heartbeat_due =
|
||||||
delivery.last_sent_time.time_since_epoch().count() == 0 ||
|
delivery.last_sent_time.time_since_epoch().count() == 0 ||
|
||||||
now - delivery.last_sent_time >= kCursorStateHeartbeatInterval;
|
now - delivery.last_sent_time >= kCursorStateHeartbeatInterval;
|
||||||
if (suppress_position) {
|
// Position feedback waits for a good sample and the echo-suppression
|
||||||
// Cursor appearance is independent of cursor position. Continue sending
|
// interval. Appearance changes and heartbeats are independent of both.
|
||||||
// shape changes to every controller while withholding only the sampled
|
const bool include_position = sample_ok && !suppress_position;
|
||||||
// position from the connection that originated recent input.
|
if (shape_changed || heartbeat_due ||
|
||||||
if (shape_changed || heartbeat_due) {
|
(include_position && (delivery.feedback_pending || position_update_due))) {
|
||||||
recipients.push_back({remote_id, false});
|
recipients.push_back({remote_id, include_position});
|
||||||
}
|
|
||||||
} else if (delivery.feedback_pending || shape_changed ||
|
|
||||||
position_update_due || heartbeat_due) {
|
|
||||||
recipients.push_back({remote_id, true});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (recipients.empty()) return;
|
if (recipients.empty()) return;
|
||||||
@@ -1964,6 +1977,18 @@ void GuiApplication::ShareLocalCursorState() {
|
|||||||
for (const auto& recipient : recipients) {
|
for (const auto& recipient : recipients) {
|
||||||
auto& delivery = cursor_delivery_states_[recipient.remote_id];
|
auto& delivery = cursor_delivery_states_[recipient.remote_id];
|
||||||
CursorState outgoing = sampled;
|
CursorState outgoing = sampled;
|
||||||
|
if (!sample_ok) {
|
||||||
|
// Explicitly report unknown to new receivers, but retain the last
|
||||||
|
// presentation for peers that only understand visible/shape.
|
||||||
|
outgoing = delivery.has_sent ? delivery.last_sent : CursorState{};
|
||||||
|
if (!delivery.has_sent) {
|
||||||
|
outgoing.visible = true;
|
||||||
|
outgoing.shape = RemoteCursorShape::default_cursor;
|
||||||
|
}
|
||||||
|
outgoing.seq = sampled.seq;
|
||||||
|
outgoing.render_mode = sampled.render_mode;
|
||||||
|
outgoing.hidden_reason = sampled.hidden_reason;
|
||||||
|
}
|
||||||
outgoing.position_update = recipient.include_position;
|
outgoing.position_update = recipient.include_position;
|
||||||
if (!recipient.include_position) {
|
if (!recipient.include_position) {
|
||||||
// Keep legacy receivers at their last acknowledged position as well.
|
// Keep legacy receivers at their last acknowledged position as well.
|
||||||
@@ -1991,6 +2016,13 @@ void GuiApplication::ShareLocalCursorState() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!delivery.has_sent ||
|
||||||
|
delivery.last_sent.render_mode != outgoing.render_mode ||
|
||||||
|
delivery.last_sent.hidden_reason != outgoing.hidden_reason) {
|
||||||
|
LOG_INFO("Send cursor presentation to [{}]: mode={}, reason={}",
|
||||||
|
recipient.remote_id, static_cast<int>(outgoing.render_mode),
|
||||||
|
static_cast<int>(outgoing.hidden_reason));
|
||||||
|
}
|
||||||
delivery.last_sent = outgoing;
|
delivery.last_sent = outgoing;
|
||||||
delivery.has_sent = true;
|
delivery.has_sent = true;
|
||||||
if (recipient.include_position) {
|
if (recipient.include_position) {
|
||||||
@@ -2582,9 +2614,7 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
props->remote_cursor_state_received_;
|
props->remote_cursor_state_received_;
|
||||||
if (remote_cursor_active) {
|
if (remote_cursor_active) {
|
||||||
remote_cursor_shape = static_cast<int>(
|
remote_cursor_shape = static_cast<int>(
|
||||||
props->remote_cursor_state_.visible
|
props->cursor_presentation_.Resolve(SDL_GetTicks()));
|
||||||
? props->remote_cursor_state_.shape
|
|
||||||
: RemoteCursorShape::none);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(*ui_->stream)->set_remote_cursor_active(remote_cursor_active);
|
(*ui_->stream)->set_remote_cursor_active(remote_cursor_active);
|
||||||
@@ -3267,6 +3297,10 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
|
|||||||
|
|
||||||
controlled_remote_id_ = props->remote_id_;
|
controlled_remote_id_ = props->remote_id_;
|
||||||
const std::string message = action.to_json();
|
const std::string message = action.to_json();
|
||||||
|
{
|
||||||
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||||
|
props->cursor_presentation_.NoteInput(SDL_GetTicks());
|
||||||
|
}
|
||||||
SendDataFrame(props->peer_, message.c_str(), message.size(),
|
SendDataFrame(props->peer_, message.c_str(), message.size(),
|
||||||
props->mouse_label_.c_str());
|
props->mouse_label_.c_str());
|
||||||
}
|
}
|
||||||
@@ -3305,6 +3339,10 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
|
|||||||
if (action.m.s == 0) {
|
if (action.m.s == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||||
|
props->cursor_presentation_.NoteInput(SDL_GetTicks());
|
||||||
|
}
|
||||||
controlled_remote_id_ = props->remote_id_;
|
controlled_remote_id_ = props->remote_id_;
|
||||||
const std::string message = action.to_json();
|
const std::string message = action.to_json();
|
||||||
SendDataFrame(props->peer_, message.c_str(), message.size(),
|
SendDataFrame(props->peer_, message.c_str(), message.size(),
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ void GuiRuntime::ResetRemoteSessionResources(
|
|||||||
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
|
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
|
||||||
props->remote_cursor_state_ = {};
|
props->remote_cursor_state_ = {};
|
||||||
props->remote_cursor_state_received_ = false;
|
props->remote_cursor_state_received_ = false;
|
||||||
|
props->cursor_presentation_ = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-09-16
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _CURSOR_PRESENTATION_H_
|
||||||
|
#define _CURSOR_PRESENTATION_H_
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// All times use one monotonic millisecond clock. The owning session's cursor
|
||||||
|
// mutex protects this policy from the transport and UI threads.
|
||||||
|
class CursorPresentation {
|
||||||
|
public:
|
||||||
|
static constexpr uint64_t kStateTimeoutMs = 1500; // Three 500ms heartbeats.
|
||||||
|
static constexpr uint64_t kInputActivityMs = 1500;
|
||||||
|
|
||||||
|
void Observe(const CursorState& state, uint64_t now) {
|
||||||
|
if (state.render_mode == CursorRenderMode::unknown) {
|
||||||
|
// Repeated unavailable samples must not extend the recovery deadline.
|
||||||
|
if (!sample_unavailable_) stale_at_ = now + kStateTimeoutMs;
|
||||||
|
sample_unavailable_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sample_unavailable_ = false;
|
||||||
|
stale_at_ = now + kStateTimeoutMs;
|
||||||
|
const bool separate =
|
||||||
|
state.render_mode == CursorRenderMode::separate ||
|
||||||
|
(state.render_mode == CursorRenderMode::legacy && state.visible);
|
||||||
|
// Old senders cannot distinguish embedded from hidden. Only explicit
|
||||||
|
// metadata from a new sender can establish the no-device compatibility
|
||||||
|
// case.
|
||||||
|
no_device_ = state.render_mode == CursorRenderMode::hidden &&
|
||||||
|
state.hidden_reason == CursorHiddenReason::no_pointing_device;
|
||||||
|
held_shape_ = separate ? state.shape : RemoteCursorShape::none;
|
||||||
|
if (held_shape_ != RemoteCursorShape::none) {
|
||||||
|
last_visible_shape_ = held_shape_;
|
||||||
|
}
|
||||||
|
if (!no_device_) fallback_engaged_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NoteInput(uint64_t now) { last_input_ = now; }
|
||||||
|
|
||||||
|
RemoteCursorShape Resolve(uint64_t now) {
|
||||||
|
if (!stale_at_) return RemoteCursorShape::default_cursor;
|
||||||
|
|
||||||
|
const bool recent_input =
|
||||||
|
last_input_ && now - *last_input_ < kInputActivityMs;
|
||||||
|
const bool can_recover =
|
||||||
|
now >= *stale_at_ || (!sample_unavailable_ && no_device_);
|
||||||
|
if (can_recover && recent_input) {
|
||||||
|
fallback_engaged_ = true;
|
||||||
|
}
|
||||||
|
// Once needed, retain the locator while idle until a good non-compatibility
|
||||||
|
// sample arrives. Otherwise it would blink off whenever input pauses.
|
||||||
|
return fallback_engaged_ ? last_visible_shape_ : held_shape_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool sample_unavailable_ = false;
|
||||||
|
bool no_device_ = false;
|
||||||
|
bool fallback_engaged_ = false;
|
||||||
|
std::optional<uint64_t> stale_at_;
|
||||||
|
std::optional<uint64_t> last_input_;
|
||||||
|
RemoteCursorShape held_shape_ = RemoteCursorShape::default_cursor;
|
||||||
|
RemoteCursorShape last_visible_shape_ = RemoteCursorShape::default_cursor;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -6,14 +6,16 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include "platform/windows/input/windows_cursor_shape.h"
|
#include "platform/windows/input/windows_cursor_state.h"
|
||||||
#include "platform/windows/screen_capturer/dxgi_cursor_state.h"
|
#include "platform/windows/screen_capturer/dxgi_cursor_state.h"
|
||||||
#include "platform/windows/screen_capturer/secure_desktop_cursor_state.h"
|
#include "platform/windows/screen_capturer/secure_desktop_cursor_state.h"
|
||||||
#include "runtime/cursor_position.h"
|
#include "runtime/cursor_position.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
struct CursorStateProvider::Impl {};
|
struct CursorStateProvider::Impl {
|
||||||
|
WindowsCursorState cursor;
|
||||||
|
};
|
||||||
|
|
||||||
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
||||||
CursorStateProvider::~CursorStateProvider() = default;
|
CursorStateProvider::~CursorStateProvider() = default;
|
||||||
@@ -24,11 +26,17 @@ bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
|
|||||||
|
|
||||||
SecureDesktopCursorSnapshot secure_cursor{};
|
SecureDesktopCursorSnapshot secure_cursor{};
|
||||||
if (SharedSecureDesktopCursorState().Get(&secure_cursor)) {
|
if (SharedSecureDesktopCursorState().Get(&secure_cursor)) {
|
||||||
if (!secure_cursor.valid) return false;
|
if (!secure_cursor.valid) {
|
||||||
|
state->hidden_reason = CursorHiddenReason::secure_desktop_pending;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
state->seq = 0;
|
state->seq = 0;
|
||||||
NormalizeCursorPosition(secure_cursor.x, secure_cursor.y, displays,
|
NormalizeCursorPosition(secure_cursor.x, secure_cursor.y, displays,
|
||||||
preferred_display, state);
|
preferred_display, state);
|
||||||
state->visible = secure_cursor.visible != 0;
|
state->visible = secure_cursor.visible != 0;
|
||||||
|
state->render_mode = static_cast<CursorRenderMode>(secure_cursor.render_mode);
|
||||||
|
state->hidden_reason =
|
||||||
|
static_cast<CursorHiddenReason>(secure_cursor.hidden_reason);
|
||||||
state->shape = state->visible
|
state->shape = state->visible
|
||||||
? static_cast<RemoteCursorShape>(secure_cursor.shape)
|
? static_cast<RemoteCursorShape>(secure_cursor.shape)
|
||||||
: RemoteCursorShape::none;
|
: RemoteCursorShape::none;
|
||||||
@@ -45,10 +53,9 @@ bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
|
|||||||
void* cursor_monitor = state->position_valid
|
void* cursor_monitor = state->position_valid
|
||||||
? displays[state->display_id].handle
|
? displays[state->display_id].handle
|
||||||
: nullptr;
|
: nullptr;
|
||||||
state->visible = SharedDxgiCursorState().ShouldDrawCursor(
|
const bool embedded = (info.flags & CURSOR_SHOWING) != 0 &&
|
||||||
(info.flags & CURSOR_SHOWING) != 0, cursor_monitor);
|
!SharedDxgiCursorState().ShouldDrawCursor(true, cursor_monitor);
|
||||||
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
|
impl_->cursor.Sample(info, embedded, state);
|
||||||
: RemoteCursorShape::none;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -234,7 +234,11 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
const bool changed =
|
const bool changed =
|
||||||
!props->remote_cursor_state_received_ ||
|
!props->remote_cursor_state_received_ ||
|
||||||
props->remote_cursor_state_.visible != remote_action.cs.visible ||
|
props->remote_cursor_state_.visible != remote_action.cs.visible ||
|
||||||
props->remote_cursor_state_.shape != remote_action.cs.shape;
|
props->remote_cursor_state_.shape != remote_action.cs.shape ||
|
||||||
|
props->remote_cursor_state_.render_mode !=
|
||||||
|
remote_action.cs.render_mode ||
|
||||||
|
props->remote_cursor_state_.hidden_reason !=
|
||||||
|
remote_action.cs.hidden_reason;
|
||||||
CursorState merged = remote_action.cs;
|
CursorState merged = remote_action.cs;
|
||||||
if (!remote_action.cs.position_update &&
|
if (!remote_action.cs.position_update &&
|
||||||
props->remote_cursor_state_received_) {
|
props->remote_cursor_state_received_) {
|
||||||
@@ -245,10 +249,14 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
}
|
}
|
||||||
props->remote_cursor_state_ = merged;
|
props->remote_cursor_state_ = merged;
|
||||||
props->remote_cursor_state_received_ = true;
|
props->remote_cursor_state_received_ = true;
|
||||||
|
props->cursor_presentation_.Observe(merged, SDL_GetTicks());
|
||||||
if (changed) {
|
if (changed) {
|
||||||
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}",
|
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}, "
|
||||||
|
"mode={}, reason={}",
|
||||||
remote_action.cs.seq, remote_action.cs.visible,
|
remote_action.cs.seq, remote_action.cs.visible,
|
||||||
static_cast<int>(remote_action.cs.shape));
|
static_cast<int>(remote_action.cs.shape),
|
||||||
|
static_cast<int>(remote_action.cs.render_mode),
|
||||||
|
static_cast<int>(remote_action.cs.hidden_reason));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||||
props->remote_cursor_state_ = {};
|
props->remote_cursor_state_ = {};
|
||||||
props->remote_cursor_state_received_ = false;
|
props->remote_cursor_state_received_ = false;
|
||||||
|
props->cursor_presentation_ = {};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
RemoteAction remote_action{};
|
RemoteAction remote_action{};
|
||||||
@@ -331,6 +332,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||||
props->remote_cursor_state_ = {};
|
props->remote_cursor_state_ = {};
|
||||||
props->remote_cursor_state_received_ = false;
|
props->remote_cursor_state_received_ = false;
|
||||||
|
props->cursor_presentation_ = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
|
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <remote_action.h>
|
#include <remote_action.h>
|
||||||
#include <stream_names.h>
|
#include <stream_names.h>
|
||||||
#include "display_info.h"
|
#include "display_info.h"
|
||||||
|
#include "cursor_presentation.h"
|
||||||
#include "minirtc.h"
|
#include "minirtc.h"
|
||||||
|
|
||||||
namespace crossdesk { class PeerEventHandler; }
|
namespace crossdesk { class PeerEventHandler; }
|
||||||
@@ -173,6 +174,7 @@ struct RemoteSession {
|
|||||||
std::mutex remote_cursor_state_mutex_;
|
std::mutex remote_cursor_state_mutex_;
|
||||||
CursorState remote_cursor_state_{};
|
CursorState remote_cursor_state_{};
|
||||||
bool remote_cursor_state_received_ = false;
|
bool remote_cursor_state_received_ = false;
|
||||||
|
CursorPresentation cursor_presentation_;
|
||||||
// Shared by minirtc callbacks, Slint rendering and SDL audio callbacks.
|
// Shared by minirtc callbacks, Slint rendering and SDL audio callbacks.
|
||||||
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
|
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
|
||||||
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
|
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
|
||||||
|
|||||||
@@ -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_
|
#ifndef CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
|
||||||
#define CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
|
#define CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
@@ -14,8 +15,10 @@ struct SecureDesktopCursorSnapshot {
|
|||||||
uint32_t shape = 0;
|
uint32_t shape = 0;
|
||||||
int32_t x = 0;
|
int32_t x = 0;
|
||||||
int32_t y = 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
|
// 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
|
// capture is active, even an unavailable sample must not fall back to sampling
|
||||||
@@ -30,13 +33,20 @@ class SecureDesktopCursorState {
|
|||||||
|
|
||||||
void Update(const SecureDesktopCursorSnapshot& snapshot) {
|
void Update(const SecureDesktopCursorSnapshot& snapshot) {
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
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 {
|
bool Get(SecureDesktopCursorSnapshot* snapshot) const {
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
if (!active_ || !snapshot) return false;
|
if (!active_ || !snapshot) return false;
|
||||||
*snapshot = snapshot_;
|
*snapshot = snapshot_;
|
||||||
|
if (std::chrono::steady_clock::now() - updated_at_ >=
|
||||||
|
std::chrono::milliseconds(1500)) {
|
||||||
|
snapshot->valid = 0;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +54,7 @@ class SecureDesktopCursorState {
|
|||||||
mutable std::mutex mutex_;
|
mutable std::mutex mutex_;
|
||||||
bool active_ = false;
|
bool active_ = false;
|
||||||
SecureDesktopCursorSnapshot snapshot_{};
|
SecureDesktopCursorSnapshot snapshot_{};
|
||||||
|
std::chrono::steady_clock::time_point updated_at_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
inline SecureDesktopCursorState& SharedSecureDesktopCursorState() {
|
inline SecureDesktopCursorState& SharedSecureDesktopCursorState() {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
#include "path_manager.h"
|
#include "path_manager.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "session_helper_shared.h"
|
#include "session_helper_shared.h"
|
||||||
#include "windows_cursor_shape.h"
|
#include "windows_cursor_state.h"
|
||||||
#include "windows_input_marker.h"
|
#include "windows_input_marker.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -1191,10 +1191,14 @@ struct SecureDesktopGdiResources {
|
|||||||
CURSORINFO cursor{};
|
CURSORINFO cursor{};
|
||||||
cursor.cbSize = sizeof(cursor);
|
cursor.cbSize = sizeof(cursor);
|
||||||
if (GetCursorInfo(&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.valid = 1;
|
||||||
cursor_snapshot.visible = (cursor.flags & CURSOR_SHOWING) != 0;
|
cursor_snapshot.visible = state.visible;
|
||||||
cursor_snapshot.shape = static_cast<uint32_t>(
|
cursor_snapshot.shape = static_cast<uint32_t>(state.shape);
|
||||||
crossdesk::ShapeFromWindowsCursor(cursor.hCursor));
|
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.x = cursor.ptScreenPos.x;
|
||||||
cursor_snapshot.y = cursor.ptScreenPos.y;
|
cursor_snapshot.y = cursor.ptScreenPos.y;
|
||||||
if (request.show_cursor && cursor_snapshot.visible &&
|
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
|
// A frame with an embedded cursor must not get a second cursor
|
||||||
// overlay from a native controller sharing the web capture path.
|
// overlay from a native controller sharing the web capture path.
|
||||||
cursor_snapshot.visible = 0;
|
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[] =
|
inline constexpr wchar_t kCrossDeskSecureDesktopFrameReadyEventPrefix[] =
|
||||||
L"Global\\CrossDeskSecureDesktopFrameReady-";
|
L"Global\\CrossDeskSecureDesktopFrameReady-";
|
||||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
|
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
|
||||||
// Version 2 adds cursor metadata to both headers. The GUI and local session
|
// Version 3 distinguishes embedded, hidden and unavailable cursor states.
|
||||||
// helper must be deployed together; the remote-control wire format is unchanged.
|
// The GUI and local session helper must be deployed together.
|
||||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 2;
|
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 3;
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
#pragma pack(push, 1)
|
||||||
struct CrossDeskSecureDesktopFrameHeader {
|
struct CrossDeskSecureDesktopFrameHeader {
|
||||||
|
|||||||
@@ -73,6 +73,24 @@ struct KeyboardState {
|
|||||||
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
|
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Optional presentation metadata. Zero preserves pre-metadata peers' behavior.
|
||||||
|
enum class CursorRenderMode : uint8_t {
|
||||||
|
legacy = 0,
|
||||||
|
separate,
|
||||||
|
embedded,
|
||||||
|
hidden,
|
||||||
|
unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CursorHiddenReason : uint8_t {
|
||||||
|
unspecified = 0,
|
||||||
|
system_hidden,
|
||||||
|
system_suppressed,
|
||||||
|
no_pointing_device,
|
||||||
|
sampling_failed,
|
||||||
|
secure_desktop_pending,
|
||||||
|
};
|
||||||
|
|
||||||
struct CursorState {
|
struct CursorState {
|
||||||
uint32_t seq;
|
uint32_t seq;
|
||||||
bool visible;
|
bool visible;
|
||||||
@@ -89,6 +107,10 @@ struct CursorState {
|
|||||||
// Shape-only updates set this to false so cursor appearance can remain
|
// Shape-only updates set this to false so cursor appearance can remain
|
||||||
// responsive while position feedback to the input source is suppressed.
|
// responsive while position feedback to the input source is suppressed.
|
||||||
bool position_update;
|
bool position_update;
|
||||||
|
// Keep visible/shape for old receivers; new receivers can distinguish an
|
||||||
|
// embedded pointer from a hidden pointer and an unavailable sample.
|
||||||
|
CursorRenderMode render_mode;
|
||||||
|
CursorHiddenReason hidden_reason;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct HostInfo {
|
struct HostInfo {
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ std::string RemoteAction::ToJson(const RemoteAction& action) {
|
|||||||
{"visual_offset_x", action.cs.visual_offset_x},
|
{"visual_offset_x", action.cs.visual_offset_x},
|
||||||
{"visual_offset_y", action.cs.visual_offset_y},
|
{"visual_offset_y", action.cs.visual_offset_y},
|
||||||
{"display_id", action.cs.display_id},
|
{"display_id", action.cs.display_id},
|
||||||
{"position_update", action.cs.position_update}};
|
{"position_update", action.cs.position_update},
|
||||||
|
{"render_mode", static_cast<int>(action.cs.render_mode)},
|
||||||
|
{"hidden_reason", static_cast<int>(action.cs.hidden_reason)}};
|
||||||
break;
|
break;
|
||||||
case ControlType::audio_capture:
|
case ControlType::audio_capture:
|
||||||
object["audio_capture"] = action.a;
|
object["audio_capture"] = action.a;
|
||||||
@@ -197,6 +199,19 @@ bool RemoteAction::FromJson(const std::string& json_string,
|
|||||||
output.cs.seq = cursor_state_object.at("seq").get<uint32_t>();
|
output.cs.seq = cursor_state_object.at("seq").get<uint32_t>();
|
||||||
output.cs.visible = cursor_state_object.at("visible").get<bool>();
|
output.cs.visible = cursor_state_object.at("visible").get<bool>();
|
||||||
output.cs.shape = static_cast<RemoteCursorShape>(shape);
|
output.cs.shape = static_cast<RemoteCursorShape>(shape);
|
||||||
|
const int mode = cursor_state_object.value("render_mode", 0);
|
||||||
|
const int reason = cursor_state_object.value("hidden_reason", 0);
|
||||||
|
// Unknown extensions from a newer peer fall back to visible/shape.
|
||||||
|
output.cs.render_mode =
|
||||||
|
mode >= 0 && mode <= static_cast<int>(CursorRenderMode::unknown)
|
||||||
|
? static_cast<CursorRenderMode>(mode)
|
||||||
|
: CursorRenderMode::legacy;
|
||||||
|
output.cs.hidden_reason =
|
||||||
|
reason >= 0 &&
|
||||||
|
reason <= static_cast<int>(
|
||||||
|
CursorHiddenReason::secure_desktop_pending)
|
||||||
|
? static_cast<CursorHiddenReason>(reason)
|
||||||
|
: CursorHiddenReason::unspecified;
|
||||||
output.cs.position_valid =
|
output.cs.position_valid =
|
||||||
cursor_state_object.value("position_valid", false);
|
cursor_state_object.value("position_valid", false);
|
||||||
output.cs.x = cursor_state_object.value("x", 0.5f);
|
output.cs.x = cursor_state_object.value("x", 0.5f);
|
||||||
|
|||||||
Reference in New Issue
Block a user