[PATCH 4/5] winemac.drv: Add a cursor clipping implementation using -setMouseConfinementRect:.

Tim Clem tclem at codeweavers.com
Wed Jan 19 13:40:27 CST 2022


This 10.13+ API is far simpler than the CGEventTap approach, and does
not require Accessibility permissions. It is not currently not enabled.

Signed-off-by: Tim Clem <tclem at codeweavers.com>
---
 dlls/winemac.drv/cocoa_cursorclipping.h |  16 +++
 dlls/winemac.drv/cocoa_cursorclipping.m | 141 ++++++++++++++++++++++++
 2 files changed, 157 insertions(+)

diff --git a/dlls/winemac.drv/cocoa_cursorclipping.h b/dlls/winemac.drv/cocoa_cursorclipping.h
index 7ce0529a1575..bec7b384d870 100644
--- a/dlls/winemac.drv/cocoa_cursorclipping.h
+++ b/dlls/winemac.drv/cocoa_cursorclipping.h
@@ -55,3 +55,19 @@ @interface WineEventTapClipCursorHandler : NSObject <WineClipCursorHandler>
 }
 
 @end
+
+
+ at interface WineConfinementClipCursorHandler : NSObject <WineClipCursorHandler>
+{
+    BOOL clippingCursor;
+    CGRect cursorClipRect;
+    /* The number of the window that "owns" the clipping (i.e., the one with a
+     * mouseConfinementRect set). Using this rather than a WineWindow* to avoid
+     * tricky retain situations. */
+    NSInteger clippingWindowNumber;
+}
+
+    /* Returns true if the API in use by this handler is available. */
+    + (BOOL) isAvailable;
+
+ at end
diff --git a/dlls/winemac.drv/cocoa_cursorclipping.m b/dlls/winemac.drv/cocoa_cursorclipping.m
index eaa243ae1a59..bbaa896099b3 100644
--- a/dlls/winemac.drv/cocoa_cursorclipping.m
+++ b/dlls/winemac.drv/cocoa_cursorclipping.m
@@ -21,9 +21,24 @@
 
 #import "cocoa_app.h"
 #import "cocoa_cursorclipping.h"
