From be4e0bb5beaa523790106a4fd45679c551bd5749 Mon Sep 17 00:00:00 2001 From: Daneel Date: Sat, 28 Feb 2026 15:51:06 +0100 Subject: [PATCH] patches: v7 0007 - Zoom left edge + selection face match Zoom rect now at text area left edge (WINDOW_TEXT_TO_FRAME_PIXEL_X) with cursor-width (FRAME_COLUMN_WIDTH) instead of full window width. Face matching adds 'selection' (company-tooltip-selection). --- ...lay-completion-candidates-for-VoiceO.patch | 99 ++++++++++--------- 1 file changed, 52 insertions(+), 47 deletions(-) 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 eaf2689..d9cf031 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 b37888bd77b77009e40b564b05164c584c9305ae Mon Sep 17 00:00:00 2001 +From 877cfc076d708c0222ef0b6e1b47da9e94a74af7 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 14:46:25 +0100 Subject: [PATCH] ns: announce overlay completion candidates for VoiceOver @@ -9,9 +9,10 @@ than buffer text. Without this patch, VoiceOver cannot read overlay-based completion UIs. Identify the selected candidate by scanning overlay strings for a -face whose symbol name contains "current" or "selected" --- this -matches vertico-current, icomplete-selected-match, ivy-current-match -and similar framework faces without hard-coding any specific name. +face whose symbol name contains "current", "selected", or +"selection" --- this matches vertico-current, icomplete-selected-match, +ivy-current-match, company-tooltip-selection, and similar framework +faces without hard-coding any specific name. Key implementation details: @@ -31,17 +32,18 @@ Key implementation details: Do not post SelectedTextChanged (that reads the AX text at cursor position, which is the minibuffer input, not the candidate). -- Zoom tracking: store the selected candidate's rect (computed from - FRAME_LINE_HEIGHT and the candidate's visual line index) in - overlayZoomRect. ns_draw_window_cursor checks overlayZoomActive - and uses the stored rect instead of the text cursor rect, keeping - Zoom focused on the candidate. The flag is cleared when the user - types (BUF_CHARS_MODIFF changes) or when no candidate is found +- Zoom tracking: store the selected candidate's rect (at the text + area left edge, computed from FRAME_LINE_HEIGHT) in overlayZoomRect. + ns_draw_window_cursor checks overlayZoomActive and uses the stored + rect instead of the text cursor rect, keeping Zoom focused on the + candidate line start. The flag is cleared when the user types + (BUF_CHARS_MODIFF changes) or when no candidate is found (minibuffer exit, C-g). * src/nsterm.h (EmacsView): Add overlayZoomActive, overlayZoomRect. (EmacsAccessibilityBuffer): Add cachedCharsModiff. -* src/nsterm.m (ns_ax_face_is_selected): New predicate. +* src/nsterm.m (ns_ax_face_is_selected): New predicate. Match +"current", "selected", and "selection" in face symbol names. (ns_ax_selected_overlay_text): New function. (ns_draw_window_cursor): Use overlayZoomRect when active. (EmacsAccessibilityBuffer ensureTextCache): Remove overlay_modiff. @@ -50,8 +52,8 @@ Independent overlay branch, BUF_CHARS_MODIFF gating, candidate announcement with overlay Zoom rect storage. --- src/nsterm.h | 3 + - src/nsterm.m | 316 +++++++++++++++++++++++++++++++++++++++++++++------ - 2 files changed, 283 insertions(+), 36 deletions(-) + src/nsterm.m | 319 +++++++++++++++++++++++++++++++++++++++++++++------ + 2 files changed, 286 insertions(+), 36 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 51c30ca..5c15639 100644 @@ -75,7 +77,7 @@ index 51c30ca..5c15639 100644 BOOL font_panel_active; NSFont *font_panel_result; diff --git a/src/nsterm.m b/src/nsterm.m -index 1780194..2fdea91 100644 +index 1780194..d13c5c7 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -3258,7 +3258,12 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, @@ -92,7 +94,7 @@ index 1780194..2fdea91 100644 NSRect screenRect = [[view window] convertRectToScreen:windowRect]; CGRect cgRect = NSRectToCGRect (screenRect); -@@ -6915,11 +6920,153 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) +@@ -6915,11 +6920,156 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) are truncated for accessibility purposes. */ #define NS_AX_TEXT_CAP 100000 @@ -106,10 +108,13 @@ index 1780194..2fdea91 100644 + if (SYMBOLP (face) && !NILP (face)) + { + const char *name = SSDATA (SYMBOL_NAME (face)); -+ /* Substring match is intentionally broad; false positives -+ are harmless since this runs only on overlay strings in -+ the minibuffer during completion. */ -+ if (strstr (name, "current") || strstr (name, "selected")) ++ /* Substring match is intentionally broad --- it catches ++ vertico-current, icomplete-selected-match, ivy-current-match, ++ company-tooltip-selection, and similar. False positives are ++ harmless since this runs only on overlay strings during ++ completion. */ ++ if (strstr (name, "current") || strstr (name, "selected") ++ || strstr (name, "selection")) + return true; + } + if (CONSP (face)) @@ -247,7 +252,7 @@ index 1780194..2fdea91 100644 static NSString * ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_visible_run **out_runs, NSUInteger *out_nruns) -@@ -6996,7 +7143,7 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, +@@ -6996,7 +7146,7 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, /* Extract this visible run's text. Use Fbuffer_substring_no_properties which correctly handles the @@ -256,7 +261,7 @@ index 1780194..2fdea91 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)); -@@ -7077,7 +7224,7 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, +@@ -7077,7 +7227,7 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, return NSZeroRect; /* charpos_start and charpos_len are already in buffer charpos @@ -265,7 +270,7 @@ index 1780194..2fdea91 100644 charposForAccessibilityIndex which handles invisible text. */ ptrdiff_t cp_start = charpos_start; ptrdiff_t cp_end = cp_start + charpos_len; -@@ -7556,6 +7703,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7556,6 +7706,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, @synthesize cachedOverlayModiff; @synthesize cachedTextStart; @synthesize cachedModiff; @@ -273,7 +278,7 @@ index 1780194..2fdea91 100644 @synthesize cachedPoint; @synthesize cachedMarkActive; @synthesize cachedCompletionAnnouncement; -@@ -7596,7 +7744,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7596,7 +7747,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, NSTRACE ("EmacsAccessibilityBuffer ensureTextCache"); /* This method is only called from the main thread (AX getters dispatch_sync to main first). Reads of cachedText/cachedTextModiff @@ -282,7 +287,7 @@ index 1780194..2fdea91 100644 write section at the end needs synchronization to protect against concurrent reads from AX server thread. */ eassert ([NSThread isMainThread]); -@@ -7609,16 +7757,15 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7609,16 +7760,15 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, return; ptrdiff_t modiff = BUF_MODIFF (b); @@ -305,7 +310,7 @@ index 1780194..2fdea91 100644 && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 -@@ -7635,7 +7782,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7635,7 +7785,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, [cachedText release]; cachedText = [text retain]; cachedTextModiff = modiff; @@ -313,7 +318,7 @@ index 1780194..2fdea91 100644 cachedTextStart = start; if (visibleRuns) -@@ -7661,7 +7807,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7661,7 +7810,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* 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 @@ -322,7 +327,7 @@ index 1780194..2fdea91 100644 NSUInteger lo = 0, hi = visibleRunCount; while (lo < hi) { -@@ -7674,7 +7820,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7674,7 +7823,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, else { /* Found: charpos is inside this run. Compute UTF-16 delta @@ -331,7 +336,7 @@ index 1780194..2fdea91 100644 NSUInteger chars_in = (NSUInteger)(charpos - r->charpos); if (chars_in == 0 || !cachedText) return r->ax_start; -@@ -7699,10 +7845,10 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7699,10 +7848,10 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* Convert accessibility string index to buffer charpos. Safe to call from any thread: uses only cachedText (NSString) and @@ -344,7 +349,7 @@ index 1780194..2fdea91 100644 @synchronized (self) { if (visibleRunCount == 0) -@@ -7736,7 +7882,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7736,7 +7885,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, return cp; } } @@ -353,7 +358,7 @@ index 1780194..2fdea91 100644 if (lo > 0) { ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; -@@ -7758,7 +7904,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7758,7 +7907,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, deadlocking the AX server thread. This is prevented by: 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every @@ -362,7 +367,7 @@ index 1780194..2fdea91 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 -@@ -8166,7 +8312,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8166,7 +8315,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, if (idx > [cachedText length]) idx = [cachedText length]; @@ -371,7 +376,7 @@ index 1780194..2fdea91 100644 NSInteger line = 0; NSUInteger scan = 0; NSUInteger len = [cachedText length]; -@@ -8422,7 +8568,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8422,7 +8571,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* =================================================================== @@ -380,7 +385,7 @@ index 1780194..2fdea91 100644 These methods notify VoiceOver of text and selection changes. Called from the redisplay cycle (postAccessibilityUpdates). -@@ -8437,7 +8583,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8437,7 +8586,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, if (point > self.cachedPoint && point - self.cachedPoint == 1) { @@ -389,7 +394,7 @@ index 1780194..2fdea91 100644 [self invalidateTextCache]; [self ensureTextCache]; if (cachedText) -@@ -8456,7 +8602,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8456,7 +8605,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* 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 @@ -398,7 +403,7 @@ index 1780194..2fdea91 100644 self.cachedPoint = point; NSDictionary *change = @{ -@@ -8789,14 +8935,112 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8789,14 +8938,112 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ @@ -487,9 +492,9 @@ index 1780194..2fdea91 100644 + if (y_off < w2->pixel_height) + { + view->overlayZoomRect = NSMakeRect ( -+ w2->pixel_left, -+ WINDOW_TOP_EDGE_Y (w2) + y_off, -+ w2->pixel_width, ++ 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; + } @@ -513,7 +518,7 @@ index 1780194..2fdea91 100644 per the WebKit/Chromium pattern. */ else if (point != self.cachedPoint || markActive != self.cachedMarkActive) { -@@ -8966,7 +9210,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8966,7 +9213,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* =================================================================== @@ -522,7 +527,7 @@ index 1780194..2fdea91 100644 =================================================================== */ /* Scan visible range of window W for interactive spans. -@@ -9157,7 +9401,7 @@ ns_ax_scan_interactive_spans (struct window *w, +@@ -9157,7 +9404,7 @@ ns_ax_scan_interactive_spans (struct window *w, - (BOOL) isAccessibilityFocused { /* Read the cached point stored by EmacsAccessibilityBuffer on the main @@ -531,7 +536,7 @@ index 1780194..2fdea91 100644 EmacsAccessibilityBuffer *pb = self.parentBuffer; if (!pb) return NO; -@@ -9174,7 +9418,7 @@ ns_ax_scan_interactive_spans (struct window *w, +@@ -9174,7 +9421,7 @@ ns_ax_scan_interactive_spans (struct window *w, 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 @@ -540,7 +545,7 @@ index 1780194..2fdea91 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))) -@@ -9200,7 +9444,7 @@ ns_ax_scan_interactive_spans (struct window *w, +@@ -9200,7 +9447,7 @@ ns_ax_scan_interactive_spans (struct window *w, @end @@ -549,7 +554,7 @@ index 1780194..2fdea91 100644 Methods are kept here (same .m file) so they access the ivars declared in the @interface ivar block. */ @implementation EmacsAccessibilityBuffer (InteractiveSpans) -@@ -10520,13 +10764,13 @@ ns_in_echo_area (void) +@@ -10520,13 +10767,13 @@ ns_in_echo_area (void) if (old_title == 0) { char *t = strdup ([[[self window] title] UTF8String]); @@ -565,7 +570,7 @@ index 1780194..2fdea91 100644 [window setTitle: [NSString stringWithUTF8String: size_title]]; [window display]; xfree (size_title); -@@ -11922,7 +12166,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -11922,7 +12169,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, if (WINDOW_LEAF_P (w)) { @@ -574,7 +579,7 @@ index 1780194..2fdea91 100644 EmacsAccessibilityBuffer *elem = [existing objectForKey:[NSValue valueWithPointer:w]]; if (!elem) -@@ -11956,7 +12200,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -11956,7 +12203,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, } else { @@ -583,7 +588,7 @@ index 1780194..2fdea91 100644 Lisp_Object child = w->contents; while (!NILP (child)) { -@@ -12068,7 +12312,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12068,7 +12315,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, accessibilityUpdating = YES; /* Detect window tree change (split, delete, new buffer). Compare @@ -592,7 +597,7 @@ index 1780194..2fdea91 100644 Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); if (!EQ (curRoot, lastRootWindow)) { -@@ -12077,12 +12321,12 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12077,12 +12324,12 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, } /* If tree is stale, rebuild FIRST so we don't iterate freed