1use crate::a11y::ReposeActionHandler;
3#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
4use accesskit_winit::Adapter;
5use repose_core::locals::dp_to_px;
6use repose_core::*;
7use repose_ui::textfield::{TF_FONT_DP, TF_PADDING_X_DP, TextMeasureConfig, measure_text};
8use std::cell::Cell;
9use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
10use std::sync::{Arc, Mutex};
11use web_time::Instant;
12
13#[cfg(target_os = "android")]
14pub mod android;
15
16#[cfg(target_arch = "wasm32")]
17pub mod web;
18
19pub mod a11y;
20mod common;
21mod common_web;
22pub mod render;
23
24use common as rc;
25use common_web as rc_web;
26
27pub use render::{ImageHandleGuard, RenderCommand, RenderContext};
28
29#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
30use winit::window::Window;
31
32#[cfg(not(target_arch = "wasm32"))]
33use std::sync::OnceLock;
34
35#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
36static APP_WINDOW: OnceLock<Arc<Window>> = OnceLock::new();
37
38#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
39static WINDOW_VISIBLE: AtomicBool = AtomicBool::new(true);
40
41#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
42static CLOSE_TO_TRAY: AtomicBool = AtomicBool::new(false);
43
44#[cfg(not(target_arch = "wasm32"))]
45static EVENT_LOOP_PROXY: OnceLock<winit::event_loop::EventLoopProxy<()>> = OnceLock::new();
46
47#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
51static ABOUT_TO_WAIT_CALLBACK: Mutex<Option<Box<dyn Fn() + Send>>> = Mutex::new(None);
52
53static DEEPLINK_CB: Mutex<Option<Box<dyn Fn(Vec<u8>) + Send>>> = Mutex::new(None);
54static PENDING_DEEPLINKS: Mutex<Vec<Vec<u8>>> = Mutex::new(Vec::new());
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum AppLifecycle {
60 Foreground,
62 Background,
64}
65
66static CURRENT_LIFECYCLE: AtomicU8 = AtomicU8::new(0);
68static LIFECYCLE_CB: Mutex<Option<Box<dyn Fn(AppLifecycle) + Send>>> = Mutex::new(None);
69#[cfg(target_os = "android")]
70static PENDING_LIFECYCLE: Mutex<Vec<AppLifecycle>> = Mutex::new(Vec::new());
71
72pub fn set_on_lifecycle(callback: Box<dyn Fn(AppLifecycle) + Send>) {
77 *LIFECYCLE_CB.lock().unwrap() = Some(callback);
78}
79
80pub fn current_lifecycle() -> Option<AppLifecycle> {
82 match CURRENT_LIFECYCLE.load(Ordering::Relaxed) {
83 1 => Some(AppLifecycle::Foreground),
84 2 => Some(AppLifecycle::Background),
85 _ => None,
86 }
87}
88
89#[cfg(target_os = "android")]
92pub(crate) fn push_lifecycle(state: AppLifecycle) {
93 let code = match state {
94 AppLifecycle::Foreground => 1,
95 AppLifecycle::Background => 2,
96 };
97 CURRENT_LIFECYCLE.store(code, Ordering::Relaxed);
98 PENDING_LIFECYCLE.lock().unwrap().push(state);
99 #[cfg(not(target_arch = "wasm32"))]
100 wake_event_loop();
101}
102
103#[cfg(target_os = "android")]
106pub(crate) fn process_lifecycle() {
107 let batch = std::mem::take(&mut *PENDING_LIFECYCLE.lock().unwrap());
108 if batch.is_empty() {
109 return;
110 }
111 if let Some(last) = batch.last().copied()
113 && let Some(cb) = LIFECYCLE_CB.lock().unwrap().as_ref()
114 {
115 cb(last);
116 }
117}
118
119pub fn set_on_deeplink(callback: Box<dyn Fn(Vec<u8>) + Send>) {
121 *DEEPLINK_CB.lock().unwrap() = Some(callback);
122}
123
124pub fn push_deeplink(data: Vec<u8>) {
126 PENDING_DEEPLINKS.lock().unwrap().push(data);
127 #[cfg(not(target_arch = "wasm32"))]
128 if let Some(proxy) = EVENT_LOOP_PROXY.get() {
129 let _ = proxy.send_event(());
130 }
131}
132
133pub(crate) fn process_deeplinks() {
136 let mut queue = PENDING_DEEPLINKS.lock().unwrap();
137 if queue.is_empty() {
138 return;
139 }
140 let batch = std::mem::take(&mut *queue);
141 drop(queue);
142
143 if let Some(cb) = DEEPLINK_CB.lock().unwrap().as_ref() {
144 for data in batch {
145 cb(data);
146 }
147 }
148}
149
150#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
152pub fn set_app_window(window: Arc<Window>) {
153 let _ = APP_WINDOW.set(window);
154}
155
156#[cfg(not(target_arch = "wasm32"))]
158pub fn set_event_loop_proxy(proxy: winit::event_loop::EventLoopProxy<()>) {
159 let _ = EVENT_LOOP_PROXY.set(proxy);
160}
161
162#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
164pub fn set_about_to_wait_callback(cb: Box<dyn Fn() + Send>) {
165 *ABOUT_TO_WAIT_CALLBACK.lock().unwrap() = Some(cb);
166}
167
168#[cfg(not(target_arch = "wasm32"))]
170pub fn wake_event_loop() {
171 if let Some(proxy) = EVENT_LOOP_PROXY.get() {
172 let _ = proxy.send_event(());
173 }
174}
175
176#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
180pub fn show_app_window() {
181 WINDOW_VISIBLE.store(true, Ordering::Relaxed);
182 if let Some(w) = APP_WINDOW.get() {
183 log::info!("show_app_window: calling set_visible(true)");
184 w.set_visible(true);
185 #[allow(deprecated)]
186 w.focus_window();
187 }
188 repose_core::frame_clock::request_frame();
189 wake_event_loop();
190}
191
192#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
193pub fn hide_app_window() {
194 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
195 if let Some(w) = APP_WINDOW.get() {
196 log::info!("hide_app_window: calling set_visible(false)");
197 w.set_visible(false);
198 }
199 repose_core::frame_clock::request_frame();
200 wake_event_loop();
201}
202
203#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
205pub fn window_is_visible() -> bool {
206 WINDOW_VISIBLE.load(Ordering::Relaxed)
207}
208
209#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
212pub fn set_close_to_tray(enabled: bool) {
213 CLOSE_TO_TRAY.store(enabled, Ordering::Relaxed);
214}
215
216pub fn tf_ensure_visible_in_rect(state: &mut repose_ui::TextFieldState, inner_rect: Rect) {
218 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
219 let m = measure_text(&state.text, font_px, TextMeasureConfig::default());
220 let caret_x_px = m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
221 state.ensure_caret_visible(
222 caret_x_px,
223 inner_rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
224 dp_to_px(2.0),
225 );
226}
227
228fn winit_key_to_repose(
230 ev: &winit::event::KeyEvent,
231 mapped_key: &repose_core::input::Key,
232 mods: &repose_core::input::Modifiers,
233) -> repose_core::input::KeyEvent {
234 let utf16 = match mapped_key {
235 repose_core::input::Key::Character(c) => *c as u16,
236 _ => 0,
237 };
238 repose_core::input::KeyEvent {
239 key: mapped_key.clone(),
240 modifiers: *mods,
241 is_repeat: ev.repeat,
242 event_type: if ev.state == winit::event::ElementState::Pressed {
243 repose_core::input::KeyEventType::Down
244 } else {
245 repose_core::input::KeyEventType::Up
246 },
247 utf16_code_point: utf16,
248 }
249}
250
251#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
252fn map_cursor(c: repose_core::CursorIcon) -> winit::window::CursorIcon {
253 use winit::window::CursorIcon as W;
254 match c {
255 repose_core::CursorIcon::Default => W::Default,
256 repose_core::CursorIcon::Pointer => W::Pointer,
257 repose_core::CursorIcon::Text => W::Text,
258 repose_core::CursorIcon::EwResize => W::EwResize,
259 repose_core::CursorIcon::NsResize => W::NsResize,
260 repose_core::CursorIcon::Grab => W::Grab,
261 repose_core::CursorIcon::Grabbing => W::Grabbing,
262 }
263}
264
265#[derive(Clone, Copy, Debug)]
267pub struct ReposeOptions {
268 pub msaa_samples: u32,
271 pub max_fps: Option<f32>,
275 pub present_mode: PresentModePref,
277}
278
279impl Default for ReposeOptions {
280 fn default() -> Self {
281 Self {
282 msaa_samples: 4,
283 max_fps: None,
284 present_mode: PresentModePref::Auto,
285 }
286 }
287}
288
289#[derive(Clone, Debug)]
295pub struct AppConfig {
296 pub common: ReposeOptions,
298 pub window_title: String,
300 pub window_size: (u32, u32),
302 pub enable_inspector: bool,
304}
305
306impl Default for AppConfig {
307 fn default() -> Self {
308 Self {
309 common: ReposeOptions::default(),
310 window_title: "Repose".to_string(),
311 window_size: (1280, 800),
312 enable_inspector: true,
313 }
314 }
315}
316
317#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
322#[deprecated(
323 note = "use run_desktop_app_with_config(root, AppConfig) instead; this may be removed in a future release"
324)]
325pub fn run_desktop_app(
326 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
327) -> anyhow::Result<()> {
328 run_desktop_app_with_config(root, AppConfig::default())
329}
330
331#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
333pub fn run_desktop_app_with_config(
334 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
335 config: AppConfig,
336) -> anyhow::Result<()> {
337 use winit::application::ApplicationHandler;
338 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
339 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
340 use winit::event_loop::EventLoop;
341 use winit::keyboard::{KeyCode, PhysicalKey};
342 use winit::window::{Window, WindowAttributes};
343
344 use crate::a11y::A11yTree;
345 use repose_app::ReposeRuntime;
346
347 struct ReposeActivationHandler {
348 initial_tree: Option<accesskit::TreeUpdate>,
349 }
350
351 impl accesskit::ActivationHandler for ReposeActivationHandler {
352 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
353 self.initial_tree.take()
354 }
355 }
356
357 struct ReposeDeactivationHandler;
358
359 impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
360 fn deactivate_accessibility(&mut self) {
361 }
363 }
364
365 struct App {
366 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
367 render: RenderContext,
368 window: Option<Arc<Window>>,
369 backend: Option<repose_render_wgpu::WgpuBackend>,
370 rt: ReposeRuntime,
371 inspector: Option<repose_devtools::Inspector>,
372 msaa_samples: u32,
373 max_fps: Option<f32>,
374 present_mode: PresentModePref,
375 window_title: String,
376 window_size: (u32, u32),
377
378 pending_dropped_files: Vec<std::path::PathBuf>,
380 pending_drop_pos_px: Option<(f32, f32)>,
381
382 external_file_drag: bool,
384 hovered_files: Vec<std::path::PathBuf>,
385
386 clipboard: Option<clipawl::Clipboard>,
387 a11y: Box<dyn A11yBridge>,
388
389 accesskit_adapter: Option<Adapter>,
390 a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
391 a11y_tree: A11yTree,
392
393 last_redraw: Instant,
394 pending_redraw: bool,
395
396 last_window_theme: Option<bool>,
398
399 redraw_requested: Cell<bool>,
401 }
402
403 impl App {
404 fn process_a11y_actions(&mut self) {
405 let mut actions = self.a11y_actions.lock().unwrap();
406 if actions.is_empty() {
407 return;
408 }
409 let pending = actions.drain(..).collect::<Vec<_>>();
410 drop(actions);
411
412 let Some(f) = &self.rt.frame_cache else {
413 return;
414 };
415
416 for req in pending {
417 let target_id = req.target_node.0;
418 match req.action {
419 accesskit::Action::Click => {
420 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
421 && let Some(cb) = &hit.on_click
422 {
423 cb();
424 self.request_redraw();
425 }
426 }
427 accesskit::Action::Focus => {
428 self.rt.sched.focused = Some(target_id);
429 self.request_redraw();
430 }
431 _ => {}
432 }
433 }
434 }
435
436 fn new(
437 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
438 config: AppConfig,
439 ) -> Self {
440 Self {
441 root,
442 render: RenderContext::new(),
443 window: None,
444 backend: None,
445 rt: ReposeRuntime::new(),
446 inspector: if config.enable_inspector {
447 Some(repose_devtools::Inspector::new())
448 } else {
449 None
450 },
451 msaa_samples: config.common.msaa_samples,
452 max_fps: config.common.max_fps,
453 present_mode: config.common.present_mode,
454 window_title: config.window_title,
455 window_size: config.window_size,
456 pending_dropped_files: Vec::new(),
457 pending_drop_pos_px: None,
458
459 external_file_drag: false,
460 hovered_files: Vec::new(),
461
462 clipboard: None,
463 a11y: {
464 #[cfg(target_os = "linux")]
465 {
466 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
467 }
468 #[cfg(not(target_os = "linux"))]
469 {
470 Box::new(NoopA11y) as Box<dyn A11yBridge>
471 }
472 },
473
474 accesskit_adapter: None,
475 a11y_actions: Arc::new(Mutex::new(Vec::new())),
476 a11y_tree: A11yTree::default(),
477
478 last_redraw: Instant::now(),
479 pending_redraw: false,
480 last_window_theme: None,
481 redraw_requested: Cell::new(false),
482 }
483 }
484
485 fn request_redraw(&self) {
486 self.redraw_requested.set(true);
487 repose_core::request_frame();
488 rc::request_redraw(&self.window);
489 }
490
491 fn frame_interval(&self) -> web_time::Duration {
494 match self.max_fps.filter(|f| *f > 0.0) {
495 Some(fps) => {
496 let secs = (1.0 / fps as f64).clamp(0.0, 1.0);
497 web_time::Duration::from_secs_f64(secs)
498 }
499 None => web_time::Duration::ZERO,
500 }
501 }
502
503 fn paste_from_primary(&self) -> Option<String> {
504 let mut opts = clipawl::ClipboardOptions::default();
505 opts.linux.selection = clipawl::LinuxSelection::Primary;
506 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
507 match pollster::block_on(cb.read()) {
508 Ok(t) => Some(t),
509 Err(e) => {
510 eprintln!("Primary paste error: {}", e);
511 None
512 }
513 }
514 } else {
515 None
516 }
517 }
518
519 fn process_render_commands(&mut self) {
520 let Some(backend) = self.backend.as_mut() else {
521 return;
522 };
523 rc::process_render_commands(backend, self.render.drain());
524 }
525
526 fn reset_pointer_state(&mut self) {
527 self.rt.capture_id = None;
528 self.rt.pressed_ids.clear();
529 self.rt.hover_id = None;
530 }
531 }
532
533 impl ApplicationHandler<()> for App {
534 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
535 self.clipboard = clipawl::Clipboard::new()
536 .map_err(|e| {
537 eprintln!("clipawl clipboard init failed: {e}");
538 e
539 })
540 .ok();
541 repose_core::clipboard::set_clipboard_read_fn(Box::new(|| {
542 clipawl::blocking::read().ok()
543 }));
544 repose_core::clipboard::set_clipboard_fn(Box::new(move |text| {
546 if let Err(e) = clipawl::blocking::write(text) {
547 eprintln!("clipboard write error: {e}");
548 }
549 }));
550
551 repose_core::clipboard::set_primary_fn(Box::new(|text| {
552 let mut opts = clipawl::ClipboardOptions::default();
553 opts.linux.selection = clipawl::LinuxSelection::Primary;
554 match clipawl::Clipboard::new_with_options(opts) {
555 Ok(cb) => {
556 if let Err(e) = pollster::block_on(cb.write(text)) {
557 eprintln!("primary selection write error: {e}");
558 }
559 }
560 Err(e) => eprintln!("primary clipboard init error: {e}"),
561 }
562 }));
563
564 if self.window.is_none() {
565 match el.create_window(
566 WindowAttributes::default()
567 .with_title(self.window_title.clone())
568 .with_inner_size(PhysicalSize::new(self.window_size.0, self.window_size.1))
569 .with_visible(false),
570 ) {
571 Ok(win) => {
572 let w = Arc::new(win);
573
574 let activation_handler = ReposeActivationHandler {
575 initial_tree: Some(A11yTree::initial_tree()),
576 };
577
578 let action_handler = ReposeActionHandler {
579 pending_actions: self.a11y_actions.clone(),
580 };
581
582 let deactivation_handler = ReposeDeactivationHandler;
583
584 let adapter = Adapter::with_direct_handlers(
585 el,
586 &w,
587 activation_handler,
588 action_handler,
589 deactivation_handler,
590 );
591
592 self.accesskit_adapter = Some(adapter);
593
594 w.set_visible(true);
595
596 let size = w.inner_size();
597 let sf = w.scale_factor() as f32;
598 self.rt.set_viewport_and_scale(size.width, size.height, sf);
599
600 match repose_render_wgpu::WgpuBackend::new_with_options(
601 w.clone(),
602 self.msaa_samples,
603 self.present_mode,
604 ) {
605 Ok(b) => {
606 self.backend = Some(b);
607 set_app_window(w.clone());
608 self.window = Some(w);
609 self.request_redraw();
610 }
611 Err(e) => {
612 log::error!("Failed to create WGPU backend: {e:?}");
613 el.exit();
614 }
615 }
616 }
617 Err(e) => {
618 log::error!("Failed to create window: {e:?}");
619 el.exit();
620 }
621 }
622 }
623 }
624
625 fn window_event(
626 &mut self,
627 el: &winit::event_loop::ActiveEventLoop,
628 _id: winit::window::WindowId,
629 event: WindowEvent,
630 ) {
631 if let Some(adapter) = &mut self.accesskit_adapter {
633 adapter.process_event(self.window.as_ref().unwrap(), &event);
634 }
635
636 match event {
637 WindowEvent::CloseRequested => {
638 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
639 self.backend = None;
641 if let Some(w) = &self.window {
642 w.set_visible(false);
643 }
644 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
645 } else {
646 el.exit();
647 }
648 }
649
650 WindowEvent::Focused(false) => {
651 self.rt.handle_focus_lost();
653
654 self.external_file_drag = false;
656 self.hovered_files.clear();
657
658 if let Some(w) = &self.window {
659 rc_web::set_ime_for_textfield(w, false);
660 }
661
662 self.request_redraw();
663 }
664
665 WindowEvent::CursorLeft { .. } => {
666 self.rt.pointer_inside = false;
667 self.rt.clear_hover();
668 self.external_file_drag = false;
669 self.hovered_files.clear();
670 self.request_redraw();
671 }
672
673 WindowEvent::HoveredFile(path) => {
674 self.external_file_drag = true;
676 if self.hovered_files.len() < 32 {
677 self.hovered_files.push(path);
678 }
679 if self.pending_drop_pos_px.is_none() {
681 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
682 }
683 self.request_redraw();
684 }
685
686 WindowEvent::HoveredFileCancelled => {
687 self.external_file_drag = false;
688 self.hovered_files.clear();
689
690 self.reset_pointer_state();
692
693 self.request_redraw();
694 }
695
696 WindowEvent::DroppedFile(path) => {
697 self.pending_dropped_files.push(path);
699 if self.pending_drop_pos_px.is_none() {
700 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
701 }
702
703 self.external_file_drag = false;
705 self.hovered_files.clear();
706
707 self.request_redraw();
708 }
709
710 WindowEvent::Resized(size) => {
711 let sf = self
712 .window
713 .as_ref()
714 .map(|w| w.scale_factor() as f32)
715 .unwrap_or(1.0);
716 self.rt.set_viewport_and_scale(size.width, size.height, sf);
717 if let Some(b) = self.backend.as_mut() {
718 b.configure_surface(size.width, size.height);
719 }
720 if let Some(w) = &self.window {
721 let sf = w.scale_factor() as f32;
722 let dp_w = size.width as f32 / sf;
723 let dp_h = size.height as f32 / sf;
724 log::info!(
725 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
726 size.width,
727 size.height,
728 sf,
729 dp_w as i32,
730 dp_h as i32
731 );
732 }
733 self.request_redraw();
734 }
735
736 WindowEvent::CursorMoved { position, .. } => {
737 self.rt.pointer_inside = true;
738
739 if self.external_file_drag {
740 self.pending_drop_pos_px = Some((position.x as f32, position.y as f32));
741 }
742
743 let pos = Vec2 {
744 x: position.x as f32,
745 y: position.y as f32,
746 };
747
748 let result = self.rt.handle_pointer_move(pos);
750
751 if let (Some(inspector), Some(f)) = (&mut self.inspector, &self.rt.frame_cache)
753 && inspector.hud.inspector_enabled
754 {
755 let hit = f.hit_regions.iter().find(|h| h.rect.contains(pos));
756 let hover_rect = hit.map(|h| h.rect);
757 let hover_info = hit.and_then(|h| {
758 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
759 repose_devtools::HoveredInfo {
760 id: s.id,
761 role: format!("{:?}", s.role),
762 label: s.label.clone(),
763 }
764 })
765 });
766 inspector.hud.set_hovered(hover_rect, hover_info);
767 }
768
769 if let Some(win) = &self.window
771 && let Some(c) = result.cursor
772 {
773 win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
774 }
775
776 self.request_redraw();
777 }
778
779 WindowEvent::MouseWheel { delta, .. } => {
780 let (dx_px, dy_px) = match delta {
781 MouseScrollDelta::LineDelta(x, y) => {
782 let unit_px = dp_to_px(60.0);
783 (-(x * unit_px), -(y * unit_px))
784 }
785 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
786 };
787 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
788
789 if self.rt.handle_scroll(Vec2 { x: dx_px, y: dy_px }) {
790 self.request_redraw();
791 }
792 }
793
794 WindowEvent::MouseInput { state, button, .. } => {
795 let pos = Vec2 {
796 x: self.rt.mouse_pos_px.0,
797 y: self.rt.mouse_pos_px.1,
798 };
799
800 let mapped = match button {
801 MouseButton::Left => PointerButton::Primary,
802 MouseButton::Right => PointerButton::Secondary,
803 MouseButton::Middle => PointerButton::Tertiary,
804 _ => return,
806 };
807
808 match state {
809 ElementState::Pressed => {
810 let result = self.rt.handle_pointer_press(pos, mapped);
811
812 if let Some(fid) = result.focused
814 && let Some(win) = &self.window
815 && let Some(f) = &self.rt.frame_cache
816 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
817 {
818 let sf = win.scale_factor();
819 rc_web::set_ime_for_textfield_ex(
820 win,
821 true,
822 hit.keyboard_type.ime_purpose_hint(),
823 hit.auto_correct.unwrap_or(true),
824 hit.capitalization,
825 );
826 win.set_ime_cursor_area(
827 LogicalPosition::new(
828 hit.rect.x as f64 / sf,
829 hit.rect.y as f64 / sf,
830 ),
831 LogicalSize::new(
832 hit.rect.w as f64 / sf,
833 hit.rect.h as f64 / sf,
834 ),
835 );
836 }
837
838 if result.focused.is_none() && self.rt.ime_preedit {
840 if let Some(win) = &self.window {
841 rc_web::set_ime_for_textfield(win, false);
842 }
843 self.rt.ime_preedit = false;
844 }
845
846 if result.needs_a11y_announce {
847 self.announce_focus_change();
848 }
849
850 if matches!(mapped, PointerButton::Tertiary)
852 && let Some(f) = &self.rt.frame_cache
853 && let Some(cid) = self.rt.capture_id
854 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
855 && self.rt.is_textfield(hit.id)
856 && let Some(txt) = self.paste_from_primary()
857 {
858 self.rt.paste_into_focused(&txt);
859 }
860
861 if matches!(mapped, PointerButton::Primary | PointerButton::Secondary)
863 && let Some(inspector) = &mut self.inspector
864 && inspector.hud.inspector_enabled
865 && let Some(f) = &self.rt.frame_cache
866 && let Some(hit) =
867 f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
868 {
869 let info =
870 f.semantics_nodes.iter().find(|s| s.id == hit.id).map(|s| {
871 repose_devtools::HoveredInfo {
872 id: s.id,
873 role: format!("{:?}", s.role),
874 label: s.label.clone(),
875 }
876 });
877 inspector
878 .hud
879 .select_widget(repose_devtools::SelectedWidget {
880 id: hit.id,
881 role: info
882 .as_ref()
883 .map(|i| i.role.clone())
884 .unwrap_or_default(),
885 label: info.as_ref().and_then(|i| i.label.clone()),
886 bounds: hit.rect,
887 });
888 }
889
890 self.request_redraw();
891 }
892
893 ElementState::Released => {
894 let result = self.rt.handle_pointer_release(pos, mapped);
895
896 if let Some(cid) = result.clicked_id
900 && let Some(f) = &self.rt.frame_cache
901 && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid)
902 {
903 let label = node.label.as_deref().unwrap_or("");
904 self.a11y.announce(&format!("Activated {}", label));
905 }
906
907 self.request_redraw();
908 }
909 }
910 }
911
912 WindowEvent::ModifiersChanged(new_mods) => {
913 let state = new_mods.state();
914 self.rt.modifiers.shift = state.shift_key();
915 self.rt.modifiers.ctrl = state.control_key();
916 self.rt.modifiers.alt = state.alt_key();
917 self.rt.modifiers.meta = state.super_key();
918 self.rt.modifiers.command = if cfg!(target_os = "macos") {
919 self.rt.modifiers.meta
920 } else {
921 self.rt.modifiers.ctrl
922 };
923 }
924
925 WindowEvent::KeyboardInput {
926 event: key_event, ..
927 } => {
928 if key_event.state == ElementState::Pressed
930 && !key_event.repeat
931 && self.rt.modifiers.ctrl
932 && self.rt.modifiers.shift
933 && key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
934 && let Some(inspector) = &mut self.inspector
935 {
936 inspector.hud.toggle_inspector();
937 self.request_redraw();
938 return;
939 }
940
941 let mapped = rc::map_key(key_event.physical_key);
945 let ke = winit_key_to_repose(&key_event, &mapped, &self.rt.modifiers);
946 if self.rt.handle_key_with_text(&ke, key_event.text.as_deref()) {
947 self.request_redraw();
948 return;
949 }
950
951 if key_event.state == ElementState::Pressed && !key_event.repeat {
954 match key_event.physical_key {
955 PhysicalKey::Code(KeyCode::BrowserBack)
956 | PhysicalKey::Code(KeyCode::Escape) => {
957 use repose_navigation::back;
958 if !back::handle() {
959 }
961 return;
962 }
963 _ => {}
964 }
965 }
966
967 if key_event.state == ElementState::Released
969 && let Some(active_id) = self.rt.key_pressed_active
970 {
971 match key_event.physical_key {
972 PhysicalKey::Code(KeyCode::Space)
973 | PhysicalKey::Code(KeyCode::Enter) => {
974 if let Some(f) = &self.rt.frame_cache
975 && let Some(node) =
976 f.semantics_nodes.iter().find(|n| n.id == active_id)
977 {
978 let label = node.label.as_deref().unwrap_or("");
979 self.a11y.announce(&format!("Activated {}", label));
980 }
981 }
982 _ => {}
983 }
984 }
985 }
986
987 WindowEvent::Ime(ime) => {
988 let ime_event = match ime {
991 winit::event::Ime::Enabled => repose_core::input::ImeEvent::Start,
992 winit::event::Ime::Preedit(text, cursor) => {
993 repose_core::input::ImeEvent::Update { text, cursor }
994 }
995 winit::event::Ime::Commit(text) => {
996 repose_core::input::ImeEvent::Commit(text)
997 }
998 winit::event::Ime::Disabled => repose_core::input::ImeEvent::Cancel,
999 };
1000 self.rt.handle_ime(&ime_event);
1001 self.request_redraw();
1002 }
1003
1004 WindowEvent::RedrawRequested => {
1005 if !self.redraw_requested.replace(false) {
1007 self.process_a11y_actions();
1008 self.process_render_commands();
1009 if let (Some(backend), Some(frame)) =
1011 (self.backend.as_mut(), self.rt.frame_cache.as_ref())
1012 {
1013 let scale = self
1014 .window
1015 .as_ref()
1016 .map(|w| w.scale_factor() as f32)
1017 .unwrap_or(1.0);
1018 let mut scene = frame.scene.clone();
1019 if let Some(inspector) = &mut self.inspector {
1020 inspector.frame(&mut scene);
1021 }
1022 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1023 }
1024 log::trace!("RedrawRequested: no frame request, skipping compose");
1025 return;
1026 }
1027 log::trace!("RedrawRequested: frame request pending, composing");
1028
1029 self.process_a11y_actions();
1031 self.process_render_commands();
1032
1033 let Some(win) = self.window.as_ref() else {
1034 return;
1035 };
1036 if self.backend.is_none() {
1037 return;
1038 }
1039
1040 repose_core::animation_driver::tick();
1043
1044 let t0 = Instant::now();
1045 let scale = win.scale_factor() as f32;
1046 self.rt.scale = scale;
1047 let focused = self.rt.sched.focused;
1048
1049 let output = self.rt.frame(&mut self.root, &self.render);
1050
1051 if let Some(cursor) = &output.platform.cursor {
1053 win.set_cursor(winit::window::Cursor::Icon(map_cursor(*cursor)));
1054 }
1055
1056 if let Some(dark) = output.platform.window_theme_dark
1058 && self.last_window_theme != Some(dark)
1059 {
1060 win.set_theme(Some(if dark {
1061 winit::window::Theme::Dark
1062 } else {
1063 winit::window::Theme::Light
1064 }));
1065 self.last_window_theme = Some(dark);
1066 }
1067
1068 if output.platform.ime_allowed {
1070 rc_web::set_ime_for_textfield_ex(
1071 win,
1072 true,
1073 output.platform.ime_purpose,
1074 output.platform.ime_auto_correct,
1075 output.platform.ime_capitalization,
1076 );
1077 if let Some((x, y, w, h)) = output.platform.ime_cursor_area {
1078 win.set_ime_cursor_area(
1079 LogicalPosition::new(x, y),
1080 LogicalSize::new(w, h),
1081 );
1082 }
1083 } else if self.rt.ime_preedit {
1084 rc_web::set_ime_for_textfield_ex(
1085 win,
1086 false,
1087 repose_core::ImePurposeHint::Normal,
1088 true,
1089 repose_core::KeyboardCapitalization::Unspecified,
1090 );
1091 self.rt.ime_preedit = false;
1092 }
1093
1094 if !output.wants_keyboard
1096 && focused.is_some()
1097 && self.rt.sched.focused.is_none()
1098 && self.rt.ime_preedit
1099 {
1100 rc_web::set_ime_for_textfield(win, false);
1101 self.rt.ime_preedit = false;
1102 }
1103
1104 let frame = Frame {
1105 scene: output.scene,
1106 hit_regions: output.hit_regions,
1107 semantics_nodes: output.semantics_nodes,
1108 focus_chain: output.focus_chain,
1109 };
1110
1111 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1112
1113 if let Some(adapter) = &mut self.accesskit_adapter {
1115 let win = self.window.as_ref().unwrap();
1116 let scale = win.scale_factor();
1117 if let Some(update) = self.a11y_tree.update(
1118 &frame.semantics_nodes,
1119 scale,
1120 self.rt.sched.focused,
1121 ) {
1122 adapter.update_if_active(|| update);
1123 }
1124 }
1125
1126 let mut scene = frame.scene.clone();
1128 if let Some(inspector) = &mut self.inspector {
1130 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1131 let signal_count = self.rt.sched.id_count() as usize;
1132 let ls = repose_ui::last_layout_stats();
1133 inspector.hud.metrics = Some(repose_devtools::Metrics {
1134 build_ms: build_layout_ms,
1135 layout_ms: ls.layout_time_ms,
1136 paint_ms: ls.paint_time_ms,
1137 scene_nodes: scene.nodes.len(),
1138 widget_count,
1139 signal_count,
1140 taffy_created: ls.taffy_created,
1141 taffy_reused: ls.taffy_reused,
1142 layout_hits: ls.layout_hits,
1143 layout_misses: ls.layout_misses,
1144 paint_cache_hits: ls.paint_cache_hits,
1145 paint_cache_misses: ls.paint_cache_misses,
1146 paint_culled: ls.paint_culled,
1147 });
1148 inspector.frame(&mut scene);
1149 }
1150
1151 repose_core::dnd::overlay_drag_indicator(
1153 &mut scene,
1154 self.rt.mouse_pos_px,
1155 self.external_file_drag,
1156 );
1157
1158 self.process_render_commands();
1161
1162 let win = self.window.as_ref().unwrap();
1164 let scale = win.scale_factor() as f32;
1165 if let Some(backend) = self.backend.as_mut() {
1166 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1167 }
1168
1169 self.rt.after_compose(&frame, scale);
1173
1174 self.rt.cache_frame(frame);
1177
1178 self.dispatch_file_drop_now();
1179
1180 self.rt.tick_overlays(self.last_redraw);
1181 self.last_redraw = Instant::now();
1182 }
1183
1184 _ => {}
1185 }
1186 }
1187
1188 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1189 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1192 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK.lock().unwrap().as_ref() {
1193 cb();
1194 }
1195 process_deeplinks();
1196
1197 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1200 if WINDOW_VISIBLE.load(Ordering::Relaxed)
1201 && self.backend.is_none()
1202 && let Some(w) = &self.window
1203 {
1204 log::info!("about_to_wait: recreating GPU backend");
1205 match repose_render_wgpu::WgpuBackend::new_with_options(
1206 w.clone(),
1207 self.msaa_samples,
1208 self.present_mode,
1209 ) {
1210 Ok(b) => self.backend = Some(b),
1211 Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
1212 }
1213 }
1214
1215 let needs_compose = take_frame_request();
1216 let needs_present = take_present_request();
1217
1218 if needs_compose {
1219 self.pending_redraw = true;
1220 }
1221
1222 if !self.pending_redraw && needs_present && self.rt.frame_cache.is_some() {
1224 let now = Instant::now();
1225 let interval = self.frame_interval();
1226 if now.saturating_duration_since(self.last_redraw) >= interval {
1227 rc::request_redraw(&self.window);
1228 self.last_redraw = now;
1229 } else {
1230 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1231 self.last_redraw + interval,
1232 ));
1233 }
1234 return;
1235 }
1236
1237 if !self.pending_redraw {
1238 let now = Instant::now();
1239 let idle_cap = web_time::Duration::from_millis(1000);
1240 let deadline = self
1241 .rt
1242 .next_caret_blink_deadline()
1243 .unwrap_or(now + idle_cap);
1244
1245 if now.saturating_duration_since(self.last_redraw) >= idle_cap || now >= deadline {
1246 self.redraw_requested.set(true);
1247 request_frame();
1248 rc::request_redraw(&self.window);
1249 self.last_redraw = now;
1250 }
1251 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(Ord::min(
1252 deadline,
1253 now + idle_cap,
1254 )));
1255 return;
1256 }
1257
1258 let now = Instant::now();
1259 let interval = self.frame_interval();
1260
1261 if now.saturating_duration_since(self.last_redraw) >= interval {
1262 self.pending_redraw = false;
1263 self.redraw_requested.set(true);
1264 rc::request_redraw(&self.window);
1265 self.last_redraw = now;
1266 } else {
1267 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1268 self.last_redraw + interval,
1269 ));
1270 }
1271 }
1272
1273 fn new_events(
1274 &mut self,
1275 _: &winit::event_loop::ActiveEventLoop,
1276 _: winit::event::StartCause,
1277 ) {
1278 }
1279 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1280 self.pending_redraw = true;
1281 }
1282 fn device_event(
1283 &mut self,
1284 _: &winit::event_loop::ActiveEventLoop,
1285 _: winit::event::DeviceId,
1286 _: winit::event::DeviceEvent,
1287 ) {
1288 }
1289 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1290 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1291 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1292 }
1293
1294 impl App {
1295 fn announce_focus_change(&mut self) {
1296 if let Some(f) = &self.rt.frame_cache {
1297 let focused_node = self
1298 .rt
1299 .sched
1300 .focused
1301 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1302 self.a11y.focus_changed(focused_node);
1303 }
1304 }
1305
1306 fn dispatch_file_drop_now(&mut self) {
1307 let Some(f) = &self.rt.frame_cache else {
1308 self.pending_dropped_files.clear();
1309 self.pending_drop_pos_px = None;
1310 return;
1311 };
1312
1313 if self.pending_dropped_files.is_empty() {
1314 return;
1315 }
1316
1317 let pos_px = self.pending_drop_pos_px.unwrap_or(self.rt.mouse_pos_px);
1318 let pos = Vec2 {
1319 x: pos_px.0,
1320 y: pos_px.1,
1321 };
1322
1323 let mut files = Vec::new();
1324 for p in self.pending_dropped_files.drain(..) {
1325 let name = p
1326 .file_name()
1327 .and_then(|s| s.to_str())
1328 .unwrap_or("file")
1329 .to_string();
1330 files.push(repose_core::dnd::DroppedFile {
1331 name,
1332 path: Some(p),
1333 });
1334 }
1335
1336 let payload: repose_core::dnd::DragPayload =
1337 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1338
1339 let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
1340 self.pending_drop_pos_px = None;
1341 return;
1342 };
1343
1344 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
1345 && let Some(cb) = &hit.on_drop
1346 {
1347 let accepted = cb(repose_core::dnd::DropEvent {
1348 source_id: 0, target_id,
1350 position: pos,
1351 modifiers: self.rt.modifiers,
1352 payload: payload.clone(),
1353 });
1354
1355 if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
1356 {
1357 let label = node.label.as_deref().unwrap_or("");
1358 self.a11y.announce(&format!("Dropped files on {}", label));
1359 }
1360 }
1361
1362 self.pending_drop_pos_px = None;
1363 self.request_redraw();
1364 }
1365 }
1366
1367 let event_loop = EventLoop::new()?;
1368 set_event_loop_proxy(event_loop.create_proxy());
1369 let mut app = App::new(Box::new(root), config);
1370 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1372 event_loop.run_app(&mut app)?;
1373 Ok(())
1374}
1375
1376pub trait A11yBridge: Send {
1384 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1386
1387 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1389
1390 fn announce(&mut self, msg: &str);
1392}
1393
1394#[cfg_attr(target_os = "linux", allow(dead_code))]
1395struct NoopA11y;
1396impl A11yBridge for NoopA11y {
1397 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1398 }
1400 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1401 if let Some(n) = node {
1402 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1403 } else {
1404 log::info!("A11y focus: None");
1405 }
1406 }
1407 fn announce(&mut self, msg: &str) {
1408 log::info!("A11y announce: {msg}");
1409 }
1410}
1411
1412#[cfg(target_os = "linux")]
1413struct LinuxAtspiStub;
1414#[cfg(target_os = "linux")]
1415impl A11yBridge for LinuxAtspiStub {
1416 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1417 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1418 }
1419 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1420 if let Some(n) = node {
1421 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1422 } else {
1423 log::info!("AT-SPI stub focus: None");
1424 }
1425 }
1426 fn announce(&mut self, msg: &str) {
1427 log::info!("AT-SPI stub announce: {msg}");
1428 }
1429}