| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/base/cocoa/cursor_utils.h" |
| |
| #import <AppKit/AppKit.h> |
| #include <Foundation/Foundation.h> |
| #include <stdint.h> |
| |
| #include "base/notreached.h" |
| #include "skia/ext/skia_utils_mac.h" |
| #include "ui/base/cursor/cursor.h" |
| #include "ui/base/cursor/mojom/cursor_type.mojom-shared.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/image/image.h" |
| |
| // Private interface to CoreCursor. See |
| // https://github.com/WebKit/WebKit/blob/main/Source/WebCore/PAL/pal/spi/mac/HIServicesSPI.h |
| // |
| // Note that the column/row resize cursors have a bar in the middle (e.g. <-|->) |
| // whereas the frame resize cursors have no bar in the middle (e.g. <-->). |
| enum class CrCoreCursorType : int32_t { |
| kArrow = 0, // NSCursor.arrowCursor |
| kIBeam = 1, // NSCursor.IBeamCursor |
| kMakeAlias = 2, // NSCursor.dragLinkCursor |
| kOperationNotAllowed = 3, // NSCursor.operationNotAllowedCursor |
| kBusyButClickable = 4, // NSCursor.busyButClickableCursor (private) |
| kCopy = 5, // NSCursor.dragCopyCursor |
| kScreenShotSelection = 7, // - |
| kScreenShotSelectionToClip = 8, // - |
| kScreenShotWindow = 9, // - |
| kScreenShotWindowToClip = 10, // - |
| kClosedHand = 11, // NSCursor.closedHandCursor |
| kOpenHand = 12, // NSCursor.openHandCursor |
| kPointingHand = 13, // NSCursor.pointingHandCursor |
| kCountingUpHand = 14, // - |
| kCountingDownHand = 15, // - |
| kCountingUpAndDownHand = 16, // - |
| kColumnResizeLeft = 17, // [NSCursor columnResizeCursorInDirections:] |
| kColumnResizeRight = 18, // [NSCursor columnResizeCursorInDirections:] |
| kColumnResizeLeftRight = 19, // NSCursor.columnResizeCursor |
| kCrosshair = 20, // NSCursor.crosshairCursor |
| kRowResizeUp = 21, // [NSCursor rowResizeCursorInDirections:] |
| kRowResizeDown = 22, // [NSCursor rowResizeCursorInDirections:] |
| kRowResizeUpDown = 23, // NSCursor.rowResizeCursor |
| kContextualMenu = 24, // NSCursor.contextualMenuCursor |
| kDisappearingItem = 25, // NSCursor.disappearingItemCursor |
| kVerticalIBeam = 26, // NSCursor.IBeamCursorForVerticalLayout |
| kFrameResizeEast = |
| 27, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeEastWest = |
| 28, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeNortheast = |
| 29, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeNortheastSouthwest = |
| 30, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeNorth = |
| 31, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeNorthSouth = |
| 32, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeNorthwest = |
| 33, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeNorthwestSoutheast = |
| 34, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeSoutheast = |
| 35, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeSouth = |
| 36, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeSouthwest = |
| 37, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kFrameResizeWest = |
| 38, // [NSCursor frameResizeCursorFromPosition:inDirections:] |
| kMove = 39, // oddly, not NSCursor._moveCursor (private) |
| kHelp = 40, // NSCursor._helpCursor (private) |
| kCell = 41, // - |
| kZoomIn = 42, // NSCursor.zoomInCursor |
| kZoomOut = 43, // NSCursor.zoomOutCursor |
| }; |
| |
| @interface CrCoreCursor : NSCursor |
| |
| + (id)cursorWithType:(CrCoreCursorType)type; |
| @property(readonly, nonatomic) CrCoreCursorType _coreCursorType; |
| |
| @end |
| |
| @implementation CrCoreCursor |
| |
| @synthesize _coreCursorType = _type; |
| |
| + (id)cursorWithType:(CrCoreCursorType)type { |
| return [[CrCoreCursor alloc] initWithType:type]; |
| } |
| |
| - (id)initWithType:(CrCoreCursorType)type { |
| if ((self = [super init])) { |
| _type = type; |
| } |
| return self; |
| } |
| |
| @end |
| |
| namespace { |
| |
| NSCursor* NoneNSCursor() { |
| static NSCursor* cursor = [[NSCursor alloc] |
| initWithImage:[[NSImage alloc] initWithSize:NSMakeSize(1, 1)] |
| hotSpot:NSZeroPoint]; |
| |
| return cursor; |
| } |
| |
| NSCursor* CustomNSCursor(const ui::Cursor& cursor) { |
| float custom_scale = cursor.image_scale_factor(); |
| gfx::Size custom_size(cursor.custom_bitmap().width(), |
| cursor.custom_bitmap().height()); |
| |
| // Convert from pixels to view units. |
| if (custom_scale == 0) { |
| custom_scale = 1; |
| } |
| NSSize dip_size = NSSizeFromCGSize( |
| gfx::ScaleToFlooredSize(custom_size, 1 / custom_scale).ToCGSize()); |
| NSPoint dip_hotspot = NSPointFromCGPoint( |
| gfx::ScaleToFlooredPoint(cursor.custom_hotspot(), 1 / custom_scale) |
| .ToCGPoint()); |
| |
| // Both the image and its representation need to have the same size for |
| // cursors to appear in high resolution on retina displays. Note that the |
| // size of a representation is not the same as pixelsWide or pixelsHigh. |
| NSImage* cursor_image = skia::SkBitmapToNSImage(cursor.custom_bitmap()); |
| cursor_image.size = dip_size; |
| cursor_image.representations[0].size = dip_size; |
| |
| return [[NSCursor alloc] initWithImage:cursor_image hotSpot:dip_hotspot]; |
| } |
| |
| } // namespace |
| |
| namespace ui { |
| |
| // Match Safari's cursor choices; see |
| // https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/mac/CursorMac.mm |
| NSCursor* GetNativeCursor(const ui::Cursor& cursor) { |
| switch (cursor.type()) { |
| case ui::mojom::CursorType::kNull: |
| case ui::mojom::CursorType::kPointer: |
| return NSCursor.arrowCursor; |
| case ui::mojom::CursorType::kCross: |
| return NSCursor.crosshairCursor; |
| case ui::mojom::CursorType::kHand: |
| return NSCursor.pointingHandCursor; |
| case ui::mojom::CursorType::kIBeam: |
| return NSCursor.IBeamCursor; |
| case ui::mojom::CursorType::kWait: |
| return [CrCoreCursor cursorWithType:CrCoreCursorType::kBusyButClickable]; |
| case ui::mojom::CursorType::kHelp: |
| return [CrCoreCursor cursorWithType:CrCoreCursorType::kHelp]; |
| case ui::mojom::CursorType::kEastResize: |
| case ui::mojom::CursorType::kEastPanning: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionRight |
| inDirections:NSCursorFrameResizeDirectionsOutward]; |
| } else { |
| return [CrCoreCursor cursorWithType:CrCoreCursorType::kFrameResizeEast]; |
| } |
| case ui::mojom::CursorType::kNorthResize: |
| case ui::mojom::CursorType::kNorthPanning: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionTop |
| inDirections:NSCursorFrameResizeDirectionsOutward]; |
| } else { |
| return |
| [CrCoreCursor cursorWithType:CrCoreCursorType::kFrameResizeNorth]; |
| } |
| case ui::mojom::CursorType::kNorthEastResize: |
| case ui::mojom::CursorType::kNorthEastPanning: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionTopRight |
| inDirections:NSCursorFrameResizeDirectionsOutward]; |
| } else { |
| return [CrCoreCursor |
| cursorWithType:CrCoreCursorType::kFrameResizeNortheast]; |
| } |
| case ui::mojom::CursorType::kNorthWestResize: |
| case ui::mojom::CursorType::kNorthWestPanning: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionTopLeft |
| inDirections:NSCursorFrameResizeDirectionsOutward]; |
| } else { |
| return [CrCoreCursor |
| cursorWithType:CrCoreCursorType::kFrameResizeNorthwest]; |
| } |
| case ui::mojom::CursorType::kSouthResize: |
| case ui::mojom::CursorType::kSouthPanning: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionBottom |
| inDirections:NSCursorFrameResizeDirectionsOutward]; |
| } else { |
| return |
| [CrCoreCursor cursorWithType:CrCoreCursorType::kFrameResizeSouth]; |
| } |
| case ui::mojom::CursorType::kSouthEastResize: |
| case ui::mojom::CursorType::kSouthEastPanning: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionBottomRight |
| inDirections:NSCursorFrameResizeDirectionsOutward]; |
| } else { |
| return [CrCoreCursor |
| cursorWithType:CrCoreCursorType::kFrameResizeSoutheast]; |
| } |
| case ui::mojom::CursorType::kSouthWestResize: |
| case ui::mojom::CursorType::kSouthWestPanning: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionBottomLeft |
| inDirections:NSCursorFrameResizeDirectionsOutward]; |
| } else { |
| return [CrCoreCursor |
| cursorWithType:CrCoreCursorType::kFrameResizeSouthwest]; |
| } |
| case ui::mojom::CursorType::kWestResize: |
| case ui::mojom::CursorType::kWestPanning: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionLeft |
| inDirections:NSCursorFrameResizeDirectionsOutward]; |
| } else { |
| return [CrCoreCursor cursorWithType:CrCoreCursorType::kFrameResizeWest]; |
| } |
| case ui::mojom::CursorType::kNorthSouthResize: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionTop |
| inDirections:NSCursorFrameResizeDirectionsAll]; |
| } else { |
| return [CrCoreCursor |
| cursorWithType:CrCoreCursorType::kFrameResizeNorthSouth]; |
| } |
| case ui::mojom::CursorType::kEastWestResize: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionLeft |
| inDirections:NSCursorFrameResizeDirectionsAll]; |
| } else { |
| return [CrCoreCursor |
| cursorWithType:CrCoreCursorType::kFrameResizeEastWest]; |
| } |
| case ui::mojom::CursorType::kNorthEastSouthWestResize: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionTopRight |
| inDirections:NSCursorFrameResizeDirectionsAll]; |
| } else { |
| return [CrCoreCursor |
| cursorWithType:CrCoreCursorType::kFrameResizeNortheastSouthwest]; |
| } |
| case ui::mojom::CursorType::kNorthWestSouthEastResize: |
| if (@available(macOS 15.0, *)) { |
| return [NSCursor |
| frameResizeCursorFromPosition:NSCursorFrameResizePositionTopLeft |
| inDirections:NSCursorFrameResizeDirectionsAll]; |
| } else { |
| return [CrCoreCursor |
| cursorWithType:CrCoreCursorType::kFrameResizeNorthwestSoutheast]; |
| } |
| case ui::mojom::CursorType::kColumnResize: |
| if (@available(macOS 15.0, *)) { |
| return NSCursor.columnResizeCursor; |
| } else { |
| return NSCursor.resizeLeftRightCursor; |
| } |
| case ui::mojom::CursorType::kRowResize: |
| if (@available(macOS 15.0, *)) { |
| return NSCursor.rowResizeCursor; |
| } else { |
| return NSCursor.resizeUpDownCursor; |
| } |
| case ui::mojom::CursorType::kMiddlePanning: |
| case ui::mojom::CursorType::kMiddlePanningVertical: |
| case ui::mojom::CursorType::kMiddlePanningHorizontal: |
| case ui::mojom::CursorType::kMove: |
| return [CrCoreCursor cursorWithType:CrCoreCursorType::kMove]; |
| case ui::mojom::CursorType::kVerticalText: |
| return NSCursor.IBeamCursorForVerticalLayout; |
| case ui::mojom::CursorType::kCell: |
| return [CrCoreCursor cursorWithType:CrCoreCursorType::kCell]; |
| case ui::mojom::CursorType::kContextMenu: |
| return NSCursor.contextualMenuCursor; |
| case ui::mojom::CursorType::kAlias: |
| return NSCursor.dragLinkCursor; |
| case ui::mojom::CursorType::kProgress: |
| return [CrCoreCursor cursorWithType:CrCoreCursorType::kBusyButClickable]; |
| case ui::mojom::CursorType::kNoDrop: |
| case ui::mojom::CursorType::kNotAllowed: |
| case ui::mojom::CursorType::kEastWestNoResize: |
| case ui::mojom::CursorType::kNorthEastSouthWestNoResize: |
| case ui::mojom::CursorType::kNorthSouthNoResize: |
| case ui::mojom::CursorType::kNorthWestSouthEastNoResize: |
| return NSCursor.operationNotAllowedCursor; |
| case ui::mojom::CursorType::kCopy: |
| return NSCursor.dragCopyCursor; |
| case ui::mojom::CursorType::kNone: |
| return NoneNSCursor(); |
| case ui::mojom::CursorType::kZoomIn: |
| if (@available(macOS 15.0, *)) { |
| return NSCursor.zoomInCursor; |
| } else { |
| return [CrCoreCursor cursorWithType:CrCoreCursorType::kZoomIn]; |
| } |
| case ui::mojom::CursorType::kZoomOut: |
| if (@available(macOS 15.0, *)) { |
| return NSCursor.zoomOutCursor; |
| } else { |
| return [CrCoreCursor cursorWithType:CrCoreCursorType::kZoomOut]; |
| } |
| case ui::mojom::CursorType::kGrab: |
| return NSCursor.openHandCursor; |
| case ui::mojom::CursorType::kGrabbing: |
| return NSCursor.closedHandCursor; |
| case ui::mojom::CursorType::kCustom: |
| return CustomNSCursor(cursor); |
| case ui::mojom::CursorType::kDndNone: |
| case ui::mojom::CursorType::kDndMove: |
| case ui::mojom::CursorType::kDndCopy: |
| case ui::mojom::CursorType::kDndLink: |
| // These cursors do not apply on Mac. |
| break; |
| } |
| NOTREACHED(); |
| } |
| |
| } // namespace ui |