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, Dp, HitRegion, Interaction, RenderContext, Scene, Sp, Vec2, View, request_frame,
15 take_focus_request,
16};
17use repose_ui::textfield::{
18 TF_FONT_SP, TextFieldState, TextMeasureConfig, caret_xy_for_byte, measure_text,
19};
20use repose_ui::{Interactions, layout_and_paint};
21
22fn ensure_tf_state(
23 map: &mut HashMap<u64, Rc<RefCell<TextFieldState>>>,
24 key: u64,
25 seed: &str,
26) -> Rc<RefCell<TextFieldState>> {
27 map.entry(key)
28 .or_insert_with(|| {
29 Rc::new(RefCell::new(if seed.is_empty() {
30 TextFieldState::new()
31 } else {
32 TextFieldState::with_text(seed.to_string())
33 }))
34 })
35 .clone()
36}
37
38fn ensure_all_tf_states_from_frame(
39 map: &mut HashMap<u64, Rc<RefCell<TextFieldState>>>,
40 frame: &Frame,
41) {
42 for hit in &frame.hit_regions {
43 if let Some(key) = hit.tf_state_key {
44 let st = ensure_tf_state(map, key, hit.tf_value.as_str());
45 st.borrow_mut()
47 .apply_controlled_value(hit.tf_value.as_str());
48 }
49 }
50}
51
52fn is_tf_hit(f: &Frame, id: u64) -> bool {
53 f.hit_regions
54 .iter()
55 .any(|h| h.id == id && h.tf_state_key.is_some())
56 || is_textfield_in_frame(f, id)
57}
58
59#[derive(Clone, Default)]
61pub struct PlatformOutput {
62 pub cursor: Option<CursorIcon>,
64 pub ime_allowed: bool,
66 pub ime_cursor_area: Option<(f64, f64, f64, f64)>,
68 pub clipboard_text: Option<String>,
70
71 pub ime_purpose: repose_core::ImePurposeHint,
74 pub ime_auto_correct: bool,
75 pub ime_capitalization: repose_core::KeyboardCapitalization,
76 pub keyboard_type: repose_core::KeyboardType,
77
78 pub window_theme_dark: Option<bool>,
81}
82
83pub struct FrameOutput {
85 pub scene: Scene,
87 pub hit_regions: Vec<HitRegion>,
89 pub semantics_nodes: Vec<repose_core::runtime::SemNode>,
91 pub focus_chain: Vec<u64>,
93 pub platform: PlatformOutput,
95 pub wants_pointer: bool,
97 pub wants_keyboard: bool,
99}
100
101impl FrameOutput {
102 pub fn into_frame(self) -> Frame {
105 Frame {
106 scene: self.scene,
107 hit_regions: self.hit_regions,
108 semantics_nodes: self.semantics_nodes,
109 focus_chain: self.focus_chain,
110 }
111 }
112}
113
114pub struct PointerMoveResult {
116 pub cursor: Option<CursorIcon>,
118 pub hover_id: Option<u64>,
120}
121
122#[derive(Debug)]
124pub struct PointerButtonResult {
125 pub focused: Option<u64>,
127 pub capture_id: Option<u64>,
129 pub consumed: bool,
131 pub needs_a11y_announce: bool,
133 pub clicked_id: Option<u64>,
136}
137
138const LONG_PRESS_MS: u128 = 500;
140const DOUBLE_CLICK_MS: u128 = 300;
141const DOUBLE_TAP_MIN_MS: u128 = 40;
142const LONG_PRESS_SLOP_DP: f32 = 18.0;
143
144pub struct ReposeRuntime {
150 pub sched: Scheduler,
151 pub scale: f32,
152
153 pub modifiers: Modifiers,
154 pub mouse_pos_px: (f32, f32),
155 pub pointer_inside: bool,
157 pub hover_id: Option<u64>,
158 pub hover_ancestors: std::collections::HashSet<u64>,
159 hover_leave: HashMap<u64, (f32, f32, f32, f32, Rc<dyn Fn(PointerEvent)>)>,
163 pub capture_id: Option<u64>,
164 pub hit_path: Option<Vec<u64>>,
167 pub scroll_capture_id: Option<u64>,
169 last_scroll_at: Option<web_time::Instant>,
170 pub pressed_ids: HashSet<u64>,
171 pub ime_preedit: bool,
172 pub key_pressed_active: Option<u64>,
173 pub last_focus: Option<u64>,
174
175 last_up: Option<(u64, web_time::Instant, f32, f32)>,
176 last_down: Option<(u64, web_time::Instant)>,
180 double_candidate: Option<u64>,
184 long_press: Option<(u64, web_time::Instant, f32, f32)>,
185 key_long_press: Option<(u64, web_time::Instant, bool)>,
188 suppress_next_click: bool,
189 pending_click: Option<(u64, web_time::Instant, Rc<dyn Fn()>)>,
190
191 pub frame_cache: Option<Frame>,
192
193 cursor: Option<CursorIcon>,
194
195 pub textfield_states: HashMap<u64, Rc<RefCell<TextFieldState>>>,
196 pub gamepads: HashMap<u32, GamepadPad>,
199 pub pending_rumble: Vec<(u32, f32, f32, u32)>,
201}
202
203#[derive(Clone, Debug, Default)]
205pub struct GamepadPad {
206 pub name: String,
207 pub pressed: HashSet<GamepadButton>,
208 pub axes: HashMap<GamepadAxis, f32>,
209}
210
211impl GamepadPad {
212 pub fn button(&self, button: GamepadButton) -> bool {
213 self.pressed.contains(&button)
214 }
215
216 pub fn axis(&self, axis: GamepadAxis) -> f32 {
217 self.axes.get(&axis).copied().unwrap_or(0.0)
218 }
219}
220
221impl ReposeRuntime {
222 pub fn new() -> Self {
223 Self {
224 sched: Scheduler::new(),
225 scale: 1.0,
226 modifiers: Modifiers::default(),
227 mouse_pos_px: (0.0, 0.0),
228 pointer_inside: false,
229 hover_id: None,
230 hover_ancestors: std::collections::HashSet::new(),
231 hover_leave: HashMap::new(),
232 capture_id: None,
233 hit_path: None,
234 scroll_capture_id: None,
235 last_scroll_at: None,
236 pressed_ids: HashSet::new(),
237 ime_preedit: false,
238 key_pressed_active: None,
239 last_focus: None,
240 last_up: None,
241 last_down: None,
242 double_candidate: None,
243 long_press: None,
244 key_long_press: None,
245 suppress_next_click: false,
246 pending_click: None,
247 frame_cache: None,
248 cursor: None,
249 textfield_states: HashMap::new(),
250 gamepads: HashMap::new(),
251 pending_rumble: Vec::new(),
252 }
253 }
254
255 pub fn set_viewport(&mut self, width_px: u32, height_px: u32) {
257 self.sched.size = (width_px, height_px);
258 }
259
260 pub fn set_viewport_and_scale(&mut self, width_px: u32, height_px: u32, scale: f32) {
262 self.scale = scale;
263 self.sched.size = (width_px, height_px);
264 }
265
266 pub fn tick_animations(&self) {
268 repose_core::animation_driver::tick();
269 }
270
271 pub fn poll_gesture_timers(&mut self) {
272 self.poll_long_press();
273 self.flush_pending_click();
274 self.poll_key_long_press();
275 }
276
277 pub fn compose<F>(&mut self, root_fn: &mut F, render_ctx: &RenderContext) -> Frame
282 where
283 F: FnMut(&mut Scheduler, &RenderContext) -> View,
284 {
285 self.poll_long_press();
286 self.flush_pending_click();
287 self.poll_key_long_press();
288
289 let size = self.sched.size;
290 let rc = render_ctx.clone();
291 let mut compose_once = |this: &mut Self| {
292 let mut inner = |s: &mut Scheduler| (root_fn)(s, &rc);
293 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
294 compose_frame_inner_with_ancestors(
295 &mut this.sched,
296 &mut inner,
297 this.scale,
298 size,
299 this.hover_id,
300 &this.hover_ancestors,
301 &this.pressed_ids,
302 &this.textfield_states,
303 )
304 })) {
305 Ok(frame) => frame,
306 Err(_) => {
307 log::error!("compose panicked; presenting last good frame");
308 this.frame_cache.clone().unwrap_or_else(|| Frame {
309 scene: Default::default(),
310 hit_regions: Vec::new(),
311 semantics_nodes: Vec::new(),
312 focus_chain: Vec::new(),
313 })
314 }
315 }
316 };
317
318 let frame = compose_once(self);
319
320 let hover_before = self.hover_id;
324 self.reconcile_hover_from_mouse_pos(&frame);
325 if self.hover_id != hover_before {
326 self.hover_leave.clear();
329 for h in &frame.hit_regions {
330 if let Some(cb) = &h.on_pointer_leave {
331 self.hover_leave
332 .insert(h.id, (h.rect.x, h.rect.y, h.rect.w, h.rect.h, cb.clone()));
333 }
334 }
335 return compose_once(self);
337 }
338 frame
339 }
340
341 pub fn frame(
343 &mut self,
344 mut root_fn: impl FnMut(&mut Scheduler, &RenderContext) -> View,
345 render_ctx: &RenderContext,
346 ) -> FrameOutput {
347 let captured = Rc::new(RefCell::new(None::<String>));
348 let hook = captured.clone();
349 repose_core::clipboard::set_clipboard_observer(Box::new(move |text| {
350 *hook.borrow_mut() = Some(text.to_string());
351 }));
352
353 let f = self.compose(&mut root_fn, render_ctx);
354
355 repose_core::clipboard::clear_clipboard_observer();
356 let clipboard_text = captured.borrow_mut().take();
357
358 let wants_pointer = self.hover_id.is_some() || self.capture_id.is_some();
359
360 let ime_allowed = self.sched.focused.is_some_and(|fid| {
361 f.semantics_nodes
362 .iter()
363 .any(|n| n.id == fid && n.role == repose_core::semantics::Role::TextField)
364 });
365
366 let focused_hit = self
367 .sched
368 .focused
369 .and_then(|fid| f.hit_regions.iter().find(|h| h.id == fid));
370
371 let ime_cursor_area = if ime_allowed {
372 focused_hit.map(|hit| {
373 let sf = self.scale as f64;
374 (
375 hit.rect.x as f64 / sf,
376 hit.rect.y as f64 / sf,
377 hit.rect.w as f64 / sf,
378 hit.rect.h as f64 / sf,
379 )
380 })
381 } else {
382 None
383 };
384
385 let focused_is_textfield = ime_allowed
386 || self.sched.focused.is_some_and(|fid| {
387 f.hit_regions
388 .iter()
389 .any(|h| h.id == fid && h.tf_state_key.is_some())
390 });
391 let wants_keyboard = focused_is_textfield || self.ime_preedit;
392
393 let (ime_purpose, ime_auto_correct, ime_capitalization, keyboard_type) =
394 match (ime_allowed, focused_hit) {
395 (true, Some(hit)) => (
396 hit.keyboard_type.ime_purpose_hint(),
397 hit.auto_correct.unwrap_or(true),
398 hit.capitalization,
399 hit.keyboard_type,
400 ),
401 _ => (
402 repose_core::ImePurposeHint::Normal,
403 true,
404 repose_core::KeyboardCapitalization::Unspecified,
405 repose_core::KeyboardType::Unspecified,
406 ),
407 };
408
409 let platform = PlatformOutput {
410 cursor: self.take_cursor_suggestion(),
411 ime_allowed,
412 ime_cursor_area,
413 clipboard_text,
414 ime_purpose,
415 ime_auto_correct,
416 ime_capitalization,
417 keyboard_type,
418 window_theme_dark: Some(repose_core::locals::theme().is_dark()),
419 };
420 FrameOutput {
421 scene: f.scene,
422 hit_regions: f.hit_regions,
423 semantics_nodes: f.semantics_nodes,
424 focus_chain: f.focus_chain,
425 platform,
426 wants_pointer,
427 wants_keyboard,
428 }
429 }
430
431 pub fn cache_frame(&mut self, frame: Frame) {
433 self.hover_leave.clear();
434 for h in &frame.hit_regions {
435 if let Some(cb) = &h.on_pointer_leave {
436 self.hover_leave
437 .insert(h.id, (h.rect.x, h.rect.y, h.rect.w, h.rect.h, cb.clone()));
438 }
439 }
440 self.frame_cache = Some(frame);
441 }
442
443 pub fn after_compose(&mut self, frame: &Frame, scale: f32) {
448 ensure_all_tf_states_from_frame(&mut self.textfield_states, frame);
449 self.ensure_focused_state_in_frame(frame);
450 self.reconcile_hover_from_mouse_pos(frame);
451 repose_core::dnd::set_dnd_frame(Some(frame.clone()));
452 repose_core::dnd::set_dnd_scale(scale);
453 }
454
455 pub fn ensure_focused_textfield_state(&mut self) {
458 let Some(f) = self.frame_cache.clone() else {
459 return;
460 };
461 self.ensure_focused_state_in_frame(&f);
462 }
463
464 fn ensure_focused_state_in_frame(&mut self, frame: &Frame) {
468 let Some(fid) = self.sched.focused else {
469 return;
470 };
471 if let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
472 && let Some(key) = hit.tf_state_key
473 {
474 let st = ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
475 st.borrow_mut().apply_controlled_value(&hit.tf_value);
476 }
477 }
478
479 pub fn cache_from_output(&mut self, out: &FrameOutput) {
483 let frame = Frame {
484 scene: out.scene.clone(),
485 hit_regions: out.hit_regions.clone(),
486 semantics_nodes: out.semantics_nodes.clone(),
487 focus_chain: out.focus_chain.clone(),
488 };
489 self.after_compose(&frame, self.scale);
490 self.cache_frame(frame);
491 }
492
493 pub fn compose_frame_output<F>(
497 &mut self,
498 root: &mut F,
499 render_ctx: &RenderContext,
500 ) -> FrameOutput
501 where
502 F: FnMut(&mut Scheduler, &RenderContext) -> View,
503 {
504 self.tick_animations();
505 let out = self.frame(root, render_ctx);
506 self.cache_from_output(&out);
507 out
508 }
509
510 fn dispatch_pointer_to_path(&self, kind: PointerEventKind, pos: Vec2, path: &[u64]) {
511 let Some(f) = &self.frame_cache else {
512 return;
513 };
514 let base = PointerEvent::new(
515 PointerId(0),
516 PointerKind::Mouse,
517 kind,
518 pos,
519 1.0,
520 self.modifiers,
521 );
522 for &id in path {
523 let Some(h) = f.hit_regions.iter().find(|h| h.id == id) else {
524 continue;
525 };
526 let cb = match kind {
527 PointerEventKind::Down(_) => &h.on_pointer_down,
528 PointerEventKind::Up(_) => &h.on_pointer_up,
529 PointerEventKind::Move => &h.on_pointer_move,
530 PointerEventKind::Cancel => &h.on_pointer_cancel,
531 PointerEventKind::Enter | PointerEventKind::Leave => continue,
532 };
533 let Some(cb) = cb else {
534 continue;
535 };
536 let mut ev = base.clone();
537 ev.origin = Vec2 {
538 x: h.rect.x,
539 y: h.rect.y,
540 };
541 ev.position = pos - ev.origin;
542 cb(ev);
543 if base.is_consumed() {
544 break;
545 }
546 }
547 }
548
549 pub fn handle_pointer_move(&mut self, pos: Vec2) -> PointerMoveResult {
551 self.mouse_pos_px = (pos.x, pos.y);
552
553 if dnd::handle_drag_action(&DragAction::Move {
554 position: pos,
555 modifiers: self.modifiers,
556 }) {
557 request_frame();
558 return PointerMoveResult {
559 cursor: if dnd::is_dragging() {
560 Some(CursorIcon::Grabbing)
561 } else {
562 self.cursor
563 },
564 hover_id: self.hover_id,
565 };
566 }
567
568 let Some(f) = &self.frame_cache else {
569 return PointerMoveResult {
570 cursor: None,
571 hover_id: None,
572 };
573 };
574
575 if let Some((_, _, x0, y0)) = self.long_press {
576 let slop = LONG_PRESS_SLOP_DP * self.scale;
577 let dx = pos.x - x0;
578 let dy = pos.y - y0;
579 if dx * dx + dy * dy > slop * slop {
580 self.long_press = None;
581 }
582 }
583
584 if self.long_press.is_some()
586 && let Some(lid) = self.long_press.map(|(id, _, _, _)| id)
587 && f.hit_regions
588 .iter()
589 .find(|h| h.id == lid)
590 .is_none_or(|h| !h.rect.contains(pos))
591 {
592 self.long_press = None;
593 }
594
595 if let Some(cid) = self.capture_id
597 && is_tf_hit(f, cid)
598 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
599 {
600 let key = tf_key_of(f, cid);
601 if let Some(st_rc) = self.textfield_states.get(&key) {
602 let mut st = st_rc.borrow_mut();
603 let (ox, oy) = hit.tf_content_origin.unwrap_or((hit.rect.x, hit.rect.y));
604 let content_x = (pos.x - ox + st.scroll_offset).max(0.0);
605 let content_y = (pos.y - oy + st.scroll_offset_y).max(0.0);
606 let font_size_sp = if hit.tf_font_size != Sp::ZERO {
607 hit.tf_font_size
608 } else {
609 TF_FONT_SP
610 };
611 let font_px = font_size_sp.to_px().0;
612 let wrap_w = st.inner_width.max(1.0);
613 let idx = if hit.tf_multiline {
614 index_for_xy_bytes_vt(&st, font_px, wrap_w, content_x, content_y)
615 } else {
616 index_for_x_bytes_vt(&st, font_px, content_x)
617 };
618 st.drag_to(idx);
619 }
620 }
621
622 let top = f
623 .hit_regions
624 .iter()
625 .rev()
626 .find(|h| !h.disabled && h.rect.contains(pos));
627
628 self.cursor = top.and_then(|h| h.cursor).or(Some(CursorIcon::Default));
629
630 let new_hover = top.map(|h| h.id);
631
632 let old_chain = hover_chain_for(Some(f), self.hover_id);
633 let new_chain = hover_chain_for(Some(f), new_hover);
634 if new_chain != old_chain {
635 dispatch_hover_change_bubbled(
636 Some(f),
637 &self.hover_leave,
638 &mut self.hover_id,
639 &mut self.hover_ancestors,
640 new_hover,
641 pos,
642 self.modifiers,
643 );
644 request_frame();
645 }
646
647 if let Some(path) = &self.hit_path {
648 self.dispatch_pointer_to_path(PointerEventKind::Move, pos, path);
649 } else if let Some(h) = top
650 && let Some(cb) = &h.on_pointer_move
651 {
652 let mut pe = PointerEvent::new(
653 PointerId(0),
654 PointerKind::Mouse,
655 PointerEventKind::Move,
656 pos,
657 1.0,
658 self.modifiers,
659 );
660 pe.origin = Vec2 {
661 x: h.rect.x,
662 y: h.rect.y,
663 };
664 pe.position = pe.position - pe.origin;
665 cb(pe);
666 }
667
668 PointerMoveResult {
669 cursor: self.cursor,
670 hover_id: self.hover_id,
671 }
672 }
673
674 pub fn handle_pointer_press(
676 &mut self,
677 pos: Vec2,
678 button: PointerButton,
679 ) -> PointerButtonResult {
680 self.mouse_pos_px = (pos.x, pos.y);
681 let _ = repose_core::request_input_mode(repose_core::InputMode::Touch);
682
683 let Some(f) = &self.frame_cache else {
684 return PointerButtonResult {
685 focused: None,
686 capture_id: None,
687 consumed: false,
688 needs_a11y_announce: false,
689 clicked_id: None,
690 };
691 };
692
693 let mut result = PointerButtonResult {
694 focused: None,
695 capture_id: None,
696 consumed: false,
697 needs_a11y_announce: false,
698 clicked_id: None,
699 };
700
701 if let Some(hit) = f
702 .hit_regions
703 .iter()
704 .rev()
705 .find(|h| !h.disabled && h.rect.contains(pos))
706 {
707 let mut path: Vec<u64> = vec![hit.id];
708 let mut cur = hit.parent;
709 while let Some(pid) = cur {
710 path.push(pid);
711 cur = f
712 .hit_regions
713 .iter()
714 .find(|h| h.id == pid)
715 .and_then(|h| h.parent);
716 }
717 self.hit_path = Some(path.clone());
718
719 dnd::handle_drag_action(&DragAction::Press {
720 position: pos,
721 capture_id: hit.id,
722 kind: PointerKind::Mouse,
723 modifiers: self.modifiers,
724 });
725
726 self.capture_id = Some(hit.id);
727 result.capture_id = Some(hit.id);
728 result.consumed = true;
729
730 self.last_down = Some((hit.id, web_time::Instant::now()));
734 self.double_candidate = if hit.on_double_click.is_some()
738 && self.last_up.is_some_and(|(pid, t0, _, _)| {
739 pid == hit.id
740 && self.last_down.is_some_and(|(did, dt)| {
741 did == hit.id
742 && dt.duration_since(t0).as_millis() >= DOUBLE_TAP_MIN_MS
743 && dt.duration_since(t0).as_millis() <= DOUBLE_CLICK_MS
744 })
745 }) {
746 Some(hit.id)
747 } else {
748 None
749 };
750 if self.double_candidate.is_some() {
751 self.pending_click = None;
752 }
753
754 if let PointerButton::Primary = button {
755 self.long_press = if hit.on_long_click.is_some() {
756 Some((hit.id, web_time::Instant::now(), pos.x, pos.y))
757 } else {
758 None
759 };
760 self.suppress_next_click = false;
761 }
762
763 if hit.tf_state_key.is_some() || is_textfield_in_frame(f, hit.id) {
764 let key = tf_key_of(f, hit.id);
765 let seed = hit.tf_value.as_str();
766 let st_rc = ensure_tf_state(&mut self.textfield_states, key, seed);
767 {
768 let mut st = st_rc.borrow_mut();
769 st.apply_controlled_value(seed);
771
772 if st.inner_width <= 0.0 {
773 let w = hit
774 .tf_content_origin
775 .map(|_| hit.rect.w)
776 .unwrap_or(hit.rect.w)
777 .max(1.0);
778 st.set_inner_width(w);
779 st.set_inner_height(hit.rect.h.max(1.0));
780 }
781
782 let (ox, oy) = hit.tf_content_origin.unwrap_or((hit.rect.x, hit.rect.y));
783 let content_x = (pos.x - ox + st.scroll_offset).max(0.0);
784 let content_y = (pos.y - oy + st.scroll_offset_y).max(0.0);
785 let font_size_sp = if hit.tf_font_size != Sp::ZERO {
786 hit.tf_font_size
787 } else {
788 TF_FONT_SP
789 };
790 let font_px = font_size_sp.to_px().0;
791 let wrap_w = st.inner_width.max(1.0);
792
793 let idx = if hit.tf_multiline {
794 index_for_xy_bytes_vt(&st, font_px, wrap_w, content_x, content_y)
795 } else {
796 index_for_x_bytes_vt(&st, font_px, content_x)
797 };
798 st.handle_pointer_down(idx, (pos.x, pos.y), self.modifiers.shift);
799 }
801 }
802
803 self.pressed_ids.insert(hit.id);
804
805 if hit.focusable {
806 self.sched.focused = Some(hit.id);
807 result.focused = Some(hit.id);
808 if hit.tf_state_key.is_some() {
809 let key = tf_key_of(f, hit.id);
810 let st =
811 ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
812 let mut s = st.borrow_mut();
813 s.apply_controlled_value(&hit.tf_value);
814 s.reset_caret_blink();
815 }
816 }
817
818 self.dispatch_pointer_to_path(PointerEventKind::Down(button), pos, &path);
819
820 request_frame();
821 } else {
822 self.hit_path = None;
823 if self.ime_preedit {
824 self.ime_preedit = false;
825 }
826 self.sched.focused = None;
827 request_frame();
828 }
829
830 result
831 }
832
833 pub fn handle_pointer_release(
835 &mut self,
836 pos: Vec2,
837 button: PointerButton,
838 ) -> PointerButtonResult {
839 self.mouse_pos_px = (pos.x, pos.y);
840 let mut result = PointerButtonResult {
841 focused: self.sched.focused,
842 capture_id: self.capture_id,
843 consumed: false,
844 needs_a11y_announce: false,
845 clicked_id: None,
846 };
847
848 if dnd::handle_drag_action(&DragAction::Release {
849 position: pos,
850 modifiers: self.modifiers,
851 }) {
852 self.capture_id = None;
853 self.hit_path = None;
854 self.pressed_ids.clear();
855 request_frame();
856 result.consumed = true;
857 return result;
858 }
859
860 self.pressed_ids.clear();
861
862 let Some(f) = &self.frame_cache else {
863 self.capture_id = None;
864 self.hit_path = None;
865 return result;
866 };
867
868 if let Some(path) = &self.hit_path {
869 self.dispatch_pointer_to_path(PointerEventKind::Up(button), pos, path);
870 result.consumed = true;
871 }
872
873 if let Some((lid, t0, _, _)) = self.long_press.take()
875 && Some(lid) == self.capture_id
876 && t0.elapsed().as_millis() >= LONG_PRESS_MS
877 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == lid && !h.disabled)
878 && let Some(cb) = &hit.on_long_click
879 {
880 cb();
881 self.suppress_next_click = true;
882 self.pending_click = None;
883 result.clicked_id = Some(lid);
884 result.needs_a11y_announce = true;
885 result.consumed = true;
886 }
887
888 if self.double_candidate.is_none()
889 && !self.suppress_next_click
890 && let Some(cid) = self.capture_id
891 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid && !h.disabled)
892 && hit.rect.contains(pos)
893 {
894 let now = web_time::Instant::now();
895 if hit.on_double_click.is_some() {
898 if let Some(cb) = hit.on_click.clone() {
899 self.pending_click = Some((cid, now, cb));
900 }
901 self.last_up = Some((cid, now, pos.x, pos.y));
902 result.consumed = true;
903 request_frame(); } else {
905 if let Some(cb) = &hit.on_click {
906 cb();
907 }
908 self.last_up = Some((cid, now, pos.x, pos.y));
909 result.clicked_id = Some(cid);
910 result.needs_a11y_announce = true;
911 result.consumed = true;
912 }
913 }
914 self.suppress_next_click = false;
915
916 if let Some(dc) = self.double_candidate.take() {
920 self.pending_click = None;
921 self.last_up = None;
922 self.last_down = None;
923 if self.capture_id == Some(dc)
924 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == dc && !h.disabled)
925 && hit.rect.contains(pos)
926 {
927 if let Some(cb) = &hit.on_double_click {
928 cb();
929 }
930 result.clicked_id = Some(dc);
931 result.needs_a11y_announce = true;
932 result.consumed = true;
933 } else if self.capture_id == Some(dc)
934 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == dc && !h.disabled)
935 {
936 if let Some(cb) = &hit.on_click {
938 cb();
939 }
940 result.clicked_id = Some(dc);
941 result.needs_a11y_announce = true;
942 result.consumed = true;
943 }
944 }
945
946 if let Some(cid) = self.capture_id
948 && is_tf_hit(f, cid)
949 {
950 let key = tf_key_of(f, cid);
951 if let Some(state_rc) = self.textfield_states.get(&key) {
952 state_rc.borrow_mut().end_drag();
953 }
954 }
955
956 self.capture_id = None;
957 self.hit_path = None;
958 request_frame();
959 result
960 }
961
962 pub fn handle_pointer_cancel(&mut self) {
964 self.long_press = None;
965 self.last_up = None;
966 dnd::handle_drag_action(&DragAction::Cancel);
967 let pos = Vec2 {
968 x: self.mouse_pos_px.0,
969 y: self.mouse_pos_px.1,
970 };
971 dispatch_hover_change_bubbled(
972 self.frame_cache.as_ref(),
973 &self.hover_leave,
974 &mut self.hover_id,
975 &mut self.hover_ancestors,
976 None,
977 pos,
978 self.modifiers,
979 );
980 if let Some(path) = &self.hit_path {
981 self.dispatch_pointer_to_path(PointerEventKind::Cancel, pos, path);
982 }
983 self.reset_pointer_state();
984 }
985
986 pub fn clear_hover(&mut self) {
988 if self.hover_id.is_none() && self.hover_ancestors.is_empty() {
989 return;
990 }
991 let pos = Vec2 {
992 x: self.mouse_pos_px.0,
993 y: self.mouse_pos_px.1,
994 };
995 dispatch_hover_change_bubbled(
996 self.frame_cache.as_ref(),
997 &self.hover_leave,
998 &mut self.hover_id,
999 &mut self.hover_ancestors,
1000 None,
1001 pos,
1002 self.modifiers,
1003 );
1004 }
1005
1006 pub fn reconcile_hover_from_mouse_pos(&mut self, new_frame: &Frame) {
1008 let pos = Vec2 {
1009 x: self.mouse_pos_px.0,
1010 y: self.mouse_pos_px.1,
1011 };
1012
1013 if let Some(prev_id) = self.hover_id
1016 && !new_frame.hit_regions.iter().any(|h| h.id == prev_id)
1017 {
1018 dispatch_hover_change_bubbled(
1019 Some(new_frame),
1020 &self.hover_leave,
1021 &mut self.hover_id,
1022 &mut self.hover_ancestors,
1023 None,
1024 pos,
1025 self.modifiers,
1026 );
1027 }
1028
1029 if !self.pointer_inside {
1030 if self.hover_id.is_some() || !self.hover_ancestors.is_empty() {
1031 dispatch_hover_change_bubbled(
1032 Some(new_frame),
1033 &self.hover_leave,
1034 &mut self.hover_id,
1035 &mut self.hover_ancestors,
1036 None,
1037 pos,
1038 self.modifiers,
1039 );
1040 request_frame();
1041 }
1042 return;
1043 }
1044
1045 let new_hover = new_frame
1046 .hit_regions
1047 .iter()
1048 .rev()
1049 .find(|h| !h.disabled && h.rect.contains(pos))
1050 .map(|h| h.id);
1051
1052 self.cursor = if dnd::is_dragging() {
1053 Some(CursorIcon::Grabbing)
1054 } else {
1055 new_hover
1056 .and_then(|id| new_frame.hit_regions.iter().find(|h| h.id == id))
1057 .and_then(|h| h.cursor)
1058 .or(Some(CursorIcon::Default))
1059 };
1060
1061 let new_chain = hover_chain_for(Some(new_frame), new_hover);
1062 let old_chain = hover_chain_for(Some(new_frame), self.hover_id);
1063 if new_chain == old_chain {
1064 return;
1065 }
1066
1067 dispatch_hover_change_bubbled(
1068 Some(new_frame),
1069 &self.hover_leave,
1070 &mut self.hover_id,
1071 &mut self.hover_ancestors,
1072 new_hover,
1073 pos,
1074 self.modifiers,
1075 );
1076 request_frame();
1077 }
1078
1079 fn reset_pointer_state(&mut self) {
1080 self.capture_id = None;
1081 self.hit_path = None;
1082 self.pressed_ids.clear();
1083 self.pending_click = None;
1084 self.last_down = None;
1085 self.double_candidate = None;
1086 self.key_long_press = None;
1087 self.suppress_next_click = false;
1088 }
1089
1090 fn flush_pending_click(&mut self) {
1091 let Some((id, t0, cb)) = self.pending_click.take() else {
1092 return;
1093 };
1094 if t0.elapsed().as_millis() >= DOUBLE_CLICK_MS {
1095 cb();
1096 request_frame();
1097 } else {
1098 self.pending_click = Some((id, t0, cb));
1099 request_frame();
1100 }
1101 }
1102
1103 fn poll_long_press(&mut self) {
1104 let Some(f) = self.frame_cache.clone() else {
1105 return;
1106 };
1107 let Some((lid, t0, _, _)) = self.long_press else {
1108 return;
1109 };
1110 if t0.elapsed().as_millis() < LONG_PRESS_MS {
1111 request_frame();
1112 return;
1113 }
1114 if self.capture_id != Some(lid) {
1117 self.long_press = None;
1118 return;
1119 }
1120 let (mx, my) = self.mouse_pos_px;
1121 let in_bounds = f
1122 .hit_regions
1123 .iter()
1124 .find(|h| h.id == lid)
1125 .is_some_and(|h| h.rect.contains(Vec2 { x: mx, y: my }));
1126 if !in_bounds {
1127 self.long_press = None;
1128 return;
1129 }
1130 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == lid && !h.disabled)
1131 && let Some(cb) = &hit.on_long_click
1132 {
1133 self.long_press = None;
1134 self.suppress_next_click = true;
1135 self.pending_click = None;
1136 self.last_up = None;
1137 cb();
1138 request_frame();
1139 } else {
1140 self.long_press = None;
1141 }
1142 }
1143
1144 fn poll_key_long_press(&mut self) {
1146 let Some(f) = self.frame_cache.clone() else {
1147 return;
1148 };
1149 let Some((kid, t0, fired)) = self.key_long_press else {
1150 return;
1151 };
1152 if t0.elapsed().as_millis() < LONG_PRESS_MS {
1153 request_frame();
1154 return;
1155 }
1156 if !fired {
1157 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == kid && !h.disabled)
1158 && let Some(cb) = &hit.on_long_click
1159 {
1160 cb();
1161 }
1162 self.key_long_press = Some((kid, t0, true));
1163 request_frame();
1164 }
1165 }
1166
1167 pub fn handle_scroll(&mut self, delta: Vec2) -> bool {
1169 let Some(f) = &self.frame_cache else {
1170 return false;
1171 };
1172
1173 let now = web_time::Instant::now();
1174 if let Some(last) = self.last_scroll_at
1175 && now.duration_since(last).as_millis() > 250
1176 {
1177 self.scroll_capture_id = None;
1178 }
1179 self.last_scroll_at = Some(now);
1180
1181 let pos = Vec2 {
1182 x: self.mouse_pos_px.0,
1183 y: self.mouse_pos_px.1,
1184 };
1185 let (consumed, cap) = dispatch_scroll(f, pos, delta, self.scroll_capture_id);
1186 self.scroll_capture_id = cap;
1187 if consumed {
1188 request_frame();
1189 }
1190 consumed
1191 }
1192
1193 pub fn handle_key(&mut self, event: &KeyEvent) -> bool {
1195 if event.event_type == KeyEventType::Down {
1196 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
1197 }
1198
1199 let Some(frame) = self.frame_cache.clone() else {
1200 return false;
1201 };
1202 let f = &frame;
1203
1204 if event.event_type == KeyEventType::Down && !event.is_repeat && event.key == Key::Escape {
1208 if dnd::handle_drag_action(&DragAction::Cancel) {
1209 request_frame();
1210 return true;
1211 }
1212 if self.dispatch_focus_key_event(f, event) {
1214 request_frame();
1215 return true;
1216 }
1217 return false;
1218 }
1219
1220 let consumed = self.dispatch_focus_key_event(f, event);
1222 if consumed {
1223 request_frame();
1224 return true;
1225 }
1226
1227 if event.event_type == KeyEventType::Down
1229 && !event.is_repeat
1230 && let Some(action) = repose_core::shortcuts::resolve_action(
1231 repose_core::shortcuts::KeyChord::new(event.key.clone(), self.modifiers),
1232 )
1233 {
1234 if self.dispatch_action(action.clone()) {
1236 return true;
1237 }
1238 }
1239
1240 if let Some(fid) = self.sched.focused {
1242 let is_tf = f
1243 .semantics_nodes
1244 .iter()
1245 .any(|n| n.id == fid && n.role == repose_core::semantics::Role::TextField);
1246 if !is_tf {
1247 if event.event_type == KeyEventType::Down && !event.is_repeat {
1248 if event.key == Key::Space || event.key == Key::Enter {
1249 let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) else {
1250 return false;
1251 };
1252 if hit.on_click.is_none()
1253 && hit.on_long_click.is_none()
1254 && hit.on_double_click.is_none()
1255 {
1256 return false; }
1258 self.pressed_ids.insert(fid);
1259 self.key_pressed_active = Some(fid);
1260 self.key_long_press = if hit.on_long_click.is_some() {
1261 Some((fid, web_time::Instant::now(), false))
1262 } else {
1263 None
1264 };
1265
1266 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1267 && let Some(src) = &hit.interaction_source
1268 {
1269 let local = Vec2 {
1270 x: hit.rect.w * 0.5,
1271 y: hit.rect.h * 0.5,
1272 };
1273 src.to_mutable().emit(Interaction::new_press(local));
1274 }
1275
1276 request_frame();
1277 return true;
1278 }
1279 } else if event.event_type == KeyEventType::Up
1280 && let Some(active_id) = self.key_pressed_active
1281 && (event.key == Key::Space || event.key == Key::Enter)
1282 {
1283 self.pressed_ids.remove(&active_id);
1284 self.key_pressed_active = None;
1285
1286 let long_fired = self
1287 .key_long_press
1288 .take()
1289 .map(|(_, _, fired)| fired)
1290 .unwrap_or(false);
1291
1292 if let Some(hit) = f
1293 .hit_regions
1294 .iter()
1295 .find(|h| h.id == active_id && !h.disabled)
1296 {
1297 if let Some(src) = &hit.interaction_source {
1298 let pid = src.collect_last_press_id().unwrap_or(0);
1299 src.to_mutable().emit(Interaction::Release(pid));
1300 }
1301 if !long_fired && let Some(cb) = &hit.on_click {
1302 cb();
1303 }
1304 }
1305 request_frame();
1306 return true;
1307 }
1308 }
1309 }
1310
1311 if event.event_type == KeyEventType::Down
1313 && !event.is_repeat
1314 && event.key == Key::Enter
1315 && let Some(fid) = self.sched.focused
1316 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1317 {
1318 let is_multiline = hit.tf_multiline;
1319 let should_submit = if is_multiline {
1320 self.modifiers.ctrl || self.modifiers.meta
1321 } else {
1322 true
1323 };
1324 if should_submit {
1325 if let Some(on_submit) = &hit.on_text_submit {
1326 let key = tf_key_of(f, fid);
1327 if let Some(state_rc) = self.textfield_states.get(&key) {
1328 let text = state_rc.borrow().text.clone();
1329 on_submit(text);
1330 request_frame();
1331 return true;
1332 }
1333 }
1334 } else {
1335 let key = tf_key_of(f, fid);
1337 if !is_tf_editable(f, fid) {
1338 return true;
1339 }
1340 if let Some(state_rc) = self.textfield_states.get(&key) {
1341 let mut st = state_rc.borrow_mut();
1342 st.insert_text("\n");
1343 let new_text = st.text.clone();
1344 notify_text_change(f, fid, new_text);
1345 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1346 request_frame();
1347 return true;
1348 }
1349 }
1350 }
1351
1352 if event.event_type == KeyEventType::Down {
1354 if let Some(fid) = self.sched.focused {
1355 let key = tf_key_of(f, fid);
1356 if let Some(state_rc) = self.textfield_states.get(&key) {
1357 let mut state = state_rc.borrow_mut();
1358 match event.key {
1359 Key::Backspace => {
1360 if !is_tf_editable(f, fid) {
1361 return true;
1362 }
1363 state.delete_backward();
1364 let new_text = state.text.clone();
1365 notify_text_change(f, fid, new_text);
1366 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1367 request_frame();
1368 return true;
1369 }
1370 Key::Delete => {
1371 if !is_tf_editable(f, fid) {
1372 return true;
1373 }
1374 state.delete_forward();
1375 let new_text = state.text.clone();
1376 notify_text_change(f, fid, new_text);
1377 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1378 request_frame();
1379 return true;
1380 }
1381 Key::ArrowLeft => {
1382 state.move_cursor(-1, self.modifiers.shift);
1383 state.preferred_x_px = None;
1384 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1385 request_frame();
1386 return true;
1387 }
1388 Key::ArrowRight => {
1389 state.move_cursor(1, self.modifiers.shift);
1390 state.preferred_x_px = None;
1391 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1392 request_frame();
1393 return true;
1394 }
1395 Key::ArrowUp => {
1396 if is_multiline_id(f, fid)
1397 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1398 {
1399 let font_size_sp = if hit.tf_font_size != Sp::ZERO {
1400 hit.tf_font_size
1401 } else {
1402 TF_FONT_SP
1403 };
1404 let font_px = font_size_sp.to_px().0;
1405 let cur = state.caret_index();
1406 let (new_pos, px) = repose_ui::textfield::move_caret_vertical(
1407 &state.text,
1408 font_px,
1409 hit.rect.w,
1410 cur,
1411 -1,
1412 state.preferred_x_px,
1413 );
1414 if self.modifiers.shift {
1415 state.selection.end = new_pos;
1416 } else {
1417 state.selection = new_pos..new_pos;
1418 }
1419 state.preferred_x_px = Some(px);
1420 let (cx, cy, _) = caret_xy_for_byte(
1421 &state.text,
1422 font_px,
1423 hit.rect.w,
1424 state.caret_index(),
1425 );
1426 let iw = state.inner_width;
1427 let ih = state.inner_height;
1428 state.ensure_caret_visible_xy(cx, cy, iw, ih, Dp(2.0).to_px().0);
1429 request_frame();
1430 return true;
1431 }
1432 }
1433 Key::ArrowDown => {
1434 if is_multiline_id(f, fid)
1435 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1436 {
1437 let font_size_sp = if hit.tf_font_size != Sp::ZERO {
1438 hit.tf_font_size
1439 } else {
1440 TF_FONT_SP
1441 };
1442 let font_px = font_size_sp.to_px().0;
1443 let cur = state.caret_index();
1444 let (new_pos, px) = repose_ui::textfield::move_caret_vertical(
1445 &state.text,
1446 font_px,
1447 hit.rect.w,
1448 cur,
1449 1,
1450 state.preferred_x_px,
1451 );
1452 if self.modifiers.shift {
1453 state.selection.end = new_pos;
1454 } else {
1455 state.selection = new_pos..new_pos;
1456 }
1457 state.preferred_x_px = Some(px);
1458 let (cx, cy, _) = caret_xy_for_byte(
1459 &state.text,
1460 font_px,
1461 hit.rect.w,
1462 state.caret_index(),
1463 );
1464 let iw = state.inner_width;
1465 let ih = state.inner_height;
1466 state.ensure_caret_visible_xy(cx, cy, iw, ih, Dp(2.0).to_px().0);
1467 request_frame();
1468 return true;
1469 }
1470 }
1471 Key::Home => {
1472 state.selection = 0..0;
1473 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1474 request_frame();
1475 return true;
1476 }
1477 Key::End => {
1478 let end = state.text.len();
1479 state.selection = end..end;
1480 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1481 request_frame();
1482 return true;
1483 }
1484 _ => {}
1485 }
1486 }
1487 }
1488
1489 if !self.ime_preedit
1491 && !self.modifiers.ctrl
1492 && !self.modifiers.alt
1493 && !self.modifiers.meta
1494 && let Key::Character(c) = event.key
1495 && !c.is_control()
1496 && c != '\n'
1497 && c != '\r'
1498 && let Some(fid) = self.sched.focused
1499 {
1500 let key = tf_key_of(f, fid);
1501 if !is_tf_editable(f, fid) {
1502 return true;
1503 }
1504 if let Some(state_rc) = self.textfield_states.get(&key) {
1505 let mut st = state_rc.borrow_mut();
1506 let text = c.to_string();
1507 st.insert_text(&text);
1508 notify_text_change(f, fid, st.text.clone());
1509 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1510 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1511 }
1512 request_frame();
1513 return true;
1514 }
1515 }
1516 }
1517
1518 false
1519 }
1520
1521 fn dispatch_focus_key_event(&self, f: &Frame, event: &KeyEvent) -> bool {
1523 let Some(focused) = self.sched.focused else {
1524 return false;
1525 };
1526
1527 let hit_by_id: HashMap<u64, &HitRegion> = f.hit_regions.iter().map(|h| (h.id, h)).collect();
1528 let sem_parent_of: HashMap<u64, u64> = f
1529 .semantics_nodes
1530 .iter()
1531 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1532 .collect();
1533
1534 let mut ancestors = Vec::new();
1535 let mut cur = focused;
1536 loop {
1537 ancestors.push(cur);
1538 if let Some(&p) = sem_parent_of.get(&cur) {
1539 cur = p;
1540 } else {
1541 break;
1542 }
1543 }
1544
1545 for &id in ancestors.iter().rev() {
1546 if let Some(hit) = hit_by_id.get(&id)
1547 && let Some(cb) = &hit.on_preview_key_event
1548 && cb(event.clone())
1549 {
1550 return true;
1551 }
1552 }
1553
1554 for &id in ancestors.iter() {
1555 if let Some(hit) = hit_by_id.get(&id)
1556 && let Some(cb) = &hit.on_key_event
1557 && cb(event.clone())
1558 {
1559 return true;
1560 }
1561 }
1562
1563 false
1564 }
1565
1566 pub fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1570 if let Some(f) = &self.frame_cache
1572 && let Some(fid) = self.sched.focused
1573 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1574 && let Some(cb) = &hit.on_action
1575 && cb(action.clone())
1576 {
1577 request_frame();
1578 return true;
1579 }
1580
1581 if self.apply_text_editing_action(&action) {
1583 return true;
1584 }
1585
1586 if repose_core::shortcuts::handle(action.clone()) {
1588 request_frame();
1589 return true;
1590 }
1591
1592 if let Some(f) = self.frame_cache.clone()
1594 && let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.sched, &f)
1595 {
1596 if let Some(active) = self.key_pressed_active.take() {
1598 self.pressed_ids.remove(&active);
1599 }
1600 self.key_long_press = None;
1601 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == new_id)
1603 && let Some(key) = hit.tf_state_key
1604 {
1605 let st = ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
1606 {
1607 let mut s = st.borrow_mut();
1608 s.apply_controlled_value(&hit.tf_value);
1609 s.reset_caret_blink();
1610 }
1611 }
1612 request_frame();
1613 return true;
1614 }
1615
1616 false
1617 }
1618
1619 fn apply_text_editing_action(&mut self, action: &repose_core::shortcuts::Action) -> bool {
1622 use repose_core::shortcuts::Action;
1623 let Some(fid) = self.sched.focused else {
1624 return false;
1625 };
1626 let Some(f) = self.frame_cache.clone() else {
1627 return false;
1628 };
1629 if !is_tf_hit(&f, fid) {
1630 return false;
1631 }
1632 let key = tf_key_of(&f, fid);
1633 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
1634 return false;
1635 };
1636 let multiline = is_multiline_id(&f, fid);
1637
1638 match action {
1639 Action::Undo => {
1640 let mut st = state_rc.borrow_mut();
1641 if !st.can_undo() {
1642 return false;
1643 }
1644 st.undo();
1645 notify_text_change(&f, fid, st.text.clone());
1646 tf_ensure_caret_visible(&mut st, multiline);
1647 request_frame();
1648 true
1649 }
1650 Action::Redo => {
1651 let mut st = state_rc.borrow_mut();
1652 if !st.can_redo() {
1653 return false;
1654 }
1655 st.redo();
1656 notify_text_change(&f, fid, st.text.clone());
1657 tf_ensure_caret_visible(&mut st, multiline);
1658 request_frame();
1659 true
1660 }
1661 Action::SelectAll => {
1662 let mut st = state_rc.borrow_mut();
1663 let len = st.text.len();
1664 st.selection = 0..len;
1665 request_frame();
1666 true
1667 }
1668 Action::Copy => {
1669 let st = state_rc.borrow();
1670 let (a, b) = (
1671 st.selection.start.min(st.selection.end),
1672 st.selection.start.max(st.selection.end),
1673 );
1674 if a == b {
1675 return false;
1676 }
1677 let slice = st.text.get(a..b).unwrap_or("").to_string();
1678 drop(st);
1679 if !slice.is_empty() {
1680 repose_core::clipboard::copy_to_clipboard(&slice);
1681 }
1682 true
1683 }
1684 Action::Cut => {
1685 if !is_tf_editable(&f, fid) {
1686 return false;
1687 }
1688 let mut st = state_rc.borrow_mut();
1689 let (a, b) = (
1690 st.selection.start.min(st.selection.end),
1691 st.selection.start.max(st.selection.end),
1692 );
1693 if a == b {
1694 return false;
1695 }
1696 let slice = st.text.get(a..b).unwrap_or("").to_string();
1697 st.insert_text_atomic("");
1699 let new_text = st.text.clone();
1700 drop(st);
1701 if !slice.is_empty() {
1702 repose_core::clipboard::copy_to_clipboard(&slice);
1703 }
1704 notify_text_change(&f, fid, new_text);
1705 if let Some(mut st) = self.textfield_states.get(&key).map(|s| s.borrow_mut()) {
1706 tf_ensure_caret_visible(&mut st, multiline);
1707 }
1708 request_frame();
1709 true
1710 }
1711 Action::Paste => {
1712 if let Some(txt) = repose_core::clipboard::paste_text() {
1713 self.paste_into_focused(&txt);
1714 return true;
1715 }
1716 false
1717 }
1718 _ => false,
1719 }
1720 }
1721
1722 pub fn handle_ime(&mut self, event: &ImeEvent) {
1724 let Some(fid) = self.sched.focused else {
1725 return;
1726 };
1727 let Some(f) = &self.frame_cache else {
1728 return;
1729 };
1730 if !is_tf_editable(f, fid) {
1731 return;
1732 }
1733 let key = tf_key_of(f, fid);
1734 let Some(state_rc) = self.textfield_states.get(&key) else {
1735 return;
1736 };
1737
1738 let mut state = state_rc.borrow_mut();
1739
1740 match event {
1741 ImeEvent::Start => {
1742 self.ime_preedit = false;
1743 }
1744 ImeEvent::Update { text, cursor } => {
1745 state.set_composition(text.clone(), *cursor);
1746 self.ime_preedit = !text.is_empty();
1747 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1748 notify_text_change(f, fid, state.text.clone());
1749 }
1750 ImeEvent::Commit(text) => {
1751 state.commit_composition(text.clone());
1752 self.ime_preedit = false;
1753 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1754 notify_text_change(f, fid, state.text.clone());
1755 }
1756 ImeEvent::Cancel => {
1757 self.ime_preedit = false;
1758 if state.composition.is_some() {
1759 state.cancel_composition();
1760 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1761 notify_text_change(f, fid, state.text.clone());
1762 }
1763 }
1764 }
1765
1766 request_frame();
1767 }
1768
1769 pub fn handle_focus_lost(&mut self) {
1771 dnd::handle_drag_action(&DragAction::Cancel);
1772 self.handle_pointer_cancel();
1773 self.ime_preedit = false;
1774 }
1775
1776 pub fn ensure_textfield_state(&mut self, key: u64) -> Rc<RefCell<TextFieldState>> {
1778 self.textfield_states
1779 .entry(key)
1780 .or_insert_with(|| Rc::new(RefCell::new(TextFieldState::new())))
1781 .clone()
1782 }
1783
1784 pub fn ensure_textfield_state_seeded(
1785 &mut self,
1786 key: u64,
1787 seed: &str,
1788 ) -> Rc<RefCell<TextFieldState>> {
1789 ensure_tf_state(&mut self.textfield_states, key, seed)
1790 }
1791
1792 pub fn tf_key_of(&self, visual_id: u64) -> u64 {
1794 self.frame_cache
1795 .as_ref()
1796 .map(|f| tf_key_of(f, visual_id))
1797 .unwrap_or(visual_id)
1798 }
1799
1800 pub fn is_textfield(&self, id: u64) -> bool {
1802 self.frame_cache
1803 .as_ref()
1804 .map(|f| is_textfield_in_frame(f, id))
1805 .unwrap_or(false)
1806 }
1807
1808 pub fn is_multiline(&self, id: u64) -> bool {
1810 self.frame_cache
1811 .as_ref()
1812 .map(|f| is_multiline_id(f, id))
1813 .unwrap_or(false)
1814 }
1815
1816 pub fn focused_keyboard_hints(
1819 &self,
1820 ) -> (
1821 repose_core::ImePurposeHint,
1822 bool,
1823 repose_core::KeyboardCapitalization,
1824 ) {
1825 let defaults = || {
1826 (
1827 repose_core::ImePurposeHint::Normal,
1828 true,
1829 repose_core::KeyboardCapitalization::Unspecified,
1830 )
1831 };
1832 let Some(fid) = self.sched.focused else {
1833 return defaults();
1834 };
1835 let Some(f) = &self.frame_cache else {
1836 return defaults();
1837 };
1838 match f.hit_regions.iter().find(|h| h.id == fid) {
1839 Some(hit) => (
1840 hit.keyboard_type.ime_purpose_hint(),
1841 hit.auto_correct.unwrap_or(true),
1842 hit.capitalization,
1843 ),
1844 None => defaults(),
1845 }
1846 }
1847
1848 pub fn insert_text_into_focused(&mut self, text: &str) -> bool {
1854 if text.is_empty()
1855 || self.ime_preedit
1856 || self.modifiers.ctrl
1857 || self.modifiers.alt
1858 || self.modifiers.meta
1859 {
1860 return false;
1861 }
1862 let Some(fid) = self.sched.focused else {
1863 return false;
1864 };
1865 let Some(f) = self.frame_cache.clone() else {
1866 return false;
1867 };
1868 if !is_textfield_in_frame(&f, fid) {
1869 return false;
1870 }
1871 if !is_tf_editable(&f, fid) {
1872 return false;
1873 }
1874 let key = tf_key_of(&f, fid);
1875 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
1876 return false;
1877 };
1878 let multiline = is_multiline_id(&f, fid);
1879 let filtered: String = text
1880 .chars()
1881 .filter(|c| {
1882 (*c == '\n' && multiline) || (!c.is_control() && *c != '\r')
1886 })
1887 .collect();
1888 if filtered.is_empty() {
1889 return false;
1890 }
1891 {
1892 let mut st = state_rc.borrow_mut();
1893 st.insert_text(&filtered);
1894 let new_text = st.text.clone();
1895 notify_text_change(&f, fid, new_text);
1896 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1897 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1898 }
1899 }
1900 request_frame();
1901 true
1902 }
1903
1904 pub fn insert_text(&mut self, text: &str) -> bool {
1908 self.insert_text_into_focused(text)
1909 }
1910
1911 pub fn paste_into_focused(&mut self, text: &str) {
1914 let Some(fid) = self.sched.focused else {
1915 return;
1916 };
1917 let Some(f) = &self.frame_cache.clone() else {
1918 return;
1919 };
1920 if !is_tf_editable(f, fid) {
1921 return;
1922 }
1923 let key = tf_key_of(f, fid);
1924 if let Some(state_rc) = self.textfield_states.get(&key) {
1925 let mut st = state_rc.borrow_mut();
1926 st.insert_text_atomic(text);
1927 let new_text = st.text.clone();
1928 notify_text_change(f, fid, new_text);
1929 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1930 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1931 }
1932 }
1933 request_frame();
1934 }
1935
1936 pub fn handle_gamepad(&mut self, event: &GamepadEvent) -> bool {
1942 match event {
1943 GamepadEvent::Connected { id, name } => {
1944 self.gamepads.insert(
1945 id.0,
1946 GamepadPad {
1947 name: name.clone(),
1948 ..GamepadPad::default()
1949 },
1950 );
1951 request_frame();
1952 return false;
1953 }
1954 GamepadEvent::Disconnected { id } => {
1955 self.gamepads.remove(&id.0);
1956 request_frame();
1957 return false;
1958 }
1959 GamepadEvent::Button {
1960 id,
1961 button,
1962 pressed,
1963 } => {
1964 if let Some(pad) = self.gamepads.get_mut(&id.0) {
1965 if *pressed {
1966 pad.pressed.insert(*button);
1967 } else {
1968 pad.pressed.remove(button);
1969 }
1970 }
1971 if *pressed {
1972 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
1973 }
1974 let key = match button {
1975 GamepadButton::South => Key::Space,
1976 GamepadButton::East => Key::Escape,
1977 GamepadButton::Start => Key::Enter,
1978 GamepadButton::DPadUp => Key::ArrowUp,
1979 GamepadButton::DPadDown => Key::ArrowDown,
1980 GamepadButton::DPadLeft => Key::ArrowLeft,
1981 GamepadButton::DPadRight => Key::ArrowRight,
1982 _ => return false,
1983 };
1984 let synthetic = KeyEvent {
1985 key,
1986 modifiers: self.modifiers,
1987 is_repeat: false,
1988 event_type: if *pressed {
1989 KeyEventType::Down
1990 } else {
1991 KeyEventType::Up
1992 },
1993 utf16_code_point: 0,
1994 };
1995 return self.handle_key(&synthetic);
1996 }
1997 GamepadEvent::Axis { id, axis, value } => {
1998 if let Some(pad) = self.gamepads.get_mut(&id.0) {
1999 pad.axes.insert(*axis, *value);
2000 }
2001 return false;
2002 }
2003 }
2004 }
2005
2006 pub fn request_rumble(
2011 &mut self,
2012 id: repose_core::input::GamepadId,
2013 low_freq: f32,
2014 high_freq: f32,
2015 duration_ms: u32,
2016 ) {
2017 self.pending_rumble.push((
2018 id.0,
2019 low_freq.clamp(0.0, 1.0),
2020 high_freq.clamp(0.0, 1.0),
2021 duration_ms,
2022 ));
2023 }
2024
2025 pub fn stop_rumble(&mut self, id: repose_core::input::GamepadId) {
2027 self.pending_rumble.push((id.0, 0.0, 0.0, 0));
2028 }
2029
2030 pub fn take_rumble_requests(&mut self) -> Vec<(u32, f32, f32, u32)> {
2032 std::mem::take(&mut self.pending_rumble)
2033 }
2034
2035 pub fn handle_key_with_text(&mut self, event: &KeyEvent, composed_text: Option<&str>) -> bool {
2041 if event.event_type == KeyEventType::Down {
2042 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
2043 }
2044 if event.event_type == KeyEventType::Down
2045 && !event.is_repeat
2046 && !self.ime_preedit
2047 && !self.modifiers.ctrl
2048 && !self.modifiers.alt
2049 && !self.modifiers.meta
2050 && let Some(text) = composed_text
2051 && !text.chars().all(|c| c.is_control())
2052 && self.insert_text_into_focused(text)
2053 {
2054 return true;
2055 }
2056 self.handle_key(event)
2057 }
2058
2059 pub fn handle_scroll_at(
2063 &mut self,
2064 pos: Vec2,
2065 delta: Vec2,
2066 scroll_capture: Option<u64>,
2067 ) -> (bool, Option<u64>) {
2068 let Some(f) = &self.frame_cache else {
2069 return (false, scroll_capture);
2070 };
2071 let (consumed, cap) = dispatch_scroll(f, pos, delta, scroll_capture);
2072 if consumed {
2073 request_frame();
2074 }
2075 (consumed, cap)
2076 }
2077
2078 fn next_caret_blink_deadline(&self) -> Option<web_time::Instant> {
2081 let fid = self.sched.focused?;
2082 let frame = self.frame_cache.as_ref()?;
2083 let hit = frame.hit_regions.iter().find(|h| h.id == fid)?;
2084 let key = hit.tf_state_key?;
2085 self.textfield_states
2086 .get(&key)?
2087 .borrow()
2088 .next_blink_deadline()
2089 }
2090
2091 pub fn next_wakeup_deadline(&self) -> Option<web_time::Instant> {
2094 [
2095 self.next_caret_blink_deadline(),
2096 repose_ui::overlay::SnackbarController::next_deadline(),
2097 repose_core::timer::next_deadline(),
2098 ]
2099 .into_iter()
2100 .flatten()
2101 .min()
2102 }
2103
2104 pub fn is_wakeup_due(&self, now: web_time::Instant) -> bool {
2106 self.next_wakeup_deadline().is_some_and(|d| d <= now)
2107 }
2108
2109 pub fn is_caret_blink_due(&self, now: web_time::Instant) -> bool {
2112 self.is_wakeup_due(now)
2113 }
2114
2115 pub fn next_frame_deadline(
2119 &self,
2120 now: web_time::Instant,
2121 idle_cap: web_time::Duration,
2122 ) -> web_time::Instant {
2123 self.next_wakeup_deadline().unwrap_or(now + idle_cap)
2124 }
2125
2126 pub fn tick_overlays(&self) {
2130 repose_ui::overlay::SnackbarController::tick_all();
2131 repose_core::timer::poll();
2132 }
2133
2134 pub fn cursor_suggestion(&self) -> Option<CursorIcon> {
2136 self.cursor
2137 }
2138
2139 pub fn take_cursor_suggestion(&mut self) -> Option<CursorIcon> {
2141 self.cursor.take()
2142 }
2143}
2144
2145impl Default for ReposeRuntime {
2146 fn default() -> Self {
2147 Self::new()
2148 }
2149}
2150
2151pub fn compose_frame_inner<F>(
2153 sched: &mut Scheduler,
2154 root_fn: &mut F,
2155 scale: f32,
2156 size_px_u32: (u32, u32),
2157 hover_id: Option<u64>,
2158 pressed_ids: &HashSet<u64>,
2159 tf_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
2160) -> Frame
2161where
2162 F: FnMut(&mut Scheduler) -> View,
2163{
2164 compose_frame_inner_with_ancestors(
2165 sched,
2166 root_fn,
2167 scale,
2168 size_px_u32,
2169 hover_id,
2170 &std::collections::HashSet::new(),
2171 pressed_ids,
2172 tf_states,
2173 )
2174}
2175
2176pub fn compose_frame_inner_with_ancestors<F>(
2177 sched: &mut Scheduler,
2178 root_fn: &mut F,
2179 scale: f32,
2180 size_px_u32: (u32, u32),
2181 hover_id: Option<u64>,
2182 hover_ancestors: &std::collections::HashSet<u64>,
2183 pressed_ids: &HashSet<u64>,
2184 tf_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
2185) -> Frame
2186where
2187 F: FnMut(&mut Scheduler) -> View,
2188{
2189 if let Some(requested_id) = take_focus_request() {
2190 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
2191 sched.focused = None;
2192 } else {
2193 sched.focused = Some(requested_id);
2194 }
2195 }
2196
2197 set_density_default(Density { scale });
2198
2199 let current_focused = sched.focused;
2200
2201 let frame = sched.repose(
2202 { move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s)) },
2203 {
2204 let hover_ancestors = hover_ancestors.clone();
2205 let pressed_ids = pressed_ids.clone();
2206 move |view, _size| {
2207 let interactions = Interactions {
2208 hover: hover_id,
2209 hover_ancestors: hover_ancestors.clone(),
2210 pressed: pressed_ids.clone(),
2211 };
2212 with_density(Density { scale }, || {
2213 layout_and_paint(view, size_px_u32, tf_states, &interactions, current_focused)
2214 })
2215 }
2216 },
2217 );
2218
2219 if let Some(fid) = sched.focused
2220 && !frame.focus_chain.contains(&fid)
2221 {
2222 sched.focused = None;
2223 }
2224
2225 frame
2226}
2227
2228fn hover_chain_for(frame: Option<&Frame>, hover: Option<u64>) -> std::collections::HashSet<u64> {
2229 let Some(f) = frame else {
2230 return std::collections::HashSet::new();
2231 };
2232 let Some(mut cur) = hover else {
2233 return std::collections::HashSet::new();
2234 };
2235 let map: std::collections::HashMap<u64, Option<u64>> =
2236 f.hit_regions.iter().map(|h| (h.id, h.parent)).collect();
2237 let mut set = std::collections::HashSet::new();
2238 loop {
2239 set.insert(cur);
2240 if let Some(Some(parent)) = map.get(&cur).copied() {
2241 cur = parent;
2242 } else {
2243 break;
2244 }
2245 }
2246 set
2247}
2248
2249fn dispatch_hover_change_bubbled(
2250 frame: Option<&Frame>,
2251 leave_map: &HashMap<u64, (f32, f32, f32, f32, Rc<dyn Fn(PointerEvent)>)>,
2252 hover_id: &mut Option<u64>,
2253 hover_ancestors: &mut std::collections::HashSet<u64>,
2254 new_hover: Option<u64>,
2255 pos: Vec2,
2256 modifiers: Modifiers,
2257) {
2258 let old_hover = *hover_id;
2259 let old_chain = hover_chain_for(frame, old_hover);
2260 let new_chain = hover_chain_for(frame, new_hover);
2261 if old_chain == new_chain {
2262 return;
2263 }
2264 for leave_id in old_chain.difference(&new_chain) {
2265 let leave_info = leave_map.get(leave_id).cloned().or_else(|| {
2266 frame.and_then(|f| {
2267 f.hit_regions
2268 .iter()
2269 .find(|h| h.id == *leave_id)
2270 .and_then(|h| {
2271 h.on_pointer_leave
2272 .as_ref()
2273 .map(|cb| (h.rect.x, h.rect.y, h.rect.w, h.rect.h, cb.clone()))
2274 })
2275 })
2276 });
2277 if let Some((rx, ry, _rw, _rh, cb)) = leave_info {
2278 let mut pe = PointerEvent::new(
2279 PointerId(0),
2280 PointerKind::Mouse,
2281 PointerEventKind::Leave,
2282 pos,
2283 1.0,
2284 modifiers,
2285 );
2286 pe.origin = Vec2 { x: rx, y: ry };
2287 pe.position = pe.position - pe.origin;
2288 cb(pe);
2289 }
2290 }
2291 for enter_id in new_chain.difference(&old_chain) {
2292 if let Some(f) = frame
2293 && let Some(h) = f.hit_regions.iter().find(|h| h.id == *enter_id)
2294 && let Some(cb) = &h.on_pointer_enter
2295 {
2296 let mut pe = PointerEvent::new(
2297 PointerId(0),
2298 PointerKind::Mouse,
2299 PointerEventKind::Enter,
2300 pos,
2301 1.0,
2302 modifiers,
2303 );
2304 pe.origin = Vec2 {
2305 x: h.rect.x,
2306 y: h.rect.y,
2307 };
2308 pe.position = pe.position - pe.origin;
2309 cb(pe);
2310 }
2311 }
2312 *hover_id = new_hover;
2313 hover_ancestors.clear();
2314 for id in &new_chain {
2315 if Some(*id) != new_hover {
2316 hover_ancestors.insert(*id);
2317 }
2318 }
2319}
2320
2321pub fn is_textfield_in_frame(f: &Frame, id: u64) -> bool {
2322 f.semantics_nodes
2323 .iter()
2324 .any(|n| n.id == id && n.role == repose_core::semantics::Role::TextField)
2325}
2326
2327pub fn is_textfield_in_frame_cache(frame_cache: &Option<Frame>, id: u64) -> bool {
2328 if let Some(f) = frame_cache {
2329 is_textfield_in_frame(f, id)
2330 } else {
2331 false
2332 }
2333}
2334
2335pub fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
2336 frame.hit_regions.iter().position(|h| h.id == id)
2337}
2338
2339fn is_multiline_id(f: &Frame, id: u64) -> bool {
2340 f.hit_regions
2341 .iter()
2342 .find(|h| h.id == id)
2343 .map(|h| h.tf_multiline)
2344 .unwrap_or(false)
2345}
2346
2347fn tf_can_edit(hit: &HitRegion) -> bool {
2350 hit.tf_enabled && !hit.tf_read_only
2351}
2352
2353fn is_tf_editable(f: &Frame, id: u64) -> bool {
2354 f.hit_regions
2355 .iter()
2356 .find(|h| h.id == id)
2357 .is_some_and(tf_can_edit)
2358}
2359
2360fn tf_key_of(frame: &Frame, visual_id: u64) -> u64 {
2361 if let Some(i) = frame.hit_regions.iter().position(|h| h.id == visual_id) {
2362 let hr = &frame.hit_regions[i];
2363 return hr.tf_state_key.unwrap_or(hr.id);
2364 }
2365 visual_id
2366}
2367
2368fn notify_text_change(f: &Frame, id: u64, text: String) {
2369 if let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
2370 && let Some(cb) = &h.on_text_change
2371 {
2372 cb(text);
2373 }
2374}
2375
2376fn tf_ensure_caret_visible(state: &mut TextFieldState, is_multiline: bool) {
2377 let font_px = TF_FONT_SP.to_px().0;
2378 let wrap_width = state.inner_width;
2379
2380 if is_multiline {
2381 let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, wrap_width, state.caret_index());
2382 state.ensure_caret_visible_xy(
2383 cx,
2384 cy,
2385 state.inner_width,
2386 state.inner_height,
2387 Dp(2.0).to_px().0,
2388 );
2389 } else {
2390 let caret_idx = state.caret_index();
2391 let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
2392 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
2393 let tfmd = vt.filter(&annotated);
2394 let off =
2395 repose_core::original_offset_to_display(&state.text, tfmd.text.as_str(), caret_idx);
2396 (tfmd.text.text, off)
2397 } else {
2398 (state.text.clone(), caret_idx)
2399 };
2400 let m = measure_text(&display, font_px, TextMeasureConfig::default());
2401 let caret_x_px = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
2402 state.ensure_caret_visible(caret_x_px, wrap_width, Dp(2.0).to_px().0);
2403 }
2404}
2405
2406fn index_for_x_bytes_vt(state: &TextFieldState, font_px: f32, x_px: f32) -> usize {
2407 if let Some(vt) = &state.visual_transformation {
2408 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
2409 let tfmd = vt.filter(&annotated);
2410 let display_idx =
2411 repose_ui::textfield::index_for_x_bytes(tfmd.text.as_str(), font_px, x_px, 400, 0);
2412 tfmd.offset_mapping.transformed_to_original(display_idx)
2413 } else {
2414 repose_ui::textfield::index_for_x_bytes(&state.text, font_px, x_px, 400, 0)
2415 }
2416}
2417
2418fn index_for_xy_bytes_vt(
2419 state: &TextFieldState,
2420 font_px: f32,
2421 wrap_w: f32,
2422 x_px: f32,
2423 y_px: f32,
2424) -> usize {
2425 if let Some(vt) = &state.visual_transformation {
2426 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
2427 let tfmd = vt.filter(&annotated);
2428 let display_idx = repose_ui::textfield::index_for_xy_bytes(
2429 tfmd.text.as_str(),
2430 font_px,
2431 wrap_w,
2432 x_px,
2433 y_px,
2434 );
2435 tfmd.offset_mapping.transformed_to_original(display_idx)
2436 } else {
2437 repose_ui::textfield::index_for_xy_bytes(&state.text, font_px, wrap_w, x_px, y_px)
2438 }
2439}
2440
2441fn dispatch_scroll(
2443 frame: &Frame,
2444 pos: Vec2,
2445 delta: Vec2,
2446 scroll_capture: Option<u64>,
2447) -> (bool, Option<u64>) {
2448 if let Some(cid) = scroll_capture
2449 && let Some(cb) = frame
2450 .hit_regions
2451 .iter()
2452 .find(|h| h.id == cid)
2453 .and_then(|h| h.on_scroll.as_ref())
2454 {
2455 cb(delta);
2456 return (true, Some(cid));
2457 }
2458 let mut remaining = delta;
2461 for hit in frame
2462 .hit_regions
2463 .iter()
2464 .rev()
2465 .filter(|h| h.rect.contains(pos))
2466 {
2467 if let Some(cb) = &hit.on_scroll {
2468 let before = remaining;
2469 let leftover = cb(before);
2470 let consumed =
2471 (before.x - leftover.x).abs() > 0.001 || (before.y - leftover.y).abs() > 0.001;
2472 if consumed {
2473 return (true, Some(hit.id));
2474 }
2475 remaining = leftover;
2476 if remaining.x.abs() <= 0.001 && remaining.y.abs() <= 0.001 {
2477 break;
2478 }
2479 }
2480 }
2481 (false, scroll_capture)
2482}
2483
2484#[cfg(test)]
2485mod gamepad_tests {
2486 use super::*;
2487 use repose_core::input::{GamepadAxis, GamepadButton, GamepadEvent, GamepadId};
2488
2489 #[test]
2490 fn gamepad_state_mirrors_connection_and_inputs() {
2491 let mut rt = ReposeRuntime::new();
2492 rt.handle_gamepad(&GamepadEvent::Connected {
2493 id: GamepadId(0),
2494 name: "Pad".to_string(),
2495 });
2496 assert_eq!(rt.gamepads.len(), 1);
2497
2498 rt.handle_gamepad(&GamepadEvent::Button {
2499 id: GamepadId(0),
2500 button: GamepadButton::West,
2501 pressed: true,
2502 });
2503 assert!(rt.gamepads[&0].button(GamepadButton::West));
2504
2505 rt.handle_gamepad(&GamepadEvent::Axis {
2506 id: GamepadId(0),
2507 axis: GamepadAxis::LeftStickX,
2508 value: 0.5,
2509 });
2510 assert_eq!(rt.gamepads[&0].axis(GamepadAxis::LeftStickX), 0.5);
2511
2512 rt.handle_gamepad(&GamepadEvent::Button {
2513 id: GamepadId(0),
2514 button: GamepadButton::West,
2515 pressed: false,
2516 });
2517 assert!(!rt.gamepads[&0].button(GamepadButton::West));
2518
2519 rt.handle_gamepad(&GamepadEvent::Disconnected { id: GamepadId(0) });
2520 assert!(rt.gamepads.is_empty());
2521 }
2522
2523 #[test]
2524 fn rumble_requests_queue_and_drain() {
2525 let mut rt = ReposeRuntime::new();
2526 assert!(rt.take_rumble_requests().is_empty());
2527 rt.request_rumble(GamepadId(1), 2.0, -1.0, 150);
2528 rt.stop_rumble(GamepadId(1));
2529 let reqs = rt.take_rumble_requests();
2530 assert_eq!(reqs.len(), 2);
2531 assert_eq!(reqs[0], (1, 1.0, 0.0, 150));
2532 assert_eq!(reqs[1], (1, 0.0, 0.0, 0));
2533 assert!(rt.take_rumble_requests().is_empty());
2534 }
2535}