diff --git a/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch b/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch index 8e2290b..73bbf43 100644 --- a/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch +++ b/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch @@ -1,34 +1,34 @@ -From 402d2959cc569ebe740f07687c37283323fce314 Mon Sep 17 00:00:00 2001 +From 085a2c40d1335819b7a0d43b67581cc7b547088f Mon Sep 17 00:00:00 2001 From: Martin Sukany -Date: Sat, 28 Feb 2026 22:08:55 +0100 +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 lastZoomCursorRect ivar. +* src/nsterm.h (EmacsView): Add lastCursorRect, zoomCursorUpdated. * 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. +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. UAZoomChangeFocus is available since macOS 10.4 -(ApplicationServices umbrella framework). +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, and normal navigation. +window splits, switches (C-x o), and normal navigation. --- - etc/NEWS | 8 ++++++ - src/nsterm.h | 4 +++ - src/nsterm.m | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ + 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..e80e124 100644 +index ef36df5..f10d17e 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -82,6 +82,14 @@ other directory on your system. You can also invoke the @@ -37,7 +37,7 @@ index ef36df5..e80e124 100644 ++++ +** The macOS NS port now integrates with macOS Zoom. -+When macOS Zoom is enabled (System Settings -> Accessibility -> 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 @@ -47,41 +47,42 @@ index ef36df5..e80e124 100644 ** '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 +index 7c1ee4c..ea6e7ba 100644 --- a/src/nsterm.h +++ b/src/nsterm.h -@@ -484,6 +484,10 @@ enum ns_return_frame_mode +@@ -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. */ -+ NSRect lastZoomCursorRect; ++ /* 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..eb1649b 100644 +index 74e4ad5..cd721c8 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -1104,6 +1104,34 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -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) 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. */ ++ 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 && UAZoomEnabled () -+ && !NSIsEmptyRect (view->lastZoomCursorRect)) ++ if (view && !view->zoomCursorUpdated && UAZoomEnabled () ++ && !NSIsEmptyRect (view->lastCursorRect)) + { -+ NSRect r = view->lastZoomCursorRect; ++ NSRect r = view->lastCursorRect; + NSRect windowRect = [view convertRect:r toView:nil]; + NSRect screenRect + = [[view window] convertRectToScreen:windowRect]; @@ -95,34 +96,33 @@ index 74e4ad5..eb1649b 100644 + UAZoomChangeFocus (&cgRect, &cgRect, + kUAZoomFocusTypeInsertionPoint); + } -+#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */ ++ if (view) ++ view->zoomCursorUpdated = NO; ++#endif +#endif /* NS_IMPL_COCOA */ } static void -@@ -3232,6 +3260,48 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. +@@ -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 -+ /* Accessibility: store cursor rect for macOS Zoom integration. ++ /* 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. -+ 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). */ ++ 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->lastZoomCursorRect = r; ++ view->lastCursorRect = r; ++ view->zoomCursorUpdated = YES; + +#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 @@ -141,7 +141,7 @@ index 74e4ad5..eb1649b 100644 + UAZoomChangeFocus (&cgRect, &cgRect, + kUAZoomFocusTypeInsertionPoint); + } -+#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */ ++#endif + } + } +#endif /* NS_IMPL_COCOA */ diff --git a/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch b/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch index de2245a..4208e15 100644 --- a/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch +++ b/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch @@ -1,4 +1,4 @@ -From 84a96f2f60f2db10cc3d38bc68ccb7d2404b6ad5 Mon Sep 17 00:00:00 2001 +From cd6ad89e786fc79f68bc0843b8122e088e8766ba Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 1/8] ns: add accessibility base classes and text extraction diff --git a/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch b/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch index 98552b2..f40bf66 100644 --- a/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch +++ b/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch @@ -1,4 +1,4 @@ -From 99ea6d5d2599c54da764ce94125e2da874de8e16 Mon Sep 17 00:00:00 2001 +From 68ce438269f04570f21e92bd2c49f2ff83244cb8 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 2/8] ns: implement buffer accessibility element (core diff --git a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch index da3b846..222dcd5 100644 --- a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch +++ b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch @@ -1,4 +1,4 @@ -From 1d346b87020909a27cb39f34110ba226dd8d92fe Mon Sep 17 00:00:00 2001 +From f5ce42e931a3ed1668e6fb8260ef736442d8d2c9 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 3/8] ns: add buffer notification dispatch and mode-line diff --git a/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch b/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch index 83c3bb3..8dd1e81 100644 --- a/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch +++ b/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch @@ -1,4 +1,4 @@ -From 9da1c8d18d2e70fc53f1f8e061a963c3adf8d960 Mon Sep 17 00:00:00 2001 +From 8675f0f75a33e4a3621e0b1e15aab7eff2c81369 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 4/8] ns: add interactive span elements for Tab navigation diff --git a/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch b/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch index 25081ee..439fd5a 100644 --- a/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch +++ b/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch @@ -1,4 +1,4 @@ -From 3304705947559e6ba83e462aed5a17f1c195c641 Mon Sep 17 00:00:00 2001 +From 9e7fa018ef779610b2fb54c1ff951d0bf6bf7652 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 5/8] ns: integrate accessibility with EmacsView and redisplay @@ -23,8 +23,9 @@ block cursor, org-mode folded headings, indirect buffers. Known limitations documented in patch 6 Texinfo node. --- etc/NEWS | 13 ++ - src/nsterm.m | 398 ++++++++++++++++++++++++++++++++++++++++++++++++++- - 2 files changed, 408 insertions(+), 3 deletions(-) + src/nsterm.h | 2 +- + src/nsterm.m | 373 ++++++++++++++++++++++++++++++++++++++++++++++++++- + 3 files changed, 384 insertions(+), 4 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index ef36df5..e76ee93 100644 @@ -50,8 +51,21 @@ index ef36df5..e76ee93 100644 --- ** Re-introduced dictation, lost in Emacs v30 (macOS). We lost macOS dictation in v30 when migrating to NSTextInputClient. +diff --git a/src/nsterm.h b/src/nsterm.h +index 5298386..ec7b587 100644 +--- a/src/nsterm.h ++++ b/src/nsterm.h +@@ -594,7 +594,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) + BOOL accessibilityTreeValid; + BOOL accessibilityUpdating; + @public /* Accessed by ns_draw_phys_cursor (C function). */ +- NSRect lastAccessibilityCursorRect; ++ NSRect lastCursorRect; + #endif + BOOL font_panel_active; + NSFont *font_panel_result; diff --git a/src/nsterm.m b/src/nsterm.m -index c852929..b3bef4b 100644 +index c852929..f0e8751 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -1105,6 +1105,11 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) @@ -66,51 +80,26 @@ index c852929..b3bef4b 100644 } static void -@@ -3233,6 +3238,43 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. +@@ -3233,6 +3238,18 @@ 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 Zoom and bounds queries. -+ Skipped when ns-accessibility-enabled is nil to avoid overhead. -+ VoiceOver notifications are handled solely by -+ postAccessibilityUpdates (called from ns_update_end) -+ to avoid duplicate notifications and mid-redisplay fragility. */ ++ /* Accessibility: store cursor rect for VoiceOver bounds queries. ++ accessibilityBoundsForRange: / accessibilityFrameForRange: ++ use this as a fallback when no valid window/glyph data is ++ available. Skipped when ns-accessibility-enabled is nil. */ + { + EmacsView *view = FRAME_NS_VIEW (f); + if (view && on_p && active_p && ns_accessibility_enabled) -+ { -+ view->lastAccessibilityCursorRect = r; -+ -+ /* Tell macOS Zoom where the cursor is. UAZoomChangeFocus() -+ expects top-left origin (CG coordinate space). -+ These APIs are available since macOS 10.4 (Universal Access -+ framework, linked via ApplicationServices umbrella). */ -+#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 */ -+ } ++ view->lastCursorRect = r; + } +#endif + ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -7281,7 +7323,6 @@ - (id)accessibilityTopLevelUIElement +@@ -7281,7 +7298,6 @@ - (id)accessibilityTopLevelUIElement @@ -118,7 +107,7 @@ index c852929..b3bef4b 100644 static BOOL ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, ptrdiff_t *out_start, -@@ -8375,7 +8416,6 @@ - (NSRect)accessibilityFrame +@@ -8375,7 +8391,6 @@ - (NSRect)accessibilityFrame @end @@ -126,7 +115,7 @@ index c852929..b3bef4b 100644 /* =================================================================== EmacsAccessibilityBuffer (Notifications) — AX event dispatch -@@ -8920,7 +8960,6 @@ - (NSRect)accessibilityFrame +@@ -8920,7 +8935,6 @@ - (NSRect)accessibilityFrame @end @@ -134,7 +123,7 @@ index c852929..b3bef4b 100644 /* =================================================================== EmacsAccessibilityInteractiveSpan — helpers and implementation =================================================================== */ -@@ -9250,6 +9289,7 @@ - (void)dealloc +@@ -9250,6 +9264,7 @@ - (void)dealloc [layer release]; #endif @@ -142,7 +131,7 @@ index c852929..b3bef4b 100644 [[self menu] release]; [super dealloc]; } -@@ -10598,6 +10638,32 @@ - (void)windowDidBecomeKey /* for direct calls */ +@@ -10598,6 +10613,32 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -175,7 +164,7 @@ index c852929..b3bef4b 100644 } -@@ -11835,6 +11901,332 @@ - (int) fullscreenState +@@ -11835,6 +11876,332 @@ - (int) fullscreenState return fs_state; } @@ -436,7 +425,7 @@ index c852929..b3bef4b 100644 + return bufRect; + } + -+ NSRect viewRect = lastAccessibilityCursorRect; ++ NSRect viewRect = lastCursorRect; + + if (viewRect.size.width < 1) + viewRect.size.width = 1; diff --git a/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch b/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch index 9b80fa0..54c4492 100644 --- a/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch +++ b/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch @@ -1,4 +1,4 @@ -From c08f128e25cb645a722c9772e9e4f3a505655d26 Mon Sep 17 00:00:00 2001 +From 683d7497cc3414a231b44363dd28d2748780c38a Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 6/8] doc: add VoiceOver accessibility section to macOS diff --git a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch index 090262f..85f1bab 100644 --- a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch +++ b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch @@ -1,4 +1,4 @@ -From 2222c14590612835529f88c0062fefeedc8f7187 Mon Sep 17 00:00:00 2001 +From 5143187e2fa42fa8f5c28e4f39be08ca981692c9 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 14:46:25 +0100 Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver @@ -44,12 +44,12 @@ Key implementation details: (EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): Independent overlay branch, BUF_CHARS_MODIFF gating, candidate --- - src/nsterm.h | 3 + - src/nsterm.m | 359 ++++++++++++++++++++++++++++++++++++++++++++++----- - 2 files changed, 327 insertions(+), 35 deletions(-) + src/nsterm.h | 1 + + src/nsterm.m | 312 +++++++++++++++++++++++++++++++++++++++++++++------ + 2 files changed, 279 insertions(+), 34 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h -index 5298386..a007925 100644 +index ec7b587..19a7e7a 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -509,6 +509,7 @@ typedef struct ns_ax_visible_run @@ -60,34 +60,11 @@ index 5298386..a007925 100644 @property (nonatomic, assign) ptrdiff_t cachedPoint; @property (nonatomic, assign) BOOL cachedMarkActive; @property (nonatomic, copy) NSString *cachedCompletionAnnouncement; -@@ -595,6 +596,8 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) - BOOL accessibilityUpdating; - @public /* Accessed by ns_draw_phys_cursor (C function). */ - NSRect lastAccessibilityCursorRect; -+ BOOL overlayZoomActive; -+ NSRect overlayZoomRect; - #endif - BOOL font_panel_active; - NSFont *font_panel_result; diff --git a/src/nsterm.m b/src/nsterm.m -index b3bef4b..7025e6e 100644 +index f0e8751..3d72b5d 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -3258,7 +3258,12 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. - && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 - if (UAZoomEnabled ()) - { -- NSRect windowRect = [view convertRect:r toView:nil]; -+ /* When overlay completion is active (e.g. Vertico), -+ focus Zoom on the selected candidate row instead -+ of the text cursor. */ -+ NSRect zoomSrc = view->overlayZoomActive -+ ? view->overlayZoomRect : r; -+ NSRect windowRect = [view convertRect:zoomSrc toView:nil]; - NSRect screenRect = [[view window] convertRectToScreen:windowRect]; - CGRect cgRect = NSRectToCGRect (screenRect); - -@@ -6909,11 +6914,154 @@ Accessibility virtual elements (macOS / Cocoa only) +@@ -6884,11 +6884,154 @@ Accessibility virtual elements (macOS / Cocoa only) /* ---- Helper: extract buffer text for accessibility ---- */ @@ -148,8 +125,8 @@ index b3bef4b..7025e6e 100644 + { + Lisp_Object ov = XCAR (tail); + Lisp_Object strings[2]; -+ strings[0] = Foverlay_get (ov, intern_c_string ("before-string")); -+ strings[1] = Foverlay_get (ov, intern_c_string ("after-string")); ++ strings[0] = Foverlay_get (ov, Qbefore_string); ++ strings[1] = Foverlay_get (ov, Qafter_string); + + for (int s = 0; s < 2; s++) + { @@ -243,7 +220,7 @@ index b3bef4b..7025e6e 100644 static NSString * ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_visible_run **out_runs, NSUInteger *out_nruns) -@@ -6984,7 +7132,7 @@ Accessibility virtual elements (macOS / Cocoa only) +@@ -6959,7 +7102,7 @@ Accessibility virtual elements (macOS / Cocoa only) /* Extract this visible run's text. Use Fbuffer_substring_no_properties which correctly handles the @@ -252,7 +229,7 @@ index b3bef4b..7025e6e 100644 include garbage bytes when the run spans the gap position. */ Lisp_Object lstr = Fbuffer_substring_no_properties ( make_fixnum (pos), make_fixnum (run_end)); -@@ -7065,7 +7213,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font) +@@ -7040,7 +7183,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font) return NSZeroRect; /* charpos_start and charpos_len are already in buffer charpos @@ -261,7 +238,7 @@ index b3bef4b..7025e6e 100644 charposForAccessibilityIndex which handles invisible text. */ ptrdiff_t cp_start = charpos_start; ptrdiff_t cp_end = cp_start + charpos_len; -@@ -7544,6 +7692,7 @@ @implementation EmacsAccessibilityBuffer +@@ -7519,6 +7662,7 @@ @implementation EmacsAccessibilityBuffer @synthesize cachedOverlayModiff; @synthesize cachedTextStart; @synthesize cachedModiff; @@ -269,7 +246,7 @@ index b3bef4b..7025e6e 100644 @synthesize cachedPoint; @synthesize cachedMarkActive; @synthesize cachedCompletionAnnouncement; -@@ -7641,7 +7790,7 @@ - (void)ensureTextCache +@@ -7616,7 +7760,7 @@ - (void)ensureTextCache NSTRACE ("EmacsAccessibilityBuffer ensureTextCache"); /* This method is only called from the main thread (AX getters dispatch_sync to main first). Reads of cachedText/cachedTextModiff @@ -278,7 +255,7 @@ index b3bef4b..7025e6e 100644 write section at the end needs synchronization to protect against concurrent reads from AX server thread. */ eassert ([NSThread isMainThread]); -@@ -7654,16 +7803,15 @@ - (void)ensureTextCache +@@ -7629,16 +7773,15 @@ - (void)ensureTextCache return; ptrdiff_t modiff = BUF_MODIFF (b); @@ -301,7 +278,7 @@ index b3bef4b..7025e6e 100644 && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 -@@ -7680,7 +7828,6 @@ - (void)ensureTextCache +@@ -7655,7 +7798,6 @@ - (void)ensureTextCache [cachedText release]; cachedText = [text retain]; cachedTextModiff = modiff; @@ -309,7 +286,7 @@ index b3bef4b..7025e6e 100644 cachedTextStart = start; if (visibleRuns) -@@ -7745,7 +7892,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +@@ -7720,7 +7862,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos /* Binary search: runs are sorted by charpos (ascending). Find the run whose [charpos, charpos+length) range contains the target, or the nearest run after an invisible gap. O(log n) instead of @@ -318,7 +295,7 @@ index b3bef4b..7025e6e 100644 NSUInteger lo = 0, hi = visibleRunCount; while (lo < hi) { -@@ -7758,7 +7905,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +@@ -7733,7 +7875,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos else { /* Found: charpos is inside this run. Compute UTF-16 delta @@ -327,7 +304,7 @@ index b3bef4b..7025e6e 100644 NSUInteger chars_in = (NSUInteger)(charpos - r->charpos); if (chars_in == 0 || !cachedText) return r->ax_start; -@@ -7783,10 +7930,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +@@ -7758,10 +7900,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos /* Convert accessibility string index to buffer charpos. Safe to call from any thread: uses only cachedText (NSString) and @@ -340,7 +317,7 @@ index b3bef4b..7025e6e 100644 @synchronized (self) { if (visibleRunCount == 0) -@@ -7820,7 +7967,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx +@@ -7795,7 +7937,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx return cp; } } @@ -349,7 +326,7 @@ index b3bef4b..7025e6e 100644 if (lo > 0) { ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; -@@ -7842,7 +7989,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx +@@ -7817,7 +7959,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx deadlocking the AX server thread. This is prevented by: 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every @@ -358,7 +335,7 @@ index b3bef4b..7025e6e 100644 2. All dispatch_sync blocks run on the main thread where no concurrent Lisp code can modify state between checks. 3. block_input prevents timer events and process output from -@@ -8196,6 +8343,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber +@@ -8171,6 +8313,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber return [self lineForAXIndex:point_idx]; } @@ -409,7 +386,7 @@ index b3bef4b..7025e6e 100644 - (NSRange)accessibilityRangeForLine:(NSInteger)line { if (![NSThread isMainThread]) -@@ -8417,7 +8608,7 @@ - (NSRect)accessibilityFrame +@@ -8392,7 +8578,7 @@ - (NSRect)accessibilityFrame /* =================================================================== @@ -418,7 +395,7 @@ index b3bef4b..7025e6e 100644 These methods notify VoiceOver of text and selection changes. Called from the redisplay cycle (postAccessibilityUpdates). -@@ -8432,7 +8623,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8407,7 +8593,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point if (point > self.cachedPoint && point - self.cachedPoint == 1) { @@ -427,7 +404,7 @@ index b3bef4b..7025e6e 100644 [self invalidateTextCache]; [self ensureTextCache]; if (cachedText) -@@ -8451,7 +8642,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8426,7 +8612,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point /* Update cachedPoint here so the selection-move branch does NOT fire for point changes caused by edits. WebKit and Chromium never send both ValueChanged and SelectedTextChanged for the @@ -436,7 +413,7 @@ index b3bef4b..7025e6e 100644 self.cachedPoint = point; NSDictionary *change = @{ -@@ -8784,14 +8975,112 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -8759,14 +8945,72 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ @@ -454,7 +431,6 @@ index b3bef4b..7025e6e 100644 + if (chars_modiff != self.cachedCharsModiff) + { + self.cachedCharsModiff = chars_modiff; -+ self.emacsView->overlayZoomActive = NO; + [self postTextChangedNotification:point]; + } + } @@ -502,46 +478,7 @@ index b3bef4b..7025e6e 100644 + NSAccessibilityAnnouncementRequestedNotification, + annInfo); + -+ /* --- Zoom tracking for overlay candidates --- -+ Store the candidate row rect so draw_window_cursor -+ focuses Zoom there instead of on the text cursor. -+ Cleared when the user types (chars_modiff change). -+ -+ Use default line height to compute the Y offset: -+ row 0 is the input line, overlay candidates start -+ from row 1. This avoids fragile glyph matrix row -+ index mapping which can be off when group titles -+ or wrapped lines shift row numbering. */ -+ if (selected_line >= 0) -+ { -+ struct window *w2 = [self validWindow]; -+ if (w2) -+ { -+ EmacsView *view = self.emacsView; -+ struct frame *f2 = XFRAME (w2->frame); -+ int line_h = FRAME_LINE_HEIGHT (f2); -+ int y_off = (selected_line + 1) * line_h; -+ -+ if (y_off < w2->pixel_height) -+ { -+ view->overlayZoomRect = NSMakeRect ( -+ WINDOW_TEXT_TO_FRAME_PIXEL_X (w2, 0), -+ WINDOW_TO_FRAME_PIXEL_Y (w2, y_off), -+ FRAME_COLUMN_WIDTH (f2), -+ line_h); -+ view->overlayZoomActive = YES; -+ } -+ } -+ } + } -+ } -+ else -+ { -+ /* No selected candidate --- overlay completion ended -+ (minibuffer exit, C-g, etc.) or overlay has no -+ recognizable selection face. Return Zoom to the -+ text cursor. */ -+ self.emacsView->overlayZoomActive = NO; + } } @@ -551,7 +488,7 @@ index b3bef4b..7025e6e 100644 per the WebKit/Chromium pattern. */ else if (point != self.cachedPoint || markActive != self.cachedMarkActive) { -@@ -8961,7 +9250,7 @@ - (NSRect)accessibilityFrame +@@ -8936,7 +9180,7 @@ - (NSRect)accessibilityFrame /* =================================================================== @@ -560,7 +497,7 @@ index b3bef4b..7025e6e 100644 =================================================================== */ /* Scan visible range of window W for interactive spans. -@@ -9152,7 +9441,7 @@ - (NSRect) accessibilityFrame +@@ -9127,7 +9371,7 @@ - (NSRect) accessibilityFrame - (BOOL) isAccessibilityFocused { /* Read the cached point stored by EmacsAccessibilityBuffer on the main @@ -569,7 +506,7 @@ index b3bef4b..7025e6e 100644 EmacsAccessibilityBuffer *pb = self.parentBuffer; if (!pb) return NO; -@@ -9169,7 +9458,7 @@ - (void) setAccessibilityFocused: (BOOL) focused +@@ -9144,7 +9388,7 @@ - (void) setAccessibilityFocused: (BOOL) focused dispatch_async (dispatch_get_main_queue (), ^{ /* lwin is a Lisp_Object captured by value. This is GC-safe because Lisp_Objects are tagged integers/pointers that @@ -578,7 +515,7 @@ index b3bef4b..7025e6e 100644 Emacs. The WINDOW_LIVE_P check below guards against the window being deleted between capture and execution. */ if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) -@@ -9195,7 +9484,7 @@ - (void) setAccessibilityFocused: (BOOL) focused +@@ -9170,7 +9414,7 @@ - (void) setAccessibilityFocused: (BOOL) focused @end @@ -587,7 +524,7 @@ index b3bef4b..7025e6e 100644 Methods are kept here (same .m file) so they access the ivars declared in the @interface ivar block. */ @implementation EmacsAccessibilityBuffer (InteractiveSpans) -@@ -10515,13 +10804,13 @@ - (NSSize)windowWillResize: (NSWindow *)sender toSize: (NSSize)frameSize +@@ -10490,13 +10734,13 @@ - (NSSize)windowWillResize: (NSWindow *)sender toSize: (NSSize)frameSize if (old_title == 0) { char *t = strdup ([[[self window] title] UTF8String]); @@ -603,7 +540,7 @@ index b3bef4b..7025e6e 100644 [window setTitle: [NSString stringWithUTF8String: size_title]]; [window display]; xfree (size_title); -@@ -11917,7 +12206,7 @@ - (int) fullscreenState +@@ -11892,7 +12136,7 @@ - (int) fullscreenState if (WINDOW_LEAF_P (w)) { @@ -612,7 +549,7 @@ index b3bef4b..7025e6e 100644 EmacsAccessibilityBuffer *elem = [existing objectForKey:[NSValue valueWithPointer:w]]; if (!elem) -@@ -11951,7 +12240,7 @@ - (int) fullscreenState +@@ -11926,7 +12170,7 @@ - (int) fullscreenState } else { @@ -621,7 +558,7 @@ index b3bef4b..7025e6e 100644 Lisp_Object child = w->contents; while (!NILP (child)) { -@@ -12063,7 +12352,7 @@ - (void)postAccessibilityUpdates +@@ -12038,7 +12282,7 @@ - (void)postAccessibilityUpdates accessibilityUpdating = YES; /* Detect window tree change (split, delete, new buffer). Compare @@ -630,7 +567,7 @@ index b3bef4b..7025e6e 100644 Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); if (!EQ (curRoot, lastRootWindow)) { -@@ -12072,12 +12361,12 @@ - (void)postAccessibilityUpdates +@@ -12047,12 +12291,12 @@ - (void)postAccessibilityUpdates } /* If tree is stale, rebuild FIRST so we don't iterate freed diff --git a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch index f9995a2..61abfa7 100644 --- a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch +++ b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch @@ -1,4 +1,4 @@ -From 992bd78124b769fdcb4c1e3231afd13349e066c9 Mon Sep 17 00:00:00 2001 +From f842c3813d3b9fd106d668fa872b1cb3b187e9cf Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 16:01:29 +0100 Subject: [PATCH 8/8] ns: announce child frame completion candidates for @@ -40,11 +40,11 @@ childFrameCompletionActive flag. (EmacsView postAccessibilityUpdates): Dispatch to child frame handler, refocus parent buffer element when child frame closes. --- - doc/emacs/macos.texi | 6 - + doc/emacs/macos.texi | 6 -- etc/NEWS | 4 +- - src/nsterm.h | 4 +- - src/nsterm.m | 310 ++++++++++++++++++++++++++++++++----------- - 4 files changed, 238 insertions(+), 86 deletions(-) + src/nsterm.h | 5 + + src/nsterm.m | 227 ++++++++++++++++++++++++++++++++++++++++++- + 4 files changed, 233 insertions(+), 9 deletions(-) diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi index 4825cf9..97777e2 100644 @@ -86,20 +86,21 @@ index e76ee93..c3e0b40 100644 for the *Completions* buffer. The implementation uses a virtual accessibility tree with per-window elements, hybrid SelectedTextChanged diff --git a/src/nsterm.h b/src/nsterm.h -index a007925..80bc8ff 100644 +index 19a7e7a..49e8f00 100644 --- a/src/nsterm.h +++ b/src/nsterm.h -@@ -596,8 +596,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) +@@ -596,6 +596,10 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) BOOL accessibilityUpdating; @public /* Accessed by ns_draw_phys_cursor (C function). */ - NSRect lastAccessibilityCursorRect; -- BOOL overlayZoomActive; -- NSRect overlayZoomRect; + NSRect lastCursorRect; + BOOL childFrameCompletionActive; ++ char *childFrameLastCandidate; ++ struct buffer *childFrameLastBuffer; ++ EMACS_INT childFrameLastModiff; #endif BOOL font_panel_active; NSFont *font_panel_result; -@@ -661,6 +660,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) +@@ -659,6 +663,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) - (void)rebuildAccessibilityTree; - (void)invalidateAccessibilityTree; - (void)postAccessibilityUpdates; @@ -108,60 +109,10 @@ index a007925..80bc8ff 100644 @end diff --git a/src/nsterm.m b/src/nsterm.m -index 7025e6e..d5d33b0 100644 +index 3d72b5d..fefe2a7 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -3239,44 +3239,14 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. - r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); - - #ifdef NS_IMPL_COCOA -- /* Accessibility: store cursor rect for Zoom and bounds queries. -- Skipped when ns-accessibility-enabled is nil to avoid overhead. -- VoiceOver notifications are handled solely by -- postAccessibilityUpdates (called from ns_update_end) -- to avoid duplicate notifications and mid-redisplay fragility. */ -+ /* Accessibility: store cursor rect for VoiceOver bounds queries. -+ accessibilityBoundsForRange: / accessibilityFrameForRange: -+ use this as a fallback when no valid window/glyph data is -+ available. Skipped when ns-accessibility-enabled is nil. */ - { - EmacsView *view = FRAME_NS_VIEW (f); - if (view && on_p && active_p && ns_accessibility_enabled) -- { -- view->lastAccessibilityCursorRect = r; -- -- /* Tell macOS Zoom where the cursor is. UAZoomChangeFocus() -- expects top-left origin (CG coordinate space). -- These APIs are available since macOS 10.4 (Universal Access -- framework, linked via ApplicationServices umbrella). */ --#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ -- && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 -- if (UAZoomEnabled ()) -- { -- /* When overlay completion is active (e.g. Vertico), -- focus Zoom on the selected candidate row instead -- of the text cursor. */ -- NSRect zoomSrc = view->overlayZoomActive -- ? view->overlayZoomRect : r; -- NSRect windowRect = [view convertRect:zoomSrc 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 */ -- } -+ view->lastAccessibilityCursorRect = r; - } - #endif - -@@ -7058,6 +7028,112 @@ visual line index for Zoom (skip whitespace-only lines +@@ -7028,6 +7028,112 @@ visual line index for Zoom (skip whitespace-only lines return nil; } @@ -274,63 +225,7 @@ index 7025e6e..d5d33b0 100644 /* Build accessibility text for window W, skipping invisible text. Populates *OUT_START with the buffer start charpos. Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS -@@ -8988,7 +9064,6 @@ Text property changes (e.g. face updates from - if (chars_modiff != self.cachedCharsModiff) - { - self.cachedCharsModiff = chars_modiff; -- self.emacsView->overlayZoomActive = NO; - [self postTextChangedNotification:point]; - } - } -@@ -9036,47 +9111,8 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property - NSAccessibilityAnnouncementRequestedNotification, - annInfo); - -- /* --- Zoom tracking for overlay candidates --- -- Store the candidate row rect so draw_window_cursor -- focuses Zoom there instead of on the text cursor. -- Cleared when the user types (chars_modiff change). -- -- Use default line height to compute the Y offset: -- row 0 is the input line, overlay candidates start -- from row 1. This avoids fragile glyph matrix row -- index mapping which can be off when group titles -- or wrapped lines shift row numbering. */ -- if (selected_line >= 0) -- { -- struct window *w2 = [self validWindow]; -- if (w2) -- { -- EmacsView *view = self.emacsView; -- struct frame *f2 = XFRAME (w2->frame); -- int line_h = FRAME_LINE_HEIGHT (f2); -- int y_off = (selected_line + 1) * line_h; -- -- if (y_off < w2->pixel_height) -- { -- view->overlayZoomRect = NSMakeRect ( -- WINDOW_TEXT_TO_FRAME_PIXEL_X (w2, 0), -- WINDOW_TO_FRAME_PIXEL_Y (w2, y_off), -- FRAME_COLUMN_WIDTH (f2), -- line_h); -- view->overlayZoomActive = YES; -- } -- } -- } - } - } -- else -- { -- /* No selected candidate --- overlay completion ended -- (minibuffer exit, C-g, etc.) or overlay has no -- recognizable selection face. Return Zoom to the -- text cursor. */ -- self.emacsView->overlayZoomActive = NO; -- } - } - - /* --- Cursor moved or selection changed --- -@@ -12336,6 +12372,80 @@ - (id)accessibilityFocusedUIElement +@@ -12266,6 +12372,77 @@ - (id)accessibilityFocusedUIElement The existing elements carry cached state (modiff, point) from the previous redisplay cycle. Rebuilding first would create fresh elements with current values, making change detection impossible. */ @@ -341,9 +236,6 @@ index 7025e6e..d5d33b0 100644 + in the minibuffer. */ +- (void)announceChildFrameCompletion +{ -+ static char *lastCandidate; -+ static struct buffer *lastBuffer; -+ static EMACS_INT lastModiff; + + /* Validate frame state --- child frames may be partially + initialized during creation. */ @@ -359,10 +251,10 @@ index 7025e6e..d5d33b0 100644 + also guards against re-entrance: if Lisp calls below + trigger redisplay, the modiff check short-circuits. */ + EMACS_INT modiff = BUF_MODIFF (b); -+ if (b == lastBuffer && modiff == lastModiff) ++ if (b == childFrameLastBuffer && modiff == childFrameLastModiff) + return; -+ lastBuffer = b; -+ lastModiff = modiff; ++ childFrameLastBuffer = b; ++ childFrameLastModiff = modiff; + + /* Skip buffers larger than a typical completion popup. + This avoids scanning eldoc, which-key, or other child @@ -379,10 +271,10 @@ index 7025e6e..d5d33b0 100644 + + /* Deduplicate --- avoid re-announcing the same candidate. */ + const char *cstr = [candidate UTF8String]; -+ if (lastCandidate && strcmp (cstr, lastCandidate) == 0) ++ if (childFrameLastCandidate && strcmp (cstr, childFrameLastCandidate) == 0) + return; -+ xfree (lastCandidate); -+ lastCandidate = xstrdup (cstr); ++ xfree (childFrameLastCandidate); ++ childFrameLastCandidate = xstrdup (cstr); + + NSDictionary *annInfo = @{ + NSAccessibilityAnnouncementKey: candidate, @@ -411,7 +303,7 @@ index 7025e6e..d5d33b0 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12346,11 +12456,59 @@ - (void)postAccessibilityUpdates +@@ -12276,11 +12453,59 @@ - (void)postAccessibilityUpdates /* Re-entrance guard: VoiceOver callbacks during notification posting can trigger redisplay, which calls ns_update_end, which calls us