Merge pull request #2100 from fufesou/fix_cursor

Fix cursor
This commit is contained in:
RustDesk 2022-11-14 22:07:05 +08:00 committed by GitHub
commit 366e287113
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 171 additions and 103 deletions

View File

@ -251,14 +251,12 @@ class _RemotePageState extends State<RemotePage>
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
} }
class ImagePaint extends StatelessWidget { class ImagePaint extends StatefulWidget {
final String id; final String id;
final Rx<bool> cursorOverImage; final Rx<bool> cursorOverImage;
final Rx<bool> keyboardEnabled; final Rx<bool> keyboardEnabled;
final Rx<bool> remoteCursorMoved; final Rx<bool> remoteCursorMoved;
final Widget Function(Widget)? listenerBuilder; final Widget Function(Widget)? listenerBuilder;
final ScrollController _horizontal = ScrollController();
final ScrollController _vertical = ScrollController();
ImagePaint( ImagePaint(
{Key? key, {Key? key,
@ -269,6 +267,21 @@ class ImagePaint extends StatelessWidget {
this.listenerBuilder}) this.listenerBuilder})
: super(key: key); : super(key: key);
@override
State<StatefulWidget> createState() => _ImagePaintState();
}
class _ImagePaintState extends State<ImagePaint> {
bool _lastRemoteCursorMoved = false;
final ScrollController _horizontal = ScrollController();
final ScrollController _vertical = ScrollController();
String get id => widget.id;
Rx<bool> get cursorOverImage => widget.cursorOverImage;
Rx<bool> get keyboardEnabled => widget.keyboardEnabled;
Rx<bool> get remoteCursorMoved => widget.remoteCursorMoved;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context); final m = Provider.of<ImageModel>(context);
@ -278,9 +291,18 @@ class ImagePaint extends StatelessWidget {
mouseRegion({child}) => Obx(() => MouseRegion( mouseRegion({child}) => Obx(() => MouseRegion(
cursor: cursorOverImage.isTrue cursor: cursorOverImage.isTrue
? keyboardEnabled.isTrue ? keyboardEnabled.isTrue
? (remoteCursorMoved.isTrue ? (() {
? SystemMouseCursors.none if (remoteCursorMoved.isTrue) {
: _buildCustomCursor(context, s)) _lastRemoteCursorMoved = true;
return SystemMouseCursors.none;
} else {
if (_lastRemoteCursorMoved) {
_lastRemoteCursorMoved = false;
_firstEnterImage.value = true;
}
return _buildCustomCursor(context, s);
}
}())
: _buildDisabledCursor(context, s) : _buildDisabledCursor(context, s)
: MouseCursor.defer, : MouseCursor.defer,
onHover: (evt) {}, onHover: (evt) {},
@ -340,10 +362,8 @@ class ImagePaint extends StatelessWidget {
return FlutterCustomMemoryImageCursor( return FlutterCustomMemoryImageCursor(
pixbuf: cache.data, pixbuf: cache.data,
key: key, key: key,
// hotx: cache.hotx, hotx: cache.hotx,
// hoty: cache.hoty, hoty: cache.hoty,
hotx: 0,
hoty: 0,
imageWidth: (cache.width * cache.scale).toInt(), imageWidth: (cache.width * cache.scale).toInt(),
imageHeight: (cache.height * cache.scale).toInt(), imageHeight: (cache.height * cache.scale).toInt(),
); );
@ -488,11 +508,19 @@ class CursorPaint extends StatelessWidget {
final m = Provider.of<CursorModel>(context); final m = Provider.of<CursorModel>(context);
final c = Provider.of<CanvasModel>(context); final c = Provider.of<CanvasModel>(context);
// final adjust = m.adjustForKeyboard(); // final adjust = m.adjustForKeyboard();
double hotx = m.hotx;
double hoty = m.hoty;
if (m.image == null) {
if (m.defaultCache != null) {
hotx = m.defaultImage!.width / 2;
hoty = m.defaultImage!.height / 2;
}
}
return CustomPaint( return CustomPaint(
painter: ImagePainter( painter: ImagePainter(
image: m.image, image: m.image ?? m.defaultImage,
x: m.x - m.hotx + c.x / c.scale, x: m.x - hotx + c.x / c.scale,
y: m.y - m.hoty + c.y / c.scale, y: m.y - hoty + c.y / c.scale,
scale: c.scale), scale: c.scale),
); );
} }

View File

@ -862,11 +862,19 @@ class CursorPaint extends StatelessWidget {
final c = Provider.of<CanvasModel>(context); final c = Provider.of<CanvasModel>(context);
final adjust = gFFI.cursorModel.adjustForKeyboard(); final adjust = gFFI.cursorModel.adjustForKeyboard();
var s = c.scale; var s = c.scale;
double hotx = m.hotx;
double hoty = m.hoty;
if (m.image == null) {
if (m.defaultCache != null) {
hotx = m.defaultImage!.width / 2;
hoty = m.defaultImage!.height / 2;
}
}
return CustomPaint( return CustomPaint(
painter: ImagePainter( painter: ImagePainter(
image: m.image, image: m.image ?? m.defaultImage,
x: m.x * s - m.hotx + c.x, x: m.x * s - hotx * s + c.x,
y: m.y * s - m.hoty + c.y - adjust, y: m.y * s - hoty * s + c.y - adjust,
scale: 1), scale: 1),
); );
} }

View File

@ -42,7 +42,7 @@ class InputModel {
// mouse // mouse
final isPhysicalMouse = false.obs; final isPhysicalMouse = false.obs;
int _lastMouseDownButtons = 0; int _lastMouseDownButtons = 0;
Offset last_mouse_pos = Offset.zero; Offset lastMousePos = Offset.zero;
get id => parent.target?.id ?? ""; get id => parent.target?.id ?? "";
@ -308,23 +308,23 @@ class InputModel {
double y = max(0.0, evt['y']); double y = max(0.0, evt['y']);
final cursorModel = parent.target!.cursorModel; final cursorModel = parent.target!.cursorModel;
if (cursorModel.is_peer_control_protected) { if (cursorModel.isPeerControlProtected) {
last_mouse_pos = ui.Offset(x, y); lastMousePos = ui.Offset(x, y);
return; return;
} }
if (!cursorModel.got_mouse_control) { if (!cursorModel.gotMouseControl) {
bool self_get_control = bool selfGetControl =
(x - last_mouse_pos.dx).abs() > kMouseControlDistance || (x - lastMousePos.dx).abs() > kMouseControlDistance ||
(y - last_mouse_pos.dy).abs() > kMouseControlDistance; (y - lastMousePos.dy).abs() > kMouseControlDistance;
if (self_get_control) { if (selfGetControl) {
cursorModel.got_mouse_control = true; cursorModel.gotMouseControl = true;
} else { } else {
last_mouse_pos = ui.Offset(x, y); lastMousePos = ui.Offset(x, y);
return; return;
} }
} }
last_mouse_pos = ui.Offset(x, y); lastMousePos = ui.Offset(x, y);
var type = ''; var type = '';
var isMove = false; var isMove = false;

View File

@ -721,11 +721,14 @@ class CursorData {
height: (height * scale).toInt(), height: (height * scale).toInt(),
) )
.getBytes(format: img2.Format.bgra); .getBytes(format: img2.Format.bgra);
hotx = (width * scale) / 2;
hoty = (height * scale) / 2;
} }
} }
this.scale = scale; this.scale = scale;
if (hotx > 0 && hoty > 0) {
// default cursor data
hotx = (width * scale) / 2;
hoty = (height * scale) / 2;
}
return scale; return scale;
} }
@ -737,6 +740,7 @@ class CursorData {
class CursorModel with ChangeNotifier { class CursorModel with ChangeNotifier {
ui.Image? _image; ui.Image? _image;
ui.Image? _defaultImage;
final _images = <int, Tuple3<ui.Image, double, double>>{}; final _images = <int, Tuple3<ui.Image, double, double>>{};
CursorData? _cache; CursorData? _cache;
final _defaultCacheId = -1; final _defaultCacheId = -1;
@ -749,13 +753,14 @@ class CursorModel with ChangeNotifier {
double _hoty = 0; double _hoty = 0;
double _displayOriginX = 0; double _displayOriginX = 0;
double _displayOriginY = 0; double _displayOriginY = 0;
bool got_mouse_control = true; bool gotMouseControl = true;
DateTime _last_peer_mouse = DateTime.now() DateTime _lastPeerMouse = DateTime.now()
.subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec)); .subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec));
String id = ''; String id = '';
WeakReference<FFI> parent; WeakReference<FFI> parent;
ui.Image? get image => _image; ui.Image? get image => _image;
ui.Image? get defaultImage => _defaultImage;
CursorData? get cache => _cache; CursorData? get cache => _cache;
CursorData? get defaultCache => _getDefaultCache(); CursorData? get defaultCache => _getDefaultCache();
@ -767,34 +772,52 @@ class CursorModel with ChangeNotifier {
double get hotx => _hotx; double get hotx => _hotx;
double get hoty => _hoty; double get hoty => _hoty;
bool get is_peer_control_protected => bool get isPeerControlProtected =>
DateTime.now().difference(_last_peer_mouse).inMilliseconds < DateTime.now().difference(_lastPeerMouse).inMilliseconds <
kMouseControlTimeoutMSec; kMouseControlTimeoutMSec;
CursorModel(this.parent); CursorModel(this.parent) {
_getDefaultImage();
_getDefaultCache();
}
Set<String> get cachedKeys => _cacheKeys; Set<String> get cachedKeys => _cacheKeys;
addKey(String key) => _cacheKeys.add(key); addKey(String key) => _cacheKeys.add(key);
Future<ui.Image?> _getDefaultImage() async {
if (_defaultImage == null) {
final defaultImg = defaultCursorImage!;
// This function is called only one time, no need to care about the performance.
Uint8List data = defaultImg.getBytes(format: img2.Format.rgba);
_defaultImage = await img.decodeImageFromPixels(
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
}
return _defaultImage;
}
CursorData? _getDefaultCache() { CursorData? _getDefaultCache() {
if (_defaultCache == null) { if (_defaultCache == null) {
Uint8List data;
double scale = 1.0;
double hotx = (defaultCursorImage!.width * scale) / 2;
double hoty = (defaultCursorImage!.height * scale) / 2;
if (Platform.isWindows) { if (Platform.isWindows) {
Uint8List data = defaultCursorImage!.getBytes(format: img2.Format.bgra); data = defaultCursorImage!.getBytes(format: img2.Format.bgra);
_hotx = defaultCursorImage!.width / 2; } else {
_hoty = defaultCursorImage!.height / 2; data = Uint8List.fromList(img2.encodePng(defaultCursorImage!));
_defaultCache = CursorData(
peerId: id,
id: _defaultCacheId,
image: defaultCursorImage?.clone(),
scale: 1.0,
data: data,
hotx: _hotx,
hoty: _hoty,
width: defaultCursorImage!.width,
height: defaultCursorImage!.height,
);
} }
_defaultCache = CursorData(
peerId: id,
id: _defaultCacheId,
image: defaultCursorImage?.clone(),
scale: scale,
data: data,
hotx: hotx,
hoty: hoty,
width: defaultCursorImage!.width,
height: defaultCursorImage!.height,
);
} }
return _defaultCache; return _defaultCache;
} }
@ -926,13 +949,15 @@ class CursorModel with ChangeNotifier {
var height = int.parse(evt['height']); var height = int.parse(evt['height']);
List<dynamic> colors = json.decode(evt['colors']); List<dynamic> colors = json.decode(evt['colors']);
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
var pid = parent.target?.id;
final image = await img.decodeImageFromPixels( final image = await img.decodeImageFromPixels(
rgba, width, height, ui.PixelFormat.rgba8888); rgba, width, height, ui.PixelFormat.rgba8888);
if (parent.target?.id != pid) return;
_image = image; _image = image;
_images[id] = Tuple3(image, _hotx, _hoty); if (await _updateCache(image, id, width, height)) {
await _updateCache(image, id, width, height); _images[id] = Tuple3(image, _hotx, _hoty);
} else {
_hotx = 0;
_hoty = 0;
}
try { try {
// my throw exception, because the listener maybe already dispose // my throw exception, because the listener maybe already dispose
notifyListeners(); notifyListeners();
@ -941,44 +966,33 @@ class CursorModel with ChangeNotifier {
} }
} }
_updateCache(ui.Image image, int id, int w, int h) async { Future<bool> _updateCache(ui.Image image, int id, int w, int h) async {
Uint8List? data; ui.ImageByteFormat imgFormat = ui.ImageByteFormat.png;
img2.Image? image2;
if (Platform.isWindows) { if (Platform.isWindows) {
ByteData? data2 = imgFormat = ui.ImageByteFormat.rawRgba;
await image.toByteData(format: ui.ImageByteFormat.rawRgba);
if (data2 != null) {
data = data2.buffer.asUint8List();
image2 = img2.Image.fromBytes(w, h, data);
} else {
data = defaultCursorImage?.getBytes(format: img2.Format.bgra);
image2 = defaultCursorImage?.clone();
_hotx = defaultCursorImage!.width / 2;
_hoty = defaultCursorImage!.height / 2;
}
} else {
ByteData? data2 = await image.toByteData(format: ui.ImageByteFormat.png);
if (data2 != null) {
data = data2.buffer.asUint8List();
} else {
data = Uint8List.fromList(img2.encodePng(defaultCursorImage!));
_hotx = defaultCursorImage!.width / 2;
_hoty = defaultCursorImage!.height / 2;
}
} }
ByteData? imgBytes = await image.toByteData(format: imgFormat);
if (imgBytes == null) {
return false;
}
Uint8List? data = imgBytes.buffer.asUint8List();
_cache = CursorData( _cache = CursorData(
peerId: this.id, peerId: this.id,
id: id, id: id,
image: image2, image: Platform.isWindows ? img2.Image.fromBytes(w, h, data) : null,
scale: 1.0, scale: 1.0,
data: data, data: data,
hotx: _hotx, hotx: 0,
hoty: _hoty, hoty: 0,
// hotx: _hotx,
// hoty: _hoty,
width: w, width: w,
height: h, height: h,
); );
_cacheMap[id] = _cache!; _cacheMap[id] = _cache!;
return true;
} }
updateCursorId(Map<String, dynamic> evt) async { updateCursorId(Map<String, dynamic> evt) async {
@ -998,8 +1012,8 @@ class CursorModel with ChangeNotifier {
/// Update the cursor position. /// Update the cursor position.
updateCursorPosition(Map<String, dynamic> evt, String id) async { updateCursorPosition(Map<String, dynamic> evt, String id) async {
got_mouse_control = false; gotMouseControl = false;
_last_peer_mouse = DateTime.now(); _lastPeerMouse = DateTime.now();
_x = double.parse(evt['x']); _x = double.parse(evt['x']);
_y = double.parse(evt['y']); _y = double.parse(evt['y']);
try { try {

View File

@ -72,7 +72,7 @@ dependencies:
flutter_custom_cursor: flutter_custom_cursor:
git: git:
url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor
ref: ac3c1bf816197863cdcfa42d008962ff644132b0 ref: bfb19c84a8244771488bc05cc5f9c9b5e0324cfd
window_size: window_size:
git: git:
url: https://github.com/google/flutter-desktop-embedding.git url: https://github.com/google/flutter-desktop-embedding.git

View File

@ -126,7 +126,7 @@ pub fn new_pos() -> GenericService {
} }
fn update_last_cursor_pos(x: i32, y: i32) { fn update_last_cursor_pos(x: i32, y: i32) {
let mut lock = LATEST_CURSOR_POS.lock().unwrap(); let mut lock = LATEST_SYS_CURSOR_POS.lock().unwrap();
if lock.1 .0 != x || lock.1 .1 != y { if lock.1 .0 != x || lock.1 .1 != y {
(lock.0, lock.1) = (Instant::now(), (x, y)) (lock.0, lock.1) = (Instant::now(), (x, y))
} }
@ -144,7 +144,7 @@ fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> {
}); });
let exclude = { let exclude = {
let now = get_time(); let now = get_time();
let lock = LATEST_INPUT_CURSOR.lock().unwrap(); let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap();
if now - lock.time < 300 { if now - lock.time < 300 {
lock.conn lock.conn
} else { } else {
@ -203,12 +203,13 @@ lazy_static::lazy_static! {
Arc::new(Mutex::new(Enigo::new())) Arc::new(Mutex::new(Enigo::new()))
}; };
static ref KEYS_DOWN: Arc<Mutex<HashMap<u64, Instant>>> = Default::default(); static ref KEYS_DOWN: Arc<Mutex<HashMap<u64, Instant>>> = Default::default();
static ref LATEST_INPUT_CURSOR: Arc<Mutex<Input>> = Default::default(); static ref LATEST_PEER_INPUT_CURSOR: Arc<Mutex<Input>> = Default::default();
static ref LATEST_CURSOR_POS: Arc<Mutex<(Instant, (i32, i32))>> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (0, 0)))); static ref LATEST_SYS_CURSOR_POS: Arc<Mutex<(Instant, (i32, i32))>> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (0, 0))));
} }
static EXITING: AtomicBool = AtomicBool::new(false); static EXITING: AtomicBool = AtomicBool::new(false);
const MOUSE_MOVE_PROTECTION_TIMEOUT: Duration = Duration::from_millis(1_000); const MOUSE_MOVE_PROTECTION_TIMEOUT: Duration = Duration::from_millis(1_000);
// Actual diff of (x,y) is (1,1) here. But 5 may be tolerant.
const MOUSE_ACTIVE_DISTANCE: i32 = 5; const MOUSE_ACTIVE_DISTANCE: i32 = 5;
// mac key input must be run in main thread, otherwise crash on >= osx 10.15 // mac key input must be run in main thread, otherwise crash on >= osx 10.15
@ -396,24 +397,42 @@ fn fix_modifiers(modifiers: &[EnumOrUnknown<ControlKey>], en: &mut Enigo, ck: i3
fn active_mouse_(conn: i32) -> bool { fn active_mouse_(conn: i32) -> bool {
// out of time protection // out of time protection
if LATEST_CURSOR_POS.lock().unwrap().0.elapsed() > MOUSE_MOVE_PROTECTION_TIMEOUT { if LATEST_SYS_CURSOR_POS.lock().unwrap().0.elapsed() > MOUSE_MOVE_PROTECTION_TIMEOUT {
return true; return true;
} }
let mut last_input = LATEST_INPUT_CURSOR.lock().unwrap();
// last conn input may be protected // last conn input may be protected
if last_input.conn != conn { if LATEST_PEER_INPUT_CURSOR.lock().unwrap().conn != conn {
return false; return false;
} }
// check if input is in valid range let in_actived_dist = |a: i32, b: i32| -> bool { (a - b).abs() < MOUSE_ACTIVE_DISTANCE };
// Check if input is in valid range
match crate::get_cursor_pos() { match crate::get_cursor_pos() {
Some((x, y)) => { Some((x, y)) => {
let can_active = (last_input.x - x).abs() < MOUSE_ACTIVE_DISTANCE let (last_in_x, last_in_y) = {
&& (last_input.y - y).abs() < MOUSE_ACTIVE_DISTANCE; let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap();
(lock.x, lock.y)
};
let mut can_active =
in_actived_dist(last_in_x, x) && in_actived_dist(last_in_y, y);
// The cursor may not have been moved to last input position if system is busy now.
// While this is not a common case, we check it again after some time later.
if !can_active { if !can_active {
last_input.x = -MOUSE_ACTIVE_DISTANCE * 2; // 10 micros may be enough for system to move cursor.
last_input.y = -MOUSE_ACTIVE_DISTANCE * 2; // We do not care about the situation which system is too slow(more than 10 micros is required).
std::thread::sleep(std::time::Duration::from_micros(10));
// Sleep here can also somehow suppress delay accumulation.
if let Some((x2, y2)) = crate::get_cursor_pos() {
can_active =
in_actived_dist(last_in_x, x2) && in_actived_dist(last_in_y, y2);
}
}
if !can_active {
let mut lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap();
lock.x = INVALID_CURSOR_POS / 2;
lock.y = INVALID_CURSOR_POS / 2;
} }
can_active can_active
} }
@ -434,15 +453,6 @@ fn handle_mouse_(evt: &MouseEvent, conn: i32) {
crate::platform::windows::try_change_desktop(); crate::platform::windows::try_change_desktop();
let buttons = evt.mask >> 3; let buttons = evt.mask >> 3;
let evt_type = evt.mask & 0x7; let evt_type = evt.mask & 0x7;
if evt_type == 0 {
let time = get_time();
*LATEST_INPUT_CURSOR.lock().unwrap() = Input {
time,
conn,
x: evt.x,
y: evt.y,
};
}
let mut en = ENIGO.lock().unwrap(); let mut en = ENIGO.lock().unwrap();
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
let mut to_release = Vec::new(); let mut to_release = Vec::new();
@ -467,6 +477,14 @@ fn handle_mouse_(evt: &MouseEvent, conn: i32) {
} }
match evt_type { match evt_type {
0 => { 0 => {
let time = get_time();
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
time,
conn,
x: evt.x,
y: evt.y,
};
en.mouse_move_to(evt.x, evt.y); en.mouse_move_to(evt.x, evt.y);
} }
1 => match buttons { 1 => match buttons {