diff --git a/platforms/ios/src/event.rs b/platforms/ios/src/event.rs index 1f9945ef..b38c900b 100644 --- a/platforms/ios/src/event.rs +++ b/platforms/ios/src/event.rs @@ -11,15 +11,18 @@ use objc2_foundation::{ }; use objc2_ui_kit::{ UIAccessibilityAnnouncementNotification, UIAccessibilityLayoutChangedNotification, - UIAccessibilityNotifications, UIAccessibilityPostNotification, UIAccessibilityPriority, - UIAccessibilityPriorityHigh, UIAccessibilityPriorityLow, - UIAccessibilityScreenChangedNotification, UIAccessibilitySpeechAttributeAnnouncementPriority, - UIAccessibilitySpeechAttributeQueueAnnouncement, + UIAccessibilityNotifications, UIAccessibilityPostNotification, + UIAccessibilityScreenChangedNotification, UIAccessibilitySpeechAttributeQueueAnnouncement, }; use std::collections::VecDeque; use std::rc::Rc; -use crate::{context::Context, filters::filter, node::PlatformNode}; +use crate::{ + context::Context, + filters::filter, + node::PlatformNode, + util::{announcement_priority_high, announcement_priority_key, announcement_priority_low}, +}; pub(crate) enum QueuedEvent { Generic { @@ -29,7 +32,7 @@ pub(crate) enum QueuedEvent { NodeDestroyed(NodeId), Announcement { text: String, - priority: &'static UIAccessibilityPriority, + assertive: bool, }, } @@ -37,11 +40,7 @@ impl QueuedEvent { fn live_region_announcement(text: String, priority: Live) -> Self { Self::Announcement { text, - priority: if priority == Live::Assertive { - unsafe { UIAccessibilityPriorityHigh } - } else { - unsafe { UIAccessibilityPriorityLow } - }, + assertive: priority == Live::Assertive, } } @@ -61,32 +60,46 @@ impl QueuedEvent { Self::NodeDestroyed(node_id) => { context.remove_platform_node(node_id); } - Self::Announcement { text, priority } => { - Self::raise_announcement(&text, priority); + Self::Announcement { text, assertive } => { + Self::raise_announcement(&text, assertive); } } } - fn raise_announcement(text: &str, priority: &'static UIAccessibilityPriority) { + fn raise_announcement(text: &str, assertive: bool) { let text = NSString::from_str(text); - let mut attrs: objc2::rc::Retained> = - NSMutableDictionary::new(); - unsafe { - attrs.setObject_forKey( - priority, - ProtocolObject::from_ref(UIAccessibilitySpeechAttributeAnnouncementPriority), - ); - attrs.setObject_forKey( - &*NSNumber::new_bool(true), - ProtocolObject::from_ref(UIAccessibilitySpeechAttributeQueueAnnouncement), - ); - } - let announcement = unsafe { NSAttributedString::new_with_attributes(&text, &attrs) }; - unsafe { - UIAccessibilityPostNotification( - UIAccessibilityAnnouncementNotification, - Some(&announcement), - ); + let priority = if assertive { + announcement_priority_high() + } else { + announcement_priority_low() + }; + match (priority, announcement_priority_key()) { + (Some(priority), Some(priority_key)) => { + let mut attrs: objc2::rc::Retained< + NSMutableDictionary, + > = NSMutableDictionary::new(); + unsafe { + attrs.setObject_forKey(priority, ProtocolObject::from_ref(priority_key)); + attrs.setObject_forKey( + &*NSNumber::new_bool(true), + ProtocolObject::from_ref(UIAccessibilitySpeechAttributeQueueAnnouncement), + ); + } + let announcement = + unsafe { NSAttributedString::new_with_attributes(&text, &attrs) }; + unsafe { + UIAccessibilityPostNotification( + UIAccessibilityAnnouncementNotification, + Some(&announcement), + ); + } + } + _ => unsafe { + UIAccessibilityPostNotification( + UIAccessibilityAnnouncementNotification, + Some(&*text), + ); + }, } } } diff --git a/platforms/ios/src/node.rs b/platforms/ios/src/node.rs index c7e317f1..b6f8e247 100644 --- a/platforms/ios/src/node.rs +++ b/platforms/ios/src/node.rs @@ -23,14 +23,14 @@ use objc2_ui_kit::{ UIAccessibilityTraitButton, UIAccessibilityTraitHeader, UIAccessibilityTraitImage, UIAccessibilityTraitLink, UIAccessibilityTraitNone, UIAccessibilityTraitNotEnabled, UIAccessibilityTraitSearchField, UIAccessibilityTraitSelected, UIAccessibilityTraitStaticText, - UIAccessibilityTraitToggleButton, UIAccessibilityTraitUpdatesFrequently, UIAccessibilityTraits, + UIAccessibilityTraitUpdatesFrequently, UIAccessibilityTraits, }; use std::rc::{Rc, Weak}; use crate::{ context::Context, filters::{filter, filter_for_is_accessibility_element}, - util::{UIAccessibilityExpandedStatus, to_cg_rect, to_screen_rect}, + util::{UIAccessibilityExpandedStatus, to_cg_rect, to_screen_rect, toggle_button_trait}, }; #[derive(Debug, PartialEq)] @@ -139,7 +139,7 @@ impl NodeWrapper<'_> { } if self.0.toggled().is_some() { - traits |= unsafe { UIAccessibilityTraitToggleButton }; + traits |= toggle_button_trait(); } if self.0.is_selected() == Some(true) { @@ -710,7 +710,7 @@ mod tests { node.set_toggled(Toggled::False); let t = node_traits(&node); assert!(t & unsafe { UIAccessibilityTraitButton } != 0); - assert!(t & unsafe { UIAccessibilityTraitToggleButton } != 0); + assert!(t & toggle_button_trait() != 0); } #[test] @@ -719,7 +719,7 @@ mod tests { node.set_toggled(Toggled::True); let t = node_traits(&node); assert!(t & unsafe { UIAccessibilityTraitButton } != 0); - assert!(t & unsafe { UIAccessibilityTraitToggleButton } != 0); + assert!(t & toggle_button_trait() != 0); } #[test] @@ -745,7 +745,7 @@ mod tests { let mut node = NodeBuilder::new(Role::CheckBox); node.set_toggled(toggled); assert!( - node_traits(&node) & unsafe { UIAccessibilityTraitToggleButton } != 0, + node_traits(&node) & toggle_button_trait() != 0, "toggled {toggled:?}", ); } @@ -821,7 +821,7 @@ mod tests { assert!(t & unsafe { UIAccessibilityTraitButton } != 0); assert!(t & unsafe { UIAccessibilityTraitNotEnabled } == 0); assert!(t & unsafe { UIAccessibilityTraitSelected } == 0); - assert!(t & unsafe { UIAccessibilityTraitToggleButton } == 0); + assert!(t & toggle_button_trait() == 0); assert!(t & unsafe { UIAccessibilityTraitUpdatesFrequently } == 0); assert!(t & unsafe { UIAccessibilityTraitAllowsDirectInteraction } == 0); } @@ -864,7 +864,7 @@ mod tests { node.set_disabled(); let t = node_traits(&node); assert!(t & unsafe { UIAccessibilityTraitButton } != 0); - assert!(t & unsafe { UIAccessibilityTraitToggleButton } != 0); + assert!(t & toggle_button_trait() != 0); assert!(t & unsafe { UIAccessibilityTraitNotEnabled } != 0); } diff --git a/platforms/ios/src/util.rs b/platforms/ios/src/util.rs index 67ee07aa..3643f8b0 100644 --- a/platforms/ios/src/util.rs +++ b/platforms/ios/src/util.rs @@ -6,8 +6,13 @@ use accesskit::Point; use accesskit_consumer::Node; use objc2::encode::{Encode, Encoding, RefEncode}; -use objc2_foundation::{CGPoint, CGRect, CGSize, NSInteger}; -use objc2_ui_kit::{UIAccessibilityConvertFrameToScreenCoordinates, UICoordinateSpace, UIView}; +use objc2_foundation::{CGPoint, CGRect, CGSize, NSAttributedStringKey, NSInteger, NSString}; +use objc2_ui_kit::{ + UIAccessibilityConvertFrameToScreenCoordinates, UIAccessibilityPriority, UIAccessibilityTraits, + UICoordinateSpace, UIView, +}; +use std::ffi::{c_char, c_void}; +use std::sync::OnceLock; // TODO: Remove once we update to objc2 0.6 #[repr(transparent)] @@ -56,3 +61,44 @@ pub(crate) fn to_cg_rect(view: &UIView, rect: accesskit::Rect) -> CGRect { }; to_screen_rect(view, local_rect) } + +const RTLD_DEFAULT: *mut c_void = -2isize as *mut c_void; + +unsafe extern "C" { + fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; +} + +pub(crate) fn toggle_button_trait() -> UIAccessibilityTraits { + static TRAIT: OnceLock = OnceLock::new(); + *TRAIT.get_or_init(|| unsafe { + let symbol = dlsym(RTLD_DEFAULT, c"UIAccessibilityTraitToggleButton".as_ptr()); + if symbol.is_null() { + 0 + } else { + *symbol.cast::() + } + }) +} + +fn resolve_nsstring_const(symbol: *const c_char) -> Option<&'static NSString> { + unsafe { + let slot = dlsym(RTLD_DEFAULT, symbol); + if slot.is_null() { + None + } else { + (*slot.cast::<*const NSString>()).as_ref() + } + } +} + +pub(crate) fn announcement_priority_high() -> Option<&'static UIAccessibilityPriority> { + resolve_nsstring_const(c"UIAccessibilityPriorityHigh".as_ptr()) +} + +pub(crate) fn announcement_priority_low() -> Option<&'static UIAccessibilityPriority> { + resolve_nsstring_const(c"UIAccessibilityPriorityLow".as_ptr()) +} + +pub(crate) fn announcement_priority_key() -> Option<&'static NSAttributedStringKey> { + resolve_nsstring_const(c"UIAccessibilitySpeechAttributeAnnouncementPriority".as_ptr()) +}