// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'package:flutter/foundation.dart'; import 'debug.dart'; /// Whether the gesture was accepted or rejected. enum GestureDisposition { /// This gesture was accepted as the interpretation of the user's input. accepted, /// This gesture was rejected as the interpretation of the user's input. rejected, } /// Represents an object participating in an arena. /// /// Receives callbacks from the GestureArena to notify the object when it wins /// or loses a gesture negotiation. Exactly one of [acceptGesture] or /// [rejectGesture] will be called for each arena this member was added to, /// regardless of what caused the arena to be resolved. For example, if a /// member resolves the arena itself, that member still receives an /// [acceptGesture] callback. abstract class GestureArenaMember { /// Called when this member wins the arena for the given pointer id. void acceptGesture(int pointer); /// Called when this member loses the arena for the given pointer id. void rejectGesture(int pointer); } /// An interface to pass information to an arena. /// /// A given [GestureArenaMember] can have multiple entries in multiple arenas /// with different pointer ids. class GestureArenaEntry { GestureArenaEntry._(this._arena, this._pointer, this._member); final GestureArenaManager _arena; final int _pointer; final GestureArenaMember _member; /// Call this member to claim victory (with accepted) or admit defeat (with rejected). /// /// It's fine to attempt to resolve a gesture recognizer for an arena that is /// already resolved. void resolve(GestureDisposition disposition) { _arena._resolve(_pointer, _member, disposition); } } class _GestureArena { final List<GestureArenaMember> members = <GestureArenaMember>[]; bool isOpen = true; bool isHeld = false; bool hasPendingSweep = false; /// If a member attempts to win while the arena is still open, it becomes the /// "eager winner". We look for an eager winner when closing the arena to new /// participants, and if there is one, we resolve the arena in its favor at /// that time. GestureArenaMember? eagerWinner; void add(GestureArenaMember member) { assert(isOpen); members.add(member); } @override String toString() { final StringBuffer buffer = StringBuffer(); if (members.isEmpty) { buffer.write('<empty>'); } else { buffer.write(members.map<String>((GestureArenaMember member) { if (member == eagerWinner) { return '$member (eager winner)'; } return '$member'; }).join(', ')); } if (isOpen) { buffer.write(' [open]'); } if (isHeld) { buffer.write(' [held]'); } if (hasPendingSweep) { buffer.write(' [hasPendingSweep]'); } return buffer.toString(); } } /// Used for disambiguating the meaning of sequences of pointer events. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=Q85LBtBdi0U} /// /// The first member to accept or the last member to not reject wins. /// /// See <https://flutter.dev/gestures/#gesture-disambiguation> for more /// information about the role this class plays in the gesture system. /// /// To debug problems with gestures, consider using /// [debugPrintGestureArenaDiagnostics]. class GestureArenaManager { final Map<int, _GestureArena> _arenas = <int, _GestureArena>{}; /// Adds a new member (e.g., gesture recognizer) to the arena. GestureArenaEntry add(int pointer, GestureArenaMember member) { final _GestureArena state = _arenas.putIfAbsent(pointer, () { assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.')); return _GestureArena(); }); state.add(member); assert(_debugLogDiagnostic(pointer, 'Adding: $member')); return GestureArenaEntry._(this, pointer, member); } /// Prevents new members from entering the arena. /// /// Called after the framework has finished dispatching the pointer down event. void close(int pointer) { final _GestureArena? state = _arenas[pointer]; if (state == null) { return; // This arena either never existed or has been resolved. } state.isOpen = false; assert(_debugLogDiagnostic(pointer, 'Closing', state)); _tryToResolveArena(pointer, state); } /// Forces resolution of the arena, giving the win to the first member. /// /// Sweep is typically after all the other processing for a [PointerUpEvent] /// have taken place. It ensures that multiple passive gestures do not cause a /// stalemate that prevents the user from interacting with the app. /// /// Recognizers that wish to delay resolving an arena past [PointerUpEvent] /// should call [hold] to delay sweep until [release] is called. /// /// See also: /// /// * [hold] /// * [release] void sweep(int pointer) { final _GestureArena? state = _arenas[pointer]; if (state == null) { return; // This arena either never existed or has been resolved. } assert(!state.isOpen); if (state.isHeld) { state.hasPendingSweep = true; assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state)); return; // This arena is being held for a long-lived member. } assert(_debugLogDiagnostic(pointer, 'Sweeping', state)); _arenas.remove(pointer); if (state.members.isNotEmpty) { // First member wins. assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}')); state.members.first.acceptGesture(pointer); // Give all the other members the bad news. for (int i = 1; i < state.members.length; i++) { state.members[i].rejectGesture(pointer); } } } /// Prevents the arena from being swept. /// /// Typically, a winner is chosen in an arena after all the other /// [PointerUpEvent] processing by [sweep]. If a recognizer wishes to delay /// resolving an arena past [PointerUpEvent], the recognizer can [hold] the /// arena open using this function. To release such a hold and let the arena /// resolve, call [release]. /// /// See also: /// /// * [sweep] /// * [release] void hold(int pointer) { final _GestureArena? state = _arenas[pointer]; if (state == null) { return; // This arena either never existed or has been resolved. } state.isHeld = true; assert(_debugLogDiagnostic(pointer, 'Holding', state)); } /// Releases a hold, allowing the arena to be swept. /// /// If a sweep was attempted on a held arena, the sweep will be done /// on release. /// /// See also: /// /// * [sweep] /// * [hold] void release(int pointer) { final _GestureArena? state = _arenas[pointer]; if (state == null) { return; // This arena either never existed or has been resolved. } state.isHeld = false; assert(_debugLogDiagnostic(pointer, 'Releasing', state)); if (state.hasPendingSweep) { sweep(pointer); } } /// Reject or accept a gesture recognizer. /// /// This is called by calling [GestureArenaEntry.resolve] on the object returned from [add]. void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) { final _GestureArena? state = _arenas[pointer]; if (state == null) { return; // This arena has already resolved. } assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member')); assert(state.members.contains(member)); if (disposition == GestureDisposition.rejected) { state.members.remove(member); member.rejectGesture(pointer); if (!state.isOpen) { _tryToResolveArena(pointer, state); } } else { assert(disposition == GestureDisposition.accepted); if (state.isOpen) { state.eagerWinner ??= member; } else { assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member')); _resolveInFavorOf(pointer, state, member); } } } void _tryToResolveArena(int pointer, _GestureArena state) { assert(_arenas[pointer] == state); assert(!state.isOpen); if (state.members.length == 1) { scheduleMicrotask(() => _resolveByDefault(pointer, state)); } else if (state.members.isEmpty) { _arenas.remove(pointer); assert(_debugLogDiagnostic(pointer, 'Arena empty.')); } else if (state.eagerWinner != null) { assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}')); _resolveInFavorOf(pointer, state, state.eagerWinner!); } } void _resolveByDefault(int pointer, _GestureArena state) { if (!_arenas.containsKey(pointer)) { return; // This arena has already resolved. } assert(_arenas[pointer] == state); assert(!state.isOpen); final List<GestureArenaMember> members = state.members; assert(members.length == 1); _arenas.remove(pointer); assert(_debugLogDiagnostic(pointer, 'Default winner: ${state.members.first}')); state.members.first.acceptGesture(pointer); } void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) { assert(state == _arenas[pointer]); assert(state.eagerWinner == null || state.eagerWinner == member); assert(!state.isOpen); _arenas.remove(pointer); for (final GestureArenaMember rejectedMember in state.members) { if (rejectedMember != member) { rejectedMember.rejectGesture(pointer); } } member.acceptGesture(pointer); } bool _debugLogDiagnostic(int pointer, String message, [ _GestureArena? state ]) { assert(() { if (debugPrintGestureArenaDiagnostics) { final int? count = state?.members.length; final String s = count != 1 ? 's' : ''; debugPrint('Gesture arena ${pointer.toString().padRight(4)} ❙ $message${ count != null ? " with $count member$s." : ""}'); } return true; }()); return true; } }