Commit b89935e5 authored by Adam Barth's avatar Adam Barth

Merge pull request #1496 from abarth/move_caret

Adds the ability to move the caret by tapping
parents 4125f386 36099383
......@@ -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