diff --git a/Cargo.lock b/Cargo.lock index 551cbf050..d301a80cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index fcc270a39..ccc7a9dac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index eda0e11cf..b2b190557 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -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"; diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index f29908d51..4a833a1bf 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -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; diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index dcccf8f7c..b47abdaf8 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -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) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 645002686..36ccca790 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -116,6 +116,7 @@ class FfiModel with ChangeNotifier { Timer? _timer; var _reconnects = 1; bool _viewOnly = false; + bool _showMyCursor = false; WeakReference 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 { diff --git a/libs/hbb_common b/libs/hbb_common index fa8f28977..d6b14975f 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit fa8f2897762d331c7c6ce3d99a34b8b7701d0c4b +Subproject commit d6b14975ffd35ed63528617a53795c479a6eaf13 diff --git a/src/client.rs b/src/client.rs index 4c2a3c315..e20aeceea 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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" { diff --git a/src/common.rs b/src/common.rs index ca2ed3cac..90384efa2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2040,6 +2040,22 @@ pub async fn get_ipv6_socket() -> Option<(Arc, 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::*; diff --git a/src/core_main.rs b/src/core_main.rs index 0caa706e7..fe2b6ece9 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -574,6 +574,12 @@ pub fn core_main() -> Option> { 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")] diff --git a/src/ipc.rs b/src/ipc.rs index 98db30eb1..9ad7f8445 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -177,7 +177,7 @@ pub enum DataPortableService { Ping, Pong, ConnCount(Option), - Mouse((Vec, i32)), + Mouse((Vec, i32, String, u32, bool, bool)), Pointer((Vec, i32)), Key(Vec), RequestStart, @@ -289,6 +289,8 @@ pub enum Data { #[cfg(target_os = "windows")] PortForwardSessionCount(Option), SocksWs(Option, String)>>), + #[cfg(target_os = "windows")] + Whiteboard((String, crate::whiteboard::CustomEvent)), } #[tokio::main(flavor = "current_thread")] diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 582cdcf49..b7e01b84d 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -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(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 61a4ed6c3..b20bd75a5 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -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(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 5b71674d3..6ce8c13ea 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -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(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 66067b261..4be5bcdec 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -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(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index ef28c34fc..080af0f3a 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -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(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index dc4e0f214..81cb50422 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -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(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index b180c5856..eb0bd426d 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -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(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 5f5fffb71..9ff401c61 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -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(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 56704fb39..4adbb566a 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -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(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 3447df9f4..b7ee142fe 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -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(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 471d7bd73..30613d7cf 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -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(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 507b580c4..ff6492004 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -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(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 769c3788f..a6ea2706a 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -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(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 6871b5fad..ebb335622 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -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(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 384199cd7..6759d23a6 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -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(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 168752abc..f3c0b718a 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -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(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 8d54091c5..7d00bcc4e 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -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(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 8339b16f2..937ed3633 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -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(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index fe22768f7..02067a8b1 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -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(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index ed179729e..7cd720641 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -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(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 7de119980..82eaf56fc 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -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(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 82121d13b..430d37574 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -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(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 2f60302b1..b4b94841d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -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(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a48f7c946..8e80a1b9d 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -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(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 72df3e737..ea176b36c 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -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(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 1b4beb301..18bb4be8d 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -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(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 6b0c4f29d..31298140c 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -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(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 51c6230e3..69a8ef7de 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -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(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 7500c5c20..8d99112c0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -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(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index bbfd26593..9fa563aa0 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -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(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 1a41dc307..c94c5bedf 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -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(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 93eb232da..41cbf4927 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -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(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 30fd697cb..5fe4c561d 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -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(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 73a7161bd..b1d5f62f6 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -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(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index c32168c70..af1c5cf6f 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -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(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 021b6dabe..4032f0b65 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -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(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index a8a1a061f..e7ae5b74b 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -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(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index f26db2360..6a25605e3 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -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(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 9e495ba01..c8dc430a3 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -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(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index e642180d9..dc4d5e855 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -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(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index d1f778835..75ec6de42 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -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(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 671491695..f5e737679 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -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(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 28b649daa..d3c8c557f 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -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(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 1c9365c6b..7e5aa9f0c 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -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(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 8ea7805aa..7254b29ea 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -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(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index e6faa4f31..b5322abfc 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -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(); } diff --git a/src/lib.rs b/src/lib.rs index 433bb5f36..c85e13d9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/server/connection.rs b/src/server/connection.rs index bc4708f5f..99f1a539e 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -126,9 +126,18 @@ pub struct ConnInner { tx_video: Option, } +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>>, 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)); + } } } } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 8573e9c7e..069a2f821 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -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(); diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 4a4eaaad1..6f5695046 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -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); } } diff --git a/src/whiteboard.rs b/src/whiteboard.rs new file mode 100644 index 000000000..e6c288ab5 --- /dev/null +++ b/src/whiteboard.rs @@ -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>> = + RwLock::new(None); + static ref TX_WHITEBOARD: RwLock>> = + RwLock::new(None); + static ref CONNS: RwLock> = 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, + 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> { + 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 = Vec::new(); + let mut last_cursors: HashMap = 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; + } + _ => {} + }, + _ => (), + } + }); +}