Skip to content

Commit 660992a

Browse files
authored
Fix text.rich to merge widget span (#113461)
* Fix text.rich to merge widget span * migrate to new API * update * addressing comment * addressing comments * update * addressing comment * Update
1 parent 56e1bdd commit 660992a

File tree

4 files changed

+312
-21
lines changed

4 files changed

+312
-21
lines changed

packages/flutter/lib/src/rendering/object.dart

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'package:flutter/semantics.dart';
1515

1616
import 'debug.dart';
1717
import 'layer.dart';
18+
import 'proxy_box.dart';
1819

1920
export 'package:flutter/foundation.dart' show
2021
DiagnosticPropertiesBuilder,
@@ -3244,14 +3245,15 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
32443245
final SemanticsConfiguration config = _semanticsConfiguration;
32453246
bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
32463247

3247-
final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
3248+
bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
32483249
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
32493250
final List<SemanticsConfiguration> childConfigurations = <SemanticsConfiguration>[];
32503251
final bool explicitChildNode = config.explicitChildNodes || parent is! RenderObject;
32513252
final bool hasChildConfigurationsDelegate = config.childConfigurationsDelegate != null;
32523253
final Map<SemanticsConfiguration, _InterestingSemanticsFragment> configToFragment = <SemanticsConfiguration, _InterestingSemanticsFragment>{};
32533254
final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
32543255
final List<List<_InterestingSemanticsFragment>> siblingMergeFragmentGroups = <List<_InterestingSemanticsFragment>>[];
3256+
final bool hasTags = config.tagsForChildren?.isNotEmpty ?? false;
32553257
visitChildrenForSemantics((RenderObject renderChild) {
32563258
assert(!_needsLayout);
32573259
final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
@@ -3267,7 +3269,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
32673269
}
32683270
for (final _InterestingSemanticsFragment fragment in parentFragment.mergeUpFragments) {
32693271
fragment.addAncestor(this);
3270-
fragment.addTags(config.tagsForChildren);
3272+
if (hasTags) {
3273+
fragment.addTags(config.tagsForChildren!);
3274+
}
32713275
if (hasChildConfigurationsDelegate && fragment.config != null) {
32723276
// This fragment need to go through delegate to determine whether it
32733277
// merge up or not.
@@ -3283,7 +3287,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
32833287
for (final List<_InterestingSemanticsFragment> siblingMergeGroup in parentFragment.siblingMergeGroups) {
32843288
for (final _InterestingSemanticsFragment siblingMergingFragment in siblingMergeGroup) {
32853289
siblingMergingFragment.addAncestor(this);
3286-
siblingMergingFragment.addTags(config.tagsForChildren);
3290+
if (hasTags) {
3291+
siblingMergingFragment.addTags(config.tagsForChildren!);
3292+
}
32873293
}
32883294
siblingMergeFragmentGroups.add(siblingMergeGroup);
32893295
}
@@ -3296,14 +3302,25 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
32963302
for (final _InterestingSemanticsFragment fragment in mergeUpFragments) {
32973303
fragment.markAsExplicit();
32983304
}
3299-
} else if (hasChildConfigurationsDelegate && childConfigurations.isNotEmpty) {
3305+
} else if (hasChildConfigurationsDelegate) {
33003306
final ChildSemanticsConfigurationsResult result = config.childConfigurationsDelegate!(childConfigurations);
33013307
mergeUpFragments.addAll(
3302-
result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!),
3308+
result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) {
3309+
final _InterestingSemanticsFragment? fragment = configToFragment[config];
3310+
if (fragment == null) {
3311+
// Parent fragment of Incomplete fragments can't be a forking
3312+
// fragment since they need to be merged.
3313+
producesForkingFragment = false;
3314+
return _IncompleteSemanticsFragment(config: config, owner: this);
3315+
}
3316+
return fragment;
3317+
}),
33033318
);
33043319
for (final Iterable<SemanticsConfiguration> group in result.siblingMergeGroups) {
33053320
siblingMergeFragmentGroups.add(
3306-
group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!).toList()
3321+
group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) {
3322+
return configToFragment[config] ?? _IncompleteSemanticsFragment(config: config, owner: this);
3323+
}).toList(),
33073324
);
33083325
}
33093326
}
@@ -4184,10 +4201,10 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
41844201
Set<SemanticsTag>? _tagsForChildren;
41854202

41864203
/// Tag all children produced by [compileChildren] with `tags`.
4187-
void addTags(Iterable<SemanticsTag>? tags) {
4188-
if (tags == null || tags.isEmpty) {
4189-
return;
4190-
}
4204+
///
4205+
/// `tags` must not be empty.
4206+
void addTags(Iterable<SemanticsTag> tags) {
4207+
assert(tags.isNotEmpty);
41914208
_tagsForChildren ??= <SemanticsTag>{};
41924209
_tagsForChildren!.addAll(tags);
41934210
}
@@ -4281,6 +4298,48 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
42814298
}
42824299
}
42834300

