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