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.
This commit is contained in:
2026-03-01 03:38:58 +01:00
parent 9110eee881
commit b283068f82
9 changed files with 340 additions and 86 deletions

View File

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