tap.dart 23.9 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 'package:flutter/foundation.dart';
7
import 'package:vector_math/vector_math_64.dart' show Matrix4;
8

9
import 'arena.dart';
10
import 'constants.dart';
11
import 'events.dart';
12
import 'recognizer.dart';
13

14
/// Details for [GestureTapDownCallback], such as position.
15 16 17 18 19
///
/// See also:
///
///  * [GestureDetector.onTapDown], which receives this information.
///  * [TapGestureRecognizer], which passes this information to one of its callbacks.
20 21 22 23
class TapDownDetails {
  /// Creates details for a [GestureTapDownCallback].
  ///
  /// The [globalPosition] argument must not be null.
24 25
  TapDownDetails({
    this.globalPosition = Offset.zero,
26
    Offset? localPosition,
27
    this.kind,
28 29
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;
30 31

  /// The global position at which the pointer contacted the screen.
32
  final Offset globalPosition;
33 34

  /// The kind of the device that initiated the event.
35
  final PointerDeviceKind? kind;
36 37 38

  /// The local position at which the pointer contacted the screen.
  final Offset localPosition;
39 40 41 42 43 44 45
}

/// Signature for when a pointer that might cause a tap has contacted the
/// screen.
///
/// The position at which the pointer contacted the screen is available in the
/// `details`.
46 47 48 49 50
///
/// See also:
///
///  * [GestureDetector.onTapDown], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
51
typedef GestureTapDownCallback = void Function(TapDownDetails details);
52 53

/// Details for [GestureTapUpCallback], such as position.
54 55 56 57 58
///
/// See also:
///
///  * [GestureDetector.onTapUp], which receives this information.
///  * [TapGestureRecognizer], which passes this information to one of its callbacks.
59 60
class TapUpDetails {
  /// The [globalPosition] argument must not be null.
61
  TapUpDetails({
62
    required this.kind,
63
    this.globalPosition = Offset.zero,
64
    Offset? localPosition,
65 66
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;
67 68

  /// The global position at which the pointer contacted the screen.
69
  final Offset globalPosition;
70 71 72

  /// The local position at which the pointer contacted the screen.
  final Offset localPosition;
73 74 75

  /// The kind of the device that initiated the event.
  final PointerDeviceKind kind;
76
}
77 78

/// Signature for when a pointer that will trigger a tap has stopped contacting
79 80 81 82
/// the screen.
///
/// The position at which the pointer stopped contacting the screen is available
/// in the `details`.
83 84 85 86 87
///
/// See also:
///
///  * [GestureDetector.onTapUp], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
88
typedef GestureTapUpCallback = void Function(TapUpDetails details);
89 90

/// Signature for when a tap has occurred.
91 92 93 94 95
///
/// See also:
///
///  * [GestureDetector.onTap], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
96
typedef GestureTapCallback = void Function();
97 98 99

/// Signature for when the pointer that previously triggered a
/// [GestureTapDownCallback] will not end up causing a tap.
100 101 102 103 104
///
/// See also:
///
///  * [GestureDetector.onTapCancel], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
105
typedef GestureTapCancelCallback = void Function();
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 133 134 135 136 137 138
/// A base class for gesture recognizers that recognize taps.
///
/// Gesture recognizers take part in gesture arenas to enable potential gestures
/// to be disambiguated from each other. This process is managed by a
/// [GestureArenaManager].
///
/// A tap is defined as a sequence of events that starts with a down, followed
/// by optional moves, then ends with an up. All move events must contain the
/// same `buttons` as the down event, and must not be too far from the initial
/// position. The gesture is rejected on any violation, a cancel event, or
/// if any other recognizers wins the arena. It is accepted only when it is the
/// last member of the arena.
///
/// The [BaseTapGestureRecognizer] considers all the pointers involved in the
/// pointer event sequence as contributing to one gesture. For this reason,
/// extra pointer interactions during a tap sequence are not recognized as
/// additional taps. For example, down-1, down-2, up-1, up-2 produces only one
/// tap on up-1.
///
/// The [BaseTapGestureRecognizer] can not be directly used, since it does not
/// define which buttons to accept, or what to do when a tap happens. If you
/// want to build a custom tap recognizer, extend this class by overriding
/// [isPointerAllowed] and the handler methods.
///
/// See also:
///
///  * [TapGestureRecognizer], a ready-to-use tap recognizer that recognizes
///    taps of the primary button and taps of the secondary button.
///  * [ModalBarrier], a widget that uses a custom tap recognizer that accepts
///    any buttons.
abstract class BaseTapGestureRecognizer extends PrimaryPointerGestureRecognizer {
  /// Creates a tap gesture recognizer.
139
  BaseTapGestureRecognizer({ Object? debugOwner })
140 141 142 143 144
    : super(deadline: kPressTimeout , debugOwner: debugOwner);

  bool _sentTapDown = false;
  bool _wonArenaForPrimaryPointer = false;

145 146
  PointerDownEvent? _down;
  PointerUpEvent? _up;
147 148 149 150 151 152 153 154 155 156 157 158

  /// A pointer has contacted the screen, which might be the start of a tap.
  ///
  /// This triggers after the down event, once a short timeout ([deadline]) has
  /// elapsed, or once the gesture has won the arena, whichever comes first.
  ///
  /// The parameter `down` is the down event of the primary pointer that started
  /// the tap sequence.
  ///
  /// If this recognizer doesn't win the arena, [handleTapCancel] is called next.
  /// Otherwise, [handleTapUp] is called next.
  @protected
159
  void handleTapDown({ required PointerDownEvent down });
160 161 162

  /// A pointer has stopped contacting the screen, which is recognized as a tap.
  ///
163
  /// This triggers on the up event if the recognizer wins the arena with it
164 165 166 167 168 169 170 171
  /// or has previously won.
  ///
  /// The parameter `down` is the down event of the primary pointer that started
  /// the tap sequence, and `up` is the up event that ended the tap sequence.
  ///
  /// If this recognizer doesn't win the arena, [handleTapCancel] is called
  /// instead.
  @protected
172
  void handleTapUp({ required PointerDownEvent down, required PointerUpEvent up });
173 174 175 176

  /// A pointer that previously triggered [handleTapDown] will not end up
  /// causing a tap.
  ///
177
  /// This triggers once the gesture loses the arena if [handleTapDown] has
178 179 180 181 182 183 184 185 186 187
  /// been previously triggered.
  ///
  /// The parameter `down` is the down event of the primary pointer that started
  /// the tap sequence; `cancel` is the cancel event, which might be null;
  /// `reason` is a short description of the cause if `cancel` is null, which
  /// can be "forced" if other gestures won the arena, or "spontaneous"
  /// otherwise.
  ///
  /// If this recognizer wins the arena, [handleTapUp] is called instead.
  @protected
188
  void handleTapCancel({ required PointerDownEvent down, PointerCancelEvent? cancel, required String reason });
189 190 191

  @override
  void addAllowedPointer(PointerDownEvent event) {
192
    assert(event != null);
193
    if (state == GestureRecognizerState.ready) {
194 195 196 197 198 199 200 201
      // If there is no result in the previous gesture arena,
      // we ignore them and prepare to accept a new pointer.
      if (_down != null && _up != null) {
        assert(_down!.pointer == _up!.pointer);
        _reset();
      }

      assert(_down == null && _up == null);
202 203 204 205 206
      // `_down` must be assigned in this method instead of `handlePrimaryPointer`,
      // because `acceptGesture` might be called before `handlePrimaryPointer`,
      // which relies on `_down` to call `handleTapDown`.
      _down = event;
    }
207 208 209 210 211 212 213 214 215 216 217 218
    if (_down != null) {
      // This happens when this tap gesture has been rejected while the pointer
      // is down (i.e. due to movement), when another allowed pointer is added,
      // in which case all pointers are simply ignored. The `_down` being null
      // means that _reset() has been called, since it is always set at the
      // first allowed down event and will not be cleared except for reset(),
      super.addAllowedPointer(event);
    }
  }

  @override
  @protected
219
  void startTrackingPointer(int pointer, [Matrix4? transform]) {
220 221 222 223
    // The recognizer should never track any pointers when `_down` is null,
    // because calling `_checkDown` in this state will throw exception.
    assert(_down != null);
    super.startTrackingPointer(pointer, transform);
224 225 226 227 228 229 230 231 232 233 234 235 236
  }

  @override
  void handlePrimaryPointer(PointerEvent event) {
    if (event is PointerUpEvent) {
      _up = event;
      _checkUp();
    } else if (event is PointerCancelEvent) {
      resolve(GestureDisposition.rejected);
      if (_sentTapDown) {
        _checkCancel(event, '');
      }
      _reset();
237
    } else if (event.buttons != _down!.buttons) {
238
      resolve(GestureDisposition.rejected);
239
      stopTrackingPointer(primaryPointer!);
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
    }
  }

  @override
  void resolve(GestureDisposition disposition) {
    if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
      // This can happen if the gesture has been canceled. For example, when
      // the pointer has exceeded the touch slop, the buttons have been changed,
      // or if the recognizer is disposed.
      assert(_sentTapDown);
      _checkCancel(null, 'spontaneous');
      _reset();
    }
    super.resolve(disposition);
  }

  @override
  void didExceedDeadline() {
    _checkDown();
  }

  @override
  void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    if (pointer == primaryPointer) {
      _checkDown();
      _wonArenaForPrimaryPointer = true;
      _checkUp();
    }
  }

  @override
  void rejectGesture(int pointer) {
    super.rejectGesture(pointer);
    if (pointer == primaryPointer) {
      // Another gesture won the arena.
      assert(state != GestureRecognizerState.possible);
      if (_sentTapDown)
        _checkCancel(null, 'forced');
      _reset();
    }
  }

  void _checkDown() {
    if (_sentTapDown) {
      return;
    }
287
    handleTapDown(down: _down!);
288 289 290 291 292 293 294
    _sentTapDown = true;
  }

  void _checkUp() {
    if (!_wonArenaForPrimaryPointer || _up == null) {
      return;
    }
295
    assert(_up!.pointer == _down!.pointer);
296
    handleTapUp(down: _down!, up: _up!);
297 298 299
    _reset();
  }

300 301
  void _checkCancel(PointerCancelEvent? event, String note) {
    handleTapCancel(down: _down!, cancel: event, reason: note);
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
  }

  void _reset() {
    _sentTapDown = false;
    _wonArenaForPrimaryPointer = false;
    _up = null;
    _down = null;
  }

  @override
  String get debugDescription => 'base tap';

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
    properties.add(DiagnosticsProperty<Offset>('finalPosition', _up?.position, defaultValue: null));
    properties.add(DiagnosticsProperty<Offset>('finalLocalPosition', _up?.localPosition, defaultValue: _up?.position));
    properties.add(DiagnosticsProperty<int>('button', _down?.buttons, defaultValue: null));
    properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
  }
}

325 326
/// Recognizes taps.
///
327 328
/// Gesture recognizers take part in gesture arenas to enable potential gestures
/// to be disambiguated from each other. This process is managed by a
329
/// [GestureArenaManager].
330
///
331 332 333
/// [TapGestureRecognizer] considers all the pointers involved in the pointer
/// event sequence as contributing to one gesture. For this reason, extra
/// pointer interactions during a tap sequence are not recognized as additional
334
/// taps. For example, down-1, down-2, up-1, up-2 produces only one tap on up-1.
335
///
336
/// [TapGestureRecognizer] competes on pointer events of [kPrimaryButton] only
337
/// when it has at least one non-null `onTap*` callback, on events of
338
/// [kSecondaryButton] only when it has at least one non-null `onSecondaryTap*`
339 340 341
/// callback, and on events of [kTertiaryButton] only when it has at least
/// one non-null `onTertiaryTap*` callback. If it has no callbacks, it is a
/// no-op.
342
///
343 344
/// See also:
///
345
///  * [GestureDetector.onTap], which uses this recognizer.
346
///  * [MultiTapGestureRecognizer]
347
class TapGestureRecognizer extends BaseTapGestureRecognizer {
348
  /// Creates a tap gesture recognizer.
349
  TapGestureRecognizer({ Object? debugOwner }) : super(debugOwner: debugOwner);
350

351 352
  /// A pointer has contacted the screen at a particular location with a primary
  /// button, which might be the start of a tap.
353
  ///
354 355
  /// This triggers after the down event, once a short timeout ([deadline]) has
  /// elapsed, or once the gestures has won the arena, whichever comes first.
356
  ///
357
  /// If this recognizer doesn't win the arena, [onTapCancel] is called next.
358 359 360 361
  /// Otherwise, [onTapUp] is called next.
  ///
  /// See also:
  ///
362 363
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onSecondaryTapDown], a similar callback but for a secondary button.
364
  ///  * [onTertiaryTapDown], a similar callback but for a tertiary button.
365
  ///  * [TapDownDetails], which is passed as an argument to this callback.
366
  ///  * [GestureDetector.onTapDown], which exposes this callback.
367
  GestureTapDownCallback? onTapDown;
368

369 370
  /// A pointer has stopped contacting the screen at a particular location,
  /// which is recognized as a tap of a primary button.
371
  ///
372 373
  /// This triggers on the up event, if the recognizer wins the arena with it
  /// or has previously won, immediately followed by [onTap].
374
  ///
375
  /// If this recognizer doesn't win the arena, [onTapCancel] is called instead.
376 377 378
  ///
  /// See also:
  ///
379 380
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onSecondaryTapUp], a similar callback but for a secondary button.
381
  ///  * [onTertiaryTapUp], a similar callback but for a tertiary button.
