Skip to content

Commit 6d2b718

Browse files
loic-sharmagaaclarke
authored andcommitted
[Embedder API] Add semantic string attributes (flutter#44616)
Flutter's `SemanticNode`s use [`StringAttribute`](https://api.flutter.dev/flutter/dart-ui/StringAttribute-class.html)s to provide additional information on text values for assistive technologies. This exposes the string attributes on the embedder API so that embedders can apply string attributes to their semantics trees. Addresses flutter/flutter#119970 Part of flutter/flutter#98948 Previous pull request: flutter#44553 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent c82ab95 commit 6d2b718

File tree

6 files changed

+386
-3
lines changed

6 files changed

+386
-3
lines changed

lib/ui/semantics/string_attribute.h

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ using StringAttributes = std::vector<StringAttributePtr>;
2020
// * engine/src/flutter/lib/ui/semantics.dart
2121
// * engine/src/flutter/lib/web_ui/lib/semantics.dart
2222
// * engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java
23+
// * engine/src/flutter/shell/platform/embedder/embedder.h
2324
// * engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_api_test.dart
2425
// * engine/src/flutter/testing/dart/semantics_test.dart
2526

shell/platform/embedder/embedder.h

+79-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
// - Function signatures (names, argument counts, argument order, and argument
2626
// type) cannot change.
2727
// - The core behavior of existing functions cannot change.
28-
// - Instead of nesting structures by value within another structure, prefer
29-
// nesting by pointer. This ensures that adding members to the nested struct
30-
// does not break the ABI of the parent struct.
28+
// - Instead of nesting structures by value within another structure/union,
29+
// prefer nesting by pointer. This ensures that adding members to the nested
30+
// struct does not break the ABI of the parent struct/union.
3131
// - Instead of array of structures, prefer array of pointers to structures.
3232
// This ensures that array indexing does not break if members are added
3333
// to the structure.
@@ -1064,6 +1064,57 @@ typedef int64_t FlutterPlatformViewIdentifier;
10641064
FLUTTER_EXPORT
10651065
extern const int32_t kFlutterSemanticsNodeIdBatchEnd;
10661066

1067+
// The enumeration of possible string attributes that affect how assistive
1068+
// technologies announce a string.
1069+
//
1070+
// See dart:ui's implementers of the StringAttribute abstract class.
1071+
typedef enum {
1072+
// Indicates the string should be announced character by character.
1073+
kSpellOut,
1074+
// Indicates the string should be announced using the specified locale.
1075+
kLocale,
1076+
} FlutterStringAttributeType;
1077+
1078+
// Indicates the assistive technology should announce out the string character
1079+
// by character.
1080+
//
1081+
// See dart:ui's SpellOutStringAttribute.
1082+
typedef struct {
1083+
/// The size of this struct. Must be sizeof(FlutterSpellOutStringAttribute).
1084+
size_t struct_size;
1085+
} FlutterSpellOutStringAttribute;
1086+
1087+
// Indicates the assistive technology should announce the string using the
1088+
// specified locale.
1089+
//
1090+
// See dart:ui's LocaleStringAttribute.
1091+
typedef struct {
1092+
/// The size of this struct. Must be sizeof(FlutterLocaleStringAttribute).
1093+
size_t struct_size;
1094+
// The locale of this attribute.
1095+
const char* locale;
1096+
} FlutterLocaleStringAttribute;
1097+
1098+
// Indicates how the assistive technology should treat the string.
1099+
//
1100+
// See dart:ui's StringAttribute.
1101+
typedef struct {
1102+
/// The size of this struct. Must be sizeof(FlutterStringAttribute).
1103+
size_t struct_size;
1104+
// The position this attribute starts.
1105+
size_t start;
1106+
// The next position after the attribute ends.
1107+
size_t end;
1108+
/// The type of the attribute described by the subsequent union.
1109+
FlutterStringAttributeType type;
1110+
union {
1111+
// Indicates the string should be announced character by character.
1112+
const FlutterSpellOutStringAttribute* spell_out;
1113+
// Indicates the string should be announced using the specified locale.
1114+
const FlutterLocaleStringAttribute* locale;
1115+
};
1116+
} FlutterStringAttribute;
1117+
10671118
/// A node that represents some semantic data.
10681119
///
10691120
/// The semantics tree is maintained during the semantics phase of the pipeline
@@ -1215,6 +1266,31 @@ typedef struct {
12151266
FlutterPlatformViewIdentifier platform_view_id;
12161267
/// A textual tooltip attached to the node.
12171268
const char* tooltip;
1269+
// The number of string attributes associated with the `label`.
1270+
size_t label_attribute_count;
1271+
// Array of string attributes associated with the `label`.
1272+
// Has length `label_attribute_count`.
1273+
const FlutterStringAttribute** label_attributes;
1274+
// The number of string attributes associated with the `hint`.
1275+
size_t hint_attribute_count;
1276+
// Array of string attributes associated with the `hint`.
1277+
// Has length `hint_attribute_count`.
1278+
const FlutterStringAttribute** hint_attributes;
1279+
// The number of string attributes associated with the `value`.
1280+
size_t value_attribute_count;
1281+
// Array of string attributes associated with the `value`.
1282+
// Has length `value_attribute_count`.
1283+
const FlutterStringAttribute** value_attributes;
1284+
// The number of string attributes associated with the `increased_value`.
1285+
size_t increased_value_attribute_count;
1286+
// Array of string attributes associated with the `increased_value`.
1287+
// Has length `increased_value_attribute_count`.
1288+
const FlutterStringAttribute** increased_value_attributes;
1289+
// The number of string attributes associated with the `decreased_value`.
1290+
size_t decreased_value_attribute_count;
1291+
// Array of string attributes associated with the `decreased_value`.
1292+
// Has length `decreased_value_attribute_count`.
1293+
const FlutterStringAttribute** decreased_value_attributes;
12181294
} FlutterSemanticsNode2;
12191295

12201296
/// `FlutterSemanticsCustomAction` ID used as a sentinel to signal the end of a

shell/platform/embedder/embedder_semantics_update.cc

+83
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ void EmbedderSemanticsUpdate2::AddNode(const SemanticsNode& node) {
131131
transform.get(SkMatrix::kMPersp0), transform.get(SkMatrix::kMPersp1),
132132
transform.get(SkMatrix::kMPersp2)};
133133

134+
auto label_attributes = CreateStringAttributes(node.labelAttributes);
135+
auto hint_attributes = CreateStringAttributes(node.hintAttributes);
136+
auto value_attributes = CreateStringAttributes(node.valueAttributes);
137+
auto increased_value_attributes =
138+
CreateStringAttributes(node.increasedValueAttributes);
139+
auto decreased_value_attributes =
140+
CreateStringAttributes(node.decreasedValueAttributes);
141+
134142
nodes_.push_back({
135143
sizeof(FlutterSemanticsNode2),
136144
node.id,
@@ -161,6 +169,16 @@ void EmbedderSemanticsUpdate2::AddNode(const SemanticsNode& node) {
161169
node.customAccessibilityActions.data(),
162170
node.platformViewId,
163171
node.tooltip.c_str(),
172+
label_attributes.count,
173+
label_attributes.attributes,
174+
hint_attributes.count,
175+
hint_attributes.attributes,
176+
value_attributes.count,
177+
value_attributes.attributes,
178+
increased_value_attributes.count,
179+
increased_value_attributes.attributes,
180+
decreased_value_attributes.count,
181+
decreased_value_attributes.attributes,
164182
});
165183
}
166184

@@ -175,4 +193,69 @@ void EmbedderSemanticsUpdate2::AddAction(
175193
});
176194
}
177195

