From e2ec6a5be809f3d554efe4048506802b15360fbd Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 30 Aug 2025 22:16:35 +0800 Subject: [PATCH] feat: whiteboard, macos (#12780) Signed-off-by: fufesou --- Cargo.lock | 124 +++++- Cargo.toml | 4 + .../lib/desktop/widgets/remote_toolbar.dart | 4 +- src/core_main.rs | 2 +- src/ipc.rs | 2 +- src/lib.rs | 2 +- src/platform/macos.rs | 2 +- src/server/connection.rs | 12 +- src/server/input_service.rs | 7 +- src/whiteboard/client.rs | 258 +++++++++++ src/whiteboard/macos.rs | 234 ++++++++++ src/whiteboard/mod.rs | 35 ++ src/whiteboard/server.rs | 120 ++++++ src/{whiteboard.rs => whiteboard/windows.rs} | 399 +----------------- 14 files changed, 796 insertions(+), 409 deletions(-) create mode 100644 src/whiteboard/client.rs create mode 100644 src/whiteboard/macos.rs create mode 100644 src/whiteboard/mod.rs create mode 100644 src/whiteboard/server.rs rename src/{whiteboard.rs => whiteboard/windows.rs} (50%) diff --git a/Cargo.lock b/Cargo.lock index d301a80cf..bcd61122c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,6 +242,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "associative-cache" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46016233fc1bb55c23b856fe556b7db6ccd05119a0a392e04f0b3b7c79058f16" + [[package]] name = "async-broadcast" version = "0.5.1" @@ -1280,9 +1286,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys 0.8.7", "libc", @@ -1392,6 +1398,18 @@ dependencies = [ "libc", ] +[[package]] +name = "core-text" +version = "19.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" +dependencies = [ + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", +] + [[package]] name = "core-video-sys" version = "0.1.4" @@ -3809,6 +3827,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -4108,6 +4135,12 @@ dependencies = [ "libc", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "md5" version = "0.7.0" @@ -5289,6 +5322,31 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "piet" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e381186490a3e2017a506d62b759ea8eaf4be14666b13ed53973e8ae193451b1" +dependencies = [ + "kurbo", + "unic-bidi", +] + +[[package]] +name = "piet-coregraphics" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819b41d2ddb1d8abf3e45e49422f866cba281b4abb5e2fb948bba06e2c3d3f7" +dependencies = [ + "associative-cache", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.7", + "core-graphics 0.22.3", + "core-text", + "foreign-types 0.3.2", + "piet", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -6269,6 +6327,7 @@ dependencies = [ "flutter_rust_bridge", "fon", "fontdb", + "foreign-types 0.3.2", "fruitbasket", "gtk", "hbb_common", @@ -6295,6 +6354,8 @@ dependencies = [ "pam", "parity-tokio-ipc", "percent-encoding", + "piet", + "piet-coregraphics", "portable-pty", "qrcode-generator", "rdev", @@ -6449,7 +6510,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5467026f437b4cb2a533865eaa73eb840019a0916f4b9ec563c6e617e086c9" dependencies = [ - "core-foundation 0.10.0", + "core-foundation 0.10.1", "core-foundation-sys 0.8.7", "jni", "log", @@ -6596,7 +6657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags 2.9.1", - "core-foundation 0.10.0", + "core-foundation 0.10.1", "core-foundation-sys 0.8.7", "libc", "security-framework-sys", @@ -6879,9 +6940,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" @@ -7979,6 +8040,57 @@ dependencies = [ "libc", ] +[[package]] +name = "unic-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1356b759fb6a82050666f11dce4b6fe3571781f1449f3ef78074e408d468ec09" +dependencies = [ + "matches", + "unic-ucd-bidi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d568b51222484e1f8209ce48caa6b430bf352962b877d592c29ab31fb53d8c" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index ccc7a9dac..7ec9d418c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,6 +149,10 @@ core-graphics = "0.22" include_dir = "0.7" fruitbasket = "0.10" objc_id = "0.1" +# If we use piet "0.7" here, we must also update core-graphics to "0.24". +piet = "0.6" +piet-coregraphics = "0.6" +foreign-types = "0.3" [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] tray-icon = { git = "https://github.com/tauri-apps/tray-icon" } diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 0613ee14d..5753c14fa 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1593,7 +1593,9 @@ class _KeyboardMenu extends StatelessWidget { inputSource(), Divider(), viewMode(), - if (pi.platform == kPeerPlatformWindows) showMyCursor(), + if (pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS) + showMyCursor(), Divider(), ...toolbarToggles(), ...mouseSpeed(), diff --git a/src/core_main.rs b/src/core_main.rs index fe2b6ece9..c6dcac0a9 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(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] { crate::whiteboard::run(); } diff --git a/src/ipc.rs b/src/ipc.rs index 9ad7f8445..4962c6817 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(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] Whiteboard((String, crate::whiteboard::CustomEvent)), } diff --git a/src/lib.rs b/src/lib.rs index c85e13d9a..02ab0fb42 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(target_os = "windows")] +#[cfg(any(target_os = "windows", target_os = "macos"))] mod whiteboard; #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/platform/macos.rs b/src/platform/macos.rs index a206bde53..4bf419952 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -780,7 +780,7 @@ pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDes Ok(()) } -pub fn update_to(file: &str) -> ResultType<()> { +pub fn update_to(_file: &str) -> ResultType<()> { update_extracted(UPDATE_TEMP_DIR)?; Ok(()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 0a6c509fb..73ed8f2f9 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3697,13 +3697,17 @@ impl Connection { self.update_terminal_persistence(q == BoolOption::Yes).await; } } - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] 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(); + #[cfg(not(target_os = "windows"))] + let is_win10_or_greater = false; if q == BoolOption::Yes { - if crate::platform::windows::is_win_10_or_greater() { + if !cfg!(target_os = "windows") || is_win10_or_greater { whiteboard::register_whiteboard(whiteboard::get_key_cursor(self.inner.id)); } else { let mut msg_out = Message::new(); @@ -3718,7 +3722,7 @@ impl Connection { self.send(msg_out).await; } } else { - if crate::platform::windows::is_win_10_or_greater() { + if !cfg!(target_os = "windows") || is_win10_or_greater { whiteboard::unregister_whiteboard(whiteboard::get_key_cursor( self.inner.id, )); @@ -4878,7 +4882,7 @@ mod raii { scrap::wayland::pipewire::try_close_session(); } Self::check_wake_lock(); - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] { 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 069a2f821..b31b48477 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(target_os = "windows")] +#[cfg(any(target_os = "windows", target_os = "macos"))] use crate::whiteboard; #[cfg(target_os = "macos")] use dispatch::Queue; @@ -204,6 +204,7 @@ impl LockModesHandler { } let mut num_lock_changed = false; + #[allow(unused)] let mut event_num_enabled = false; if is_numpad_key { let local_num_enabled = en.get_key_state(enigo::Key::NumLock); @@ -999,7 +1000,7 @@ pub fn handle_mouse_( if simulate { handle_mouse_simulation_(evt, conn); } - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] if _show_cursor { handle_mouse_show_cursor_(evt, conn, username, argb); } @@ -1148,7 +1149,7 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) { } } -#[cfg(target_os = "windows")] +#[cfg(any(target_os = "windows", target_os = "macos"))] 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/client.rs b/src/whiteboard/client.rs new file mode 100644 index 000000000..0d816ba27 --- /dev/null +++ b/src/whiteboard/client.rs @@ -0,0 +1,258 @@ +use super::{Cursor, CustomEvent}; +use crate::{ + ipc::{self, Data}, + CHILD_PROCESS, +}; +use hbb_common::{ + allow_err, + anyhow::anyhow, + bail, log, sleep, + tokio::{ + self, + sync::mpsc::{unbounded_channel, UnboundedSender}, + time::interval_at, + }, + ResultType, +}; +use lazy_static::lazy_static; +use std::{collections::HashMap, sync::RwLock, time::Instant}; + +lazy_static! { + 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, +} + +struct LastCursorEvent { + evt: Option, + tm: Instant, + c: usize, +} + +#[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? { + CHILD_PROCESS.lock().unwrap().push(task); + } + run_done = true; + } else { + run_done = false; + } + if !run_done { + log::debug!("Start whiteboard"); + 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(()) +} diff --git a/src/whiteboard/macos.rs b/src/whiteboard/macos.rs new file mode 100644 index 000000000..32271b74d --- /dev/null +++ b/src/whiteboard/macos.rs @@ -0,0 +1,234 @@ +use super::{server::EVENT_PROXY, Cursor, CustomEvent}; +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, RenderContext}; +use piet_coregraphics::CoreGraphicsContext; +use std::{collections::HashMap, sync::Arc, time::Instant}; +use tao::{ + dpi::{PhysicalPosition, PhysicalSize}, + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoopBuilder}, + rwh_06::{HasWindowHandle, RawWindowHandle}, + window::{Window, WindowBuilder}, +}; + +const MAXIMUM_WINDOW_LEVEL: i64 = 2147483647; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct NSRect { + origin: NSPoint, + size: NSSize, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct NSPoint { + x: f64, + y: f64, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct NSSize { + width: f64, + height: f64, +} + +fn set_window_properties(window: &Arc) -> ResultType<()> { + let handle = window.window_handle()?; + if let RawWindowHandle::AppKit(appkit_handle) = handle.as_raw() { + unsafe { + let ns_view = appkit_handle.ns_view.as_ptr() as *mut Object; + if ns_view.is_null() { + bail!("Ns view of the window handle is null."); + } + let ns_window: *mut Object = msg_send![ns_view, window]; + if ns_window.is_null() { + bail!("Ns window of the ns view is null."); + } + let _: () = msg_send![ns_window, setOpaque: false]; + let _: () = msg_send![ns_window, setLevel: MAXIMUM_WINDOW_LEVEL]; + // NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorIgnoresCycle + let _: () = msg_send![ns_window, setCollectionBehavior: 5]; + let current_style_mask: u64 = msg_send![ns_window, styleMask]; + // NSWindowStyleMaskNonactivatingPanel + let new_style_mask = current_style_mask | (1 << 7); + let _: () = msg_send![ns_window, setStyleMask: new_style_mask]; + let ns_screen_class = class!(NSScreen); + let main_screen: *mut Object = msg_send![ns_screen_class, mainScreen]; + let screen_frame: NSRect = msg_send![main_screen, frame]; + let _: () = msg_send![ns_window, setFrame: screen_frame display: true]; + let ns_color_class = class!(NSColor); + let clear_color: *mut Object = msg_send![ns_color_class, clearColor]; + let _: () = msg_send![ns_window, setBackgroundColor: clear_color]; + let _: () = msg_send![ns_window, setIgnoresMouseEvents: true]; + } + } + Ok(()) +} + +pub(super) fn create_event_loop() -> ResultType<()> { + crate::platform::hide_dock(); + let event_loop = EventLoopBuilder::<(String, CustomEvent)>::with_user_event().build(); + let mut window_builder = WindowBuilder::new() + .with_title("RustDesk whiteboard") + .with_transparent(true) + .with_decorations(false); + + let (x, y, w, h) = super::server::get_displays_rect()?; + if w > 0 && h > 0 { + window_builder = window_builder + .with_position(PhysicalPosition::new(x, y)) + .with_inner_size(PhysicalSize::new(w, h)); + } else { + bail!("No valid display found, wxh: {}x{}", w, h); + } + + let window = Arc::new(window_builder.build::<(String, CustomEvent)>(&event_loop)?); + set_window_properties(&window)?; + + 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(); + }), + }; + + // to-do: The scale factor may not be correct. + // There may be multiple monitors with different scale factors. + // But we only have one window, and one scale factor. + let mut scale_factor = window.scale_factor(); + if scale_factor == 0.0 { + scale_factor = 1.0; + } + let physical_size = window.inner_size(); + let logical_size = physical_size.to_logical::(scale_factor); + + struct Ripple { + x: f64, + y: f64, + start_time: Instant, + } + let mut ripples: Vec = Vec::new(); + let mut last_cursors: HashMap = HashMap::new(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Poll; + + match event { + Event::NewEvents(StartCause::Init) => { + window.set_outer_position(PhysicalPosition::new(0, 0)); + window.request_redraw(); + crate::platform::hide_dock(); + } + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => { + *control_flow = ControlFlow::Exit; + } + _ => {} + }, + Event::RedrawRequested(_) => { + if let Ok(handle) = window.window_handle() { + if let RawWindowHandle::AppKit(appkit_handle) = handle.as_raw() { + unsafe { + let ns_view = appkit_handle.ns_view.as_ptr() as *mut Object; + let current_context: *mut Object = + msg_send![class!(NSGraphicsContext), currentContext]; + if !current_context.is_null() { + let cg_context_ptr: *mut std::ffi::c_void = + msg_send![current_context, CGContext]; + if !cg_context_ptr.is_null() { + let cg_context_ref = + CGContextRef::from_ptr_mut(cg_context_ptr as *mut _); + let mut context = CoreGraphicsContext::new_y_up( + cg_context_ref, + logical_size.height, + None, + ); + context.clear(None, piet::Color::TRANSPARENT); + + 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 = 45.0 * progress / scale_factor; + 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 / scale_factor, ripple.y / scale_factor), + radius, + ); + context.stroke(circle, &color, 2.0); + true + } else { + false + } + }); + + for cursor in last_cursors.values() { + let (x, y) = ( + cursor.x as f64 / scale_factor, + cursor.y as f64 / scale_factor, + ); + let size = 1.0; + + let mut pb = BezPath::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)); + + 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, + ); + context.fill(pb, &color); + } + if let Err(e) = context.finish() { + log::error!("Failed to draw cursor: {}", e); + } + } else { + log::warn!("CGContext is null"); + } + } + let _: () = msg_send![ns_view, setNeedsDisplay:true]; + } + } + } + } + Event::MainEventsCleared => { + window.request_redraw(); + } + Event::UserEvent((k, evt)) => match evt { + CustomEvent::Cursor(cursor) => { + if cursor.btns != 0 { + ripples.push(Ripple { + x: cursor.x as _, + y: cursor.y as _, + start_time: Instant::now(), + }); + } + last_cursors.insert(k, cursor); + window.request_redraw(); + } + CustomEvent::Exit => { + *control_flow = ControlFlow::Exit; + } + _ => {} + }, + _ => (), + } + }); +} diff --git a/src/whiteboard/mod.rs b/src/whiteboard/mod.rs new file mode 100644 index 000000000..e3fa13042 --- /dev/null +++ b/src/whiteboard/mod.rs @@ -0,0 +1,35 @@ +use serde_derive::{Deserialize, Serialize}; + +mod client; +mod server; + +#[cfg(target_os = "windows")] +mod windows; +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(target_os = "windows")] +use windows::create_event_loop; +#[cfg(target_os = "macos")] +use macos::create_event_loop; + +pub use client::*; +pub use server::*; + +#[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, +} diff --git a/src/whiteboard/server.rs b/src/whiteboard/server.rs new file mode 100644 index 000000000..0853e35c3 --- /dev/null +++ b/src/whiteboard/server.rs @@ -0,0 +1,120 @@ +use super::{create_event_loop, CustomEvent}; +use crate::ipc::{new_listener, Connection, Data}; +use hbb_common::{ + allow_err, log, + tokio::{ + self, + sync::mpsc::{unbounded_channel, UnboundedReceiver}, + }, + ResultType, +}; +use lazy_static::lazy_static; +use std::sync::RwLock; +use tao::event_loop::EventLoopProxy; + +lazy_static! { + pub(super) static ref EVENT_PROXY: RwLock>> = + RwLock::new(None); +} + +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))); + }); +} + +pub(super) fn get_displays_rect() -> ResultType<(i32, i32, u32, u32)> { + let 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); + Ok((x, y, w, h)) +} diff --git a/src/whiteboard.rs b/src/whiteboard/windows.rs similarity index 50% rename from src/whiteboard.rs rename to src/whiteboard/windows.rs index e6c288ab5..7f2ca3149 100644 --- a/src/whiteboard.rs +++ b/src/whiteboard/windows.rs @@ -1,73 +1,20 @@ -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 super::{server::EVENT_PROXY, Cursor, CustomEvent}; +use hbb_common::{anyhow::anyhow, bail, log, ResultType}; use softbuffer::{Context, Surface}; -use std::{ - collections::HashMap, - num::NonZeroU32, - sync::{Arc, RwLock}, - time::Instant, -}; +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, EventLoopProxy}, + event_loop::{ControlFlow, EventLoopBuilder}, 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, @@ -148,314 +95,6 @@ fn draw_text( } } -#[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(); @@ -478,7 +117,7 @@ fn create_font_face() -> ResultType> { Ok(face) } -fn create_event_loop() -> ResultType<()> { +pub(super) fn create_event_loop() -> ResultType<()> { let face = match create_font_face() { Ok(face) => Some(face), Err(err) => { @@ -492,28 +131,11 @@ fn create_event_loop() -> ResultType<()> { .with_title("RustDesk whiteboard") .with_transparent(true) .with_always_on_top(true) + .with_skip_taskbar(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 let Ok((x, y, w, h)) = super::server::get_displays_rect() { if w > 0 && h > 0 { final_size = Some(PhysicalSize::new(w, h)); window_builder = window_builder @@ -528,11 +150,6 @@ fn create_event_loop() -> ResultType<()> { 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)?;