overlay.dart
10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../../ast/syntax_tree.dart';
import 'handle_overlay.dart';
import 'overlay_manager.dart';
import 'selection_manager.dart';
enum MathSelectionHandlePosition { start, end }
class MathSelectionOverlay {
MathSelectionOverlay({
this.debugRequiredFor,
required this.toolbarLayerLink,
required this.startHandleLayerLink,
required this.endHandleLayerLink,
this.selectionControls,
bool handlesVisible = false,
required this.manager,
this.dragStartBehavior = DragStartBehavior.start,
this.onSelectionHandleTapped,
this.clipboardStatus,
}) : _handlesVisible = handlesVisible {
final overlay = Overlay.of(context, rootOverlay: true);
_toolbarController =
AnimationController(duration: fadeDuration, vsync: overlay);
}
/// The context in which the selection handles should appear.
///
/// This context must have an [Overlay] as an ancestor because this object
/// will display the text selection handles in that [Overlay].
BuildContext get context => manager.context;
/// Debugging information for explaining why the [Overlay] is required.
final Widget? debugRequiredFor;
/// The object supplied to the [CompositedTransformTarget] that wraps the text
/// field.
final LayerLink toolbarLayerLink;
/// The objects supplied to the [CompositedTransformTarget] that wraps the
/// location of start selection handle.
final LayerLink startHandleLayerLink;
/// The objects supplied to the [CompositedTransformTarget] that wraps the
/// location of end selection handle.
final LayerLink endHandleLayerLink;
/// Builds text selection handles and toolbar.
final TextSelectionControls? selectionControls;
/// The delegate for manipulating the current selection in the owning
/// text field.
final SelectionOverlayManagerMixin manager;
/// Determines the way that drag start behavior is handled.
///
/// If set to [DragStartBehavior.start], handle drag behavior will
/// begin upon the detection of a drag gesture. If set to
/// [DragStartBehavior.down] it will begin when a down event is first
/// detected.
///
/// In general, setting this to [DragStartBehavior.start] will make drag
/// animation smoother and setting it to [DragStartBehavior.down] will make
/// drag behavior feel slightly more reactive.
///
/// By default, the drag start behavior is [DragStartBehavior.start].
///
/// See also:
///
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for
/// the different behaviors.
final DragStartBehavior dragStartBehavior;
/// A callback that's invoked when a selection handle is tapped.
///
/// Both regular taps and long presses invoke this callback, but a drag
/// gesture won't.
final VoidCallback? onSelectionHandleTapped;
/// Maintains the status of the clipboard for determining if its contents can
/// be pasted or not.
///
/// Useful because the actual value of the clipboard can only be checked
/// asynchronously (see [Clipboard.getData]).
final ClipboardStatusNotifier? clipboardStatus;
/// Controls the fade-in and fade-out animations for the toolbar and handles.
static const Duration fadeDuration = Duration(milliseconds: 150);
late AnimationController _toolbarController;
Animation<double> get _toolbarOpacity => _toolbarController.view;
/// Retrieve current value.
@visibleForTesting
SyntaxTree? get value => _value;
SyntaxTree? _value;
/// A pair of handles. If this is non-null, there are always 2, though the
/// second is hidden when the selection is collapsed.
List<OverlayEntry>? _handles;
/// A copy/paste toolbar.
OverlayEntry? _toolbar;
TextSelection get _selection => manager.controller.selection;
/// Whether selection handles are visible.
///
/// Set to false if you want to hide the handles. Use this property to show or
/// hide the handle without rebuilding them.
///
/// If this method is called while the [SchedulerBinding.schedulerPhase] is
/// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
/// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
/// until the post-frame callbacks phase. Otherwise the update is done
/// synchronously. This means that it is safe to call during builds, but also
/// that if you do call this during a build, the UI will not update until the
/// next frame (i.e. many milliseconds later).
///
/// Defaults to false.
bool get handlesVisible => _handlesVisible;
bool _handlesVisible;
set handlesVisible(bool visible) {
if (_handlesVisible == visible) return;
_handlesVisible = visible;
// If we are in build state, it will be too late to update visibility.
// We will need to schedule the build in next frame.
if (SchedulerBinding.instance.schedulerPhase ==
SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
} else {
_markNeedsBuild();
}
}
/// Builds the handles by inserting them into the [context]'s overlay.
void showHandles() {
assert(_handles == null);
_handles = <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) =>
_buildHandle(context, MathSelectionHandlePosition.start)),
OverlayEntry(
builder: (BuildContext context) =>
_buildHandle(context, MathSelectionHandlePosition.end)),
];
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)
.insertAll(_handles!);
}
/// Destroys the handles by removing them from overlay.
void hideHandles() {
if (_handles != null) {
_handles![0].remove();
_handles![1].remove();
_handles = null;
}
}
/// Shows the toolbar by inserting it into the [context]'s overlay.
void showToolbar() {
assert(_toolbar == null);
_toolbar = OverlayEntry(builder: _buildToolbar);
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)
.insert(_toolbar!);
_toolbarController.forward(from: 0.0);
}
/// Updates the overlay after the selection has changed.
///
/// If this method is called while the [SchedulerBinding.schedulerPhase] is
/// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
/// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
/// until the post-frame callbacks phase. Otherwise the update is done
/// synchronously. This means that it is safe to call during builds, but also
/// that if you do call this during a build, the UI will not update until the
/// next frame (i.e. many milliseconds later).
void update() {
if (SchedulerBinding.instance.schedulerPhase ==
SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
} else {
_markNeedsBuild();
}
}
void _markNeedsBuild([Duration? duration]) {
if (_handles != null) {
_handles![0].markNeedsBuild();
_handles![1].markNeedsBuild();
}
_toolbar?.markNeedsBuild();
}
/// Whether the handles are currently visible.
bool get handlesAreVisible => _handles != null && handlesVisible;
/// Whether the toolbar is currently visible.
bool get toolbarIsVisible => _toolbar != null;
/// Hides the entire overlay including the toolbar and the handles.
void hide() {
if (_handles != null) {
_handles![0].remove();
_handles![1].remove();
_handles = null;
}
if (_toolbar != null) {
hideToolbar();
}
}
/// Hides the toolbar part of the overlay.
///
/// To hide the whole overlay, see [hide].
void hideToolbar() {
assert(_toolbar != null);
_toolbarController.stop();
_toolbar!.remove();
_toolbar = null;
}
/// Final cleanup.
void dispose() {
hide();
_toolbarController.dispose();
}
Widget _buildHandle(
BuildContext context, MathSelectionHandlePosition position) {
if ((_selection.isCollapsed &&
position == MathSelectionHandlePosition.end) ||
selectionControls == null) {
return Container();
} // hide the second handle when collapsed
return Visibility(
visible: handlesVisible,
child: MathSelectionHandleOverlay(
manager: manager,
onSelectionHandleChanged: (TextSelection newSelection) {
_handleSelectionHandleChanged(newSelection, position);
},
onSelectionHandleTapped: onSelectionHandleTapped,
startHandleLayerLink: startHandleLayerLink,
endHandleLayerLink: endHandleLayerLink,
selection: _selection,
selectionControls: selectionControls!,
position: position,
dragStartBehavior: dragStartBehavior,
),
);
}
Widget _buildToolbar(BuildContext context) {
if (selectionControls == null) return Container();
// Find the horizontal midpoint, just above the selected text.
final endpoint1 = manager.getLocalEndpointForPosition(_selection.start);
final endpoint2 = manager.getLocalEndpointForPosition(_selection.end);
final editingRegion = manager.getLocalEditingRegion();
final isMultiline = false; // TODO
// endpoints.last.point.dy - endpoints.first.point.dy >
// manager.preferredLineHeight / 2;
// If the selected text spans more than 1 line, horizontally center the
// toolbar.
// Derived from both iOS and Android.
final midX = isMultiline
? editingRegion.width / 2
: (endpoint1.dx + endpoint2.dx) / 2;
final midpoint = Offset(
midX,
// The y-coordinate won't be made use of most likely.
endpoint1.dy - manager.preferredLineHeight,
);
return FadeTransition(
opacity: _toolbarOpacity,
child: CompositedTransformFollower(
link: toolbarLayerLink,
showWhenUnlinked: false,
offset: -editingRegion.topLeft,
child: selectionControls!.buildToolbar(
context,
editingRegion,
manager.preferredLineHeight,
midpoint,
[
TextSelectionPoint(endpoint1, TextDirection.ltr),
TextSelectionPoint(endpoint2, TextDirection.ltr),
],
manager,
clipboardStatus!,
null,
),
),
);
}
void _handleSelectionHandleChanged(
TextSelection newSelection, MathSelectionHandlePosition position) {
TextPosition textPosition;
switch (position) {
case MathSelectionHandlePosition.start:
textPosition = newSelection.base;
break;
case MathSelectionHandlePosition.end:
textPosition = newSelection.extent;
break;
}
manager.handleSelectionChanged(
newSelection, null, ExtraSelectionChangedCause.handle);
manager.bringIntoView(textPosition);
}
}