feat: show my cursor (#12745)

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2025-08-28 15:20:01 +08:00
committed by GitHub
parent ac70f380a6
commit d0e9c6dc57
62 changed files with 1276 additions and 27 deletions

235
Cargo.lock generated
View File

@@ -224,12 +224,24 @@ dependencies = [
"x11rb 0.13.1", "x11rb 0.13.1",
] ]
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "as-raw-xcb-connection"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.5.1" version = "0.5.1"
@@ -751,9 +763,23 @@ dependencies = [
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.21.0" version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29"
dependencies = [
"proc-macro2 1.0.93",
"quote 1.0.36",
"syn 2.0.98",
]
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@@ -1380,6 +1406,15 @@ dependencies = [
"objc", "objc",
] ]
[[package]]
name = "core_maths"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
dependencies = [
"libm",
]
[[package]] [[package]]
name = "coreaudio-rs" name = "coreaudio-rs"
version = "0.11.3" version = "0.11.3"
@@ -1515,6 +1550,12 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "ctor-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b"
[[package]] [[package]]
name = "ctrlc" name = "ctrlc"
version = "3.4.4" version = "3.4.4"
@@ -1943,6 +1984,45 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
[[package]]
name = "drm"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1"
dependencies = [
"bitflags 2.9.1",
"bytemuck",
"drm-ffi",
"drm-fourcc",
"rustix 0.38.34",
]
[[package]]
name = "drm-ffi"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53"
dependencies = [
"drm-sys",
"rustix 0.38.34",
]
[[package]]
name = "drm-fourcc"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4"
[[package]]
name = "drm-sys"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986"
dependencies = [
"libc",
"linux-raw-sys 0.6.5",
]
[[package]] [[package]]
name = "dtoa" name = "dtoa"
version = "0.4.8" version = "0.4.8"
@@ -2340,6 +2420,29 @@ dependencies = [
"libm", "libm",
] ]
[[package]]
name = "fontconfig-parser"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
dependencies = [
"roxmltree",
]
[[package]]
name = "fontdb"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
dependencies = [
"fontconfig-parser",
"log",
"memmap2",
"slotmap",
"tinyvec",
"ttf-parser",
]
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@@ -3929,6 +4032,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "linux-raw-sys"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.12"
@@ -4017,6 +4126,15 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmap2"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.6.5" version = "0.6.5"
@@ -4730,6 +4848,7 @@ checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"block2 0.5.1", "block2 0.5.1",
"dispatch",
"libc", "libc",
"objc2 0.5.2", "objc2 0.5.2",
] ]
@@ -6007,6 +6126,12 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "roxmltree"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]] [[package]]
name = "rpassword" name = "rpassword"
version = "2.1.0" version = "2.1.0"
@@ -6117,6 +6242,7 @@ dependencies = [
"arboard", "arboard",
"async-process", "async-process",
"async-trait", "async-trait",
"bytemuck",
"bytes", "bytes",
"cc", "cc",
"cfg-if 1.0.0", "cfg-if 1.0.0",
@@ -6142,6 +6268,7 @@ dependencies = [
"evdev", "evdev",
"flutter_rust_bridge", "flutter_rust_bridge",
"fon", "fon",
"fontdb",
"fruitbasket", "fruitbasket",
"gtk", "gtk",
"hbb_common", "hbb_common",
@@ -6189,6 +6316,7 @@ dependencies = [
"sha2", "sha2",
"shared_memory", "shared_memory",
"shutdown_hooks", "shutdown_hooks",
"softbuffer",
"stunclient", "stunclient",
"sys-locale", "sys-locale",
"system_shutdown", "system_shutdown",
@@ -6196,8 +6324,10 @@ dependencies = [
"tauri-winrt-notification", "tauri-winrt-notification",
"terminfo", "terminfo",
"termios 0.3.3", "termios 0.3.3",
"tiny-skia",
"totp-rs", "totp-rs",
"tray-icon", "tray-icon",
"ttf-parser",
"url", "url",
"users 0.11.0", "users 0.11.0",
"uuid", "uuid",
@@ -6738,6 +6868,15 @@ dependencies = [
"autocfg 1.3.0", "autocfg 1.3.0",
] ]
[[package]]
name = "slotmap"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.13.2"
@@ -6787,6 +6926,39 @@ dependencies = [
"serde 1.0.203", "serde 1.0.203",
] ]
[[package]]
name = "softbuffer"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd"
dependencies = [
"as-raw-xcb-connection",
"bytemuck",
"cfg_aliases 0.2.1",
"core-graphics 0.23.2",
"drm",
"fastrand 2.1.0",
"foreign-types 0.5.0",
"js-sys",
"log",
"memmap2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle 0.6.2",
"redox_syscall 0.5.2",
"rustix 0.38.34",
"tiny-xlib",
"wasm-bindgen",
"wayland-backend",
"wayland-client",
"wayland-sys",
"web-sys",
"windows-sys 0.52.0",
"x11rb 0.13.1",
]
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@@ -6808,6 +6980,12 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
[[package]]
name = "strict-num"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"
@@ -7290,6 +7468,45 @@ dependencies = [
"time-core", "time-core",
] ]
[[package]]
name = "tiny-skia"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
dependencies = [
"arrayref",
"arrayvec",
"bytemuck",
"cfg-if 1.0.0",
"log",
"png",
"tiny-skia-path",
]
[[package]]
name = "tiny-skia-path"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
dependencies = [
"arrayref",
"bytemuck",
"strict-num",
]
[[package]]
name = "tiny-xlib"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e"
dependencies = [
"as-raw-xcb-connection",
"ctor-lite",
"libloading 0.8.4",
"pkg-config",
"tracing",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.1" version = "1.6.1"
@@ -7665,6 +7882,15 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
dependencies = [
"core_maths",
]
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.26.2" version = "0.26.2"
@@ -8147,6 +8373,7 @@ checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148"
dependencies = [ dependencies = [
"dlib", "dlib",
"log", "log",
"once_cell",
"pkg-config", "pkg-config",
] ]
@@ -9085,7 +9312,11 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
dependencies = [ dependencies = [
"as-raw-xcb-connection",
"gethostname 0.4.3", "gethostname 0.4.3",
"libc",
"libloading 0.8.4",
"once_cell",
"rustix 0.38.34", "rustix 0.38.34",
"x11rb-protocol 0.13.1", "x11rb-protocol 0.13.1",
] ]

View File

@@ -134,6 +134,11 @@ impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system
shared_memory = "0.12" shared_memory = "0.12"
tauri-winrt-notification = "0.1" tauri-winrt-notification = "0.1"
runas = "1.2" runas = "1.2"
tiny-skia = "0.11"
softbuffer = "0.4"
fontdb = "0.23"
bytemuck = "1.23"
ttf-parser = "0.25"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2" objc = "0.2"

View File

@@ -172,6 +172,7 @@ const kHideUsernameOnCard = "hide-username-on-card";
const String kOptionHideHelpCards = "hide-help-cards"; const String kOptionHideHelpCards = "hide-help-cards";
const String kOptionToggleViewOnly = "view-only"; const String kOptionToggleViewOnly = "view-only";
const String kOptionToggleShowMyCursor = "show-my-cursor";
const String kOptionDisableFloatingWindow = "disable-floating-window"; const String kOptionDisableFloatingWindow = "disable-floating-window";

View File

@@ -1593,6 +1593,7 @@ class _KeyboardMenu extends StatelessWidget {
inputSource(), inputSource(),
Divider(), Divider(),
viewMode(), viewMode(),
if (pi.platform == kPeerPlatformWindows) showMyCursor(),
Divider(), Divider(),
...toolbarToggles(), ...toolbarToggles(),
...mouseSpeed(), ...mouseSpeed(),
@@ -1749,12 +1750,36 @@ class _KeyboardMenu extends StatelessWidget {
final viewOnly = await bind.sessionGetToggleOption( final viewOnly = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly); sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, viewOnly ?? value); ffiModel.setViewOnly(id, viewOnly ?? value);
final showMyCursor = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId, arg: kOptionToggleShowMyCursor);
ffiModel.setShowMyCursor(showMyCursor ?? value);
} }
: null, : null,
ffi: ffi, ffi: ffi,
child: Text(translate('View Mode'))); child: Text(translate('View Mode')));
} }
showMyCursor() {
final ffiModel = ffi.ffiModel;
return CkbMenuButton(
value: ffiModel.showMyCursor,
onChanged: ffiModel.viewOnly
? (value) async {
if (value == null) return;
await bind.sessionToggleOption(
sessionId: ffi.sessionId,
value: kOptionToggleShowMyCursor);
final showMyCursor = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId,
arg: kOptionToggleShowMyCursor);
ffiModel.setShowMyCursor(showMyCursor ?? value);
}
: null,
ffi: ffi,
child: Text(translate('Show my cursor')))
.paddingOnly(left: 26.0);
}
mobileActions() { mobileActions() {
if (pi.platform != kPeerPlatformAndroid) return []; if (pi.platform != kPeerPlatformAndroid) return [];
final enabled = versionCmp(pi.version, '1.2.7') >= 0; final enabled = versionCmp(pi.version, '1.2.7') >= 0;

View File

@@ -371,6 +371,7 @@ class InputModel {
String get id => parent.target?.id ?? ''; String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform; String? get peerPlatform => parent.target?.ffiModel.pi.platform;
bool get isViewOnly => parent.target!.ffiModel.viewOnly; bool get isViewOnly => parent.target!.ffiModel.viewOnly;
bool get showMyCursor => parent.target!.ffiModel.showMyCursor;
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio; double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera; bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
int get trackpadSpeed => _trackpadSpeed; int get trackpadSpeed => _trackpadSpeed;
@@ -876,7 +877,7 @@ class InputModel {
void onPointHoverImage(PointerHoverEvent e) { void onPointHoverImage(PointerHoverEvent e) {
_stopFling = true; _stopFling = true;
if (isViewOnly) return; if (isViewOnly && !showMyCursor) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (!isPhysicalMouse.value) { if (!isPhysicalMouse.value) {
isPhysicalMouse.value = true; isPhysicalMouse.value = true;
@@ -1037,7 +1038,7 @@ class InputModel {
if (isDesktop) _queryOtherWindowCoords = true; if (isDesktop) _queryOtherWindowCoords = true;
_remoteWindowCoords = []; _remoteWindowCoords = [];
_windowRect = null; _windowRect = null;
if (isViewOnly) return; if (isViewOnly && !showMyCursor) return;
if (isViewCamera) return; if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) { if (e.kind != ui.PointerDeviceKind.mouse) {
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
@@ -1051,7 +1052,7 @@ class InputModel {
void onPointUpImage(PointerUpEvent e) { void onPointUpImage(PointerUpEvent e) {
if (isDesktop) _queryOtherWindowCoords = false; if (isDesktop) _queryOtherWindowCoords = false;
if (isViewOnly) return; if (isViewOnly && !showMyCursor) return;
if (isViewCamera) return; if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
@@ -1060,7 +1061,7 @@ class InputModel {
} }
void onPointMoveImage(PointerMoveEvent e) { void onPointMoveImage(PointerMoveEvent e) {
if (isViewOnly) return; if (isViewOnly && !showMyCursor) return;
if (isViewCamera) return; if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_queryOtherWindowCoords) { if (_queryOtherWindowCoords) {

View File

@@ -116,6 +116,7 @@ class FfiModel with ChangeNotifier {
Timer? _timer; Timer? _timer;
var _reconnects = 1; var _reconnects = 1;
bool _viewOnly = false; bool _viewOnly = false;
bool _showMyCursor = false;
WeakReference<FFI> parent; WeakReference<FFI> parent;
late final SessionID sessionId; late final SessionID sessionId;
@@ -154,6 +155,7 @@ class FfiModel with ChangeNotifier {
bool get isPeerMobile => isPeerAndroid; bool get isPeerMobile => isPeerAndroid;
bool get viewOnly => _viewOnly; bool get viewOnly => _viewOnly;
bool get showMyCursor => _showMyCursor;
set inputBlocked(v) { set inputBlocked(v) {
_inputBlocked = v; _inputBlocked = v;
@@ -1144,6 +1146,8 @@ class FfiModel with ChangeNotifier {
peerId, peerId,
bind.sessionGetToggleOptionSync( bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionToggleViewOnly)); sessionId: sessionId, arg: kOptionToggleViewOnly));
setShowMyCursor(bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionToggleShowMyCursor));
} }
if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) { if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) {
final platformAdditions = evt['platform_additions']; final platformAdditions = evt['platform_additions'];
@@ -1494,6 +1498,13 @@ class FfiModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
} }
void setShowMyCursor(bool value) {
if (_showMyCursor != value) {
_showMyCursor = value;
notifyListeners();
}
}
} }
class ImageModel with ChangeNotifier { class ImageModel with ChangeNotifier {

View File

@@ -2132,7 +2132,19 @@ impl LoginConfigHandler {
option.show_remote_cursor = f(self.get_toggle_option("show-remote-cursor")); option.show_remote_cursor = f(self.get_toggle_option("show-remote-cursor"));
option.enable_file_transfer = f(self.config.enable_file_copy_paste.v); option.enable_file_transfer = f(self.config.enable_file_copy_paste.v);
option.lock_after_session_end = f(self.config.lock_after_session_end.v); option.lock_after_session_end = f(self.config.lock_after_session_end.v);
if config.show_my_cursor.v {
config.show_my_cursor.v = false;
option.show_my_cursor = BoolOption::No.into();
}
} }
} else if name == "show-my-cursor" {
config.show_my_cursor.v = !config.show_my_cursor.v;
option.show_my_cursor = if config.show_my_cursor.v {
BoolOption::Yes
} else {
BoolOption::No
}
.into();
} else { } else {
let is_set = self let is_set = self
.options .options
@@ -2225,6 +2237,9 @@ impl LoginConfigHandler {
if view_only || self.get_toggle_option("show-remote-cursor") { if view_only || self.get_toggle_option("show-remote-cursor") {
msg.show_remote_cursor = BoolOption::Yes.into(); msg.show_remote_cursor = BoolOption::Yes.into();
} }
if view_only && self.get_toggle_option("show-my-cursor") {
msg.show_my_cursor = BoolOption::Yes.into();
}
if self.get_toggle_option("follow-remote-cursor") { if self.get_toggle_option("follow-remote-cursor") {
msg.follow_remote_cursor = BoolOption::Yes.into(); msg.follow_remote_cursor = BoolOption::Yes.into();
} }
@@ -2309,6 +2324,8 @@ impl LoginConfigHandler {
self.config.allow_swap_key.v self.config.allow_swap_key.v
} else if name == "view-only" { } else if name == "view-only" {
self.config.view_only.v self.config.view_only.v
} else if name == "show-my-cursor" {
self.config.show_my_cursor.v
} else if name == "follow-remote-cursor" { } else if name == "follow-remote-cursor" {
self.config.follow_remote_cursor.v self.config.follow_remote_cursor.v
} else if name == "follow-remote-window" { } else if name == "follow-remote-window" {

View File

@@ -2040,6 +2040,22 @@ pub async fn get_ipv6_socket() -> Option<(Arc<UdpSocket>, bytes::Bytes)> {
None None
} }
// The color is the same to `str2color()` in flutter.
pub fn str2color(s: &str, alpha: u8) -> u32 {
let bytes = s.as_bytes();
// dart code `160 << 16 + 114 << 8 + 91` results `0`.
let mut hash: u32 = 0;
for &byte in bytes {
let code = byte as u32;
hash = code.wrapping_add((hash << 5).wrapping_sub(hash));
}
hash = hash % 16777216;
let rgb = hash & 0xFF7FFF;
(alpha as u32) << 24 | rgb
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -574,6 +574,12 @@ pub fn core_main() -> Option<Vec<String>> {
crate::flutter::connection_manager::start_cm_no_ui(); crate::flutter::connection_manager::start_cm_no_ui();
} }
return None; return None;
} else if args[0] == "--whiteboard" {
#[cfg(target_os = "windows")]
{
crate::whiteboard::run();
}
return None;
} else if args[0] == "-gtk-sudo" { } else if args[0] == "-gtk-sudo" {
// rustdesk service kill `rustdesk --` processes // rustdesk service kill `rustdesk --` processes
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]

View File

@@ -177,7 +177,7 @@ pub enum DataPortableService {
Ping, Ping,
Pong, Pong,
ConnCount(Option<usize>), ConnCount(Option<usize>),
Mouse((Vec<u8>, i32)), Mouse((Vec<u8>, i32, String, u32, bool, bool)),
Pointer((Vec<u8>, i32)), Pointer((Vec<u8>, i32)),
Key(Vec<u8>), Key(Vec<u8>),
RequestStart, RequestStart,
@@ -289,6 +289,8 @@ pub enum Data {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
PortForwardSessionCount(Option<usize>), PortForwardSessionCount(Option<usize>),
SocksWs(Option<Box<(Option<config::Socks5Server>, String)>>), SocksWs(Option<Box<(Option<config::Socks5Server>, String)>>),
#[cfg(target_os = "windows")]
Whiteboard((String, crate::whiteboard::CustomEvent)),
} }
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]

View File

@@ -708,6 +708,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Failed to check if the user is an administrator.", "فشل التحقق مما إذا كان المستخدم لديه صلاحيات المسؤول."), ("Failed to check if the user is an administrator.", "فشل التحقق مما إذا كان المستخدم لديه صلاحيات المسؤول."),
("Supported only in the installed version.", "مدعوم فقط في النسخة المُثبتة."), ("Supported only in the installed version.", "مدعوم فقط في النسخة المُثبتة."),
("elevation_username_tip", "يرجى إدخال اسم مستخدم بصلاحيات المسؤول للمتابعة."), ("elevation_username_tip", "يرجى إدخال اسم مستخدم بصلاحيات المسؤول للمتابعة."),
("Preparing for installation ...", "جارٍ التحضير للتثبيت...") ("Preparing for installation ...", "جارٍ التحضير للتثبيت..."),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "仅在以安装版本受支持。"), ("Supported only in the installed version.", "仅在以安装版本受支持。"),
("elevation_username_tip", "输入用户名或域名\\用户名"), ("elevation_username_tip", "输入用户名或域名\\用户名"),
("Preparing for installation ...", "准备安装..."), ("Preparing for installation ...", "准备安装..."),
("Show my cursor", "显示我的光标"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "Wird nur in der installierten Version unterstützt."), ("Supported only in the installed version.", "Wird nur in der installierten Version unterstützt."),
("elevation_username_tip", "Geben Sie Benutzername oder Domäne\\Benutzername ein"), ("elevation_username_tip", "Geben Sie Benutzername oder Domäne\\Benutzername ein"),
("Preparing for installation ...", "Installation wird vorbereitet …"), ("Preparing for installation ...", "Installation wird vorbereitet …"),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "Soportado solo en la versión instalada."), ("Supported only in the installed version.", "Soportado solo en la versión instalada."),
("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"), ("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "فقط در نسخه نصب‌شده پشتیبانی می‌شود."), ("Supported only in the installed version.", "فقط در نسخه نصب‌شده پشتیبانی می‌شود."),
("elevation_username_tip", "لطفاً نام کاربری مدیریتی را برای ارتقاء دسترسی وارد کنید."), ("elevation_username_tip", "لطفاً نام کاربری مدیریتی را برای ارتقاء دسترسی وارد کنید."),
("Preparing for installation ...", "در حال آماده‌سازی برای نصب..."), ("Preparing for installation ...", "در حال آماده‌سازی برای نصب..."),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "Uniquement pris en charge dans la version installée."), ("Supported only in the installed version.", "Uniquement pris en charge dans la version installée."),
("elevation_username_tip", "Saisissez un nom dutilisateur ou un domaine\\utilisateur"), ("elevation_username_tip", "Saisissez un nom dutilisateur ou un domaine\\utilisateur"),
("Preparing for installation ...", "Préparation de linstallation…"), ("Preparing for installation ...", "Préparation de linstallation…"),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "נתמך רק בגרסה המותקנת"), ("Supported only in the installed version.", "נתמך רק בגרסה המותקנת"),
("elevation_username_tip", "רמז_ליוזר_להעלאת_הרשאה"), ("elevation_username_tip", "רמז_ליוזר_להעלאת_הרשאה"),
("Preparing for installation ...", "הכנה להתקנה..."), ("Preparing for installation ...", "הכנה להתקנה..."),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "Csak a telepített változatban támogatott."), ("Supported only in the installed version.", "Csak a telepített változatban támogatott."),
("elevation_username_tip", "Felhasználónév vagy tartománynév megadása\\felhasználónév"), ("elevation_username_tip", "Felhasználónév vagy tartománynév megadása\\felhasználónév"),
("Preparing for installation ...", "Felkészülés a telepítésre ..."), ("Preparing for installation ...", "Felkészülés a telepítésre ..."),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "Supportato solo nella versione installata."), ("Supported only in the installed version.", "Supportato solo nella versione installata."),
("elevation_username_tip", "Inserisci Nome utente o dominio sorgente\\nome Utente"), ("elevation_username_tip", "Inserisci Nome utente o dominio sorgente\\nome Utente"),
("Preparing for installation ...", "Preparazione per l'installazione..."), ("Preparing for installation ...", "Preparazione per l'installazione..."),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "インストールされたバージョンでのみサポートされます。"), ("Supported only in the installed version.", "インストールされたバージョンでのみサポートされます。"),
("elevation_username_tip", "ユーザー名またはドメインのユーザー名を入力してください。"), ("elevation_username_tip", "ユーザー名またはドメインのユーザー名を入力してください。"),
("Preparing for installation ...", "インストールの準備中です..."), ("Preparing for installation ...", "インストールの準備中です..."),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "설치된 버전에서만 지원됩니다."), ("Supported only in the installed version.", "설치된 버전에서만 지원됩니다."),
("elevation_username_tip", "사용자 이름 또는 도메인\\사용자 이름 입력"), ("elevation_username_tip", "사용자 이름 또는 도메인\\사용자 이름 입력"),
("Preparing for installation ...", "설치 준비 중 ..."), ("Preparing for installation ...", "설치 준비 중 ..."),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "Alleen ondersteund in de geïnstalleerde versie."), ("Supported only in the installed version.", "Alleen ondersteund in de geïnstalleerde versie."),
("elevation_username_tip", "Voer je gebruikersnaam of domeinnaam in"), ("elevation_username_tip", "Voer je gebruikersnaam of domeinnaam in"),
("Preparing for installation ...", "Installatie voorbereiden ..."), ("Preparing for installation ...", "Installatie voorbereiden ..."),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "Wspierane tylko dla zainstalowanej aplikacji."), ("Supported only in the installed version.", "Wspierane tylko dla zainstalowanej aplikacji."),
("elevation_username_tip", "Podaj nazwę użytkownika lub domena\\użytkownik"), ("elevation_username_tip", "Podaj nazwę użytkownika lub domena\\użytkownik"),
("Preparing for installation ...", "Przygotowywanie do instalacji ..."), ("Preparing for installation ...", "Przygotowywanie do instalacji ..."),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "Поддерживается только в установочной версии."), ("Supported only in the installed version.", "Поддерживается только в установочной версии."),
("elevation_username_tip", "Введите пользователя или домен\\пользователя"), ("elevation_username_tip", "Введите пользователя или домен\\пользователя"),
("Preparing for installation ...", "Подготовка к установке..."), ("Preparing for installation ...", "Подготовка к установке..."),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "Suportadu petzi in sa versione installada."), ("Supported only in the installed version.", "Suportadu petzi in sa versione installada."),
("elevation_username_tip", "Inserta Nùmene utente o domìniu de fonte\\nùmene Utente"), ("elevation_username_tip", "Inserta Nùmene utente o domìniu de fonte\\nùmene Utente"),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", "僅支援於已安裝的版本"), ("Supported only in the installed version.", "僅支援於已安裝的版本"),
("elevation_username_tip", "輸入使用者名稱或網域\\使用者名稱"), ("elevation_username_tip", "輸入使用者名稱或網域\\使用者名稱"),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Supported only in the installed version.", ""), ("Supported only in the installed version.", ""),
("elevation_username_tip", ""), ("elevation_username_tip", ""),
("Preparing for installation ...", ""), ("Preparing for installation ...", ""),
("Show my cursor", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -55,6 +55,9 @@ pub mod plugin;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
mod tray; mod tray;
#[cfg(target_os = "windows")]
mod whiteboard;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
mod updater; mod updater;

View File

@@ -126,9 +126,18 @@ pub struct ConnInner {
tx_video: Option<Sender>, tx_video: Option<Sender>,
} }
struct InputMouse {
msg: MouseEvent,
conn_id: i32,
username: String,
argb: u32,
simulate: bool,
show_cursor: bool,
}
enum MessageInput { enum MessageInput {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
Mouse((MouseEvent, i32)), Mouse(InputMouse),
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
Key((KeyEvent, bool)), Key((KeyEvent, bool)),
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -225,6 +234,9 @@ pub struct Connection {
// by peer // by peer
disable_keyboard: bool, disable_keyboard: bool,
// by peer // by peer
#[cfg(not(any(target_os = "android", target_os = "ios")))]
show_my_cursor: bool,
// by peer
disable_clipboard: bool, disable_clipboard: bool,
// by peer // by peer
disable_audio: bool, disable_audio: bool,
@@ -240,6 +252,7 @@ pub struct Connection {
server_audit_conn: String, server_audit_conn: String,
server_audit_file: String, server_audit_file: String,
lr: LoginRequest, lr: LoginRequest,
peer_argb: u32,
session_last_recv_time: Option<Arc<Mutex<Instant>>>, session_last_recv_time: Option<Arc<Mutex<Instant>>>,
chat_unanswered: bool, chat_unanswered: bool,
file_transferred: bool, file_transferred: bool,
@@ -403,11 +416,14 @@ impl Connection {
enable_file_transfer: false, enable_file_transfer: false,
disable_clipboard: false, disable_clipboard: false,
disable_keyboard: false, disable_keyboard: false,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
show_my_cursor: false,
tx_input, tx_input,
video_ack_required: false, video_ack_required: false,
server_audit_conn: "".to_owned(), server_audit_conn: "".to_owned(),
server_audit_file: "".to_owned(), server_audit_file: "".to_owned(),
lr: Default::default(), lr: Default::default(),
peer_argb: 0u32,
session_last_recv_time: None, session_last_recv_time: None,
chat_unanswered: false, chat_unanswered: false,
file_transferred: false, file_transferred: false,
@@ -938,8 +954,15 @@ impl Connection {
loop { loop {
match receiver.recv_timeout(std::time::Duration::from_millis(500)) { match receiver.recv_timeout(std::time::Duration::from_millis(500)) {
Ok(v) => match v { Ok(v) => match v {
MessageInput::Mouse((msg, id)) => { MessageInput::Mouse(mouse_input) => {
handle_mouse(&msg, id); handle_mouse(
&mouse_input.msg,
mouse_input.conn_id,
mouse_input.username,
mouse_input.argb,
mouse_input.simulate,
mouse_input.show_cursor,
);
} }
MessageInput::Key((mut msg, press)) => { MessageInput::Key((mut msg, press)) => {
// Set the press state to false, use `down` only in `handle_key()`. // Set the press state to false, use `down` only in `handle_key()`.
@@ -1784,8 +1807,25 @@ impl Connection {
#[inline] #[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
fn input_mouse(&self, msg: MouseEvent, conn_id: i32) { fn input_mouse(
self.tx_input.send(MessageInput::Mouse((msg, conn_id))).ok(); &self,
msg: MouseEvent,
conn_id: i32,
username: String,
argb: u32,
simulate: bool,
show_cursor: bool,
) {
self.tx_input
.send(MessageInput::Mouse(InputMouse {
msg,
conn_id,
username,
argb,
simulate,
show_cursor,
}))
.ok();
} }
#[inline] #[inline]
@@ -1900,6 +1940,7 @@ impl Connection {
async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) {
self.lr = lr.clone(); self.lr = lr.clone();
self.peer_argb = crate::str2color(&format!("{}{}", &lr.my_id, &lr.my_platform), 0xff);
if let Some(o) = lr.option.as_ref() { if let Some(o) = lr.option.as_ref() {
self.options_in_login = Some(o.clone()); self.options_in_login = Some(o.clone());
} }
@@ -2279,7 +2320,23 @@ impl Connection {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
self.retina.on_mouse_event(&mut me, self.display_idx); self.retina.on_mouse_event(&mut me, self.display_idx);
self.input_mouse(me, self.inner.id()); self.input_mouse(
me,
self.inner.id(),
self.lr.my_name.clone(),
self.peer_argb,
true,
self.show_my_cursor,
);
} else if self.show_my_cursor {
self.input_mouse(
me,
self.inner.id(),
self.lr.my_name.clone(),
self.peer_argb,
false,
true,
);
} }
self.update_auto_disconnect_timer(); self.update_auto_disconnect_timer();
} }
@@ -3640,6 +3697,18 @@ impl Connection {
self.update_terminal_persistence(q == BoolOption::Yes).await; self.update_terminal_persistence(q == BoolOption::Yes).await;
} }
} }
#[cfg(target_os = "windows")]
if let Ok(q) = o.show_my_cursor.enum_value() {
if q != BoolOption::NotSet {
use crate::whiteboard;
self.show_my_cursor = q == BoolOption::Yes;
if q == BoolOption::Yes {
whiteboard::register_whiteboard(whiteboard::get_key_cursor(self.inner.id));
} else {
whiteboard::unregister_whiteboard(whiteboard::get_key_cursor(self.inner.id));
}
}
}
} }
async fn turn_on_privacy(&mut self, impl_key: String) { async fn turn_on_privacy(&mut self, impl_key: String) {
@@ -4792,6 +4861,11 @@ mod raii {
scrap::wayland::pipewire::try_close_session(); scrap::wayland::pipewire::try_close_session();
} }
Self::check_wake_lock(); Self::check_wake_lock();
#[cfg(target_os = "windows")]
{
use crate::whiteboard;
whiteboard::unregister_whiteboard(whiteboard::get_key_cursor(self.0));
}
} }
} }
} }

View File

@@ -2,6 +2,8 @@
use super::rdp_input::client::{RdpInputKeyboard, RdpInputMouse}; use super::rdp_input::client::{RdpInputKeyboard, RdpInputMouse};
use super::*; use super::*;
use crate::input::*; use crate::input::*;
#[cfg(target_os = "windows")]
use crate::whiteboard;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use dispatch::Queue; use dispatch::Queue;
use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable};
@@ -698,18 +700,25 @@ fn get_modifier_state(key: Key, en: &mut Enigo) -> bool {
} }
#[allow(unreachable_code)] #[allow(unreachable_code)]
pub fn handle_mouse(evt: &MouseEvent, conn: i32) { pub fn handle_mouse(
evt: &MouseEvent,
conn: i32,
username: String,
argb: u32,
simulate: bool,
show_cursor: bool,
) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
// having GUI (--server has tray, it is GUI too), run main GUI thread, otherwise crash // having GUI (--server has tray, it is GUI too), run main GUI thread, otherwise crash
let evt = evt.clone(); let evt = evt.clone();
QUEUE.exec_async(move || handle_mouse_(&evt, conn)); QUEUE.exec_async(move || handle_mouse_(&evt, conn, username, argb, simulate, show_cursor));
return; return;
} }
#[cfg(windows)] #[cfg(windows)]
crate::portable_service::client::handle_mouse(evt, conn); crate::portable_service::client::handle_mouse(evt, conn, username, argb, simulate, show_cursor);
#[cfg(not(windows))] #[cfg(not(windows))]
handle_mouse_(evt, conn); handle_mouse_(evt, conn, username, argb, simulate, show_cursor);
} }
// to-do: merge handle_mouse and handle_pointer // to-do: merge handle_mouse and handle_pointer
@@ -979,7 +988,24 @@ pub fn handle_pointer_(evt: &PointerDeviceEvent, conn: i32) {
} }
} }
pub fn handle_mouse_(evt: &MouseEvent, conn: i32) { pub fn handle_mouse_(
evt: &MouseEvent,
conn: i32,
username: String,
argb: u32,
simulate: bool,
_show_cursor: bool,
) {
if simulate {
handle_mouse_simulation_(evt, conn);
}
#[cfg(target_os = "windows")]
if _show_cursor {
handle_mouse_show_cursor_(evt, conn, username, argb);
}
}
pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
if !active_mouse_(conn) { if !active_mouse_(conn) {
return; return;
} }
@@ -1122,6 +1148,41 @@ pub fn handle_mouse_(evt: &MouseEvent, conn: i32) {
} }
} }
#[cfg(target_os = "windows")]
pub fn handle_mouse_show_cursor_(evt: &MouseEvent, conn: i32, username: String, argb: u32) {
let buttons = evt.mask >> 3;
let evt_type = evt.mask & 0x7;
match evt_type {
MOUSE_TYPE_MOVE => {
whiteboard::update_whiteboard(
whiteboard::get_key_cursor(conn),
whiteboard::CustomEvent::Cursor(whiteboard::Cursor {
x: evt.x as _,
y: evt.y as _,
argb,
btns: 0,
text: username,
}),
);
}
MOUSE_TYPE_UP => {
if buttons == MOUSE_BUTTON_LEFT {
whiteboard::update_whiteboard(
whiteboard::get_key_cursor(conn),
whiteboard::CustomEvent::Cursor(whiteboard::Cursor {
x: evt.x as _,
y: evt.y as _,
argb,
btns: buttons,
text: username,
}),
);
}
}
_ => {}
}
}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn handle_scale(scale: i32) { fn handle_scale(scale: i32) {
let mut en = ENIGO.lock().unwrap(); let mut en = ENIGO.lock().unwrap();

View File

@@ -476,9 +476,9 @@ pub mod server {
break; break;
} }
} }
Mouse((v, conn)) => { Mouse((v, conn, username, argb, simulate, show_cursor)) => {
if let Ok(evt) = MouseEvent::parse_from_bytes(&v) { if let Ok(evt) = MouseEvent::parse_from_bytes(&v) {
crate::input_service::handle_mouse_(&evt, conn); crate::input_service::handle_mouse_(&evt, conn, username, argb, simulate, show_cursor);
} }
} }
Pointer((v, conn)) => { Pointer((v, conn)) => {
@@ -875,11 +875,23 @@ pub mod client {
} }
} }
fn handle_mouse_(evt: &MouseEvent, conn: i32) -> ResultType<()> { fn handle_mouse_(
evt: &MouseEvent,
conn: i32,
username: String,
argb: u32,
simulate: bool,
show_cursor: bool,
) -> ResultType<()> {
let mut v = vec![]; let mut v = vec![];
evt.write_to_vec(&mut v)?; evt.write_to_vec(&mut v)?;
ipc_send(Data::DataPortableService(DataPortableService::Mouse(( ipc_send(Data::DataPortableService(DataPortableService::Mouse((
v, conn, v,
conn,
username,
argb,
simulate,
show_cursor,
)))) ))))
} }
@@ -927,12 +939,19 @@ pub mod client {
} }
} }
pub fn handle_mouse(evt: &MouseEvent, conn: i32) { pub fn handle_mouse(
evt: &MouseEvent,
conn: i32,
username: String,
argb: u32,
simulate: bool,
show_cursor: bool,
) {
if RUNNING.lock().unwrap().clone() { if RUNNING.lock().unwrap().clone() {
crate::input_service::update_latest_input_cursor_time(conn); crate::input_service::update_latest_input_cursor_time(conn);
handle_mouse_(evt, conn).ok(); handle_mouse_(evt, conn, username, argb, simulate, show_cursor).ok();
} else { } else {
crate::input_service::handle_mouse_(evt, conn); crate::input_service::handle_mouse_(evt, conn, username, argb, simulate, show_cursor);
} }
} }

