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
67cee630
Unverified
Commit
67cee630
authored
Jul 30, 2021
by
chunhtai
Committed by
GitHub
Jul 30, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add string attribute api to text span (#86667)
parent
3ca8d7b7
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
484 additions
and
31 deletions
+484
-31
inline_span.dart
packages/flutter/lib/src/painting/inline_span.dart
+26
-12
text_span.dart
packages/flutter/lib/src/painting/text_span.dart
+52
-3
editable.dart
packages/flutter/lib/src/rendering/editable.dart
+31
-5
paragraph.dart
packages/flutter/lib/src/rendering/paragraph.dart
+29
-6
text_span_test.dart
packages/flutter/test/painting/text_span_test.dart
+49
-0
rich_text_test.dart
packages/flutter/test/widgets/rich_text_test.dart
+98
-1
selectable_text_test.dart
packages/flutter/test/widgets/selectable_text_test.dart
+66
-0
semantics_tester.dart
packages/flutter/test/widgets/semantics_tester.dart
+53
-2
matchers.dart
packages/flutter_test/lib/src/matchers.dart
+80
-2
No files found.
packages/flutter/lib/src/painting/inline_span.dart
View file @
67cee630
...
...
@@ -3,7 +3,7 @@
// found in the LICENSE file.
import
'dart:ui'
as
ui
show
ParagraphBuilder
;
import
'dart:ui'
as
ui
show
ParagraphBuilder
,
StringAttribute
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
...
...
@@ -56,6 +56,7 @@ class InlineSpanSemanticsInformation {
this
.
text
,
{
this
.
isPlaceholder
=
false
,
this
.
semanticsLabel
,
this
.
stringAttributes
=
const
<
ui
.
StringAttribute
>[],
this
.
recognizer
,
})
:
assert
(
text
!=
null
),
assert
(
isPlaceholder
!=
null
),
...
...
@@ -84,13 +85,17 @@ class InlineSpanSemanticsInformation {
/// [isPlaceholder] is true.
final
bool
requiresOwnNode
;
/// The string attributes attached to this semantics information
final
List
<
ui
.
StringAttribute
>
stringAttributes
;
@override
bool
operator
==(
Object
other
)
{
return
other
is
InlineSpanSemanticsInformation
&&
other
.
text
==
text
&&
other
.
semanticsLabel
==
semanticsLabel
&&
other
.
recognizer
==
recognizer
&&
other
.
isPlaceholder
==
isPlaceholder
;
&&
other
.
isPlaceholder
==
isPlaceholder
&&
listEquals
<
ui
.
StringAttribute
>(
other
.
stringAttributes
,
stringAttributes
);
}
@override
...
...
@@ -107,31 +112,40 @@ class InlineSpanSemanticsInformation {
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
;
String
workingLabel
=
''
;
List
<
ui
.
StringAttribute
>
workingAttributes
=
<
ui
.
StringAttribute
>[];
for
(
final
InlineSpanSemanticsInformation
info
in
infoList
)
{
if
(
info
.
requiresOwnNode
)
{
combined
.
add
(
InlineSpanSemanticsInformation
(
workingText
,
semanticsLabel:
workingLabel
??
workingText
,
semanticsLabel:
workingLabel
,
stringAttributes:
workingAttributes
,
));
workingText
=
''
;
workingLabel
=
null
;
workingLabel
=
''
;
workingAttributes
=
<
ui
.
StringAttribute
>[];
combined
.
add
(
info
);
}
else
{
workingText
+=
info
.
text
;
workingLabel
??=
''
;
if
(
info
.
semanticsLabel
!=
null
)
{
workingLabel
+=
info
.
semanticsLabel
!;
}
else
{
workingLabel
+=
info
.
text
;
final
String
effectiveLabel
=
info
.
semanticsLabel
??
info
.
text
;
for
(
final
ui
.
StringAttribute
infoAttribute
in
info
.
stringAttributes
)
{
workingAttributes
.
add
(
infoAttribute
.
copy
(
range:
TextRange
(
start:
infoAttribute
.
range
.
start
+
workingLabel
.
length
,
end:
infoAttribute
.
range
.
end
+
workingLabel
.
length
,
),
),
);
}
workingLabel
+=
effectiveLabel
;
}
}
combined
.
add
(
InlineSpanSemanticsInformation
(
workingText
,
semanticsLabel:
workingLabel
,
stringAttributes:
workingAttributes
,
));
return
combined
;
}
...
...
packages/flutter/lib/src/painting/text_span.dart
View file @
67cee630
...
...
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:ui'
as
ui
show
ParagraphBuilder
;
import
'dart:ui'
as
ui
show
ParagraphBuilder
,
Locale
,
StringAttribute
,
LocaleStringAttribute
,
SpellOutStringAttribute
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
...
...
@@ -74,6 +74,8 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
this
.
onEnter
,
this
.
onExit
,
this
.
semanticsLabel
,
this
.
locale
,
this
.
spellOut
,
})
:
mouseCursor
=
mouseCursor
??
(
recognizer
==
null
?
MouseCursor
.
defer
:
SystemMouseCursors
.
click
),
assert
(!(
text
==
null
&&
semanticsLabel
!=
null
)),
...
...
@@ -218,6 +220,32 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
/// ```
final
String
?
semanticsLabel
;
/// The language of the text in this span and its span children.
///
/// Setting the locale of this text span affects the way that assistive
/// technologies, such as VoiceOver or TalkBack, pronounce the text.
///
/// If this span contains other text span children, they also inherit the
/// locale from this span unless explicitly set to different locales.
final
ui
.
Locale
?
locale
;
/// Whether the assistive technologies should spell out this text character
/// by character.
///
/// If the text is 'hello world', setting this to true causes the assistive
/// technologies, such as VoiceOver or TalkBack, to pronounce
/// 'h-e-l-l-o-space-w-o-r-l-d' instead of complete words. This is useful for
/// texts, such as passwords or verification codes.
///
/// If this span contains other text span children, they also inherit the
/// property from this span unless explicitly set.
///
/// If the property is not set, this text span inherits the spell out setting
/// from its parent. If this text span does not have a parent or the parent
/// does not have a spell out setting, this text span does not spell out the
/// text by default.
final
bool
?
spellOut
;
@override
bool
get
validForMouseTracker
=>
true
;
...
...
@@ -333,21 +361,42 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
}
@override
void
computeSemanticsInformation
(
List
<
InlineSpanSemanticsInformation
>
collector
)
{
void
computeSemanticsInformation
(
List
<
InlineSpanSemanticsInformation
>
collector
,
{
ui
.
Locale
?
inheritedLocale
,
bool
inheritedSpellOut
=
false
,
})
{
assert
(
debugAssertIsValid
());
final
ui
.
Locale
?
effectiveLocale
=
locale
??
inheritedLocale
;
final
bool
effectiveSpellOut
=
spellOut
??
inheritedSpellOut
;
if
(
text
!=
null
)
{
collector
.
add
(
InlineSpanSemanticsInformation
(
text
!,
stringAttributes:
<
ui
.
StringAttribute
>[
if
(
effectiveSpellOut
)
ui
.
SpellOutStringAttribute
(
range:
TextRange
(
start:
0
,
end:
semanticsLabel
?.
length
??
text
!.
length
)),
if
(
effectiveLocale
!=
null
)
ui
.
LocaleStringAttribute
(
locale:
effectiveLocale
,
range:
TextRange
(
start:
0
,
end:
semanticsLabel
?.
length
??
text
!.
length
)),
],
semanticsLabel:
semanticsLabel
,
recognizer:
recognizer
,
));
}
if
(
children
!=
null
)
{
for
(
final
InlineSpan
child
in
children
!)
{
if
(
child
is
TextSpan
)
{
child
.
computeSemanticsInformation
(
collector
,
inheritedLocale:
effectiveLocale
,
inheritedSpellOut:
effectiveSpellOut
,
);
}
else
{
child
.
computeSemanticsInformation
(
collector
);
}
}
}
}
@override
int
?
codeUnitAtVisitor
(
int
index
,
Accumulator
offset
)
{
...
...
packages/flutter/lib/src/rendering/editable.dart
View file @
67cee630
...
...
@@ -2274,11 +2274,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// The text to display.
InlineSpan
?
get
text
=>
_textPainter
.
text
;
final
TextPainter
_textPainter
;
AttributedString
?
_cachedAttributedValue
;
List
<
InlineSpanSemanticsInformation
>?
_cachedCombinedSemanticsInfos
;
set
text
(
InlineSpan
?
value
)
{
if
(
_textPainter
.
text
==
value
)
return
;
_textPainter
.
text
=
value
;
_cachedPlainText
=
null
;
_cachedAttributedValue
=
null
;
_cachedCombinedSemanticsInfos
=
null
;
_extractPlaceholderSpans
(
value
);
markNeedsTextLayout
();
markNeedsSemanticsUpdate
();
...
...
@@ -2739,10 +2743,31 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
..
explicitChildNodes
=
true
;
return
;
}
if
(
_cachedAttributedValue
==
null
)
{
if
(
obscureText
)
{
_cachedAttributedValue
=
AttributedString
(
obscuringCharacter
*
_plainText
.
length
);
}
else
{
final
StringBuffer
buffer
=
StringBuffer
();
int
offset
=
0
;
final
List
<
StringAttribute
>
attributes
=
<
StringAttribute
>[];
for
(
final
InlineSpanSemanticsInformation
info
in
_semanticsInfo
!)
{
final
String
label
=
info
.
semanticsLabel
??
info
.
text
;
for
(
final
StringAttribute
infoAttribute
in
info
.
stringAttributes
)
{
final
TextRange
originalRange
=
infoAttribute
.
range
;
attributes
.
add
(
infoAttribute
.
copy
(
range:
TextRange
(
start:
offset
+
originalRange
.
start
,
end:
offset
+
originalRange
.
end
),
),
);
}
buffer
.
write
(
label
);
offset
+=
label
.
length
;
}
_cachedAttributedValue
=
AttributedString
(
buffer
.
toString
(),
attributes:
attributes
);
}
}
config
..
value
=
obscureText
?
obscuringCharacter
*
_plainText
.
length
:
_plainText
..
attributedValue
=
_cachedAttributedValue
!
..
isObscured
=
obscureText
..
isMultiline
=
_isMultiline
..
textDirection
=
textDirection
...
...
@@ -2793,7 +2818,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
int
childIndex
=
0
;
RenderBox
?
child
=
firstChild
;
final
Queue
<
SemanticsNode
>
newChildCache
=
Queue
<
SemanticsNode
>();
for
(
final
InlineSpanSemanticsInformation
info
in
combineSemanticsInfo
(
_semanticsInfo
!))
{
_cachedCombinedSemanticsInfos
??=
combineSemanticsInfo
(
_semanticsInfo
!);
for
(
final
InlineSpanSemanticsInformation
info
in
_cachedCombinedSemanticsInfos
!)
{
final
TextSelection
selection
=
TextSelection
(
baseOffset:
start
,
extentOffset:
start
+
info
.
text
.
length
,
...
...
@@ -2849,7 +2875,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final
SemanticsConfiguration
configuration
=
SemanticsConfiguration
()
..
sortKey
=
OrdinalSortKey
(
ordinal
++)
..
textDirection
=
initialDirection
..
label
=
info
.
semanticsLabel
??
info
.
text
;
..
attributedLabel
=
AttributedString
(
info
.
semanticsLabel
??
info
.
text
,
attributes:
info
.
stringAttributes
)
;
final
GestureRecognizer
?
recognizer
=
info
.
recognizer
;
if
(
recognizer
!=
null
)
{
if
(
recognizer
is
TapGestureRecognizer
)
{
...
...
packages/flutter/lib/src/rendering/paragraph.dart
View file @
67cee630
...
...
@@ -118,6 +118,8 @@ class RenderParagraph extends RenderBox
}
final
TextPainter
_textPainter
;
AttributedString
?
_cachedAttributedLabel
;
List
<
InlineSpanSemanticsInformation
>?
_cachedCombinedSemanticsInfos
;
/// The text to display.
InlineSpan
get
text
=>
_textPainter
.
text
!;
...
...
@@ -129,6 +131,8 @@ class RenderParagraph extends RenderBox
return
;
case
RenderComparison
.
paint
:
_textPainter
.
text
=
value
;
_cachedAttributedLabel
=
null
;
_cachedCombinedSemanticsInfos
=
null
;
_extractPlaceholderSpans
(
value
);
markNeedsPaint
();
markNeedsSemanticsUpdate
();
...
...
@@ -136,6 +140,8 @@ class RenderParagraph extends RenderBox
case
RenderComparison
.
layout
:
_textPainter
.
text
=
value
;
_overflowShader
=
null
;
_cachedAttributedLabel
=
null
;
_cachedCombinedSemanticsInfos
=
null
;
_extractPlaceholderSpans
(
value
);
markNeedsLayout
();
break
;
...
...
@@ -869,11 +875,27 @@ class RenderParagraph extends RenderBox
config
.
explicitChildNodes
=
true
;
config
.
isSemanticBoundary
=
true
;
}
else
{
if
(
_cachedAttributedLabel
==
null
)
{
final
StringBuffer
buffer
=
StringBuffer
();
int
offset
=
0
;
final
List
<
StringAttribute
>
attributes
=
<
StringAttribute
>[];
for
(
final
InlineSpanSemanticsInformation
info
in
_semanticsInfo
!)
{
buffer
.
write
(
info
.
semanticsLabel
??
info
.
text
);
final
String
label
=
info
.
semanticsLabel
??
info
.
text
;
for
(
final
StringAttribute
infoAttribute
in
info
.
stringAttributes
)
{
final
TextRange
originalRange
=
infoAttribute
.
range
;
attributes
.
add
(
infoAttribute
.
copy
(
range:
TextRange
(
start:
offset
+
originalRange
.
start
,
end:
offset
+
originalRange
.
end
)
),
);
}
buffer
.
write
(
label
);
offset
+=
label
.
length
;
}
_cachedAttributedLabel
=
AttributedString
(
buffer
.
toString
(),
attributes:
attributes
);
}
config
.
label
=
buffer
.
toString
()
;
config
.
attributedLabel
=
_cachedAttributedLabel
!
;
config
.
textDirection
=
textDirection
;
}
}
...
...
@@ -896,7 +918,8 @@ class RenderParagraph extends RenderBox
int
childIndex
=
0
;
RenderBox
?
child
=
firstChild
;
final
Queue
<
SemanticsNode
>
newChildCache
=
Queue
<
SemanticsNode
>();
for
(
final
InlineSpanSemanticsInformation
info
in
combineSemanticsInfo
(
_semanticsInfo
!))
{
_cachedCombinedSemanticsInfos
??=
combineSemanticsInfo
(
_semanticsInfo
!);
for
(
final
InlineSpanSemanticsInformation
info
in
_cachedCombinedSemanticsInfos
!)
{
final
TextSelection
selection
=
TextSelection
(
baseOffset:
start
,
extentOffset:
start
+
info
.
text
.
length
,
...
...
@@ -952,7 +975,7 @@ class RenderParagraph extends RenderBox
final
SemanticsConfiguration
configuration
=
SemanticsConfiguration
()
..
sortKey
=
OrdinalSortKey
(
ordinal
++)
..
textDirection
=
initialDirection
..
label
=
info
.
semanticsLabel
??
info
.
text
;
..
attributedLabel
=
AttributedString
(
info
.
semanticsLabel
??
info
.
text
,
attributes:
info
.
stringAttributes
)
;
final
GestureRecognizer
?
recognizer
=
info
.
recognizer
;
if
(
recognizer
!=
null
)
{
if
(
recognizer
is
TapGestureRecognizer
)
{
...
...
packages/flutter/test/painting/text_span_test.dart
View file @
67cee630
...
...
@@ -364,4 +364,53 @@ void main() {
expect
(
logEvents
[
1
],
isA
<
PointerExitEvent
>());
});
testWidgets
(
'TextSpan can compute StringAttributes'
,
(
WidgetTester
tester
)
async
{
const
TextSpan
span
=
TextSpan
(
text:
'aaaaa'
,
spellOut:
true
,
children:
<
InlineSpan
>[
TextSpan
(
text:
'yyyyy'
,
locale:
Locale
(
'es'
,
'MX'
)),
TextSpan
(
text:
'xxxxx'
,
spellOut:
false
,
children:
<
InlineSpan
>[
TextSpan
(
text:
'zzzzz'
),
TextSpan
(
text:
'bbbbb'
,
spellOut:
true
),
]
),
],
);
final
List
<
InlineSpanSemanticsInformation
>
collector
=
<
InlineSpanSemanticsInformation
>[];
span
.
computeSemanticsInformation
(
collector
);
expect
(
collector
.
length
,
5
);
expect
(
collector
[
0
].
stringAttributes
.
length
,
1
);
expect
(
collector
[
0
].
stringAttributes
[
0
],
isA
<
SpellOutStringAttribute
>());
expect
(
collector
[
0
].
stringAttributes
[
0
].
range
,
const
TextRange
(
start:
0
,
end:
5
));
expect
(
collector
[
1
].
stringAttributes
.
length
,
2
);
expect
(
collector
[
1
].
stringAttributes
[
0
],
isA
<
SpellOutStringAttribute
>());
expect
(
collector
[
1
].
stringAttributes
[
0
].
range
,
const
TextRange
(
start:
0
,
end:
5
));
expect
(
collector
[
1
].
stringAttributes
[
1
],
isA
<
LocaleStringAttribute
>());
expect
(
collector
[
1
].
stringAttributes
[
1
].
range
,
const
TextRange
(
start:
0
,
end:
5
));
final
LocaleStringAttribute
localeStringAttribute
=
collector
[
1
].
stringAttributes
[
1
]
as
LocaleStringAttribute
;
expect
(
localeStringAttribute
.
locale
,
const
Locale
(
'es'
,
'MX'
));
expect
(
collector
[
2
].
stringAttributes
.
length
,
0
);
expect
(
collector
[
3
].
stringAttributes
.
length
,
0
);
expect
(
collector
[
4
].
stringAttributes
.
length
,
1
);
expect
(
collector
[
4
].
stringAttributes
[
0
],
isA
<
SpellOutStringAttribute
>());
expect
(
collector
[
4
].
stringAttributes
[
0
].
range
,
const
TextRange
(
start:
0
,
end:
5
));
final
List
<
InlineSpanSemanticsInformation
>
combined
=
combineSemanticsInfo
(
collector
);
expect
(
combined
.
length
,
1
);
expect
(
combined
[
0
].
stringAttributes
.
length
,
4
);
expect
(
combined
[
0
].
stringAttributes
[
0
],
isA
<
SpellOutStringAttribute
>());
expect
(
combined
[
0
].
stringAttributes
[
0
].
range
,
const
TextRange
(
start:
0
,
end:
5
));
expect
(
combined
[
0
].
stringAttributes
[
1
],
isA
<
SpellOutStringAttribute
>());
expect
(
combined
[
0
].
stringAttributes
[
1
].
range
,
const
TextRange
(
start:
5
,
end:
10
));
expect
(
combined
[
0
].
stringAttributes
[
2
],
isA
<
LocaleStringAttribute
>());
expect
(
combined
[
0
].
stringAttributes
[
2
].
range
,
const
TextRange
(
start:
5
,
end:
10
));
final
LocaleStringAttribute
combinedLocaleStringAttribute
=
combined
[
0
].
stringAttributes
[
2
]
as
LocaleStringAttribute
;
expect
(
combinedLocaleStringAttribute
.
locale
,
const
Locale
(
'es'
,
'MX'
));
expect
(
combined
[
0
].
stringAttributes
[
3
],
isA
<
SpellOutStringAttribute
>());
expect
(
combined
[
0
].
stringAttributes
[
3
].
range
,
const
TextRange
(
start:
20
,
end:
25
));
});
}
packages/flutter/test/widgets/rich_text_test.dart
View file @
67cee630
...
...
@@ -48,8 +48,105 @@ void main() {
));
});
testWidgets
(
'TextSpan Locale works'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
RichText
(
text:
TextSpan
(
text:
'root'
,
locale:
const
Locale
(
'es'
,
'MX'
),
children:
<
InlineSpan
>[
TextSpan
(
text:
'one'
,
recognizer:
TapGestureRecognizer
()),
const
WidgetSpan
(
child:
SizedBox
(),
),
TextSpan
(
text:
'three'
,
recognizer:
DoubleTapGestureRecognizer
()),
]
),
),
),
);
expect
(
tester
.
getSemantics
(
find
.
byType
(
RichText
)),
matchesSemantics
(
children:
<
Matcher
>[
matchesSemantics
(
attributedLabel:
AttributedString
(
'root'
,
attributes:
<
StringAttribute
>[
LocaleStringAttribute
(
range:
const
TextRange
(
start:
0
,
end:
4
),
locale:
const
Locale
(
'es'
,
'MX'
)),
]
),
),
matchesSemantics
(
attributedLabel:
AttributedString
(
'one'
,
attributes:
<
StringAttribute
>[
LocaleStringAttribute
(
range:
const
TextRange
(
start:
0
,
end:
3
),
locale:
const
Locale
(
'es'
,
'MX'
)),
]
),
),
matchesSemantics
(
attributedLabel:
AttributedString
(
'three'
,
attributes:
<
StringAttribute
>[
LocaleStringAttribute
(
range:
const
TextRange
(
start:
0
,
end:
5
),
locale:
const
Locale
(
'es'
,
'MX'
)),
]
),
),
],
));
});
testWidgets
(
'TextSpan spellOut works'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
RichText
(
text:
TextSpan
(
text:
'root'
,
spellOut:
true
,
children:
<
InlineSpan
>[
TextSpan
(
text:
'one'
,
recognizer:
TapGestureRecognizer
()),
const
WidgetSpan
(
child:
SizedBox
(),
),
TextSpan
(
text:
'three'
,
recognizer:
DoubleTapGestureRecognizer
()),
]
),
),
),
);
expect
(
tester
.
getSemantics
(
find
.
byType
(
RichText
)),
matchesSemantics
(
children:
<
Matcher
>[
matchesSemantics
(
attributedLabel:
AttributedString
(
'root'
,
attributes:
<
StringAttribute
>[
SpellOutStringAttribute
(
range:
const
TextRange
(
start:
0
,
end:
4
)),
]
),
),
matchesSemantics
(
attributedLabel:
AttributedString
(
'one'
,
attributes:
<
StringAttribute
>[
SpellOutStringAttribute
(
range:
const
TextRange
(
start:
0
,
end:
3
)),
]
),
),
matchesSemantics
(
attributedLabel:
AttributedString
(
'three'
,
attributes:
<
StringAttribute
>[
SpellOutStringAttribute
(
range:
const
TextRange
(
start:
0
,
end:
5
)),
]
),
),
],
));
});
testWidgets
(
'WidgetSpan calculate correct intrinsic heights'
,
(
WidgetTester
tester
)
async
{
// Regression test for https://github.com/flutter/flutter/issues/48679.
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
...
...
packages/flutter/test/widgets/selectable_text_test.dart
View file @
67cee630
...
...
@@ -1488,6 +1488,72 @@ void main() {
semantics
.
dispose
();
});
testWidgets
(
'Selectable text rich text with spell out in semantics'
,
(
WidgetTester
tester
)
async
{
final
SemanticsTester
semantics
=
SemanticsTester
(
tester
);
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Material
(
child:
Center
(
child:
SelectableText
.
rich
(
TextSpan
(
text:
'some text'
,
spellOut:
true
)),
),
),
),
);
expect
(
semantics
,
includesNodeWith
(
attributedValue:
AttributedString
(
'some text'
,
attributes:
<
StringAttribute
>[
SpellOutStringAttribute
(
range:
const
TextRange
(
start:
0
,
end:
9
)),
],
),
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isTextField
,
SemanticsFlag
.
isReadOnly
,
SemanticsFlag
.
isMultiline
,
],
),
);
semantics
.
dispose
();
});
testWidgets
(
'Selectable text rich text with locale in semantics'
,
(
WidgetTester
tester
)
async
{
final
SemanticsTester
semantics
=
SemanticsTester
(
tester
);
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Material
(
child:
Center
(
child:
SelectableText
.
rich
(
TextSpan
(
text:
'some text'
,
locale:
Locale
(
'es'
,
'MX'
))),
),
),
),
);
expect
(
semantics
,
includesNodeWith
(
attributedValue:
AttributedString
(
'some text'
,
attributes:
<
StringAttribute
>[
LocaleStringAttribute
(
range:
const
TextRange
(
start:
0
,
end:
9
),
locale:
const
Locale
(
'es'
,
'MX'
)),
],
),
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isTextField
,
SemanticsFlag
.
isReadOnly
,
SemanticsFlag
.
isMultiline
,
],
),
);
semantics
.
dispose
();
});
testWidgets
(
'Selectable rich text with gesture recognizer has correct semantics'
,
(
WidgetTester
tester
)
async
{
final
SemanticsTester
semantics
=
SemanticsTester
(
tester
);
await
tester
.
pumpWidget
(
...
...
packages/flutter/test/widgets/semantics_tester.dart
View file @
67cee630
...
...
@@ -427,6 +427,25 @@ class SemanticsTester {
@override
String
toString
()
=>
'SemanticsTester for
${tester.binding.pipelineOwner.semanticsOwner?.rootSemanticsNode}
'
;
bool
_stringAttributesEqual
(
List
<
StringAttribute
>
first
,
List
<
StringAttribute
>
second
)
{
if
(
first
.
length
!=
second
.
length
)
return
false
;
for
(
int
i
=
0
;
i
<
first
.
length
;
i
++)
{
if
(
first
[
i
]
is
SpellOutStringAttribute
&&
(
second
[
i
]
is
!
SpellOutStringAttribute
||
second
[
i
].
range
!=
first
[
i
].
range
))
{
return
false
;
}
if
(
first
[
i
]
is
LocaleStringAttribute
&&
(
second
[
i
]
is
!
LocaleStringAttribute
||
second
[
i
].
range
!=
first
[
i
].
range
||
(
second
[
i
]
as
LocaleStringAttribute
).
locale
!=
(
second
[
i
]
as
LocaleStringAttribute
).
locale
))
{
return
false
;
}
}
return
true
;
}
/// Returns all semantics nodes in the current semantics tree whose properties
/// match the non-null arguments.
///
...
...
@@ -435,6 +454,9 @@ class SemanticsTester {
///
/// If `ancestor` is not null, only the descendants of it are returned.
Iterable
<
SemanticsNode
>
nodesWith
({
AttributedString
?
attributedLabel
,
AttributedString
?
attributedValue
,
AttributedString
?
attributedHint
,
String
?
label
,
String
?
value
,
String
?
hint
,
...
...
@@ -451,10 +473,25 @@ class SemanticsTester {
bool
checkNode
(
SemanticsNode
node
)
{
if
(
label
!=
null
&&
node
.
label
!=
label
)
return
false
;
if
(
attributedLabel
!=
null
&&
(
attributedLabel
.
string
!=
node
.
attributedLabel
.
string
||
!
_stringAttributesEqual
(
attributedLabel
.
attributes
,
node
.
attributedLabel
.
attributes
)))
{
return
false
;
}
if
(
value
!=
null
&&
node
.
value
!=
value
)
return
false
;
if
(
attributedValue
!=
null
&&
(
attributedValue
.
string
!=
node
.
attributedValue
.
string
||
!
_stringAttributesEqual
(
attributedValue
.
attributes
,
node
.
attributedValue
.
attributes
)))
{
return
false
;
}
if
(
hint
!=
null
&&
node
.
hint
!=
hint
)
return
false
;
if
(
attributedHint
!=
null
&&
(
attributedHint
.
string
!=
node
.
attributedHint
.
string
||
!
_stringAttributesEqual
(
attributedHint
.
attributes
,
node
.
attributedHint
.
attributes
)))
{
return
false
;
}
if
(
textDirection
!=
null
&&
node
.
textDirection
!=
textDirection
)
return
false
;
if
(
actions
!=
null
)
{
...
...
@@ -714,6 +751,9 @@ Matcher hasSemantics(
class _IncludesNodeWith extends Matcher {
const _IncludesNodeWith({
this.attributedLabel,
this.attributedValue,
this.attributedHint,
this.label,
this.value,
this.hint,
...
...
@@ -725,7 +765,7 @@ class _IncludesNodeWith extends Matcher {
this.scrollExtentMin,
this.maxValueLength,
this.currentValueLength,
}) : assert(
}) : assert(
label != null ||
value != null ||
actions != null ||
...
...
@@ -736,7 +776,9 @@ class _IncludesNodeWith extends Matcher {
maxValueLength != null ||
currentValueLength != null,
);
final AttributedString? attributedLabel;
final AttributedString? attributedValue;
final AttributedString? attributedHint;
final String? label;
final String? value;
final String? hint;
...
...
@@ -752,6 +794,9 @@ class _IncludesNodeWith extends Matcher {
@override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
return item.nodesWith(
attributedLabel: attributedLabel,
attributedValue: attributedValue,
attributedHint: attributedHint,
label: label,
value: value,
hint: hint,
...
...
@@ -800,8 +845,11 @@ class _IncludesNodeWith extends Matcher {
/// If null is provided for an argument, it will match against any value.
Matcher includesNodeWith({
String? label,
AttributedString? attributedLabel,
String? value,
AttributedString? attributedValue,
String? hint,
AttributedString? attributedHint,
TextDirection? textDirection,
List<SemanticsAction>? actions,
List<SemanticsFlag>? flags,
...
...
@@ -813,8 +861,11 @@ Matcher includesNodeWith({
}) {
return _IncludesNodeWith(
label: label,
attributedLabel: attributedLabel,
value: value,
attributedValue: attributedValue,
hint: hint,
attributedHint: attributedHint,
textDirection: textDirection,
actions: actions,
flags: flags,
...
...
packages/flutter_test/lib/src/matchers.dart
View file @
67cee630
...
...
@@ -431,10 +431,15 @@ AsyncMatcher matchesReferenceImage(ui.Image image) {
/// * [WidgetTester.getSemantics], the tester method which retrieves semantics.
Matcher
matchesSemantics
(
{
String
?
label
,
AttributedString
?
attributedLabel
,
String
?
hint
,
AttributedString
?
attributedHint
,
String
?
value
,
AttributedString
?
attributedValue
,
String
?
increasedValue
,
AttributedString
?
attributedIncreasedValue
,
String
?
decreasedValue
,
AttributedString
?
attributedDecreasedValue
,
TextDirection
?
textDirection
,
Rect
?
rect
,
Size
?
size
,
...
...
@@ -559,10 +564,15 @@ Matcher matchesSemantics({
return
_MatchesSemanticsData
(
label:
label
,
attributedLabel:
attributedLabel
,
hint:
hint
,
attributedHint:
attributedHint
,
value:
value
,
attributedValue:
attributedValue
,
increasedValue:
increasedValue
,
attributedIncreasedValue:
attributedIncreasedValue
,
decreasedValue:
decreasedValue
,
attributedDecreasedValue:
attributedDecreasedValue
,
actions:
actions
,
flags:
flags
,
textDirection:
textDirection
,
...
...
@@ -1708,10 +1718,15 @@ class _MatchesReferenceImage extends AsyncMatcher {
class
_MatchesSemanticsData
extends
Matcher
{
_MatchesSemanticsData
({
this
.
label
,
this
.
attributedLabel
,
this
.
hint
,
this
.
attributedHint
,
this
.
value
,
this
.
attributedValue
,
this
.
increasedValue
,
this
.
attributedIncreasedValue
,
this
.
decreasedValue
,
this
.
hint
,
this
.
attributedDecreasedValue
,
this
.
flags
,
this
.
actions
,
this
.
textDirection
,
...
...
@@ -1728,10 +1743,15 @@ class _MatchesSemanticsData extends Matcher {
});
final
String
?
label
;
final
String
?
value
;
final
AttributedString
?
attributedLabel
;
final
String
?
hint
;
final
AttributedString
?
attributedHint
;
final
String
?
value
;
final
AttributedString
?
attributedValue
;
final
String
?
increasedValue
;
final
AttributedString
?
attributedIncreasedValue
;
final
String
?
decreasedValue
;
final
AttributedString
?
attributedDecreasedValue
;
final
SemanticsHintOverrides
?
hintOverrides
;
final
List
<
SemanticsAction
>?
actions
;
final
List
<
CustomSemanticsAction
>?
customActions
;
...
...
@@ -1751,14 +1771,24 @@ class _MatchesSemanticsData extends Matcher {
description
.
add
(
'has semantics'
);
if
(
label
!=
null
)
description
.
add
(
' with label:
$label
'
);
if
(
attributedLabel
!=
null
)
description
.
add
(
' with attributedLabel:
$attributedLabel
'
);
if
(
value
!=
null
)
description
.
add
(
' with value:
$value
'
);
if
(
attributedValue
!=
null
)
description
.
add
(
' with attributedValue:
$attributedValue
'
);
if
(
hint
!=
null
)
description
.
add
(
' with hint:
$hint
'
);
if
(
attributedHint
!=
null
)
description
.
add
(
' with attributedHint:
$attributedHint
'
);
if
(
increasedValue
!=
null
)
description
.
add
(
' with increasedValue:
$increasedValue
'
);
if
(
attributedIncreasedValue
!=
null
)
description
.
add
(
' with attributedIncreasedValue:
$attributedIncreasedValue
'
);
if
(
decreasedValue
!=
null
)
description
.
add
(
' with decreasedValue:
$decreasedValue
'
);
if
(
attributedDecreasedValue
!=
null
)
description
.
add
(
' with attributedDecreasedValue:
$attributedDecreasedValue
'
);
if
(
actions
!=
null
)
description
.
add
(
' with actions: '
).
addDescriptionOf
(
actions
);
if
(
flags
!=
null
)
...
...
@@ -1791,6 +1821,24 @@ class _MatchesSemanticsData extends Matcher {
return
description
;
}
bool
_stringAttributesEqual
(
List
<
StringAttribute
>
first
,
List
<
StringAttribute
>
second
)
{
if
(
first
.
length
!=
second
.
length
)
return
false
;
for
(
int
i
=
0
;
i
<
first
.
length
;
i
++)
{
if
(
first
[
i
]
is
SpellOutStringAttribute
&&
(
second
[
i
]
is
!
SpellOutStringAttribute
||
second
[
i
].
range
!=
first
[
i
].
range
))
{
return
false
;
}
if
(
first
[
i
]
is
LocaleStringAttribute
&&
(
second
[
i
]
is
!
LocaleStringAttribute
||
second
[
i
].
range
!=
first
[
i
].
range
||
(
second
[
i
]
as
LocaleStringAttribute
).
locale
!=
(
second
[
i
]
as
LocaleStringAttribute
).
locale
))
{
return
false
;
}
}
return
true
;
}
@override
bool
matches
(
dynamic
node
,
Map
<
dynamic
,
dynamic
>
matchState
)
{
...
...
@@ -1801,14 +1849,44 @@ class _MatchesSemanticsData extends Matcher {
final
SemanticsData
data
=
node
is
SemanticsNode
?
node
.
getSemanticsData
()
:
(
node
as
SemanticsData
);
if
(
label
!=
null
&&
label
!=
data
.
label
)
return
failWithDescription
(
matchState
,
'label was:
${data.label}
'
);
if
(
attributedLabel
!=
null
&&
(
attributedLabel
!.
string
!=
data
.
attributedLabel
.
string
||
!
_stringAttributesEqual
(
attributedLabel
!.
attributes
,
data
.
attributedLabel
.
attributes
)))
{
return
failWithDescription
(
matchState
,
'attributedLabel was:
${data.attributedLabel}
'
);
}
if
(
hint
!=
null
&&
hint
!=
data
.
hint
)
return
failWithDescription
(
matchState
,
'hint was:
${data.hint}
'
);
if
(
attributedHint
!=
null
&&
(
attributedHint
!.
string
!=
data
.
attributedHint
.
string
||
!
_stringAttributesEqual
(
attributedHint
!.
attributes
,
data
.
attributedHint
.
attributes
)))
{
return
failWithDescription
(
matchState
,
'attributedHint was:
${data.attributedHint}
'
);
}
if
(
value
!=
null
&&
value
!=
data
.
value
)
return
failWithDescription
(
matchState
,
'value was:
${data.value}
'
);
if
(
attributedValue
!=
null
&&
(
attributedValue
!.
string
!=
data
.
attributedValue
.
string
||
!
_stringAttributesEqual
(
attributedValue
!.
attributes
,
data
.
attributedValue
.
attributes
)))
{
return
failWithDescription
(
matchState
,
'attributedValue was:
${data.attributedValue}
'
);
}
if
(
increasedValue
!=
null
&&
increasedValue
!=
data
.
increasedValue
)
return
failWithDescription
(
matchState
,
'increasedValue was:
${data.increasedValue}
'
);
if
(
attributedIncreasedValue
!=
null
&&
(
attributedIncreasedValue
!.
string
!=
data
.
attributedIncreasedValue
.
string
||
!
_stringAttributesEqual
(
attributedIncreasedValue
!.
attributes
,
data
.
attributedIncreasedValue
.
attributes
)))
{
return
failWithDescription
(
matchState
,
'attributedIncreasedValue was:
${data.attributedIncreasedValue}
'
);
}
if
(
decreasedValue
!=
null
&&
decreasedValue
!=
data
.
decreasedValue
)
return
failWithDescription
(
matchState
,
'decreasedValue was:
${data.decreasedValue}
'
);
if
(
attributedDecreasedValue
!=
null
&&
(
attributedDecreasedValue
!.
string
!=
data
.
attributedDecreasedValue
.
string
||
!
_stringAttributesEqual
(
attributedDecreasedValue
!.
attributes
,
data
.
attributedDecreasedValue
.
attributes
)))
{
return
failWithDescription
(
matchState
,
'attributedDecreasedValue was:
${data.attributedDecreasedValue}
'
);
}
if
(
textDirection
!=
null
&&
textDirection
!=
data
.
textDirection
)
return
failWithDescription
(
matchState
,
'textDirection was:
$textDirection
'
);
if
(
rect
!=
null
&&
rect
!=
data
.
rect
)
...
...
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