use std::collections::HashSet;
use accesskit::{NodeId, Role, TreeUpdate};
use accesskit_consumer::{FilterResult, NodeRef, Tree, TreeChangeHandler, common_filter};
use teksilo_canvas::SizeProposal;
use crate::accessibility::AccessNodeBuilder;
use crate::signal::Signal;
use crate::test_widgets::{FillWidget, StackWidget};
use crate::widget::{LayoutContext, LayoutResponse, Widget};
use crate::widget_builder::WidgetBuilder;
use crate::widget_id::WidgetId;
use crate::widget_tree::WidgetTree;
fn handed_to_adapter(tree: &mut WidgetTree, window_focused: bool) -> TreeUpdate {
let _ = tree.sync_accessibility();
tree.deliver_accessibility(window_focused)
}
struct Adapter {
replay: Tree,
defunct: HashSet<NodeId>,
window_focused: bool,
}
#[derive(Debug, Default)]
struct Frame {
revived: Vec<(NodeId, String)>,
focus: Option<(String, NodeId, bool)>,
}
struct Rules<'a> {
defunct: &'a mut HashSet<NodeId>,
frame: Frame,
}
fn included(node: &NodeRef) -> bool {
common_filter(node) == FilterResult::Include
}
fn name(node: &NodeRef) -> String {
node.label().unwrap_or_default()
}
impl Rules<'_> {
fn add(&mut self, node: &NodeRef) {
let id = node.locate().0;
if self.defunct.contains(&id) {
self.frame.revived.push((id, name(node)));
}
}
fn add_subtree(&mut self, node: &NodeRef) {
self.add(node);
for child in node.filtered_children(&common_filter) {
self.add_subtree(&child);
}
}
fn remove(&mut self, node: &NodeRef) {
self.defunct.insert(node.locate().0);
}
fn remove_subtree(&mut self, node: &NodeRef) {
for child in node.filtered_children(&common_filter) {
self.remove_subtree(&child);
}
self.remove(node);
}
}
impl TreeChangeHandler for Rules<'_> {
fn node_added(&mut self, node: &NodeRef) {
if included(node) {
self.add(node);
}
}
fn node_updated(&mut self, old: &NodeRef, new: &NodeRef) {
let (was, is) = (common_filter(old), common_filter(new));
if was == is {
return;
}
if is == FilterResult::Include {
if was == FilterResult::ExcludeSubtree {
self.add_subtree(new);
} else {
self.add(new);
}
} else if was == FilterResult::Include {
if is == FilterResult::ExcludeSubtree {
self.remove_subtree(old);
} else {
self.remove(old);
}
}
}
fn focus_moved(&mut self, _old: Option<&NodeRef>, new: Option<&NodeRef>) {
if let Some(node) = new {
let id = node.locate().0;
self.frame.focus = Some((name(node), id, self.defunct.contains(&id)));
}
}
fn node_removed(&mut self, node: &NodeRef) {
if included(node) {
self.remove(node);
}
}
}
impl Adapter {
fn attach(tree: &mut WidgetTree) -> Self {
tree.layout(SizeProposal::exact(400.0, 300.0));
Self {
replay: Tree::new(handed_to_adapter(tree, true), true),
defunct: HashSet::new(),
window_focused: true,
}
}
fn frame(&mut self, tree: &mut WidgetTree) -> Frame {
tree.layout(SizeProposal::exact(400.0, 300.0));
let update = handed_to_adapter(tree, self.window_focused);
let mut rules = Rules {
defunct: &mut self.defunct,
frame: Frame::default(),
};
self.replay.update_and_process_changes(update, &mut rules);
rules.frame
}
fn window_focus(&mut self, focused: bool) -> Frame {
self.window_focused = focused;
let mut rules = Rules {
defunct: &mut self.defunct,
frame: Frame::default(),
};
self.replay
.update_host_focus_state_and_process_changes(focused, &mut rules);
rules.frame
}
fn id_of(&self, text: &str) -> Option<NodeId> {
fn walk(node: NodeRef<'_>, text: &str) -> Option<NodeId> {
if node.label().as_deref() == Some(text) {
return Some(node.locate().0);
}
node.filtered_children(&common_filter)
.find_map(|child| walk(child, text))
}
walk(self.replay.state().root(), text)
}
}
#[track_caller]
fn assert_heard(frame: &Frame, what: &str, expected_focus: &str) {
assert!(
frame.revived.is_empty(),
"{what}: the adapter was handed {:?} again after it had removed them; libatspi holds \
each for defunct and Orca drops its events",
frame.revived
);
match &frame.focus {
Some((name, id, defunct)) => {
assert_eq!(name, expected_focus, "{what}: focus went elsewhere");
assert!(
!defunct,
"{what}: focus landed on {name:?} under #{}, an id the adapter had already \
removed, so Orca drops it ('Ignoring defunct object') and says nothing",
id.0
);
}
None => panic!("{what}: focus never reached {expected_focus:?}"),
}
}
fn button(label: &str) -> impl Widget + 'static {
FillWidget::new()
.access_role(Role::Button)
.access_label(label.to_string())
.focusable(true)
}
#[test]
fn a_page_shown_again_comes_back_under_ids_the_adapter_never_removed() {
let mut tree = WidgetTree::new();
let username = tree.add(
FillWidget::new()
.access_role(Role::TextInput)
.access_label("Username")
.focusable(true),
);
let page = tree.add(StackWidget::new().child(username));
let tab = tree.add(button("Scene"));
tree.focus(username);
let mut adapter = Adapter::attach(&mut tree);
for round in 1..=3 {
tree.focus(tab);
tree.set_dormant(page);
let _ = adapter.frame(&mut tree);
assert_eq!(
adapter.id_of("Username"),
None,
"the hidden page left the tree"
);
tree.activate(page);
let back = adapter.frame(&mut tree);
assert!(
back.revived.is_empty(),
"round {round}: the page came back under ids the adapter had removed: {:?}",
back.revived
);
tree.focus(username);
let focus = adapter.frame(&mut tree);
assert_heard(&focus, &format!("round {round}"), "Username");
}
}
#[derive(Debug)]
struct Viewport {
offset: Signal<f32>,
child: Option<WidgetId>,
}
impl Widget for Viewport {
fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
self.offset.bind_to(
ctx.self_id(),
ctx.binding_registry(),
crate::binding::BindingLevel::Relayout,
);
let child = ctx.add(button("Bar chart"));
self.child = Some(child);
vec![child]
}
fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
teksilo_canvas::Size::new(200.0, 100.0).into()
}
fn children(&self) -> Vec<WidgetId> {
self.child.into_iter().collect()
}
fn place_children(
&self,
bounds: teksilo_canvas::Rect,
_proposal: SizeProposal,
children: &mut [crate::widget::WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = teksilo_canvas::Point::new(bounds.x, bounds.y + self.offset.get());
child.size = teksilo_canvas::Size::new(80.0, 20.0);
}
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(Role::ScrollView);
builder.set_name("Charts");
builder.inner_mut().set_clips_children();
}
}
#[test]
fn a_control_scrolled_back_into_view_is_a_live_object() {
let offset = Signal::new(10.0_f32);
let mut tree = WidgetTree::new();
let viewport = tree.add(Viewport {
offset: offset.clone(),
child: None,
});
let other = tree.add(button("Donut"));
tree.layout(SizeProposal::exact(400.0, 300.0));
let bar = tree.arena.children(viewport)[0];
tree.focus(bar);
let mut adapter = Adapter::attach(&mut tree);
tree.focus(other);
let _ = adapter.frame(&mut tree);
offset.set(400.0);
let _ = adapter.frame(&mut tree);
assert_eq!(
adapter.id_of("Bar chart"),
None,
"scrolled out, the chart left the tree"
);
offset.set(10.0);
let back = adapter.frame(&mut tree);
assert!(
back.revived.is_empty(),
"the chart scrolled back under an id the adapter had removed: {:?}",
back.revived
);
tree.focus(bar);
let focus = adapter.frame(&mut tree);
assert_heard(&focus, "Shift+Tab back to the chart", "Bar chart");
}
#[test]
fn a_subtree_hidden_and_shown_again_comes_back_live() {
let hidden = Signal::new(false);
let mut tree = WidgetTree::new();
let save = tree.add(button("Save"));
let _panel = tree.add(StackWidget::new().child(save).access_hidden(hidden.clone()));
let other = tree.add(button("Close"));
tree.focus(save);
let mut adapter = Adapter::attach(&mut tree);
tree.focus(other);
let _ = adapter.frame(&mut tree);
hidden.set(true);
let _ = adapter.frame(&mut tree);
assert_eq!(
adapter.id_of("Save"),
None,
"the hidden panel left the tree"
);
hidden.set(false);
let back = adapter.frame(&mut tree);
assert!(
back.revived.is_empty(),
"the panel came back under ids the adapter had removed: {:?}",
back.revived
);
tree.focus(save);
let focus = adapter.frame(&mut tree);
assert_heard(&focus, "focus back on Save", "Save");
}
#[derive(Debug)]
struct Wrapper;
impl Widget for Wrapper {
fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
Vec::new()
}
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
proposal.resolve(100.0, 40.0).into()
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(Role::GenericContainer);
}
}
#[test]
fn a_container_that_is_in_the_tree_only_while_focused_comes_back_live() {
let mut tree = WidgetTree::new();
let editor = tree.add(Wrapper);
let divider = tree.add(button("Splitter divider"));
tree.focus(editor);
let mut adapter = Adapter::attach(&mut tree);
for round in 1..=2 {
tree.focus(divider);
let _ = adapter.frame(&mut tree);
tree.focus(editor);
let back = adapter.frame(&mut tree);
assert_heard(&back, &format!("round {round}, back into the editor"), "");
}
}
#[test]
fn a_focused_container_comes_back_live_when_the_window_is_refocused() {
let mut tree = WidgetTree::new();
let editor = tree.add(Wrapper);
tree.add(button("Splitter divider"));
tree.focus(editor);
let mut adapter = Adapter::attach(&mut tree);
let _ = adapter.frame(&mut tree);
let _ = adapter.window_focus(false);
let _ = adapter.frame(&mut tree);
let back = adapter.window_focus(true);
assert!(
back.revived.is_empty(),
"the editor came back under an id the adapter had removed as the window lost focus: {:?}",
back.revived
);
}
#[test]
fn a_focused_control_that_left_while_the_window_was_away_comes_back_live() {
let offset = Signal::new(10.0_f32);
let mut tree = WidgetTree::new();
let viewport = tree.add(Viewport {
offset: offset.clone(),
child: None,
});
tree.layout(SizeProposal::exact(400.0, 300.0));
let bar = tree.arena.children(viewport)[0];
tree.focus(bar);
let mut adapter = Adapter::attach(&mut tree);
let _ = adapter.window_focus(false);
let _ = adapter.frame(&mut tree);
offset.set(400.0);
let _ = adapter.frame(&mut tree);
let back = adapter.window_focus(true);
assert_heard(
&back,
"back to the window, the chart scrolled out",
"Bar chart",
);
let hidden = Signal::new(false);
let mut tree = WidgetTree::new();
let save = tree.add(button("Save"));
tree.add(StackWidget::new().child(save).access_hidden(hidden.clone()));
tree.focus(save);
let mut adapter = Adapter::attach(&mut tree);
let _ = adapter.window_focus(false);
let _ = adapter.frame(&mut tree);
hidden.set(true);
let _ = adapter.frame(&mut tree);
let back = adapter.window_focus(true);
assert_heard(&back, "back to the window, the panel hidden", "Save");
}
#[test]
fn an_action_on_a_control_that_came_back_reaches_it() {
let mut tree = WidgetTree::new();
let username = tree.add(
FillWidget::new()
.access_role(Role::TextInput)
.access_label("Username")
.focusable(true),
);
let page = tree.add(StackWidget::new().child(username));
tree.add(button("Scene"));
let mut adapter = Adapter::attach(&mut tree);
tree.set_dormant(page);
let _ = adapter.frame(&mut tree);
tree.activate(page);
let _ = adapter.frame(&mut tree);
let handed = adapter.id_of("Username").expect("the page is back");
let own = crate::accessibility::widget_id_to_node_id(username);
assert_ne!(handed, own, "the field came back under a new id");
let request = tree
.resolve_adapter_action(accesskit::ActionRequest {
action: accesskit::Action::Focus,
target_tree: accesskit::TreeId::ROOT,
target_node: handed,
data: None,
})
.expect("the id names the field");
assert_eq!(request.target_node, own);
}