using System; using System.Collections.Generic; using Unity.UIWidgets.foundation; using Unity.UIWidgets.gestures; using Unity.UIWidgets.painting; using Unity.UIWidgets.rendering; using Unity.UIWidgets.ui; namespace Unity.UIWidgets.widgets { public delegate bool DragTargetWillAccept(T data); public delegate void DragTargetAccept(T data); public delegate Widget DragTargetBuilder(BuildContext context, List candidateData, List rejectedData); public delegate void DraggableCanceledCallback(Velocity velocity, Offset offset); public delegate void DragEndCallback(DraggableDetails details); public delegate void DragTargetLeave(T data); public enum DragAnchor { child, pointer } static class _DragUtils { public static List _mapAvatarsToData(List<_DragAvatar> avatars) { List ret = new List(avatars.Count); foreach (var avatar in avatars) { ret.Add(avatar.data); } return ret; } } public class Draggable : StatefulWidget { public Draggable( Key key = null, Widget child = null, Widget feedback = null, T data = default, Axis? axis = null, Widget childWhenDragging = null, Offset feedbackOffset = null, DragAnchor dragAnchor = DragAnchor.child, Axis? affinity = null, int? maxSimultaneousDrags = null, VoidCallback onDragStarted = null, DraggableCanceledCallback onDraggableCanceled = null, DragEndCallback onDragEnd = null, VoidCallback onDragCompleted = null ) : base(key) { D.assert(child != null); D.assert(feedback != null); D.assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0); this.child = child; this.feedback = feedback; this.data = data; this.axis = axis; this.childWhenDragging = childWhenDragging; this.feedbackOffset = feedbackOffset ?? Offset.zero; this.dragAnchor = dragAnchor; this.affinity = affinity; this.maxSimultaneousDrags = maxSimultaneousDrags; this.onDragStarted = onDragStarted; this.onDraggableCanceled = onDraggableCanceled; this.onDragEnd = onDragEnd; this.onDragCompleted = onDragCompleted; } public readonly T data; public readonly Axis? axis; public readonly Widget child; public readonly Widget childWhenDragging; public readonly Widget feedback; public readonly Offset feedbackOffset; public readonly DragAnchor dragAnchor; readonly Axis? affinity; public readonly int? maxSimultaneousDrags; public readonly VoidCallback onDragStarted; public readonly DraggableCanceledCallback onDraggableCanceled; public readonly VoidCallback onDragCompleted; public readonly DragEndCallback onDragEnd; public virtual GestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) { switch (affinity) { case Axis.horizontal: { return new HorizontalMultiDragGestureRecognizer(this) {onStart = onStart}; } case Axis.vertical: { return new VerticalMultiDragGestureRecognizer(this) {onStart = onStart}; } } return new ImmediateMultiDragGestureRecognizer(this) {onStart = onStart}; } public override State createState() { return new _DraggableState(); } } public class LongPressDraggable : Draggable { public LongPressDraggable( Key key = null, Widget child = null, Widget feedback = null, T data = default, Axis? axis = null, Widget childWhenDragging = null, Offset feedbackOffset = null, DragAnchor dragAnchor = DragAnchor.child, int? maxSimultaneousDrags = null, VoidCallback onDragStarted = null, DraggableCanceledCallback onDraggableCanceled = null, DragEndCallback onDragEnd = null, VoidCallback onDragCompleted = null ) : base( key: key, child: child, feedback: feedback, data: data, axis: axis, childWhenDragging: childWhenDragging, feedbackOffset: feedbackOffset, dragAnchor: dragAnchor, maxSimultaneousDrags: maxSimultaneousDrags, onDragStarted: onDragStarted, onDraggableCanceled: onDraggableCanceled, onDragEnd: onDragEnd, onDragCompleted: onDragCompleted ) { } public override GestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) { return new DelayedMultiDragGestureRecognizer(Constants.kLongPressTimeout) { onStart = (Offset position) => { Drag result = onStart(position); return result; } }; } } public class _DraggableState : State> { public override void initState() { base.initState(); _recognizer = widget.createRecognizer(_startDrag); } public override void dispose() { _disposeRecognizerIfInactive(); base.dispose(); } GestureRecognizer _recognizer; int _activeCount; void _disposeRecognizerIfInactive() { if (_activeCount > 0) { return; } _recognizer.dispose(); _recognizer = null; } void _routePointer(PointerDownEvent pEvent) { if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags) { return; } if (pEvent is PointerDownEvent) { _recognizer.addPointer((PointerDownEvent) pEvent); } } _DragAvatar _startDrag(Offset position) { if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags) { return null; } var dragStartPoint = Offset.zero; switch (widget.dragAnchor) { case DragAnchor.child: RenderBox renderObject = context.findRenderObject() as RenderBox; dragStartPoint = renderObject.globalToLocal(position); break; case DragAnchor.pointer: dragStartPoint = Offset.zero; break; } setState(() => { _activeCount += 1; }); _DragAvatar avatar = new _DragAvatar( overlayState: Overlay.of(context, debugRequiredFor: widget), data: widget.data, axis: widget.axis, initialPosition: position, dragStartPoint: dragStartPoint, feedback: widget.feedback, feedbackOffset: widget.feedbackOffset, onDragEnd: (Velocity velocity, Offset offset, bool wasAccepted) => { if (mounted) { setState(() => { _activeCount -= 1; }); } else { _activeCount -= 1; _disposeRecognizerIfInactive(); } if (mounted && widget.onDragEnd != null) { widget.onDragEnd(new DraggableDetails( wasAccepted: wasAccepted, velocity: velocity, offset: offset )); } if (wasAccepted && widget.onDragCompleted != null) { widget.onDragCompleted(); } if (!wasAccepted && widget.onDraggableCanceled != null) { widget.onDraggableCanceled(velocity, offset); } } ); if (widget.onDragStarted != null) { widget.onDragStarted(); } return avatar; } public override Widget build(BuildContext context) { D.assert(Overlay.of(context, debugRequiredFor: widget) != null); bool canDrag = widget.maxSimultaneousDrags == null || _activeCount < widget.maxSimultaneousDrags; bool showChild = _activeCount == 0 || widget.childWhenDragging == null; if (canDrag) { return new Listener( onPointerDown: _routePointer, child: showChild ? widget.child : widget.childWhenDragging ); } return new Listener( child: showChild ? widget.child : widget.childWhenDragging); } } public class DraggableDetails { public DraggableDetails( bool wasAccepted = false, Velocity velocity = null, Offset offset = null ) { D.assert(velocity != null); D.assert(offset != null); this.wasAccepted = wasAccepted; this.velocity = velocity; this.offset = offset; } public readonly bool wasAccepted; public readonly Velocity velocity; public readonly Offset offset; } public class DragTarget : StatefulWidget { public DragTarget( Key key = null, DragTargetBuilder builder = null, DragTargetWillAccept onWillAccept = null, DragTargetAccept onAccept = null, DragTargetLeave onLeave = null ) : base(key) { D.assert(builder != null); this.builder = builder; this.onWillAccept = onWillAccept; this.onAccept = onAccept; this.onLeave = onLeave; } public readonly DragTargetBuilder builder; public readonly DragTargetWillAccept onWillAccept; public readonly DragTargetAccept onAccept; public readonly DragTargetLeave onLeave; public override State createState() { return new _DragTargetState(); } } public class _DragTargetState : State> { readonly List<_DragAvatar> _candidateAvatars = new List<_DragAvatar>(); readonly List<_DragAvatar> _rejectedAvatars = new List<_DragAvatar>(); public bool didEnter(_DragAvatar avatar) { D.assert(!_candidateAvatars.Contains(avatar)); D.assert(!_rejectedAvatars.Contains(avatar)); if (avatar is _DragAvatar && (widget.onWillAccept == null || widget.onWillAccept(avatar.data))) { setState(() => { _candidateAvatars.Add(avatar); }); return true; } else { setState(() => { _rejectedAvatars.Add(avatar); }); return false; } } public void didLeave(_DragAvatar avatar) { D.assert(_candidateAvatars.Contains(avatar) || _rejectedAvatars.Contains(avatar)); if (!mounted) { return; } setState(() => { _candidateAvatars.Remove(avatar); _rejectedAvatars.Remove(avatar); }); if (widget.onLeave != null) { widget.onLeave(avatar.data); } } public void didDrop(_DragAvatar avatar) { D.assert(_candidateAvatars.Contains(avatar)); if (!mounted) { return; } setState(() => { _candidateAvatars.Remove(avatar); }); if (widget.onAccept != null) { widget.onAccept(avatar.data); } } public override Widget build(BuildContext context) { D.assert(widget.builder != null); return new MetaData( metaData: this, behavior: HitTestBehavior.translucent, child: widget.builder(context, _DragUtils._mapAvatarsToData(_candidateAvatars), _DragUtils._mapAvatarsToData(_rejectedAvatars))); } } public enum _DragEndKind { dropped, canceled } public delegate void _OnDragEnd(Velocity velocity, Offset offset, bool wasAccepted); public class _DragAvatar : Drag { public _DragAvatar( OverlayState overlayState, T data = default, Axis? axis = null, Offset initialPosition = null, Offset dragStartPoint = null, Widget feedback = null, Offset feedbackOffset = null, _OnDragEnd onDragEnd = null ) { if (feedbackOffset == null) { feedbackOffset = Offset.zero; } D.assert(overlayState != null); this.overlayState = overlayState; this.data = data; this.axis = axis; this.dragStartPoint = dragStartPoint ?? Offset.zero; this.feedback = feedback; this.feedbackOffset = feedbackOffset ?? Offset.zero; this.onDragEnd = onDragEnd; _entry = new OverlayEntry(_build); this.overlayState.insert(_entry); _position = initialPosition ?? Offset.zero; updateDrag(initialPosition); } public readonly T data; readonly Axis? axis; readonly Offset dragStartPoint; readonly Widget feedback; readonly Offset feedbackOffset; readonly _OnDragEnd onDragEnd; readonly OverlayState overlayState; _DragTargetState _activeTarget; readonly List<_DragTargetState> _enteredTargets = new List<_DragTargetState>(); Offset _position; Offset _lastOffset; OverlayEntry _entry; public void update(DragUpdateDetails details) { _position += _restrictAxis(details.delta); updateDrag(_position); } public void end(DragEndDetails details) { finishDrag(_DragEndKind.dropped, _restrictVelocityAxis(details.velocity)); } public void cancel() { finishDrag(_DragEndKind.canceled); } void updateDrag(Offset globalPosition) { _lastOffset = globalPosition - dragStartPoint; _entry.markNeedsBuild(); HitTestResult result = new HitTestResult(); WidgetsBinding.instance.hitTest(result, globalPosition + feedbackOffset); List<_DragTargetState> targets = _getDragTargets(result.path); bool listsMatch = false; if (targets.Count >= _enteredTargets.Count && _enteredTargets.isNotEmpty()) { listsMatch = true; List<_DragTargetState>.Enumerator iterator = targets.GetEnumerator(); for (int i = 0; i < _enteredTargets.Count; i++) { iterator.MoveNext(); if (iterator.Current != _enteredTargets[i]) { listsMatch = false; break; } } } if (listsMatch) { return; } _leaveAllEntered(); _DragTargetState newTarget = null; foreach (var target in targets) { _enteredTargets.Add(target); if (target.didEnter(this)) { newTarget = target; break; } } _activeTarget = newTarget; } List<_DragTargetState> _getDragTargets(IList path) { List<_DragTargetState> ret = new List<_DragTargetState>(); foreach (HitTestEntry entry in path) { if (entry.target is RenderMetaData) { RenderMetaData renderMetaData = (RenderMetaData) entry.target; if (renderMetaData.metaData is _DragTargetState) { ret.Add((_DragTargetState) renderMetaData.metaData); } } } return ret; } void _leaveAllEntered() { for (int i = 0; i < _enteredTargets.Count; i++) { _enteredTargets[i].didLeave(this); } _enteredTargets.Clear(); } void finishDrag(_DragEndKind endKind, Velocity velocity = null) { bool wasAccepted = false; if (endKind == _DragEndKind.dropped && _activeTarget != null) { _activeTarget.didDrop(this); wasAccepted = true; _enteredTargets.Remove(_activeTarget); } _leaveAllEntered(); _activeTarget = null; _entry.remove(); _entry = null; if (onDragEnd != null) { onDragEnd(velocity == null ? Velocity.zero : velocity, _lastOffset, wasAccepted); } } public Widget _build(BuildContext context) { RenderBox box = overlayState.context.findRenderObject() as RenderBox; Offset overlayTopLeft = box.localToGlobal(Offset.zero); return new Positioned( left: _lastOffset.dx - overlayTopLeft.dx, top: _lastOffset.dy - overlayTopLeft.dy, child: new IgnorePointer( child: feedback ) ); } Velocity _restrictVelocityAxis(Velocity velocity) { if (axis == null) { return velocity; } return new Velocity( _restrictAxis(velocity.pixelsPerSecond)); } Offset _restrictAxis(Offset offset) { if (axis == null) { return offset; } if (axis == Axis.horizontal) { return new Offset(offset.dx, 0.0f); } return new Offset(0.0f, offset.dy); } } }