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 last_redraw: Instant,
227 pending_redraw: bool,
228
229 redraw_requested: Cell<bool>,
231
232 touch_gestures: rc::TouchGestureState,
234
235 gamepad: Option<Box<dyn gamepad::GamepadBackend>>,
236 }
237
238 impl App {
239 fn process_a11y_actions(&mut self) {
240 let mut actions = self.a11y_actions.lock().unwrap_or_else(|e| e.into_inner());
241 if actions.is_empty() {
242 return;
243 }
244 let pending = actions.drain(..).collect::<Vec<_>>();
245 drop(actions);
246
247 let Some(f) = &self.rt.frame_cache else {
248 return;
249 };
250
251 for req in pending {
252 let target_id = req.target_node.0;
253 match req.action {
254 accesskit::Action::Click => {
255 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
256 && let Some(cb) = &hit.on_click
257 {
258 cb();
259 self.request_redraw();
260 }
261 }
262 accesskit::Action::Focus => {
263 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
265 self.rt.sched.focused = Some(target_id);
266 self.request_redraw();
267 }
268 _ => {}
269 }
270 }
271 }
272
273 fn new(
274 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
275 config: AppConfig,
276 ) -> Self {
277 Self {
278 root,
279 render: RenderContext::new(),
280 window: None,
281 backend: None,
282 rt: ReposeRuntime::new(),
283 inspector: if config.enable_inspector {
284 Some(repose_devtools::Inspector::new())
285 } else {
286 None
287 },
288 msaa_samples: config.common.msaa_samples,
289 max_fps: config.common.max_fps,
290 present_mode: config.common.present_mode,
291 window_title: config.window_title,
292 window_size: config.window_size,
293 pending_dropped_files: Vec::new(),
294 pending_drop_pos_px: None,
295
296 external_file_drag: false,
297 hovered_files: Vec::new(),
298
299 clipboard: None,
300 a11y: {
301 #[cfg(target_os = "linux")]
302 {
303 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
304 }
305 #[cfg(not(target_os = "linux"))]
306 {
307 Box::new(NoopA11y) as Box<dyn A11yBridge>
308 }
309 },
310
311 accesskit_adapter: None,
312 a11y_actions: Arc::new(Mutex::new(Vec::new())),
313 a11y_tree: A11yTree::default(),
314
315 last_redraw: Instant::now(),
316 pending_redraw: false,
317 last_window_theme: None,
318 redraw_requested: Cell::new(false),
319 touch_gestures: rc::TouchGestureState::default(),
320 gamepad: gamepad::create_backend()
321 .map(|b| Box::new(b) as Box<dyn gamepad::GamepadBackend>),
322 }
323 }
324
325 fn request_redraw(&self) {
326 self.redraw_requested.set(true);
327 repose_core::request_frame();
328 rc::request_redraw(&self.window);
329 }
330
331 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
332 if self.rt.dispatch_action(action) {
333 if let Some(win) = &self.window {
334 rc::set_ime_for_textfield(
335 win,
336 self.rt
337 .sched
338 .focused
339 .is_some_and(|id| self.rt.is_textfield(id)),
340 );
341 }
342 return true;
343 }
344 false
345 }
346
347 fn frame_interval(&self) -> web_time::Duration {
350 match self.max_fps.filter(|f| *f > 0.0) {
351 Some(fps) => {
352 let secs = (1.0 / fps as f64).clamp(0.0, 1.0);
353 web_time::Duration::from_secs_f64(secs)
354 }
355 None => web_time::Duration::ZERO,
356 }
357 }
358
359 fn paste_from_primary(&self) -> Option<String> {
360 let mut opts = clipawl::ClipboardOptions::default();
361 opts.linux.selection = clipawl::LinuxSelection::Primary;
362 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
363 match pollster::block_on(cb.read()) {
364 Ok(t) => Some(t),
365 Err(e) => {
366 eprintln!("Primary paste error: {}", e);
367 None
368 }
369 }
370 } else {
371 None
372 }
373 }
374
375 fn process_render_commands(&mut self) {
376 let Some(backend) = self.backend.as_mut() else {
377 return;
378 };
379 repose_render_wgpu::apply_render_commands(backend, self.render.drain());
380 }
381
382 fn reset_pointer_state(&mut self) {
383 self.rt.capture_id = None;
384 self.rt.pressed_ids.clear();
385 self.rt.hover_id = None;
386 }
387 }
388
389 impl ApplicationHandler<()> for App {
390 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
391 self.clipboard = rc::setup_clipboard();
392
393 if self.window.is_none() {
394 match el.create_window(
395 WindowAttributes::default()
396 .with_title(self.window_title.clone())
397 .with_inner_size(PhysicalSize::new(self.window_size.0, self.window_size.1))
398 .with_visible(false),
399 ) {
400 Ok(win) => {
401 let w = Arc::new(win);
402
403 let activation_handler = ReposeActivationHandler {
404 initial_tree: Some(A11yTree::initial_tree()),
405 };
406
407 let action_handler = ReposeActionHandler {
408 pending_actions: self.a11y_actions.clone(),
409 };
410
411 let deactivation_handler = ReposeDeactivationHandler;
412
413 let adapter = Adapter::with_direct_handlers(
414 el,
415 &w,
416 activation_handler,
417 action_handler,
418 deactivation_handler,
419 );
420
421 self.accesskit_adapter = Some(adapter);
422
423 w.set_visible(true);
424
425 let size = w.inner_size();
426 let sf = w.scale_factor() as f32;
427 self.rt.set_viewport_and_scale(size.width, size.height, sf);
428
429 match repose_render_wgpu::WgpuBackend::new_with_options(
430 w.clone(),
431 self.msaa_samples,
432 self.present_mode,
433 ) {
434 Ok(mut b) => {
435 b.set_pixels_per_point(sf);
436 repose_render_wgpu::offscreen::set_shared_device(
437 b.device.clone(),
438 b.queue.clone(),
439 );
440 self.backend = Some(b);
441 set_app_window(w.clone());
442 self.window = Some(w);
443 self.request_redraw();
444 }
445 Err(e) => {
446 log::error!("Failed to create WGPU backend: {e:?}");
447 el.exit();
448 }
449 }
450 }
451 Err(e) => {
452 log::error!("Failed to create window: {e:?}");
453 el.exit();
454 }
455 }
456 }
457 }
458
459 fn window_event(
460 &mut self,
461 el: &winit::event_loop::ActiveEventLoop,
462 _id: winit::window::WindowId,
463 event: WindowEvent,
464 ) {
465 if let (Some(adapter), Some(window)) = (&mut self.accesskit_adapter, &self.window) {
467 adapter.process_event(window, &event);
468 }
469
470 match event {
471 WindowEvent::CloseRequested => {
472 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
473 self.backend = None;
475 if let Some(w) = &self.window {
476 w.set_visible(false);
477 }
478 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
479 } else {
480 el.exit();
481 }
482 }
483
484 WindowEvent::Focused(focused) => {
485 self.rt.sched.window_focused = focused;
486 if !focused {
487 self.rt.handle_focus_lost();
488 self.rt.sched.held_keys.clear();
489 self.rt.sched.mouse_primary = false;
490 self.rt.sched.mouse_secondary = false;
491 self.rt.sched.mouse_middle = false;
492 self.external_file_drag = false;
493 self.hovered_files.clear();
494
495 if let Some(w) = &self.window {
496 rc::set_ime_for_textfield(w, false);
497 }
498
499 self.request_redraw();
500 }
501 }
502
503 WindowEvent::CursorLeft { .. } => {
504 self.rt.pointer_inside = false;
505 self.rt.handle_pointer_cancel();
506 self.external_file_drag = false;
507 self.hovered_files.clear();
508 self.request_redraw();
509 }
510
511 WindowEvent::HoveredFile(path) => {
512 self.external_file_drag = true;
514 if self.hovered_files.len() < 32 {
515 self.hovered_files.push(path);
516 }
517 if self.pending_drop_pos_px.is_none() {
518 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
519 }
520 self.request_redraw();
521 }
522
523 WindowEvent::HoveredFileCancelled => {
524 self.external_file_drag = false;
525 self.hovered_files.clear();
526
527 self.reset_pointer_state();
529
530 self.request_redraw();
531 }
532
533 WindowEvent::DroppedFile(path) => {
534 self.pending_dropped_files.push(path);
536 if self.pending_drop_pos_px.is_none() {
537 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
538 }
539
540 self.external_file_drag = false;
542 self.hovered_files.clear();
543
544 self.request_redraw();
545 }
546
547 WindowEvent::Resized(size) => {
548 let sf = self
549 .window
550 .as_ref()
551 .map(|w| w.scale_factor() as f32)
552 .unwrap_or(1.0);
553 rc::sync_viewport(&mut self.rt, &mut self.backend, size, sf);
554 if let Some(w) = &self.window {
555 let sf = w.scale_factor() as f32;
556 let dp_w = size.width as f32 / sf;
557 let dp_h = size.height as f32 / sf;
558 log::info!(
559 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
560 size.width,
561 size.height,
562 sf,
563 dp_w as i32,
564 dp_h as i32
565 );
566 }
567 self.request_redraw();
568 }
569
570 WindowEvent::CursorMoved { position, .. } => {
571 self.rt.pointer_inside = true;
572
573 if self.external_file_drag {
574 self.pending_drop_pos_px = Some((position.x as f32, position.y as f32));
575 }
576
577 let pos = Vec2 {
578 x: position.x as f32,
579 y: position.y as f32,
580 };
581
582 let result = self.rt.handle_pointer_move(pos);
584
585 if let (Some(inspector), Some(f)) = (&mut self.inspector, &self.rt.frame_cache)
587 && inspector.hud.inspector_enabled
588 {
589 let hit = f.hit_regions.iter().find(|h| h.rect.contains(pos));
590 let hover_rect = hit.map(|h| h.rect);
591 let hover_info = hit.and_then(|h| {
592 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
593 repose_devtools::HoveredInfo {
594 id: s.id,
595 role: format!("{:?}", s.role),
596 label: s.label.clone(),
597 }
598 })
599 });
600 inspector.hud.set_hovered(hover_rect, hover_info);
601 }
602
603 if let Some(win) = &self.window
604 && let Some(c) = result.cursor
605 {
606 win.set_cursor_visible(!cursor_is_hidden(c));
607 win.set_cursor(map_cursor(c));
608 }
609
610 self.request_redraw();
611 }
612
613 WindowEvent::MouseWheel { delta, .. } => {
614 let scale = self
615 .window
616 .as_ref()
617 .map(|w| w.scale_factor() as f32)
618 .unwrap_or(1.0);
619 if crate::runner_common::on_mouse_wheel(&mut self.rt, delta, scale) {
620 self.request_redraw();
621 }
622 }
623
624 WindowEvent::MouseInput { state, button, .. } => {
625 let pos = Vec2 {
626 x: self.rt.mouse_pos_px.0,
627 y: self.rt.mouse_pos_px.1,
628 };
629
630 let mapped = match button {
631 MouseButton::Left => PointerButton::Primary,
632 MouseButton::Right => PointerButton::Secondary,
633 MouseButton::Middle => PointerButton::Tertiary,
634 _ => return,
636 };
637 match (mapped, state) {
638 (PointerButton::Primary, ElementState::Pressed) => {
639 self.rt.sched.mouse_primary = true;
640 }
641 (PointerButton::Primary, ElementState::Released) => {
642 self.rt.sched.mouse_primary = false;
643 }
644 (PointerButton::Secondary, ElementState::Pressed) => {
645 self.rt.sched.mouse_secondary = true;
646 }
647 (PointerButton::Secondary, ElementState::Released) => {
648 self.rt.sched.mouse_secondary = false;
649 }
650 (PointerButton::Tertiary, ElementState::Pressed) => {
651 self.rt.sched.mouse_middle = true;
652 }
653 (PointerButton::Tertiary, ElementState::Released) => {
654 self.rt.sched.mouse_middle = false;
655 }
656 }
657
658 match state {
659 ElementState::Pressed => {
660 let result = self.rt.handle_pointer_press(pos, mapped);
661
662 if let Some(fid) = result.focused
664 && let Some(win) = &self.window
665 && let Some(f) = &self.rt.frame_cache
666 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
667 {
668 let sf = win.scale_factor();
669 rc::set_ime_for_textfield_ex(
670 win,
671 true,
672 hit.keyboard_type.ime_purpose_hint(),
673 hit.auto_correct.unwrap_or(true),
674 hit.capitalization,
675 );
676 win.set_ime_cursor_area(
677 LogicalPosition::new(
678 hit.rect.x as f64 / sf,
679 hit.rect.y as f64 / sf,
680 ),
681 LogicalSize::new(
682 hit.rect.w as f64 / sf,
683 hit.rect.h as f64 / sf,
684 ),
685 );
686 }
687
688 if result.focused.is_none() && self.rt.ime_preedit {
690 if let Some(win) = &self.window {
691 rc::set_ime_for_textfield(win, false);
692 }
693 self.rt.ime_preedit = false;
694 }
695
696 if result.needs_a11y_announce {
697 self.announce_focus_change();
698 }
699
700 if matches!(mapped, PointerButton::Tertiary)
702 && let Some(f) = &self.rt.frame_cache
703 && let Some(cid) = self.rt.capture_id
704 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
705 && self.rt.is_textfield(hit.id)
706 && let Some(txt) = self.paste_from_primary()
707 {
708 self.rt.paste_into_focused(&txt);
709 }
710
711 if matches!(mapped, PointerButton::Primary | PointerButton::Secondary)
713 && let Some(inspector) = &mut self.inspector
714 && inspector.hud.inspector_enabled
715 && let Some(f) = &self.rt.frame_cache
716 && let Some(hit) =
717 f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
718 {
719 let info =
720 f.semantics_nodes.iter().find(|s| s.id == hit.id).map(|s| {
721 repose_devtools::HoveredInfo {
722 id: s.id,
723 role: format!("{:?}", s.role),
724 label: s.label.clone(),
725 }
726 });
727 inspector
728 .hud
729 .select_widget(repose_devtools::SelectedWidget {
730 id: hit.id,
731 role: info
732 .as_ref()
733 .map(|i| i.role.clone())
734 .unwrap_or_default(),
735 label: info.as_ref().and_then(|i| i.label.clone()),
736 bounds: hit.rect,
737 });
738 }
739
740 self.request_redraw();
741 }
742
743 ElementState::Released => {
744 let result = self.rt.handle_pointer_release(pos, mapped);
745
746 if let Some(cid) = result.clicked_id
750 && let Some(f) = &self.rt.frame_cache
751 && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid)
752 {
753 let label = node.label.as_deref().unwrap_or("");
754 self.a11y.announce(&format!("Activated {}", label));
755 }
756
757 self.request_redraw();
758 }
759 }
760 }
761
762 WindowEvent::Touch(t) => {
763 let scale = self
764 .window
765 .as_ref()
766 .map(|w| w.scale_factor() as f32)
767 .unwrap_or(1.0);
768 let r = crate::runner_common::handle_touch_raw(
769 &mut self.rt,
770 &mut self.touch_gestures,
771 &t,
772 scale,
773 );
774 let mut dirty = r.dirty;
775 if let Some((delta_scale, center)) = r.pinch
776 && self.dispatch_action(repose_core::shortcuts::Action::Gesture(
777 repose_core::shortcuts::Gesture::PinchWithCenter {
778 delta_scale,
779 center,
780 },
781 ))
782 {
783 dirty = true;
784 }
785 if let Some((delta, center)) = r.pan
786 && self.dispatch_action(repose_core::shortcuts::Action::Gesture(
787 repose_core::shortcuts::Gesture::Pan { delta, center },
788 ))
789 {
790 dirty = true;
791 }
792 if let Some((delta_rotation, center)) = r.rotation
793 && self.dispatch_action(repose_core::shortcuts::Action::Gesture(
794 repose_core::shortcuts::Gesture::Rotate {
795 delta_rotation,
796 center,
797 },
798 ))
799 {
800 dirty = true;
801 }
802 if let Some(right) = r.swipe_right {
803 let g = if right {
804 repose_core::shortcuts::Gesture::SwipeRight
805 } else {
806 repose_core::shortcuts::Gesture::SwipeLeft
807 };
808 if self.dispatch_action(repose_core::shortcuts::Action::Gesture(g)) {
809 dirty = true;
810 }
811 }
812 if dirty {
813 self.request_redraw();
814 }
815 }
816
817 WindowEvent::ModifiersChanged(new_mods) => {
818 crate::runner_common::on_modifiers_changed(&mut self.rt, &new_mods.state());
819 }
820
821 WindowEvent::KeyboardInput {
822 event: key_event, ..
823 } => {
824 if crate::runner_common::on_keyboard_input(
825 &mut self.rt,
826 &key_event,
827 &mut self.inspector,
828 ) {
829 self.request_redraw();
830 return;
831 }
832
833 if key_event.state == ElementState::Pressed && !key_event.repeat {
836 match key_event.physical_key {
837 PhysicalKey::Code(KeyCode::BrowserBack)
838 | PhysicalKey::Code(KeyCode::Escape) => {
839 use repose_navigation::back;
840 if !back::handle() {
841 }
843 return;
844 }
845 _ => {}
846 }
847 }
848
849 if key_event.state == ElementState::Released
851 && let Some(active_id) = self.rt.key_pressed_active
852 {
853 match key_event.physical_key {
854 PhysicalKey::Code(KeyCode::Space)
855 | PhysicalKey::Code(KeyCode::Enter) => {
856 if let Some(f) = &self.rt.frame_cache
857 && let Some(node) =
858 f.semantics_nodes.iter().find(|n| n.id == active_id)
859 {
860 let label = node.label.as_deref().unwrap_or("");
861 self.a11y.announce(&format!("Activated {}", label));
862 }
863 }
864 _ => {}
865 }
866 }
867 }
868
869 WindowEvent::Ime(ime) => {
870 crate::runner_common::on_ime(&mut self.rt, &ime);
871 self.request_redraw();
872 }
873
874 WindowEvent::RedrawRequested => {
875 crate::run_pre_redraw(&self.render);
877
878 if !self.redraw_requested.replace(false) {
880 self.process_a11y_actions();
881 self.process_render_commands();
882 if let (Some(backend), Some(frame)) =
884 (self.backend.as_mut(), self.rt.frame_cache.as_ref())
885 {
886 let scale = self
887 .window
888 .as_ref()
889 .map(|w| w.scale_factor() as f32)
890 .unwrap_or(1.0);
891 let mut scene = frame.scene.clone();
892 if let Some(inspector) = &mut self.inspector {
893 inspector.frame(&mut scene);
894 }
895 backend.frame(
896 &scene,
897 GlyphRasterConfig {
898 px: Px(18.0 * scale),
899 },
900 );
901 }
902 log::trace!("RedrawRequested: no frame request, skipping compose");
903 return;
904 }
905 log::trace!("RedrawRequested: frame request pending, composing");
906
907 self.process_a11y_actions();
909 self.process_render_commands();
910
911 let Some(win) = self.window.as_ref() else {
912 return;
913 };
914 if self.backend.is_none() {
915 return;
916 }
917
918 repose_core::animation_driver::tick();
921
922 let t0 = Instant::now();
923 let scale = win.scale_factor() as f32;
924 self.rt.scale = scale;
925 let focused = self.rt.sched.focused;
926
927 let output = self.rt.frame(&mut self.root, &self.render);
928
929 if let Some(cursor) = &output.platform.cursor {
930 win.set_cursor_visible(!cursor_is_hidden(*cursor));
931 win.set_cursor(map_cursor(*cursor));
932 }
933
934 if let Some(dark) = output.platform.window_theme_dark
935 && self.last_window_theme != Some(dark)
936 {
937 win.set_theme(Some(if dark {
938 winit::window::Theme::Dark
939 } else {
940 winit::window::Theme::Light
941 }));
942 self.last_window_theme = Some(dark);
943 }
944
945 if output.platform.ime_allowed {
947 rc::set_ime_for_textfield_ex(
948 win,
949 true,
950 output.platform.ime_purpose,
951 output.platform.ime_auto_correct,
952 output.platform.ime_capitalization,
953 );
954 if let Some((x, y, w, h)) = output.platform.ime_cursor_area {
955 win.set_ime_cursor_area(
956 LogicalPosition::new(x, y),
957 LogicalSize::new(w, h),
958 );
959 }
960 } else if self.rt.ime_preedit {
961 rc::set_ime_for_textfield_ex(
962 win,
963 false,
964 repose_core::ImePurposeHint::Normal,
965 true,
966 repose_core::KeyboardCapitalization::Unspecified,
967 );
968 self.rt.ime_preedit = false;
969 }
970
971 if !output.wants_keyboard
973 && focused.is_some()
974 && self.rt.sched.focused.is_none()
975 && self.rt.ime_preedit
976 {
977 rc::set_ime_for_textfield(win, false);
978 self.rt.ime_preedit = false;
979 }
980
981 let frame = output.into_frame();
982
983 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
984
985 if let (Some(adapter), Some(win)) = (&mut self.accesskit_adapter, &self.window)
987 {
988 let scale = win.scale_factor();
989 if let Some(update) = self.a11y_tree.update(
990 &frame.semantics_nodes,
991 scale,
992 self.rt.sched.focused,
993 ) {
994 adapter.update_if_active(|| update);
995 }
996 }
997
998 let mut scene = frame.scene.clone();
1000 if let Some(inspector) = &mut self.inspector {
1002 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1003 let signal_count = self.rt.sched.id_count() as usize;
1004 let ls = repose_ui::last_layout_stats();
1005 inspector.hud.metrics = Some(repose_devtools::Metrics {
1006 build_ms: build_layout_ms,
1007 layout_ms: ls.layout_time_ms,
1008 paint_ms: ls.paint_time_ms,
1009 scene_nodes: scene.nodes.len(),
1010 widget_count,
1011 signal_count,
1012 taffy_created: ls.taffy_created,
1013 taffy_reused: ls.taffy_reused,
1014 layout_hits: ls.layout_hits,
1015 layout_misses: ls.layout_misses,
1016 paint_cache_hits: ls.paint_cache_hits,
1017 paint_cache_misses: ls.paint_cache_misses,
1018 paint_culled: ls.paint_culled,
1019 });
1020 inspector.frame(&mut scene);
1021 }
1022
1023 repose_core::dnd::overlay_drag_indicator(
1025 &mut scene,
1026 self.rt.mouse_pos_px,
1027 self.external_file_drag,
1028 );
1029
1030 self.process_render_commands();
1033
1034 let Some(win) = self.window.as_ref() else {
1036 return;
1037 };
1038 let scale = win.scale_factor() as f32;
1039 if let Some(backend) = self.backend.as_mut() {
1040 backend.frame(
1041 &scene,
1042 GlyphRasterConfig {
1043 px: Px(18.0 * scale),
1044 },
1045 );
1046 }
1047
1048 self.rt.after_compose(&frame, scale);
1052
1053 self.rt.cache_frame(frame);
1056
1057 self.dispatch_file_drop_now();
1058
1059 self.rt.tick_overlays();
1060 self.last_redraw = Instant::now();
1061 }
1062
1063 _ => {}
1064 }
1065 }
1066
1067 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1068 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1071 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK
1072 .lock()
1073 .unwrap_or_else(|e| e.into_inner())
1074 .as_ref()
1075 {
1076 let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(cb));
1077 if let Err(e) = res {
1078 log::error!(
1079 "ABOUT_TO_WAIT_CALLBACK panicked: {}",
1080 e.downcast_ref::<String>()
1081 .map(|s| s.as_str())
1082 .or_else(|| e.downcast_ref::<&str>().copied())
1083 .unwrap_or("unknown")
1084 );
1085 }
1086 }
1087 process_deeplinks();
1088
1089 if let Some(backend) = &mut self.gamepad {
1090 for ev in backend.poll() {
1091 self.rt.handle_gamepad(&ev);
1092 }
1093 for (id, low, high, duration_ms) in self.rt.take_rumble_requests() {
1094 use repose_core::input::GamepadId;
1095 let gid = GamepadId(id);
1096 if duration_ms == 0 {
1097 backend.stop_rumble(gid);
1098 } else if !backend.set_rumble(gid, low, high, duration_ms) {
1099 log::warn!("gamepad: rumble not supported for pad {id}");
1100 }
1101 }
1102 } else {
1103 self.rt.take_rumble_requests();
1104 }
1105
1106 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1107 if WINDOW_VISIBLE.load(Ordering::Relaxed)
1108 && self.backend.is_none()
1109 && let Some(w) = &self.window
1110 {
1111 log::info!("about_to_wait: recreating GPU backend");
1112 match repose_render_wgpu::WgpuBackend::new_with_options(
1113 w.clone(),
1114 self.msaa_samples,
1115 self.present_mode,
1116 ) {
1117 Ok(b) => {
1118 repose_render_wgpu::offscreen::set_shared_device(
1119 b.device.clone(),
1120 b.queue.clone(),
1121 );
1122 self.backend = Some(b)
1123 }
1124 Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
1125 }
1126 }
1127
1128 let needs_compose = take_frame_request();
1129 let needs_present = take_present_request();
1130
1131 if needs_compose {
1132 self.pending_redraw = true;
1133 }
1134
1135 if !self.pending_redraw && needs_present && self.rt.frame_cache.is_some() {
1137 let now = Instant::now();
1138 let interval = self.frame_interval();
1139 if now.saturating_duration_since(self.last_redraw) >= interval {
1140 rc::request_redraw(&self.window);
1141 self.last_redraw = now;
1142 } else {
1143 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1144 self.last_redraw + interval,
1145 ));
1146 }
1147 return;
1148 }
1149
1150 if !self.pending_redraw {
1151 let now = Instant::now();
1152 let idle_cap = web_time::Duration::from_millis(1000);
1153 let deadline = self.rt.next_frame_deadline(now, idle_cap);
1154
1155 if now.saturating_duration_since(self.last_redraw) >= idle_cap
1156 || self.rt.is_wakeup_due(now)
1157 {
1158 self.redraw_requested.set(true);
1159 request_frame();
1160 rc::request_redraw(&self.window);
1161 self.last_redraw = now;
1162 }
1163 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(Ord::min(
1164 deadline,
1165 now + idle_cap,
1166 )));
1167 return;
1168 }
1169
1170 let now = Instant::now();
1171 let interval = self.frame_interval();
1172
1173 if now.saturating_duration_since(self.last_redraw) >= interval {
1174 self.pending_redraw = false;
1175 self.redraw_requested.set(true);
1176 rc::request_redraw(&self.window);
1177 self.last_redraw = now;
1178 } else {
1179 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1180 self.last_redraw + interval,
1181 ));
1182 }
1183 }
1184
1185 fn new_events(
1186 &mut self,
1187 _: &winit::event_loop::ActiveEventLoop,
1188 _: winit::event::StartCause,
1189 ) {
1190 }
1191 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1192 self.pending_redraw = true;
1193 }
1194 fn device_event(
1195 &mut self,
1196 _: &winit::event_loop::ActiveEventLoop,
1197 _: winit::event::DeviceId,
1198 _: winit::event::DeviceEvent,
1199 ) {
1200 }
1201 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1202 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1203 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1204 }
1205
1206 impl App {
1207 fn announce_focus_change(&mut self) {
1208 if let Some(f) = &self.rt.frame_cache {
1209 let focused_node = self
1210 .rt
1211 .sched
1212 .focused
1213 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1214 self.a11y.focus_changed(focused_node);
1215 }
1216 }
1217
1218 fn dispatch_file_drop_now(&mut self) {
1219 let Some(f) = &self.rt.frame_cache else {
1220 self.pending_dropped_files.clear();
1221 self.pending_drop_pos_px = None;
1222 return;
1223 };
1224
1225 if self.pending_dropped_files.is_empty() {
1226 return;
1227 }
1228
1229 let pos_px = self.pending_drop_pos_px.unwrap_or(self.rt.mouse_pos_px);
1230 let pos = Vec2 {
1231 x: pos_px.0,
1232 y: pos_px.1,
1233 };
1234
1235 let mut files = Vec::new();
1236 for p in self.pending_dropped_files.drain(..) {
1237 let name = p
1238 .file_name()
1239 .and_then(|s| s.to_str())
1240 .unwrap_or("file")
1241 .to_string();
1242 files.push(repose_core::dnd::DroppedFile {
1243 name,
1244 path: Some(p),
1245 });
1246 }
1247
1248 let payload: repose_core::dnd::DragPayload =
1249 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1250
1251 let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
1252 self.pending_drop_pos_px = None;
1253 return;
1254 };
1255
1256 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
1257 && let Some(cb) = &hit.on_drop
1258 {
1259 let accepted = cb(repose_core::dnd::DropEvent {
1260 source_id: 0, target_id,
1262 position: pos,
1263 modifiers: self.rt.modifiers,
1264 payload: payload.clone(),
1265 });
1266
1267 if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
1268 {
1269 let label = node.label.as_deref().unwrap_or("");
1270 self.a11y.announce(&format!("Dropped files on {}", label));
1271 }
1272 }
1273
1274 self.pending_drop_pos_px = None;
1275 self.request_redraw();
1276 }
1277 }
1278
1279 let event_loop = EventLoop::new()?;
1280 set_event_loop_proxy(event_loop.create_proxy());
1281 let mut app = App::new(Box::new(root), config);
1282 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1284 event_loop.run_app(&mut app)?;
1285 Ok(())
1286}
1287
1288pub trait A11yBridge: Send {
1296 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1298
1299 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1301
1302 fn announce(&mut self, msg: &str);
1304}
1305
1306#[cfg_attr(target_os = "linux", allow(dead_code))]
1307struct NoopA11y;
1308impl A11yBridge for NoopA11y {
1309 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1310 }
1312 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1313 if let Some(n) = node {
1314 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1315 } else {
1316 log::info!("A11y focus: None");
1317 }
1318 }
1319 fn announce(&mut self, msg: &str) {
1320 log::info!("A11y announce: {msg}");
1321 }
1322}
1323
1324#[cfg(target_os = "linux")]
1325struct LinuxAtspiStub;
1326#[cfg(target_os = "linux")]
1327impl A11yBridge for LinuxAtspiStub {
1328 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1329 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1330 }
1331 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1332 if let Some(n) = node {
1333 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1334 } else {
1335 log::info!("AT-SPI stub focus: None");
1336 }
1337 }
1338 fn announce(&mut self, msg: &str) {
1339 log::info!("AT-SPI stub announce: {msg}");
1340 }
1341}