recognizer.dart 22 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5

6
import 'dart:async';
7
import 'dart:collection';
8

9
import 'package:flutter/foundation.dart';
10
import 'package:vector_math/vector_math_64.dart';
11

12
import 'arena.dart';
13
import 'binding.dart';
14
import 'constants.dart';
15
import 'debug.dart';
16
import 'events.dart';
17
import 'pointer_router.dart';
18
import 'team.dart';
19

20
export 'pointer_router.dart' show PointerRouter;
21

22 23 24 25
/// Generic signature for callbacks passed to
/// [GestureRecognizer.invokeCallback]. This allows the
/// [GestureRecognizer.invokeCallback] mechanism to be generically used with
/// anonymous functions that return objects of particular types.
26
typedef RecognizerCallback<T> = T Function();
27

28 29 30 31 32 33 34
/// Configuration of offset passed to [DragStartDetails].
///
/// The settings determines when a drag formally starts when the user
/// initiates a drag.
///
/// See also:
///
35
///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
36
enum DragStartBehavior {
37
  /// Set the initial offset, at the position where the first down event was
38 39 40 41 42 43 44 45
  /// detected.
  down,

  /// Set the initial position at the position where the drag start event was
  /// detected.
  start,
}

46
/// The base class that all gesture recognizers inherit from.
47 48 49 50
///
/// Provides a basic API that can be used by classes that work with
/// gesture recognizers but don't care about the specific details of
/// the gestures recognizers themselves.
51 52 53
///
/// See also:
///
54 55
///  * [GestureDetector], the widget that is used to detect built-in gestures.
///  * [RawGestureDetector], the widget that is used to detect custom gestures.
56 57
///  * [debugPrintRecognizerCallbacksTrace], a flag that can be set to help
///    debug issues with gesture recognizers.
58
abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin {
59 60 61 62
  /// Initializes the gesture recognizer.
  ///
  /// The argument is optional and is only used for debug purposes (e.g. in the
  /// [toString] serialization).
63
  ///
64
  /// {@template flutter.gestures.GestureRecognizer.kind}
65 66 67 68
  /// It's possible to limit this recognizer to a specific [PointerDeviceKind]
  /// by providing the optional [kind] argument. If [kind] is null,
  /// the recognizer will accept pointer events from all device kinds.
  /// {@endtemplate}
69
  GestureRecognizer({ this.debugOwner, PointerDeviceKind? kind }) : _kindFilter = kind;
70 71 72 73 74

  /// The recognizer's owner.
  ///
  /// This is used in the [toString] serialization to report the object for which
  /// this gesture recognizer was created, to aid in debugging.
75
  final Object? debugOwner;
76

77 78
  /// The kind of device that's allowed to be recognized. If null, events from
  /// all device kinds will be tracked and recognized.
79
  final PointerDeviceKind? _kindFilter;
80 81 82 83

  /// Holds a mapping between pointer IDs and the kind of devices they are
  /// coming from.
  final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{};
84

85 86
  /// Registers a new pointer that might be relevant to this gesture
  /// detector.
Florian Loitsch's avatar
Florian Loitsch committed
87
  ///
88 89 90 91 92 93 94
  /// The owner of this gesture recognizer calls addPointer() with the
  /// PointerDownEvent of each pointer that should be considered for
  /// this gesture.
  ///
  /// It's the GestureRecognizer's responsibility to then add itself
  /// to the global pointer router (see [PointerRouter]) to receive
  /// subsequent events for this pointer, and to add the pointer to
95
  /// the global gesture arena manager (see [GestureArenaManager]) to track
96
  /// that pointer.
97 98 99 100
  ///
  /// This method is called for each and all pointers being added. In
  /// most cases, you want to override [addAllowedPointer] instead.
  void addPointer(PointerDownEvent event) {
101
    _pointerToKind[event.pointer] = event.kind;
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
    if (isPointerAllowed(event)) {
      addAllowedPointer(event);
    } else {
      handleNonAllowedPointer(event);
    }
  }

  /// Registers a new pointer that's been checked to be allowed by this gesture
  /// recognizer.
  ///
  /// Subclasses of [GestureRecognizer] are supposed to override this method
  /// instead of [addPointer] because [addPointer] will be called for each
  /// pointer being added while [addAllowedPointer] is only called for pointers
  /// that are allowed by this recognizer.
  @protected
  void addAllowedPointer(PointerDownEvent event) { }

  /// Handles a pointer being added that's not allowed by this recognizer.
  ///
  /// Subclasses can override this method and reject the gesture.
  ///
  /// See:
  /// - [OneSequenceGestureRecognizer.handleNonAllowedPointer].
  @protected
  void handleNonAllowedPointer(PointerDownEvent event) { }

  /// Checks whether or not a pointer is allowed to be tracked by this recognizer.
  @protected
  bool isPointerAllowed(PointerDownEvent event) {
    // Currently, it only checks for device kind. But in the future we could check
    // for other things e.g. mouse button.
133 134 135 136 137 138 139 140 141 142
    return _kindFilter == null || _kindFilter == event.kind;
  }

  /// For a given pointer ID, returns the device kind associated with it.
  ///
  /// The pointer ID is expected to be a valid one i.e. an event was received
  /// with that pointer ID.
  @protected
  PointerDeviceKind getKindForPointer(int pointer) {
    assert(_pointerToKind.containsKey(pointer));
143
    return _pointerToKind[pointer]!;
144
  }
145

Florian Loitsch's avatar
Florian Loitsch committed
146 147
  /// Releases any resources used by the object.
  ///
148 149
  /// This method is called by the owner of this gesture recognizer
  /// when the object is no longer needed (e.g. when a gesture
150
  /// recognizer is being unregistered from a [GestureDetector], the
151
  /// GestureDetector widget calls this method).
152
  @mustCallSuper
153 154
  void dispose() { }

155 156
  /// Returns a very short pretty description of the gesture that the
  /// recognizer looks for, like 'tap' or 'horizontal drag'.
157
  String get debugDescription;
158

159 160
  /// Invoke a callback provided by the application, catching and logging any
  /// exceptions.
161 162
  ///
  /// The `name` argument is ignored except when reporting exceptions.
163 164 165 166 167
  ///
  /// The `debugReport` argument is optional and is used when
  /// [debugPrintRecognizerCallbacksTrace] is true. If specified, it must be a
  /// callback that returns a string describing useful debugging information,
  /// e.g. the arguments passed to the callback.
168
  @protected
169
  @pragma('vm:notify-debugger-on-exception')
170
  T? invokeCallback<T>(String name, RecognizerCallback<T> callback, { String Function()? debugReport }) {
171
    assert(callback != null);
172
    T? result;
173
    try {
174 175
      assert(() {
        if (debugPrintRecognizerCallbacksTrace) {
176
          final String? report = debugReport != null ? debugReport() : null;
177 178 179 180 181 182
          // The 19 in the line below is the width of the prefix used by
          // _debugLogDiagnostic in arena.dart.
          final String prefix = debugPrintGestureArenaDiagnostics ? ' ' * 19 + '❙ ' : '';
          debugPrint('$prefix$this calling $name callback.${ report?.isNotEmpty == true ? " $report" : "" }');
        }
        return true;
183
      }());
184 185
      result = callback();
    } catch (exception, stack) {
186
      InformationCollector? collector;
187 188 189 190 191 192 193
      assert(() {
        collector = () sync* {
          yield StringProperty('Handler', name);
          yield DiagnosticsProperty<GestureRecognizer>('Recognizer', this, style: DiagnosticsTreeStyle.errorProperty);
        };
        return true;
      }());
194
      FlutterError.reportError(FlutterErrorDetails(
195 196 197
        exception: exception,
        stack: stack,
        library: 'gesture',
198
        context: ErrorDescription('while handling a gesture'),
199
        informationCollector: collector,
200 201 202 203
      ));
    }
    return result;
  }
204 205

  @override
206 207
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
208
    properties.add(DiagnosticsProperty<Object>('debugOwner', debugOwner, defaultValue: null));
209
  }
