arena.dart 9.66 KB
Newer Older
Adam Barth's avatar
Adam Barth committed
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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 8 9 10
import 'package:flutter/foundation.dart';

import 'debug.dart';

11
/// Whether the gesture was accepted or rejected.
Adam Barth's avatar
Adam Barth committed
12
enum GestureDisposition {
13
  /// This gesture was accepted as the interpretation of the user's input.
Adam Barth's avatar
Adam Barth committed
14
  accepted,
15 16 17

  /// This gesture was rejected as the interpretation  of the user's input.
  rejected,
Adam Barth's avatar
Adam Barth committed
18 19
}

20 21 22 23
/// 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
24
/// [rejectGesture] will be called for each arena this member was added to,
25 26 27
/// 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.
Adam Barth's avatar
Adam Barth committed
28
abstract class GestureArenaMember {
29 30
  /// Called when this member wins the arena for the given pointer id.
  void acceptGesture(int pointer);
Adam Barth's avatar
Adam Barth committed
31

32 33
  /// Called when this member loses the arena for the given pointer id.
  void rejectGesture(int pointer);
Adam Barth's avatar
Adam Barth committed
34 35
}

Florian Loitsch's avatar
Florian Loitsch committed
36
/// An interface to information to an arena.
37 38
///
/// A given [GestureArenaMember] can have multiple entries in multiple arenas
39
/// with different pointer ids.
Adam Barth's avatar
Adam Barth committed
40
class GestureArenaEntry {
41
  GestureArenaEntry._(this._arena, this._pointer, this._member);
Adam Barth's avatar
Adam Barth committed
42

43 44
  final GestureArenaManager _arena;
  final int _pointer;
Adam Barth's avatar
Adam Barth committed
45 46 47
  final GestureArenaMember _member;

  /// Call this member to claim victory (with accepted) or admit defeat (with rejected).
48
  ///
49 50
  /// It's fine to attempt to resolve a gesture recognizer for an arena that is
  /// already resolved.
Adam Barth's avatar
Adam Barth committed
51
  void resolve(GestureDisposition disposition) {
52
    _arena._resolve(_pointer, _member, disposition);
Adam Barth's avatar
Adam Barth committed
53 54 55
  }
}

56
class _GestureArena {
57
  final List<GestureArenaMember> members = <GestureArenaMember>[];
58
  bool isOpen = true;
59
  bool isHeld = false;
60
  bool hasPendingSweep = false;
61

Hixie's avatar
Hixie committed
62
  /// If a gesture attempts to win while the arena is still open, it becomes the
63
  /// "eager winner". We look for an eager winner when closing the arena to new
64
  /// participants, and if there is one, we resolve the arena in its favor at
Hixie's avatar
Hixie committed
65 66 67
  /// that time.
  GestureArenaMember eagerWinner;

68 69 70 71
  void add(GestureArenaMember member) {
    assert(isOpen);
    members.add(member);
  }
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92

  @override
  String toString() {
    final StringBuffer buffer = new 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();
  }
93 94
}

Adam Barth's avatar
Adam Barth committed
95
/// The first member to accept or the last member to not to reject wins.
96 97 98
///
/// See [https://flutter.io/gestures/#gesture-disambiguation] for more
/// information about the role this class plays in the gesture system.
99 100 101
///
/// To debug problems with gestures, consider using
/// [debugPrintGestureArenaDiagnostics].
102
class GestureArenaManager {
103
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
Adam Barth's avatar
Adam Barth committed
104

105
  /// Adds a new member (e.g., gesture recognizer) to the arena.
106
  GestureArenaEntry add(int pointer, GestureArenaMember member) {
107 108 109 110
    final _GestureArena state = _arenas.putIfAbsent(pointer, () {
      assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
      return new _GestureArena();
    });
111
    state.add(member);
112
    assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
113
    return new GestureArenaEntry._(this, pointer, member);
Adam Barth's avatar
Adam Barth committed
114 115
  }

116 117 118
  /// Prevents new members from entering the arena.
  ///
  /// Called after the framework has finished dispatching the pointer down event.
119
  void close(int pointer) {
120
    final _GestureArena state = _arenas[pointer];
121 122 123
    if (state == null)
      return;  // This arena either never existed or has been resolved.
    state.isOpen = false;
124
    assert(_debugLogDiagnostic(pointer, 'Closing', state));
125
    _tryToResolveArena(pointer, state);
126 127
  }

128
  /// Forces resolution of the arena, giving the win to the first member.
129 130 131 132 133 134 135 136 137 138 139 140
  ///
  /// 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]
141
  void sweep(int pointer) {
142
    final _GestureArena state = _arenas[pointer];
143 144 145
    if (state == null)
      return;  // This arena either never existed or has been resolved.
    assert(!state.isOpen);
146 147
    if (state.isHeld) {
      state.hasPendingSweep = true;
148 149
      assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
      return;  // This arena is being held for a long-lived member.
150
    }
151
    assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
152
    _arenas.remove(pointer);
Ian Hickson's avatar
Ian Hickson committed
153
    if (state.members.isNotEmpty) {
154 155
      // First member wins.
      assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
156
      state.members.first.acceptGesture(pointer);
157
      // Give all the other members the bad news.
158
      for (int i = 1; i < state.members.length; i++)
159
        state.members[i].rejectGesture(pointer);
160 161 162
    }
  }

Florian Loitsch's avatar
Florian Loitsch committed
163
  /// Prevents the arena from being swept.
164 165 166 167 168 169 170 171 172 173 174
  ///
  /// 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]
175
  void hold(int pointer) {
176
    final _GestureArena state = _arenas[pointer];
177 178 179
    if (state == null)
      return;  // This arena either never existed or has been resolved.
    state.isHeld = true;
180
    assert(_debugLogDiagnostic(pointer, 'Holding', state));
181 182
  }

Florian Loitsch's avatar
Florian Loitsch committed
183 184
  /// Releases a hold, allowing the arena to be swept.
  ///
185
  /// If a sweep was attempted on a held arena, the sweep will be done
Florian Loitsch's avatar
Florian Loitsch committed
186
  /// on release.
187 188 189 190 191
  ///
  /// See also:
  ///
  ///  * [sweep]
  ///  * [hold]
192
  void release(int pointer) {
193
    final _GestureArena state = _arenas[pointer];
194 195 196
    if (state == null)
      return;  // This arena either never existed or has been resolved.
    state.isHeld = false;
197
    assert(_debugLogDiagnostic(pointer, 'Releasing', state));
198
    if (state.hasPendingSweep)
199
      sweep(pointer);
200 201
  }

202 203 204
  /// Reject or accept a gesture recognizer.
  ///
  /// This is called by calling [GestureArenaEntry.resolve] on the object returned from [add].
205
  void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
206
    final _GestureArena state = _arenas[pointer];
207
    if (state == null)
208
      return;  // This arena has already resolved.
209
    assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member'));
210
    assert(state.members.contains(member));
Adam Barth's avatar
Adam Barth committed
211
    if (disposition == GestureDisposition.rejected) {
212
      state.members.remove(member);
213
      member.rejectGesture(pointer);
Hixie's avatar
Hixie committed
214
      if (!state.isOpen)
215
        _tryToResolveArena(pointer, state);
Adam Barth's avatar
Adam Barth committed
216 217
    } else {
      assert(disposition == GestureDisposition.accepted);
Hixie's avatar
Hixie committed
218
      if (state.isOpen) {
Ian Hickson's avatar
Ian Hickson committed
219
        state.eagerWinner ??= member;
Hixie's avatar
Hixie committed
220
      } else {
221
        assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
222
        _resolveInFavorOf(pointer, state, member);
Adam Barth's avatar
Adam Barth committed
223 224 225
      }
    }
  }
Hixie's avatar
Hixie committed
226

227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
  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;  // Already resolved earlier.
    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);
  }

253 254
  void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
    assert(state == _arenas[pointer]);
Hixie's avatar
Hixie committed
255 256 257
    assert(state != null);
    assert(state.eagerWinner == null || state.eagerWinner == member);
    assert(!state.isOpen);
258
    _arenas.remove(pointer);
Hixie's avatar
Hixie committed
259 260
    for (GestureArenaMember rejectedMember in state.members) {
      if (rejectedMember != member)
261
        rejectedMember.rejectGesture(pointer);
Hixie's avatar
Hixie committed
262
    }
263
    member.acceptGesture(pointer);
Hixie's avatar
Hixie committed
264
  }
265 266 267 268 269 270 271 272 273

  bool _debugLogDiagnostic(int pointer, String message, [ _GestureArena state ]) {
    assert(() {
      if (debugPrintGestureArenaDiagnostics) {
        final int count = state != null ? state.members.length : null;
        final String s = count != 1 ? 's' : '';
        debugPrint('Gesture arena ${pointer.toString().padRight(4)}$message${ count != null ? " with $count member$s." : ""}');
      }
      return true;
274
    }());
275 276
    return true;
  }
Ian Hickson's avatar
Ian Hickson committed
277
}