From 085a2c40d1335819b7a0d43b67581cc7b547088f Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 22:39:35 +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 lastCursorRect, zoomCursorUpdated. * src/nsterm.m (ns_draw_window_cursor): Store cursor rect in lastCursorRect; call UAZoomChangeFocus with CG-space coordinates when UAZoomEnabled returns true. Set zoomCursorUpdated flag. (ns_update_end): Call UAZoomChangeFocus as fallback when cursor was not physically redrawn in this cycle (e.g., after C-x o window switch). Gated by zoomCursorUpdated to avoid double calls. Coordinate conversion: EmacsView pixels (AppKit, flipped) -> NSWindow -> NSScreen -> CGRect with y-flip for CoreGraphics top-left origin. UAZoomEnabled returns false when Zoom is inactive, so overhead is a single function call per redisplay cycle. Tested on macOS 14 with Zoom enabled: cursor tracking works across window splits, switches (C-x o), and normal navigation. --- etc/NEWS | 8 +++++++ src/nsterm.h | 6 +++++ src/nsterm.m | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/etc/NEWS b/etc/NEWS index ef36df5..f10d17e 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..ea6e7ba 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -484,6 +484,12 @@ enum ns_return_frame_mode @public struct frame *emacsframe; int scrollbarsNeedingUpdate; +#ifdef NS_IMPL_COCOA + /* Cached cursor rect for macOS Zoom integration. Set by + ns_draw_window_cursor, used by ns_update_end fallback. */ + NSRect lastCursorRect; + BOOL zoomCursorUpdated; +#endif NSRect ns_userRect; } diff --git a/src/nsterm.m b/src/nsterm.m index 74e4ad5..cd721c8 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -1104,6 +1104,35 @@ 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) where the physical cursor may not be redrawn. + Only fires when ns_draw_window_cursor did NOT run in this cycle + (zoomCursorUpdated is NO). */ +#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 + if (view && !view->zoomCursorUpdated && UAZoomEnabled () + && !NSIsEmptyRect (view->lastCursorRect)) + { + NSRect r = view->lastCursorRect; + 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); + } + if (view) + view->zoomCursorUpdated = NO; +#endif +#endif /* NS_IMPL_COCOA */ } static void @@ -3232,6 +3261,45 @@ 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 + /* Zoom integration: inform macOS Zoom of the cursor position. + Zoom (System Settings -> Accessibility -> Zoom) tracks a focus + element to keep the zoomed viewport centered on the cursor. + + Coordinate conversion: + EmacsView pixels (AppKit, flipped, top-left origin) + -> NSWindow (convertRect:toView:nil) + -> NSScreen (convertRectToScreen:) + -> CGRect with y-flip for CoreGraphics top-left origin. */ + { + EmacsView *view = FRAME_NS_VIEW (f); + if (view && on_p && active_p) + { + view->lastCursorRect = r; + view->zoomCursorUpdated = YES; + +#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 + } + } +#endif /* NS_IMPL_COCOA */ + ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -- 2.43.0