fltk_accesskit/fltk_adapter.rs
1#![allow(unused_imports)]
2#![allow(unused_variables)]
3use accesskit::{
4 Action, ActionData, ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler,
5 Node, NodeId, Point, Rect, Size, Tree, TreeUpdate,
6};
7use fltk::{
8 button, enums::*, input, misc, prelude::*, text, utils, valuator, widget, *,
9};
10use std::cell::RefCell;
11use std::rc::Rc;
12
13use crate::platform_adapter;
14
15pub(crate) struct FltkActivationHandler {
16 pub wids: Vec<(NodeId, Node)>,
17 pub win_id: NodeId,
18}
19
20impl ActivationHandler for FltkActivationHandler {
21 fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
22 Some(TreeUpdate {
23 nodes: self.wids.clone(),
24 tree: Some(Tree::new(self.win_id)),
25 focus: if let Some(focused) = app::focus() {
26 let focused = focused.as_widget_ptr() as usize as u64;
27 NodeId(focused)
28 } else {
29 self.win_id
30 },
31 })
32 }
33}
34
35pub(crate) struct FltkActionHandler {
36 tx: app::Sender<ActionRequest>,
37}
38
39impl ActionHandler for FltkActionHandler {
40 fn do_action(&mut self, request: ActionRequest) {
41 self.tx.send(request);
42 app::awake();
43 }
44}
45
46pub(crate) struct FltkDeactivationHandler {}
47
48impl DeactivationHandler for FltkDeactivationHandler {
49 fn deactivate_accessibility(&mut self) {}
50}
51
52#[derive(Clone)]
53pub struct Adapter {
54 adapter: Rc<RefCell<platform_adapter::Adapter>>,
55}
56
57impl Adapter {
58 pub fn new(window: &window::Window, source: impl 'static + ActivationHandler + Send) -> Self {
59 let (tx, rx) = app::channel::<ActionRequest>();
60 let action_handler = FltkActionHandler { tx };
61 let this = Self::with_action_handler(window, source, action_handler);
62 // Drain action requests on the UI thread when awakened.
63 let rx = Rc::new(RefCell::new(rx));
64 app::awake_callback({
65 let rx = rx.clone();
66 move || {
67 while let Some(req) = rx.borrow_mut().recv() {
68 unsafe {
69 let mut w = widget::Widget::from_widget_ptr(req.target.0 as _);
70 match req.action {
71 Action::Click => {
72 w.do_callback();
73 }
74 Action::Focus => {
75 let _ = w.take_focus();
76 }
77 Action::ReplaceSelectedText => {
78 if let Some(ActionData::Value(s)) = req.data.clone() {
79 // TextEditor: operate on buffer
80 if utils::is_ptr_of::<text::TextEditor>(w.as_widget_ptr()) {
81 let mut e =
82 text::TextEditor::from_widget_ptr(w.as_widget_ptr() as _);
83 if let Some(mut buf) = e.buffer() {
84 if let Some((start, end)) = buf.selection_position() {
85 if start != end {
86 buf.replace(start, end, &s);
87 e.set_insert_position(start + s.len() as i32);
88 } else {
89 let pos = e.insert_position();
90 buf.insert(pos, &s);
91 e.set_insert_position(pos + s.len() as i32);
92 }
93 } else {
94 let pos = e.insert_position();
95 buf.insert(pos, &s);
96 e.set_insert_position(pos + s.len() as i32);
97 }
98 }
99 // Input family
100 } else if utils::is_ptr_of::<input::Input>(w.as_widget_ptr())
101 || utils::is_ptr_of::<input::IntInput>(w.as_widget_ptr())
102 || utils::is_ptr_of::<input::FloatInput>(w.as_widget_ptr())
103 || utils::is_ptr_of::<input::MultilineInput>(w.as_widget_ptr())
104 {
105 let mut i = input::Input::from_widget_ptr(w.as_widget_ptr() as _);
106 let start = i.position();
107 let end = i.mark();
108 if start != end {
109 let _ = i.replace(start, end, &s);
110 let _ = i.set_position(start + s.len() as i32);
111 let _ = i.set_mark(start + s.len() as i32);
112 } else {
113 let _ = i.insert(&s);
114 let new_pos = start + s.len() as i32;
115 let _ = i.set_position(new_pos);
116 let _ = i.set_mark(new_pos);
117 }
118 }
119 }
120 }
121 Action::ScrollIntoView => {
122 // Best effort: for TextEditor, ensure caret is visible
123 if utils::is_ptr_of::<text::TextEditor>(w.as_widget_ptr()) {
124 let mut e = text::TextEditor::from_widget_ptr(w.as_widget_ptr() as _);
125 e.show_insert_position();
126 }
127 }
128 Action::ScrollToPoint => {
129 // No robust XY->position mapping for editors; best effort noop.
130 // Could be extended for specific widgets/containers.
131 }
132 Action::SetTextSelection => {
133 if let Some(ActionData::SetTextSelection(sel)) = req.data.clone() {
134 // Only apply when selection nodes target this widget
135 if sel.anchor.node == req.target && sel.focus.node == req.target {
136 // TextEditor path
137 if utils::is_ptr_of::<text::TextEditor>(w.as_widget_ptr()) {
138 let mut e =
139 text::TextEditor::from_widget_ptr(w.as_widget_ptr() as _);
140 let mut buf = if let Some(b) = e.buffer() {
141 b
142 } else {
143 let b = text::TextBuffer::default();
144 e.set_buffer(Some(b));
145 e.buffer().unwrap()
146 };
147 let len = buf.length();
148 let mut a = sel.anchor.character_index as i32;
149 let mut f = sel.focus.character_index as i32;
150 a = a.clamp(0, len);
151 f = f.clamp(0, len);
152 if a == f {
153 // Caret move
154 buf.unselect();
155 e.set_insert_position(a);
156 } else {
157 let (start, end) = if a <= f { (a, f) } else { (f, a) };
158 buf.select(start, end);
159 e.set_insert_position(end);
160 }
161 // Input family path
162 } else if utils::is_ptr_of::<input::Input>(w.as_widget_ptr())
163 || utils::is_ptr_of::<input::IntInput>(w.as_widget_ptr())
164 || utils::is_ptr_of::<input::FloatInput>(w.as_widget_ptr())
165 || utils::is_ptr_of::<input::MultilineInput>(w.as_widget_ptr())
166 {
167 let mut i = input::Input::from_widget_ptr(w.as_widget_ptr() as _);
168 let len = i.value().len() as i32;
169 let mut a = sel.anchor.character_index as i32;
170 let mut f = sel.focus.character_index as i32;
171 a = a.clamp(0, len);
172 f = f.clamp(0, len);
173 let (start, end) = if a <= f { (a, f) } else { (f, a) };
174 // Set selection; on collapse, mark==position
175 let _ = i.set_position(start);
176 let _ = i.set_mark(end);
177 }
178 }
179 }
180 }
181 Action::Expand => {
182 // Expand menu-like controls
183 if utils::is_ptr_of::<menu::MenuButton>(w.as_widget_ptr()) {
184 let mb = menu::MenuButton::from_widget_ptr(w.as_widget_ptr() as _);
185 // Show popup; user may select an item, which will close automatically
186 let _ = mb.popup();
187 }
188 // Choice and others: no standard programmatic popup; noop.
189 }
190 Action::Collapse => {
191 // No-op: popups close automatically after selection
192 }
193 Action::SetValue => {
194 if let Some(data) = req.data {
195 match data {
196 ActionData::Value(s) => {
197 // Choice (by label)
198 if utils::is_ptr_of::<menu::Choice>(w.as_widget_ptr()) {
199 let mut c = menu::Choice::from_widget_ptr(w.as_widget_ptr() as _);
200 let idx = c.find_index(&s);
201 if idx >= 0 {
202 c.set_value(idx);
203 }
204 }
205 // Text-capable inputs
206 if utils::is_ptr_of::<input::IntInput>(w.as_widget_ptr()) {
207 let mut i = input::IntInput::from_widget_ptr(w.as_widget_ptr() as _);
208 i.set_value(&s);
209 } else if utils::is_ptr_of::<input::FloatInput>(w.as_widget_ptr()) {
210 let mut i = input::FloatInput::from_widget_ptr(w.as_widget_ptr() as _);
211 i.set_value(&s);
212 } else if utils::is_ptr_of::<input::MultilineInput>(w.as_widget_ptr()) {
213 let mut i =
214 input::MultilineInput::from_widget_ptr(w.as_widget_ptr() as _);
215 i.set_value(&s);
216 } else if utils::is_ptr_of::<input::Input>(w.as_widget_ptr()) {
217 let mut i = input::Input::from_widget_ptr(w.as_widget_ptr() as _);
218 i.set_value(&s);
219 } else if utils::is_ptr_of::<text::TextEditor>(w.as_widget_ptr()) {
220 let mut e = text::TextEditor::from_widget_ptr(w.as_widget_ptr() as _);
221 if let Some(mut buf) = e.buffer() {
222 buf.set_text(&s);
223 } else {
224 let mut buf = text::TextBuffer::default();
225 buf.set_text(&s);
226 e.set_buffer(Some(buf));
227 }
228 // Toggle/Check buttons (boolean from string)
229 } else if utils::is_ptr_of::<button::CheckButton>(w.as_widget_ptr()) {
230 let mut b = button::CheckButton::from_widget_ptr(
231 w.as_widget_ptr() as _,
232 );
233 let on = matches!(
234 s.to_ascii_lowercase().as_str(),
235 "1" | "true" | "on" | "yes"
236 );
237 b.set_value(on);
238 } else if utils::is_ptr_of::<button::ToggleButton>(w.as_widget_ptr()) {
239 let mut b = button::ToggleButton::from_widget_ptr(
240 w.as_widget_ptr() as _,
241 );
242 let on = matches!(
243 s.to_ascii_lowercase().as_str(),
244 "1" | "true" | "on" | "yes"
245 );
246 b.set_value(on);
247 // Valuators (parse string -> f64)
248 } else if let Ok(n) = s.parse::<f64>() {
249 macro_rules! set_val {
250 ($t:ty) => {{
251 if utils::is_ptr_of::<$t>(w.as_widget_ptr()) {
252 let mut v = <$t>::from_widget_ptr(w.as_widget_ptr() as _);
253 v.set_value(n);
254 true
255 } else {
256 false
257 }
258 }};
259 }
260 let _handled =
261 set_val!(valuator::Slider)
262 || set_val!(valuator::NiceSlider)
263 || set_val!(valuator::Dial)
264 || set_val!(valuator::LineDial)
265 || set_val!(valuator::Counter)
266 || set_val!(valuator::Scrollbar)
267 || set_val!(valuator::ValueInput)
268 || set_val!(valuator::ValueOutput)
269 || set_val!(valuator::ValueSlider)
270 || set_val!(valuator::HorValueSlider)
271 || set_val!(valuator::HorSlider)
272 || set_val!(valuator::HorNiceSlider)
273 || set_val!(valuator::FillSlider)
274 || set_val!(valuator::HorFillSlider)
275 || set_val!(misc::Spinner)
276 || set_val!(misc::Progress);
277 // else: fallback noop
278 }
279 }
280 ActionData::NumericValue(n) => {
281 // Choice (by index)
282 if utils::is_ptr_of::<menu::Choice>(w.as_widget_ptr()) {
283 let mut c = menu::Choice::from_widget_ptr(w.as_widget_ptr() as _);
284 let total = c.size();
285 let mut idx = n.round() as i32;
286 if idx < 0 { idx = 0; }
287 if idx >= total { idx = total - 1; }
288 if total > 0 {
289 c.set_value(idx);
290 }
291 }
292 // Inputs (apply rounding for IntInput)
293 if utils::is_ptr_of::<input::IntInput>(w.as_widget_ptr()) {
294 let mut i = input::IntInput::from_widget_ptr(w.as_widget_ptr() as _);
295 i.set_value(&format!("{}", n.round() as i64));
296 } else if utils::is_ptr_of::<input::FloatInput>(w.as_widget_ptr()) {
297 let mut i = input::FloatInput::from_widget_ptr(w.as_widget_ptr() as _);
298 i.set_value(&format!("{}", n));
299 } else if utils::is_ptr_of::<input::MultilineInput>(w.as_widget_ptr()) {
300 let mut i =
301 input::MultilineInput::from_widget_ptr(w.as_widget_ptr() as _);
302 i.set_value(&format!("{}", n));
303 } else if utils::is_ptr_of::<input::Input>(w.as_widget_ptr()) {
304 let mut i = input::Input::from_widget_ptr(w.as_widget_ptr() as _);
305 i.set_value(&format!("{}", n));
306 } else if utils::is_ptr_of::<text::TextEditor>(w.as_widget_ptr()) {
307 let mut e = text::TextEditor::from_widget_ptr(w.as_widget_ptr() as _);
308 let s = format!("{}", n);
309 if let Some(mut buf) = e.buffer() {
310 buf.set_text(&s);
311 } else {
312 let mut buf = text::TextBuffer::default();
313 buf.set_text(&s);
314 e.set_buffer(Some(buf));
315 }
316 // Toggle/Check buttons (numeric → bool)
317 } else if utils::is_ptr_of::<button::CheckButton>(w.as_widget_ptr()) {
318 let mut b = button::CheckButton::from_widget_ptr(
319 w.as_widget_ptr() as _,
320 );
321 b.set_value(n != 0.0);
322 } else if utils::is_ptr_of::<button::ToggleButton>(w.as_widget_ptr()) {
323 let mut b = button::ToggleButton::from_widget_ptr(
324 w.as_widget_ptr() as _,
325 );
326 b.set_value(n != 0.0);
327 // Valuators
328 } else {
329 macro_rules! set_val {
330 ($t:ty) => {{
331 if utils::is_ptr_of::<$t>(w.as_widget_ptr()) {
332 let mut v = <$t>::from_widget_ptr(w.as_widget_ptr() as _);
333 v.set_value(n);
334 true
335 } else {
336 false
337 }
338 }};
339 }
340 let _handled =
341 set_val!(valuator::Slider)
342 || set_val!(valuator::NiceSlider)
343 || set_val!(valuator::Dial)
344 || set_val!(valuator::LineDial)
345 || set_val!(valuator::Counter)
346 || set_val!(valuator::Scrollbar)
347 || set_val!(valuator::ValueInput)
348 || set_val!(valuator::ValueOutput)
349 || set_val!(valuator::ValueSlider)
350 || set_val!(valuator::HorValueSlider)
351 || set_val!(valuator::HorSlider)
352 || set_val!(valuator::HorNiceSlider)
353 || set_val!(valuator::FillSlider)
354 || set_val!(valuator::HorFillSlider)
355 || set_val!(misc::Spinner)
356 || set_val!(misc::Progress);
357 // else: fallback noop
358 }
359 }
360 _ => {}
361 }
362 }
363 }
364 _ => {}
365 }
366 }
367 }
368 }
369 });
370 this
371 }
372
373 pub fn with_action_handler(
374 window: &window::Window,
375 source: impl 'static + ActivationHandler + Send,
376 action_handler: impl 'static + ActionHandler + Send,
377 ) -> Self {
378 let deactivation_handler = FltkDeactivationHandler {};
379 let adapter =
380 platform_adapter::Adapter::new(window, source, action_handler, deactivation_handler);
381 window.clone().resize_callback({
382 let adapter = adapter.clone();
383 move |win, _x, _y, w, h| {
384 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
385 {
386 let outer_origin = Point {
387 x: win.x_root() as _,
388 y: win.y_root() as _,
389 };
390 // Client-area origin (top-left) in root/screen coordinates
391 let inner_origin = Point {
392 x: win.x() as _,
393 y: win.y() as _,
394 };
395 let outer_size = Size {
396 width: win.decorated_w() as _,
397 height: win.decorated_h() as _,
398 };
399 let inner_size = Size {
400 width: w as _,
401 height: h as _,
402 };
403 adapter.borrow_mut().set_root_window_bounds(
404 Rect::from_origin_size(outer_origin, outer_size),
405 Rect::from_origin_size(inner_origin, inner_size),
406 );
407 }
408 }
409 });
410 Self { adapter }
411 }
412
413 #[cfg(all(
414 not(target_os = "linux"),
415 not(target_os = "dragonfly"),
416 not(target_os = "freebsd"),
417 not(target_os = "netbsd"),
418 not(target_os = "openbsd")
419 ))]
420 #[must_use]
421 pub fn on_event(&self, window: &window::Window, event: &Event) -> bool {
422 unsafe { app::handle_raw(*event, window.as_widget_ptr() as _) }
423 }
424 #[cfg(any(
425 target_os = "linux",
426 target_os = "dragonfly",
427 target_os = "freebsd",
428 target_os = "netbsd",
429 target_os = "openbsd"
430 ))]
431 #[must_use]
432 pub fn on_event(&self, window: &mut window::Window, event: &Event) -> bool {
433 unsafe { app::handle_raw(*event, window.as_widget_ptr() as _) }
434 }
435
436 // pub fn update_window_focus_state(&mut self, is_focused: bool) {
437 // self.adapter.borrow_mut().update_window_focus_state(is_focused)
438 // }
439
440 pub fn update_if_active(&mut self, updater: impl FnOnce() -> TreeUpdate) {
441 self.adapter.borrow_mut().update_if_active(updater)
442 }
443}