mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-08-27 16:46:35 +00:00
* fix(flutter): make Adjust Window reliable across desktop platforms
- Fix incorrect sizing on scaled displays by calculating the target from the
rendered canvas scale and platform-specific window coordinate units.
- Fix adjustments using the wrong monitor by querying the current remote
window's screen, with the main window as fallback.
- Fix stale geometry after fullscreen or maximized transitions by refreshing
metrics before calculating and applying the target frame.
- Fix fullscreen availability checks on Windows and macOS by predicting the
restored window borders and caching each macOS window's pre-fullscreen work area.
- Fix incorrect Linux work areas by handling GNOME Wayland fractional scaling
and caching compositor/X11 work-area measurements when visibleFrame is wrong.
- Prevent unsafe adjustments by rejecting invalid, oversized, or implausibly
small target frames.
- Avoid failures during window teardown by skipping adjustment when the view,
screen, or native window frame is unavailable.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(flutter): harden Adjust Window handling
- Use the dynamic Linux resize edge when predicting restored window bounds.
- Treat GNOME fractional-scaling lookup failures as unknown without repeating
the lookup for the remote window.
- Stop adjustment safely when native window calls fail during window teardown.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(flutter): correct Linux monitor selection
Update window_size to use monitor height for vertical bounds, preventing incorrect screen selection with vertically stacked displays.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* docs(flutter): simplify Linux screen handling comments
Keep the source rationale concise and move platform measurements and investigation details out of the implementation.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(flutter): align Adjust Window resize padding
Use the shared drag-to-resize padding for Linux restored-window predictions so menu validation matches the applied frame dimensions.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(flutter): remove Adjust Window screen fallback
Return null when the current window screen is unavailable instead of using the main window's scale factor and work area.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(linux): query Mutter monitor layout mode
Use DisplayConfig.GetCurrentState instead of inferring scaling from
experimental features, and handle Ubuntu's UI-scaled logical mode.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(flutter): use native maximized state for Wayland cache
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(flutter): allow Adjust Window to fill work area
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(flutter): avoid racing screen info updates
Signed-off-by: 21pages <sunboeasy@gmail.com>
* refactor(flutter): remove dead Adjust Window web plumbing
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(flutter): tolerate near-unity Wayland scale factors
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(flutter): harden window screen detection
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(linux): drop deprecated GNOME session detection
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(flutter): remove GNOME monitor layout mode flutter cache
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
317 lines
14 KiB
Swift
317 lines
14 KiB
Swift
import Cocoa
|
|
import AVFoundation
|
|
import FlutterMacOS
|
|
import desktop_multi_window
|
|
// import bitsdojo_window_macos
|
|
|
|
import desktop_drop
|
|
import device_info_plus
|
|
import flutter_custom_cursor
|
|
import package_info_plus
|
|
import path_provider_foundation
|
|
import screen_retriever
|
|
import sqflite
|
|
// import tray_manager
|
|
import uni_links_desktop
|
|
import url_launcher_macos
|
|
import wakelock_plus
|
|
import window_manager
|
|
import window_size
|
|
import texture_rgba_renderer
|
|
|
|
// Global state for relative mouse mode
|
|
// All properties and methods must be accessed on the main thread since they
|
|
// interact with NSEvent monitors, CoreGraphics APIs, and Flutter channels.
|
|
// Note: We avoid @MainActor to maintain macOS 10.14 compatibility.
|
|
class RelativeMouseState {
|
|
static let shared = RelativeMouseState()
|
|
|
|
var enabled = false
|
|
var eventMonitor: Any?
|
|
var deltaChannel: FlutterMethodChannel?
|
|
var accumulatedDeltaX: CGFloat = 0
|
|
var accumulatedDeltaY: CGFloat = 0
|
|
|
|
private init() {}
|
|
}
|
|
|
|
class MainFlutterWindow: NSWindow {
|
|
private static let fullscreenWorkAreaSizes = NSMapTable<NSWindow, NSValue>(
|
|
keyOptions: [.weakMemory, .objectPointerPersonality],
|
|
valueOptions: .strongMemory
|
|
)
|
|
private static let fullscreenObserver = NotificationCenter.default.addObserver(
|
|
forName: NSWindow.willEnterFullScreenNotification,
|
|
object: nil,
|
|
queue: .main
|
|
) { notification in
|
|
guard let window = notification.object as? NSWindow,
|
|
let screen = window.screen else {
|
|
return
|
|
}
|
|
fullscreenWorkAreaSizes.setObject(
|
|
NSValue(size: screen.visibleFrame.size),
|
|
forKey: window
|
|
)
|
|
}
|
|
|
|
override func awakeFromNib() {
|
|
rustdesk_core_main();
|
|
_ = MainFlutterWindow.fullscreenObserver
|
|
let flutterViewController = FlutterViewController.init()
|
|
let windowFrame = self.frame
|
|
self.contentViewController = flutterViewController
|
|
self.setFrame(windowFrame, display: true)
|
|
// register self method handler
|
|
let registrar = flutterViewController.registrar(forPlugin: "RustDeskPlugin")
|
|
setMethodHandler(registrar: registrar)
|
|
|
|
RegisterGeneratedPlugins(registry: flutterViewController)
|
|
|
|
FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in
|
|
// Register the plugin which you want access from other isolate.
|
|
// DesktopLifecyclePlugin.register(with: controller.registrar(forPlugin: "DesktopLifecyclePlugin"))
|
|
// Note: copy below from above RegisterGeneratedPlugins
|
|
self.setMethodHandler(registrar: controller.registrar(forPlugin: "RustDeskPlugin"))
|
|
DesktopDropPlugin.register(with: controller.registrar(forPlugin: "DesktopDropPlugin"))
|
|
DeviceInfoPlusMacosPlugin.register(with: controller.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
|
FlutterCustomCursorPlugin.register(with: controller.registrar(forPlugin: "FlutterCustomCursorPlugin"))
|
|
FPPPackageInfoPlusPlugin.register(with: controller.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
|
PathProviderPlugin.register(with: controller.registrar(forPlugin: "PathProviderPlugin"))
|
|
SqflitePlugin.register(with: controller.registrar(forPlugin: "SqflitePlugin"))
|
|
// TrayManagerPlugin.register(with: controller.registrar(forPlugin: "TrayManagerPlugin"))
|
|
UniLinksDesktopPlugin.register(with: controller.registrar(forPlugin: "UniLinksDesktopPlugin"))
|
|
UrlLauncherPlugin.register(with: controller.registrar(forPlugin: "UrlLauncherPlugin"))
|
|
WakelockPlusMacosPlugin.register(with: controller.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
|
WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin"))
|
|
TextureRgbaRendererPlugin.register(with: controller.registrar(forPlugin: "TextureRgbaRendererPlugin"))
|
|
}
|
|
|
|
super.awakeFromNib()
|
|
}
|
|
|
|
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
|
|
super.order(place, relativeTo: otherWin)
|
|
hiddenWindowAtLaunch()
|
|
}
|
|
|
|
/// Override window theme.
|
|
public func setWindowInterfaceMode(window: NSWindow, themeName: String) {
|
|
window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua)
|
|
}
|
|
|
|
private func enableNativeRelativeMouseMode(channel: FlutterMethodChannel) -> Bool {
|
|
assert(Thread.isMainThread, "enableNativeRelativeMouseMode must be called on the main thread")
|
|
let state = RelativeMouseState.shared
|
|
if state.enabled {
|
|
// Already enabled: update the channel so this caller receives deltas.
|
|
state.deltaChannel = channel
|
|
return true
|
|
}
|
|
|
|
// Dissociate mouse from cursor position - this locks the cursor in place
|
|
// Do this FIRST before setting any state
|
|
let result = CGAssociateMouseAndMouseCursorPosition(0)
|
|
if result != CGError.success {
|
|
NSLog("[RustDesk] Failed to dissociate mouse from cursor position: %d", result.rawValue)
|
|
return false
|
|
}
|
|
|
|
// Only set state after CG call succeeds
|
|
state.deltaChannel = channel
|
|
state.accumulatedDeltaX = 0
|
|
state.accumulatedDeltaY = 0
|
|
|
|
// Add local event monitor to capture mouse delta.
|
|
// Note: Local event monitors are always called on the main thread,
|
|
// so accessing main-thread-only state is safe here.
|
|
state.eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged]) { [weak state] event in
|
|
guard let state = state else { return event }
|
|
// Guard against race: mode may be disabled between weak capture and this check.
|
|
guard state.enabled else { return event }
|
|
let deltaX = event.deltaX
|
|
let deltaY = event.deltaY
|
|
|
|
if deltaX != 0 || deltaY != 0 {
|
|
// Accumulate delta (main thread only - NSEvent local monitors always run on main thread)
|
|
state.accumulatedDeltaX += deltaX
|
|
state.accumulatedDeltaY += deltaY
|
|
|
|
// Only send if we have integer movement
|
|
let intX = Int(state.accumulatedDeltaX)
|
|
let intY = Int(state.accumulatedDeltaY)
|
|
|
|
if intX != 0 || intY != 0 {
|
|
state.accumulatedDeltaX -= CGFloat(intX)
|
|
state.accumulatedDeltaY -= CGFloat(intY)
|
|
|
|
// Send delta to Flutter (already on main thread)
|
|
state.deltaChannel?.invokeMethod("onMouseDelta", arguments: ["dx": intX, "dy": intY])
|
|
}
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
// Check if monitor was created successfully
|
|
if state.eventMonitor == nil {
|
|
NSLog("[RustDesk] Failed to create event monitor for relative mouse mode")
|
|
// Re-associate mouse since we failed
|
|
CGAssociateMouseAndMouseCursorPosition(1)
|
|
state.deltaChannel = nil
|
|
return false
|
|
}
|
|
|
|
// Set enabled LAST after everything succeeds
|
|
state.enabled = true
|
|
return true
|
|
}
|
|
|
|
private func disableNativeRelativeMouseMode() {
|
|
assert(Thread.isMainThread, "disableNativeRelativeMouseMode must be called on the main thread")
|
|
let state = RelativeMouseState.shared
|
|
if !state.enabled { return }
|
|
|
|
state.enabled = false
|
|
|
|
// Remove event monitor
|
|
if let monitor = state.eventMonitor {
|
|
NSEvent.removeMonitor(monitor)
|
|
state.eventMonitor = nil
|
|
}
|
|
|
|
state.deltaChannel = nil
|
|
state.accumulatedDeltaX = 0
|
|
state.accumulatedDeltaY = 0
|
|
|
|
// Re-associate mouse with cursor position (non-blocking with async retry)
|
|
let result = CGAssociateMouseAndMouseCursorPosition(1)
|
|
if result != CGError.success {
|
|
NSLog("[RustDesk] Failed to re-associate mouse with cursor position: %d, scheduling retry...", result.rawValue)
|
|
// Non-blocking retry after 50ms
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
let retryResult = CGAssociateMouseAndMouseCursorPosition(1)
|
|
if retryResult != CGError.success {
|
|
NSLog("[RustDesk] Retry failed to re-associate mouse: %d. Cursor may remain locked.", retryResult.rawValue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func setMethodHandler(registrar: FlutterPluginRegistrar) {
|
|
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger)
|
|
channel.setMethodCallHandler({
|
|
(call, result) -> Void in
|
|
switch call.method {
|
|
case "setWindowTheme":
|
|
let arg = call.arguments as! [String: Any]
|
|
let themeName = arg["themeName"] as? String
|
|
guard let window = registrar.view?.window else {
|
|
result(nil)
|
|
return
|
|
}
|
|
self.setWindowInterfaceMode(window: window,themeName: themeName ?? "light")
|
|
result(nil)
|
|
break;
|
|
case "terminate":
|
|
NSApplication.shared.terminate(self)
|
|
result(nil)
|
|
case "canRecordAudio":
|
|
switch AVCaptureDevice.authorizationStatus(for: .audio) {
|
|
case .authorized:
|
|
result(1)
|
|
break
|
|
case .notDetermined:
|
|
result(0)
|
|
break
|
|
default:
|
|
result(-1)
|
|
break
|
|
}
|
|
case "requestRecordAudio":
|
|
AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in
|
|
DispatchQueue.main.async {
|
|
result(granted)
|
|
}
|
|
})
|
|
break
|
|
case "bumpMouse":
|
|
var dx = 0
|
|
var dy = 0
|
|
|
|
if let argMap = call.arguments as? [String: Any] {
|
|
dx = (argMap["dx"] as? Int) ?? 0
|
|
dy = (argMap["dy"] as? Int) ?? 0
|
|
}
|
|
else if let argList = call.arguments as? [Any] {
|
|
dx = argList.count >= 1 ? (argList[0] as? Int) ?? 0 : 0
|
|
dy = argList.count >= 2 ? (argList[1] as? Int) ?? 0 : 0
|
|
}
|
|
|
|
var mouseLoc: CGPoint
|
|
|
|
if let dummyEvent = CGEvent(source: nil) { // can this ever fail?
|
|
mouseLoc = dummyEvent.location
|
|
}
|
|
else if let screenFrame = NSScreen.screens.first?.frame {
|
|
// NeXTStep: Origin is lower-left of primary screen, positive is up
|
|
// Cocoa Core Graphics: Origin is upper-left of primary screen, positive is down
|
|
let nsMouseLoc = NSEvent.mouseLocation
|
|
|
|
mouseLoc = CGPoint(
|
|
x: nsMouseLoc.x,
|
|
y: NSHeight(screenFrame) - nsMouseLoc.y)
|
|
}
|
|
else {
|
|
result(false)
|
|
break
|
|
}
|
|
|
|
let newLoc = CGPoint(x: mouseLoc.x + CGFloat(dx), y: mouseLoc.y + CGFloat(dy))
|
|
|
|
CGDisplayMoveCursorToPoint(0, newLoc)
|
|
|
|
// By default, Cocoa suppresses mouse events briefly after a call to warp the
|
|
// cursor to a new location. This is good if you want to draw the user's
|
|
// attention to the fact that the mouse is now in a particular location, but
|
|
// it's bad in this case; we get called as part of the handling of edge
|
|
// scrolling, which means the mouse is typically still in motion, and we want
|
|
// the cursor to keep moving smoothly uninterrupted.
|
|
//
|
|
// This function's main action is to toggle whether the mouse cursor is
|
|
// associated with the mouse position, but setting it to true when it's
|
|
// already true has the side-effect of cancelling this motion suppression.
|
|
//
|
|
// However, we must NOT call this when relative mouse mode is active,
|
|
// as it would break the pointer lock established by enableNativeRelativeMouseMode.
|
|
if !RelativeMouseState.shared.enabled {
|
|
CGAssociateMouseAndMouseCursorPosition(1 /* true */)
|
|
}
|
|
|
|
result(true)
|
|
|
|
case "enableNativeRelativeMouseMode":
|
|
let success = self.enableNativeRelativeMouseMode(channel: channel)
|
|
result(success)
|
|
|
|
case "disableNativeRelativeMouseMode":
|
|
self.disableNativeRelativeMouseMode()
|
|
result(true)
|
|
|
|
case "getMacOSWorkAreaSize":
|
|
guard Thread.isMainThread,
|
|
let window = registrar.view?.window,
|
|
let size = MainFlutterWindow.fullscreenWorkAreaSizes
|
|
.object(forKey: window)?.sizeValue else {
|
|
result(nil)
|
|
break
|
|
}
|
|
result([Double(size.width), Double(size.height)])
|
|
|
|
default:
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
})
|
|
}
|
|
}
|