# Conflicts: # .github/workflows/ci.yml # Cargo.lock # Cargo.toml # flutter/lib/common.dart # flutter/lib/mobile/pages/remote_page.dart # flutter/lib/mobile/pages/server_page.dart # flutter/lib/mobile/pages/settings_page.dart # flutter/lib/mobile/widgets/dialog.dart # flutter/lib/models/model.dart # flutter/lib/models/server_model.dart # src/client.rs # src/common.rs # src/ipc.rs # src/mobile_ffi.rs # src/rendezvous_mediator.rs # src/ui.rs
		
			
				
	
	
		
			745 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			745 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:async';
 | |
| import 'package:flutter/gestures.dart';
 | |
| import 'package:flutter/widgets.dart';
 | |
| 
 | |
| enum GestureState {
 | |
|   none,
 | |
|   oneFingerPan,
 | |
|   twoFingerScale,
 | |
|   threeFingerVerticalDrag
 | |
| }
 | |
| 
 | |
| class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
 | |
|   CustomTouchGestureRecognizer({
 | |
|     Object? debugOwner,
 | |
|     Set<PointerDeviceKind>? supportedDevices,
 | |
|   }) : super(
 | |
|           debugOwner: debugOwner,
 | |
|           supportedDevices: supportedDevices,
 | |
|         ) {
 | |
|     _init();
 | |
|   }
 | |
| 
 | |
|   // oneFingerPan
 | |
|   GestureDragStartCallback? onOneFingerPanStart;
 | |
|   GestureDragUpdateCallback? onOneFingerPanUpdate;
 | |
|   GestureDragEndCallback? onOneFingerPanEnd;
 | |
| 
 | |
|   // twoFingerScale : scale + pan event
 | |
|   GestureScaleStartCallback? onTwoFingerScaleStart;
 | |
|   GestureScaleUpdateCallback? onTwoFingerScaleUpdate;
 | |
|   GestureScaleEndCallback? onTwoFingerScaleEnd;
 | |
| 
 | |
|   // threeFingerVerticalDrag
 | |
|   GestureDragStartCallback? onThreeFingerVerticalDragStart;
 | |
|   GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate;
 | |
|   GestureDragEndCallback? onThreeFingerVerticalDragEnd;
 | |
| 
 | |
|   var _currentState = GestureState.none;
 | |
|   Timer? _debounceTimer;
 | |
| 
 | |
|   void _init() {
 | |
|     debugPrint("CustomTouchGestureRecognizer init");
 | |
|     // onStart = (d) {};
 | |
|     onUpdate = (d) {
 | |
|       _debounceTimer?.cancel();
 | |
|       if (d.pointerCount == 1 && _currentState != GestureState.oneFingerPan) {
 | |
|         onOneFingerStartDebounce(d);
 | |
|       } else if (d.pointerCount == 2 &&
 | |
|           _currentState != GestureState.twoFingerScale) {
 | |
|         onTwoFingerStartDebounce(d);
 | |
|       } else if (d.pointerCount == 3 &&
 | |
|           _currentState != GestureState.threeFingerVerticalDrag) {
 | |
|         _currentState = GestureState.threeFingerVerticalDrag;
 | |
|         if (onThreeFingerVerticalDragStart != null) {
 | |
|           onThreeFingerVerticalDragStart!(
 | |
|               DragStartDetails(globalPosition: d.localFocalPoint));
 | |
|         }
 | |
|         debugPrint("start threeFingerScale");
 | |
|       }
 | |
|       if (_currentState != GestureState.none) {
 | |
|         switch (_currentState) {
 | |
|           case GestureState.oneFingerPan:
 | |
|             if (onOneFingerPanUpdate != null) {
 | |
|               onOneFingerPanUpdate!(_getDragUpdateDetails(d));
 | |
|             }
 | |
|             break;
 | |
|           case GestureState.twoFingerScale:
 | |
|             if (onTwoFingerScaleUpdate != null) {
 | |
|               onTwoFingerScaleUpdate!(d);
 | |
|             }
 | |
|             break;
 | |
|           case GestureState.threeFingerVerticalDrag:
 | |
|             if (onThreeFingerVerticalDragUpdate != null) {
 | |
|               onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d));
 | |
|             }
 | |
|             break;
 | |
|           default:
 | |
|             break;
 | |
|         }
 | |
|         return;
 | |
|       }
 | |
