Unverified Commit 44cdb049 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Update Text/Rich text to describe recognizers in semantics (#20676)

parent a7d954eb
......@@ -78,6 +78,7 @@ class RenderParagraph extends RenderBox {
case RenderComparison.paint:
_textPainter.text = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
break;
case RenderComparison.layout:
_textPainter.text = value;
......@@ -437,12 +438,105 @@ class RenderParagraph extends RenderBox {
return _textPainter.size;
}
final List<int> _recognizerOffsets = <int>[];
final List<GestureRecognizer> _recognizers = <GestureRecognizer>[];
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config
..label = text.toPlainText()
..textDirection = textDirection;
_recognizerOffsets.clear();
_recognizers.clear();
int offset = 0;
text.visitTextSpan((TextSpan span) {
if (span.recognizer != null && (span.recognizer is TapGestureRecognizer || span.recognizer is LongPressGestureRecognizer)) {
_recognizerOffsets.add(offset);
_recognizerOffsets.add(offset + span.text.length);
_recognizers.add(span.recognizer);
}
offset += span.text.length;
return true;
});
if (_recognizerOffsets.isNotEmpty) {
config.explicitChildNodes = true;
config.isSemanticBoundary = true;
} else {
config.label = text.toPlainText();
config.textDirection = textDirection;
}
}
@override
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
assert(_recognizerOffsets.isNotEmpty);
assert(_recognizerOffsets.length.isEven);
assert(_recognizers.isNotEmpty);
assert(children.isEmpty);
final List<SemanticsNode> newChildren = <SemanticsNode>[];
final String rawLabel = text.toPlainText();
int current = 0;
double order = -1.0;
TextDirection currentDirection = textDirection;
Rect currentRect;
SemanticsConfiguration buildSemanticsConfig(int start, int end) {
final TextDirection initialDirection = currentDirection;
final TextSelection selection = new TextSelection(baseOffset: start, extentOffset: end);
final List<ui.TextBox> rects = getBoxesForSelection(selection);
Rect rect;
for (ui.TextBox textBox in rects) {
rect ??= textBox.toRect();
rect = rect.expandToInclude(textBox.toRect());
currentDirection = textBox.direction;
}
// round the current rectangle to make this API testable and add some
// padding so that the accessibility rects do not overlap with the text.
// TODO(jonahwilliams): implement this for all text accessibility rects.
currentRect = new Rect.fromLTRB(
rect.left.floorToDouble() - 4.0,
rect.top.floorToDouble() - 4.0,
rect.right.ceilToDouble() + 4.0,
rect.bottom.ceilToDouble() + 4.0,
);
order += 1;
return new SemanticsConfiguration()
..sortKey = new OrdinalSortKey(order)
..textDirection = initialDirection
..label = rawLabel.substring(start, end);
}
for (int i = 0, j = 0; i < _recognizerOffsets.length; i += 2, j++) {
final int start = _recognizerOffsets[i];
final int end = _recognizerOffsets[i + 1];
if (current != start) {
final SemanticsNode node = new SemanticsNode();
final SemanticsConfiguration configuration = buildSemanticsConfig(current, start);
node.updateWith(config: configuration);
node.rect = currentRect;
newChildren.add(node);
}
final SemanticsNode node = new SemanticsNode();
final SemanticsConfiguration configuration = buildSemanticsConfig(start, end);
final GestureRecognizer recognizer = _recognizers[j];
if (recognizer is TapGestureRecognizer) {
configuration.onTap = recognizer.onTap;
} else if (recognizer is LongPressGestureRecognizer) {
configuration.onLongPress = recognizer.onLongPress;
} else {
assert(false);
}
node.updateWith(config: configuration);
node.rect = currentRect;
newChildren.add(node);
current = end;
}
if (current < rawLabel.length) {
final SemanticsNode node = new SemanticsNode();
final SemanticsConfiguration configuration = buildSemanticsConfig(current, rawLabel.length);
node.updateWith(config: configuration);
node.rect = currentRect;
newChildren.add(node);
}
node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
}
@override
......
......@@ -2,8 +2,10 @@
// 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
import 'semantics_tester.dart';
......@@ -136,4 +138,116 @@ void main() {
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('recognizers split semantic node', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
await tester.pumpWidget(
new Text.rich(
new TextSpan(
children: <TextSpan>[
const TextSpan(text: 'hello '),
new TextSpan(text: 'world', recognizer: new TapGestureRecognizer()..onTap = () {}),
const TextSpan(text: ' this is a '),
const TextSpan(text: 'cat-astrophe'),
],
style: textStyle,
),
textDirection: TextDirection.ltr,
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics>[
new TestSemantics(
rect: new Rect.fromLTRB(-4.0, -4.0, 88.0, 18.0),
label: 'hello ',
textDirection: TextDirection.ltr,
),
new TestSemantics(
rect: new Rect.fromLTRB(80.0, -4.0, 158.0, 18.0),
label: 'world',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
],
),
new TestSemantics(
rect: new Rect.fromLTRB(150.0, -4.0, 480.0, 18.0),
label: ' this is a cat-astrophe',
textDirection: TextDirection.ltr,
)
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true));
semantics.dispose();
});
testWidgets('recognizers split semantic node - bidi', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
await tester.pumpWidget(
new RichText(
text: new TextSpan(
style: textStyle,
children: <TextSpan>[
const TextSpan(text: 'hello world${Unicode.RLE}${Unicode.RLO} '),
new TextSpan(text: 'BOY', recognizer: new LongPressGestureRecognizer()..onLongPress = () {}),
const TextSpan(text: ' HOW DO${Unicode.PDF} you ${Unicode.RLO} DO '),
new TextSpan(text: 'SIR', recognizer: new TapGestureRecognizer()..onTap = () {}),
const TextSpan(text: '${Unicode.PDF}${Unicode.PDF} good bye'),
],
),
textDirection: TextDirection.ltr,
)
);
// The expected visual order of the text is:
// hello world RIS OD you OD WOH YOB good bye
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics>[
new TestSemantics(
rect: new Rect.fromLTRB(-4.0, -4.0, 480.0, 18.0),
label: 'hello world ',
textDirection: TextDirection.ltr, // text direction is declared as LTR.
),
new TestSemantics(
rect: new Rect.fromLTRB(150.0, -4.0, 200.0, 18.0),
label: 'RIS',
textDirection: TextDirection.rtl, // in the last string we switched to RTL using RLE.
actions: <SemanticsAction>[
SemanticsAction.tap,
],
),
new TestSemantics(
rect: new Rect.fromLTRB(192.0, -4.0, 424.0, 18.0),
label: ' OD you OD WOH ', // Still RTL.
textDirection: TextDirection.rtl,
),
new TestSemantics(
rect: new Rect.fromLTRB(416.0, -4.0, 466.0, 18.0),
label: 'YOB',
textDirection: TextDirection.rtl, // Still RTL.
actions: <SemanticsAction>[
SemanticsAction.longPress,
],
),
new TestSemantics(
rect: new Rect.fromLTRB(472.0, -4.0, 606.0, 18.0),
label: ' good bye',
textDirection: TextDirection.rtl, // Begin as RTL but pop to LTR.
),
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true));
semantics.dispose();
}, skip: true); // TODO(jonahwilliams): correct once https://github.com/flutter/flutter/issues/20891 is resolved.
}
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