Merge pull request #1490 from Hixie/navigator-ensure-pop

Handle a route being dismissed before being popped
diff --git a/examples/address_book/lib/main.dart b/examples/address_book/lib/main.dart
index c12a7d2..2bca9be 100644
--- a/examples/address_book/lib/main.dart
+++ b/examples/address_book/lib/main.dart
@@ -101,7 +101,7 @@
     title: 'Address Book',
     theme: theme,
     routes: <String, RouteBuilder>{
-      '/': (NavigatorState navigator, Route route) => new AddressBookHome(navigator: navigator)
+      '/': (RouteArguments args) => new AddressBookHome(navigator: args.navigator)
     }
   ));
 }
diff --git a/examples/demo_launcher/lib/main.dart b/examples/demo_launcher/lib/main.dart
index 4fc089c..2bbef23 100644
--- a/examples/demo_launcher/lib/main.dart
+++ b/examples/demo_launcher/lib/main.dart
@@ -206,7 +206,7 @@
     title: 'Flutter Demos',
     theme: _theme,
     routes: {
-      '/': (NavigatorState navigator, Route route) => new DemoHome()
+      '/': (RouteArguments args) => new DemoHome()
     }
   ));
 }
diff --git a/examples/fitness/lib/main.dart b/examples/fitness/lib/main.dart
index 1f12e11..98f87db 100644
--- a/examples/fitness/lib/main.dart
+++ b/examples/fitness/lib/main.dart
@@ -92,8 +92,6 @@
 class FitnessAppState extends State<FitnessApp> {
   UserDataImpl _userData;
 
-  Map<String, RouteBuilder> _routes;
-
   void initState() {
     super.initState();
     loadFitnessData().then((UserData data) {
@@ -102,36 +100,6 @@
       print("Failed to load data: $e");
       setState(() => _userData = new UserDataImpl());
     });
-
-    _routes = {
-      '/': (NavigatorState navigator, Route route) {
-        return new FeedFragment(
-          navigator: navigator,
-          userData: _userData,
-          onItemCreated: _handleItemCreated,
-          onItemDeleted: _handleItemDeleted
-        );
-      },
-      '/meals/new': (navigator, route) {
-        return new MealFragment(
-          navigator: navigator,
-          onCreated: _handleItemCreated
-        );
-      },
-      '/measurements/new': (NavigatorState navigator, Route route) {
-        return new MeasurementFragment(
-          navigator: navigator,
-          onCreated: _handleItemCreated
-        );
-      },
-      '/settings': (navigator, route) {
-        return new SettingsFragment(
-          navigator: navigator,
-          userData: _userData,
-          updater: settingsUpdater
-        );
-      }
-    };
   }
 
   void _handleItemCreated(FitnessItem item) {
@@ -158,17 +126,43 @@
     });
   }
 
-  final ThemeData _theme = new ThemeData(
-    brightness: ThemeBrightness.light,
-    primarySwatch: Colors.indigo,
-    accentColor: Colors.pinkAccent[200]
-  );
-
   Widget build(BuildContext) {
     return new App(
-      theme: _theme,
+      theme: new ThemeData(
+        brightness: ThemeBrightness.light,
+        primarySwatch: Colors.indigo,
+        accentColor: Colors.pinkAccent[200]
+      ),
       title: 'Fitness',
-      routes: _routes
+      routes: {
+        '/': (RouteArguments args) {
+          return new FeedFragment(
+            navigator: args.navigator,
+            userData: _userData,
+            onItemCreated: _handleItemCreated,
+            onItemDeleted: _handleItemDeleted
+          );
+        },
+        '/meals/new': (RouteArguments args) {
+          return new MealFragment(
+            navigator: args.navigator,
+            onCreated: _handleItemCreated
+          );
+        },
+        '/measurements/new': (RouteArguments args) {
+          return new MeasurementFragment(
+            navigator: args.navigator,
+            onCreated: _handleItemCreated
+          );
+        },
+        '/settings': (RouteArguments args) {
+          return new SettingsFragment(
+            navigator: args.navigator,
+            userData: _userData,
+            updater: settingsUpdater
+          );
+        }
+      }
     );
   }
 }
