Initial commit: RustDesk 1.4.7 macOS desktop port

- Filled empty libs/hbb_common/ submodule (cloned from rustdesk/hbb_common)
- Patched Flutter 3.44 / Dart 3.12 compatibility:
  * flutter/lib/generated_bridge.dart: asTypedList with cast<>, DartPort=Int64
  * flutter/lib/common.dart: DialogTheme->DialogThemeData, TabBarTheme->TabBarThemeData
  * flutter/pubspec.yaml: extended_text 14.0.0->15.0.2, google_fonts override 5.0.0
  * flutter/macos/Runner/Configs/Release.xcconfig: EXCLUDED_ARCHS=x86_64
- Build verified: cargo check + cargo build --features flutter + cargo build --release --features flutter
- Verified flutter build macos --debug and --release both produce working .app
- Verified .dmg installer (27MB arm64) created via hdiutil
- Build deps: Xcode 26.5, CocoaPods 1.16.2, Flutter 3.44, VCPKG arm64-osx
This commit is contained in:
xuwenwei
2026-06-08 21:42:20 +08:00
parent 3d8db09123
commit d3b6026bfd
840 changed files with 291780 additions and 1 deletions
+79
View File
@@ -0,0 +1,79 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
typedef EventCallback<Data> = Future<dynamic> Function(Data data);
abstract class BaseEvent<EventType, Data> {
EventType type;
Data data;
/// Constructor.
BaseEvent(this.type, this.data);
/// Consume this event.
@visibleForTesting
Future<dynamic> consume() async {
final cb = findCallback(type);
if (cb == null) {
return null;
} else {
return cb(data);
}
}
EventCallback<Data>? findCallback(EventType type);
}
abstract class BaseEventLoop<EventType, Data> {
final List<BaseEvent<EventType, Data>> _evts = [];
Timer? _timer;
List<BaseEvent<EventType, Data>> get evts => _evts;
Future<void> onReady() async {
// Poll every 100ms.
_timer = Timer.periodic(Duration(milliseconds: 100), _handleTimer);
}
/// An Event is about to be consumed.
Future<void> onPreConsume(BaseEvent<EventType, Data> evt) async {}
/// An Event was consumed.
Future<void> onPostConsume(BaseEvent<EventType, Data> evt) async {}
/// Events are all handled and cleared.
Future<void> onEventsClear() async {}
/// Events start to consume.
Future<void> onEventsStartConsuming() async {}
Future<void> _handleTimer(Timer timer) async {
if (_evts.isEmpty) {
return;
}
timer.cancel();
_timer = null;
// Handle the logic.
await onEventsStartConsuming();
while (_evts.isNotEmpty) {
final evt = _evts.first;
_evts.remove(evt);
await onPreConsume(evt);
await evt.consume();
await onPostConsume(evt);
}
await onEventsClear();
// Now events are all processed.
_timer = Timer.periodic(Duration(milliseconds: 100), _handleTimer);
}
Future<void> close() async {
_timer?.cancel();
}
void pushEvent(BaseEvent<EventType, Data> evt) {
_evts.add(evt);
}
void clear() {
_evts.clear();
}
}
+126
View File
@@ -0,0 +1,126 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:http/http.dart' as http;
import '../models/platform_model.dart';
import 'package:flutter_hbb/common.dart';
export 'package:http/http.dart' show Response;
enum HttpMethod { get, post, put, delete }
class HttpService {
Future<http.Response> sendRequest(
Uri url,
HttpMethod method, {
Map<String, String>? headers,
dynamic body,
}) async {
headers ??= {'Content-Type': 'application/json'};
// Use Rust HTTP implementation for non-web platforms for consistency.
var useFlutterHttp = (isWeb || kIsWeb);
if (!useFlutterHttp) {
final enableFlutterHttpOnRust =
mainGetLocalBoolOptionSync(kOptionEnableFlutterHttpOnRust);
// Use flutter http if:
// Not `enableFlutterHttpOnRust` and no proxy is set
useFlutterHttp =
!(enableFlutterHttpOnRust || await bind.mainGetProxyStatus());
}
if (useFlutterHttp) {
return await _pollFlutterHttp(url, method, headers: headers, body: body);
}
String headersJson = jsonEncode(headers);
String methodName = method.toString().split('.').last;
await bind.mainHttpRequest(
url: url.toString(),
method: methodName.toLowerCase(),
body: body,
header: headersJson);
var resJson = await _pollForResponse(url.toString());
return _parseHttpResponse(resJson);
}
Future<http.Response> _pollFlutterHttp(
Uri url,
HttpMethod method, {
Map<String, String>? headers,
dynamic body,
}) async {
var response = http.Response('', 400);
switch (method) {
case HttpMethod.get:
response = await http.get(url, headers: headers);
break;
case HttpMethod.post:
response = await http.post(url, headers: headers, body: body);
break;
case HttpMethod.put:
response = await http.put(url, headers: headers, body: body);
break;
case HttpMethod.delete:
response = await http.delete(url, headers: headers, body: body);
break;
default:
throw Exception('Unsupported HTTP method');
}
return response;
}
Future<String> _pollForResponse(String url) async {
String? responseJson = " ";
while (responseJson == " ") {
responseJson = await bind.mainGetHttpStatus(url: url);
if (responseJson == null) {
throw Exception('The HTTP request failed');
}
if (responseJson == " ") {
await Future.delayed(const Duration(milliseconds: 100));
}
}
return responseJson!;
}
http.Response _parseHttpResponse(String responseJson) {
try {
var parsedJson = jsonDecode(responseJson);
String body = parsedJson['body'];
Map<String, String> headers = {};
for (var key in parsedJson['headers'].keys) {
headers[key] = parsedJson['headers'][key];
}
int statusCode = parsedJson['status_code'];
return http.Response(body, statusCode, headers: headers);
} catch (e) {
print('Failed to parse response\n$responseJson\nError:\n$e');
throw Exception('Failed to parse response.\n$responseJson');
}
}
}
Future<http.Response> get(Uri url, {Map<String, String>? headers}) async {
return await HttpService().sendRequest(url, HttpMethod.get, headers: headers);
}
Future<http.Response> post(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.post, body: body, headers: headers);
}
Future<http.Response> put(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.put, body: body, headers: headers);
}
Future<http.Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.delete, body: body, headers: headers);
}
+133
View File
@@ -0,0 +1,133 @@
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common.dart';
Future<ui.Image?> decodeImageFromPixels(
Uint8List pixels,
int width,
int height,
ui.PixelFormat format, {
int? rowBytes,
int? targetWidth,
int? targetHeight,
bool allowUpscaling = true,
}) async {
if (targetWidth != null) {
assert(allowUpscaling || targetWidth <= width);
if (!(allowUpscaling || targetWidth <= width)) {
print("not allow upscaling but targetWidth > width");
return null;
}
}
if (targetHeight != null) {
assert(allowUpscaling || targetHeight <= height);
if (!(allowUpscaling || targetHeight <= height)) {
print("not allow upscaling but targetHeight > height");
return null;
}
}
final ui.ImmutableBuffer buffer;
try {
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
} catch (e) {
return null;
}
final ui.ImageDescriptor descriptor;
try {
descriptor = ui.ImageDescriptor.raw(
buffer,
width: width,
height: height,
rowBytes: rowBytes,
pixelFormat: format,
);
if (!allowUpscaling) {
if (targetWidth != null && targetWidth > descriptor.width) {
targetWidth = descriptor.width;
}
if (targetHeight != null && targetHeight > descriptor.height) {
targetHeight = descriptor.height;
}
}
} catch (e) {
print("ImageDescriptor.raw failed: $e");
buffer.dispose();
return null;
}
final ui.Codec codec;
try {
codec = await descriptor.instantiateCodec(
targetWidth: targetWidth,
targetHeight: targetHeight,
);
} catch (e) {
print("instantiateCodec failed: $e");
buffer.dispose();
descriptor.dispose();
return null;
}
final ui.FrameInfo frameInfo;
try {
frameInfo = await codec.getNextFrame();
} catch (e) {
print("getNextFrame failed: $e");
codec.dispose();
buffer.dispose();
descriptor.dispose();
return null;
}
codec.dispose();
buffer.dispose();
descriptor.dispose();
return frameInfo.image;
}
class ImagePainter extends CustomPainter {
ImagePainter({
required this.image,
required this.x,
required this.y,
required this.scale,
});
ui.Image? image;
double x;
double y;
double scale;
@override
void paint(Canvas canvas, Size size) {
if (image == null) return;
if (x.isNaN || y.isNaN) return;
canvas.scale(scale, scale);
// https://github.com/flutter/flutter/issues/76187#issuecomment-784628161
// https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html
var paint = Paint();
if ((scale - 1.0).abs() > 0.001) {
paint.filterQuality = FilterQuality.medium;
if (scale > 10.00000) {
paint.filterQuality = FilterQuality.high;
}
}
// It's strange that if (scale < 0.5 && paint.filterQuality == FilterQuality.medium)
// The canvas.drawImage will not work on web
if (isWeb) {
paint.filterQuality = FilterQuality.high;
}
canvas.drawImage(
image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return oldDelegate != this;
}
}
+581
View File
@@ -0,0 +1,581 @@
import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/input_model.dart';
/// must keep the order
// ignore: constant_identifier_names
enum WindowType {
Main,
RemoteDesktop,
FileTransfer,
ViewCamera,
PortForward,
Terminal,
Unknown
}
extension Index on int {
WindowType get windowType {
switch (this) {
case 0:
return WindowType.Main;
case 1:
return WindowType.RemoteDesktop;
case 2:
return WindowType.FileTransfer;
case 3:
return WindowType.ViewCamera;
case 4:
return WindowType.PortForward;
case 5:
return WindowType.Terminal;
default:
return WindowType.Unknown;
}
}
}
class MultiWindowCallResult {
int windowId;
dynamic result;
MultiWindowCallResult(this.windowId, this.result);
}
/// Window Manager
/// mainly use it in `Main Window`
/// use it in sub window is not recommended
class RustDeskMultiWindowManager {
RustDeskMultiWindowManager._();
static final instance = RustDeskMultiWindowManager._();
final Set<int> _inactiveWindows = {};
final Set<int> _activeWindows = {};
final List<AsyncCallback> _windowActiveCallbacks = List.empty(growable: true);
final List<int> _remoteDesktopWindows = List.empty(growable: true);
final List<int> _fileTransferWindows = List.empty(growable: true);
final List<int> _viewCameraWindows = List.empty(growable: true);
final List<int> _portForwardWindows = List.empty(growable: true);
final List<int> _terminalWindows = List.empty(growable: true);
moveTabToNewWindow(int windowId, String peerId, String sessionId,
WindowType windowType) async {
var params = {
'type': windowType.index,
'id': peerId,
'tab_window_id': windowId,
'session_id': sessionId,
};
if (windowType == WindowType.RemoteDesktop) {
await _newSession(
false,
WindowType.RemoteDesktop,
kWindowEventNewRemoteDesktop,
peerId,
_remoteDesktopWindows,
jsonEncode(params),
);
} else if (windowType == WindowType.ViewCamera) {
await _newSession(
false,
WindowType.ViewCamera,
kWindowEventNewViewCamera,
peerId,
_viewCameraWindows,
jsonEncode(params),
);
}
}
// This function must be called in the main window thread.
// Because the _remoteDesktopWindows is managed in that thread.
openMonitorSession(int windowId, String peerId, int display, int displayCount,
Rect? screenRect, int windowType) async {
final isCamera = windowType == WindowType.ViewCamera.index;
final windowIDs = isCamera ? _viewCameraWindows : _remoteDesktopWindows;
if (windowIDs.length > 1) {
for (final windowId in windowIDs) {
if (await DesktopMultiWindow.invokeMethod(
windowId,
kWindowEventActiveDisplaySession,
jsonEncode({
'id': peerId,
'display': display,
}))) {
return;
}
}
}
final displays = display == kAllDisplayValue
? List.generate(displayCount, (index) => index)
: [display];
var params = {
'type': windowType,
'id': peerId,
'tab_window_id': windowId,
'display': display,
'displays': displays,
};
if (screenRect != null) {
params['screen_rect'] = {
'l': screenRect.left,
't': screenRect.top,
'r': screenRect.right,
'b': screenRect.bottom,
};
}
await _newSession(
false,
windowType.windowType,
isCamera ? kWindowEventNewViewCamera : kWindowEventNewRemoteDesktop,
peerId,
windowIDs,
jsonEncode(params),
screenRect: screenRect,
);
}
Future<int> newSessionWindow(
WindowType type,
String remoteId,
String msg,
List<int> windows,
bool withScreenRect,
) async {
final windowController = await DesktopMultiWindow.createWindow(msg);
if (isWindows) {
windowController.setInitBackgroundColor(Colors.black);
}
final windowId = windowController.windowId;
if (!withScreenRect) {
windowController
..setFrame(const Offset(0, 0) &
Size(1280 + windowId * 20, 720 + windowId * 20))
..center()
..setTitle(getWindowNameWithId(
remoteId,
overrideType: type,
));
} else {
windowController.setTitle(getWindowNameWithId(
remoteId,
overrideType: type,
));
}
if (isMacOS) {
Future.microtask(() => windowController.show());
}
registerActiveWindow(windowId);
windows.add(windowId);
return windowId;
}
Future<MultiWindowCallResult> _newSession(
bool openInTabs,
WindowType type,
String methodName,
String remoteId,
List<int> windows,
String msg, {
Rect? screenRect,
}) async {
if (openInTabs) {
if (windows.isEmpty) {
final windowId = await newSessionWindow(
type, remoteId, msg, windows, screenRect != null);
return MultiWindowCallResult(windowId, null);
} else {
return call(type, methodName, msg);
}
} else {
if (_inactiveWindows.isNotEmpty) {
for (final windowId in windows) {
if (_inactiveWindows.contains(windowId)) {
if (screenRect == null) {
await restoreWindowPosition(type,
windowId: windowId, peerId: remoteId);
}
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
if (methodName != kWindowEventNewRemoteDesktop) {
WindowController.fromWindowId(windowId).show();
}
registerActiveWindow(windowId);
return MultiWindowCallResult(windowId, null);
}
}
}
final windowId = await newSessionWindow(
type, remoteId, msg, windows, screenRect != null);
return MultiWindowCallResult(windowId, null);
}
}
Future<MultiWindowCallResult> newSession(
WindowType type,
String methodName,
String remoteId,
List<int> windows, {
String? password,
bool? forceRelay,
String? switchUuid,
bool? isRDP,
bool? isSharedPassword,
String? connToken,
}) async {
var params = {
"type": type.index,
"id": remoteId,
"password": password,
"forceRelay": forceRelay
};
if (switchUuid != null) {
params['switch_uuid'] = switchUuid;
}
if (isRDP != null) {
params['isRDP'] = isRDP;
}
if (isSharedPassword != null) {
params['isSharedPassword'] = isSharedPassword;
}
if (connToken != null) {
params['connToken'] = connToken;
}
final msg = jsonEncode(params);
// separate window for file transfer is not supported
bool openInTabs = type != WindowType.RemoteDesktop ||
mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs);
if (windows.length > 1 || !openInTabs) {
for (final windowId in windows) {
if (await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventActiveSession, remoteId)) {
return MultiWindowCallResult(windowId, null);
}
}
}
return _newSession(openInTabs, type, methodName, remoteId, windows, msg);
}
Future<MultiWindowCallResult> newRemoteDesktop(
String remoteId, {
String? password,
bool? isSharedPassword,
String? switchUuid,
bool? forceRelay,
}) async {
return await newSession(
WindowType.RemoteDesktop,
kWindowEventNewRemoteDesktop,
remoteId,
_remoteDesktopWindows,
password: password,
forceRelay: forceRelay,
switchUuid: switchUuid,
isSharedPassword: isSharedPassword,
);
}
Future<MultiWindowCallResult> newFileTransfer(
String remoteId, {
String? password,
bool? isSharedPassword,
bool? forceRelay,
String? connToken,
}) async {
return await newSession(
WindowType.FileTransfer,
kWindowEventNewFileTransfer,
remoteId,
_fileTransferWindows,
password: password,
forceRelay: forceRelay,
isSharedPassword: isSharedPassword,
connToken: connToken,
);
}
Future<MultiWindowCallResult> newViewCamera(
String remoteId, {
String? password,
bool? isSharedPassword,
String? switchUuid,
bool? forceRelay,
String? connToken,
}) async {
return await newSession(
WindowType.ViewCamera,
kWindowEventNewViewCamera,
remoteId,
_viewCameraWindows,
password: password,
forceRelay: forceRelay,
switchUuid: switchUuid,
isSharedPassword: isSharedPassword,
connToken: connToken,
);
}
Future<MultiWindowCallResult> newPortForward(
String remoteId,
bool isRDP, {
String? password,
bool? isSharedPassword,
bool? forceRelay,
String? connToken,
}) async {
return await newSession(
WindowType.PortForward,
kWindowEventNewPortForward,
remoteId,
_portForwardWindows,
password: password,
forceRelay: forceRelay,
isRDP: isRDP,
isSharedPassword: isSharedPassword,
connToken: connToken,
);
}
Future<MultiWindowCallResult> newTerminal(
String remoteId, {
String? password,
bool? isSharedPassword,
bool? forceRelay,
String? connToken,
}) async {
// Iterate through terminal windows in reverse order to prioritize
// the most recently added or used windows, as they are more likely
// to have an active session.
for (final windowId in _terminalWindows.reversed) {
if (await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventActiveSession, remoteId)) {
return MultiWindowCallResult(windowId, null);
}
}
// Terminal windows should always create new windows, not reuse
// This avoids the MissingPluginException when trying to invoke
// new_terminal on an inactive window
var params = {
"type": WindowType.Terminal.index,
"id": remoteId,
"password": password,
"forceRelay": forceRelay,
"isSharedPassword": isSharedPassword,
"connToken": connToken,
};
final msg = jsonEncode(params);
// Always create a new window for terminal
final windowId = await newSessionWindow(
WindowType.Terminal, remoteId, msg, _terminalWindows, false);
return MultiWindowCallResult(windowId, null);
}
Future<MultiWindowCallResult> call(
WindowType type, String methodName, dynamic args) async {
final wnds = _findWindowsByType(type);
if (wnds.isEmpty) {
return MultiWindowCallResult(kInvalidWindowId, null);
}
for (final windowId in wnds) {
if (_activeWindows.contains(windowId)) {
final res =
await DesktopMultiWindow.invokeMethod(windowId, methodName, args);
return MultiWindowCallResult(windowId, res);
}
}
final res =
await DesktopMultiWindow.invokeMethod(wnds[0], methodName, args);
return MultiWindowCallResult(wnds[0], res);
}
List<int> _findWindowsByType(WindowType type) {
switch (type) {
case WindowType.Main:
return [kMainWindowId];
case WindowType.RemoteDesktop:
return _remoteDesktopWindows;
case WindowType.FileTransfer:
return _fileTransferWindows;
case WindowType.ViewCamera:
return _viewCameraWindows;
case WindowType.PortForward:
return _portForwardWindows;
case WindowType.Terminal:
return _terminalWindows;
case WindowType.Unknown:
break;
}
return [];
}
void clearWindowType(WindowType type) {
switch (type) {
case WindowType.Main:
return;
case WindowType.RemoteDesktop:
_remoteDesktopWindows.clear();
break;
case WindowType.FileTransfer:
_fileTransferWindows.clear();
break;
case WindowType.ViewCamera:
_viewCameraWindows.clear();
break;
case WindowType.PortForward:
_portForwardWindows.clear();
break;
case WindowType.Terminal:
_terminalWindows.clear();
case WindowType.Unknown:
break;
}
}
void setMethodHandler(
Future<dynamic> Function(MethodCall call, int fromWindowId)? handler) {
DesktopMultiWindow.setMethodHandler(handler);
}
Future<void> closeAllSubWindows() async {
await Future.wait(WindowType.values.map((e) => _closeWindows(e)));
}
Future<void> _closeWindows(WindowType type) async {
if (type == WindowType.Main) {
// skip main window, use window manager instead
return;
}
List<int> windows = [];
try {
windows = _findWindowsByType(type);
} catch (e) {
debugPrint('Failed to getAllSubWindowIds of $type, $e');
return;
}
if (windows.isEmpty) {
return;
}
for (int i = 0; i < windows.length; i++) {
final wId = windows[i];
final shouldSavePos = type != WindowType.Terminal || i == windows.length - 1;
if (shouldSavePos) {
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
try {
await saveWindowPosition(type, windowId: wId);
} catch (e) {
debugPrint('Failed to save window position of $wId, $e');
}
}
try {
await WindowController.fromWindowId(wId).setPreventClose(false);
await WindowController.fromWindowId(wId).close();
_activeWindows.remove(wId);
} catch (e) {
debugPrint("$e");
return;
}
}
clearWindowType(type);
}
Future<List<int>> getAllSubWindowIds() async {
try {
final windows = await DesktopMultiWindow.getAllSubWindowIds();
return windows;
} catch (err) {
if (err is AssertionError) {
return [];
} else {
rethrow;
}
}
}
Set<int> getActiveWindows() {
return _activeWindows;
}
Future<void> _notifyActiveWindow() async {
for (final callback in _windowActiveCallbacks) {
await callback.call();
}
}
Future<void> registerActiveWindow(int windowId) async {
_activeWindows.add(windowId);
_inactiveWindows.remove(windowId);
await _notifyActiveWindow();
}
/// Remove active window which has [`windowId`]
///
/// [Availability]
/// This function should only be called from main window.
/// For other windows, please post a unregister(hide) event to main window handler:
/// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});`
Future<void> unregisterActiveWindow(int windowId) async {
_activeWindows.remove(windowId);
if (windowId != kMainWindowId) {
_inactiveWindows.add(windowId);
}
await _notifyActiveWindow();
}
void registerActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.add(callback);
}
void unregisterActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.remove(callback);
}
// This function is called from the main window.
// It will query the active remote windows to get their coords.
Future<List<String>> getOtherRemoteWindowCoords(int wId) async {
List<String> coords = [];
for (final windowId in _remoteDesktopWindows) {
if (windowId != wId) {
if (_activeWindows.contains(windowId)) {
final res = await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventRemoteWindowCoords, '');
if (res != null) {
coords.add(res);
}
}
}
}
return coords;
}
// This function is called from one remote window.
// Only the main window knows `_remoteDesktopWindows` and `_activeWindows`.
// So we need to call the main window to get the other remote windows' coords.
Future<List<RemoteWindowCoords>> getOtherRemoteWindowCoordsFromMain() async {
List<RemoteWindowCoords> coords = [];
// Call the main window to get the coords of other remote windows.
String res = await DesktopMultiWindow.invokeMethod(
kMainWindowId, kWindowEventRemoteWindowCoords, kWindowId.toString());
List<dynamic> list = jsonDecode(res);
for (var item in list) {
coords.add(RemoteWindowCoords.fromJson(jsonDecode(item)));
}
return coords;
}
}
final rustDeskWinManager = RustDeskMultiWindowManager.instance;
+45
View File
@@ -0,0 +1,45 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/common.dart';
enum SystemWindowTheme { light, dark }
/// The platform channel for RustDesk.
class RdPlatformChannel {
RdPlatformChannel._();
static final RdPlatformChannel _windowUtil = RdPlatformChannel._();
static RdPlatformChannel get instance => _windowUtil;
final MethodChannel _hostMethodChannel =
MethodChannel("org.rustdesk.rustdesk/host");
/// Bump the position of the mouse cursor, if applicable
Future<bool> bumpMouse({required int dx, required int dy}) async {
// No debug output; this call is too chatty.
bool? result = await _hostMethodChannel
.invokeMethod("bumpMouse", {"dx": dx, "dy": dy});
return result ?? false;
}
/// Change the theme of the system window
Future<void> changeSystemWindowTheme(SystemWindowTheme theme) {
assert(isMacOS);
if (kDebugMode) {
print(
"[Window ${kWindowId ?? 'Main'}] change system window theme to ${theme.name}");
}
return _hostMethodChannel
.invokeMethod("setWindowTheme", {"themeName": theme.name});
}
/// Terminate .app manually.
Future<void> terminate() {
assert(isMacOS);
return _hostMethodChannel.invokeMethod("terminate");
}
}
@@ -0,0 +1,58 @@
/// A small helper for accumulating fractional mouse deltas and emitting integer deltas.
///
/// Relative mouse mode uses integer deltas on the wire, but Flutter pointer deltas
/// are doubles. This accumulator preserves sub-pixel movement by carrying the
/// fractional remainder across events.
class RelativeMouseDelta {
final int x;
final int y;
const RelativeMouseDelta(this.x, this.y);
}
/// Accumulates fractional mouse deltas and returns integer deltas when available.
class RelativeMouseAccumulator {
double _fracX = 0.0;
double _fracY = 0.0;
/// Adds a delta and returns an integer delta when at least one axis reaches a
/// magnitude of 1px (after truncation towards zero).
///
/// If [maxDelta] is > 0, the returned integer delta is clamped to
/// [-maxDelta, maxDelta] on each axis.
RelativeMouseDelta? add(
double dx,
double dy, {
required int maxDelta,
}) {
// Guard against misuse: negative maxDelta would silently disable clamping.
assert(maxDelta >= 0, 'maxDelta must be non-negative');
_fracX += dx;
_fracY += dy;
int intX = _fracX.truncate();
int intY = _fracY.truncate();
if (intX == 0 && intY == 0) {
return null;
}
// Clamp before subtracting so excess movement is preserved in the accumulator
// rather than being permanently discarded during spikes.
if (maxDelta > 0) {
intX = intX.clamp(-maxDelta, maxDelta);
intY = intY.clamp(-maxDelta, maxDelta);
}
_fracX -= intX;
_fracY -= intY;
return RelativeMouseDelta(intX, intY);
}
void reset() {
_fracX = 0.0;
_fracY = 0.0;
}
}
+34
View File
@@ -0,0 +1,34 @@
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:uuid/uuid.dart';
/// Clamp custom scale percent to supported bounds.
/// Keep this in sync with the slider's minimum in the desktop toolbar UI.
///
/// This function exists to ensure consistent clamping behavior across the app
/// and to provide a single point of reference for the valid scale range.
int clampCustomScalePercent(int percent) {
return percent.clamp(kScaleCustomMinPercent, kScaleCustomMaxPercent);
}
/// Parse a string percent and clamp. Defaults to 100 when invalid.
int parseCustomScalePercent(String? s, {int defaultPercent = 100}) {
final parsed = int.tryParse(s ?? '') ?? defaultPercent;
return clampCustomScalePercent(parsed);
}
/// Convert a percent value to scale factor after clamping.
double percentToScale(int percent) => clampCustomScalePercent(percent) / 100.0;
/// Fetch, parse and clamp the custom scale percent for a session.
Future<int> getSessionCustomScalePercent(UuidValue sessionId) async {
final opt = await bind.sessionGetFlutterOption(
sessionId: sessionId, k: kCustomScalePercentKey);
return parseCustomScalePercent(opt);
}
/// Fetch and compute the custom scale factor for a session.
Future<double> getSessionCustomScale(UuidValue sessionId) async {
final p = await getSessionCustomScalePercent(sessionId);
return percentToScale(p);
}