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