382
  ///  * [TapUpDetails], which is passed as an argument to this callback.
383
  ///  * [GestureDetector.onTapUp], which exposes this callback.
384
  GestureTapUpCallback? onTapUp;
385

386 387
  /// A pointer has stopped contacting the screen, which is recognized as a tap
  /// of a primary button.
388
  ///
389
  /// This triggers on the up event, if the recognizer wins the arena with it
390
  /// or has previously won, immediately following [onTapUp].
391
  ///
392
  /// If this recognizer doesn't win the arena, [onTapCancel] is called instead.
393 394 395
  ///
  /// See also:
  ///
396
  ///  * [kPrimaryButton], the button this callback responds to.
397
  ///  * [onSecondaryTap], a similar callback but for a secondary button.
398
  ///  * [onTapUp], which has the same timing but with details.
399
  ///  * [GestureDetector.onTap], which exposes this callback.
400
  GestureTapCallback? onTap;
401

402
  /// A pointer that previously triggered [onTapDown] will not end up causing
403
  /// a tap.
404
  ///
405
  /// This triggers once the gesture loses the arena if [onTapDown] has
406
  /// previously been triggered.
407
  ///
408 409
  /// If this recognizer wins the arena, [onTapUp] and [onTap] are called
  /// instead.
410 411 412
  ///
  /// See also:
  ///
