From b283068f82b799241d6b0f300979889aa1cc1f80 Mon Sep 17 00:00:00 2001 From: Daneel Date: Sun, 1 Mar 2026 03:38:58 +0100 Subject: [PATCH] patches: add Zoom completion tracking (overlay + child frame) Zoom patch 0000 now tracks completion candidates: - Overlay: Vertico, Icomplete, Ivy (face heuristic on before-string) - Child frame: Corfu, Company-box (scan buffer text for selected face) Also fixes duplicate lastCursorRect ivar when applied with VoiceOver. --- ...-with-macOS-Zoom-for-cursor-tracking.patch | 273 +++++++++++++++++- ...lity-base-classes-and-text-extractio.patch | 10 +- ...fer-accessibility-element-core-proto.patch | 6 +- ...tification-dispatch-and-mode-line-el.patch | 6 +- ...ive-span-elements-for-Tab-navigation.patch | 6 +- ...essibility-with-EmacsView-and-redisp.patch | 57 ++-- ...r-accessibility-section-to-macOS-app.patch | 2 +- ...lay-completion-candidates-for-VoiceO.patch | 52 ++-- ...d-frame-completion-candidates-for-Vo.patch | 14 +- 9 files changed, 340 insertions(+), 86 deletions(-) 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 b4bdd98..2e6b81c 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,4 +1,4 @@ -From 8a45d478b23b3bc2a1ae039493ba90b07eb89c72 Mon Sep 17 00:00:00 2001 +From 45076d26a15ae82b489349d481f3c1a1792730a5 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 22:39:35 +0100 Subject: [PATCH 1/9] ns: integrate with macOS Zoom for cursor tracking @@ -6,32 +6,46 @@ Subject: [PATCH 1/9] 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. +Basic cursor tracking: * 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. +was not physically redrawn (e.g. after C-x o window switch). +Gated by zoomCursorUpdated to avoid double calls. + +Completion candidate tracking: +* src/nsterm.m (ns_zoom_face_is_selected): New predicate. +Match 'current', 'selected', and 'selection' in face symbol +names to identify the highlighted completion candidate. +(ns_zoom_find_overlay_candidate_line): Scan overlay +before-string/after-string for the selected candidate line. +Handles Vertico, Icomplete, Ivy, and similar overlay frameworks. +(ns_zoom_find_child_frame_candidate): Scan child frame buffer +text for the selected candidate. Handles Corfu, Company-box, +and similar child frame frameworks. +(ns_zoom_track_completion): Called from ns_update_end after +cursor tracking. Overrides Zoom focus to the selected +completion candidate when one is found. 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. +top-left origin. Tested on macOS 14 with Zoom enabled: cursor tracking works across -window splits, switches (C-x o), and normal navigation. +window splits, switches (C-x o), and completion frameworks. --- - etc/NEWS | 8 +++++++ - src/nsterm.h | 6 +++++ - src/nsterm.m | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ - 3 files changed, 82 insertions(+) + etc/NEWS | 11 ++ + src/nsterm.h | 6 ++ + src/nsterm.m | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 302 insertions(+) diff --git a/etc/NEWS b/etc/NEWS -index ef36df5..f10d17e 100644 +index ef36df5..80661a9 100644 --- a/etc/NEWS +++ b/etc/NEWS -@@ -82,6 +82,14 @@ other directory on your system. You can also invoke the +@@ -82,6 +82,17 @@ other directory on your system. You can also invoke the * Changes in Emacs 31.1 @@ -41,7 +55,10 @@ index ef36df5..f10d17e 100644 +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. ++switches. Completion frameworks (Vertico, Icomplete, Ivy for overlay ++candidates; Corfu, Company-box for child frame popups) are also ++tracked: Zoom follows the selected candidate rather than the text ++cursor during completion. + +++ ** 'line-spacing' now supports specifying spacing above the line. @@ -64,10 +81,228 @@ index 7c1ee4c..ea6e7ba 100644 } diff --git a/src/nsterm.m b/src/nsterm.m -index 74e4ad5..cd721c8 100644 +index 74e4ad5..05ec3d1 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -1104,6 +1104,35 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -1081,6 +1081,217 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) + } + + ++ ++#ifdef NS_IMPL_COCOA ++#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ ++ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 ++ ++/* Check whether FACE (a Lisp symbol or list) has a name suggesting ++ it marks the currently selected completion candidate. Matches ++ vertico-current, icomplete-selected-match, ivy-current-match, ++ company-tooltip-selection, corfu-current, and similar. */ ++static bool ++ns_zoom_face_is_selected (Lisp_Object face) ++{ ++ if (SYMBOLP (face) && !NILP (face)) ++ { ++ const char *name = SSDATA (SYMBOL_NAME (face)); ++ if (strstr (name, "current") ++ || strstr (name, "selected") ++ || strstr (name, "selection")) ++ return true; ++ } ++ /* Handle face list (face1 face2 ...). */ ++ if (CONSP (face)) ++ { ++ Lisp_Object tail; ++ for (tail = face; CONSP (tail); tail = XCDR (tail)) ++ if (ns_zoom_face_is_selected (XCAR (tail))) ++ return true; ++ } ++ return false; ++} ++ ++/* Scan overlay before-string / after-string properties in the ++ selected window for a completion candidate with a "selected" ++ face. Return the 0-based visual line index of the selected ++ candidate, or -1 if none found. */ ++static int ++ns_zoom_find_overlay_candidate_line (struct window *w) ++{ ++ struct buffer *b = XBUFFER (w->contents); ++ ptrdiff_t beg = marker_position (w->start); ++ ptrdiff_t end = ZV_S (b); ++ Lisp_Object overlays = Foverlays_in (make_fixnum (beg), ++ make_fixnum (end)); ++ Lisp_Object tail; ++ ++ for (tail = overlays; CONSP (tail); tail = XCDR (tail)) ++ { ++ Lisp_Object ov = XCAR (tail); ++ Lisp_Object str = Foverlay_get (ov, Qbefore_string); ++ ++ if (NILP (str)) ++ str = Foverlay_get (ov, Qafter_string); ++ if (!STRINGP (str) || SCHARS (str) < 2) ++ continue; ++ ++ /* Walk the string line by line, checking faces. */ ++ ptrdiff_t len = SCHARS (str); ++ int line = 0; ++ ptrdiff_t line_start = 0; ++ ++ for (ptrdiff_t i = 0; i <= len; i++) ++ { ++ bool at_newline = (i == len ++ || SREF (str, i) == '\n'); ++ if (at_newline && i > line_start) ++ { ++ /* Check the face at line_start. */ ++ Lisp_Object face ++ = Fget_text_property (make_fixnum (line_start), ++ Qface, str); ++ if (ns_zoom_face_is_selected (face)) ++ return line; ++ line++; ++ line_start = i + 1; ++ } ++ else if (at_newline) ++ { ++ line++; ++ line_start = i + 1; ++ } ++ } ++ } ++ return -1; ++} ++ ++/* Scan child frames for a completion popup with a selected ++ candidate. Return the 0-based line index, or -1 if none. ++ Set *CHILD_FRAME to the child frame if found. */ ++static int ++ns_zoom_find_child_frame_candidate (struct frame *f, ++ struct frame **child_frame) ++{ ++ Lisp_Object frames, tail; ++ ++ FOR_EACH_FRAME (tail, frames) ++ { ++ struct frame *cf = XFRAME (frames); ++ if (!FRAME_NS_P (cf) || !FRAME_LIVE_P (cf)) ++ continue; ++ if (FRAME_PARENT_FRAME (cf) != f) ++ continue; ++ /* Small buffer = likely completion popup. */ ++ struct buffer *b = XBUFFER (cf->current_buffer); ++ if (BUF_ZV (b) - BUF_BEGV (b) > 10000) ++ continue; ++ ++ ptrdiff_t beg = BUF_BEGV (b); ++ ptrdiff_t zv = BUF_ZV (b); ++ int line = 0; ++ ++ ptrdiff_t pos = beg; ++ while (pos < zv) ++ { ++ Lisp_Object face ++ = Fget_char_property (make_fixnum (pos), Qface, ++ cf->current_buffer); ++ if (ns_zoom_face_is_selected (face)) ++ { ++ *child_frame = cf; ++ return line; ++ } ++ /* Advance to next line. */ ++ ptrdiff_t next = find_newline (pos, -1, zv, -1, ++ 1, NULL, NULL, false); ++ if (next <= pos) ++ break; ++ pos = next; ++ line++; ++ } ++ } ++ return -1; ++} ++ ++/* Update Zoom focus based on completion candidates. ++ Called from ns_update_end after normal cursor tracking. ++ If a completion candidate is selected (overlay or child frame), ++ move Zoom to that candidate instead of the text cursor. */ ++static void ++ns_zoom_track_completion (struct frame *f, EmacsView *view) ++{ ++ if (!UAZoomEnabled ()) ++ return; ++ ++ struct window *w = XWINDOW (f->selected_window); ++ int line_h = FRAME_LINE_HEIGHT (f); ++ ++ /* 1. Check overlay completions (Vertico, Icomplete, Ivy). */ ++ int ov_line = ns_zoom_find_overlay_candidate_line (w); ++ if (ov_line >= 0) ++ { ++ /* Overlay candidates typically start after the input line, ++ so the visual offset is (ov_line + 1) * line_h from ++ the window top. */ ++ int y_off = (ov_line + 1) * line_h; ++ if (y_off < w->pixel_height) ++ { ++ NSRect r = NSMakeRect ( ++ WINDOW_TEXT_TO_FRAME_PIXEL_X (w, 0), ++ WINDOW_TO_FRAME_PIXEL_Y (w, y_off), ++ FRAME_COLUMN_WIDTH (f), ++ line_h); ++ ++ 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); ++ return; ++ } ++ } ++ ++ /* 2. Check child frame completions (Corfu, Company-box). */ ++ struct frame *cf = NULL; ++ int cf_line = ns_zoom_find_child_frame_candidate (f, &cf); ++ if (cf_line >= 0 && cf) ++ { ++ EmacsView *cv = FRAME_NS_VIEW (cf); ++ struct window *cw ++ = XWINDOW (cf->selected_window); ++ int cf_line_h = FRAME_LINE_HEIGHT (cf); ++ int y_off = cf_line * cf_line_h; ++ ++ NSRect r = NSMakeRect ( ++ WINDOW_TEXT_TO_FRAME_PIXEL_X (cw, 0), ++ WINDOW_TO_FRAME_PIXEL_Y (cw, y_off), ++ FRAME_COLUMN_WIDTH (cf), ++ cf_line_h); ++ ++ NSRect windowRect = [cv convertRect:r toView:nil]; ++ NSRect screenRect ++ = [[cv 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 + ns_update_end (struct frame *f) + /* -------------------------------------------------------------------------- +@@ -1104,6 +1315,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) unblock_input (); ns_updating_frame = NULL; @@ -99,11 +334,17 @@ index 74e4ad5..cd721c8 100644 + if (view) + view->zoomCursorUpdated = NO; +#endif ++ ++ /* Track completion candidates for Zoom (overlay and child frame). ++ Runs after cursor tracking so the selected candidate overrides ++ the default cursor position. */ ++ if (view) ++ ns_zoom_track_completion (f, view); +#endif /* NS_IMPL_COCOA */ } static void -@@ -3232,6 +3261,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. +@@ -3232,6 +3478,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)); 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 cf022c0..4d61b25 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 67bf4786bb3896241bf7ab75cdada0e3da924ec5 Mon Sep 17 00:00:00 2001 +From 59ec39bdda532d02c748a7e27f9a90ba3be3c338 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 2/9] ns: add accessibility base classes and text extraction @@ -188,7 +188,7 @@ index ea6e7ba..6e830de 100644 diff --git a/src/nsterm.m b/src/nsterm.m -index cd721c8..1320a9f 100644 +index 05ec3d1..a4d0e02 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -199,7 +199,7 @@ index cd721c8..1320a9f 100644 #include "systime.h" #include "character.h" #include "xwidget.h" -@@ -6924,6 +6925,430 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg +@@ -7141,6 +7142,430 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg } #endif @@ -630,7 +630,7 @@ index cd721c8..1320a9f 100644 /* ========================================================================== EmacsView implementation -@@ -11380,6 +11805,28 @@ Convert an X font name (XLFD) to an NS font name. +@@ -11597,6 +12022,28 @@ Convert an X font name (XLFD) to an NS font name. DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); @@ -659,7 +659,7 @@ index cd721c8..1320a9f 100644 Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier)); Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier)); -@@ -11528,6 +11975,15 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with +@@ -11745,6 +12192,15 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with This variable is ignored on Mac OS X < 10.7 and GNUstep. */); ns_use_srgb_colorspace = YES; 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 146fe30..4e5e5fa 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 80e590420ce74bb886849d91c245d0c0bece39bf Mon Sep 17 00:00:00 2001 +From 80f6de2020c50717e2238528e6241e00017f7230 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 3/9] ns: implement buffer accessibility element (core @@ -22,10 +22,10 @@ line-by-line navigation, word/character announcements. 1 file changed, 1097 insertions(+) diff --git a/src/nsterm.m b/src/nsterm.m -index 1320a9f..ab9a287 100644 +index a4d0e02..e281073 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -7346,6 +7346,1103 @@ - (id)accessibilityTopLevelUIElement +@@ -7563,6 +7563,1103 @@ - (id)accessibilityTopLevelUIElement @end 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 2abc762..87b9d6f 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 afadafa16eef6e8e6d01ef4f340c451132198ba8 Mon Sep 17 00:00:00 2001 +From 672223b6ced41898156aa476b8c77b27d3719e4a Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 4/9] ns: add buffer notification dispatch and mode-line @@ -24,10 +24,10 @@ region selection feedback, completion popups, mode-line reading. 1 file changed, 545 insertions(+) diff --git a/src/nsterm.m b/src/nsterm.m -index ab9a287..330667d 100644 +index e281073..a29f039 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -8443,6 +8443,551 @@ - (NSRect)accessibilityFrame +@@ -8660,6 +8660,551 @@ - (NSRect)accessibilityFrame @end 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 7b87e6a..a0ccd1f 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 010ec7a6e568653e12b904c5bb812a1b3c8e52cf Mon Sep 17 00:00:00 2001 +From 64b373bebadd9b9c52c90da297173ea108c6e394 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 5/9] ns: add interactive span elements for Tab navigation @@ -17,10 +17,10 @@ Tested on macOS 14. Verified: Tab-cycling through org-mode links, 1 file changed, 286 insertions(+) diff --git a/src/nsterm.m b/src/nsterm.m -index 330667d..907ce47 100644 +index a29f039..86bb7b6 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -8988,6 +8988,292 @@ - (NSRect)accessibilityFrame +@@ -9205,6 +9205,292 @@ - (NSRect)accessibilityFrame @end 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 347a720..51b0abd 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 f10036eeadf681cd87bbec7ec0581b572053c38a Mon Sep 17 00:00:00 2001 +From be594f584a94e6fccd649be029c1af30242a12ee Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 6/9] ns: integrate accessibility with EmacsView and redisplay @@ -23,14 +23,14 @@ block cursor, org-mode folded headings, indirect buffers. Known limitations documented in patch 6 Texinfo node. --- etc/NEWS | 13 ++ - src/nsterm.m | 369 +++++++++++++++++++++++++++++++++++++++++++++++++-- - 2 files changed, 374 insertions(+), 8 deletions(-) + src/nsterm.m | 376 +++++++++++++++++++++++++++++++++++++++++++++++++-- + 2 files changed, 376 insertions(+), 13 deletions(-) diff --git a/etc/NEWS b/etc/NEWS -index f10d17e..f48d05b 100644 +index 80661a9..2b1f9e6 100644 --- a/etc/NEWS +++ b/etc/NEWS -@@ -4397,6 +4397,19 @@ allowing Emacs users access to speech recognition utilities. +@@ -4400,6 +4400,19 @@ allowing Emacs users access to speech recognition utilities. Note: Accepting this permission allows the use of system APIs, which may send user data to Apple's speech recognition servers. @@ -51,20 +51,28 @@ index f10d17e..f48d05b 100644 ** Re-introduced dictation, lost in Emacs v30 (macOS). We lost macOS dictation in v30 when migrating to NSTextInputClient. diff --git a/src/nsterm.m b/src/nsterm.m -index 907ce47..d813274 100644 +index 86bb7b6..2e8ae20 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -1133,6 +1133,9 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -1343,13 +1343,16 @@ so the visual offset is (ov_line + 1) * line_h from + } if (view) view->zoomCursorUpdated = NO; - #endif +-#endif + + /* Track completion candidates for Zoom (overlay and child frame). + Runs after cursor tracking so the selected candidate overrides + the default cursor position. */ + if (view) + ns_zoom_track_completion (f, view); ++#endif + + /* Post accessibility notifications after each redisplay cycle. */ + [view postAccessibilityUpdates]; #endif /* NS_IMPL_COCOA */ } -@@ -3263,11 +3266,11 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. +@@ -3480,15 +3483,12 @@ 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 @@ -73,15 +81,20 @@ index 907ce47..d813274 100644 - element to keep the zoomed viewport centered on the cursor. - - Coordinate conversion: -+ /* Store cursor rect and inform macOS Zoom / VoiceOver. -+ lastCursorRect is used by: -+ - Zoom: UAZoomChangeFocus below (unconditional when active) -+ - VoiceOver: accessibilityBoundsForRange: fallback +- EmacsView pixels (AppKit, flipped, top-left origin) +- -> NSWindow (convertRect:toView:nil) +- -> NSScreen (convertRectToScreen:) +- -> CGRect with y-flip for CoreGraphics top-left origin. */ ++ /* Store cursor rect for Zoom and VoiceOver bounds queries. ++ Zoom: UAZoomChangeFocus below. ++ VoiceOver: accessibilityBoundsForRange: fallback. + Coordinate conversion for Zoom: - EmacsView pixels (AppKit, flipped, top-left origin) - -> NSWindow (convertRect:toView:nil) - -> NSScreen (convertRectToScreen:) -@@ -7349,7 +7352,6 @@ - (id)accessibilityTopLevelUIElement ++ EmacsView (AppKit, flipped) -> NSWindow -> NSScreen ++ -> CGRect with y-flip for CoreGraphics top-left origin. */ + { + EmacsView *view = FRAME_NS_VIEW (f); + if (view && on_p && active_p) +@@ -7566,7 +7566,6 @@ - (id)accessibilityTopLevelUIElement @@ -89,7 +102,7 @@ index 907ce47..d813274 100644 static BOOL ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, ptrdiff_t *out_start, -@@ -8444,7 +8446,6 @@ - (NSRect)accessibilityFrame +@@ -8661,7 +8660,6 @@ - (NSRect)accessibilityFrame @end @@ -97,7 +110,7 @@ index 907ce47..d813274 100644 /* =================================================================== EmacsAccessibilityBuffer (Notifications) — AX event dispatch -@@ -8989,7 +8990,6 @@ - (NSRect)accessibilityFrame +@@ -9206,7 +9204,6 @@ - (NSRect)accessibilityFrame @end @@ -105,7 +118,7 @@ index 907ce47..d813274 100644 /* =================================================================== EmacsAccessibilityInteractiveSpan — helpers and implementation =================================================================== */ -@@ -9319,6 +9319,7 @@ - (void)dealloc +@@ -9536,6 +9533,7 @@ - (void)dealloc [layer release]; #endif @@ -113,7 +126,7 @@ index 907ce47..d813274 100644 [[self menu] release]; [super dealloc]; } -@@ -10667,6 +10668,32 @@ - (void)windowDidBecomeKey /* for direct calls */ +@@ -10884,6 +10882,32 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -146,7 +159,7 @@ index 907ce47..d813274 100644 } -@@ -11904,6 +11931,332 @@ - (int) fullscreenState +@@ -12121,6 +12145,332 @@ - (int) fullscreenState return fs_state; } 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 ca831c6..3054641 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 40bf8a0163b876e1d8f71fa8bd3c4aaa2a28ad52 Mon Sep 17 00:00:00 2001 +From 10ae74cefffd3733b8ff60e07a2b7f0dcb4d62cb Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 7/9] 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 6f9c86e..b9ff257 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 456c6a734fb94943b1d9acfaa8c1ffd0f1321ab5 Mon Sep 17 00:00:00 2001 +From 43b0625a7b9ce634006811e814fff037af8a51cd Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 14:46:25 +0100 Subject: [PATCH 8/9] ns: announce overlay completion candidates for VoiceOver @@ -61,10 +61,10 @@ index 6e830de..2102fb9 100644 @property (nonatomic, assign) BOOL cachedMarkActive; @property (nonatomic, copy) NSString *cachedCompletionAnnouncement; diff --git a/src/nsterm.m b/src/nsterm.m -index d813274..9e089f3 100644 +index 2e8ae20..b9b2a80 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -6938,11 +6938,154 @@ Accessibility virtual elements (macOS / Cocoa only) +@@ -7152,11 +7152,154 @@ Accessibility virtual elements (macOS / Cocoa only) /* ---- Helper: extract buffer text for accessibility ---- */ @@ -220,7 +220,7 @@ index d813274..9e089f3 100644 static NSString * ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_visible_run **out_runs, NSUInteger *out_nruns) -@@ -7013,7 +7156,7 @@ Accessibility virtual elements (macOS / Cocoa only) +@@ -7227,7 +7370,7 @@ Accessibility virtual elements (macOS / Cocoa only) /* Extract this visible run's text. Use Fbuffer_substring_no_properties which correctly handles the @@ -229,7 +229,7 @@ index d813274..9e089f3 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)); -@@ -7094,7 +7237,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font) +@@ -7308,7 +7451,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 @@ -238,7 +238,7 @@ index d813274..9e089f3 100644 charposForAccessibilityIndex which handles invisible text. */ ptrdiff_t cp_start = charpos_start; ptrdiff_t cp_end = cp_start + charpos_len; -@@ -7573,6 +7716,7 @@ @implementation EmacsAccessibilityBuffer +@@ -7787,6 +7930,7 @@ @implementation EmacsAccessibilityBuffer @synthesize cachedOverlayModiff; @synthesize cachedTextStart; @synthesize cachedModiff; @@ -246,7 +246,7 @@ index d813274..9e089f3 100644 @synthesize cachedPoint; @synthesize cachedMarkActive; @synthesize cachedCompletionAnnouncement; -@@ -7670,7 +7814,7 @@ - (void)ensureTextCache +@@ -7884,7 +8028,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 @@ -255,7 +255,7 @@ index d813274..9e089f3 100644 write section at the end needs synchronization to protect against concurrent reads from AX server thread. */ eassert ([NSThread isMainThread]); -@@ -7683,16 +7827,15 @@ - (void)ensureTextCache +@@ -7897,16 +8041,15 @@ - (void)ensureTextCache return; ptrdiff_t modiff = BUF_MODIFF (b); @@ -278,7 +278,7 @@ index d813274..9e089f3 100644 && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 -@@ -7709,7 +7852,6 @@ - (void)ensureTextCache +@@ -7923,7 +8066,6 @@ - (void)ensureTextCache [cachedText release]; cachedText = [text retain]; cachedTextModiff = modiff; @@ -286,7 +286,7 @@ index d813274..9e089f3 100644 cachedTextStart = start; if (visibleRuns) -@@ -7774,7 +7916,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +@@ -7988,7 +8130,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 @@ -295,7 +295,7 @@ index d813274..9e089f3 100644 NSUInteger lo = 0, hi = visibleRunCount; while (lo < hi) { -@@ -7787,7 +7929,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +@@ -8001,7 +8143,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos else { /* Found: charpos is inside this run. Compute UTF-16 delta @@ -304,7 +304,7 @@ index d813274..9e089f3 100644 NSUInteger chars_in = (NSUInteger)(charpos - r->charpos); if (chars_in == 0 || !cachedText) return r->ax_start; -@@ -7812,10 +7954,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +@@ -8026,10 +8168,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos /* Convert accessibility string index to buffer charpos. Safe to call from any thread: uses only cachedText (NSString) and @@ -317,7 +317,7 @@ index d813274..9e089f3 100644 @synchronized (self) { if (visibleRunCount == 0) -@@ -7849,7 +7991,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx +@@ -8063,7 +8205,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx return cp; } } @@ -326,7 +326,7 @@ index d813274..9e089f3 100644 if (lo > 0) { ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; -@@ -7871,7 +8013,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx +@@ -8085,7 +8227,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 @@ -335,7 +335,7 @@ index d813274..9e089f3 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 -@@ -8225,6 +8367,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber +@@ -8439,6 +8581,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber return [self lineForAXIndex:point_idx]; } @@ -386,7 +386,7 @@ index d813274..9e089f3 100644 - (NSRange)accessibilityRangeForLine:(NSInteger)line { if (![NSThread isMainThread]) -@@ -8447,7 +8633,7 @@ - (NSRect)accessibilityFrame +@@ -8661,7 +8847,7 @@ - (NSRect)accessibilityFrame /* =================================================================== @@ -395,7 +395,7 @@ index d813274..9e089f3 100644 These methods notify VoiceOver of text and selection changes. Called from the redisplay cycle (postAccessibilityUpdates). -@@ -8462,7 +8648,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8676,7 +8862,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point if (point > self.cachedPoint && point - self.cachedPoint == 1) { @@ -404,7 +404,7 @@ index d813274..9e089f3 100644 [self invalidateTextCache]; [self ensureTextCache]; if (cachedText) -@@ -8481,7 +8667,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8695,7 +8881,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 @@ -413,7 +413,7 @@ index d813274..9e089f3 100644 self.cachedPoint = point; NSDictionary *change = @{ -@@ -8814,14 +9000,72 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9028,14 +9214,72 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ @@ -488,7 +488,7 @@ index d813274..9e089f3 100644 per the WebKit/Chromium pattern. */ else if (point != self.cachedPoint || markActive != self.cachedMarkActive) { -@@ -8991,7 +9235,7 @@ - (NSRect)accessibilityFrame +@@ -9205,7 +9449,7 @@ - (NSRect)accessibilityFrame /* =================================================================== @@ -497,7 +497,7 @@ index d813274..9e089f3 100644 =================================================================== */ /* Scan visible range of window W for interactive spans. -@@ -9199,7 +9443,7 @@ - (void) setAccessibilityFocused: (BOOL) focused +@@ -9413,7 +9657,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 @@ -506,7 +506,7 @@ index d813274..9e089f3 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))) -@@ -9225,7 +9469,7 @@ - (void) setAccessibilityFocused: (BOOL) focused +@@ -9439,7 +9683,7 @@ - (void) setAccessibilityFocused: (BOOL) focused @end @@ -515,7 +515,7 @@ index d813274..9e089f3 100644 Methods are kept here (same .m file) so they access the ivars declared in the @interface ivar block. */ @implementation EmacsAccessibilityBuffer (InteractiveSpans) -@@ -11947,7 +12191,7 @@ - (int) fullscreenState +@@ -12161,7 +12405,7 @@ - (int) fullscreenState if (WINDOW_LEAF_P (w)) { @@ -524,7 +524,7 @@ index d813274..9e089f3 100644 EmacsAccessibilityBuffer *elem = [existing objectForKey:[NSValue valueWithPointer:w]]; if (!elem) -@@ -11981,7 +12225,7 @@ - (int) fullscreenState +@@ -12195,7 +12439,7 @@ - (int) fullscreenState } else { @@ -533,7 +533,7 @@ index d813274..9e089f3 100644 Lisp_Object child = w->contents; while (!NILP (child)) { -@@ -12093,7 +12337,7 @@ - (void)postAccessibilityUpdates +@@ -12307,7 +12551,7 @@ - (void)postAccessibilityUpdates accessibilityUpdating = YES; /* Detect window tree change (split, delete, new buffer). Compare @@ -542,7 +542,7 @@ index d813274..9e089f3 100644 Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); if (!EQ (curRoot, lastRootWindow)) { -@@ -12102,12 +12346,12 @@ - (void)postAccessibilityUpdates +@@ -12316,12 +12560,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 e11e77f..e777709 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 16e60a514212d8e5e541eb731db242b2bcd1bca2 Mon Sep 17 00:00:00 2001 +From 62e619d508d4ce3b3bf0f8dd959041bcd9a75350 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 16:01:29 +0100 Subject: [PATCH 9/9] ns: announce child frame completion candidates for @@ -71,10 +71,10 @@ index 4825cf9..97777e2 100644 To disable the accessibility interface entirely (for instance, to eliminate overhead on systems where assistive technology is not in diff --git a/etc/NEWS b/etc/NEWS -index f48d05b..ec4b95e 100644 +index 2b1f9e6..8a40850 100644 --- a/etc/NEWS +++ b/etc/NEWS -@@ -4401,8 +4401,8 @@ send user data to Apple's speech recognition servers. +@@ -4404,8 +4404,8 @@ send user data to Apple's speech recognition servers. ** VoiceOver accessibility support on macOS. Emacs now exposes buffer content, cursor position, and interactive elements to the macOS accessibility subsystem (VoiceOver). This @@ -109,10 +109,10 @@ index 2102fb9..2fc4de4 100644 @end diff --git a/src/nsterm.m b/src/nsterm.m -index 9e089f3..1a623be 100644 +index b9b2a80..dc49417 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -7082,6 +7082,112 @@ visual line index for Zoom (skip whitespace-only lines +@@ -7296,6 +7296,112 @@ visual line index for Zoom (skip whitespace-only lines return nil; } @@ -225,7 +225,7 @@ index 9e089f3..1a623be 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 -@@ -12321,6 +12427,77 @@ - (id)accessibilityFocusedUIElement +@@ -12535,6 +12641,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. */ @@ -303,7 +303,7 @@ index 9e089f3..1a623be 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12331,11 +12508,59 @@ - (void)postAccessibilityUpdates +@@ -12545,11 +12722,59 @@ - (void)postAccessibilityUpdates /* Re-entrance guard: VoiceOver callbacks during notification posting can trigger redisplay, which calls ns_update_end, which calls us