// Copyright 2022 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. // Derived from Chromium's accessibility abstraction. // Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE.chromium file. #![allow(non_upper_case_globals)] use accesskit::{ Action, ActionData, ActionRequest, NodeId, Orientation, Role, TextSelection, Toggled, }; use accesskit_consumer::{FilterResult, Node}; use objc2::{ declare_class, msg_send_id, mutability::InteriorMutable, rc::Id, runtime::{AnyObject, Sel}, sel, ClassType, DeclaredClass, }; use objc2_app_kit::*; use objc2_foundation::{ ns_string, NSArray, NSCopying, NSInteger, NSNumber, NSObject, NSPoint, NSRange, NSRect, NSString, }; use std::rc::{Rc, Weak}; use crate::{context::Context, filters::filter, util::*}; fn ns_role(node: &Node) -> &'static NSAccessibilityRole { let role = node.role(); // TODO: Handle special cases. unsafe { match role { Role::Unknown => NSAccessibilityUnknownRole, Role::TextRun => NSAccessibilityUnknownRole, Role::Cell => NSAccessibilityCellRole, Role::Label => NSAccessibilityStaticTextRole, Role::Image => NSAccessibilityImageRole, Role::Link => NSAccessibilityLinkRole, Role::Row => NSAccessibilityRowRole, Role::ListItem => NSAccessibilityGroupRole, Role::ListMarker => ns_string!("AXListMarker"), Role::TreeItem => NSAccessibilityRowRole, Role::ListBoxOption => NSAccessibilityStaticTextRole, Role::MenuItem => NSAccessibilityMenuItemRole, Role::MenuListOption => NSAccessibilityMenuItemRole, Role::Paragraph => NSAccessibilityGroupRole, Role::GenericContainer => NSAccessibilityUnknownRole, Role::CheckBox => NSAccessibilityCheckBoxRole, Role::RadioButton => NSAccessibilityRadioButtonRole, Role::TextInput | Role::SearchInput | Role::EmailInput | Role::NumberInput | Role::PasswordInput | Role::PhoneNumberInput | Role::UrlInput => NSAccessibilityTextFieldRole, Role::Button => { if node.toggled().is_some() { NSAccessibilityCheckBoxRole } else { NSAccessibilityButtonRole } } Role::DefaultButton => NSAccessibilityButtonRole, Role::Pane => NSAccessibilityUnknownRole, Role::RowHeader => NSAccessibilityCellRole, Role::ColumnHeader => NSAccessibilityCellRole, Role::RowGroup => NSAccessibilityGroupRole, Role::List => NSAccessibilityListRole, Role::Table => NSAccessibilityTableRole, Role::LayoutTableCell => NSAccessibilityGroupRole, Role::LayoutTableRow => NSAccessibilityGroupRole, Role::LayoutTable => NSAccessibilityGroupRole, Role::Switch => NSAccessibilityCheckBoxRole, Role::Menu => NSAccessibilityMenuRole, Role::MultilineTextInput => NSAccessibilityTextAreaRole, Role::DateInput | Role::DateTimeInput | Role::WeekInput | Role::MonthInput => { ns_string!("AXDateField") } Role::TimeInput => ns_string!("AXTimeField"), Role::Abbr => NSAccessibilityGroupRole, Role::Alert => NSAccessibilityGroupRole, Role::AlertDialog => NSAccessibilityGroupRole, Role::Application => NSAccessibilityGroupRole, Role::Article => NSAccessibilityGroupRole, Role::Audio => NSAccessibilityGroupRole, Role::Banner => NSAccessibilityGroupRole, Role::Blockquote => NSAccessibilityGroupRole, Role::Canvas => NSAccessibilityImageRole, Role::Caption => NSAccessibilityGroupRole, Role::Caret => NSAccessibilityUnknownRole, Role::Code => NSAccessibilityGroupRole, Role::ColorWell => NSAccessibilityColorWellRole, Role::ComboBox => NSAccessibilityPopUpButtonRole, Role::EditableComboBox => NSAccessibilityComboBoxRole, Role::Complementary => NSAccessibilityGroupRole, Role::Comment => NSAccessibilityGroupRole, Role::ContentDeletion => NSAccessibilityGroupRole, Role::ContentInsertion => NSAccessibilityGroupRole, Role::ContentInfo => NSAccessibilityGroupRole, Role::Definition => NSAccessibilityGroupRole, Role::DescriptionList => NSAccessibilityListRole, Role::DescriptionListDetail => NSAccessibilityGroupRole, Role::DescriptionListTerm => NSAccessibilityGroupRole, Role::Details => NSAccessibilityGroupRole, Role::Dialog => NSAccessibilityGroupRole, Role::Directory => NSAccessibilityListRole, Role::DisclosureTriangle => NSAccessibilityButtonRole, Role::Document => NSAccessibilityGroupRole, Role::EmbeddedObject => NSAccessibilityGroupRole, Role::Emphasis => NSAccessibilityGroupRole, Role::Feed => NSAccessibilityUnknownRole, Role::FigureCaption => NSAccessibilityGroupRole, Role::Figure => NSAccessibilityGroupRole, Role::Footer => NSAccessibilityGroupRole, Role::FooterAsNonLandmark => NSAccessibilityGroupRole, Role::Form => NSAccessibilityGroupRole, Role::Grid => NSAccessibilityTableRole, Role::Group => NSAccessibilityGroupRole, Role::Header => NSAccessibilityGroupRole, Role::HeaderAsNonLandmark => NSAccessibilityGroupRole, Role::Heading => ns_string!("Heading"), Role::Iframe => NSAccessibilityGroupRole, Role::IframePresentational => NSAccessibilityGroupRole, Role::ImeCandidate => NSAccessibilityUnknownRole, Role::Keyboard => NSAccessibilityUnknownRole, Role::Legend => NSAccessibilityGroupRole, Role::LineBreak => NSAccessibilityGroupRole, Role::ListBox => NSAccessibilityListRole, Role::Log => NSAccessibilityGroupRole, Role::Main => NSAccessibilityGroupRole, Role::Mark => NSAccessibilityGroupRole, Role::Marquee => NSAccessibilityGroupRole, Role::Math => NSAccessibilityGroupRole, Role::MenuBar => NSAccessibilityMenuBarRole, Role::MenuItemCheckBox => NSAccessibilityMenuItemRole, Role::MenuItemRadio => NSAccessibilityMenuItemRole, Role::MenuListPopup => NSAccessibilityMenuRole, Role::Meter => NSAccessibilityLevelIndicatorRole, Role::Navigation => NSAccessibilityGroupRole, Role::Note => NSAccessibilityGroupRole, Role::PluginObject => NSAccessibilityGroupRole, Role::Portal => NSAccessibilityButtonRole, Role::Pre => NSAccessibilityGroupRole, Role::ProgressIndicator => NSAccessibilityProgressIndicatorRole, Role::RadioGroup => NSAccessibilityRadioGroupRole, Role::Region => NSAccessibilityGroupRole, Role::RootWebArea => ns_string!("AXWebArea"), Role::Ruby => NSAccessibilityGroupRole, Role::RubyAnnotation => NSAccessibilityUnknownRole, Role::ScrollBar => NSAccessibilityScrollBarRole, Role::ScrollView => NSAccessibilityUnknownRole, Role::Search => NSAccessibilityGroupRole, Role::Section => NSAccessibilityGroupRole, Role::Slider => NSAccessibilitySliderRole, Role::SpinButton => NSAccessibilityIncrementorRole, Role::Splitter => NSAccessibilitySplitterRole, Role::Status => NSAccessibilityGroupRole, Role::Strong => NSAccessibilityGroupRole, Role::Suggestion => NSAccessibilityGroupRole, Role::SvgRoot => NSAccessibilityGroupRole, Role::Tab => NSAccessibilityRadioButtonRole, Role::TabList => NSAccessibilityTabGroupRole, Role::TabPanel => NSAccessibilityGroupRole, Role::Term => NSAccessibilityGroupRole, Role::Time => NSAccessibilityGroupRole, Role::Timer => NSAccessibilityGroupRole, Role::TitleBar => NSAccessibilityStaticTextRole, Role::Toolbar => NSAccessibilityToolbarRole, Role::Tooltip => NSAccessibilityGroupRole, Role::Tree => NSAccessibilityOutlineRole, Role::TreeGrid => NSAccessibilityTableRole, Role::Video => NSAccessibilityGroupRole, Role::WebView => NSAccessibilityUnknownRole, // Use the group role for Role::Window, since the NSWindow // provides the top-level accessibility object for the window. Role::Window => NSAccessibilityGroupRole, Role::PdfActionableHighlight => NSAccessibilityButtonRole, Role::PdfRoot => NSAccessibilityGroupRole, Role::GraphicsDocument => NSAccessibilityGroupRole, Role::GraphicsObject => NSAccessibilityGroupRole, Role::GraphicsSymbol => NSAccessibilityImageRole, Role::DocAbstract => NSAccessibilityGroupRole, Role::DocAcknowledgements => NSAccessibilityGroupRole, Role::DocAfterword => NSAccessibilityGroupRole, Role::DocAppendix => NSAccessibilityGroupRole, Role::DocBackLink => NSAccessibilityLinkRole, Role::DocBiblioEntry => NSAccessibilityGroupRole, Role::DocBibliography => NSAccessibilityGroupRole, Role::DocBiblioRef => NSAccessibilityGroupRole, Role::DocChapter => NSAccessibilityGroupRole, Role::DocColophon => NSAccessibilityGroupRole, Role::DocConclusion => NSAccessibilityGroupRole, Role::DocCover => NSAccessibilityImageRole, Role::DocCredit => NSAccessibilityGroupRole, Role::DocCredits => NSAccessibilityGroupRole, Role::DocDedication => NSAccessibilityGroupRole, Role::DocEndnote => NSAccessibilityGroupRole, Role::DocEndnotes => NSAccessibilityGroupRole, Role::DocEpigraph => NSAccessibilityGroupRole, Role::DocEpilogue => NSAccessibilityGroupRole, Role::DocErrata => NSAccessibilityGroupRole, Role::DocExample => NSAccessibilityGroupRole, Role::DocFootnote => NSAccessibilityGroupRole, Role::DocForeword => NSAccessibilityGroupRole, Role::DocGlossary => NSAccessibilityGroupRole, Role::DocGlossRef => NSAccessibilityLinkRole, Role::DocIndex => NSAccessibilityGroupRole, Role::DocIntroduction => NSAccessibilityGroupRole, Role::DocNoteRef => NSAccessibilityLinkRole, Role::DocNotice => NSAccessibilityGroupRole, Role::DocPageBreak => NSAccessibilitySplitterRole, Role::DocPageFooter => NSAccessibilityGroupRole, Role::DocPageHeader => NSAccessibilityGroupRole, Role::DocPageList => NSAccessibilityGroupRole, Role::DocPart => NSAccessibilityGroupRole, Role::DocPreface => NSAccessibilityGroupRole, Role::DocPrologue => NSAccessibilityGroupRole, Role::DocPullquote => NSAccessibilityGroupRole, Role::DocQna => NSAccessibilityGroupRole, Role::DocSubtitle => ns_string!("AXHeading"), Role::DocTip => NSAccessibilityGroupRole, Role::DocToc => NSAccessibilityGroupRole, Role::ListGrid => NSAccessibilityUnknownRole, Role::Terminal => NSAccessibilityTextAreaRole, } } } fn ns_sub_role(node: &Node) -> &'static NSAccessibilitySubrole { let role = node.role(); unsafe { match role { Role::Alert => ns_string!("AXApplicationAlert"), Role::AlertDialog => ns_string!("AXApplicationAlertDialog"), Role::Article => ns_string!("AXDocumentArticle"), Role::Banner => ns_string!("AXLandmarkBanner"), Role::Button if node.toggled().is_some() => NSAccessibilityToggleSubrole, Role::Code => ns_string!("AXCodeStyleGroup"), Role::Complementary => ns_string!("AXLandmarkComplementary"), Role::ContentDeletion => ns_string!("AXDeleteStyleGroup"), Role::ContentInsertion => ns_string!("AXInsertStyleGroup"), Role::ContentInfo => ns_string!("AXLandmarkContentInfo"), Role::Definition => ns_string!("AXDefinition"), Role::Dialog => NSAccessibilityDialogSubrole, Role::Document => ns_string!("AXDocument"), Role::Emphasis => ns_string!("AXEmphasisStyleGroup"), Role::Feed => ns_string!("AXApplicationGroup"), Role::Footer => ns_string!("AXLandmarkContentInfo"), Role::Form => ns_string!("AXLandmarkForm"), Role::GraphicsDocument => ns_string!("AXDocument"), Role::Group => ns_string!("AXApplicationGroup"), Role::Header => ns_string!("AXLandmarkBanner"), Role::LayoutTableCell => NSAccessibilityGroupRole, Role::LayoutTableRow => NSAccessibilityTableRowSubrole, Role::Log => ns_string!("AXApplicationLog"), Role::Main => ns_string!("AXLandmarkMain"), Role::Marquee => ns_string!("AXApplicationMarquee"), Role::Math => ns_string!("AXDocumentMath"), Role::Meter => ns_string!("AXMeter"), Role::Navigation => ns_string!("AXLandmarkNavigation"), Role::Note => ns_string!("AXDocumentNote"), Role::PasswordInput => NSAccessibilitySecureTextFieldSubrole, Role::Region => ns_string!("AXLandmarkRegion"), Role::Search => ns_string!("AXLandmarkSearch"), Role::SearchInput => NSAccessibilitySearchFieldSubrole, Role::Status => ns_string!("AXApplicationStatus"), Role::Strong => ns_string!("AXStrongStyleGroup"), Role::Switch => NSAccessibilitySwitchSubrole, Role::Tab => NSAccessibilityTabButtonSubrole, Role::TabPanel => ns_string!("AXTabPanel"), Role::Term => ns_string!("AXTerm"), Role::Time => ns_string!("AXTimeGroup"), Role::Timer => ns_string!("AXApplicationTimer"), Role::TreeItem => NSAccessibilityOutlineRowSubrole, Role::Tooltip => ns_string!("AXUserInterfaceTooltip"), _ => NSAccessibilityUnknownSubrole, } } } pub(crate) fn can_be_focused(node: &Node) -> bool { filter(node) == FilterResult::Include && node.role() != Role::Window } #[derive(PartialEq)] pub(crate) enum Value { Bool(bool), Number(f64), String(String), } pub(crate) struct NodeWrapper<'a>(pub(crate) &'a Node<'a>); impl NodeWrapper<'_> { fn is_root(&self) -> bool { self.0.is_root() } pub(crate) fn title(&self) -> Option { if self.is_root() && self.0.role() == Role::Window { // If the group element that we expose for the top-level window // includes a title, VoiceOver behavior is broken. return None; } self.0.label() } pub(crate) fn description(&self) -> Option { self.0.description() } pub(crate) fn placeholder(&self) -> Option<&str> { self.0.placeholder() } pub(crate) fn value(&self) -> Option { if let Some(toggled) = self.0.toggled() { return Some(Value::Bool(toggled != Toggled::False)); } if let Some(value) = self.0.value() { return Some(Value::String(value)); } if let Some(value) = self.0.numeric_value() { return Some(Value::Number(value)); } None } pub(crate) fn supports_text_ranges(&self) -> bool { self.0.supports_text_ranges() } pub(crate) fn raw_text_selection(&self) -> Option<&TextSelection> { self.0.raw_text_selection() } } pub(crate) struct PlatformNodeIvars { context: Weak, node_id: NodeId, } declare_class!( #[derive(Debug)] pub(crate) struct PlatformNode; unsafe impl ClassType for PlatformNode { #[inherits(NSObject)] type Super = NSAccessibilityElement; type Mutability = InteriorMutable; const NAME: &'static str = "AccessKitNode"; } impl DeclaredClass for PlatformNode { type Ivars = PlatformNodeIvars; } unsafe impl PlatformNode { #[method_id(accessibilityParent)] fn parent(&self) -> Option> { self.resolve_with_context(|node, context| { if let Some(parent) = node.filtered_parent(&filter) { Some(Id::into_super(Id::into_super(Id::into_super(context.get_or_create_platform_node(parent.id()))))) } else { context .view .load() .and_then(|view| unsafe { NSAccessibility::accessibilityParent(&*view) }) } }) .flatten() } #[method_id(accessibilityWindow)] fn window(&self) -> Option> { self.resolve_with_context(|_, context| { context .view .load() .and_then(|view| unsafe { NSAccessibility::accessibilityParent(&*view) }) }) .flatten() } #[method_id(accessibilityTopLevelUIElement)] fn top_level(&self) -> Option> { self.resolve_with_context(|_, context| { context .view .load() .and_then(|view| unsafe { NSAccessibility::accessibilityParent(&*view) }) }) .flatten() } #[method_id(accessibilityChildren)] fn children(&self) -> Option>> { self.children_internal() } #[method_id(accessibilityChildrenInNavigationOrder)] fn children_in_navigation_order(&self) -> Option>> { // For now, we assume the children are in navigation order. self.children_internal() } #[method_id(accessibilitySelectedChildren)] fn selected_children(&self) -> Option>> { self.resolve_with_context(|node, context| { if !node.is_container_with_selectable_children() { return None; } let platform_nodes = node .items(filter) .filter(|item| item.is_selected() == Some(true)) .map(|child| context.get_or_create_platform_node(child.id())) .collect::>>(); Some(NSArray::from_vec(platform_nodes)) }) .flatten() } #[method(accessibilityFrame)] fn frame(&self) -> NSRect { self.resolve_with_context(|node, context| { let view = match context.view.load() { Some(view) => view, None => { return NSRect::ZERO; } }; node.bounding_box().map_or_else( || { if node.is_root() { unsafe { NSAccessibility::accessibilityFrame(&*view) } } else { NSRect::ZERO } }, |rect| to_ns_rect(&view, rect), ) }) .unwrap_or(NSRect::ZERO) } #[method_id(accessibilityRole)] fn role(&self) -> Id { self.resolve(ns_role) .unwrap_or(unsafe { NSAccessibilityUnknownRole }) .copy() } #[method_id(accessibilitySubrole)] fn sub_role(&self) -> Id { self.resolve(ns_sub_role) .unwrap_or(unsafe { NSAccessibilityUnknownSubrole }) .copy() } #[method_id(accessibilityRoleDescription)] fn role_description(&self) -> Option> { self.resolve(|node| { if let Some(role_description) = node.role_description() { Some(NSString::from_str(role_description)) } else { unsafe { msg_send_id![super(self), accessibilityRoleDescription] } } }) .flatten() } #[method_id(accessibilityIdentifier)] fn identifier(&self) -> Option> { self.resolve(|node| { node.author_id().map(NSString::from_str) }) .flatten() } #[method_id(accessibilityTitle)] fn title(&self) -> Option> { self.resolve(|node| { let wrapper = NodeWrapper(node); wrapper.title().map(|title| NSString::from_str(&title)) }) .flatten() } #[method_id(accessibilityHelp)] fn description(&self) -> Option> { self.resolve(|node| { let wrapper = NodeWrapper(node); wrapper.description().map(|description| NSString::from_str(&description)) }) .flatten() } #[method_id(accessibilityPlaceholderValue)] fn placeholder(&self) -> Option> { self.resolve(|node| { let wrapper = NodeWrapper(node); wrapper.placeholder().map(NSString::from_str) }) .flatten() } #[method_id(accessibilityValue)] fn value(&self) -> Option> { self.resolve(|node| { let wrapper = NodeWrapper(node); wrapper.value().map(|value| match value { Value::Bool(value) => { Id::into_super(Id::into_super(NSNumber::new_bool(value))) } Value::Number(value) => { Id::into_super(Id::into_super(NSNumber::new_f64(value))) } Value::String(value) => { Id::into_super(NSString::from_str(&value)) } }) }) .flatten() } #[method(setAccessibilityValue:)] fn set_value(&self, _value: &NSObject) { // This isn't yet implemented. See the comment on this selector // in `is_selector_allowed`. } #[method_id(accessibilityMinValue)] fn min_value(&self) -> Option> { self.resolve(|node| { node.min_numeric_value().map(NSNumber::new_f64) }) .flatten() } #[method_id(accessibilityMaxValue)] fn max_value(&self) -> Option> { self.resolve(|node| { node.max_numeric_value().map(NSNumber::new_f64) }) .flatten() } #[method(accessibilityOrientation)] fn orientation(&self) -> NSAccessibilityOrientation { self.resolve(|node| { match node.orientation() { Some(Orientation::Horizontal) => NSAccessibilityOrientation::Horizontal, Some(Orientation::Vertical) => NSAccessibilityOrientation::Vertical, None => NSAccessibilityOrientation::Unknown, } }) .unwrap_or(NSAccessibilityOrientation::Unknown) } #[method(isAccessibilityElement)] fn is_accessibility_element(&self) -> bool { self.resolve(|node| filter(node) == FilterResult::Include) .unwrap_or(false) } #[method(isAccessibilityFocused)] fn is_focused(&self) -> bool { self.resolve(|node| node.is_focused() && can_be_focused(node)) .unwrap_or(false) } #[method(isAccessibilityEnabled)] fn is_enabled(&self) -> bool { self.resolve(|node| !node.is_disabled()).unwrap_or(false) } #[method(setAccessibilityFocused:)] fn set_focused(&self, focused: bool) { self.resolve_with_context(|node, context| { if focused { if node.is_focusable() { context.do_action(ActionRequest { action: Action::Focus, target: node.id(), data: None, }); } } else { let root = node.tree_state.root(); if root.is_focusable() { context.do_action(ActionRequest { action: Action::Focus, target: root.id(), data: None, }); } } }); } #[method(accessibilityPerformPress)] fn press(&self) -> bool { self.resolve_with_context(|node, context| { let clickable = node.is_clickable(); if clickable { context.do_action(ActionRequest { action: Action::Click, target: node.id(), data: None, }); } clickable }) .unwrap_or(false) } #[method(accessibilityPerformIncrement)] fn increment(&self) -> bool { self.resolve_with_context(|node, context| { let supports_increment = node.supports_increment(); if supports_increment { context.do_action(ActionRequest { action: Action::Increment, target: node.id(), data: None, }); } supports_increment }) .unwrap_or(false) } #[method(accessibilityPerformDecrement)] fn decrement(&self) -> bool { self.resolve_with_context(|node, context| { let supports_decrement = node.supports_decrement(); if supports_decrement { context.do_action(ActionRequest { action: Action::Decrement, target: node.id(), data: None, }); } supports_decrement }) .unwrap_or(false) } #[method(accessibilityNotifiesWhenDestroyed)] fn notifies_when_destroyed(&self) -> bool { true } #[method(accessibilityNumberOfCharacters)] fn number_of_characters(&self) -> NSInteger { self.resolve(|node| { if node.supports_text_ranges() { node.document_range().end().to_global_utf16_index() as _ } else { 0 } }) .unwrap_or(0) } #[method_id(accessibilitySelectedText)] fn selected_text(&self) -> Option> { self.resolve(|node| { if node.supports_text_ranges() { if let Some(range) = node.text_selection() { let text = range.text(); return Some(NSString::from_str(&text)); } } None }) .flatten() } #[method(accessibilitySelectedTextRange)] fn selected_text_range(&self) -> NSRange { self.resolve(|node| { if node.supports_text_ranges() { if let Some(range) = node.text_selection() { return to_ns_range(&range); } } NSRange::new(0, 0) }) .unwrap_or_else(|| NSRange::new(0, 0)) } #[method(accessibilityInsertionPointLineNumber)] fn insertion_point_line_number(&self) -> NSInteger { self.resolve(|node| { if node.supports_text_ranges() { if let Some(pos) = node.text_selection_focus() { return pos.to_line_index() as _; } } 0 }) .unwrap_or(0) } #[method(accessibilityRangeForLine:)] fn range_for_line(&self, line_index: NSInteger) -> NSRange { self.resolve(|node| { if node.supports_text_ranges() && line_index >= 0 { if let Some(range) = node.line_range_from_index(line_index as _) { return to_ns_range(&range); } } NSRange::new(0, 0) }) .unwrap_or_else(|| NSRange::new(0, 0)) } #[method(accessibilityRangeForPosition:)] fn range_for_position(&self, point: NSPoint) -> NSRange { self.resolve_with_context(|node, context| { let view = match context.view.load() { Some(view) => view, None => { return NSRange::new(0, 0); } }; if node.supports_text_ranges() { let point = from_ns_point(&view, node, point); let pos = node.text_position_at_point(point); return to_ns_range_for_character(&pos); } NSRange::new(0, 0) }) .unwrap_or_else(|| NSRange::new(0, 0)) } #[method_id(accessibilityStringForRange:)] fn string_for_range(&self, range: NSRange) -> Option> { self.resolve(|node| { if node.supports_text_ranges() { if let Some(range) = from_ns_range(node, range) { let text = range.text(); return Some(NSString::from_str(&text)); } } None }) .flatten() } #[method(accessibilityFrameForRange:)] fn frame_for_range(&self, range: NSRange) -> NSRect { self.resolve_with_context(|node, context| { let view = match context.view.load() { Some(view) => view, None => { return NSRect::ZERO; } }; if node.supports_text_ranges() { if let Some(range) = from_ns_range(node, range) { let rects = range.bounding_boxes(); if let Some(rect) = rects.into_iter().reduce(|rect1, rect2| rect1.union(rect2)) { return to_ns_rect(&view, rect); } } } NSRect::ZERO }) .unwrap_or(NSRect::ZERO) } #[method(accessibilityLineForIndex:)] fn line_for_index(&self, index: NSInteger) -> NSInteger { self.resolve(|node| { if node.supports_text_ranges() && index >= 0 { if let Some(pos) = node.text_position_from_global_utf16_index(index as _) { return pos.to_line_index() as _; } } 0 }) .unwrap_or(0) } #[method(accessibilityRangeForIndex:)] fn range_for_index(&self, index: NSInteger) -> NSRange { self.resolve(|node| { if node.supports_text_ranges() && index >= 0 { if let Some(pos) = node.text_position_from_global_utf16_index(index as _) { return to_ns_range_for_character(&pos); } } NSRange::new(0, 0) }) .unwrap_or_else(|| NSRange::new(0, 0)) } #[method(setAccessibilitySelectedTextRange:)] fn set_selected_text_range(&self, range: NSRange) { self.resolve_with_context(|node, context| { if node.supports_text_ranges() { if let Some(range) = from_ns_range(node, range) { context.do_action(ActionRequest { action: Action::SetTextSelection, target: node.id(), data: Some(ActionData::SetTextSelection(range.to_text_selection())), }); } } }); } #[method(isAccessibilityRequired)] fn is_required(&self) -> bool { self.resolve(|node| node.is_required()) .unwrap_or(false) } #[method(isAccessibilitySelected)] fn is_selected(&self) -> bool { self.resolve(|node| node.is_selected()).flatten().unwrap_or(false) } #[method(setAccessibilitySelected:)] fn set_selected(&self, selected: bool) { self.resolve_with_context(|node, context| { if !node.is_clickable() || !node.is_selectable() { return; } if node.is_selected() == Some(selected) { return; } context.do_action(ActionRequest { action: Action::Click, target: node.id(), data: None, }); }); } #[method_id(accessibilityRows)] fn rows(&self) -> Option>> { self.resolve_with_context(|node, context| { if !node.is_container_with_selectable_children() { return None; } let platform_nodes = node .items(filter) .map(|child| context.get_or_create_platform_node(child.id())) .collect::>>(); Some(NSArray::from_vec(platform_nodes)) }) .flatten() } #[method_id(accessibilitySelectedRows)] fn selected_rows(&self) -> Option>> { self.resolve_with_context(|node, context| { if !node.is_container_with_selectable_children() { return None; } let platform_nodes = node .items(filter) .filter(|item| item.is_selected() == Some(true)) .map(|child| context.get_or_create_platform_node(child.id())) .collect::>>(); Some(NSArray::from_vec(platform_nodes)) }) .flatten() } #[method(accessibilityPerformPick)] fn pick(&self) -> bool { self.resolve_with_context(|node, context| { let selectable = node.is_clickable() && node.is_selectable(); if selectable { context.do_action(ActionRequest { action: Action::Click, target: node.id(), data: None, }); } selectable }) .unwrap_or(false) } #[method(isAccessibilitySelectorAllowed:)] fn is_selector_allowed(&self, selector: Sel) -> bool { self.resolve(|node| { if selector == sel!(setAccessibilityFocused:) { return node.is_focusable(); } if selector == sel!(accessibilityPerformPress) { return node.is_clickable(); } if selector == sel!(accessibilityPerformIncrement) { return node.supports_increment(); } if selector == sel!(accessibilityPerformDecrement) { return node.supports_decrement(); } if selector == sel!(accessibilityNumberOfCharacters) || selector == sel!(accessibilitySelectedText) || selector == sel!(accessibilitySelectedTextRange) || selector == sel!(accessibilityInsertionPointLineNumber) || selector == sel!(accessibilityRangeForLine:) || selector == sel!(accessibilityRangeForPosition:) || selector == sel!(accessibilityStringForRange:) || selector == sel!(accessibilityFrameForRange:) || selector == sel!(accessibilityLineForIndex:) || selector == sel!(accessibilityRangeForIndex:) || selector == sel!(setAccessibilitySelectedTextRange:) { return node.supports_text_ranges(); } if selector == sel!(setAccessibilityValue:) { // Our implementation of this currently does nothing, // and it's not clear if VoiceOver ever actually uses it, // but it must be allowed for editable text in order to get // the expected VoiceOver behavior. return node.supports_text_ranges() && !node.is_read_only(); } if selector == sel!(isAccessibilitySelected) { return node.is_selectable(); } if selector == sel!(accessibilityRows) || selector == sel!(accessibilitySelectedRows) { return node.is_container_with_selectable_children() } if selector == sel!(setAccessibilitySelected:) || selector == sel!(accessibilityPerformPick) { return node.is_clickable() && node.is_selectable(); } selector == sel!(accessibilityParent) || selector == sel!(accessibilityChildren) || selector == sel!(accessibilityChildrenInNavigationOrder) || selector == sel!(accessibilitySelectedChildren) || selector == sel!(accessibilityFrame) || selector == sel!(accessibilityRole) || selector == sel!(accessibilitySubrole) || selector == sel!(isAccessibilityEnabled) || selector == sel!(accessibilityWindow) || selector == sel!(accessibilityTopLevelUIElement) || selector == sel!(accessibilityRoleDescription) || selector == sel!(accessibilityIdentifier) || selector == sel!(accessibilityTitle) || selector == sel!(accessibilityHelp) || selector == sel!(accessibilityPlaceholderValue) || selector == sel!(accessibilityValue) || selector == sel!(accessibilityMinValue) || selector == sel!(accessibilityMaxValue) || selector == sel!(isAccessibilityRequired) || selector == sel!(accessibilityOrientation) || selector == sel!(isAccessibilityElement) || selector == sel!(isAccessibilityFocused) || selector == sel!(accessibilityNotifiesWhenDestroyed) || selector == sel!(isAccessibilitySelectorAllowed:) }) .unwrap_or(false) } } ); impl PlatformNode { pub(crate) fn new(context: Weak, node_id: NodeId) -> Id { let this = Self::alloc().set_ivars(PlatformNodeIvars { context, node_id }); unsafe { msg_send_id![super(this), init] } } fn resolve_with_context(&self, f: F) -> Option where F: FnOnce(&Node, &Rc) -> T, { let context = self.ivars().context.upgrade()?; let tree = context.tree.borrow(); let state = tree.state(); let node = state.node_by_id(self.ivars().node_id)?; Some(f(&node, &context)) } fn resolve(&self, f: F) -> Option where F: FnOnce(&Node) -> T, { self.resolve_with_context(|node, _| f(node)) } fn children_internal(&self) -> Option>> { self.resolve_with_context(|node, context| { let platform_nodes = node .filtered_children(filter) .map(|child| context.get_or_create_platform_node(child.id())) .collect::>>(); NSArray::from_vec(platform_nodes) }) } }