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, caret_xy_for_byte, measure_text,
9};
10use std::cell::{Cell, RefCell};
11use std::rc::Rc;
12use std::sync::atomic::{AtomicBool, 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
63pub fn set_on_deeplink(callback: Box<dyn Fn(Vec<u8>) + Send>) {
65 *DEEPLINK_CB.lock().unwrap() = Some(callback);
66}
67
68pub fn push_deeplink(data: Vec<u8>) {
70 PENDING_DEEPLINKS.lock().unwrap().push(data);
71 #[cfg(not(target_arch = "wasm32"))]
72 if let Some(proxy) = EVENT_LOOP_PROXY.get() {
73 let _ = proxy.send_event(());
74 }
75}
76
77pub(crate) fn process_deeplinks() {
80 let mut queue = PENDING_DEEPLINKS.lock().unwrap();
81 if queue.is_empty() {
82 return;
83 }
84 let batch = std::mem::take(&mut *queue);
85 drop(queue);
86
87 if let Some(cb) = DEEPLINK_CB.lock().unwrap().as_ref() {
88 for data in batch {
89 cb(data);
90 }
91 }
92}
93
94#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
96pub fn set_app_window(window: Arc<Window>) {
97 let _ = APP_WINDOW.set(window);
98}
99
100#[cfg(not(target_arch = "wasm32"))]
102pub fn set_event_loop_proxy(proxy: winit::event_loop::EventLoopProxy<()>) {
103 let _ = EVENT_LOOP_PROXY.set(proxy);
104}
105
106#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
108pub fn set_about_to_wait_callback(cb: Box<dyn Fn() + Send>) {
109 *ABOUT_TO_WAIT_CALLBACK.lock().unwrap() = Some(cb);
110}
111
112#[cfg(not(target_arch = "wasm32"))]
114pub fn wake_event_loop() {
115 if let Some(proxy) = EVENT_LOOP_PROXY.get() {
116 let _ = proxy.send_event(());
117 }
118}
119
120#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
124pub fn show_app_window() {
125 WINDOW_VISIBLE.store(true, Ordering::Relaxed);
126 if let Some(w) = APP_WINDOW.get() {
127 log::info!("show_app_window: calling set_visible(true)");
128 w.set_visible(true);
129 #[allow(deprecated)]
130 w.focus_window();
131 }
132 repose_core::frame_clock::request_frame();
133 wake_event_loop();
134}
135
136#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
137pub fn hide_app_window() {
138 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
139 if let Some(w) = APP_WINDOW.get() {
140 log::info!("hide_app_window: calling set_visible(false)");
141 w.set_visible(false);
142 }
143 repose_core::frame_clock::request_frame();
144 wake_event_loop();
145}
146
147#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
149pub fn window_is_visible() -> bool {
150 WINDOW_VISIBLE.load(Ordering::Relaxed)
151}
152
153#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
156pub fn set_close_to_tray(enabled: bool) {
157 CLOSE_TO_TRAY.store(enabled, Ordering::Relaxed);
158}
159
160pub fn compose_frame<F>(
162 sched: &mut Scheduler,
163 root_fn: &mut F,
164 scale: f32,
165 size_px_u32: (u32, u32),
166 hover_id: Option<u64>,
167 pressed_ids: &std::collections::HashSet<u64>,
168 tf_states: &std::collections::HashMap<u64, Rc<RefCell<repose_ui::TextFieldState>>>,
169 _focused: Option<u64>,
170) -> Frame
171where
172 F: FnMut(&mut Scheduler) -> View,
173{
174 if let Some(requested_id) = repose_core::take_focus_request() {
176 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
177 sched.focused = None;
178 } else {
179 sched.focused = Some(requested_id);
180 }
181 }
182
183 set_density_default(Density { scale });
184
185 let current_focused = sched.focused;
187
188 let frame = sched.repose(
189 {
190 let scale = scale;
191 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s))
192 },
193 {
194 let hover_id = hover_id;
195 let pressed_ids = pressed_ids.clone();
196 move |view, _size| {
197 let interactions = repose_ui::Interactions {
198 hover: hover_id,
199 pressed: pressed_ids.clone(),
200 };
201
202 with_density(Density { scale }, || {
203 repose_ui::layout_and_paint(
204 view,
205 size_px_u32,
206 tf_states,
207 &interactions,
208 current_focused,
209 )
210 })
211 }
212 },
213 );
214
215 if let Some(fid) = sched.focused {
216 if !frame.focus_chain.contains(&fid) {
217 sched.focused = None;
218 }
219 }
220
221 frame
222}
223
224pub fn tf_ensure_visible_in_rect(state: &mut repose_ui::TextFieldState, inner_rect: Rect) {
226 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
227 let m = measure_text(&state.text, font_px, None, 400, 0);
228 let caret_x_px = m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
229 state.ensure_caret_visible(
230 caret_x_px,
231 inner_rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
232 dp_to_px(2.0),
233 );
234}
235
236#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
237fn map_cursor(c: repose_core::CursorIcon) -> winit::window::CursorIcon {
238 use winit::window::CursorIcon as W;
239 match c {
240 repose_core::CursorIcon::Default => W::Default,
241 repose_core::CursorIcon::Pointer => W::Pointer,
242 repose_core::CursorIcon::Text => W::Text,
243 repose_core::CursorIcon::EwResize => W::EwResize,
244 repose_core::CursorIcon::NsResize => W::NsResize,
245 repose_core::CursorIcon::Grab => W::Grab,
246 repose_core::CursorIcon::Grabbing => W::Grabbing,
247 }
248}
249
250#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
251pub fn run_desktop_app(
252 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
253) -> anyhow::Result<()> {
254 use std::collections::{HashMap, HashSet};
255 use winit::application::ApplicationHandler;
256 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
257 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
258 use winit::event_loop::EventLoop;
259 use winit::keyboard::{KeyCode, PhysicalKey};
260 use winit::window::{Window, WindowAttributes};
261
262 use crate::a11y::A11yTree;
263
264 struct ReposeActivationHandler {
265 initial_tree: Option<accesskit::TreeUpdate>,
266 }
267
268 impl accesskit::ActivationHandler for ReposeActivationHandler {
269 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
270 self.initial_tree.take()
271 }
272 }
273
274 struct ReposeDeactivationHandler;
275
276 impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
277 fn deactivate_accessibility(&mut self) {
278 }
280 }
281
282 struct App {
283 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
284 render: RenderContext,
285 window: Option<Arc<Window>>,
286 backend: Option<repose_render_wgpu::WgpuBackend>,
287 sched: Scheduler,
288 inspector: repose_devtools::Inspector,
289 frame_cache: Option<Frame>,
290 mouse_pos_px: (f32, f32),
291 modifiers: Modifiers,
292 textfield_states: HashMap<u64, Rc<RefCell<TextFieldState>>>,
293 ime_preedit: bool,
294 hover_id: Option<u64>,
295 capture_id: Option<u64>,
296 pressed_ids: HashSet<u64>,
297
298 pending_dropped_files: Vec<std::path::PathBuf>,
300 pending_drop_pos_px: Option<(f32, f32)>,
301
302 external_file_drag: bool,
304 hovered_files: Vec<std::path::PathBuf>,
305
306 key_pressed_active: Option<u64>,
307 clipboard: Option<clipawl::Clipboard>,
308 a11y: Box<dyn A11yBridge>,
309 last_focus: Option<u64>,
310
311 accesskit_adapter: Option<Adapter>,
312 a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
313 a11y_tree: A11yTree,
314
315 last_redraw: Instant,
316 pending_redraw: bool,
317
318 redraw_requested: Cell<bool>,
320 }
321
322 impl App {
323 fn process_a11y_actions(&mut self) {
324 let mut actions = self.a11y_actions.lock().unwrap();
325 if actions.is_empty() {
326 return;
327 }
328 let pending = actions.drain(..).collect::<Vec<_>>();
329 drop(actions);
330
331 let Some(f) = &self.frame_cache else {
332 return;
333 };
334
335 for req in pending {
336 let target_id = req.target_node.0;
337 match req.action {
338 accesskit::Action::Click => {
339 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
340 && let Some(cb) = &hit.on_click
341 {
342 cb();
343 self.request_redraw();
344 }
345 }
346 accesskit::Action::Focus => {
347 self.sched.focused = Some(target_id);
348 self.request_redraw();
349 }
350 _ => {}
351 }
352 }
353 }
354
355 fn new(root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>) -> Self {
356 Self {
357 root,
358 render: RenderContext::new(),
359 window: None,
360 backend: None,
361 sched: Scheduler::new(),
362 inspector: repose_devtools::Inspector::new(),
363 frame_cache: None,
364 mouse_pos_px: (0.0, 0.0),
365 modifiers: Modifiers::default(),
366 textfield_states: HashMap::new(),
367 ime_preedit: false,
368 hover_id: None,
369 capture_id: None,
370 pressed_ids: HashSet::new(),
371 pending_dropped_files: Vec::new(),
372 pending_drop_pos_px: None,
373
374 external_file_drag: false,
375 hovered_files: Vec::new(),
376
377 key_pressed_active: None,
378 clipboard: None,
379 a11y: {
380 #[cfg(target_os = "linux")]
381 {
382 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
383 }
384 #[cfg(not(target_os = "linux"))]
385 {
386 Box::new(NoopA11y) as Box<dyn A11yBridge>
387 }
388 },
389 last_focus: None,
390
391 accesskit_adapter: None,
392 a11y_actions: Arc::new(Mutex::new(Vec::new())),
393 a11y_tree: A11yTree::default(),
394
395 last_redraw: Instant::now(),
396 pending_redraw: false,
397 redraw_requested: Cell::new(false),
398 }
399 }
400
401 fn request_redraw(&self) {
402 self.redraw_requested.set(true);
403 repose_core::request_frame();
404 rc::request_redraw(&self.window);
405 }
406
407 fn tf_ensure_caret_visible(st: &mut TextFieldState, is_multiline: bool) {
409 rc::tf_ensure_caret_visible(st, is_multiline);
410 }
411
412 fn paste_from_primary(&self) -> Option<String> {
413 let opts = clipawl::ClipboardOptions {
414 linux: clipawl::LinuxOptions {
415 selection: clipawl::LinuxSelection::Primary,
416 ..Default::default()
417 },
418 };
419 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
420 match pollster::block_on(cb.read()) {
421 Ok(t) => Some(t),
422 Err(e) => {
423 eprintln!("Primary paste error: {}", e);
424 None
425 }
426 }
427 } else {
428 None
429 }
430 }
431
432 fn process_render_commands(&mut self) {
433 let Some(backend) = self.backend.as_mut() else {
434 return;
435 };
436 rc::process_render_commands(backend, self.render.drain());
437 }
438
439 fn reset_pointer_state(&mut self) {
440 self.capture_id = None;
441 self.pressed_ids.clear();
442 self.hover_id = None;
443 }
444
445 fn is_textfield(&self, id: u64) -> bool {
446 rc::is_textfield_in_frame(&self.frame_cache, id)
447 }
448
449 fn is_multiline_id(&self, id: u64) -> bool {
450 if let Some(f) = &self.frame_cache {
451 f.hit_regions
452 .iter()
453 .find(|h| h.id == id)
454 .map(|h| h.tf_multiline)
455 .unwrap_or(false)
456 } else {
457 false
458 }
459 }
460
461 fn hit_by_id(f: &Frame, id: u64) -> Option<&HitRegion> {
462 f.hit_regions.iter().find(|h| h.id == id)
463 }
464
465 fn dp_px(&self, dp: f32) -> f32 {
466 dp_to_px(dp)
467 }
468 }
469
470 impl ApplicationHandler<()> for App {
471 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
472 self.clipboard = clipawl::Clipboard::new()
473 .map_err(|e| {
474 eprintln!("clipawl clipboard init failed: {e}");
475 e
476 })
477 .ok();
478 repose_core::clipboard::set_clipboard_read_fn(Box::new(|| {
479 clipawl::blocking::read().ok()
480 }));
481 repose_core::clipboard::set_clipboard_fn(Box::new(move |text| {
483 if let Err(e) = clipawl::blocking::write(text) {
484 eprintln!("clipboard write error: {e}");
485 }
486 }));
487
488 repose_core::clipboard::set_primary_fn(Box::new(|text| {
489 let opts = clipawl::ClipboardOptions {
490 linux: clipawl::LinuxOptions {
491 selection: clipawl::LinuxSelection::Primary,
492 ..Default::default()
493 },
494 };
495 match clipawl::Clipboard::new_with_options(opts) {
496 Ok(cb) => {
497 if let Err(e) = pollster::block_on(cb.write(text)) {
498 eprintln!("primary selection write error: {e}");
499 }
500 }
501 Err(e) => eprintln!("primary clipboard init error: {e}"),
502 }
503 }));
504
505 if self.window.is_none() {
506 match el.create_window(
507 WindowAttributes::default()
508 .with_title("Repose")
509 .with_inner_size(PhysicalSize::new(1280, 800))
510 .with_visible(false),
511 ) {
512 Ok(win) => {
513 let w = Arc::new(win);
514
515 let activation_handler = ReposeActivationHandler {
516 initial_tree: Some(A11yTree::initial_tree()),
517 };
518
519 let action_handler = ReposeActionHandler {
520 pending_actions: self.a11y_actions.clone(),
521 };
522
523 let deactivation_handler = ReposeDeactivationHandler;
524
525 let adapter = Adapter::with_direct_handlers(
526 el,
527 &w,
528 activation_handler,
529 action_handler,
530 deactivation_handler,
531 );
532
533 self.accesskit_adapter = Some(adapter);
534
535 w.set_visible(true);
536
537 let size = w.inner_size();
538 self.sched.size = (size.width, size.height);
539
540 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
541 Ok(b) => {
542 self.backend = Some(b);
543 set_app_window(w.clone());
544 self.window = Some(w);
545 self.request_redraw();
546 }
547 Err(e) => {
548 log::error!("Failed to create WGPU backend: {e:?}");
549 el.exit();
550 }
551 }
552 }
553 Err(e) => {
554 log::error!("Failed to create window: {e:?}");
555 el.exit();
556 }
557 }
558 }
559 }
560
561 fn window_event(
562 &mut self,
563 el: &winit::event_loop::ActiveEventLoop,
564 _id: winit::window::WindowId,
565 event: WindowEvent,
566 ) {
567 if let Some(adapter) = &mut self.accesskit_adapter {
569 adapter.process_event(self.window.as_ref().unwrap(), &event);
570 }
571
572 match event {
573 WindowEvent::CloseRequested => {
574 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
575 self.backend = None;
577 if let Some(w) = &self.window {
578 w.set_visible(false);
579 }
580 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
581 } else {
582 el.exit();
583 }
584 }
585
586 WindowEvent::Focused(false) => {
587 repose_core::dnd::handle_drag_action(
589 &repose_core::shortcuts::DragAction::Cancel,
590 );
591
592 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
594 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
595 if let Some(cb) = &hit.on_pointer_cancel {
596 let pos = repose_core::Vec2 {
597 x: self.mouse_pos_px.0,
598 y: self.mouse_pos_px.1,
599 };
600 let pe = PointerEvent::new(
601 PointerId(0),
602 PointerKind::Mouse,
603 PointerEventKind::Cancel,
604 pos,
605 1.0,
606 self.modifiers,
607 );
608 cb(pe);
609 }
610 }
611 }
612
613 self.external_file_drag = false;
615 self.hovered_files.clear();
616 self.reset_pointer_state();
617
618 if let Some(w) = &self.window {
619 rc_web::set_ime_for_textfield(w, false);
620 }
621 self.ime_preedit = false;
622
623 self.request_redraw();
624 }
625
626 WindowEvent::HoveredFile(path) => {
627 self.external_file_drag = true;
629 if self.hovered_files.len() < 32 {
630 self.hovered_files.push(path);
631 }
632 if self.pending_drop_pos_px.is_none() {
634 self.pending_drop_pos_px = Some(self.mouse_pos_px);
635 }
636 self.request_redraw();
637 }
638
639 WindowEvent::HoveredFileCancelled => {
640 self.external_file_drag = false;
641 self.hovered_files.clear();
642
643 self.reset_pointer_state();
645
646 self.request_redraw();
647 }
648
649 WindowEvent::DroppedFile(path) => {
650 self.pending_dropped_files.push(path);
652 if self.pending_drop_pos_px.is_none() {
653 self.pending_drop_pos_px = Some(self.mouse_pos_px);
654 }
655
656 self.external_file_drag = false;
658 self.hovered_files.clear();
659
660 self.request_redraw();
661 }
662
663 WindowEvent::Resized(size) => {
664 self.sched.size = (size.width, size.height);
665 if let Some(b) = self.backend.as_mut() {
666 b.configure_surface(size.width, size.height);
667 }
668 if let Some(w) = &self.window {
669 let sf = w.scale_factor() as f32;
670 let dp_w = size.width as f32 / sf;
671 let dp_h = size.height as f32 / sf;
672 log::info!(
673 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
674 size.width,
675 size.height,
676 sf,
677 dp_w as i32,
678 dp_h as i32
679 );
680 }
681 self.request_redraw();
682 }
683
684 WindowEvent::CursorMoved { position, .. } => {
685 self.mouse_pos_px = (position.x as f32, position.y as f32);
686
687 if self.external_file_drag {
688 self.pending_drop_pos_px = Some(self.mouse_pos_px);
689 }
690
691 let pos = Vec2 {
692 x: self.mouse_pos_px.0,
693 y: self.mouse_pos_px.1,
694 };
695
696 if repose_core::dnd::handle_drag_action(
697 &repose_core::shortcuts::DragAction::Move {
698 position: pos,
699 modifiers: self.modifiers,
700 },
701 ) {
702 self.request_redraw();
703 return;
704 }
705
706 if self.inspector.hud.inspector_enabled
708 && let Some(f) = &self.frame_cache
709 {
710 let hit = f.hit_regions.iter().find(|h| {
711 h.rect.contains(Vec2 {
712 x: self.mouse_pos_px.0,
713 y: self.mouse_pos_px.1,
714 })
715 });
716 let hover_rect = hit.map(|h| h.rect);
717 let hover_info = hit.and_then(|h| {
718 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
719 repose_devtools::HoveredInfo {
720 id: s.id,
721 role: format!("{:?}", s.role),
722 label: s.label.clone(),
723 }
724 })
725 });
726 self.inspector.hud.set_hovered(hover_rect, hover_info);
727 self.request_redraw();
728 }
729
730 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
732 && self.is_textfield(cid)
733 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
734 {
735 let key = self.tf_key_of(cid);
736 if let Some(state_rc) = self.textfield_states.get(&key) {
737 let mut st = state_rc.borrow_mut();
738
739 let content_x =
740 (self.mouse_pos_px.0 - hit.rect.x + st.scroll_offset).max(0.0);
741 let content_y =
742 (self.mouse_pos_px.1 - hit.rect.y + st.scroll_offset_y).max(0.0);
743
744 let font_px =
745 dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
746
747 let idx = if hit.tf_multiline {
748 rc::index_for_xy_bytes_vt(
749 &st, font_px, hit.rect.w, content_x, content_y,
750 )
751 } else {
752 rc::index_for_x_bytes_vt(&st, font_px, content_x)
753 };
754
755 st.drag_to(idx);
756
757 if hit.tf_multiline {
759 let (cx, cy, _) =
760 caret_xy_for_byte(&st.text, font_px, hit.rect.w, st.caret_index());
761 st.ensure_caret_visible_xy(cx, cy, hit.rect.w, hit.rect.h, dp_to_px(2.0));
762 } else {
763 let m = measure_text(&st.text, font_px, None, 400, 0);
764 let cx = m.positions.get(st.caret_index()).copied().unwrap_or(0.0);
765 st.ensure_caret_visible(cx, hit.rect.w, dp_to_px(2.0));
766 }
767
768 self.request_redraw();
769 }
770 }
771
772 if let Some(f) = &self.frame_cache {
774 let pos = Vec2 {
776 x: self.mouse_pos_px.0,
777 y: self.mouse_pos_px.1,
778 };
779 let top = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos));
780
781 if let Some(win) = &self.window {
783 let c = top
784 .and_then(|h| h.cursor)
785 .unwrap_or(repose_core::CursorIcon::Default);
786 win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
787 }
788
789 let new_hover = top.map(|h| h.id);
790
791 if new_hover != self.hover_id {
793 if let Some(prev_id) = self.hover_id
794 && let Some(prev) = f.hit_regions.iter().find(|h| h.id == prev_id)
795 && let Some(cb) = &prev.on_pointer_leave
796 {
797 let pe = PointerEvent::new(
798 PointerId(0),
799 PointerKind::Mouse,
800 PointerEventKind::Leave,
801 pos,
802 1.0,
803 self.modifiers,
804 );
805 cb(pe);
806 }
807 if let Some(h) = top
808 && let Some(cb) = &h.on_pointer_enter
809 {
810 let pe = PointerEvent::new(
811 PointerId(0),
812 PointerKind::Mouse,
813 PointerEventKind::Enter,
814 pos,
815 1.0,
816 self.modifiers,
817 );
818 cb(pe);
819 }
820 self.hover_id = new_hover;
821 self.request_redraw();
822 }
823
824 let pe = PointerEvent::new(
826 PointerId(0),
827 PointerKind::Mouse,
828 PointerEventKind::Move,
829 pos,
830 1.0,
831 self.modifiers,
832 );
833
834 if let Some(cid) = self.capture_id {
836 if let Some(h) = f.hit_regions.iter().find(|h| h.id == cid)
837 && let Some(cb) = &h.on_pointer_move
838 {
839 cb(pe.clone());
840 }
841 } else if let Some(h) = &top
842 && let Some(cb) = &h.on_pointer_move
843 {
844 cb(pe);
845 }
846 }
847 }
848
849 WindowEvent::MouseWheel { delta, .. } => {
850 let (dx_px, dy_px) = match delta {
852 MouseScrollDelta::LineDelta(x, y) => {
853 let unit_px = dp_to_px(60.0);
854 (-(x * unit_px), -(y * unit_px))
855 }
856 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
857 };
858 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
859
860 if let Some(f) = &self.frame_cache {
861 let pos = Vec2 {
862 x: self.mouse_pos_px.0,
863 y: self.mouse_pos_px.1,
864 };
865
866 if rc::dispatch_scroll(f, pos, Vec2 { x: dx_px, y: dy_px }, None).0 {
867 self.request_redraw();
868 }
869 }
870 }
871
872 WindowEvent::MouseInput {
873 state: ElementState::Pressed,
874 button: MouseButton::Left,
875 ..
876 } => {
877 let mut need_announce = false;
878 if let Some(f) = &self.frame_cache {
879 let pos = Vec2 {
880 x: self.mouse_pos_px.0,
881 y: self.mouse_pos_px.1,
882 };
883 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
884 {
885 repose_core::dnd::handle_drag_action(
886 &repose_core::shortcuts::DragAction::Press {
887 position: Vec2 {
888 x: self.mouse_pos_px.0,
889 y: self.mouse_pos_px.1,
890 },
891 capture_id: hit.id,
892 kind: repose_core::input::PointerKind::Mouse,
893 modifiers: self.modifiers,
894 },
895 );
896
897 self.capture_id = Some(hit.id);
899
900 if self.is_textfield(hit.id) {
902 let key = self.tf_key_of(hit.id);
903 self.textfield_states.entry(key).or_insert_with(|| {
904 Rc::new(RefCell::new(TextFieldState::new()))
905 });
906 if let Some(st_rc) = self.textfield_states.get(&key) {
907 let mut st = st_rc.borrow_mut();
908 let content_x =
909 (self.mouse_pos_px.0 - hit.rect.x + st.scroll_offset)
910 .max(0.0);
911 let content_y = (self.mouse_pos_px.1 - hit.rect.y
912 + st.scroll_offset_y)
913 .max(0.0);
914 let font_px = self.dp_px(TF_FONT_DP)
915 * repose_core::locals::text_scale().0;
916
917 let idx = if hit.tf_multiline {
918 rc::index_for_xy_bytes_vt(
919 &st,
920 font_px,
921 hit.rect.w,
922 content_x,
923 content_y,
924 )
925 } else {
926 rc::index_for_x_bytes_vt(&st, font_px, content_x)
927 };
928
929 st.begin_drag(idx, self.modifiers.shift);
930
931 let caret_idx = st.caret_index();
933 let iw = st.inner_width;
934 let ih = st.inner_height;
935 if hit.tf_multiline {
936 let (cx, cy, _) = textfield::caret_xy_for_byte(
937 &st.text, font_px, hit.rect.w, caret_idx,
938 );
939 st.ensure_caret_visible_xy(cx, cy, iw, ih, self.dp_px(2.0));
940 } else {
941 let m = measure_text(&st.text, font_px, None, 400, 0);
942 let cx = m.positions.get(caret_idx).copied().unwrap_or(0.0);
943 st.ensure_caret_visible(cx, iw, self.dp_px(2.0));
944 }
945 }
946 }
947 self.pressed_ids.insert(hit.id);
949 self.request_redraw();
951
952 if hit.focusable {
954 self.sched.focused = Some(hit.id);
955 need_announce = true;
956 let key = self.tf_key_of(hit.id);
957 self.textfield_states.entry(key).or_insert_with(|| {
958 Rc::new(RefCell::new(TextFieldState::new()))
959 });
960 if let Some(win) = &self.window {
961 let sf = win.scale_factor();
962 rc_web::set_ime_for_textfield(win, true);
963 win.set_ime_cursor_area(
964 LogicalPosition::new(
965 hit.rect.x as f64 / sf,
966 hit.rect.y as f64 / sf,
967 ),
968 LogicalSize::new(
969 hit.rect.w as f64 / sf,
970 hit.rect.h as f64 / sf,
971 ),
972 );
973 }
974 }
975
976 if let Some(cb) = &hit.on_pointer_down {
978 let pe = PointerEvent::new(
979 PointerId(0),
980 PointerKind::Mouse,
981 PointerEventKind::Down(PointerButton::Primary),
982 pos,
983 1.0,
984 self.modifiers,
985 );
986 cb(pe);
987 }
988
989 if need_announce {
990 self.announce_focus_change();
991 }
992
993 self.request_redraw();
994 } else {
995 if self.ime_preedit {
997 if let Some(win) = &self.window {
998 rc_web::set_ime_for_textfield(win, false);
999 }
1000 self.ime_preedit = false;
1001 }
1002 self.sched.focused = None;
1003 self.request_redraw();
1004 }
1005 }
1006 }
1007
1008 WindowEvent::MouseInput {
1009 state: ElementState::Pressed,
1010 button: MouseButton::Middle,
1011 ..
1012 } => {
1013 let Some(f) = &self.frame_cache else {
1014 return;
1015 };
1016 let pos = Vec2 {
1017 x: self.mouse_pos_px.0,
1018 y: self.mouse_pos_px.1,
1019 };
1020 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos)) {
1021 if let Some(cb) = &hit.on_pointer_down {
1023 cb(PointerEvent::new(
1024 PointerId(0),
1025 PointerKind::Mouse,
1026 PointerEventKind::Down(PointerButton::Tertiary),
1027 pos,
1028 1.0,
1029 self.modifiers,
1030 ));
1031 }
1032 if self.is_textfield(hit.id) {
1034 let key = self.tf_key_of(hit.id);
1035 if let Some(state_rc) = self.textfield_states.get(&key) {
1036 if let Some(txt) = self.paste_from_primary() {
1037 let mut st = state_rc.borrow_mut();
1038 st.insert_text_atomic(&txt);
1039 self.notify_text_change(hit.id, st.text.clone());
1040 if let Some(f) = &self.frame_cache
1041 && let Some(h) =
1042 f.hit_regions.iter().find(|h| h.id == hit.id)
1043 {
1044 App::tf_ensure_caret_visible(&mut st, h.tf_multiline);
1045 }
1046 }
1047 }
1048 }
1049 }
1050 self.request_redraw();
1051 }
1052
1053 WindowEvent::MouseInput {
1054 state: ElementState::Released,
1055 button: MouseButton::Left,
1056 ..
1057 } => {
1058 let pos = Vec2 {
1059 x: self.mouse_pos_px.0,
1060 y: self.mouse_pos_px.1,
1061 };
1062
1063 if repose_core::dnd::handle_drag_action(
1064 &repose_core::shortcuts::DragAction::Release {
1065 position: pos,
1066 modifiers: self.modifiers,
1067 },
1068 ) {
1069 self.capture_id = None;
1070 self.pressed_ids.clear();
1071 repose_core::request_frame();
1072 return;
1073 }
1074
1075 if let Some(cid) = self.capture_id {
1076 self.pressed_ids.remove(&cid);
1077 self.request_redraw();
1078 }
1079
1080 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
1081 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1082 && let Some(cb) = &hit.on_pointer_up
1083 {
1084 let pos = Vec2 {
1085 x: self.mouse_pos_px.0,
1086 y: self.mouse_pos_px.1,
1087 };
1088 let pe = PointerEvent::new(
1089 PointerId(0),
1090 PointerKind::Mouse,
1091 PointerEventKind::Up(PointerButton::Primary),
1092 pos,
1093 1.0,
1094 self.modifiers,
1095 );
1096 cb(pe);
1097 }
1098
1099 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
1101 let pos = Vec2 {
1102 x: self.mouse_pos_px.0,
1103 y: self.mouse_pos_px.1,
1104 };
1105 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1106 && hit.rect.contains(pos)
1107 && let Some(cb) = &hit.on_click
1108 {
1109 cb();
1110 if let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid) {
1112 let label = node.label.as_deref().unwrap_or("");
1113 self.a11y.announce(&format!("Activated {}", label));
1114 }
1115 }
1116 }
1117 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
1119 && let Some(_sem) = f
1120 .semantics_nodes
1121 .iter()
1122 .find(|n| n.id == cid && n.role == Role::TextField)
1123 {
1124 let key = self.tf_key_of(cid);
1125 if let Some(state_rc) = self.textfield_states.get(&key) {
1126 state_rc.borrow_mut().end_drag();
1127 }
1128 }
1129
1130 self.capture_id = None;
1131
1132 repose_core::request_frame();
1133 }
1134
1135 WindowEvent::MouseInput {
1136 state: ElementState::Released,
1137 button: MouseButton::Middle,
1138 ..
1139 } => {
1140 if let Some(f) = &self.frame_cache {
1141 let pos = Vec2 {
1142 x: self.mouse_pos_px.0,
1143 y: self.mouse_pos_px.1,
1144 };
1145 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
1146 {
1147 if let Some(cb) = &hit.on_pointer_up {
1148 cb(PointerEvent::new(
1149 PointerId(0),
1150 PointerKind::Mouse,
1151 PointerEventKind::Up(PointerButton::Tertiary),
1152 pos,
1153 1.0,
1154 self.modifiers,
1155 ));
1156 }
1157 }
1158 }
1159 }
1160
1161 WindowEvent::ModifiersChanged(new_mods) => {
1162 rc::update_modifiers(&mut self.modifiers, &new_mods.state());
1163 }
1164
1165 WindowEvent::KeyboardInput {
1166 event: key_event, ..
1167 } => {
1168 let mapped_key = rc::map_key(key_event.physical_key);
1169
1170 if key_event.state == ElementState::Pressed && !key_event.repeat {
1171 match key_event.physical_key {
1172 PhysicalKey::Code(KeyCode::BrowserBack)
1173 | PhysicalKey::Code(KeyCode::Escape) => {
1174 use repose_navigation::back;
1175
1176 if repose_core::dnd::handle_drag_action(
1177 &repose_core::shortcuts::DragAction::Cancel,
1178 ) {
1179 return;
1180 }
1181
1182 if self.dispatch_focus_key_event(&key_event, &mapped_key) {
1185 self.request_redraw();
1186 return;
1187 }
1188
1189 if !back::handle() {
1190 }
1192 return;
1193 }
1194 _ => {}
1195 }
1196 }
1197
1198 let utf16 = match mapped_key {
1200 repose_core::input::Key::Character(c) => c as u16,
1201 _ => 0,
1202 };
1203 let mods = self.modifiers;
1204 let repeat = key_event.repeat;
1205 let ev_type = if key_event.state == ElementState::Pressed {
1206 repose_core::input::KeyEventType::Down
1207 } else {
1208 repose_core::input::KeyEventType::Up
1209 };
1210 let consumed = self
1211 .frame_cache
1212 .as_ref()
1213 .and_then(|f| {
1214 let focused = self.sched.focused.or_else(|| {
1215 f.semantics_nodes
1216 .iter()
1217 .find(|n| n.parent.is_none())
1218 .map(|n| n.id)
1219 })?;
1220 let sem_parent_of: std::collections::HashMap<u64, u64> = f
1221 .semantics_nodes
1222 .iter()
1223 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1224 .collect();
1225 let hit_by_id: std::collections::HashMap<u64, &HitRegion> =
1226 f.hit_regions.iter().map(|h| (h.id, h)).collect();
1227 let mut ancestors = Vec::new();
1228 let mut cur = focused;
1229 loop {
1230 ancestors.push(cur);
1231 if let Some(&p) = sem_parent_of.get(&cur) {
1232 cur = p;
1233 } else {
1234 break;
1235 }
1236 }
1237 let make_ke = || repose_core::input::KeyEvent {
1238 key: mapped_key.clone(),
1239 modifiers: mods,
1240 is_repeat: repeat,
1241 event_type: ev_type,
1242 utf16_code_point: utf16,
1243 };
1244 for &id in ancestors.iter().rev() {
1246 if let Some(hit) = hit_by_id.get(&id) {
1247 if let Some(cb) = &hit.on_preview_key_event {
1248 if cb(make_ke()) {
1249 return Some(true);
1250 }
1251 }
1252 }
1253 }
1254 for &id in ancestors.iter() {
1256 if let Some(hit) = hit_by_id.get(&id) {
1257 if let Some(cb) = &hit.on_key_event {
1258 if cb(make_ke()) {
1259 return Some(true);
1260 }
1261 }
1262 }
1263 }
1264 None
1265 })
1266 .unwrap_or(false);
1267 if consumed {
1268 self.request_redraw();
1269 return;
1270 }
1271
1272 if key_event.state == ElementState::Pressed
1273 && let Some(action) = repose_core::shortcuts::resolve_action(
1274 repose_core::shortcuts::KeyChord::new(mapped_key, self.modifiers),
1275 )
1276 && self.dispatch_action(action)
1277 {
1278 self.request_redraw();
1279 return;
1280 }
1281
1282 if let Some(fid) = self.sched.focused {
1283 let is_textfield = if let Some(f) = &self.frame_cache {
1285 f.semantics_nodes
1286 .iter()
1287 .any(|n| n.id == fid && n.role == Role::TextField)
1288 } else {
1289 false
1290 };
1291
1292 if !is_textfield {
1293 match key_event.physical_key {
1294 PhysicalKey::Code(KeyCode::Space)
1295 | PhysicalKey::Code(KeyCode::Enter) => {
1296 if key_event.state == ElementState::Pressed && !key_event.repeat
1297 {
1298 self.pressed_ids.insert(fid);
1299 self.key_pressed_active = Some(fid);
1300 self.request_redraw();
1301 return;
1302 }
1303 }
1304 _ => {}
1305 }
1306 }
1307 }
1308
1309 if key_event.state == ElementState::Pressed
1313 && !key_event.repeat
1314 && let PhysicalKey::Code(KeyCode::Enter) = key_event.physical_key
1315 && let Some(focused_id) = self.sched.focused
1316 && let Some(f) = &self.frame_cache
1317 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == focused_id)
1318 {
1319 let is_multiline = hit.tf_multiline;
1320 let should_submit = if is_multiline {
1321 self.modifiers.ctrl || self.modifiers.meta
1323 } else {
1324 true
1326 };
1327
1328 if should_submit {
1329 if let Some(on_submit) = &hit.on_text_submit {
1330 let key = self.tf_key_of(focused_id);
1331 if let Some(state) = self.textfield_states.get(&key) {
1332 let text = state.borrow().text.clone();
1333 on_submit(text);
1334 self.request_redraw();
1335 return;
1336 }
1337 }
1338 } else {
1339 let key = self.tf_key_of(focused_id);
1341 if let Some(state_rc) = self.textfield_states.get(&key) {
1342 let mut st = state_rc.borrow_mut();
1343 st.insert_text("\n");
1344 let new_text = st.text.clone();
1345 self.notify_text_change(focused_id, new_text);
1346 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1347 self.request_redraw();
1348 return;
1349 }
1350 }
1351 }
1352
1353 if key_event.state == ElementState::Pressed {
1354 if self.modifiers.ctrl
1356 && self.modifiers.shift
1357 && let PhysicalKey::Code(KeyCode::KeyI) = key_event.physical_key
1358 {
1359 self.inspector.hud.toggle_inspector();
1360 self.request_redraw();
1361 return;
1362 }
1363
1364 if let Some(focused_id) = self.sched.focused {
1366 let key = self.tf_key_of(focused_id);
1367 if let Some(state_rc) = self.textfield_states.get(&key) {
1368 let mut state = state_rc.borrow_mut();
1369 match key_event.physical_key {
1370 PhysicalKey::Code(KeyCode::Backspace) => {
1371 state.delete_backward();
1372 let new_text = state.text.clone();
1373 self.notify_text_change(focused_id, new_text);
1374 App::tf_ensure_caret_visible(
1375 &mut state,
1376 self.is_multiline_id(focused_id),
1377 );
1378 self.request_redraw();
1379 }
1380 PhysicalKey::Code(KeyCode::Delete) => {
1381 state.delete_forward();
1382 let new_text = state.text.clone();
1383 self.notify_text_change(focused_id, new_text);
1384 App::tf_ensure_caret_visible(
1385 &mut state,
1386 self.is_multiline_id(focused_id),
1387 );
1388 self.request_redraw();
1389 }
1390 PhysicalKey::Code(KeyCode::ArrowLeft) => {
1391 state.move_cursor(-1, self.modifiers.shift);
1392 state.preferred_x_px = None; App::tf_ensure_caret_visible(
1394 &mut state,
1395 self.is_multiline_id(focused_id),
1396 );
1397 self.request_redraw();
1398 }
1399 PhysicalKey::Code(KeyCode::ArrowRight) => {
1400 state.move_cursor(1, self.modifiers.shift);
1401 state.preferred_x_px = None; App::tf_ensure_caret_visible(
1403 &mut state,
1404 self.is_multiline_id(focused_id),
1405 );
1406 self.request_redraw();
1407 }
1408 PhysicalKey::Code(KeyCode::ArrowUp) => {
1409 if self.is_multiline_id(focused_id)
1410 && let Some(f) = &self.frame_cache
1411 && let Some(hit) =
1412 f.hit_regions.iter().find(|h| h.id == focused_id)
1413 {
1414 let font_px = dp_to_px(TF_FONT_DP);
1415 let cur = state.caret_index();
1416 let (new_pos, px) =
1417 repose_ui::textfield::move_caret_vertical(
1418 &state.text,
1419 font_px,
1420 hit.rect.w,
1421 cur,
1422 -1,
1423 state.preferred_x_px,
1424 );
1425 if self.modifiers.shift {
1426 state.selection.end = new_pos;
1427 } else {
1428 state.selection = new_pos..new_pos;
1429 }
1430 state.preferred_x_px = Some(px);
1431 let (cx, cy, _) = caret_xy_for_byte(
1433 &state.text,
1434 font_px,
1435 hit.rect.w,
1436 state.caret_index(),
1437 );
1438 let iw = state.inner_width;
1439 let ih = state.inner_height;
1440 state.ensure_caret_visible_xy(
1441 cx,
1442 cy,
1443 iw,
1444 ih,
1445 self.dp_px(2.0),
1446 );
1447 self.request_redraw();
1448 }
1449 }
1450 PhysicalKey::Code(KeyCode::ArrowDown) => {
1451 if self.is_multiline_id(focused_id)
1452 && let Some(f) = &self.frame_cache
1453 && let Some(hit) =
1454 f.hit_regions.iter().find(|h| h.id == focused_id)
1455 {
1456 let font_px = dp_to_px(TF_FONT_DP);
1457 let wrap_w = hit.rect.w;
1458 let cur = state.caret_index();
1459 let (new_pos, px) =
1460 repose_ui::textfield::move_caret_vertical(
1461 &state.text,
1462 font_px,
1463 wrap_w,
1464 cur,
1465 1,
1466 state.preferred_x_px,
1467 );
1468 if self.modifiers.shift {
1469 state.selection.end = new_pos;
1470 } else {
1471 state.selection = new_pos..new_pos;
1472 }
1473 state.preferred_x_px = Some(px);
1474 let (cx, cy, _) = caret_xy_for_byte(
1476 &state.text,
1477 font_px,
1478 wrap_w,
1479 state.caret_index(),
1480 );
1481 let iw = state.inner_width;
1482 let ih = state.inner_height;
1483 state.ensure_caret_visible_xy(
1484 cx,
1485 cy,
1486 iw,
1487 ih,
1488 self.dp_px(2.0),
1489 );
1490 self.request_redraw();
1491 }
1492 }
1493 PhysicalKey::Code(KeyCode::Home) => {
1494 state.selection = 0..0;
1495 App::tf_ensure_caret_visible(
1496 &mut state,
1497 self.is_multiline_id(focused_id),
1498 );
1499 self.request_redraw();
1500 }
1501 PhysicalKey::Code(KeyCode::End) => {
1502 {
1503 let end = state.text.len();
1504 state.selection = end..end;
1505 }
1506 App::tf_ensure_caret_visible(
1507 &mut state,
1508 self.is_multiline_id(focused_id),
1509 );
1510 self.request_redraw();
1511 }
1512 _ => {}
1513 }
1514 }
1515 }
1516
1517 if !self.ime_preedit
1519 && !self.modifiers.ctrl
1520 && !self.modifiers.alt
1521 && !self.modifiers.meta
1522 && let Some(raw) = key_event.text.as_deref()
1523 {
1524 let text: String = raw
1525 .chars()
1526 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
1527 .collect();
1528 if !text.is_empty()
1529 && let Some(fid) = self.sched.focused
1530 {
1531 let key = self.tf_key_of(fid);
1532 if let Some(state_rc) = self.textfield_states.get(&key) {
1533 let mut st = state_rc.borrow_mut();
1534 st.insert_text(&text);
1535 self.notify_text_change(fid, st.text.clone());
1536 if let Some(f) = &self.frame_cache
1537 && let Some(hit) =
1538 f.hit_regions.iter().find(|h| h.id == fid)
1539 {
1540 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1541 }
1542 self.request_redraw();
1543 }
1544 }
1545 }
1546 } else if key_event.state == ElementState::Released {
1547 if let Some(active_id) = self.key_pressed_active {
1549 match key_event.physical_key {
1550 PhysicalKey::Code(KeyCode::Space)
1551 | PhysicalKey::Code(KeyCode::Enter) => {
1552 self.pressed_ids.remove(&active_id);
1553 self.key_pressed_active = None;
1554
1555 if let Some(f) = &self.frame_cache
1556 && let Some(hit) =
1557 f.hit_regions.iter().find(|h| h.id == active_id)
1558 {
1559 if let Some(cb) = &hit.on_click {
1560 cb();
1561 } else if let Some(cb) = &hit.on_pointer_down {
1562 let pe = PointerEvent::new(
1563 PointerId(0),
1564 PointerKind::Mouse,
1565 PointerEventKind::Down(PointerButton::Primary),
1566 Vec2 { x: 0.0, y: 0.0 },
1567 1.0,
1568 self.modifiers,
1569 );
1570 cb(pe);
1571 }
1572 if let Some(node) =
1573 f.semantics_nodes.iter().find(|n| n.id == active_id)
1574 {
1575 let label = node.label.as_deref().unwrap_or("");
1576 self.a11y.announce(&format!("Activated {}", label));
1577 }
1578 }
1579 self.request_redraw();
1580 }
1581 _ => {}
1582 }
1583 }
1584 }
1585 }
1586
1587 WindowEvent::Ime(ime) => {
1588 if let Some(focused_id) = self.sched.focused {
1589 let key = self.tf_key_of(focused_id);
1590 if let Some(state_rc) = self.textfield_states.get(&key)
1591 && let Some(f) = &self.frame_cache
1592 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == focused_id)
1593 {
1594 let mut state = state_rc.borrow_mut();
1595 let hit_rect = hit.rect;
1596 let on_text_change = hit.on_text_change.clone();
1597 let mut notify = |text: String| {
1598 if let Some(cb) = &on_text_change {
1599 cb(text);
1600 }
1601 };
1602 rc_android::handle_ime_event(
1603 ime,
1604 &mut state,
1605 hit_rect,
1606 &mut notify,
1607 &mut self.ime_preedit,
1608 );
1609 self.request_redraw();
1610 }
1611 }
1612 }
1613
1614 WindowEvent::RedrawRequested => {
1615 if !self.redraw_requested.replace(false) {
1617 self.process_a11y_actions();
1618 self.process_render_commands();
1619 log::trace!("RedrawRequested: no frame request, skipping compose");
1620 return;
1621 }
1622 log::trace!("RedrawRequested: frame request pending, composing");
1623
1624 self.process_a11y_actions();
1626 self.process_render_commands();
1627
1628 let Some(win) = self.window.as_ref() else {
1629 return;
1630 };
1631 if self.backend.is_none() {
1632 return;
1633 }
1634
1635 repose_core::animation_driver::tick();
1638
1639 let t0 = Instant::now();
1640 let scale = win.scale_factor() as f32;
1641 let size_px_u32 = self.sched.size;
1642 let focused = self.sched.focused;
1643
1644 let rc = self.render.clone();
1645 let root_fn = &mut self.root;
1646 let mut composed_root = |s: &mut Scheduler| (root_fn)(s, &rc);
1647
1648 let frame = compose_frame(
1649 &mut self.sched,
1650 &mut composed_root,
1651 scale,
1652 size_px_u32,
1653 self.hover_id,
1654 &self.pressed_ids,
1655 &self.textfield_states,
1656 focused,
1657 );
1658
1659 if focused.is_some() && self.sched.focused.is_none() && self.ime_preedit {
1660 rc_web::set_ime_for_textfield(win, false);
1661 self.ime_preedit = false;
1662 }
1663
1664 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1665
1666 if let Some(adapter) = &mut self.accesskit_adapter {
1668 let win = self.window.as_ref().unwrap();
1669 let scale = win.scale_factor();
1670 if let Some(update) =
1671 self.a11y_tree
1672 .update(&frame.semantics_nodes, scale, self.sched.focused)
1673 {
1674 adapter.update_if_active(|| update);
1675 }
1676 }
1677
1678 let mut scene = frame.scene.clone();
1680 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1682 let signal_count = self.sched.id_count() as usize;
1683 self.inspector.hud.metrics = Some(repose_devtools::Metrics {
1684 build_ms: build_layout_ms,
1685 layout_ms: build_layout_ms * 0.5,
1686 scene_nodes: scene.nodes.len(),
1687 widget_count,
1688 signal_count,
1689 });
1690 self.inspector.frame(&mut scene);
1691
1692 repose_core::dnd::overlay_drag_indicator(
1694 &mut scene,
1695 self.mouse_pos_px,
1696 self.external_file_drag,
1697 );
1698
1699 let win = self.window.as_ref().unwrap();
1701 let scale = win.scale_factor() as f32;
1702 if let Some(backend) = self.backend.as_mut() {
1703 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1704 }
1705
1706 if let Some(fid) = self.sched.focused {
1709 if let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
1710 && let Some(key) = hit.tf_state_key
1711 && !self.textfield_states.contains_key(&key)
1712 {
1713 self.textfield_states
1714 .entry(key)
1715 .or_insert_with(|| {
1716 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1717 })
1718 .borrow_mut()
1719 .reset_caret_blink();
1720 }
1721 }
1722
1723 repose_core::dnd::set_dnd_frame(Some(frame.clone()));
1724 self.frame_cache = Some(frame);
1725 repose_core::dnd::set_dnd_scale(scale);
1726
1727 self.dispatch_file_drop_now();
1728
1729 rc::tick_snackbar(self.last_redraw);
1730 self.last_redraw = Instant::now();
1731 }
1732
1733 _ => {}
1734 }
1735 }
1736
1737 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1738 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1741 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK.lock().unwrap().as_ref() {
1742 cb();
1743 }
1744 process_deeplinks();
1745
1746 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1749 if WINDOW_VISIBLE.load(Ordering::Relaxed) && self.backend.is_none() {
1750 if let Some(w) = &self.window {
1751 log::info!("about_to_wait: recreating GPU backend");
1752 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
1753 Ok(b) => self.backend = Some(b),
1754 Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
1755 }
1756 }
1757 }
1758
1759 if take_frame_request() {
1760 self.pending_redraw = true;
1761 }
1762 if !self.pending_redraw {
1763 let now = Instant::now();
1764 let idle_interval = web_time::Duration::from_millis(1000);
1765 if now.saturating_duration_since(self.last_redraw) >= idle_interval {
1766 self.redraw_requested.set(true);
1767 request_frame();
1768 rc::request_redraw(&self.window);
1769 self.last_redraw = now;
1770 }
1771 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1772 self.last_redraw + idle_interval,
1773 ));
1774 return;
1775 }
1776
1777 let now = Instant::now();
1778 let interval = web_time::Duration::from_millis(16);
1779
1780 if now.saturating_duration_since(self.last_redraw) >= interval {
1781 self.pending_redraw = false;
1782 self.redraw_requested.set(true);
1783 rc::request_redraw(&self.window);
1784 self.last_redraw = now;
1785 } else {
1786 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1787 self.last_redraw + interval,
1788 ));
1789 }
1790 }
1791
1792 fn new_events(
1793 &mut self,
1794 _: &winit::event_loop::ActiveEventLoop,
1795 _: winit::event::StartCause,
1796 ) {
1797 }
1798 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1799 self.pending_redraw = true;
1800 }
1801 fn device_event(
1802 &mut self,
1803 _: &winit::event_loop::ActiveEventLoop,
1804 _: winit::event::DeviceId,
1805 _: winit::event::DeviceEvent,
1806 ) {
1807 }
1808 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1809 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1810 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1811 }
1812
1813 impl App {
1814 fn dispatch_focus_key_event(
1817 &self,
1818 key_event: &winit::event::KeyEvent,
1819 mapped_key: &repose_core::input::Key,
1820 ) -> bool {
1821 let Some(f) = &self.frame_cache else {
1822 return false;
1823 };
1824 let Some(focused) = self.sched.focused else {
1825 return false;
1826 };
1827 let utf16 = match mapped_key {
1828 repose_core::input::Key::Character(c) => *c as u16,
1829 _ => 0,
1830 };
1831 let mods = self.modifiers;
1832 let repeat = key_event.repeat;
1833 let ev_type = if key_event.state == ElementState::Pressed {
1834 repose_core::input::KeyEventType::Down
1835 } else {
1836 repose_core::input::KeyEventType::Up
1837 };
1838 let hit_by_id: std::collections::HashMap<u64, &HitRegion> =
1839 f.hit_regions.iter().map(|h| (h.id, h)).collect();
1840 let sem_parent_of: std::collections::HashMap<u64, u64> = f
1841 .semantics_nodes
1842 .iter()
1843 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1844 .collect();
1845 let mut ancestors = Vec::new();
1846 let mut cur = focused;
1847 loop {
1848 ancestors.push(cur);
1849 if let Some(&p) = sem_parent_of.get(&cur) {
1850 cur = p;
1851 } else {
1852 break;
1853 }
1854 }
1855 let make_ke = || repose_core::input::KeyEvent {
1856 key: mapped_key.clone(),
1857 modifiers: mods,
1858 is_repeat: repeat,
1859 event_type: ev_type,
1860 utf16_code_point: utf16,
1861 };
1862 for &id in ancestors.iter().rev() {
1864 if let Some(hit) = hit_by_id.get(&id) {
1865 if let Some(cb) = &hit.on_preview_key_event {
1866 if cb(make_ke()) {
1867 return true;
1868 }
1869 }
1870 }
1871 }
1872 for &id in ancestors.iter() {
1874 if let Some(hit) = hit_by_id.get(&id) {
1875 if let Some(cb) = &hit.on_key_event {
1876 if cb(make_ke()) {
1877 return true;
1878 }
1879 }
1880 }
1881 }
1882 false
1883 }
1884
1885 fn announce_focus_change(&mut self) {
1886 if let Some(f) = &self.frame_cache {
1887 let focused_node = self
1888 .sched
1889 .focused
1890 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1891 self.a11y.focus_changed(focused_node);
1892 }
1893 }
1894
1895 fn notify_text_change(&self, id: u64, text: String) {
1896 if let Some(f) = &self.frame_cache
1897 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1898 && let Some(cb) = &h.on_text_change
1899 {
1900 cb(text);
1901 }
1902 }
1903
1904 fn tf_key_of(&self, visual_id: u64) -> u64 {
1905 rc::tf_key_of_in_frame(&self.frame_cache, visual_id)
1906 }
1907
1908 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1909 use repose_core::shortcuts;
1910
1911 if let (Some(f), Some(fid)) = (&self.frame_cache, self.sched.focused)
1912 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1913 && let Some(cb) = &hit.on_action
1914 && cb(action.clone())
1915 {
1916 return true;
1917 }
1918
1919 if shortcuts::handle(action.clone()) {
1920 return true;
1921 }
1922
1923 if let Some(f) = &self.frame_cache {
1925 if let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.sched, f)
1926 {
1927 if let Some(active) = self.key_pressed_active.take() {
1928 self.pressed_ids.remove(&active);
1929 }
1930 let tf_state_key = f
1931 .hit_regions
1932 .iter()
1933 .find(|h| h.id == new_id)
1934 .and_then(|h| h.tf_state_key);
1935 if let Some(key) = tf_state_key {
1936 self.textfield_states.entry(key).or_insert_with(|| {
1937 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1938 });
1939 if let Some(state_rc) = self.textfield_states.get(&key) {
1940 state_rc.borrow_mut().reset_caret_blink();
1941 }
1942 }
1943 if let Some(win) = &self.window {
1944 let is_textfield = f.semantics_nodes.iter().any(|n| {
1945 n.id == new_id && n.role == repose_core::semantics::Role::TextField
1946 });
1947 rc_web::set_ime_for_textfield(win, is_textfield);
1948 }
1949 self.announce_focus_change();
1950 return true;
1951 }
1952 }
1953
1954 false
1955 }
1956
1957 fn dispatch_file_drop_now(&mut self) {
1958 let Some(f) = &self.frame_cache else {
1959 self.pending_dropped_files.clear();
1960 self.pending_drop_pos_px = None;
1961 return;
1962 };
1963
1964 if self.pending_dropped_files.is_empty() {
1965 return;
1966 }
1967
1968 let pos_px = self.pending_drop_pos_px.unwrap_or(self.mouse_pos_px);
1969 let pos = Vec2 {
1970 x: pos_px.0,
1971 y: pos_px.1,
1972 };
1973
1974 let mut files = Vec::new();
1975 for p in self.pending_dropped_files.drain(..) {
1976 let name = p
1977 .file_name()
1978 .and_then(|s| s.to_str())
1979 .unwrap_or("file")
1980 .to_string();
1981 files.push(repose_core::dnd::DroppedFile {
1982 name,
1983 path: Some(p),
1984 });
1985 }
1986
1987 let payload: repose_core::dnd::DragPayload =
1988 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1989
1990 let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
1991 self.pending_drop_pos_px = None;
1992 return;
1993 };
1994
1995 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
1996 && let Some(cb) = &hit.on_drop
1997 {
1998 let accepted = cb(repose_core::dnd::DropEvent {
1999 source_id: 0, target_id,
2001 position: pos,
2002 modifiers: self.modifiers,
2003 payload: payload.clone(),
2004 });
2005
2006 if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
2007 {
2008 let label = node.label.as_deref().unwrap_or("");
2009 self.a11y.announce(&format!("Dropped files on {}", label));
2010 }
2011 }
2012
2013 self.pending_drop_pos_px = None;
2014 self.request_redraw();
2015 }
2016 }
2017
2018 let event_loop = EventLoop::new()?;
2019 set_event_loop_proxy(event_loop.create_proxy());
2020 let mut app = App::new(Box::new(root));
2021 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
2023 event_loop.run_app(&mut app)?;
2024 Ok(())
2025}
2026
2027pub trait A11yBridge: Send {
2035 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
2037
2038 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
2040
2041 fn announce(&mut self, msg: &str);
2043}
2044
2045struct NoopA11y;
2046impl A11yBridge for NoopA11y {
2047 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
2048 }
2050 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
2051 if let Some(n) = node {
2052 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
2053 } else {
2054 log::info!("A11y focus: None");
2055 }
2056 }
2057 fn announce(&mut self, msg: &str) {
2058 log::info!("A11y announce: {msg}");
2059 }
2060}
2061
2062#[cfg(target_os = "linux")]
2063struct LinuxAtspiStub;
2064#[cfg(target_os = "linux")]
2065impl A11yBridge for LinuxAtspiStub {
2066 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
2067 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
2068 }
2069 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
2070 if let Some(n) = node {
2071 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
2072 } else {
2073 log::info!("AT-SPI stub focus: None");
2074 }
2075 }
2076 fn announce(&mut self, msg: &str) {
2077 log::info!("AT-SPI stub announce: {msg}");
2078 }
2079}