From 5196674d9e0e68e4a7996a171f653729b2cd2ebf Mon Sep 17 00:00:00 2001 From: Daneel Date: Mon, 23 Feb 2026 10:22:59 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20UAZoomChangeFocus()=20=E2=80=94=20actual?= =?UTF-8?q?=20fix=20for=20macOS=20Zoom=20cursor=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause found via research pipeline: NSAccessibility notifications alone are insufficient for custom NSView. macOS Zoom 'Follow keyboard focus' requires UAZoomChangeFocus() from HIServices/UniversalAccess.h — same as iTerm2 and Chromium. Squashed into single clean patch. 159 insertions, 2 files. --- ...oundsForRange-for-macOS-Zoom-cursor-.patch | 89 ++++++++++++------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch index 2b98e1f..dc5fb8a 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,34 +1,40 @@ -From ea7573e32a140ffce0758485abd959954e74c271 Mon Sep 17 00:00:00 2001 +From 09f293daca75996e7348c89e2b0d31a16c1c2500 Mon Sep 17 00:00:00 2001 From: Daneel -Date: Mon, 23 Feb 2026 10:03:39 +0100 -Subject: [PATCH] ns: implement NSAccessibility cursor tracking for macOS Zoom +Date: Mon, 23 Feb 2026 10:22:47 +0100 +Subject: [PATCH] ns: implement UAZoomChangeFocus + NSAccessibility for macOS + Zoom -Add full NSAccessibility support to EmacsView so that macOS Zoom -'Follow keyboard focus' tracks the text cursor position. +Add full cursor tracking support for macOS Zoom 'Follow keyboard focus' +and other assistive technology tools. -Key insight: macOS Zoom is event-driven. It only calls -accessibilityBoundsForRange: / AXBoundsForRange AFTER receiving -NSAccessibilitySelectedTextChangedNotification. Without posting that -notification, Zoom never queries cursor position -- even if the bounds -methods are correctly implemented. +ROOT CAUSE: NSAccessibility notifications alone are insufficient for +custom-drawn views. macOS Zoom requires an explicit call to +UAZoomChangeFocus() from HIServices/UniversalAccess.h. This is the +same mechanism used by iTerm2 (PTYTextView.m:refreshAccessibility) and +Chromium (render_widget_host_view_mac.mm:OnSelectionBoundsChanged). -Changes: -- Add lastAccessibilityCursorRect ivar to EmacsView (nsterm.h) -- Store cursor rect in ns_draw_window_cursor + post SelectedTextChanged -- Post FocusedUIElementChanged in windowDidBecomeKey -- Implement accessibilityIsIgnored, accessibilityFocusedUIElement -- Implement accessibilityRole (NSAccessibilityTextAreaRole) -- Implement isAccessibilityElement (new API, returns YES) -- Implement accessibilityAttributeNames + accessibilityAttributeValue: - responds to NSAccessibilitySelectedTextRangeAttribute with {0,0} -- Implement accessibilityBoundsForRange: (new NSAccessibilityProtocol) -- Implement accessibilityParameterizedAttributeNames + old API fallback +Changes to src/nsterm.h: +- Add lastAccessibilityCursorRect ivar to EmacsView + +Changes to src/nsterm.m: +- ns_draw_window_cursor: store cursor rect, post SelectedTextChanged, + call UAZoomChangeFocus() with cursor rect in AX screen coordinates +- windowDidBecomeKey: post FocusedUIElementChangedNotification +- EmacsView: add accessibilityIsIgnored (NO), isAccessibilityElement (YES), + accessibilityFocusedUIElement (self), accessibilityRole (TextAreaRole), + accessibilityAttributeNames, accessibilityAttributeValue: (responds to + NSAccessibilitySelectedTextRangeAttribute with {0,0}), + accessibilityBoundsForRange: (new NSAccessibilityProtocol, macOS 10.10+), + accessibilityParameterizedAttributeNames + old AXBoundsForRange fallback + +Carbon/Carbon.h (already included in NS_IMPL_COCOA) provides UAZoomEnabled(), +UAZoomChangeFocus(), and kUAZoomFocusTypeInsertionPoint. See also: https://github.com/nicowillis/Ghostty/issues/4053 --- - src/nsterm.h | 3 ++ - src/nsterm.m | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++ - 2 files changed, 142 insertions(+) + src/nsterm.h | 3 + + src/nsterm.m | 156 +++++++++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 159 insertions(+) diff --git a/src/nsterm.h b/src/nsterm.h index 7c1ee4cf535..6c1ff3434a3 100644 @@ -45,26 +51,43 @@ index 7c1ee4cf535..6c1ff3434a3 100644 /* AppKit-side interface. */ diff --git a/src/nsterm.m b/src/nsterm.m -index 932d209f56b..906c76bc468 100644 +index 932d209f56b..38d946f1d9f 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -3232,6 +3232,23 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. +@@ -3232,6 +3232,40 @@ 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)); +#ifdef NS_IMPL_COCOA -+ /* Store cursor rect for accessibility and notify assistive tools -+ (e.g. macOS Zoom "Follow keyboard focus") that the cursor moved. -+ macOS Zoom is event-driven: it only calls accessibilityBoundsForRange: -+ AFTER receiving NSAccessibilitySelectedTextChangedNotification. -+ Without posting this notification, Zoom never queries cursor position. */ ++ /* Update accessibility cursor tracking for macOS Zoom and VoiceOver. ++ NSAccessibility notifications alone are NOT sufficient for custom-drawn ++ views -- macOS Zoom "Follow keyboard focus" requires an explicit call to ++ UAZoomChangeFocus() from HIServices/UniversalAccess.h, which directly ++ tells Zoom the cursor's screen position. This is the same mechanism used ++ by iTerm2 (PTYTextView.m:refreshAccessibility) and Chromium. */ + { + EmacsView *view = FRAME_NS_VIEW (f); + if (view) + { + view->lastAccessibilityCursorRect = r; ++ ++ /* Post standard AX notification for VoiceOver and other AT tools. */ + NSAccessibilityPostNotification (view, + NSAccessibilitySelectedTextChangedNotification); ++ ++ /* Tell macOS Zoom exactly where the cursor is. UAZoomChangeFocus() ++ expects coordinates in the NSAccessibility coordinate system (origin ++ at top-left of primary screen). Convert: view → window → screen ++ (Quartz, origin bottom-left) → AX (origin top-left) via ++ accessibilityConvertScreenRect:. */ ++ if (UAZoomEnabled ()) ++ { ++ NSRect windowRect = [view convertRect:r toView:nil]; ++ NSRect screenRect = [[view window] convertRectToScreen:windowRect]; ++ CGRect cgRect = [view accessibilityConvertScreenRect: ++ NSRectToCGRect (screenRect)]; ++ UAZoomChangeFocus (&cgRect, &cgRect, kUAZoomFocusTypeInsertionPoint); ++ } + } + } +#endif @@ -72,7 +95,7 @@ index 932d209f56b..906c76bc468 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -8237,6 +8254,14 @@ - (void)windowDidBecomeKey /* for direct calls */ +@@ -8237,6 +8271,14 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -87,7 +110,7 @@ index 932d209f56b..906c76bc468 100644 } -@@ -9474,6 +9499,120 @@ - (int) fullscreenState +@@ -9474,6 +9516,120 @@ - (int) fullscreenState return fs_state; }