Browse Source

fix some selection handling issues

lucky1213 4 years ago
parent
commit
641a74ae78

+ 1
- 1
packages/zefyr/lib/src/widgets/editable_text.dart View File

323
     _input.openOrCloseConnection(_focusNode,
323
     _input.openOrCloseConnection(_focusNode,
324
         widget.controller.plainTextEditingValue, widget.keyboardAppearance, showKeyboard: showKeyboard);
324
         widget.controller.plainTextEditingValue, widget.keyboardAppearance, showKeyboard: showKeyboard);
325
     _cursorTimer.startOrStop(_focusNode, selection);
325
     _cursorTimer.startOrStop(_focusNode, selection);
326
-    // updateKeepAlive();
326
+    updateKeepAlive();
327
   }
327
   }
328
 
328
 
329
   void _handleRemoteValueChange(
329
   void _handleRemoteValueChange(

+ 2
- 0
packages/zefyr/lib/src/widgets/field.dart View File

65
       mode: _effectiveMode,
65
       mode: _effectiveMode,
66
       toolbarDelegate: widget.toolbarDelegate,
66
       toolbarDelegate: widget.toolbarDelegate,
67
       imageDelegate: widget.imageDelegate,
67
       imageDelegate: widget.imageDelegate,
68
+      linkDelegate: widget.linkDelegate,
68
       physics: widget.physics,
69
       physics: widget.physics,
69
       keyboardAppearance: widget.keyboardAppearance,
70
       keyboardAppearance: widget.keyboardAppearance,
71
+      onSave: widget.onSave,
70
     );
72
     );
71
 
73
 
72
     if (widget.height != null) {
74
     if (widget.height != null) {

+ 8
- 5
packages/zefyr/lib/src/widgets/image.dart View File

13
 import 'package:zefyr/zefyr.dart';
13
 import 'package:zefyr/zefyr.dart';
14
 
14
 
15
 import 'editable_box.dart';
15
 import 'editable_box.dart';
16
+import 'rich_text.dart';
16
 
17
 
17
 /// Provides interface for embedding images into Zefyr editor.
18
 /// Provides interface for embedding images into Zefyr editor.
18
 // TODO: allow configuring image sources and related toolbar buttons.
19
 // TODO: allow configuring image sources and related toolbar buttons.
129
 
130
 
130
     int nodeBase = node.documentOffset;
131
     int nodeBase = node.documentOffset;
131
     int nodeExtent = nodeBase + node.length;
132
     int nodeExtent = nodeBase + node.length;
132
-    int base = math.max(0, documentSelection.baseOffset - nodeBase);
133
-    int extent =
134
-        math.min(documentSelection.extentOffset, nodeExtent) - nodeBase;
135
-    return documentSelection.copyWith(baseOffset: base, extentOffset: extent);
133
+    // int base = math.max(0, documentSelection.baseOffset - nodeBase);
134
+    // int extent =
135
+    //     math.min(documentSelection.extentOffset, nodeExtent) - nodeBase;
136
+    // return documentSelection.copyWith(baseOffset: base, extentOffset: extent);
137
+    return getSelectionRebase(nodeBase, nodeExtent, documentSelection);
136
   }
138
   }
137
 
139
 
138
   @override
140
   @override
174
   bool intersectsWithSelection(TextSelection selection) {
176
   bool intersectsWithSelection(TextSelection selection) {
175
     final int base = node.documentOffset;
177
     final int base = node.documentOffset;
176
     final int extent = base + node.length;
178
     final int extent = base + node.length;
177
-    return base <= selection.extentOffset && selection.baseOffset <= extent;
179
+    // return base <= selection.extentOffset && selection.baseOffset <= extent;
180
+    return selectionIntersectsWith(base, extent, selection);
178
   }
181
   }
179
 
182
 
180
   @override
183
   @override

+ 82
- 39
packages/zefyr/lib/src/widgets/rich_text.dart View File

11
 import 'caret.dart';
11
 import 'caret.dart';
12
 import 'editable_box.dart';
12
 import 'editable_box.dart';
13
 
13
 
14
+bool selectionIntersectsWith(int base, int extent, TextSelection selection) {
15
+  return base <= selection.end && selection.start <= extent;
16
+}
17
+
18
+// return a point between base and extent no matter what!
19
+int selectionPointRestrict(int base, int extent, int point) {
20
+  if (point < base) return base;
21
+  if (point > extent) return extent;
22
+  return point;
23
+}
24
+
25
+TextSelection getSelectionRebase(
26
+    int base, int extent, TextSelection selection) {
27
+  if (!selectionIntersectsWith(base, extent, selection)) {
28
+    return null;
29
+  }
30
+
31
+  int newBase =
32
+      selectionPointRestrict(base, extent, selection.baseOffset) - base;
33
+  int newExtent =
34
+      selectionPointRestrict(base, extent, selection.extentOffset) - base;
35
+
36
+  return selection.copyWith(baseOffset: newBase, extentOffset: newExtent);
37
+}
38
+
14
 /// Represents single paragraph of Zefyr rich-text.
