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