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