1#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
3use accesskit_winit::Adapter;
4use repose_core::a11y::ReposeActionHandler;
5use repose_core::*;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8use web_time::Instant;
9
10#[cfg(target_os = "android")]
11pub mod android;
12
13#[cfg(target_arch = "wasm32")]
14pub mod web;
15
16mod common;
17pub mod gamepad;
18pub mod render;
19pub mod runner_common;
20pub mod window_v2;
21
22use common as rc;
23
24pub use render::{ImageHandleGuard, RenderCommand, RenderContext};
25
26#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
27use winit::window::Window;
28
29#[cfg(not(target_arch = "wasm32"))]
30use std::sync::OnceLock;
31
32#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
33static APP_WINDOW: OnceLock<Arc<Window>> = OnceLock::new();
34
35#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
36static WINDOW_VISIBLE: AtomicBool = AtomicBool::new(true);
37
38#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
39static WINDOW_OCCLUDED: AtomicBool = AtomicBool::new(false);
40
41#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
42static CLOSE_TO_TRAY: AtomicBool = AtomicBool::new(false);
43
44#[cfg(not(target_arch = "wasm32"))]
45static EVENT_LOOP_PROXY: OnceLock<winit::event_loop::EventLoopProxy<()>> = OnceLock::new();
46
47#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
49static ABOUT_TO_WAIT_CALLBACK: Mutex<Option<Box<dyn Fn() + Send>>> = Mutex::new(None);
50
51pub use repose_app::lifecycle::{
52 AppLifecycle, add_deeplink_listener, add_lifecycle_listener, current_lifecycle,
53 process_deeplinks, process_lifecycle, remove_deeplink_listener, remove_lifecycle_listener,
54 run_pre_redraw, set_on_deeplink, set_on_lifecycle, set_pre_redraw,
55};
56
57#[cfg(target_os = "android")]
62pub(crate) fn push_lifecycle(state: AppLifecycle) {
63 repose_app::lifecycle::push_lifecycle(state);
64 #[cfg(not(target_arch = "wasm32"))]
65 wake_event_loop();
66}
67
68#[cfg(not(target_os = "android"))]
71pub fn push_lifecycle(state: AppLifecycle) {
72 repose_app::lifecycle::push_lifecycle(state);
73 #[cfg(not(target_arch = "wasm32"))]
74 wake_event_loop();
75}
76
77#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
78fn push_runtime_window_lifecycle(rt: &mut repose_app::ReposeRuntime, visible: bool) {
79 let foreground = visible && !WINDOW_OCCLUDED.load(Ordering::Relaxed);
80 let state = if foreground {
81 AppLifecycle::Foreground
82 } else {
83 AppLifecycle::Background
84 };
85 if rt.current_lifecycle() != Some(state) {
86 rt.push_lifecycle(state);
87 }
88}
89
90#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
93pub fn push_window_lifecycle(visible: bool) {
94 let foreground = visible && !WINDOW_OCCLUDED.load(Ordering::Relaxed);
95 let state = if foreground {
96 AppLifecycle::Foreground
97 } else {
98 AppLifecycle::Background
99 };
100 if current_lifecycle() != Some(state) {
101 push_lifecycle(state);
102 }
103}
104
105pub fn push_deeplink(data: Vec<u8>) {
107 repose_app::lifecycle::push_deeplink(data);
108 #[cfg(not(target_arch = "wasm32"))]
109 if let Some(proxy) = EVENT_LOOP_PROXY.get() {
110 let _ = proxy.send_event(());
111 }
112}
113
114#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
116pub fn set_app_window(window: Arc<Window>) {
117 let _ = APP_WINDOW.set(window);
118}
119
120#[cfg(not(target_arch = "wasm32"))]
122pub fn set_event_loop_proxy(proxy: winit::event_loop::EventLoopProxy<()>) {
123 let _ = EVENT_LOOP_PROXY.set(proxy);
124}
125
126#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
128pub fn set_about_to_wait_callback(cb: Box<dyn Fn() + Send>) {
129 *ABOUT_TO_WAIT_CALLBACK
130 .lock()
131 .unwrap_or_else(|e| e.into_inner()) = Some(cb);
132}
133
134#[cfg(not(target_arch = "wasm32"))]
136pub fn wake_event_loop() {
137 if let Some(proxy) = EVENT_LOOP_PROXY.get() {
138 let _ = proxy.send_event(());
139 }
140}
141
142#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
146pub fn show_app_window() {
147 WINDOW_VISIBLE.store(true, Ordering::Relaxed);
148 if let Some(w) = APP_WINDOW.get() {
149 log::info!("show_app_window: calling set_visible(true)");
150 w.set_visible(true);
151 #[allow(deprecated)]
152 w.focus_window();
153 }
154 push_window_lifecycle(true);
155 repose_core::frame_clock::request_frame();
156 wake_event_loop();
157}
158
159#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
160pub fn hide_app_window() {
161 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
162 if let Some(w) = APP_WINDOW.get() {
163 log::info!("hide_app_window: calling set_visible(false)");
164 w.set_visible(false);
165 }
166 push_window_lifecycle(false);
167 repose_core::frame_clock::request_frame();
168 wake_event_loop();
169}
170
171#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
173pub fn window_is_visible() -> bool {
174 WINDOW_VISIBLE.load(Ordering::Relaxed)
175}
176
177#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
180pub fn set_close_to_tray(enabled: bool) {
181 CLOSE_TO_TRAY.store(enabled, Ordering::Relaxed);
182}
183
184pub use repose_ui::textfield::tf_ensure_visible_in_rect;
186
187#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
188use common::{cursor_is_hidden, map_cursor};
189
190pub use repose_app::{AndroidOptions, AppConfig, ReposeOptions};
191
192#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
197#[deprecated(
198 note = "use run_desktop_app_with_config(root, AppConfig) instead; this may be removed in a future release"
199)]
200pub fn run_desktop_app(
201 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
202) -> anyhow::Result<()> {
203 run_desktop_app_with_config(root, AppConfig::default())
204}
205
206#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
208pub fn run_desktop_app_with_config(
209 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
210 config: AppConfig,
211) -> anyhow::Result<()> {
212 use winit::application::ApplicationHandler;
213 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
214 use winit::event::{ElementState, MouseButton, WindowEvent};
215 use winit::event_loop::EventLoop;
216 use winit::keyboard::{KeyCode, PhysicalKey};
217 use winit::window::{Window, WindowAttributes};
218
219 use repose_app::ReposeRuntime;
220 use repose_core::a11y::A11yTree;
221
222 struct ReposeActivationHandler {
223 active: Arc<AtomicBool>,
224 needs_full: Arc<AtomicBool>,
225 }
226
227 impl accesskit::ActivationHandler for ReposeActivationHandler {
228 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
229 self.active.store(true, Ordering::Release);
230 self.needs_full.store(true, Ordering::Release);
231 wake_event_loop();
232 Some(A11yTree::initial_tree())
233 }
234 }
235
236 struct ReposeDeactivationHandler {
237 active: Arc<AtomicBool>,
238 }
239
240 impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
241 fn deactivate_accessibility(&mut self) {
242 self.active.store(false, Ordering::Release);
243 }
244 }
245
246 #[derive(Clone, Copy, PartialEq)]
247 struct ImeState {
248 allowed: bool,
249 purpose: repose_core::ImePurposeHint,
250 auto_correct: bool,
251 capitalization: repose_core::KeyboardCapitalization,
252 cursor_area: Option<(f64, f64, f64, f64)>,
253 }
254
255 impl Default for ImeState {
256 fn default() -> Self {
257 Self {
258 allowed: false,
259 purpose: repose_core::ImePurposeHint::Normal,
260 auto_correct: true,
261 capitalization: repose_core::KeyboardCapitalization::Unspecified,
262 cursor_area: None,
263 }
264 }
265 }
266
267 struct App {
268 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
269 render: RenderContext,
270 window: Option<Arc<Window>>,
271 backend: Option<repose_render_wgpu::WgpuBackend>,
272 rt: ReposeRuntime,
273 inspector: Option<repose_devtools::Inspector>,
274 msaa_samples: u32,
275 present_mode: PresentModePref,
276 window_title: String,
277 window_size: (u32, u32),
278
279 pending_dropped_files: Vec<std::path::PathBuf>,
281 pending_drop_pos_px: Option<(f32, f32)>,
282
283 external_file_drag: bool,
285 hovered_files: Vec<std::path::PathBuf>,
286
287 a11y: Box<dyn A11yBridge>,
288
289 accesskit_adapter: Option<Adapter>,
290 a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
291 a11y_tree: A11yTree,
292 a11y_active: Arc<AtomicBool>,
293 a11y_needs_full: Arc<AtomicBool>,
294
295 last_window_theme: Option<bool>,
297
298 custom_cursor: Option<(u64, winit::window::CustomCursor)>,
299 last_cursor_key: Option<(u8, u64)>,
300
301 frame_pacer: rc::FramePacer,
302 redraw_deferred: bool,
303 occluded: bool,
304 os_focused: bool,
305 ime_output_allowed: bool,
306 ime_state: ImeState,
307
308 touch_gestures: rc::TouchGestureState,
310
311 gamepad: Option<Box<dyn gamepad::GamepadBackend>>,
312 }
313
314 impl App {
315 fn process_a11y_actions(&mut self) {
316 let mut actions = self.a11y_actions.lock().unwrap_or_else(|e| e.into_inner());
317 if actions.is_empty() {
318 return;
319 }
320 let pending = actions.drain(..).collect::<Vec<_>>();
321 drop(actions);
322
323 let Some(f) = &self.rt.frame_cache else {
324 return;
325 };
326
327 for req in pending {
328 let target_id = req.target_node.0;
329 match req.action {
330 accesskit::Action::Click => {
331 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
332 && let Some(cb) = &hit.on_click
333 {
334 cb();
335 self.request_redraw();
336 }
337 }
338 accesskit::Action::Focus => {
339 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
341 self.rt.sched.focused = Some(target_id);
342 self.request_redraw();
343 }
344 _ => {}
345 }
346 }
347 }
348
349 fn new(
350 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
351 config: AppConfig,
352 ) -> Self {
353 Self {
354 root,
355 render: RenderContext::new(),
356 window: None,
357 backend: None,
358 rt: ReposeRuntime::new(),
359 inspector: if config.enable_inspector {
360 Some(repose_devtools::Inspector::new())
361 } else {
362 None
363 },
364 msaa_samples: config.common.msaa_samples,
365 present_mode: config.common.present_mode,
366 window_title: config.window_title,
367 window_size: config.window_size,
368 pending_dropped_files: Vec::new(),
369 pending_drop_pos_px: None,
370
371 external_file_drag: false,
372 hovered_files: Vec::new(),
373
374 a11y: {
375 #[cfg(target_os = "linux")]
376 {
377 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
378 }
379 #[cfg(not(target_os = "linux"))]
380 {
381 Box::new(NoopA11y) as Box<dyn A11yBridge>
382 }
383 },
384
385 accesskit_adapter: None,
386 a11y_actions: Arc::new(Mutex::new(Vec::new())),
387 a11y_tree: A11yTree::default(),
388 a11y_active: Arc::new(AtomicBool::new(false)),
389 a11y_needs_full: Arc::new(AtomicBool::new(false)),
390
391 frame_pacer: rc::FramePacer::new(config.common.max_fps),
392 redraw_deferred: false,
393 occluded: false,
394 os_focused: true,
395 ime_output_allowed: false,
396 ime_state: ImeState::default(),
397 last_window_theme: None,
398 custom_cursor: None,
399 last_cursor_key: None,
400 touch_gestures: rc::TouchGestureState::default(),
401 gamepad: gamepad::create_backend()
402 .map(|b| Box::new(b) as Box<dyn gamepad::GamepadBackend>),
403 }
404 }
405
406 fn request_redraw(&self) {
407 repose_core::request_frame();
408 }
409
410 fn ime_allowed_for_frame(&self, frame: &repose_core::runtime::Frame) -> bool {
411 WINDOW_VISIBLE.load(Ordering::Relaxed)
412 && !WINDOW_OCCLUDED.load(Ordering::Relaxed)
413 && self.ime_output_allowed
414 && self.rt.sched.window_focused
415 && self
416 .rt
417 .sched
418 .focused
419 .is_some_and(|id| rc::is_editable_textfield_hit(frame, id))
420 }
421
422 fn ime_allowed_for_output(
423 &self,
424 hits: &[repose_core::HitRegion],
425 semantics: &[repose_core::runtime::SemNode],
426 output_allowed: bool,
427 ) -> bool {
428 WINDOW_VISIBLE.load(Ordering::Relaxed)
429 && !WINDOW_OCCLUDED.load(Ordering::Relaxed)
430 && output_allowed
431 && self.rt.sched.window_focused
432 && self
433 .rt
434 .sched
435 .focused
436 .is_some_and(|id| rc::editable_textfield_hit(hits, semantics, id).is_some())
437 }
438
439 fn sync_ime_for_focus(&mut self) {
440 let hit = self.rt.frame_cache.as_ref().and_then(|frame| {
441 (self.ime_allowed_for_frame(frame))
442 .then(|| {
443 self.rt.sched.focused.and_then(|id| {
444 rc::editable_textfield_hit(
445 &frame.hit_regions,
446 &frame.semantics_nodes,
447 id,
448 )
449 })
450 })
451 .flatten()
452 });
453 let state = match hit {
454 Some(hit) => {
455 let scale = self.window.as_ref().map(|window| window.scale_factor());
456 ImeState {
457 allowed: true,
458 purpose: hit.keyboard_type.ime_purpose_hint(),
459 auto_correct: hit.auto_correct.unwrap_or(true),
460 capitalization: hit.capitalization,
461 cursor_area: scale.map(|scale| {
462 (
463 hit.rect.x as f64 / scale,
464 hit.rect.y as f64 / scale,
465 hit.rect.w as f64 / scale,
466 hit.rect.h as f64 / scale,
467 )
468 }),
469 }
470 }
471 None => ImeState::default(),
472 };
473 self.apply_ime_state(state);
474 }
475
476 fn apply_ime_state(&mut self, state: ImeState) {
477 if self.ime_state == state {
478 return;
479 }
480 let Some(window) = self.window.clone() else {
481 return;
482 };
483 self.ime_state = state;
484 if state.allowed {
485 rc::set_ime_for_textfield_ex(
486 &window,
487 true,
488 state.purpose,
489 state.auto_correct,
490 state.capitalization,
491 );
492 if let Some((x, y, w, h)) = state.cursor_area {
493 window.set_ime_cursor_area(LogicalPosition::new(x, y), LogicalSize::new(w, h));
494 }
495 } else {
496 rc::set_ime_for_textfield(&window, false);
497 self.rt.finish_compositions();
498 }
499 }
500
501 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
502 if self.rt.dispatch_action(action) {
503 self.sync_ime_for_focus();
504 return true;
505 }
506 false
507 }
508
509 fn paste_from_primary(&self) -> Option<String> {
510 let mut opts = clipawl::ClipboardOptions::default();
511 opts.linux.selection = clipawl::LinuxSelection::Primary;
512 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
513 match pollster::block_on(cb.read()) {
514 Ok(t) => Some(t),
515 Err(e) => {
516 eprintln!("Primary paste error: {}", e);
517 None
518 }
519 }
520 } else {
521 None
522 }
523 }
524
525 fn process_render_commands(&mut self) {
526 let Some(backend) = self.backend.as_mut() else {
527 return;
528 };
529 repose_render_wgpu::apply_render_commands(backend, self.render.drain());
530 }
531
532 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
536 fn custom_cursor_key(img: &repose_core::CustomCursorImage) -> u64 {
537 let mut h: u64 = 0xcbf29ce484222325;
538 for b in img.rgba.iter().copied().chain(
539 img.size
540 .iter()
541 .flat_map(|v| v.to_le_bytes())
542 .chain(img.hotspot.iter().flat_map(|v| v.to_le_bytes())),
543 ) {
544 h ^= b as u64;
545 h = h.wrapping_mul(0x100000001b3);
546 }
547 h ^= (img.rgba.len() as u64).wrapping_mul(0x9e3779b97f4a7c15);
548 h
549 }
550
551 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
554 fn cursor_key(c: &repose_core::CursorIcon) -> (u8, u64) {
555 match c {
556 repose_core::CursorIcon::Default => (0, 0),
557 repose_core::CursorIcon::Pointer => (1, 0),
558 repose_core::CursorIcon::Text => (2, 0),
559 repose_core::CursorIcon::EwResize => (3, 0),
560 repose_core::CursorIcon::NsResize => (4, 0),
561 repose_core::CursorIcon::NwseResize => (5, 0),
562 repose_core::CursorIcon::NeswResize => (6, 0),
563 repose_core::CursorIcon::Grab => (7, 0),
564 repose_core::CursorIcon::Grabbing => (8, 0),
565 repose_core::CursorIcon::Hidden => (9, 0),
566 repose_core::CursorIcon::Custom(img) => (10, Self::custom_cursor_key(img)),
567 }
568 }
569
570 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
576 fn apply_cursor_static(
577 win: &std::sync::Arc<winit::window::Window>,
578 cache: &mut Option<(u64, winit::window::CustomCursor)>,
579 last_key: &mut Option<(u8, u64)>,
580 el: Option<&winit::event_loop::ActiveEventLoop>,
581 c: &repose_core::CursorIcon,
582 ) {
583 let key = Self::cursor_key(c);
584 if *last_key == Some(key) {
585 return;
586 }
587 *last_key = Some(key);
588 if let repose_core::CursorIcon::Custom(img) = c {
589 let hash = key.1;
590 let hit = cache
591 .as_ref()
592 .filter(|(k, _)| *k == hash)
593 .map(|(_, h)| h.clone());
594 let custom = match hit {
595 Some(h) => Some(h),
596 None => match el {
597 Some(el) => {
598 match winit::window::CustomCursor::from_rgba(
599 img.rgba.to_vec(),
600 img.size[0],
601 img.size[1],
602 img.hotspot[0],
603 img.hotspot[1],
604 ) {
605 Ok(source) => {
606 let h = el.create_custom_cursor(source);
607 *cache = Some((hash, h.clone()));
608 Some(h)
609 }
610 Err(e) => {
611 log::warn!("invalid custom cursor, falling back: {e:?}");
612 *cache = None;
613 None
614 }
615 }
616 }
617 None => cache.as_ref().map(|(_, h)| h.clone()),
618 },
619 };
620 win.set_cursor_visible(true);
621 match custom {
622 Some(h) => win.set_cursor(h),
623 None => win.set_cursor(map_cursor(c)),
624 }
625 return;
626 }
627 *cache = cache.take().and(None);
628 win.set_cursor_visible(!cursor_is_hidden(c));
629 win.set_cursor(map_cursor(c));
630 }
631
632 fn clear_input_state(&mut self) {
633 let active_touches = self
634 .touch_gestures
635 .active_touches()
636 .iter()
637 .map(|(id, pos)| (*id, *pos))
638 .collect::<Vec<_>>();
639 for (touch_id, pos) in active_touches {
640 self.touch_gestures
641 .touch_ended(&mut self.rt, touch_id, pos, true);
642 self.touch_gestures.contact_up(touch_id);
643 }
644 self.touch_gestures = rc::TouchGestureState::default();
645 self.rt.handle_focus_lost();
646 self.rt.touch_paths.clear();
647 self.rt.scroll_capture_id = None;
648 self.rt.hover_id = None;
649 self.rt.hover_ancestors.clear();
650 self.rt.last_focus = None;
651 self.rt.sched.pointer_pos_px = None;
652 self.rt.ime_preedit = false;
653 self.rt.modifiers = Default::default();
654 self.rt.sched.window_focused = false;
655 self.rt.sched.held_keys.clear();
656 self.rt.sched.mouse_primary = false;
657 self.rt.sched.mouse_secondary = false;
658 self.rt.sched.mouse_middle = false;
659 self.rt.pressed_ids.clear();
660 self.rt.key_pressed_active = None;
661 self.external_file_drag = false;
662 self.hovered_files.clear();
663 self.apply_ime_state(ImeState::default());
664 }
665
666 fn reset_pointer_state(&mut self) {
667 self.rt.capture_id = None;
668 self.rt.pressed_ids.clear();
669 self.rt.hover_id = None;
670 }
671 }
672
673 impl ApplicationHandler<()> for App {
674 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
675 let _event_scope =
676 repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
677 self.occluded = false;
678 WINDOW_OCCLUDED.store(false, Ordering::Relaxed);
679 self.rt.sched.window_focused = self.os_focused;
680 let _ = rc::setup_clipboard();
681
682 if self.window.is_none() {
683 match el.create_window(
684 WindowAttributes::default()
685 .with_title(self.window_title.clone())
686 .with_inner_size(PhysicalSize::new(self.window_size.0, self.window_size.1))
687 .with_visible(false),
688 ) {
689 Ok(win) => {
690 let w = Arc::new(win);
691
692 let activation_handler = ReposeActivationHandler {
693 active: self.a11y_active.clone(),
694 needs_full: self.a11y_needs_full.clone(),
695 };
696
697 let action_handler = ReposeActionHandler {
698 pending_actions: self.a11y_actions.clone(),
699 };
700
701 let deactivation_handler = ReposeDeactivationHandler {
702 active: self.a11y_active.clone(),
703 };
704
705 let adapter = Adapter::with_direct_handlers(
706 el,
707 &w,
708 activation_handler,
709 action_handler,
710 deactivation_handler,
711 );
712
713 self.accesskit_adapter = Some(adapter);
714
715 w.set_visible(true);
716
717 let size = w.inner_size();
718 let sf = w.scale_factor() as f32;
719 self.rt.set_viewport_and_scale(size.width, size.height, sf);
720
721 match repose_render_wgpu::WgpuBackend::new_with_options(
722 w.clone(),
723 self.msaa_samples,
724 self.present_mode,
725 ) {
726 Ok(mut b) => {
727 b.set_pixels_per_point(sf);
728 repose_render_wgpu::offscreen::set_shared_device(
729 b.device.clone(),
730 b.queue.clone(),
731 );
732 self.backend = Some(b);
733 set_app_window(w.clone());
734 self.window = Some(w);
735 self.request_redraw();
736 }
737 Err(e) => {
738 log::error!("Failed to create WGPU backend: {e:?}");
739 el.exit();
740 }
741 }
742 }
743 Err(e) => {
744 log::error!("Failed to create window: {e:?}");
745 el.exit();
746 }
747 }
748 }
749 if self.window.is_some() {
750 push_runtime_window_lifecycle(&mut self.rt, WINDOW_VISIBLE.load(Ordering::Relaxed));
751 }
752 }
753
754 fn window_event(
755 &mut self,
756 el: &winit::event_loop::ActiveEventLoop,
757 _id: winit::window::WindowId,
758 event: WindowEvent,
759 ) {
760 let _event_scope =
761 repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
762 let _dnd_guard = self.rt.dnd_context.enter();
763 if let (Some(adapter), Some(window)) = (&mut self.accesskit_adapter, &self.window) {
765 adapter.process_event(window, &event);
766 }
767
768 match event {
769 WindowEvent::CloseRequested => {
770 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
771 self.backend = None;
773 if let Some(w) = &self.window {
774 w.set_visible(false);
775 }
776 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
777 push_runtime_window_lifecycle(&mut self.rt, false);
778 } else {
779 el.exit();
780 }
781 }
782
783 WindowEvent::Focused(focused) => {
784 self.os_focused = focused;
785 let focused = focused && !self.occluded;
786 self.rt.sched.window_focused = focused;
787 if !focused {
788 self.clear_input_state();
789 } else {
790 self.sync_ime_for_focus();
791 }
792 self.request_redraw();
793 }
794
795 WindowEvent::Occluded(occluded) => {
796 self.occluded = occluded;
797 WINDOW_OCCLUDED.store(occluded, Ordering::Relaxed);
798 if occluded {
799 self.rt.sched.window_focused = false;
800 self.clear_input_state();
801 } else {
802 self.rt.sched.window_focused = self.os_focused;
803 if self.os_focused {
804 self.sync_ime_for_focus();
805 }
806 }
807 push_runtime_window_lifecycle(
808 &mut self.rt,
809 WINDOW_VISIBLE.load(Ordering::Relaxed),
810 );
811 self.request_redraw();
812 }
813
814 WindowEvent::CursorLeft { .. } => {
815 self.rt.pointer_inside = false;
816 self.rt.handle_pointer_cancel();
817 self.external_file_drag = false;
818 self.hovered_files.clear();
819 self.request_redraw();
820 }
821
822 WindowEvent::HoveredFile(path) => {
823 self.external_file_drag = true;
825 if self.hovered_files.len() < 32 {
826 self.hovered_files.push(path);
827 }
828 if self.pending_drop_pos_px.is_none() {
829 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
830 }
831 self.request_redraw();
832 }
833
834 WindowEvent::HoveredFileCancelled => {
835 self.external_file_drag = false;
836 self.hovered_files.clear();
837
838 self.reset_pointer_state();
840
841 self.request_redraw();
842 }
843
844 WindowEvent::DroppedFile(path) => {
845 self.pending_dropped_files.push(path);
847 if self.pending_drop_pos_px.is_none() {
848 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
849 }
850
851 self.external_file_drag = false;
853 self.hovered_files.clear();
854
855 self.request_redraw();
856 }
857
858 WindowEvent::Resized(size) => {
859 let sf = self
860 .window
861 .as_ref()
862 .map(|w| w.scale_factor() as f32)
863 .unwrap_or(1.0);
864 rc::sync_viewport(&mut self.rt, &mut self.backend, size, sf);
865 if let Some(w) = &self.window {
866 let sf = w.scale_factor() as f32;
867 let dp_w = size.width as f32 / sf;
868 let dp_h = size.height as f32 / sf;
869 log::info!(
870 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
871 size.width,
872 size.height,
873 sf,
874 dp_w as i32,
875 dp_h as i32
876 );
877 }
878 self.request_redraw();
879 }
880
881 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
882 let size = self
883 .window
884 .as_ref()
885 .map(|w| w.inner_size())
886 .unwrap_or_else(|| PhysicalSize::new(0, 0));
887 rc::sync_viewport(&mut self.rt, &mut self.backend, size, scale_factor as f32);
888 self.request_redraw();
889 }
890
891 WindowEvent::CursorMoved { position, .. } => {
892 self.rt.pointer_inside = true;
893
894 if self.external_file_drag {
895 self.pending_drop_pos_px = Some((position.x as f32, position.y as f32));
896 }
897
898 let pos = Vec2 {
899 x: position.x as f32,
900 y: position.y as f32,
901 };
902
903 let result = self.rt.handle_pointer_move(pos);
905
906 if let (Some(inspector), Some(f)) = (&mut self.inspector, &self.rt.frame_cache)
908 && inspector.hud.inspector_enabled
909 {
910 let hit = f.hit_regions.iter().find(|h| h.rect.contains(pos));
911 let hover_rect = hit.map(|h| h.rect);
912 let hover_info = hit.and_then(|h| {
913 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
914 repose_devtools::HoveredInfo {
915 id: s.id,
916 role: format!("{:?}", s.role),
917 label: s.label.clone(),
918 }
919 })
920 });
921 inspector.hud.set_hovered(hover_rect, hover_info);
922 }
923
924 if let Some(win) = &self.window
925 && let Some(c) = result.cursor
926 {
927 Self::apply_cursor_static(
928 win,
929 &mut self.custom_cursor,
930 &mut self.last_cursor_key,
931 Some(el),
932 &c,
933 );
934 }
935
936 self.request_redraw();
937 }
938
939 WindowEvent::MouseWheel { delta, .. } => {
940 let scale = self
941 .window
942 .as_ref()
943 .map(|w| w.scale_factor() as f32)
944 .unwrap_or(1.0);
945 if crate::runner_common::on_mouse_wheel(&mut self.rt, delta, scale) {
946 self.request_redraw();
947 }
948 }
949
950 WindowEvent::MouseInput { state, button, .. } => {
951 let pos = Vec2 {
952 x: self.rt.mouse_pos_px.0,
953 y: self.rt.mouse_pos_px.1,
954 };
955
956 let mapped = match button {
957 MouseButton::Left => PointerButton::Primary,
958 MouseButton::Right => PointerButton::Secondary,
959 MouseButton::Middle => PointerButton::Tertiary,
960 _ => return,
962 };
963 match (mapped, state) {
964 (PointerButton::Primary, ElementState::Pressed) => {
965 self.rt.sched.mouse_primary = true;
966 }
967 (PointerButton::Primary, ElementState::Released) => {
968 self.rt.sched.mouse_primary = false;
969 }
970 (PointerButton::Secondary, ElementState::Pressed) => {
971 self.rt.sched.mouse_secondary = true;
972 }
973 (PointerButton::Secondary, ElementState::Released) => {
974 self.rt.sched.mouse_secondary = false;
975 }
976 (PointerButton::Tertiary, ElementState::Pressed) => {
977 self.rt.sched.mouse_middle = true;
978 }
979 (PointerButton::Tertiary, ElementState::Released) => {
980 self.rt.sched.mouse_middle = false;
981 }
982 }
983
984 match state {
985 ElementState::Pressed => {
986 let result = self.rt.handle_pointer_press(pos, mapped);
987
988 self.sync_ime_for_focus();
989
990 if result.focused.is_none() {
991 self.rt.ime_preedit = false;
992 }
993
994 if result.needs_a11y_announce {
995 self.announce_focus_change();
996 }
997
998 if matches!(mapped, PointerButton::Tertiary)
1000 && let Some(f) = &self.rt.frame_cache
1001 && let Some(cid) = self.rt.capture_id
1002 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1003 && self.rt.sched.window_focused
1004 && rc::is_editable_textfield_hit(f, hit.id)
1005 && let Some(txt) = self.paste_from_primary()
1006 {
1007 self.rt.paste_into_focused(&txt);
1008 }
1009
1010 if matches!(mapped, PointerButton::Primary | PointerButton::Secondary)
1012 && let Some(inspector) = &mut self.inspector
1013 && inspector.hud.inspector_enabled
1014 && let Some(f) = &self.rt.frame_cache
1015 && let Some(hit) =
1016 f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
1017 {
1018 let info =
1019 f.semantics_nodes.iter().find(|s| s.id == hit.id).map(|s| {
1020 repose_devtools::HoveredInfo {
1021 id: s.id,
1022 role: format!("{:?}", s.role),
1023 label: s.label.clone(),
1024 }
1025 });
1026 inspector
1027 .hud
1028 .select_widget(repose_devtools::SelectedWidget {
1029 id: hit.id,
1030 role: info
1031 .as_ref()
1032 .map(|i| i.role.clone())
1033 .unwrap_or_default(),
1034 label: info.as_ref().and_then(|i| i.label.clone()),
1035 bounds: hit.rect,
1036 });
1037 }
1038
1039 self.request_redraw();
1040 }
1041
1042 ElementState::Released => {
1043 let result = self.rt.handle_pointer_release(pos, mapped);
1044
1045 if let Some(cid) = result.clicked_id
1049 && let Some(f) = &self.rt.frame_cache
1050 && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid)
1051 {
1052 let label = node.label.as_deref().unwrap_or("");
1053 self.a11y.announce(&format!("Activated {}", label));
1054 }
1055
1056 self.request_redraw();
1057 }
1058 }
1059 }
1060
1061 WindowEvent::Touch(t) => {
1062 let scale = self
1063 .window
1064 .as_ref()
1065 .map(|w| w.scale_factor() as f32)
1066 .unwrap_or(1.0);
1067 let r = crate::runner_common::handle_touch_raw(
1068 &mut self.rt,
1069 &mut self.touch_gestures,
1070 &t,
1071 scale,
1072 );
1073 if matches!(
1074 t.phase,
1075 winit::event::TouchPhase::Started | winit::event::TouchPhase::Ended
1076 ) {
1077 self.sync_ime_for_focus();
1078 }
1079 let mut dirty = r.dirty;
1080 if let Some((delta_scale, center)) = r.pinch
1081 && self.dispatch_action(repose_core::shortcuts::Action::Gesture(
1082 repose_core::shortcuts::Gesture::PinchWithCenter {
1083 delta_scale,
1084 center,
1085 },
1086 ))
1087 {
1088 dirty = true;
1089 }
1090 if let Some((delta, center)) = r.pan
1091 && self.dispatch_action(repose_core::shortcuts::Action::Gesture(
1092 repose_core::shortcuts::Gesture::Pan { delta, center },
1093 ))
1094 {
1095 dirty = true;
1096 }
1097 if let Some((delta_rotation, center)) = r.rotation
1098 && self.dispatch_action(repose_core::shortcuts::Action::Gesture(
1099 repose_core::shortcuts::Gesture::Rotate {
1100 delta_rotation,
1101 center,
1102 },
1103 ))
1104 {
1105 dirty = true;
1106 }
1107 if let Some(right) = r.swipe_right {
1108 let g = if right {
1109 repose_core::shortcuts::Gesture::SwipeRight
1110 } else {
1111 repose_core::shortcuts::Gesture::SwipeLeft
1112 };
1113 if self.dispatch_action(repose_core::shortcuts::Action::Gesture(g)) {
1114 dirty = true;
1115 }
1116 }
1117 if dirty {
1118 self.request_redraw();
1119 }
1120 }
1121
1122 WindowEvent::ModifiersChanged(new_mods) => {
1123 crate::runner_common::on_modifiers_changed(&mut self.rt, &new_mods.state());
1124 }
1125
1126 WindowEvent::KeyboardInput {
1127 event: key_event, ..
1128 } => {
1129 if crate::runner_common::on_keyboard_input(
1130 &mut self.rt,
1131 &key_event,
1132 &mut self.inspector,
1133 ) {
1134 self.request_redraw();
1135 return;
1136 }
1137
1138 if key_event.state == ElementState::Pressed
1139 && !key_event.repeat
1140 && (rc::is_back_key(&key_event) || rc::is_escape_key(&key_event))
1141 {
1142 if self.rt.overlay.handle_back() {
1143 self.request_redraw();
1144 return;
1145 }
1146 use repose_navigation::back;
1147 if back::handle() {
1148 self.request_redraw();
1149 return;
1150 }
1151 if rc::is_back_key(&key_event) {
1152 el.exit();
1153 return;
1154 }
1155 }
1156
1157 if key_event.state == ElementState::Released
1159 && let Some(active_id) = self.rt.key_pressed_active
1160 {
1161 match key_event.physical_key {
1162 PhysicalKey::Code(KeyCode::Space)
1163 | PhysicalKey::Code(KeyCode::Enter) => {
1164 if let Some(f) = &self.rt.frame_cache
1165 && let Some(node) =
1166 f.semantics_nodes.iter().find(|n| n.id == active_id)
1167 {
1168 let label = node.label.as_deref().unwrap_or("");
1169 self.a11y.announce(&format!("Activated {}", label));
1170 }
1171 }
1172 _ => {}
1173 }
1174 }
1175 }
1176
1177 WindowEvent::Ime(ime) => {
1178 crate::runner_common::on_ime(&mut self.rt, &ime);
1179 self.request_redraw();
1180 }
1181
1182 WindowEvent::RedrawRequested => {
1183 crate::run_pre_redraw(&self.render);
1185
1186 let now = Instant::now();
1187 let has_frame = self.rt.frame_cache.is_some();
1188 if (has_frame || repose_core::frame_clock::peek_frame_request())
1189 && !self.frame_pacer.due(now)
1190 {
1191 self.redraw_deferred = true;
1192 if let Some(deadline) = self.frame_pacer.deadline(now) {
1193 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1194 deadline,
1195 ));
1196 }
1197 return;
1198 }
1199 self.redraw_deferred = false;
1200 let compose_requested = take_frame_request();
1201
1202 self.process_a11y_actions();
1203 self.process_render_commands();
1204
1205 let Some(win) = self.window.clone() else {
1206 return;
1207 };
1208 if !compose_requested {
1209 if let (Some(backend), Some(frame)) =
1210 (self.backend.as_mut(), self.rt.frame_cache.as_ref())
1211 {
1212 let scale = win.scale_factor() as f32;
1213 let inspector_active = self
1214 .inspector
1215 .as_ref()
1216 .is_some_and(|inspector| inspector.hud.inspector_enabled);
1217 let _dnd_guard = self.rt.dnd_context.enter();
1218 let drag_active =
1219 repose_core::dnd::is_dragging() || self.external_file_drag;
1220 let mut overlay =
1221 (inspector_active || drag_active).then(|| frame.scene.clone());
1222 if let Some(scene) = &mut overlay {
1223 if let Some(inspector) = &mut self.inspector {
1224 inspector.frame(scene);
1225 }
1226 repose_core::dnd::overlay_drag_indicator(
1227 scene,
1228 self.rt.mouse_pos_px,
1229 self.external_file_drag,
1230 );
1231 }
1232 let _ = take_present_request();
1233 let presented = backend.frame(
1234 overlay.as_ref().unwrap_or(&frame.scene),
1235 GlyphRasterConfig {
1236 px: Px(18.0 * scale),
1237 },
1238 );
1239 if presented {
1240 self.frame_pacer.rendered(Instant::now());
1241 }
1242 }
1243 return;
1244 }
1245 log::trace!("RedrawRequested: frame request pending, composing");
1246
1247 if self.backend.is_none() {
1248 return;
1249 }
1250
1251 repose_core::animation_driver::tick();
1254
1255 let t0 = Instant::now();
1256 let scale = win.scale_factor() as f32;
1257 self.rt.scale = scale;
1258
1259 let output = self.rt.frame(&mut self.root, &self.render);
1260
1261 if let Some(cursor) = &output.platform.cursor {
1262 let win = win.clone();
1263 Self::apply_cursor_static(
1264 &win,
1265 &mut self.custom_cursor,
1266 &mut self.last_cursor_key,
1267 Some(el),
1268 cursor,
1269 );
1270 }
1271
1272 if let Some(dark) = output.platform.window_theme_dark
1273 && self.last_window_theme != Some(dark)
1274 {
1275 win.set_theme(Some(if dark {
1276 winit::window::Theme::Dark
1277 } else {
1278 winit::window::Theme::Light
1279 }));
1280 self.last_window_theme = Some(dark);
1281 }
1282
1283 self.ime_output_allowed = output.platform.ime_allowed;
1284 let ime_allowed = self.ime_allowed_for_output(
1285 &output.hit_regions,
1286 &output.semantics_nodes,
1287 output.platform.ime_allowed,
1288 );
1289 self.apply_ime_state(if ime_allowed {
1290 ImeState {
1291 allowed: true,
1292 purpose: output.platform.ime_purpose,
1293 auto_correct: output.platform.ime_auto_correct,
1294 capitalization: output.platform.ime_capitalization,
1295 cursor_area: output.platform.ime_cursor_area,
1296 }
1297 } else {
1298 ImeState::default()
1299 });
1300
1301 let frame = output.into_shared_frame();
1302
1303 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1304
1305 if self.a11y_active.load(Ordering::Acquire) {
1306 if self.a11y_needs_full.swap(false, Ordering::AcqRel) {
1307 self.a11y_tree = A11yTree::default();
1308 }
1309 if let Some(adapter) = &mut self.accesskit_adapter {
1310 let scale = win.scale_factor();
1311 if let Some(update) = self.a11y_tree.update(
1312 &frame.semantics_nodes,
1313 scale,
1314 self.rt.sched.focused,
1315 ) {
1316 adapter.update_if_active(|| update);
1317 }
1318 }
1319 }
1320
1321 self.rt.after_compose_shared(frame.clone(), scale);
1322
1323 let inspector_active = self
1324 .inspector
1325 .as_ref()
1326 .is_some_and(|inspector| inspector.hud.inspector_enabled);
1327 let _dnd_guard = self.rt.dnd_context.enter();
1328 let drag_active = repose_core::dnd::is_dragging() || self.external_file_drag;
1329 let mut overlay =
1330 (inspector_active || drag_active).then(|| frame.scene.clone());
1331 let scene = overlay.as_ref().unwrap_or(&frame.scene);
1332 if let Some(inspector) = &mut self.inspector {
1333 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1334 let signal_count = self.rt.sched.id_count() as usize;
1335 let ls = repose_ui::last_layout_stats();
1336 inspector.hud.metrics = Some(repose_devtools::Metrics {
1337 build_ms: build_layout_ms,
1338 layout_ms: ls.layout_time_ms,
1339 paint_ms: ls.paint_time_ms,
1340 scene_nodes: scene.nodes.len(),
1341 widget_count,
1342 signal_count,
1343 taffy_created: ls.taffy_created,
1344 taffy_reused: ls.taffy_reused,
1345 layout_hits: ls.layout_hits,
1346 layout_misses: ls.layout_misses,
1347 paint_cache_hits: ls.paint_cache_hits,
1348 paint_cache_misses: ls.paint_cache_misses,
1349 paint_culled: ls.paint_culled,
1350 });
1351 if let Some(scene) = &mut overlay {
1352 inspector.frame(scene);
1353 }
1354 }
1355 if let Some(scene) = &mut overlay {
1356 repose_core::dnd::overlay_drag_indicator(
1357 scene,
1358 self.rt.mouse_pos_px,
1359 self.external_file_drag,
1360 );
1361 }
1362
1363 self.process_render_commands();
1366
1367 let _ = take_present_request();
1368 let presented = if let Some(backend) = self.backend.as_mut() {
1369 backend.frame(
1370 overlay.as_ref().unwrap_or(&frame.scene),
1371 GlyphRasterConfig {
1372 px: Px(18.0 * scale),
1373 },
1374 )
1375 } else {
1376 false
1377 };
1378
1379 self.rt.cache_frame(frame);
1380
1381 self.dispatch_file_drop_now();
1382
1383 self.rt.tick_overlays();
1384 if presented {
1385 self.frame_pacer.rendered(Instant::now());
1386 }
1387 }
1388
1389 _ => {}
1390 }
1391 }
1392
1393 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1394 let _event_scope =
1395 repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
1396 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1399 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK
1400 .lock()
1401 .unwrap_or_else(|e| e.into_inner())
1402 .as_ref()
1403 {
1404 let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(cb));
1405 if let Err(e) = res {
1406 log::error!(
1407 "ABOUT_TO_WAIT_CALLBACK panicked: {}",
1408 e.downcast_ref::<String>()
1409 .map(|s| s.as_str())
1410 .or_else(|| e.downcast_ref::<&str>().copied())
1411 .unwrap_or("unknown")
1412 );
1413 }
1414 }
1415 self.rt.process_deeplinks();
1416 self.rt.process_lifecycle();
1417
1418 if let Some(backend) = &mut self.gamepad {
1419 for ev in backend.poll() {
1420 self.rt.handle_gamepad(&ev);
1421 }
1422 for (id, low, high, duration_ms) in self.rt.take_rumble_requests() {
1423 use repose_core::input::GamepadId;
1424 let gid = GamepadId(id);
1425 if duration_ms == 0 {
1426 backend.stop_rumble(gid);
1427 } else if !backend.set_rumble(gid, low, high, duration_ms) {
1428 log::warn!("gamepad: rumble not supported for pad {id}");
1429 }
1430 }
1431 } else {
1432 self.rt.take_rumble_requests();
1433 }
1434
1435 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1436 if WINDOW_VISIBLE.load(Ordering::Relaxed)
1437 && !WINDOW_OCCLUDED.load(Ordering::Relaxed)
1438 && self.backend.is_none()
1439 && let Some(w) = &self.window
1440 {
1441 log::info!("about_to_wait: recreating GPU backend");
1442 match repose_render_wgpu::WgpuBackend::new_with_options(
1443 w.clone(),
1444 self.msaa_samples,
1445 self.present_mode,
1446 ) {
1447 Ok(mut b) => {
1448 let size = w.inner_size();
1449 let scale = w.scale_factor() as f32;
1450 b.set_pixels_per_point(scale);
1451 self.rt
1452 .set_viewport_and_scale(size.width, size.height, scale);
1453 repose_render_wgpu::offscreen::set_shared_device(
1454 b.device.clone(),
1455 b.queue.clone(),
1456 );
1457 self.backend = Some(b);
1458 self.request_redraw();
1459 }
1460 Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
1461 }
1462 }
1463
1464 if self.redraw_deferred {
1465 let now = Instant::now();
1466 if self.frame_pacer.due(now) {
1467 self.redraw_deferred = false;
1468 rc::request_redraw(&self.window);
1469 } else if let Some(deadline) = self.frame_pacer.deadline(now) {
1470 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
1471 } else {
1472 self.redraw_deferred = false;
1473 rc::request_redraw(&self.window);
1474 }
1475 return;
1476 }
1477
1478 let now = Instant::now();
1479 let idle_cap = web_time::Duration::from_millis(1000);
1480 let idle_deadline = now + idle_cap;
1481 let wakeup_due = self.rt.is_wakeup_due(now);
1482 if wakeup_due
1483 || self.frame_pacer.idle_due(now, idle_cap)
1484 || repose_core::animation_driver::is_active()
1485 || (self.a11y_active.load(Ordering::Acquire)
1486 && self.a11y_needs_full.load(Ordering::Acquire))
1487 {
1488 request_frame();
1489 }
1490
1491 let wakeup_deadline = self
1492 .rt
1493 .next_wakeup_deadline()
1494 .filter(|deadline| *deadline > now);
1495 if wakeup_due {
1496 rc::request_redraw(&self.window);
1497 return;
1498 }
1499
1500 let compose_requested = repose_core::frame_clock::peek_frame_request();
1501 let present_requested = repose_core::frame_clock::peek_present_request();
1502 if compose_requested {
1503 if self.frame_pacer.due(now) {
1504 rc::request_redraw(&self.window);
1505 } else if let Some(deadline) = self
1506 .frame_pacer
1507 .deadline(now)
1508 .into_iter()
1509 .chain(wakeup_deadline)
1510 .chain(Some(idle_deadline))
1511 .min()
1512 {
1513 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
1514 }
1515 return;
1516 }
1517
1518 if present_requested && self.rt.frame_cache.is_some() {
1519 if self.frame_pacer.due(now) {
1520 rc::request_redraw(&self.window);
1521 } else if let Some(deadline) = self
1522 .frame_pacer
1523 .deadline(now)
1524 .into_iter()
1525 .chain(wakeup_deadline)
1526 .chain(Some(idle_deadline))
1527 .min()
1528 {
1529 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
1530 }
1531 return;
1532 }
1533
1534 let deadline = wakeup_deadline.unwrap_or(idle_deadline);
1535 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
1536 }
1537
1538 fn new_events(
1539 &mut self,
1540 _: &winit::event_loop::ActiveEventLoop,
1541 _: winit::event::StartCause,
1542 ) {
1543 }
1544 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1545 request_frame();
1546 }
1547 fn device_event(
1548 &mut self,
1549 _: &winit::event_loop::ActiveEventLoop,
1550 _: winit::event::DeviceId,
1551 _: winit::event::DeviceEvent,
1552 ) {
1553 }
1554 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {
1555 let _event_scope =
1556 repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
1557 push_runtime_window_lifecycle(&mut self.rt, false);
1558 }
1559 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {
1560 #[cfg(not(target_arch = "wasm32"))]
1561 rc::flush_clipboard_writes();
1562 repose_core::shutdown_composition();
1563 }
1564 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {
1565 repose_core::runtime::COMPOSER.with(|c| {
1566 let mut c = c.borrow_mut();
1567 let n = c.cursor;
1568 if c.slots.len() > n {
1569 c.slots.truncate(n);
1570 }
1571 if c.slot_callers.len() > n {
1572 c.slot_callers.truncate(n);
1573 }
1574 });
1575 repose_core::scope_cache::gc_dead_scopes();
1576 }
1577 }
1578
1579 impl App {
1580 fn announce_focus_change(&mut self) {
1581 if let Some(f) = &self.rt.frame_cache {
1582 let focused_node = self
1583 .rt
1584 .sched
1585 .focused
1586 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1587 self.a11y.focus_changed(focused_node);
1588 }
1589 }
1590
1591 fn dispatch_file_drop_now(&mut self) {
1592 let Some(f) = &self.rt.frame_cache else {
1593 self.pending_dropped_files.clear();
1594 self.pending_drop_pos_px = None;
1595 return;
1596 };
1597
1598 if self.pending_dropped_files.is_empty() {
1599 return;
1600 }
1601
1602 let pos_px = self.pending_drop_pos_px.unwrap_or(self.rt.mouse_pos_px);
1603 let pos = Vec2 {
1604 x: pos_px.0,
1605 y: pos_px.1,
1606 };
1607
1608 let mut files = Vec::new();
1609 for p in self.pending_dropped_files.drain(..) {
1610 let name = p
1611 .file_name()
1612 .and_then(|s| s.to_str())
1613 .unwrap_or("file")
1614 .to_string();
1615 files.push(repose_core::dnd::DroppedFile {
1616 name,
1617 path: Some(p),
1618 });
1619 }
1620
1621 let payload: repose_core::dnd::DragPayload =
1622 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1623
1624 let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
1625 self.pending_drop_pos_px = None;
1626 return;
1627 };
1628
1629 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
1630 && let Some(cb) = &hit.on_drop
1631 {
1632 let accepted = cb(repose_core::dnd::DropEvent {
1633 source_id: 0, target_id,
1635 position: pos,
1636 modifiers: self.rt.modifiers,
1637 payload: payload.clone(),
1638 });
1639
1640 if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
1641 {
1642 let label = node.label.as_deref().unwrap_or("");
1643 self.a11y.announce(&format!("Dropped files on {}", label));
1644 }
1645 }
1646
1647 self.pending_drop_pos_px = None;
1648 self.request_redraw();
1649 }
1650 }
1651
1652 let event_loop = EventLoop::new()?;
1653 set_event_loop_proxy(event_loop.create_proxy());
1654 let mut app = App::new(Box::new(root), config);
1655 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1657 event_loop.run_app(&mut app)?;
1658 Ok(())
1659}
1660
1661pub trait A11yBridge: Send {
1669 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1671
1672 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1674
1675 fn announce(&mut self, msg: &str);
1677}
1678
1679#[cfg_attr(target_os = "linux", allow(dead_code))]
1680struct NoopA11y;
1681impl A11yBridge for NoopA11y {
1682 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1683 }
1685 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1686 if let Some(n) = node {
1687 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1688 } else {
1689 log::info!("A11y focus: None");
1690 }
1691 }
1692 fn announce(&mut self, msg: &str) {
1693 log::info!("A11y announce: {msg}");
1694 }
1695}
1696
1697#[cfg(target_os = "linux")]
1698struct LinuxAtspiStub;
1699#[cfg(target_os = "linux")]
1700impl A11yBridge for LinuxAtspiStub {
1701 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1702 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1703 }
1704 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1705 if let Some(n) = node {
1706 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1707 } else {
1708 log::info!("AT-SPI stub focus: None");
1709 }
1710 }
1711 fn announce(&mut self, msg: &str) {
1712 log::info!("AT-SPI stub announce: {msg}");
1713 }
1714}