Commit 5ed8f1a1 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add semantics for Sliders (#4808)

Also, make SemanticsOwner into a real class and use it instead of a static in
several places.
parent f0671edf
...@@ -180,6 +180,8 @@ final Tween<double> _kLabelBalloonRadiusTween = new Tween<double>(begin: _kThumb ...@@ -180,6 +180,8 @@ final Tween<double> _kLabelBalloonRadiusTween = new Tween<double>(begin: _kThumb
final Tween<double> _kLabelBalloonTipTween = new Tween<double>(begin: 0.0, end: -8.0); final Tween<double> _kLabelBalloonTipTween = new Tween<double>(begin: 0.0, end: -8.0);
final double _kLabelBalloonTipAttachmentRatio = math.sin(math.PI / 4.0); final double _kLabelBalloonTipAttachmentRatio = math.sin(math.PI / 4.0);
const double _kAdjustmentUnit = 0.1; // Matches iOS implementation of material slider.
double _getAdditionalHeightForLabel(String label) { double _getAdditionalHeightForLabel(String label) {
return label == null ? 0.0 : _kLabelBalloonRadius * 2.0; return label == null ? 0.0 : _kLabelBalloonRadius * 2.0;
} }
...@@ -191,7 +193,7 @@ BoxConstraints _getAdditionalConstraints(String label) { ...@@ -191,7 +193,7 @@ BoxConstraints _getAdditionalConstraints(String label) {
); );
} }
class _RenderSlider extends RenderConstrainedBox { class _RenderSlider extends RenderConstrainedBox implements SemanticActionHandler {
_RenderSlider({ _RenderSlider({
double value, double value,
int divisions, int divisions,
...@@ -291,8 +293,10 @@ class _RenderSlider extends RenderConstrainedBox { ...@@ -291,8 +293,10 @@ class _RenderSlider extends RenderConstrainedBox {
return dragValue; return dragValue;
} }
bool get isInteractive => onChanged != null;
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
if (onChanged != null) { if (isInteractive) {
_active = true; _active = true;
_currentDragValue = (globalToLocal(details.globalPosition).x - _kReactionRadius) / _trackLength; _currentDragValue = (globalToLocal(details.globalPosition).x - _kReactionRadius) / _trackLength;
onChanged(_discretizedCurrentDragValue); onChanged(_discretizedCurrentDragValue);
...@@ -302,7 +306,7 @@ class _RenderSlider extends RenderConstrainedBox { ...@@ -302,7 +306,7 @@ class _RenderSlider extends RenderConstrainedBox {
} }
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
if (onChanged != null) { if (isInteractive) {
_currentDragValue += details.primaryDelta / _trackLength; _currentDragValue += details.primaryDelta / _trackLength;
onChanged(_discretizedCurrentDragValue); onChanged(_discretizedCurrentDragValue);
} }
...@@ -322,7 +326,7 @@ class _RenderSlider extends RenderConstrainedBox { ...@@ -322,7 +326,7 @@ class _RenderSlider extends RenderConstrainedBox {
@override @override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) { void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
if (event is PointerDownEvent && onChanged != null) if (event is PointerDownEvent && isInteractive)
_drag.addPointer(event); _drag.addPointer(event);
} }
...@@ -331,7 +335,7 @@ class _RenderSlider extends RenderConstrainedBox { ...@@ -331,7 +335,7 @@ class _RenderSlider extends RenderConstrainedBox {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
final double trackLength = _trackLength; final double trackLength = _trackLength;
final bool enabled = onChanged != null; final bool enabled = isInteractive;
final double value = _position.value; final double value = _position.value;
final double additionalHeightForLabel = _getAdditionalHeightForLabel(label); final double additionalHeightForLabel = _getAdditionalHeightForLabel(label);
...@@ -417,4 +421,32 @@ class _RenderSlider extends RenderConstrainedBox { ...@@ -417,4 +421,32 @@ class _RenderSlider extends RenderConstrainedBox {
} }
canvas.drawCircle(thumbCenter, thumbRadius + thumbRadiusDelta, thumbPaint); canvas.drawCircle(thumbCenter, thumbRadius + thumbRadiusDelta, thumbPaint);
} }
@override
bool get hasSemantics => isInteractive;
@override
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
yield (SemanticsNode semantics) {
if (isInteractive)
semantics.addAdjustmentActions();
};
}
@override
void performAction(SemanticAction action) {
switch (action) {
case SemanticAction.increase:
if (isInteractive)
onChanged((value + _kAdjustmentUnit).clamp(0.0, 1.0));
break;
case SemanticAction.decrease:
if (isInteractive)
onChanged((value - _kAdjustmentUnit).clamp(0.0, 1.0));
break;
default:
assert(false);
break;
}
}
} }
...@@ -133,13 +133,19 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, ...@@ -133,13 +133,19 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
/// ///
/// Called automatically when the binding is created. /// Called automatically when the binding is created.
void initSemantics() { void initSemantics() {
SemanticsNode.onSemanticsEnabled = renderView.scheduleInitialSemantics;
shell.provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) { shell.provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) {
ensureSemantics();
mojom.SemanticsServerStub server = new mojom.SemanticsServerStub.fromEndpoint(endpoint); mojom.SemanticsServerStub server = new mojom.SemanticsServerStub.fromEndpoint(endpoint);
server.impl = new SemanticsServer(); server.impl = new SemanticsServer(semanticsOwner: pipelineOwner.semanticsOwner);
}); });
} }
void ensureSemantics() {
if (pipelineOwner.semanticsOwner == null)
renderView.scheduleInitialSemantics();
assert(pipelineOwner.semanticsOwner != null);
}
void _handlePersistentFrameCallback(Duration timeStamp) { void _handlePersistentFrameCallback(Duration timeStamp) {
beginFrame(); beginFrame();
} }
...@@ -153,10 +159,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, ...@@ -153,10 +159,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
pipelineOwner.flushCompositingBits(); pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint(); pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU renderView.compositeFrame(); // this sends the bits to the GPU
if (SemanticsNode.hasListeners) { pipelineOwner.flushSemantics();
pipelineOwner.flushSemantics();
SemanticsNode.sendSemanticsTree();
}
} }
@override @override
......
...@@ -78,7 +78,7 @@ class SemanticsNode extends AbstractNode { ...@@ -78,7 +78,7 @@ class SemanticsNode extends AbstractNode {
/// The root node is assigned an identifier of zero. /// The root node is assigned an identifier of zero.
SemanticsNode.root({ SemanticsNode.root({
SemanticActionHandler handler, SemanticActionHandler handler,
Object owner SemanticsOwner owner
}) : _id = 0, }) : _id = 0,
_actionHandler = handler { _actionHandler = handler {
attach(owner); attach(owner);
...@@ -131,21 +131,34 @@ class SemanticsNode extends AbstractNode { ...@@ -131,21 +131,34 @@ class SemanticsNode extends AbstractNode {
final Set<SemanticAction> _actions = new Set<SemanticAction>(); final Set<SemanticAction> _actions = new Set<SemanticAction>();
/// Adds the given action to the set of semantic actions.
///
/// If the user chooses to perform an action,
/// [SemanticActionHandler.performAction] will be called with the chosen
/// action.
void addAction(SemanticAction action) { void addAction(SemanticAction action) {
if (_actions.add(action)) if (_actions.add(action))
_markDirty(); _markDirty();
} }
/// Adds the [SemanticAction.scrollLeft] and [SemanticAction.scrollRight] actions.
void addHorizontalScrollingActions() { void addHorizontalScrollingActions() {
addAction(SemanticAction.scrollLeft); addAction(SemanticAction.scrollLeft);
addAction(SemanticAction.scrollRight); addAction(SemanticAction.scrollRight);
} }
/// Adds the [SemanticAction.scrollUp] and [SemanticAction.scrollDown] actions.
void addVerticalScrollingActions() { void addVerticalScrollingActions() {
addAction(SemanticAction.scrollUp); addAction(SemanticAction.scrollUp);
addAction(SemanticAction.scrollDown); addAction(SemanticAction.scrollDown);
} }
/// Adds the [SemanticAction.increase] and [SemanticAction.decrease] actions.
void addAdjustmentActions() {
addAction(SemanticAction.increase);
addAction(SemanticAction.decrease);
}
bool _hasAction(SemanticAction action) { bool _hasAction(SemanticAction action) {
return _actionHandler != null && _actions.contains(action); return _actionHandler != null && _actions.contains(action);
} }
...@@ -285,6 +298,9 @@ class SemanticsNode extends AbstractNode { ...@@ -285,6 +298,9 @@ class SemanticsNode extends AbstractNode {
_markDirty(); _markDirty();
} }
@override
SemanticsOwner get owner => super.owner;
@override @override
SemanticsNode get parent => super.parent; SemanticsNode get parent => super.parent;
...@@ -309,15 +325,16 @@ class SemanticsNode extends AbstractNode { ...@@ -309,15 +325,16 @@ class SemanticsNode extends AbstractNode {
return true; return true;
} }
static Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
static Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();
@override @override
void attach(Object owner) { void attach(SemanticsOwner owner) {
super.attach(owner); super.attach(owner);
assert(!_nodes.containsKey(_id)); assert(!owner._nodes.containsKey(_id));
_nodes[_id] = this; owner._nodes[_id] = this;
_detachedNodes.remove(this); owner._detachedNodes.remove(this);
if (_dirty) {
_dirty = false;
_markDirty();
}
if (parent != null) if (parent != null)
_inheritedMergeAllDescendantsIntoThisNode = parent._shouldMergeAllDescendantsIntoThisNode; _inheritedMergeAllDescendantsIntoThisNode = parent._shouldMergeAllDescendantsIntoThisNode;
if (_children != null) { if (_children != null) {
...@@ -328,26 +345,27 @@ class SemanticsNode extends AbstractNode { ...@@ -328,26 +345,27 @@ class SemanticsNode extends AbstractNode {
@override @override
void detach() { void detach() {
assert(owner._nodes.containsKey(_id));
assert(!owner._detachedNodes.contains(this));
owner._nodes.remove(_id);
owner._detachedNodes.add(this);
super.detach(); super.detach();
assert(_nodes.containsKey(_id));
assert(!_detachedNodes.contains(this));
_nodes.remove(_id);
_detachedNodes.add(this);
if (_children != null) { if (_children != null) {
for (SemanticsNode child in _children) for (SemanticsNode child in _children)
child.detach(); child.detach();
} }
} }
static List<SemanticsNode> _dirtyNodes = <SemanticsNode>[];
bool _dirty = false; bool _dirty = false;
void _markDirty() { void _markDirty() {
if (_dirty) if (_dirty)
return; return;
_dirty = true; _dirty = true;
assert(!_dirtyNodes.contains(this)); if (attached) {
assert(!_detachedNodes.contains(this)); assert(!owner._dirtyNodes.contains(this));
_dirtyNodes.add(this); assert(!owner._detachedNodes.contains(this));
owner._dirtyNodes.add(this);
}
} }
mojom.SemanticsNode _serialize() { mojom.SemanticsNode _serialize() {
...@@ -397,35 +415,71 @@ class SemanticsNode extends AbstractNode { ...@@ -397,35 +415,71 @@ class SemanticsNode extends AbstractNode {
return result; return result;
} }
static List<mojom.SemanticsListener> _listeners; @override
String toString() {
StringBuffer buffer = new StringBuffer();
buffer.write('$runtimeType($_id');
if (_dirty)
buffer.write(" (${ owner != null && owner._dirtyNodes.contains(this) ? 'dirty' : 'STALE' })");
if (_shouldMergeAllDescendantsIntoThisNode)
buffer.write(' (leaf merge)');
buffer.write('; $rect');
if (wasAffectedByClip)
buffer.write(' (clipped)');
for (SemanticAction action in _actions) {
buffer.write('; $action');
}
if (hasCheckedState) {
if (isChecked)
buffer.write('; checked');
else
buffer.write('; unchecked');
}
if (label.isNotEmpty)
buffer.write('; "$label"');
buffer.write(')');
return buffer.toString();
}
/// Returns a string representation of this node and its descendants.
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
String result = '$prefixLineOne$this\n';
if (_children != null && _children.isNotEmpty) {
for (int index = 0; index < _children.length - 1; index += 1) {
SemanticsNode child = _children[index];
result += '${child.toStringDeep("$prefixOtherLines \u251C", "$prefixOtherLines \u2502")}';
}
result += '${_children.last.toStringDeep("$prefixOtherLines \u2514", "$prefixOtherLines ")}';
}
return result;
}
}
class SemanticsOwner {
final List<SemanticsNode> _dirtyNodes = <SemanticsNode>[];
final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
final Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();
List<mojom.SemanticsListener> _listeners;
/// Whether there are currently any consumers of semantic data. /// Whether there are currently any consumers of semantic data.
/// ///
/// If there are no consumers of semantic data, there is no need to compile /// If there are no consumers of semantic data, there is no need to compile
/// semantic data into a [SemanticsNode] tree. /// semantic data into a [SemanticsNode] tree.
static bool get hasListeners => _listeners != null && _listeners.length > 0; bool get hasListeners => _listeners != null && _listeners.length > 0;
/// Called when the first consumer of semantic data arrives.
///
/// Typically set by [RendererBinding].
static VoidCallback onSemanticsEnabled;
/// Add a consumer of semantic data. /// Add a consumer of semantic data.
/// ///
/// After the [PipelineOwner] updates the semantic data for a given frame, it /// After the [PipelineOwner] updates the semantic data for a given frame, it
/// calls [sendSemanticsTree], which uploads the data to each listener /// calls [sendSemanticsTree], which uploads the data to each listener
/// registered with this function. /// registered with this function.
static void addListener(mojom.SemanticsListener listener) { void addListener(mojom.SemanticsListener listener) {
if (!hasListeners) {
assert(onSemanticsEnabled != null); // initialise the binding _before_ adding listeners
onSemanticsEnabled();
}
_listeners ??= <mojom.SemanticsListener>[]; _listeners ??= <mojom.SemanticsListener>[];
_listeners.add(listener); _listeners.add(listener);
} }
/// Uploads the semantics tree to the listeners registered with [addListener]. /// Uploads the semantics tree to the listeners registered with [addListener].
static void sendSemanticsTree() { void sendSemanticsTree() {
assert(hasListeners); assert(hasListeners);
for (SemanticsNode oldNode in _detachedNodes) { for (SemanticsNode oldNode in _detachedNodes) {
// The other side will have forgotten this node if we even send // The other side will have forgotten this node if we even send
...@@ -491,7 +545,7 @@ class SemanticsNode extends AbstractNode { ...@@ -491,7 +545,7 @@ class SemanticsNode extends AbstractNode {
_dirtyNodes.clear(); _dirtyNodes.clear();
} }
static SemanticActionHandler _getSemanticActionHandlerForId(int id, { @required SemanticAction action }) { SemanticActionHandler _getSemanticActionHandlerForId(int id, { @required SemanticAction action }) {
assert(action != null); assert(action != null);
SemanticsNode result = _nodes[id]; SemanticsNode result = _nodes[id];
if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._hasAction(action)) { if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._hasAction(action)) {
...@@ -508,59 +562,29 @@ class SemanticsNode extends AbstractNode { ...@@ -508,59 +562,29 @@ class SemanticsNode extends AbstractNode {
return result._actionHandler; return result._actionHandler;
} }
@override void performAction(int id, SemanticAction action) {
String toString() { SemanticActionHandler handler = _getSemanticActionHandlerForId(id, action: action);
StringBuffer buffer = new StringBuffer(); handler?.performAction(action);
buffer.write('$runtimeType($_id');
if (_dirty)
buffer.write(" (${ _dirtyNodes.contains(this) ? 'dirty' : 'STALE' })");
if (_shouldMergeAllDescendantsIntoThisNode)
buffer.write(' (leaf merge)');
buffer.write('; $rect');
if (wasAffectedByClip)
buffer.write(' (clipped)');
for (SemanticAction action in _actions) {
buffer.write('; $action');
}
if (hasCheckedState) {
if (isChecked)
buffer.write('; checked');
else
buffer.write('; unchecked');
}
if (label.isNotEmpty)
buffer.write('; "$label"');
buffer.write(')');
return buffer.toString();
}
/// Returns a string representation of this node and its descendants.
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
String result = '$prefixLineOne$this\n';
if (_children != null && _children.isNotEmpty) {
for (int index = 0; index < _children.length - 1; index += 1) {
SemanticsNode child = _children[index];
result += '${child.toStringDeep("$prefixOtherLines \u251C", "$prefixOtherLines \u2502")}';
}
result += '${_children.last.toStringDeep("$prefixOtherLines \u2514", "$prefixOtherLines ")}';
}
return result;
} }
} }
/// Exposes the [SemanticsNode] tree to the underlying platform. /// Exposes the [SemanticsNode] tree to the underlying platform.
class SemanticsServer extends mojom.SemanticsServer { class SemanticsServer extends mojom.SemanticsServer {
SemanticsServer({ @required this.semanticsOwner }) {
assert(semanticsOwner != null);
}
final SemanticsOwner semanticsOwner;
@override @override
void addSemanticsListener(mojom.SemanticsListenerProxy listener) { void addSemanticsListener(mojom.SemanticsListenerProxy listener) {
// TODO(abarth): We should remove the listener when this pipe closes. // TODO(abarth): We should remove the listener when this pipe closes.
// See <https://github.com/flutter/flutter/issues/3342>. // See <https://github.com/flutter/flutter/issues/3342>.
SemanticsNode.addListener(listener); semanticsOwner.addListener(listener);
} }
@override @override
void performAction(int id, mojom.SemanticAction encodedAction) { void performAction(int id, mojom.SemanticAction encodedAction) {
SemanticAction action = SemanticAction.values[encodedAction.mojoEnumValue]; semanticsOwner.performAction(id, SemanticAction.values[encodedAction.mojoEnumValue]);
SemanticActionHandler node = SemanticsNode._getSemanticActionHandlerForId(id, action: action);
node?.performAction(action);
} }
} }
...@@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart'; ...@@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom; import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
import 'basic.dart'; import 'basic.dart';
import 'binding.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
...@@ -30,16 +31,23 @@ class SemanticsDebugger extends StatefulWidget { ...@@ -30,16 +31,23 @@ class SemanticsDebugger extends StatefulWidget {
} }
class _SemanticsDebuggerState extends State<SemanticsDebugger> { class _SemanticsDebuggerState extends State<SemanticsDebugger> {
_SemanticsClient _client;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_SemanticsDebuggerListener.ensureInstantiated(); // TODO(abarth): We shouldn't reach out to the WidgetsBinding.instance
_SemanticsDebuggerListener.instance.addListener(_update); // static here because we might not be in a tree that's attached to that
// binding. Instead, we should find a way to get to the PipelineOwner from
// the BuildContext.
WidgetsBinding.instance.ensureSemantics();
_client = new _SemanticsClient(WidgetsBinding.instance.pipelineOwner.semanticsOwner)
..addListener(_update);
} }
@override @override
void dispose() { void dispose() {
_SemanticsDebuggerListener.instance.removeListener(_update); _client.removeListener(_update);
super.dispose(); super.dispose();
} }
...@@ -58,21 +66,21 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> { ...@@ -58,21 +66,21 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
void _handleTap() { void _handleTap() {
assert(_lastPointerDownLocation != null); assert(_lastPointerDownLocation != null);
_SemanticsDebuggerListener.instance._performAction(_lastPointerDownLocation, SemanticAction.tap); _client._performAction(_lastPointerDownLocation, SemanticAction.tap);
setState(() { setState(() {
_lastPointerDownLocation = null; _lastPointerDownLocation = null;
}); });
} }
void _handleLongPress() { void _handleLongPress() {
assert(_lastPointerDownLocation != null); assert(_lastPointerDownLocation != null);
_SemanticsDebuggerListener.instance._performAction(_lastPointerDownLocation, SemanticAction.longPress); _client._performAction(_lastPointerDownLocation, SemanticAction.longPress);
setState(() { setState(() {
_lastPointerDownLocation = null; _lastPointerDownLocation = null;
}); });
} }
void _handlePanEnd(DragEndDetails details) { void _handlePanEnd(DragEndDetails details) {
assert(_lastPointerDownLocation != null); assert(_lastPointerDownLocation != null);
_SemanticsDebuggerListener.instance.handlePanEnd(_lastPointerDownLocation, details.velocity); _client.handlePanEnd(_lastPointerDownLocation, details.velocity);
setState(() { setState(() {
_lastPointerDownLocation = null; _lastPointerDownLocation = null;
}); });
...@@ -81,7 +89,7 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> { ...@@ -81,7 +89,7 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new CustomPaint( return new CustomPaint(
foregroundPainter: new _SemanticsDebuggerPainter(_SemanticsDebuggerListener.instance.generation, _lastPointerDownLocation), foregroundPainter: new _SemanticsDebuggerPainter(_client.generation, _client, _lastPointerDownLocation),
child: new GestureDetector( child: new GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: _handleTap, onTap: _handleTap,
...@@ -196,6 +204,11 @@ class _SemanticsDebuggerEntry { ...@@ -196,6 +204,11 @@ class _SemanticsDebuggerEntry {
|| actions.contains(SemanticAction.scrollDown); || actions.contains(SemanticAction.scrollDown);
} }
bool get _isAdjustable {
return actions.contains(SemanticAction.increase)
|| actions.contains(SemanticAction.decrease);
}
TextPainter textPainter; TextPainter textPainter;
void _updateMessage() { void _updateMessage() {
List<String> annotations = <String>[]; List<String> annotations = <String>[];
...@@ -215,6 +228,8 @@ class _SemanticsDebuggerEntry { ...@@ -215,6 +228,8 @@ class _SemanticsDebuggerEntry {
annotations.add('long-pressable'); annotations.add('long-pressable');
if (_isScrollable) if (_isScrollable)
annotations.add('scrollable'); annotations.add('scrollable');
if (_isAdjustable)
annotations.add('adjustable');
String message; String message;
if (annotations.isEmpty) { if (annotations.isEmpty) {
assert(label != null); assert(label != null);
...@@ -295,16 +310,12 @@ class _SemanticsDebuggerEntry { ...@@ -295,16 +310,12 @@ class _SemanticsDebuggerEntry {
} }
} }
class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.SemanticsListener { class _SemanticsClient extends ChangeNotifier implements mojom.SemanticsListener {
_SemanticsDebuggerListener._() { _SemanticsClient(this.semanticsOwner) {
SemanticsNode.addListener(this); semanticsOwner.addListener(this);
} }
static _SemanticsDebuggerListener instance; final SemanticsOwner semanticsOwner;
static final SemanticsServer _server = new SemanticsServer();
static void ensureInstantiated() {
instance ??= new _SemanticsDebuggerListener._();
}
_SemanticsDebuggerEntry get rootNode => _nodes[0]; _SemanticsDebuggerEntry get rootNode => _nodes[0];
final Map<int, _SemanticsDebuggerEntry> _nodes = <int, _SemanticsDebuggerEntry>{}; final Map<int, _SemanticsDebuggerEntry> _nodes = <int, _SemanticsDebuggerEntry>{};
...@@ -357,7 +368,7 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti ...@@ -357,7 +368,7 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti
void _performAction(Point position, SemanticAction action) { void _performAction(Point position, SemanticAction action) {
_SemanticsDebuggerEntry entry = _hitTest(position, (_SemanticsDebuggerEntry entry) => entry.actions.contains(action)); _SemanticsDebuggerEntry entry = _hitTest(position, (_SemanticsDebuggerEntry entry) => entry.actions.contains(action));
_server.performAction(entry?.id ?? 0, mojom.SemanticAction.values[action.index]); semanticsOwner.performAction(entry?.id ?? 0, action);
} }
void handlePanEnd(Point position, Velocity velocity) { void handlePanEnd(Point position, Velocity velocity) {
...@@ -366,10 +377,13 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti ...@@ -366,10 +377,13 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti
if (vx.abs() == vy.abs()) if (vx.abs() == vy.abs())
return; return;
if (vx.abs() > vy.abs()) { if (vx.abs() > vy.abs()) {
if (vx.sign < 0) if (vx.sign < 0) {
_performAction(position, SemanticAction.decrease);
_performAction(position, SemanticAction.scrollLeft); _performAction(position, SemanticAction.scrollLeft);
else } else {
_performAction(position, SemanticAction.increase);
_performAction(position, SemanticAction.scrollRight); _performAction(position, SemanticAction.scrollRight);
}
} else { } else {
if (vy.sign < 0) if (vy.sign < 0)
_performAction(position, SemanticAction.scrollUp); _performAction(position, SemanticAction.scrollUp);
...@@ -380,14 +394,15 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti ...@@ -380,14 +394,15 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti
} }
class _SemanticsDebuggerPainter extends CustomPainter { class _SemanticsDebuggerPainter extends CustomPainter {
const _SemanticsDebuggerPainter(this.generation, this.pointerPosition); const _SemanticsDebuggerPainter(this.generation, this.client, this.pointerPosition);
final int generation; final int generation;
final _SemanticsClient client;
final Point pointerPosition; final Point pointerPosition;
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
_SemanticsDebuggerEntry rootNode = _SemanticsDebuggerListener.instance.rootNode; _SemanticsDebuggerEntry rootNode = client.rootNode;
rootNode?.paint(canvas, rootNode.findDepth()); rootNode?.paint(canvas, rootNode.findDepth());
if (pointerPosition != null) { if (pointerPosition != null) {
Paint paint = new Paint(); Paint paint = new Paint();
...@@ -399,6 +414,7 @@ class _SemanticsDebuggerPainter extends CustomPainter { ...@@ -399,6 +414,7 @@ class _SemanticsDebuggerPainter extends CustomPainter {
@override @override
bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) { bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) {
return generation != oldDelegate.generation return generation != oldDelegate.generation
|| client != oldDelegate.client
|| pointerPosition != oldDelegate.pointerPosition; || pointerPosition != oldDelegate.pointerPosition;
} }
} }
...@@ -394,7 +394,7 @@ void main() { ...@@ -394,7 +394,7 @@ void main() {
}); });
testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async { testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener(); TestSemanticsListener client = new TestSemanticsListener(tester);
GlobalKey key = new GlobalKey(); GlobalKey key = new GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
new Overlay( new Overlay(
......
...@@ -136,9 +136,10 @@ void main() { ...@@ -136,9 +136,10 @@ void main() {
test('objects can be detached and re-attached: semantics', () { test('objects can be detached and re-attached: semantics', () {
TestTree testTree = new TestTree(); TestTree testTree = new TestTree();
TestSemanticsListener listener = new TestSemanticsListener(); TestSemanticsListener listener = new TestSemanticsListener();
SemanticsNode.addListener(listener); renderer.ensureSemantics();
renderer.pipelineOwner.semanticsOwner.addListener(listener);
// Lay out, composite, paint, and update semantics // Lay out, composite, paint, and update semantics
layout(testTree.root, phase: EnginePhase.sendSemanticsTree); layout(testTree.root, phase: EnginePhase.flushSemantics);
expect(listener.updates.length, equals(1)); expect(listener.updates.length, equals(1));
// Remove testTree from the custom render view // Remove testTree from the custom render view
renderer.renderView.child = null; renderer.renderView.child = null;
...@@ -148,7 +149,7 @@ void main() { ...@@ -148,7 +149,7 @@ void main() {
testTree.child.markNeedsSemanticsUpdate(); testTree.child.markNeedsSemanticsUpdate();
expect(listener.updates.length, equals(0)); expect(listener.updates.length, equals(0));
// Lay out, composite, paint, and update semantics again // Lay out, composite, paint, and update semantics again
layout(testTree.root, phase: EnginePhase.sendSemanticsTree); layout(testTree.root, phase: EnginePhase.flushSemantics);
expect(listener.updates.length, equals(1)); expect(listener.updates.length, equals(1));
}); });
} }
...@@ -12,8 +12,7 @@ enum EnginePhase { ...@@ -12,8 +12,7 @@ enum EnginePhase {
compositingBits, compositingBits,
paint, paint,
composite, composite,
flushSemantics, flushSemantics
sendSemanticsTree
} }
class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, ServicesBinding, RendererBinding, GestureBinding { class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, ServicesBinding, RendererBinding, GestureBinding {
...@@ -33,24 +32,21 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser ...@@ -33,24 +32,21 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
renderView.compositeFrame(); renderView.compositeFrame();
if (phase == EnginePhase.composite) if (phase == EnginePhase.composite)
return; return;
if (SemanticsNode.hasListeners) { pipelineOwner.flushSemantics();
pipelineOwner.flushSemantics(); assert(phase == EnginePhase.flushSemantics);
if (phase == EnginePhase.flushSemantics)
return;
SemanticsNode.sendSemanticsTree();
}
} }
} }
TestRenderingFlutterBinding _renderer; TestRenderingFlutterBinding _renderer;
TestRenderingFlutterBinding get renderer => _renderer; TestRenderingFlutterBinding get renderer {
_renderer ??= new TestRenderingFlutterBinding();
return _renderer;
}
void layout(RenderBox box, { BoxConstraints constraints, EnginePhase phase: EnginePhase.layout }) { void layout(RenderBox box, { BoxConstraints constraints, EnginePhase phase: EnginePhase.layout }) {
assert(box != null); // If you want to just repump the last box, call pumpFrame(). assert(box != null); // If you want to just repump the last box, call pumpFrame().
assert(box.parent == null); // We stick the box in another, so you can't reuse it easily, sorry. assert(box.parent == null); // We stick the box in another, so you can't reuse it easily, sorry.
_renderer ??= new TestRenderingFlutterBinding();
renderer.renderView.child = null; renderer.renderView.child = null;
if (constraints != null) { if (constraints != null) {
box = new RenderPositionedBox( box = new RenderPositionedBox(
......
...@@ -11,7 +11,7 @@ import 'test_semantics.dart'; ...@@ -11,7 +11,7 @@ import 'test_semantics.dart';
void main() { void main() {
testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async { testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener(); TestSemanticsListener client = new TestSemanticsListener(tester);
await tester.pumpWidget( await tester.pumpWidget(
new Material( new Material(
child: new Center( child: new Center(
......
...@@ -43,10 +43,7 @@ class OffscreenWidgetTree { ...@@ -43,10 +43,7 @@ class OffscreenWidgetTree {
pipelineOwner.flushCompositingBits(); pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint(); pipelineOwner.flushPaint();
renderView.compositeFrame(); renderView.compositeFrame();
if (SemanticsNode.hasListeners) { pipelineOwner.flushSemantics();
pipelineOwner.flushSemantics();
SemanticsNode.sendSemanticsTree();
}
buildOwner.finalizeTree(); buildOwner.finalizeTree();
} }
......
...@@ -11,7 +11,7 @@ import 'test_semantics.dart'; ...@@ -11,7 +11,7 @@ import 'test_semantics.dart';
void main() { void main() {
testWidgets('Semantics 1', (WidgetTester tester) async { testWidgets('Semantics 1', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener(); TestSemanticsListener client = new TestSemanticsListener(tester);
// smoketest // smoketest
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -11,7 +11,7 @@ import 'test_semantics.dart'; ...@@ -11,7 +11,7 @@ import 'test_semantics.dart';
void main() { void main() {
testWidgets('Semantics 2', (WidgetTester tester) async { testWidgets('Semantics 2', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener(); TestSemanticsListener client = new TestSemanticsListener(tester);
// this test is the same as the test in Semantics 1, but // this test is the same as the test in Semantics 1, but
// starting with the second branch being ignored and then // starting with the second branch being ignored and then
......
...@@ -10,7 +10,7 @@ import 'test_semantics.dart'; ...@@ -10,7 +10,7 @@ import 'test_semantics.dart';
void main() { void main() {
testWidgets('Semantics 3', (WidgetTester tester) async { testWidgets('Semantics 3', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener(); TestSemanticsListener client = new TestSemanticsListener(tester);
// implicit annotators // implicit annotators
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -10,7 +10,7 @@ import 'test_semantics.dart'; ...@@ -10,7 +10,7 @@ import 'test_semantics.dart';
void main() { void main() {
testWidgets('Semantics 4', (WidgetTester tester) async { testWidgets('Semantics 4', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener(); TestSemanticsListener client = new TestSemanticsListener(tester);
// O // O
// / \ O=root // / \ O=root
......
...@@ -10,7 +10,7 @@ import 'test_semantics.dart'; ...@@ -10,7 +10,7 @@ import 'test_semantics.dart';
void main() { void main() {
testWidgets('Semantics 5', (WidgetTester tester) async { testWidgets('Semantics 5', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener(); TestSemanticsListener client = new TestSemanticsListener(tester);
await tester.pumpWidget( await tester.pumpWidget(
new Stack( new Stack(
......
...@@ -11,7 +11,7 @@ import 'package:sky_services/semantics/semantics.mojom.dart' as mojom; ...@@ -11,7 +11,7 @@ import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
void main() { void main() {
testWidgets('Semantics 7 - Merging', (WidgetTester tester) async { testWidgets('Semantics 7 - Merging', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener(); TestSemanticsListener client = new TestSemanticsListener(tester);
String label; String label;
......
...@@ -10,7 +10,7 @@ import 'test_semantics.dart'; ...@@ -10,7 +10,7 @@ import 'test_semantics.dart';
void main() { void main() {
testWidgets('Semantics 8 - Merging with reset', (WidgetTester tester) async { testWidgets('Semantics 8 - Merging with reset', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener(); TestSemanticsListener client = new TestSemanticsListener(tester);
await tester.pumpWidget( await tester.pumpWidget(
new MergeSemantics( new MergeSemantics(
......
...@@ -2,12 +2,13 @@ ...@@ -2,12 +2,13 @@
// 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 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom; import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
class TestSemanticsListener implements mojom.SemanticsListener { class TestSemanticsListener implements mojom.SemanticsListener {
TestSemanticsListener() { TestSemanticsListener(WidgetTester tester) {
SemanticsNode.addListener(this); tester.binding.ensureSemantics();
tester.binding.pipelineOwner.semanticsOwner.addListener(this);
} }
final List<mojom.SemanticsNode> updates = <mojom.SemanticsNode>[]; final List<mojom.SemanticsNode> updates = <mojom.SemanticsNode>[];
......
...@@ -463,12 +463,9 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -463,12 +463,9 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
renderView.compositeFrame(); // this sends the bits to the GPU renderView.compositeFrame(); // this sends the bits to the GPU
if (_phase == EnginePhase.composite) if (_phase == EnginePhase.composite)
return; return;
if (SemanticsNode.hasListeners) { pipelineOwner.flushSemantics();
pipelineOwner.flushSemantics(); if (_phase == EnginePhase.flushSemantics)
if (_phase == EnginePhase.flushSemantics) return;
return;
SemanticsNode.sendSemanticsTree();
}
buildOwner.finalizeTree(); buildOwner.finalizeTree();
} }
......
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