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
c96806ec
Unverified
Commit
c96806ec
authored
Jul 17, 2019
by
Dan Field
Committed by
GitHub
Jul 17, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Allow semantics labels to be shorter or longer than raw text (#36243)
parent
9b08effa
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
228 additions
and
87 deletions
+228
-87
inline_span.dart
packages/flutter/lib/src/painting/inline_span.dart
+85
-0
placeholder_span.dart
packages/flutter/lib/src/painting/placeholder_span.dart
+5
-0
text_span.dart
packages/flutter/lib/src/painting/text_span.dart
+17
-0
paragraph.dart
packages/flutter/lib/src/rendering/paragraph.dart
+83
-87
text_test.dart
packages/flutter/test/widgets/text_test.dart
+38
-0
No files found.
packages/flutter/lib/src/painting/inline_span.dart
View file @
c96806ec
...
...
@@ -35,6 +35,68 @@ class Accumulator {
/// [InlineSpan]s.
typedef
InlineSpanVisitor
=
bool
Function
(
InlineSpan
span
);
/// The textual and semantic label information for an [InlineSpan].
///
/// For [PlaceholderSpan]s, [InlineSpanSemanticsInformation.placeholder] is used by default.
///
/// See also:
/// * [InlineSpan.getSemanticsInformation]
@immutable
class
InlineSpanSemanticsInformation
{
/// Constructs an object that holds the text and sematnics label values of an
/// [InlineSpan].
///
/// The text parameter must not be null.
///
/// Use [InlineSpanSemanticsInformation.placeholder] instead of directly setting
/// [isPlaceholder].
const
InlineSpanSemanticsInformation
(
this
.
text
,
{
this
.
isPlaceholder
=
false
,
this
.
semanticsLabel
,
this
.
recognizer
})
:
assert
(
text
!=
null
),
assert
(
isPlaceholder
!=
null
),
assert
(
isPlaceholder
==
false
||
(
text
==
'
\
uFFFC'
&&
semanticsLabel
==
null
&&
recognizer
==
null
)),
requiresOwnNode
=
isPlaceholder
||
recognizer
!=
null
;
/// The text info for a [PlaceholderSpan].
static
const
InlineSpanSemanticsInformation
placeholder
=
InlineSpanSemanticsInformation
(
'
\
uFFFC'
,
isPlaceholder:
true
);
/// The text value, if any. For [PlaceholderSpan]s, this will be the unicode
/// placeholder value.
final
String
text
;
/// The semanticsLabel, if any.
final
String
semanticsLabel
;
/// The gesture recognizer, if any, for this span.
final
GestureRecognizer
recognizer
;
/// Whether this is for a placeholder span.
final
bool
isPlaceholder
;
/// True if this configuration should get its own semantics node.
///
/// This will be the case of the [recognizer] is not null, of if
/// [isPlaceholder] is true.
final
bool
requiresOwnNode
;
@override
bool
operator
==(
dynamic
other
)
{
if
(
other
is
!
InlineSpanSemanticsInformation
)
{
return
false
;
}
return
other
.
text
==
text
&&
other
.
semanticsLabel
==
semanticsLabel
&&
other
.
recognizer
==
recognizer
&&
other
.
isPlaceholder
==
isPlaceholder
;
}
@override
int
get
hashCode
=>
hashValues
(
text
,
semanticsLabel
,
recognizer
,
isPlaceholder
);
@override
String
toString
()
=>
'
$runtimeType
{text:
$text
, semanticsLabel:
$semanticsLabel
, recognizer:
$recognizer
}'
;
}
/// An immutable span of inline content which forms part of a paragraph.
///
/// * The subclass [TextSpan] specifies text and may contain child [InlineSpan]s.
...
...
@@ -175,6 +237,28 @@ abstract class InlineSpan extends DiagnosticableTree {
return
buffer
.
toString
();
}
/// Flattens the [InlineSpan] tree to a list of
/// [InlineSpanSemanticsInformation] objects.
///
/// [PlaceholderSpan]s in the tree will be represented with a
/// [InlineSpanSemanticsInformation.placeholder] value.
List
<
InlineSpanSemanticsInformation
>
getSemanticsInformation
()
{
final
List
<
InlineSpanSemanticsInformation
>
collector
=
<
InlineSpanSemanticsInformation
>[];
computeSemanticsInformation
(
collector
);
return
collector
;
}
/// Walks the [InlineSpan] tree and accumulates a list of
/// [InlineSpanSemanticsInformation] objects.
///
/// This method should not be directly called. Use
/// [getSemanticsInformation] instead.
///
/// [PlaceholderSpan]s in the tree will be represented with a
/// [InlineSpanSemanticsInformation.placeholder] value.
@protected
void
computeSemanticsInformation
(
List
<
InlineSpanSemanticsInformation
>
collector
);
/// Walks the [InlineSpan] tree and writes the plain text representation to `buffer`.
///
/// This method should not be directly called. Use [toPlainText] instead.
...
...
@@ -229,6 +313,7 @@ abstract class InlineSpan extends DiagnosticableTree {
///
/// Any [GestureRecognizer]s are added to `semanticsElements`. Null is added to
/// `semanticsElements` for [PlaceholderSpan]s.
@Deprecated
(
'Implement computeSemanticsInformation instead.'
)
void
describeSemantics
(
Accumulator
offset
,
List
<
int
>
semanticsOffsets
,
List
<
dynamic
>
semanticsElements
);
/// In checked mode, throws an exception if the object is not in a
...
...
packages/flutter/lib/src/painting/placeholder_span.dart
View file @
c96806ec
...
...
@@ -60,6 +60,11 @@ abstract class PlaceholderSpan extends InlineSpan {
}
}
@override
void
computeSemanticsInformation
(
List
<
InlineSpanSemanticsInformation
>
collector
)
{
collector
.
add
(
InlineSpanSemanticsInformation
.
placeholder
);
}
// TODO(garyq): Remove this after next stable release.
/// The [visitTextSpan] method is invalid on [PlaceholderSpan]s
@override
...
...
packages/flutter/lib/src/painting/text_span.dart
View file @
c96806ec
...
...
@@ -291,6 +291,23 @@ class TextSpan extends InlineSpan {
}
}
@override
void
computeSemanticsInformation
(
List
<
InlineSpanSemanticsInformation
>
collector
)
{
assert
(
debugAssertIsValid
());
if
(
text
!=
null
||
semanticsLabel
!=
null
)
{
collector
.
add
(
InlineSpanSemanticsInformation
(
text
,
semanticsLabel:
semanticsLabel
,
recognizer:
recognizer
,
));
}
if
(
children
!=
null
)
{
for
(
InlineSpan
child
in
children
)
{
child
.
computeSemanticsInformation
(
collector
);
}
}
}
@override
int
codeUnitAtVisitor
(
int
index
,
Accumulator
offset
)
{
if
(
text
==
null
)
{
...
...
packages/flutter/lib/src/rendering/paragraph.dart
View file @
c96806ec
...
...
@@ -731,49 +731,83 @@ class RenderParagraph extends RenderBox
return
_textPainter
.
size
;
}
// The offsets for each span that requires custom semantics.
final
List
<
int
>
_inlineSemanticsOffsets
=
<
int
>[];
// Holds either [GestureRecognizer] or null (for placeholders) to generate
// proper semnatics configurations.
final
List
<
dynamic
>
_inlineSemanticsElements
=
<
dynamic
>[];
/// Collected during [describeSemanticsConfiguration], used by
/// [assembleSemanticsNode] and [_combineSemanticsInfo].
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
=
''
;
String
workingLabel
;
for
(
InlineSpanSemanticsInformation
info
in
_semanticsInfo
)
{
if
(
info
.
requiresOwnNode
)
{
if
(
workingText
!=
null
)
{
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
;
}
}
}
if
(
workingText
!=
null
)
{
combined
.
add
(
InlineSpanSemanticsInformation
(
workingText
,
semanticsLabel:
workingLabel
,
));
}
else
{
assert
(
workingLabel
!=
null
);
}
return
combined
;
}
@override
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
super
.
describeSemanticsConfiguration
(
config
);
_inlineSemanticsOffsets
.
clear
();
_inlineSemanticsElements
.
clear
();
final
Accumulator
offset
=
Accumulator
();
text
.
visitChildren
((
InlineSpan
span
)
{
span
.
describeSemantics
(
offset
,
_inlineSemanticsOffsets
,
_inlineSemanticsElements
);
return
true
;
});
if
(
_inlineSemanticsOffsets
.
isNotEmpty
)
{
_semanticsInfo
=
text
.
getSemanticsInformation
();
if
(
_semanticsInfo
.
any
((
InlineSpanSemanticsInformation
info
)
=>
info
.
recognizer
!=
null
))
{
config
.
explicitChildNodes
=
true
;
config
.
isSemanticBoundary
=
true
;
}
else
{
config
.
label
=
text
.
toPlainText
();
final
StringBuffer
buffer
=
StringBuffer
();
for
(
InlineSpanSemanticsInformation
info
in
_semanticsInfo
)
{
buffer
.
write
(
info
.
semanticsLabel
??
info
.
text
);
}
config
.
label
=
buffer
.
toString
();
config
.
textDirection
=
textDirection
;
}
}
@override
void
assembleSemanticsNode
(
SemanticsNode
node
,
SemanticsConfiguration
config
,
Iterable
<
SemanticsNode
>
children
)
{
assert
(
_inlineSemanticsOffsets
.
isNotEmpty
);
assert
(
_inlineSemanticsOffsets
.
length
.
isEven
);
assert
(
_inlineSemanticsElements
.
isNotEmpty
);
assert
(
_semanticsInfo
!=
null
&&
_semanticsInfo
.
isNotEmpty
);
final
List
<
SemanticsNode
>
newChildren
=
<
SemanticsNode
>[];
final
String
rawLabel
=
text
.
toPlainText
();
int
current
=
0
;
double
order
=
-
1.0
;
TextDirection
currentDirection
=
textDirection
;
Rect
currentRect
;
SemanticsConfiguration
buildSemanticsConfig
(
int
start
,
int
end
)
{
double
ordinal
=
0.0
;
int
start
=
0
;
int
placeholderIndex
=
0
;
RenderBox
child
=
firstChild
;
for
(
InlineSpanSemanticsInformation
info
in
_combineSemanticsInfo
())
{
final
TextDirection
initialDirection
=
currentDirection
;
final
TextSelection
selection
=
TextSelection
(
baseOffset:
start
,
extentOffset:
end
);
final
TextSelection
selection
=
TextSelection
(
baseOffset:
start
,
extentOffset:
start
+
info
.
text
.
length
);
final
List
<
ui
.
TextBox
>
rects
=
getBoxesForSelection
(
selection
);
if
(
rects
.
isEmpty
)
{
return
null
;
continue
;
}
Rect
rect
=
rects
.
first
.
toRect
();
currentDirection
=
rects
.
first
.
direction
;
...
...
@@ -791,64 +825,15 @@ class RenderParagraph extends RenderBox
);
// 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.
currentRect
=
Rect
.
fromLTRB
(
rect
.
left
.
floorToDouble
()
-
4.0
,
rect
.
top
.
floorToDouble
()
-
4.0
,
rect
.
right
.
ceilToDouble
()
+
4.0
,
rect
.
bottom
.
ceilToDouble
()
+
4.0
,
);
order
+=
1
;
final
SemanticsConfiguration
configuration
=
SemanticsConfiguration
()
..
sortKey
=
OrdinalSortKey
(
order
)
..
textDirection
=
initialDirection
..
label
=
rawLabel
.
substring
(
start
,
end
);
return
configuration
;
}
int
childIndex
=
0
;
RenderBox
child
=
firstChild
;
for
(
int
i
=
0
,
j
=
0
;
i
<
_inlineSemanticsOffsets
.
length
;
i
+=
2
,
j
++)
{
final
int
start
=
_inlineSemanticsOffsets
[
i
];
final
int
end
=
_inlineSemanticsOffsets
[
i
+
1
];
// Add semantics for any text between the previous recognizer/widget and this one.
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
();
if
(
inlineElement
is
TapGestureRecognizer
)
{
final
TapGestureRecognizer
recognizer
=
inlineElement
;
configuration
.
onTap
=
recognizer
.
onTap
;
}
else
if
(
inlineElement
is
LongPressGestureRecognizer
)
{
final
LongPressGestureRecognizer
recognizer
=
inlineElement
;
configuration
.
onLongPress
=
recognizer
.
onLongPress
;
}
else
{
assert
(
false
);
}
node
.
updateWith
(
config:
configuration
);
node
.
rect
=
currentRect
;
newChildren
.
add
(
node
);
}
else
if
(
childIndex
<
children
.
length
)
{
// Add semantics for this placeholder. Semantics are precomputed in the children
// argument.
// Placeholders should not get a label, which would come through as an
// object replacement character.
configuration
.
label
=
''
;
final
SemanticsNode
childNode
=
children
.
elementAt
(
childIndex
);
if
(
info
.
isPlaceholder
)
{
final
SemanticsNode
childNode
=
children
.
elementAt
(
placeholderIndex
++);
final
TextParentData
parentData
=
child
.
parentData
;
childNode
.
rect
=
Rect
.
fromLTWH
(
childNode
.
rect
.
left
,
...
...
@@ -856,20 +841,31 @@ class RenderParagraph extends RenderBox
childNode
.
rect
.
width
*
parentData
.
scale
,
childNode
.
rect
.
height
*
parentData
.
scale
,
);
newChildren
.
add
(
children
.
elementAt
(
childIndex
));
childIndex
+=
1
;
newChildren
.
add
(
childNode
);
child
=
childAfter
(
child
);
}
else
{
final
SemanticsConfiguration
configuration
=
SemanticsConfiguration
()
..
sortKey
=
OrdinalSortKey
(
ordinal
++)
..
textDirection
=
initialDirection
..
label
=
info
.
semanticsLabel
??
info
.
text
;
if
(
info
.
recognizer
!=
null
)
{
if
(
info
.
recognizer
is
TapGestureRecognizer
)
{
final
TapGestureRecognizer
recognizer
=
info
.
recognizer
;
configuration
.
onTap
=
recognizer
.
onTap
;
}
else
if
(
info
.
recognizer
is
LongPressGestureRecognizer
)
{
final
LongPressGestureRecognizer
recognizer
=
info
.
recognizer
;
configuration
.
onLongPress
=
recognizer
.
onLongPress
;
}
else
{
assert
(
false
);
}
}
newChildren
.
add
(
SemanticsNode
()
..
updateWith
(
config:
configuration
)
..
rect
=
currentRect
,
);
}
current
=
end
;
}
if
(
current
<
rawLabel
.
length
)
{
final
SemanticsNode
node
=
SemanticsNode
();
final
SemanticsConfiguration
configuration
=
buildSemanticsConfig
(
current
,
rawLabel
.
length
);
if
(
configuration
!=
null
)
{
node
.
updateWith
(
config:
configuration
);
node
.
rect
=
currentRect
;
newChildren
.
add
(
node
);
}
start
+=
info
.
text
.
length
;
}
node
.
updateWith
(
config:
config
,
childrenInInversePaintOrder:
newChildren
);
}
...
...
packages/flutter/test/widgets/text_test.dart
View file @
c96806ec
...
...
@@ -141,6 +141,44 @@ void main() {
semantics
.
dispose
();
});
testWidgets
(
'semanticsLabel can be shorter than text'
,
(
WidgetTester
tester
)
async
{
final
SemanticsTester
semantics
=
SemanticsTester
(
tester
);
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
RichText
(
text:
TextSpan
(
children:
<
InlineSpan
>[
const
TextSpan
(
text:
'Some Text'
,
semanticsLabel:
''
,
),
TextSpan
(
text:
'Clickable'
,
recognizer:
TapGestureRecognizer
()..
onTap
=
()
{
},
),
]),
),
));
final
TestSemantics
expectedSemantics
=
TestSemantics
.
root
(
children:
<
TestSemantics
>[
TestSemantics
(
children:
<
TestSemantics
>[
TestSemantics
(
textDirection:
TextDirection
.
ltr
,
),
TestSemantics
(
label:
'Clickable'
,
actions:
<
SemanticsAction
>[
SemanticsAction
.
tap
],
textDirection:
TextDirection
.
ltr
,
),
],
),
],
);
expect
(
semantics
,
hasSemantics
(
expectedSemantics
,
ignoreTransform:
true
,
ignoreId:
true
,
ignoreRect:
true
));
semantics
.
dispose
();
});
testWidgets
(
'recognizers split semantic node'
,
(
WidgetTester
tester
)
async
{
final
SemanticsTester
semantics
=
SemanticsTester
(
tester
);
const
TextStyle
textStyle
=
TextStyle
(
fontFamily:
'Ahem'
);
...
...
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