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
15 changes: 15 additions & 0 deletions consumer/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,21 @@ impl<'a> Node<'a> {
.map(|description| description.to_string())
}

pub fn url(&self) -> Option<&str> {
self.data().url()
}

pub fn supports_url(&self) -> bool {
matches!(
self.role(),
Role::Link
| Role::DocBackLink
| Role::DocBiblioRef
| Role::DocGlossRef
| Role::DocNoteRef
) && self.url().is_some()
}

fn is_empty_text_input(&self) -> bool {
let mut text_runs = self.text_runs();
if let Some(first_text_run) = text_runs.next() {
Expand Down
26 changes: 26 additions & 0 deletions platforms/android/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ impl NodeWrapper<'_> {
self.0.label()
}

fn target_url(&self) -> Option<&str> {
// Use supports_url() for link-type roles, plus Image as Android-specific
if self.0.supports_url() || self.0.role() == Role::Image {
self.0.url()
} else {
None
}
}

pub(crate) fn text(&self) -> Option<String> {
self.0.value().or_else(|| {
self.0
Expand Down Expand Up @@ -321,6 +330,23 @@ impl NodeWrapper<'_> {
.unwrap();
}

if let Some(url) = self.target_url() {
let extras = env
.call_method(node_info, "getExtras", "()Landroid/os/Bundle;", &[])
.unwrap()
.l()
.unwrap();
let key = env.new_string("url").unwrap();
let value = env.new_string(url).unwrap();
env.call_method(
&extras,
"putString",
"(Ljava/lang/String;Ljava/lang/String;)V",
&[(&key).into(), (&value).into()],
)
.unwrap();
}

let class_name = env.new_string(self.class_name()).unwrap();
env.call_method(
node_info,
Expand Down
56 changes: 56 additions & 0 deletions platforms/atspi-common/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,10 @@ impl NodeWrapper<'_> {
self.current_value().is_some()
}

fn supports_hyperlink(&self) -> bool {
self.0.supports_url()
}

pub(crate) fn interfaces(&self) -> InterfaceSet {
let mut interfaces = InterfaceSet::new(Interface::Accessible);
if self.supports_action() {
Expand All @@ -435,6 +439,9 @@ impl NodeWrapper<'_> {
if self.supports_component() {
interfaces.insert(Interface::Component);
}
if self.supports_hyperlink() {
interfaces.insert(Interface::Hyperlink);
}
if self.supports_selection() {
interfaces.insert(Interface::Selection);
}
Expand Down Expand Up @@ -908,6 +915,13 @@ impl PlatformNode {
})
}

pub fn supports_hyperlink(&self) -> Result<bool> {
self.resolve(|node| {
let wrapper = NodeWrapper(&node);
Ok(wrapper.supports_hyperlink())
})
}

pub fn supports_selection(&self) -> Result<bool> {
self.resolve(|node| {
let wrapper = NodeWrapper(&node);
Expand Down Expand Up @@ -1065,6 +1079,48 @@ impl PlatformNode {
Ok(true)
}

pub fn n_anchors(&self) -> Result<i32> {
self.resolve(|node| if node.url().is_some() { Ok(1) } else { Ok(0) })
}

pub fn hyperlink_start_index(&self) -> Result<i32> {
self.resolve(|_| {
// TODO: Support rich text
Ok(-1)
})
}

pub fn hyperlink_end_index(&self) -> Result<i32> {
self.resolve(|_| {
// TODO: Support rich text
Ok(-1)
})
}

pub fn hyperlink_object(&self, index: i32) -> Result<Option<NodeId>> {
self.resolve(|_| {
if index == 0 {
Ok(Some(self.id))
} else {
Ok(None)
}
})
}

pub fn uri(&self, index: i32) -> Result<String> {
self.resolve(|node| {
if index == 0 {
Ok(node.url().map(|s| s.to_string()).unwrap_or_default())
} else {
Ok(String::new())
}
})
}

pub fn hyperlink_is_valid(&self) -> Result<bool> {
self.resolve(|node| Ok(node.url().is_some()))
}

pub fn n_selected_children(&self) -> Result<i32> {
self.resolve_for_selection(|node| {
node.items(filter)
Expand Down
51 changes: 51 additions & 0 deletions platforms/atspi-common/src/simplified.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,57 @@ impl Accessible {
}
}

pub fn supports_hyperlink(&self) -> Result<bool> {
match self {
Self::Node(node) => node.supports_hyperlink(),
Self::Root(_) => Ok(false),
}
}

pub fn n_anchors(&self) -> Result<i32> {
match self {
Self::Node(node) => node.n_anchors(),
Self::Root(_) => Err(Error::UnsupportedInterface),
}
}

pub fn hyperlink_start_index(&self) -> Result<i32> {
match self {
Self::Node(node) => node.hyperlink_start_index(),
Self::Root(_) => Err(Error::UnsupportedInterface),
}
}

pub fn hyperlink_end_index(&self) -> Result<i32> {
match self {
Self::Node(node) => node.hyperlink_end_index(),
Self::Root(_) => Err(Error::UnsupportedInterface),
}
}

pub fn hyperlink_object(&self, index: i32) -> Result<Option<Self>> {
match self {
Self::Node(node) => node
.hyperlink_object(index)
.map(|id| id.map(|id| Self::Node(node.relative(id)))),
Self::Root(_) => Err(Error::UnsupportedInterface),
}
}

pub fn uri(&self, index: i32) -> Result<String> {
match self {
Self::Node(node) => node.uri(index),
Self::Root(_) => Err(Error::UnsupportedInterface),
}
}

pub fn hyperlink_is_valid(&self) -> Result<bool> {
match self {
Self::Node(node) => node.hyperlink_is_valid(),
Self::Root(_) => Err(Error::UnsupportedInterface),
}
}

pub fn supports_selection(&self) -> Result<bool> {
match self {
Self::Node(node) => node.supports_selection(),
Expand Down
16 changes: 15 additions & 1 deletion platforms/macos/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use objc2::{
use objc2_app_kit::*;
use objc2_foundation::{
ns_string, NSArray, NSCopying, NSInteger, NSNumber, NSObject, NSObjectProtocol, NSPoint,
NSRange, NSRect, NSString,
NSRange, NSRect, NSString, NSURL,
};
use std::rc::{Rc, Weak};

Expand Down Expand Up @@ -588,6 +588,17 @@ declare_class!(
.flatten()
}

#[method_id(accessibilityURL)]
fn url(&self) -> Option<Id<NSURL>> {
self.resolve(|node| {
node.supports_url().then(|| node.url()).flatten().and_then(|url| {
let ns_string = NSString::from_str(url);
unsafe { NSURL::URLWithString(&ns_string) }
})
})
.flatten()
}

#[method(accessibilityOrientation)]
fn orientation(&self) -> NSAccessibilityOrientation {
self.resolve(|node| {
Expand Down Expand Up @@ -1092,6 +1103,9 @@ declare_class!(
if selector == sel!(accessibilityAttributeValue:) {
return node.has_braille_label() || node.has_braille_role_description()
}
if selector == sel!(accessibilityURL) {
return node.supports_url();
}
selector == sel!(accessibilityParent)
|| selector == sel!(accessibilityChildren)
|| selector == sel!(accessibilityChildrenInNavigationOrder)
Expand Down
11 changes: 11 additions & 0 deletions platforms/unix/src/atspi/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ impl Bus {
self.register_interface(&path, ValueInterface::new(node.clone()))
.await?;
}
if new_interfaces.contains(Interface::Hyperlink) {
self.register_interface(
&path,
HyperlinkInterface::new(bus_name.clone(), node.clone()),
)
.await?;
}

Ok(())
}
Expand Down Expand Up @@ -181,6 +188,10 @@ impl Bus {
if old_interfaces.contains(Interface::Value) {
self.unregister_interface::<ValueInterface>(&path).await?;
}
if old_interfaces.contains(Interface::Hyperlink) {
self.unregister_interface::<HyperlinkInterface>(&path)
.await?;
}

Ok(())
}
Expand Down
63 changes: 63 additions & 0 deletions platforms/unix/src/atspi/interfaces/hyperlink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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.

use accesskit_atspi_common::PlatformNode;
use zbus::{fdo, interface, names::OwnedUniqueName};

use crate::atspi::{ObjectId, OwnedObjectAddress};

pub(crate) struct HyperlinkInterface {
bus_name: OwnedUniqueName,
node: PlatformNode,
}

impl HyperlinkInterface {
pub fn new(bus_name: OwnedUniqueName, node: PlatformNode) -> Self {
Self { bus_name, node }
}

fn map_error(&self) -> impl '_ + FnOnce(accesskit_atspi_common::Error) -> fdo::Error {
|error| crate::util::map_error_from_node(&self.node, error)
}
}

#[interface(name = "org.a11y.atspi.Hyperlink")]
impl HyperlinkInterface {
#[zbus(property)]
fn n_anchors(&self) -> fdo::Result<i32> {
self.node.n_anchors().map_err(self.map_error())
}

#[zbus(property)]
fn start_index(&self) -> fdo::Result<i32> {
self.node.hyperlink_start_index().map_err(self.map_error())
}

#[zbus(property)]
fn end_index(&self) -> fdo::Result<i32> {
self.node.hyperlink_end_index().map_err(self.map_error())
}

fn get_object(&self, index: i32) -> fdo::Result<(OwnedObjectAddress,)> {
let object = self
.node
.hyperlink_object(index)
.map_err(self.map_error())?
.map(|node| ObjectId::Node {
adapter: self.node.adapter_id(),
node,
});
Ok(super::optional_object_address(&self.bus_name, object))
}

#[zbus(name = "GetURI")]
fn get_uri(&self, index: i32) -> fdo::Result<String> {
self.node.uri(index).map_err(self.map_error())
}

fn is_valid(&self) -> fdo::Result<bool> {
self.node.hyperlink_is_valid().map_err(self.map_error())
}
}
2 changes: 2 additions & 0 deletions platforms/unix/src/atspi/interfaces/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod accessible;
mod action;
mod application;
mod component;
mod hyperlink;
mod selection;
mod text;
mod value;
Expand All @@ -32,6 +33,7 @@ pub(crate) use accessible::*;
pub(crate) use action::*;
pub(crate) use application::*;
pub(crate) use component::*;
pub(crate) use hyperlink::*;
pub(crate) use selection::*;
pub(crate) use text::*;
pub(crate) use value::*;
13 changes: 12 additions & 1 deletion platforms/windows/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ use accesskit::{
Orientation, Point, Role, SortDirection, Toggled,
};
use accesskit_consumer::{FilterResult, Node, TreeState};
use std::sync::{atomic::Ordering, Arc, Weak};
use std::{
fmt::Write,
sync::{atomic::Ordering, Arc, Weak},
};
use windows::{
core::*,
Win32::{
Expand Down Expand Up @@ -568,6 +571,9 @@ impl NodeWrapper<'_> {
}

fn is_value_pattern_supported(&self) -> bool {
if self.0.supports_url() {
return true;
}
self.0.has_value() && !self.0.label_comes_from_value()
}

Expand All @@ -576,6 +582,11 @@ impl NodeWrapper<'_> {
}

fn value(&self) -> WideString {
if let Some(url) = self.0.supports_url().then(|| self.0.url()).flatten() {
let mut result = WideString::default();
result.write_str(url).unwrap();
return result;
}
let mut result = WideString::default();
self.0.write_value(&mut result).unwrap();
result
Expand Down