diff --git a/examples/game/example_effect_line.dart b/examples/game/example_effect_line.dart
index 4a6e7cc..05323d0 100644
--- a/examples/game/example_effect_line.dart
+++ b/examples/game/example_effect_line.dart
@@ -62,7 +62,7 @@
     );
   }
 
-  Column _buildColumn(NavigatorState navigator, Route route) {
+  Column _buildColumn(RouteArguments args) {
     return new Column([
       new Flexible(child: _buildSpriteWidget()),
       _buildTabBar()
diff --git a/examples/game/lib/main.dart b/examples/game/lib/main.dart
index a93038d..2cb407a 100644
--- a/examples/game/lib/main.dart
+++ b/examples/game/lib/main.dart
@@ -92,11 +92,11 @@
     );
   }
 
-  Widget _buildGameScene(NavigatorState navigator, Route route) {
+  Widget _buildGameScene(RouteArguments args) {
     return new SpriteWidget(_game, SpriteBoxTransformMode.fixedWidth);
   }
 
-  Widget _buildMainScene(navigator, route) {
+  Widget _buildMainScene(RouteArguments args) {
     return new Stack([
       new SpriteWidget(new MainScreenBackground(), SpriteBoxTransformMode.fixedWidth),
       new Column([
@@ -109,10 +109,10 @@
                 _sounds,
                 (lastScore) {
                   setState(() {_lastScore = lastScore;});
-                  navigator.pop();
+                  args.navigator.pop();
                 }
               );
-              navigator.pushNamed('/game');
+              args.navigator.pushNamed('/game');
             },
             texture: _spriteSheetUI['btn_play_up.png'],
             textureDown: _spriteSheetUI['btn_play_down.png'],
diff --git a/examples/game/test_drawatlas.dart b/examples/game/test_drawatlas.dart
index 135334e..dba1748 100644
--- a/examples/game/test_drawatlas.dart
+++ b/examples/game/test_drawatlas.dart
@@ -36,7 +36,7 @@
     title: 'Test drawAtlas',
     theme: _theme,
     routes: {
-      '/': (NavigatorState navigator, Route route) {
+      '/': (RouteArguments args) {
         return new SpriteWidget(
           new TestDrawAtlas(),
           SpriteBoxTransformMode.fixedWidth
diff --git a/examples/game/test_physics.dart b/examples/game/test_physics.dart
index a7c23f9..aabb773 100644
--- a/examples/game/test_physics.dart
+++ b/examples/game/test_physics.dart
@@ -34,7 +34,7 @@
       primarySwatch: Colors.purple
     ),
     routes: {
-      '/': (navigator, route) {
+      '/': (RouteArguments args) {
         return new SpriteWidget(
           new TestBed(),
           SpriteBoxTransformMode.letterbox
@@ -83,6 +83,7 @@
 
   void myCallback(PhysicsContactType type, PhysicsContact contact) {
     print("CONTACT type: $type");
+    contact.nodeB.removeFromParent();
   }
 
   bool handleEvent(SpriteBoxEvent event) {
diff --git a/examples/stocks/lib/main.dart b/examples/stocks/lib/main.dart
index 4b2c1e8..a931d54 100644
--- a/examples/stocks/lib/main.dart
+++ b/examples/stocks/lib/main.dart
@@ -82,7 +82,7 @@
       if (path.length != 3)
         return null;
       if (_stocks.containsKey(path[2]))
-        return (navigator, route) => new StockSymbolViewer(navigator, _stocks[path[2]]);
+        return (RouteArguments args) => new StockSymbolViewer(args.navigator, _stocks[path[2]]);
       return null;
     }
     return null;
@@ -93,8 +93,8 @@
       title: 'Stocks',
       theme: theme,
       routes: <String, RouteBuilder>{
-         '/':         (navigator, route) => new StockHome(navigator, _stocks, _symbols, _optimismSetting, modeUpdater),
-         '/settings': (navigator, route) => new StockSettings(navigator, _optimismSetting, _backupSetting, settingsUpdater)
+         '/':         (RouteArguments args) => new StockHome(args.navigator, _stocks, _symbols, _optimismSetting, modeUpdater),
+         '/settings': (RouteArguments args) => new StockSettings(args.navigator, _optimismSetting, _backupSetting, settingsUpdater)
       },
       onGenerateRoute: _getRoute
     );
diff --git a/examples/widgets/card_collection.dart b/examples/widgets/card_collection.dart
index 0e7a06c..fb0458b 100644
--- a/examples/widgets/card_collection.dart
+++ b/examples/widgets/card_collection.dart
@@ -357,7 +357,7 @@
       accentColor: Colors.redAccent[200]
     ),
     routes: {
-      '/': (NavigatorState navigator, Route route) => new CardCollection(navigator: navigator),
+      '/': (RouteArguments args) => new CardCollection(navigator: args.navigator),
     }
   ));
 }
diff --git a/examples/widgets/drag_and_drop.dart b/examples/widgets/drag_and_drop.dart
index c7a6187..f35934c 100644
--- a/examples/widgets/drag_and_drop.dart
+++ b/examples/widgets/drag_and_drop.dart
@@ -133,7 +133,7 @@
   runApp(new App(
     title: 'Drag and Drop Flutter Demo',
     routes: {
-     '/': (NavigatorState navigator, Route route) => new DragAndDropApp(navigator: navigator)
+     '/': (RouteArguments args) => new DragAndDropApp(navigator: args.navigator)
     }
   ));
 }
diff --git a/examples/widgets/navigation.dart b/examples/widgets/navigation.dart
index c9dd69b..7ade991 100644
--- a/examples/widgets/navigation.dart
+++ b/examples/widgets/navigation.dart
@@ -6,46 +6,46 @@
 import 'package:sky/widgets.dart';
 
 final Map<String, RouteBuilder> routes = <String, RouteBuilder>{
-  '/': (NavigatorState navigator, Route route) => new Container(
+  '/': (RouteArguments args) => new Container(
     padding: const EdgeDims.all(30.0),
     decoration: new BoxDecoration(backgroundColor: const Color(0xFFCCCCCC)),
     child: new Column([
       new Text("You are at home"),
       new RaisedButton(
         child: new Text('GO SHOPPING'),
-        onPressed: () => navigator.pushNamed('/shopping')
+        onPressed: () => args.navigator.pushNamed('/shopping')
       ),
       new RaisedButton(
         child: new Text('START ADVENTURE'),
-        onPressed: () => navigator.pushNamed('/adventure')
+        onPressed: () => args.navigator.pushNamed('/adventure')
       )],
       justifyContent: FlexJustifyContent.center
     )
   ),
-  '/shopping': (NavigatorState navigator, Route route) => new Container(
+  '/shopping': (RouteArguments args) => new Container(
     padding: const EdgeDims.all(20.0),
     decoration: new BoxDecoration(backgroundColor: const Color(0xFFBF5FFF)),
     child: new Column([
       new Text("Village Shop"),
       new RaisedButton(
         child: new Text('RETURN HOME'),
-        onPressed: () => navigator.pop()
+        onPressed: () => args.navigator.pop()
       ),
       new RaisedButton(
         child: new Text('GO TO DUNGEON'),
-        onPressed: () => navigator.pushNamed('/adventure')
+        onPressed: () => args.navigator.pushNamed('/adventure')
       )],
       justifyContent: FlexJustifyContent.center
     )
   ),
-  '/adventure': (NavigatorState navigator, Route route) => new Container(
+  '/adventure': (RouteArguments args) => new Container(
     padding: const EdgeDims.all(20.0),
     decoration: new BoxDecoration(backgroundColor: const Color(0xFFDC143C)),
     child: new Column([
       new Text("Monster's Lair"),
       new RaisedButton(
         child: new Text('RUN!!!'),
-        onPressed: () => navigator.pop()
+        onPressed: () => args.navigator.pop()
       )],
       justifyContent: FlexJustifyContent.center
     )
diff --git a/examples/widgets/overlay_geometry.dart b/examples/widgets/overlay_geometry.dart
index 8a52520..6aa66e9 100644
--- a/examples/widgets/overlay_geometry.dart
+++ b/examples/widgets/overlay_geometry.dart
@@ -165,7 +165,7 @@
     ),
     title: 'Cards',
     routes: {
-      '/': (navigator, route) => new OverlayGeometryApp()
+      '/': (RouteArguments args) => new OverlayGeometryApp()
     }
   ));
 }
diff --git a/examples/widgets/pageable_list.dart b/examples/widgets/pageable_list.dart
index c00da4b..e68ebbc 100644
--- a/examples/widgets/pageable_list.dart
+++ b/examples/widgets/pageable_list.dart
@@ -165,7 +165,7 @@
       accentColor: Colors.redAccent[200]
     ),
     routes: {
-      '/': (NavigatorState navigator, Route route) => new PageableListApp(navigator: navigator),
+      '/': (RouteArguments args) => new PageableListApp(navigator: args.navigator),
     }
   ));
 }
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index d5dbb44..22f7d3e 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -712,6 +712,11 @@
   }
 
   bool updateShouldNotify(DefaultTextStyle old) => style != old.style;
+
+  void debugFillDescription(List<String> description) {
+    super.debugFillDescription(description);
+    '$style'.split('\n').forEach(description.add);
+  }
 }
 
 class Text extends StatelessComponent {
@@ -738,6 +743,13 @@
       text = new StyledTextSpan(combinedStyle, [text]);
     return new Paragraph(text: text);
   }
+
+  void debugFillDescription(List<String> description) {
+    super.debugFillDescription(description);
+    description.add('"$data"');
+    if (style != null)
+      '$style'.split('\n').forEach(description.add);
+  }
 }
 
 class Image extends LeafRenderObjectWidget {
diff --git a/packages/flutter/lib/src/widgets/dialog.dart b/packages/flutter/lib/src/widgets/dialog.dart
index 54c2a6f..4efe02b 100644
--- a/packages/flutter/lib/src/widgets/dialog.dart
+++ b/packages/flutter/lib/src/widgets/dialog.dart
@@ -145,7 +145,7 @@
     return new FadeTransition(
       performance: performance,
       opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
-      child: builder(navigator, this)
+      child: builder(new RouteArguments(navigator: navigator, previousPerformance: this.performance, nextPerformance: nextRoutePerformance))
     );
   }
 
@@ -159,11 +159,11 @@
   Completer completer = new Completer();
   navigator.push(new DialogRoute(
     completer: completer,
-    builder: (navigator, route) {
+    builder: (RouteArguments args) {
       return new Focus(
-        key: new GlobalObjectKey(route),
+        key: new GlobalObjectKey(completer),
         autofocus: true,
-        child: builder(navigator)
+        child: builder(args.navigator)
       );
     }
   ));
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index c0cce2b..05ce1e6 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -182,10 +182,15 @@
   Element createElement();
 
   String toString() {
-    if (key == null)
-      return '$runtimeType';
-    return '$runtimeType-$key';
-  }
+    final String name = key == null ? '$runtimeType' : '$runtimeType-$key';
+    final List<String> data = <String>[];
+    debugFillDescription(data);
+    if (data.isEmpty)
+      return 'name';
+    return 'name(${data.join("; ")})';
+   }
+
+  void debugFillDescription(List<String> description) { }
 }
 
 /// RenderObjectWidgets provide the configuration for [RenderObjectElement]s,
@@ -486,9 +491,11 @@
 typedef void ElementVisitor(Element element);
 
 abstract class BuildContext {
-  InheritedWidget inheritedWidgetOfType(Type targetType);
+  Widget get widget;
   RenderObject findRenderObject();
+  InheritedWidget inheritedWidgetOfType(Type targetType);
   void visitAncestorElements(bool visitor(Element element));
+  void visitChildElements(void visitor(Element element));
 }
 
 /// Elements are the instantiations of Widget configurations.
@@ -540,6 +547,13 @@
   /// Calls the argument for each child. Must be overridden by subclasses that support having children.
   void visitChildren(ElementVisitor visitor) { }
 
+  /// Wrapper around visitChildren for BuildContext.
+  void visitChildElements(void visitor(Element element)) {
+    // don't allow visitChildElements() during build, since children aren't necessarily built yet
+    assert(BuildableElement._debugStateLockLevel == 0);
+    visitChildren(visitor);
+  }
+
   bool detachChild(Element child) => false;
 
   /// This method is the core of the system.
@@ -786,6 +800,8 @@
       description.add('no depth');
     if (widget == null)
       description.add('no widget');
+    else
+      widget.debugFillDescription(description);
   }
 
   String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
