Skip to content

macOS: add dynamic accepts_first_mouse callback#4488

Open
tilladam wants to merge 1 commit into
rust-windowing:masterfrom
tilladam:dynamic-accepts-first-mouse
Open

macOS: add dynamic accepts_first_mouse callback#4488
tilladam wants to merge 1 commit into
rust-windowing:masterfrom
tilladam:dynamic-accepts-first-mouse

Conversation

@tilladam

Copy link
Copy Markdown

Summary

  • Add ApplicationHandlerExtMacOS::accepts_first_mouse callback that receives the window ID and click position (PhysicalPosition<f64>), enabling per-click decisions about whether to accept first mouse on inactive windows.
  • Add EventHandler::handle_with_result for synchronous, re-entrant-safe handler dispatch that returns Option<R>. Refactor existing handle on top of it to eliminate duplication.
  • When the handler is unavailable (not set or re-entrant), the static accepts_first_mouse value from WindowAttributes is used as a fallback.

Motivated by slint-ui/slint#10451 — applications need per-click decisions to follow the macOS convention of accepting first mouse for low-risk actions (selection, scrolling) but rejecting it for buttons and destructive actions.

Implementation notes

  • acceptsFirstMouse: must return bool synchronously, so the handler is called via handle_with_result which returns None on re-entrancy instead of panicking.
  • The NSEvent parameter is Option<&NSEvent> per Apple's API contract (can be nil); nil falls back to the static default.
  • Coordinates use the same conversion as existing mouse event handlers (convertPoint_fromView + scale factor), with isFlipped providing top-left origin.

Test plan

  • cargo build -p winit-core / winit-appkit / winit — compiles
  • cargo test -p winit-core — passes
  • cargo test -p winit-common --features event-handler — 5 new tests pass:
    • handle_with_result returns value (normal path)
    • handle_with_result returns None when handler not set
    • handle_with_result returns None on re-entrancy via handle
    • handle_with_result returns None on re-entrancy via itself
    • handle still panics on re-entrancy (regression test)
  • cargo clippy -p winit-appkit — no warnings
  • cargo +nightly fmt -- --check — clean

@tilladam tilladam requested a review from madsmtm as a code owner February 15, 2026 21:27
@tilladam tilladam force-pushed the dynamic-accepts-first-mouse branch from 9e4e2f6 to 91b3567 Compare February 15, 2026 21:30
@madsmtm madsmtm added S - enhancement Wouldn't this be the coolest? DS - appkit Affects the AppKit/macOS backend labels Mar 1, 2026

@madsmtm madsmtm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, and sorry for being slow to get back to it, the continuos rebases helped ;)

Comment thread winit-appkit/src/view.rs Outdated
})
.flatten()
})
.unwrap_or(self.ivars().accepts_first_mouse)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is acceptsFirstMouse: called by AppKit? If it's only done when responding to events from the OS, and not in e.g. methods like Window::set_title() or whatever, maybe it'd make sense to warn if we had a fn accepts_first_mouse, but couldn't call it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`acceptsFirstMouse:` is only called by AppKit in response to mouse-down events on an inactive window — not from programmatic calls like `set_title()`. Added a warning in 7ea93ef for the case where the handler can't be called synchronously (re-entrancy); it logs before falling back to the static value.

