From 3609938327b56b3af96e8922842c8c60473625ca Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 29 Jan 2016 23:08:35 -0800 Subject: [PATCH] Adds the ability to move the caret by tapping Now the text input control knows how to move the caret when you tap inside the string. There's still some rough edges to polish up, but this patch is the first step. Fixes #108 --- packages/flutter/lib/src/material/input.dart | 32 ++++++++---- .../lib/src/painting/text_editing.dart | 49 ++++++------------- .../lib/src/painting/text_painter.dart | 5 ++ .../lib/src/rendering/editable_line.dart | 49 +++++++++++++++---- .../flutter/lib/src/rendering/proxy_box.dart | 7 +-- packages/flutter/lib/src/widgets/basic.dart | 2 +- .../flutter/lib/src/widgets/editable.dart | 32 ++++++++---- 7 files changed, 108 insertions(+), 68 deletions(-) diff --git a/packages/flutter/lib/src/material/input.dart b/packages/flutter/lib/src/material/input.dart index 9c551b0de14..e1092dbb922 100644 --- a/packages/flutter/lib/src/material/input.dart +++ b/packages/flutter/lib/src/material/input.dart @@ -121,6 +121,16 @@ class _InputState extends State { } } + void _requestKeyboard() { + if (Focus.at(context)) { + assert(_isAttachedToKeyboard); + _keyboardHandle.showByRequest(); + } else { + Focus.moveTo(config.key); + // we'll get told to rebuild and we'll take care of the keyboard then + } + } + void _handleTextUpdated() { if (_value != _editableString.text) { setState(() { @@ -137,6 +147,15 @@ class _InputState extends State { config.onSubmitted(_value); } + void _handleSelectionChanged(TextSelection selection) { + if (_isAttachedToKeyboard) { + _keyboardHandle.setSelection(selection.start, selection.end); + } else { + _editableString.setSelection(selection); + _requestKeyboard(); + } + } + Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); ThemeData themeData = Theme.of(context); @@ -221,7 +240,8 @@ class _InputState extends State { style: textStyle, hideText: config.hideText, cursorColor: cursorColor, - selectionColor: cursorColor + selectionColor: cursorColor, + onSelectionChanged: _handleSelectionChanged ) )); @@ -258,15 +278,7 @@ class _InputState extends State { return new GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () { - if (Focus.at(context)) { - assert(_isAttachedToKeyboard); - _keyboardHandle.showByRequest(); - } else { - Focus.moveTo(config.key); - // we'll get told to rebuild and we'll take care of the keyboard then - } - }, + onTap: _requestKeyboard, child: new Padding( padding: const EdgeDims.symmetric(horizontal: 16.0), child: child diff --git a/packages/flutter/lib/src/painting/text_editing.dart b/packages/flutter/lib/src/painting/text_editing.dart index 00a0835c713..736582b51af 100644 --- a/packages/flutter/lib/src/painting/text_editing.dart +++ b/packages/flutter/lib/src/painting/text_editing.dart @@ -2,38 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// Whether a [TextPosition] is visually upstream or downstream of its offset. -/// -/// For example, when a text position exists at a line break, a single offset has -/// two visual positions, one prior to the line break (at the end of the first -/// line) and one after the line break (at the start of the second line). A text -/// affinity disambiguates between those cases. (Something similar happens with -/// between runs of bidirectional text.) -enum TextAffinity { - /// The position has affinity for the upstream side of the text position. - /// - /// For example, if the offset of the text position is a line break, the - /// position represents the end of the first line. - upstream, +import 'dart:ui' show TextAffinity, TextPosition; - /// The position has affinity for the downstream side of the text position. - /// - /// For example, if the offset of the text position is a line break, the - /// position represents the start of the second line. - downstream -} - -/// A visual position in a string of text. -class TextPosition { - const TextPosition({ this.offset, this.affinity: TextAffinity.downstream }); - - /// The index of the character just prior to the position. - final int offset; - - /// If the offset has more than one visual location (e.g., occurs at a line - /// break), which of the two locations is represented by this position. - final TextAffinity affinity; -} +export 'dart:ui' show TextAffinity, TextPosition; /// A range of characters in a string of text. class TextRange { @@ -97,9 +68,15 @@ class TextSelection extends TextRange { const TextSelection.collapsed({ int offset, - this.affinity: TextAffinity.downstream, - this.isDirectional: false - }) : baseOffset = offset, extentOffset = offset, super.collapsed(offset); + this.affinity: TextAffinity.downstream + }) : baseOffset = offset, extentOffset = offset, isDirectional = false, super.collapsed(offset); + + TextSelection.fromPosition(TextPosition position) + : baseOffset = position.offset, + extentOffset = position.offset, + affinity = position.affinity, + isDirectional = false, + super.collapsed(position.offset); /// The offset at which the selection originates. /// @@ -141,4 +118,8 @@ class TextSelection extends TextRange { /// /// Might be larger than, smaller than, or equal to base. TextPosition get extent => new TextPosition(offset: extentOffset, affinity: affinity); + + String toString() { + return '$runtimeType(baseOffset: $baseOffset, extentOffset: $extentOffset, affinity: $affinity, isDirectional: $isDirectional)'; + } } diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index 4325c70d0b3..14bf7f1ba6d 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart @@ -265,4 +265,9 @@ class TextPainter { return _paragraph.getBoxesForRange(selection.start, selection.end); } + TextPosition getPositionForOffset(Offset offset) { + assert(!_needsLayout); + return _paragraph.getPositionForOffset(offset); + } + } diff --git a/packages/flutter/lib/src/rendering/editable_line.dart b/packages/flutter/lib/src/rendering/editable_line.dart index 4b8c47b67cd..4d346af84a8 100644 --- a/packages/flutter/lib/src/rendering/editable_line.dart +++ b/packages/flutter/lib/src/rendering/editable_line.dart @@ -4,12 +4,12 @@ import 'dart:ui' as ui; +import 'package:flutter/gestures.dart'; import 'package:flutter/painting.dart'; import 'box.dart'; import 'object.dart'; import 'paragraph.dart'; -import 'proxy_box.dart' show SizeChangedCallback; const _kCaretGap = 1.0; // pixels const _kCaretHeightOffset = 2.0; // pixels @@ -26,22 +26,28 @@ class RenderEditableLine extends RenderBox { Color selectionColor, TextSelection selection, Offset paintOffset: Offset.zero, + this.onSelectionChanged, this.onContentSizeChanged }) : _textPainter = new TextPainter(text), _cursorColor = cursorColor, _showCursor = showCursor, _selection = selection, _paintOffset = paintOffset { - assert(!showCursor || cursorColor != null); - // TODO(abarth): These min/max values should be the default for TextPainter. - _textPainter - ..minWidth = 0.0 - ..maxWidth = double.INFINITY - ..minHeight = 0.0 - ..maxHeight = double.INFINITY; + assert(!showCursor || cursorColor != null); + // TODO(abarth): These min/max values should be the default for TextPainter. + _textPainter + ..minWidth = 0.0 + ..maxWidth = double.INFINITY + ..minHeight = 0.0 + ..maxHeight = double.INFINITY; + _tap = new TapGestureRecognizer(router: Gesturer.instance.pointerRouter, gestureArena: Gesturer.instance.gestureArena) + ..onTapDown = _handleTapDown + ..onTap = _handleTap + ..onTapCancel = _handleTapCancel; } - SizeChangedCallback onContentSizeChanged; + ValueChanged onContentSizeChanged; + ValueChanged onSelectionChanged; /// The text to display StyledTextSpan get text => _textPainter.text; @@ -147,6 +153,31 @@ class RenderEditableLine extends RenderBox { bool hitTestSelf(Point position) => true; + TapGestureRecognizer _tap; + void handleEvent(PointerEvent event, BoxHitTestEntry entry) { + if (event is PointerDownEvent && onSelectionChanged != null) + _tap.addPointer(event); + } + + Point _lastTapDownPosition; + void _handleTapDown(Point globalPosition) { + _lastTapDownPosition = globalPosition; + } + + 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)); + } + } + + void _handleTapCancel() { + _lastTapDownPosition = null; + } + BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout // TODO(abarth): This logic should live in TextPainter and be shared with RenderParagraph. diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 44c1f4847d5..c65f2967954 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -1115,9 +1115,6 @@ class RenderFractionalTranslation extends RenderProxyBox { } } -/// Called when a size changes. -typedef void SizeChangedCallback(Size newSize); - /// Calls [onSizeChanged] whenever the child's layout size changes /// /// Because size observer calls its callback during layout, you cannot modify @@ -1131,7 +1128,7 @@ class RenderSizeObserver extends RenderProxyBox { } /// The callback to call whenever the child's layout size changes - SizeChangedCallback onSizeChanged; + ValueChanged onSizeChanged; void performLayout() { Size oldSize = hasSize ? size : null; @@ -1540,7 +1537,7 @@ class RenderSemanticAnnotations extends RenderProxyBox { /// If 'container' is true, this RenderObject will introduce a new /// node in the semantics tree. Otherwise, the semantics will be /// merged with the semantics of any ancestors. - /// + /// /// The 'container' flag is implicitly set to true on the immediate /// semantics-providing descendants of a node where multiple /// children have semantics or have descendants providing semantics. diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 06be46988f5..29552b691d1 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -819,7 +819,7 @@ class SizeObserver extends OneChildRenderObjectWidget { } /// The callback to call whenever the child's layout size changes - final SizeChangedCallback onSizeChanged; + final ValueChanged onSizeChanged; RenderSizeObserver createRenderObject() => new RenderSizeObserver(onSizeChanged: onSizeChanged); diff --git a/packages/flutter/lib/src/widgets/editable.dart b/packages/flutter/lib/src/widgets/editable.dart index e2222bf4ba0..361db24cd6a 100644 --- a/packages/flutter/lib/src/widgets/editable.dart +++ b/packages/flutter/lib/src/widgets/editable.dart @@ -158,6 +158,10 @@ class EditableString { /// The range of text that is currently selected. TextSelection get selection => _client.selection; + void setSelection(TextSelection selection) { + _client.selection = selection; + } + /// A keyboard client stub that can be attached to a keyboard service. /// /// See [Keyboard]. @@ -180,7 +184,8 @@ class RawEditableLine extends Scrollable { this.hideText: false, this.style, this.cursorColor, - this.selectionColor + this.selectionColor, + this.onSelectionChanged }) : super( key: key, initialScrollOffset: 0.0, @@ -205,6 +210,9 @@ class RawEditableLine extends Scrollable { /// The color to use when painting the selection. final Color selectionColor; + /// Called when the user requests a change to the selection. + final ValueChanged onSelectionChanged; + RawEditableTextState createState() => new RawEditableTextState(); } @@ -290,6 +298,7 @@ class RawEditableTextState extends ScrollableState { selectionColor: config.selectionColor, hideText: config.hideText, onContentSizeChanged: _handleContentSizeChanged, + onSelectionChanged: config.onSelectionChanged, paintOffset: new Offset(-scrollOffset, 0.0) ) ); @@ -306,6 +315,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget { this.selectionColor, this.hideText, this.onContentSizeChanged, + this.onSelectionChanged, this.paintOffset }) : super(key: key); @@ -315,7 +325,8 @@ class _EditableLineWidget extends LeafRenderObjectWidget { final bool showCursor; final Color selectionColor; final bool hideText; - final SizeChangedCallback onContentSizeChanged; + final ValueChanged onContentSizeChanged; + final ValueChanged onSelectionChanged; final Offset paintOffset; RenderEditableLine createRenderObject() { @@ -326,19 +337,22 @@ class _EditableLineWidget extends LeafRenderObjectWidget { selectionColor: selectionColor, selection: value.selection, onContentSizeChanged: onContentSizeChanged, + onSelectionChanged: onSelectionChanged, paintOffset: paintOffset ); } void updateRenderObject(RenderEditableLine renderObject, _EditableLineWidget oldWidget) { - renderObject.text = _styledTextSpan; - renderObject.cursorColor = cursorColor; - renderObject.showCursor = showCursor; - renderObject.selectionColor = selectionColor; - renderObject.selection = value.selection; - renderObject.onContentSizeChanged = onContentSizeChanged; - renderObject.paintOffset = paintOffset; + renderObject + ..text = _styledTextSpan + ..cursorColor = cursorColor + ..showCursor = showCursor + ..selectionColor = selectionColor + ..selection = value.selection + ..onContentSizeChanged = onContentSizeChanged + ..onSelectionChanged = onSelectionChanged + ..paintOffset = paintOffset; } StyledTextSpan get _styledTextSpan {