diff --git a/Cargo.lock b/Cargo.lock index d25646626..ef07965fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,9 +199,8 @@ dependencies = [ [[package]] name = "atspi" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5e1f70f28c52ffd68d18b2c3653f850f90b7e5acc753aff03a7095d5efd58d" +version = "0.5.0" +source = "git+https://github.com/DataTriny/atspi?branch=component_layer#e3a01bdc990924f39a9fcd42d7e909b25c36b11c" dependencies = [ "async-recursion", "async-trait", diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 95c9a69a6..197ed116f 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -3,6 +3,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. +use accesskit::kurbo::Point; use accesskit::{ Action, ActionData, ActionHandler, ActionRequest, Live, Node as NodeData, NodeId, TextSelection, Tree as TreeData, TreeUpdate, @@ -386,6 +387,14 @@ impl Tree { }) } + pub fn scroll_to_point(&self, target: NodeId, point: Point) { + self.action_handler.do_action(ActionRequest { + action: Action::ScrollToPoint, + target, + data: Some(ActionData::ScrollToPoint(point)), + }) + } + pub fn select_text_range(&self, range: &TextRange) { let selection = TextSelection { anchor: range.start.downgrade(), diff --git a/platforms/unix/Cargo.toml b/platforms/unix/Cargo.toml index 3fc4f05fa..4804ac281 100644 --- a/platforms/unix/Cargo.toml +++ b/platforms/unix/Cargo.toml @@ -13,7 +13,7 @@ edition = "2021" [dependencies] accesskit = { version = "0.8.1", path = "../../common" } accesskit_consumer = { version = "0.11.0", path = "../../consumer" } -atspi = "0.3.10" +atspi = { git = "https://github.com/DataTriny/atspi", branch = "component_layer" } parking_lot = "0.12.1" serde = "1.0" zbus = "3.6" diff --git a/platforms/unix/src/adapter.rs b/platforms/unix/src/adapter.rs index 831f6b19c..74bf4353c 100644 --- a/platforms/unix/src/adapter.rs +++ b/platforms/unix/src/adapter.rs @@ -163,7 +163,11 @@ impl Adapter { self.adapter .register_interfaces(self.tree, new_node.id(), new_interfaces ^ kept_interfaces) .unwrap(); - new_wrapper.enqueue_changes(&mut self.queue, &old_wrapper); + new_wrapper.enqueue_changes( + &self.adapter.root_window_bounds.read(), + &mut self.queue, + &old_wrapper, + ); } fn focus_moved(&mut self, old_node: Option<&DetachedNode>, new_node: Option<&Node>) { if let Some(root_window) = root_window(&self.tree.read()) { diff --git a/platforms/unix/src/atspi/bus.rs b/platforms/unix/src/atspi/bus.rs index cceb3dd25..69540f08c 100644 --- a/platforms/unix/src/atspi/bus.rs +++ b/platforms/unix/src/atspi/bus.rs @@ -75,20 +75,21 @@ impl Bus { pub fn emit_object_event(&self, target: &ObjectId, event: &ObjectEvent) -> Result<()> { let interface = "org.a11y.atspi.Event.Object"; let signal = match event { - ObjectEvent::StateChanged(_, _) => "StateChanged", + ObjectEvent::BoundsChanged(_) => "BoundsChanged", ObjectEvent::PropertyChanged(_) => "PropertyChange", + ObjectEvent::StateChanged(_, _) => "StateChanged", }; let properties = HashMap::new(); match event { - ObjectEvent::StateChanged(state, value) => self.emit_event( + ObjectEvent::BoundsChanged(bounds) => self.emit_event( target, interface, signal, EventBody { - kind: state, - detail1: *value as i32, + kind: "", + detail1: 0, detail2: 0, - any_data: 0i32.into(), + any_data: Value::from(*bounds), properties, }, ), @@ -128,6 +129,18 @@ impl Bus { properties, }, ), + ObjectEvent::StateChanged(state, value) => self.emit_event( + target, + interface, + signal, + EventBody { + kind: state, + detail1: *value as i32, + detail2: 0, + any_data: 0i32.into(), + properties, + }, + ), } } diff --git a/platforms/unix/src/atspi/interfaces/component.rs b/platforms/unix/src/atspi/interfaces/component.rs index 50762b8a9..42b638c34 100644 --- a/platforms/unix/src/atspi/interfaces/component.rs +++ b/platforms/unix/src/atspi/interfaces/component.rs @@ -9,7 +9,7 @@ use crate::{ util::WindowBounds, PlatformNode, }; -use atspi::CoordType; +use atspi::{component::Layer, CoordType}; use parking_lot::RwLock; use std::sync::{Arc, Weak}; use zbus::{fdo, MessageHeader}; @@ -64,7 +64,19 @@ impl ComponentInterface { extents } + fn get_layer(&self) -> fdo::Result { + self.node.get_layer() + } + fn grab_focus(&self) -> fdo::Result { self.node.grab_focus() } + + fn scroll_to_point(&self, coord_type: CoordType, x: i32, y: i32) -> fdo::Result { + let window_bounds = self.upgrade_bounds()?; + let scrolled = self + .node + .scroll_to_point(&window_bounds.read(), coord_type, x, y); + scrolled + } } diff --git a/platforms/unix/src/atspi/interfaces/events.rs b/platforms/unix/src/atspi/interfaces/events.rs index fd3017f5b..25054d145 100644 --- a/platforms/unix/src/atspi/interfaces/events.rs +++ b/platforms/unix/src/atspi/interfaces/events.rs @@ -3,7 +3,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use crate::atspi::{ObjectId, ObjectRef}; +use crate::atspi::{ObjectId, ObjectRef, Rect}; use atspi::{accessible::Role, State}; pub(crate) enum QueuedEvent { @@ -26,9 +26,11 @@ pub(crate) enum Property { Value(f64), } +#[allow(clippy::enum_variant_names)] pub(crate) enum ObjectEvent { - StateChanged(State, bool), + BoundsChanged(Rect), PropertyChanged(Property), + StateChanged(State, bool), } pub(crate) enum WindowEvent { diff --git a/platforms/unix/src/atspi/mod.rs b/platforms/unix/src/atspi/mod.rs index 4291f4a91..fc85461d7 100644 --- a/platforms/unix/src/atspi/mod.rs +++ b/platforms/unix/src/atspi/mod.rs @@ -4,7 +4,7 @@ // the LICENSE-MIT file), at your option. use serde::{Deserialize, Serialize}; -use zbus::zvariant::Type; +use zbus::zvariant::{OwnedValue, Type, Value}; mod bus; pub(crate) mod interfaces; @@ -12,7 +12,7 @@ mod object_address; mod object_id; mod object_ref; -#[derive(Debug, Default, Serialize, Deserialize, Type)] +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, OwnedValue, Type, Value)] pub(crate) struct Rect { x: i32, y: i32, @@ -20,6 +20,15 @@ pub(crate) struct Rect { height: i32, } +impl Rect { + pub const INVALID: Rect = Rect { + x: -1, + y: -1, + width: -1, + height: -1, + }; +} + impl From for Rect { fn from(value: accesskit::kurbo::Rect) -> Rect { Rect { diff --git a/platforms/unix/src/node.rs b/platforms/unix/src/node.rs index 38846a90f..0ebf1b981 100644 --- a/platforms/unix/src/node.rs +++ b/platforms/unix/src/node.rs @@ -15,9 +15,15 @@ use crate::{ }, util::{AppContext, WindowBounds}, }; -use accesskit::{kurbo::Point, CheckedState, DefaultActionVerb, NodeId, Role}; +use accesskit::{ + kurbo::{Affine, Point, Rect}, + CheckedState, DefaultActionVerb, NodeId, Role, +}; use accesskit_consumer::{DetachedNode, FilterResult, Node, NodeState, Tree, TreeState}; -use atspi::{accessible::Role as AtspiRole, CoordType, Interface, InterfaceSet, State, StateSet}; +use atspi::{ + accessible::Role as AtspiRole, component::Layer, CoordType, Interface, InterfaceSet, State, + StateSet, +}; use parking_lot::RwLock; use std::{ iter::FusedIterator, @@ -432,13 +438,45 @@ impl<'a> NodeWrapper<'a> { }) } + fn raw_bounds_and_transform(&self) -> (Option, Affine) { + let state = self.node_state(); + (state.raw_bounds(), state.direct_transform()) + } + + fn extents(&self, window_bounds: &WindowBounds) -> AtspiRect { + if self.is_root() { + return window_bounds.outer.into(); + } + match self { + Self::Node(node) => node.bounding_box().map_or_else( + || AtspiRect::INVALID, + |bounds| { + let window_top_left = window_bounds.inner.origin(); + let node_origin = bounds.origin(); + let new_origin = Point::new( + window_top_left.x + node_origin.x, + window_top_left.y + node_origin.y, + ); + bounds.with_origin(new_origin).into() + }, + ), + _ => unreachable!(), + } + } + fn current_value(&self) -> Option { self.node_state().numeric_value() } - pub fn enqueue_changes(&self, queue: &mut Vec, old: &NodeWrapper) { + pub fn enqueue_changes( + &self, + window_bounds: &WindowBounds, + queue: &mut Vec, + old: &NodeWrapper, + ) { self.enqueue_state_changes(queue, old); self.enqueue_property_changes(queue, old); + self.enqueue_bounds_changes(window_bounds, queue, old); } fn enqueue_state_changes(&self, queue: &mut Vec, old: &NodeWrapper) { @@ -491,6 +529,20 @@ impl<'a> NodeWrapper<'a> { } } } + + fn enqueue_bounds_changes( + &self, + window_bounds: &WindowBounds, + queue: &mut Vec, + old: &NodeWrapper, + ) { + if self.raw_bounds_and_transform() != old.raw_bounds_and_transform() { + queue.push(QueuedEvent::Object { + target: self.id(), + event: ObjectEvent::BoundsChanged(self.extents(window_bounds)), + }); + } + } } pub(crate) fn unknown_object(id: &ObjectId) -> fdo::Error { @@ -740,12 +792,38 @@ impl PlatformNode { }) } + pub fn get_layer(&self) -> fdo::Result { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + if wrapper.role() == AtspiRole::Window { + Ok(Layer::Window) + } else { + Ok(Layer::Widget) + } + }) + } + pub fn grab_focus(&self) -> fdo::Result { let tree = self.validate_for_action()?; tree.set_focus(self.node_id); Ok(true) } + pub fn scroll_to_point( + &self, + window_bounds: &WindowBounds, + coord_type: CoordType, + x: i32, + y: i32, + ) -> fdo::Result { + let tree = self.validate_for_action()?; + let is_root = self.node_id == tree.read().root_id(); + let top_left = window_bounds.top_left(coord_type, is_root); + let point = Point::new(f64::from(x) - top_left.x, f64::from(y) - top_left.y); + tree.scroll_to_point(self.node_id, point); + Ok(true) + } + pub fn minimum_value(&self) -> fdo::Result { self.resolve(|node| Ok(node.state().min_numeric_value().unwrap_or(std::f64::MIN))) }