1#![doc = include_str!("../README.md")]
2
3use accesskit::{NodeId, TreeUpdate};
4use fltk::{enums::*, prelude::*, widget, *};
5use std::collections::HashSet;
6type ExcludePred = Box<dyn Fn(&widget::Widget) -> bool>;
7
8pub mod accessible;
9mod fltk_adapter;
10mod platform_adapter;
11
12pub use accessible::Accessible;
13pub use fltk_adapter::Adapter;
14
15#[derive(Default)]
16pub struct Excludes {
17 ptrs: HashSet<u64>,
18 subtree_ptrs: HashSet<u64>,
19 preds: Vec<ExcludePred>,
20}
21
22impl Excludes {
23 fn matches(&self, w: &widget::Widget) -> bool {
24 let p = w.as_widget_ptr() as usize as u64;
25 if self.ptrs.contains(&p) {
26 return true;
27 }
28 for f in &self.preds {
29 if f(w) {
30 return true;
31 }
32 }
33 false
34 }
35 fn skip_subtree(&self, w: &widget::Widget) -> bool {
36 let p = w.as_widget_ptr() as usize as u64;
37 self.subtree_ptrs.contains(&p)
38 }
39}
40
41pub struct AccessibilityBuilder {
42 root: window::Window,
43 excludes: Excludes,
44}
45
46impl AccessibilityBuilder {
47 pub fn new(root: window::Window) -> Self {
48 Self { root, excludes: Excludes::default() }
49 }
50 pub fn exclude_widget<W: WidgetExt>(mut self, w: &W) -> Self {
51 self.excludes.ptrs.insert(w.as_widget_ptr() as usize as u64);
52 self
53 }
54 pub fn exclude_subtree<W: WidgetExt>(mut self, w: &W) -> Self {
55 self.excludes
56 .subtree_ptrs
57 .insert(w.as_widget_ptr() as usize as u64);
58 self
59 }
60 pub fn exclude_type<T: WidgetBase>(mut self) -> Self {
61 self.excludes
62 .preds
63 .push(Box::new(|w: &widget::Widget| fltk::utils::is_ptr_of::<T>(w.as_widget_ptr())));
64 self
65 }
66 pub fn exclude_if(mut self, pred: impl Fn(&widget::Widget) -> bool + 'static) -> Self {
67 self.excludes.preds.push(Box::new(pred));
68 self
69 }
70 pub fn attach(self) -> AccessibilityContext {
71 let mut wids = collect_nodes(&self.root, &self.excludes);
72 let (win_id, win_node) = self
73 .root
74 .make_node(&wids.iter().map(|x| x.0).collect::<Vec<_>>());
75 wids.push((win_id, win_node));
76 let activation_handler = crate::fltk_adapter::FltkActivationHandler { wids, win_id };
77 let adapter = Adapter::new(&self.root, activation_handler);
78 AccessibilityContext {
79 adapter,
80 root: self.root,
81 excludes: self.excludes,
82 }
83 }
84}
85
86pub fn builder(root: window::Window) -> AccessibilityBuilder {
87 AccessibilityBuilder::new(root)
88}
89
90pub struct AccessibilityContext {
91 adapter: Adapter,
92 root: window::Window,
93 excludes: Excludes,
94}
95
96impl AccessibilityContext {
97 fn collect(&self) -> Vec<(NodeId, accesskit::Node)> {
98 let mut wids = collect_nodes(&self.root, &self.excludes);
99 let (win_id, win_node) = self
100 .root
101 .make_node(&wids.iter().map(|x| x.0).collect::<Vec<_>>());
102 wids.push((win_id, win_node));
103 wids
104 }
105}
106
107pub trait AccessibleApp {
108 fn run_with_accessibility(&self, ac: AccessibilityContext) -> Result<(), FltkError>;
109}
110
111impl AccessibleApp for app::App {
112 fn run_with_accessibility(&self, ac: AccessibilityContext) -> Result<(), FltkError> {
113 let ctx = ac;
115 let mut root = ctx.root.clone();
116 let mut adapter = ctx.adapter.clone();
117 root.handle({
118 move |_, ev| {
119 match ev {
120 Event::KeyUp => {
121 let wids = ctx.collect();
122 if let Some(focused) = fltk::app::focus() {
123 let node_id = NodeId(focused.as_widget_ptr() as _);
124 adapter.update_if_active(|| TreeUpdate {
125 nodes: wids,
126 tree: None,
127 focus: node_id,
128 });
129 }
130 false
131 }
132 _ => false,
133 }
134 }
135 });
136 self.run()
137 }
138}
139
140fn collect_nodes(
141 root: &window::Window,
142 excludes: &Excludes,
143) -> Vec<(NodeId, accesskit::Node)> {
144 let mut out = Vec::new();
145 let root_w = root.as_base_widget();
147 if let Some(grp) = root_w.as_group() {
148 walk_group(&grp, excludes, &mut out);
149 }
150 out
151}
152
153fn walk_group(grp: &group::Group, excludes: &Excludes, out: &mut Vec<(NodeId, accesskit::Node)>) {
154 for i in 0..grp.children() {
155 if let Some(child) = grp.child(i) {
156 if excludes.skip_subtree(&child) {
157 continue;
158 }
159 let subgrp = child.as_group();
162 if excludes.matches(&child) {
163 continue;
165 }
166 let nodes = crate::accessible::nodes_for_widget(&child);
168 out.extend(nodes);
169 if let Some(subgrp) = subgrp {
171 walk_group(&subgrp, excludes, out);
172 }
173 }
174 }
175}