Commit 36099383 authored by Adam Barth's avatar Adam Barth

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
parent 4125f386
...@@ -121,6 +121,16 @@ class _InputState extends State<Input> { ...@@ -121,6 +121,16 @@ class _InputState extends State<Input> {
} }
} }
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() { void _handleTextUpdated() {
if (_value != _editableString.text) { if (_value != _editableString.text) {
setState(() { setState(() {
...@@ -137,6 +147,15 @@ class _InputState extends State<Input> { ...@@ -137,6 +147,15 @@ class _InputState extends State<Input> {
config.onSubmitted(_value); config.onSubmitted(_value);
} }
void _handleSelectionChanged(TextSelection selection) {
if (_isAttachedToKeyboard) {
_keyboardHandle.setSelection(selection.start, selection.end);
} else {
_editableString.setSelection(selection);
_requestKeyboard();
}
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
ThemeData themeData = Theme.of(context); ThemeData themeData = Theme.of(context);
...@@ -221,7 +240,8 @@ class _InputState extends State<Input> { ...@@ -221,7 +240,8 @@ class _InputState extends State<Input> {
style: textStyle, style: textStyle,
hideText: config.hideText, hideText: config.hideText,
cursorColor: cursorColor, cursorColor: cursorColor,
selectionColor: cursorColor selectionColor: cursorColor,
onSelectionChanged: _handleSelectionChanged
) )
)); ));
...@@ -258,15 +278,7 @@ class _InputState extends State<Input> { ...@@ -258,15 +278,7 @@ class _InputState extends State<Input> {
return new GestureDetector( return new GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: _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
}
},
child: new Padding( child: new Padding(
padding: const EdgeDims.symmetric(horizontal: 16.0), padding: const EdgeDims.symmetric(horizontal: 16.0),
child: child child: child
......
...@@ -2,38 +2,9 @@ ...@@ -2,38 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
/// Whether a [TextPosition] is visually upstream or downstream of its offset. import 'dart:ui' show TextAffinity, TextPosition;
///
/// 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,
/// 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. export 'dart:ui' show TextAffinity, TextPosition;
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;
}
/// A range of characters in a string of text. /// A range of characters in a string of text.
class TextRange { class TextRange {
...@@ -97,9 +68,15 @@ class TextSelection extends TextRange { ...@@ -97,9 +68,15 @@ class TextSelection extends TextRange {
const TextSelection.collapsed({ const TextSelection.collapsed({
int offset, int offset,
this.affinity: TextAffinity.downstream, this.affinity: TextAffinity.downstream
this.isDirectional: false }) : baseOffset = offset, extentOffset = offset, isDirectional = false, super.collapsed(offset);
}) : baseOffset = offset, extentOffset = offset, 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. /// The offset at which the selection originates.
/// ///
...@@ -141,4 +118,8 @@ class TextSelection extends TextRange { ...@@ -141,4 +118,8 @@ class TextSelection extends TextRange {
/// ///
/// Might be larger than, smaller than, or equal to base. /// Might be larger than, smaller than, or equal to base.
TextPosition get extent => new TextPosition(offset: extentOffset, affinity: affinity); TextPosition get extent => new TextPosition(offset: extentOffset, affinity: affinity);
String toString() {
return '$runtimeType(baseOffset: $baseOffset, extentOffset: $extentOffset, affinity: $affinity, isDirectional: $isDirectional)';
}
} }
...@@ -265,4 +265,9 @@ class TextPainter { ...@@ -265,4 +265,9 @@ class TextPainter {
return _paragraph.getBoxesForRange(selection.start, selection.end); return _paragraph.getBoxesForRange(selection.start, selection.end);
} }
TextPosition getPositionForOffset(Offset offset) {
assert(!_needsLayout);
return _paragraph.getPositionForOffset(offset);
}
} }
...@@ -4,12 +4,12 @@ ...@@ -4,12 +4,12 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'paragraph.dart'; import 'paragraph.dart';
import 'proxy_box.dart' show SizeChangedCallback;
const _kCaretGap = 1.0; // pixels const _kCaretGap = 1.0; // pixels
const _kCaretHeightOffset = 2.0; // pixels const _kCaretHeightOffset = 2.0; // pixels
...@@ -26,6 +26,7 @@ class RenderEditableLine extends RenderBox { ...@@ -26,6 +26,7 @@ class RenderEditableLine extends RenderBox {
Color selectionColor, Color selectionColor,
TextSelection selection, TextSelection selection,
Offset paintOffset: Offset.zero, Offset paintOffset: Offset.zero,
this.onSelectionChanged,
this.onContentSizeChanged this.onContentSizeChanged
}) : _textPainter = new TextPainter(text), }) : _textPainter = new TextPainter(text),
_cursorColor = cursorColor, _cursorColor = cursorColor,
...@@ -39,9 +40,14 @@ class RenderEditableLine extends RenderBox { ...@@ -39,9 +40,14 @@ class RenderEditableLine extends RenderBox {
..maxWidth = double.INFINITY ..maxWidth = double.INFINITY
..minHeight = 0.0 ..minHeight = 0.0
..maxHeight = double.INFINITY; ..maxHeight = double.INFINITY;
_tap = new TapGestureRecognizer(router: Gesturer.instance.pointerRouter, gestureArena: Gesturer.instance.gestureArena)
..onTapDown = _handleTapDown
..onTap = _handleTap
..onTapCancel = _handleTapCancel;
} }
SizeChangedCallback onContentSizeChanged; ValueChanged<Size> onContentSizeChanged;
ValueChanged<TextSelection> onSelectionChanged;
/// The text to display /// The text to display
StyledTextSpan get text => _textPainter.text; StyledTextSpan get text => _textPainter.text;
...@@ -147,6 +153,31 @@ class RenderEditableLine extends RenderBox { ...@@ -147,6 +153,31 @@ class RenderEditableLine extends RenderBox {
bool hitTestSelf(Point position) => true; 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 BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout
// TODO(abarth): This logic should live in TextPainter and be shared with RenderParagraph. // TODO(abarth): This logic should live in TextPainter and be shared with RenderParagraph.
......
...@@ -1115,9 +1115,6 @@ class RenderFractionalTranslation extends RenderProxyBox { ...@@ -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 /// Calls [onSizeChanged] whenever the child's layout size changes
/// ///
/// Because size observer calls its callback during layout, you cannot modify /// Because size observer calls its callback during layout, you cannot modify
...@@ -1131,7 +1128,7 @@ class RenderSizeObserver extends RenderProxyBox { ...@@ -1131,7 +1128,7 @@ class RenderSizeObserver extends RenderProxyBox {
} }
/// The callback to call whenever the child's layout size changes /// The callback to call whenever the child's layout size changes
SizeChangedCallback onSizeChanged; ValueChanged<Size> onSizeChanged;
void performLayout() { void performLayout() {
Size oldSize = hasSize ? size : null; Size oldSize = hasSize ? size : null;
......
...@@ -819,7 +819,7 @@ class SizeObserver extends OneChildRenderObjectWidget { ...@@ -819,7 +819,7 @@ class SizeObserver extends OneChildRenderObjectWidget {
} }
/// The callback to call whenever the child's layout size changes /// The callback to call whenever the child's layout size changes
final SizeChangedCallback onSizeChanged; final ValueChanged<Size> onSizeChanged;
RenderSizeObserver createRenderObject() => new RenderSizeObserver(onSizeChanged: onSizeChanged); RenderSizeObserver createRenderObject() => new RenderSizeObserver(onSizeChanged: onSizeChanged);
......
...@@ -158,6 +158,10 @@ class EditableString { ...@@ -158,6 +158,10 @@ class EditableString {
/// The range of text that is currently selected. /// The range of text that is currently selected.
TextSelection get selection => _client.selection; TextSelection get selection => _client.selection;
void setSelection(TextSelection selection) {
_client.selection = selection;
}
/// A keyboard client stub that can be attached to a keyboard service. /// A keyboard client stub that can be attached to a keyboard service.
/// ///
/// See [Keyboard]. /// See [Keyboard].
...@@ -180,7 +184,8 @@ class RawEditableLine extends Scrollable { ...@@ -180,7 +184,8 @@ class RawEditableLine extends Scrollable {
this.hideText: false, this.hideText: false,
this.style, this.style,
this.cursorColor, this.cursorColor,
this.selectionColor this.selectionColor,
this.onSelectionChanged
}) : super( }) : super(
key: key, key: key,
initialScrollOffset: 0.0, initialScrollOffset: 0.0,
...@@ -205,6 +210,9 @@ class RawEditableLine extends Scrollable { ...@@ -205,6 +210,9 @@ class RawEditableLine extends Scrollable {
/// The color to use when painting the selection. /// The color to use when painting the selection.
final Color selectionColor; final Color selectionColor;
/// Called when the user requests a change to the selection.
final ValueChanged<TextSelection> onSelectionChanged;
RawEditableTextState createState() => new RawEditableTextState(); RawEditableTextState createState() => new RawEditableTextState();
} }
...@@ -290,6 +298,7 @@ class RawEditableTextState extends ScrollableState<RawEditableLine> { ...@@ -290,6 +298,7 @@ class RawEditableTextState extends ScrollableState<RawEditableLine> {
selectionColor: config.selectionColor, selectionColor: config.selectionColor,
hideText: config.hideText, hideText: config.hideText,
onContentSizeChanged: _handleContentSizeChanged, onContentSizeChanged: _handleContentSizeChanged,
onSelectionChanged: config.onSelectionChanged,
paintOffset: new Offset(-scrollOffset, 0.0) paintOffset: new Offset(-scrollOffset, 0.0)
) )
); );
...@@ -306,6 +315,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ...@@ -306,6 +315,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
this.selectionColor, this.selectionColor,
this.hideText, this.hideText,
this.onContentSizeChanged, this.onContentSizeChanged,
this.onSelectionChanged,
this.paintOffset this.paintOffset
}) : super(key: key); }) : super(key: key);
...@@ -315,7 +325,8 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ...@@ -315,7 +325,8 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
final bool showCursor; final bool showCursor;
final Color selectionColor; final Color selectionColor;
final bool hideText; final bool hideText;
final SizeChangedCallback onContentSizeChanged; final ValueChanged<Size> onContentSizeChanged;
final ValueChanged<TextSelection> onSelectionChanged;
final Offset paintOffset; final Offset paintOffset;
RenderEditableLine createRenderObject() { RenderEditableLine createRenderObject() {
...@@ -326,19 +337,22 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ...@@ -326,19 +337,22 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
selectionColor: selectionColor, selectionColor: selectionColor,
selection: value.selection, selection: value.selection,
onContentSizeChanged: onContentSizeChanged, onContentSizeChanged: onContentSizeChanged,
onSelectionChanged: onSelectionChanged,
paintOffset: paintOffset paintOffset: paintOffset
); );
} }
void updateRenderObject(RenderEditableLine renderObject, void updateRenderObject(RenderEditableLine renderObject,
_EditableLineWidget oldWidget) { _EditableLineWidget oldWidget) {
renderObject.text = _styledTextSpan; renderObject
renderObject.cursorColor = cursorColor; ..text = _styledTextSpan
renderObject.showCursor = showCursor; ..cursorColor = cursorColor
renderObject.selectionColor = selectionColor; ..showCursor = showCursor
renderObject.selection = value.selection; ..selectionColor = selectionColor
renderObject.onContentSizeChanged = onContentSizeChanged; ..selection = value.selection
renderObject.paintOffset = paintOffset; ..onContentSizeChanged = onContentSizeChanged
..onSelectionChanged = onSelectionChanged
..paintOffset = paintOffset;
} }
StyledTextSpan get _styledTextSpan { StyledTextSpan get _styledTextSpan {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment