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 self, TF_FONT_DP, TF_PADDING_X_DP, TextFieldState, TextMeasureConfig, caret_xy_for_byte, 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 if !frame.focus_chain.contains(&fid) {
277 sched.focused = None;
278 }
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}
352
353impl Default for ReposeOptions {
354 fn default() -> Self {
355 Self { msaa_samples: 4 }
356 }
357}
358
359#[derive(Clone, Debug)]
365pub struct AppConfig {
366 pub common: ReposeOptions,
368 pub window_title: String,
370 pub window_size: (u32, u32),
372 pub enable_inspector: bool,
374}
375
376impl Default for AppConfig {
377 fn default() -> Self {
378 Self {
379 common: ReposeOptions::default(),
380 window_title: "Repose".to_string(),
381 window_size: (1280, 800),
382 enable_inspector: true,
383 }
384 }
385}
386
387#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
392#[deprecated(
393 note = "use run_desktop_app_with_config(root, AppConfig) instead; this may be removed in a future release"
394)]
395pub fn run_desktop_app(
396 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
397) -> anyhow::Result<()> {
398 run_desktop_app_with_config(root, AppConfig::default())
399}
400
401#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
403pub fn run_desktop_app_with_config(
404 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
405 config: AppConfig,
406) -> anyhow::Result<()> {
407 use std::collections::{HashMap, HashSet};
408 use winit::application::ApplicationHandler;
409 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
410 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
411 use winit::event_loop::EventLoop;
412 use winit::keyboard::{KeyCode, PhysicalKey};
413 use winit::window::{Window, WindowAttributes};
414
415 use crate::a11y::A11yTree;
416 use repose_app::ReposeRuntime;
417
418 struct ReposeActivationHandler {
419 initial_tree: Option<accesskit::TreeUpdate>,
420 }
421
422 impl accesskit::ActivationHandler for ReposeActivationHandler {
423 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
424 self.initial_tree.take()
425 }
426 }
427
428 struct ReposeDeactivationHandler;
429
430 impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
431 fn deactivate_accessibility(&mut self) {
432 }
434 }
435
436 struct App {
437 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
438 render: RenderContext,
439 window: Option<Arc<Window>>,
440 backend: Option<repose_render_wgpu::WgpuBackend>,
441 rt: ReposeRuntime,
442 inspector: Option<repose_devtools::Inspector>,
443 msaa_samples: u32,
444 window_title: String,
445 window_size: (u32, u32),
446
447 pending_dropped_files: Vec<std::path::PathBuf>,
449 pending_drop_pos_px: Option<(f32, f32)>,
450
451 external_file_drag: bool,
453 hovered_files: Vec<std::path::PathBuf>,
454
455 clipboard: Option<clipawl::Clipboard>,
456 a11y: Box<dyn A11yBridge>,
457
458 accesskit_adapter: Option<Adapter>,
459 a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
460 a11y_tree: A11yTree,
461
462 last_redraw: Instant,
463 pending_redraw: bool,
464
465 redraw_requested: Cell<bool>,
467 }
468
469 impl App {
470 fn process_a11y_actions(&mut self) {
471 let mut actions = self.a11y_actions.lock().unwrap();
472 if actions.is_empty() {
473 return;
474 }
475 let pending = actions.drain(..).collect::<Vec<_>>();
476 drop(actions);
477
478 let Some(f) = &self.rt.frame_cache else {
479 return;
480 };
481
482 for req in pending {
483 let target_id = req.target_node.0;
484 match req.action {
485 accesskit::Action::Click => {
486 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
487 && let Some(cb) = &hit.on_click
488 {
489 cb();
490 self.request_redraw();
491 }
492 }
493 accesskit::Action::Focus => {
494 self.rt.sched.focused = Some(target_id);
495 self.request_redraw();
496 }
497 _ => {}
498 }
499 }
500 }
501
502 fn new(
503 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
504 config: AppConfig,
505 ) -> Self {
506 Self {
507 root,
508 render: RenderContext::new(),
509 window: None,
510 backend: None,
511 rt: ReposeRuntime::new(),
512 inspector: if config.enable_inspector {
513 Some(repose_devtools::Inspector::new())
514 } else {
515 None
516 },
517 msaa_samples: config.common.msaa_samples,
518 window_title: config.window_title,
519 window_size: config.window_size,
520 pending_dropped_files: Vec::new(),
521 pending_drop_pos_px: None,
522
523 external_file_drag: false,
524 hovered_files: Vec::new(),
525
526 clipboard: None,
527 a11y: {
528 #[cfg(target_os = "linux")]
529 {
530 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
531 }
532 #[cfg(not(target_os = "linux"))]
533 {
534 Box::new(NoopA11y) as Box<dyn A11yBridge>
535 }
536 },
537
538 accesskit_adapter: None,
539 a11y_actions: Arc::new(Mutex::new(Vec::new())),
540 a11y_tree: A11yTree::default(),
541
542 last_redraw: Instant::now(),
543 pending_redraw: false,
544 redraw_requested: Cell::new(false),
545 }
546 }
547
548 fn request_redraw(&self) {
549 self.redraw_requested.set(true);
550 repose_core::request_frame();
551 rc::request_redraw(&self.window);
552 }
553
554 fn tf_ensure_caret_visible(st: &mut TextFieldState, is_multiline: bool) {
556 rc::tf_ensure_caret_visible(st, is_multiline);
557 }
558
559 fn paste_from_primary(&self) -> Option<String> {
560 let mut opts = clipawl::ClipboardOptions::default();
561 opts.linux.selection = clipawl::LinuxSelection::Primary;
562 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
563 match pollster::block_on(cb.read()) {
564 Ok(t) => Some(t),
565 Err(e) => {
566 eprintln!("Primary paste error: {}", e);
567 None
568 }
569 }
570 } else {
571 None
572 }
573 }
574
575 fn process_render_commands(&mut self) {
576 let Some(backend) = self.backend.as_mut() else {
577 return;
578 };
579 rc::process_render_commands(backend, self.render.drain());
580 }
581
582 fn reset_pointer_state(&mut self) {
583 self.rt.capture_id = None;
584 self.rt.pressed_ids.clear();
585 self.rt.hover_id = None;
586 }
587
588 fn is_textfield(&self, id: u64) -> bool {
589 rc::is_textfield_in_frame(&self.rt.frame_cache, id)
590 }
591
592 fn is_multiline_id(&self, id: u64) -> bool {
593 if let Some(f) = &self.rt.frame_cache {
594 f.hit_regions
595 .iter()
596 .find(|h| h.id == id)
597 .map(|h| h.tf_multiline)
598 .unwrap_or(false)
599 } else {
600 false
601 }
602 }
603
604 fn hit_by_id(f: &Frame, id: u64) -> Option<&HitRegion> {
605 f.hit_regions.iter().find(|h| h.id == id)
606 }
607
608 fn dp_px(&self, dp: f32) -> f32 {
609 dp_to_px(dp)
610 }
611 }
612
613 impl ApplicationHandler<()> for App {
614 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
615 self.clipboard = clipawl::Clipboard::new()
616 .map_err(|e| {
617 eprintln!("clipawl clipboard init failed: {e}");
618 e
619 })
620 .ok();
621 repose_core::clipboard::set_clipboard_read_fn(Box::new(|| {
622 clipawl::blocking::read().ok()
623 }));
624 repose_core::clipboard::set_clipboard_fn(Box::new(move |text| {
626 if let Err(e) = clipawl::blocking::write(text) {
627 eprintln!("clipboard write error: {e}");
628 }
629 }));
630
631 repose_core::clipboard::set_primary_fn(Box::new(|text| {
632 let mut opts = clipawl::ClipboardOptions::default();
633 opts.linux.selection = clipawl::LinuxSelection::Primary;
634 match clipawl::Clipboard::new_with_options(opts) {
635 Ok(cb) => {
636 if let Err(e) = pollster::block_on(cb.write(text)) {
637 eprintln!("primary selection write error: {e}");
638 }
639 }
640 Err(e) => eprintln!("primary clipboard init error: {e}"),
641 }
642 }));
643
644 if self.window.is_none() {
645 match el.create_window(
646 WindowAttributes::default()
647 .with_title(self.window_title.clone())
648 .with_inner_size(PhysicalSize::new(
649 self.window_size.0,
650 self.window_size.1,
651 ))
652 .with_visible(false),
653 ) {
654 Ok(win) => {
655 let w = Arc::new(win);
656
657 let activation_handler = ReposeActivationHandler {
658 initial_tree: Some(A11yTree::initial_tree()),
659 };
660
661 let action_handler = ReposeActionHandler {
662 pending_actions: self.a11y_actions.clone(),
663 };
664
665 let deactivation_handler = ReposeDeactivationHandler;
666
667 let adapter = Adapter::with_direct_handlers(
668 el,
669 &w,
670 activation_handler,
671 action_handler,
672 deactivation_handler,
673 );
674
675 self.accesskit_adapter = Some(adapter);
676
677 w.set_visible(true);
678
679 let size = w.inner_size();
680 let sf = w.scale_factor() as f32;
681 self.rt.set_viewport_and_scale(size.width, size.height, sf);
682
683 match repose_render_wgpu::WgpuBackend::new_with_msaa(
684 w.clone(),
685 self.msaa_samples,
686 ) {
687 Ok(b) => {
688 self.backend = Some(b);
689 set_app_window(w.clone());
690 self.window = Some(w);
691 self.request_redraw();
692 }
693 Err(e) => {
694 log::error!("Failed to create WGPU backend: {e:?}");
695 el.exit();
696 }
697 }
698 }
699 Err(e) => {
700 log::error!("Failed to create window: {e:?}");
701 el.exit();
702 }
703 }
704 }
705 }
706
707 fn window_event(
708 &mut self,
709 el: &winit::event_loop::ActiveEventLoop,
710 _id: winit::window::WindowId,
711 event: WindowEvent,
712 ) {
713 if let Some(adapter) = &mut self.accesskit_adapter {
715 adapter.process_event(self.window.as_ref().unwrap(), &event);
716 }
717
718 match event {
719 WindowEvent::CloseRequested => {
720 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
721 self.backend = None;
723 if let Some(w) = &self.window {
724 w.set_visible(false);
725 }
726 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
727 } else {
728 el.exit();
729 }
730 }
731
732 WindowEvent::Focused(false) => {
733 self.rt.handle_focus_lost();
735
736 self.external_file_drag = false;
738 self.hovered_files.clear();
739
740 if let Some(w) = &self.window {
741 rc_web::set_ime_for_textfield(w, false);
742 }
743
744 self.request_redraw();
745 }
746
747 WindowEvent::CursorLeft { .. } => {
748 self.rt.pointer_inside = false;
749 self.rt.clear_hover();
750 self.external_file_drag = false;
751 self.hovered_files.clear();
752 self.request_redraw();
753 }
754
755 WindowEvent::HoveredFile(path) => {
756 self.external_file_drag = true;
758 if self.hovered_files.len() < 32 {
759 self.hovered_files.push(path);
760 }
761 if self.pending_drop_pos_px.is_none() {
763 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
764 }
765 self.request_redraw();
766 }
767
768 WindowEvent::HoveredFileCancelled => {
769 self.external_file_drag = false;
770 self.hovered_files.clear();
771
772 self.reset_pointer_state();
774
775 self.request_redraw();
776 }
777
778 WindowEvent::DroppedFile(path) => {
779 self.pending_dropped_files.push(path);
781 if self.pending_drop_pos_px.is_none() {
782 self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
783 }
784
785 self.external_file_drag = false;
787 self.hovered_files.clear();
788
789 self.request_redraw();
790 }
791
792 WindowEvent::Resized(size) => {
793 let sf = self.window.as_ref().map(|w| w.scale_factor() as f32).unwrap_or(1.0);
794 self.rt.set_viewport_and_scale(size.width, size.height, sf);
795 if let Some(b) = self.backend.as_mut() {
796 b.configure_surface(size.width, size.height);
797 }
798 if let Some(w) = &self.window {
799 let sf = w.scale_factor() as f32;
800 let dp_w = size.width as f32 / sf;
801 let dp_h = size.height as f32 / sf;
802 log::info!(
803 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
804 size.width,
805 size.height,
806 sf,
807 dp_w as i32,
808 dp_h as i32
809 );
810 }
811 self.request_redraw();
812 }
813
814 WindowEvent::CursorMoved { position, .. } => {
815 self.rt.pointer_inside = true;
816
817 if self.external_file_drag {
818 self.pending_drop_pos_px = Some((position.x as f32, position.y as f32));
819 }
820
821 let pos = Vec2 {
822 x: position.x as f32,
823 y: position.y as f32,
824 };
825
826 let result = self.rt.handle_pointer_move(pos);
828
829 if let (Some(inspector), Some(f)) =
831 (&mut self.inspector, &self.rt.frame_cache)
832 && inspector.hud.inspector_enabled
833 {
834 let hit = f.hit_regions.iter().find(|h| {
835 h.rect.contains(pos)
836 });
837 let hover_rect = hit.map(|h| h.rect);
838 let hover_info = hit.and_then(|h| {
839 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
840 repose_devtools::HoveredInfo {
841 id: s.id,
842 role: format!("{:?}", s.role),
843 label: s.label.clone(),
844 }
845 })
846 });
847 inspector.hud.set_hovered(hover_rect, hover_info);
848 }
849
850 if let Some(win) = &self.window {
852 if let Some(c) = result.cursor {
853 win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
854 }
855 }
856
857 self.request_redraw();
858 }
859
860 WindowEvent::MouseWheel { delta, .. } => {
861 let (dx_px, dy_px) = match delta {
862 MouseScrollDelta::LineDelta(x, y) => {
863 let unit_px = dp_to_px(60.0);
864 (-(x * unit_px), -(y * unit_px))
865 }
866 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
867 };
868 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
869
870 if self.rt.handle_scroll(Vec2 { x: dx_px, y: dy_px }) {
871 self.request_redraw();
872 }
873 }
874
875 WindowEvent::MouseInput {
876 state: ElementState::Pressed,
877 button: MouseButton::Left,
878 ..
879 } => {
880 let pos = Vec2 {
881 x: self.rt.mouse_pos_px.0,
882 y: self.rt.mouse_pos_px.1,
883 };
884
885 let result = self.rt.handle_pointer_press(pos, PointerButton::Primary);
886
887 if let Some(fid) = result.focused {
889 if let Some(win) = &self.window
890 && let Some(f) = &self.rt.frame_cache
891 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
892 {
893 let sf = win.scale_factor();
894 rc_web::set_ime_for_textfield(win, true);
895 win.set_ime_cursor_area(
896 LogicalPosition::new(
897 hit.rect.x as f64 / sf,
898 hit.rect.y as f64 / sf,
899 ),
900 LogicalSize::new(
901 hit.rect.w as f64 / sf,
902 hit.rect.h as f64 / sf,
903 ),
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 let Some(inspector) = &mut self.inspector
922 && inspector.hud.inspector_enabled
923 && let Some(f) = &self.rt.frame_cache
924 && let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
925 {
926 let info = f
927 .semantics_nodes
928 .iter()
929 .find(|s| s.id == hit.id)
930 .map(|s| repose_devtools::HoveredInfo {
931 id: s.id,
932 role: format!("{:?}", s.role),
933 label: s.label.clone(),
934 });
935 inspector.hud.select_widget(repose_devtools::SelectedWidget {
936 id: hit.id,
937 role: info
938 .as_ref()
939 .map(|i| i.role.clone())
940 .unwrap_or_default(),
941 label: info.as_ref().and_then(|i| i.label.clone()),
942 bounds: hit.rect,
943 });
944 }
945
946 self.request_redraw();
947 }
948
949 WindowEvent::MouseInput {
950 state: ElementState::Pressed,
951 button: MouseButton::Middle,
952 ..
953 } => {
954 let Some(f) = &self.rt.frame_cache else {
955 return;
956 };
957 let pos = Vec2 {
958 x: self.rt.mouse_pos_px.0,
959 y: self.rt.mouse_pos_px.1,
960 };
961 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos)) {
962 if let Some(cb) = &hit.on_pointer_down {
964 cb(PointerEvent::new(
965 PointerId(0),
966 PointerKind::Mouse,
967 PointerEventKind::Down(PointerButton::Tertiary),
968 pos,
969 1.0,
970 self.rt.modifiers,
971 ));
972 }
973 if self.is_textfield(hit.id) {
975 let key = self.tf_key_of(hit.id);
976 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
977 if let Some(txt) = self.paste_from_primary() {
978 let mut st = state_rc.borrow_mut();
979 st.insert_text_atomic(&txt);
980 self.notify_text_change(hit.id, st.text.clone());
981 if let Some(f) = &self.rt.frame_cache
982 && let Some(h) =
983 f.hit_regions.iter().find(|h| h.id == hit.id)
984 {
985 App::tf_ensure_caret_visible(&mut st, h.tf_multiline);
986 }
987 }
988 }
989 }
990 }
991 self.request_redraw();
992 }
993
994 WindowEvent::MouseInput {
995 state: ElementState::Released,
996 button: MouseButton::Left,
997 ..
998 } => {
999 let pos = Vec2 {
1000 x: self.rt.mouse_pos_px.0,
1001 y: self.rt.mouse_pos_px.1,
1002 };
1003
1004 self.rt.handle_pointer_release(pos, PointerButton::Primary);
1005
1006 if let (Some(f), Some(cid)) = (&self.rt.frame_cache, self.rt.capture_id) {
1008 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1009 && hit.rect.contains(pos)
1010 && hit.on_click.is_some()
1011 && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid)
1012 {
1013 let label = node.label.as_deref().unwrap_or("");
1014 self.a11y.announce(&format!("Activated {}", label));
1015 }
1016 }
1017
1018 repose_core::request_frame();
1019 }
1020
1021 WindowEvent::MouseInput {
1022 state: ElementState::Released,
1023 button: MouseButton::Middle,
1024 ..
1025 } => {
1026 if let Some(f) = &self.rt.frame_cache {
1027 let pos = Vec2 {
1028 x: self.rt.mouse_pos_px.0,
1029 y: self.rt.mouse_pos_px.1,
1030 };
1031 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
1032 {
1033 if let Some(cb) = &hit.on_pointer_up {
1034 cb(PointerEvent::new(
1035 PointerId(0),
1036 PointerKind::Mouse,
1037 PointerEventKind::Up(PointerButton::Tertiary),
1038 pos,
1039 1.0,
1040 self.rt.modifiers,
1041 ));
1042 }
1043 }
1044 }
1045 }
1046
1047 WindowEvent::ModifiersChanged(new_mods) => {
1048 let state = new_mods.state();
1049 self.rt.modifiers.shift = state.shift_key();
1050 self.rt.modifiers.ctrl = state.control_key();
1051 self.rt.modifiers.alt = state.alt_key();
1052 self.rt.modifiers.meta = state.super_key();
1053 self.rt.modifiers.command = if cfg!(target_os = "macos") {
1054 self.rt.modifiers.meta
1055 } else {
1056 self.rt.modifiers.ctrl
1057 };
1058 }
1059
1060 WindowEvent::KeyboardInput {
1061 event: key_event, ..
1062 } => {
1063 if key_event.state == ElementState::Pressed && !key_event.repeat {
1067 match key_event.physical_key {
1068 PhysicalKey::Code(KeyCode::BrowserBack)
1069 | PhysicalKey::Code(KeyCode::Escape) => {
1070 use repose_navigation::back;
1071
1072 if repose_core::dnd::handle_drag_action(
1073 &repose_core::shortcuts::DragAction::Cancel,
1074 ) {
1075 return;
1076 }
1077
1078 let mapped = rc::map_key(key_event.physical_key);
1080 if self.dispatch_focus_key_event(&key_event, &mapped) {
1081 self.request_redraw();
1082 return;
1083 }
1084
1085 if !back::handle() {
1086 }
1088 return;
1089 }
1090 _ => {}
1091 }
1092 }
1093
1094 if let Some(inspector) = &mut self.inspector
1096 && key_event.state == ElementState::Pressed
1097 && self.rt.modifiers.ctrl
1098 && self.rt.modifiers.shift
1099 && key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
1100 {
1101 inspector.hud.toggle_inspector();
1102 self.request_redraw();
1103 return;
1104 }
1105
1106 if key_event.state == ElementState::Pressed
1108 && !key_event.repeat
1109 && self.rt.modifiers.command
1110 {
1111 match key_event.physical_key {
1112 PhysicalKey::Code(KeyCode::KeyZ) if self.rt.modifiers.shift => {
1113 if let Some(fid) = self.rt.sched.focused {
1114 let key = self.tf_key_of(fid);
1115 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1116 let mut st = state_rc.borrow_mut();
1117 if st.can_redo() {
1118 st.redo();
1119 self.notify_text_change(fid, st.text.clone());
1120 self.request_redraw();
1121 return;
1122 }
1123 }
1124 }
1125 }
1126 PhysicalKey::Code(KeyCode::KeyZ) => {
1127 if let Some(fid) = self.rt.sched.focused {
1128 let key = self.tf_key_of(fid);
1129 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1130 let mut st = state_rc.borrow_mut();
1131 if st.can_undo() {
1132 st.undo();
1133 self.notify_text_change(fid, st.text.clone());
1134 self.request_redraw();
1135 return;
1136 }
1137 }
1138 }
1139 }
1140 _ => {}
1141 }
1142 }
1143
1144 let mapped = rc::map_key(key_event.physical_key);
1147 let ke = winit_key_to_repose(&key_event, &mapped, &self.rt.modifiers);
1148 let consumed = self.rt.handle_key(&ke);
1149 if consumed {
1150 self.request_redraw();
1151 return;
1152 }
1153
1154 if key_event.state == ElementState::Pressed
1159 && !key_event.repeat
1160 && !self.rt.ime_preedit
1161 && !self.rt.modifiers.ctrl
1162 && !self.rt.modifiers.alt
1163 && !self.rt.modifiers.meta
1164 && let Some(raw) = key_event.text.as_deref()
1165 {
1166 let text: String = raw
1167 .chars()
1168 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
1169 .collect();
1170 if !text.is_empty()
1171 && let Some(fid) = self.rt.sched.focused
1172 {
1173 let key = self.tf_key_of(fid);
1174 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1175 let mut st = state_rc.borrow_mut();
1176 st.insert_text(&text);
1177 self.notify_text_change(fid, st.text.clone());
1178 if let Some(f) = &self.rt.frame_cache
1179 && let Some(hit) =
1180 f.hit_regions.iter().find(|h| h.id == fid)
1181 {
1182 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1183 }
1184 self.request_redraw();
1185 return;
1186 }
1187 }
1188 }
1189
1190 if key_event.state == ElementState::Released
1192 && let Some(active_id) = self.rt.key_pressed_active
1193 {
1194 match key_event.physical_key {
1195 PhysicalKey::Code(KeyCode::Space)
1196 | PhysicalKey::Code(KeyCode::Enter) => {
1197 if let Some(f) = &self.rt.frame_cache
1198 && let Some(node) =
1199 f.semantics_nodes.iter().find(|n| n.id == active_id)
1200 {
1201 let label = node.label.as_deref().unwrap_or("");
1202 self.a11y.announce(&format!("Activated {}", label));
1203 }
1204 }
1205 _ => {}
1206 }
1207 }
1208 }
1209
1210 WindowEvent::Ime(ime) => {
1211 if let Some(focused_id) = self.rt.sched.focused {
1212 let key = self.tf_key_of(focused_id);
1213 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1214 let mut state = state_rc.borrow_mut();
1215 let on_text_change = self.rt
1216 .frame_cache
1217 .as_ref()
1218 .and_then(|f| f.hit_regions.iter().find(|h| h.id == focused_id))
1219 .and_then(|h| h.on_text_change.clone());
1220 let mut notify = |text: String| {
1221 if let Some(cb) = &on_text_change {
1222 cb(text);
1223 }
1224 };
1225 rc_android::handle_ime_event(
1226 ime,
1227 &mut state,
1228 &mut notify,
1229 &mut self.rt.ime_preedit,
1230 );
1231 self.request_redraw();
1232 }
1233 }
1234 }
1235
1236 WindowEvent::RedrawRequested => {
1237 if !self.redraw_requested.replace(false) {
1239 self.process_a11y_actions();
1240 self.process_render_commands();
1241 if let (Some(backend), Some(frame)) =
1243 (self.backend.as_mut(), self.rt.frame_cache.as_ref())
1244 {
1245 let scale = self.window.as_ref().map(|w| w.scale_factor() as f32).unwrap_or(1.0);
1246 let mut scene = frame.scene.clone();
1247 if let Some(inspector) = &mut self.inspector {
1248 inspector.frame(&mut scene);
1249 }
1250 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1251 }
1252 log::trace!("RedrawRequested: no frame request, skipping compose");
1253 return;
1254 }
1255 log::trace!("RedrawRequested: frame request pending, composing");
1256
1257 self.process_a11y_actions();
1259 self.process_render_commands();
1260
1261 let Some(win) = self.window.as_ref() else {
1262 return;
1263 };
1264 if self.backend.is_none() {
1265 return;
1266 }
1267
1268 repose_core::animation_driver::tick();
1271
1272 let t0 = Instant::now();
1273 let scale = win.scale_factor() as f32;
1274 self.rt.scale = scale;
1275 let focused = self.rt.sched.focused;
1276
1277 let output = self.rt.frame(&mut self.root, &self.render);
1278
1279 if let Some(cursor) = &output.platform.cursor {
1281 win.set_cursor(winit::window::Cursor::Icon(map_cursor(*cursor)));
1282 }
1283
1284 if !output.wants_keyboard && focused.is_some() && self.rt.sched.focused.is_none() && self.rt.ime_preedit {
1286 rc_web::set_ime_for_textfield(win, false);
1287 self.rt.ime_preedit = false;
1288 }
1289
1290 let frame = Frame {
1291 scene: output.scene,
1292 hit_regions: output.hit_regions,
1293 semantics_nodes: output.semantics_nodes,
1294 focus_chain: output.focus_chain,
1295 };
1296
1297 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1298
1299 if let Some(adapter) = &mut self.accesskit_adapter {
1301 let win = self.window.as_ref().unwrap();
1302 let scale = win.scale_factor();
1303 if let Some(update) =
1304 self.a11y_tree
1305 .update(&frame.semantics_nodes, scale, self.rt.sched.focused)
1306 {
1307 adapter.update_if_active(|| update);
1308 }
1309 }
1310
1311 let mut scene = frame.scene.clone();
1313 if let Some(inspector) = &mut self.inspector {
1315 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1316 let signal_count = self.rt.sched.id_count() as usize;
1317 let ls = repose_ui::last_layout_stats();
1318 inspector.hud.metrics = Some(repose_devtools::Metrics {
1319 build_ms: build_layout_ms,
1320 layout_ms: ls.layout_time_ms,
1321 paint_ms: ls.paint_time_ms,
1322 scene_nodes: scene.nodes.len(),
1323 widget_count,
1324 signal_count,
1325 taffy_created: ls.taffy_created,
1326 taffy_reused: ls.taffy_reused,
1327 layout_hits: ls.layout_hits,
1328 layout_misses: ls.layout_misses,
1329 paint_cache_hits: ls.paint_cache_hits,
1330 paint_cache_misses: ls.paint_cache_misses,
1331 paint_culled: ls.paint_culled,
1332 });
1333 inspector.frame(&mut scene);
1334 }
1335
1336 repose_core::dnd::overlay_drag_indicator(
1338 &mut scene,
1339 self.rt.mouse_pos_px,
1340 self.external_file_drag,
1341 );
1342
1343 self.process_render_commands();
1346
1347 let win = self.window.as_ref().unwrap();
1349 let scale = win.scale_factor() as f32;
1350 if let Some(backend) = self.backend.as_mut() {
1351 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1352 }
1353
1354 if let Some(fid) = self.rt.sched.focused {
1357 if let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
1358 && let Some(key) = hit.tf_state_key
1359 && !self.rt.textfield_states.contains_key(&key)
1360 {
1361 self.rt.textfield_states
1362 .entry(key)
1363 .or_insert_with(|| {
1364 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1365 })
1366 .borrow_mut()
1367 .reset_caret_blink();
1368 }
1369 }
1370
1371 self.rt.reconcile_hover_from_mouse_pos(&frame);
1372 repose_core::dnd::set_dnd_frame(Some(frame.clone()));
1373 self.rt.frame_cache = Some(frame);
1374 repose_core::dnd::set_dnd_scale(scale);
1375
1376 self.dispatch_file_drop_now();
1377
1378 rc::tick_snackbar(self.last_redraw);
1379 self.last_redraw = Instant::now();
1380 }
1381
1382 _ => {}
1383 }
1384 }
1385
1386 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1387 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1390 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK.lock().unwrap().as_ref() {
1391 cb();
1392 }
1393 process_deeplinks();
1394
1395 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1398 if WINDOW_VISIBLE.load(Ordering::Relaxed) && self.backend.is_none() {
1399 if let Some(w) = &self.window {
1400 log::info!("about_to_wait: recreating GPU backend");
1401 match repose_render_wgpu::WgpuBackend::new_with_msaa(
1402 w.clone(),
1403 self.msaa_samples,
1404 ) {
1405 Ok(b) => self.backend = Some(b),
1406 Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
1407 }
1408 }
1409 }
1410
1411 let needs_compose = take_frame_request();
1412 let needs_present = take_present_request();
1413
1414 if needs_compose {
1415 self.pending_redraw = true;
1416 }
1417
1418 if !self.pending_redraw && needs_present && self.rt.frame_cache.is_some() {
1420 let now = Instant::now();
1421 let interval = web_time::Duration::from_millis(16);
1422 if now.saturating_duration_since(self.last_redraw) >= interval {
1423 rc::request_redraw(&self.window);
1424 self.last_redraw = now;
1425 } else {
1426 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1427 self.last_redraw + interval,
1428 ));
1429 }
1430 return;
1431 }
1432
1433 if !self.pending_redraw {
1434 let now = Instant::now();
1435 let idle_cap = web_time::Duration::from_millis(1000);
1436 let deadline = self
1437 .next_caret_blink_deadline()
1438 .unwrap_or(now + idle_cap);
1439
1440 if now.saturating_duration_since(self.last_redraw) >= idle_cap || now >= deadline {
1441 self.redraw_requested.set(true);
1442 request_frame();
1443 rc::request_redraw(&self.window);
1444 self.last_redraw = now;
1445 }
1446 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1447 Ord::min(deadline, now + idle_cap),
1448 ));
1449 return;
1450 }
1451
1452 let now = Instant::now();
1453 let interval = web_time::Duration::from_millis(16);
1454
1455 if now.saturating_duration_since(self.last_redraw) >= interval {
1456 self.pending_redraw = false;
1457 self.redraw_requested.set(true);
1458 rc::request_redraw(&self.window);
1459 self.last_redraw = now;
1460 } else {
1461 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1462 self.last_redraw + interval,
1463 ));
1464 }
1465 }
1466
1467 fn new_events(
1468 &mut self,
1469 _: &winit::event_loop::ActiveEventLoop,
1470 _: winit::event::StartCause,
1471 ) {
1472 }
1473 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1474 self.pending_redraw = true;
1475 }
1476 fn device_event(
1477 &mut self,
1478 _: &winit::event_loop::ActiveEventLoop,
1479 _: winit::event::DeviceId,
1480 _: winit::event::DeviceEvent,
1481 ) {
1482 }
1483 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1484 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1485 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1486 }
1487
1488 impl App {
1489 fn dispatch_focus_key_event(
1492 &self,
1493 key_event: &winit::event::KeyEvent,
1494 mapped_key: &repose_core::input::Key,
1495 ) -> bool {
1496 let Some(f) = &self.rt.frame_cache else {
1497 return false;
1498 };
1499 let Some(focused) = self.rt.sched.focused else {
1500 return false;
1501 };
1502 let utf16 = match mapped_key {
1503 repose_core::input::Key::Character(c) => *c as u16,
1504 _ => 0,
1505 };
1506 let mods = self.rt.modifiers;
1507 let repeat = key_event.repeat;
1508 let ev_type = if key_event.state == ElementState::Pressed {
1509 repose_core::input::KeyEventType::Down
1510 } else {
1511 repose_core::input::KeyEventType::Up
1512 };
1513 let hit_by_id: std::collections::HashMap<u64, &HitRegion> =
1514 f.hit_regions.iter().map(|h| (h.id, h)).collect();
1515 let sem_parent_of: std::collections::HashMap<u64, u64> = f
1516 .semantics_nodes
1517 .iter()
1518 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1519 .collect();
1520 let mut ancestors = Vec::new();
1521 let mut cur = focused;
1522 loop {
1523 ancestors.push(cur);
1524 if let Some(&p) = sem_parent_of.get(&cur) {
1525 cur = p;
1526 } else {
1527 break;
1528 }
1529 }
1530 let make_ke = || repose_core::input::KeyEvent {
1531 key: mapped_key.clone(),
1532 modifiers: mods,
1533 is_repeat: repeat,
1534 event_type: ev_type,
1535 utf16_code_point: utf16,
1536 };
1537 for &id in ancestors.iter().rev() {
1539 if let Some(hit) = hit_by_id.get(&id) {
1540 if let Some(cb) = &hit.on_preview_key_event {
1541 if cb(make_ke()) {
1542 return true;
1543 }
1544 }
1545 }
1546 }
1547 for &id in ancestors.iter() {
1549 if let Some(hit) = hit_by_id.get(&id) {
1550 if let Some(cb) = &hit.on_key_event {
1551 if cb(make_ke()) {
1552 return true;
1553 }
1554 }
1555 }
1556 }
1557 false
1558 }
1559
1560 fn announce_focus_change(&mut self) {
1561 if let Some(f) = &self.rt.frame_cache {
1562 let focused_node = self.rt
1563 .sched
1564 .focused
1565 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1566 self.a11y.focus_changed(focused_node);
1567 }
1568 }
1569
1570 fn notify_text_change(&self, id: u64, text: String) {
1571 if let Some(f) = &self.rt.frame_cache
1572 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1573 && let Some(cb) = &h.on_text_change
1574 {
1575 cb(text);
1576 }
1577 }
1578
1579 fn tf_key_of(&self, visual_id: u64) -> u64 {
1580 rc::tf_key_of_in_frame(&self.rt.frame_cache, visual_id)
1581 }
1582
1583 fn next_caret_blink_deadline(&self) -> Option<Instant> {
1586 next_caret_blink_deadline(&self.rt.sched, &self.rt.frame_cache, &self.rt.textfield_states)
1587 }
1588
1589 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1590 use repose_core::shortcuts;
1591
1592 if let (Some(f), Some(fid)) = (&self.rt.frame_cache, self.rt.sched.focused)
1593 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1594 && let Some(cb) = &hit.on_action
1595 && cb(action.clone())
1596 {
1597 return true;
1598 }
1599
1600 if shortcuts::handle(action.clone()) {
1601 return true;
1602 }
1603
1604 if let Some(f) = &self.rt.frame_cache {
1606 if let Some(new_id) = 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.textfield_states.entry(key).or_insert_with(|| {
1618 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1619 });
1620 if let Some(state_rc) = self.rt.textfield_states.get(&key) {
1621 state_rc.borrow_mut().reset_caret_blink();
1622 }
1623 }
1624 if let Some(win) = &self.window {
1625 let is_textfield = f.semantics_nodes.iter().any(|n| {
1626 n.id == new_id && n.role == repose_core::semantics::Role::TextField
1627 });
1628 rc_web::set_ime_for_textfield(win, is_textfield);
1629 }
1630 self.announce_focus_change();
1631 return true;
1632 }
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}