mirror of
https://github.com/unslothai/unsloth.git
synced 2026-10-21 05:11:50 +00:00
Studio: route clicks and scrolls to the browser page on macOS 27 (#14216)
Fixes https://github.com/unslothai/unsloth/issues/13629 ## Problem On macOS 27, web pages in the Desktop browser panel ignore clicks and scrolling. Hover still works, so a button looks like it reacts, but nothing happens. On macOS the page sits under the app's transparent webview (#13122). The app's webview returns nil from `hitTest:` over the page, so AppKit gives the click to the page below it. macOS 27 no longer asks `hitTest:` for a left click, a scroll or a cursor update. `NSWindow` now calls `-[NSView hitTestForContext:]`, a new method that WKWebView answers on its own without calling `hitTest:`. The app's webview claims every click and scroll over the page. ## Fix `browser_layer.rs` also replaces `hitTestForContext:` when the webview class has it (macOS 27 and later), with the same rule as `hitTest:`: nil over the page, unless panel UI covers that point. The context's point is in window coordinates, not the superview's. If the context ever lacks a `point`, the call goes to the original method unchanged. Before macOS 27 the method does not exist, so nothing is replaced and nothing changes. ## Testing A standalone AppKit probe copies `browser_layer.rs`: a transparent app WKWebView over a page WKWebView of the same class, with `hitTest:` replaced after both exist. It sends real HID mouse and scroll events through `CGEventPost`. GitHub `macos-26` (26.6.2) and `xcode-27` (macOS 27.0.1) runners: | macOS | code | click on the page | scroll on the page | click on app UI next to the page | |---|---|---|---|---| | 26.6.2 | before | page | page scrolls 1000 px | app | | 26.6.2 | after | page | page scrolls 1000 px | app | | 27.0.1 | before | **app** (page gets nothing) | **app** (page stays at 0) | app | | 27.0.1 | after | page | page scrolls 1000 px | app | | 27.0.1 | after, point read as superview coordinates (control) | page | page scrolls 1000 px | **lost** (neither view) | - On macOS 27 the window's left-click path is `-[NSWindow _handleLeftMouseDownEvent:]`. It calls `hitTestForContext:` with an `NSHitTestContext` for cursor updates (event type 17), left mouse downs (1) and scroll wheels (22). The context's `point` is the window point, passed unchanged down the view tree. - The last column clicks the app 20 px left of the page's edge, with the app's webview offset 37 px in the window. Read as window coordinates, the click stays with the app. Read as superview coordinates, the app gives it up, the page refuses it, and the click is lost. - `cargo check --all-targets` (debug and `--release`) of `studio/src-tauri` with this change passes on `macos-26` and `xcode-27`.
This commit is contained in:
parent
8de2de25bb
commit
88afa84b87
1 changed files with 45 additions and 2 deletions
|
|
@ -2,8 +2,9 @@
|
|||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
//! macOS: browser pages sit under the app's webview, which is transparent over them, so panel
|
||||
//! overlays draw on top of the live page. The app's webview returns nil from `hitTest:` and
|
||||
//! `_hitTest:dragTypes:` over the page unless panel UI covers that point (`browser_view_input`).
|
||||
//! overlays draw on top of the live page. The app's webview returns nil from `hitTest:`,
|
||||
//! `_hitTest:dragTypes:` and `hitTestForContext:` over the page unless panel UI covers that point
|
||||
//! (`browser_view_input`).
|
||||
|
||||
use crate::browser_webview::ViewBounds;
|
||||
use block2::RcBlock;
|
||||
|
|
@ -25,12 +26,17 @@ type DragHitTest = unsafe extern "C-unwind" fn(
|
|||
*mut NSPoint,
|
||||
*mut AnyObject,
|
||||
) -> *mut AnyObject;
|
||||
/// macOS 27+: clicks, scrolls and cursor updates hit-test through `hitTestForContext:`, which
|
||||
/// WKWebView answers itself without calling `hitTest:`.
|
||||
type ContextHitTest =
|
||||
unsafe extern "C-unwind" fn(*mut AnyObject, Sel, *mut AnyObject) -> *mut AnyObject;
|
||||
|
||||
/// The app's webview, set once on the main thread; it lives as long as the app.
|
||||
static MAIN: AtomicPtr<AnyObject> = AtomicPtr::new(null_mut());
|
||||
static INSTALLING: AtomicBool = AtomicBool::new(false);
|
||||
static HIT_TEST: OnceLock<HitTest> = OnceLock::new();
|
||||
static DRAG_HIT_TEST: OnceLock<DragHitTest> = OnceLock::new();
|
||||
static CONTEXT_HIT_TEST: OnceLock<ContextHitTest> = OnceLock::new();
|
||||
/// Which webview takes mouse moves (`APP` or `PAGE`, 0 before the first). WebKit's tracking
|
||||
/// areas hand every move in the window to both views whatever is on top, so both would set the
|
||||
/// cursor and hover in turn, a flicker over links, text and annotate.
|
||||
|
|
@ -170,6 +176,8 @@ unsafe fn install_on(view: *mut AnyObject) {
|
|||
log::warn!("browser pages stay over the panel: the webview class can't be routed");
|
||||
return;
|
||||
};
|
||||
// Absent before macOS 27.
|
||||
let context_hit_test = class.instance_method(sel!(hitTestForContext:));
|
||||
// Safety: same signatures; the originals are stored before the swap can call them.
|
||||
unsafe {
|
||||
let _ = HIT_TEST.set(std::mem::transmute::<Imp, HitTest>(
|
||||
|
|
@ -191,6 +199,17 @@ unsafe fn install_on(view: *mut AnyObject) {
|
|||
std::mem::transmute::<DragHitTest, Imp>(routed_drag_hit_test),
|
||||
objc2::ffi::method_getTypeEncoding(drag_hit_test),
|
||||
);
|
||||
if let Some(context_hit_test) = context_hit_test {
|
||||
let _ = CONTEXT_HIT_TEST.set(std::mem::transmute::<Imp, ContextHitTest>(
|
||||
context_hit_test.implementation(),
|
||||
));
|
||||
objc2::ffi::class_replaceMethod(
|
||||
class,
|
||||
sel!(hitTestForContext:),
|
||||
std::mem::transmute::<ContextHitTest, Imp>(routed_context_hit_test),
|
||||
objc2::ffi::method_getTypeEncoding(context_hit_test),
|
||||
);
|
||||
}
|
||||
// Private KVC key, as used by wry's `transparent` option.
|
||||
let main = &*(view as *const NSView);
|
||||
main.setValue_forKey(
|
||||
|
|
@ -295,6 +314,30 @@ unsafe extern "C-unwind" fn routed_hit_test(
|
|||
unsafe { original(this, cmd, point) }
|
||||
}
|
||||
|
||||
unsafe extern "C-unwind" fn routed_context_hit_test(
|
||||
this: *mut AnyObject,
|
||||
cmd: Sel,
|
||||
context: *mut AnyObject,
|
||||
) -> *mut AnyObject {
|
||||
if let Some(main) = main_view().filter(|main| std::ptr::eq(*main as *const NSView, this.cast()))
|
||||
{
|
||||
// Safety: AppKit's NSHitTestContext, on the main thread. Its point is in window
|
||||
// coordinates, passed down unchanged (unlike `hitTest:`'s superview coordinates).
|
||||
if let Some(context) = unsafe { context.as_ref() } {
|
||||
let has_point: bool = unsafe { msg_send![context, respondsToSelector: sel!(point)] };
|
||||
if has_point {
|
||||
let at: NSPoint = unsafe { msg_send![context, point] };
|
||||
if over_page(main, main.convertPoint_fromView(at, None)) {
|
||||
return null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let original = CONTEXT_HIT_TEST.get().expect("installed before routing");
|
||||
// Safety: AppKit's arguments, forwarded to the original implementation.
|
||||
unsafe { original(this, cmd, context) }
|
||||
}
|
||||
|
||||
/// Files dragged over the page drop on it, as before it sat under the app's webview.
|
||||
unsafe extern "C-unwind" fn routed_drag_hit_test(
|
||||
this: *mut AnyObject,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue