-
Notifications
You must be signed in to change notification settings - Fork 26.7k
/
editable_line.dart
366 lines (315 loc) · 12.9 KB
/
editable_line.dart
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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, TextBox;
import 'package:flutter/gestures.dart';
import 'box.dart';
import 'object.dart';
import 'viewport.dart';
const double _kCaretGap = 1.0; // pixels
const double _kCaretHeightOffset = 2.0; // pixels
const double _kCaretWidth = 1.0; // pixels
final String _kZeroWidthSpace = new String.fromCharCode(0x200B);
/// Called when the user changes the selection (including cursor location).
typedef void SelectionChangedHandler(TextSelection selection, RenderEditableLine renderObject, bool longPress);
/// Represents a global screen coordinate of the point in a selection, and the
/// text direction at that point.
class TextSelectionPoint {
/// Creates a description of a point in a text selection.
///
/// The [point] argument must not be null.
TextSelectionPoint(this.point, this.direction) {
assert(point != null);
}
/// Screen coordinates of the lower left or lower right corner of the selection.
final Point point;
/// Direction of the text at this edge of the selection.
final TextDirection direction;
}
typedef Offset RenderEditableLinePaintOffsetNeededCallback(ViewportDimensions dimensions, Rect caretRect);
/// A single line of editable text.
class RenderEditableLine extends RenderBox {
/// Creates a render object for a single line of editable text.
RenderEditableLine({
TextSpan text,
Color cursorColor,
bool showCursor: false,
int maxLines: 1,
Color selectionColor,
double textScaleFactor: 1.0,
TextSelection selection,
this.onSelectionChanged,
Offset paintOffset: Offset.zero,
this.onPaintOffsetUpdateNeeded,
}) : _textPainter = new TextPainter(text: text, textScaleFactor: textScaleFactor),
_cursorColor = cursorColor,
_showCursor = showCursor,
_maxLines = maxLines,
_selection = selection,
_paintOffset = paintOffset {
assert(!showCursor || cursorColor != null);
_tap = new TapGestureRecognizer()
..onTapDown = _handleTapDown
..onTap = _handleTap
..onTapCancel = _handleTapCancel;
_longPress = new LongPressGestureRecognizer()
..onLongPress = _handleLongPress;
}
/// Called when the selection changes.
SelectionChangedHandler onSelectionChanged;
/// Called when the inner or outer dimensions of this render object change.
RenderEditableLinePaintOffsetNeededCallback onPaintOffsetUpdateNeeded;
/// The text to display
TextSpan get text => _textPainter.text;
final TextPainter _textPainter;
set text(TextSpan value) {
if (_textPainter.text == value)
return;
TextSpan oldStyledText = _textPainter.text;
if (oldStyledText.style != value.style)
_layoutTemplate = null;
_textPainter.text = value;
markNeedsLayout();
}
/// The color to use when painting the cursor.
Color get cursorColor => _cursorColor;
Color _cursorColor;
set cursorColor(Color value) {
if (_cursorColor == value)
return;
_cursorColor = value;
markNeedsPaint();
}
/// Whether to paint the cursor.
bool get showCursor => _showCursor;
bool _showCursor;
set showCursor(bool value) {
if (_showCursor == value)
return;
_showCursor = value;
markNeedsPaint();
}
/// Whether to paint the cursor.
int get maxLines => _maxLines;
int _maxLines;
set maxLines(int value) {
if (_maxLines == value)
return;
_maxLines = value;
markNeedsLayout();
}
/// The color to use when painting the selection.
Color get selectionColor => _selectionColor;
Color _selectionColor;
set selectionColor(Color value) {
if (_selectionColor == value)
return;
_selectionColor = value;
markNeedsPaint();
}
/// The number of font pixels for each logical pixel.
///
/// For example, if the text scale factor is 1.5, text will be 50% larger than
/// the specified font size.
double get textScaleFactor => _textPainter.textScaleFactor;
set textScaleFactor(double value) {
assert(value != null);
if (_textPainter.textScaleFactor == value)
return;
_textPainter.textScaleFactor = value;
markNeedsLayout();
}
List<ui.TextBox> _selectionRects;
/// The region of text that is selected, if any.
TextSelection get selection => _selection;
TextSelection _selection;
set selection(TextSelection value) {
if (_selection == value)
return;
_selection = value;
_selectionRects = null;
markNeedsPaint();
}
/// The offset at which the text should be painted.
///
/// If the text content is larger than the editable line itself, the editable
/// line clips the text. This property controls which part of the text is
/// visible by shifting the text by the given offset before clipping.
Offset get paintOffset => _paintOffset;
Offset _paintOffset;
set paintOffset(Offset value) {
if (_paintOffset == value)
return;
_paintOffset = value;
markNeedsPaint();
}
/// Returns the global coordinates of the endpoints of the given selection.
///
/// If the selection is collapsed (and therefore occupies a single point), the
/// returned list is of length one. Otherwise, the selection is not collapsed
/// and the returned list is of length two. In this case, however, the two
/// points might actually be co-located (e.g., because of a bidirectional
/// selection that contains some text but whose ends meet in the middle).
List<TextSelectionPoint> getEndpointsForSelection(TextSelection selection) {
// TODO(mpcomplete): We should be more disciplined about when we dirty the
// layout state of the text painter so that we can know that the layout is
// clean at this point.
_textPainter.layout(maxWidth: _maxContentWidth);
Offset offset = _paintOffset;
if (selection.isCollapsed) {
// TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype);
Point start = new Point(0.0, constraints.constrainHeight(_preferredLineHeight)) + caretOffset + offset;
return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)];
} else {
List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection);
Point start = new Point(boxes.first.start, boxes.first.bottom) + offset;
Point end = new Point(boxes.last.end, boxes.last.bottom) + offset;
return <TextSelectionPoint>[
new TextSelectionPoint(localToGlobal(start), boxes.first.direction),
new TextSelectionPoint(localToGlobal(end), boxes.last.direction),
];
}
}
/// Returns the position in the text for the given global coordinate.
TextPosition getPositionForPoint(Point globalPosition) {
globalPosition += -paintOffset;
return _textPainter.getPositionForOffset(globalToLocal(globalPosition).toOffset());
}
/// Returns the Rect in local coordinates for the caret at the given text
/// position.
Rect getLocalRectForCaret(TextPosition caretPosition) {
double lineHeight = constraints.constrainHeight(_preferredLineHeight);
Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
// This rect is the same as _caretPrototype but without the vertical padding.
return new Rect.fromLTWH(0.0, 0.0, _kCaretWidth, lineHeight).shift(caretOffset + _paintOffset);
}
Size _contentSize;
ui.Paragraph _layoutTemplate;
double get _preferredLineHeight {
if (_layoutTemplate == null) {
ui.ParagraphBuilder builder = new ui.ParagraphBuilder()
..pushStyle(text.style.getTextStyle(textScaleFactor: textScaleFactor))
..addText(_kZeroWidthSpace);
// TODO(abarth): ParagraphBuilder#build's argument should be optional.
// TODO(abarth): These min/max values should be the default for ui.Paragraph.
_layoutTemplate = builder.build(new ui.ParagraphStyle())
..layout(new ui.ParagraphConstraints(width: _maxContentWidth));
}
return _layoutTemplate.height;
}
double get _maxContentWidth {
return _maxLines > 1 ?
constraints.maxWidth - (_kCaretGap + _kCaretWidth) :
double.INFINITY;
}
@override
double computeMinIntrinsicHeight(double width) {
return _preferredLineHeight;
}
@override
double computeMaxIntrinsicHeight(double width) {
return _preferredLineHeight;
}
@override
bool hitTestSelf(Point position) => true;
TapGestureRecognizer _tap;
LongPressGestureRecognizer _longPress;
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && onSelectionChanged != null) {
_tap.addPointer(event);
_longPress.addPointer(event);
}
}
Point _lastTapDownPosition;
Point _longPressPosition;
void _handleTapDown(TapDownDetails details) {
_lastTapDownPosition = details.globalPosition + -paintOffset;
}
void _handleTap() {
assert(_lastTapDownPosition != null);
final Point global = _lastTapDownPosition;
_lastTapDownPosition = null;
if (onSelectionChanged != null) {
TextPosition position = _textPainter.getPositionForOffset(globalToLocal(global).toOffset());
onSelectionChanged(new TextSelection.fromPosition(position), this, false);
}
}
void _handleTapCancel() {
// longPress arrives after tapCancel, so remember the tap position.
_longPressPosition = _lastTapDownPosition;
_lastTapDownPosition = null;
}
void _handleLongPress() {
final Point global = _longPressPosition;
_longPressPosition = null;
if (onSelectionChanged != null) {
TextPosition position = _textPainter.getPositionForOffset(globalToLocal(global).toOffset());
onSelectionChanged(_selectWordAtOffset(position), this, true);
}
}
TextSelection _selectWordAtOffset(TextPosition position) {
TextRange word = _textPainter.getWordBoundary(position);
// When long-pressing past the end of the text, we want a collapsed cursor.
if (position.offset >= word.end)
return new TextSelection.fromPosition(position);
return new TextSelection(baseOffset: word.start, extentOffset: word.end);
}
Rect _caretPrototype;
@override
void performLayout() {
Size oldSize = hasSize ? size : null;
double lineHeight = constraints.constrainHeight(_preferredLineHeight);
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, lineHeight - 2.0 * _kCaretHeightOffset);
_selectionRects = null;
_textPainter.layout(maxWidth: _maxContentWidth);
size = new Size(constraints.maxWidth, constraints.constrainHeight(
_textPainter.height.clamp(lineHeight, lineHeight * _maxLines)
));
Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height);
assert(_selection != null);
Rect caretRect = getLocalRectForCaret(_selection.extent);
if (onPaintOffsetUpdateNeeded != null && (size != oldSize || contentSize != _contentSize || !_withinBounds(caretRect)))
onPaintOffsetUpdateNeeded(new ViewportDimensions(containerSize: size, contentSize: contentSize), caretRect);
_contentSize = contentSize;
}
bool _withinBounds(Rect caretRect) {
Rect bounds = new Rect.fromLTWH(0.0, 0.0, size.width, size.height);
return (bounds.contains(caretRect.topLeft) && bounds.contains(caretRect.bottomRight));
}
void _paintCaret(Canvas canvas, Offset effectiveOffset) {
Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype);
Paint paint = new Paint()..color = _cursorColor;
canvas.drawRect(_caretPrototype.shift(caretOffset + effectiveOffset), paint);
}
void _paintSelection(Canvas canvas, Offset effectiveOffset) {
assert(_selectionRects != null);
Paint paint = new Paint()..color = _selectionColor;
for (ui.TextBox box in _selectionRects)
canvas.drawRect(box.toRect().shift(effectiveOffset), paint);
}
void _paintContents(PaintingContext context, Offset offset) {
Offset effectiveOffset = offset + _paintOffset;
if (_selection != null) {
if (_selection.isCollapsed && _showCursor && cursorColor != null) {
_paintCaret(context.canvas, effectiveOffset);
} else if (!_selection.isCollapsed && _selectionColor != null) {
_selectionRects ??= _textPainter.getBoxesForSelection(_selection);
_paintSelection(context.canvas, effectiveOffset);
}
}
_textPainter.paint(context.canvas, effectiveOffset);
}
bool get _hasVisualOverflow => _contentSize.width > size.width;
@override
void paint(PaintingContext context, Offset offset) {
if (_hasVisualOverflow)
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
else
_paintContents(context, offset);
}
@override
Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Point.origin & size : null;
}