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 {