From 6c949a9602b9985705eaff7644f1cd1dd1d39af7 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 6 Sep 2025 12:11:43 +0800 Subject: [PATCH] feat: cursor, linux (#12822) * feat: cursor, linux Signed-off-by: fufesou * refact: cursor, text, white background Signed-off-by: fufesou --------- Signed-off-by: fufesou --- Cargo.lock | 419 ++++++++++++++++- Cargo.toml | 11 +- .../lib/desktop/widgets/remote_toolbar.dart | 4 +- src/core_main.rs | 2 +- src/ipc.rs | 2 +- src/lib.rs | 2 +- src/server/connection.rs | 25 +- src/server/input_service.rs | 6 +- src/whiteboard/linux.rs | 426 ++++++++++++++++++ src/whiteboard/macos.rs | 58 ++- src/whiteboard/mod.rs | 4 + src/whiteboard/server.rs | 69 ++- src/whiteboard/win_linux.rs | 180 ++++++++ src/whiteboard/windows.rs | 154 +------ 14 files changed, 1153 insertions(+), 209 deletions(-) create mode 100644 src/whiteboard/linux.rs create mode 100644 src/whiteboard/win_linux.rs diff --git a/Cargo.lock b/Cargo.lock index bcd61122c..5cd90655c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "addr2line" version = "0.22.0" @@ -39,6 +55,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if 1.0.0", + "getrandom 0.3.2", + "once_cell", + "version_check", + "zerocopy 0.8.26", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -96,6 +125,33 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.9.1", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk 0.9.0", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum 0.7.2", + "thiserror 1.0.61", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -866,6 +922,32 @@ dependencies = [ "system-deps 6.2.2", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.9.1", + "log", + "polling 3.7.2", + "rustix 0.38.34", + "slab", + "thiserror 1.0.61", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.34", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cc" version = "1.2.13" @@ -1584,6 +1666,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "dart-sys" version = "4.1.5" @@ -2568,7 +2656,7 @@ dependencies = [ "nix 0.29.0", "page_size", "smallvec", - "zerocopy 0.8.14", + "zerocopy 0.8.26", ] [[package]] @@ -3212,7 +3300,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.8", ] [[package]] @@ -3987,6 +4075,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.1", "libc", + "redox_syscall 0.5.2", ] [[package]] @@ -4380,6 +4469,21 @@ dependencies = [ "thiserror 1.0.61", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum 0.7.2", + "raw-window-handle 0.6.2", + "thiserror 1.0.61", +] + [[package]] name = "ndk-context" version = "0.1.1" @@ -4404,6 +4508,15 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + [[package]] name = "netlink-packet-core" version = "0.5.0" @@ -4834,6 +4947,30 @@ dependencies = [ "objc2-quartz-core", ] +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -4858,6 +4995,18 @@ dependencies = [ "objc2-metal", ] +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation", +] + [[package]] name = "objc2-encode" version = "2.0.0-pre.2" @@ -4886,6 +5035,18 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-metal" version = "0.2.2" @@ -4911,6 +5072,61 @@ dependencies = [ "objc2-metal", ] +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation", +] + [[package]] name = "objc_exception" version = "0.1.2" @@ -5017,6 +5233,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libredox", +] + [[package]] name = "ordered-multimap" version = "0.4.3" @@ -5087,6 +5312,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + [[package]] name = "page_size" version = "0.6.0" @@ -5823,7 +6057,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.14", + "zerocopy 0.8.26", ] [[package]] @@ -6397,6 +6631,7 @@ dependencies = [ "winapi 0.3.9", "windows 0.61.1", "windows-service", + "winit", "winreg 0.11.0", "winres", "wol-rs", @@ -6637,6 +6872,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "security-framework" version = "2.10.0" @@ -6944,6 +7192,40 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.9.1", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.34", + "thiserror 1.0.61", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde 1.0.203", +] + [[package]] name = "socket2" version = "0.3.19" @@ -8373,12 +8655,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if 1.0.0", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] @@ -8441,6 +8724,28 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.9.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +dependencies = [ + "rustix 0.38.34", + "wayland-client", + "xcursor", +] + [[package]] name = "wayland-protocols" version = "0.32.3" @@ -8453,6 +8758,19 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79f2d57c7fcc6ab4d602adba364bf59a5c24de57bd194486bf9b8360e06bfc4" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + [[package]] name = "wayland-protocols-wlr" version = "0.3.3" @@ -8491,9 +8809,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -9277,6 +9595,58 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winit" +version = "0.30.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0" +dependencies = [ + "ahash 0.8.12", + "android-activity", + "atomic-waker", + "bitflags 2.9.1", + "block2 0.5.1", + "bytemuck", + "calloop", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk 0.9.0", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle 0.6.2", + "redox_syscall 0.4.1", + "rustix 0.38.34", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb 0.13.1", + "xkbcommon-dl", +] + [[package]] name = "winnow" version = "0.5.40" @@ -9459,6 +9829,12 @@ dependencies = [ "rustix 0.38.34", ] +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + [[package]] name = "xdg-home" version = "1.2.0" @@ -9469,6 +9845,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.9.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "zbus" version = "3.15.2" @@ -9547,11 +9942,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.14" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "zerocopy-derive 0.8.14", + "zerocopy-derive 0.8.26", ] [[package]] @@ -9567,9 +9962,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.14" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2 1.0.93", "quote 1.0.36", diff --git a/Cargo.toml b/Cargo.toml index 7ec9d418c..57d949e57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,11 +134,6 @@ 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" @@ -164,6 +159,11 @@ keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" } [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" } +tiny-skia = "0.11" +softbuffer = "0.4" +fontdb = "0.23" +bytemuck = "1.23" +ttf-parser = "0.25" [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] # https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support @@ -190,6 +190,7 @@ nix = { version = "0.29", features = ["term", "process"]} gtk = "0.18" termios = "0.3" terminfo = "0.8" +winit = "0.30" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13" diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 5753c14fa..14b1fcd22 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1593,8 +1593,8 @@ class _KeyboardMenu extends StatelessWidget { inputSource(), Divider(), viewMode(), - if (pi.platform == kPeerPlatformWindows || - pi.platform == kPeerPlatformMacOS) + if ([kPeerPlatformWindows, kPeerPlatformMacOS, kPeerPlatformLinux] + .contains(pi.platform)) showMyCursor(), Divider(), ...toolbarToggles(), diff --git a/src/core_main.rs b/src/core_main.rs index c6dcac0a9..114f0d68b 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -575,7 +575,7 @@ pub fn core_main() -> Option> { } return None; } else if args[0] == "--whiteboard" { - #[cfg(any(target_os = "windows", target_os = "macos"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { crate::whiteboard::run(); } diff --git a/src/ipc.rs b/src/ipc.rs index 4962c6817..21af59e99 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -289,7 +289,7 @@ pub enum Data { #[cfg(target_os = "windows")] PortForwardSessionCount(Option), SocksWs(Option, String)>>), - #[cfg(any(target_os = "windows", target_os = "macos"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Whiteboard((String, crate::whiteboard::CustomEvent)), } diff --git a/src/lib.rs b/src/lib.rs index 02ab0fb42..1f5061015 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,7 @@ pub mod plugin; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod tray; -#[cfg(any(target_os = "windows", target_os = "macos"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] mod whiteboard; #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/server/connection.rs b/src/server/connection.rs index c28e5bee2..2dfa52df3 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3699,24 +3699,35 @@ impl Connection { self.update_terminal_persistence(q == BoolOption::Yes).await; } } - #[cfg(any(target_os = "windows", target_os = "macos"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Ok(q) = o.show_my_cursor.enum_value() { if q != BoolOption::NotSet { use crate::whiteboard; self.show_my_cursor = q == BoolOption::Yes; #[cfg(target_os = "windows")] - let is_win10_or_greater = crate::platform::windows::is_win_10_or_greater(); + let is_lower_win10 = !crate::platform::windows::is_win_10_or_greater(); #[cfg(not(target_os = "windows"))] - let is_win10_or_greater = false; + let is_lower_win10 = false; + #[cfg(target_os = "linux")] + let is_wayland = !crate::platform::linux::is_x11(); + #[cfg(not(target_os = "linux"))] + let is_wayland = false; + let not_support_msg = if is_lower_win10 { + "Windows 10 or greater is required." + } else if is_wayland { + "This feature is not supported on Wayland, please switch to X11." + } else { + "" + }; if q == BoolOption::Yes { - if !cfg!(target_os = "windows") || is_win10_or_greater { + if not_support_msg.is_empty() { whiteboard::register_whiteboard(whiteboard::get_key_cursor(self.inner.id)); } else { let mut msg_out = Message::new(); let res = MessageBox { msgtype: "nook-nocancel-hasclose".to_owned(), title: "Show my cursor".to_owned(), - text: "Windows 10 or greater is required.".to_owned(), + text: not_support_msg.to_owned(), link: "".to_owned(), ..Default::default() }; @@ -3724,7 +3735,7 @@ impl Connection { self.send(msg_out).await; } } else { - if !cfg!(target_os = "windows") || is_win10_or_greater { + if not_support_msg.is_empty() { whiteboard::unregister_whiteboard(whiteboard::get_key_cursor( self.inner.id, )); @@ -4884,7 +4895,7 @@ mod raii { scrap::wayland::pipewire::try_close_session(); } Self::check_wake_lock(); - #[cfg(any(target_os = "windows", target_os = "macos"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { 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 8cdab3e7b..6a6c6e3a6 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -2,7 +2,7 @@ use super::rdp_input::client::{RdpInputKeyboard, RdpInputMouse}; use super::*; use crate::input::*; -#[cfg(any(target_os = "windows", target_os = "macos"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::whiteboard; #[cfg(target_os = "macos")] use dispatch::Queue; @@ -1000,7 +1000,7 @@ pub fn handle_mouse_( if simulate { handle_mouse_simulation_(evt, conn); } - #[cfg(any(target_os = "windows", target_os = "macos"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] if _show_cursor { handle_mouse_show_cursor_(evt, conn, _username, _argb); } @@ -1149,7 +1149,7 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) { } } -#[cfg(any(target_os = "windows", target_os = "macos"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] 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; diff --git a/src/whiteboard/linux.rs b/src/whiteboard/linux.rs new file mode 100644 index 000000000..806bf7848 --- /dev/null +++ b/src/whiteboard/linux.rs @@ -0,0 +1,426 @@ +use super::{ + server::{Ripple, EVENT_PROXY}, + win_linux::{create_font_face, draw_text}, + Cursor, CustomEvent, +}; +use hbb_common::{bail, log, tokio::sync::mpsc::unbounded_channel, ResultType}; +use softbuffer::{Context, Surface}; +use std::{ + collections::HashMap, + ffi::{c_int, c_short, c_ulong, c_ushort}, + num::NonZeroU32, + sync::Arc, + time::Instant, +}; +use tiny_skia::{Color, FillRule, Paint, PathBuilder, PixmapMut, Stroke, Transform}; +use ttf_parser::Face; +use winit::raw_window_handle::{ + DisplayHandle, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle, +}; +use winit::{ + application::ApplicationHandler, + dpi::{PhysicalPosition, PhysicalSize}, + event::WindowEvent, + event_loop::{ActiveEventLoop, EventLoop}, + platform::x11::{WindowAttributesExtX11, WindowType}, + window::{Window, WindowId, WindowLevel}, +}; + +enum _XDisplay {} +type Display = _XDisplay; + +type XID = c_ulong; +type XserverRegion = XID; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(C)] +pub struct XRectangle { + pub x: c_short, + pub y: c_short, + pub width: c_ushort, + pub height: c_ushort, +} + +#[link(name = "Xfixes")] +extern "C" { + fn XFixesCreateRegion( + dpy: *mut Display, + rectangles: *mut XRectangle, + nrectangles: c_int, + ) -> XserverRegion; + fn XFixesDestroyRegion(dpy: *mut Display, region: XserverRegion) -> (); + fn XFixesSetWindowShapeRegion( + dpy: *mut Display, + win: XID, + shape_kind: c_int, + x_off: c_int, + y_off: c_int, + region: XserverRegion, + ) -> (); +} + +const SHAPE_INPUT: std::ffi::c_int = 2; + +pub fn run() { + let event_loop = match EventLoop::<(String, CustomEvent)>::with_user_event().build() { + Ok(el) => el, + Err(e) => { + log::error!("Failed to create event loop: {}", e); + return; + } + }; + + let event_loop_proxy = event_loop.create_proxy(); + EVENT_PROXY.write().unwrap().replace(event_loop_proxy); + + let (tx_exit, rx_exit) = unbounded_channel(); + std::thread::spawn(move || { + super::server::start_ipc(rx_exit); + }); + + let mut app = match WhiteboardApplication::new(&event_loop) { + Ok(app) => app, + Err(e) => { + log::error!("Failed to create whiteboard application: {}", e); + tx_exit.send(()).ok(); + return; + } + }; + + if let Err(e) = event_loop.run_app(&mut app) { + log::error!("Failed to run app: {}", e); + tx_exit.send(()).ok(); + return; + } +} + +struct WindowState { + window: Arc, + // NOTE: This surface must be dropped before the `Window`. + surface: Surface, Arc>, + ripples: Vec, + last_cursors: HashMap, +} + +struct WhiteboardApplication { + windows: Vec, + // Drawing context. + // + // With OpenGL it could be EGLDisplay. + context: Option>>, + face: Option>, + close_requested: bool, +} + +impl WhiteboardApplication { + fn new(event_loop: &EventLoop) -> ResultType { + // https://github.com/rust-windowing/winit/blob/f6893a4390dfe6118ce4b33458d458fd3efd3025/examples/window.rs#L91 + // SAFETY: we drop the context right before the event loop is stopped, thus making it safe. + let context = match Context::new(unsafe { + std::mem::transmute::, DisplayHandle<'static>>( + event_loop.display_handle()?, + ) + }) { + Ok(ctx) => Some(ctx), + Err(e) => { + bail!("Failed to create context: {}", e); + } + }; + let face = match create_font_face() { + Ok(face) => Some(face), + Err(err) => { + log::error!("Failed to create font face: {}", err); + None + } + }; + Ok(Self { + windows: Vec::new(), + context, + face, + close_requested: false, + }) + } +} + +impl ApplicationHandler<(String, CustomEvent)> for WhiteboardApplication { + fn user_event(&mut self, _event_loop: &ActiveEventLoop, (k, evt): (String, CustomEvent)) { + match evt { + CustomEvent::Cursor(cursor) => { + if let Some(state) = self.windows.first_mut() { + if cursor.btns != 0 { + state.ripples.push(Ripple { + x: cursor.x, + y: cursor.y, + start_time: Instant::now(), + }); + } + state.last_cursors.insert(k, cursor); + state.window.request_redraw(); + } + } + CustomEvent::Exit => { + self.close_requested = true; + } + _ => {} + } + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let (x, y, w, h) = match super::server::get_displays_rect() { + Ok(r) => r, + Err(err) => { + log::error!("Failed to get displays rect: {}", err); + self.close_requested = true; + return; + } + }; + + let window_attributes = Window::default_attributes() + .with_title("RustDesk whiteboard") + .with_inner_size(PhysicalSize::new(w, h)) + .with_position(PhysicalPosition::new(x, y)) + .with_decorations(false) + .with_transparent(true) + .with_window_level(WindowLevel::AlwaysOnTop) + .with_x11_window_type(vec![WindowType::Dock]) + .with_override_redirect(true); + + let window = match event_loop.create_window(window_attributes) { + Ok(window) => Arc::new(window), + Err(e) => { + log::error!("Failed to create window: {}", e); + self.close_requested = true; + return; + } + }; + + let display = match window.display_handle() { + Ok(d) => d, + Err(e) => { + log::error!("Failed to get display handle: {}", e); + self.close_requested = true; + return; + } + }; + let rwh = match window.window_handle() { + Ok(w) => w, + Err(e) => { + log::error!("Failed to get window handle: {}", e); + self.close_requested = true; + return; + } + }; + + // Both the following block and `window.set_cursor_hittest(false)` in `draw()` are necessary to ensure cursor events are properly passed through the window. + // These issues may be related to winit X11 handling. + // https://github.com/rust-windowing/winit/issues/3509 + // https://github.com/rust-windowing/winit/issues/4120 + // If either block is removed, cursor events may not be passed through as expected. + // If you update winit, please revisit this workaround. + match (rwh.as_raw(), display.as_raw()) { + (RawWindowHandle::Xlib(xlib_window), RawDisplayHandle::Xlib(xlib_display)) => { + unsafe { + let xwindow = xlib_window.window; + if let Some(display_ptr) = xlib_display.display { + let xdisplay = display_ptr.as_ptr() as *mut Display; + // Mouse event passthrough + let empty_region = XFixesCreateRegion(xdisplay, std::ptr::null_mut(), 0); + if empty_region == 0 { + log::error!("XFixesCreateRegion failed: returned null region"); + } else { + XFixesSetWindowShapeRegion( + xdisplay, + xwindow, + SHAPE_INPUT, + 0, + 0, + empty_region, + ); + XFixesDestroyRegion(xdisplay, empty_region); + } + } + } + } + _ => { + log::error!("Unsupported windowing system for shape extension"); + self.close_requested = true; + return; + } + } + + let Some(ctx) = self.context.as_ref() else { + // unreachable + self.close_requested = true; + return; + }; + + let surface = match Surface::new(ctx, window.clone()) { + Ok(s) => s, + Err(e) => { + log::error!("Failed to create surface: {}", e); + self.close_requested = true; + return; + } + }; + + let state = WindowState { + window, + surface, + ripples: Vec::new(), + last_cursors: HashMap::new(), + }; + + self.windows.push(state); + } + + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + match event { + WindowEvent::CloseRequested => { + self.close_requested = true; + } + WindowEvent::RedrawRequested => { + let Some(state) = self.windows.iter_mut().find(|w| w.window.id() == window_id) + else { + log::error!("No window found for id: {:?}", window_id); + return; + }; + if let Err(err) = state.draw(&self.face) { + log::error!("Failed to draw window: {}", err); + } + } + _ => (), + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if !self.close_requested { + for state in self.windows.iter() { + state.window.request_redraw(); + } + } else { + event_loop.exit(); + } + } + + fn exiting(&mut self, _event_loop: &ActiveEventLoop) { + // We must drop the context here. + self.context = None; + } +} + +impl WindowState { + fn draw(&mut self, face: &Option>) -> ResultType<()> { + let (width, height) = { + let size = self.window.inner_size(); + (size.width, size.height) + }; + + let (Some(width), Some(height)) = (NonZeroU32::new(width), NonZeroU32::new(height)) else { + bail!("Invalid window size, {width}x{height}") + }; + if let Err(e) = self.surface.resize(width, height) { + bail!("Failed to resize surface: {}", e); + } + + let mut buffer = match self.surface.buffer_mut() { + Ok(buf) => buf, + Err(e) => { + bail!("Failed to get buffer: {}", e); + } + }; + + let Some(mut pixmap) = PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut buffer), + width.get(), + height.get(), + ) else { + bail!("Failed to create pixmap from buffer"); + }; + pixmap.fill(Color::TRANSPARENT); + + Ripple::retain_active(&mut self.ripples); + for ripple in &self.ripples { + let (radius, alpha) = ripple.get_radius_alpha(); + + let mut ripple_paint = Paint::default(); + // Note: The real color is bgra here. + ripple_paint.set_color_rgba8(64, 64, 255, (alpha * 128.0) as u8); + ripple_paint.anti_alias = true; + + let mut ripple_pb = PathBuilder::new(); + ripple_pb.push_circle(ripple.x, ripple.y, radius); + if let Some(path) = ripple_pb.finish() { + pixmap.fill_path( + &path, + &ripple_paint, + FillRule::Winding, + Transform::identity(), + None, + ); + } + } + + for cursor in self.last_cursors.values() { + let (x, y) = (cursor.x, cursor.y); + let size = 1.5f32; + + 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(); + let rgba = super::argb_to_rgba(cursor.argb); + arrow_paint.set_color_rgba8(rgba.2, rgba.1, rgba.0, rgba.3); + 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.0f32; + 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, + 14.0f32, + ); + }); + } + } + + self.window.pre_present_notify(); + + if let Err(e) = buffer.present() { + log::error!("Failed to present buffer: {}", e); + } + + self.window.set_cursor_hittest(false).ok(); + + Ok(()) + } +} diff --git a/src/whiteboard/macos.rs b/src/whiteboard/macos.rs index f3479361f..d1c28b57c 100644 --- a/src/whiteboard/macos.rs +++ b/src/whiteboard/macos.rs @@ -1,9 +1,12 @@ -use super::{server::EVENT_PROXY, Cursor, CustomEvent}; +use super::{server::EVENT_PROXY, Cursor, CustomEvent, Ripple}; use core_graphics::context::CGContextRef; use foreign_types::ForeignTypeRef; use hbb_common::{bail, log, ResultType}; use objc::{class, msg_send, runtime::Object, sel, sel_impl}; -use piet::{kurbo::BezPath, FontFamily, RenderContext, Text, TextLayoutBuilder}; +use piet::{ + kurbo::{BezPath, Point}, + FontFamily, RenderContext, Text, TextLayout, TextLayoutBuilder, +}; use piet_coregraphics::{CoreGraphicsContext, CoreGraphicsTextLayout}; use std::{collections::HashMap, sync::Arc, time::Instant}; use tao::{ @@ -27,12 +30,6 @@ struct WindowState { display_origin: (f64, f64), } -struct Ripple { - x: f64, - y: f64, - start_time: Instant, -} - struct CursorInfo { window_id: WindowId, text_key: (String, u32), @@ -144,23 +141,14 @@ fn draw_cursors( context.clear(None, piet::Color::TRANSPARENT); if let Some(ripples) = window_ripples.get_mut(&window_id) { - let ripple_duration = std::time::Duration::from_millis(500); - ripples.retain_mut(|ripple| { - let elapsed = ripple.start_time.elapsed(); - let progress = - elapsed.as_secs_f64() / ripple_duration.as_secs_f64(); - let radius = 25.0 * progress; - let alpha = 1.0 - progress; - if alpha > 0.0 { - let color = piet::Color::rgba(1.0, 0.5, 0.5, alpha); - let circle = - piet::kurbo::Circle::new((ripple.x, ripple.y), radius); - context.stroke(circle, &color, 2.0); - true - } else { - false - } - }); + Ripple::retain_active(ripples); + for ripple in ripples.iter() { + let (radius, alpha) = ripple.get_radius_alpha(); + let color = piet::Color::rgba(1.0, 0.25, 0.25, alpha * 0.5); + let circle = + piet::kurbo::Circle::new((ripple.x, ripple.y), radius); + context.stroke(circle, &color, 2.0); + } } for info in last_cursors.values() { @@ -181,26 +169,34 @@ fn draw_cursors( pb.line_to((x + 6.0 * size, y + 12.0 * size)); pb.line_to((x + 11.0 * size, y + 12.0 * size)); - let color = piet::Color::rgba8( - (cursor.argb >> 16 & 0xFF) as u8, - (cursor.argb >> 8 & 0xFF) as u8, - (cursor.argb & 0xFF) as u8, - (cursor.argb >> 24 & 0xFF) as u8, - ); + let rgba = super::argb_to_rgba(cursor.argb); + let color = piet::Color::rgba8(rgba.0, rgba.1, rgba.2, rgba.3); context.fill(pb, &color); let pos = (x + CURSOR_TEXT_OFFSET * size, y + CURSOR_TEXT_OFFSET * size); + let get_rounded_rect = |layout: &CoreGraphicsTextLayout| { + let text_pos = Point::new(pos.0, pos.1); + let padded_bounds = (layout.image_bounds() + + text_pos.to_vec2()) + .inflate(3.0, 3.0); + padded_bounds.to_rounded_rect(5.0) + }; + if let Some(layout) = map_cursor_text.get(&info.text_key) { + context.fill(get_rounded_rect(layout), &piet::Color::WHITE); context.draw_text(layout, pos); } else { let text = context.text(); + let color = piet::Color::rgba8(0, 0, 0, 255); if let Ok(layout) = text .new_text_layout(cursor.text.clone()) .font(FontFamily::SYSTEM_UI, CURSOR_TEXT_FONT_SIZE) .text_color(color) .build() { + context + .fill(get_rounded_rect(&layout), &piet::Color::WHITE); context.draw_text(&layout, pos); map_cursor_text.insert(info.text_key.clone(), layout); } diff --git a/src/whiteboard/mod.rs b/src/whiteboard/mod.rs index e3fa13042..00d2d7791 100644 --- a/src/whiteboard/mod.rs +++ b/src/whiteboard/mod.rs @@ -5,8 +5,12 @@ mod server; #[cfg(target_os = "windows")] mod windows; +#[cfg(target_os = "linux")] +mod linux; #[cfg(target_os = "macos")] mod macos; +#[cfg(any(target_os = "windows", target_os = "linux"))] +mod win_linux; #[cfg(target_os = "windows")] use windows::create_event_loop; diff --git a/src/whiteboard/server.rs b/src/whiteboard/server.rs index 443633629..040110598 100644 --- a/src/whiteboard/server.rs +++ b/src/whiteboard/server.rs @@ -1,29 +1,43 @@ -use super::{create_event_loop, CustomEvent}; +use super::CustomEvent; use crate::ipc::{new_listener, Connection, Data}; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use hbb_common::tokio::sync::mpsc::unbounded_channel; #[cfg(any(target_os = "windows", target_os = "linux"))] use hbb_common::ResultType; use hbb_common::{ allow_err, log, - tokio::{ - self, - sync::mpsc::{unbounded_channel, UnboundedReceiver}, - }, + tokio::{self, sync::mpsc::UnboundedReceiver}, }; use lazy_static::lazy_static; use std::sync::RwLock; +use std::time::{Duration, Instant}; + +#[cfg(any(target_os = "windows", target_os = "macos"))] use tao::event_loop::EventLoopProxy; +#[cfg(target_os = "linux")] +use winit::event_loop::EventLoopProxy; lazy_static! { pub(super) static ref EVENT_PROXY: RwLock>> = RwLock::new(None); } +const RIPPLE_DURATION: Duration = Duration::from_millis(500); +#[cfg(target_os = "macos")] +type RippleFloat = f64; +#[cfg(any(target_os = "windows", target_os = "linux"))] +type RippleFloat = f32; + +#[cfg(target_os = "linux")] +pub use super::linux::run; + +#[cfg(any(target_os = "windows", target_os = "macos"))] 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() { + if let Err(e) = super::create_event_loop() { log::error!("Failed to create event loop: {}", e); tx_exit.send(()).ok(); return; @@ -31,7 +45,7 @@ pub fn run() { } #[tokio::main(flavor = "current_thread")] -async fn start_ipc(mut rx_exit: UnboundedReceiver<()>) { +pub(super) async fn start_ipc(mut rx_exit: UnboundedReceiver<()>) { match new_listener("_whiteboard").await { Ok(mut incoming) => loop { tokio::select! { @@ -82,9 +96,7 @@ async fn handle_new_stream(mut conn: Connection) { }); } } - _ => { - - } + _ => {} } } Ok(None) => { @@ -120,3 +132,40 @@ pub(super) fn get_displays_rect() -> ResultType<(i32, i32, u32, u32)> { let (w, h) = ((max_x - min_x) as u32, (max_y - min_y) as u32); Ok((x, y, w, h)) } + +#[inline] +pub(super) fn argb_to_rgba(argb: u32) -> (u8, u8, u8, u8) { + ( + (argb >> 16 & 0xFF) as u8, + (argb >> 8 & 0xFF) as u8, + (argb & 0xFF) as u8, + (argb >> 24 & 0xFF) as u8, + ) +} + +pub(super) struct Ripple { + pub x: RippleFloat, + pub y: RippleFloat, + pub start_time: Instant, +} + +impl Ripple { + #[inline] + pub fn retain_active(ripples: &mut Vec) { + ripples.retain(|r| r.start_time.elapsed() < RIPPLE_DURATION); + } + + pub fn get_radius_alpha(&self) -> (RippleFloat, RippleFloat) { + let elapsed = self.start_time.elapsed(); + #[cfg(target_os = "macos")] + let progress = (elapsed.as_secs_f64() / RIPPLE_DURATION.as_secs_f64()).min(1.0); + #[cfg(any(target_os = "windows", target_os = "linux"))] + let progress = (elapsed.as_secs_f32() / RIPPLE_DURATION.as_secs_f32()).min(1.0); + #[cfg(target_os = "macos")] + let radius = 25.0 * progress; + #[cfg(any(target_os = "windows", target_os = "linux"))] + let radius = 45.0 * progress; + let alpha = 1.0 - progress; + (radius, alpha) + } +} diff --git a/src/whiteboard/win_linux.rs b/src/whiteboard/win_linux.rs new file mode 100644 index 000000000..f279bebb7 --- /dev/null +++ b/src/whiteboard/win_linux.rs @@ -0,0 +1,180 @@ +use hbb_common::{bail, ResultType}; +use tiny_skia::{FillRule, Paint, PathBuilder, PixmapMut, Point, Rect, Transform}; +use ttf_parser::Face; +// 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 with the white background rectangle onto the pixmap. +pub(super) 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; + + // --- 1. Calculate text dimensions for the background --- + let mut total_width = 0.0; + for ch in text.chars() { + let glyph_id = face.glyph_index(ch).unwrap_or_default(); + if let Some(h_advance) = face.glyph_hor_advance(glyph_id) { + total_width += h_advance as f32 * scale; + } + } + + // Use font metrics for a consistent background height. + let font_height = (face.ascender() - face.descender()) as f32 * scale; + let ascent = face.ascender() as f32 * scale; + // Add some padding around the text + let padding = 3.0; + + let mut bg_filled = false; + // --- 2. Draw the white background rectangle --- + if let Some(bg_rect) = Rect::from_xywh( + x - padding, + y - ascent - padding, + total_width + 2.0 * padding, + font_height + 2.0 * padding, + ) { + // Corner radius + let radius = 5.0; + let path = { + let mut pb = PathBuilder::new(); + let r_x = bg_rect.x(); + let r_y = bg_rect.y(); + let r_w = bg_rect.width(); + let r_h = bg_rect.height(); + pb.move_to(r_x + radius, r_y); + pb.line_to(r_x + r_w - radius, r_y); + pb.quad_to(r_x + r_w, r_y, r_x + r_w, r_y + radius); + pb.line_to(r_x + r_w, r_y + r_h - radius); + pb.quad_to(r_x + r_w, r_y + r_h, r_x + r_w - radius, r_y + r_h); + pb.line_to(r_x + radius, r_y + r_h); + pb.quad_to(r_x, r_y + r_h, r_x, r_y + r_h - radius); + pb.line_to(r_x, r_y + radius); + pb.quad_to(r_x, r_y, r_x + radius, r_y); + pb.close(); + pb.finish() + }; + + if let Some(path) = path { + let mut bg_paint = Paint::default(); + bg_paint.set_color_rgba8(255, 255, 255, 255); + bg_paint.anti_alias = true; + pixmap.fill_path( + &path, + &bg_paint, + FillRule::Winding, + Transform::identity(), + None, + ); + bg_filled = true; + } + } + + // --- 3. Draw the text --- + 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() { + if bg_filled { + let mut text_paint = Paint::default(); + text_paint.set_color_rgba8(0, 0, 0, 255); + text_paint.anti_alias = true; + pixmap.fill_path( + &path, + &text_paint, + FillRule::Winding, + Transform::identity(), + None, + ); + } else { + pixmap.fill_path(&path, paint, FillRule::Winding, Transform::identity(), None); + } + } +} + +pub(super) 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!"); + }; + // Load the font data into a static slice to satisfy `ttf-parser`'s lifetime requirements. + // We use `Box::leak` to leak the memory, which is acceptable here since the font data + // is needed for the entire lifetime of the application. + 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) +} diff --git a/src/whiteboard/windows.rs b/src/whiteboard/windows.rs index 7f2ca3149..dc6a8c30e 100644 --- a/src/whiteboard/windows.rs +++ b/src/whiteboard/windows.rs @@ -1,121 +1,19 @@ -use super::{server::EVENT_PROXY, Cursor, CustomEvent}; -use hbb_common::{anyhow::anyhow, bail, log, ResultType}; +use super::{ + server::{Ripple, EVENT_PROXY}, + win_linux::{create_font_face, draw_text}, + Cursor, CustomEvent, +}; +use hbb_common::{anyhow::anyhow, log, ResultType}; use softbuffer::{Context, Surface}; use std::{collections::HashMap, num::NonZeroU32, sync::Arc, time::Instant}; -#[cfg(target_os = "linux")] -use tao::platform::unix::WindowBuilderExtUnix; -#[cfg(target_os = "windows")] -use tao::platform::windows::WindowBuilderExtWindows; use tao::{ dpi::{PhysicalPosition, PhysicalSize}, event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoopBuilder}, + platform::windows::WindowBuilderExtWindows, window::WindowBuilder, }; -use tiny_skia::{Color, FillRule, Paint, PathBuilder, PixmapMut, Point, Stroke, Transform}; -use ttf_parser::Face; - -// 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); - } -} - -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) -} +use tiny_skia::{Color, FillRule, Paint, PathBuilder, PixmapMut, Stroke, Transform}; pub(super) fn create_event_loop() -> ResultType<()> { let face = match create_font_face() { @@ -171,11 +69,6 @@ pub(super) fn create_event_loop() -> ResultType<()> { }), }; - 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(); @@ -230,23 +123,17 @@ pub(super) fn create_event_loop() -> ResultType<()> { }; pixmap.fill(Color::TRANSPARENT); - let ripple_duration = std::time::Duration::from_millis(500); - ripples.retain(|r| r.start_time.elapsed() < ripple_duration); - + Ripple::retain_active(&mut ripples); 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 (radius, alpha) = ripple.get_radius_alpha(); 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.set_color_rgba8(64, 64, 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); + ripple_pb.push_circle(ripple.x, ripple.y, radius); if let Some(path) = ripple_pb.finish() { pixmap.fill_path( &path, @@ -259,9 +146,8 @@ pub(super) fn create_event_loop() -> ResultType<()> { } 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 (x, y) = (cursor.x, cursor.y); + let size = 1.5f32; let mut pb = PathBuilder::new(); pb.move_to(x, y); @@ -274,14 +160,10 @@ pub(super) fn create_event_loop() -> ResultType<()> { pb.close(); if let Some(path) = pb.finish() { + let rgba = super::argb_to_rgba(cursor.argb); 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.set_color_rgba8(rgba.2, rgba.1, rgba.0, rgba.3); arrow_paint.anti_alias = true; pixmap.fill_path( &path, @@ -295,7 +177,7 @@ pub(super) fn create_event_loop() -> ResultType<()> { 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; + stroke.width = 1.0f32; pixmap.stroke_path( &path, &black_paint, @@ -312,7 +194,7 @@ pub(super) fn create_event_loop() -> ResultType<()> { x + 24.0 * size, y + 24.0 * size, &arrow_paint, - 24.0 as f32, + 14.0f32, ); }); }