196+
EmbedderSemanticsUpdate2::EmbedderStringAttributes
197+
EmbedderSemanticsUpdate2::CreateStringAttributes(
198+
const StringAttributes& attributes) {
199+
// Minimize allocations if attributes are empty.
200+
if (attributes.empty()) {
201+
return {.count = 0, .attributes = nullptr};
202+
}
203+
204+
// Translate the engine attributes to embedder attributes.
205+
// The result vector's data is returned by this method.
206+
// The result vector will be owned by |node_string_attributes_|
207+
// so that the embedder attributes are cleaned up at the end of the
208+
// semantics update callback when when the |EmbedderSemanticsUpdate2|
209+
// is destroyed.
210+
auto result = std::make_unique<std::vector<const FlutterStringAttribute*>>();
211+
result->reserve(attributes.size());
212+
213+
for (const auto& attribute : attributes) {
214+
auto embedder_attribute = std::make_unique<FlutterStringAttribute>();
215+
embedder_attribute->struct_size = sizeof(FlutterStringAttribute);
216+
embedder_attribute->start = attribute->start;
217+
embedder_attribute->end = attribute->end;
218+
219+
switch (attribute->type) {
220+
case StringAttributeType::kLocale: {
221+
std::shared_ptr<flutter::LocaleStringAttribute> locale_attribute =
222+
std::static_pointer_cast<flutter::LocaleStringAttribute>(attribute);
223+
224+
auto embedder_locale = std::make_unique<FlutterLocaleStringAttribute>();
225+
embedder_locale->struct_size = sizeof(FlutterLocaleStringAttribute);
226+
embedder_locale->locale = locale_attribute->locale.c_str();
227+
locale_attributes_.push_back(std::move(embedder_locale));
228+
229+
embedder_attribute->type = FlutterStringAttributeType::kLocale;
230+
embedder_attribute->locale = locale_attributes_.back().get();
231+
break;
232+
}
233+
case flutter::StringAttributeType::kSpellOut: {
234+
// All spell out attributes are identical and share a lazily created
235+
// instance.
236+
if (!spell_out_attribute_) {
237+
auto spell_out_attribute_ =
238+
std::make_unique<FlutterSpellOutStringAttribute>();
239+
spell_out_attribute_->struct_size =
240+
sizeof(FlutterSpellOutStringAttribute);
241+
}
242+
243+
embedder_attribute->type = FlutterStringAttributeType::kSpellOut;
244+
embedder_attribute->spell_out = spell_out_attribute_.get();
245+
break;
246+
}
247+
}
248+
249+
string_attributes_.push_back(std::move(embedder_attribute));
250+
result->push_back(string_attributes_.back().get());
251+
}
252+
253+
node_string_attributes_.push_back(std::move(result));
254+
255+
return {
256+
.count = node_string_attributes_.back()->size(),
257+
.attributes = node_string_attributes_.back()->data(),
258+
};
259+
}
260+
178261
} // namespace flutter

