[Mac] Adjust cursor position for dragged link (and stop it from moving based on how...
[WebKit-https.git] / Source / WebCore / platform / mac / DragImageMac.mm
1 /*
2  * Copyright (C) 2007, 2009, 2012 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. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #import "config.h"
27 #import "DragImage.h"
28
29 #if ENABLE(DRAG_SUPPORT) && PLATFORM(MAC)
30
31 #import "BitmapImage.h"
32 #import "CoreGraphicsSPI.h"
33 #import "Element.h"
34 #import "FloatRoundedRect.h"
35 #import "FontCascade.h"
36 #import "FontDescription.h"
37 #import "FontSelector.h"
38 #import "GraphicsContext.h"
39 #import "Image.h"
40 #import "LinkPresentationSPI.h"
41 #import "SoftLinking.h"
42 #import "StringTruncator.h"
43 #import "TextIndicator.h"
44 #import "TextRun.h"
45 #import "URL.h"
46 #import <wtf/NeverDestroyed.h>
47
48 #if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200
49 SOFT_LINK_PRIVATE_FRAMEWORK(LinkPresentation)
50 #endif
51
52 namespace WebCore {
53
54 IntSize dragImageSize(RetainPtr<NSImage> image)
55 {
56     return (IntSize)[image size];
57 }
58
59 void deleteDragImage(RetainPtr<NSImage>)
60 {
61     // Since this is a RetainPtr, there's nothing additional we need to do to
62     // delete it. It will be released when it falls out of scope.
63 }
64
65 RetainPtr<NSImage> scaleDragImage(RetainPtr<NSImage> image, FloatSize scale)
66 {
67     NSSize originalSize = [image size];
68     NSSize newSize = NSMakeSize((originalSize.width * scale.width()), (originalSize.height * scale.height()));
69     newSize.width = roundf(newSize.width);
70     newSize.height = roundf(newSize.height);
71 #pragma clang diagnostic push
72 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
73     [image setScalesWhenResized:YES];
74 #pragma clang diagnostic pop
75     [image setSize:newSize];
76     return image;
77 }
78     
79 RetainPtr<NSImage> dissolveDragImageToFraction(RetainPtr<NSImage> image, float delta)
80 {
81     if (!image)
82         return nil;
83
84     RetainPtr<NSImage> dissolvedImage = adoptNS([[NSImage alloc] initWithSize:[image size]]);
85     
86     [dissolvedImage lockFocus];
87     [image drawAtPoint:NSZeroPoint fromRect:NSMakeRect(0, 0, [image size].width, [image size].height) operation:NSCompositingOperationCopy fraction:delta];
88     [dissolvedImage unlockFocus];
89
90     return dissolvedImage;
91 }
92         
93 RetainPtr<NSImage> createDragImageFromImage(Image* image, ImageOrientationDescription description)
94 {
95     FloatSize size = image->size();
96
97     if (is<BitmapImage>(*image)) {
98         ImageOrientation orientation;
99         BitmapImage& bitmapImage = downcast<BitmapImage>(*image);
100         IntSize sizeRespectingOrientation = bitmapImage.sizeRespectingOrientation();
101
102         if (description.respectImageOrientation() == RespectImageOrientation)
103             orientation = bitmapImage.orientationForCurrentFrame();
104
105         if (orientation != DefaultImageOrientation) {
106             // Construct a correctly-rotated copy of the image to use as the drag image.
107             FloatRect destRect(FloatPoint(), sizeRespectingOrientation);
108
109             RetainPtr<NSImage> rotatedDragImage = adoptNS([[NSImage alloc] initWithSize:(NSSize)(sizeRespectingOrientation)]);
110             [rotatedDragImage lockFocus];
111
112             // ImageOrientation uses top-left coordinates, need to flip to bottom-left, apply...
113             CGAffineTransform transform = CGAffineTransformMakeTranslation(0, destRect.height());
114             transform = CGAffineTransformScale(transform, 1, -1);
115             transform = CGAffineTransformConcat(orientation.transformFromDefault(sizeRespectingOrientation), transform);
116
117             if (orientation.usesWidthAsHeight())
118                 destRect = FloatRect(destRect.x(), destRect.y(), destRect.height(), destRect.width());
119
120             // ...and flip back.
121             transform = CGAffineTransformTranslate(transform, 0, destRect.height());
122             transform = CGAffineTransformScale(transform, 1, -1);
123
124             RetainPtr<NSAffineTransform> cocoaTransform = adoptNS([[NSAffineTransform alloc] init]);
125             [cocoaTransform setTransformStruct:*(NSAffineTransformStruct*)&transform];
126             [cocoaTransform concat];
127
128             [image->snapshotNSImage() drawInRect:destRect fromRect:NSMakeRect(0, 0, size.width(), size.height()) operation:NSCompositingOperationSourceOver fraction:1.0];
129
130             [rotatedDragImage unlockFocus];
131
132             return rotatedDragImage;
133         }
134     }
135
136     auto dragImage = image->snapshotNSImage();
137     [dragImage setSize:(NSSize)size];
138     return dragImage;
139 }
140     
141 RetainPtr<NSImage> createDragImageIconForCachedImageFilename(const String& filename)
142 {
143     NSString *extension = nil;
144     size_t dotIndex = filename.reverseFind('.');
145     
146     if (dotIndex != notFound && dotIndex < (filename.length() - 1)) // require that a . exists after the first character and before the last
147         extension = filename.substring(dotIndex + 1);
148     else {
149         // It might be worth doing a further lookup to pull the extension from the MIME type.
150         extension = @"";
151     }
152     
153     return [[NSWorkspace sharedWorkspace] iconForFileType:extension];
154 }
155
156
157 const CGFloat linkImagePadding = 10;
158 const CGFloat linkImageDomainBaselineToTitleBaseline = 18;
159 const CGFloat linkImageCornerRadius = 5;
160 const CGFloat linkImageMaximumWidth = 400;
161 const CGFloat linkImageFontSize = 11;
162 const CFIndex linkImageTitleMaximumLineCount = 2;
163 const int linkImageDragCornerOutsetX = 6;
164 const int linkImageDragCornerOutsetY = 10;
165
166 IntPoint dragOffsetForLinkDragImage(DragImageRef dragImage)
167 {
168     IntSize size = dragImageSize(dragImage);
169     return { linkImageDragCornerOutsetX, size.height() + linkImageDragCornerOutsetY };
170 }
171
172 FloatPoint anchorPointForLinkDragImage(DragImageRef dragImage)
173 {
174     IntSize size = dragImageSize(dragImage);
175     return { -static_cast<float>(linkImageDragCornerOutsetX) / size.width(), -static_cast<float>(linkImageDragCornerOutsetY) / size.height() };
176 }
177
178 struct LinkImageLayout {
179     LinkImageLayout(URL&, const String& title);
180
181     struct LabelLine {
182         FloatPoint origin;
183         RetainPtr<CTLineRef> line;
184     };
185     Vector<LabelLine> lines;
186
187     FloatRect boundingRect;
188
189 private:
190     void addLine(CTLineRef, const Vector<CGPoint>& origins, CFIndex lineIndex, CGFloat x, CGFloat& y, CGFloat& maximumUsedTextWidth, bool isLastLine);
191 };
192
193 LinkImageLayout::LinkImageLayout(URL& url, const String& titleString)
194 {
195     NSString *title = nsStringNilIfEmpty(titleString);
196     NSURL *cocoaURL = url;
197     NSString *absoluteURLString = [cocoaURL absoluteString];
198
199 #if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200
200     LinkPresentationLibrary();
201     NSString *domain = [cocoaURL _lp_simplifiedDisplayString];
202 #else
203     NSString *domain = absoluteURLString;
204 #endif
205
206     if ([title isEqualToString:absoluteURLString])
207         title = nil;
208
209     NSFont *titleFont = [NSFont boldSystemFontOfSize:linkImageFontSize];
210     NSFont *domainFont = [NSFont systemFontOfSize:linkImageFontSize];
211
212     NSColor *titleColor = [NSColor labelColor];
213     NSColor *domainColor = [NSColor secondaryLabelColor];
214
215     CGFloat maximumAvailableWidth = linkImageMaximumWidth - linkImagePadding * 2;
216
217     CGFloat currentY = linkImagePadding;
218     CGFloat maximumUsedTextWidth = 0;
219
220     auto buildLines = [this, maximumAvailableWidth, &maximumUsedTextWidth, &currentY] (NSString *text, NSColor *color, NSFont *font, CFIndex maximumLines, CTLineTruncationType truncationType) {
221         NSDictionary *textAttributes = @{
222             (id)kCTFontAttributeName: font,
223             (id)kCTForegroundColorAttributeName: color
224         };
225         RetainPtr<NSAttributedString> attributedText = adoptNS([[NSAttributedString alloc] initWithString:text attributes:textAttributes]);
226         RetainPtr<CTFramesetterRef> textFramesetter = adoptCF(CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedText.get()));
227
228         CFRange fitRange;
229         CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(textFramesetter.get(), CFRangeMake(0, 0), nullptr, CGSizeMake(maximumAvailableWidth, CGFLOAT_MAX), &fitRange);
230
231         RetainPtr<CGPathRef> textPath = adoptCF(CGPathCreateWithRect(CGRectMake(0, 0, textSize.width, textSize.height), nullptr));
232         RetainPtr<CTFrameRef> textFrame = adoptCF(CTFramesetterCreateFrame(textFramesetter.get(), fitRange, textPath.get(), nullptr));
233
234         CFArrayRef ctLines = CTFrameGetLines(textFrame.get());
235         CFIndex lineCount = CFArrayGetCount(ctLines);
236         if (!lineCount)
237             return;
238
239         Vector<CGPoint> origins(lineCount);
240         CTFrameGetLineOrigins(textFrame.get(), CFRangeMake(0, 0), origins.data());
241
242         // Lay out and record the first (maximumLines - 1) lines.
243         CFIndex lineIndex = 0;
244         for (; lineIndex < std::min(maximumLines - 1, lineCount); ++lineIndex) {
245             CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(ctLines, lineIndex);
246             addLine(line, origins, lineIndex, linkImagePadding, currentY, maximumUsedTextWidth, lineIndex == lineCount - 1);
247         }
248
249         if (lineIndex == lineCount)
250             return;
251
252         // We had text that didn't fit in the first (maximumLines - 1) lines.
253         // Combine it into one last line, and truncate it.
254         CTLineRef firstRemainingLine = (CTLineRef)CFArrayGetValueAtIndex(ctLines, lineIndex);
255         CFIndex remainingRangeStart = CTLineGetStringRange(firstRemainingLine).location;
256         NSRange remainingRange = NSMakeRange(remainingRangeStart, [attributedText length] - remainingRangeStart);
257         NSAttributedString *remainingString = [attributedText attributedSubstringFromRange:remainingRange];
258         RetainPtr<CTLineRef> remainingLine = adoptCF(CTLineCreateWithAttributedString((CFAttributedStringRef)remainingString));
259         RetainPtr<NSAttributedString> ellipsisString = adoptNS([[NSAttributedString alloc] initWithString:@"\u2026" attributes:textAttributes]);
260         RetainPtr<CTLineRef> ellipsisLine = adoptCF(CTLineCreateWithAttributedString((CFAttributedStringRef)ellipsisString.get()));
261         RetainPtr<CTLineRef> truncatedLine = adoptCF(CTLineCreateTruncatedLine(remainingLine.get(), maximumAvailableWidth, truncationType, ellipsisLine.get()));
262         
263         if (!truncatedLine)
264             truncatedLine = remainingLine;
265         
266         addLine(truncatedLine.get(), origins, lineIndex, linkImagePadding, currentY, maximumUsedTextWidth, true);
267     };
268
269     if (title)
270         buildLines(title, titleColor, titleFont, linkImageTitleMaximumLineCount, kCTLineTruncationEnd);
271
272     if (title && domain)
273         currentY += linkImageDomainBaselineToTitleBaseline - (domainFont.ascender - domainFont.descender);
274
275     if (domain)
276         buildLines(domain, domainColor, domainFont, 1, kCTLineTruncationMiddle);
277
278     currentY += linkImagePadding;
279
280     boundingRect = FloatRect(0, 0, maximumUsedTextWidth + linkImagePadding * 2, currentY);
281 }
282
283 void LinkImageLayout::addLine(CTLineRef ctLine, const Vector<CGPoint>& origins, CFIndex lineIndex, CGFloat x, CGFloat& y, CGFloat& maximumUsedTextWidth, bool isLastLine)
284 {
285     CGRect lineBounds = CTLineGetBoundsWithOptions(ctLine, 0);
286     CGFloat trailingWhitespaceWidth = CTLineGetTrailingWhitespaceWidth(ctLine);
287     CGFloat lineWidthIgnoringTrailingWhitespace = lineBounds.size.width - trailingWhitespaceWidth;
288     maximumUsedTextWidth = std::max(maximumUsedTextWidth, lineWidthIgnoringTrailingWhitespace);
289
290     if (lineIndex)
291         y += origins[lineIndex - 1].y - origins[lineIndex].y;
292
293     LinkImageLayout::LabelLine line;
294     line.line = ctLine;
295     line.origin = FloatPoint(x, y);
296     lines.append(line);
297
298     if (isLastLine)
299         y += lineBounds.size.height;
300 }
301
302 DragImageRef createDragImageForLink(Element&, URL& url, const String& title, TextIndicatorData&, FontRenderingMode, float)
303 {
304     LinkImageLayout layout(url, title);
305
306     RetainPtr<NSImage> dragImage = adoptNS([[NSImage alloc] initWithSize:layout.boundingRect.size()]);
307     [dragImage lockFocus];
308
309     GraphicsContext context((CGContextRef)[NSGraphicsContext currentContext].graphicsPort);
310     context.fillRoundedRect(FloatRoundedRect(layout.boundingRect, FloatRoundedRect::Radii(linkImageCornerRadius)), Color::white);
311
312     for (const auto& line : layout.lines) {
313         GraphicsContextStateSaver saver(context);
314         context.translate(line.origin.x(), layout.boundingRect.height() - line.origin.y() - linkImagePadding);
315         CGContextSetTextPosition(context.platformContext(), 0, 0);
316         CTLineDraw(line.line.get(), context.platformContext());
317     }
318
319     [dragImage unlockFocus];
320
321     return dragImage;
322 }
323    
324 } // namespace WebCore
325
326 #endif // ENABLE(DRAG_SUPPORT) && PLATFORM(MAC)