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).
This commit is contained in:
2026-02-28 15:51:06 +01:00
parent 4e5596d9de
commit be4e0bb5be

View File

@@ -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 <martin@sukany.cz>
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