From 402d2959cc569ebe740f07687c37283323fce314 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 22:08:55 +0100 Subject: [PATCH] ns: integrate with macOS Zoom for cursor tracking Inform macOS Zoom of the text cursor position so the zoomed viewport follows keyboard focus in Emacs. * src/nsterm.h (EmacsView): Add lastZoomCursorRect ivar. * src/nsterm.m (ns_draw_window_cursor): Store cursor rect in lastZoomCursorRect; call UAZoomChangeFocus with CG-space coordinates when UAZoomEnabled returns true. (ns_update_end): Call UAZoomChangeFocus as fallback after each redisplay cycle to ensure Zoom tracks cursor across window switches (C-x o) where the physical cursor may not be redrawn. Coordinate conversion: EmacsView pixels (AppKit, flipped) -> NSWindow -> NSScreen -> CGRect with y-flip for CoreGraphics top-left origin. UAZoomChangeFocus is available since macOS 10.4 (ApplicationServices umbrella framework). Tested on macOS 14 with Zoom enabled: cursor tracking works across window splits, switches, and normal navigation. --- etc/NEWS | 8 ++++++ src/nsterm.h | 4 +++ src/nsterm.m | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/etc/NEWS b/etc/NEWS index ef36df5..e80e124 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -82,6 +82,14 @@ other directory on your system. You can also invoke the * Changes in Emacs 31.1 ++++ +** The macOS NS port now integrates with macOS Zoom. +When macOS Zoom is enabled (System Settings -> Accessibility -> Zoom -> +Follow keyboard focus), Emacs informs Zoom of the text cursor position +after every cursor redraw via 'UAZoomChangeFocus'. The zoomed viewport +automatically tracks the insertion point across window splits and +switches. + +++ ** 'line-spacing' now supports specifying spacing above the line. Previously, only spacing below the line could be specified. The user diff --git a/src/nsterm.h b/src/nsterm.h index 7c1ee4c..f2755e4 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -484,6 +484,10 @@ enum ns_return_frame_mode @public struct frame *emacsframe; int scrollbarsNeedingUpdate; +#ifdef NS_IMPL_COCOA + /* Cached cursor rect for macOS Zoom integration. */ + NSRect lastZoomCursorRect; +#endif NSRect ns_userRect; } diff --git a/src/nsterm.m b/src/nsterm.m index 74e4ad5..eb1649b 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -1104,6 +1104,34 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) unblock_input (); ns_updating_frame = NULL; + +#ifdef NS_IMPL_COCOA + /* Zoom fallback: ensure Zoom tracks the cursor after window + switches (C-x o) and other operations where the physical cursor + may not be redrawn but the focused window changed. This uses + lastZoomCursorRect which was set by ns_draw_window_cursor + during the current or a previous redisplay cycle. */ +#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 + if (view && UAZoomEnabled () + && !NSIsEmptyRect (view->lastZoomCursorRect)) + { + NSRect r = view->lastZoomCursorRect; + NSRect windowRect = [view convertRect:r toView:nil]; + NSRect screenRect + = [[view window] convertRectToScreen:windowRect]; + CGRect cgRect = NSRectToCGRect (screenRect); + + CGFloat primaryH + = [[[NSScreen screens] firstObject] frame].size.height; + cgRect.origin.y + = primaryH - cgRect.origin.y - cgRect.size.height; + + UAZoomChangeFocus (&cgRect, &cgRect, + kUAZoomFocusTypeInsertionPoint); + } +#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */ +#endif /* NS_IMPL_COCOA */ } static void @@ -3232,6 +3260,48 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. /* Prevent the cursor from being drawn outside the text area. */ r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); +#ifdef NS_IMPL_COCOA + /* Accessibility: store cursor rect for macOS Zoom integration. + Zoom (System Settings -> Accessibility -> Zoom) tracks a focus + element to keep the zoomed viewport centered on the cursor. + UAZoomChangeFocus() informs Zoom of the cursor position after + every physical cursor redraw. + + The coordinate conversion chain: + EmacsView pixels (AppKit, flipped, origin at top-left) + -> NSWindow coordinates (convertRect:toView:nil) + -> NSScreen coordinates (convertRectToScreen:) + -> CGRect (NSRectToCGRect, same values) + -> CG y-flip (CoreGraphics uses top-left origin on + the primary screen; AppKit uses bottom-left). */ + { + EmacsView *view = FRAME_NS_VIEW (f); + if (view && on_p && active_p) + { + view->lastZoomCursorRect = r; + +#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 + if (UAZoomEnabled ()) + { + NSRect windowRect = [view convertRect:r toView:nil]; + NSRect screenRect + = [[view window] convertRectToScreen:windowRect]; + CGRect cgRect = NSRectToCGRect (screenRect); + + CGFloat primaryH + = [[[NSScreen screens] firstObject] frame].size.height; + cgRect.origin.y + = primaryH - cgRect.origin.y - cgRect.size.height; + + UAZoomChangeFocus (&cgRect, &cgRect, + kUAZoomFocusTypeInsertionPoint); + } +#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */ + } + } +#endif /* NS_IMPL_COCOA */ + ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -- 2.43.0