Merge pull request #202 from vlidholt/master

Fixes issues with invalidating matrix for SpriteBox & adds HUD to demo game
diff --git a/packages/flutter/README.md b/packages/flutter/README.md
index a70daae..fddf171 100644
--- a/packages/flutter/README.md
+++ b/packages/flutter/README.md
@@ -3,7 +3,9 @@
 
 Sky apps are written in Dart. To get started, we need to set up Dart SDK:
 
- - Install the [Dart SDK](https://www.dartlang.org/downloads/).
+ - Install the [Dart SDK](https://www.dartlang.org/downloads/):
+   - Mac: `brew tap dart-lang/dart && brew install dart`
+   - Linux: See [https://www.dartlang.org/downloads/linux.html](https://www.dartlang.org/downloads/linux.html)
  - Ensure that `$DART_SDK` is set to the path of your Dart SDK and that the
    `dart` and `pub` executables are on your `$PATH`.
 
@@ -44,20 +46,20 @@
 string and centers it on the screen using a `Center` widget. To learn more about
 the widget system, please see the [widgets tutorial](lib/widgets/README.md).
 
-Setup your Android device
+Setting up your Android device
 -------------------------
 
 Currently Sky requires an Android device running the Lollipop (or newer) version
 of the Android operating system.
 
- - Install the `adb` tool from the [Android SDK](https://developer.android.com/sdk/installing/index.html)
-   and ensure that `adb` (inside `platform-tools` in the Android SDK) is in your
-   `$PATH`.
+ - Install the `adb` tool from the [Android SDK](https://developer.android.com/sdk/installing/index.html?pkg=tools):
+  - Mac: `brew install android-platform-tools`
+  - Linux: `sudo apt-get install android-tools-adb`
 
  - Enable developer mode on your device by visiting `Settings > About phone`
    and tapping the `Build number` field five times.
 
- - Enable `USB debugging` in `Settings > Developer options`.
+ - Enable `Android debugging` in `Settings > Developer options`.
 
  - Using a USB cable, plug your phone into your computer. If prompted on your
    device, authorize your computer to access your device.
diff --git a/packages/flutter/example/mine_digger/lib/main.dart b/packages/flutter/example/mine_digger/lib/main.dart
index 0db72e2..3219837 100644
--- a/packages/flutter/example/mine_digger/lib/main.dart
+++ b/packages/flutter/example/mine_digger/lib/main.dart
@@ -9,125 +9,152 @@
 import 'package:sky/rendering/flex.dart';
 import 'package:sky/theme/colors.dart' as colors;
 import 'package:sky/widgets/basic.dart';
+import 'package:sky/widgets/widget.dart';
 import 'package:sky/widgets/scaffold.dart';
 import 'package:sky/widgets/task_description.dart';
 import 'package:sky/widgets/theme.dart';
 import 'package:sky/widgets/tool_bar.dart';
 
-// Classic minesweeper-inspired game. The mouse controls are standard except
-// for left + right combo which is not implemented. For touch, the duration of
-// the pointer determines probing versus flagging.
+// Classic minesweeper-inspired game. The mouse controls are standard
+// except for left + right combo which is not implemented. For touch,
+// the duration of the pointer determines probing versus flagging.
 //
-// There are only 3 classes to understand. Game, which is contains all the
-// logic and two UI classes: CoveredMineNode and ExposedMineNode, none of them
-// holding state.
+// There are only 3 classes to understand. MineDiggerApp, which is
+// contains all the logic and two classes that describe the mines:
+// CoveredMineNode and ExposedMineNode, none of them holding state.
 
-class Game {
+// Colors for each mine count (0-8):
+const List<TextStyle> textStyles = const <TextStyle>[
+  const TextStyle(color: const Color(0xFF555555), fontWeight: bold),
+  const TextStyle(color: const Color(0xFF0094FF), fontWeight: bold), // blue
+  const TextStyle(color: const Color(0xFF13A023), fontWeight: bold), // green
+  const TextStyle(color: const Color(0xFFDA1414), fontWeight: bold), // red
+  const TextStyle(color: const Color(0xFF1E2347), fontWeight: bold), // black
+  const TextStyle(color: const Color(0xFF7F0037), fontWeight: bold), // dark red
+  const TextStyle(color: const Color(0xFF000000), fontWeight: bold),
+  const TextStyle(color: const Color(0xFF000000), fontWeight: bold),
+  const TextStyle(color: const Color(0xFF000000), fontWeight: bold),
+];
+
+enum CellState { covered, exploded, cleared, flagged, shown }
+
+class MineDiggerApp extends App {
+
+  void initState() {
+    resetGame();
+  }
+
   static const int rows = 9;
   static const int cols = 9;
   static const int totalMineCount = 11;
 
-  static const int coveredCell = 0;
-  static const int explodedCell = 1;
-  static const int clearedCell = 2;
-  static const int flaggedCell = 3;
-  static const int shownCell = 4;
-
-  static final List<TextStyle> textStyles = new List<TextStyle>();
-
-  final App app;
-
   bool alive;
   bool hasWon;
   int detectedCount;
-  int randomSeed;
 
   // |cells| keeps track of the positions of the mines.
   List<List<bool>> cells;
   // |uiState| keeps track of the visible player progess.
-  List<List<int>> uiState;
+  List<List<CellState>> uiState;
 
-  Game(this.app) {
-    randomSeed = 22;
-    // Colors for each mine count:
-    // 0 - none, 1 - blue, 2-green, 3-red, 4-black, 5-dark red .. etc.
-    textStyles.add(
-      new TextStyle(color: const Color(0xFF555555), fontWeight: bold));
-    textStyles.add(
-      new TextStyle(color: const Color(0xFF0094FF), fontWeight: bold));
-    textStyles.add(
-      new TextStyle(color: const Color(0xFF13A023), fontWeight: bold));
-    textStyles.add(
-      new TextStyle(color: const Color(0xFFDA1414), fontWeight: bold));
-    textStyles.add(
-      new TextStyle(color: const Color(0xFF1E2347), fontWeight: bold));
-    textStyles.add(
-      new TextStyle(color: const Color(0xFF7F0037), fontWeight: bold));
-    textStyles.add(
-      new TextStyle(color: const Color(0xFFE93BE9), fontWeight: bold));
-    initialize();
-  }
-
-  void initialize() {
+  void resetGame() {
     alive = true;
     hasWon = false;
     detectedCount = 0;
     // Build the arrays.
     cells = new List<List<bool>>();
-    uiState = new List<List<int>>();
+    uiState = new List<List<CellState>>();
     for (int iy = 0; iy != rows; iy++) {
       cells.add(new List<bool>());
-      uiState.add(new List<int>());
+      uiState.add(new List<CellState>());
       for (int ix = 0; ix != cols; ix++) {
         cells[iy].add(false);
-        uiState[iy].add(coveredCell);
+        uiState[iy].add(CellState.covered);
       }
     }
     // Place the mines.
-    Random random = new Random(++randomSeed);
-    for (int mc = 0; mc != totalMineCount; mc++) {
-      int rx = random.nextInt(rows);
-      int ry = random.nextInt(cols);
-      if (cells[ry][rx]) {
-        // Mine already there. Try again.
-        --mc;
-      } else {
-        cells[ry][rx] = true;
+    Random random = new Random();
+    int cellsRemaining = rows * cols;
+    int minesRemaining = totalMineCount;
+    for (int x = 0; x < cols; x += 1) {
+      for (int y = 0; y < rows; y += 1) {
+        if (random.nextInt(cellsRemaining) < minesRemaining) {
+          cells[y][x] = true;
+          minesRemaining -= 1;
+          if (minesRemaining <= 0)
+            return;
+        }
+        cellsRemaining -= 1;
       }
     }
+    assert(false);
+  }
+
+  Stopwatch longPressStopwatch;
+
+  PointerEventListener _pointerDownHandlerFor(int posX, int posY) {
+    return (sky.PointerEvent event) {
+      if (event.buttons == 1) {
+        probe(posX, posY);
+      } else if (event.buttons == 2) {
+        flag(posX, posY);
+      } else {
+        // Touch event.
+        longPressStopwatch = new Stopwatch()..start();
+      }
+    };
+  }
+
+  PointerEventListener _pointerUpHandlerFor(int posX, int posY) {
+    return (sky.PointerEvent event) {
+      if (longPressStopwatch == null)
+        return;
+      // Pointer down was a touch event.
+      if (longPressStopwatch.elapsedMilliseconds < 250) {
+        probe(posX, posY);
+      } else {
+        // Long press flags.
+        flag(posX, posY);
+      }
+      longPressStopwatch = null;
+    };
   }
 
   Widget buildBoard() {
     bool hasCoveredCell = false;
-    List<Flex> flexRows = new List<Flex>();
+    List<Flex> flexRows = <Flex>[];
     for (int iy = 0; iy != 9; iy++) {
-      List<Component> row = new List<Component>();
+      List<Widget> row = <Widget>[];
       for (int ix = 0; ix != 9; ix++) {
-        int state = uiState[iy][ix];
+        CellState state = uiState[iy][ix];
         int count = mineCount(ix, iy);
-
         if (!alive) {
-          if (state != explodedCell)
-            state = cells[iy][ix] ? shownCell : state;
+          if (state != CellState.exploded)
+            state = cells[iy][ix] ? CellState.shown : state;
         }
-
-        if (state == coveredCell) {
+        if (state == CellState.covered) {
+          row.add(new Listener(
+            onPointerDown: _pointerDownHandlerFor(ix, iy),
+            onPointerUp: _pointerUpHandlerFor(ix, iy),
+            child: new CoveredMineNode(
+              flagged: false,
+              posX: ix,
+              posY: iy
+            )
+          ));
+          // Mutating |hasCoveredCell| here is hacky, but convenient, same
+          // goes for mutating |hasWon| below.
+          hasCoveredCell = true;
+        } else if (state == CellState.flagged) {
           row.add(new CoveredMineNode(
-            this,
-            flagged: false,
-            posX: ix, posY: iy));
-            // Mutating |hasCoveredCell| here is hacky, but convenient, same
-            // goes for mutating |hasWon| below.
-            hasCoveredCell = true;
-        } else if (state == flaggedCell) {
-          row.add(new CoveredMineNode(
-            this,
             flagged: true,
-            posX: ix, posY: iy));
+            posX: ix, posY: iy)
+          );
         } else {
           row.add(new ExposedMineNode(
             state: state,
-            count: count));
+            count: count)
+          );
         }
       }
       flexRows.add(
@@ -147,33 +174,32 @@
     }
 
     return new Container(
-      key: 'minefield',
       padding: new EdgeDims.all(10.0),
       margin: new EdgeDims.all(10.0),
       decoration: new BoxDecoration(backgroundColor: const Color(0xFF6B6B6B)),
       child: new Flex(
         flexRows,
-        direction: FlexDirection.vertical,
-        key: 'flxv'));
+        direction: FlexDirection.vertical
+      )
+    );
   }
 
   Widget buildToolBar() {
-    String banner = hasWon ?
+    String toolbarCaption = hasWon ?
       'Awesome!!' : alive ?
         'Mine Digger [$detectedCount-$totalMineCount]': 'Kaboom! [press here]';
 
     return new ToolBar(
       // FIXME: Strange to have the toolbar be tapable.
       center: new Listener(
-        onPointerDown: handleBannerPointerDown,
-        child: new Text(banner, style: Theme.of(this.app).text.title)
+        onPointerDown: handleToolbarPointerDown,
+        child: new Text(toolbarCaption, style: Theme.of(this).text.title)
       )
     );
   }
 
-  Widget buildUI() {
-    // FIXME: We need to build the board before we build the toolbar because
-    // we compute the win state during build step.
+  Widget build() {
+    // We build the board before we build the toolbar because we compute the win state during build step.
     Widget board = buildBoard();
     return new TaskDescription(
       label: 'Mine Digger',
@@ -187,39 +213,42 @@
     );
   }
 
-  void handleBannerPointerDown(sky.PointerEvent event) {
-    initialize();
-    app.scheduleBuild();
+  void handleToolbarPointerDown(sky.PointerEvent event) {
+    setState(() {
+      resetGame();
+    });
   }
 
   // User action. The user uncovers the cell which can cause losing the game.
   void probe(int x, int y) {
     if (!alive)
       return;
-    if (uiState[y][x] == flaggedCell)
+    if (uiState[y][x] == CellState.flagged)
       return;
-    // Allowed to probe.
-    if (cells[y][x]) {
-      // Probed on a mine --> dead!!
-      uiState[y][x] = explodedCell;
-      alive = false;
-    } else {
-      // No mine, uncover nearby if possible.
-      cull(x, y);
-    }
-    app.scheduleBuild();
+    setState(() {
+      // Allowed to probe.
+      if (cells[y][x]) {
+        // Probed on a mine --> dead!!
+        uiState[y][x] = CellState.exploded;
+        alive = false;
+      } else {
+        // No mine, uncover nearby if possible.
+        cull(x, y);
+      }
+    });
   }
 
   // User action. The user is sure a mine is at this location.
   void flag(int x, int y) {
-    if (uiState[y][x] == flaggedCell) {
-      uiState[y][x] = coveredCell;
-      --detectedCount;
-    } else {
-      uiState[y][x] = flaggedCell;
-      ++detectedCount;
-    }
-    app.scheduleBuild();
+    setState(() {
+      if (uiState[y][x] == CellState.flagged) {
+        uiState[y][x] = CellState.covered;
+        --detectedCount;
+      } else {
+        uiState[y][x] = CellState.flagged;
+        ++detectedCount;
+      }
+    });
   }
 
   // Recursively uncovers cells whose totalMineCount is zero.
@@ -229,9 +258,9 @@
     if ((y < 0) || (y > cols - 1))
       return;
 
-    if (uiState[y][x] == clearedCell)
+    if (uiState[y][x] == CellState.cleared)
       return;
-    uiState[y][x] = clearedCell;
+    uiState[y][x] = CellState.cleared;
 
     if (mineCount(x, y) > 0)
       return;
@@ -269,106 +298,70 @@
   }
 }
 
-Widget makeCell(Widget widget) {
+Widget buildCell(Widget child) {
   return new Container(
     padding: new EdgeDims.all(1.0),
     height: 27.0, width: 27.0,
     decoration: new BoxDecoration(backgroundColor: const Color(0xFFC0C0C0)),
     margin: new EdgeDims.all(2.0),
-    child: widget);
+    child: child
+  );
 }
 
-Widget makeInnerCell(Widget widget) {
+Widget buildInnerCell(Widget child) {
   return new Container(
     padding: new EdgeDims.all(1.0),
     margin: new EdgeDims.all(3.0),
     height: 17.0, width: 17.0,
-    child: widget);
+    child: child
+  );
 }
 
 class CoveredMineNode extends Component {
-  final Game game;
+
+  CoveredMineNode({ this.flagged, this.posX, this.posY });
+
   final bool flagged;
   final int posX;
   final int posY;
-  Stopwatch stopwatch;
-
-  CoveredMineNode(this.game, {this.flagged, this.posX, this.posY});
-
-  void _handlePointerDown(sky.PointerEvent event) {
-    if (event.buttons == 1) {
-      game.probe(posX, posY);
-    } else if (event.buttons == 2) {
-      game.flag(posX, posY);
-    } else {
-      // Touch event.
-      stopwatch = new Stopwatch()..start();
-    }
-  }
-
-  void _handlePointerUp(sky.PointerEvent event) {
-    if (stopwatch == null)
-      return;
-    // Pointer down was a touch event.
-    if (stopwatch.elapsedMilliseconds < 250) {
-      game.probe(posX, posY);
-    } else {
-      // Long press flags.
-      game.flag(posX, posY);
-    }
-    stopwatch = null;
-  }
 
   Widget build() {
-    Widget text = flagged ?
-      makeInnerCell(new StyledText(elements : [Game.textStyles[5], '\u2691'])) :
-      null;
+    Widget text;
+    if (flagged)
+      text = buildInnerCell(new StyledText(elements : [textStyles[5], '\u2691']));
 
     Container inner = new Container(
       margin: new EdgeDims.all(2.0),
       height: 17.0, width: 17.0,
       decoration: new BoxDecoration(backgroundColor: const Color(0xFFD9D9D9)),
-      child: text);
+      child: text
+    );
 
-    return makeCell(new Listener(
-      child: inner,
-      onPointerDown: _handlePointerDown,
-      onPointerUp: _handlePointerUp));
+    return buildCell(inner);
   }
 }
 
 class ExposedMineNode extends Component {
-  final int state;
-  final int count;
 
-  ExposedMineNode({this.state, this.count});
+  ExposedMineNode({ this.state, this.count });
+
+  final CellState state;
+  final int count;
 
   Widget build() {
     StyledText text;
-    if (state == Game.clearedCell) {
+    if (state == CellState.cleared) {
       // Uncovered cell with nearby mine count.
       if (count != 0)
-        text = new StyledText(elements : [Game.textStyles[count], '$count']);
+        text = new StyledText(elements : [textStyles[count], '$count']);
     } else {
       // Exploded mine or shown mine for 'game over'.
-      int color = state == Game.explodedCell ? 3 : 0;
-      text = new StyledText(elements : [Game.textStyles[color], '\u2600']);
+      int color = state == CellState.exploded ? 3 : 0;
+      text = new StyledText(elements : [textStyles[color], '\u2600']);
     }
-
-    return makeCell(makeInnerCell(text));
-  }
-}
-
-class MineDiggerApp extends App {
-  Game game;
-
-  MineDiggerApp() {
-    game = new Game(this);
+    return buildCell(buildInnerCell(text));
   }
 
-  Widget build() {
-    return game.buildUI();
-  }
 }
 
 void main() {
diff --git a/packages/flutter/example/raw/mutating-dom.dart b/packages/flutter/example/raw/mutating-dom.dart
index 0d27a3f..6de893c 100644
--- a/packages/flutter/example/raw/mutating-dom.dart
+++ b/packages/flutter/example/raw/mutating-dom.dart
@@ -48,9 +48,12 @@
       node = root;
     } else if (node != root && other != null && pickThis(0.1)) {
       report("insertBefore()");
-      node.insertBefore([other]);
+      try {
+        node.insertBefore([other]);
+      } catch (_) {
+      }
       break;
-    } else if (pickThis(0.001)) {
+    } else if (node != root && pickThis(0.001)) {
       report("remove()");
       node.remove();
     } else if (node is sky.Element) {
@@ -148,7 +151,7 @@
         break;
       }
     } else {
-      assert(node is sky.Text); //
+      assert(node is sky.Text);
       final sky.Text text = node;
       if (pickThis(0.1)) {
         report("appending a new text node (ASCII)");
diff --git a/packages/flutter/lib/sky_tool b/packages/flutter/lib/sky_tool
index e57159c..8152b8a 100755
--- a/packages/flutter/lib/sky_tool
+++ b/packages/flutter/lib/sky_tool
@@ -23,6 +23,7 @@
 OBSERVATORY_PORT = 8181
 APK_NAME = 'SkyDemo.apk'
 ANDROID_PACKAGE = "org.domokit.sky.demo"
+ANDROID_COMPONENT = '%s/%s.SkyDemoActivity' % (ANDROID_PACKAGE, ANDROID_PACKAGE)
 # FIXME: This assumes adb is in $PATH, we could look for ANDROID_HOME, etc?
 ADB_PATH = 'adb'
 # FIXME: Do we need to look in $DART_SDK?
@@ -111,7 +112,7 @@
 
 def _url_for_path(port, root, path):
     relative_path = os.path.relpath(path, root)
-    return 'sky://localhost:%s/%s' % (port, relative_path)
+    return 'http://localhost:%s/%s' % (port, relative_path)
 
 
 class StartSky(object):
@@ -193,13 +194,14 @@
         pids['remote_sky_server_port'] = sky_server_port
 
         # The load happens on the remote device, use the remote port.
-        sky_url = _url_for_path(pids['remote_sky_server_port'], sky_server_root,
+        url = _url_for_path(pids['remote_sky_server_port'], sky_server_root,
             main_dart)
 
         subprocess.check_call([ADB_PATH, 'shell',
             'am', 'start',
             '-a', 'android.intent.action.VIEW',
-            '-d', sky_url])
+            '-d', url,
+            ANDROID_COMPONENT])
 
 
 class StopSky(object):
diff --git a/packages/flutter/lib/widgets/widget.dart b/packages/flutter/lib/widgets/widget.dart
index 6f96239..cc9e396 100644
--- a/packages/flutter/lib/widgets/widget.dart
+++ b/packages/flutter/lib/widgets/widget.dart
@@ -585,7 +585,7 @@
     return super.syncChild(node, oldNode, slot);
   }
 
-  void setState(Function fn()) {
+  void setState(void fn()) {
     assert(!_disqualifiedFromEverAppearingAgain);
     fn();
     scheduleBuild();
diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml
index 7ef9c6b..4efba0c 100644
--- a/packages/flutter/pubspec.yaml
+++ b/packages/flutter/pubspec.yaml
@@ -11,4 +11,4 @@
   sdk: '>=1.8.0 <2.0.0'
 homepage: https://github.com/domokit/mojo/tree/master/sky
 name: sky
-version: 0.0.20
+version: 0.0.21