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