731
src/whiteboard.rs Normal file
View File

@@ -0,0 +1,731 @@
use crate::ipc::{self, new_listener, Connection, Data};
use hbb_common::{
allow_err,
anyhow::anyhow,
bail, log, sleep,
tokio::{
self,
sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
time::interval_at,
},
ResultType,
};
use lazy_static::lazy_static;
use serde_derive::{Deserialize, Serialize};
use softbuffer::{Context, Surface};
use std::{
collections::HashMap,
num::NonZeroU32,
sync::{Arc, RwLock},
time::Instant,
};
#[cfg(target_os = "linux")]
use tao::platform::unix::WindowBuilderExtUnix;
#[cfg(target_os = "windows")]
use tao::platform::windows::WindowBuilderExtWindows;
use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy},
window::WindowBuilder,
};
use tiny_skia::{Color, FillRule, Paint, PathBuilder, PixmapMut, Point, Stroke, Transform};
use ttf_parser::Face;
lazy_static! {
static ref EVENT_PROXY: RwLock<Option<EventLoopProxy<(String, CustomEvent)>>> =
RwLock::new(None);
static ref TX_WHITEBOARD: RwLock<Option<UnboundedSender<(String, CustomEvent)>>> =
RwLock::new(None);
static ref CONNS: RwLock<HashMap<String, Conn>> = Default::default();
}
struct Conn {
last_cursor_pos: (f32, f32), // For click ripple
last_cursor_evt: LastCursorEvent,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum CustomEvent {
Cursor(Cursor),
Clear,
Exit,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t")]
pub struct Cursor {
pub x: f32,
pub y: f32,
pub argb: u32,
pub btns: i32,
pub text: String,
}
struct LastCursorEvent {
evt: Option<CustomEvent>,
tm: Instant,
c: usize,
}
// A helper struct to bridge `ttf-parser` and `tiny-skia`.
struct PathBuilderWrapper<'a> {
path_builder: &'a mut PathBuilder,
transform: Transform,
}
impl ttf_parser::OutlineBuilder for PathBuilderWrapper<'_> {
fn move_to(&mut self, x: f32, y: f32) {
let mut pt = Point::from_xy(x, y);
self.transform.map_point(&mut pt);
self.path_builder.move_to(pt.x, pt.y);
}
fn line_to(&mut self, x: f32, y: f32) {
let mut pt = Point::from_xy(x, y);
self.transform.map_point(&mut pt);
self.path_builder.line_to(pt.x, pt.y);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
let mut pt1 = Point::from_xy(x1, y1);
self.transform.map_point(&mut pt1);
let mut pt = Point::from_xy(x, y);
self.transform.map_point(&mut pt);
self.path_builder.quad_to(pt1.x, pt1.y, pt.x, pt.y);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
let mut pt1 = Point::from_xy(x1, y1);
self.transform.map_point(&mut pt1);
let mut pt2 = Point::from_xy(x2, y2);
self.transform.map_point(&mut pt2);
let mut pt = Point::from_xy(x, y);
self.transform.map_point(&mut pt);
self.path_builder
.cubic_to(pt1.x, pt1.y, pt2.x, pt2.y, pt.x, pt.y);
}
fn close(&mut self) {
self.path_builder.close();
}
}
// Draws a string of text onto the pixmap.
fn draw_text(
pixmap: &mut PixmapMut,
face: &Face,
text: &str,
x: f32,
y: f32,
paint: &Paint,
font_size: f32,
) {
let units_per_em = face.units_per_em() as f32;
let scale = font_size / units_per_em;
let transform = Transform::from_translate(x, y).pre_scale(scale, -scale);
let mut path_builder = PathBuilder::new();
let mut current_x = 0.0;
for ch in text.chars() {
let glyph_id = face.glyph_index(ch).unwrap_or_default();
let mut builder = PathBuilderWrapper {
path_builder: &mut path_builder,
transform: transform.post_translate(current_x, 0.0),
};
face.outline_glyph(glyph_id, &mut builder);
if let Some(h_advance) = face.glyph_hor_advance(glyph_id) {
current_x += h_advance as f32 * scale;
}
}
if let Some(path) = path_builder.finish() {
pixmap.fill_path(&path, paint, FillRule::Winding, Transform::identity(), None);
}
}
#[inline]
pub fn get_key_cursor(conn_id: i32) -> String {
format!("{}-cursor", conn_id)
}
pub fn register_whiteboard(k: String) {
std::thread::spawn(|| {
allow_err!(start_whiteboard_());
});
let mut conns = CONNS.write().unwrap();
if !conns.contains_key(&k) {
conns.insert(
k,
Conn {
last_cursor_pos: (0.0, 0.0),
last_cursor_evt: LastCursorEvent {
evt: None,
tm: Instant::now(),
c: 0,
},
},
);
}
}
pub fn unregister_whiteboard(k: String) {
let mut conns = CONNS.write().unwrap();
conns.remove(&k);
let is_conns_empty = conns.is_empty();
drop(conns);
TX_WHITEBOARD.read().unwrap().as_ref().map(|tx| {
allow_err!(tx.send((k, CustomEvent::Clear)));
});
if is_conns_empty {
std::thread::spawn(|| {
let mut whiteboard = TX_WHITEBOARD.write().unwrap();
whiteboard.as_ref().map(|tx| {
allow_err!(tx.send(("".to_string(), CustomEvent::Exit)));
// Simple sleep to wait the whiteboard process exiting.
std::thread::sleep(std::time::Duration::from_millis(3_00));
});
whiteboard.take();
});
}
}
pub fn update_whiteboard(k: String, e: CustomEvent) {
let mut conns = CONNS.write().unwrap();
let Some(conn) = conns.get_mut(&k) else {
return;
};
match &e {
CustomEvent::Cursor(cursor) => {
conn.last_cursor_evt.c += 1;
conn.last_cursor_evt.tm = Instant::now();
if cursor.btns == 0 {
// Send one movement event every 4.
if conn.last_cursor_evt.c > 3 {
conn.last_cursor_evt.c = 0;
conn.last_cursor_evt.evt = None;
tx_send_event(conn, k, e);
} else {
conn.last_cursor_evt.evt = Some(e);
}
} else {
if let Some(evt) = conn.last_cursor_evt.evt.take() {
tx_send_event(conn, k.clone(), evt);
conn.last_cursor_evt.c = 0;
}
let click_evt = CustomEvent::Cursor(Cursor {
x: conn.last_cursor_pos.0,
y: conn.last_cursor_pos.1,
argb: cursor.argb,
btns: cursor.btns,
text: cursor.text.clone(),
});
tx_send_event(conn, k, click_evt);
}
}
_ => {
tx_send_event(conn, k, e);
}
}
}
#[inline]
fn tx_send_event(conn: &mut Conn, k: String, event: CustomEvent) {
if let CustomEvent::Cursor(cursor) = &event {
if cursor.btns == 0 {
conn.last_cursor_pos = (cursor.x, cursor.y);
}
}
TX_WHITEBOARD.read().unwrap().as_ref().map(|tx| {
allow_err!(tx.send((k, event)));
});
}
#[tokio::main(flavor = "current_thread")]
async fn start_whiteboard_() -> ResultType<()> {
let mut tx_whiteboard = TX_WHITEBOARD.write().unwrap();
if tx_whiteboard.is_some() {
log::warn!("Whiteboard already started");
return Ok(());
}
loop {
if !crate::platform::is_prelogin() {
break;
}
sleep(1.).await;
}
let mut stream = None;
if let Ok(s) = ipc::connect(1000, "_whiteboard").await {
stream = Some(s);
} else {
#[allow(unused_mut)]
#[allow(unused_assignments)]
let mut args = vec!["--whiteboard"];
#[allow(unused_mut)]
#[cfg(target_os = "linux")]
let mut user = None;
let run_done;
if crate::platform::is_root() {
let mut res = Ok(None);
for _ in 0..10 {
#[cfg(not(any(target_os = "linux")))]
{
log::debug!("Start whiteboard");
res = crate::platform::run_as_user(args.clone());
}
#[cfg(target_os = "linux")]
{
log::debug!("Start whiteboard");
res = crate::platform::run_as_user(
args.clone(),
user.clone(),
None::<(&str, &str)>,
);
}
if res.is_ok() {
break;
}
log::error!("Failed to run whiteboard: {res:?}");
sleep(1.).await;
}
if let Some(task) = res? {
super::CHILD_PROCESS.lock().unwrap().push(task);
}
run_done = true;
} else {
run_done = false;
}
if !run_done {
log::debug!("Start whiteboard");
super::CHILD_PROCESS
.lock()
.unwrap()
.push(crate::run_me(args)?);
}
for _ in 0..20 {
sleep(0.3).await;
if let Ok(s) = ipc::connect(1000, "_whiteboard").await {
stream = Some(s);
break;
}
}
if stream.is_none() {
bail!("Failed to connect to connection manager");
}
}
let mut stream = stream.ok_or(anyhow!("none stream"))?;
let (tx, mut rx) = unbounded_channel();
tx_whiteboard.replace(tx);
drop(tx_whiteboard);
let _call_on_ret = crate::common::SimpleCallOnReturn {
b: true,
f: Box::new(move || {
let _ = TX_WHITEBOARD.write().unwrap().take();
}),
};
let dur = tokio::time::Duration::from_millis(300);
let mut timer = interval_at(tokio::time::Instant::now() + dur, dur);
timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
tokio::select! {
res = rx.recv() => {
match res {
Some(data) => {
if matches!(data.1, CustomEvent::Exit) {
break;
} else {
allow_err!(stream.send(&Data::Whiteboard(data)).await);
timer.reset();
}
}
None => {
bail!("expected");
}
}
},
_ = timer.tick() => {
let mut conns = CONNS.write().unwrap();
for (k, conn) in conns.iter_mut() {
if conn.last_cursor_evt.tm.elapsed().as_millis() > 300 {
if let Some(evt) = conn.last_cursor_evt.evt.take() {
allow_err!(stream.send(&Data::Whiteboard((k.clone(), evt))).await);
conn.last_cursor_evt.c = 0;
}
}
}
}
}
}
allow_err!(
stream
.send(&Data::Whiteboard(("".to_string(), CustomEvent::Exit)))
.await
);
Ok(())
}
pub fn run() {
let (tx_exit, rx_exit) = unbounded_channel();
std::thread::spawn(move || {
start_ipc(rx_exit);
});
if let Err(e) = create_event_loop() {
log::error!("Failed to create event loop: {}", e);
tx_exit.send(()).ok();
return;
}
}
#[tokio::main(flavor = "current_thread")]
async fn start_ipc(mut rx_exit: UnboundedReceiver<()>) {
match new_listener("_whiteboard").await {
Ok(mut incoming) => loop {
tokio::select! {
_ = rx_exit.recv() => {
log::info!("Exiting IPC");
break;
}
res = incoming.next() => match res {
Some(result) => match result {
Ok(stream) => {
log::debug!("Got new connection");
tokio::spawn(handle_new_stream(Connection::new(stream)));
}
Err(err) => {
log::error!("Couldn't get whiteboard client: {:?}", err);
}
},
None => {
log::error!("Failed to get whiteboard client");
}
}
}
},
Err(err) => {
log::error!("Failed to start whiteboard ipc server: {}", err);
}
}
}
async fn handle_new_stream(mut conn: Connection) {
loop {
tokio::select! {
res = conn.next() => {
match res {
Err(err) => {
log::info!("whiteboard ipc connection closed: {}", err);
break;
}
Ok(Some(data)) => {
match data {
Data::Whiteboard((k, evt)) => {
if matches!(evt, CustomEvent::Exit) {
log::info!("whiteboard ipc connection closed");
break;
} else {
EVENT_PROXY.read().unwrap().as_ref().map(|ep| {
allow_err!(ep.send_event((k, evt)));
});
}
}
_ => {
}
}
}
Ok(None) => {
log::info!("whiteboard ipc connection closed");
break;
}
}
}
}
}
EVENT_PROXY.read().unwrap().as_ref().map(|ep| {
allow_err!(ep.send_event(("".to_string(), CustomEvent::Exit)));
});
}
fn create_font_face() -> ResultType<Face<'static>> {
let mut font_db = fontdb::Database::new();
font_db.load_system_fonts();
let query = fontdb::Query {
families: &[fontdb::Family::Monospace],
..fontdb::Query::default()
};
let Some(font_id) = font_db.query(&query) else {
bail!("No monospace font found!");
};
let Some((font_source, face_index)) = font_db.face_source(font_id) else {
bail!("No face found for font!");
};
let font_data: &'static [u8] = Box::leak(match font_source {
fontdb::Source::File(path) => std::fs::read(path)?.into_boxed_slice(),
fontdb::Source::Binary(data) => data.as_ref().as_ref().to_vec().into_boxed_slice(),
fontdb::Source::SharedFile(path, _) => std::fs::read(path)?.into_boxed_slice(),
});
let face = Face::parse(font_data, face_index)?;
Ok(face)
}
fn create_event_loop() -> ResultType<()> {
let face = match create_font_face() {
Ok(face) => Some(face),
Err(err) => {
log::error!("Failed to create font face: {}", err);
None
}
};
let event_loop = EventLoopBuilder::<(String, CustomEvent)>::with_user_event().build();
let mut window_builder = WindowBuilder::new()
.with_title("RustDesk whiteboard")
.with_transparent(true)
.with_always_on_top(true)
.with_decorations(false);
use tao::dpi::{PhysicalPosition, PhysicalSize};
let mut final_size = None;
if let Ok(displays) = crate::server::display_service::try_get_displays() {
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
for display in displays {
let (x, y) = (display.origin().0 as i32, display.origin().1 as i32);
let (w, h) = (display.width() as i32, display.height() as i32);
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x + w);
max_y = max_y.max(y + h);
}
let (x, y) = (min_x, min_y);
let (w, h) = ((max_x - min_x) as u32, (max_y - min_y) as u32);
if w > 0 && h > 0 {
final_size = Some(PhysicalSize::new(w, h));
window_builder = window_builder
.with_position(PhysicalPosition::new(x, y))
.with_inner_size(PhysicalSize::new(1, 1));
} else {
window_builder =
window_builder.with_fullscreen(Some(tao::window::Fullscreen::Borderless(None)));
}
} else {
window_builder =
window_builder.with_fullscreen(Some(tao::window::Fullscreen::Borderless(None)));
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
window_builder = window_builder.with_skip_taskbar(true);
}
let window = Arc::new(window_builder.build::<(String, CustomEvent)>(&event_loop)?);
window.set_ignore_cursor_events(true)?;
let context = Context::new(window.clone()).map_err(|e| {
log::error!("Failed to create context: {}", e);
anyhow!(e.to_string())
})?;
let mut surface = Surface::new(&context, window.clone()).map_err(|e| {
log::error!("Failed to create surface: {}", e);
anyhow!(e.to_string())
})?;
let proxy = event_loop.create_proxy();
EVENT_PROXY.write().unwrap().replace(proxy);
let _call_on_ret = crate::common::SimpleCallOnReturn {
b: true,
f: Box::new(move || {
let _ = EVENT_PROXY.write().unwrap().take();
}),
};
struct Ripple {
x: f32,
y: f32,
start_time: Instant,
}
let mut ripples: Vec<Ripple> = Vec::new();
let mut last_cursors: HashMap<String, Cursor> = HashMap::new();
let mut resized = final_size.is_none();
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Poll;
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}
_ => {}
},
Event::RedrawRequested(_) => {
if !resized {
if let Some(size) = final_size.take() {
window.set_inner_size(size);
}
resized = true;
return;
}
let (width, height) = {
let size = window.inner_size();
(size.width, size.height)
};
let (Some(width), Some(height)) = (NonZeroU32::new(width), NonZeroU32::new(height))
else {
return;
};
if let Err(e) = surface.resize(width, height) {
log::error!("Failed to resize surface: {}", e);
return;
}
let mut buffer = match surface.buffer_mut() {
Ok(buf) => buf,
Err(e) => {
log::error!("Failed to get buffer: {}", e);
return;
}
};
let Some(mut pixmap) = PixmapMut::from_bytes(
bytemuck::cast_slice_mut(&mut buffer),
width.get(),
height.get(),
) else {
log::error!("Failed to create pixmap from buffer");
return;
};
pixmap.fill(Color::TRANSPARENT);
let ripple_duration = std::time::Duration::from_millis(500);
ripples.retain(|r| r.start_time.elapsed() < ripple_duration);
for ripple in &ripples {
let elapsed = ripple.start_time.elapsed();
let progress = elapsed.as_secs_f32() / ripple_duration.as_secs_f32();
let radius = 45.0 * progress;
let alpha = 1.0 - progress;
let mut ripple_paint = Paint::default();
// Note: The real color is bgra here.
ripple_paint.set_color_rgba8(128, 128, 255, (alpha * 128.0) as u8);
ripple_paint.anti_alias = true;
let mut ripple_pb = PathBuilder::new();
let (rx, ry) = (ripple.x as f64, ripple.y as f64);
ripple_pb.push_circle(rx as f32, ry as f32, radius as f32);
if let Some(path) = ripple_pb.finish() {
pixmap.fill_path(
&path,
&ripple_paint,
FillRule::Winding,
Transform::identity(),
None,
);
}
}
for cursor in last_cursors.values() {
let (x, y) = (cursor.x as f64, cursor.y as f64);
let (x, y) = (x as f32, y as f32);
let size = 1.5 as f32;
let mut pb = PathBuilder::new();
pb.move_to(x, y);
pb.line_to(x, y + 16.0 * size);
pb.line_to(x + 4.0 * size, y + 13.0 * size);
pb.line_to(x + 7.0 * size, y + 20.0 * size);
pb.line_to(x + 9.0 * size, y + 19.0 * size);
pb.line_to(x + 6.0 * size, y + 12.0 * size);
pb.line_to(x + 11.0 * size, y + 12.0 * size);
pb.close();
if let Some(path) = pb.finish() {
let mut arrow_paint = Paint::default();
// Note: The real color is bgra here.
arrow_paint.set_color_rgba8(
(cursor.argb & 0xFF) as u8,
(cursor.argb >> 8 & 0xFF) as u8,
(cursor.argb >> 16 & 0xFF) as u8,
(cursor.argb >> 24 & 0xFF) as u8,
);
arrow_paint.anti_alias = true;
pixmap.fill_path(
&path,
&arrow_paint,
FillRule::Winding,
Transform::identity(),
None,
);
let mut black_paint = Paint::default();
black_paint.set_color_rgba8(0, 0, 0, 255);
black_paint.anti_alias = true;
let mut stroke = Stroke::default();
stroke.width = 1.0 as f32;
pixmap.stroke_path(
&path,
&black_paint,
&stroke,
Transform::identity(),
None,
);
face.as_ref().map(|face| {
draw_text(
&mut pixmap,
face,
&cursor.text,
x + 24.0 * size,
y + 24.0 * size,
&arrow_paint,
24.0 as f32,
);
});
}
}
if let Err(e) = buffer.present() {
log::error!("Failed to present surface: {}", e);
return;
}
}
Event::MainEventsCleared => {
window.request_redraw();
}
Event::UserEvent((k, evt)) => match evt {
CustomEvent::Cursor(cursor) => {
if cursor.btns != 0 {
ripples.push(Ripple {
x: cursor.x,
y: cursor.y,
start_time: Instant::now(),
});
}
last_cursors.insert(k, cursor);
}
CustomEvent::Exit => {
*control_flow = ControlFlow::Exit;
}
_ => {}
},
_ => (),
}
});
}