1use std::cell::RefCell;
6use std::fmt::Debug;
7use std::iter::repeat;
8use std::sync::atomic;
9use std::sync::atomic::AtomicU64;
10
11use accesskit::{ActionRequest, NodeId, Role};
12use layout_api::{
13 AccessibilityActionRequest, AccessibilityDamage, LayoutNode, node_id_from_scroll_id,
14};
15use paint_api::display_list::SpatialTreeNodeInfo;
16use rustc_hash::{FxHashMap, FxHashSet};
17use script::layout_dom::ServoLayoutNode;
18use servo_base::Epoch;
19use servo_base::print_tree::PrintTree;
20use servo_config::opts::{self, DiagnosticsLogging, DiagnosticsLoggingOption};
21use servo_config::pref;
22use style::dom::OpaqueNode;
23use webrender_api::ExternalScrollId;
24use webrender_api::units::LayoutVector2D;
25
26use super::accessibility_node::{AccessibilityNode, DirtyState};
27use crate::ArcRefCell;
28use crate::accessibility::{AccessibilityContext, AccessibilityDamageMap};
29
30#[derive(Debug, Default)]
31pub struct UpdateCounters {
32 pub nodes_updated_from_dom: u32,
33 pub nodes_updated_from_tree: u32,
34 pub nodes_updated_bounds: u32,
35 pub nodes_in_tree_update: u32,
36}
37
38#[derive(Debug)]
43pub struct AccessibilityTree {
44 nodes: FxHashMap<NodeId, ArcRefCell<AccessibilityNode>>,
47 focused_node_id: Option<NodeId>,
49 opaque_node_to_id: FxHashMap<OpaqueNode, NodeId>,
54 id_to_opaque_node: FxHashMap<NodeId, OpaqueNode>,
59 tree_id: accesskit::TreeId,
63 root_node: Option<ArcRefCell<AccessibilityNode>>,
67 pending_scroll_updates: FxHashMap<ExternalScrollId, LayoutVector2D>,
70 embedder_epoch: Epoch,
73 pending_actions: Vec<AccessibilityActionRequest>,
79 debug: DiagnosticsLogging,
82}
83
84pub(super) struct AccessibilityUpdate<'update> {
86 changed_nodes: FxHashSet<NodeId>,
88 accesskit_tree: Option<accesskit::Tree>,
92 tree_changes: FxHashMap<NodeId, TreeChange>,
94 focused_node_changed: bool,
96 pub(super) counters: UpdateCounters,
98
99 damage_map: FxHashMap<NodeId, AccessibilityDamage>,
101 dom_node_map: RefCell<FxHashMap<NodeId, ServoLayoutNode<'update>>>,
104}
105
106#[derive(Debug, PartialEq, Copy, Clone)]
111pub(super) enum TreeChange {
112 New,
114
115 Moved,
117
118 PendingMove,
134
135 Removed,
137}
138
139impl AccessibilityTree {
140 pub(crate) fn new(tree_id: accesskit::TreeId, embedder_epoch: Epoch) -> Self {
142 Self {
143 nodes: FxHashMap::default(),
144 focused_node_id: None,
145 opaque_node_to_id: FxHashMap::default(),
146 id_to_opaque_node: FxHashMap::default(),
147 tree_id,
148 root_node: None,
149 pending_scroll_updates: FxHashMap::default(),
150 embedder_epoch,
151 pending_actions: vec![],
152 debug: opts::get().debug.clone(),
153 }
154 }
155
156 pub(crate) fn update_tree<'update>(
159 &mut self,
160 root_dom_node: &ServoLayoutNode<'update>,
161 damage_from_dom: AccessibilityDamageMap<'update>,
162 action_requests: Vec<ActionRequest>,
163 context: AccessibilityContext<'update>,
164 ) -> (Option<accesskit::TreeUpdate>, UpdateCounters) {
165 let mut update = AccessibilityUpdate::new(damage_from_dom, self);
166
167 self.ensure_root_node(root_dom_node, &context, &mut update);
168
169 self.apply_changes_from_dom_tree(&context, &mut update);
170
171 self.update_focused_node(context.focused_element, &mut update);
172
173 self.handle_pending_scroll_updates(&mut update);
174
175 update.finalize(
176 self,
177 context.rooted_nodes_for_integrity_check,
178 action_requests,
179 )
180 }
181
182 pub(crate) fn add_pending_scroll_updates(
185 &mut self,
186 scroll_states: FxHashMap<ExternalScrollId, LayoutVector2D>,
187 ) {
188 self.pending_scroll_updates.extend(scroll_states);
189 }
190
191 pub(crate) fn add_pending_scroll_update(
194 &mut self,
195 external_scroll_id: ExternalScrollId,
196 offset: LayoutVector2D,
197 ) {
198 self.pending_scroll_updates
199 .insert(external_scroll_id, offset);
200 }
201
202 fn ensure_root_node<'update>(
206 &mut self,
207 root_dom_node: &ServoLayoutNode<'update>,
208 context: &AccessibilityContext<'update>,
209 update: &mut AccessibilityUpdate<'update>,
210 ) {
211 let (root_id, root_node) = self.get_or_create_node(root_dom_node, update);
212 if update.is_new(&root_id) {
213 update.clear_damage();
215 update.insert_damage(root_id, AccessibilityDamage::Rebuild);
216 update.insert_dom_node(root_id, *root_dom_node);
217 self.populate_pending_scroll_updates_from_scroll_tree(context);
218
219 update.accesskit_tree = Some(accesskit::Tree::new(root_id));
220 }
221
222 self.root_node = Some(root_node);
223 }
224
225 fn apply_changes_from_dom_tree(
229 &mut self,
230 context: &AccessibilityContext,
231 update: &mut AccessibilityUpdate,
232 ) {
233 let Some(damage_root_id) = self.mark_nodes_and_ancestors_dirty(update) else {
234 return;
235 };
236 let damage_root = self.assert_node_for_id(&damage_root_id);
237 let hidden = false;
238 let local_damage = damage_root.borrow_mut().update_subtree(
239 damage_root.clone(),
240 AccessibilityDamage::empty(),
241 hidden,
242 context,
243 self,
244 update,
245 );
246
247 damage_root.borrow().update_ancestors(local_damage, update);
248 }
249
250 fn populate_pending_scroll_updates_from_scroll_tree(&mut self, context: &AccessibilityContext) {
255 let scroll_tree = &context.stacking_context_tree.paint_info.scroll_tree;
256 let scroll_updates = scroll_tree
257 .nodes
258 .iter()
259 .filter_map(|node| match node.info {
260 SpatialTreeNodeInfo::Scroll(ref info) => {
261 let offset = info.offset;
262 Some((info.external_id, offset))
263 },
264 _ => None,
265 })
266 .collect();
267 self.pending_scroll_updates = scroll_updates;
268 }
269
270 fn handle_pending_scroll_updates(&mut self, update: &mut AccessibilityUpdate) {
277 let pending_scroll_updates = std::mem::take(&mut self.pending_scroll_updates);
278 for (opaque, offset) in
279 pending_scroll_updates
280 .into_iter()
281 .filter_map(|(scroll_id, translate)| {
282 if scroll_id.is_root() {
283 let root_node_opaque =
284 self.root_node.as_ref()?.clone().borrow().opaque_node()?;
285 return Some((root_node_opaque, translate));
286 }
287 let node_id = node_id_from_scroll_id(scroll_id.0 as usize);
288 let opaque = OpaqueNode(node_id);
289 Some((opaque, translate))
290 })
291 {
292 let Some(node) = self.node_for_opaque(opaque) else {
293 continue;
294 };
295 node.borrow_mut().set_scroll_offset(offset, update);
296 }
297 }
298
299 fn mark_nodes_and_ancestors_dirty(
308 &mut self,
309 update: &mut AccessibilityUpdate,
310 ) -> Option<NodeId> {
311 let mut dirty_node_ids = update.damage_map.keys();
312
313 let mut common_ancestors: Vec<NodeId> = Vec::new();
316
317 {
318 let node_id = dirty_node_ids.next()?;
320 update.collect_dom_node_ancestors(node_id, self);
321 let first_node = self.assert_node_for_id(node_id);
322 let mut first_node = first_node.borrow_mut();
323 first_node.add_dirty_state(DirtyState::HasDamage);
324 common_ancestors.push(first_node.id());
325 common_ancestors.extend(first_node.ancestors().map(|ancestor| {
326 let mut ancestor = ancestor.borrow_mut();
327 ancestor.add_dirty_state(DirtyState::DescendantHasDamage);
328 ancestor.id()
329 }));
330 common_ancestors.reverse();
331 }
332
333 let mut truncate_ancestors = |node: &AccessibilityNode| -> bool {
334 if node.dirty_state().descendant_has_damage() {
335 if let Some(pos) = common_ancestors.iter().position(|&id| id == node.id()) {
336 common_ancestors.truncate(pos + 1);
337 }
338 return true;
339 }
340 false
341 };
342
343 for node_id in dirty_node_ids {
344 let node = self.assert_node_for_id(node_id);
345 let mut node = node.borrow_mut();
346 node.add_dirty_state(DirtyState::HasDamage);
347
348 if truncate_ancestors(&node) {
349 continue;
350 }
351
352 for ancestor in node.ancestors() {
353 let mut ancestor = ancestor.borrow_mut();
354
355 if truncate_ancestors(&ancestor) {
358 break;
359 }
360
361 ancestor.add_dirty_state(DirtyState::DescendantHasDamage);
362 }
363 }
364
365 let lowest_common_ancestor = common_ancestors.pop();
366
367 for ancestor_id in common_ancestors {
368 let ancestor = self.assert_node_for_id(&ancestor_id);
369 ancestor
370 .borrow_mut()
371 .remove_dirty_state(DirtyState::DescendantHasDamage);
372 }
373
374 lowest_common_ancestor
375 }
376
377 fn update_focused_node(
378 &mut self,
379 focused_element: Option<OpaqueNode>,
380 update: &mut AccessibilityUpdate,
381 ) {
382 let mut focused_node_id = None;
383 if let Some(focused_element) = focused_element &&
384 let Some(node_id) = self.existing_id_for_opaque(focused_element) &&
385 let Some(focused_node) = self.node_for_id(node_id)
386 {
387 let focused_node = focused_node.borrow();
388 if focused_node.role() == Role::GenericContainer {
389 focused_node_id = self.focused_node_id;
392 } else {
393 focused_node_id = Some(focused_node.id());
394 }
395 }
396
397 if focused_node_id == self.focused_node_id {
398 return;
399 }
400
401 update.focused_node_changed = true;
402 self.focused_node_id = focused_node_id;
403 }
404
405 pub(super) fn focused_node_id(&self) -> Option<NodeId> {
406 self.focused_node_id
407 }
408
409 pub(super) fn get_or_create_node(
410 &mut self,
411 dom_node: &ServoLayoutNode<'_>,
412 update: &mut AccessibilityUpdate,
413 ) -> (NodeId, ArcRefCell<AccessibilityNode>) {
414 let id = self.get_or_create_id_for_opaque(dom_node.opaque());
415 let node_ref = self.get_or_create_node_with_id(id, update);
416
417 if update.is_new(&id) {
418 let mut node = node_ref.borrow_mut();
419 node.initialize_from_dom_node(dom_node);
420 update.insert_damage(id, AccessibilityDamage::Rebuild);
421 node.add_dirty_state(DirtyState::HasDamage);
422 }
423
424 (id, node_ref)
425 }
426
427 pub(crate) fn accesskit_node_for_dom_node(
428 &self,
429 dom_node: &ServoLayoutNode,
430 ) -> Option<accesskit::Node> {
431 let node = self.node_for_opaque(dom_node.opaque())?;
432 Some(node.borrow().clone_accesskit_node())
433 }
434
435 fn get_or_create_node_with_id(
436 &mut self,
437 id: NodeId,
438 update: &mut AccessibilityUpdate,
439 ) -> ArcRefCell<AccessibilityNode> {
440 if let Some(node) = self.nodes.get(&id) {
441 return node.clone();
442 }
443
444 let node = ArcRefCell::new(AccessibilityNode::new(id));
445 update.set_tree_state_change(id, TreeChange::New);
446 self.nodes.insert(id, node.clone());
447
448 node
449 }
450
451 fn node_for_id(&self, id: NodeId) -> Option<ArcRefCell<AccessibilityNode>> {
452 self.nodes.get(&id).cloned()
453 }
454
455 fn assert_node_for_id(&self, id: &NodeId) -> ArcRefCell<AccessibilityNode> {
456 let Some(node) = self.nodes.get(id) else {
457 panic!("{id:?} does not exist in tree");
458 };
459 node.clone()
460 }
461
462 fn node_for_opaque(&self, opaque: OpaqueNode) -> Option<ArcRefCell<AccessibilityNode>> {
463 self.nodes
464 .get(&self.existing_id_for_opaque(opaque)?)
465 .cloned()
466 }
467
468 fn drop_removed_nodes(
471 &mut self,
472 mut update: AccessibilityUpdate,
473 mut rooted_nodes_for_integrity_check: Option<FxHashSet<OpaqueNode>>,
474 ) {
475 if let Some(rooted_nodes) = rooted_nodes_for_integrity_check.as_mut() {
476 self.assert_removed_nodes_were_rooted(&update, rooted_nodes);
477 }
478
479 let mut ids_to_remove: Vec<_> = update
480 .tree_changes
481 .iter()
482 .filter_map(|(id, change)| match change {
483 TreeChange::Removed => Some(id),
484 TreeChange::PendingMove => None,
485 TreeChange::New => None,
486 TreeChange::Moved => None,
487 })
488 .cloned()
489 .collect();
490
491 while let Some(id) = ids_to_remove.pop() {
492 if update.tree_changes.get(&id) == Some(&TreeChange::PendingMove) {
493 update.set_tree_state_change(id, TreeChange::Removed);
495
496 continue;
498 }
499
500 if let Some(opaque_node) = self.id_to_opaque_node.remove(&id) {
501 self.opaque_node_to_id.remove(&opaque_node);
502 }
503 let node = self.nodes.remove(&id).expect("Node {id:?} already removed");
504 ids_to_remove.extend(node.borrow().child_ids());
505 }
506
507 update
508 .tree_changes
509 .drain()
510 .for_each(|(id, change)| match change {
511 TreeChange::PendingMove => unreachable!(
512 "Pending move found for node id {id:?} when draining tree state changes"
513 ),
514 TreeChange::Removed => (),
515 TreeChange::New => (),
516 TreeChange::Moved => (),
517 });
518
519 if let Some(rooted_nodes) = rooted_nodes_for_integrity_check {
520 self.assert_remaining_rooted_nodes_not_in_tree(rooted_nodes);
521 }
522
523 if self
524 .debug
525 .is_enabled(DiagnosticsLoggingOption::AccessibilityTree)
526 {
527 self.print();
528 }
529
530 if pref!(expensive_accessibility_test_assertions_enabled) {
531 self.assert_integrity();
532 }
533 }
534
535 fn assert_removed_nodes_were_rooted(
538 &mut self,
539 update: &AccessibilityUpdate,
540 rooted_nodes: &mut FxHashSet<OpaqueNode>,
541 ) {
542 debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
543 for (id, change) in update.tree_changes.iter() {
544 if change == &TreeChange::Removed {
545 let Some(&opaque_node) = self.id_to_opaque_node.get(id) else {
546 panic!("No opaque node found for removed node: id {id:?}");
547 };
548 assert!(
549 rooted_nodes.remove(&opaque_node),
550 "Node removed from accessibility tree wasn't rooted: id {id:?}"
551 );
552 };
553 }
554 }
555
556 fn assert_remaining_rooted_nodes_not_in_tree(&self, rooted_nodes: FxHashSet<OpaqueNode>) {
562 for leftover_node in rooted_nodes {
563 assert!(
564 !self.opaque_node_to_id.contains_key(&leftover_node),
565 "Found node removed from DOM tree but not accessibility tree: {:#x}",
566 leftover_node.0
567 );
568 }
569 }
570
571 fn get_or_create_id_for_opaque(&mut self, opaque: OpaqueNode) -> NodeId {
572 let id = self.opaque_node_to_id.entry(opaque).or_insert_with(|| {
573 static LAST_ID: AtomicU64 = AtomicU64::new(0);
574 let id = LAST_ID.fetch_add(1, atomic::Ordering::SeqCst).into();
575 self.id_to_opaque_node.insert(id, opaque);
576 id
577 });
578 *id
579 }
580
581 pub(super) fn existing_id_for_opaque(&self, opaque: OpaqueNode) -> Option<NodeId> {
582 self.opaque_node_to_id.get(&opaque).cloned()
583 }
584
585 pub(crate) fn embedder_epoch(&self) -> Epoch {
586 self.embedder_epoch
587 }
588
589 pub(crate) fn take_pending_actions(&mut self) -> Vec<AccessibilityActionRequest> {
590 std::mem::take(&mut self.pending_actions)
591 }
592
593 fn assert_integrity(&self) {
597 debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
598 let Some(root_node) = self.root_node.clone() else {
599 return;
600 };
601
602 let mut nodes = vec![(root_node, None)];
605 let mut seen_node_ids = FxHashSet::default();
606 while let Some((node, expected_parent)) = nodes.pop() {
607 let node = node.borrow();
608
609 assert!(
611 seen_node_ids.insert(node.id()),
612 "Tree contains {:?} in multiple places",
613 node.id()
614 );
615
616 node.assert_integrity(expected_parent);
617
618 let weak_node = Some(self.assert_node_for_id(&node.id()).downgrade());
621 nodes.extend(node.children().cloned().zip(repeat(weak_node)));
622 }
623
624 assert_eq!(seen_node_ids, self.nodes.keys().copied().collect());
627 }
628
629 fn print(&self) {
630 let Some(root_node) = self.root_node.clone() else {
631 return;
632 };
633
634 let mut print_tree = PrintTree::new("Accessibility Tree");
635 root_node.borrow().print(&mut print_tree, self);
636 print_tree.end_level();
637 }
638}
639
640impl<'update> AccessibilityUpdate<'update> {
641 fn new(dom_damage: AccessibilityDamageMap<'update>, tree: &AccessibilityTree) -> Self {
642 let damage_map = dom_damage
643 .iter()
644 .filter_map(|(&opaque, &(_dom_node, damage))| {
645 let id = tree.existing_id_for_opaque(opaque)?;
646 Some((id, damage))
647 })
648 .collect();
649 let dom_node_map = dom_damage
650 .into_iter()
651 .filter_map(|(opaque, (dom_node, _damage))| {
652 let id = tree.existing_id_for_opaque(opaque)?;
653 Some((id, dom_node))
654 })
655 .collect();
656 Self {
657 changed_nodes: FxHashSet::default(),
658 accesskit_tree: None,
659 tree_changes: FxHashMap::default(),
660 focused_node_changed: false,
661 counters: UpdateCounters::default(),
662 damage_map,
663 dom_node_map: RefCell::new(dom_node_map),
664 }
665 }
666
667 pub(super) fn add(&mut self, node: &mut AccessibilityNode) {
668 self.changed_nodes.insert(node.id());
669 node.remove_dirty_state(DirtyState::Updated);
670 }
671
672 pub(super) fn set_tree_state_change(&mut self, node_id: NodeId, change: TreeChange) {
673 let old_change = self.tree_changes.get(&node_id);
674
675 assert!(
676 change != TreeChange::Moved,
677 "Incoming change must never be Moved"
678 );
679
680 let resolved_change = old_change
681 .map(|old_change| match (old_change, change) {
682 (TreeChange::PendingMove, TreeChange::Removed) => TreeChange::Moved,
683 (TreeChange::Removed, TreeChange::PendingMove) => TreeChange::Moved,
684 _ => {
685 unreachable!("Logically impossible state change: {old_change:?} → {change:?}")
686 },
687 })
688 .unwrap_or(change);
689
690 self.tree_changes.insert(node_id, resolved_change);
691 }
692
693 pub(super) fn is_new(&mut self, node_id: &NodeId) -> bool {
694 self.tree_changes.get(node_id) == Some(&TreeChange::New)
695 }
696
697 fn finalize(
702 mut self,
703 tree: &mut AccessibilityTree,
704 rooted_nodes_for_integrity_check: Option<FxHashSet<OpaqueNode>>,
705 action_requests: Vec<ActionRequest>,
706 ) -> (Option<accesskit::TreeUpdate>, UpdateCounters) {
707 let mut tree_update = None;
708 let mut counters = std::mem::take(&mut self.counters);
709 if !self.changed_nodes.is_empty() ||
710 self.accesskit_tree.is_some() ||
711 self.focused_node_changed
712 {
713 let changed_nodes = std::mem::take(&mut self.changed_nodes);
714 let accesskit_tree = std::mem::take(&mut self.accesskit_tree);
715
716 tree.drop_removed_nodes(self, rooted_nodes_for_integrity_check);
717
718 let changed_nodes: Vec<_> = changed_nodes
719 .into_iter()
720 .filter_map(|id| Some((id, tree.node_for_id(id)?.borrow().clone_accesskit_node())))
721 .collect();
722
723 counters.nodes_in_tree_update = changed_nodes.len().try_into().unwrap_or_default();
724
725 let focus = tree.focused_node_id.unwrap_or(
726 tree.root_node
727 .as_ref()
728 .expect("Root node must be set")
729 .borrow()
730 .id(),
731 );
732 tree_update = Some(accesskit::TreeUpdate {
733 nodes: changed_nodes,
735 tree: accesskit_tree,
736 focus,
737 tree_id: tree.tree_id,
738 });
739 } else {
740 assert!(self.tree_changes.is_empty());
741 }
742
743 for action in action_requests {
744 assert_eq!(
745 action.target_tree, tree.tree_id,
746 "Got action with wrong tree ID: {action:?}"
747 );
748 let Some(&opaque) = tree.id_to_opaque_node.get(&action.target_node) else {
749 continue;
751 };
752 let dom_action_request = AccessibilityActionRequest {
753 action: action.action,
754 target: opaque,
755 data: action.data,
756 };
757 tree.pending_actions.push(dom_action_request);
758 }
759
760 (tree_update, counters)
761 }
762
763 fn clear_damage(&mut self) {
764 self.damage_map.clear();
765 }
766
767 fn insert_damage(&mut self, node_id: NodeId, damage: AccessibilityDamage) {
768 self.damage_map.insert(node_id, damage);
769 }
770
771 pub(super) fn insert_dom_node(&self, node_id: NodeId, dom_node: ServoLayoutNode<'update>) {
772 self.dom_node_map.borrow_mut().insert(node_id, dom_node);
773 }
774
775 pub(super) fn take_damage(&mut self, node_id: &NodeId) -> AccessibilityDamage {
776 self.damage_map
777 .remove(node_id)
778 .unwrap_or(AccessibilityDamage::empty())
779 }
780
781 pub(super) fn take_dom_node(&mut self, node_id: &NodeId) -> Option<ServoLayoutNode<'update>> {
782 self.dom_node_map.borrow_mut().remove(node_id)
783 }
784
785 #[expect(unsafe_code)]
786 fn collect_dom_node_ancestors(&self, node_id: &NodeId, tree: &AccessibilityTree) {
787 let mut dom_node_map = self.dom_node_map.borrow_mut();
788 let dom_node = dom_node_map
789 .get(node_id)
790 .expect("collect_dom_node_ancestors should be called for a known DOM node");
791 let mut parent = unsafe { dom_node.dangerous_flat_tree_parent() };
792 while let Some(node) = parent {
793 if let Some(node_id) = tree.existing_id_for_opaque(node.opaque()) {
794 dom_node_map.insert(node_id, node);
795 }
796 parent = unsafe { node.dangerous_flat_tree_parent() };
797 }
798 }
799}
800
801#[cfg(test)]
802#[test]
803fn test_accessibility_update_add_some_nodes_twice() {
804 let mut tree = AccessibilityTree::new(accesskit::TreeId::ROOT, Epoch::default());
805 let mut root_update = AccessibilityUpdate::new(AccessibilityDamageMap::default(), &tree);
806
807 let root_node = tree.get_or_create_node_with_id(NodeId(2), &mut root_update);
808 tree.root_node = Some(root_node.clone());
809
810 let nodes: Vec<_> = [
811 (3, Role::GenericContainer),
812 (4, Role::Heading),
813 (5, Role::Paragraph),
814 ]
815 .into_iter()
816 .map(|(id, role)| {
817 let id = NodeId(id);
818 let node = tree.get_or_create_node_with_id(id, &mut root_update);
819 node.borrow_mut().set_role(role);
820 (id, node)
821 })
822 .collect();
823
824 {
825 let (child_node_ids, child_nodes): (Vec<_>, Vec<_>) = nodes.iter().cloned().unzip();
826 let mut root_node = root_node.borrow_mut();
827 root_node.set_children_for_testing(child_node_ids, child_nodes);
828 }
829
830 let mut update = AccessibilityUpdate::new(AccessibilityDamageMap::default(), &tree);
831
832 {
833 let node_3 = tree.assert_node_for_id(&NodeId(3));
834 let mut node_3 = node_3.borrow_mut();
835 let node_4 = tree.assert_node_for_id(&NodeId(4));
836 let mut node_4 = node_4.borrow_mut();
837 let node_5 = tree.assert_node_for_id(&NodeId(5));
838 let mut node_5 = node_5.borrow_mut();
839
840 update.add(&mut node_5);
841 update.add(&mut node_3);
842 update.add(&mut node_4);
843 update.add(&mut node_4);
844
845 node_3.set_role(Role::ScrollView);
846 update.add(&mut node_3);
847 }
848
849 let (tree_update, _) = update.finalize(&mut tree, None, vec![]);
850 let mut tree_update = tree_update.expect("finalize should produce a tree update");
851 tree_update.nodes.sort_by_key(|(node_id, _node)| *node_id);
852 assert_eq!(
853 tree_update,
854 accesskit::TreeUpdate {
855 nodes: vec![
856 (NodeId(3), accesskit::Node::new(Role::ScrollView)),
857 (NodeId(4), accesskit::Node::new(Role::Heading)),
858 (NodeId(5), accesskit::Node::new(Role::Paragraph)),
859 ],
860 tree: None,
861 tree_id: accesskit::TreeId::ROOT,
862 focus: NodeId(2),
863 }
864 );
865}