Commit f8a2bd20 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Redesign Semantic Tree Compilation Algorithm (#12605)

* Oct 12 10:12am

* implicit_semantics_test.dart passes

* refactoring

* works in nice

* minor rename

* more doc comments

* to be explicit check better

* fix test

* ++

* ++

* semantics_9_test (BlockSemantics) and implicit_semantics_test are passing

* doc updates

* tiny refactor

* fix static errors in tests

* fix gesture detector

* ++

* ++

* geometry

* ++

* remove noGeometry

* revert test

* +

* all tests but scrolling/clipping pass

* clipping works

* scrolling halfway

* sliver tests pass

* ALL TESTS PASS

* SemanticsNode changed

* docs and tiny fixes

* card test

* more doc comments

* remove missed print

* more tests

* make test pass on Linux

* remove changes to intellij proj file

* review comments
parent d47d2687
...@@ -251,8 +251,7 @@ class RecipeCard extends StatelessWidget { ...@@ -251,8 +251,7 @@ class RecipeCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new MergeSemantics( return new GestureDetector(
child: new GestureDetector(
onTap: onTap, onTap: onTap,
child: new Card( child: new Card(
child: new Column( child: new Column(
...@@ -294,7 +293,6 @@ class RecipeCard extends StatelessWidget { ...@@ -294,7 +293,6 @@ class RecipeCard extends StatelessWidget {
], ],
), ),
), ),
),
); );
} }
} }
......
...@@ -31,8 +31,7 @@ class GalleryItem extends StatelessWidget { ...@@ -31,8 +31,7 @@ class GalleryItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new MergeSemantics( return new ListTile(
child: new ListTile(
title: new Text(title), title: new Text(title),
subtitle: new Text(subtitle), subtitle: new Text(subtitle),
onTap: () { onTap: () {
...@@ -44,7 +43,6 @@ class GalleryItem extends StatelessWidget { ...@@ -44,7 +43,6 @@ class GalleryItem extends StatelessWidget {
Navigator.pushNamed(context, routeName); Navigator.pushNamed(context, routeName);
} }
} }
),
); );
} }
} }
......
...@@ -187,7 +187,7 @@ final Duration _kDiscreteTransitionDuration = const Duration(milliseconds: 500); ...@@ -187,7 +187,7 @@ final Duration _kDiscreteTransitionDuration = const Duration(milliseconds: 500);
const double _kAdjustmentUnit = 0.1; // Matches iOS implementation of material slider. const double _kAdjustmentUnit = 0.1; // Matches iOS implementation of material slider.
class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsActionHandler { class _RenderCupertinoSlider extends RenderConstrainedBox {
_RenderCupertinoSlider({ _RenderCupertinoSlider({
@required double value, @required double value,
int divisions, int divisions,
...@@ -253,7 +253,7 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc ...@@ -253,7 +253,7 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc
final bool wasInteractive = isInteractive; final bool wasInteractive = isInteractive;
_onChanged = value; _onChanged = value;
if (wasInteractive != isInteractive) if (wasInteractive != isInteractive)
markNeedsSemanticsUpdate(noGeometry: true); markNeedsSemanticsUpdate();
} }
TextDirection get textDirection => _textDirection; TextDirection get textDirection => _textDirection;
...@@ -379,31 +379,25 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc ...@@ -379,31 +379,25 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc
} }
@override @override
bool get isSemanticBoundary => isInteractive; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
@override config.isSemanticBoundary = isInteractive;
SemanticsAnnotator get semanticsAnnotator => _annotate; if (isInteractive) {
config.addAction(SemanticsAction.increase, _increaseAction);
config.addAction(SemanticsAction.decrease, _decreaseAction);
}
}
double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _kAdjustmentUnit;
void _annotate(SemanticsNode semantics) { void _increaseAction() {
if (isInteractive) if (isInteractive)
semantics.addAdjustmentActions(); onChanged((value + _semanticActionUnit).clamp(0.0, 1.0));
} }
@override void _decreaseAction() {
void performAction(SemanticsAction action) {
final double unit = divisions != null ? 1.0 / divisions : _kAdjustmentUnit;
switch (action) {
case SemanticsAction.increase:
if (isInteractive) if (isInteractive)
onChanged((value + unit).clamp(0.0, 1.0)); onChanged((value - _semanticActionUnit).clamp(0.0, 1.0));
break;
case SemanticsAction.decrease:
if (isInteractive)
onChanged((value - unit).clamp(0.0, 1.0));
break;
default:
assert(false);
break;
}
} }
} }
...@@ -156,7 +156,7 @@ const Color _kTrackColor = CupertinoColors.lightBackgroundGray; ...@@ -156,7 +156,7 @@ const Color _kTrackColor = CupertinoColors.lightBackgroundGray;
const Duration _kReactionDuration = const Duration(milliseconds: 300); const Duration _kReactionDuration = const Duration(milliseconds: 300);
const Duration _kToggleDuration = const Duration(milliseconds: 200); const Duration _kToggleDuration = const Duration(milliseconds: 200);
class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsActionHandler { class _RenderCupertinoSwitch extends RenderConstrainedBox {
_RenderCupertinoSwitch({ _RenderCupertinoSwitch({
@required bool value, @required bool value,
@required Color activeColor, @required Color activeColor,
...@@ -214,7 +214,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc ...@@ -214,7 +214,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc
if (value == _value) if (value == _value)
return; return;
_value = value; _value = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: true, noGeometry: true); markNeedsSemanticsUpdate(onlyLocalUpdates: true);
_position _position
..curve = Curves.ease ..curve = Curves.ease
..reverseCurve = Curves.ease.flipped; ..reverseCurve = Curves.ease.flipped;
...@@ -254,7 +254,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc ...@@ -254,7 +254,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc
_onChanged = value; _onChanged = value;
if (wasInteractive != isInteractive) { if (wasInteractive != isInteractive) {
markNeedsPaint(); markNeedsPaint();
markNeedsSemanticsUpdate(noGeometry: true); markNeedsSemanticsUpdate();
} }
} }
...@@ -375,23 +375,13 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc ...@@ -375,23 +375,13 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc
} }
@override @override
bool get isSemanticBoundary => isInteractive; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
@override config.isSemanticBoundary = isInteractive;
SemanticsAnnotator get semanticsAnnotator => _annotate;
void _annotate(SemanticsNode semantics) {
semantics
..hasCheckedState = true
..isChecked = _value;
if (isInteractive) if (isInteractive)
semantics.addAction(SemanticsAction.tap); config.addAction(SemanticsAction.tap, _handleTap);
} config.isChecked = _value;
@override
void performAction(SemanticsAction action) {
if (action == SemanticsAction.tap)
_handleTap();
} }
final CupertinoThumbPainter _thumbPainter = new CupertinoThumbPainter(); final CupertinoThumbPainter _thumbPainter = new CupertinoThumbPainter();
......
...@@ -80,7 +80,9 @@ class Card extends StatelessWidget { ...@@ -80,7 +80,9 @@ class Card extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Container( return new Semantics(
container: true,
child: new Container(
margin: const EdgeInsets.all(4.0), margin: const EdgeInsets.all(4.0),
child: new Material( child: new Material(
color: color, color: color,
...@@ -88,6 +90,7 @@ class Card extends StatelessWidget { ...@@ -88,6 +90,7 @@ class Card extends StatelessWidget {
elevation: elevation, elevation: elevation,
child: child child: child
) )
),
); );
} }
} }
...@@ -307,7 +307,7 @@ double _getPreferredTotalHeight(String label) { ...@@ -307,7 +307,7 @@ double _getPreferredTotalHeight(String label) {
return 2 * _kReactionRadius + _getAdditionalHeightForLabel(label); return 2 * _kReactionRadius + _getAdditionalHeightForLabel(label);
} }
class _RenderSlider extends RenderBox implements SemanticsActionHandler { class _RenderSlider extends RenderBox {
_RenderSlider({ _RenderSlider({
@required double value, @required double value,
int divisions, int divisions,
...@@ -441,7 +441,7 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler { ...@@ -441,7 +441,7 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler {
_onChanged = value; _onChanged = value;
if (wasInteractive != isInteractive) { if (wasInteractive != isInteractive) {
markNeedsPaint(); markNeedsPaint();
markNeedsSemanticsUpdate(noGeometry: true); markNeedsSemanticsUpdate();
} }
} }
...@@ -708,31 +708,25 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler { ...@@ -708,31 +708,25 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler {
} }
@override @override
bool get isSemanticBoundary => isInteractive; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
@override config.isSemanticBoundary = isInteractive;
SemanticsAnnotator get semanticsAnnotator => _annotate; if (isInteractive) {
config.addAction(SemanticsAction.increase, _increaseAction);
config.addAction(SemanticsAction.decrease, _decreaseAction);
}
}
double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _kAdjustmentUnit;
void _annotate(SemanticsNode semantics) { void _increaseAction() {
if (isInteractive) if (isInteractive)
semantics.addAdjustmentActions(); onChanged((value + _semanticActionUnit).clamp(0.0, 1.0));
} }
@override void _decreaseAction() {
void performAction(SemanticsAction action) {
final double unit = divisions != null ? 1.0 / divisions : _kAdjustmentUnit;
switch (action) {
case SemanticsAction.increase:
if (isInteractive) if (isInteractive)
onChanged((value + unit).clamp(0.0, 1.0)); onChanged((value - _semanticActionUnit).clamp(0.0, 1.0));
break;
case SemanticsAction.decrease:
if (isInteractive)
onChanged((value - unit).clamp(0.0, 1.0));
break;
default:
assert(false);
break;
}
} }
} }
...@@ -754,22 +754,20 @@ class _TabBarState extends State<TabBar> { ...@@ -754,22 +754,20 @@ class _TabBarState extends State<TabBar> {
// reflect the intrinsic width of their labels. // reflect the intrinsic width of their labels.
final int tabCount = widget.tabs.length; final int tabCount = widget.tabs.length;
for (int index = 0; index < tabCount; index++) { for (int index = 0; index < tabCount; index++) {
wrappedTabs[index] = new MergeSemantics( wrappedTabs[index] = new InkWell(
child: new Stack(
children: <Widget>[
new InkWell(
onTap: () { _handleTap(index); }, onTap: () { _handleTap(index); },
child: new Padding( child: new Padding(
padding: new EdgeInsets.only(bottom: widget.indicatorWeight), padding: new EdgeInsets.only(bottom: widget.indicatorWeight),
child: wrappedTabs[index], child: new Stack(
), children: <Widget>[
), wrappedTabs[index],
new Semantics( new Semantics(
selected: index == _currentIndex, selected: index == _currentIndex,
// TODO(goderbauer): I10N-ify // TODO(goderbauer): I10N-ify
label: 'Tab ${index + 1} of $tabCount', label: 'Tab ${index + 1} of $tabCount',
), ),
], ]
),
), ),
); );
if (!widget.isScrollable) if (!widget.isScrollable)
......
...@@ -18,7 +18,7 @@ final Tween<double> _kRadialReactionRadiusTween = new Tween<double>(begin: 0.0, ...@@ -18,7 +18,7 @@ final Tween<double> _kRadialReactionRadiusTween = new Tween<double>(begin: 0.0,
/// This class handles storing the current value, dispatching ValueChanged on a /// This class handles storing the current value, dispatching ValueChanged on a
/// tap gesture and driving a changed animation. Subclasses are responsible for /// tap gesture and driving a changed animation. Subclasses are responsible for
/// painting. /// painting.
abstract class RenderToggleable extends RenderConstrainedBox implements SemanticsActionHandler { abstract class RenderToggleable extends RenderConstrainedBox {
/// Creates a toggleable render object. /// Creates a toggleable render object.
/// ///
/// The [value], [activeColor], and [inactiveColor] arguments must not be /// The [value], [activeColor], and [inactiveColor] arguments must not be
...@@ -122,7 +122,7 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic ...@@ -122,7 +122,7 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic
if (value == _value) if (value == _value)
return; return;
_value = value; _value = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: true, noGeometry: true); markNeedsSemanticsUpdate(onlyLocalUpdates: true);
_position _position
..curve = Curves.easeIn ..curve = Curves.easeIn
..reverseCurve = Curves.easeOut; ..reverseCurve = Curves.easeOut;
...@@ -178,7 +178,7 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic ...@@ -178,7 +178,7 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic
_onChanged = value; _onChanged = value;
if (wasInteractive != isInteractive) { if (wasInteractive != isInteractive) {
markNeedsPaint(); markNeedsPaint();
markNeedsSemanticsUpdate(noGeometry: true); markNeedsSemanticsUpdate();
} }
} }
...@@ -283,23 +283,13 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic ...@@ -283,23 +283,13 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic
} }
@override @override
bool get isSemanticBoundary => isInteractive; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
@override config.isSemanticBoundary = isInteractive;
SemanticsAnnotator get semanticsAnnotator => _annotate;
void _annotate(SemanticsNode semantics) {
semantics
..hasCheckedState = true
..isChecked = _value;
if (isInteractive) if (isInteractive)
semantics.addAction(SemanticsAction.tap); config.addAction(SemanticsAction.tap, _handleTap);
} config.isChecked = _value;
@override
void performAction(SemanticsAction action) {
if (action == SemanticsAction.tap)
_handleTap();
} }
@override @override
......
...@@ -546,419 +546,6 @@ typedef void RenderObjectVisitor(RenderObject child); ...@@ -546,419 +546,6 @@ typedef void RenderObjectVisitor(RenderObject child);
/// Used by [RenderObject.invokeLayoutCallback]. /// Used by [RenderObject.invokeLayoutCallback].
typedef void LayoutCallback<T extends Constraints>(T constraints); typedef void LayoutCallback<T extends Constraints>(T constraints);
class _SemanticsGeometry {
_SemanticsGeometry() : _transform = new Matrix4.identity();
_SemanticsGeometry.withClipFrom(_SemanticsGeometry other) {
_clipRect = other?._clipRect;
_transform = new Matrix4.identity();
}
_SemanticsGeometry.copy(_SemanticsGeometry other) {
if (other != null) {
_clipRect = other._clipRect;
_transform = new Matrix4.copy(other._transform);
} else {
_transform = new Matrix4.identity();
}
}
Rect _clipRect;
Rect _intersectClipRect(Rect other) {
if (_clipRect == null)
return other;
if (other == null)
return _clipRect;
return _clipRect.intersect(other);
}
Matrix4 _transform;
void applyAncestorChain(List<RenderObject> ancestorChain) {
for (int index = ancestorChain.length-1; index > 0; index -= 1) {
final RenderObject parent = ancestorChain[index];
final RenderObject child = ancestorChain[index-1];
_clipRect = _intersectClipRect(parent.describeApproximatePaintClip(child));
if (_clipRect != null) {
if (_clipRect.isEmpty) {
_clipRect = Rect.zero;
} else {
final Matrix4 clipTransform = new Matrix4.identity();
parent.applyPaintTransform(child, clipTransform);
_clipRect = MatrixUtils.inverseTransformRect(clipTransform, _clipRect);
}
}
parent.applyPaintTransform(child, _transform);
}
}
void updateSemanticsNode({
@required RenderObject rendering,
@required SemanticsNode semantics,
}) {
assert(rendering != null);
assert(semantics != null);
semantics.transform = _transform;
final Rect semanticBounds = rendering.semanticBounds;
if (_clipRect != null) {
final Rect rect = _clipRect.intersect(semanticBounds);
semantics.rect = rect;
semantics.wasAffectedByClip = rect != semanticBounds;
} else {
semantics.rect = semanticBounds;
semantics.wasAffectedByClip = false;
}
}
}
/// Describes the shape of the semantic tree.
///
/// A [_SemanticsFragment] object is a node in a short-lived tree which is used
/// to create the final [SemanticsNode] tree that is sent to the semantics
/// server. These objects have a [SemanticsAnnotator], and a list of
/// [_SemanticsFragment] children.
abstract class _SemanticsFragment {
_SemanticsFragment({
@required RenderObject renderObjectOwner,
this.annotator,
List<_SemanticsFragment> children,
this.dropSemanticsOfPreviousSiblings,
}) : assert(renderObjectOwner != null),
assert(() {
if (children == null)
return true;
final Set<_SemanticsFragment> seenChildren = new Set<_SemanticsFragment>();
for (_SemanticsFragment child in children)
assert(seenChildren.add(child)); // check for duplicate adds
return true;
}()),
_ancestorChain = <RenderObject>[renderObjectOwner],
_children = children ?? const <_SemanticsFragment>[];
final SemanticsAnnotator annotator;
bool dropSemanticsOfPreviousSiblings;
bool get producesSemanticNodes => true;
List<RenderObject> _ancestorChain;
void addAncestor(RenderObject ancestor) {
_ancestorChain.add(ancestor);
}
RenderObject get renderObjectOwner => _ancestorChain.first;
List<_SemanticsFragment> _children;
bool _debugCompiled = false;
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics });
@override
String toString() => describeIdentity(this);
}
/// A SemanticsFragment that doesn't produce any [SemanticsNode]s when compiled.
class _EmptySemanticsFragment extends _SemanticsFragment {
_EmptySemanticsFragment({
@required RenderObject renderObjectOwner,
bool dropSemanticsOfPreviousSiblings,
}) : super(renderObjectOwner: renderObjectOwner, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
@override
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics }) sync* { }
@override
bool get producesSemanticNodes => false;
}
/// Represents a [RenderObject] which is in no way dirty.
///
/// This class has no children and no annotators, and when compiled, it returns
/// the [SemanticsNode] that the [RenderObject] already has. (We still update
/// the matrix, since that comes from the (dirty) ancestors.)
class _CleanSemanticsFragment extends _SemanticsFragment {
_CleanSemanticsFragment({
@required RenderObject renderObjectOwner,
bool dropSemanticsOfPreviousSiblings,
}) : assert(renderObjectOwner != null),
assert(renderObjectOwner._semantics != null),
super(
renderObjectOwner: renderObjectOwner,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings
);
@override
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics }) sync* {
assert(!_debugCompiled);
assert(() { _debugCompiled = true; return true; }());
final SemanticsNode node = renderObjectOwner._semantics;
assert(node != null);
if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain);
geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node);
if (node.isInvisible)
return; // drop the node
} else {
assert(_ancestorChain.length == 1);
}
yield node;
}
}
abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
_InterestingSemanticsFragment({
RenderObject renderObjectOwner,
SemanticsAnnotator annotator,
Iterable<_SemanticsFragment> children,
bool dropSemanticsOfPreviousSiblings,
}) : super(
renderObjectOwner: renderObjectOwner,
annotator: annotator,
children: children,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
@override
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics }) sync* {
assert(!_debugCompiled);
assert(() { _debugCompiled = true; return true; }());
final SemanticsNode node = establishSemanticsNode(geometry, currentSemantics);
if (node.isInvisible)
return; // drop the node
final List<SemanticsNode> children = <SemanticsNode>[];
for (_SemanticsFragment child in _children) {
assert(child._ancestorChain.last == renderObjectOwner);
children.addAll(child.compile(
geometry: createSemanticsGeometryForChild(geometry),
currentSemantics: _children.length > 1 ? null : node,
));
}
yield* finalizeSemanticsNode(node, children);
}
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics);
Iterable<SemanticsNode> finalizeSemanticsNode(SemanticsNode node, List<SemanticsNode> children);
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry);
}
/// Represents the [RenderObject] found at the top of the render tree.
///
/// This class always compiles to a [SemanticsNode] with ID 0.
class _RootSemanticsFragment extends _InterestingSemanticsFragment {
_RootSemanticsFragment({
RenderObject renderObjectOwner,
SemanticsAnnotator annotator,
Iterable<_SemanticsFragment> children,
bool dropSemanticsOfPreviousSiblings,
}) : super(
renderObjectOwner: renderObjectOwner,
annotator: annotator,
children: children,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
@override
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics) {
assert(_ancestorChain.length == 1);
assert(geometry == null);
assert(currentSemantics == null);
renderObjectOwner._semantics ??= new SemanticsNode.root(
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
owner: renderObjectOwner.owner.semanticsOwner,
showOnScreen: renderObjectOwner.showOnScreen,
);
final SemanticsNode node = renderObjectOwner._semantics;
assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity()));
assert(!node.wasAffectedByClip);
node.rect = renderObjectOwner.semanticBounds;
return node;
}
@override
Iterable<SemanticsNode> finalizeSemanticsNode(SemanticsNode node, List<SemanticsNode> children) sync* {
if (annotator != null)
annotator(node);
node.addChildren(children);
node.finalizeChildren();
yield node;
}
@override
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
return new _SemanticsGeometry();
}
}
/// Represents a RenderObject that has [isSemanticBoundary] set to true.
///
/// It returns the SemanticsNode for that [RenderObject].
class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
_ConcreteSemanticsFragment({
RenderObject renderObjectOwner,
SemanticsAnnotator annotator,
Iterable<_SemanticsFragment> children,
bool dropSemanticsOfPreviousSiblings,
bool mergeIntoParent,
bool mergesAllDescendants,
}) : _mergeIntoParent = mergeIntoParent,
_mergesAllDescendants = mergesAllDescendants,
super(
renderObjectOwner: renderObjectOwner,
annotator: annotator,
children: children,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
final bool _mergeIntoParent;
final bool _mergesAllDescendants;
@override
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics) {
renderObjectOwner._semantics ??= new SemanticsNode(
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
showOnScreen: renderObjectOwner.showOnScreen,
);
final SemanticsNode node = renderObjectOwner._semantics;
node.isMergedIntoParent = _mergeIntoParent;
node.mergeAllDescendantsIntoThisNode = _mergesAllDescendants;
if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain);
geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node);
} else {
assert(_ancestorChain.length == 1);
}
return node;
}
@override
Iterable<SemanticsNode> finalizeSemanticsNode(SemanticsNode node, List<SemanticsNode> children) sync* {
renderObjectOwner.assembleSemanticsNode(node, children);
yield node;
}
@override
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
return new _SemanticsGeometry.withClipFrom(geometry);
}
}
/// Represents a RenderObject that does not have [isSemanticBoundary] set to
/// true, but which does have some semantic annotators.
///
/// When it is compiled, if the nearest ancestor [_SemanticsFragment] that isn't
/// also an [_ImplicitSemanticsFragment] is a [_RootSemanticsFragment] or a
/// [_ConcreteSemanticsFragment], then the [SemanticsNode] from that object is
/// reused. Otherwise, a new one is created.
class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
_ImplicitSemanticsFragment({
RenderObject renderObjectOwner,
SemanticsAnnotator annotator,
Iterable<_SemanticsFragment> children,
bool dropSemanticsOfPreviousSiblings,
bool mergeIntoParent,
bool mergesAllDescendants,
}) : _mergeIntoParent = mergeIntoParent,
_mergesAllDescendants = mergesAllDescendants,
super(
renderObjectOwner: renderObjectOwner,
annotator: annotator,
children: children,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
// If true, this fragment will introduce its own node into the Semantics Tree.
// If false, a borrowed semantics node from an ancestor is used.
bool _introducesOwnNode;
final bool _mergeIntoParent;
final bool _mergesAllDescendants;
@override
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics) {
SemanticsNode node;
assert(_introducesOwnNode == null);
assert(annotator != null || _mergesAllDescendants);
_introducesOwnNode = currentSemantics == null;
if (_introducesOwnNode) {
renderObjectOwner._semantics ??= new SemanticsNode(
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
showOnScreen: renderObjectOwner.showOnScreen,
);
node = renderObjectOwner._semantics;
node.isMergedIntoParent = _mergeIntoParent;
} else {
renderObjectOwner._semantics = null;
node = currentSemantics;
}
if (!node.mergeAllDescendantsIntoThisNode)
node.mergeAllDescendantsIntoThisNode = _mergesAllDescendants;
if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain);
if (_introducesOwnNode)
geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node);
} else {
assert(_ancestorChain.length == 1);
}
return node;
}
@override
Iterable<SemanticsNode> finalizeSemanticsNode(SemanticsNode node, List<SemanticsNode> children) sync* {
if (annotator != null)
annotator(node);
if (_introducesOwnNode) {
node.addChildren(children);
node.finalizeChildren();
yield node;
} else {
// Transparently forward children to the borrowed node.
yield* children;
}
}
@override
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
if (_introducesOwnNode)
return new _SemanticsGeometry.withClipFrom(geometry);
return new _SemanticsGeometry.copy(geometry);
}
}
/// Represents a [RenderObject] that introduces no semantics of its own, but
/// which has two or more descendants that do introduce semantics
/// (and which are not ancestors or descendants of each other).
class _ForkingSemanticsFragment extends _SemanticsFragment {
_ForkingSemanticsFragment({
RenderObject renderObjectOwner,
@required Iterable<_SemanticsFragment> children,
bool dropSemanticsOfPreviousSiblings,
}) : assert(children != null),
assert(children.length > 1),
super(
renderObjectOwner: renderObjectOwner,
children: children,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings
);
@override
Iterable<SemanticsNode> compile({
@required _SemanticsGeometry geometry,
SemanticsNode currentSemantics,
}) sync* {
assert(!_debugCompiled);
assert(() { _debugCompiled = true; return true; }());
assert(geometry != null);
geometry.applyAncestorChain(_ancestorChain);
for (_SemanticsFragment child in _children) {
assert(child._ancestorChain.last == renderObjectOwner);
yield* child.compile(
geometry: new _SemanticsGeometry.copy(geometry),
currentSemantics: null,
);
}
}
}
/// A reference to the semantics tree. /// A reference to the semantics tree.
/// ///
/// The framework maintains the semantics tree (used for accessibility and /// The framework maintains the semantics tree (used for accessibility and
...@@ -1629,7 +1216,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1629,7 +1216,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
_needsPaint = false; _needsPaint = false;
markNeedsPaint(); markNeedsPaint();
} }
if (_needsSemanticsUpdate && isSemanticBoundary) { if (_needsSemanticsUpdate && _semanticsConfiguration.isSemanticBoundary) {
// Don't enter this block if we've never updated semantics at all; // Don't enter this block if we've never updated semantics at all;
// scheduleInitialSemantics() will handle it // scheduleInitialSemantics() will handle it
_needsSemanticsUpdate = false; _needsSemanticsUpdate = false;
...@@ -2528,44 +2115,27 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2528,44 +2115,27 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
owner.requestVisualUpdate(); owner.requestVisualUpdate();
} }
/// Whether this [RenderObject] introduces a new box for accessibility purposes. @protected
/// void describeSemanticsConfiguration(SemanticsConfiguration config) {
/// See also: // Nothing to do by default.
/// }
/// * [semanticsAnnotator], which fills in the [SemanticsNode] implied by
/// setting [isSemanticBoundary] to true.
bool get isSemanticBoundary => false;
/// Whether this [RenderObject] makes other [RenderObject]s previously painted // Use [_semanticsConfiguration] to access.
/// within the same semantic boundary unreachable for accessibility purposes. SemanticsConfiguration _cachedSemanticsConfiguration;
///
/// If true is returned, the [SemanticsNode]s for all siblings and cousins
/// of this node, that are earlier in a depth-first pre-order traversal, are
/// dropped from the semantics tree up until a semantic boundary (as defined
/// by [isSemanticBoundary]) is reached.
///
/// If [isSemanticBoundary] and [isBlockingSemanticsOfPreviouslyPaintedNodes]
/// is set on the same node, all previously painted siblings and cousins
/// up until the next ancestor that is a semantic boundary are dropped.
///
/// Paint order as established by [visitChildrenForSemantics] is used to
/// determine if a node is previous to this one.
bool get isBlockingSemanticsOfPreviouslyPaintedNodes => false;
/// Whether the semantic information provided by this [RenderObject] and all SemanticsConfiguration get _semanticsConfiguration {
/// of its descendants should be treated as one logical entity. if (_cachedSemanticsConfiguration == null) {
/// _cachedSemanticsConfiguration = new SemanticsConfiguration();
/// If true is returned, the descendants of this [RenderObject]'s describeSemanticsConfiguration(_cachedSemanticsConfiguration);
/// [SemanticsNode] will merge their semantic information into the }
/// [SemanticsNode] representing this [RenderObject]. return _cachedSemanticsConfiguration;
bool get isMergingSemanticsOfDescendants => false; }
/// The bounding box, in the local coordinate system, of this /// The bounding box, in the local coordinate system, of this
/// object, for accessibility purposes. /// object, for accessibility purposes.
Rect get semanticBounds; Rect get semanticBounds;
bool _needsSemanticsUpdate = true; bool _needsSemanticsUpdate = true;
bool _needsSemanticsGeometryUpdate = true;
SemanticsNode _semantics; SemanticsNode _semantics;
/// The semantics of this render object. /// The semantics of this render object.
...@@ -2590,27 +2160,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2590,27 +2160,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// Should only be called on objects whose [parent] is not a [RenderObject]. /// Should only be called on objects whose [parent] is not a [RenderObject].
void clearSemantics() { void clearSemantics() {
_needsSemanticsUpdate = true; _needsSemanticsUpdate = true;
_needsSemanticsGeometryUpdate = true;
_semantics = null; _semantics = null;
visitChildren((RenderObject child) { visitChildren((RenderObject child) {
child.clearSemantics(); child.clearSemantics();
}); });
} }
/// Restore the [SemanticsNode]s owned by this render object to its default
/// state.
@mustCallSuper
@protected
void resetSemantics() {
_semantics?.reset();
}
/// Mark this node as needing an update to its semantics description. /// Mark this node as needing an update to its semantics description.
/// ///
/// The parameters `onlyLocalUpdates` and `noGeometry` tell the framework
/// how much of the semantics have changed. Bigger changes (indicated by
/// setting one or both parameters to false) are more expansive to compute.
///
/// `onlyLocalUpdates` should be set to true to reduce cost if the semantics /// `onlyLocalUpdates` should be set to true to reduce cost if the semantics
/// update does not in any way change the shape of the semantics tree (e.g. /// update does not in any way change the shape of the semantics tree (e.g.
/// [SemanticsNode]s will neither be added/removed from the tree nor be moved /// [SemanticsNode]s will neither be added/removed from the tree nor be moved
...@@ -2626,22 +2183,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2626,22 +2183,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// 2. [semanticsAnnotator] changed from or to returning null and /// 2. [semanticsAnnotator] changed from or to returning null and
/// [isSemanticBoundary] isn't true. /// [isSemanticBoundary] isn't true.
/// ///
/// `noGeometry` should be set to true to reduce cost if the geometry (e.g. /// If `onlyLocalUpdates` is incorrectly set to true, asserts
/// size and position) of the corresponding [SemanticsNode] has not
/// changed. Examples for such semantic updates that don't require a geometry
/// update are changes to flags, labels, or actions.
///
/// If `onlyLocalUpdates` or `noGeometry` are incorrectly set to true, asserts
/// might throw or the computed semantics tree might be out-of-date without /// might throw or the computed semantics tree might be out-of-date without
/// warning. /// warning.
void markNeedsSemanticsUpdate({ bool onlyLocalUpdates: false, bool noGeometry: false }) { void markNeedsSemanticsUpdate({ bool onlyLocalUpdates: false }) {
assert(!attached || !owner._debugDoingSemantics); assert(!attached || !owner._debugDoingSemantics);
if ((attached && owner._semanticsOwner == null) || (_needsSemanticsUpdate && onlyLocalUpdates && (_needsSemanticsGeometryUpdate || noGeometry))) _cachedSemanticsConfiguration = null;
if ((attached && owner._semanticsOwner == null))
return; return;
if (!noGeometry && (_semantics == null || (_semantics.hasChildren && _semantics.wasAffectedByClip))) {
// Since the geometry might have changed, we need to make sure to reapply any clips.
_needsSemanticsGeometryUpdate = true;
}
if (onlyLocalUpdates) { if (onlyLocalUpdates) {
// The shape of the tree didn't change, but the details did. // The shape of the tree didn't change, but the details did.
// If we have our own SemanticsNode (our _semantics isn't null) // If we have our own SemanticsNode (our _semantics isn't null)
...@@ -2651,13 +2200,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2651,13 +2200,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
while (node._semantics == null && node.parent is RenderObject) { while (node._semantics == null && node.parent is RenderObject) {
if (node._needsSemanticsUpdate) if (node._needsSemanticsUpdate)
return; return;
node._cachedSemanticsConfiguration = null;
node._needsSemanticsUpdate = true; node._needsSemanticsUpdate = true;
node.resetSemantics();
node = node.parent; node = node.parent;
} }
if (!node._needsSemanticsUpdate) { if (!node._needsSemanticsUpdate) {
node.resetSemantics();
node._needsSemanticsUpdate = true; node._needsSemanticsUpdate = true;
node._cachedSemanticsConfiguration = null;
if (owner != null) { if (owner != null) {
owner._nodesNeedingSemantics.add(node); owner._nodesNeedingSemantics.add(node);
owner.requestVisualUpdate(); owner.requestVisualUpdate();
...@@ -2674,10 +2223,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2674,10 +2223,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
if (node.parent is! RenderObject) if (node.parent is! RenderObject)
break; break;
node._needsSemanticsUpdate = true; node._needsSemanticsUpdate = true;
node.resetSemantics(); node._cachedSemanticsConfiguration = null;
node = node.parent; node = node.parent;
} while (node._semantics == null); } while (node._semantics == null);
node.resetSemantics();
if (node != this && _semantics != null && _needsSemanticsUpdate) { if (node != this && _semantics != null && _needsSemanticsUpdate) {
// If [this] node has already been added to [owner._nodesNeedingSemantics] // If [this] node has already been added to [owner._nodesNeedingSemantics]
// remove it as it is no longer guaranteed that its semantics // remove it as it is no longer guaranteed that its semantics
...@@ -2689,6 +2237,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2689,6 +2237,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
owner._nodesNeedingSemantics.remove(this); owner._nodesNeedingSemantics.remove(this);
} }
if (!node._needsSemanticsUpdate) { if (!node._needsSemanticsUpdate) {
node._cachedSemanticsConfiguration = null;
node._needsSemanticsUpdate = true; node._needsSemanticsUpdate = true;
if (owner != null) { if (owner != null) {
owner._nodesNeedingSemantics.add(node); owner._nodesNeedingSemantics.add(node);
...@@ -2699,122 +2248,119 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2699,122 +2248,119 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
} }
/// Updates the semantic information of the render object. /// Updates the semantic information of the render object.
///
/// This is essentially a two-pass walk of the render tree. The first pass
/// determines the shape of the output tree (captured in
/// [_SemanticsFragment]s), and the second creates the nodes of this tree and
/// hooks them together. The second walk is a sparse walk; it only walks the
/// nodes that are interesting for the purpose of semantics.
void _updateSemantics() { void _updateSemantics() {
try { assert(_semanticsConfiguration.isSemanticBoundary || parent is! RenderObject);
assert(_needsSemanticsUpdate); final _SemanticsFragment fragment = _getSemanticsForParent(
assert(_semantics != null || parent is! RenderObject); parentClippingRect: _semanticsClippingRect,
final _SemanticsFragment fragment = _getSemanticsFragment(mergeIntoParent: _semantics?.parent?.isPartOfNodeMerging ?? false); mergeIntoParent: _semantics?.parent?.isPartOfNodeMerging ?? false,
);
assert(fragment is _InterestingSemanticsFragment); assert(fragment is _InterestingSemanticsFragment);
final SemanticsNode node = fragment.compile().single; final _InterestingSemanticsFragment interestingFragment = fragment;
assert(node != null); final SemanticsNode node = interestingFragment.compileChildren().single;
assert(!node.isInvisible); // Fragment only wants to add this node's SemanticsNode to the parent.
assert(node == _semantics); assert(interestingFragment.config == null && node == _semantics);
} catch (e, stack) {
_debugReportException('_updateSemantics', e, stack);
}
} }
/// Core function that walks the render tree to obtain the semantics. /// Clip that needs to be applied to any [SemanticsNode] owned by this
/// [RenderObject].
///
/// Can be null if no clip is to be applied.
/// ///
/// It collects semantic annotators for this RenderObject, then walks its /// Updated by [_getSemanticsForParent].
/// children collecting [_SemanticsFragments] for them, and then returns an Rect _semanticsClippingRect;
/// appropriate [_SemanticsFragment] object that describes the RenderObject's
/// semantics. /// Returns the semantics that this node would like to add to its parent.
_SemanticsFragment _getSemanticsFragment({ bool mergeIntoParent: false }) { _SemanticsFragment _getSemanticsForParent({
// early-exit if we're not dirty and have our own semantics @required Rect parentClippingRect,
if (!_needsSemanticsUpdate && isSemanticBoundary) { @required bool mergeIntoParent,
assert(_semantics != null); }) {
if (mergeIntoParent == _semantics.isMergedIntoParent) { assert(mergeIntoParent != null);
return new _CleanSemanticsFragment(
renderObjectOwner: this, final SemanticsConfiguration config = _semanticsConfiguration;
dropSemanticsOfPreviousSiblings: isBlockingSemanticsOfPreviouslyPaintedNodes, bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
final _SemanticsGeometry geometry = new _SemanticsGeometry(
owner: this,
parentClippingRect: parentClippingRect,
); );
_semanticsClippingRect = geometry.clipRect;
// Shortcut if this fragment cannot be exposed to user.
if (_semanticsConfiguration.isSemanticBoundary && !mergeIntoParent && geometry.isInvisible)
return new _ContainerSemanticsFragment(dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
final List<_InterestingSemanticsFragment> fragments = <_InterestingSemanticsFragment>[];
final Set<_InterestingSemanticsFragment> toBeMarkedExplicit = new Set<_InterestingSemanticsFragment>();
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
visitChildrenForSemantics((RenderObject renderChild) {
final _SemanticsFragment fragment = renderChild._getSemanticsForParent(
parentClippingRect: _semanticsClippingRect,
mergeIntoParent: childrenMergeIntoParent,
);
if (fragment.dropsSemanticsOfPreviousSiblings) {
fragments.clear();
toBeMarkedExplicit.clear();
if (!config.isSemanticBoundary)
dropSemanticsOfPreviousSiblings = true;
} }
// Figure out which child fragments are to be made explicit.
for (_InterestingSemanticsFragment fragment in fragment.interestingFragments) {
fragments.add(fragment);
fragment.geometry.addAncestor(this);
fragment.addTags(config.tagsForChildren);
if (config.explicitChildNodes || parent is! RenderObject) {
fragment.markAsExplicit();
continue;
} }
List<_SemanticsFragment> children; if (!fragment.hasConfigForParent || producesForkingFragment)
bool dropSemanticsOfPreviousSiblings = isBlockingSemanticsOfPreviouslyPaintedNodes; continue;
final bool childrenMergeIntoParent = mergeIntoParent || isMergingSemanticsOfDescendants; if (!config.isCompatibleWith(fragment.config))
visitChildrenForSemantics((RenderObject child) { toBeMarkedExplicit.add(fragment);
if (_needsSemanticsGeometryUpdate) { for (_InterestingSemanticsFragment siblingFragment in fragments.sublist(0, fragments.length - 1)) {
// If our geometry changed, make sure the child also does a if (!fragment.config.isCompatibleWith(siblingFragment.config)) {
// full update so that any changes to the clip are fully toBeMarkedExplicit.add(fragment);
// applied. toBeMarkedExplicit.add(siblingFragment);
child._needsSemanticsUpdate = true;
child._needsSemanticsGeometryUpdate = true;
}
final _SemanticsFragment fragment = child._getSemanticsFragment(mergeIntoParent: childrenMergeIntoParent);
assert(fragment != null);
if (fragment.dropSemanticsOfPreviousSiblings) {
children = null; // throw away all left siblings of [child].
dropSemanticsOfPreviousSiblings = true;
} }
if (fragment.producesSemanticNodes) {
fragment.addAncestor(this);
children ??= <_SemanticsFragment>[];
assert(!children.contains(fragment));
children.add(fragment);
} }
});
if (isSemanticBoundary && !isBlockingSemanticsOfPreviouslyPaintedNodes) {
// Don't propagate [dropSemanticsOfPreviousSiblings] up through a semantic boundary.
dropSemanticsOfPreviousSiblings = false;
} }
});
for (_InterestingSemanticsFragment fragment in toBeMarkedExplicit)
fragment.markAsExplicit();
_needsSemanticsUpdate = false; _needsSemanticsUpdate = false;
_needsSemanticsGeometryUpdate = false;
final SemanticsAnnotator annotator = semanticsAnnotator; _SemanticsFragment result;
if (parent is! RenderObject) { if (parent is! RenderObject) {
assert(!config.hasBeenAnnotated);
assert(!mergeIntoParent); assert(!mergeIntoParent);
assert(!isMergingSemanticsOfDescendants); result = new _RootSemanticsFragment(
return new _RootSemanticsFragment( owner: this,
renderObjectOwner: this, dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
annotator: annotator,
children: children,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
); );
} } else if (producesForkingFragment) {
if (isSemanticBoundary) { result = new _ContainerSemanticsFragment(
return new _ConcreteSemanticsFragment( dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
renderObjectOwner: this,
annotator: annotator,
children: children,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
mergeIntoParent: mergeIntoParent,
mergesAllDescendants: isMergingSemanticsOfDescendants,
); );
} } else {
if (annotator != null || isMergingSemanticsOfDescendants) { result = new _SwitchableSemanticsFragment(
return new _ImplicitSemanticsFragment( config: config,
renderObjectOwner: this, geometry: geometry,
annotator: annotator,
children: children,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
mergeIntoParent: mergeIntoParent, mergeIntoParent: mergeIntoParent,
mergesAllDescendants: isMergingSemanticsOfDescendants, owner: this,
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
); );
if (config.isSemanticBoundary) {
final _SwitchableSemanticsFragment fragment = result;
fragment.markAsExplicit();
} }
_semantics = null;
if (children == null) {
// Introduces no semantics and has no descendants that introduce semantics.
return new _EmptySemanticsFragment(
renderObjectOwner: this,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
}
if (children.length > 1) {
return new _ForkingSemanticsFragment(
renderObjectOwner: this,
children: children,
dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
} }
assert(children.length == 1);
return children.single..dropSemanticsOfPreviousSiblings = dropSemanticsOfPreviousSiblings; result.addAll(fragments);
return result;
} }
/// Called when collecting the semantics of this node. /// Called when collecting the semantics of this node.
...@@ -2829,51 +2375,25 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2829,51 +2375,25 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
visitChildren(visitor); visitChildren(visitor);
} }
/// Returns a function that will annotate a [SemanticsNode] with the semantics
/// of this [RenderObject].
///
/// To annotate a [SemanticsNode] for this node, return an annotator that adds
/// the annotations. When the behavior of the annotator would change (e.g. the
/// box is now checked rather than unchecked), call [markNeedsSemanticsUpdate]
/// to indicate to the rendering system that the semantics tree needs to be
/// rebuilt.
///
/// To introduce a new [SemanticsNode], set [isSemanticBoundary] to true for
/// this object. The function returned by this function will be used to
/// annotate the [SemanticsNode] for this object.
///
/// Semantic annotations are not persisted between subsequent calls to an
/// annotator. The [SemanticsAnnotator] should always set all options
/// (e.g. flags, labels, actions, etc.) to the values it cares about given
/// the current state of the [RenderObject].
///
/// If the return value will change from null to non-null (or vice versa), and
/// [isSemanticBoundary] isn't true, then the associated call to
/// [markNeedsSemanticsUpdate] must not have `onlyLocalUpdates` set, as it is
/// possible that the node should be entirely removed.
///
/// If the annotation should only happen under certain conditions, null
/// should be returned if those conditions are currently not met to avoid
/// the creation of an empty [SemanticsNode].
SemanticsAnnotator get semanticsAnnotator => null;
/// Assemble the [SemanticsNode] for this [RenderObject]. /// Assemble the [SemanticsNode] for this [RenderObject].
/// ///
/// If [isSemanticBoundary] is true, this method is called with the semantics /// If [isSemanticBoundary] is true, this method is called with the `node`
/// [node] created for this [RenderObject] and its semantics [children]. /// created for this [RenderObject], the `config` to be applied to that node
/// By default, the method will annotate [node] with the [semanticsAnnotator] /// and the `children` [SemanticNode]s that decedents of this RenderObject
/// and add the [children] to it. /// have generated.
///
/// By default, the method will annotate `node` with `config` and add the
/// `children` to it.
/// ///
/// Subclasses can override this method to add additional [SemanticsNode]s /// Subclasses can override this method to add additional [SemanticsNode]s
/// to the tree. If a subclass adds additional nodes in this method, it also /// to the tree.
/// needs to override [resetSemantics] to call [SemanticsNode.reset] on those void assembleSemanticsNode(
/// additional [SemanticsNode]s. SemanticsNode node,
void assembleSemanticsNode(SemanticsNode node, Iterable<SemanticsNode> children) { SemanticsConfiguration config,
Iterable<SemanticsNode> children,
) {
assert(node == _semantics); assert(node == _semantics);
if (semanticsAnnotator != null) node.updateWith(config: config, childrenInInversePaintOrder: children);
semanticsAnnotator(node);
node.addChildren(children);
node.finalizeChildren();
} }
// EVENTS // EVENTS
...@@ -2978,10 +2498,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2978,10 +2498,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
description.add(new DiagnosticsProperty<SemanticsNode>('semantics node', _semantics, defaultValue: null)); description.add(new DiagnosticsProperty<SemanticsNode>('semantics node', _semantics, defaultValue: null));
description.add(new FlagProperty( description.add(new FlagProperty(
'isBlockingSemanticsOfPreviouslyPaintedNodes', 'isBlockingSemanticsOfPreviouslyPaintedNodes',
value: isBlockingSemanticsOfPreviouslyPaintedNodes, value: _semanticsConfiguration.isBlockingSemanticsOfPreviouslyPaintedNodes,
ifTrue: 'blocks semantics of earlier render objects below the common boundary', ifTrue: 'blocks semantics of earlier render objects below the common boundary',
)); ));
description.add(new FlagProperty('isSemanticBoundary', value: isSemanticBoundary, ifTrue: 'semantic boundary')); description.add(new FlagProperty('isSemanticBoundary', value: _semanticsConfiguration.isSemanticBoundary, ifTrue: 'semantic boundary'));
} }
@override @override
...@@ -3422,3 +2942,362 @@ class FlutterErrorDetailsForRendering extends FlutterErrorDetails { ...@@ -3422,3 +2942,362 @@ class FlutterErrorDetailsForRendering extends FlutterErrorDetails {
/// The RenderObject that was being processed when the exception was caught. /// The RenderObject that was being processed when the exception was caught.
final RenderObject renderObject; final RenderObject renderObject;
} }
/// Describes the semantics information a [RenderObject] wants to add to its
/// parent.
///
/// It has two notable subclasses:
/// * [_InterestingSemanticsFragment] describing actual semantic information to
/// be added to the parent.
/// * [_ContainerSemanticsFragment]: a container class to transport the semantic
/// information of multiple [_InterestingSemanticsFragment] to a parent.
abstract class _SemanticsFragment {
_SemanticsFragment({@required this.dropsSemanticsOfPreviousSiblings })
: assert (dropsSemanticsOfPreviousSiblings != null);
/// Incorporate the fragments of children into this fragment.
void addAll(Iterable<_InterestingSemanticsFragment> fragments);
/// Whether this fragment wants to make the semantics information of
/// previously painted [RenderObject]s unreachable for accessibility purposes.
///
/// See also:
/// * [SemanticsConfiguration.isBlockingSemanticsOfPreviouslyPaintedNodes]
/// describes what semantics are dropped in more detail.
final bool dropsSemanticsOfPreviousSiblings;
/// Returns [_InterestingSemanticsFragment] describing the actual semantic
/// information that this fragment wants to add to the parent.
Iterable<_InterestingSemanticsFragment> get interestingFragments;
}
/// A container used when a [RenderObject] wants to add multiple independent
/// [_InterestingSemanticsFragment] to its parent.
///
/// The [_InterestingSemanticsFragment] to be added to the parent can be
/// obtained via [interestingFragments].
class _ContainerSemanticsFragment extends _SemanticsFragment {
_ContainerSemanticsFragment({ @required bool dropsSemanticsOfPreviousSiblings })
: super(dropsSemanticsOfPreviousSiblings: dropsSemanticsOfPreviousSiblings);
@override
void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
interestingFragments.addAll(fragments);
}
@override
final List<_InterestingSemanticsFragment> interestingFragments = <_InterestingSemanticsFragment>[];
}
/// A [_SemanticsFragment] that describes which concrete semantic information
/// a [RenderObject] wants to add to the [SemanticsNode] of its parent.
///
/// Specifically, it describes what children (as returned by [compileChildren])
/// should be added to the parent's [SemanticsNode] and what [config] should be
/// merged into the parent's [SemanticsNode].
abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
_InterestingSemanticsFragment({
this.geometry,
@required this.owner,
@required bool dropsSemanticsOfPreviousSiblings
}) : assert(owner != null),
super(dropsSemanticsOfPreviousSiblings: dropsSemanticsOfPreviousSiblings);
/// The [RenderObject] that owns this fragment (and any new [SemanticNode]
/// introduced by it).
final RenderObject owner;
/// The children to be added to the parent.
Iterable<SemanticsNode> compileChildren();
/// The [SemanticsConfiguration] the child wants to merge into the parent's
/// [SemanticsNode] or null if it doesn't want to merge anything.
SemanticsConfiguration get config;
/// Disallows this fragment to merge any configuration into its parent's
/// [SemanticsNode].
///
/// After calling this the fragment will only produce children to be added
/// to the parent and it will return null for [config].
void markAsExplicit();
/// Consume the fragments of children.
///
/// For each provided fragment it will add that fragment's children to
/// this fragment's children (as returned by [compileChildren]) and merge that
/// fragment's [config] into this fragment's [config].
///
/// If a provided fragment should not merge anything into [config] call
/// [markAsExplicit] before passing the fragment to this method.
@override
void addAll(Iterable<_InterestingSemanticsFragment> fragments);
final _SemanticsGeometry geometry;
/// Whether this fragment wants to add any semantic information to the parent
/// [SemanticsNode].
bool get hasConfigForParent => config != null;
@override
Iterable<_InterestingSemanticsFragment> get interestingFragments sync* {
yield this;
}
Set<SemanticsTag> _tagsForChildren;
/// Tag all children produced by [compileChildren] with `tags`.
void addTags(Iterable<SemanticsTag> tags) {
if (tags == null || tags.isEmpty)
return;
_tagsForChildren ??= new Set<SemanticsTag>();
_tagsForChildren.addAll(tags);
}
}
/// A [_InterestingSemanticsFragment] that produces the root [SemanticsNode] of
/// the semantics tree.
///
/// The root node is available as only element in the Iterable returned by
/// [children].
class _RootSemanticsFragment extends _InterestingSemanticsFragment {
_RootSemanticsFragment({
@required RenderObject owner,
@required bool dropsSemanticsOfPreviousSiblings,
}) : super(owner: owner, dropsSemanticsOfPreviousSiblings: dropsSemanticsOfPreviousSiblings);
@override
Iterable<SemanticsNode> compileChildren() sync* {
assert(_tagsForChildren == null || _tagsForChildren.isEmpty);
owner._semantics ??= new SemanticsNode.root(
showOnScreen: owner.showOnScreen,
owner: owner.owner.semanticsOwner,
);
final SemanticsNode node = owner._semantics;
assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity()));
assert(!node.wasAffectedByClip);
node.rect = owner.semanticBounds;
assert(!node.isInvisible);
yield node;
}
@override
SemanticsConfiguration get config => null;
@override
void markAsExplicit() {
// nothing to do, we are always explicit.
}
@override
void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
final SemanticsNode root = compileChildren().first;
final List<SemanticsNode> children = <SemanticsNode>[];
for (_InterestingSemanticsFragment fragment in fragments) {
assert(fragment.config == null);
children.addAll(fragment.compileChildren());
}
root.updateWith(config: null, childrenInInversePaintOrder: children);
}
}
/// A [_InterstingSemanticsFragment] that can be told to only add explicit
/// [SemanticsNode]s to the parent.
///
/// If [markAsExplicit] was not called before this fragment is added to
/// another fragment it will merge [config] into the parent's [SemanticsNode]
/// and add its [children] to it.
///
/// If [markAsExplicit] was called before adding this fragment to another
/// fragment it will create a new [SemanticsNode]. The newly created node will
/// be annotated with the [SemanticsConfiguration] that - without the call to
/// [markAsExplicit] - would have been merged into the parent's [SemanticsNode].
/// Similarity, the new node will also take over the children that otherwise
/// would have been added to the parent's [SemanticsNode].
///
/// After a call to [markAsExplicit] the only element returned by [children]
/// is the newly created node and [config] will return null as the fragment
/// no longer wants to merge any semantic information into the parent's
/// [SemanticsNode].
class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
_SwitchableSemanticsFragment({
@required bool mergeIntoParent,
@required SemanticsConfiguration config,
@required _SemanticsGeometry geometry,
@required RenderObject owner,
@required bool dropsSemanticsOfPreviousSiblings,
}) : _mergeIntoParent = mergeIntoParent,
_config = config,
assert(mergeIntoParent != null),
assert(config != null),
super(geometry: geometry, owner: owner, dropsSemanticsOfPreviousSiblings: dropsSemanticsOfPreviousSiblings);
final bool _mergeIntoParent;
SemanticsConfiguration _config;
bool _isConfigWritable = false;
final List<SemanticsNode> _children = <SemanticsNode>[];
@override
Iterable<SemanticsNode> compileChildren() sync* {
if (!_isExplicit) {
owner._semantics = null;
yield* _children;
return;
}
if (!_mergeIntoParent && geometry.rect.isEmpty)
return; // Drop the node, it's not going to be visible.
owner._semantics ??= new SemanticsNode(showOnScreen: owner.showOnScreen);
final SemanticsNode node = owner._semantics
..isMergedIntoParent = _mergeIntoParent
..tags = _tagsForChildren;
geometry.updateNode(node);
if (_config.isSemanticBoundary) {
owner.assembleSemanticsNode(node, _config, _children);
} else {
node.updateWith(config: _config, childrenInInversePaintOrder: _children);
}
yield node;
}
@override
SemanticsConfiguration get config {
return _isExplicit ? null : _config;
}
@override
void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
for (_InterestingSemanticsFragment fragment in fragments) {
_children.addAll(fragment.compileChildren());
if (fragment.config == null)
continue;
if (!_isConfigWritable) {
_config = _config.copy();
_isConfigWritable = true;
}
_config.absorb(fragment.config);
}
}
bool _isExplicit = false;
@override
void markAsExplicit() {
_isExplicit = true;
}
}
/// Helper class that keeps track of the geometry of a [SemanticsNode].
///
/// It is used to annotate a [SemanticsNode] with the current information for
/// [SemanticsNode.rect] and [SemanticsNode.transform].
class _SemanticsGeometry {
/// `parentClippingRect` may be null if no clip is to be applied.
_SemanticsGeometry({
@required RenderObject owner,
@required Rect parentClippingRect
}) : assert(owner != null),
_ancestorChain = <RenderObject>[owner],
_parentClippingRect = parentClippingRect;
final Rect _parentClippingRect;
final List<RenderObject> _ancestorChain;
RenderObject get _owner => _ancestorChain.first;
/// The current clip [Rect] that would be applied to the [SemanticsNode]
/// owned by [owner].
Rect get clipRect {
if (!_isClipRectValid) {
__clipRect = _computeClipRect();
_isClipRectValid = true;
}
return __clipRect;
}
Rect __clipRect;
bool _isClipRectValid = false;
Rect _computeClipRect() {
if (_owner.parent is! RenderObject)
return null;
final RenderObject parent = _owner.parent;
// Clip rect in parent's coordinate system.
Rect clip = parent.describeApproximatePaintClip(_owner);
if (clip == null) {
if (_parentClippingRect == null)
return null;
clip = _parentClippingRect;
} else if (_parentClippingRect != null) {
clip = _parentClippingRect.intersect(clip);
}
assert(clip != null);
if (clip.isEmpty)
return Rect.zero;
// Translate clip into owner's coordinate system.
final Matrix4 clipTransform = new Matrix4.identity();
parent.applyPaintTransform(_owner, clipTransform);
return MatrixUtils.inverseTransformRect(clipTransform, clip);
}
/// The value for [SemanticsNode.rect].
///
/// This is essentially [RenderObject.semanticsBound] with [clipRect] applied.
Rect get rect {
if (__rect == null)
__rect = _computeRect();
return __rect;
}
Rect __rect;
Rect _computeRect() {
if (clipRect == null)
return _owner.semanticBounds;
return clipRect.intersect(_owner.semanticBounds);
}
Matrix4 _computeTransformation() {
assert(_ancestorChain.length > 1);
final Matrix4 transform = new Matrix4.identity();
for (int index = _ancestorChain.length-1; index > 0; index -= 1) {
final RenderObject parent = _ancestorChain[index];
final RenderObject child = _ancestorChain[index - 1];
parent.applyPaintTransform(child, transform);
}
return transform;
}
/// Annotates `node` with the latest geometry information.
///
/// It sets [SemanticsNode.rect], [SemanticsNode.transform], and
/// [SemanticsNode.wasAffectedByClip] to the current values.
void updateNode(SemanticsNode node) {
if (_ancestorChain.length > 1)
node.transform = _computeTransformation();
node.rect = rect;
node.wasAffectedByClip = rect != _owner.semanticBounds;
}
/// Adds the geometric information of `ancestor` to this object.
///
/// Those information are required to properly compute the value for
/// [SemanticsNode.transform].
///
/// Ancestors have to be added in order from [owner] up until the next
/// [RenderObject] that owns a [SemanticsNode] is reached.
void addAncestor(RenderObject ancestor) {
_ancestorChain.add(ancestor);
}
/// Whether a [SemanticsNode] annotated with the geometric information tracked
/// by this object would be visible on screen.
bool get isInvisible {
return clipRect != null && clipRect.isEmpty || rect.isEmpty;
}
}
...@@ -404,11 +404,11 @@ class RenderParagraph extends RenderBox { ...@@ -404,11 +404,11 @@ class RenderParagraph extends RenderBox {
} }
@override @override
SemanticsAnnotator get semanticsAnnotator => _annotate; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
void _annotate(SemanticsNode node) { config
node.label = text.toPlainText(); ..label = text.toPlainText()
node.textDirection = textDirection; ..textDirection = textDirection;
} }
@override @override
......
...@@ -2860,7 +2860,7 @@ class RenderMetaData extends RenderProxyBoxWithHitTestBehavior { ...@@ -2860,7 +2860,7 @@ class RenderMetaData extends RenderProxyBoxWithHitTestBehavior {
/// Listens for the specified gestures from the semantics server (e.g. /// Listens for the specified gestures from the semantics server (e.g.
/// an accessibility tool). /// an accessibility tool).
class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsActionHandler { class RenderSemanticsGestureHandler extends RenderProxyBox {
/// Creates a render object that listens for specific semantic gestures. /// Creates a render object that listens for specific semantic gestures.
/// ///
/// The [scrollFactor] argument must not be null. /// The [scrollFactor] argument must not be null.
...@@ -2937,11 +2937,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -2937,11 +2937,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
set onTap(GestureTapCallback value) { set onTap(GestureTapCallback value) {
if (_onTap == value) if (_onTap == value)
return; return;
final bool wasSemanticBoundary = isSemanticBoundary; final bool hadHandlers = _hasHandlers;
final bool hadHandler = _onTap != null; final bool hadHandler = _onTap != null;
_onTap = value; _onTap = value;
if ((value != null) != hadHandler) if ((value != null) != hadHandler)
markNeedsSemanticsUpdate(onlyLocalUpdates: isSemanticBoundary == wasSemanticBoundary); markNeedsSemanticsUpdate(onlyLocalUpdates: _hasHandlers == hadHandlers);
} }
/// Called when the user presses on the render object for a long period of time. /// Called when the user presses on the render object for a long period of time.
...@@ -2950,11 +2950,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -2950,11 +2950,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
set onLongPress(GestureLongPressCallback value) { set onLongPress(GestureLongPressCallback value) {
if (_onLongPress == value) if (_onLongPress == value)
return; return;
final bool wasSemanticBoundary = isSemanticBoundary; final bool hadHandlers = _hasHandlers;
final bool hadHandler = _onLongPress != null; final bool hadHandler = _onLongPress != null;
_onLongPress = value; _onLongPress = value;
if ((value != null) != hadHandler) if ((value != null) != hadHandler)
markNeedsSemanticsUpdate(onlyLocalUpdates: isSemanticBoundary == wasSemanticBoundary); markNeedsSemanticsUpdate(onlyLocalUpdates: _hasHandlers == hadHandlers);
} }
/// Called when the user scrolls to the left or to the right. /// Called when the user scrolls to the left or to the right.
...@@ -2963,11 +2963,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -2963,11 +2963,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
set onHorizontalDragUpdate(GestureDragUpdateCallback value) { set onHorizontalDragUpdate(GestureDragUpdateCallback value) {
if (_onHorizontalDragUpdate == value) if (_onHorizontalDragUpdate == value)
return; return;
final bool wasSemanticBoundary = isSemanticBoundary; final bool hadHandlers = _hasHandlers;
final bool hadHandler = _onHorizontalDragUpdate != null; final bool hadHandler = _onHorizontalDragUpdate != null;
_onHorizontalDragUpdate = value; _onHorizontalDragUpdate = value;
if ((value != null) != hadHandler) if ((value != null) != hadHandler)
markNeedsSemanticsUpdate(onlyLocalUpdates: isSemanticBoundary == wasSemanticBoundary); markNeedsSemanticsUpdate(onlyLocalUpdates: _hasHandlers == hadHandlers);
} }
/// Called when the user scrolls up or down. /// Called when the user scrolls up or down.
...@@ -2976,11 +2976,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -2976,11 +2976,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
set onVerticalDragUpdate(GestureDragUpdateCallback value) { set onVerticalDragUpdate(GestureDragUpdateCallback value) {
if (_onVerticalDragUpdate == value) if (_onVerticalDragUpdate == value)
return; return;
final bool wasSemanticBoundary = isSemanticBoundary; final bool hadHandlers = _hasHandlers;
final bool hadHandler = _onVerticalDragUpdate != null; final bool hadHandler = _onVerticalDragUpdate != null;
_onVerticalDragUpdate = value; _onVerticalDragUpdate = value;
if ((value != null) != hadHandler) if ((value != null) != hadHandler)
markNeedsSemanticsUpdate(onlyLocalUpdates: isSemanticBoundary == wasSemanticBoundary); markNeedsSemanticsUpdate(onlyLocalUpdates: _hasHandlers == hadHandlers);
} }
/// The fraction of the dimension of this render box to use when /// The fraction of the dimension of this render box to use when
...@@ -2990,8 +2990,7 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -2990,8 +2990,7 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
/// leftwards drag. /// leftwards drag.
double scrollFactor; double scrollFactor;
@override bool get _hasHandlers {
bool get isSemanticBoundary {
return onTap != null return onTap != null
|| onLongPress != null || onLongPress != null
|| onHorizontalDragUpdate != null || onHorizontalDragUpdate != null
...@@ -2999,7 +2998,38 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -2999,7 +2998,38 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
} }
@override @override
SemanticsAnnotator get semanticsAnnotator => isSemanticBoundary ? _annotate : null; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = _hasHandlers;
// TODO(goderbauer): this needs to be set even when there is only potential
// for this to become a scroll view.
config.explicitChildNodes = onHorizontalDragUpdate != null
|| onVerticalDragUpdate != null;
final Map<SemanticsAction, VoidCallback> actions = <SemanticsAction, VoidCallback>{};
if (onTap != null)
actions[SemanticsAction.tap] = onTap;
if (onLongPress != null)
actions[SemanticsAction.longPress] = onLongPress;
if (onHorizontalDragUpdate != null) {
actions[SemanticsAction.scrollRight] = _performSemanticScrollRight;
actions[SemanticsAction.scrollLeft] = _performSemanticScrollLeft;
}
if (onVerticalDragUpdate != null) {
actions[SemanticsAction.scrollUp] = _performSemanticScrollUp;
actions[SemanticsAction.scrollDown] = _performSemanticScrollDown;
}
final Iterable<SemanticsAction> actionsToAdd = validActions ?? actions.keys;
for (SemanticsAction action in actionsToAdd) {
final VoidCallback handler = actions[action];
if (handler != null)
config.addAction(action, handler);
}
}
SemanticsNode _innerNode; SemanticsNode _innerNode;
SemanticsNode _annotatedNode; SemanticsNode _annotatedNode;
...@@ -3011,75 +3041,34 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -3011,75 +3041,34 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
} }
@override @override
void assembleSemanticsNode(SemanticsNode node, Iterable<SemanticsNode> children) { void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
if (!node.hasTag(useTwoPaneSemantics)) { if (children.isEmpty || !children.first.isTagged(useTwoPaneSemantics)) {
super.assembleSemanticsNode(node, children); _annotatedNode = node;
super.assembleSemanticsNode(node, config, children);
return; return;
} }
_innerNode ??= new SemanticsNode(handler: this, showOnScreen: showOnScreen); _innerNode ??= new SemanticsNode(showOnScreen: showOnScreen);
_innerNode _innerNode
..wasAffectedByClip = node.wasAffectedByClip ..wasAffectedByClip = node.wasAffectedByClip
..isMergedIntoParent = node.isPartOfNodeMerging ..isMergedIntoParent = node.isPartOfNodeMerging
..rect = Offset.zero & node.rect.size; ..rect = Offset.zero & node.rect.size;
_annotatedNode = _innerNode;
semanticsAnnotator(_innerNode);
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode]; final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode];
final List<SemanticsNode> included = <SemanticsNode>[]; final List<SemanticsNode> included = <SemanticsNode>[];
for (SemanticsNode child in children) { for (SemanticsNode child in children) {
if (child.hasTag(excludeFromScrolling)) assert(child.isTagged(useTwoPaneSemantics));
if (child.isTagged(excludeFromScrolling))
excluded.add(child); excluded.add(child);
else else
included.add(child); included.add(child);
} }
node.addChildren(excluded); node.updateWith(config: null, childrenInInversePaintOrder: excluded);
_innerNode.addChildren(included); _innerNode.updateWith(config: config, childrenInInversePaintOrder: included);
_innerNode.finalizeChildren();
node.finalizeChildren();
}
@override
void resetSemantics() {
_innerNode?.reset();
super.resetSemantics();
}
void _annotate(SemanticsNode node) {
_annotatedNode = node;
List<SemanticsAction> actions = <SemanticsAction>[];
if (onTap != null)
actions.add(SemanticsAction.tap);
if (onLongPress != null)
actions.add(SemanticsAction.longPress);
if (onHorizontalDragUpdate != null) {
actions.add(SemanticsAction.scrollRight);
actions.add(SemanticsAction.scrollLeft);
}
if (onVerticalDragUpdate != null) {
actions.add(SemanticsAction.scrollUp);
actions.add(SemanticsAction.scrollDown);
}
// If a set of validActions has been provided only expose those.
if (validActions != null)
actions = actions.where((SemanticsAction action) => validActions.contains(action)).toList();
actions.forEach(node.addAction);
} }
@override void _performSemanticScrollLeft() {
void performAction(SemanticsAction action) {
switch (action) {
case SemanticsAction.tap:
if (onTap != null)
onTap();
break;
case SemanticsAction.longPress:
if (onLongPress != null)
onLongPress();
break;
case SemanticsAction.scrollLeft:
if (onHorizontalDragUpdate != null) { if (onHorizontalDragUpdate != null) {
final double primaryDelta = size.width * -scrollFactor; final double primaryDelta = size.width * -scrollFactor;
onHorizontalDragUpdate(new DragUpdateDetails( onHorizontalDragUpdate(new DragUpdateDetails(
...@@ -3087,8 +3076,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -3087,8 +3076,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
globalPosition: localToGlobal(size.center(Offset.zero)), globalPosition: localToGlobal(size.center(Offset.zero)),
)); ));
} }
break; }
case SemanticsAction.scrollRight:
void _performSemanticScrollRight() {
if (onHorizontalDragUpdate != null) { if (onHorizontalDragUpdate != null) {
final double primaryDelta = size.width * scrollFactor; final double primaryDelta = size.width * scrollFactor;
onHorizontalDragUpdate(new DragUpdateDetails( onHorizontalDragUpdate(new DragUpdateDetails(
...@@ -3096,8 +3086,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -3096,8 +3086,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
globalPosition: localToGlobal(size.center(Offset.zero)), globalPosition: localToGlobal(size.center(Offset.zero)),
)); ));
} }
break; }
case SemanticsAction.scrollUp:
void _performSemanticScrollUp() {
if (onVerticalDragUpdate != null) { if (onVerticalDragUpdate != null) {
final double primaryDelta = size.height * -scrollFactor; final double primaryDelta = size.height * -scrollFactor;
onVerticalDragUpdate(new DragUpdateDetails( onVerticalDragUpdate(new DragUpdateDetails(
...@@ -3105,8 +3096,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -3105,8 +3096,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
globalPosition: localToGlobal(size.center(Offset.zero)), globalPosition: localToGlobal(size.center(Offset.zero)),
)); ));
} }
break; }
case SemanticsAction.scrollDown:
void _performSemanticScrollDown() {
if (onVerticalDragUpdate != null) { if (onVerticalDragUpdate != null) {
final double primaryDelta = size.height * scrollFactor; final double primaryDelta = size.height * scrollFactor;
onVerticalDragUpdate(new DragUpdateDetails( onVerticalDragUpdate(new DragUpdateDetails(
...@@ -3114,12 +3106,6 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -3114,12 +3106,6 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
globalPosition: localToGlobal(size.center(Offset.zero)), globalPosition: localToGlobal(size.center(Offset.zero)),
)); ));
} }
break;
case SemanticsAction.increase:
case SemanticsAction.decrease:
assert(false);
break;
}
} }
@override @override
...@@ -3150,28 +3136,27 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3150,28 +3136,27 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
RenderSemanticsAnnotations({ RenderSemanticsAnnotations({
RenderBox child, RenderBox child,
bool container: false, bool container: false,
bool explicitChildNodes,
bool checked, bool checked,
bool selected, bool selected,
String label, String label,
TextDirection textDirection, TextDirection textDirection,
}) : assert(container != null), }) : assert(container != null),
_container = container, _container = container,
_explicitChildNodes = explicitChildNodes,
_checked = checked, _checked = checked,
_selected = selected, _selected = selected,
_label = label, _label = label,
_textDirection = textDirection, _textDirection = textDirection,
super(child); super(child);
/// If 'container' is true, this RenderObject will introduce a new /// If 'container' is true, this [RenderObject] will introduce a new
/// node in the semantics tree. Otherwise, the semantics will be /// node in the semantics tree. Otherwise, the semantics will be
/// merged with the semantics of any ancestors. /// merged with the semantics of any ancestors.
/// ///
/// The 'container' flag is implicitly set to true on the immediate /// Whether descendants of this [RenderObject] can add their semantic information
/// semantics-providing descendants of a node where multiple /// to the [SemanticsNode] introduced by this configuration is controlled by
/// children have semantics or have descendants providing semantics. /// [explicitChildNodes].
/// In other words, the semantics of siblings are not merged. To
/// merge the semantics of an entire subtree, including siblings,
/// you can use a [RenderMergeSemantics].
bool get container => _container; bool get container => _container;
bool _container; bool _container;
set container(bool value) { set container(bool value) {
...@@ -3182,6 +3167,28 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3182,6 +3167,28 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// Whether descendants of this [RenderObject] are allowed to add semantic
/// information to the [SemanticsNode] annotated by this widget.
///
/// When set to false descendants are allowed to annotate [SemanticNode]s of
/// their parent with the semantic information they want to contribute to the
/// semantic tree.
/// When set to true the only way for descendants to contribute semantic
/// information to the semantic tree is to introduce new explicit
/// [SemanticNode]s to the tree.
///
/// This setting is often used in combination with [isSemanticBoundary] to
/// create semantic boundaries that are either writable or not for children.
bool get explicitChildNodes => _explicitChildNodes;
bool _explicitChildNodes;
set explicitChildNodes(bool value) {
assert(value != null);
if (_explicitChildNodes == value)
return;
_explicitChildNodes = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.hasCheckedState] semantic to true and /// If non-null, sets the [SemanticsNode.hasCheckedState] semantic to true and
/// the [SemanticsNode.isChecked] semantic to the given value. /// the [SemanticsNode.isChecked] semantic to the given value.
bool get checked => _checked; bool get checked => _checked;
...@@ -3231,23 +3238,18 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3231,23 +3238,18 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
} }
@override @override
bool get isSemanticBoundary => container; void describeSemanticsConfiguration(SemanticsConfiguration config) {
config.isSemanticBoundary = container;
config.explicitChildNodes = explicitChildNodes;
@override if (checked != null)
SemanticsAnnotator get semanticsAnnotator => checked != null || selected != null || label != null || textDirection != null ? _annotate : null; config.isChecked = checked;
void _annotate(SemanticsNode node) {
if (checked != null) {
node
..hasCheckedState = true
..isChecked = checked;
}
if (selected != null) if (selected != null)
node.isSelected = selected; config.isSelected = selected;
if (label != null) if (label != null)
node.label = label; config.label = label;
if (textDirection != null) if (textDirection != null)
node.textDirection = textDirection; config.textDirection = textDirection;
} }
} }
...@@ -3262,7 +3264,10 @@ class RenderBlockSemantics extends RenderProxyBox { ...@@ -3262,7 +3264,10 @@ class RenderBlockSemantics extends RenderProxyBox {
RenderBlockSemantics({ RenderBox child }) : super(child); RenderBlockSemantics({ RenderBox child }) : super(child);
@override @override
bool get isBlockingSemanticsOfPreviouslyPaintedNodes => true; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isBlockingSemanticsOfPreviouslyPaintedNodes = true;
}
} }
/// Causes the semantics of all descendants to be merged into this /// Causes the semantics of all descendants to be merged into this
...@@ -3277,8 +3282,12 @@ class RenderMergeSemantics extends RenderProxyBox { ...@@ -3277,8 +3282,12 @@ class RenderMergeSemantics extends RenderProxyBox {
RenderMergeSemantics({ RenderBox child }) : super(child); RenderMergeSemantics({ RenderBox child }) : super(child);
@override @override
bool get isMergingSemanticsOfDescendants => true; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true;
}
} }
/// Excludes this subtree from the semantic tree. /// Excludes this subtree from the semantic tree.
......
...@@ -18,31 +18,6 @@ import 'semantics_event.dart'; ...@@ -18,31 +18,6 @@ import 'semantics_event.dart';
export 'dart:ui' show SemanticsAction; export 'dart:ui' show SemanticsAction;
export 'semantics_event.dart'; export 'semantics_event.dart';
/// Interface for [RenderObject]s to implement when they want to support
/// being tapped, etc.
///
/// The handler will only be called for a particular flag if that flag is set
/// (e.g. [performAction] will only be called with [SemanticsAction.tap] if
/// [SemanticsNode.addAction] was called for [SemanticsAction.tap].)
abstract class SemanticsActionHandler { // ignore: one_member_abstracts
/// Called when the object implementing this interface receives a
/// [SemanticsAction]. For example, if the user of an accessibility tool
/// instructs their device that they wish to tap a button, the [RenderObject]
/// behind that button would have its [performAction] method called with the
/// [SemanticsAction.tap] action.
void performAction(SemanticsAction action);
}
/// Signature for functions returned by [RenderObject.semanticsAnnotator].
///
/// These callbacks are called with the [SemanticsNode] object that
/// corresponds to the [RenderObject]. (One [SemanticsNode] can
/// correspond to multiple [RenderObject] objects.)
///
/// See [RenderObject.semanticsAnnotator] for details on the
/// contract that semantic annotators must follow.
typedef void SemanticsAnnotator(SemanticsNode semantics);
/// Signature for a function that is called for each [SemanticsNode]. /// Signature for a function that is called for each [SemanticsNode].
/// ///
/// Return false to stop visiting nodes. /// Return false to stop visiting nodes.
...@@ -103,14 +78,13 @@ class SemanticsData extends Diagnosticable { ...@@ -103,14 +78,13 @@ class SemanticsData extends Diagnosticable {
@required this.label, @required this.label,
@required this.textDirection, @required this.textDirection,
@required this.rect, @required this.rect,
@required this.tags, this.tags,
this.transform, this.transform,
}) : assert(flags != null), }) : assert(flags != null),
assert(actions != null), assert(actions != null),
assert(label != null), assert(label != null),
assert(label == '' || textDirection != null, 'A SemanticsData object with label "$label" had a null textDirection.'), assert(label == '' || textDirection != null, 'A SemanticsData object with label "$label" had a null textDirection.'),
assert(rect != null), assert(rect != null);
assert(tags != null);
/// A bit field of [SemanticsFlags] that apply to this node. /// A bit field of [SemanticsFlags] that apply to this node.
final int flags; final int flags;
...@@ -223,22 +197,18 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -223,22 +197,18 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// Each semantic node has a unique identifier that is assigned when the node /// Each semantic node has a unique identifier that is assigned when the node
/// is created. /// is created.
SemanticsNode({ SemanticsNode({
SemanticsActionHandler handler,
VoidCallback showOnScreen, VoidCallback showOnScreen,
}) : id = _generateNewId(), }) : id = _generateNewId(),
_showOnScreen = showOnScreen, _showOnScreen = showOnScreen;
_actionHandler = handler;
/// Creates a semantic node to represent the root of the semantics tree. /// Creates a semantic node to represent the root of the semantics tree.
/// ///
/// The root node is assigned an identifier of zero. /// The root node is assigned an identifier of zero.
SemanticsNode.root({ SemanticsNode.root({
SemanticsActionHandler handler,
VoidCallback showOnScreen, VoidCallback showOnScreen,
SemanticsOwner owner, SemanticsOwner owner,
}) : id = 0, }) : id = 0,
_showOnScreen = showOnScreen, _showOnScreen = showOnScreen {
_actionHandler = handler {
attach(owner); attach(owner);
} }
...@@ -254,11 +224,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -254,11 +224,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// they are created. /// they are created.
final int id; final int id;
final SemanticsActionHandler _actionHandler;
final VoidCallback _showOnScreen; final VoidCallback _showOnScreen;
// GEOMETRY // GEOMETRY
// These are automatically handled by RenderObject's own logic
/// The transform from this node's coordinate system to its parent's coordinate system. /// The transform from this node's coordinate system to its parent's coordinate system.
/// ///
...@@ -285,11 +253,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -285,11 +253,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
} }
} }
/// Whether [rect] was clipped by ancestors. /// Whether [rect] was affected by a clip from an ancestors.
/// ///
/// This is only true if the [rect] of this [SemanticsNode] has been altered /// If this is true it means that an ancestor imposed a clip on this
/// due to clipping by an ancestor. If ancestors have been clipped, but the /// [SemanticsNode]. However, it does not mean that the clip had any effect
/// [rect] of this node was unaffected it will be false. /// on the [rect] whatsoever.
bool wasAffectedByClip = false; bool wasAffectedByClip = false;
/// Whether the node is invisible. /// Whether the node is invisible.
...@@ -304,57 +272,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -304,57 +272,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// currently shown on screen. /// currently shown on screen.
bool get isInvisible => !isMergedIntoParent && rect.isEmpty; bool get isInvisible => !isMergedIntoParent && rect.isEmpty;
// FLAGS AND LABELS // MERGING
// These are supposed to be set by SemanticsAnnotator obtained from getSemanticsAnnotators
int _actions = 0;
/// Adds the given action to the set of semantic actions.
///
/// If the user chooses to perform an action,
/// [SemanticsActionHandler.performAction] will be called with the chosen
/// action.
void addAction(SemanticsAction action) {
assert(action != null);
final int index = action.index;
if ((_actions & index) == 0) {
_actions |= index;
_markDirty();
}
}
/// Adds the [SemanticsAction.scrollLeft] and [SemanticsAction.scrollRight] actions.
void addHorizontalScrollingActions() {
addAction(SemanticsAction.scrollLeft);
addAction(SemanticsAction.scrollRight);
}
/// Adds the [SemanticsAction.scrollUp] and [SemanticsAction.scrollDown] actions.
void addVerticalScrollingActions() {
addAction(SemanticsAction.scrollUp);
addAction(SemanticsAction.scrollDown);
}
/// Adds the [SemanticsAction.increase] and [SemanticsAction.decrease] actions.
void addAdjustmentActions() {
addAction(SemanticsAction.increase);
addAction(SemanticsAction.decrease);
}
bool _canPerformAction(SemanticsAction action) {
return _actionHandler != null && (_actions & action.index) != 0;
}
/// Whether this node and all of its descendants should be treated as one logical entity.
bool get mergeAllDescendantsIntoThisNode => _mergeAllDescendantsIntoThisNode;
bool _mergeAllDescendantsIntoThisNode = false;
set mergeAllDescendantsIntoThisNode(bool value) {
assert(value != null);
if (_mergeAllDescendantsIntoThisNode == value)
return;
_mergeAllDescendantsIntoThisNode = value;
_markDirty();
}
/// Whether this node merges its semantic information into an ancestor node. /// Whether this node merges its semantic information into an ancestor node.
bool get isMergedIntoParent => _isMergedIntoParent; bool get isMergedIntoParent => _isMergedIntoParent;
...@@ -377,164 +295,39 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -377,164 +295,39 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// * [mergeAllDescendantsIntoThisNode] /// * [mergeAllDescendantsIntoThisNode]
bool get isPartOfNodeMerging => mergeAllDescendantsIntoThisNode || isMergedIntoParent; bool get isPartOfNodeMerging => mergeAllDescendantsIntoThisNode || isMergedIntoParent;
int _flags = 0; /// Whether this node and all of its descendants should be treated as one logical entity.
void _setFlag(SemanticsFlags flag, bool value) { bool get mergeAllDescendantsIntoThisNode => _mergeAllDescendantsIntoThisNode;
final int index = flag.index; bool _mergeAllDescendantsIntoThisNode = _kEmptyConfig.isMergingSemanticsOfDescendants;
if (value) {
if ((_flags & index) == 0) {
_flags |= index;
_markDirty();
}
} else {
if ((_flags & index) != 0) {
_flags &= ~index;
_markDirty();
}
}
}
/// Whether this node has Boolean state that can be controlled by the user.
bool get hasCheckedState => (_flags & SemanticsFlags.hasCheckedState.index) != 0;
set hasCheckedState(bool value) => _setFlag(SemanticsFlags.hasCheckedState, value);
/// If this node has Boolean state that can be controlled by the user, whether
/// that state is on or off, corresponding to true and false, respectively.
bool get isChecked => (_flags & SemanticsFlags.isChecked.index) != 0;
set isChecked(bool value) => _setFlag(SemanticsFlags.isChecked, value);
/// Whether the current node is selected (true) or not (false).
bool get isSelected => (_flags & SemanticsFlags.isSelected.index) != 0;
set isSelected(bool value) => _setFlag(SemanticsFlags.isSelected, value);
/// A textual description of this node.
///
/// The text's reading direction is given by [textDirection].
String get label => _label;
String _label = '';
set label(String value) {
assert(value != null);
if (_label != value) {
_label = value;
_markDirty();
}
}
/// The reading direction for the text in [label].
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
assert(value != null);
if (_textDirection != value) {
_textDirection = value;
_markDirty();
}
}
final Set<SemanticsTag> _tags = new Set<SemanticsTag>();
/// Tags the [SemanticsNode] with [tag].
///
/// Tags are not sent to the engine. They can be used by a parent
/// [SemanticsNode] to figure out how to add the node as a child.
///
/// See also:
///
/// * [SemanticsTag], whose documentation discusses the purposes of tags.
/// * [hasTag] to check if the node has a certain tag.
void addTag(SemanticsTag tag) {
assert(tag != null);
_tags.add(tag);
}
/// Check if the [SemanticsNode] is tagged with [tag]. // CHILDREN
///
/// Tags can be added and removed with [ensureTag].
///
/// See also:
///
/// * [SemanticsTag], whose documentation discusses the purposes of tags.
bool hasTag(SemanticsTag tag) => _tags.contains(tag);
/// Restore this node to its default state.
void reset() {
_actions = 0;
_flags = 0;
_label = '';
_textDirection = null;
_mergeAllDescendantsIntoThisNode = false;
_tags.clear();
_markDirty();
}
List<SemanticsNode> _newChildren; /// Contains the children in inverse hit test order (i.e. paint order).
List<SemanticsNode> _children;
/// Append the given children as children of this node. void _replaceChildren(List<SemanticsNode> newChildren) {
/// assert(!newChildren.any((SemanticsNode child) => child == this));
/// Children must be added in inverse hit test order (i.e. paint order).
///
/// The [finalizeChildren] method must be called after all children have been
/// added.
void addChildren(Iterable<SemanticsNode> childrenInInverseHitTestOrder) {
_newChildren ??= <SemanticsNode>[];
_newChildren.addAll(childrenInInverseHitTestOrder);
// we do the asserts afterwards because children is an Iterable
// and doing the asserts before would mean the behavior is
// different in checked mode vs release mode (if you walk an
// iterator after having reached the end, it'll just start over;
// the values are not cached).
assert(!_newChildren.any((SemanticsNode child) => child == this));
assert(() { assert(() {
SemanticsNode ancestor = this; SemanticsNode ancestor = this;
while (ancestor.parent is SemanticsNode) while (ancestor.parent is SemanticsNode)
ancestor = ancestor.parent; ancestor = ancestor.parent;
assert(!_newChildren.any((SemanticsNode child) => child == ancestor)); assert(!newChildren.any((SemanticsNode child) => child == ancestor));
return true; return true;
}()); }());
assert(() { assert(() {
final Set<SemanticsNode> seenChildren = new Set<SemanticsNode>(); final Set<SemanticsNode> seenChildren = new Set<SemanticsNode>();
for (SemanticsNode child in _newChildren) for (SemanticsNode child in newChildren)
assert(seenChildren.add(child)); // check for duplicate adds assert(seenChildren.add(child)); // check for duplicate adds
return true; return true;
}()); }());
}
/// Contains the children in inverse hit test order (i.e. paint order).
List<SemanticsNode> _children;
/// Whether this node has a non-zero number of children.
bool get hasChildren => _children?.isNotEmpty ?? false;
bool _dead = false;
/// The number of children this node has.
int get childrenCount => hasChildren ? _children.length : 0;
/// Visits the immediate children of this node.
///
/// This function calls visitor for each child in a pre-order travseral
/// until visitor returns false. Returns true if all the visitor calls
/// returned true, otherwise returns false.
void visitChildren(SemanticsNodeVisitor visitor) {
if (_children != null) {
for (SemanticsNode child in _children) {
if (!visitor(child))
return;
}
}
}
/// Called during the compilation phase after all the children of this node have been compiled.
///
/// This function lets the semantic node respond to all the changes to its
/// child list for the given frame at once instead of needing to process the
/// changes incrementally as new children are compiled.
void finalizeChildren() {
// The goal of this function is updating sawChange. // The goal of this function is updating sawChange.
if (_children != null) { if (_children != null) {
for (SemanticsNode child in _children) for (SemanticsNode child in _children)
child._dead = true; child._dead = true;
} }
if (_newChildren != null) { if (newChildren != null) {
for (SemanticsNode child in _newChildren) { for (SemanticsNode child in newChildren) {
assert(!child.isInvisible, 'Child with id ${child.id} is invisible and should not be added to tree.'); assert(!child.isInvisible, 'Child with id ${child.id} is invisible and should not be added to tree.');
child._dead = false; child._dead = false;
} }
...@@ -552,8 +345,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -552,8 +345,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
} }
} }
} }
if (_newChildren != null) { if (newChildren != null) {
for (SemanticsNode child in _newChildren) { for (SemanticsNode child in newChildren) {
if (child.parent != this) { if (child.parent != this) {
if (child.parent != null) { if (child.parent != null) {
// we're rebuilding the tree from the bottom up, so it's possible // we're rebuilding the tree from the bottom up, so it's possible
...@@ -570,35 +363,42 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -570,35 +363,42 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
} }
} }
if (!sawChange && _children != null) { if (!sawChange && _children != null) {
assert(_newChildren != null); assert(newChildren != null);
assert(_newChildren.length == _children.length); assert(newChildren.length == _children.length);
// Did the order change? // Did the order change?
for (int i = 0; i < _children.length; i++) { for (int i = 0; i < _children.length; i++) {
if (_children[i].id != _newChildren[i].id) { if (_children[i].id != newChildren[i].id) {
sawChange = true; sawChange = true;
break; break;
} }
} }
} }
final List<SemanticsNode> oldChildren = _children; final List<SemanticsNode> oldChildren = _children;
_children = _newChildren; _children = newChildren;
oldChildren?.clear(); oldChildren?.clear();
_newChildren = oldChildren; newChildren = oldChildren;
if (sawChange) if (sawChange)
_markDirty(); _markDirty();
} }
@override /// Whether this node has a non-zero number of children.
SemanticsOwner get owner => super.owner; bool get hasChildren => _children?.isNotEmpty ?? false;
bool _dead = false;
@override /// The number of children this node has.
SemanticsNode get parent => super.parent; int get childrenCount => hasChildren ? _children.length : 0;
@override /// Visits the immediate children of this node.
void redepthChildren() { ///
/// This function calls visitor for each child in a pre-order travseral
/// until visitor returns false. Returns true if all the visitor calls
/// returned true, otherwise returns false.
void visitChildren(SemanticsNodeVisitor visitor) {
if (_children != null) { if (_children != null) {
for (SemanticsNode child in _children) for (SemanticsNode child in _children) {
redepthChild(child); if (!visitor(child))
return;
}
} }
} }
...@@ -617,6 +417,22 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -617,6 +417,22 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
return true; return true;
} }
// AbstractNode OVERRIDES
@override
SemanticsOwner get owner => super.owner;
@override
SemanticsNode get parent => super.parent;
@override
void redepthChildren() {
if (_children != null) {
for (SemanticsNode child in _children)
redepthChild(child);
}
}
@override @override
void attach(SemanticsOwner owner) { void attach(SemanticsOwner owner) {
super.attach(owner); super.attach(owner);
...@@ -627,7 +443,6 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -627,7 +443,6 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_dirty = false; _dirty = false;
_markDirty(); _markDirty();
} }
assert(isMergedIntoParent == (parent?.isPartOfNodeMerging ?? false));
if (_children != null) { if (_children != null) {
for (SemanticsNode child in _children) for (SemanticsNode child in _children)
child.attach(owner); child.attach(owner);
...@@ -656,6 +471,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -656,6 +471,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_markDirty(); _markDirty();
} }
// DIRTY MANAGEMENT
bool _dirty = false; bool _dirty = false;
void _markDirty() { void _markDirty() {
if (_dirty) if (_dirty)
...@@ -667,6 +484,66 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -667,6 +484,66 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
} }
} }
bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) {
return _label != config.label ||
_flags != config._flags ||
_textDirection != config.textDirection ||
_actionsAsBitMap(_actions) != _actionsAsBitMap(config._actions) ||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
}
// TAGS, LABELS, ACTIONS
Map<SemanticsAction, VoidCallback> _actions = _kEmptyConfig._actions;
/// The [SemanticsTag]s this node is tagged with.
///
/// Tags are used during the construction of the semantics tree. They are not
/// transfered to the engine.
Set<SemanticsTag> tags;
/// Whether this node is tagged with `tag`.
bool isTagged(SemanticsTag tag) => tags != null && tags.contains(tag);
int _flags = _kEmptyConfig._flags;
bool _hasFlag(SemanticsFlags flag) => _flags & flag.index != 0;
/// A textual description of this node.
///
/// The text's reading direction is given by [textDirection].
String get label => _label;
String _label = _kEmptyConfig.label;
/// The reading direction for [label].
TextDirection get textDirection => _textDirection;
TextDirection _textDirection = _kEmptyConfig.textDirection;
int _actionsAsBitMap(Map<SemanticsAction, VoidCallback> actions) {
return actions.keys.fold(0, (int prev, SemanticsAction action) => prev |= action.index);
}
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
static final SemanticsConfiguration _kEmptyConfig = new SemanticsConfiguration();
void updateWith({
@required SemanticsConfiguration config,
@required List<SemanticsNode> childrenInInversePaintOrder,
}) {
config ??= _kEmptyConfig;
if (_isDifferentFromCurrentSemanticAnnotation(config))
_markDirty();
_label = config.label;
_flags = config._flags;
_textDirection = config.textDirection;
_actions = new Map<SemanticsAction, VoidCallback>.from(config._actions);
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
}
/// Returns a summary of the semantics for this node. /// Returns a summary of the semantics for this node.
/// ///
/// If this node has [mergeAllDescendantsIntoThisNode], then the returned data /// If this node has [mergeAllDescendantsIntoThisNode], then the returned data
...@@ -674,21 +551,25 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -674,21 +551,25 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// returned data matches the data on this node. /// returned data matches the data on this node.
SemanticsData getSemanticsData() { SemanticsData getSemanticsData() {
int flags = _flags; int flags = _flags;
int actions = _actions; int actions = _actionsAsBitMap(_actions);
String label = _label; String label = _label;
TextDirection textDirection = _textDirection; TextDirection textDirection = _textDirection;
final Set<SemanticsTag> tags = new Set<SemanticsTag>.from(_tags); Set<SemanticsTag> mergedTags = tags == null ? null : new Set<SemanticsTag>.from(tags);
if (mergeAllDescendantsIntoThisNode) { if (mergeAllDescendantsIntoThisNode) {
_visitDescendants((SemanticsNode node) { _visitDescendants((SemanticsNode node) {
assert(node.isMergedIntoParent);
flags |= node._flags; flags |= node._flags;
actions |= node._actions; actions |= _actionsAsBitMap(node._actions);
textDirection ??= node._textDirection; textDirection ??= node._textDirection;
tags.addAll(node._tags); if (node.tags != null) {
if (node.label.isNotEmpty) { mergedTags ??= new Set<SemanticsTag>();
String nestedLabel = node.label; mergedTags.addAll(node.tags);
if (textDirection != node.textDirection && node.textDirection != null) { }
switch (node.textDirection) { if (node._label.isNotEmpty) {
String nestedLabel = node._label;
if (textDirection != node._textDirection && node._textDirection != null) {
switch (node._textDirection) {
case TextDirection.rtl: case TextDirection.rtl:
nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}'; nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}';
break; break;
...@@ -713,7 +594,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -713,7 +594,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
textDirection: textDirection, textDirection: textDirection,
rect: rect, rect: rect,
transform: transform, transform: transform,
tags: tags, tags: mergedTags,
); );
} }
...@@ -798,18 +679,13 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -798,18 +679,13 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
properties.add(new DiagnosticsProperty<Rect>('rect', rect, description: description, showName: false)); properties.add(new DiagnosticsProperty<Rect>('rect', rect, description: description, showName: false));
} }
properties.add(new FlagProperty('wasAffectedByClip', value: wasAffectedByClip, ifTrue: 'clipped')); properties.add(new FlagProperty('wasAffectedByClip', value: wasAffectedByClip, ifTrue: 'clipped'));
final List<String> actions = <String>[]; final List<String> actions = _actions.keys.map((SemanticsAction action) => describeEnum(action)).toList()..sort();
for (SemanticsAction action in SemanticsAction.values.values) {
if ((_actions & action.index) != 0)
actions.add(describeEnum(action));
}
properties.add(new IterableProperty<String>('actions', actions, ifEmpty: null)); properties.add(new IterableProperty<String>('actions', actions, ifEmpty: null));
properties.add(new IterableProperty<SemanticsTag>('tags', _tags, ifEmpty: null)); if (_hasFlag(SemanticsFlags.hasCheckedState))
if (hasCheckedState) properties.add(new FlagProperty('isChecked', value: _hasFlag(SemanticsFlags.isChecked), ifTrue: 'checked', ifFalse: 'unchecked'));
properties.add(new FlagProperty('isChecked', value: isChecked, ifTrue: 'checked', ifFalse: 'unchecked')); properties.add(new FlagProperty('isSelected', value: _hasFlag(SemanticsFlags.isSelected), ifTrue: 'selected'));
properties.add(new FlagProperty('isSelected', value: isSelected, ifTrue: 'selected')); properties.add(new StringProperty('label', _label, defaultValue: ''));
properties.add(new StringProperty('label', label, defaultValue: '')); properties.add(new EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
} }
/// Returns a string representation of this node and its descendants. /// Returns a string representation of this node and its descendants.
...@@ -939,7 +815,7 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -939,7 +815,7 @@ class SemanticsOwner extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) { VoidCallback _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
SemanticsNode result = _nodes[id]; SemanticsNode result = _nodes[id];
if (result != null && result.isPartOfNodeMerging && !result._canPerformAction(action)) { if (result != null && result.isPartOfNodeMerging && !result._canPerformAction(action)) {
result._visitDescendants((SemanticsNode node) { result._visitDescendants((SemanticsNode node) {
...@@ -952,7 +828,7 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -952,7 +828,7 @@ class SemanticsOwner extends ChangeNotifier {
} }
if (result == null || !result._canPerformAction(action)) if (result == null || !result._canPerformAction(action))
return null; return null;
return result._actionHandler; return result._actions[action];
} }
/// Asks the [SemanticsNode] with the given id to perform the given action. /// Asks the [SemanticsNode] with the given id to perform the given action.
...@@ -961,9 +837,9 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -961,9 +837,9 @@ class SemanticsOwner extends ChangeNotifier {
/// this function does nothing. /// this function does nothing.
void performAction(int id, SemanticsAction action) { void performAction(int id, SemanticsAction action) {
assert(action != null); assert(action != null);
final SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action); final VoidCallback handler = _getSemanticsActionHandlerForId(id, action);
if (handler != null) { if (handler != null) {
handler.performAction(action); handler();
return; return;
} }
...@@ -972,7 +848,7 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -972,7 +848,7 @@ class SemanticsOwner extends ChangeNotifier {
_nodes[id]._showOnScreen(); _nodes[id]._showOnScreen();
} }
SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) { VoidCallback _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) {
if (node.transform != null) { if (node.transform != null) {
final Matrix4 inverse = new Matrix4.identity(); final Matrix4 inverse = new Matrix4.identity();
if (inverse.copyInverse(node.transform) == 0.0) if (inverse.copyInverse(node.transform) == 0.0)
...@@ -990,16 +866,16 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -990,16 +866,16 @@ class SemanticsOwner extends ChangeNotifier {
} }
return true; return true;
}); });
return result?._actionHandler; return result?._actions[action];
} }
if (node.hasChildren) { if (node.hasChildren) {
for (SemanticsNode child in node._children.reversed) { for (SemanticsNode child in node._children.reversed) {
final SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(child, position, action); final VoidCallback handler = _getSemanticsActionHandlerForPosition(child, position, action);
if (handler != null) if (handler != null)
return handler; return handler;
} }
} }
return node._canPerformAction(action) ? node._actionHandler : null; return node._canPerformAction(action) ? node._actions[action] : null;
} }
/// Asks the [SemanticsNode] at the given position to perform the given action. /// Asks the [SemanticsNode] at the given position to perform the given action.
...@@ -1011,10 +887,256 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -1011,10 +887,256 @@ class SemanticsOwner extends ChangeNotifier {
final SemanticsNode node = rootSemanticsNode; final SemanticsNode node = rootSemanticsNode;
if (node == null) if (node == null)
return; return;
final SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action); final VoidCallback handler = _getSemanticsActionHandlerForPosition(node, position, action);
handler?.performAction(action); if (handler != null)
handler();
} }
@override @override
String toString() => describeIdentity(this); String toString() => describeIdentity(this);
} }
/// Describes the semantic information associated with the owning
/// [RenderObject].
///
/// The information provided in the configuration is used to to generate the
/// semantics tree.
class SemanticsConfiguration {
// SEMANTIC BOUNDARY BEHAVIOR
/// Whether the [RenderObject] owner of this configuration wants to own its
/// own [SemanticsNode].
///
/// When set to true semantic information associated with the [RenderObject]
/// owner of this configuration or any of its defendants will not leak into
/// parents. The [SemanticsNode] generated out of this configuration will
/// act as a boundary.
///
/// Whether descendants of the owning [RenderObject] can add their semantic
/// information to the [SemanticsNode] introduced by this configuration
/// is controlled by [explicitChildNodes].
///
/// This has to be true if [isMergingDescendantsIntoOneNode] is also true.
bool get isSemanticBoundary => _isSemanticBoundary;
bool _isSemanticBoundary = false;
set isSemanticBoundary(bool value) {
assert(!isMergingDescendantsIntoOneNode || value);
_isSemanticBoundary = value;
}
/// Whether the configuration forces all children of the owning [RenderObject]
/// that want to contribute semantic information to the semantics tree to do
/// so in the form of explicit [SemanticsNode]s.
///
/// When set to false children of the owning [RenderObject] are allowed to
/// annotate [SemanticNode]s of their parent with the semantic information
/// they want to contribute to the semantic tree.
/// When set to true the only way for children of the owning [RenderObject]
/// to contribute semantic information to the semantic tree is to introduce
/// new explicit [SemanticNode]s to the tree.
///
/// This setting is often used in combination with [isSemanticBoundary] to
/// create semantic boundaries that are either writable or not for children.
bool explicitChildNodes = false;
/// Whether the owning [RenderObject] makes other [RenderObjects] previously
/// painted within the same semantic boundary unreachable for accessibility
/// purposes.
///
/// If set to true, the semantic information for all siblings and cousins of
/// this node, that are earlier in a depth-first pre-order traversal, are
/// dropped from the semantics tree up until a semantic boundary (as defined
/// by [isSemanticBoundary]) is reached.
///
/// If [isSemanticBoundary] and [isBlockingSemanticsOfPreviouslyPaintedNodes]
/// is set on the same node, all previously painted siblings and cousins up
/// until the next ancestor that is a semantic boundary are dropped.
///
/// Paint order as established by [visitChildrenForSemantics] is used to
/// determine if a node is previous to this one.
bool isBlockingSemanticsOfPreviouslyPaintedNodes = false;
/// Whether the semantics information of all descendants should be merged
/// into the owning [RenderObject] semantics node.
///
/// When this is set to true the [SemanticsNode] of the owning [RenderObject]
/// will not have any children.
///
/// Setting this to true requires that [isSemanticBoundary] is also true.
bool get isMergingDescendantsIntoOneNode => _isMergingDescendantsIntoOneNode;
bool _isMergingDescendantsIntoOneNode = false;
set isMergingDescendantsIntoOneNode(bool value) {
assert(isSemanticBoundary);
_isMergingDescendantsIntoOneNode = isMergingDescendantsIntoOneNode;
}
// SEMANTIC ANNOTATIONS
// These will end up on [SemanticNode]s generated from
// [SemanticsConfiguration]s.
/// Whether this configuration is empty.
///
/// An empty configuration doesn't contain any semantic information that it
/// wants to contribute to the semantics tree.
bool get hasBeenAnnotated => _hasBeenAnnotated;
bool _hasBeenAnnotated = false;
/// The actions (with associated action handlers) that this configuration
/// would like to contribute to the semantics tree.
///
/// See also:
/// * [addAction] to add an action.
final Map<SemanticsAction, VoidCallback> _actions = <SemanticsAction, VoidCallback>{};
/// Adds an `action` to the semantics tree.
///
/// Whenever the user performs `action` the provided `handler` is called.
void addAction(SemanticsAction action, VoidCallback handler) {
_actions[action] = handler;
_hasBeenAnnotated = true;
}
/// Returns the action handler registered for [action] or null if none was
/// registered.
///
/// See also:
/// * [addAction] to add an action.
VoidCallback getActionHandler(SemanticsAction action) => _actions[action];
/// Whether the semantic information provided by the owning [RenderObject] and
/// all of its descendants should be treated as one logical entity.
///
/// If set to true, the descendants of the owning [RenderObject]'s
/// [SemanticsNode] will merge their semantic information into the
/// [SemanticsNode] representing the owning [RenderObject].
bool get isMergingSemanticsOfDescendants => _isMergingSemanticsOfDescendants;
bool _isMergingSemanticsOfDescendants = false;
set isMergingSemanticsOfDescendants(bool value) {
_isMergingSemanticsOfDescendants = value;
_hasBeenAnnotated = true;
}
/// A textual description of the owning [RenderObject].
///
/// The text's reading direction is given by [textDirection].
String get label => _label;
String _label = '';
set label(String label) {
_label = label;
_hasBeenAnnotated = true;
}
/// The reading direction for the text in [label].
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection textDirection) {
_textDirection = textDirection;
_hasBeenAnnotated = true;
}
/// Whether the owning [RenderObject] is selected (true) or not (false).
set isSelected(bool value) {
_setFlag(SemanticsFlags.isSelected, value);
}
/// If this node has Boolean state that can be controlled by the user, whether
/// that state is on or off, corresponding to true and false, respectively.
///
/// Do not set this to any value if the owning [RenderObject] doesn't have
/// Booleans state that can be controlled by the user.
set isChecked(bool value) {
_setFlag(SemanticsFlags.hasCheckedState, true);
_setFlag(SemanticsFlags.isChecked, value);
}
// TAGS
Iterable<SemanticsTag> get tagsForChildren => _tagsForChildren;
Set<SemanticsTag> _tagsForChildren;
void addTagForChildren(SemanticsTag tag) {
_tagsForChildren ??= new Set<SemanticsTag>();
_tagsForChildren.add(tag);
}
// INTERNAL FLAG MANAGEMENT
int _flags = 0;
void _setFlag(SemanticsFlags flag, bool value) {
if (value) {
_flags |= flag.index;
} else {
_flags &= ~flag.index;
}
_hasBeenAnnotated = true;
}
// CONFIGURATION COMBINATION LOGIC
/// Whether this configuration is compatible with the provided `other`
/// configuration.
///
/// Two configurations are said to be compatible if they can be added to the
/// same [SemanticsNode] without losing any semantics information.
bool isCompatibleWith(SemanticsConfiguration other) {
if (other == null || !other.hasBeenAnnotated || !hasBeenAnnotated)
return true;
if (_actions.keys.toSet().intersection(other._actions.keys.toSet()).isNotEmpty)
return false;
if ((_flags & other._flags) != 0)
return false;
return true;
}
/// Absorb the semantic information from `other` into this configuration.
///
/// This adds the semantic information of both configurations and saves the
/// result in this configuration.
///
/// Only configurations that have [explicitChildNodes] set to false can
/// absorb other configurations and its recommended to only absorb compatible
/// configurations as determined by [isCompatibleWith].
void absorb(SemanticsConfiguration other) {
assert(!explicitChildNodes);
if (!other.hasBeenAnnotated)
return;
_actions.addAll(other._actions);
_flags |= other._flags;
textDirection ??= other.textDirection;
if (other.label.isNotEmpty) {
String nestedLabel = other.label;
if (textDirection != other.textDirection && other.textDirection != null) {
switch (other.textDirection) {
case TextDirection.rtl:
nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}';
break;
case TextDirection.ltr:
nestedLabel = '${Unicode.LRE}$nestedLabel${Unicode.PDF}';
break;
}
}
if (label.isEmpty)
label = nestedLabel;
else
label = '$label\n$nestedLabel';
}
_hasBeenAnnotated = _hasBeenAnnotated || other._hasBeenAnnotated;
}
/// Returns an exact copy of this configuration.
SemanticsConfiguration copy() {
return new SemanticsConfiguration()
..isSemanticBoundary = isSemanticBoundary
..explicitChildNodes = explicitChildNodes
.._hasBeenAnnotated = _hasBeenAnnotated
.._textDirection = _textDirection
.._label = _label
.._flags = _flags
.._actions.addAll(_actions);
}
}
...@@ -221,10 +221,11 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje ...@@ -221,10 +221,11 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
} }
@override @override
SemanticsAnnotator get semanticsAnnotator => _excludeFromSemanticsScrolling ? _annotate : null; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
void _annotate(SemanticsNode node) { if (_excludeFromSemanticsScrolling)
node.addTag(RenderSemanticsGestureHandler.excludeFromScrolling); config.addTagForChildren(RenderSemanticsGestureHandler.excludeFromScrolling);
} }
@override @override
......
...@@ -91,11 +91,12 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -91,11 +91,12 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
_crossAxisDirection = crossAxisDirection, _crossAxisDirection = crossAxisDirection,
_offset = offset; _offset = offset;
@override @override
SemanticsAnnotator get semanticsAnnotator => _annotate; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
void _annotate(SemanticsNode node) { config.addTagForChildren(RenderSemanticsGestureHandler.useTwoPaneSemantics);
node.addTag(RenderSemanticsGestureHandler.useTwoPaneSemantics);
} }
@override @override
......
...@@ -4434,6 +4434,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4434,6 +4434,7 @@ class Semantics extends SingleChildRenderObjectWidget {
Key key, Key key,
Widget child, Widget child,
this.container: false, this.container: false,
this.explicitChildNodes: false,
this.checked, this.checked,
this.selected, this.selected,
this.label, this.label,
...@@ -4441,18 +4442,29 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4441,18 +4442,29 @@ class Semantics extends SingleChildRenderObjectWidget {
}) : assert(container != null), }) : assert(container != null),
super(key: key, child: child); super(key: key, child: child);
/// If 'container' is true, this Widget will introduce a new node in /// If 'container' is true, this widget will introduce a new
/// the semantics tree. Otherwise, the semantics will be merged with /// node in the semantics tree. Otherwise, the semantics will be
/// the semantics of any ancestors. /// merged with the semantics of any ancestors (if the ancestor allows that).
/// ///
/// The 'container' flag is implicitly set to true on the immediate /// Whether descendants of this widget can add their semantic information to the
/// semantics-providing descendants of a node where multiple /// [SemanticsNode] introduced by this configuration is controlled by
/// children have semantics or have descendants providing semantics. /// [explicitChildNodes].
/// In other words, the semantics of siblings are not merged. To
/// merge the semantics of an entire subtree, including siblings,
/// you can use a [MergeSemantics] widget.
final bool container; final bool container;
/// Whether descendants of this widget are allowed to add semantic information
/// to the [SemanticsNode] annotated by this widget.
///
/// When set to false descendants are allowed to annotate [SemanticNode]s of
/// their parent with the semantic information they want to contribute to the
/// semantic tree.
/// When set to true the only way for descendants to contribute semantic
/// information to the semantic tree is to introduce new explicit
/// [SemanticNode]s to the tree.
///
/// This setting is often used in combination with [isSemanticBoundary] to
/// create semantic boundaries that are either writable or not for children.
final bool explicitChildNodes;
/// If non-null, indicates that this subtree represents a checkbox /// If non-null, indicates that this subtree represents a checkbox
/// or similar widget with a "checked" state, and what its current /// or similar widget with a "checked" state, and what its current
/// state is. /// state is.
...@@ -4484,6 +4496,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4484,6 +4496,7 @@ class Semantics extends SingleChildRenderObjectWidget {
RenderSemanticsAnnotations createRenderObject(BuildContext context) { RenderSemanticsAnnotations createRenderObject(BuildContext context) {
return new RenderSemanticsAnnotations( return new RenderSemanticsAnnotations(
container: container, container: container,
explicitChildNodes: explicitChildNodes,
checked: checked, checked: checked,
selected: selected, selected: selected,
label: label, label: label,
...@@ -4495,6 +4508,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4495,6 +4508,7 @@ class Semantics extends SingleChildRenderObjectWidget {
void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) { void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) {
renderObject renderObject
..container = container ..container = container
..explicitChildNodes = explicitChildNodes
..checked = checked ..checked = checked
..selected = selected ..selected = selected
..label = label ..label = label
......
...@@ -104,7 +104,7 @@ class _FocusScopeState extends State<FocusScope> { ...@@ -104,7 +104,7 @@ class _FocusScopeState extends State<FocusScope> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
FocusScope.of(context).reparentScopeIfNeeded(widget.node); FocusScope.of(context).reparentScopeIfNeeded(widget.node);
return new Semantics( return new Semantics(
container: true, explicitChildNodes: true,
child: new _FocusScopeMarker( child: new _FocusScopeMarker(
node: widget.node, node: widget.node,
child: widget.child, child: widget.child,
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
void main() {
testWidgets('Card can take semantic text from multiple children', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
child: new Card(
child: new Column(
children: <Widget>[
const Text('I am text!'),
const Text('Moar text!!1'),
new MaterialButton(
child: const Text('Button'),
onPressed: () { },
)
],
)
),
),
),
),
);
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 2,
label: 'I am text!\nMoar text!!1',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
new TestSemantics(
id: 1,
label: 'Button',
textDirection: TextDirection.ltr,
actions: SemanticsAction.tap.index,
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
));
semantics.dispose();
});
}
...@@ -95,7 +95,7 @@ void main() { ...@@ -95,7 +95,7 @@ void main() {
expect(semantics, hasSemantics(new TestSemantics.root( expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 7,
rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0), rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
transform: null, transform: null,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
...@@ -103,7 +103,7 @@ void main() { ...@@ -103,7 +103,7 @@ void main() {
label: 'aaa\nAAA', label: 'aaa\nAAA',
), ),
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 6, id: 8,
rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0), rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
transform: new Matrix4.translationValues(0.0, 56.0, 0.0), transform: new Matrix4.translationValues(0.0, 56.0, 0.0),
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
...@@ -111,7 +111,7 @@ void main() { ...@@ -111,7 +111,7 @@ void main() {
label: 'bbb\nBBB', label: 'bbb\nBBB',
), ),
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 11, id: 9,
rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0), rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
transform: new Matrix4.translationValues(0.0, 112.0, 0.0), transform: new Matrix4.translationValues(0.0, 112.0, 0.0),
flags: SemanticsFlags.hasCheckedState.index, flags: SemanticsFlags.hasCheckedState.index,
......
...@@ -1011,11 +1011,11 @@ void main() { ...@@ -1011,11 +1011,11 @@ void main() {
final TestSemantics expectedSemantics = new TestSemantics.root( final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 3,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 2, id: 1,
actions: SemanticsAction.tap.index, actions: SemanticsAction.tap.index,
flags: SemanticsFlags.isSelected.index, flags: SemanticsFlags.isSelected.index,
label: 'TAB #0\nTab 1 of 2', label: 'TAB #0\nTab 1 of 2',
...@@ -1023,13 +1023,14 @@ void main() { ...@@ -1023,13 +1023,14 @@ void main() {
transform: new Matrix4.translationValues(0.0, 276.0, 0.0), transform: new Matrix4.translationValues(0.0, 276.0, 0.0),
), ),
new TestSemantics( new TestSemantics(
id: 5, id: 2,
actions: SemanticsAction.tap.index, actions: SemanticsAction.tap.index,
label: 'TAB #1\nTab 2 of 2', label: 'TAB #1\nTab 2 of 2',
rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight), rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight),
transform: new Matrix4.translationValues(108.0, 276.0, 0.0), transform: new Matrix4.translationValues(108.0, 276.0, 0.0),
), ),
]), ],
),
], ],
); );
...@@ -1064,15 +1065,19 @@ void main() { ...@@ -1064,15 +1065,19 @@ void main() {
), ),
); );
const String tab0title = 'This is a very wide tab #0\nTab 1 of 20';
const String tab10title = 'This is a very wide tab #10\nTab 11 of 20';
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft])); expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
expect(semantics, isNot(includesNodeWith(label: 'This is a very wide tab #10'))); expect(semantics, includesNodeWith(label: tab0title));
expect(semantics, isNot(includesNodeWith(label: tab10title)));
controller.index = 10; controller.index = 10;
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(semantics, isNot(includesNodeWith(label: 'This is a very wide tab #0'))); expect(semantics, isNot(includesNodeWith(label: tab0title)));
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight])); expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight]));
expect(semantics, includesNodeWith(label: 'This is a very wide tab #10')); expect(semantics, includesNodeWith(label: tab10title));
controller.index = 19; controller.index = 19;
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -1083,7 +1088,8 @@ void main() { ...@@ -1083,7 +1088,8 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft])); expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
expect(semantics, includesNodeWith(label: 'This is a very wide tab #0')); expect(semantics, includesNodeWith(label: tab0title));
expect(semantics, isNot(includesNodeWith(label: tab10title)));
semantics.dispose(); semantics.dispose();
}); });
......
...@@ -473,7 +473,7 @@ void main() { ...@@ -473,7 +473,7 @@ void main() {
child: new Tooltip( child: new Tooltip(
key: key, key: key,
message: tooltipText, message: tooltipText,
child: new Container(width: 0.0, height: 0.0), child: new Container(width: 10.0, height: 10.0),
), ),
), ),
], ],
...@@ -485,14 +485,23 @@ void main() { ...@@ -485,14 +485,23 @@ void main() {
), ),
); );
expect(semantics, hasSemantics(new TestSemantics.root(label: tooltipText))); final TestSemantics expected = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: tooltipText,
),
]
);
expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true));
// before using "as dynamic" in your code, see note top of file // before using "as dynamic" in your code, see note top of file
(key.currentState as dynamic).ensureTooltipVisible(); // this triggers a rebuild of the semantics because the tree changes (key.currentState as dynamic).ensureTooltipVisible(); // this triggers a rebuild of the semantics because the tree changes
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
expect(semantics, hasSemantics(new TestSemantics.root(label: tooltipText))); expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true));
semantics.dispose(); semantics.dispose();
}); });
......
...@@ -55,6 +55,9 @@ class TestRenderObject extends RenderObject { ...@@ -55,6 +55,9 @@ class TestRenderObject extends RenderObject {
Rect get semanticBounds => new Rect.fromLTWH(0.0, 0.0, 10.0, 20.0); Rect get semanticBounds => new Rect.fromLTWH(0.0, 0.0, 10.0, 20.0);
@override @override
bool get isSemanticBoundary => true; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = true;
}
} }
...@@ -72,23 +72,23 @@ void main() { ...@@ -72,23 +72,23 @@ void main() {
}); });
test('RenderSemanticsGestureHandler adds/removes correct semantic actions', () { test('RenderSemanticsGestureHandler adds/removes correct semantic actions', () {
SemanticsNode node = new SemanticsNode();
final RenderSemanticsGestureHandler renderObj = new RenderSemanticsGestureHandler( final RenderSemanticsGestureHandler renderObj = new RenderSemanticsGestureHandler(
onTap: () {}, onTap: () {},
onHorizontalDragUpdate: (DragUpdateDetails details) {}, onHorizontalDragUpdate: (DragUpdateDetails details) {},
); );
renderObj.semanticsAnnotator(node); SemanticsConfiguration config = new SemanticsConfiguration();
expect(node.getSemanticsData().hasAction(SemanticsAction.tap), isTrue); renderObj.describeSemanticsConfiguration(config);
expect(node.getSemanticsData().hasAction(SemanticsAction.scrollLeft), isTrue); expect(config.getActionHandler(SemanticsAction.tap), isNotNull);
expect(node.getSemanticsData().hasAction(SemanticsAction.scrollRight), isTrue); expect(config.getActionHandler(SemanticsAction.scrollLeft), isNotNull);
expect(config.getActionHandler(SemanticsAction.scrollRight), isNotNull);
node = new SemanticsNode(); config = new SemanticsConfiguration();
renderObj.validActions = <SemanticsAction>[SemanticsAction.tap, SemanticsAction.scrollLeft].toSet(); renderObj.validActions = <SemanticsAction>[SemanticsAction.tap, SemanticsAction.scrollLeft].toSet();
renderObj.semanticsAnnotator(node); renderObj.describeSemanticsConfiguration(config);
expect(node.getSemanticsData().hasAction(SemanticsAction.tap), isTrue); expect(config.getActionHandler(SemanticsAction.tap), isNotNull);
expect(node.getSemanticsData().hasAction(SemanticsAction.scrollLeft), isTrue); expect(config.getActionHandler(SemanticsAction.scrollLeft), isNotNull);
expect(node.getSemanticsData().hasAction(SemanticsAction.scrollRight), isFalse); expect(config.getActionHandler(SemanticsAction.scrollRight), isNull);
}); });
} }
...@@ -29,6 +29,7 @@ class TestTree { ...@@ -29,6 +29,7 @@ class TestTree {
child: new RenderPositionedBox( child: new RenderPositionedBox(
child: child = new RenderConstrainedBox( child: child = new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0), additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0),
child: new RenderSemanticsAnnotations(label: 'Hello there foo', textDirection: TextDirection.ltr)
), ),
), ),
), ),
...@@ -127,7 +128,7 @@ void main() { ...@@ -127,7 +128,7 @@ void main() {
layout(testTree.root, phase: EnginePhase.paint); layout(testTree.root, phase: EnginePhase.paint);
expect(testTree.painted, isTrue); expect(testTree.painted, isTrue);
}); });
test('objects can be detached and re-attached: semantics', () { test('objects can be detached and re-attached: semantics (no change)', () {
final TestTree testTree = new TestTree(); final TestTree testTree = new TestTree();
int semanticsUpdateCount = 0; int semanticsUpdateCount = 0;
final SemanticsHandle semanticsHandle = renderer.pipelineOwner.ensureSemantics( final SemanticsHandle semanticsHandle = renderer.pipelineOwner.ensureSemantics(
...@@ -147,7 +148,31 @@ void main() { ...@@ -147,7 +148,31 @@ void main() {
expect(semanticsUpdateCount, 0); expect(semanticsUpdateCount, 0);
// Lay out, composite, paint, and update semantics again // Lay out, composite, paint, and update semantics again
layout(testTree.root, phase: EnginePhase.flushSemantics); layout(testTree.root, phase: EnginePhase.flushSemantics);
expect(semanticsUpdateCount, 0); // no semantics have changed.
semanticsHandle.dispose();
});
test('objects can be detached and re-attached: semantics (with change)', () {
final TestTree testTree = new TestTree();
int semanticsUpdateCount = 0;
final SemanticsHandle semanticsHandle = renderer.pipelineOwner.ensureSemantics(
listener: () {
++semanticsUpdateCount;
}
);
// Lay out, composite, paint, and update semantics
layout(testTree.root, phase: EnginePhase.flushSemantics);
expect(semanticsUpdateCount, 1); expect(semanticsUpdateCount, 1);
// Remove testTree from the custom render view
renderer.renderView.child = null;
expect(testTree.child.owner, isNull);
// Dirty one of the elements
semanticsUpdateCount = 0;
testTree.child.additionalConstraints = const BoxConstraints.tightFor(height: 20.0, width: 30.0);
testTree.child.markNeedsSemanticsUpdate();
expect(semanticsUpdateCount, 0);
// Lay out, composite, paint, and update semantics again
layout(testTree.root, phase: EnginePhase.flushSemantics);
expect(semanticsUpdateCount, 1); // semantics have changed.
semanticsHandle.dispose(); semanticsHandle.dispose();
}); });
} }
...@@ -18,41 +18,45 @@ void main() { ...@@ -18,41 +18,45 @@ void main() {
test('tagging', () { test('tagging', () {
final SemanticsNode node = new SemanticsNode(); final SemanticsNode node = new SemanticsNode();
expect(node.hasTag(tag1), isFalse); expect(node.isTagged(tag1), isFalse);
expect(node.hasTag(tag2), isFalse); expect(node.isTagged(tag2), isFalse);
node.addTag(tag1); node.tags = new Set<SemanticsTag>()..add(tag1);
expect(node.hasTag(tag1), isTrue); expect(node.isTagged(tag1), isTrue);
expect(node.hasTag(tag2), isFalse); expect(node.isTagged(tag2), isFalse);
node.addTag(tag2); node.tags.add(tag2);
expect(node.hasTag(tag1), isTrue); expect(node.isTagged(tag1), isTrue);
expect(node.hasTag(tag2), isTrue); expect(node.isTagged(tag2), isTrue);
}); });
test('getSemanticsData includes tags', () { test('getSemanticsData includes tags', () {
final Set<SemanticsTag> tags = new Set<SemanticsTag>()
..add(tag1)
..add(tag2);
final SemanticsNode node = new SemanticsNode() final SemanticsNode node = new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0) ..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0)
..addTag(tag1) ..tags = tags;
..addTag(tag2);
final Set<SemanticsTag> expected = new Set<SemanticsTag>() expect(node.getSemanticsData().tags, tags);
..add(tag1)
..add(tag2); tags.add(tag3);
expect(node.getSemanticsData().tags, expected); final SemanticsConfiguration config = new SemanticsConfiguration()
..isMergingSemanticsOfDescendants = true;
node.mergeAllDescendantsIntoThisNode = true; node.updateWith(
node.addChildren(<SemanticsNode>[ config: config,
childrenInInversePaintOrder: <SemanticsNode>[
new SemanticsNode() new SemanticsNode()
..isMergedIntoParent = true
..rect = new Rect.fromLTRB(5.0, 5.0, 10.0, 10.0) ..rect = new Rect.fromLTRB(5.0, 5.0, 10.0, 10.0)
..addTag(tag3), ..tags = tags,
]); ],
node.finalizeChildren(); );
expected.add(tag3);
expect(node.getSemanticsData().tags, expected); expect(node.getSemanticsData().tags, tags);
}); });
test('after markNeedsSemanticsUpdate(onlyLocalUpdates: true) all render objects between two semantic boundaries are asked for annotations', () { test('after markNeedsSemanticsUpdate(onlyLocalUpdates: true) all render objects between two semantic boundaries are asked for annotations', () {
...@@ -88,7 +92,6 @@ void main() { ...@@ -88,7 +92,6 @@ void main() {
middle.action = SemanticsAction.scrollDown; middle.action = SemanticsAction.scrollDown;
middle.markNeedsSemanticsUpdate(onlyLocalUpdates: true); middle.markNeedsSemanticsUpdate(onlyLocalUpdates: true);
expect(root.debugSemantics.getSemanticsData().actions, 0); // SemanticsNode is reset
pumpFrame(phase: EnginePhase.flushSemantics); pumpFrame(phase: EnginePhase.flushSemantics);
...@@ -104,8 +107,10 @@ void main() { ...@@ -104,8 +107,10 @@ void main() {
..rect = new Rect.fromLTRB(5.0, 0.0, 10.0, 5.0); ..rect = new Rect.fromLTRB(5.0, 0.0, 10.0, 5.0);
final SemanticsNode root = new SemanticsNode() final SemanticsNode root = new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 5.0); ..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
root.addChildren(<SemanticsNode>[child1, child2]); root.updateWith(
root.finalizeChildren(); config: null,
childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
);
expect(root.transform, isNull); expect(root.transform, isNull);
expect(child1.transform, isNull); expect(child1.transform, isNull);
...@@ -126,8 +131,10 @@ void main() { ...@@ -126,8 +131,10 @@ void main() {
..rect = new Rect.fromLTRB(10.0, 0.0, 15.0, 5.0); ..rect = new Rect.fromLTRB(10.0, 0.0, 15.0, 5.0);
final SemanticsNode root = new SemanticsNode() final SemanticsNode root = new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 20.0, 5.0); ..rect = new Rect.fromLTRB(0.0, 0.0, 20.0, 5.0);
root.addChildren(<SemanticsNode>[child1, child2]); root.updateWith(
root.finalizeChildren(); config: null,
childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
);
expect( expect(
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal), root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal),
'SemanticsNode#11(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 20.0, 5.0))\n' 'SemanticsNode#11(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 20.0, 5.0))\n'
...@@ -144,18 +151,22 @@ void main() { ...@@ -144,18 +151,22 @@ void main() {
final SemanticsNode child3 = new SemanticsNode() final SemanticsNode child3 = new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 5.0); ..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
child3.addChildren(<SemanticsNode>[ child3.updateWith(
config: null,
childrenInInversePaintOrder: <SemanticsNode>[
new SemanticsNode() new SemanticsNode()
..rect = new Rect.fromLTRB(5.0, 0.0, 10.0, 5.0), ..rect = new Rect.fromLTRB(5.0, 0.0, 10.0, 5.0),
new SemanticsNode() new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 5.0, 5.0), ..rect = new Rect.fromLTRB(0.0, 0.0, 5.0, 5.0),
]); ],
child3.finalizeChildren(); );
final SemanticsNode rootComplex = new SemanticsNode() final SemanticsNode rootComplex = new SemanticsNode()
..rect = new Rect.fromLTRB(0.0, 0.0, 25.0, 5.0); ..rect = new Rect.fromLTRB(0.0, 0.0, 25.0, 5.0);
rootComplex.addChildren(<SemanticsNode>[child1, child2, child3]); rootComplex.updateWith(
rootComplex.finalizeChildren(); config: null,
childrenInInversePaintOrder: <SemanticsNode>[child1, child2, child3]
);
expect( expect(
rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal), rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal),
...@@ -187,28 +198,30 @@ void main() { ...@@ -187,28 +198,30 @@ void main() {
expect( expect(
minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden), minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), wasAffectedByClip: false, actions: [], tags: [], isSelected: false, label: "", textDirection: null)\n', 'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), wasAffectedByClip: false, actions: [], isSelected: false, label: "", textDirection: null)\n',
); );
final SemanticsNode allProperties = new SemanticsNode() final SemanticsConfiguration config = new SemanticsConfiguration()
..rect = new Rect.fromLTWH(50.0, 10.0, 20.0, 30.0) ..isMergingSemanticsOfDescendants = true
..mergeAllDescendantsIntoThisNode = true ..addAction(SemanticsAction.scrollUp, () { })
..transform = new Matrix4.translation(new Vector3(10.0, 10.0, 0.0)) ..addAction(SemanticsAction.longPress, () { })
..wasAffectedByClip = true ..addAction(SemanticsAction.showOnScreen, () { })
..addAction(SemanticsAction.scrollUp)
..addAction(SemanticsAction.longPress)
..addAction(SemanticsAction.showOnScreen)
..isChecked = false ..isChecked = false
..isSelected = true ..isSelected = true
..label = "Use all the properties" ..label = "Use all the properties"
..textDirection = TextDirection.rtl; ..textDirection = TextDirection.rtl;
final SemanticsNode allProperties = new SemanticsNode()
..rect = new Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
..transform = new Matrix4.translation(new Vector3(10.0, 10.0, 0.0))
..wasAffectedByClip = true
..updateWith(config: config, childrenInInversePaintOrder: null);
expect( expect(
allProperties.toStringDeep(), allProperties.toStringDeep(),
'SemanticsNode#17(STALE, owner: null, leaf merge, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), clipped, actions: [longPress, scrollUp, showOnScreen], selected, label: "Use all the properties", textDirection: rtl)\n', 'SemanticsNode#17(STALE, owner: null, leaf merge, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), clipped, actions: [longPress, scrollUp, showOnScreen], unchecked, selected, label: "Use all the properties", textDirection: rtl)\n',
); );
expect( expect(
allProperties.getSemanticsData().toString(), allProperties.getSemanticsData().toString(),
'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [1.0,0.0,0.0,10.0; 0.0,1.0,0.0,10.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0], actions: [longPress, scrollUp, showOnScreen], flags: [isSelected], label: "Use all the properties", textDirection: rtl)', 'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [1.0,0.0,0.0,10.0; 0.0,1.0,0.0,10.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0], actions: [longPress, scrollUp, showOnScreen], flags: [hasCheckedState, isSelected], label: "Use all the properties", textDirection: rtl)',
); );
final SemanticsNode scaled = new SemanticsNode() final SemanticsNode scaled = new SemanticsNode()
...@@ -223,37 +236,22 @@ void main() { ...@@ -223,37 +236,22 @@ void main() {
'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [10.0,0.0,0.0,0.0; 0.0,10.0,0.0,0.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0])', 'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [10.0,0.0,0.0,0.0; 0.0,10.0,0.0,0.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0])',
); );
}); });
test('reset clears tags', () {
const SemanticsTag tag = const SemanticsTag('tag for testing');
final SemanticsNode node = new SemanticsNode();
expect(node.hasTag(tag), isFalse);
node.addTag(tag);
expect(node.hasTag(tag), isTrue);
node.reset();
expect(node.hasTag(tag), isFalse);
});
} }
class TestRender extends RenderProxyBox { class TestRender extends RenderProxyBox {
TestRender({ this.action, this.isSemanticBoundary, RenderObject child }) : super(child); TestRender({ this.action, this.isSemanticBoundary, RenderObject child }) : super(child);
@override
final bool isSemanticBoundary; final bool isSemanticBoundary;
SemanticsAction action; SemanticsAction action;
@override @override
SemanticsAnnotator get semanticsAnnotator => _annotate; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
void _annotate(SemanticsNode node) { config
node.addAction(action); ..isSemanticBoundary = isSemanticBoundary
..addAction(action, () { });
} }
} }
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:test/test.dart';
import 'rendering_tester.dart';
void main() {
test('only send semantics update if semantics have changed', () {
final TestRender testRender = new TestRender()
..label = 'hello'
..textDirection = TextDirection.ltr;
final RenderObject tree = new RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0),
child: testRender,
);
int semanticsUpdateCount = 0;
final SemanticsHandle semanticsHandle = renderer.pipelineOwner.ensureSemantics(
listener: () {
++semanticsUpdateCount;
}
);
layout(tree, phase: EnginePhase.flushSemantics);
// Initial render does semantics.
expect(semanticsUpdateCount, 1);
expect(testRender.describeSemanticsConfigurationCallCount, isNot(0));
testRender.describeSemanticsConfigurationCallCount = 0;
semanticsUpdateCount = 0;
// Request semantics update even though nothing changed.
testRender.markNeedsSemanticsUpdate();
pumpFrame(phase: EnginePhase.flushSemantics);
// Object is asked for semantics, but no update is sent.
expect(semanticsUpdateCount, 0);
expect(testRender.describeSemanticsConfigurationCallCount, 1);
testRender.describeSemanticsConfigurationCallCount = 0;
semanticsUpdateCount = 0;
// Change semantics and request update.
testRender.label = 'bye';
testRender.markNeedsSemanticsUpdate();
pumpFrame(phase: EnginePhase.flushSemantics);
// Object is asked for semantics, and update is sent.
expect(semanticsUpdateCount, 1);
expect(testRender.describeSemanticsConfigurationCallCount, 1);
semanticsHandle.dispose();
});
}
class TestRender extends RenderSemanticsAnnotations {
int describeSemanticsConfigurationCallCount = 0;
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
describeSemanticsConfigurationCallCount += 1;
}
}
...@@ -141,7 +141,7 @@ void main() { ...@@ -141,7 +141,7 @@ void main() {
), ),
); );
expect(semantics, hasSemantics(new TestSemantics.root(label: 'a label'))); expect(semantics, includesNodeWith(label: 'a label'));
}); });
testWidgets('Null icon with semantic label', (WidgetTester tester) async { testWidgets('Null icon with semantic label', (WidgetTester tester) async {
...@@ -159,7 +159,7 @@ void main() { ...@@ -159,7 +159,7 @@ void main() {
), ),
); );
expect(semantics, hasSemantics(new TestSemantics.root(label: 'a label'))); expect(semantics, includesNodeWith(label: 'a label'));
}); });
testWidgets('Changing semantic label from null doesn\'t rebuild tree ', (WidgetTester tester) async { testWidgets('Changing semantic label from null doesn\'t rebuild tree ', (WidgetTester tester) async {
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
testWidgets('Implicit Semantics merge behavior', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
explicitChildNodes: false,
child: new Column(
children: <Widget>[
const Text('Michael Goderbauer'),
const Text('goderbauer@google.com'),
],
),
),
),
);
// SemanticsNode#0()
// └SemanticsNode#1(label: "Michael Goderbauer\ngoderbauer@google.com", textDirection: ltr)
expect(
semantics,
hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: 'Michael Goderbauer\ngoderbauer@google.com',
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
explicitChildNodes: true,
child: new Column(
children: <Widget>[
const Text('Michael Goderbauer'),
const Text('goderbauer@google.com'),
],
),
),
),
);
// SemanticsNode#0()
// └SemanticsNode#1()
// ├SemanticsNode#2(label: "Michael Goderbauer", textDirection: ltr)
// └SemanticsNode#3(label: "goderbauer@google.com", textDirection: ltr)
expect(
semantics,
hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
children: <TestSemantics>[
new TestSemantics(
id: 2,
label: 'Michael Goderbauer',
),
new TestSemantics(
id: 3,
label: 'goderbauer@google.com',
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
explicitChildNodes: true,
child: new Semantics(
label: 'Signed in as',
child: new Column(
children: <Widget>[
const Text('Michael Goderbauer'),
const Text('goderbauer@google.com'),
],
),
),
),
),
);
// SemanticsNode#0()
// └SemanticsNode#1()
// └SemanticsNode#4(label: "Signed in as\nMichael Goderbauer\ngoderbauer@google.com", textDirection: ltr)
expect(
semantics,
hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
children: <TestSemantics>[
new TestSemantics(
id: 4,
label: 'Signed in as\nMichael Goderbauer\ngoderbauer@google.com',
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
explicitChildNodes: false,
child: new Semantics(
label: 'Signed in as',
child: new Column(
children: <Widget>[
const Text('Michael Goderbauer'),
const Text('goderbauer@google.com'),
],
),
),
),
),
);
// SemanticsNode#0()
// └SemanticsNode#1(label: "Signed in as\nMichael Goderbauer\ngoderbauer@google.com", textDirection: ltr)
expect(
semantics,
hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: 'Signed in as\nMichael Goderbauer\ngoderbauer@google.com',
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
testWidgets('Do not merge with conflicts', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
explicitChildNodes: false,
child: new Column(
children: <Widget>[
new Semantics(
label: 'node 1',
selected: true,
child: new Container(
width: 10.0,
height: 10.0,
),
),
new Semantics(
label: 'node 2',
selected: true,
child: new Container(
width: 10.0,
height: 10.0,
),
),
new Semantics(
label: 'node 3',
selected: true,
child: new Container(
width: 10.0,
height: 10.0,
),
),
],
),
),
),
);
// SemanticsNode#0()
// └SemanticsNode#8()
// ├SemanticsNode#5(selected, label: "node 1", textDirection: ltr)
// ├SemanticsNode#6(selected, label: "node 2", textDirection: ltr)
// └SemanticsNode#7(selected, label: "node 3", textDirection: ltr)
expect(
semantics,
hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 8,
children: <TestSemantics>[
new TestSemantics(
id: 5,
flags: SemanticsFlags.isSelected.index,
label: 'node 1',
),
new TestSemantics(
id: 6,
flags: SemanticsFlags.isSelected.index,
label: 'node 2',
),
new TestSemantics(
id: 7,
flags: SemanticsFlags.isSelected.index,
label: 'node 3',
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
}
...@@ -104,11 +104,11 @@ void main() { ...@@ -104,11 +104,11 @@ void main() {
final TestSemantics expectedSemantics = new TestSemantics.root( final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 2,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 2, id: 1,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
actions: SemanticsAction.tap.index, actions: SemanticsAction.tap.index,
), ),
......
...@@ -94,12 +94,19 @@ class TestWidget extends SingleChildRenderObjectWidget { ...@@ -94,12 +94,19 @@ class TestWidget extends SingleChildRenderObjectWidget {
} }
class RenderTest extends RenderProxyBox { class RenderTest extends RenderProxyBox {
@override @override
SemanticsAnnotator get semanticsAnnotator => isSemanticBoundary ? _annotate : null; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
if (!_isSemanticBoundary)
return;
config
..isSemanticBoundary = _isSemanticBoundary
..label = _label
..textDirection = TextDirection.ltr;
void _annotate(SemanticsNode node) {
node.label = _label;
node.textDirection = TextDirection.ltr;
} }
String _label; String _label;
...@@ -111,8 +118,7 @@ class RenderTest extends RenderProxyBox { ...@@ -111,8 +118,7 @@ class RenderTest extends RenderProxyBox {
callLog.add('markNeedsSemanticsUpdate(onlyChanges: true)'); callLog.add('markNeedsSemanticsUpdate(onlyChanges: true)');
} }
@override
bool get isSemanticBoundary => _isSemanticBoundary;
bool _isSemanticBoundary; bool _isSemanticBoundary;
set isSemanticBoundary(bool value) { set isSemanticBoundary(bool value) {
if (_isSemanticBoundary == value) if (_isSemanticBoundary == value)
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
testWidgets('markNeedsSemanticsUpdate allways resets node', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(const TestWidget());
final RenderTest renderObj = tester.renderObject(find.byType(TestWidget));
expect(renderObj.labelWasReset, hasLength(1));
expect(renderObj.labelWasReset.last, true);
expect(semantics, includesNodeWith(label: 'Label 1'));
renderObj.markNeedsSemanticsUpdate(onlyLocalUpdates: false, noGeometry: false);
await tester.pumpAndSettle();
expect(renderObj.labelWasReset, hasLength(2));
expect(renderObj.labelWasReset.last, true);
expect(semantics, includesNodeWith(label: 'Label 2'));
renderObj.markNeedsSemanticsUpdate(onlyLocalUpdates: true, noGeometry: false);
await tester.pumpAndSettle();
expect(renderObj.labelWasReset, hasLength(3));
expect(renderObj.labelWasReset.last, true);
expect(semantics, includesNodeWith(label: 'Label 3'));
renderObj.markNeedsSemanticsUpdate(onlyLocalUpdates: true, noGeometry: true);
await tester.pumpAndSettle();
expect(renderObj.labelWasReset, hasLength(4));
expect(renderObj.labelWasReset.last, true);
expect(semantics, includesNodeWith(label: 'Label 4'));
renderObj.markNeedsSemanticsUpdate(onlyLocalUpdates: false, noGeometry: true);
await tester.pumpAndSettle();
expect(renderObj.labelWasReset, hasLength(5));
expect(renderObj.labelWasReset.last, true);
expect(semantics, includesNodeWith(label: 'Label 5'));
semantics.dispose();
});
}
class TestWidget extends SingleChildRenderObjectWidget {
const TestWidget({
Key key,
Widget child,
}) : super(key: key, child: child);
@override
RenderTest createRenderObject(BuildContext context) {
return new RenderTest();
}
}
class RenderTest extends RenderProxyBox {
List<bool> labelWasReset = <bool>[];
@override
SemanticsAnnotator get semanticsAnnotator => _annotate;
void _annotate(SemanticsNode node) {
labelWasReset.add(node.label == '');
node.label = 'Label ${labelWasReset.length}';
node.textDirection = TextDirection.ltr;
}
}
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// 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:ui' show SemanticsFlags;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -15,136 +17,221 @@ void main() { ...@@ -15,136 +17,221 @@ void main() {
// smoketest // smoketest
await tester.pumpWidget( await tester.pumpWidget(
new Container( new Semantics(
container: true,
child: new Container(
child: new Semantics( child: new Semantics(
label: 'test1', label: 'test1',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Container() child: new Container(),
) selected: true,
) ),
),
),
); );
expect(semantics, hasSemantics(new TestSemantics.root(label: 'test1'))); expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: 'test1',
rect: TestSemantics.fullScreen,
flags: SemanticsFlags.isSelected.index,
)
]
)));
// control for forking // control for forking
await tester.pumpWidget( await tester.pumpWidget(
new Column( new Semantics(
container: true,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Container( new Container(
height: 10.0, height: 10.0,
child: const Semantics(label: 'child1', textDirection: TextDirection.ltr), child: const Semantics(
label: 'child1',
textDirection: TextDirection.ltr,
selected: true,
),
), ),
new Container( new Container(
height: 10.0, height: 10.0,
child: const IgnorePointer( child: const IgnorePointer(
ignoring: true, ignoring: true,
child: const Semantics(label: 'child2', textDirection: TextDirection.ltr), child: const Semantics(
) label: 'child1',
textDirection: TextDirection.ltr,
selected: true,
),
),
), ),
], ],
) ),
),
); );
expect(semantics, hasSemantics(new TestSemantics.root(label: 'child1'))); expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: 'child1',
rect: TestSemantics.fullScreen,
flags: SemanticsFlags.isSelected.index,
)
],
)));
// forking semantics // forking semantics
await tester.pumpWidget( await tester.pumpWidget(
new Column( new Semantics(
container: true,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Container( new Container(
height: 10.0, height: 10.0,
child: const Semantics(label: 'child1', textDirection: TextDirection.ltr), child: const Semantics(
label: 'child1',
textDirection: TextDirection.ltr,
selected: true,
),
), ),
new Container( new Container(
height: 10.0, height: 10.0,
child: const IgnorePointer( child: const IgnorePointer(
ignoring: false, ignoring: false,
child: const Semantics(label: 'child2', textDirection: TextDirection.ltr), child: const Semantics(
) label: 'child2',
textDirection: TextDirection.ltr,
selected: true,
),
),
), ),
], ],
) ),
),
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(new TestSemantics.root(
new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
new TestSemantics(
id: 2,
label: 'child1', label: 'child1',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
flags: SemanticsFlags.isSelected.index,
), ),
new TestSemantics.rootChild( new TestSemantics(
id: 2, id: 3,
label: 'child2', label: 'child2',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
transform: new Matrix4.translationValues(0.0, 10.0, 0.0), flags: SemanticsFlags.isSelected.index,
), ),
], ],
) ),
)); ],
), ignoreTransform: true));
// toggle a branch off // toggle a branch off
await tester.pumpWidget( await tester.pumpWidget(
new Column( new Semantics(
container: true,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Container( new Container(
height: 10.0, height: 10.0,
child: const Semantics(label: 'child1', textDirection: TextDirection.ltr) child: const Semantics(
label: 'child1',
textDirection: TextDirection.ltr,
selected: true,
),
), ),
new Container( new Container(
height: 10.0, height: 10.0,
child: const IgnorePointer( child: const IgnorePointer(
ignoring: true, ignoring: true,
child: const Semantics(label: 'child2', textDirection: TextDirection.ltr) child: const Semantics(
) label: 'child2',
textDirection: TextDirection.ltr,
selected: true,
),
),
), ),
], ],
) ),
),
); );
expect(semantics, hasSemantics(new TestSemantics.root(label: 'child1'))); expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: 'child1',
rect: TestSemantics.fullScreen,
flags: SemanticsFlags.isSelected.index,
)
],
)));
// toggle a branch back on // toggle a branch back on
await tester.pumpWidget( await tester.pumpWidget(
new Column( new Semantics(
container: true,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Container( new Container(
height: 10.0, height: 10.0,
child: const Semantics(label: 'child1', textDirection: TextDirection.ltr) child: const Semantics(
label: 'child1',
textDirection: TextDirection.ltr,
selected: true,
),
), ),
new Container( new Container(
height: 10.0, height: 10.0,
child: const IgnorePointer( child: const IgnorePointer(
ignoring: false, ignoring: false,
child: const Semantics(label: 'child2', textDirection: TextDirection.ltr) child: const Semantics(
) label: 'child2',
textDirection: TextDirection.ltr,
selected: true,
),
),
), ),
], ],
crossAxisAlignment: CrossAxisAlignment.stretch ),
) ),
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(new TestSemantics.root(
new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 3, id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
new TestSemantics(
id: 4,
label: 'child1', label: 'child1',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
flags: SemanticsFlags.isSelected.index,
), ),
new TestSemantics.rootChild( new TestSemantics(
id: 2, id: 3,
label: 'child2', label: 'child2',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
transform: new Matrix4.translationValues(0.0, 10.0, 0.0), flags: SemanticsFlags.isSelected.index,
), ),
], ],
) ),
)); ],
), ignoreTransform: true));
semantics.dispose(); semantics.dispose();
}); });
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// 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:ui' show SemanticsFlags;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -19,105 +21,153 @@ void main() { ...@@ -19,105 +21,153 @@ void main() {
// forking semantics // forking semantics
await tester.pumpWidget( await tester.pumpWidget(
new Column( new Semantics(
container: true,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Container( new Container(
height: 10.0, height: 10.0,
child: const Semantics(label: 'child1', textDirection: TextDirection.ltr) child: const Semantics(
label: 'child1',
textDirection: TextDirection.ltr,
selected: true,
),
), ),
new Container( new Container(
height: 10.0, height: 10.0,
child: const IgnorePointer( child: const IgnorePointer(
ignoring: false, ignoring: false,
child: const Semantics(label: 'child2', textDirection: TextDirection.ltr) child: const Semantics(
) label: 'child2',
textDirection: TextDirection.ltr,
selected: true,
),
),
), ),
], ],
crossAxisAlignment: CrossAxisAlignment.stretch ),
) ),
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(new TestSemantics.root(
new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 3,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
new TestSemantics(
id: 1, id: 1,
label: 'child1', label: 'child1',
textDirection: TextDirection.ltr,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
flags: SemanticsFlags.isSelected.index,
), ),
new TestSemantics.rootChild( new TestSemantics(
id: 2, id: 2,
label: 'child2', label: 'child2',
textDirection: TextDirection.ltr,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
transform: new Matrix4.translationValues(0.0, 10.0, 0.0), flags: SemanticsFlags.isSelected.index,
), ),
], ],
) ),
)); ],
), ignoreTransform: true));
// toggle a branch off // toggle a branch off
await tester.pumpWidget( await tester.pumpWidget(
new Column( new Semantics(
container: true,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Container( new Container(
height: 10.0, height: 10.0,
child: const Semantics(label: 'child1', textDirection: TextDirection.ltr) child: const Semantics(
label: 'child1',
textDirection: TextDirection.ltr,
selected: true,
),
), ),
new Container( new Container(
height: 10.0, height: 10.0,
child: const IgnorePointer( child: const IgnorePointer(
ignoring: true, ignoring: true,
child: const Semantics(label: 'child2', textDirection: TextDirection.ltr) child: const Semantics(
) label: 'child2',
textDirection: TextDirection.ltr,
selected: true,
),
),
), ),
], ],
crossAxisAlignment: CrossAxisAlignment.stretch ),
) ),
); );
expect(semantics, hasSemantics(new TestSemantics.root(label: 'child1'))); expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 3,
label: 'child1',
rect: TestSemantics.fullScreen,
flags: SemanticsFlags.isSelected.index,
)
],
)));
// toggle a branch back on // toggle a branch back on
await tester.pumpWidget( await tester.pumpWidget(
new Column( new Semantics(
container: true,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Container( new Container(
height: 10.0, height: 10.0,
child: const Semantics(label: 'child1', textDirection: TextDirection.ltr) child: const Semantics(
label: 'child1',
textDirection: TextDirection.ltr,
selected: true,
),
), ),
new Container( new Container(
height: 10.0, height: 10.0,
child: const IgnorePointer( child: const IgnorePointer(
ignoring: false, ignoring: false,
child: const Semantics(label: 'child2', textDirection: TextDirection.ltr) child: const Semantics(
) label: 'child2',
textDirection: TextDirection.ltr,
selected: true,
),
),
), ),
], ],
crossAxisAlignment: CrossAxisAlignment.stretch ),
) ),
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(new TestSemantics.root(
new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 3, id: 3,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
new TestSemantics(
id: 4,
label: 'child1', label: 'child1',
textDirection: TextDirection.ltr,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
flags: SemanticsFlags.isSelected.index,
), ),
new TestSemantics.rootChild( new TestSemantics(
id: 2, id: 2,
label: 'child2', label: 'child2',
textDirection: TextDirection.ltr,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
transform: new Matrix4.translationValues(0.0, 10.0, 0.0), flags: SemanticsFlags.isSelected.index,
), ),
], ],
) ),
)); ],
), ignoreTransform: true));
semantics.dispose(); semantics.dispose();
}); });
......
...@@ -16,81 +16,112 @@ void main() { ...@@ -16,81 +16,112 @@ void main() {
// implicit annotators // implicit annotators
await tester.pumpWidget( await tester.pumpWidget(
new Container( new Semantics(
container: true,
child: new Container(
child: new Semantics( child: new Semantics(
label: 'test', label: 'test',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Container( child: new Container(
child: const Semantics( child: const Semantics(
checked: true checked: true
) ),
) ),
) ),
) ),
),
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: 'test', label: 'test',
rect: TestSemantics.fullScreen,
)
]
) )
)); ));
// remove one // remove one
await tester.pumpWidget( await tester.pumpWidget(
new Container( new Semantics(
container: true,
child: new Container( child: new Container(
child: const Semantics( child: const Semantics(
checked: true checked: true,
) ),
) ),
) ),
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
rect: TestSemantics.fullScreen,
),
]
) )
)); ));
// change what it says // change what it says
await tester.pumpWidget( await tester.pumpWidget(
new Container( new Semantics(
container: true,
child: new Container( child: new Container(
child: const Semantics( child: const Semantics(
label: 'test', label: 'test',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
) ),
) ),
) ),
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: 'test', label: 'test',
textDirection: TextDirection.ltr,
rect: TestSemantics.fullScreen,
),
]
) )
)); ));
// add a node // add a node
await tester.pumpWidget( await tester.pumpWidget(
new Container( new Semantics(
child: new Semantics( container: true,
checked: true,
child: new Container( child: new Container(
child: const Semantics(
checked: true,
child: const Semantics( child: const Semantics(
label: 'test', label: 'test',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
) ),
) ),
) ),
) ),
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: 'test', label: 'test',
rect: TestSemantics.fullScreen,
) )
],
),
)); ));
int changeCount = 0; int changeCount = 0;
...@@ -100,17 +131,18 @@ void main() { ...@@ -100,17 +131,18 @@ void main() {
// make no changes // make no changes
await tester.pumpWidget( await tester.pumpWidget(
new Container( new Semantics(
child: new Semantics( container: true,
checked: true,
child: new Container( child: new Container(
child: const Semantics(
checked: true,
child: const Semantics( child: const Semantics(
label: 'test', label: 'test',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
) ),
) ),
) ),
) ),
); );
expect(changeCount, 0); expect(changeCount, 0);
......
...@@ -6,6 +6,7 @@ import 'dart:ui' show SemanticsFlags; ...@@ -6,6 +6,7 @@ import 'dart:ui' show SemanticsFlags;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
...@@ -26,10 +27,12 @@ void main() { ...@@ -26,10 +27,12 @@ void main() {
fit: StackFit.expand, fit: StackFit.expand,
children: <Widget>[ children: <Widget>[
const Semantics( const Semantics(
container: true,
label: 'L1', label: 'L1',
), ),
new Semantics( new Semantics(
label: 'L2', label: 'L2',
container: true,
child: new Stack( child: new Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: <Widget>[ children: <Widget>[
...@@ -50,22 +53,22 @@ void main() { ...@@ -50,22 +53,22 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 3,
label: 'L1', label: 'L1',
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
), ),
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 2, id: 4,
label: 'L2', label: 'L2',
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 3, id: 1,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
), ),
new TestSemantics( new TestSemantics(
id: 4, id: 2,
flags: SemanticsFlags.hasCheckedState.index, flags: SemanticsFlags.hasCheckedState.index,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
), ),
...@@ -87,9 +90,11 @@ void main() { ...@@ -87,9 +90,11 @@ void main() {
children: <Widget>[ children: <Widget>[
const Semantics( const Semantics(
label: 'L1', label: 'L1',
container: true,
), ),
new Semantics( new Semantics(
label: 'L2', label: 'L2',
container: true,
child: new Stack( child: new Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: <Widget>[ children: <Widget>[
...@@ -108,12 +113,12 @@ void main() { ...@@ -108,12 +113,12 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 3,
label: 'L1', label: 'L1',
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
), ),
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 2, id: 4,
label: 'L2', label: 'L2',
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
...@@ -134,6 +139,7 @@ void main() { ...@@ -134,6 +139,7 @@ void main() {
const Semantics(), const Semantics(),
new Semantics( new Semantics(
label: 'L2', label: 'L2',
container: true,
child: new Stack( child: new Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: <Widget>[ children: <Widget>[
...@@ -150,8 +156,14 @@ void main() { ...@@ -150,8 +156,14 @@ void main() {
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 4,
label: 'L2', label: 'L2',
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
rect: TestSemantics.fullScreen,
),
],
) )
)); ));
......
...@@ -55,7 +55,7 @@ void main() { ...@@ -55,7 +55,7 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 3,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: label, label: label,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
...@@ -111,7 +111,7 @@ void main() { ...@@ -111,7 +111,7 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 3,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: label, label: label,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
......
...@@ -39,9 +39,15 @@ void main() { ...@@ -39,9 +39,15 @@ void main() {
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 3,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: 'label', label: 'label',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
rect: TestSemantics.fullScreen,
)
]
) )
)); ));
...@@ -71,10 +77,16 @@ void main() { ...@@ -71,10 +77,16 @@ void main() {
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 3,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: 'label', label: 'label',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
rect: TestSemantics.fullScreen,
) )
],
),
)); ));
semantics.dispose(); semantics.dispose();
......
...@@ -61,6 +61,7 @@ void main() { ...@@ -61,6 +61,7 @@ void main() {
new Semantics( new Semantics(
label: '#2', label: '#2',
container: true, container: true,
explicitChildNodes: true,
child: new Stack( child: new Stack(
children: <Widget>[ children: <Widget>[
new Semantics( new Semantics(
...@@ -146,9 +147,12 @@ class RenderBoundaryBlockSemantics extends RenderProxyBox { ...@@ -146,9 +147,12 @@ class RenderBoundaryBlockSemantics extends RenderProxyBox {
RenderBoundaryBlockSemantics({ RenderBox child }) : super(child); RenderBoundaryBlockSemantics({ RenderBox child }) : super(child);
@override @override
bool get isBlockingSemanticsOfPreviouslyPaintedNodes => true; void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
@override config
bool get isSemanticBoundary => true; ..isBlockingSemanticsOfPreviouslyPaintedNodes = true
..isSemanticBoundary = true;
}
} }
...@@ -62,15 +62,9 @@ void main() { ...@@ -62,15 +62,9 @@ void main() {
final SemanticsNode node1 = tester.renderObject(find.byWidget(const Text('1'))).debugSemantics; final SemanticsNode node1 = tester.renderObject(find.byWidget(const Text('1'))).debugSemantics;
final SemanticsNode node2 = tester.renderObject(find.byWidget(const Text('2'))).debugSemantics; final SemanticsNode node2 = tester.renderObject(find.byWidget(const Text('2'))).debugSemantics;
final SemanticsNode node3 = tester.renderObject(find.byWidget(const Text('3'))).debugSemantics;
expect(node1.wasAffectedByClip, false); expect(node1.wasAffectedByClip, false);
expect(node2.wasAffectedByClip, true); expect(node2.wasAffectedByClip, true);
expect(node3.wasAffectedByClip, true);
expect(node1.isInvisible, isFalse);
expect(node2.isInvisible, isFalse);
expect(node3.isInvisible, isTrue);
semantics.dispose(); semantics.dispose();
}); });
...@@ -117,12 +111,12 @@ void main() { ...@@ -117,12 +111,12 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 4, id: 3,
label: '1', label: '1',
rect: new Rect.fromLTRB(0.0, 0.0, 75.0, 14.0), rect: new Rect.fromLTRB(0.0, 0.0, 75.0, 14.0),
), ),
new TestSemantics( new TestSemantics(
id: 5, id: 4,
label: '2\n3', label: '2\n3',
rect: new Rect.fromLTRB(0.0, 0.0, 25.0, 14.0), // clipped form original 75.0 to 25.0 rect: new Rect.fromLTRB(0.0, 0.0, 25.0, 14.0), // clipped form original 75.0 to 25.0
), ),
......
...@@ -53,7 +53,14 @@ void main() { ...@@ -53,7 +53,14 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root(label: 'test1\ntest2'), new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 3,
label: 'test1\ntest2',
),
]
),
ignoreRect: true, ignoreRect: true,
ignoreTransform: true, ignoreTransform: true,
)); ));
...@@ -74,8 +81,8 @@ void main() { ...@@ -74,8 +81,8 @@ void main() {
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild(id: 5, label: 'test1'), new TestSemantics.rootChild(id: 4, label: 'test1'),
new TestSemantics.rootChild(id: 6, label: 'test2'), new TestSemantics.rootChild(id: 5, label: 'test2'),
], ],
), ),
ignoreRect: true, ignoreRect: true,
......
...@@ -13,8 +13,12 @@ void main() { ...@@ -13,8 +13,12 @@ void main() {
SemanticsTester semantics = new SemanticsTester(tester); SemanticsTester semantics = new SemanticsTester(tester);
final TestSemantics expectedSemantics = new TestSemantics.root( final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
label: 'test1', label: 'test1',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
)
],
); );
await tester.pumpWidget( await tester.pumpWidget(
...@@ -27,7 +31,12 @@ void main() { ...@@ -27,7 +31,12 @@ void main() {
) )
); );
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(
expectedSemantics,
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
));
semantics.dispose(); semantics.dispose();
semantics = null; semantics = null;
...@@ -37,7 +46,12 @@ void main() { ...@@ -37,7 +46,12 @@ void main() {
expect(tester.binding.hasScheduledFrame, isTrue); expect(tester.binding.hasScheduledFrame, isTrue);
await tester.pump(); await tester.pump();
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(
expectedSemantics,
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
));
semantics.dispose(); semantics.dispose();
}); });
...@@ -62,15 +76,20 @@ void main() { ...@@ -62,15 +76,20 @@ void main() {
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
label: 'test1',
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, label: 'test1',
children: <TestSemantics>[
new TestSemantics(
label: 'test2a', label: 'test2a',
rect: TestSemantics.fullScreen,
) )
] ]
) )
]
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
)); ));
await tester.pumpWidget(new Directionality( await tester.pumpWidget(new Directionality(
...@@ -94,22 +113,25 @@ void main() { ...@@ -94,22 +113,25 @@ void main() {
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
label: 'test1',
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 2, label: 'test1',
children: <TestSemantics>[
new TestSemantics(
label: 'middle', label: 'middle',
rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 1,
label: 'test2b', label: 'test2b',
rect: TestSemantics.fullScreen, ),
],
) )
] ]
) )
] ]
) ),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
)); ));
semantics.dispose(); semantics.dispose();
...@@ -118,11 +140,6 @@ void main() { ...@@ -118,11 +140,6 @@ void main() {
testWidgets('Semantics and Directionality - RTL', (WidgetTester tester) async { testWidgets('Semantics and Directionality - RTL', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
final TestSemantics expectedSemantics = new TestSemantics.root(
label: 'test1',
textDirection: TextDirection.rtl,
);
await tester.pumpWidget( await tester.pumpWidget(
new Directionality( new Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
...@@ -133,17 +150,12 @@ void main() { ...@@ -133,17 +150,12 @@ void main() {
), ),
); );
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.rtl));
}); });
testWidgets('Semantics and Directionality - LTR', (WidgetTester tester) async { testWidgets('Semantics and Directionality - LTR', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
final TestSemantics expectedSemantics = new TestSemantics.root(
label: 'test1',
textDirection: TextDirection.ltr,
);
await tester.pumpWidget( await tester.pumpWidget(
new Directionality( new Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -154,15 +166,19 @@ void main() { ...@@ -154,15 +166,19 @@ void main() {
), ),
); );
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.ltr));
}); });
testWidgets('Semantics and Directionality - overriding RTL with LTR', (WidgetTester tester) async { testWidgets('Semantics and Directionality - cannot override RTL with LTR', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
final TestSemantics expectedSemantics = new TestSemantics.root( final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
label: 'test1', label: 'test1',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
)
]
); );
await tester.pumpWidget( await tester.pumpWidget(
...@@ -176,15 +192,19 @@ void main() { ...@@ -176,15 +192,19 @@ void main() {
), ),
); );
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
}); });
testWidgets('Semantics and Directionality - overriding LTR with RTL', (WidgetTester tester) async { testWidgets('Semantics and Directionality - cannot override LTR with RTL', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
final TestSemantics expectedSemantics = new TestSemantics.root( final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
label: 'test1', label: 'test1',
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
)
]
); );
await tester.pumpWidget( await tester.pumpWidget(
...@@ -198,6 +218,6 @@ void main() { ...@@ -198,6 +218,6 @@ void main() {
), ),
); );
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
}); });
} }
...@@ -31,7 +31,7 @@ class TestSemantics { ...@@ -31,7 +31,7 @@ class TestSemantics {
/// * [TestSemantics.fullScreen] 800x600, the test screen's size in logical /// * [TestSemantics.fullScreen] 800x600, the test screen's size in logical
/// pixels, useful for other full-screen widgets. /// pixels, useful for other full-screen widgets.
TestSemantics({ TestSemantics({
@required this.id, this.id,
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
...@@ -40,8 +40,7 @@ class TestSemantics { ...@@ -40,8 +40,7 @@ class TestSemantics {
this.transform, this.transform,
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : assert(id != null), }) : assert(flags != null),
assert(flags != null),
assert(label != null), assert(label != null),
assert(children != null), assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>(); tags = tags?.toSet() ?? new Set<SemanticsTag>();
...@@ -73,7 +72,7 @@ class TestSemantics { ...@@ -73,7 +72,7 @@ class TestSemantics {
/// [TestSemantics.fullScreen] property may be useful as a value; it describes /// [TestSemantics.fullScreen] property may be useful as a value; it describes
/// an 800x600 rectangle, which is the test screen's size in logical pixels. /// an 800x600 rectangle, which is the test screen's size in logical pixels.
TestSemantics.rootChild({ TestSemantics.rootChild({
@required this.id, this.id,
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
...@@ -152,7 +151,7 @@ class TestSemantics { ...@@ -152,7 +151,7 @@ class TestSemantics {
/// The tags of this node. /// The tags of this node.
final Set<SemanticsTag> tags; final Set<SemanticsTag> tags;
bool _matches(SemanticsNode node, Map<dynamic, dynamic> matchState, { bool ignoreRect: false, bool ignoreTransform: false }) { bool _matches(SemanticsNode node, Map<dynamic, dynamic> matchState, { bool ignoreRect: false, bool ignoreTransform: false, bool ignoreId: false }) {
final SemanticsData nodeData = node.getSemanticsData(); final SemanticsData nodeData = node.getSemanticsData();
bool fail(String message) { bool fail(String message) {
...@@ -162,7 +161,7 @@ class TestSemantics { ...@@ -162,7 +161,7 @@ class TestSemantics {
if (node == null) if (node == null)
return fail('could not find node with id $id.'); return fail('could not find node with id $id.');
if (id != node.id) if (!ignoreId && id != node.id)
return fail('expected node id $id but found id ${node.id}.'); return fail('expected node id $id but found id ${node.id}.');
if (flags != nodeData.flags) if (flags != nodeData.flags)
return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.'); return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.');
...@@ -188,7 +187,7 @@ class TestSemantics { ...@@ -188,7 +187,7 @@ class TestSemantics {
final Iterator<TestSemantics> it = children.iterator; final Iterator<TestSemantics> it = children.iterator;
node.visitChildren((SemanticsNode node) { node.visitChildren((SemanticsNode node) {
it.moveNext(); it.moveNext();
if (!it.current._matches(node, matchState, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform)) { if (!it.current._matches(node, matchState, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform, ignoreId: ignoreId)) {
result = false; result = false;
return false; return false;
} }
...@@ -233,15 +232,16 @@ class SemanticsTester { ...@@ -233,15 +232,16 @@ class SemanticsTester {
} }
class _HasSemantics extends Matcher { class _HasSemantics extends Matcher {
const _HasSemantics(this._semantics, { this.ignoreRect: false, this.ignoreTransform: false }) : assert(_semantics != null), assert(ignoreRect != null), assert(ignoreTransform != null); const _HasSemantics(this._semantics, { this.ignoreRect: false, this.ignoreTransform: false, this.ignoreId: false }) : assert(_semantics != null), assert(ignoreRect != null), assert(ignoreId != null), assert(ignoreTransform != null);
final TestSemantics _semantics; final TestSemantics _semantics;
final bool ignoreRect; final bool ignoreRect;
final bool ignoreTransform; final bool ignoreTransform;
final bool ignoreId;
@override @override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) { bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
return _semantics._matches(item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode, matchState, ignoreTransform: ignoreTransform, ignoreRect: ignoreRect); return _semantics._matches(item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode, matchState, ignoreTransform: ignoreTransform, ignoreRect: ignoreRect, ignoreId: ignoreId);
} }
@override @override
...@@ -259,7 +259,8 @@ class _HasSemantics extends Matcher { ...@@ -259,7 +259,8 @@ class _HasSemantics extends Matcher {
Matcher hasSemantics(TestSemantics semantics, { Matcher hasSemantics(TestSemantics semantics, {
bool ignoreRect: false, bool ignoreRect: false,
bool ignoreTransform: false, bool ignoreTransform: false,
}) => new _HasSemantics(semantics, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform); bool ignoreId: false,
}) => new _HasSemantics(semantics, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform, ignoreId: ignoreId);
class _IncludesNodeWith extends Matcher { class _IncludesNodeWith extends Matcher {
const _IncludesNodeWith({ const _IncludesNodeWith({
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
testWidgets('Simple tree is simple', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
const Center(
child: const Text('Hello!', textDirection: TextDirection.ltr)
),
);
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: 'Hello!',
textDirection: TextDirection.ltr,
rect: new Rect.fromLTRB(0.0, 0.0, 84.0, 14.0),
transform: new Matrix4.translationValues(358.0, 293.0, 0.0),
)
],
)));
semantics.dispose();
});
testWidgets('Simple tree is simple - material', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
// Not using Text widget because of https://github.com/flutter/flutter/issues/12357.
await tester.pumpWidget(new MaterialApp(
home: new Center(
child: new Semantics(
label: 'Hello!',
child: new Container(
width: 10.0,
height: 10.0,
),
),
),
));
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 2,
label: 'Hello!',
textDirection: TextDirection.ltr,
rect: new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
transform: new Matrix4.translationValues(395.0, 295.0, 0.0),
)
],
)));
semantics.dispose();
});
}
...@@ -50,7 +50,7 @@ void main() { ...@@ -50,7 +50,7 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 4,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
...@@ -58,15 +58,15 @@ void main() { ...@@ -58,15 +58,15 @@ void main() {
actions: SemanticsAction.scrollUp.index, actions: SemanticsAction.scrollUp.index,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 2, id: 1,
label: 'Item 0', label: 'Item 0',
), ),
new TestSemantics( new TestSemantics(
id: 3, id: 2,
label: 'Item 1', label: 'Item 1',
), ),
new TestSemantics( new TestSemantics(
id: 4, id: 3,
label: 'Semantics Test with Slivers', label: 'Semantics Test with Slivers',
), ),
], ],
...@@ -88,7 +88,7 @@ void main() { ...@@ -88,7 +88,7 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 4,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
...@@ -96,11 +96,11 @@ void main() { ...@@ -96,11 +96,11 @@ void main() {
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 2, id: 1,
label: 'Item 0', label: 'Item 0',
), ),
new TestSemantics( new TestSemantics(
id: 3, id: 2,
label: 'Item 1', label: 'Item 1',
), ),
new TestSemantics( new TestSemantics(
...@@ -110,7 +110,7 @@ void main() { ...@@ -110,7 +110,7 @@ void main() {
], ],
), ),
new TestSemantics( new TestSemantics(
id: 7, id: 3,
label: 'Semantics Test with Slivers', label: 'Semantics Test with Slivers',
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling], tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling],
), ),
...@@ -131,7 +131,7 @@ void main() { ...@@ -131,7 +131,7 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 4,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
...@@ -139,11 +139,11 @@ void main() { ...@@ -139,11 +139,11 @@ void main() {
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 2, id: 1,
label: 'Item 0', label: 'Item 0',
), ),
new TestSemantics( new TestSemantics(
id: 3, id: 2,
label: 'Item 1', label: 'Item 1',
), ),
new TestSemantics( new TestSemantics(
...@@ -151,7 +151,7 @@ void main() { ...@@ -151,7 +151,7 @@ void main() {
label: 'Item 2', label: 'Item 2',
), ),
new TestSemantics( new TestSemantics(
id: 8, id: 3,
label: 'Semantics Test with Slivers', label: 'Semantics Test with Slivers',
), ),
], ],
...@@ -206,16 +206,16 @@ void main() { ...@@ -206,16 +206,16 @@ void main() {
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 12, id: 10,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 10, id: 7,
label: 'Item 2', label: 'Item 2',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
new TestSemantics( new TestSemantics(
id: 11, id: 8,
label: 'Item 1', label: 'Item 1',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
...@@ -256,34 +256,34 @@ void main() { ...@@ -256,34 +256,34 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 13, id: 16,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 19, id: 17,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 14, id: 11,
label: 'Item 4', label: 'Item 4',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
new TestSemantics( new TestSemantics(
id: 15, id: 12,
label: 'Item 3', label: 'Item 3',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
new TestSemantics( new TestSemantics(
id: 16, id: 13,
label: 'Item 2', label: 'Item 2',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
new TestSemantics( new TestSemantics(
id: 17, id: 14,
label: 'Item 1', label: 'Item 1',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
new TestSemantics( new TestSemantics(
id: 18, id: 15,
label: 'Item 0', label: 'Item 0',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
...@@ -319,6 +319,7 @@ void main() { ...@@ -319,6 +319,7 @@ void main() {
const SliverAppBar( const SliverAppBar(
pinned: true, pinned: true,
expandedHeight: 100.0, expandedHeight: 100.0,
title: const Text('AppBar'),
), ),
new SliverList( new SliverList(
delegate: new SliverChildListDelegate(listChildren), delegate: new SliverChildListDelegate(listChildren),
...@@ -336,31 +337,31 @@ void main() { ...@@ -336,31 +337,31 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 20, id: 22,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 25, id: 23,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
// Item 0 is missing because its covered by the app bar. // Item 0 is missing because its covered by the app bar.
new TestSemantics( new TestSemantics(
id: 21, id: 18,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
// Item 1 starts 20.0dp below edge, so there would be room for Item 0. // Item 1 starts 20.0dp below edge, so there would be room for Item 0.
transform: new Matrix4.translation(new Vector3(0.0, 20.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 20.0, 0.0)),
label: 'Item 1', label: 'Item 1',
), ),
new TestSemantics( new TestSemantics(
id: 22, id: 19,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 220.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 220.0, 0.0)),
label: 'Item 2', label: 'Item 2',
), ),
new TestSemantics( new TestSemantics(
id: 23, id: 20,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 420.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 420.0, 0.0)),
label: 'Item 3', label: 'Item 3',
...@@ -368,14 +369,16 @@ void main() { ...@@ -368,14 +369,16 @@ void main() {
], ],
), ),
new TestSemantics( new TestSemantics(
id: 24, id: 21,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0),
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling], tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling],
label: 'AppBar',
), ),
], ],
) )
], ],
), ),
ignoreTransform: true,
)); ));
semantics.dispose(); semantics.dispose();
...@@ -403,6 +406,7 @@ void main() { ...@@ -403,6 +406,7 @@ void main() {
const SliverAppBar( const SliverAppBar(
pinned: true, pinned: true,
expandedHeight: 100.0, expandedHeight: 100.0,
title: const Text('AppBar'),
), ),
]..addAll(slivers), ]..addAll(slivers),
), ),
...@@ -416,29 +420,29 @@ void main() { ...@@ -416,29 +420,29 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 26, id: 28,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 31, id: 29,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 27, id: 24,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 420.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 420.0, 0.0)),
label: 'Item 3', label: 'Item 3',
), ),
new TestSemantics( new TestSemantics(
id: 28, id: 25,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 220.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 220.0, 0.0)),
label: 'Item 2', label: 'Item 2',
), ),
new TestSemantics( new TestSemantics(
id: 29, id: 26,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
// Item 1 starts 20.0dp below edge, so there would be room for Item 0. // Item 1 starts 20.0dp below edge, so there would be room for Item 0.
transform: new Matrix4.translation(new Vector3(0.0, 20.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 20.0, 0.0)),
...@@ -448,14 +452,16 @@ void main() { ...@@ -448,14 +452,16 @@ void main() {
], ],
), ),
new TestSemantics( new TestSemantics(
id: 30, id: 27,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0),
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling], tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling],
label: 'AppBar'
), ),
], ],
) )
], ],
), ),
ignoreTransform: true,
)); ));
semantics.dispose(); semantics.dispose();
...@@ -481,6 +487,7 @@ void main() { ...@@ -481,6 +487,7 @@ void main() {
const SliverAppBar( const SliverAppBar(
pinned: true, pinned: true,
expandedHeight: 100.0, expandedHeight: 100.0,
title: const Text('AppBar'),
), ),
new SliverList( new SliverList(
delegate: new SliverChildListDelegate(listChildren), delegate: new SliverChildListDelegate(listChildren),
...@@ -498,31 +505,31 @@ void main() { ...@@ -498,31 +505,31 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 32, id: 34,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 37, id: 35,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
// Item 0 is missing because its covered by the app bar. // Item 0 is missing because its covered by the app bar.
new TestSemantics( new TestSemantics(
id: 33, id: 30,
// Item 1 ends at 580dp, so there would be 20dp space for Item 0. // Item 1 ends at 580dp, so there would be 20dp space for Item 0.
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 380.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 380.0, 0.0)),
label: 'Item 1', label: 'Item 1',
), ),
new TestSemantics( new TestSemantics(
id: 34, id: 31,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 180.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 180.0, 0.0)),
label: 'Item 2', label: 'Item 2',
), ),
new TestSemantics( new TestSemantics(
id: 35, id: 32,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, -20.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, -20.0, 0.0)),
label: 'Item 3', label: 'Item 3',
...@@ -530,15 +537,17 @@ void main() { ...@@ -530,15 +537,17 @@ void main() {
], ],
), ),
new TestSemantics( new TestSemantics(
id: 36, id: 33,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0),
transform: new Matrix4.translation(new Vector3(0.0, 544.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 544.0, 0.0)),
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling], tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling],
label: 'AppBar'
), ),
], ],
) )
], ],
), ),
ignoreTransform: true,
)); ));
semantics.dispose(); semantics.dispose();
...@@ -567,6 +576,7 @@ void main() { ...@@ -567,6 +576,7 @@ void main() {
const SliverAppBar( const SliverAppBar(
pinned: true, pinned: true,
expandedHeight: 100.0, expandedHeight: 100.0,
title: const Text('AppBar'),
), ),
]..addAll(slivers), ]..addAll(slivers),
), ),
...@@ -580,29 +590,29 @@ void main() { ...@@ -580,29 +590,29 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 38, id: 40,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 43, id: 41,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 39, id: 36,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, -20.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, -20.0, 0.0)),
label: 'Item 3', label: 'Item 3',
), ),
new TestSemantics( new TestSemantics(
id: 40, id: 37,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
transform: new Matrix4.translation(new Vector3(0.0, 180.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 180.0, 0.0)),
label: 'Item 2', label: 'Item 2',
), ),
new TestSemantics( new TestSemantics(
id: 41, id: 38,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0),
// Item 1 ends at 580dp, so there would be 20dp space for Item 0. // Item 1 ends at 580dp, so there would be 20dp space for Item 0.
transform: new Matrix4.translation(new Vector3(0.0, 380.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 380.0, 0.0)),
...@@ -612,15 +622,17 @@ void main() { ...@@ -612,15 +622,17 @@ void main() {
], ],
), ),
new TestSemantics( new TestSemantics(
id: 42, id: 39,
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0),
transform: new Matrix4.translation(new Vector3(0.0, 544.0, 0.0)), transform: new Matrix4.translation(new Vector3(0.0, 544.0, 0.0)),
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling], tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling],
label: 'AppBar'
), ),
], ],
) )
], ],
), ),
ignoreTransform: true,
)); ));
semantics.dispose(); semantics.dispose();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment