Flutter web, custom cursor (#7545)
* Flutter web, custom cursor Signed-off-by: fufesou <shuanglongchen@yeah.net> * trivial changes Signed-off-by: fufesou <shuanglongchen@yeah.net> * Flutter web, custom cursor, use date after 'updateGetKey()' Signed-off-by: fufesou <shuanglongchen@yeah.net> * trivial changes Signed-off-by: fufesou <shuanglongchen@yeah.net> --------- Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
parent
4b0e88ce46
commit
3ef9824d8e
@ -3,12 +3,9 @@ import 'dart:async';
|
|||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.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 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
||||||
|
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
@ -26,6 +23,9 @@ import '../widgets/remote_toolbar.dart';
|
|||||||
import '../widgets/kb_layout_type_chooser.dart';
|
import '../widgets/kb_layout_type_chooser.dart';
|
||||||
import '../widgets/tabbar_widget.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<bool> _firstEnterImage = SimpleWrapper(false);
|
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
|
||||||
|
|
||||||
// Used to skip session close if "move to new window" is clicked.
|
// Used to skip session close if "move to new window" is clicked.
|
||||||
@ -667,48 +667,16 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
MouseCursor _buildCustomCursor(BuildContext context, double scale) {
|
||||||
final cursor = Provider.of<CursorModel>(context);
|
final cursor = Provider.of<CursorModel>(context);
|
||||||
final cache = cursor.cache ?? preDefaultCursor.cache;
|
final cache = cursor.cache ?? preDefaultCursor.cache;
|
||||||
return _buildCursorOfCache(cursor, scale, cache);
|
return buildCursorOfCache(cursor, scale, cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
|
MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
|
||||||
final cursor = Provider.of<CursorModel>(context);
|
final cursor = Provider.of<CursorModel>(context);
|
||||||
final cache = preForbiddenCursor.cache;
|
final cache = preForbiddenCursor.cache;
|
||||||
return _buildCursorOfCache(cursor, scale, cache);
|
return buildCursorOfCache(cursor, scale, cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCrossScrollbarFromLayout(
|
Widget _buildCrossScrollbarFromLayout(
|
||||||
|
@ -25,7 +25,6 @@ import 'package:flutter_hbb/common/shared_state.dart';
|
|||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
import 'package:image/image.dart' as img2;
|
import 'package:image/image.dart' as img2;
|
||||||
import 'package:flutter_custom_cursor/cursor_manager.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@ -39,6 +38,8 @@ import 'platform_model.dart';
|
|||||||
|
|
||||||
import 'package:flutter_hbb/generated_bridge.dart'
|
import 'package:flutter_hbb/generated_bridge.dart'
|
||||||
if (dart.library.html) 'package:flutter_hbb/web/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<String, dynamic> evt, String id);
|
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
|
||||||
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
|
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
|
||||||
@ -1951,7 +1952,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
final keys = {...cachedKeys};
|
final keys = {...cachedKeys};
|
||||||
for (var k in keys) {
|
for (var k in keys) {
|
||||||
debugPrint("deleting cursor with key $k");
|
debugPrint("deleting cursor with key $k");
|
||||||
CursorManager.instance.deleteCursor(k);
|
deleteCustomCursor(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
43
flutter/lib/native/custom_cursor.dart
Normal file
43
flutter/lib/native/custom_cursor.dart
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
121
flutter/lib/web/custom_cursor.dart
Normal file
121
flutter/lib/web/custom_cursor.dart
Normal file
@ -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<String, CursorData> _cursors = <String, CursorData>{};
|
||||||
|
String latestKey = '';
|
||||||
|
|
||||||
|
CursorManager._();
|
||||||
|
static CursorManager instance = CursorManager._();
|
||||||
|
|
||||||
|
Future<void> registerCursor(CursorData data) async {
|
||||||
|
_cursors[data.key] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteCursor(String key) async {
|
||||||
|
_cursors.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void> 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);
|
||||||
|
}
|
||||||
|
}
|
@ -333,6 +333,9 @@ window.setByName = (name, value) => {
|
|||||||
break;
|
break;
|
||||||
case 'change_prefer_codec':
|
case 'change_prefer_codec':
|
||||||
curConn.changePreferCodec(value);
|
curConn.changePreferCodec(value);
|
||||||
|
case 'cursor':
|
||||||
|
setCustomCursor(value);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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 ==========================
|
// ========================== options begin ==========================
|
||||||
function setUserDefaultOption(value) {
|
function setUserDefaultOption(value) {
|
||||||
try {
|
try {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user