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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment;
import 'package:flutter/foundation.dart';
......@@ -766,12 +767,23 @@ class RenderParagraph extends RenderBox
final TextDirection initialDirection = currentDirection;
final TextSelection selection = TextSelection(baseOffset: start, extentOffset: end);
final List<ui.TextBox> rects = getBoxesForSelection(selection);
Rect rect;
for (ui.TextBox textBox in rects) {
rect ??= textBox.toRect();
if (rects.isEmpty) {
return null;
}
Rect rect = rects.first.toRect();
currentDirection = rects.first.direction;
for (ui.TextBox textBox in rects.skip(1)) {
rect = rect.expandToInclude(textBox.toRect());
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
// padding so that the accessibility rects do not overlap with the text.
// TODO(jonahwilliams): implement this for all text accessibility rects.
......@@ -798,12 +810,18 @@ class RenderParagraph extends RenderBox
if (current != start) {
final SemanticsNode node = SemanticsNode();
final SemanticsConfiguration configuration = buildSemanticsConfig(current, start);
if (configuration == null) {
continue;
}
node.updateWith(config: configuration);
node.rect = currentRect;
newChildren.add(node);
}
final dynamic inlineElement = _inlineSemanticsElements[j];
final SemanticsConfiguration configuration = buildSemanticsConfig(start, end);
if (configuration == null) {
continue;
}
if (inlineElement != null) {
// Add semantics for this recognizer.
final SemanticsNode node = SemanticsNode();
......@@ -842,9 +860,11 @@ class RenderParagraph extends RenderBox
if (current < rawLabel.length) {
final SemanticsNode node = SemanticsNode();
final SemanticsConfiguration configuration = buildSemanticsConfig(current, rawLabel.length);
node.updateWith(config: configuration);
node.rect = currentRect;
newChildren.add(node);
if (configuration != null) {
node.updateWith(config: configuration);
node.rect = currentRect;
newChildren.add(node);
}
}
node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
}
......
......@@ -1162,6 +1162,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
Rect _rect = Rect.zero;
set rect(Rect value) {
assert(value != null);
assert(value.isFinite, '$this (with $owner) tried to set a non-finite rect.');
if (_rect != value) {
_rect = value;
_markDirty();
......@@ -2178,6 +2179,7 @@ class _BoxEdge implements Comparable<_BoxEdge> {
@required this.node,
}) : assert(isLeadingEdge != null),
assert(offset != null),
assert(offset.isFinite),
assert(node != null);
/// True if the edge comes before the seconds edge along the traversal
......@@ -2384,6 +2386,7 @@ Offset _pointInParentCoordinates(SemanticsNode node, Offset point) {
List<SemanticsNode> _childrenInDefaultOrder(List<SemanticsNode> children, TextDirection textDirection) {
final List<_BoxEdge> edges = <_BoxEdge>[];
for (SemanticsNode child in children) {
assert(child.rect.isFinite);
// Using a small delta to shrink child rects removes overlapping cases.
final Rect childRect = child.rect.deflate(0.1);
edges.add(_BoxEdge(
......
......@@ -185,6 +185,45 @@ void main() {
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 {
final SemanticsTester semantics = SemanticsTester(tester);
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