210 211
}

212 213 214 215 216 217 218 219
/// Base class for gesture recognizers that can only recognize one
/// gesture at a time. For example, a single [TapGestureRecognizer]
/// can never recognize two taps happening simultaneously, even if
/// multiple pointers are placed on the same widget.
///
/// This is in contrast to, for instance, [MultiTapGestureRecognizer],
/// which manages each pointer independently and can consider multiple
/// simultaneous touches to each result in a separate tap.
220
abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
221
  /// Initialize the object.
222
  ///
223
  /// {@macro flutter.gestures.GestureRecognizer.kind}
224
  OneSequenceGestureRecognizer({
225 226
    Object? debugOwner,
    PointerDeviceKind? kind,
227
  }) : super(debugOwner: debugOwner, kind: kind);
228

229
  final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
230
  final Set<int> _trackedPointers = HashSet<int>();
231

232 233 234 235 236
  @override
  void handleNonAllowedPointer(PointerDownEvent event) {
    resolve(GestureDisposition.rejected);
  }

237 238
  /// Called when a pointer event is routed to this recognizer.
  @protected
Ian Hickson's avatar
Ian Hickson committed
239
  void handleEvent(PointerEvent event);
240 241

  @override
242
  void acceptGesture(int pointer) { }
243 244

  @override
