#![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();
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,
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<String> {
if self.is_root() && self.0.role() == Role::Window {
return None;
}
self.0.label()
}
pub(crate) fn description(&self) -> Option<String> {
self.0.description()
}
pub(crate) fn placeholder(&self) -> Option<&str> {
self.0.placeholder()
}
pub(crate) fn value(&self) -> Option<Value> {
if let Some(toggled) = self.0.toggled() {
return Some(Value::Bool(toggled != Toggled::False));
}
if self.0.role() == Role::Tab {
return Some(Value::Bool(self.0.is_selected().unwrap_or(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()
}
fn is_container_with_selectable_children(&self) -> bool {
self.0.is_container_with_selectable_children() && self.0.role() != Role::TabList
}
pub(crate) fn is_item_like(&self) -> bool {
self.0.is_item_like() && self.0.role() != Role::Tab
}
}
pub(crate) struct PlatformNodeIvars {
context: Weak<Context>,
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<Id<AnyObject>> {
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<Id<AnyObject>> {
self.resolve_with_context(|_, context| {
context
.view
.load()
.and_then(|view| unsafe { NSAccessibility::accessibilityParent(&*view) })
})
.flatten()
}
#[method_id(accessibilityTopLevelUIElement)]
fn top_level(&self) -> Option<Id<AnyObject>> {
self.resolve_with_context(|_, context| {
context
.view
.load()
.and_then(|view| unsafe { NSAccessibility::accessibilityParent(&*view) })
})
.flatten()
}
#[method_id(accessibilityChildren)]
fn children(&self) -> Option<Id<NSArray<PlatformNode>>> {
self.children_internal()
}
#[method_id(accessibilityChildrenInNavigationOrder)]
fn children_in_navigation_order(&self) -> Option<Id<NSArray<PlatformNode>>> {
self.children_internal()
}
#[method_id(accessibilitySelectedChildren)]
fn selected_children(&self) -> Option<Id<NSArray<PlatformNode>>> {
self.resolve_with_context(|node, context| {
let wrapper = NodeWrapper(node);
if !wrapper.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::<Vec<Id<PlatformNode>>>();
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<NSAccessibilityRole> {
self.resolve(ns_role)
.unwrap_or(unsafe { NSAccessibilityUnknownRole })
.copy()
}
#[method_id(accessibilitySubrole)]
fn sub_role(&self) -> Id<NSAccessibilitySubrole> {
self.resolve(ns_sub_role)
.unwrap_or(unsafe { NSAccessibilityUnknownSubrole })
.copy()
}
#[method_id(accessibilityRoleDescription)]
fn role_description(&self) -> Option<Id<NSString>> {
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<Id<NSString>> {
self.resolve(|node| {
node.author_id().map(NSString::from_str)
})
.flatten()
}
#[method_id(accessibilityTitle)]
fn title(&self) -> Option<Id<NSString>> {
self.resolve(|node| {
let wrapper = NodeWrapper(node);
wrapper.title().map(|title| NSString::from_str(&title))
})
.flatten()
}
#[method_id(accessibilityHelp)]
fn description(&self) -> Option<Id<NSString>> {
self.resolve(|node| {
let wrapper = NodeWrapper(node);
wrapper.description().map(|description| NSString::from_str(&description))
})
.flatten()
}
#[method_id(accessibilityPlaceholderValue)]
fn placeholder(&self) -> Option<Id<NSString>> {
self.resolve(|node| {
let wrapper = NodeWrapper(node);
wrapper.placeholder().map(NSString::from_str)
})
.flatten()
}
#[method_id(accessibilityValue)]
fn value(&self) -> Option<Id<NSObject>> {
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) {
}
#[method_id(accessibilityMinValue)]
fn min_value(&self) -> Option<Id<NSNumber>> {
self.resolve(|node| {
node.min_numeric_value().map(NSNumber::new_f64)
})
.flatten()
}
#[method_id(accessibilityMaxValue)]
fn max_value(&self) -> Option<Id<NSNumber>> {
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<Id<NSString>> {
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<Id<NSString>> {
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| {
let wrapper = NodeWrapper(node);
wrapper.is_item_like()
&& node.is_selectable()
&& node.is_selected().unwrap_or(false)
})
.unwrap_or(false)
}
#[method(setAccessibilitySelected:)]
fn set_selected(&self, selected: bool) {
self.resolve_with_context(|node, context| {
let wrapper = NodeWrapper(node);
if !node.is_clickable()
|| !wrapper.is_item_like()
|| !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<Id<NSArray<PlatformNode>>> {
self.resolve_with_context(|node, context| {
let wrapper = NodeWrapper(node);
if !wrapper.is_container_with_selectable_children() {
return None;
}
let platform_nodes = node
.items(filter)
.map(|child| context.get_or_create_platform_node(child.id()))
.collect::<Vec<Id<PlatformNode>>>();
Some(NSArray::from_vec(platform_nodes))
})
.flatten()
}
#[method_id(accessibilitySelectedRows)]
fn selected_rows(&self) -> Option<Id<NSArray<PlatformNode>>> {
self.resolve_with_context(|node, context| {
let wrapper = NodeWrapper(node);
if !wrapper.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::<Vec<Id<PlatformNode>>>();
Some(NSArray::from_vec(platform_nodes))
})
.flatten()
}
#[method(accessibilityPerformPick)]
fn pick(&self) -> bool {
self.resolve_with_context(|node, context| {
let wrapper = NodeWrapper(node);
let selectable = node.is_clickable()
&& wrapper.is_item_like()
&& node.is_selectable();
if selectable {
context.do_action(ActionRequest {
action: Action::Click,
target: node.id(),
data: None,
});
}
selectable
})
.unwrap_or(false)
}
#[method_id(accessibilityLinkedUIElements)]
fn linked_ui_elements(&self) -> Option<Id<NSArray<PlatformNode>>> {
self.resolve_with_context(|node, context| {
let platform_nodes: Vec<Id<PlatformNode>> = node
.controls()
.filter(|controlled| filter(controlled) == FilterResult::Include)
.map(|controlled| context.get_or_create_platform_node(controlled.id()))
.collect();
if platform_nodes.is_empty() {
None
} else {
Some(NSArray::from_vec(platform_nodes))
}
})
.flatten()
}
#[method_id(accessibilityTabs)]
fn tabs(&self) -> Option<Id<NSArray<PlatformNode>>> {
self.resolve_with_context(|node, context| {
if node.role() != Role::TabList {
return None;
}
let platform_nodes = node
.filtered_children(filter)
.filter(|child| child.role() == Role::Tab)
.map(|tab| context.get_or_create_platform_node(tab.id()))
.collect::<Vec<Id<PlatformNode>>>();
Some(NSArray::from_vec(platform_nodes))
})
.flatten()
}
#[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:) {
return node.supports_text_ranges() && !node.is_read_only();
}
if selector == sel!(isAccessibilitySelected) {
let wrapper = NodeWrapper(node);
return wrapper.is_item_like();
}
if selector == sel!(accessibilityRows)
|| selector == sel!(accessibilitySelectedRows)
{
let wrapper = NodeWrapper(node);
return wrapper.is_container_with_selectable_children()
}
if selector == sel!(setAccessibilitySelected:)
|| selector == sel!(accessibilityPerformPick)
{
let wrapper = NodeWrapper(node);
return node.is_clickable()
&& wrapper.is_item_like()
&& node.is_selectable();
}
if selector == sel!(accessibilityTabs) {
return node.role() == Role::TabList;
}
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!(accessibilityLinkedUIElements)
|| 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<Context>, node_id: NodeId) -> Id<Self> {
let this = Self::alloc().set_ivars(PlatformNodeIvars { context, node_id });
unsafe { msg_send_id![super(this), init] }
}
fn resolve_with_context<F, T>(&self, f: F) -> Option<T>
where
F: FnOnce(&Node, &Rc<Context>) -> 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<F, T>(&self, f: F) -> Option<T>
where
F: FnOnce(&Node) -> T,
{
self.resolve_with_context(|node, _| f(node))
}
fn children_internal(&self) -> Option<Id<NSArray<PlatformNode>>> {
self.resolve_with_context(|node, context| {
let platform_nodes = node
.filtered_children(filter)
.map(|child| context.get_or_create_platform_node(child.id()))
.collect::<Vec<Id<PlatformNode>>>();
NSArray::from_vec(platform_nodes)
})
}
}