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::{
8 TF_FONT_DP, TF_PADDING_X_DP, TextFieldState, TextMeasureConfig, measure_text,
9};
10use std::cell::{Cell, RefCell};
11use std::rc::Rc;
12use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
13use std::sync::{Arc, Mutex};
14use web_time::Instant;
15
16#[cfg(target_os = "android")]
17pub mod android;
18
19#[cfg(target_arch = "wasm32")]
20pub mod web;
21
22pub mod a11y;
23mod common;
24#[cfg(not(target_arch = "wasm32"))]
25mod common_android;
26mod common_web;
27pub mod render;
28
29use common as rc;
30#[cfg(not(target_arch = "wasm32"))]
31use common_android as rc_android;
32use common_web as rc_web;
33
34pub use render::{ImageHandleGuard, RenderCommand, RenderContext};
35
36#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
37use winit::window::Window;
38
39#[cfg(not(target_arch = "wasm32"))]
40use std::sync::OnceLock;
41
42#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
43static APP_WINDOW: OnceLock<Arc<Window>> = OnceLock::new();
44
45#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
46static WINDOW_VISIBLE: AtomicBool = AtomicBool::new(true);
47
48#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
49static CLOSE_TO_TRAY: AtomicBool = AtomicBool::new(false);
50
51#[cfg(not(target_arch = "wasm32"))]
52static EVENT_LOOP_PROXY: OnceLock<winit::event_loop::EventLoopProxy<()>> = OnceLock::new();
53
54#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
58static ABOUT_TO_WAIT_CALLBACK: Mutex<Option<Box<dyn Fn() + Send>>> = Mutex::new(None);
59
60static DEEPLINK_CB: Mutex<Option<Box<dyn Fn(Vec<u8>) + Send>>> = Mutex::new(None);
61static PENDING_DEEPLINKS: Mutex<Vec<Vec<u8>>> = Mutex::new(Vec::new());
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum AppLifecycle {
67 Foreground,
69 Background,
71}
72
73static CURRENT_LIFECYCLE: AtomicU8 = AtomicU8::new(0);
75static LIFECYCLE_CB: Mutex<Option<Box<dyn Fn(AppLifecycle) + Send>>> = Mutex::new(None);
76static PENDING_LIFECYCLE: Mutex<Vec<AppLifecycle>> = Mutex::new(Vec::new());
77
78pub fn set_on_lifecycle(callback: Box<dyn Fn(AppLifecycle) + Send>) {
83 *LIFECYCLE_CB.lock().unwrap() = Some(callback);
84}
85
86pub fn current_lifecycle() -> Option<AppLifecycle> {
88 match CURRENT_LIFECYCLE.load(Ordering::Relaxed) {
89 1 => Some(AppLifecycle::Foreground),
90 2 => Some(AppLifecycle::Background),
91 _ => None,
92 }
93}
94
95pub(crate) fn push_lifecycle(state: AppLifecycle) {
98 let code = match state {
99 AppLifecycle::Foreground => 1,
100 AppLifecycle::Background => 2,
101 };
102 CURRENT_LIFECYCLE.store(code, Ordering::Relaxed);
103 PENDING_LIFECYCLE.lock().unwrap().push(state);
104 #[cfg(not(target_arch = "wasm32"))]
105 wake_event_loop();
106}
107
108pub(crate) fn process_lifecycle() {
111 let batch = std::mem::take(&mut *PENDING_LIFECYCLE.lock().unwrap());
112 if batch.is_empty() {
113 return;
114 }
115 if let Some(last) = batch.last().copied()
117 && let Some(cb) = LIFECYCLE_CB.lock().unwrap().as_ref()
118 {
119 cb(last);
120 }
121}
122
123pub fn set_on_deeplink(callback: Box<dyn Fn(Vec<u8>) + Send>) {
125 *DEEPLINK_CB.lock().unwrap() = Some(callback);
126}
127
128pub fn push_deeplink(data: Vec<u8>) {
130 PENDING_DEEPLINKS.lock().unwrap().push(data);
131 #[cfg(not(target_arch = "wasm32"))]
132 if let Some(proxy) = EVENT_LOOP_PROXY.get() {
133 let _ = proxy.send_event(());
134 }
135}
136
137pub(crate) fn process_deeplinks() {
140 let mut queue = PENDING_DEEPLINKS.lock().unwrap();
141 if queue.is_empty() {
142 return;
143 }
144 let batch = std::mem::take(&mut *queue);
145 drop(queue);
146
147 if let Some(cb) = DEEPLINK_CB.lock().unwrap().as_ref() {
148 for data in batch {
149 cb(data);
150 }
151 }
152}
153
154#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
156pub fn set_app_window(window: Arc<Window>) {
157 let _ = APP_WINDOW.set(window);
158}
159
160#[cfg(not(target_arch = "wasm32"))]
162pub fn set_event_loop_proxy(proxy: winit::event_loop::EventLoopProxy<()>) {
163 let _ = EVENT_LOOP_PROXY.set(proxy);
164}
165
166#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
168pub fn set_about_to_wait_callback(cb: Box<dyn Fn() + Send>) {
169 *ABOUT_TO_WAIT_CALLBACK.lock().unwrap() = Some(cb);
170}
171
172#[cfg(not(target_arch = "wasm32"))]
174pub fn wake_event_loop() {
175 if let Some(proxy) = EVENT_LOOP_PROXY.get() {
176 let _ = proxy.send_event(());
177 }
178}
179
180#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
184pub fn show_app_window() {
185 WINDOW_VISIBLE.store(true, Ordering::Relaxed);
186 if let Some(w) = APP_WINDOW.get() {
187 log::info!("show_app_window: calling set_visible(true)");
188 w.set_visible(true);
189 #[allow(deprecated)]
190 w.focus_window();
191 }
192 repose_core::frame_clock::request_frame();
193 wake_event_loop();
194}
195
196#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
197pub fn hide_app_window() {
198 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
199 if let Some(w) = APP_WINDOW.get() {
200 log::info!("hide_app_window: calling set_visible(false)");
201 w.set_visible(false);
202 }
203 repose_core::frame_clock::request_frame();
204 wake_event_loop();
205}
206
207#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
209pub fn window_is_visible() -> bool {
210 WINDOW_VISIBLE.load(Ordering::Relaxed)
211}
212
213#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
216pub fn set_close_to_tray(enabled: bool) {
217 CLOSE_TO_TRAY.store(enabled, Ordering::Relaxed);
218}
219
220pub fn compose_frame<F>(
222 sched: &mut Scheduler,
223 root_fn: &mut F,
224 scale: f32,
225 size_px_u32: (u32, u32),
226 hover_id: Option<u64>,
227 pressed_ids: &std::collections::HashSet<u64>,
228 tf_states: &std::collections::HashMap<u64, Rc<RefCell<repose_ui::TextFieldState>>>,
229 _focused: Option<u64>,
230) -> Frame
231where
232 F: FnMut(&mut Scheduler) -> View,
233{
234 if let Some(requested_id) = repose_core::take_focus_request() {
236 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
237 sched.focused = None;
238 } else {
239 sched.focused = Some(requested_id);
240 }
241 }
242
243 set_density_default(Density { scale });
244
245 let current_focused = sched.focused;
247
248 let frame = sched.repose(
249 {
250 let scale = scale;
251 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s))
252 },
253 {
254 let hover_id = hover_id;
255 let pressed_ids = pressed_ids.clone();
256 move |view, _size| {
257 let interactions = repose_ui::Interactions {
258 hover: hover_id,
259 pressed: pressed_ids.clone(),
260 };
261
262 with_density(Density { scale }, || {
263 repose_ui::layout_and_paint(
264 view,
265 size_px_u32,
266 tf_states,
267 &interactions,
268 current_focused,
269 )
270 })
271 }
272 },
273 );
274
275 if let Some(fid) = sched.focused
276 && !frame.focus_chain.contains(&fid)
277 {
278 sched.focused = None;
279 }
280
281 frame
282}
283
284pub(crate) fn next_caret_blink_deadline(
285 sched: &Scheduler,
286 frame_cache: &Option<Frame>,
287 textfield_states: &std::collections::HashMap<u64, Rc<RefCell<TextFieldState>>>,
288) -> Option<Instant> {
289 let fid = sched.focused?;
290 let frame = frame_cache.as_ref()?;
291 let hit = frame.hit_regions.iter().find(|h| h.id == fid)?;
292 let key = hit.tf_state_key?;
293 textfield_states.get(&key)?.borrow().next_blink_deadline()
294}
295
296pub fn tf_ensure_visible_in_rect(state: &mut repose_ui::TextFieldState, inner_rect: Rect) {
298 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
299 let m = measure_text(&state.text, font_px, TextMeasureConfig::default());
300 let caret_x_px = m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
301 state.ensure_caret_visible(
302 caret_x_px,
303 inner_rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
304 dp_to_px(2.0),
305 );
306}
307
308fn winit_key_to_repose(
310 ev: &winit::event::KeyEvent,
311 mapped_key: &repose_core::input::Key,
312 mods: &repose_core::input::Modifiers,
313) -> repose_core::input::KeyEvent {
314 let utf16 = match mapped_key {
315 repose_core::input::Key::Character(c) => *c as u16,
316 _ => 0,
317 };
318 repose_core::input::KeyEvent {
319 key: mapped_key.clone(),
320 modifiers: *mods,
321 is_repeat: ev.repeat,
322 event_type: if ev.state == winit::event::ElementState::Pressed {
323 repose_core::input::KeyEventType::Down
324 } else {
325 repose_core::input::KeyEventType::Up
326 },
327 utf16_code_point: utf16,
328 }
329}
330
331#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
332fn map_cursor(c: repose_core::CursorIcon) -> winit::window::CursorIcon {
333 use winit::window::CursorIcon as W;
334 match c {
335 repose_core::CursorIcon::Default => W::Default,
336 repose_core::CursorIcon::Pointer => W::Pointer,
337 repose_core::CursorIcon::Text => W::Text,
338 repose_core::CursorIcon::EwResize => W::EwResize,
339 repose_core::CursorIcon::NsResize => W::NsResize,
340 repose_core::CursorIcon::Grab => W::Grab,
341 repose_core::CursorIcon::Grabbing => W::Grabbing,
342 }
343}
344
345#[derive(Clone, Copy, Debug)]
347pub struct ReposeOptions {
348 pub msaa_samples: u32,
351 pub max_fps: Option<f32>,
355 pub present_mode: PresentModePref,
357}
358
359impl Default for ReposeOptions {
360 fn default() -> Self {
361 Self {
362 msaa_samples: 4,
363 max_fps: None,
364 present_mode: PresentModePref::Auto,
365 }
366 }
367}
368
369#[derive(Clone, Debug)]
375pub struct AppConfig {
376 pub common: ReposeOptions,
378 pub window_title: String,
380 pub window_size: (u32, u32),
382 pub enable_inspector: bool,
384}
385
386impl Default for AppConfig {
387 fn default() -> Self {
388 Self {
389 common: ReposeOptions::default(),
390 window_title: "Repose".to_string(),
391 window_size: (1280, 800),
392 enable_inspector: true,
393 }
394 }
395}
396
397#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
402#[deprecated(
403 note = "use run_desktop_app_with_config(root, AppConfig) instead; this may be removed in a future release"
404)]
405pub fn run_desktop_app(
406 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
407) -> anyhow::Result<()> {
408 run_desktop_app_with_config(root, AppConfig::default())
409}
410
411#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
413pub fn run_desktop_app_with_config(
414 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
415 config: AppConfig,
416) -> anyhow::Result<()> {
417 use winit::application::ApplicationHandler;
418 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
419 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
420 use winit::event_loop::EventLoop;
421 use winit::keyboard::{KeyCode, PhysicalKey};
422 use winit::window::{Window, WindowAttributes};
423
424 use crate::a11y::A11yTree;
425 use repose_app::ReposeRuntime;
426
427 struct ReposeActivationHandler {
428 initial_tree: Option<accesskit::TreeUpdate>,
429 }
430
431 impl accesskit::ActivationHandler for ReposeActivationHandler {
432 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
433 self.initial_tree.take()
434 }
435 }
436
437 struct ReposeDeactivationHandler;
438
439 impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
440 fn deactivate_accessibility(&mut self) {
441 }
443 }
444
445 struct App {
446 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
447 render: RenderContext,
448 window: Option<Arc<Window>>,
449 backend: Option<repose_render_wgpu::WgpuBackend>,
450 rt: ReposeRuntime,
451 inspector: Option<repose_devtools::Inspector>,
452 msaa_samples: u32,
453 max_fps: Option<f32>,
454 present_mode: PresentModePref,
455 window_title: String,
456 window_size: (u32, u32),
457
458 pending_dropped_files: Vec<std::path::PathBuf>,
460 pending_drop_pos_px: Option<(f32, f32)>,
461
462 external_file_drag: bool,
464 hovered_files: Vec<std::path::PathBuf>,
465
466 clipboard: Option<clipawl::Clipboard>,
467 a11y: Box<dyn A11yBridge>,
468
469 accesskit_adapter: Option<Adapter>,
470 a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
471 a11y_tree: A11yTree,
472
473 last_redraw: Instant,
474 pending_redraw: bool,
475
476 last_window_theme: Option<bool>,
478
479 redraw_requested: Cell<bool>,
481 }
482
483 impl App {
484 fn process_a11y_actions(&mut self) {
485 let mut actions = self.a11y_actions.lock().unwrap();
486 if actions.is_empty() {
487 return;
488 }
489 let pending = actions.drain(..).collect::<Vec<_>>();
490 drop(actions);
491
492 let Some(f) = &self.rt.frame_cache else {
493 return;
494 };
495
496 for req in pending {
497 let target_id = req.target_node.0;
498 match req.action {
499 accesskit::Action::Click => {
500 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
501 && let Some(cb) = &hit.on_click
502 {
503 cb();
504 self.request_redraw();
505 }
506 }
507 accesskit::Action::Focus => {
508 self.rt.sched.focused = Some(target_id);
509 self.request_redraw();
510 }
511 _ => {}
512 }
513 }
514 }
515
516 fn new(
517 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
518 config: AppConfig,
519 ) -> Self {
520 Self {
521 root,
522 render: RenderContext::new(),
523 window: None,
524 backend: None,
525 rt: ReposeRuntime::new(),
526 inspector: if config.enable_inspector {
527 Some(repose_devtools::Inspector::new())
528 } else {
529 None
530 },
531 msaa_samples: config.common.msaa_samples,
532 max_fps: config.common.max_fps,
533 present_mode: config.common.present_mode,
534 window_title: config.window_title,
535 window_size: config.window_size,
536 pending_dropped_files: Vec::new(),
537 pending_drop_pos_px: None,
538
539 external_file_drag: false,
540 hovered_files: Vec::new(),
541
542 clipboard: None,
543 a11y: {
544 #[cfg(target_os = "linux")]
545 {
546 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
547 }
548 #[cfg(not(target_os = "linux"))]
549 {
550 Box::new(NoopA11y) as Box<dyn A11yBridge>
551 }
552 },
553
554 accesskit_adapter: None,
555 a11y_actions: Arc::new(Mutex::new(Vec::new())),
556 a11y_tree: A11yTree::default(),
557
558 last_redraw: Instant::now(),
559 pending_redraw: false,
560 last_window_theme: None,
561 redraw_requested: Cell::new(false),
562 }
563 }
564
565 fn request_redraw(&self) {
566 self.redraw_requested.set(true);
567 repose_core::request_frame();
568 rc::request_redraw(&self.window);
569 }
570
571 fn frame_interval(&self) -> web_time::Duration {
574 match self.max_fps.filter(|f| *f > 0.0) {
575 Some(fps) => {
576 let secs = (1.0 / fps as f64).clamp(0.0, 1.0);
577 web_time::Duration::from_secs_f64(secs)
578 }
579 None => web_time::Duration::ZERO,
580 }
581 }
582
583 fn tf_ensure_caret_visible(st: &mut TextFieldState, is_multiline: bool) {
585 rc::tf_ensure_caret_visible(st, is_multiline);
586 }
587
588 fn paste_from_primary(&self) -> Option<String> {
589 let mut opts = clipawl::ClipboardOptions::default();
590 opts.linux.selection = clipawl::LinuxSelection::Primary;
591 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
592 match pollster::block_on(cb.read()) {
593 Ok(t) => Some(t),
594 Err(e) => {
595 eprintln!("Primary paste error: {}", e);
596 None
597 }
598 }
599 } else {
600 None
601 }
602 }
603
604 fn process_render_commands(&mut self) {
605 let Some(backend) = self.backend.as_mut() else {
606 return;
607 };
608 rc::process_render_commands(backend, self.render.drain());
609 }
610
611 fn reset_pointer_state(&mut self) {
612 self.rt.capture_id = None;
613 self.rt.pressed_ids.clear();
614 self.rt.hover_id = None;
615 }
616
617 fn is_textfield(&self, id: u64) -> bool {
618 rc::is_textfield_in_frame(&self.rt.frame_cache, id)
619 }
620
621 fn is_multiline_id(&self, id: u64) -> bool {
622 if let Some(f) = &self.rt.frame_cache {
623 f.hit_regions
624 .iter()
625 .find(|h| h.id == id)
626 .map(|h| h.tf_multiline)
627 .unwrap_or(false)
628 } else {
629 false
630 }
631 }
632
633 fn hit_by_id(f: &Frame, id: u64) -> Option<&HitRegion> {
634 f.hit_regions.iter().find(|h| h.id == id)
635 }
636
637 fn dp_px(&self, dp: f32) -> f32 {
638 dp_to_px(dp)
639 }
640 }
641
642 impl ApplicationHandler<()> for App {
643 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
644 self.clipboard = clipawl::Clipboard::new()
645 .map_err(|e| {
646 eprintln!("clipawl clipboard init failed: {e}");
647 e
648 })
649 .ok();
650 repose_core::clipboard::set_clipboard_read_fn(Box::new(|| {
651 clipawl::blocking::read().ok()
652 }));
653 repose_core::clipboard::set_clipboard_fn(Box::new(move |text| {
655 if let Err(e) = clipawl::blocking::write(text) {
656 eprintln!("clipboard write error: {e}");
657 }
658 }));
659
660 repose_core::clipboard::set_primary_fn(Box::new(|text| {
661 let mut opts = clipawl::ClipboardOptions::default();
662 opts.linux.selection = clipawl::LinuxSelection::Primary;
663 match clipawl::Clipboard::new_with_options(opts) {
664 Ok(cb) => {
665 if let Err(e) = pollster::block_on(cb.write(text)) {
666 eprintln!("primary selection write error: {e}");
667 }
668 }
669 Err(e) => eprintln!("primary clipboard init error: {e}"),
670 }
671 }));
672
673 if self.window.is_none() {
674 match el.create_window(
675 WindowAttributes::default()
676 .with_title(self.window_title.clone())
677 .with_inner_size(PhysicalSize::new(self.window_size.0, self.window_size.1))
678 .with_visible(false),
679 ) {
680 Ok(win) => {
681 let w = Arc::new(win);
682
683 let activation_handler = ReposeActivationHandler {
684 initial_tree: Some(A11yTree::initial_tree()),
685 };
686
687 let action_handler = ReposeActionHandler {
688 pending_actions: self.a11y_actions.clone(),
689 };
690
691 let deactivation_handler = ReposeDeactivationHandler;
692
693 let adapter = Adapter::with_direct_handlers(
694 el,
695 &w,
696 activation_handler,
697 action_handler,
698 deactivation_handler,
699 );
700
701 self.accesskit_adapter = Some(adapter);
702
703 w.set_visible(true);
704
705 let size = w.inner_size();
706 let sf = w.scale_factor() as f32;
707 self.rt.set_viewport_and_scale(size.width, size.height, sf);
708
709 match repose_render_wgpu::WgpuBackend::new_with_options(
710 w.clone(),
711 self.msaa_samples,
712 self.present_mode,
713 ) {
714 Ok(b) => {
715 self.backend = Some(b);
716 set_app_window(w.clone());
717 self.window = Some(w);
718 self.request_redraw();
719 }
720 Err(e) => {
721 log::error!("Failed to create WGPU backend: {e:?}");
722 el.exit();
723 }
724 }
725 }
726 Err(e) => {
727 log::error!("Failed to create window: {e:?}");
728 el.exit();
729 }
730 }
731 }
732 }
733
734 fn window_event(
735 &mut self,
736 el: &winit::event_loop::ActiveEventLoop,
737 _id: winit::window::WindowId,
738 event: WindowEvent,
739 ) {
740 if let Some(adapter) = &mut self.accesskit_adapter {
742 adapter.process_event(self.window.as_ref().unwrap(), &event);
743 }
744
745 match event {
746 WindowEvent::CloseRequested => {
747 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
748 self.backend = None;
750 if let Some(w) = &self.window {
751 w.set_visible(false);
752 }
753 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
754 } else {
755 el.exit();
756 }
757 }
758
759 WindowEvent::Focused(false) => {
760 self.rt.handle_focus_lost();
762
763 self.external_file_drag = false;
765 self.hovered_files.clear();
766
767 if let Some(w) = &self.window {
768 rc_web::set_ime_for_textfield(w, false);
769 }
770
771 self.request_redraw();
772 }
773
774 WindowEvent::CursorLeft { .. } => {
775 self.rt.pointer_inside = false;
776 self.rt.clear_hover();
777 self.external_file_drag = false;
778 self.hovered_files.clear();
779 self.request_redraw();
780 }
781
782 WindowEvent::HoveredFile(path) => {
783 self.external_file_drag = true;
785 if self.hovered_files.len() < 32 {
786 self.hovered_files.push(path);
787 }
788 if self.pending_drop_pos_px.is_none() {
790 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
791 }
792 self.request_redraw();
793 }
794
795 WindowEvent::HoveredFileCancelled => {
796 self.external_file_drag = false;
797 self.hovered_files.clear();
798
799 self.reset_pointer_state();
801
802 self.request_redraw();
803 }
804
805 WindowEvent::DroppedFile(path) => {
806 self.pending_dropped_files.push(path);
808 if self.pending_drop_pos_px.is_none() {
809 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
810 }
811
812 self.external_file_drag = false;
814 self.hovered_files.clear();
815
816 self.request_redraw();
817 }
818
819 WindowEvent::Resized(size) => {
820 let sf = self
821 .window
822 .as_ref()
823 .map(|w| w.scale_factor() as f32)
824 .unwrap_or(1.0);
825 self.rt.set_viewport_and_scale(size.width, size.height, sf);
826 if let Some(b) = self.backend.as_mut() {
827 b.configure_surface(size.width, size.height);
828 }
829 if let Some(w) = &self.window {
830 let sf = w.scale_factor() as f32;
831 let dp_w = size.width as f32 / sf;
832 let dp_h = size.height as f32 / sf;
833 log::info!(
834 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
835 size.width,
836 size.height,
837 sf,
838 dp_w as i32,
839 dp_h as i32
840 );
841 }
842 self.request_redraw();
843 }
844
845 WindowEvent::CursorMoved { position, .. } => {
846 self.rt.pointer_inside = true;
847
848 if self.external_file_drag {
849 self.pending_drop_pos_px = Some((position.x as f32, position.y as f32));
850 }
851
852 let pos = Vec2 {
853 x: position.x as f32,
854 y: position.y as f32,
855 };
856
857 let result = self.rt.handle_pointer_move(pos);
859
860 if let (Some(inspector), Some(f)) = (&mut self.inspector, &self.rt.frame_cache)
862 && inspector.hud.inspector_enabled
863 {
864 let hit = f.hit_regions.iter().find(|h| h.rect.contains(pos));
865 let hover_rect = hit.map(|h| h.rect);
866 let hover_info = hit.and_then(|h| {
867 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
868 repose_devtools::HoveredInfo {
869 id: s.id,
870 role: format!("{:?}", s.role),
871 label: s.label.clone(),
872 }
873 })
874 });
875 inspector.hud.set_hovered(hover_rect, hover_info);
876 }
877
878 if let Some(win) = &self.window
880 && let Some(c) = result.cursor
881 {
882 win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
883 }
884
885 self.request_redraw();
886 }
887
888 WindowEvent::MouseWheel { delta, .. } => {
889 let (dx_px, dy_px) = match delta {
890 MouseScrollDelta::LineDelta(x, y) => {
891 let unit_px = dp_to_px(60.0);
892 (-(x * unit_px), -(y * unit_px))
893 }
894 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
895 };
896 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
897
898 if self.rt.handle_scroll(Vec2 { x: dx_px, y: dy_px }) {
899 self.request_redraw();
900 }
901 }
902
903 WindowEvent::MouseInput {
904 state: ElementState::Pressed,
905 button: MouseButton::Left,
906 ..
907 } => {
908 let pos = Vec2 {
909 x: self.rt.mouse_pos_px.0,
910 y: self.rt.mouse_pos_px.1,
911 };
912
913 let result = self.rt.handle_pointer_press(pos, PointerButton::Primary);
914
915 if let Some(fid) = result.focused
917 && let Some(win) = &self.window
918 && let Some(f) = &self.rt.frame_cache
919 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
920 {
921 let sf = win.scale_factor();
922 rc_web::set_ime_for_textfield_ex(
923 win,
924 true,
925 hit.keyboard_type.ime_purpose_hint(),
926 hit.auto_correct.unwrap_or(true),
927 hit.capitalization,
928 );
929 win.set_ime_cursor_area(
930 LogicalPosition::new(hit.rect.x as f64 / sf, hit.rect.y as f64 / sf),
931 LogicalSize::new(hit.rect.w as f64 / sf, hit.rect.h as f64 / sf),
932 );
933 }
934
935 if result.focused.is_none() && self.rt.ime_preedit {
937 if let Some(win) = &self.window {
938 rc_web::set_ime_for_textfield(win, false);
939 }
940 self.rt.ime_preedit = false;
941 }
942
943 if result.needs_a11y_announce {
944 self.announce_focus_change();
945 }
946
947 if let Some(inspector) = &mut self.inspector
949 && inspector.hud.inspector_enabled
950 && let Some(f) = &self.rt.frame_cache
951 && let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
952 {
953 let info = f.semantics_nodes.iter().find(|s| s.id == hit.id).map(|s| {
954 repose_devtools::HoveredInfo {
955 id: s.id,
956 role: format!("{:?}", s.role),
957 label: s.label.clone(),
958 }
959 });
960 inspector
961 .hud
962 .select_widget(repose_devtools::SelectedWidget {
963 id: hit.id,
964 role: info.as_ref().map(|i| i.role.clone()).unwrap_or_default(),
965 label: info.as_ref().and_then(|i| i.label.clone()),
966 bounds: hit.rect,
967 });
968 }
969
970 self.request_redraw();
971 }
972
973 WindowEvent::MouseInput {
974 state: ElementState::Pressed,
975 button: MouseButton::Middle,
976 ..
977 } => {
978 let Some(f) = &self.rt.frame_cache else {
979 return;
980 };
981 let pos = Vec2 {
982 x: self.rt.mouse_pos_px.0,
983 y: self.rt.mouse_pos_px.1,
984 };
985 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos)) {
986 if let Some(cb) = &hit.on_pointer_down {
988 cb(PointerEvent::new(
989 PointerId(0),
990 PointerKind::Mouse,
991 PointerEventKind::Down(PointerButton::Tertiary),
992 pos,
993 1.0,
994 self.rt.modifiers,
995 ));
996 }
997 if self.is_textfield(hit.id) {
999 let key = self.tf_key_of(hit.id);
1000 if let Some(state_rc) = self.rt.textfield_states.get(&key)
1001 && let Some(txt) = self.paste_from_primary()
1002 {
1003 let mut st = state_rc.borrow_mut();
1004 st.insert_text_atomic(&txt);
1005 self.notify_text_change(hit.id, st.text.clone());
1006 if let Some(f) = &self.rt.frame_cache
1007 && let Some(h) = f.hit_regions.iter().find(|h| h.id == hit.id)
1008 {
1009 App::tf_ensure_caret_visible(&mut st, h.tf_multiline);
1010 }
1011 }
1012 }
1013 }
1014 self.request_redraw();
1015 }
1016
1017 WindowEvent::MouseInput {
1018 state: ElementState::Released,
1019 button: MouseButton::Left,
1020 ..
1021 } => {
1022 let pos = Vec2 {
1023 x: self.rt.mouse_pos_px.0,
1024 y: self.rt.mouse_pos_px.1,
1025 };
1026
1027 self.rt.handle_pointer_release(pos, PointerButton::Primary);
1028
1029 if let (Some(f), Some(cid)) = (&self.rt.frame_cache, self.rt.capture_id)
1031 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1032 && hit.rect.contains(pos)
1033 && hit.on_click.is_some()
1034 && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid)
1035 {
1036 let label = node.label.as_deref().unwrap_or("");
1037 self.a11y.announce(&format!("Activated {}", label));
1038 }
1039
1040 repose_core::request_frame();
1041 }
1042
1043 WindowEvent::MouseInput {
1044 state: ElementState::Released,
1045 button: MouseButton::Middle,
1046 ..
1047 } => {
1048 if let Some(f) = &self.rt.frame_cache {
1049 let pos = Vec2 {
1050 x: self.rt.mouse_pos_px.0,
1051 y: self.rt.mouse_pos_px.1,
1052 };
1053 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
1054 && let Some(cb) = &hit.on_pointer_up
1055 {
1056 cb(PointerEvent::new(
1057 PointerId(0),
1058 PointerKind::Mouse,
1059 PointerEventKind::Up(PointerButton::Tertiary),
1060 pos,
1061 1.0,
1062 self.rt.modifiers,
1063 ));
1064 }
1065 }
1066 }
1067
1068 WindowEvent::ModifiersChanged(new_mods) => {
1069 let state = new_mods.state();
1070 self.rt.modifiers.shift = state.shift_key();
1071 self.rt.modifiers.ctrl = state.control_key();
1072 self.rt.modifiers.alt = state.alt_key();
1073 self.rt.modifiers.meta = state.super_key();
1074 self.rt.modifiers.command = if cfg!(target_os = "macos") {
1075 self.rt.modifiers.meta
1076 } else {
1077 self.rt.modifiers.ctrl
1078 };
1079 }
1080
1081 WindowEvent::KeyboardInput {
1082 event: key_event, ..
1083 } => {
1084 if key_event.state == ElementState::Pressed && !key_event.repeat {
1088 match key_event.physical_key {
1089 PhysicalKey::Code(KeyCode::BrowserBack)
1090 | PhysicalKey::Code(KeyCode::Escape) => {
1091 use repose_navigation::back;
1092
1093 if repose_core::dnd::handle_drag_action(
1094 &repose_core::shortcuts::DragAction::Cancel,
1095 ) {
1096 return;
1097 }
1098
1099 let mapped = rc::map_key(key_event.physical_key);
1101 if self.dispatch_focus_key_event(&key_event, &mapped) {
1102 self.request_redraw();
1103 return;
1104 }
1105
1106 if !back::handle() {
1107 }
1109 return;
1110 }
1111 _ => {}
1112 }
1113 }
1114
1115 if let Some(inspector) = &mut self.inspector
1117 && key_event.state == ElementState::Pressed
1118 && self.rt.modifiers.ctrl
1119 && self.rt.modifiers.shift
1120 && key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
1121 {
1122 inspector.hud.toggle_inspector();
1123 self.request_redraw();
1124 return;
1125 }
1126
1127 if key_event.state == ElementState::Pressed
1129 && !key_event.repeat
1130 && self.rt.modifiers.command
1131 {
1132 match key_event.physical_key {
1133 PhysicalKey::Code(KeyCode::KeyZ) if self.rt.modifiers.shift => {
1134 if let Some(fid) = self.rt.sched.focused {
1135 let key = self.tf_key_of(fid);
1136 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1137 let mut st = state_rc.borrow_mut();
1138 if st.can_redo() {
1139 st.redo();
1140 self.notify_text_change(fid, st.text.clone());
1141 self.request_redraw();
1142 return;
1143 }
1144 }
1145 }
1146 }
1147 PhysicalKey::Code(KeyCode::KeyZ) => {
1148 if let Some(fid) = self.rt.sched.focused {
1149 let key = self.tf_key_of(fid);
1150 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1151 let mut st = state_rc.borrow_mut();
1152 if st.can_undo() {
1153 st.undo();
1154 self.notify_text_change(fid, st.text.clone());
1155 self.request_redraw();
1156 return;
1157 }
1158 }
1159 }
1160 }
1161 _ => {}
1162 }
1163 }
1164
1165 let mapped = rc::map_key(key_event.physical_key);
1168 let ke = winit_key_to_repose(&key_event, &mapped, &self.rt.modifiers);
1169 let consumed = self.rt.handle_key(&ke);
1170 if consumed {
1171 self.request_redraw();
1172 return;
1173 }
1174
1175 if key_event.state == ElementState::Pressed
1180 && !key_event.repeat
1181 && !self.rt.ime_preedit
1182 && !self.rt.modifiers.ctrl
1183 && !self.rt.modifiers.alt
1184 && !self.rt.modifiers.meta
1185 && let Some(raw) = key_event.text.as_deref()
1186 {
1187 let text: String = raw
1188 .chars()
1189 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
1190 .collect();
1191 if !text.is_empty()
1192 && let Some(fid) = self.rt.sched.focused
1193 {
1194 let key = self.tf_key_of(fid);
1195 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1196 let mut st = state_rc.borrow_mut();
1197 st.insert_text(&text);
1198 self.notify_text_change(fid, st.text.clone());
1199 if let Some(f) = &self.rt.frame_cache
1200 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1201 {
1202 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1203 }
1204 self.request_redraw();
1205 return;
1206 }
1207 }
1208 }
1209
1210 if key_event.state == ElementState::Released
1212 && let Some(active_id) = self.rt.key_pressed_active
1213 {
1214 match key_event.physical_key {
1215 PhysicalKey::Code(KeyCode::Space)
1216 | PhysicalKey::Code(KeyCode::Enter) => {
1217 if let Some(f) = &self.rt.frame_cache
1218 && let Some(node) =
1219 f.semantics_nodes.iter().find(|n| n.id == active_id)
1220 {
1221 let label = node.label.as_deref().unwrap_or("");
1222 self.a11y.announce(&format!("Activated {}", label));
1223 }
1224 }
1225 _ => {}
1226 }
1227 }
1228 }
1229
1230 WindowEvent::Ime(ime) => {
1231 if let Some(focused_id) = self.rt.sched.focused {
1232 let key = self.tf_key_of(focused_id);
1233 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1234 let mut state = state_rc.borrow_mut();
1235 let on_text_change = self
1236 .rt
1237 .frame_cache
1238 .as_ref()
1239 .and_then(|f| f.hit_regions.iter().find(|h| h.id == focused_id))
1240 .and_then(|h| h.on_text_change.clone());
1241 let mut notify = |text: String| {
1242 if let Some(cb) = &on_text_change {
1243 cb(text);
1244 }
1245 };
1246 rc_android::handle_ime_event(
1247 ime,
1248 &mut state,
1249 &mut notify,
1250 &mut self.rt.ime_preedit,
1251 );
1252 self.request_redraw();
1253 }
1254 }
1255 }
1256
1257 WindowEvent::RedrawRequested => {
1258 if !self.redraw_requested.replace(false) {
1260 self.process_a11y_actions();
1261 self.process_render_commands();
1262 if let (Some(backend), Some(frame)) =
1264 (self.backend.as_mut(), self.rt.frame_cache.as_ref())
1265 {
1266 let scale = self
1267 .window
1268 .as_ref()
1269 .map(|w| w.scale_factor() as f32)
1270 .unwrap_or(1.0);
1271 let mut scene = frame.scene.clone();
1272 if let Some(inspector) = &mut self.inspector {
1273 inspector.frame(&mut scene);
1274 }
1275 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1276 }
1277 log::trace!("RedrawRequested: no frame request, skipping compose");
1278 return;
1279 }
1280 log::trace!("RedrawRequested: frame request pending, composing");
1281
1282 self.process_a11y_actions();
1284 self.process_render_commands();
1285
1286 let Some(win) = self.window.as_ref() else {
1287 return;
1288 };
1289 if self.backend.is_none() {
1290 return;
1291 }
1292
1293 repose_core::animation_driver::tick();
1296
1297 let t0 = Instant::now();
1298 let scale = win.scale_factor() as f32;
1299 self.rt.scale = scale;
1300 let focused = self.rt.sched.focused;
1301
1302 let output = self.rt.frame(&mut self.root, &self.render);
1303
1304 if let Some(cursor) = &output.platform.cursor {
1306 win.set_cursor(winit::window::Cursor::Icon(map_cursor(*cursor)));
1307 }
1308
1309 if let Some(dark) = output.platform.window_theme_dark
1311 && self.last_window_theme != Some(dark)
1312 {
1313 win.set_theme(Some(if dark {
1314 winit::window::Theme::Dark
1315 } else {
1316 winit::window::Theme::Light
1317 }));
1318 self.last_window_theme = Some(dark);
1319 }
1320
1321 if output.platform.ime_allowed {
1323 rc_web::set_ime_for_textfield_ex(
1324 win,
1325 true,
1326 output.platform.ime_purpose,
1327 output.platform.ime_auto_correct,
1328 output.platform.ime_capitalization,
1329 );
1330 if let Some((x, y, w, h)) = output.platform.ime_cursor_area {
1331 win.set_ime_cursor_area(
1332 LogicalPosition::new(x, y),
1333 LogicalSize::new(w, h),
1334 );
1335 }
1336 } else if self.rt.ime_preedit {
1337 rc_web::set_ime_for_textfield_ex(
1338 win,
1339 false,
1340 repose_core::ImePurposeHint::Normal,
1341 true,
1342 repose_core::KeyboardCapitalization::Unspecified,
1343 );
1344 self.rt.ime_preedit = false;
1345 }
1346
1347 if !output.wants_keyboard
1349 && focused.is_some()
1350 && self.rt.sched.focused.is_none()
1351 && self.rt.ime_preedit
1352 {
1353 rc_web::set_ime_for_textfield(win, false);
1354 self.rt.ime_preedit = false;
1355 }
1356
1357 let frame = Frame {
1358 scene: output.scene,
1359 hit_regions: output.hit_regions,
1360 semantics_nodes: output.semantics_nodes,
1361 focus_chain: output.focus_chain,
1362 };
1363
1364 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1365
1366 if let Some(adapter) = &mut self.accesskit_adapter {
1368 let win = self.window.as_ref().unwrap();
1369 let scale = win.scale_factor();
1370 if let Some(update) = self.a11y_tree.update(
1371 &frame.semantics_nodes,
1372 scale,
1373 self.rt.sched.focused,
1374 ) {
1375 adapter.update_if_active(|| update);
1376 }
1377 }
1378
1379 let mut scene = frame.scene.clone();
1381 if let Some(inspector) = &mut self.inspector {
1383 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1384 let signal_count = self.rt.sched.id_count() as usize;
1385 let ls = repose_ui::last_layout_stats();
1386 inspector.hud.metrics = Some(repose_devtools::Metrics {
1387 build_ms: build_layout_ms,
1388 layout_ms: ls.layout_time_ms,
1389 paint_ms: ls.paint_time_ms,
1390 scene_nodes: scene.nodes.len(),
1391 widget_count,
1392 signal_count,
1393 taffy_created: ls.taffy_created,
1394 taffy_reused: ls.taffy_reused,
1395 layout_hits: ls.layout_hits,
1396 layout_misses: ls.layout_misses,
1397 paint_cache_hits: ls.paint_cache_hits,
1398 paint_cache_misses: ls.paint_cache_misses,
1399 paint_culled: ls.paint_culled,
1400 });
1401 inspector.frame(&mut scene);
1402 }
1403
1404 repose_core::dnd::overlay_drag_indicator(
1406 &mut scene,
1407 self.rt.mouse_pos_px,
1408 self.external_file_drag,
1409 );
1410
1411 self.process_render_commands();
1414
1415 let win = self.window.as_ref().unwrap();
1417 let scale = win.scale_factor() as f32;
1418 if let Some(backend) = self.backend.as_mut() {
1419 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1420 }
1421
1422 if let Some(fid) = self.rt.sched.focused
1425 && let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
1426 && let Some(key) = hit.tf_state_key
1427 && !self.rt.textfield_states.contains_key(&key)
1428 {
1429 self.rt
1430 .textfield_states
1431 .entry(key)
1432 .or_insert_with(|| {
1433 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1434 })
1435 .borrow_mut()
1436 .reset_caret_blink();
1437 }
1438
1439 self.rt.reconcile_hover_from_mouse_pos(&frame);
1440 repose_core::dnd::set_dnd_frame(Some(frame.clone()));
1441 self.rt.frame_cache = Some(frame);
1442 repose_core::dnd::set_dnd_scale(scale);
1443
1444 self.dispatch_file_drop_now();
1445
1446 rc::tick_snackbar(self.last_redraw);
1447 self.last_redraw = Instant::now();
1448 }
1449
1450 _ => {}
1451 }
1452 }
1453
1454 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1455 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1458 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK.lock().unwrap().as_ref() {
1459 cb();
1460 }
1461 process_deeplinks();
1462
1463 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1466 if WINDOW_VISIBLE.load(Ordering::Relaxed)
1467 && self.backend.is_none()
1468 && let Some(w) = &self.window
1469 {
1470 log::info!("about_to_wait: recreating GPU backend");
1471 match repose_render_wgpu::WgpuBackend::new_with_options(
1472 w.clone(),
1473 self.msaa_samples,
1474 self.present_mode,
1475 ) {
1476 Ok(b) => self.backend = Some(b),
1477 Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
1478 }
1479 }
1480
1481 let needs_compose = take_frame_request();
1482 let needs_present = take_present_request();
1483
1484 if needs_compose {
1485 self.pending_redraw = true;
1486 }
1487
1488 if !self.pending_redraw && needs_present && self.rt.frame_cache.is_some() {
1490 let now = Instant::now();
1491 let interval = self.frame_interval();
1492 if now.saturating_duration_since(self.last_redraw) >= interval {
1493 rc::request_redraw(&self.window);
1494 self.last_redraw = now;
1495 } else {
1496 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1497 self.last_redraw + interval,
1498 ));
1499 }
1500 return;
1501 }
1502
1503 if !self.pending_redraw {
1504 let now = Instant::now();
1505 let idle_cap = web_time::Duration::from_millis(1000);
1506 let deadline = self.next_caret_blink_deadline().unwrap_or(now + idle_cap);
1507
1508 if now.saturating_duration_since(self.last_redraw) >= idle_cap || now >= deadline {
1509 self.redraw_requested.set(true);
1510 request_frame();
1511 rc::request_redraw(&self.window);
1512 self.last_redraw = now;
1513 }
1514 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(Ord::min(
1515 deadline,
1516 now + idle_cap,
1517 )));
1518 return;
1519 }
1520
1521 let now = Instant::now();
1522 let interval = self.frame_interval();
1523
1524 if now.saturating_duration_since(self.last_redraw) >= interval {
1525 self.pending_redraw = false;
1526 self.redraw_requested.set(true);
1527 rc::request_redraw(&self.window);
1528 self.last_redraw = now;
1529 } else {
1530 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1531 self.last_redraw + interval,
1532 ));
1533 }
1534 }
1535
1536 fn new_events(
1537 &mut self,
1538 _: &winit::event_loop::ActiveEventLoop,
1539 _: winit::event::StartCause,
1540 ) {
1541 }
1542 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1543 self.pending_redraw = true;
1544 }
1545 fn device_event(
1546 &mut self,
1547 _: &winit::event_loop::ActiveEventLoop,
1548 _: winit::event::DeviceId,
1549 _: winit::event::DeviceEvent,
1550 ) {
1551 }
1552 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1553 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1554 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1555 }
1556
1557 impl App {
1558 fn dispatch_focus_key_event(
1561 &self,
1562 key_event: &winit::event::KeyEvent,
1563 mapped_key: &repose_core::input::Key,
1564 ) -> bool {
1565 let Some(f) = &self.rt.frame_cache else {
1566 return false;
1567 };
1568 let Some(focused) = self.rt.sched.focused else {
1569 return false;
1570 };
1571 let utf16 = match mapped_key {
1572 repose_core::input::Key::Character(c) => *c as u16,
1573 _ => 0,
1574 };
1575 let mods = self.rt.modifiers;
1576 let repeat = key_event.repeat;
1577 let ev_type = if key_event.state == ElementState::Pressed {
1578 repose_core::input::KeyEventType::Down
1579 } else {
1580 repose_core::input::KeyEventType::Up
1581 };
1582 let hit_by_id: std::collections::HashMap<u64, &HitRegion> =
1583 f.hit_regions.iter().map(|h| (h.id, h)).collect();
1584 let sem_parent_of: std::collections::HashMap<u64, u64> = f
1585 .semantics_nodes
1586 .iter()
1587 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1588 .collect();
1589 let mut ancestors = Vec::new();
1590 let mut cur = focused;
1591 loop {
1592 ancestors.push(cur);
1593 if let Some(&p) = sem_parent_of.get(&cur) {
1594 cur = p;
1595 } else {
1596 break;
1597 }
1598 }
1599 let make_ke = || repose_core::input::KeyEvent {
1600 key: mapped_key.clone(),
1601 modifiers: mods,
1602 is_repeat: repeat,
1603 event_type: ev_type,
1604 utf16_code_point: utf16,
1605 };
1606 for &id in ancestors.iter().rev() {
1608 if let Some(hit) = hit_by_id.get(&id)
1609 && let Some(cb) = &hit.on_preview_key_event
1610 && cb(make_ke())
1611 {
1612 return true;
1613 }
1614 }
1615 for &id in ancestors.iter() {
1617 if let Some(hit) = hit_by_id.get(&id)
1618 && let Some(cb) = &hit.on_key_event
1619 && cb(make_ke())
1620 {
1621 return true;
1622 }
1623 }
1624 false
1625 }
1626
1627 fn announce_focus_change(&mut self) {
1628 if let Some(f) = &self.rt.frame_cache {
1629 let focused_node = self
1630 .rt
1631 .sched
1632 .focused
1633 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1634 self.a11y.focus_changed(focused_node);
1635 }
1636 }
1637
1638 fn notify_text_change(&self, id: u64, text: String) {
1639 if let Some(f) = &self.rt.frame_cache
1640 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1641 && let Some(cb) = &h.on_text_change
1642 {
1643 cb(text);
1644 }
1645 }
1646
1647 fn tf_key_of(&self, visual_id: u64) -> u64 {
1648 rc::tf_key_of_in_frame(&self.rt.frame_cache, visual_id)
1649 }
1650
1651 fn next_caret_blink_deadline(&self) -> Option<Instant> {
1654 next_caret_blink_deadline(
1655 &self.rt.sched,
1656 &self.rt.frame_cache,
1657 &self.rt.textfield_states,
1658 )
1659 }
1660
1661 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1662 use repose_core::shortcuts;
1663
1664 if let (Some(f), Some(fid)) = (&self.rt.frame_cache, self.rt.sched.focused)
1665 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1666 && let Some(cb) = &hit.on_action
1667 && cb(action.clone())
1668 {
1669 return true;
1670 }
1671
1672 if shortcuts::handle(action.clone()) {
1673 return true;
1674 }
1675
1676 if let Some(f) = &self.rt.frame_cache
1678 && let Some(new_id) =
1679 repose_core::focus::handle_action(&action, &mut self.rt.sched, f)
1680 {
1681 if let Some(active) = self.rt.key_pressed_active.take() {
1682 self.rt.pressed_ids.remove(&active);
1683 }
1684 let tf_state_key = f
1685 .hit_regions
1686 .iter()
1687 .find(|h| h.id == new_id)
1688 .and_then(|h| h.tf_state_key);
1689 if let Some(key) = tf_state_key {
1690 self.rt
1691 .textfield_states
1692 .entry(key)
1693 .or_insert_with(|| Rc::new(RefCell::new(repose_ui::TextFieldState::new())));
1694 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1695 state_rc.borrow_mut().reset_caret_blink();
1696 }
1697 }
1698 if let Some(win) = &self.window {
1699 let is_textfield = f.semantics_nodes.iter().any(|n| {
1700 n.id == new_id && n.role == repose_core::semantics::Role::TextField
1701 });
1702 rc_web::set_ime_for_textfield(win, is_textfield);
1703 }
1704 self.announce_focus_change();
1705 return true;
1706 }
1707
1708 false
1709 }
1710
1711 fn dispatch_file_drop_now(&mut self) {
1712 let Some(f) = &self.rt.frame_cache else {
1713 self.pending_dropped_files.clear();
1714 self.pending_drop_pos_px = None;
1715 return;
1716 };
1717
1718 if self.pending_dropped_files.is_empty() {
1719 return;
1720 }
1721
1722 let pos_px = self.pending_drop_pos_px.unwrap_or(self.rt.mouse_pos_px);
1723 let pos = Vec2 {
1724 x: pos_px.0,
1725 y: pos_px.1,
1726 };
1727
1728 let mut files = Vec::new();
1729 for p in self.pending_dropped_files.drain(..) {
1730 let name = p
1731 .file_name()
1732 .and_then(|s| s.to_str())
1733 .unwrap_or("file")
1734 .to_string();
1735 files.push(repose_core::dnd::DroppedFile {
1736 name,
1737 path: Some(p),
1738 });
1739 }
1740
1741 let payload: repose_core::dnd::DragPayload =
1742 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1743
1744 let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
1745 self.pending_drop_pos_px = None;
1746 return;
1747 };
1748
1749 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
1750 && let Some(cb) = &hit.on_drop
1751 {
1752 let accepted = cb(repose_core::dnd::DropEvent {
1753 source_id: 0, target_id,
1755 position: pos,
1756 modifiers: self.rt.modifiers,
1757 payload: payload.clone(),
1758 });
1759
1760 if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
1761 {
1762 let label = node.label.as_deref().unwrap_or("");
1763 self.a11y.announce(&format!("Dropped files on {}", label));
1764 }
1765 }
1766
1767 self.pending_drop_pos_px = None;
1768 self.request_redraw();
1769 }
1770 }
1771
1772 let event_loop = EventLoop::new()?;
1773 set_event_loop_proxy(event_loop.create_proxy());
1774 let mut app = App::new(Box::new(root), config);
1775 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1777 event_loop.run_app(&mut app)?;
1778 Ok(())
1779}
1780
1781pub trait A11yBridge: Send {
1789 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1791
1792 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1794
1795 fn announce(&mut self, msg: &str);
1797}
1798
1799struct NoopA11y;
1800impl A11yBridge for NoopA11y {
1801 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1802 }
1804 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1805 if let Some(n) = node {
1806 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1807 } else {
1808 log::info!("A11y focus: None");
1809 }
1810 }
1811 fn announce(&mut self, msg: &str) {
1812 log::info!("A11y announce: {msg}");
1813 }
1814}
1815
1816#[cfg(target_os = "linux")]
1817struct LinuxAtspiStub;
1818#[cfg(target_os = "linux")]
1819impl A11yBridge for LinuxAtspiStub {
1820 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1821 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1822 }
1823 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1824 if let Some(n) = node {
1825 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1826 } else {
1827 log::info!("AT-SPI stub focus: None");
1828 }
1829 }
1830 fn announce(&mut self, msg: &str) {
1831 log::info!("AT-SPI stub announce: {msg}");
1832 }
1833}