mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-26 13:29:31 +08:00
[feat] synchronize remote cursor state
This commit is contained in:
@@ -28,6 +28,7 @@ typedef enum {
|
|||||||
service_status = 5,
|
service_status = 5,
|
||||||
service_command = 6,
|
service_command = 6,
|
||||||
keyboard_state = 7,
|
keyboard_state = 7,
|
||||||
|
cursor_state = 8,
|
||||||
} ControlType;
|
} ControlType;
|
||||||
typedef enum {
|
typedef enum {
|
||||||
move = 0,
|
move = 0,
|
||||||
@@ -70,6 +71,47 @@ typedef struct {
|
|||||||
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
|
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
|
||||||
} KeyboardState;
|
} KeyboardState;
|
||||||
|
|
||||||
|
// Keep these values aligned with Slint's MouseCursor enum. The wire protocol
|
||||||
|
// intentionally carries a semantic cursor instead of a platform handle so a
|
||||||
|
// Windows, macOS or Linux host can control a different desktop platform.
|
||||||
|
enum class CursorShape : uint8_t {
|
||||||
|
default_cursor = 0,
|
||||||
|
none,
|
||||||
|
help,
|
||||||
|
pointer,
|
||||||
|
progress,
|
||||||
|
wait,
|
||||||
|
crosshair,
|
||||||
|
text,
|
||||||
|
alias,
|
||||||
|
copy,
|
||||||
|
move,
|
||||||
|
no_drop,
|
||||||
|
not_allowed,
|
||||||
|
grab,
|
||||||
|
grabbing,
|
||||||
|
col_resize,
|
||||||
|
row_resize,
|
||||||
|
n_resize,
|
||||||
|
e_resize,
|
||||||
|
s_resize,
|
||||||
|
w_resize,
|
||||||
|
ne_resize,
|
||||||
|
nw_resize,
|
||||||
|
se_resize,
|
||||||
|
sw_resize,
|
||||||
|
ew_resize,
|
||||||
|
ns_resize,
|
||||||
|
nesw_resize,
|
||||||
|
nwse_resize,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t seq;
|
||||||
|
bool visible;
|
||||||
|
CursorShape shape;
|
||||||
|
} CursorState;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char host_name[64];
|
char host_name[64];
|
||||||
size_t host_name_size;
|
size_t host_name_size;
|
||||||
@@ -96,6 +138,7 @@ struct RemoteAction {
|
|||||||
Mouse m;
|
Mouse m;
|
||||||
Key k;
|
Key k;
|
||||||
KeyboardState ks;
|
KeyboardState ks;
|
||||||
|
CursorState cs;
|
||||||
HostInfo i;
|
HostInfo i;
|
||||||
bool a;
|
bool a;
|
||||||
int d;
|
int d;
|
||||||
@@ -141,6 +184,11 @@ struct RemoteAction {
|
|||||||
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
|
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case ControlType::cursor_state:
|
||||||
|
j["cursor_state"] = {{"seq", a.cs.seq},
|
||||||
|
{"visible", a.cs.visible},
|
||||||
|
{"shape", static_cast<int>(a.cs.shape)}};
|
||||||
|
break;
|
||||||
case ControlType::audio_capture:
|
case ControlType::audio_capture:
|
||||||
j["audio_capture"] = a.a;
|
j["audio_capture"] = a.a;
|
||||||
break;
|
break;
|
||||||
@@ -219,6 +267,18 @@ struct RemoteAction {
|
|||||||
out.ks.pressed_count = count;
|
out.ks.pressed_count = count;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case ControlType::cursor_state: {
|
||||||
|
const auto& cursor_state_json = j.at("cursor_state");
|
||||||
|
const int shape = cursor_state_json.at("shape").get<int>();
|
||||||
|
if (shape < static_cast<int>(CursorShape::default_cursor) ||
|
||||||
|
shape > static_cast<int>(CursorShape::nwse_resize)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.cs.seq = cursor_state_json.at("seq").get<uint32_t>();
|
||||||
|
out.cs.visible = cursor_state_json.at("visible").get<bool>();
|
||||||
|
out.cs.shape = static_cast<CursorShape>(shape);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case ControlType::audio_capture:
|
case ControlType::audio_capture:
|
||||||
out.a = j.at("audio_capture").get<bool>();
|
out.a = j.at("audio_capture").get<bool>();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1796,6 +1796,7 @@ void GuiApplication::Tick() {
|
|||||||
SyncStreamKeyboardFocus();
|
SyncStreamKeyboardFocus();
|
||||||
#endif
|
#endif
|
||||||
devices_.UpdateInteractions();
|
devices_.UpdateInteractions();
|
||||||
|
ShareLocalCursorState();
|
||||||
|
|
||||||
UpdateLocalization();
|
UpdateLocalization();
|
||||||
SyncMainWindow();
|
SyncMainWindow();
|
||||||
@@ -1805,6 +1806,58 @@ void GuiApplication::Tick() {
|
|||||||
SyncServerWindow();
|
SyncServerWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GuiApplication::ShareLocalCursorState() {
|
||||||
|
constexpr auto kCursorStateHeartbeatInterval = 500ms;
|
||||||
|
|
||||||
|
bool has_connected_controller = false;
|
||||||
|
{
|
||||||
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
|
has_connected_controller =
|
||||||
|
std::any_of(connection_status_.begin(), connection_status_.end(),
|
||||||
|
[](const auto& entry) {
|
||||||
|
return entry.second == ConnectionStatus::Connected;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!is_server_mode_ || !peer_ || !has_connected_controller) {
|
||||||
|
has_shared_cursor_state_ = false;
|
||||||
|
last_cursor_state_share_time_ = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CursorState sampled{};
|
||||||
|
if (!cursor_state_provider_.Sample(&sampled)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
const bool changed =
|
||||||
|
!has_shared_cursor_state_ ||
|
||||||
|
sampled.visible != last_shared_cursor_state_.visible ||
|
||||||
|
sampled.shape != last_shared_cursor_state_.shape;
|
||||||
|
const bool heartbeat_due =
|
||||||
|
last_cursor_state_share_time_.time_since_epoch().count() == 0 ||
|
||||||
|
now - last_cursor_state_share_time_ >= kCursorStateHeartbeatInterval;
|
||||||
|
if (!changed && !heartbeat_due) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sampled.seq = ++cursor_state_sequence_;
|
||||||
|
RemoteAction action{};
|
||||||
|
action.type = ControlType::cursor_state;
|
||||||
|
action.cs = sampled;
|
||||||
|
const std::string message = action.to_json();
|
||||||
|
const int result = SendDataFrame(peer_, message.c_str(), message.size(),
|
||||||
|
mouse_label_.c_str());
|
||||||
|
if (result != 0) {
|
||||||
|
LOG_WARN("Send cursor state failed, ret={}", result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_shared_cursor_state_ = sampled;
|
||||||
|
has_shared_cursor_state_ = true;
|
||||||
|
last_cursor_state_share_time_ = now;
|
||||||
|
}
|
||||||
|
|
||||||
void GuiApplication::HandlePasswordChangeResult() {
|
void GuiApplication::HandlePasswordChangeResult() {
|
||||||
bool succeeded = false;
|
bool succeeded = false;
|
||||||
bool uncertain = false;
|
bool uncertain = false;
|
||||||
@@ -2364,6 +2417,23 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
? localization::receiving_screen[localization_language_index_]
|
? localization::receiving_screen[localization_language_index_]
|
||||||
: std::string{}));
|
: std::string{}));
|
||||||
(*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
|
(*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
|
||||||
|
int remote_cursor_shape =
|
||||||
|
static_cast<int>(CursorShape::default_cursor);
|
||||||
|
bool remote_cursor_active = false;
|
||||||
|
{
|
||||||
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||||
|
remote_cursor_active =
|
||||||
|
status == ConnectionStatus::Connected && props->control_mouse_ &&
|
||||||
|
props->remote_cursor_state_received_;
|
||||||
|
if (remote_cursor_active) {
|
||||||
|
remote_cursor_shape = static_cast<int>(
|
||||||
|
props->remote_cursor_state_.visible
|
||||||
|
? props->remote_cursor_state_.shape
|
||||||
|
: CursorShape::none);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*ui_->stream)->set_remote_cursor_active(remote_cursor_active);
|
||||||
|
(*ui_->stream)->set_remote_cursor_shape(remote_cursor_shape);
|
||||||
(*ui_->stream)->set_audio_enabled(props->audio_capture_button_pressed_);
|
(*ui_->stream)->set_audio_enabled(props->audio_capture_button_pressed_);
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
fullscreen_button_pressed_ = IsStreamWindowFullscreen();
|
fullscreen_button_pressed_ = IsStreamWindowFullscreen();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "runtime/cursor_state_provider.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -32,6 +33,7 @@ private:
|
|||||||
void BindStreamCallbacks();
|
void BindStreamCallbacks();
|
||||||
void BindServerCallbacks();
|
void BindServerCallbacks();
|
||||||
void Tick();
|
void Tick();
|
||||||
|
void ShareLocalCursorState();
|
||||||
void HandlePasswordChangeResult();
|
void HandlePasswordChangeResult();
|
||||||
void HandleCredentialRecovery();
|
void HandleCredentialRecovery();
|
||||||
void SyncMainWindow();
|
void SyncMainWindow();
|
||||||
@@ -70,6 +72,11 @@ private:
|
|||||||
bool OpenUrl(const std::string &url);
|
bool OpenUrl(const std::string &url);
|
||||||
|
|
||||||
std::unique_ptr<SlintUi> ui_;
|
std::unique_ptr<SlintUi> ui_;
|
||||||
|
CursorStateProvider cursor_state_provider_;
|
||||||
|
CursorState last_shared_cursor_state_{};
|
||||||
|
bool has_shared_cursor_state_ = false;
|
||||||
|
uint32_t cursor_state_sequence_ = 0;
|
||||||
|
std::chrono::steady_clock::time_point last_cursor_state_share_time_{};
|
||||||
std::chrono::steady_clock::time_point next_video_frame_time_{};
|
std::chrono::steady_clock::time_point next_video_frame_time_{};
|
||||||
#if defined(__linux__) && !defined(__APPLE__)
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
bool use_xwayland_gui_ = false;
|
bool use_xwayland_gui_ = false;
|
||||||
|
|||||||
@@ -338,6 +338,11 @@ void GuiRuntime::ResetRemoteSessionResources(
|
|||||||
props->render_rect_dirty_ = true;
|
props->render_rect_dirty_ = true;
|
||||||
props->stream_cleanup_pending_ = false;
|
props->stream_cleanup_pending_ = false;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
|
||||||
|
props->remote_cursor_state_ = {};
|
||||||
|
props->remote_cursor_state_received_ = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
#include "runtime/cursor_state_provider.h"
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) {
|
||||||
|
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
CursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
|
||||||
|
if (IsSystemCursor(cursor, IDC_HELP)) return CursorShape::help;
|
||||||
|
if (IsSystemCursor(cursor, IDC_HAND)) return CursorShape::pointer;
|
||||||
|
if (IsSystemCursor(cursor, IDC_APPSTARTING)) return CursorShape::progress;
|
||||||
|
if (IsSystemCursor(cursor, IDC_WAIT)) return CursorShape::wait;
|
||||||
|
if (IsSystemCursor(cursor, IDC_CROSS)) return CursorShape::crosshair;
|
||||||
|
if (IsSystemCursor(cursor, IDC_IBEAM)) return CursorShape::text;
|
||||||
|
if (IsSystemCursor(cursor, IDC_NO)) return CursorShape::not_allowed;
|
||||||
|
if (IsSystemCursor(cursor, IDC_SIZEALL)) return CursorShape::move;
|
||||||
|
if (IsSystemCursor(cursor, IDC_SIZEWE)) return CursorShape::ew_resize;
|
||||||
|
if (IsSystemCursor(cursor, IDC_SIZENS)) return CursorShape::ns_resize;
|
||||||
|
if (IsSystemCursor(cursor, IDC_SIZENESW)) return CursorShape::nesw_resize;
|
||||||
|
if (IsSystemCursor(cursor, IDC_SIZENWSE)) return CursorShape::nwse_resize;
|
||||||
|
if (IsSystemCursor(cursor, IDC_UPARROW)) return CursorShape::n_resize;
|
||||||
|
return CursorShape::default_cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct CursorStateProvider::Impl {};
|
||||||
|
|
||||||
|
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
||||||
|
CursorStateProvider::~CursorStateProvider() = default;
|
||||||
|
|
||||||
|
bool CursorStateProvider::Sample(CursorState* state) {
|
||||||
|
if (!state) return false;
|
||||||
|
|
||||||
|
CURSORINFO info{};
|
||||||
|
info.cbSize = sizeof(info);
|
||||||
|
if (!GetCursorInfo(&info)) return false;
|
||||||
|
|
||||||
|
state->seq = 0;
|
||||||
|
state->visible = (info.flags & CURSOR_SHOWING) != 0;
|
||||||
|
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
|
||||||
|
: CursorShape::none;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#elif defined(__linux__) && !defined(__APPLE__)
|
||||||
|
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/extensions/Xfixes.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string Lowercase(std::string value) {
|
||||||
|
std::transform(value.begin(), value.end(), value.begin(),
|
||||||
|
[](unsigned char ch) { return std::tolower(ch); });
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Contains(const std::string& value, const char* token) {
|
||||||
|
return value.find(token) != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
CursorShape ShapeFromXCursorName(const std::string& raw_name) {
|
||||||
|
const std::string name = Lowercase(raw_name);
|
||||||
|
if (Contains(name, "left_ptr_watch") || Contains(name, "progress"))
|
||||||
|
return CursorShape::progress;
|
||||||
|
if (Contains(name, "watch") || Contains(name, "wait"))
|
||||||
|
return CursorShape::wait;
|
||||||
|
if (Contains(name, "question") || Contains(name, "help"))
|
||||||
|
return CursorShape::help;
|
||||||
|
if (Contains(name, "xterm") || Contains(name, "vertical-text") ||
|
||||||
|
name == "text")
|
||||||
|
return CursorShape::text;
|
||||||
|
if (Contains(name, "crosshair") || name == "cross" || name == "tcross")
|
||||||
|
return CursorShape::crosshair;
|
||||||
|
if (Contains(name, "closedhand") || Contains(name, "grabbing"))
|
||||||
|
return CursorShape::grabbing;
|
||||||
|
if (Contains(name, "openhand") || Contains(name, "grab"))
|
||||||
|
return CursorShape::grab;
|
||||||
|
if (Contains(name, "dnd-link") || name == "alias")
|
||||||
|
return CursorShape::alias;
|
||||||
|
if (Contains(name, "hand") || Contains(name, "pointer") ||
|
||||||
|
Contains(name, "link"))
|
||||||
|
return CursorShape::pointer;
|
||||||
|
if (Contains(name, "dnd-copy") || name == "copy")
|
||||||
|
return CursorShape::copy;
|
||||||
|
if (Contains(name, "no-drop")) return CursorShape::no_drop;
|
||||||
|
if (Contains(name, "not-allowed") || Contains(name, "crossed_circle"))
|
||||||
|
return CursorShape::not_allowed;
|
||||||
|
if (name == "fleur" || Contains(name, "size_all") || name == "move")
|
||||||
|
return CursorShape::move;
|
||||||
|
if (Contains(name, "top_left_corner") ||
|
||||||
|
Contains(name, "bottom_right_corner") ||
|
||||||
|
Contains(name, "nwse-resize") || Contains(name, "size_fdiag"))
|
||||||
|
return CursorShape::nwse_resize;
|
||||||
|
if (Contains(name, "top_right_corner") ||
|
||||||
|
Contains(name, "bottom_left_corner") ||
|
||||||
|
Contains(name, "nesw-resize") || Contains(name, "size_bdiag"))
|
||||||
|
return CursorShape::nesw_resize;
|
||||||
|
if (Contains(name, "sb_h_double_arrow") || Contains(name, "ew-resize") ||
|
||||||
|
Contains(name, "size_hor"))
|
||||||
|
return CursorShape::ew_resize;
|
||||||
|
if (Contains(name, "sb_v_double_arrow") || Contains(name, "ns-resize") ||
|
||||||
|
Contains(name, "size_ver"))
|
||||||
|
return CursorShape::ns_resize;
|
||||||
|
if (Contains(name, "col-resize")) return CursorShape::col_resize;
|
||||||
|
if (Contains(name, "row-resize")) return CursorShape::row_resize;
|
||||||
|
if (Contains(name, "ne-resize")) return CursorShape::ne_resize;
|
||||||
|
if (Contains(name, "nw-resize")) return CursorShape::nw_resize;
|
||||||
|
if (Contains(name, "se-resize")) return CursorShape::se_resize;
|
||||||
|
if (Contains(name, "sw-resize")) return CursorShape::sw_resize;
|
||||||
|
if (Contains(name, "top_side") || name == "n-resize")
|
||||||
|
return CursorShape::n_resize;
|
||||||
|
if (Contains(name, "right_side") || name == "e-resize")
|
||||||
|
return CursorShape::e_resize;
|
||||||
|
if (Contains(name, "bottom_side") || name == "s-resize")
|
||||||
|
return CursorShape::s_resize;
|
||||||
|
if (Contains(name, "left_side") || name == "w-resize")
|
||||||
|
return CursorShape::w_resize;
|
||||||
|
return CursorShape::default_cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CursorHasVisiblePixel(const XFixesCursorImage& image) {
|
||||||
|
const size_t pixel_count =
|
||||||
|
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
|
||||||
|
for (size_t index = 0; index < pixel_count; ++index) {
|
||||||
|
if (((image.pixels[index] >> 24U) & 0xffU) != 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct CursorStateProvider::Impl {
|
||||||
|
Display* display = XOpenDisplay(nullptr);
|
||||||
|
|
||||||
|
~Impl() {
|
||||||
|
if (display) XCloseDisplay(display);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
||||||
|
CursorStateProvider::~CursorStateProvider() = default;
|
||||||
|
|
||||||
|
bool CursorStateProvider::Sample(CursorState* state) {
|
||||||
|
if (!state || !impl_ || !impl_->display) return false;
|
||||||
|
|
||||||
|
XFixesCursorImage* image = XFixesGetCursorImageAndName(impl_->display);
|
||||||
|
if (!image) return false;
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
if (image->atom != None) {
|
||||||
|
char* atom_name = XGetAtomName(impl_->display, image->atom);
|
||||||
|
if (atom_name) {
|
||||||
|
name = atom_name;
|
||||||
|
XFree(atom_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state->seq = 0;
|
||||||
|
state->visible = CursorHasVisiblePixel(*image);
|
||||||
|
state->shape = state->visible ? ShapeFromXCursorName(name)
|
||||||
|
: CursorShape::none;
|
||||||
|
XFree(image);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#ifndef CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
|
||||||
|
#define CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "device_controller.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// Samples the cursor that is currently displayed by the controlled desktop
|
||||||
|
// and converts platform-specific cursor handles to protocol cursor shapes.
|
||||||
|
class CursorStateProvider {
|
||||||
|
public:
|
||||||
|
CursorStateProvider();
|
||||||
|
~CursorStateProvider();
|
||||||
|
|
||||||
|
CursorStateProvider(const CursorStateProvider&) = delete;
|
||||||
|
CursorStateProvider& operator=(const CursorStateProvider&) = delete;
|
||||||
|
|
||||||
|
bool Sample(CursorState* state);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif // CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#include "runtime/cursor_state_provider.h"
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
|
||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
#import <CoreGraphics/CGRemoteOperation.h>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool SameCursor(NSCursor* left, NSCursor* right) {
|
||||||
|
if (left == right || [left isEqual:right]) return true;
|
||||||
|
return left && right && NSEqualPoints(left.hotSpot, right.hotSpot) &&
|
||||||
|
[left.image isEqual:right.image];
|
||||||
|
}
|
||||||
|
|
||||||
|
CursorShape ShapeFromMacCursor(NSCursor* cursor) {
|
||||||
|
if (SameCursor(cursor, NSCursor.pointingHandCursor))
|
||||||
|
return CursorShape::pointer;
|
||||||
|
if (SameCursor(cursor, NSCursor.crosshairCursor))
|
||||||
|
return CursorShape::crosshair;
|
||||||
|
if (SameCursor(cursor, NSCursor.IBeamCursor) ||
|
||||||
|
SameCursor(cursor, NSCursor.IBeamCursorForVerticalLayout))
|
||||||
|
return CursorShape::text;
|
||||||
|
if (SameCursor(cursor, NSCursor.operationNotAllowedCursor))
|
||||||
|
return CursorShape::not_allowed;
|
||||||
|
if (SameCursor(cursor, NSCursor.dragLinkCursor)) return CursorShape::alias;
|
||||||
|
if (SameCursor(cursor, NSCursor.dragCopyCursor)) return CursorShape::copy;
|
||||||
|
if (SameCursor(cursor, NSCursor.openHandCursor)) return CursorShape::grab;
|
||||||
|
if (SameCursor(cursor, NSCursor.closedHandCursor))
|
||||||
|
return CursorShape::grabbing;
|
||||||
|
if (SameCursor(cursor, NSCursor.resizeLeftRightCursor))
|
||||||
|
return CursorShape::ew_resize;
|
||||||
|
if (SameCursor(cursor, NSCursor.resizeUpDownCursor))
|
||||||
|
return CursorShape::ns_resize;
|
||||||
|
if (SameCursor(cursor, NSCursor.resizeUpCursor))
|
||||||
|
return CursorShape::n_resize;
|
||||||
|
if (SameCursor(cursor, NSCursor.resizeRightCursor))
|
||||||
|
return CursorShape::e_resize;
|
||||||
|
if (SameCursor(cursor, NSCursor.resizeDownCursor))
|
||||||
|
return CursorShape::s_resize;
|
||||||
|
if (SameCursor(cursor, NSCursor.resizeLeftCursor))
|
||||||
|
return CursorShape::w_resize;
|
||||||
|
return CursorShape::default_cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct CursorStateProvider::Impl {};
|
||||||
|
|
||||||
|
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
||||||
|
CursorStateProvider::~CursorStateProvider() = default;
|
||||||
|
|
||||||
|
bool CursorStateProvider::Sample(CursorState* state) {
|
||||||
|
if (!state) return false;
|
||||||
|
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
|
NSCursor* cursor = NSCursor.currentSystemCursor;
|
||||||
|
const bool visible = CGCursorIsVisible();
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
|
||||||
|
state->seq = 0;
|
||||||
|
state->visible = visible && cursor != nil;
|
||||||
|
state->shape = state->visible ? ShapeFromMacCursor(cursor)
|
||||||
|
: CursorShape::none;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -164,6 +164,31 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remote_action.type == ControlType::cursor_state) {
|
||||||
|
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
||||||
|
{
|
||||||
|
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||||
|
auto props_it = runtime->remote_sessions_.find(remote_id);
|
||||||
|
if (props_it != runtime->remote_sessions_.end()) {
|
||||||
|
props = props_it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!props) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||||
|
const uint32_t previous_seq = props->remote_cursor_state_.seq;
|
||||||
|
const bool is_newer =
|
||||||
|
!props->remote_cursor_state_received_ ||
|
||||||
|
static_cast<int32_t>(remote_action.cs.seq - previous_seq) > 0;
|
||||||
|
if (is_newer) {
|
||||||
|
props->remote_cursor_state_ = remote_action.cs;
|
||||||
|
props->remote_cursor_state_received_ = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (remote_action.type == ControlType::host_infomation) {
|
if (remote_action.type == ControlType::host_infomation) {
|
||||||
bool is_client_mode = false;
|
bool is_client_mode = false;
|
||||||
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
||||||
|
|||||||
@@ -249,6 +249,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus::Connected: {
|
case ConnectionStatus::Connected: {
|
||||||
runtime->ResetRemoteServiceStatus(*props);
|
runtime->ResetRemoteServiceStatus(*props);
|
||||||
|
{
|
||||||
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||||
|
props->remote_cursor_state_ = {};
|
||||||
|
props->remote_cursor_state_received_ = false;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
RemoteAction remote_action;
|
RemoteAction remote_action;
|
||||||
remote_action.i.display_num =
|
remote_action.i.display_num =
|
||||||
@@ -313,6 +318,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
props->connection_established_ = false;
|
props->connection_established_ = false;
|
||||||
props->enable_mouse_control_ = false;
|
props->enable_mouse_control_ = false;
|
||||||
runtime->ResetRemoteServiceStatus(*props);
|
runtime->ResetRemoteServiceStatus(*props);
|
||||||
|
{
|
||||||
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||||
|
props->remote_cursor_state_ = {};
|
||||||
|
props->remote_cursor_state_received_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
|
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
|
||||||
int native_snapshot_width = 0;
|
int native_snapshot_width = 0;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "device_controller.h"
|
||||||
#include "display_info.h"
|
#include "display_info.h"
|
||||||
#include "minirtc.h"
|
#include "minirtc.h"
|
||||||
|
|
||||||
@@ -148,6 +149,12 @@ struct RemoteSession {
|
|||||||
bool remote_service_available_ = false;
|
bool remote_service_available_ = false;
|
||||||
std::string remote_interactive_stage_;
|
std::string remote_interactive_stage_;
|
||||||
std::vector<DisplayInfo> display_info_list_;
|
std::vector<DisplayInfo> display_info_list_;
|
||||||
|
// Cursor snapshots arrive on the transport callback thread and are applied
|
||||||
|
// by the Slint UI thread. Keep the snapshot atomic as a unit so visibility
|
||||||
|
// and shape cannot briefly come from different protocol messages.
|
||||||
|
std::mutex remote_cursor_state_mutex_;
|
||||||
|
CursorState remote_cursor_state_{};
|
||||||
|
bool remote_cursor_state_received_ = false;
|
||||||
// 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;
|
||||||
|
|||||||
@@ -133,6 +133,9 @@ export component StreamWindow inherits Window {
|
|||||||
in property <[string]> displays;
|
in property <[string]> displays;
|
||||||
in-out property <int> selected-display: 0;
|
in-out property <int> selected-display: 0;
|
||||||
in property <bool> mouse-control-enabled: true;
|
in property <bool> mouse-control-enabled: true;
|
||||||
|
// Numeric values mirror crossdesk::CursorShape and Slint's MouseCursor.
|
||||||
|
in property <bool> remote-cursor-active: false;
|
||||||
|
in property <int> remote-cursor-shape: 0;
|
||||||
in property <bool> audio-enabled: true;
|
in property <bool> audio-enabled: true;
|
||||||
in property <bool> srtp-enabled: false;
|
in property <bool> srtp-enabled: false;
|
||||||
in property <bool> fullscreen-enabled: false;
|
in property <bool> fullscreen-enabled: false;
|
||||||
@@ -551,6 +554,36 @@ export component StreamWindow inherits Window {
|
|||||||
accept
|
accept
|
||||||
}
|
}
|
||||||
pointer := TouchArea {
|
pointer := TouchArea {
|
||||||
|
mouse-cursor: !root.remote-cursor-active ? MouseCursor.default
|
||||||
|
: root.remote-cursor-shape == 1 ? MouseCursor.none
|
||||||
|
: root.remote-cursor-shape == 2 ? MouseCursor.help
|
||||||
|
: root.remote-cursor-shape == 3 ? MouseCursor.pointer
|
||||||
|
: root.remote-cursor-shape == 4 ? MouseCursor.progress
|
||||||
|
: root.remote-cursor-shape == 5 ? MouseCursor.wait
|
||||||
|
: root.remote-cursor-shape == 6 ? MouseCursor.crosshair
|
||||||
|
: root.remote-cursor-shape == 7 ? MouseCursor.text
|
||||||
|
: root.remote-cursor-shape == 8 ? MouseCursor.alias
|
||||||
|
: root.remote-cursor-shape == 9 ? MouseCursor.copy
|
||||||
|
: root.remote-cursor-shape == 10 ? MouseCursor.move
|
||||||
|
: root.remote-cursor-shape == 11 ? MouseCursor.no-drop
|
||||||
|
: root.remote-cursor-shape == 12 ? MouseCursor.not-allowed
|
||||||
|
: root.remote-cursor-shape == 13 ? MouseCursor.grab
|
||||||
|
: root.remote-cursor-shape == 14 ? MouseCursor.grabbing
|
||||||
|
: root.remote-cursor-shape == 15 ? MouseCursor.col-resize
|
||||||
|
: root.remote-cursor-shape == 16 ? MouseCursor.row-resize
|
||||||
|
: root.remote-cursor-shape == 17 ? MouseCursor.n-resize
|
||||||
|
: root.remote-cursor-shape == 18 ? MouseCursor.e-resize
|
||||||
|
: root.remote-cursor-shape == 19 ? MouseCursor.s-resize
|
||||||
|
: root.remote-cursor-shape == 20 ? MouseCursor.w-resize
|
||||||
|
: root.remote-cursor-shape == 21 ? MouseCursor.ne-resize
|
||||||
|
: root.remote-cursor-shape == 22 ? MouseCursor.nw-resize
|
||||||
|
: root.remote-cursor-shape == 23 ? MouseCursor.se-resize
|
||||||
|
: root.remote-cursor-shape == 24 ? MouseCursor.sw-resize
|
||||||
|
: root.remote-cursor-shape == 25 ? MouseCursor.ew-resize
|
||||||
|
: root.remote-cursor-shape == 26 ? MouseCursor.ns-resize
|
||||||
|
: root.remote-cursor-shape == 27 ? MouseCursor.nesw-resize
|
||||||
|
: root.remote-cursor-shape == 28 ? MouseCursor.nwse-resize
|
||||||
|
: MouseCursor.default;
|
||||||
pointer-event(event) => {
|
pointer-event(event) => {
|
||||||
let over-control = self.mouse-x >= control.x
|
let over-control = self.mouse-x >= control.x
|
||||||
&& self.mouse-x <= control.x + control.width
|
&& self.mouse-x <= control.x + control.width
|
||||||
|
|||||||
+2
-1
@@ -84,7 +84,8 @@ function setup_platform_settings()
|
|||||||
add_ldflags("-Wl,-ld_classic")
|
add_ldflags("-Wl,-ld_classic")
|
||||||
end
|
end
|
||||||
add_cxflags("-Wno-unused-variable")
|
add_cxflags("-Wno-unused-variable")
|
||||||
add_frameworks("Cocoa", "Metal", "QuartzCore", "IOSurface",
|
add_frameworks("Cocoa", "CoreGraphics", "Metal", "QuartzCore",
|
||||||
|
"IOSurface",
|
||||||
"ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo",
|
"ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo",
|
||||||
"CoreAudio", "AudioToolbox")
|
"CoreAudio", "AudioToolbox")
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user