1use std::cell::RefCell;
2use std::collections::{HashMap, HashSet};
3use std::rc::Rc;
4
5use repose_core::dnd;
6use repose_core::input::{
7 GamepadAxis, GamepadButton, GamepadEvent, GamepadId, ImeEvent, Key, KeyEvent, KeyEventType,
8 Modifiers, PointerButton, PointerEvent, PointerEventKind, PointerId, PointerKind, SensorKind,
9 SensorSample,
10};
11use repose_core::locals::{Density, set_density_default, with_density};
12use repose_core::runtime::{Frame, Scheduler};
13use repose_core::shortcuts::DragAction;
14use repose_core::{
15 CursorIcon, HitRegion, Interaction, Modifier, RenderContext, Scene, Vec2, View, request_frame,
16};
17use repose_ui::textfield::TextFieldState;
18use repose_ui::{Interactions, layout_and_paint};
19
20fn ensure_tf_state(
21 map: &mut HashMap<u64, Rc<RefCell<TextFieldState>>>,
22 key: u64,
23 seed: &str,
24) -> Rc<RefCell<TextFieldState>> {
25 map.entry(key)
26 .or_insert_with(|| {
27 Rc::new(RefCell::new(if seed.is_empty() {
28 TextFieldState::new()
29 } else {
30 TextFieldState::with_text(seed.to_string())
31 }))
32 })
33 .clone()
34}
35
36fn ensure_all_tf_states_from_frame(
37 map: &mut HashMap<u64, Rc<RefCell<TextFieldState>>>,
38 frame: &Frame,
39) {
40 for hit in &frame.hit_regions {
41 if let Some(key) = hit.tf_state_key {
42 let st = ensure_tf_state(map, key, hit.tf_value.as_str());
43 st.borrow_mut()
45 .apply_controlled_value(hit.tf_value.as_str());
46 }
47 }
48}
49
50fn is_tf_hit(f: &Frame, id: u64) -> bool {
51 f.hit_regions
52 .iter()
53 .any(|h| h.id == id && h.tf_state_key.is_some())
54 || is_textfield_in_frame(f, id)
55}
56
57#[derive(Clone, Default)]
59pub struct PlatformOutput {
60 pub cursor: Option<CursorIcon>,
62 pub ime_allowed: bool,
64 pub ime_cursor_area: Option<(f64, f64, f64, f64)>,
66 pub clipboard_text: Option<String>,
68
69 pub ime_purpose: repose_core::ImePurposeHint,
72 pub ime_auto_correct: bool,
73 pub ime_capitalization: repose_core::KeyboardCapitalization,
74 pub keyboard_type: repose_core::KeyboardType,
75
76 pub window_theme_dark: Option<bool>,
79}
80
81pub struct FrameOutput {
83 pub scene: Scene,
85 pub hit_regions: Vec<HitRegion>,
87 pub semantics_nodes: Vec<repose_core::runtime::SemNode>,
89 pub focus_chain: Vec<u64>,
91 pub platform: PlatformOutput,
93 pub wants_pointer: bool,
95 pub wants_keyboard: bool,
97}
98
99impl FrameOutput {
100 pub fn into_frame(self) -> Frame {
103 Frame {
104 scene: self.scene,
105 hit_regions: self.hit_regions,
106 semantics_nodes: self.semantics_nodes,
107 focus_chain: self.focus_chain,
108 }
109 }
110
111 pub fn into_shared_frame(self) -> Rc<Frame> {
112 Rc::new(self.into_frame())
113 }
114}
115
116pub struct PointerMoveResult {
118 pub cursor: Option<CursorIcon>,
120 pub hover_id: Option<u64>,
122}
123
124#[derive(Debug)]
126pub struct PointerButtonResult {
127 pub focused: Option<u64>,
129 pub capture_id: Option<u64>,
131 pub consumed: bool,
133 pub needs_a11y_announce: bool,
135 pub clicked_id: Option<u64>,
138}
139
140const LONG_PRESS_MS: u128 = 500;
142const DOUBLE_CLICK_MS: u128 = 300;
143const DOUBLE_TAP_MIN_MS: u128 = 40;
144const LONG_PRESS_SLOP_DP: f32 = 18.0;
145pub(crate) const TAP_SLOP_DP: f32 = 18.0;
150
151fn moved_past_slop(x0: f32, y0: f32, pos: Vec2, slop_dp: f32, scale: f32) -> bool {
154 let slop = slop_dp * scale;
155 let dx = pos.x - x0;
156 let dy = pos.y - y0;
157 dx * dx + dy * dy > slop * slop
158}
159
160struct TouchPressState {
161 capture_id: u64,
162 pressed_ids: HashSet<u64>,
163}
164
165struct DndPointerCapture {
166 touch: Option<u64>,
167 source_id: u64,
168}
169
170pub struct ReposeRuntime {
176 pub sched: Scheduler,
177 pub scale: f32,
178 pub overlay: repose_ui::overlay::OverlayHandle,
181 pub dnd_context: repose_core::dnd::DndContext,
182 lifecycle_events: std::sync::Arc<crate::lifecycle::LifecycleDispatcher>,
183 deeplink_events: std::sync::Arc<crate::lifecycle::DeeplinkDispatcher>,
184
185 pub modifiers: Modifiers,
186 pub mouse_pos_px: (f32, f32),
187 pub pointer_inside: bool,
189 pub hover_id: Option<u64>,
190 pub hover_ancestors: std::collections::HashSet<u64>,
191 hover_leave: HashMap<u64, repose_ui::HitRegionSnapshot>,
195 pub capture_id: Option<u64>,
196 pub hit_path: Option<Vec<u64>>,
199 pub touch_paths: HashMap<u64, Vec<u64>>,
208 mouse_targets: Option<Vec<repose_ui::HitRegionSnapshot>>,
209 touch_targets: HashMap<u64, Vec<repose_ui::HitRegionSnapshot>>,
210 touch_positions: HashMap<u64, Vec2>,
211 touch_presses: HashMap<u64, TouchPressState>,
212 dnd_capture: Option<DndPointerCapture>,
213 touch_primary: Option<u64>,
214 suppressed_touch_clicks: HashSet<u64>,
215 pub scroll_capture_id: Option<u64>,
217 last_scroll_at: Option<web_time::Instant>,
218 pub pressed_ids: HashSet<u64>,
219 pub ime_preedit: bool,
220 pub key_pressed_active: Option<u64>,
221 key_pressed_key: Option<Key>,
222 pub last_focus: Option<u64>,
223 pub held_keys: HashSet<String>,
231 pub held_mouse: HashSet<PointerButton>,
236
237 last_up: Option<(u64, web_time::Instant, f32, f32)>,
238 last_down: Option<(u64, web_time::Instant)>,
242 press_origin: Option<(u64, f32, f32)>,
246 double_candidate: Option<u64>,
251 long_press: Option<(u64, web_time::Instant, f32, f32)>,
252 long_press_touch: Option<u64>,
253 key_long_press: Option<(u64, web_time::Instant, bool)>,
256 suppress_next_click: bool,
257 pending_click: Option<(u64, web_time::Instant, Rc<dyn Fn()>)>,
258
259 pub frame_cache: Option<Rc<Frame>>,
260
261 cursor: Option<CursorIcon>,
262 frame_cursor_override: Option<CursorIcon>,
263 fullscreen_request: Option<bool>,
264
265 pub shortcuts: repose_core::shortcuts::ShortcutState,
266
267 pub textfield_states: HashMap<u64, Rc<RefCell<TextFieldState>>>,
268 pub gamepads: HashMap<u32, GamepadPad>,
271 pub pending_rumble: Vec<(u32, f32, f32, u32)>,
273 pub sensors: HashMap<(u32, SensorKind), [f32; 3]>,
276}
277
278#[derive(Clone, Debug, Default)]
280pub struct GamepadPad {
281 pub name: String,
282 pub vendor_id: u16,
286 pub product_id: u16,
287 pub pressed: HashSet<GamepadButton>,
288 pub axes: HashMap<GamepadAxis, f32>,
289}
290
291impl GamepadPad {
292 pub fn button(&self, button: GamepadButton) -> bool {
293 self.pressed.contains(&button)
294 }
295
296 pub fn axis(&self, axis: GamepadAxis) -> f32 {
297 self.axes.get(&axis).copied().unwrap_or(0.0)
298 }
299}
300
301impl ReposeRuntime {
302 pub fn new() -> Self {
303 Self::with_overlay(repose_ui::overlay::OverlayHandle::new())
304 }
305
306 pub fn with_overlay(overlay: repose_ui::overlay::OverlayHandle) -> Self {
310 Self {
311 overlay,
312 dnd_context: repose_core::dnd::DndContext::default(),
313 lifecycle_events: std::sync::Arc::new(crate::lifecycle::LifecycleDispatcher::default()),
314 deeplink_events: std::sync::Arc::new(crate::lifecycle::DeeplinkDispatcher::default()),
315 sched: Scheduler::new(),
316 scale: 1.0,
317 modifiers: Modifiers::default(),
318 mouse_pos_px: (0.0, 0.0),
319 pointer_inside: false,
320 hover_id: None,
321 hover_ancestors: std::collections::HashSet::new(),
322 hover_leave: HashMap::new(),
323 capture_id: None,
324 hit_path: None,
325 touch_paths: HashMap::new(),
326 mouse_targets: None,
327 touch_targets: HashMap::new(),
328 touch_positions: HashMap::new(),
329 touch_presses: HashMap::new(),
330 dnd_capture: None,
331 touch_primary: None,
332 suppressed_touch_clicks: HashSet::new(),
333 scroll_capture_id: None,
334 last_scroll_at: None,
335 pressed_ids: HashSet::new(),
336 ime_preedit: false,
337 key_pressed_active: None,
338 key_pressed_key: None,
339 last_focus: None,
340 held_keys: HashSet::new(),
341 held_mouse: HashSet::new(),
342 last_up: None,
343 last_down: None,
344 press_origin: None,
345 double_candidate: None,
346 long_press: None,
347 long_press_touch: None,
348 key_long_press: None,
349 suppress_next_click: false,
350 pending_click: None,
351 frame_cache: None,
352 cursor: None,
353 frame_cursor_override: None,
354 fullscreen_request: None,
355 shortcuts: repose_core::shortcuts::ShortcutState::new().without_global_fallback(),
356 textfield_states: HashMap::new(),
357 gamepads: HashMap::new(),
358 pending_rumble: Vec::new(),
359 sensors: HashMap::new(),
360 }
361 }
362
363 pub fn event_dispatchers(&self) -> crate::lifecycle::RuntimeDispatchers {
364 crate::lifecycle::RuntimeDispatchers {
365 lifecycle: self.lifecycle_events.clone(),
366 deeplink: self.deeplink_events.clone(),
367 }
368 }
369
370 pub fn set_lifecycle_callback(
371 &mut self,
372 callback: Box<dyn Fn(crate::lifecycle::AppLifecycle) + Send>,
373 ) {
374 self.lifecycle_events.set_callback(callback);
375 }
376
377 pub fn add_lifecycle_listener(
378 &mut self,
379 callback: Box<dyn Fn(crate::lifecycle::AppLifecycle) + Send>,
380 ) -> u64 {
381 self.lifecycle_events.add_listener(callback)
382 }
383
384 pub fn remove_lifecycle_listener(&mut self, id: u64) -> bool {
385 self.lifecycle_events.remove_listener(id)
386 }
387
388 pub fn current_lifecycle(&self) -> Option<crate::lifecycle::AppLifecycle> {
389 self.lifecycle_events.current()
390 }
391
392 pub fn push_lifecycle(&mut self, state: crate::lifecycle::AppLifecycle) {
393 self.lifecycle_events.push(state);
394 }
395
396 pub fn process_lifecycle(&mut self) {
397 self.lifecycle_events.process();
398 }
399
400 pub fn set_deeplink_callback(&mut self, callback: Box<dyn Fn(Vec<u8>) + Send>) {
401 self.deeplink_events.set_callback(callback);
402 }
403
404 pub fn add_deeplink_listener(&mut self, callback: Box<dyn Fn(Vec<u8>) + Send>) -> u64 {
405 self.deeplink_events.add_listener(callback)
406 }
407
408 pub fn remove_deeplink_listener(&mut self, id: u64) -> bool {
409 self.deeplink_events.remove_listener(id)
410 }
411
412 pub fn push_deeplink(&mut self, data: Vec<u8>) {
413 self.deeplink_events.push(data);
414 }
415
416 pub fn process_deeplinks(&mut self) {
417 self.deeplink_events.process();
418 }
419
420 pub fn set_viewport(&mut self, width_px: u32, height_px: u32) {
422 self.sched.size = (width_px, height_px);
423 }
424
425 pub fn set_viewport_and_scale(&mut self, width_px: u32, height_px: u32, scale: f32) {
427 self.scale = scale;
428 self.sched.size = (width_px, height_px);
429 }
430
431 pub fn tick_animations(&self) {
433 repose_core::animation_driver::tick();
434 }
435
436 pub fn poll_gesture_timers(&mut self) {
437 self.poll_long_press();
438 self.flush_pending_click();
439 self.poll_key_long_press();
440 repose_ui::scrollbar_repeat::poll_track_repeat();
441 }
442
443 pub fn compose<F>(&mut self, root_fn: &mut F, render_ctx: &RenderContext) -> Frame
448 where
449 F: FnMut(&mut Scheduler, &RenderContext) -> View,
450 {
451 let _dnd_guard = self.dnd_context.enter();
452 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
453 self.poll_long_press();
454 self.flush_pending_click();
455 self.poll_key_long_press();
456 repose_ui::scrollbar_repeat::poll_track_repeat();
457
458 let size = self.sched.size;
459 let rc = render_ctx.clone();
460 let overlay = self.overlay.clone();
461 let mut compose_once = |this: &mut Self| {
462 let overlay = overlay.clone();
463 let ambient = this.overlay.clone();
464 let mut inner = |s: &mut Scheduler| {
465 repose_ui::overlay::with_ambient_overlay(ambient.clone(), || {
466 let content = (root_fn)(s, &rc);
467 overlay.host(Modifier::new().fill_max_size(), content)
468 })
469 };
470 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
471 compose_frame_inner_with_ancestors(
472 &mut this.sched,
473 &mut inner,
474 this.scale,
475 size,
476 this.hover_id,
477 &this.hover_ancestors,
478 &this.pressed_ids,
479 &this.textfield_states,
480 )
481 })) {
482 Ok(frame) => frame,
483 Err(_) => {
484 log::error!("compose panicked; presenting last good frame");
485 this.frame_cache
486 .as_deref()
487 .cloned()
488 .unwrap_or_else(|| Frame {
489 scene: Default::default(),
490 hit_regions: Vec::new(),
491 semantics_nodes: Vec::new(),
492 focus_chain: Vec::new(),
493 })
494 }
495 }
496 };
497
498 let shortcut_state = self.shortcuts.clone();
499 let frame =
500 repose_core::shortcuts::with_runtime_state(&shortcut_state, || compose_once(self));
501
502 let hover_before = self.hover_id;
506 self.reconcile_hover_from_mouse_pos(&frame);
507 if self.hover_id != hover_before {
508 self.hover_leave.clear();
511 for h in &frame.hit_regions {
512 if h.on_pointer_leave.is_some() {
513 self.hover_leave
514 .insert(h.id, repose_ui::HitRegionSnapshot::new(h));
515 }
516 }
517 return repose_core::shortcuts::with_runtime_state(&shortcut_state, || {
519 compose_once(self)
520 });
521 }
522 frame
523 }
524
525 pub fn frame(
527 &mut self,
528 mut root_fn: impl FnMut(&mut Scheduler, &RenderContext) -> View,
529 render_ctx: &RenderContext,
530 ) -> FrameOutput {
531 let (f, clipboard_text) = repose_core::clipboard::with_captured_clipboard(|| {
532 self.compose(&mut root_fn, render_ctx)
533 });
534
535 let wants_pointer = self.hover_id.is_some() || self.capture_id.is_some();
536
537 let focused_hit = self
538 .sched
539 .focused
540 .and_then(|fid| f.hit_regions.iter().find(|h| h.id == fid));
541 let ime_allowed = focused_hit.is_some_and(|hit| {
542 hit.on_ime.is_some()
543 || (hit.tf_enabled
544 && !hit.tf_read_only
545 && f.semantics_nodes.iter().any(|node| {
546 node.id == hit.id && node.role == repose_core::semantics::Role::TextField
547 }))
548 });
549
550 let ime_cursor_area = if ime_allowed {
551 focused_hit.map(|hit| {
552 let sf = self.scale as f64;
553 let origin = repose_ui::hit_region_transformed_origin(hit);
554 let local = repose_ui::hit_region_local_rect(hit);
555 (
556 origin.x as f64 / sf,
557 origin.y as f64 / sf,
558 local.w as f64 / sf,
559 local.h as f64 / sf,
560 )
561 })
562 } else {
563 None
564 };
565
566 let focused_is_textfield = ime_allowed
567 || self.sched.focused.is_some_and(|fid| {
568 f.hit_regions
569 .iter()
570 .any(|h| h.id == fid && h.tf_state_key.is_some())
571 });
572 let wants_keyboard = focused_is_textfield || self.ime_preedit;
573
574 let (ime_purpose, ime_auto_correct, ime_capitalization, keyboard_type) =
575 match (ime_allowed, focused_hit) {
576 (true, Some(hit)) => (
577 hit.keyboard_type.ime_purpose_hint(),
578 hit.auto_correct.unwrap_or(true),
579 hit.capitalization,
580 hit.keyboard_type,
581 ),
582 _ => (
583 repose_core::ImePurposeHint::Normal,
584 true,
585 repose_core::KeyboardCapitalization::Unspecified,
586 repose_core::KeyboardType::Unspecified,
587 ),
588 };
589
590 let declared = self.sched.cursor_override.take();
591 self.frame_cursor_override = declared.clone();
592 let platform = PlatformOutput {
593 cursor: declared.or_else(|| self.take_cursor_suggestion()),
594 ime_allowed,
595 ime_cursor_area,
596 clipboard_text,
597 ime_purpose,
598 ime_auto_correct,
599 ime_capitalization,
600 keyboard_type,
601 window_theme_dark: Some(repose_core::locals::theme().is_dark()),
602 };
603 FrameOutput {
604 scene: f.scene,
605 hit_regions: f.hit_regions,
606 semantics_nodes: f.semantics_nodes,
607 focus_chain: f.focus_chain,
608 platform,
609 wants_pointer,
610 wants_keyboard,
611 }
612 }
613
614 pub fn cache_frame(&mut self, frame: impl Into<Rc<Frame>>) {
616 let frame = frame.into();
617 let _dnd_guard = self.dnd_context.enter();
618 if self.key_pressed_active.is_some_and(|id| {
619 self.sched.focused != Some(id)
620 || !frame
621 .hit_regions
622 .iter()
623 .any(|hit| hit.id == id && !hit.disabled)
624 }) {
625 self.cancel_keyboard_press();
626 }
627 self.hover_leave.clear();
628 for hit in &frame.hit_regions {
629 if hit.on_pointer_leave.is_some() {
630 self.hover_leave
631 .insert(hit.id, repose_ui::HitRegionSnapshot::new(hit));
632 }
633 }
634 self.frame_cache = Some(frame);
635 self.reconcile_cached_targets();
636 }
637
638 pub fn after_compose(&mut self, frame: &Frame, scale: f32) {
643 self.after_compose_shared(Rc::new(frame.clone()), scale);
644 }
645
646 pub fn after_compose_shared(&mut self, frame: Rc<Frame>, scale: f32) {
647 let _dnd_guard = self.dnd_context.enter();
648 ensure_all_tf_states_from_frame(&mut self.textfield_states, &frame);
649 self.prune_textfield_states(&frame);
650 self.ensure_focused_state_in_frame(&frame);
651 self.reconcile_hover_from_mouse_pos(&frame);
652 repose_core::dnd::set_shared_dnd_frame(Some(frame));
653 repose_core::dnd::set_dnd_scale(scale);
654 }
655
656 fn prune_textfield_states(&mut self, frame: &Frame) {
657 if self.textfield_states.is_empty() {
658 return;
659 }
660 let live: std::collections::HashSet<u64> = frame
661 .hit_regions
662 .iter()
663 .filter_map(|h| h.tf_state_key)
664 .collect();
665 self.textfield_states.retain(|k, _| live.contains(k));
666 }
667
668 fn end_textfield_drag(&mut self, id: u64) {
669 let key = self
670 .frame_cache
671 .as_ref()
672 .and_then(|frame| is_tf_hit(frame, id).then(|| tf_key_of(frame, id)));
673 if let Some(key) = key {
674 self.end_textfield_drag_key(key);
675 }
676 }
677
678 fn end_textfield_drag_key(&mut self, key: u64) {
679 if let Some(state) = self.textfield_states.get(&key) {
680 state.borrow_mut().end_drag();
681 }
682 }
683
684 pub fn ensure_focused_textfield_state(&mut self) {
687 let Some(f) = self.frame_cache.clone() else {
688 return;
689 };
690 self.ensure_focused_state_in_frame(&f);
691 }
692
693 fn ensure_focused_state_in_frame(&mut self, frame: &Frame) {
697 let Some(fid) = self.sched.focused else {
698 return;
699 };
700 if let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
701 && let Some(key) = hit.tf_state_key
702 {
703 let st = ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
704 st.borrow_mut().apply_controlled_value(&hit.tf_value);
705 }
706 }
707
708 pub fn cache_from_output(&mut self, out: &FrameOutput) {
712 let frame = Rc::new(Frame {
713 scene: out.scene.clone(),
714 hit_regions: out.hit_regions.clone(),
715 semantics_nodes: out.semantics_nodes.clone(),
716 focus_chain: out.focus_chain.clone(),
717 });
718 self.cache_frame(frame.clone());
719 self.after_compose_shared(frame, self.scale);
720 }
721
722 pub fn compose_frame_output<F>(
726 &mut self,
727 root: &mut F,
728 render_ctx: &RenderContext,
729 ) -> FrameOutput
730 where
731 F: FnMut(&mut Scheduler, &RenderContext) -> View,
732 {
733 self.tick_animations();
734 let out = self.frame(root, render_ctx);
735 self.cache_from_output(&out);
736 out
737 }
738
739 fn dispatch_pointer_to_targets(
740 &self,
741 kind: PointerEventKind,
742 pos: Vec2,
743 targets: &[repose_ui::HitRegionSnapshot],
744 touch: Option<u64>,
745 ) {
746 let (id, pkind) = match touch {
747 Some(tid) => (PointerId(tid), PointerKind::Touch),
748 None => (PointerId(0), PointerKind::Mouse),
749 };
750 let base = PointerEvent::new(id, pkind, kind, pos, 1.0, self.modifiers);
751 for target in targets {
752 let hit = target.hit();
753 let callback = match kind {
754 PointerEventKind::Down(_) => &hit.on_pointer_down,
755 PointerEventKind::Up(_) => &hit.on_pointer_up,
756 PointerEventKind::Move => &hit.on_pointer_move,
757 PointerEventKind::Cancel => &hit.on_pointer_cancel,
758 PointerEventKind::Enter | PointerEventKind::Leave => continue,
759 };
760 let Some(callback) = callback else {
761 continue;
762 };
763 let mut event = base.clone();
764 let (origin, local) = target.pointer_coordinates(pos);
765 event.origin = origin;
766 event.position = local;
767 callback(event);
768 if !matches!(kind, PointerEventKind::Cancel) && base.is_consumed() {
769 break;
770 }
771 }
772 }
773
774 fn dispatch_pointer_to_path(
775 &self,
776 kind: PointerEventKind,
777 pos: Vec2,
778 path: &[u64],
779 touch: Option<u64>,
780 ) {
781 let Some(frame) = &self.frame_cache else {
782 return;
783 };
784 let targets: Vec<_> = path
785 .iter()
786 .filter_map(|id| frame.hit_regions.iter().find(|hit| hit.id == *id))
787 .map(repose_ui::HitRegionSnapshot::new)
788 .collect();
789 self.dispatch_pointer_to_targets(kind, pos, &targets, touch);
790 }
791
792 fn refreshed_capture_targets(
798 &self,
799 targets: &[repose_ui::HitRegionSnapshot],
800 ) -> (
801 Vec<repose_ui::HitRegionSnapshot>,
802 Vec<repose_ui::HitRegionSnapshot>,
803 ) {
804 let Some(frame) = &self.frame_cache else {
805 return (targets.to_vec(), Vec::new());
806 };
807 let mut live = Vec::with_capacity(targets.len());
808 let mut stale = Vec::new();
809 for target in targets {
810 if frame
811 .hit_regions
812 .iter()
813 .any(|hit| hit.id == target.id() && !hit.disabled)
814 {
815 live.push(target.clone());
816 } else {
817 stale.push(target.clone());
818 }
819 }
820 (live, stale)
821 }
822
823 fn cancel_dnd_capture(&mut self, touch: Option<u64>, source_id: u64) -> bool {
824 if !self
825 .dnd_capture
826 .as_ref()
827 .is_some_and(|capture| capture.touch == touch && capture.source_id == source_id)
828 {
829 return false;
830 }
831 self.dnd_capture = None;
832 dnd::handle_drag_action(&DragAction::Cancel)
833 }
834
835 fn cancel_active_dnd(&mut self) -> bool {
836 if self.dnd_capture.take().is_none() {
837 return false;
838 }
839 dnd::handle_drag_action(&DragAction::Cancel)
840 }
841
842 fn reconcile_cached_targets(&mut self) {
843 let mouse_pos = Vec2 {
844 x: self.mouse_pos_px.0,
845 y: self.mouse_pos_px.1,
846 };
847 if let Some(targets) = self.mouse_targets.take() {
848 let capture_id = targets.first().map(|target| target.id());
849 let capture_key = targets.first().and_then(|target| target.hit().tf_state_key);
850 let (live, stale) = self.refreshed_capture_targets(&targets);
851 if !stale.is_empty() {
852 self.dispatch_pointer_to_targets(PointerEventKind::Cancel, mouse_pos, &stale, None);
853 }
854 let capture_stale =
855 capture_id.is_some_and(|id| stale.iter().any(|target| target.id() == id));
856 if capture_stale {
857 self.pressed_ids.remove(&capture_id.unwrap_or_default());
858 self.capture_id = None;
859 self.hit_path = None;
860 if let Some(key) = capture_key {
861 self.end_textfield_drag_key(key);
862 }
863 if self
864 .long_press
865 .is_some_and(|(id, _, _, _)| Some(id) == capture_id)
866 {
867 self.long_press = None;
868 self.long_press_touch = None;
869 }
870 self.cancel_dnd_capture(None, capture_id.unwrap_or_default());
871 } else {
872 self.mouse_targets = (!live.is_empty()).then_some(live);
873 self.hit_path = self
874 .mouse_targets
875 .as_ref()
876 .map(|targets| targets.iter().map(|target| target.id()).collect());
877 }
878 if !stale.is_empty() {
879 request_frame();
880 }
881 }
882
883 let mut touch_ids: Vec<u64> = self.touch_targets.keys().copied().collect();
884 touch_ids.sort_unstable();
885 let mut changed = false;
886 for tid in touch_ids {
887 let Some(targets) = self.touch_targets.remove(&tid) else {
888 continue;
889 };
890 let pos = self.touch_positions.get(&tid).copied().unwrap_or(mouse_pos);
891 let old_capture = self.touch_presses.get(&tid).map(|state| state.capture_id);
892 let old_capture_key = targets.first().and_then(|target| target.hit().tf_state_key);
893 let (live, stale) = self.refreshed_capture_targets(&targets);
894 if !stale.is_empty() {
895 self.dispatch_pointer_to_targets(PointerEventKind::Cancel, pos, &stale, Some(tid));
896 changed = true;
897 }
898 let capture_stale =
899 old_capture.is_some_and(|id| stale.iter().any(|target| target.id() == id));
900 if capture_stale {
901 self.touch_targets.remove(&tid);
902 self.touch_paths.remove(&tid);
903 self.touch_presses.remove(&tid);
904 self.suppressed_touch_clicks.remove(&tid);
905 if let Some(key) = old_capture_key {
906 self.end_textfield_drag_key(key);
907 }
908 if self.long_press_touch == Some(tid)
909 && old_capture.is_some_and(|id| {
910 self.long_press
911 .is_some_and(|(long_id, _, _, _)| long_id == id)
912 })
913 {
914 self.long_press = None;
915 self.long_press_touch = None;
916 }
917 if self.touch_primary == Some(tid) {
918 self.touch_primary = None;
919 self.double_candidate = None;
920 self.pending_click = None;
921 self.last_up = None;
922 self.last_down = None;
923 self.press_origin = None;
924 self.suppress_next_click = false;
925 self.scroll_capture_id = None;
926 if let Some(source_id) = old_capture {
927 self.cancel_dnd_capture(Some(tid), source_id);
928 }
929 }
930 } else if live.is_empty() {
931 self.touch_paths.remove(&tid);
932 self.touch_presses.remove(&tid);
933 self.suppressed_touch_clicks.remove(&tid);
934 } else {
935 self.touch_targets.insert(tid, live.clone());
936 self.touch_paths
937 .insert(tid, live.iter().map(|target| target.id()).collect());
938 }
939 }
940 if changed {
941 self.rebuild_pressed_ids();
942 request_frame();
943 }
944 }
945
946 pub fn handle_pointer_move(&mut self, pos: Vec2) -> PointerMoveResult {
948 self.handle_touch_move(None, pos)
949 }
950
951 pub fn handle_touch_move(&mut self, touch: Option<u64>, pos: Vec2) -> PointerMoveResult {
959 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
960 let _dnd_guard = self.dnd_context.enter();
961 self.mouse_pos_px = (pos.x, pos.y);
962 if let Some(tid) = touch {
963 self.touch_positions.insert(tid, pos);
964 }
965 let from_mouse = touch.is_none();
966 if from_mouse {
967 self.pointer_inside = true;
968 self.sched.pointer_pos_px = Some((pos.x, pos.y));
969 }
970
971 let owns_dnd_move = self
972 .dnd_capture
973 .as_ref()
974 .is_some_and(|capture| capture.touch == touch);
975 let dnd_move_consumed = owns_dnd_move
976 && dnd::handle_drag_action(&DragAction::Move {
977 position: pos,
978 modifiers: self.modifiers,
979 });
980 if dnd_move_consumed {
981 self.cancel_keyboard_press();
982 request_frame();
983 }
984
985 let Some(f) = &self.frame_cache else {
986 return PointerMoveResult {
987 cursor: self.move_cursor_suggestion(),
988 hover_id: None,
989 };
990 };
991
992 let active_long_press_matches = match (touch, self.long_press_touch) {
993 (Some(tid), Some(active)) => tid == active,
994 (None, None) => true,
995 _ => false,
996 };
997 if active_long_press_matches && let Some((_, _, x0, y0)) = self.long_press {
998 let slop = LONG_PRESS_SLOP_DP * self.scale;
999 let dx = pos.x - x0;
1000 let dy = pos.y - y0;
1001 if dx * dx + dy * dy > slop * slop {
1002 self.long_press = None;
1003 self.long_press_touch = None;
1004 }
1005 }
1006
1007 if active_long_press_matches
1009 && self.long_press.is_some()
1010 && let Some(lid) = self.long_press.map(|(id, _, _, _)| id)
1011 && f.hit_regions
1012 .iter()
1013 .find(|h| h.id == lid)
1014 .is_none_or(|h| !repose_ui::hit_region_contains(h, pos))
1015 {
1016 self.long_press = None;
1017 self.long_press_touch = None;
1018 }
1019
1020 let text_capture = match touch {
1021 Some(tid) => self.touch_presses.get(&tid).map(|state| state.capture_id),
1022 None => self.capture_id,
1023 };
1024 let owns_text_drag = touch.is_none() || self.touch_primary == touch;
1025 if owns_text_drag
1026 && let Some(cid) = text_capture
1027 && is_tf_hit(f, cid)
1028 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1029 {
1030 let key = tf_key_of(f, cid);
1031 if let Some(st_rc) = self.textfield_states.get(&key) {
1032 let mut st = st_rc.borrow_mut();
1033 let local = repose_ui::hit_region_to_local(hit, pos);
1034 let local_rect = repose_ui::hit_region_local_rect(hit);
1035 let content_origin = hit
1036 .tf_content_origin
1037 .map(|(x, y)| repose_ui::hit_region_to_local(hit, Vec2 { x, y }))
1038 .unwrap_or(Vec2 {
1039 x: local_rect.x,
1040 y: local_rect.y,
1041 });
1042 let content_x = (local.x - content_origin.x + st.scroll_offset).max(0.0);
1043 let content_y = (local.y - content_origin.y + st.scroll_offset_y).max(0.0);
1044 let metrics = repose_ui::textfield::textfield_metrics(hit);
1045 let wrap_w = st.inner_width.max(1.0);
1046 let idx = if hit.tf_multiline {
1047 index_for_xy_bytes_vt(&st, &metrics, wrap_w, content_x, content_y)
1048 } else {
1049 index_for_x_bytes_vt(&st, &metrics, content_x)
1050 };
1051 st.drag_to(idx);
1052 }
1053 }
1054
1055 let top = repose_ui::hit_test_enabled_frame(f, pos);
1056
1057 if from_mouse {
1058 self.cursor = top
1059 .and_then(|h| h.cursor.clone())
1060 .or(Some(CursorIcon::Default));
1061 if dnd_move_consumed && dnd::is_dragging() {
1062 self.cursor = Some(CursorIcon::Grabbing);
1063 }
1064
1065 let new_hover = top.map(|h| h.id);
1066
1067 let old_chain = hover_chain_for(Some(f), self.hover_id);
1068 let new_chain = hover_chain_for(Some(f), new_hover);
1069 if new_chain != old_chain {
1070 dispatch_hover_change_bubbled(
1071 Some(f),
1072 &self.hover_leave,
1073 &mut self.hover_id,
1074 &mut self.hover_ancestors,
1075 new_hover,
1076 pos,
1077 self.modifiers,
1078 );
1079 request_frame();
1080 }
1081 }
1082
1083 if let Some(tid) = touch {
1084 if let Some(targets) = self.touch_targets.get(&tid) {
1085 self.dispatch_pointer_to_targets(PointerEventKind::Move, pos, targets, Some(tid));
1086 } else if let Some(path) = self.touch_paths.get(&tid).cloned() {
1087 self.dispatch_pointer_to_path(PointerEventKind::Move, pos, &path, Some(tid));
1088 }
1089 return PointerMoveResult {
1090 cursor: self.move_cursor_suggestion(),
1091 hover_id: self.hover_id,
1092 };
1093 }
1094
1095 if let Some(targets) = &self.mouse_targets {
1096 self.dispatch_pointer_to_targets(PointerEventKind::Move, pos, targets, None);
1097 } else if let Some(path) = self.hit_path.clone() {
1098 self.dispatch_pointer_to_path(PointerEventKind::Move, pos, &path, None);
1099 }
1100 if self.hit_path.is_none()
1101 && let Some(h) = top
1102 && let Some(cb) = &h.on_pointer_move
1103 {
1104 let (id, kind) = match touch {
1105 Some(tid) => (PointerId(tid), PointerKind::Touch),
1106 None => (PointerId(0), PointerKind::Mouse),
1107 };
1108 let mut pe =
1109 PointerEvent::new(id, kind, PointerEventKind::Move, pos, 1.0, self.modifiers);
1110 let (origin, local) = repose_ui::hit_region_pointer_coordinates(h, pos);
1111 pe.origin = origin;
1112 pe.position = local;
1113 cb(pe);
1114 }
1115
1116 PointerMoveResult {
1117 cursor: self.move_cursor_suggestion(),
1118 hover_id: self.hover_id,
1119 }
1120 }
1121
1122 pub fn handle_pointer_press(
1124 &mut self,
1125 pos: Vec2,
1126 button: PointerButton,
1127 ) -> PointerButtonResult {
1128 self.handle_touch_press(None, pos, button)
1129 }
1130
1131 pub fn handle_touch_press(
1136 &mut self,
1137 touch: Option<u64>,
1138 pos: Vec2,
1139 button: PointerButton,
1140 ) -> PointerButtonResult {
1141 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
1142 let _dnd_guard = self.dnd_context.enter();
1143 self.mouse_pos_px = (pos.x, pos.y);
1144 if let Some(tid) = touch {
1145 self.touch_positions.insert(tid, pos);
1146 }
1147 if touch.is_none() {
1148 self.sched.pointer_pos_px = Some((pos.x, pos.y));
1149 }
1150 if touch.is_none() {
1151 self.held_mouse.insert(button);
1152 }
1153 let _ = repose_core::request_input_mode(repose_core::InputMode::Touch);
1154
1155 if touch.is_none() && (self.capture_id.is_some() || self.hit_path.is_some()) {
1156 self.handle_pointer_cancel();
1157 }
1158 repose_ui::scrollbar_repeat::cancel_track_repeat_for_touch(touch);
1161 if let Some(tid) = touch
1162 && self.touch_presses.contains_key(&tid)
1163 {
1164 self.handle_touch_cancel(Some(tid));
1165 }
1166 if let Some(tid) = touch {
1167 self.touch_positions.insert(tid, pos);
1168 }
1169
1170 let Some(frame) = self.frame_cache.clone() else {
1171 return PointerButtonResult {
1172 focused: None,
1173 capture_id: None,
1174 consumed: false,
1175 needs_a11y_announce: false,
1176 clicked_id: None,
1177 };
1178 };
1179 let f = &frame;
1180
1181 let mut result = PointerButtonResult {
1182 focused: None,
1183 capture_id: None,
1184 consumed: false,
1185 needs_a11y_announce: false,
1186 clicked_id: None,
1187 };
1188 let primary_pointer = match touch {
1189 Some(tid) => {
1190 if self.touch_primary.is_none() {
1191 self.touch_primary = Some(tid);
1192 }
1193 self.touch_primary == Some(tid)
1194 }
1195 None => true,
1196 };
1197
1198 if primary_pointer {
1199 self.cancel_keyboard_press();
1200 }
1201
1202 let path = repose_ui::hit_test_frame_path(f, pos);
1203 if let Some(&capture_id) = path.first()
1204 && let Some(hit) = f.hit_regions.iter().find(|hit| hit.id == capture_id)
1205 {
1206 if touch.is_none() {
1207 self.hit_path = Some(path.clone());
1208 }
1209
1210 if touch.is_none() || primary_pointer {
1211 dnd::handle_drag_action(&DragAction::Press {
1212 position: pos,
1213 capture_id: hit.id,
1214 kind: match touch {
1215 Some(_) => PointerKind::Touch,
1216 None => PointerKind::Mouse,
1217 },
1218 modifiers: self.modifiers,
1219 });
1220 self.dnd_capture = Some(DndPointerCapture {
1221 touch,
1222 source_id: hit.id,
1223 });
1224 }
1225
1226 if touch.is_none() {
1227 self.capture_id = Some(hit.id);
1228 }
1229 result.capture_id = Some(hit.id);
1230 result.consumed = true;
1231 let targets: Vec<_> = path
1232 .iter()
1233 .filter_map(|id| f.hit_regions.iter().find(|hit| hit.id == *id))
1234 .map(repose_ui::HitRegionSnapshot::new)
1235 .collect();
1236 if let Some(tid) = touch {
1237 self.touch_paths.insert(tid, path.clone());
1238 self.touch_targets.insert(tid, targets);
1239 self.touch_presses.insert(
1240 tid,
1241 TouchPressState {
1242 capture_id: hit.id,
1243 pressed_ids: HashSet::from([hit.id]),
1244 },
1245 );
1246 } else {
1247 self.mouse_targets = Some(targets);
1248 }
1249
1250 if primary_pointer {
1254 self.last_down = Some((hit.id, web_time::Instant::now()));
1255 self.press_origin = Some((hit.id, pos.x, pos.y));
1256 self.double_candidate = if hit.on_double_click.is_some()
1261 && self.last_up.is_some_and(|(pid, t0, ux, uy)| {
1262 pid == hit.id
1263 && !moved_past_slop(ux, uy, pos, TAP_SLOP_DP, self.scale)
1264 && self.last_down.is_some_and(|(did, dt)| {
1265 did == hit.id
1266 && dt.duration_since(t0).as_millis() >= DOUBLE_TAP_MIN_MS
1267 && dt.duration_since(t0).as_millis() <= DOUBLE_CLICK_MS
1268 })
1269 }) {
1270 Some(hit.id)
1271 } else {
1272 None
1273 };
1274 if self.double_candidate.is_some() {
1275 self.pending_click = None;
1277 } else if self
1278 .pending_click
1279 .as_ref()
1280 .is_some_and(|(pid, ..)| *pid == hit.id)
1281 {
1282 if let Some((_, _, cb)) = self.pending_click.take() {
1287 cb();
1288 }
1289 }
1290
1291 if let PointerButton::Primary = button {
1292 self.long_press = if hit.on_long_click.is_some() {
1293 Some((hit.id, web_time::Instant::now(), pos.x, pos.y))
1294 } else {
1295 None
1296 };
1297 self.long_press_touch = touch;
1298 self.suppress_next_click = false;
1299 }
1300 }
1301
1302 if primary_pointer
1303 && hit.tf_enabled
1304 && (hit.tf_state_key.is_some() || is_textfield_in_frame(f, hit.id))
1305 {
1306 let key = tf_key_of(f, hit.id);
1307 let seed = hit.tf_value.as_str();
1308 let st_rc = ensure_tf_state(&mut self.textfield_states, key, seed);
1309 {
1310 let mut st = st_rc.borrow_mut();
1311 st.apply_controlled_value(seed);
1313
1314 if st.inner_width <= 0.0 {
1315 let local_rect = repose_ui::hit_region_local_rect(hit);
1316 st.set_inner_width(local_rect.w.max(1.0));
1317 st.set_inner_height(local_rect.h.max(1.0));
1318 }
1319
1320 let local = repose_ui::hit_region_to_local(hit, pos);
1321 let local_rect = repose_ui::hit_region_local_rect(hit);
1322 let content_origin = hit
1323 .tf_content_origin
1324 .map(|(x, y)| repose_ui::hit_region_to_local(hit, Vec2 { x, y }))
1325 .unwrap_or(Vec2 {
1326 x: local_rect.x,
1327 y: local_rect.y,
1328 });
1329 let content_x = (local.x - content_origin.x + st.scroll_offset).max(0.0);
1330 let content_y = (local.y - content_origin.y + st.scroll_offset_y).max(0.0);
1331 let metrics = repose_ui::textfield::textfield_metrics(hit);
1332 let wrap_w = st.inner_width.max(1.0);
1333
1334 let idx = if hit.tf_multiline {
1335 index_for_xy_bytes_vt(&st, &metrics, wrap_w, content_x, content_y)
1336 } else {
1337 index_for_x_bytes_vt(&st, &metrics, content_x)
1338 };
1339 st.handle_pointer_down(idx, (pos.x, pos.y), self.modifiers.shift);
1340 }
1342 }
1343
1344 self.pressed_ids.insert(hit.id);
1345
1346 if primary_pointer && hit.focusable {
1347 if self.sched.focused != Some(hit.id) {
1348 self.finish_compositions();
1349 }
1350 self.sched.focused = Some(hit.id);
1351 result.focused = Some(hit.id);
1352 if hit.tf_state_key.is_some() {
1353 let key = tf_key_of(f, hit.id);
1354 let st =
1355 ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
1356 let mut s = st.borrow_mut();
1357 s.apply_controlled_value(&hit.tf_value);
1358 s.reset_caret_blink();
1359 }
1360 }
1361
1362 let targets = if touch.is_some() {
1363 self.touch_targets.get(&touch.unwrap()).cloned()
1364 } else {
1365 self.mouse_targets.clone()
1366 };
1367 if let Some(targets) = targets {
1368 self.dispatch_pointer_to_targets(
1369 PointerEventKind::Down(button),
1370 pos,
1371 &targets,
1372 touch,
1373 );
1374 }
1375
1376 request_frame();
1377 } else if primary_pointer {
1378 if touch.is_none() {
1379 self.hit_path = None;
1380 self.mouse_targets = None;
1381 self.capture_id = None;
1382 self.pressed_ids.clear();
1383 } else if let Some(tid) = touch {
1384 self.touch_paths.remove(&tid);
1385 self.touch_targets.remove(&tid);
1386 self.touch_positions.remove(&tid);
1387 }
1388 self.finish_compositions();
1389 self.sched.focused = None;
1390 request_frame();
1391 }
1392
1393 result
1394 }
1395
1396 pub fn handle_pointer_release(
1398 &mut self,
1399 pos: Vec2,
1400 button: PointerButton,
1401 ) -> PointerButtonResult {
1402 self.handle_touch_release(None, pos, button)
1403 }
1404
1405 pub fn handle_touch_release(
1408 &mut self,
1409 touch: Option<u64>,
1410 pos: Vec2,
1411 button: PointerButton,
1412 ) -> PointerButtonResult {
1413 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
1414 let _dnd_guard = self.dnd_context.enter();
1415 self.mouse_pos_px = (pos.x, pos.y);
1416 if let Some(tid) = touch {
1417 self.touch_positions.insert(tid, pos);
1418 } else {
1419 self.sched.pointer_pos_px = Some((pos.x, pos.y));
1420 self.held_mouse.remove(&button);
1421 }
1422 repose_ui::scrollbar_repeat::cancel_track_repeat_for_touch(touch);
1425 let mut result = PointerButtonResult {
1426 focused: self.sched.focused,
1427 capture_id: self.capture_id,
1428 consumed: false,
1429 needs_a11y_announce: false,
1430 clicked_id: None,
1431 };
1432 if let Some(tid) = touch {
1433 return self.finish_touch_release(tid, pos, button, result);
1434 }
1435
1436 let owns_dnd_release = self.dnd_capture.as_ref().is_some_and(|capture| {
1437 capture.touch.is_none() && Some(capture.source_id) == self.capture_id
1438 });
1439 let drag_consumed = if owns_dnd_release {
1440 self.dnd_capture = None;
1441 dnd::handle_drag_action(&DragAction::Release {
1442 position: pos,
1443 modifiers: self.modifiers,
1444 })
1445 } else {
1446 false
1447 };
1448 if let Some(targets) = &self.mouse_targets {
1449 self.dispatch_pointer_to_targets(PointerEventKind::Up(button), pos, targets, None);
1450 result.consumed = true;
1451 } else if let Some(path) = &self.hit_path {
1452 self.dispatch_pointer_to_path(PointerEventKind::Up(button), pos, path, None);
1453 result.consumed = true;
1454 }
1455 if let Some(cid) = self.capture_id {
1456 self.pressed_ids.remove(&cid);
1457 self.end_textfield_drag(cid);
1458 }
1459 if drag_consumed {
1460 self.capture_id = None;
1461 self.hit_path = None;
1462 self.mouse_targets = None;
1463 request_frame();
1464 result.consumed = true;
1465 return result;
1466 }
1467
1468 let f = match &self.frame_cache {
1469 Some(f) => f.clone(),
1470 None => {
1471 self.capture_id = None;
1472 self.hit_path = None;
1473 self.mouse_targets = None;
1474 self.pressed_ids.clear();
1475 return result;
1476 }
1477 };
1478
1479 if self.long_press_touch.is_none()
1481 && let Some((lid, t0, _, _)) = self.long_press.take()
1482 && Some(lid) == self.capture_id
1483 && t0.elapsed().as_millis() >= LONG_PRESS_MS
1484 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == lid && !h.disabled)
1485 && let Some(cb) = &hit.on_long_click
1486 {
1487 cb();
1488 self.suppress_next_click = true;
1489 self.pending_click = None;
1490 result.clicked_id = Some(lid);
1491 result.needs_a11y_announce = true;
1492 result.consumed = true;
1493 }
1494
1495 let dragged = self.drag_cancelled_tap(self.capture_id, pos);
1500 if dragged {
1501 self.pending_click = None;
1502 }
1503
1504 if self.double_candidate.is_none()
1505 && !dragged
1506 && !self.suppress_next_click
1507 && let Some(cid) = self.capture_id
1508 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid && !h.disabled)
1509 && repose_ui::hit_region_contains(hit, pos)
1510 {
1511 let now = web_time::Instant::now();
1512 if hit.on_double_click.is_some() {
1515 if let Some(cb) = hit.on_click.clone() {
1516 self.pending_click = Some((cid, now, cb));
1517 }
1518 self.last_up = Some((cid, now, pos.x, pos.y));
1519 result.consumed = true;
1520 request_frame(); } else {
1522 if let Some(cb) = &hit.on_click {
1523 cb();
1524 }
1525 self.last_up = Some((cid, now, pos.x, pos.y));
1526 result.clicked_id = Some(cid);
1527 result.needs_a11y_announce = true;
1528 result.consumed = true;
1529 }
1530 }
1531 self.suppress_next_click = false;
1532
1533 if let Some(dc) = self.double_candidate.take() {
1538 self.pending_click = None;
1539 self.last_up = None;
1540 self.last_down = None;
1541 if dragged {
1542 } else if self.capture_id == Some(dc)
1545 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == dc && !h.disabled)
1546 && repose_ui::hit_region_contains(hit, pos)
1547 {
1548 if let Some(cb) = &hit.on_double_click {
1549 cb();
1550 }
1551 result.clicked_id = Some(dc);
1552 result.needs_a11y_announce = true;
1553 result.consumed = true;
1554 } else if self.capture_id == Some(dc)
1555 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == dc && !h.disabled)
1556 {
1557 if let Some(cb) = &hit.on_click {
1559 cb();
1560 }
1561 result.clicked_id = Some(dc);
1562 result.needs_a11y_announce = true;
1563 result.consumed = true;
1564 }
1565 }
1566
1567 self.capture_id = None;
1568 self.hit_path = None;
1569 self.mouse_targets = None;
1570 self.press_origin = None;
1571 request_frame();
1572 result
1573 }
1574
1575 fn finish_touch_release(
1576 &mut self,
1577 tid: u64,
1578 pos: Vec2,
1579 button: PointerButton,
1580 mut result: PointerButtonResult,
1581 ) -> PointerButtonResult {
1582 let capture_id = self
1583 .touch_presses
1584 .remove(&tid)
1585 .map(|state| state.capture_id);
1586 result.capture_id = capture_id;
1587 let targets = self.touch_targets.remove(&tid);
1588 if let Some(targets) = &targets {
1589 self.dispatch_pointer_to_targets(PointerEventKind::Up(button), pos, targets, Some(tid));
1590 result.consumed = true;
1591 } else if let Some(path) = self.touch_paths.remove(&tid) {
1592 self.dispatch_pointer_to_path(PointerEventKind::Up(button), pos, &path, Some(tid));
1593 result.consumed = true;
1594 }
1595 self.touch_paths.remove(&tid);
1596 self.touch_positions.remove(&tid);
1597 let primary = self.touch_primary == Some(tid);
1598 if let Some(cid) = capture_id {
1599 self.end_textfield_drag(cid);
1600 }
1601 let owns_dnd_release = self.dnd_capture.as_ref().is_some_and(|capture| {
1602 capture.touch == Some(tid) && Some(capture.source_id) == capture_id
1603 });
1604 let drag_consumed = if owns_dnd_release {
1605 self.dnd_capture = None;
1606 dnd::handle_drag_action(&DragAction::Release {
1607 position: pos,
1608 modifiers: self.modifiers,
1609 })
1610 } else {
1611 false
1612 };
1613 let suppressed = self.suppressed_touch_clicks.remove(&tid);
1614 if primary
1615 && !drag_consumed
1616 && !suppressed
1617 && let Some(frame) = self.frame_cache.clone()
1618 && let Some(cid) = capture_id
1619 {
1620 if self.long_press_touch == Some(tid)
1621 && let Some((lid, t0, _, _)) = self.long_press.take()
1622 && lid == cid
1623 && t0.elapsed().as_millis() >= LONG_PRESS_MS
1624 && let Some(hit) = frame
1625 .hit_regions
1626 .iter()
1627 .find(|hit| hit.id == lid && !hit.disabled)
1628 && let Some(callback) = &hit.on_long_click
1629 {
1630 callback();
1631 self.suppress_next_click = true;
1632 self.pending_click = None;
1633 result.clicked_id = Some(lid);
1634 result.needs_a11y_announce = true;
1635 result.consumed = true;
1636 }
1637
1638 let dragged = self.drag_cancelled_tap(capture_id, pos);
1640 if dragged {
1641 self.pending_click = None;
1642 }
1643
1644 if self.double_candidate.is_none()
1645 && !dragged
1646 && !self.suppress_next_click
1647 && let Some(hit) = frame
1648 .hit_regions
1649 .iter()
1650 .find(|hit| hit.id == cid && !hit.disabled)
1651 && repose_ui::hit_region_contains(hit, pos)
1652 {
1653 let now = web_time::Instant::now();
1654 if hit.on_double_click.is_some() {
1655 if let Some(callback) = hit.on_click.clone() {
1656 self.pending_click = Some((cid, now, callback));
1657 }
1658 self.last_up = Some((cid, now, pos.x, pos.y));
1659 result.consumed = true;
1660 request_frame();
1661 } else {
1662 if let Some(callback) = &hit.on_click {
1663 callback();
1664 }
1665 self.last_up = Some((cid, now, pos.x, pos.y));
1666 result.clicked_id = Some(cid);
1667 result.needs_a11y_announce = true;
1668 result.consumed = true;
1669 }
1670 }
1671 self.suppress_next_click = false;
1672
1673 if let Some(double_id) = self.double_candidate.take() {
1674 self.pending_click = None;
1675 self.last_up = None;
1676 self.last_down = None;
1677 if let Some(hit) = frame
1678 .hit_regions
1679 .iter()
1680 .find(|hit| hit.id == double_id && !hit.disabled)
1681 && !dragged
1682 {
1683 if repose_ui::hit_region_contains(hit, pos) {
1684 if let Some(callback) = &hit.on_double_click {
1685 callback();
1686 }
1687 } else if let Some(callback) = &hit.on_click {
1688 callback();
1689 }
1690 result.clicked_id = Some(double_id);
1691 result.needs_a11y_announce = true;
1692 result.consumed = true;
1693 }
1694 }
1695 }
1696 if self.long_press_touch == Some(tid) {
1697 self.long_press = None;
1698 self.long_press_touch = None;
1699 }
1700 if primary {
1701 self.press_origin = None;
1702 }
1703 self.rebuild_pressed_ids();
1704 if primary {
1705 self.touch_primary = None;
1706 }
1707 request_frame();
1708 result
1709 }
1710
1711 fn drag_cancelled_tap(&self, capture_id: Option<u64>, pos: Vec2) -> bool {
1715 capture_id.is_some_and(|cid| {
1716 self.press_origin.is_some_and(|(oid, x0, y0)| {
1717 oid == cid && moved_past_slop(x0, y0, pos, TAP_SLOP_DP, self.scale)
1718 })
1719 })
1720 }
1721
1722 fn cancel_keyboard_press(&mut self) {
1723 let active = self.key_pressed_active.take();
1724 self.key_pressed_key = None;
1725 self.key_long_press = None;
1726 let Some(active) = active else {
1727 return;
1728 };
1729 self.pressed_ids.remove(&active);
1730 let hit = self
1731 .frame_cache
1732 .as_ref()
1733 .and_then(|frame| frame.hit_regions.iter().find(|hit| hit.id == active))
1734 .cloned();
1735 if let Some(hit) = hit
1736 && let Some(source) = &hit.interaction_source
1737 {
1738 let press_id = source.collect_last_press_id().unwrap_or(0);
1739 source.to_mutable().emit(Interaction::Cancel(press_id));
1740 }
1741 }
1742
1743 fn rebuild_pressed_ids(&mut self) {
1744 self.pressed_ids = self
1745 .touch_presses
1746 .values()
1747 .flat_map(|state| state.pressed_ids.iter().copied())
1748 .collect();
1749 if let Some(active) = self.key_pressed_active {
1750 self.pressed_ids.insert(active);
1751 }
1752 }
1753
1754 pub fn suppress_touch_click(&mut self, tid: u64) {
1755 self.suppressed_touch_clicks.insert(tid);
1756 if self.touch_primary == Some(tid) {
1757 self.suppress_next_click = true;
1758 self.long_press = None;
1759 self.pending_click = None;
1760 self.double_candidate = None;
1761 }
1762 }
1763
1764 pub fn touch_owns_drag(&self, tid: u64) -> bool {
1772 if dnd::is_dragging() {
1773 return true;
1774 }
1775 let Some(capture_id) = self.touch_presses.get(&tid).map(|state| state.capture_id) else {
1776 return false;
1777 };
1778 self.frame_cache.as_ref().is_some_and(|frame| {
1779 frame
1780 .hit_regions
1781 .iter()
1782 .find(|hit| hit.id == capture_id)
1783 .is_some_and(|hit| hit.on_pointer_move.is_some())
1784 })
1785 }
1786
1787 pub fn handle_touch_cancel(&mut self, touch: Option<u64>) {
1793 if let Some(tid) = touch {
1794 let pos = self.touch_positions.get(&tid).copied().unwrap_or(Vec2 {
1795 x: self.mouse_pos_px.0,
1796 y: self.mouse_pos_px.1,
1797 });
1798 self.handle_touch_cancel_at(tid, pos);
1799 } else {
1800 self.handle_pointer_cancel();
1801 }
1802 }
1803
1804 pub fn handle_touch_cancel_at(&mut self, tid: u64, pos: Vec2) {
1805 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
1806 let _dnd_guard = self.dnd_context.enter();
1807 repose_ui::scrollbar_repeat::cancel_track_repeat_for_touch(Some(tid));
1808 let was_primary = self.touch_primary == Some(tid);
1809 let capture_id = self
1810 .touch_presses
1811 .remove(&tid)
1812 .map(|state| state.capture_id);
1813 let targets = self.touch_targets.remove(&tid);
1814 if let Some(targets) = &targets {
1815 self.dispatch_pointer_to_targets(PointerEventKind::Cancel, pos, targets, Some(tid));
1816 } else if let Some(path) = self.touch_paths.remove(&tid) {
1817 self.dispatch_pointer_to_path(PointerEventKind::Cancel, pos, &path, Some(tid));
1818 }
1819 self.touch_paths.remove(&tid);
1820 self.touch_positions.remove(&tid);
1821 if let Some(cid) = capture_id {
1822 self.end_textfield_drag(cid);
1823 }
1824 self.suppressed_touch_clicks.remove(&tid);
1825 if was_primary {
1826 self.touch_primary = None;
1827 self.double_candidate = None;
1828 self.pending_click = None;
1829 self.last_up = None;
1830 self.last_down = None;
1831 self.suppress_next_click = false;
1832 self.scroll_capture_id = None;
1833 }
1834 if self.long_press_touch == Some(tid) {
1835 self.long_press = None;
1836 self.long_press_touch = None;
1837 }
1838 self.rebuild_pressed_ids();
1839 if let Some(source_id) = capture_id {
1840 self.cancel_dnd_capture(Some(tid), source_id);
1841 }
1842 request_frame();
1843 }
1844
1845 pub fn handle_pointer_cancel(&mut self) {
1846 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
1847 let _dnd_guard = self.dnd_context.enter();
1848 repose_ui::scrollbar_repeat::cancel_all_track_repeats();
1849 self.sched.pointer_pos_px = None;
1850 self.held_mouse.clear();
1851 self.sched.mouse_primary = false;
1852 self.sched.mouse_secondary = false;
1853 self.sched.mouse_middle = false;
1854 self.scroll_capture_id = None;
1855 self.last_scroll_at = None;
1856 if let Some(cid) = self.capture_id {
1857 self.end_textfield_drag(cid);
1858 }
1859 let touch_targets = std::mem::take(&mut self.touch_targets);
1860 let touch_paths = std::mem::take(&mut self.touch_paths);
1861 let mut touch_ids: Vec<u64> = touch_targets
1862 .keys()
1863 .chain(touch_paths.keys())
1864 .copied()
1865 .collect();
1866 touch_ids.sort_unstable();
1867 touch_ids.dedup();
1868 for tid in touch_ids {
1869 let pos = self.touch_positions.remove(&tid).unwrap_or(Vec2 {
1870 x: self.mouse_pos_px.0,
1871 y: self.mouse_pos_px.1,
1872 });
1873 if let Some(targets) = touch_targets.get(&tid) {
1874 self.dispatch_pointer_to_targets(PointerEventKind::Cancel, pos, targets, Some(tid));
1875 } else if let Some(path) = touch_paths.get(&tid) {
1876 self.dispatch_pointer_to_path(PointerEventKind::Cancel, pos, path, Some(tid));
1877 }
1878 }
1879 let touch_presses = std::mem::take(&mut self.touch_presses);
1880 for (tid, state) in &touch_presses {
1881 let source_id = state.capture_id;
1882 self.cancel_dnd_capture(Some(*tid), source_id);
1883 self.end_textfield_drag(source_id);
1884 }
1885 self.touch_primary = None;
1886 self.suppressed_touch_clicks.clear();
1887 self.cancel_keyboard_press();
1888 self.long_press = None;
1889 self.long_press_touch = None;
1890 self.last_up = None;
1891 self.last_down = None;
1892 self.press_origin = None;
1893 self.double_candidate = None;
1894 self.suppress_next_click = false;
1895 if self.dnd_capture.is_some() {
1896 self.cancel_active_dnd();
1897 }
1898 let pos = Vec2 {
1899 x: self.mouse_pos_px.0,
1900 y: self.mouse_pos_px.1,
1901 };
1902 dispatch_hover_change_bubbled(
1903 self.frame_cache.as_deref(),
1904 &self.hover_leave,
1905 &mut self.hover_id,
1906 &mut self.hover_ancestors,
1907 None,
1908 pos,
1909 self.modifiers,
1910 );
1911 if let Some(targets) = &self.mouse_targets {
1912 self.dispatch_pointer_to_targets(PointerEventKind::Cancel, pos, targets, None);
1913 } else if let Some(path) = &self.hit_path {
1914 self.dispatch_pointer_to_path(PointerEventKind::Cancel, pos, path, None);
1915 }
1916 self.reset_pointer_state();
1917 request_frame();
1918 }
1919
1920 pub fn clear_hover(&mut self) {
1922 if self.hover_id.is_none() && self.hover_ancestors.is_empty() {
1923 return;
1924 }
1925 let pos = Vec2 {
1926 x: self.mouse_pos_px.0,
1927 y: self.mouse_pos_px.1,
1928 };
1929 dispatch_hover_change_bubbled(
1930 self.frame_cache.as_deref(),
1931 &self.hover_leave,
1932 &mut self.hover_id,
1933 &mut self.hover_ancestors,
1934 None,
1935 pos,
1936 self.modifiers,
1937 );
1938 }
1939
1940 pub fn reconcile_hover_from_mouse_pos(&mut self, new_frame: &Frame) {
1942 let pos = Vec2 {
1943 x: self.mouse_pos_px.0,
1944 y: self.mouse_pos_px.1,
1945 };
1946
1947 if let Some(prev_id) = self.hover_id
1950 && !new_frame.hit_regions.iter().any(|h| h.id == prev_id)
1951 {
1952 dispatch_hover_change_bubbled(
1953 Some(new_frame),
1954 &self.hover_leave,
1955 &mut self.hover_id,
1956 &mut self.hover_ancestors,
1957 None,
1958 pos,
1959 self.modifiers,
1960 );
1961 }
1962
1963 if !self.pointer_inside {
1964 if self.hover_id.is_some() || !self.hover_ancestors.is_empty() {
1965 dispatch_hover_change_bubbled(
1966 Some(new_frame),
1967 &self.hover_leave,
1968 &mut self.hover_id,
1969 &mut self.hover_ancestors,
1970 None,
1971 pos,
1972 self.modifiers,
1973 );
1974 request_frame();
1975 }
1976 return;
1977 }
1978
1979 let new_hover = repose_ui::hit_test_enabled_frame(new_frame, pos).map(|hit| hit.id);
1980
1981 self.cursor = if dnd::is_dragging() {
1982 Some(CursorIcon::Grabbing)
1983 } else {
1984 new_hover
1985 .and_then(|id| new_frame.hit_regions.iter().find(|h| h.id == id))
1986 .and_then(|h| h.cursor.clone())
1987 .or(Some(CursorIcon::Default))
1988 };
1989
1990 let new_chain = hover_chain_for(Some(new_frame), new_hover);
1991 let old_chain = hover_chain_for(Some(new_frame), self.hover_id);
1992 if new_chain == old_chain {
1993 return;
1994 }
1995
1996 dispatch_hover_change_bubbled(
1997 Some(new_frame),
1998 &self.hover_leave,
1999 &mut self.hover_id,
2000 &mut self.hover_ancestors,
2001 new_hover,
2002 pos,
2003 self.modifiers,
2004 );
2005 request_frame();
2006 }
2007
2008 fn reset_pointer_state(&mut self) {
2009 self.capture_id = None;
2010 self.hit_path = None;
2011 self.mouse_targets = None;
2012 self.touch_paths.clear();
2013 self.touch_targets.clear();
2014 self.touch_positions.clear();
2015 self.touch_presses.clear();
2016 self.dnd_capture = None;
2017 self.touch_primary = None;
2018 self.suppressed_touch_clicks.clear();
2019 self.pressed_ids.clear();
2020 self.pending_click = None;
2021 self.last_down = None;
2022 self.press_origin = None;
2023 self.double_candidate = None;
2024 self.key_pressed_active = None;
2025 self.key_pressed_key = None;
2026 self.key_long_press = None;
2027 self.last_up = None;
2028 self.last_down = None;
2029 self.double_candidate = None;
2030 self.suppress_next_click = false;
2031 }
2032
2033 fn flush_pending_click(&mut self) {
2034 let Some((id, t0, cb)) = self.pending_click.take() else {
2035 return;
2036 };
2037 if t0.elapsed().as_millis() >= DOUBLE_CLICK_MS {
2038 cb();
2039 request_frame();
2040 } else {
2041 self.pending_click = Some((id, t0, cb));
2042 }
2043 }
2044
2045 fn poll_long_press(&mut self) {
2046 let Some((lid, t0, _, _)) = self.long_press else {
2047 return;
2048 };
2049 if t0.elapsed().as_millis() < LONG_PRESS_MS {
2050 return;
2051 }
2052 let Some(f) = self.frame_cache.clone() else {
2053 return;
2054 };
2055 let captured = self
2058 .long_press_touch
2059 .and_then(|tid| self.touch_presses.get(&tid).map(|state| state.capture_id))
2060 .or(self.capture_id);
2061 if captured != Some(lid) {
2062 self.long_press = None;
2063 self.long_press_touch = None;
2064 return;
2065 }
2066 let (mx, my) = self.mouse_pos_px;
2067 let in_bounds = f
2068 .hit_regions
2069 .iter()
2070 .find(|h| h.id == lid)
2071 .is_some_and(|h| repose_ui::hit_region_contains(h, Vec2 { x: mx, y: my }));
2072 if !in_bounds {
2073 self.long_press = None;
2074 return;
2075 }
2076 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == lid && !h.disabled)
2077 && let Some(cb) = &hit.on_long_click
2078 {
2079 self.long_press = None;
2080 self.suppress_next_click = true;
2081 self.pending_click = None;
2082 self.last_up = None;
2083 cb();
2084 request_frame();
2085 } else {
2086 self.long_press = None;
2087 }
2088 }
2089
2090 fn poll_key_long_press(&mut self) {
2092 let Some((kid, t0, fired)) = self.key_long_press else {
2093 return;
2094 };
2095 if t0.elapsed().as_millis() < LONG_PRESS_MS {
2096 return;
2097 }
2098 let Some(f) = self.frame_cache.clone() else {
2099 return;
2100 };
2101 if !fired {
2102 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == kid && !h.disabled)
2103 && let Some(cb) = &hit.on_long_click
2104 {
2105 cb();
2106 }
2107 self.key_long_press = Some((kid, t0, true));
2108 request_frame();
2109 }
2110 }
2111
2112 pub fn handle_scroll(&mut self, delta: Vec2) -> bool {
2114 let Some(f) = &self.frame_cache else {
2115 return false;
2116 };
2117
2118 let now = web_time::Instant::now();
2119 if let Some(last) = self.last_scroll_at
2120 && now.duration_since(last).as_millis() > 250
2121 {
2122 self.scroll_capture_id = None;
2123 }
2124 self.last_scroll_at = Some(now);
2125
2126 let pos = Vec2 {
2127 x: self.mouse_pos_px.0,
2128 y: self.mouse_pos_px.1,
2129 };
2130 let (consumed, cap) = dispatch_scroll(
2131 f,
2132 pos,
2133 delta,
2134 self.scroll_capture_id,
2135 repose_core::input::ScrollSource::Wheel,
2136 );
2137 self.scroll_capture_id = cap;
2138 if consumed {
2139 request_frame();
2140 }
2141 consumed
2142 }
2143
2144 fn resolve_shortcut_action(
2145 &self,
2146 chord: &repose_core::shortcuts::KeyChord,
2147 ) -> Option<repose_core::shortcuts::Action> {
2148 self.shortcuts.resolve_action(chord)
2149 }
2150
2151 fn handle_shortcut_action(&self, action: repose_core::shortcuts::Action) -> bool {
2152 self.shortcuts.handle(action)
2153 }
2154
2155 pub fn handle_key(&mut self, event: &KeyEvent) -> bool {
2157 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
2158 let shortcut_state = self.shortcuts.clone();
2159 repose_core::shortcuts::with_runtime_state(&shortcut_state, || self.handle_key_inner(event))
2160 }
2161
2162 fn handle_key_inner(&mut self, event: &KeyEvent) -> bool {
2163 let _dnd_guard = self.dnd_context.enter();
2164 if event.event_type == KeyEventType::Down {
2165 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
2166 }
2167
2168 let Some(frame) = self.frame_cache.clone() else {
2169 if event.event_type == KeyEventType::Down
2170 && !event.is_repeat
2171 && let Some(action) = self.resolve_shortcut_action(
2172 &repose_core::shortcuts::KeyChord::new(event.key.clone(), event.modifiers),
2173 )
2174 {
2175 return self.dispatch_action(action);
2176 }
2177 return false;
2178 };
2179 let f = &frame;
2180
2181 if event.event_type == KeyEventType::Down && !event.is_repeat && event.key == Key::Escape {
2182 self.cancel_keyboard_press();
2183 if self.cancel_active_dnd() {
2184 request_frame();
2185 return true;
2186 }
2187 }
2188
2189 if event.event_type == KeyEventType::Down && !event.is_repeat {
2190 let is_back = event.key == Key::Escape
2191 || self
2192 .resolve_shortcut_action(&repose_core::shortcuts::KeyChord::new(
2193 event.key.clone(),
2194 event.modifiers,
2195 ))
2196 .is_some_and(|action| matches!(action, repose_core::shortcuts::Action::Back));
2197 if is_back && self.overlay.handle_back() {
2198 request_frame();
2199 return true;
2200 }
2201 }
2202
2203 if self.dispatch_key_event(f, event) {
2204 request_frame();
2205 return true;
2206 }
2207
2208 if event.event_type == KeyEventType::Down
2210 && !event.is_repeat
2211 && let Some(action) = self.resolve_shortcut_action(
2212 &repose_core::shortcuts::KeyChord::new(event.key.clone(), event.modifiers),
2213 )
2214 {
2215 if self.dispatch_action(action.clone()) {
2217 return true;
2218 }
2219 }
2220
2221 if let Some(fid) = self.sched.focused {
2223 let is_tf = f
2224 .semantics_nodes
2225 .iter()
2226 .any(|n| n.id == fid && n.role == repose_core::semantics::Role::TextField);
2227 if !is_tf {
2228 if event.event_type == KeyEventType::Down && !event.is_repeat {
2229 if event.key == Key::Space || event.key == Key::Enter {
2230 let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) else {
2231 return false;
2232 };
2233 if hit.disabled {
2234 return false;
2235 }
2236 if hit.on_click.is_none()
2237 && hit.on_long_click.is_none()
2238 && hit.on_double_click.is_none()
2239 {
2240 return false; }
2242 self.cancel_keyboard_press();
2243 self.pressed_ids.insert(fid);
2244 self.key_pressed_active = Some(fid);
2245 self.key_pressed_key = Some(event.key.clone());
2246 self.key_long_press = if hit.on_long_click.is_some() {
2247 Some((fid, web_time::Instant::now(), false))
2248 } else {
2249 None
2250 };
2251
2252 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
2253 && let Some(src) = &hit.interaction_source
2254 {
2255 let local = Vec2 {
2256 x: hit.rect.w * 0.5,
2257 y: hit.rect.h * 0.5,
2258 };
2259 src.to_mutable().emit(Interaction::new_press(local));
2260 }
2261
2262 request_frame();
2263 return true;
2264 }
2265 } else if event.event_type == KeyEventType::Up
2266 && let Some(active_id) = self.key_pressed_active
2267 && self
2268 .key_pressed_key
2269 .as_ref()
2270 .is_some_and(|key| key == &event.key)
2271 {
2272 self.pressed_ids.remove(&active_id);
2273 self.key_pressed_active = None;
2274 self.key_pressed_key = None;
2275
2276 let long_fired = self
2277 .key_long_press
2278 .take()
2279 .map(|(_, _, fired)| fired)
2280 .unwrap_or(false);
2281
2282 if let Some(hit) = f
2283 .hit_regions
2284 .iter()
2285 .find(|h| h.id == active_id && !h.disabled)
2286 {
2287 if let Some(src) = &hit.interaction_source {
2288 let pid = src.collect_last_press_id().unwrap_or(0);
2289 src.to_mutable().emit(Interaction::Release(pid));
2290 }
2291 if !long_fired && let Some(cb) = &hit.on_click {
2292 cb();
2293 }
2294 }
2295 request_frame();
2296 return true;
2297 }
2298 }
2299 }
2300
2301 if event.event_type == KeyEventType::Down
2303 && !event.is_repeat
2304 && event.key == Key::Enter
2305 && let Some(fid) = self.sched.focused
2306 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
2307 {
2308 let is_multiline = hit.tf_multiline;
2309 let should_submit = if is_multiline {
2310 self.modifiers.ctrl || self.modifiers.meta
2311 } else {
2312 true
2313 };
2314 if should_submit {
2315 let key = tf_key_of(f, fid);
2316 if let Some(state_rc) = self.textfield_states.get(&key) {
2317 let text = state_rc.borrow().text.clone();
2318 let action = match hit.ime_action {
2319 repose_core::text::ImeAction::Unspecified => {
2320 repose_core::text::ImeAction::Done
2321 }
2322 action => action,
2323 };
2324 repose_ui::textfield::dispatch_textfield_keyboard_action(hit, action, &|| {
2325 if let Some(on_submit) = &hit.on_text_submit {
2326 on_submit(text.clone());
2327 }
2328 });
2329 request_frame();
2330 return true;
2331 }
2332 } else {
2333 let key = tf_key_of(f, fid);
2335 if !is_tf_editable(f, fid) {
2336 return true;
2337 }
2338 if let Some(state_rc) = self.textfield_states.get(&key) {
2339 let mut edited = state_rc.borrow().clone();
2340 repose_ui::textfield::insert_text_with_input_transformation(
2341 hit,
2342 &mut edited,
2343 "\n",
2344 false,
2345 );
2346 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2347 let new_text = edited.text.clone();
2348 *state_rc.borrow_mut() = edited;
2349 notify_text_change(f, fid, new_text);
2350 request_frame();
2351 return true;
2352 }
2353 }
2354 }
2355
2356 if event.event_type == KeyEventType::Down
2358 && let Some(fid) = self.sched.focused
2359 && self.handle_text_navigation_key(f, fid, &event.key)
2360 {
2361 return true;
2362 }
2363
2364 if event.event_type == KeyEventType::Down {
2365 if !self.ime_preedit
2367 && !self.modifiers.ctrl
2368 && !self.modifiers.alt
2369 && !self.modifiers.meta
2370 && let Key::Character(c) = event.key
2371 && !c.is_control()
2372 && c != '\n'
2373 && c != '\r'
2374 && let Some(fid) = self.sched.focused
2375 {
2376 let key = tf_key_of(f, fid);
2377 if !is_tf_editable(f, fid) {
2378 return true;
2379 }
2380 if let Some(state_rc) = self.textfield_states.get(&key) {
2381 let new_text = if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
2382 let mut edited = state_rc.borrow().clone();
2383 repose_ui::textfield::insert_text_with_input_transformation(
2384 hit,
2385 &mut edited,
2386 &c.to_string(),
2387 false,
2388 );
2389 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2390 let text = edited.text.clone();
2391 *state_rc.borrow_mut() = edited;
2392 text
2393 } else {
2394 return false;
2395 };
2396 notify_text_change(f, fid, new_text);
2397 request_frame();
2398 return true;
2399 }
2400 }
2401 }
2402
2403 false
2404 }
2405
2406 fn display_text_for_state(
2407 state: &TextFieldState,
2408 ) -> (String, usize, Option<Box<dyn repose_core::OffsetMapping>>) {
2409 let caret = state.caret_index();
2410 let Some(transformation) = state.visual_transformation.clone() else {
2411 return (state.text.clone(), caret, None);
2412 };
2413 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
2414 let transformed = transformation.filter(&annotated);
2415 let display_caret = transformed.offset_mapping.original_to_transformed(caret);
2416 (
2417 transformed.text.text,
2418 display_caret,
2419 Some(transformed.offset_mapping),
2420 )
2421 }
2422
2423 fn handle_text_navigation_key(&mut self, frame: &Frame, id: u64, key: &Key) -> bool {
2424 let Some(hit) = frame.hit_regions.iter().find(|hit| hit.id == id) else {
2425 return false;
2426 };
2427 let state_key = tf_key_of(frame, id);
2428 let Some(state) = self.textfield_states.get(&state_key) else {
2429 return false;
2430 };
2431 let editable = is_tf_editable(frame, id);
2432 let metrics = repose_ui::textfield::textfield_metrics(hit);
2433 let shift = self.modifiers.shift;
2434 let word = self.modifiers.command;
2435 let mut edited = state.borrow().clone();
2436 let (display, display_caret, mapping) = Self::display_text_for_state(&edited);
2437 let mut changed = false;
2438 let consumed = match key {
2439 Key::Backspace => {
2440 changed = editable
2441 && repose_ui::textfield::delete_backward_with_input_transformation(
2442 hit,
2443 &mut edited,
2444 );
2445 true
2446 }
2447 Key::Delete => {
2448 changed = editable
2449 && repose_ui::textfield::delete_forward_with_input_transformation(
2450 hit,
2451 &mut edited,
2452 );
2453 true
2454 }
2455 Key::ArrowLeft => {
2456 edited.move_cursor(-1, shift, word);
2457 edited.preferred_x_px = None;
2458 true
2459 }
2460 Key::ArrowRight => {
2461 edited.move_cursor(1, shift, word);
2462 edited.preferred_x_px = None;
2463 true
2464 }
2465 Key::ArrowUp if hit.tf_multiline => {
2466 let (next_display, preferred) =
2467 repose_ui::textfield::move_caret_vertical_with_metrics(
2468 &display,
2469 edited.inner_width.max(1.0),
2470 display_caret,
2471 -1,
2472 edited.preferred_x_px,
2473 &metrics,
2474 );
2475 let next = mapping.as_ref().map_or(next_display, |mapping| {
2476 mapping.transformed_to_original(next_display)
2477 });
2478 set_caret(&mut edited, next, shift);
2479 edited.preferred_x_px = Some(preferred);
2480 true
2481 }
2482 Key::ArrowDown if hit.tf_multiline => {
2483 let (next_display, preferred) =
2484 repose_ui::textfield::move_caret_vertical_with_metrics(
2485 &display,
2486 edited.inner_width.max(1.0),
2487 display_caret,
2488 1,
2489 edited.preferred_x_px,
2490 &metrics,
2491 );
2492 let next = mapping.as_ref().map_or(next_display, |mapping| {
2493 mapping.transformed_to_original(next_display)
2494 });
2495 set_caret(&mut edited, next, shift);
2496 edited.preferred_x_px = Some(preferred);
2497 true
2498 }
2499 Key::Home => {
2500 let next_display = if hit.tf_multiline {
2501 repose_ui::textfield::line_home_end_with_metrics(
2502 &display,
2503 edited.inner_width.max(1.0),
2504 display_caret,
2505 false,
2506 &metrics,
2507 )
2508 } else {
2509 0
2510 };
2511 let next = mapping.as_ref().map_or(next_display, |mapping| {
2512 mapping.transformed_to_original(next_display)
2513 });
2514 set_caret(&mut edited, next, shift);
2515 edited.preferred_x_px = None;
2516 true
2517 }
2518 Key::End => {
2519 let next_display = if hit.tf_multiline {
2520 repose_ui::textfield::line_home_end_with_metrics(
2521 &display,
2522 edited.inner_width.max(1.0),
2523 display_caret,
2524 true,
2525 &metrics,
2526 )
2527 } else {
2528 display.len()
2529 };
2530 let next = mapping.as_ref().map_or(next_display, |mapping| {
2531 mapping.transformed_to_original(next_display)
2532 });
2533 set_caret(&mut edited, next, shift);
2534 edited.preferred_x_px = None;
2535 true
2536 }
2537 _ => false,
2538 };
2539 if !consumed {
2540 return false;
2541 }
2542 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2543 let text = changed.then(|| edited.text.clone());
2544 *state.borrow_mut() = edited;
2545 if let Some(text) = text {
2546 notify_text_change(frame, id, text);
2547 }
2548 request_frame();
2549 true
2550 }
2551
2552 fn dispatch_key_event(&self, f: &Frame, event: &KeyEvent) -> bool {
2557 let chain = key_ancestor_chain(f, self.sched.focused);
2558 let hit_by_id: HashMap<u64, &HitRegion> = f.hit_regions.iter().map(|h| (h.id, h)).collect();
2559
2560 for &id in &chain {
2561 let Some(hit) = hit_by_id.get(&id) else {
2562 continue;
2563 };
2564 if hit.disabled {
2565 continue;
2566 }
2567 if let Some(cb) = &hit.on_preview_key_event
2568 && cb(event.clone())
2569 {
2570 return true;
2571 }
2572 }
2573
2574 for &id in chain.iter().rev() {
2575 let Some(hit) = hit_by_id.get(&id) else {
2576 continue;
2577 };
2578 if hit.disabled {
2579 continue;
2580 }
2581 if let Some(cb) = &hit.on_key_event
2582 && cb(event.clone())
2583 {
2584 return true;
2585 }
2586 }
2587
2588 false
2589 }
2590
2591 pub fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
2595 if let Some(f) = &self.frame_cache
2597 && let Some(fid) = self.sched.focused
2598 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
2599 && let Some(cb) = &hit.on_action
2600 && cb(action.clone())
2601 {
2602 request_frame();
2603 return true;
2604 }
2605
2606 if self.apply_text_editing_action(&action) {
2608 return true;
2609 }
2610
2611 if self.handle_shortcut_action(action.clone()) {
2613 request_frame();
2614 return true;
2615 }
2616
2617 if let Some(f) = self.frame_cache.clone()
2619 && let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.sched, &f)
2620 {
2621 self.cancel_keyboard_press();
2623 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == new_id)
2625 && let Some(key) = hit.tf_state_key
2626 {
2627 let st = ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
2628 {
2629 let mut s = st.borrow_mut();
2630 s.apply_controlled_value(&hit.tf_value);
2631 s.reset_caret_blink();
2632 }
2633 }
2634 request_frame();
2635 return true;
2636 }
2637
2638 false
2639 }
2640
2641 fn apply_text_editing_action(&mut self, action: &repose_core::shortcuts::Action) -> bool {
2644 use repose_core::shortcuts::Action;
2645 let Some(fid) = self.sched.focused else {
2646 return false;
2647 };
2648 let Some(f) = self.frame_cache.clone() else {
2649 return false;
2650 };
2651 if !is_tf_hit(&f, fid) {
2652 return false;
2653 }
2654 let key = tf_key_of(&f, fid);
2655 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
2656 return false;
2657 };
2658 let hit = f.hit_regions.iter().find(|hit| hit.id == fid);
2659
2660 match action {
2661 Action::Undo => {
2662 if !is_tf_editable(&f, fid) {
2663 return true;
2664 }
2665 let Some(hit) = hit else {
2666 return false;
2667 };
2668 let mut edited = state_rc.borrow().clone();
2669 if !repose_ui::textfield::undo_with_input_transformation(hit, &mut edited) {
2670 return false;
2671 }
2672 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2673 let new_text = edited.text.clone();
2674 *state_rc.borrow_mut() = edited;
2675 notify_text_change(&f, fid, new_text);
2676 request_frame();
2677 true
2678 }
2679 Action::Redo => {
2680 if !is_tf_editable(&f, fid) {
2681 return true;
2682 }
2683 let Some(hit) = hit else {
2684 return false;
2685 };
2686 let mut edited = state_rc.borrow().clone();
2687 if !repose_ui::textfield::redo_with_input_transformation(hit, &mut edited) {
2688 return false;
2689 }
2690 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2691 let new_text = edited.text.clone();
2692 *state_rc.borrow_mut() = edited;
2693 notify_text_change(&f, fid, new_text);
2694 request_frame();
2695 true
2696 }
2697 Action::SelectAll => {
2698 let mut st = state_rc.borrow_mut();
2699 let len = st.text.len();
2700 st.selection = 0..len;
2701 if let Some(hit) = hit {
2702 tf_ensure_caret_visible_for_hit(hit, &mut st);
2703 }
2704 request_frame();
2705 true
2706 }
2707 Action::Copy => {
2708 if hit.is_some_and(tf_is_sensitive) {
2709 return true;
2710 }
2711 let st = state_rc.borrow();
2712 let (a, b) = (
2713 st.selection.start.min(st.selection.end),
2714 st.selection.start.max(st.selection.end),
2715 );
2716 if a == b {
2717 return false;
2718 }
2719 let slice = st.text.get(a..b).unwrap_or("").to_string();
2720 drop(st);
2721 if !slice.is_empty() {
2722 repose_core::clipboard::copy_to_clipboard(&slice);
2723 }
2724 true
2725 }
2726 Action::Cut => {
2727 if hit.is_some_and(tf_is_sensitive) {
2728 return true;
2729 }
2730 if !is_tf_editable(&f, fid) {
2731 return true;
2732 }
2733 let Some(hit) = hit else {
2734 return false;
2735 };
2736 let mut edited = state_rc.borrow().clone();
2737 let Some(slice) =
2738 repose_ui::textfield::cut_with_input_transformation(hit, &mut edited)
2739 else {
2740 return false;
2741 };
2742 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2743 let new_text = edited.text.clone();
2744 *state_rc.borrow_mut() = edited;
2745 repose_core::clipboard::copy_to_clipboard(&slice);
2746 notify_text_change(&f, fid, new_text);
2747 request_frame();
2748 true
2749 }
2750 Action::Paste => {
2751 if let Some(txt) = repose_core::clipboard::paste_text() {
2752 self.paste_into_focused(&txt);
2753 return true;
2754 }
2755 false
2756 }
2757 _ => false,
2758 }
2759 }
2760
2761 fn cancel_ime_compositions(&mut self) {
2762 let mut changed = None;
2763 for (key, state) in &self.textfield_states {
2764 let mut edited = state.borrow().clone();
2765 if edited.composition.is_none() {
2766 continue;
2767 }
2768 edited.cancel_composition();
2769 *state.borrow_mut() = edited;
2770 changed = Some((*key, self.textfield_states[key].borrow().text.clone()));
2771 }
2772 self.ime_preedit = false;
2773 if let (Some((key, text)), Some(frame)) = (changed, &self.frame_cache)
2774 && let Some(id) = frame
2775 .hit_regions
2776 .iter()
2777 .find(|hit| hit.tf_state_key.unwrap_or(hit.id) == key)
2778 .map(|hit| hit.id)
2779 {
2780 notify_text_change(frame, id, text);
2781 }
2782 request_frame();
2783 }
2784
2785 pub fn handle_ime(&mut self, event: &ImeEvent) {
2787 if matches!(event, ImeEvent::Cancel) {
2788 self.cancel_ime_compositions();
2789 }
2790 let Some(fid) = self.sched.focused else {
2791 return;
2792 };
2793 let Some(frame) = self.frame_cache.clone() else {
2794 return;
2795 };
2796 if !(is_tf_editable(&frame, fid) && is_textfield_in_frame(&frame, fid)) {
2797 if let Some(hit) = frame.hit_regions.iter().find(|hit| hit.id == fid)
2798 && !hit.disabled
2799 && let Some(cb) = &hit.on_ime
2800 && cb(event.clone())
2801 {
2802 request_frame();
2803 }
2804 return;
2805 }
2806 if matches!(event, ImeEvent::Cancel) {
2807 return;
2808 }
2809 let key = tf_key_of(&frame, fid);
2810 let Some(state) = self.textfield_states.get(&key).cloned() else {
2811 return;
2812 };
2813 let hit = frame.hit_regions.iter().find(|hit| hit.id == fid);
2814 let mut edited = state.borrow().clone();
2815 let changed = match event {
2816 ImeEvent::Start => {
2817 self.ime_preedit = false;
2818 None
2819 }
2820 ImeEvent::Update { text, cursor } => {
2821 edited.set_composition(text.clone(), *cursor);
2822 if let Some(hit) = hit {
2823 repose_ui::textfield::apply_textfield_input_transformation(hit, &mut edited);
2824 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2825 }
2826 self.ime_preedit = !text.is_empty();
2827 Some(edited.text.clone())
2828 }
2829 ImeEvent::Commit(text) => {
2830 if let Some(hit) = hit {
2831 repose_ui::textfield::commit_composition_with_input_transformation(
2832 hit,
2833 &mut edited,
2834 text.clone(),
2835 );
2836 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2837 } else {
2838 edited.commit_composition(text.clone());
2839 }
2840 self.ime_preedit = false;
2841 Some(edited.text.clone())
2842 }
2843 ImeEvent::Cancel => unreachable!(),
2844 };
2845 *state.borrow_mut() = edited;
2846 if let Some(text) = changed {
2847 notify_text_change(&frame, fid, text);
2848 }
2849 request_frame();
2850 }
2851
2852 pub fn finish_compositions(&mut self) {
2856 for state_rc in self.textfield_states.values() {
2857 let mut st = state_rc.borrow_mut();
2858 if st.composition.is_some() {
2859 st.finish_composition();
2860 }
2861 }
2862 self.ime_preedit = false;
2863 }
2864
2865 pub fn handle_focus_lost(&mut self) {
2870 self.pointer_inside = false;
2871 self.sched.window_focused = false;
2872 self.handle_pointer_cancel();
2873 self.held_keys.clear();
2874 self.held_mouse.clear();
2875 self.sched.held_keys.clear();
2876 self.sched.mouse_primary = false;
2877 self.sched.mouse_secondary = false;
2878 self.sched.mouse_middle = false;
2879 self.sched.touch_points.clear();
2880 self.key_pressed_active = None;
2881 self.key_pressed_key = None;
2882 self.key_long_press = None;
2883 self.pressed_ids.clear();
2884 self.pending_click = None;
2885 self.last_down = None;
2886 self.press_origin = None;
2887 self.last_up = None;
2888 self.double_candidate = None;
2889 self.scroll_capture_id = None;
2890 self.last_scroll_at = None;
2891 self.finish_compositions();
2892 }
2893
2894 pub fn set_physical_key(&mut self, name: &str, down: bool) {
2899 if down {
2900 self.held_keys.insert(name.to_string());
2901 } else {
2902 self.held_keys.remove(name);
2903 }
2904 }
2905
2906 pub fn held_physical_keys(&self) -> Vec<String> {
2908 let mut out: Vec<String> = self.held_keys.iter().cloned().collect();
2909 out.sort();
2910 out
2911 }
2912
2913 pub fn physical_key_held(&self, name: &str) -> bool {
2915 self.held_keys.contains(name)
2916 }
2917
2918 pub fn mouse_button_held(&self, button: PointerButton) -> bool {
2920 self.held_mouse.contains(&button)
2921 }
2922
2923 pub fn ensure_textfield_state(&mut self, key: u64) -> Rc<RefCell<TextFieldState>> {
2925 self.textfield_states
2926 .entry(key)
2927 .or_insert_with(|| Rc::new(RefCell::new(TextFieldState::new())))
2928 .clone()
2929 }
2930
2931 pub fn ensure_textfield_state_seeded(
2932 &mut self,
2933 key: u64,
2934 seed: &str,
2935 ) -> Rc<RefCell<TextFieldState>> {
2936 ensure_tf_state(&mut self.textfield_states, key, seed)
2937 }
2938
2939 pub fn tf_key_of(&self, visual_id: u64) -> u64 {
2941 self.frame_cache
2942 .as_ref()
2943 .map(|f| tf_key_of(f, visual_id))
2944 .unwrap_or(visual_id)
2945 }
2946
2947 pub fn is_textfield(&self, id: u64) -> bool {
2949 self.frame_cache
2950 .as_ref()
2951 .map(|f| is_textfield_in_frame(f, id))
2952 .unwrap_or(false)
2953 }
2954
2955 pub fn is_editable_textfield(&self, id: u64) -> bool {
2957 self.frame_cache
2958 .as_ref()
2959 .map(|f| is_tf_editable(f, id))
2960 .unwrap_or(false)
2961 }
2962
2963 pub fn is_multiline(&self, id: u64) -> bool {
2965 self.frame_cache
2966 .as_ref()
2967 .map(|f| is_multiline_id(f, id))
2968 .unwrap_or(false)
2969 }
2970
2971 pub fn focused_keyboard_hints(
2974 &self,
2975 ) -> (
2976 repose_core::ImePurposeHint,
2977 bool,
2978 repose_core::KeyboardCapitalization,
2979 ) {
2980 let defaults = || {
2981 (
2982 repose_core::ImePurposeHint::Normal,
2983 true,
2984 repose_core::KeyboardCapitalization::Unspecified,
2985 )
2986 };
2987 let Some(fid) = self.sched.focused else {
2988 return defaults();
2989 };
2990 let Some(f) = &self.frame_cache else {
2991 return defaults();
2992 };
2993 match f.hit_regions.iter().find(|h| h.id == fid) {
2994 Some(hit) => (
2995 hit.keyboard_type.ime_purpose_hint(),
2996 hit.auto_correct.unwrap_or(true),
2997 hit.capitalization,
2998 ),
2999 None => defaults(),
3000 }
3001 }
3002
3003 pub fn focused_ime_action(&self) -> repose_core::text::ImeAction {
3004 self.frame_cache
3005 .as_ref()
3006 .and_then(|frame| {
3007 self.sched
3008 .focused
3009 .and_then(|id| frame.hit_regions.iter().find(|hit| hit.id == id))
3010 })
3011 .map(|hit| hit.ime_action)
3012 .unwrap_or_default()
3013 }
3014
3015 pub fn delete_surrounding_into_focused(&mut self, before: usize, after: usize) -> bool {
3019 if self.ime_preedit {
3020 return false;
3021 }
3022 let Some(fid) = self.sched.focused else {
3023 return false;
3024 };
3025 let Some(f) = self.frame_cache.clone() else {
3026 return false;
3027 };
3028 if !(is_textfield_in_frame(&f, fid) && is_tf_editable(&f, fid)) {
3029 return false;
3030 }
3031 let key = tf_key_of(&f, fid);
3032 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
3033 return false;
3034 };
3035 let Some(hit) = f.hit_regions.iter().find(|hit| hit.id == fid) else {
3036 return false;
3037 };
3038 let mut edited = state_rc.borrow().clone();
3039 let changed = repose_ui::textfield::delete_surrounding_with_input_transformation(
3040 hit,
3041 &mut edited,
3042 before,
3043 after,
3044 );
3045 if changed {
3046 tf_ensure_caret_visible_for_hit(hit, &mut edited);
3047 }
3048 let text = changed.then(|| edited.text.clone());
3049 *state_rc.borrow_mut() = edited;
3050 if let Some(text) = text {
3051 notify_text_change(&f, fid, text);
3052 }
3053 if changed {
3054 request_frame();
3055 }
3056 changed
3057 }
3058
3059 pub fn insert_text_into_focused(&mut self, text: &str) -> bool {
3065 if text.is_empty()
3066 || self.ime_preedit
3067 || self.modifiers.ctrl
3068 || self.modifiers.alt
3069 || self.modifiers.meta
3070 {
3071 return false;
3072 }
3073 let Some(fid) = self.sched.focused else {
3074 return false;
3075 };
3076 let Some(f) = self.frame_cache.clone() else {
3077 return false;
3078 };
3079 if !is_textfield_in_frame(&f, fid) {
3080 return false;
3081 }
3082 if !is_tf_editable(&f, fid) {
3083 return false;
3084 }
3085 let key = tf_key_of(&f, fid);
3086 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
3087 return false;
3088 };
3089 let multiline = is_multiline_id(&f, fid);
3090 let filtered: String = text
3091 .chars()
3092 .filter(|c| {
3093 (*c == '\n' && multiline) || (!c.is_control() && *c != '\r')
3097 })
3098 .collect();
3099 if filtered.is_empty() {
3100 return false;
3101 }
3102 let new_text = if let Some(hit) = f.hit_regions.iter().find(|hit| hit.id == fid) {
3103 let mut edited = state_rc.borrow().clone();
3104 repose_ui::textfield::insert_text_with_input_transformation(
3105 hit,
3106 &mut edited,
3107 &filtered,
3108 false,
3109 );
3110 tf_ensure_caret_visible_for_hit(hit, &mut edited);
3111 let text = edited.text.clone();
3112 *state_rc.borrow_mut() = edited;
3113 text
3114 } else {
3115 return false;
3116 };
3117 notify_text_change(&f, fid, new_text);
3118 request_frame();
3119 true
3120 }
3121
3122 pub fn insert_text(&mut self, text: &str) -> bool {
3126 self.insert_text_into_focused(text)
3127 }
3128
3129 pub fn paste_into_focused(&mut self, text: &str) {
3132 let Some(fid) = self.sched.focused else {
3133 return;
3134 };
3135 let Some(f) = &self.frame_cache.clone() else {
3136 return;
3137 };
3138 if !is_tf_editable(f, fid) {
3139 return;
3140 }
3141 let key = tf_key_of(f, fid);
3142 let Some(state_rc) = self.textfield_states.get(&key) else {
3143 return;
3144 };
3145 let Some(hit) = f.hit_regions.iter().find(|hit| hit.id == fid) else {
3146 return;
3147 };
3148 let filtered: String = text
3149 .chars()
3150 .filter(|c| (*c == '\n' && hit.tf_multiline) || (!c.is_control() && *c != '\r'))
3151 .collect();
3152 if filtered.is_empty() {
3153 return;
3154 }
3155 let new_text = {
3156 let mut edited = state_rc.borrow().clone();
3157 repose_ui::textfield::insert_text_with_input_transformation(
3158 hit,
3159 &mut edited,
3160 &filtered,
3161 true,
3162 );
3163 tf_ensure_caret_visible_for_hit(hit, &mut edited);
3164 let text = edited.text.clone();
3165 *state_rc.borrow_mut() = edited;
3166 text
3167 };
3168 notify_text_change(f, fid, new_text);
3169 request_frame();
3170 }
3171
3172 pub fn handle_gamepad(&mut self, event: &GamepadEvent) -> bool {
3178 match event {
3179 GamepadEvent::Connected {
3180 id,
3181 name,
3182 vendor_id,
3183 product_id,
3184 } => {
3185 self.gamepads.insert(
3186 id.0,
3187 GamepadPad {
3188 name: name.clone(),
3189 vendor_id: *vendor_id,
3190 product_id: *product_id,
3191 ..GamepadPad::default()
3192 },
3193 );
3194 request_frame();
3195 false
3196 }
3197 GamepadEvent::Disconnected { id } => {
3198 self.gamepads.remove(&id.0);
3199 self.sensors.retain(|(pad, _), _| *pad != id.0);
3200 request_frame();
3201 false
3202 }
3203 GamepadEvent::Button {
3204 id,
3205 button,
3206 pressed,
3207 } => {
3208 if let Some(pad) = self.gamepads.get_mut(&id.0) {
3209 if *pressed {
3210 pad.pressed.insert(*button);
3211 } else {
3212 pad.pressed.remove(button);
3213 }
3214 }
3215 if *pressed {
3216 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
3217 }
3218 let key = match button {
3219 GamepadButton::South => Key::Space,
3220 GamepadButton::East => Key::Escape,
3221 GamepadButton::Start => Key::Enter,
3222 GamepadButton::DPadUp => Key::ArrowUp,
3223 GamepadButton::DPadDown => Key::ArrowDown,
3224 GamepadButton::DPadLeft => Key::ArrowLeft,
3225 GamepadButton::DPadRight => Key::ArrowRight,
3226 _ => return false,
3227 };
3228 let synthetic = KeyEvent {
3229 key,
3230 modifiers: self.modifiers,
3231 is_repeat: false,
3232 event_type: if *pressed {
3233 KeyEventType::Down
3234 } else {
3235 KeyEventType::Up
3236 },
3237 utf16_code_point: 0,
3238 physical: None,
3239 };
3240 self.handle_key(&synthetic)
3241 }
3242 GamepadEvent::Axis { id, axis, value } => {
3243 if let Some(pad) = self.gamepads.get_mut(&id.0) {
3244 pad.axes.insert(*axis, *value);
3245 }
3246 false
3247 }
3248 }
3249 }
3250
3251 pub fn request_rumble(
3256 &mut self,
3257 id: repose_core::input::GamepadId,
3258 low_freq: f32,
3259 high_freq: f32,
3260 duration_ms: u32,
3261 ) {
3262 self.pending_rumble.push((
3263 id.0,
3264 low_freq.clamp(0.0, 1.0),
3265 high_freq.clamp(0.0, 1.0),
3266 duration_ms,
3267 ));
3268 }
3269
3270 pub fn stop_rumble(&mut self, id: repose_core::input::GamepadId) {
3272 self.pending_rumble.push((id.0, 0.0, 0.0, 0));
3273 }
3274
3275 pub fn take_rumble_requests(&mut self) -> Vec<(u32, f32, f32, u32)> {
3278 let mut requests = std::mem::take(&mut self.pending_rumble);
3279 let global = repose_core::rumble::take();
3280 if !global.is_empty() {
3281 let pads: Vec<u32> = self.gamepads.keys().copied().collect();
3282 for (low, high, duration_ms) in global {
3283 for &id in &pads {
3284 requests.push((id, low, high, duration_ms));
3285 }
3286 }
3287 }
3288 requests
3289 }
3290
3291 pub fn handle_sensor_sample(&mut self, id: GamepadId, sample: SensorSample) {
3297 let key = (id.0, sample.kind);
3298 let deadband = match sample.kind {
3299 SensorKind::Gyroscope => SENSOR_GYRO_DEADBAND,
3300 SensorKind::Accelerometer => SENSOR_ACCEL_DEADBAND,
3301 };
3302 let moved = self.sensors.get(&key).is_none_or(|stored| {
3303 sample
3304 .data
3305 .iter()
3306 .zip(stored)
3307 .any(|(now, was)| (now - was).abs() > deadband)
3308 });
3309 if moved {
3310 self.sensors.insert(key, sample.data);
3311 request_frame();
3312 }
3313 }
3314
3315 pub fn gamepad_sensor(&self, id: GamepadId, kind: SensorKind) -> Option<[f32; 3]> {
3320 self.sensors.get(&(id.0, kind)).copied()
3321 }
3322
3323 pub fn gamepad_sensor_kinds(&self, id: GamepadId) -> Vec<SensorKind> {
3325 [SensorKind::Gyroscope, SensorKind::Accelerometer]
3326 .into_iter()
3327 .filter(|kind| self.sensors.contains_key(&(id.0, *kind)))
3328 .collect()
3329 }
3330
3331 pub fn gamepad_vendor_product(&self, id: GamepadId) -> Option<(u16, u16)> {
3334 self.gamepads
3335 .get(&id.0)
3336 .map(|pad| (pad.vendor_id, pad.product_id))
3337 }
3338
3339 pub fn gamepad_id_by_sensor_device(&self, device: &str) -> Option<GamepadId> {
3345 let exact: Vec<u32> = self
3346 .gamepads
3347 .iter()
3348 .filter(|(_, pad)| pad.name == device)
3349 .map(|(id, _)| *id)
3350 .collect();
3351 if exact.len() == 1 {
3352 return Some(GamepadId(exact[0]));
3353 }
3354 if !exact.is_empty() {
3355 return None;
3356 }
3357 let needle = repose_core::input::normalize_device_name(device);
3358 let matches: Vec<u32> = self
3359 .gamepads
3360 .iter()
3361 .filter(|(_, pad)| repose_core::input::normalize_device_name(&pad.name) == needle)
3362 .map(|(id, _)| *id)
3363 .collect();
3364 if matches.len() == 1 {
3365 Some(GamepadId(matches[0]))
3366 } else {
3367 None
3368 }
3369 }
3370
3371 pub fn handle_key_with_text(&mut self, event: &KeyEvent, composed_text: Option<&str>) -> bool {
3377 if event.event_type == KeyEventType::Down {
3378 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
3379 }
3380 if event.event_type == KeyEventType::Down
3381 && !event.is_repeat
3382 && !self.ime_preedit
3383 && !event.modifiers.ctrl
3384 && !event.modifiers.alt
3385 && !event.modifiers.meta
3386 && let Some(text) = composed_text
3387 && !text.chars().all(|c| c.is_control())
3388 && self.insert_text_into_focused(text)
3389 {
3390 return true;
3391 }
3392 self.handle_key(event)
3393 }
3394
3395 pub fn handle_scroll_at(
3399 &mut self,
3400 pos: Vec2,
3401 delta: Vec2,
3402 scroll_capture: Option<u64>,
3403 ) -> (bool, Option<u64>) {
3404 let Some(f) = &self.frame_cache else {
3405 return (false, scroll_capture);
3406 };
3407 let (consumed, cap) = dispatch_scroll(
3410 f,
3411 pos,
3412 delta,
3413 scroll_capture,
3414 repose_core::input::ScrollSource::Touch,
3415 );
3416 if consumed {
3417 request_frame();
3418 }
3419 (consumed, cap)
3420 }
3421
3422 fn next_caret_blink_deadline(&self) -> Option<web_time::Instant> {
3425 let fid = self.sched.focused?;
3426 let frame = self.frame_cache.as_ref()?;
3427 let hit = frame.hit_regions.iter().find(|h| h.id == fid)?;
3428 let key = hit.tf_state_key?;
3429 self.textfield_states
3430 .get(&key)?
3431 .borrow()
3432 .next_blink_deadline()
3433 }
3434
3435 fn next_gesture_deadline(&self) -> Option<web_time::Instant> {
3436 [
3437 self.pending_click
3438 .as_ref()
3439 .map(|(_, at, _)| *at + web_time::Duration::from_millis(DOUBLE_CLICK_MS as u64)),
3440 self.long_press
3441 .as_ref()
3442 .map(|(_, at, _, _)| *at + web_time::Duration::from_millis(LONG_PRESS_MS as u64)),
3443 self.key_long_press
3444 .as_ref()
3445 .filter(|(_, _, fired)| !*fired)
3446 .map(|(_, at, _)| *at + web_time::Duration::from_millis(LONG_PRESS_MS as u64)),
3447 repose_ui::scrollbar_repeat::track_repeat_deadline(),
3448 repose_ui::scrollbar_fade::scrollbar_fade_deadline(),
3449 ]
3450 .into_iter()
3451 .flatten()
3452 .min()
3453 }
3454
3455 pub fn next_wakeup_deadline(&self) -> Option<web_time::Instant> {
3458 [
3459 self.next_caret_blink_deadline(),
3460 self.next_gesture_deadline(),
3461 repose_ui::overlay::SnackbarController::next_deadline(),
3462 repose_core::timer::next_deadline(),
3463 ]
3464 .into_iter()
3465 .flatten()
3466 .min()
3467 }
3468
3469 pub fn is_wakeup_due(&self, now: web_time::Instant) -> bool {
3471 self.next_wakeup_deadline().is_some_and(|d| d <= now)
3472 }
3473
3474 pub fn is_caret_blink_due(&self, now: web_time::Instant) -> bool {
3477 self.is_wakeup_due(now)
3478 }
3479
3480 pub fn next_frame_deadline(
3484 &self,
3485 now: web_time::Instant,
3486 idle_cap: web_time::Duration,
3487 ) -> web_time::Instant {
3488 self.next_wakeup_deadline().unwrap_or(now + idle_cap)
3489 }
3490
3491 pub fn tick_overlays(&self) {
3495 repose_ui::overlay::SnackbarController::tick_all();
3496 repose_core::timer::poll();
3497 }
3498
3499 pub fn cursor_suggestion(&self) -> Option<CursorIcon> {
3501 self.cursor.clone()
3502 }
3503
3504 pub fn take_cursor_suggestion(&mut self) -> Option<CursorIcon> {
3506 self.cursor.take()
3507 }
3508
3509 pub fn request_fullscreen(&mut self, fullscreen: bool) {
3513 self.fullscreen_request = Some(fullscreen);
3514 }
3515
3516 pub fn fullscreen(&self) -> Option<bool> {
3518 self.fullscreen_request
3519 }
3520
3521 pub fn take_fullscreen_request(&mut self) -> Option<bool> {
3523 self.fullscreen_request.take()
3524 }
3525
3526 fn move_cursor_suggestion(&self) -> Option<CursorIcon> {
3527 self.frame_cursor_override
3528 .clone()
3529 .or_else(|| self.cursor.clone())
3530 }
3531}
3532
3533impl Default for ReposeRuntime {
3534 fn default() -> Self {
3535 Self::new()
3536 }
3537}
3538
3539pub fn compose_frame_inner<F>(
3541 sched: &mut Scheduler,
3542 root_fn: &mut F,
3543 scale: f32,
3544 size_px_u32: (u32, u32),
3545 hover_id: Option<u64>,
3546 pressed_ids: &HashSet<u64>,
3547 tf_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
3548) -> Frame
3549where
3550 F: FnMut(&mut Scheduler) -> View,
3551{
3552 compose_frame_inner_with_ancestors(
3553 sched,
3554 root_fn,
3555 scale,
3556 size_px_u32,
3557 hover_id,
3558 &std::collections::HashSet::new(),
3559 pressed_ids,
3560 tf_states,
3561 )
3562}
3563
3564pub fn compose_frame_inner_with_ancestors<F>(
3565 sched: &mut Scheduler,
3566 root_fn: &mut F,
3567 scale: f32,
3568 size_px_u32: (u32, u32),
3569 hover_id: Option<u64>,
3570 hover_ancestors: &std::collections::HashSet<u64>,
3571 pressed_ids: &HashSet<u64>,
3572 tf_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
3573) -> Frame
3574where
3575 F: FnMut(&mut Scheduler) -> View,
3576{
3577 for requested_id in repose_core::runtime::drain_focus_requests() {
3578 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
3579 sched.focused = None;
3580 continue;
3581 }
3582 sched.focused = Some(requested_id);
3583 }
3584
3585 set_density_default(Density { scale });
3586
3587 let current_focused = sched.focused;
3588
3589 let frame = sched.repose(
3590 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s)),
3591 {
3592 let hover_ancestors = hover_ancestors.clone();
3593 let pressed_ids = pressed_ids.clone();
3594 move |view, _size| {
3595 let interactions = Interactions {
3596 hover: hover_id,
3597 hover_ancestors: hover_ancestors.clone(),
3598 pressed: pressed_ids.clone(),
3599 };
3600 with_density(Density { scale }, || {
3601 layout_and_paint(view, size_px_u32, tf_states, &interactions, current_focused)
3602 })
3603 }
3604 },
3605 );
3606
3607 for requested_id in repose_core::runtime::drain_focus_requests() {
3608 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
3609 sched.focused = None;
3610 } else if frame.focus_chain.contains(&requested_id) {
3611 sched.focused = Some(requested_id);
3612 }
3613 }
3614
3615 if let Some(fid) = sched.focused
3616 && !frame.focus_chain.contains(&fid)
3617 {
3618 sched.focused = None;
3619 }
3620
3621 frame
3622}
3623
3624fn key_ancestor_chain(f: &Frame, focused: Option<u64>) -> Vec<u64> {
3628 let parent_of: HashMap<u64, Option<u64>> =
3629 f.hit_regions.iter().map(|h| (h.id, h.parent)).collect();
3630 let mut leaf = focused.filter(|id| parent_of.contains_key(id));
3631 if leaf.is_none() {
3632 let ids: HashSet<u64> = parent_of.keys().copied().collect();
3633 leaf = f
3634 .hit_regions
3635 .iter()
3636 .filter(|h| !h.disabled)
3637 .find(|h| h.parent.is_none_or(|p| !ids.contains(&p)))
3638 .map(|h| h.id);
3639 }
3640 let Some(mut cur) = leaf else {
3641 return Vec::new();
3642 };
3643 let mut chain = vec![cur];
3644 while let Some(parent) = parent_of.get(&cur).copied().flatten() {
3645 if chain.contains(&parent) {
3646 break;
3647 }
3648 chain.push(parent);
3649 cur = parent;
3650 }
3651 chain.reverse();
3652 chain
3653}
3654
3655fn hover_chain_for(frame: Option<&Frame>, hover: Option<u64>) -> std::collections::HashSet<u64> {
3656 let Some(f) = frame else {
3657 return std::collections::HashSet::new();
3658 };
3659 let Some(mut cur) = hover else {
3660 return std::collections::HashSet::new();
3661 };
3662 let map: std::collections::HashMap<u64, Option<u64>> =
3663 f.hit_regions.iter().map(|h| (h.id, h.parent)).collect();
3664 let mut set = std::collections::HashSet::new();
3665 loop {
3666 set.insert(cur);
3667 if let Some(Some(parent)) = map.get(&cur).copied() {
3668 cur = parent;
3669 } else {
3670 break;
3671 }
3672 }
3673 set
3674}
3675
3676fn dispatch_hover_change_bubbled(
3677 frame: Option<&Frame>,
3678 leave_map: &HashMap<u64, repose_ui::HitRegionSnapshot>,
3679 hover_id: &mut Option<u64>,
3680 hover_ancestors: &mut std::collections::HashSet<u64>,
3681 new_hover: Option<u64>,
3682 pos: Vec2,
3683 modifiers: Modifiers,
3684) {
3685 let old_hover = *hover_id;
3686 let mut old_chain = hover_ancestors.clone();
3687 if let Some(id) = old_hover {
3688 old_chain.insert(id);
3689 }
3690 let new_chain = hover_chain_for(frame, new_hover);
3691 if old_chain == new_chain {
3692 return;
3693 }
3694 for leave_id in old_chain.difference(&new_chain) {
3695 let current = frame.and_then(|f| f.hit_regions.iter().find(|h| h.id == *leave_id));
3696 if let Some(hit) = current
3697 && let Some(cb) = &hit.on_pointer_leave
3698 {
3699 let mut pe = PointerEvent::new(
3700 PointerId(0),
3701 PointerKind::Mouse,
3702 PointerEventKind::Leave,
3703 pos,
3704 1.0,
3705 modifiers,
3706 );
3707 let (origin, local) = repose_ui::hit_region_pointer_coordinates(hit, pos);
3708 pe.origin = origin;
3709 pe.position = local;
3710 cb(pe);
3711 continue;
3712 }
3713 if let Some(snapshot) = leave_map.get(leave_id)
3714 && let Some(callback) = &snapshot.hit().on_pointer_leave
3715 {
3716 let mut pe = PointerEvent::new(
3717 PointerId(0),
3718 PointerKind::Mouse,
3719 PointerEventKind::Leave,
3720 pos,
3721 1.0,
3722 modifiers,
3723 );
3724 let (origin, local) = snapshot.pointer_coordinates(pos);
3725 pe.origin = origin;
3726 pe.position = local;
3727 callback(pe);
3728 }
3729 }
3730 for enter_id in new_chain.difference(&old_chain) {
3731 if let Some(f) = frame
3732 && let Some(h) = f.hit_regions.iter().find(|h| h.id == *enter_id)
3733 && let Some(cb) = &h.on_pointer_enter
3734 {
3735 let mut pe = PointerEvent::new(
3736 PointerId(0),
3737 PointerKind::Mouse,
3738 PointerEventKind::Enter,
3739 pos,
3740 1.0,
3741 modifiers,
3742 );
3743 let (origin, local) = repose_ui::hit_region_pointer_coordinates(h, pos);
3744 pe.origin = origin;
3745 pe.position = local;
3746 cb(pe);
3747 }
3748 }
3749 *hover_id = new_hover;
3750 hover_ancestors.clear();
3751 for id in &new_chain {
3752 if Some(*id) != new_hover {
3753 hover_ancestors.insert(*id);
3754 }
3755 }
3756}
3757
3758pub fn is_textfield_in_frame(f: &Frame, id: u64) -> bool {
3759 f.semantics_nodes
3760 .iter()
3761 .any(|n| n.id == id && n.role == repose_core::semantics::Role::TextField)
3762}
3763
3764pub fn is_textfield_in_frame_cache(frame_cache: &Option<Rc<Frame>>, id: u64) -> bool {
3765 if let Some(f) = frame_cache {
3766 is_textfield_in_frame(f, id)
3767 } else {
3768 false
3769 }
3770}
3771
3772pub fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
3773 frame.hit_regions.iter().position(|h| h.id == id)
3774}
3775
3776fn is_multiline_id(f: &Frame, id: u64) -> bool {
3777 f.hit_regions
3778 .iter()
3779 .find(|h| h.id == id)
3780 .map(|h| h.tf_multiline)
3781 .unwrap_or(false)
3782}
3783
3784fn tf_can_edit(hit: &HitRegion) -> bool {
3787 hit.tf_enabled && !hit.tf_read_only
3788}
3789
3790const SENSOR_GYRO_DEADBAND: f32 = 0.5;
3796const SENSOR_ACCEL_DEADBAND: f32 = 0.02;
3797
3798fn tf_is_sensitive(hit: &HitRegion) -> bool {
3799 hit.tf_sensitive
3800 || matches!(
3801 hit.keyboard_type,
3802 repose_core::KeyboardType::Password
3803 | repose_core::KeyboardType::NumberPassword
3804 | repose_core::KeyboardType::DecimalPassword
3805 | repose_core::KeyboardType::NumberPasswordSigned
3806 | repose_core::KeyboardType::DecimalPasswordSigned
3807 )
3808}
3809
3810fn is_tf_editable(f: &Frame, id: u64) -> bool {
3811 f.hit_regions
3812 .iter()
3813 .find(|h| h.id == id)
3814 .is_some_and(tf_can_edit)
3815}
3816
3817fn tf_key_of(frame: &Frame, visual_id: u64) -> u64 {
3818 if let Some(i) = frame.hit_regions.iter().position(|h| h.id == visual_id) {
3819 let hr = &frame.hit_regions[i];
3820 return hr.tf_state_key.unwrap_or(hr.id);
3821 }
3822 visual_id
3823}
3824
3825fn set_caret(state: &mut TextFieldState, position: usize, extend: bool) {
3826 if extend {
3827 state.selection.end = position;
3828 } else {
3829 state.selection = position..position;
3830 }
3831}
3832
3833fn notify_text_change(f: &Frame, id: u64, text: String) {
3834 if let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
3835 && let Some(cb) = &h.on_text_change
3836 {
3837 cb(text);
3838 }
3839}
3840
3841fn tf_ensure_caret_visible_for_hit(hit: &HitRegion, state: &mut TextFieldState) {
3842 let metrics = repose_ui::textfield::textfield_metrics(hit);
3843 tf_ensure_caret_visible_with_metrics(state, hit.tf_multiline, &metrics);
3844}
3845
3846fn tf_ensure_caret_visible_with_metrics(
3847 state: &mut TextFieldState,
3848 is_multiline: bool,
3849 metrics: &repose_ui::textfield::TextFieldMetrics,
3850) {
3851 repose_ui::textfield::ensure_caret_visible_with_metrics(state, is_multiline, metrics);
3852}
3853
3854fn index_for_x_bytes_vt(
3855 state: &TextFieldState,
3856 metrics: &repose_ui::textfield::TextFieldMetrics,
3857 x_px: f32,
3858) -> usize {
3859 if let Some(transformation) = &state.visual_transformation {
3860 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
3861 let transformed = transformation.filter(&annotated);
3862 let display = repose_ui::textfield::index_for_x_bytes_with_config(
3863 transformed.text.as_str(),
3864 metrics.font_px,
3865 x_px,
3866 metrics.measure_config(),
3867 );
3868 transformed.offset_mapping.transformed_to_original(display)
3869 } else {
3870 repose_ui::textfield::index_for_x_bytes_with_config(
3871 &state.text,
3872 metrics.font_px,
3873 x_px,
3874 metrics.measure_config(),
3875 )
3876 }
3877}
3878
3879fn index_for_xy_bytes_vt(
3880 state: &TextFieldState,
3881 metrics: &repose_ui::textfield::TextFieldMetrics,
3882 wrap_width: f32,
3883 x_px: f32,
3884 y_px: f32,
3885) -> usize {
3886 if let Some(transformation) = &state.visual_transformation {
3887 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
3888 let transformed = transformation.filter(&annotated);
3889 let display = repose_ui::textfield::index_for_xy_bytes_with_metrics(
3890 transformed.text.as_str(),
3891 wrap_width,
3892 x_px,
3893 y_px,
3894 metrics,
3895 );
3896 transformed.offset_mapping.transformed_to_original(display)
3897 } else {
3898 repose_ui::textfield::index_for_xy_bytes_with_metrics(
3899 &state.text,
3900 wrap_width,
3901 x_px,
3902 y_px,
3903 metrics,
3904 )
3905 }
3906}
3907
3908fn dispatch_scroll(
3910 frame: &Frame,
3911 pos: Vec2,
3912 delta: Vec2,
3913 scroll_capture: Option<u64>,
3914 source: repose_core::input::ScrollSource,
3915) -> (bool, Option<u64>) {
3916 let mut remaining = delta;
3917 let mut first_consumer: Option<u64> = None;
3918 if let Some(cid) = scroll_capture
3919 && let Some(hit) = frame.hit_regions.iter().find(|h| h.id == cid)
3920 && let Some(leftover) = run_scroll_handler(hit, delta, source)
3921 {
3922 if (delta.x - leftover.x).abs() > 0.001 || (delta.y - leftover.y).abs() > 0.001 {
3923 first_consumer = Some(cid);
3924 }
3925 remaining = leftover;
3926 if remaining.x.abs() <= 0.001 && remaining.y.abs() <= 0.001 {
3927 return (true, Some(cid));
3928 }
3929 }
3930
3931 let mut consumed_any = first_consumer.is_some();
3932 for id in repose_ui::hit_test_frame_regions(frame, pos) {
3933 if Some(id) == scroll_capture {
3934 continue;
3935 }
3936 let Some(hit) = frame.hit_regions.iter().find(|hit| hit.id == id) else {
3937 continue;
3938 };
3939 if remaining.x.abs() <= 0.001 && remaining.y.abs() <= 0.001 {
3940 break;
3941 }
3942 if let Some(leftover) = run_scroll_handler(hit, remaining, source) {
3943 let before = remaining;
3944 if (before.x - leftover.x).abs() > 0.001 || (before.y - leftover.y).abs() > 0.001 {
3945 consumed_any = true;
3946 if first_consumer.is_none() {
3947 first_consumer = Some(hit.id);
3948 }
3949 }
3950 remaining = leftover;
3951 }
3952 }
3953 if consumed_any {
3954 (true, first_consumer)
3955 } else {
3956 (false, None)
3957 }
3958}
3959
3960fn run_scroll_handler(
3963 hit: &HitRegion,
3964 delta: Vec2,
3965 source: repose_core::input::ScrollSource,
3966) -> Option<Vec2> {
3967 if let Some(cb) = &hit.on_scroll_with_source {
3968 return Some(cb(delta, source));
3969 }
3970 hit.on_scroll.as_ref().map(|cb| cb(delta))
3971}
3972
3973#[cfg(test)]
3974mod scroll_source_tests {
3975 use super::*;
3976 use repose_core::input::ScrollSource;
3977 use std::cell::RefCell;
3978 use std::rc::Rc;
3979
3980 fn recording_frame(id: u64) -> (Frame, Rc<RefCell<Vec<ScrollSource>>>) {
3983 let seen: Rc<RefCell<Vec<ScrollSource>>> = Rc::new(RefCell::new(Vec::new()));
3984 let recorder = seen.clone();
3985 let frame = Frame {
3986 scene: Default::default(),
3987 hit_regions: vec![HitRegion {
3988 id,
3989 rect: repose_core::Rect {
3990 x: 0.0,
3991 y: 0.0,
3992 w: 200.0,
3993 h: 200.0,
3994 },
3995 on_scroll_with_source: Some(Rc::new(move |_d: Vec2, s: ScrollSource| {
3996 recorder.borrow_mut().push(s);
3997 Vec2 { x: 0.0, y: 0.0 }
3999 })),
4000 ..Default::default()
4001 }],
4002 semantics_nodes: vec![],
4003 focus_chain: vec![id],
4004 };
4005 (frame, seen)
4006 }
4007
4008 #[test]
4009 fn dispatch_passes_the_source_through_to_the_handler() {
4010 let (frame, seen) = recording_frame(1);
4011 let pos = Vec2 { x: 50.0, y: 50.0 };
4012 let delta = Vec2 { x: 0.0, y: 4.0 };
4013
4014 let (consumed, _) = dispatch_scroll(&frame, pos, delta, None, ScrollSource::Wheel);
4015 assert!(consumed, "handler consumed the delta");
4016 let (consumed, _) = dispatch_scroll(&frame, pos, delta, None, ScrollSource::Touch);
4017 assert!(consumed);
4018
4019 assert_eq!(
4020 *seen.borrow(),
4021 vec![ScrollSource::Wheel, ScrollSource::Touch]
4022 );
4023 }
4024
4025 #[test]
4026 fn legacy_on_scroll_still_runs_and_reports_its_leftover() {
4027 let hit = HitRegion {
4028 id: 3,
4029 on_scroll: Some(Rc::new(|d: Vec2| Vec2 { x: 0.0, y: d.y })),
4030 ..Default::default()
4031 };
4032 let leftover = run_scroll_handler(&hit, Vec2 { x: 7.0, y: 5.0 }, ScrollSource::Touch);
4033 assert_eq!(leftover, Some(Vec2 { x: 0.0, y: 5.0 }));
4034 }
4035
4036 #[test]
4037 fn source_aware_handler_takes_precedence_over_legacy() {
4038 let legacy = Rc::new(RefCell::new(0u32));
4039 let aware = Rc::new(RefCell::new(0u32));
4040 let (legacy_cb, aware_cb) = (legacy.clone(), aware.clone());
4041 let hit = HitRegion {
4042 id: 4,
4043 on_scroll: Some(Rc::new(move |d: Vec2| {
4044 *legacy_cb.borrow_mut() += 1;
4045 d
4046 })),
4047 on_scroll_with_source: Some(Rc::new(move |d: Vec2, _s: ScrollSource| {
4048 *aware_cb.borrow_mut() += 1;
4049 d
4050 })),
4051 ..Default::default()
4052 };
4053 run_scroll_handler(&hit, Vec2 { x: 1.0, y: 1.0 }, ScrollSource::Wheel);
4054 assert_eq!(*aware.borrow(), 1, "source-aware handler runs");
4055 assert_eq!(*legacy.borrow(), 0, "legacy handler is not also run");
4056 }
4057
4058 #[test]
4059 fn region_without_any_scroll_handler_is_skipped() {
4060 let hit = HitRegion {
4061 id: 5,
4062 ..Default::default()
4063 };
4064 assert_eq!(
4065 run_scroll_handler(&hit, Vec2 { x: 1.0, y: 1.0 }, ScrollSource::Wheel),
4066 None
4067 );
4068 }
4069}
4070
4071#[cfg(test)]
4072mod ime_tests {
4073 use super::*;
4074 use repose_core::input::ImeEvent;
4075
4076 const TF_ID: u64 = 100;
4077
4078 fn editable_frame(id: u64) -> Frame {
4079 let rect = repose_core::Rect {
4080 x: 0.0,
4081 y: 0.0,
4082 w: 200.0,
4083 h: 30.0,
4084 };
4085 Frame {
4086 scene: Default::default(),
4087 hit_regions: vec![HitRegion {
4088 id,
4089 rect,
4090 focusable: true,
4091 tf_state_key: Some(id),
4092 tf_multiline: false,
4093 tf_enabled: true,
4094 tf_read_only: false,
4095 ..Default::default()
4096 }],
4097 semantics_nodes: vec![repose_core::runtime::SemNode {
4098 id,
4099 role: repose_core::semantics::Role::TextField,
4100 label: Some("Field".into()),
4101 rect,
4102 focused: true,
4103 ..Default::default()
4104 }],
4105 focus_chain: vec![id],
4106 }
4107 }
4108
4109 fn focused_rt() -> ReposeRuntime {
4110 let mut rt = ReposeRuntime::new();
4111 rt.sched.focused = Some(TF_ID);
4112 rt.cache_frame(editable_frame(TF_ID));
4113 let _ = rt.ensure_textfield_state(TF_ID);
4114 rt
4115 }
4116
4117 #[test]
4118 fn preedit_cursor_uses_byte_offsets_for_non_ascii() {
4119 let mut rt = focused_rt();
4120 rt.handle_ime(&ImeEvent::Update {
4121 text: "あb".to_string(),
4122 cursor: Some((3, 3)),
4123 });
4124 let st = rt.textfield_states[&TF_ID].borrow();
4125 assert_eq!(st.text, "あb");
4126 assert_eq!(st.selection, 3..3);
4127 }
4128
4129 #[test]
4130 fn cancel_discards_preedit() {
4131 let mut rt = focused_rt();
4132 rt.handle_ime(&ImeEvent::Update {
4133 text: "あ".to_string(),
4134 cursor: None,
4135 });
4136 assert!(rt.ime_preedit);
4137 rt.handle_ime(&ImeEvent::Cancel);
4138 let st = rt.textfield_states[&TF_ID].borrow();
4139 assert_eq!(st.text, "");
4140 assert!(st.composition.is_none());
4141 assert!(!rt.ime_preedit);
4142 }
4143
4144 #[test]
4145 fn defocus_finish_keeps_text() {
4146 let mut rt = focused_rt();
4147 rt.handle_ime(&ImeEvent::Update {
4148 text: "x".to_string(),
4149 cursor: None,
4150 });
4151 rt.sched.focused = None;
4152 rt.handle_focus_lost();
4153 assert_eq!(rt.textfield_states[&TF_ID].borrow().text, "x");
4154 assert!(!rt.ime_preedit);
4155 }
4156
4157 #[test]
4158 fn focused_game_node_receives_events() {
4159 let mut rt = ReposeRuntime::new();
4160 rt.sched.focused = Some(TF_ID);
4161 let mut frame = editable_frame(TF_ID);
4162 frame.hit_regions[0].tf_state_key = None;
4163 frame.hit_regions[0].tf_enabled = false;
4164 frame.semantics_nodes.clear();
4165 let seen: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
4166 let sink = seen.clone();
4167 frame.hit_regions[0].on_ime = Some(Rc::new(move |event| {
4168 if let ImeEvent::Commit(text) = event {
4169 sink.borrow_mut().push(text);
4170 }
4171 true
4172 }));
4173 rt.cache_frame(frame);
4174 rt.handle_ime(&ImeEvent::Commit("あ".to_string()));
4175 rt.handle_ime(&ImeEvent::Cancel);
4176 assert_eq!(&*seen.borrow(), &["あ".to_string()]);
4177 assert!(rt.textfield_states.is_empty());
4178 }
4179
4180 #[test]
4181 fn unconsumed_events_leave_textfields_untouched() {
4182 let mut rt = ReposeRuntime::new();
4183 rt.sched.focused = Some(TF_ID);
4184 let mut frame = editable_frame(TF_ID);
4185 frame.hit_regions[0].tf_state_key = None;
4186 frame.hit_regions[0].tf_enabled = false;
4187 frame.semantics_nodes.clear();
4188 frame.hit_regions[0].on_ime = Some(Rc::new(|_| false));
4189 rt.cache_frame(frame);
4190 rt.handle_ime(&ImeEvent::Commit("x".to_string()));
4191 assert!(rt.textfield_states.is_empty());
4192 }
4193}
4194
4195#[cfg(test)]
4196mod gamepad_tests {
4197 use super::*;
4198 use repose_core::input::{GamepadAxis, GamepadButton, GamepadEvent, GamepadId};
4199
4200 #[test]
4201 fn gamepad_state_mirrors_connection_and_inputs() {
4202 let mut rt = ReposeRuntime::new();
4203 rt.handle_gamepad(&GamepadEvent::Connected {
4204 id: GamepadId(0),
4205 name: "Pad".to_string(),
4206 vendor_id: 0,
4207 product_id: 0,
4208 });
4209 assert_eq!(rt.gamepads.len(), 1);
4210
4211 rt.handle_gamepad(&GamepadEvent::Button {
4212 id: GamepadId(0),
4213 button: GamepadButton::West,
4214 pressed: true,
4215 });
4216 assert!(rt.gamepads[&0].button(GamepadButton::West));
4217
4218 rt.handle_gamepad(&GamepadEvent::Axis {
4219 id: GamepadId(0),
4220 axis: GamepadAxis::LeftStickX,
4221 value: 0.5,
4222 });
4223 assert_eq!(rt.gamepads[&0].axis(GamepadAxis::LeftStickX), 0.5);
4224
4225 rt.handle_gamepad(&GamepadEvent::Button {
4226 id: GamepadId(0),
4227 button: GamepadButton::West,
4228 pressed: false,
4229 });
4230 assert!(!rt.gamepads[&0].button(GamepadButton::West));
4231
4232 rt.handle_gamepad(&GamepadEvent::Disconnected { id: GamepadId(0) });
4233 assert!(rt.gamepads.is_empty());
4234 }
4235
4236 #[test]
4237 fn rumble_requests_queue_and_drain() {
4238 let mut rt = ReposeRuntime::new();
4239 assert!(rt.take_rumble_requests().is_empty());
4240 rt.request_rumble(GamepadId(1), 2.0, -1.0, 150);
4241 rt.stop_rumble(GamepadId(1));
4242 let reqs = rt.take_rumble_requests();
4243 assert_eq!(reqs.len(), 2);
4244 assert_eq!(reqs[0], (1, 1.0, 0.0, 150));
4245 assert_eq!(reqs[1], (1, 0.0, 0.0, 0));
4246 assert!(rt.take_rumble_requests().is_empty());
4247 }
4248
4249 #[test]
4250 fn global_rumble_broadcasts_to_connected_pads() {
4251 let mut rt = ReposeRuntime::new();
4252 rt.handle_gamepad(&GamepadEvent::Connected {
4253 id: GamepadId(1),
4254 name: "Pad".to_string(),
4255 vendor_id: 0,
4256 product_id: 0,
4257 });
4258 repose_core::rumble::push(0.9, 0.5, 200);
4259 repose_core::rumble::push(0.0, 0.0, 0);
4260 let reqs = rt.take_rumble_requests();
4261 assert_eq!(reqs, vec![(1, 0.9, 0.5, 200), (1, 0.0, 0.0, 0)]);
4262 assert!(rt.take_rumble_requests().is_empty());
4263 }
4264
4265 #[test]
4266 fn global_rumble_without_pads_is_dropped() {
4267 let mut rt = ReposeRuntime::new();
4268 repose_core::rumble::push(0.5, 0.5, 100);
4269 assert!(rt.take_rumble_requests().is_empty());
4270 assert!(repose_core::rumble::take().is_empty());
4271 }
4272
4273 #[test]
4274 fn sensor_samples_are_latest_wins_per_pad_and_kind() {
4275 let mut rt = ReposeRuntime::new();
4276 rt.handle_sensor_sample(GamepadId(0), SensorSample::gyroscope(1.0, 2.0, 3.0));
4277 rt.handle_sensor_sample(GamepadId(0), SensorSample::gyroscope(4.0, 5.0, 6.0));
4278 rt.handle_sensor_sample(GamepadId(0), SensorSample::accelerometer(0.0, 0.0, 1.0));
4279 rt.handle_sensor_sample(GamepadId(2), SensorSample::gyroscope(-1.0, 0.0, 0.0));
4280
4281 assert_eq!(
4282 rt.gamepad_sensor(GamepadId(0), SensorKind::Gyroscope),
4283 Some([4.0, 5.0, 6.0])
4284 );
4285 assert_eq!(
4286 rt.gamepad_sensor(GamepadId(0), SensorKind::Accelerometer),
4287 Some([0.0, 0.0, 1.0])
4288 );
4289 assert_eq!(
4290 rt.gamepad_sensor(GamepadId(2), SensorKind::Gyroscope),
4291 Some([-1.0, 0.0, 0.0])
4292 );
4293 assert_eq!(rt.gamepad_sensor(GamepadId(1), SensorKind::Gyroscope), None);
4294 assert_eq!(
4295 rt.gamepad_sensor_kinds(GamepadId(0)),
4296 vec![SensorKind::Gyroscope, SensorKind::Accelerometer]
4297 );
4298 assert_eq!(
4299 rt.gamepad_sensor_kinds(GamepadId(2)),
4300 vec![SensorKind::Gyroscope]
4301 );
4302 }
4303
4304 #[test]
4305 fn resting_sensor_noise_holds_still_until_motion_accumulates() {
4306 let mut rt = ReposeRuntime::new();
4307 rt.handle_sensor_sample(GamepadId(0), SensorSample::gyroscope(0.0, 0.0, 0.0));
4308 rt.handle_sensor_sample(GamepadId(0), SensorSample::gyroscope(0.1, -0.1, 0.05));
4309 rt.handle_sensor_sample(GamepadId(0), SensorSample::accelerometer(1.0, 0.004, 0.0));
4310 rt.handle_sensor_sample(GamepadId(0), SensorSample::accelerometer(1.0, -0.004, 0.0));
4311
4312 assert_eq!(
4313 rt.gamepad_sensor(GamepadId(0), SensorKind::Gyroscope),
4314 Some([0.0, 0.0, 0.0])
4315 );
4316 assert_eq!(
4317 rt.gamepad_sensor(GamepadId(0), SensorKind::Accelerometer),
4318 Some([1.0, 0.004, 0.0])
4319 );
4320
4321 rt.handle_sensor_sample(GamepadId(0), SensorSample::gyroscope(40.0, 0.0, 0.0));
4322 rt.handle_sensor_sample(GamepadId(0), SensorSample::accelerometer(1.03, 0.0, 0.0));
4323
4324 assert_eq!(
4325 rt.gamepad_sensor(GamepadId(0), SensorKind::Gyroscope),
4326 Some([40.0, 0.0, 0.0])
4327 );
4328 assert_eq!(
4329 rt.gamepad_sensor(GamepadId(0), SensorKind::Accelerometer),
4330 Some([1.03, 0.0, 0.0])
4331 );
4332 }
4333
4334 #[test]
4335 fn sensor_state_drops_with_the_pad() {
4336 let mut rt = ReposeRuntime::new();
4337 rt.handle_gamepad(&GamepadEvent::Connected {
4338 id: GamepadId(0),
4339 name: "Pad".to_string(),
4340 vendor_id: 0,
4341 product_id: 0,
4342 });
4343 rt.handle_sensor_sample(GamepadId(0), SensorSample::gyroscope(1.0, 1.0, 1.0));
4344 assert!(
4345 rt.gamepad_sensor(GamepadId(0), SensorKind::Gyroscope)
4346 .is_some()
4347 );
4348
4349 rt.handle_gamepad(&GamepadEvent::Disconnected { id: GamepadId(0) });
4350 assert_eq!(rt.gamepad_sensor(GamepadId(0), SensorKind::Gyroscope), None);
4351 }
4352
4353 #[test]
4354 fn sensor_devices_resolve_to_connected_pads_by_name() {
4355 let mut rt = ReposeRuntime::new();
4356 rt.handle_gamepad(&GamepadEvent::Connected {
4357 id: GamepadId(3),
4358 name: "Wireless Controller (Vendor: 054c)".to_string(),
4359 vendor_id: 0,
4360 product_id: 0,
4361 });
4362
4363 assert_eq!(
4364 rt.gamepad_id_by_sensor_device("Wireless Controller (Vendor: 054c)"),
4365 Some(GamepadId(3))
4366 );
4367 assert_eq!(
4368 rt.gamepad_id_by_sensor_device("wireless controller vendor 054c"),
4369 Some(GamepadId(3))
4370 );
4371 assert_eq!(rt.gamepad_id_by_sensor_device("Some Other Pad"), None);
4372 }
4373
4374 #[test]
4375 fn ambiguous_sensor_device_names_resolve_to_nothing() {
4376 let mut rt = ReposeRuntime::new();
4377 for (id, name) in [(0, "Wireless Controller"), (1, "wireless controller!")] {
4378 rt.handle_gamepad(&GamepadEvent::Connected {
4379 id: GamepadId(id),
4380 name: name.to_string(),
4381 vendor_id: 0,
4382 product_id: 0,
4383 });
4384 }
4385 assert_eq!(rt.gamepad_id_by_sensor_device("WIRELESS CONTROLLER"), None);
4386 assert_eq!(
4387 rt.gamepad_id_by_sensor_device("Wireless Controller"),
4388 Some(GamepadId(0))
4389 );
4390 }
4391
4392 #[test]
4393 fn two_pads_sharing_one_name_never_win_by_hash_order() {
4394 let mut rt = ReposeRuntime::new();
4395 for id in 0..2 {
4396 rt.handle_gamepad(&GamepadEvent::Connected {
4397 id: GamepadId(id),
4398 name: "Nintendo Switch Pro Controller".to_string(),
4399 vendor_id: 0,
4400 product_id: 0,
4401 });
4402 }
4403 assert_eq!(
4404 rt.gamepad_id_by_sensor_device("Nintendo Switch Pro Controller"),
4405 None,
4406 "an identical name cannot pick a pad by map order"
4407 );
4408 }
4409}