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, 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, TextMeasureConfig::default());
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 mut opts = clipawl::ClipboardOptions::default();
414 opts.linux.selection = clipawl::LinuxSelection::Primary;
415 if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
416 match pollster::block_on(cb.read()) {
417 Ok(t) => Some(t),
418 Err(e) => {
419 eprintln!("Primary paste error: {}", e);
420 None
421 }
422 }
423 } else {
424 None
425 }
426 }
427
428 fn process_render_commands(&mut self) {
429 let Some(backend) = self.backend.as_mut() else {
430 return;
431 };
432 rc::process_render_commands(backend, self.render.drain());
433 }
434
435 fn reset_pointer_state(&mut self) {
436 self.capture_id = None;
437 self.pressed_ids.clear();
438 self.hover_id = None;
439 }
440
441 fn is_textfield(&self, id: u64) -> bool {
442 rc::is_textfield_in_frame(&self.frame_cache, id)
443 }
444
445 fn is_multiline_id(&self, id: u64) -> bool {
446 if let Some(f) = &self.frame_cache {
447 f.hit_regions
448 .iter()
449 .find(|h| h.id == id)
450 .map(|h| h.tf_multiline)
451 .unwrap_or(false)
452 } else {
453 false
454 }
455 }
456
457 fn hit_by_id(f: &Frame, id: u64) -> Option<&HitRegion> {
458 f.hit_regions.iter().find(|h| h.id == id)
459 }
460
461 fn dp_px(&self, dp: f32) -> f32 {
462 dp_to_px(dp)
463 }
464 }
465
466 impl ApplicationHandler<()> for App {
467 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
468 self.clipboard = clipawl::Clipboard::new()
469 .map_err(|e| {
470 eprintln!("clipawl clipboard init failed: {e}");
471 e
472 })
473 .ok();
474 repose_core::clipboard::set_clipboard_read_fn(Box::new(|| {
475 clipawl::blocking::read().ok()
476 }));
477 repose_core::clipboard::set_clipboard_fn(Box::new(move |text| {
479 if let Err(e) = clipawl::blocking::write(text) {
480 eprintln!("clipboard write error: {e}");
481 }
482 }));
483
484 repose_core::clipboard::set_primary_fn(Box::new(|text| {
485 let mut opts = clipawl::ClipboardOptions::default();
486 opts.linux.selection = clipawl::LinuxSelection::Primary;
487 match clipawl::Clipboard::new_with_options(opts) {
488 Ok(cb) => {
489 if let Err(e) = pollster::block_on(cb.write(text)) {
490 eprintln!("primary selection write error: {e}");
491 }
492 }
493 Err(e) => eprintln!("primary clipboard init error: {e}"),
494 }
495 }));
496
497 if self.window.is_none() {
498 match el.create_window(
499 WindowAttributes::default()
500 .with_title("Repose")
501 .with_inner_size(PhysicalSize::new(1280, 800))
502 .with_visible(false),
503 ) {
504 Ok(win) => {
505 let w = Arc::new(win);
506
507 let activation_handler = ReposeActivationHandler {
508 initial_tree: Some(A11yTree::initial_tree()),
509 };
510
511 let action_handler = ReposeActionHandler {
512 pending_actions: self.a11y_actions.clone(),
513 };
514
515 let deactivation_handler = ReposeDeactivationHandler;
516
517 let adapter = Adapter::with_direct_handlers(
518 el,
519 &w,
520 activation_handler,
521 action_handler,
522 deactivation_handler,
523 );
524
525 self.accesskit_adapter = Some(adapter);
526
527 w.set_visible(true);
528
529 let size = w.inner_size();
530 self.sched.size = (size.width, size.height);
531
532 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
533 Ok(b) => {
534 self.backend = Some(b);
535 set_app_window(w.clone());
536 self.window = Some(w);
537 self.request_redraw();
538 }
539 Err(e) => {
540 log::error!("Failed to create WGPU backend: {e:?}");
541 el.exit();
542 }
543 }
544 }
545 Err(e) => {
546 log::error!("Failed to create window: {e:?}");
547 el.exit();
548 }
549 }
550 }
551 }
552
553 fn window_event(
554 &mut self,
555 el: &winit::event_loop::ActiveEventLoop,
556 _id: winit::window::WindowId,
557 event: WindowEvent,
558 ) {
559 if let Some(adapter) = &mut self.accesskit_adapter {
561 adapter.process_event(self.window.as_ref().unwrap(), &event);
562 }
563
564 match event {
565 WindowEvent::CloseRequested => {
566 if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
567 self.backend = None;
569 if let Some(w) = &self.window {
570 w.set_visible(false);
571 }
572 WINDOW_VISIBLE.store(false, Ordering::Relaxed);
573 } else {
574 el.exit();
575 }
576 }
577
578 WindowEvent::Focused(false) => {
579 repose_core::dnd::handle_drag_action(
581 &repose_core::shortcuts::DragAction::Cancel,
582 );
583
584 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
586 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
587 if let Some(cb) = &hit.on_pointer_cancel {
588 let pos = repose_core::Vec2 {
589 x: self.mouse_pos_px.0,
590 y: self.mouse_pos_px.1,
591 };
592 let pe = PointerEvent::new(
593 PointerId(0),
594 PointerKind::Mouse,
595 PointerEventKind::Cancel,
596 pos,
597 1.0,
598 self.modifiers,
599 );
600 cb(pe);
601 }
602 }
603 }
604
605 self.external_file_drag = false;
607 self.hovered_files.clear();
608 self.reset_pointer_state();
609
610 if let Some(w) = &self.window {
611 rc_web::set_ime_for_textfield(w, false);
612 }
613 self.ime_preedit = false;
614
615 self.request_redraw();
616 }
617
618 WindowEvent::HoveredFile(path) => {
619 self.external_file_drag = true;
621 if self.hovered_files.len() < 32 {
622 self.hovered_files.push(path);
623 }
624 if self.pending_drop_pos_px.is_none() {
626 self.pending_drop_pos_px = Some(self.mouse_pos_px);
627 }
628 self.request_redraw();
629 }
630
631 WindowEvent::HoveredFileCancelled => {
632 self.external_file_drag = false;
633 self.hovered_files.clear();
634
635 self.reset_pointer_state();
637
638 self.request_redraw();
639 }
640
641 WindowEvent::DroppedFile(path) => {
642 self.pending_dropped_files.push(path);
644 if self.pending_drop_pos_px.is_none() {
645 self.pending_drop_pos_px = Some(self.mouse_pos_px);
646 }
647
648 self.external_file_drag = false;
650 self.hovered_files.clear();
651
652 self.request_redraw();
653 }
654
655 WindowEvent::Resized(size) => {
656 self.sched.size = (size.width, size.height);
657 if let Some(b) = self.backend.as_mut() {
658 b.configure_surface(size.width, size.height);
659 }
660 if let Some(w) = &self.window {
661 let sf = w.scale_factor() as f32;
662 let dp_w = size.width as f32 / sf;
663 let dp_h = size.height as f32 / sf;
664 log::info!(
665 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
666 size.width,
667 size.height,
668 sf,
669 dp_w as i32,
670 dp_h as i32
671 );
672 }
673 self.request_redraw();
674 }
675
676 WindowEvent::CursorMoved { position, .. } => {
677 self.mouse_pos_px = (position.x as f32, position.y as f32);
678
679 if self.external_file_drag {
680 self.pending_drop_pos_px = Some(self.mouse_pos_px);
681 }
682
683 let pos = Vec2 {
684 x: self.mouse_pos_px.0,
685 y: self.mouse_pos_px.1,
686 };
687
688 if repose_core::dnd::handle_drag_action(
689 &repose_core::shortcuts::DragAction::Move {
690 position: pos,
691 modifiers: self.modifiers,
692 },
693 ) {
694 self.request_redraw();
695 return;
696 }
697
698 if self.inspector.hud.inspector_enabled
700 && let Some(f) = &self.frame_cache
701 {
702 let hit = f.hit_regions.iter().find(|h| {
703 h.rect.contains(Vec2 {
704 x: self.mouse_pos_px.0,
705 y: self.mouse_pos_px.1,
706 })
707 });
708 let hover_rect = hit.map(|h| h.rect);
709 let hover_info = hit.and_then(|h| {
710 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
711 repose_devtools::HoveredInfo {
712 id: s.id,
713 role: format!("{:?}", s.role),
714 label: s.label.clone(),
715 }
716 })
717 });
718 self.inspector.hud.set_hovered(hover_rect, hover_info);
719 self.request_redraw();
720 }
721
722 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
724 && self.is_textfield(cid)
725 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
726 {
727 let key = self.tf_key_of(cid);
728 if let Some(state_rc) = self.textfield_states.get(&key) {
729 let mut st = state_rc.borrow_mut();
730
731 let (ox, oy) = hit.tf_content_origin.unwrap_or((hit.rect.x, hit.rect.y));
732 let content_x =
733 (self.mouse_pos_px.0 - ox + st.scroll_offset).max(0.0);
734 let content_y =
735 (self.mouse_pos_px.1 - oy + st.scroll_offset_y).max(0.0);
736
737 let font_px =
738 dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
739 let wrap_w = st.inner_width.max(1.0);
740
741 let idx = if hit.tf_multiline {
742 rc::index_for_xy_bytes_vt(
743 &st, font_px, wrap_w, content_x, content_y,
744 )
745 } else {
746 rc::index_for_x_bytes_vt(&st, font_px, content_x)
747 };
748
749 st.drag_to(idx);
750
751 self.request_redraw();
752 }
753 }
754
755 if let Some(f) = &self.frame_cache {
757 let pos = Vec2 {
759 x: self.mouse_pos_px.0,
760 y: self.mouse_pos_px.1,
761 };
762 let top = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos));
763
764 if let Some(win) = &self.window {
766 let c = top
767 .and_then(|h| h.cursor)
768 .unwrap_or(repose_core::CursorIcon::Default);
769 win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
770 }
771
772 let new_hover = top.map(|h| h.id);
773
774 if new_hover != self.hover_id {
776 if let Some(prev_id) = self.hover_id
777 && let Some(prev) = f.hit_regions.iter().find(|h| h.id == prev_id)
778 && let Some(cb) = &prev.on_pointer_leave
779 {
780 let pe = PointerEvent::new(
781 PointerId(0),
782 PointerKind::Mouse,
783 PointerEventKind::Leave,
784 pos,
785 1.0,
786 self.modifiers,
787 );
788 cb(pe);
789 }
790 if let Some(h) = top
791 && let Some(cb) = &h.on_pointer_enter
792 {
793 let pe = PointerEvent::new(
794 PointerId(0),
795 PointerKind::Mouse,
796 PointerEventKind::Enter,
797 pos,
798 1.0,
799 self.modifiers,
800 );
801 cb(pe);
802 }
803 self.hover_id = new_hover;
804 self.request_redraw();
805 }
806
807 let pe = PointerEvent::new(
809 PointerId(0),
810 PointerKind::Mouse,
811 PointerEventKind::Move,
812 pos,
813 1.0,
814 self.modifiers,
815 );
816
817 if let Some(cid) = self.capture_id {
819 if let Some(h) = f.hit_regions.iter().find(|h| h.id == cid)
820 && let Some(cb) = &h.on_pointer_move
821 {
822 cb(pe.clone());
823 }
824 } else if let Some(h) = &top
825 && let Some(cb) = &h.on_pointer_move
826 {
827 cb(pe);
828 }
829 }
830 }
831
832 WindowEvent::MouseWheel { delta, .. } => {
833 let (dx_px, dy_px) = match delta {
835 MouseScrollDelta::LineDelta(x, y) => {
836 let unit_px = dp_to_px(60.0);
837 (-(x * unit_px), -(y * unit_px))
838 }
839 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
840 };
841 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
842
843 if let Some(f) = &self.frame_cache {
844 let pos = Vec2 {
845 x: self.mouse_pos_px.0,
846 y: self.mouse_pos_px.1,
847 };
848
849 if rc::dispatch_scroll(f, pos, Vec2 { x: dx_px, y: dy_px }, None).0 {
850 self.request_redraw();
851 }
852 }
853 }
854
855 WindowEvent::MouseInput {
856 state: ElementState::Pressed,
857 button: MouseButton::Left,
858 ..
859 } => {
860 let mut need_announce = false;
861 if let Some(f) = &self.frame_cache {
862 let pos = Vec2 {
863 x: self.mouse_pos_px.0,
864 y: self.mouse_pos_px.1,
865 };
866 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
867 {
868 repose_core::dnd::handle_drag_action(
869 &repose_core::shortcuts::DragAction::Press {
870 position: Vec2 {
871 x: self.mouse_pos_px.0,
872 y: self.mouse_pos_px.1,
873 },
874 capture_id: hit.id,
875 kind: repose_core::input::PointerKind::Mouse,
876 modifiers: self.modifiers,
877 },
878 );
879
880 self.capture_id = Some(hit.id);
882
883 if self.is_textfield(hit.id) {
885 let key = self.tf_key_of(hit.id);
886 self.textfield_states.entry(key).or_insert_with(|| {
887 Rc::new(RefCell::new(TextFieldState::new()))
888 });
889 if let Some(st_rc) = self.textfield_states.get(&key) {
890 let mut st = st_rc.borrow_mut();
891 let (ox, oy) = hit.tf_content_origin.unwrap_or((hit.rect.x, hit.rect.y));
892 let content_x =
893 (self.mouse_pos_px.0 - ox + st.scroll_offset)
894 .max(0.0);
895 let content_y = (self.mouse_pos_px.1 - oy
896 + st.scroll_offset_y)
897 .max(0.0);
898 let font_px = self.dp_px(TF_FONT_DP)
899 * repose_core::locals::text_scale().0;
900 let wrap_w = st.inner_width.max(1.0);
901
902 let idx = if hit.tf_multiline {
903 rc::index_for_xy_bytes_vt(
904 &st,
905 font_px,
906 wrap_w,
907 content_x,
908 content_y,
909 )
910 } else {
911 rc::index_for_x_bytes_vt(&st, font_px, content_x)
912 };
913
914 st.handle_pointer_down(idx, self.mouse_pos_px, self.modifiers.shift);
915 }
916 }
917 self.pressed_ids.insert(hit.id);
919 self.request_redraw();
921
922 if hit.focusable {
924 self.sched.focused = Some(hit.id);
925 need_announce = true;
926 let key = self.tf_key_of(hit.id);
927 self.textfield_states.entry(key).or_insert_with(|| {
928 Rc::new(RefCell::new(TextFieldState::new()))
929 });
930 if let Some(win) = &self.window {
931 let sf = win.scale_factor();
932 rc_web::set_ime_for_textfield(win, true);
933 win.set_ime_cursor_area(
934 LogicalPosition::new(
935 hit.rect.x as f64 / sf,
936 hit.rect.y as f64 / sf,
937 ),
938 LogicalSize::new(
939 hit.rect.w as f64 / sf,
940 hit.rect.h as f64 / sf,
941 ),
942 );
943 }
944 }
945
946 if let Some(cb) = &hit.on_pointer_down {
948 let pe = PointerEvent::new(
949 PointerId(0),
950 PointerKind::Mouse,
951 PointerEventKind::Down(PointerButton::Primary),
952 pos,
953 1.0,
954 self.modifiers,
955 );
956 cb(pe);
957 }
958
959 if need_announce {
960 self.announce_focus_change();
961 }
962
963 self.request_redraw();
964 } else {
965 if self.ime_preedit {
967 if let Some(win) = &self.window {
968 rc_web::set_ime_for_textfield(win, false);
969 }
970 self.ime_preedit = false;
971 }
972 self.sched.focused = None;
973 self.request_redraw();
974 }
975 }
976 }
977
978 WindowEvent::MouseInput {
979 state: ElementState::Pressed,
980 button: MouseButton::Middle,
981 ..
982 } => {
983 let Some(f) = &self.frame_cache else {
984 return;
985 };
986 let pos = Vec2 {
987 x: self.mouse_pos_px.0,
988 y: self.mouse_pos_px.1,
989 };
990 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos)) {
991 if let Some(cb) = &hit.on_pointer_down {
993 cb(PointerEvent::new(
994 PointerId(0),
995 PointerKind::Mouse,
996 PointerEventKind::Down(PointerButton::Tertiary),
997 pos,
998 1.0,
999 self.modifiers,
1000 ));
1001 }
1002 if self.is_textfield(hit.id) {
1004 let key = self.tf_key_of(hit.id);
1005 if let Some(state_rc) = self.textfield_states.get(&key) {
1006 if let Some(txt) = self.paste_from_primary() {
1007 let mut st = state_rc.borrow_mut();
1008 st.insert_text_atomic(&txt);
1009 self.notify_text_change(hit.id, st.text.clone());
1010 if let Some(f) = &self.frame_cache
1011 && let Some(h) =
1012 f.hit_regions.iter().find(|h| h.id == hit.id)
1013 {
1014 App::tf_ensure_caret_visible(&mut st, h.tf_multiline);
1015 }
1016 }
1017 }
1018 }
1019 }
1020 self.request_redraw();
1021 }
1022
1023 WindowEvent::MouseInput {
1024 state: ElementState::Released,
1025 button: MouseButton::Left,
1026 ..
1027 } => {
1028 let pos = Vec2 {
1029 x: self.mouse_pos_px.0,
1030 y: self.mouse_pos_px.1,
1031 };
1032
1033 if repose_core::dnd::handle_drag_action(
1034 &repose_core::shortcuts::DragAction::Release {
1035 position: pos,
1036 modifiers: self.modifiers,
1037 },
1038 ) {
1039 self.capture_id = None;
1040 self.pressed_ids.clear();
1041 repose_core::request_frame();
1042 return;
1043 }
1044
1045 if let Some(cid) = self.capture_id {
1046 self.pressed_ids.remove(&cid);
1047 self.request_redraw();
1048 }
1049
1050 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
1051 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1052 && let Some(cb) = &hit.on_pointer_up
1053 {
1054 let pos = Vec2 {
1055 x: self.mouse_pos_px.0,
1056 y: self.mouse_pos_px.1,
1057 };
1058 let pe = PointerEvent::new(
1059 PointerId(0),
1060 PointerKind::Mouse,
1061 PointerEventKind::Up(PointerButton::Primary),
1062 pos,
1063 1.0,
1064 self.modifiers,
1065 );
1066 cb(pe);
1067 }
1068
1069 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
1071 let pos = Vec2 {
1072 x: self.mouse_pos_px.0,
1073 y: self.mouse_pos_px.1,
1074 };
1075 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1076 && hit.rect.contains(pos)
1077 && let Some(cb) = &hit.on_click
1078 {
1079 cb();
1080 if let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid) {
1082 let label = node.label.as_deref().unwrap_or("");
1083 self.a11y.announce(&format!("Activated {}", label));
1084 }
1085 }
1086 }
1087 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
1089 && let Some(_sem) = f
1090 .semantics_nodes
1091 .iter()
1092 .find(|n| n.id == cid && n.role == Role::TextField)
1093 {
1094 let key = self.tf_key_of(cid);
1095 if let Some(state_rc) = self.textfield_states.get(&key) {
1096 state_rc.borrow_mut().end_drag();
1097 }
1098 }
1099
1100 self.capture_id = None;
1101
1102 repose_core::request_frame();
1103 }
1104
1105 WindowEvent::MouseInput {
1106 state: ElementState::Released,
1107 button: MouseButton::Middle,
1108 ..
1109 } => {
1110 if let Some(f) = &self.frame_cache {
1111 let pos = Vec2 {
1112 x: self.mouse_pos_px.0,
1113 y: self.mouse_pos_px.1,
1114 };
1115 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
1116 {
1117 if let Some(cb) = &hit.on_pointer_up {
1118 cb(PointerEvent::new(
1119 PointerId(0),
1120 PointerKind::Mouse,
1121 PointerEventKind::Up(PointerButton::Tertiary),
1122 pos,
1123 1.0,
1124 self.modifiers,
1125 ));
1126 }
1127 }
1128 }
1129 }
1130
1131 WindowEvent::ModifiersChanged(new_mods) => {
1132 rc::update_modifiers(&mut self.modifiers, &new_mods.state());
1133 }
1134
1135 WindowEvent::KeyboardInput {
1136 event: key_event, ..
1137 } => {
1138 let mapped_key = rc::map_key(key_event.physical_key);
1139
1140 if key_event.state == ElementState::Pressed && !key_event.repeat {
1141 match key_event.physical_key {
1142 PhysicalKey::Code(KeyCode::BrowserBack)
1143 | PhysicalKey::Code(KeyCode::Escape) => {
1144 use repose_navigation::back;
1145
1146 if repose_core::dnd::handle_drag_action(
1147 &repose_core::shortcuts::DragAction::Cancel,
1148 ) {
1149 return;
1150 }
1151
1152 if self.dispatch_focus_key_event(&key_event, &mapped_key) {
1155 self.request_redraw();
1156 return;
1157 }
1158
1159 if !back::handle() {
1160 }
1162 return;
1163 }
1164 _ => {}
1165 }
1166 }
1167
1168 let utf16 = match mapped_key {
1170 repose_core::input::Key::Character(c) => c as u16,
1171 _ => 0,
1172 };
1173 let mods = self.modifiers;
1174 let repeat = key_event.repeat;
1175 let ev_type = if key_event.state == ElementState::Pressed {
1176 repose_core::input::KeyEventType::Down
1177 } else {
1178 repose_core::input::KeyEventType::Up
1179 };
1180 let consumed = self
1181 .frame_cache
1182 .as_ref()
1183 .and_then(|f| {
1184 let focused = self.sched.focused.or_else(|| {
1185 f.semantics_nodes
1186 .iter()
1187 .find(|n| n.parent.is_none())
1188 .map(|n| n.id)
1189 })?;
1190 let sem_parent_of: std::collections::HashMap<u64, u64> = f
1191 .semantics_nodes
1192 .iter()
1193 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1194 .collect();
1195 let hit_by_id: std::collections::HashMap<u64, &HitRegion> =
1196 f.hit_regions.iter().map(|h| (h.id, h)).collect();
1197 let mut ancestors = Vec::new();
1198 let mut cur = focused;
1199 loop {
1200 ancestors.push(cur);
1201 if let Some(&p) = sem_parent_of.get(&cur) {
1202 cur = p;
1203 } else {
1204 break;
1205 }
1206 }
1207 let make_ke = || repose_core::input::KeyEvent {
1208 key: mapped_key.clone(),
1209 modifiers: mods,
1210 is_repeat: repeat,
1211 event_type: ev_type,
1212 utf16_code_point: utf16,
1213 };
1214 for &id in ancestors.iter().rev() {
1216 if let Some(hit) = hit_by_id.get(&id) {
1217 if let Some(cb) = &hit.on_preview_key_event {
1218 if cb(make_ke()) {
1219 return Some(true);
1220 }
1221 }
1222 }
1223 }
1224 for &id in ancestors.iter() {
1226 if let Some(hit) = hit_by_id.get(&id) {
1227 if let Some(cb) = &hit.on_key_event {
1228 if cb(make_ke()) {
1229 return Some(true);
1230 }
1231 }
1232 }
1233 }
1234 None
1235 })
1236 .unwrap_or(false);
1237 if consumed {
1238 self.request_redraw();
1239 return;
1240 }
1241
1242 if key_event.state == ElementState::Pressed
1243 && let Some(action) = repose_core::shortcuts::resolve_action(
1244 repose_core::shortcuts::KeyChord::new(mapped_key, self.modifiers),
1245 )
1246 && self.dispatch_action(action)
1247 {
1248 self.request_redraw();
1249 return;
1250 }
1251
1252 if let Some(fid) = self.sched.focused {
1253 let is_textfield = if let Some(f) = &self.frame_cache {
1255 f.semantics_nodes
1256 .iter()
1257 .any(|n| n.id == fid && n.role == Role::TextField)
1258 } else {
1259 false
1260 };
1261
1262 if !is_textfield {
1263 match key_event.physical_key {
1264 PhysicalKey::Code(KeyCode::Space)
1265 | PhysicalKey::Code(KeyCode::Enter) => {
1266 if key_event.state == ElementState::Pressed && !key_event.repeat
1267 {
1268 self.pressed_ids.insert(fid);
1269 self.key_pressed_active = Some(fid);
1270 self.request_redraw();
1271 return;
1272 }
1273 }
1274 _ => {}
1275 }
1276 }
1277 }
1278
1279 if key_event.state == ElementState::Pressed
1283 && !key_event.repeat
1284 && let PhysicalKey::Code(KeyCode::Enter) = key_event.physical_key
1285 && let Some(focused_id) = self.sched.focused
1286 && let Some(f) = &self.frame_cache
1287 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == focused_id)
1288 {
1289 let is_multiline = hit.tf_multiline;
1290 let should_submit = if is_multiline {
1291 self.modifiers.ctrl || self.modifiers.meta
1293 } else {
1294 true
1296 };
1297
1298 if should_submit {
1299 if let Some(on_submit) = &hit.on_text_submit {
1300 let key = self.tf_key_of(focused_id);
1301 if let Some(state) = self.textfield_states.get(&key) {
1302 let text = state.borrow().text.clone();
1303 on_submit(text);
1304 self.request_redraw();
1305 return;
1306 }
1307 }
1308 } else {
1309 let key = self.tf_key_of(focused_id);
1311 if let Some(state_rc) = self.textfield_states.get(&key) {
1312 let mut st = state_rc.borrow_mut();
1313 st.insert_text("\n");
1314 let new_text = st.text.clone();
1315 self.notify_text_change(focused_id, new_text);
1316 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1317 self.request_redraw();
1318 return;
1319 }
1320 }
1321 }
1322
1323 if key_event.state == ElementState::Pressed {
1324 if self.modifiers.ctrl
1326 && self.modifiers.shift
1327 && let PhysicalKey::Code(KeyCode::KeyI) = key_event.physical_key
1328 {
1329 self.inspector.hud.toggle_inspector();
1330 self.request_redraw();
1331 return;
1332 }
1333
1334 if let Some(focused_id) = self.sched.focused {
1336 let key = self.tf_key_of(focused_id);
1337 if let Some(state_rc) = self.textfield_states.get(&key) {
1338 let mut state = state_rc.borrow_mut();
1339 match key_event.physical_key {
1340 PhysicalKey::Code(KeyCode::Backspace) => {
1341 state.delete_backward();
1342 let new_text = state.text.clone();
1343 self.notify_text_change(focused_id, new_text);
1344 App::tf_ensure_caret_visible(
1345 &mut state,
1346 self.is_multiline_id(focused_id),
1347 );
1348 self.request_redraw();
1349 }
1350 PhysicalKey::Code(KeyCode::Delete) => {
1351 state.delete_forward();
1352 let new_text = state.text.clone();
1353 self.notify_text_change(focused_id, new_text);
1354 App::tf_ensure_caret_visible(
1355 &mut state,
1356 self.is_multiline_id(focused_id),
1357 );
1358 self.request_redraw();
1359 }
1360 PhysicalKey::Code(KeyCode::ArrowLeft) => {
1361 state.move_cursor(-1, self.modifiers.shift);
1362 state.preferred_x_px = None; App::tf_ensure_caret_visible(
1364 &mut state,
1365 self.is_multiline_id(focused_id),
1366 );
1367 self.request_redraw();
1368 }
1369 PhysicalKey::Code(KeyCode::ArrowRight) => {
1370 state.move_cursor(1, self.modifiers.shift);
1371 state.preferred_x_px = None; App::tf_ensure_caret_visible(
1373 &mut state,
1374 self.is_multiline_id(focused_id),
1375 );
1376 self.request_redraw();
1377 }
1378 PhysicalKey::Code(KeyCode::ArrowUp) => {
1379 if self.is_multiline_id(focused_id)
1380 && let Some(f) = &self.frame_cache
1381 && let Some(hit) =
1382 f.hit_regions.iter().find(|h| h.id == focused_id)
1383 {
1384 let font_px = dp_to_px(TF_FONT_DP);
1385 let cur = state.caret_index();
1386 let (new_pos, px) =
1387 repose_ui::textfield::move_caret_vertical(
1388 &state.text,
1389 font_px,
1390 hit.rect.w,
1391 cur,
1392 -1,
1393 state.preferred_x_px,
1394 );
1395 if self.modifiers.shift {
1396 state.selection.end = new_pos;
1397 } else {
1398 state.selection = new_pos..new_pos;
1399 }
1400 state.preferred_x_px = Some(px);
1401 let (cx, cy, _) = caret_xy_for_byte(
1403 &state.text,
1404 font_px,
1405 hit.rect.w,
1406 state.caret_index(),
1407 );
1408 let iw = state.inner_width;
1409 let ih = state.inner_height;
1410 state.ensure_caret_visible_xy(
1411 cx,
1412 cy,
1413 iw,
1414 ih,
1415 self.dp_px(2.0),
1416 );
1417 self.request_redraw();
1418 }
1419 }
1420 PhysicalKey::Code(KeyCode::ArrowDown) => {
1421 if self.is_multiline_id(focused_id)
1422 && let Some(f) = &self.frame_cache
1423 && let Some(hit) =
1424 f.hit_regions.iter().find(|h| h.id == focused_id)
1425 {
1426 let font_px = dp_to_px(TF_FONT_DP);
1427 let wrap_w = hit.rect.w;
1428 let cur = state.caret_index();
1429 let (new_pos, px) =
1430 repose_ui::textfield::move_caret_vertical(
1431 &state.text,
1432 font_px,
1433 wrap_w,
1434 cur,
1435 1,
1436 state.preferred_x_px,
1437 );
1438 if self.modifiers.shift {
1439 state.selection.end = new_pos;
1440 } else {
1441 state.selection = new_pos..new_pos;
1442 }
1443 state.preferred_x_px = Some(px);
1444 let (cx, cy, _) = caret_xy_for_byte(
1446 &state.text,
1447 font_px,
1448 wrap_w,
1449 state.caret_index(),
1450 );
1451 let iw = state.inner_width;
1452 let ih = state.inner_height;
1453 state.ensure_caret_visible_xy(
1454 cx,
1455 cy,
1456 iw,
1457 ih,
1458 self.dp_px(2.0),
1459 );
1460 self.request_redraw();
1461 }
1462 }
1463 PhysicalKey::Code(KeyCode::Home) => {
1464 state.selection = 0..0;
1465 App::tf_ensure_caret_visible(
1466 &mut state,
1467 self.is_multiline_id(focused_id),
1468 );
1469 self.request_redraw();
1470 }
1471 PhysicalKey::Code(KeyCode::End) => {
1472 {
1473 let end = state.text.len();
1474 state.selection = end..end;
1475 }
1476 App::tf_ensure_caret_visible(
1477 &mut state,
1478 self.is_multiline_id(focused_id),
1479 );
1480 self.request_redraw();
1481 }
1482 _ => {}
1483 }
1484 }
1485 }
1486
1487 if !self.ime_preedit
1489 && !self.modifiers.ctrl
1490 && !self.modifiers.alt
1491 && !self.modifiers.meta
1492 && let Some(raw) = key_event.text.as_deref()
1493 {
1494 let text: String = raw
1495 .chars()
1496 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
1497 .collect();
1498 if !text.is_empty()
1499 && let Some(fid) = self.sched.focused
1500 {
1501 let key = self.tf_key_of(fid);
1502 if let Some(state_rc) = self.textfield_states.get(&key) {
1503 let mut st = state_rc.borrow_mut();
1504 st.insert_text(&text);
1505 self.notify_text_change(fid, st.text.clone());
1506 if let Some(f) = &self.frame_cache
1507 && let Some(hit) =
1508 f.hit_regions.iter().find(|h| h.id == fid)
1509 {
1510 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1511 }
1512 self.request_redraw();
1513 }
1514 }
1515 }
1516 } else if key_event.state == ElementState::Released {
1517 if let Some(active_id) = self.key_pressed_active {
1519 match key_event.physical_key {
1520 PhysicalKey::Code(KeyCode::Space)
1521 | PhysicalKey::Code(KeyCode::Enter) => {
1522 self.pressed_ids.remove(&active_id);
1523 self.key_pressed_active = None;
1524
1525 if let Some(f) = &self.frame_cache
1526 && let Some(hit) =
1527 f.hit_regions.iter().find(|h| h.id == active_id)
1528 {
1529 if let Some(cb) = &hit.on_click {
1530 cb();
1531 } else if let Some(cb) = &hit.on_pointer_down {
1532 let pe = PointerEvent::new(
1533 PointerId(0),
1534 PointerKind::Mouse,
1535 PointerEventKind::Down(PointerButton::Primary),
1536 Vec2 { x: 0.0, y: 0.0 },
1537 1.0,
1538 self.modifiers,
1539 );
1540 cb(pe);
1541 }
1542 if let Some(node) =
1543 f.semantics_nodes.iter().find(|n| n.id == active_id)
1544 {
1545 let label = node.label.as_deref().unwrap_or("");
1546 self.a11y.announce(&format!("Activated {}", label));
1547 }
1548 }
1549 self.request_redraw();
1550 }
1551 _ => {}
1552 }
1553 }
1554 }
1555 }
1556
1557 WindowEvent::Ime(ime) => {
1558 if let Some(focused_id) = self.sched.focused {
1559 let key = self.tf_key_of(focused_id);
1560 if let Some(state_rc) = self.textfield_states.get(&key) {
1561 let mut state = state_rc.borrow_mut();
1562 let on_text_change = self
1563 .frame_cache
1564 .as_ref()
1565 .and_then(|f| f.hit_regions.iter().find(|h| h.id == focused_id))
1566 .and_then(|h| h.on_text_change.clone());
1567 let mut notify = |text: String| {
1568 if let Some(cb) = &on_text_change {
1569 cb(text);
1570 }
1571 };
1572 rc_android::handle_ime_event(
1573 ime,
1574 &mut state,
1575 &mut notify,
1576 &mut self.ime_preedit,
1577 );
1578 self.request_redraw();
1579 }
1580 }
1581 }
1582
1583 WindowEvent::RedrawRequested => {
1584 if !self.redraw_requested.replace(false) {
1586 self.process_a11y_actions();
1587 self.process_render_commands();
1588 log::trace!("RedrawRequested: no frame request, skipping compose");
1589 return;
1590 }
1591 log::trace!("RedrawRequested: frame request pending, composing");
1592
1593 self.process_a11y_actions();
1595 self.process_render_commands();
1596
1597 let Some(win) = self.window.as_ref() else {
1598 return;
1599 };
1600 if self.backend.is_none() {
1601 return;
1602 }
1603
1604 repose_core::animation_driver::tick();
1607
1608 let t0 = Instant::now();
1609 let scale = win.scale_factor() as f32;
1610 let size_px_u32 = self.sched.size;
1611 let focused = self.sched.focused;
1612
1613 let rc = self.render.clone();
1614 let root_fn = &mut self.root;
1615 let mut composed_root = |s: &mut Scheduler| (root_fn)(s, &rc);
1616
1617 let frame = compose_frame(
1618 &mut self.sched,
1619 &mut composed_root,
1620 scale,
1621 size_px_u32,
1622 self.hover_id,
1623 &self.pressed_ids,
1624 &self.textfield_states,
1625 focused,
1626 );
1627
1628 if focused.is_some() && self.sched.focused.is_none() && self.ime_preedit {
1629 rc_web::set_ime_for_textfield(win, false);
1630 self.ime_preedit = false;
1631 }
1632
1633 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1634
1635 if let Some(adapter) = &mut self.accesskit_adapter {
1637 let win = self.window.as_ref().unwrap();
1638 let scale = win.scale_factor();
1639 if let Some(update) =
1640 self.a11y_tree
1641 .update(&frame.semantics_nodes, scale, self.sched.focused)
1642 {
1643 adapter.update_if_active(|| update);
1644 }
1645 }
1646
1647 let mut scene = frame.scene.clone();
1649 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1651 let signal_count = self.sched.id_count() as usize;
1652 self.inspector.hud.metrics = Some(repose_devtools::Metrics {
1653 build_ms: build_layout_ms,
1654 layout_ms: build_layout_ms * 0.5,
1655 scene_nodes: scene.nodes.len(),
1656 widget_count,
1657 signal_count,
1658 });
1659 self.inspector.frame(&mut scene);
1660
1661 repose_core::dnd::overlay_drag_indicator(
1663 &mut scene,
1664 self.mouse_pos_px,
1665 self.external_file_drag,
1666 );
1667
1668 let win = self.window.as_ref().unwrap();
1670 let scale = win.scale_factor() as f32;
1671 if let Some(backend) = self.backend.as_mut() {
1672 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1673 }
1674
1675 if let Some(fid) = self.sched.focused {
1678 if let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
1679 && let Some(key) = hit.tf_state_key
1680 && !self.textfield_states.contains_key(&key)
1681 {
1682 self.textfield_states
1683 .entry(key)
1684 .or_insert_with(|| {
1685 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1686 })
1687 .borrow_mut()
1688 .reset_caret_blink();
1689 }
1690 }
1691
1692 repose_core::dnd::set_dnd_frame(Some(frame.clone()));
1693 self.frame_cache = Some(frame);
1694 repose_core::dnd::set_dnd_scale(scale);
1695
1696 self.dispatch_file_drop_now();
1697
1698 rc::tick_snackbar(self.last_redraw);
1699 self.last_redraw = Instant::now();
1700 }
1701
1702 _ => {}
1703 }
1704 }
1705
1706 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1707 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1710 if let Some(cb) = ABOUT_TO_WAIT_CALLBACK.lock().unwrap().as_ref() {
1711 cb();
1712 }
1713 process_deeplinks();
1714
1715 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
1718 if WINDOW_VISIBLE.load(Ordering::Relaxed) && self.backend.is_none() {
1719 if let Some(w) = &self.window {
1720 log::info!("about_to_wait: recreating GPU backend");
1721 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
1722 Ok(b) => self.backend = Some(b),
1723 Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
1724 }
1725 }
1726 }
1727
1728 if take_frame_request() {
1729 self.pending_redraw = true;
1730 }
1731 if !self.pending_redraw {
1732 let now = Instant::now();
1733 let idle_interval = web_time::Duration::from_millis(1000);
1734 if now.saturating_duration_since(self.last_redraw) >= idle_interval {
1735 self.redraw_requested.set(true);
1736 request_frame();
1737 rc::request_redraw(&self.window);
1738 self.last_redraw = now;
1739 }
1740 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1741 self.last_redraw + idle_interval,
1742 ));
1743 return;
1744 }
1745
1746 let now = Instant::now();
1747 let interval = web_time::Duration::from_millis(16);
1748
1749 if now.saturating_duration_since(self.last_redraw) >= interval {
1750 self.pending_redraw = false;
1751 self.redraw_requested.set(true);
1752 rc::request_redraw(&self.window);
1753 self.last_redraw = now;
1754 } else {
1755 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1756 self.last_redraw + interval,
1757 ));
1758 }
1759 }
1760
1761 fn new_events(
1762 &mut self,
1763 _: &winit::event_loop::ActiveEventLoop,
1764 _: winit::event::StartCause,
1765 ) {
1766 }
1767 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
1768 self.pending_redraw = true;
1769 }
1770 fn device_event(
1771 &mut self,
1772 _: &winit::event_loop::ActiveEventLoop,
1773 _: winit::event::DeviceId,
1774 _: winit::event::DeviceEvent,
1775 ) {
1776 }
1777 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1778 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1779 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1780 }
1781
1782 impl App {
1783 fn dispatch_focus_key_event(
1786 &self,
1787 key_event: &winit::event::KeyEvent,
1788 mapped_key: &repose_core::input::Key,
1789 ) -> bool {
1790 let Some(f) = &self.frame_cache else {
1791 return false;
1792 };
1793 let Some(focused) = self.sched.focused else {
1794 return false;
1795 };
1796 let utf16 = match mapped_key {
1797 repose_core::input::Key::Character(c) => *c as u16,
1798 _ => 0,
1799 };
1800 let mods = self.modifiers;
1801 let repeat = key_event.repeat;
1802 let ev_type = if key_event.state == ElementState::Pressed {
1803 repose_core::input::KeyEventType::Down
1804 } else {
1805 repose_core::input::KeyEventType::Up
1806 };
1807 let hit_by_id: std::collections::HashMap<u64, &HitRegion> =
1808 f.hit_regions.iter().map(|h| (h.id, h)).collect();
1809 let sem_parent_of: std::collections::HashMap<u64, u64> = f
1810 .semantics_nodes
1811 .iter()
1812 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1813 .collect();
1814 let mut ancestors = Vec::new();
1815 let mut cur = focused;
1816 loop {
1817 ancestors.push(cur);
1818 if let Some(&p) = sem_parent_of.get(&cur) {
1819 cur = p;
1820 } else {
1821 break;
1822 }
1823 }
1824 let make_ke = || repose_core::input::KeyEvent {
1825 key: mapped_key.clone(),
1826 modifiers: mods,
1827 is_repeat: repeat,
1828 event_type: ev_type,
1829 utf16_code_point: utf16,
1830 };
1831 for &id in ancestors.iter().rev() {
1833 if let Some(hit) = hit_by_id.get(&id) {
1834 if let Some(cb) = &hit.on_preview_key_event {
1835 if cb(make_ke()) {
1836 return true;
1837 }
1838 }
1839 }
1840 }
1841 for &id in ancestors.iter() {
1843 if let Some(hit) = hit_by_id.get(&id) {
1844 if let Some(cb) = &hit.on_key_event {
1845 if cb(make_ke()) {
1846 return true;
1847 }
1848 }
1849 }
1850 }
1851 false
1852 }
1853
1854 fn announce_focus_change(&mut self) {
1855 if let Some(f) = &self.frame_cache {
1856 let focused_node = self
1857 .sched
1858 .focused
1859 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1860 self.a11y.focus_changed(focused_node);
1861 }
1862 }
1863
1864 fn notify_text_change(&self, id: u64, text: String) {
1865 if let Some(f) = &self.frame_cache
1866 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1867 && let Some(cb) = &h.on_text_change
1868 {
1869 cb(text);
1870 }
1871 }
1872
1873 fn tf_key_of(&self, visual_id: u64) -> u64 {
1874 rc::tf_key_of_in_frame(&self.frame_cache, visual_id)
1875 }
1876
1877 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1878 use repose_core::shortcuts;
1879
1880 if let (Some(f), Some(fid)) = (&self.frame_cache, self.sched.focused)
1881 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1882 && let Some(cb) = &hit.on_action
1883 && cb(action.clone())
1884 {
1885 return true;
1886 }
1887
1888 if shortcuts::handle(action.clone()) {
1889 return true;
1890 }
1891
1892 if let Some(f) = &self.frame_cache {
1894 if let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.sched, f)
1895 {
1896 if let Some(active) = self.key_pressed_active.take() {
1897 self.pressed_ids.remove(&active);
1898 }
1899 let tf_state_key = f
1900 .hit_regions
1901 .iter()
1902 .find(|h| h.id == new_id)
1903 .and_then(|h| h.tf_state_key);
1904 if let Some(key) = tf_state_key {
1905 self.textfield_states.entry(key).or_insert_with(|| {
1906 Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
1907 });
1908 if let Some(state_rc) = self.textfield_states.get(&key) {
1909 state_rc.borrow_mut().reset_caret_blink();
1910 }
1911 }
1912 if let Some(win) = &self.window {
1913 let is_textfield = f.semantics_nodes.iter().any(|n| {
1914 n.id == new_id && n.role == repose_core::semantics::Role::TextField
1915 });
1916 rc_web::set_ime_for_textfield(win, is_textfield);
1917 }
1918 self.announce_focus_change();
1919 return true;
1920 }
1921 }
1922
1923 false
1924 }
1925
1926 fn dispatch_file_drop_now(&mut self) {
1927 let Some(f) = &self.frame_cache else {
1928 self.pending_dropped_files.clear();
1929 self.pending_drop_pos_px = None;
1930 return;
1931 };
1932
1933 if self.pending_dropped_files.is_empty() {
1934 return;
1935 }
1936
1937 let pos_px = self.pending_drop_pos_px.unwrap_or(self.mouse_pos_px);
1938 let pos = Vec2 {
1939 x: pos_px.0,
1940 y: pos_px.1,
1941 };
1942
1943 let mut files = Vec::new();
1944 for p in self.pending_dropped_files.drain(..) {
1945 let name = p
1946 .file_name()
1947 .and_then(|s| s.to_str())
1948 .unwrap_or("file")
1949 .to_string();
1950 files.push(repose_core::dnd::DroppedFile {
1951 name,
1952 path: Some(p),
1953 });
1954 }
1955
1956 let payload: repose_core::dnd::DragPayload =
1957 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1958
1959 let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
1960 self.pending_drop_pos_px = None;
1961 return;
1962 };
1963
1964 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
1965 && let Some(cb) = &hit.on_drop
1966 {
1967 let accepted = cb(repose_core::dnd::DropEvent {
1968 source_id: 0, target_id,
1970 position: pos,
1971 modifiers: self.modifiers,
1972 payload: payload.clone(),
1973 });
1974
1975 if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
1976 {
1977 let label = node.label.as_deref().unwrap_or("");
1978 self.a11y.announce(&format!("Dropped files on {}", label));
1979 }
1980 }
1981
1982 self.pending_drop_pos_px = None;
1983 self.request_redraw();
1984 }
1985 }
1986
1987 let event_loop = EventLoop::new()?;
1988 set_event_loop_proxy(event_loop.create_proxy());
1989 let mut app = App::new(Box::new(root));
1990 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1992 event_loop.run_app(&mut app)?;
1993 Ok(())
1994}
1995
1996pub trait A11yBridge: Send {
2004 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
2006
2007 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
2009
2010 fn announce(&mut self, msg: &str);
2012}
2013
2014struct NoopA11y;
2015impl A11yBridge for NoopA11y {
2016 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
2017 }
2019 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
2020 if let Some(n) = node {
2021 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
2022 } else {
2023 log::info!("A11y focus: None");
2024 }
2025 }
2026 fn announce(&mut self, msg: &str) {
2027 log::info!("A11y announce: {msg}");
2028 }
2029}
2030
2031#[cfg(target_os = "linux")]
2032struct LinuxAtspiStub;
2033#[cfg(target_os = "linux")]
2034impl A11yBridge for LinuxAtspiStub {
2035 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
2036 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
2037 }
2038 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
2039 if let Some(n) = node {
2040 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
2041 } else {
2042 log::info!("AT-SPI stub focus: None");
2043 }
2044 }
2045 fn announce(&mut self, msg: &str) {
2046 log::info!("AT-SPI stub announce: {msg}");
2047 }
2048}