|     };
 | |
|     onEnd = (d) {
 | |
|       debugPrint("ScaleGestureRecognizer onEnd");
 | |
|       _debounceTimer?.cancel();
 | |
|       // end
 | |
|       switch (_currentState) {
 | |
|         case GestureState.oneFingerPan:
 | |
|           debugPrint("TwoFingerState.pan onEnd");
 | |
|           if (onOneFingerPanEnd != null) {
 | |
|             onOneFingerPanEnd!(_getDragEndDetails(d));
 | |
|           }
 | |
|           break;
 | |
|         case GestureState.twoFingerScale:
 | |
|           debugPrint("TwoFingerState.scale onEnd");
 | |
|           if (onTwoFingerScaleEnd != null) {
 | |
|             onTwoFingerScaleEnd!(d);
 | |
|           }
 | |
|           break;
 | |
|         case GestureState.threeFingerVerticalDrag:
 | |
|           debugPrint("ThreeFingerState.vertical onEnd");
 | |
|           if (onThreeFingerVerticalDragEnd != null) {
 | |
|             onThreeFingerVerticalDragEnd!(_getDragEndDetails(d));
 | |
|           }
 | |
|           break;
 | |
|         default:
 | |
|           break;
 | |
|       }
 | |
|       _debounceTimer = Timer(Duration(milliseconds: 200), () {
 | |
|         _currentState = GestureState.none;
 | |
|       });
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   void onOneFingerStartDebounce(ScaleUpdateDetails d) {
 | |
|     final start = (ScaleUpdateDetails d) {
 | |
|       _currentState = GestureState.oneFingerPan;
 | |
|       if (onOneFingerPanStart != null) {
 | |
|         onOneFingerPanStart!(DragStartDetails(
 | |
|             localPosition: d.localFocalPoint, globalPosition: d.focalPoint));
 | |
|       }
 | |
|     };
 | |
|     if (_currentState != GestureState.none) {
 | |
|       _debounceTimer = Timer(Duration(milliseconds: 200), () {
 | |
|         start(d);
 | |
|         debugPrint("debounce start oneFingerPan");
 | |
|       });
 | |
|     } else {
 | |
|       start(d);
 | |
|       debugPrint("start oneFingerPan");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void onTwoFingerStartDebounce(ScaleUpdateDetails d) {
 | |
|     final start = (ScaleUpdateDetails d) {
 | |
|       _currentState = GestureState.twoFingerScale;
 | |
|       if (onTwoFingerScaleStart != null) {
 | |
|         onTwoFingerScaleStart!(ScaleStartDetails(
 | |
|             localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
 | |
|       }
 | |
|     };
 | |
|     if (_currentState == GestureState.threeFingerVerticalDrag) {
 | |
|       _debounceTimer = Timer(Duration(milliseconds: 200), () {
 | |
|         start(d);
 | |
|         debugPrint("debounce start twoFingerScale");
 | |
|       });
 | |
|     } else {
 | |
|       start(d);
 | |
|       debugPrint("start twoFingerScale");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   DragUpdateDetails _getDragUpdateDetails(ScaleUpdateDetails d) =>
 | |
|       DragUpdateDetails(
 | |
|           globalPosition: d.focalPoint,
 | |
|           localPosition: d.localFocalPoint,
 | |
|           delta: d.focalPointDelta);
 | |
| 
 | |
|   DragEndDetails _getDragEndDetails(ScaleEndDetails d) =>
 | |
|       DragEndDetails(velocity: d.velocity);
 | |
| }
 | |
| 
 | |
| class HoldTapMoveGestureRecognizer extends GestureRecognizer {
 | |
|   HoldTapMoveGestureRecognizer({
 | |
|     Object? debugOwner,
 | |
|     Set<PointerDeviceKind>? supportedDevices,
 | |
|   }) : super(
 | |
|           debugOwner: debugOwner,
 | |
|           supportedDevices: supportedDevices,
 | |
|         );
 | |
| 
 | |
|   GestureDragStartCallback? onHoldDragStart;
 | |
|   GestureDragUpdateCallback? onHoldDragUpdate;
 | |
|   GestureDragDownCallback? onHoldDragDown;
 | |
|   GestureDragCancelCallback? onHoldDragCancel;
 | |
|   GestureDragEndCallback? onHoldDragEnd;
 | |
| 
 | |
|   bool _isStart = false;
 | |
| 
 | |
|   Timer? _firstTapUpTimer;
 | |
|   Timer? _secondTapDownTimer;
 | |
|   _TapTracker? _firstTap;
 | |
|   _TapTracker? _secondTap;
 | |
| 
 | |
|   final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
 | |
| 
 | |
|   @override
 | |
|   bool isPointerAllowed(PointerDownEvent event) {
 | |
|     if (_firstTap == null) {
 | |
|       switch (event.buttons) {
 | |
|         case kPrimaryButton:
 | |
|           if (onHoldDragStart == null &&
 | |
|               onHoldDragUpdate == null &&
 | |
|               onHoldDragCancel == null &&
 | |
|               onHoldDragEnd == null) {
 | |
|             return false;
 | |
|           }
 | |
|           break;
 | |
|         default:
 | |
|           return false;
 | |
|       }
 | |
|     }
 | |
|     return super.isPointerAllowed(event);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void addAllowedPointer(PointerDownEvent event) {
 | |
|     if (_firstTap != null) {
 | |
|       if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
 | |
|         // Ignore out-of-bounds second taps.
 | |
|         return;
 | |
|       } else if (!_firstTap!.hasElapsedMinTime() ||
 | |
|           !_firstTap!.hasSameButton(event)) {
 | |
|         // Restart when the second tap is too close to the first (touch screens
 | |
|         // often detect touches intermittently), or when buttons mismatch.
 | |
|         _reset();
 | |
|         return _trackTap(event);
 | |
|       } else if (onHoldDragDown != null) {
 | |
|         invokeCallback<void>(
 | |
|             'onHoldDragDown',
 | |
|             () => onHoldDragDown!(DragDownDetails(
 | |
|                 globalPosition: event.position,
 | |
|                 localPosition: event.localPosition)));
 | |
|       }
 | |
|     }
 | |
|     _trackTap(event);
 | |
|   }
 | |
| 
 | |
|   void _trackTap(PointerDownEvent event) {
 | |
|     _stopFirstTapUpTimer();
 | |
|     _stopSecondTapDownTimer();
 | |
|     final _TapTracker tracker = _TapTracker(
 | |
|       event: event,
 | |
|       entry: GestureBinding.instance.gestureArena.add(event.pointer, this),
 | |
|       doubleTapMinTime: kDoubleTapMinTime,
 | |
|       gestureSettings: gestureSettings,
 | |
|     );
 | |
|     _trackers[event.pointer] = tracker;
 | |
|     tracker.startTrackingPointer(_handleEvent, event.transform);
 | |
|   }
 | |
| 
 | |
|   void _handleEvent(PointerEvent event) {
 | |
|     final _TapTracker tracker = _trackers[event.pointer]!;
 | |
|     if (event is PointerUpEvent) {
 | |
|       if (_firstTap == null && _secondTap == null) {
 | |
|         _registerFirstTap(tracker);
 | |
|       } else if (_secondTap != null) {
 | |
|         if (event.pointer == _secondTap!.pointer) {
 | |
|           if (onHoldDragEnd != null) onHoldDragEnd!(DragEndDetails());
 | |
|         }
 | |
|       } else {
 | |
|         _reject(tracker);
 | |
|       }
 | |
|     } else if (event is PointerDownEvent) {
 | |
|       if (_firstTap != null && _secondTap == null) {
 | |
|         _registerSecondTap(tracker);
 | |
|       }
 | |
|     } else if (event is PointerMoveEvent) {
 | |
|       if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) {
 | |
|         if (_firstTap != null && _firstTap!.pointer == event.pointer) {
 | |
|           // first tap move
 | |
|           _reject(tracker);
 | |
|         } else if (_secondTap != null && _secondTap!.pointer == event.pointer) {
 | |
|           // debugPrint("_secondTap move");
 | |
|           // second tap move
 | |
|           if (!_isStart) {
 | |
|             _resolve();
 | |
|           }
 | |
|           if (onHoldDragUpdate != null)
 | |
|             onHoldDragUpdate!(DragUpdateDetails(
 | |
|                 globalPosition: event.position,
 | |
|                 localPosition: event.localPosition,
 | |
|                 delta: event.delta));
 | |
|         }
 | |
|       }
 | |
|     } else if (event is PointerCancelEvent) {
 | |
|       _reject(tracker);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void acceptGesture(int pointer) {}
 | |
| 
 | |
|   @override
 | |
|   void rejectGesture(int pointer) {
 | |
|     _TapTracker? tracker = _trackers[pointer];
 | |
|     // If tracker isn't in the list, check if this is the first tap tracker
 | |
|     if (tracker == null && _firstTap != null && _firstTap!.pointer == pointer) {
 | |
|       tracker = _firstTap;
 | |
|     }
 | |
|     // If tracker is still null, we rejected ourselves already
 | |
|     if (tracker != null) {
 | |
|       _reject(tracker);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _resolve() {
 | |
|     _stopSecondTapDownTimer();
 | |
|     _firstTap?.entry.resolve(GestureDisposition.accepted);
 | |
|     _secondTap?.entry.resolve(GestureDisposition.accepted);
 | |
|     _isStart = true;
 | |
|     // TODO start details
 | |
|     if (onHoldDragStart != null) onHoldDragStart!(DragStartDetails());
 | |
|   }
 | |
| 
 | |
|   void _reject(_TapTracker tracker) {
 | |
|     try {
 | |
|       _checkCancel();
 | |
|       _isStart = false;
 | |
|       _trackers.remove(tracker.pointer);
 | |
|       tracker.entry.resolve(GestureDisposition.rejected);
 | |
|       _freezeTracker(tracker);
 | |
|       _reset();
 | |
|     } catch (e) {
 | |
|       debugPrint("Failed to _reject:$e");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     _reset();
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   void _reset() {
 | |
|     _isStart = false;
 | |
|     // debugPrint("reset");
 | |
|     _stopFirstTapUpTimer();
 | |
|     _stopSecondTapDownTimer();
 | |
|     if (_firstTap != null) {
 | |
|       if (_trackers.isNotEmpty) {
 | |
|         _checkCancel();
 | |
|       }
 | |
|       // Note, order is important below in order for the resolve -> reject logic
 | |
|       // to work properly.
 | |
|       final _TapTracker tracker = _firstTap!;
 | |
|       _firstTap = null;
 | |
|       _reject(tracker);
 | |
|       GestureBinding.instance.gestureArena.release(tracker.pointer);
 | |
| 
 | |
|       if (_secondTap != null) {
 | |
|         final _TapTracker tracker = _secondTap!;
 | |
|         _secondTap = null;
 | |
|         _reject(tracker);
 | |
|         GestureBinding.instance.gestureArena.release(tracker.pointer);
 | |
|       }
 | |
|     }
 | |
|     _firstTap = null;
 | |
|     _secondTap = null;
 | |
|     _clearTrackers();
 | |
|   }
 | |
| 
 | |
|   void _registerFirstTap(_TapTracker tracker) {
 | |
|     _startFirstTapUpTimer();
 | |
|     GestureBinding.instance.gestureArena.hold(tracker.pointer);
 | |
|     // Note, order is important below in order for the clear -> reject logic to
 | |
|     // work properly.
 | |
|     _freezeTracker(tracker);
 | |
|     _trackers.remove(tracker.pointer);
 | |
|     _firstTap = tracker;
 | |
|   }
 | |
| 
 | |
|   void _registerSecondTap(_TapTracker tracker) {
 | |
|     if (_firstTap != null) {
 | |
|       _stopFirstTapUpTimer();
 | |
|       _freezeTracker(_firstTap!);
 | |
|       _firstTap = null;
 | |
|     }
 | |
| 
 | |
|     _startSecondTapDownTimer();
 | |
|     GestureBinding.instance.gestureArena.hold(tracker.pointer);
 | |
| 
 | |
|     _secondTap = tracker;
 | |
| 
 | |
|     // TODO
 | |
|   }
 | |
| 
 | |
|   void _clearTrackers() {
 | |
|     _trackers.values.toList().forEach(_reject);
 | |
|     assert(_trackers.isEmpty);
 | |
|   }
 | |
| 
 | |
|   void _freezeTracker(_TapTracker tracker) {
 | |
|     tracker.stopTrackingPointer(_handleEvent);
 | |
|   }
 | |
| 
 | |
|   void _startFirstTapUpTimer() {
 | |
|     _firstTapUpTimer ??= Timer(kDoubleTapTimeout, _reset);
 | |
|   }
 | |
| 
 | |
|   void _startSecondTapDownTimer() {
 | |
|     _secondTapDownTimer ??= Timer(kDoubleTapTimeout, _resolve);
 | |
|   }
 | |
| 
 | |
|   void _stopFirstTapUpTimer() {
 | |
|     if (_firstTapUpTimer != null) {
 | |
|       _firstTapUpTimer!.cancel();
 | |
|       _firstTapUpTimer = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _stopSecondTapDownTimer() {
 | |
|     if (_secondTapDownTimer != null) {
 | |
|       _secondTapDownTimer!.cancel();
 | |
|       _secondTapDownTimer = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _checkCancel() {
 | |
|     if (onHoldDragCancel != null) {
 | |
|       invokeCallback<void>('onHoldDragCancel', onHoldDragCancel!);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   String get debugDescription => 'double tap';
 | |
| }
 | |
| 
 | |
| class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
 | |
|   DoubleFinerTapGestureRecognizer({
 | |
|     Object? debugOwner,
 | |
|     Set<PointerDeviceKind>? supportedDevices,
 | |
|   }) : super(
 | |
|           debugOwner: debugOwner,
 | |
|           supportedDevices: supportedDevices,
 | |
|         );
 | |
| 
 | |
|   GestureTapDownCallback? onDoubleFinerTapDown;
 | |
|   GestureTapDownCallback? onDoubleFinerTap;
 | |
|   GestureTapCancelCallback? onDoubleFinerTapCancel;
 | |
| 
 | |
|   Timer? _firstTapTimer;
 | |
|   _TapTracker? _firstTap;
 | |
| 
 | |
|   var _isStart = false;
 | |
| 
 | |
|   final Set<int> _upTap = {};
 | |
| 
 | |
|   final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
 | |
| 
 | |
|   @override
 | |
|   bool isPointerAllowed(PointerDownEvent event) {
 | |
|     if (_firstTap == null) {
 | |
|       switch (event.buttons) {
 | |
|         case kPrimaryButton:
 | |
|           if (onDoubleFinerTapDown == null &&
 | |
|               onDoubleFinerTap == null &&
 | |
|               onDoubleFinerTapCancel == null) {
 | |
|             return false;
 | |
|           }
 | |
|           break;
 | |
|         default:
 | |
|           return false;
 | |
|       }
 | |
|     }
 | |
|     return super.isPointerAllowed(event);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void addAllowedPointer(PointerDownEvent event) {
 | |
|     debugPrint("addAllowedPointer");
 | |
|     if (_isStart) {
 | |
|       // second
 | |
|       if (onDoubleFinerTapDown != null) {
 | |
|         final TapDownDetails details = TapDownDetails(
 | |
|           globalPosition: event.position,
 | |
|           localPosition: event.localPosition,
 | |
|           kind: getKindForPointer(event.pointer),
 | |
|         );
 | |
|         invokeCallback<void>(
 | |
|             'onDoubleFinerTapDown', () => onDoubleFinerTapDown!(details));
 | |
|       }
 | |
|     } else {
 | |
|       // first tap
 | |
|       _isStart = true;
 | |
|       _startFirstTapDownTimer();
 | |
|     }
 | |
|     _trackTap(event);
 | |
|   }
 | |
| 
 | |
|   void _trackTap(PointerDownEvent event) {
 | |
|     final _TapTracker tracker = _TapTracker(
 | |
|       event: event,
 | |
|       entry: GestureBinding.instance.gestureArena.add(event.pointer, this),
 | |
|       doubleTapMinTime: kDoubleTapMinTime,
 | |
|       gestureSettings: gestureSettings,
 | |
|     );
 | |
|     _trackers[event.pointer] = tracker;
 | |
|     // debugPrint("_trackers:$_trackers");
 | |
|     tracker.startTrackingPointer(_handleEvent, event.transform);
 | |
| 
 | |
|     _registerTap(tracker);
 | |
|   }
 | |
| 
 | |
|   void _handleEvent(PointerEvent event) {
 | |
|     final _TapTracker tracker = _trackers[event.pointer]!;
 | |
|     if (event is PointerUpEvent) {
 | |
|       debugPrint("PointerUpEvent");
 | |
|       _upTap.add(tracker.pointer);
 | |
|     } else if (event is PointerMoveEvent) {
 | |
|       if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop))
 | |
|         _reject(tracker);
 | |
|     } else if (event is PointerCancelEvent) {
 | |
|       _reject(tracker);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void acceptGesture(int pointer) {}
 | |
| 
 | |
|   @override
 | |
|   void rejectGesture(int pointer) {
 | |
|     _TapTracker? tracker = _trackers[pointer];
 | |
|     // If tracker isn't in the list, check if this is the first tap tracker
 | |
|     if (tracker == null && _firstTap != null && _firstTap!.pointer == pointer) {
 | |
|       tracker = _firstTap;
 | |
|     }
 | |
|     // If tracker is still null, we rejected ourselves already
 | |
|     if (tracker != null) {
 | |
|       _reject(tracker);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _reject(_TapTracker tracker) {
 | |
|     _trackers.remove(tracker.pointer);
 | |
|     tracker.entry.resolve(GestureDisposition.rejected);
 | |
|     _freezeTracker(tracker);
 | |
|     if (_firstTap != null) {
 | |
|       if (tracker == _firstTap) {
 | |
|         _reset();
 | |
|       } else {
 | |
|         _checkCancel();
 | |
|         if (_trackers.isEmpty) {
 | |
|           _reset();
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     _reset();
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   void _reset() {
 | |
|     _stopFirstTapUpTimer();
 | |
|     _firstTap = null;
 | |
|     _clearTrackers();
 | |
|   }
 | |
| 
 | |
|   void _registerTap(_TapTracker tracker) {
 | |
|     GestureBinding.instance.gestureArena.hold(tracker.pointer);
 | |
|     // Note, order is important below in order for the clear -> reject logic to
 | |
|     // work properly.
 | |
|   }
 | |
| 
 | |
|   void _clearTrackers() {
 | |
|     _trackers.values.toList().forEach(_reject);
 | |
|     assert(_trackers.isEmpty);
 | |
|   }
 | |
| 
 | |
|   void _freezeTracker(_TapTracker tracker) {
 | |
|     tracker.stopTrackingPointer(_handleEvent);
 | |
|   }
 | |
| 
 | |
|   void _startFirstTapDownTimer() {
 | |
|     _firstTapTimer ??= Timer(kDoubleTapTimeout, _timeoutCheck);
 | |
|   }
 | |
| 
 | |
|   void _stopFirstTapUpTimer() {
 | |
|     if (_firstTapTimer != null) {
 | |
|       _firstTapTimer!.cancel();
 | |
|       _firstTapTimer = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _timeoutCheck() {
 | |
|     _isStart = false;
 | |
|     if (_upTap.length == 2) {
 | |
|       _resolve();
 | |
|     } else {
 | |
|       _reset();
 | |
|     }
 | |
|     _upTap.clear();
 | |
|   }
 | |
| 
 | |
|   void _resolve() {
 | |
|     // TODO tap down details
 | |
|     if (onDoubleFinerTap != null) onDoubleFinerTap!(TapDownDetails());
 | |
|     _trackers.forEach((key, value) {
 | |
|       value.entry.resolve(GestureDisposition.accepted);
 | |
|     });
 | |
|     _reset();
 | |
|   }
 | |
| 
 | |
|   void _checkCancel() {
 | |
|     if (onDoubleFinerTapCancel != null) {
 | |
|       invokeCallback<void>('onHoldDragCancel', onDoubleFinerTapCancel!);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   String get debugDescription => 'double tap';
 | |
| }
 | |
| 
 | |
| /// TapTracker helps track individual tap sequences as part of a
 | |
| /// larger gesture.
 | |
| class _TapTracker {
 | |
|   _TapTracker({
 | |
|     required PointerDownEvent event,
 | |
|     required this.entry,
 | |
|     required Duration doubleTapMinTime,
 | |
|     required this.gestureSettings,
 | |
|   })  : pointer = event.pointer,
 | |
|         _initialGlobalPosition = event.position,
 | |
|         initialButtons = event.buttons,
 | |
|         _doubleTapMinTimeCountdown =
 | |
|             _CountdownZoned(duration: doubleTapMinTime);
 | |
| 
 | |
|   final DeviceGestureSettings? gestureSettings;
 | |
|   final int pointer;
 | |
|   final GestureArenaEntry entry;
 | |
|   final Offset _initialGlobalPosition;
 | |
|   final int initialButtons;
 | |
|   final _CountdownZoned _doubleTapMinTimeCountdown;
 | |
| 
 | |
|   bool _isTrackingPointer = false;
 | |
| 
 | |
|   void startTrackingPointer(PointerRoute route, Matrix4? transform) {
 | |
|     if (!_isTrackingPointer) {
 | |
|       _isTrackingPointer = true;
 | |
|       GestureBinding.instance.pointerRouter.addRoute(pointer, route, transform);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void stopTrackingPointer(PointerRoute route) {
 | |
|     if (_isTrackingPointer) {
 | |
|       _isTrackingPointer = false;
 | |
|       GestureBinding.instance.pointerRouter.removeRoute(pointer, route);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   bool isWithinGlobalTolerance(PointerEvent event, double tolerance) {
 | |
|     final Offset offset = event.position - _initialGlobalPosition;
 | |
|     return offset.distance <= tolerance;
 | |
|   }
 | |
| 
 | |
|   bool hasElapsedMinTime() {
 | |
|     return _doubleTapMinTimeCountdown.timeout;
 | |
|   }
 | |
| 
 | |
|   bool hasSameButton(PointerDownEvent event) {
 | |
|     return event.buttons == initialButtons;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /// CountdownZoned tracks whether the specified duration has elapsed since
 | |
| /// creation, honoring [Zone].
 | |
| class _CountdownZoned {
 | |
|   _CountdownZoned({required Duration duration}) {
 | |
|     Timer(duration, _onTimeout);
 | |
|   }
 | |
| 
 | |
|   bool _timeout = false;
 | |
| 
 | |
|   bool get timeout => _timeout;
 | |
| 
 | |
|   void _onTimeout() {
 | |
|     _timeout = true;
 | |
|   }
 | |
| }
 | |
| 
 | |
| RawGestureDetector getMixinGestureDetector({
 | |
|   Widget? child,
 | |
|   GestureTapUpCallback? onTapUp,
 | |
|   GestureTapDownCallback? onDoubleTapDown,
 | |
|   GestureDoubleTapCallback? onDoubleTap,
 | |
|   GestureLongPressDownCallback? onLongPressDown,
 | |
|   GestureLongPressCallback? onLongPress,
 | |
|   GestureDragStartCallback? onHoldDragStart,
 | |
|   GestureDragUpdateCallback? onHoldDragUpdate,
 | |
|   GestureDragCancelCallback? onHoldDragCancel,
 | |
|   GestureDragEndCallback? onHoldDragEnd,
 | |
|   GestureTapDownCallback? onDoubleFinerTap,
 | |
|   GestureDragStartCallback? onOneFingerPanStart,
 | |
|   GestureDragUpdateCallback? onOneFingerPanUpdate,
 | |
|   GestureDragEndCallback? onOneFingerPanEnd,
 | |
|   GestureScaleUpdateCallback? onTwoFingerScaleUpdate,
 | |
|   GestureScaleEndCallback? onTwoFingerScaleEnd,
 | |
|   GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate,
 | |
| }) {
 | |
|   return RawGestureDetector(
 | |
|       child: child,
 | |
|       gestures: <Type, GestureRecognizerFactory>{
 | |
|         // Official
 | |
|         TapGestureRecognizer:
 | |
|             GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
 | |
|                 () => TapGestureRecognizer(), (instance) {
 | |
|           instance.onTapUp = onTapUp;
 | |
|         }),
 | |
|         DoubleTapGestureRecognizer:
 | |
|             GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
 | |
|                 () => DoubleTapGestureRecognizer(), (instance) {
 | |
|           instance
 | |
|             ..onDoubleTapDown = onDoubleTapDown
 | |
|             ..onDoubleTap = onDoubleTap;
 | |
|         }),
 | |
|         LongPressGestureRecognizer:
 | |
|             GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
 | |
|                 () => LongPressGestureRecognizer(), (instance) {
 | |
|           instance
 | |
|             ..onLongPressDown = onLongPressDown
 | |
|             ..onLongPress = onLongPress;
 | |
|         }),
 | |
|         // Customized
 | |
|         HoldTapMoveGestureRecognizer:
 | |
|             GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
 | |
|                 () => HoldTapMoveGestureRecognizer(),
 | |
|                 (instance) => {
 | |
|                       instance
 | |
|                         ..onHoldDragStart = onHoldDragStart
 | |
|                         ..onHoldDragUpdate = onHoldDragUpdate
 | |
|                         ..onHoldDragCancel = onHoldDragCancel
 | |
|                         ..onHoldDragEnd = onHoldDragEnd
 | |
|                     }),
 | |
|         DoubleFinerTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
 | |
|                 DoubleFinerTapGestureRecognizer>(
 | |
|             () => DoubleFinerTapGestureRecognizer(), (instance) {
 | |
|           instance.onDoubleFinerTap = onDoubleFinerTap;
 | |
|         }),
 | |
|         CustomTouchGestureRecognizer:
 | |
|             GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
 | |
|                 () => CustomTouchGestureRecognizer(), (instance) {
 | |
|           instance
 | |
|             ..onOneFingerPanStart = onOneFingerPanStart
 | |
|             ..onOneFingerPanUpdate = onOneFingerPanUpdate
 | |
|             ..onOneFingerPanEnd = onOneFingerPanEnd
 | |
|             ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
 | |
|             ..onTwoFingerScaleEnd = onTwoFingerScaleEnd
 | |
|             ..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;
 | |
|         }),
 | |
|       });
 | |
| }
 |