diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index fe2843a9..c4900301 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -1584,6 +1584,10 @@ void GuiApplication::BindStreamCallbacks() { return; } props->selected_display_ = index; + { + std::lock_guard lock(props->remote_cursor_state_mutex_); + props->cursor_presentation_ = {}; + } RemoteAction action{}; action.type = ControlType::display_id; action.d = index; @@ -1883,9 +1887,20 @@ void GuiApplication::ShareLocalCursorState() { } CursorState sampled{}; - if (!cursor_state_provider_.Sample(devices_.display_info_list(), - selected_display_, &sampled)) { - return; + const bool sample_ok = cursor_state_provider_.Sample( + devices_.display_info_list(), selected_display_, &sampled); + 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(); @@ -1924,21 +1939,23 @@ void GuiApplication::ShareLocalCursorState() { const CursorState& previous = delivery.last_sent; const bool shape_changed = - !delivery.has_sent || sampled.visible != previous.visible || - sampled.shape != previous.shape || - std::abs(sampled.visual_offset_x - previous.visual_offset_x) > - kCursorPositionEpsilon || - std::abs(sampled.visual_offset_y - previous.visual_offset_y) > - kCursorPositionEpsilon; + !delivery.has_sent || sampled.render_mode != previous.render_mode || + sampled.hidden_reason != previous.hidden_reason || + (sample_ok && + (sampled.visible != previous.visible || sampled.shape != previous.shape || + std::abs(sampled.visual_offset_x - previous.visual_offset_x) > + kCursorPositionEpsilon || + std::abs(sampled.visual_offset_y - previous.visual_offset_y) > + kCursorPositionEpsilon)); // Preserve sub-point cursor motion. At 10x client zoom, the old roughly // one-logical-pixel threshold became several visible phone points. const bool position_changed = - !delivery.has_sent || - sampled.position_valid != previous.position_valid || - sampled.display_id != previous.display_id || - (sampled.position_valid && previous.position_valid && - (std::abs(sampled.x - previous.x) > kCursorPositionEpsilon || - std::abs(sampled.y - previous.y) > kCursorPositionEpsilon)); + sample_ok && + (!delivery.has_sent || sampled.position_valid != previous.position_valid || + sampled.display_id != previous.display_id || + (sampled.position_valid && previous.position_valid && + (std::abs(sampled.x - previous.x) > kCursorPositionEpsilon || + std::abs(sampled.y - previous.y) > kCursorPositionEpsilon))); const bool position_update_due = position_changed && (delivery.last_sent_time.time_since_epoch().count() == 0 || @@ -1946,16 +1963,12 @@ void GuiApplication::ShareLocalCursorState() { const bool heartbeat_due = delivery.last_sent_time.time_since_epoch().count() == 0 || now - delivery.last_sent_time >= kCursorStateHeartbeatInterval; - if (suppress_position) { - // Cursor appearance is independent of cursor position. Continue sending - // shape changes to every controller while withholding only the sampled - // position from the connection that originated recent input. - if (shape_changed || heartbeat_due) { - recipients.push_back({remote_id, false}); - } - } else if (delivery.feedback_pending || shape_changed || - position_update_due || heartbeat_due) { - recipients.push_back({remote_id, true}); + // Position feedback waits for a good sample and the echo-suppression + // interval. Appearance changes and heartbeats are independent of both. + const bool include_position = sample_ok && !suppress_position; + if (shape_changed || heartbeat_due || + (include_position && (delivery.feedback_pending || position_update_due))) { + recipients.push_back({remote_id, include_position}); } } if (recipients.empty()) return; @@ -1964,6 +1977,18 @@ void GuiApplication::ShareLocalCursorState() { for (const auto& recipient : recipients) { auto& delivery = cursor_delivery_states_[recipient.remote_id]; 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; if (!recipient.include_position) { // Keep legacy receivers at their last acknowledged position as well. @@ -1991,6 +2016,13 @@ void GuiApplication::ShareLocalCursorState() { 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(outgoing.render_mode), + static_cast(outgoing.hidden_reason)); + } delivery.last_sent = outgoing; delivery.has_sent = true; if (recipient.include_position) { @@ -2582,9 +2614,7 @@ void GuiApplication::SyncStreamWindow() { props->remote_cursor_state_received_; if (remote_cursor_active) { remote_cursor_shape = static_cast( - props->remote_cursor_state_.visible - ? props->remote_cursor_state_.shape - : RemoteCursorShape::none); + props->cursor_presentation_.Resolve(SDL_GetTicks())); } } (*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_; 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(), 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) { return; } + { + std::lock_guard lock(props->remote_cursor_state_mutex_); + props->cursor_presentation_.NoteInput(SDL_GetTicks()); + } controlled_remote_id_ = props->remote_id_; const std::string message = action.to_json(); SendDataFrame(props->peer_, message.c_str(), message.size(), diff --git a/apps/desktop/src/gui/runtime/connection_runtime.cpp b/apps/desktop/src/gui/runtime/connection_runtime.cpp index 63cb8385..15ed0f47 100644 --- a/apps/desktop/src/gui/runtime/connection_runtime.cpp +++ b/apps/desktop/src/gui/runtime/connection_runtime.cpp @@ -438,6 +438,7 @@ void GuiRuntime::ResetRemoteSessionResources( std::lock_guard lock(props->remote_cursor_state_mutex_); props->remote_cursor_state_ = {}; props->remote_cursor_state_received_ = false; + props->cursor_presentation_ = {}; } } diff --git a/apps/desktop/src/gui/runtime/cursor_presentation.h b/apps/desktop/src/gui/runtime/cursor_presentation.h new file mode 100644 index 00000000..0f37df2b --- /dev/null +++ b/apps/desktop/src/gui/runtime/cursor_presentation.h @@ -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 + +#include +#include + +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 stale_at_; + std::optional last_input_; + RemoteCursorShape held_shape_ = RemoteCursorShape::default_cursor; + RemoteCursorShape last_visible_shape_ = RemoteCursorShape::default_cursor; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/gui/runtime/cursor_state_provider.cpp b/apps/desktop/src/gui/runtime/cursor_state_provider.cpp index e2508acb..1199b8f7 100644 --- a/apps/desktop/src/gui/runtime/cursor_state_provider.cpp +++ b/apps/desktop/src/gui/runtime/cursor_state_provider.cpp @@ -6,14 +6,16 @@ #include -#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/secure_desktop_cursor_state.h" #include "runtime/cursor_position.h" namespace crossdesk { -struct CursorStateProvider::Impl {}; +struct CursorStateProvider::Impl { + WindowsCursorState cursor; +}; CursorStateProvider::CursorStateProvider() : impl_(std::make_unique()) {} CursorStateProvider::~CursorStateProvider() = default; @@ -24,11 +26,17 @@ bool CursorStateProvider::Sample(const std::vector& displays, SecureDesktopCursorSnapshot 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; NormalizeCursorPosition(secure_cursor.x, secure_cursor.y, displays, preferred_display, state); state->visible = secure_cursor.visible != 0; + state->render_mode = static_cast(secure_cursor.render_mode); + state->hidden_reason = + static_cast(secure_cursor.hidden_reason); state->shape = state->visible ? static_cast(secure_cursor.shape) : RemoteCursorShape::none; @@ -45,10 +53,9 @@ bool CursorStateProvider::Sample(const std::vector& displays, void* cursor_monitor = state->position_valid ? displays[state->display_id].handle : nullptr; - state->visible = SharedDxgiCursorState().ShouldDrawCursor( - (info.flags & CURSOR_SHOWING) != 0, cursor_monitor); - state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor) - : RemoteCursorShape::none; + const bool embedded = (info.flags & CURSOR_SHOWING) != 0 && + !SharedDxgiCursorState().ShouldDrawCursor(true, cursor_monitor); + impl_->cursor.Sample(info, embedded, state); return true; } diff --git a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp index ec0e1a25..c6b79295 100644 --- a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp +++ b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp @@ -234,7 +234,11 @@ void PeerEventHandler::OnReceiveDataBuffer( const bool changed = !props->remote_cursor_state_received_ || 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; if (!remote_action.cs.position_update && props->remote_cursor_state_received_) { @@ -245,10 +249,14 @@ void PeerEventHandler::OnReceiveDataBuffer( } props->remote_cursor_state_ = merged; props->remote_cursor_state_received_ = true; + props->cursor_presentation_.Observe(merged, SDL_GetTicks()); 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, - static_cast(remote_action.cs.shape)); + static_cast(remote_action.cs.shape), + static_cast(remote_action.cs.render_mode), + static_cast(remote_action.cs.hidden_reason)); } } return; diff --git a/apps/desktop/src/gui/runtime/peer_event_handler.cpp b/apps/desktop/src/gui/runtime/peer_event_handler.cpp index ad8cb791..6432af51 100644 --- a/apps/desktop/src/gui/runtime/peer_event_handler.cpp +++ b/apps/desktop/src/gui/runtime/peer_event_handler.cpp @@ -262,6 +262,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, std::lock_guard lock(props->remote_cursor_state_mutex_); props->remote_cursor_state_ = {}; props->remote_cursor_state_received_ = false; + props->cursor_presentation_ = {}; } { RemoteAction remote_action{}; @@ -331,6 +332,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, std::lock_guard lock(props->remote_cursor_state_mutex_); props->remote_cursor_state_ = {}; props->remote_cursor_state_received_ = false; + props->cursor_presentation_ = {}; } std::shared_ptr> native_snapshot; diff --git a/apps/desktop/src/gui/runtime/remote_session.h b/apps/desktop/src/gui/runtime/remote_session.h index 4e33e891..bb7d13f3 100644 --- a/apps/desktop/src/gui/runtime/remote_session.h +++ b/apps/desktop/src/gui/runtime/remote_session.h @@ -22,6 +22,7 @@ #include #include #include "display_info.h" +#include "cursor_presentation.h" #include "minirtc.h" namespace crossdesk { class PeerEventHandler; } @@ -173,6 +174,7 @@ struct RemoteSession { std::mutex remote_cursor_state_mutex_; CursorState remote_cursor_state_{}; bool remote_cursor_state_received_ = false; + CursorPresentation cursor_presentation_; // Shared by minirtc callbacks, Slint rendering and SDL audio callbacks. std::atomic connection_status_ = ConnectionStatus::Closed; TraversalMode traversal_mode_ = TraversalMode::UnknownMode; diff --git a/apps/desktop/src/platform/windows/input/windows_cursor_state.h b/apps/desktop/src/platform/windows/input/windows_cursor_state.h new file mode 100644 index 00000000..7ace2638 --- /dev/null +++ b/apps/desktop/src/platform/windows/input/windows_cursor_state.h @@ -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 +#include + +#include +#include + +#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(-1) || + count > 4096) + return false; + if (count == 0) return true; + std::vector devices(count); + const UINT read = GetRawInputDeviceList(devices.data(), &count, + sizeof(RAWINPUTDEVICELIST)); + if (read == static_cast(-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 \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_cursor_state.h b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_cursor_state.h index 0cd6b9fb..38cb5a45 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_cursor_state.h +++ b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_cursor_state.h @@ -1,6 +1,7 @@ #ifndef CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_ #define CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_ +#include #include #include @@ -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 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 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() { diff --git a/apps/desktop/src/platform/windows/service/session_helper_main.cpp b/apps/desktop/src/platform/windows/service/session_helper_main.cpp index ce9aac21..b651bd72 100644 --- a/apps/desktop/src/platform/windows/service/session_helper_main.cpp +++ b/apps/desktop/src/platform/windows/service/session_helper_main.cpp @@ -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( - crossdesk::ShapeFromWindowsCursor(cursor.hCursor)); + cursor_snapshot.visible = state.visible; + cursor_snapshot.shape = static_cast(state.shape); + cursor_snapshot.render_mode = static_cast(state.render_mode); + cursor_snapshot.hidden_reason = static_cast(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(crossdesk::CursorRenderMode::embedded); } } } diff --git a/apps/desktop/src/platform/windows/service/session_helper_shared.h b/apps/desktop/src/platform/windows/service/session_helper_shared.h index c150b8f2..5b744297 100644 --- a/apps/desktop/src/platform/windows/service/session_helper_shared.h +++ b/apps/desktop/src/platform/windows/service/session_helper_shared.h @@ -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 { diff --git a/libs/wire/include/remote_action.h b/libs/wire/include/remote_action.h index 395d0e71..8c994976 100644 --- a/libs/wire/include/remote_action.h +++ b/libs/wire/include/remote_action.h @@ -73,6 +73,24 @@ struct KeyboardState { 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 { uint32_t seq; bool visible; @@ -89,6 +107,10 @@ struct CursorState { // Shape-only updates set this to false so cursor appearance can remain // responsive while position feedback to the input source is suppressed. 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 { diff --git a/libs/wire/src/remote_action.cpp b/libs/wire/src/remote_action.cpp index 5d1f46d4..fa11493e 100644 --- a/libs/wire/src/remote_action.cpp +++ b/libs/wire/src/remote_action.cpp @@ -91,7 +91,9 @@ std::string RemoteAction::ToJson(const RemoteAction& action) { {"visual_offset_x", action.cs.visual_offset_x}, {"visual_offset_y", action.cs.visual_offset_y}, {"display_id", action.cs.display_id}, - {"position_update", action.cs.position_update}}; + {"position_update", action.cs.position_update}, + {"render_mode", static_cast(action.cs.render_mode)}, + {"hidden_reason", static_cast(action.cs.hidden_reason)}}; break; case ControlType::audio_capture: 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(); output.cs.visible = cursor_state_object.at("visible").get(); output.cs.shape = static_cast(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(CursorRenderMode::unknown) + ? static_cast(mode) + : CursorRenderMode::legacy; + output.cs.hidden_reason = + reason >= 0 && + reason <= static_cast( + CursorHiddenReason::secure_desktop_pending) + ? static_cast(reason) + : CursorHiddenReason::unspecified; output.cs.position_valid = cursor_state_object.value("position_valid", false); output.cs.x = cursor_state_object.value("x", 0.5f);