413 414
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onSecondaryTapCancel], a similar callback but for a secondary button.
415
  ///  * [onTertiaryTapCancel], a similar callback but for a tertiary button.
416
  ///  * [GestureDetector.onTapCancel], which exposes this callback.
417
  GestureTapCancelCallback? onTapCancel;
418

419 420 421 422 423 424 425 426 427 428 429 430 431 432
  /// A pointer has stopped contacting the screen, which is recognized as a tap
  /// of a secondary button.
  ///
  /// This triggers on the up event, if the recognizer wins the arena with it or
  /// has previously won, immediately following [onSecondaryTapUp].
  ///
  /// If this recognizer doesn't win the arena, [onSecondaryTapCancel] is called
  /// instead.
  ///
  /// See also:
  ///
  ///  * [kSecondaryButton], the button this callback responds to.
  ///  * [onSecondaryTapUp], which has the same timing but with details.
  ///  * [GestureDetector.onSecondaryTap], which exposes this callback.
433
  GestureTapCallback? onSecondaryTap;
434

435 436
  /// A pointer has contacted the screen at a particular location with a
  /// secondary button, which might be the start of a secondary tap.
437
  ///
438 439
  /// This triggers after the down event, once a short timeout ([deadline]) has
  /// elapsed, or once the gestures has won the arena, whichever comes first.
