mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 14:07:28 +08:00
feat: show my cursor (#12745)
Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
235
Cargo.lock
generated
235
Cargo.lock
generated
@@ -224,12 +224,24 @@ dependencies = [
|
||||
"x11rb 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "async-broadcast"
|
||||
version = "0.5.1"
|
||||
@@ -751,9 +763,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.21.0"
|
||||
version = "1.23.2"
|
||||
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]]
|
||||
name = "byteorder"
|
||||
@@ -1380,6 +1406,15 @@ dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core_maths"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.11.3"
|
||||
@@ -1515,6 +1550,12 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b"
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.4.4"
|
||||
@@ -1943,6 +1984,45 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "dtoa"
|
||||
version = "0.4.8"
|
||||
@@ -2340,6 +2420,29 @@ dependencies = [
|
||||
"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]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -3929,6 +4032,12 @@ version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
@@ -4017,6 +4126,15 @@ version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.6.5"
|
||||
@@ -4730,6 +4848,7 @@ checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"block2 0.5.1",
|
||||
"dispatch",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
@@ -6007,6 +6126,12 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "2.1.0"
|
||||
@@ -6117,6 +6242,7 @@ dependencies = [
|
||||
"arboard",
|
||||
"async-process",
|
||||
"async-trait",
|
||||
"bytemuck",
|
||||
"bytes",
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -6142,6 +6268,7 @@ dependencies = [
|
||||
"evdev",
|
||||
"flutter_rust_bridge",
|
||||
"fon",
|
||||
"fontdb",
|
||||
"fruitbasket",
|
||||
"gtk",
|
||||
"hbb_common",
|
||||
@@ -6189,6 +6316,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"shared_memory",
|
||||
"shutdown_hooks",
|
||||
"softbuffer",
|
||||
"stunclient",
|
||||
"sys-locale",
|
||||
"system_shutdown",
|
||||
@@ -6196,8 +6324,10 @@ dependencies = [
|
||||
"tauri-winrt-notification",
|
||||
"terminfo",
|
||||
"termios 0.3.3",
|
||||
"tiny-skia",
|
||||
"totp-rs",
|
||||
"tray-icon",
|
||||
"ttf-parser",
|
||||
"url",
|
||||
"users 0.11.0",
|
||||
"uuid",
|
||||
@@ -6738,6 +6868,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
@@ -6787,6 +6926,39 @@ dependencies = [
|
||||
"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]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -6808,6 +6980,12 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
|
||||
|
||||
[[package]]
|
||||
name = "strict-num"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
@@ -7290,6 +7468,45 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.1"
|
||||
@@ -7665,6 +7882,15 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "tungstenite"
|
||||
version = "0.26.2"
|
||||
@@ -8147,6 +8373,7 @@ checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"log",
|
||||
"once_cell",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
@@ -9085,7 +9312,11 @@ version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"gethostname 0.4.3",
|
||||
"libc",
|
||||
"libloading 0.8.4",
|
||||
"once_cell",
|
||||
"rustix 0.38.34",
|
||||
"x11rb-protocol 0.13.1",
|
||||
]
|
||||
|
||||
@@ -134,6 +134,11 @@ impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system
|
||||
shared_memory = "0.12"
|
||||
tauri-winrt-notification = "0.1"
|
||||
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]
|
||||
objc = "0.2"
|
||||
|
||||
@@ -172,6 +172,7 @@ const kHideUsernameOnCard = "hide-username-on-card";
|
||||
const String kOptionHideHelpCards = "hide-help-cards";
|
||||
|
||||
const String kOptionToggleViewOnly = "view-only";
|
||||
const String kOptionToggleShowMyCursor = "show-my-cursor";
|
||||
|
||||
const String kOptionDisableFloatingWindow = "disable-floating-window";
|
||||
|
||||
|
||||
@@ -1593,6 +1593,7 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
inputSource(),
|
||||
Divider(),
|
||||
viewMode(),
|
||||
if (pi.platform == kPeerPlatformWindows) showMyCursor(),
|
||||
Divider(),
|
||||
...toolbarToggles(),
|
||||
...mouseSpeed(),
|
||||
@@ -1749,12 +1750,36 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
final viewOnly = await bind.sessionGetToggleOption(
|
||||
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
|
||||
ffiModel.setViewOnly(id, viewOnly ?? value);
|
||||
final showMyCursor = await bind.sessionGetToggleOption(
|
||||
sessionId: ffi.sessionId, arg: kOptionToggleShowMyCursor);
|
||||
ffiModel.setShowMyCursor(showMyCursor ?? value);
|
||||
}
|
||||
: null,
|
||||
ffi: ffi,
|
||||
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() {
|
||||
if (pi.platform != kPeerPlatformAndroid) return [];
|
||||
final enabled = versionCmp(pi.version, '1.2.7') >= 0;
|
||||
|
||||
@@ -371,6 +371,7 @@ class InputModel {
|
||||
String get id => parent.target?.id ?? '';
|
||||
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
||||
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
|
||||
bool get showMyCursor => parent.target!.ffiModel.showMyCursor;
|
||||
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
|
||||
int get trackpadSpeed => _trackpadSpeed;
|
||||
@@ -876,7 +877,7 @@ class InputModel {
|
||||
|
||||
void onPointHoverImage(PointerHoverEvent e) {
|
||||
_stopFling = true;
|
||||
if (isViewOnly) return;
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (!isPhysicalMouse.value) {
|
||||
isPhysicalMouse.value = true;
|
||||
@@ -1037,7 +1038,7 @@ class InputModel {
|
||||
if (isDesktop) _queryOtherWindowCoords = true;
|
||||
_remoteWindowCoords = [];
|
||||
_windowRect = null;
|
||||
if (isViewOnly) return;
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (isViewCamera) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
if (isPhysicalMouse.value) {
|
||||
@@ -1051,7 +1052,7 @@ class InputModel {
|
||||
|
||||
void onPointUpImage(PointerUpEvent e) {
|
||||
if (isDesktop) _queryOtherWindowCoords = false;
|
||||
if (isViewOnly) return;
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (isViewCamera) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (isPhysicalMouse.value) {
|
||||
@@ -1060,7 +1061,7 @@ class InputModel {
|
||||
}
|
||||
|
||||
void onPointMoveImage(PointerMoveEvent e) {
|
||||
if (isViewOnly) return;
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (isViewCamera) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_queryOtherWindowCoords) {
|
||||
|
||||
@@ -116,6 +116,7 @@ class FfiModel with ChangeNotifier {
|
||||
Timer? _timer;
|
||||
var _reconnects = 1;
|
||||
bool _viewOnly = false;
|
||||
bool _showMyCursor = false;
|
||||
WeakReference<FFI> parent;
|
||||
late final SessionID sessionId;
|
||||
|
||||
@@ -154,6 +155,7 @@ class FfiModel with ChangeNotifier {
|
||||
bool get isPeerMobile => isPeerAndroid;
|
||||
|
||||
bool get viewOnly => _viewOnly;
|
||||
bool get showMyCursor => _showMyCursor;
|
||||
|
||||
set inputBlocked(v) {
|
||||
_inputBlocked = v;
|
||||
@@ -1144,6 +1146,8 @@ class FfiModel with ChangeNotifier {
|
||||
peerId,
|
||||
bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionToggleViewOnly));
|
||||
setShowMyCursor(bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionToggleShowMyCursor));
|
||||
}
|
||||
if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) {
|
||||
final platformAdditions = evt['platform_additions'];
|
||||
@@ -1494,6 +1498,13 @@ class FfiModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void setShowMyCursor(bool value) {
|
||||
if (_showMyCursor != value) {
|
||||
_showMyCursor = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ImageModel with ChangeNotifier {
|
||||
|
||||
Submodule libs/hbb_common updated: fa8f289776...d6b14975ff
@@ -2132,7 +2132,19 @@ impl LoginConfigHandler {
|
||||
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.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 {
|
||||
let is_set = self
|
||||
.options
|
||||
@@ -2225,6 +2237,9 @@ impl LoginConfigHandler {
|
||||
if view_only || self.get_toggle_option("show-remote-cursor") {
|
||||
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") {
|
||||
msg.follow_remote_cursor = BoolOption::Yes.into();
|
||||
}
|
||||
@@ -2309,6 +2324,8 @@ impl LoginConfigHandler {
|
||||
self.config.allow_swap_key.v
|
||||
} else if name == "view-only" {
|
||||
self.config.view_only.v
|
||||
} else if name == "show-my-cursor" {
|
||||
self.config.show_my_cursor.v
|
||||
} else if name == "follow-remote-cursor" {
|
||||
self.config.follow_remote_cursor.v
|
||||
} else if name == "follow-remote-window" {
|
||||
|
||||
@@ -2040,6 +2040,22 @@ pub async fn get_ipv6_socket() -> Option<(Arc<UdpSocket>, bytes::Bytes)> {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -574,6 +574,12 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
crate::flutter::connection_manager::start_cm_no_ui();
|
||||
}
|
||||
return None;
|
||||
} else if args[0] == "--whiteboard" {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
crate::whiteboard::run();
|
||||
}
|
||||
return None;
|
||||
} else if args[0] == "-gtk-sudo" {
|
||||
// rustdesk service kill `rustdesk --` processes
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -177,7 +177,7 @@ pub enum DataPortableService {
|
||||
Ping,
|
||||
Pong,
|
||||
ConnCount(Option<usize>),
|
||||
Mouse((Vec<u8>, i32)),
|
||||
Mouse((Vec<u8>, i32, String, u32, bool, bool)),
|
||||
Pointer((Vec<u8>, i32)),
|
||||
Key(Vec<u8>),
|
||||
RequestStart,
|
||||
@@ -289,6 +289,8 @@ pub enum Data {
|
||||
#[cfg(target_os = "windows")]
|
||||
PortForwardSessionCount(Option<usize>),
|
||||
SocksWs(Option<Box<(Option<config::Socks5Server>, String)>>),
|
||||
#[cfg(target_os = "windows")]
|
||||
Whiteboard((String, crate::whiteboard::CustomEvent)),
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
|
||||
@@ -708,6 +708,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Failed to check if the user is an administrator.", "فشل التحقق مما إذا كان المستخدم لديه صلاحيات المسؤول."),
|
||||
("Supported only in the installed version.", "مدعوم فقط في النسخة المُثبتة."),
|
||||
("elevation_username_tip", "يرجى إدخال اسم مستخدم بصلاحيات المسؤول للمتابعة."),
|
||||
("Preparing for installation ...", "جارٍ التحضير للتثبيت...")
|
||||
("Preparing for installation ...", "جارٍ التحضير للتثبيت..."),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", "仅在以安装版本受支持。"),
|
||||
("elevation_username_tip", "输入用户名或域名\\用户名"),
|
||||
("Preparing for installation ...", "准备安装..."),
|
||||
("Show my cursor", "显示我的光标"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
("elevation_username_tip", "Geben Sie Benutzername oder Domäne\\Benutzername ein"),
|
||||
("Preparing for installation ...", "Installation wird vorbereitet …"),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", "فقط در نسخه نصبشده پشتیبانی میشود."),
|
||||
("elevation_username_tip", "لطفاً نام کاربری مدیریتی را برای ارتقاء دسترسی وارد کنید."),
|
||||
("Preparing for installation ...", "در حال آمادهسازی برای نصب..."),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
("elevation_username_tip", "Saisissez un nom d’utilisateur ou un domaine\\utilisateur"),
|
||||
("Preparing for installation ...", "Préparation de l’installation…"),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", "נתמך רק בגרסה המותקנת"),
|
||||
("elevation_username_tip", "רמז_ליוזר_להעלאת_הרשאה"),
|
||||
("Preparing for installation ...", "הכנה להתקנה..."),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
("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 ..."),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
("elevation_username_tip", "Inserisci Nome utente o dominio sorgente\\nome Utente"),
|
||||
("Preparing for installation ...", "Preparazione per l'installazione..."),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", "インストールされたバージョンでのみサポートされます。"),
|
||||
("elevation_username_tip", "ユーザー名またはドメインのユーザー名を入力してください。"),
|
||||
("Preparing for installation ...", "インストールの準備中です..."),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", "설치된 버전에서만 지원됩니다."),
|
||||
("elevation_username_tip", "사용자 이름 또는 도메인\\사용자 이름 입력"),
|
||||
("Preparing for installation ...", "설치 준비 중 ..."),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
("elevation_username_tip", "Voer je gebruikersnaam of domeinnaam in"),
|
||||
("Preparing for installation ...", "Installatie voorbereiden ..."),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
("elevation_username_tip", "Podaj nazwę użytkownika lub domena\\użytkownik"),
|
||||
("Preparing for installation ...", "Przygotowywanie do instalacji ..."),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", "Поддерживается только в установочной версии."),
|
||||
("elevation_username_tip", "Введите пользователя или домен\\пользователя"),
|
||||
("Preparing for installation ...", "Подготовка к установке..."),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
("elevation_username_tip", "Inserta Nùmene utente o domìniu de fonte\\nùmene Utente"),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", "僅支援於已安裝的版本"),
|
||||
("elevation_username_tip", "輸入使用者名稱或網域\\使用者名稱"),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -709,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@ pub mod plugin;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
mod tray;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod whiteboard;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
mod updater;
|
||||
|
||||
|
||||
@@ -126,9 +126,18 @@ pub struct ConnInner {
|
||||
tx_video: Option<Sender>,
|
||||
}
|
||||
|
||||
struct InputMouse {
|
||||
msg: MouseEvent,
|
||||
conn_id: i32,
|
||||
username: String,
|
||||
argb: u32,
|
||||
simulate: bool,
|
||||
show_cursor: bool,
|
||||
}
|
||||
|
||||
enum MessageInput {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
Mouse((MouseEvent, i32)),
|
||||
Mouse(InputMouse),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
Key((KeyEvent, bool)),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -225,6 +234,9 @@ pub struct Connection {
|
||||
// by peer
|
||||
disable_keyboard: bool,
|
||||
// by peer
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
show_my_cursor: bool,
|
||||
// by peer
|
||||
disable_clipboard: bool,
|
||||
// by peer
|
||||
disable_audio: bool,
|
||||
@@ -240,6 +252,7 @@ pub struct Connection {
|
||||
server_audit_conn: String,
|
||||
server_audit_file: String,
|
||||
lr: LoginRequest,
|
||||
peer_argb: u32,
|
||||
session_last_recv_time: Option<Arc<Mutex<Instant>>>,
|
||||
chat_unanswered: bool,
|
||||
file_transferred: bool,
|
||||
@@ -403,11 +416,14 @@ impl Connection {
|
||||
enable_file_transfer: false,
|
||||
disable_clipboard: false,
|
||||
disable_keyboard: false,
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
show_my_cursor: false,
|
||||
tx_input,
|
||||
video_ack_required: false,
|
||||
server_audit_conn: "".to_owned(),
|
||||
server_audit_file: "".to_owned(),
|
||||
lr: Default::default(),
|
||||
peer_argb: 0u32,
|
||||
session_last_recv_time: None,
|
||||
chat_unanswered: false,
|
||||
file_transferred: false,
|
||||
@@ -938,8 +954,15 @@ impl Connection {
|
||||
loop {
|
||||
match receiver.recv_timeout(std::time::Duration::from_millis(500)) {
|
||||
Ok(v) => match v {
|
||||
MessageInput::Mouse((msg, id)) => {
|
||||
handle_mouse(&msg, id);
|
||||
MessageInput::Mouse(mouse_input) => {
|
||||
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)) => {
|
||||
// Set the press state to false, use `down` only in `handle_key()`.
|
||||
@@ -1784,8 +1807,25 @@ impl Connection {
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn input_mouse(&self, msg: MouseEvent, conn_id: i32) {
|
||||
self.tx_input.send(MessageInput::Mouse((msg, conn_id))).ok();
|
||||
fn input_mouse(
|
||||
&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]
|
||||
@@ -1900,6 +1940,7 @@ impl Connection {
|
||||
|
||||
async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) {
|
||||
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() {
|
||||
self.options_in_login = Some(o.clone());
|
||||
}
|
||||
@@ -2279,7 +2320,23 @@ impl Connection {
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
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();
|
||||
}
|
||||
@@ -3640,6 +3697,18 @@ impl Connection {
|
||||
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) {
|
||||
@@ -4792,6 +4861,11 @@ mod raii {
|
||||
scrap::wayland::pipewire::try_close_session();
|
||||
}
|
||||
Self::check_wake_lock();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::whiteboard;
|
||||
whiteboard::unregister_whiteboard(whiteboard::get_key_cursor(self.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
use super::rdp_input::client::{RdpInputKeyboard, RdpInputMouse};
|
||||
use super::*;
|
||||
use crate::input::*;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::whiteboard;
|
||||
#[cfg(target_os = "macos")]
|
||||
use dispatch::Queue;
|
||||
use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable};
|
||||
@@ -698,18 +700,25 @@ fn get_modifier_state(key: Key, en: &mut Enigo) -> bool {
|
||||
}
|
||||
|
||||
#[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")]
|
||||
{
|
||||
// having GUI (--server has tray, it is GUI too), run main GUI thread, otherwise crash
|
||||
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;
|
||||
}
|
||||
#[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))]
|
||||
handle_mouse_(evt, conn);
|
||||
handle_mouse_(evt, conn, username, argb, simulate, show_cursor);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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")]
|
||||
fn handle_scale(scale: i32) {
|
||||
let mut en = ENIGO.lock().unwrap();
|
||||
|
||||
@@ -476,9 +476,9 @@ pub mod server {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Mouse((v, conn)) => {
|
||||
Mouse((v, conn, username, argb, simulate, show_cursor)) => {
|
||||
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)) => {
|
||||
@@ -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![];
|
||||
evt.write_to_vec(&mut v)?;
|
||||
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() {
|
||||
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 {
|
||||
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
731
src/whiteboard.rs
Normal 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;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user