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