440
  ///
441 442
  /// If this recognizer doesn't win the arena, [onSecondaryTapCancel] is called
  /// next. Otherwise, [onSecondaryTapUp] is called next.
443 444 445 446
  ///
  /// See also:
  ///
  ///  * [kSecondaryButton], the button this callback responds to.
Dan Field's avatar
Dan Field committed
447
  ///  * [onTapDown], a similar callback but for a primary button.
448
  ///  * [onTertiaryTapDown], a similar callback but for a tertiary button.
449 450
  ///  * [TapDownDetails], which is passed as an argument to this callback.
  ///  * [GestureDetector.onSecondaryTapDown], which exposes this callback.
451
  GestureTapDownCallback? onSecondaryTapDown;
452

453 454
  /// A pointer has stopped contacting the screen at a particular location,
  /// which is recognized as a tap of a secondary button.
455
  ///
456
  /// This triggers on the up event if the recognizer wins the arena with it
457
  /// or has previously won.
458
  ///
459
  /// If this recognizer doesn't win the arena, [onSecondaryTapCancel] is called
460 461 462 463
  /// instead.
  ///
  /// See also:
  ///
464 465
  ///  * [onSecondaryTap], a handler triggered right after this one that doesn't
  ///    pass any details about the tap.
466
  ///  * [kSecondaryButton], the button this callback responds to.
