Merge pull request #2434 from johnmccutchan/service_protocol_discovery

Add ServiceProtocolDiscovery with tests
diff --git a/examples/layers/raw/canvas.dart b/examples/layers/raw/canvas.dart
index 7108aaa..bbf861c 100644
--- a/examples/layers/raw/canvas.dart
+++ b/examples/layers/raw/canvas.dart
@@ -69,18 +69,12 @@
 
 ui.Scene composite(ui.Picture picture, ui.Rect paintBounds) {
   final double devicePixelRatio = ui.window.devicePixelRatio;
-  ui.Rect sceneBounds = new ui.Rect.fromLTWH(
-    0.0,
-    0.0,
-    ui.window.size.width * devicePixelRatio,
-    ui.window.size.height * devicePixelRatio
-  );
   Float64List deviceTransform = new Float64List(16)
     ..[0] = devicePixelRatio
     ..[5] = devicePixelRatio
     ..[10] = 1.0
     ..[15] = 1.0;
-  ui.SceneBuilder sceneBuilder = new ui.SceneBuilder(sceneBounds)
+  ui.SceneBuilder sceneBuilder = new ui.SceneBuilder()
     ..pushTransform(deviceTransform)
     ..addPicture(ui.Offset.zero, picture)
     ..pop();
diff --git a/examples/layers/raw/hello_world.dart b/examples/layers/raw/hello_world.dart
index a9f1f3e..d624c28 100644
--- a/examples/layers/raw/hello_world.dart
+++ b/examples/layers/raw/hello_world.dart
@@ -28,7 +28,7 @@
   ));
   final ui.Picture picture = recorder.endRecording();
 
-  final ui.SceneBuilder sceneBuilder = new ui.SceneBuilder(physicalBounds)
+  final ui.SceneBuilder sceneBuilder = new ui.SceneBuilder()
     // TODO(abarth): We should be able to add a picture without pushing a
     // container layer first.
     ..pushClipRect(physicalBounds)
diff --git a/examples/layers/raw/spinning_square.dart b/examples/layers/raw/spinning_square.dart
index c4f464a..24b3e73 100644
--- a/examples/layers/raw/spinning_square.dart
+++ b/examples/layers/raw/spinning_square.dart
@@ -36,18 +36,12 @@
   // COMPOSITE
 
   final double devicePixelRatio = ui.window.devicePixelRatio;
-  ui.Rect sceneBounds = new ui.Rect.fromLTWH(
-    0.0,
-    0.0,
-    ui.window.size.width * devicePixelRatio,
-    ui.window.size.height * devicePixelRatio
-  );
   Float64List deviceTransform = new Float64List(16)
     ..[0] = devicePixelRatio
     ..[5] = devicePixelRatio
     ..[10] = 1.0
     ..[15] = 1.0;
-  ui.SceneBuilder sceneBuilder = new ui.SceneBuilder(sceneBounds)
+  ui.SceneBuilder sceneBuilder = new ui.SceneBuilder()
     ..pushTransform(deviceTransform)
     ..addPicture(ui.Offset.zero, picture)
     ..pop();
