From 1a7860c18b094368f57cdebf56eac378dfd231e7 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 30 Nov 2025 13:30:57 +0100 Subject: [PATCH 1/9] Add uuid dependency --- Cargo.lock | 11 +++++++++++ common/Cargo.toml | 5 +++-- common/src/lib.rs | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24ce4213..b1900eb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "uuid", ] [[package]] @@ -1677,6 +1678,7 @@ dependencies = [ "schemars_derive", "serde", "serde_json", + "uuid", ] [[package]] @@ -2101,6 +2103,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "serde", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/common/Cargo.toml b/common/Cargo.toml index d61bb7b6..a2f595ba 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -20,9 +20,10 @@ pyo3 = { version = "0.26", optional = true } schemars = { version = "1", optional = true } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true } serde_json = { version = "1.0", default-features = false, optional = true } +uuid = { version = "1", default-features = false } [features] enumn = ["dep:enumn"] pyo3 = ["dep:pyo3"] -serde = ["dep:serde", "enumn"] -schemars = ["dep:schemars", "dep:serde_json", "serde"] +serde = ["dep:serde", "enumn", "uuid/serde"] +schemars = ["dep:schemars", "dep:serde_json", "serde", "schemars/uuid1"] diff --git a/common/src/lib.rs b/common/src/lib.rs index 1ba8b2c2..1571e79b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -29,6 +29,8 @@ use serde::{ #[cfg(feature = "schemars")] use serde_json::{Map as SchemaMap, Value as SchemaValue}; +pub use uuid::Uuid; + mod geometry; pub use geometry::{Affine, Point, Rect, Size, Vec2}; From 7afd225fc56cb14303e87a629a2b7e1c8a89ebe1 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 30 Nov 2025 13:36:55 +0100 Subject: [PATCH 2/9] Add TreeId type --- common/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/src/lib.rs b/common/src/lib.rs index 1571e79b..b1378c55 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -654,6 +654,21 @@ impl fmt::Debug for NodeId { } } +/// The stable identity of a [`Tree`]. +/// +/// Use [`TreeId::ROOT`] for the main/root tree. For subtrees, use a random +/// UUID (version 4) to avoid collisions between independently created trees. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +#[repr(transparent)] +pub struct TreeId(pub Uuid); + +impl TreeId { + /// A reserved tree ID for the root tree. This uses a nil UUID. + pub const ROOT: Self = Self(Uuid::nil()); +} + /// Defines a custom action for a UI element. /// /// For example, a list UI can allow a user to reorder items in the list by dragging the From 0c20c0b0b7b8aa797135c3bc1f61a404151d0826 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 30 Nov 2025 14:57:13 +0100 Subject: [PATCH 3/9] Add tree_id field to TreeUpdate --- common/src/lib.rs | 3 ++ consumer/src/filters.rs | 10 ++++++- consumer/src/lib.rs | 3 +- consumer/src/node.rs | 7 ++++- consumer/src/text.rs | 11 +++++-- consumer/src/tree.rs | 35 ++++++++++++++++++++-- platforms/android/src/adapter.rs | 4 ++- platforms/macos/src/adapter.rs | 3 +- platforms/windows/examples/hello_world.rs | 5 +++- platforms/windows/src/adapter.rs | 3 +- platforms/windows/src/tests/simple.rs | 4 ++- platforms/windows/src/tests/subclassed.rs | 4 ++- platforms/winit/examples/mixed_handlers.rs | 6 +++- platforms/winit/examples/simple.rs | 5 +++- 14 files changed, 88 insertions(+), 15 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index b1378c55..565a69de 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -2651,6 +2651,9 @@ pub struct TreeUpdate { /// a tree. pub tree: Option, + /// The identifier of the tree. + pub tree_id: TreeId, + /// The node within this tree that has keyboard focus when the native /// host (e.g. window) has focus. If no specific node within the tree /// has keyboard focus, this must be set to the root. The latest focus state diff --git a/consumer/src/filters.rs b/consumer/src/filters.rs index df9025a4..488f2f39 100644 --- a/consumer/src/filters.rs +++ b/consumer/src/filters.rs @@ -95,7 +95,7 @@ pub fn common_filter_with_root_exception(node: &Node) -> FilterResult { #[cfg(test)] mod tests { - use accesskit::{Node, NodeId, Rect, Role, Tree, TreeUpdate}; + use accesskit::{Node, NodeId, Rect, Role, Tree, TreeId, TreeUpdate}; use alloc::vec; use super::{ @@ -123,6 +123,7 @@ mod tests { (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -145,6 +146,7 @@ mod tests { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -167,6 +169,7 @@ mod tests { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(1), }; let tree = crate::Tree::new(update, true); @@ -185,6 +188,7 @@ mod tests { (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -209,6 +213,7 @@ mod tests { (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -229,6 +234,7 @@ mod tests { (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(1), }; let tree = crate::Tree::new(update, true); @@ -248,6 +254,7 @@ mod tests { (NodeId(1), Node::new(Role::TextRun)), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -333,6 +340,7 @@ mod tests { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; crate::Tree::new(update, false) diff --git a/consumer/src/lib.rs b/consumer/src/lib.rs index dcb54fba..8642c179 100644 --- a/consumer/src/lib.rs +++ b/consumer/src/lib.rs @@ -26,7 +26,7 @@ pub use text::{ #[cfg(test)] mod tests { - use accesskit::{Affine, Node, NodeId, Rect, Role, Tree, TreeUpdate, Vec2}; + use accesskit::{Affine, Node, NodeId, Rect, Role, Tree, TreeId, TreeUpdate, Vec2}; use alloc::vec; use crate::FilterResult; @@ -179,6 +179,7 @@ mod tests { (EMPTY_CONTAINER_3_3_IGNORED_ID, empty_container_3_3_ignored), ], tree: Some(Tree::new(ROOT_ID)), + tree_id: TreeId::ROOT, focus: ROOT_ID, }; crate::tree::Tree::new(initial_update, false) diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 2f9df237..9091501e 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -944,7 +944,7 @@ impl fmt::Write for SpacePrefixingWriter { mod tests { use accesskit::{ Action, Node, NodeId, Point, Rect, Role, TextDirection, TextPosition, TextSelection, Tree, - TreeUpdate, + TreeId, TreeUpdate, }; use alloc::vec; @@ -1219,6 +1219,7 @@ mod tests { (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -1261,6 +1262,7 @@ mod tests { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -1411,6 +1413,7 @@ mod tests { }), ], tree: Some(Tree::new(ROOT_ID)), + tree_id: TreeId::ROOT, focus: ROOT_ID, }; let tree = crate::Tree::new(update, false); @@ -1506,6 +1509,7 @@ mod tests { }), ], tree: Some(Tree::new(ROOT_ID)), + tree_id: TreeId::ROOT, focus: TEXT_INPUT_ID, }; let tree = crate::Tree::new(update, false); @@ -1574,6 +1578,7 @@ mod tests { }), ], tree: Some(Tree::new(ROOT_ID)), + tree_id: TreeId::ROOT, focus: TEXT_INPUT_ID, }; let tree = crate::Tree::new(update, false); diff --git a/consumer/src/text.rs b/consumer/src/text.rs index e1c6f70e..8e5fc1c6 100644 --- a/consumer/src/text.rs +++ b/consumer/src/text.rs @@ -978,7 +978,7 @@ macro_rules! inherited_properties { } $(#[cfg(test)] mod $getter { - use accesskit::{Node, NodeId, Role, Tree, TreeUpdate}; + use accesskit::{Node, NodeId, Role, Tree, TreeId, TreeUpdate}; use alloc::vec; use super::RangePropertyValue; #[test] @@ -999,6 +999,7 @@ macro_rules! inherited_properties { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -1027,6 +1028,7 @@ macro_rules! inherited_properties { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -1056,6 +1058,7 @@ macro_rules! inherited_properties { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -1084,6 +1087,7 @@ macro_rules! inherited_properties { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -1118,6 +1122,7 @@ macro_rules! inherited_properties { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -1151,6 +1156,7 @@ macro_rules! inherited_properties { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = crate::Tree::new(update, false); @@ -1475,7 +1481,7 @@ mod tests { // This was originally based on an actual tree produced by egui but // has since been heavily modified by hand to cover various test cases. fn main_multiline_tree(selection: Option) -> crate::Tree { - use accesskit::{Action, Affine, Node, Role, TextDirection, Tree, TreeUpdate}; + use accesskit::{Action, Affine, Node, Role, TextDirection, Tree, TreeId, TreeUpdate}; let update = TreeUpdate { nodes: vec![ @@ -1686,6 +1692,7 @@ mod tests { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(1), }; diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index d781e603..0e9c1c5f 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -3,7 +3,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::{Node as NodeData, NodeId, Tree as TreeData, TreeUpdate}; +use accesskit::{Node as NodeData, NodeId, Tree as TreeData, TreeId, TreeUpdate}; use alloc::vec; use core::fmt; use hashbrown::{HashMap, HashSet}; @@ -176,6 +176,7 @@ impl State { let update = TreeUpdate { nodes: vec![], tree: None, + tree_id: TreeId::ROOT, focus: self.focus, }; self.update(update, is_host_focused, changes); @@ -259,6 +260,9 @@ impl Tree { let Some(tree) = initial_state.tree.take() else { panic!("Tried to initialize the accessibility tree without a root tree. TreeUpdate::tree must be Some."); }; + if initial_state.tree_id != TreeId::ROOT { + panic!("Cannot initialize with a subtree. TreeUpdate::tree_id must be TreeId::ROOT."); + } let mut state = State { nodes: HashMap::new(), data: tree, @@ -383,7 +387,7 @@ impl fmt::Display for ShortNodeList<'_, T> { #[cfg(test)] mod tests { - use accesskit::{Node, NodeId, Role, Tree, TreeUpdate}; + use accesskit::{Node, NodeId, Role, Tree, TreeId, TreeUpdate, Uuid}; use alloc::{vec, vec::Vec}; #[test] @@ -391,6 +395,7 @@ mod tests { let update = TreeUpdate { nodes: vec![(NodeId(0), Node::new(Role::Window))], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = super::Tree::new(update, false); @@ -399,6 +404,20 @@ mod tests { assert!(tree.state().root().parent().is_none()); } + #[test] + #[should_panic( + expected = "Cannot initialize with a subtree. TreeUpdate::tree_id must be TreeId::ROOT." + )] + fn init_tree_with_non_root_tree_id_panics() { + let update = TreeUpdate { + nodes: vec![(NodeId(0), Node::new(Role::Window))], + tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId(Uuid::from_u128(1)), + focus: NodeId(0), + }; + let _ = super::Tree::new(update, false); + } + #[test] fn root_node_has_children() { let update = TreeUpdate { @@ -412,6 +431,7 @@ mod tests { (NodeId(2), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let tree = super::Tree::new(update, false); @@ -433,6 +453,7 @@ mod tests { let first_update = TreeUpdate { nodes: vec![(NodeId(0), root_node.clone())], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let mut tree = super::Tree::new(first_update, false); @@ -447,6 +468,7 @@ mod tests { (NodeId(1), Node::new(Role::RootWebArea)), ], tree: None, + tree_id: TreeId::ROOT, focus: NodeId(0), }; struct Handler { @@ -514,6 +536,7 @@ mod tests { (NodeId(1), Node::new(Role::RootWebArea)), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let mut tree = super::Tree::new(first_update, false); @@ -521,6 +544,7 @@ mod tests { let second_update = TreeUpdate { nodes: vec![(NodeId(0), root_node)], tree: None, + tree_id: TreeId::ROOT, focus: NodeId(0), }; struct Handler { @@ -583,6 +607,7 @@ mod tests { (NodeId(2), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(1), }; let mut tree = super::Tree::new(first_update, true); @@ -590,6 +615,7 @@ mod tests { let second_update = TreeUpdate { nodes: vec![], tree: None, + tree_id: TreeId::ROOT, focus: NodeId(2), }; struct Handler { @@ -670,6 +696,7 @@ mod tests { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let mut tree = super::Tree::new(first_update, false); @@ -684,6 +711,7 @@ mod tests { node })], tree: None, + tree_id: TreeId::ROOT, focus: NodeId(0), }; struct Handler { @@ -748,6 +776,7 @@ mod tests { }), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let mut tree = super::Tree::new(update.clone(), false); @@ -837,6 +866,7 @@ mod tests { (NodeId(2), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, focus: NodeId(0), }; let mut tree = crate::Tree::new(update, false); @@ -850,6 +880,7 @@ mod tests { TreeUpdate { nodes: vec![(NodeId(0), root)], tree: None, + tree_id: TreeId::ROOT, focus: NodeId(0), }, &mut handler, diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index 3b7d52cb..36442da5 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -10,7 +10,7 @@ use accesskit::{ Action, ActionData, ActionHandler, ActionRequest, ActivationHandler, Node as NodeData, NodeId, - Orientation, Point, Role, ScrollUnit, TextSelection, Tree as TreeData, TreeUpdate, + Orientation, Point, Role, ScrollUnit, TextSelection, Tree as TreeData, TreeId, TreeUpdate, }; use accesskit_consumer::{FilterResult, Node, TextPosition, Tree, TreeChangeHandler}; use jni::{ @@ -179,6 +179,7 @@ impl State { let placeholder_update = TreeUpdate { nodes: vec![(PLACEHOLDER_ROOT_ID, NodeData::new(Role::Window))], tree: Some(TreeData::new(PLACEHOLDER_ROOT_ID)), + tree_id: TreeId::ROOT, focus: PLACEHOLDER_ROOT_ID, }; Self::Placeholder(Tree::new(placeholder_update, true)) @@ -508,6 +509,7 @@ impl Adapter { let update = TreeUpdate { nodes: vec![(node.id(), new_node)], tree: None, + tree_id: TreeId::ROOT, focus: tree_state.focus_id_in_tree(), }; update_tree(events, &mut self.node_id_map, tree, update); diff --git a/platforms/macos/src/adapter.rs b/platforms/macos/src/adapter.rs index 2de5dbaf..fbeca27c 100644 --- a/platforms/macos/src/adapter.rs +++ b/platforms/macos/src/adapter.rs @@ -12,7 +12,7 @@ use crate::{ }; use accesskit::{ ActionHandler, ActionRequest, ActivationHandler, Node as NodeProvider, NodeId, Role, - Tree as TreeData, TreeUpdate, + Tree as TreeData, TreeId, TreeUpdate, }; use accesskit_consumer::{FilterResult, Tree}; use objc2::rc::{Id, WeakId}; @@ -196,6 +196,7 @@ impl Adapter { let placeholder_update = TreeUpdate { nodes: vec![(PLACEHOLDER_ROOT_ID, NodeProvider::new(Role::Window))], tree: Some(TreeData::new(PLACEHOLDER_ROOT_ID)), + tree_id: TreeId::ROOT, focus: PLACEHOLDER_ROOT_ID, }; let placeholder_tree = Tree::new(placeholder_update, false); diff --git a/platforms/windows/examples/hello_world.rs b/platforms/windows/examples/hello_world.rs index 7d06dc20..296a5e66 100644 --- a/platforms/windows/examples/hello_world.rs +++ b/platforms/windows/examples/hello_world.rs @@ -2,7 +2,7 @@ use accesskit::{ Action, ActionHandler, ActionRequest, ActivationHandler, Live, Node, NodeId, Rect, Role, Tree, - TreeUpdate, + TreeId, TreeUpdate, }; use accesskit_windows::Adapter; use once_cell::sync::Lazy; @@ -115,6 +115,7 @@ impl ActivationHandler for InnerWindowState { (BUTTON_2_ID, button_2), ], tree: Some(tree), + tree_id: TreeId::ROOT, focus: self.focus, }; if let Some(announcement) = &self.announcement { @@ -138,6 +139,7 @@ impl WindowState { if let Some(events) = adapter.update_if_active(|| TreeUpdate { nodes: vec![], tree: None, + tree_id: TreeId::ROOT, focus, }) { drop(adapter); @@ -160,6 +162,7 @@ impl WindowState { TreeUpdate { nodes: vec![(ANNOUNCEMENT_ID, announcement), (WINDOW_ID, root)], tree: None, + tree_id: TreeId::ROOT, focus: inner_state.focus, } }) { diff --git a/platforms/windows/src/adapter.rs b/platforms/windows/src/adapter.rs index 0c889f34..f51ac123 100644 --- a/platforms/windows/src/adapter.rs +++ b/platforms/windows/src/adapter.rs @@ -5,7 +5,7 @@ use accesskit::{ ActionHandler, ActivationHandler, Live, Node as NodeProvider, NodeId, Role, Tree as TreeData, - TreeUpdate, + TreeId, TreeUpdate, }; use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler}; use hashbrown::{HashMap, HashSet}; @@ -518,6 +518,7 @@ impl Adapter { let placeholder_update = TreeUpdate { nodes: vec![(PLACEHOLDER_ROOT_ID, NodeProvider::new(Role::Window))], tree: Some(TreeData::new(PLACEHOLDER_ROOT_ID)), + tree_id: TreeId::ROOT, focus: PLACEHOLDER_ROOT_ID, }; let placeholder_tree = Tree::new(placeholder_update, *is_window_focused); diff --git a/platforms/windows/src/tests/simple.rs b/platforms/windows/src/tests/simple.rs index 382545cb..6c33cbf3 100644 --- a/platforms/windows/src/tests/simple.rs +++ b/platforms/windows/src/tests/simple.rs @@ -4,7 +4,8 @@ // the LICENSE-MIT file), at your option. use accesskit::{ - Action, ActionHandler, ActionRequest, ActivationHandler, Node, NodeId, Role, Tree, TreeUpdate, + Action, ActionHandler, ActionRequest, ActivationHandler, Node, NodeId, Role, Tree, TreeId, + TreeUpdate, }; use windows::{core::*, Win32::UI::Accessibility::*}; @@ -35,6 +36,7 @@ fn get_initial_state() -> TreeUpdate { (BUTTON_2_ID, button_2), ], tree: Some(Tree::new(WINDOW_ID)), + tree_id: TreeId::ROOT, focus: BUTTON_1_ID, } } diff --git a/platforms/windows/src/tests/subclassed.rs b/platforms/windows/src/tests/subclassed.rs index b9eeb4c7..d81236c8 100644 --- a/platforms/windows/src/tests/subclassed.rs +++ b/platforms/windows/src/tests/subclassed.rs @@ -4,7 +4,8 @@ // the LICENSE-MIT file), at your option. use accesskit::{ - Action, ActionHandler, ActionRequest, ActivationHandler, Node, NodeId, Role, Tree, TreeUpdate, + Action, ActionHandler, ActionRequest, ActivationHandler, Node, NodeId, Role, Tree, TreeId, + TreeUpdate, }; use once_cell::sync::Lazy; use windows::{ @@ -52,6 +53,7 @@ fn get_initial_state() -> TreeUpdate { (BUTTON_2_ID, button_2), ], tree: Some(Tree::new(WINDOW_ID)), + tree_id: TreeId::ROOT, focus: BUTTON_1_ID, } } diff --git a/platforms/winit/examples/mixed_handlers.rs b/platforms/winit/examples/mixed_handlers.rs index b3a454b3..de3ebe96 100644 --- a/platforms/winit/examples/mixed_handlers.rs +++ b/platforms/winit/examples/mixed_handlers.rs @@ -2,7 +2,8 @@ mod fill; use accesskit::{ - Action, ActionRequest, ActivationHandler, Live, Node, NodeId, Rect, Role, Tree, TreeUpdate, + Action, ActionRequest, ActivationHandler, Live, Node, NodeId, Rect, Role, Tree, TreeId, + TreeUpdate, }; use accesskit_winit::{Adapter, Event as AccessKitEvent, WindowEvent as AccessKitWindowEvent}; use std::{ @@ -96,6 +97,7 @@ impl UiState { (BUTTON_2_ID, button_2), ], tree: Some(tree), + tree_id: TreeId::ROOT, focus: self.focus, }; if let Some(announcement) = &self.announcement { @@ -111,6 +113,7 @@ impl UiState { adapter.update_if_active(|| TreeUpdate { nodes: vec![], tree: None, + tree_id: TreeId::ROOT, focus, }); } @@ -128,6 +131,7 @@ impl UiState { TreeUpdate { nodes: vec![(ANNOUNCEMENT_ID, announcement), (WINDOW_ID, root)], tree: None, + tree_id: TreeId::ROOT, focus: self.focus, } }); diff --git a/platforms/winit/examples/simple.rs b/platforms/winit/examples/simple.rs index f4ab4a63..2badefc9 100644 --- a/platforms/winit/examples/simple.rs +++ b/platforms/winit/examples/simple.rs @@ -1,7 +1,7 @@ #[path = "util/fill.rs"] mod fill; -use accesskit::{Action, ActionRequest, Live, Node, NodeId, Rect, Role, Tree, TreeUpdate}; +use accesskit::{Action, ActionRequest, Live, Node, NodeId, Rect, Role, Tree, TreeId, TreeUpdate}; use accesskit_winit::{Adapter, Event as AccessKitEvent, WindowEvent as AccessKitWindowEvent}; use std::error::Error; use winit::{ @@ -91,6 +91,7 @@ impl UiState { (BUTTON_2_ID, button_2), ], tree: Some(tree), + tree_id: TreeId::ROOT, focus: self.focus, }; if let Some(announcement) = &self.announcement { @@ -106,6 +107,7 @@ impl UiState { adapter.update_if_active(|| TreeUpdate { nodes: vec![], tree: None, + tree_id: TreeId::ROOT, focus, }); } @@ -123,6 +125,7 @@ impl UiState { TreeUpdate { nodes: vec![(ANNOUNCEMENT_ID, announcement), (WINDOW_ID, root)], tree: None, + tree_id: TreeId::ROOT, focus: self.focus, } }); From 418de439503c0e8c8cfe317d29b96af4f78489f2 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 30 Nov 2025 15:46:53 +0100 Subject: [PATCH 4/9] Add TreeIndex and TreeIndexMap infrastructure --- consumer/src/tree.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 0e9c1c5f..ec26d5dd 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -10,6 +10,32 @@ use hashbrown::{HashMap, HashSet}; use crate::node::{Node, NodeState, ParentAndIndex}; +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub(crate) struct TreeIndex(pub(crate) u32); + +#[derive(Debug, Default)] +struct TreeIndexMap { + id_to_index: HashMap, + index_to_id: HashMap, + next: u32, +} + +impl TreeIndexMap { + fn get_index(&mut self, id: TreeId) -> TreeIndex { + *self.id_to_index.entry(id).or_insert_with(|| { + let tree_index = TreeIndex(self.next); + self.next += 1; + self.index_to_id.insert(tree_index, id); + tree_index + }) + } + + fn get_id(&self, index: TreeIndex) -> Option { + self.index_to_id.get(&index).copied() + } +} + #[derive(Clone, Debug)] pub struct State { pub(crate) nodes: HashMap, @@ -253,6 +279,7 @@ pub trait ChangeHandler { pub struct Tree { state: State, next_state: State, + tree_index_map: TreeIndexMap, } impl Tree { @@ -263,6 +290,8 @@ impl Tree { if initial_state.tree_id != TreeId::ROOT { panic!("Cannot initialize with a subtree. TreeUpdate::tree_id must be TreeId::ROOT."); } + let mut tree_index_map = TreeIndexMap::default(); + tree_index_map.get_index(initial_state.tree_id); let mut state = State { nodes: HashMap::new(), data: tree, @@ -273,6 +302,7 @@ impl Tree { Self { next_state: state.clone(), state, + tree_index_map, } } @@ -390,6 +420,55 @@ mod tests { use accesskit::{Node, NodeId, Role, Tree, TreeId, TreeUpdate, Uuid}; use alloc::{vec, vec::Vec}; + use super::{TreeIndex, TreeIndexMap}; + + #[test] + fn tree_index_map_assigns_sequential_indices() { + let mut map = TreeIndexMap::default(); + let id1 = TreeId::ROOT; + let id2 = TreeId(Uuid::from_u128(1)); + let id3 = TreeId(Uuid::from_u128(2)); + + let index1 = map.get_index(id1); + let index2 = map.get_index(id2); + let index3 = map.get_index(id3); + + assert_eq!(index1, TreeIndex(0)); + assert_eq!(index2, TreeIndex(1)); + assert_eq!(index3, TreeIndex(2)); + } + + #[test] + fn tree_index_map_returns_same_index_for_same_id() { + let mut map = TreeIndexMap::default(); + let id = TreeId::ROOT; + + let index1 = map.get_index(id); + let index2 = map.get_index(id); + + assert_eq!(index1, index2); + } + + #[test] + fn tree_index_map_get_id_returns_correct_id() { + let mut map = TreeIndexMap::default(); + let id1 = TreeId::ROOT; + let id2 = TreeId(Uuid::from_u128(1)); + + let index1 = map.get_index(id1); + let index2 = map.get_index(id2); + + assert_eq!(map.get_id(index1), Some(id1)); + assert_eq!(map.get_id(index2), Some(id2)); + } + + #[test] + fn tree_index_map_get_id_returns_none_for_unknown_index() { + let map = TreeIndexMap::default(); + assert_eq!(map.get_id(TreeIndex(0)), None); + assert_eq!(map.get_id(TreeIndex(999)), None); + } + #[test] fn init_tree_with_root_node() { let update = TreeUpdate { From 7a7588b08f413f3ec7e7c264594272aa1f3770db Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 30 Nov 2025 20:20:17 +0100 Subject: [PATCH 5/9] Add NodeId type combining local ID with tree index --- consumer/src/filters.rs | 5 +- consumer/src/iterators.rs | 163 ++++++++------- consumer/src/lib.rs | 58 +++--- consumer/src/node.rs | 230 ++++++++++++++++----- consumer/src/text.rs | 108 ++++++---- consumer/src/tree.rs | 274 ++++++++++++++----------- platforms/android/src/adapter.rs | 25 ++- platforms/android/src/util.rs | 3 +- platforms/atspi-common/src/adapter.rs | 4 +- platforms/atspi-common/src/callback.rs | 2 +- platforms/atspi-common/src/events.rs | 2 +- platforms/atspi-common/src/lib.rs | 1 + platforms/atspi-common/src/node.rs | 123 ++++++----- platforms/macos/src/adapter.rs | 6 +- platforms/macos/src/context.rs | 4 +- platforms/macos/src/event.rs | 4 +- platforms/macos/src/node.rs | 190 +++++++++-------- platforms/unix/src/adapter.rs | 4 +- platforms/unix/src/atspi/bus.rs | 3 +- platforms/unix/src/atspi/object_id.rs | 11 +- platforms/windows/src/adapter.rs | 8 +- platforms/windows/src/node.rs | 67 +++--- platforms/windows/src/text.rs | 22 +- 23 files changed, 792 insertions(+), 525 deletions(-) diff --git a/consumer/src/filters.rs b/consumer/src/filters.rs index 488f2f39..1401169a 100644 --- a/consumer/src/filters.rs +++ b/consumer/src/filters.rs @@ -102,12 +102,13 @@ mod tests { common_filter, common_filter_with_root_exception, FilterResult::{self, *}, }; + use crate::tests::nid; #[track_caller] fn assert_filter_result(expected: FilterResult, tree: &crate::Tree, id: NodeId) { assert_eq!( expected, - common_filter(&tree.state().node_by_id(id).unwrap()) + common_filter(&tree.state().node_by_id(nid(id)).unwrap()) ); } @@ -195,7 +196,7 @@ mod tests { assert_filter_result(ExcludeNode, &tree, NodeId(0)); assert_eq!( Include, - common_filter_with_root_exception(&tree.state().node_by_id(NodeId(0)).unwrap()) + common_filter_with_root_exception(&tree.state().node_by_id(nid(NodeId(0))).unwrap()) ); assert_filter_result(Include, &tree, NodeId(1)); } diff --git a/consumer/src/iterators.rs b/consumer/src/iterators.rs index 30faaac3..fb382bf6 100644 --- a/consumer/src/iterators.rs +++ b/consumer/src/iterators.rs @@ -10,9 +10,13 @@ use core::iter::FusedIterator; -use accesskit::NodeId; +use accesskit::NodeId as LocalNodeId; -use crate::{filters::FilterResult, node::Node, tree::State as TreeState}; +use crate::{ + filters::FilterResult, + node::{Node, NodeId}, + tree::State as TreeState, +}; /// An iterator that yields following siblings of a node. /// @@ -22,6 +26,7 @@ pub struct FollowingSiblings<'a> { done: bool, front_position: usize, parent: Option>, + node_id: NodeId, } impl<'a> FollowingSiblings<'a> { @@ -44,6 +49,7 @@ impl<'a> FollowingSiblings<'a> { done, front_position, parent: parent_and_index.map(|(parent, _)| parent), + node_id: node.id, } } } @@ -63,7 +69,7 @@ impl Iterator for FollowingSiblings<'_> { .children() .get(self.front_position)?; self.front_position += 1; - Some(*child) + Some(self.node_id.with_same_tree(*child)) } } @@ -89,7 +95,7 @@ impl DoubleEndedIterator for FollowingSiblings<'_> { .children() .get(self.back_position)?; self.back_position -= 1; - Some(*child) + Some(self.node_id.with_same_tree(*child)) } } } @@ -106,6 +112,7 @@ pub struct PrecedingSiblings<'a> { done: bool, front_position: usize, parent: Option>, + node_id: NodeId, } impl<'a> PrecedingSiblings<'a> { @@ -122,6 +129,7 @@ impl<'a> PrecedingSiblings<'a> { done, front_position, parent: parent_and_index.map(|(parent, _)| parent), + node_id: node.id, } } } @@ -143,7 +151,7 @@ impl Iterator for PrecedingSiblings<'_> { if !self.done { self.front_position -= 1; } - Some(*child) + Some(self.node_id.with_same_tree(*child)) } } @@ -169,7 +177,7 @@ impl DoubleEndedIterator for PrecedingSiblings<'_> { .children() .get(self.back_position)?; self.back_position += 1; - Some(*child) + Some(self.node_id.with_same_tree(*child)) } } } @@ -459,8 +467,9 @@ impl FilterResult> FusedIterator for FilteredChildren<'_, F pub(crate) enum LabelledBy<'a, Filter: Fn(&Node) -> FilterResult> { FromDescendants(FilteredChildren<'a, Filter>), Explicit { - ids: core::slice::Iter<'a, NodeId>, + ids: core::slice::Iter<'a, LocalNodeId>, tree_state: &'a TreeState, + node_id: NodeId, }, } @@ -470,9 +479,13 @@ impl<'a, Filter: Fn(&Node) -> FilterResult> Iterator for LabelledBy<'a, Filter> fn next(&mut self) -> Option { match self { Self::FromDescendants(iter) => iter.next(), - Self::Explicit { ids, tree_state } => { - ids.next().map(|id| tree_state.node_by_id(*id).unwrap()) - } + Self::Explicit { + ids, + tree_state, + node_id, + } => ids + .next() + .map(|id| tree_state.node_by_id(node_id.with_same_tree(*id)).unwrap()), } } @@ -488,9 +501,13 @@ impl FilterResult> DoubleEndedIterator for LabelledBy<'_, F fn next_back(&mut self) -> Option { match self { Self::FromDescendants(iter) => iter.next_back(), - Self::Explicit { ids, tree_state } => ids + Self::Explicit { + ids, + tree_state, + node_id, + } => ids .next_back() - .map(|id| tree_state.node_by_id(*id).unwrap()), + .map(|id| tree_state.node_by_id(node_id.with_same_tree(*id)).unwrap()), } } } @@ -500,7 +517,7 @@ impl FilterResult> FusedIterator for LabelledBy<'_, Filter> #[cfg(test)] mod tests { use crate::tests::*; - use accesskit::NodeId; + use accesskit::NodeId as LocalNodeId; use alloc::vec::Vec; #[test] @@ -515,23 +532,23 @@ mod tests { PARAGRAPH_3_IGNORED_ID ], tree.state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() - .following_siblings() - .map(|node| node.id()) - .collect::>()[..] + .following_sibling_ids() + .map(|id| id.to_components().0) + .collect::>()[..] ); assert_eq!( 3, tree.state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .following_siblings() .len() ); assert!(tree .state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() .following_siblings() .next() @@ -539,7 +556,7 @@ mod tests { assert_eq!( 0, tree.state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() .following_siblings() .len() @@ -562,16 +579,16 @@ mod tests { PARAGRAPH_1_IGNORED_ID ], tree.state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() - .following_siblings() + .following_sibling_ids() .rev() - .map(|node| node.id()) - .collect::>()[..] + .map(|id| id.to_components().0) + .collect::>()[..] ); assert!(tree .state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() .following_siblings() .next_back() @@ -586,23 +603,23 @@ mod tests { assert_eq!( [PARAGRAPH_2_ID, PARAGRAPH_1_IGNORED_ID, PARAGRAPH_0_ID], tree.state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() - .preceding_siblings() - .map(|node| node.id()) - .collect::>()[..] + .preceding_sibling_ids() + .map(|id| id.to_components().0) + .collect::>()[..] ); assert_eq!( 3, tree.state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() .preceding_siblings() .len() ); assert!(tree .state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .preceding_siblings() .next() @@ -610,7 +627,7 @@ mod tests { assert_eq!( 0, tree.state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .preceding_siblings() .len() @@ -629,16 +646,16 @@ mod tests { assert_eq!( [PARAGRAPH_0_ID, PARAGRAPH_1_IGNORED_ID, PARAGRAPH_2_ID], tree.state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() - .preceding_siblings() + .preceding_sibling_ids() .rev() - .map(|node| node.id()) - .collect::>()[..] + .map(|id| id.to_components().0) + .collect::>()[..] ); assert!(tree .state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .preceding_siblings() .next_back() @@ -657,24 +674,24 @@ mod tests { assert_eq!( [LABEL_1_1_ID, PARAGRAPH_2_ID, LABEL_3_1_0_ID, BUTTON_3_2_ID], tree.state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .following_filtered_siblings(test_tree_filter) - .map(|node| node.id()) - .collect::>()[..] + .map(|node| node.id().to_components().0) + .collect::>()[..] ); assert_eq!( [BUTTON_3_2_ID], tree.state() - .node_by_id(LABEL_3_1_0_ID) + .node_by_id(nid(LABEL_3_1_0_ID)) .unwrap() .following_filtered_siblings(test_tree_filter) - .map(|node| node.id()) - .collect::>()[..] + .map(|node| node.id().to_components().0) + .collect::>()[..] ); assert!(tree .state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() .following_filtered_siblings(test_tree_filter) .next() @@ -693,26 +710,26 @@ mod tests { assert_eq!( [BUTTON_3_2_ID, LABEL_3_1_0_ID, PARAGRAPH_2_ID, LABEL_1_1_ID], tree.state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .following_filtered_siblings(test_tree_filter) .rev() - .map(|node| node.id()) - .collect::>()[..] + .map(|node| node.id().to_components().0) + .collect::>()[..] ); assert_eq!( [BUTTON_3_2_ID,], tree.state() - .node_by_id(LABEL_3_1_0_ID) + .node_by_id(nid(LABEL_3_1_0_ID)) .unwrap() .following_filtered_siblings(test_tree_filter) .rev() - .map(|node| node.id()) - .collect::>()[..] + .map(|node| node.id().to_components().0) + .collect::>()[..] ); assert!(tree .state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() .following_filtered_siblings(test_tree_filter) .next_back() @@ -731,24 +748,24 @@ mod tests { assert_eq!( [PARAGRAPH_2_ID, LABEL_1_1_ID, PARAGRAPH_0_ID], tree.state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() .preceding_filtered_siblings(test_tree_filter) - .map(|node| node.id()) - .collect::>()[..] + .map(|node| node.id().to_components().0) + .collect::>()[..] ); assert_eq!( [PARAGRAPH_2_ID, LABEL_1_1_ID, PARAGRAPH_0_ID], tree.state() - .node_by_id(LABEL_3_1_0_ID) + .node_by_id(nid(LABEL_3_1_0_ID)) .unwrap() .preceding_filtered_siblings(test_tree_filter) - .map(|node| node.id()) - .collect::>()[..] + .map(|node| node.id().to_components().0) + .collect::>()[..] ); assert!(tree .state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .preceding_filtered_siblings(test_tree_filter) .next() @@ -767,26 +784,26 @@ mod tests { assert_eq!( [PARAGRAPH_0_ID, LABEL_1_1_ID, PARAGRAPH_2_ID], tree.state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() .preceding_filtered_siblings(test_tree_filter) .rev() - .map(|node| node.id()) - .collect::>()[..] + .map(|node| node.id().to_components().0) + .collect::>()[..] ); assert_eq!( [PARAGRAPH_0_ID, LABEL_1_1_ID, PARAGRAPH_2_ID], tree.state() - .node_by_id(LABEL_3_1_0_ID) + .node_by_id(nid(LABEL_3_1_0_ID)) .unwrap() .preceding_filtered_siblings(test_tree_filter) .rev() - .map(|node| node.id()) - .collect::>()[..] + .map(|node| node.id().to_components().0) + .collect::>()[..] ); assert!(tree .state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .preceding_filtered_siblings(test_tree_filter) .next_back() @@ -807,19 +824,19 @@ mod tests { tree.state() .root() .filtered_children(test_tree_filter) - .map(|node| node.id()) - .collect::>()[..] + .map(|node| node.id().to_components().0) + .collect::>()[..] ); assert!(tree .state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .filtered_children(test_tree_filter) .next() .is_none()); assert!(tree .state() - .node_by_id(LABEL_0_0_IGNORED_ID) + .node_by_id(nid(LABEL_0_0_IGNORED_ID)) .unwrap() .filtered_children(test_tree_filter) .next() @@ -841,19 +858,19 @@ mod tests { .root() .filtered_children(test_tree_filter) .rev() - .map(|node| node.id()) - .collect::>()[..] + .map(|node| node.id().to_components().0) + .collect::>()[..] ); assert!(tree .state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .filtered_children(test_tree_filter) .next_back() .is_none()); assert!(tree .state() - .node_by_id(LABEL_0_0_IGNORED_ID) + .node_by_id(nid(LABEL_0_0_IGNORED_ID)) .unwrap() .filtered_children(test_tree_filter) .next_back() diff --git a/consumer/src/lib.rs b/consumer/src/lib.rs index 8642c179..6e277040 100644 --- a/consumer/src/lib.rs +++ b/consumer/src/lib.rs @@ -11,7 +11,7 @@ pub(crate) mod tree; pub use tree::{ChangeHandler as TreeChangeHandler, State as TreeState, Tree}; pub(crate) mod node; -pub use node::Node; +pub use node::{Node, NodeId}; pub(crate) mod filters; pub use filters::{common_filter, common_filter_with_root_exception, FilterResult}; @@ -26,28 +26,36 @@ pub use text::{ #[cfg(test)] mod tests { - use accesskit::{Affine, Node, NodeId, Rect, Role, Tree, TreeId, TreeUpdate, Vec2}; + use accesskit::{ + Affine, Node, NodeId as LocalNodeId, Rect, Role, Tree, TreeId, TreeUpdate, Vec2, + }; use alloc::vec; + use crate::node::NodeId; + use crate::tree::TreeIndex; use crate::FilterResult; - pub const ROOT_ID: NodeId = NodeId(0); - pub const PARAGRAPH_0_ID: NodeId = NodeId(1); - pub const LABEL_0_0_IGNORED_ID: NodeId = NodeId(2); - pub const PARAGRAPH_1_IGNORED_ID: NodeId = NodeId(3); - pub const BUTTON_1_0_HIDDEN_ID: NodeId = NodeId(4); - pub const CONTAINER_1_0_0_HIDDEN_ID: NodeId = NodeId(5); - pub const LABEL_1_1_ID: NodeId = NodeId(6); - pub const BUTTON_1_2_HIDDEN_ID: NodeId = NodeId(7); - pub const CONTAINER_1_2_0_HIDDEN_ID: NodeId = NodeId(8); - pub const PARAGRAPH_2_ID: NodeId = NodeId(9); - pub const LABEL_2_0_ID: NodeId = NodeId(10); - pub const PARAGRAPH_3_IGNORED_ID: NodeId = NodeId(11); - pub const EMPTY_CONTAINER_3_0_IGNORED_ID: NodeId = NodeId(12); - pub const LINK_3_1_IGNORED_ID: NodeId = NodeId(13); - pub const LABEL_3_1_0_ID: NodeId = NodeId(14); - pub const BUTTON_3_2_ID: NodeId = NodeId(15); - pub const EMPTY_CONTAINER_3_3_IGNORED_ID: NodeId = NodeId(16); + pub fn nid(id: LocalNodeId) -> NodeId { + NodeId::new(id, TreeIndex(0)) + } + + pub const ROOT_ID: LocalNodeId = LocalNodeId(0); + pub const PARAGRAPH_0_ID: LocalNodeId = LocalNodeId(1); + pub const LABEL_0_0_IGNORED_ID: LocalNodeId = LocalNodeId(2); + pub const PARAGRAPH_1_IGNORED_ID: LocalNodeId = LocalNodeId(3); + pub const BUTTON_1_0_HIDDEN_ID: LocalNodeId = LocalNodeId(4); + pub const CONTAINER_1_0_0_HIDDEN_ID: LocalNodeId = LocalNodeId(5); + pub const LABEL_1_1_ID: LocalNodeId = LocalNodeId(6); + pub const BUTTON_1_2_HIDDEN_ID: LocalNodeId = LocalNodeId(7); + pub const CONTAINER_1_2_0_HIDDEN_ID: LocalNodeId = LocalNodeId(8); + pub const PARAGRAPH_2_ID: LocalNodeId = LocalNodeId(9); + pub const LABEL_2_0_ID: LocalNodeId = LocalNodeId(10); + pub const PARAGRAPH_3_IGNORED_ID: LocalNodeId = LocalNodeId(11); + pub const EMPTY_CONTAINER_3_0_IGNORED_ID: LocalNodeId = LocalNodeId(12); + pub const LINK_3_1_IGNORED_ID: LocalNodeId = LocalNodeId(13); + pub const LABEL_3_1_0_ID: LocalNodeId = LocalNodeId(14); + pub const BUTTON_3_2_ID: LocalNodeId = LocalNodeId(15); + pub const EMPTY_CONTAINER_3_3_IGNORED_ID: LocalNodeId = LocalNodeId(16); pub fn test_tree() -> crate::tree::Tree { let root = { @@ -189,12 +197,12 @@ mod tests { let id = node.id(); if node.is_hidden() { FilterResult::ExcludeSubtree - } else if id == LABEL_0_0_IGNORED_ID - || id == PARAGRAPH_1_IGNORED_ID - || id == PARAGRAPH_3_IGNORED_ID - || id == EMPTY_CONTAINER_3_0_IGNORED_ID - || id == LINK_3_1_IGNORED_ID - || id == EMPTY_CONTAINER_3_3_IGNORED_ID + } else if id == nid(LABEL_0_0_IGNORED_ID) + || id == nid(PARAGRAPH_1_IGNORED_ID) + || id == nid(PARAGRAPH_3_IGNORED_ID) + || id == nid(EMPTY_CONTAINER_3_0_IGNORED_ID) + || id == nid(LINK_3_1_IGNORED_ID) + || id == nid(EMPTY_CONTAINER_3_3_IGNORED_ID) { FilterResult::ExcludeNode } else { diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 9091501e..d01a94d2 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -9,8 +9,8 @@ // found in the LICENSE.chromium file. use accesskit::{ - Action, Affine, AriaCurrent, HasPopup, Live, Node as NodeData, NodeId, Orientation, Point, - Rect, Role, SortDirection, TextSelection, Toggled, + Action, Affine, AriaCurrent, HasPopup, Live, Node as NodeData, NodeId as LocalNodeId, + Orientation, Point, Rect, Role, SortDirection, TextSelection, Toggled, }; use alloc::{ string::{String, ToString}, @@ -23,7 +23,32 @@ use crate::iterators::{ FilteredChildren, FollowingFilteredSiblings, FollowingSiblings, LabelledBy, PrecedingFilteredSiblings, PrecedingSiblings, }; -use crate::tree::State as TreeState; +use crate::tree::{State as TreeState, TreeIndex}; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct NodeId(TreeIndex, LocalNodeId); + +impl NodeId { + pub(crate) fn new(local_id: LocalNodeId, tree_index: TreeIndex) -> Self { + Self(tree_index, local_id) + } + + pub(crate) fn with_same_tree(&self, local_id: LocalNodeId) -> Self { + Self(self.0, local_id) + } + + pub(crate) fn to_components(self) -> (LocalNodeId, TreeIndex) { + (self.1, self.0) + } +} + +impl From for u128 { + fn from(id: NodeId) -> Self { + let tree_index = id.0 .0 as u128; + let local_id = id.1 .0 as u128; + (local_id << 64) | tree_index + } +} #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct ParentAndIndex(pub(crate) NodeId, pub(crate) usize); @@ -101,8 +126,11 @@ impl<'a> Node<'a> { + ExactSizeIterator + FusedIterator + '_ { + let id = self.id; let data = &self.state.data; - data.children().iter().copied() + data.children() + .iter() + .map(move |child_id| id.with_same_tree(*child_id)) } pub fn children( @@ -112,10 +140,11 @@ impl<'a> Node<'a> { + FusedIterator> + 'a { let state = self.tree_state; + let id = self.id; let data = &self.state.data; data.children() .iter() - .map(move |id| state.node_by_id(*id).unwrap()) + .map(move |child_id| state.node_by_id(id.with_same_tree(*child_id)).unwrap()) } pub fn filtered_children( @@ -632,6 +661,7 @@ impl<'a> Node<'a> { LabelledBy::Explicit { ids: explicit.iter(), tree_state: self.tree_state, + node_id: self.id, } } } @@ -817,10 +847,11 @@ impl<'a> Node<'a> { &self, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { let state = self.tree_state; + let id = self.id; let data = &self.state.data; data.controls() .iter() - .map(move |id| state.node_by_id(*id).unwrap()) + .map(move |control_id| state.node_by_id(id.with_same_tree(*control_id)).unwrap()) } pub fn raw_text_selection(&self) -> Option<&TextSelection> { @@ -957,26 +988,26 @@ mod tests { assert_eq!( Some((ROOT_ID, 0)), tree.state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .parent_and_index() - .map(|(parent, index)| (parent.id(), index)) + .map(|(parent, index)| (parent.id().to_components().0, index)) ); assert_eq!( Some((PARAGRAPH_0_ID, 0)), tree.state() - .node_by_id(LABEL_0_0_IGNORED_ID) + .node_by_id(nid(LABEL_0_0_IGNORED_ID)) .unwrap() .parent_and_index() - .map(|(parent, index)| (parent.id(), index)) + .map(|(parent, index)| (parent.id().to_components().0, index)) ); assert_eq!( Some((ROOT_ID, 1)), tree.state() - .node_by_id(PARAGRAPH_1_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_1_IGNORED_ID)) .unwrap() .parent_and_index() - .map(|(parent, index)| (parent.id(), index)) + .map(|(parent, index)| (parent.id().to_components().0, index)) ); } @@ -985,20 +1016,28 @@ mod tests { let tree = test_tree(); assert_eq!( LABEL_0_0_IGNORED_ID, - tree.state().root().deepest_first_child().unwrap().id() + tree.state() + .root() + .deepest_first_child() + .unwrap() + .id() + .to_components() + .0 ); assert_eq!( LABEL_0_0_IGNORED_ID, tree.state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .deepest_first_child() .unwrap() .id() + .to_components() + .0 ); assert!(tree .state() - .node_by_id(LABEL_0_0_IGNORED_ID) + .node_by_id(nid(LABEL_0_0_IGNORED_ID)) .unwrap() .deepest_first_child() .is_none()); @@ -1010,11 +1049,13 @@ mod tests { assert_eq!( ROOT_ID, tree.state() - .node_by_id(LABEL_1_1_ID) + .node_by_id(nid(LABEL_1_1_ID)) .unwrap() .filtered_parent(&test_tree_filter) .unwrap() .id() + .to_components() + .0 ); assert!(tree .state() @@ -1033,16 +1074,18 @@ mod tests { .deepest_first_filtered_child(&test_tree_filter) .unwrap() .id() + .to_components() + .0 ); assert!(tree .state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .deepest_first_filtered_child(&test_tree_filter) .is_none()); assert!(tree .state() - .node_by_id(LABEL_0_0_IGNORED_ID) + .node_by_id(nid(LABEL_0_0_IGNORED_ID)) .unwrap() .deepest_first_filtered_child(&test_tree_filter) .is_none()); @@ -1053,20 +1096,28 @@ mod tests { let tree = test_tree(); assert_eq!( EMPTY_CONTAINER_3_3_IGNORED_ID, - tree.state().root().deepest_last_child().unwrap().id() + tree.state() + .root() + .deepest_last_child() + .unwrap() + .id() + .to_components() + .0 ); assert_eq!( EMPTY_CONTAINER_3_3_IGNORED_ID, tree.state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() .deepest_last_child() .unwrap() .id() + .to_components() + .0 ); assert!(tree .state() - .node_by_id(BUTTON_3_2_ID) + .node_by_id(nid(BUTTON_3_2_ID)) .unwrap() .deepest_last_child() .is_none()); @@ -1082,25 +1133,29 @@ mod tests { .deepest_last_filtered_child(&test_tree_filter) .unwrap() .id() + .to_components() + .0 ); assert_eq!( BUTTON_3_2_ID, tree.state() - .node_by_id(PARAGRAPH_3_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_3_IGNORED_ID)) .unwrap() .deepest_last_filtered_child(&test_tree_filter) .unwrap() .id() + .to_components() + .0 ); assert!(tree .state() - .node_by_id(BUTTON_3_2_ID) + .node_by_id(nid(BUTTON_3_2_ID)) .unwrap() .deepest_last_filtered_child(&test_tree_filter) .is_none()); assert!(tree .state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .deepest_last_filtered_child(&test_tree_filter) .is_none()); @@ -1111,36 +1166,40 @@ mod tests { let tree = test_tree(); assert!(tree .state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() .is_descendant_of(&tree.state().root())); assert!(tree .state() - .node_by_id(LABEL_0_0_IGNORED_ID) + .node_by_id(nid(LABEL_0_0_IGNORED_ID)) .unwrap() .is_descendant_of(&tree.state().root())); assert!(tree .state() - .node_by_id(LABEL_0_0_IGNORED_ID) + .node_by_id(nid(LABEL_0_0_IGNORED_ID)) .unwrap() - .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_0_ID).unwrap())); + .is_descendant_of(&tree.state().node_by_id(nid(PARAGRAPH_0_ID)).unwrap())); assert!(!tree .state() - .node_by_id(LABEL_0_0_IGNORED_ID) + .node_by_id(nid(LABEL_0_0_IGNORED_ID)) .unwrap() - .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_2_ID).unwrap())); + .is_descendant_of(&tree.state().node_by_id(nid(PARAGRAPH_2_ID)).unwrap())); assert!(!tree .state() - .node_by_id(PARAGRAPH_0_ID) + .node_by_id(nid(PARAGRAPH_0_ID)) .unwrap() - .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_2_ID).unwrap())); + .is_descendant_of(&tree.state().node_by_id(nid(PARAGRAPH_2_ID)).unwrap())); } #[test] fn is_root() { let tree = test_tree(); - assert!(tree.state().node_by_id(ROOT_ID).unwrap().is_root()); - assert!(!tree.state().node_by_id(PARAGRAPH_0_ID).unwrap().is_root()); + assert!(tree.state().node_by_id(nid(ROOT_ID)).unwrap().is_root()); + assert!(!tree + .state() + .node_by_id(nid(PARAGRAPH_0_ID)) + .unwrap() + .is_root()); } #[test] @@ -1148,7 +1207,7 @@ mod tests { let tree = test_tree(); assert!(tree .state() - .node_by_id(ROOT_ID) + .node_by_id(nid(ROOT_ID)) .unwrap() .bounding_box() .is_none()); @@ -1160,7 +1219,7 @@ mod tests { y1: 80.0, }), tree.state() - .node_by_id(PARAGRAPH_1_IGNORED_ID) + .node_by_id(nid(PARAGRAPH_1_IGNORED_ID)) .unwrap() .bounding_box() ); @@ -1172,7 +1231,7 @@ mod tests { y1: 70.0, }), tree.state() - .node_by_id(LABEL_1_1_ID) + .node_by_id(nid(LABEL_1_1_ID)) .unwrap() .bounding_box() ); @@ -1187,14 +1246,14 @@ mod tests { .node_at_point(Point::new(10.0, 40.0), &test_tree_filter) .is_none()); assert_eq!( - Some(LABEL_1_1_ID), + Some(nid(LABEL_1_1_ID)), tree.state() .root() .node_at_point(Point::new(20.0, 50.0), &test_tree_filter) .map(|node| node.id()) ); assert_eq!( - Some(LABEL_1_1_ID), + Some(nid(LABEL_1_1_ID)), tree.state() .root() .node_at_point(Point::new(50.0, 60.0), &test_tree_filter) @@ -1223,7 +1282,10 @@ mod tests { focus: NodeId(0), }; let tree = crate::Tree::new(update, false); - assert_eq!(None, tree.state().node_by_id(NodeId(1)).unwrap().label()); + assert_eq!( + None, + tree.state().node_by_id(nid(NodeId(1))).unwrap().label() + ); } #[test] @@ -1268,11 +1330,11 @@ mod tests { let tree = crate::Tree::new(update, false); assert_eq!( Some([LABEL_1, LABEL_2].join(" ")), - tree.state().node_by_id(NodeId(1)).unwrap().label() + tree.state().node_by_id(nid(NodeId(1))).unwrap().label() ); assert_eq!( Some(LABEL_2.into()), - tree.state().node_by_id(NodeId(3)).unwrap().label() + tree.state().node_by_id(nid(NodeId(3))).unwrap().label() ); } @@ -1419,38 +1481,50 @@ mod tests { let tree = crate::Tree::new(update, false); assert_eq!( Some(DEFAULT_BUTTON_LABEL.into()), - tree.state().node_by_id(DEFAULT_BUTTON_ID).unwrap().label() + tree.state() + .node_by_id(nid(DEFAULT_BUTTON_ID)) + .unwrap() + .label() ); assert_eq!( Some(LINK_LABEL.into()), - tree.state().node_by_id(LINK_ID).unwrap().label() + tree.state().node_by_id(nid(LINK_ID)).unwrap().label() ); assert_eq!( Some(CHECKBOX_LABEL.into()), - tree.state().node_by_id(CHECKBOX_ID).unwrap().label() + tree.state().node_by_id(nid(CHECKBOX_ID)).unwrap().label() ); assert_eq!( Some(RADIO_BUTTON_LABEL.into()), - tree.state().node_by_id(RADIO_BUTTON_ID).unwrap().label() + tree.state() + .node_by_id(nid(RADIO_BUTTON_ID)) + .unwrap() + .label() ); assert_eq!( Some(MENU_BUTTON_LABEL.into()), - tree.state().node_by_id(MENU_BUTTON_ID).unwrap().label() + tree.state() + .node_by_id(nid(MENU_BUTTON_ID)) + .unwrap() + .label() ); assert_eq!( Some(MENU_ITEM_LABEL.into()), - tree.state().node_by_id(MENU_ITEM_ID).unwrap().label() + tree.state().node_by_id(nid(MENU_ITEM_ID)).unwrap().label() ); assert_eq!( Some(MENU_ITEM_CHECKBOX_LABEL.into()), tree.state() - .node_by_id(MENU_ITEM_CHECKBOX_ID) + .node_by_id(nid(MENU_ITEM_CHECKBOX_ID)) .unwrap() .label() ); assert_eq!( Some(MENU_ITEM_RADIO_LABEL.into()), - tree.state().node_by_id(MENU_ITEM_RADIO_ID).unwrap().label() + tree.state() + .node_by_id(nid(MENU_ITEM_RADIO_ID)) + .unwrap() + .label() ); } @@ -1516,7 +1590,7 @@ mod tests { assert_eq!( Some(PLACEHOLDER), tree.state() - .node_by_id(TEXT_INPUT_ID) + .node_by_id(nid(TEXT_INPUT_ID)) .unwrap() .placeholder() ); @@ -1585,9 +1659,61 @@ mod tests { assert_eq!( None, tree.state() - .node_by_id(TEXT_INPUT_ID) + .node_by_id(nid(TEXT_INPUT_ID)) .unwrap() .placeholder() ); } + + mod node_id { + use super::NodeId as LocalNodeId; + use crate::node::NodeId; + use crate::tree::TreeIndex; + + #[test] + fn new_and_to_components_round_trip() { + let node_id = LocalNodeId(42); + let tree_index = TreeIndex(7); + let id = NodeId::new(node_id, tree_index); + let (extracted_node_id, extracted_tree_index) = id.to_components(); + assert_eq!(node_id, extracted_node_id); + assert_eq!(tree_index, extracted_tree_index); + } + + #[test] + fn with_same_tree_preserves_tree_index() { + let original_node_id = LocalNodeId(100); + let tree_index = TreeIndex(5); + let id = NodeId::new(original_node_id, tree_index); + + let new_node_id = LocalNodeId(200); + let new_id = id.with_same_tree(new_node_id); + + let (extracted_node_id, extracted_tree_index) = new_id.to_components(); + assert_eq!(new_node_id, extracted_node_id); + assert_eq!(tree_index, extracted_tree_index); + } + + #[test] + fn into_u128() { + let node_id = LocalNodeId(12345); + let tree_index = TreeIndex(67); + let id = NodeId::new(node_id, tree_index); + let (extracted_node_id, extracted_tree_index) = id.to_components(); + assert_eq!(node_id, extracted_node_id); + assert_eq!(tree_index, extracted_tree_index); + } + + #[test] + fn equality() { + let id1 = NodeId::new(LocalNodeId(1), TreeIndex(2)); + let id2 = NodeId::new(LocalNodeId(1), TreeIndex(2)); + let id3 = NodeId::new(LocalNodeId(1), TreeIndex(3)); + let id4 = NodeId::new(LocalNodeId(2), TreeIndex(2)); + + assert_eq!(id1, id2); + assert_ne!(id1, id3); + assert_ne!(id1, id4); + } + } } diff --git a/consumer/src/text.rs b/consumer/src/text.rs index 8e5fc1c6..b18e3740 100644 --- a/consumer/src/text.rs +++ b/consumer/src/text.rs @@ -4,13 +4,13 @@ // the LICENSE-MIT file), at your option. use accesskit::{ - Node as NodeData, NodeId, Point, Rect, Role, TextAlign, TextDecoration, TextDirection, + Node as NodeData, Point, Rect, Role, TextAlign, TextDecoration, TextDirection, TextPosition as WeakPosition, TextSelection, VerticalOffset, }; use alloc::{string::String, vec::Vec}; use core::{cmp::Ordering, fmt, iter::FusedIterator}; -use crate::{FilterResult, Node, TreeState}; +use crate::{node::NodeId, FilterResult, Node, TreeState}; #[derive(Clone, Copy, Debug)] pub(crate) struct InnerPosition<'a> { @@ -19,8 +19,8 @@ pub(crate) struct InnerPosition<'a> { } impl<'a> InnerPosition<'a> { - fn upgrade(tree_state: &'a TreeState, weak: WeakPosition) -> Option { - let node = tree_state.node_by_id(weak.node)?; + fn upgrade(tree_state: &'a TreeState, weak: WeakPosition, node_id: NodeId) -> Option { + let node = tree_state.node_by_id(node_id.with_same_tree(weak.node))?; if node.role() != Role::TextRun { return None; } @@ -34,8 +34,12 @@ impl<'a> InnerPosition<'a> { }) } - fn clamped_upgrade(tree_state: &'a TreeState, weak: WeakPosition) -> Option { - let node = tree_state.node_by_id(weak.node)?; + fn clamped_upgrade( + tree_state: &'a TreeState, + weak: WeakPosition, + node_id: NodeId, + ) -> Option { + let node = tree_state.node_by_id(node_id.with_same_tree(weak.node))?; if node.role() != Role::TextRun { return None; } @@ -111,7 +115,10 @@ impl<'a> InnerPosition<'a> { fn line_start(&self) -> Self { let mut node = self.node; while let Some(id) = node.data().previous_on_line() { - node = node.tree_state.node_by_id(id).unwrap(); + node = node + .tree_state + .node_by_id(node.id.with_same_tree(id)) + .unwrap(); } Self { node, @@ -122,7 +129,10 @@ impl<'a> InnerPosition<'a> { fn line_end(&self) -> Self { let mut node = self.node; while let Some(id) = node.data().next_on_line() { - node = node.tree_state.node_by_id(id).unwrap(); + node = node + .tree_state + .node_by_id(node.id.with_same_tree(id)) + .unwrap(); } Self { node, @@ -131,8 +141,9 @@ impl<'a> InnerPosition<'a> { } pub(crate) fn downgrade(&self) -> WeakPosition { + let (local_node_id, _) = self.node.id.to_components(); WeakPosition { - node: self.node.id(), + node: local_node_id, character_index: self.character_index, } } @@ -905,8 +916,8 @@ impl WeakRange { pub fn upgrade<'a>(&self, tree_state: &'a TreeState) -> Option> { let node = self.upgrade_node(tree_state)?; - let start = InnerPosition::upgrade(tree_state, self.start)?; - let end = InnerPosition::upgrade(tree_state, self.end)?; + let start = InnerPosition::upgrade(tree_state, self.start, self.node_id)?; + let end = InnerPosition::upgrade(tree_state, self.end, self.node_id)?; Some(Range { node, start, end }) } } @@ -981,6 +992,7 @@ macro_rules! inherited_properties { use accesskit::{Node, NodeId, Role, Tree, TreeId, TreeUpdate}; use alloc::vec; use super::RangePropertyValue; + use crate::tests::nid; #[test] fn directly_set() { let update = TreeUpdate { @@ -1004,7 +1016,7 @@ macro_rules! inherited_properties { }; let tree = crate::Tree::new(update, false); let state = tree.state(); - let node = state.node_by_id(NodeId(0)).unwrap(); + let node = state.node_by_id(nid(NodeId(0))).unwrap(); let pos = node.document_start(); assert_eq!(pos.$getter(), Some($test_value_1)); let range = node.document_range(); @@ -1033,7 +1045,7 @@ macro_rules! inherited_properties { }; let tree = crate::Tree::new(update, false); let state = tree.state(); - let node = state.node_by_id(NodeId(0)).unwrap(); + let node = state.node_by_id(nid(NodeId(0))).unwrap(); let pos = node.document_start(); assert_eq!(pos.$getter(), Some($test_value_1)); let range = node.document_range(); @@ -1063,7 +1075,7 @@ macro_rules! inherited_properties { }; let tree = crate::Tree::new(update, false); let state = tree.state(); - let node = state.node_by_id(NodeId(0)).unwrap(); + let node = state.node_by_id(nid(NodeId(0))).unwrap(); assert_eq!(node.$getter(), Some($test_value_1)); let pos = node.document_start(); assert_eq!(pos.$getter(), Some($test_value_2)); @@ -1092,7 +1104,7 @@ macro_rules! inherited_properties { }; let tree = crate::Tree::new(update, false); let state = tree.state(); - let node = state.node_by_id(NodeId(0)).unwrap(); + let node = state.node_by_id(nid(NodeId(0))).unwrap(); let pos = node.document_start(); assert_eq!(pos.$getter(), None); let range = node.document_range(); @@ -1127,7 +1139,7 @@ macro_rules! inherited_properties { }; let tree = crate::Tree::new(update, false); let state = tree.state(); - let node = state.node_by_id(NodeId(0)).unwrap(); + let node = state.node_by_id(nid(NodeId(0))).unwrap(); let range = node.document_range(); assert_eq!(range.$getter(), RangePropertyValue::Mixed); } @@ -1161,7 +1173,7 @@ macro_rules! inherited_properties { }; let tree = crate::Tree::new(update, false); let state = tree.state(); - let node = state.node_by_id(NodeId(0)).unwrap(); + let node = state.node_by_id(nid(NodeId(0))).unwrap(); assert_eq!(node.$getter(), Some($test_value_1)); let start = node.document_start(); assert_eq!(start.$getter(), Some($test_value_2)); @@ -1279,16 +1291,21 @@ impl<'a> Node<'a> { } pub fn text_selection(&self) -> Option> { + let id = self.id; self.data().text_selection().map(|selection| { - let anchor = InnerPosition::clamped_upgrade(self.tree_state, selection.anchor).unwrap(); - let focus = InnerPosition::clamped_upgrade(self.tree_state, selection.focus).unwrap(); + let anchor = + InnerPosition::clamped_upgrade(self.tree_state, selection.anchor, id).unwrap(); + let focus = + InnerPosition::clamped_upgrade(self.tree_state, selection.focus, id).unwrap(); Range::new(*self, anchor, focus) }) } pub fn text_selection_anchor(&self) -> Option> { + let id = self.id; self.data().text_selection().map(|selection| { - let anchor = InnerPosition::clamped_upgrade(self.tree_state, selection.anchor).unwrap(); + let anchor = + InnerPosition::clamped_upgrade(self.tree_state, selection.anchor, id).unwrap(); Position { root_node: *self, inner: anchor, @@ -1297,8 +1314,10 @@ impl<'a> Node<'a> { } pub fn text_selection_focus(&self) -> Option> { + let id = self.id; self.data().text_selection().map(|selection| { - let focus = InnerPosition::clamped_upgrade(self.tree_state, selection.focus).unwrap(); + let focus = + InnerPosition::clamped_upgrade(self.tree_state, selection.focus, id).unwrap(); Position { root_node: *self, inner: focus, @@ -1475,6 +1494,7 @@ impl<'a> Node<'a> { #[cfg(test)] mod tests { + use crate::tests::nid; use accesskit::{NodeId, Point, Rect, TextDecoration, TextSelection}; use alloc::vec; @@ -1778,15 +1798,21 @@ mod tests { fn supports_text_ranges() { let tree = main_multiline_tree(None); let state = tree.state(); - assert!(!state.node_by_id(NodeId(0)).unwrap().supports_text_ranges()); - assert!(state.node_by_id(NodeId(1)).unwrap().supports_text_ranges()); + assert!(!state + .node_by_id(nid(NodeId(0))) + .unwrap() + .supports_text_ranges()); + assert!(state + .node_by_id(nid(NodeId(1))) + .unwrap() + .supports_text_ranges()); } #[test] fn multiline_document_range() { let tree = main_multiline_tree(None); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let range = node.document_range(); let start = range.start(); assert!(start.is_word_start()); @@ -1857,7 +1883,7 @@ mod tests { fn multiline_document_range_to_first_format_change() { let tree = main_multiline_tree(None); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let mut range = node.document_range(); range.set_end(range.start().forward_to_format_end()); assert_eq!( @@ -1887,7 +1913,7 @@ mod tests { fn multiline_document_range_from_last_format_change() { let tree = main_multiline_tree(None); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let mut range = node.document_range(); range.set_start(range.end().backward_to_format_start()); assert_eq!( @@ -1929,7 +1955,7 @@ mod tests { fn multiline_end_degenerate_range() { let tree = main_multiline_tree(Some(multiline_end_selection())); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); @@ -1955,7 +1981,7 @@ mod tests { fn multiline_wrapped_line_end_range() { let tree = main_multiline_tree(Some(multiline_wrapped_line_end_selection())); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); @@ -2017,7 +2043,7 @@ mod tests { fn multiline_find_line_ends_from_middle() { let tree = main_multiline_tree(Some(multiline_second_line_middle_selection())); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let mut range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); @@ -2063,7 +2089,7 @@ mod tests { fn multiline_find_wrapped_line_ends_from_middle() { let tree = main_multiline_tree(Some(multiline_first_line_middle_selection())); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let mut range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); @@ -2095,7 +2121,7 @@ mod tests { fn multiline_find_paragraph_ends_from_middle() { let tree = main_multiline_tree(Some(multiline_second_line_middle_selection())); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let mut range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); @@ -2151,7 +2177,7 @@ mod tests { fn multiline_find_format_ends_from_middle() { let tree = main_multiline_tree(Some(multiline_second_line_middle_selection())); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let mut range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); @@ -2179,7 +2205,7 @@ mod tests { fn multiline_find_word_ends_from_middle() { let tree = main_multiline_tree(Some(multiline_second_line_middle_selection())); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let mut range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); @@ -2219,7 +2245,7 @@ mod tests { fn text_position_at_point() { let tree = main_multiline_tree(None); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); { let pos = node.text_position_at_point(Point::new(8.0, 31.666664123535156)); @@ -2311,7 +2337,7 @@ mod tests { fn to_global_usv_index() { let tree = main_multiline_tree(None); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); { let range = node.document_range(); @@ -2336,7 +2362,7 @@ mod tests { fn to_global_utf16_index() { let tree = main_multiline_tree(None); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); { let range = node.document_range(); @@ -2361,7 +2387,7 @@ mod tests { fn to_line_index() { let tree = main_multiline_tree(None); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); { let range = node.document_range(); @@ -2387,7 +2413,7 @@ mod tests { fn line_range_from_index() { let tree = main_multiline_tree(None); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); { let range = node.line_range_from_index(0).unwrap(); @@ -2426,7 +2452,7 @@ mod tests { fn text_position_from_global_usv_index() { let tree = main_multiline_tree(None); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); { let pos = node.text_position_from_global_usv_index(0).unwrap(); @@ -2505,7 +2531,7 @@ mod tests { fn text_position_from_global_utf16_index() { let tree = main_multiline_tree(None); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); { let pos = node.text_position_from_global_utf16_index(0).unwrap(); @@ -2584,7 +2610,7 @@ mod tests { fn multiline_selection_clamping() { let tree = main_multiline_tree(Some(multiline_past_end_selection())); let state = tree.state(); - let node = state.node_by_id(NodeId(1)).unwrap(); + let node = state.node_by_id(nid(NodeId(1))).unwrap(); let _ = node.text_selection().unwrap(); } diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index ec26d5dd..6ceada75 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -3,17 +3,21 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::{Node as NodeData, NodeId, Tree as TreeData, TreeId, TreeUpdate}; +use accesskit::{Node as NodeData, NodeId as LocalNodeId, Tree as TreeData, TreeId, TreeUpdate}; use alloc::vec; use core::fmt; use hashbrown::{HashMap, HashSet}; -use crate::node::{Node, NodeState, ParentAndIndex}; +use crate::node::{Node, NodeId, NodeState, ParentAndIndex}; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub(crate) struct TreeIndex(pub(crate) u32); +fn nid(id: LocalNodeId) -> NodeId { + NodeId::new(id, TreeIndex(0)) +} + #[derive(Debug, Default)] struct TreeIndexMap { id_to_index: HashMap, @@ -53,11 +57,14 @@ struct InternalChanges { impl State { fn validate_global(&self) { - if !self.nodes.contains_key(&self.data.root) { + if !self.nodes.contains_key(&nid(self.data.root)) { panic!("Root ID {:?} is not in the node list", self.data.root); } if !self.nodes.contains_key(&self.focus) { - panic!("Focused ID {:?} is not in the node list", self.focus); + panic!( + "Focused ID {:?} is not in the node list", + self.focus.to_components().0 + ); } } @@ -72,7 +79,7 @@ impl State { if let Some(tree) = update.tree { if tree.root != self.data.root { - unreachable.insert(self.data.root); + unreachable.insert(nid(self.data.root)); } self.data = tree; } @@ -98,7 +105,8 @@ impl State { } } - for (node_id, node_data) in update.nodes { + for (local_node_id, node_data) in update.nodes { + let node_id = nid(local_node_id); unreachable.remove(&node_id); for (child_index, child_id) in node_data.children().iter().enumerate() { @@ -106,35 +114,35 @@ impl State { panic!("TreeUpdate includes duplicate child {:?}", child_id); } seen_child_ids.insert(*child_id); - unreachable.remove(child_id); + unreachable.remove(&nid(*child_id)); let parent_and_index = ParentAndIndex(node_id, child_index); - if let Some(child_state) = self.nodes.get_mut(child_id) { + if let Some(child_state) = self.nodes.get_mut(&nid(*child_id)) { if child_state.parent_and_index != Some(parent_and_index) { child_state.parent_and_index = Some(parent_and_index); if let Some(changes) = &mut changes { - changes.updated_node_ids.insert(*child_id); + changes.updated_node_ids.insert(nid(*child_id)); } } - } else if let Some(child_data) = pending_nodes.remove(child_id) { + } else if let Some(child_data) = pending_nodes.remove(&nid(*child_id)) { add_node( &mut self.nodes, &mut changes, Some(parent_and_index), - *child_id, + nid(*child_id), child_data, ); } else { - pending_children.insert(*child_id, parent_and_index); + pending_children.insert(nid(*child_id), parent_and_index); } } if let Some(node_state) = self.nodes.get_mut(&node_id) { - if node_id == root { + if local_node_id == root { node_state.parent_and_index = None; } for child_id in node_state.data.children().iter() { if !seen_child_ids.contains(child_id) { - unreachable.insert(*child_id); + unreachable.insert(nid(*child_id)); } } if node_state.data != node_data { @@ -151,7 +159,7 @@ impl State { node_id, node_data, ); - } else if node_id == root { + } else if local_node_id == root { add_node(&mut self.nodes, &mut changes, None, node_id, node_data); } else { pending_nodes.insert(node_id, node_data); @@ -159,20 +167,28 @@ impl State { } if !pending_nodes.is_empty() { - panic!("TreeUpdate includes {} nodes which are neither in the current tree nor a child of another node from the update: {}", pending_nodes.len(), ShortNodeList(&pending_nodes)); + panic!( + "TreeUpdate includes {} nodes which are neither in the current tree nor a child of another node from the update: {}", + pending_nodes.len(), + ShortNodeList(&pending_nodes) + ); } if !pending_children.is_empty() { - panic!("TreeUpdate's nodes include {} children ids which are neither in the current tree nor the ID of another node from the update: {}", pending_children.len(), ShortNodeList(&pending_children)); + panic!( + "TreeUpdate's nodes include {} children ids which are neither in the current tree nor the ID of another node from the update: {}", + pending_children.len(), + ShortNodeList(&pending_children) + ); } - self.focus = update.focus; + self.focus = nid(update.focus); self.is_host_focused = is_host_focused; if !unreachable.is_empty() { fn traverse_unreachable( nodes: &mut HashMap, changes: &mut Option<&mut InternalChanges>, - seen_child_ids: &HashSet, + seen_child_ids: &HashSet, id: NodeId, ) { if let Some(changes) = changes { @@ -181,7 +197,7 @@ impl State { let node = nodes.remove(&id).unwrap(); for child_id in node.data.children().iter() { if !seen_child_ids.contains(child_id) { - traverse_unreachable(nodes, changes, seen_child_ids, *child_id); + traverse_unreachable(nodes, changes, seen_child_ids, nid(*child_id)); } } } @@ -199,17 +215,18 @@ impl State { is_host_focused: bool, changes: Option<&mut InternalChanges>, ) { + let (focus, _) = self.focus.to_components(); let update = TreeUpdate { nodes: vec![], tree: None, tree_id: TreeId::ROOT, - focus: self.focus, + focus, }; self.update(update, is_host_focused, changes); } pub fn has_node(&self, id: NodeId) -> bool { - self.nodes.get(&id).is_some() + self.nodes.contains_key(&id) } pub fn node_by_id(&self, id: NodeId) -> Option> { @@ -221,7 +238,7 @@ impl State { } pub fn root_id(&self) -> NodeId { - self.data.root + nid(self.data.root) } pub fn root(&self) -> Node<'_> { @@ -285,7 +302,9 @@ pub struct Tree { impl Tree { pub fn new(mut initial_state: TreeUpdate, is_host_focused: bool) -> Self { let Some(tree) = initial_state.tree.take() else { - panic!("Tried to initialize the accessibility tree without a root tree. TreeUpdate::tree must be Some."); + panic!( + "Tried to initialize the accessibility tree without a root tree. TreeUpdate::tree must be Some." + ); }; if initial_state.tree_id != TreeId::ROOT { panic!("Cannot initialize with a subtree. TreeUpdate::tree_id must be TreeId::ROOT."); @@ -295,7 +314,7 @@ impl Tree { let mut state = State { nodes: HashMap::new(), data: tree, - focus: initial_state.focus, + focus: nid(initial_state.focus), is_host_focused, }; state.update(initial_state, is_host_focused, None); @@ -391,6 +410,16 @@ impl Tree { pub fn state(&self) -> &State { &self.state } + + pub fn locate_node(&self, node_id: NodeId) -> Option<(LocalNodeId, TreeId)> { + if !self.state.has_node(node_id) { + return None; + } + let (local_id, tree_index) = node_id.to_components(); + self.tree_index_map + .get_id(tree_index) + .map(|tree_id| (local_id, tree_id)) + } } struct ShortNodeList<'a, T>(&'a HashMap); @@ -406,7 +435,7 @@ impl fmt::Display for ShortNodeList<'_, T> { if i != 0 { write!(f, ", ")?; } - write!(f, "{id:?}")?; + write!(f, "{:?}", id.to_components().0)?; } if iter.next().is_some() { write!(f, " ...")?; @@ -417,10 +446,15 @@ impl fmt::Display for ShortNodeList<'_, T> { #[cfg(test)] mod tests { - use accesskit::{Node, NodeId, Role, Tree, TreeId, TreeUpdate, Uuid}; + use accesskit::{Node, NodeId as LocalNodeId, Role, Tree, TreeId, TreeUpdate, Uuid}; use alloc::{vec, vec::Vec}; use super::{TreeIndex, TreeIndexMap}; + use crate::node::NodeId; + + fn node_id(n: u64) -> NodeId { + NodeId::new(LocalNodeId(n), TreeIndex(0)) + } #[test] fn tree_index_map_assigns_sequential_indices() { @@ -472,13 +506,13 @@ mod tests { #[test] fn init_tree_with_root_node() { let update = TreeUpdate { - nodes: vec![(NodeId(0), Node::new(Role::Window))], - tree: Some(Tree::new(NodeId(0))), + nodes: vec![(LocalNodeId(0), Node::new(Role::Window))], + tree: Some(Tree::new(LocalNodeId(0))), tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }; let tree = super::Tree::new(update, false); - assert_eq!(NodeId(0), tree.state().root().id()); + assert_eq!(node_id(0), tree.state().root().id()); assert_eq!(Role::Window, tree.state().root().role()); assert!(tree.state().root().parent().is_none()); } @@ -489,10 +523,10 @@ mod tests { )] fn init_tree_with_non_root_tree_id_panics() { let update = TreeUpdate { - nodes: vec![(NodeId(0), Node::new(Role::Window))], - tree: Some(Tree::new(NodeId(0))), + nodes: vec![(LocalNodeId(0), Node::new(Role::Window))], + tree: Some(Tree::new(LocalNodeId(0))), tree_id: TreeId(Uuid::from_u128(1)), - focus: NodeId(0), + focus: LocalNodeId(0), }; let _ = super::Tree::new(update, false); } @@ -501,27 +535,27 @@ mod tests { fn root_node_has_children() { let update = TreeUpdate { nodes: vec![ - (NodeId(0), { + (LocalNodeId(0), { let mut node = Node::new(Role::Window); - node.set_children(vec![NodeId(1), NodeId(2)]); + node.set_children(vec![LocalNodeId(1), LocalNodeId(2)]); node }), - (NodeId(1), Node::new(Role::Button)), - (NodeId(2), Node::new(Role::Button)), + (LocalNodeId(1), Node::new(Role::Button)), + (LocalNodeId(2), Node::new(Role::Button)), ], - tree: Some(Tree::new(NodeId(0))), + tree: Some(Tree::new(LocalNodeId(0))), tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }; let tree = super::Tree::new(update, false); let state = tree.state(); assert_eq!( - NodeId(0), - state.node_by_id(NodeId(1)).unwrap().parent().unwrap().id() + node_id(0), + state.node_by_id(node_id(1)).unwrap().parent().unwrap().id() ); assert_eq!( - NodeId(0), - state.node_by_id(NodeId(2)).unwrap().parent().unwrap().id() + node_id(0), + state.node_by_id(node_id(2)).unwrap().parent().unwrap().id() ); assert_eq!(2, state.root().children().count()); } @@ -530,25 +564,25 @@ mod tests { fn add_child_to_root_node() { let root_node = Node::new(Role::Window); let first_update = TreeUpdate { - nodes: vec![(NodeId(0), root_node.clone())], - tree: Some(Tree::new(NodeId(0))), + nodes: vec![(LocalNodeId(0), root_node.clone())], + tree: Some(Tree::new(LocalNodeId(0))), tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }; let mut tree = super::Tree::new(first_update, false); assert_eq!(0, tree.state().root().children().count()); let second_update = TreeUpdate { nodes: vec![ - (NodeId(0), { + (LocalNodeId(0), { let mut node = root_node; - node.push_child(NodeId(1)); + node.push_child(LocalNodeId(1)); node }), - (NodeId(1), Node::new(Role::RootWebArea)), + (LocalNodeId(1), Node::new(Role::RootWebArea)), ], tree: None, tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }; struct Handler { got_new_child_node: bool, @@ -559,16 +593,16 @@ mod tests { } impl super::ChangeHandler for Handler { fn node_added(&mut self, node: &crate::Node) { - if node.id() == NodeId(1) { + if node.id() == node_id(1) { self.got_new_child_node = true; return; } unexpected_change(); } fn node_updated(&mut self, old_node: &crate::Node, new_node: &crate::Node) { - if new_node.id() == NodeId(0) + if new_node.id() == node_id(0) && old_node.data().children().is_empty() - && new_node.data().children() == [NodeId(1)] + && new_node.data().children() == [LocalNodeId(1)] { self.got_updated_root_node = true; return; @@ -595,10 +629,10 @@ mod tests { assert!(handler.got_updated_root_node); let state = tree.state(); assert_eq!(1, state.root().children().count()); - assert_eq!(NodeId(1), state.root().children().next().unwrap().id()); + assert_eq!(node_id(1), state.root().children().next().unwrap().id()); assert_eq!( - NodeId(0), - state.node_by_id(NodeId(1)).unwrap().parent().unwrap().id() + node_id(0), + state.node_by_id(node_id(1)).unwrap().parent().unwrap().id() ); } @@ -607,24 +641,24 @@ mod tests { let root_node = Node::new(Role::Window); let first_update = TreeUpdate { nodes: vec![ - (NodeId(0), { + (LocalNodeId(0), { let mut node = root_node.clone(); - node.push_child(NodeId(1)); + node.push_child(LocalNodeId(1)); node }), - (NodeId(1), Node::new(Role::RootWebArea)), + (LocalNodeId(1), Node::new(Role::RootWebArea)), ], - tree: Some(Tree::new(NodeId(0))), + tree: Some(Tree::new(LocalNodeId(0))), tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }; let mut tree = super::Tree::new(first_update, false); assert_eq!(1, tree.state().root().children().count()); let second_update = TreeUpdate { - nodes: vec![(NodeId(0), root_node)], + nodes: vec![(LocalNodeId(0), root_node)], tree: None, tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }; struct Handler { got_updated_root_node: bool, @@ -638,8 +672,8 @@ mod tests { unexpected_change(); } fn node_updated(&mut self, old_node: &crate::Node, new_node: &crate::Node) { - if new_node.id() == NodeId(0) - && old_node.data().children() == [NodeId(1)] + if new_node.id() == node_id(0) + && old_node.data().children() == [LocalNodeId(1)] && new_node.data().children().is_empty() { self.got_updated_root_node = true; @@ -655,7 +689,7 @@ mod tests { unexpected_change(); } fn node_removed(&mut self, node: &crate::Node) { - if node.id() == NodeId(1) { + if node.id() == node_id(1) { self.got_removed_child_node = true; return; } @@ -670,32 +704,32 @@ mod tests { assert!(handler.got_updated_root_node); assert!(handler.got_removed_child_node); assert_eq!(0, tree.state().root().children().count()); - assert!(tree.state().node_by_id(NodeId(1)).is_none()); + assert!(tree.state().node_by_id(node_id(1)).is_none()); } #[test] fn move_focus_between_siblings() { let first_update = TreeUpdate { nodes: vec![ - (NodeId(0), { + (LocalNodeId(0), { let mut node = Node::new(Role::Window); - node.set_children(vec![NodeId(1), NodeId(2)]); + node.set_children(vec![LocalNodeId(1), LocalNodeId(2)]); node }), - (NodeId(1), Node::new(Role::Button)), - (NodeId(2), Node::new(Role::Button)), + (LocalNodeId(1), Node::new(Role::Button)), + (LocalNodeId(2), Node::new(Role::Button)), ], - tree: Some(Tree::new(NodeId(0))), + tree: Some(Tree::new(LocalNodeId(0))), tree_id: TreeId::ROOT, - focus: NodeId(1), + focus: LocalNodeId(1), }; let mut tree = super::Tree::new(first_update, true); - assert!(tree.state().node_by_id(NodeId(1)).unwrap().is_focused()); + assert!(tree.state().node_by_id(node_id(1)).unwrap().is_focused()); let second_update = TreeUpdate { nodes: vec![], tree: None, tree_id: TreeId::ROOT, - focus: NodeId(2), + focus: LocalNodeId(2), }; struct Handler { got_old_focus_node_update: bool, @@ -710,16 +744,16 @@ mod tests { unexpected_change(); } fn node_updated(&mut self, old_node: &crate::Node, new_node: &crate::Node) { - if old_node.id() == NodeId(1) - && new_node.id() == NodeId(1) + if old_node.id() == node_id(1) + && new_node.id() == node_id(1) && old_node.is_focused() && !new_node.is_focused() { self.got_old_focus_node_update = true; return; } - if old_node.id() == NodeId(2) - && new_node.id() == NodeId(2) + if old_node.id() == node_id(2) + && new_node.id() == node_id(2) && !old_node.is_focused() && new_node.is_focused() { @@ -734,7 +768,7 @@ mod tests { new_node: Option<&crate::Node>, ) { if let (Some(old_node), Some(new_node)) = (old_node, new_node) { - if old_node.id() == NodeId(1) && new_node.id() == NodeId(2) { + if old_node.id() == node_id(1) && new_node.id() == node_id(2) { self.got_focus_change = true; return; } @@ -754,8 +788,8 @@ mod tests { assert!(handler.got_old_focus_node_update); assert!(handler.got_new_focus_node_update); assert!(handler.got_focus_change); - assert!(tree.state().node_by_id(NodeId(2)).unwrap().is_focused()); - assert!(!tree.state().node_by_id(NodeId(1)).unwrap().is_focused()); + assert!(tree.state().node_by_id(node_id(2)).unwrap().is_focused()); + assert!(!tree.state().node_by_id(node_id(1)).unwrap().is_focused()); } #[test] @@ -763,35 +797,35 @@ mod tests { let child_node = Node::new(Role::Button); let first_update = TreeUpdate { nodes: vec![ - (NodeId(0), { + (LocalNodeId(0), { let mut node = Node::new(Role::Window); - node.set_children(vec![NodeId(1)]); + node.set_children(vec![LocalNodeId(1)]); node }), - (NodeId(1), { + (LocalNodeId(1), { let mut node = child_node.clone(); node.set_label("foo"); node }), ], - tree: Some(Tree::new(NodeId(0))), + tree: Some(Tree::new(LocalNodeId(0))), tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }; let mut tree = super::Tree::new(first_update, false); assert_eq!( Some("foo".into()), - tree.state().node_by_id(NodeId(1)).unwrap().label() + tree.state().node_by_id(node_id(1)).unwrap().label() ); let second_update = TreeUpdate { - nodes: vec![(NodeId(1), { + nodes: vec![(LocalNodeId(1), { let mut node = child_node; node.set_label("bar"); node })], tree: None, tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }; struct Handler { got_updated_child_node: bool, @@ -804,7 +838,7 @@ mod tests { unexpected_change(); } fn node_updated(&mut self, old_node: &crate::Node, new_node: &crate::Node) { - if new_node.id() == NodeId(1) + if new_node.id() == node_id(1) && old_node.label() == Some("foo".into()) && new_node.label() == Some("bar".into()) { @@ -831,7 +865,7 @@ mod tests { assert!(handler.got_updated_child_node); assert_eq!( Some("bar".into()), - tree.state().node_by_id(NodeId(1)).unwrap().label() + tree.state().node_by_id(node_id(1)).unwrap().label() ); } @@ -843,20 +877,20 @@ mod tests { fn no_change_update() { let update = TreeUpdate { nodes: vec![ - (NodeId(0), { + (LocalNodeId(0), { let mut node = Node::new(Role::Window); - node.set_children(vec![NodeId(1)]); + node.set_children(vec![LocalNodeId(1)]); node }), - (NodeId(1), { + (LocalNodeId(1), { let mut node = Node::new(Role::Button); node.set_label("foo"); node }), ], - tree: Some(Tree::new(NodeId(0))), + tree: Some(Tree::new(LocalNodeId(0))), tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }; let mut tree = super::Tree::new(update.clone(), false); struct Handler; @@ -902,16 +936,16 @@ mod tests { unexpected_change(); } fn node_updated(&mut self, old_node: &crate::Node, new_node: &crate::Node) { - if new_node.id() == NodeId(0) - && old_node.child_ids().collect::>() == vec![NodeId(1)] - && new_node.child_ids().collect::>() == vec![NodeId(2)] + if new_node.id() == node_id(0) + && old_node.child_ids().collect::>() == vec![node_id(1)] + && new_node.child_ids().collect::>() == vec![node_id(2)] { self.got_updated_root = true; return; } - if new_node.id() == NodeId(2) - && old_node.parent_id() == Some(NodeId(1)) - && new_node.parent_id() == Some(NodeId(0)) + if new_node.id() == node_id(2) + && old_node.parent_id() == Some(node_id(1)) + && new_node.parent_id() == Some(node_id(0)) { self.got_updated_child = true; return; @@ -926,7 +960,7 @@ mod tests { unexpected_change(); } fn node_removed(&mut self, node: &crate::Node) { - if node.id() == NodeId(1) { + if node.id() == node_id(1) { self.got_removed_container = true; return; } @@ -935,21 +969,21 @@ mod tests { } let mut root = Node::new(Role::Window); - root.set_children([NodeId(1)]); + root.set_children([LocalNodeId(1)]); let mut container = Node::new(Role::GenericContainer); - container.set_children([NodeId(2)]); + container.set_children([LocalNodeId(2)]); let update = TreeUpdate { nodes: vec![ - (NodeId(0), root.clone()), - (NodeId(1), container), - (NodeId(2), Node::new(Role::Button)), + (LocalNodeId(0), root.clone()), + (LocalNodeId(1), container), + (LocalNodeId(2), Node::new(Role::Button)), ], - tree: Some(Tree::new(NodeId(0))), + tree: Some(Tree::new(LocalNodeId(0))), tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }; let mut tree = crate::Tree::new(update, false); - root.set_children([NodeId(2)]); + root.set_children([LocalNodeId(2)]); let mut handler = Handler { got_updated_root: false, got_updated_child: false, @@ -957,10 +991,10 @@ mod tests { }; tree.update_and_process_changes( TreeUpdate { - nodes: vec![(NodeId(0), root)], + nodes: vec![(LocalNodeId(0), root)], tree: None, tree_id: TreeId::ROOT, - focus: NodeId(0), + focus: LocalNodeId(0), }, &mut handler, ); @@ -969,16 +1003,16 @@ mod tests { assert!(handler.got_removed_container); assert_eq!( tree.state() - .node_by_id(NodeId(0)) + .node_by_id(node_id(0)) .unwrap() .child_ids() .collect::>(), - vec![NodeId(2)] + vec![node_id(2)] ); - assert!(tree.state().node_by_id(NodeId(1)).is_none()); + assert!(tree.state().node_by_id(node_id(1)).is_none()); assert_eq!( - tree.state().node_by_id(NodeId(2)).unwrap().parent_id(), - Some(NodeId(0)) + tree.state().node_by_id(node_id(2)).unwrap().parent_id(), + Some(node_id(0)) ); } } diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index 36442da5..c12c6a03 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -9,8 +9,9 @@ // found in the LICENSE.chromium file. use accesskit::{ - Action, ActionData, ActionHandler, ActionRequest, ActivationHandler, Node as NodeData, NodeId, - Orientation, Point, Role, ScrollUnit, TextSelection, Tree as TreeData, TreeId, TreeUpdate, + Action, ActionData, ActionHandler, ActionRequest, ActivationHandler, Node as NodeData, + NodeId as LocalNodeId, Orientation, Point, Role, ScrollUnit, TextSelection, Tree as TreeData, + TreeId, TreeUpdate, }; use accesskit_consumer::{FilterResult, Node, TextPosition, Tree, TreeChangeHandler}; use jni::{ @@ -156,7 +157,7 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> { } } -const PLACEHOLDER_ROOT_ID: NodeId = NodeId(0); +const PLACEHOLDER_ROOT_ID: LocalNodeId = LocalNodeId(0); #[derive(Debug, Default)] enum State { @@ -383,6 +384,7 @@ impl Adapter { } else { self.node_id_map.get_accesskit_id(virtual_view_id)? }; + let (target_node, _) = tree.locate_node(target)?; let mut events = Vec::new(); let request = match action { ACTION_CLICK => ActionRequest { @@ -397,12 +399,12 @@ impl Adapter { Action::Click } }, - target, + target: target_node, data: None, }, ACTION_FOCUS => ActionRequest { action: Action::Focus, - target, + target: target_node, data: None, }, ACTION_SCROLL_BACKWARD | ACTION_SCROLL_FORWARD => ActionRequest { @@ -437,7 +439,7 @@ impl Adapter { Action::ScrollRight } }, - target, + target: target_node, data: Some(ActionData::ScrollUnit(ScrollUnit::Page)), }, ACTION_ACCESSIBILITY_FOCUS => { @@ -492,7 +494,8 @@ impl Adapter { let id = self.node_id_map.get_accesskit_id(virtual_view_id)?; tree_state.node_by_id(id).unwrap() }; - let target = node.id(); + let (node_id, tree_id) = tree.locate_node(node.id())?; + let (focus_id, _) = tree.locate_node(tree_state.focus_id_in_tree())?; // TalkBack expects the text selection change to take effect // immediately, so we optimistically update the node. // But don't be *too* optimistic. @@ -507,14 +510,14 @@ impl Adapter { let mut new_node = node.data().clone(); new_node.set_text_selection(selection); let update = TreeUpdate { - nodes: vec![(node.id(), new_node)], + nodes: vec![(node_id, new_node)], tree: None, - tree_id: TreeId::ROOT, - focus: tree_state.focus_id_in_tree(), + tree_id, + focus: focus_id, }; update_tree(events, &mut self.node_id_map, tree, update); let request = ActionRequest { - target, + target: node_id, action: Action::SetTextSelection, data: Some(ActionData::SetTextSelection(selection)), }; diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index 283f4b91..0f9c708e 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -3,8 +3,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::NodeId; -use accesskit_consumer::Node; +use accesskit_consumer::{Node, NodeId}; use jni::{objects::JObject, sys::jint, JNIEnv}; use std::collections::HashMap; diff --git a/platforms/atspi-common/src/adapter.rs b/platforms/atspi-common/src/adapter.rs index a602e62a..0406fae4 100644 --- a/platforms/atspi-common/src/adapter.rs +++ b/platforms/atspi-common/src/adapter.rs @@ -15,8 +15,8 @@ use crate::{ util::WindowBounds, AdapterCallback, Event, ObjectEvent, WindowEvent, }; -use accesskit::{ActionHandler, NodeId, Role, TreeUpdate}; -use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler, TreeState}; +use accesskit::{ActionHandler, Role, TreeUpdate}; +use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeChangeHandler, TreeState}; use atspi_common::{InterfaceSet, Politeness, State}; use std::fmt::{Debug, Formatter}; use std::{ diff --git a/platforms/atspi-common/src/callback.rs b/platforms/atspi-common/src/callback.rs index deb1df82..81dce9b9 100644 --- a/platforms/atspi-common/src/callback.rs +++ b/platforms/atspi-common/src/callback.rs @@ -3,7 +3,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::NodeId; +use accesskit_consumer::NodeId; use atspi_common::InterfaceSet; use crate::{Adapter, Event}; diff --git a/platforms/atspi-common/src/events.rs b/platforms/atspi-common/src/events.rs index 8e8cd6ee..ae031ed6 100644 --- a/platforms/atspi-common/src/events.rs +++ b/platforms/atspi-common/src/events.rs @@ -3,7 +3,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::NodeId; +use accesskit_consumer::NodeId; use atspi_common::{Politeness, Role, State}; use crate::{NodeIdOrRoot, Rect}; diff --git a/platforms/atspi-common/src/lib.rs b/platforms/atspi-common/src/lib.rs index 8680944d..ece8ef0d 100644 --- a/platforms/atspi-common/src/lib.rs +++ b/platforms/atspi-common/src/lib.rs @@ -16,6 +16,7 @@ mod rect; pub mod simplified; mod util; +pub use accesskit_consumer::NodeId; pub use atspi_common::{ CoordType, Granularity, InterfaceSet, Layer, RelationType, Role, ScrollType, State, StateSet, }; diff --git a/platforms/atspi-common/src/node.rs b/platforms/atspi-common/src/node.rs index 33aa2279..0eacbf78 100644 --- a/platforms/atspi-common/src/node.rs +++ b/platforms/atspi-common/src/node.rs @@ -9,10 +9,10 @@ // found in the LICENSE.chromium file. use accesskit::{ - Action, ActionData, ActionRequest, Affine, Live, NodeId, Orientation, Point, Rect, Role, - Toggled, + Action, ActionData, ActionRequest, Affine, Live, NodeId as LocalNodeId, Orientation, Point, + Rect, Role, Toggled, }; -use accesskit_consumer::{FilterResult, Node, TreeState}; +use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeState}; use atspi_common::{ CoordType, Granularity, Interface, InterfaceSet, Layer, Politeness, RelationType, Role as AtspiRole, ScrollType, State, StateSet, @@ -655,22 +655,22 @@ impl PlatformNode { f(tree.state()) } - fn with_tree_state_and_context(&self, f: F) -> Result + fn with_tree_and_context(&self, f: F) -> Result where - F: FnOnce(&TreeState, &Context) -> Result, + F: FnOnce(&Tree, &Context) -> Result, { let context = self.upgrade_context()?; let tree = context.read_tree(); - f(tree.state(), &context) + f(&tree, &context) } fn resolve_with_context(&self, f: F) -> Result where - for<'a> F: FnOnce(Node<'a>, &Context) -> Result, + for<'a> F: FnOnce(Node<'a>, &'a Tree, &Context) -> Result, { - self.with_tree_state_and_context(|state, context| { - if let Some(node) = state.node_by_id(self.id) { - f(node, context) + self.with_tree_and_context(|tree, context| { + if let Some(node) = tree.state().node_by_id(self.id) { + f(node, tree, context) } else { Err(Error::Defunct) } @@ -679,12 +679,12 @@ impl PlatformNode { fn resolve_for_selection_with_context(&self, f: F) -> Result where - for<'a> F: FnOnce(Node<'a>, &Context) -> Result, + for<'a> F: FnOnce(Node<'a>, &'a Tree, &Context) -> Result, { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, context| { let wrapper = NodeWrapper(&node); if wrapper.supports_selection() { - f(node, context) + f(node, tree, context) } else { Err(Error::UnsupportedInterface) } @@ -693,12 +693,12 @@ impl PlatformNode { fn resolve_for_text_with_context(&self, f: F) -> Result where - for<'a> F: FnOnce(Node<'a>, &Context) -> Result, + for<'a> F: FnOnce(Node<'a>, &'a Tree, &Context) -> Result, { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, context| { let wrapper = NodeWrapper(&node); if wrapper.supports_text() { - f(node, context) + f(node, tree, context) } else { Err(Error::UnsupportedInterface) } @@ -709,7 +709,7 @@ impl PlatformNode { where for<'a> F: FnOnce(Node<'a>) -> Result, { - self.resolve_with_context(|node, _| f(node)) + self.resolve_with_context(|node, _, _| f(node)) } fn resolve_for_selection(&self, f: F) -> Result @@ -730,17 +730,17 @@ impl PlatformNode { where for<'a> F: FnOnce(Node<'a>) -> Result, { - self.resolve_for_text_with_context(|node, _| f(node)) + self.resolve_for_text_with_context(|node, _, _| f(node)) } - fn do_action_internal(&self, f: F) -> Result<()> + fn do_action_internal(&self, target: NodeId, f: F) -> Result<()> where - F: FnOnce(&TreeState, &Context) -> ActionRequest, + F: FnOnce(&TreeState, &Context, LocalNodeId) -> ActionRequest, { let context = self.upgrade_context()?; let tree = context.read_tree(); - if tree.state().has_node(self.id) { - let request = f(tree.state(), &context); + if let Some((target_node, _)) = tree.locate_node(target) { + let request = f(tree.state(), &context, target_node); drop(tree); context.do_action(request); Ok(()) @@ -880,9 +880,9 @@ impl PlatformNode { } pub fn state(&self) -> StateSet { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, _| { let wrapper = NodeWrapper(&node); - Ok(wrapper.state(context.read_tree().state().focus_id().is_some())) + Ok(wrapper.state(tree.state().focus_id().is_some())) }) .unwrap_or(State::Defunct.into()) } @@ -970,16 +970,16 @@ impl PlatformNode { if index != 0 { return Ok(false); } - self.do_action_internal(|_, _| ActionRequest { + self.do_action_internal(self.id, |_, _, target| ActionRequest { action: Action::Click, - target: self.id, + target, data: None, })?; Ok(true) } pub fn contains(&self, x: i32, y: i32, coord_type: CoordType) -> Result { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let window_bounds = context.read_root_window_bounds(); let wrapper = NodeWrapper(&node); if let Some(extents) = wrapper.extents(&window_bounds, coord_type) { @@ -996,7 +996,7 @@ impl PlatformNode { y: i32, coord_type: CoordType, ) -> Result> { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let window_bounds = context.read_root_window_bounds(); let point = window_bounds.atspi_point_to_accesskit_point( Point::new(x.into(), y.into()), @@ -1009,7 +1009,7 @@ impl PlatformNode { } pub fn extents(&self, coord_type: CoordType) -> Result { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let window_bounds = context.read_root_window_bounds(); let wrapper = NodeWrapper(&node); Ok(wrapper @@ -1030,34 +1030,35 @@ impl PlatformNode { } pub fn grab_focus(&self) -> Result { - self.do_action_internal(|_, _| ActionRequest { + self.do_action_internal(self.id, |_, _, target| ActionRequest { action: Action::Focus, - target: self.id, + target, data: None, })?; Ok(true) } pub fn scroll_to(&self, scroll_type: ScrollType) -> Result { - self.do_action_internal(|_, _| ActionRequest { + self.do_action_internal(self.id, |_, _, target| ActionRequest { action: Action::ScrollIntoView, - target: self.id, + target, data: atspi_scroll_type_to_scroll_hint(scroll_type).map(ActionData::ScrollHint), })?; Ok(true) } pub fn scroll_to_point(&self, coord_type: CoordType, x: i32, y: i32) -> Result { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, context| { let window_bounds = context.read_root_window_bounds(); let point = window_bounds.atspi_point_to_accesskit_point( Point::new(x.into(), y.into()), node.filtered_parent(&filter), coord_type, ); + let (target, _) = tree.locate_node(self.id).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::ScrollToPoint, - target: self.id, + target, data: Some(ActionData::ScrollToPoint(point)), }); Ok(()) @@ -1086,14 +1087,15 @@ impl PlatformNode { } pub fn select_child(&self, child_index: usize) -> Result { - self.resolve_for_selection_with_context(|node, context| { + self.resolve_for_selection_with_context(|node, tree, context| { if let Some(child) = node.filtered_children(filter).nth(child_index) { if let Some(true) = child.is_selected() { Ok(true) } else if child.is_selectable() && child.is_clickable(&filter) { + let (target, _) = tree.locate_node(child.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::Click, - target: child.id(), + target, data: None, }); Ok(true) @@ -1107,16 +1109,17 @@ impl PlatformNode { } pub fn deselect_selected_child(&self, selected_child_index: usize) -> Result { - self.resolve_for_selection_with_context(|node, context| { + self.resolve_for_selection_with_context(|node, tree, context| { if let Some(child) = node .items(filter) .filter(|c| c.is_selected() == Some(true)) .nth(selected_child_index) { if child.is_clickable(&filter) { + let (target, _) = tree.locate_node(child.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::Click, - target: child.id(), + target, data: None, }); Ok(true) @@ -1149,14 +1152,15 @@ impl PlatformNode { } pub fn deselect_child(&self, child_index: usize) -> Result { - self.resolve_for_selection_with_context(|node, context| { + self.resolve_for_selection_with_context(|node, tree, context| { if let Some(child) = node.filtered_children(filter).nth(child_index) { if let Some(false) = child.is_selected() { Ok(true) } else if child.is_selectable() && child.is_clickable(&filter) { + let (target, _) = tree.locate_node(child.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::Click, - target: child.id(), + target, data: None, }); Ok(true) @@ -1222,11 +1226,12 @@ impl PlatformNode { } pub fn set_caret_offset(&self, offset: i32) -> Result { - self.resolve_for_text_with_context(|node, context| { + self.resolve_for_text_with_context(|node, tree, context| { let offset = text_position_from_offset(&node, offset).ok_or(Error::IndexOutOfRange)?; + let (target, _) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::SetTextSelection, - target: node.id(), + target, data: Some(ActionData::SetTextSelection( offset.to_degenerate_range().to_text_selection(), )), @@ -1251,7 +1256,7 @@ impl PlatformNode { } pub fn character_extents(&self, offset: i32, coord_type: CoordType) -> Result { - self.resolve_for_text_with_context(|node, context| { + self.resolve_for_text_with_context(|node, _, context| { let range = text_range_from_offset(&node, offset, Granularity::Char)?; if let Some(bounds) = range.bounding_boxes().first() { let window_bounds = context.read_root_window_bounds(); @@ -1268,7 +1273,7 @@ impl PlatformNode { } pub fn offset_at_point(&self, x: i32, y: i32, coord_type: CoordType) -> Result { - self.resolve_for_text_with_context(|node, context| { + self.resolve_for_text_with_context(|node, _, context| { let window_bounds = context.read_root_window_bounds(); let point = window_bounds.atspi_point_to_accesskit_point( Point::new(x.into(), y.into()), @@ -1327,15 +1332,16 @@ impl PlatformNode { return Ok(false); } - self.resolve_for_text_with_context(|node, context| { + self.resolve_for_text_with_context(|node, tree, context| { // Simply collapse the selection to the position of the caret if a caret is // visible, otherwise set the selection to 0. let selection_end = node .text_selection_focus() .unwrap_or_else(|| node.document_range().start()); + let (target, _) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::SetTextSelection, - target: node.id(), + target, data: Some(ActionData::SetTextSelection( selection_end.to_degenerate_range().to_text_selection(), )), @@ -1354,12 +1360,13 @@ impl PlatformNode { return Ok(false); } - self.resolve_for_text_with_context(|node, context| { + self.resolve_for_text_with_context(|node, tree, context| { let range = text_range_from_offsets(&node, start_offset, end_offset) .ok_or(Error::IndexOutOfRange)?; + let (target, _) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::SetTextSelection, - target: node.id(), + target, data: Some(ActionData::SetTextSelection(range.to_text_selection())), }); Ok(true) @@ -1372,7 +1379,7 @@ impl PlatformNode { end_offset: i32, coord_type: CoordType, ) -> Result { - self.resolve_for_text_with_context(|node, context| { + self.resolve_for_text_with_context(|node, _, context| { if let Some(rect) = text_range_bounds_from_offsets(&node, start_offset, end_offset) { let window_bounds = context.read_root_window_bounds(); let new_origin = window_bounds.accesskit_point_to_atspi_point( @@ -1405,7 +1412,7 @@ impl PlatformNode { end_offset: i32, scroll_type: ScrollType, ) -> Result { - self.resolve_for_text_with_context(|node, context| { + self.resolve_for_text_with_context(|node, tree, context| { if let Some(range) = text_range_from_offsets(&node, start_offset, end_offset) { let position = if matches!( scroll_type, @@ -1415,9 +1422,12 @@ impl PlatformNode { } else { range.start() }; + let (target, _) = tree + .locate_node(position.inner_node().id()) + .ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::ScrollIntoView, - target: position.inner_node().id(), + target, data: atspi_scroll_type_to_scroll_hint(scroll_type).map(ActionData::ScrollHint), }); Ok(true) @@ -1435,7 +1445,7 @@ impl PlatformNode { x: i32, y: i32, ) -> Result { - self.resolve_for_text_with_context(|node, context| { + self.resolve_for_text_with_context(|node, tree, context| { let window_bounds = context.read_root_window_bounds(); let target_point = window_bounds.atspi_point_to_accesskit_point( Point::new(x.into(), y.into()), @@ -1445,9 +1455,10 @@ impl PlatformNode { if let Some(rect) = text_range_bounds_from_offsets(&node, start_offset, end_offset) { let point = Point::new(target_point.x - rect.x0, target_point.y - rect.y0); + let (target, _) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::ScrollToPoint, - target: node.id(), + target, data: Some(ActionData::ScrollToPoint(point)), }); return Ok(true); @@ -1476,9 +1487,9 @@ impl PlatformNode { } pub fn set_current_value(&self, value: f64) -> Result<()> { - self.do_action_internal(|_, _| ActionRequest { + self.do_action_internal(self.id, |_, _, target| ActionRequest { action: Action::SetValue, - target: self.id, + target, data: Some(ActionData::NumericValue(value)), }) } diff --git a/platforms/macos/src/adapter.rs b/platforms/macos/src/adapter.rs index fbeca27c..6c8c934c 100644 --- a/platforms/macos/src/adapter.rs +++ b/platforms/macos/src/adapter.rs @@ -11,8 +11,8 @@ use crate::{ util::*, }; use accesskit::{ - ActionHandler, ActionRequest, ActivationHandler, Node as NodeProvider, NodeId, Role, - Tree as TreeData, TreeId, TreeUpdate, + ActionHandler, ActionRequest, ActivationHandler, Node as NodeProvider, NodeId as LocalNodeId, + Role, Tree as TreeData, TreeId, TreeUpdate, }; use accesskit_consumer::{FilterResult, Tree}; use objc2::rc::{Id, WeakId}; @@ -21,7 +21,7 @@ use objc2_foundation::{MainThreadMarker, NSArray, NSObject, NSPoint}; use std::fmt::{Debug, Formatter}; use std::{ffi::c_void, ptr::null_mut, rc::Rc}; -const PLACEHOLDER_ROOT_ID: NodeId = NodeId(0); +const PLACEHOLDER_ROOT_ID: LocalNodeId = LocalNodeId(0); enum State { Inactive { diff --git a/platforms/macos/src/context.rs b/platforms/macos/src/context.rs index b3d51fa2..687fcbc6 100644 --- a/platforms/macos/src/context.rs +++ b/platforms/macos/src/context.rs @@ -4,8 +4,8 @@ // the LICENSE-MIT file), at your option. use crate::node::PlatformNode; -use accesskit::{ActionHandler, ActionRequest, NodeId}; -use accesskit_consumer::Tree; +use accesskit::{ActionHandler, ActionRequest}; +use accesskit_consumer::{NodeId, Tree}; use hashbrown::HashMap; use objc2::rc::{Id, WeakId}; use objc2_app_kit::*; diff --git a/platforms/macos/src/event.rs b/platforms/macos/src/event.rs index 6758b55e..5a1cdc41 100644 --- a/platforms/macos/src/event.rs +++ b/platforms/macos/src/event.rs @@ -3,8 +3,8 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::{Live, NodeId, Role}; -use accesskit_consumer::{FilterResult, Node, TreeChangeHandler}; +use accesskit::{Live, Role}; +use accesskit_consumer::{FilterResult, Node, NodeId, TreeChangeHandler}; use hashbrown::HashSet; use objc2::runtime::{AnyObject, ProtocolObject}; use objc2_app_kit::*; diff --git a/platforms/macos/src/node.rs b/platforms/macos/src/node.rs index 095e8547..2931de2c 100644 --- a/platforms/macos/src/node.rs +++ b/platforms/macos/src/node.rs @@ -10,10 +10,8 @@ #![allow(non_upper_case_globals)] -use accesskit::{ - Action, ActionData, ActionRequest, NodeId, Orientation, Role, TextSelection, Toggled, -}; -use accesskit_consumer::{FilterResult, Node}; +use accesskit::{Action, ActionData, ActionRequest, Orientation, Role, TextSelection, Toggled}; +use accesskit_consumer::{FilterResult, Node, NodeId, Tree}; use objc2::{ declare_class, msg_send_id, mutability::InteriorMutable, @@ -384,7 +382,7 @@ declare_class!( unsafe impl PlatformNode { #[method_id(accessibilityParent)] fn parent(&self) -> Option> { - self.resolve_with_context(|node, context| { + 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 { @@ -399,7 +397,7 @@ declare_class!( #[method_id(accessibilityWindow)] fn window(&self) -> Option> { - self.resolve_with_context(|_, context| { + self.resolve_with_context(|_, _, context| { context .view .load() @@ -410,7 +408,7 @@ declare_class!( #[method_id(accessibilityTopLevelUIElement)] fn top_level(&self) -> Option> { - self.resolve_with_context(|_, context| { + self.resolve_with_context(|_, _, context| { context .view .load() @@ -432,7 +430,7 @@ declare_class!( #[method_id(accessibilitySelectedChildren)] fn selected_children(&self) -> Option>> { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let wrapper = NodeWrapper(node); if !wrapper.is_container_with_selectable_children() { return None; @@ -449,7 +447,7 @@ declare_class!( #[method(accessibilityFrame)] fn frame(&self) -> NSRect { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let view = match context.view.load() { Some(view) => view, None => { @@ -554,20 +552,24 @@ declare_class!( #[method(setAccessibilityValue:)] fn set_value(&self, value: &NSObject) { if let Some(string) = downcast_ref::(value) { - self.resolve_with_context(|node, context| { - context.do_action(ActionRequest { - action: Action::SetValue, - target: node.id(), - data: Some(ActionData::Value(string.to_string().into())), - }); + self.resolve_with_context(|node, tree, context| { + if let Some((target, _)) = tree.locate_node(node.id()) { + context.do_action(ActionRequest { + action: Action::SetValue, + target, + data: Some(ActionData::Value(string.to_string().into())), + }); + } }); } else if let Some(number) = downcast_ref::(value) { - self.resolve_with_context(|node, context| { - context.do_action(ActionRequest { - action: Action::SetValue, - target: node.id(), - data: Some(ActionData::NumericValue(number.doubleValue())), - }); + self.resolve_with_context(|node, tree, context| { + if let Some((target, _)) = tree.locate_node(node.id()) { + context.do_action(ActionRequest { + action: Action::SetValue, + target, + data: Some(ActionData::NumericValue(number.doubleValue())), + }); + } }); } } @@ -619,23 +621,27 @@ declare_class!( #[method(setAccessibilityFocused:)] fn set_focused(&self, focused: bool) { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, context| { if focused { if node.is_focusable(&filter) { - context.do_action(ActionRequest { - action: Action::Focus, - target: node.id(), - data: None, - }); + if let Some((target, _)) = tree.locate_node(node.id()) { + context.do_action(ActionRequest { + action: Action::Focus, + target, + data: None, + }); + } } } else { - let root = node.tree_state.root(); + let root = tree.state().root(); if root.is_focusable(&filter) { - context.do_action(ActionRequest { - action: Action::Focus, - target: root.id(), - data: None, - }); + if let Some((target, _)) = tree.locate_node(root.id()) { + context.do_action(ActionRequest { + action: Action::Focus, + target, + data: None, + }); + } } } }); @@ -643,14 +649,16 @@ declare_class!( #[method(accessibilityPerformPress)] fn press(&self) -> bool { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, context| { let clickable = node.is_clickable(&filter); if clickable { - context.do_action(ActionRequest { - action: Action::Click, - target: node.id(), - data: None, - }); + if let Some((target, _)) = tree.locate_node(node.id()) { + context.do_action(ActionRequest { + action: Action::Click, + target, + data: None, + }); + } } clickable }) @@ -659,14 +667,16 @@ declare_class!( #[method(accessibilityPerformIncrement)] fn increment(&self) -> bool { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, context| { let supports_increment = node.supports_increment(&filter); if supports_increment { - context.do_action(ActionRequest { - action: Action::Increment, - target: node.id(), - data: None, - }); + if let Some((target, _)) = tree.locate_node(node.id()) { + context.do_action(ActionRequest { + action: Action::Increment, + target, + data: None, + }); + } } supports_increment }) @@ -675,14 +685,16 @@ declare_class!( #[method(accessibilityPerformDecrement)] fn decrement(&self) -> bool { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, context| { let supports_decrement = node.supports_decrement(&filter); if supports_decrement { - context.do_action(ActionRequest { - action: Action::Decrement, - target: node.id(), - data: None, - }); + if let Some((target, _)) = tree.locate_node(node.id()) { + context.do_action(ActionRequest { + action: Action::Decrement, + target, + data: None, + }); + } } supports_decrement }) @@ -761,7 +773,7 @@ declare_class!( #[method(accessibilityRangeForPosition:)] fn range_for_position(&self, point: NSPoint) -> NSRange { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let view = match context.view.load() { Some(view) => view, None => { @@ -795,7 +807,7 @@ declare_class!( #[method(accessibilityFrameForRange:)] fn frame_for_range(&self, range: NSRange) -> NSRect { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let view = match context.view.load() { Some(view) => view, None => { @@ -846,14 +858,16 @@ declare_class!( #[method(setAccessibilitySelectedTextRange:)] fn set_selected_text_range(&self, range: NSRange) { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, 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())), - }); + if let Some((target, _)) = tree.locate_node(node.id()) { + context.do_action(ActionRequest { + action: Action::SetTextSelection, + target, + data: Some(ActionData::SetTextSelection(range.to_text_selection())), + }); + } } } }); @@ -878,7 +892,7 @@ declare_class!( #[method(setAccessibilitySelected:)] fn set_selected(&self, selected: bool) { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, context| { let wrapper = NodeWrapper(node); if !node.is_clickable(&filter) || !wrapper.is_item_like() @@ -889,11 +903,13 @@ declare_class!( if node.is_selected() == Some(selected) { return; } - context.do_action(ActionRequest { - action: Action::Click, - target: node.id(), - data: None, - }); + if let Some((target, _)) = tree.locate_node(node.id()) { + context.do_action(ActionRequest { + action: Action::Click, + target, + data: None, + }); + } }); } @@ -913,7 +929,7 @@ declare_class!( #[method_id(accessibilityRows)] fn rows(&self) -> Option>> { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let wrapper = NodeWrapper(node); if !wrapper.is_container_with_selectable_children() { return None; @@ -929,7 +945,7 @@ declare_class!( #[method_id(accessibilitySelectedRows)] fn selected_rows(&self) -> Option>> { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let wrapper = NodeWrapper(node); if !wrapper.is_container_with_selectable_children() { return None; @@ -946,17 +962,19 @@ declare_class!( #[method(accessibilityPerformPick)] fn pick(&self) -> bool { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, context| { let wrapper = NodeWrapper(node); let selectable = node.is_clickable(&filter) && wrapper.is_item_like() && node.is_selectable(); if selectable { - context.do_action(ActionRequest { - action: Action::Click, - target: node.id(), - data: None, - }); + if let Some((target, _)) = tree.locate_node(node.id()) { + context.do_action(ActionRequest { + action: Action::Click, + target, + data: None, + }); + } } selectable }) @@ -965,7 +983,7 @@ declare_class!( #[method_id(accessibilityLinkedUIElements)] fn linked_ui_elements(&self) -> Option>> { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let platform_nodes: Vec> = node .controls() .filter(|controlled| filter(controlled) == FilterResult::Include) @@ -982,7 +1000,7 @@ declare_class!( #[method_id(accessibilityTabs)] fn tabs(&self) -> Option>> { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { if node.role() != Role::TabList { return None; } @@ -1022,13 +1040,15 @@ declare_class!( #[method(accessibilityPerformAction:)] fn perform_action(&self, action: &NSString) { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, tree, context| { if action == ns_string!(SCROLL_TO_VISIBLE_ACTION) { - context.do_action(ActionRequest { - action: Action::ScrollIntoView, - target: node.id(), - data: None, - }); + if let Some((target, _)) = tree.locate_node(node.id()) { + context.do_action(ActionRequest { + action: Action::ScrollIntoView, + target, + data: None, + }); + } } }); } @@ -1134,24 +1154,24 @@ impl PlatformNode { fn resolve_with_context(&self, f: F) -> Option where - F: FnOnce(&Node, &Rc) -> T, + F: FnOnce(&Node, &Tree, &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)) + Some(f(&node, &tree, &context)) } fn resolve(&self, f: F) -> Option where F: FnOnce(&Node) -> T, { - self.resolve_with_context(|node, _| f(node)) + self.resolve_with_context(|node, _, _| f(node)) } fn children_internal(&self) -> Option>> { - self.resolve_with_context(|node, context| { + self.resolve_with_context(|node, _, context| { let platform_nodes = node .filtered_children(filter) .map(|child| context.get_or_create_platform_node(child.id())) diff --git a/platforms/unix/src/adapter.rs b/platforms/unix/src/adapter.rs index a7c44d58..66bfa9b8 100644 --- a/platforms/unix/src/adapter.rs +++ b/platforms/unix/src/adapter.rs @@ -3,10 +3,10 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::{ActionHandler, ActivationHandler, DeactivationHandler, NodeId, Rect, TreeUpdate}; +use accesskit::{ActionHandler, ActivationHandler, DeactivationHandler, Rect, TreeUpdate}; use accesskit_atspi_common::{ next_adapter_id, ActionHandlerNoMut, ActionHandlerWrapper, Adapter as AdapterImpl, - AdapterCallback, Event, PlatformNode, WindowBounds, + AdapterCallback, Event, NodeId, PlatformNode, WindowBounds, }; #[cfg(not(feature = "tokio"))] use async_channel::Sender; diff --git a/platforms/unix/src/atspi/bus.rs b/platforms/unix/src/atspi/bus.rs index afeb9d09..2e8fef47 100644 --- a/platforms/unix/src/atspi/bus.rs +++ b/platforms/unix/src/atspi/bus.rs @@ -8,9 +8,8 @@ use crate::{ context::get_or_init_app_context, executor::{Executor, Task}, }; -use accesskit::NodeId; use accesskit_atspi_common::{ - NodeIdOrRoot, ObjectEvent, PlatformNode, PlatformRoot, Property, WindowEvent, + NodeId, NodeIdOrRoot, ObjectEvent, PlatformNode, PlatformRoot, Property, WindowEvent, }; use atspi::{ events::EventBodyBorrowed, diff --git a/platforms/unix/src/atspi/object_id.rs b/platforms/unix/src/atspi/object_id.rs index d291cf62..e89dde9d 100644 --- a/platforms/unix/src/atspi/object_id.rs +++ b/platforms/unix/src/atspi/object_id.rs @@ -4,8 +4,7 @@ // the LICENSE-MIT file), at your option. use crate::atspi::OwnedObjectAddress; -use accesskit::NodeId; -use accesskit_atspi_common::PlatformNode; +use accesskit_atspi_common::{NodeId, PlatformNode}; use serde::{Serialize, Serializer}; use zbus::{ names::UniqueName, @@ -31,7 +30,9 @@ impl ObjectId { Self::Root => ObjectPath::from_str_unchecked(ROOT_PATH), Self::Node { adapter, node } => ObjectPath::from_string_unchecked(format!( "{}{}/{}", - ACCESSIBLE_PATH_PREFIX, adapter, node.0 + ACCESSIBLE_PATH_PREFIX, + adapter, + u128::from(*node) )), } .into() @@ -45,7 +46,7 @@ impl Serialize for ObjectId { { match self { Self::Root => serializer.serialize_str("root"), - Self::Node { node, .. } => serializer.serialize_str(&node.0.to_string()), + Self::Node { node, .. } => serializer.serialize_str(&u128::from(*node).to_string()), } } } @@ -58,7 +59,7 @@ impl From for Structure<'_> { fn from(id: ObjectId) -> Self { Self::from((match id { ObjectId::Root => "root".into(), - ObjectId::Node { node, .. } => node.0.to_string(), + ObjectId::Node { node, .. } => u128::from(node).to_string(), },)) } } diff --git a/platforms/windows/src/adapter.rs b/platforms/windows/src/adapter.rs index f51ac123..b9ce64f1 100644 --- a/platforms/windows/src/adapter.rs +++ b/platforms/windows/src/adapter.rs @@ -4,10 +4,10 @@ // the LICENSE-MIT file), at your option. use accesskit::{ - ActionHandler, ActivationHandler, Live, Node as NodeProvider, NodeId, Role, Tree as TreeData, - TreeId, TreeUpdate, + ActionHandler, ActivationHandler, Live, Node as NodeProvider, NodeId as LocalNodeId, Role, + Tree as TreeData, TreeId, TreeUpdate, }; -use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler}; +use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeChangeHandler}; use hashbrown::{HashMap, HashSet}; use std::fmt::{Debug, Formatter}; use std::sync::{atomic::Ordering, Arc}; @@ -323,7 +323,7 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> { // TODO: handle other events (#20) } -const PLACEHOLDER_ROOT_ID: NodeId = NodeId(0); +const PLACEHOLDER_ROOT_ID: LocalNodeId = LocalNodeId(0); enum State { Inactive { diff --git a/platforms/windows/src/node.rs b/platforms/windows/src/node.rs index ee8f37ca..3d6c5b6d 100644 --- a/platforms/windows/src/node.rs +++ b/platforms/windows/src/node.rs @@ -11,10 +11,10 @@ #![allow(non_upper_case_globals)] use accesskit::{ - Action, ActionData, ActionRequest, AriaCurrent, HasPopup, Live, NodeId, NodeIdContent, - Orientation, Point, Role, SortDirection, Toggled, + Action, ActionData, ActionRequest, AriaCurrent, HasPopup, Live, NodeId as LocalNodeId, + Orientation, Point, Role, SortDirection, Toggled, TreeId, }; -use accesskit_consumer::{FilterResult, Node, TreeState}; +use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeState}; use std::sync::{atomic::Ordering, Arc, Weak}; use windows::{ core::*, @@ -32,13 +32,15 @@ use crate::{ util::*, }; -const RUNTIME_ID_SIZE: usize = 3; +const RUNTIME_ID_SIZE: usize = 5; fn runtime_id_from_node_id(id: NodeId) -> [i32; RUNTIME_ID_SIZE] { - static_assertions::assert_eq_size!(NodeIdContent, u64); - let id = id.0; + static_assertions::assert_eq_size!(NodeId, u128); + let id: u128 = id.into(); [ UiaAppendRuntimeId as _, + ((id >> 96) & 0xFFFFFFFF) as _, + ((id >> 64) & 0xFFFFFFFF) as _, ((id >> 32) & 0xFFFFFFFF) as _, (id & 0xFFFFFFFF) as _, ] @@ -779,10 +781,17 @@ impl PlatformNode { fn with_tree_state_and_context(&self, f: F) -> Result where F: FnOnce(&TreeState, &Context) -> Result, + { + self.with_tree_and_context(|tree, context| f(tree.state(), context)) + } + + fn with_tree_and_context(&self, f: F) -> Result + where + F: FnOnce(&Tree, &Context) -> Result, { let context = self.upgrade_context()?; let tree = context.read_tree(); - f(tree.state(), &context) + f(&tree, &context) } fn with_tree_state(&self, f: F) -> Result @@ -792,7 +801,8 @@ impl PlatformNode { self.with_tree_state_and_context(|state, _| f(state)) } - fn node<'a>(&self, state: &'a TreeState) -> Result> { + fn node<'a>(&self, tree: &'a Tree) -> Result> { + let state = tree.state(); if let Some(id) = self.node_id { if let Some(node) = state.node_by_id(id) { Ok(node) @@ -804,12 +814,20 @@ impl PlatformNode { } } + fn node_with_location<'a>(&self, tree: &'a Tree) -> Result<(Node<'a>, LocalNodeId, TreeId)> { + let node = self.node(tree)?; + let (local_id, tree_id) = tree + .locate_node(node.id()) + .ok_or_else(element_not_available)?; + Ok((node, local_id, tree_id)) + } + fn resolve_with_context(&self, f: F) -> Result where for<'a> F: FnOnce(Node<'a>, &Context) -> Result, { - self.with_tree_state_and_context(|state, context| { - let node = self.node(state)?; + self.with_tree_and_context(|tree, context| { + let node = self.node(tree)?; f(node, context) }) } @@ -818,9 +836,9 @@ impl PlatformNode { where for<'a> F: FnOnce(Node<'a>, &TreeState, &Context) -> Result, { - self.with_tree_state_and_context(|state, context| { - let node = self.node(state)?; - f(node, state, context) + self.with_tree_and_context(|tree, context| { + let node = self.node(tree)?; + f(node, tree.state(), context) }) } @@ -835,8 +853,8 @@ impl PlatformNode { where for<'a> F: FnOnce(Node<'a>, &Context) -> Result, { - self.with_tree_state_and_context(|state, context| { - let node = self.node(state)?; + self.with_tree_and_context(|tree, context| { + let node = self.node(tree)?; if node.supports_text_ranges() { f(node, context) } else { @@ -854,16 +872,15 @@ impl PlatformNode { fn do_complex_action(&self, f: F) -> Result<()> where - for<'a> F: FnOnce(Node<'a>) -> Result>, + for<'a> F: FnOnce(Node<'a>, LocalNodeId) -> Result>, { let context = self.upgrade_context()?; if context.is_placeholder.load(Ordering::SeqCst) { return Err(element_not_enabled()); } let tree = context.read_tree(); - let state = tree.state(); - let node = self.node(state)?; - if let Some(request) = f(node)? { + let (node, local_id, _) = self.node_with_location(&tree)?; + if let Some(request) = f(node, local_id)? { drop(tree); context.do_action(request); } @@ -874,13 +891,13 @@ impl PlatformNode { where F: FnOnce() -> (Action, Option), { - self.do_complex_action(|node| { + self.do_complex_action(|node, target| { if node.is_disabled() { return Err(element_not_enabled()); } let (action, data) = f(); Ok(Some(ActionRequest { - target: node.id(), + target, action, data, })) @@ -892,7 +909,7 @@ impl PlatformNode { } fn set_selected(&self, selected: bool) -> Result<()> { - self.do_complex_action(|node| { + self.do_complex_action(|node, target| { if node.is_disabled() { return Err(element_not_enabled()); } @@ -902,7 +919,7 @@ impl PlatformNode { } Ok(Some(ActionRequest { action: Action::Click, - target: node.id(), + target, data: None, })) }) @@ -1266,9 +1283,9 @@ patterns! { )), (UIA_ScrollItemPatternId, IScrollItemProvider, IScrollItemProvider_Impl, is_scroll_item_pattern_supported, (), ( fn ScrollIntoView(&self) -> Result<()> { - self.do_complex_action(|node| { + self.do_complex_action(|_node, target| { Ok(Some(ActionRequest { - target: node.id(), + target, action: Action::ScrollIntoView, data: None, })) diff --git a/platforms/windows/src/text.rs b/platforms/windows/src/text.rs index 39404936..4fc2cd6b 100644 --- a/platforms/windows/src/text.rs +++ b/platforms/windows/src/text.rs @@ -7,7 +7,7 @@ use accesskit::{Action, ActionData, ActionRequest, ScrollHint}; use accesskit_consumer::{ - Node, TextPosition as Position, TextRange as Range, TreeState, WeakTextRange as WeakRange, + Node, TextPosition as Position, TextRange as Range, Tree, TreeState, WeakTextRange as WeakRange, }; use std::sync::{Arc, RwLock, Weak}; use windows::{ @@ -291,12 +291,12 @@ impl PlatformRange { fn do_action(&self, f: F) -> Result<()> where - for<'a> F: FnOnce(Range<'a>) -> ActionRequest, + for<'a> F: FnOnce(Range<'a>, &Tree) -> ActionRequest, { let context = self.upgrade_context()?; let tree = context.read_tree(); let range = self.upgrade_for_read(tree.state())?; - let request = f(range); + let request = f(range, &tree); drop(tree); context.do_action(request); Ok(()) @@ -557,10 +557,13 @@ impl ITextRangeProvider_Impl for PlatformRange_Impl { } fn Select(&self) -> Result<()> { - self.do_action(|range| ActionRequest { - action: Action::SetTextSelection, - target: range.node().id(), - data: Some(ActionData::SetTextSelection(range.to_text_selection())), + self.do_action(|range, tree| { + let (target, _) = tree.locate_node(range.node().id()).unwrap(); + ActionRequest { + action: Action::SetTextSelection, + target, + data: Some(ActionData::SetTextSelection(range.to_text_selection())), + } }) } @@ -575,15 +578,16 @@ impl ITextRangeProvider_Impl for PlatformRange_Impl { } fn ScrollIntoView(&self, align_to_top: BOOL) -> Result<()> { - self.do_action(|range| { + self.do_action(|range, tree| { let position = if align_to_top.into() { range.start() } else { range.end() }; + let (target, _) = tree.locate_node(position.inner_node().id()).unwrap(); ActionRequest { - target: position.inner_node().id(), action: Action::ScrollIntoView, + target, data: Some(ActionData::ScrollHint(if align_to_top.into() { ScrollHint::TopEdge } else { From 73e7f554f44239610c1e88011e9c9b667fa708e1 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 30 Nov 2025 20:48:06 +0100 Subject: [PATCH 6/9] Update ActionRequest with target_tree and target_node fields --- common/src/lib.rs | 3 +- platforms/android/src/adapter.rs | 14 ++-- platforms/atspi-common/src/node.rs | 77 +++++++++++++--------- platforms/macos/src/node.rs | 55 +++++++++------- platforms/windows/examples/hello_world.rs | 4 +- platforms/windows/src/node.rs | 21 +++--- platforms/windows/src/text.rs | 10 +-- platforms/winit/examples/mixed_handlers.rs | 12 ++-- platforms/winit/examples/simple.rs | 12 ++-- 9 files changed, 127 insertions(+), 81 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index 565a69de..93817d6b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -2732,7 +2732,8 @@ pub enum ActionData { #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct ActionRequest { pub action: Action, - pub target: NodeId, + pub target_tree: TreeId, + pub target_node: NodeId, pub data: Option, } diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index c12c6a03..a39d56fa 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -384,7 +384,7 @@ impl Adapter { } else { self.node_id_map.get_accesskit_id(virtual_view_id)? }; - let (target_node, _) = tree.locate_node(target)?; + let (target_node, target_tree) = tree.locate_node(target)?; let mut events = Vec::new(); let request = match action { ACTION_CLICK => ActionRequest { @@ -399,12 +399,14 @@ impl Adapter { Action::Click } }, - target: target_node, + target_tree, + target_node, data: None, }, ACTION_FOCUS => ActionRequest { action: Action::Focus, - target: target_node, + target_tree, + target_node, data: None, }, ACTION_SCROLL_BACKWARD | ACTION_SCROLL_FORWARD => ActionRequest { @@ -439,7 +441,8 @@ impl Adapter { Action::ScrollRight } }, - target: target_node, + target_tree, + target_node, data: Some(ActionData::ScrollUnit(ScrollUnit::Page)), }, ACTION_ACCESSIBILITY_FOCUS => { @@ -517,8 +520,9 @@ impl Adapter { }; update_tree(events, &mut self.node_id_map, tree, update); let request = ActionRequest { - target: node_id, action: Action::SetTextSelection, + target_tree: tree_id, + target_node: node_id, data: Some(ActionData::SetTextSelection(selection)), }; action_handler.do_action(request); diff --git a/platforms/atspi-common/src/node.rs b/platforms/atspi-common/src/node.rs index 0eacbf78..ad59a618 100644 --- a/platforms/atspi-common/src/node.rs +++ b/platforms/atspi-common/src/node.rs @@ -10,7 +10,7 @@ use accesskit::{ Action, ActionData, ActionRequest, Affine, Live, NodeId as LocalNodeId, Orientation, Point, - Rect, Role, Toggled, + Rect, Role, Toggled, TreeId, }; use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeState}; use atspi_common::{ @@ -735,12 +735,12 @@ impl PlatformNode { fn do_action_internal(&self, target: NodeId, f: F) -> Result<()> where - F: FnOnce(&TreeState, &Context, LocalNodeId) -> ActionRequest, + F: FnOnce(&TreeState, &Context, LocalNodeId, TreeId) -> ActionRequest, { let context = self.upgrade_context()?; let tree = context.read_tree(); - if let Some((target_node, _)) = tree.locate_node(target) { - let request = f(tree.state(), &context, target_node); + if let Some((target_node, target_tree)) = tree.locate_node(target) { + let request = f(tree.state(), &context, target_node, target_tree); drop(tree); context.do_action(request); Ok(()) @@ -970,9 +970,10 @@ impl PlatformNode { if index != 0 { return Ok(false); } - self.do_action_internal(self.id, |_, _, target| ActionRequest { + self.do_action_internal(self.id, |_, _, target_node, target_tree| ActionRequest { action: Action::Click, - target, + target_tree, + target_node, data: None, })?; Ok(true) @@ -1030,18 +1031,20 @@ impl PlatformNode { } pub fn grab_focus(&self) -> Result { - self.do_action_internal(self.id, |_, _, target| ActionRequest { + self.do_action_internal(self.id, |_, _, target_node, target_tree| ActionRequest { action: Action::Focus, - target, + target_tree, + target_node, data: None, })?; Ok(true) } pub fn scroll_to(&self, scroll_type: ScrollType) -> Result { - self.do_action_internal(self.id, |_, _, target| ActionRequest { + self.do_action_internal(self.id, |_, _, target_node, target_tree| ActionRequest { action: Action::ScrollIntoView, - target, + target_tree, + target_node, data: atspi_scroll_type_to_scroll_hint(scroll_type).map(ActionData::ScrollHint), })?; Ok(true) @@ -1055,10 +1058,11 @@ impl PlatformNode { node.filtered_parent(&filter), coord_type, ); - let (target, _) = tree.locate_node(self.id).ok_or(Error::Defunct)?; + let (target_node, target_tree) = tree.locate_node(self.id).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::ScrollToPoint, - target, + target_tree, + target_node, data: Some(ActionData::ScrollToPoint(point)), }); Ok(()) @@ -1092,10 +1096,12 @@ impl PlatformNode { if let Some(true) = child.is_selected() { Ok(true) } else if child.is_selectable() && child.is_clickable(&filter) { - let (target, _) = tree.locate_node(child.id()).ok_or(Error::Defunct)?; + let (target_node, target_tree) = + tree.locate_node(child.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::Click, - target, + target_tree, + target_node, data: None, }); Ok(true) @@ -1116,10 +1122,12 @@ impl PlatformNode { .nth(selected_child_index) { if child.is_clickable(&filter) { - let (target, _) = tree.locate_node(child.id()).ok_or(Error::Defunct)?; + let (target_node, target_tree) = + tree.locate_node(child.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::Click, - target, + target_tree, + target_node, data: None, }); Ok(true) @@ -1157,10 +1165,12 @@ impl PlatformNode { if let Some(false) = child.is_selected() { Ok(true) } else if child.is_selectable() && child.is_clickable(&filter) { - let (target, _) = tree.locate_node(child.id()).ok_or(Error::Defunct)?; + let (target_node, target_tree) = + tree.locate_node(child.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::Click, - target, + target_tree, + target_node, data: None, }); Ok(true) @@ -1228,10 +1238,11 @@ impl PlatformNode { pub fn set_caret_offset(&self, offset: i32) -> Result { self.resolve_for_text_with_context(|node, tree, context| { let offset = text_position_from_offset(&node, offset).ok_or(Error::IndexOutOfRange)?; - let (target, _) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; + let (target_node, target_tree) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::SetTextSelection, - target, + target_tree, + target_node, data: Some(ActionData::SetTextSelection( offset.to_degenerate_range().to_text_selection(), )), @@ -1338,10 +1349,11 @@ impl PlatformNode { let selection_end = node .text_selection_focus() .unwrap_or_else(|| node.document_range().start()); - let (target, _) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; + let (target_node, target_tree) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::SetTextSelection, - target, + target_tree, + target_node, data: Some(ActionData::SetTextSelection( selection_end.to_degenerate_range().to_text_selection(), )), @@ -1363,10 +1375,11 @@ impl PlatformNode { self.resolve_for_text_with_context(|node, tree, context| { let range = text_range_from_offsets(&node, start_offset, end_offset) .ok_or(Error::IndexOutOfRange)?; - let (target, _) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; + let (target_node, target_tree) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::SetTextSelection, - target, + target_tree, + target_node, data: Some(ActionData::SetTextSelection(range.to_text_selection())), }); Ok(true) @@ -1422,12 +1435,13 @@ impl PlatformNode { } else { range.start() }; - let (target, _) = tree + let (target_node, target_tree) = tree .locate_node(position.inner_node().id()) .ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::ScrollIntoView, - target, + target_tree, + target_node, data: atspi_scroll_type_to_scroll_hint(scroll_type).map(ActionData::ScrollHint), }); Ok(true) @@ -1455,10 +1469,12 @@ impl PlatformNode { if let Some(rect) = text_range_bounds_from_offsets(&node, start_offset, end_offset) { let point = Point::new(target_point.x - rect.x0, target_point.y - rect.y0); - let (target, _) = tree.locate_node(node.id()).ok_or(Error::Defunct)?; + let (target_node, target_tree) = + tree.locate_node(node.id()).ok_or(Error::Defunct)?; context.do_action(ActionRequest { action: Action::ScrollToPoint, - target, + target_tree, + target_node, data: Some(ActionData::ScrollToPoint(point)), }); return Ok(true); @@ -1487,9 +1503,10 @@ impl PlatformNode { } pub fn set_current_value(&self, value: f64) -> Result<()> { - self.do_action_internal(self.id, |_, _, target| ActionRequest { + self.do_action_internal(self.id, |_, _, target_node, target_tree| ActionRequest { action: Action::SetValue, - target, + target_tree, + target_node, data: Some(ActionData::NumericValue(value)), }) } diff --git a/platforms/macos/src/node.rs b/platforms/macos/src/node.rs index 2931de2c..0502592f 100644 --- a/platforms/macos/src/node.rs +++ b/platforms/macos/src/node.rs @@ -553,20 +553,22 @@ declare_class!( fn set_value(&self, value: &NSObject) { if let Some(string) = downcast_ref::(value) { self.resolve_with_context(|node, tree, context| { - if let Some((target, _)) = tree.locate_node(node.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(node.id()) { context.do_action(ActionRequest { action: Action::SetValue, - target, + target_tree, + target_node, data: Some(ActionData::Value(string.to_string().into())), }); } }); } else if let Some(number) = downcast_ref::(value) { self.resolve_with_context(|node, tree, context| { - if let Some((target, _)) = tree.locate_node(node.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(node.id()) { context.do_action(ActionRequest { action: Action::SetValue, - target, + target_tree, + target_node, data: Some(ActionData::NumericValue(number.doubleValue())), }); } @@ -624,10 +626,11 @@ declare_class!( self.resolve_with_context(|node, tree, context| { if focused { if node.is_focusable(&filter) { - if let Some((target, _)) = tree.locate_node(node.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(node.id()) { context.do_action(ActionRequest { action: Action::Focus, - target, + target_tree, + target_node, data: None, }); } @@ -635,10 +638,11 @@ declare_class!( } else { let root = tree.state().root(); if root.is_focusable(&filter) { - if let Some((target, _)) = tree.locate_node(root.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(root.id()) { context.do_action(ActionRequest { action: Action::Focus, - target, + target_tree, + target_node, data: None, }); } @@ -652,10 +656,11 @@ declare_class!( self.resolve_with_context(|node, tree, context| { let clickable = node.is_clickable(&filter); if clickable { - if let Some((target, _)) = tree.locate_node(node.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(node.id()) { context.do_action(ActionRequest { action: Action::Click, - target, + target_tree, + target_node, data: None, }); } @@ -670,10 +675,11 @@ declare_class!( self.resolve_with_context(|node, tree, context| { let supports_increment = node.supports_increment(&filter); if supports_increment { - if let Some((target, _)) = tree.locate_node(node.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(node.id()) { context.do_action(ActionRequest { action: Action::Increment, - target, + target_tree, + target_node, data: None, }); } @@ -688,10 +694,11 @@ declare_class!( self.resolve_with_context(|node, tree, context| { let supports_decrement = node.supports_decrement(&filter); if supports_decrement { - if let Some((target, _)) = tree.locate_node(node.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(node.id()) { context.do_action(ActionRequest { action: Action::Decrement, - target, + target_tree, + target_node, data: None, }); } @@ -861,10 +868,11 @@ declare_class!( self.resolve_with_context(|node, tree, context| { if node.supports_text_ranges() { if let Some(range) = from_ns_range(node, range) { - if let Some((target, _)) = tree.locate_node(node.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(node.id()) { context.do_action(ActionRequest { action: Action::SetTextSelection, - target, + target_tree, + target_node, data: Some(ActionData::SetTextSelection(range.to_text_selection())), }); } @@ -903,10 +911,11 @@ declare_class!( if node.is_selected() == Some(selected) { return; } - if let Some((target, _)) = tree.locate_node(node.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(node.id()) { context.do_action(ActionRequest { action: Action::Click, - target, + target_tree, + target_node, data: None, }); } @@ -968,10 +977,11 @@ declare_class!( && wrapper.is_item_like() && node.is_selectable(); if selectable { - if let Some((target, _)) = tree.locate_node(node.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(node.id()) { context.do_action(ActionRequest { action: Action::Click, - target, + target_tree, + target_node, data: None, }); } @@ -1042,10 +1052,11 @@ declare_class!( fn perform_action(&self, action: &NSString) { self.resolve_with_context(|node, tree, context| { if action == ns_string!(SCROLL_TO_VISIBLE_ACTION) { - if let Some((target, _)) = tree.locate_node(node.id()) { + if let Some((target_node, target_tree)) = tree.locate_node(node.id()) { context.do_action(ActionRequest { action: Action::ScrollIntoView, - target, + target_tree, + target_node, data: None, }); } diff --git a/platforms/windows/examples/hello_world.rs b/platforms/windows/examples/hello_world.rs index 296a5e66..33a71a4f 100644 --- a/platforms/windows/examples/hello_world.rs +++ b/platforms/windows/examples/hello_world.rs @@ -204,7 +204,7 @@ impl ActionHandler for SimpleActionHandler { Some(self.window), SET_FOCUS_MSG, WPARAM(0), - LPARAM(request.target.0 as _), + LPARAM(request.target_node.0 as _), ) } .unwrap(); @@ -215,7 +215,7 @@ impl ActionHandler for SimpleActionHandler { Some(self.window), CLICK_MSG, WPARAM(0), - LPARAM(request.target.0 as _), + LPARAM(request.target_node.0 as _), ) } .unwrap(); diff --git a/platforms/windows/src/node.rs b/platforms/windows/src/node.rs index 3d6c5b6d..d0d1c7a3 100644 --- a/platforms/windows/src/node.rs +++ b/platforms/windows/src/node.rs @@ -872,15 +872,15 @@ impl PlatformNode { fn do_complex_action(&self, f: F) -> Result<()> where - for<'a> F: FnOnce(Node<'a>, LocalNodeId) -> Result>, + for<'a> F: FnOnce(Node<'a>, LocalNodeId, TreeId) -> Result>, { let context = self.upgrade_context()?; if context.is_placeholder.load(Ordering::SeqCst) { return Err(element_not_enabled()); } let tree = context.read_tree(); - let (node, local_id, _) = self.node_with_location(&tree)?; - if let Some(request) = f(node, local_id)? { + let (node, target_node, target_tree) = self.node_with_location(&tree)?; + if let Some(request) = f(node, target_node, target_tree)? { drop(tree); context.do_action(request); } @@ -891,14 +891,15 @@ impl PlatformNode { where F: FnOnce() -> (Action, Option), { - self.do_complex_action(|node, target| { + self.do_complex_action(|node, target_node, target_tree| { if node.is_disabled() { return Err(element_not_enabled()); } let (action, data) = f(); Ok(Some(ActionRequest { - target, action, + target_tree, + target_node, data, })) }) @@ -909,7 +910,7 @@ impl PlatformNode { } fn set_selected(&self, selected: bool) -> Result<()> { - self.do_complex_action(|node, target| { + self.do_complex_action(|node, target_node, target_tree| { if node.is_disabled() { return Err(element_not_enabled()); } @@ -919,7 +920,8 @@ impl PlatformNode { } Ok(Some(ActionRequest { action: Action::Click, - target, + target_tree, + target_node, data: None, })) }) @@ -1283,10 +1285,11 @@ patterns! { )), (UIA_ScrollItemPatternId, IScrollItemProvider, IScrollItemProvider_Impl, is_scroll_item_pattern_supported, (), ( fn ScrollIntoView(&self) -> Result<()> { - self.do_complex_action(|_node, target| { + self.do_complex_action(|_node, target_node, target_tree| { Ok(Some(ActionRequest { - target, action: Action::ScrollIntoView, + target_tree, + target_node, data: None, })) }) diff --git a/platforms/windows/src/text.rs b/platforms/windows/src/text.rs index 4fc2cd6b..2b2d0df4 100644 --- a/platforms/windows/src/text.rs +++ b/platforms/windows/src/text.rs @@ -558,10 +558,11 @@ impl ITextRangeProvider_Impl for PlatformRange_Impl { fn Select(&self) -> Result<()> { self.do_action(|range, tree| { - let (target, _) = tree.locate_node(range.node().id()).unwrap(); + let (target_node, target_tree) = tree.locate_node(range.node().id()).unwrap(); ActionRequest { action: Action::SetTextSelection, - target, + target_tree, + target_node, data: Some(ActionData::SetTextSelection(range.to_text_selection())), } }) @@ -584,10 +585,11 @@ impl ITextRangeProvider_Impl for PlatformRange_Impl { } else { range.end() }; - let (target, _) = tree.locate_node(position.inner_node().id()).unwrap(); + let (target_node, target_tree) = tree.locate_node(position.inner_node().id()).unwrap(); ActionRequest { action: Action::ScrollIntoView, - target, + target_tree, + target_node, data: Some(ActionData::ScrollHint(if align_to_top.into() { ScrollHint::TopEdge } else { diff --git a/platforms/winit/examples/mixed_handlers.rs b/platforms/winit/examples/mixed_handlers.rs index de3ebe96..8ed575bd 100644 --- a/platforms/winit/examples/mixed_handlers.rs +++ b/platforms/winit/examples/mixed_handlers.rs @@ -262,15 +262,19 @@ impl ApplicationHandler for Application { match user_event.window_event { AccessKitWindowEvent::InitialTreeRequested => unreachable!(), - AccessKitWindowEvent::ActionRequested(ActionRequest { action, target, .. }) => { - if target == BUTTON_1_ID || target == BUTTON_2_ID { + AccessKitWindowEvent::ActionRequested(ActionRequest { + action, + target_node, + .. + }) => { + if target_node == BUTTON_1_ID || target_node == BUTTON_2_ID { let mut state = state.lock().unwrap(); match action { Action::Focus => { - state.set_focus(adapter, target); + state.set_focus(adapter, target_node); } Action::Click => { - state.press_button(adapter, target); + state.press_button(adapter, target_node); } _ => (), } diff --git a/platforms/winit/examples/simple.rs b/platforms/winit/examples/simple.rs index 2badefc9..c390454b 100644 --- a/platforms/winit/examples/simple.rs +++ b/platforms/winit/examples/simple.rs @@ -238,14 +238,18 @@ impl ApplicationHandler for Application { AccessKitWindowEvent::InitialTreeRequested => { adapter.update_if_active(|| state.build_initial_tree()); } - AccessKitWindowEvent::ActionRequested(ActionRequest { action, target, .. }) => { - if target == BUTTON_1_ID || target == BUTTON_2_ID { + AccessKitWindowEvent::ActionRequested(ActionRequest { + action, + target_node, + .. + }) => { + if target_node == BUTTON_1_ID || target_node == BUTTON_2_ID { match action { Action::Focus => { - state.set_focus(adapter, target); + state.set_focus(adapter, target_node); } Action::Click => { - state.press_button(adapter, target); + state.press_button(adapter, target_node); } _ => (), } From 39193d83beb7487efb6b9f6b15d8a4000037fbe5 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 30 Nov 2025 21:04:53 +0100 Subject: [PATCH 7/9] Add ChildIds iterator for child node traversal --- consumer/src/iterators.rs | 51 +++++++++++++++++++++++++++++++++++++++ consumer/src/node.rs | 20 ++++++--------- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/consumer/src/iterators.rs b/consumer/src/iterators.rs index fb382bf6..317f88ed 100644 --- a/consumer/src/iterators.rs +++ b/consumer/src/iterators.rs @@ -18,6 +18,57 @@ use crate::{ tree::State as TreeState, }; +/// Iterator over child NodeIds. +pub enum ChildIds<'a> { + Normal { + parent_id: NodeId, + children: core::slice::Iter<'a, LocalNodeId>, + }, +} + +impl Iterator for ChildIds<'_> { + type Item = NodeId; + + fn next(&mut self) -> Option { + match self { + Self::Normal { + parent_id, + children, + } => children + .next() + .map(|child| parent_id.with_same_tree(*child)), + } + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } +} + +impl DoubleEndedIterator for ChildIds<'_> { + fn next_back(&mut self) -> Option { + match self { + Self::Normal { + parent_id, + children, + } => children + .next_back() + .map(|child| parent_id.with_same_tree(*child)), + } + } +} + +impl ExactSizeIterator for ChildIds<'_> { + fn len(&self) -> usize { + match self { + Self::Normal { children, .. } => children.len(), + } + } +} + +impl FusedIterator for ChildIds<'_> {} + /// An iterator that yields following siblings of a node. /// /// This struct is created by the [`following_siblings`](Node::following_siblings) method on [`Node`]. diff --git a/consumer/src/node.rs b/consumer/src/node.rs index d01a94d2..78679ca0 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -20,7 +20,7 @@ use core::{fmt, iter::FusedIterator}; use crate::filters::FilterResult; use crate::iterators::{ - FilteredChildren, FollowingFilteredSiblings, FollowingSiblings, LabelledBy, + ChildIds, FilteredChildren, FollowingFilteredSiblings, FollowingSiblings, LabelledBy, PrecedingFilteredSiblings, PrecedingSiblings, }; use crate::tree::{State as TreeState, TreeIndex}; @@ -125,12 +125,11 @@ impl<'a> Node<'a> { ) -> impl DoubleEndedIterator + ExactSizeIterator + FusedIterator - + '_ { - let id = self.id; - let data = &self.state.data; - data.children() - .iter() - .map(move |child_id| id.with_same_tree(*child_id)) + + 'a { + ChildIds::Normal { + parent_id: self.id, + children: self.state.data.children().iter(), + } } pub fn children( @@ -140,11 +139,8 @@ impl<'a> Node<'a> { + FusedIterator> + 'a { let state = self.tree_state; - let id = self.id; - let data = &self.state.data; - data.children() - .iter() - .map(move |child_id| state.node_by_id(id.with_same_tree(*child_id)).unwrap()) + self.child_ids() + .map(move |id| state.node_by_id(id).unwrap()) } pub fn filtered_children( From 15251510dea4f1ace3ac656723cde2d496d4128a Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 30 Nov 2025 21:43:28 +0100 Subject: [PATCH 8/9] Add SubtreeState type for tracking subtree root and focus --- consumer/src/tree.rs | 144 ++++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 36 deletions(-) diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 6ceada75..24bcf23c 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -14,10 +14,6 @@ use crate::node::{Node, NodeId, NodeState, ParentAndIndex}; #[repr(transparent)] pub(crate) struct TreeIndex(pub(crate) u32); -fn nid(id: LocalNodeId) -> NodeId { - NodeId::new(id, TreeIndex(0)) -} - #[derive(Debug, Default)] struct TreeIndexMap { id_to_index: HashMap, @@ -40,12 +36,22 @@ impl TreeIndexMap { } } +/// State for a subtree, including its root node and current focus. +#[derive(Clone, Debug)] +pub(crate) struct SubtreeState { + pub(crate) root: NodeId, + pub(crate) focus: NodeId, +} + #[derive(Clone, Debug)] pub struct State { pub(crate) nodes: HashMap, pub(crate) data: TreeData, + pub(crate) root: NodeId, pub(crate) focus: NodeId, is_host_focused: bool, + /// Maps TreeId to the state of each subtree (root and focus). + pub(crate) subtrees: HashMap, } #[derive(Default)] @@ -57,7 +63,7 @@ struct InternalChanges { impl State { fn validate_global(&self) { - if !self.nodes.contains_key(&nid(self.data.root)) { + if !self.nodes.contains_key(&self.root) { panic!("Root ID {:?} is not in the node list", self.data.root); } if !self.nodes.contains_key(&self.focus) { @@ -73,18 +79,39 @@ impl State { update: TreeUpdate, is_host_focused: bool, mut changes: Option<&mut InternalChanges>, + tree_index: TreeIndex, ) { - let mut unreachable = HashSet::new(); - let mut seen_child_ids = HashSet::new(); + let map_id = |id: LocalNodeId| NodeId::new(id, tree_index); + + let mut unreachable: HashSet = HashSet::new(); + let mut seen_child_ids: HashSet = HashSet::new(); - if let Some(tree) = update.tree { - if tree.root != self.data.root { - unreachable.insert(nid(self.data.root)); + let tree_id = update.tree_id; + + let new_tree_root = if let Some(tree) = update.tree { + let new_root = map_id(tree.root); + if tree_id == TreeId::ROOT { + // Only update main tree root/data for ROOT tree + if tree.root != self.data.root { + unreachable.insert(self.root); + } + self.root = new_root; + self.data = tree; } - self.data = tree; - } + Some(new_root) + } else { + None + }; - let root = self.data.root; + // Use the tree's root from the update, or fallback to existing subtree/main tree root + let root = new_tree_root + .map(|r| r.to_components().0) + .unwrap_or_else(|| { + self.subtrees + .get(&tree_id) + .map(|s| s.root.to_components().0) + .unwrap_or(self.data.root) + }); let mut pending_nodes: HashMap = HashMap::new(); let mut pending_children = HashMap::new(); @@ -106,33 +133,34 @@ impl State { } for (local_node_id, node_data) in update.nodes { - let node_id = nid(local_node_id); + let node_id = map_id(local_node_id); unreachable.remove(&node_id); for (child_index, child_id) in node_data.children().iter().enumerate() { - if seen_child_ids.contains(child_id) { + let mapped_child_id = map_id(*child_id); + if seen_child_ids.contains(&mapped_child_id) { panic!("TreeUpdate includes duplicate child {:?}", child_id); } - seen_child_ids.insert(*child_id); - unreachable.remove(&nid(*child_id)); + seen_child_ids.insert(mapped_child_id); + unreachable.remove(&mapped_child_id); let parent_and_index = ParentAndIndex(node_id, child_index); - if let Some(child_state) = self.nodes.get_mut(&nid(*child_id)) { + if let Some(child_state) = self.nodes.get_mut(&mapped_child_id) { if child_state.parent_and_index != Some(parent_and_index) { child_state.parent_and_index = Some(parent_and_index); if let Some(changes) = &mut changes { - changes.updated_node_ids.insert(nid(*child_id)); + changes.updated_node_ids.insert(mapped_child_id); } } - } else if let Some(child_data) = pending_nodes.remove(&nid(*child_id)) { + } else if let Some(child_data) = pending_nodes.remove(&mapped_child_id) { add_node( &mut self.nodes, &mut changes, Some(parent_and_index), - nid(*child_id), + mapped_child_id, child_data, ); } else { - pending_children.insert(nid(*child_id), parent_and_index); + pending_children.insert(mapped_child_id, parent_and_index); } } @@ -141,8 +169,9 @@ impl State { node_state.parent_and_index = None; } for child_id in node_state.data.children().iter() { - if !seen_child_ids.contains(child_id) { - unreachable.insert(nid(*child_id)); + let mapped_existing_child_id = map_id(*child_id); + if !seen_child_ids.contains(&mapped_existing_child_id) { + unreachable.insert(mapped_existing_child_id); } } if node_state.data != node_data { @@ -181,29 +210,63 @@ impl State { ); } - self.focus = nid(update.focus); + // Store subtree state (root and focus) per tree + let tree_focus = map_id(update.focus); + if let Some(new_root) = new_tree_root { + // New tree: insert both root and focus + self.subtrees.insert( + tree_id, + SubtreeState { + root: new_root, + focus: tree_focus, + }, + ); + } else if let Some(subtree) = self.subtrees.get_mut(&tree_id) { + // Existing tree: just update focus + subtree.focus = tree_focus; + } else if tree_id == TreeId::ROOT { + // ROOT tree focus update without tree change (e.g., during Tree::new after take()) + // Use the main tree's root for the subtree state + self.subtrees.insert( + tree_id, + SubtreeState { + root: self.root, + focus: tree_focus, + }, + ); + } + + self.focus = tree_focus; self.is_host_focused = is_host_focused; if !unreachable.is_empty() { fn traverse_unreachable( nodes: &mut HashMap, changes: &mut Option<&mut InternalChanges>, - seen_child_ids: &HashSet, + seen_child_ids: &HashSet, id: NodeId, + map_id: impl Fn(LocalNodeId) -> NodeId + Copy, ) { if let Some(changes) = changes { changes.removed_node_ids.insert(id); } let node = nodes.remove(&id).unwrap(); for child_id in node.data.children().iter() { - if !seen_child_ids.contains(child_id) { - traverse_unreachable(nodes, changes, seen_child_ids, nid(*child_id)); + let mapped_child_id = map_id(*child_id); + if !seen_child_ids.contains(&mapped_child_id) { + traverse_unreachable( + nodes, + changes, + seen_child_ids, + mapped_child_id, + map_id, + ); } } } for id in unreachable { - traverse_unreachable(&mut self.nodes, &mut changes, &seen_child_ids, id); + traverse_unreachable(&mut self.nodes, &mut changes, &seen_child_ids, id, map_id); } } @@ -222,7 +285,7 @@ impl State { tree_id: TreeId::ROOT, focus, }; - self.update(update, is_host_focused, changes); + self.update(update, is_host_focused, changes, TreeIndex(0)); } pub fn has_node(&self, id: NodeId) -> bool { @@ -238,7 +301,7 @@ impl State { } pub fn root_id(&self) -> NodeId { - nid(self.data.root) + self.root } pub fn root(&self) -> Node<'_> { @@ -310,14 +373,16 @@ impl Tree { panic!("Cannot initialize with a subtree. TreeUpdate::tree_id must be TreeId::ROOT."); } let mut tree_index_map = TreeIndexMap::default(); - tree_index_map.get_index(initial_state.tree_id); + let tree_index = tree_index_map.get_index(initial_state.tree_id); let mut state = State { nodes: HashMap::new(), + root: NodeId::new(tree.root, tree_index), data: tree, - focus: nid(initial_state.focus), + focus: NodeId::new(initial_state.focus, tree_index), is_host_focused, + subtrees: HashMap::new(), }; - state.update(initial_state, is_host_focused, None); + state.update(initial_state, is_host_focused, None, tree_index); Self { next_state: state.clone(), state, @@ -330,9 +395,14 @@ impl Tree { update: TreeUpdate, handler: &mut impl ChangeHandler, ) { + let tree_index = self.tree_index_map.get_index(update.tree_id); let mut changes = InternalChanges::default(); - self.next_state - .update(update, self.state.is_host_focused, Some(&mut changes)); + self.next_state.update( + update, + self.state.is_host_focused, + Some(&mut changes), + tree_index, + ); self.process_changes(changes, handler); } @@ -403,8 +473,10 @@ impl Tree { if self.state.data != self.next_state.data { self.state.data.clone_from(&self.next_state.data); } + self.state.root = self.next_state.root; self.state.focus = self.next_state.focus; self.state.is_host_focused = self.next_state.is_host_focused; + self.state.subtrees.clone_from(&self.next_state.subtrees); } pub fn state(&self) -> &State { From 783c1e8fea164f337a0cd62739fb6e59f159d77c Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 30 Nov 2025 22:51:30 +0100 Subject: [PATCH 9/9] Add Node::tree_id property for graft support --- common/src/lib.rs | 71 +- consumer/src/filters.rs | 31 + consumer/src/iterators.rs | 142 ++- consumer/src/node.rs | 23 +- consumer/src/tree.rs | 1822 ++++++++++++++++++++++++++++++++++++- 5 files changed, 2041 insertions(+), 48 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index 93817d6b..974227a0 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -628,6 +628,11 @@ pub enum TextDecoration { pub type NodeIdContent = u64; /// The stable identity of a [`Node`], unique within the node's tree. +/// +/// Each tree (root or subtree) has its own independent ID space. The same +/// `NodeId` value can exist in different trees without conflict. When working +/// with multiple trees, the combination of `NodeId` and [`TreeId`] uniquely +/// identifies a node. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "schemars", derive(JsonSchema))] @@ -778,6 +783,7 @@ enum PropertyValue { Rect(Rect), TextSelection(Box), CustomActionVec(Vec), + TreeId(TreeId), } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -894,6 +900,7 @@ enum PropertyId { Bounds, TextSelection, CustomActions, + TreeId, // This MUST be last. Unset, @@ -1695,7 +1702,8 @@ copy_type_getters! { (get_usize_property, usize, Usize), (get_color_property, u32, Color), (get_text_decoration_property, TextDecoration, TextDecoration), - (get_bool_property, bool, Bool) + (get_bool_property, bool, Bool), + (get_tree_id_property, TreeId, TreeId) } box_type_setters! { @@ -1713,7 +1721,8 @@ copy_type_setters! { (set_usize_property, usize, Usize), (set_color_property, u32, Color), (set_text_decoration_property, TextDecoration, TextDecoration), - (set_bool_property, bool, Bool) + (set_bool_property, bool, Bool), + (set_tree_id_property, TreeId, TreeId) } vec_type_methods! { @@ -2014,11 +2023,20 @@ property_methods! { /// [`transform`]: Node::transform (Bounds, bounds, get_rect_property, Option, set_bounds, set_rect_property, Rect, clear_bounds), - (TextSelection, text_selection, get_text_selection_property, Option<&TextSelection>, set_text_selection, set_text_selection_property, impl Into>, clear_text_selection) + (TextSelection, text_selection, get_text_selection_property, Option<&TextSelection>, set_text_selection, set_text_selection_property, impl Into>, clear_text_selection), + + /// The tree that this node grafts. When set, this node acts as a graft + /// point, and its child is the root of the specified subtree. + /// + /// A graft node must be created before its subtree is pushed. + /// + /// Removing a graft node or clearing this property removes its subtree, + /// unless a new graft node is provided in the same update. + (TreeId, tree_id, get_tree_id_property, Option, set_tree_id, set_tree_id_property, TreeId, clear_tree_id) } impl Node { - option_properties_debug_method! { debug_option_properties, [transform, bounds, text_selection,] } + option_properties_debug_method! { debug_option_properties, [transform, bounds, text_selection, tree_id,] } } #[cfg(test)] @@ -2123,6 +2141,31 @@ mod text_selection { } } +#[cfg(test)] +mod tree_id { + use super::{Node, Role, TreeId, Uuid}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::GenericContainer); + assert!(node.tree_id().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::GenericContainer); + let value = TreeId(Uuid::nil()); + node.set_tree_id(value); + assert_eq!(node.tree_id(), Some(value)); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(TreeId(Uuid::nil())); + node.clear_tree_id(); + assert!(node.tree_id().is_none()); + } +} + vec_property_methods! { (CustomActions, CustomAction, custom_actions, get_custom_action_vec, set_custom_actions, set_custom_action_vec, push_custom_action, push_to_custom_action_vec, clear_custom_actions) } @@ -2291,7 +2334,8 @@ impl Serialize for Properties { Affine, Rect, TextSelection, - CustomActionVec + CustomActionVec, + TreeId }); } map.end() @@ -2421,7 +2465,8 @@ impl<'de> Visitor<'de> for PropertiesVisitor { Affine { Transform }, Rect { Bounds }, TextSelection { TextSelection }, - CustomActionVec { CustomActions } + CustomActionVec { CustomActions }, + TreeId { TreeId } }); } @@ -2651,7 +2696,15 @@ pub struct TreeUpdate { /// a tree. pub tree: Option, - /// The identifier of the tree. + /// The identifier of the tree that this update applies to. + /// + /// Use [`TreeId::ROOT`] for the main/root tree. For subtrees, use a unique + /// [`TreeId`] that identifies the subtree. + /// + /// When updating a subtree (non-ROOT tree_id): + /// - A graft node with [`Node::tree_id`] set to this tree's ID must already + /// exist in the parent tree before the first subtree update. + /// - The first update for a subtree must include [`tree`](Self::tree) data. pub tree_id: TreeId, /// The node within this tree that has keyboard focus when the native @@ -2659,6 +2712,10 @@ pub struct TreeUpdate { /// has keyboard focus, this must be set to the root. The latest focus state /// must be provided with every tree update, even if the focus state /// didn't change in a given update. + /// + /// For subtrees, this specifies which node has focus when the subtree + /// itself is focused (i.e., when focus is on the graft node in the parent + /// tree). pub focus: NodeId, } diff --git a/consumer/src/filters.rs b/consumer/src/filters.rs index 1401169a..8905bef3 100644 --- a/consumer/src/filters.rs +++ b/consumer/src/filters.rs @@ -23,6 +23,11 @@ fn common_filter_base(node: &Node) -> Option { return Some(FilterResult::ExcludeSubtree); } + // Graft nodes are transparent containers pointing to a subtree + if node.is_graft() { + return Some(FilterResult::ExcludeNode); + } + let role = node.role(); if role == Role::GenericContainer || role == Role::TextRun { return Some(FilterResult::ExcludeNode); @@ -387,4 +392,30 @@ mod tests { assert_filter_result(ExcludeSubtree, &tree, NodeId(10)); assert_filter_result(ExcludeSubtree, &tree, NodeId(11)); } + + #[test] + fn graft_node() { + use accesskit::Uuid; + + let subtree_id = TreeId(Uuid::from_u128(1)); + let update = TreeUpdate { + nodes: vec![ + (NodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![NodeId(1)]); + node + }), + (NodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id); + node + }), + ], + tree: Some(Tree::new(NodeId(0))), + tree_id: TreeId::ROOT, + focus: NodeId(0), + }; + let tree = crate::Tree::new(update, false); + assert_filter_result(ExcludeNode, &tree, NodeId(1)); + } } diff --git a/consumer/src/iterators.rs b/consumer/src/iterators.rs index 317f88ed..f9783f13 100644 --- a/consumer/src/iterators.rs +++ b/consumer/src/iterators.rs @@ -18,12 +18,13 @@ use crate::{ tree::State as TreeState, }; -/// Iterator over child NodeIds. +/// Iterator over child NodeIds, handling both normal nodes and graft nodes. pub enum ChildIds<'a> { Normal { parent_id: NodeId, children: core::slice::Iter<'a, LocalNodeId>, }, + Graft(Option), } impl Iterator for ChildIds<'_> { @@ -37,6 +38,7 @@ impl Iterator for ChildIds<'_> { } => children .next() .map(|child| parent_id.with_same_tree(*child)), + Self::Graft(id) => id.take(), } } @@ -55,6 +57,7 @@ impl DoubleEndedIterator for ChildIds<'_> { } => children .next_back() .map(|child| parent_id.with_same_tree(*child)), + Self::Graft(id) => id.take(), } } } @@ -63,6 +66,7 @@ impl ExactSizeIterator for ChildIds<'_> { fn len(&self) -> usize { match self { Self::Normal { children, .. } => children.len(), + Self::Graft(id) => usize::from(id.is_some()), } } } @@ -85,13 +89,18 @@ impl<'a> FollowingSiblings<'a> { let parent_and_index = node.parent_and_index(); let (back_position, front_position, done) = if let Some((ref parent, index)) = parent_and_index { - let back_position = parent.data().children().len() - 1; - let front_position = index + 1; - ( - back_position, - front_position, - front_position > back_position, - ) + // Graft nodes have only one child (the subtree root) + if parent.is_graft() { + (0, 0, true) + } else { + let back_position = parent.data().children().len() - 1; + let front_position = index + 1; + ( + back_position, + front_position, + front_position > back_position, + ) + } } else { (0, 0, true) }; @@ -169,12 +178,18 @@ pub struct PrecedingSiblings<'a> { impl<'a> PrecedingSiblings<'a> { pub(crate) fn new(node: Node<'a>) -> Self { let parent_and_index = node.parent_and_index(); - let (back_position, front_position, done) = if let Some((_, index)) = parent_and_index { - let front_position = index.saturating_sub(1); - (0, front_position, index == 0) - } else { - (0, 0, true) - }; + let (back_position, front_position, done) = + if let Some((ref parent, index)) = parent_and_index { + // Graft nodes have only one child (the subtree root) + if parent.is_graft() { + (0, 0, true) + } else { + let front_position = index.saturating_sub(1); + (0, front_position, index == 0) + } + } else { + (0, 0, true) + }; Self { back_position, done, @@ -567,9 +582,14 @@ impl FilterResult> FusedIterator for LabelledBy<'_, Filter> #[cfg(test)] mod tests { - use crate::tests::*; - use accesskit::NodeId as LocalNodeId; - use alloc::vec::Vec; + use crate::{ + filters::common_filter, + tests::*, + tree::{ChangeHandler, TreeIndex}, + NodeId, + }; + use accesskit::{Node, NodeId as LocalNodeId, Role, Tree, TreeId, TreeUpdate, Uuid}; + use alloc::{vec, vec::Vec}; #[test] fn following_siblings() { @@ -927,4 +947,92 @@ mod tests { .next_back() .is_none()); } + + #[test] + fn graft_node_without_subtree_has_no_filtered_children() { + let subtree_id = TreeId(Uuid::from_u128(1)); + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let tree = crate::Tree::new(update, false); + + let graft_node_id = NodeId::new(LocalNodeId(1), TreeIndex(0)); + let graft_node = tree.state().node_by_id(graft_node_id).unwrap(); + assert!(graft_node.filtered_children(common_filter).next().is_none()); + } + + #[test] + fn filtered_children_crosses_subtree_boundary() { + struct NoOpHandler; + impl ChangeHandler for NoOpHandler { + fn node_added(&mut self, _: &crate::Node) {} + fn node_updated(&mut self, _: &crate::Node, _: &crate::Node) {} + fn focus_moved(&mut self, _: Option<&crate::Node>, _: Option<&crate::Node>) {} + fn node_removed(&mut self, _: &crate::Node) {} + } + + let subtree_id = TreeId(Uuid::from_u128(1)); + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = crate::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id, + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let root = tree.state().root(); + let filtered_children: Vec<_> = root.filtered_children(common_filter).collect(); + + assert_eq!(1, filtered_children.len()); + let subtree_root_id = NodeId::new(LocalNodeId(0), TreeIndex(1)); + assert_eq!(subtree_root_id, filtered_children[0].id()); + + let document = &filtered_children[0]; + let doc_children: Vec<_> = document.filtered_children(common_filter).collect(); + assert_eq!(1, doc_children.len()); + let button_id = NodeId::new(LocalNodeId(1), TreeIndex(1)); + assert_eq!(button_id, doc_children[0].id()); + } } diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 78679ca0..339876b2 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -89,6 +89,11 @@ impl<'a> Node<'a> { self.id() == self.tree_state.root_id() } + /// Returns true if this node is a graft node (has a tree_id property set). + pub fn is_graft(&self) -> bool { + self.state.data.tree_id().is_some() + } + pub fn parent_id(&self) -> Option { self.state .parent_and_index @@ -120,15 +125,27 @@ impl<'a> Node<'a> { }) } + /// Returns the single child of a graft node (the subtree root), if available. + fn graft_child_id(&self) -> Option { + self.state + .data + .tree_id() + .and_then(|tree_id| self.tree_state.subtree_root(tree_id)) + } + pub fn child_ids( &self, ) -> impl DoubleEndedIterator + ExactSizeIterator + FusedIterator + 'a { - ChildIds::Normal { - parent_id: self.id, - children: self.state.data.children().iter(), + if self.is_graft() { + ChildIds::Graft(self.graft_child_id()) + } else { + ChildIds::Normal { + parent_id: self.id, + children: self.state.data.children().iter(), + } } } diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 24bcf23c..91d3fab6 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -4,7 +4,7 @@ // the LICENSE-MIT file), at your option. use accesskit::{Node as NodeData, NodeId as LocalNodeId, Tree as TreeData, TreeId, TreeUpdate}; -use alloc::vec; +use alloc::{vec, vec::Vec}; use core::fmt; use hashbrown::{HashMap, HashSet}; @@ -50,8 +50,8 @@ pub struct State { pub(crate) root: NodeId, pub(crate) focus: NodeId, is_host_focused: bool, - /// Maps TreeId to the state of each subtree (root and focus). pub(crate) subtrees: HashMap, + pub(crate) graft_parents: HashMap, } #[derive(Default)] @@ -74,6 +74,35 @@ impl State { } } + /// Computes the effective focus by following the graft chain from ROOT. + /// If ROOT's focus is on a graft node, follows through to that subtree's focus, + /// and continues recursively until reaching a non-graft node. + fn compute_effective_focus(&self) -> NodeId { + let Some(root_subtree) = self.subtrees.get(&TreeId::ROOT) else { + return self.focus; + }; + + let mut current_focus = root_subtree.focus; + loop { + let Some(node_state) = self.nodes.get(¤t_focus) else { + break; + }; + let Some(subtree_id) = node_state.data.tree_id() else { + break; + }; + let subtree = self.subtrees.get(&subtree_id).unwrap_or_else(|| { + panic!( + "Focus is on graft node {:?} but subtree {:?} does not exist. \ + Graft nodes cannot be focused without their subtree.", + current_focus.to_components().0, + subtree_id + ); + }); + current_focus = subtree.focus; + } + current_focus + } + fn update( &mut self, update: TreeUpdate, @@ -83,27 +112,46 @@ impl State { ) { let map_id = |id: LocalNodeId| NodeId::new(id, tree_index); - let mut unreachable: HashSet = HashSet::new(); - let mut seen_child_ids: HashSet = HashSet::new(); + let mut unreachable = HashSet::new(); + let mut seen_child_ids = HashSet::new(); let tree_id = update.tree_id; + if tree_id != TreeId::ROOT { + let subtree_exists = self.subtrees.contains_key(&tree_id); + if update.tree.is_some() && !self.graft_parents.contains_key(&tree_id) { + panic!( + "Cannot push subtree {:?}: no graft node exists for this tree. \ + Push the graft node (with tree_id property set) before pushing the subtree.", + tree_id + ); + } + if !subtree_exists && update.tree.is_none() { + panic!( + "Cannot update subtree {:?}: subtree does not exist. \ + The first update for a subtree must include tree data.", + tree_id + ); + } + } let new_tree_root = if let Some(tree) = update.tree { let new_root = map_id(tree.root); if tree_id == TreeId::ROOT { - // Only update main tree root/data for ROOT tree if tree.root != self.data.root { unreachable.insert(self.root); } self.root = new_root; self.data = tree; + } else if let Some(subtree) = self.subtrees.get(&tree_id) { + if subtree.root != new_root { + unreachable.insert(subtree.root); + } } Some(new_root) } else { None }; - // Use the tree's root from the update, or fallback to existing subtree/main tree root let root = new_tree_root .map(|r| r.to_components().0) .unwrap_or_else(|| { @@ -112,16 +160,49 @@ impl State { .map(|s| s.root.to_components().0) .unwrap_or(self.data.root) }); + let mut pending_nodes: HashMap = HashMap::new(); let mut pending_children = HashMap::new(); + let mut pending_grafts: HashMap = HashMap::new(); + let mut grafts_to_remove: HashSet = HashSet::new(); + + fn record_graft( + pending_grafts: &mut HashMap, + subtree_id: TreeId, + graft_node_id: NodeId, + ) { + if subtree_id == TreeId::ROOT { + panic!("Cannot graft the root tree"); + } + if let Some(existing_graft) = pending_grafts.get(&subtree_id) { + panic!( + "Subtree {:?} already has a graft parent {:?}, cannot assign to {:?}", + subtree_id, + existing_graft.to_components().0, + graft_node_id.to_components().0 + ); + } + pending_grafts.insert(subtree_id, graft_node_id); + } fn add_node( nodes: &mut HashMap, + pending_grafts: &mut HashMap, changes: &mut Option<&mut InternalChanges>, parent_and_index: Option, id: NodeId, data: NodeData, ) { + if let Some(subtree_id) = data.tree_id() { + if !data.children().is_empty() { + panic!( + "Node {:?} has both tree_id and children. \ + A graft node's only child comes from its subtree.", + id.to_components().0 + ); + } + record_graft(pending_grafts, subtree_id, id); + } let state = NodeState { parent_and_index, data, @@ -138,10 +219,9 @@ impl State { for (child_index, child_id) in node_data.children().iter().enumerate() { let mapped_child_id = map_id(*child_id); - if seen_child_ids.contains(&mapped_child_id) { + if !seen_child_ids.insert(mapped_child_id) { panic!("TreeUpdate includes duplicate child {:?}", child_id); } - seen_child_ids.insert(mapped_child_id); unreachable.remove(&mapped_child_id); let parent_and_index = ParentAndIndex(node_id, child_index); if let Some(child_state) = self.nodes.get_mut(&mapped_child_id) { @@ -154,6 +234,7 @@ impl State { } else if let Some(child_data) = pending_nodes.remove(&mapped_child_id) { add_node( &mut self.nodes, + &mut pending_grafts, &mut changes, Some(parent_and_index), mapped_child_id, @@ -175,6 +256,23 @@ impl State { } } if node_state.data != node_data { + if node_data.tree_id().is_some() && !node_data.children().is_empty() { + panic!( + "Node {:?} has both tree_id and children. \ + A graft node's only child comes from its subtree.", + node_id.to_components().0 + ); + } + let old_tree_id = node_state.data.tree_id(); + let new_tree_id = node_data.tree_id(); + if old_tree_id != new_tree_id { + if let Some(old_subtree_id) = old_tree_id { + grafts_to_remove.insert(old_subtree_id); + } + if let Some(new_subtree_id) = new_tree_id { + record_graft(&mut pending_grafts, new_subtree_id, node_id); + } + } node_state.data.clone_from(&node_data); if let Some(changes) = &mut changes { changes.updated_node_ids.insert(node_id); @@ -183,13 +281,21 @@ impl State { } else if let Some(parent_and_index) = pending_children.remove(&node_id) { add_node( &mut self.nodes, + &mut pending_grafts, &mut changes, Some(parent_and_index), node_id, node_data, ); } else if local_node_id == root { - add_node(&mut self.nodes, &mut changes, None, node_id, node_data); + add_node( + &mut self.nodes, + &mut pending_grafts, + &mut changes, + None, + node_id, + node_data, + ); } else { pending_nodes.insert(node_id, node_data); } @@ -210,10 +316,8 @@ impl State { ); } - // Store subtree state (root and focus) per tree let tree_focus = map_id(update.focus); if let Some(new_root) = new_tree_root { - // New tree: insert both root and focus self.subtrees.insert( tree_id, SubtreeState { @@ -222,11 +326,8 @@ impl State { }, ); } else if let Some(subtree) = self.subtrees.get_mut(&tree_id) { - // Existing tree: just update focus subtree.focus = tree_focus; } else if tree_id == TreeId::ROOT { - // ROOT tree focus update without tree change (e.g., during Tree::new after take()) - // Use the main tree's root for the subtree state self.subtrees.insert( tree_id, SubtreeState { @@ -236,40 +337,147 @@ impl State { ); } - self.focus = tree_focus; self.is_host_focused = is_host_focused; if !unreachable.is_empty() { fn traverse_unreachable( nodes: &mut HashMap, + grafts_to_remove: &mut HashSet, changes: &mut Option<&mut InternalChanges>, seen_child_ids: &HashSet, + new_tree_root: Option, id: NodeId, - map_id: impl Fn(LocalNodeId) -> NodeId + Copy, ) { if let Some(changes) = changes { changes.removed_node_ids.insert(id); } let node = nodes.remove(&id).unwrap(); + if let Some(subtree_id) = node.data.tree_id() { + grafts_to_remove.insert(subtree_id); + } + let (_, tree_index) = id.to_components(); for child_id in node.data.children().iter() { - let mapped_child_id = map_id(*child_id); - if !seen_child_ids.contains(&mapped_child_id) { + let child_node_id = NodeId::new(*child_id, tree_index); + if !seen_child_ids.contains(&child_node_id) + && new_tree_root != Some(child_node_id) + { traverse_unreachable( nodes, + grafts_to_remove, changes, seen_child_ids, - mapped_child_id, - map_id, + new_tree_root, + child_node_id, ); } } } for id in unreachable { - traverse_unreachable(&mut self.nodes, &mut changes, &seen_child_ids, id, map_id); + traverse_unreachable( + &mut self.nodes, + &mut grafts_to_remove, + &mut changes, + &seen_child_ids, + new_tree_root, + id, + ); + } + } + + fn traverse_subtree( + nodes: &mut HashMap, + subtrees_to_remove: &mut Vec, + subtrees_queued: &mut HashSet, + changes: &mut Option<&mut InternalChanges>, + id: NodeId, + ) { + let Some(node) = nodes.remove(&id) else { + return; + }; + if let Some(changes) = changes { + changes.removed_node_ids.insert(id); + } + if let Some(nested_subtree_id) = node.data.tree_id() { + if subtrees_queued.insert(nested_subtree_id) { + subtrees_to_remove.push(nested_subtree_id); + } + } + let (_, tree_index) = id.to_components(); + for child_id in node.data.children().iter() { + traverse_subtree( + nodes, + subtrees_to_remove, + subtrees_queued, + changes, + NodeId::new(*child_id, tree_index), + ); + } + } + + let mut subtrees_queued: HashSet = grafts_to_remove; + let mut subtrees_to_remove: Vec = subtrees_queued.iter().copied().collect(); + let mut i = 0; + while i < subtrees_to_remove.len() { + let subtree_id = subtrees_to_remove[i]; + i += 1; + + if self.graft_parents.remove(&subtree_id).is_none() { + continue; + } + + if pending_grafts.contains_key(&subtree_id) { + continue; + } + if let Some(subtree) = self.subtrees.remove(&subtree_id) { + traverse_subtree( + &mut self.nodes, + &mut subtrees_to_remove, + &mut subtrees_queued, + &mut changes, + subtree.root, + ); + } + } + + for (subtree_id, node_id) in pending_grafts { + if let Some(&existing_graft) = self.graft_parents.get(&subtree_id) { + panic!( + "Subtree {:?} already has a graft parent {:?}, cannot assign to {:?}", + subtree_id, + existing_graft.to_components().0, + node_id.to_components().0 + ); + } + self.graft_parents.insert(subtree_id, node_id); + if let Some(subtree) = self.subtrees.get(&subtree_id) { + let subtree_root_id = subtree.root; + if let Some(root_state) = self.nodes.get_mut(&subtree_root_id) { + root_state.parent_and_index = Some(ParentAndIndex(node_id, 0)); + if let Some(changes) = &mut changes { + if !changes.added_node_ids.contains(&subtree_root_id) { + changes.updated_node_ids.insert(subtree_root_id); + } + } + } + } + } + + if let Some(new_root_id) = new_tree_root { + if let Some(&graft_node_id) = self.graft_parents.get(&tree_id) { + if let Some(root_state) = self.nodes.get_mut(&new_root_id) { + root_state.parent_and_index = Some(ParentAndIndex(graft_node_id, 0)); + if let Some(changes) = &mut changes { + if !changes.added_node_ids.contains(&new_root_id) { + changes.updated_node_ids.insert(new_root_id); + } + } + } } } + self.focus = self.compute_effective_focus(); + self.validate_global(); } @@ -308,6 +516,11 @@ impl State { self.node_by_id(self.root_id()).unwrap() } + /// Returns the root NodeId of the subtree with the given TreeId, if it exists. + pub fn subtree_root(&self, tree_id: TreeId) -> Option { + self.subtrees.get(&tree_id).map(|s| s.root) + } + pub fn is_host_focused(&self) -> bool { self.is_host_focused } @@ -381,6 +594,7 @@ impl Tree { focus: NodeId::new(initial_state.focus, tree_index), is_host_focused, subtrees: HashMap::new(), + graft_parents: HashMap::new(), }; state.update(initial_state, is_host_focused, None, tree_index); Self { @@ -477,6 +691,9 @@ impl Tree { self.state.focus = self.next_state.focus; self.state.is_host_focused = self.next_state.is_host_focused; self.state.subtrees.clone_from(&self.next_state.subtrees); + self.state + .graft_parents + .clone_from(&self.next_state.graft_parents); } pub fn state(&self) -> &State { @@ -524,6 +741,14 @@ mod tests { use super::{TreeIndex, TreeIndexMap}; use crate::node::NodeId; + struct NoOpHandler; + impl super::ChangeHandler for NoOpHandler { + fn node_added(&mut self, _: &crate::Node) {} + fn node_updated(&mut self, _: &crate::Node, _: &crate::Node) {} + fn focus_moved(&mut self, _: Option<&crate::Node>, _: Option<&crate::Node>) {} + fn node_removed(&mut self, _: &crate::Node) {} + } + fn node_id(n: u64) -> NodeId { NodeId::new(LocalNodeId(n), TreeIndex(0)) } @@ -1087,4 +1312,1559 @@ mod tests { Some(node_id(0)) ); } + + fn subtree_id() -> TreeId { + TreeId(Uuid::from_u128(1)) + } + + #[test] + fn graft_node_tracks_subtree() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let tree = super::Tree::new(update, false); + assert_eq!( + tree.state().graft_parents.get(&subtree_id()), + Some(&node_id(1)) + ); + } + + #[test] + #[should_panic(expected = "already has a graft parent")] + fn duplicate_graft_parent_panics() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1), LocalNodeId(2)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + (LocalNodeId(2), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let _ = super::Tree::new(update, false); + } + + #[test] + fn reparent_subtree_by_removing_old_graft() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1), LocalNodeId(2)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + (LocalNodeId(2), Node::new(Role::GenericContainer)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + assert_eq!( + tree.state().graft_parents.get(&subtree_id()), + Some(&node_id(1)) + ); + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(2)]); + node + }), + (LocalNodeId(2), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + assert_eq!( + tree.state().graft_parents.get(&subtree_id()), + Some(&node_id(2)) + ); + } + + #[test] + fn reparent_subtree_by_clearing_old_graft_tree_id() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1), LocalNodeId(2)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + (LocalNodeId(2), Node::new(Role::GenericContainer)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + assert_eq!( + tree.state().graft_parents.get(&subtree_id()), + Some(&node_id(1)) + ); + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(1), Node::new(Role::GenericContainer)), + (LocalNodeId(2), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + assert_eq!( + tree.state().graft_parents.get(&subtree_id()), + Some(&node_id(2)) + ); + } + + #[test] + #[should_panic(expected = "already has a graft parent")] + fn duplicate_graft_parent_on_update_panics() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1), LocalNodeId(2)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + (LocalNodeId(2), Node::new(Role::GenericContainer)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let update = TreeUpdate { + nodes: vec![(LocalNodeId(2), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + })], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + } + + #[test] + #[should_panic(expected = "Cannot graft the root tree")] + fn graft_root_tree_panics() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(TreeId::ROOT); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let _ = super::Tree::new(update, false); + } + + #[test] + #[should_panic(expected = "Cannot graft the root tree")] + fn graft_root_tree_on_update_panics() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::GenericContainer)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let update = TreeUpdate { + nodes: vec![(LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(TreeId::ROOT); + node + })], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + } + + fn subtree_node_id(id: u64) -> NodeId { + NodeId::new(LocalNodeId(id), TreeIndex(1)) + } + + #[test] + fn subtree_root_parent_is_graft_when_graft_exists_first() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![(LocalNodeId(0), Node::new(Role::Document))], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let subtree_root = tree.state().node_by_id(subtree_node_id(0)).unwrap(); + assert_eq!(subtree_root.parent_id(), Some(node_id(1))); + + let graft_node = tree.state().node_by_id(node_id(1)).unwrap(); + let children: Vec<_> = graft_node.child_ids().collect(); + assert_eq!(children.len(), 1); + assert_eq!(children[0], subtree_node_id(0)); + } + + #[test] + #[should_panic(expected = "no graft node exists for this tree")] + fn subtree_push_without_graft_panics() { + let update = TreeUpdate { + nodes: vec![(LocalNodeId(0), Node::new(Role::Window))], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![(LocalNodeId(0), Node::new(Role::Document))], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + } + + #[test] + #[should_panic(expected = "subtree does not exist")] + fn subtree_update_without_tree_data_panics() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![(LocalNodeId(0), Node::new(Role::Document))], + tree: None, + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + } + + #[test] + fn subtree_nodes_removed_when_graft_removed() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(nested_subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let nested_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Paragraph)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: nested_subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(nested_update, &mut NoOpHandler); + + assert!(tree.state().node_by_id(subtree_node_id(0)).is_some()); + assert!(tree.state().node_by_id(subtree_node_id(1)).is_some()); + assert!(tree.state().node_by_id(nested_subtree_node_id(0)).is_some()); + assert!(tree.state().node_by_id(nested_subtree_node_id(1)).is_some()); + + let update = TreeUpdate { + nodes: vec![(LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![]); + node + })], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + + assert!(tree.state().node_by_id(subtree_node_id(0)).is_none()); + assert!(tree.state().node_by_id(subtree_node_id(1)).is_none()); + assert!(tree.state().node_by_id(nested_subtree_node_id(0)).is_none()); + assert!(tree.state().node_by_id(nested_subtree_node_id(1)).is_none()); + assert!(tree.state().subtrees.get(&subtree_id()).is_none()); + assert!(tree.state().subtrees.get(&nested_subtree_id()).is_none()); + } + + #[test] + fn subtree_nodes_removed_when_graft_tree_id_cleared() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Paragraph)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + assert!(tree.state().node_by_id(subtree_node_id(0)).is_some()); + assert!(tree.state().node_by_id(subtree_node_id(1)).is_some()); + + let update = TreeUpdate { + nodes: vec![(LocalNodeId(1), Node::new(Role::GenericContainer))], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + + assert!(tree.state().node_by_id(subtree_node_id(0)).is_none()); + assert!(tree.state().node_by_id(subtree_node_id(1)).is_none()); + assert!(tree.state().subtrees.get(&subtree_id()).is_none()); + } + + #[test] + fn graft_node_has_no_children_when_subtree_not_pushed() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let tree = super::Tree::new(update, false); + + let graft_node = tree.state().node_by_id(node_id(1)).unwrap(); + assert_eq!(graft_node.child_ids().count(), 0); + assert_eq!(graft_node.children().count(), 0); + } + + #[test] + #[should_panic(expected = "has both tree_id")] + fn graft_node_with_children_panics() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node.set_children(vec![LocalNodeId(2)]); + node + }), + (LocalNodeId(2), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + super::Tree::new(update, false); + } + + #[test] + fn node_added_called_when_subtree_pushed() { + struct Handler { + added_nodes: Vec, + } + impl super::ChangeHandler for Handler { + fn node_added(&mut self, node: &crate::Node) { + self.added_nodes.push(node.id()); + } + fn node_updated(&mut self, _: &crate::Node, _: &crate::Node) {} + fn focus_moved(&mut self, _: Option<&crate::Node>, _: Option<&crate::Node>) {} + fn node_removed(&mut self, _: &crate::Node) {} + } + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let mut handler = Handler { + added_nodes: Vec::new(), + }; + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1), LocalNodeId(2)]); + node + }), + (LocalNodeId(1), Node::new(Role::Paragraph)), + (LocalNodeId(2), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut handler); + + assert_eq!(handler.added_nodes.len(), 3,); + assert!(handler.added_nodes.contains(&subtree_node_id(0)),); + assert!(handler.added_nodes.contains(&subtree_node_id(1)),); + assert!(handler.added_nodes.contains(&subtree_node_id(2)),); + } + + #[test] + fn node_removed_called_when_graft_removed() { + struct Handler { + removed_nodes: Vec, + } + impl super::ChangeHandler for Handler { + fn node_added(&mut self, _: &crate::Node) {} + fn node_updated(&mut self, _: &crate::Node, _: &crate::Node) {} + fn focus_moved(&mut self, _: Option<&crate::Node>, _: Option<&crate::Node>) {} + fn node_removed(&mut self, node: &crate::Node) { + self.removed_nodes.push(node.id()); + } + } + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Paragraph)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + assert!(tree.state().node_by_id(subtree_node_id(0)).is_some()); + assert!(tree.state().node_by_id(subtree_node_id(1)).is_some()); + + let mut handler = Handler { + removed_nodes: Vec::new(), + }; + + let update = TreeUpdate { + nodes: vec![(LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![]); + node + })], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(update, &mut handler); + + assert!(handler.removed_nodes.contains(&node_id(1)),); + assert!(handler.removed_nodes.contains(&subtree_node_id(0)),); + assert!(handler.removed_nodes.contains(&subtree_node_id(1)),); + assert_eq!(handler.removed_nodes.len(), 3,); + } + + #[test] + fn node_updated_called_when_subtree_reparented() { + struct Handler { + updated_nodes: Vec, + } + impl super::ChangeHandler for Handler { + fn node_added(&mut self, _: &crate::Node) {} + fn node_updated(&mut self, _old: &crate::Node, new: &crate::Node) { + self.updated_nodes.push(new.id()); + } + fn focus_moved(&mut self, _: Option<&crate::Node>, _: Option<&crate::Node>) {} + fn node_removed(&mut self, _: &crate::Node) {} + } + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1), LocalNodeId(2)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + (LocalNodeId(2), Node::new(Role::GenericContainer)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![(LocalNodeId(0), Node::new(Role::Document))], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let subtree_root = tree.state().node_by_id(subtree_node_id(0)).unwrap(); + assert_eq!(subtree_root.parent().unwrap().id(), node_id(1)); + + let mut handler = Handler { + updated_nodes: Vec::new(), + }; + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(1), Node::new(Role::GenericContainer)), + (LocalNodeId(2), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(update, &mut handler); + + assert!(handler.updated_nodes.contains(&subtree_node_id(0)),); + + let subtree_root = tree.state().node_by_id(subtree_node_id(0)).unwrap(); + assert_eq!(subtree_root.parent().unwrap().id(), node_id(2)); + } + + #[test] + fn focus_moved_called_when_focus_moves_to_subtree() { + struct Handler { + focus_moves: Vec<(Option, Option)>, + } + impl super::ChangeHandler for Handler { + fn node_added(&mut self, _: &crate::Node) {} + fn node_updated(&mut self, _: &crate::Node, _: &crate::Node) {} + fn focus_moved(&mut self, old: Option<&crate::Node>, new: Option<&crate::Node>) { + self.focus_moves + .push((old.map(|n| n.id()), new.map(|n| n.id()))); + } + fn node_removed(&mut self, _: &crate::Node) {} + } + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, true); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let mut handler = Handler { + focus_moves: Vec::new(), + }; + + let update = TreeUpdate { + nodes: vec![], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(update, &mut handler); + + assert_eq!(handler.focus_moves.len(), 1,); + let (old_focus, new_focus) = &handler.focus_moves[0]; + assert_eq!(*old_focus, Some(node_id(0)),); + assert_eq!(*new_focus, Some(subtree_node_id(0)),); + } + + #[test] + fn focus_moved_called_when_subtree_focus_changes() { + struct Handler { + focus_moves: Vec<(Option, Option)>, + } + impl super::ChangeHandler for Handler { + fn node_added(&mut self, _: &crate::Node) {} + fn node_updated(&mut self, _: &crate::Node, _: &crate::Node) {} + fn focus_moved(&mut self, old: Option<&crate::Node>, new: Option<&crate::Node>) { + self.focus_moves + .push((old.map(|n| n.id()), new.map(|n| n.id()))); + } + fn node_removed(&mut self, _: &crate::Node) {} + } + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, true); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let root_update = TreeUpdate { + nodes: vec![], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(root_update, &mut NoOpHandler); + + let mut handler = Handler { + focus_moves: Vec::new(), + }; + + let subtree_update = TreeUpdate { + nodes: vec![], + tree: None, + tree_id: subtree_id(), + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(subtree_update, &mut handler); + + assert_eq!(handler.focus_moves.len(), 1,); + let (old_focus, new_focus) = &handler.focus_moves[0]; + assert_eq!(*old_focus, Some(subtree_node_id(0)),); + assert_eq!(*new_focus, Some(subtree_node_id(1)),); + } + + fn nested_subtree_id() -> TreeId { + TreeId(Uuid::from_u128(2)) + } + + fn nested_subtree_node_id(n: u64) -> NodeId { + NodeId::new(LocalNodeId(n), TreeIndex(2)) + } + + #[test] + fn nested_subtree_focus_follows_graft_chain() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, true); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(nested_subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let nested_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Group); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: nested_subtree_id(), + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(nested_update, &mut NoOpHandler); + + let update = TreeUpdate { + nodes: vec![], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + + let update = TreeUpdate { + nodes: vec![], + tree: None, + tree_id: subtree_id(), + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + + assert_eq!(tree.state().focus_id(), Some(nested_subtree_node_id(1)),); + } + + #[test] + fn nested_subtree_focus_update_changes_effective_focus() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, true); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(nested_subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let nested_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Group); + node.set_children(vec![LocalNodeId(1), LocalNodeId(2)]); + node + }), + (LocalNodeId(1), Node::new(Role::Button)), + (LocalNodeId(2), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: nested_subtree_id(), + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(nested_update, &mut NoOpHandler); + + let root_update = TreeUpdate { + nodes: vec![], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(root_update, &mut NoOpHandler); + + assert_eq!(tree.state().focus_id(), Some(nested_subtree_node_id(1))); + + let update = TreeUpdate { + nodes: vec![], + tree: None, + tree_id: nested_subtree_id(), + focus: LocalNodeId(2), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + + assert_eq!(tree.state().focus_id(), Some(nested_subtree_node_id(2)),); + } + + #[test] + #[should_panic(expected = "Graft nodes cannot be focused without their subtree")] + fn removing_nested_subtree_while_intermediate_focus_on_graft_panics() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(1), + }; + let mut tree = super::Tree::new(update, true); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(nested_subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let nested_update = TreeUpdate { + nodes: vec![(LocalNodeId(0), Node::new(Role::Button))], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: nested_subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(nested_update, &mut NoOpHandler); + + let update = TreeUpdate { + nodes: vec![(LocalNodeId(1), Node::new(Role::GenericContainer))], + tree: None, + tree_id: subtree_id(), + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + } + + #[test] + fn nested_subtree_root_lookup_for_focus_only_update() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, true); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1), LocalNodeId(2)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(nested_subtree_id()); + node + }), + (LocalNodeId(2), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let nested_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Group); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: nested_subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(nested_update, &mut NoOpHandler); + + let update = TreeUpdate { + nodes: vec![], + tree: None, + tree_id: subtree_id(), + focus: LocalNodeId(2), + }; + tree.update_and_process_changes(update, &mut NoOpHandler); + + assert_eq!( + tree.state().subtrees.get(&subtree_id()).unwrap().focus, + subtree_node_id(2), + ); + } + + #[test] + fn subtree_root_change_updates_graft_and_parent() { + struct Handler { + updated_nodes: Vec, + added_nodes: Vec, + removed_nodes: Vec, + } + impl super::ChangeHandler for Handler { + fn node_added(&mut self, node: &crate::Node) { + self.added_nodes.push(node.id()); + } + fn node_updated(&mut self, _old: &crate::Node, new: &crate::Node) { + self.updated_nodes.push(new.id()); + } + fn focus_moved(&mut self, _: Option<&crate::Node>, _: Option<&crate::Node>) {} + fn node_removed(&mut self, node: &crate::Node) { + self.removed_nodes.push(node.id()); + } + } + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Paragraph)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let mut handler = Handler { + updated_nodes: Vec::new(), + added_nodes: Vec::new(), + removed_nodes: Vec::new(), + }; + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(2), { + let mut node = Node::new(Role::Article); + node.set_children(vec![LocalNodeId(3)]); + node + }), + (LocalNodeId(3), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(2))), + tree_id: subtree_id(), + focus: LocalNodeId(2), + }; + tree.update_and_process_changes(subtree_update, &mut handler); + + let graft_node = tree.state().node_by_id(node_id(1)).unwrap(); + let children: Vec<_> = graft_node.child_ids().collect(); + assert_eq!(children.len(), 1); + assert_eq!(children[0], subtree_node_id(2)); + + let new_subtree_root = tree.state().node_by_id(subtree_node_id(2)).unwrap(); + assert_eq!(new_subtree_root.parent_id(), Some(node_id(1))); + assert_eq!(new_subtree_root.role(), Role::Article); + + assert!(tree.state().node_by_id(subtree_node_id(0)).is_none()); + assert!(tree.state().node_by_id(subtree_node_id(1)).is_none()); + + assert!(tree.state().node_by_id(subtree_node_id(2)).is_some()); + assert!(tree.state().node_by_id(subtree_node_id(3)).is_some()); + + assert!(handler.removed_nodes.contains(&subtree_node_id(0)),); + assert!(handler.removed_nodes.contains(&subtree_node_id(1)),); + assert!(handler.added_nodes.contains(&subtree_node_id(2)),); + assert!(handler.added_nodes.contains(&subtree_node_id(3)),); + } + + #[test] + fn subtree_root_change_to_existing_child() { + struct Handler { + updated_nodes: Vec, + added_nodes: Vec, + removed_nodes: Vec, + } + impl super::ChangeHandler for Handler { + fn node_added(&mut self, node: &crate::Node) { + self.added_nodes.push(node.id()); + } + fn node_updated(&mut self, _old: &crate::Node, new: &crate::Node) { + self.updated_nodes.push(new.id()); + } + fn focus_moved(&mut self, _: Option<&crate::Node>, _: Option<&crate::Node>) {} + fn node_removed(&mut self, node: &crate::Node) { + self.removed_nodes.push(node.id()); + } + } + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::Article); + node.set_children(vec![LocalNodeId(2)]); + node + }), + (LocalNodeId(2), Node::new(Role::Paragraph)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let graft_node = tree.state().node_by_id(node_id(1)).unwrap(); + assert_eq!(graft_node.child_ids().next(), Some(subtree_node_id(0))); + + let old_root = tree.state().node_by_id(subtree_node_id(0)).unwrap(); + assert_eq!(old_root.role(), Role::Document); + assert_eq!(old_root.parent_id(), Some(node_id(1))); + + let child = tree.state().node_by_id(subtree_node_id(1)).unwrap(); + assert_eq!(child.role(), Role::Article); + assert_eq!(child.parent_id(), Some(subtree_node_id(0))); + + let grandchild = tree.state().node_by_id(subtree_node_id(2)).unwrap(); + assert_eq!(grandchild.parent_id(), Some(subtree_node_id(1))); + + let mut handler = Handler { + updated_nodes: Vec::new(), + added_nodes: Vec::new(), + removed_nodes: Vec::new(), + }; + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(1), { + let mut node = Node::new(Role::Article); + node.set_children(vec![LocalNodeId(2)]); + node + }), + (LocalNodeId(2), Node::new(Role::Paragraph)), + ], + tree: Some(Tree::new(LocalNodeId(1))), + tree_id: subtree_id(), + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(subtree_update, &mut handler); + + let graft_node = tree.state().node_by_id(node_id(1)).unwrap(); + let children: Vec<_> = graft_node.child_ids().collect(); + assert_eq!(children.len(), 1); + assert_eq!(children[0], subtree_node_id(1)); + + let new_root = tree.state().node_by_id(subtree_node_id(1)).unwrap(); + assert_eq!(new_root.parent_id(), Some(node_id(1))); + assert_eq!(new_root.role(), Role::Article); + + assert!(tree.state().node_by_id(subtree_node_id(0)).is_none(),); + + let grandchild = tree.state().node_by_id(subtree_node_id(2)).unwrap(); + assert_eq!(grandchild.parent_id(), Some(subtree_node_id(1))); + + assert!(handler.removed_nodes.contains(&subtree_node_id(0)),); + assert!(handler.updated_nodes.contains(&subtree_node_id(1)),); + assert!(!handler.added_nodes.contains(&subtree_node_id(1)),); + assert!(!handler.added_nodes.contains(&subtree_node_id(2)),); + } + + #[test] + fn subtree_root_change_to_new_parent_of_old_root() { + struct Handler { + updated_nodes: Vec, + added_nodes: Vec, + removed_nodes: Vec, + } + impl super::ChangeHandler for Handler { + fn node_added(&mut self, node: &crate::Node) { + self.added_nodes.push(node.id()); + } + fn node_updated(&mut self, _old: &crate::Node, new: &crate::Node) { + self.updated_nodes.push(new.id()); + } + fn focus_moved(&mut self, _: Option<&crate::Node>, _: Option<&crate::Node>) {} + fn node_removed(&mut self, node: &crate::Node) { + self.removed_nodes.push(node.id()); + } + } + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Paragraph)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let mut handler = Handler { + updated_nodes: Vec::new(), + added_nodes: Vec::new(), + removed_nodes: Vec::new(), + }; + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(2), { + let mut node = Node::new(Role::Article); + node.set_children(vec![LocalNodeId(0)]); + node + }), + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Paragraph)), + ], + tree: Some(Tree::new(LocalNodeId(2))), + tree_id: subtree_id(), + focus: LocalNodeId(2), + }; + tree.update_and_process_changes(subtree_update, &mut handler); + + let graft_node = tree.state().node_by_id(node_id(1)).unwrap(); + let children: Vec<_> = graft_node.child_ids().collect(); + assert_eq!(children.len(), 1); + assert_eq!(children[0], subtree_node_id(2)); + + let new_root = tree.state().node_by_id(subtree_node_id(2)).unwrap(); + assert_eq!(new_root.parent_id(), Some(node_id(1))); + assert_eq!(new_root.role(), Role::Article); + + let old_root = tree.state().node_by_id(subtree_node_id(0)).unwrap(); + assert_eq!(old_root.parent_id(), Some(subtree_node_id(2))); + assert_eq!(old_root.role(), Role::Document); + + let grandchild = tree.state().node_by_id(subtree_node_id(1)).unwrap(); + assert_eq!(grandchild.parent_id(), Some(subtree_node_id(0))); + + assert!(handler.added_nodes.contains(&subtree_node_id(2))); + assert!(handler.updated_nodes.contains(&subtree_node_id(0))); + assert!(!handler.removed_nodes.contains(&subtree_node_id(0))); + assert!(!handler.removed_nodes.contains(&subtree_node_id(1))); + } + + #[test] + fn subtree_update_without_tree_preserves_root() { + struct Handler { + updated_nodes: Vec, + added_nodes: Vec, + removed_nodes: Vec, + } + impl super::ChangeHandler for Handler { + fn node_added(&mut self, node: &crate::Node) { + self.added_nodes.push(node.id()); + } + fn node_updated(&mut self, _old: &crate::Node, new: &crate::Node) { + self.updated_nodes.push(new.id()); + } + fn focus_moved(&mut self, _: Option<&crate::Node>, _: Option<&crate::Node>) {} + fn node_removed(&mut self, node: &crate::Node) { + self.removed_nodes.push(node.id()); + } + } + + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::GenericContainer); + node.set_tree_id(subtree_id()); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(0), + }; + let mut tree = super::Tree::new(update, false); + + let subtree_update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Document); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::Paragraph); + node.set_label("original"); + node + }), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut NoOpHandler); + + let mut handler = Handler { + updated_nodes: Vec::new(), + added_nodes: Vec::new(), + removed_nodes: Vec::new(), + }; + + let subtree_update = TreeUpdate { + nodes: vec![(LocalNodeId(1), { + let mut node = Node::new(Role::Paragraph); + node.set_label("modified"); + node + })], + tree: None, + tree_id: subtree_id(), + focus: LocalNodeId(0), + }; + tree.update_and_process_changes(subtree_update, &mut handler); + + let subtree_root = tree.state().node_by_id(subtree_node_id(0)).unwrap(); + assert_eq!(subtree_root.role(), Role::Document); + assert_eq!(subtree_root.parent_id(), Some(node_id(1))); + + let graft_node = tree.state().node_by_id(node_id(1)).unwrap(); + assert_eq!(graft_node.child_ids().next(), Some(subtree_node_id(0))); + + let child = tree.state().node_by_id(subtree_node_id(1)).unwrap(); + assert_eq!(child.label().as_deref(), Some("modified")); + + assert!(handler.removed_nodes.is_empty(),); + assert!(handler.added_nodes.is_empty()); + assert!(handler.updated_nodes.contains(&subtree_node_id(1)),); + assert!(!handler.updated_nodes.contains(&subtree_node_id(0)),); + } }