1use crate::a11y::ReposeActionHandler;
3#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
4use accesskit_winit::Adapter;
5use repose_core::locals::dp_to_px;
6use repose_core::*;
7use repose_ui::textfield::{
8 self, TF_FONT_DP, TF_PADDING_X_DP, TextFieldState, TextMeasureConfig, caret_xy_for_byte, measure_text,
9};
10use std::cell::{Cell, RefCell};
11use std::rc::Rc;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::{Arc, Mutex};
14use web_time::Instant;
15
16#[cfg(target_os = "android")]
17pub mod android;
18
19#[cfg(target_arch = "wasm32")]
20pub mod web;
21
22pub mod a11y;
23mod common;
24#[cfg(not(target_arch = "wasm32"))]
25mod common_android;
26mod common_web;
27pub mod render;
28
29use common as rc;
30#[cfg(not(target_arch = "wasm32"))]
31use common_android as rc_android;
32use common_web as rc_web;
33
34pub use render::{ImageHandleGuard, RenderCommand, RenderContext};
35
36#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
37use winit::window::Window;
38
39#[cfg(not(target_arch = "wasm32"))]
40use std::sync::OnceLock;
41
42#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
43static APP_WINDOW: OnceLock<Arc<Window>> = OnceLock::new();
44
45#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
46static WINDOW_VISIBLE: AtomicBool = AtomicBool::new(true);
47
48#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
49static CLOSE_TO_TRAY: AtomicBool = AtomicBool::new(false);
50
51#[cfg(not(target_arch = "wasm32"))]
52static EVENT_LOOP_PROXY: OnceLock<winit::event_loop::EventLoopProxy<()>> = OnceLock::new();
53
54#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
58static ABOUT_TO_WAIT_CALLBACK: Mutex<Option<Box<dyn Fn() + Send>>> = Mutex::new(None);
59
60static DEEPLINK_CB: Mutex<Option<Box<dyn Fn(Vec<u8>) + Send>>> = Mutex::new(None);
61static PENDING_DEEPLINKS: Mutex<Vec<Vec<u8>>> = Mutex::new(Vec::new());
62
63pub fn set_on_deeplink(callback: Box<dyn Fn(Vec<u8>) + Send>) {
65 *DEEPLINK_CB.lock().unwrap() = Some(callback);
66}
67
68pub fn push_deeplink(data: Vec<u8>) {
70 PENDING_DEEPLINKS.lock().unwrap().push(data);
71 #[cfg(not(target_arch = "wasm32"))]
72 if let Some(proxy) = EVENT_LOOP_PROXY.get() {
73 let _ = proxy.send_event(());
74 }
75}
76
77pub(crate) fn process_deeplinks() {
80 let mut queue = PENDING_DEEPLINKS.lock().unwrap();
81 if queue.is_empty() {
82 return;
83 }
84 let batch = std::mem::take(&mut *queue);
85 drop(queue);
86
87 if let Some(cb) = DEEPLINK_CB.lock().unwrap().as_ref() {
88 for data in batch {
89 cb(data);
90 }
91 }
92}
93
94#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
96pub fn set_app_window(window: Arc<Window>) {
97 let _ = APP_WINDOW.set(window);
98}
99
100#[cfg(not(target_arch = "wasm32"))]
102pub fn set_event_loop_proxy(proxy: winit::event_loop::EventLoopProxy<()>) {
103 let _ = EVENT_LOOP_PROXY.set(proxy);
104}
105
106#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
108pub fn set_about_to_wait_callback(cb: Box<dyn Fn() + Send>) {
109 *ABOUT_TO_WAIT_CALLBACK.lock().unwrap() = Some(cb);
110}
111
112#[cfg(not(target_arch = "wasm32"))]
114pub fn wake_event_loop() {
115 if let Some(proxy) = EVENT_LOOP_PROXY.get() {
116 let _ = proxy.send_event(());
117 }
118}
119
120#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
124pub fn show_app_window() {
125 WINDOW_VISIBLE.store(true, Ordering::Relaxed);
126 if let Some(w) = APP_WINDOW.get() {
127 log::info!("show_app_window: calling set_visible(true)");
128 w.set_visible(true);
129 #[allow(deprecated)]
130 w.focus_window();
131 }
132 repose_core::frame_clock::request_frame();
133 wake_event_loop();
134}
135
136#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
137pub fn hide_app_window() {
138 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
139 if let Some(w) = APP_WINDOW.get() {
140 log::info!("hide_app_window: calling set_visible(false)");
141 w.set_visible(false);
142 }
143 repose_core::frame_clock::request_frame();
144 wake_event_loop();
145}
146
147#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
149pub fn window_is_visible() -> bool {
150 WINDOW_VISIBLE.load(Ordering::Relaxed)
151}
152
153#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
156pub fn set_close_to_tray(enabled: bool) {
157 CLOSE_TO_TRAY.store(enabled, Ordering::Relaxed);
158}
159
160pub fn compose_frame<F>(
162 sched: &mut Scheduler,
163 root_fn: &mut F,
164 scale: f32,
165 size_px_u32: (u32, u32),
166 hover_id: Option<u64>,
167 pressed_ids: &std::collections::HashSet<u64>,
168 tf_states: &std::collections::HashMap<u64, Rc<RefCell<repose_ui::TextFieldState>>>,
169 _focused: Option<u64>,
170) -> Frame
171where
172 F: FnMut(&mut Scheduler) -> View,
173{
174 if let Some(requested_id) = repose_core::take_focus_request() {
176 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
177 sched.focused = None;
178 } else {
179 sched.focused = Some(requested_id);
180 }
181 }
182
183 set_density_default(Density { scale });
184
185 let current_focused = sched.focused;
187
188 let frame = sched.repose(
189 {
190 let scale = scale;
191 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s))
192 },
193 {
194 let hover_id = hover_id;
195 let pressed_ids = pressed_ids.clone();
196 move |view, _size| {
197 let interactions = repose_ui::Interactions {
198 hover: hover_id,
199 pressed: pressed_ids.clone(),
200 };
201
202 with_density(Density { scale }, || {
203 repose_ui::layout_and_paint(
204 view,
205 size_px_u32,
206 tf_states,
207 &interactions,
208 current_focused,
209 )
210 })
211 }
212 },
213 );
214
215 if let Some(fid) = sched.focused {
216 if !frame.focus_chain.contains(&fid) {
217 sched.focused = None;
218 }
219 }
220
221 frame
222}
223
224pub(crate) fn next_caret_blink_deadline(
225 sched: &Scheduler,
226 frame_cache: &Option<Frame>,
227 textfield_states: &std::collections::HashMap<u64, Rc<RefCell<TextFieldState>>>,
228) -> Option<Instant> {
229 let fid = sched.focused?;
230 let frame = frame_cache.as_ref()?;
231 let hit = frame.hit_regions.iter().find(|h| h.id == fid)?;
232 let key = hit.tf_state_key?;
233 textfield_states.get(&key)?.borrow().next_blink_deadline()
234}
235
236pub fn tf_ensure_visible_in_rect(state: &mut repose_ui::TextFieldState, inner_rect: Rect) {
238 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
239 let m = measure_text(&state.text, font_px, TextMeasureConfig::default());
240 let caret_x_px = m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
241 state.ensure_caret_visible(
242 caret_x_px,
243 inner_rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
244 dp_to_px(2.0),
245 );
246}
247
248fn winit_key_to_repose(
250 ev: &winit::event::KeyEvent,
251 mapped_key: &repose_core::input::Key,
252 mods: &repose_core::input::Modifiers,
253) -> repose_core::input::KeyEvent {
254 let utf16 = match mapped_key {
255 repose_core::input::Key::Character(c) => *c as u16,
256 _ => 0,
257 };
258 repose_core::input::KeyEvent {
259 key: mapped_key.clone(),
260 modifiers: *mods,
261 is_repeat: ev.repeat,
262 event_type: if ev.state == winit::event::ElementState::Pressed {
263 repose_core::input::KeyEventType::Down
264 } else {
265 repose_core::input::KeyEventType::Up
266 },
267 utf16_code_point: utf16,
268 }
269}
270
271#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
272fn map_cursor(c: repose_core::CursorIcon) -> winit::window::CursorIcon {
273 use winit::window::CursorIcon as W;
274 match c {
275 repose_core::CursorIcon::Default => W::Default,
276 repose_core::CursorIcon::Pointer => W::Pointer,
277 repose_core::CursorIcon::Text => W::Text,
278 repose_core::CursorIcon::EwResize => W::EwResize,
279 repose_core::CursorIcon::NsResize => W::NsResize,
280 repose_core::CursorIcon::Grab => W::Grab,
281 repose_core::CursorIcon::Grabbing => W::Grabbing,
282 }
283}
284
285#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
286pub fn run_desktop_app(
287 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
288) -> anyhow::Result<()> {
289 use std::collections::{HashMap, HashSet};
290 use winit::application::ApplicationHandler;
291 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
292 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
293 use winit::event_loop::EventLoop;
294 use winit::keyboard::{KeyCode, PhysicalKey};
295 use winit::window::{Window, WindowAttributes};
296
297 use crate::a11y::A11yTree;
298 use repose_app::ReposeRuntime;
299
300 struct ReposeActivationHandler {
301 initial_tree: Option<accesskit::TreeUpdate>,
302 }
303
304 impl accesskit::ActivationHandler for ReposeActivationHandler {
305 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
306 self.initial_tree.take()
307 }
308 }
309
310 struct ReposeDeactivationHandler;
311
312 impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
313 fn deactivate_accessibility(&mut self) {
314 }
316 }
317
318 struct App {
319 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
320 render: RenderContext,
321 window: Option<Arc<Window>>,
322 backend: Option<repose_render_wgpu::WgpuBackend>,
323 rt: ReposeRuntime,
324 inspector: repose_devtools::Inspector,
325
326 pending_dropped_files: Vec<std::path::PathBuf>,
328 pending_drop_pos_px: Option<(f32, f32)>,
329
330 external_file_drag: bool,
332 hovered_files: Vec<std::path::PathBuf>,
333
334 clipboard: Option<clipawl::Clipboard>,
335 a11y: Box<dyn A11yBridge>,
336
337 accesskit_adapter: Option<Adapter>,
338 a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
339 a11y_tree: A11yTree,
340
341 last_redraw: Instant,
342 pending_redraw: bool,
343
344 redraw_requested: Cell<bool>,
346 }
347
348 impl App {
349 fn process_a11y_actions(&mut self) {
350 let mut actions = self.a11y_actions.lock().unwrap();
351 if actions.is_empty() {
352 return;
353 }
354 let pending = actions.drain(..).collect::<Vec<_>>();
355 drop(actions);
356
357 let Some(f) = &self.rt.frame_cache else {
358 return;
359 };
360
361 for req in pending {
362 let target_id = req.target_node.0;
363 match req.action {
364 accesskit::Action::Click => {
365 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
366 && let Some(cb) = &hit.on_click
367 {
368 cb();
369 self.request_redraw();
370 }
371 }
372 accesskit::Action::Focus => {
373 self.rt.sched.focused = Some(target_id);
374 self.request_redraw();
375 }
376 _ => {}
377 }
378 }
379 }
380
381 fn new(root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>) -> Self {
382 Self {
383 root,
384 render: RenderContext::new(),
385 window: None,
386 backend: None,
387 rt: ReposeRuntime::new(),
388 inspector: repose_devtools::Inspector::new(),
389 pending_dropped_files: Vec::new(),
390 pending_drop_pos_px: None,
391
392 external_file_drag: false,
393 hovered_files: Vec::new(),
394
395 clipboard: None,
396 a11y: {
397 #[cfg(target_os = "linux")]
398 {
399 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
400 }
401 #[cfg(not(target_os = "linux"))]
402 {
403 Box::new(NoopA11y) as Box<dyn A11yBridge>
404 }
405 },
406
407 accesskit_adapter: None,
408 a11y_actions: Arc::new(Mutex::new(Vec::new())),
409 a11y_tree: A11yTree::default(),
410
411 last_redraw: Instant::now(),
412 pending_redraw: false,
413 redraw_requested: Cell::new(false),
414 }
415 }
416
417 fn request_redraw(&self) {
418 self.redraw_requested.set(true);
419 repose_core::request_frame();
420 rc::request_redraw(&self.window);
421 }
422
423 fn tf_ensure_caret_visible(st: &mut TextFieldState, is_multiline: bool) {
425 rc::tf_ensure_caret_visible(st, is_multiline);
426 }
427
428 fn paste_from_primary(&self) -> Option<String> {
429 let mut opts = clipawl::ClipboardOptions::default();
430 opts.linux.selection = clipawl::LinuxSelection::Primary;
431 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
432 match pollster::block_on(cb.read()) {
433 Ok(t) => Some(t),
434 Err(e) => {
435 eprintln!("Primary paste error: {}", e);
436 None
437 }
438 }
439 } else {
440 None
441 }
442 }
443
444 fn process_render_commands(&mut self) {
445 let Some(backend) = self.backend.as_mut() else {
446 return;
447 };
448 rc::process_render_commands(backend, self.render.drain());
449 }
450
451 fn reset_pointer_state(&mut self) {
452 self.rt.capture_id = None;
453 self.rt.pressed_ids.clear();
454 self.rt.hover_id = None;
455 }
456
457 fn is_textfield(&self, id: u64) -> bool {
458 rc::is_textfield_in_frame(&self.rt.frame_cache, id)
459 }
460
461 fn is_multiline_id(&self, id: u64) -> bool {
462 if let Some(f) = &self.rt.frame_cache {
463 f.hit_regions
464 .iter()
465 .find(|h| h.id == id)
466 .map(|h| h.tf_multiline)
467 .unwrap_or(false)
468 } else {
469 false
470 }
471 }
472
473 fn hit_by_id(f: &Frame, id: u64) -> Option<&HitRegion> {
474 f.hit_regions.iter().find(|h| h.id == id)
475 }
476
477 fn dp_px(&self, dp: f32) -> f32 {
478 dp_to_px(dp)
479 }
480 }
481
482 impl ApplicationHandler<()> for App {
483 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
484 self.clipboard = clipawl::Clipboard::new()
485 .map_err(|e| {
486 eprintln!("clipawl clipboard init failed: {e}");
487 e
488 })
489 .ok();
490 repose_core::clipboard::set_clipboard_read_fn(Box::new(|| {
491 clipawl::blocking::read().ok()
492 }));
493 repose_core::clipboard::set_clipboard_fn(Box::new(move |text| {
495 if let Err(e) = clipawl::blocking::write(text) {
496 eprintln!("clipboard write error: {e}");
497 }
498 }));
499
500 repose_core::clipboard::set_primary_fn(Box::new(|text| {
501 let mut opts = clipawl::ClipboardOptions::default();
502 opts.linux.selection = clipawl::LinuxSelection::Primary;
503 match clipawl::Clipboard::new_with_options(opts) {
504 Ok(cb) => {
505 if let Err(e) = pollster::block_on(cb.write(text)) {
506 eprintln!("primary selection write error: {e}");
507 }
508 }
509 Err(e) => eprintln!("primary clipboard init error: {e}"),
510 }
511 }));
512
513 if self.window.is_none() {
514 match el.create_window(
515 WindowAttributes::default()
516 .with_title("Repose")
517 .with_inner_size(PhysicalSize::new(1280, 800))
518 .with_visible(false),
519 ) {
520 Ok(win) => {
521 let w = Arc::new(win);
522
523 let activation_handler = ReposeActivationHandler {
524 initial_tree: Some(A11yTree::initial_tree()),
525 };
526
527 let action_handler = ReposeActionHandler {
528 pending_actions: self.a11y_actions.clone(),
529 };
530
531 let deactivation_handler = ReposeDeactivationHandler;
532
533 let adapter = Adapter::with_direct_handlers(
534 el,
535 &w,
536 activation_handler,
537 action_handler,
538 deactivation_handler,
539 );
540
541 self.accesskit_adapter = Some(adapter);
542
543 w.set_visible(true);
544
545 let size = w.inner_size();
546 let sf = w.scale_factor() as f32;
547 self.rt.set_viewport_and_scale(size.width, size.height, sf);
548
549 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
550 Ok(b) => {
551 self.backend = Some(b);
552 set_app_window(w.clone());
553 self.window = Some(w);
554 self.request_redraw();
555 }
556 Err(e) => {
557 log::error!("Failed to create WGPU backend: {e:?}");
558 el.exit();
559 }
560 }
561 }
562 Err(e) => {
563 log::error!("Failed to create window: {e:?}");
564 el.exit();
565 }
566 }
567 }
568 }
569
570 fn window_event(
571 &mut self,
572 el: &winit::event_loop::ActiveEventLoop,
573 _id: winit::window::WindowId,
574 event: WindowEvent,
575 ) {
576 if let Some(adapter) = &mut self.accesskit_adapter {
578 adapter.process_event(self.window.as_ref().unwrap(), &event);
579 }
580
581 match event {
582 WindowEvent::CloseRequested => {
583 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
584 self.backend = None;
586 if let Some(w) = &self.window {
587 w.set_visible(false);
588 }
589 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
590 } else {
591 el.exit();
592 }
593 }
594
595 WindowEvent::Focused(false) => {
596 self.rt.handle_focus_lost();
598
599 self.external_file_drag = false;
601 self.hovered_files.clear();
602
603 if let Some(w) = &self.window {
604 rc_web::set_ime_for_textfield(w, false);
605 }
606
607 self.request_redraw();
608 }
609
610 WindowEvent::HoveredFile(path) => {
611 self.external_file_drag = true;
613 if self.hovered_files.len() < 32 {
614 self.hovered_files.push(path);
615 }
616 if self.pending_drop_pos_px.is_none() {
618 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
619 }
620 self.request_redraw();
621 }
622
623 WindowEvent::HoveredFileCancelled => {
624 self.external_file_drag = false;
625 self.hovered_files.clear();
626
627 self.reset_pointer_state();
629
630 self.request_redraw();
631 }
632
633 WindowEvent::DroppedFile(path) => {
634 self.pending_dropped_files.push(path);
636 if self.pending_drop_pos_px.is_none() {
637 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
638 }
639
640 self.external_file_drag = false;
642 self.hovered_files.clear();
643
644 self.request_redraw();
645 }
646
647 WindowEvent::Resized(size) => {
648 let sf = self.window.as_ref().map(|w| w.scale_factor() as f32).unwrap_or(1.0);
649 self.rt.set_viewport_and_scale(size.width, size.height, sf);
650 if let Some(b) = self.backend.as_mut() {
651 b.configure_surface(size.width, size.height);
652 }
653 if let Some(w) = &self.window {
654 let sf = w.scale_factor() as f32;
655 let dp_w = size.width as f32 / sf;
656 let dp_h = size.height as f32 / sf;
657 log::info!(
658 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
659 size.width,
660 size.height,
661 sf,
662 dp_w as i32,
663 dp_h as i32
664 );
665 }
666 self.request_redraw();
667 }
668
669 WindowEvent::CursorMoved { position, .. } => {
670 if self.external_file_drag {
671 self.pending_drop_pos_px = Some((position.x as f32, position.y as f32));
672 }
673
674 let pos = Vec2 {
675 x: position.x as f32,
676 y: position.y as f32,
677 };
678
679 let result = self.rt.handle_pointer_move(pos);
681
682 if self.inspector.hud.inspector_enabled
684 && let Some(f) = &self.rt.frame_cache
685 {
686 let hit = f.hit_regions.iter().find(|h| {
687 h.rect.contains(pos)
688 });
689 let hover_rect = hit.map(|h| h.rect);
690 let hover_info = hit.and_then(|h| {
691 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
692 repose_devtools::HoveredInfo {
693 id: s.id,
694 role: format!("{:?}", s.role),
695 label: s.label.clone(),
696 }
697 })
698 });
699 self.inspector.hud.set_hovered(hover_rect, hover_info);
700 }
701
702 if let Some(win) = &self.window {
704 if let Some(c) = result.cursor {
705 win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
706 }
707 }
708
709 self.request_redraw();
710 }
711
712 WindowEvent::MouseWheel { delta, .. } => {
713 let (dx_px, dy_px) = match delta {
714 MouseScrollDelta::LineDelta(x, y) => {
715 let unit_px = dp_to_px(60.0);
716 (-(x * unit_px), -(y * unit_px))
717 }
718 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
719 };
720 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
721
722 if self.rt.handle_scroll(Vec2 { x: dx_px, y: dy_px }) {
723 self.request_redraw();
724 }
725 }
726
727 WindowEvent::MouseInput {
728 state: ElementState::Pressed,
729 button: MouseButton::Left,
730 ..
731 } => {
732 let pos = Vec2 {
733 x: self.rt.mouse_pos_px.0,
734 y: self.rt.mouse_pos_px.1,
735 };
736
737 let result = self.rt.handle_pointer_press(pos, PointerButton::Primary);
738
739 if let Some(fid) = result.focused {
741 if let Some(win) = &self.window
742 && let Some(f) = &self.rt.frame_cache
743 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
744 {
745 let sf = win.scale_factor();
746 rc_web::set_ime_for_textfield(win, true);
747 win.set_ime_cursor_area(
748 LogicalPosition::new(
749 hit.rect.x as f64 / sf,
750 hit.rect.y as f64 / sf,
751 ),
752 LogicalSize::new(
753 hit.rect.w as f64 / sf,
754 hit.rect.h as f64 / sf,
755 ),
756 );
757 }
758 }
759
760 if result.focused.is_none() && self.rt.ime_preedit {
762 if let Some(win) = &self.window {
763 rc_web::set_ime_for_textfield(win, false);
764 }
765 self.rt.ime_preedit = false;
766 }
767
768 if result.needs_a11y_announce {
769 self.announce_focus_change();
770 }
771
772 self.request_redraw();
773 }
774
775 WindowEvent::MouseInput {
776 state: ElementState::Pressed,
777 button: MouseButton::Middle,
778 ..
779 } => {
780 let Some(f) = &self.rt.frame_cache else {
781 return;
782 };
783 let pos = Vec2 {
784 x: self.rt.mouse_pos_px.0,
785 y: self.rt.mouse_pos_px.1,
786 };
787 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos)) {
788 if let Some(cb) = &hit.on_pointer_down {
790 cb(PointerEvent::new(
791 PointerId(0),
792 PointerKind::Mouse,
793 PointerEventKind::Down(PointerButton::Tertiary),
794 pos,
795 1.0,
796 self.rt.modifiers,
797 ));
798 }
799 if self.is_textfield(hit.id) {
801 let key = self.tf_key_of(hit.id);
802 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
803 if let Some(txt) = self.paste_from_primary() {
804 let mut st = state_rc.borrow_mut();
805 st.insert_text_atomic(&txt);
806 self.notify_text_change(hit.id, st.text.clone());
807 if let Some(f) = &self.rt.frame_cache
808 && let Some(h) =
809 f.hit_regions.iter().find(|h| h.id == hit.id)
810 {
811 App::tf_ensure_caret_visible(&mut st, h.tf_multiline);
812 }
813 }
814 }
815 }
816 }
817 self.request_redraw();
818 }
819
820 WindowEvent::MouseInput {
821 state: ElementState::Released,
822 button: MouseButton::Left,
823 ..
824 } => {
825 let pos = Vec2 {
826 x: self.rt.mouse_pos_px.0,
827 y: self.rt.mouse_pos_px.1,
828 };
829
830 self.rt.handle_pointer_release(pos, PointerButton::Primary);
831
832 if let (Some(f), Some(cid)) = (&self.rt.frame_cache, self.rt.capture_id) {
834 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
835 && hit.rect.contains(pos)
836 && hit.on_click.is_some()
837 && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid)
838 {
839 let label = node.label.as_deref().unwrap_or("");
840 self.a11y.announce(&format!("Activated {}", label));
841 }
842 }
843
844 repose_core::request_frame();
845 }
846
847 WindowEvent::MouseInput {
848 state: ElementState::Released,
849 button: MouseButton::Middle,
850 ..
851 } => {
852 if let Some(f) = &self.rt.frame_cache {
853 let pos = Vec2 {
854 x: self.rt.mouse_pos_px.0,
855 y: self.rt.mouse_pos_px.1,
856 };
857 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
858 {
859 if let Some(cb) = &hit.on_pointer_up {
860 cb(PointerEvent::new(
861 PointerId(0),
862 PointerKind::Mouse,
863 PointerEventKind::Up(PointerButton::Tertiary),
864 pos,
865 1.0,
866 self.rt.modifiers,
867 ));
868 }
869 }
870 }
871 }
872
873 WindowEvent::ModifiersChanged(new_mods) => {
874 let state = new_mods.state();
875 self.rt.modifiers.shift = state.shift_key();
876 self.rt.modifiers.ctrl = state.control_key();
877 self.rt.modifiers.alt = state.alt_key();
878 self.rt.modifiers.meta = state.super_key();
879 self.rt.modifiers.command = if cfg!(target_os = "macos") {
880 self.rt.modifiers.meta
881 } else {
882 self.rt.modifiers.ctrl
883 };
884 }
885
886 WindowEvent::KeyboardInput {
887 event: key_event, ..
888 } => {
889 if key_event.state == ElementState::Pressed && !key_event.repeat {
893 match key_event.physical_key {
894 PhysicalKey::Code(KeyCode::BrowserBack)
895 | PhysicalKey::Code(KeyCode::Escape) => {
896 use repose_navigation::back;
897
898 if repose_core::dnd::handle_drag_action(
899 &repose_core::shortcuts::DragAction::Cancel,
900 ) {
901 return;
902 }
903
904 let mapped = rc::map_key(key_event.physical_key);
906 if self.dispatch_focus_key_event(&key_event, &mapped) {
907 self.request_redraw();
908 return;
909 }
910
911 if !back::handle() {
912 }
914 return;
915 }
916 _ => {}
917 }
918 }
919
920 if key_event.state == ElementState::Pressed
922 && self.rt.modifiers.ctrl
923 && self.rt.modifiers.shift
924 && key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
925 {
926 self.inspector.hud.toggle_inspector();
927 self.request_redraw();
928 return;
929 }
930
931 if key_event.state == ElementState::Pressed
933 && !key_event.repeat
934 && self.rt.modifiers.command
935 {
936 match key_event.physical_key {
937 PhysicalKey::Code(KeyCode::KeyZ) if self.rt.modifiers.shift => {
938 if let Some(fid) = self.rt.sched.focused {
939 let key = self.tf_key_of(fid);
940 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
941 let mut st = state_rc.borrow_mut();
942 if st.can_redo() {
943 st.redo();
944 self.notify_text_change(fid, st.text.clone());
945 self.request_redraw();
946 return;
947 }
948 }
949 }
950 }
951 PhysicalKey::Code(KeyCode::KeyZ) => {
952 if let Some(fid) = self.rt.sched.focused {
953 let key = self.tf_key_of(fid);
954 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
955 let mut st = state_rc.borrow_mut();
956 if st.can_undo() {
957 st.undo();
958 self.notify_text_change(fid, st.text.clone());
959 self.request_redraw();
960 return;
961 }
962 }
963 }
964 }
965 _ => {}
966 }
967 }
968
969 let mapped = rc::map_key(key_event.physical_key);
972 let ke = winit_key_to_repose(&key_event, &mapped, &self.rt.modifiers);
973 let consumed = self.rt.handle_key(&ke);
974 if consumed {
975 self.request_redraw();
976 return;
977 }
978
979 if key_event.state == ElementState::Pressed
984 && !key_event.repeat
985 && !self.rt.ime_preedit
986 && !self.rt.modifiers.ctrl
987 && !self.rt.modifiers.alt
988 && !self.rt.modifiers.meta
989 && let Some(raw) = key_event.text.as_deref()
990 {
991 let text: String = raw
992 .chars()
993 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
994 .collect();
995 if !text.is_empty()
996 && let Some(fid) = self.rt.sched.focused
997 {
998 let key = self.tf_key_of(fid);
999 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1000 let mut st = state_rc.borrow_mut();
1001 st.insert_text(&text);
1002 self.notify_text_change(fid, st.text.clone());
1003 if let Some(f) = &self.rt.frame_cache
1004 && let Some(hit) =
1005 f.hit_regions.iter().find(|h| h.id == fid)
1006 {
1007 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1008 }
1009 self.request_redraw();
1010 return;
1011 }
1012 }
1013 }
1014
1015 if key_event.state == ElementState::Released
1017 && let Some(active_id) = self.rt.key_pressed_active
1018 {
1019 match key_event.physical_key {
1020 PhysicalKey::Code(KeyCode::Space)
1021 | PhysicalKey::Code(KeyCode::Enter) => {
1022 if let Some(f) = &self.rt.frame_cache
1023 && let Some(node) =
1024 f.semantics_nodes.iter().find(|n| n.id == active_id)
1025 {
1026 let label = node.label.as_deref().unwrap_or("");
1027 self.a11y.announce(&format!("Activated {}", label));
1028 }
1029 }
1030 _ => {}
1031 }
1032 }
1033 }
1034
1035 WindowEvent::Ime(ime) => {
1036 if let Some(focused_id) = self.rt.sched.focused {
1037 let key = self.tf_key_of(focused_id);
1038 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1039 let mut state = state_rc.borrow_mut();
1040 let on_text_change = self.rt
1041 .frame_cache
1042 .as_ref()
1043 .and_then(|f| f.hit_regions.iter().find(|h| h.id == focused_id))
1044 .and_then(|h| h.on_text_change.clone());
1045 let mut notify = |text: String| {
1046 if let Some(cb) = &on_text_change {
1047 cb(text);
1048 }
1049 };
1050 rc_android::handle_ime_event(
1051 ime,
1052 &mut state,
1053 &mut notify,
1054 &mut self.rt.ime_preedit,
1055 );
1056 self.request_redraw();
1057 }
1058 }
1059 }
1060
1061 WindowEvent::RedrawRequested => {
1062 if !self.redraw_requested.replace(false) {
1064 self.process_a11y_actions();
1065 self.process_render_commands();
1066 log::trace!("RedrawRequested: no frame request, skipping compose");
1067 return;
1068 }
1069 log::trace!("RedrawRequested: frame request pending, composing");
1070
1071 self.process_a11y_actions();
1073 self.process_render_commands();
1074
1075 let Some(win) = self.window.as_ref() else {
1076 return;
1077 };
1078 if self.backend.is_none() {
1079 return;
1080 }
1081
1082 repose_core::animation_driver::tick();
1085
1086 let t0 = Instant::now();
1087 let scale = win.scale_factor() as f32;
1088 self.rt.scale = scale;
1089 let focused = self.rt.sched.focused;
1090
1091 let output = self.rt.frame(&mut self.root, &self.render);
1092
1093 if let Some(cursor) = &output.platform.cursor {
1095 win.set_cursor(winit::window::Cursor::Icon(map_cursor(*cursor)));
1096 }
1097
1098 if !output.wants_keyboard && focused.is_some() && self.rt.sched.focused.is_none() && self.rt.ime_preedit {
1100 rc_web::set_ime_for_textfield(win, false);
1101 self.rt.ime_preedit = false;
1102 }
1103
1104 let frame = Frame {
1105 scene: output.scene,
1106 hit_regions: output.hit_regions,
1107 semantics_nodes: output.semantics_nodes,
1108 focus_chain: output.focus_chain,
1109 };
1110
1111 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1112
1113 if let Some(adapter) = &mut self.accesskit_adapter {
1115 let win = self.window.as_ref().unwrap();
1116 let scale = win.scale_factor();
1117 if let Some(update) =
1118 self.a11y_tree
1119 .update(&frame.semantics_nodes, scale, self.rt.sched.focused)
1120 {
1121 adapter.update_if_active(|| update);
1122 }
1123 }
1124
1125 let mut scene = frame.scene.clone();
1127 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1129 let signal_count = self.rt.sched.id_count() as usize;
1130 self.inspector.hud.metrics = Some(repose_devtools::Metrics {
1131 build_ms: build_layout_ms,
1132 layout_ms: build_layout_ms * 0.5,
1133 scene_nodes: scene.nodes.len(),
1134 widget_count,
1135 signal_count,
1136 });
1137 self.inspector.frame(&mut scene);
1138
1139 repose_core::dnd::overlay_drag_indicator(
1141 &mut scene,
1142 self.rt.mouse_pos_px,
1143 self.external_file_drag,
1144 );
1145
1146 let win = self.window.as_ref().unwrap();
1148 let scale = win.scale_factor() as f32;
1149 if let Some(backend) = self.backend.as_mut() {
1150 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1151 }
1152
1153 if let Some(fid) = self.rt.sched.focused {
1156 if let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
1157 && let Some(key) = hit.tf_state_key
1158 && !self.rt.textfield_states.contains_key(&key)
1159 {
1160 self.rt.textfield_states
1161 .entry(key)
1162 .or_insert_with(|| {
1163 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1164 })
1165 .borrow_mut()
1166 .reset_caret_blink();
1167 }
1168 }
1169
1170 repose_core::dnd::set_dnd_frame(Some(frame.clone()));
1171 self.rt.frame_cache = Some(frame);
1172 repose_core::dnd::set_dnd_scale(scale);
1173
1174 self.dispatch_file_drop_now();
1175
1176 rc::tick_snackbar(self.last_redraw);
1177 self.last_redraw = Instant::now();
1178 }
1179
1180 _ => {}
1181 }
1182 }
1183
1184 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1185 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1188 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK.lock().unwrap().as_ref() {
1189 cb();
1190 }
1191 process_deeplinks();
1192
1193 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1196 if WINDOW_VISIBLE.load(Ordering::Relaxed) && self.backend.is_none() {
1197 if let Some(w) = &self.window {
1198 log::info!("about_to_wait: recreating GPU backend");
1199 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
1200 Ok(b) => self.backend = Some(b),
1201 Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
1202 }
1203 }
1204 }
1205
1206 if take_frame_request() {
1207 self.pending_redraw = true;
1208 }
1209 if !self.pending_redraw {
1210 let now = Instant::now();
1211 let idle_cap = web_time::Duration::from_millis(1000);
1212 let deadline = self
1213 .next_caret_blink_deadline()
1214 .unwrap_or(now + idle_cap);
1215
1216 if now.saturating_duration_since(self.last_redraw) >= idle_cap || now >= deadline {
1217 self.redraw_requested.set(true);
1218 request_frame();
1219 rc::request_redraw(&self.window);
1220 self.last_redraw = now;
1221 }
1222 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1223 Ord::min(deadline, now + idle_cap),
1224 ));
1225 return;
1226 }
1227
1228 let now = Instant::now();
1229 let interval = web_time::Duration::from_millis(16);
1230
1231 if now.saturating_duration_since(self.last_redraw) >= interval {
1232 self.pending_redraw = false;
1233 self.redraw_requested.set(true);
1234 rc::request_redraw(&self.window);
1235 self.last_redraw = now;
1236 } else {
1237 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1238 self.last_redraw + interval,
1239 ));
1240 }
1241 }
1242
1243 fn new_events(
1244 &mut self,
1245 _: &winit::event_loop::ActiveEventLoop,
1246 _: winit::event::StartCause,
1247 ) {
1248 }
1249 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1250 self.pending_redraw = true;
1251 }
1252 fn device_event(
1253 &mut self,
1254 _: &winit::event_loop::ActiveEventLoop,
1255 _: winit::event::DeviceId,
1256 _: winit::event::DeviceEvent,
1257 ) {
1258 }
1259 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1260 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1261 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1262 }
1263
1264 impl App {
1265 fn dispatch_focus_key_event(
1268 &self,
1269 key_event: &winit::event::KeyEvent,
1270 mapped_key: &repose_core::input::Key,
1271 ) -> bool {
1272 let Some(f) = &self.rt.frame_cache else {
1273 return false;
1274 };
1275 let Some(focused) = self.rt.sched.focused else {
1276 return false;
1277 };
1278 let utf16 = match mapped_key {
1279 repose_core::input::Key::Character(c) => *c as u16,
1280 _ => 0,
1281 };
1282 let mods = self.rt.modifiers;
1283 let repeat = key_event.repeat;
1284 let ev_type = if key_event.state == ElementState::Pressed {
1285 repose_core::input::KeyEventType::Down
1286 } else {
1287 repose_core::input::KeyEventType::Up
1288 };
1289 let hit_by_id: std::collections::HashMap<u64, &HitRegion> =
1290 f.hit_regions.iter().map(|h| (h.id, h)).collect();
1291 let sem_parent_of: std::collections::HashMap<u64, u64> = f
1292 .semantics_nodes
1293 .iter()
1294 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1295 .collect();
1296 let mut ancestors = Vec::new();
1297 let mut cur = focused;
1298 loop {
1299 ancestors.push(cur);
1300 if let Some(&p) = sem_parent_of.get(&cur) {
1301 cur = p;
1302 } else {
1303 break;
1304 }
1305 }
1306 let make_ke = || repose_core::input::KeyEvent {
1307 key: mapped_key.clone(),
1308 modifiers: mods,
1309 is_repeat: repeat,
1310 event_type: ev_type,
1311 utf16_code_point: utf16,
1312 };
1313 for &id in ancestors.iter().rev() {
1315 if let Some(hit) = hit_by_id.get(&id) {
1316 if let Some(cb) = &hit.on_preview_key_event {
1317 if cb(make_ke()) {
1318 return true;
1319 }
1320 }
1321 }
1322 }
1323 for &id in ancestors.iter() {
1325 if let Some(hit) = hit_by_id.get(&id) {
1326 if let Some(cb) = &hit.on_key_event {
1327 if cb(make_ke()) {
1328 return true;
1329 }
1330 }
1331 }
1332 }
1333 false
1334 }
1335
1336 fn announce_focus_change(&mut self) {
1337 if let Some(f) = &self.rt.frame_cache {
1338 let focused_node = self.rt
1339 .sched
1340 .focused
1341 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1342 self.a11y.focus_changed(focused_node);
1343 }
1344 }
1345
1346 fn notify_text_change(&self, id: u64, text: String) {
1347 if let Some(f) = &self.rt.frame_cache
1348 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1349 && let Some(cb) = &h.on_text_change
1350 {
1351 cb(text);
1352 }
1353 }
1354
1355 fn tf_key_of(&self, visual_id: u64) -> u64 {
1356 rc::tf_key_of_in_frame(&self.rt.frame_cache, visual_id)
1357 }
1358
1359 fn next_caret_blink_deadline(&self) -> Option<Instant> {
1362 next_caret_blink_deadline(&self.rt.sched, &self.rt.frame_cache, &self.rt.textfield_states)
1363 }
1364
1365 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1366 use repose_core::shortcuts;
1367
1368 if let (Some(f), Some(fid)) = (&self.rt.frame_cache, self.rt.sched.focused)
1369 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1370 && let Some(cb) = &hit.on_action
1371 && cb(action.clone())
1372 {
1373 return true;
1374 }
1375
1376 if shortcuts::handle(action.clone()) {
1377 return true;
1378 }
1379
1380 if let Some(f) = &self.rt.frame_cache {
1382 if let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.rt.sched, f)
1383 {
1384 if let Some(active) = self.rt.key_pressed_active.take() {
1385 self.rt.pressed_ids.remove(&active);
1386 }
1387 let tf_state_key = f
1388 .hit_regions
1389 .iter()
1390 .find(|h| h.id == new_id)
1391 .and_then(|h| h.tf_state_key);
1392 if let Some(key) = tf_state_key {
1393 self.rt.textfield_states.entry(key).or_insert_with(|| {
1394 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1395 });
1396 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1397 state_rc.borrow_mut().reset_caret_blink();
1398 }
1399 }
1400 if let Some(win) = &self.window {
1401 let is_textfield = f.semantics_nodes.iter().any(|n| {
1402 n.id == new_id && n.role == repose_core::semantics::Role::TextField
1403 });
1404 rc_web::set_ime_for_textfield(win, is_textfield);
1405 }
1406 self.announce_focus_change();
1407 return true;
1408 }
1409 }
1410
1411 false
1412 }
1413
1414 fn dispatch_file_drop_now(&mut self) {
1415 let Some(f) = &self.rt.frame_cache else {
1416 self.pending_dropped_files.clear();
1417 self.pending_drop_pos_px = None;
1418 return;
1419 };
1420
1421 if self.pending_dropped_files.is_empty() {
1422 return;
1423 }
1424
1425 let pos_px = self.pending_drop_pos_px.unwrap_or(self.rt.mouse_pos_px);
1426 let pos = Vec2 {
1427 x: pos_px.0,
1428 y: pos_px.1,
1429 };
1430
1431 let mut files = Vec::new();
1432 for p in self.pending_dropped_files.drain(..) {
1433 let name = p
1434 .file_name()
1435 .and_then(|s| s.to_str())
1436 .unwrap_or("file")
1437 .to_string();
1438 files.push(repose_core::dnd::DroppedFile {
1439 name,
1440 path: Some(p),
1441 });
1442 }
1443
1444 let payload: repose_core::dnd::DragPayload =
1445 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1446
1447 let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
1448 self.pending_drop_pos_px = None;
1449 return;
1450 };
1451
1452 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
1453 && let Some(cb) = &hit.on_drop
1454 {
1455 let accepted = cb(repose_core::dnd::DropEvent {
1456 source_id: 0, target_id,
1458 position: pos,
1459 modifiers: self.rt.modifiers,
1460 payload: payload.clone(),
1461 });
1462
1463 if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
1464 {
1465 let label = node.label.as_deref().unwrap_or("");
1466 self.a11y.announce(&format!("Dropped files on {}", label));
1467 }
1468 }
1469
1470 self.pending_drop_pos_px = None;
1471 self.request_redraw();
1472 }
1473 }
1474
1475 let event_loop = EventLoop::new()?;
1476 set_event_loop_proxy(event_loop.create_proxy());
1477 let mut app = App::new(Box::new(root));
1478 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1480 event_loop.run_app(&mut app)?;
1481 Ok(())
1482}
1483
1484pub trait A11yBridge: Send {
1492 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1494
1495 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1497
1498 fn announce(&mut self, msg: &str);
1500}
1501
1502struct NoopA11y;
1503impl A11yBridge for NoopA11y {
1504 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1505 }
1507 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1508 if let Some(n) = node {
1509 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1510 } else {
1511 log::info!("A11y focus: None");
1512 }
1513 }
1514 fn announce(&mut self, msg: &str) {
1515 log::info!("A11y announce: {msg}");
1516 }
1517}
1518
1519#[cfg(target_os = "linux")]
1520struct LinuxAtspiStub;
1521#[cfg(target_os = "linux")]
1522impl A11yBridge for LinuxAtspiStub {
1523 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1524 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1525 }
1526 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1527 if let Some(n) = node {
1528 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1529 } else {
1530 log::info!("AT-SPI stub focus: None");
1531 }
1532 }
1533 fn announce(&mut self, msg: &str) {
1534 log::info!("AT-SPI stub announce: {msg}");
1535 }
1536}