From 70973e02147e2d019a4405c8b1e975ac498be7cb Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Fri, 2 Oct 2020 10:57:05 -0700 Subject: [PATCH] Handle empty selection box lists in RenderParagraph.assembleSemanticsNode (#67017) --- .../flutter/lib/src/rendering/paragraph.dart | 2 +- .../test/rendering/paragraph_test.dart | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index d18a15019d..a8d2faa229 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -891,6 +891,7 @@ class RenderParagraph extends RenderBox extentOffset: start + info.text.length, ); final List rects = getBoxesForSelection(selection); + start += info.text.length; if (rects.isEmpty) { continue; } @@ -958,7 +959,6 @@ class RenderParagraph extends RenderBox newChildCache.addLast(newChild); newChildren.add(newChild); } - start += info.text.length; } _cachedChildNodes = newChildCache; node.updateWith(config: config, childrenInInversePaintOrder: newChildren); diff --git a/packages/flutter/test/rendering/paragraph_test.dart b/packages/flutter/test/rendering/paragraph_test.dart index 71ed2fb0de..d993859bfe 100644 --- a/packages/flutter/test/rendering/paragraph_test.dart +++ b/packages/flutter/test/rendering/paragraph_test.dart @@ -16,6 +16,28 @@ import 'rendering_tester.dart'; const String _kText = "I polished up that handle so carefullee\nThat now I am the Ruler of the Queen's Navee!"; +// A subclass of RenderParagraph that returns an empty list in getBoxesForSelection +// for a given TextSelection. +// This is intended to simulate SkParagraph's implementation of Paragraph.getBoxesForRange, +// which may return an empty list in some situations where Libtxt would return a list +// containing an empty box. +class RenderParagraphWithEmptySelectionBoxList extends RenderParagraph { + RenderParagraphWithEmptySelectionBoxList(InlineSpan text, { + TextDirection textDirection, + this.emptyListSelection, + }) : super(text, textDirection: textDirection); + + TextSelection emptyListSelection; + + @override + List getBoxesForSelection(TextSelection selection) { + if (selection == emptyListSelection) { + return []; + } + return super.getBoxesForSelection(selection); + } +} + void main() { test('getOffsetForCaret control test', () { final RenderParagraph paragraph = RenderParagraph( @@ -546,4 +568,21 @@ void main() { } expect(failed, true); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020 + + test('assembleSemanticsNode handles text spans that do not yield selection boxes', () { + final RenderParagraph paragraph = RenderParagraphWithEmptySelectionBoxList( + TextSpan(text: '', children: [ + TextSpan(text: 'A', recognizer: TapGestureRecognizer()..onTap = () {}), + TextSpan(text: 'B', recognizer: TapGestureRecognizer()..onTap = () {}), + TextSpan(text: 'C', recognizer: TapGestureRecognizer()..onTap = () {}), + ]), + textDirection: TextDirection.rtl, + emptyListSelection: const TextSelection(baseOffset: 0, extentOffset: 1), + ); + layout(paragraph); + + final SemanticsNode node = SemanticsNode(); + paragraph.assembleSemanticsNode(node, SemanticsConfiguration(), []); + expect(node.childrenCount, 2); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020 }