diff --git a/adapters/atspi-common/src/node.rs b/adapters/atspi-common/src/node.rs index a05062649..7f012660c 100644 --- a/adapters/atspi-common/src/node.rs +++ b/adapters/atspi-common/src/node.rs @@ -481,6 +481,13 @@ impl NodeWrapper<'_> { self.0.supports_url() } + fn supports_image(&self) -> bool { + matches!( + self.0.role(), + Role::Canvas | Role::DocCover | Role::GraphicsSymbol | Role::Image | Role::SvgRoot + ) + } + fn supports_selection(&self) -> bool { self.0.is_container_with_selectable_children() } @@ -510,6 +517,9 @@ impl NodeWrapper<'_> { if self.supports_hyperlink() { interfaces.insert(Interface::Hyperlink); } + if self.supports_image() { + interfaces.insert(Interface::Image); + } if self.supports_selection() { interfaces.insert(Interface::Selection); } @@ -1045,6 +1055,10 @@ impl PlatformNode { }) } + pub fn supports_image(&self) -> Result { + self.resolve(|node| Ok(NodeWrapper(&node).supports_image())) + } + pub fn supports_selection(&self) -> Result { self.resolve(|node| { let wrapper = NodeWrapper(&node); @@ -1267,6 +1281,24 @@ impl PlatformNode { self.resolve(|node| Ok(node.url().is_some())) } + pub fn image_description(&self) -> Result { + self.description() + } + + pub fn image_extents(&self, coord_type: CoordType) -> Result { + self.extents(coord_type) + } + + pub fn image_position(&self, coord_type: CoordType) -> Result<(i32, i32)> { + let extents = self.image_extents(coord_type)?; + Ok((extents.x, extents.y)) + } + + pub fn image_size(&self) -> Result<(i32, i32)> { + let extents = self.image_extents(CoordType::Window)?; + Ok((extents.width, extents.height)) + } + pub fn n_selected_children(&self) -> Result { self.resolve_for_selection(|node| { node.items(filter) diff --git a/adapters/atspi-common/src/simplified.rs b/adapters/atspi-common/src/simplified.rs index ba814cb21..c1fec212d 100644 --- a/adapters/atspi-common/src/simplified.rs +++ b/adapters/atspi-common/src/simplified.rs @@ -324,6 +324,41 @@ impl Accessible { } } + pub fn supports_image(&self) -> Result { + match self { + Self::Node(node) => node.supports_image(), + Self::Root(_) => Ok(false), + } + } + + pub fn image_description(&self) -> Result { + match self { + Self::Node(node) => node.image_description(), + Self::Root(_) => Err(Error::UnsupportedInterface), + } + } + + pub fn image_extents(&self, coord_type: CoordType) -> Result { + match self { + Self::Node(node) => node.image_extents(coord_type), + Self::Root(_) => Err(Error::UnsupportedInterface), + } + } + + pub fn image_position(&self, coord_type: CoordType) -> Result<(i32, i32)> { + match self { + Self::Node(node) => node.image_position(coord_type), + Self::Root(_) => Err(Error::UnsupportedInterface), + } + } + + pub fn image_size(&self) -> Result<(i32, i32)> { + match self { + Self::Node(node) => node.image_size(), + Self::Root(_) => Err(Error::UnsupportedInterface), + } + } + pub fn supports_selection(&self) -> Result { match self { Self::Node(node) => node.supports_selection(), diff --git a/adapters/unix/src/atspi/bus.rs b/adapters/unix/src/atspi/bus.rs index 9375ff5eb..336818534 100644 --- a/adapters/unix/src/atspi/bus.rs +++ b/adapters/unix/src/atspi/bus.rs @@ -157,6 +157,10 @@ impl Bus { ) .await?; } + if new_interfaces.contains(Interface::Image) { + self.register_interface(&path, ImageInterface::new(node.clone())) + .await?; + } if new_interfaces.contains(Interface::Selection) { self.register_interface( &path, @@ -221,6 +225,9 @@ impl Bus { self.unregister_interface::(&path) .await?; } + if old_interfaces.contains(Interface::Image) { + self.unregister_interface::(&path).await?; + } if old_interfaces.contains(Interface::Selection) { self.unregister_interface::(&path) .await?; diff --git a/adapters/unix/src/atspi/interfaces/image.rs b/adapters/unix/src/atspi/interfaces/image.rs new file mode 100644 index 000000000..183e9437e --- /dev/null +++ b/adapters/unix/src/atspi/interfaces/image.rs @@ -0,0 +1,50 @@ +// 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, Rect}; +use atspi::CoordType; +use zbus::{fdo, interface}; + +pub(crate) struct ImageInterface(PlatformNode); + +impl ImageInterface { + pub fn new(node: PlatformNode) -> Self { + Self(node) + } + + fn map_error(&self) -> impl '_ + FnOnce(accesskit_atspi_common::Error) -> fdo::Error { + |error| crate::util::map_error_from_node(&self.0, error) + } +} + +#[interface(name = "org.a11y.atspi.Image")] +impl ImageInterface { + #[zbus(property)] + fn image_description(&self) -> fdo::Result { + self.0.image_description().map_err(self.map_error()) + } + + #[zbus(property)] + fn image_locale(&self) -> fdo::Result { + Err(fdo::Error::NotSupported( + "image locale is not supported".into(), + )) + } + + fn get_image_extents(&self, coord_type: CoordType) -> fdo::Result<(Rect,)> { + self.0 + .image_extents(coord_type) + .map(|rect| (rect,)) + .map_err(self.map_error()) + } + + fn get_image_position(&self, coord_type: CoordType) -> fdo::Result<(i32, i32)> { + self.0.image_position(coord_type).map_err(self.map_error()) + } + + fn get_image_size(&self) -> fdo::Result<(i32, i32)> { + self.0.image_size().map_err(self.map_error()) + } +} diff --git a/adapters/unix/src/atspi/interfaces/mod.rs b/adapters/unix/src/atspi/interfaces/mod.rs index 99bdbd294..239413b40 100644 --- a/adapters/unix/src/atspi/interfaces/mod.rs +++ b/adapters/unix/src/atspi/interfaces/mod.rs @@ -11,6 +11,7 @@ mod component; mod document; mod editable_text; mod hyperlink; +mod image; mod selection; mod text; mod value; @@ -40,6 +41,7 @@ pub(crate) use component::*; pub(crate) use document::*; pub(crate) use editable_text::*; pub(crate) use hyperlink::*; +pub(crate) use image::*; pub(crate) use selection::*; pub(crate) use text::*; pub(crate) use value::*;