+#import "cocoa_window.h"
 
 
 /* Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
+ *
+ * Historically, we've used a CGEventTap and the
+ * CGAssociateMouseAndMouseCursorPosition function, as implemented in
+ * the WineEventTapClipCursorHandler class.
+ *
+ * As of macOS 10.13, there is an undocumented alternative,
+ * -[NSWindow setMouseConfinementRect:]. It comes with its own drawbacks,
+ * but is generally far simpler. It is described and implemented in
+ * the WineConfinementClipCursorHandler class.
+ */
+
+
+/* Clipping via CGEventTap and CGAssociateMouseAndMouseCursorPosition:
+ *
  * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
  * equivalent: CGAssociateMouseAndMouseCursorPosition(false).  For the
  * general case, we leverage that.  We disassociate mouse movements from
@@ -363,3 +378,129 @@ - (void) setRetinaMode:(int)mode
     }
 
 @end
+
+
+/* Clipping via mouse confinement rects:
+ *
+ * The undocumented -[NSWindow setMouseConfinementRect:] method is almost
+ * perfect for our needs. It has two main drawbacks compared to the CGEventTap
+ * approach:
+ * 1. It requires macOS 10.13+
+ * 2. A mouse confinement rect is tied to a region of a particular window. If
+ *    an app calls ClipCursor with a rect that is outside the bounds of a
+ *    window, the best we can do is intersect that rect with the window's bounds
+ *    and clip to the result. If no windows are visible in the app, we can't do
+ *    any clipping. Switching between windows in the same app while clipping is
+ *    active is likewise impossible.
+ *
+ * But it has two major benefits:
+ * 1. The code is far simpler.
+ * 2. CGEventTap started requiring Accessibility permissions from macOS in
+ *    Catalina. It's a hassle to enable, and if it's triggered while an app is
+ *    fullscreen (which is often the case with clipping), it's easy to miss.
+ */
+
+
+ at interface NSWindow (UndocumentedMouseConfinement)
+    /* Confines the system's mouse location to the provided window-relative rect
+     * while the app is frontmost and the window is key or a child of the key
+     * window. Confinement rects will be unioned among the key window and its
+     * children. The app should invoke this any time internal window geometry
+     * changes to keep the region up to date. Set NSZeroRect to remove mouse
+     * location confinement.
+     *
+     * These have been available since 10.13.
+     */
+    - (NSRect) mouseConfinementRect;
+    - (void) setMouseConfinementRect:(NSRect)mouseConfinementRect;
+ at end
+
+
+ at implementation WineConfinementClipCursorHandler
+
+ at synthesize clippingCursor, cursorClipRect;
+
+    + (BOOL) isAvailable
+    {
+        if ([NSProcessInfo instancesRespondToSelector:@selector(isOperatingSystemAtLeastVersion:)])
+        {
+            NSOperatingSystemVersion requiredVersion = { 10, 13, 0 };
+            return [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:requiredVersion] &&
+                   [NSWindow instancesRespondToSelector:@selector(setMouseConfinementRect:)];
+        }
+
+        return FALSE;
+    }
+
+    /* Returns the region of the given rect that intersects with the given
+     * window. The rect should be in screen coordinates. The result will be in
+     * window-relative coordinates.
+     *
+     * Returns NSZeroRect if the rect lies entirely outside the window.
+     */
+    + (NSRect) rectForScreenRect:(CGRect)rect inWindow:(NSWindow*)window
+    {
+        NSRect flippedRect = NSRectFromCGRect(rect);
+        [[WineApplicationController sharedController] flipRect:&flippedRect];
+
+        NSRect intersection = NSIntersectionRect([window frame], flippedRect);
+
+        if (NSIsEmptyRect(intersection))
+            return NSZeroRect;
+
+        return [window convertRectFromScreen:intersection];
+    }
+
+    - (BOOL) startClippingCursor:(CGRect)rect
+    {
+        if (clippingCursor && ![self stopClippingCursor])
+            return FALSE;
+
+        WineWindow *ownerWindow = [[WineApplicationController sharedController] frontWineWindow];
+        if (!ownerWindow)
+        {
+            /* There's nothing we can do here in this case, since confinement
+             * rects must be tied to a window. */
+            return FALSE;
+        }
+
+        NSRect clipRectInWindowCoords = [WineConfinementClipCursorHandler rectForScreenRect:rect
+                                                                                   inWindow:ownerWindow];
+
+        if (NSIsEmptyRect(clipRectInWindowCoords))
+        {
+            /* If the clip region is entirely outside of the bounds of the
+             * window, there's again nothing we can do. */
+            return FALSE;
+        }
+
+        [ownerWindow setMouseConfinementRect:clipRectInWindowCoords];
+
+        clippingWindowNumber = ownerWindow.windowNumber;
+        cursorClipRect = rect;
+        clippingCursor = TRUE;
+
+        return TRUE;
+    }
+
+    - (BOOL) stopClippingCursor
+    {
+        NSWindow *ownerWindow = [NSApp windowWithWindowNumber:clippingWindowNumber];
+        [ownerWindow setMouseConfinementRect:NSZeroRect];
+
+        clippingCursor = FALSE;
+
+        return TRUE;
+    }
+
+    - (void) clipCursorLocation:(CGPoint*)location
+    {
+        clip_cursor_location(cursorClipRect, location);
+    }
+
+    - (void) setRetinaMode:(int)mode
+    {
+        scale_rect_for_retina_mode(mode, &cursorClipRect);
+    }
+
+ at end
-- 
2.34.1




More information about the wine-devel mailing list