Add support for drag-and-drop

Widgets that want to receive drops should include a DropTarget in their build.
Currently there's no widget for initiating a drag. Components can use the
DragController directly. In the future, we'll probably want to add a Draggable
that knows how to do some of this work automatically.

Fixes #612
diff --git a/examples/widgets/drag_and_drop.dart b/examples/widgets/drag_and_drop.dart
new file mode 100644
index 0000000..7c2f00f
--- /dev/null
+++ b/examples/widgets/drag_and_drop.dart
@@ -0,0 +1,151 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:sky' as sky;
+
+import 'package:sky/theme/colors.dart' as colors;
+import 'package:sky/widgets.dart';
+
+final double kTop = 10.0 + sky.view.paddingTop;
+final double kLeft = 10.0;
+
+class DragData {
+  DragData(this.text);
+
+  final String text;
+}
+
+class ExampleDragTarget extends StatefulComponent {
+  String _text = 'ready';
+
+  void syncFields(ExampleDragTarget source) {
+  }
+
+  void _handleAccept(DragData data) {
+    setState(() {
+      _text = data.text;
+    });
+  }
+
+  Widget build() {
+    return new DragTarget<DragData>(
+      onAccept: _handleAccept,
+      builder: (List<DragData> data, _) {
+        return new Container(
+          width: 100.0,
+          height: 100.0,
+          margin: new EdgeDims.all(10.0),
+          decoration: new BoxDecoration(
+            border: new Border.all(
+              width: 3.0,
+              color: data.isEmpty ? colors.white : colors.Blue[500]
+            ),
+            backgroundColor: data.isEmpty ? colors.Grey[500] : colors.Green[500]
+          ),
+          child: new Center(
+            child: new Text(_text)
+          )
+        );
+      }
+    );
+  }
+}
+
+class Dot extends Component {
+  Widget build() {
+    return new Container(
+      width: 50.0,
+      height: 50.0,
+      decoration: new BoxDecoration(
+        backgroundColor: colors.DeepOrange[500]
+      )
+    );
+  }
+}
+
+class DragAndDropApp extends App {
+  DragController _dragController;
+  Offset _displacement = Offset.zero;
+
+  EventDisposition _startDrag(sky.PointerEvent event) {
+    setState(() {
+      _dragController = new DragController(new DragData("Orange"));
+      _dragController.update(new Point(event.x, event.y));
+      _displacement = Offset.zero;
+    });
+    return EventDisposition.consumed;
+  }
+
+  EventDisposition _updateDrag(sky.PointerEvent event) {
+    setState(() {
+      _dragController.update(new Point(event.x, event.y));
+      _displacement += new Offset(event.dx, event.dy);
+    });
+    return EventDisposition.consumed;
+  }
+
+  EventDisposition _cancelDrag(sky.PointerEvent event) {
+    setState(() {
+      _dragController.cancel();
+      _dragController = null;
+    });
+    return EventDisposition.consumed;
+  }
+
+  EventDisposition _drop(sky.PointerEvent event) {
+    setState(() {
+      _dragController.update(new Point(event.x, event.y));
+      _dragController.drop();
+      _dragController = null;
+      _displacement = Offset.zero;
+    });
+    return EventDisposition.consumed;
+  }
+
+  Widget build() {
+    List<Widget> layers = <Widget>[
+      new Flex([
+        new ExampleDragTarget(),
+        new ExampleDragTarget(),
+        new ExampleDragTarget(),
+        new ExampleDragTarget(),
+      ]),
+      new Positioned(
+        top: kTop,
+        left: kLeft,
+        child: new Listener(
+          onPointerDown: _startDrag,
+          onPointerMove: _updateDrag,
+          onPointerCancel: _cancelDrag,
+          onPointerUp: _drop,
+          child: new Dot()
+        )
+      ),
+    ];
+
+    if (_dragController != null) {
+      layers.add(
+        new Positioned(
+          top: kTop + _displacement.dy,
+          left: kLeft + _displacement.dx,
+          child: new IgnorePointer(
+            child: new Opacity(
+              opacity: 0.5,
+              child: new Dot()
+            )
+          )
+        )
+      );
+    }
+
+    return new Container(
+      decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
+      child: new Stack(layers)
+    );
+  }
+}
+
+void main() {
+  runApp(new DragAndDropApp());
+}
diff --git a/sky/packages/sky/lib/rendering/sky_binding.dart b/sky/packages/sky/lib/rendering/sky_binding.dart
index b81c82c..d77e5a3 100644
--- a/sky/packages/sky/lib/rendering/sky_binding.dart
+++ b/sky/packages/sky/lib/rendering/sky_binding.dart
@@ -85,9 +85,7 @@
     if (event is sky.PointerEvent) {
       _handlePointerEvent(event);
     } else if (event is sky.GestureEvent) {
-      HitTestResult result = new HitTestResult();
-      _renderView.hitTest(result, position: new Point(event.x, event.y));
-      dispatchEvent(event, result);
+      dispatchEvent(event, hitTest(new Point(event.x, event.y)));
     } else {
       for (EventListener e in _eventListeners)
         e(event);
@@ -97,8 +95,7 @@
   Map<int, PointerState> _stateForPointer = new Map<int, PointerState>();
 
   PointerState _createStateForPointer(sky.PointerEvent event, Point position) {
-    HitTestResult result = new HitTestResult();
-    _renderView.hitTest(result, position: position);
+    HitTestResult result = hitTest(position);
     PointerState state = new PointerState(result: result, lastPosition: position);
     _stateForPointer[event.pointer] = state;
     return state;
@@ -128,6 +125,12 @@
     return dispatchEvent(event, state.result);
   }
 
+  HitTestResult hitTest(Point position) {
+    HitTestResult result = new HitTestResult();
+    _renderView.hitTest(result, position: position);
+    return result;
+  }
+
   EventDisposition dispatchEvent(sky.Event event, HitTestResult result) {
     assert(result != null);
     EventDisposition disposition = EventDisposition.ignored;
diff --git a/sky/packages/sky/lib/widgets.dart b/sky/packages/sky/lib/widgets.dart
index 65fb174..309caea 100644
--- a/sky/packages/sky/lib/widgets.dart
+++ b/sky/packages/sky/lib/widgets.dart
@@ -14,6 +14,7 @@
 export 'widgets/default_text_style.dart';
 export 'widgets/dialog.dart';
 export 'widgets/dismissable.dart';
+export 'widgets/drag_target.dart';
 export 'widgets/drawer.dart';
 export 'widgets/drawer_divider.dart';
 export 'widgets/drawer_header.dart';
diff --git a/sky/packages/sky/lib/widgets/drag_target.dart b/sky/packages/sky/lib/widgets/drag_target.dart
new file mode 100644
index 0000000..7f9402b
--- /dev/null
+++ b/sky/packages/sky/lib/widgets/drag_target.dart
@@ -0,0 +1,119 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:collection';
+
+import 'package:sky/base/hit_test.dart';
+import 'package:sky/rendering/sky_binding.dart';
+import 'package:sky/widgets/basic.dart';
+import 'package:sky/widgets/framework.dart';
+
+typedef bool DragTargetWillAccept<T>(T data);
+typedef void DragTargetAccept<T>(T data);
+typedef Widget DragTargetBuilder<T>(List<T> candidateData, List<dynamic> rejectedData);
+
+class DragTarget<T> extends StatefulComponent {
+  DragTarget({
+    Key key,
+    this.builder,
+    this.onWillAccept,
+    this.onAccept
+  }) : super(key: key);
+
+  DragTargetBuilder<T> builder;
+  DragTargetWillAccept<T> onWillAccept;
+  DragTargetAccept<T> onAccept;
+
+  final List<T> _candidateData = new List<T>();
+  final List<dynamic> _rejectedData = new List<dynamic>();
+
+  void syncFields(DragTarget source) {
+    builder = source.builder;
+    onWillAccept = source.onWillAccept;
+    onAccept = source.onAccept;
+  }
+
+  bool didEnter(dynamic data) {
+    assert(!_candidateData.contains(data));
+    assert(!_rejectedData.contains(data));
+    if (data is T && (onWillAccept == null || onWillAccept(data))) {
+      setState(() {
+        _candidateData.add(data);
+      });
+      return true;
+    }
+    _rejectedData.add(data);
+    return false;
+  }
+
+  void didLeave(dynamic data) {
+    assert(_candidateData.contains(data) || _rejectedData.contains(data));
+    setState(() {
+      _candidateData.remove(data);
+      _rejectedData.remove(data);
+    });
+  }
+
+  void didDrop(dynamic data) {
+    assert(_candidateData.contains(data));
+    setState(() {
+      _candidateData.remove(data);
+    });
+    if (onAccept != null)
+      onAccept(data);
+  }
+
+  Widget build() {
+    return builder(new UnmodifiableListView<T>(_candidateData),
+                   new UnmodifiableListView<dynamic>(_rejectedData));
+  }
+}
+
+class DragController {
+  DragController(this.data);
+
+  final dynamic data;
+
+  DragTarget _activeTarget;
+  bool _activeTargetWillAcceptDrop = false;
+
+  DragTarget _getDragTarget(List<HitTestEntry> path) {
+    for (HitTestEntry entry in path.reversed) {
+      for (Widget widget in RenderObjectWrapper.getWidgetsForRenderObject(entry.target)) {
+        if (widget is DragTarget)
+          return widget;
+      }
+    }
+    return null;
+  }
+
+  void update(Point globalPosition) {
+    HitTestResult result = SkyBinding.instance.hitTest(globalPosition);
+    DragTarget target = _getDragTarget(result.path);
+    if (target == _activeTarget)
+      return;
+    if (_activeTarget != null)
+      _activeTarget.didLeave(data);
+    _activeTarget = target;
+    _activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data);
+  }
+
+  void cancel() {
+    if (_activeTarget != null)
+      _activeTarget.didLeave(data);
+    _activeTarget = null;
+    _activeTargetWillAcceptDrop = false;
+  }
+
+  void drop() {
+    if (_activeTarget == null)
+      return;
+    if (_activeTargetWillAcceptDrop)
+      _activeTarget.didDrop(data);
+    else
+      _activeTarget.didLeave(data);
+    _activeTarget = null;
+    _activeTargetWillAcceptDrop = false;
+  }
+}
diff --git a/sky/packages/sky/lib/widgets/framework.dart b/sky/packages/sky/lib/widgets/framework.dart
index 8453a68..bc2df79 100644
--- a/sky/packages/sky/lib/widgets/framework.dart
+++ b/sky/packages/sky/lib/widgets/framework.dart
@@ -866,6 +866,17 @@
       new HashMap<RenderObject, RenderObjectWrapper>();
   static RenderObjectWrapper _getMounted(RenderObject node) => _nodeMap[node];
 
+  static Iterable<Widget> getWidgetsForRenderObject(RenderObject renderObject) sync* {
+    Widget target = RenderObjectWrapper._getMounted(renderObject);
+    if (target == null)
+      return;
+    RenderObject targetRoot = target.root;
+    while (target != null && target.root == targetRoot) {
+      yield target;
+      target = target.parent;
+    }
+  }
+
   RenderObjectWrapper _ancestor;
   void insertChildRoot(RenderObjectWrapper child, dynamic slot);
   void detachChildRoot(RenderObjectWrapper child);
@@ -1209,11 +1220,7 @@
     if (disposition == EventDisposition.consumed)
       return EventDisposition.consumed;
     for (HitTestEntry entry in result.path.reversed) {
-      Widget target = RenderObjectWrapper._getMounted(entry.target);
-      if (target == null)
-        continue;
-      RenderObject targetRoot = target.root;
-      while (target != null && target.root == targetRoot) {
+      for (Widget target in RenderObjectWrapper.getWidgetsForRenderObject(entry.target)) {
         if (target is Listener) {
           EventDisposition targetDisposition = target._handleEvent(event);
           if (targetDisposition == EventDisposition.consumed) {