Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
aaa7f842
Unverified
Commit
aaa7f842
authored
Mar 11, 2021
by
chunhtai
Committed by
GitHub
Mar 11, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix gesture recognizer in selectable rich text should be focusable in… (#77730)
parent
38fd5af5
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
196 additions
and
38 deletions
+196
-38
inline_span.dart
packages/flutter/lib/src/painting/inline_span.dart
+36
-0
editable.dart
packages/flutter/lib/src/rendering/editable.dart
+107
-1
paragraph.dart
packages/flutter/lib/src/rendering/paragraph.dart
+2
-37
selectable_text_test.dart
packages/flutter/test/widgets/selectable_text_test.dart
+51
-0
No files found.
packages/flutter/lib/src/painting/inline_span.dart
View file @
aaa7f842
...
@@ -100,6 +100,42 @@ class InlineSpanSemanticsInformation {
...
@@ -100,6 +100,42 @@ class InlineSpanSemanticsInformation {
String
toString
()
=>
'
${objectRuntimeType(this, 'InlineSpanSemanticsInformation')}
{text:
$text
, semanticsLabel:
$semanticsLabel
, recognizer:
$recognizer
}'
;
String
toString
()
=>
'
${objectRuntimeType(this, 'InlineSpanSemanticsInformation')}
{text:
$text
, semanticsLabel:
$semanticsLabel
, recognizer:
$recognizer
}'
;
}
}
/// Combines _semanticsInfo entries where permissible.
///
/// Consecutive inline spans can be combined if their
/// [InlineSpanSemanticsInformation.requiresOwnNode] return false.
List
<
InlineSpanSemanticsInformation
>
combineSemanticsInfo
(
List
<
InlineSpanSemanticsInformation
>
infoList
)
{
final
List
<
InlineSpanSemanticsInformation
>
combined
=
<
InlineSpanSemanticsInformation
>[];
String
workingText
=
''
;
// TODO(ianh): this algorithm is internally inconsistent. workingText
// never becomes null, but we check for it being so below.
String
?
workingLabel
;
for
(
final
InlineSpanSemanticsInformation
info
in
infoList
)
{
if
(
info
.
requiresOwnNode
)
{
combined
.
add
(
InlineSpanSemanticsInformation
(
workingText
,
semanticsLabel:
workingLabel
??
workingText
,
));
workingText
=
''
;
workingLabel
=
null
;
combined
.
add
(
info
);
}
else
{
workingText
+=
info
.
text
;
workingLabel
??=
''
;
if
(
info
.
semanticsLabel
!=
null
)
{
workingLabel
+=
info
.
semanticsLabel
!;
}
else
{
workingLabel
+=
info
.
text
;
}
}
}
combined
.
add
(
InlineSpanSemanticsInformation
(
workingText
,
semanticsLabel:
workingLabel
,
));
return
combined
;
}
/// An immutable span of inline content which forms part of a paragraph.
/// An immutable span of inline content which forms part of a paragraph.
///
///
/// * The subclass [TextSpan] specifies text and may contain child [InlineSpan]s.
/// * The subclass [TextSpan] specifies text and may contain child [InlineSpan]s.
...
...
packages/flutter/lib/src/rendering/editable.dart
View file @
aaa7f842
...
@@ -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:collection'
;
import
'dart:math'
as
math
;
import
'dart:math'
as
math
;
import
'dart:ui'
as
ui
show
TextBox
,
BoxHeightStyle
,
BoxWidthStyle
;
import
'dart:ui'
as
ui
show
TextBox
,
BoxHeightStyle
,
BoxWidthStyle
;
...
@@ -1474,10 +1475,34 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
...
@@ -1474,10 +1475,34 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
}
}
}
/// Collected during [describeSemanticsConfiguration], used by
/// [assembleSemanticsNode] and [_combineSemanticsInfo].
List
<
InlineSpanSemanticsInformation
>?
_semanticsInfo
;
// Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
// can be re-used when [assembleSemanticsNode] is called again. This ensures
// stable ids for the [SemanticsNode]s of [TextSpan]s across
// [assembleSemanticsNode] invocations.
Queue
<
SemanticsNode
>?
_cachedChildNodes
;
@override
@override
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
super
.
describeSemanticsConfiguration
(
config
);
super
.
describeSemanticsConfiguration
(
config
);
_semanticsInfo
=
_textPainter
.
text
!.
getSemanticsInformation
();
// TODO(chunhtai): the macOS does not provide a public API to support text
// selections across multiple semantics nodes. Remove this platform check
// once we can support it.
// https://github.com/flutter/flutter/issues/77957
if
(
_semanticsInfo
!.
any
((
InlineSpanSemanticsInformation
info
)
=>
info
.
recognizer
!=
null
)
&&
defaultTargetPlatform
!=
TargetPlatform
.
macOS
)
{
assert
(
readOnly
&&
!
obscureText
);
// For Selectable rich text with recognizer, we need to create a semantics
// node for each text fragment.
config
..
isSemanticBoundary
=
true
..
explicitChildNodes
=
true
;
return
;
}
config
config
..
value
=
obscureText
..
value
=
obscureText
?
obscuringCharacter
*
_plainText
.
length
?
obscuringCharacter
*
_plainText
.
length
...
@@ -1520,6 +1545,87 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
...
@@ -1520,6 +1545,87 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
);
);
}
}
@override
void
assembleSemanticsNode
(
SemanticsNode
node
,
SemanticsConfiguration
config
,
Iterable
<
SemanticsNode
>
children
)
{
assert
(
_semanticsInfo
!=
null
&&
_semanticsInfo
!.
isNotEmpty
);
final
List
<
SemanticsNode
>
newChildren
=
<
SemanticsNode
>[];
TextDirection
currentDirection
=
textDirection
;
Rect
currentRect
;
double
ordinal
=
0.0
;
int
start
=
0
;
final
Queue
<
SemanticsNode
>
newChildCache
=
Queue
<
SemanticsNode
>();
for
(
final
InlineSpanSemanticsInformation
info
in
combineSemanticsInfo
(
_semanticsInfo
!))
{
assert
(!
info
.
isPlaceholder
);
final
TextSelection
selection
=
TextSelection
(
baseOffset:
start
,
extentOffset:
start
+
info
.
text
.
length
,
);
start
+=
info
.
text
.
length
;
final
TextDirection
initialDirection
=
currentDirection
;
final
List
<
ui
.
TextBox
>
rects
=
_textPainter
.
getBoxesForSelection
(
selection
);
if
(
rects
.
isEmpty
)
{
continue
;
}
Rect
rect
=
rects
.
first
.
toRect
();
currentDirection
=
rects
.
first
.
direction
;
for
(
final
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.
currentRect
=
Rect
.
fromLTRB
(
rect
.
left
.
floorToDouble
()
-
4.0
,
rect
.
top
.
floorToDouble
()
-
4.0
,
rect
.
right
.
ceilToDouble
()
+
4.0
,
rect
.
bottom
.
ceilToDouble
()
+
4.0
,
);
final
SemanticsConfiguration
configuration
=
SemanticsConfiguration
()
..
sortKey
=
OrdinalSortKey
(
ordinal
++)
..
textDirection
=
initialDirection
..
label
=
info
.
semanticsLabel
??
info
.
text
;
final
GestureRecognizer
?
recognizer
=
info
.
recognizer
;
if
(
recognizer
!=
null
)
{
if
(
recognizer
is
TapGestureRecognizer
)
{
if
(
recognizer
.
onTap
!=
null
)
{
configuration
.
onTap
=
recognizer
.
onTap
;
configuration
.
isLink
=
true
;
}
}
else
if
(
recognizer
is
DoubleTapGestureRecognizer
)
{
if
(
recognizer
.
onDoubleTap
!=
null
)
{
configuration
.
onTap
=
recognizer
.
onDoubleTap
;
configuration
.
isLink
=
true
;
}
}
else
if
(
recognizer
is
LongPressGestureRecognizer
)
{
if
(
recognizer
.
onLongPress
!=
null
)
{
configuration
.
onLongPress
=
recognizer
.
onLongPress
;
}
}
else
{
assert
(
false
,
'
${recognizer.runtimeType}
is not supported.'
);
}
}
final
SemanticsNode
newChild
=
(
_cachedChildNodes
?.
isNotEmpty
==
true
)
?
_cachedChildNodes
!.
removeFirst
()
:
SemanticsNode
();
newChild
..
updateWith
(
config:
configuration
)
..
rect
=
currentRect
;
newChildCache
.
addLast
(
newChild
);
newChildren
.
add
(
newChild
);
}
_cachedChildNodes
=
newChildCache
;
node
.
updateWith
(
config:
config
,
childrenInInversePaintOrder:
newChildren
);
}
// TODO(ianh): in theory, [selection] could become null between when
// TODO(ianh): in theory, [selection] could become null between when
// we last called describeSemanticsConfiguration and when the
// we last called describeSemanticsConfiguration and when the
// callbacks are invoked, in which case the callbacks will crash...
// callbacks are invoked, in which case the callbacks will crash...
...
...
packages/flutter/lib/src/rendering/paragraph.dart
View file @
aaa7f842
...
@@ -867,41 +867,6 @@ class RenderParagraph extends RenderBox
...
@@ -867,41 +867,6 @@ class RenderParagraph extends RenderBox
/// [assembleSemanticsNode] and [_combineSemanticsInfo].
/// [assembleSemanticsNode] and [_combineSemanticsInfo].
List
<
InlineSpanSemanticsInformation
>?
_semanticsInfo
;
List
<
InlineSpanSemanticsInformation
>?
_semanticsInfo
;
/// Combines _semanticsInfo entries where permissible, determined by
/// [InlineSpanSemanticsInformation.requiresOwnNode].
List
<
InlineSpanSemanticsInformation
>
_combineSemanticsInfo
()
{
assert
(
_semanticsInfo
!=
null
);
final
List
<
InlineSpanSemanticsInformation
>
combined
=
<
InlineSpanSemanticsInformation
>[];
String
workingText
=
''
;
// TODO(ianh): this algorithm is internally inconsistent. workingText
// never becomes null, but we check for it being so below.
String
?
workingLabel
;
for
(
final
InlineSpanSemanticsInformation
info
in
_semanticsInfo
!)
{
if
(
info
.
requiresOwnNode
)
{
combined
.
add
(
InlineSpanSemanticsInformation
(
workingText
,
semanticsLabel:
workingLabel
??
workingText
,
));
workingText
=
''
;
workingLabel
=
null
;
combined
.
add
(
info
);
}
else
{
workingText
+=
info
.
text
;
workingLabel
??=
''
;
if
(
info
.
semanticsLabel
!=
null
)
{
workingLabel
+=
info
.
semanticsLabel
!;
}
else
{
workingLabel
+=
info
.
text
;
}
}
}
combined
.
add
(
InlineSpanSemanticsInformation
(
workingText
,
semanticsLabel:
workingLabel
,
));
return
combined
;
}
@override
@override
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
super
.
describeSemanticsConfiguration
(
config
);
super
.
describeSemanticsConfiguration
(
config
);
...
@@ -938,7 +903,7 @@ class RenderParagraph extends RenderBox
...
@@ -938,7 +903,7 @@ class RenderParagraph extends RenderBox
int
childIndex
=
0
;
int
childIndex
=
0
;
RenderBox
?
child
=
firstChild
;
RenderBox
?
child
=
firstChild
;
final
Queue
<
SemanticsNode
>
newChildCache
=
Queue
<
SemanticsNode
>();
final
Queue
<
SemanticsNode
>
newChildCache
=
Queue
<
SemanticsNode
>();
for
(
final
InlineSpanSemanticsInformation
info
in
_combineSemanticsInfo
(
))
{
for
(
final
InlineSpanSemanticsInformation
info
in
combineSemanticsInfo
(
_semanticsInfo
!
))
{
final
TextSelection
selection
=
TextSelection
(
final
TextSelection
selection
=
TextSelection
(
baseOffset:
start
,
baseOffset:
start
,
extentOffset:
start
+
info
.
text
.
length
,
extentOffset:
start
+
info
.
text
.
length
,
...
@@ -946,7 +911,7 @@ class RenderParagraph extends RenderBox
...
@@ -946,7 +911,7 @@ class RenderParagraph extends RenderBox
start
+=
info
.
text
.
length
;
start
+=
info
.
text
.
length
;
if
(
info
.
isPlaceholder
)
{
if
(
info
.
isPlaceholder
)
{
// A placeholder span may have 0 to multple semantics nodes, we need
// A placeholder span may have 0 to mult
i
ple semantics nodes, we need
// to annotate all of the semantics nodes belong to this span.
// to annotate all of the semantics nodes belong to this span.
while
(
children
.
length
>
childIndex
&&
while
(
children
.
length
>
childIndex
&&
children
.
elementAt
(
childIndex
).
isTagged
(
PlaceholderSpanIndexSemanticsTag
(
placeholderIndex
)))
{
children
.
elementAt
(
childIndex
).
isTagged
(
PlaceholderSpanIndexSemanticsTag
(
placeholderIndex
)))
{
...
...
packages/flutter/test/widgets/selectable_text_test.dart
View file @
aaa7f842
...
@@ -1444,6 +1444,57 @@ void main() {
...
@@ -1444,6 +1444,57 @@ void main() {
semantics
.
dispose
();
semantics
.
dispose
();
});
});
testWidgets
(
'Selectable rich text with gesture recognizer has correct semantics'
,
(
WidgetTester
tester
)
async
{
final
SemanticsTester
semantics
=
SemanticsTester
(
tester
);
await
tester
.
pumpWidget
(
overlay
(
child:
SelectableText
.
rich
(
TextSpan
(
children:
<
TextSpan
>[
const
TextSpan
(
text:
'text'
),
TextSpan
(
text:
'link'
,
recognizer:
TapGestureRecognizer
()
..
onTap
=
()
{
},
),
],
),
),
),
);
expect
(
semantics
,
hasSemantics
(
TestSemantics
.
root
(
children:
<
TestSemantics
>[
TestSemantics
.
rootChild
(
id:
1
,
actions:
<
SemanticsAction
>[
SemanticsAction
.
longPress
],
textDirection:
TextDirection
.
ltr
,
children:
<
TestSemantics
>[
TestSemantics
(
id:
2
,
children:
<
TestSemantics
>[
TestSemantics
(
id:
3
,
label:
'text'
,
textDirection:
TextDirection
.
ltr
,
),
TestSemantics
(
id:
4
,
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isLink
],
actions:
<
SemanticsAction
>[
SemanticsAction
.
tap
],
label:
'link'
,
textDirection:
TextDirection
.
ltr
,
),
],
),
],
),
],
),
ignoreTransform:
true
,
ignoreRect:
true
));
semantics
.
dispose
();
});
group
(
'Keyboard Tests'
,
()
{
group
(
'Keyboard Tests'
,
()
{
late
TextEditingController
controller
;
late
TextEditingController
controller
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment