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) {