2023-07-19 01:26:43 +08:00
|
|
|
import 'dart:convert';
|
|
|
|
|
2022-09-27 22:16:27 +08:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/services.dart';
|
2023-07-17 20:07:55 +08:00
|
|
|
import 'package:flutter/gestures.dart';
|
2022-09-27 22:16:27 +08:00
|
|
|
|
2023-07-17 20:07:55 +08:00
|
|
|
import 'package:flutter_hbb/models/platform_model.dart';
|
|
|
|
import 'package:flutter_hbb/common.dart';
|
2023-08-09 23:42:53 +08:00
|
|
|
import 'package:flutter_hbb/consts.dart';
|
2023-07-17 20:07:55 +08:00
|
|
|
import 'package:flutter_hbb/models/model.dart';
|
|
|
|
import 'package:flutter_hbb/models/input_model.dart';
|
|
|
|
|
|
|
|
import './gestures.dart';
|
2022-09-27 22:16:27 +08:00
|
|
|
|
|
|
|
class RawKeyFocusScope extends StatelessWidget {
|
|
|
|
final FocusNode? focusNode;
|
|
|
|
final ValueChanged<bool>? onFocusChange;
|
|
|
|
final InputModel inputModel;
|
|
|
|
final Widget child;
|
|
|
|
|
2022-12-21 22:39:30 +08:00
|
|
|
RawKeyFocusScope({
|
|
|
|
this.focusNode,
|
|
|
|
this.onFocusChange,
|
|
|
|
required this.inputModel,
|
|
|
|
required this.child,
|
|
|
|
});
|
2022-09-27 22:16:27 +08:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return FocusScope(
|
|
|
|
autofocus: true,
|
|
|
|
child: Focus(
|
|
|
|
autofocus: true,
|
|
|
|
canRequestFocus: true,
|
|
|
|
focusNode: focusNode,
|
|
|
|
onFocusChange: onFocusChange,
|
2023-11-29 20:57:44 +08:00
|
|
|
onKey: (FocusNode data, RawKeyEvent e) =>
|
|
|
|
inputModel.handleRawKeyEvent(e),
|
2022-09-27 22:16:27 +08:00
|
|
|
child: child));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-17 20:07:55 +08:00
|
|
|
class RawTouchGestureDetectorRegion extends StatefulWidget {
|
|
|
|
final Widget child;
|
|
|
|
final FFI ffi;
|
|
|
|
|
|
|
|
late final InputModel inputModel = ffi.inputModel;
|
|
|
|
late final FfiModel ffiModel = ffi.ffiModel;
|
|
|
|
|
|
|
|
RawTouchGestureDetectorRegion({
|
|
|
|
required this.child,
|
|
|
|
required this.ffi,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<RawTouchGestureDetectorRegion> createState() =>
|
|
|
|
_RawTouchGestureDetectorRegionState();
|
|
|
|
}
|
|
|
|
|
2023-07-17 20:48:58 +08:00
|
|
|
/// touchMode only:
|
|
|
|
/// LongPress -> right click
|
|
|
|
/// OneFingerPan -> start/end -> left down start/end
|
|
|
|
/// onDoubleTapDown -> move to
|
|
|
|
/// onLongPressDown => move to
|
|
|
|
///
|
|
|
|
/// mouseMode only:
|
|
|
|
/// DoubleFiner -> right click
|
|
|
|
/// HoldDrag -> left drag
|
2023-07-17 20:07:55 +08:00
|
|
|
class _RawTouchGestureDetectorRegionState
|
|
|
|
extends State<RawTouchGestureDetectorRegion> {
|
|
|
|
Offset _cacheLongPressPosition = Offset(0, 0);
|
|
|
|
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
|
|
|
double _scale = 1;
|
|
|
|
|
|
|
|
PointerDeviceKind? lastDeviceKind;
|
|
|
|
|
|
|
|
FFI get ffi => widget.ffi;
|
|
|
|
FfiModel get ffiModel => widget.ffiModel;
|
|
|
|
InputModel get inputModel => widget.inputModel;
|
2024-03-28 11:38:11 +08:00
|
|
|
bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode;
|
2023-07-17 20:07:55 +08:00
|
|
|
SessionID get sessionId => ffi.sessionId;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return RawGestureDetector(
|
|
|
|
child: widget.child,
|
|
|
|
gestures: makeGestures(context),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
onTapDown(TapDownDetails d) {
|
|
|
|
lastDeviceKind = d.kind;
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (handleTouch) {
|
2023-10-02 12:17:11 +08:00
|
|
|
// Desktop or mobile "Touch mode"
|
2023-07-17 20:07:55 +08:00
|
|
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
2023-07-25 15:32:42 +08:00
|
|
|
inputModel.tapDown(MouseButtons.left);
|
2023-07-17 20:07:55 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onTapUp(TapUpDetails d) {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (handleTouch) {
|
|
|
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
2023-07-25 22:09:42 +08:00
|
|
|
inputModel.tapUp(MouseButtons.left);
|
2023-07-17 20:07:55 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-25 15:32:42 +08:00
|
|
|
onTap() {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
2023-10-02 10:15:20 +08:00
|
|
|
if (!handleTouch) {
|
2023-10-02 12:17:11 +08:00
|
|
|
// Mobile, "Mouse mode"
|
2023-10-02 10:15:20 +08:00
|
|
|
inputModel.tap(MouseButtons.left);
|
|
|
|
}
|
2023-07-25 15:32:42 +08:00
|
|
|
}
|
|
|
|
|
2023-07-17 20:07:55 +08:00
|
|
|
onDoubleTapDown(TapDownDetails d) {
|
|
|
|
lastDeviceKind = d.kind;
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (handleTouch) {
|
|
|
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onDoubleTap() {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
inputModel.tap(MouseButtons.left);
|
|
|
|
inputModel.tap(MouseButtons.left);
|
|
|
|
}
|
|
|
|
|
|
|
|
onLongPressDown(LongPressDownDetails d) {
|
|
|
|
lastDeviceKind = d.kind;
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (handleTouch) {
|
|
|
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
|
|
|
_cacheLongPressPosition = d.localPosition;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-25 22:09:42 +08:00
|
|
|
onLongPressUp() {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (handleTouch) {
|
|
|
|
inputModel.tapUp(MouseButtons.left);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-17 20:07:55 +08:00
|
|
|
// for mobiles
|
|
|
|
onLongPress() {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (handleTouch) {
|
|
|
|
ffi.cursorModel
|
|
|
|
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
|
|
|
}
|
|
|
|
inputModel.tap(MouseButtons.right);
|
|
|
|
}
|
|
|
|
|
2023-07-25 22:09:42 +08:00
|
|
|
onDoubleFinerTapDown(TapDownDetails d) {
|
|
|
|
lastDeviceKind = d.kind;
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// ignore for desktop and mobile
|
|
|
|
}
|
|
|
|
|
2023-07-17 20:07:55 +08:00
|
|
|
onDoubleFinerTap(TapDownDetails d) {
|
|
|
|
lastDeviceKind = d.kind;
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
2024-03-28 11:38:11 +08:00
|
|
|
if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) {
|
2023-07-17 20:07:55 +08:00
|
|
|
inputModel.tap(MouseButtons.right);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onHoldDragStart(DragStartDetails d) {
|
|
|
|
lastDeviceKind = d.kind;
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!handleTouch) {
|
|
|
|
inputModel.sendMouse('down', MouseButtons.left);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onHoldDragUpdate(DragUpdateDetails d) {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!handleTouch) {
|
2024-04-27 13:45:44 +08:00
|
|
|
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
2023-07-17 20:07:55 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onHoldDragEnd(DragEndDetails d) {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!handleTouch) {
|
|
|
|
inputModel.sendMouse('up', MouseButtons.left);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onOneFingerPanStart(BuildContext context, DragStartDetails d) {
|
|
|
|
lastDeviceKind = d.kind ?? lastDeviceKind;
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (handleTouch) {
|
2024-04-27 13:45:44 +08:00
|
|
|
if (isDesktop) {
|
|
|
|
ffi.cursorModel.trySetRemoteWindowCoords();
|
|
|
|
}
|
2023-07-17 20:07:55 +08:00
|
|
|
inputModel.sendMouse('down', MouseButtons.left);
|
2023-07-17 20:48:58 +08:00
|
|
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
2023-07-17 20:07:55 +08:00
|
|
|
} else {
|
|
|
|
final offset = ffi.cursorModel.offset;
|
|
|
|
final cursorX = offset.dx;
|
|
|
|
final cursorY = offset.dy;
|
|
|
|
final visible =
|
|
|
|
ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
|
|
|
|
final size = MediaQueryData.fromView(View.of(context)).size;
|
|
|
|
if (!visible.contains(Offset(cursorX, cursorY))) {
|
|
|
|
ffi.cursorModel.move(size.width / 2, size.height / 2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onOneFingerPanUpdate(DragUpdateDetails d) {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
2024-04-27 13:45:44 +08:00
|
|
|
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
2023-07-17 20:07:55 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
onOneFingerPanEnd(DragEndDetails d) {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
2024-04-27 13:45:44 +08:00
|
|
|
if (isDesktop) {
|
|
|
|
ffi.cursorModel.clearRemoteWindowCoords();
|
|
|
|
}
|
2023-07-25 22:09:42 +08:00
|
|
|
inputModel.sendMouse('up', MouseButtons.left);
|
2023-07-17 20:07:55 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// scale + pan event
|
2023-07-25 22:09:42 +08:00
|
|
|
onTwoFingerScaleStart(ScaleStartDetails d) {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-17 20:07:55 +08:00
|
|
|
onTwoFingerScaleUpdate(ScaleUpdateDetails d) {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
2024-03-28 11:38:11 +08:00
|
|
|
if ((isDesktop || isWebDesktop)) {
|
2023-07-19 01:26:43 +08:00
|
|
|
final scale = ((d.scale - _scale) * 1000).toInt();
|
|
|
|
_scale = d.scale;
|
|
|
|
|
|
|
|
if (scale != 0) {
|
|
|
|
bind.sessionSendPointer(
|
|
|
|
sessionId: sessionId,
|
2023-08-09 23:42:53 +08:00
|
|
|
msg: json.encode(
|
|
|
|
PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
|
|
|
|
.toJson()));
|
2023-07-19 01:26:43 +08:00
|
|
|
}
|
2023-07-17 20:07:55 +08:00
|
|
|
} else {
|
|
|
|
// mobile
|
|
|
|
ffi.canvasModel.updateScale(d.scale / _scale);
|
|
|
|
_scale = d.scale;
|
|
|
|
ffi.canvasModel.panX(d.focalPointDelta.dx);
|
|
|
|
ffi.canvasModel.panY(d.focalPointDelta.dy);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onTwoFingerScaleEnd(ScaleEndDetails d) {
|
|
|
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
|
|
|
return;
|
|
|
|
}
|
2024-03-28 11:38:11 +08:00
|
|
|
if ((isDesktop || isWebDesktop)) {
|
2023-07-19 01:26:43 +08:00
|
|
|
bind.sessionSendPointer(
|
|
|
|
sessionId: sessionId,
|
2023-08-09 23:42:53 +08:00
|
|
|
msg: json.encode(
|
|
|
|
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
|
2023-07-17 20:07:55 +08:00
|
|
|
} else {
|
|
|
|
// mobile
|
|
|
|
_scale = 1;
|
|
|
|
bind.sessionSetViewStyle(sessionId: sessionId, value: "");
|
|
|
|
}
|
2023-07-19 01:26:43 +08:00
|
|
|
inputModel.sendMouse('up', MouseButtons.left);
|
2023-07-17 20:07:55 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
get onHoldDragCancel => null;
|
|
|
|
get onThreeFingerVerticalDragUpdate => ffi.ffiModel.isPeerAndroid
|
|
|
|
? null
|
|
|
|
: (d) {
|
|
|
|
_mouseScrollIntegral += d.delta.dy / 4;
|
|
|
|
if (_mouseScrollIntegral > 1) {
|
|
|
|
inputModel.scroll(1);
|
|
|
|
_mouseScrollIntegral = 0;
|
|
|
|
} else if (_mouseScrollIntegral < -1) {
|
|
|
|
inputModel.scroll(-1);
|
|
|
|
_mouseScrollIntegral = 0;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
makeGestures(BuildContext context) {
|
|
|
|
return <Type, GestureRecognizerFactory>{
|
|
|
|
// Official
|
|
|
|
TapGestureRecognizer:
|
|
|
|
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
|
|
|
() => TapGestureRecognizer(), (instance) {
|
|
|
|
instance
|
|
|
|
..onTapDown = onTapDown
|
2023-07-25 15:32:42 +08:00
|
|
|
..onTapUp = onTapUp
|
|
|
|
..onTap = onTap;
|
2023-07-17 20:07:55 +08:00
|
|
|
}),
|
|
|
|
DoubleTapGestureRecognizer:
|
|
|
|
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
|
|
|
() => DoubleTapGestureRecognizer(), (instance) {
|
|
|
|
instance
|
|
|
|
..onDoubleTapDown = onDoubleTapDown
|
|
|
|
..onDoubleTap = onDoubleTap;
|
|
|
|
}),
|
|
|
|
LongPressGestureRecognizer:
|
|
|
|
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
|
|
|
() => LongPressGestureRecognizer(), (instance) {
|
|
|
|
instance
|
|
|
|
..onLongPressDown = onLongPressDown
|
2023-07-25 22:09:42 +08:00
|
|
|
..onLongPressUp = onLongPressUp
|
2023-07-17 20:07:55 +08:00
|
|
|
..onLongPress = onLongPress;
|
|
|
|
}),
|
|
|
|
// Customized
|
|
|
|
HoldTapMoveGestureRecognizer:
|
|
|
|
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
|
|
|
() => HoldTapMoveGestureRecognizer(),
|
|
|
|
(instance) => instance
|
|
|
|
..onHoldDragStart = onHoldDragStart
|
|
|
|
..onHoldDragUpdate = onHoldDragUpdate
|
|
|
|
..onHoldDragCancel = onHoldDragCancel
|
|
|
|
..onHoldDragEnd = onHoldDragEnd),
|
|
|
|
DoubleFinerTapGestureRecognizer:
|
|
|
|
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
|
|
|
|
() => DoubleFinerTapGestureRecognizer(), (instance) {
|
2023-07-25 22:09:42 +08:00
|
|
|
instance
|
|
|
|
..onDoubleFinerTap = onDoubleFinerTap
|
|
|
|
..onDoubleFinerTapDown = onDoubleFinerTapDown;
|
2023-07-17 20:07:55 +08:00
|
|
|
}),
|
|
|
|
CustomTouchGestureRecognizer:
|
|
|
|
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
|
|
|
() => CustomTouchGestureRecognizer(), (instance) {
|
|
|
|
instance.onOneFingerPanStart =
|
|
|
|
(DragStartDetails d) => onOneFingerPanStart(context, d);
|
|
|
|
instance
|
|
|
|
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
|
|
|
..onOneFingerPanEnd = onOneFingerPanEnd
|
2023-07-25 22:09:42 +08:00
|
|
|
..onTwoFingerScaleStart = onTwoFingerScaleStart
|
2023-07-17 20:07:55 +08:00
|
|
|
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
|
|
|
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
|
|
|
..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-27 22:16:27 +08:00
|
|
|
class RawPointerMouseRegion extends StatelessWidget {
|
|
|
|
final InputModel inputModel;
|
|
|
|
final Widget child;
|
|
|
|
final MouseCursor? cursor;
|
|
|
|
final PointerEnterEventListener? onEnter;
|
|
|
|
final PointerExitEventListener? onExit;
|
2022-12-21 22:39:30 +08:00
|
|
|
final PointerDownEventListener? onPointerDown;
|
|
|
|
final PointerUpEventListener? onPointerUp;
|
2022-09-27 22:16:27 +08:00
|
|
|
|
2023-07-17 20:07:55 +08:00
|
|
|
RawPointerMouseRegion({
|
|
|
|
this.onEnter,
|
|
|
|
this.onExit,
|
|
|
|
this.cursor,
|
|
|
|
this.onPointerDown,
|
|
|
|
this.onPointerUp,
|
|
|
|
required this.inputModel,
|
|
|
|
required this.child,
|
|
|
|
});
|
2022-09-27 22:16:27 +08:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Listener(
|
2023-07-17 20:07:55 +08:00
|
|
|
onPointerHover: inputModel.onPointHoverImage,
|
|
|
|
onPointerDown: (evt) {
|
|
|
|
onPointerDown?.call(evt);
|
|
|
|
inputModel.onPointDownImage(evt);
|
|
|
|
},
|
|
|
|
onPointerUp: (evt) {
|
|
|
|
onPointerUp?.call(evt);
|
|
|
|
inputModel.onPointUpImage(evt);
|
|
|
|
},
|
|
|
|
onPointerMove: inputModel.onPointMoveImage,
|
|
|
|
onPointerSignal: inputModel.onPointerSignalImage,
|
|
|
|
onPointerPanZoomStart: inputModel.onPointerPanZoomStart,
|
|
|
|
onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
|
|
|
|
onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
|
|
|
|
child: MouseRegion(
|
2024-03-28 11:38:11 +08:00
|
|
|
cursor: inputModel.isViewOnly
|
|
|
|
? MouseCursor.defer
|
|
|
|
: (cursor ?? MouseCursor.defer),
|
2023-07-17 20:07:55 +08:00
|
|
|
onEnter: onEnter,
|
|
|
|
onExit: onExit,
|
|
|
|
child: child,
|
|
|
|
),
|
|
|
|
);
|
2022-09-27 22:16:27 +08:00
|
|
|
}
|
|
|
|
}
|