Support drag-and-drop for input[type=color]
[WebKit.git] / Source / WebKit / UIProcess / ios / DragDropInteractionState.mm
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "DragDropInteractionState.h"
28
29 #if ENABLE(DRAG_SUPPORT) && PLATFORM(IOS)
30
31 #import <WebCore/DragItem.h>
32 #import <WebCore/Image.h>
33
34 using namespace WebCore;
35 using namespace WebKit;
36
37 namespace WebKit {
38
39 static UIDragItem *dragItemMatchingIdentifier(id <UIDragSession> session, NSInteger identifier)
40 {
41     for (UIDragItem *item in session.items) {
42         id context = item.privateLocalContext;
43         if ([context isKindOfClass:[NSNumber class]] && [context integerValue] == identifier)
44             return item;
45     }
46     return nil;
47 }
48
49 static UITargetedDragPreview *createTargetedDragPreview(UIImage *image, UIView *rootView, UIView *previewContainer, const FloatRect& frameInRootViewCoordinates, const Vector<FloatRect>& clippingRectsInFrameCoordinates, UIColor *backgroundColor, UIBezierPath *visiblePath)
50 {
51     if (frameInRootViewCoordinates.isEmpty() || !image)
52         return nullptr;
53
54     NSMutableArray *clippingRectValuesInFrameCoordinates = [NSMutableArray arrayWithCapacity:clippingRectsInFrameCoordinates.size()];
55
56     FloatRect frameInContainerCoordinates = [rootView convertRect:frameInRootViewCoordinates toView:previewContainer];
57     if (frameInContainerCoordinates.isEmpty())
58         return nullptr;
59
60     FloatSize scalingRatio = frameInContainerCoordinates.size() / frameInRootViewCoordinates.size();
61     for (auto rect : clippingRectsInFrameCoordinates) {
62         rect.scale(scalingRatio);
63         [clippingRectValuesInFrameCoordinates addObject:[NSValue valueWithCGRect:rect]];
64     }
65
66     auto imageView = adoptNS([[UIImageView alloc] initWithImage:image]);
67     [imageView setFrame:frameInContainerCoordinates];
68
69     RetainPtr<UIDragPreviewParameters> parameters;
70     if (clippingRectValuesInFrameCoordinates.count)
71         parameters = adoptNS([[UIDragPreviewParameters alloc] initWithTextLineRects:clippingRectValuesInFrameCoordinates]);
72     else
73         parameters = adoptNS([[UIDragPreviewParameters alloc] init]);
74
75     if (backgroundColor)
76         [parameters setBackgroundColor:backgroundColor];
77
78     if (visiblePath)
79         [parameters setVisiblePath:visiblePath];
80
81     CGPoint centerInContainerCoordinates = { CGRectGetMidX(frameInContainerCoordinates), CGRectGetMidY(frameInContainerCoordinates) };
82     auto target = adoptNS([[UIDragPreviewTarget alloc] initWithContainer:previewContainer center:centerInContainerCoordinates]);
83     auto dragPreview = adoptNS([[UITargetedDragPreview alloc] initWithView:imageView.get() parameters:parameters.get() target:target.get()]);
84     return dragPreview.autorelease();
85 }
86
87 static RetainPtr<UIImage> uiImageForImage(Image* image)
88 {
89     if (!image)
90         return nullptr;
91
92     auto cgImage = image->nativeImage();
93     if (!cgImage)
94         return nullptr;
95
96     return adoptNS([[UIImage alloc] initWithCGImage:cgImage.get()]);
97 }
98
99 static bool shouldUseDragImageToCreatePreviewForDragSource(const DragSourceState& source)
100 {
101     if (!source.image)
102         return false;
103
104 #if ENABLE(INPUT_TYPE_COLOR)
105     if (source.action & DragSourceActionColor)
106         return true;
107 #endif
108
109     return source.action & (DragSourceActionDHTML | DragSourceActionImage);
110 }
111
112 static bool shouldUseVisiblePathToCreatePreviewForDragSource(const DragSourceState& source)
113 {
114     if (!source.visiblePath)
115         return false;
116
117 #if ENABLE(INPUT_TYPE_COLOR)
118     if (source.action & DragSourceActionColor)
119         return true;
120 #endif
121
122     return false;
123 }
124
125 static bool shouldUseTextIndicatorToCreatePreviewForDragSource(const DragSourceState& source)
126 {
127     if (!source.indicatorData)
128         return false;
129
130     if (source.action & (DragSourceActionLink | DragSourceActionSelection))
131         return true;
132
133 #if ENABLE(ATTACHMENT_ELEMENT)
134     if (source.action & DragSourceActionAttachment)
135         return true;
136 #endif
137
138     return false;
139 }
140
141 static bool canUpdatePreviewForActiveDragSource(const DragSourceState& source)
142 {
143     if (!source.possiblyNeedsDragPreviewUpdate)
144         return false;
145
146 #if ENABLE(INPUT_TYPE_COLOR)
147     if (source.action & DragSourceActionColor)
148         return true;
149 #endif
150
151     if (source.action & DragSourceActionLink && !(source.action & DragSourceActionImage))
152         return true;
153
154     return false;
155 }
156
157 std::optional<DragSourceState> DragDropInteractionState::activeDragSourceForItem(UIDragItem *item) const
158 {
159     if (![item.privateLocalContext isKindOfClass:[NSNumber class]])
160         return std::nullopt;
161
162     auto identifier = [(NSNumber *)item.privateLocalContext integerValue];
163     for (auto& source : m_activeDragSources) {
164         if (source.itemIdentifier == identifier)
165             return source;
166     }
167     return std::nullopt;
168 }
169
170 bool DragDropInteractionState::anyActiveDragSourceIs(WebCore::DragSourceAction action) const
171 {
172     for (auto& source : m_activeDragSources) {
173         if (source.action & action)
174             return true;
175     }
176     return false;
177 }
178
179 void DragDropInteractionState::prepareForDragSession(id <UIDragSession> session, dispatch_block_t completionHandler)
180 {
181     m_dragSession = session;
182     m_dragStartCompletionBlock = completionHandler;
183 }
184
185 void DragDropInteractionState::dragSessionWillBegin()
186 {
187     m_didBeginDragging = true;
188     updatePreviewsForActiveDragSources();
189 }
190
191 UITargetedDragPreview *DragDropInteractionState::previewForDragItem(UIDragItem *item, UIView *contentView, UIView *previewContainer) const
192 {
193     auto foundSource = activeDragSourceForItem(item);
194     if (!foundSource)
195         return nil;
196
197     auto& source = foundSource.value();
198     if (shouldUseDragImageToCreatePreviewForDragSource(source)) {
199         if (shouldUseVisiblePathToCreatePreviewForDragSource(source)) {
200             auto path = source.visiblePath.value();
201             UIBezierPath *visiblePath = [UIBezierPath bezierPathWithCGPath:path.ensurePlatformPath()];
202             return createTargetedDragPreview(source.image.get(), contentView, previewContainer, source.dragPreviewFrameInRootViewCoordinates, { }, nil, visiblePath);
203         }
204         return createTargetedDragPreview(source.image.get(), contentView, previewContainer, source.dragPreviewFrameInRootViewCoordinates, { }, nil, nil);
205     }
206
207     if (shouldUseTextIndicatorToCreatePreviewForDragSource(source)) {
208         auto indicator = source.indicatorData.value();
209         auto textIndicatorImage = uiImageForImage(indicator.contentImage.get());
210         return createTargetedDragPreview(textIndicatorImage.get(), contentView, previewContainer, indicator.textBoundingRectInRootViewCoordinates, indicator.textRectsInBoundingRectCoordinates, [UIColor colorWithCGColor:cachedCGColor(indicator.estimatedBackgroundColor)], nil);
211     }
212
213     return nil;
214 }
215
216 void DragDropInteractionState::dragSessionWillDelaySetDownAnimation(dispatch_block_t completion)
217 {
218     m_dragCancelSetDownBlock = completion;
219 }
220
221 bool DragDropInteractionState::shouldRequestAdditionalItemForDragSession(id <UIDragSession> session) const
222 {
223     return m_dragSession == session && !m_addDragItemCompletionBlock && !m_dragStartCompletionBlock;
224 }
225
226 void DragDropInteractionState::dragSessionWillRequestAdditionalItem(void (^completion)(NSArray <UIDragItem *> *))
227 {
228     clearStagedDragSource();
229     m_addDragItemCompletionBlock = completion;
230 }
231
232 void DragDropInteractionState::dropSessionDidEnterOrUpdate(id <UIDropSession> session, const DragData& dragData)
233 {
234     m_dropSession = session;
235     m_lastGlobalPosition = dragData.globalPosition();
236 }
237
238 void DragDropInteractionState::stageDragItem(const DragItem& item, UIImage *dragImage)
239 {
240     static NSInteger currentDragSourceItemIdentifier = 0;
241
242     m_adjustedPositionForDragEnd = item.eventPositionInContentCoordinates;
243     m_stagedDragSource = {{
244         static_cast<DragSourceAction>(item.sourceAction),
245         item.eventPositionInContentCoordinates,
246         item.dragPreviewFrameInRootViewCoordinates,
247         dragImage,
248         item.image.indicatorData(),
249         item.image.visiblePath(),
250         item.title.isEmpty() ? nil : (NSString *)item.title,
251         item.url.isEmpty() ? nil : (NSURL *)item.url,
252         true, // We assume here that drag previews need to be updated until proven otherwise in updatePreviewsForActiveDragSources().
253         ++currentDragSourceItemIdentifier
254     }};
255 }
256
257 bool DragDropInteractionState::hasStagedDragSource() const
258 {
259     return m_stagedDragSource && stagedDragSource().action != WebCore::DragSourceActionNone;
260 }
261
262 void DragDropInteractionState::clearStagedDragSource(DidBecomeActive didBecomeActive)
263 {
264     if (didBecomeActive == DidBecomeActive::Yes)
265         m_activeDragSources.append(stagedDragSource());
266     m_stagedDragSource = std::nullopt;
267 }
268
269 void DragDropInteractionState::dragAndDropSessionsDidEnd()
270 {
271     // If any of UIKit's completion blocks are still in-flight when the drag interaction ends, we need to ensure that they are still invoked
272     // to prevent UIKit from getting into an inconsistent state.
273     if (auto completionBlock = takeDragCancelSetDownBlock())
274         completionBlock();
275
276     if (auto completionBlock = takeAddDragItemCompletionBlock())
277         completionBlock(@[ ]);
278
279     if (auto completionBlock = takeDragStartCompletionBlock())
280         completionBlock();
281 }
282
283 void DragDropInteractionState::updatePreviewsForActiveDragSources()
284 {
285     for (auto& source : m_activeDragSources) {
286         if (!canUpdatePreviewForActiveDragSource(source))
287             continue;
288
289         UIDragItem *dragItem = dragItemMatchingIdentifier(m_dragSession.get(), source.itemIdentifier);
290         if (!dragItem)
291             continue;
292
293         if (source.action & DragSourceActionLink) {
294             dragItem.previewProvider = [title = retainPtr((NSString *)source.linkTitle), url = retainPtr((NSURL *)source.linkURL), center = source.adjustedOrigin] () -> UIDragPreview * {
295                 UIURLDragPreviewView *previewView = [UIURLDragPreviewView viewWithTitle:title.get() URL:url.get()];
296                 previewView.center = center;
297                 UIDragPreviewParameters *parameters = [[[UIDragPreviewParameters alloc] initWithTextLineRects:@[ [NSValue valueWithCGRect:previewView.bounds] ]] autorelease];
298                 return [[[UIDragPreview alloc] initWithView:previewView parameters:parameters] autorelease];
299             };
300         }
301 #if ENABLE(INPUT_TYPE_COLOR)
302         else if (source.action & DragSourceActionColor) {
303             dragItem.previewProvider = [image = source.image] () -> UIDragPreview * {
304                 UIImageView *imageView = [[[UIImageView alloc] initWithImage:image.get()] autorelease];
305                 UIDragPreviewParameters *parameters = [[[UIDragPreviewParameters alloc] initWithTextLineRects:@[ [NSValue valueWithCGRect:[imageView bounds]] ]] autorelease];
306                 return [[[UIDragPreview alloc] initWithView:imageView parameters:parameters] autorelease];
307             };
308         }
309 #endif
310
311         source.possiblyNeedsDragPreviewUpdate = false;
312     }
313 }
314
315 } // namespace WebKit
316
317 #endif // ENABLE(DRAG_SUPPORT) && PLATFORM(IOS)