245
  void rejectGesture(int pointer) { }
246

247
  /// Called when the number of pointers this recognizer is tracking changes from one to zero.
248 249 250
  ///
  /// The given pointer ID is the ID of the last pointer this recognizer was
  /// tracking.
251
  @protected
252
  void didStopTrackingLastPointer(int pointer);
253

254 255
  /// Resolves this recognizer's participation in each gesture arena with the
  /// given disposition.
256 257
  @protected
  @mustCallSuper
258
  void resolve(GestureDisposition disposition) {
259
    final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
260
    _entries.clear();
261
    for (final GestureArenaEntry entry in localEntries)
262 263 264
      entry.resolve(disposition);
  }

265 266 267 268 269
  /// Resolves this recognizer's participation in the given gesture arena with
  /// the given disposition.
  @protected
  @mustCallSuper
  void resolvePointer(int pointer, GestureDisposition disposition) {
270
    final GestureArenaEntry? entry = _entries[pointer];
271
    if (entry != null) {
272
      _entries.remove(pointer);
273
      entry.resolve(disposition);
274 275 276
    }
  }

277
  @override
278 279
  void dispose() {
    resolve(GestureDisposition.rejected);
280
    for (final int pointer in _trackedPointers)
281
      GestureBinding.instance!.pointerRouter.removeRoute(pointer, handleEvent);
282 283
    _trackedPointers.clear();
    assert(_entries.isEmpty);
284
    super.dispose();
285 286
  }

287 288 289 290 291 292 293 294 295 296
  /// The team that this recognizer belongs to, if any.
  ///
  /// If [team] is null, this recognizer competes directly in the
  /// [GestureArenaManager] to recognize a sequence of pointer events as a
  /// gesture. If [team] is non-null, this recognizer competes in the arena in
  /// a group with other recognizers on the same team.
  ///
  /// A recognizer can be assigned to a team only when it is not participating
  /// in the arena. For example, a common time to assign a recognizer to a team
  /// is shortly after creating the recognizer.
297 298
  GestureArenaTeam? get team => _team;
  GestureArenaTeam? _team;
299
  /// The [team] can only be set once.
300
  set team(GestureArenaTeam? value) {
301
    assert(value != null);
302 303 304 305 306 307 308 309
    assert(_entries.isEmpty);
    assert(_trackedPointers.isEmpty);
    assert(_team == null);
    _team = value;
  }

  GestureArenaEntry _addPointerToArena(int pointer) {
    if (_team != null)
310 311
      return _team!.add(pointer, this);
    return GestureBinding.instance!.gestureArena.add(pointer, this);
312 313
  }

