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 repose_app::compose_frame_inner(
235 sched,
236 root_fn,
237 scale,
238 size_px_u32,
239 hover_id,
240 pressed_ids,
241 tf_states,
242 )
243}
244
245pub(crate) fn next_caret_blink_deadline(
246 sched: &Scheduler,
247 frame_cache: &Option<Frame>,
248 textfield_states: &std::collections::HashMap<u64, Rc<RefCell<TextFieldState>>>,
249) -> Option<Instant> {
250 let fid = sched.focused?;
251 let frame = frame_cache.as_ref()?;
252 let hit = frame.hit_regions.iter().find(|h| h.id == fid)?;
253 let key = hit.tf_state_key?;
254 textfield_states.get(&key)?.borrow().next_blink_deadline()
255}
256
257pub fn tf_ensure_visible_in_rect(state: &mut repose_ui::TextFieldState, inner_rect: Rect) {
259 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
260 let m = measure_text(&state.text, font_px, TextMeasureConfig::default());
261 let caret_x_px = m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
262 state.ensure_caret_visible(
263 caret_x_px,
264 inner_rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
265 dp_to_px(2.0),
266 );
267}
268
269fn winit_key_to_repose(
271 ev: &winit::event::KeyEvent,
272 mapped_key: &repose_core::input::Key,
273 mods: &repose_core::input::Modifiers,
274) -> repose_core::input::KeyEvent {
275 let utf16 = match mapped_key {
276 repose_core::input::Key::Character(c) => *c as u16,
277 _ => 0,
278 };
279 repose_core::input::KeyEvent {
280 key: mapped_key.clone(),
281 modifiers: *mods,
282 is_repeat: ev.repeat,
283 event_type: if ev.state == winit::event::ElementState::Pressed {
284 repose_core::input::KeyEventType::Down
285 } else {
286 repose_core::input::KeyEventType::Up
287 },
288 utf16_code_point: utf16,
289 }
290}
291
292#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
293fn map_cursor(c: repose_core::CursorIcon) -> winit::window::CursorIcon {
294 use winit::window::CursorIcon as W;
295 match c {
296 repose_core::CursorIcon::Default => W::Default,
297 repose_core::CursorIcon::Pointer => W::Pointer,
298 repose_core::CursorIcon::Text => W::Text,
299 repose_core::CursorIcon::EwResize => W::EwResize,
300 repose_core::CursorIcon::NsResize => W::NsResize,
301 repose_core::CursorIcon::Grab => W::Grab,
302 repose_core::CursorIcon::Grabbing => W::Grabbing,
303 }
304}
305
306#[derive(Clone, Copy, Debug)]
308pub struct ReposeOptions {
309 pub msaa_samples: u32,
312 pub max_fps: Option<f32>,
316 pub present_mode: PresentModePref,
318}
319
320impl Default for ReposeOptions {
321 fn default() -> Self {
322 Self {
323 msaa_samples: 4,
324 max_fps: None,
325 present_mode: PresentModePref::Auto,
326 }
327 }
328}
329
330#[derive(Clone, Debug)]
336pub struct AppConfig {
337 pub common: ReposeOptions,
339 pub window_title: String,
341 pub window_size: (u32, u32),
343 pub enable_inspector: bool,
345}
346
347impl Default for AppConfig {
348 fn default() -> Self {
349 Self {
350 common: ReposeOptions::default(),
351 window_title: "Repose".to_string(),
352 window_size: (1280, 800),
353 enable_inspector: true,
354 }
355 }
356}
357
358#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
363#[deprecated(
364 note = "use run_desktop_app_with_config(root, AppConfig) instead; this may be removed in a future release"
365)]
366pub fn run_desktop_app(
367 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
368) -> anyhow::Result<()> {
369 run_desktop_app_with_config(root, AppConfig::default())
370}
371
372#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
374pub fn run_desktop_app_with_config(
375 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
376 config: AppConfig,
377) -> anyhow::Result<()> {
378 use winit::application::ApplicationHandler;
379 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
380 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
381 use winit::event_loop::EventLoop;
382 use winit::keyboard::{KeyCode, PhysicalKey};
383 use winit::window::{Window, WindowAttributes};
384
385 use crate::a11y::A11yTree;
386 use repose_app::ReposeRuntime;
387
388 struct ReposeActivationHandler {
389 initial_tree: Option<accesskit::TreeUpdate>,
390 }
391
392 impl accesskit::ActivationHandler for ReposeActivationHandler {
393 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
394 self.initial_tree.take()
395 }
396 }
397
398 struct ReposeDeactivationHandler;
399
400 impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
401 fn deactivate_accessibility(&mut self) {
402 }
404 }
405
406 struct App {
407 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
408 render: RenderContext,
409 window: Option<Arc<Window>>,
410 backend: Option<repose_render_wgpu::WgpuBackend>,
411 rt: ReposeRuntime,
412 inspector: Option<repose_devtools::Inspector>,
413 msaa_samples: u32,
414 max_fps: Option<f32>,
415 present_mode: PresentModePref,
416 window_title: String,
417 window_size: (u32, u32),
418
419 pending_dropped_files: Vec<std::path::PathBuf>,
421 pending_drop_pos_px: Option<(f32, f32)>,
422
423 external_file_drag: bool,
425 hovered_files: Vec<std::path::PathBuf>,
426
427 clipboard: Option<clipawl::Clipboard>,
428 a11y: Box<dyn A11yBridge>,
429
430 accesskit_adapter: Option<Adapter>,
431 a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
432 a11y_tree: A11yTree,
433
434 last_redraw: Instant,
435 pending_redraw: bool,
436
437 last_window_theme: Option<bool>,
439
440 redraw_requested: Cell<bool>,
442 }
443
444 impl App {
445 fn process_a11y_actions(&mut self) {
446 let mut actions = self.a11y_actions.lock().unwrap();
447 if actions.is_empty() {
448 return;
449 }
450 let pending = actions.drain(..).collect::<Vec<_>>();
451 drop(actions);
452
453 let Some(f) = &self.rt.frame_cache else {
454 return;
455 };
456
457 for req in pending {
458 let target_id = req.target_node.0;
459 match req.action {
460 accesskit::Action::Click => {
461 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
462 && let Some(cb) = &hit.on_click
463 {
464 cb();
465 self.request_redraw();
466 }
467 }
468 accesskit::Action::Focus => {
469 self.rt.sched.focused = Some(target_id);
470 self.request_redraw();
471 }
472 _ => {}
473 }
474 }
475 }
476
477 fn new(
478 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
479 config: AppConfig,
480 ) -> Self {
481 Self {
482 root,
483 render: RenderContext::new(),
484 window: None,
485 backend: None,
486 rt: ReposeRuntime::new(),
487 inspector: if config.enable_inspector {
488 Some(repose_devtools::Inspector::new())
489 } else {
490 None
491 },
492 msaa_samples: config.common.msaa_samples,
493 max_fps: config.common.max_fps,
494 present_mode: config.common.present_mode,
495 window_title: config.window_title,
496 window_size: config.window_size,
497 pending_dropped_files: Vec::new(),
498 pending_drop_pos_px: None,
499
500 external_file_drag: false,
501 hovered_files: Vec::new(),
502
503 clipboard: None,
504 a11y: {
505 #[cfg(target_os = "linux")]
506 {
507 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
508 }
509 #[cfg(not(target_os = "linux"))]
510 {
511 Box::new(NoopA11y) as Box<dyn A11yBridge>
512 }
513 },
514
515 accesskit_adapter: None,
516 a11y_actions: Arc::new(Mutex::new(Vec::new())),
517 a11y_tree: A11yTree::default(),
518
519 last_redraw: Instant::now(),
520 pending_redraw: false,
521 last_window_theme: None,
522 redraw_requested: Cell::new(false),
523 }
524 }
525
526 fn request_redraw(&self) {
527 self.redraw_requested.set(true);
528 repose_core::request_frame();
529 rc::request_redraw(&self.window);
530 }
531
532 fn frame_interval(&self) -> web_time::Duration {
535 match self.max_fps.filter(|f| *f > 0.0) {
536 Some(fps) => {
537 let secs = (1.0 / fps as f64).clamp(0.0, 1.0);
538 web_time::Duration::from_secs_f64(secs)
539 }
540 None => web_time::Duration::ZERO,
541 }
542 }
543
544 fn tf_ensure_caret_visible(st: &mut TextFieldState, is_multiline: bool) {
546 rc::tf_ensure_caret_visible(st, is_multiline);
547 }
548
549 fn paste_from_primary(&self) -> Option<String> {
550 let mut opts = clipawl::ClipboardOptions::default();
551 opts.linux.selection = clipawl::LinuxSelection::Primary;
552 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
553 match pollster::block_on(cb.read()) {
554 Ok(t) => Some(t),
555 Err(e) => {
556 eprintln!("Primary paste error: {}", e);
557 None
558 }
559 }
560 } else {
561 None
562 }
563 }
564
565 fn process_render_commands(&mut self) {
566 let Some(backend) = self.backend.as_mut() else {
567 return;
568 };
569 rc::process_render_commands(backend, self.render.drain());
570 }
571
572 fn reset_pointer_state(&mut self) {
573 self.rt.capture_id = None;
574 self.rt.pressed_ids.clear();
575 self.rt.hover_id = None;
576 }
577
578 fn is_textfield(&self, id: u64) -> bool {
579 rc::is_textfield_in_frame(&self.rt.frame_cache, id)
580 }
581
582 fn is_multiline_id(&self, id: u64) -> bool {
583 if let Some(f) = &self.rt.frame_cache {
584 f.hit_regions
585 .iter()
586 .find(|h| h.id == id)
587 .map(|h| h.tf_multiline)
588 .unwrap_or(false)
589 } else {
590 false
591 }
592 }
593
594 fn hit_by_id(f: &Frame, id: u64) -> Option<&HitRegion> {
595 f.hit_regions.iter().find(|h| h.id == id)
596 }
597
598 fn dp_px(&self, dp: f32) -> f32 {
599 dp_to_px(dp)
600 }
601 }
602
603 impl ApplicationHandler<()> for App {
604 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
605 self.clipboard = clipawl::Clipboard::new()
606 .map_err(|e| {
607 eprintln!("clipawl clipboard init failed: {e}");
608 e
609 })
610 .ok();
611 repose_core::clipboard::set_clipboard_read_fn(Box::new(|| {
612 clipawl::blocking::read().ok()
613 }));
614 repose_core::clipboard::set_clipboard_fn(Box::new(move |text| {
616 if let Err(e) = clipawl::blocking::write(text) {
617 eprintln!("clipboard write error: {e}");
618 }
619 }));
620
621 repose_core::clipboard::set_primary_fn(Box::new(|text| {
622 let mut opts = clipawl::ClipboardOptions::default();
623 opts.linux.selection = clipawl::LinuxSelection::Primary;
624 match clipawl::Clipboard::new_with_options(opts) {
625 Ok(cb) => {
626 if let Err(e) = pollster::block_on(cb.write(text)) {
627 eprintln!("primary selection write error: {e}");
628 }
629 }
630 Err(e) => eprintln!("primary clipboard init error: {e}"),
631 }
632 }));
633
634 if self.window.is_none() {
635 match el.create_window(
636 WindowAttributes::default()
637 .with_title(self.window_title.clone())
638 .with_inner_size(PhysicalSize::new(self.window_size.0, self.window_size.1))
639 .with_visible(false),
640 ) {
641 Ok(win) => {
642 let w = Arc::new(win);
643
644 let activation_handler = ReposeActivationHandler {
645 initial_tree: Some(A11yTree::initial_tree()),
646 };
647
648 let action_handler = ReposeActionHandler {
649 pending_actions: self.a11y_actions.clone(),
650 };
651
652 let deactivation_handler = ReposeDeactivationHandler;
653
654 let adapter = Adapter::with_direct_handlers(
655 el,
656 &w,
657 activation_handler,
658 action_handler,
659 deactivation_handler,
660 );
661
662 self.accesskit_adapter = Some(adapter);
663
664 w.set_visible(true);
665
666 let size = w.inner_size();
667 let sf = w.scale_factor() as f32;
668 self.rt.set_viewport_and_scale(size.width, size.height, sf);
669
670 match repose_render_wgpu::WgpuBackend::new_with_options(
671 w.clone(),
672 self.msaa_samples,
673 self.present_mode,
674 ) {
675 Ok(b) => {
676 self.backend = Some(b);
677 set_app_window(w.clone());
678 self.window = Some(w);
679 self.request_redraw();
680 }
681 Err(e) => {
682 log::error!("Failed to create WGPU backend: {e:?}");
683 el.exit();
684 }
685 }
686 }
687 Err(e) => {
688 log::error!("Failed to create window: {e:?}");
689 el.exit();
690 }
691 }
692 }
693 }
694
695 fn window_event(
696 &mut self,
697 el: &winit::event_loop::ActiveEventLoop,
698 _id: winit::window::WindowId,
699 event: WindowEvent,
700 ) {
701 if let Some(adapter) = &mut self.accesskit_adapter {
703 adapter.process_event(self.window.as_ref().unwrap(), &event);
704 }
705
706 match event {
707 WindowEvent::CloseRequested => {
708 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
709 self.backend = None;
711 if let Some(w) = &self.window {
712 w.set_visible(false);
713 }
714 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
715 } else {
716 el.exit();
717 }
718 }
719
720 WindowEvent::Focused(false) => {
721 self.rt.handle_focus_lost();
723
724 self.external_file_drag = false;
726 self.hovered_files.clear();
727
728 if let Some(w) = &self.window {
729 rc_web::set_ime_for_textfield(w, false);
730 }
731
732 self.request_redraw();
733 }
734
735 WindowEvent::CursorLeft { .. } => {
736 self.rt.pointer_inside = false;
737 self.rt.clear_hover();
738 self.external_file_drag = false;
739 self.hovered_files.clear();
740 self.request_redraw();
741 }
742
743 WindowEvent::HoveredFile(path) => {
744 self.external_file_drag = true;
746 if self.hovered_files.len() < 32 {
747 self.hovered_files.push(path);
748 }
749 if self.pending_drop_pos_px.is_none() {
751 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
752 }
753 self.request_redraw();
754 }
755
756 WindowEvent::HoveredFileCancelled => {
757 self.external_file_drag = false;
758 self.hovered_files.clear();
759
760 self.reset_pointer_state();
762
763 self.request_redraw();
764 }
765
766 WindowEvent::DroppedFile(path) => {
767 self.pending_dropped_files.push(path);
769 if self.pending_drop_pos_px.is_none() {
770 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
771 }
772
773 self.external_file_drag = false;
775 self.hovered_files.clear();
776
777 self.request_redraw();
778 }
779
780 WindowEvent::Resized(size) => {
781 let sf = self
782 .window
783 .as_ref()
784 .map(|w| w.scale_factor() as f32)
785 .unwrap_or(1.0);
786 self.rt.set_viewport_and_scale(size.width, size.height, sf);
787 if let Some(b) = self.backend.as_mut() {
788 b.configure_surface(size.width, size.height);
789 }
790 if let Some(w) = &self.window {
791 let sf = w.scale_factor() as f32;
792 let dp_w = size.width as f32 / sf;
793 let dp_h = size.height as f32 / sf;
794 log::info!(
795 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
796 size.width,
797 size.height,
798 sf,
799 dp_w as i32,
800 dp_h as i32
801 );
802 }
803 self.request_redraw();
804 }
805
806 WindowEvent::CursorMoved { position, .. } => {
807 self.rt.pointer_inside = true;
808
809 if self.external_file_drag {
810 self.pending_drop_pos_px = Some((position.x as f32, position.y as f32));
811 }
812
813 let pos = Vec2 {
814 x: position.x as f32,
815 y: position.y as f32,
816 };
817
818 let result = self.rt.handle_pointer_move(pos);
820
821 if let (Some(inspector), Some(f)) = (&mut self.inspector, &self.rt.frame_cache)
823 && inspector.hud.inspector_enabled
824 {
825 let hit = f.hit_regions.iter().find(|h| h.rect.contains(pos));
826 let hover_rect = hit.map(|h| h.rect);
827 let hover_info = hit.and_then(|h| {
828 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
829 repose_devtools::HoveredInfo {
830 id: s.id,
831 role: format!("{:?}", s.role),
832 label: s.label.clone(),
833 }
834 })
835 });
836 inspector.hud.set_hovered(hover_rect, hover_info);
837 }
838
839 if let Some(win) = &self.window
841 && let Some(c) = result.cursor
842 {
843 win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
844 }
845
846 self.request_redraw();
847 }
848
849 WindowEvent::MouseWheel { delta, .. } => {
850 let (dx_px, dy_px) = match delta {
851 MouseScrollDelta::LineDelta(x, y) => {
852 let unit_px = dp_to_px(60.0);
853 (-(x * unit_px), -(y * unit_px))
854 }
855 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
856 };
857 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
858
859 if self.rt.handle_scroll(Vec2 { x: dx_px, y: dy_px }) {
860 self.request_redraw();
861 }
862 }
863
864 WindowEvent::MouseInput { state, button, .. } => {
865 let pos = Vec2 {
866 x: self.rt.mouse_pos_px.0,
867 y: self.rt.mouse_pos_px.1,
868 };
869
870 let mapped = match button {
871 MouseButton::Left => PointerButton::Primary,
872 MouseButton::Right => PointerButton::Secondary,
873 MouseButton::Middle => PointerButton::Tertiary,
874 _ => return,
876 };
877
878 match state {
879 ElementState::Pressed => {
880 let result = self.rt.handle_pointer_press(pos, mapped);
881
882 if let Some(fid) = result.focused
884 && let Some(win) = &self.window
885 && let Some(f) = &self.rt.frame_cache
886 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
887 {
888 let sf = win.scale_factor();
889 rc_web::set_ime_for_textfield_ex(
890 win,
891 true,
892 hit.keyboard_type.ime_purpose_hint(),
893 hit.auto_correct.unwrap_or(true),
894 hit.capitalization,
895 );
896 win.set_ime_cursor_area(
897 LogicalPosition::new(
898 hit.rect.x as f64 / sf,
899 hit.rect.y as f64 / sf,
900 ),
901 LogicalSize::new(
902 hit.rect.w as f64 / sf,
903 hit.rect.h as f64 / sf,
904 ),
905 );
906 }
907
908 if result.focused.is_none() && self.rt.ime_preedit {
910 if let Some(win) = &self.window {
911 rc_web::set_ime_for_textfield(win, false);
912 }
913 self.rt.ime_preedit = false;
914 }
915
916 if result.needs_a11y_announce {
917 self.announce_focus_change();
918 }
919
920 if matches!(mapped, PointerButton::Tertiary)
922 && let Some(f) = &self.rt.frame_cache
923 && let Some(cid) = self.rt.capture_id
924 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
925 && self.is_textfield(hit.id)
926 {
927 let key = self.tf_key_of(hit.id);
928 if let Some(state_rc) = self.rt.textfield_states.get(&key)
929 && let Some(txt) = self.paste_from_primary()
930 {
931 let mut st = state_rc.borrow_mut();
932 st.insert_text_atomic(&txt);
933 self.notify_text_change(hit.id, st.text.clone());
934 if let Some(f) = &self.rt.frame_cache
935 && let Some(h) =
936 f.hit_regions.iter().find(|h| h.id == hit.id)
937 {
938 App::tf_ensure_caret_visible(&mut st, h.tf_multiline);
939 }
940 }
941 }
942
943 if matches!(mapped, PointerButton::Primary | PointerButton::Secondary)
945 && let Some(inspector) = &mut self.inspector
946 && inspector.hud.inspector_enabled
947 && let Some(f) = &self.rt.frame_cache
948 && let Some(hit) =
949 f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
950 {
951 let info =
952 f.semantics_nodes.iter().find(|s| s.id == hit.id).map(|s| {
953 repose_devtools::HoveredInfo {
954 id: s.id,
955 role: format!("{:?}", s.role),
956 label: s.label.clone(),
957 }
958 });
959 inspector
960 .hud
961 .select_widget(repose_devtools::SelectedWidget {
962 id: hit.id,
963 role: info
964 .as_ref()
965 .map(|i| i.role.clone())
966 .unwrap_or_default(),
967 label: info.as_ref().and_then(|i| i.label.clone()),
968 bounds: hit.rect,
969 });
970 }
971
972 self.request_redraw();
973 }
974
975 ElementState::Released => {
976 self.rt.handle_pointer_release(pos, mapped);
977
978 if let (Some(f), Some(cid)) = (&self.rt.frame_cache, self.rt.capture_id)
980 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
981 && hit.rect.contains(pos)
982 && hit.on_click.is_some()
983 && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid)
984 {
985 let label = node.label.as_deref().unwrap_or("");
986 self.a11y.announce(&format!("Activated {}", label));
987 }
988
989 self.request_redraw();
990 }
991 }
992 }
993
994 WindowEvent::ModifiersChanged(new_mods) => {
995 let state = new_mods.state();
996 self.rt.modifiers.shift = state.shift_key();
997 self.rt.modifiers.ctrl = state.control_key();
998 self.rt.modifiers.alt = state.alt_key();
999 self.rt.modifiers.meta = state.super_key();
1000 self.rt.modifiers.command = if cfg!(target_os = "macos") {
1001 self.rt.modifiers.meta
1002 } else {
1003 self.rt.modifiers.ctrl
1004 };
1005 }
1006
1007 WindowEvent::KeyboardInput {
1008 event: key_event, ..
1009 } => {
1010 if key_event.state == ElementState::Pressed && !key_event.repeat {
1014 match key_event.physical_key {
1015 PhysicalKey::Code(KeyCode::BrowserBack)
1016 | PhysicalKey::Code(KeyCode::Escape) => {
1017 use repose_navigation::back;
1018
1019 if repose_core::dnd::handle_drag_action(
1020 &repose_core::shortcuts::DragAction::Cancel,
1021 ) {
1022 return;
1023 }
1024
1025 let mapped = rc::map_key(key_event.physical_key);
1027 if self.dispatch_focus_key_event(&key_event, &mapped) {
1028 self.request_redraw();
1029 return;
1030 }
1031
1032 if !back::handle() {
1033 }
1035 return;
1036 }
1037 _ => {}
1038 }
1039 }
1040
1041 if let Some(inspector) = &mut self.inspector
1043 && key_event.state == ElementState::Pressed
1044 && self.rt.modifiers.ctrl
1045 && self.rt.modifiers.shift
1046 && key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
1047 {
1048 inspector.hud.toggle_inspector();
1049 self.request_redraw();
1050 return;
1051 }
1052
1053 if key_event.state == ElementState::Pressed
1055 && !key_event.repeat
1056 && self.rt.modifiers.command
1057 {
1058 match key_event.physical_key {
1059 PhysicalKey::Code(KeyCode::KeyZ) if self.rt.modifiers.shift => {
1060 if let Some(fid) = self.rt.sched.focused {
1061 let key = self.tf_key_of(fid);
1062 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1063 let mut st = state_rc.borrow_mut();
1064 if st.can_redo() {
1065 st.redo();
1066 self.notify_text_change(fid, st.text.clone());
1067 self.request_redraw();
1068 return;
1069 }
1070 }
1071 }
1072 }
1073 PhysicalKey::Code(KeyCode::KeyZ) => {
1074 if let Some(fid) = self.rt.sched.focused {
1075 let key = self.tf_key_of(fid);
1076 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1077 let mut st = state_rc.borrow_mut();
1078 if st.can_undo() {
1079 st.undo();
1080 self.notify_text_change(fid, st.text.clone());
1081 self.request_redraw();
1082 return;
1083 }
1084 }
1085 }
1086 }
1087 _ => {}
1088 }
1089 }
1090
1091 let mapped = rc::map_key(key_event.physical_key);
1094 let ke = winit_key_to_repose(&key_event, &mapped, &self.rt.modifiers);
1095 let consumed = self.rt.handle_key(&ke);
1096 if consumed {
1097 self.request_redraw();
1098 return;
1099 }
1100
1101 if key_event.state == ElementState::Pressed
1106 && !key_event.repeat
1107 && !self.rt.ime_preedit
1108 && !self.rt.modifiers.ctrl
1109 && !self.rt.modifiers.alt
1110 && !self.rt.modifiers.meta
1111 && let Some(raw) = key_event.text.as_deref()
1112 {
1113 let text: String = raw
1114 .chars()
1115 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
1116 .collect();
1117 if !text.is_empty()
1118 && let Some(fid) = self.rt.sched.focused
1119 {
1120 let key = self.tf_key_of(fid);
1121 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1122 let mut st = state_rc.borrow_mut();
1123 st.insert_text(&text);
1124 self.notify_text_change(fid, st.text.clone());
1125 if let Some(f) = &self.rt.frame_cache
1126 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1127 {
1128 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1129 }
1130 self.request_redraw();
1131 return;
1132 }
1133 }
1134 }
1135
1136 if key_event.state == ElementState::Released
1138 && let Some(active_id) = self.rt.key_pressed_active
1139 {
1140 match key_event.physical_key {
1141 PhysicalKey::Code(KeyCode::Space)
1142 | PhysicalKey::Code(KeyCode::Enter) => {
1143 if let Some(f) = &self.rt.frame_cache
1144 && let Some(node) =
1145 f.semantics_nodes.iter().find(|n| n.id == active_id)
1146 {
1147 let label = node.label.as_deref().unwrap_or("");
1148 self.a11y.announce(&format!("Activated {}", label));
1149 }
1150 }
1151 _ => {}
1152 }
1153 }
1154 }
1155
1156 WindowEvent::Ime(ime) => {
1157 if let Some(focused_id) = self.rt.sched.focused {
1158 let key = self.tf_key_of(focused_id);
1159 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1160 let mut state = state_rc.borrow_mut();
1161 let on_text_change = self
1162 .rt
1163 .frame_cache
1164 .as_ref()
1165 .and_then(|f| f.hit_regions.iter().find(|h| h.id == focused_id))
1166 .and_then(|h| h.on_text_change.clone());
1167 let mut notify = |text: String| {
1168 if let Some(cb) = &on_text_change {
1169 cb(text);
1170 }
1171 };
1172 rc_android::handle_ime_event(
1173 ime,
1174 &mut state,
1175 &mut notify,
1176 &mut self.rt.ime_preedit,
1177 );
1178 self.request_redraw();
1179 }
1180 }
1181 }
1182
1183 WindowEvent::RedrawRequested => {
1184 if !self.redraw_requested.replace(false) {
1186 self.process_a11y_actions();
1187 self.process_render_commands();
1188 if let (Some(backend), Some(frame)) =
1190 (self.backend.as_mut(), self.rt.frame_cache.as_ref())
1191 {
1192 let scale = self
1193 .window
1194 .as_ref()
1195 .map(|w| w.scale_factor() as f32)
1196 .unwrap_or(1.0);
1197 let mut scene = frame.scene.clone();
1198 if let Some(inspector) = &mut self.inspector {
1199 inspector.frame(&mut scene);
1200 }
1201 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1202 }
1203 log::trace!("RedrawRequested: no frame request, skipping compose");
1204 return;
1205 }
1206 log::trace!("RedrawRequested: frame request pending, composing");
1207
1208 self.process_a11y_actions();
1210 self.process_render_commands();
1211
1212 let Some(win) = self.window.as_ref() else {
1213 return;
1214 };
1215 if self.backend.is_none() {
1216 return;
1217 }
1218
1219 repose_core::animation_driver::tick();
1222
1223 let t0 = Instant::now();
1224 let scale = win.scale_factor() as f32;
1225 self.rt.scale = scale;
1226 let focused = self.rt.sched.focused;
1227
1228 let output = self.rt.frame(&mut self.root, &self.render);
1229
1230 if let Some(cursor) = &output.platform.cursor {
1232 win.set_cursor(winit::window::Cursor::Icon(map_cursor(*cursor)));
1233 }
1234
1235 if let Some(dark) = output.platform.window_theme_dark
1237 && self.last_window_theme != Some(dark)
1238 {
1239 win.set_theme(Some(if dark {
1240 winit::window::Theme::Dark
1241 } else {
1242 winit::window::Theme::Light
1243 }));
1244 self.last_window_theme = Some(dark);
1245 }
1246
1247 if output.platform.ime_allowed {
1249 rc_web::set_ime_for_textfield_ex(
1250 win,
1251 true,
1252 output.platform.ime_purpose,
1253 output.platform.ime_auto_correct,
1254 output.platform.ime_capitalization,
1255 );
1256 if let Some((x, y, w, h)) = output.platform.ime_cursor_area {
1257 win.set_ime_cursor_area(
1258 LogicalPosition::new(x, y),
1259 LogicalSize::new(w, h),
1260 );
1261 }
1262 } else if self.rt.ime_preedit {
1263 rc_web::set_ime_for_textfield_ex(
1264 win,
1265 false,
1266 repose_core::ImePurposeHint::Normal,
1267 true,
1268 repose_core::KeyboardCapitalization::Unspecified,
1269 );
1270 self.rt.ime_preedit = false;
1271 }
1272
1273 if !output.wants_keyboard
1275 && focused.is_some()
1276 && self.rt.sched.focused.is_none()
1277 && self.rt.ime_preedit
1278 {
1279 rc_web::set_ime_for_textfield(win, false);
1280 self.rt.ime_preedit = false;
1281 }
1282
1283 let frame = Frame {
1284 scene: output.scene,
1285 hit_regions: output.hit_regions,
1286 semantics_nodes: output.semantics_nodes,
1287 focus_chain: output.focus_chain,
1288 };
1289
1290 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1291
1292 if let Some(adapter) = &mut self.accesskit_adapter {
1294 let win = self.window.as_ref().unwrap();
1295 let scale = win.scale_factor();
1296 if let Some(update) = self.a11y_tree.update(
1297 &frame.semantics_nodes,
1298 scale,
1299 self.rt.sched.focused,
1300 ) {
1301 adapter.update_if_active(|| update);
1302 }
1303 }
1304
1305 let mut scene = frame.scene.clone();
1307 if let Some(inspector) = &mut self.inspector {
1309 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1310 let signal_count = self.rt.sched.id_count() as usize;
1311 let ls = repose_ui::last_layout_stats();
1312 inspector.hud.metrics = Some(repose_devtools::Metrics {
1313 build_ms: build_layout_ms,
1314 layout_ms: ls.layout_time_ms,
1315 paint_ms: ls.paint_time_ms,
1316 scene_nodes: scene.nodes.len(),
1317 widget_count,
1318 signal_count,
1319 taffy_created: ls.taffy_created,
1320 taffy_reused: ls.taffy_reused,
1321 layout_hits: ls.layout_hits,
1322 layout_misses: ls.layout_misses,
1323 paint_cache_hits: ls.paint_cache_hits,
1324 paint_cache_misses: ls.paint_cache_misses,
1325 paint_culled: ls.paint_culled,
1326 });
1327 inspector.frame(&mut scene);
1328 }
1329
1330 repose_core::dnd::overlay_drag_indicator(
1332 &mut scene,
1333 self.rt.mouse_pos_px,
1334 self.external_file_drag,
1335 );
1336
1337 self.process_render_commands();
1340
1341 let win = self.window.as_ref().unwrap();
1343 let scale = win.scale_factor() as f32;
1344 if let Some(backend) = self.backend.as_mut() {
1345 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1346 }
1347
1348 if let Some(fid) = self.rt.sched.focused
1351 && let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
1352 && let Some(key) = hit.tf_state_key
1353 && !self.rt.textfield_states.contains_key(&key)
1354 {
1355 self.rt
1356 .textfield_states
1357 .entry(key)
1358 .or_insert_with(|| {
1359 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1360 })
1361 .borrow_mut()
1362 .reset_caret_blink();
1363 }
1364
1365 repose_core::dnd::set_dnd_frame(Some(frame.clone()));
1368 self.rt.cache_frame(frame);
1369 repose_core::dnd::set_dnd_scale(scale);
1370
1371 self.dispatch_file_drop_now();
1372
1373 rc::tick_snackbar(self.last_redraw);
1374 self.last_redraw = Instant::now();
1375 }
1376
1377 _ => {}
1378 }
1379 }
1380
1381 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1382 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1385 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK.lock().unwrap().as_ref() {
1386 cb();
1387 }
1388 process_deeplinks();
1389
1390 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1393 if WINDOW_VISIBLE.load(Ordering::Relaxed)
1394 && self.backend.is_none()
1395 && let Some(w) = &self.window
1396 {
1397 log::info!("about_to_wait: recreating GPU backend");
1398 match repose_render_wgpu::WgpuBackend::new_with_options(
1399 w.clone(),
1400 self.msaa_samples,
1401 self.present_mode,
1402 ) {
1403 Ok(b) => self.backend = Some(b),
1404 Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
1405 }
1406 }
1407
1408 let needs_compose = take_frame_request();
1409 let needs_present = take_present_request();
1410
1411 if needs_compose {
1412 self.pending_redraw = true;
1413 }
1414
1415 if !self.pending_redraw && needs_present && self.rt.frame_cache.is_some() {
1417 let now = Instant::now();
1418 let interval = self.frame_interval();
1419 if now.saturating_duration_since(self.last_redraw) >= interval {
1420 rc::request_redraw(&self.window);
1421 self.last_redraw = now;
1422 } else {
1423 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1424 self.last_redraw + interval,
1425 ));
1426 }
1427 return;
1428 }
1429
1430 if !self.pending_redraw {
1431 let now = Instant::now();
1432 let idle_cap = web_time::Duration::from_millis(1000);
1433 let deadline = self.next_caret_blink_deadline().unwrap_or(now + idle_cap);
1434
1435 if now.saturating_duration_since(self.last_redraw) >= idle_cap || now >= deadline {
1436 self.redraw_requested.set(true);
1437 request_frame();
1438 rc::request_redraw(&self.window);
1439 self.last_redraw = now;
1440 }
1441 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(Ord::min(
1442 deadline,
1443 now + idle_cap,
1444 )));
1445 return;
1446 }
1447
1448 let now = Instant::now();
1449 let interval = self.frame_interval();
1450
1451 if now.saturating_duration_since(self.last_redraw) >= interval {
1452 self.pending_redraw = false;
1453 self.redraw_requested.set(true);
1454 rc::request_redraw(&self.window);
1455 self.last_redraw = now;
1456 } else {
1457 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1458 self.last_redraw + interval,
1459 ));
1460 }
1461 }
1462
1463 fn new_events(
1464 &mut self,
1465 _: &winit::event_loop::ActiveEventLoop,
1466 _: winit::event::StartCause,
1467 ) {
1468 }
1469 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1470 self.pending_redraw = true;
1471 }
1472 fn device_event(
1473 &mut self,
1474 _: &winit::event_loop::ActiveEventLoop,
1475 _: winit::event::DeviceId,
1476 _: winit::event::DeviceEvent,
1477 ) {
1478 }
1479 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1480 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1481 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1482 }
1483
1484 impl App {
1485 fn dispatch_focus_key_event(
1488 &self,
1489 key_event: &winit::event::KeyEvent,
1490 mapped_key: &repose_core::input::Key,
1491 ) -> bool {
1492 let Some(f) = &self.rt.frame_cache else {
1493 return false;
1494 };
1495 let Some(focused) = self.rt.sched.focused else {
1496 return false;
1497 };
1498 let utf16 = match mapped_key {
1499 repose_core::input::Key::Character(c) => *c as u16,
1500 _ => 0,
1501 };
1502 let mods = self.rt.modifiers;
1503 let repeat = key_event.repeat;
1504 let ev_type = if key_event.state == ElementState::Pressed {
1505 repose_core::input::KeyEventType::Down
1506 } else {
1507 repose_core::input::KeyEventType::Up
1508 };
1509 let hit_by_id: std::collections::HashMap<u64, &HitRegion> =
1510 f.hit_regions.iter().map(|h| (h.id, h)).collect();
1511 let sem_parent_of: std::collections::HashMap<u64, u64> = f
1512 .semantics_nodes
1513 .iter()
1514 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1515 .collect();
1516 let mut ancestors = Vec::new();
1517 let mut cur = focused;
1518 loop {
1519 ancestors.push(cur);
1520 if let Some(&p) = sem_parent_of.get(&cur) {
1521 cur = p;
1522 } else {
1523 break;
1524 }
1525 }
1526 let make_ke = || repose_core::input::KeyEvent {
1527 key: mapped_key.clone(),
1528 modifiers: mods,
1529 is_repeat: repeat,
1530 event_type: ev_type,
1531 utf16_code_point: utf16,
1532 };
1533 for &id in ancestors.iter().rev() {
1535 if let Some(hit) = hit_by_id.get(&id)
1536 && let Some(cb) = &hit.on_preview_key_event
1537 && cb(make_ke())
1538 {
1539 return true;
1540 }
1541 }
1542 for &id in ancestors.iter() {
1544 if let Some(hit) = hit_by_id.get(&id)
1545 && let Some(cb) = &hit.on_key_event
1546 && cb(make_ke())
1547 {
1548 return true;
1549 }
1550 }
1551 false
1552 }
1553
1554 fn announce_focus_change(&mut self) {
1555 if let Some(f) = &self.rt.frame_cache {
1556 let focused_node = self
1557 .rt
1558 .sched
1559 .focused
1560 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1561 self.a11y.focus_changed(focused_node);
1562 }
1563 }
1564
1565 fn notify_text_change(&self, id: u64, text: String) {
1566 if let Some(f) = &self.rt.frame_cache
1567 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1568 && let Some(cb) = &h.on_text_change
1569 {
1570 cb(text);
1571 }
1572 }
1573
1574 fn tf_key_of(&self, visual_id: u64) -> u64 {
1575 rc::tf_key_of_in_frame(&self.rt.frame_cache, visual_id)
1576 }
1577
1578 fn next_caret_blink_deadline(&self) -> Option<Instant> {
1581 next_caret_blink_deadline(
1582 &self.rt.sched,
1583 &self.rt.frame_cache,
1584 &self.rt.textfield_states,
1585 )
1586 }
1587
1588 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1589 use repose_core::shortcuts;
1590
1591 if let (Some(f), Some(fid)) = (&self.rt.frame_cache, self.rt.sched.focused)
1592 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1593 && let Some(cb) = &hit.on_action
1594 && cb(action.clone())
1595 {
1596 return true;
1597 }
1598
1599 if shortcuts::handle(action.clone()) {
1600 return true;
1601 }
1602
1603 if let Some(f) = &self.rt.frame_cache
1605 && let Some(new_id) =
1606 repose_core::focus::handle_action(&action, &mut self.rt.sched, f)
1607 {
1608 if let Some(active) = self.rt.key_pressed_active.take() {
1609 self.rt.pressed_ids.remove(&active);
1610 }
1611 let tf_state_key = f
1612 .hit_regions
1613 .iter()
1614 .find(|h| h.id == new_id)
1615 .and_then(|h| h.tf_state_key);
1616 if let Some(key) = tf_state_key {
1617 self.rt
1618 .textfield_states
1619 .entry(key)
1620 .or_insert_with(|| Rc::new(RefCell::new(repose_ui::TextFieldState::new())));
1621 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1622 state_rc.borrow_mut().reset_caret_blink();
1623 }
1624 }
1625 if let Some(win) = &self.window {
1626 let is_textfield = f.semantics_nodes.iter().any(|n| {
1627 n.id == new_id && n.role == repose_core::semantics::Role::TextField
1628 });
1629 rc_web::set_ime_for_textfield(win, is_textfield);
1630 }
1631 self.announce_focus_change();
1632 return true;
1633 }
1634
1635 false
1636 }
1637
1638 fn dispatch_file_drop_now(&mut self) {
1639 let Some(f) = &self.rt.frame_cache else {
1640 self.pending_dropped_files.clear();
1641 self.pending_drop_pos_px = None;
1642 return;
1643 };
1644
1645 if self.pending_dropped_files.is_empty() {
1646 return;
1647 }
1648
1649 let pos_px = self.pending_drop_pos_px.unwrap_or(self.rt.mouse_pos_px);
1650 let pos = Vec2 {
1651 x: pos_px.0,
1652 y: pos_px.1,
1653 };
1654
1655 let mut files = Vec::new();
1656 for p in self.pending_dropped_files.drain(..) {
1657 let name = p
1658 .file_name()
1659 .and_then(|s| s.to_str())
1660 .unwrap_or("file")
1661 .to_string();
1662 files.push(repose_core::dnd::DroppedFile {
1663 name,
1664 path: Some(p),
1665 });
1666 }
1667
1668 let payload: repose_core::dnd::DragPayload =
1669 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1670
1671 let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
1672 self.pending_drop_pos_px = None;
1673 return;
1674 };
1675
1676 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
1677 && let Some(cb) = &hit.on_drop
1678 {
1679 let accepted = cb(repose_core::dnd::DropEvent {
1680 source_id: 0, target_id,
1682 position: pos,
1683 modifiers: self.rt.modifiers,
1684 payload: payload.clone(),
1685 });
1686
1687 if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
1688 {
1689 let label = node.label.as_deref().unwrap_or("");
1690 self.a11y.announce(&format!("Dropped files on {}", label));
1691 }
1692 }
1693
1694 self.pending_drop_pos_px = None;
1695 self.request_redraw();
1696 }
1697 }
1698
1699 let event_loop = EventLoop::new()?;
1700 set_event_loop_proxy(event_loop.create_proxy());
1701 let mut app = App::new(Box::new(root), config);
1702 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1704 event_loop.run_app(&mut app)?;
1705 Ok(())
1706}
1707
1708pub trait A11yBridge: Send {
1716 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1718
1719 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1721
1722 fn announce(&mut self, msg: &str);
1724}
1725
1726struct NoopA11y;
1727impl A11yBridge for NoopA11y {
1728 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1729 }
1731 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1732 if let Some(n) = node {
1733 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1734 } else {
1735 log::info!("A11y focus: None");
1736 }
1737 }
1738 fn announce(&mut self, msg: &str) {
1739 log::info!("A11y announce: {msg}");
1740 }
1741}
1742
1743#[cfg(target_os = "linux")]
1744struct LinuxAtspiStub;
1745#[cfg(target_os = "linux")]
1746impl A11yBridge for LinuxAtspiStub {
1747 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1748 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1749 }
1750 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1751 if let Some(n) = node {
1752 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1753 } else {
1754 log::info!("AT-SPI stub focus: None");
1755 }
1756 }
1757 fn announce(&mut self, msg: &str) {
1758 log::info!("AT-SPI stub announce: {msg}");
1759 }
1760}