Unverified Commit 8a6e9737 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Remove semantics boundary from gesture detector AND MORE (#13983)

`RenderSemanticsGestureHandler` is no longer a semantics boundary, which allows us to correctly mark disabled buttons as disabled without having their semantics size and semantics node id change unexpectedly.

Fixes https://github.com/flutter/flutter/issues/12589.
Fixes https://github.com/flutter/flutter/issues/11991.
See also https://github.com/flutter/flutter/issues/11993.

This change also required some refactoring to how we deal with `twoPaneSemantics` scrolling as it previously relied on `RenderSemanticsGestureHandler` being a semantics boundary. This should also make the underlying logic easier to understand.

In addition, the following minor changes are included in this PR:
* Removal of orphaned and unused `SemanticsConfiguration.isMergingDescendantsIntoOneNode`.
* Logic optimizations for `markNeedsSemanticsUpdate` .
* Fix for edge case where `MergeSemantics` failed to merge semantics.
* Use of emojis to better indicate leaf merging in the printed semantics tree.
* Better assert message for adding invisible child semantics nodes.
* Make some semantics tests robuster by not relying on creation order of SemanticsNode ids across test boundaries.

Fixes https://github.com/flutter/flutter/issues/13943.
parent 33d8a035
...@@ -332,7 +332,7 @@ class _MaterialButtonState extends State<MaterialButton> { ...@@ -332,7 +332,7 @@ class _MaterialButtonState extends State<MaterialButton> {
child: new Center( child: new Center(
widthFactor: 1.0, widthFactor: 1.0,
heightFactor: 1.0, heightFactor: 1.0,
child: new Semantics(button: true, child: widget.child), child: widget.child,
) )
) )
) )
...@@ -352,12 +352,17 @@ class _MaterialButtonState extends State<MaterialButton> { ...@@ -352,12 +352,17 @@ class _MaterialButtonState extends State<MaterialButton> {
child: contents child: contents
); );
} }
return new ConstrainedBox( return new Semantics(
container: true,
button: true,
enabled: widget.enabled,
child: new ConstrainedBox(
constraints: new BoxConstraints( constraints: new BoxConstraints(
minWidth: widget.minWidth ?? buttonTheme.minWidth, minWidth: widget.minWidth ?? buttonTheme.minWidth,
minHeight: height, minHeight: height,
), ),
child: contents child: contents
),
); );
} }
} }
...@@ -2243,7 +2243,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2243,7 +2243,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// RenderObject are still up-to date. Therefore, we will later only rebuild // RenderObject are still up-to date. Therefore, we will later only rebuild
// the semantics subtree starting at th identified semantics boundary. // the semantics subtree starting at th identified semantics boundary.
final bool wasSemanticsBoundary = _cachedSemanticsConfiguration?.isSemanticBoundary == true; final bool wasSemanticsBoundary = _semantics != null && _cachedSemanticsConfiguration?.isSemanticBoundary == true;
_cachedSemanticsConfiguration = null; _cachedSemanticsConfiguration = null;
bool isEffectiveSemanticsBoundary = _semanticsConfiguration.isSemanticBoundary && wasSemanticsBoundary; bool isEffectiveSemanticsBoundary = _semanticsConfiguration.isSemanticBoundary && wasSemanticsBoundary;
RenderObject node = this; RenderObject node = this;
...@@ -2254,7 +2254,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2254,7 +2254,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
node._needsSemanticsUpdate = true; node._needsSemanticsUpdate = true;
node = node.parent; node = node.parent;
node._cachedSemanticsConfiguration = null;
isEffectiveSemanticsBoundary = node._semanticsConfiguration.isSemanticBoundary; isEffectiveSemanticsBoundary = node._semanticsConfiguration.isSemanticBoundary;
if (isEffectiveSemanticsBoundary && node._semantics == null) { if (isEffectiveSemanticsBoundary && node._semantics == null) {
// We have reached a semantics boundary that doesn't own a semantics node. // We have reached a semantics boundary that doesn't own a semantics node.
...@@ -2274,10 +2273,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2274,10 +2273,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
owner._nodesNeedingSemantics.remove(this); owner._nodesNeedingSemantics.remove(this);
} }
if (!node._needsSemanticsUpdate) { if (!node._needsSemanticsUpdate) {
if (node != this) {
// Reset for `this` happened above already.
node._cachedSemanticsConfiguration = null;
}
node._needsSemanticsUpdate = true; node._needsSemanticsUpdate = true;
if (owner != null) { if (owner != null) {
assert(node._semanticsConfiguration.isSemanticBoundary || node.parent is! RenderObject); assert(node._semanticsConfiguration.isSemanticBoundary || node.parent is! RenderObject);
......
...@@ -2565,37 +2565,6 @@ class RenderSemanticsGestureHandler extends RenderProxyBox { ...@@ -2565,37 +2565,6 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
_onVerticalDragUpdate = onVerticalDragUpdate, _onVerticalDragUpdate = onVerticalDragUpdate,
super(child); super(child);
/// When a [SemanticsNode] that is a direct child of this object's
/// [SemanticsNode] is tagged with [excludeFromScrolling] it will not be
/// part of the scrolling area for semantic purposes.
///
/// This behavior is only active if the [SemanticsNode] of this
/// [RenderSemanticsGestureHandler] is tagged with [useTwoPaneSemantics].
/// Otherwise, the [excludeFromScrolling] tag is ignored.
///
/// As an example, a [RenderSliver] that stays on the screen within a
/// [Scrollable] even though the user has scrolled past it (e.g. a pinned app
/// bar) can tag its [SemanticsNode] with [excludeFromScrolling] to indicate
/// that it should no longer be considered for semantic actions related to
/// scrolling.
static const SemanticsTag excludeFromScrolling = const SemanticsTag('RenderSemanticsGestureHandler.excludeFromScrolling');
/// If the [SemanticsNode] of this [RenderSemanticsGestureHandler] is tagged
/// with [useTwoPaneSemantics], two semantics nodes will be used to represent
/// this render object in the semantics tree.
///
/// Two semantics nodes are necessary to exclude certain child nodes (via the
/// [excludeFromScrolling] tag) from the scrollable area for semantic
/// purposes.
///
/// If this tag is used, the first "outer" semantics node is the regular node
/// of this object. The second "inner" node is introduced as a child to that
/// node. All scrollable children become children of the inner node, which has
/// the semantic scrolling logic enabled. All children that have been
/// excluded from scrolling with [excludeFromScrolling] are turned into
/// children of the outer node.
static const SemanticsTag useTwoPaneSemantics = const SemanticsTag('RenderSemanticsGestureHandler.twoPane');
/// If non-null, the set of actions to allow. Other actions will be omitted, /// If non-null, the set of actions to allow. Other actions will be omitted,
/// even if their callback is provided. /// even if their callback is provided.
/// ///
...@@ -2673,24 +2642,10 @@ class RenderSemanticsGestureHandler extends RenderProxyBox { ...@@ -2673,24 +2642,10 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
/// leftwards drag. /// leftwards drag.
double scrollFactor; double scrollFactor;
bool get _hasHandlers {
return onTap != null
|| onLongPress != null
|| onHorizontalDragUpdate != null
|| onVerticalDragUpdate != null;
}
@override @override
void describeSemanticsConfiguration(SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(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;
if (onTap != null && _isValidAction(SemanticsAction.tap)) if (onTap != null && _isValidAction(SemanticsAction.tap))
config.onTap = onTap; config.onTap = onTap;
if (onLongPress != null && _isValidAction(SemanticsAction.longPress)) if (onLongPress != null && _isValidAction(SemanticsAction.longPress))
...@@ -2713,42 +2668,6 @@ class RenderSemanticsGestureHandler extends RenderProxyBox { ...@@ -2713,42 +2668,6 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
return validActions == null || validActions.contains(action); return validActions == null || validActions.contains(action);
} }
SemanticsNode _innerNode;
SemanticsNode _annotatedNode;
/// Sends a [SemanticsEvent] in the context of the [SemanticsNode] that is
/// annotated with this object's semantics information.
void sendSemanticsEvent(SemanticsEvent event) {
_annotatedNode?.sendEvent(event);
}
@override
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
if (children.isEmpty || !children.first.isTagged(useTwoPaneSemantics)) {
_annotatedNode = node;
super.assembleSemanticsNode(node, config, children);
return;
}
_innerNode ??= new SemanticsNode(showOnScreen: showOnScreen);
_innerNode
..isMergedIntoParent = node.isPartOfNodeMerging
..rect = Offset.zero & node.rect.size;
_annotatedNode = _innerNode;
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode];
final List<SemanticsNode> included = <SemanticsNode>[];
for (SemanticsNode child in children) {
assert(child.isTagged(useTwoPaneSemantics));
if (child.isTagged(excludeFromScrolling))
excluded.add(child);
else
included.add(child);
}
node.updateWith(config: null, childrenInInversePaintOrder: excluded);
_innerNode.updateWith(config: config, childrenInInversePaintOrder: included);
}
void _performSemanticScrollLeft() { void _performSemanticScrollLeft() {
if (onHorizontalDragUpdate != null) { if (onHorizontalDragUpdate != null) {
final double primaryDelta = size.width * -scrollFactor; final double primaryDelta = size.width * -scrollFactor;
......
...@@ -14,8 +14,8 @@ import 'package:vector_math/vector_math_64.dart'; ...@@ -14,8 +14,8 @@ import 'package:vector_math/vector_math_64.dart';
import 'binding.dart'; import 'binding.dart';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'proxy_box.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'viewport.dart';
import 'viewport_offset.dart'; import 'viewport_offset.dart';
/// A base class for slivers that have a [RenderBox] child which scrolls /// A base class for slivers that have a [RenderBox] child which scrolls
...@@ -225,7 +225,7 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje ...@@ -225,7 +225,7 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
if (_excludeFromSemanticsScrolling) if (_excludeFromSemanticsScrolling)
config.addTagForChildren(RenderSemanticsGestureHandler.excludeFromScrolling); config.addTagForChildren(RenderViewport.excludeFromScrolling);
} }
@override @override
......
...@@ -12,7 +12,6 @@ import 'package:vector_math/vector_math_64.dart'; ...@@ -12,7 +12,6 @@ import 'package:vector_math/vector_math_64.dart';
import 'binding.dart'; import 'binding.dart';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'proxy_box.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'viewport_offset.dart'; import 'viewport_offset.dart';
...@@ -91,12 +90,11 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -91,12 +90,11 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
_crossAxisDirection = crossAxisDirection, _crossAxisDirection = crossAxisDirection,
_offset = offset; _offset = offset;
@override @override
void describeSemanticsConfiguration(SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
config.addTagForChildren(RenderSemanticsGestureHandler.useTwoPaneSemantics); config.addTagForChildren(RenderViewport.useTwoPaneSemantics);
} }
@override @override
...@@ -746,6 +744,36 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat ...@@ -746,6 +744,36 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
_center = firstChild; _center = firstChild;
} }
/// If a [RenderAbstractViewport] overrides
/// [RenderObject.describeSemanticsConfiguration] to add the [SemanticsTag]
/// [useTwoPaneSemantics] to its [SemanticsConfiguration], two semantics nodes
/// will be used to represent the viewport with its associated scrolling
/// actions in the semantics tree.
///
/// Two semantics nodes (an inner and an outer node) are necessary to exclude
/// certain child nodes (via the [excludeFromScrolling] tag) from the
/// scrollable area for semantic purposes: The [SemanticsNode]s of children
/// that should be excluded from scrolling will be attached to the outer node.
/// The semantic scrolling actions and the [SemanticsNode]s of scrollable
/// children will be attached to the inner node, which itself is a child of
/// the outer node.
static const SemanticsTag useTwoPaneSemantics = const SemanticsTag('RenderViewport.twoPane');
/// When a top-level [SemanticsNode] below a [RenderAbstractViewport] is
/// tagged with [excludeFromScrolling] it will not be part of the scrolling
/// area for semantic purposes.
///
/// This behavior is only active if the [RenderAbstractViewport]
/// tagged its [SemanticsConfiguration] with [useTwoPaneSemantics].
/// Otherwise, the [excludeFromScrolling] tag is ignored.
///
/// As an example, a [RenderSliver] that stays on the screen within a
/// [Scrollable] even though the user has scrolled past it (e.g. a pinned app
/// bar) can tag its [SemanticsNode] with [excludeFromScrolling] to indicate
/// that it should no longer be considered for semantic actions related to
/// scrolling.
static const SemanticsTag excludeFromScrolling = const SemanticsTag('RenderViewport.excludeFromScrolling');
@override @override
void setupParentData(RenderObject child) { void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalContainerParentData) if (child.parentData is! SliverPhysicalContainerParentData)
......
...@@ -654,6 +654,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -654,6 +654,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
); );
} }
} }
assert(!newChildren.any((SemanticsNode node) => node.isMergedIntoParent) || isPartOfNodeMerging);
_debugPreviousSnapshot = new List<SemanticsNode>.from(newChildren); _debugPreviousSnapshot = new List<SemanticsNode>.from(newChildren);
...@@ -677,7 +678,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -677,7 +678,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
} }
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 is invisible and should not be added as child of $this.');
child._dead = false; child._dead = false;
} }
} }
...@@ -1072,7 +1073,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1072,7 +1073,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
hideOwner = inDirtyNodes; hideOwner = inDirtyNodes;
} }
properties.add(new DiagnosticsProperty<SemanticsOwner>('owner', owner, level: hideOwner ? DiagnosticLevel.hidden : DiagnosticLevel.info)); properties.add(new DiagnosticsProperty<SemanticsOwner>('owner', owner, level: hideOwner ? DiagnosticLevel.hidden : DiagnosticLevel.info));
properties.add(new FlagProperty('isPartOfNodeMerging', value: isPartOfNodeMerging, ifTrue: 'leaf merge')); properties.add(new FlagProperty('isMergedIntoParent', value: isMergedIntoParent, ifTrue: 'merged up ⬆️'));
properties.add(new FlagProperty('mergeAllDescendantsIntoThisNode', value: mergeAllDescendantsIntoThisNode, ifTrue: 'merge boundary ⛔️'));
final Offset offset = transform != null ? MatrixUtils.getAsTranslation(transform) : null; final Offset offset = transform != null ? MatrixUtils.getAsTranslation(transform) : null;
if (offset != null) { if (offset != null) {
properties.add(new DiagnosticsProperty<Rect>('rect', rect.shift(offset), showName: false)); properties.add(new DiagnosticsProperty<Rect>('rect', rect.shift(offset), showName: false));
...@@ -1344,7 +1346,7 @@ class SemanticsConfiguration { ...@@ -1344,7 +1346,7 @@ class SemanticsConfiguration {
bool get isSemanticBoundary => _isSemanticBoundary; bool get isSemanticBoundary => _isSemanticBoundary;
bool _isSemanticBoundary = false; bool _isSemanticBoundary = false;
set isSemanticBoundary(bool value) { set isSemanticBoundary(bool value) {
assert(!isMergingDescendantsIntoOneNode || value); assert(!isMergingSemanticsOfDescendants || value);
_isSemanticBoundary = value; _isSemanticBoundary = value;
} }
...@@ -1380,20 +1382,6 @@ class SemanticsConfiguration { ...@@ -1380,20 +1382,6 @@ class SemanticsConfiguration {
/// determine if a node is previous to this one. /// determine if a node is previous to this one.
bool isBlockingSemanticsOfPreviouslyPaintedNodes = false; 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 = value;
}
// SEMANTIC ANNOTATIONS // SEMANTIC ANNOTATIONS
// These will end up on [SemanticNode]s generated from // These will end up on [SemanticNode]s generated from
// [SemanticsConfiguration]s. // [SemanticsConfiguration]s.
...@@ -1645,9 +1633,12 @@ class SemanticsConfiguration { ...@@ -1645,9 +1633,12 @@ class SemanticsConfiguration {
/// If set to true, the descendants of the owning [RenderObject]'s /// If set to true, the descendants of the owning [RenderObject]'s
/// [SemanticsNode] will merge their semantic information into the /// [SemanticsNode] will merge their semantic information into the
/// [SemanticsNode] representing the owning [RenderObject]. /// [SemanticsNode] representing the owning [RenderObject].
///
/// Setting this to true requires that [isSemanticBoundary] is also true.
bool get isMergingSemanticsOfDescendants => _isMergingSemanticsOfDescendants; bool get isMergingSemanticsOfDescendants => _isMergingSemanticsOfDescendants;
bool _isMergingSemanticsOfDescendants = false; bool _isMergingSemanticsOfDescendants = false;
set isMergingSemanticsOfDescendants(bool value) { set isMergingSemanticsOfDescendants(bool value) {
assert(isSemanticBoundary);
_isMergingSemanticsOfDescendants = value; _isMergingSemanticsOfDescendants = value;
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
...@@ -1910,9 +1901,11 @@ class SemanticsConfiguration { ...@@ -1910,9 +1901,11 @@ class SemanticsConfiguration {
/// Returns an exact copy of this configuration. /// Returns an exact copy of this configuration.
SemanticsConfiguration copy() { SemanticsConfiguration copy() {
return new SemanticsConfiguration() return new SemanticsConfiguration()
..isSemanticBoundary = isSemanticBoundary .._isSemanticBoundary = _isSemanticBoundary
..explicitChildNodes = explicitChildNodes ..explicitChildNodes = explicitChildNodes
..isBlockingSemanticsOfPreviouslyPaintedNodes = isBlockingSemanticsOfPreviouslyPaintedNodes
.._hasBeenAnnotated = _hasBeenAnnotated .._hasBeenAnnotated = _hasBeenAnnotated
.._isMergingSemanticsOfDescendants = _isMergingSemanticsOfDescendants
.._textDirection = _textDirection .._textDirection = _textDirection
.._label = _label .._label = _label
.._increasedValue = _increasedValue .._increasedValue = _increasedValue
...@@ -1920,6 +1913,7 @@ class SemanticsConfiguration { ...@@ -1920,6 +1913,7 @@ class SemanticsConfiguration {
.._decreasedValue = _decreasedValue .._decreasedValue = _decreasedValue
.._hint = _hint .._hint = _hint
.._flags = _flags .._flags = _flags
.._tagsForChildren = _tagsForChildren
.._actionsAsBits = _actionsAsBits .._actionsAsBits = _actionsAsBits
.._actions.addAll(_actions); .._actions.addAll(_actions);
} }
......
...@@ -570,21 +570,6 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -570,21 +570,6 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
} }
} }
/// Sends a [SemanticsEvent] in the context of the [SemanticsNode] that is
/// annotated with this object's semantics information.
///
/// The event can be interpreted by assistive technologies to provide
/// additional feedback to the user about the state of the UI.
///
/// The event will not be sent if [RawGestureDetector.excludeFromSemantics] is
/// set to true.
void sendSemanticsEvent(SemanticsEvent event) {
if (!widget.excludeFromSemantics) {
final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
semanticsGestureHandler.sendSemanticsEvent(event);
}
}
@override @override
void dispose() { void dispose() {
for (GestureRecognizer recognizer in _recognizers.values) for (GestureRecognizer recognizer in _recognizers.values)
......
...@@ -281,7 +281,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -281,7 +281,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
if (_semanticsScrollEventScheduled) if (_semanticsScrollEventScheduled)
return; return;
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
_gestureDetectorKey.currentState?.sendSemanticsEvent(new ScrollCompletedSemanticsEvent( final _RenderExcludableScrollSemantics render = _excludableScrollSemanticsKey.currentContext?.findRenderObject();
render?.sendSemanticsEvent(new ScrollCompletedSemanticsEvent(
axis: position.axis, axis: position.axis,
pixels: position.pixels, pixels: position.pixels,
minScrollExtent: position.minScrollExtent, minScrollExtent: position.minScrollExtent,
...@@ -332,7 +333,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -332,7 +333,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
} }
// SEMANTICS ACTIONS // SEMANTICS
final GlobalKey _excludableScrollSemanticsKey = new GlobalKey();
@override @override
@protected @protected
...@@ -487,10 +490,14 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -487,10 +490,14 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(position != null); assert(position != null);
// TODO(ianh): Having all these global keys is sad. // TODO(ianh): Having all these global keys is sad.
final Widget result = new RawGestureDetector( final Widget result = new _ExcludableScrollSemantics(
key: _excludableScrollSemanticsKey,
child: new RawGestureDetector(
key: _gestureDetectorKey, key: _gestureDetectorKey,
gestures: _gestureRecognizers, gestures: _gestureRecognizers,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: new Semantics(
explicitChildNodes: true,
child: new IgnorePointer( child: new IgnorePointer(
key: _ignorePointerKey, key: _ignorePointerKey,
ignoring: _shouldIgnorePointer, ignoring: _shouldIgnorePointer,
...@@ -501,6 +508,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -501,6 +508,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
child: widget.viewportBuilder(context, position), child: widget.viewportBuilder(context, position),
), ),
), ),
),
),
); );
return _configuration.buildViewportChrome(context, result, widget.axisDirection); return _configuration.buildViewportChrome(context, result, widget.axisDirection);
} }
...@@ -511,3 +520,70 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -511,3 +520,70 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
description.add(new DiagnosticsProperty<ScrollPosition>('position', position)); description.add(new DiagnosticsProperty<ScrollPosition>('position', position));
} }
} }
/// With [_ExcludableScrollSemantics] certain child [SemanticsNode]s can be
/// excluded from the scrollable area for semantics purposes.
///
/// Nodes, that are to be excluded, have to be tagged with
/// [RenderViewport.excludeFromScrolling] and the [RenderAbstractViewport] in
/// use has to add the [RenderViewport.useTwoPaneSemantics] tag to its
/// [SemanticsConfiguration] by overriding
/// [RenderObject.describeSemanticsConfiguration].
///
/// If the tag [RenderViewport.useTwoPaneSemantics] is present on the viewport,
/// two semantics nodes will be used to represent the [Scrollable]: The outer
/// node will contain all children, that are excluded from scrolling. The inner
/// node, which is annotated with the scrolling actions, will house the
/// scrollable children.
class _ExcludableScrollSemantics extends SingleChildRenderObjectWidget {
const _ExcludableScrollSemantics({ Key key, Widget child }) : super(key: key, child: child);
@override
_RenderExcludableScrollSemantics createRenderObject(BuildContext context) => new _RenderExcludableScrollSemantics();
}
class _RenderExcludableScrollSemantics extends RenderProxyBox {
_RenderExcludableScrollSemantics({ RenderBox child }) : super(child);
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = true;
}
SemanticsNode _innerNode;
SemanticsNode _annotatedNode;
@override
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
if (children.isEmpty || !children.first.isTagged(RenderViewport.useTwoPaneSemantics)) {
_annotatedNode = node;
super.assembleSemanticsNode(node, config, children);
return;
}
_innerNode ??= new SemanticsNode(showOnScreen: showOnScreen);
_innerNode
..isMergedIntoParent = node.isPartOfNodeMerging
..rect = Offset.zero & node.rect.size;
_annotatedNode = _innerNode;
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode];
final List<SemanticsNode> included = <SemanticsNode>[];
for (SemanticsNode child in children) {
assert(child.isTagged(RenderViewport.useTwoPaneSemantics));
if (child.isTagged(RenderViewport.excludeFromScrolling))
excluded.add(child);
else
included.add(child);
}
node.updateWith(config: null, childrenInInversePaintOrder: excluded);
_innerNode.updateWith(config: config, childrenInInversePaintOrder: included);
}
/// Sends a [SemanticsEvent] in the context of the [SemanticsNode] that is
/// annotated with this object's semantics information.
void sendSemanticsEvent(SemanticsEvent event) {
_annotatedNode?.sendEvent(event);
}
}
...@@ -13,6 +13,10 @@ import '../rendering/mock_canvas.dart'; ...@@ -13,6 +13,10 @@ import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
void main() { void main() {
setUp(() {
debugResetSemanticsIdCounter();
});
testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async { testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
...@@ -33,11 +37,17 @@ void main() { ...@@ -33,11 +37,17 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
actions: SemanticsAction.tap.index, actions: <SemanticsAction>[
SemanticsAction.tap,
],
label: 'ABC', label: 'ABC',
rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0),
transform: new Matrix4.translationValues(356.0, 282.0, 0.0), transform: new Matrix4.translationValues(356.0, 282.0, 0.0),
flags: SemanticsFlag.isButton.index, flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
) )
], ],
), ),
...@@ -67,11 +77,17 @@ void main() { ...@@ -67,11 +77,17 @@ void main() {
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
actions: SemanticsAction.tap.index, actions: <SemanticsAction>[
SemanticsAction.tap,
],
label: 'ABC', label: 'ABC',
rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0),
transform: new Matrix4.translationValues(356.0, 282.0, 0.0), transform: new Matrix4.translationValues(356.0, 282.0, 0.0),
flags: SemanticsFlag.isButton.index, flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
) )
] ]
), ),
...@@ -248,4 +264,83 @@ void main() { ...@@ -248,4 +264,83 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgets('Disabled MaterialButton has same semantic size as enabled and exposes disabled semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final Rect expectedButtonSize = new Rect.fromLTRB(0.0, 0.0, 116.0, 36.0);
// Button is in center of screen
final Matrix4 expectedButtonTransform = new Matrix4.identity()
..translate(
TestSemantics.fullScreen.width / 2 - expectedButtonSize.width /2,
TestSemantics.fullScreen.height / 2 - expectedButtonSize.height /2,
);
// enabled button
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
child: new MaterialButton(
child: const Text('Button'),
onPressed: () { /* to make sure the button is enabled */ },
),
),
),
));
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: expectedButtonSize,
transform: expectedButtonTransform,
label: 'Button',
actions: <SemanticsAction>[
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
),
],
),
));
// disabled button
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr,
child: const Material(
child: const Center(
child: const MaterialButton(
child: const Text('Button'),
onPressed: null, // button is disabled
),
),
),
));
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: expectedButtonSize,
transform: expectedButtonTransform,
label: 'Button',
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
],
),
],
),
));
semantics.dispose();
});
} }
...@@ -48,8 +48,14 @@ void main() { ...@@ -48,8 +48,14 @@ void main() {
id: 2, id: 2,
label: 'Button', label: 'Button',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
actions: SemanticsAction.tap.index, actions: <SemanticsAction>[
flags: SemanticsFlag.isButton.index, SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
), ),
], ],
), ),
......
...@@ -94,11 +94,11 @@ void main() { ...@@ -94,11 +94,11 @@ void main() {
], ],
), ),
)); ));
// This test verifies that the label and the control get merged. // This test verifies that the label and the control get merged.
expect(semantics, hasSemantics(new TestSemantics.root( expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1,
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: <SemanticsFlag>[ flags: <SemanticsFlag>[
...@@ -111,7 +111,6 @@ void main() { ...@@ -111,7 +111,6 @@ void main() {
label: 'aaa\nAAA', label: 'aaa\nAAA',
), ),
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 4,
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: <SemanticsFlag>[ flags: <SemanticsFlag>[
...@@ -124,7 +123,6 @@ void main() { ...@@ -124,7 +123,6 @@ void main() {
label: 'bbb\nBBB', label: 'bbb\nBBB',
), ),
new TestSemantics.rootChild( new TestSemantics.rootChild(
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: new Matrix4.translationValues(0.0, 112.0, 0.0), transform: new Matrix4.translationValues(0.0, 112.0, 0.0),
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
...@@ -136,7 +134,7 @@ void main() { ...@@ -136,7 +134,7 @@ void main() {
label: 'CCC\nccc', label: 'CCC\nccc',
), ),
], ],
))); ), ignoreId: true));
}); });
} }
...@@ -585,13 +585,21 @@ void _tests() { ...@@ -585,13 +585,21 @@ void _tests() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
new TestSemantics( new TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isButton], flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
actions: <SemanticsAction>[SemanticsAction.tap], actions: <SemanticsAction>[SemanticsAction.tap],
label: r'CANCEL', label: r'CANCEL',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
new TestSemantics( new TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isButton], flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
actions: <SemanticsAction>[SemanticsAction.tap], actions: <SemanticsAction>[SemanticsAction.tap],
label: r'OK', label: r'OK',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
......
...@@ -165,6 +165,10 @@ class TestScrollPhysics extends ScrollPhysics { ...@@ -165,6 +165,10 @@ class TestScrollPhysics extends ScrollPhysics {
} }
void main() { void main() {
setUp(() {
debugResetSemanticsIdCounter();
});
testWidgets('TabBar tap selects tab', (WidgetTester tester) async { testWidgets('TabBar tap selects tab', (WidgetTester tester) async {
final List<String> tabs = <String>['A', 'B', 'C']; final List<String> tabs = <String>['A', 'B', 'C'];
...@@ -1213,6 +1217,10 @@ void main() { ...@@ -1213,6 +1217,10 @@ void main() {
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 2, id: 2,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
new TestSemantics(
id: 3,
actions: SemanticsAction.tap.index, actions: SemanticsAction.tap.index,
flags: SemanticsFlag.isSelected.index, flags: SemanticsFlag.isSelected.index,
label: 'TAB #0\nTab 1 of 2', label: 'TAB #0\nTab 1 of 2',
...@@ -1220,12 +1228,14 @@ void main() { ...@@ -1220,12 +1228,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: 3, id: 4,
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),
), ),
]
)
], ],
), ),
], ],
...@@ -1458,11 +1468,15 @@ void main() { ...@@ -1458,11 +1468,15 @@ 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: 23, id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
new TestSemantics(
id: 2,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 24, id: 3,
actions: SemanticsAction.tap.index, actions: SemanticsAction.tap.index,
flags: SemanticsFlag.isSelected.index, flags: SemanticsFlag.isSelected.index,
label: 'Semantics override 0\nTab 1 of 2', label: 'Semantics override 0\nTab 1 of 2',
...@@ -1470,12 +1484,14 @@ void main() { ...@@ -1470,12 +1484,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: 25, id: 4,
actions: SemanticsAction.tap.index, actions: SemanticsAction.tap.index,
label: 'Semantics override 1\nTab 2 of 2', label: 'Semantics override 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),
), ),
]
)
], ],
), ),
], ],
......
...@@ -11,6 +11,10 @@ import '../rendering/rendering_tester.dart'; ...@@ -11,6 +11,10 @@ import '../rendering/rendering_tester.dart';
void main() { void main() {
setUp(() {
debugResetSemanticsIdCounter();
});
group('SemanticsNode', () { group('SemanticsNode', () {
const SemanticsTag tag1 = const SemanticsTag('Tag One'); const SemanticsTag tag1 = const SemanticsTag('Tag One');
const SemanticsTag tag2 = const SemanticsTag('Tag Two'); const SemanticsTag tag2 = const SemanticsTag('Tag Two');
...@@ -45,6 +49,7 @@ void main() { ...@@ -45,6 +49,7 @@ void main() {
tags.add(tag3); tags.add(tag3);
final SemanticsConfiguration config = new SemanticsConfiguration() final SemanticsConfiguration config = new SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true; ..isMergingSemanticsOfDescendants = true;
node.updateWith( node.updateWith(
...@@ -121,9 +126,9 @@ void main() { ...@@ -121,9 +126,9 @@ void main() {
expect( expect(
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal), root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal),
'SemanticsNode#8(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n' 'SemanticsNode#3(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n'
'├SemanticsNode#6(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n' '├SemanticsNode#1(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n'
'└SemanticsNode#7(STALE, owner: null, Rect.fromLTRB(5.0, 0.0, 10.0, 5.0))\n', '└SemanticsNode#2(STALE, owner: null, Rect.fromLTRB(5.0, 0.0, 10.0, 5.0))\n',
); );
}); });
...@@ -140,16 +145,16 @@ void main() { ...@@ -140,16 +145,16 @@ void main() {
); );
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#3(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 20.0, 5.0))\n'
'├SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n' '├SemanticsNode#2(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n'
'└SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n', '└SemanticsNode#1(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n',
); );
expect( expect(
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest), root.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
'SemanticsNode#11(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 20.0, 5.0))\n' 'SemanticsNode#3(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 20.0, 5.0))\n'
'├SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n' '├SemanticsNode#1(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n'
'└SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n', '└SemanticsNode#2(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n',
); );
final SemanticsNode child3 = new SemanticsNode() final SemanticsNode child3 = new SemanticsNode()
...@@ -173,22 +178,22 @@ void main() { ...@@ -173,22 +178,22 @@ void main() {
expect( expect(
rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal), rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal),
'SemanticsNode#15(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 25.0, 5.0))\n' 'SemanticsNode#7(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 25.0, 5.0))\n'
'├SemanticsNode#12(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n' '├SemanticsNode#4(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n'
'│├SemanticsNode#14(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n' '│├SemanticsNode#6(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n'
'│└SemanticsNode#13(STALE, owner: null, Rect.fromLTRB(5.0, 0.0, 10.0, 5.0))\n' '│└SemanticsNode#5(STALE, owner: null, Rect.fromLTRB(5.0, 0.0, 10.0, 5.0))\n'
'├SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n' '├SemanticsNode#2(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n'
'└SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n', '└SemanticsNode#1(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n',
); );
expect( expect(
rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest), rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
'SemanticsNode#15(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 25.0, 5.0))\n' 'SemanticsNode#7(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 25.0, 5.0))\n'
'├SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n' '├SemanticsNode#1(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n'
'├SemanticsNode#10(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n' '├SemanticsNode#2(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n'
'└SemanticsNode#12(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n' '└SemanticsNode#4(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n'
' ├SemanticsNode#13(STALE, owner: null, Rect.fromLTRB(5.0, 0.0, 10.0, 5.0))\n' ' ├SemanticsNode#5(STALE, owner: null, Rect.fromLTRB(5.0, 0.0, 10.0, 5.0))\n'
' └SemanticsNode#14(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n', ' └SemanticsNode#6(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n',
); );
}); });
...@@ -196,15 +201,16 @@ void main() { ...@@ -196,15 +201,16 @@ void main() {
final SemanticsNode minimalProperties = new SemanticsNode(); final SemanticsNode minimalProperties = new SemanticsNode();
expect( expect(
minimalProperties.toStringDeep(), minimalProperties.toStringDeep(),
'SemanticsNode#16(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n', 'SemanticsNode#1(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n',
); );
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), actions: [], isSelected: false, isFocused: false, isButton: false, isTextField: false, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null)\n' 'SemanticsNode#1(owner: null, isMergedIntoParent: false, mergeAllDescendantsIntoThisNode: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isSelected: false, isFocused: false, isButton: false, isTextField: false, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null)\n'
); );
final SemanticsConfiguration config = new SemanticsConfiguration() final SemanticsConfiguration config = new SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true ..isMergingSemanticsOfDescendants = true
..onScrollUp = () { } ..onScrollUp = () { }
..onLongPress = () { } ..onLongPress = () { }
...@@ -220,7 +226,7 @@ void main() { ...@@ -220,7 +226,7 @@ void main() {
..updateWith(config: config, childrenInInversePaintOrder: null); ..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), actions: [longPress, scrollUp, showOnScreen], unchecked, selected, button, label: "Use all the properties", textDirection: rtl)\n', 'SemanticsNode#2(STALE, owner: null, merge boundary ⛔️, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), actions: [longPress, scrollUp, showOnScreen], unchecked, selected, button, label: "Use all the properties", textDirection: rtl)\n',
); );
expect( expect(
allProperties.getSemanticsData().toString(), allProperties.getSemanticsData().toString(),
...@@ -232,7 +238,7 @@ void main() { ...@@ -232,7 +238,7 @@ void main() {
..transform = new Matrix4.diagonal3(new Vector3(10.0, 10.0, 1.0)); ..transform = new Matrix4.diagonal3(new Vector3(10.0, 10.0, 1.0));
expect( expect(
scaled.toStringDeep(), scaled.toStringDeep(),
'SemanticsNode#18(STALE, owner: null, Rect.fromLTRB(50.0, 10.0, 70.0, 40.0) scaled by 10.0x)\n', 'SemanticsNode#3(STALE, owner: null, Rect.fromLTRB(50.0, 10.0, 70.0, 40.0) scaled by 10.0x)\n',
); );
expect( expect(
scaled.getSemanticsData().toString(), scaled.getSemanticsData().toString(),
...@@ -251,7 +257,6 @@ void main() { ...@@ -251,7 +257,6 @@ void main() {
expect(config.isSelected, isFalse); expect(config.isSelected, isFalse);
expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isFalse); expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isFalse);
expect(config.isFocused, isFalse); expect(config.isFocused, isFalse);
expect(config.isMergingDescendantsIntoOneNode, isFalse);
expect(config.isTextField, isFalse); expect(config.isTextField, isFalse);
expect(config.onShowOnScreen, isNull); expect(config.onShowOnScreen, isNull);
...@@ -274,7 +279,6 @@ void main() { ...@@ -274,7 +279,6 @@ void main() {
config.isSelected = true; config.isSelected = true;
config.isBlockingSemanticsOfPreviouslyPaintedNodes = true; config.isBlockingSemanticsOfPreviouslyPaintedNodes = true;
config.isFocused = true; config.isFocused = true;
config.isMergingDescendantsIntoOneNode = true;
config.isTextField = true; config.isTextField = true;
final VoidCallback onShowOnScreen = () { }; final VoidCallback onShowOnScreen = () { };
...@@ -309,7 +313,6 @@ void main() { ...@@ -309,7 +313,6 @@ void main() {
expect(config.isSelected, isTrue); expect(config.isSelected, isTrue);
expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isTrue); expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isTrue);
expect(config.isFocused, isTrue); expect(config.isFocused, isTrue);
expect(config.isMergingDescendantsIntoOneNode, isTrue);
expect(config.isTextField, isTrue); expect(config.isTextField, isTrue);
expect(config.onShowOnScreen, same(onShowOnScreen)); expect(config.onShowOnScreen, same(onShowOnScreen));
......
...@@ -220,6 +220,12 @@ void main() { ...@@ -220,6 +220,12 @@ void main() {
' │ diagnosis: insufficient data to draw conclusion (less than five\n' ' │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ repaints)\n' ' │ repaints)\n'
' │\n' ' │\n'
' └─child: _RenderExcludableScrollSemantics#00000\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ semantic boundary\n'
' │ size: Size(800.0, 600.0)\n'
' │\n'
' └─child: RenderSemanticsGestureHandler#00000\n' ' └─child: RenderSemanticsGestureHandler#00000\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
...@@ -233,6 +239,11 @@ void main() { ...@@ -233,6 +239,11 @@ void main() {
' │ behavior: opaque\n' ' │ behavior: opaque\n'
' │ listeners: down\n' ' │ listeners: down\n'
' │\n' ' │\n'
' └─child: RenderSemanticsAnnotations#00000\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │\n'
' └─child: RenderIgnorePointer#00000\n' ' └─child: RenderIgnorePointer#00000\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
...@@ -324,6 +335,12 @@ void main() { ...@@ -324,6 +335,12 @@ void main() {
' │ diagnosis: insufficient data to draw conclusion (less than five\n' ' │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ repaints)\n' ' │ repaints)\n'
' │\n' ' │\n'
' └─child: _RenderExcludableScrollSemantics#00000\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ semantic boundary\n'
' │ size: Size(800.0, 600.0)\n'
' │\n'
' └─child: RenderSemanticsGestureHandler#00000\n' ' └─child: RenderSemanticsGestureHandler#00000\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
...@@ -337,6 +354,11 @@ void main() { ...@@ -337,6 +354,11 @@ void main() {
' │ behavior: opaque\n' ' │ behavior: opaque\n'
' │ listeners: down\n' ' │ listeners: down\n'
' │\n' ' │\n'
' └─child: RenderSemanticsAnnotations#00000\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │\n'
' └─child: RenderIgnorePointer#00000\n' ' └─child: RenderIgnorePointer#00000\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
......
...@@ -110,7 +110,6 @@ void main() { ...@@ -110,7 +110,6 @@ 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: 2,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
actions: SemanticsAction.tap.index, actions: SemanticsAction.tap.index,
label: 'Dismiss', label: 'Dismiss',
...@@ -118,7 +117,7 @@ void main() { ...@@ -118,7 +117,7 @@ void main() {
), ),
] ]
); );
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
semantics.dispose(); semantics.dispose();
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
......
...@@ -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 SemanticsFlag;
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';
...@@ -10,6 +12,10 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -10,6 +12,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
void main() { void main() {
setUp(() {
debugResetSemanticsIdCounter();
});
testWidgets('MergeSemantics', (WidgetTester tester) async { testWidgets('MergeSemantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
...@@ -19,8 +25,14 @@ void main() { ...@@ -19,8 +25,14 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
const Text('test1'), new Semantics(
const Text('test2'), container: true,
child: const Text('test1'),
),
new Semantics(
container: true,
child: const Text('test2'),
),
], ],
), ),
), ),
...@@ -44,8 +56,14 @@ void main() { ...@@ -44,8 +56,14 @@ void main() {
child: new MergeSemantics( child: new MergeSemantics(
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
const Text('test1'), new Semantics(
const Text('test2'), container: true,
child: const Text('test1'),
),
new Semantics(
container: true,
child: const Text('test2'),
),
], ],
), ),
), ),
...@@ -71,19 +89,70 @@ void main() { ...@@ -71,19 +89,70 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
const Text('test1'), new Semantics(
const Text('test2'), container: true,
child: const Text('test1'),
),
new Semantics(
container: true,
child: const Text('test2'),
),
],
),
),
);
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(id: 6, label: 'test1'),
new TestSemantics.rootChild(id: 7, label: 'test2'),
],
),
ignoreRect: true,
ignoreTransform: true,
));
semantics.dispose();
});
testWidgets('MergeSemantics works if other nodes are implicitly merged into its node', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new MergeSemantics(
child: new Semantics(
selected: true, // this is implicitly merged into the MergeSemantics node
child: new Row(
children: <Widget>[
new Semantics(
container: true,
child: const Text('test1'),
),
new Semantics(
container: true,
child: const Text('test2'),
),
], ],
), ),
), ),
),
),
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild(id: 4, label: 'test1'), new TestSemantics.rootChild(
new TestSemantics.rootChild(id: 5, label: 'test2'), id: 1,
flags: <SemanticsFlag>[
SemanticsFlag.isSelected,
], ],
label: 'test1\ntest2',
),
]
), ),
ignoreRect: true, ignoreRect: true,
ignoreTransform: true, ignoreTransform: true,
......
...@@ -51,7 +51,7 @@ void main() { ...@@ -51,7 +51,7 @@ void main() {
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 1,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderViewport.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 5, id: 5,
...@@ -89,7 +89,7 @@ void main() { ...@@ -89,7 +89,7 @@ void main() {
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 1,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderViewport.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 5, id: 5,
...@@ -112,7 +112,7 @@ void main() { ...@@ -112,7 +112,7 @@ void main() {
new TestSemantics( new TestSemantics(
id: 4, id: 4,
label: 'Semantics Test with Slivers', label: 'Semantics Test with Slivers',
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling], tags: <SemanticsTag>[RenderViewport.excludeFromScrolling],
), ),
], ],
) )
...@@ -132,7 +132,7 @@ void main() { ...@@ -132,7 +132,7 @@ void main() {
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 1,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderViewport.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 5, id: 5,
...@@ -203,7 +203,7 @@ void main() { ...@@ -203,7 +203,7 @@ void main() {
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 7, id: 7,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderViewport.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 10, id: 10,
...@@ -257,7 +257,7 @@ void main() { ...@@ -257,7 +257,7 @@ void main() {
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 11, id: 11,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderViewport.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 17, id: 17,
...@@ -339,7 +339,7 @@ void main() { ...@@ -339,7 +339,7 @@ void main() {
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 18, id: 18,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderViewport.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 23, id: 23,
...@@ -371,7 +371,7 @@ void main() { ...@@ -371,7 +371,7 @@ void main() {
new TestSemantics( new TestSemantics(
id: 22, id: 22,
rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0), rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0),
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling], tags: <SemanticsTag>[RenderViewport.excludeFromScrolling],
label: 'AppBar', label: 'AppBar',
), ),
], ],
...@@ -422,7 +422,7 @@ void main() { ...@@ -422,7 +422,7 @@ void main() {
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 24, id: 24,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderViewport.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 29, id: 29,
...@@ -454,7 +454,7 @@ void main() { ...@@ -454,7 +454,7 @@ void main() {
new TestSemantics( new TestSemantics(
id: 28, id: 28,
rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0), rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0),
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling], tags: <SemanticsTag>[RenderViewport.excludeFromScrolling],
label: 'AppBar' label: 'AppBar'
), ),
], ],
...@@ -507,7 +507,7 @@ void main() { ...@@ -507,7 +507,7 @@ void main() {
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 30, id: 30,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderViewport.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 35, id: 35,
...@@ -540,7 +540,7 @@ void main() { ...@@ -540,7 +540,7 @@ void main() {
id: 34, id: 34,
rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.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>[RenderViewport.excludeFromScrolling],
label: 'AppBar' label: 'AppBar'
), ),
], ],
...@@ -592,7 +592,7 @@ void main() { ...@@ -592,7 +592,7 @@ void main() {
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 36, id: 36,
rect: TestSemantics.fullScreen, rect: TestSemantics.fullScreen,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics], tags: <SemanticsTag>[RenderViewport.useTwoPaneSemantics],
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 41, id: 41,
...@@ -625,7 +625,7 @@ void main() { ...@@ -625,7 +625,7 @@ void main() {
id: 40, id: 40,
rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.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>[RenderViewport.excludeFromScrolling],
label: 'AppBar' label: 'AppBar'
), ),
], ],
......
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