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> {
}
}
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<Input> {
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<Input> {
style: textStyle,
hideText: config.hideText,
cursorColor: cursorColor,
selectionColor: cursorColor
selectionColor: cursorColor,
onSelectionChanged: _handleSelectionChanged
)
));
......@@ -258,15 +278,7 @@ class _InputState extends State<Input> {
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
......
......@@ -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,
/// 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
}
import 'dart:ui' show TextAffinity, TextPosition;
/// 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)';
}
}
......@@ -265,4 +265,9 @@ class TextPainter {
return _paragraph.getBoxesForRange(selection.start, selection.end);
}
TextPosition getPositionForOffset(Offset offset) {
assert(!_needsLayout);
return _paragraph.getPositionForOffset(offset);
}
}
......@@ -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<Size> onContentSizeChanged;
ValueChanged<TextSelection> 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.
......
......@@ -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<Size> 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.
......
......@@ -819,7 +819,7 @@ class SizeObserver extends OneChildRenderObjectWidget {
}
/// The callback to call whenever the child's layout size changes
final SizeChangedCallback onSizeChanged;
final ValueChanged<Size> onSizeChanged;
RenderSizeObserver createRenderObject() => new RenderSizeObserver(onSizeChanged: onSizeChanged);
......
......@@ -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<TextSelection> onSelectionChanged;
RawEditableTextState createState() => new RawEditableTextState();
}
......@@ -290,6 +298,7 @@ class RawEditableTextState extends ScrollableState<RawEditableLine> {
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<Size> onContentSizeChanged;
final ValueChanged<TextSelection> 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 {
......
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