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 {
49 root,
50 excludes: Excludes::default(),
51 }
52 }
53 pub fn exclude_widget<W: WidgetExt>(mut self, w: &W) -> Self {
54 self.excludes.ptrs.insert(w.as_widget_ptr() as usize as u64);
55 self
56 }
57 pub fn exclude_subtree<W: WidgetExt>(mut self, w: &W) -> Self {
58 self.excludes
59 .subtree_ptrs
60 .insert(w.as_widget_ptr() as usize as u64);
61 self
62 }
63 pub fn exclude_type<T: WidgetBase>(mut self) -> Self {
64 self.excludes.preds.push(Box::new(|w: &widget::Widget| {
65 fltk::utils::is_ptr_of::<T>(w.as_widget_ptr())
66 }));
67 self
68 }
69 pub fn exclude_if(mut self, pred: impl Fn(&widget::Widget) -> bool + 'static) -> Self {
70 self.excludes.preds.push(Box::new(pred));
71 self
72 }
73 pub fn attach(self) -> AccessibilityContext {
74 let mut wids = collect_nodes(&self.root, &self.excludes);
75 let (win_id, win_node) = self
76 .root
77 .make_node(&wids.iter().map(|x| x.0).collect::<Vec<_>>());
78 wids.push((win_id, win_node));
79 let activation_handler = crate::fltk_adapter::FltkActivationHandler { wids, win_id };
80 let adapter = Adapter::new(&self.root, activation_handler);
81 AccessibilityContext {
82 adapter,
83 root: self.root,
84 excludes: self.excludes,
85 }
86 }
87}
88
89pub fn builder(root: window::Window) -> AccessibilityBuilder {
90 AccessibilityBuilder::new(root)
91}
92
93pub struct AccessibilityContext {
94 adapter: Adapter,
95 root: window::Window,
96 excludes: Excludes,
97}
98
99impl AccessibilityContext {
100 fn collect(&self) -> Vec<(NodeId, accesskit::Node)> {
101 let mut wids = collect_nodes(&self.root, &self.excludes);
102 let (win_id, win_node) = self
103 .root
104 .make_node(&wids.iter().map(|x| x.0).collect::<Vec<_>>());
105 wids.push((win_id, win_node));
106 wids
107 }
108}
109
110pub trait AccessibleApp {
111 fn run_with_accessibility(&self, ac: AccessibilityContext) -> Result<(), FltkError>;
112}
113
114pub fn update_focused(ac: &AccessibilityContext) {
115 let mut adapter = ac.adapter.clone();
116 let wids = ac.collect();
117 if let Some(focused) = fltk::app::focus() {
118 let node_id = NodeId(focused.as_widget_ptr() as _);
119 adapter.update_if_active(|| TreeUpdate {
120 nodes: wids,
121 tree: None,
122 focus: node_id,
123 });
124 }
125}
126
127impl AccessibleApp for app::App {
128 fn run_with_accessibility(&self, ac: AccessibilityContext) -> Result<(), FltkError> {
129 let ctx = ac;
131 let mut root = ctx.root.clone();
132 root.handle({
133 move |_, ev| match ev {
134 Event::KeyUp => {
135 update_focused(&ctx);
136 false
137 }
138 _ => false,
139 }
140 });
141 self.run()
142 }
143}
144
145fn collect_nodes(root: &window::Window, excludes: &Excludes) -> Vec<(NodeId, accesskit::Node)> {
146 let mut out = Vec::new();
147 let root_w = root.as_base_widget();
149 if let Some(grp) = root_w.as_group() {
150 walk_group(&grp, excludes, &mut out);
151 }
152 out
153}
154
155fn walk_group(grp: &group::Group, excludes: &Excludes, out: &mut Vec<(NodeId, accesskit::Node)>) {
156 for i in 0..grp.children() {
157 if let Some(child) = grp.child(i) {
158 if excludes.skip_subtree(&child) {
159 continue;
160 }
161 let subgrp = child.as_group();
164 if excludes.matches(&child) {
165 continue;
167 }
168 let nodes = crate::accessible::nodes_for_widget(&child);
170 out.extend(nodes);
171 if let Some(subgrp) = subgrp {
173 walk_group(&subgrp, excludes, out);
174 }
175 }
176 }
177}