4301+
/// A fragment with partial information that must not form an explicit
4302+
/// semantics node without merging into another _SwitchableSemanticsFragment.
4303+
///
4304+
/// This fragment is generated from synthetic SemanticsConfiguration returned from
4305+
/// [SemanticsConfiguration.childConfigurationsDelegate].
4306+
class _IncompleteSemanticsFragment extends _InterestingSemanticsFragment {
4307+
_IncompleteSemanticsFragment({
4308+
required this.config,
4309+
required super.owner,
4310+
}) : super(dropsSemanticsOfPreviousSiblings: false);
4311+
4312+
@override
4313+
void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
4314+
assert(false, 'This fragment must be a leaf node');
4315+
}
4316+
4317+
@override
4318+
void compileChildren({
4319+
required Rect? parentSemanticsClipRect,
4320+
required Rect? parentPaintClipRect,
4321+
required double elevationAdjustment,
4322+
required List<SemanticsNode> result,
4323+
required List<SemanticsNode> siblingNodes,
4324+
}) {
4325+
// There is nothing to do because this fragment must be a leaf node and
4326+
// must not be explicit.
4327+
}
4328+
4329+
@override
4330+
final SemanticsConfiguration config;
4331+
4332+
@override
4333+
void markAsExplicit() {
4334+
assert(
4335+
false,
4336+
'SemanticsConfiguration created in '
4337+
'SemanticsConfiguration.childConfigurationsDelegate must not produce '
4338+
'its own semantics node'
4339+
);
4340+
}
4341+
}
4342+
42844343
/// An [_InterestingSemanticsFragment] that can be told to only add explicit
42854344
/// [SemanticsNode]s to the parent.
42864345
///
@@ -4559,6 +4618,14 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
45594618
}
45604619
}
45614620

