Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions adapters/macos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ objc2-app-kit = { version = "0.2.0", features = [
"NSView",
"NSWindow",
] }

[[test]]
name = "kvo_teardown"
harness = false
14 changes: 14 additions & 0 deletions adapters/macos/src/subclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,24 @@ impl SubclassingAdapter {
}
}

fn has_our_subclass(view: &NSView, prev_class: &'static AnyClass) -> bool {
let current: *const AnyClass = view.class();
let subclasses = SUBCLASSES.lock().unwrap();
subclasses
.iter()
.any(|entry| std::ptr::eq(entry.0, prev_class) && std::ptr::eq(entry.1, current))
}

impl Drop for SubclassingAdapter {
fn drop(&mut self) {
let prev_class = self.associated.ivars().prev_class;
let view = Id::as_ptr(&self.view) as *mut NSView;
// KVO may have installed its own subclass on top of ours (AppKit
// observes the first responder). Resetting the class would break
// those observers, so leave the view as it is in that case.
if !has_our_subclass(unsafe { &*view }, prev_class) {
return;
}
unsafe { object_setClass(view as *mut _, (prev_class as *const AnyClass).cast()) };
unsafe {
objc_setAssociatedObject(
Expand Down
85 changes: 85 additions & 0 deletions adapters/macos/tests/kvo_teardown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2026 The AccessKit Authors. All rights reserved.
// Licensed under the Apache License, Version 2.0 (found in
// the LICENSE-APACHE file) or the MIT license (found in
// the LICENSE-MIT file), at your option.

// This test doesn't use the default harness, because the adapter
// must be created on the main thread.

use std::ffi::c_void;

use accesskit::{ActionHandler, ActionRequest, ActivationHandler, TreeUpdate};
use accesskit_macos::SubclassingAdapter;
use objc2::rc::Id;
use objc2::runtime::AnyObject;
use objc2::{class, msg_send, msg_send_id};
use objc2_app_kit::NSView;
use objc2_foundation::{NSArray, NSObject, NSString};

struct NoTree;

impl ActivationHandler for NoTree {
fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
None
}
}

struct NoActions;

impl ActionHandler for NoActions {
fn do_action(&mut self, _request: ActionRequest) {}
}

fn new_view() -> Id<NSView> {
unsafe { msg_send_id![class!(NSView), new] }
}

fn adapter_for(view: &NSView) -> SubclassingAdapter {
unsafe { SubclassingAdapter::new(view as *const NSView as *mut c_void, NoTree, NoActions) }
}

fn class_name(view: &NSView) -> String {
let object: &AnyObject = view;
object.class().name().to_owned()
}

fn an_unobserved_view_gets_its_class_back() {
let view = new_view();
let adapter = adapter_for(&view);
assert_eq!(class_name(&view), "AccessKitSubclassOfNSView");
drop(adapter);
assert_eq!(class_name(&view), "NSView");
drop(adapter_for(&view));
assert_eq!(class_name(&view), "NSView");
}

// AppKit observes the first responder and can remove its observers
// after the adapter is dropped.
fn an_observed_view_keeps_its_observers() {
let view = new_view();
let adapter = adapter_for(&view);
let observer = NSObject::new();
let key_path = NSString::from_str("nextResponder");
let no_context: *mut c_void = std::ptr::null_mut();
let _: () = unsafe {
msg_send![&view, addObserver: &*observer, forKeyPath: &*key_path, options: 0usize, context: no_context]
};
assert_eq!(
class_name(&view),
"NSKVONotifying_AccessKitSubclassOfNSView"
);

drop(adapter);
assert_eq!(
class_name(&view),
"NSKVONotifying_AccessKitSubclassOfNSView"
);
let _children: *mut NSArray<NSObject> = unsafe { msg_send![&view, accessibilityChildren] };
let _: () = unsafe { msg_send![&view, removeObserver: &*observer, forKeyPath: &*key_path] };
assert_eq!(class_name(&view), "AccessKitSubclassOfNSView");
}

fn main() {
an_unobserved_view_gets_its_class_back();
an_observed_view_keeps_its_observers();
}