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