@@ -952,11 +968,11 @@
 
 /// Instantiation of StatelessComponent widgets.
 class StatelessComponentElement<T extends StatelessComponent> extends BuildableElement<T> {
-  StatelessComponentElement(StatelessComponent widget) : super(widget) {
+  StatelessComponentElement(T widget) : super(widget) {
     _builder = widget.build;
   }
 
-  void update(StatelessComponent newWidget) {
+  void update(T newWidget) {
     super.update(newWidget);
     assert(widget == newWidget);
     _builder = widget.build;
@@ -966,10 +982,10 @@
 }
 
 /// Instantiation of StatefulComponent widgets.
-class StatefulComponentElement extends BuildableElement<StatefulComponent> {
-  StatefulComponentElement(StatefulComponent widget)
+class StatefulComponentElement<T extends StatefulComponent, U extends State<T>> extends BuildableElement<T> {
+  StatefulComponentElement(T widget)
     : _state = widget.createState(), super(widget) {
-    assert(_state._debugTypesAreRight(widget));
+    assert(_state._debugTypesAreRight(widget)); // can't use T and U, since normally we don't actually set those
     assert(_state._element == null);
     _state._element = this;
     assert(_builder == null);
@@ -992,10 +1008,10 @@
     assert(() { _state._debugLifecycleState = _StateLifecycle.ready; return true; });
   }
 
-  State get state => _state;
-  State _state;
+  U get state => _state;
+  U _state;
 
-  void update(StatefulComponent newWidget) {
+  void update(T newWidget) {
     super.update(newWidget);
     assert(widget == newWidget);
     StatefulComponent oldConfig = _state._config;
diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart
index 212b37f..d69d75a 100644
--- a/packages/flutter/lib/src/widgets/navigator.dart
+++ b/packages/flutter/lib/src/widgets/navigator.dart
@@ -8,7 +8,14 @@
 import 'package:sky/src/widgets/framework.dart';
 import 'package:sky/src/widgets/transitions.dart';
 
-typedef Widget RouteBuilder(NavigatorState navigator, Route route);
+class RouteArguments {
+  const RouteArguments({ this.navigator, this.previousPerformance, this.nextPerformance });
+  final NavigatorState navigator;
+  final PerformanceView previousPerformance;
+  final PerformanceView nextPerformance;
+}
+
+typedef Widget RouteBuilder(RouteArguments args);
 typedef RouteBuilder RouteGenerator(String name);
 typedef void StateRouteCallback(StateRoute route);
 typedef void NotificationCallback();
@@ -159,6 +166,7 @@
     }
     return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
   }
+
 }
 
 
@@ -282,7 +290,7 @@
       child: new FadeTransition(
         performance: performance,
         opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
-        child: builder(navigator, this)
+        child: builder(new RouteArguments(navigator: navigator, previousPerformance: this.performance, nextPerformance: nextRoutePerformance))
       )
     );
   }
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
index cf1693a..da4d940 100644
--- a/packages/flutter/lib/src/widgets/scrollable.dart
+++ b/packages/flutter/lib/src/widgets/scrollable.dart
@@ -318,7 +318,7 @@
     });
   }
   void _updateScrollBehaviour() {
-    // if you don't call this from build() or syncConstructorArguments(), you must call it from setState().
+    // if you don't call this from build(), you must call it from setState().
     scrollTo(scrollBehavior.updateExtents(
       contentExtent: _childSize,
       containerExtent: _viewportSize,
diff --git a/packages/flutter_sprites/lib/physics_body.dart b/packages/flutter_sprites/lib/physics_body.dart
index 99c8ea4..e9446b6 100644
--- a/packages/flutter_sprites/lib/physics_body.dart
+++ b/packages/flutter_sprites/lib/physics_body.dart
@@ -120,7 +120,7 @@
 
   void _detach() {
     if (_attached) {
-      _physicsNode.b2World.destroyBody(_body);
+      _physicsNode._bodiesScheduledForDestruction.add(_body);
       _attached = false;
     }
   }
diff --git a/packages/flutter_sprites/lib/physics_node.dart b/packages/flutter_sprites/lib/physics_node.dart
index bc19c6a..12ea4f8 100644
--- a/packages/flutter_sprites/lib/physics_node.dart
+++ b/packages/flutter_sprites/lib/physics_node.dart
@@ -31,6 +31,8 @@
 
   _ContactHandler _contactHandler;
 
+  List<box2d.Body> _bodiesScheduledForDestruction = [];
+
   double b2WorldToNodeConversionFactor = 500.0;
 
   Offset get gravity {
@@ -57,6 +59,9 @@
   }
 
   void _stepPhysics(double dt) {
+    // Remove bodies that were marked for destruction during the update phase
+    _removeBodiesScheduledForDestruction();
+
     // Calculate a step in the simulation
     b2World.stepDt(dt, 10, 10);
 
@@ -71,6 +76,16 @@
 
       body._node._setRotationFromPhysics(degrees(b2Body.getAngle()));
     }
+
+    // Remove bodies that were marked for destruction during the simulation
+    _removeBodiesScheduledForDestruction();
+  }
+
+  void _removeBodiesScheduledForDestruction() {
+    for (box2d.Body b2Body in _bodiesScheduledForDestruction) {
+      b2World.destroyBody(b2Body);
+    }
+    _bodiesScheduledForDestruction.clear();
   }
 
   void _updatePosition(PhysicsBody body, Point position) {
@@ -257,6 +272,11 @@
           b2Contact.isTouching(),
           b2Contact.isEnabled()
         );
+
+        if (type == PhysicsContactType.postSolve) {
+
+        }
+
         // Make callback
         info.callback(type, contact);
 
diff --git a/packages/unit/test/widget/draggable_test.dart b/packages/unit/test/widget/draggable_test.dart
index 31120bc..8e8b224 100644
--- a/packages/unit/test/widget/draggable_test.dart
+++ b/packages/unit/test/widget/draggable_test.dart
@@ -13,9 +13,9 @@
 
       tester.pumpWidget(new Navigator(
         routes: {
-          '/': (NavigatorState navigator, Route route) { return new Column([
+          '/': (RouteArguments args) { return new Column([
               new Draggable(
-                navigator: navigator,
+                navigator: args.navigator,
                 data: 1,
                 child: new Text('Source'),
                 feedback: new Text('Dragging')
diff --git a/packages/unit/test/widget/navigator_test.dart b/packages/unit/test/widget/navigator_test.dart
index 5da23d0..a9bfcd5 100644
--- a/packages/unit/test/widget/navigator_test.dart
+++ b/packages/unit/test/widget/navigator_test.dart
@@ -49,8 +49,8 @@
   test('Can navigator navigate to and from a stateful component', () {
     testWidgets((WidgetTester tester) {
       final Map<String, RouteBuilder> routes = <String, RouteBuilder>{
-        '/': (navigator, route) => new FirstComponent(navigator),
-        '/second': (navigator, route) => new SecondComponent(navigator),
+        '/': (RouteArguments args) => new FirstComponent(args.navigator),
+        '/second': (RouteArguments args) => new SecondComponent(args.navigator),
       };
 
       tester.pumpWidget(new Navigator(routes: routes));