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, ImeEvent, Key, KeyEvent, KeyEventType, Modifiers,
8 PointerButton, PointerEvent, PointerEventKind, PointerId, PointerKind,
9};
10use repose_core::locals::{Density, set_density_default, with_density};
11use repose_core::runtime::{Frame, Scheduler};
12use repose_core::shortcuts::DragAction;
13use repose_core::{
14 CursorIcon, HitRegion, Interaction, Modifier, RenderContext, Scene, Vec2, View, request_frame,
15};
16use repose_ui::textfield::TextFieldState;
17use repose_ui::{Interactions, layout_and_paint};
18
19fn ensure_tf_state(
20 map: &mut HashMap<u64, Rc<RefCell<TextFieldState>>>,
21 key: u64,
22 seed: &str,
23) -> Rc<RefCell<TextFieldState>> {
24 map.entry(key)
25 .or_insert_with(|| {
26 Rc::new(RefCell::new(if seed.is_empty() {
27 TextFieldState::new()
28 } else {
29 TextFieldState::with_text(seed.to_string())
30 }))
31 })
32 .clone()
33}
34
35fn ensure_all_tf_states_from_frame(
36 map: &mut HashMap<u64, Rc<RefCell<TextFieldState>>>,
37 frame: &Frame,
38) {
39 for hit in &frame.hit_regions {
40 if let Some(key) = hit.tf_state_key {
41 let st = ensure_tf_state(map, key, hit.tf_value.as_str());
42 st.borrow_mut()
44 .apply_controlled_value(hit.tf_value.as_str());
45 }
46 }
47}
48
49fn is_tf_hit(f: &Frame, id: u64) -> bool {
50 f.hit_regions
51 .iter()
52 .any(|h| h.id == id && h.tf_state_key.is_some())
53 || is_textfield_in_frame(f, id)
54}
55
56#[derive(Clone, Default)]
58pub struct PlatformOutput {
59 pub cursor: Option<CursorIcon>,
61 pub ime_allowed: bool,
63 pub ime_cursor_area: Option<(f64, f64, f64, f64)>,
65 pub clipboard_text: Option<String>,
67
68 pub ime_purpose: repose_core::ImePurposeHint,
71 pub ime_auto_correct: bool,
72 pub ime_capitalization: repose_core::KeyboardCapitalization,
73 pub keyboard_type: repose_core::KeyboardType,
74
75 pub window_theme_dark: Option<bool>,
78}
79
80pub struct FrameOutput {
82 pub scene: Scene,
84 pub hit_regions: Vec<HitRegion>,
86 pub semantics_nodes: Vec<repose_core::runtime::SemNode>,
88 pub focus_chain: Vec<u64>,
90 pub platform: PlatformOutput,
92 pub wants_pointer: bool,
94 pub wants_keyboard: bool,
96}
97
98impl FrameOutput {
99 pub fn into_frame(self) -> Frame {
102 Frame {
103 scene: self.scene,
104 hit_regions: self.hit_regions,
105 semantics_nodes: self.semantics_nodes,
106 focus_chain: self.focus_chain,
107 }
108 }
109
110 pub fn into_shared_frame(self) -> Rc<Frame> {
111 Rc::new(self.into_frame())
112 }
113}
114
115pub struct PointerMoveResult {
117 pub cursor: Option<CursorIcon>,
119 pub hover_id: Option<u64>,
121}
122
123#[derive(Debug)]
125pub struct PointerButtonResult {
126 pub focused: Option<u64>,
128 pub capture_id: Option<u64>,
130 pub consumed: bool,
132 pub needs_a11y_announce: bool,
134 pub clicked_id: Option<u64>,
137}
138
139const LONG_PRESS_MS: u128 = 500;
141const DOUBLE_CLICK_MS: u128 = 300;
142const DOUBLE_TAP_MIN_MS: u128 = 40;
143const LONG_PRESS_SLOP_DP: f32 = 18.0;
144pub(crate) const TAP_SLOP_DP: f32 = 18.0;
149
150fn moved_past_slop(x0: f32, y0: f32, pos: Vec2, slop_dp: f32, scale: f32) -> bool {
153 let slop = slop_dp * scale;
154 let dx = pos.x - x0;
155 let dy = pos.y - y0;
156 dx * dx + dy * dy > slop * slop
157}
158
159struct TouchPressState {
160 capture_id: u64,
161 pressed_ids: HashSet<u64>,
162}
163
164struct DndPointerCapture {
165 touch: Option<u64>,
166 source_id: u64,
167}
168
169pub struct ReposeRuntime {
175 pub sched: Scheduler,
176 pub scale: f32,
177 pub overlay: repose_ui::overlay::OverlayHandle,
180 pub dnd_context: repose_core::dnd::DndContext,
181 lifecycle_events: std::sync::Arc<crate::lifecycle::LifecycleDispatcher>,
182 deeplink_events: std::sync::Arc<crate::lifecycle::DeeplinkDispatcher>,
183
184 pub modifiers: Modifiers,
185 pub mouse_pos_px: (f32, f32),
186 pub pointer_inside: bool,
188 pub hover_id: Option<u64>,
189 pub hover_ancestors: std::collections::HashSet<u64>,
190 hover_leave: HashMap<u64, repose_ui::HitRegionSnapshot>,
194 pub capture_id: Option<u64>,
195 pub hit_path: Option<Vec<u64>>,
198 pub touch_paths: HashMap<u64, Vec<u64>>,
207 mouse_targets: Option<Vec<repose_ui::HitRegionSnapshot>>,
208 touch_targets: HashMap<u64, Vec<repose_ui::HitRegionSnapshot>>,
209 touch_positions: HashMap<u64, Vec2>,
210 touch_presses: HashMap<u64, TouchPressState>,
211 dnd_capture: Option<DndPointerCapture>,
212 touch_primary: Option<u64>,
213 suppressed_touch_clicks: HashSet<u64>,
214 pub scroll_capture_id: Option<u64>,
216 last_scroll_at: Option<web_time::Instant>,
217 pub pressed_ids: HashSet<u64>,
218 pub ime_preedit: bool,
219 pub key_pressed_active: Option<u64>,
220 key_pressed_key: Option<Key>,
221 pub last_focus: Option<u64>,
222 pub held_keys: HashSet<String>,
230 pub held_mouse: HashSet<PointerButton>,
235
236 last_up: Option<(u64, web_time::Instant, f32, f32)>,
237 last_down: Option<(u64, web_time::Instant)>,
241 press_origin: Option<(u64, f32, f32)>,
245 double_candidate: Option<u64>,
250 long_press: Option<(u64, web_time::Instant, f32, f32)>,
251 long_press_touch: Option<u64>,
252 key_long_press: Option<(u64, web_time::Instant, bool)>,
255 suppress_next_click: bool,
256 pending_click: Option<(u64, web_time::Instant, Rc<dyn Fn()>)>,
257
258 pub frame_cache: Option<Rc<Frame>>,
259
260 cursor: Option<CursorIcon>,
261
262 pub shortcuts: repose_core::shortcuts::ShortcutState,
263
264 pub textfield_states: HashMap<u64, Rc<RefCell<TextFieldState>>>,
265 pub gamepads: HashMap<u32, GamepadPad>,
268 pub pending_rumble: Vec<(u32, f32, f32, u32)>,
270}
271
272#[derive(Clone, Debug, Default)]
274pub struct GamepadPad {
275 pub name: String,
276 pub pressed: HashSet<GamepadButton>,
277 pub axes: HashMap<GamepadAxis, f32>,
278}
279
280impl GamepadPad {
281 pub fn button(&self, button: GamepadButton) -> bool {
282 self.pressed.contains(&button)
283 }
284
285 pub fn axis(&self, axis: GamepadAxis) -> f32 {
286 self.axes.get(&axis).copied().unwrap_or(0.0)
287 }
288}
289
290impl ReposeRuntime {
291 pub fn new() -> Self {
292 Self::with_overlay(repose_ui::overlay::OverlayHandle::new())
293 }
294
295 pub fn with_overlay(overlay: repose_ui::overlay::OverlayHandle) -> Self {
299 Self {
300 overlay,
301 dnd_context: repose_core::dnd::DndContext::default(),
302 lifecycle_events: std::sync::Arc::new(crate::lifecycle::LifecycleDispatcher::default()),
303 deeplink_events: std::sync::Arc::new(crate::lifecycle::DeeplinkDispatcher::default()),
304 sched: Scheduler::new(),
305 scale: 1.0,
306 modifiers: Modifiers::default(),
307 mouse_pos_px: (0.0, 0.0),
308 pointer_inside: false,
309 hover_id: None,
310 hover_ancestors: std::collections::HashSet::new(),
311 hover_leave: HashMap::new(),
312 capture_id: None,
313 hit_path: None,
314 touch_paths: HashMap::new(),
315 mouse_targets: None,
316 touch_targets: HashMap::new(),
317 touch_positions: HashMap::new(),
318 touch_presses: HashMap::new(),
319 dnd_capture: None,
320 touch_primary: None,
321 suppressed_touch_clicks: HashSet::new(),
322 scroll_capture_id: None,
323 last_scroll_at: None,
324 pressed_ids: HashSet::new(),
325 ime_preedit: false,
326 key_pressed_active: None,
327 key_pressed_key: None,
328 last_focus: None,
329 held_keys: HashSet::new(),
330 held_mouse: HashSet::new(),
331 last_up: None,
332 last_down: None,
333 press_origin: None,
334 double_candidate: None,
335 long_press: None,
336 long_press_touch: None,
337 key_long_press: None,
338 suppress_next_click: false,
339 pending_click: None,
340 frame_cache: None,
341 cursor: None,
342 shortcuts: repose_core::shortcuts::ShortcutState::new().without_global_fallback(),
343 textfield_states: HashMap::new(),
344 gamepads: HashMap::new(),
345 pending_rumble: Vec::new(),
346 }
347 }
348
349 pub fn event_dispatchers(&self) -> crate::lifecycle::RuntimeDispatchers {
350 crate::lifecycle::RuntimeDispatchers {
351 lifecycle: self.lifecycle_events.clone(),
352 deeplink: self.deeplink_events.clone(),
353 }
354 }
355
356 pub fn set_lifecycle_callback(
357 &mut self,
358 callback: Box<dyn Fn(crate::lifecycle::AppLifecycle) + Send>,
359 ) {
360 self.lifecycle_events.set_callback(callback);
361 }
362
363 pub fn add_lifecycle_listener(
364 &mut self,
365 callback: Box<dyn Fn(crate::lifecycle::AppLifecycle) + Send>,
366 ) -> u64 {
367 self.lifecycle_events.add_listener(callback)
368 }
369
370 pub fn remove_lifecycle_listener(&mut self, id: u64) -> bool {
371 self.lifecycle_events.remove_listener(id)
372 }
373
374 pub fn current_lifecycle(&self) -> Option<crate::lifecycle::AppLifecycle> {
375 self.lifecycle_events.current()
376 }
377
378 pub fn push_lifecycle(&mut self, state: crate::lifecycle::AppLifecycle) {
379 self.lifecycle_events.push(state);
380 }
381
382 pub fn process_lifecycle(&mut self) {
383 self.lifecycle_events.process();
384 }
385
386 pub fn set_deeplink_callback(&mut self, callback: Box<dyn Fn(Vec<u8>) + Send>) {
387 self.deeplink_events.set_callback(callback);
388 }
389
390 pub fn add_deeplink_listener(&mut self, callback: Box<dyn Fn(Vec<u8>) + Send>) -> u64 {
391 self.deeplink_events.add_listener(callback)
392 }
393
394 pub fn remove_deeplink_listener(&mut self, id: u64) -> bool {
395 self.deeplink_events.remove_listener(id)
396 }
397
398 pub fn push_deeplink(&mut self, data: Vec<u8>) {
399 self.deeplink_events.push(data);
400 }
401
402 pub fn process_deeplinks(&mut self) {
403 self.deeplink_events.process();
404 }
405
406 pub fn set_viewport(&mut self, width_px: u32, height_px: u32) {
408 self.sched.size = (width_px, height_px);
409 }
410
411 pub fn set_viewport_and_scale(&mut self, width_px: u32, height_px: u32, scale: f32) {
413 self.scale = scale;
414 self.sched.size = (width_px, height_px);
415 }
416
417 pub fn tick_animations(&self) {
419 repose_core::animation_driver::tick();
420 }
421
422 pub fn poll_gesture_timers(&mut self) {
423 self.poll_long_press();
424 self.flush_pending_click();
425 self.poll_key_long_press();
426 }
427
428 pub fn compose<F>(&mut self, root_fn: &mut F, render_ctx: &RenderContext) -> Frame
433 where
434 F: FnMut(&mut Scheduler, &RenderContext) -> View,
435 {
436 let _dnd_guard = self.dnd_context.enter();
437 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
438 self.poll_long_press();
439 self.flush_pending_click();
440 self.poll_key_long_press();
441
442 let size = self.sched.size;
443 let rc = render_ctx.clone();
444 let overlay = self.overlay.clone();
445 let mut compose_once = |this: &mut Self| {
446 let overlay = overlay.clone();
447 let ambient = this.overlay.clone();
448 let mut inner = |s: &mut Scheduler| {
449 repose_ui::overlay::with_ambient_overlay(ambient.clone(), || {
450 let content = (root_fn)(s, &rc);
451 overlay.host(Modifier::new().fill_max_size(), content)
452 })
453 };
454 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
455 compose_frame_inner_with_ancestors(
456 &mut this.sched,
457 &mut inner,
458 this.scale,
459 size,
460 this.hover_id,
461 &this.hover_ancestors,
462 &this.pressed_ids,
463 &this.textfield_states,
464 )
465 })) {
466 Ok(frame) => frame,
467 Err(_) => {
468 log::error!("compose panicked; presenting last good frame");
469 this.frame_cache
470 .as_deref()
471 .cloned()
472 .unwrap_or_else(|| Frame {
473 scene: Default::default(),
474 hit_regions: Vec::new(),
475 semantics_nodes: Vec::new(),
476 focus_chain: Vec::new(),
477 })
478 }
479 }
480 };
481
482 let shortcut_state = self.shortcuts.clone();
483 let frame =
484 repose_core::shortcuts::with_runtime_state(&shortcut_state, || compose_once(self));
485
486 let hover_before = self.hover_id;
490 self.reconcile_hover_from_mouse_pos(&frame);
491 if self.hover_id != hover_before {
492 self.hover_leave.clear();
495 for h in &frame.hit_regions {
496 if h.on_pointer_leave.is_some() {
497 self.hover_leave
498 .insert(h.id, repose_ui::HitRegionSnapshot::new(h));
499 }
500 }
501 return repose_core::shortcuts::with_runtime_state(&shortcut_state, || {
503 compose_once(self)
504 });
505 }
506 frame
507 }
508
509 pub fn frame(
511 &mut self,
512 mut root_fn: impl FnMut(&mut Scheduler, &RenderContext) -> View,
513 render_ctx: &RenderContext,
514 ) -> FrameOutput {
515 let (f, clipboard_text) = repose_core::clipboard::with_captured_clipboard(|| {
516 self.compose(&mut root_fn, render_ctx)
517 });
518
519 let wants_pointer = self.hover_id.is_some() || self.capture_id.is_some();
520
521 let focused_hit = self
522 .sched
523 .focused
524 .and_then(|fid| f.hit_regions.iter().find(|h| h.id == fid));
525 let ime_allowed = focused_hit.is_some_and(|hit| {
526 hit.tf_enabled
527 && !hit.tf_read_only
528 && f.semantics_nodes.iter().any(|node| {
529 node.id == hit.id && node.role == repose_core::semantics::Role::TextField
530 })
531 });
532
533 let ime_cursor_area = if ime_allowed {
534 focused_hit.map(|hit| {
535 let sf = self.scale as f64;
536 let origin = repose_ui::hit_region_transformed_origin(hit);
537 let local = repose_ui::hit_region_local_rect(hit);
538 (
539 origin.x as f64 / sf,
540 origin.y as f64 / sf,
541 local.w as f64 / sf,
542 local.h as f64 / sf,
543 )
544 })
545 } else {
546 None
547 };
548
549 let focused_is_textfield = ime_allowed
550 || self.sched.focused.is_some_and(|fid| {
551 f.hit_regions
552 .iter()
553 .any(|h| h.id == fid && h.tf_state_key.is_some())
554 });
555 let wants_keyboard = focused_is_textfield || self.ime_preedit;
556
557 let (ime_purpose, ime_auto_correct, ime_capitalization, keyboard_type) =
558 match (ime_allowed, focused_hit) {
559 (true, Some(hit)) => (
560 hit.keyboard_type.ime_purpose_hint(),
561 hit.auto_correct.unwrap_or(true),
562 hit.capitalization,
563 hit.keyboard_type,
564 ),
565 _ => (
566 repose_core::ImePurposeHint::Normal,
567 true,
568 repose_core::KeyboardCapitalization::Unspecified,
569 repose_core::KeyboardType::Unspecified,
570 ),
571 };
572
573 let platform = PlatformOutput {
574 cursor: self
575 .sched
576 .cursor_override
577 .take()
578 .or_else(|| self.take_cursor_suggestion()),
579 ime_allowed,
580 ime_cursor_area,
581 clipboard_text,
582 ime_purpose,
583 ime_auto_correct,
584 ime_capitalization,
585 keyboard_type,
586 window_theme_dark: Some(repose_core::locals::theme().is_dark()),
587 };
588 FrameOutput {
589 scene: f.scene,
590 hit_regions: f.hit_regions,
591 semantics_nodes: f.semantics_nodes,
592 focus_chain: f.focus_chain,
593 platform,
594 wants_pointer,
595 wants_keyboard,
596 }
597 }
598
599 pub fn cache_frame(&mut self, frame: impl Into<Rc<Frame>>) {
601 let frame = frame.into();
602 let _dnd_guard = self.dnd_context.enter();
603 if self.key_pressed_active.is_some_and(|id| {
604 self.sched.focused != Some(id)
605 || !frame
606 .hit_regions
607 .iter()
608 .any(|hit| hit.id == id && !hit.disabled)
609 }) {
610 self.cancel_keyboard_press();
611 }
612 self.hover_leave.clear();
613 for hit in &frame.hit_regions {
614 if hit.on_pointer_leave.is_some() {
615 self.hover_leave
616 .insert(hit.id, repose_ui::HitRegionSnapshot::new(hit));
617 }
618 }
619 self.frame_cache = Some(frame);
620 self.reconcile_cached_targets();
621 }
622
623 pub fn after_compose(&mut self, frame: &Frame, scale: f32) {
628 self.after_compose_shared(Rc::new(frame.clone()), scale);
629 }
630
631 pub fn after_compose_shared(&mut self, frame: Rc<Frame>, scale: f32) {
632 let _dnd_guard = self.dnd_context.enter();
633 ensure_all_tf_states_from_frame(&mut self.textfield_states, &frame);
634 self.prune_textfield_states(&frame);
635 self.ensure_focused_state_in_frame(&frame);
636 self.reconcile_hover_from_mouse_pos(&frame);
637 repose_core::dnd::set_shared_dnd_frame(Some(frame));
638 repose_core::dnd::set_dnd_scale(scale);
639 }
640
641 fn prune_textfield_states(&mut self, frame: &Frame) {
642 if self.textfield_states.is_empty() {
643 return;
644 }
645 let live: std::collections::HashSet<u64> = frame
646 .hit_regions
647 .iter()
648 .filter_map(|h| h.tf_state_key)
649 .collect();
650 self.textfield_states.retain(|k, _| live.contains(k));
651 }
652
653 fn end_textfield_drag(&mut self, id: u64) {
654 let key = self
655 .frame_cache
656 .as_ref()
657 .and_then(|frame| is_tf_hit(frame, id).then(|| tf_key_of(frame, id)));
658 if let Some(key) = key {
659 self.end_textfield_drag_key(key);
660 }
661 }
662
663 fn end_textfield_drag_key(&mut self, key: u64) {
664 if let Some(state) = self.textfield_states.get(&key) {
665 state.borrow_mut().end_drag();
666 }
667 }
668
669 pub fn ensure_focused_textfield_state(&mut self) {
672 let Some(f) = self.frame_cache.clone() else {
673 return;
674 };
675 self.ensure_focused_state_in_frame(&f);
676 }
677
678 fn ensure_focused_state_in_frame(&mut self, frame: &Frame) {
682 let Some(fid) = self.sched.focused else {
683 return;
684 };
685 if let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
686 && let Some(key) = hit.tf_state_key
687 {
688 let st = ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
689 st.borrow_mut().apply_controlled_value(&hit.tf_value);
690 }
691 }
692
693 pub fn cache_from_output(&mut self, out: &FrameOutput) {
697 let frame = Rc::new(Frame {
698 scene: out.scene.clone(),
699 hit_regions: out.hit_regions.clone(),
700 semantics_nodes: out.semantics_nodes.clone(),
701 focus_chain: out.focus_chain.clone(),
702 });
703 self.cache_frame(frame.clone());
704 self.after_compose_shared(frame, self.scale);
705 }
706
707 pub fn compose_frame_output<F>(
711 &mut self,
712 root: &mut F,
713 render_ctx: &RenderContext,
714 ) -> FrameOutput
715 where
716 F: FnMut(&mut Scheduler, &RenderContext) -> View,
717 {
718 self.tick_animations();
719 let out = self.frame(root, render_ctx);
720 self.cache_from_output(&out);
721 out
722 }
723
724 fn dispatch_pointer_to_targets(
725 &self,
726 kind: PointerEventKind,
727 pos: Vec2,
728 targets: &[repose_ui::HitRegionSnapshot],
729 touch: Option<u64>,
730 ) {
731 let (id, pkind) = match touch {
732 Some(tid) => (PointerId(tid), PointerKind::Touch),
733 None => (PointerId(0), PointerKind::Mouse),
734 };
735 let base = PointerEvent::new(id, pkind, kind, pos, 1.0, self.modifiers);
736 for target in targets {
737 let hit = target.hit();
738 let callback = match kind {
739 PointerEventKind::Down(_) => &hit.on_pointer_down,
740 PointerEventKind::Up(_) => &hit.on_pointer_up,
741 PointerEventKind::Move => &hit.on_pointer_move,
742 PointerEventKind::Cancel => &hit.on_pointer_cancel,
743 PointerEventKind::Enter | PointerEventKind::Leave => continue,
744 };
745 let Some(callback) = callback else {
746 continue;
747 };
748 let mut event = base.clone();
749 let (origin, local) = target.pointer_coordinates(pos);
750 event.origin = origin;
751 event.position = local;
752 callback(event);
753 if !matches!(kind, PointerEventKind::Cancel) && base.is_consumed() {
754 break;
755 }
756 }
757 }
758
759 fn dispatch_pointer_to_path(
760 &self,
761 kind: PointerEventKind,
762 pos: Vec2,
763 path: &[u64],
764 touch: Option<u64>,
765 ) {
766 let Some(frame) = &self.frame_cache else {
767 return;
768 };
769 let targets: Vec<_> = path
770 .iter()
771 .filter_map(|id| frame.hit_regions.iter().find(|hit| hit.id == *id))
772 .map(repose_ui::HitRegionSnapshot::new)
773 .collect();
774 self.dispatch_pointer_to_targets(kind, pos, &targets, touch);
775 }
776
777 fn refreshed_capture_targets(
778 &self,
779 targets: &[repose_ui::HitRegionSnapshot],
780 ) -> (
781 Vec<repose_ui::HitRegionSnapshot>,
782 Vec<repose_ui::HitRegionSnapshot>,
783 ) {
784 let Some(frame) = &self.frame_cache else {
785 return (targets.to_vec(), Vec::new());
786 };
787 let mut live = Vec::with_capacity(targets.len());
788 let mut stale = Vec::new();
789 for target in targets {
790 if let Some(hit) = frame
791 .hit_regions
792 .iter()
793 .find(|hit| hit.id == target.id() && !hit.disabled)
794 {
795 live.push(repose_ui::HitRegionSnapshot::new(hit));
796 } else {
797 stale.push(target.clone());
798 }
799 }
800 (live, stale)
801 }
802
803 fn cancel_dnd_capture(&mut self, touch: Option<u64>, source_id: u64) -> bool {
804 if !self
805 .dnd_capture
806 .as_ref()
807 .is_some_and(|capture| capture.touch == touch && capture.source_id == source_id)
808 {
809 return false;
810 }
811 self.dnd_capture = None;
812 dnd::handle_drag_action(&DragAction::Cancel)
813 }
814
815 fn cancel_active_dnd(&mut self) -> bool {
816 if self.dnd_capture.take().is_none() {
817 return false;
818 }
819 dnd::handle_drag_action(&DragAction::Cancel)
820 }
821
822 fn reconcile_cached_targets(&mut self) {
823 let mouse_pos = Vec2 {
824 x: self.mouse_pos_px.0,
825 y: self.mouse_pos_px.1,
826 };
827 if let Some(targets) = self.mouse_targets.take() {
828 let capture_id = targets.first().map(|target| target.id());
829 let capture_key = targets.first().and_then(|target| target.hit().tf_state_key);
830 let (live, stale) = self.refreshed_capture_targets(&targets);
831 if !stale.is_empty() {
832 self.dispatch_pointer_to_targets(PointerEventKind::Cancel, mouse_pos, &stale, None);
833 }
834 let capture_stale =
835 capture_id.is_some_and(|id| stale.iter().any(|target| target.id() == id));
836 if capture_stale {
837 self.pressed_ids.remove(&capture_id.unwrap_or_default());
838 self.capture_id = None;
839 self.hit_path = None;
840 if let Some(key) = capture_key {
841 self.end_textfield_drag_key(key);
842 }
843 if self
844 .long_press
845 .is_some_and(|(id, _, _, _)| Some(id) == capture_id)
846 {
847 self.long_press = None;
848 self.long_press_touch = None;
849 }
850 self.cancel_dnd_capture(None, capture_id.unwrap_or_default());
851 } else {
852 self.mouse_targets = (!live.is_empty()).then_some(live);
853 self.hit_path = self
854 .mouse_targets
855 .as_ref()
856 .map(|targets| targets.iter().map(|target| target.id()).collect());
857 }
858 if !stale.is_empty() {
859 request_frame();
860 }
861 }
862
863 let mut touch_ids: Vec<u64> = self.touch_targets.keys().copied().collect();
864 touch_ids.sort_unstable();
865 let mut changed = false;
866 for tid in touch_ids {
867 let Some(targets) = self.touch_targets.remove(&tid) else {
868 continue;
869 };
870 let pos = self.touch_positions.get(&tid).copied().unwrap_or(mouse_pos);
871 let old_capture = self.touch_presses.get(&tid).map(|state| state.capture_id);
872 let old_capture_key = targets.first().and_then(|target| target.hit().tf_state_key);
873 let (live, stale) = self.refreshed_capture_targets(&targets);
874 if !stale.is_empty() {
875 self.dispatch_pointer_to_targets(PointerEventKind::Cancel, pos, &stale, Some(tid));
876 changed = true;
877 }
878 let capture_stale =
879 old_capture.is_some_and(|id| stale.iter().any(|target| target.id() == id));
880 if capture_stale {
881 self.touch_targets.remove(&tid);
882 self.touch_paths.remove(&tid);
883 self.touch_presses.remove(&tid);
884 self.suppressed_touch_clicks.remove(&tid);
885 if let Some(key) = old_capture_key {
886 self.end_textfield_drag_key(key);
887 }
888 if self.long_press_touch == Some(tid)
889 && old_capture.is_some_and(|id| {
890 self.long_press
891 .is_some_and(|(long_id, _, _, _)| long_id == id)
892 })
893 {
894 self.long_press = None;
895 self.long_press_touch = None;
896 }
897 if self.touch_primary == Some(tid) {
898 self.touch_primary = None;
899 self.double_candidate = None;
900 self.pending_click = None;
901 self.last_up = None;
902 self.last_down = None;
903 self.press_origin = None;
904 self.suppress_next_click = false;
905 self.scroll_capture_id = None;
906 if let Some(source_id) = old_capture {
907 self.cancel_dnd_capture(Some(tid), source_id);
908 }
909 }
910 } else if live.is_empty() {
911 self.touch_paths.remove(&tid);
912 self.touch_presses.remove(&tid);
913 self.suppressed_touch_clicks.remove(&tid);
914 } else {
915 self.touch_targets.insert(tid, live.clone());
916 self.touch_paths
917 .insert(tid, live.iter().map(|target| target.id()).collect());
918 }
919 }
920 if changed {
921 self.rebuild_pressed_ids();
922 request_frame();
923 }
924 }
925
926 pub fn handle_pointer_move(&mut self, pos: Vec2) -> PointerMoveResult {
928 self.handle_touch_move(None, pos)
929 }
930
931 pub fn handle_touch_move(&mut self, touch: Option<u64>, pos: Vec2) -> PointerMoveResult {
934 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
935 let _dnd_guard = self.dnd_context.enter();
936 self.mouse_pos_px = (pos.x, pos.y);
937 if let Some(tid) = touch {
938 self.touch_positions.insert(tid, pos);
939 }
940 self.pointer_inside = true;
941 if touch.is_none() {
942 self.sched.pointer_pos_px = Some((pos.x, pos.y));
943 }
944
945 let owns_dnd_move = self
946 .dnd_capture
947 .as_ref()
948 .is_some_and(|capture| capture.touch == touch);
949 let dnd_move_consumed = owns_dnd_move
950 && dnd::handle_drag_action(&DragAction::Move {
951 position: pos,
952 modifiers: self.modifiers,
953 });
954 if dnd_move_consumed {
955 self.cancel_keyboard_press();
956 request_frame();
957 }
958
959 let Some(f) = &self.frame_cache else {
960 return PointerMoveResult {
961 cursor: None,
962 hover_id: None,
963 };
964 };
965
966 let active_long_press_matches = match (touch, self.long_press_touch) {
967 (Some(tid), Some(active)) => tid == active,
968 (None, None) => true,
969 _ => false,
970 };
971 if active_long_press_matches && let Some((_, _, x0, y0)) = self.long_press {
972 let slop = LONG_PRESS_SLOP_DP * self.scale;
973 let dx = pos.x - x0;
974 let dy = pos.y - y0;
975 if dx * dx + dy * dy > slop * slop {
976 self.long_press = None;
977 self.long_press_touch = None;
978 }
979 }
980
981 if active_long_press_matches
983 && self.long_press.is_some()
984 && let Some(lid) = self.long_press.map(|(id, _, _, _)| id)
985 && f.hit_regions
986 .iter()
987 .find(|h| h.id == lid)
988 .is_none_or(|h| !repose_ui::hit_region_contains(h, pos))
989 {
990 self.long_press = None;
991 self.long_press_touch = None;
992 }
993
994 let text_capture = match touch {
995 Some(tid) => self.touch_presses.get(&tid).map(|state| state.capture_id),
996 None => self.capture_id,
997 };
998 let owns_text_drag = touch.is_none() || self.touch_primary == touch;
999 if owns_text_drag
1000 && let Some(cid) = text_capture
1001 && is_tf_hit(f, cid)
1002 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1003 {
1004 let key = tf_key_of(f, cid);
1005 if let Some(st_rc) = self.textfield_states.get(&key) {
1006 let mut st = st_rc.borrow_mut();
1007 let local = repose_ui::hit_region_to_local(hit, pos);
1008 let local_rect = repose_ui::hit_region_local_rect(hit);
1009 let content_origin = hit
1010 .tf_content_origin
1011 .map(|(x, y)| repose_ui::hit_region_to_local(hit, Vec2 { x, y }))
1012 .unwrap_or(Vec2 {
1013 x: local_rect.x,
1014 y: local_rect.y,
1015 });
1016 let content_x = (local.x - content_origin.x + st.scroll_offset).max(0.0);
1017 let content_y = (local.y - content_origin.y + st.scroll_offset_y).max(0.0);
1018 let metrics = repose_ui::textfield::textfield_metrics(hit);
1019 let wrap_w = st.inner_width.max(1.0);
1020 let idx = if hit.tf_multiline {
1021 index_for_xy_bytes_vt(&st, &metrics, wrap_w, content_x, content_y)
1022 } else {
1023 index_for_x_bytes_vt(&st, &metrics, content_x)
1024 };
1025 st.drag_to(idx);
1026 }
1027 }
1028
1029 let top = repose_ui::hit_test_enabled_frame(f, pos);
1030
1031 self.cursor = top
1032 .and_then(|h| h.cursor.clone())
1033 .or(Some(CursorIcon::Default));
1034 if dnd_move_consumed && dnd::is_dragging() {
1035 self.cursor = Some(CursorIcon::Grabbing);
1036 }
1037
1038 let new_hover = top.map(|h| h.id);
1039
1040 let old_chain = hover_chain_for(Some(f), self.hover_id);
1041 let new_chain = hover_chain_for(Some(f), new_hover);
1042 if new_chain != old_chain {
1043 dispatch_hover_change_bubbled(
1044 Some(f),
1045 &self.hover_leave,
1046 &mut self.hover_id,
1047 &mut self.hover_ancestors,
1048 new_hover,
1049 pos,
1050 self.modifiers,
1051 );
1052 request_frame();
1053 }
1054
1055 if let Some(tid) = touch {
1056 if let Some(targets) = self.touch_targets.get(&tid) {
1057 self.dispatch_pointer_to_targets(PointerEventKind::Move, pos, targets, Some(tid));
1058 } else if let Some(path) = self.touch_paths.get(&tid).cloned() {
1059 self.dispatch_pointer_to_path(PointerEventKind::Move, pos, &path, Some(tid));
1060 }
1061 return PointerMoveResult {
1062 cursor: self.cursor.clone(),
1063 hover_id: self.hover_id,
1064 };
1065 }
1066
1067 if let Some(targets) = &self.mouse_targets {
1068 self.dispatch_pointer_to_targets(PointerEventKind::Move, pos, targets, None);
1069 } else if let Some(path) = self.hit_path.clone() {
1070 self.dispatch_pointer_to_path(PointerEventKind::Move, pos, &path, None);
1071 }
1072 if self.hit_path.is_none()
1073 && let Some(h) = top
1074 && let Some(cb) = &h.on_pointer_move
1075 {
1076 let (id, kind) = match touch {
1077 Some(tid) => (PointerId(tid), PointerKind::Touch),
1078 None => (PointerId(0), PointerKind::Mouse),
1079 };
1080 let mut pe =
1081 PointerEvent::new(id, kind, PointerEventKind::Move, pos, 1.0, self.modifiers);
1082 let (origin, local) = repose_ui::hit_region_pointer_coordinates(h, pos);
1083 pe.origin = origin;
1084 pe.position = local;
1085 cb(pe);
1086 }
1087
1088 PointerMoveResult {
1089 cursor: self.cursor.clone(),
1090 hover_id: self.hover_id,
1091 }
1092 }
1093
1094 pub fn handle_pointer_press(
1096 &mut self,
1097 pos: Vec2,
1098 button: PointerButton,
1099 ) -> PointerButtonResult {
1100 self.handle_touch_press(None, pos, button)
1101 }
1102
1103 pub fn handle_touch_press(
1108 &mut self,
1109 touch: Option<u64>,
1110 pos: Vec2,
1111 button: PointerButton,
1112 ) -> PointerButtonResult {
1113 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
1114 let _dnd_guard = self.dnd_context.enter();
1115 self.mouse_pos_px = (pos.x, pos.y);
1116 if let Some(tid) = touch {
1117 self.touch_positions.insert(tid, pos);
1118 }
1119 if touch.is_none() {
1120 self.sched.pointer_pos_px = Some((pos.x, pos.y));
1121 }
1122 if touch.is_none() {
1123 self.held_mouse.insert(button);
1124 }
1125 let _ = repose_core::request_input_mode(repose_core::InputMode::Touch);
1126
1127 if touch.is_none() && (self.capture_id.is_some() || self.hit_path.is_some()) {
1128 self.handle_pointer_cancel();
1129 }
1130 if let Some(tid) = touch
1131 && self.touch_presses.contains_key(&tid)
1132 {
1133 self.handle_touch_cancel(Some(tid));
1134 }
1135 if let Some(tid) = touch {
1136 self.touch_positions.insert(tid, pos);
1137 }
1138
1139 let Some(frame) = self.frame_cache.clone() else {
1140 return PointerButtonResult {
1141 focused: None,
1142 capture_id: None,
1143 consumed: false,
1144 needs_a11y_announce: false,
1145 clicked_id: None,
1146 };
1147 };
1148 let f = &frame;
1149
1150 let mut result = PointerButtonResult {
1151 focused: None,
1152 capture_id: None,
1153 consumed: false,
1154 needs_a11y_announce: false,
1155 clicked_id: None,
1156 };
1157 let primary_pointer = match touch {
1158 Some(tid) => {
1159 if self.touch_primary.is_none() {
1160 self.touch_primary = Some(tid);
1161 }
1162 self.touch_primary == Some(tid)
1163 }
1164 None => true,
1165 };
1166
1167 if primary_pointer {
1168 self.cancel_keyboard_press();
1169 }
1170
1171 let path = repose_ui::hit_test_frame_path(f, pos);
1172 if let Some(&capture_id) = path.first()
1173 && let Some(hit) = f.hit_regions.iter().find(|hit| hit.id == capture_id)
1174 {
1175 if touch.is_none() {
1176 self.hit_path = Some(path.clone());
1177 }
1178
1179 if touch.is_none() || primary_pointer {
1180 dnd::handle_drag_action(&DragAction::Press {
1181 position: pos,
1182 capture_id: hit.id,
1183 kind: match touch {
1184 Some(_) => PointerKind::Touch,
1185 None => PointerKind::Mouse,
1186 },
1187 modifiers: self.modifiers,
1188 });
1189 self.dnd_capture = Some(DndPointerCapture {
1190 touch,
1191 source_id: hit.id,
1192 });
1193 }
1194
1195 if touch.is_none() {
1196 self.capture_id = Some(hit.id);
1197 }
1198 result.capture_id = Some(hit.id);
1199 result.consumed = true;
1200 let targets: Vec<_> = path
1201 .iter()
1202 .filter_map(|id| f.hit_regions.iter().find(|hit| hit.id == *id))
1203 .map(repose_ui::HitRegionSnapshot::new)
1204 .collect();
1205 if let Some(tid) = touch {
1206 self.touch_paths.insert(tid, path.clone());
1207 self.touch_targets.insert(tid, targets);
1208 self.touch_presses.insert(
1209 tid,
1210 TouchPressState {
1211 capture_id: hit.id,
1212 pressed_ids: HashSet::from([hit.id]),
1213 },
1214 );
1215 } else {
1216 self.mouse_targets = Some(targets);
1217 }
1218
1219 if primary_pointer {
1223 self.last_down = Some((hit.id, web_time::Instant::now()));
1224 self.press_origin = Some((hit.id, pos.x, pos.y));
1225 self.double_candidate = if hit.on_double_click.is_some()
1230 && self.last_up.is_some_and(|(pid, t0, ux, uy)| {
1231 pid == hit.id
1232 && !moved_past_slop(ux, uy, pos, TAP_SLOP_DP, self.scale)
1233 && self.last_down.is_some_and(|(did, dt)| {
1234 did == hit.id
1235 && dt.duration_since(t0).as_millis() >= DOUBLE_TAP_MIN_MS
1236 && dt.duration_since(t0).as_millis() <= DOUBLE_CLICK_MS
1237 })
1238 }) {
1239 Some(hit.id)
1240 } else {
1241 None
1242 };
1243 if self.double_candidate.is_some() {
1244 self.pending_click = None;
1246 } else if self
1247 .pending_click
1248 .as_ref()
1249 .is_some_and(|(pid, ..)| *pid == hit.id)
1250 {
1251 if let Some((_, _, cb)) = self.pending_click.take() {
1256 cb();
1257 }
1258 }
1259
1260 if let PointerButton::Primary = button {
1261 self.long_press = if hit.on_long_click.is_some() {
1262 Some((hit.id, web_time::Instant::now(), pos.x, pos.y))
1263 } else {
1264 None
1265 };
1266 self.long_press_touch = touch;
1267 self.suppress_next_click = false;
1268 }
1269 }
1270
1271 if primary_pointer
1272 && hit.tf_enabled
1273 && (hit.tf_state_key.is_some() || is_textfield_in_frame(f, hit.id))
1274 {
1275 let key = tf_key_of(f, hit.id);
1276 let seed = hit.tf_value.as_str();
1277 let st_rc = ensure_tf_state(&mut self.textfield_states, key, seed);
1278 {
1279 let mut st = st_rc.borrow_mut();
1280 st.apply_controlled_value(seed);
1282
1283 if st.inner_width <= 0.0 {
1284 let local_rect = repose_ui::hit_region_local_rect(hit);
1285 st.set_inner_width(local_rect.w.max(1.0));
1286 st.set_inner_height(local_rect.h.max(1.0));
1287 }
1288
1289 let local = repose_ui::hit_region_to_local(hit, pos);
1290 let local_rect = repose_ui::hit_region_local_rect(hit);
1291 let content_origin = hit
1292 .tf_content_origin
1293 .map(|(x, y)| repose_ui::hit_region_to_local(hit, Vec2 { x, y }))
1294 .unwrap_or(Vec2 {
1295 x: local_rect.x,
1296 y: local_rect.y,
1297 });
1298 let content_x = (local.x - content_origin.x + st.scroll_offset).max(0.0);
1299 let content_y = (local.y - content_origin.y + st.scroll_offset_y).max(0.0);
1300 let metrics = repose_ui::textfield::textfield_metrics(hit);
1301 let wrap_w = st.inner_width.max(1.0);
1302
1303 let idx = if hit.tf_multiline {
1304 index_for_xy_bytes_vt(&st, &metrics, wrap_w, content_x, content_y)
1305 } else {
1306 index_for_x_bytes_vt(&st, &metrics, content_x)
1307 };
1308 st.handle_pointer_down(idx, (pos.x, pos.y), self.modifiers.shift);
1309 }
1311 }
1312
1313 self.pressed_ids.insert(hit.id);
1314
1315 if primary_pointer && hit.focusable {
1316 if self.sched.focused != Some(hit.id) {
1317 self.finish_compositions();
1318 }
1319 self.sched.focused = Some(hit.id);
1320 result.focused = Some(hit.id);
1321 if hit.tf_state_key.is_some() {
1322 let key = tf_key_of(f, hit.id);
1323 let st =
1324 ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
1325 let mut s = st.borrow_mut();
1326 s.apply_controlled_value(&hit.tf_value);
1327 s.reset_caret_blink();
1328 }
1329 }
1330
1331 let targets = if touch.is_some() {
1332 self.touch_targets.get(&touch.unwrap()).cloned()
1333 } else {
1334 self.mouse_targets.clone()
1335 };
1336 if let Some(targets) = targets {
1337 self.dispatch_pointer_to_targets(
1338 PointerEventKind::Down(button),
1339 pos,
1340 &targets,
1341 touch,
1342 );
1343 }
1344
1345 request_frame();
1346 } else if primary_pointer {
1347 if touch.is_none() {
1348 self.hit_path = None;
1349 self.mouse_targets = None;
1350 self.capture_id = None;
1351 self.pressed_ids.clear();
1352 } else if let Some(tid) = touch {
1353 self.touch_paths.remove(&tid);
1354 self.touch_targets.remove(&tid);
1355 self.touch_positions.remove(&tid);
1356 }
1357 self.finish_compositions();
1358 self.sched.focused = None;
1359 request_frame();
1360 }
1361
1362 result
1363 }
1364
1365 pub fn handle_pointer_release(
1367 &mut self,
1368 pos: Vec2,
1369 button: PointerButton,
1370 ) -> PointerButtonResult {
1371 self.handle_touch_release(None, pos, button)
1372 }
1373
1374 pub fn handle_touch_release(
1377 &mut self,
1378 touch: Option<u64>,
1379 pos: Vec2,
1380 button: PointerButton,
1381 ) -> PointerButtonResult {
1382 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
1383 let _dnd_guard = self.dnd_context.enter();
1384 self.mouse_pos_px = (pos.x, pos.y);
1385 if let Some(tid) = touch {
1386 self.touch_positions.insert(tid, pos);
1387 } else {
1388 self.sched.pointer_pos_px = Some((pos.x, pos.y));
1389 self.held_mouse.remove(&button);
1390 }
1391 let mut result = PointerButtonResult {
1392 focused: self.sched.focused,
1393 capture_id: self.capture_id,
1394 consumed: false,
1395 needs_a11y_announce: false,
1396 clicked_id: None,
1397 };
1398 if let Some(tid) = touch {
1399 return self.finish_touch_release(tid, pos, button, result);
1400 }
1401
1402 let owns_dnd_release = self.dnd_capture.as_ref().is_some_and(|capture| {
1403 capture.touch.is_none() && Some(capture.source_id) == self.capture_id
1404 });
1405 let drag_consumed = if owns_dnd_release {
1406 self.dnd_capture = None;
1407 dnd::handle_drag_action(&DragAction::Release {
1408 position: pos,
1409 modifiers: self.modifiers,
1410 })
1411 } else {
1412 false
1413 };
1414 if let Some(targets) = &self.mouse_targets {
1415 self.dispatch_pointer_to_targets(PointerEventKind::Up(button), pos, targets, None);
1416 result.consumed = true;
1417 } else if let Some(path) = &self.hit_path {
1418 self.dispatch_pointer_to_path(PointerEventKind::Up(button), pos, path, None);
1419 result.consumed = true;
1420 }
1421 if let Some(cid) = self.capture_id {
1422 self.pressed_ids.remove(&cid);
1423 self.end_textfield_drag(cid);
1424 }
1425 if drag_consumed {
1426 self.capture_id = None;
1427 self.hit_path = None;
1428 self.mouse_targets = None;
1429 request_frame();
1430 result.consumed = true;
1431 return result;
1432 }
1433
1434 let f = match &self.frame_cache {
1435 Some(f) => f.clone(),
1436 None => {
1437 self.capture_id = None;
1438 self.hit_path = None;
1439 self.mouse_targets = None;
1440 self.pressed_ids.clear();
1441 return result;
1442 }
1443 };
1444
1445 if self.long_press_touch.is_none()
1447 && let Some((lid, t0, _, _)) = self.long_press.take()
1448 && Some(lid) == self.capture_id
1449 && t0.elapsed().as_millis() >= LONG_PRESS_MS
1450 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == lid && !h.disabled)
1451 && let Some(cb) = &hit.on_long_click
1452 {
1453 cb();
1454 self.suppress_next_click = true;
1455 self.pending_click = None;
1456 result.clicked_id = Some(lid);
1457 result.needs_a11y_announce = true;
1458 result.consumed = true;
1459 }
1460
1461 let dragged = self.drag_cancelled_tap(self.capture_id, pos);
1466 if dragged {
1467 self.pending_click = None;
1468 }
1469
1470 if self.double_candidate.is_none()
1471 && !dragged
1472 && !self.suppress_next_click
1473 && let Some(cid) = self.capture_id
1474 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid && !h.disabled)
1475 && repose_ui::hit_region_contains(hit, pos)
1476 {
1477 let now = web_time::Instant::now();
1478 if hit.on_double_click.is_some() {
1481 if let Some(cb) = hit.on_click.clone() {
1482 self.pending_click = Some((cid, now, cb));
1483 }
1484 self.last_up = Some((cid, now, pos.x, pos.y));
1485 result.consumed = true;
1486 request_frame(); } else {
1488 if let Some(cb) = &hit.on_click {
1489 cb();
1490 }
1491 self.last_up = Some((cid, now, pos.x, pos.y));
1492 result.clicked_id = Some(cid);
1493 result.needs_a11y_announce = true;
1494 result.consumed = true;
1495 }
1496 }
1497 self.suppress_next_click = false;
1498
1499 if let Some(dc) = self.double_candidate.take() {
1504 self.pending_click = None;
1505 self.last_up = None;
1506 self.last_down = None;
1507 if dragged {
1508 } else if self.capture_id == Some(dc)
1511 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == dc && !h.disabled)
1512 && repose_ui::hit_region_contains(hit, pos)
1513 {
1514 if let Some(cb) = &hit.on_double_click {
1515 cb();
1516 }
1517 result.clicked_id = Some(dc);
1518 result.needs_a11y_announce = true;
1519 result.consumed = true;
1520 } else if self.capture_id == Some(dc)
1521 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == dc && !h.disabled)
1522 {
1523 if let Some(cb) = &hit.on_click {
1525 cb();
1526 }
1527 result.clicked_id = Some(dc);
1528 result.needs_a11y_announce = true;
1529 result.consumed = true;
1530 }
1531 }
1532
1533 self.capture_id = None;
1534 self.hit_path = None;
1535 self.mouse_targets = None;
1536 self.press_origin = None;
1537 request_frame();
1538 result
1539 }
1540
1541 fn finish_touch_release(
1542 &mut self,
1543 tid: u64,
1544 pos: Vec2,
1545 button: PointerButton,
1546 mut result: PointerButtonResult,
1547 ) -> PointerButtonResult {
1548 let capture_id = self
1549 .touch_presses
1550 .remove(&tid)
1551 .map(|state| state.capture_id);
1552 result.capture_id = capture_id;
1553 let targets = self.touch_targets.remove(&tid);
1554 if let Some(targets) = &targets {
1555 self.dispatch_pointer_to_targets(PointerEventKind::Up(button), pos, targets, Some(tid));
1556 result.consumed = true;
1557 } else if let Some(path) = self.touch_paths.remove(&tid) {
1558 self.dispatch_pointer_to_path(PointerEventKind::Up(button), pos, &path, Some(tid));
1559 result.consumed = true;
1560 }
1561 self.touch_paths.remove(&tid);
1562 self.touch_positions.remove(&tid);
1563 let primary = self.touch_primary == Some(tid);
1564 if let Some(cid) = capture_id {
1565 self.end_textfield_drag(cid);
1566 }
1567 let owns_dnd_release = self.dnd_capture.as_ref().is_some_and(|capture| {
1568 capture.touch == Some(tid) && Some(capture.source_id) == capture_id
1569 });
1570 let drag_consumed = if owns_dnd_release {
1571 self.dnd_capture = None;
1572 dnd::handle_drag_action(&DragAction::Release {
1573 position: pos,
1574 modifiers: self.modifiers,
1575 })
1576 } else {
1577 false
1578 };
1579 let suppressed = self.suppressed_touch_clicks.remove(&tid);
1580 if primary
1581 && !drag_consumed
1582 && !suppressed
1583 && let Some(frame) = self.frame_cache.clone()
1584 && let Some(cid) = capture_id
1585 {
1586 if self.long_press_touch == Some(tid)
1587 && let Some((lid, t0, _, _)) = self.long_press.take()
1588 && lid == cid
1589 && t0.elapsed().as_millis() >= LONG_PRESS_MS
1590 && let Some(hit) = frame
1591 .hit_regions
1592 .iter()
1593 .find(|hit| hit.id == lid && !hit.disabled)
1594 && let Some(callback) = &hit.on_long_click
1595 {
1596 callback();
1597 self.suppress_next_click = true;
1598 self.pending_click = None;
1599 result.clicked_id = Some(lid);
1600 result.needs_a11y_announce = true;
1601 result.consumed = true;
1602 }
1603
1604 let dragged = self.drag_cancelled_tap(capture_id, pos);
1606 if dragged {
1607 self.pending_click = None;
1608 }
1609
1610 if self.double_candidate.is_none()
1611 && !dragged
1612 && !self.suppress_next_click
1613 && let Some(hit) = frame
1614 .hit_regions
1615 .iter()
1616 .find(|hit| hit.id == cid && !hit.disabled)
1617 && repose_ui::hit_region_contains(hit, pos)
1618 {
1619 let now = web_time::Instant::now();
1620 if hit.on_double_click.is_some() {
1621 if let Some(callback) = hit.on_click.clone() {
1622 self.pending_click = Some((cid, now, callback));
1623 }
1624 self.last_up = Some((cid, now, pos.x, pos.y));
1625 result.consumed = true;
1626 request_frame();
1627 } else {
1628 if let Some(callback) = &hit.on_click {
1629 callback();
1630 }
1631 self.last_up = Some((cid, now, pos.x, pos.y));
1632 result.clicked_id = Some(cid);
1633 result.needs_a11y_announce = true;
1634 result.consumed = true;
1635 }
1636 }
1637 self.suppress_next_click = false;
1638
1639 if let Some(double_id) = self.double_candidate.take() {
1640 self.pending_click = None;
1641 self.last_up = None;
1642 self.last_down = None;
1643 if let Some(hit) = frame
1644 .hit_regions
1645 .iter()
1646 .find(|hit| hit.id == double_id && !hit.disabled)
1647 && !dragged
1648 {
1649 if repose_ui::hit_region_contains(hit, pos) {
1650 if let Some(callback) = &hit.on_double_click {
1651 callback();
1652 }
1653 } else if let Some(callback) = &hit.on_click {
1654 callback();
1655 }
1656 result.clicked_id = Some(double_id);
1657 result.needs_a11y_announce = true;
1658 result.consumed = true;
1659 }
1660 }
1661 }
1662 if self.long_press_touch == Some(tid) {
1663 self.long_press = None;
1664 self.long_press_touch = None;
1665 }
1666 if primary {
1667 self.press_origin = None;
1668 }
1669 self.rebuild_pressed_ids();
1670 if primary {
1671 self.touch_primary = None;
1672 }
1673 request_frame();
1674 result
1675 }
1676
1677 fn drag_cancelled_tap(&self, capture_id: Option<u64>, pos: Vec2) -> bool {
1681 capture_id.is_some_and(|cid| {
1682 self.press_origin.is_some_and(|(oid, x0, y0)| {
1683 oid == cid && moved_past_slop(x0, y0, pos, TAP_SLOP_DP, self.scale)
1684 })
1685 })
1686 }
1687
1688 fn cancel_keyboard_press(&mut self) {
1689 let active = self.key_pressed_active.take();
1690 self.key_pressed_key = None;
1691 self.key_long_press = None;
1692 let Some(active) = active else {
1693 return;
1694 };
1695 self.pressed_ids.remove(&active);
1696 let hit = self
1697 .frame_cache
1698 .as_ref()
1699 .and_then(|frame| frame.hit_regions.iter().find(|hit| hit.id == active))
1700 .cloned();
1701 if let Some(hit) = hit
1702 && let Some(source) = &hit.interaction_source
1703 {
1704 let press_id = source.collect_last_press_id().unwrap_or(0);
1705 source.to_mutable().emit(Interaction::Cancel(press_id));
1706 }
1707 }
1708
1709 fn rebuild_pressed_ids(&mut self) {
1710 self.pressed_ids = self
1711 .touch_presses
1712 .values()
1713 .flat_map(|state| state.pressed_ids.iter().copied())
1714 .collect();
1715 if let Some(active) = self.key_pressed_active {
1716 self.pressed_ids.insert(active);
1717 }
1718 }
1719
1720 pub fn suppress_touch_click(&mut self, tid: u64) {
1721 self.suppressed_touch_clicks.insert(tid);
1722 if self.touch_primary == Some(tid) {
1723 self.suppress_next_click = true;
1724 self.long_press = None;
1725 self.pending_click = None;
1726 self.double_candidate = None;
1727 }
1728 }
1729
1730 pub fn touch_owns_drag(&self, tid: u64) -> bool {
1738 if dnd::is_dragging() {
1739 return true;
1740 }
1741 let Some(capture_id) = self.touch_presses.get(&tid).map(|state| state.capture_id) else {
1742 return false;
1743 };
1744 self.frame_cache.as_ref().is_some_and(|frame| {
1745 frame
1746 .hit_regions
1747 .iter()
1748 .find(|hit| hit.id == capture_id)
1749 .is_some_and(|hit| hit.on_pointer_move.is_some())
1750 })
1751 }
1752
1753 pub fn handle_touch_cancel(&mut self, touch: Option<u64>) {
1759 if let Some(tid) = touch {
1760 let pos = self.touch_positions.get(&tid).copied().unwrap_or(Vec2 {
1761 x: self.mouse_pos_px.0,
1762 y: self.mouse_pos_px.1,
1763 });
1764 self.handle_touch_cancel_at(tid, pos);
1765 } else {
1766 self.handle_pointer_cancel();
1767 }
1768 }
1769
1770 pub fn handle_touch_cancel_at(&mut self, tid: u64, pos: Vec2) {
1771 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
1772 let _dnd_guard = self.dnd_context.enter();
1773 let was_primary = self.touch_primary == Some(tid);
1774 let capture_id = self
1775 .touch_presses
1776 .remove(&tid)
1777 .map(|state| state.capture_id);
1778 let targets = self.touch_targets.remove(&tid);
1779 if let Some(targets) = &targets {
1780 self.dispatch_pointer_to_targets(PointerEventKind::Cancel, pos, targets, Some(tid));
1781 } else if let Some(path) = self.touch_paths.remove(&tid) {
1782 self.dispatch_pointer_to_path(PointerEventKind::Cancel, pos, &path, Some(tid));
1783 }
1784 self.touch_paths.remove(&tid);
1785 self.touch_positions.remove(&tid);
1786 if let Some(cid) = capture_id {
1787 self.end_textfield_drag(cid);
1788 }
1789 self.suppressed_touch_clicks.remove(&tid);
1790 if was_primary {
1791 self.touch_primary = None;
1792 self.double_candidate = None;
1793 self.pending_click = None;
1794 self.last_up = None;
1795 self.last_down = None;
1796 self.suppress_next_click = false;
1797 self.scroll_capture_id = None;
1798 }
1799 if self.long_press_touch == Some(tid) {
1800 self.long_press = None;
1801 self.long_press_touch = None;
1802 }
1803 self.rebuild_pressed_ids();
1804 if let Some(source_id) = capture_id {
1805 self.cancel_dnd_capture(Some(tid), source_id);
1806 }
1807 request_frame();
1808 }
1809
1810 pub fn handle_pointer_cancel(&mut self) {
1811 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
1812 let _dnd_guard = self.dnd_context.enter();
1813 self.sched.pointer_pos_px = None;
1814 self.held_mouse.clear();
1815 self.sched.mouse_primary = false;
1816 self.sched.mouse_secondary = false;
1817 self.sched.mouse_middle = false;
1818 self.scroll_capture_id = None;
1819 self.last_scroll_at = None;
1820 if let Some(cid) = self.capture_id {
1821 self.end_textfield_drag(cid);
1822 }
1823 let touch_targets = std::mem::take(&mut self.touch_targets);
1824 let touch_paths = std::mem::take(&mut self.touch_paths);
1825 let mut touch_ids: Vec<u64> = touch_targets
1826 .keys()
1827 .chain(touch_paths.keys())
1828 .copied()
1829 .collect();
1830 touch_ids.sort_unstable();
1831 touch_ids.dedup();
1832 for tid in touch_ids {
1833 let pos = self.touch_positions.remove(&tid).unwrap_or(Vec2 {
1834 x: self.mouse_pos_px.0,
1835 y: self.mouse_pos_px.1,
1836 });
1837 if let Some(targets) = touch_targets.get(&tid) {
1838 self.dispatch_pointer_to_targets(PointerEventKind::Cancel, pos, targets, Some(tid));
1839 } else if let Some(path) = touch_paths.get(&tid) {
1840 self.dispatch_pointer_to_path(PointerEventKind::Cancel, pos, path, Some(tid));
1841 }
1842 }
1843 let touch_presses = std::mem::take(&mut self.touch_presses);
1844 for (tid, state) in &touch_presses {
1845 let source_id = state.capture_id;
1846 self.cancel_dnd_capture(Some(*tid), source_id);
1847 self.end_textfield_drag(source_id);
1848 }
1849 self.touch_primary = None;
1850 self.suppressed_touch_clicks.clear();
1851 self.cancel_keyboard_press();
1852 self.long_press = None;
1853 self.long_press_touch = None;
1854 self.last_up = None;
1855 self.last_down = None;
1856 self.press_origin = None;
1857 self.double_candidate = None;
1858 self.suppress_next_click = false;
1859 if self.dnd_capture.is_some() {
1860 self.cancel_active_dnd();
1861 }
1862 let pos = Vec2 {
1863 x: self.mouse_pos_px.0,
1864 y: self.mouse_pos_px.1,
1865 };
1866 dispatch_hover_change_bubbled(
1867 self.frame_cache.as_deref(),
1868 &self.hover_leave,
1869 &mut self.hover_id,
1870 &mut self.hover_ancestors,
1871 None,
1872 pos,
1873 self.modifiers,
1874 );
1875 if let Some(targets) = &self.mouse_targets {
1876 self.dispatch_pointer_to_targets(PointerEventKind::Cancel, pos, targets, None);
1877 } else if let Some(path) = &self.hit_path {
1878 self.dispatch_pointer_to_path(PointerEventKind::Cancel, pos, path, None);
1879 }
1880 self.reset_pointer_state();
1881 request_frame();
1882 }
1883
1884 pub fn clear_hover(&mut self) {
1886 if self.hover_id.is_none() && self.hover_ancestors.is_empty() {
1887 return;
1888 }
1889 let pos = Vec2 {
1890 x: self.mouse_pos_px.0,
1891 y: self.mouse_pos_px.1,
1892 };
1893 dispatch_hover_change_bubbled(
1894 self.frame_cache.as_deref(),
1895 &self.hover_leave,
1896 &mut self.hover_id,
1897 &mut self.hover_ancestors,
1898 None,
1899 pos,
1900 self.modifiers,
1901 );
1902 }
1903
1904 pub fn reconcile_hover_from_mouse_pos(&mut self, new_frame: &Frame) {
1906 let pos = Vec2 {
1907 x: self.mouse_pos_px.0,
1908 y: self.mouse_pos_px.1,
1909 };
1910
1911 if let Some(prev_id) = self.hover_id
1914 && !new_frame.hit_regions.iter().any(|h| h.id == prev_id)
1915 {
1916 dispatch_hover_change_bubbled(
1917 Some(new_frame),
1918 &self.hover_leave,
1919 &mut self.hover_id,
1920 &mut self.hover_ancestors,
1921 None,
1922 pos,
1923 self.modifiers,
1924 );
1925 }
1926
1927 if !self.pointer_inside {
1928 if self.hover_id.is_some() || !self.hover_ancestors.is_empty() {
1929 dispatch_hover_change_bubbled(
1930 Some(new_frame),
1931 &self.hover_leave,
1932 &mut self.hover_id,
1933 &mut self.hover_ancestors,
1934 None,
1935 pos,
1936 self.modifiers,
1937 );
1938 request_frame();
1939 }
1940 return;
1941 }
1942
1943 let new_hover = repose_ui::hit_test_enabled_frame(new_frame, pos).map(|hit| hit.id);
1944
1945 self.cursor = if dnd::is_dragging() {
1946 Some(CursorIcon::Grabbing)
1947 } else {
1948 new_hover
1949 .and_then(|id| new_frame.hit_regions.iter().find(|h| h.id == id))
1950 .and_then(|h| h.cursor.clone())
1951 .or(Some(CursorIcon::Default))
1952 };
1953
1954 let new_chain = hover_chain_for(Some(new_frame), new_hover);
1955 let old_chain = hover_chain_for(Some(new_frame), self.hover_id);
1956 if new_chain == old_chain {
1957 return;
1958 }
1959
1960 dispatch_hover_change_bubbled(
1961 Some(new_frame),
1962 &self.hover_leave,
1963 &mut self.hover_id,
1964 &mut self.hover_ancestors,
1965 new_hover,
1966 pos,
1967 self.modifiers,
1968 );
1969 request_frame();
1970 }
1971
1972 fn reset_pointer_state(&mut self) {
1973 self.capture_id = None;
1974 self.hit_path = None;
1975 self.mouse_targets = None;
1976 self.touch_paths.clear();
1977 self.touch_targets.clear();
1978 self.touch_positions.clear();
1979 self.touch_presses.clear();
1980 self.dnd_capture = None;
1981 self.touch_primary = None;
1982 self.suppressed_touch_clicks.clear();
1983 self.pressed_ids.clear();
1984 self.pending_click = None;
1985 self.last_down = None;
1986 self.press_origin = None;
1987 self.double_candidate = None;
1988 self.key_pressed_active = None;
1989 self.key_pressed_key = None;
1990 self.key_long_press = None;
1991 self.last_up = None;
1992 self.last_down = None;
1993 self.double_candidate = None;
1994 self.suppress_next_click = false;
1995 }
1996
1997 fn flush_pending_click(&mut self) {
1998 let Some((id, t0, cb)) = self.pending_click.take() else {
1999 return;
2000 };
2001 if t0.elapsed().as_millis() >= DOUBLE_CLICK_MS {
2002 cb();
2003 request_frame();
2004 } else {
2005 self.pending_click = Some((id, t0, cb));
2006 }
2007 }
2008
2009 fn poll_long_press(&mut self) {
2010 let Some((lid, t0, _, _)) = self.long_press else {
2011 return;
2012 };
2013 if t0.elapsed().as_millis() < LONG_PRESS_MS {
2014 return;
2015 }
2016 let Some(f) = self.frame_cache.clone() else {
2017 return;
2018 };
2019 let captured = self
2022 .long_press_touch
2023 .and_then(|tid| self.touch_presses.get(&tid).map(|state| state.capture_id))
2024 .or(self.capture_id);
2025 if captured != Some(lid) {
2026 self.long_press = None;
2027 self.long_press_touch = None;
2028 return;
2029 }
2030 let (mx, my) = self.mouse_pos_px;
2031 let in_bounds = f
2032 .hit_regions
2033 .iter()
2034 .find(|h| h.id == lid)
2035 .is_some_and(|h| repose_ui::hit_region_contains(h, Vec2 { x: mx, y: my }));
2036 if !in_bounds {
2037 self.long_press = None;
2038 return;
2039 }
2040 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == lid && !h.disabled)
2041 && let Some(cb) = &hit.on_long_click
2042 {
2043 self.long_press = None;
2044 self.suppress_next_click = true;
2045 self.pending_click = None;
2046 self.last_up = None;
2047 cb();
2048 request_frame();
2049 } else {
2050 self.long_press = None;
2051 }
2052 }
2053
2054 fn poll_key_long_press(&mut self) {
2056 let Some((kid, t0, fired)) = self.key_long_press else {
2057 return;
2058 };
2059 if t0.elapsed().as_millis() < LONG_PRESS_MS {
2060 return;
2061 }
2062 let Some(f) = self.frame_cache.clone() else {
2063 return;
2064 };
2065 if !fired {
2066 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == kid && !h.disabled)
2067 && let Some(cb) = &hit.on_long_click
2068 {
2069 cb();
2070 }
2071 self.key_long_press = Some((kid, t0, true));
2072 request_frame();
2073 }
2074 }
2075
2076 pub fn handle_scroll(&mut self, delta: Vec2) -> bool {
2078 let Some(f) = &self.frame_cache else {
2079 return false;
2080 };
2081
2082 let now = web_time::Instant::now();
2083 if let Some(last) = self.last_scroll_at
2084 && now.duration_since(last).as_millis() > 250
2085 {
2086 self.scroll_capture_id = None;
2087 }
2088 self.last_scroll_at = Some(now);
2089
2090 let pos = Vec2 {
2091 x: self.mouse_pos_px.0,
2092 y: self.mouse_pos_px.1,
2093 };
2094 let (consumed, cap) = dispatch_scroll(f, pos, delta, self.scroll_capture_id);
2095 self.scroll_capture_id = cap;
2096 if consumed {
2097 request_frame();
2098 }
2099 consumed
2100 }
2101
2102 fn resolve_shortcut_action(
2103 &self,
2104 chord: &repose_core::shortcuts::KeyChord,
2105 ) -> Option<repose_core::shortcuts::Action> {
2106 self.shortcuts.resolve_action(chord)
2107 }
2108
2109 fn handle_shortcut_action(&self, action: repose_core::shortcuts::Action) -> bool {
2110 self.shortcuts.handle(action)
2111 }
2112
2113 pub fn handle_key(&mut self, event: &KeyEvent) -> bool {
2115 let _event_scope = crate::lifecycle::enter_dispatchers(self.event_dispatchers());
2116 let shortcut_state = self.shortcuts.clone();
2117 repose_core::shortcuts::with_runtime_state(&shortcut_state, || self.handle_key_inner(event))
2118 }
2119
2120 fn handle_key_inner(&mut self, event: &KeyEvent) -> bool {
2121 let _dnd_guard = self.dnd_context.enter();
2122 if event.event_type == KeyEventType::Down {
2123 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
2124 }
2125
2126 let Some(frame) = self.frame_cache.clone() else {
2127 if event.event_type == KeyEventType::Down
2128 && !event.is_repeat
2129 && let Some(action) = self.resolve_shortcut_action(
2130 &repose_core::shortcuts::KeyChord::new(event.key.clone(), event.modifiers),
2131 )
2132 {
2133 return self.dispatch_action(action);
2134 }
2135 return false;
2136 };
2137 let f = &frame;
2138
2139 if event.event_type == KeyEventType::Down && !event.is_repeat && event.key == Key::Escape {
2140 self.cancel_keyboard_press();
2141 if self.cancel_active_dnd() {
2142 request_frame();
2143 return true;
2144 }
2145 }
2146
2147 if event.event_type == KeyEventType::Down && !event.is_repeat {
2148 let is_back = event.key == Key::Escape
2149 || self
2150 .resolve_shortcut_action(&repose_core::shortcuts::KeyChord::new(
2151 event.key.clone(),
2152 event.modifiers,
2153 ))
2154 .is_some_and(|action| matches!(action, repose_core::shortcuts::Action::Back));
2155 if is_back && self.overlay.handle_back() {
2156 request_frame();
2157 return true;
2158 }
2159 }
2160
2161 if self.dispatch_key_event(f, event) {
2162 request_frame();
2163 return true;
2164 }
2165
2166 if event.event_type == KeyEventType::Down
2168 && !event.is_repeat
2169 && let Some(action) = self.resolve_shortcut_action(
2170 &repose_core::shortcuts::KeyChord::new(event.key.clone(), event.modifiers),
2171 )
2172 {
2173 if self.dispatch_action(action.clone()) {
2175 return true;
2176 }
2177 }
2178
2179 if let Some(fid) = self.sched.focused {
2181 let is_tf = f
2182 .semantics_nodes
2183 .iter()
2184 .any(|n| n.id == fid && n.role == repose_core::semantics::Role::TextField);
2185 if !is_tf {
2186 if event.event_type == KeyEventType::Down && !event.is_repeat {
2187 if event.key == Key::Space || event.key == Key::Enter {
2188 let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) else {
2189 return false;
2190 };
2191 if hit.disabled {
2192 return false;
2193 }
2194 if hit.on_click.is_none()
2195 && hit.on_long_click.is_none()
2196 && hit.on_double_click.is_none()
2197 {
2198 return false; }
2200 self.cancel_keyboard_press();
2201 self.pressed_ids.insert(fid);
2202 self.key_pressed_active = Some(fid);
2203 self.key_pressed_key = Some(event.key.clone());
2204 self.key_long_press = if hit.on_long_click.is_some() {
2205 Some((fid, web_time::Instant::now(), false))
2206 } else {
2207 None
2208 };
2209
2210 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
2211 && let Some(src) = &hit.interaction_source
2212 {
2213 let local = Vec2 {
2214 x: hit.rect.w * 0.5,
2215 y: hit.rect.h * 0.5,
2216 };
2217 src.to_mutable().emit(Interaction::new_press(local));
2218 }
2219
2220 request_frame();
2221 return true;
2222 }
2223 } else if event.event_type == KeyEventType::Up
2224 && let Some(active_id) = self.key_pressed_active
2225 && self
2226 .key_pressed_key
2227 .as_ref()
2228 .is_some_and(|key| key == &event.key)
2229 {
2230 self.pressed_ids.remove(&active_id);
2231 self.key_pressed_active = None;
2232 self.key_pressed_key = None;
2233
2234 let long_fired = self
2235 .key_long_press
2236 .take()
2237 .map(|(_, _, fired)| fired)
2238 .unwrap_or(false);
2239
2240 if let Some(hit) = f
2241 .hit_regions
2242 .iter()
2243 .find(|h| h.id == active_id && !h.disabled)
2244 {
2245 if let Some(src) = &hit.interaction_source {
2246 let pid = src.collect_last_press_id().unwrap_or(0);
2247 src.to_mutable().emit(Interaction::Release(pid));
2248 }
2249 if !long_fired && let Some(cb) = &hit.on_click {
2250 cb();
2251 }
2252 }
2253 request_frame();
2254 return true;
2255 }
2256 }
2257 }
2258
2259 if event.event_type == KeyEventType::Down
2261 && !event.is_repeat
2262 && event.key == Key::Enter
2263 && let Some(fid) = self.sched.focused
2264 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
2265 {
2266 let is_multiline = hit.tf_multiline;
2267 let should_submit = if is_multiline {
2268 self.modifiers.ctrl || self.modifiers.meta
2269 } else {
2270 true
2271 };
2272 if should_submit {
2273 let key = tf_key_of(f, fid);
2274 if let Some(state_rc) = self.textfield_states.get(&key) {
2275 let text = state_rc.borrow().text.clone();
2276 let action = match hit.ime_action {
2277 repose_core::text::ImeAction::Unspecified => {
2278 repose_core::text::ImeAction::Done
2279 }
2280 action => action,
2281 };
2282 repose_ui::textfield::dispatch_textfield_keyboard_action(hit, action, &|| {
2283 if let Some(on_submit) = &hit.on_text_submit {
2284 on_submit(text.clone());
2285 }
2286 });
2287 request_frame();
2288 return true;
2289 }
2290 } else {
2291 let key = tf_key_of(f, fid);
2293 if !is_tf_editable(f, fid) {
2294 return true;
2295 }
2296 if let Some(state_rc) = self.textfield_states.get(&key) {
2297 let mut edited = state_rc.borrow().clone();
2298 repose_ui::textfield::insert_text_with_input_transformation(
2299 hit,
2300 &mut edited,
2301 "\n",
2302 false,
2303 );
2304 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2305 let new_text = edited.text.clone();
2306 *state_rc.borrow_mut() = edited;
2307 notify_text_change(f, fid, new_text);
2308 request_frame();
2309 return true;
2310 }
2311 }
2312 }
2313
2314 if event.event_type == KeyEventType::Down
2316 && let Some(fid) = self.sched.focused
2317 && self.handle_text_navigation_key(f, fid, &event.key)
2318 {
2319 return true;
2320 }
2321
2322 if event.event_type == KeyEventType::Down {
2323 if !self.ime_preedit
2325 && !self.modifiers.ctrl
2326 && !self.modifiers.alt
2327 && !self.modifiers.meta
2328 && let Key::Character(c) = event.key
2329 && !c.is_control()
2330 && c != '\n'
2331 && c != '\r'
2332 && let Some(fid) = self.sched.focused
2333 {
2334 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 new_text = if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
2340 let mut edited = state_rc.borrow().clone();
2341 repose_ui::textfield::insert_text_with_input_transformation(
2342 hit,
2343 &mut edited,
2344 &c.to_string(),
2345 false,
2346 );
2347 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2348 let text = edited.text.clone();
2349 *state_rc.borrow_mut() = edited;
2350 text
2351 } else {
2352 return false;
2353 };
2354 notify_text_change(f, fid, new_text);
2355 request_frame();
2356 return true;
2357 }
2358 }
2359 }
2360
2361 false
2362 }
2363
2364 fn display_text_for_state(
2365 state: &TextFieldState,
2366 ) -> (String, usize, Option<Box<dyn repose_core::OffsetMapping>>) {
2367 let caret = state.caret_index();
2368 let Some(transformation) = state.visual_transformation.clone() else {
2369 return (state.text.clone(), caret, None);
2370 };
2371 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
2372 let transformed = transformation.filter(&annotated);
2373 let display_caret = transformed.offset_mapping.original_to_transformed(caret);
2374 (
2375 transformed.text.text,
2376 display_caret,
2377 Some(transformed.offset_mapping),
2378 )
2379 }
2380
2381 fn handle_text_navigation_key(&mut self, frame: &Frame, id: u64, key: &Key) -> bool {
2382 let Some(hit) = frame.hit_regions.iter().find(|hit| hit.id == id) else {
2383 return false;
2384 };
2385 let state_key = tf_key_of(frame, id);
2386 let Some(state) = self.textfield_states.get(&state_key) else {
2387 return false;
2388 };
2389 let editable = is_tf_editable(frame, id);
2390 let metrics = repose_ui::textfield::textfield_metrics(hit);
2391 let shift = self.modifiers.shift;
2392 let mut edited = state.borrow().clone();
2393 let (display, display_caret, mapping) = Self::display_text_for_state(&edited);
2394 let mut changed = false;
2395 let consumed = match key {
2396 Key::Backspace => {
2397 changed = editable
2398 && repose_ui::textfield::delete_backward_with_input_transformation(
2399 hit,
2400 &mut edited,
2401 );
2402 true
2403 }
2404 Key::Delete => {
2405 changed = editable
2406 && repose_ui::textfield::delete_forward_with_input_transformation(
2407 hit,
2408 &mut edited,
2409 );
2410 true
2411 }
2412 Key::ArrowLeft => {
2413 edited.move_cursor(-1, shift);
2414 edited.preferred_x_px = None;
2415 true
2416 }
2417 Key::ArrowRight => {
2418 edited.move_cursor(1, shift);
2419 edited.preferred_x_px = None;
2420 true
2421 }
2422 Key::ArrowUp if hit.tf_multiline => {
2423 let (next_display, preferred) =
2424 repose_ui::textfield::move_caret_vertical_with_metrics(
2425 &display,
2426 edited.inner_width.max(1.0),
2427 display_caret,
2428 -1,
2429 edited.preferred_x_px,
2430 &metrics,
2431 );
2432 let next = mapping.as_ref().map_or(next_display, |mapping| {
2433 mapping.transformed_to_original(next_display)
2434 });
2435 set_caret(&mut edited, next, shift);
2436 edited.preferred_x_px = Some(preferred);
2437 true
2438 }
2439 Key::ArrowDown if hit.tf_multiline => {
2440 let (next_display, preferred) =
2441 repose_ui::textfield::move_caret_vertical_with_metrics(
2442 &display,
2443 edited.inner_width.max(1.0),
2444 display_caret,
2445 1,
2446 edited.preferred_x_px,
2447 &metrics,
2448 );
2449 let next = mapping.as_ref().map_or(next_display, |mapping| {
2450 mapping.transformed_to_original(next_display)
2451 });
2452 set_caret(&mut edited, next, shift);
2453 edited.preferred_x_px = Some(preferred);
2454 true
2455 }
2456 Key::Home => {
2457 let next_display = repose_ui::textfield::line_home_end_with_metrics(
2458 &display,
2459 edited.inner_width.max(1.0),
2460 display_caret,
2461 false,
2462 &metrics,
2463 );
2464 let next = mapping.as_ref().map_or(next_display, |mapping| {
2465 mapping.transformed_to_original(next_display)
2466 });
2467 set_caret(&mut edited, next, shift);
2468 edited.preferred_x_px = None;
2469 true
2470 }
2471 Key::End => {
2472 let next_display = repose_ui::textfield::line_home_end_with_metrics(
2473 &display,
2474 edited.inner_width.max(1.0),
2475 display_caret,
2476 true,
2477 &metrics,
2478 );
2479 let next = mapping.as_ref().map_or(next_display, |mapping| {
2480 mapping.transformed_to_original(next_display)
2481 });
2482 set_caret(&mut edited, next, shift);
2483 edited.preferred_x_px = None;
2484 true
2485 }
2486 _ => false,
2487 };
2488 if !consumed {
2489 return false;
2490 }
2491 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2492 let text = changed.then(|| edited.text.clone());
2493 *state.borrow_mut() = edited;
2494 if let Some(text) = text {
2495 notify_text_change(frame, id, text);
2496 }
2497 request_frame();
2498 true
2499 }
2500
2501 fn dispatch_key_event(&self, f: &Frame, event: &KeyEvent) -> bool {
2506 let chain = key_ancestor_chain(f, self.sched.focused);
2507 let hit_by_id: HashMap<u64, &HitRegion> = f.hit_regions.iter().map(|h| (h.id, h)).collect();
2508
2509 for &id in &chain {
2510 let Some(hit) = hit_by_id.get(&id) else {
2511 continue;
2512 };
2513 if hit.disabled {
2514 continue;
2515 }
2516 if let Some(cb) = &hit.on_preview_key_event
2517 && cb(event.clone())
2518 {
2519 return true;
2520 }
2521 }
2522
2523 for &id in chain.iter().rev() {
2524 let Some(hit) = hit_by_id.get(&id) else {
2525 continue;
2526 };
2527 if hit.disabled {
2528 continue;
2529 }
2530 if let Some(cb) = &hit.on_key_event
2531 && cb(event.clone())
2532 {
2533 return true;
2534 }
2535 }
2536
2537 false
2538 }
2539
2540 pub fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
2544 if let Some(f) = &self.frame_cache
2546 && let Some(fid) = self.sched.focused
2547 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
2548 && let Some(cb) = &hit.on_action
2549 && cb(action.clone())
2550 {
2551 request_frame();
2552 return true;
2553 }
2554
2555 if self.apply_text_editing_action(&action) {
2557 return true;
2558 }
2559
2560 if self.handle_shortcut_action(action.clone()) {
2562 request_frame();
2563 return true;
2564 }
2565
2566 if let Some(f) = self.frame_cache.clone()
2568 && let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.sched, &f)
2569 {
2570 self.cancel_keyboard_press();
2572 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == new_id)
2574 && let Some(key) = hit.tf_state_key
2575 {
2576 let st = ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
2577 {
2578 let mut s = st.borrow_mut();
2579 s.apply_controlled_value(&hit.tf_value);
2580 s.reset_caret_blink();
2581 }
2582 }
2583 request_frame();
2584 return true;
2585 }
2586
2587 false
2588 }
2589
2590 fn apply_text_editing_action(&mut self, action: &repose_core::shortcuts::Action) -> bool {
2593 use repose_core::shortcuts::Action;
2594 let Some(fid) = self.sched.focused else {
2595 return false;
2596 };
2597 let Some(f) = self.frame_cache.clone() else {
2598 return false;
2599 };
2600 if !is_tf_hit(&f, fid) {
2601 return false;
2602 }
2603 let key = tf_key_of(&f, fid);
2604 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
2605 return false;
2606 };
2607 let hit = f.hit_regions.iter().find(|hit| hit.id == fid);
2608
2609 match action {
2610 Action::Undo => {
2611 if !is_tf_editable(&f, fid) {
2612 return true;
2613 }
2614 let Some(hit) = hit else {
2615 return false;
2616 };
2617 let mut edited = state_rc.borrow().clone();
2618 if !repose_ui::textfield::undo_with_input_transformation(hit, &mut edited) {
2619 return false;
2620 }
2621 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2622 let new_text = edited.text.clone();
2623 *state_rc.borrow_mut() = edited;
2624 notify_text_change(&f, fid, new_text);
2625 request_frame();
2626 true
2627 }
2628 Action::Redo => {
2629 if !is_tf_editable(&f, fid) {
2630 return true;
2631 }
2632 let Some(hit) = hit else {
2633 return false;
2634 };
2635 let mut edited = state_rc.borrow().clone();
2636 if !repose_ui::textfield::redo_with_input_transformation(hit, &mut edited) {
2637 return false;
2638 }
2639 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2640 let new_text = edited.text.clone();
2641 *state_rc.borrow_mut() = edited;
2642 notify_text_change(&f, fid, new_text);
2643 request_frame();
2644 true
2645 }
2646 Action::SelectAll => {
2647 let mut st = state_rc.borrow_mut();
2648 let len = st.text.len();
2649 st.selection = 0..len;
2650 if let Some(hit) = hit {
2651 tf_ensure_caret_visible_for_hit(hit, &mut st);
2652 }
2653 request_frame();
2654 true
2655 }
2656 Action::Copy => {
2657 if hit.is_some_and(tf_is_sensitive) {
2658 return true;
2659 }
2660 let st = state_rc.borrow();
2661 let (a, b) = (
2662 st.selection.start.min(st.selection.end),
2663 st.selection.start.max(st.selection.end),
2664 );
2665 if a == b {
2666 return false;
2667 }
2668 let slice = st.text.get(a..b).unwrap_or("").to_string();
2669 drop(st);
2670 if !slice.is_empty() {
2671 repose_core::clipboard::copy_to_clipboard(&slice);
2672 }
2673 true
2674 }
2675 Action::Cut => {
2676 if hit.is_some_and(tf_is_sensitive) {
2677 return true;
2678 }
2679 if !is_tf_editable(&f, fid) {
2680 return true;
2681 }
2682 let Some(hit) = hit else {
2683 return false;
2684 };
2685 let mut edited = state_rc.borrow().clone();
2686 let Some(slice) =
2687 repose_ui::textfield::cut_with_input_transformation(hit, &mut edited)
2688 else {
2689 return false;
2690 };
2691 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2692 let new_text = edited.text.clone();
2693 *state_rc.borrow_mut() = edited;
2694 repose_core::clipboard::copy_to_clipboard(&slice);
2695 notify_text_change(&f, fid, new_text);
2696 request_frame();
2697 true
2698 }
2699 Action::Paste => {
2700 if let Some(txt) = repose_core::clipboard::paste_text() {
2701 self.paste_into_focused(&txt);
2702 return true;
2703 }
2704 false
2705 }
2706 _ => false,
2707 }
2708 }
2709
2710 fn cancel_ime_compositions(&mut self) {
2711 let mut changed = None;
2712 for (key, state) in &self.textfield_states {
2713 let mut edited = state.borrow().clone();
2714 if edited.composition.is_none() {
2715 continue;
2716 }
2717 edited.cancel_composition();
2718 *state.borrow_mut() = edited;
2719 changed = Some((*key, self.textfield_states[key].borrow().text.clone()));
2720 }
2721 self.ime_preedit = false;
2722 if let (Some((key, text)), Some(frame)) = (changed, &self.frame_cache)
2723 && let Some(id) = frame
2724 .hit_regions
2725 .iter()
2726 .find(|hit| hit.tf_state_key.unwrap_or(hit.id) == key)
2727 .map(|hit| hit.id)
2728 {
2729 notify_text_change(frame, id, text);
2730 }
2731 request_frame();
2732 }
2733
2734 pub fn handle_ime(&mut self, event: &ImeEvent) {
2736 if matches!(event, ImeEvent::Cancel) {
2737 self.cancel_ime_compositions();
2738 return;
2739 }
2740 let Some(fid) = self.sched.focused else {
2741 return;
2742 };
2743 let Some(frame) = self.frame_cache.clone() else {
2744 return;
2745 };
2746 if !matches!(event, ImeEvent::Cancel) && !is_tf_editable(&frame, fid) {
2747 return;
2748 }
2749 let key = tf_key_of(&frame, fid);
2750 let Some(state) = self.textfield_states.get(&key).cloned() else {
2751 return;
2752 };
2753 let hit = frame.hit_regions.iter().find(|hit| hit.id == fid);
2754 let mut edited = state.borrow().clone();
2755 let changed = match event {
2756 ImeEvent::Start => {
2757 self.ime_preedit = false;
2758 None
2759 }
2760 ImeEvent::Update { text, cursor } => {
2761 edited.set_composition(text.clone(), *cursor);
2762 if let Some(hit) = hit {
2763 repose_ui::textfield::apply_textfield_input_transformation(hit, &mut edited);
2764 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2765 }
2766 self.ime_preedit = !text.is_empty();
2767 Some(edited.text.clone())
2768 }
2769 ImeEvent::Commit(text) => {
2770 if let Some(hit) = hit {
2771 repose_ui::textfield::commit_composition_with_input_transformation(
2772 hit,
2773 &mut edited,
2774 text.clone(),
2775 );
2776 tf_ensure_caret_visible_for_hit(hit, &mut edited);
2777 } else {
2778 edited.commit_composition(text.clone());
2779 }
2780 self.ime_preedit = false;
2781 Some(edited.text.clone())
2782 }
2783 ImeEvent::Cancel => unreachable!(),
2784 };
2785 *state.borrow_mut() = edited;
2786 if let Some(text) = changed {
2787 notify_text_change(&frame, fid, text);
2788 }
2789 request_frame();
2790 }
2791
2792 pub fn finish_compositions(&mut self) {
2796 for state_rc in self.textfield_states.values() {
2797 let mut st = state_rc.borrow_mut();
2798 if st.composition.is_some() {
2799 st.finish_composition();
2800 }
2801 }
2802 self.ime_preedit = false;
2803 }
2804
2805 pub fn handle_focus_lost(&mut self) {
2810 self.pointer_inside = false;
2811 self.sched.window_focused = false;
2812 self.handle_pointer_cancel();
2813 self.held_keys.clear();
2814 self.held_mouse.clear();
2815 self.sched.held_keys.clear();
2816 self.sched.mouse_primary = false;
2817 self.sched.mouse_secondary = false;
2818 self.sched.mouse_middle = false;
2819 self.sched.touch_points.clear();
2820 self.key_pressed_active = None;
2821 self.key_pressed_key = None;
2822 self.key_long_press = None;
2823 self.pressed_ids.clear();
2824 self.pending_click = None;
2825 self.last_down = None;
2826 self.press_origin = None;
2827 self.last_up = None;
2828 self.double_candidate = None;
2829 self.scroll_capture_id = None;
2830 self.last_scroll_at = None;
2831 self.finish_compositions();
2832 }
2833
2834 pub fn set_physical_key(&mut self, name: &str, down: bool) {
2839 if down {
2840 self.held_keys.insert(name.to_string());
2841 } else {
2842 self.held_keys.remove(name);
2843 }
2844 }
2845
2846 pub fn held_physical_keys(&self) -> Vec<String> {
2848 let mut out: Vec<String> = self.held_keys.iter().cloned().collect();
2849 out.sort();
2850 out
2851 }
2852
2853 pub fn physical_key_held(&self, name: &str) -> bool {
2855 self.held_keys.contains(name)
2856 }
2857
2858 pub fn mouse_button_held(&self, button: PointerButton) -> bool {
2860 self.held_mouse.contains(&button)
2861 }
2862
2863 pub fn ensure_textfield_state(&mut self, key: u64) -> Rc<RefCell<TextFieldState>> {
2865 self.textfield_states
2866 .entry(key)
2867 .or_insert_with(|| Rc::new(RefCell::new(TextFieldState::new())))
2868 .clone()
2869 }
2870
2871 pub fn ensure_textfield_state_seeded(
2872 &mut self,
2873 key: u64,
2874 seed: &str,
2875 ) -> Rc<RefCell<TextFieldState>> {
2876 ensure_tf_state(&mut self.textfield_states, key, seed)
2877 }
2878
2879 pub fn tf_key_of(&self, visual_id: u64) -> u64 {
2881 self.frame_cache
2882 .as_ref()
2883 .map(|f| tf_key_of(f, visual_id))
2884 .unwrap_or(visual_id)
2885 }
2886
2887 pub fn is_textfield(&self, id: u64) -> bool {
2889 self.frame_cache
2890 .as_ref()
2891 .map(|f| is_textfield_in_frame(f, id))
2892 .unwrap_or(false)
2893 }
2894
2895 pub fn is_editable_textfield(&self, id: u64) -> bool {
2897 self.frame_cache
2898 .as_ref()
2899 .map(|f| is_tf_editable(f, id))
2900 .unwrap_or(false)
2901 }
2902
2903 pub fn is_multiline(&self, id: u64) -> bool {
2905 self.frame_cache
2906 .as_ref()
2907 .map(|f| is_multiline_id(f, id))
2908 .unwrap_or(false)
2909 }
2910
2911 pub fn focused_keyboard_hints(
2914 &self,
2915 ) -> (
2916 repose_core::ImePurposeHint,
2917 bool,
2918 repose_core::KeyboardCapitalization,
2919 ) {
2920 let defaults = || {
2921 (
2922 repose_core::ImePurposeHint::Normal,
2923 true,
2924 repose_core::KeyboardCapitalization::Unspecified,
2925 )
2926 };
2927 let Some(fid) = self.sched.focused else {
2928 return defaults();
2929 };
2930 let Some(f) = &self.frame_cache else {
2931 return defaults();
2932 };
2933 match f.hit_regions.iter().find(|h| h.id == fid) {
2934 Some(hit) => (
2935 hit.keyboard_type.ime_purpose_hint(),
2936 hit.auto_correct.unwrap_or(true),
2937 hit.capitalization,
2938 ),
2939 None => defaults(),
2940 }
2941 }
2942
2943 pub fn focused_ime_action(&self) -> repose_core::text::ImeAction {
2944 self.frame_cache
2945 .as_ref()
2946 .and_then(|frame| {
2947 self.sched
2948 .focused
2949 .and_then(|id| frame.hit_regions.iter().find(|hit| hit.id == id))
2950 })
2951 .map(|hit| hit.ime_action)
2952 .unwrap_or_default()
2953 }
2954
2955 pub fn insert_text_into_focused(&mut self, text: &str) -> bool {
2961 if text.is_empty()
2962 || self.ime_preedit
2963 || self.modifiers.ctrl
2964 || self.modifiers.alt
2965 || self.modifiers.meta
2966 {
2967 return false;
2968 }
2969 let Some(fid) = self.sched.focused else {
2970 return false;
2971 };
2972 let Some(f) = self.frame_cache.clone() else {
2973 return false;
2974 };
2975 if !is_textfield_in_frame(&f, fid) {
2976 return false;
2977 }
2978 if !is_tf_editable(&f, fid) {
2979 return false;
2980 }
2981 let key = tf_key_of(&f, fid);
2982 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
2983 return false;
2984 };
2985 let multiline = is_multiline_id(&f, fid);
2986 let filtered: String = text
2987 .chars()
2988 .filter(|c| {
2989 (*c == '\n' && multiline) || (!c.is_control() && *c != '\r')
2993 })
2994 .collect();
2995 if filtered.is_empty() {
2996 return false;
2997 }
2998 let new_text = if let Some(hit) = f.hit_regions.iter().find(|hit| hit.id == fid) {
2999 let mut edited = state_rc.borrow().clone();
3000 repose_ui::textfield::insert_text_with_input_transformation(
3001 hit,
3002 &mut edited,
3003 &filtered,
3004 false,
3005 );
3006 tf_ensure_caret_visible_for_hit(hit, &mut edited);
3007 let text = edited.text.clone();
3008 *state_rc.borrow_mut() = edited;
3009 text
3010 } else {
3011 return false;
3012 };
3013 notify_text_change(&f, fid, new_text);
3014 request_frame();
3015 true
3016 }
3017
3018 pub fn insert_text(&mut self, text: &str) -> bool {
3022 self.insert_text_into_focused(text)
3023 }
3024
3025 pub fn paste_into_focused(&mut self, text: &str) {
3028 let Some(fid) = self.sched.focused else {
3029 return;
3030 };
3031 let Some(f) = &self.frame_cache.clone() else {
3032 return;
3033 };
3034 if !is_tf_editable(f, fid) {
3035 return;
3036 }
3037 let key = tf_key_of(f, fid);
3038 let Some(state_rc) = self.textfield_states.get(&key) else {
3039 return;
3040 };
3041 let Some(hit) = f.hit_regions.iter().find(|hit| hit.id == fid) else {
3042 return;
3043 };
3044 let filtered: String = text
3045 .chars()
3046 .filter(|c| (*c == '\n' && hit.tf_multiline) || (!c.is_control() && *c != '\r'))
3047 .collect();
3048 if filtered.is_empty() {
3049 return;
3050 }
3051 let new_text = {
3052 let mut edited = state_rc.borrow().clone();
3053 repose_ui::textfield::insert_text_with_input_transformation(
3054 hit,
3055 &mut edited,
3056 &filtered,
3057 true,
3058 );
3059 tf_ensure_caret_visible_for_hit(hit, &mut edited);
3060 let text = edited.text.clone();
3061 *state_rc.borrow_mut() = edited;
3062 text
3063 };
3064 notify_text_change(f, fid, new_text);
3065 request_frame();
3066 }
3067
3068 pub fn handle_gamepad(&mut self, event: &GamepadEvent) -> bool {
3074 match event {
3075 GamepadEvent::Connected { id, name } => {
3076 self.gamepads.insert(
3077 id.0,
3078 GamepadPad {
3079 name: name.clone(),
3080 ..GamepadPad::default()
3081 },
3082 );
3083 request_frame();
3084 return false;
3085 }
3086 GamepadEvent::Disconnected { id } => {
3087 self.gamepads.remove(&id.0);
3088 request_frame();
3089 return false;
3090 }
3091 GamepadEvent::Button {
3092 id,
3093 button,
3094 pressed,
3095 } => {
3096 if let Some(pad) = self.gamepads.get_mut(&id.0) {
3097 if *pressed {
3098 pad.pressed.insert(*button);
3099 } else {
3100 pad.pressed.remove(button);
3101 }
3102 }
3103 if *pressed {
3104 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
3105 }
3106 let key = match button {
3107 GamepadButton::South => Key::Space,
3108 GamepadButton::East => Key::Escape,
3109 GamepadButton::Start => Key::Enter,
3110 GamepadButton::DPadUp => Key::ArrowUp,
3111 GamepadButton::DPadDown => Key::ArrowDown,
3112 GamepadButton::DPadLeft => Key::ArrowLeft,
3113 GamepadButton::DPadRight => Key::ArrowRight,
3114 _ => return false,
3115 };
3116 let synthetic = KeyEvent {
3117 key,
3118 modifiers: self.modifiers,
3119 is_repeat: false,
3120 event_type: if *pressed {
3121 KeyEventType::Down
3122 } else {
3123 KeyEventType::Up
3124 },
3125 utf16_code_point: 0,
3126 physical: None,
3127 };
3128 return self.handle_key(&synthetic);
3129 }
3130 GamepadEvent::Axis { id, axis, value } => {
3131 if let Some(pad) = self.gamepads.get_mut(&id.0) {
3132 pad.axes.insert(*axis, *value);
3133 }
3134 return false;
3135 }
3136 }
3137 }
3138
3139 pub fn request_rumble(
3144 &mut self,
3145 id: repose_core::input::GamepadId,
3146 low_freq: f32,
3147 high_freq: f32,
3148 duration_ms: u32,
3149 ) {
3150 self.pending_rumble.push((
3151 id.0,
3152 low_freq.clamp(0.0, 1.0),
3153 high_freq.clamp(0.0, 1.0),
3154 duration_ms,
3155 ));
3156 }
3157
3158 pub fn stop_rumble(&mut self, id: repose_core::input::GamepadId) {
3160 self.pending_rumble.push((id.0, 0.0, 0.0, 0));
3161 }
3162
3163 pub fn take_rumble_requests(&mut self) -> Vec<(u32, f32, f32, u32)> {
3165 std::mem::take(&mut self.pending_rumble)
3166 }
3167
3168 pub fn handle_key_with_text(&mut self, event: &KeyEvent, composed_text: Option<&str>) -> bool {
3174 if event.event_type == KeyEventType::Down {
3175 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
3176 }
3177 if event.event_type == KeyEventType::Down
3178 && !event.is_repeat
3179 && !self.ime_preedit
3180 && !event.modifiers.ctrl
3181 && !event.modifiers.alt
3182 && !event.modifiers.meta
3183 && let Some(text) = composed_text
3184 && !text.chars().all(|c| c.is_control())
3185 && self.insert_text_into_focused(text)
3186 {
3187 return true;
3188 }
3189 self.handle_key(event)
3190 }
3191
3192 pub fn handle_scroll_at(
3196 &mut self,
3197 pos: Vec2,
3198 delta: Vec2,
3199 scroll_capture: Option<u64>,
3200 ) -> (bool, Option<u64>) {
3201 let Some(f) = &self.frame_cache else {
3202 return (false, scroll_capture);
3203 };
3204 let (consumed, cap) = dispatch_scroll(f, pos, delta, scroll_capture);
3205 if consumed {
3206 request_frame();
3207 }
3208 (consumed, cap)
3209 }
3210
3211 fn next_caret_blink_deadline(&self) -> Option<web_time::Instant> {
3214 let fid = self.sched.focused?;
3215 let frame = self.frame_cache.as_ref()?;
3216 let hit = frame.hit_regions.iter().find(|h| h.id == fid)?;
3217 let key = hit.tf_state_key?;
3218 self.textfield_states
3219 .get(&key)?
3220 .borrow()
3221 .next_blink_deadline()
3222 }
3223
3224 fn next_gesture_deadline(&self) -> Option<web_time::Instant> {
3225 [
3226 self.pending_click
3227 .as_ref()
3228 .map(|(_, at, _)| *at + web_time::Duration::from_millis(DOUBLE_CLICK_MS as u64)),
3229 self.long_press
3230 .as_ref()
3231 .map(|(_, at, _, _)| *at + web_time::Duration::from_millis(LONG_PRESS_MS as u64)),
3232 self.key_long_press
3233 .as_ref()
3234 .filter(|(_, _, fired)| !*fired)
3235 .map(|(_, at, _)| *at + web_time::Duration::from_millis(LONG_PRESS_MS as u64)),
3236 ]
3237 .into_iter()
3238 .flatten()
3239 .min()
3240 }
3241
3242 pub fn next_wakeup_deadline(&self) -> Option<web_time::Instant> {
3245 [
3246 self.next_caret_blink_deadline(),
3247 self.next_gesture_deadline(),
3248 repose_ui::overlay::SnackbarController::next_deadline(),
3249 repose_core::timer::next_deadline(),
3250 ]
3251 .into_iter()
3252 .flatten()
3253 .min()
3254 }
3255
3256 pub fn is_wakeup_due(&self, now: web_time::Instant) -> bool {
3258 self.next_wakeup_deadline().is_some_and(|d| d <= now)
3259 }
3260
3261 pub fn is_caret_blink_due(&self, now: web_time::Instant) -> bool {
3264 self.is_wakeup_due(now)
3265 }
3266
3267 pub fn next_frame_deadline(
3271 &self,
3272 now: web_time::Instant,
3273 idle_cap: web_time::Duration,
3274 ) -> web_time::Instant {
3275 self.next_wakeup_deadline().unwrap_or(now + idle_cap)
3276 }
3277
3278 pub fn tick_overlays(&self) {
3282 repose_ui::overlay::SnackbarController::tick_all();
3283 repose_core::timer::poll();
3284 }
3285
3286 pub fn cursor_suggestion(&self) -> Option<CursorIcon> {
3288 self.cursor.clone()
3289 }
3290
3291 pub fn take_cursor_suggestion(&mut self) -> Option<CursorIcon> {
3293 self.cursor.take()
3294 }
3295}
3296
3297impl Default for ReposeRuntime {
3298 fn default() -> Self {
3299 Self::new()
3300 }
3301}
3302
3303pub fn compose_frame_inner<F>(
3305 sched: &mut Scheduler,
3306 root_fn: &mut F,
3307 scale: f32,
3308 size_px_u32: (u32, u32),
3309 hover_id: Option<u64>,
3310 pressed_ids: &HashSet<u64>,
3311 tf_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
3312) -> Frame
3313where
3314 F: FnMut(&mut Scheduler) -> View,
3315{
3316 compose_frame_inner_with_ancestors(
3317 sched,
3318 root_fn,
3319 scale,
3320 size_px_u32,
3321 hover_id,
3322 &std::collections::HashSet::new(),
3323 pressed_ids,
3324 tf_states,
3325 )
3326}
3327
3328pub fn compose_frame_inner_with_ancestors<F>(
3329 sched: &mut Scheduler,
3330 root_fn: &mut F,
3331 scale: f32,
3332 size_px_u32: (u32, u32),
3333 hover_id: Option<u64>,
3334 hover_ancestors: &std::collections::HashSet<u64>,
3335 pressed_ids: &HashSet<u64>,
3336 tf_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
3337) -> Frame
3338where
3339 F: FnMut(&mut Scheduler) -> View,
3340{
3341 for requested_id in repose_core::runtime::drain_focus_requests() {
3342 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
3343 sched.focused = None;
3344 continue;
3345 }
3346 sched.focused = Some(requested_id);
3347 }
3348
3349 set_density_default(Density { scale });
3350
3351 let current_focused = sched.focused;
3352
3353 let frame = sched.repose(
3354 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s)),
3355 {
3356 let hover_ancestors = hover_ancestors.clone();
3357 let pressed_ids = pressed_ids.clone();
3358 move |view, _size| {
3359 let interactions = Interactions {
3360 hover: hover_id,
3361 hover_ancestors: hover_ancestors.clone(),
3362 pressed: pressed_ids.clone(),
3363 };
3364 with_density(Density { scale }, || {
3365 layout_and_paint(view, size_px_u32, tf_states, &interactions, current_focused)
3366 })
3367 }
3368 },
3369 );
3370
3371 for requested_id in repose_core::runtime::drain_focus_requests() {
3372 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
3373 sched.focused = None;
3374 } else if frame.focus_chain.contains(&requested_id) {
3375 sched.focused = Some(requested_id);
3376 }
3377 }
3378
3379 if let Some(fid) = sched.focused
3380 && !frame.focus_chain.contains(&fid)
3381 {
3382 sched.focused = None;
3383 }
3384
3385 frame
3386}
3387
3388fn key_ancestor_chain(f: &Frame, focused: Option<u64>) -> Vec<u64> {
3392 let parent_of: HashMap<u64, Option<u64>> =
3393 f.hit_regions.iter().map(|h| (h.id, h.parent)).collect();
3394 let mut leaf = focused.filter(|id| parent_of.contains_key(id));
3395 if leaf.is_none() {
3396 let ids: HashSet<u64> = parent_of.keys().copied().collect();
3397 leaf = f
3398 .hit_regions
3399 .iter()
3400 .filter(|h| !h.disabled)
3401 .find(|h| h.parent.is_none_or(|p| !ids.contains(&p)))
3402 .map(|h| h.id);
3403 }
3404 let Some(mut cur) = leaf else {
3405 return Vec::new();
3406 };
3407 let mut chain = vec![cur];
3408 while let Some(parent) = parent_of.get(&cur).copied().flatten() {
3409 if chain.contains(&parent) {
3410 break;
3411 }
3412 chain.push(parent);
3413 cur = parent;
3414 }
3415 chain.reverse();
3416 chain
3417}
3418
3419fn hover_chain_for(frame: Option<&Frame>, hover: Option<u64>) -> std::collections::HashSet<u64> {
3420 let Some(f) = frame else {
3421 return std::collections::HashSet::new();
3422 };
3423 let Some(mut cur) = hover else {
3424 return std::collections::HashSet::new();
3425 };
3426 let map: std::collections::HashMap<u64, Option<u64>> =
3427 f.hit_regions.iter().map(|h| (h.id, h.parent)).collect();
3428 let mut set = std::collections::HashSet::new();
3429 loop {
3430 set.insert(cur);
3431 if let Some(Some(parent)) = map.get(&cur).copied() {
3432 cur = parent;
3433 } else {
3434 break;
3435 }
3436 }
3437 set
3438}
3439
3440fn dispatch_hover_change_bubbled(
3441 frame: Option<&Frame>,
3442 leave_map: &HashMap<u64, repose_ui::HitRegionSnapshot>,
3443 hover_id: &mut Option<u64>,
3444 hover_ancestors: &mut std::collections::HashSet<u64>,
3445 new_hover: Option<u64>,
3446 pos: Vec2,
3447 modifiers: Modifiers,
3448) {
3449 let old_hover = *hover_id;
3450 let mut old_chain = hover_ancestors.clone();
3451 if let Some(id) = old_hover {
3452 old_chain.insert(id);
3453 }
3454 let new_chain = hover_chain_for(frame, new_hover);
3455 if old_chain == new_chain {
3456 return;
3457 }
3458 for leave_id in old_chain.difference(&new_chain) {
3459 let current = frame.and_then(|f| f.hit_regions.iter().find(|h| h.id == *leave_id));
3460 if let Some(hit) = current
3461 && let Some(cb) = &hit.on_pointer_leave
3462 {
3463 let mut pe = PointerEvent::new(
3464 PointerId(0),
3465 PointerKind::Mouse,
3466 PointerEventKind::Leave,
3467 pos,
3468 1.0,
3469 modifiers,
3470 );
3471 let (origin, local) = repose_ui::hit_region_pointer_coordinates(hit, pos);
3472 pe.origin = origin;
3473 pe.position = local;
3474 cb(pe);
3475 continue;
3476 }
3477 if let Some(snapshot) = leave_map.get(leave_id)
3478 && let Some(callback) = &snapshot.hit().on_pointer_leave
3479 {
3480 let mut pe = PointerEvent::new(
3481 PointerId(0),
3482 PointerKind::Mouse,
3483 PointerEventKind::Leave,
3484 pos,
3485 1.0,
3486 modifiers,
3487 );
3488 let (origin, local) = snapshot.pointer_coordinates(pos);
3489 pe.origin = origin;
3490 pe.position = local;
3491 callback(pe);
3492 }
3493 }
3494 for enter_id in new_chain.difference(&old_chain) {
3495 if let Some(f) = frame
3496 && let Some(h) = f.hit_regions.iter().find(|h| h.id == *enter_id)
3497 && let Some(cb) = &h.on_pointer_enter
3498 {
3499 let mut pe = PointerEvent::new(
3500 PointerId(0),
3501 PointerKind::Mouse,
3502 PointerEventKind::Enter,
3503 pos,
3504 1.0,
3505 modifiers,
3506 );
3507 let (origin, local) = repose_ui::hit_region_pointer_coordinates(h, pos);
3508 pe.origin = origin;
3509 pe.position = local;
3510 cb(pe);
3511 }
3512 }
3513 *hover_id = new_hover;
3514 hover_ancestors.clear();
3515 for id in &new_chain {
3516 if Some(*id) != new_hover {
3517 hover_ancestors.insert(*id);
3518 }
3519 }
3520}
3521
3522pub fn is_textfield_in_frame(f: &Frame, id: u64) -> bool {
3523 f.semantics_nodes
3524 .iter()
3525 .any(|n| n.id == id && n.role == repose_core::semantics::Role::TextField)
3526}
3527
3528pub fn is_textfield_in_frame_cache(frame_cache: &Option<Rc<Frame>>, id: u64) -> bool {
3529 if let Some(f) = frame_cache {
3530 is_textfield_in_frame(f, id)
3531 } else {
3532 false
3533 }
3534}
3535
3536pub fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
3537 frame.hit_regions.iter().position(|h| h.id == id)
3538}
3539
3540fn is_multiline_id(f: &Frame, id: u64) -> bool {
3541 f.hit_regions
3542 .iter()
3543 .find(|h| h.id == id)
3544 .map(|h| h.tf_multiline)
3545 .unwrap_or(false)
3546}
3547
3548fn tf_can_edit(hit: &HitRegion) -> bool {
3551 hit.tf_enabled && !hit.tf_read_only
3552}
3553
3554fn tf_is_sensitive(hit: &HitRegion) -> bool {
3555 hit.tf_sensitive
3556 || matches!(
3557 hit.keyboard_type,
3558 repose_core::KeyboardType::Password
3559 | repose_core::KeyboardType::NumberPassword
3560 | repose_core::KeyboardType::DecimalPassword
3561 | repose_core::KeyboardType::NumberPasswordSigned
3562 | repose_core::KeyboardType::DecimalPasswordSigned
3563 )
3564}
3565
3566fn is_tf_editable(f: &Frame, id: u64) -> bool {
3567 f.hit_regions
3568 .iter()
3569 .find(|h| h.id == id)
3570 .is_some_and(tf_can_edit)
3571}
3572
3573fn tf_key_of(frame: &Frame, visual_id: u64) -> u64 {
3574 if let Some(i) = frame.hit_regions.iter().position(|h| h.id == visual_id) {
3575 let hr = &frame.hit_regions[i];
3576 return hr.tf_state_key.unwrap_or(hr.id);
3577 }
3578 visual_id
3579}
3580
3581fn set_caret(state: &mut TextFieldState, position: usize, extend: bool) {
3582 if extend {
3583 state.selection.end = position;
3584 } else {
3585 state.selection = position..position;
3586 }
3587}
3588
3589fn notify_text_change(f: &Frame, id: u64, text: String) {
3590 if let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
3591 && let Some(cb) = &h.on_text_change
3592 {
3593 cb(text);
3594 }
3595}
3596
3597fn tf_ensure_caret_visible_for_hit(hit: &HitRegion, state: &mut TextFieldState) {
3598 let metrics = repose_ui::textfield::textfield_metrics(hit);
3599 tf_ensure_caret_visible_with_metrics(state, hit.tf_multiline, &metrics);
3600}
3601
3602fn tf_ensure_caret_visible_with_metrics(
3603 state: &mut TextFieldState,
3604 is_multiline: bool,
3605 metrics: &repose_ui::textfield::TextFieldMetrics,
3606) {
3607 repose_ui::textfield::ensure_caret_visible_with_metrics(state, is_multiline, metrics);
3608}
3609
3610fn index_for_x_bytes_vt(
3611 state: &TextFieldState,
3612 metrics: &repose_ui::textfield::TextFieldMetrics,
3613 x_px: f32,
3614) -> usize {
3615 if let Some(transformation) = &state.visual_transformation {
3616 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
3617 let transformed = transformation.filter(&annotated);
3618 let display = repose_ui::textfield::index_for_x_bytes_with_config(
3619 transformed.text.as_str(),
3620 metrics.font_px,
3621 x_px,
3622 metrics.measure_config(),
3623 );
3624 transformed.offset_mapping.transformed_to_original(display)
3625 } else {
3626 repose_ui::textfield::index_for_x_bytes_with_config(
3627 &state.text,
3628 metrics.font_px,
3629 x_px,
3630 metrics.measure_config(),
3631 )
3632 }
3633}
3634
3635fn index_for_xy_bytes_vt(
3636 state: &TextFieldState,
3637 metrics: &repose_ui::textfield::TextFieldMetrics,
3638 wrap_width: f32,
3639 x_px: f32,
3640 y_px: f32,
3641) -> usize {
3642 if let Some(transformation) = &state.visual_transformation {
3643 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
3644 let transformed = transformation.filter(&annotated);
3645 let display = repose_ui::textfield::index_for_xy_bytes_with_metrics(
3646 transformed.text.as_str(),
3647 wrap_width,
3648 x_px,
3649 y_px,
3650 metrics,
3651 );
3652 transformed.offset_mapping.transformed_to_original(display)
3653 } else {
3654 repose_ui::textfield::index_for_xy_bytes_with_metrics(
3655 &state.text,
3656 wrap_width,
3657 x_px,
3658 y_px,
3659 metrics,
3660 )
3661 }
3662}
3663
3664fn dispatch_scroll(
3666 frame: &Frame,
3667 pos: Vec2,
3668 delta: Vec2,
3669 scroll_capture: Option<u64>,
3670) -> (bool, Option<u64>) {
3671 let mut remaining = delta;
3672 let mut first_consumer: Option<u64> = None;
3673 if let Some(cid) = scroll_capture
3674 && let Some(cb) = frame
3675 .hit_regions
3676 .iter()
3677 .find(|h| h.id == cid)
3678 .and_then(|h| h.on_scroll.as_ref())
3679 {
3680 let leftover = cb(delta);
3681 if (delta.x - leftover.x).abs() > 0.001 || (delta.y - leftover.y).abs() > 0.001 {
3682 first_consumer = Some(cid);
3683 }
3684 remaining = leftover;
3685 if remaining.x.abs() <= 0.001 && remaining.y.abs() <= 0.001 {
3686 return (true, Some(cid));
3687 }
3688 }
3689
3690 let mut consumed_any = first_consumer.is_some();
3691 for id in repose_ui::hit_test_frame_regions(frame, pos) {
3692 if Some(id) == scroll_capture {
3693 continue;
3694 }
3695 let Some(hit) = frame.hit_regions.iter().find(|hit| hit.id == id) else {
3696 continue;
3697 };
3698 if remaining.x.abs() <= 0.001 && remaining.y.abs() <= 0.001 {
3699 break;
3700 }
3701 if let Some(cb) = &hit.on_scroll {
3702 let before = remaining;
3703 let leftover = cb(before);
3704 if (before.x - leftover.x).abs() > 0.001 || (before.y - leftover.y).abs() > 0.001 {
3705 consumed_any = true;
3706 if first_consumer.is_none() {
3707 first_consumer = Some(hit.id);
3708 }
3709 }
3710 remaining = leftover;
3711 }
3712 }
3713 if consumed_any {
3714 (true, first_consumer)
3715 } else {
3716 (false, None)
3717 }
3718}
3719
3720#[cfg(test)]
3721mod ime_tests {
3722 use super::*;
3723 use repose_core::input::ImeEvent;
3724
3725 const TF_ID: u64 = 100;
3726
3727 fn editable_frame(id: u64) -> Frame {
3728 let rect = repose_core::Rect {
3729 x: 0.0,
3730 y: 0.0,
3731 w: 200.0,
3732 h: 30.0,
3733 };
3734 Frame {
3735 scene: Default::default(),
3736 hit_regions: vec![HitRegion {
3737 id,
3738 rect,
3739 focusable: true,
3740 tf_state_key: Some(id),
3741 tf_multiline: false,
3742 tf_enabled: true,
3743 tf_read_only: false,
3744 ..Default::default()
3745 }],
3746 semantics_nodes: vec![repose_core::runtime::SemNode {
3747 id,
3748 role: repose_core::semantics::Role::TextField,
3749 label: Some("Field".into()),
3750 rect,
3751 focused: true,
3752 ..Default::default()
3753 }],
3754 focus_chain: vec![id],
3755 }
3756 }
3757
3758 fn focused_rt() -> ReposeRuntime {
3759 let mut rt = ReposeRuntime::new();
3760 rt.sched.focused = Some(TF_ID);
3761 rt.cache_frame(editable_frame(TF_ID));
3762 let _ = rt.ensure_textfield_state(TF_ID);
3763 rt
3764 }
3765
3766 #[test]
3767 fn preedit_cursor_uses_byte_offsets_for_non_ascii() {
3768 let mut rt = focused_rt();
3769 rt.handle_ime(&ImeEvent::Update {
3770 text: "あb".to_string(),
3771 cursor: Some((3, 3)),
3772 });
3773 let st = rt.textfield_states[&TF_ID].borrow();
3774 assert_eq!(st.text, "あb");
3775 assert_eq!(st.selection, 3..3);
3776 }
3777
3778 #[test]
3779 fn cancel_discards_preedit() {
3780 let mut rt = focused_rt();
3781 rt.handle_ime(&ImeEvent::Update {
3782 text: "あ".to_string(),
3783 cursor: None,
3784 });
3785 assert!(rt.ime_preedit);
3786 rt.handle_ime(&ImeEvent::Cancel);
3787 let st = rt.textfield_states[&TF_ID].borrow();
3788 assert_eq!(st.text, "");
3789 assert!(st.composition.is_none());
3790 assert!(!rt.ime_preedit);
3791 }
3792
3793 #[test]
3794 fn defocus_finish_keeps_text() {
3795 let mut rt = focused_rt();
3796 rt.handle_ime(&ImeEvent::Update {
3797 text: "x".to_string(),
3798 cursor: None,
3799 });
3800 rt.sched.focused = None;
3801 rt.handle_focus_lost();
3802 assert_eq!(rt.textfield_states[&TF_ID].borrow().text, "x");
3803 assert!(!rt.ime_preedit);
3804 }
3805}
3806
3807#[cfg(test)]
3808mod gamepad_tests {
3809 use super::*;
3810 use repose_core::input::{GamepadAxis, GamepadButton, GamepadEvent, GamepadId};
3811
3812 #[test]
3813 fn gamepad_state_mirrors_connection_and_inputs() {
3814 let mut rt = ReposeRuntime::new();
3815 rt.handle_gamepad(&GamepadEvent::Connected {
3816 id: GamepadId(0),
3817 name: "Pad".to_string(),
3818 });
3819 assert_eq!(rt.gamepads.len(), 1);
3820
3821 rt.handle_gamepad(&GamepadEvent::Button {
3822 id: GamepadId(0),
3823 button: GamepadButton::West,
3824 pressed: true,
3825 });
3826 assert!(rt.gamepads[&0].button(GamepadButton::West));
3827
3828 rt.handle_gamepad(&GamepadEvent::Axis {
3829 id: GamepadId(0),
3830 axis: GamepadAxis::LeftStickX,
3831 value: 0.5,
3832 });
3833 assert_eq!(rt.gamepads[&0].axis(GamepadAxis::LeftStickX), 0.5);
3834
3835 rt.handle_gamepad(&GamepadEvent::Button {
3836 id: GamepadId(0),
3837 button: GamepadButton::West,
3838 pressed: false,
3839 });
3840 assert!(!rt.gamepads[&0].button(GamepadButton::West));
3841
3842 rt.handle_gamepad(&GamepadEvent::Disconnected { id: GamepadId(0) });
3843 assert!(rt.gamepads.is_empty());
3844 }
3845
3846 #[test]
3847 fn rumble_requests_queue_and_drain() {
3848 let mut rt = ReposeRuntime::new();
3849 assert!(rt.take_rumble_requests().is_empty());
3850 rt.request_rumble(GamepadId(1), 2.0, -1.0, 150);
3851 rt.stop_rumble(GamepadId(1));
3852 let reqs = rt.take_rumble_requests();
3853 assert_eq!(reqs.len(), 2);
3854 assert_eq!(reqs[0], (1, 1.0, 0.0, 150));
3855 assert_eq!(reqs[1], (1, 0.0, 0.0, 0));
3856 assert!(rt.take_rumble_requests().is_empty());
3857 }
3858}