39
 /// Represents single paragraph of Zefyr rich-text.
15
 class ZefyrRichText extends MultiChildRenderObjectWidget {
40
 class ZefyrRichText extends MultiChildRenderObjectWidget {
16
   ZefyrRichText({
41
   ZefyrRichText({
92
 
117
 
93
   @override
118
   @override
94
   TextSelection getLocalSelection(TextSelection documentSelection) {
119
   TextSelection getLocalSelection(TextSelection documentSelection) {
95
-    if (!intersectsWithSelection(documentSelection)) return null;
120
+    if (!intersectsWithSelection(documentSelection)) {
121
+      return null;
122
+    }
96
 
123
 
97
     int nodeBase = node.documentOffset;
124
     int nodeBase = node.documentOffset;
98
     int nodeExtent = nodeBase + node.length;
125
     int nodeExtent = nodeBase + node.length;
99
-    int base = math.max(0, documentSelection.baseOffset - nodeBase);
100
-    int extent =
101
-        math.min(documentSelection.extentOffset, nodeExtent) - nodeBase;
102
-    return documentSelection.copyWith(baseOffset: base, extentOffset: extent);
126
+    // int base = math.max(0, documentSelection.baseOffset - nodeBase);
127
+    // int extent =
128
+    //     math.min(documentSelection.extentOffset, nodeExtent) - nodeBase;
129
+    // return documentSelection.copyWith(baseOffset: base, extentOffset: extent);
130
+    return getSelectionRebase(nodeBase, nodeExtent, documentSelection);
103
   }
131
   }
104
 
132
 
105
   @override
133
   @override
167
     return super.getOffsetForCaret(localPosition, caretPrototype);
195
     return super.getOffsetForCaret(localPosition, caretPrototype);
168
   }
196
   }
169
 
197
 
198
+  // the trailing \n is not handled by the span, drop it from the sel.
199
+  // otherwise getBoxesForSelection fails on the web. (out of range)
200
+  TextSelection trimSelection(TextSelection selection) {
201
+    if (selection.baseOffset > node.length - 1) {
202
+      selection = selection.copyWith(baseOffset: node.length - 1);
203
+    }
204
+    if (selection.extentOffset > node.length - 1) {
205
+      selection = selection.copyWith(extentOffset: node.length - 1);
206
+    }
207
+    return selection;
208
+  }
209
+
170
   // This method works around some issues in getBoxesForSelection and handles
210
   // This method works around some issues in getBoxesForSelection and handles
171
   // edge-case with our TextSpan objects not having last line-break character.
211
   // edge-case with our TextSpan objects not having last line-break character.
172
   @override
212
   @override
186
       ];
226
       ];
187
     }
227
     }
188
 
228
 
189
-    int isBaseShifted = 0;
190
-    bool isExtentShifted = false;
191
-    if (local.baseOffset == node.length - 1 && local.baseOffset > 0) {
192
-      // Since we exclude last line-break from rendered TextSpan we have to
193
-      // handle end-of-line selection explicitly.
194
-      local = local.copyWith(baseOffset: local.baseOffset - 1);
195
-      isBaseShifted = -1;
196
-    } else if (local.baseOffset == 0 && local.isCollapsed) {
197
-      // This takes care of beginning of line position.
198
-      local = local.copyWith(baseOffset: local.baseOffset + 1);
199
-      isBaseShifted = 1;
200
-    }
201
-    if (text.codeUnitAt(local.extentOffset - 1) == 0xA) {
202
-      // This takes care of the rest end-of-line scenarios, where there are
203
-      // actually line-breaks in the TextSpan (e.g. in code blocks).
204
-      local = local.copyWith(extentOffset: local.extentOffset + 1);
205
-      isExtentShifted = true;
206
-    }
207
-    final result = getBoxesForSelection(local).toList();
208
-    if (isBaseShifted != 0) {
209
-      final box = result.first;
210
-      final dx = isBaseShifted == -1 ? box.right : box.left;
211
-      result.removeAt(0);
212
-      result.insert(
213
-          0, ui.TextBox.fromLTRBD(dx, box.top, dx, box.bottom, box.direction));
214
-    }
215
-    if (isExtentShifted) {
216
-      final box = result.last;
217
-      result.removeLast();
218
-      result.add(ui.TextBox.fromLTRBD(
219
-          box.left, box.top, box.left, box.bottom, box.direction));
220
-    }
229
+    // int isBaseShifted = 0;
230
+    // bool isExtentShifted = false;
231
+    // if (local.baseOffset == node.length - 1 && local.baseOffset > 0) {
232
+    //   // Since we exclude last line-break from rendered TextSpan we have to
233
+    //   // handle end-of-line selection explicitly.
234
+    //   local = local.copyWith(baseOffset: local.baseOffset - 1);
235
+    //   isBaseShifted = -1;
236
+    // } else if (local.baseOffset == 0 && local.isCollapsed) {
237
+    //   // This takes care of beginning of line position.
238
+    //   local = local.copyWith(baseOffset: local.baseOffset + 1);
239
+    //   isBaseShifted = 1;
240
+    // }
241
+    // if (text.codeUnitAt(local.extentOffset - 1) == 0xA) {
242
+    //   // This takes care of the rest end-of-line scenarios, where there are
243
+    //   // actually line-breaks in the TextSpan (e.g. in code blocks).
244
+    //   local = local.copyWith(extentOffset: local.extentOffset + 1);
245
+    //   isExtentShifted = true;
246
+    // }
247
+    final result = getBoxesForSelection(trimSelection(local)).toList();
248
+    // if (isBaseShifted != 0) {
249
+    //   final box = result.first;
250
+    //   final dx = isBaseShifted == -1 ? box.right : box.left;
251
+    //   result.removeAt(0);
252
+    //   result.insert(
253
+    //       0, ui.TextBox.fromLTRBD(dx, box.top, dx, box.bottom, box.direction));
254
+    // }
255
+    // if (isExtentShifted) {
256
+    //   final box = result.last;
257
+    //   result.removeLast();
258
+    //   result.add(ui.TextBox.fromLTRBD(
259
+    //       box.left, box.top, box.left, box.bottom, box.direction));
260
+    // }
221
     return result;
