patches: v4 0007 - Zoom follows overlay candidate

Zoom now tracks the selected candidate row via overlayZoomRect
instead of always pointing at the text cursor. Returns to cursor
on typing (chars_modiff change).
This commit is contained in:
2026-02-28 15:33:45 +01:00
parent 9359277143
commit 99609f0437

View File

@@ -1,4 +1,4 @@
From bfba1d81a0b70651fb626da57c0f3cc68e77998c Mon Sep 17 00:00:00 2001 From 403a1c4664e7491c20eac86c143898bc366a57bc Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 14:46:25 +0100 Date: Sat, 28 Feb 2026 14:46:25 +0100
Subject: [PATCH] ns: announce overlay completion candidates for VoiceOver Subject: [PATCH] ns: announce overlay completion candidates for VoiceOver
@@ -31,24 +31,28 @@ Key implementation details:
Do not post SelectedTextChanged (that reads the AX text at cursor Do not post SelectedTextChanged (that reads the AX text at cursor
position, which is the minibuffer input, not the candidate). position, which is the minibuffer input, not the candidate).
- Add Zoom tracking (UAZoomChangeFocus) for the selected candidate - Zoom tracking: store the selected candidate's glyph row rect in
glyph row in the minibuffer window matrix. overlayZoomRect. 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) to return Zoom to the cursor.
* src/nsterm.h (EmacsAccessibilityBuffer): Add cachedCharsModiff. * 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.
(ns_ax_selected_overlay_text): New function. (ns_ax_selected_overlay_text): New function.
(EmacsAccessibilityBuffer ensureTextCache): Remove overlay_modiff (ns_draw_window_cursor): Use overlayZoomRect when active.
from cache validity check. (EmacsAccessibilityBuffer ensureTextCache): Remove overlay_modiff.
(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): (EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:):
Use BUF_CHARS_MODIFF for ValueChanged gating; make overlay branch Independent overlay branch, BUF_CHARS_MODIFF gating, candidate
independent; announce candidate with Zoom tracking. announcement with overlay Zoom rect storage.
--- ---
src/nsterm.h | 1 + src/nsterm.h | 3 +
src/nsterm.m | 259 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/nsterm.m | 252 ++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 250 insertions(+), 10 deletions(-) 2 files changed, 244 insertions(+), 11 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 51c30ca..dd0e226 100644 index 51c30ca..5c15639 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -507,6 +507,7 @@ typedef struct ns_ax_visible_run @@ -507,6 +507,7 @@ typedef struct ns_ax_visible_run
@@ -59,11 +63,34 @@ index 51c30ca..dd0e226 100644
@property (nonatomic, assign) ptrdiff_t cachedPoint; @property (nonatomic, assign) ptrdiff_t cachedPoint;
@property (nonatomic, assign) BOOL cachedMarkActive; @property (nonatomic, assign) BOOL cachedMarkActive;
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement; @property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
@@ -591,6 +592,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 diff --git a/src/nsterm.m b/src/nsterm.m
index 1780194..203d3a8 100644 index 1780194..5c3758a 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -6915,11 +6915,145 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) @@ -3258,7 +3258,12 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row,
&& 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);
@@ -6915,11 +6920,145 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
are truncated for accessibility purposes. */ are truncated for accessibility purposes. */
#define NS_AX_TEXT_CAP 100000 #define NS_AX_TEXT_CAP 100000
@@ -210,7 +237,7 @@ index 1780194..203d3a8 100644
static NSString * static NSString *
ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
ns_ax_visible_run **out_runs, NSUInteger *out_nruns) ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
@@ -7556,6 +7690,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, @@ -7556,6 +7695,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
@synthesize cachedOverlayModiff; @synthesize cachedOverlayModiff;
@synthesize cachedTextStart; @synthesize cachedTextStart;
@synthesize cachedModiff; @synthesize cachedModiff;
@@ -218,7 +245,7 @@ index 1780194..203d3a8 100644
@synthesize cachedPoint; @synthesize cachedPoint;
@synthesize cachedMarkActive; @synthesize cachedMarkActive;
@synthesize cachedCompletionAnnouncement; @synthesize cachedCompletionAnnouncement;
@@ -7609,16 +7744,15 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, @@ -7609,16 +7749,15 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
return; return;
ptrdiff_t modiff = BUF_MODIFF (b); ptrdiff_t modiff = BUF_MODIFF (b);
@@ -241,7 +268,7 @@ index 1780194..203d3a8 100644
&& cachedTextStart == BUF_BEGV (b) && cachedTextStart == BUF_BEGV (b)
&& pt >= cachedTextStart && pt >= cachedTextStart
&& (textLen == 0 && (textLen == 0
@@ -7635,7 +7769,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, @@ -7635,7 +7774,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
[cachedText release]; [cachedText release];
cachedText = [text retain]; cachedText = [text retain];
cachedTextModiff = modiff; cachedTextModiff = modiff;
@@ -249,7 +276,7 @@ index 1780194..203d3a8 100644
cachedTextStart = start; cachedTextStart = start;
if (visibleRuns) if (visibleRuns)
@@ -8789,10 +8922,116 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, @@ -8789,10 +8927,102 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem,
BOOL markActive = !NILP (BVAR (b, mark_active)); BOOL markActive = !NILP (BVAR (b, mark_active));
/* --- Text changed (edit) --- */ /* --- Text changed (edit) --- */
@@ -267,6 +294,7 @@ index 1780194..203d3a8 100644
+ if (chars_modiff != self.cachedCharsModiff) + if (chars_modiff != self.cachedCharsModiff)
+ { + {
+ self.cachedCharsModiff = chars_modiff; + self.cachedCharsModiff = chars_modiff;
+ self.emacsView->overlayZoomActive = NO;
+ [self postTextChangedNotification:point]; + [self postTextChangedNotification:point];
+ } + }
+ } + }
@@ -315,13 +343,10 @@ index 1780194..203d3a8 100644
+ annInfo); + annInfo);
+ +
+ /* --- Zoom tracking for overlay candidates --- + /* --- Zoom tracking for overlay candidates ---
+ The overlay candidates appear as visual lines in the + Store the candidate row rect so draw_window_cursor
+ minibuffer window. Row 0 is the input line; overlay + focuses Zoom there instead of on the text cursor.
+ candidates start from row 1 onward. Find the glyph + Cleared when the user types (chars_modiff change). */
+ row for the selected candidate and focus Zoom there. */ + if (selected_line >= 0)
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
+ if (selected_line >= 0 && UAZoomEnabled ())
+ { + {
+ struct window *w2 = [self validWindow]; + struct window *w2 = [self validWindow];
+ if (w2 && w2->current_matrix) + if (w2 && w2->current_matrix)
@@ -336,33 +361,21 @@ index 1780194..203d3a8 100644
+ if (row->enabled_p + if (row->enabled_p
+ && row->visible_height > 0) + && row->visible_height > 0)
+ { + {
+ NSRect r = NSMakeRect ( + view->overlayZoomRect = NSMakeRect (
+ w2->pixel_left, + w2->pixel_left,
+ WINDOW_TOP_EDGE_Y (w2) + row->y, + WINDOW_TOP_EDGE_Y (w2) + row->y,
+ w2->pixel_width, + w2->pixel_width,
+ row->visible_height); + row->visible_height);
+ NSRect winRect + view->overlayZoomActive = YES;
+ = [view convertRect:r toView:nil];
+ NSRect screenRect
+ = [[view window]
+ convertRectToScreen:winRect];
+ 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
+ } + }
+ }
+ else
+ {
+ self.emacsView->overlayZoomActive = NO;
+ } + }
} }