Dan Field's avatar
Dan Field committed
467
  ///  * [onTapUp], a similar callback but for a primary button.
468
  ///  * [onTertiaryTapUp], a similar callback but for a tertiary button.
469 470
  ///  * [TapUpDetails], which is passed as an argument to this callback.
  ///  * [GestureDetector.onSecondaryTapUp], which exposes this callback.
471
  GestureTapUpCallback? onSecondaryTapUp;
472

473
  /// A pointer that previously triggered [onSecondaryTapDown] will not end up
474 475
  /// causing a tap.
  ///
476
  /// This triggers once the gesture loses the arena if [onSecondaryTapDown]
477
  /// has previously been triggered.
478
  ///
479
  /// If this recognizer wins the arena, [onSecondaryTapUp] is called instead.
480 481 482 483
  ///
  /// See also:
  ///
  ///  * [kSecondaryButton], the button this callback responds to.
Dan Field's avatar
Dan Field committed
484
  ///  * [onTapCancel], a similar callback but for a primary button.
485 486
  ///  * [onTertiaryTapCancel], a similar callback but for a tertiary button.
  ///  * [GestureDetector.onSecondaryTapCancel], which exposes this callback.
487
  GestureTapCancelCallback? onSecondaryTapCancel;
488

489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540
  /// A pointer has contacted the screen at a particular location with a
  /// tertiary button, which might be the start of a tertiary tap.
  ///
  /// This triggers after the down event, once a short timeout ([deadline]) has
  /// elapsed, or once the gestures has won the arena, whichever comes first.
  ///
  /// If this recognizer doesn't win the arena, [onTertiaryTapCancel] is called
  /// next. Otherwise, [onTertiaryTapUp] is called next.
  ///
  /// See also:
  ///
  ///  * [kTertiaryButton], the button this callback responds to.
  ///  * [onTapDown], a similar callback but for a primary button.
  ///  * [onSecondaryTapDown], a similar callback but for a secondary button.
  ///  * [TapDownDetails], which is passed as an argument to this callback.
  ///  * [GestureDetector.onTertiaryTapDown], which exposes this callback.
  GestureTapDownCallback? onTertiaryTapDown;

  /// A pointer has stopped contacting the screen at a particular location,
  /// which is recognized as a tap of a tertiary button.
  ///
  /// This triggers on the up event if the recognizer wins the arena with it
  /// or has previously won.
  ///
  /// If this recognizer doesn't win the arena, [onTertiaryTapCancel] is called
  /// instead.
  ///
  /// See also:
  ///
  ///  * [kTertiaryButton], the button this callback responds to.
  ///  * [onTapUp], a similar callback but for a primary button.
  ///  * [onSecondaryTapUp], a similar callback but for a secondary button.
  ///  * [TapUpDetails], which is passed as an argument to this callback.
  ///  * [GestureDetector.onTertiaryTapUp], which exposes this callback.
  GestureTapUpCallback? onTertiaryTapUp;

  /// A pointer that previously triggered [onTertiaryTapDown] will not end up
  /// causing a tap.
  ///
  /// This triggers once the gesture loses the arena if [onTertiaryTapDown]
  /// has previously been triggered.
  ///
  /// If this recognizer wins the arena, [onTertiaryTapUp] is called instead.
  ///
  /// See also:
  ///
  ///  * [kSecondaryButton], the button this callback responds to.
  ///  * [onTapCancel], a similar callback but for a primary button.
  ///  * [onSecondaryTapCancel], a similar callback but for a secondary button.
  ///  * [GestureDetector.onTertiaryTapCancel], which exposes this callback.
  GestureTapCancelCallback? onTertiaryTapCancel;

