Bug 888689 - Render SVG cursors correctly on retina displays. r=mstange
authorEvan Wallace <evan.exe@gmail.com>
Wed, 04 Dec 2013 17:46:19 -0500
changeset 158808 b4a366445b29d89bd04bc2136d01e8999c8d470a
parent 158807 ad0d9f62c29c96abe9a1651c3c41a5dcbfe393a0
child 158809 9ceffd9514da3f9b5dd8d2a41f9980dccbc3ee09
push id25763
push usercbook@mozilla.com
push dateThu, 05 Dec 2013 11:39:19 +0000
treeherdermozilla-central@056164bcce96 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmstange
bugs888689
milestone28.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 888689 - Render SVG cursors correctly on retina displays. r=mstange
widget/cocoa/nsChildView.mm
widget/cocoa/nsCocoaUtils.h
widget/cocoa/nsCocoaUtils.mm
widget/cocoa/nsCursorManager.h
widget/cocoa/nsCursorManager.mm
--- a/widget/cocoa/nsChildView.mm
+++ b/widget/cocoa/nsChildView.mm
@@ -905,17 +905,17 @@ NS_IMETHODIMP nsChildView::SetCursor(nsC
 
 // implement to fix "hidden virtual function" warning
 NS_IMETHODIMP nsChildView::SetCursor(imgIContainer* aCursor,
                                       uint32_t aHotspotX, uint32_t aHotspotY)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
   nsBaseWidget::SetCursor(aCursor, aHotspotX, aHotspotY);
-  return [[nsCursorManager sharedInstance] setCursorWithImage:aCursor hotSpotX:aHotspotX hotSpotY:aHotspotY];
+  return [[nsCursorManager sharedInstance] setCursorWithImage:aCursor hotSpotX:aHotspotX hotSpotY:aHotspotY scaleFactor:BackingScaleFactor()];
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 #pragma mark -
 
 // Get this component dimension
 NS_IMETHODIMP nsChildView::GetBounds(nsIntRect &aRect)
--- a/widget/cocoa/nsCocoaUtils.h
+++ b/widget/cocoa/nsCocoaUtils.h
@@ -237,19 +237,20 @@ class nsCocoaUtils
    */
   static nsresult CreateNSImageFromCGImage(CGImageRef aInputImage, NSImage **aResult);
 
   /** Creates a Cocoa <code>NSImage</code> from a frame of an <code>imgIContainer</code>.
       Combines the two methods above. The caller owns the <code>NSImage</code>.
       @param aImage the image to extract a frame from
       @param aWhichFrame the frame to extract (see imgIContainer FRAME_*)
       @param aResult the resulting NSImage
+      @param scaleFactor the desired scale factor of the NSImage (2 for a retina display)
       @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise
    */  
-  static nsresult CreateNSImageFromImageContainer(imgIContainer *aImage, uint32_t aWhichFrame, NSImage **aResult);
+  static nsresult CreateNSImageFromImageContainer(imgIContainer *aImage, uint32_t aWhichFrame, NSImage **aResult, CGFloat scaleFactor);
 
   /**
    * Returns nsAString for aSrc.
    */
   static void GetStringForNSString(const NSString *aSrc, nsAString& aDist);
 
   /**
    * Makes NSString instance for aString.
--- a/widget/cocoa/nsCocoaUtils.mm
+++ b/widget/cocoa/nsCocoaUtils.mm
@@ -277,71 +277,132 @@ nsresult nsCocoaUtils::CreateCGImageFrom
                                                                   NULL);
   CGColorSpaceRef colorSpace = ::CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
   *aResult = ::CGImageCreate(width,
                              height,
                              8,
                              32,
                              stride,
                              colorSpace,
-                             kCGBitmapByteOrder32Host | kCGImageAlphaFirst,
+                             kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst,
                              dataProvider,
                              NULL,
                              0,
                              kCGRenderingIntentDefault);
   ::CGColorSpaceRelease(colorSpace);
   ::CGDataProviderRelease(dataProvider);
   return *aResult ? NS_OK : NS_ERROR_FAILURE;
 }
 
 nsresult nsCocoaUtils::CreateNSImageFromCGImage(CGImageRef aInputImage, NSImage **aResult)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
+  // Be very careful when creating the NSImage that the backing NSImageRep is
+  // exactly 1:1 with the input image. On a retina display, both [NSImage
+  // lockFocus] and [NSImage initWithCGImage:size:] will create an image with a
+  // 2x backing NSImageRep. This prevents NSCursor from recognizing a retina
+  // cursor, which only occurs if pixelsWide and pixelsHigh are exactly 2x the
+  // size of the NSImage.
+  //
+  // For example, if a 32x32 SVG cursor is rendered on a retina display, then
+  // aInputImage will be 64x64. The resulting NSImage will be scaled back down
+  // to 32x32 so it stays the correct size on the screen by changing its size
+  // (resizing a NSImage only scales the image and doesn't resample the data).
+  // If aInputImage is converted using [NSImage initWithCGImage:size:] then the
+  // bitmap will be 128x128 and NSCursor won't recognize a retina cursor, since
+  // it will expect a 64x64 bitmap.
+
   int32_t width = ::CGImageGetWidth(aInputImage);
   int32_t height = ::CGImageGetHeight(aInputImage);
   NSRect imageRect = ::NSMakeRect(0.0, 0.0, width, height);
 
-  // Create a new image to receive the Quartz image data.
-  *aResult = [[NSImage alloc] initWithSize:imageRect.size];
+  NSBitmapImageRep *offscreenRep = [[NSBitmapImageRep alloc]
+    initWithBitmapDataPlanes:NULL
+    pixelsWide:width
+    pixelsHigh:height
+    bitsPerSample:8
+    samplesPerPixel:4
+    hasAlpha:YES
+    isPlanar:NO
+    colorSpaceName:NSDeviceRGBColorSpace
+    bitmapFormat:NSAlphaFirstBitmapFormat
+    bytesPerRow:0
+    bitsPerPixel:0];
 
-  [*aResult lockFocus];
+  NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:offscreenRep];
+  [NSGraphicsContext saveGraphicsState];
+  [NSGraphicsContext setCurrentContext:context];
 
   // Get the Quartz context and draw.
   CGContextRef imageContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
   ::CGContextDrawImage(imageContext, *(CGRect*)&imageRect, aInputImage);
 
-  [*aResult unlockFocus];
+  [NSGraphicsContext restoreGraphicsState];
+
+  *aResult = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
+  [*aResult addRepresentation:offscreenRep];
+  [offscreenRep release];
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
-nsresult nsCocoaUtils::CreateNSImageFromImageContainer(imgIContainer *aImage, uint32_t aWhichFrame, NSImage **aResult)
+nsresult nsCocoaUtils::CreateNSImageFromImageContainer(imgIContainer *aImage, uint32_t aWhichFrame, NSImage **aResult, CGFloat scaleFactor)
 {
-  nsRefPtr<gfxASurface> surface;
-  aImage->GetFrame(aWhichFrame,
-                   imgIContainer::FLAG_SYNC_DECODE,
-                   getter_AddRefs(surface));
-  NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
+  nsRefPtr<gfxImageSurface> frame;
+  int32_t width = 0, height = 0;
+  aImage->GetWidth(&width);
+  aImage->GetHeight(&height);
+
+  // Render a vector image at the correct resolution on a retina display
+  if (aImage->GetType() == imgIContainer::TYPE_VECTOR && scaleFactor != 1.0f) {
+    int scaledWidth = (int)ceilf(width * scaleFactor);
+    int scaledHeight = (int)ceilf(height * scaleFactor);
+
+    frame = new gfxImageSurface(gfxIntSize(scaledWidth, scaledHeight), gfxImageFormatARGB32);
+    NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
+
+    nsRefPtr<gfxContext> context = new gfxContext(frame);
+    NS_ENSURE_TRUE(context, NS_ERROR_FAILURE);
 
-  nsRefPtr<gfxImageSurface> frame(surface->GetAsReadableARGB32ImageSurface());
-  NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
+    aImage->Draw(context, GraphicsFilter::FILTER_NEAREST, gfxMatrix(),
+      gfxRect(0.0f, 0.0f, scaledWidth, scaledHeight),
+      nsIntRect(0, 0, width, height),
+      nsIntSize(scaledWidth, scaledHeight),
+      nullptr, aWhichFrame, imgIContainer::FLAG_SYNC_DECODE);
+  }
+
+  else {
+    nsRefPtr<gfxASurface> surface;
+    aImage->GetFrame(aWhichFrame,
+                     imgIContainer::FLAG_SYNC_DECODE,
+                     getter_AddRefs(surface));
+    NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
+
+    frame = surface->GetAsReadableARGB32ImageSurface();
+    NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
+  }
 
   CGImageRef imageRef = NULL;
   nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(frame, &imageRef);
   if (NS_FAILED(rv) || !imageRef) {
     return NS_ERROR_FAILURE;
   }
 
   rv = nsCocoaUtils::CreateNSImageFromCGImage(imageRef, aResult);
   if (NS_FAILED(rv) || !aResult) {
     return NS_ERROR_FAILURE;
   }
   ::CGImageRelease(imageRef);
+
+  // Ensure the image will be rendered the correct size on a retina display
+  NSSize size = NSMakeSize(width, height);
+  [*aResult setSize:size];
+  [[[*aResult representations] objectAtIndex:0] setSize:size];
   return NS_OK;
 }
 
 // static
 void
 nsCocoaUtils::GetStringForNSString(const NSString *aSrc, nsAString& aDist)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
--- a/widget/cocoa/nsCursorManager.h
+++ b/widget/cocoa/nsCursorManager.h
@@ -32,18 +32,19 @@
 
 /*! @method  setCursorWithImage:hotSpotX:hotSpotY:
  @abstract   Sets the current cursor to a custom image
  @discussion Sets the current cursor to the cursor given by the aCursorImage argument.
  Resources associated with the previous cursor are cleaned up.
  @param aCursorImage the cursor image to use
  @param aHotSpotX the x coordinate of the cursor's hotspot
  @param aHotSpotY the y coordinate of the cursor's hotspot
+ @param scaleFactor the scale factor of the target display (2 for a retina display)
  */
-- (nsresult) setCursorWithImage: (imgIContainer*) aCursorImage hotSpotX: (uint32_t) aHotspotX hotSpotY: (uint32_t) aHotspotY;
+- (nsresult) setCursorWithImage: (imgIContainer*) aCursorImage hotSpotX: (uint32_t) aHotspotX hotSpotY: (uint32_t) aHotspotY scaleFactor: (CGFloat) scaleFactor;
 
 
 /*! @method     sharedInstance
     @abstract   Get the Singleton instance of the cursor manager.
     @discussion Use this method to obtain a reference to the cursor manager.
     @result a reference to the cursor manager
 */
 + (nsCursorManager *) sharedInstance;
--- a/widget/cocoa/nsCursorManager.mm
+++ b/widget/cocoa/nsCursorManager.mm
@@ -4,16 +4,17 @@
 
 #include "imgIContainer.h"
 #include "nsCocoaUtils.h"
 #include "nsCursorManager.h"
 #include "nsObjCExceptions.h"
 #include <math.h>
 
 static nsCursorManager *gInstance;
+static CGFloat sCursorScaleFactor = 0.0f;
 static imgIContainer *sCursorImgContainer = nullptr;
 static const nsCursor sCustomCursor = eCursorCount;
 
 /*! @category nsCursorManager(PrivateMethods)
     Private methods for the cursor manager class.
 */
 @interface nsCursorManager(PrivateMethods)
 /*! @method     getCursor:
@@ -235,50 +236,51 @@ static const nsCursor sCustomCursor = eC
     mCurrentMacCursor = aMacCursor;
   }
 
   return NS_OK;
   
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
-- (nsresult) setCursorWithImage: (imgIContainer*) aCursorImage hotSpotX: (uint32_t) aHotspotX hotSpotY: (uint32_t) aHotspotY
+- (nsresult) setCursorWithImage: (imgIContainer*) aCursorImage hotSpotX: (uint32_t) aHotspotX hotSpotY: (uint32_t) aHotspotY scaleFactor: (CGFloat) scaleFactor
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
   // As the user moves the mouse, this gets called repeatedly with the same aCursorImage
-  if (sCursorImgContainer == aCursorImage && mCurrentMacCursor) {
+  if (sCursorImgContainer == aCursorImage && sCursorScaleFactor == scaleFactor && mCurrentMacCursor) {
     [self setMacCursor:mCurrentMacCursor];
     return NS_OK;
   }
   
   [[NSCursor currentCursor] set];
   int32_t width = 0, height = 0;
   aCursorImage->GetWidth(&width);
   aCursorImage->GetHeight(&height);
   // prevent DoS attacks
   if (width > 128 || height > 128) {
     return NS_OK;
   }
 
   NSImage *cursorImage;
-  nsresult rv = nsCocoaUtils::CreateNSImageFromImageContainer(aCursorImage, imgIContainer::FRAME_FIRST, &cursorImage);
+  nsresult rv = nsCocoaUtils::CreateNSImageFromImageContainer(aCursorImage, imgIContainer::FRAME_FIRST, &cursorImage, scaleFactor);
   if (NS_FAILED(rv) || !cursorImage) {
     return NS_ERROR_FAILURE;
   }
 
   // if the hotspot is nonsensical, make it 0,0
   aHotspotX = (aHotspotX > (uint32_t)width - 1) ? 0 : aHotspotX;
   aHotspotY = (aHotspotY > (uint32_t)height - 1) ? 0 : aHotspotY;
 
   NSPoint hotSpot = ::NSMakePoint(aHotspotX, aHotspotY);
   [self setMacCursor:[nsMacCursor cursorWithCursor:[[NSCursor alloc] initWithImage:cursorImage hotSpot:hotSpot] type:sCustomCursor]];
   [cursorImage release];
   
   NS_IF_RELEASE(sCursorImgContainer);
   sCursorImgContainer = aCursorImage;
+  sCursorScaleFactor = scaleFactor;
   NS_ADDREF(sCursorImgContainer);
   
   return NS_OK;
   
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 - (nsMacCursor *) getCursor: (enum nsCursor) aCursor