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,7 +323,7 @@ class ZefyrEditableTextState extends State<ZefyrEditableText>
323 323
     _input.openOrCloseConnection(_focusNode,
324 324
         widget.controller.plainTextEditingValue, widget.keyboardAppearance, showKeyboard: showKeyboard);
325 325
     _cursorTimer.startOrStop(_focusNode, selection);
326
-    // updateKeepAlive();
326
+    updateKeepAlive();
327 327
   }
328 328
 
329 329
   void _handleRemoteValueChange(

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

@@ -65,8 +65,10 @@ class _ZefyrFieldState extends State<ZefyrField> {
65 65
       mode: _effectiveMode,
66 66
       toolbarDelegate: widget.toolbarDelegate,
67 67
       imageDelegate: widget.imageDelegate,
68
+      linkDelegate: widget.linkDelegate,
68 69
       physics: widget.physics,
69 70
       keyboardAppearance: widget.keyboardAppearance,
71
+      onSave: widget.onSave,
70 72
     );
71 73
 
72 74
     if (widget.height != null) {

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

@@ -13,6 +13,7 @@ import 'package:notus/notus.dart';
13 13
 import 'package:zefyr/zefyr.dart';
14 14
 
15 15
 import 'editable_box.dart';
16
+import 'rich_text.dart';
16 17
 
17 18
 /// Provides interface for embedding images into Zefyr editor.
18 19
 // TODO: allow configuring image sources and related toolbar buttons.
@@ -129,10 +130,11 @@ class RenderEditableImage extends RenderBox
129 130
 
130 131
     int nodeBase = node.documentOffset;
131 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 140
   @override
@@ -174,7 +176,8 @@ class RenderEditableImage extends RenderBox
174 176
   bool intersectsWithSelection(TextSelection selection) {
175 177
     final int base = node.documentOffset;
176 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 183
   @override

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

@@ -11,6 +11,31 @@ import 'package:notus/notus.dart';
11 11
 import 'caret.dart';
12 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 39
 /// Represents single paragraph of Zefyr rich-text.
15 40
 class ZefyrRichText extends MultiChildRenderObjectWidget {
16 41
   ZefyrRichText({
@@ -92,14 +117,17 @@ class RenderZefyrParagraph extends RenderParagraph
92 117
 
93 118
   @override
94 119
   TextSelection getLocalSelection(TextSelection documentSelection) {
95
-    if (!intersectsWithSelection(documentSelection)) return null;
120
+    if (!intersectsWithSelection(documentSelection)) {
121
+      return null;
122
+    }
96 123
 
97 124
     int nodeBase = node.documentOffset;
98 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 133
   @override
@@ -167,6 +195,18 @@ class RenderZefyrParagraph extends RenderParagraph
167 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 210
   // This method works around some issues in getBoxesForSelection and handles
171 211
   // edge-case with our TextSpan objects not having last line-break character.
172 212
   @override
@@ -186,38 +226,38 @@ class RenderZefyrParagraph extends RenderParagraph
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 261
     return result;
222 262
   }
223 263
 
@@ -252,7 +292,8 @@ class RenderZefyrParagraph extends RenderParagraph
252 292
   bool intersectsWithSelection(TextSelection selection) {
253 293
     final int base = node.documentOffset;
254 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 299
   TextSelection _lastPaintedSelection;
@@ -262,7 +303,9 @@ class RenderZefyrParagraph extends RenderParagraph
262 303
     if (_lastPaintedSelection != selection) {
263 304
       _selectionRects = null;
264 305
     }
265
-    _selectionRects ??= getBoxesForSelection(getLocalSelection(selection));
306
+    var localSel = getLocalSelection(selection);
307
+
308
+    _selectionRects ??= getBoxesForSelection(trimSelection(localSel));
266 309
     final Paint paint = Paint()..color = selectionColor;
267 310
     for (ui.TextBox box in _selectionRects) {
268 311
       context.canvas.drawRect(box.toRect().shift(offset), paint);

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

@@ -4,6 +4,7 @@
4 4
 import 'dart:math' as math;
5 5
 import 'dart:ui' as ui;
6 6
 
7
+import 'package:flutter/foundation.dart';
7 8
 import 'package:flutter/gestures.dart';
8 9
 import 'package:flutter/material.dart';
9 10
 import 'package:flutter/rendering.dart';
@@ -69,6 +70,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
69 70
   }
70 71
 
71 72
   void showToolbar() {
73
+    if (kIsWeb) return;
72 74
     final toolbarOpacity = _toolbarController.view;
73 75
     _toolbar = OverlayEntry(
74 76
       builder: (context) => FadeTransition(
@@ -454,7 +456,15 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver>
454 456
     Offset point;
455 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 468
       case _SelectionHandlePosition.base:
459 469
         point = endpoints[0].point;
460 470
         type = _chooseType(endpoints[0], TextSelectionHandleType.left,