macOS: the canvas cursor stays after the pointer moves

The AppKit cursor adapter set its cursor again only when
NSCursor.currentCursor differed from it. But that is the app's last set
cursor, not the one on screen: when the pointer moves, AppKit can put
the system arrow back while currentCursor still reads as ours, so the
adapter kept the arrow. Pressing V over the canvas showed the Selection
tool's arrow, and the first move replaced it with the system pointer.

While it owns the cursor, the adapter now sets it on every pass. That
costs nothing when it is already showing.
This commit is contained in:
Dan Green
2026-11-12 06:59:40 -08:00
parent 71b889605d
commit cea0eb8358
2 changed files with 9 additions and 21 deletions
+4 -7
View File
@@ -35,7 +35,6 @@ trait Backend {
fn suspend(&mut self, window: &Self::Window);
fn restore(&mut self, window: &Self::Window);
fn build(&mut self, request: &Request) -> Option<Self::Cursor>;
fn current(&self) -> Option<Self::Cursor>;
fn set(&mut self, cursor: &Self::Cursor);
}
@@ -73,9 +72,10 @@ impl<B: Backend> State<B> {
&& *key == request.key
&& *owned == window
{
if self.backend.current().as_ref() != Some(cursor) {
self.backend.set(cursor);
}
// AppKit can show the system arrow when the pointer moves while
// `NSCursor.currentCursor` (the app's last set cursor) still reads as ours, so a
// comparison would keep the arrow. Setting the cursor again costs nothing.
self.backend.set(cursor);
return;
}
self.release();
@@ -135,9 +135,6 @@ impl Backend for AppKit {
fn build(&mut self, request: &Request) -> Option<Self::Cursor> {
build(&request.bitmap)
}
fn current(&self) -> Option<Self::Cursor> {
Some(NSCursor::currentCursor())
}
fn set(&mut self, cursor: &Self::Cursor) {
cursor.set();
}
+5 -14
View File
@@ -6,7 +6,6 @@ use std::cell::Cell;
struct Fake {
window: Option<u8>,
suspended: Vec<u8>,
current: Option<Key>,
sets: usize,
builds: usize,
restores: Vec<u8>,
@@ -27,17 +26,12 @@ impl Backend for Fake {
fn restore(&mut self, w: &u8) {
self.suspended.retain(|v| v != w);
self.restores.push(*w);
self.current = None;
}
fn build(&mut self, r: &Request) -> Option<Key> {
self.builds += 1;
(!self.fail).then_some(r.key)
}
fn current(&self) -> Option<Key> {
self.current
}
fn set(&mut self, cursor: &Key) {
self.current = Some(*cursor);
fn set(&mut self, _: &Key) {
self.sets += 1;
}
}
@@ -66,19 +60,16 @@ fn deliver(state: &mut State<Fake>, request: Option<Request>) {
}
#[test]
fn mac_cursor_ownership_deduplicates_reasserts_and_restores() {
fn mac_cursor_ownership_reasserts_every_pass_and_restores() {
let req = request(1.0);
let mut state = fake_state();
deliver(&mut state, Some(req.clone()));
assert_eq!(state.backend.suspended, [1]);
assert_eq!((state.backend.builds, state.backend.sets), (1, 1));
// The pointer moves and AppKit shows the system arrow, but the app's current cursor
// still reads as ours. The next pass sets it again without rebuilding or re-suspending.
deliver(&mut state, Some(req.clone()));
assert_eq!(state.backend.sets, 1);
// Simulate a platform fallback replacing the cursor after finalization. The next pass
// reasserts; this proves the protocol, not AppKit timing or absence of transient flicker.
state.backend.current = None;
deliver(&mut state, Some(req.clone()));
assert_eq!(state.backend.sets, 2);
assert_eq!((state.backend.builds, state.backend.sets, state.backend.suspended.len()), (1, 2, 1));
deliver(&mut state, None);
assert_eq!(state.backend.restores, [1]);
deliver(&mut state, Some(req.clone()));