261
     return result;
222
   }
262
   }
223
 
263
 
252
   bool intersectsWithSelection(TextSelection selection) {
292
   bool intersectsWithSelection(TextSelection selection) {
253
     final int base = node.documentOffset;
293
     final int base = node.documentOffset;
254
     final int extent = base + node.length;
294
     final int extent = base + node.length;
255
-    return base <= selection.extentOffset && selection.baseOffset <= extent;
295
+    // return base <= selection.extentOffset && selection.baseOffset <= extent;
296
+    return selectionIntersectsWith(base, extent, selection);
256
   }
297
   }
257
 
298
 
258
   TextSelection _lastPaintedSelection;
299
   TextSelection _lastPaintedSelection;
262
     if (_lastPaintedSelection != selection) {
303
     if (_lastPaintedSelection != selection) {
263
       _selectionRects = null;
304
       _selectionRects = null;
264
     }
305
     }
265
-    _selectionRects ??= getBoxesForSelection(getLocalSelection(selection));
306
+    var localSel = getLocalSelection(selection);
307
+
308
+    _selectionRects ??= getBoxesForSelection(trimSelection(localSel));
266
     final Paint paint = Paint()..color = selectionColor;
309
     final Paint paint = Paint()..color = selectionColor;
267
     for (ui.TextBox box in _selectionRects) {
310
     for (ui.TextBox box in _selectionRects) {
268
       context.canvas.drawRect(box.toRect().shift(offset), paint);
311
       context.canvas.drawRect(box.toRect().shift(offset), paint);

+ 11
- 1
packages/zefyr/lib/src/widgets/selection.dart View File

4
 import 'dart:math' as math;
4
 import 'dart:math' as math;
5
 import 'dart:ui' as ui;
5
 import 'dart:ui' as ui;
6
 
6
 
7
+import 'package:flutter/foundation.dart';
7
 import 'package:flutter/gestures.dart';
8
 import 'package:flutter/gestures.dart';
8
 import 'package:flutter/material.dart';
9
 import 'package:flutter/material.dart';
9
 import 'package:flutter/rendering.dart';
10
 import 'package:flutter/rendering.dart';
69
   }
70
   }
70
 
71
 
71
   void showToolbar() {
72
   void showToolbar() {
73
+    if (kIsWeb) return;
72
     final toolbarOpacity = _toolbarController.view;
74
     final toolbarOpacity = _toolbarController.view;
73
     _toolbar = OverlayEntry(
75
     _toolbar = OverlayEntry(
74
       builder: (context) => FadeTransition(
76
       builder: (context) => FadeTransition(
454
     Offset point;
456
     Offset point;
455
     TextSelectionHandleType type;
457
     TextSelectionHandleType type;
456
 
458
 
457
-    switch (widget.position) {
459
+    // we invert base / extend if the selection is from bottom to top
460
+    var pos = widget.position;
461
+    if (selection.baseOffset > selection.extentOffset) {
462
+      pos = pos == _SelectionHandlePosition.base
463
+          ? _SelectionHandlePosition.extent
464
+          : _SelectionHandlePosition.base;
465
+    }
466
+
467
+    switch (pos) {
458
       case _SelectionHandlePosition.base:
468
       case _SelectionHandlePosition.base:
459
         point = endpoints[0].point;
469
         point = endpoints[0].point;
460
         type = _chooseType(endpoints[0], TextSelectionHandleType.left,
470
         type = _chooseType(endpoints[0], TextSelectionHandleType.left,