fltk_accesskit/
lib.rs

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        // Move context into the handler, using a cloned root to register the closure.
114        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    // Traverse children of root
146    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            // If the child is excluded and it's a group, skip its entire subtree.
160            // If it's excluded and not a group, just skip the node.
161            let subgrp = child.as_group();
162            if excludes.matches(&child) {
163                // For groups this prevents iterating children.
164                continue;
165            }
166            // Add node(s) if supported (some widgets expand to multiple nodes)
167            let nodes = crate::accessible::nodes_for_widget(&child);
168            out.extend(nodes);
169            // Recurse into groups that weren't excluded
170            if let Some(subgrp) = subgrp {
171                walk_group(&subgrp, excludes, out);
172            }
173        }
174    }
175}