Comment thread winit-common/src/event_handler.rs Outdated
let handler = EventHandler::new();
handler.set(Box::new(DummyApp), || {
handler.handle(|_app| {
// Re-entrant handle must still panic after the refactoring.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Makes no sense to talk about "after the refactoring" in a code comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 959c126.

Comment thread winit-core/src/application/macos.rs Outdated
/// here so that the application can make per-click decisions, e.g. accept first mouse for
/// low-risk actions (selection, scrolling) but reject it for buttons or destructive actions.
///
/// The default implementation returns `true`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, but that'd mean that by returning Some(self) in macos_handler, you overwrite WindowAttributesMacOS.accepts_first_mouse?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — I've clarified the docs in 6cf8480 to make the precedence explicit: when macos_handler() returns Some, the dynamic callback takes precedence over the static WindowAttributesMacOS::with_accepts_first_mouse setting. The static value is only used as a fallback during re-entrancy.

I've also added guidance in 2cb9dce on when to use one vs the other: use the static setting when a per-window value is sufficient, use the dynamic callback when you need to decide based on click position.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this is confusing to have both. I'm wondering if it would not be better here to remove WindowAttributesMacOS::with_accepts_first_mouse entirely.

Comment thread winit-core/src/application/macos.rs Outdated
///
/// [`acceptsFirstMouse:`]: https://developer.apple.com/documentation/appkit/nsview/acceptsfirstmouse(_:)
#[doc(alias = "acceptsFirstMouse:")]
fn accepts_first_mouse(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add usage to the app example?

Or maybe just document when, as a user, you'd want to implement this vs. WindowAttributesMacOS.accepts_first_mouse.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 2cb9dce — added accepts_first_mouse to the application example and documented when you'd use the static setting vs the dynamic callback.

Comment thread winit-common/src/event_handler.rs Outdated
/// It is important that we keep the `RefMut` borrowed during the callback, so that `in_use`
/// can properly detect that the handler is still in use. If the handler unwinds, the `RefMut`
/// will ensure that the handler is no longer borrowed.
pub fn handle_with_result<R>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's the right approach to the problem, but boy do I not like the fallback behavior when we're re-entrant, I wish we could do something different - but I don't see a good solution, AppKit is hella re-entrant and that's hard to avoid. Spitballing, maybe making ApplicationHandlerExtMacOS take &self, and do fn macos_handler(&self) -> Option<Box<dyn macos::ApplicationHandlerExtMacOS>> instead? That would push the re-entrancy to the user.

Anyhow, not something we need to fix in this PR, such changes can be made later when we better understand the requirements.

@madsmtm madsmtm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, thinking about it a bit more, maybe the better approach would be to allow querying whether an event was the first mouse event?

E.g. something like:

match event {
    WindowEvent::PointerButton {
        button: ButtonSource::Mouse(MouseButton::Left),
        is_macos_first_after_focus_bikeshed, // bool
        position,
        ..
    } => {
        if is_macos_first_after_focus_bikeshed && self.thing_at_pos_is_button(position) {
            return; // Don't try to click buttons for first
        }
        // Select item / click button / whatever you'd normally do
    }
    _ => ...,
}

Whether we can actually implement that is a different story, but I think it'd be more consistent with how Winit otherwise works.

@tilladam tilladam force-pushed the dynamic-accepts-first-mouse branch from 82da762 to 9b0b42b Compare March 28, 2026 10:51
@tilladam

Copy link
Copy Markdown
Author

Thanks for the review! I've addressed the inline comments — see replies above.

Regarding the alternative approach (a is_macos_first_after_focus bool on PointerButton events): I agree it would be more consistent with winit's event-driven model and would avoid the re-entrancy complexity entirely. However, I think the callback approach is the better fit for now:

  • Native mapping: The current approach maps directly onto acceptsFirstMouse:, which AppKit calls before deciding whether to deliver the click. With the event-based approach, we'd always return true from acceptsFirstMouse: and reconstruct the "was this first mouse?" state after the fact.
  • Reliable detection is tricky: By the time mouseDown: fires, the window is already key. We'd need to track focus-transition state and correlate it with the next mouse-down, with edge cases around rapid clicks, focus changes without clicks, and multiple windows.
  • Suppression semantics: With acceptsFirstMouse: returning false, AppKit never delivers the mouseDown at all. The event-based approach can only ignore the click in userland, which is likely equivalent for a custom view but is a semantic difference.

That said, if this is something you'd prefer to explore further or if it's a blocker for the PR, happy to discuss. The event-based approach could also work as a future addition alongside the callback — they aren't mutually exclusive.

@tilladam tilladam requested a review from madsmtm April 10, 2026 10:17
@tilladam

tilladam commented May 1, 2026

Copy link
Copy Markdown
Author

Sorry to nag, but is there anything more I can supply to move this forward? Happy to rework this if you have other ideas for how to fix it or if you prefer the event approach described above.

@ogoffart ogoffart left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a review. But please note that i am not familiar with macOS at all and i don't know really how this is supposed to work.

I have the feeling is_macos_first_after_focus might still be nicer.

Regarding your arguments:

Native mapping

On the other hand winit is an abstraction and doesn't need to map exactly to each platform API (otherwise we'd use the platform API directly)

Reliable detection is tricky
Suppression semantics

That's the question whether it is possible to implement or not.

Regardless, how do you think people will use this? For example, in Slint we'd also probably map this to a is_first_after_forcus or something like that. Or do you have other idea in mind?

Comment thread winit-core/src/application/macos.rs Outdated
/// here so that the application can make per-click decisions, e.g. accept first mouse for
/// low-risk actions (selection, scrolling) but reject it for buttons or destructive actions.
///
/// The default implementation returns `true`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this is confusing to have both. I'm wondering if it would not be better here to remove WindowAttributesMacOS::with_accepts_first_mouse entirely.

Comment thread winit-core/src/application/macos.rs Outdated

/// Called when the user clicks on an inactive window to determine whether the click should
/// also be processed as a normal mouse event.
///

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it the equivalent of a press, or a release event?

When returning true, do we get normal press end release event as well?

@tilladam tilladam force-pushed the dynamic-accepts-first-mouse branch from 955851a to d073235 Compare June 18, 2026 20:41
@tilladam

Copy link
Copy Markdown
Author

Reworked this as the event-flag approach you both suggested. Force-pushed a single squashed commit (d073235e) rebased on current master.

What changed:

  • New: WindowEvent::PointerButton::is_macos_activation_click: bool. On macOS, it's set on both the press and the matching release of the click that activated an inactive window, so apps can short-circuit the whole gesture with a single check — no state tracking needed for press/release pairs:

    WindowEvent::PointerButton { is_macos_activation_click: true, .. } => return,

    Always false on other platforms. Intervening drag motion (PointerMoved) is not tagged; apps that care about drags during the activation gesture have to track that themselves.

  • Removed: ApplicationHandlerExtMacOS::accepts_first_mouse and WindowAttributesMacOS::with_accepts_first_mouse. Per @ogoffart's suggestion, dropping the static one entirely — it's fully redundant once apps can decide per click.

  • The acceptsFirstMouse: impl simplifies to "set a Cell<bool> flag, return true". No re-entrancy handling, no handle_with_result primitive, no precedence docs to write.

@ogoffart, to answer your specific questions:

Is it the equivalent of a press, or a release event? When returning true, do we get normal press and release event as well?

Both — the activating left press and its matching release are tagged with is_macos_activation_click: true, intentionally, so a single if is_macos_activation_click { return; } drops the whole gesture. The release event is delivered normally otherwise (so apps that don't care about activation clicks see press + release as usual).

I'm wondering if it would not be better here to remove WindowAttributesMacOS::with_accepts_first_mouse entirely.

Done.

Verified locally: cargo build, cargo test, cargo clippy, cargo +nightly fmt --check clean on macOS host. cargo check -p winit-web --target wasm32-unknown-unknown clean. Cross-platform PointerButton sites (22 across win32/x11/wayland/android/uikit/web/orbital plus core helpers) all pass is_macos_activation_click: false — mechanical, but I couldn't compile the non-macOS/web backends locally; relying on CI for those.

@madsmtm, this also obviates the re-entrancy fallback you flagged — there's nothing to fall back to anymore.

@tilladam tilladam force-pushed the dynamic-accepts-first-mouse branch from d073235 to be05dc0 Compare June 30, 2026 08:27
Apps need per-click decisions to follow the macOS convention of accepting
first mouse for low-risk actions (selection, scrolling) but rejecting it
for buttons and destructive actions. Motivated by slint-ui/slint#10451.

Always return `true` from `acceptsFirstMouse:`, and tag the resulting
`PointerButton` events with `is_macos_activation_click: true` (on both
the activating left press and its matching release) so the app can
short-circuit the whole gesture with a single check:

    WindowEvent::PointerButton { is_macos_activation_click: true, .. } => return,

Replaces an earlier callback-based design (`accepts_first_mouse` on
`ApplicationHandlerExtMacOS`) that mapped more closely to AppKit but
didn't fit winit's event-driven model and required re-entrancy handling.

Also removes `WindowAttributesMacOS::with_accepts_first_mouse`, which is
now redundant — apps that want to reject activation clicks check the
flag on the per-event instead.
@tilladam tilladam force-pushed the dynamic-accepts-first-mouse branch from be05dc0 to d0ddda0 Compare July 2, 2026 06:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DS - appkit Affects the AppKit/macOS backend S - enhancement Wouldn't this be the coolest?

Development

Successfully merging this pull request may close these issues.

3 participants