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 cursor_grab: Option<repose_core::CursorGrabMode>,
326
327 frame_pacer: rc::FramePacer,
328 redraw_deferred: bool,
329 occluded: bool,
330 os_focused: bool,
331 ime_output_allowed: bool,
332 ime_state: ImeState,
333
334 touch_gestures: rc::TouchGestureState,
336
337 gamepad: Option<Box<dyn gamepad::GamepadBackend>>,
338 sensors: Option<Box<dyn sensor::SensorBackend>>,
339 sensor_drops: Vec<String>,
342 }
343
344 impl App {
345 fn process_a11y_actions(&mut self) {
346 let mut actions = self.a11y_actions.lock().unwrap_or_else(|e| e.into_inner());
347 if actions.is_empty() {
348 return;
349 }
350 let pending = actions.drain(..).collect::<Vec<_>>();
351 drop(actions);
352
353 let Some(f) = &self.rt.frame_cache else {
354 return;
355 };
356
357 for req in pending {
358 let target_id = req.target_node.0;
359 match req.action {
360 accesskit::Action::Click => {
361 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
362 && let Some(cb) = &hit.on_click
363 {
364 cb();
365 self.request_redraw();
366 }
367 }
368 accesskit::Action::Focus => {
369 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
371 self.rt.sched.focused = Some(target_id);
372 self.request_redraw();
373 }
374 accesskit::Action::ScrollUp
375 | accesskit::Action::ScrollDown
376 | accesskit::Action::ScrollLeft
377 | accesskit::Action::ScrollRight => {
378 let delta = match req.action {
379 accesskit::Action::ScrollUp => repose_core::Vec2 { x: 0.0, y: -200.0 },
380 accesskit::Action::ScrollDown => repose_core::Vec2 { x: 0.0, y: 200.0 },
381 accesskit::Action::ScrollLeft => {
382 repose_core::Vec2 { x: -200.0, y: 0.0 }
383 }
384 _ => repose_core::Vec2 { x: 200.0, y: 0.0 },
385 };
386 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id) {
387 if let Some(scroll) = &hit.on_scroll_with_source {
388 scroll(delta, repose_core::input::ScrollSource::Wheel);
389 self.request_redraw();
390 } else if let Some(scroll) = &hit.on_scroll {
391 scroll(delta);
392 self.request_redraw();
393 }
394 }
395 }
396 accesskit::Action::SetValue => {
397 let value = match &req.data {
398 Some(accesskit::ActionData::NumericValue(v)) => Some(*v as f32),
399 Some(accesskit::ActionData::Value(v)) => v.parse::<f32>().ok(),
400 _ => None,
401 };
402 if let Some(value) = value
403 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
404 && let Some(cb) = &hit.on_set_progress
405 {
406 cb(value);
407 self.request_redraw();
408 }
409 }
410 accesskit::Action::Increment | accesskit::Action::Decrement => {
411 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
412 && let Some(cb) = &hit.on_set_progress
413 {
414 let dir = if matches!(req.action, accesskit::Action::Increment) {
415 1.0
416 } else {
417 -1.0
418 };
419 let value = f
420 .semantics_nodes
421 .iter()
422 .find(|n| n.id == target_id)
423 .and_then(|n| n.progress)
424 .map(|p| {
425 p.current + dir * p.step.unwrap_or((p.max - p.min) / 20.0)
426 });
427 if let Some(value) = value {
428 cb(value);
429 self.request_redraw();
430 }
431 }
432 }
433 accesskit::Action::ReplaceSelectedText => {
434 if let Some(accesskit::ActionData::Value(text)) = &req.data
435 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
436 && let Some(cb) = &hit.on_text_change
437 {
438 cb(text.to_string());
439 self.request_redraw();
440 }
441 }
442 accesskit::Action::Expand => {
443 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
444 && let Some(cb) = &hit.on_expand
445 {
446 cb();
447 self.request_redraw();
448 }
449 }
450 accesskit::Action::Collapse => {
451 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
452 && let Some(cb) = &hit.on_collapse
453 {
454 cb();
455 self.request_redraw();
456 }
457 }
458 _ => {}
459 }
460 }
461 }
462
463 fn new(
464 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
465 config: AppConfig,
466 ) -> Self {
467 Self {
468 root,
469 render: RenderContext::new(),
470 window: None,
471 backend: None,
472 backend_lifecycle: crate::runner_common::BackendLifecycle::new(),
473 backend_retry_at: None,
474 rt: ReposeRuntime::new(),
475 input_fit: 1.0,
476 inspector: if config.enable_inspector {
477 Some(repose_devtools::Inspector::new())
478 } else {
479 None
480 },
481 msaa_samples: config.common.msaa_samples,
482 present_mode: config.common.present_mode,
483 window_title: config.window_title,
484 window_size: config.window_size,
485 fullscreen: config.fullscreen,
486 pending_dropped_files: Vec::new(),
487 pending_drop_pos_px: None,
488
489 external_file_drag: false,
490 hovered_files: Vec::new(),
491
492 a11y: {
493 #[cfg(target_os = "linux")]
494 {
495 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
496 }
497 #[cfg(not(target_os = "linux"))]
498 {
499 Box::new(NoopA11y) as Box<dyn A11yBridge>
500 }
501 },
502
503 accesskit_adapter: None,
504 a11y_actions: Arc::new(Mutex::new(Vec::new())),
505 a11y_tree: A11yTree::default(),
506 a11y_active: Arc::new(AtomicBool::new(false)),
507 a11y_needs_full: Arc::new(AtomicBool::new(false)),
508
509 frame_pacer: rc::FramePacer::new(config.common.max_fps),
510 redraw_deferred: false,
511 occluded: false,
512 os_focused: true,
513 ime_output_allowed: false,
514 ime_state: ImeState::default(),
515 last_window_theme: None,
516 custom_cursor: None,
517 last_cursor_key: None,
518 cursor_grab: None,
519 touch_gestures: rc::TouchGestureState::default(),
520 gamepad: gamepad::create_backend()
521 .map(|b| Box::new(b) as Box<dyn gamepad::GamepadBackend>),
522 sensors: sensor::create_backend()
523 .map(|b| Box::new(b) as Box<dyn sensor::SensorBackend>),
524 sensor_drops: Vec::new(),
525 }
526 }
527
528 fn request_redraw(&self) {
529 repose_core::request_frame();
530 }
531
532 fn ime_allowed_for_frame(&self, frame: &repose_core::runtime::Frame) -> bool {
533 WINDOW_VISIBLE.load(Ordering::Relaxed)
534 && !WINDOW_OCCLUDED.load(Ordering::Relaxed)
535 && self.ime_output_allowed
536 && self.rt.sched.window_focused
537 && self.rt.sched.focused.is_some_and(|id| {
538 rc::is_editable_textfield_hit(frame, id) || rc::has_ime_handler(frame, id)
539 })
540 }
541
542 fn ime_allowed_for_output(
543 &self,
544 hits: &[repose_core::HitRegion],
545 semantics: &[repose_core::runtime::SemNode],
546 output_allowed: bool,
547 ) -> bool {
548 WINDOW_VISIBLE.load(Ordering::Relaxed)
549 && !WINDOW_OCCLUDED.load(Ordering::Relaxed)
550 && output_allowed
551 && self.rt.sched.window_focused
552 && self.rt.sched.focused.is_some_and(|id| {
553 rc::editable_textfield_hit(hits, semantics, id).is_some()
554 || rc::has_ime_handler_hit(hits, id)
555 })
556 }
557
558 fn sync_ime_for_focus(&mut self) {
559 let hit = self.rt.frame_cache.as_ref().and_then(|frame| {
560 (self.ime_allowed_for_frame(frame))
561 .then(|| {
562 self.rt.sched.focused.and_then(|id| {
563 rc::editable_textfield_hit(
564 &frame.hit_regions,
565 &frame.semantics_nodes,
566 id,
567 )
568 })
569 })
570 .flatten()
571 });
572 let state = match hit {
573 Some(hit) => {
574 let scale = self.window.as_ref().map(|window| window.scale_factor());
575 ImeState {
576 allowed: true,
577 purpose: hit.keyboard_type.ime_purpose_hint(),
578 auto_correct: hit.auto_correct.unwrap_or(true),
579 capitalization: hit.capitalization,
580 cursor_area: scale.map(|scale| {
581 (
582 hit.rect.x as f64 / scale,
583 hit.rect.y as f64 / scale,
584 hit.rect.w as f64 / scale,
585 hit.rect.h as f64 / scale,
586 )
587 }),
588 }
589 }
590 None => ImeState::default(),
591 };
592 self.apply_ime_state(state);
593 }
594
595 fn apply_ime_state(&mut self, state: ImeState) {
596 if self.ime_state == state {
597 return;
598 }
599 let Some(window) = self.window.clone() else {
600 return;
601 };
602 self.ime_state = state;
603 if state.allowed {
604 rc::set_ime_for_textfield_ex(
605 &window,
606 true,
607 state.purpose,
608 state.auto_correct,
609 state.capitalization,
610 );
611 if let Some((x, y, w, h)) = state.cursor_area {
612 window.set_ime_cursor_area(LogicalPosition::new(x, y), LogicalSize::new(w, h));
613 }
614 } else {
615 rc::set_ime_for_textfield(&window, false);
616 self.rt.finish_compositions();
617 }
618 }
619
620 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
621 if self.rt.dispatch_action(action) {
622 self.sync_ime_for_focus();
623 return true;
624 }
625 false
626 }
627
628 fn paste_from_primary(&self) -> Option<String> {
629 let mut opts = clipawl::ClipboardOptions::default();
630 opts.linux.selection = clipawl::LinuxSelection::Primary;
631 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
632 match pollster::block_on(cb.read()) {
633 Ok(t) => Some(t),
634 Err(e) => {
635 eprintln!("Primary paste error: {}", e);
636 None
637 }
638 }
639 } else {
640 None
641 }
642 }
643
644 fn process_render_commands(&mut self) {
645 let Some(backend) = self.backend.as_mut() else {
646 return;
647 };
648 repose_render_wgpu::apply_render_commands(backend, self.render.drain()).log_failures();
649 }
650
651 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
655 fn custom_cursor_key(img: &repose_core::CustomCursorImage) -> u64 {
656 let mut h: u64 = 0xcbf29ce484222325;
657 for b in img.rgba.iter().copied().chain(
658 img.size
659 .iter()
660 .flat_map(|v| v.to_le_bytes())
661 .chain(img.hotspot.iter().flat_map(|v| v.to_le_bytes())),
662 ) {
663 h ^= b as u64;
664 h = h.wrapping_mul(0x100000001b3);
665 }
666 h ^= (img.rgba.len() as u64).wrapping_mul(0x9e3779b97f4a7c15);
667 h
668 }
669
670 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
673 fn cursor_key(c: &repose_core::CursorIcon) -> (u8, u64) {
674 match c {
675 repose_core::CursorIcon::Default => (0, 0),
676 repose_core::CursorIcon::Pointer => (1, 0),
677 repose_core::CursorIcon::Text => (2, 0),
678 repose_core::CursorIcon::EwResize => (3, 0),
679 repose_core::CursorIcon::NsResize => (4, 0),
680 repose_core::CursorIcon::NwseResize => (5, 0),
681 repose_core::CursorIcon::NeswResize => (6, 0),
682 repose_core::CursorIcon::Grab => (7, 0),
683 repose_core::CursorIcon::Grabbing => (8, 0),
684 repose_core::CursorIcon::Hidden => (9, 0),
685 repose_core::CursorIcon::Custom(img) => (10, Self::custom_cursor_key(img)),
686 }
687 }
688
689 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
695 fn apply_cursor_static(
696 win: &std::sync::Arc<winit::window::Window>,
697 cache: &mut Option<(u64, winit::window::CustomCursor)>,
698 last_key: &mut Option<(u8, u64)>,
699 el: Option<&winit::event_loop::ActiveEventLoop>,
700 c: &repose_core::CursorIcon,
701 ) {
702 let key = Self::cursor_key(c);
703 if *last_key == Some(key) {
704 return;
705 }
706 *last_key = Some(key);
707 if let repose_core::CursorIcon::Custom(img) = c {
708 let hash = key.1;
709 let hit = cache
710 .as_ref()
711 .filter(|(k, _)| *k == hash)
712 .map(|(_, h)| h.clone());
713 let custom = match hit {
714 Some(h) => Some(h),
715 None => match el {
716 Some(el) => {
717 match winit::window::CustomCursor::from_rgba(
718 img.rgba.to_vec(),
719 img.size[0],
720 img.size[1],
721 img.hotspot[0],
722 img.hotspot[1],
723 ) {
724 Ok(source) => {
725 let h = el.create_custom_cursor(source);
726 *cache = Some((hash, h.clone()));
727 Some(h)
728 }
729 Err(e) => {
730 log::warn!("invalid custom cursor, falling back: {e:?}");
731 *cache = None;
732 None
733 }
734 }
735 }
736 None => cache.as_ref().map(|(_, h)| h.clone()),
737 },
738 };
739 win.set_cursor_visible(true);
740 match custom {
741 Some(h) => win.set_cursor(h),
742 None => win.set_cursor(map_cursor(c)),
743 }
744 return;
745 }
746 *cache = cache.take().and(None);
747 win.set_cursor_visible(!cursor_is_hidden(c));
748 win.set_cursor(map_cursor(c));
749 }
750
751 fn apply_cursor_grab(win: &winit::window::Window, mode: repose_core::CursorGrabMode) {
759 use repose_core::CursorGrabMode as Repose;
760 use winit::window::CursorGrabMode as Winit;
761 let result = match mode {
762 Repose::None => win.set_cursor_grab(Winit::None),
763 Repose::Confined => win
764 .set_cursor_grab(Winit::Confined)
765 .or_else(|_| win.set_cursor_grab(Winit::None)),
766 Repose::Locked => win
767 .set_cursor_grab(Winit::Locked)
768 .or_else(|_| win.set_cursor_grab(Winit::Confined)),
769 };
770 if let Err(e) = result {
771 log::warn!("cannot grab cursor ({mode:?}): {e:?}");
772 }
773 }
774
775 fn clear_input_state(&mut self) {
776 let active_touches = self
777 .touch_gestures
778 .active_touches()
779 .iter()
780 .map(|(id, pos)| (*id, *pos))
781 .collect::<Vec<_>>();
782 for (touch_id, pos) in active_touches {
783 self.touch_gestures
784 .touch_ended(&mut self.rt, touch_id, pos, true);
785 self.touch_gestures.contact_up(touch_id);
786 }
787 self.touch_gestures = rc::TouchGestureState::default();
788 self.rt.handle_focus_lost();
789 self.rt.touch_paths.clear();
790 self.rt.scroll_capture_id = None;
791 self.rt.hover_id = None;
792 self.rt.hover_ancestors.clear();
793 self.rt.last_focus = None;
794 self.rt.sched.pointer_pos_px = None;
795 self.rt.ime_preedit = false;
796 self.rt.modifiers = Default::default();
797 self.rt.sched.window_focused = false;
798 self.rt.sched.held_keys.clear();
799 self.rt.sched.mouse_primary = false;
800 self.rt.sched.mouse_secondary = false;
801 self.rt.sched.mouse_middle = false;
802 self.rt.pressed_ids.clear();
803 self.rt.key_pressed_active = None;
804 self.external_file_drag = false;
805 self.hovered_files.clear();
806 self.apply_ime_state(ImeState::default());
807 }
808
809 fn reset_pointer_state(&mut self) {
810 self.rt.capture_id = None;
811 self.rt.pressed_ids.clear();
812 self.rt.hover_id = None;
813 }
814 }
815
816 impl ApplicationHandler<()> for App {
817 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
818 let _event_scope =
819 repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
820 self.occluded = false;
821 WINDOW_OCCLUDED.store(false, Ordering::Relaxed);
822 self.rt.sched.window_focused = self.os_focused;
823 let _ = rc::setup_clipboard();
824
825 if self.window.is_none() {
826 let mut attributes = WindowAttributes::default()
827 .with_title(self.window_title.clone())
828 .with_inner_size(PhysicalSize::new(self.window_size.0, self.window_size.1))
829 .with_visible(false);
830 if self.fullscreen {
831 attributes = attributes
832 .with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
833 }
834 match el.create_window(attributes) {
835 Ok(win) => {
836 let w = Arc::new(win);
837
838 let activation_handler = ReposeActivationHandler {
839 active: self.a11y_active.clone(),
840 needs_full: self.a11y_needs_full.clone(),
841 };
842
843 let action_handler = ReposeActionHandler {
844 pending_actions: self.a11y_actions.clone(),
845 };
846
847 let deactivation_handler = ReposeDeactivationHandler {
848 active: self.a11y_active.clone(),
849 };
850
851 let adapter = Adapter::with_direct_handlers(
852 el,
853 &w,
854 activation_handler,
855 action_handler,
856 deactivation_handler,
857 );
858
859 self.accesskit_adapter = Some(adapter);
860
861 w.set_visible(true);
862
863 let size = w.inner_size();
864 let sf = w.scale_factor() as f32;
865 self.input_fit = rc::sync_viewport(&mut self.rt, &mut None, size, sf);
866
867 match repose_render_wgpu::WgpuBackend::new_with_options(
868 w.clone(),
869 Some(el.owned_display_handle()),
870 self.msaa_samples,
871 self.present_mode,
872 ) {
873 Ok(mut b) => {
874 b.set_pixels_per_point(sf);
875 repose_render_wgpu::offscreen::set_shared_device(
876 b.device.clone(),
877 b.queue.clone(),
878 );
879 self.backend = Some(b);
880 set_app_window(w.clone());
881 self.window = Some(w);
882 self.cursor_grab = None;
887 self.request_redraw();
888 }
889 Err(e) => {
890 log::error!("Failed to create WGPU backend: {e:?}");
891 el.exit();
892 }
893 }
894 }
895 Err(e) => {
896 log::error!("Failed to create window: {e:?}");
897 el.exit();
898 }
899 }
900 }
901 if self.window.is_some() {
902 push_runtime_window_lifecycle(&mut self.rt, WINDOW_VISIBLE.load(Ordering::Relaxed));
903 }
904 }
905
906 fn window_event(
907 &mut self,
908 el: &winit::event_loop::ActiveEventLoop,
909 _id: winit::window::WindowId,
910 event: WindowEvent,
911 ) {
912 let _event_scope =
913 repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
914 let _dnd_guard = self.rt.dnd_context.enter();
915 if let (Some(adapter), Some(window)) = (&mut self.accesskit_adapter, &self.window) {
917 adapter.process_event(window, &event);
918 }
919
920 match event {
921 WindowEvent::CloseRequested => {
922 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
923 self.backend = None;
925 if let Some(w) = &self.window {
926 w.set_visible(false);
927 }
928 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
929 push_runtime_window_lifecycle(&mut self.rt, false);
930 } else {
931 el.exit();
932 }
933 }
934
935 WindowEvent::Focused(focused) => {
936 self.os_focused = focused;
937 let focused = focused && !self.occluded;
938 self.rt.sched.window_focused = focused;
939 if !focused {
940 self.clear_input_state();
941 } else {
942 self.sync_ime_for_focus();
943 }
944 self.request_redraw();
945 }
946
947 WindowEvent::Occluded(occluded) => {
948 self.occluded = occluded;
949 WINDOW_OCCLUDED.store(occluded, Ordering::Relaxed);
950 if occluded {
951 self.rt.sched.window_focused = false;
952 self.clear_input_state();
953 } else {
954 self.rt.sched.window_focused = self.os_focused;
955 if self.os_focused {
956 self.sync_ime_for_focus();
957 }
958 }
959 push_runtime_window_lifecycle(
960 &mut self.rt,
961 WINDOW_VISIBLE.load(Ordering::Relaxed),
962 );
963 self.request_redraw();
964 }
965
966 WindowEvent::CursorLeft { .. } => {
967 self.rt.pointer_inside = false;
968 self.rt.handle_pointer_cancel();
969 self.external_file_drag = false;
970 self.hovered_files.clear();
971 self.request_redraw();
972 }
973
974 WindowEvent::HoveredFile(path) => {
975 self.external_file_drag = true;
977 if self.hovered_files.len() < 32 {
978 self.hovered_files.push(path);
979 }
980 if self.pending_drop_pos_px.is_none() {
981 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
982 }
983 self.request_redraw();
984 }
985
986 WindowEvent::HoveredFileCancelled => {
987 self.external_file_drag = false;
988 self.hovered_files.clear();
989
990 self.reset_pointer_state();
992
993 self.request_redraw();
994 }
995
996 WindowEvent::DroppedFile(path) => {
997 self.pending_dropped_files.push(path);
999 if self.pending_drop_pos_px.is_none() {
1000 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
1001 }
1002
1003 self.external_file_drag = false;
1005 self.hovered_files.clear();
1006
1007 self.request_redraw();
1008 }
1009
1010 WindowEvent::Resized(size) => {
1011 let sf = self
1012 .window
1013 .as_ref()
1014 .map(|w| w.scale_factor() as f32)
1015 .unwrap_or(1.0);
1016 self.input_fit = rc::sync_viewport(&mut self.rt, &mut self.backend, size, sf);
1017 if let Some(w) = &self.window {
1018 let sf = w.scale_factor() as f32;
1019 let dp_w = size.width as f32 / sf;
1020 let dp_h = size.height as f32 / sf;
1021 log::info!(
1022 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
1023 size.width,
1024 size.height,
1025 sf,
1026 dp_w as i32,
1027 dp_h as i32
1028 );
1029 }
1030 self.request_redraw();
1031 }
1032
1033 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
1034 let size = self
1035 .window
1036 .as_ref()
1037 .map(|w| w.inner_size())
1038 .unwrap_or_else(|| PhysicalSize::new(0, 0));
1039 self.input_fit = rc::sync_viewport(
1040 &mut self.rt,
1041 &mut self.backend,
1042 size,
1043 scale_factor as f32,
1044 );
1045 self.request_redraw();
1046 }
1047
1048 WindowEvent::CursorMoved { position, .. } => {
1049 self.rt.pointer_inside = true;
1050
1051 if self.external_file_drag {
1052 let k = self.input_fit;
1053 self.pending_drop_pos_px =
1054 Some((position.x as f32 * k, position.y as f32 * k));
1055 }
1056
1057 let pos = Vec2 {
1058 x: position.x as f32 * self.input_fit,
1059 y: position.y as f32 * self.input_fit,
1060 };
1061
1062 let result = self.rt.handle_pointer_move(pos);
1064
1065 if let (Some(inspector), Some(f)) = (&mut self.inspector, &self.rt.frame_cache)
1067 && inspector.hud.inspector_enabled
1068 {
1069 let hit = f.hit_regions.iter().find(|h| h.rect.contains(pos));
1070 let hover_rect = hit.map(|h| h.rect);
1071 let hover_info = hit.and_then(|h| {
1072 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
1073 repose_devtools::HoveredInfo {
1074 id: s.id,
1075 role: format!("{:?}", s.role),
1076 label: s.label.clone(),
1077 }
1078 })
1079 });
1080 inspector.hud.set_hovered(hover_rect, hover_info);
1081 }
1082
1083 if let Some(win) = &self.window
1084 && let Some(c) = result.cursor
1085 {
1086 Self::apply_cursor_static(
1087 win,
1088 &mut self.custom_cursor,
1089 &mut self.last_cursor_key,
1090 Some(el),
1091 &c,
1092 );
1093 }
1094
1095 self.request_redraw();
1096 }
1097
1098 WindowEvent::MouseWheel { delta, .. } => {
1099 let scale = self.rt.scale;
1100 let k = self.input_fit;
1101 let delta = match delta {
1102 winit::event::MouseScrollDelta::PixelDelta(p) => {
1103 winit::event::MouseScrollDelta::PixelDelta(
1104 winit::dpi::PhysicalPosition::new(p.x * k as f64, p.y * k as f64),
1105 )
1106 }
1107 other => other,
1108 };
1109 if crate::runner_common::on_mouse_wheel(&mut self.rt, delta, scale) {
1110 self.request_redraw();
1111 }
1112 }
1113
1114 WindowEvent::MouseInput { state, button, .. } => {
1115 let pos = Vec2 {
1116 x: self.rt.mouse_pos_px.0,
1117 y: self.rt.mouse_pos_px.1,
1118 };
1119
1120 let mapped = match button {
1121 MouseButton::Left => PointerButton::Primary,
1122 MouseButton::Right => PointerButton::Secondary,
1123 MouseButton::Middle => PointerButton::Tertiary,
1124 _ => return,
1126 };
1127 match (mapped, state) {
1128 (PointerButton::Primary, ElementState::Pressed) => {
1129 self.rt.sched.mouse_primary = true;
1130 }
1131 (PointerButton::Primary, ElementState::Released) => {
1132 self.rt.sched.mouse_primary = false;
1133 }
1134 (PointerButton::Secondary, ElementState::Pressed) => {
1135 self.rt.sched.mouse_secondary = true;
1136 }
1137 (PointerButton::Secondary, ElementState::Released) => {
1138 self.rt.sched.mouse_secondary = false;
1139 }
1140 (PointerButton::Tertiary, ElementState::Pressed) => {
1141 self.rt.sched.mouse_middle = true;
1142 }
1143 (PointerButton::Tertiary, ElementState::Released) => {
1144 self.rt.sched.mouse_middle = false;
1145 }
1146 }
1147
1148 match state {
1149 ElementState::Pressed => {
1150 let result = self.rt.handle_pointer_press(pos, mapped);
1151
1152 self.sync_ime_for_focus();
1153
1154 if result.focused.is_none() {
1155 self.rt.ime_preedit = false;
1156 }
1157
1158 if result.needs_a11y_announce {
1159 self.announce_focus_change();
1160 }
1161
1162 if matches!(mapped, PointerButton::Tertiary)
1164 && let Some(f) = &self.rt.frame_cache
1165 && let Some(cid) = self.rt.capture_id
1166 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1167 && self.rt.sched.window_focused
1168 && rc::is_editable_textfield_hit(f, hit.id)
1169 && let Some(txt) = self.paste_from_primary()
1170 {
1171 self.rt.paste_into_focused(&txt);
1172 }
1173
1174 if matches!(mapped, PointerButton::Primary | PointerButton::Secondary)
1176 && let Some(inspector) = &mut self.inspector
1177 && inspector.hud.inspector_enabled
1178 && let Some(f) = &self.rt.frame_cache
1179 && let Some(hit) =
1180 f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
1181 {
1182 let info =
1183 f.semantics_nodes.iter().find(|s| s.id == hit.id).map(|s| {
1184 repose_devtools::HoveredInfo {
1185 id: s.id,
1186 role: format!("{:?}", s.role),
1187 label: s.label.clone(),
1188 }
1189 });
1190 inspector
1191 .hud
1192 .select_widget(repose_devtools::SelectedWidget {
1193 id: hit.id,
1194 role: info
1195 .as_ref()
1196 .map(|i| i.role.clone())
1197 .unwrap_or_default(),
1198 label: info.as_ref().and_then(|i| i.label.clone()),
1199 bounds: hit.rect,
1200 });
1201 }
1202
1203 self.request_redraw();
1204 }
1205
1206 ElementState::Released => {
1207 let result = self.rt.handle_pointer_release(pos, mapped);
1208
1209 if let Some(cid) = result.clicked_id
1213 && let Some(f) = &self.rt.frame_cache
1214 && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid)
1215 {
1216 let label = node.label.as_deref().unwrap_or("");
1217 self.a11y.announce(&format!("Activated {}", label));
1218 }
1219
1220 self.request_redraw();
1221 }
1222 }
1223 }
1224
1225 WindowEvent::Touch(mut t) => {
1226 let scale = self.rt.scale;
1227 let k = self.input_fit as f64;
1228 t.location.x *= k;
1229 t.location.y *= k;
1230 let r = crate::runner_common::handle_touch_raw(
1231 &mut self.rt,
1232 &mut self.touch_gestures,
1233 &t,
1234 scale,
1235 );
1236 if matches!(
1237 t.phase,
1238 winit::event::TouchPhase::Started | winit::event::TouchPhase::Ended
1239 ) {
1240 self.sync_ime_for_focus();
1241 }
1242 let mut dirty = r.dirty;
1243 if let Some((delta_scale, center)) = r.pinch
1244 && self.dispatch_action(repose_core::shortcuts::Action::Gesture(
1245 repose_core::shortcuts::Gesture::PinchWithCenter {
1246 delta_scale,
1247 center,
1248 },
1249 ))
1250 {
1251 dirty = true;
1252 }
1253 if let Some((delta, center)) = r.pan
1254 && self.dispatch_action(repose_core::shortcuts::Action::Gesture(
1255 repose_core::shortcuts::Gesture::Pan { delta, center },
1256 ))
1257 {
1258 dirty = true;
1259 }
1260 if let Some((delta_rotation, center)) = r.rotation
1261 && self.dispatch_action(repose_core::shortcuts::Action::Gesture(
1262 repose_core::shortcuts::Gesture::Rotate {
1263 delta_rotation,
1264 center,
1265 },
1266 ))
1267 {
1268 dirty = true;
1269 }
1270 if let Some(right) = r.swipe_right {
1271 let g = if right {
1272 repose_core::shortcuts::Gesture::SwipeRight
1273 } else {
1274 repose_core::shortcuts::Gesture::SwipeLeft
1275 };
1276 if self.dispatch_action(repose_core::shortcuts::Action::Gesture(g)) {
1277 dirty = true;
1278 }
1279 }
1280 if dirty {
1281 self.request_redraw();
1282 }
1283 }
1284
1285 WindowEvent::ModifiersChanged(new_mods) => {
1286 crate::runner_common::on_modifiers_changed(&mut self.rt, &new_mods.state());
1287 }
1288
1289 WindowEvent::KeyboardInput {
1290 event: key_event, ..
1291 } => {
1292 if crate::runner_common::on_keyboard_input(
1293 &mut self.rt,
1294 &key_event,
1295 &mut self.inspector,
1296 ) {
1297 self.request_redraw();
1298 return;
1299 }
1300
1301 if key_event.state == ElementState::Pressed
1302 && !key_event.repeat
1303 && (rc::is_back_key(&key_event) || rc::is_escape_key(&key_event))
1304 {
1305 if self.rt.overlay.handle_back() {
1306 self.request_redraw();
1307 return;
1308 }
1309 use repose_navigation::back;
1310 if back::handle() {
1311 self.request_redraw();
1312 return;
1313 }
1314 if rc::is_back_key(&key_event) {
1315 el.exit();
1316 return;
1317 }
1318 }
1319
1320 if key_event.state == ElementState::Released
1322 && let Some(active_id) = self.rt.key_pressed_active
1323 {
1324 match key_event.physical_key {
1325 PhysicalKey::Code(KeyCode::Space)
1326 | PhysicalKey::Code(KeyCode::Enter) => {
1327 if let Some(f) = &self.rt.frame_cache
1328 && let Some(node) =
1329 f.semantics_nodes.iter().find(|n| n.id == active_id)
1330 {
1331 let label = node.label.as_deref().unwrap_or("");
1332 self.a11y.announce(&format!("Activated {}", label));
1333 }
1334 }
1335 _ => {}
1336 }
1337 }
1338 }
1339
1340 WindowEvent::Ime(ime) => {
1341 crate::runner_common::on_ime(&mut self.rt, &ime);
1342 self.request_redraw();
1343 }
1344
1345 WindowEvent::RedrawRequested => {
1346 crate::run_pre_redraw(&self.render);
1348
1349 let now = Instant::now();
1350 let has_frame = self.rt.frame_cache.is_some();
1351 if (has_frame || repose_core::frame_clock::peek_frame_request())
1352 && !self.frame_pacer.due(now)
1353 {
1354 self.redraw_deferred = true;
1355 if let Some(deadline) = self.frame_pacer.deadline(now) {
1356 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1357 deadline,
1358 ));
1359 }
1360 return;
1361 }
1362 self.redraw_deferred = false;
1363 let compose_requested = take_frame_request();
1364
1365 self.process_a11y_actions();
1366 self.process_render_commands();
1367
1368 let Some(win) = self.window.clone() else {
1369 return;
1370 };
1371 if !compose_requested {
1372 if let (Some(backend), Some(frame)) =
1373 (self.backend.as_mut(), self.rt.frame_cache.as_ref())
1374 {
1375 let scale = win.scale_factor() as f32;
1376 let inspector_active = self
1377 .inspector
1378 .as_ref()
1379 .is_some_and(|inspector| inspector.hud.inspector_enabled);
1380 let _dnd_guard = self.rt.dnd_context.enter();
1381 let drag_active =
1382 repose_core::dnd::is_dragging() || self.external_file_drag;
1383 let mut overlay =
1384 (inspector_active || drag_active).then(|| frame.scene.clone());
1385 if let Some(scene) = &mut overlay {
1386 if let Some(inspector) = &mut self.inspector {
1387 inspector.frame(scene);
1388 }
1389 repose_core::dnd::overlay_drag_indicator(
1390 scene,
1391 self.rt.mouse_pos_px,
1392 self.external_file_drag,
1393 );
1394 }
1395 let _ = take_present_request();
1396 let presented = backend.frame(
1397 overlay.as_ref().unwrap_or(&frame.scene),
1398 GlyphRasterConfig {
1399 px: Px(18.0 * scale),
1400 },
1401 );
1402 if presented {
1403 self.frame_pacer.rendered(Instant::now());
1404 }
1405 }
1406 return;
1407 }
1408 log::trace!("RedrawRequested: frame request pending, composing");
1409
1410 if self.backend.is_none() {
1411 return;
1412 }
1413
1414 repose_core::animation_driver::tick();
1417
1418 let t0 = Instant::now();
1419 let scale = self.rt.scale;
1420
1421 let output = self.rt.frame(&mut self.root, &self.render);
1422
1423 let declared_grab = output.platform.cursor_grab;
1424 if self.cursor_grab != Some(declared_grab) {
1425 self.cursor_grab = Some(declared_grab);
1426 Self::apply_cursor_grab(&win, declared_grab);
1427 }
1428
1429 if let Some(cursor) = &output.platform.cursor {
1430 let win = win.clone();
1431 Self::apply_cursor_static(
1432 &win,
1433 &mut self.custom_cursor,
1434 &mut self.last_cursor_key,
1435 Some(el),
1436 cursor,
1437 );
1438 }
1439
1440 if let Some(fullscreen) = self.rt.take_fullscreen_request()
1441 && fullscreen != self.fullscreen
1442 {
1443 self.fullscreen = fullscreen;
1444 let _ = win.set_fullscreen(if fullscreen {
1445 Some(winit::window::Fullscreen::Borderless(None))
1446 } else {
1447 None
1448 });
1449 }
1450
1451 if let Some(dark) = output.platform.window_theme_dark
1452 && self.last_window_theme != Some(dark)
1453 {
1454 win.set_theme(Some(if dark {
1455 winit::window::Theme::Dark
1456 } else {
1457 winit::window::Theme::Light
1458 }));
1459 self.last_window_theme = Some(dark);
1460 }
1461
1462 self.ime_output_allowed = output.platform.ime_allowed;
1463 let ime_allowed = self.ime_allowed_for_output(
1464 &output.hit_regions,
1465 &output.semantics_nodes,
1466 output.platform.ime_allowed,
1467 );
1468 self.apply_ime_state(if ime_allowed {
1469 ImeState {
1470 allowed: true,
1471 purpose: output.platform.ime_purpose,
1472 auto_correct: output.platform.ime_auto_correct,
1473 capitalization: output.platform.ime_capitalization,
1474 cursor_area: output.platform.ime_cursor_area,
1475 }
1476 } else {
1477 ImeState::default()
1478 });
1479
1480 let frame = output.into_shared_frame();
1481
1482 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1483
1484 if self.a11y_active.load(Ordering::Acquire) {
1485 if self.a11y_needs_full.swap(false, Ordering::AcqRel) {
1486 self.a11y_tree = A11yTree::default();
1487 }
1488 if let Some(adapter) = &mut self.accesskit_adapter {
1489 let scale = win.scale_factor();
1490 if let Some(update) = self.a11y_tree.update(
1491 &frame.semantics_nodes,
1492 scale,
1493 self.rt.sched.focused,
1494 ) {
1495 adapter.update_if_active(|| update);
1496 }
1497 }
1498 }
1499
1500 self.rt.after_compose_shared(frame.clone(), scale);
1501
1502 let inspector_active = self
1503 .inspector
1504 .as_ref()
1505 .is_some_and(|inspector| inspector.hud.inspector_enabled);
1506 let _dnd_guard = self.rt.dnd_context.enter();
1507 let drag_active = repose_core::dnd::is_dragging() || self.external_file_drag;
1508 let mut overlay =
1509 (inspector_active || drag_active).then(|| frame.scene.clone());
1510 let scene = overlay.as_ref().unwrap_or(&frame.scene);
1511 if let Some(inspector) = &mut self.inspector {
1512 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1513 let signal_count = self.rt.sched.id_count() as usize;
1514 let ls = repose_ui::last_layout_stats();
1515 inspector.hud.metrics = Some(repose_devtools::Metrics {
1516 build_ms: build_layout_ms,
1517 layout_ms: ls.layout_time_ms,
1518 paint_ms: ls.paint_time_ms,
1519 scene_nodes: scene.nodes.len(),
1520 widget_count,
1521 signal_count,
1522 taffy_created: ls.taffy_created,
1523 taffy_reused: ls.taffy_reused,
1524 layout_hits: ls.layout_hits,
1525 layout_misses: ls.layout_misses,
1526 paint_cache_hits: ls.paint_cache_hits,
1527 paint_cache_misses: ls.paint_cache_misses,
1528 paint_culled: ls.paint_culled,
1529 });
1530 if let Some(scene) = &mut overlay {
1531 inspector.frame(scene);
1532 }
1533 }
1534 if let Some(scene) = &mut overlay {
1535 repose_core::dnd::overlay_drag_indicator(
1536 scene,
1537 self.rt.mouse_pos_px,
1538 self.external_file_drag,
1539 );
1540 }
1541
1542 self.process_render_commands();
1545
1546 let _ = take_present_request();
1547 let presented = if let Some(backend) = self.backend.as_mut() {
1548 backend.frame(
1549 overlay.as_ref().unwrap_or(&frame.scene),
1550 GlyphRasterConfig {
1551 px: Px(18.0 * scale),
1552 },
1553 )
1554 } else {
1555 false
1556 };
1557
1558 self.rt.cache_frame(frame);
1559
1560 self.dispatch_file_drop_now();
1561
1562 self.rt.tick_overlays();
1563 if presented {
1564 self.frame_pacer.rendered(Instant::now());
1565 }
1566 }
1567
1568 _ => {}
1569 }
1570 }
1571
1572 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1573 let _event_scope =
1574 repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
1575 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1578 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK
1579 .lock()
1580 .unwrap_or_else(|e| e.into_inner())
1581 .as_ref()
1582 {
1583 let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(cb));
1584 if let Err(e) = res {
1585 log::error!(
1586 "ABOUT_TO_WAIT_CALLBACK panicked: {}",
1587 e.downcast_ref::<String>()
1588 .map(|s| s.as_str())
1589 .or_else(|| e.downcast_ref::<&str>().copied())
1590 .unwrap_or("unknown")
1591 );
1592 }
1593 }
1594 self.rt.process_deeplinks();
1595 self.rt.process_lifecycle();
1596
1597 if let Some(backend) = &mut self.gamepad {
1598 for ev in backend.poll() {
1599 self.rt.handle_gamepad(&ev);
1600 }
1601 for (id, low, high, duration_ms) in self.rt.take_rumble_requests() {
1602 use repose_core::input::GamepadId;
1603 let gid = GamepadId(id);
1604 if duration_ms == 0 {
1605 backend.stop_rumble(gid);
1606 } else if !backend.set_rumble(gid, low, high, duration_ms) {
1607 log::warn!("gamepad: rumble not supported for pad {id}");
1608 }
1609 }
1610 } else {
1611 self.rt.take_rumble_requests();
1612 }
1613
1614 if let Some(backend) = &mut self.sensors {
1615 for reading in backend.poll_sensors() {
1616 match self.rt.gamepad_id_by_sensor_device(&reading.device) {
1617 Some(id) => self.rt.handle_sensor_sample(id, reading.sample),
1618 None if !self.sensor_drops.contains(&reading.device) => {
1619 self.sensor_drops.push(reading.device.clone());
1620 log::warn!(
1621 "sensor: no connected pad named {:?}, dropping samples",
1622 reading.device
1623 );
1624 }
1625 None => {}
1626 }
1627 }
1628 }
1629
1630 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1631 if let Some(reason) = self.backend.as_ref().and_then(|b| b.take_device_lost()) {
1632 log::error!("GPU device lost, rebuilding backend: {reason}");
1633 self.backend = None;
1634 self.backend_retry_at = None;
1635 self.backend_lifecycle.begin();
1636 self.rt.frame_cache = None;
1637 self.request_redraw();
1638 }
1639
1640 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1641 if WINDOW_VISIBLE.load(Ordering::Relaxed)
1642 && !WINDOW_OCCLUDED.load(Ordering::Relaxed)
1643 && self.backend.is_none()
1644 && self.backend_lifecycle.state() != BackendState::Unavailable
1645 && let Some(w) = &self.window
1646 {
1647 let now = Instant::now();
1648 if let Some(retry_at) = self.backend_retry_at
1649 && now < retry_at
1650 {
1651 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(retry_at));
1652 return;
1653 }
1654 self.backend_retry_at = None;
1655 log::info!("about_to_wait: recreating GPU backend");
1656 match repose_render_wgpu::WgpuBackend::new_with_options(
1657 w.clone(),
1658 Some(el.owned_display_handle()),
1659 self.msaa_samples,
1660 self.present_mode,
1661 ) {
1662 Ok(b) => {
1663 let size = w.inner_size();
1664 let scale = w.scale_factor() as f32;
1665 let mut backend = Some(b);
1666 self.input_fit = rc::sync_viewport(&mut self.rt, &mut backend, size, scale);
1667 let Some(b) = backend.take() else {
1668 unreachable!()
1669 };
1670 repose_render_wgpu::offscreen::set_shared_device(
1671 b.device.clone(),
1672 b.queue.clone(),
1673 );
1674 self.backend = Some(b);
1675 self.backend_lifecycle.ready();
1676 self.request_redraw();
1677 }
1678 Err(e) => match self.backend_lifecycle.failed() {
1679 BackendRetry::RetryAfter(delay) => {
1680 self.backend_retry_at = Some(now + delay);
1681 log::error!(
1682 "about_to_wait: failed to recreate backend, retrying in \
1683 {delay:?}: {e:?}"
1684 );
1685 }
1686 BackendRetry::Unavailable => log::error!(
1687 "about_to_wait: GPU backend permanently unavailable after {} \
1688 attempts, giving up: {e:?}",
1689 MAX_BACKEND_INIT_ATTEMPTS
1690 ),
1691 BackendRetry::Ready => unreachable!(),
1692 },
1693 }
1694 }
1695
1696 if self.redraw_deferred {
1697 let now = Instant::now();
1698 if self.frame_pacer.due(now) {
1699 self.redraw_deferred = false;
1700 rc::request_redraw(&self.window);
1701 } else if let Some(deadline) = self.frame_pacer.deadline(now) {
1702 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
1703 } else {
1704 self.redraw_deferred = false;
1705 rc::request_redraw(&self.window);
1706 }
1707 return;
1708 }
1709
1710 let now = Instant::now();
1711 let idle_cap = web_time::Duration::from_millis(1000);
1712 let idle_deadline = now + idle_cap;
1713 let wakeup_due = self.rt.is_wakeup_due(now);
1714 if wakeup_due
1715 || self.frame_pacer.idle_due(now, idle_cap)
1716 || repose_core::animation_driver::is_active()
1717 || (self.a11y_active.load(Ordering::Acquire)
1718 && self.a11y_needs_full.load(Ordering::Acquire))
1719 {
1720 request_frame();
1721 }
1722
1723 let wakeup_deadline = self
1724 .rt
1725 .next_wakeup_deadline()
1726 .filter(|deadline| *deadline > now);
1727 if wakeup_due {
1728 rc::request_redraw(&self.window);
1729 return;
1730 }
1731
1732 let compose_requested = repose_core::frame_clock::peek_frame_request();
1733 let present_requested = repose_core::frame_clock::peek_present_request();
1734 if compose_requested {
1735 if self.frame_pacer.due(now) {
1736 rc::request_redraw(&self.window);
1737 } else if let Some(deadline) = self
1738 .frame_pacer
1739 .deadline(now)
1740 .into_iter()
1741 .chain(wakeup_deadline)
1742 .chain(Some(idle_deadline))
1743 .min()
1744 {
1745 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
1746 }
1747 return;
1748 }
1749
1750 if present_requested && self.rt.frame_cache.is_some() {
1751 if self.frame_pacer.due(now) {
1752 rc::request_redraw(&self.window);
1753 } else if let Some(deadline) = self
1754 .frame_pacer
1755 .deadline(now)
1756 .into_iter()
1757 .chain(wakeup_deadline)
1758 .chain(Some(idle_deadline))
1759 .min()
1760 {
1761 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
1762 }
1763 return;
1764 }
1765
1766 let deadline = wakeup_deadline.unwrap_or(idle_deadline);
1767 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
1768 }
1769
1770 fn new_events(
1771 &mut self,
1772 _: &winit::event_loop::ActiveEventLoop,
1773 _: winit::event::StartCause,
1774 ) {
1775 }
1776 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1777 request_frame();
1778 }
1779 fn device_event(
1780 &mut self,
1781 _: &winit::event_loop::ActiveEventLoop,
1782 _: winit::event::DeviceId,
1783 _: winit::event::DeviceEvent,
1784 ) {
1785 }
1786 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {
1787 let _event_scope =
1788 repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
1789 push_runtime_window_lifecycle(&mut self.rt, false);
1790 }
1791 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {
1792 #[cfg(not(target_arch = "wasm32"))]
1793 rc::flush_clipboard_writes();
1794 repose_core::shutdown_composition();
1795 }
1796 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {
1797 repose_core::runtime::COMPOSER.with(|c| {
1798 let mut c = c.borrow_mut();
1799 let n = c.cursor;
1800 if c.slots.len() > n {
1801 c.slots.truncate(n);
1802 }
1803 if c.slot_callers.len() > n {
1804 c.slot_callers.truncate(n);
1805 }
1806 });
1807 repose_core::scope_cache::gc_dead_scopes();
1808 }
1809 }
1810
1811 impl App {
1812 fn announce_focus_change(&mut self) {
1813 if let Some(f) = &self.rt.frame_cache {
1814 let focused_node = self
1815 .rt
1816 .sched
1817 .focused
1818 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1819 self.a11y.focus_changed(focused_node);
1820 }
1821 }
1822
1823 fn dispatch_file_drop_now(&mut self) {
1824 let Some(f) = &self.rt.frame_cache else {
1825 self.pending_dropped_files.clear();
1826 self.pending_drop_pos_px = None;
1827 return;
1828 };
1829
1830 if self.pending_dropped_files.is_empty() {
1831 return;
1832 }
1833
1834 let pos_px = self.pending_drop_pos_px.unwrap_or(self.rt.mouse_pos_px);
1835 let pos = Vec2 {
1836 x: pos_px.0,
1837 y: pos_px.1,
1838 };
1839
1840 let mut files = Vec::new();
1841 for p in self.pending_dropped_files.drain(..) {
1842 let name = p
1843 .file_name()
1844 .and_then(|s| s.to_str())
1845 .unwrap_or("file")
1846 .to_string();
1847 files.push(repose_core::dnd::DroppedFile {
1848 name,
1849 path: Some(p),
1850 });
1851 }
1852
1853 let payload: repose_core::dnd::DragPayload =
1854 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1855
1856 let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
1857 self.pending_drop_pos_px = None;
1858 return;
1859 };
1860
1861 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
1862 && let Some(cb) = &hit.on_drop
1863 {
1864 let accepted = cb(repose_core::dnd::DropEvent {
1865 source_id: 0, target_id,
1867 position: pos,
1868 modifiers: self.rt.modifiers,
1869 payload: payload.clone(),
1870 });
1871
1872 if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
1873 {
1874 let label = node.label.as_deref().unwrap_or("");
1875 self.a11y.announce(&format!("Dropped files on {}", label));
1876 }
1877 }
1878
1879 self.pending_drop_pos_px = None;
1880 self.request_redraw();
1881 }
1882 }
1883
1884 let event_loop = EventLoop::new()?;
1885 set_event_loop_proxy(event_loop.create_proxy());
1886 let mut app = App::new(Box::new(root), config);
1887 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1889 event_loop.run_app(&mut app)?;
1890 Ok(())
1891}
1892
1893pub trait A11yBridge: Send {
1901 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1903
1904 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1906
1907 fn announce(&mut self, msg: &str);
1909}
1910
1911#[cfg_attr(target_os = "linux", allow(dead_code))]
1912struct NoopA11y;
1913impl A11yBridge for NoopA11y {
1914 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1915 }
1917 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1918 if let Some(n) = node {
1919 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1920 } else {
1921 log::info!("A11y focus: None");
1922 }
1923 }
1924 fn announce(&mut self, msg: &str) {
1925 log::info!("A11y announce: {msg}");
1926 }
1927}
1928
1929#[cfg(target_os = "linux")]
1930struct LinuxAtspiStub;
1931#[cfg(target_os = "linux")]
1932impl A11yBridge for LinuxAtspiStub {
1933 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1934 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1935 }
1936 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1937 if let Some(n) = node {
1938 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1939 } else {
1940 log::info!("AT-SPI stub focus: None");
1941 }
1942 }
1943 fn announce(&mut self, msg: &str) {
1944 log::info!("AT-SPI stub announce: {msg}");
1945 }
1946}