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