|
@@ -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);
|