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