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