541 542 543 544 545 546 547 548 549 550 551
  @override
  bool isPointerAllowed(PointerDownEvent event) {
    switch (event.buttons) {
      case kPrimaryButton:
        if (onTapDown == null &&
            onTap == null &&
            onTapUp == null &&
            onTapCancel == null)
          return false;
        break;
      case kSecondaryButton:
552 553
        if (onSecondaryTap == null &&
            onSecondaryTapDown == null &&
554 555 556 557
            onSecondaryTapUp == null &&
            onSecondaryTapCancel == null)
          return false;
        break;
558 559 560 561 562 563
      case kTertiaryButton:
        if (onTertiaryTapDown == null &&
            onTertiaryTapUp == null &&
            onTertiaryTapCancel == null)
          return false;
        break;
564 565 566 567 568 569
      default:
        return false;
    }
    return super.isPointerAllowed(event);
  }

570
  @protected
571
  @override
572
  void handleTapDown({required PointerDownEvent down}) {
573
    final TapDownDetails details = TapDownDetails(
574 575 576
      globalPosition: down.position,
      localPosition: down.localPosition,
      kind: getKindForPointer(down.pointer),
577
    );
578
    switch (down.buttons) {
579 580
      case kPrimaryButton:
        if (onTapDown != null)
581
          invokeCallback<void>('onTapDown', () => onTapDown!(details));
582 583 584
        break;
      case kSecondaryButton:
        if (onSecondaryTapDown != null)
585
          invokeCallback<void>('onSecondaryTapDown', () => onSecondaryTapDown!(details));
586
        break;
587 588 589 590
      case kTertiaryButton:
        if (onTertiaryTapDown != null)
          invokeCallback<void>('onTertiaryTapDown', () => onTertiaryTapDown!(details));
        break;
591
      default:
Hixie's avatar
Hixie committed
592 593 594
    }
  }

595 596
  @protected
  @override
597
  void handleTapUp({ required PointerDownEvent down, required PointerUpEvent up}) {
598
    final TapUpDetails details = TapUpDetails(
599
      kind: up.kind,
600 601
      globalPosition: up.position,
      localPosition: up.localPosition,
602
    );
603
    switch (down.buttons) {
604 605
      case kPrimaryButton:
        if (onTapUp != null)
606
          invokeCallback<void>('onTapUp', () => onTapUp!(details));
607
        if (onTap != null)
608
          invokeCallback<void>('onTap', onTap!);
609 610 611
        break;
      case kSecondaryButton:
        if (onSecondaryTapUp != null)
612
          invokeCallback<void>('onSecondaryTapUp', () => onSecondaryTapUp!(details));
613
        if (onSecondaryTap != null)
614
          invokeCallback<void>('onSecondaryTap', () => onSecondaryTap!());
615
        break;
616 617 618 619
      case kTertiaryButton:
        if (onTertiaryTapUp != null)
          invokeCallback<void>('onTertiaryTapUp', () => onTertiaryTapUp!(details));
        break;
620 621 622 623
      default:
    }
  }

624 625
  @protected
  @override
626
  void handleTapCancel({ required PointerDownEvent down, PointerCancelEvent? cancel, required String reason }) {
627
    final String note = reason == '' ? reason : '$reason ';
628
    switch (down.buttons) {
629 630
      case kPrimaryButton:
        if (onTapCancel != null)
631
          invokeCallback<void>('${note}onTapCancel', onTapCancel!);
632 633 634
        break;
      case kSecondaryButton:
        if (onSecondaryTapCancel != null)
635
          invokeCallback<void>('${note}onSecondaryTapCancel', onSecondaryTapCancel!);
636
        break;
637 638 639 640
      case kTertiaryButton:
        if (onTertiaryTapCancel != null)
          invokeCallback<void>('${note}onTertiaryTapCancel', onTertiaryTapCancel!);
        break;
641
      default:
642 643
    }
  }
644

645
  @override
646
  String get debugDescription => 'tap';
647
}