diff --git a/examples/layers/raw/text.dart b/examples/layers/raw/text.dart
index 24e90fb..d8e431f 100644
--- a/examples/layers/raw/text.dart
+++ b/examples/layers/raw/text.dart
@@ -28,18 +28,12 @@
 
 ui.Scene composite(ui.Picture picture, ui.Rect paintBounds) {
   final double devicePixelRatio = ui.window.devicePixelRatio;
-  ui.Rect sceneBounds = new ui.Rect.fromLTWH(
-    0.0,
-    0.0,
-    ui.window.size.width * devicePixelRatio,
-    ui.window.size.height * devicePixelRatio
-  );
   Float64List deviceTransform = new Float64List(16)
     ..[0] = devicePixelRatio
     ..[5] = devicePixelRatio
     ..[10] = 1.0
     ..[15] = 1.0;
-  ui.SceneBuilder sceneBuilder = new ui.SceneBuilder(sceneBounds)
+  ui.SceneBuilder sceneBuilder = new ui.SceneBuilder()
     ..pushTransform(deviceTransform)
     ..addPicture(ui.Offset.zero, picture)
     ..pop();
diff --git a/examples/layers/raw/touch_input.dart b/examples/layers/raw/touch_input.dart
index aca8938..b8569bf 100644
--- a/examples/layers/raw/touch_input.dart
+++ b/examples/layers/raw/touch_input.dart
@@ -48,12 +48,6 @@
   // pixels, which are then scalled by the device pixel ratio before being drawn
   // on the screen.
   final double devicePixelRatio = ui.window.devicePixelRatio;
-  ui.Rect sceneBounds = new ui.Rect.fromLTWH(
-      0.0,
-      0.0,
-      ui.window.size.width * devicePixelRatio,
-      ui.window.size.height * devicePixelRatio
-  );
 
   // This transform scales the x and y coordinates by the devicePixelRatio.
   Float64List deviceTransform = new Float64List(16)
@@ -66,7 +60,7 @@
   // transform that scale its children by the device pixel ratio. This transform
   // lets us paint in "logical" pixels which are converted to device pixels by
   // this scaling operation.
-  ui.SceneBuilder sceneBuilder = new ui.SceneBuilder(sceneBounds)
+  ui.SceneBuilder sceneBuilder = new ui.SceneBuilder()
     ..pushTransform(deviceTransform)
     ..addPicture(ui.Offset.zero, picture)
     ..pop();
diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart
index 2effaf7..c6701d4 100644
--- a/packages/flutter/lib/src/material/popup_menu.dart
+++ b/packages/flutter/lib/src/material/popup_menu.dart
@@ -28,7 +28,7 @@
 const double _kMenuWidthStep = 56.0;
 const double _kMenuScreenPadding = 8.0;
 
-abstract class PopupMenuEntry<T> extends StatelessComponent {
+abstract class PopupMenuEntry<T> extends StatefulComponent {
   PopupMenuEntry({ Key key }) : super(key: key);
 
   double get height;
@@ -41,7 +41,11 @@
 
   final double height;
 
-  Widget build(BuildContext context) => new Divider(height: height);
+  _PopupMenuDividerState createState() => new _PopupMenuDividerState();
+}
+
+class _PopupMenuDividerState extends State<PopupMenuDivider> {
+  Widget build(BuildContext context) => new Divider(height: config.height);
 }
 
 class PopupMenuItem<T> extends PopupMenuEntry<T> {
@@ -58,20 +62,31 @@
 
   double get height => _kMenuItemHeight;
 
+  _PopupMenuItemState<PopupMenuItem<T>> createState() => new _PopupMenuItemState<PopupMenuItem<T>>();
+}
+
+class _PopupMenuItemState<T extends PopupMenuItem> extends State<T> {
+  // Override this to put something else in the menu entry.
+  Widget buildChild() => config.child;
+
+  void onTap() {
+    Navigator.pop(context, config.value);
+  }
+
   Widget build(BuildContext context) {
     final ThemeData theme = Theme.of(context);
     TextStyle style = theme.text.subhead;
-    if (!enabled)
+    if (!config.enabled)
       style = style.copyWith(color: theme.disabledColor);
 
     Widget item = new DefaultTextStyle(
       style: style,
       child: new Baseline(
-        baseline: height - _kBaselineOffsetFromBottom,
-        child: child
+        baseline: config.height - _kBaselineOffsetFromBottom,
+        child: buildChild()
       )
     );
-    if (!enabled) {
+    if (!config.enabled) {
       final bool isDark = theme.brightness == ThemeBrightness.dark;
       item = new IconTheme(
         data: new IconThemeData(opacity: isDark ? 0.5 : 0.38),
@@ -79,11 +94,14 @@
       );
     }
 
-    return new MergeSemantics(
-      child: new Container(
-        height: height,
-        padding: const EdgeDims.symmetric(horizontal: _kMenuHorizontalPadding),
-        child: item
+    return new InkWell(
+      onTap: config.enabled ? onTap : null,
+      child: new MergeSemantics(
+        child: new Container(
+          height: config.height,
+          padding: const EdgeDims.symmetric(horizontal: _kMenuHorizontalPadding),
+          child: item
+        )
       )
     );
   }
@@ -93,19 +111,52 @@
   CheckedPopupMenuItem({
     Key key,
     T value,
-    checked: false,
+    this.checked: false,
     bool enabled: true,
     Widget child
   }) : super(
     key: key,
     value: value,
     enabled: enabled,
-    child: new ListItem(
-      enabled: enabled,
-      left: new Icon(icon: checked ? Icons.done : null),
-      primary: child
-    )
+    child: child
   );
+
+  final bool checked;
+
+  _CheckedPopupMenuItemState<T> createState() => new _CheckedPopupMenuItemState<T>();
+}
+
+class _CheckedPopupMenuItemState<T> extends _PopupMenuItemState<CheckedPopupMenuItem<T>> {
+  static const Duration _kFadeDuration = const Duration(milliseconds: 150);
+  AnimationController _controller;
+  Animation<double> get _opacity => _controller.view;
+
+  void initState() {
+    super.initState();
+    _controller = new AnimationController(duration: _kFadeDuration)
+      ..value = config.checked ? 1.0 : 0.0
+      ..addListener(() => setState(() { /* animation changed */ }));
+  }
+
+  void onTap() {
+    // This fades the checkmark in or out when tapped.
+    if (config.checked)
+      _controller.reverse();
+    else
+      _controller.forward();
+    super.onTap();
+  }
+
+  Widget buildChild() {
+    return new ListItem(
+      enabled: config.enabled,
+      left: new FadeTransition(
+        opacity: _opacity,
+        child: new Icon(icon: _controller.isDismissed ? null : Icons.done)
+      ),
+      primary: config.child
+    );
+  }
 }
 
 class _PopupMenu<T> extends StatelessComponent {
@@ -127,7 +178,6 @@
         parent: route.animation,
         curve: new Interval(start, end)
       );
-      final bool enabled = route.items[i].enabled;
       Widget item = route.items[i];
       if (route.initialValue != null && route.initialValue == route.items[i].value) {
         item = new Container(
@@ -137,10 +187,7 @@
       }
       children.add(new FadeTransition(
         opacity: opacity,
-        child: new InkWell(
-          onTap: enabled ? () { Navigator.pop(context, route.items[i].value); } : null,
-          child: item
-        )
+        child: item
       ));
     }
 
diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart
index 826caed..9a982ad 100644
--- a/packages/flutter/lib/src/rendering/view.dart
+++ b/packages/flutter/lib/src/rendering/view.dart
@@ -122,8 +122,7 @@
     try {
       final TransformLayer transformLayer = layer;
       transformLayer.transform = _logicalToDeviceTransform;
-      Rect bounds = Point.origin & (size * ui.window.devicePixelRatio);
-      ui.SceneBuilder builder = new ui.SceneBuilder(bounds);
+      ui.SceneBuilder builder = new ui.SceneBuilder();
       transformLayer.addToScene(builder, Offset.zero);
       assert(layer == transformLayer);
       ui.Scene scene = builder.build();