Unverified Commit 71b9d461 authored by Dan Field's avatar Dan Field Committed by GitHub

Fix inline text span semantics (#34434)

parent 300e8f84
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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.
import 'dart:math' as math;
import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment; import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -766,12 +767,23 @@ class RenderParagraph extends RenderBox ...@@ -766,12 +767,23 @@ class RenderParagraph extends RenderBox
final TextDirection initialDirection = currentDirection; final TextDirection initialDirection = currentDirection;
final TextSelection selection = TextSelection(baseOffset: start, extentOffset: end); final TextSelection selection = TextSelection(baseOffset: start, extentOffset: end);
final List<ui.TextBox> rects = getBoxesForSelection(selection); final List<ui.TextBox> rects = getBoxesForSelection(selection);
Rect rect; if (rects.isEmpty) {
for (ui.TextBox textBox in rects) { return null;
rect ??= textBox.toRect(); }
Rect rect = rects.first.toRect();
currentDirection = rects.first.direction;
for (ui.TextBox textBox in rects.skip(1)) {
rect = rect.expandToInclude(textBox.toRect()); rect = rect.expandToInclude(textBox.toRect());
currentDirection = textBox.direction; currentDirection = textBox.direction;
} }
// Any of the text boxes may have had infinite dimensions.
// We shouldn't pass infinite dimensions up to the bridges.
rect = Rect.fromLTWH(
math.max(0.0, rect.left),
math.max(0.0, rect.top),
math.min(rect.width, constraints.maxWidth),
math.min(rect.height, constraints.maxHeight),
);
// round the current rectangle to make this API testable and add some // round the current rectangle to make this API testable and add some
// padding so that the accessibility rects do not overlap with the text. // padding so that the accessibility rects do not overlap with the text.
// TODO(jonahwilliams): implement this for all text accessibility rects. // TODO(jonahwilliams): implement this for all text accessibility rects.
...@@ -798,12 +810,18 @@ class RenderParagraph extends RenderBox ...@@ -798,12 +810,18 @@ class RenderParagraph extends RenderBox
if (current != start) { if (current != start) {
final SemanticsNode node = SemanticsNode(); final SemanticsNode node = SemanticsNode();
final SemanticsConfiguration configuration = buildSemanticsConfig(current, start); final SemanticsConfiguration configuration = buildSemanticsConfig(current, start);
if (configuration == null) {
continue;
}
node.updateWith(config: configuration); node.updateWith(config: configuration);
node.rect = currentRect; node.rect = currentRect;
newChildren.add(node); newChildren.add(node);
} }
final dynamic inlineElement = _inlineSemanticsElements[j]; final dynamic inlineElement = _inlineSemanticsElements[j];
final SemanticsConfiguration configuration = buildSemanticsConfig(start, end); final SemanticsConfiguration configuration = buildSemanticsConfig(start, end);
if (configuration == null) {
continue;
}
if (inlineElement != null) { if (inlineElement != null) {
// Add semantics for this recognizer. // Add semantics for this recognizer.
final SemanticsNode node = SemanticsNode(); final SemanticsNode node = SemanticsNode();
...@@ -842,9 +860,11 @@ class RenderParagraph extends RenderBox ...@@ -842,9 +860,11 @@ class RenderParagraph extends RenderBox
if (current < rawLabel.length) { if (current < rawLabel.length) {
final SemanticsNode node = SemanticsNode(); final SemanticsNode node = SemanticsNode();
final SemanticsConfiguration configuration = buildSemanticsConfig(current, rawLabel.length); final SemanticsConfiguration configuration = buildSemanticsConfig(current, rawLabel.length);
node.updateWith(config: configuration); if (configuration != null) {
node.rect = currentRect; node.updateWith(config: configuration);
newChildren.add(node); node.rect = currentRect;
newChildren.add(node);
}
} }
node.updateWith(config: config, childrenInInversePaintOrder: newChildren); node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
} }
......
...@@ -1162,6 +1162,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1162,6 +1162,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
Rect _rect = Rect.zero; Rect _rect = Rect.zero;
set rect(Rect value) { set rect(Rect value) {
assert(value != null); assert(value != null);
assert(value.isFinite, '$this (with $owner) tried to set a non-finite rect.');
if (_rect != value) { if (_rect != value) {
_rect = value; _rect = value;
_markDirty(); _markDirty();
...@@ -2178,6 +2179,7 @@ class _BoxEdge implements Comparable<_BoxEdge> { ...@@ -2178,6 +2179,7 @@ class _BoxEdge implements Comparable<_BoxEdge> {
@required this.node, @required this.node,
}) : assert(isLeadingEdge != null), }) : assert(isLeadingEdge != null),
assert(offset != null), assert(offset != null),
assert(offset.isFinite),
assert(node != null); assert(node != null);
/// True if the edge comes before the seconds edge along the traversal /// True if the edge comes before the seconds edge along the traversal
...@@ -2384,6 +2386,7 @@ Offset _pointInParentCoordinates(SemanticsNode node, Offset point) { ...@@ -2384,6 +2386,7 @@ Offset _pointInParentCoordinates(SemanticsNode node, Offset point) {
List<SemanticsNode> _childrenInDefaultOrder(List<SemanticsNode> children, TextDirection textDirection) { List<SemanticsNode> _childrenInDefaultOrder(List<SemanticsNode> children, TextDirection textDirection) {
final List<_BoxEdge> edges = <_BoxEdge>[]; final List<_BoxEdge> edges = <_BoxEdge>[];
for (SemanticsNode child in children) { for (SemanticsNode child in children) {
assert(child.rect.isFinite);
// Using a small delta to shrink child rects removes overlapping cases. // Using a small delta to shrink child rects removes overlapping cases.
final Rect childRect = child.rect.deflate(0.1); final Rect childRect = child.rect.deflate(0.1);
edges.add(_BoxEdge( edges.add(_BoxEdge(
......
...@@ -185,6 +185,45 @@ void main() { ...@@ -185,6 +185,45 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('recognizers split semantic node when TextSpan overflows', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
await tester.pumpWidget(
SizedBox(
height: 10,
child: Text.rich(
TextSpan(
children: <TextSpan>[
const TextSpan(text: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'),
TextSpan(text: 'world', recognizer: TapGestureRecognizer()..onTap = () { }),
],
style: textStyle,
),
textDirection: TextDirection.ltr,
),
),
);
final TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
label: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'world',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[SemanticsAction.tap]
),
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('recognizers split semantic nodes with text span labels', (WidgetTester tester) async { testWidgets('recognizers split semantic nodes with text span labels', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontFamily: 'Ahem'); const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
......
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