Browse Source

Paint selection and caret, plus some cleanups

Anatoly Pulyaevskiy 6 years ago
parent
commit
43e7a73cf9

+ 6
- 2
packages/zefyr/lib/src/widgets/caret.dart View File

@@ -9,13 +9,17 @@ class CaretPainter {
9 9
   static const double _kCaretHeightOffset = 2.0; // pixels
10 10
   static const double _kCaretWidth = 1.0; // pixels
11 11
 
12
+  static Rect buildPrototype(double lineHeight) {
13
+    return new Rect.fromLTWH(
14
+        0.0, 0.0, _kCaretWidth, lineHeight - _kCaretHeightOffset);
15
+  }
16
+
12 17
   Rect _prototype;
13 18
 
14 19
   Rect get prototype => _prototype;
15 20
 
16 21
   void layout(double lineHeight) {
17
-    _prototype = new Rect.fromLTWH(
18
-        0.0, 0.0, _kCaretWidth, lineHeight - _kCaretHeightOffset);
22
+    _prototype = buildPrototype(lineHeight);
19 23
   }
20 24
 
21 25
   void paint(Canvas canvas, Offset offset) {

+ 77
- 10
packages/zefyr/lib/src/widgets/editable_box.dart View File

@@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart';
8 8
 import 'package:flutter/widgets.dart';
9 9
 import 'package:notus/notus.dart';
10 10
 
11
+import 'caret.dart';
11 12
 import 'render_context.dart';
12 13
 
13 14
 class EditableBox extends SingleChildRenderObjectWidget {
@@ -29,7 +30,7 @@ class EditableBox extends SingleChildRenderObjectWidget {
29 30
   final Color selectionColor;
30 31
 
31 32
   @override
32
-  RenderObject createRenderObject(BuildContext context) {
33
+  RenderEditableProxyBox createRenderObject(BuildContext context) {
33 34
     return new RenderEditableProxyBox(
34 35
       node: node,
35 36
       layerLink: layerLink,
@@ -39,6 +40,18 @@ class EditableBox extends SingleChildRenderObjectWidget {
39 40
       selectionColor: selectionColor,
40 41
     );
41 42
   }
43
+
44
+  @override
45
+  void updateRenderObject(
46
+      BuildContext context, RenderEditableProxyBox renderObject) {
47
+    renderObject
48
+      ..node = node
49
+      ..layerLink = layerLink
50
+      ..renderContext = renderContext
51
+      ..showCursor = showCursor
52
+      ..selection = selection
53
+      ..selectionColor = selectionColor;
54
+  }
42 55
 }
43 56
 
44 57
 class RenderEditableProxyBox extends RenderBox
@@ -114,8 +127,6 @@ class RenderEditableProxyBox extends RenderBox
114 127
     markNeedsPaint();
115 128
   }
116 129
 
117
-  double get preferredLineHeight => child.preferredLineHeight;
118
-
119 130
   /// Returns `true` if current selection is collapsed, located within
120 131
   /// this paragraph and is visible according to tick timer.
121 132
   bool get isCaretVisible {
@@ -153,12 +164,11 @@ class RenderEditableProxyBox extends RenderBox
153 164
     super.detach();
154 165
   }
155 166
 
156
-  /// Subclasses must call super as the last statement when overriding this
157
-  /// method.
158 167
   @override
159 168
   @mustCallSuper
160 169
   void performLayout() {
161 170
     super.performLayout();
171
+    _caretPainter.layout(preferredLineHeight);
162 172
     // Indicate to render context that this object can be used by other
163 173
     // layers (selection overlay, for instance).
164 174
     _renderContext.markDirty(this, false);
@@ -171,6 +181,30 @@ class RenderEditableProxyBox extends RenderBox
171 181
     super.markNeedsLayout();
172 182
   }
173 183
 
184
+  @override
185
+  void paint(PaintingContext context, Offset offset) {
186
+    if (selectionOrder == SelectionOrder.background && isSelectionVisible) {
187
+      paintSelection(context, offset, selection, selectionColor);
188
+    }
189
+    super.paint(context, offset);
190
+    if (selectionOrder == SelectionOrder.foreground && isSelectionVisible) {
191
+      paintSelection(context, offset, selection, selectionColor);
192
+    }
193
+    if (isCaretVisible) {
194
+      _paintCaret(context, offset);
195
+    }
196
+  }
197
+
198
+  final CaretPainter _caretPainter = new CaretPainter();
199
+
200
+  void _paintCaret(PaintingContext context, Offset offset) {
201
+    final TextPosition caret = new TextPosition(
202
+      offset: _selection.extentOffset - node.documentOffset,
203
+    );
204
+    Offset caretOffset = getOffsetForCaret(caret, _caretPainter.prototype);
205
+    _caretPainter.paint(context.canvas, caretOffset + offset);
206
+  }
207
+
174 208
   @override
175 209
   bool hitTestSelf(Offset position) => true;
176 210
 
@@ -183,6 +217,25 @@ class RenderEditableProxyBox extends RenderBox
183 217
     return false;
184 218
   }
185 219
 
220
+  //
221
+  // Proxy methods
222
+  //
223
+
224
+  @override
225
+  double get preferredLineHeight => child.preferredLineHeight;
226
+
227
+  @override
228
+  SelectionOrder get selectionOrder => child.selectionOrder;
229
+
230
+  @override
231
+  void paintSelection(PaintingContext context, Offset offset,
232
+          TextSelection selection, Color selectionColor) =>
233
+      child.paintSelection(context, offset, selection, selectionColor);
234
+
235
+  @override
236
+  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) =>
237
+      child.getOffsetForCaret(position, caretPrototype);
238
+
186 239
   @override
187 240
   TextSelection getLocalSelection(TextSelection documentSelection) =>
188 241
       child.getLocalSelection(documentSelection);
@@ -191,9 +244,8 @@ class RenderEditableProxyBox extends RenderBox
191 244
       child.intersectsWithSelection(selection);
192 245
 
193 246
   @override
194
-  List<ui.TextBox> getEndpointsForSelection(TextSelection selection,
195
-          {bool isLocal: false}) =>
196
-      child.getEndpointsForSelection(selection, isLocal: isLocal);
247
+  List<ui.TextBox> getEndpointsForSelection(TextSelection selection) =>
248
+      child.getEndpointsForSelection(selection);
197 249
 
198 250
   @override
199 251
   ui.TextPosition getPositionForOffset(ui.Offset offset) =>
@@ -204,13 +256,20 @@ class RenderEditableProxyBox extends RenderBox
204 256
       child.getWordBoundary(position);
205 257
 }
206 258
 
259
+enum SelectionOrder {
260
+  /// Background selection is painted before primary content of editable box.
261
+  background,
262
+
263
+  /// Foreground selection is painted after primary content of editable box.
264
+  foreground,
265
+}
266
+
207 267
 abstract class RenderEditableBox extends RenderBox {
208 268
   Node get node;
209 269
   double get preferredLineHeight;
210 270
 
211 271
   TextPosition getPositionForOffset(Offset offset);
212
-  List<ui.TextBox> getEndpointsForSelection(TextSelection selection,
213
-      {bool isLocal: false});
272
+  List<ui.TextBox> getEndpointsForSelection(TextSelection selection);
214 273
 
215 274
   /// Returns the text range of the word at the given offset. Characters not
216 275
   /// part of a word, such as spaces, symbols, and punctuation, have word breaks
@@ -223,6 +282,14 @@ abstract class RenderEditableBox extends RenderBox {
223 282
   /// Valid only after [layout].
224 283
   TextRange getWordBoundary(TextPosition position);
225 284
 
285
+  /// Paint order of selection in this editable box.
286
+  SelectionOrder get selectionOrder;
287
+
288
+  void paintSelection(PaintingContext context, Offset offset,
289
+      TextSelection selection, Color selectionColor);
290
+
291
+  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype);
292
+
226 293
   /// Returns part of [documentSelection] local to this box. May return
227 294
   /// `null`.
228 295
   ///

+ 29
- 23
packages/zefyr/lib/src/widgets/editable_image.dart View File

@@ -46,8 +46,12 @@ class RenderEditableImage extends RenderImage implements RenderEditableBox {
46 46
     _node = value;
47 47
   }
48 48
 
49
+  @override
49 50
   double get preferredLineHeight => size.height;
50 51
 
52
+  @override
53
+  SelectionOrder get selectionOrder => SelectionOrder.foreground;
54
+
51 55
   @override
52 56
   TextSelection getLocalSelection(TextSelection documentSelection) {
53 57
     if (!intersectsWithSelection(documentSelection)) return null;
@@ -60,43 +64,28 @@ class RenderEditableImage extends RenderImage implements RenderEditableBox {
60 64
     return documentSelection.copyWith(baseOffset: base, extentOffset: extent);
61 65
   }
62 66
 
63
-  List<ui.TextBox> getEndpointsForSelection(TextSelection selection,
64
-      {bool isLocal: false}) {
65
-    TextSelection local = isLocal ? selection : getLocalSelection(selection);
67
+  @override
68
+  List<TextBox> getEndpointsForSelection(TextSelection selection) {
69
+    TextSelection local = getLocalSelection(selection);
66 70
     if (local.isCollapsed) {
67 71
       return [
68
-        new ui.TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr),
72
+        new TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr),
69 73
       ];
70 74
     }
71 75
 
72 76
     return [
73
-      new ui.TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr),
74
-      new ui.TextBox.fromLTRBD(
77
+      new TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr),
78
+      new TextBox.fromLTRBD(
75 79
           size.width, 0.0, size.width, size.height, TextDirection.ltr),
76 80
     ];
77 81
   }
78 82
 
79 83
   @override
80
-  void performLayout() {
81
-    super.performLayout();
82
-//    _prototypePainter.layout(
83
-//        minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
84
-//    _caretPainter.layout(_prototypePainter.height);
84
+  TextPosition getPositionForOffset(Offset offset) {
85
+    return new TextPosition(offset: node.documentOffset);
85 86
   }
86 87
 
87 88
   @override
88
-  void paint(PaintingContext context, Offset offset) {
89
-//    if (_isSelectionVisible) _paintSelection(context, offset);
90
-    super.paint(context, offset);
91
-//    if (_isCaretVisible) _paintCaret(context, offset);
92
-  }
93
-
94
-  @override
95
-  ui.TextPosition getPositionForOffset(ui.Offset offset) {
96
-    return new ui.TextPosition(offset: node.documentOffset);
97
-  }
98
-
99
-  /// Returns `true` if this paragraph intersects with document [selection].
100 89
   bool intersectsWithSelection(TextSelection selection) {
101 90
     final int base = node.documentOffset;
102 91
     final int extent = base + node.length;
@@ -107,4 +96,21 @@ class RenderEditableImage extends RenderImage implements RenderEditableBox {
107 96
   TextRange getWordBoundary(ui.TextPosition position) {
108 97
     return new TextRange(start: position.offset, end: position.offset + 1);
109 98
   }
99
+
100
+  @override
101
+  ui.Offset getOffsetForCaret(
102
+      ui.TextPosition position, ui.Rect caretPrototype) {
103
+    final pos = position.offset - node.documentOffset;
104
+    Offset caretOffset = Offset.zero;
105
+    if (pos == 1) {
106
+      caretOffset = caretOffset + new Offset(size.width - 1.0, 0.0);
107
+    }
108
+    return caretOffset;
109
+  }
110
+
111
+  @override
112
+  void paintSelection(PaintingContext context, ui.Offset offset,
113
+      TextSelection selection, ui.Color selectionColor) {
114
+    // TODO: implement paintSelection
115
+  }
110 116
 }

+ 17
- 25
packages/zefyr/lib/src/widgets/editable_rich_text.dart View File

@@ -75,6 +75,9 @@ class RenderEditableParagraph extends RenderParagraph
75 75
   @override
76 76
   double get preferredLineHeight => _prototypePainter.height;
77 77
 
78
+  @override
79
+  SelectionOrder get selectionOrder => SelectionOrder.background;
80
+
78 81
   @override
79 82
   TextSelection getLocalSelection(TextSelection documentSelection) {
80 83
     if (!intersectsWithSelection(documentSelection)) return null;
@@ -89,19 +92,18 @@ class RenderEditableParagraph extends RenderParagraph
89 92
 
90 93
   // This method works around some issues in getBoxesForSelection and handles
91 94
   // edge-case with our TextSpan objects not having last line-break character.
92
-  // Wait for https://github.com/flutter/flutter/issues/16418 to be resolved.
93 95
   @override
94
-  List<ui.TextBox> getEndpointsForSelection(TextSelection selection,
95
-      {bool isLocal: false}) {
96
-    TextSelection local = isLocal ? selection : getLocalSelection(selection);
96
+  List<ui.TextBox> getEndpointsForSelection(TextSelection selection) {
97
+    TextSelection local = getLocalSelection(selection);
97 98
     if (local.isCollapsed) {
98
-      final offset = getOffsetForCaret(local.extent, _caretPainter.prototype);
99
+      final caret = CaretPainter.buildPrototype(preferredLineHeight);
100
+      final offset = getOffsetForCaret(local.extent, caret);
99 101
       return [
100 102
         new ui.TextBox.fromLTRBD(
101 103
           offset.dx,
102 104
           offset.dy,
103 105
           offset.dx,
104
-          offset.dy + _caretPainter.prototype.height,
106
+          offset.dy + caret.height,
105 107
           TextDirection.ltr,
106 108
         )
107 109
       ];
@@ -158,46 +160,36 @@ class RenderEditableParagraph extends RenderParagraph
158 160
     super.performLayout();
159 161
     _prototypePainter.layout(
160 162
         minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
161
-    _caretPainter.layout(_prototypePainter.height);
162 163
   }
163 164
 
164 165
   @override
165 166
   void paint(PaintingContext context, Offset offset) {
166
-//    if (isSelectionVisible) _paintSelection(context, offset);
167 167
     super.paint(context, offset);
168
-//    if (isCaretVisible) _paintCaret(context, offset);
169 168
   }
170 169
 
171
-  //
172
-  // Private members
173
-  //
174
-
175 170
   final TextPainter _prototypePainter;
176
-  final CaretPainter _caretPainter = new CaretPainter();
177 171
   List<ui.TextBox> _selectionRects;
178 172
 
179 173
   /// Returns `true` if this paragraph intersects with document [selection].
174
+  @override
180 175
   bool intersectsWithSelection(TextSelection selection) {
181 176
     final int base = node.documentOffset;
182 177
     final int extent = base + node.length;
183 178
     return base <= selection.extentOffset && selection.baseOffset <= extent;
184 179
   }
185 180
 
186
-//
187
-//  void _paintCaret(PaintingContext context, Offset offset) {
188
-//    final TextPosition caret = new TextPosition(
189
-//      offset: _selection.extentOffset - node.documentOffset,
190
-//    );
191
-//    Offset caretOffset = getOffsetForCaret(caret, _caretPainter.prototype);
192
-//    _caretPainter.paint(context.canvas, caretOffset + offset);
193
-//  }
194
-//
181
+  TextSelection _lastPaintedSelection;
182
+  @override
195 183
   void paintSelection(PaintingContext context, Offset offset,
196 184
       TextSelection selection, Color selectionColor) {
197
-    // TODO: this could be improved by painting additional box for line-break characters.
185
+    if (_lastPaintedSelection != selection) {
186
+      _selectionRects = null;
187
+    }
198 188
     _selectionRects ??= getBoxesForSelection(getLocalSelection(selection));
199 189
     final Paint paint = new Paint()..color = selectionColor;
200
-    for (ui.TextBox box in _selectionRects)
190
+    for (ui.TextBox box in _selectionRects) {
201 191
       context.canvas.drawRect(box.toRect().shift(offset), paint);
192
+    }
193
+    _lastPaintedSelection = selection;
202 194
   }
203 195
 }

+ 24
- 28
packages/zefyr/lib/src/widgets/horizontal_rule.dart View File

@@ -48,9 +48,12 @@ class RenderHorizontalRule extends RenderEditableBox {
48 48
   @override
49 49
   double get preferredLineHeight => size.height;
50 50
 
51
-  List<ui.TextBox> getEndpointsForSelection(TextSelection selection,
52
-      {bool isLocal: false}) {
53
-    TextSelection local = isLocal ? selection : getLocalSelection(selection);
51
+  @override
52
+  SelectionOrder get selectionOrder => SelectionOrder.background;
53
+
54
+  @override
55
+  List<ui.TextBox> getEndpointsForSelection(TextSelection selection) {
56
+    TextSelection local =  getLocalSelection(selection);
54 57
     if (local.isCollapsed) {
55 58
       return [
56 59
         new ui.TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr),
@@ -68,16 +71,13 @@ class RenderHorizontalRule extends RenderEditableBox {
68 71
   void performLayout() {
69 72
     assert(constraints.hasBoundedWidth);
70 73
     size = new Size(constraints.maxWidth, _kHeight);
71
-//    _caretPainter.layout(height);
72 74
   }
73 75
 
74 76
   @override
75 77
   void paint(PaintingContext context, Offset offset) {
76
-//    if (isSelectionVisible) _paintSelection(context, offset);
77 78
     final rect = new Rect.fromLTWH(0.0, 0.0, size.width, _kThickness);
78 79
     final paint = new ui.Paint()..color = Colors.grey.shade200;
79 80
     context.canvas.drawRect(rect.shift(offset), paint);
80
-//    if (isCaretVisible) _paintCaret(context, offset);
81 81
   }
82 82
 
83 83
   @override
@@ -86,29 +86,25 @@ class RenderHorizontalRule extends RenderEditableBox {
86 86
   }
87 87
 
88 88
   @override
89
-  TextRange getWordBoundary(ui.TextPosition position) {
89
+  TextRange getWordBoundary(TextPosition position) {
90 90
     return new TextRange(start: position.offset, end: position.offset + 1);
91 91
   }
92 92
 
93
-  //
94
-  // Private members
95
-  //
96
-//
97
-//  final CaretPainter _caretPainter = new CaretPainter();
98
-//
99
-//  void _paintCaret(PaintingContext context, Offset offset) {
100
-//    final pos = selection.extentOffset - node.documentOffset;
101
-//    Offset caretOffset = Offset.zero;
102
-//    if (pos == 1) {
103
-//      caretOffset = caretOffset + new Offset(size.width - 1.0, 0.0);
104
-//    }
105
-//    _caretPainter.paint(context.canvas, caretOffset + offset);
106
-//  }
107
-//
108
-//  void _paintSelection(PaintingContext context, Offset offset) {
109
-//    assert(isSelectionVisible);
110
-//    final Paint paint = new Paint()..color = selectionColor;
111
-//    final rect = new Rect.fromLTWH(0.0, 0.0, size.width, _kHeight);
112
-//    context.canvas.drawRect(rect.shift(offset), paint);
113
-//  }
93
+  @override
94
+  void paintSelection(PaintingContext context, Offset offset,
95
+      TextSelection selection, Color selectionColor) {
96
+    final Paint paint = new Paint()..color = selectionColor;
97
+    final rect = new Rect.fromLTWH(0.0, 0.0, size.width, _kHeight);
98
+    context.canvas.drawRect(rect.shift(offset), paint);
99
+  }
100
+
101
+  @override
102
+  Offset getOffsetForCaret(ui.TextPosition position, ui.Rect caretPrototype) {
103
+    final pos = position.offset - node.documentOffset;
104
+    Offset caretOffset = Offset.zero;
105
+    if (pos == 1) {
106
+      caretOffset = caretOffset + new Offset(size.width - 1.0, 0.0);
107
+    }
108
+    return caretOffset;
109
+  }
114 110
 }