4621+
@override
4622+
void addTags(Iterable<SemanticsTag> tags) {
4623+
super.addTags(tags);
4624+
// _ContainerSemanticsFragments add their tags to child fragments through
4625+
// this method. This fragment must make sure its _config is in sync.
4626+
tags.forEach(_config.addTagForChildren);
4627+
}
4628+
45624629
void _ensureConfigIsWritable() {
45634630
if (!_isConfigWritable) {
45644631
_config = _config.copy();

packages/flutter/lib/src/rendering/paragraph.dart

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ class RenderParagraph extends RenderBox
119119

120120
static final String _placeholderCharacter = String.fromCharCode(PlaceholderSpan.placeholderCodeUnit);
121121
final TextPainter _textPainter;
122-
AttributedString? _cachedAttributedLabel;
122+
123+
List<AttributedString>? _cachedAttributedLabels;
124+
123125
List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
124126

125127
/// The text to display.
@@ -135,7 +137,7 @@ class RenderParagraph extends RenderBox
135137
break;
136138
case RenderComparison.paint:
137139
_textPainter.text = value;
138-
_cachedAttributedLabel = null;
140+
_cachedAttributedLabels = null;
139141
_cachedCombinedSemanticsInfos = null;
140142
_extractPlaceholderSpans(value);
141143
markNeedsPaint();
@@ -144,7 +146,7 @@ class RenderParagraph extends RenderBox
144146
case RenderComparison.layout:
145147
_textPainter.text = value;
146148
_overflowShader = null;
147-
_cachedAttributedLabel = null;
149+
_cachedAttributedLabels = null;
148150
_cachedCombinedSemanticsInfos = null;
149151
_extractPlaceholderSpans(value);
150152
markNeedsLayout();
@@ -1035,12 +1037,23 @@ class RenderParagraph extends RenderBox
10351037
void describeSemanticsConfiguration(SemanticsConfiguration config) {
10361038
super.describeSemanticsConfiguration(config);
10371039
_semanticsInfo = text.getSemanticsInformation();
1040+
bool needsAssembleSemanticsNode = false;
1041+
bool needsChildConfigrationsDelegate = false;
1042+
for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
1043+
if (info.recognizer != null) {
1044+
needsAssembleSemanticsNode = true;
1045+
break;
1046+
}
1047+
needsChildConfigrationsDelegate = needsChildConfigrationsDelegate || info.isPlaceholder;
1048+
}
10381049

1039-
if (_semanticsInfo!.any((InlineSpanSemanticsInformation info) => info.recognizer != null)) {
1050+
if (needsAssembleSemanticsNode) {
10401051
config.explicitChildNodes = true;
10411052
config.isSemanticBoundary = true;
1053+
} else if (needsChildConfigrationsDelegate) {
1054+
config.childConfigurationsDelegate = _childSemanticsConfigurationsDelegate;
10421055
} else {
1043-
if (_cachedAttributedLabel == null) {
1056+
if (_cachedAttributedLabels == null) {
10441057
final StringBuffer buffer = StringBuffer();
10451058
int offset = 0;
10461059
final List<StringAttribute> attributes = <StringAttribute>[];
@@ -1050,21 +1063,77 @@ class RenderParagraph extends RenderBox
10501063
final TextRange originalRange = infoAttribute.range;
10511064
attributes.add(
10521065
infoAttribute.copy(
1053-
range: TextRange(start: offset + originalRange.start,
1054-
end: offset + originalRange.end)
1066+
range: TextRange(
1067+
start: offset + originalRange.start,
1068+
end: offset + originalRange.end,
1069+
),
10551070
),
10561071
);
10571072
}
10581073
buffer.write(label);
10591074
offset += label.length;
10601075
}
1061-
_cachedAttributedLabel = AttributedString(buffer.toString(), attributes: attributes);
1076+
_cachedAttributedLabels = <AttributedString>[AttributedString(buffer.toString(), attributes: attributes)];
10621077
}
1063-
config.attributedLabel = _cachedAttributedLabel!;
1078+
config.attributedLabel = _cachedAttributedLabels![0];
10641079
config.textDirection = textDirection;
10651080
}
10661081
}
10671082

1083+
ChildSemanticsConfigurationsResult _childSemanticsConfigurationsDelegate(List<SemanticsConfiguration> childConfigs) {
1084+
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
1085+
int placeholderIndex = 0;
1086+
int childConfigsIndex = 0;
1087+
int attributedLabelCacheIndex = 0;
1088+
InlineSpanSemanticsInformation? seenTextInfo;
1089+
_cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
1090+
for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
1091+
if (info.isPlaceholder) {
1092+
if (seenTextInfo != null) {
1093+
builder.markAsMergeUp(_createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex));
1094+
attributedLabelCacheIndex += 1;
1095+
}
1096+
// Mark every childConfig belongs to this placeholder to merge up group.
1097+
while (childConfigsIndex < childConfigs.length &&
1098+
childConfigs[childConfigsIndex].tagsChildrenWith(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
1099+
builder.markAsMergeUp(childConfigs[childConfigsIndex]);
1100+
childConfigsIndex += 1;
1101+
}
1102+
placeholderIndex += 1;
1103+
} else {
1104+
seenTextInfo = info;
1105+
}
1106+
}
1107+
1108+
// Handle plain text info at the end.
1109+
if (seenTextInfo != null) {
1110+
builder.markAsMergeUp(_createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex));
1111+
}
1112+
return builder.build();
1113+
}
1114+
1115+
SemanticsConfiguration _createSemanticsConfigForTextInfo(InlineSpanSemanticsInformation textInfo, int cacheIndex) {
1116+
assert(!textInfo.requiresOwnNode);
1117+
final List<AttributedString> cachedStrings = _cachedAttributedLabels ??= <AttributedString>[];
1118+
assert(cacheIndex <= cachedStrings.length);
1119+
final bool hasCache = cacheIndex < cachedStrings.length;
1120+
1121+
late AttributedString attributedLabel;
1122+
if (hasCache) {
1123+
attributedLabel = cachedStrings[cacheIndex];
1124+
} else {
1125+
assert(cachedStrings.length == cacheIndex);
1126+
attributedLabel = AttributedString(
1127+
textInfo.semanticsLabel ?? textInfo.text,
1128+
attributes: textInfo.stringAttributes,
1129+
);
1130+
cachedStrings.add(attributedLabel);
1131+
}
1132+
return SemanticsConfiguration()
1133+
..textDirection = textDirection
1134+
..attributedLabel = attributedLabel;
1135+
}
1136+
10681137
// Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
10691138
// can be re-used when [assembleSemanticsNode] is called again. This ensures
10701139
// stable ids for the [SemanticsNode]s of [TextSpan]s across

packages/flutter/lib/src/semantics/semantics.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ class AttributedString {
346346
}
347347

348348
@override
349-
int get hashCode => Object.hash(string, attributes,);
349+
int get hashCode => Object.hash(string, attributes);
350350

351351
@override
352352
String toString() {
@@ -3805,7 +3805,8 @@ class SemanticsConfiguration {
38053805
/// which of them should be merged upwards into the parent SemanticsNode.
38063806
///
38073807
/// The input list of [SemanticsConfiguration]s can be empty if the rendering
3808-
/// object of this semantics configuration is a leaf node.
3808+
/// object of this semantics configuration is a leaf node or child rendering
3809+
/// objects do not contribute to the semantics.
38093810
ChildSemanticsConfigurationsDelegate? get childConfigurationsDelegate => _childConfigurationsDelegate;
38103811
ChildSemanticsConfigurationsDelegate? _childConfigurationsDelegate;
38113812
set childConfigurationsDelegate(ChildSemanticsConfigurationsDelegate? value) {

0 commit comments

Comments
 (0)