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