shell/platform/embedder/embedder_semantics_update.h

+27
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ class EmbedderSemanticsUpdate {
4040
};
4141

4242
// A semantic update, used by the embedder API's v3 semantic update callback.
43+
//
44+
// This holds temporary embedder-specific objects that are translated from
45+
// the engine's internal representation and passed back to the semantics
46+
// update callback. Once the callback finishes, this object is destroyed
47+
// and the temporary embedder-specific objects are automatically cleaned up.
4348
class EmbedderSemanticsUpdate2 {
4449
public:
4550
EmbedderSemanticsUpdate2(const SemanticsNodeUpdates& nodes,
@@ -52,19 +57,41 @@ class EmbedderSemanticsUpdate2 {
5257
FlutterSemanticsUpdate2* get() { return &update_; }
5358

5459
private:
60+
// These fields hold temporary embedder-specific objects that
61+
// must remain valid for the duration of the semantics update callback.
62+
// They are automatically cleaned up when |EmbedderSemanticsUpdate2| is
63+
// destroyed.
5564
FlutterSemanticsUpdate2 update_;
5665
std::vector<FlutterSemanticsNode2> nodes_;
5766
std::vector<FlutterSemanticsNode2*> node_pointers_;
5867
std::vector<FlutterSemanticsCustomAction2> actions_;
5968
std::vector<FlutterSemanticsCustomAction2*> action_pointers_;
6069

70+
std::vector<std::unique_ptr<std::vector<const FlutterStringAttribute*>>>
71+
node_string_attributes_;
72+
std::vector<std::unique_ptr<FlutterStringAttribute>> string_attributes_;
73+
std::vector<std::unique_ptr<FlutterLocaleStringAttribute>> locale_attributes_;
74+
std::unique_ptr<FlutterSpellOutStringAttribute> spell_out_attribute_;
75+
6176
// Translates engine semantic nodes to embedder semantic nodes.
6277
void AddNode(const SemanticsNode& node);
6378

6479
// Translates engine semantic custom actions to embedder semantic custom
6580
// actions.
6681
void AddAction(const CustomAccessibilityAction& action);
6782

83+
// A helper struct for |CreateStringAttributes|.
84+
struct EmbedderStringAttributes {
85+
// The number of string attribute pointers in |attributes|.
86+
size_t count;
87+
// An array of string attribute pointers.
88+
const FlutterStringAttribute** attributes;
89+
};
90+
91+
// Translates engine string attributes to embedder string attributes.
92+
EmbedderStringAttributes CreateStringAttributes(
93+
const StringAttributes& attribute);
94+
6895
FML_DISALLOW_COPY_AND_ASSIGN(EmbedderSemanticsUpdate2);
6996
};
7097

shell/platform/embedder/fixtures/main.dart

+76
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,82 @@ void a11y_main() async {
290290
notifySemanticsEnabled(PlatformDispatcher.instance.semanticsEnabled);
291291
}
292292

293+
@pragma('vm:entry-point')
294+
void a11y_string_attributes() async {
295+
// 1: Wait until semantics are enabled.
296+
if (!PlatformDispatcher.instance.semanticsEnabled) {
297+
await semanticsChanged;
298+
}
299+
300+
// 2: Update semantics with string attributes.
301+
final SemanticsUpdateBuilder builder = SemanticsUpdateBuilder()
302+
..updateNode(
303+
id: 42,
304+
label: 'What is the meaning of life?',
305+
labelAttributes: <StringAttribute>[
306+
LocaleStringAttribute(
307+
range: TextRange(start: 0, end: 'What is the meaning of life?'.length),
308+
locale: Locale('en'),
309+
),
310+
SpellOutStringAttribute(
311+
range: TextRange(start: 0, end: 1),
312+
),
313+
],
314+
rect: Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
315+
transform: kTestTransform,
316+
childrenInTraversalOrder: Int32List.fromList(<int>[84, 96]),
317+
childrenInHitTestOrder: Int32List.fromList(<int>[96, 84]),
318+
actions: 0,
319+
flags: 0,
320+
maxValueLength: 0,
321+
currentValueLength: 0,
322+
textSelectionBase: 0,
323+
textSelectionExtent: 0,
324+
platformViewId: 0,
325+
scrollChildren: 0,
326+
scrollIndex: 0,
327+
scrollPosition: 0.0,
328+
scrollExtentMax: 0.0,
329+
scrollExtentMin: 0.0,
330+
elevation: 0.0,
331+
thickness: 0.0,
332+
hint: "It's a number",
333+
hintAttributes: <StringAttribute>[
334+
LocaleStringAttribute(
335+
range: TextRange(start: 0, end: 1),
336+
locale: Locale('en'),
337+
),
338+
LocaleStringAttribute(
339+
range: TextRange(start: 2, end: 3),
340+
locale: Locale('fr'),
341+
),
342+
],
343+
value: '42',
344+
valueAttributes: <StringAttribute>[
345+
LocaleStringAttribute(
346+
range: TextRange(start: 0, end: '42'.length),
347+
locale: Locale('en', 'US'),
348+
),
349+
],
350+
increasedValue: '43',
351+
increasedValueAttributes: <StringAttribute>[
352+
SpellOutStringAttribute(
353+
range: TextRange(start: 0, end: 1),
354+
),
355+
SpellOutStringAttribute(
356+
range: TextRange(start: 1, end: 2),
357+
),
358+
],
359+
decreasedValue: '41',
360+
decreasedValueAttributes: <StringAttribute>[],
361+
tooltip: 'tooltip',
362+
additionalActions: Int32List(0),
363+
);
364+
365+
PlatformDispatcher.instance.views.first.updateSemantics(builder.build());
366+
signalNativeTest();
367+
}
368+
293369
@pragma('vm:entry-point')
294370
void platform_messages_response() {
295371
PlatformDispatcher.instance.onPlatformMessage =

0 commit comments

Comments
 (0)