314 315
  /// Causes events related to the given pointer ID to be routed to this recognizer.
  ///
316 317 318 319 320
  /// The pointer events are transformed according to `transform` and then delivered
  /// to [handleEvent]. The value for the `transform` argument is usually obtained
  /// from [PointerDownEvent.transform] to transform the events from the global
  /// coordinate space into the coordinate space of the event receiver. It may be
  /// null if no transformation is necessary.
321 322
  ///
  /// Use [stopTrackingPointer] to remove the route added by this function.
323
  @protected
324 325
  void startTrackingPointer(int pointer, [Matrix4? transform]) {
    GestureBinding.instance!.pointerRouter.addRoute(pointer, handleEvent, transform);
326
    _trackedPointers.add(pointer);
327
    assert(!_entries.containsValue(pointer));
328
    _entries[pointer] = _addPointerToArena(pointer);
329 330
  }

331 332 333 334 335 336
  /// Stops events related to the given pointer ID from being routed to this recognizer.
  ///
  /// If this function reduces the number of tracked pointers to zero, it will
  /// call [didStopTrackingLastPointer] synchronously.
  ///
  /// Use [startTrackingPointer] to add the routes in the first place.
337
  @protected
338
  void stopTrackingPointer(int pointer) {
339
    if (_trackedPointers.contains(pointer)) {
340
      GestureBinding.instance!.pointerRouter.removeRoute(pointer, handleEvent);
341 342 343 344
      _trackedPointers.remove(pointer);
      if (_trackedPointers.isEmpty)
        didStopTrackingLastPointer(pointer);
    }
345 346
  }

347 348
  /// Stops tracking the pointer associated with the given event if the event is
  /// a [PointerUpEvent] or a [PointerCancelEvent] event.
349
  @protected
Ian Hickson's avatar
Ian Hickson committed
350 351
  void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
    if (event is PointerUpEvent || event is PointerCancelEvent)
352 353 354 355
      stopTrackingPointer(event.pointer);
  }
}

356 357
/// The possible states of a [PrimaryPointerGestureRecognizer].
///
358
/// The recognizer advances from [ready] to [possible] when it starts tracking a
359 360 361 362 363 364 365 366 367
/// primary pointer. Where it advances from there depends on how the gesture is
/// resolved for that pointer:
///
///  * If the primary pointer is resolved by the gesture winning the arena, the
///    recognizer stays in the [possible] state as long as it continues to track
///    a pointer.
///  * If the primary pointer is resolved by the gesture being rejected and
///    losing the arena, the recognizer's state advances to [defunct].
///
368 369
/// Once the recognizer has stopped tracking any remaining pointers, the
/// recognizer returns to [ready].
370
enum GestureRecognizerState {
371
  /// The recognizer is ready to start recognizing a gesture.
372
  ready,
373

374
  /// The sequence of pointer events seen thus far is consistent with the
375 376
  /// gesture the recognizer is attempting to recognize but the gesture has not
  /// been accepted definitively.
377
  possible,
378

379
  /// Further pointer events cannot cause this recognizer to recognize the
380 381 382
  /// gesture until the recognizer returns to the [ready] state (typically when
  /// all the pointers the recognizer is tracking are removed from the screen).
  defunct,
383 384
}

385
/// A base class for gesture recognizers that track a single primary pointer.
386
///
387 388 389 390 391 392
/// Gestures based on this class will stop tracking the gesture if the primary
/// pointer travels beyond [preAcceptSlopTolerance] or [postAcceptSlopTolerance]
/// pixels from the original contact point of the gesture.
///
/// If the [preAcceptSlopTolerance] was breached before the gesture was accepted
/// in the gesture arena, the gesture will be rejected.
393
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
394
  /// Initializes the [deadline] field during construction of subclasses.
395
  ///
396
  /// {@macro flutter.gestures.GestureRecognizer.kind}
397 398
  PrimaryPointerGestureRecognizer({
    this.deadline,
399 400
    this.preAcceptSlopTolerance = kTouchSlop,
    this.postAcceptSlopTolerance = kTouchSlop,
401 402
    Object? debugOwner,
    PointerDeviceKind? kind,
403 404 405 406 407 408 409 410
  }) : assert(
         preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
         'The preAcceptSlopTolerance must be positive or null',
       ),
       assert(
         postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0,
         'The postAcceptSlopTolerance must be positive or null',
       ),
411
       super(debugOwner: debugOwner, kind: kind);
412

413 414
  /// If non-null, the recognizer will call [didExceedDeadline] after this
  /// amount of time has elapsed since starting to track the primary pointer.
415 416 417
  ///
  /// The [didExceedDeadline] will not be called if the primary pointer is
  /// accepted, rejected, or all pointers are up or canceled before [deadline].
418
  final Duration? deadline;
419

420 421 422 423 424 425 426
  /// The maximum distance in logical pixels the gesture is allowed to drift
  /// from the initial touch down position before the gesture is accepted.
  ///
  /// Drifting past the allowed slop amount causes the gesture to be rejected.
  ///
  /// Can be null to indicate that the gesture can drift for any distance.
  /// Defaults to 18 logical pixels.
427
  final double? preAcceptSlopTolerance;
428 429 430 431 432 433 434 435 436

  /// The maximum distance in logical pixels the gesture is allowed to drift
  /// after the gesture has been accepted.
  ///
  /// Drifting past the allowed slop amount causes the gesture to stop tracking
  /// and signaling subsequent callbacks.
  ///
  /// Can be null to indicate that the gesture can drift for any distance.
  /// Defaults to 18 logical pixels.
437
  final double? postAcceptSlopTolerance;
438

439 440 441
  /// The current state of the recognizer.
  ///
  /// See [GestureRecognizerState] for a description of the states.
442
  GestureRecognizerState state = GestureRecognizerState.ready;
443 444

  /// The ID of the primary pointer this recognizer is tracking.
445
  int? primaryPointer;
446

447
  /// The location at which the primary pointer contacted the screen.
448
  OffsetPair? initialPosition;
449

450 451 452
  // Whether this pointer is accepted by winning the arena or as defined by
  // a subclass calling acceptGesture.
  bool _gestureAccepted = false;
453
  Timer? _timer;
454

455
  @override
456
  void addAllowedPointer(PointerDownEvent event) {
457
    startTrackingPointer(event.pointer, event.transform);
458 459 460
    if (state == GestureRecognizerState.ready) {
      state = GestureRecognizerState.possible;
      primaryPointer = event.pointer;
461
      initialPosition = OffsetPair(local: event.localPosition, global: event.position);
462
      if (deadline != null)
463
        _timer = Timer(deadline!, () => didExceedDeadlineWithEvent(event));
464 465 466
    }
  }

467 468 469 470 471 472 473
  @override
  void handleNonAllowedPointer(PointerDownEvent event) {
    if (!_gestureAccepted) {
      super.handleNonAllowedPointer(event);
    }
  }

474
  @override
Ian Hickson's avatar
Ian Hickson committed
475
  void handleEvent(PointerEvent event) {
476
    assert(state != GestureRecognizerState.ready);
xster's avatar
xster committed
477
    if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
478 479 480
      final bool isPreAcceptSlopPastTolerance =
          !_gestureAccepted &&
          preAcceptSlopTolerance != null &&
481
          _getGlobalDistance(event) > preAcceptSlopTolerance!;
482 483 484
      final bool isPostAcceptSlopPastTolerance =
          _gestureAccepted &&
          postAcceptSlopTolerance != null &&
485
          _getGlobalDistance(event) > postAcceptSlopTolerance!;
486 487

      if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
488
        resolve(GestureDisposition.rejected);
489
        stopTrackingPointer(primaryPointer!);
490
      } else {
491
        handlePrimaryPointer(event);
492
      }
493 494 495 496 497
    }
    stopTrackingIfPointerNoLongerDown(event);
  }

  /// Override to provide behavior for the primary pointer when the gesture is still possible.
