diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 23d597937..ecd9e62bb 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -3,12 +3,9 @@ import 'dart:async'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_custom_cursor/cursor_manager.dart' - as custom_cursor_manager; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; import '../../consts.dart'; @@ -26,6 +23,9 @@ import '../widgets/remote_toolbar.dart'; import '../widgets/kb_layout_type_chooser.dart'; import '../widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/native/custom_cursor.dart' + if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart'; + final SimpleWrapper _firstEnterImage = SimpleWrapper(false); // Used to skip session close if "move to new window" is clicked. @@ -667,48 +667,16 @@ class _ImagePaintState extends State { ); } - MouseCursor _buildCursorOfCache( - CursorModel cursor, double scale, CursorData? cache) { - // TODO: web cursor - if (isWeb) { - return MouseCursor.defer; - } - - if (cache == null) { - return MouseCursor.defer; - } else { - final key = cache.updateGetKey(scale); - if (!cursor.cachedKeys.contains(key)) { - debugPrint( - "Register custom cursor with key $key (${cache.hotx},${cache.hoty})"); - // [Safety] - // It's ok to call async registerCursor in current synchronous context, - // because activating the cursor is also an async call and will always - // be executed after this. - custom_cursor_manager.CursorManager.instance - .registerCursor(custom_cursor_manager.CursorData() - ..buffer = cache.data! - ..height = (cache.height * cache.scale).toInt() - ..width = (cache.width * cache.scale).toInt() - ..hotX = cache.hotx - ..hotY = cache.hoty - ..name = key); - cursor.addKey(key); - } - return FlutterCustomMemoryImageCursor(key: key); - } - } - MouseCursor _buildCustomCursor(BuildContext context, double scale) { final cursor = Provider.of(context); final cache = cursor.cache ?? preDefaultCursor.cache; - return _buildCursorOfCache(cursor, scale, cache); + return buildCursorOfCache(cursor, scale, cache); } MouseCursor _buildDisabledCursor(BuildContext context, double scale) { final cursor = Provider.of(context); final cache = preForbiddenCursor.cache; - return _buildCursorOfCache(cursor, scale, cache); + return buildCursorOfCache(cursor, scale, cache); } Widget _buildCrossScrollbarFromLayout( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 0ed6fc8e8..3f316ef46 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -25,7 +25,6 @@ import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:tuple/tuple.dart'; import 'package:image/image.dart' as img2; -import 'package:flutter_custom_cursor/cursor_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:uuid/uuid.dart'; @@ -39,6 +38,8 @@ import 'platform_model.dart'; import 'package:flutter_hbb/generated_bridge.dart' if (dart.library.html) 'package:flutter_hbb/web/bridge.dart'; +import 'package:flutter_hbb/native/custom_cursor.dart' + if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart'; typedef HandleMsgBox = Function(Map evt, String id); typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool); @@ -1951,7 +1952,7 @@ class CursorModel with ChangeNotifier { final keys = {...cachedKeys}; for (var k in keys) { debugPrint("deleting cursor with key $k"); - CursorManager.instance.deleteCursor(k); + deleteCustomCursor(k); } } } diff --git a/flutter/lib/native/custom_cursor.dart b/flutter/lib/native/custom_cursor.dart new file mode 100644 index 000000000..3e53f3cc5 --- /dev/null +++ b/flutter/lib/native/custom_cursor.dart @@ -0,0 +1,43 @@ +import 'package:flutter_custom_cursor/cursor_manager.dart' + as custom_cursor_manager; +import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'package:flutter_hbb/models/model.dart'; + +deleteCustomCursor(String key) => + custom_cursor_manager.CursorManager.instance.deleteCursor(key); + +MouseCursor buildCursorOfCache( + CursorModel cursor, double scale, CursorData? cache) { + if (cache == null) { + return MouseCursor.defer; + } else { + final key = cache.updateGetKey(scale); + if (!cursor.cachedKeys.contains(key)) { + // data should be checked here, because it may be changed after `updateGetKey()` + final data = cache.data; + if (data == null) { + return MouseCursor.defer; + } + debugPrint( + "Register custom cursor with key $key (${cache.hotx},${cache.hoty})"); + // [Safety] + // It's ok to call async registerCursor in current synchronous context, + // because activating the cursor is also an async call and will always + // be executed after this. + custom_cursor_manager.CursorManager.instance + .registerCursor(custom_cursor_manager.CursorData() + ..name = key + ..buffer = data + ..width = (cache.width * cache.scale).toInt() + ..height = (cache.height * cache.scale).toInt() + ..hotX = cache.hotx + ..hotY = cache.hoty); + cursor.addKey(key); + } + return FlutterCustomMemoryImageCursor(key: key); + } +} diff --git a/flutter/lib/web/custom_cursor.dart b/flutter/lib/web/custom_cursor.dart new file mode 100644 index 000000000..fd1fc4a18 --- /dev/null +++ b/flutter/lib/web/custom_cursor.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; +import 'dart:js' as js; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'package:flutter_hbb/models/model.dart' as model; + +class CursorData { + final String key; + final String url; + final double hotX; + final double hotY; + final int width; + final int height; + + CursorData({ + required this.key, + required this.url, + required this.hotX, + required this.hotY, + required this.width, + required this.height, + }); +} + +/// The cursor manager +class CursorManager { + final Map _cursors = {}; + String latestKey = ''; + + CursorManager._(); + static CursorManager instance = CursorManager._(); + + Future registerCursor(CursorData data) async { + _cursors[data.key] = data; + } + + Future deleteCursor(String key) async { + _cursors.remove(key); + } + + Future setSystemCursor(String key) async { + if (latestKey == key) { + return; + } + latestKey = key; + + final CursorData? cursorData = _cursors[key]; + if (cursorData != null) { + js.context.callMethod('setByName', [ + 'cursor', + jsonEncode({ + 'url': cursorData.url, + 'hotx': cursorData.hotX.toInt(), + 'hoty': cursorData.hotY.toInt(), + }) + ]); + } + } +} + +class FlutterCustomMemoryImageCursor extends MouseCursor { + final String key; + const FlutterCustomMemoryImageCursor({required this.key}); + + @override + MouseCursorSession createSession(int device) => + _FlutterCustomMemoryImageCursorSession(this, device); + + @override + String get debugDescription => + objectRuntimeType(this, 'FlutterCustomMemoryImageCursor'); +} + +class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession { + _FlutterCustomMemoryImageCursorSession( + FlutterCustomMemoryImageCursor cursor, int device) + : super(cursor, device); + + @override + FlutterCustomMemoryImageCursor get cursor => + super.cursor as FlutterCustomMemoryImageCursor; + + @override + Future activate() async { + await CursorManager.instance.setSystemCursor(cursor.key); + } + + @override + void dispose() {} +} + +deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key); + +MouseCursor buildCursorOfCache( + model.CursorModel cursor, double scale, model.CursorData? cache) { + if (cache == null) { + return MouseCursor.defer; + } else { + final key = cache.updateGetKey(scale); + if (!cursor.cachedKeys.contains(key)) { + // data should be checked here, because it may be changed after `updateGetKey()` + final data = cache.data; + if (data == null) { + return MouseCursor.defer; + } + debugPrint( + "Register custom cursor with key $key (${cache.hotx},${cache.hoty})"); + CursorManager.instance.registerCursor(CursorData( + key: key, + url: 'data:image/rgba;base64,${base64Encode(data)}', + width: (cache.width * cache.scale).toInt(), + height: (cache.height * cache.scale).toInt(), + hotX: cache.hotx, + hotY: cache.hoty)); + cursor.addKey(key); + } + return FlutterCustomMemoryImageCursor(key: key); + } +} diff --git a/flutter/web/js/src/globals.js b/flutter/web/js/src/globals.js index 68c665f64..5c2823dbc 100644 --- a/flutter/web/js/src/globals.js +++ b/flutter/web/js/src/globals.js @@ -333,6 +333,9 @@ window.setByName = (name, value) => { break; case 'change_prefer_codec': curConn.changePreferCodec(value); + case 'cursor': + setCustomCursor(value); + break; default: break; } @@ -552,6 +555,23 @@ export function getVersionNumber(v) { } } +// Set the cursor for the flutter-view element +function setCustomCursor(value) { + try { + const obj = JSON.parse(value); + // document querySelector or evaluate can not find the custom element + var body = document.body; + for (var i = 0; i < body.children.length; i++) { + var child = body.children[i]; + if (child.tagName == 'FLUTTER-VIEW') { + child.style.cursor = `url(${obj.url}) ${obj.hotx} ${obj.hoty}, auto`; + } + } + } catch (e) { + console.error('Failed to set custom cursor: ' + e.message); + } +} + // ========================== options begin ========================== function setUserDefaultOption(value) { try {