Commit 8e326d72 authored by Adam Barth's avatar Adam Barth

Add the ability to recognize gestures on text spans

Currently the interface for recognizing gestures on text spans is pretty ugly,
but hopefully we can improve it with time.

Fixes #156
parent 62609669
......@@ -4,6 +4,8 @@
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphStyle, TextBox;
import 'package:flutter/gestures.dart';
import 'basic_types.dart';
import 'text_editing.dart';
import 'text_style.dart';
......@@ -26,7 +28,8 @@ class TextSpan {
const TextSpan({
this.style,
this.text,
this.children
this.children,
this.recognizer
});
/// The style to apply to the text and the children.
......@@ -44,6 +47,9 @@ class TextSpan {
/// children.
final List<TextSpan> children;
/// If non-null, will receive events that hit this text span.
final GestureRecognizer recognizer;
void build(ui.ParagraphBuilder builder) {
final bool hasStyle = style != null;
if (hasStyle)
......@@ -60,13 +66,47 @@ class TextSpan {
builder.pop();
}
void writePlainText(StringBuffer result) {
if (text != null)
result.write(text);
bool visitTextSpan(bool visitor(TextSpan span)) {
if (text != null) {
if (!visitor(this))
return false;
}
if (children != null) {
for (TextSpan child in children)
child.writePlainText(result);
for (TextSpan child in children) {
if (!child.visitTextSpan(visitor))
return false;
}
}
return true;
}
TextSpan getSpanForPosition(TextPosition position) {
TextAffinity affinity = position.affinity;
int targetOffset = position.offset;
int offset = 0;
TextSpan result;
visitTextSpan((TextSpan span) {
assert(result == null);
int endOffset = offset + span.text.length;
if (targetOffset == offset && affinity == TextAffinity.downstream ||
targetOffset > offset && targetOffset < endOffset ||
targetOffset == endOffset && affinity == TextAffinity.upstream) {
result = span;
return false;
}
offset = endOffset;
return true;
});
return result;
}
String toPlainText() {
StringBuffer buffer = new StringBuffer();
visitTextSpan((TextSpan span) {
buffer.write(span.text);
return true;
});
return buffer.toString();
}
String toString([String prefix = '']) {
......@@ -89,9 +129,10 @@ class TextSpan {
final TextSpan typedOther = other;
return typedOther.text == text
&& typedOther.style == style
&& typedOther.recognizer == recognizer
&& _deepEquals(typedOther.children, children);
}
int get hashCode => hashValues(style, text, hashList(children));
int get hashCode => hashValues(style, text, recognizer, hashList(children));
}
/// An object that paints a [TextSpan] into a canvas.
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'box.dart';
import 'object.dart';
import 'semantics.dart';
......@@ -81,6 +83,16 @@ class RenderParagraph extends RenderBox {
bool hitTestSelf(Point position) => true;
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
if (event is! PointerDownEvent)
return;
_layoutText(constraints);
Offset offset = entry.localPosition.toOffset();
TextPosition position = _textPainter.getPositionForOffset(offset);
TextSpan span = _textPainter.text.getSpanForPosition(position);
span?.recognizer?.addPointer(event);
}
void performLayout() {
_layoutText(constraints);
size = constraints.constrain(_textPainter.size);
......@@ -100,9 +112,7 @@ class RenderParagraph extends RenderBox {
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
yield (SemanticsNode node) {
StringBuffer buffer = new StringBuffer();
text.writePlainText(buffer);
node.label = buffer.toString();
node.label = text.toPlainText();
};
}
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart';
void main() {
test('Can tap a hyperlink', () {
testWidgets((WidgetTester tester) {
bool didTapLeft = false;
TapGestureRecognizer tapLeft = new TapGestureRecognizer(
router: Gesturer.instance.pointerRouter,
gestureArena: Gesturer.instance.gestureArena,
onTap: () {
didTapLeft = true;
}
);
bool didTapRight = false;
TapGestureRecognizer tapRight = new TapGestureRecognizer(
router: Gesturer.instance.pointerRouter,
gestureArena: Gesturer.instance.gestureArena,
onTap: () {
didTapRight = true;
}
);
Key textKey = new Key('text');
tester.pumpWidget(
new Center(
child: new RichText(
key: textKey,
text: new TextSpan(
children: <TextSpan>[
new TextSpan(
text: 'xxxxxxxx',
recognizer: tapLeft
),
new TextSpan(text: 'yyyyyyyy'),
new TextSpan(
text: 'zzzzzzzzz',
recognizer: tapRight
),
]
)
)
)
);
Element element = tester.findElementByKey(textKey);
RenderBox box = element.renderObject;
expect(didTapLeft, isFalse);
expect(didTapRight, isFalse);
tester.tapAt(box.localToGlobal(Point.origin) + new Offset(2.0, 2.0));
expect(didTapLeft, isTrue);
expect(didTapRight, isFalse);
didTapLeft = false;
tester.tapAt(box.localToGlobal(Point.origin) + new Offset(30.0, 2.0));
expect(didTapLeft, isTrue);
expect(didTapRight, isFalse);
didTapLeft = false;
tester.tapAt(box.localToGlobal(new Point(box.size.width, 0.0)) + new Offset(-2.0, 2.0));
expect(didTapLeft, isFalse);
expect(didTapRight, isTrue);
});
});
}
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