498
  @protected
Ian Hickson's avatar
Ian Hickson committed
499
  void handlePrimaryPointer(PointerEvent event);
500

Florian Loitsch's avatar
Florian Loitsch committed
501
  /// Override to be notified when [deadline] is exceeded.
502
  ///
503 504
  /// You must override this method or [didExceedDeadlineWithEvent] if you
  /// supply a [deadline].
505
  @protected
506 507 508 509
  void didExceedDeadline() {
    assert(deadline == null);
  }

510 511 512 513 514 515 516 517 518 519
  /// Same as [didExceedDeadline] but receives the [event] that initiated the
  /// gesture.
  ///
  /// You must override this method or [didExceedDeadline] if you supply a
  /// [deadline].
  @protected
  void didExceedDeadlineWithEvent(PointerDownEvent event) {
    didExceedDeadline();
  }

520 521
  @override
  void acceptGesture(int pointer) {
522 523 524 525
    if (pointer == primaryPointer) {
      _stopTimer();
      _gestureAccepted = true;
    }
526 527
  }

528 529
  @override
  void rejectGesture(int pointer) {
xster's avatar
xster committed
530
    if (pointer == primaryPointer && state == GestureRecognizerState.possible) {
531
      _stopTimer();
532
      state = GestureRecognizerState.defunct;
533
    }
534 535
  }

536
  @override
537
  void didStopTrackingLastPointer(int pointer) {
538
    assert(state != GestureRecognizerState.ready);
539
    _stopTimer();
540 541 542
    state = GestureRecognizerState.ready;
  }

543
  @override
544 545 546 547 548 549 550
  void dispose() {
    _stopTimer();
    super.dispose();
  }

  void _stopTimer() {
    if (_timer != null) {
551
      _timer!.cancel();
552 553 554 555
      _timer = null;
    }
  }

556
  double _getGlobalDistance(PointerEvent event) {
557
    final Offset offset = event.position - initialPosition!.global;
558 559 560
    return offset.distance;
  }

561
  @override
562 563
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
564
    properties.add(EnumProperty<GestureRecognizerState>('state', state));
565
  }
566
}
567 568 569 570 571 572 573 574 575

/// A container for a [local] and [global] [Offset] pair.
///
/// Usually, the [global] [Offset] is in the coordinate space of the screen
/// after conversion to logical pixels and the [local] offset is the same
/// [Offset], but transformed to a local coordinate space.
class OffsetPair {
  /// Creates a [OffsetPair] combining a [local] and [global] [Offset].
  const OffsetPair({
576 577
    required this.local,
    required this.global,
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
  });

  /// Creates a [OffsetPair] from [PointerEvent.localPosition] and
  /// [PointerEvent.position].
  factory OffsetPair.fromEventPosition(PointerEvent event) {
    return OffsetPair(local: event.localPosition, global: event.position);
  }

  /// Creates a [OffsetPair] from [PointerEvent.localDelta] and
  /// [PointerEvent.delta].
  factory OffsetPair.fromEventDelta(PointerEvent event) {
    return OffsetPair(local: event.localDelta, global: event.delta);
  }

  /// A [OffsetPair] where both [Offset]s are [Offset.zero].
  static const OffsetPair zero = OffsetPair(local: Offset.zero, global: Offset.zero);

  /// The [Offset] in the local coordinate space.
  final Offset local;

  /// The [Offset] in the global coordinate space after conversion to logical
  /// pixels.
  final Offset global;

  /// Adds the `other.global` to [global] and `other.local` to [local].
  OffsetPair operator+(OffsetPair other) {
    return OffsetPair(
      local: local + other.local,
      global: global + other.global,
    );
  }

  /// Subtracts the `other.global` from [global] and `other.local` from [local].
  OffsetPair operator-(OffsetPair other) {
    return OffsetPair(
      local: local - other.local,
      global: global - other.global,
    );
  }

  @override
619
  String toString() => '${objectRuntimeType(this, 'OffsetPair')}(local: $local, global: $global)';
620
}