Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
92 changes: 85 additions & 7 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -626,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))]
Expand All @@ -652,6 +659,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
Expand Down Expand Up @@ -761,6 +783,7 @@ enum PropertyValue {
Rect(Rect),
TextSelection(Box<TextSelection>),
CustomActionVec(Vec<CustomAction>),
TreeId(TreeId),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
Expand Down Expand Up @@ -877,6 +900,7 @@ enum PropertyId {
Bounds,
TextSelection,
CustomActions,
TreeId,

// This MUST be last.
Unset,
Expand Down Expand Up @@ -1678,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! {
Expand All @@ -1696,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! {
Expand Down Expand Up @@ -1997,11 +2023,20 @@ property_methods! {
/// [`transform`]: Node::transform
(Bounds, bounds, get_rect_property, Option<Rect>, 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<Box<TextSelection>>, clear_text_selection)
(TextSelection, text_selection, get_text_selection_property, Option<&TextSelection>, set_text_selection, set_text_selection_property, impl Into<Box<TextSelection>>, 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<TreeId>, 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)]
Expand Down Expand Up @@ -2106,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)
}
Expand Down Expand Up @@ -2274,7 +2334,8 @@ impl Serialize for Properties {
Affine,
Rect,
TextSelection,
CustomActionVec
CustomActionVec,
TreeId
});
}
map.end()
Expand Down Expand Up @@ -2404,7 +2465,8 @@ impl<'de> Visitor<'de> for PropertiesVisitor {
Affine { Transform },
Rect { Bounds },
TextSelection { TextSelection },
CustomActionVec { CustomActions }
CustomActionVec { CustomActions },
TreeId { TreeId }
});
}

Expand Down Expand Up @@ -2634,11 +2696,26 @@ pub struct TreeUpdate {
/// a tree.
pub tree: Option<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
/// 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
/// 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,
}

Expand Down Expand Up @@ -2712,7 +2789,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<ActionData>,
}

Expand Down
46 changes: 43 additions & 3 deletions consumer/src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ fn common_filter_base(node: &Node) -> Option<FilterResult> {
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);
Expand Down Expand Up @@ -95,19 +100,20 @@ 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::{
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())
);
}

Expand All @@ -123,6 +129,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);
Expand All @@ -145,6 +152,7 @@ mod tests {
}),
],
tree: Some(Tree::new(NodeId(0))),
tree_id: TreeId::ROOT,
focus: NodeId(0),
};
let tree = crate::Tree::new(update, false);
Expand All @@ -167,6 +175,7 @@ mod tests {
}),
],
tree: Some(Tree::new(NodeId(0))),
tree_id: TreeId::ROOT,
focus: NodeId(1),
};
let tree = crate::Tree::new(update, true);
Expand All @@ -185,13 +194,14 @@ 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);
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));
}
Expand All @@ -209,6 +219,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);
Expand All @@ -229,6 +240,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);
Expand All @@ -248,6 +260,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);
Expand Down Expand Up @@ -333,6 +346,7 @@ mod tests {
}),
],
tree: Some(Tree::new(NodeId(0))),
tree_id: TreeId::ROOT,
focus: NodeId(0),
};
crate::Tree::new(update, false)
Expand Down Expand Up @@ -378,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));
}
}
Loading