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 run_desktop_app_with_msaa(root, 4)
350}
351
352#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
355pub fn run_desktop_app_with_msaa(
356 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
357 msaa_samples: u32,
358) -> anyhow::Result<()> {
359 use std::collections::{HashMap, HashSet};
360 use winit::application::ApplicationHandler;
361 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
362 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
363 use winit::event_loop::EventLoop;
364 use winit::keyboard::{KeyCode, PhysicalKey};
365 use winit::window::{Window, WindowAttributes};
366
367 use crate::a11y::A11yTree;
368 use repose_app::ReposeRuntime;
369
370 struct ReposeActivationHandler {
371 initial_tree: Option<accesskit::TreeUpdate>,
372 }
373
374 impl accesskit::ActivationHandler for ReposeActivationHandler {
375 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
376 self.initial_tree.take()
377 }
378 }
379
380 struct ReposeDeactivationHandler;
381
382 impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
383 fn deactivate_accessibility(&mut self) {
384 }
386 }
387
388 struct App {
389 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
390 render: RenderContext,
391 window: Option<Arc<Window>>,
392 backend: Option<repose_render_wgpu::WgpuBackend>,
393 rt: ReposeRuntime,
394 inspector: repose_devtools::Inspector,
395 msaa_samples: u32,
396
397 pending_dropped_files: Vec<std::path::PathBuf>,
399 pending_drop_pos_px: Option<(f32, f32)>,
400
401 external_file_drag: bool,
403 hovered_files: Vec<std::path::PathBuf>,
404
405 clipboard: Option<clipawl::Clipboard>,
406 a11y: Box<dyn A11yBridge>,
407
408 accesskit_adapter: Option<Adapter>,
409 a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
410 a11y_tree: A11yTree,
411
412 last_redraw: Instant,
413 pending_redraw: bool,
414
415 redraw_requested: Cell<bool>,
417 }
418
419 impl App {
420 fn process_a11y_actions(&mut self) {
421 let mut actions = self.a11y_actions.lock().unwrap();
422 if actions.is_empty() {
423 return;
424 }
425 let pending = actions.drain(..).collect::<Vec<_>>();
426 drop(actions);
427
428 let Some(f) = &self.rt.frame_cache else {
429 return;
430 };
431
432 for req in pending {
433 let target_id = req.target_node.0;
434 match req.action {
435 accesskit::Action::Click => {
436 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
437 && let Some(cb) = &hit.on_click
438 {
439 cb();
440 self.request_redraw();
441 }
442 }
443 accesskit::Action::Focus => {
444 self.rt.sched.focused = Some(target_id);
445 self.request_redraw();
446 }
447 _ => {}
448 }
449 }
450 }
451
452 fn new(
453 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
454 msaa_samples: u32,
455 ) -> Self {
456 Self {
457 root,
458 render: RenderContext::new(),
459 window: None,
460 backend: None,
461 rt: ReposeRuntime::new(),
462 inspector: repose_devtools::Inspector::new(),
463 msaa_samples,
464 pending_dropped_files: Vec::new(),
465 pending_drop_pos_px: None,
466
467 external_file_drag: false,
468 hovered_files: Vec::new(),
469
470 clipboard: None,
471 a11y: {
472 #[cfg(target_os = "linux")]
473 {
474 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
475 }
476 #[cfg(not(target_os = "linux"))]
477 {
478 Box::new(NoopA11y) as Box<dyn A11yBridge>
479 }
480 },
481
482 accesskit_adapter: None,
483 a11y_actions: Arc::new(Mutex::new(Vec::new())),
484 a11y_tree: A11yTree::default(),
485
486 last_redraw: Instant::now(),
487 pending_redraw: false,
488 redraw_requested: Cell::new(false),
489 }
490 }
491
492 fn request_redraw(&self) {
493 self.redraw_requested.set(true);
494 repose_core::request_frame();
495 rc::request_redraw(&self.window);
496 }
497
498 fn tf_ensure_caret_visible(st: &mut TextFieldState, is_multiline: bool) {
500 rc::tf_ensure_caret_visible(st, is_multiline);
501 }
502
503 fn paste_from_primary(&self) -> Option<String> {
504 let mut opts = clipawl::ClipboardOptions::default();
505 opts.linux.selection = clipawl::LinuxSelection::Primary;
506 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
507 match pollster::block_on(cb.read()) {
508 Ok(t) => Some(t),
509 Err(e) => {
510 eprintln!("Primary paste error: {}", e);
511 None
512 }
513 }
514 } else {
515 None
516 }
517 }
518
519 fn process_render_commands(&mut self) {
520 let Some(backend) = self.backend.as_mut() else {
521 return;
522 };
523 rc::process_render_commands(backend, self.render.drain());
524 }
525
526 fn reset_pointer_state(&mut self) {
527 self.rt.capture_id = None;
528 self.rt.pressed_ids.clear();
529 self.rt.hover_id = None;
530 }
531
532 fn is_textfield(&self, id: u64) -> bool {
533 rc::is_textfield_in_frame(&self.rt.frame_cache, id)
534 }
535
536 fn is_multiline_id(&self, id: u64) -> bool {
537 if let Some(f) = &self.rt.frame_cache {
538 f.hit_regions
539 .iter()
540 .find(|h| h.id == id)
541 .map(|h| h.tf_multiline)
542 .unwrap_or(false)
543 } else {
544 false
545 }
546 }
547
548 fn hit_by_id(f: &Frame, id: u64) -> Option<&HitRegion> {
549 f.hit_regions.iter().find(|h| h.id == id)
550 }
551
552 fn dp_px(&self, dp: f32) -> f32 {
553 dp_to_px(dp)
554 }
555 }
556
557 impl ApplicationHandler<()> for App {
558 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
559 self.clipboard = clipawl::Clipboard::new()
560 .map_err(|e| {
561 eprintln!("clipawl clipboard init failed: {e}");
562 e
563 })
564 .ok();
565 repose_core::clipboard::set_clipboard_read_fn(Box::new(|| {
566 clipawl::blocking::read().ok()
567 }));
568 repose_core::clipboard::set_clipboard_fn(Box::new(move |text| {
570 if let Err(e) = clipawl::blocking::write(text) {
571 eprintln!("clipboard write error: {e}");
572 }
573 }));
574
575 repose_core::clipboard::set_primary_fn(Box::new(|text| {
576 let mut opts = clipawl::ClipboardOptions::default();
577 opts.linux.selection = clipawl::LinuxSelection::Primary;
578 match clipawl::Clipboard::new_with_options(opts) {
579 Ok(cb) => {
580 if let Err(e) = pollster::block_on(cb.write(text)) {
581 eprintln!("primary selection write error: {e}");
582 }
583 }
584 Err(e) => eprintln!("primary clipboard init error: {e}"),
585 }
586 }));
587
588 if self.window.is_none() {
589 match el.create_window(
590 WindowAttributes::default()
591 .with_title("Repose")
592 .with_inner_size(PhysicalSize::new(1280, 800))
593 .with_visible(false),
594 ) {
595 Ok(win) => {
596 let w = Arc::new(win);
597
598 let activation_handler = ReposeActivationHandler {
599 initial_tree: Some(A11yTree::initial_tree()),
600 };
601
602 let action_handler = ReposeActionHandler {
603 pending_actions: self.a11y_actions.clone(),
604 };
605
606 let deactivation_handler = ReposeDeactivationHandler;
607
608 let adapter = Adapter::with_direct_handlers(
609 el,
610 &w,
611 activation_handler,
612 action_handler,
613 deactivation_handler,
614 );
615
616 self.accesskit_adapter = Some(adapter);
617
618 w.set_visible(true);
619
620 let size = w.inner_size();
621 let sf = w.scale_factor() as f32;
622 self.rt.set_viewport_and_scale(size.width, size.height, sf);
623
624 match repose_render_wgpu::WgpuBackend::new_with_msaa(
625 w.clone(),
626 self.msaa_samples,
627 ) {
628 Ok(b) => {
629 self.backend = Some(b);
630 set_app_window(w.clone());
631 self.window = Some(w);
632 self.request_redraw();
633 }
634 Err(e) => {
635 log::error!("Failed to create WGPU backend: {e:?}");
636 el.exit();
637 }
638 }
639 }
640 Err(e) => {
641 log::error!("Failed to create window: {e:?}");
642 el.exit();
643 }
644 }
645 }
646 }
647
648 fn window_event(
649 &mut self,
650 el: &winit::event_loop::ActiveEventLoop,
651 _id: winit::window::WindowId,
652 event: WindowEvent,
653 ) {
654 if let Some(adapter) = &mut self.accesskit_adapter {
656 adapter.process_event(self.window.as_ref().unwrap(), &event);
657 }
658
659 match event {
660 WindowEvent::CloseRequested => {
661 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
662 self.backend = None;
664 if let Some(w) = &self.window {
665 w.set_visible(false);
666 }
667 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
668 } else {
669 el.exit();
670 }
671 }
672
673 WindowEvent::Focused(false) => {
674 self.rt.handle_focus_lost();
676
677 self.external_file_drag = false;
679 self.hovered_files.clear();
680
681 if let Some(w) = &self.window {
682 rc_web::set_ime_for_textfield(w, false);
683 }
684
685 self.request_redraw();
686 }
687
688 WindowEvent::CursorLeft { .. } => {
689 self.rt.pointer_inside = false;
690 self.rt.clear_hover();
691 self.external_file_drag = false;
692 self.hovered_files.clear();
693 self.request_redraw();
694 }
695
696 WindowEvent::HoveredFile(path) => {
697 self.external_file_drag = true;
699 if self.hovered_files.len() < 32 {
700 self.hovered_files.push(path);
701 }
702 if self.pending_drop_pos_px.is_none() {
704 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
705 }
706 self.request_redraw();
707 }
708
709 WindowEvent::HoveredFileCancelled => {
710 self.external_file_drag = false;
711 self.hovered_files.clear();
712
713 self.reset_pointer_state();
715
716 self.request_redraw();
717 }
718
719 WindowEvent::DroppedFile(path) => {
720 self.pending_dropped_files.push(path);
722 if self.pending_drop_pos_px.is_none() {
723 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
724 }
725
726 self.external_file_drag = false;
728 self.hovered_files.clear();
729
730 self.request_redraw();
731 }
732
733 WindowEvent::Resized(size) => {
734 let sf = self.window.as_ref().map(|w| w.scale_factor() as f32).unwrap_or(1.0);
735 self.rt.set_viewport_and_scale(size.width, size.height, sf);
736 if let Some(b) = self.backend.as_mut() {
737 b.configure_surface(size.width, size.height);
738 }
739 if let Some(w) = &self.window {
740 let sf = w.scale_factor() as f32;
741 let dp_w = size.width as f32 / sf;
742 let dp_h = size.height as f32 / sf;
743 log::info!(
744 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
745 size.width,
746 size.height,
747 sf,
748 dp_w as i32,
749 dp_h as i32
750 );
751 }
752 self.request_redraw();
753 }
754
755 WindowEvent::CursorMoved { position, .. } => {
756 self.rt.pointer_inside = true;
757
758 if self.external_file_drag {
759 self.pending_drop_pos_px = Some((position.x as f32, position.y as f32));
760 }
761
762 let pos = Vec2 {
763 x: position.x as f32,
764 y: position.y as f32,
765 };
766
767 let result = self.rt.handle_pointer_move(pos);
769
770 if self.inspector.hud.inspector_enabled
772 && let Some(f) = &self.rt.frame_cache
773 {
774 let hit = f.hit_regions.iter().find(|h| {
775 h.rect.contains(pos)
776 });
777 let hover_rect = hit.map(|h| h.rect);
778 let hover_info = hit.and_then(|h| {
779 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
780 repose_devtools::HoveredInfo {
781 id: s.id,
782 role: format!("{:?}", s.role),
783 label: s.label.clone(),
784 }
785 })
786 });
787 self.inspector.hud.set_hovered(hover_rect, hover_info);
788 }
789
790 if let Some(win) = &self.window {
792 if let Some(c) = result.cursor {
793 win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
794 }
795 }
796
797 self.request_redraw();
798 }
799
800 WindowEvent::MouseWheel { delta, .. } => {
801 let (dx_px, dy_px) = match delta {
802 MouseScrollDelta::LineDelta(x, y) => {
803 let unit_px = dp_to_px(60.0);
804 (-(x * unit_px), -(y * unit_px))
805 }
806 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
807 };
808 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
809
810 if self.rt.handle_scroll(Vec2 { x: dx_px, y: dy_px }) {
811 self.request_redraw();
812 }
813 }
814
815 WindowEvent::MouseInput {
816 state: ElementState::Pressed,
817 button: MouseButton::Left,
818 ..
819 } => {
820 let pos = Vec2 {
821 x: self.rt.mouse_pos_px.0,
822 y: self.rt.mouse_pos_px.1,
823 };
824
825 let result = self.rt.handle_pointer_press(pos, PointerButton::Primary);
826
827 if let Some(fid) = result.focused {
829 if let Some(win) = &self.window
830 && let Some(f) = &self.rt.frame_cache
831 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
832 {
833 let sf = win.scale_factor();
834 rc_web::set_ime_for_textfield(win, true);
835 win.set_ime_cursor_area(
836 LogicalPosition::new(
837 hit.rect.x as f64 / sf,
838 hit.rect.y as f64 / sf,
839 ),
840 LogicalSize::new(
841 hit.rect.w as f64 / sf,
842 hit.rect.h as f64 / sf,
843 ),
844 );
845 }
846 }
847
848 if result.focused.is_none() && self.rt.ime_preedit {
850 if let Some(win) = &self.window {
851 rc_web::set_ime_for_textfield(win, false);
852 }
853 self.rt.ime_preedit = false;
854 }
855
856 if result.needs_a11y_announce {
857 self.announce_focus_change();
858 }
859
860 self.request_redraw();
861 }
862
863 WindowEvent::MouseInput {
864 state: ElementState::Pressed,
865 button: MouseButton::Middle,
866 ..
867 } => {
868 let Some(f) = &self.rt.frame_cache else {
869 return;
870 };
871 let pos = Vec2 {
872 x: self.rt.mouse_pos_px.0,
873 y: self.rt.mouse_pos_px.1,
874 };
875 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos)) {
876 if let Some(cb) = &hit.on_pointer_down {
878 cb(PointerEvent::new(
879 PointerId(0),
880 PointerKind::Mouse,
881 PointerEventKind::Down(PointerButton::Tertiary),
882 pos,
883 1.0,
884 self.rt.modifiers,
885 ));
886 }
887 if self.is_textfield(hit.id) {
889 let key = self.tf_key_of(hit.id);
890 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
891 if let Some(txt) = self.paste_from_primary() {
892 let mut st = state_rc.borrow_mut();
893 st.insert_text_atomic(&txt);
894 self.notify_text_change(hit.id, st.text.clone());
895 if let Some(f) = &self.rt.frame_cache
896 && let Some(h) =
897 f.hit_regions.iter().find(|h| h.id == hit.id)
898 {
899 App::tf_ensure_caret_visible(&mut st, h.tf_multiline);
900 }
901 }
902 }
903 }
904 }
905 self.request_redraw();
906 }
907
908 WindowEvent::MouseInput {
909 state: ElementState::Released,
910 button: MouseButton::Left,
911 ..
912 } => {
913 let pos = Vec2 {
914 x: self.rt.mouse_pos_px.0,
915 y: self.rt.mouse_pos_px.1,
916 };
917
918 self.rt.handle_pointer_release(pos, PointerButton::Primary);
919
920 if let (Some(f), Some(cid)) = (&self.rt.frame_cache, self.rt.capture_id) {
922 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
923 && hit.rect.contains(pos)
924 && hit.on_click.is_some()
925 && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid)
926 {
927 let label = node.label.as_deref().unwrap_or("");
928 self.a11y.announce(&format!("Activated {}", label));
929 }
930 }
931
932 repose_core::request_frame();
933 }
934
935 WindowEvent::MouseInput {
936 state: ElementState::Released,
937 button: MouseButton::Middle,
938 ..
939 } => {
940 if let Some(f) = &self.rt.frame_cache {
941 let pos = Vec2 {
942 x: self.rt.mouse_pos_px.0,
943 y: self.rt.mouse_pos_px.1,
944 };
945 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
946 {
947 if let Some(cb) = &hit.on_pointer_up {
948 cb(PointerEvent::new(
949 PointerId(0),
950 PointerKind::Mouse,
951 PointerEventKind::Up(PointerButton::Tertiary),
952 pos,
953 1.0,
954 self.rt.modifiers,
955 ));
956 }
957 }
958 }
959 }
960
961 WindowEvent::ModifiersChanged(new_mods) => {
962 let state = new_mods.state();
963 self.rt.modifiers.shift = state.shift_key();
964 self.rt.modifiers.ctrl = state.control_key();
965 self.rt.modifiers.alt = state.alt_key();
966 self.rt.modifiers.meta = state.super_key();
967 self.rt.modifiers.command = if cfg!(target_os = "macos") {
968 self.rt.modifiers.meta
969 } else {
970 self.rt.modifiers.ctrl
971 };
972 }
973
974 WindowEvent::KeyboardInput {
975 event: key_event, ..
976 } => {
977 if key_event.state == ElementState::Pressed && !key_event.repeat {
981 match key_event.physical_key {
982 PhysicalKey::Code(KeyCode::BrowserBack)
983 | PhysicalKey::Code(KeyCode::Escape) => {
984 use repose_navigation::back;
985
986 if repose_core::dnd::handle_drag_action(
987 &repose_core::shortcuts::DragAction::Cancel,
988 ) {
989 return;
990 }
991
992 let mapped = rc::map_key(key_event.physical_key);
994 if self.dispatch_focus_key_event(&key_event, &mapped) {
995 self.request_redraw();
996 return;
997 }
998
999 if !back::handle() {
1000 }
1002 return;
1003 }
1004 _ => {}
1005 }
1006 }
1007
1008 if key_event.state == ElementState::Pressed
1010 && self.rt.modifiers.ctrl
1011 && self.rt.modifiers.shift
1012 && key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
1013 {
1014 self.inspector.hud.toggle_inspector();
1015 self.request_redraw();
1016 return;
1017 }
1018
1019 if key_event.state == ElementState::Pressed
1021 && !key_event.repeat
1022 && self.rt.modifiers.command
1023 {
1024 match key_event.physical_key {
1025 PhysicalKey::Code(KeyCode::KeyZ) if self.rt.modifiers.shift => {
1026 if let Some(fid) = self.rt.sched.focused {
1027 let key = self.tf_key_of(fid);
1028 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1029 let mut st = state_rc.borrow_mut();
1030 if st.can_redo() {
1031 st.redo();
1032 self.notify_text_change(fid, st.text.clone());
1033 self.request_redraw();
1034 return;
1035 }
1036 }
1037 }
1038 }
1039 PhysicalKey::Code(KeyCode::KeyZ) => {
1040 if let Some(fid) = self.rt.sched.focused {
1041 let key = self.tf_key_of(fid);
1042 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1043 let mut st = state_rc.borrow_mut();
1044 if st.can_undo() {
1045 st.undo();
1046 self.notify_text_change(fid, st.text.clone());
1047 self.request_redraw();
1048 return;
1049 }
1050 }
1051 }
1052 }
1053 _ => {}
1054 }
1055 }
1056
1057 let mapped = rc::map_key(key_event.physical_key);
1060 let ke = winit_key_to_repose(&key_event, &mapped, &self.rt.modifiers);
1061 let consumed = self.rt.handle_key(&ke);
1062 if consumed {
1063 self.request_redraw();
1064 return;
1065 }
1066
1067 if key_event.state == ElementState::Pressed
1072 && !key_event.repeat
1073 && !self.rt.ime_preedit
1074 && !self.rt.modifiers.ctrl
1075 && !self.rt.modifiers.alt
1076 && !self.rt.modifiers.meta
1077 && let Some(raw) = key_event.text.as_deref()
1078 {
1079 let text: String = raw
1080 .chars()
1081 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
1082 .collect();
1083 if !text.is_empty()
1084 && let Some(fid) = self.rt.sched.focused
1085 {
1086 let key = self.tf_key_of(fid);
1087 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1088 let mut st = state_rc.borrow_mut();
1089 st.insert_text(&text);
1090 self.notify_text_change(fid, st.text.clone());
1091 if let Some(f) = &self.rt.frame_cache
1092 && let Some(hit) =
1093 f.hit_regions.iter().find(|h| h.id == fid)
1094 {
1095 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1096 }
1097 self.request_redraw();
1098 return;
1099 }
1100 }
1101 }
1102
1103 if key_event.state == ElementState::Released
1105 && let Some(active_id) = self.rt.key_pressed_active
1106 {
1107 match key_event.physical_key {
1108 PhysicalKey::Code(KeyCode::Space)
1109 | PhysicalKey::Code(KeyCode::Enter) => {
1110 if let Some(f) = &self.rt.frame_cache
1111 && let Some(node) =
1112 f.semantics_nodes.iter().find(|n| n.id == active_id)
1113 {
1114 let label = node.label.as_deref().unwrap_or("");
1115 self.a11y.announce(&format!("Activated {}", label));
1116 }
1117 }
1118 _ => {}
1119 }
1120 }
1121 }
1122
1123 WindowEvent::Ime(ime) => {
1124 if let Some(focused_id) = self.rt.sched.focused {
1125 let key = self.tf_key_of(focused_id);
1126 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1127 let mut state = state_rc.borrow_mut();
1128 let on_text_change = self.rt
1129 .frame_cache
1130 .as_ref()
1131 .and_then(|f| f.hit_regions.iter().find(|h| h.id == focused_id))
1132 .and_then(|h| h.on_text_change.clone());
1133 let mut notify = |text: String| {
1134 if let Some(cb) = &on_text_change {
1135 cb(text);
1136 }
1137 };
1138 rc_android::handle_ime_event(
1139 ime,
1140 &mut state,
1141 &mut notify,
1142 &mut self.rt.ime_preedit,
1143 );
1144 self.request_redraw();
1145 }
1146 }
1147 }
1148
1149 WindowEvent::RedrawRequested => {
1150 if !self.redraw_requested.replace(false) {
1152 self.process_a11y_actions();
1153 self.process_render_commands();
1154 if let (Some(backend), Some(frame)) =
1156 (self.backend.as_mut(), self.rt.frame_cache.as_ref())
1157 {
1158 let scale = self.window.as_ref().map(|w| w.scale_factor() as f32).unwrap_or(1.0);
1159 let mut scene = frame.scene.clone();
1160 self.inspector.frame(&mut scene);
1161 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1162 }
1163 log::trace!("RedrawRequested: no frame request, skipping compose");
1164 return;
1165 }
1166 log::trace!("RedrawRequested: frame request pending, composing");
1167
1168 self.process_a11y_actions();
1170 self.process_render_commands();
1171
1172 let Some(win) = self.window.as_ref() else {
1173 return;
1174 };
1175 if self.backend.is_none() {
1176 return;
1177 }
1178
1179 repose_core::animation_driver::tick();
1182
1183 let t0 = Instant::now();
1184 let scale = win.scale_factor() as f32;
1185 self.rt.scale = scale;
1186 let focused = self.rt.sched.focused;
1187
1188 let output = self.rt.frame(&mut self.root, &self.render);
1189
1190 if let Some(cursor) = &output.platform.cursor {
1192 win.set_cursor(winit::window::Cursor::Icon(map_cursor(*cursor)));
1193 }
1194
1195 if !output.wants_keyboard && focused.is_some() && self.rt.sched.focused.is_none() && self.rt.ime_preedit {
1197 rc_web::set_ime_for_textfield(win, false);
1198 self.rt.ime_preedit = false;
1199 }
1200
1201 let frame = Frame {
1202 scene: output.scene,
1203 hit_regions: output.hit_regions,
1204 semantics_nodes: output.semantics_nodes,
1205 focus_chain: output.focus_chain,
1206 };
1207
1208 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1209
1210 if let Some(adapter) = &mut self.accesskit_adapter {
1212 let win = self.window.as_ref().unwrap();
1213 let scale = win.scale_factor();
1214 if let Some(update) =
1215 self.a11y_tree
1216 .update(&frame.semantics_nodes, scale, self.rt.sched.focused)
1217 {
1218 adapter.update_if_active(|| update);
1219 }
1220 }
1221
1222 let mut scene = frame.scene.clone();
1224 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1226 let signal_count = self.rt.sched.id_count() as usize;
1227 self.inspector.hud.metrics = Some(repose_devtools::Metrics {
1228 build_ms: build_layout_ms,
1229 layout_ms: build_layout_ms * 0.5,
1230 scene_nodes: scene.nodes.len(),
1231 widget_count,
1232 signal_count,
1233 });
1234 self.inspector.frame(&mut scene);
1235
1236 repose_core::dnd::overlay_drag_indicator(
1238 &mut scene,
1239 self.rt.mouse_pos_px,
1240 self.external_file_drag,
1241 );
1242
1243 self.process_render_commands();
1246
1247 let win = self.window.as_ref().unwrap();
1249 let scale = win.scale_factor() as f32;
1250 if let Some(backend) = self.backend.as_mut() {
1251 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1252 }
1253
1254 if let Some(fid) = self.rt.sched.focused {
1257 if let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
1258 && let Some(key) = hit.tf_state_key
1259 && !self.rt.textfield_states.contains_key(&key)
1260 {
1261 self.rt.textfield_states
1262 .entry(key)
1263 .or_insert_with(|| {
1264 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1265 })
1266 .borrow_mut()
1267 .reset_caret_blink();
1268 }
1269 }
1270
1271 self.rt.reconcile_hover_from_mouse_pos(&frame);
1272 repose_core::dnd::set_dnd_frame(Some(frame.clone()));
1273 self.rt.frame_cache = Some(frame);
1274 repose_core::dnd::set_dnd_scale(scale);
1275
1276 self.dispatch_file_drop_now();
1277
1278 rc::tick_snackbar(self.last_redraw);
1279 self.last_redraw = Instant::now();
1280 }
1281
1282 _ => {}
1283 }
1284 }
1285
1286 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1287 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1290 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK.lock().unwrap().as_ref() {
1291 cb();
1292 }
1293 process_deeplinks();
1294
1295 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1298 if WINDOW_VISIBLE.load(Ordering::Relaxed) && self.backend.is_none() {
1299 if let Some(w) = &self.window {
1300 log::info!("about_to_wait: recreating GPU backend");
1301 match repose_render_wgpu::WgpuBackend::new_with_msaa(
1302 w.clone(),
1303 self.msaa_samples,
1304 ) {
1305 Ok(b) => self.backend = Some(b),
1306 Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
1307 }
1308 }
1309 }
1310
1311 let needs_compose = take_frame_request();
1312 let needs_present = take_present_request();
1313
1314 if needs_compose {
1315 self.pending_redraw = true;
1316 }
1317
1318 if !self.pending_redraw && needs_present && self.rt.frame_cache.is_some() {
1320 let now = Instant::now();
1321 let interval = web_time::Duration::from_millis(16);
1322 if now.saturating_duration_since(self.last_redraw) >= interval {
1323 rc::request_redraw(&self.window);
1324 self.last_redraw = now;
1325 } else {
1326 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1327 self.last_redraw + interval,
1328 ));
1329 }
1330 return;
1331 }
1332
1333 if !self.pending_redraw {
1334 let now = Instant::now();
1335 let idle_cap = web_time::Duration::from_millis(1000);
1336 let deadline = self
1337 .next_caret_blink_deadline()
1338 .unwrap_or(now + idle_cap);
1339
1340 if now.saturating_duration_since(self.last_redraw) >= idle_cap || now >= deadline {
1341 self.redraw_requested.set(true);
1342 request_frame();
1343 rc::request_redraw(&self.window);
1344 self.last_redraw = now;
1345 }
1346 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1347 Ord::min(deadline, now + idle_cap),
1348 ));
1349 return;
1350 }
1351
1352 let now = Instant::now();
1353 let interval = web_time::Duration::from_millis(16);
1354
1355 if now.saturating_duration_since(self.last_redraw) >= interval {
1356 self.pending_redraw = false;
1357 self.redraw_requested.set(true);
1358 rc::request_redraw(&self.window);
1359 self.last_redraw = now;
1360 } else {
1361 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1362 self.last_redraw + interval,
1363 ));
1364 }
1365 }
1366
1367 fn new_events(
1368 &mut self,
1369 _: &winit::event_loop::ActiveEventLoop,
1370 _: winit::event::StartCause,
1371 ) {
1372 }
1373 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1374 self.pending_redraw = true;
1375 }
1376 fn device_event(
1377 &mut self,
1378 _: &winit::event_loop::ActiveEventLoop,
1379 _: winit::event::DeviceId,
1380 _: winit::event::DeviceEvent,
1381 ) {
1382 }
1383 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1384 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1385 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1386 }
1387
1388 impl App {
1389 fn dispatch_focus_key_event(
1392 &self,
1393 key_event: &winit::event::KeyEvent,
1394 mapped_key: &repose_core::input::Key,
1395 ) -> bool {
1396 let Some(f) = &self.rt.frame_cache else {
1397 return false;
1398 };
1399 let Some(focused) = self.rt.sched.focused else {
1400 return false;
1401 };
1402 let utf16 = match mapped_key {
1403 repose_core::input::Key::Character(c) => *c as u16,
1404 _ => 0,
1405 };
1406 let mods = self.rt.modifiers;
1407 let repeat = key_event.repeat;
1408 let ev_type = if key_event.state == ElementState::Pressed {
1409 repose_core::input::KeyEventType::Down
1410 } else {
1411 repose_core::input::KeyEventType::Up
1412 };
1413 let hit_by_id: std::collections::HashMap<u64, &HitRegion> =
1414 f.hit_regions.iter().map(|h| (h.id, h)).collect();
1415 let sem_parent_of: std::collections::HashMap<u64, u64> = f
1416 .semantics_nodes
1417 .iter()
1418 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1419 .collect();
1420 let mut ancestors = Vec::new();
1421 let mut cur = focused;
1422 loop {
1423 ancestors.push(cur);
1424 if let Some(&p) = sem_parent_of.get(&cur) {
1425 cur = p;
1426 } else {
1427 break;
1428 }
1429 }
1430 let make_ke = || repose_core::input::KeyEvent {
1431 key: mapped_key.clone(),
1432 modifiers: mods,
1433 is_repeat: repeat,
1434 event_type: ev_type,
1435 utf16_code_point: utf16,
1436 };
1437 for &id in ancestors.iter().rev() {
1439 if let Some(hit) = hit_by_id.get(&id) {
1440 if let Some(cb) = &hit.on_preview_key_event {
1441 if cb(make_ke()) {
1442 return true;
1443 }
1444 }
1445 }
1446 }
1447 for &id in ancestors.iter() {
1449 if let Some(hit) = hit_by_id.get(&id) {
1450 if let Some(cb) = &hit.on_key_event {
1451 if cb(make_ke()) {
1452 return true;
1453 }
1454 }
1455 }
1456 }
1457 false
1458 }
1459
1460 fn announce_focus_change(&mut self) {
1461 if let Some(f) = &self.rt.frame_cache {
1462 let focused_node = self.rt
1463 .sched
1464 .focused
1465 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1466 self.a11y.focus_changed(focused_node);
1467 }
1468 }
1469
1470 fn notify_text_change(&self, id: u64, text: String) {
1471 if let Some(f) = &self.rt.frame_cache
1472 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1473 && let Some(cb) = &h.on_text_change
1474 {
1475 cb(text);
1476 }
1477 }
1478
1479 fn tf_key_of(&self, visual_id: u64) -> u64 {
1480 rc::tf_key_of_in_frame(&self.rt.frame_cache, visual_id)
1481 }
1482
1483 fn next_caret_blink_deadline(&self) -> Option<Instant> {
1486 next_caret_blink_deadline(&self.rt.sched, &self.rt.frame_cache, &self.rt.textfield_states)
1487 }
1488
1489 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1490 use repose_core::shortcuts;
1491
1492 if let (Some(f), Some(fid)) = (&self.rt.frame_cache, self.rt.sched.focused)
1493 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1494 && let Some(cb) = &hit.on_action
1495 && cb(action.clone())
1496 {
1497 return true;
1498 }
1499
1500 if shortcuts::handle(action.clone()) {
1501 return true;
1502 }
1503
1504 if let Some(f) = &self.rt.frame_cache {
1506 if let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.rt.sched, f)
1507 {
1508 if let Some(active) = self.rt.key_pressed_active.take() {
1509 self.rt.pressed_ids.remove(&active);
1510 }
1511 let tf_state_key = f
1512 .hit_regions
1513 .iter()
1514 .find(|h| h.id == new_id)
1515 .and_then(|h| h.tf_state_key);
1516 if let Some(key) = tf_state_key {
1517 self.rt.textfield_states.entry(key).or_insert_with(|| {
1518 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1519 });
1520 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1521 state_rc.borrow_mut().reset_caret_blink();
1522 }
1523 }
1524 if let Some(win) = &self.window {
1525 let is_textfield = f.semantics_nodes.iter().any(|n| {
1526 n.id == new_id && n.role == repose_core::semantics::Role::TextField
1527 });
1528 rc_web::set_ime_for_textfield(win, is_textfield);
1529 }
1530 self.announce_focus_change();
1531 return true;
1532 }
1533 }
1534
1535 false
1536 }
1537
1538 fn dispatch_file_drop_now(&mut self) {
1539 let Some(f) = &self.rt.frame_cache else {
1540 self.pending_dropped_files.clear();
1541 self.pending_drop_pos_px = None;
1542 return;
1543 };
1544
1545 if self.pending_dropped_files.is_empty() {
1546 return;
1547 }
1548
1549 let pos_px = self.pending_drop_pos_px.unwrap_or(self.rt.mouse_pos_px);
1550 let pos = Vec2 {
1551 x: pos_px.0,
1552 y: pos_px.1,
1553 };
1554
1555 let mut files = Vec::new();
1556 for p in self.pending_dropped_files.drain(..) {
1557 let name = p
1558 .file_name()
1559 .and_then(|s| s.to_str())
1560 .unwrap_or("file")
1561 .to_string();
1562 files.push(repose_core::dnd::DroppedFile {
1563 name,
1564 path: Some(p),
1565 });
1566 }
1567
1568 let payload: repose_core::dnd::DragPayload =
1569 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1570
1571 let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
1572 self.pending_drop_pos_px = None;
1573 return;
1574 };
1575
1576 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
1577 && let Some(cb) = &hit.on_drop
1578 {
1579 let accepted = cb(repose_core::dnd::DropEvent {
1580 source_id: 0, target_id,
1582 position: pos,
1583 modifiers: self.rt.modifiers,
1584 payload: payload.clone(),
1585 });
1586
1587 if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
1588 {
1589 let label = node.label.as_deref().unwrap_or("");
1590 self.a11y.announce(&format!("Dropped files on {}", label));
1591 }
1592 }
1593
1594 self.pending_drop_pos_px = None;
1595 self.request_redraw();
1596 }
1597 }
1598
1599 let event_loop = EventLoop::new()?;
1600 set_event_loop_proxy(event_loop.create_proxy());
1601 let mut app = App::new(Box::new(root), msaa_samples);
1602 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1604 event_loop.run_app(&mut app)?;
1605 Ok(())
1606}
1607
1608pub trait A11yBridge: Send {
1616 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1618
1619 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1621
1622 fn announce(&mut self, msg: &str);
1624}
1625
1626struct NoopA11y;
1627impl A11yBridge for NoopA11y {
1628 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1629 }
1631 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1632 if let Some(n) = node {
1633 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1634 } else {
1635 log::info!("A11y focus: None");
1636 }
1637 }
1638 fn announce(&mut self, msg: &str) {
1639 log::info!("A11y announce: {msg}");
1640 }
1641}
1642
1643#[cfg(target_os = "linux")]
1644struct LinuxAtspiStub;
1645#[cfg(target_os = "linux")]
1646impl A11yBridge for LinuxAtspiStub {
1647 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1648 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1649 }
1650 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1651 if let Some(n) = node {
1652 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1653 } else {
1654 log::info!("AT-SPI stub focus: None");
1655 }
1656 }
1657 fn announce(&mut self, msg: &str) {
1658 log::info!("AT-SPI stub announce: {msg}");
1659 }
1660}