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